tracing_configuration/
lib.rs

1//! Configuration-as-a-struct for [`tracing_subscriber::fmt::Subscriber`], to allow
2//! for serializable, dynamic configuration, at the cost of compile-time specialization.
3
4pub mod format;
5pub mod time;
6pub mod writer;
7
8#[cfg(feature = "clap4")]
9use clap::{
10    builder::PossibleValue,
11    builder::{TypedValueParser, ValueParser, ValueParserFactory},
12    ValueEnum,
13};
14#[cfg(feature = "schemars1")]
15use schemars::JsonSchema;
16#[cfg(feature = "serde1")]
17use serde::{Deserialize, Serialize};
18#[cfg(feature = "serde1")]
19use serde_with::*;
20use std::{fmt, path::PathBuf, str::FromStr};
21use tracing_subscriber::{filter::Filtered, fmt::format::FmtSpan, EnvFilter, Layer as _};
22use winnow::{
23    combinator::{alt, preceded},
24    token::rest,
25    Parser as _,
26};
27
28use writer::Guard;
29
30/// Configuration for a totally dynamic subscriber.
31#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default)]
32#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
33#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
34pub struct Subscriber {
35    #[cfg_attr(feature = "serde1", serde(skip_serializing_if = "Option::is_none"))]
36    pub format: Option<Format>,
37    #[cfg_attr(feature = "serde1", serde(skip_serializing_if = "Option::is_none"))]
38    pub writer: Option<Writer>,
39    #[cfg_attr(feature = "serde1", serde(skip_serializing_if = "Option::is_none"))]
40    pub filter: Option<Filter>,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default)]
44#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
45#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
46pub struct Filter {
47    #[cfg_attr(feature = "serde1", serde(skip_serializing_if = "Option::is_none"))]
48    pub regex: Option<bool>,
49    #[cfg_attr(
50        feature = "serde1",
51        serde(
52            default,
53            skip_serializing_if = "Vec::is_empty",
54            with = "As::<Vec<DisplayFromStr>>"
55        )
56    )]
57    #[cfg_attr(feature = "schemars1", schemars(with = "Vec<String>"))]
58    pub directives: Vec<tracing_subscriber::filter::Directive>,
59}
60
61impl From<Filter> for EnvFilter {
62    fn from(value: Filter) -> Self {
63        let Filter { regex, directives } = value;
64        let mut builder = EnvFilter::builder();
65        if let Some(regex) = regex {
66            builder = builder.with_regex(regex)
67        }
68        directives
69            .into_iter()
70            .fold(builder.parse_lossy(""), EnvFilter::add_directive)
71    }
72}
73
74#[derive(Debug)]
75pub struct ParseError(&'static str);
76
77impl fmt::Display for ParseError {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        f.write_str(self.0)
80    }
81}
82
83impl std::error::Error for ParseError {}
84
85/// A totally dynamically configured [`tracing_subscriber::fmt::SubscriberBuilder`].
86pub type SubscriberBuilder<
87    N = format::FormatFields,
88    E = format::FormatEvent,
89    F = EnvFilter,
90    W = writer::MakeWriter,
91> = tracing_subscriber::fmt::SubscriberBuilder<N, E, F, W>;
92
93/// A totally dynamically configured [`tracing_subscriber::fmt::Layer`].
94pub type Layer<S, N = format::FormatFields, E = format::FormatEvent, W = writer::MakeWriter> =
95    Filtered<tracing_subscriber::fmt::Layer<S, N, E, W>, EnvFilter, S>;
96
97impl Subscriber {
98    #[expect(clippy::type_complexity)]
99    fn into_components(
100        self,
101        defer: bool,
102    ) -> Result<
103        (
104            writer::MakeWriter,
105            format::FormatFields,
106            format::FormatEvent,
107            EnvFilter,
108            Guard,
109            Option<FmtSpan>,
110        ),
111        writer::Error,
112    > {
113        let Self {
114            format,
115            writer,
116            filter,
117        } = self;
118        let mut format = format.unwrap_or_default();
119        let writer = writer.unwrap_or_default();
120        let (writer, guard) = match defer {
121            true => writer::MakeWriter::try_new(writer)?,
122            false => writer::MakeWriter::new(writer),
123        };
124        let fields = format::FormatFields::from(format.formatter.clone().unwrap_or_default());
125        let span_events = format.span_events.take();
126        let event = format::FormatEvent::from(format);
127        let filter = EnvFilter::from(filter.unwrap_or_default());
128        Ok((writer, fields, event, filter, guard, span_events))
129    }
130    /// Create a new [`Layer`], and a [`Guard`] that handles e.g flushing [`NonBlocking`] IO.
131    ///
132    /// Errors when opening files or directories are deferred for the subscriber to handle (typically by logging).
133    /// If you wish to handle them yourself, see [`Self::try_layer`].
134    ///
135    /// Note that filtering is ignored for layers.
136    pub fn layer<S>(self) -> (Layer<S>, Guard)
137    where
138        S: tracing_core::Subscriber + for<'s> tracing_subscriber::registry::LookupSpan<'s>,
139    {
140        let (writer, fields, event, filter, guard, span_events) = self
141            .into_components(true)
142            .expect("errors have been deferred");
143        let layer = tracing_subscriber::fmt::layer()
144            .with_span_events(span_events.unwrap_or(FmtSpan::NONE))
145            .fmt_fields(fields)
146            .event_format(event)
147            .with_writer(writer)
148            .with_filter(filter);
149        (layer, guard)
150    }
151    /// Create a new [`Layer`], and a [`Guard`] that handles e.g flushing [`NonBlocking`] IO.
152    ///
153    /// Returns [`Err`] if e.g opening a log file fails.
154    /// If you wish the subscriber to handle them (typically by logging), see [`Self::layer`].
155    ///
156    /// Note that filtering is ignored for layers.
157    pub fn try_layer<S>(self) -> Result<(Layer<S>, Guard), writer::Error>
158    where
159        S: tracing_core::Subscriber + for<'s> tracing_subscriber::registry::LookupSpan<'s>,
160    {
161        let (writer, fields, event, filter, guard, span_events) = self.into_components(false)?;
162        let layer = tracing_subscriber::fmt::layer()
163            .with_span_events(span_events.unwrap_or(FmtSpan::NONE))
164            .fmt_fields(fields)
165            .event_format(event)
166            .with_writer(writer)
167            .with_filter(filter);
168        Ok((layer, guard))
169    }
170    /// Create a new [`SubscriberBuilder`], and a [`Guard`] that handles e.g flushing [`NonBlocking`] IO.
171    ///
172    /// Errors when opening files or directories are deferred for the subscriber to handle (typically by logging).
173    /// If you wish to handle them yourself, see [`Self::try_builder`].
174    pub fn builder(self) -> (SubscriberBuilder, Guard) {
175        let (writer, fields, event, filter, guard, span_events) = self
176            .into_components(true)
177            .expect("errors have been deferred");
178        let builder = tracing_subscriber::fmt()
179            .with_span_events(span_events.unwrap_or(FmtSpan::NONE))
180            .fmt_fields(fields)
181            .event_format(event)
182            .with_writer(writer)
183            .with_env_filter(filter);
184        (builder, guard)
185    }
186    /// Create a new [`SubscriberBuilder`], and a [`Guard`] that handles e.g flushing [`NonBlocking`] IO.
187    ///
188    /// Returns [`Err`] if e.g opening a log file fails.
189    /// If you wish the subscriber to handle them (typically by logging), see [`Self::builder`].
190    pub fn try_builder(self) -> Result<(SubscriberBuilder, Guard), writer::Error> {
191        let (writer, fields, event, filter, guard, span_events) = self.into_components(false)?;
192        let builder = tracing_subscriber::fmt()
193            .with_span_events(span_events.unwrap_or(FmtSpan::NONE))
194            .fmt_fields(fields)
195            .event_format(event)
196            .with_writer(writer)
197            .with_env_filter(filter);
198        Ok((builder, guard))
199    }
200}
201
202/// Config for formatters.
203#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default)]
204#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
205#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
206pub struct Format {
207    /// See [`tracing_subscriber::fmt::SubscriberBuilder::with_ansi`].
208    #[cfg_attr(feature = "serde1", serde(skip_serializing_if = "Option::is_none"))]
209    pub ansi: Option<bool>,
210    /// See [`tracing_subscriber::fmt::SubscriberBuilder::with_target`].
211    #[cfg_attr(feature = "serde1", serde(skip_serializing_if = "Option::is_none"))]
212    pub target: Option<bool>,
213    /// See [`tracing_subscriber::fmt::SubscriberBuilder::with_level`].
214    #[cfg_attr(feature = "serde1", serde(skip_serializing_if = "Option::is_none"))]
215    pub level: Option<bool>,
216    /// See [`tracing_subscriber::fmt::SubscriberBuilder::with_thread_ids`].
217    #[cfg_attr(feature = "serde1", serde(skip_serializing_if = "Option::is_none"))]
218    pub thread_ids: Option<bool>,
219    /// See [`tracing_subscriber::fmt::SubscriberBuilder::with_thread_names`].
220    #[cfg_attr(feature = "serde1", serde(skip_serializing_if = "Option::is_none"))]
221    pub thread_names: Option<bool>,
222    /// See [`tracing_subscriber::fmt::SubscriberBuilder::with_file`].
223    #[cfg_attr(feature = "serde1", serde(skip_serializing_if = "Option::is_none"))]
224    pub file: Option<bool>,
225    /// See [`tracing_subscriber::fmt::SubscriberBuilder::with_line_number`].
226    #[cfg_attr(feature = "serde1", serde(skip_serializing_if = "Option::is_none"))]
227    pub line_number: Option<bool>,
228    /// Specific output formats.
229    #[cfg_attr(feature = "serde1", serde(skip_serializing_if = "Option::is_none"))]
230    pub formatter: Option<Formatter>,
231    /// What timing information to include.
232    #[cfg_attr(feature = "serde1", serde(skip_serializing_if = "Option::is_none"))]
233    pub timer: Option<Timer>,
234    /// What span events to emit.
235    #[cfg_attr(
236        feature = "serde1",
237        serde(
238            default,
239            skip_serializing_if = "Option::is_none",
240            with = "As::<Option<VecFmtSpan>>"
241        )
242    )]
243    #[cfg_attr(feature = "schemars1", schemars(with = "Option<Vec<FmtSpanItem>>"))]
244    pub span_events: Option<FmtSpan>,
245}
246
247#[cfg(feature = "serde1")]
248struct VecFmtSpan;
249#[cfg(feature = "serde1")]
250impl<'de> DeserializeAs<'de, FmtSpan> for VecFmtSpan {
251    fn deserialize_as<D: serde::Deserializer<'de>>(d: D) -> Result<FmtSpan, D::Error> {
252        Ok(Vec::<FmtSpanItem>::deserialize(d)?
253            .into_iter()
254            .fold(FmtSpan::NONE, |acc, el| {
255                acc & match el {
256                    FmtSpanItem::New => FmtSpan::NEW,
257                    FmtSpanItem::Enter => FmtSpan::ENTER,
258                    FmtSpanItem::Exit => FmtSpan::EXIT,
259                    FmtSpanItem::Close => FmtSpan::CLOSE,
260                    FmtSpanItem::None => FmtSpan::NONE,
261                    FmtSpanItem::Active => FmtSpan::ACTIVE,
262                    FmtSpanItem::Full => FmtSpan::FULL,
263                }
264            }))
265    }
266}
267#[cfg(feature = "serde1")]
268impl SerializeAs<FmtSpan> for VecFmtSpan {
269    fn serialize_as<S: serde::Serializer>(source: &FmtSpan, s: S) -> Result<S::Ok, S::Error> {
270        match source.clone() {
271            FmtSpan::NONE => [FmtSpanItem::None].serialize(s),
272            FmtSpan::ACTIVE => [FmtSpanItem::Active].serialize(s),
273            FmtSpan::FULL => [FmtSpanItem::Full].serialize(s),
274            _ => {
275                let mut v = vec![];
276                for (theirs, ours) in [
277                    (FmtSpan::NEW, FmtSpanItem::New),
278                    (FmtSpan::ENTER, FmtSpanItem::Enter),
279                    (FmtSpan::EXIT, FmtSpanItem::Exit),
280                    (FmtSpan::CLOSE, FmtSpanItem::Close),
281                ] {
282                    if source.clone() & theirs.clone() == theirs {
283                        v.push(ours)
284                    }
285                }
286                v.serialize(s)
287            }
288        }
289    }
290}
291
292#[cfg(feature = "serde1")]
293#[derive(Serialize, Deserialize)]
294#[serde(rename_all = "lowercase")]
295#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
296enum FmtSpanItem {
297    New,
298    Enter,
299    Exit,
300    Close,
301    None,
302    Active,
303    Full,
304}
305
306/// The specific output format.
307#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord, Default)]
308#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
309#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
310#[cfg_attr(feature = "serde1", serde(rename_all = "lowercase"))]
311pub enum Formatter {
312    /// See [`tracing_subscriber::fmt::format::Full`].
313    #[default]
314    Full,
315    /// See [`tracing_subscriber::fmt::format::Compact`].
316    Compact,
317    /// See [`tracing_subscriber::fmt::format::Pretty`].
318    Pretty,
319    /// See [`tracing_subscriber::fmt::format::Json`].
320    Json(Option<Json>),
321}
322
323impl FromStr for Formatter {
324    type Err = ParseError;
325    fn from_str(s: &str) -> Result<Self, Self::Err> {
326        Ok(match s {
327            "full" => Self::Full,
328            "compact" => Self::Compact,
329            "pretty" => Self::Pretty,
330            "json" => Self::Json(None),
331            _ => {
332                return Err(ParseError(
333                    "Expected one of `full`, `compact`, `pretty`, or `json`",
334                ))
335            }
336        })
337    }
338}
339
340#[cfg(feature = "clap4")]
341impl ValueEnum for Formatter {
342    fn value_variants<'a>() -> &'a [Self] {
343        &[Self::Full, Self::Compact, Self::Pretty, Self::Json(None)]
344    }
345
346    fn to_possible_value(&self) -> Option<PossibleValue> {
347        Some(match self {
348            Formatter::Full => PossibleValue::new("full"),
349            Formatter::Compact => PossibleValue::new("compact"),
350            Formatter::Pretty => PossibleValue::new("pretty"),
351            Formatter::Json(_) => PossibleValue::new("json"),
352        })
353    }
354}
355
356#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord, Default)]
357#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
358#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
359pub struct Json {
360    /// See [`tracing_subscriber::fmt::format::Json::flatten_event`].
361    #[cfg_attr(feature = "serde1", serde(skip_serializing_if = "Option::is_none"))]
362    pub flatten_event: Option<bool>,
363    /// See [`tracing_subscriber::fmt::format::Json::with_current_span`].
364    #[cfg_attr(feature = "serde1", serde(skip_serializing_if = "Option::is_none"))]
365    pub current_span: Option<bool>,
366    /// See [`tracing_subscriber::fmt::format::Json::with_span_list`].
367    #[cfg_attr(feature = "serde1", serde(skip_serializing_if = "Option::is_none"))]
368    pub span_list: Option<bool>,
369}
370
371/// Which timer implementation to use.
372#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord, Default)]
373#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
374#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
375#[cfg_attr(feature = "serde1", serde(rename_all = "lowercase"))]
376pub enum Timer {
377    /// See [`tracing_subscriber::fmt::SubscriberBuilder::without_time`].
378    None,
379    /// See [`tracing_subscriber::fmt::time::ChronoLocal`].
380    Local(
381        #[cfg_attr(feature = "serde1", serde(skip_serializing_if = "Option::is_none"))]
382        Option<String>,
383    ),
384    /// See [`tracing_subscriber::fmt::time::ChronoUtc`].
385    Utc(
386        #[cfg_attr(feature = "serde1", serde(skip_serializing_if = "Option::is_none"))]
387        Option<String>,
388    ),
389    /// See [`tracing_subscriber::fmt::time::SystemTime`].
390    #[default]
391    System,
392    /// See [`tracing_subscriber::fmt::time::Uptime`].
393    Uptime,
394}
395
396impl Timer {
397    const PARSE_ERROR: &str = "Expected one of `none`, `local`, `local=<format>`, `utc`, `utc=<format>`, `system`, or `uptime`";
398}
399
400impl FromStr for Timer {
401    type Err = ParseError;
402    fn from_str(s: &str) -> Result<Self, Self::Err> {
403        alt::<_, _, winnow::error::EmptyError, _>((
404            "none".map(|_| Self::None),
405            preceded("local=", rest).map(|it| Self::Local(Some(String::from(it)))),
406            "local".map(|_| Self::Local(None)),
407            preceded("utc=", rest).map(|it| Self::Utc(Some(String::from(it)))),
408            "utc".map(|_| Self::Utc(None)),
409            "system".map(|_| Self::System),
410            "uptime".map(|_| Self::Uptime),
411        ))
412        .parse(s)
413        .map_err(|_| ParseError(Self::PARSE_ERROR))
414    }
415}
416
417#[cfg(feature = "clap4")]
418impl ValueEnum for Timer {
419    fn value_variants<'a>() -> &'a [Self] {
420        const {
421            &[
422                Timer::None,
423                Timer::Local(None),
424                Timer::Local(Some(String::new())),
425                Timer::Utc(None),
426                Timer::Utc(Some(String::new())),
427                Timer::System,
428                Timer::Uptime,
429            ]
430        }
431    }
432    fn to_possible_value(&self) -> Option<PossibleValue> {
433        Some(match self {
434            Timer::None => PossibleValue::new("none"),
435            Timer::Local(None) => PossibleValue::new("local"),
436            Timer::Local(Some(_)) => PossibleValue::new("local=<format>"),
437            Timer::Utc(None) => PossibleValue::new("utc"),
438            Timer::Utc(Some(_)) => PossibleValue::new("utc=<format>"),
439            Timer::System => PossibleValue::new("system"),
440            Timer::Uptime => PossibleValue::new("uptime"),
441        })
442    }
443    fn from_str(input: &str, _ignore_case: bool) -> Result<Self, String> {
444        input.parse().map_err(|ParseError(it)| String::from(it))
445    }
446}
447
448/// Write to a [`File`](std::fs::File).
449#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord, Default)]
450#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
451#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
452#[cfg_attr(feature = "serde1", serde(rename_all = "lowercase"))]
453pub struct File {
454    pub path: PathBuf,
455    pub mode: FileOpenMode,
456    /// Wrap the writer in a [`tracing_appender::non_blocking::NonBlocking`].
457    #[cfg_attr(feature = "serde1", serde(skip_serializing_if = "Option::is_none"))]
458    pub non_blocking: Option<NonBlocking>,
459}
460
461#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord, Default)]
462#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
463#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
464#[cfg_attr(feature = "serde1", serde(rename_all = "lowercase"))]
465/// Use a [`tracing_appender::rolling::RollingFileAppender`].
466pub struct Rolling {
467    pub directory: PathBuf,
468    pub roll: Option<Roll>,
469    /// Wrap the writer in a [`tracing_appender::non_blocking::NonBlocking`].
470    #[cfg_attr(feature = "serde1", serde(skip_serializing_if = "Option::is_none"))]
471    pub non_blocking: Option<NonBlocking>,
472}
473
474/// Which writer to use.
475#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord, Default)]
476#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
477#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
478#[cfg_attr(feature = "serde1", serde(rename_all = "lowercase"))]
479pub enum Writer {
480    /// No writer.
481    Null,
482    /// Use [`io::stdout`](std::io::stdout).
483    #[default]
484    Stdout,
485    /// Use [`io::stderr`](std::io::stderr).
486    Stderr,
487    File(File),
488    Rolling(Rolling),
489}
490
491impl Writer {
492    const PARSE_ERROR: &str =
493        "Expected one of `null`, `stdout`, `stderr`, `file=<file>`, or `rolling=<directory>`";
494}
495
496impl FromStr for Writer {
497    type Err = ParseError;
498
499    fn from_str(s: &str) -> Result<Self, Self::Err> {
500        alt::<_, _, winnow::error::EmptyError, _>((
501            alt(("null", "none")).map(|_| Self::Null),
502            "stdout".map(|_| Self::Stdout),
503            "stderr".map(|_| Self::Stderr),
504            preceded("file=", rest)
505                .verify(|it| !str::is_empty(it))
506                .map(|it| {
507                    Self::File(File {
508                        path: PathBuf::from(it),
509                        ..Default::default()
510                    })
511                }),
512            preceded("rolling=", rest).map(|it| {
513                Self::Rolling(Rolling {
514                    directory: PathBuf::from(it),
515                    ..Default::default()
516                })
517            }),
518        ))
519        .parse(s)
520        .map_err(|_| ParseError(Self::PARSE_ERROR))
521    }
522}
523
524#[cfg(feature = "clap4")]
525// can't `const { PathBuf::new() }` so this is what we need
526impl ValueParserFactory for Writer {
527    type Parser = ValueParser;
528    fn value_parser() -> Self::Parser {
529        #[derive(Clone)]
530        struct _TypedValueParser;
531        impl TypedValueParser for _TypedValueParser {
532            type Value = Writer;
533            fn parse_ref(
534                &self,
535                cmd: &clap::Command,
536                _arg: Option<&clap::Arg>,
537                value: &std::ffi::OsStr,
538            ) -> Result<Self::Value, clap::Error> {
539                value
540                    .to_str()
541                    .ok_or(clap::Error::new(clap::error::ErrorKind::InvalidUtf8))?
542                    .parse()
543                    .map_err(|_| {
544                        clap::Error::new(clap::error::ErrorKind::InvalidValue).with_cmd(cmd)
545                    })
546            }
547            fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
548                Some(Box::new(
549                    [
550                        PossibleValue::new("null"),
551                        PossibleValue::new("stdout"),
552                        PossibleValue::new("stderr"),
553                        PossibleValue::new("file=<file>"),
554                        PossibleValue::new("rolling=<directory>"),
555                    ]
556                    .into_iter(),
557                ))
558            }
559        }
560        ValueParser::new(_TypedValueParser)
561    }
562}
563
564strum_lite::strum! {
565/// How often to rotate the [`tracing_appender::rolling::RollingFileAppender`].
566///
567/// See [`tracing_appender::rolling::Rotation`].
568#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord, Default)]
569#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
570#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
571#[cfg_attr(feature = "serde1", serde(rename_all = "lowercase"))]
572#[cfg_attr(feature = "clap4", derive(ValueEnum))]
573pub enum Rotation {
574    Minutely = "minutely",
575    Hourly = "hourly",
576    Daily = "daily",
577    #[default]
578    Never = "never",
579}}
580
581/// Config for [`tracing_appender::rolling::RollingFileAppender`].
582#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord, Default)]
583#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
584#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
585pub struct Roll {
586    /// See [`tracing_appender::rolling::Builder::max_log_files`].
587    #[cfg_attr(feature = "serde1", serde(skip_serializing_if = "Option::is_none"))]
588    pub limit: Option<usize>,
589    /// See [`tracing_appender::rolling::Builder::filename_prefix`].
590    #[cfg_attr(feature = "serde1", serde(skip_serializing_if = "Option::is_none"))]
591    pub prefix: Option<String>,
592    /// See [`tracing_appender::rolling::Builder::filename_suffix`].
593    #[cfg_attr(feature = "serde1", serde(skip_serializing_if = "Option::is_none"))]
594    pub suffix: Option<String>,
595    /// See [`tracing_appender::rolling::Builder::rotation`].
596    #[cfg_attr(feature = "serde1", serde(skip_serializing_if = "Option::is_none"))]
597    pub rotation: Option<Rotation>,
598}
599
600strum_lite::strum! {
601/// How the [`tracing_appender::non_blocking::NonBlocking`] should behave on a full queue.
602///
603/// See [`tracing_appender::non_blocking::NonBlockingBuilder::lossy`].
604#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord)]
605#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
606#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
607#[cfg_attr(feature = "serde1", serde(rename_all = "lowercase"))]
608#[cfg_attr(feature = "clap4", derive(ValueEnum))]
609pub enum BackpressureBehaviour {
610    Drop = "drop",
611    Block = "block",
612}}
613
614strum_lite::strum! {
615/// How to treat a newly created log file in [`Writer::File`].
616#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord, Default)]
617#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
618#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
619#[cfg_attr(feature = "serde1", serde(rename_all = "lowercase"))]
620#[cfg_attr(feature = "clap4", derive(ValueEnum))]
621pub enum FileOpenMode {
622    #[default]
623    Truncate = "truncate",
624    Append = "append",
625}}
626
627/// Configuration for [`tracing_appender::non_blocking::NonBlocking`].
628#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord, Default)]
629#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
630#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
631#[cfg_attr(feature = "serde1", serde(rename_all = "lowercase"))]
632pub struct NonBlocking {
633    /// See [`tracing_appender::non_blocking::NonBlockingBuilder::buffered_lines_limit`].
634    #[cfg_attr(feature = "serde1", serde(skip_serializing_if = "Option::is_none"))]
635    pub buffer_length: Option<usize>,
636    #[cfg_attr(feature = "serde1", serde(skip_serializing_if = "Option::is_none"))]
637    pub behaviour: Option<BackpressureBehaviour>,
638}
639
640#[cfg(all(test, feature = "schemars1"))]
641#[test]
642fn schema() {
643    let s = serde_json::to_string_pretty(&schemars::schema_for!(Subscriber)).unwrap();
644    expect_test::expect_file!["../snapshots/schema.json"].assert_eq(&s);
645}