opentelemetry_datadog_cloudflare/
lib.rs

1//! # `OpenTelemetry` Datadog Exporter for Cloudflare
2//!
3//! An `OpenTelemetry` datadog exporter implementation for Cloudflare
4//!
5//! ## Quirks
6//!
7//! There are currently some incompatibilities between Datadog and `OpenTelemetry`, and this manifests
8//! as minor quirks to this exporter.
9//!
10//! Firstly Datadog uses `operation_name` to describe what `OpenTracing` would call a component.
11//! Or to put it another way, in `OpenTracing` the operation / span name's are relatively
12//! granular and might be used to identify a specific endpoint. In datadog, however, they
13//! are less granular - it is expected in Datadog that a service will have single
14//! primary span name that is the root of all traces within that service, with an additional piece of
15//! metadata called `resource_name` providing granularity. See [here](https://docs.datadoghq.com/tracing/guide/configuring-primary-operation/)
16//!
17//! The Datadog Golang API takes the approach of using a `resource.name` `OpenTelemetry` attribute to set the
18//! `resource_name`. See [here](https://github.com/DataDog/dd-trace-go/blob/ecb0b805ef25b00888a2fb62d465a5aa95e7301e/ddtrace/opentracer/tracer.go#L10)
19//!
20//! Unfortunately, this breaks compatibility with other `OpenTelemetry` exporters which expect
21//! a more granular operation name - as per the `OpenTracing` specification.
22//!
23//! This exporter therefore takes a different approach of naming the span with the name of the
24//! tracing provider, and using the span name to set the `resource_name`. This should in most cases
25//! lead to the behaviour that users expect.
26//!
27//! Datadog additionally has a `span_type` string that alters the rendering of the spans in the web UI.
28//! This can be set as the `span.type` `OpenTelemetry` span attribute.
29//!
30//! For standard values see [here](https://github.com/DataDog/dd-trace-go/blob/ecb0b805ef25b00888a2fb62d465a5aa95e7301e/ddtrace/ext/app_types.go#L31)
31//!
32//! ## Bring your own http client
33//!
34//! Users can choose appropriate http clients to align with their runtime.
35//!
36//! Based on the feature enabled. The only client available is reqwest, feel free to implement
37//! other http clients.
38//!
39//! Note that async http clients may need specific runtime otherwise it will panic. User should make
40//! sure the http client is running in appropriate runime.
41//!
42//! Users can always use their own http clients by implementing `HttpClient` trait.
43
44#![deny(unused_crate_dependencies)]
45
46pub(crate) mod dd_proto {
47    include!(concat!(env!("OUT_DIR"), "/dd_trace.rs"));
48}
49
50mod exporter;
51
52mod propagator {
53    use opentelemetry::{
54        propagation::{text_map_propagator::FieldIter, Extractor, Injector, TextMapPropagator},
55        trace::{SpanContext, SpanId, TraceContextExt, TraceFlags, TraceId, TraceState},
56        Context,
57    };
58
59    use crate::exporter::u128_to_u64s;
60
61    const DATADOG_TRACE_ID_HEADER: &str = "x-datadog-trace-id";
62    const DATADOG_PARENT_ID_HEADER: &str = "x-datadog-parent-id";
63    const DATADOG_SAMPLING_PRIORITY_HEADER: &str = "x-datadog-sampling-priority";
64
65    const TRACE_FLAG_DEFERRED: TraceFlags = TraceFlags::new(0x02);
66
67    lazy_static::lazy_static! {
68        static ref DATADOG_HEADER_FIELDS: [String; 3] = [
69            DATADOG_TRACE_ID_HEADER.to_string(),
70            DATADOG_PARENT_ID_HEADER.to_string(),
71            DATADOG_SAMPLING_PRIORITY_HEADER.to_string(),
72        ];
73    }
74
75    enum SamplingPriority {
76        UserReject = -1,
77        AutoReject = 0,
78        AutoKeep = 1,
79        UserKeep = 2,
80    }
81
82    #[derive(Debug)]
83    enum ExtractError {
84        TraceId,
85        SpanId,
86        SamplingPriority,
87    }
88
89    /// Extracts and injects `SpanContext`s into `Extractor`s or `Injector`s using Datadog's header format.
90    ///
91    /// The Datadog header format does not have an explicit spec, but can be divined from the client libraries,
92    /// such as [dd-trace-go]
93    ///
94    /// ## Example
95    ///
96    /// ```
97    /// use opentelemetry::global;
98    /// use opentelemetry_datadog_cloudflare::DatadogPropagator;
99    ///
100    /// global::set_text_map_propagator(DatadogPropagator::default());
101    /// ```
102    ///
103    /// [dd-trace-go]: https://github.com/DataDog/dd-trace-go/blob/v1.28.0/ddtrace/tracer/textmap.go#L293
104    #[derive(Clone, Debug, Default)]
105    #[allow(clippy::module_name_repetitions)]
106    pub struct DatadogPropagator {
107        _private: (),
108    }
109
110    impl DatadogPropagator {
111        /// Creates a new `DatadogPropagator`.
112        #[must_use]
113        pub fn new() -> Self {
114            DatadogPropagator::default()
115        }
116
117        fn extract_trace_id(trace_id: &str) -> Result<TraceId, ExtractError> {
118            trace_id
119                .parse::<u64>()
120                .map(|id| TraceId::from(u128::from(id).to_be_bytes()))
121                .map_err(|_| ExtractError::TraceId)
122        }
123
124        fn extract_span_id(span_id: &str) -> Result<SpanId, ExtractError> {
125            span_id
126                .parse::<u64>()
127                .map(|id| SpanId::from(id.to_be_bytes()))
128                .map_err(|_| ExtractError::SpanId)
129        }
130
131        fn extract_sampling_priority(
132            sampling_priority: &str,
133        ) -> Result<SamplingPriority, ExtractError> {
134            let i = sampling_priority
135                .parse::<i32>()
136                .map_err(|_| ExtractError::SamplingPriority)?;
137
138            match i {
139                -1 => Ok(SamplingPriority::UserReject),
140                0 => Ok(SamplingPriority::AutoReject),
141                1 => Ok(SamplingPriority::AutoKeep),
142                2 => Ok(SamplingPriority::UserKeep),
143                _ => Err(ExtractError::SamplingPriority),
144            }
145        }
146
147        fn extract_span_context(extractor: &dyn Extractor) -> Result<SpanContext, ExtractError> {
148            let trace_id =
149                Self::extract_trace_id(extractor.get(DATADOG_TRACE_ID_HEADER).unwrap_or(""))?;
150            // If we have a trace_id but can't get the parent span, we default it to invalid instead of completely erroring
151            // out so that the rest of the spans aren't completely lost
152            let span_id =
153                Self::extract_span_id(extractor.get(DATADOG_PARENT_ID_HEADER).unwrap_or(""))
154                    .unwrap_or(SpanId::INVALID);
155            let sampling_priority = Self::extract_sampling_priority(
156                extractor
157                    .get(DATADOG_SAMPLING_PRIORITY_HEADER)
158                    .unwrap_or(""),
159            );
160            let sampled = match sampling_priority {
161                Ok(SamplingPriority::UserReject | SamplingPriority::AutoReject) => {
162                    TraceFlags::default()
163                }
164                Ok(SamplingPriority::UserKeep | SamplingPriority::AutoKeep) => TraceFlags::SAMPLED,
165                // Treat the sampling as DEFERRED instead of erroring on extracting the span context
166                Err(_) => TRACE_FLAG_DEFERRED,
167            };
168
169            let trace_state = TraceState::default();
170
171            Ok(SpanContext::new(
172                trace_id,
173                span_id,
174                sampled,
175                true,
176                trace_state,
177            ))
178        }
179    }
180
181    impl TextMapPropagator for DatadogPropagator {
182        fn inject_context(&self, cx: &Context, injector: &mut dyn Injector) {
183            let span = cx.span();
184            let span_context = span.span_context();
185            if span_context.is_valid() {
186                let [t0, _] = u128_to_u64s(u128::from_be_bytes(span_context.trace_id().to_bytes()));
187                injector.set(DATADOG_TRACE_ID_HEADER, t0.to_string());
188                injector.set(
189                    DATADOG_PARENT_ID_HEADER,
190                    u64::from_be_bytes(span_context.span_id().to_bytes()).to_string(),
191                );
192
193                if span_context.trace_flags() & TRACE_FLAG_DEFERRED != TRACE_FLAG_DEFERRED {
194                    let sampling_priority = if span_context.is_sampled() {
195                        SamplingPriority::AutoKeep
196                    } else {
197                        SamplingPriority::AutoReject
198                    };
199
200                    injector.set(
201                        DATADOG_SAMPLING_PRIORITY_HEADER,
202                        (sampling_priority as i32).to_string(),
203                    );
204                }
205            }
206        }
207
208        fn extract_with_context(&self, cx: &Context, extractor: &dyn Extractor) -> Context {
209            let extracted = Self::extract_span_context(extractor)
210                .unwrap_or_else(|_| SpanContext::empty_context());
211
212            cx.with_remote_span_context(extracted)
213        }
214
215        fn fields(&self) -> FieldIter<'_> {
216            FieldIter::new(DATADOG_HEADER_FIELDS.as_ref())
217        }
218    }
219
220    #[cfg(test)]
221    mod tests {
222        use super::*;
223        use opentelemetry::testing::trace::TestSpan;
224        use opentelemetry::trace::TraceState;
225        use std::collections::HashMap;
226
227        #[rustfmt::skip]
228        fn extract_test_data() -> Vec<(Vec<(&'static str, &'static str)>, SpanContext)> {
229            vec![
230                (vec![], SpanContext::empty_context()),
231                (vec![(DATADOG_SAMPLING_PRIORITY_HEADER, "0")], SpanContext::empty_context()),
232                (vec![(DATADOG_TRACE_ID_HEADER, "garbage")], SpanContext::empty_context()),
233                (vec![(DATADOG_TRACE_ID_HEADER, "1234"), (DATADOG_PARENT_ID_HEADER, "garbage")], SpanContext::new(TraceId::from_u128(1234), SpanId::INVALID, TRACE_FLAG_DEFERRED, true, TraceState::default())),
234                (vec![(DATADOG_TRACE_ID_HEADER, "1234"), (DATADOG_PARENT_ID_HEADER, "12")], SpanContext::new(TraceId::from_u128(1234), SpanId::from_u64(12), TRACE_FLAG_DEFERRED, true, TraceState::default())),
235                (vec![(DATADOG_TRACE_ID_HEADER, "1234"), (DATADOG_PARENT_ID_HEADER, "12"), (DATADOG_SAMPLING_PRIORITY_HEADER, "0")], SpanContext::new(TraceId::from_u128(1234), SpanId::from_u64(12), TraceFlags::default(), true, TraceState::default())),
236                (vec![(DATADOG_TRACE_ID_HEADER, "1234"), (DATADOG_PARENT_ID_HEADER, "12"), (DATADOG_SAMPLING_PRIORITY_HEADER, "1")], SpanContext::new(TraceId::from_u128(1234), SpanId::from_u64(12), TraceFlags::SAMPLED, true, TraceState::default())),
237            ]
238        }
239
240        #[rustfmt::skip]
241        fn inject_test_data() -> Vec<(Vec<(&'static str, &'static str)>, SpanContext)> {
242            vec![
243                (vec![], SpanContext::empty_context()),
244                (vec![], SpanContext::new(TraceId::INVALID, SpanId::INVALID, TRACE_FLAG_DEFERRED, true, TraceState::default())),
245                (vec![], SpanContext::new(TraceId::from_hex("1234").unwrap(), SpanId::INVALID, TRACE_FLAG_DEFERRED, true, TraceState::default())),
246                (vec![], SpanContext::new(TraceId::from_hex("1234").unwrap(), SpanId::INVALID, TraceFlags::SAMPLED, true, TraceState::default())),
247                (vec![(DATADOG_TRACE_ID_HEADER, "1234"), (DATADOG_PARENT_ID_HEADER, "12")], SpanContext::new(TraceId::from_u128(1234), SpanId::from_u64(12), TRACE_FLAG_DEFERRED, true, TraceState::default())),
248                (vec![(DATADOG_TRACE_ID_HEADER, "1234"), (DATADOG_PARENT_ID_HEADER, "12"), (DATADOG_SAMPLING_PRIORITY_HEADER, "0")], SpanContext::new(TraceId::from_u128(1234), SpanId::from_u64(12), TraceFlags::default(), true, TraceState::default())),
249                (vec![(DATADOG_TRACE_ID_HEADER, "1234"), (DATADOG_PARENT_ID_HEADER, "12"), (DATADOG_SAMPLING_PRIORITY_HEADER, "1")], SpanContext::new(TraceId::from_u128(1234), SpanId::from_u64(12), TraceFlags::SAMPLED, true, TraceState::default())),
250            ]
251        }
252
253        #[test]
254        fn test_extract() {
255            for (header_list, expected) in extract_test_data() {
256                let map: HashMap<String, String> = header_list
257                    .into_iter()
258                    .map(|(k, v)| (k.to_string(), v.to_string()))
259                    .collect();
260
261                let propagator = DatadogPropagator::default();
262                let context = propagator.extract(&map);
263                assert_eq!(context.span().span_context(), &expected);
264            }
265        }
266
267        #[test]
268        fn test_extract_empty() {
269            let map: HashMap<String, String> = HashMap::new();
270            let propagator = DatadogPropagator::default();
271            let context = propagator.extract(&map);
272            assert_eq!(context.span().span_context(), &SpanContext::empty_context());
273        }
274
275        #[test]
276        fn test_inject() {
277            let propagator = DatadogPropagator::default();
278            for (header_values, span_context) in inject_test_data() {
279                let mut injector: HashMap<String, String> = HashMap::new();
280                propagator.inject_context(
281                    &Context::current_with_span(TestSpan(span_context)),
282                    &mut injector,
283                );
284
285                if !header_values.is_empty() {
286                    for (k, v) in header_values {
287                        let injected_value: Option<&String> = injector.get(k);
288                        assert_eq!(injected_value, Some(&v.to_string()));
289                        injector.remove(k);
290                    }
291                }
292                assert!(injector.is_empty());
293            }
294        }
295    }
296}
297
298pub use exporter::{
299    new_pipeline, DatadogExporter, DatadogPipelineBuilder, Error, SpanProcessExt,
300    WASMWorkerSpanProcessor,
301};
302pub use propagator::DatadogPropagator;