Class DedupingErrorReportPublisher

java.lang.Object
com.tnsai.observability.errors.DedupingErrorReportPublisher
All Implemented Interfaces:
ErrorReportPublisher

public final class DedupingErrorReportPublisher extends Object implements ErrorReportPublisher
Decorator that suppresses repeat ErrorReports of the same ErrorReport.fingerprint() within a configurable time window. The first occurrence in each window passes through to the wrapped publisher; subsequent occurrences are dropped and counted.

Standard usage — wrap the SPI-discovered chain to keep your log aggregator from drowning when 1,000 concurrent users hit the same provider 500:


 ErrorReportPublisher base = new Slf4jErrorReportPublisher();
 ErrorReportPublisher deduped = new DedupingErrorReportPublisher(
         base, Duration.ofMinutes(5));
 ErrorReports.setPublisher(deduped);
 

Five-minute window matches Sentry / Rollbar defaults and the heuristic "if you can't see the error twice in 5 minutes you don't notice it's still happening."

What "same fingerprint" means

The ErrorFingerprint hash already partitions by error class + code + normalised stack digest + relevant attributes (e.g. provider, model). Two crashes from the same root cause collapse to one fingerprint; crashes that differ in a meaningful dimension stay separate. The dedup is therefore semantically grouping by bug, not by exception object identity.

Counter exposure

getSuppressedCount(String) returns the running suppressed count for a fingerprint. Tests assert against it; production dashboards can expose it as a Prometheus gauge to surface "how many duplicates were swallowed in the current window." The counter resets to zero when the window for that fingerprint rolls over (i.e. the next emit through the decorator).

Memory

One entry per distinct fingerprint, kept in a ConcurrentHashMap. In practice fingerprint counts stay in the tens to hundreds for a healthy service; the map doesn't need eviction. If a deployment is producing thousands of distinct fingerprints, that's a deeper problem than dedup memory pressure.

Thread safety

Backed by a ConcurrentHashMap keyed by fingerprint. Within a single fingerprint, the per-fingerprint state object uses AtomicLongs for last-emit-nanos + suppressed-count; publish() synchronises on the state object briefly to atomically decide emit-vs-drop. Cross-fingerprint emits are fully parallel.
Since:
0.5.1 (issue #86 follow-up)
  • Field Details

    • DEFAULT_WINDOW

      public static final Duration DEFAULT_WINDOW
      Default dedup window — matches industry-standard error trackers.
  • Constructor Details

    • DedupingErrorReportPublisher

      public DedupingErrorReportPublisher(ErrorReportPublisher delegate)
      Wraps delegate with the DEFAULT_WINDOW dedup interval (5 minutes).
    • DedupingErrorReportPublisher

      public DedupingErrorReportPublisher(ErrorReportPublisher delegate, Duration window)
      Wraps delegate with the given dedup window.
      Parameters:
      delegate - downstream publisher; required
      window - minimum time between emissions of the same fingerprint; must be positive. Duration.ZERO effectively disables dedup (every report emits).
  • Method Details

    • publish

      public void publish(ErrorReport report)
      Description copied from interface: ErrorReportPublisher
      Publish a single error report. Errors during publication must not propagate to the caller — the caller is already in an error path; an exception here would mask the original failure.
      Specified by:
      publish in interface ErrorReportPublisher
    • getSuppressedCount

      public long getSuppressedCount(String fingerprint)
      Number of reports for fingerprint suppressed since the last emit. Returns 0 if the fingerprint has never been seen or its window has just rolled over.
    • trackedFingerprintCount

      public int trackedFingerprintCount()
      Total number of distinct fingerprints currently tracked. Useful as a sanity check / Prometheus gauge — should stay bounded for a healthy service.
    • wrap

      public static DedupingErrorReportPublisher wrap(ErrorReportPublisher delegate)
      Helper for callers building a default pipeline: DedupingErrorReportPublisher.wrap(new Slf4jErrorReportPublisher()) reads more naturally than the constructor in fluent code.
    • snapshotSuppressedCounts

      public Map<String,Long> snapshotSuppressedCounts()
      Visible for tests / SPI introspection.