Skip to main content

apollo_router/
tracer.rs

1//! Trace Ids for the router.
2
3#![warn(unreachable_pub)]
4use std::fmt;
5
6use opentelemetry::trace::TraceContextExt;
7use serde::Deserialize;
8use serde::Serialize;
9use tracing::Span;
10use tracing_subscriber::Registry;
11use tracing_subscriber::registry::LookupSpan;
12
13use crate::plugins::telemetry::otel::OpenTelemetrySpanExt;
14use crate::plugins::telemetry::reload::otel::IsSampled;
15
16/// Trace ID
17#[cfg_attr(test, derive(Default))]
18#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
19pub struct TraceId([u8; 16]);
20
21impl TraceId {
22    /// Create a TraceId. If the span is not sampled then return None.
23    pub fn maybe_new() -> Option<Self> {
24        let span = Span::current();
25        let context = span.context();
26        let span_ref = context.span();
27        let span_context = span_ref.span_context();
28        if span_context.is_sampled() {
29            Some(Self(span_context.trace_id().to_bytes()))
30        } else {
31            None
32        }
33    }
34
35    /// Get the current trace id if it's a valid one, even if it's not sampled
36    pub(crate) fn current() -> Option<Self> {
37        Span::current()
38            .with_subscriber(move |(id, dispatch)| {
39                if let Some(reg) = dispatch.downcast_ref::<Registry>() {
40                    match reg.span(id) {
41                        None => {
42                            eprintln!("no spanref, this is a bug");
43                            None
44                        }
45                        Some(s) => s.get_trace_id(),
46                    }
47                } else {
48                    ::tracing::error!("no Registry, this is a bug");
49                    None
50                }
51            })
52            .flatten()
53    }
54
55    /// Convert the TraceId to bytes.
56    pub fn as_bytes(&self) -> &[u8; 16] {
57        &self.0
58    }
59
60    /// Convert the TraceId to u128.
61    pub const fn to_u128(&self) -> u128 {
62        u128::from_be_bytes(self.0)
63    }
64}
65
66impl fmt::Display for TraceId {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        write!(f, "{:032x}", self.to_u128())
69    }
70}
71
72impl From<[u8; 16]> for TraceId {
73    fn from(value: [u8; 16]) -> Self {
74        Self(value)
75    }
76}
77
78// Note: These tests all end up writing what look like dbg!() spans to stdout when the tests are
79// run as part of the full suite.
80// Why? It's probably related to the way that the rust test framework tries to capture test
81// output. I spent a little time investigating it and concluded it will be harder to fix than to
82// live with...
83#[cfg(test)]
84mod test {
85    use once_cell::sync::Lazy;
86    use opentelemetry::trace::TracerProvider;
87    use parking_lot::Mutex;
88    use tracing_subscriber::Registry;
89    use tracing_subscriber::layer::SubscriberExt;
90
91    use super::TraceId;
92    use crate::plugins::telemetry::otel;
93
94    // If we try to run more than one test concurrently which relies on the existence of a pipeline,
95    // then the tests will fail due to manipulation of global state in the opentelemetry crates.
96    // If we set test-threads=1, then this avoids the problem but means all our tests will run slowly.
97    // So: to avoid this problem, we have a mutex lock which just exists to serialize access to the
98    // global resources.
99    static TRACING_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
100
101    #[test]
102    fn it_returns_invalid_trace_id() {
103        let my_id = TraceId::maybe_new();
104        assert!(my_id.is_none());
105    }
106
107    #[test]
108    fn it_correctly_compares_invalid_and_invalid_trace_id() {
109        let my_id = TraceId::maybe_new();
110        let other_id = TraceId::maybe_new();
111        assert!(my_id.is_none());
112        assert!(other_id.is_none());
113        assert!(other_id == my_id);
114    }
115
116    #[tokio::test]
117    async fn it_returns_valid_trace_id() {
118        use opentelemetry::InstrumentationScope;
119
120        let _guard = TRACING_LOCK.lock();
121        // Create a tracing layer with the configured tracer
122
123        let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
124            .with_simple_exporter(opentelemetry_stdout::SpanExporter::default())
125            .build();
126        let tracer = provider.tracer_with_scope(InstrumentationScope::builder("noop").build());
127
128        let telemetry = otel::layer().force_sampling().with_tracer(tracer);
129        // Use the tracing subscriber `Registry`, or any other subscriber
130        // that impls `LookupSpan`
131        let subscriber = Registry::default().with(telemetry);
132        // Trace executed code
133        tracing::subscriber::with_default(subscriber, || {
134            // Spans will be sent to the configured OpenTelemetry exporter
135            let _span = tracing::trace_span!("trace test").entered();
136            assert!(TraceId::maybe_new().is_some());
137        });
138    }
139
140    #[test]
141    fn it_correctly_compares_valid_and_invalid_trace_id() {
142        let _guard = TRACING_LOCK.lock();
143        let my_id = TraceId::maybe_new();
144        assert!(my_id.is_none());
145        // Create a tracing layer with the configured tracer
146        let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
147            .with_simple_exporter(opentelemetry_stdout::SpanExporter::default())
148            .build();
149        let tracer = provider
150            .tracer_with_scope(opentelemetry::InstrumentationScope::builder("noop").build());
151        let telemetry = otel::layer().force_sampling().with_tracer(tracer);
152        // Use the tracing subscriber `Registry`, or any other subscriber
153        // that impls `LookupSpan`
154        let subscriber = Registry::default().with(telemetry);
155        // Trace executed code
156        tracing::subscriber::with_default(subscriber, || {
157            // Spans will be sent to the configured OpenTelemetry exporter
158            let _span = tracing::trace_span!("trace test").entered();
159
160            let other_id = TraceId::maybe_new();
161            assert!(other_id.is_some());
162            assert_ne!(other_id, my_id);
163        });
164    }
165
166    #[test]
167    fn it_correctly_compares_valid_and_valid_trace_id() {
168        let _guard = TRACING_LOCK.lock();
169        // Create a tracing layer with the configured tracer
170        let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
171            .with_simple_exporter(opentelemetry_stdout::SpanExporter::default())
172            .build();
173        let tracer = provider
174            .tracer_with_scope(opentelemetry::InstrumentationScope::builder("noop").build());
175        let telemetry = otel::layer().force_sampling().with_tracer(tracer);
176        // Use the tracing subscriber `Registry`, or any other subscriber
177        // that impls `LookupSpan`
178        let subscriber = Registry::default().with(telemetry);
179        // Trace executed code
180        tracing::subscriber::with_default(subscriber, || {
181            // Spans will be sent to the configured OpenTelemetry exporter
182            let _span = tracing::trace_span!("trace test").entered();
183
184            let my_id = TraceId::maybe_new();
185            assert!(my_id.is_some());
186            let other_id = TraceId::maybe_new();
187            assert_eq!(other_id, my_id);
188        });
189    }
190}