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