Skip to main content

libdd_trace_utils/span/v05/
mod.rs

1// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/
2// SPDX-License-Identifier: Apache-2.0
3
4pub mod dict;
5
6use crate::span::v04::{AttributeAnyValue, AttributeArrayValue, SpanEvent, SpanLink};
7use crate::span::{SharedDictBytes, SpanText, TraceData, SPAN_LINK_FLAGS_SET_SENTINEL};
8use anyhow::Result;
9use indexmap::map::RawEntryApiV1;
10use libdd_tinybytes::BytesString;
11use serde::ser::{SerializeMap, SerializeSeq};
12use serde::{Serialize, Serializer};
13use std::borrow::Borrow;
14use std::collections::HashMap;
15
16/// Structure that represent a TraceChunk Span which String fields are interned in a shared
17/// dictionary. The number of elements is fixed by the spec and they all need to be serialized, in
18/// case of adding more items the constant msgpack_decoder::v05::SPAN_ELEM_COUNT need to be
19/// updated.
20#[derive(Clone, Debug, Default, PartialEq, Serialize)]
21pub struct Span {
22    pub service: u32,
23    pub name: u32,
24    pub resource: u32,
25    pub trace_id: u64,
26    pub span_id: u64,
27    pub parent_id: u64,
28    pub start: i64,
29    pub duration: i64,
30    pub error: i32,
31    pub meta: HashMap<u32, u32>,
32    pub metrics: HashMap<u32, f64>,
33    pub r#type: u32,
34}
35
36/// Serializes a slice of [`SpanLink`]s into the JSON array the Datadog agent and backend
37/// expect under the `_dd.span_links` meta key.
38///
39/// This matches the agent's `transform.MarshalLinks`
40/// (datadog-agent `pkg/trace/transform/transform.go`), the canonical producer of
41/// `_dd.span_links`:
42/// - `trace_id` is the full 128-bit id hex-encoded as 32 lowercase chars (high 64 bits first, then
43///   low 64 bits).
44/// - `span_id` is the 64-bit id hex-encoded as 16 lowercase chars.
45/// - `tracestate` and `attributes` are only emitted when non-empty.
46/// - `flags` is only emitted when not zero, with [`SPAN_LINK_FLAGS_SET_SENTINEL`] masked off.
47struct SpanLinksSerializerV05<'a, T: TraceData>(&'a [SpanLink<T>]);
48struct SpanLinkSerializerV05<'a, T: TraceData>(&'a SpanLink<T>);
49
50impl<'a, T: TraceData> Serialize for SpanLinksSerializerV05<'a, T> {
51    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
52        let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
53        for link in self.0 {
54            seq.serialize_element(&SpanLinkSerializerV05::<T>(link))?;
55        }
56        seq.end()
57    }
58}
59
60impl<'a, T: TraceData> Serialize for SpanLinkSerializerV05<'a, T> {
61    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
62        let link = self.0;
63        let tracestate: &str = link.tracestate.borrow();
64        let has_tracestate = !tracestate.is_empty();
65        let has_attributes = !link.attributes.is_empty();
66        let has_flags = link.flags != 0;
67        let len = 2 + has_tracestate as usize + has_attributes as usize + has_flags as usize;
68        let mut map = serializer.serialize_map(Some(len))?;
69        map.serialize_entry(
70            "trace_id",
71            &format!("{:016x}{:016x}", link.trace_id_high, link.trace_id),
72        )?;
73        map.serialize_entry("span_id", &format!("{:016x}", link.span_id))?;
74        if has_tracestate {
75            map.serialize_entry("tracestate", &link.tracestate)?;
76        }
77        if has_attributes {
78            map.serialize_entry(
79                "attributes",
80                &SortedStrMapSerializerV05::<T>(&link.attributes),
81            )?;
82        }
83        if has_flags {
84            map.serialize_entry("flags", &(link.flags & !SPAN_LINK_FLAGS_SET_SENTINEL))?;
85        }
86        map.end()
87    }
88}
89
90/// Serializes a `HashMap<T::Text, T::Text>` as a JSON object with keys in sorted order,
91/// keeping the output deterministic for snapshot testing
92struct SortedStrMapSerializerV05<'a, T: TraceData>(&'a HashMap<T::Text, T::Text>);
93
94impl<'a, T: TraceData> Serialize for SortedStrMapSerializerV05<'a, T> {
95    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
96        let mut entries: Vec<(&str, &T::Text)> =
97            self.0.iter().map(|(k, v)| (k.borrow(), v)).collect();
98        entries.sort_unstable_by_key(|(k, _)| *k);
99        let mut map = serializer.serialize_map(Some(entries.len()))?;
100        for (key, value) in entries {
101            map.serialize_entry(key, value)?;
102        }
103        map.end()
104    }
105}
106
107/// Serializes a slice of [`SpanEvent`]s into the JSON array the Datadog agent and backend
108/// expect under the `events` meta key, matching the agent's `MarshalEvents`
109/// (`{time_unix_nano, name, attributes}` with attributes rendered as natural JSON rather than
110/// the v0.4 msgpack tagged form).
111struct SpanEventsSerializerV05<'a, T: TraceData>(&'a [SpanEvent<T>]);
112struct SpanEventSerializerV05<'a, T: TraceData>(&'a SpanEvent<T>);
113struct SpanEventAttributesSerializerV05<'a, T: TraceData>(
114    &'a HashMap<T::Text, AttributeAnyValue<T>>,
115);
116struct AttributeAnyValueV05<'a, T: TraceData>(&'a AttributeAnyValue<T>);
117struct AttributeArrayValueV05<'a, T: TraceData>(&'a AttributeArrayValue<T>);
118
119impl<'a, T: TraceData> Serialize for SpanEventsSerializerV05<'a, T> {
120    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
121        let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
122        for event in self.0 {
123            seq.serialize_element(&SpanEventSerializerV05::<T>(event))?;
124        }
125        seq.end()
126    }
127}
128
129impl<'a, T: TraceData> Serialize for SpanEventSerializerV05<'a, T> {
130    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
131        let event = self.0;
132        let mut map = serializer.serialize_map(Some(3))?;
133        map.serialize_entry("time_unix_nano", &event.time_unix_nano)?;
134        map.serialize_entry("name", &event.name)?;
135        map.serialize_entry(
136            "attributes",
137            &SpanEventAttributesSerializerV05::<T>(&event.attributes),
138        )?;
139        map.end()
140    }
141}
142
143impl<'a, T: TraceData> Serialize for SpanEventAttributesSerializerV05<'a, T> {
144    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
145        // Sort keys to match Go's `encoding/json` (used by the agent) and keep output
146        // deterministic, since the source is an unordered `HashMap`.
147        let mut entries: Vec<(&str, &AttributeAnyValue<T>)> =
148            self.0.iter().map(|(k, v)| (k.borrow(), v)).collect();
149        entries.sort_unstable_by_key(|(k, _)| *k);
150        let mut map = serializer.serialize_map(Some(entries.len()))?;
151        for (key, value) in entries {
152            map.serialize_entry(key, &AttributeAnyValueV05::<T>(value))?;
153        }
154        map.end()
155    }
156}
157
158impl<'a, T: TraceData> Serialize for AttributeAnyValueV05<'a, T> {
159    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
160        match self.0 {
161            AttributeAnyValue::SingleValue(value) => {
162                AttributeArrayValueV05::<T>(value).serialize(serializer)
163            }
164            AttributeAnyValue::Array(values) => {
165                let mut seq = serializer.serialize_seq(Some(values.len()))?;
166                for value in values {
167                    seq.serialize_element(&AttributeArrayValueV05::<T>(value))?;
168                }
169                seq.end()
170            }
171        }
172    }
173}
174
175impl<'a, T: TraceData> Serialize for AttributeArrayValueV05<'a, T> {
176    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
177        match self.0 {
178            AttributeArrayValue::String(value) => value.serialize(serializer),
179            AttributeArrayValue::Boolean(value) => serializer.serialize_bool(*value),
180            AttributeArrayValue::Integer(value) => serializer.serialize_i64(*value),
181            AttributeArrayValue::Double(value) => serializer.serialize_f64(*value),
182        }
183    }
184}
185
186/// Gets the index of the interned string. If the string is not part of the dictionary it is
187/// added and its corresponding index returned.
188///
189/// Checks if the span text is already interned before creating a
190/// new ByteString instance from it.
191fn get_or_insert(
192    dict: &mut SharedDictBytes,
193    str: &impl SpanText,
194) -> Result<u32, std::num::TryFromIntError> {
195    let entry = dict.map.raw_entry_mut_v1().from_key(str.borrow());
196    let idx = entry.index();
197    entry.or_insert_with(|| (str.to_bytes_string(), ()));
198    idx.try_into()
199}
200
201/// Converts a v0.4 [`Span`](crate::span::v04::Span) into its v0.5 dictionary-encoded form.
202///
203/// The v0.5 format is a fixed 12-element positional array (service, name, resource, trace_id,
204/// span_id, parent_id, start, duration, error, meta, metrics, type). It predates `span_links`,
205/// `span_events`, and `meta_struct`, none of which have a dedicated slot.
206///
207/// `span_links` and `span_events` are carried in `meta` as JSON strings under the
208/// `_dd.span_links` and `events` keys, matching the shapes the Datadog agent/backend understand
209/// (the agent's `MarshalLinks` / `MarshalEvents`). Both are only emitted when non-empty.
210///
211/// `meta_struct` is intentionally dropped: it carries arbitrary binary (msgpack) blobs, the
212/// v0.5 `meta` map is string->string only, and there is no agent-side meta-key convention for
213/// reconstructing it from a v0.5 payload. Callers that must preserve `meta_struct` should use
214/// the v0.4 output format.
215///
216/// Carrying links/events requires interning dynamically-built JSON strings, so the shared
217/// dictionary always owns its strings ([`SharedDictBytes`]). Borrowed input text is copied into
218/// the dictionary; owned text is reference-counted.
219pub fn from_v04_span<T: TraceData>(
220    span: crate::span::v04::Span<T>,
221    dict: &mut SharedDictBytes,
222) -> Result<Span> {
223    let meta_len = span.meta.len();
224    let metrics_len = span.metrics.len();
225
226    // Serialize span links / span events before `span` is consumed below. v0.5 has no
227    // dedicated slots for them, so they are flattened into `meta` as JSON strings.
228    let serialized_span_links = if span.span_links.is_empty() {
229        None
230    } else {
231        Some(serde_json::to_string(&SpanLinksSerializerV05::<T>(
232            &span.span_links,
233        ))?)
234    };
235    let serialized_span_events = if span.span_events.is_empty() {
236        None
237    } else {
238        Some(serde_json::to_string(&SpanEventsSerializerV05::<T>(
239            &span.span_events,
240        ))?)
241    };
242
243    let extra_meta =
244        serialized_span_links.is_some() as usize + serialized_span_events.is_some() as usize;
245
246    // Intern fields in the same order as the base conversion to keep dictionary indices
247    // stable; the span links / events keys are appended to `meta` afterwards.
248    let service = get_or_insert(dict, &span.service)?;
249    let name = get_or_insert(dict, &span.name)?;
250    let resource = get_or_insert(dict, &span.resource)?;
251    let mut meta = span.meta.into_iter().try_fold(
252        HashMap::with_capacity(meta_len + extra_meta),
253        |mut meta, (k, v)| -> anyhow::Result<HashMap<u32, u32>> {
254            meta.insert(get_or_insert(dict, &k)?, get_or_insert(dict, &v)?);
255            Ok(meta)
256        },
257    )?;
258
259    if let Some(links_json) = serialized_span_links {
260        let key = dict.get_or_insert(BytesString::from_static("_dd.span_links"))?;
261        let value = dict.get_or_insert(BytesString::from(links_json))?;
262        meta.insert(key, value);
263    }
264    if let Some(events_json) = serialized_span_events {
265        let key = dict.get_or_insert(BytesString::from_static("events"))?;
266        let value = dict.get_or_insert(BytesString::from(events_json))?;
267        meta.insert(key, value);
268    }
269
270    let metrics = span.metrics.into_iter().try_fold(
271        HashMap::with_capacity(metrics_len),
272        |mut metrics, (k, v)| -> anyhow::Result<HashMap<u32, f64>> {
273            metrics.insert(get_or_insert(dict, &k)?, v);
274            Ok(metrics)
275        },
276    )?;
277    let r#type = get_or_insert(dict, &span.r#type)?;
278
279    Ok(Span {
280        service,
281        name,
282        resource,
283        trace_id: span.trace_id as u64,
284        span_id: span.span_id,
285        parent_id: span.parent_id,
286        start: span.start,
287        duration: span.duration,
288        error: span.error,
289        meta,
290        metrics,
291        r#type,
292    })
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298    use crate::span::v04::{SpanBytes, VecMap};
299    use crate::span::BytesData;
300    use libdd_tinybytes::BytesString;
301
302    /// Returns the JSON string interned in `meta` under `key`, if present.
303    fn meta_json(dict: &SharedDictBytes, span: &Span, key: &str) -> Option<String> {
304        let entries: Vec<&str> = dict.iter().map(|s| s.as_str()).collect();
305        let key_idx = entries.iter().position(|s| *s == key)? as u32;
306        let val_idx = *span.meta.get(&key_idx)?;
307        Some(entries[val_idx as usize].to_string())
308    }
309
310    #[test]
311    fn from_span_bytes_test() {
312        let span = SpanBytes {
313            service: BytesString::from("service"),
314            name: BytesString::from("name"),
315            resource: BytesString::from("resource"),
316            r#type: BytesString::from("type"),
317            trace_id: 1,
318            span_id: 1,
319            parent_id: 0,
320            start: 1,
321            duration: 111,
322            error: 0,
323            meta: vec![(
324                BytesString::from("meta_field"),
325                BytesString::from("meta_value"),
326            )]
327            .into(),
328            metrics: vec![(BytesString::from("metrics_field"), 1.1)].into(),
329            meta_struct: VecMap::new(),
330            span_links: vec![],
331            span_events: vec![],
332        };
333
334        let mut dict = SharedDictBytes::default();
335        let v05_span = from_v04_span(span, &mut dict).unwrap();
336
337        let get_index_from_str = |str: &str| -> u32 {
338            dict.iter()
339                .position(|s| s.as_str() == str)
340                .unwrap()
341                .try_into()
342                .unwrap()
343        };
344
345        assert_eq!(v05_span.service, get_index_from_str("service"));
346        assert_eq!(v05_span.name, get_index_from_str("name"));
347        assert_eq!(v05_span.resource, get_index_from_str("resource"));
348        assert_eq!(v05_span.r#type, get_index_from_str("type"));
349        assert_eq!(v05_span.trace_id, 1);
350        assert_eq!(v05_span.span_id, 1);
351        assert_eq!(v05_span.parent_id, 0);
352        assert_eq!(v05_span.start, 1);
353        assert_eq!(v05_span.duration, 111);
354        assert_eq!(v05_span.error, 0);
355        assert_eq!(v05_span.meta.len(), 1);
356        assert_eq!(v05_span.metrics.len(), 1);
357
358        assert_eq!(
359            *v05_span
360                .meta
361                .get(&get_index_from_str("meta_field"))
362                .unwrap(),
363            get_index_from_str("meta_value")
364        );
365        assert_eq!(
366            *v05_span
367                .metrics
368                .get(&get_index_from_str("metrics_field"))
369                .unwrap(),
370            1.1
371        );
372    }
373
374    fn base_span() -> SpanBytes {
375        SpanBytes {
376            service: BytesString::from("service"),
377            name: BytesString::from("name"),
378            resource: BytesString::from("resource"),
379            r#type: BytesString::from("type"),
380            trace_id: 1,
381            span_id: 1,
382            parent_id: 0,
383            start: 1,
384            duration: 111,
385            error: 0,
386            meta: vec![(
387                BytesString::from("meta_field"),
388                BytesString::from("meta_value"),
389            )]
390            .into(),
391            metrics: VecMap::new(),
392            meta_struct: VecMap::new(),
393            span_links: vec![],
394            span_events: vec![],
395        }
396    }
397
398    /// Span links and span events are flattened into `meta` as agent-compatible JSON under
399    /// the `_dd.span_links` and `events` keys.
400    #[test]
401    fn from_v04_span_serializes_links_and_events_test() {
402        let mut span = base_span();
403        span.span_links = vec![SpanLink::<BytesData> {
404            trace_id: 12345,
405            trace_id_high: 67890,
406            span_id: 54321,
407            attributes: HashMap::from([(BytesString::from("key"), BytesString::from("val"))]),
408            tracestate: BytesString::from("tracestate_value"),
409            flags: 1,
410        }];
411        span.span_events = vec![SpanEvent::<BytesData> {
412            time_unix_nano: 123,
413            name: BytesString::from("ev1"),
414            attributes: HashMap::from([(
415                BytesString::from("str_attr"),
416                AttributeAnyValue::SingleValue(AttributeArrayValue::String(BytesString::from(
417                    "val",
418                ))),
419            )]),
420        }];
421
422        let mut dict = SharedDictBytes::default();
423        let v05_span = from_v04_span(span, &mut dict).unwrap();
424
425        let links_json = meta_json(&dict, &v05_span, "_dd.span_links").unwrap();
426        assert_eq!(
427            links_json,
428            "[{\"trace_id\":\"00000000000109320000000000003039\",\"span_id\":\"000000000000d431\",\"tracestate\":\"tracestate_value\",\"attributes\":{\"key\":\"val\"},\"flags\":1}]"
429        );
430        let events_json = meta_json(&dict, &v05_span, "events").unwrap();
431        assert_eq!(
432            events_json,
433            "[{\"time_unix_nano\":123,\"name\":\"ev1\",\"attributes\":{\"str_attr\":\"val\"}}]"
434        );
435        // Original meta entry plus the two synthesized keys.
436        assert_eq!(v05_span.meta.len(), 3);
437    }
438
439    /// Empty links/events add no meta keys (matches the agent, which only writes them when
440    /// non-empty).
441    #[test]
442    fn from_v04_span_empty_links_events_no_meta_keys_test() {
443        let mut dict = SharedDictBytes::default();
444        let v05_span = from_v04_span(base_span(), &mut dict).unwrap();
445        assert_eq!(v05_span.meta.len(), 1);
446        assert!(meta_json(&dict, &v05_span, "_dd.span_links").is_none());
447        assert!(meta_json(&dict, &v05_span, "events").is_none());
448    }
449
450    /// `meta_struct` has no v0.5 representation and must be dropped; conversion still succeeds
451    /// and produces no extra meta keys.
452    #[test]
453    fn from_v04_span_drops_meta_struct_test() {
454        let mut span = base_span();
455        span.meta_struct = vec![(
456            BytesString::from("appsec"),
457            libdd_tinybytes::Bytes::from_static(&[0x01, 0x02, 0x03]),
458        )]
459        .into();
460
461        let mut dict = SharedDictBytes::default();
462        let v05_span = from_v04_span(span, &mut dict).unwrap();
463        assert_eq!(v05_span.meta.len(), 1);
464        assert!(meta_json(&dict, &v05_span, "appsec").is_none());
465        assert!(meta_json(&dict, &v05_span, "meta_struct").is_none());
466    }
467
468    /// A link with no tracestate and no attributes serializes only hex `trace_id`/`span_id`,
469    /// plus `flags` since it is non-zero.
470    #[test]
471    fn span_link_minimal_serialization_test() {
472        let links = vec![SpanLink::<BytesData> {
473            trace_id: 0xdead_beef,
474            trace_id_high: 0,
475            span_id: 0xfeed,
476            attributes: HashMap::new(),
477            tracestate: BytesString::from(""),
478            flags: 7,
479        }];
480        let json = serde_json::to_string(&SpanLinksSerializerV05::<BytesData>(&links)).unwrap();
481        assert_eq!(
482            json,
483            "[{\"trace_id\":\"000000000000000000000000deadbeef\",\"span_id\":\"000000000000feed\",\"flags\":7}]"
484        );
485    }
486
487    /// Covers all three [`SPAN_LINK_FLAGS_SET_SENTINEL`] states: unset, kept, and explicitly
488    /// dropped.
489    #[test]
490    fn span_link_flags_sentinel_bit_masked_test() {
491        let kept = vec![SpanLink::<BytesData> {
492            span_id: 1,
493            flags: 0x8000_0001,
494            ..Default::default()
495        }];
496        let json = serde_json::to_string(&SpanLinksSerializerV05::<BytesData>(&kept)).unwrap();
497        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
498        assert_eq!(parsed[0]["flags"], serde_json::json!(1));
499
500        let dropped = vec![SpanLink::<BytesData> {
501            span_id: 2,
502            flags: 0x8000_0000,
503            ..Default::default()
504        }];
505        let json = serde_json::to_string(&SpanLinksSerializerV05::<BytesData>(&dropped)).unwrap();
506        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
507        assert_eq!(parsed[0]["flags"], serde_json::json!(0));
508
509        let unset = vec![SpanLink::<BytesData> {
510            span_id: 3,
511            flags: 0,
512            ..Default::default()
513        }];
514        let json = serde_json::to_string(&SpanLinksSerializerV05::<BytesData>(&unset)).unwrap();
515        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
516        assert!(parsed[0].get("flags").is_none());
517    }
518
519    /// Multiple links serialize as an ordered JSON array preserving input order.
520    #[test]
521    fn span_links_multiple_serialization_test() {
522        let links = vec![
523            SpanLink::<BytesData> {
524                span_id: 0x22,
525                ..Default::default()
526            },
527            SpanLink::<BytesData> {
528                span_id: 0x44,
529                ..Default::default()
530            },
531        ];
532        let json = serde_json::to_string(&SpanLinksSerializerV05::<BytesData>(&links)).unwrap();
533        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
534        assert_eq!(parsed.as_array().unwrap().len(), 2);
535        assert_eq!(parsed[0]["span_id"], serde_json::json!("0000000000000022"));
536        assert_eq!(parsed[1]["span_id"], serde_json::json!("0000000000000044"));
537    }
538
539    /// A link with tracestate but no attributes emits `tracestate`, omits `attributes`.
540    #[test]
541    fn span_link_only_tracestate_serialization_test() {
542        let links = vec![SpanLink::<BytesData> {
543            span_id: 2,
544            tracestate: BytesString::from("ts"),
545            ..Default::default()
546        }];
547        let json = serde_json::to_string(&SpanLinksSerializerV05::<BytesData>(&links)).unwrap();
548        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
549        assert_eq!(parsed[0]["tracestate"], serde_json::json!("ts"));
550        assert!(parsed[0].get("attributes").is_none());
551    }
552
553    /// A link with attributes but no tracestate emits `attributes`, omits `tracestate`.
554    #[test]
555    fn span_link_only_attributes_serialization_test() {
556        let links = vec![SpanLink::<BytesData> {
557            span_id: 2,
558            attributes: HashMap::from([(BytesString::from("k"), BytesString::from("v"))]),
559            ..Default::default()
560        }];
561        let json = serde_json::to_string(&SpanLinksSerializerV05::<BytesData>(&links)).unwrap();
562        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
563        assert_eq!(parsed[0]["attributes"]["k"], serde_json::json!("v"));
564        assert!(parsed[0].get("tracestate").is_none());
565    }
566
567    /// Event attributes of every scalar type render as natural JSON.
568    #[test]
569    fn span_event_attribute_types_serialization_test() {
570        let events = vec![SpanEvent::<BytesData> {
571            time_unix_nano: 42,
572            name: BytesString::from("ev"),
573            attributes: HashMap::from([
574                (
575                    BytesString::from("int_attr"),
576                    AttributeAnyValue::SingleValue(AttributeArrayValue::Integer(-7)),
577                ),
578                (
579                    BytesString::from("dbl_attr"),
580                    AttributeAnyValue::SingleValue(AttributeArrayValue::Double(2.5)),
581                ),
582                (
583                    BytesString::from("bool_attr"),
584                    AttributeAnyValue::SingleValue(AttributeArrayValue::Boolean(true)),
585                ),
586            ]),
587        }];
588        let json = serde_json::to_string(&SpanEventsSerializerV05::<BytesData>(&events)).unwrap();
589        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
590        let attrs = &parsed[0]["attributes"];
591        assert_eq!(attrs["int_attr"], serde_json::json!(-7));
592        assert_eq!(attrs["dbl_attr"], serde_json::json!(2.5));
593        assert_eq!(attrs["bool_attr"], serde_json::json!(true));
594        assert_eq!(parsed[0]["time_unix_nano"], serde_json::json!(42));
595        assert_eq!(parsed[0]["name"], serde_json::json!("ev"));
596    }
597
598    /// Arrays of non-string scalars serialize as natural JSON arrays.
599    #[test]
600    fn span_event_non_string_array_serialization_test() {
601        let events = vec![SpanEvent::<BytesData> {
602            time_unix_nano: 1,
603            name: BytesString::from("ev"),
604            attributes: HashMap::from([(
605                BytesString::from("arr"),
606                AttributeAnyValue::Array(vec![
607                    AttributeArrayValue::Integer(1),
608                    AttributeArrayValue::Boolean(true),
609                    AttributeArrayValue::Double(3.5),
610                ]),
611            )]),
612        }];
613        let json = serde_json::to_string(&SpanEventsSerializerV05::<BytesData>(&events)).unwrap();
614        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
615        assert_eq!(
616            parsed[0]["attributes"]["arr"],
617            serde_json::json!([1, true, 3.5])
618        );
619    }
620
621    /// Event and link attributes are emitted with keys in sorted order (matching Go's
622    /// `encoding/json`, used by the agent) so the output is deterministic despite the
623    /// `HashMap` source.
624    #[test]
625    fn attributes_serialized_in_sorted_key_order_test() {
626        let events = vec![SpanEvent::<BytesData> {
627            time_unix_nano: 1,
628            name: BytesString::from("ev"),
629            attributes: HashMap::from([
630                (
631                    BytesString::from("zebra"),
632                    AttributeAnyValue::SingleValue(AttributeArrayValue::Integer(1)),
633                ),
634                (
635                    BytesString::from("alpha"),
636                    AttributeAnyValue::SingleValue(AttributeArrayValue::Integer(2)),
637                ),
638                (
639                    BytesString::from("mike"),
640                    AttributeAnyValue::SingleValue(AttributeArrayValue::Integer(3)),
641                ),
642            ]),
643        }];
644        let json = serde_json::to_string(&SpanEventsSerializerV05::<BytesData>(&events)).unwrap();
645        assert_eq!(
646            json,
647            "[{\"time_unix_nano\":1,\"name\":\"ev\",\"attributes\":{\"alpha\":2,\"mike\":3,\"zebra\":1}}]"
648        );
649
650        let links = vec![SpanLink::<BytesData> {
651            span_id: 1,
652            attributes: HashMap::from([
653                (BytesString::from("zzz"), BytesString::from("1")),
654                (BytesString::from("aaa"), BytesString::from("2")),
655            ]),
656            ..Default::default()
657        }];
658        let json = serde_json::to_string(&SpanLinksSerializerV05::<BytesData>(&links)).unwrap();
659        assert!(
660            json.contains("\"attributes\":{\"aaa\":\"2\",\"zzz\":\"1\"}"),
661            "link attributes not sorted: {json}"
662        );
663    }
664
665    /// An event with an empty attributes map renders `"attributes":{}`.
666    #[test]
667    fn span_event_empty_attributes_serialization_test() {
668        let events = vec![SpanEvent::<BytesData> {
669            time_unix_nano: 1,
670            name: BytesString::from("ev"),
671            attributes: HashMap::new(),
672        }];
673        let json = serde_json::to_string(&SpanEventsSerializerV05::<BytesData>(&events)).unwrap();
674        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
675        assert_eq!(parsed[0]["attributes"], serde_json::json!({}));
676    }
677}