Skip to content

Commit 8abd42f

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 8abd42f

4 files changed

Lines changed: 331 additions & 54 deletions

File tree

plugins/out_s3/s3.c

Lines changed: 188 additions & 47 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,42 @@ 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+
flb_plg_warn(ctx->ins,
881+
"'compression=parquet' is deprecated. "
882+
"Use 'format parquet' with 'compression' set to "
883+
"the desired page-level codec (snappy, zstd, gzip)");
884+
if (enable_parquet_format(ctx) == -1) {
885+
return -1;
886+
}
812887
}
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;
888+
else {
889+
ret = flb_aws_compression_get_type(tmp);
890+
if (ret == -1) {
891+
flb_plg_error(ctx->ins, "unknown compression: %s", tmp);
892+
return -1;
893+
}
894+
if (ctx->use_put_object == FLB_FALSE &&
895+
ret == FLB_AWS_COMPRESS_ARROW) {
896+
flb_plg_error(ctx->ins,
897+
"use_put_object must be enabled when "
898+
"Apache Arrow is enabled");
899+
return -1;
900+
}
901+
ctx->compression = ret;
902+
903+
if (ctx->s3_format == FLB_S3_FORMAT_PARQUET) {
904+
ctx->parquet_compression = map_to_parquet_codec(ret);
905+
if (ctx->parquet_compression == -1) {
906+
flb_plg_error(ctx->ins,
907+
"'%s' is not a supported parquet page "
908+
"codec. Supported: snappy, zstd, gzip",
909+
tmp);
910+
return -1;
911+
}
912+
ctx->compression = FLB_AWS_COMPRESS_NONE;
913+
}
819914
}
820-
ctx->compression = ret;
821915
}
822916

823917
tmp = flb_output_get_property("content_type", ins);
@@ -1236,6 +1330,7 @@ static int upload_data(struct flb_s3 *ctx, struct s3_file *chunk,
12361330
int size_check = FLB_FALSE;
12371331
int part_num_check = FLB_FALSE;
12381332
int timeout_check = FLB_FALSE;
1333+
int payload_needs_free = FLB_FALSE;
12391334
int ret;
12401335
void *payload_buf = NULL;
12411336
size_t payload_size = 0;
@@ -1252,9 +1347,29 @@ static int upload_data(struct flb_s3 *ctx, struct s3_file *chunk,
12521347
file_first_log_time = chunk->first_log_time;
12531348
}
12541349

1350+
#ifdef FLB_HAVE_ARROW_PARQUET
1351+
if (ctx->s3_format == FLB_S3_FORMAT_PARQUET) {
1352+
ret = out_s3_compress_parquet(body, body_size, &payload_buf,
1353+
&payload_size,
1354+
ctx->parquet_compression);
1355+
if (ret == -1) {
1356+
flb_plg_error(ctx->ins, "Failed to convert data to Parquet");
1357+
if (chunk != NULL) {
1358+
s3_store_file_unlock(chunk);
1359+
chunk->failures += 1;
1360+
}
1361+
return FLB_RETRY;
1362+
}
1363+
preCompress_size = body_size;
1364+
body = (void *) payload_buf;
1365+
body_size = payload_size;
1366+
payload_needs_free = FLB_TRUE;
1367+
}
1368+
else
1369+
#endif
12551370
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);
1371+
ret = flb_aws_compression_compress(ctx->compression, body, body_size,
1372+
&payload_buf, &payload_size);
12581373
if (ret == -1) {
12591374
flb_plg_error(ctx->ins, "Failed to compress data");
12601375
if (chunk != NULL) {
@@ -1267,6 +1382,7 @@ static int upload_data(struct flb_s3 *ctx, struct s3_file *chunk,
12671382
preCompress_size = body_size;
12681383
body = (void *) payload_buf;
12691384
body_size = payload_size;
1385+
payload_needs_free = FLB_TRUE;
12701386
}
12711387
}
12721388

@@ -1322,7 +1438,7 @@ static int upload_data(struct flb_s3 *ctx, struct s3_file *chunk,
13221438
* remove chunk from buffer list
13231439
*/
13241440
ret = s3_put_object(ctx, tag, file_first_log_time, body, body_size);
1325-
if (ctx->compression != FLB_AWS_COMPRESS_NONE) {
1441+
if (payload_needs_free) {
13261442
flb_free(payload_buf);
13271443
}
13281444
if (ret < 0) {
@@ -1349,7 +1465,7 @@ static int upload_data(struct flb_s3 *ctx, struct s3_file *chunk,
13491465
if (chunk) {
13501466
s3_store_file_unlock(chunk);
13511467
}
1352-
if (ctx->compression != FLB_AWS_COMPRESS_NONE) {
1468+
if (payload_needs_free) {
13531469
flb_free(payload_buf);
13541470
}
13551471
return FLB_RETRY;
@@ -1363,7 +1479,7 @@ static int upload_data(struct flb_s3 *ctx, struct s3_file *chunk,
13631479
if (chunk) {
13641480
s3_store_file_unlock(chunk);
13651481
}
1366-
if (ctx->compression != FLB_AWS_COMPRESS_NONE) {
1482+
if (payload_needs_free) {
13671483
flb_free(payload_buf);
13681484
}
13691485
return FLB_RETRY;
@@ -1373,7 +1489,7 @@ static int upload_data(struct flb_s3 *ctx, struct s3_file *chunk,
13731489

13741490
ret = upload_part(ctx, m_upload, body, body_size, NULL);
13751491
if (ret < 0) {
1376-
if (ctx->compression != FLB_AWS_COMPRESS_NONE) {
1492+
if (payload_needs_free) {
13771493
flb_free(payload_buf);
13781494
}
13791495
m_upload->upload_errors += 1;
@@ -1390,7 +1506,7 @@ static int upload_data(struct flb_s3 *ctx, struct s3_file *chunk,
13901506
s3_store_file_delete(ctx, chunk);
13911507
chunk = NULL;
13921508
}
1393-
if (ctx->compression != FLB_AWS_COMPRESS_NONE) {
1509+
if (payload_needs_free) {
13941510
flb_free(payload_buf);
13951511
}
13961512
if (m_upload->bytes >= ctx->file_size) {
@@ -1478,15 +1594,40 @@ static int put_all_chunks(struct flb_s3 *ctx)
14781594
return -1;
14791595
}
14801596

1597+
#ifdef FLB_HAVE_ARROW_PARQUET
1598+
if (ctx->s3_format == FLB_S3_FORMAT_PARQUET) {
1599+
ret = out_s3_compress_parquet(buffer, buffer_size,
1600+
&payload_buf, &payload_size,
1601+
ctx->parquet_compression);
1602+
if (ret == -1) {
1603+
flb_plg_error(ctx->ins,
1604+
"Failed to convert to Parquet, uploading "
1605+
"raw data to prevent data loss");
1606+
}
1607+
else {
1608+
flb_free(buffer);
1609+
buffer = (void *) payload_buf;
1610+
buffer_size = payload_size;
1611+
}
1612+
}
1613+
else
1614+
#endif
14811615
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);
1616+
ret = flb_aws_compression_compress(ctx->compression,
1617+
buffer, buffer_size,
1618+
&payload_buf,
1619+
&payload_size);
14841620
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);
1621+
flb_plg_error(ctx->ins,
1622+
"Failed to compress data, uploading "
1623+
"uncompressed data to prevent data loss");
1624+
}
1625+
else {
1626+
flb_plg_info(ctx->ins,
1627+
"Pre-compression chunk size is %zu, "
1628+
"After compression, chunk is %zu bytes",
1629+
buffer_size, payload_size);
14881630
flb_free(buffer);
1489-
14901631
buffer = (void *) payload_buf;
14911632
buffer_size = payload_size;
14921633
}
@@ -4076,7 +4217,9 @@ static struct flb_config_map config_map[] = {
40764217
{
40774218
FLB_CONFIG_MAP_STR, "format", "json_lines",
40784219
0, FLB_FALSE, 0,
4079-
"Set record output format. Supported values are json_lines, and otlp_json."
4220+
"Set output format. Supported values: json_lines, otlp_json, parquet. "
4221+
"When format is parquet, the 'compression' option controls the page-level "
4222+
"codec inside the Parquet file (snappy, zstd, gzip). Default: uncompressed."
40804223
},
40814224
{
40824225
FLB_CONFIG_MAP_STR, "json_date_format", NULL,
@@ -4147,12 +4290,10 @@ static struct flb_config_map config_map[] = {
41474290
{
41484291
FLB_CONFIG_MAP_STR, "compression", NULL,
41494292
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'."
4293+
"Compression type for S3 objects. Supported values: 'gzip', 'zstd', 'snappy', "
4294+
"'arrow'. When format is 'parquet', this sets the page-level codec inside the "
4295+
"Parquet file. 'compression=parquet' is deprecated; use 'format parquet' instead. "
4296+
"Defaults to no compression."
41564297
},
41574298
{
41584299
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;

src/aws/compression/arrow/compress.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,12 @@ int out_s3_compress_arrow(void *json, size_t size, void **out_buf, size_t *out_s
172172
#ifdef FLB_HAVE_ARROW_PARQUET
173173
#include <fluent-bit/aws/flb_aws_compress.h>
174174

175+
/*
176+
* parquet_compression_to_garrow - map internal codec to GArrow enum
177+
*
178+
* Translates FLB_PARQUET_COMPRESSION_* to GArrowCompressionType for
179+
* use with gparquet_writer_properties_set_compression.
180+
*/
175181
static GArrowCompressionType parquet_compression_to_garrow(int parquet_compression)
176182
{
177183
switch (parquet_compression) {

0 commit comments

Comments
 (0)