miden_node_utils/
logging.rs

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