diff --git a/Cargo.lock b/Cargo.lock index 010b6c4..f28975a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -868,7 +868,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "highlights" -version = "2.1.1" +version = "2.1.2" dependencies = [ "anyhow", "chrono", @@ -2404,6 +2404,16 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.15" @@ -2414,12 +2424,15 @@ dependencies = [ "matchers", "once_cell", "regex", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a7f8dd9..7b2c63a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "highlights" -version = "2.1.1" +version = "2.1.2" authors = ["ThatsNoMoon "] repository = "https://github.com/ThatsNoMoon/highlights" license = "OSL-3.0" @@ -43,7 +43,7 @@ serde_json = { version = "1.0", optional = true } tinyvec = { version = "1.5", features = ["alloc"] } tracing = "0.1" tracing-opentelemetry = { version = "0.17", optional = true } -tracing-subscriber = "0.3" +tracing-subscriber = { version = "0.3", features = ["json"] } [dependencies.config] version = "0.13" diff --git a/example_config.toml b/example_config.toml index 862d2ea..718d94a 100644 --- a/example_config.toml +++ b/example_config.toml @@ -26,6 +26,8 @@ sample_ratio = 1.0 level = "WARN" # Whether or not to use ANSI color codes (may not work on Windows) color = true +# Format of standard output logging (can be "compact", "pretty", or "json") +format = "compact" [logging.filters] # The `highlights` crate will log at INFO instead highlights = "INFO" diff --git a/src/logging/mod.rs b/src/logging/mod.rs index 9558897..91888c3 100644 --- a/src/logging/mod.rs +++ b/src/logging/mod.rs @@ -6,14 +6,15 @@ use anyhow::Result; #[cfg(any(feature = "monitoring", feature = "reporting"))] use tracing::warn; -use tracing::Metadata; +use tracing::{Metadata, Subscriber}; use tracing_subscriber::{ filter::FilterFn, - layer::{Layer, SubscriberExt}, + layer::{Layer, Layered, SubscriberExt}, + registry::LookupSpan, util::SubscriberInitExt, }; -use crate::settings::{settings, Settings}; +use crate::settings::{settings, LogFormat, Settings}; #[cfg(feature = "monitoring")] mod monitoring; @@ -49,38 +50,61 @@ fn use_filters(settings: &Settings, metadata: &Metadata) -> bool { /// This initializes [`reporting`] and [`monitoring`], if /// enabled, as well as basic stdout logging. pub(crate) fn init() -> Result<()> { - let subscriber = tracing_subscriber::registry().with( - tracing_subscriber::fmt::layer() - .with_ansi(settings().logging.color) - .with_filter({ - let settings = settings(); - FilterFn::new(|metadata| use_filters(settings, metadata)) - }), - ); + let fmt = + tracing_subscriber::fmt::layer().with_ansi(settings().logging.color); - #[cfg(feature = "monitoring")] - let (is_monitoring, subscriber) = { - let layer = monitoring::init()?; - (layer.is_some(), subscriber.with(layer)) + let filter = { + let settings = settings(); + FilterFn::new(|metadata| use_filters(settings, metadata)) }; - #[cfg(feature = "reporting")] - let (is_reporting, subscriber) = { - let layer = reporting::init(); - (layer.is_some(), subscriber.with(layer)) - }; + fn init_rest(subscriber: Layered) -> Result<()> + where + L: Layer + Send + Sync + 'static, + S: Subscriber + for<'span> LookupSpan<'span> + Send + Sync + 'static, + { + #[cfg(feature = "monitoring")] + let (is_monitoring, subscriber) = { + let layer = monitoring::init()?; + (layer.is_some(), subscriber.with(layer)) + }; - subscriber.try_init()?; + #[cfg(feature = "reporting")] + let (is_reporting, subscriber) = { + let layer = reporting::init(); + (layer.is_some(), subscriber.with(layer)) + }; - #[cfg(feature = "monitoring")] - if !is_monitoring { - warn!("Jaeger agent address not provided; not reporting traces"); - } + subscriber.try_init()?; + + #[cfg(feature = "monitoring")] + if !is_monitoring { + warn!("Jaeger agent address not provided; not reporting traces"); + } - #[cfg(feature = "reporting")] - if !is_reporting { - warn!("Webhook URL is not present, not reporting panics"); + #[cfg(feature = "reporting")] + if !is_reporting { + warn!("Webhook URL is not present, not reporting panics"); + } + + Ok(()) } - Ok(()) + match &settings().logging.format { + LogFormat::Compact => { + let subscriber = tracing_subscriber::registry() + .with(fmt.compact().with_filter(filter)); + init_rest(subscriber) + } + LogFormat::Pretty => { + let subscriber = tracing_subscriber::registry() + .with(fmt.pretty().with_filter(filter)); + init_rest(subscriber) + } + LogFormat::Json => { + let subscriber = tracing_subscriber::registry() + .with(fmt.json().with_filter(filter)); + init_rest(subscriber) + } + } } diff --git a/src/settings.rs b/src/settings.rs index 98ea03b..5623d0c 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -196,6 +196,51 @@ mod level { } use level::{deserialize_level_filter, deserialize_level_filters}; + +mod log_format { + use std::fmt; + + use serde::{de, Deserialize, Deserializer}; + + #[derive(Debug)] + pub(crate) enum LogFormat { + Compact, + Pretty, + Json, + } + + /// Visitor to deserialize a `LevelFilter` from a string. + struct LogFormatVisitor; + impl<'de> de::Visitor<'de> for LogFormatVisitor { + type Value = LogFormat; + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "a log format (compact, pretty, json)") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + match v { + "compact" | "COMPACT" => Ok(LogFormat::Compact), + "pretty" | "PRETTY" => Ok(LogFormat::Pretty), + "json" | "JSON" => Ok(LogFormat::Json), + _ => Err(E::invalid_value(de::Unexpected::Str(v), &self)), + } + } + } + + impl<'de> Deserialize<'de> for LogFormat { + fn deserialize(d: D) -> Result + where + D: Deserializer<'de>, + { + d.deserialize_str(LogFormatVisitor) + } + } +} + +pub(crate) use log_format::LogFormat; #[cfg(feature = "monitoring")] pub(crate) use user_address::UserAddress; @@ -259,6 +304,8 @@ pub(crate) struct LoggingSettings { /// Whether or not to use ANSI color codes. pub(crate) color: bool, + /// Standard output logging format. + pub(crate) format: LogFormat, } /// Settings for the database.