Skip to content

Commit 2215654

Browse files
committed
feat(datadog_logs): Emit warn logs when misconfigured reserved attributes
1 parent 6649e96 commit 2215654

1 file changed

Lines changed: 216 additions & 3 deletions

File tree

src/sinks/datadog/logs/sink.rs

Lines changed: 216 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::{collections::VecDeque, fmt::Debug, io, sync::Arc};
33
use itertools::Itertools;
44
use snafu::Snafu;
55
use 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)]
221306
pub 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

Comments
 (0)