Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file.
### Breaking changes

* Rename `DefaultTrap` to `BestEffortTrap` for better clarity.
* Add `Level::Critical` variant to represent critical level logs.
* Redesign `LevelFilter`.

## [0.28.1] 2025-10-06

Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,16 @@ Configure multiple dispatches with different filters and appenders:

```rust
use logforth::append;
use logforth::record::Level;
use logforth::record::LevelFilter;

fn main() {
logforth::starter_log::builder()
.dispatch(|d| d
.filter(LevelFilter::Error)
.filter(LevelFilter::MoreSevereEqual(Level::Error))
.append(append::Stderr::default()))
.dispatch(|d| d
.filter(LevelFilter::Info)
.filter(LevelFilter::MoreSevereEqual(Level::Info))
.append(append::Stdout::default()))
.apply();

Expand Down
2 changes: 1 addition & 1 deletion appenders/file/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
//! .unwrap();
//!
//! logforth_core::builder()
//! .dispatch(|d| d.filter(LevelFilter::Trace).append(rolling))
//! .dispatch(|d| d.filter(LevelFilter::All).append(rolling))
//! .apply();
//!
//! log::info!("This log will be written to a rolling file.");
Expand Down
1 change: 1 addition & 0 deletions appenders/journald/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ impl Append for Journald {
// write them directly, everything else goes through the put functions
// for property mangling and length-encoding
let priority = match record.level() {
Level::Critical => b"2",
Level::Error => b"3",
Level::Warn => b"4",
Level::Info => b"5",
Expand Down
1 change: 1 addition & 0 deletions appenders/opentelemetry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ impl Drop for OpentelemetryLog {

fn log_level_to_otel_severity(level: Level) -> opentelemetry::logs::Severity {
match level {
Level::Critical => opentelemetry::logs::Severity::Fatal,
Level::Error => opentelemetry::logs::Severity::Error,
Level::Warn => opentelemetry::logs::Severity::Warn,
Level::Info => opentelemetry::logs::Severity::Info,
Expand Down
3 changes: 2 additions & 1 deletion appenders/syslog/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
//! let append = SyslogBuilder::tcp_well_known().unwrap().build();
//!
//! logforth_core::builder()
//! .dispatch(|d| d.filter(LevelFilter::Trace).append(append))
//! .dispatch(|d| d.filter(LevelFilter::All).append(append))
//! .apply();
//!
//! log::info!("This log will be written to syslog.");
Expand Down Expand Up @@ -324,6 +324,7 @@ struct SyslogFormatter {

fn log_level_to_syslog_severity(level: Level) -> fasyslog::Severity {
match level {
Level::Critical => fasyslog::Severity::CRITICAL,
Level::Error => fasyslog::Severity::ERROR,
Level::Warn => fasyslog::Severity::WARNING,
Level::Info => fasyslog::Severity::NOTICE,
Expand Down
30 changes: 21 additions & 9 deletions core/src/filter/env_filter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ use crate::Diagnostic;
use crate::Error;
use crate::Filter;
use crate::filter::FilterResult;
use crate::record::Level;
use crate::record::LevelFilter;
use crate::record::Metadata;

Expand All @@ -96,7 +97,6 @@ pub const DEFAULT_FILTER_ENV: &str = "RUST_LOG";
///
/// Read more from the [module level documentation](self) about the directive syntax and use cases.
///
/// [`Level`]: crate::record::Level
/// [`Record`]: crate::record::Record
#[derive(Debug)]
pub struct EnvFilter {
Expand All @@ -121,10 +121,10 @@ impl Filter for EnvFilter {
let name = directive.name.as_deref();
if name.is_none_or(|n| target.starts_with(n)) {
// longest match wins; return immediately
return if directive.level < level {
FilterResult::Reject
} else {
return if directive.level.test(level) {
FilterResult::Neutral
} else {
FilterResult::Reject
};
}
}
Expand Down Expand Up @@ -325,7 +325,7 @@ impl EnvFilterBuilder {
if directives.is_empty() {
EnvFilter::from_directives(vec![Directive {
name: None,
level: LevelFilter::Error,
level: LevelFilter::MoreSevereEqual(Level::Error),
}])
} else {
EnvFilter::from_directives(directives)
Expand Down Expand Up @@ -422,17 +422,17 @@ fn parse_spec(spec: &str) -> ParseResult {

let (level, name) = match part1 {
None => {
if let Ok(level) = part0.parse() {
if let Some(level) = from_str_for_env(part0) {
// if the single argument is a log level string, treat that as a global fallback
(level, None)
} else {
(LevelFilter::Trace, Some(part0.to_owned()))
(LevelFilter::All, Some(part0.to_owned()))
}
}
Some(part1) => {
if part1.is_empty() {
(LevelFilter::Trace, Some(part0.to_owned()))
} else if let Ok(level) = part1.parse() {
(LevelFilter::All, Some(part0.to_owned()))
} else if let Some(level) = from_str_for_env(part1) {
(level, Some(part0.to_owned()))
} else {
errors.push(format!("malformed logging spec '{part1}'"));
Expand All @@ -446,3 +446,15 @@ fn parse_spec(spec: &str) -> ParseResult {

ParseResult { directives, errors }
}

fn from_str_for_env(text: &str) -> Option<LevelFilter> {
if let Ok(level) = Level::from_str(text) {
Some(LevelFilter::MoreSevereEqual(level))
} else if text.eq_ignore_ascii_case("off") {
Some(LevelFilter::Off)
} else if text.eq_ignore_ascii_case("all") {
Some(LevelFilter::All)
} else {
None
}
}
69 changes: 37 additions & 32 deletions core/src/filter/env_filter/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ fn parse_spec_valid() {

assert_eq!(dirs.len(), 3);
assert_eq!(dirs[0].name, Some("crate1::mod1".to_owned()));
assert_eq!(dirs[0].level, LevelFilter::Error);
assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Error));

assert_eq!(dirs[1].name, Some("crate1::mod2".to_owned()));
assert_eq!(dirs[1].level, LevelFilter::Trace);
assert_eq!(dirs[1].level, LevelFilter::All);

assert_eq!(dirs[2].name, Some("crate2".to_owned()));
assert_eq!(dirs[2].level, LevelFilter::Debug);
assert_eq!(dirs[2].level, LevelFilter::MoreSevereEqual(Level::Debug));

assert!(errors.is_empty());
}
Expand All @@ -65,7 +65,7 @@ fn parse_spec_invalid_crate() {

assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0].name, Some("crate2".to_owned()));
assert_eq!(dirs[0].level, LevelFilter::Debug);
assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Debug));

assert_eq!(errors.len(), 1);
assert_snapshot!(
Expand All @@ -84,7 +84,7 @@ fn parse_spec_invalid_level() {

assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0].name, Some("crate2".to_owned()));
assert_eq!(dirs[0].level, LevelFilter::Debug);
assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Debug));

assert_eq!(errors.len(), 1);
assert_snapshot!(&errors[0], @"malformed logging spec 'noNumber'");
Expand All @@ -100,7 +100,7 @@ fn parse_spec_string_level() {

assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0].name, Some("crate2".to_owned()));
assert_eq!(dirs[0].level, LevelFilter::Warn);
assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Warn));

assert_eq!(errors.len(), 1);
assert_snapshot!(&errors[0], @"malformed logging spec 'wrong'");
Expand All @@ -116,7 +116,7 @@ fn parse_spec_empty_level() {

assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0].name, Some("crate2".to_owned()));
assert_eq!(dirs[0].level, LevelFilter::Trace);
assert_eq!(dirs[0].level, LevelFilter::All);

assert_eq!(errors.len(), 1);
assert_snapshot!(&errors[0], @"malformed logging spec 'wrong'");
Expand Down Expand Up @@ -197,9 +197,9 @@ fn parse_spec_global() {
} = parse_spec("warn,crate2=debug");
assert_eq!(dirs.len(), 2);
assert_eq!(dirs[0].name, None);
assert_eq!(dirs[0].level, LevelFilter::Warn);
assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Warn));
assert_eq!(dirs[1].name, Some("crate2".to_owned()));
assert_eq!(dirs[1].level, LevelFilter::Debug);
assert_eq!(dirs[1].level, LevelFilter::MoreSevereEqual(Level::Debug));

assert!(errors.is_empty());
}
Expand All @@ -213,7 +213,7 @@ fn parse_spec_global_bare_warn_lc() {
} = parse_spec("warn");
assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0].name, None);
assert_eq!(dirs[0].level, LevelFilter::Warn);
assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Warn));

assert!(errors.is_empty());
}
Expand All @@ -227,7 +227,7 @@ fn parse_spec_global_bare_warn_uc() {
} = parse_spec("WARN");
assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0].name, None);
assert_eq!(dirs[0].level, LevelFilter::Warn);
assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Warn));

assert!(errors.is_empty());
}
Expand All @@ -241,7 +241,7 @@ fn parse_spec_global_bare_warn_mixed() {
} = parse_spec("wArN");
assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0].name, None);
assert_eq!(dirs[0].level, LevelFilter::Warn);
assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Warn));

assert!(errors.is_empty());
}
Expand All @@ -256,7 +256,7 @@ fn parse_spec_multiple_invalid_crates() {

assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0].name, Some("crate2".to_owned()));
assert_eq!(dirs[0].level, LevelFilter::Debug);
assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Debug));

assert_eq!(errors.len(), 2);
assert_snapshot!(
Expand All @@ -279,7 +279,7 @@ fn parse_spec_multiple_invalid_levels() {

assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0].name, Some("crate2".to_owned()));
assert_eq!(dirs[0].level, LevelFilter::Debug);
assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Debug));

assert_eq!(errors.len(), 2);
assert_snapshot!(&errors[0], @"malformed logging spec 'noNumber'");
Expand All @@ -296,7 +296,7 @@ fn parse_spec_invalid_crate_and_level() {

assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0].name, Some("crate2".to_owned()));
assert_eq!(dirs[0].level, LevelFilter::Debug);
assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Debug));

assert_eq!(errors.len(), 2);
assert_snapshot!(
Expand Down Expand Up @@ -328,7 +328,7 @@ fn parse_error_message_multiple_errors() {
#[test]
fn filter_info() {
let logger = EnvFilterBuilder::default()
.filter_level(LevelFilter::Info)
.filter_level(LevelFilter::MoreSevereEqual(Level::Info))
.build();
assert!(!logger.rejected(Level::Info, "crate1"));
assert!(logger.rejected(Level::Debug, "crate1"));
Expand All @@ -337,9 +337,9 @@ fn filter_info() {
#[test]
fn filter_beginning_longest_match() {
let logger = EnvFilterBuilder::default()
.filter_module("crate2", LevelFilter::Info)
.filter_module("crate2::mod", LevelFilter::Debug)
.filter_module("crate1::mod1", LevelFilter::Warn)
.filter_module("crate2", LevelFilter::MoreSevereEqual(Level::Info))
.filter_module("crate2::mod", LevelFilter::MoreSevereEqual(Level::Debug))
.filter_module("crate1::mod1", LevelFilter::MoreSevereEqual(Level::Warn))
.build();
assert!(!logger.rejected(Level::Debug, "crate2::mod1"));
assert!(logger.rejected(Level::Debug, "crate2"));
Expand All @@ -361,7 +361,12 @@ fn filter_beginning_longest_match() {
fn ensure_tests_cover_level_universe() {
let level_universe: Level = Level::Trace; // use of trace variant is arbitrary
match level_universe {
Level::Error | Level::Warn | Level::Info | Level::Debug | Level::Trace => (),
Level::Critical
| Level::Error
| Level::Warn
| Level::Info
| Level::Debug
| Level::Trace => (),
}
}

Expand Down Expand Up @@ -552,11 +557,11 @@ fn match_full_path() {
let logger = EnvFilter::from_directives(vec![
Directive {
name: Some("crate2".to_owned()),
level: LevelFilter::Info,
level: LevelFilter::MoreSevereEqual(Level::Info),
},
Directive {
name: Some("crate1::mod1".to_owned()),
level: LevelFilter::Warn,
level: LevelFilter::MoreSevereEqual(Level::Warn),
},
]);
assert!(!logger.rejected(Level::Warn, "crate1::mod1"));
Expand All @@ -570,11 +575,11 @@ fn no_match() {
let logger = EnvFilter::from_directives(vec![
Directive {
name: Some("crate2".to_owned()),
level: LevelFilter::Info,
level: LevelFilter::MoreSevereEqual(Level::Info),
},
Directive {
name: Some("crate1::mod1".to_owned()),
level: LevelFilter::Warn,
level: LevelFilter::MoreSevereEqual(Level::Warn),
},
]);
assert!(logger.rejected(Level::Warn, "crate3"));
Expand All @@ -585,11 +590,11 @@ fn match_beginning() {
let logger = EnvFilter::from_directives(vec![
Directive {
name: Some("crate2".to_owned()),
level: LevelFilter::Info,
level: LevelFilter::MoreSevereEqual(Level::Info),
},
Directive {
name: Some("crate1::mod1".to_owned()),
level: LevelFilter::Warn,
level: LevelFilter::MoreSevereEqual(Level::Warn),
},
]);
assert!(!logger.rejected(Level::Info, "crate2::mod1"));
Expand All @@ -600,15 +605,15 @@ fn match_beginning_longest_match() {
let logger = EnvFilter::from_directives(vec![
Directive {
name: Some("crate2".to_owned()),
level: LevelFilter::Info,
level: LevelFilter::MoreSevereEqual(Level::Info),
},
Directive {
name: Some("crate2::mod".to_owned()),
level: LevelFilter::Debug,
level: LevelFilter::MoreSevereEqual(Level::Debug),
},
Directive {
name: Some("crate1::mod1".to_owned()),
level: LevelFilter::Warn,
level: LevelFilter::MoreSevereEqual(Level::Warn),
},
]);
assert!(!logger.rejected(Level::Debug, "crate2::mod1"));
Expand All @@ -620,11 +625,11 @@ fn match_default() {
let logger = EnvFilter::from_directives(vec![
Directive {
name: None,
level: LevelFilter::Info,
level: LevelFilter::MoreSevereEqual(Level::Info),
},
Directive {
name: Some("crate1::mod1".to_owned()),
level: LevelFilter::Warn,
level: LevelFilter::MoreSevereEqual(Level::Warn),
},
]);
assert!(!logger.rejected(Level::Warn, "crate1::mod1"));
Expand All @@ -636,7 +641,7 @@ fn zero_level() {
let logger = EnvFilter::from_directives(vec![
Directive {
name: None,
level: LevelFilter::Info,
level: LevelFilter::MoreSevereEqual(Level::Info),
},
Directive {
name: Some("crate1::mod1".to_owned()),
Expand Down
2 changes: 1 addition & 1 deletion core/src/filter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ pub trait Filter: fmt::Debug + Send + Sync + 'static {

impl Filter for LevelFilter {
fn enabled(&self, metadata: &Metadata, _: &[Box<dyn Diagnostic>]) -> FilterResult {
if metadata.level() <= *self {
if self.test(metadata.level()) {
FilterResult::Neutral
} else {
FilterResult::Reject
Expand Down
Loading