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_with_writer<W>(app_name: &str, default_level: &str, writer: W) -> LogGuard
42where
43 W: for<'a> MakeWriter<'a> + Send + Sync + 'static,
44{
45 if INITIALIZED.set(()).is_err() {
47 return LogGuard { _file_guard: None };
48 }
49
50 let env_filter = build_env_filter(default_level);
51 let log_format = read_env("ARCANUM_LOG_FORMAT", "text");
52 let file_enabled = matches!(
53 read_env("ARCANUM_LOG_FILE", "").to_lowercase().as_str(),
54 "1" | "true" | "yes"
55 );
56
57 let json_mode = log_format == "json";
58
59 let file_appender = if file_enabled {
66 let dir = log_dir();
67 ensure_log_dir(&dir);
68 match tracing_appender::rolling::RollingFileAppender::builder()
69 .rotation(tracing_appender::rolling::Rotation::DAILY)
70 .filename_prefix(format!("{app_name}.log"))
71 .build(&dir)
72 {
73 Ok(appender) => Some(appender),
74 Err(e) => {
75 eprintln!(
76 "warning: file logging disabled — cannot write to {}: {e}",
77 dir.display()
78 );
79 None
80 }
81 }
82 } else {
83 None
84 };
85
86 let (file_layer, file_guard) = match file_appender {
88 Some(file_appender) => {
89 let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
90 let layer = if json_mode {
91 fmt::layer().json().with_writer(non_blocking).boxed()
92 } else {
93 fmt::layer().with_writer(non_blocking).boxed()
94 };
95 (Some(layer), Some(guard))
96 }
97 None => (None, None),
98 };
99
100 let console_layer = if json_mode {
101 fmt::layer().json().with_writer(writer).boxed()
102 } else {
103 fmt::layer().with_writer(writer).boxed()
104 };
105
106 #[cfg(feature = "otel")]
109 let otel_layer = build_otel_layer(app_name).map(|l| l.boxed());
110
111 let registry = tracing_subscriber::registry()
115 .with(env_filter)
116 .with(console_layer)
117 .with(file_layer);
118
119 #[cfg(feature = "otel")]
120 let registry = registry.with(otel_layer);
121
122 let _ = registry.try_init();
123
124 #[cfg(not(feature = "otel"))]
127 let _ = app_name;
128
129 LogGuard {
130 _file_guard: file_guard,
131 }
132}
133
134pub fn log_dir() -> PathBuf {
138 if let Ok(dir) = std::env::var("ARCANUM_LOG_DIR") {
139 return PathBuf::from(dir);
140 }
141 std::env::home_dir()
142 .unwrap_or_else(|| PathBuf::from("."))
143 .join(".arcanum")
144 .join("logs")
145}
146
147fn ensure_log_dir(dir: &std::path::Path) {
154 let _ = std::fs::create_dir_all(dir);
155 #[cfg(unix)]
156 {
157 use std::os::unix::fs::PermissionsExt;
158 let _ = std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700));
161 }
162}
163
164fn build_env_filter(default_level: &str) -> EnvFilter {
167 let level = read_env_chain(&["ARCANUM_LOG_LEVEL", "RUST_LOG"], default_level);
168 EnvFilter::try_new(&level).unwrap_or_else(|_| EnvFilter::new(default_level))
169}
170
171fn read_env(key: &str, default: &str) -> String {
172 std::env::var(key).unwrap_or_else(|_| default.to_string())
173}
174
175fn read_env_chain(keys: &[&str], default: &str) -> String {
176 for key in keys {
177 if let Ok(val) = std::env::var(key) {
178 if !val.is_empty() {
179 return val;
180 }
181 }
182 }
183 default.to_string()
184}
185
186#[cfg(feature = "otel")]
190fn build_otel_layer<S>(
191 service_name: &str,
192) -> Option<tracing_opentelemetry::OpenTelemetryLayer<S, opentelemetry_sdk::trace::SdkTracer>>
193where
194 S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
195{
196 use opentelemetry::trace::TracerProvider as _;
197 use opentelemetry_otlp::WithExportConfig as _;
198
199 let endpoint = std::env::var("ARCANUM_OTEL_ENDPOINT").ok()?;
200 if endpoint.is_empty() {
201 return None;
202 }
203
204 let exporter = opentelemetry_otlp::SpanExporter::builder()
206 .with_tonic()
207 .with_endpoint(&endpoint)
208 .build()
209 .ok()?;
210
211 let tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
212 .with_batch_exporter(exporter)
213 .with_resource(
214 opentelemetry_sdk::Resource::builder()
215 .with_service_name(service_name.to_string())
216 .build(),
217 )
218 .build();
219
220 let tracer = tracer_provider.tracer(service_name.to_string());
221
222 std::mem::forget(tracer_provider);
225
226 Some(tracing_opentelemetry::layer().with_tracer(tracer))
227}
228
229#[cfg(test)]
230mod tests {
231 use super::build_env_filter;
232
233 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
236
237 fn with_clean_env<F: FnOnce() -> R, R>(f: F) -> R {
238 let _guard = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
239 let prev_arcanum = std::env::var("ARCANUM_LOG_LEVEL").ok();
240 let prev_rust = std::env::var("RUST_LOG").ok();
241 std::env::remove_var("ARCANUM_LOG_LEVEL");
242 std::env::remove_var("RUST_LOG");
243 let result = f();
244 match prev_arcanum {
245 Some(v) => std::env::set_var("ARCANUM_LOG_LEVEL", v),
246 None => std::env::remove_var("ARCANUM_LOG_LEVEL"),
247 }
248 match prev_rust {
249 Some(v) => std::env::set_var("RUST_LOG", v),
250 None => std::env::remove_var("RUST_LOG"),
251 }
252 result
253 }
254
255 #[test]
256 fn test_default_level_used_when_no_env_set() {
257 with_clean_env(|| {
258 let filter = build_env_filter("error");
259 assert_eq!(format!("{}", filter), "error");
261 });
262 }
263
264 #[test]
265 fn test_arcanum_log_level_overrides_default() {
266 with_clean_env(|| {
267 std::env::set_var("ARCANUM_LOG_LEVEL", "debug");
268 let filter = build_env_filter("error");
269 assert_eq!(format!("{}", filter), "debug");
270 });
271 }
272
273 #[test]
274 fn test_rust_log_overrides_default() {
275 with_clean_env(|| {
276 std::env::set_var("RUST_LOG", "info");
277 let filter = build_env_filter("error");
278 assert_eq!(format!("{}", filter), "info");
279 });
280 }
281
282 #[test]
283 fn test_arcanum_log_level_takes_precedence_over_rust_log() {
284 with_clean_env(|| {
285 std::env::set_var("ARCANUM_LOG_LEVEL", "warn");
286 std::env::set_var("RUST_LOG", "trace");
287 let filter = build_env_filter("error");
288 assert_eq!(format!("{}", filter), "warn");
289 });
290 }
291
292 #[cfg(unix)]
293 #[test]
294 #[allow(clippy::unwrap_used)]
295 fn test_ensure_log_dir_sets_owner_only_permissions() {
296 use std::os::unix::fs::PermissionsExt;
297
298 let dir =
299 std::env::temp_dir().join(format!("seer-ensure-log-dir-test-{}", std::process::id()));
300 let _ = std::fs::remove_dir_all(&dir);
301
302 super::ensure_log_dir(&dir);
304 let mode = std::fs::metadata(&dir).unwrap().permissions().mode();
305 assert_eq!(
306 mode & 0o777,
307 0o700,
308 "fresh log dir must be owner-only, got {:o}",
309 mode & 0o777
310 );
311
312 std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o755)).unwrap();
315 super::ensure_log_dir(&dir);
316 let mode = std::fs::metadata(&dir).unwrap().permissions().mode();
317 assert_eq!(
318 mode & 0o777,
319 0o700,
320 "pre-existing log dir must be tightened to owner-only, got {:o}",
321 mode & 0o777
322 );
323
324 let _ = std::fs::remove_dir_all(&dir);
325 }
326}