#![cfg_attr(docsrs, feature(doc_cfg))]
#![deny(missing_docs)]
use std::borrow::Cow;
use std::str::FromStr;
use logforth_core::Diagnostic;
use logforth_core::Error;
use logforth_core::Filter;
use logforth_core::filter::FilterResult;
use logforth_core::record::FilterCriteria;
use logforth_core::record::Level;
use logforth_core::record::LevelFilter;
#[cfg(test)]
mod tests;
pub const DEFAULT_FILTER_ENV: &str = "RUST_LOG";
#[derive(Debug)]
pub struct RustLogFilter {
directives: Vec<Directive>,
}
impl RustLogFilter {
fn from_directives(directives: Vec<Directive>) -> Self {
let mut directives = directives;
directives.sort();
RustLogFilter { directives }
}
}
impl Filter for RustLogFilter {
fn enabled(&self, criteria: &FilterCriteria, _: &[Box<dyn Diagnostic>]) -> FilterResult {
let level = criteria.level();
let target = criteria.target();
for directive in self.directives.iter().rev() {
let name = directive.name.as_deref();
if name.is_none_or(|n| target.starts_with(n)) {
return if directive.level.test(level) {
FilterResult::Neutral
} else {
FilterResult::Reject
};
}
}
FilterResult::Reject
}
}
impl From<LevelFilter> for RustLogFilter {
fn from(filter: LevelFilter) -> Self {
RustLogFilterBuilder::default().filter_level(filter).build()
}
}
impl<'a> From<&'a str> for RustLogFilter {
fn from(filter: &'a str) -> Self {
RustLogFilterBuilder::from_spec(filter).build()
}
}
impl FromStr for RustLogFilter {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
RustLogFilterBuilder::try_from_spec(s).map(|b| b.build())
}
}
#[derive(Debug, Default)]
pub struct RustLogFilterBuilder {
directives: Vec<Directive>,
}
impl RustLogFilterBuilder {
pub fn from_default_env() -> Self {
RustLogFilterBuilder::from_env(DEFAULT_FILTER_ENV)
}
pub fn from_default_env_or<'a, V>(default: V) -> Self
where
V: Into<Cow<'a, str>>,
{
RustLogFilterBuilder::from_env_or(DEFAULT_FILTER_ENV, default)
}
pub fn from_env<'a, V>(name: V) -> RustLogFilterBuilder
where
V: Into<Cow<'a, str>>,
{
let name = name.into();
if let Ok(s) = std::env::var(&*name) {
Self::from_spec(s)
} else {
Self::default()
}
}
pub fn from_env_or<'a, 'b, E, V>(name: E, default: V) -> Self
where
E: Into<Cow<'a, str>>,
V: Into<Cow<'b, str>>,
{
let name = name.into();
if let Ok(s) = std::env::var(&*name) {
Self::from_spec(s)
} else {
let default = default.into();
Self::from_spec(default)
}
}
pub fn from_spec<'a, V>(spec: V) -> Self
where
V: Into<Cow<'a, str>>,
{
let spec = spec.into();
let ParseResult { directives, errors } = parse_spec(&spec);
for error in errors {
eprintln!("warning: {error}, ignoring it");
}
let mut builder = RustLogFilterBuilder::default();
for directive in directives {
builder.upsert_directive(directive);
}
builder
}
pub fn try_from_spec<'a, V>(spec: V) -> Result<Self, Error>
where
V: Into<Cow<'a, str>>,
{
let spec = spec.into();
let ParseResult { directives, errors } = parse_spec(&spec);
if let Some(error) = errors.into_iter().next() {
return Err(Error::new(error));
}
let mut builder = RustLogFilterBuilder::default();
for directive in directives {
builder.upsert_directive(directive);
}
Ok(builder)
}
pub fn build(self) -> RustLogFilter {
let Self { directives } = self;
if directives.is_empty() {
RustLogFilter::from_directives(vec![Directive {
name: None,
level: LevelFilter::MoreSevereEqual(Level::Error),
}])
} else {
RustLogFilter::from_directives(directives)
}
}
pub fn filter_module(mut self, module: impl Into<String>, level: LevelFilter) -> Self {
let name = Some(module.into());
self.upsert_directive(Directive { name, level });
self
}
pub fn filter_level(mut self, level: LevelFilter) -> Self {
self.upsert_directive(Directive { name: None, level });
self
}
fn upsert_directive(&mut self, mut directive: Directive) {
if let Some(pos) = self
.directives
.iter()
.position(|d| d.name == directive.name)
{
std::mem::swap(&mut self.directives[pos], &mut directive);
} else {
self.directives.push(directive);
}
}
}
#[derive(Debug, Eq, PartialEq)]
struct Directive {
name: Option<String>,
level: LevelFilter,
}
impl PartialOrd for Directive {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Directive {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
let this_len = self.name.as_ref().map(|n| n.len()).unwrap_or(0);
let other_len = other.name.as_ref().map(|n| n.len()).unwrap_or(0);
Ord::cmp(&this_len, &other_len)
}
}
#[derive(Debug)]
struct ParseResult {
directives: Vec<Directive>,
errors: Vec<String>,
}
fn parse_spec(spec: &str) -> ParseResult {
let mut directives = vec![];
let mut errors = vec![];
for s in spec.split(',').map(str::trim) {
if s.is_empty() {
continue;
}
let mut parts = s.split('=');
let part0 = parts.next().map(str::trim);
let part1 = parts.next().map(str::trim);
let Some(part0) = part0 else {
errors.push(format!("malformed logging spec '{s}'"));
continue;
};
if parts.next().is_some() {
errors.push(format!("malformed logging spec '{s}'"));
continue;
}
let (level, name) = match part1 {
None => {
if let Some(level) = from_str_for_env(part0) {
(level, None)
} else {
(LevelFilter::All, Some(part0.to_owned()))
}
}
Some(part1) => {
if part1.is_empty() {
(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}'"));
continue;
}
}
};
directives.push(Directive { name, level });
}
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
}
}