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 ensure_log_dir(&dir);
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 ensure_log_dir(dir: &std::path::Path) {
150 let _ = std::fs::create_dir_all(dir);
151 #[cfg(unix)]
152 {
153 use std::os::unix::fs::PermissionsExt;
154 let _ = std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700));
157 }
158}
159
160fn build_env_filter(default_level: &str) -> EnvFilter {
163 let level = read_env_chain(&["ARCANUM_LOG_LEVEL", "RUST_LOG"], default_level);
164 EnvFilter::try_new(&level).unwrap_or_else(|_| EnvFilter::new(default_level))
165}
166
167fn read_env(key: &str, default: &str) -> String {
168 std::env::var(key).unwrap_or_else(|_| default.to_string())
169}
170
171fn read_env_chain(keys: &[&str], default: &str) -> String {
172 for key in keys {
173 if let Ok(val) = std::env::var(key) {
174 if !val.is_empty() {
175 return val;
176 }
177 }
178 }
179 default.to_string()
180}
181
182#[cfg(feature = "otel")]
186fn build_otel_layer<S>(
187 service_name: &str,
188) -> Option<tracing_opentelemetry::OpenTelemetryLayer<S, opentelemetry_sdk::trace::SdkTracer>>
189where
190 S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
191{
192 use opentelemetry::trace::TracerProvider as _;
193 use opentelemetry_otlp::WithExportConfig as _;
194
195 let endpoint = std::env::var("ARCANUM_OTEL_ENDPOINT").ok()?;
196 if endpoint.is_empty() {
197 return None;
198 }
199
200 let exporter = opentelemetry_otlp::SpanExporter::builder()
202 .with_tonic()
203 .with_endpoint(&endpoint)
204 .build()
205 .ok()?;
206
207 let tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
208 .with_batch_exporter(exporter)
209 .with_resource(
210 opentelemetry_sdk::Resource::builder()
211 .with_service_name(service_name.to_string())
212 .build(),
213 )
214 .build();
215
216 let tracer = tracer_provider.tracer(service_name.to_string());
217
218 std::mem::forget(tracer_provider);
221
222 Some(tracing_opentelemetry::layer().with_tracer(tracer))
223}
224
225#[cfg(test)]
226mod tests {
227 use super::build_env_filter;
228
229 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
232
233 fn with_clean_env<F: FnOnce() -> R, R>(f: F) -> R {
234 let _guard = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
235 let prev_arcanum = std::env::var("ARCANUM_LOG_LEVEL").ok();
236 let prev_rust = std::env::var("RUST_LOG").ok();
237 std::env::remove_var("ARCANUM_LOG_LEVEL");
238 std::env::remove_var("RUST_LOG");
239 let result = f();
240 match prev_arcanum {
241 Some(v) => std::env::set_var("ARCANUM_LOG_LEVEL", v),
242 None => std::env::remove_var("ARCANUM_LOG_LEVEL"),
243 }
244 match prev_rust {
245 Some(v) => std::env::set_var("RUST_LOG", v),
246 None => std::env::remove_var("RUST_LOG"),
247 }
248 result
249 }
250
251 #[test]
252 fn test_default_level_used_when_no_env_set() {
253 with_clean_env(|| {
254 let filter = build_env_filter("error");
255 assert_eq!(format!("{}", filter), "error");
257 });
258 }
259
260 #[test]
261 fn test_arcanum_log_level_overrides_default() {
262 with_clean_env(|| {
263 std::env::set_var("ARCANUM_LOG_LEVEL", "debug");
264 let filter = build_env_filter("error");
265 assert_eq!(format!("{}", filter), "debug");
266 });
267 }
268
269 #[test]
270 fn test_rust_log_overrides_default() {
271 with_clean_env(|| {
272 std::env::set_var("RUST_LOG", "info");
273 let filter = build_env_filter("error");
274 assert_eq!(format!("{}", filter), "info");
275 });
276 }
277
278 #[test]
279 fn test_arcanum_log_level_takes_precedence_over_rust_log() {
280 with_clean_env(|| {
281 std::env::set_var("ARCANUM_LOG_LEVEL", "warn");
282 std::env::set_var("RUST_LOG", "trace");
283 let filter = build_env_filter("error");
284 assert_eq!(format!("{}", filter), "warn");
285 });
286 }
287
288 #[cfg(unix)]
289 #[test]
290 #[allow(clippy::unwrap_used)]
291 fn test_ensure_log_dir_sets_owner_only_permissions() {
292 use std::os::unix::fs::PermissionsExt;
293
294 let dir =
295 std::env::temp_dir().join(format!("seer-ensure-log-dir-test-{}", std::process::id()));
296 let _ = std::fs::remove_dir_all(&dir);
297
298 super::ensure_log_dir(&dir);
300 let mode = std::fs::metadata(&dir).unwrap().permissions().mode();
301 assert_eq!(
302 mode & 0o777,
303 0o700,
304 "fresh log dir must be owner-only, got {:o}",
305 mode & 0o777
306 );
307
308 std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o755)).unwrap();
311 super::ensure_log_dir(&dir);
312 let mode = std::fs::metadata(&dir).unwrap().permissions().mode();
313 assert_eq!(
314 mode & 0o777,
315 0o700,
316 "pre-existing log dir must be tightened to owner-only, got {:o}",
317 mode & 0o777
318 );
319
320 let _ = std::fs::remove_dir_all(&dir);
321 }
322}