use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use tracing_appender::non_blocking::{NonBlockingBuilder, WorkerGuard};
use tracing_appender::rolling::{RollingFileAppender, Rotation};
use tracing_subscriber::EnvFilter;
#[inline]
pub fn init_locale(lang_override: Option<&str>) {
crate::locale::init_locale(lang_override);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogFormat {
Compact,
Json,
}
pub fn cli_log_level(verbose: u8, quiet: u8) -> &'static str {
match (verbose, quiet) {
(0, 0) => "warn",
(1, _) => "info",
(2, _) => "debug",
(3.., _) => "trace",
(_, 1) => "error",
(_, 2..) => "off",
}
}
pub fn build_env_filter(cli_default: &str) -> EnvFilter {
EnvFilter::new(cli_default)
}
pub fn resolve_log_format(file_sink: bool) -> LogFormat {
if file_sink {
LogFormat::Json
} else {
LogFormat::Compact
}
}
fn log_dir_from_xdg() -> Option<PathBuf> {
crate::storage::state_dir().map(|s| s.join("logs"))
}
fn log_lossy_default() -> bool {
true
}
struct LogSink {
stderr: io::Stderr,
file: Option<RollingFileAppender>,
}
impl Write for LogSink {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let stderr_res = self.stderr.write_all(buf);
if let Some(file) = self.file.as_mut() {
if let Err(e) = file.write_all(buf) {
let _ = writeln!(
self.stderr,
"atomwrite: log file write failed: {e}; continuing on stderr only"
);
self.file = None;
}
}
stderr_res.map(|()| buf.len())
}
fn flush(&mut self) -> io::Result<()> {
self.stderr.flush()?;
if let Some(file) = self.file.as_mut() {
let _ = file.flush();
}
Ok(())
}
}
fn try_open_log_file(dir: &Path, json: bool) -> Option<RollingFileAppender> {
let suffix = if json { "jsonl" } else { "log" };
match RollingFileAppender::builder()
.rotation(Rotation::NEVER)
.filename_prefix("atomwrite")
.filename_suffix(suffix)
.max_log_files(7)
.build(dir)
{
Ok(appender) => Some(appender),
Err(e) => {
let _ = writeln!(
io::stderr(),
"atomwrite: failed to open log dir {}: {e}; stderr only",
dir.display()
);
None
}
}
}
pub fn init_tracing(
verbose: u8,
quiet: u8,
color: ColorMode,
) -> WorkerGuard {
let cli_default = cli_log_level(verbose, quiet);
let filter = build_env_filter(cli_default);
let filter_display = filter.to_string();
let log_dir = log_dir_from_xdg();
let format = resolve_log_format(log_dir.is_some());
let file = log_dir
.as_ref()
.and_then(|dir| try_open_log_file(dir, matches!(format, LogFormat::Json)));
let file_sink = file.is_some();
let ansi = !matches!(format, LogFormat::Json) && stderr_color_enabled(color);
let show_source = matches!(cli_default, "debug" | "trace");
let sink = LogSink {
stderr: io::stderr(),
file,
};
let (non_blocking, guard) = NonBlockingBuilder::default()
.lossy(log_lossy_default())
.thread_name("atomwrite-log")
.finish(sink);
use tracing_subscriber::prelude::*;
let init_result = match format {
LogFormat::Json => {
let fmt_layer = tracing_subscriber::fmt::layer()
.json()
.flatten_event(true)
.with_current_span(true)
.with_span_list(true)
.with_writer(non_blocking)
.with_ansi(false)
.with_target(true)
.with_thread_ids(true)
.with_thread_names(true)
.with_file(show_source)
.with_line_number(show_source);
tracing_subscriber::registry()
.with(filter)
.with(fmt_layer)
.with(tracing_error::ErrorLayer::default())
.try_init()
}
LogFormat::Compact => {
let fmt_layer = tracing_subscriber::fmt::layer()
.with_writer(non_blocking)
.with_target(true)
.with_ansi(ansi)
.with_thread_ids(true)
.with_thread_names(true)
.with_file(show_source)
.with_line_number(show_source)
.compact();
tracing_subscriber::registry()
.with(filter)
.with(fmt_layer)
.with(tracing_error::ErrorLayer::default())
.try_init()
}
};
if let Err(e) = init_result {
let _ = writeln!(
io::stderr(),
"atomwrite: tracing subscriber already installed ({e}); keeping existing subscriber"
);
} else {
tracing::info!(
filter = %filter_display,
cli_default,
format = ?format,
file_sink,
"tracing initialized"
);
}
guard
}
pub fn install_panic_hook() {
let default_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let payload = if let Some(s) = info.payload().downcast_ref::<&str>() {
(*s).to_string()
} else if let Some(s) = info.payload().downcast_ref::<String>() {
s.clone()
} else {
"unknown panic".to_string()
};
let location = info
.location()
.map(|l| format!("{}:{}", l.file(), l.line()));
tracing::error!(
panic.payload = %payload,
panic.location = location.as_deref().unwrap_or("unknown"),
"process panicked"
);
default_hook(info);
}));
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ColorMode {
#[default]
Auto,
Always,
Never,
}
include!("runtime_cli.inc.rs");