@@ -3,7 +3,7 @@ use std::{collections::VecDeque, fmt::Debug, io, sync::Arc};
33use itertools:: Itertools ;
44use snafu:: Snafu ;
55use vector_lib:: {
6- event:: { ObjectMap , Value } ,
6+ event:: { LogEvent , ObjectMap , Value } ,
77 internal_event:: { ComponentEventsDropped , UNINTENTIONAL } ,
88 lookup:: event_path,
99} ;
@@ -217,6 +217,91 @@ pub fn path_is_field(path: &OwnedTargetPath, field: &str) -> bool {
217217 && matches ! ( & path. path. segments[ ..] , [ OwnedSegment :: Field ( f) ] if f. as_str( ) == field)
218218}
219219
220+ // Helper function to check if a field exists and is a string
221+ fn is_valid_string_field ( log : & LogEvent , field_name : & str ) -> bool {
222+ let field_path = event_path ! ( field_name) ;
223+ match log. get ( field_path) {
224+ Some ( Value :: Bytes ( _) ) => true ,
225+ _ => false ,
226+ }
227+ }
228+
229+ // Helper function to check if a field is either undefined or a string (no other types allowed)
230+ fn is_string_or_undefined ( log : & LogEvent , field_name : & str ) -> bool {
231+ let field_path = event_path ! ( field_name) ;
232+ match log. get ( field_path) {
233+ None => true , // undefined is OK
234+ Some ( Value :: Bytes ( _) ) => true , // string is OK
235+ _ => false , // any other type is not OK
236+ }
237+ }
238+
239+ // Helper function to check if ddtags field is valid (array of strings or undefined)
240+ fn is_valid_ddtags_field ( log : & LogEvent ) -> bool {
241+ let ddtags_path = event_path ! ( DDTAGS ) ;
242+ match log. get ( ddtags_path) {
243+ None => true , // undefined is OK
244+ Some ( Value :: Array ( arr) ) => {
245+ // Must be array of strings
246+ arr. iter ( ) . all ( |item| matches ! ( item, Value :: Bytes ( _) ) )
247+ }
248+ _ => false , // any other type is not OK
249+ }
250+ }
251+
252+ // Check for missing/invalid reserved attributes and warn about them. This helps ensure proper processing
253+ // by the Datadog logs intake.
254+ pub fn warn_missing_reserved_attributes ( event : & Event ) {
255+ let log = event. as_log ( ) ;
256+ let mut validation_errors = Vec :: new ( ) ;
257+
258+ // service - must be defined and string
259+ if !is_valid_string_field ( log, "service" ) {
260+ validation_errors. push ( "service (must be string)" . to_string ( ) ) ;
261+ }
262+
263+ // ddsource - must be defined and string
264+ if !is_valid_string_field ( log, "ddsource" ) {
265+ validation_errors. push ( "ddsource (must be string)" . to_string ( ) ) ;
266+ }
267+
268+ // host/hostname - at least one must be defined and string, none should be non-string
269+ let host_valid = is_string_or_undefined ( log, "host" ) ;
270+ let hostname_valid = is_string_or_undefined ( log, "hostname" ) ;
271+ let host_exists = log. contains ( event_path ! ( "host" ) ) ;
272+ let hostname_exists = log. contains ( event_path ! ( "hostname" ) ) ;
273+
274+ if !host_valid || !hostname_valid {
275+ validation_errors. push ( "host/hostname (if present, must be string)" . to_string ( ) ) ;
276+ } else if !host_exists && !hostname_exists {
277+ validation_errors. push ( "host/hostname (at least one must be defined)" . to_string ( ) ) ;
278+ }
279+
280+ // status/level - at least one must be defined and string, none should be non-string
281+ let status_valid = is_string_or_undefined ( log, "status" ) ;
282+ let level_valid = is_string_or_undefined ( log, "level" ) ;
283+ let status_exists = log. contains ( event_path ! ( "status" ) ) ;
284+ let level_exists = log. contains ( event_path ! ( "level" ) ) ;
285+
286+ if !status_valid || !level_valid {
287+ validation_errors. push ( "status/level (if present, must be string)" . to_string ( ) ) ;
288+ } else if !status_exists && !level_exists {
289+ validation_errors. push ( "status/level (at least one must be defined)" . to_string ( ) ) ;
290+ }
291+
292+ // ddtags - must be array of strings or undefined
293+ if !is_valid_ddtags_field ( log) {
294+ validation_errors. push ( "ddtags (must be array of strings or undefined)" . to_string ( ) ) ;
295+ }
296+
297+ if !validation_errors. is_empty ( ) {
298+ warn ! (
299+ message = "Invalid reserved attributes for optimal Datadog logs intake processing." ,
300+ validation_errors = ?validation_errors,
301+ ) ;
302+ }
303+ }
304+
220305#[ derive( Debug , Snafu ) ]
221306pub enum RequestBuildError {
222307 #[ snafu( display( "Encoded payload is greater than the max limit." ) ) ]
@@ -260,6 +345,8 @@ impl LogRequestBuilder {
260345 if self . conforms_as_agent {
261346 normalize_as_agent_event ( & mut event) ;
262347 }
348+ // Check for missing reserved attributes and warn if any are missing
349+ warn_missing_reserved_attributes ( & event) ;
263350 self . transformer . transform ( & mut event) ;
264351 let estimated_json_size = event. estimated_json_encoded_size_of ( ) ;
265352 ( event, estimated_json_size)
@@ -447,8 +534,11 @@ mod tests {
447534 value:: { Kind , kind:: Collection } ,
448535 } ;
449536
450- use super :: { normalize_as_agent_event, normalize_event} ;
451- use crate :: common:: datadog:: DD_RESERVED_SEMANTIC_ATTRS ;
537+ use super :: {
538+ is_valid_ddtags_field, is_valid_string_field, is_string_or_undefined,
539+ normalize_as_agent_event, normalize_event, warn_missing_reserved_attributes,
540+ } ;
541+ use crate :: common:: datadog:: { DDTAGS , DD_RESERVED_SEMANTIC_ATTRS } ;
452542
453543 fn assert_normalized_log_has_expected_attrs ( log : & LogEvent ) {
454544 assert ! (
@@ -738,4 +828,127 @@ mod tests {
738828 } ) )
739829 ) ;
740830 }
831+
832+ #[ test]
833+ fn test_is_valid_string_field ( ) {
834+ let mut log = LogEvent :: default ( ) ;
835+ log. insert ( event_path ! ( "string_field" ) , "test-value" ) ;
836+ log. insert ( event_path ! ( "integer_field" ) , 123 ) ;
837+ log. insert ( event_path ! ( "array_field" ) , vec ! [ "item1" , "item2" ] ) ;
838+
839+ assert ! ( is_valid_string_field( & log, "string_field" ) ) ;
840+ assert ! ( !is_valid_string_field( & log, "integer_field" ) ) ;
841+ assert ! ( !is_valid_string_field( & log, "array_field" ) ) ;
842+ assert ! ( !is_valid_string_field( & log, "missing_field" ) ) ;
843+ }
844+
845+ #[ test]
846+ fn test_is_string_or_undefined ( ) {
847+ let mut log = LogEvent :: default ( ) ;
848+ log. insert ( event_path ! ( "string_field" ) , "test-value" ) ;
849+ log. insert ( event_path ! ( "integer_field" ) , 123 ) ;
850+
851+ assert ! ( is_string_or_undefined( & log, "string_field" ) ) ;
852+ assert ! ( !is_string_or_undefined( & log, "integer_field" ) ) ;
853+ assert ! ( is_string_or_undefined( & log, "missing_field" ) ) ;
854+ }
855+
856+ #[ test]
857+ fn test_is_valid_ddtags_field ( ) {
858+ let mut log1 = LogEvent :: default ( ) ;
859+ // undefined ddtags is valid
860+ assert ! ( is_valid_ddtags_field( & log1) ) ;
861+
862+ let mut log2 = LogEvent :: default ( ) ;
863+ log2. insert ( event_path ! ( DDTAGS ) , vec ! [ "tag1:value1" , "tag2:value2" ] ) ;
864+ assert ! ( is_valid_ddtags_field( & log2) ) ;
865+
866+ let mut log3 = LogEvent :: default ( ) ;
867+ log3. insert ( event_path ! ( DDTAGS ) , "invalid-string" ) ;
868+ assert ! ( !is_valid_ddtags_field( & log3) ) ;
869+
870+ let mut log4 = LogEvent :: default ( ) ;
871+ log4. insert ( event_path ! ( DDTAGS ) , vec ! [ "tag1:value1" , 123 ] ) ;
872+ assert ! ( !is_valid_ddtags_field( & log4) ) ;
873+ }
874+
875+ #[ test]
876+ fn warn_valid_complete_event ( ) {
877+ // Test with a complete valid event
878+ let mut log = LogEvent :: default ( ) ;
879+ log. insert ( event_path ! ( "message" ) , "test message" ) ;
880+ log. insert ( event_path ! ( "timestamp" ) , 1234567890 ) ; // timestamp has no constraints
881+ log. insert ( event_path ! ( "hostname" ) , "test-host" ) ;
882+ log. insert ( event_path ! ( "service" ) , "test-service" ) ;
883+ log. insert ( event_path ! ( "ddsource" ) , "test-source" ) ;
884+ log. insert ( event_path ! ( DDTAGS ) , vec ! [ "env:test" , "service:my-service" ] ) ;
885+ log. insert ( event_path ! ( "status" ) , "info" ) ;
886+
887+ let event = Event :: Log ( log) ;
888+
889+ // This should not produce any warnings
890+ warn_missing_reserved_attributes ( & event) ;
891+ }
892+
893+ #[ test]
894+ fn warn_valid_event_with_fallbacks ( ) {
895+ // Test with valid event using fallback fields
896+ let mut log = LogEvent :: default ( ) ;
897+ log. insert ( event_path ! ( "host" ) , "test-host" ) ; // fallback for hostname
898+ log. insert ( event_path ! ( "level" ) , "info" ) ; // fallback for status
899+ log. insert ( event_path ! ( "service" ) , "test-service" ) ;
900+ log. insert ( event_path ! ( "ddsource" ) , "test-source" ) ;
901+ // ddtags is undefined, which is valid
902+
903+ let event = Event :: Log ( log) ;
904+
905+ // This should not produce any warnings
906+ warn_missing_reserved_attributes ( & event) ;
907+ }
908+
909+ #[ test]
910+ fn warn_invalid_types ( ) {
911+ // Test with invalid field types
912+ let mut log = LogEvent :: default ( ) ;
913+ log. insert ( event_path ! ( "hostname" ) , "test-host" ) ; // valid
914+ log. insert ( event_path ! ( "service" ) , 123 ) ; // invalid type
915+ log. insert ( event_path ! ( "ddsource" ) , true ) ; // invalid type
916+ log. insert ( event_path ! ( "status" ) , "info" ) ; // valid
917+ log. insert ( event_path ! ( DDTAGS ) , "invalid-string" ) ; // should be array
918+
919+ let event = Event :: Log ( log) ;
920+
921+ // This should produce warnings for service, ddsource, and ddtags
922+ warn_missing_reserved_attributes ( & event) ;
923+ }
924+
925+ #[ test]
926+ fn warn_missing_required_fields ( ) {
927+ // Test with missing required fields
928+ let mut log = LogEvent :: default ( ) ;
929+ log. insert ( event_path ! ( "timestamp" ) , 1234567890 ) ;
930+ // Missing: service, ddsource, hostname/host, status/level
931+
932+ let event = Event :: Log ( log) ;
933+
934+ // This should produce warnings for all missing required fields
935+ warn_missing_reserved_attributes ( & event) ;
936+ }
937+
938+ #[ test]
939+ fn warn_mixed_valid_invalid_fallbacks ( ) {
940+ // Test with mix of valid and invalid fallback scenarios
941+ let mut log = LogEvent :: default ( ) ;
942+ log. insert ( event_path ! ( "host" ) , 123 ) ; // invalid type
943+ log. insert ( event_path ! ( "hostname" ) , "test-host" ) ; // valid
944+ log. insert ( event_path ! ( "status" ) , "info" ) ; // valid
945+ // missing level is OK since status is present
946+ log. insert ( event_path ! ( "service" ) , "test-service" ) ; // valid
947+ log. insert ( event_path ! ( "ddsource" ) , "test-source" ) ; // valid
948+
949+ let event = Event :: Log ( log) ;
950+
951+ // This should warn about host field having wrong type
952+ warn_missing_reserved_attributes ( & event) ;
953+ }
741954}
0 commit comments