use flexi_logger::{
filter::{LogLineFilter, LogLineWriter},
DeferredNow,
};
use log::Record;
use std::{borrow::ToOwned, cmp::Ordering, num::NonZeroUsize, sync::Mutex};
fn main() {
#[cfg(colors)]
let format = flexi_logger::colored_detailed_format;
#[cfg(not(colors))]
let format = flexi_logger::detailed_format;
flexi_logger::Logger::try_with_str("info")
.unwrap()
.format(format)
.log_to_stdout()
.filter(Box::new(DedupWriter::with_leeway(
std::num::NonZeroUsize::new(2).unwrap(),
)))
.start()
.unwrap();
for i in 0..10 {
log::info!("{}", if i == 5 { "bar" } else { "foo" });
}
log::info!("the end");
}
pub struct DedupWriter {
deduper: Mutex<Deduper>,
}
impl DedupWriter {
pub fn with_leeway(leeway: NonZeroUsize) -> Self {
Self {
deduper: Mutex::new(Deduper::with_leeway(leeway)),
}
}
}
impl LogLineFilter for DedupWriter {
fn write(
&self,
now: &mut DeferredNow,
record: &Record,
log_line_writer: &dyn LogLineWriter,
) -> std::io::Result<()> {
let mut deduper = self.deduper.lock().unwrap();
match deduper.dedup(record) {
DedupAction::Allow => {
log_line_writer.write(now, record)
}
DedupAction::AllowLastOfLeeway(_) => {
log_line_writer.write(now, record)?;
log_line_writer.write(
now,
&log::Record::builder()
.level(log::Level::Warn)
.file_static(Some(file!()))
.line(Some(line!()))
.module_path_static(Some("flexi_logger"))
.target("flexi_logger")
.args(format_args!(
"last record has been repeated consecutive times, \
following duplicates will be skipped...",
))
.build(),
)
}
DedupAction::AllowAfterSkipped(skipped) => {
log_line_writer.write(
now,
&log::Record::builder()
.level(log::Level::Info)
.file_static(Some(file!()))
.line(Some(line!()))
.module_path_static(Some("flexi_logger"))
.target("flexi_logger")
.args(format_args!("last record was skipped {} times", skipped))
.build(),
)?;
log_line_writer.write(now, record)
}
DedupAction::Skip => Ok(()),
}
}
}
struct Deduper {
leeway: NonZeroUsize,
last_record: LastRecord,
duplicates: usize,
}
#[derive(Debug, PartialEq)]
enum DedupAction {
Allow,
AllowLastOfLeeway(usize),
AllowAfterSkipped(usize),
Skip,
}
impl Deduper {
pub fn with_leeway(leeway: NonZeroUsize) -> Self {
Self {
leeway,
last_record: LastRecord {
file: None,
line: None,
msg: String::new(),
},
duplicates: 0,
}
}
fn dedup(&mut self, record: &Record) -> DedupAction {
let new_line = record.line();
let new_file = record.file();
let new_msg = record.args().to_string();
if new_line == self.last_record.line
&& new_file == self.last_record.file.as_deref()
&& new_msg == self.last_record.msg
{
if let Some(updated_dups) = self.duplicates.checked_add(1) {
self.duplicates = updated_dups;
} else {
let skipped = self.duplicates - self.leeway();
self.duplicates = 0;
return DedupAction::AllowAfterSkipped(skipped);
}
match self.duplicates.cmp(&self.leeway()) {
Ordering::Less => DedupAction::Allow,
Ordering::Equal => DedupAction::AllowLastOfLeeway(self.leeway()),
Ordering::Greater => DedupAction::Skip,
}
} else {
self.last_record.file = new_file.map(ToOwned::to_owned);
self.last_record.line = new_line;
self.last_record.msg = new_msg;
let dups = self.duplicates;
self.duplicates = 0;
match dups {
n if n > self.leeway() => DedupAction::AllowAfterSkipped(n - self.leeway()),
_ => DedupAction::Allow,
}
}
}
fn leeway(&self) -> usize {
self.leeway.get()
}
}
struct LastRecord {
file: Option<String>,
line: Option<u32>,
msg: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_record_eq() {
let leeway = NonZeroUsize::new(1).unwrap();
let msg = format_args!("b");
let mut deduper = Deduper::with_leeway(leeway);
let record = Record::builder()
.file(Some("a"))
.line(Some(1))
.args(msg)
.build();
let diff_file = Record::builder()
.file(Some("b"))
.line(Some(1))
.args(msg)
.build();
let diff_line = Record::builder()
.file(Some("b"))
.line(Some(2))
.args(msg)
.build();
let diff_msg = Record::builder()
.file(Some("b"))
.line(Some(2))
.args(format_args!("diff msg"))
.build();
assert_eq!(deduper.dedup(&record), DedupAction::Allow);
assert_eq!(deduper.dedup(&diff_file), DedupAction::Allow);
assert_eq!(deduper.dedup(&diff_line), DedupAction::Allow);
assert_eq!(deduper.dedup(&diff_msg), DedupAction::Allow);
}
#[test]
fn test_within_leeway_and_reset() {
let leeway = NonZeroUsize::new(2).unwrap();
let mut deduper = Deduper::with_leeway(leeway);
let record_a = Record::builder()
.file(Some("a"))
.line(Some(1))
.args(format_args!("b"))
.build();
let record_b = Record::builder()
.file(Some("b"))
.line(Some(1))
.args(format_args!("b"))
.build();
assert_eq!(deduper.dedup(&record_a), DedupAction::Allow);
assert_eq!(deduper.dedup(&record_a), DedupAction::Allow);
assert_eq!(deduper.dedup(&record_b), DedupAction::Allow);
assert_eq!(deduper.dedup(&record_b), DedupAction::Allow);
assert_eq!(deduper.dedup(&record_a), DedupAction::Allow);
assert_eq!(deduper.dedup(&record_a), DedupAction::Allow);
}
#[test]
fn test_leeway_warning() {
let leeway = NonZeroUsize::new(4).unwrap();
let mut deduper = Deduper::with_leeway(leeway);
let dup = Record::builder()
.file(Some("a"))
.line(Some(1))
.args(format_args!("b"))
.build();
assert_eq!(deduper.dedup(&dup), DedupAction::Allow);
for _ in 0..(deduper.leeway() - 1) {
assert_eq!(deduper.dedup(&dup), DedupAction::Allow);
}
assert_eq!(
deduper.dedup(&dup),
DedupAction::AllowLastOfLeeway(deduper.leeway())
);
}
#[test]
fn test_dups() {
let mut deduper = Deduper::with_leeway(NonZeroUsize::new(1).unwrap());
let dup = Record::builder()
.file(Some("a"))
.line(Some(1))
.args(format_args!("b"))
.build();
let new_record = Record::builder()
.file(Some("a"))
.line(Some(1))
.args(format_args!("c"))
.build();
assert_eq!(deduper.dedup(&dup), DedupAction::Allow);
assert_eq!(deduper.dedup(&dup), DedupAction::AllowLastOfLeeway(1));
assert_eq!(deduper.dedup(&dup), DedupAction::Skip);
assert_eq!(
deduper.dedup(&new_record),
DedupAction::AllowAfterSkipped(1)
);
}
#[test]
fn test_overflowed_dups() {
let mut deduper = Deduper::with_leeway(NonZeroUsize::new(1).unwrap());
let dup = Record::builder()
.file(Some("a"))
.line(Some(1))
.args(format_args!("b"))
.build();
deduper.duplicates = usize::MAX;
assert_eq!(
deduper.dedup(&dup),
DedupAction::AllowAfterSkipped(usize::MAX - deduper.leeway())
);
assert_eq!(
deduper.dedup(&dup),
DedupAction::AllowLastOfLeeway(deduper.leeway())
);
assert_eq!(deduper.duplicates, 1);
}
}