pub mod format;
pub mod otel;
pub mod time;
pub mod writer;
#[cfg(feature = "clap4")]
use clap::{
builder::PossibleValue,
builder::{TypedValueParser, ValueParser, ValueParserFactory},
ValueEnum,
};
#[cfg(feature = "schemars1")]
use schemars::JsonSchema;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_with::*;
use std::{fmt, path::PathBuf, str::FromStr};
use tracing_subscriber::{filter::Filtered, fmt::format::FmtSpan, EnvFilter, Layer as _};
use winnow::{
combinator::{alt, preceded},
token::rest,
Parser as _,
};
use writer::Guard;
fn _serde_from_str<T: DeserializeOwned>(s: &str) -> Result<T, serde::de::value::Error> {
T::deserialize(serde::de::value::StrDeserializer::new(s))
}
macro_rules! serde_from_str {
($ty:ty) => {
impl FromStr for $ty {
type Err = serde::de::value::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
$crate::_serde_from_str(s)
}
}
};
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Subscriber {
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<Format>,
#[serde(skip_serializing_if = "Option::is_none")]
pub writer: Option<Writer>,
#[serde(skip_serializing_if = "Option::is_none")]
pub filter: Option<Filter>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Filter {
#[serde(skip_serializing_if = "Option::is_none")]
pub regex: Option<bool>,
#[serde(
default,
skip_serializing_if = "Vec::is_empty",
with = "As::<Vec<DisplayFromStr>>"
)]
#[cfg_attr(feature = "schemars1", schemars(with = "Vec<String>"))]
pub directives: Vec<tracing_subscriber::filter::Directive>,
}
impl From<Filter> for EnvFilter {
fn from(value: Filter) -> Self {
let Filter { regex, directives } = value;
let mut builder = EnvFilter::builder();
if let Some(regex) = regex {
builder = builder.with_regex(regex)
}
directives
.into_iter()
.fold(builder.parse_lossy(""), EnvFilter::add_directive)
}
}
#[derive(Debug)]
pub struct ParseError(&'static str);
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.0)
}
}
impl std::error::Error for ParseError {}
pub type SubscriberBuilder<
N = format::FormatFields,
E = format::FormatEvent,
F = EnvFilter,
W = writer::MakeWriter,
> = tracing_subscriber::fmt::SubscriberBuilder<N, E, F, W>;
pub type Layer<S, N = format::FormatFields, E = format::FormatEvent, W = writer::MakeWriter> =
Filtered<tracing_subscriber::fmt::Layer<S, N, E, W>, EnvFilter, S>;
impl Subscriber {
#[expect(clippy::type_complexity)]
fn into_components(
self,
defer: bool,
) -> Result<
(
writer::MakeWriter,
format::FormatFields,
format::FormatEvent,
EnvFilter,
Guard,
Option<FmtSpan>,
),
writer::Error,
> {
let Self {
format,
writer,
filter,
} = self;
let mut format = format.unwrap_or_default();
let writer = writer.unwrap_or_default();
let (writer, guard) = match defer {
true => writer::MakeWriter::try_new(writer)?,
false => writer::MakeWriter::new(writer),
};
let fields = format::FormatFields::from(format.formatter.clone().unwrap_or_default());
let span_events = format.span_events.take();
let event = format::FormatEvent::from(format);
let filter = EnvFilter::from(filter.unwrap_or_default());
Ok((writer, fields, event, filter, guard, span_events))
}
pub fn layer<S>(self) -> (Layer<S>, Guard)
where
S: tracing_core::Subscriber + for<'s> tracing_subscriber::registry::LookupSpan<'s>,
{
let (writer, fields, event, filter, guard, span_events) = self
.into_components(true)
.expect("errors have been deferred");
let layer = tracing_subscriber::fmt::layer()
.with_span_events(span_events.unwrap_or(FmtSpan::NONE))
.fmt_fields(fields)
.event_format(event)
.with_writer(writer)
.with_filter(filter);
(layer, guard)
}
pub fn try_layer<S>(self) -> Result<(Layer<S>, Guard), writer::Error>
where
S: tracing_core::Subscriber + for<'s> tracing_subscriber::registry::LookupSpan<'s>,
{
let (writer, fields, event, filter, guard, span_events) = self.into_components(false)?;
let layer = tracing_subscriber::fmt::layer()
.with_span_events(span_events.unwrap_or(FmtSpan::NONE))
.fmt_fields(fields)
.event_format(event)
.with_writer(writer)
.with_filter(filter);
Ok((layer, guard))
}
pub fn builder(self) -> (SubscriberBuilder, Guard) {
let (writer, fields, event, filter, guard, span_events) = self
.into_components(true)
.expect("errors have been deferred");
let builder = tracing_subscriber::fmt()
.with_span_events(span_events.unwrap_or(FmtSpan::NONE))
.fmt_fields(fields)
.event_format(event)
.with_writer(writer)
.with_env_filter(filter);
(builder, guard)
}
pub fn try_builder(self) -> Result<(SubscriberBuilder, Guard), writer::Error> {
let (writer, fields, event, filter, guard, span_events) = self.into_components(false)?;
let builder = tracing_subscriber::fmt()
.with_span_events(span_events.unwrap_or(FmtSpan::NONE))
.fmt_fields(fields)
.event_format(event)
.with_writer(writer)
.with_env_filter(filter);
Ok((builder, guard))
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Format {
#[serde(skip_serializing_if = "Option::is_none")]
pub ansi: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub level: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thread_ids: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thread_names: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub line_number: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub formatter: Option<Formatter>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timer: Option<Timer>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "As::<Option<VecFmtSpan>>"
)]
#[cfg_attr(feature = "schemars1", schemars(with = "Option<Vec<FmtSpanItem>>"))]
pub span_events: Option<FmtSpan>,
}
struct VecFmtSpan;
impl<'de> DeserializeAs<'de, FmtSpan> for VecFmtSpan {
fn deserialize_as<D: serde::Deserializer<'de>>(d: D) -> Result<FmtSpan, D::Error> {
Ok(Vec::<FmtSpanItem>::deserialize(d)?
.into_iter()
.fold(FmtSpan::NONE, |acc, el| {
acc & match el {
FmtSpanItem::New => FmtSpan::NEW,
FmtSpanItem::Enter => FmtSpan::ENTER,
FmtSpanItem::Exit => FmtSpan::EXIT,
FmtSpanItem::Close => FmtSpan::CLOSE,
FmtSpanItem::None => FmtSpan::NONE,
FmtSpanItem::Active => FmtSpan::ACTIVE,
FmtSpanItem::Full => FmtSpan::FULL,
}
}))
}
}
impl SerializeAs<FmtSpan> for VecFmtSpan {
fn serialize_as<S: serde::Serializer>(source: &FmtSpan, s: S) -> Result<S::Ok, S::Error> {
match source.clone() {
FmtSpan::NONE => [FmtSpanItem::None].serialize(s),
FmtSpan::ACTIVE => [FmtSpanItem::Active].serialize(s),
FmtSpan::FULL => [FmtSpanItem::Full].serialize(s),
_ => {
let mut v = vec![];
for (theirs, ours) in [
(FmtSpan::NEW, FmtSpanItem::New),
(FmtSpan::ENTER, FmtSpanItem::Enter),
(FmtSpan::EXIT, FmtSpanItem::Exit),
(FmtSpan::CLOSE, FmtSpanItem::Close),
] {
if source.clone() & theirs.clone() == theirs {
v.push(ours)
}
}
v.serialize(s)
}
}
}
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
enum FmtSpanItem {
New,
Enter,
Exit,
Close,
None,
Active,
Full,
}
#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub enum Formatter {
#[default]
Full,
Compact,
Pretty,
Json(Option<Json>),
}
impl FromStr for Formatter {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"full" => Self::Full,
"compact" => Self::Compact,
"pretty" => Self::Pretty,
"json" => Self::Json(None),
_ => {
return Err(ParseError(
"Expected one of `full`, `compact`, `pretty`, or `json`",
))
}
})
}
}
#[cfg(feature = "clap4")]
impl ValueEnum for Formatter {
fn value_variants<'a>() -> &'a [Self] {
&[Self::Full, Self::Compact, Self::Pretty, Self::Json(None)]
}
fn to_possible_value(&self) -> Option<PossibleValue> {
Some(match self {
Formatter::Full => PossibleValue::new("full"),
Formatter::Compact => PossibleValue::new("compact"),
Formatter::Pretty => PossibleValue::new("pretty"),
Formatter::Json(_) => PossibleValue::new("json"),
})
}
}
#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Json {
#[serde(skip_serializing_if = "Option::is_none")]
pub flatten_event: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub current_span: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub span_list: Option<bool>,
}
#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub enum Timer {
None,
Local(#[serde(skip_serializing_if = "Option::is_none")] Option<String>),
Utc(#[serde(skip_serializing_if = "Option::is_none")] Option<String>),
#[default]
System,
Uptime,
}
impl Timer {
const PARSE_ERROR: &str = "Expected one of `none`, `local`, `local=<format>`, `utc`, `utc=<format>`, `system`, or `uptime`";
}
impl FromStr for Timer {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
alt::<_, _, winnow::error::EmptyError, _>((
"none".map(|_| Self::None),
preceded("local=", rest).map(|it| Self::Local(Some(String::from(it)))),
"local".map(|_| Self::Local(None)),
preceded("utc=", rest).map(|it| Self::Utc(Some(String::from(it)))),
"utc".map(|_| Self::Utc(None)),
"system".map(|_| Self::System),
"uptime".map(|_| Self::Uptime),
))
.parse(s)
.map_err(|_| ParseError(Self::PARSE_ERROR))
}
}
#[cfg(feature = "clap4")]
impl ValueEnum for Timer {
fn value_variants<'a>() -> &'a [Self] {
const {
&[
Timer::None,
Timer::Local(None),
Timer::Local(Some(String::new())),
Timer::Utc(None),
Timer::Utc(Some(String::new())),
Timer::System,
Timer::Uptime,
]
}
}
fn to_possible_value(&self) -> Option<PossibleValue> {
Some(match self {
Timer::None => PossibleValue::new("none"),
Timer::Local(None) => PossibleValue::new("local"),
Timer::Local(Some(_)) => PossibleValue::new("local=<format>"),
Timer::Utc(None) => PossibleValue::new("utc"),
Timer::Utc(Some(_)) => PossibleValue::new("utc=<format>"),
Timer::System => PossibleValue::new("system"),
Timer::Uptime => PossibleValue::new("uptime"),
})
}
fn from_str(input: &str, _ignore_case: bool) -> Result<Self, String> {
input.parse().map_err(|ParseError(it)| String::from(it))
}
}
#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct File {
pub path: PathBuf,
pub mode: FileOpenMode,
#[serde(skip_serializing_if = "Option::is_none")]
pub non_blocking: Option<NonBlocking>,
}
#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Rolling {
pub directory: PathBuf,
pub roll: Option<Roll>,
#[serde(skip_serializing_if = "Option::is_none")]
pub non_blocking: Option<NonBlocking>,
}
#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub enum Writer {
Null,
#[default]
Stdout,
Stderr,
File(File),
Rolling(Rolling),
}
impl Writer {
const PARSE_ERROR: &str =
"Expected one of `null`, `stdout`, `stderr`, `file=<file>`, or `rolling=<directory>`";
}
impl FromStr for Writer {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
alt::<_, _, winnow::error::EmptyError, _>((
alt(("null", "none")).map(|_| Self::Null),
"stdout".map(|_| Self::Stdout),
"stderr".map(|_| Self::Stderr),
preceded("file=", rest)
.verify(|it| !str::is_empty(it))
.map(|it| {
Self::File(File {
path: PathBuf::from(it),
..Default::default()
})
}),
preceded("rolling=", rest).map(|it| {
Self::Rolling(Rolling {
directory: PathBuf::from(it),
..Default::default()
})
}),
))
.parse(s)
.map_err(|_| ParseError(Self::PARSE_ERROR))
}
}
#[cfg(feature = "clap4")]
impl ValueParserFactory for Writer {
type Parser = ValueParser;
fn value_parser() -> Self::Parser {
#[derive(Clone)]
struct _TypedValueParser;
impl TypedValueParser for _TypedValueParser {
type Value = Writer;
fn parse_ref(
&self,
cmd: &clap::Command,
_arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
value
.to_str()
.ok_or(clap::Error::new(clap::error::ErrorKind::InvalidUtf8))?
.parse()
.map_err(|_| {
clap::Error::new(clap::error::ErrorKind::InvalidValue).with_cmd(cmd)
})
}
fn possible_values(&self) -> Option<Box<dyn Iterator<Item = PossibleValue> + '_>> {
Some(Box::new(
[
PossibleValue::new("null"),
PossibleValue::new("stdout"),
PossibleValue::new("stderr"),
PossibleValue::new("file=<file>"),
PossibleValue::new("rolling=<directory>"),
]
.into_iter(),
))
}
}
ValueParser::new(_TypedValueParser)
}
}
#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "clap4", derive(ValueEnum))]
pub enum Rotation {
Minutely,
Hourly,
Daily,
#[default]
Never,
}
serde_from_str!(Rotation);
#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Roll {
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prefix: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub suffix: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rotation: Option<Rotation>,
}
#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "clap4", derive(ValueEnum))]
pub enum BackpressureBehaviour {
Drop,
Block,
}
serde_from_str!(BackpressureBehaviour);
#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "clap4", derive(ValueEnum))]
pub enum FileOpenMode {
#[default]
Truncate,
Append,
}
serde_from_str!(FileOpenMode);
#[derive(Debug, Hash, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars1", derive(JsonSchema))]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct NonBlocking {
#[serde(skip_serializing_if = "Option::is_none")]
pub buffer_length: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub behaviour: Option<BackpressureBehaviour>,
}
#[cfg(all(test, feature = "schemars1"))]
#[test]
fn schema() {
let s = serde_json::to_string_pretty(&schemars::schema_for!(Subscriber)).unwrap();
expect_test::expect_file!["../snapshots/schema.json"].assert_eq(&s);
}