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