Skip to main content

libdd_trace_utils/otlp_encoder/
mod.rs

1// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/
2// SPDX-License-Identifier: Apache-2.0
3
4//! OTLP encoder: maps Datadog spans to the prost OTLP types (the IR), then to the HTTP/protobuf
5//! or HTTP/JSON wire format.
6
7pub(crate) mod json_serializer;
8pub mod mapper;
9
10pub use mapper::map_traces_to_otlp;
11
12pub use libdd_trace_protobuf::opentelemetry::proto::collector::trace::v1::ExportTraceServiceRequest as ProtoExportTraceServiceRequest;
13use prost::Message;
14
15/// Serialize the prost OTLP request to the HTTP/protobuf wire format.
16pub fn encode_otlp_protobuf(req: &ProtoExportTraceServiceRequest) -> Vec<u8> {
17    req.encode_to_vec()
18}
19
20/// Serialize the prost OTLP request to the HTTP/JSON wire format (OTLP/JSON spec).
21pub fn encode_otlp_json(req: &ProtoExportTraceServiceRequest) -> serde_json::Result<Vec<u8>> {
22    json_serializer::to_otlp_json_vec(req)
23}
24
25/// Tracer-level attributes used to populate the OTLP Resource on export.
26///
27/// These are the fields from the tracer's configuration that map to OTLP Resource attributes
28/// (service.name, deployment.environment.name, service.version, telemetry.sdk.*, runtime-id).
29/// Callers should build this from their own tracer metadata struct.
30#[derive(Clone, Debug, Default)]
31#[non_exhaustive]
32pub struct OtlpResourceInfo {
33    pub service: String,
34    pub env: String,
35    pub app_version: String,
36    pub language: String,
37    pub tracer_version: String,
38    pub runtime_id: String,
39    pub hostname: String,
40    pub process_tags: String,
41    pub tracer_tags: Vec<String>,
42    pub instrumentation_scope_name: String,
43    pub instrumentation_scope_version: String,
44    /// When true, emits `_dd.stats_computed: "true"` on the OTLP resource to prevent
45    /// double-counted APM metrics in Datadog Agent OTLP receivers (backwards compatible).
46    pub client_computed_stats: bool,
47}
48
49#[cfg(test)]
50mod encode_tests {
51    use super::*;
52    use crate::span::v04::Span;
53    use crate::span::BytesData;
54    use libdd_trace_protobuf::opentelemetry::proto::collector::trace::v1::ExportTraceServiceRequest as ProtoReq;
55    use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value::Value as ProtoValue;
56    use prost::Message;
57
58    fn sample_native() -> (Vec<Vec<Span<BytesData>>>, OtlpResourceInfo) {
59        let resource_info = OtlpResourceInfo {
60            service: "svc".to_string(),
61            ..Default::default()
62        };
63        let mut span: Span<BytesData> = Span {
64            trace_id: 0x5b8efff798038103_d269b633813fc60c_u128,
65            span_id: 0xEEE19B7EC3C1B174,
66            name: libdd_tinybytes::BytesString::from_static("op"),
67            resource: libdd_tinybytes::BytesString::from_static("res"),
68            start: 1,
69            duration: 2,
70            error: 1,
71            ..Default::default()
72        };
73        span.meta.insert(
74            "error.msg".into(),
75            libdd_tinybytes::BytesString::from_static("boom"),
76        );
77        span.meta.insert(
78            "http.method".into(),
79            libdd_tinybytes::BytesString::from_static("GET"),
80        );
81        (vec![vec![span]], resource_info)
82    }
83
84    #[test]
85    fn json_and_protobuf_carry_same_span() {
86        // Decisive guard: JSON and protobuf are encoded from the *same* prost IR, so the two
87        // wire formats cannot drift.
88        let (chunks, info) = sample_native();
89        let req = map_traces_to_otlp(chunks, &info, false);
90        let json = encode_otlp_json(&req).unwrap();
91        let pb = encode_otlp_protobuf(&req);
92
93        let jv: serde_json::Value = serde_json::from_slice(&json).unwrap();
94        let jspan = &jv["resourceSpans"][0]["scopeSpans"][0]["spans"][0];
95        let proto = ProtoReq::decode(pb.as_slice()).unwrap();
96        let pspan = &proto.resource_spans[0].scope_spans[0].spans[0];
97
98        assert_eq!(jspan["name"].as_str().unwrap(), pspan.name);
99        assert_eq!(
100            jspan["spanId"].as_str().unwrap(),
101            hex::encode(&pspan.span_id)
102        );
103        assert_eq!(
104            jspan["traceId"].as_str().unwrap(),
105            hex::encode(&pspan.trace_id)
106        );
107        let pst = pspan.status.as_ref().unwrap();
108        assert_eq!(jspan["status"]["code"].as_i64().unwrap() as i32, pst.code);
109        assert_eq!(jspan["status"]["message"].as_str().unwrap(), pst.message);
110        let jattr = jspan["attributes"]
111            .as_array()
112            .unwrap()
113            .iter()
114            .find(|a| a["key"] == "http.method")
115            .unwrap();
116        let pattr = pspan
117            .attributes
118            .iter()
119            .find(|a| a.key == "http.method")
120            .unwrap();
121        let pval = match pattr.value.as_ref().unwrap().value.as_ref().unwrap() {
122            ProtoValue::StringValue(v) => v.as_str(),
123            other => panic!("expected string, got {other:?}"),
124        };
125        assert_eq!(jattr["value"]["stringValue"].as_str().unwrap(), pval);
126        assert_eq!(jattr["value"]["stringValue"].as_str().unwrap(), "GET");
127    }
128
129    #[test]
130    fn protobuf_round_trips_through_prost() {
131        // Round-trip the IR through the protobuf wire format: decoding the encoded bytes
132        // reproduces the original prost request, i.e. the encoding is lossless. (A JSON round-trip
133        // would need a deserializer mirroring `json_serializer`, which this crate doesn't ship;
134        // `json_and_protobuf_carry_same_span` guards that the JSON matches this same IR.)
135        let (chunks, info) = sample_native();
136        let req = map_traces_to_otlp(chunks, &info, false);
137        let decoded = ProtoReq::decode(encode_otlp_protobuf(&req).as_slice()).unwrap();
138        assert_eq!(decoded, req);
139    }
140}