Skip to main content

atb_logging/
lib.rs

1use std::path::PathBuf;
2
3use tracing_appender::{
4    non_blocking::{NonBlocking, NonBlockingBuilder, WorkerGuard},
5    rolling::{RollingFileAppender, Rotation},
6};
7use tracing_subscriber::{
8    EnvFilter, Registry,
9    fmt::{self, Layer as FmtLayer, format},
10    layer::SubscriberExt,
11};
12
13pub use tracing_appender;
14pub use tracing_subscriber;
15
16pub struct TraceOpts {
17    pub filters: Option<String>,
18    pub buffer: usize,
19    pub lossy: bool,
20    pub json: bool,
21    /// When present, logs are written to a rolling file instead of stdout.
22    pub file: Option<FileSinkOpts>,
23}
24
25impl Default for TraceOpts {
26    fn default() -> Self {
27        Self {
28            filters: None,
29            buffer: 20_000,
30            lossy: false,
31            json: false,
32            file: None,
33        }
34    }
35}
36
37#[derive(Clone)]
38pub struct FileSinkOpts {
39    pub directory: PathBuf,
40    pub file_name: String,
41    pub rotation: Rotation,
42}
43
44impl Default for FileSinkOpts {
45    fn default() -> Self {
46        Self {
47            directory: PathBuf::from("./target/logs"),
48            file_name: "app.log".to_string(),
49            rotation: Rotation::DAILY,
50        }
51    }
52}
53
54/// Initialize tracing. Keep the returned guard alive until shutdown.
55#[must_use = "keep the returned WorkerGuard alive or logs may be dropped"]
56pub fn init_tracer(opts: TraceOpts) -> anyhow::Result<WorkerGuard> {
57    let (writer, guard) = build_nonblocking(&opts);
58    let env_filter = opts
59        .filters
60        .as_ref()
61        .map(|filters| EnvFilter::builder().parse_lossy(filters))
62        .unwrap_or_else(|| {
63            EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"))
64        });
65
66    if opts.json {
67        let subscriber = Registry::default()
68            .with(noisy_layer_json().with_writer(writer))
69            .with(env_filter);
70        tracing::subscriber::set_global_default(subscriber)?;
71    } else {
72        let subscriber = Registry::default()
73            .with(
74                noisy_pretty_layer()
75                    .with_ansi(opts.file.is_none())
76                    .with_writer(writer),
77            )
78            .with(env_filter);
79        tracing::subscriber::set_global_default(subscriber)?;
80    }
81    Ok(guard)
82}
83
84/// Initialize a JSON rolling-file tracer with default settings.
85#[must_use = "keep the returned WorkerGuard alive or logs may be dropped"]
86pub fn init_file_tracer() -> anyhow::Result<WorkerGuard> {
87    init_tracer(TraceOpts {
88        file: Some(FileSinkOpts::default()),
89        json: true,
90        ..Default::default()
91    })
92}
93
94/// Build a non-blocking stdout or rolling-file writer.
95pub fn build_nonblocking(opts: &TraceOpts) -> (NonBlocking, WorkerGuard) {
96    let builder = NonBlockingBuilder::default()
97        .buffered_lines_limit(opts.buffer)
98        .lossy(opts.lossy);
99    if let Some(file) = &opts.file {
100        builder.finish(RollingFileAppender::new(
101            file.rotation.clone(),
102            &file.directory,
103            &file.file_name,
104        ))
105    } else {
106        builder.finish(std::io::stdout())
107    }
108}
109
110pub fn noisy_pretty_layer() -> FmtLayer<Registry> {
111    fmt::layer()
112        .with_span_events(format::FmtSpan::CLOSE)
113        .with_target(true)
114        .with_line_number(true)
115        .with_file(true)
116        .with_ansi(true)
117        .with_thread_ids(true)
118        .with_thread_names(true)
119}
120
121pub fn noisy_layer_json() -> FmtLayer<Registry, format::JsonFields, format::Format<format::Json>> {
122    fmt::layer()
123        .json()
124        .with_current_span(true)
125        .with_span_events(fmt::format::FmtSpan::CLOSE)
126        .with_span_list(true)
127        .with_target(true)
128        .with_line_number(true)
129        .with_file(true)
130        .with_thread_ids(true)
131        .with_thread_names(true)
132        .with_ansi(false)
133        .flatten_event(true)
134}
135
136#[deprecated(note = "use init_tracer; this will be removed in a future major release")]
137pub fn init_logger(pattern: &str, deep: bool) {
138    use ansi_term::Colour;
139    use chrono::Utc;
140    use std::io::Write;
141
142    let mut builder = env_logger::Builder::new();
143    builder.parse_filters(pattern);
144    if let Ok(level) = std::env::var("RUST_LOG") {
145        builder.parse_filters(&level);
146    }
147
148    let json_enabled = std::env::var("RUST_LOG_JSON")
149        .map(|value| value == "1" || value.eq_ignore_ascii_case("true"))
150        .unwrap_or(false);
151
152    if json_enabled {
153        builder.format(move |buffer, record| {
154            use serde_json::json;
155            let timestamp = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
156            let payload = json!({
157                "ts": timestamp,
158                "level": record.level().to_string(),
159                "target": record.target(),
160                "module_path": record.module_path(),
161                "file": record.file(),
162                "line": record.line(),
163                "msg": record.args().to_string(),
164            });
165            writeln!(buffer, "{payload}")
166        });
167    } else if deep {
168        builder.format(move |buffer, record| {
169            let timestamp = Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
170            let level = colored_level(record.level());
171            writeln!(
172                buffer,
173                "[{level}] {} {}\n  {}|{}:{}",
174                Colour::Blue.bold().paint(timestamp),
175                record.args(),
176                record.module_path().unwrap_or("UNKNOWN_MODULE"),
177                record.file().unwrap_or("UNKNOWN_FILE"),
178                record.line().unwrap_or(0),
179            )
180        });
181    } else {
182        builder.format(move |buffer, record| {
183            let timestamp = Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
184            let level = colored_level(record.level());
185            writeln!(
186                buffer,
187                "[{level}] {} {}",
188                Colour::Blue.bold().paint(timestamp),
189                record.args(),
190            )
191        });
192    }
193
194    if builder.try_init().is_err() {
195        log::trace!("global logger already initialized; skipping initialization");
196    }
197}
198
199fn colored_level(level: log::Level) -> ansi_term::ANSIString<'static> {
200    use ansi_term::Colour;
201
202    match level {
203        log::Level::Error => Colour::Red.bold().paint("ERR"),
204        log::Level::Warn => Colour::Yellow.bold().paint("WRN"),
205        log::Level::Info => Colour::Green.bold().paint("INF"),
206        log::Level::Debug => Colour::Cyan.bold().paint("DBG"),
207        log::Level::Trace => Colour::White.bold().paint("TRC"),
208    }
209}