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) -> LogGuard {
41 init_logging_with_writer(app_name, std::io::stderr)
42}
43
44pub fn init_logging_with_writer<W>(app_name: &str, writer: W) -> LogGuard
48where
49 W: for<'a> MakeWriter<'a> + Send + Sync + 'static,
50{
51 if INITIALIZED.set(()).is_err() {
53 return LogGuard { _file_guard: None };
54 }
55
56 let env_filter = build_env_filter();
57 let log_format = read_env("ARCANUM_LOG_FORMAT", "text");
58 let file_enabled = matches!(
59 read_env("ARCANUM_LOG_FILE", "").to_lowercase().as_str(),
60 "1" | "true" | "yes"
61 );
62
63 let json_mode = log_format == "json";
64
65 let (file_layer_json, file_layer_text, file_guard) = if file_enabled {
67 let dir = log_dir();
68 std::fs::create_dir_all(&dir).ok();
69 let file_appender = tracing_appender::rolling::daily(&dir, format!("{app_name}.log"));
70 let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
71
72 if json_mode {
73 (
74 Some(fmt::layer().json().with_writer(non_blocking).boxed()),
75 None,
76 Some(guard),
77 )
78 } else {
79 (
80 None,
81 Some(fmt::layer().with_writer(non_blocking).boxed()),
82 Some(guard),
83 )
84 }
85 } else {
86 (None, None, None)
87 };
88
89 let (console_json, console_text) = if json_mode {
91 (Some(fmt::layer().json().with_writer(writer).boxed()), None)
92 } else {
93 (None, Some(fmt::layer().with_writer(writer).boxed()))
94 };
95
96 let otel_layer = build_otel_layer(app_name).map(|l| l.boxed());
98
99 let _ = tracing_subscriber::registry()
103 .with(env_filter)
104 .with(console_json)
105 .with(console_text)
106 .with(file_layer_json)
107 .with(file_layer_text)
108 .with(otel_layer)
109 .try_init();
110
111 LogGuard {
112 _file_guard: file_guard,
113 }
114}
115
116pub fn log_dir() -> PathBuf {
120 if let Ok(dir) = std::env::var("ARCANUM_LOG_DIR") {
121 return PathBuf::from(dir);
122 }
123 dirs::home_dir()
124 .unwrap_or_else(|| PathBuf::from("."))
125 .join(".arcanum")
126 .join("logs")
127}
128
129fn build_env_filter() -> EnvFilter {
132 let level = read_env_chain(&["ARCANUM_LOG_LEVEL", "RUST_LOG"], "warn");
133 EnvFilter::try_new(&level).unwrap_or_else(|_| EnvFilter::new("warn"))
134}
135
136fn read_env(key: &str, default: &str) -> String {
137 std::env::var(key).unwrap_or_else(|_| default.to_string())
138}
139
140fn read_env_chain(keys: &[&str], default: &str) -> String {
141 for key in keys {
142 if let Ok(val) = std::env::var(key) {
143 if !val.is_empty() {
144 return val;
145 }
146 }
147 }
148 default.to_string()
149}
150
151fn build_otel_layer<S>(
154 service_name: &str,
155) -> Option<tracing_opentelemetry::OpenTelemetryLayer<S, opentelemetry_sdk::trace::SdkTracer>>
156where
157 S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
158{
159 use opentelemetry::trace::TracerProvider as _;
160 use opentelemetry_otlp::WithExportConfig as _;
161
162 let endpoint = std::env::var("ARCANUM_OTEL_ENDPOINT").ok()?;
163 if endpoint.is_empty() {
164 return None;
165 }
166
167 let exporter = opentelemetry_otlp::SpanExporter::builder()
169 .with_tonic()
170 .with_endpoint(&endpoint)
171 .build()
172 .ok()?;
173
174 let tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
175 .with_batch_exporter(exporter)
176 .with_resource(
177 opentelemetry_sdk::Resource::builder()
178 .with_service_name(service_name.to_string())
179 .build(),
180 )
181 .build();
182
183 let tracer = tracer_provider.tracer(service_name.to_string());
184
185 std::mem::forget(tracer_provider);
188
189 Some(tracing_opentelemetry::layer().with_tracer(tracer))
190}