dora_tracing/
lib.rs

1//! Enable tracing using OpenTelemetry with OTLP.
2//!
3//! This module initializes a tracing propagator for Rust code that requires tracing, and is
4//! able to serialize and deserialize context that has been sent via the middleware.
5//! Supports any OTLP-compatible backend (Jaeger, Zipkin, Tempo, etc.).
6
7use std::path::Path;
8
9use eyre::Context as EyreContext;
10use opentelemetry::trace::TracerProvider;
11use opentelemetry_sdk::metrics::SdkMeterProvider;
12use opentelemetry_sdk::trace::SdkTracerProvider;
13use tracing::metadata::LevelFilter;
14
15use tracing_opentelemetry::{MetricsLayer, OpenTelemetryLayer};
16use tracing_subscriber::{
17    EnvFilter, Layer, filter::FilterExt, prelude::__tracing_subscriber_SubscriberExt,
18};
19
20use tracing_subscriber::Registry;
21pub mod metrics;
22pub mod telemetry;
23
24/// Setup tracing with a default configuration.
25///
26/// This will set up a global subscriber that logs to stdout with a filter level of "warn".
27///
28/// Should **ONLY** be used in `DoraNode` implementations.
29pub fn set_up_tracing(name: &str) -> eyre::Result<()> {
30    TracingBuilder::new(name)
31        .with_stdout("warn", false)
32        .build()
33        .wrap_err(format!(
34            "failed to set tracing global subscriber for {name}"
35        ))?;
36    Ok(())
37}
38
39pub struct OtelGuard {
40    tracer_provider: SdkTracerProvider,
41    meter_provider: SdkMeterProvider,
42}
43
44#[must_use = "call `build` to finalize the tracing setup"]
45pub struct TracingBuilder {
46    name: String,
47    layers: Vec<Box<dyn Layer<Registry> + Send + Sync>>,
48    pub guard: Option<OtelGuard>,
49}
50
51impl TracingBuilder {
52    pub fn new(name: impl Into<String>) -> Self {
53        Self {
54            name: name.into(),
55            layers: Vec::new(),
56            guard: None,
57        }
58    }
59
60    /// Add a layer that write logs to the [std::io::stdout] with the given filter.
61    ///
62    /// **DO NOT** use this in `DoraNode` implementations,
63    /// it uses [std::io::stdout] which is synchronous
64    /// and might block the logging thread.
65    pub fn with_stdout(mut self, filter: impl AsRef<str>, json: bool) -> Self {
66        let parsed = EnvFilter::builder()
67            .parse_lossy(filter)
68            .add_directive("hyper=off".parse().unwrap())
69            .add_directive("tonic=off".parse().unwrap())
70            .add_directive("h2=off".parse().unwrap())
71            .add_directive("reqwest=off".parse().unwrap());
72        let env_filter = EnvFilter::from_default_env().or(parsed);
73        let layer = tracing_subscriber::fmt::layer()
74            .compact()
75            .with_writer(std::io::stdout);
76
77        if json {
78            let layer = layer.json().with_filter(env_filter);
79            self.layers.push(layer.boxed());
80        } else {
81            let layer = layer.with_filter(env_filter);
82            self.layers.push(layer.boxed());
83        };
84        self
85    }
86
87    /// Add a layer that write logs to a file with the given name and filter.
88    pub fn with_file(
89        mut self,
90        file_name: impl Into<String>,
91        filter: LevelFilter,
92    ) -> eyre::Result<Self> {
93        let file_name = file_name.into();
94        let out_dir = Path::new("out");
95        std::fs::create_dir_all(out_dir).context("failed to create `out` directory")?;
96        let path = out_dir.join(file_name).with_extension("txt");
97        let file = std::fs::OpenOptions::new()
98            .create(true)
99            .append(true)
100            .open(path)
101            .context("failed to create log file")?;
102        let layer = tracing_subscriber::fmt::layer()
103            .with_ansi(false)
104            .json()
105            .with_writer(file)
106            .with_filter(filter);
107        self.layers.push(layer.boxed());
108        Ok(self)
109    }
110
111    /// Add OpenTelemetry tracing layer with OTLP exporter.
112    ///
113    /// Reads the OTLP endpoint from `DORA_OTLP_ENDPOINT` environment variable.
114    /// If not set, falls back to `DORA_JAEGER_TRACING` for backward compatibility.
115    ///
116    /// The endpoint should be in the format: "http://localhost:4317"
117    pub fn with_otlp_tracing(mut self) -> eyre::Result<Self> {
118        let endpoint = std::env::var("DORA_OTLP_ENDPOINT")
119            .or_else(|_| std::env::var("DORA_JAEGER_TRACING"))
120            .wrap_err("DORA_OTLP_ENDPOINT or DORA_JAEGER_TRACING environment variable not set")?;
121
122        // Initialize OTLP tracing - this returns a tracer and sets the global provider
123        let sdk_tracer_provider = crate::telemetry::init_tracing(&self.name, &endpoint);
124        let meter_provider = metrics::init_meter_provider();
125
126        // TODO: Maybe this needs to be removed in favor of application level global.
127        // global::set_meter_provider(meter_provider.clone());
128        // Use the specific tracer instance returned from init_tracing
129        let tracer = sdk_tracer_provider.tracer("tracing-otel-subscriber");
130
131        let guard = OtelGuard {
132            tracer_provider: sdk_tracer_provider,
133            meter_provider: meter_provider.clone(),
134        };
135
136        self.guard = Some(guard);
137        self.layers.push(MetricsLayer::new(meter_provider).boxed());
138        let filter_otel = EnvFilter::new("trace")
139            .add_directive("hyper=off".parse().unwrap())
140            .add_directive("tonic=off".parse().unwrap())
141            .add_directive("h2=off".parse().unwrap())
142            .add_directive("reqwest=off".parse().unwrap());
143        self.layers.push(
144            OpenTelemetryLayer::new(tracer)
145                .with_filter(filter_otel)
146                .boxed(),
147        );
148        Ok(self)
149    }
150
151    /// Legacy method name for backward compatibility.
152    #[deprecated(since = "0.4.0", note = "Use `with_otlp_tracing` instead")]
153    pub fn with_jaeger_tracing(self) -> eyre::Result<Self> {
154        self.with_otlp_tracing()
155    }
156
157    pub fn add_layer<L>(mut self, layer: L) -> Self
158    where
159        L: Layer<Registry> + Send + Sync + 'static,
160    {
161        self.layers.push(layer.boxed());
162        self
163    }
164
165    pub fn with_layers<I, L>(mut self, layers: I) -> Self
166    where
167        I: IntoIterator<Item = L>,
168        L: Layer<Registry> + Send + Sync + 'static,
169    {
170        for layer in layers {
171            self.layers.push(layer.boxed());
172        }
173        self
174    }
175
176    pub fn build(self) -> eyre::Result<()> {
177        let registry = Registry::default().with(self.layers);
178
179        // TODO: Maybe this needs to be removed in favor of application level global.
180        tracing::subscriber::set_global_default(registry).context(format!(
181            "failed to set tracing global subscriber for {}",
182            self.name
183        ))
184    }
185}
186
187impl Drop for OtelGuard {
188    fn drop(&mut self) {
189        self.meter_provider.force_flush().ok();
190        self.meter_provider.shutdown().ok();
191        self.tracer_provider.force_flush().ok();
192        self.tracer_provider.shutdown().ok();
193    }
194}