1use 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}