use std::env;
use std::path::PathBuf;
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::{
fmt::{self, format::FmtSpan},
prelude::*,
EnvFilter,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TracingFormat {
#[default]
Pretty,
Compact,
Json,
}
impl std::str::FromStr for TracingFormat {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"pretty" => Ok(TracingFormat::Pretty),
"compact" => Ok(TracingFormat::Compact),
"json" => Ok(TracingFormat::Json),
_ => Err(format!(
"Unknown format: {}. Expected: pretty, compact, or json",
s
)),
}
}
}
impl TracingFormat {
pub fn parse(s: &str) -> Option<Self> {
s.parse().ok()
}
}
#[derive(Debug, Clone)]
pub struct TracingConfig {
pub format: TracingFormat,
pub span_events: bool,
pub file_info: bool,
pub thread_ids: bool,
pub thread_names: bool,
pub target: bool,
pub default_filter: String,
pub log_file: Option<PathBuf>,
pub log_to_stdout: bool,
}
impl Default for TracingConfig {
fn default() -> Self {
Self {
format: TracingFormat::Pretty,
span_events: false,
file_info: false,
thread_ids: false,
thread_names: false,
target: true,
default_filter: "lgp=info".to_string(),
log_file: None,
log_to_stdout: true,
}
}
}
impl TracingConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_format(mut self, format: TracingFormat) -> Self {
self.format = format;
self
}
pub fn with_span_events(mut self, enabled: bool) -> Self {
self.span_events = enabled;
self
}
pub fn with_file_info(mut self, enabled: bool) -> Self {
self.file_info = enabled;
self
}
pub fn with_thread_ids(mut self, enabled: bool) -> Self {
self.thread_ids = enabled;
self
}
pub fn with_thread_names(mut self, enabled: bool) -> Self {
self.thread_names = enabled;
self
}
pub fn with_target(mut self, enabled: bool) -> Self {
self.target = enabled;
self
}
pub fn with_default_filter(mut self, filter: impl Into<String>) -> Self {
self.default_filter = filter.into();
self
}
pub fn with_log_file(mut self, path: impl Into<PathBuf>) -> Self {
self.log_file = Some(path.into());
self
}
pub fn with_stdout(mut self, enabled: bool) -> Self {
self.log_to_stdout = enabled;
self
}
pub fn verbose() -> Self {
Self {
format: TracingFormat::Pretty,
span_events: true,
file_info: true,
thread_ids: true,
thread_names: false,
target: true,
default_filter: "lgp=debug".to_string(),
log_file: None,
log_to_stdout: true,
}
}
pub fn production() -> Self {
Self {
format: TracingFormat::Json,
span_events: false,
file_info: false,
thread_ids: false,
thread_names: false,
target: true,
default_filter: "lgp=info".to_string(),
log_file: None,
log_to_stdout: true,
}
}
}
pub struct TracingGuard {
_guards: Vec<WorkerGuard>,
}
pub fn init_tracing(config: TracingConfig) -> TracingGuard {
let format = env::var("LGP_LOG_FORMAT")
.ok()
.and_then(|s| TracingFormat::parse(&s))
.unwrap_or(config.format);
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new(&config.default_filter));
let span_events = if config.span_events {
FmtSpan::NEW | FmtSpan::CLOSE
} else {
FmtSpan::NONE
};
if let Some(log_path) = &config.log_file {
if let Some(parent) = log_path.parent() {
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent).ok();
}
}
let file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(log_path)
.expect("Failed to open log file");
let (non_blocking, file_guard) = tracing_appender::non_blocking(file);
let mut guards = vec![file_guard];
if config.log_to_stdout {
let stdout_guard =
init_with_file_and_stdout(format, filter, span_events, &config, non_blocking);
guards.push(stdout_guard);
} else {
init_with_file_only(format, filter, span_events, &config, non_blocking);
}
return TracingGuard { _guards: guards };
}
let stdout_guard = init_stdout_only(format, filter, span_events, &config);
TracingGuard {
_guards: vec![stdout_guard],
}
}
fn init_with_file_only(
format: TracingFormat,
filter: EnvFilter,
span_events: FmtSpan,
config: &TracingConfig,
writer: tracing_appender::non_blocking::NonBlocking,
) {
match format {
TracingFormat::Pretty => {
let subscriber = tracing_subscriber::registry().with(filter).with(
fmt::layer()
.with_writer(writer)
.with_ansi(false)
.pretty()
.with_span_events(span_events)
.with_file(config.file_info)
.with_line_number(config.file_info)
.with_thread_ids(config.thread_ids)
.with_thread_names(config.thread_names)
.with_target(config.target),
);
tracing::subscriber::set_global_default(subscriber)
.expect("Failed to set tracing subscriber");
}
TracingFormat::Compact => {
let subscriber = tracing_subscriber::registry().with(filter).with(
fmt::layer()
.with_writer(writer)
.with_ansi(false)
.compact()
.with_span_events(span_events)
.with_file(config.file_info)
.with_line_number(config.file_info)
.with_thread_ids(config.thread_ids)
.with_thread_names(config.thread_names)
.with_target(config.target),
);
tracing::subscriber::set_global_default(subscriber)
.expect("Failed to set tracing subscriber");
}
TracingFormat::Json => {
let subscriber = tracing_subscriber::registry().with(filter).with(
fmt::layer()
.with_writer(writer)
.json()
.with_span_events(span_events)
.with_file(config.file_info)
.with_line_number(config.file_info)
.with_thread_ids(config.thread_ids)
.with_thread_names(config.thread_names)
.with_target(config.target),
);
tracing::subscriber::set_global_default(subscriber)
.expect("Failed to set tracing subscriber");
}
}
}
fn init_with_file_and_stdout(
format: TracingFormat,
filter: EnvFilter,
span_events: FmtSpan,
config: &TracingConfig,
file_writer: tracing_appender::non_blocking::NonBlocking,
) -> WorkerGuard {
let (nb_stdout, stdout_guard) = tracing_appender::non_blocking(std::io::stdout());
match format {
TracingFormat::Pretty => {
let file_layer = fmt::layer()
.with_writer(file_writer)
.with_ansi(false)
.pretty()
.with_span_events(span_events.clone())
.with_file(config.file_info)
.with_line_number(config.file_info)
.with_thread_ids(config.thread_ids)
.with_thread_names(config.thread_names)
.with_target(config.target);
let stdout_layer = fmt::layer()
.with_writer(nb_stdout)
.pretty()
.with_span_events(span_events)
.with_file(config.file_info)
.with_line_number(config.file_info)
.with_thread_ids(config.thread_ids)
.with_thread_names(config.thread_names)
.with_target(config.target);
let subscriber = tracing_subscriber::registry()
.with(filter)
.with(file_layer)
.with(stdout_layer);
tracing::subscriber::set_global_default(subscriber)
.expect("Failed to set tracing subscriber");
}
TracingFormat::Compact => {
let file_layer = fmt::layer()
.with_writer(file_writer)
.with_ansi(false)
.compact()
.with_span_events(span_events.clone())
.with_file(config.file_info)
.with_line_number(config.file_info)
.with_thread_ids(config.thread_ids)
.with_thread_names(config.thread_names)
.with_target(config.target);
let stdout_layer = fmt::layer()
.with_writer(nb_stdout)
.compact()
.with_span_events(span_events)
.with_file(config.file_info)
.with_line_number(config.file_info)
.with_thread_ids(config.thread_ids)
.with_thread_names(config.thread_names)
.with_target(config.target);
let subscriber = tracing_subscriber::registry()
.with(filter)
.with(file_layer)
.with(stdout_layer);
tracing::subscriber::set_global_default(subscriber)
.expect("Failed to set tracing subscriber");
}
TracingFormat::Json => {
let file_layer = fmt::layer()
.with_writer(file_writer)
.json()
.with_span_events(span_events.clone())
.with_file(config.file_info)
.with_line_number(config.file_info)
.with_thread_ids(config.thread_ids)
.with_thread_names(config.thread_names)
.with_target(config.target);
let stdout_layer = fmt::layer()
.with_writer(nb_stdout)
.json()
.with_span_events(span_events)
.with_file(config.file_info)
.with_line_number(config.file_info)
.with_thread_ids(config.thread_ids)
.with_thread_names(config.thread_names)
.with_target(config.target);
let subscriber = tracing_subscriber::registry()
.with(filter)
.with(file_layer)
.with(stdout_layer);
tracing::subscriber::set_global_default(subscriber)
.expect("Failed to set tracing subscriber");
}
}
stdout_guard
}
fn init_stdout_only(
format: TracingFormat,
filter: EnvFilter,
span_events: FmtSpan,
config: &TracingConfig,
) -> WorkerGuard {
let (nb_stdout, guard) = tracing_appender::non_blocking(std::io::stdout());
match format {
TracingFormat::Pretty => {
let subscriber = tracing_subscriber::registry().with(filter).with(
fmt::layer()
.with_writer(nb_stdout)
.pretty()
.with_span_events(span_events)
.with_file(config.file_info)
.with_line_number(config.file_info)
.with_thread_ids(config.thread_ids)
.with_thread_names(config.thread_names)
.with_target(config.target),
);
tracing::subscriber::set_global_default(subscriber)
.expect("Failed to set tracing subscriber");
}
TracingFormat::Compact => {
let subscriber = tracing_subscriber::registry().with(filter).with(
fmt::layer()
.with_writer(nb_stdout)
.compact()
.with_span_events(span_events)
.with_file(config.file_info)
.with_line_number(config.file_info)
.with_thread_ids(config.thread_ids)
.with_thread_names(config.thread_names)
.with_target(config.target),
);
tracing::subscriber::set_global_default(subscriber)
.expect("Failed to set tracing subscriber");
}
TracingFormat::Json => {
let subscriber = tracing_subscriber::registry().with(filter).with(
fmt::layer()
.with_writer(nb_stdout)
.json()
.with_span_events(span_events)
.with_file(config.file_info)
.with_line_number(config.file_info)
.with_thread_ids(config.thread_ids)
.with_thread_names(config.thread_names)
.with_target(config.target),
);
tracing::subscriber::set_global_default(subscriber)
.expect("Failed to set tracing subscriber");
}
}
guard
}
pub fn try_init_tracing(config: TracingConfig) -> Result<(), Box<dyn std::error::Error>> {
let format = env::var("LGP_LOG_FORMAT")
.ok()
.and_then(|s| TracingFormat::parse(&s))
.unwrap_or(config.format);
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new(&config.default_filter));
let span_events = if config.span_events {
FmtSpan::NEW | FmtSpan::CLOSE
} else {
FmtSpan::NONE
};
let result = match format {
TracingFormat::Pretty => {
let subscriber = tracing_subscriber::registry().with(filter).with(
fmt::layer()
.pretty()
.with_span_events(span_events)
.with_file(config.file_info)
.with_line_number(config.file_info)
.with_thread_ids(config.thread_ids)
.with_thread_names(config.thread_names)
.with_target(config.target),
);
tracing::subscriber::set_global_default(subscriber)
}
TracingFormat::Compact => {
let subscriber = tracing_subscriber::registry().with(filter).with(
fmt::layer()
.compact()
.with_span_events(span_events)
.with_file(config.file_info)
.with_line_number(config.file_info)
.with_thread_ids(config.thread_ids)
.with_thread_names(config.thread_names)
.with_target(config.target),
);
tracing::subscriber::set_global_default(subscriber)
}
TracingFormat::Json => {
let subscriber = tracing_subscriber::registry().with(filter).with(
fmt::layer()
.json()
.with_span_events(span_events)
.with_file(config.file_info)
.with_line_number(config.file_info)
.with_thread_ids(config.thread_ids)
.with_thread_names(config.thread_names)
.with_target(config.target),
);
tracing::subscriber::set_global_default(subscriber)
}
};
result.map_err(|e| e.into())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_from_str() {
assert_eq!(TracingFormat::parse("pretty"), Some(TracingFormat::Pretty));
assert_eq!(TracingFormat::parse("PRETTY"), Some(TracingFormat::Pretty));
assert_eq!(
TracingFormat::parse("compact"),
Some(TracingFormat::Compact)
);
assert_eq!(TracingFormat::parse("json"), Some(TracingFormat::Json));
assert_eq!(TracingFormat::parse("invalid"), None);
}
#[test]
fn test_config_builder() {
let config = TracingConfig::new()
.with_format(TracingFormat::Json)
.with_span_events(true)
.with_file_info(true)
.with_thread_ids(true)
.with_default_filter("lgp=trace");
assert_eq!(config.format, TracingFormat::Json);
assert!(config.span_events);
assert!(config.file_info);
assert!(config.thread_ids);
assert_eq!(config.default_filter, "lgp=trace");
}
#[test]
fn test_verbose_config() {
let config = TracingConfig::verbose();
assert_eq!(config.format, TracingFormat::Pretty);
assert!(config.span_events);
assert!(config.file_info);
assert_eq!(config.default_filter, "lgp=debug");
}
#[test]
fn test_production_config() {
let config = TracingConfig::production();
assert_eq!(config.format, TracingFormat::Json);
assert!(!config.span_events);
assert!(!config.file_info);
assert_eq!(config.default_filter, "lgp=info");
}
#[test]
fn test_file_logging_config() {
let config = TracingConfig::new()
.with_log_file("/tmp/test.log")
.with_stdout(false);
assert_eq!(config.log_file, Some(PathBuf::from("/tmp/test.log")));
assert!(!config.log_to_stdout);
let default = TracingConfig::default();
assert!(default.log_file.is_none());
assert!(default.log_to_stdout);
}
}