Skip to main content

otel_rs/
lib.rs

1//! Modern OpenTelemetry observability helpers for Rust.
2//!
3//! Provides composable configuration, dynamic layer composition, and
4//! async-aware shutdown for traces, logs, and metrics.
5//!
6//! # Quick Start
7//!
8//! ```rust,ignore
9//! use otel_rs::OtelConfig;
10//!
11//! #[tokio::main]
12//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
13//!     let mut guard = OtelConfig::builder()
14//!         .service_name("my-service")
15//!         .service_version("1.0.0")
16//!         .allow_crate("my_service")
17//!         .init()
18//!         .await?;
19//!
20//!     tracing::info!("Hello from my-service!");
21//!
22//!     // Graceful async shutdown (or let Drop handle it).
23//!     guard.shutdown().await?;
24//!     Ok(())
25//! }
26//! ```
27//!
28//! # Error Recording
29//!
30//! ```rust,ignore
31//! use otel_rs::{SpanExt, InstrumentedResult};
32//!
33//! #[tracing::instrument]
34//! async fn work() -> Result<(), MyError> {
35//!     // Automatically records errors to the current span:
36//!     fallible_call().await.record_to_span()?;
37//!
38//!     // Or use the macro:
39//!     let val = otel_rs::try_record_return!(another_call().await);
40//!     Ok(())
41//! }
42//! ```
43
44pub mod config;
45pub(crate) mod env;
46pub mod error;
47pub mod filter;
48#[macro_use]
49pub mod macros;
50pub mod metrics;
51pub mod span;
52pub(crate) mod transport;
53
54// ── Re-exports ─────────────────────────────────────────────────────
55
56pub use config::{
57    ExporterConfig, ExporterConfigBuilder, LogLevel, MetricsConfig, MetricsConfigBuilder,
58    OtelConfig, OtelConfigBuilder, OtlpCredentials, OtlpProtocol, OutputFormat, SamplingStrategy,
59    TracingConfig, TracingConfigBuilder,
60};
61pub use error::{ErrorContext, OtelError, OtelResult};
62pub use filter::FilterBuilder;
63pub use metrics::Metrics;
64use opentelemetry::{KeyValue, trace::TracerProvider};
65use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
66use opentelemetry_sdk::{
67    Resource,
68    logs::SdkLoggerProvider,
69    trace::{RandomIdGenerator, Sampler, SdkTracerProvider},
70};
71pub use span::{InstrumentedResult, SpanExt, TimingContext};
72use tracing_opentelemetry::OpenTelemetryLayer;
73use tracing_subscriber::{Layer, Registry, fmt, layer::SubscriberExt, util::SubscriberInitExt};
74
75use crate::{config::SamplingStrategy as Sampling, filter::build_env_filter};
76
77// ── OtelGuard ──────────────────────────────────────────────────────
78
79/// Guard managing the lifecycle of the observability stack.
80///
81/// **Keep alive** for the duration of your application. Dropping the
82/// guard triggers a synchronous best-effort shutdown. For proper error
83/// reporting, call [`shutdown()`](OtelGuard::shutdown) explicitly.
84pub struct OtelGuard {
85    trace_provider: Option<SdkTracerProvider>,
86    log_provider: Option<SdkLoggerProvider>,
87    metrics: Option<Metrics>,
88    service_name: String,
89}
90
91impl OtelGuard {
92    /// Get the metrics handle (if metrics are enabled).
93    pub const fn metrics(&self) -> Option<&Metrics> {
94        self.metrics.as_ref()
95    }
96
97    /// Get the service name.
98    pub fn service_name(&self) -> &str {
99        &self.service_name
100    }
101
102    /// Flush all pending telemetry without shutting down.
103    pub fn flush(&self) {
104        if let Some(ref tp) = self.trace_provider {
105            let _ = tp.force_flush();
106        }
107        if let Some(ref lp) = self.log_provider {
108            let _ = lp.force_flush();
109        }
110    }
111
112    /// Gracefully shut down all telemetry providers.
113    ///
114    /// This is preferred over relying on [`Drop`] because errors can
115    /// be reported. After calling this, the [`Drop`] impl becomes a
116    /// no-op.
117    ///
118    /// # Errors
119    ///
120    /// Returns an error if any provider fails to shut down.
121    pub async fn shutdown(&mut self) -> OtelResult<()> {
122        // Take ownership to prevent double-shutdown in Drop.
123        if let Some(tp) = self.trace_provider.take() {
124            tp.shutdown()
125                .map_err(|e| OtelError::init(format!("trace shutdown: {e}")))?;
126        }
127        if let Some(lp) = self.log_provider.take() {
128            lp.shutdown()
129                .map_err(|e| OtelError::init(format!("log shutdown: {e}")))?;
130        }
131        if let Some(m) = self.metrics.take() {
132            m.shutdown()?;
133        }
134        Ok(())
135    }
136}
137
138impl Drop for OtelGuard {
139    fn drop(&mut self) {
140        // Sync fallback — only runs if async shutdown() was not called.
141        if let Some(ref tp) = self.trace_provider {
142            let _ = tp.shutdown();
143        }
144        if let Some(ref lp) = self.log_provider {
145            let _ = lp.shutdown();
146        }
147        if let Some(ref m) = self.metrics {
148            let _ = m.shutdown();
149        }
150    }
151}
152
153// ── Builder → init bridge ──────────────────────────────────────────
154
155impl OtelConfigBuilder {
156    /// Build configuration and initialize the observability stack.
157    ///
158    /// # Errors
159    ///
160    /// Returns an error if exporter building or subscriber
161    /// initialization fails.
162    pub async fn init(self) -> OtelResult<OtelGuard> {
163        init_with_config(self.build()).await
164    }
165}
166
167// ── Public initialization ──────────────────────────────────────────
168
169/// Initialize the observability stack with a pre-built configuration.
170///
171/// Prefer [`OtelConfigBuilder::init()`] for a fluent API.
172///
173/// # Errors
174///
175/// Returns an error if exporter building or subscriber initialization
176/// fails.
177pub async fn init_with_config(config: OtelConfig) -> OtelResult<OtelGuard> {
178    // ── Build OTel resource ────────────────────────────────────────
179    let mut rb = Resource::builder()
180        .with_service_name(config.service_name.clone())
181        .with_attribute(KeyValue::new(
182            "service.version",
183            config.service_version.clone(),
184        ))
185        .with_attribute(KeyValue::new(
186            "deployment.environment.name",
187            config.environment.clone(),
188        ));
189
190    if let Some(ref ns) = config.service_namespace {
191        rb = rb.with_attribute(KeyValue::new("service.namespace", ns.clone()));
192    }
193    if let Some(ref id) = config.service_instance_id {
194        rb = rb.with_attribute(KeyValue::new("service.instance.id", id.clone()));
195    }
196    for (key, value) in &config.custom_attributes {
197        rb = rb.with_attribute(KeyValue::new(key.clone(), value.clone()));
198    }
199
200    let resource = rb.build();
201
202    // ── Collect layers dynamically ─────────────────────────────────
203    let mut layers: Vec<Box<dyn Layer<Registry> + Send + Sync>> = Vec::new();
204
205    // 1. Global env filter.
206    layers.push(Box::new(build_env_filter(&config)));
207
208    // 2. Console output.
209    if config.enable_console_output {
210        match config.output_format {
211            config::OutputFormat::Pretty => {
212                layers.push(Box::new(fmt::layer().pretty()));
213            }
214            config::OutputFormat::Compact => {
215                layers.push(Box::new(fmt::layer()));
216            }
217            config::OutputFormat::Json => {
218                layers.push(Box::new(fmt::layer().json()));
219            }
220        }
221    }
222
223    // 3. Trace provider + OpenTelemetry layer.
224    let trace_provider = if let Some(ref tc) = config.tracing {
225        let exporter = transport::build_span_exporter(&config.exporter)?;
226
227        let sampler = match tc.sampling {
228            Sampling::AlwaysOn => Sampler::AlwaysOn,
229            Sampling::AlwaysOff => Sampler::AlwaysOff,
230            Sampling::TraceIdRatio(r) => Sampler::TraceIdRatioBased(r),
231            Sampling::ParentBased => Sampler::ParentBased(Box::new(Sampler::AlwaysOn)),
232        };
233
234        let provider = SdkTracerProvider::builder()
235            .with_id_generator(RandomIdGenerator::default())
236            .with_resource(resource.clone())
237            .with_sampler(sampler)
238            .with_batch_exporter(exporter)
239            .build();
240
241        let tracer = provider.tracer(config.service_name.clone());
242        layers.push(Box::new(OpenTelemetryLayer::new(tracer)));
243
244        Some(provider)
245    } else {
246        None
247    };
248
249    // 4. Log provider + bridge layer.
250    let log_provider = if config.logging {
251        let exporter = transport::build_log_exporter(&config.exporter)?;
252
253        let provider = SdkLoggerProvider::builder()
254            .with_resource(resource.clone())
255            .with_batch_exporter(exporter)
256            .build();
257
258        layers.push(Box::new(OpenTelemetryTracingBridge::new(&provider)));
259
260        Some(provider)
261    } else {
262        None
263    };
264
265    // 5. Metrics (not a subscriber layer — independent provider).
266    let metrics = if config.metrics.is_some() {
267        Some(Metrics::new(&config, resource)?)
268    } else {
269        None
270    };
271
272    // ── Initialize the global subscriber ───────────────────────────
273    tracing_subscriber::registry()
274        .with(layers)
275        .try_init()
276        .map_err(|_| OtelError::SubscriberAlreadySet)?;
277
278    tracing::info!(
279        service.name = %config.service_name,
280        service.version = %config.service_version,
281        environment = %config.environment,
282        "Observability initialized"
283    );
284
285    Ok(OtelGuard {
286        trace_provider,
287        log_provider,
288        metrics,
289        service_name: config.service_name,
290    })
291}