Skip to main content

miden_node_utils/
logging.rs

1use std::str::FromStr;
2use std::sync::OnceLock;
3
4use opentelemetry::trace::TracerProvider as _;
5use opentelemetry_otlp::WithTonicConfig;
6use opentelemetry_sdk::propagation::TraceContextPropagator;
7use opentelemetry_sdk::trace::SdkTracerProvider;
8use tracing::subscriber::Subscriber;
9use tracing_opentelemetry::OpenTelemetryLayer;
10use tracing_subscriber::layer::{Filter, SubscriberExt};
11use tracing_subscriber::{Layer, Registry};
12
13/// Global tracer provider for flushing traces on panic.
14///
15/// This is necessary because the panic hook needs access to the tracer provider to flush
16/// pending spans before the program terminates.
17static TRACER_PROVIDER: OnceLock<SdkTracerProvider> = OnceLock::new();
18
19/// Configures [`setup_tracing`] to enable or disable the open-telemetry exporter.
20#[derive(Clone, Copy)]
21pub enum OpenTelemetry {
22    Enabled,
23    Disabled,
24}
25
26impl OpenTelemetry {
27    fn is_enabled(self) -> bool {
28        matches!(self, OpenTelemetry::Enabled)
29    }
30}
31
32/// A guard that shuts down the tracer provider when dropped. This ensures that the logs are flushed
33/// to the exporter before the program exits.
34pub struct OtelGuard {
35    tracer_provider: SdkTracerProvider,
36}
37
38impl Drop for OtelGuard {
39    fn drop(&mut self) {
40        if let Err(err) = self.tracer_provider.shutdown() {
41            eprintln!("{err:?}");
42        }
43    }
44}
45
46/// Initializes tracing to stdout and optionally an open-telemetry exporter.
47///
48/// Trace filtering defaults to `INFO` and can be configured using the conventional `RUST_LOG`
49/// environment variable.
50///
51/// The open-telemetry configuration is controlled via environment variables as defined in the
52/// [specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#opentelemetry-protocol-exporter)
53///
54/// Registers a panic hook so that panic errors are reported to the open-telemetry exporter.
55///
56/// Returns an [`OtelGuard`] if open-telemetry is enabled, otherwise `None`. When this guard is
57/// dropped, the tracer provider is shutdown.
58pub fn setup_tracing(otel: OpenTelemetry) -> anyhow::Result<Option<OtelGuard>> {
59    if otel.is_enabled() {
60        opentelemetry::global::set_text_map_propagator(TraceContextPropagator::new());
61    }
62
63    // Note: open-telemetry requires a tokio-runtime, so this _must_ be lazily evaluated (aka not
64    // `then_some`) to avoid crashing sync callers (with OpenTelemetry::Disabled set). Examples of
65    // such callers are tests with logging enabled.
66    let tracer_provider = if otel.is_enabled() {
67        let provider = init_tracer_provider()?;
68
69        // Store the provider globally so the panic hook can flush it.
70        // SdkTracerProvider is internally reference-counted, so cloning is cheap.
71        TRACER_PROVIDER
72            .set(provider.clone())
73            .expect("setup_tracing should only be called once");
74
75        Some(provider)
76    } else {
77        None
78    };
79    let otel_layer = tracer_provider.as_ref().map(|provider| {
80        OpenTelemetryLayer::new(provider.tracer("tracing-otel-subscriber")).boxed()
81    });
82
83    let subscriber = Registry::default()
84        .with(stdout_layer().with_filter(env_or_default_filter()))
85        .with(otel_layer.with_filter(env_or_default_filter()));
86    tracing::subscriber::set_global_default(subscriber).map_err(Into::<anyhow::Error>::into)?;
87
88    // Register panic hook now that tracing is initialized.
89    // This chains with the default panic hook to preserve backtrace printing.
90    let default_hook = std::panic::take_hook();
91    std::panic::set_hook(Box::new(move |info| {
92        tracing::error!(panic = true, "{info}");
93
94        // Flush traces before the program terminates.
95        // This ensures the panic trace is exported even though the OtelGuard won't be dropped.
96        if let Some(provider) = TRACER_PROVIDER.get() {
97            if let Err(err) = provider.force_flush() {
98                eprintln!("Failed to flush traces on panic: {err:?}");
99            }
100        }
101
102        // Call the default hook to print the backtrace.
103        default_hook(info);
104    }));
105
106    Ok(tracer_provider.map(|tracer_provider| OtelGuard { tracer_provider }))
107}
108
109fn init_tracer_provider() -> anyhow::Result<SdkTracerProvider> {
110    let exporter = opentelemetry_otlp::SpanExporter::builder()
111        .with_tonic()
112        .with_tls_config(tonic::transport::ClientTlsConfig::new().with_native_roots())
113        .build()?;
114
115    Ok(opentelemetry_sdk::trace::SdkTracerProvider::builder()
116        .with_batch_exporter(exporter)
117        .build())
118}
119
120/// Initializes tracing to a test exporter.
121///
122/// Allows trace content to be inspected via the returned receiver.
123///
124/// All tests that use this function must be annotated with `#[serial(open_telemetry_tracing)]`.
125/// This forces serialization of all such tests. Otherwise, the tested spans could
126/// be interleaved during runtime. Also, the global exporter could be re-initialized in
127/// the middle of a concurrently running test.
128#[cfg(feature = "testing")]
129pub fn setup_test_tracing() -> anyhow::Result<(
130    tokio::sync::mpsc::UnboundedReceiver<opentelemetry_sdk::trace::SpanData>,
131    tokio::sync::mpsc::UnboundedReceiver<()>,
132)> {
133    let (exporter, rx_export, rx_shutdown) =
134        opentelemetry_sdk::testing::trace::new_tokio_test_exporter();
135
136    let tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
137        .with_batch_exporter(exporter)
138        .build();
139    let otel_layer =
140        OpenTelemetryLayer::new(tracer_provider.tracer("tracing-otel-subscriber")).boxed();
141    let subscriber = Registry::default()
142        .with(stdout_layer().with_filter(env_or_default_filter()))
143        .with(otel_layer.with_filter(env_or_default_filter()));
144    tracing::subscriber::set_global_default(subscriber)?;
145    Ok((rx_export, rx_shutdown))
146}
147
148#[cfg(not(feature = "tracing-forest"))]
149fn stdout_layer<S>() -> Box<dyn tracing_subscriber::Layer<S> + Send + Sync + 'static>
150where
151    S: Subscriber,
152    for<'a> S: tracing_subscriber::registry::LookupSpan<'a>,
153{
154    use tracing_subscriber::fmt::format::FmtSpan;
155
156    tracing_subscriber::fmt::layer()
157        .pretty()
158        .compact()
159        .with_level(true)
160        .with_file(true)
161        .with_line_number(true)
162        .with_target(true)
163        .with_span_events(FmtSpan::CLOSE)
164        .boxed()
165}
166
167#[cfg(feature = "tracing-forest")]
168fn stdout_layer<S>() -> Box<dyn tracing_subscriber::Layer<S> + Send + Sync + 'static>
169where
170    S: Subscriber,
171    for<'a> S: tracing_subscriber::registry::LookupSpan<'a>,
172{
173    tracing_forest::ForestLayer::default().boxed()
174}
175
176/// Creates a filter from the `RUST_LOG` env var with a default of `INFO` if unset.
177///
178/// # Panics
179///
180/// Panics if `RUST_LOG` fails to parse.
181fn env_or_default_filter<S>() -> Box<dyn Filter<S> + Send + Sync + 'static> {
182    use tracing::level_filters::LevelFilter;
183    use tracing_subscriber::EnvFilter;
184    use tracing_subscriber::filter::{FilterExt, Targets};
185
186    // `tracing` does not allow differentiating between invalid and missing env var so we manually
187    // do this instead. The alternative is to silently ignore parsing errors which I think is worse.
188    match std::env::var(EnvFilter::DEFAULT_ENV) {
189        Ok(rust_log) => FilterExt::boxed(
190            EnvFilter::from_str(&rust_log)
191                .expect("RUST_LOG should contain a valid filter configuration"),
192        ),
193        Err(std::env::VarError::NotUnicode(_)) => panic!("RUST_LOG contained non-unicode"),
194        Err(std::env::VarError::NotPresent) => {
195            // Default level is INFO, and additionally enable logs from axum extractor rejections.
196            FilterExt::boxed(
197                Targets::new()
198                    .with_default(LevelFilter::INFO)
199                    .with_target("axum::rejection", LevelFilter::TRACE),
200            )
201        },
202    }
203}