miden_node_utils/
logging.rs

1use std::str::FromStr;
2
3use anyhow::Result;
4use opentelemetry::trace::TracerProvider as _;
5use opentelemetry_otlp::WithTonicConfig;
6use opentelemetry_sdk::{propagation::TraceContextPropagator, trace::SpanExporter};
7use tracing::subscriber::Subscriber;
8use tracing_opentelemetry::OpenTelemetryLayer;
9use tracing_subscriber::{
10    Layer, Registry,
11    layer::{Filter, SubscriberExt},
12};
13
14/// Configures [`setup_tracing`] to enable or disable the open-telemetry exporter.
15#[derive(Clone, Copy)]
16pub enum OpenTelemetry {
17    Enabled,
18    Disabled,
19}
20
21impl OpenTelemetry {
22    fn is_enabled(self) -> bool {
23        matches!(self, OpenTelemetry::Enabled)
24    }
25}
26
27/// Initializes tracing to stdout and optionally an open-telemetry exporter.
28///
29/// Trace filtering defaults to `INFO` and can be configured using the conventional `RUST_LOG`
30/// environment variable.
31///
32/// The open-telemetry configuration is controlled via environment variables as defined in the
33/// [specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#opentelemetry-protocol-exporter)
34pub fn setup_tracing(otel: OpenTelemetry) -> Result<()> {
35    if otel.is_enabled() {
36        opentelemetry::global::set_text_map_propagator(TraceContextPropagator::new());
37    }
38
39    // Note: open-telemetry requires a tokio-runtime, so this _must_ be lazily evaluated (aka not
40    // `then_some`) to avoid crashing sync callers (with OpenTelemetry::Disabled set). Examples of
41    // such callers are tests with logging enabled.
42    let otel_layer = {
43        if otel.is_enabled() {
44            let exporter = opentelemetry_otlp::SpanExporter::builder()
45                .with_tonic()
46                .with_tls_config(tonic::transport::ClientTlsConfig::new().with_native_roots())
47                .build()?;
48            Some(open_telemetry_layer(exporter))
49        } else {
50            None
51        }
52    };
53
54    let subscriber = Registry::default()
55        .with(stdout_layer().with_filter(env_or_default_filter()))
56        .with(otel_layer.with_filter(env_or_default_filter()));
57    tracing::subscriber::set_global_default(subscriber).map_err(Into::into)
58}
59
60/// Initializes tracing to a test exporter.
61///
62/// Allows trace content to be inspected via the returned receiver.
63///
64/// All tests that use this function must be annotated with `#[serial(open_telemetry_tracing)]`.
65/// This forces serialization of all such tests. Otherwise, the tested spans could
66/// be interleaved during runtime. Also, the global exporter could be re-initialized in
67/// the middle of a concurrently running test.
68#[cfg(feature = "testing")]
69pub fn setup_test_tracing() -> Result<(
70    tokio::sync::mpsc::UnboundedReceiver<opentelemetry_sdk::trace::SpanData>,
71    tokio::sync::mpsc::UnboundedReceiver<()>,
72)> {
73    let (exporter, rx_export, rx_shutdown) =
74        opentelemetry_sdk::testing::trace::new_tokio_test_exporter();
75
76    let otel_layer = open_telemetry_layer(exporter);
77    let subscriber = Registry::default()
78        .with(stdout_layer().with_filter(env_or_default_filter()))
79        .with(otel_layer.with_filter(env_or_default_filter()));
80    tracing::subscriber::set_global_default(subscriber)?;
81    Ok((rx_export, rx_shutdown))
82}
83
84fn open_telemetry_layer<S>(
85    exporter: impl SpanExporter + 'static,
86) -> Box<dyn tracing_subscriber::Layer<S> + Send + Sync + 'static>
87where
88    S: Subscriber + Sync + Send,
89    for<'a> S: tracing_subscriber::registry::LookupSpan<'a>,
90{
91    let tracer = opentelemetry_sdk::trace::SdkTracerProvider::builder()
92        .with_batch_exporter(exporter)
93        .build();
94
95    let tracer = tracer.tracer("tracing-otel-subscriber");
96    OpenTelemetryLayer::new(tracer).boxed()
97}
98
99#[cfg(not(feature = "tracing-forest"))]
100fn stdout_layer<S>() -> Box<dyn tracing_subscriber::Layer<S> + Send + Sync + 'static>
101where
102    S: Subscriber,
103    for<'a> S: tracing_subscriber::registry::LookupSpan<'a>,
104{
105    use tracing_subscriber::fmt::format::FmtSpan;
106
107    tracing_subscriber::fmt::layer()
108        .pretty()
109        .compact()
110        .with_level(true)
111        .with_file(true)
112        .with_line_number(true)
113        .with_target(true)
114        .with_span_events(FmtSpan::CLOSE)
115        .boxed()
116}
117
118#[cfg(feature = "tracing-forest")]
119fn stdout_layer<S>() -> Box<dyn tracing_subscriber::Layer<S> + Send + Sync + 'static>
120where
121    S: Subscriber,
122    for<'a> S: tracing_subscriber::registry::LookupSpan<'a>,
123{
124    tracing_forest::ForestLayer::default().boxed()
125}
126
127/// Creates a filter from the `RUST_LOG` env var with a default of `INFO` if unset.
128///
129/// # Panics
130///
131/// Panics if `RUST_LOG` fails to parse.
132fn env_or_default_filter<S>() -> Box<dyn Filter<S> + Send + Sync + 'static> {
133    use tracing::level_filters::LevelFilter;
134    use tracing_subscriber::{
135        EnvFilter,
136        filter::{FilterExt, Targets},
137    };
138
139    // `tracing` does not allow differentiating between invalid and missing env var so we manually
140    // do this instead. The alternative is to silently ignore parsing errors which I think is worse.
141    match std::env::var(EnvFilter::DEFAULT_ENV) {
142        Ok(rust_log) => FilterExt::boxed(
143            EnvFilter::from_str(&rust_log)
144                .expect("RUST_LOG should contain a valid filter configuration"),
145        ),
146        Err(std::env::VarError::NotUnicode(_)) => panic!("RUST_LOG contained non-unicode"),
147        Err(std::env::VarError::NotPresent) => {
148            // Default level is INFO, and additionally enable logs from axum extractor rejections.
149            FilterExt::boxed(
150                Targets::new()
151                    .with_default(LevelFilter::INFO)
152                    .with_target("axum::rejection", LevelFilter::TRACE),
153            )
154        },
155    }
156}