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::SpanExporter;
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/// Initializes tracing to stdout and optionally an open-telemetry exporter.
26///
27/// Trace filtering defaults to `INFO` and can be configured using the conventional `RUST_LOG`
28/// environment variable.
29///
30/// The open-telemetry configuration is controlled via environment variables as defined in the
31/// [specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#opentelemetry-protocol-exporter)
32///
33/// Registers a panic hook so that panic errors are reported to the open-telemetry exporter.
34pub fn setup_tracing(otel: OpenTelemetry) -> anyhow::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::<anyhow::Error>::into)?;
58
59    // Register panic hook now that tracing is initialized.
60    std::panic::set_hook(Box::new(|info| {
61        tracing::error!(panic = true, "{info}");
62    }));
63    Ok(())
64}
65
66/// Initializes tracing to a test exporter.
67///
68/// Allows trace content to be inspected via the returned receiver.
69///
70/// All tests that use this function must be annotated with `#[serial(open_telemetry_tracing)]`.
71/// This forces serialization of all such tests. Otherwise, the tested spans could
72/// be interleaved during runtime. Also, the global exporter could be re-initialized in
73/// the middle of a concurrently running test.
74#[cfg(feature = "testing")]
75pub fn setup_test_tracing() -> anyhow::Result<(
76    tokio::sync::mpsc::UnboundedReceiver<opentelemetry_sdk::trace::SpanData>,
77    tokio::sync::mpsc::UnboundedReceiver<()>,
78)> {
79    let (exporter, rx_export, rx_shutdown) =
80        opentelemetry_sdk::testing::trace::new_tokio_test_exporter();
81
82    let otel_layer = open_telemetry_layer(exporter);
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)?;
87    Ok((rx_export, rx_shutdown))
88}
89
90fn open_telemetry_layer<S>(
91    exporter: impl SpanExporter + 'static,
92) -> Box<dyn tracing_subscriber::Layer<S> + Send + Sync + 'static>
93where
94    S: Subscriber + Sync + Send,
95    for<'a> S: tracing_subscriber::registry::LookupSpan<'a>,
96{
97    let tracer = opentelemetry_sdk::trace::SdkTracerProvider::builder()
98        .with_batch_exporter(exporter)
99        .build();
100
101    let tracer = tracer.tracer("tracing-otel-subscriber");
102    OpenTelemetryLayer::new(tracer).boxed()
103}
104
105#[cfg(not(feature = "tracing-forest"))]
106fn stdout_layer<S>() -> Box<dyn tracing_subscriber::Layer<S> + Send + Sync + 'static>
107where
108    S: Subscriber,
109    for<'a> S: tracing_subscriber::registry::LookupSpan<'a>,
110{
111    use tracing_subscriber::fmt::format::FmtSpan;
112
113    tracing_subscriber::fmt::layer()
114        .pretty()
115        .compact()
116        .with_level(true)
117        .with_file(true)
118        .with_line_number(true)
119        .with_target(true)
120        .with_span_events(FmtSpan::CLOSE)
121        .boxed()
122}
123
124#[cfg(feature = "tracing-forest")]
125fn stdout_layer<S>() -> Box<dyn tracing_subscriber::Layer<S> + Send + Sync + 'static>
126where
127    S: Subscriber,
128    for<'a> S: tracing_subscriber::registry::LookupSpan<'a>,
129{
130    tracing_forest::ForestLayer::default().boxed()
131}
132
133/// Creates a filter from the `RUST_LOG` env var with a default of `INFO` if unset.
134///
135/// # Panics
136///
137/// Panics if `RUST_LOG` fails to parse.
138fn env_or_default_filter<S>() -> Box<dyn Filter<S> + Send + Sync + 'static> {
139    use tracing::level_filters::LevelFilter;
140    use tracing_subscriber::EnvFilter;
141    use tracing_subscriber::filter::{FilterExt, Targets};
142
143    // `tracing` does not allow differentiating between invalid and missing env var so we manually
144    // do this instead. The alternative is to silently ignore parsing errors which I think is worse.
145    match std::env::var(EnvFilter::DEFAULT_ENV) {
146        Ok(rust_log) => FilterExt::boxed(
147            EnvFilter::from_str(&rust_log)
148                .expect("RUST_LOG should contain a valid filter configuration"),
149        ),
150        Err(std::env::VarError::NotUnicode(_)) => panic!("RUST_LOG contained non-unicode"),
151        Err(std::env::VarError::NotPresent) => {
152            // Default level is INFO, and additionally enable logs from axum extractor rejections.
153            FilterExt::boxed(
154                Targets::new()
155                    .with_default(LevelFilter::INFO)
156                    .with_target("axum::rejection", LevelFilter::TRACE),
157            )
158        },
159    }
160}