@@ -34,6 +34,7 @@ use crate::{
3434// of escaped double-quotes -- but we believe this should be very rare in
3535// practice.
3636pub const MAX_PAYLOAD_BYTES : usize = 5_000_000 ;
37+ pub ( super ) const DEFAULT_MAX_LOG_BYTES : usize = 1_000_000 ;
3738pub const BATCH_HEADROOM_BYTES : usize = 750_000 ;
3839pub const BATCH_MAX_EVENTS : usize = 1_000 ;
3940pub const BATCH_DEFAULT_TIMEOUT_SECS : f64 = 5.0 ;
@@ -48,6 +49,18 @@ impl SinkBatchSettings for DatadogLogsDefaultBatchSettings {
4849 const TIMEOUT_SECS : f64 = BATCH_DEFAULT_TIMEOUT_SECS ;
4950}
5051
52+ /// Options for truncating logs that exceed Datadog's per-log size limit.
53+ #[ configurable_component]
54+ #[ derive( Clone , Copy , Debug , Derivative ) ]
55+ #[ derivative( Default ) ]
56+ #[ serde( deny_unknown_fields) ]
57+ pub struct DatadogLogsTruncationConfig {
58+ /// Maximum encoded size, in bytes, of a log before truncation is applied.
59+ #[ derivative( Default ( value = "default_max_log_bytes()" ) ) ]
60+ #[ serde( default = "default_max_log_bytes" ) ]
61+ pub max_log_bytes : usize ,
62+ }
63+
5164/// Configuration for the `datadog_logs` sink.
5265#[ configurable_component( sink( "datadog_logs" , "Publish log events to Datadog." ) ) ]
5366#[ derive( Clone , Debug , Derivative ) ]
@@ -81,17 +94,29 @@ pub struct DatadogLogsConfig {
8194 /// to not set it above 5,000,000 (5 MB, the standard Datadog API limit). Increase
8295 /// this when targeting a compatible endpoint that accepts larger payloads. The batch
8396 /// goal is derived as `max_payload_bytes - 750,000` bytes; events larger than the
84- /// batch goal are sent alone in their batch. Events exceeding `max_payload_bytes` are
85- /// dropped.
97+ /// batch goal are sent alone in their batch. Single events that still exceed
98+ /// `max_payload_bytes` after optional truncation are dropped.
8699 #[ derivative( Default ( value = "default_max_payload_bytes()" ) ) ]
87100 #[ serde( default = "default_max_payload_bytes" ) ]
88101 pub max_payload_bytes : Option < usize > ,
102+
103+ /// Attempt to truncate logs whose encoded JSON exceeds `max_log_bytes`.
104+ ///
105+ /// The message is shortened to the largest size that fits and `...TRUNCATED...` is appended.
106+ /// Every reduced log is tagged with `truncated:single_line`. Logs with no string message, or
107+ /// whose non-message fields leave no room for a truncated message, are sent unchanged if they
108+ /// fit `max_payload_bytes`; the Datadog intake can further truncate them.
109+ pub truncate_oversized_logs : Option < DatadogLogsTruncationConfig > ,
89110}
90111
91112const fn default_max_payload_bytes ( ) -> Option < usize > {
92113 Some ( MAX_PAYLOAD_BYTES )
93114}
94115
116+ const fn default_max_log_bytes ( ) -> usize {
117+ DEFAULT_MAX_LOG_BYTES
118+ }
119+
95120const fn default_compression ( ) -> Option < Compression > {
96121 Some ( Compression :: zstd_default ( ) )
97122}
@@ -175,6 +200,7 @@ impl DatadogLogsConfig {
175200 self . max_payload_bytes . unwrap_or ( MAX_PAYLOAD_BYTES ) ,
176201 )
177202 . compression ( self . compression . or_else ( default_compression) . unwrap ( ) )
203+ . truncation ( self . truncate_oversized_logs )
178204 . build ( ) ;
179205
180206 Ok ( VectorSink :: from_event_streamsink ( sink) )
@@ -253,6 +279,19 @@ impl ValidatedSink for DatadogLogsConfig {
253279 . into ( ) ) ;
254280 }
255281
282+ if let Some ( truncation) = self . truncate_oversized_logs {
283+ let max_payload_bytes = self . max_payload_bytes . unwrap_or ( MAX_PAYLOAD_BYTES ) ;
284+ // A single log is wrapped in `[` and `]`, so leave two bytes for the JSON array.
285+ let maximum_log_bytes = max_payload_bytes - 2 ;
286+ if !( 1 ..=maximum_log_bytes) . contains ( & truncation. max_log_bytes ) {
287+ return Err ( format ! (
288+ "truncate_oversized_logs.max_log_bytes ({}) must be between 1 and {}" ,
289+ truncation. max_log_bytes, maximum_log_bytes
290+ )
291+ . into ( ) ) ;
292+ }
293+ }
294+
256295 let batch_goal_bytes =
257296 self . max_payload_bytes . unwrap_or ( MAX_PAYLOAD_BYTES ) - BATCH_HEADROOM_BYTES ;
258297
@@ -309,6 +348,46 @@ mod test {
309348 crate :: test_util:: test_generate_config :: < DatadogLogsConfig > ( ) ;
310349 }
311350
351+ #[ test]
352+ fn truncate_oversized_logs_accepts_empty_configuration ( ) {
353+ let config = serde_yaml:: from_str :: < DatadogLogsConfig > ( indoc:: indoc! { r#"
354+ default_api_key: "test_key"
355+ truncate_oversized_logs: {}
356+ "# } )
357+ . expect ( "truncation configuration should deserialize" ) ;
358+
359+ let truncation = config
360+ . truncate_oversized_logs
361+ . expect ( "truncation should be enabled" ) ;
362+ assert_eq ! ( truncation. max_log_bytes, 1_000_000 ) ;
363+ }
364+
365+ #[ test]
366+ fn validate_rejects_truncation_limit_without_array_wrapper_room ( ) {
367+ let config: DatadogLogsConfig = serde_yaml:: from_str ( indoc:: indoc! { r#"
368+ default_api_key: "test_key"
369+ max_payload_bytes: 800000
370+ truncate_oversized_logs:
371+ max_log_bytes: 799999
372+ "# } )
373+ . unwrap ( ) ;
374+
375+ assert ! ( config. validate( ) . is_err( ) ) ;
376+ }
377+
378+ #[ test]
379+ fn validate_accepts_custom_log_limit_above_datadog_default ( ) {
380+ let config: DatadogLogsConfig = serde_yaml:: from_str ( indoc:: indoc! { r#"
381+ default_api_key: "test_key"
382+ max_payload_bytes: 3000000
383+ truncate_oversized_logs:
384+ max_log_bytes: 2000000
385+ "# } )
386+ . unwrap ( ) ;
387+
388+ assert ! ( config. validate( ) . is_ok( ) ) ;
389+ }
390+
312391 #[ test]
313392 fn validate_produces_usable_batch_settings ( ) {
314393 let config = DatadogLogsConfig :: default ( ) ;
0 commit comments