1pub(crate) mod json;
4pub(crate) mod span;
5
6use std::{collections::BTreeMap, fs, io};
7
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10use tracing_appender::{
11 non_blocking::{NonBlocking, NonBlockingBuilder, WorkerGuard},
12 rolling::{RollingFileAppender, Rotation},
13};
14use tracing_subscriber::{
15 filter::{LevelFilter, Targets},
16 fmt::{self, writer::BoxMakeWriter},
17 layer::{Layer, Layered, SubscriberExt},
18 registry::Registry,
19};
20
21use crate::logging::json::{ExtensibleJsonFormat, JsonKeyNames};
22
23type LoggingRegistry = Layered<Vec<Box<dyn Layer<Registry> + Send + Sync>>, Registry>;
24
25#[derive(Debug, Error)]
27pub enum LoggingError {
28 #[error("Log destination I/O error: {0}")]
30 Io(#[from] io::Error),
31 #[error("Error while initializing log directory writer: {0}")]
33 Directory(#[from] tracing_appender::rolling::InitError),
34}
35
36#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
38#[non_exhaustive]
39pub struct LoggingConfig {
40 #[serde(default, skip_serializing_if = "Vec::is_empty")]
42 pub subscribers: Vec<LoggingSubscriberConfig>,
43}
44
45impl LoggingConfig {
46 pub fn make_registry(&self) -> Result<(LoggingRegistry, Vec<WorkerGuard>), LoggingError> {
52 let num_subs = self.subscribers.len();
53 let (subs, buf_guards) = self.subscribers.iter().try_fold(
54 (Vec::with_capacity(num_subs), Vec::with_capacity(num_subs)),
55 |(mut acc_s, mut acc_g), sub_cfg| {
56 let (sub, guard) = sub_cfg.make_layer()?;
57 acc_s.push(sub);
58 acc_g.push(guard);
59 Ok::<_, LoggingError>((acc_s, acc_g))
60 },
61 )?;
62 Ok((Registry::default().with(subs), buf_guards))
63 }
64}
65
66#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
68#[non_exhaustive]
69pub struct LoggingSubscriberConfig {
70 #[serde(default, flatten)]
72 pub format: LoggingFormat,
73 #[serde(default)]
75 pub level: LoggingLevel,
76 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
78 pub targets: BTreeMap<String, LoggingLevel>,
79 #[serde(default)]
81 pub color: bool,
82 #[serde(default = "crate::util::default_true")]
84 pub internal_errors: bool,
85 #[serde(default)]
87 pub print: LoggingPrintingConfig,
88 #[serde(default)]
90 pub buffer: LoggingBufferConfig,
91 #[serde(default)]
93 pub output: LoggingDestination,
94}
95
96impl Default for LoggingSubscriberConfig {
97 fn default() -> Self {
98 Self {
99 format: LoggingFormat::default(),
100 level: LoggingLevel::default(),
101 targets: BTreeMap::new(),
102 color: false,
103 internal_errors: true,
104 print: LoggingPrintingConfig::default(),
105 buffer: LoggingBufferConfig::default(),
106 output: LoggingDestination::default(),
107 }
108 }
109}
110
111impl LoggingSubscriberConfig {
112 #[must_use]
114 pub fn default_for_dev() -> Self {
115 Self {
116 format: LoggingFormat::Pretty,
117 level: LoggingLevel::Trace,
118 targets: BTreeMap::new(),
119 color: true,
120 internal_errors: true,
121 print: LoggingPrintingConfig {
122 target: true,
123 file: true,
124 line_number: true,
125 level: true,
126 thread_name: true,
127 thread_id: false,
128 },
129 buffer: LoggingBufferConfig::default(),
130 output: LoggingDestination::default(),
131 }
132 }
133
134 pub fn make_layer<T>(
136 &self,
137 ) -> Result<(Box<dyn Layer<T> + Send + Sync>, WorkerGuard), LoggingError>
138 where
139 T: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
140 {
141 let buf_builder = self.buffer.make_builder();
142 let (buf_writer, buf_guard) = self.output.make_writer(buf_builder)?;
143 let layer = fmt::layer()
144 .with_writer(buf_writer)
145 .with_ansi(self.color)
146 .log_internal_errors(self.internal_errors)
147 .with_target(self.print.target)
148 .with_file(self.print.file)
149 .with_line_number(self.print.line_number)
150 .with_level(self.print.level)
151 .with_thread_names(self.print.thread_name)
152 .with_thread_ids(self.print.thread_id);
153 let boxed_layer = match self.format {
154 LoggingFormat::Full => layer.boxed(),
155 LoggingFormat::Compact => layer.compact().boxed(),
156 LoggingFormat::Pretty => layer.pretty().boxed(),
157 LoggingFormat::Json {
158 flatten_metadata,
159 current_span,
160 ref static_fields,
161 ref key_names,
162 } => {
163 let json_fmt = ExtensibleJsonFormat::new()
164 .with_target(self.print.target)
165 .with_file(self.print.file)
166 .with_line_number(self.print.line_number)
167 .with_level(self.print.level)
168 .with_thread_names(self.print.thread_name)
169 .with_thread_ids(self.print.thread_id)
170 .flatten_event(flatten_metadata)
171 .with_current_span(current_span)
172 .with_static_fields(static_fields.clone())
173 .with_key_names(*key_names.clone());
174 layer.json().event_format(json_fmt).boxed()
175 }
176 };
177 let boxed_layer = if self.targets.is_empty() {
178 boxed_layer
179 .with_filter(LevelFilter::from(self.level))
180 .boxed()
181 } else {
182 boxed_layer
183 .with_filter(
184 Targets::new()
185 .with_targets(self.targets.clone())
186 .with_default(LevelFilter::from(self.level)),
187 )
188 .boxed()
189 };
190 Ok((boxed_layer, buf_guard))
191 }
192}
193
194#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
196#[non_exhaustive]
197#[serde(rename_all = "lowercase", tag = "format")]
198pub enum LoggingFormat {
199 #[default]
203 Full,
204 Compact,
208 Pretty,
213 Json {
217 #[serde(default)]
221 flatten_metadata: bool,
222 #[serde(default)]
226 current_span: bool,
227 #[serde(default)]
229 static_fields: BTreeMap<String, serde_json::Value>,
230 #[serde(default)]
232 key_names: Box<JsonKeyNames>,
233 },
234}
235
236#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize)]
238#[serde(rename_all = "UPPERCASE")]
239pub enum LoggingLevel {
240 #[serde(alias = "off", alias = "disabled", alias = "DISABLED")]
244 Off,
245 #[serde(alias = "error", alias = "err", alias = "ERR")]
249 Error,
250 #[serde(alias = "warn", alias = "warning", alias = "WARNING")]
254 Warn,
255 #[serde(alias = "info")]
259 Info,
260 #[serde(alias = "debug")]
264 #[default]
265 Debug,
266 #[serde(alias = "trace")]
270 Trace,
271}
272
273impl From<LoggingLevel> for LevelFilter {
274 fn from(value: LoggingLevel) -> Self {
275 match value {
276 LoggingLevel::Off => LevelFilter::OFF,
277 LoggingLevel::Error => LevelFilter::ERROR,
278 LoggingLevel::Warn => LevelFilter::WARN,
279 LoggingLevel::Info => LevelFilter::INFO,
280 LoggingLevel::Debug => LevelFilter::DEBUG,
281 LoggingLevel::Trace => LevelFilter::TRACE,
282 }
283 }
284}
285
286#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
288#[non_exhaustive]
289#[allow(clippy::struct_excessive_bools)]
290pub struct LoggingPrintingConfig {
291 #[serde(default)]
293 pub target: bool,
294 #[serde(default)]
296 pub file: bool,
297 #[serde(default)]
299 pub line_number: bool,
300 #[serde(default = "crate::util::default_true")]
302 pub level: bool,
303 #[serde(default)]
305 pub thread_name: bool,
306 #[serde(default)]
308 pub thread_id: bool,
309}
310
311impl Default for LoggingPrintingConfig {
312 fn default() -> Self {
313 Self {
314 target: false,
315 file: false,
316 line_number: false,
317 level: true,
318 thread_name: false,
319 thread_id: false,
320 }
321 }
322}
323
324#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
326#[non_exhaustive]
327pub struct LoggingBufferConfig {
328 #[serde(default = "LoggingBufferConfig::default_lines")]
335 pub lines: usize,
336 #[serde(default = "crate::util::default_true")]
343 pub lossy: bool,
344 #[serde(default, skip_serializing_if = "Option::is_none")]
348 pub thread_name: Option<String>,
349}
350
351impl Default for LoggingBufferConfig {
352 fn default() -> Self {
353 Self {
354 lines: Self::default_lines(),
355 lossy: true,
356 thread_name: None,
357 }
358 }
359}
360
361impl LoggingBufferConfig {
362 #[must_use]
364 #[inline]
365 fn default_lines() -> usize {
366 128_000
367 }
368
369 #[must_use]
371 pub fn make_builder(&self) -> NonBlockingBuilder {
372 let mut builder = NonBlockingBuilder::default()
373 .buffered_lines_limit(self.lines)
374 .lossy(self.lossy);
375 if let Some(thr_name) = &self.thread_name {
376 builder = builder.thread_name(thr_name);
377 }
378 builder
379 }
380
381 pub fn make_writer<W>(&self, ll_writer: W) -> (NonBlocking, WorkerGuard)
383 where
384 W: io::Write + Send + 'static,
385 {
386 self.make_builder().finish(ll_writer)
387 }
388}
389
390#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
392#[non_exhaustive]
393#[serde(rename_all = "lowercase", tag = "type")]
394pub enum LoggingDestination {
395 #[default]
397 #[serde(alias = "stdout", alias = "out")]
398 StdOut,
399 #[serde(alias = "stderr", alias = "err")]
401 StdErr,
402 File(LoggingFileConfig),
404 #[serde(alias = "dir")]
406 Directory(LoggingDirectoryConfig),
407}
408
409impl LoggingDestination {
410 pub fn make_writer(
412 &self,
413 buf_builder: NonBlockingBuilder,
414 ) -> Result<(BoxMakeWriter, WorkerGuard), LoggingError> {
415 match self {
416 Self::StdOut => {
417 let (wr, wg) = buf_builder.finish(io::stdout());
418 Ok((BoxMakeWriter::new(wr), wg))
419 }
420 Self::StdErr => {
421 let (wr, wg) = buf_builder.finish(io::stderr());
422 Ok((BoxMakeWriter::new(wr), wg))
423 }
424 Self::File(file_cfg) => {
425 let file = fs::OpenOptions::new()
426 .append(true)
427 .create(true)
428 .open(&file_cfg.path)?;
429 let (wr, wg) = buf_builder.finish(file);
430 Ok((BoxMakeWriter::new(wr), wg))
431 }
432 Self::Directory(dir_cfg) => {
433 let mut builder = RollingFileAppender::builder().rotation(dir_cfg.rotate.into());
434 if let Some(prefix) = &dir_cfg.prefix {
435 builder = builder.filename_prefix(prefix);
436 }
437 if let Some(suffix) = &dir_cfg.suffix {
438 builder = builder.filename_suffix(suffix);
439 }
440 if let Some(max_files) = dir_cfg.max_files {
441 builder = builder.max_log_files(max_files);
442 }
443 let appender = builder.build(&dir_cfg.path)?;
444 let (wr, wg) = buf_builder.finish(appender);
445 Ok((BoxMakeWriter::new(wr), wg))
446 }
447 }
448 }
449}
450
451#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
453#[non_exhaustive]
454pub struct LoggingFileConfig {
455 pub path: String,
457}
458
459#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
461#[non_exhaustive]
462pub struct LoggingDirectoryConfig {
463 #[serde(default = "LoggingDirectoryConfig::default_path")]
465 pub path: String,
466 #[serde(default)]
468 pub rotate: LogRotation,
469 #[serde(default, skip_serializing_if = "Option::is_none")]
471 pub prefix: Option<String>,
472 #[serde(default = "LoggingDirectoryConfig::default_suffix")]
474 pub suffix: Option<String>,
475 #[serde(default)]
477 pub max_files: Option<usize>,
478}
479
480impl Default for LoggingDirectoryConfig {
481 fn default() -> Self {
482 Self {
483 path: Self::default_path(),
484 rotate: LogRotation::default(),
485 prefix: None,
486 suffix: Self::default_suffix(),
487 max_files: None,
488 }
489 }
490}
491
492impl LoggingDirectoryConfig {
493 #[must_use]
495 #[inline]
496 fn default_path() -> String {
497 ".".into()
498 }
499
500 #[must_use]
502 #[inline]
503 #[allow(clippy::unnecessary_wraps)]
504 fn default_suffix() -> Option<String> {
505 Some("log".into())
506 }
507}
508
509#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize)]
511#[non_exhaustive]
512#[serde(rename_all = "UPPERCASE")]
513pub enum LogRotation {
514 Minutely,
516 Hourly,
518 #[default]
520 Daily,
521 Never,
523}
524
525impl From<LogRotation> for Rotation {
526 fn from(value: LogRotation) -> Self {
527 match value {
528 LogRotation::Minutely => Rotation::MINUTELY,
529 LogRotation::Hourly => Rotation::HOURLY,
530 LogRotation::Daily => Rotation::DAILY,
531 LogRotation::Never => Rotation::NEVER,
532 }
533 }
534}