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