1use std::env;
2use std::path::PathBuf;
3use tracing_appender::non_blocking::WorkerGuard;
4use tracing_subscriber::EnvFilter;
5use tracing_subscriber::fmt;
6use tracing_subscriber::prelude::*;
7
8const DEFAULT_LOG_DIR: &str = "./logs";
9const ENV_LOG_DIR: &str = "EVO_LOG_DIR";
10
11pub fn log_dir() -> PathBuf {
12 env::var(ENV_LOG_DIR)
13 .map(PathBuf::from)
14 .unwrap_or_else(|_| PathBuf::from(DEFAULT_LOG_DIR))
15}
16
17pub fn init_logging(component: &str) -> WorkerGuard {
18 let dir = log_dir();
19 std::fs::create_dir_all(&dir).expect("Failed to create log directory");
20
21 let file_appender = tracing_appender::rolling::daily(&dir, format!("{component}.log"));
22 let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
23
24 let file_layer = fmt::layer()
25 .json()
26 .with_writer(non_blocking)
27 .with_target(true)
28 .with_thread_ids(true)
29 .with_file(true)
30 .with_line_number(true);
31
32 let stdout_layer = fmt::layer().with_target(true).with_thread_ids(false);
33
34 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
35
36 tracing_subscriber::registry()
37 .with(filter)
38 .with(file_layer)
39 .with(stdout_layer)
40 .init();
41
42 guard
43}
44
45#[cfg(feature = "tracing-otel")]
48pub struct OtelGuard {
49 provider: opentelemetry_sdk::trace::SdkTracerProvider,
50}
51
52#[cfg(feature = "tracing-otel")]
53impl Drop for OtelGuard {
54 fn drop(&mut self) {
55 if let Err(e) = self.provider.shutdown() {
56 eprintln!("OpenTelemetry shutdown error: {e}");
57 }
58 }
59}
60
61#[cfg(feature = "tracing-otel")]
72pub fn init_logging_with_otel(component: &str, otlp_endpoint: &str) -> (WorkerGuard, OtelGuard) {
73 use opentelemetry::global;
74 use opentelemetry::trace::TracerProvider;
75 use opentelemetry_otlp::{SpanExporter, WithExportConfig};
76 use opentelemetry_sdk::Resource;
77 use opentelemetry_sdk::propagation::TraceContextPropagator;
78 use opentelemetry_sdk::trace::SdkTracerProvider;
79 use tracing_opentelemetry::OpenTelemetryLayer;
80
81 global::set_text_map_propagator(TraceContextPropagator::new());
83
84 let exporter = SpanExporter::builder()
86 .with_http()
87 .with_endpoint(otlp_endpoint)
88 .build()
89 .expect("Failed to build OTLP span exporter");
90
91 let provider = SdkTracerProvider::builder()
92 .with_batch_exporter(exporter)
93 .with_resource(
94 Resource::builder()
95 .with_service_name(component.to_owned())
96 .build(),
97 )
98 .build();
99
100 global::set_tracer_provider(provider.clone());
101
102 let otel_layer = OpenTelemetryLayer::new(provider.tracer(component.to_owned()));
103
104 let dir = log_dir();
106 std::fs::create_dir_all(&dir).expect("Failed to create log directory");
107 let file_appender = tracing_appender::rolling::daily(&dir, format!("{component}.log"));
108 let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
109
110 let file_layer = fmt::layer()
111 .json()
112 .with_writer(non_blocking)
113 .with_target(true)
114 .with_thread_ids(true)
115 .with_file(true)
116 .with_line_number(true);
117
118 let stdout_layer = fmt::layer().with_target(true).with_thread_ids(false);
119
120 let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
121
122 tracing_subscriber::registry()
123 .with(filter)
124 .with(file_layer)
125 .with(stdout_layer)
126 .with(otel_layer)
127 .init();
128
129 (guard, OtelGuard { provider })
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
138
139 #[test]
140 fn default_log_dir() {
141 let _guard = ENV_MUTEX.lock().unwrap();
142 unsafe { env::remove_var(ENV_LOG_DIR) };
143 assert_eq!(log_dir(), PathBuf::from("./logs"));
144 }
145
146 #[test]
147 fn custom_log_dir() {
148 let _guard = ENV_MUTEX.lock().unwrap();
149 unsafe { env::set_var(ENV_LOG_DIR, "/tmp/evo-test-logs") };
150 let result = log_dir();
151 unsafe { env::remove_var(ENV_LOG_DIR) };
152 assert_eq!(result, PathBuf::from("/tmp/evo-test-logs"));
153 }
154}