Skip to main content

miden_note_transport_node/
logging.rs

1use std::str::FromStr;
2
3use anyhow::Result;
4use opentelemetry::trace::TracerProvider as _;
5use opentelemetry_otlp::WithExportConfig;
6use opentelemetry_sdk::metrics::{PeriodicReader, SdkMeterProvider};
7use opentelemetry_sdk::propagation::TraceContextPropagator;
8use opentelemetry_sdk::trace::SpanExporter;
9use tracing::subscriber::Subscriber;
10use tracing_opentelemetry::OpenTelemetryLayer;
11use tracing_subscriber::layer::{Filter, SubscriberExt};
12use tracing_subscriber::{Layer, Registry};
13
14/// Configures [`setup_tracing`] to enable or disable the open-telemetry exporter.
15#[derive(Clone)]
16pub struct TracingConfig {
17    /// OpenTelemetry configuration
18    pub otel: OpenTelemetry,
19    /// Export data JSON-formatted
20    pub json_format: bool,
21}
22
23/// OpenTelemetry configuration
24#[derive(Clone)]
25pub enum OpenTelemetry {
26    /// Enable OpenTelemetry export
27    Enabled {
28        /// Endpoint
29        endpoint: String,
30    },
31    /// Disable OpenTelemetry
32    Disabled,
33}
34
35impl TracingConfig {
36    /// Build tracing configuration from the environment.
37    ///
38    /// OpenTelemetry export is enabled when a standard OTLP endpoint is configured via
39    /// `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` or `OTEL_EXPORTER_OTLP_ENDPOINT` (the former
40    /// takes precedence). When neither is set, export is disabled.
41    pub fn from_otel_env() -> Self {
42        let endpoint = std::env::var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT")
43            .or_else(|_| std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT"))
44            .ok()
45            .filter(|s| !s.is_empty());
46
47        let otel = match endpoint {
48            Some(endpoint) => OpenTelemetry::Enabled { endpoint },
49            None => OpenTelemetry::Disabled,
50        };
51
52        TracingConfig {
53            otel,
54            json_format: std::env::var("JSON_LOGGING")
55                .unwrap_or_else(|_| "false".to_string())
56                .parse()
57                .unwrap_or(false),
58        }
59    }
60}
61
62impl OpenTelemetry {
63    /// Is OpenTelemetry enabled
64    pub fn is_enabled(&self) -> bool {
65        matches!(self, OpenTelemetry::Enabled { .. })
66    }
67}
68
69/// Initializes tracing to stdout and optionally an open-telemetry exporter.
70///
71/// Trace filtering defaults to `INFO` and can be configured using the conventional `RUST_LOG`
72/// environment variable.
73///
74/// The open-telemetry configuration is controlled via environment variables as defined in the
75/// [specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#opentelemetry-protocol-exporter)
76pub fn setup_tracing(cfg: TracingConfig) -> Result<()> {
77    if cfg.otel.is_enabled() {
78        opentelemetry::global::set_text_map_propagator(TraceContextPropagator::new());
79
80        // Setup metrics export if OTEL is enabled
81        setup_metrics_export(&cfg.otel)?;
82    }
83
84    // Note: open-telemetry requires a tokio-runtime, so this _must_ be lazily evaluated (aka not
85    // `then_some`) to avoid crashing sync callers (with OpenTelemetry::Disabled set). Examples of
86    // such callers are tests with logging enabled.
87    let otel_layer = {
88        if let OpenTelemetry::Enabled { endpoint } = cfg.otel {
89            let exporter_builder =
90                opentelemetry_otlp::SpanExporter::builder().with_tonic().with_endpoint(endpoint);
91
92            match exporter_builder.build() {
93                Ok(exporter) => {
94                    Some(open_telemetry_layer(exporter, "miden-note-transport-node".to_string()))
95                },
96                Err(_) => None,
97            }
98        } else {
99            None
100        }
101    };
102
103    let subscriber = Registry::default()
104        .with(stdout_layer(cfg.json_format).with_filter(env_or_default_filter()))
105        .with(otel_layer.with_filter(env_or_default_filter()));
106
107    tracing::subscriber::set_global_default(subscriber).map_err(Into::into)
108}
109
110/// Setup OpenTelemetry metrics export using the proper SDK API
111fn setup_metrics_export(otel_cfg: &OpenTelemetry) -> Result<()> {
112    if let OpenTelemetry::Enabled { endpoint } = otel_cfg {
113        // Configure OTLP metrics pipeline
114        let exporter = opentelemetry_otlp::MetricExporter::builder()
115            .with_tonic()
116            .with_endpoint(endpoint)
117            .build()?;
118
119        let provider = SdkMeterProvider::builder()
120            .with_reader(
121                PeriodicReader::builder(exporter)
122                    .with_interval(std::time::Duration::from_secs(5)) // Push interval
123                    .build(),
124            )
125            .build();
126
127        // Set the meter provider globally
128        opentelemetry::global::set_meter_provider(provider);
129    }
130
131    Ok(())
132}
133
134/// Initializes tracing to a test exporter.
135///
136/// Allows trace content to be inspected via the returned receiver.
137///
138/// All tests that use this function must be annotated with `#[serial(open_telemetry_tracing)]`.
139/// This forces serialization of all such tests. Otherwise, the tested spans could
140/// be interleaved during runtime. Also, the global exporter could be re-initialized in
141/// the middle of a concurrently running test.
142#[cfg(test)]
143pub fn setup_test_tracing() -> Result<(
144    tokio::sync::mpsc::UnboundedReceiver<opentelemetry_sdk::trace::SpanData>,
145    tokio::sync::mpsc::UnboundedReceiver<()>,
146)> {
147    let (exporter, rx_export, rx_shutdown) =
148        opentelemetry_sdk::testing::trace::new_tokio_test_exporter();
149
150    let otel_layer = open_telemetry_layer(exporter, "test-service".to_string());
151    let subscriber = Registry::default()
152        .with(stdout_layer(true).with_filter(env_or_default_filter()))
153        .with(otel_layer.with_filter(env_or_default_filter()));
154    tracing::subscriber::set_global_default(subscriber)?;
155    Ok((rx_export, rx_shutdown))
156}
157
158fn open_telemetry_layer<S>(
159    exporter: impl SpanExporter + 'static,
160    service_name: String,
161) -> Box<dyn tracing_subscriber::Layer<S> + Send + Sync + 'static>
162where
163    S: Subscriber + Sync + Send,
164    for<'a> S: tracing_subscriber::registry::LookupSpan<'a>,
165{
166    let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
167        .with_batch_exporter(exporter)
168        .build();
169
170    let tracer = provider.tracer(service_name);
171
172    // Set the tracer provider globally
173    opentelemetry::global::set_tracer_provider(provider);
174
175    OpenTelemetryLayer::new(tracer).boxed()
176}
177
178fn stdout_layer<S>(
179    json_logging: bool,
180) -> Box<dyn tracing_subscriber::Layer<S> + Send + Sync + 'static>
181where
182    S: Subscriber,
183    for<'a> S: tracing_subscriber::registry::LookupSpan<'a>,
184{
185    use tracing_subscriber::fmt::format::FmtSpan;
186
187    if json_logging {
188        tracing_subscriber::fmt::layer()
189            .with_level(true)
190            .with_file(true)
191            .with_line_number(true)
192            .with_target(true)
193            .with_span_events(FmtSpan::CLOSE)
194            .json()
195            .boxed()
196    } else {
197        tracing_subscriber::fmt::layer()
198            .pretty()
199            .compact()
200            .with_level(true)
201            .with_file(true)
202            .with_line_number(true)
203            .with_target(true)
204            .with_span_events(FmtSpan::CLOSE)
205            .boxed()
206    }
207}
208
209/// Creates a filter from the `RUST_LOG` env var with a default of `INFO` if unset.
210///
211/// # Panics
212///
213/// Panics if `RUST_LOG` fails to parse.
214fn env_or_default_filter<S>() -> Box<dyn Filter<S> + Send + Sync + 'static> {
215    use tracing::level_filters::LevelFilter;
216    use tracing_subscriber::EnvFilter;
217    use tracing_subscriber::filter::{FilterExt, Targets};
218
219    // `tracing` does not allow differentiating between invalid and missing env var so we manually
220    // do this instead. The alternative is to silently ignore parsing errors which I think is worse.
221    match std::env::var(EnvFilter::DEFAULT_ENV) {
222        Ok(rust_log) => FilterExt::boxed(
223            EnvFilter::from_str(&rust_log)
224                .expect("RUST_LOG should contain a valid filter configuration"),
225        ),
226        Err(std::env::VarError::NotUnicode(_)) => panic!("RUST_LOG contained non-unicode"),
227        Err(std::env::VarError::NotPresent) => {
228            // Default level is INFO, and additionally enable logs from axum extractor rejections.
229            FilterExt::boxed(
230                Targets::new()
231                    .with_default(LevelFilter::INFO)
232                    .with_target("axum::rejection", LevelFilter::TRACE),
233            )
234        },
235    }
236}