Skip to main content

barrzen_axum_obs/
lib.rs

1//! Barrzen Axum Observability
2//!
3//! Handles tracing setup and OpenTelemetry integration.
4
5use barrzen_axum_core::{Config, LogBackend, LogFormat};
6use tracing_subscriber::{
7    fmt::format::FmtSpan, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter,
8};
9
10#[cfg(feature = "otel")]
11use tracing_subscriber::Layer;
12#[cfg(feature = "otel")]
13use std::sync::OnceLock;
14
15#[cfg(feature = "otel")]
16static OTEL_PROVIDER: OnceLock<opentelemetry_sdk::trace::SdkTracerProvider> = OnceLock::new();
17
18/// Initialize tracing based on configuration
19///
20/// # Errors
21/// Returns error if tracing subscriber setup fails.
22pub fn init_tracing(config: &Config) -> anyhow::Result<()> {
23    match config.logging.log_backend {
24        LogBackend::Tracing => {
25            let env_filter = EnvFilter::try_from_default_env()
26                .unwrap_or_else(|_| EnvFilter::new(&config.logging.log_level));
27            init_tracing_subscriber(config, env_filter)
28        }
29        LogBackend::FastLog => init_fast_log(config),
30    }
31}
32
33/// Shutdown observability
34///
35/// Flushes pending spans (relevant for OTEL).
36pub fn shutdown() {
37    #[cfg(feature = "otel")]
38    {
39        if let Some(provider) = OTEL_PROVIDER.get() {
40            let _ = provider.shutdown();
41        }
42    }
43}
44
45fn init_tracing_subscriber(config: &Config, env_filter: EnvFilter) -> anyhow::Result<()> {
46    // Console layer
47    let fmt_layer = tracing_subscriber::fmt::layer()
48        .with_target(config.logging.log_include_target)
49        .with_span_events(FmtSpan::NONE);
50
51    // Apply format
52    let registry = tracing_subscriber::registry().with(env_filter);
53
54    match config.logging.log_format {
55        LogFormat::Pretty => {
56            let registry = registry.with(
57                fmt_layer
58                    .pretty()
59                    .with_file(config.logging.log_include_fileline)
60                    .with_line_number(config.logging.log_include_fileline),
61            );
62
63            #[cfg(feature = "otel")]
64            if config.features.feature_otel {
65                let otel_layer = init_otel_layer(config)?;
66                registry.with(otel_layer).try_init()?;
67                return Ok(());
68            }
69
70            registry.try_init()?;
71        }
72        LogFormat::Compact => {
73            let registry = registry.with(
74                fmt_layer
75                    .compact()
76                    .with_ansi(false)
77                    .with_file(config.logging.log_include_fileline)
78                    .with_line_number(config.logging.log_include_fileline),
79            );
80
81            #[cfg(feature = "otel")]
82            if config.features.feature_otel {
83                let otel_layer = init_otel_layer(config)?;
84                registry.with(otel_layer).try_init()?;
85                return Ok(());
86            }
87
88            registry.try_init()?;
89        }
90        LogFormat::Json => {
91            let registry = registry.with(
92                fmt_layer
93                    .json()
94                    .with_file(config.logging.log_include_fileline)
95                    .with_line_number(config.logging.log_include_fileline),
96            );
97
98            #[cfg(feature = "otel")]
99            if config.features.feature_otel {
100                let otel_layer = init_otel_layer(config)?;
101                registry.with(otel_layer).try_init()?;
102                return Ok(());
103            }
104
105            registry.try_init()?;
106        }
107    }
108
109    Ok(())
110}
111
112fn init_fast_log(config: &Config) -> anyhow::Result<()> {
113    #[cfg(feature = "fast-log")]
114    {
115        use fast_log::config::Config as FastLogConfig;
116
117        if config.features.feature_otel {
118            anyhow::bail!("LOG_BACKEND=fast_log is not compatible with FEATURE_OTEL=true");
119        }
120
121        if let Err(err) = fast_log::init(FastLogConfig::new().console()) {
122            let message = err.to_string();
123            if message.contains("logging system was already initialized") {
124                anyhow::bail!("fast_log init failed because another logger is already set. Ensure init_tracing runs before any other logger initialization.");
125            }
126            return Err(err.into());
127        }
128        log::set_max_level(resolve_log_level(config));
129        return Ok(());
130    }
131
132    #[cfg(not(feature = "fast-log"))]
133    {
134        let _ = config;
135        anyhow::bail!("LOG_BACKEND=fast_log requires the \"fast-log\" feature on barrzen-axum-obs")
136    }
137}
138
139#[cfg(feature = "fast-log")]
140fn resolve_log_level(config: &Config) -> log::LevelFilter {
141    let mut level = parse_log_level(&config.logging.log_level).unwrap_or(log::LevelFilter::Info);
142
143    if let Ok(rust_log) = std::env::var("RUST_LOG") {
144        for directive in rust_log.split(',') {
145            let directive = directive.trim();
146            if directive.is_empty() {
147                continue;
148            }
149            let level_str = directive
150                .split_once('=')
151                .map_or(directive, |(_, level)| level);
152            if let Some(parsed) = parse_log_level(level_str) {
153                level = level.max(parsed);
154            }
155        }
156    }
157
158    level
159}
160
161#[cfg(feature = "fast-log")]
162fn parse_log_level(value: &str) -> Option<log::LevelFilter> {
163    match value.trim().to_lowercase().as_str() {
164        "off" => Some(log::LevelFilter::Off),
165        "error" => Some(log::LevelFilter::Error),
166        "warn" | "warning" => Some(log::LevelFilter::Warn),
167        "info" => Some(log::LevelFilter::Info),
168        "debug" => Some(log::LevelFilter::Debug),
169        "trace" => Some(log::LevelFilter::Trace),
170        _ => None,
171    }
172}
173
174// OpenTelemetry Setup
175
176#[cfg(feature = "otel")]
177fn init_otel_layer<S>(config: &Config) -> anyhow::Result<impl Layer<S>>
178where
179    S: tracing::Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
180{
181    use opentelemetry::{global, trace::TracerProvider as _};
182    use opentelemetry_otlp::WithExportConfig;
183    use opentelemetry_sdk::{propagation::TraceContextPropagator, trace as sdktrace, Resource};
184    use tracing_opentelemetry::OpenTelemetryLayer;
185
186    // Set global propagator
187    global::set_text_map_propagator(TraceContextPropagator::new());
188
189    let app_name = &config.app.app_name;
190    let otel_endpoint = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT")
191        .unwrap_or_else(|_| "http://localhost:4317".to_string());
192
193    // OTEL 0.31: Use SpanExporter::builder().with_tonic()
194    let exporter = opentelemetry_otlp::SpanExporter::builder()
195        .with_tonic()
196        .with_endpoint(otel_endpoint)
197        .build()?;
198
199    // OTEL 0.31: Use Resource::builder()
200    let resource = Resource::builder()
201        .with_service_name(app_name.clone())
202        .build();
203
204    let provider = sdktrace::SdkTracerProvider::builder()
205        .with_batch_exporter(exporter)
206        .with_resource(resource)
207        .build();
208    
209    // Set global provider
210    global::set_tracer_provider(provider.clone());
211    let _ = OTEL_PROVIDER.set(provider.clone());
212
213    let tracer = provider.tracer("barrzen-axum");
214
215    Ok(OpenTelemetryLayer::new(tracer))
216}