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+
92148static 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 ,
0 commit comments