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 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#[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#[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
94pub 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}