tracing-dedup 0.1.0

tokio.rs tracing formatter that replaces repeated messages with a counter
Documentation
use std::fmt;
use std::sync::Mutex;
use tracing::{Event, Subscriber};
use tracing_subscriber::fmt::format::Writer;
use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields};
use tracing_subscriber::registry::LookupSpan;

#[derive(Clone, Hash, Eq, PartialEq)]
struct EventKey {
  message: String,
  level: tracing::Level,
  target: String,
}

pub struct DeduplicatingFormatter<F> {
  inner: F,
  state: Mutex<DeduplicationState>,
}

struct DeduplicationState {
  last_event: Option<EventKey>,
  repeat_count: usize,
}

impl<F> DeduplicatingFormatter<F> {
  pub fn new(inner: F) -> Self {
    Self {
      inner,
      state: Mutex::new(DeduplicationState {
        last_event: None,
        repeat_count: 0,
      }),
    }
  }
}

impl<S, N, F> FormatEvent<S, N> for DeduplicatingFormatter<F>
where
  S: Subscriber + for<'a> LookupSpan<'a>,
  N: for<'a> FormatFields<'a> + 'static,
  F: FormatEvent<S, N>,
{
  fn format_event(
    &self,
    ctx: &FmtContext<'_, S, N>,
    mut writer: Writer<'_>,
    event: &Event<'_>,
  ) -> fmt::Result {
    let mut visitor = MessageVisitor::default();
    event.record(&mut visitor);

    let key = EventKey {
      message: visitor.message,
      level: *event.metadata().level(),
      target: event.metadata().target().to_string(),
    };

    let mut state = self.state.lock().unwrap();

    match &state.last_event {
      Some(last) if last == &key => {
        // Same event: suppress it and increment counter
        state.repeat_count += 1;
        return Ok(()); // Skip this event
      }
      _ => {
        // Different event: output repeat count if needed
        if state.repeat_count > 0 {
          let repeat_count = state.repeat_count + 1;
          writeln!(writer, "previous message repeated {} times", repeat_count)?;
        }

        state.last_event = Some(key);
        state.repeat_count = 0;
      }
    }

    drop(state);

    // Format the event using the inner formatter
    self.inner.format_event(ctx, writer, event)
  }
}

#[derive(Default)]
struct MessageVisitor {
  message: String,
}

impl tracing::field::Visit for MessageVisitor {
  fn record_debug(
    &mut self,
    field: &tracing::field::Field,
    value: &dyn std::fmt::Debug,
  ) {
    if field.name() == "message" {
      self.message = format!("{:?}", value);
    }
  }

  fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
    if field.name() == "message" {
      self.message = value.to_string();
    }
  }
}