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
60fn open_telemetry_layer<S>(
61    exporter: impl SpanExporter + 'static,
62) -> Box<dyn tracing_subscriber::Layer<S> + Send + Sync + 'static>
63where
64    S: Subscriber + Sync + Send,
65    for<'a> S: tracing_subscriber::registry::LookupSpan<'a>,
66{
67    let tracer = opentelemetry_sdk::trace::SdkTracerProvider::builder()
68        .with_batch_exporter(exporter)
69        .build();
70
71    let tracer = tracer.tracer("tracing-otel-subscriber");
72    OpenTelemetryLayer::new(tracer).boxed()
73}
74
75#[cfg(not(feature = "tracing-forest"))]
76fn stdout_layer<S>() -> Box<dyn tracing_subscriber::Layer<S> + Send + Sync + 'static>
77where
78    S: Subscriber,
79    for<'a> S: tracing_subscriber::registry::LookupSpan<'a>,
80{
81    use tracing_subscriber::fmt::format::FmtSpan;
82
83    tracing_subscriber::fmt::layer()
84        .pretty()
85        .compact()
86        .with_level(true)
87        .with_file(true)
88        .with_line_number(true)
89        .with_target(true)
90        .with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)
91        .boxed()
92}
93
94#[cfg(feature = "tracing-forest")]
95fn stdout_layer<S>() -> Box<dyn tracing_subscriber::Layer<S> + Send + Sync + 'static>
96where
97    S: Subscriber,
98    for<'a> S: tracing_subscriber::registry::LookupSpan<'a>,
99{
100    tracing_forest::ForestLayer::default().boxed()
101}
102
103/// Creates a filter from the `RUST_LOG` env var with a default of `INFO` if unset.
104///
105/// # Panics
106///
107/// Panics if `RUST_LOG` fails to parse.
108fn env_or_default_filter<S>() -> Box<dyn Filter<S> + Send + Sync + 'static> {
109    use tracing::level_filters::LevelFilter;
110    use tracing_subscriber::{
111        EnvFilter,
112        filter::{FilterExt, Targets},
113    };
114
115    // `tracing` does not allow differentiating between invalid and missing env var so we manually
116    // do this instead. The alternative is to silently ignore parsing errors which I think is worse.
117    match std::env::var(EnvFilter::DEFAULT_ENV) {
118        Ok(rust_log) => FilterExt::boxed(
119            EnvFilter::from_str(&rust_log)
120                .expect("RUST_LOG should contain a valid filter configuration"),
121        ),
122        Err(std::env::VarError::NotUnicode(_)) => panic!("RUST_LOG contained non-unicode"),
123        Err(std::env::VarError::NotPresent) => {
124            // Default level is INFO, and additionally enable logs from axum extractor rejections.
125            FilterExt::boxed(
126                Targets::new()
127                    .with_default(LevelFilter::INFO)
128                    .with_target("axum::rejection", LevelFilter::TRACE),
129            )
130        },
131    }
132}