Skip to content

Commit 3abff86

Browse files
Handle null span tag values (#1394)
add failing tests convert null to empty string handle null value consistently fixes clippy fmt update comment Co-authored-by: lucaspimentel <[email protected]> Co-authored-by: shreya.malpani <[email protected]>
1 parent 928e65f commit 3abff86

File tree

6 files changed

+113
-4
lines changed

6 files changed

+113
-4
lines changed

libdd-trace-protobuf/build.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ fn generate_protobuf() {
9393
);
9494
config.field_attribute(
9595
".pb.Span.meta",
96-
"#[serde(default)] #[serde(deserialize_with = \"crate::deserializers::deserialize_null_into_default\")]",
96+
"#[serde(default)] #[serde(deserialize_with = \"crate::deserializers::deserialize_map_with_nullable_values\")]",
9797
);
9898
config.field_attribute(
9999
".pb.Span.metrics",

libdd-trace-protobuf/src/deserializers.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
use serde::{Deserialize, Deserializer};
5+
use std::collections::HashMap;
56

67
pub fn deserialize_null_into_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
78
where
@@ -12,6 +13,23 @@ where
1213
Ok(opt.unwrap_or_default())
1314
}
1415

16+
/// Deserialize a HashMap<String, String> where null values are skipped.
17+
pub fn deserialize_map_with_nullable_values<'de, D>(
18+
deserializer: D,
19+
) -> Result<HashMap<String, String>, D::Error>
20+
where
21+
D: Deserializer<'de>,
22+
{
23+
let opt: Option<HashMap<String, Option<String>>> = Option::deserialize(deserializer)?;
24+
match opt {
25+
None => Ok(HashMap::new()),
26+
Some(map) => Ok(map
27+
.into_iter()
28+
.filter_map(|(k, v)| v.map(|val| (k, val)))
29+
.collect()),
30+
}
31+
}
32+
1533
pub fn is_default<T: Default + PartialEq>(t: &T) -> bool {
1634
t == &T::default()
1735
}

libdd-trace-protobuf/src/pb.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,9 @@ pub struct Span {
301301
/// @gotags: json:"meta,omitempty" msg:"meta,omitempty"
302302
#[prost(map = "string, string", tag = "10")]
303303
#[serde(default)]
304-
#[serde(deserialize_with = "crate::deserializers::deserialize_null_into_default")]
304+
#[serde(
305+
deserialize_with = "crate::deserializers::deserialize_map_with_nullable_values"
306+
)]
305307
pub meta: ::std::collections::HashMap<
306308
::prost::alloc::string::String,
307309
::prost::alloc::string::String,

libdd-trace-utils/src/msgpack_decoder/decode/string.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ pub fn read_nullable_string<'a>(buf: &mut &'a [u8]) -> Result<&'a str, DecodeErr
5656
/// # Errors
5757
/// Fails if the buffer does not contain a valid map length prefix,
5858
/// or if any key or value is not a valid utf8 msgpack string.
59+
/// Null values are skipped (key not inserted into map).
5960
#[inline]
6061
pub fn read_str_map_to_strings<'a>(
6162
buf: &mut &'a [u8],
@@ -67,8 +68,11 @@ pub fn read_str_map_to_strings<'a>(
6768
let mut map = HashMap::with_capacity(len.try_into().expect("Unable to cast map len to usize"));
6869
for _ in 0..len {
6970
let key = read_string_ref(buf)?;
70-
let value = read_string_ref(buf)?;
71-
map.insert(key, value);
71+
// Only insert if value is not null
72+
if !handle_null_marker(buf) {
73+
let value = read_string_ref(buf)?;
74+
map.insert(key, value);
75+
}
7276
}
7377
Ok(map)
7478
}
@@ -78,6 +82,7 @@ pub fn read_str_map_to_strings<'a>(
7882
/// # Errors
7983
/// Fails if the buffer does not contain a valid map length prefix,
8084
/// or if any key or value is not a valid utf8 msgpack string.
85+
/// Null values are skipped (key not inserted into map).
8186
#[inline]
8287
pub fn read_nullable_str_map_to_strings<'a>(
8388
buf: &mut &'a [u8],

libdd-trace-utils/src/msgpack_decoder/v04/mod.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,39 @@ mod tests {
492492
assert!(decoded_span.span_links.is_empty());
493493
}
494494

495+
#[test]
496+
fn test_decoder_meta_with_null_values() {
497+
let mut span = create_test_json_span(1, 2, 0, 0, false);
498+
span["meta"] = json!({
499+
"key1": "value1",
500+
"key2": null, // This null value should be skipped
501+
"key3": "value3"
502+
});
503+
504+
let encoded_data = rmp_serde::to_vec_named(&vec![vec![span]]).unwrap();
505+
let (decoded_traces, _) =
506+
from_bytes(libdd_tinybytes::Bytes::from(encoded_data)).expect("Decoding failed");
507+
508+
assert_eq!(1, decoded_traces.len());
509+
assert_eq!(1, decoded_traces[0].len());
510+
let decoded_span = &decoded_traces[0][0];
511+
512+
assert_eq!(
513+
"value1",
514+
decoded_span.meta[&BytesString::from_slice("key1".as_ref()).unwrap()].as_str()
515+
);
516+
assert!(
517+
!decoded_span
518+
.meta
519+
.contains_key(&BytesString::from_slice("key2".as_ref()).unwrap()),
520+
"Null value should be skipped, but key was present"
521+
);
522+
assert_eq!(
523+
"value3",
524+
decoded_span.meta[&BytesString::from_slice("key3".as_ref()).unwrap()].as_str()
525+
);
526+
}
527+
495528
#[test]
496529
fn test_decoder_read_string_wrong_format() {
497530
let span = SpanBytes {

libdd-trace-utils/src/trace_utils.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,4 +1197,55 @@ mod tests {
11971197
1.0
11981198
);
11991199
}
1200+
1201+
#[test]
1202+
fn test_rmp_serde_deserialize_meta_with_null_values() {
1203+
// Create a JSON representation with null value in meta
1204+
let span_json = json!({
1205+
"service": "test-service",
1206+
"name": "test_name",
1207+
"resource": "test-resource",
1208+
"trace_id": 1_u64,
1209+
"span_id": 2_u64,
1210+
"parent_id": 0_u64,
1211+
"start": 0_i64,
1212+
"duration": 5_i64,
1213+
"error": 0_i32,
1214+
"meta": {
1215+
"service": "test-service",
1216+
"env": "test-env",
1217+
"runtime-id": "test-runtime-id-value",
1218+
"problematic_key": null // Ensure this null value does not cause an error
1219+
},
1220+
"metrics": {},
1221+
"type": "",
1222+
"meta_struct": {},
1223+
"span_links": [],
1224+
"span_events": []
1225+
});
1226+
1227+
let traces_json = vec![vec![span_json]];
1228+
let encoded_data = rmp_serde::to_vec(&traces_json).unwrap();
1229+
let traces: Vec<Vec<pb::Span>> = rmp_serde::from_read(&encoded_data[..])
1230+
.expect("Failed to deserialize traces with null values in meta");
1231+
1232+
assert_eq!(1, traces.len());
1233+
assert_eq!(1, traces[0].len());
1234+
let decoded_span = &traces[0][0];
1235+
1236+
assert_eq!("test-service", decoded_span.service);
1237+
assert_eq!("test_name", decoded_span.name);
1238+
assert_eq!("test-resource", decoded_span.resource);
1239+
assert_eq!("test-service", decoded_span.meta.get("service").unwrap());
1240+
assert_eq!("test-env", decoded_span.meta.get("env").unwrap());
1241+
assert_eq!(
1242+
"test-runtime-id-value",
1243+
decoded_span.meta.get("runtime-id").unwrap()
1244+
);
1245+
// Assert that the null value was filtered out (key not present in map)
1246+
assert!(
1247+
!decoded_span.meta.contains_key("problematic_key"),
1248+
"Null value should be skipped, but key was present"
1249+
);
1250+
}
12001251
}

0 commit comments

Comments
 (0)