use std::cmp;
use std::str::FromStr;
use tracing::{Level, Metadata};
use tracing_subscriber::layer::Context;
#[derive(Debug, Clone, derive_builder::Builder)]
#[non_exhaustive]
#[builder(default, derive(Debug), setter(into))]
pub struct LoggingOptions {
pub verbose: bool,
pub otlp_endpoint: Option<String>,
pub app_name: String,
pub levels: LogFilters,
}
impl LoggingOptions {
#[must_use]
pub fn with_level(level: LogLevel) -> Self {
Self {
levels: LogFilters::with_level(level),
..Default::default()
}
}
}
impl Default for LoggingOptions {
fn default() -> Self {
Self {
verbose: Default::default(),
otlp_endpoint: Default::default(),
app_name: "app".to_owned(),
levels: Default::default(),
}
}
}
#[derive(Debug, Default, Clone, derive_builder::Builder)]
#[builder(default)]
#[non_exhaustive]
pub struct LogFilters {
pub telemetry: FilterOptions,
pub stderr: FilterOptions,
}
impl LogFilters {
#[must_use]
pub fn with_level(level: LogLevel) -> Self {
Self {
telemetry: FilterOptions {
level,
..Default::default()
},
stderr: FilterOptions {
level,
..Default::default()
},
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct FilterOptions {
pub level: LogLevel,
pub filter: Vec<TargetLevel>,
}
impl Default for FilterOptions {
fn default() -> Self {
Self {
level: LogLevel::Info,
filter: vec![],
}
}
}
impl FilterOptions {
#[must_use]
pub fn new(level: LogLevel, filter: Vec<TargetLevel>) -> Self {
Self { level, filter }
}
fn test_enabled(&self, module: &str, level: Level) -> bool {
let matches = self.filter.iter().filter(|config| module.starts_with(&config.target));
let match_hit = matches.fold(None, |acc, next| {
let enabled = next.modifier.compare(filter_as_usize(level), next.level as usize);
let next_len = next.target.len();
acc.map_or(Some((next_len, enabled)), |(last_len, last_enabled)| {
match next_len.cmp(&last_len) {
cmp::Ordering::Greater => {
Some((next_len, enabled))
}
cmp::Ordering::Equal => {
Some((last_len, enabled && last_enabled))
}
cmp::Ordering::Less => {
Some((last_len, last_enabled))
}
}
})
});
match_hit.map_or(self.level >= level, |(_, enabled)| enabled)
}
}
impl<S> tracing_subscriber::layer::Filter<S> for FilterOptions
where
S: tracing::Subscriber + for<'lookup> tracing_subscriber::registry::LookupSpan<'lookup>,
{
fn enabled(&self, metadata: &Metadata<'_>, _cx: &Context<'_, S>) -> bool {
let enabled = metadata.target().starts_with("wick")
|| metadata.target().starts_with("flow")
|| metadata.target().starts_with("wasmrs");
metadata.is_span() || enabled
}
fn event_enabled(&self, event: &tracing::Event<'_>, _cx: &Context<'_, S>) -> bool {
let module = event.metadata().target().split("::").next().unwrap_or_default();
let level = event.metadata().level();
self.test_enabled(module, *level)
}
}
#[derive(Debug, Default, PartialEq, Clone)]
#[non_exhaustive]
pub struct TargetLevel {
pub target: String,
pub level: LogLevel,
pub modifier: LogModifier,
}
impl TargetLevel {
pub fn new<T: Into<String>>(target: T, level: LogLevel, modifier: LogModifier) -> Self {
Self {
target: target.into(),
level,
modifier,
}
}
#[must_use]
pub fn not<T: Into<String>>(target: T, level: LogLevel) -> Self {
Self::new(target, level, LogModifier::Not)
}
#[must_use]
pub fn gt<T: Into<String>>(target: T, level: LogLevel) -> Self {
Self::new(target, level, LogModifier::GreaterThan)
}
#[must_use]
pub fn gte<T: Into<String>>(target: T, level: LogLevel) -> Self {
Self::new(target, level, LogModifier::GreaterThanOrEqualTo)
}
#[must_use]
pub fn lt<T: Into<String>>(target: T, level: LogLevel) -> Self {
Self::new(target, level, LogModifier::LessThan)
}
#[must_use]
pub fn lte<T: Into<String>>(target: T, level: LogLevel) -> Self {
Self::new(target, level, LogModifier::LessThanOrEqualTo)
}
#[must_use]
pub fn is<T: Into<String>>(target: T, level: LogLevel) -> Self {
Self::new(target, level, LogModifier::Equal)
}
}
impl LoggingOptions {
pub fn name<T: Into<String>>(&self, name: T) -> Self {
Self {
app_name: name.into(),
..self.clone()
}
}
}
#[derive(Debug, Clone, PartialEq, Copy)]
#[non_exhaustive]
pub enum LogModifier {
Not,
GreaterThan,
GreaterThanOrEqualTo,
LessThan,
LessThanOrEqualTo,
Equal,
}
impl Default for LogModifier {
fn default() -> Self {
Self::LessThanOrEqualTo
}
}
impl std::fmt::Display for LogModifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LogModifier::Not => write!(f, "!="),
LogModifier::GreaterThan => write!(f, ">"),
LogModifier::GreaterThanOrEqualTo => write!(f, ">="),
LogModifier::LessThan => write!(f, "<"),
LogModifier::LessThanOrEqualTo => write!(f, "<="),
LogModifier::Equal => write!(f, "="),
}
}
}
impl LogModifier {
const fn compare(self, a: usize, b: usize) -> bool {
match self {
LogModifier::Not => a != b,
LogModifier::GreaterThan => a > b,
LogModifier::GreaterThanOrEqualTo => a >= b,
LogModifier::LessThan => a < b,
LogModifier::LessThanOrEqualTo => a <= b,
LogModifier::Equal => a == b,
}
}
}
impl FromStr for LogModifier {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"!=" => Ok(LogModifier::Not),
">" => Ok(LogModifier::GreaterThan),
">=" => Ok(LogModifier::GreaterThanOrEqualTo),
"<" => Ok(LogModifier::LessThan),
"<=" => Ok(LogModifier::LessThanOrEqualTo),
"=" | "==" => Ok(LogModifier::Equal),
_ => Err(()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[non_exhaustive]
#[repr(usize)]
pub enum LogLevel {
Quiet = 0,
Error = 1,
Warn = 2,
Info = 3,
Debug = 4,
Trace = 5,
}
impl Default for LogLevel {
fn default() -> Self {
Self::Info
}
}
impl FromStr for LogLevel {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"quiet" => Ok(LogLevel::Quiet),
"error" => Ok(LogLevel::Error),
"warn" => Ok(LogLevel::Warn),
"info" => Ok(LogLevel::Info),
"debug" => Ok(LogLevel::Debug),
"trace" => Ok(LogLevel::Trace),
_ => Err(()),
}
}
}
impl std::fmt::Display for LogLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LogLevel::Quiet => write!(f, "QUIET"),
LogLevel::Error => write!(f, "ERROR"),
LogLevel::Warn => write!(f, "WARN"),
LogLevel::Info => write!(f, "INFO"),
LogLevel::Debug => write!(f, "DEBUG"),
LogLevel::Trace => write!(f, "TRACE"),
}
}
}
impl PartialEq<Level> for LogLevel {
#[inline(always)]
fn eq(&self, other: &Level) -> bool {
match self {
LogLevel::Quiet => false,
LogLevel::Error => other.eq(&Level::ERROR),
LogLevel::Warn => other.eq(&Level::WARN),
LogLevel::Info => other.eq(&Level::INFO),
LogLevel::Debug => other.eq(&Level::DEBUG),
LogLevel::Trace => other.eq(&Level::TRACE),
}
}
}
impl PartialOrd<Level> for LogLevel {
#[inline(always)]
fn partial_cmp(&self, other: &Level) -> Option<cmp::Ordering> {
Some((*self as usize).cmp(&filter_as_usize(*other)))
}
#[inline(always)]
fn lt(&self, other: &Level) -> bool {
(*self as usize) < filter_as_usize(*other)
}
#[inline(always)]
fn le(&self, other: &Level) -> bool {
(*self as usize) <= filter_as_usize(*other)
}
#[inline(always)]
fn gt(&self, other: &Level) -> bool {
(*self as usize) > filter_as_usize(*other)
}
#[inline(always)]
fn ge(&self, other: &Level) -> bool {
(*self as usize) >= filter_as_usize(*other)
}
}
#[inline(always)]
const fn filter_as_usize(x: Level) -> usize {
(match x {
Level::ERROR => 0,
Level::WARN => 1,
Level::INFO => 2,
Level::DEBUG => 3,
Level::TRACE => 4,
} + 1)
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_modifier_compare() {
assert!(LogModifier::Equal.compare(2, 2));
assert!(LogModifier::GreaterThan.compare(4, 2));
assert!(LogModifier::GreaterThanOrEqualTo.compare(4, 2));
assert!(LogModifier::GreaterThanOrEqualTo.compare(2, 2));
assert!(LogModifier::Not.compare(4, 3));
assert!(LogModifier::LessThan.compare(1, 2));
assert!(LogModifier::LessThanOrEqualTo.compare(1, 2));
assert!(LogModifier::LessThanOrEqualTo.compare(2, 2));
}
#[test]
fn test_modifier_compare_level() {
assert!(LogModifier::Equal.compare(filter_as_usize(Level::TRACE), LogLevel::Trace as usize));
assert!(LogModifier::GreaterThan.compare(filter_as_usize(Level::TRACE), LogLevel::Warn as usize));
assert!(LogModifier::GreaterThanOrEqualTo.compare(filter_as_usize(Level::INFO), LogLevel::Info as usize));
assert!(LogModifier::GreaterThanOrEqualTo.compare(filter_as_usize(Level::TRACE), LogLevel::Debug as usize));
assert!(LogModifier::Not.compare(filter_as_usize(Level::ERROR), LogLevel::Trace as usize));
assert!(LogModifier::LessThan.compare(filter_as_usize(Level::INFO), LogLevel::Debug as usize));
assert!(LogModifier::LessThanOrEqualTo.compare(filter_as_usize(Level::TRACE), LogLevel::Trace as usize));
assert!(LogModifier::LessThanOrEqualTo.compare(filter_as_usize(Level::INFO), LogLevel::Trace as usize));
}
#[allow(clippy::needless_pass_by_value)]
fn opts<const K: usize>(default: LogLevel, targets: [TargetLevel; K]) -> FilterOptions {
FilterOptions {
level: default,
filter: targets.to_vec(),
}
}
#[test]
fn test_default_level() {
assert!(opts(LogLevel::Info, []).test_enabled("wick", Level::INFO));
assert!(!opts(LogLevel::Info, []).test_enabled("wick", Level::TRACE));
}
#[rstest::rstest]
#[case(LogLevel::Info, [TargetLevel::lte("wick",LogLevel::Trace)], "wick", Level::TRACE, true)]
#[case(LogLevel::Info, [TargetLevel::lte("wick",LogLevel::Info),TargetLevel::lte("wick_packet",LogLevel::Trace)], "wick_packet", Level::TRACE, true)]
#[case(LogLevel::Info, [
TargetLevel::lte("a",LogLevel::Info),
TargetLevel::not("ab",LogLevel::Trace),
TargetLevel::lte("abc",LogLevel::Trace)
], "abcdef", Level::TRACE, true)]
#[case(LogLevel::Info, [
TargetLevel::lte("a",LogLevel::Info),
TargetLevel::lte("abc",LogLevel::Trace),
TargetLevel::not("ab",LogLevel::Trace),
], "abcdef", Level::TRACE, true)]
fn test_specificity<const K: usize>(
#[case] default: LogLevel,
#[case] filter: [TargetLevel; K],
#[case] span_target: &str,
#[case] span_level: Level,
#[case] expect_enabled: bool,
) {
assert_eq!(
opts(default, filter).test_enabled(span_target, span_level),
expect_enabled
);
}
#[rstest::rstest]
#[case(LogLevel::Info, LogLevel::Trace, Level::TRACE, false)]
#[case(LogLevel::Info, LogLevel::Trace, Level::DEBUG, true)]
#[case(LogLevel::Info, LogLevel::Trace, Level::INFO, true)]
#[case(LogLevel::Info, LogLevel::Trace, Level::WARN, true)]
#[case(LogLevel::Info, LogLevel::Trace, Level::ERROR, true)]
fn test_not(
#[case] default: LogLevel,
#[case] target_level: LogLevel,
#[case] span_level: Level,
#[case] expect_enabled: bool,
) {
assert_eq!(
opts(default, [TargetLevel::not("wick", target_level)]).test_enabled("wick", span_level),
expect_enabled
);
}
#[rstest::rstest]
#[case(LogLevel::Info, LogLevel::Trace, Level::TRACE, false)]
#[case(LogLevel::Info, LogLevel::Trace, Level::DEBUG, true)]
#[case(LogLevel::Info, LogLevel::Trace, Level::INFO, true)]
#[case(LogLevel::Info, LogLevel::Trace, Level::WARN, true)]
#[case(LogLevel::Info, LogLevel::Trace, Level::ERROR, true)]
fn test_lt(
#[case] default: LogLevel,
#[case] target_level: LogLevel,
#[case] span_level: Level,
#[case] expect_enabled: bool,
) {
assert_eq!(
opts(default, [TargetLevel::lt("wick", target_level)]).test_enabled("wick", span_level),
expect_enabled
);
}
#[rstest::rstest]
#[case(LogLevel::Info, LogLevel::Trace, Level::TRACE, true)]
#[case(LogLevel::Info, LogLevel::Trace, Level::DEBUG, true)]
#[case(LogLevel::Info, LogLevel::Trace, Level::INFO, true)]
#[case(LogLevel::Info, LogLevel::Trace, Level::WARN, true)]
#[case(LogLevel::Info, LogLevel::Trace, Level::ERROR, true)]
fn test_lte(
#[case] default: LogLevel,
#[case] target_level: LogLevel,
#[case] span_level: Level,
#[case] expect_enabled: bool,
) {
assert_eq!(
opts(default, [TargetLevel::lte("wick", target_level)]).test_enabled("wick", span_level),
expect_enabled
);
}
#[rstest::rstest]
#[case(LogLevel::Info, LogLevel::Info, Level::TRACE, true)]
#[case(LogLevel::Info, LogLevel::Info, Level::DEBUG, true)]
#[case(LogLevel::Info, LogLevel::Info, Level::INFO, true)]
#[case(LogLevel::Info, LogLevel::Info, Level::WARN, false)]
#[case(LogLevel::Info, LogLevel::Info, Level::ERROR, false)]
fn test_gte(
#[case] default: LogLevel,
#[case] target_level: LogLevel,
#[case] span_level: Level,
#[case] expect_enabled: bool,
) {
assert_eq!(
opts(default, [TargetLevel::gte("wick", target_level)]).test_enabled("wick", span_level),
expect_enabled
);
}
#[rstest::rstest]
#[case(LogLevel::Info, LogLevel::Info, Level::TRACE, true)]
#[case(LogLevel::Info, LogLevel::Info, Level::DEBUG, true)]
#[case(LogLevel::Info, LogLevel::Info, Level::INFO, false)]
#[case(LogLevel::Info, LogLevel::Info, Level::WARN, false)]
#[case(LogLevel::Info, LogLevel::Info, Level::ERROR, false)]
fn test_gt(
#[case] default: LogLevel,
#[case] target_level: LogLevel,
#[case] span_level: Level,
#[case] expect_enabled: bool,
) {
assert_eq!(
opts(default, [TargetLevel::gt("wick", target_level)]).test_enabled("wick", span_level),
expect_enabled
);
}
#[rstest::rstest]
#[case(LogLevel::Info, LogLevel::Info, Level::TRACE, false)]
#[case(LogLevel::Info, LogLevel::Info, Level::DEBUG, false)]
#[case(LogLevel::Info, LogLevel::Info, Level::INFO, true)]
#[case(LogLevel::Info, LogLevel::Info, Level::WARN, false)]
#[case(LogLevel::Info, LogLevel::Info, Level::ERROR, false)]
fn test_eq(
#[case] default: LogLevel,
#[case] target_level: LogLevel,
#[case] span_level: Level,
#[case] expect_enabled: bool,
) {
assert_eq!(
opts(default, [TargetLevel::is("wick", target_level)]).test_enabled("wick", span_level),
expect_enabled
);
}
}