#![warn(missing_debug_implementations)]
#![warn(unreachable_pub)]
#![deny(unused_must_use)]
mod config;
mod error;
mod fmt;
mod reload;
mod rotation;
#[cfg(any(feature = "custom-async", feature = "native-async"))]
mod writer;
pub use error::{ActaError, Result};
pub use fmt::{AnsiFormatter, Icons, LevelLabels, StyleConfig, Theme};
pub use rotation::rotate_log_file;
#[cfg(feature = "custom-async")]
pub use writer::{AsyncWriter, AsyncWriterTarget, async_writer, async_writer_for};
#[cfg(any(feature = "custom-async", feature = "native-async"))]
pub use config::AsyncWriterMode;
pub use config::{
ConsoleConfig, ConsoleWriter, FileLoggingConfig, FilterDirective, LogFilter, LogFormat,
LogLevel, LogRotation, LoggingConfig,
};
#[cfg(feature = "file")]
use std::path::PathBuf;
use tracing_subscriber::fmt::format::FmtSpan;
use tracing_subscriber::prelude::*;
#[cfg(feature = "file")]
pub type LogHandle = tracing_appender::non_blocking::WorkerGuard;
use crate::reload::FmtLayer;
pub use crate::reload::ReloadHandle;
#[cfg(feature = "file")]
#[must_use = "dropping TracingGuard will stop file logging"]
#[derive(Debug)]
pub struct TracingGuard {
pub worker_guard: Option<LogHandle>,
pub log_path: Option<PathBuf>,
pub reload_handle: ReloadHandle,
}
pub fn build_console_layer(console: &ConsoleConfig) -> FmtLayer {
let mut formatter = AnsiFormatter::new()
.with_show_path(console.show_path)
.with_show_spans(console.show_spans);
if let Some(tf) = &console.time_format {
formatter = formatter.with_time_format(tf.clone());
}
build_console_layer_with(console, formatter)
}
pub fn build_console_layer_with(console: &ConsoleConfig, formatter: AnsiFormatter) -> FmtLayer {
macro_rules! with_writer {
($layer:expr, $console:expr) => {
match $console.writer {
ConsoleWriter::Stdout => $layer.with_writer(std::io::stdout).boxed(),
ConsoleWriter::Stderr => $layer.with_writer(std::io::stderr).boxed(),
#[cfg(any(feature = "custom-async", feature = "native-async"))]
ConsoleWriter::AsyncStdout(mode) => match mode {
#[cfg(feature = "custom-async")]
config::AsyncWriterMode::Custom => $layer
.with_writer(writer::async_writer_for(writer::AsyncWriterTarget::Stdout))
.boxed(),
#[cfg(feature = "native-async")]
config::AsyncWriterMode::Native => $layer
.with_writer(writer::native_async_writer(
writer::AsyncWriterTarget::Stdout,
))
.boxed(),
},
#[cfg(any(feature = "custom-async", feature = "native-async"))]
ConsoleWriter::AsyncStderr(mode) => match mode {
#[cfg(feature = "custom-async")]
config::AsyncWriterMode::Custom => $layer
.with_writer(writer::async_writer_for(writer::AsyncWriterTarget::Stderr))
.boxed(),
#[cfg(feature = "native-async")]
config::AsyncWriterMode::Native => $layer
.with_writer(writer::native_async_writer(
writer::AsyncWriterTarget::Stderr,
))
.boxed(),
},
}
};
}
match &console.format {
LogFormat::Pretty => {
let layer = tracing_subscriber::fmt::Layer::default()
.pretty()
.with_target(true)
.with_file(true)
.with_line_number(true)
.with_thread_ids(false)
.with_thread_names(false)
.with_ansi(console.ansi)
.with_span_events(FmtSpan::NONE);
with_writer!(layer, console)
}
LogFormat::Compact => {
let layer = tracing_subscriber::fmt::Layer::default()
.with_target(false)
.with_file(false)
.with_line_number(false)
.with_thread_ids(false)
.with_thread_names(false)
.with_ansi(console.ansi)
.with_span_events(FmtSpan::NONE)
.event_format(formatter);
with_writer!(layer, console)
}
LogFormat::Json => {
let layer = tracing_subscriber::fmt::Layer::default()
.json()
.with_target(false)
.with_file(false)
.with_line_number(false)
.with_current_span(false)
.with_span_list(false)
.flatten_event(true)
.with_ansi(false);
with_writer!(layer, console)
}
}
}
#[cfg(feature = "file")]
#[must_use = "dropping FileLayerParts.guard will stop file logging"]
pub struct FileLayerParts {
pub writer: tracing_appender::non_blocking::NonBlocking,
pub guard: LogHandle,
pub path: PathBuf,
}
#[cfg(feature = "file")]
impl std::fmt::Debug for FileLayerParts {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FileLayerParts")
.field("path", &self.path)
.finish_non_exhaustive()
}
}
#[cfg(feature = "file")]
pub fn build_file_layer(file_config: &FileLoggingConfig) -> Result<FileLayerParts> {
let path = &file_config.path;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
rotate_log_file(path, file_config.rotation)?;
let path = resolve_log_path(path);
let file_appender = tracing_appender::rolling::never(
path.parent().unwrap_or(std::path::Path::new(".")),
path.file_name().unwrap_or_default(),
);
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
Ok(FileLayerParts {
writer: non_blocking,
guard,
path,
})
}
pub use crate::reload::build_reload_filter;
#[cfg(feature = "file")]
pub fn init_tracing(config: &LoggingConfig) -> Result<TracingGuard> {
let (filter, reload_handle) = build_reload_filter(&config.level, None);
let console_layer: FmtLayer = match &config.console {
Some(console) => build_console_layer(console),
None => tracing_subscriber::fmt::Layer::default()
.with_writer(std::io::sink)
.boxed(),
};
let subscriber = tracing_subscriber::Registry::default()
.with(console_layer)
.with(filter);
let (worker_guard, log_path) = if let Some(file_config) = &config.file {
let parts = build_file_layer(file_config)?;
let file_layer = tracing_subscriber::fmt::layer()
.json()
.with_target(true)
.with_file(true)
.with_line_number(true)
.with_current_span(true)
.with_span_list(true)
.flatten_event(true)
.with_ansi(false)
.with_writer(parts.writer);
let subscriber = subscriber.with(file_layer);
tracing::subscriber::set_global_default(subscriber)?;
(Some(parts.guard), Some(parts.path))
} else {
tracing::subscriber::set_global_default(subscriber)?;
(None, None)
};
Ok(TracingGuard {
worker_guard,
log_path,
reload_handle,
})
}
#[cfg(feature = "file")]
fn resolve_log_path(path: &std::path::Path) -> PathBuf {
match std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
{
Ok(_) => path.to_path_buf(),
Err(_) => {
let pid = std::process::id();
let stem = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("latest");
let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("log");
path.with_file_name(format!("{stem}-{pid}.{ext}"))
}
}
}
#[cfg(test)]
mod test;