1use std::future::Future;
29
30use tracing::Instrument;
31
32use crate::Result;
33
34pub const METRIC_REQUESTS: &str = "canton_client_requests_total";
36pub const METRIC_ERRORS: &str = "canton_client_errors_total";
38
39pub const TRANSPORT_GRPC: &str = "grpc";
41pub const TRANSPORT_JSON: &str = "json";
43
44pub 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 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
90pub 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 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#[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#[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 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 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 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 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 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 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 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 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 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 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 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 #[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 let recorder = metrics_util::debugging::DebuggingRecorder::new();
581 let snapshotter = recorder.snapshotter();
582 recorder.install().expect("install metrics recorder");
583
584 let captured = SpanCapture::default();
586 let subscriber = tracing_subscriber::registry().with(captured.clone());
587 let _guard = set_default(subscriber);
588
589 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 {
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 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 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 #[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 #[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 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}