otlp_logger/
lib.rs

1//! # OpenTelemetry Logging with Tokio Tracing
2//! 
3//! This crate provides a convienent way to initialize the OpenTelemetry logger
4//! with otlp endpoint. It uses the [`opentelemetry`] and [`tracing`]
5//! crates to provide structured, context-aware logging for Rust applications.
6//! 
7//! Simply add the following to your `Cargo.toml`:
8//! ```toml
9//! [dependencies]
10//! tracing = "0.1"
11//! otlp-logger = "0.6"
12//! tokio = { version = "1", features = ["rt", "macros"] }
13//! ```
14//! 
15//! Because this crate uses the batching function of the OpenTelemetry SDK, it is
16//! required to use the `tokio` runtime. Due to this requirement, the [`tokio`] crate
17//! must be added as a dependency in your `Cargo.toml` file.
18//! 
19//! In your code initialize the logger with:
20//! ```rust
21//! use otlp_logger::OtlpLogger;
22//! 
23//! #[tokio::main]
24//! async fn main() {
25//!   // Initialize the OpenTelemetry logger using environment variables
26//!   let logger: OtlpLogger = otlp_logger::init().await.expect("Initialized logger");
27//!   // ... your application code
28//! 
29//!   // and optionally call open telemetry logger shutdown to make sure all the 
30//!   // data is sent to the configured endpoint before the application exits
31//!   logger.shutdown();
32//! }
33//! ```
34//! 
35//! If the `OTEL_EXPORTER_OTLP_ENDPOINT` environment variable is set, the
36//! OpenTelemetry logger will be used. Otherwise, the logger will default to
37//! only stdout.
38//! 
39//! The OpenTelemetry logger can be configured with the following environment
40//! variables:
41//!   - `OTEL_EXPORTER_OTLP_ENDPOINT`: The endpoint to send OTLP data to.
42//!   - `OTEL_SERVICE_NAME`: The name of the service.
43//!   - `OTEL_SERVICE_NAMESPACE`: The namespace of the service.
44//!   - `OTEL_SERVICE_VERSION`: The version of the service.
45//!   - `OTEL_SERVICE_INSTANCE_ID`: The instance ID of the service.
46//!   - `OTEL_DEPLOYMENT_ENVIRONMENT`: The deployment environment of the service.
47//! 
48//! The OpenTelemetry logger can also be configured with the `OtlpConfig` struct, which
49//! can be passed to the `init_with_config` function. The `OtlpConfig` struct can be built
50//! with the `OtlpConfigBuilder` struct.
51//! 
52//! Once the logger is initialized, you can use the [`tracing`] macros to log
53//! messages. For example:
54//! ```rust
55//! use tracing::{info, error};
56//! 
57//! #[tokio::main]
58//! async fn main() {
59//!    let logger = otlp_logger::init().await.expect("Initialized logger");
60//!    info!("This is an info message");
61//!    error!("This is an error message");
62//! }
63//! ```
64//! 
65//! Traces, metrics, and logs are sent to the configured OTLP endpoint. The traces,  
66//! metrics, and log levels are configured via the RUST_LOG environment variable.
67//! This behavior can be overridden by setting the `trace_level`, `metrics_level` or
68//! `log_level` fields in the `OtlpConfig` struct. You can control what
69//! goes to stdout by setting the `stdout_level` field. 
70//! ```rust
71//! use otlp_logger::{OtlpConfigBuilder, LevelFilter};
72//! 
73//! #[tokio::main]
74//! async fn main() {
75//!   let config = OtlpConfigBuilder::default()
76//!                  .otlp_endpoint("http://localhost:4317".to_string())
77//!                  .trace_level(LevelFilter::INFO)
78//!                  .log_level(LevelFilter::ERROR)
79//!                  .stdout_level(LevelFilter::OFF)
80//!                  .build()
81//!                  .expect("failed to create otlp config builder");
82//! 
83//!   let logger = otlp_logger::init_with_config(config).await.expect("failed to initialize logger");
84//! 
85//!   // ... your application code
86//! 
87//!   // shutdown the logger
88//!   logger.shutdown();
89//! }
90//! ```
91//! 
92//! [`tokio`]: https://crates.io/crates/tokio
93//! [`tracing`]: https://crates.io/crates/tracing
94//! [`opentelemetry`]: https://crates.io/crates/opentelemetry
95//!
96use derive_builder::*;
97use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
98use thiserror::Error;
99
100use anyhow::{Context, Result};
101
102use opentelemetry_otlp::OTEL_EXPORTER_OTLP_ENDPOINT;
103use opentelemetry_sdk::{error::{OTelSdkError, OTelSdkResult}, logs::SdkLoggerProvider, metrics::SdkMeterProvider, propagation::TraceContextPropagator, trace::SdkTracerProvider};
104use opentelemetry::trace::TracerProvider as _;
105
106use tracing_opentelemetry::{MetricsLayer, OpenTelemetryLayer};
107pub use tracing_subscriber::filter::LevelFilter;
108use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, *};
109
110mod resource;
111mod trace;
112mod metrics;
113mod logs;
114
115use resource::*;
116use trace::*;
117
118
119#[derive(Debug, Default, Builder)]
120#[builder(setter(into), default)]
121pub struct OtlpConfig {    
122    service_name: Option<String>,
123    service_namespace: Option<String>,
124    service_version: Option<String>,
125    service_instant_id: Option<String>,
126    deployment_environment: Option<String>,  
127    otlp_endpoint: Option<String>,   
128    trace_level: Option<LevelFilter>,   
129    metrics_level: Option<LevelFilter>,
130    log_level: Option<LevelFilter>,
131    stdout_level: Option<LevelFilter>,
132}
133
134impl OtlpConfig {
135    pub fn builder() -> OtlpConfigBuilder {
136        OtlpConfigBuilder::default()
137    }
138}
139
140#[derive(Debug)]
141pub struct EndpointLogger {
142    tracer_provider: SdkTracerProvider,
143    logger_provider: SdkLoggerProvider,
144    meter_provider: SdkMeterProvider
145}
146
147impl EndpointLogger {
148    pub async fn init(config: OtlpConfig) -> Result<Self> {
149
150        let otlp_endpoint = config.otlp_endpoint.as_ref().context("OTLP endpoint not set")?;
151        let resource = otel_resource(&config);
152        
153        let logger_provider = logs::otel_logs(otlp_endpoint, resource.clone())?;
154        let tracer_provider = otel_tracer(otlp_endpoint, resource.clone())?;
155        let meter_provider = metrics::otel_metrics(otlp_endpoint, resource.clone())?;   
156
157        let logs_layer = OpenTelemetryTracingBridge::new(&logger_provider)
158            .with_filter(define_filter_level(config.log_level));
159
160        let tracer = tracer_provider.tracer("otlp-tracing");
161        let tracer_layer = OpenTelemetryLayer::new(tracer)
162            .with_filter(define_filter_level(config.trace_level));
163
164        let metrics_layer = MetricsLayer::new(meter_provider.clone())
165            .with_filter(define_filter_level(config.metrics_level));
166
167        let stdout_layer = tracing_subscriber::fmt::layer()
168            .compact()
169            .with_file(true)
170            .with_line_number(true)
171            .with_filter(define_filter_level(config.stdout_level.or_else(||config.log_level)));
172        
173        tracing_subscriber::registry()
174            .with(stdout_layer)
175            .with(tracer_layer)
176            .with(metrics_layer)
177            .with(logs_layer)
178            .try_init()
179            .context("Could not init tracing registry")?;
180
181        Ok(EndpointLogger {
182            tracer_provider,
183            logger_provider,
184            meter_provider
185        }) 
186
187    }
188
189    pub fn shutdown(&self) {
190        let mut shutdown_errors = Vec::new();
191        if let Some(err) = shutdown_helper(self.tracer_provider.shutdown()) {
192            shutdown_errors.push(err);
193        }
194        if let Some(err) = shutdown_helper(self.logger_provider.shutdown()) {
195            shutdown_errors.push(err);
196        }
197        if let Some(err) = shutdown_helper(self.meter_provider.shutdown()) {
198            shutdown_errors.push(err);
199        }
200        if !shutdown_errors.is_empty() {
201            eprintln!("Errors shutting down providers: {:?}", shutdown_errors);
202        }
203    }
204}
205
206fn shutdown_helper(result: OTelSdkResult) -> Option<OTelSdkError> {
207    match result {
208        Ok(_) | Err(OTelSdkError::AlreadyShutdown) => None,
209        Err(err) => {
210            Some(err)
211        }         
212    }
213}
214
215#[derive(Debug)]
216pub struct StdoutOnlyLogger;
217
218impl StdoutOnlyLogger {
219    pub fn init() -> Result<Self> {
220        let stdout_layer = tracing_subscriber::fmt::layer()
221            .compact()
222            .with_file(true)
223            .with_line_number(true)
224            .with_filter(define_filter_level(None));
225
226        tracing_subscriber::registry()
227            .with(stdout_layer)
228            .try_init()
229            .context("Could not init tracing registry")?;
230
231        opentelemetry::global::set_text_map_propagator(TraceContextPropagator::new());
232        Ok(StdoutOnlyLogger)
233    }
234}
235
236
237fn define_filter_level(level: Option<LevelFilter>) -> EnvFilter {
238    match level {
239        Some(l) => EnvFilter::default().add_directive(l.into()),
240        None => EnvFilter::from_default_env(),
241    }
242}
243
244#[derive(Debug)]
245pub enum OtlpLogger {
246    WithEndpoint(EndpointLogger),
247    StdoutOnly(StdoutOnlyLogger),
248}
249
250impl OtlpLogger {
251    pub async fn init_with_config(config: OtlpConfig) -> Result<Self, TryInitError> {
252        if config.otlp_endpoint.is_some() {
253            let logger = EndpointLogger::init(config).await.map_err(|e| TryInitError {
254                msg: "Failed to initialize OTLP Endpoint Logger".to_string(),
255                source: e,
256            })?;
257            Ok(OtlpLogger::WithEndpoint(logger))
258        } else {
259            let logger = StdoutOnlyLogger::init().map_err(|e| TryInitError {
260                msg: "Failed to initialize Stdout Only Logger".to_string(),
261                source: e,
262            })?;
263            Ok(OtlpLogger::StdoutOnly(logger))
264        }
265    }
266
267    pub async fn try_init() -> Result<Self, TryInitError> {
268        let endpoint = std::env::var(OTEL_EXPORTER_OTLP_ENDPOINT).ok();
269        let config = OtlpConfigBuilder::default()
270            .otlp_endpoint(endpoint)
271            .build()
272            .map_err(|e| TryInitError {
273                msg: "Failed to configure endpoint from environment".to_string(),
274                source: e.into(),
275            })?;
276        Self::init_with_config(config).await
277    }
278
279    pub fn shutdown(&self) {
280        match self {
281            OtlpLogger::WithEndpoint(logger) => logger.shutdown(),
282            OtlpLogger::StdoutOnly(_) => {}
283        }
284    }
285}
286
287impl Drop for OtlpLogger {
288    fn drop(&mut self) {
289        self.shutdown();
290    }
291}
292
293pub async fn init() -> Result<OtlpLogger, TryInitError> {
294    OtlpLogger::try_init().await
295}
296
297pub async fn init_with_config(config: OtlpConfig) -> Result<OtlpLogger, TryInitError> {
298    OtlpLogger::init_with_config(config).await
299}
300
301#[derive(Error, Debug)]
302pub struct TryInitError {
303    msg: String,
304    source: anyhow::Error,
305}
306
307impl std::fmt::Display for TryInitError {
308    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
309        write!(f, "Error initializing OtlpLogger: {}", self.msg)
310    }
311}
312
313#[cfg(test)]
314mod tests {
315
316    use super::*;
317
318    #[test]
319    fn test_config_builder_all() {
320        let config = OtlpConfig::builder()
321            .service_name("test-service".to_string())
322            .service_namespace("test-namespace".to_string())
323            .service_version("test-version".to_string())
324            .service_instant_id("test-instant-id".to_string())
325            .deployment_environment("test-environment".to_string())
326            .otlp_endpoint(Some("http://localhost:4317".to_string()))
327            .trace_level(LevelFilter::DEBUG)
328            .stdout_level(LevelFilter::WARN)
329            .build()
330            .unwrap();
331
332        assert_eq!(config.service_name, Some("test-service".to_string()));
333        assert_eq!(config.service_namespace, Some("test-namespace".to_string()));
334        assert_eq!(config.service_version, Some("test-version".to_string()));
335        assert_eq!(config.service_instant_id, Some("test-instant-id".to_string()));
336        assert_eq!(config.deployment_environment, Some("test-environment".to_string()));
337        assert_eq!(config.otlp_endpoint, Some("http://localhost:4317".to_string()));        
338        assert_eq!(config.trace_level, Some(LevelFilter::DEBUG));
339        assert_eq!(config.stdout_level, Some(LevelFilter::WARN));
340    }
341
342    #[test]
343    fn test_config_builder_some() {
344        let config = OtlpConfig::builder()
345             .otlp_endpoint("http://localhost:4317".to_string())
346             .trace_level(LevelFilter::INFO)
347             .stdout_level(LevelFilter::ERROR)
348             .build()
349             .expect("failed to configure otlp-logger");
350
351        assert_eq!(config.service_name, None);
352        assert_eq!(config.service_namespace, None);
353        assert_eq!(config.service_version, None);
354        assert_eq!(config.service_instant_id, None);
355        assert_eq!(config.deployment_environment, None);
356        assert_eq!(config.otlp_endpoint, Some("http://localhost:4317".to_string()));
357        assert_eq!(config.trace_level, Some(LevelFilter::INFO));
358        assert_eq!(config.stdout_level, Some(LevelFilter::ERROR));        
359    }
360
361    #[test]
362    fn test_config_builder_none() {
363        let config = OtlpConfig::builder()
364            .build()
365            .unwrap();
366
367        assert_eq!(config.service_name, None);
368        assert_eq!(config.service_namespace, None);
369        assert_eq!(config.service_version, None);
370        assert_eq!(config.service_instant_id, None);
371        assert_eq!(config.deployment_environment, None);
372        assert_eq!(config.otlp_endpoint, None);      
373        assert_eq!(config.trace_level, None);
374        assert_eq!(config.stdout_level, None); 
375    }
376}