1use std::path::PathBuf;
17use std::sync::OnceLock;
18
19use tracing_subscriber::{
20 fmt::{self, MakeWriter},
21 layer::SubscriberExt,
22 util::SubscriberInitExt,
23 EnvFilter, Layer,
24};
25
26static INITIALIZED: OnceLock<()> = OnceLock::new();
27
28pub struct LogGuard {
33 _file_guard: Option<tracing_appender::non_blocking::WorkerGuard>,
34}
35
36pub fn init_logging(app_name: &str, default_level: &str) -> LogGuard {
44 init_logging_with_writer(app_name, default_level, std::io::stderr)
45}
46
47pub fn init_logging_with_writer<W>(app_name: &str, default_level: &str, writer: W) -> LogGuard
52where
53 W: for<'a> MakeWriter<'a> + Send + Sync + 'static,
54{
55 if INITIALIZED.set(()).is_err() {
57 return LogGuard { _file_guard: None };
58 }
59
60 let env_filter = build_env_filter(default_level);
61 let log_format = read_env("ARCANUM_LOG_FORMAT", "text");
62 let file_enabled = matches!(
63 read_env("ARCANUM_LOG_FILE", "").to_lowercase().as_str(),
64 "1" | "true" | "yes"
65 );
66
67 let json_mode = log_format == "json";
68
69 let (file_layer_json, file_layer_text, file_guard) = if file_enabled {
71 let dir = log_dir();
72 std::fs::create_dir_all(&dir).ok();
73 let file_appender = tracing_appender::rolling::daily(&dir, format!("{app_name}.log"));
74 let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
75
76 if json_mode {
77 (
78 Some(fmt::layer().json().with_writer(non_blocking).boxed()),
79 None,
80 Some(guard),
81 )
82 } else {
83 (
84 None,
85 Some(fmt::layer().with_writer(non_blocking).boxed()),
86 Some(guard),
87 )
88 }
89 } else {
90 (None, None, None)
91 };
92
93 let (console_json, console_text) = if json_mode {
95 (Some(fmt::layer().json().with_writer(writer).boxed()), None)
96 } else {
97 (None, Some(fmt::layer().with_writer(writer).boxed()))
98 };
99
100 #[cfg(feature = "otel")]
103 let otel_layer = build_otel_layer(app_name).map(|l| l.boxed());
104
105 let registry = tracing_subscriber::registry()
109 .with(env_filter)
110 .with(console_json)
111 .with(console_text)
112 .with(file_layer_json)
113 .with(file_layer_text);
114
115 #[cfg(feature = "otel")]
116 let registry = registry.with(otel_layer);
117
118 let _ = registry.try_init();
119
120 #[cfg(not(feature = "otel"))]
123 let _ = app_name;
124
125 LogGuard {
126 _file_guard: file_guard,
127 }
128}
129
130pub fn log_dir() -> PathBuf {
134 if let Ok(dir) = std::env::var("ARCANUM_LOG_DIR") {
135 return PathBuf::from(dir);
136 }
137 dirs::home_dir()
138 .unwrap_or_else(|| PathBuf::from("."))
139 .join(".arcanum")
140 .join("logs")
141}
142
143fn build_env_filter(default_level: &str) -> EnvFilter {
146 let level = read_env_chain(&["ARCANUM_LOG_LEVEL", "RUST_LOG"], default_level);
147 EnvFilter::try_new(&level).unwrap_or_else(|_| EnvFilter::new(default_level))
148}
149
150fn read_env(key: &str, default: &str) -> String {
151 std::env::var(key).unwrap_or_else(|_| default.to_string())
152}
153
154fn read_env_chain(keys: &[&str], default: &str) -> String {
155 for key in keys {
156 if let Ok(val) = std::env::var(key) {
157 if !val.is_empty() {
158 return val;
159 }
160 }
161 }
162 default.to_string()
163}
164
165#[cfg(feature = "otel")]
169fn build_otel_layer<S>(
170 service_name: &str,
171) -> Option<tracing_opentelemetry::OpenTelemetryLayer<S, opentelemetry_sdk::trace::SdkTracer>>
172where
173 S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
174{
175 use opentelemetry::trace::TracerProvider as _;
176 use opentelemetry_otlp::WithExportConfig as _;
177
178 let endpoint = std::env::var("ARCANUM_OTEL_ENDPOINT").ok()?;
179 if endpoint.is_empty() {
180 return None;
181 }
182
183 let exporter = opentelemetry_otlp::SpanExporter::builder()
185 .with_tonic()
186 .with_endpoint(&endpoint)
187 .build()
188 .ok()?;
189
190 let tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
191 .with_batch_exporter(exporter)
192 .with_resource(
193 opentelemetry_sdk::Resource::builder()
194 .with_service_name(service_name.to_string())
195 .build(),
196 )
197 .build();
198
199 let tracer = tracer_provider.tracer(service_name.to_string());
200
201 std::mem::forget(tracer_provider);
204
205 Some(tracing_opentelemetry::layer().with_tracer(tracer))
206}
207
208#[cfg(test)]
209mod tests {
210 use super::build_env_filter;
211
212 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
215
216 fn with_clean_env<F: FnOnce() -> R, R>(f: F) -> R {
217 let _guard = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
218 let prev_arcanum = std::env::var("ARCANUM_LOG_LEVEL").ok();
219 let prev_rust = std::env::var("RUST_LOG").ok();
220 std::env::remove_var("ARCANUM_LOG_LEVEL");
221 std::env::remove_var("RUST_LOG");
222 let result = f();
223 match prev_arcanum {
224 Some(v) => std::env::set_var("ARCANUM_LOG_LEVEL", v),
225 None => std::env::remove_var("ARCANUM_LOG_LEVEL"),
226 }
227 match prev_rust {
228 Some(v) => std::env::set_var("RUST_LOG", v),
229 None => std::env::remove_var("RUST_LOG"),
230 }
231 result
232 }
233
234 #[test]
235 fn test_default_level_used_when_no_env_set() {
236 with_clean_env(|| {
237 let filter = build_env_filter("error");
238 assert_eq!(format!("{}", filter), "error");
240 });
241 }
242
243 #[test]
244 fn test_arcanum_log_level_overrides_default() {
245 with_clean_env(|| {
246 std::env::set_var("ARCANUM_LOG_LEVEL", "debug");
247 let filter = build_env_filter("error");
248 assert_eq!(format!("{}", filter), "debug");
249 });
250 }
251
252 #[test]
253 fn test_rust_log_overrides_default() {
254 with_clean_env(|| {
255 std::env::set_var("RUST_LOG", "info");
256 let filter = build_env_filter("error");
257 assert_eq!(format!("{}", filter), "info");
258 });
259 }
260
261 #[test]
262 fn test_arcanum_log_level_takes_precedence_over_rust_log() {
263 with_clean_env(|| {
264 std::env::set_var("ARCANUM_LOG_LEVEL", "warn");
265 std::env::set_var("RUST_LOG", "trace");
266 let filter = build_env_filter("error");
267 assert_eq!(format!("{}", filter), "warn");
268 });
269 }
270}