use std::path::PathBuf;
use std::sync::Arc;
use tracing::field::{Field, Visit};
use tracing::{Event, Level, Subscriber};
use tracing_subscriber::Layer;
use tracing_subscriber::layer::Context;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum LogRotation {
#[default]
Daily,
Hourly,
Never,
}
#[derive(Clone)]
pub enum LogOutput {
Stdout,
File {
path: PathBuf,
rotation: LogRotation,
},
Custom(Arc<dyn LogSink>),
}
impl std::fmt::Debug for LogOutput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Stdout => f.write_str("Stdout"),
Self::File { path, rotation } => f
.debug_struct("File")
.field("path", path)
.field("rotation", rotation)
.finish(),
Self::Custom(_) => f.write_str("Custom(<sink>)"),
}
}
}
impl LogOutput {
#[must_use]
pub fn stdout() -> Self {
Self::Stdout
}
#[must_use]
pub fn file(path: impl Into<PathBuf>) -> Self {
Self::File {
path: path.into(),
rotation: LogRotation::Daily,
}
}
#[must_use]
pub fn sink(sink: impl LogSink) -> Self {
Self::Custom(Arc::new(sink))
}
#[must_use]
pub fn rotation(mut self, rotation: LogRotation) -> Self {
if let Self::File { rotation: r, .. } = &mut self {
*r = rotation;
}
self
}
pub(super) fn is_json(path: &std::path::Path) -> bool {
path.extension()
.and_then(|e| e.to_str())
.is_some_and(|e| e.eq_ignore_ascii_case("json"))
}
}
#[derive(Debug)]
pub struct LogRecord<'a> {
pub level: Level,
pub target: &'a str,
pub file: Option<&'a str>,
pub line: Option<u32>,
pub message: String,
pub fields: Vec<(&'static str, String)>,
}
pub trait LogSink: Send + Sync + 'static {
fn log(&self, record: &LogRecord<'_>);
}
pub(super) struct SinkLayer(Arc<dyn LogSink>);
impl SinkLayer {
pub(super) fn new(sink: Arc<dyn LogSink>) -> Self {
Self(sink)
}
}
impl<S: Subscriber> Layer<S> for SinkLayer {
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
let meta = event.metadata();
let mut visitor = RecordVisitor::default();
event.record(&mut visitor);
let record = LogRecord {
level: *meta.level(),
target: meta.target(),
file: meta.file(),
line: meta.line(),
message: visitor.message,
fields: visitor.fields,
};
self.0.log(&record);
}
}
#[derive(Default)]
struct RecordVisitor {
message: String,
fields: Vec<(&'static str, String)>,
}
impl Visit for RecordVisitor {
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" {
self.message = format!("{value:?}");
} else {
self.fields.push((field.name(), format!("{value:?}")));
}
}
fn record_str(&mut self, field: &Field, value: &str) {
self.fields.push((field.name(), value.to_string()));
}
fn record_i64(&mut self, field: &Field, value: i64) {
self.fields.push((field.name(), value.to_string()));
}
fn record_u64(&mut self, field: &Field, value: u64) {
self.fields.push((field.name(), value.to_string()));
}
fn record_bool(&mut self, field: &Field, value: bool) {
self.fields.push((field.name(), value.to_string()));
}
fn record_f64(&mut self, field: &Field, value: f64) {
self.fields.push((field.name(), value.to_string()));
}
}