1pub 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#[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
97pub 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
105pub 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 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 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 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 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#[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 #[serde(skip_serializing_if = "Option::is_none")]
221 pub ansi: Option<bool>,
222 #[serde(skip_serializing_if = "Option::is_none")]
224 pub target: Option<bool>,
225 #[serde(skip_serializing_if = "Option::is_none")]
227 pub level: Option<bool>,
228 #[serde(skip_serializing_if = "Option::is_none")]
230 pub thread_ids: Option<bool>,
231 #[serde(skip_serializing_if = "Option::is_none")]
233 pub thread_names: Option<bool>,
234 #[serde(skip_serializing_if = "Option::is_none")]
236 pub file: Option<bool>,
237 #[serde(skip_serializing_if = "Option::is_none")]
239 pub line_number: Option<bool>,
240 #[serde(skip_serializing_if = "Option::is_none")]
242 pub formatter: Option<Formatter>,
243 #[serde(skip_serializing_if = "Option::is_none")]
245 pub timer: Option<Timer>,
246 #[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#[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 #[default]
320 Full,
321 Compact,
323 Pretty,
325 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 #[serde(skip_serializing_if = "Option::is_none")]
368 pub flatten_event: Option<bool>,
369 #[serde(skip_serializing_if = "Option::is_none")]
371 pub current_span: Option<bool>,
372 #[serde(skip_serializing_if = "Option::is_none")]
374 pub span_list: Option<bool>,
375}
376
377#[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 None,
384 Local(#[serde(skip_serializing_if = "Option::is_none")] Option<String>),
386 Utc(#[serde(skip_serializing_if = "Option::is_none")] Option<String>),
388 #[default]
390 System,
391 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#[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 #[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")]
462pub struct Rolling {
464 pub directory: PathBuf,
465 pub roll: Option<Roll>,
466 #[serde(skip_serializing_if = "Option::is_none")]
468 pub non_blocking: Option<NonBlocking>,
469}
470
471#[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 Null,
478 #[default]
480 Stdout,
481 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")]
521impl 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#[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#[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 #[serde(skip_serializing_if = "Option::is_none")]
584 pub limit: Option<usize>,
585 #[serde(skip_serializing_if = "Option::is_none")]
587 pub prefix: Option<String>,
588 #[serde(skip_serializing_if = "Option::is_none")]
590 pub suffix: Option<String>,
591 #[serde(skip_serializing_if = "Option::is_none")]
593 pub rotation: Option<Rotation>,
594}
595
596#[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#[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#[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 #[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}