1use std::str::FromStr;
2use std::sync::OnceLock;
3
4use opentelemetry::trace::TracerProvider as _;
5use opentelemetry::{KeyValue, Value};
6use opentelemetry_otlp::WithTonicConfig as _;
7use opentelemetry_sdk::Resource;
8use opentelemetry_sdk::propagation::TraceContextPropagator;
9use opentelemetry_sdk::resource::{EnvResourceDetector, TelemetryResourceDetector};
10use opentelemetry_sdk::trace::SdkTracerProvider;
11use tracing::subscriber::Subscriber;
12use tracing_opentelemetry::OpenTelemetryLayer;
13use tracing_subscriber::layer::{Filter, SubscriberExt};
14use tracing_subscriber::{EnvFilter, Layer, Registry};
15
16use crate::tracing::ErrorSpanExt;
17
18static TRACER_PROVIDER: OnceLock<SdkTracerProvider> = OnceLock::new();
23
24#[derive(Clone, Default)]
26pub struct ResourceConfig {
27 service_name: Option<&'static str>,
28 attributes: Vec<(&'static str, &'static str)>,
29}
30
31impl ResourceConfig {
32 #[must_use]
33 pub fn with_name(mut self, service_name: &'static str) -> Self {
34 self.service_name = Some(service_name);
35 self
36 }
37
38 #[must_use]
39 pub fn with_attribute(mut self, key: &'static str, value: &'static str) -> Self {
40 self.attributes.push((key, value));
41 self
42 }
43}
44
45#[derive(Clone)]
47pub enum OpenTelemetry {
48 Enabled(ResourceConfig),
49 Disabled,
50}
51
52impl OpenTelemetry {
53 pub fn enabled() -> Self {
54 OpenTelemetry::Enabled(ResourceConfig::default())
55 }
56
57 pub fn from_env() -> Self {
58 if otlp_endpoint_configured() {
59 OpenTelemetry::enabled()
60 } else {
61 OpenTelemetry::Disabled
62 }
63 }
64
65 #[must_use]
66 pub fn with_name(self, service_name: &'static str) -> Self {
67 match self {
68 OpenTelemetry::Enabled(config) => {
69 OpenTelemetry::Enabled(config.with_name(service_name))
70 },
71 OpenTelemetry::Disabled => OpenTelemetry::Disabled,
72 }
73 }
74
75 #[must_use]
76 pub fn with_attribute(self, key: &'static str, value: &'static str) -> Self {
77 match self {
78 OpenTelemetry::Enabled(config) => {
79 OpenTelemetry::Enabled(config.with_attribute(key, value))
80 },
81 OpenTelemetry::Disabled => OpenTelemetry::Disabled,
82 }
83 }
84
85 fn is_enabled(&self) -> bool {
86 matches!(self, OpenTelemetry::Enabled(_))
87 }
88
89 fn resource_config(self) -> Option<ResourceConfig> {
90 match self {
91 OpenTelemetry::Enabled(config) => Some(config),
92 OpenTelemetry::Disabled => None,
93 }
94 }
95}
96
97#[derive(Clone)]
99pub struct TracingConfig {
100 pub open_telemetry: OpenTelemetry,
101 pub stdout_filter: String,
102 pub otel_filter: String,
103}
104
105impl TracingConfig {
106 #[must_use]
107 pub fn from_env(open_telemetry: OpenTelemetry) -> Self {
108 Self {
109 open_telemetry,
110 stdout_filter: filter_env_or_default(
111 "MIDEN_STDOUT_FILTER",
112 "info,user=debug,miden_prover=warn",
113 ),
114 otel_filter: filter_env_or_default(
115 "MIDEN_OTEL_FILTER",
116 "info,axum::rejection=trace,miden_prover=warn",
117 ),
118 }
119 }
120}
121
122fn filter_env_or_default(var: &str, default: &str) -> String {
123 std::env::var(var)
124 .ok()
125 .filter(|value| !value.trim().is_empty())
126 .or_else(|| {
127 std::env::var(EnvFilter::DEFAULT_ENV)
128 .ok()
129 .filter(|value| !value.trim().is_empty())
130 })
131 .unwrap_or_else(|| default.to_owned())
132}
133
134fn filter_from_string<S>(
135 filter: &str,
136) -> anyhow::Result<Box<dyn Filter<S> + Send + Sync + 'static>> {
137 use tracing_subscriber::filter::FilterExt;
138
139 Ok(FilterExt::boxed(EnvFilter::from_str(filter)?))
140}
141
142pub struct OtelGuard {
145 tracer_provider: SdkTracerProvider,
146}
147
148impl Drop for OtelGuard {
149 fn drop(&mut self) {
150 if let Err(err) = self.tracer_provider.shutdown() {
151 eprintln!("{err:?}");
152 }
153 }
154}
155
156pub fn setup_tracing(otel: OpenTelemetry) -> anyhow::Result<Option<OtelGuard>> {
170 setup_tracing_with_config(TracingConfig::from_env(otel))
171}
172
173pub fn setup_tracing_with_config(config: TracingConfig) -> anyhow::Result<Option<OtelGuard>> {
178 let TracingConfig {
179 open_telemetry: otel,
180 stdout_filter,
181 otel_filter,
182 } = config;
183
184 if otel.is_enabled() {
185 opentelemetry::global::set_text_map_propagator(TraceContextPropagator::new());
186 }
187
188 let tracer_provider = if otel.is_enabled() {
192 let provider = init_tracer_provider(
193 otel.resource_config()
194 .expect("resource config is set when OpenTelemetry is enabled"),
195 )?;
196
197 TRACER_PROVIDER
200 .set(provider.clone())
201 .expect("setup_tracing should only be called once");
202
203 Some(provider)
204 } else {
205 None
206 };
207 let otel_layer = tracer_provider.as_ref().map(|provider| {
208 OpenTelemetryLayer::new(provider.tracer("tracing-otel-subscriber")).boxed()
209 });
210
211 let subscriber = Registry::default()
212 .with(stdout_layer().with_filter(filter_from_string(&stdout_filter)?))
213 .with(otel_layer.with_filter(filter_from_string(&otel_filter)?));
214 tracing::subscriber::set_global_default(subscriber).map_err(Into::<anyhow::Error>::into)?;
215
216 let default_hook = std::panic::take_hook();
219 std::panic::set_hook(Box::new(move |info| {
220 tracing::error!(panic = true, info = %info, "panic");
221
222 let info_str = info.to_string();
224 let wrapped = anyhow::Error::msg(info_str);
225 tracing::Span::current().set_error(wrapped.as_ref());
226
227 if let Some(provider) = TRACER_PROVIDER.get() {
230 if let Err(err) = provider.force_flush() {
231 eprintln!("Failed to flush traces on panic: {err:?}");
232 }
233 }
234
235 default_hook(info);
237 }));
238
239 Ok(tracer_provider.map(|tracer_provider| OtelGuard { tracer_provider }))
240}
241
242fn init_tracer_provider(resource_config: ResourceConfig) -> anyhow::Result<SdkTracerProvider> {
243 let exporter = opentelemetry_otlp::SpanExporter::builder()
244 .with_tonic()
245 .with_tls_config(tonic::transport::ClientTlsConfig::new().with_enabled_roots())
246 .build()?;
247 let resource = resource(resource_config);
248
249 Ok(opentelemetry_sdk::trace::SdkTracerProvider::builder()
250 .with_resource(resource)
251 .with_batch_exporter(exporter)
252 .build())
253}
254
255fn resource(config: ResourceConfig) -> Resource {
256 let detected_resource = Resource::builder_empty()
257 .with_detector(Box::new(TelemetryResourceDetector))
258 .with_detector(Box::new(EnvResourceDetector::new()))
259 .build();
260
261 resource_from_detected(config, &detected_resource, otel_service_name_override())
262}
263
264fn resource_from_detected(
265 config: ResourceConfig,
266 detected_resource: &Resource,
267 service_name_override: Option<Value>,
268) -> Resource {
269 const SERVICE_NAME: &str = "service.name";
270 const SERVICE_NAMESPACE: &str = "service.namespace";
271
272 let mut attributes =
273 std::collections::BTreeMap::from([(SERVICE_NAMESPACE.to_string(), Value::from("miden"))]);
274
275 if let Some(service_name) = config.service_name {
276 attributes.insert(SERVICE_NAME.to_string(), Value::from(service_name));
277 }
278
279 for (key, value) in config.attributes {
280 attributes.insert(key.to_string(), Value::from(value));
281 }
282
283 for (key, value) in detected_resource {
286 attributes.insert(key.as_str().to_string(), value.clone());
287 }
288
289 if let Some(service_name) = service_name_override {
290 attributes.insert(SERVICE_NAME.to_string(), service_name);
291 }
292
293 Resource::builder_empty()
294 .with_attributes(attributes.into_iter().map(|(key, value)| KeyValue::new(key, value)))
295 .build()
296}
297
298fn otel_service_name_override() -> Option<Value> {
299 std::env::var("OTEL_SERVICE_NAME")
300 .ok()
301 .filter(|value| !value.is_empty())
302 .map(Value::from)
303}
304
305fn otlp_endpoint_configured() -> bool {
306 ["OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "OTEL_EXPORTER_OTLP_ENDPOINT"]
307 .into_iter()
308 .any(|key| std::env::var(key).is_ok_and(|value| !value.trim().is_empty()))
309}
310
311#[cfg(feature = "testing")]
320pub fn setup_test_tracing() -> anyhow::Result<(
321 tokio::sync::mpsc::UnboundedReceiver<opentelemetry_sdk::trace::SpanData>,
322 tokio::sync::mpsc::UnboundedReceiver<()>,
323)> {
324 let (exporter, rx_export, rx_shutdown) =
325 opentelemetry_sdk::testing::trace::new_tokio_test_exporter();
326
327 let tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
328 .with_batch_exporter(exporter)
329 .build();
330 let otel_layer =
331 OpenTelemetryLayer::new(tracer_provider.tracer("tracing-otel-subscriber")).boxed();
332 let subscriber = Registry::default()
333 .with(stdout_layer().with_filter(filter_from_string("debug")?))
334 .with(otel_layer.with_filter(filter_from_string("info,axum::rejection=trace")?));
335 tracing::subscriber::set_global_default(subscriber)?;
336 Ok((rx_export, rx_shutdown))
337}
338
339#[cfg(not(feature = "tracing-forest"))]
340fn stdout_layer<S>() -> Box<dyn tracing_subscriber::Layer<S> + Send + Sync + 'static>
341where
342 S: Subscriber,
343 for<'a> S: tracing_subscriber::registry::LookupSpan<'a>,
344{
345 tracing_subscriber::fmt::layer()
346 .compact()
347 .with_level(true)
348 .with_file(false)
349 .with_line_number(false)
350 .with_target(false)
351 .boxed()
352}
353
354#[cfg(feature = "tracing-forest")]
355fn stdout_layer<S>() -> Box<dyn tracing_subscriber::Layer<S> + Send + Sync + 'static>
356where
357 S: Subscriber,
358 for<'a> S: tracing_subscriber::registry::LookupSpan<'a>,
359{
360 tracing_forest::ForestLayer::default().boxed()
361}
362
363#[cfg(test)]
364mod tests {
365 use opentelemetry::Key;
366
367 use super::*;
368
369 #[test]
370 fn resource_uses_configured_defaults() {
371 let detected_resource = Resource::builder_empty()
372 .with_attributes([KeyValue::new("telemetry.sdk.language", "rust")])
373 .build();
374
375 let resource = resource_from_detected(
376 ResourceConfig::default()
377 .with_name("node")
378 .with_attribute("miden.node.role", "sequencer"),
379 &detected_resource,
380 None,
381 );
382
383 assert_eq!(resource_value(&resource, "service.name"), Some(Value::from("node")),);
384 assert_eq!(resource_value(&resource, "service.namespace"), Some(Value::from("miden")),);
385 assert_eq!(resource_value(&resource, "miden.node.role"), Some(Value::from("sequencer")),);
386 assert_eq!(resource_value(&resource, "telemetry.sdk.language"), Some(Value::from("rust")),);
387 }
388
389 #[test]
390 fn resource_prefers_detected_attributes_over_configured_defaults() {
391 let detected_resource = Resource::builder_empty()
392 .with_attributes([
393 KeyValue::new("service.name", "custom-node"),
394 KeyValue::new("service.namespace", "custom-namespace"),
395 KeyValue::new("miden.node.role", "custom-role"),
396 ])
397 .build();
398
399 let resource = resource_from_detected(
400 ResourceConfig::default()
401 .with_name("node")
402 .with_attribute("miden.node.role", "sequencer"),
403 &detected_resource,
404 None,
405 );
406
407 assert_eq!(resource_value(&resource, "service.name"), Some(Value::from("custom-node")),);
408 assert_eq!(
409 resource_value(&resource, "service.namespace"),
410 Some(Value::from("custom-namespace")),
411 );
412 assert_eq!(resource_value(&resource, "miden.node.role"), Some(Value::from("custom-role")),);
413 }
414
415 #[test]
416 fn resource_prefers_explicit_service_name_override() {
417 let detected_resource = Resource::builder_empty()
418 .with_attributes([KeyValue::new("service.name", "resource-attribute-node")])
419 .build();
420
421 let resource = resource_from_detected(
422 ResourceConfig::default().with_name("node"),
423 &detected_resource,
424 Some(Value::from("service-env-node")),
425 );
426
427 assert_eq!(
428 resource_value(&resource, "service.name"),
429 Some(Value::from("service-env-node")),
430 );
431 }
432
433 fn resource_value(resource: &Resource, key: &'static str) -> Option<Value> {
434 resource.get(&Key::from_static_str(key))
435 }
436}