Skip to main content

canton_core/
telemetry.rs

1//! Telemetry: tracing spans, metrics, and structured events for client calls.
2//!
3//! This is the transport-neutral instrumentation the client crates wrap their
4//! RPCs with (Option B: telemetry lives in `canton-core`). Every instrumented
5//! call opens a `canton.rpc` [`tracing`] span, emits request/error counters via
6//! the [`metrics`] facade, and logs a structured success/error event.
7//!
8//! **Exporting.** Following the standard Rust telemetry model, this crate
9//! *emits* and the application chooses the exporters: a `tracing_subscriber`
10//! for logs and spans, a [`metrics`] recorder for counters. Metrics carry
11//! `method` + `transport` labels, so any recorder (Prometheus, an OTLP bridge,
12//! …) gets the per-endpoint request/error breakdown for free (success =
13//! requests − errors).
14//!
15//! The `otel` feature supplies a supported OpenTelemetry path for both halves
16//! rather than leaving it as an exercise: `otel::otlp_tracer` builds the span
17//! exporter, `otel::otlp_metrics` builds the metrics pipeline **and** installs
18//! the recorder that bridges this crate's counters onto it, and trace context
19//! is injected into every outgoing request — gRPC metadata, JSON headers, and
20//! the WebSocket upgrade — automatically.
21//!
22//! **Streams.** Use [`instrument`] for a call that returns a value and
23//! [`instrument_stream`] for one that returns a stream. The difference is not
24//! cosmetic: a subscription's outcome is not known when it opens, so
25//! instrumenting only the opening records every long-lived stream as a success
26//! and never revisits it.
27
28use std::future::Future;
29
30use tracing::Instrument;
31
32use crate::Result;
33
34/// Counter: total client requests, labelled by `method` and `transport`.
35pub const METRIC_REQUESTS: &str = "canton_client_requests_total";
36/// Counter: client errors, labelled by `method`, `transport`, and `retriable`.
37pub const METRIC_ERRORS: &str = "canton_client_errors_total";
38
39/// `transport` label / span-field value for the gRPC lane.
40pub const TRANSPORT_GRPC: &str = "grpc";
41/// `transport` label / span-field value for the JSON lane.
42pub const TRANSPORT_JSON: &str = "json";
43
44/// Instrument a client RPC future: open a `canton.rpc` span, count the request
45/// (and any error), and log a structured outcome event.
46///
47/// `method` and `transport` become both span fields and metric labels. The
48/// future is polled inside the span, so any spans/events the RPC itself emits
49/// nest correctly and inherit the trace context.
50pub async fn instrument<T, F>(method: &'static str, transport: &'static str, fut: F) -> Result<T>
51where
52    F: Future<Output = Result<T>>,
53{
54    metrics::counter!(METRIC_REQUESTS, "method" => method, "transport" => transport).increment(1);
55
56    let span = tracing::info_span!("canton.rpc", method = method, transport = transport);
57    async move {
58        let result = fut.await;
59        // Recorded on the event rather than left to the subscriber: correlating
60        // a log line with its trace otherwise needs subscriber plumbing the
61        // application has to write, and the SDK is the thing that knows.
62        let trace_id = current_trace_id().unwrap_or_default();
63        match &result {
64            Ok(_) => tracing::debug!(method, transport, trace_id, "rpc completed"),
65            Err(error) => {
66                let retriable = error.is_retriable();
67                metrics::counter!(
68                    METRIC_ERRORS,
69                    "method" => method,
70                    "transport" => transport,
71                    "retriable" => retriable.to_string(),
72                )
73                .increment(1);
74                tracing::warn!(
75                    method,
76                    transport,
77                    retriable,
78                    trace_id,
79                    error = %error,
80                    "rpc failed",
81                );
82            }
83        }
84        result
85    }
86    .instrument(span)
87    .await
88}
89
90/// Instrument a client *stream*: the same span, counters and events as
91/// [`instrument`], but for the life of the stream rather than the moment it
92/// opens.
93///
94/// Opening a stream and consuming it are different events, and only the first
95/// one is a future. A subscription that opens cleanly and fails ten minutes
96/// later — the participant restarting, the connection dropping — was recorded
97/// as a success and never corrected, so the error counter said nothing about
98/// the failure mode long-lived clients actually hit.
99///
100/// Each item is polled inside the span, so whatever the transport logs while
101/// producing it is attributed correctly. Errors increment
102/// [`METRIC_ERRORS`] as they arrive; the stream ending is logged with the
103/// number of items it delivered.
104pub fn instrument_stream<T, S>(
105    method: &'static str,
106    transport: &'static str,
107    stream: S,
108) -> impl futures_core::Stream<Item = Result<T>> + Send
109where
110    S: futures_core::Stream<Item = Result<T>> + Send,
111    T: Send,
112{
113    use tokio_stream::StreamExt as _;
114
115    let span = tracing::info_span!("canton.stream", method = method, transport = transport);
116    async_stream::stream! {
117        tokio::pin!(stream);
118        let mut items = 0u64;
119        loop {
120            // The span wraps the poll rather than being held across it: a
121            // guard kept over an await attributes whatever else the task runs
122            // to this stream.
123            let next = stream.next().instrument(span.clone()).await;
124            match next {
125                Some(Ok(item)) => {
126                    items += 1;
127                    yield Ok(item);
128                }
129                Some(Err(error)) => {
130                    let retriable = error.is_retriable();
131                    metrics::counter!(
132                        METRIC_ERRORS,
133                        "method" => method,
134                        "transport" => transport,
135                        "retriable" => retriable.to_string(),
136                    )
137                    .increment(1);
138                    span.in_scope(|| {
139                        tracing::warn!(
140                            method,
141                            transport,
142                            retriable,
143                            items,
144                            trace_id = current_trace_id().unwrap_or_default(),
145                            error = %error,
146                            "stream failed",
147                        );
148                    });
149                    yield Err(error);
150                }
151                None => {
152                    span.in_scope(|| {
153                        tracing::debug!(
154                            method,
155                            transport,
156                            items,
157                            trace_id = current_trace_id().unwrap_or_default(),
158                            "stream ended",
159                        );
160                    });
161                    return;
162                }
163            }
164        }
165    }
166}
167
168/// The current span's trace id as 32 hex characters, when an OpenTelemetry
169/// context is active.
170///
171/// Structured events carry this so a log line can be joined to the trace it
172/// belongs to. Without the `otel` feature — or with no subscriber bridging to
173/// OpenTelemetry — there is no trace to name, and this is `None`.
174#[must_use]
175pub fn current_trace_id() -> Option<String> {
176    #[cfg(feature = "otel")]
177    {
178        otel::current_trace_id()
179    }
180    #[cfg(not(feature = "otel"))]
181    {
182        None
183    }
184}
185
186/// OpenTelemetry export helpers (enable the `otel` feature).
187///
188/// The SDK emits `tracing` spans unconditionally; this module bridges them to
189/// an OTLP collector and propagates W3C trace context into outgoing requests.
190#[cfg(feature = "otel")]
191pub mod otel {
192    use std::collections::HashMap;
193    use std::sync::Arc;
194    use std::sync::Mutex;
195    use std::sync::atomic::{AtomicU64, Ordering};
196
197    use opentelemetry::metrics::MeterProvider as _;
198    use opentelemetry::propagation::TextMapPropagator as _;
199    use opentelemetry::trace::TracerProvider as _;
200    use opentelemetry_otlp::WithExportConfig as _;
201    use opentelemetry_sdk::propagation::TraceContextPropagator;
202
203    /// Build an OTLP-exporting tracer named `service_name`, batch-sending spans
204    /// to the gRPC OTLP `endpoint` (e.g. `http://localhost:4317`). Compose the
205    /// returned tracer into a `tracing` subscriber with
206    /// `tracing_opentelemetry::layer().with_tracer(tracer)`.
207    ///
208    /// # Errors
209    /// Returns a [`opentelemetry::trace::TraceError`] if the exporter cannot be
210    /// built (e.g. an invalid endpoint).
211    pub fn otlp_tracer(
212        service_name: &'static str,
213        endpoint: impl Into<String>,
214    ) -> Result<opentelemetry_sdk::trace::Tracer, opentelemetry::trace::TraceError> {
215        Ok(otlp_tracer_provider(service_name, endpoint)?.tracer(service_name))
216    }
217
218    /// [`otlp_tracer`]'s provider, for an application that needs to flush or
219    /// shut it down.
220    ///
221    /// Spans are exported in batches, so a process that exits without calling
222    /// `force_flush()` or `shutdown()` loses whatever the last batch held —
223    /// which tends to be the spans around whatever made it exit. The tracer
224    /// alone does not expose its provider, so this returns it.
225    ///
226    /// # Errors
227    /// Returns a [`opentelemetry::trace::TraceError`] if the exporter cannot be
228    /// built (e.g. an invalid endpoint).
229    pub fn otlp_tracer_provider(
230        service_name: &'static str,
231        endpoint: impl Into<String>,
232    ) -> Result<opentelemetry_sdk::trace::TracerProvider, opentelemetry::trace::TraceError> {
233        let exporter = opentelemetry_otlp::SpanExporter::builder()
234            .with_tonic()
235            .with_endpoint(endpoint.into())
236            .build()?;
237        Ok(opentelemetry_sdk::trace::TracerProvider::builder()
238            .with_batch_exporter(exporter, opentelemetry_sdk::runtime::Tokio)
239            .with_resource(opentelemetry_sdk::Resource::new(vec![
240                opentelemetry::KeyValue::new("service.name", service_name),
241            ]))
242            .build())
243    }
244
245    /// Build an OTLP metrics pipeline and install it as the process's
246    /// [`metrics`] recorder, so the counters this SDK emits reach a collector.
247    ///
248    /// The SDK emits through the [`metrics`] facade, which does nothing until
249    /// an application installs a recorder — and the one supported path to
250    /// OpenTelemetry was left to the reader. This is that path: one call, after
251    /// which `canton_client_requests_total` and `canton_client_errors_total`
252    /// arrive at `endpoint` with their `method`, `transport` and `retriable`
253    /// labels intact.
254    ///
255    /// Keep the returned provider alive for the life of the process and call
256    /// `shutdown()` before exit — metrics are exported periodically, so
257    /// dropping it early loses the last interval.
258    ///
259    /// ```no_run
260    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
261    /// let meters = canton_core::telemetry::otel::otlp_metrics(
262    ///     "my-service",
263    ///     "http://localhost:4317",
264    /// )?;
265    /// // … run the application …
266    /// meters.shutdown()?;
267    /// # Ok(()) }
268    /// ```
269    ///
270    /// # Errors
271    /// Returns an error if the exporter cannot be built (an invalid endpoint,
272    /// say) or if a `metrics` recorder is already installed.
273    pub fn otlp_metrics(
274        service_name: &'static str,
275        endpoint: impl Into<String>,
276    ) -> Result<opentelemetry_sdk::metrics::SdkMeterProvider, Box<dyn std::error::Error>> {
277        let exporter = opentelemetry_otlp::MetricExporter::builder()
278            .with_tonic()
279            .with_endpoint(endpoint.into())
280            .build()?;
281        let reader = opentelemetry_sdk::metrics::PeriodicReader::builder(
282            exporter,
283            opentelemetry_sdk::runtime::Tokio,
284        )
285        .build();
286        let provider = opentelemetry_sdk::metrics::SdkMeterProvider::builder()
287            .with_reader(reader)
288            .with_resource(opentelemetry_sdk::Resource::new(vec![
289                opentelemetry::KeyValue::new("service.name", service_name),
290            ]))
291            .build();
292        metrics::set_global_recorder(OtelRecorder::new(provider.meter(service_name)))
293            .map_err(|e| -> Box<dyn std::error::Error> { Box::new(e) })?;
294        Ok(provider)
295    }
296
297    /// Bridges the [`metrics`] facade onto an OpenTelemetry meter.
298    ///
299    /// The two models differ in one way that matters: a `metrics` key carries
300    /// its labels, while an OpenTelemetry instrument is created once per name
301    /// and takes attributes at record time. So instruments are cached by name
302    /// and the key's labels become attributes.
303    struct OtelRecorder {
304        meter: opentelemetry::metrics::Meter,
305        counters: Mutex<HashMap<String, opentelemetry::metrics::Counter<u64>>>,
306        gauges: Mutex<HashMap<String, opentelemetry::metrics::Gauge<f64>>>,
307        histograms: Mutex<HashMap<String, opentelemetry::metrics::Histogram<f64>>>,
308    }
309
310    impl OtelRecorder {
311        fn new(meter: opentelemetry::metrics::Meter) -> Self {
312            Self {
313                meter,
314                counters: Mutex::new(HashMap::new()),
315                gauges: Mutex::new(HashMap::new()),
316                histograms: Mutex::new(HashMap::new()),
317            }
318        }
319    }
320
321    /// A key's labels, as OpenTelemetry attributes.
322    fn attributes(key: &metrics::Key) -> Vec<opentelemetry::KeyValue> {
323        key.labels()
324            .map(|label| {
325                opentelemetry::KeyValue::new(label.key().to_string(), label.value().to_string())
326            })
327            .collect()
328    }
329
330    struct BridgedCounter {
331        counter: opentelemetry::metrics::Counter<u64>,
332        attributes: Vec<opentelemetry::KeyValue>,
333        /// The last value seen from `absolute`, so a cumulative report can be
334        /// turned into the delta an OpenTelemetry counter takes.
335        last_absolute: AtomicU64,
336    }
337
338    impl metrics::CounterFn for BridgedCounter {
339        fn increment(&self, value: u64) {
340            self.counter.add(value, &self.attributes);
341        }
342
343        fn absolute(&self, value: u64) {
344            let previous = self.last_absolute.swap(value, Ordering::SeqCst);
345            self.counter
346                .add(value.saturating_sub(previous), &self.attributes);
347        }
348    }
349
350    struct BridgedGauge {
351        gauge: opentelemetry::metrics::Gauge<f64>,
352        attributes: Vec<opentelemetry::KeyValue>,
353        /// OpenTelemetry's synchronous gauge only takes absolute values, so
354        /// relative moves are tracked here.
355        value: Mutex<f64>,
356    }
357
358    impl BridgedGauge {
359        fn apply(&self, change: impl FnOnce(f64) -> f64) {
360            let mut current = self
361                .value
362                .lock()
363                .unwrap_or_else(std::sync::PoisonError::into_inner);
364            *current = change(*current);
365            self.gauge.record(*current, &self.attributes);
366        }
367    }
368
369    impl metrics::GaugeFn for BridgedGauge {
370        fn increment(&self, value: f64) {
371            self.apply(|current| current + value);
372        }
373
374        fn decrement(&self, value: f64) {
375            self.apply(|current| current - value);
376        }
377
378        fn set(&self, value: f64) {
379            self.apply(|_| value);
380        }
381    }
382
383    struct BridgedHistogram {
384        histogram: opentelemetry::metrics::Histogram<f64>,
385        attributes: Vec<opentelemetry::KeyValue>,
386    }
387
388    impl metrics::HistogramFn for BridgedHistogram {
389        fn record(&self, value: f64) {
390            self.histogram.record(value, &self.attributes);
391        }
392    }
393
394    impl metrics::Recorder for OtelRecorder {
395        fn describe_counter(
396            &self,
397            _key: metrics::KeyName,
398            _unit: Option<metrics::Unit>,
399            _description: metrics::SharedString,
400        ) {
401        }
402        fn describe_gauge(
403            &self,
404            _key: metrics::KeyName,
405            _unit: Option<metrics::Unit>,
406            _description: metrics::SharedString,
407        ) {
408        }
409        fn describe_histogram(
410            &self,
411            _key: metrics::KeyName,
412            _unit: Option<metrics::Unit>,
413            _description: metrics::SharedString,
414        ) {
415        }
416
417        fn register_counter(
418            &self,
419            key: &metrics::Key,
420            _metadata: &metrics::Metadata<'_>,
421        ) -> metrics::Counter {
422            let name = key.name().to_string();
423            let counter = {
424                let mut counters = self
425                    .counters
426                    .lock()
427                    .unwrap_or_else(std::sync::PoisonError::into_inner);
428                counters
429                    .entry(name.clone())
430                    .or_insert_with(|| self.meter.u64_counter(name).build())
431                    .clone()
432            };
433            metrics::Counter::from_arc(Arc::new(BridgedCounter {
434                counter,
435                attributes: attributes(key),
436                last_absolute: AtomicU64::new(0),
437            }))
438        }
439
440        fn register_gauge(
441            &self,
442            key: &metrics::Key,
443            _metadata: &metrics::Metadata<'_>,
444        ) -> metrics::Gauge {
445            let name = key.name().to_string();
446            let gauge = {
447                let mut gauges = self
448                    .gauges
449                    .lock()
450                    .unwrap_or_else(std::sync::PoisonError::into_inner);
451                gauges
452                    .entry(name.clone())
453                    .or_insert_with(|| self.meter.f64_gauge(name).build())
454                    .clone()
455            };
456            metrics::Gauge::from_arc(Arc::new(BridgedGauge {
457                gauge,
458                attributes: attributes(key),
459                value: Mutex::new(0.0),
460            }))
461        }
462
463        fn register_histogram(
464            &self,
465            key: &metrics::Key,
466            _metadata: &metrics::Metadata<'_>,
467        ) -> metrics::Histogram {
468            let name = key.name().to_string();
469            let histogram = {
470                let mut histograms = self
471                    .histograms
472                    .lock()
473                    .unwrap_or_else(std::sync::PoisonError::into_inner);
474                histograms
475                    .entry(name.clone())
476                    .or_insert_with(|| self.meter.f64_histogram(name).build())
477                    .clone()
478            };
479            metrics::Histogram::from_arc(Arc::new(BridgedHistogram {
480                histogram,
481                attributes: attributes(key),
482            }))
483        }
484    }
485
486    /// The W3C trace-context headers (`traceparent` / `tracestate`) for the
487    /// current span, or empty when no valid OpenTelemetry context is active
488    /// (i.e. no `tracing_opentelemetry` layer installed, or an unsampled span).
489    fn trace_context_carrier() -> std::collections::HashMap<String, String> {
490        use opentelemetry::trace::TraceContextExt as _;
491        use tracing_opentelemetry::OpenTelemetrySpanExt as _;
492
493        let context = tracing::Span::current().context();
494        let mut carrier = std::collections::HashMap::new();
495        if context.span().span_context().is_valid() {
496            TraceContextPropagator::new().inject_context(&context, &mut carrier);
497        }
498        carrier
499    }
500
501    /// The current span's trace id as 32 hex characters, when a valid
502    /// OpenTelemetry context is active.
503    pub(super) fn current_trace_id() -> Option<String> {
504        use opentelemetry::trace::TraceContextExt as _;
505        use tracing_opentelemetry::OpenTelemetrySpanExt as _;
506
507        let context = tracing::Span::current().context();
508        let span_context = context.span().span_context().clone();
509        span_context.is_valid().then(|| {
510            format!(
511                "{:032x}",
512                u128::from_be_bytes(span_context.trace_id().to_bytes())
513            )
514        })
515    }
516
517    /// Inject the current span's W3C trace context into an outgoing HTTP header
518    /// map (the JSON transport), so the participant can correlate the request.
519    pub fn inject_trace_context(headers: &mut http::HeaderMap) {
520        for (key, value) in trace_context_carrier() {
521            if let (Ok(name), Ok(val)) = (
522                http::header::HeaderName::try_from(key),
523                http::HeaderValue::from_str(&value),
524            ) {
525                headers.insert(name, val);
526            }
527        }
528    }
529
530    /// Inject the current span's W3C trace context into outgoing gRPC request
531    /// metadata, so the participant can correlate the request.
532    pub fn inject_trace_context_metadata(metadata: &mut tonic::metadata::MetadataMap) {
533        for (key, value) in trace_context_carrier() {
534            if let (Ok(name), Ok(val)) = (
535                tonic::metadata::MetadataKey::from_bytes(key.as_bytes()),
536                tonic::metadata::MetadataValue::try_from(value),
537            ) {
538                metadata.insert(name, val);
539            }
540        }
541    }
542}
543
544#[cfg(test)]
545#[allow(clippy::unwrap_used, clippy::expect_used)]
546mod tests {
547    use super::*;
548    use crate::Error;
549    use std::sync::{Arc, Mutex};
550    use tokio_stream::StreamExt as _;
551    use tracing::subscriber::set_default;
552    use tracing_subscriber::Layer;
553    use tracing_subscriber::layer::{Context, SubscriberExt};
554    use tracing_subscriber::registry::LookupSpan;
555
556    /// A tiny tracing layer that records the names of spans it sees created.
557    #[derive(Clone, Default)]
558    struct SpanCapture(Arc<Mutex<Vec<String>>>);
559
560    impl<S> Layer<S> for SpanCapture
561    where
562        S: tracing::Subscriber + for<'a> LookupSpan<'a>,
563    {
564        fn on_new_span(
565            &self,
566            attrs: &tracing::span::Attributes<'_>,
567            _id: &tracing::span::Id,
568            _ctx: Context<'_, S>,
569        ) {
570            self.0
571                .lock()
572                .unwrap()
573                .push(attrs.metadata().name().to_string());
574        }
575    }
576
577    #[tokio::test]
578    async fn instrument_emits_span_and_metrics() {
579        // Global metrics recorder (installed once for this test binary).
580        let recorder = metrics_util::debugging::DebuggingRecorder::new();
581        let snapshotter = recorder.snapshotter();
582        recorder.install().expect("install metrics recorder");
583
584        // Capture tracing spans on this (current-thread) test runtime.
585        let captured = SpanCapture::default();
586        let subscriber = tracing_subscriber::registry().with(captured.clone());
587        let _guard = set_default(subscriber);
588
589        // One success, one (non-retriable) failure.
590        let ok: Result<u8> = instrument("version", TRANSPORT_GRPC, async { Ok(1) }).await;
591        assert_eq!(ok.unwrap(), 1);
592        let err: Result<u8> = instrument("ledger_end", TRANSPORT_GRPC, async {
593            Err(Error::InvalidRequest("boom".into()))
594        })
595        .await;
596        assert!(err.is_err());
597
598        // A `canton.rpc` span was opened for each call.
599        {
600            let spans = captured.0.lock().unwrap();
601            assert!(
602                spans.iter().filter(|n| *n == "canton.rpc").count() >= 2,
603                "expected canton.rpc spans, saw {spans:?}"
604            );
605        }
606
607        // Metrics: 2 requests, 1 error.
608        let snapshot = snapshotter.snapshot().into_vec();
609        let counter_total = |name: &str| -> u64 {
610            snapshot
611                .iter()
612                .filter(|(key, _, _, _)| key.key().name() == name)
613                .filter_map(|(_, _, _, value)| match value {
614                    metrics_util::debugging::DebugValue::Counter(c) => Some(*c),
615                    _ => None,
616                })
617                .sum()
618        };
619        assert_eq!(counter_total(METRIC_REQUESTS), 2, "two requests counted");
620        assert_eq!(counter_total(METRIC_ERRORS), 1, "one error counted");
621
622        // A stream that opens cleanly and fails later: the failure arrives
623        // after the call that opened it has already returned, which is why
624        // wrapping only that call recorded this as a success and stopped
625        // watching.
626        let source = tokio_stream::iter(vec![
627            Ok(1u8),
628            Err(Error::Connection("the participant went away".into())),
629        ]);
630        let stream = instrument_stream("updates", TRANSPORT_GRPC, source);
631        tokio::pin!(stream);
632        let mut outcomes = Vec::new();
633        while let Some(item) = stream.next().await {
634            outcomes.push(item.is_ok());
635        }
636        assert_eq!(outcomes, vec![true, false], "both items reach the caller");
637
638        let snapshot = snapshotter.snapshot().into_vec();
639        let counter_total = |name: &str| -> u64 {
640            snapshot
641                .iter()
642                .filter(|(key, _, _, _)| key.key().name() == name)
643                .filter_map(|(_, _, _, value)| match value {
644                    metrics_util::debugging::DebugValue::Counter(c) => Some(*c),
645                    _ => None,
646                })
647                .sum()
648        };
649        assert_eq!(
650            counter_total(METRIC_ERRORS),
651            2,
652            "the stream's mid-life failure is counted too"
653        );
654        let spans = captured.0.lock().unwrap();
655        assert!(
656            spans.iter().any(|name| name == "canton.stream"),
657            "expected a canton.stream span, saw {spans:?}"
658        );
659    }
660
661    /// With no active OTel span context, injection is a no-op (nothing to
662    /// propagate) and must never panic.
663    #[cfg(feature = "otel")]
664    #[test]
665    fn inject_trace_context_is_a_noop_without_a_context() {
666        let mut headers = http::HeaderMap::new();
667        super::otel::inject_trace_context(&mut headers);
668        assert!(
669            headers.is_empty(),
670            "no trace context should be injected outside a span, saw {headers:?}"
671        );
672    }
673
674    /// Under an installed OTel tracer, an active span's W3C trace context is
675    /// injected into both HTTP headers (JSON) and gRPC metadata.
676    /// The trace id an application correlates a log line with. Without a
677    /// tracer there is nothing to report, and reporting a made-up id would be
678    /// worse than reporting none.
679    #[cfg(feature = "otel")]
680    #[test]
681    fn the_trace_id_in_a_structured_event_is_the_active_span_s() {
682        use opentelemetry::trace::TracerProvider as _;
683
684        assert_eq!(
685            super::current_trace_id(),
686            None,
687            "no tracer installed means no trace id to name"
688        );
689
690        let provider = opentelemetry_sdk::trace::TracerProvider::builder().build();
691        let otel_layer = tracing_opentelemetry::layer().with_tracer(provider.tracer("test"));
692        let subscriber = tracing_subscriber::registry().with(otel_layer);
693        let _guard = set_default(subscriber);
694
695        let span = tracing::info_span!("test.rpc");
696        let _entered = span.enter();
697
698        let trace_id = super::current_trace_id().expect("a span is active");
699        assert_eq!(
700            trace_id.len(),
701            32,
702            "a W3C trace id is 32 hex digits: {trace_id}"
703        );
704        assert!(
705            trace_id.chars().all(|c| c.is_ascii_hexdigit()),
706            "not hex: {trace_id}"
707        );
708        assert_ne!(
709            trace_id, "00000000000000000000000000000000",
710            "the all-zero id means no trace, and must not be reported as one"
711        );
712
713        // The same id the W3C header carries, or the log line and the trace it
714        // points at belong to different requests.
715        let mut headers = http::HeaderMap::new();
716        super::otel::inject_trace_context(&mut headers);
717        let traceparent = headers["traceparent"].to_str().expect("ascii").to_string();
718        assert!(
719            traceparent.contains(&trace_id),
720            "traceparent {traceparent} should carry trace id {trace_id}"
721        );
722    }
723
724    #[cfg(feature = "otel")]
725    #[test]
726    fn trace_context_is_injected_under_a_tracer() {
727        use opentelemetry::trace::TracerProvider as _;
728
729        let provider = opentelemetry_sdk::trace::TracerProvider::builder().build();
730        let otel_layer = tracing_opentelemetry::layer().with_tracer(provider.tracer("test"));
731        let subscriber = tracing_subscriber::registry().with(otel_layer);
732        let _guard = set_default(subscriber);
733
734        let span = tracing::info_span!("test.rpc");
735        let _entered = span.enter();
736
737        let mut headers = http::HeaderMap::new();
738        super::otel::inject_trace_context(&mut headers);
739        assert!(
740            headers.contains_key("traceparent"),
741            "expected a W3C traceparent header, saw {headers:?}"
742        );
743
744        let mut metadata = tonic::metadata::MetadataMap::new();
745        super::otel::inject_trace_context_metadata(&mut metadata);
746        assert!(
747            metadata.get("traceparent").is_some(),
748            "expected traceparent in gRPC metadata"
749        );
750    }
751}