1use std::{
17 fmt,
18 io::{IsTerminal, Write},
19 str::FromStr,
20 sync::{Arc, Mutex},
21};
22
23use chacha20::ChaCha20Rng;
24use http::Request;
25use rand::{Rng, SeedableRng, rng};
26use tower_http::{
27 LatencyUnit,
28 classify::{ServerErrorsAsFailures, SharedClassifier},
29 trace::{DefaultOnFailure, DefaultOnResponse, MakeSpan, TraceLayer},
30};
31use tracing::Span;
32use tracing_appender::non_blocking::{NonBlocking, WorkerGuard};
33use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer};
34use tracing_subscriber::{
35 EnvFilter, Layer, Registry,
36 field::RecordFields,
37 fmt::{
38 FormatFields,
39 format::{DefaultFields, Format, Writer},
40 time::UtcTime,
41 },
42 prelude::*,
43};
44
45use crate::{dedup::DeduplicatingFormatter, log_metrics::LogEntriesLayer};
46
47pub mod dedup;
48pub mod log_metrics;
49pub mod metrics;
50pub mod prometheus_json;
51
52pub use tracing_subscriber;
53
54pub const LOG_LEVEL_ENV: &str = "RUST_LOG";
56
57#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
59#[serde(rename_all = "lowercase")]
60pub enum LogOutput {
61 #[default]
63 Stderr,
64 Stdout,
66}
67
68impl FromStr for LogOutput {
69 type Err = String;
70
71 fn from_str(s: &str) -> Result<Self, Self::Err> {
72 match s.to_lowercase().as_str() {
73 "stdout" => Ok(LogOutput::Stdout),
74 "stderr" => Ok(LogOutput::Stderr),
75 _ => {
76 Err(format!(
77 "Invalid log output: '{}', expected 'stdout' or 'stderr'",
78 s
79 ))
80 }
81 }
82 }
83}
84
85impl fmt::Display for LogOutput {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 match self {
88 LogOutput::Stdout => write!(f, "stdout"),
89 LogOutput::Stderr => write!(f, "stderr"),
90 }
91 }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
96#[serde(rename_all = "lowercase")]
97pub enum LogFormat {
98 #[default]
100 Text,
101 Json,
103}
104
105impl FromStr for LogFormat {
106 type Err = String;
107
108 fn from_str(s: &str) -> Result<Self, Self::Err> {
109 match s.to_lowercase().as_str() {
110 "text" => Ok(LogFormat::Text),
111 "json" => Ok(LogFormat::Json),
112 _ => {
113 Err(format!(
114 "Invalid log format: '{}', expected 'text' or 'json'",
115 s
116 ))
117 }
118 }
119 }
120}
121
122impl fmt::Display for LogFormat {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 match self {
125 LogFormat::Text => write!(f, "text"),
126 LogFormat::Json => write!(f, "json"),
127 }
128 }
129}
130
131pub struct TracingConfig {
133 console_output: Option<LogOutput>,
135 console_format: LogFormat,
136 console_dedup: bool,
138 log_entries: Option<LogEntriesLayer>,
140 directives: Vec<String>,
141 extra_layers: Vec<Box<dyn Layer<Registry> + Send + Sync + 'static>>,
142}
143
144impl Default for TracingConfig {
145 fn default() -> Self {
146 Self {
147 console_output: Some(LogOutput::Stderr),
149 console_format: LogFormat::Text,
150 console_dedup: false,
151 log_entries: None,
152 directives: Vec::new(),
153 extra_layers: Vec::new(),
154 }
155 }
156}
157
158impl TracingConfig {
159 pub fn new() -> Self {
161 Self::default()
162 }
163
164 pub fn with_output(mut self, output: LogOutput) -> Self {
169 self.console_output = Some(output);
170 self
171 }
172
173 pub fn with_format(mut self, format: LogFormat) -> Self {
175 self.console_format = format;
176 self
177 }
178
179 pub fn with_deduplication(mut self, enabled: bool) -> Self {
185 self.console_dedup = enabled;
186 self
187 }
188
189 pub fn with_log_metrics(mut self, registry: &metrics::registry::MetricsRegistry) -> Self {
196 self.log_entries = Some(LogEntriesLayer::new(registry));
197 self
198 }
199
200 pub fn add_directive<S: Into<String>>(mut self, directive: S) -> Self {
202 self.directives.push(directive.into());
203 self
204 }
205
206 pub fn add_directives<I, S>(mut self, directives: I) -> Self
208 where
209 I: IntoIterator<Item = S>,
210 S: AsRef<str>,
211 {
212 for directive in directives {
213 self.directives.push(directive.as_ref().to_string());
214 }
215 self
216 }
217
218 pub fn with_layer<L>(mut self, layer: L) -> Self
220 where
221 L: Layer<Registry> + Send + Sync + 'static,
222 {
223 self.extra_layers.push(layer.boxed());
224 self
225 }
226
227 pub fn init(self) -> Result<Vec<WorkerGuard>, TracingSetupError> {
231 tracing_log::LogTracer::init().map_err(|err| {
234 TracingSetupError {
235 message: format!("Failed to initialize log tracer: {err}"),
236 }
237 })?;
238
239 let TracingConfig {
240 console_output,
241 console_format,
242 console_dedup,
243 log_entries,
244 directives,
245 extra_layers,
246 } = self;
247
248 let make_filter = |directives: &[String]| -> Result<EnvFilter, TracingSetupError> {
252 let mut filter =
253 EnvFilter::try_from_env(LOG_LEVEL_ENV).unwrap_or_else(|_| EnvFilter::new("info"));
254 for d in directives {
255 filter = filter.add_directive(
256 d.parse::<tracing_subscriber::filter::Directive>()
257 .map_err(|_| {
258 TracingSetupError {
259 message: format!("Invalid log directive: {d}"),
260 }
261 })?,
262 );
263 }
264 Ok(filter)
265 };
266
267 let mut guards = vec![];
268 let mut layers = vec![JsonStorageLayer.boxed()];
269
270 if let Some(output) = console_output {
271 let (writer, guard, ansi) = match output {
272 LogOutput::Stdout => {
273 let (writer, guard) = tracing_appender::non_blocking(std::io::stdout());
274 (writer, guard, std::io::stdout().is_terminal())
275 }
276 LogOutput::Stderr => {
277 let (writer, guard) = tracing_appender::non_blocking(std::io::stderr());
278 (writer, guard, std::io::stderr().is_terminal())
279 }
280 };
281 layers.push(console_layer(
282 writer,
283 &console_format,
284 ansi,
285 console_dedup,
286 make_filter(&directives)?,
287 ));
288 guards.push(guard);
289 }
290
291 if let Some(layer) = log_entries {
292 layers.push(layer.with_filter(make_filter(&directives)?).boxed());
293 }
294
295 for layer in extra_layers {
297 layers.push(layer.with_filter(make_filter(&directives)?).boxed());
298 }
299
300 let subscriber = Registry::default().with(layers);
302 tracing::subscriber::set_global_default(subscriber).map_err(|err| {
303 TracingSetupError {
304 message: format!("Failed to set global tracing subscriber: {err}"),
305 }
306 })?;
307
308 tracing::debug!("Logging initialized!");
309 Ok(guards)
310 }
311}
312
313#[derive(Default)]
325struct ConsoleFields(DefaultFields);
326
327impl<'writer> FormatFields<'writer> for ConsoleFields {
328 fn format_fields<R: RecordFields>(&self, writer: Writer<'writer>, fields: R) -> fmt::Result {
329 self.0.format_fields(writer, fields)
330 }
331}
332
333fn console_layer(
336 writer: NonBlocking,
337 format: &LogFormat,
338 ansi: bool,
339 dedup: bool,
340 filter: EnvFilter,
341) -> Box<dyn Layer<Registry> + Send + Sync + 'static> {
342 match format {
343 LogFormat::Json => {
344 let fmt = Format::default().json().with_timer(UtcTime::rfc_3339());
345 let layer = tracing_subscriber::fmt::layer().json().with_writer(writer);
346 if dedup {
347 let fmt = DeduplicatingFormatter::new(fmt).with_format(LogFormat::Json);
348 layer.event_format(fmt).with_filter(filter).boxed()
349 } else {
350 layer.event_format(fmt).with_filter(filter).boxed()
351 }
352 }
353 LogFormat::Text => {
354 let fmt = Format::default()
355 .with_timer(UtcTime::rfc_3339())
356 .with_ansi(ansi);
357 let layer = tracing_subscriber::fmt::layer()
358 .fmt_fields(ConsoleFields::default())
359 .with_writer(writer);
360 if dedup {
361 layer
362 .event_format(DeduplicatingFormatter::new(fmt))
363 .with_filter(filter)
364 .boxed()
365 } else {
366 layer.event_format(fmt).with_filter(filter).boxed()
367 }
368 }
369 }
370}
371
372#[derive(Debug)]
374pub struct TracingSetupError {
375 message: String,
376}
377
378impl fmt::Display for TracingSetupError {
379 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
380 write!(f, "{}", self.message)
381 }
382}
383
384impl std::error::Error for TracingSetupError {}
385
386#[allow(unused)]
387fn json_formatted_layer<W: Write + Send + 'static>(
388 w: W,
389) -> (BunyanFormattingLayer<NonBlocking>, WorkerGuard) {
390 let app_name = env!("CARGO_PKG_NAME").to_string();
391 let (non_blocking_writer, guard) = tracing_appender::non_blocking(w);
392 (
393 BunyanFormattingLayer::new(app_name, non_blocking_writer),
394 guard,
395 )
396}
397
398pub fn info_trace_layer() -> TraceLayer<SharedClassifier<ServerErrorsAsFailures>, RandomSpans> {
400 let lvl = tracing::Level::INFO;
401 let trace_id_seed = rng().next_u64();
402 let latency_unit = LatencyUnit::Nanos;
403
404 TraceLayer::new_for_http()
405 .make_span_with(RandomSpans::new(trace_id_seed))
406 .on_failure(
407 DefaultOnFailure::new()
408 .latency_unit(latency_unit)
409 .level(lvl),
410 )
411 .on_response(
412 DefaultOnResponse::new()
413 .latency_unit(latency_unit)
414 .level(lvl),
415 )
416}
417
418#[derive(Clone)]
420pub struct RandomSpans {
421 counter: Arc<Mutex<ChaCha20Rng>>,
422}
423
424impl RandomSpans {
425 fn new(seed: u64) -> Self {
426 Self {
427 counter: Arc::new(Mutex::new(ChaCha20Rng::seed_from_u64(seed))),
428 }
429 }
430}
431
432impl<B> MakeSpan<B> for RandomSpans {
433 fn make_span(&mut self, request: &Request<B>) -> Span {
434 let cur = self.counter.lock().unwrap().next_u64();
435 let span_id = format!("{cur:016x}");
436 tracing::span!(
437 tracing::Level::INFO,
438 "request",
439 span_id = span_id,
440 method = %request.method(),
441 uri = %request.uri(),
442 version = ?request.version(),
443 )
444 }
445}
446
447#[cfg(test)]
448mod tests {
449 use std::sync::{Arc, Mutex};
450
451 use tracing_subscriber::{EnvFilter, Layer, Registry, prelude::*};
452
453 use super::{LogFormat, console_layer};
454
455 #[derive(Clone)]
457 struct BufWriter(Arc<Mutex<Vec<u8>>>);
458
459 impl std::io::Write for BufWriter {
460 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
461 self.0.lock().unwrap().extend_from_slice(buf);
462 Ok(buf.len())
463 }
464
465 fn flush(&mut self) -> std::io::Result<()> {
466 Ok(())
467 }
468 }
469
470 #[test]
479 fn console_ansi_does_not_leak_into_extra_fmt_layer() {
480 let captured = Arc::new(Mutex::new(Vec::new()));
481
482 let (console_writer, _guard) = tracing_appender::non_blocking(std::io::sink());
484 let console = console_layer(
485 console_writer,
486 &LogFormat::Text,
487 true,
488 false,
489 EnvFilter::new("info"),
490 );
491
492 let extra = {
495 let captured = captured.clone();
496 tracing_subscriber::fmt::layer()
497 .with_ansi(false)
498 .with_writer(move || BufWriter(captured.clone()))
499 .boxed()
500 };
501
502 let layers: Vec<Box<dyn Layer<Registry> + Send + Sync>> = vec![console, extra];
504 let subscriber = Registry::default().with(layers);
505
506 tracing::subscriber::with_default(subscriber, || {
507 let span = tracing::info_span!("meta-comment", test = "test-field");
508 let _guard = span.enter();
509 tracing::info!("test log");
510 });
511
512 let out = String::from_utf8(captured.lock().unwrap().clone()).expect("valid utf-8");
513 assert!(
514 !out.contains('\u{1b}'),
515 "extra layer must not inherit ANSI escapes from the console layer, got: {out:?}"
516 );
517 assert!(
519 out.contains("meta-comment") && out.contains("test-field"),
520 "extra layer should still render span name and fields, got: {out:?}"
521 );
522 }
523}