miden_note_transport_node/
logging.rs1use 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#[derive(Clone)]
16pub struct TracingConfig {
17 pub otel: OpenTelemetry,
19 pub json_format: bool,
21}
22
23#[derive(Clone)]
25pub enum OpenTelemetry {
26 Enabled {
28 endpoint: String,
30 },
31 Disabled,
33}
34
35impl TracingConfig {
36 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 pub fn is_enabled(&self) -> bool {
65 matches!(self, OpenTelemetry::Enabled { .. })
66 }
67}
68
69pub 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(&cfg.otel)?;
82 }
83
84 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
110fn setup_metrics_export(otel_cfg: &OpenTelemetry) -> Result<()> {
112 if let OpenTelemetry::Enabled { endpoint } = otel_cfg {
113 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)) .build(),
124 )
125 .build();
126
127 opentelemetry::global::set_meter_provider(provider);
129 }
130
131 Ok(())
132}
133
134#[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 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
209fn 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 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 FilterExt::boxed(
230 Targets::new()
231 .with_default(LevelFilter::INFO)
232 .with_target("axum::rejection", LevelFilter::TRACE),
233 )
234 },
235 }
236}