Skip to content

Commit daf983a

Browse files
Rituparna KhaundRituparna Khaund
authored andcommitted
out_s3: add format=parquet option with page-level compression
The compression config option conflated byte-level compression (gzip, zstd, snappy) with format conversion (parquet), making it impossible to produce Parquet files with page-level compression. This adds 'format parquet' as a new option. When format is parquet, the compression option (snappy, zstd, gzip) controls the page-level codec inside the Parquet file. Default is uncompressed to preserve existing behavior. The old 'compression=parquet' syntax is preserved as a deprecated alias that emits a warning and maps to format=parquet with no page-level compression (identical to current behavior). Arrow support is untouched and continues to work via 'compression=arrow' as before. Signed-off-by: Rituparna Khaund <ritukhau@amazon.co.uk>
1 parent 69b3dd9 commit daf983a

4 files changed

Lines changed: 338 additions & 55 deletions

File tree

plugins/out_s3/s3.c

Lines changed: 195 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242

4343
#include "s3.h"
4444
#include "s3_store.h"
45+
#include <fluent-bit/aws/flb_aws_compress.h>
4546

4647
#define DEFAULT_S3_PORT 443
4748
#define DEFAULT_S3_INSECURE_PORT 80
@@ -89,6 +90,61 @@ static flb_sds_t s3_format_event_chunk(struct flb_s3 *ctx,
8990
struct flb_event_chunk *event_chunk,
9091
struct flb_config *config);
9192

93+
/*
94+
* enable_parquet_format - configure context for Parquet output
95+
*
96+
* Sets the S3 format to Parquet and forces PutObject mode.
97+
* Returns 0 on success, -1 if Parquet support was not compiled in.
98+
*/
99+
static int enable_parquet_format(struct flb_s3 *ctx)
100+
{
101+
#ifdef FLB_HAVE_ARROW_PARQUET
102+
ctx->s3_format = FLB_S3_FORMAT_PARQUET;
103+
ctx->use_put_object = FLB_TRUE;
104+
return 0;
105+
#else
106+
flb_plg_error(ctx->ins,
107+
"parquet format requires parquet-glib at compile time");
108+
return -1;
109+
#endif
110+
}
111+
112+
/*
113+
* parse_output_format - resolve format string to format constant
114+
*
115+
* Returns FLB_S3_FORMAT_PARQUET for "parquet", otherwise delegates
116+
* to flb_pack_to_json_format_type for JSON format types.
117+
*/
118+
static int parse_output_format(const char *format)
119+
{
120+
if (strcasecmp(format, "parquet") == 0) {
121+
return FLB_S3_FORMAT_PARQUET;
122+
}
123+
return flb_pack_to_json_format_type(format);
124+
}
125+
126+
/*
127+
* map_to_parquet_codec - map AWS compression type to Parquet page codec
128+
*
129+
* Translates FLB_AWS_COMPRESS_* values to the corresponding
130+
* FLB_PARQUET_COMPRESSION_* constant. Returns -1 for unsupported codecs.
131+
*/
132+
static int map_to_parquet_codec(int compression_type)
133+
{
134+
switch (compression_type) {
135+
case FLB_AWS_COMPRESS_NONE:
136+
return FLB_PARQUET_COMPRESSION_NONE;
137+
case FLB_AWS_COMPRESS_SNAPPY:
138+
return FLB_PARQUET_COMPRESSION_SNAPPY;
139+
case FLB_AWS_COMPRESS_ZSTD:
140+
return FLB_PARQUET_COMPRESSION_ZSTD;
141+
case FLB_AWS_COMPRESS_GZIP:
142+
return FLB_PARQUET_COMPRESSION_GZIP;
143+
default:
144+
return -1;
145+
}
146+
}
147+
92148
static struct flb_aws_header *get_content_encoding_header(int compression_type)
93149
{
94150
static struct flb_aws_header gzip_header = {
@@ -655,6 +711,8 @@ static int cb_s3_init(struct flb_output_instance *ins,
655711
ctx->retry_time = 0;
656712
ctx->upload_queue_success = FLB_FALSE;
657713
ctx->out_format = FLB_PACK_JSON_FORMAT_LINES;
714+
ctx->s3_format = FLB_S3_FORMAT_JSON_LINES;
715+
ctx->parquet_compression = FLB_PARQUET_COMPRESSION_NONE;
658716

659717
/*
660718
* The engine default retry_limit (1) is too low for S3's internal
@@ -693,30 +751,43 @@ static int cb_s3_init(struct flb_output_instance *ins,
693751
/* Format key */
694752
tmp = flb_output_get_property("format", ins);
695753
if (tmp) {
696-
ret = flb_pack_to_json_format_type(tmp);
754+
ret = parse_output_format(tmp);
697755
if (ret == -1) {
698756
flb_plg_error(ctx->ins, "invalid format '%s'", tmp);
699757
return -1;
700758
}
701759

702-
if (ret == FLB_PACK_JSON_FORMAT_JSON) {
703-
flb_plg_warn(ctx->ins,
704-
"'json' format is implicitly interpreted as 'json_lines' before."
705-
"Now interpreted as 'json_lines' explicitly now");
706-
ret = FLB_PACK_JSON_FORMAT_LINES;
760+
if (ret == FLB_S3_FORMAT_PARQUET) {
761+
if (ctx->log_key != NULL) {
762+
flb_plg_error(ctx->ins,
763+
"'log_key' is not supported when format is "
764+
"parquet");
765+
return -1;
766+
}
767+
if (enable_parquet_format(ctx) == -1) {
768+
return -1;
769+
}
707770
}
708-
else if (ret != FLB_PACK_JSON_FORMAT_LINES &&
709-
ret != FLB_PACK_JSON_FORMAT_OTLP) {
710-
flb_plg_error(ctx->ins, "unsupported format '%s'", tmp);
711-
return -1;
771+
else if (ret == FLB_PACK_JSON_FORMAT_JSON) {
772+
flb_plg_warn(ctx->ins,
773+
"'json' format is implicitly interpreted as "
774+
"'json_lines'. Now interpreted as 'json_lines' "
775+
"explicitly");
776+
ctx->out_format = FLB_PACK_JSON_FORMAT_LINES;
712777
}
713-
ctx->out_format = ret;
778+
else if (ret == FLB_PACK_JSON_FORMAT_LINES ||
779+
ret == FLB_PACK_JSON_FORMAT_OTLP) {
780+
ctx->out_format = ret;
714781

715-
if (ctx->out_format == FLB_PACK_JSON_FORMAT_OTLP &&
716-
ctx->log_key != NULL) {
717-
flb_plg_error(ctx->ins,
718-
"'log_key' is not supported when format is "
719-
"otlp_json or otlp_json_pretty");
782+
if (ret == FLB_PACK_JSON_FORMAT_OTLP && ctx->log_key != NULL) {
783+
flb_plg_error(ctx->ins,
784+
"'log_key' is not supported when format is "
785+
"otlp_json or otlp_json_pretty");
786+
return -1;
787+
}
788+
}
789+
else {
790+
flb_plg_error(ctx->ins, "unsupported format '%s'", tmp);
720791
return -1;
721792
}
722793
}
@@ -805,19 +876,48 @@ static int cb_s3_init(struct flb_output_instance *ins,
805876

806877
tmp = flb_output_get_property("compression", ins);
807878
if (tmp) {
808-
ret = flb_aws_compression_get_type(tmp);
809-
if (ret == -1) {
810-
flb_plg_error(ctx->ins, "unknown compression: %s", tmp);
811-
return -1;
879+
if (strcasecmp(tmp, "parquet") == 0) {
880+
if (ctx->log_key != NULL) {
881+
flb_plg_error(ctx->ins,
882+
"'log_key' is not supported when format is "
883+
"parquet");
884+
return -1;
885+
}
886+
flb_plg_warn(ctx->ins,
887+
"'compression=parquet' is deprecated. "
888+
"Use 'format parquet' with 'compression' set to "
889+
"the desired page-level codec (snappy, zstd, gzip)");
890+
if (enable_parquet_format(ctx) == -1) {
891+
return -1;
892+
}
812893
}
813-
if (ctx->use_put_object == FLB_FALSE &&
814-
(ret == FLB_AWS_COMPRESS_ARROW ||
815-
ret == FLB_AWS_COMPRESS_PARQUET)) {
816-
flb_plg_error(ctx->ins,
817-
"use_put_object must be enabled when Apache Arrow or Parquet is enabled");
818-
return -1;
894+
else {
895+
ret = flb_aws_compression_get_type(tmp);
896+
if (ret == -1) {
897+
flb_plg_error(ctx->ins, "unknown compression: %s", tmp);
898+
return -1;
899+
}
900+
if (ctx->use_put_object == FLB_FALSE &&
901+
ret == FLB_AWS_COMPRESS_ARROW) {
902+
flb_plg_error(ctx->ins,
903+
"use_put_object must be enabled when "
904+
"Apache Arrow is enabled");
905+
return -1;
906+
}
907+
ctx->compression = ret;
908+
909+
if (ctx->s3_format == FLB_S3_FORMAT_PARQUET) {
910+
ctx->parquet_compression = map_to_parquet_codec(ret);
911+
if (ctx->parquet_compression == -1) {
912+
flb_plg_error(ctx->ins,
913+
"'%s' is not a supported parquet page "
914+
"codec. Supported: snappy, zstd, gzip",
915+
tmp);
916+
return -1;
917+
}
918+
ctx->compression = FLB_AWS_COMPRESS_NONE;
919+
}
819920
}
820-
ctx->compression = ret;
821921
}
822922

823923
tmp = flb_output_get_property("content_type", ins);
@@ -834,7 +934,7 @@ static int cb_s3_init(struct flb_output_instance *ins,
834934
"total_file_size is less than 10 MB, will use PutObject API");
835935
ctx->use_put_object = FLB_TRUE;
836936
}
837-
937+
838938
if (ctx->use_put_object == FLB_FALSE) {
839939
/* upload_chunk_size */
840940
if (ctx->upload_chunk_size <= 0) {
@@ -1236,6 +1336,7 @@ static int upload_data(struct flb_s3 *ctx, struct s3_file *chunk,
12361336
int size_check = FLB_FALSE;
12371337
int part_num_check = FLB_FALSE;
12381338
int timeout_check = FLB_FALSE;
1339+
int payload_needs_free = FLB_FALSE;
12391340
int ret;
12401341
void *payload_buf = NULL;
12411342
size_t payload_size = 0;
@@ -1252,9 +1353,29 @@ static int upload_data(struct flb_s3 *ctx, struct s3_file *chunk,
12521353
file_first_log_time = chunk->first_log_time;
12531354
}
12541355

1356+
#ifdef FLB_HAVE_ARROW_PARQUET
1357+
if (ctx->s3_format == FLB_S3_FORMAT_PARQUET) {
1358+
ret = out_s3_compress_parquet(body, body_size, &payload_buf,
1359+
&payload_size,
1360+
ctx->parquet_compression);
1361+
if (ret == -1) {
1362+
flb_plg_error(ctx->ins, "Failed to convert data to Parquet");
1363+
if (chunk != NULL) {
1364+
s3_store_file_unlock(chunk);
1365+
chunk->failures += 1;
1366+
}
1367+
return FLB_RETRY;
1368+
}
1369+
preCompress_size = body_size;
1370+
body = (void *) payload_buf;
1371+
body_size = payload_size;
1372+
payload_needs_free = FLB_TRUE;
1373+
}
1374+
else
1375+
#endif
12551376
if (ctx->compression != FLB_AWS_COMPRESS_NONE) {
1256-
/* Map payload */
1257-
ret = flb_aws_compression_compress(ctx->compression, body, body_size, &payload_buf, &payload_size);
1377+
ret = flb_aws_compression_compress(ctx->compression, body, body_size,
1378+
&payload_buf, &payload_size);
12581379
if (ret == -1) {
12591380
flb_plg_error(ctx->ins, "Failed to compress data");
12601381
if (chunk != NULL) {
@@ -1267,6 +1388,7 @@ static int upload_data(struct flb_s3 *ctx, struct s3_file *chunk,
12671388
preCompress_size = body_size;
12681389
body = (void *) payload_buf;
12691390
body_size = payload_size;
1391+
payload_needs_free = FLB_TRUE;
12701392
}
12711393
}
12721394

@@ -1322,7 +1444,7 @@ static int upload_data(struct flb_s3 *ctx, struct s3_file *chunk,
13221444
* remove chunk from buffer list
13231445
*/
13241446
ret = s3_put_object(ctx, tag, file_first_log_time, body, body_size);
1325-
if (ctx->compression != FLB_AWS_COMPRESS_NONE) {
1447+
if (payload_needs_free) {
13261448
flb_free(payload_buf);
13271449
}
13281450
if (ret < 0) {
@@ -1349,7 +1471,7 @@ static int upload_data(struct flb_s3 *ctx, struct s3_file *chunk,
13491471
if (chunk) {
13501472
s3_store_file_unlock(chunk);
13511473
}
1352-
if (ctx->compression != FLB_AWS_COMPRESS_NONE) {
1474+
if (payload_needs_free) {
13531475
flb_free(payload_buf);
13541476
}
13551477
return FLB_RETRY;
@@ -1363,7 +1485,7 @@ static int upload_data(struct flb_s3 *ctx, struct s3_file *chunk,
13631485
if (chunk) {
13641486
s3_store_file_unlock(chunk);
13651487
}
1366-
if (ctx->compression != FLB_AWS_COMPRESS_NONE) {
1488+
if (payload_needs_free) {
13671489
flb_free(payload_buf);
13681490
}
13691491
return FLB_RETRY;
@@ -1373,7 +1495,7 @@ static int upload_data(struct flb_s3 *ctx, struct s3_file *chunk,
13731495

13741496
ret = upload_part(ctx, m_upload, body, body_size, NULL);
13751497
if (ret < 0) {
1376-
if (ctx->compression != FLB_AWS_COMPRESS_NONE) {
1498+
if (payload_needs_free) {
13771499
flb_free(payload_buf);
13781500
}
13791501
m_upload->upload_errors += 1;
@@ -1390,7 +1512,7 @@ static int upload_data(struct flb_s3 *ctx, struct s3_file *chunk,
13901512
s3_store_file_delete(ctx, chunk);
13911513
chunk = NULL;
13921514
}
1393-
if (ctx->compression != FLB_AWS_COMPRESS_NONE) {
1515+
if (payload_needs_free) {
13941516
flb_free(payload_buf);
13951517
}
13961518
if (m_upload->bytes >= ctx->file_size) {
@@ -1478,15 +1600,40 @@ static int put_all_chunks(struct flb_s3 *ctx)
14781600
return -1;
14791601
}
14801602

1603+
#ifdef FLB_HAVE_ARROW_PARQUET
1604+
if (ctx->s3_format == FLB_S3_FORMAT_PARQUET) {
1605+
ret = out_s3_compress_parquet(buffer, buffer_size,
1606+
&payload_buf, &payload_size,
1607+
ctx->parquet_compression);
1608+
if (ret == -1) {
1609+
flb_plg_error(ctx->ins,
1610+
"Failed to convert to Parquet, uploading "
1611+
"raw data to prevent data loss");
1612+
}
1613+
else {
1614+
flb_free(buffer);
1615+
buffer = (void *) payload_buf;
1616+
buffer_size = payload_size;
1617+
}
1618+
}
1619+
else
1620+
#endif
14811621
if (ctx->compression != FLB_AWS_COMPRESS_NONE) {
1482-
/* Map payload */
1483-
ret = flb_aws_compression_compress(ctx->compression, buffer, buffer_size, &payload_buf, &payload_size);
1622+
ret = flb_aws_compression_compress(ctx->compression,
1623+
buffer, buffer_size,
1624+
&payload_buf,
1625+
&payload_size);
14841626
if (ret == -1) {
1485-
flb_plg_error(ctx->ins, "Failed to compress data, uploading uncompressed data instead to prevent data loss");
1486-
} else {
1487-
flb_plg_info(ctx->ins, "Pre-compression chunk size is %zu, After compression, chunk is %zu bytes", buffer_size, payload_size);
1627+
flb_plg_error(ctx->ins,
1628+
"Failed to compress data, uploading "
1629+
"uncompressed data to prevent data loss");
1630+
}
1631+
else {
1632+
flb_plg_info(ctx->ins,
1633+
"Pre-compression chunk size is %zu, "
1634+
"After compression, chunk is %zu bytes",
1635+
buffer_size, payload_size);
14881636
flb_free(buffer);
1489-
14901637
buffer = (void *) payload_buf;
14911638
buffer_size = payload_size;
14921639
}
@@ -4076,7 +4223,9 @@ static struct flb_config_map config_map[] = {
40764223
{
40774224
FLB_CONFIG_MAP_STR, "format", "json_lines",
40784225
0, FLB_FALSE, 0,
4079-
"Set record output format. Supported values are json_lines, and otlp_json."
4226+
"Set output format. Supported values: json_lines, otlp_json, parquet. "
4227+
"When format is parquet, the 'compression' option controls the page-level "
4228+
"codec inside the Parquet file (snappy, zstd, gzip). Default: uncompressed."
40804229
},
40814230
{
40824231
FLB_CONFIG_MAP_STR, "json_date_format", NULL,
@@ -4147,12 +4296,10 @@ static struct flb_config_map config_map[] = {
41474296
{
41484297
FLB_CONFIG_MAP_STR, "compression", NULL,
41494298
0, FLB_FALSE, 0,
4150-
"Compression type for S3 objects. Supported values: 'gzip', 'zstd', 'snappy'. "
4151-
"'arrow' and 'parquet' are also available if Apache Arrow was enabled at compile time. "
4152-
"Defaults to no compression. "
4153-
"If 'gzip' is selected, the Content-Encoding HTTP Header will be set to 'gzip'. "
4154-
"If 'zstd' is selected, the Content-Encoding HTTP Header will be set to 'zstd'. "
4155-
"If 'snappy' is selected, the Content-Encoding HTTP Header will be set to 'snappy'."
4299+
"Compression type for S3 objects. Supported values: 'gzip', 'zstd', 'snappy', "
4300+
"'arrow'. When format is 'parquet', this sets the page-level codec inside the "
4301+
"Parquet file. 'compression=parquet' is deprecated; use 'format parquet' instead. "
4302+
"Defaults to no compression."
41564303
},
41574304
{
41584305
FLB_CONFIG_MAP_STR, "content_type", NULL,

plugins/out_s3/s3.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727
#include <fluent-bit/flb_aws_util.h>
2828
#include <fluent-bit/flb_blob_db.h>
2929

30+
/* S3 output format types */
31+
#define FLB_S3_FORMAT_JSON_LINES 0
32+
#define FLB_S3_FORMAT_PARQUET 100
33+
3034
/* Upload data to S3 in 5MB chunks */
3135
#define MIN_CHUNKED_UPLOAD_SIZE 5242880
3236
#define MAX_CHUNKED_UPLOAD_SIZE 50000000
@@ -123,6 +127,8 @@ struct flb_s3 {
123127
int send_content_md5;
124128
int static_file_path;
125129
int compression;
130+
int s3_format;
131+
int parquet_compression;
126132
int port;
127133
int insecure;
128134
size_t store_dir_limit_size;

0 commit comments

Comments
 (0)