use std::{path::Path, sync::Arc};
use tracing_subscriber::{
EnvFilter, Layer as _,
fmt::time::FormatTime,
layer::SubscriberExt,
util::{SubscriberInitExt, TryInitError},
};
use crate::config::ObservabilityConfig;
#[derive(Clone, Copy)]
struct LocalTime;
impl FormatTime for LocalTime {
fn format_time(&self, w: &mut tracing_subscriber::fmt::format::Writer<'_>) -> std::fmt::Result {
write!(
w,
"{}",
chrono::Local::now().format("%Y-%m-%dT%H:%M:%S%.3f%:z")
)
}
}
pub fn init_tracing_from_config(config: &ObservabilityConfig) -> Result<(), TryInitError> {
let filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&config.log_level));
let (audit_writer, audit_warnings) = config
.audit_log_path
.as_ref()
.map_or((None, Vec::new()), |p| open_audit_file(p));
let result = if config.log_format == "json" {
let subscriber = tracing_subscriber::registry().with(filter).with(
tracing_subscriber::fmt::layer()
.json()
.with_timer(LocalTime)
.with_writer(std::io::stderr),
);
init_with_optional_audit(subscriber, audit_writer)
} else {
let subscriber = tracing_subscriber::registry().with(filter).with(
tracing_subscriber::fmt::layer()
.with_timer(LocalTime)
.with_writer(std::io::stderr),
);
init_with_optional_audit(subscriber, audit_writer)
};
if result.is_ok() {
for warning in audit_warnings {
tracing::warn!(warning = %warning, "audit logging initialization warning");
}
}
result
}
fn init_with_optional_audit<S>(
subscriber: S,
audit_writer: Option<AuditFile>,
) -> Result<(), TryInitError>
where
S: tracing::Subscriber
+ for<'span> tracing_subscriber::registry::LookupSpan<'span>
+ Send
+ Sync
+ 'static,
{
if let Some(writer) = audit_writer {
subscriber
.with(
tracing_subscriber::fmt::layer()
.json()
.with_timer(LocalTime)
.with_writer(writer)
.with_filter(tracing_subscriber::filter::LevelFilter::INFO),
)
.try_init()
} else {
subscriber.try_init()
}
}
pub fn init_tracing(default_filter: &str) -> Result<(), TryInitError> {
tracing_subscriber::registry()
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_filter)))
.with(
tracing_subscriber::fmt::layer()
.with_timer(LocalTime)
.with_writer(std::io::stderr),
)
.try_init()
}
#[derive(Clone)]
struct AuditFile(Arc<std::fs::File>);
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for AuditFile {
type Writer = AuditFileWriter;
fn make_writer(&'a self) -> Self::Writer {
AuditFileWriter(Arc::clone(&self.0))
}
}
struct AuditFileWriter(Arc<std::fs::File>);
impl std::io::Write for AuditFileWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
std::io::Write::write(&mut &*self.0, buf)
}
fn flush(&mut self) -> std::io::Result<()> {
std::io::Write::flush(&mut &*self.0)
}
}
fn open_audit_file(path: &Path) -> (Option<AuditFile>, Vec<String>) {
let mut warnings = Vec::new();
if let Some(parent) = path.parent()
&& !parent.exists()
&& let Err(e) = std::fs::create_dir_all(parent)
{
warnings.push(format!(
"failed to create audit log directory {}: {e}",
path.display()
));
return (None, warnings);
}
match std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
{
Ok(f) => {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Err(e) = f.set_permissions(std::fs::Permissions::from_mode(0o600)) {
warnings.push(format!("failed to set audit log permissions to 0o600: {e}"));
}
}
(Some(AuditFile(Arc::new(f))), warnings)
}
Err(e) => {
warnings.push(format!(
"failed to open audit log file {}: {e}",
path.display()
));
(None, warnings)
}
}
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
clippy::unwrap_in_result,
clippy::print_stdout,
clippy::print_stderr
)]
use super::{init_tracing, init_tracing_from_config};
use crate::config::ObservabilityConfig;
#[test]
fn config_format_valid() {
let config = ObservabilityConfig {
log_level: "debug".into(),
log_format: "json".into(),
audit_log_path: None,
log_request_headers: false,
metrics_enabled: false,
metrics_bind: "127.0.0.1:9090".into(),
};
assert!(config.log_format == "json" || config.log_format == "pretty");
}
#[test]
fn init_tracing_double_init_returns_err_not_panic() {
let _ = init_tracing("info");
let second = init_tracing("debug");
assert!(
second.is_err(),
"second init_tracing must return Err once a global subscriber exists"
);
let cfg = ObservabilityConfig {
log_level: "info".into(),
log_format: "pretty".into(),
audit_log_path: None,
log_request_headers: false,
metrics_enabled: false,
metrics_bind: "127.0.0.1:9090".into(),
};
let third = init_tracing_from_config(&cfg);
assert!(
third.is_err(),
"init_tracing_from_config must return Err once a global subscriber exists"
);
}
}