use std::sync::OnceLock;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::Layer;
use tracing_subscriber::Registry;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogTarget {
Stdout,
File,
Test,
}
#[must_use = "drop the LoggingGuard only at process exit; dropping early loses file log lines"]
pub struct LoggingGuard {
_file: Option<tracing_appender::non_blocking::WorkerGuard>,
}
type BoxedLayer = Box<dyn Layer<Registry> + Send + Sync + 'static>;
pub fn init(target: LogTarget) -> LoggingGuard {
static INIT: OnceLock<()> = OnceLock::new();
let mut guard: Option<tracing_appender::non_blocking::WorkerGuard> = None;
INIT.get_or_init(|| {
install_first_panic_capture();
guard = init_inner(target);
});
LoggingGuard { _file: guard }
}
fn install_first_panic_capture() {
use std::io::Write as _;
let default_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open("/tmp/neomacs-first-panic.txt")
{
let loc = info
.location()
.map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column()))
.unwrap_or_else(|| "<unknown>".to_string());
let payload = info
.payload()
.downcast_ref::<&'static str>()
.map(|s| s.to_string())
.or_else(|| info.payload().downcast_ref::<String>().cloned())
.unwrap_or_else(|| "<non-string payload>".to_string());
let _ = writeln!(
f,
"=== PANIC ===\nAT: {}\nPAYLOAD: {}\nBACKTRACE:\n{}\n",
loc,
payload,
std::backtrace::Backtrace::force_capture()
);
let _ = f.flush();
}
default_hook(info);
}));
}
pub fn init_for_tests() {
let _ = init(LogTarget::Test);
}
fn make_env_filter() -> EnvFilter {
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("warn"))
}
fn resolve_env_log_file() -> Option<std::path::PathBuf> {
if let Some(path) = std::env::var_os("NEOMACS_LOG_FILE") {
let path = std::path::PathBuf::from(path);
if path.as_os_str().is_empty() {
return None;
}
return Some(path);
}
let legacy = std::env::var("NEOMACS_LOG_TO_FILE")
.map(|v| v == "1")
.unwrap_or(false);
if legacy {
let pid = std::process::id();
return Some(std::path::PathBuf::from(format!("neomacs-{pid}.log")));
}
None
}
fn default_tui_log_path() -> std::path::PathBuf {
std::path::PathBuf::from(format!("/tmp/neomacs-{}.log", std::process::id()))
}
fn default_layer(target: LogTarget) -> Option<BoxedLayer> {
match target {
LogTarget::Stdout => Some(
tracing_subscriber::fmt::layer()
.with_writer(std::io::stdout)
.with_filter(make_env_filter())
.boxed(),
),
LogTarget::File => None,
LogTarget::Test => Some(
tracing_subscriber::fmt::layer()
.with_test_writer()
.with_filter(make_env_filter())
.boxed(),
),
}
}
fn open_file_layer(
path: &std::path::Path,
) -> (
Option<BoxedLayer>,
Option<tracing_appender::non_blocking::WorkerGuard>,
) {
match std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
{
Ok(file) => {
let (writer, worker_guard) = tracing_appender::non_blocking(file);
let layer = tracing_subscriber::fmt::layer()
.with_writer(writer)
.with_ansi(false)
.with_filter(make_env_filter())
.boxed();
(Some(layer), Some(worker_guard))
}
Err(e) => {
eprintln!(
"warning: could not open log file {}: {e}; continuing without file output",
path.display(),
);
(None, None)
}
}
}
fn file_layer_for(
target: LogTarget,
) -> (
Option<BoxedLayer>,
Option<tracing_appender::non_blocking::WorkerGuard>,
) {
let path = match (target, resolve_env_log_file()) {
(_, Some(path)) => path,
(LogTarget::File, None) => default_tui_log_path(),
(LogTarget::Stdout | LogTarget::Test, None) => return (None, None),
};
open_file_layer(&path)
}
fn init_inner(target: LogTarget) -> Option<tracing_appender::non_blocking::WorkerGuard> {
let default = default_layer(target);
let (file, file_guard) = file_layer_for(target);
let mut layers: Vec<BoxedLayer> = Vec::new();
if let Some(layer) = default {
layers.push(layer);
}
if let Some(layer) = file {
layers.push(layer);
}
let result = tracing_subscriber::registry().with(layers).try_init();
if let Err(e) = result {
eprintln!("warning: tracing subscriber init failed: {e}");
}
file_guard
}