Skip to content

Suppression

How Auditty reduces repetitive log noise

What Is Suppression?

When the same log line repeats multiple times, Auditty automatically suppresses the duplicates and replaces them with a concise summary message. This dramatically reduces log volume while preserving critical information about what was suppressed.

Key Benefits:

  • 80-95% cost reduction on observability platforms
  • Nothing is lost: every searchable id is listed inline in full (a window rolls to a new summary every 50 distinct identity groups, so ids are enumerated, never sampled), and every original line — including the context fields not indexed inline — is always recoverable from the Vault
  • Clean, searchable summaries: one clean envelope where every suppressed correlation key is indexed — query by any id: auditty.ids.trace_id, auditty.ids.account_id, and more
  • Visibility: a readable message (the log’s template) plus which correlation keys varied under ids, how many events, and a one-click backfill link

When Does Suppression Happen?

1. When Suppression Starts

Suppression kicks in when the same log pattern repeats approximately 25 times. Auditty identifies patterns by normalizing variable parts (timestamps, IDs, numbers) into placeholders, so logs like User 123 logged in and User 456 logged in are recognized as the same pattern.

High-severity logs (WARN/WARNING, ERROR, CRITICAL, FATAL, PANIC, ALERT, EMERGENCY) are never suppressed by default. Automatic suppression only applies to lower-severity repetitive patterns (info, debug, notice). You can override this with an explicit suppress rule if needed.

2. When Summaries Are Emitted

Once suppression starts, Auditty tracks suppressed logs and emits a summary when any of these conditions are met:

  • Count threshold: After suppressing 100 similar events — emitted immediately
  • Time window: After 10 minutes, regardless of count — emitted immediately if at least summary_min_count (default 25) events were suppressed; otherwise the window defers (up to 3 more windows) before force-emitting
  • Id capacity: When the window accumulates 50 distinct identity rows (unique per-event id combinations, e.g. distinct trace_id values) — emitted immediately and a new window starts
  • Eviction: If a window has to give up its place because a great many patterns are active at once, it is flushed immediately rather than discarded — you get the summary early, never not at all

3. When Suppression Stops

Auditty continuously evaluates log frequency. When a pattern becomes less frequent or stops appearing, suppression automatically stops for that pattern. This ensures that if a previously noisy log starts appearing rarely again, each occurrence will be preserved.

Key behavior: Count, capacity, and eviction closes always emit immediately, even if the window only contains a few events. A time-window close emits immediately once it has reached the minimum count (summary_min_count, default 25) — below that, Auditty defers up to three more windows before force-emitting, so a handful of stray repeats do not turn into a flood of tiny summaries. This ensures no suppressed logs are ever lost.

The Summary Envelope

Every suppression summary — in JSON, logfmt, or plain text — carries the same unified envelope. It never embeds a full copy of a suppressed log line; instead it states, in one clean record: a readable message, how many events were suppressed, the window they spanned, exactly which correlation keys varied under ids (and their values), and a backfill_url to the exact originals in the Vault.

The contract:

  • suppressed: number of events collapsed into this summary
  • window.start / window.end: the exact time range the suppressed events spanned
  • ids: the search index — one entry per searchable correlation key, each mapping directly to an array of its complete distinct values (a window rolls to a new summary every 50 distinct identity groups, so ids are enumerated, never sampled). Magnitudes and dimensions are kept in the Vault, not here
  • backfill_url: a signed one-click link to the exact originals in the Vault

Plain Text Logs

For plain text or unstructured logs, Auditty emits a single-line summary: a readable message (the masked template) followed by the bracketed envelope. Every id is its own greppable | name=values segment listing the complete distinct set for the window.

connection timeout after <decimal> ms [auditty: suppressed 100 over 3s (12:38:00-12:38:03) | namespace:prod | workload:api-gateway | fingerprint:f7e8d9c0 | host=10.0.0.5,10.0.0.7,10.0.0.9,10.0.0.11,10.0.0.13 | backfill_url:https://<your-hivemind-url>/backfill/replay?token=<TOKEN>&sig=<SIG>]

Readable message (the template)

The masked template of the collapsed lines — the log line itself with every varying span replaced by a typed placeholder (<decimal>, <str>, <int>, …), e.g. connection timeout after <decimal> ms. It is the template string, not a copy of one raw line; the exact originals live in the Vault.

suppressed N over T

N identical or similar logs were collapsed into this one message, over the time window T (3 seconds here), with the exact start–end range in parentheses.

| name=values

Each varying id is a greppable segment listing every distinct value (host=10.0.0.5,10.0.0.7). A window rolls to a new summary every 50 distinct identity groups, so ids are enumerated in full — never sampled — and the complete set is also always recoverable via backfill_url.

JSON Logs

For JSON-formatted logs, Auditty emits a clean, standalone object — a readable message plus the auditty envelope. It does not copy the original event's fields; the exact originals live in the Vault, one click away via backfill_url:

{
  "message": "Alert rule evaluated",

  "auditty": {
    "suppressed": 85,
    "window": { "start": "2026-01-15T09:03:00Z", "end": "2026-01-15T09:03:06Z" },
    "duration": "6s",
    "fingerprint": "f7e8d9c052931784",
    "class": "lifecycle",
    "namespace": "monitoring",
    "workload": "grafana",
      "ids": {
      "org_id":   ["1"],
      "rule_uid": ["cdyy38hxcuadcd", "edyy38fwgaz28a", "b2c3d4e5f6a1b7"]
    },
    "backfill_url": "https://<your-hivemind-url>/backfill/replay?token=<TOKEN>&sig=<SIG>"
  }
}

What's Included:

Envelope

  • message: a readable headline that describes the log itself — for free-text/logfmt logs the masked template verbatim, with varying spans shown as typed placeholders (e.g. connection timeout after <decimal> ms); for structured JSON logs an HTTP subject (GET /path) or the log’s natural message field (e.g. Alert rule evaluated). It never mentions suppression and is never a hash — the auditty object is what marks the line as a rollup
  • suppressed: number of similar events collapsed (85 here)
  • window.start / window.end + duration: the exact time range the events spanned
  • fingerprint: unique pattern ID for this log shape — the full hash in JSON and logfmt; truncated to the first 8 hex characters in plain text for readability
  • class: semantic classification (lifecycle, http, db, network, …)
  • namespace / workload / node: where the logs originated

Identifiers & Recovery

The ids object is the summary's search index: one entry per searchable correlation key, each mapping directly to an array of its distinct values. It lists only the values you would pivot on to find the raw lines — not every log attribute. A single-valued id is simply an array of length one, so there is only ever one place to look.

  • Every id → an array of values: each id maps directly to its full distinct value set, e.g. "org_id": ["1"] or "rule_uid": ["cdyy…","edyy…","b2c3…"]. Fully searchable: auditty.ids.org_id:1. A window holds up to 50 distinct identity groups before rolling to a new summary, so ids are enumerated in full across summaries — never sampled down. The complete set for any window is also recoverable via backfill_url
  • What counts as an id: a value is an id when it is (1) an operator-declared identity key (policy.suppression.identity_keys), (2) shaped like a correlation key — uuid, hex trace/span/RayID, prefixed id (cus_…), long token, compound id, IP, MAC, JWT (these use their shape category as the name — uuid, long_id, ip, … — when unkeyed), or (3) carried under a strong id-ish key name — a real id suffix (…_id, …_token), a bare id/arn/urn, or a name ending in id (sessionid, traceid, requestid). A bare generic keyword alone is not enough: a dimension or measurement like ClientRequestMethod (=GET) or ClientRequestBytes (=1234) is treated as context, not an id, so it never clutters the index — declare it in identity_keys if you do want it indexed. The rule is identical across JSON, logfmt, and plain text
  • Context is kept, not indexed: bare magnitudes and dimensions (bytes_sent, duration_ms, status, method) are not ids — they are archived losslessly to the Vault and replayable via backfill_url, but are excluded from ids so it stays a clean search index. To index a bare numeric id that has no id-ish key name, declare its key in identity_keys
  • The summary is an index, never the data: every raw line — including the fields excluded from ids — is stored in the Vault and recoverable via backfill_url
  • No misleading counts: a value is never tagged with an occurrence count, so a unique identifier never looks like it recurred
  • Bounded, never dropped from the Vault: when a window accumulates more distinct identity rows than one summary holds (50), Auditty emits another summary instead of dropping rows. Within a single summary, the inline ids listing is capped at 24 distinct id names (sorted, deterministic) — any names beyond that are omitted from that summary’s inline listing, but every value is still archived losslessly in the Vault and recoverable via backfill_url

The backfill_url is a signed link (HMAC-signed locally by Edge, using a signing key issued by Edge API) that retrieves the exact original logs from the Vault. The inline ids make correlation keys searchable, and the backfill_url retrieves the complete raw lines — including the context fields not listed in ids — so nothing is lost: 85 log lines reduced to 1, every id searchable inline and every original one click away.

Logfmt Logs

For logfmt (key=value) logs, Auditty emits the same envelope as one flat logfmt line: the human-readable template under msg= (note: msg, matching logfmt convention — not message), then every envelope key namespaced under the auditty.* prefix. Each id is auditty.ids.<name>, carrying its complete comma-separated value set. Any logfmt parser auto-facets the fields.

msg="connection timeout after <decimal> ms" auditty.suppressed=100 auditty.start=2026-01-15T09:03:00Z auditty.end=2026-01-15T09:03:06Z auditty.duration=6s auditty.namespace=monitoring auditty.workload=grafana auditty.fingerprint=f7e8d9c052931784 auditty.ids.host="10.0.0.5,10.0.0.7,10.0.0.9" auditty.ids.rule_uid="cdyy38hxcuadcd,edyy38fwgaz28a,b2c3d4e5f6a1b7" auditty.backfill_url="https://<your-hivemind-url>/backfill/replay?token=<TOKEN>&sig=<SIG>"

The msg= value is the same masked template as the plain-text head (varying spans as <decimal>/<str>/<int>); the auditty.* keys mirror the JSON envelope exactly, so a single set of parsing rules covers both structured formats.

Declaring Named Correlation Keys

Auditty indexes searchable ids automatically: shape-detected correlation keys (uuid, hex trace/RayID, prefixed id, long token, IP, MAC, JWT) and values under id-ish key names (trace_id=abc, request_id, …) land in ids under their real name. For a known schema — the correlation keys your team searches on every day — declare them in identity_keys so they are always indexed under their exact name at the highest priority, even a plain word or bare number, even on very wide logs dominated by other fields. (To index a specific magnitude such as bytes_sent, use promote_numeric_fields.)

policy:
  suppression:
    # Correlation keys that must always appear in summaries, by exact
    # (case-insensitive) field name. Matched anywhere in the log — top-level
    # or nested — and never crowded out by other fields.
    identity_keys: [RayID, X-Speleo-Trace-Id, trace_id, request_id]

How declared keys behave:

  • Deterministic: a declared key is captured under its real name every time, on both the cache-hit and cache-miss paths — no value-shape guessing
  • Highest priority: on a wide log (many fields) declared keys are never evicted to make room for other captures
  • Any depth: matched at the top level or nested inside an object (e.g. RequestHeaders.X-Speleo-Trace-Id)
  • Case-insensitive: rayid, RayID, and RAYID all match the same declared key
  • Hot-reloadable: add or change keys without restarting Edge
  • Optional: leave it empty for the automatic behavior — declaring keys only makes a known schema bulletproof

Once declared, a key shows up like any other id — auditty.ids.RayID in JSON/logfmt, | RayID=… in plain text — and is queryable in your SIEM immediately.

Platform Parsing Rules

JSON logs are auto-extracted by all major platforms — no setup needed (the nested auditty.ids.* become searchable attributes automatically). For logfmt, a built-in key-value parser auto-extracts every auditty.* key — including per-id values like auditty.ids.trace_id — so a custom rule is rarely needed. For plain text, add a Grok rule to extract the fixed envelope (suppressed, window, namespace, workload, fingerprint, backfill_url, …); the per-id | name=values segments are best consumed from the JSON or logfmt formats, or matched individually if you know the id name.

Datadog

Add a Grok Parser processor in Logs → Configuration → Pipelines. Datadog uses lowercase matchers.

Plain text
auditty_rule ^.*\[auditty: suppressed %{number:auditty.suppressed} over %{data:auditty.duration} \(%{data:auditty.window}\)( \| namespace:%{notSpace:auditty.namespace})?( \| workload:%{notSpace:auditty.workload})?( \| node:%{notSpace:auditty.node})?( \| severity:%{word:auditty.severity})?( \| class:%{notSpace:auditty.class})?( \| fingerprint:%{word:auditty.fingerprint})?( \| anomaly:%{notSpace:auditty.anomaly})?%{data}( \| backfill_url:%{data:auditty.backfill_url})?\]
Logfmt
auditty_logfmt_rule %{data} auditty\.suppressed=%{number:auditty.suppressed} auditty\.start=%{notSpace:auditty.start} auditty\.end=%{notSpace:auditty.end} auditty\.duration=%{notSpace:auditty.duration}%{data}( auditty\.backfill_url="%{data:auditty.backfill_url}")?

Datadog's built-in key-value parser can also auto-extract logfmt fields without a custom Grok rule.

Splunk

Use rex for inline extraction, or set KV_MODE = auto in props.conf for logfmt.

Plain text
index=your_index "[auditty: suppressed"
| rex "\[auditty: suppressed (?<auditty_suppressed>\d+) over (?<auditty_duration>[^ ]+) \((?<auditty_window>[^)]+)\)"
| rex "namespace:(?<auditty_namespace>[^ |\]]+)"
| rex "workload:(?<auditty_workload>[^ |\]]+)"
| rex "node:(?<auditty_node>[^ |\]]+)"
| rex "fingerprint:(?<auditty_fingerprint>[^ |\]]+)"
| rex "class:(?<auditty_class>[^ |\]]+)"
| rex "severity:(?<auditty_severity>[^ |\]]+)"
| rex "anomaly:(?<auditty_anomaly>[^ |\]]+)"
| rex "backfill_url:(?<auditty_backfill_url>[^ \]]+)"
| rex "\| host=(?<auditty_host>[^|\]]+)"
Logfmt
index=your_index "auditty.suppressed="
| kv

Elastic / Kibana

Use an ingest pipeline with a Grok processor (plain text) or KV processor (logfmt). Elastic uses uppercase matchers.

Plain text ingest pipeline
PUT _ingest/pipeline/auditty-suppression
{
  "processors": [{
    "grok": {
      "field": "message",
      "patterns": [
        "\\[auditty: suppressed %{NUMBER:auditty.suppressed} over %{DATA:auditty.duration} \\(%{DATA:auditty.window}\\)(?: \\| namespace:%{NOTSPACE:auditty.namespace})?(?: \\| workload:%{NOTSPACE:auditty.workload})?(?: \\| node:%{NOTSPACE:auditty.node})?(?: \\| severity:%{WORD:auditty.severity})?(?: \\| class:%{NOTSPACE:auditty.class})?(?: \\| fingerprint:%{WORD:auditty.fingerprint})?(?: \\| anomaly:%{NOTSPACE:auditty.anomaly})?%{DATA}(?: \\| backfill_url:%{DATA:auditty.backfill_url})?\\]"
      ],
      "ignore_missing": true, "ignore_failure": true
    }
  }]
}
Logfmt ingest pipeline
PUT _ingest/pipeline/auditty-suppression-logfmt
{
  "processors": [{
    "kv": {
      "field": "message", "field_split": " ", "value_split": "=",
      "ignore_missing": true, "ignore_failure": true
    }
  }]
}

Grafana Loki

Loki extracts at query time. For logfmt, the built-in | logfmt parser auto-extracts all auditty.* fields (dots become underscores).

Plain text
{namespace="production"} |= "[auditty: suppressed"
  | regexp `\[auditty: suppressed (?P<auditty_suppressed>\d+) over (?P<auditty_duration>[^ ]+) \((?P<auditty_window>[^)]+)\)`
  | regexp `namespace:(?P<auditty_namespace>[^ |\]]+)`
  | regexp `workload:(?P<auditty_workload>[^ |\]]+)`
  | regexp `fingerprint:(?P<auditty_fingerprint>[^ |\]]+)`
Logfmt
{namespace="production"} |= "auditty.suppressed"
  | logfmt
  | auditty_suppressed != ""

New Relic

Add a Grok parsing rule in Logs → Parsing. New Relic uses uppercase matchers and underscores in attribute names.

Plain text
%{GREEDYDATA}\[auditty: suppressed %{NUMBER:auditty_suppressed} over %{DATA:auditty_duration} \(%{DATA:auditty_window}\)( \| namespace:%{NOTSPACE:auditty_namespace})?( \| workload:%{NOTSPACE:auditty_workload})?( \| node:%{NOTSPACE:auditty_node})?( \| severity:%{WORD:auditty_severity})?( \| class:%{NOTSPACE:auditty_class})?( \| fingerprint:%{WORD:auditty_fingerprint})?( \| anomaly:%{NOTSPACE:auditty_anomaly})?%{DATA}( \| backfill_url:%{DATA:auditty_backfill_url})?\]
Logfmt
%{GREEDYDATA}auditty\.suppressed=%{NUMBER:auditty_suppressed} auditty\.start=%{NOTSPACE:auditty_start} auditty\.end=%{NOTSPACE:auditty_end} auditty\.duration=%{NOTSPACE:auditty_duration}%{GREEDYDATA}( auditty\.backfill_url="%{DATA:auditty_backfill_url}")?

Configuration

Key settings in your Edge configuration:

policy:
  suppression:
    summary_window: 600s    # Time window (default: 10 minutes)
    summary_count: 100      # Count threshold (default: 100)
    summary_min_count: 25   # Minimum count to emit (default: 25)

Tuning Tips:

  • High-frequency logs (>1000/min): Lower window to 5 minutes, increase count to 200
  • Low-frequency logs (<100/min): Increase window to 20 minutes, lower count to 50
  • Start with defaults: They work well for most workloads

Per-namespace / per-workload overrides: The window and minimum count are cluster-wide by default, but timestamp-sensitive or bursty streams (e.g. CDN or security logs that matter most during an incident) can use a shorter window for near-real-time visibility while noisy workloads keep the longer default. Add overrides under suppression:

policy:
  suppression:
    summary_window: 600s         # cluster-wide default
    overrides:
      - namespace: cdn           # all workloads in the "cdn" namespace
        summary_window: 30s
        summary_count: 50        # emit after 50 events (overrides the cluster-wide count)
      - namespace: cdn           # this workload gets an even shorter window
        workload: edge-proxy
        summary_window: 10s
        summary_min_count: 10

How overrides match:

  • Most-specific wins: a {namespace, workload} entry beats a namespace-only or workload-only entry
  • Empty workload matches any workload in that namespace; empty namespace matches that workload/service in any namespace
  • Tunable per override: summary_window, summary_count, and summary_min_count can each be set independently
  • Zero/omitted fields inherit the cluster-wide value, so you can tune just the window, just the count, or just the min-count
  • Streams with no matching override use the cluster-wide settings above

Proxy / non-Kubernetes sources: namespace is optional. OTLP / Edge Proxy streams have no namespace, so key the override by service (or workload) alone — at least one of namespace, workload, or service must be set. service is an alias for the workload slot (the suppression key folds service into workload), so either names the same stream:

policy:
  suppression:
    summary_window: 600s
    overrides:
      - service: checkout-api    # no namespace — matches this service in any namespace
        summary_window: 5s
        summary_min_count: 10

Suppression grouping key: each window is scoped per source so unrelated streams never collapse into one. For Kubernetes logs that scope is namespace + workload; for non-Kubernetes sources that have no namespace (e.g. Edge Proxy / OTLP senders), Auditty keys the window off the service attribute instead, so distinct services are summarized independently.

Renaming the Summary Key

By default Auditty namespaces every summary field under a top-level auditty key — the JSON object key ("auditty": { … }), the logfmt prefix (auditty.namespace=…), and the plain-text tag ([auditty: suppressed …]). If your SIEM dashboards and monitors already key on a house-standard namespace, you can rename it with summary_key so no downstream queries need to change:

policy:
  suppression:
    summary_key: auditty   # top-level key for summary fields (default: "auditty")

Only the top-level key changes — nested fields (suppressed, window, ids, backfill_url, …) keep their names. The value must be a safe identifier (^[A-Za-z][A-Za-z0-9_]*$ — letters, digits, underscore; no dots, spaces, or quotes) so it stays a single logfmt key segment and a valid JSON/plain-text key. The setting is hot-reloadable.

The SIEM parsing rules above (Datadog, Splunk, Elasticsearch, Loki) match on the default auditty.* prefix. If you change summary_key, update those grok/regex/pipeline patterns to your new prefix so the fields still parse.

Forwarding the Suppressed Stream (Archival)

By default suppressed events are replaced by summaries on the primary stream and the originals are kept in the Vault for backfill. If you maintain your own downstream archive (e.g. a second Vector pipeline), you can instead have Auditty forward the full suppressed stream to your primary destination tagged suppressed=true, so you can route it to cold storage and drop it from your index.

policy:
  suppression:
    tag_suppressed: true       # forward suppressed events tagged suppressed=true
    suppressed_tag: suppressed # tag key (default: "suppressed")
  • Full enrichment preserved: in DaemonSet mode the tagged stream arrives on the same path your kubernetes_logs source already tails, so pod labels/annotations are intact — no enrichment table needed
  • Proxy mode: tagged events are emitted over OTLP with suppressed=true as an attribute
  • Summaries still emit on the reduced stream, and the Vault still receives originals for backfill

Promoting Numeric Fields

The summary ids object indexes only searchable correlation keys, so bare magnitudes and counters (bytes_sent, duration_ms, committed) are kept losslessly in the Vault but are not listed in ids by default — they are context, not identifiers. If a specific stream emits a magnitude you genuinely want searchable inline, promote it per namespace/workload to add it to ids for that stream:

policy:
  suppression:
    overrides:
      - namespace: tracon
        promote_numeric_fields: [bytes_sent, bytes_received, duration]

Total-Recall Search Index (1.7.1)

Shipped in Edge 1.7.1 / Auditty 1.7.1. ids deliberately indexes only correlation keys. If your requirement is search on anything — measurements, dimensions, status codes, every value of every field, directly in your log platform — enable the Total-Recall search index. Every summary then also carries vals: the complete deduplicated set of distinct values for every extracted field in the window, plus the same tokens in the summary message so bare free-text search gets a hit.

Full configuration (defaults shown — only enabled is required)
policy:
  suppression:
    search_index:
      enabled: true                  # off by default; hot-reloadable
      channel: both                  # attributes | text | both
      max_value_len: 256             # bytes; longer values (blobs) stay Vault-only
      max_bytes_per_window: 131072   # 128 KiB per suppression window
      max_chunk_bytes: 65536         # 64 KiB per emitted summary line
      max_total_bytes: 16777216      # 16 MiB node-wide budget
    overrides:
      - namespace: payments
        search_index: true   # or scope it to specific streams only

Every summary then carries vals next to ids — the same tokens are also appended to the summary message when channel includes text:

What the summary looks like
{
  "message": "payment authorized tr_9f2ab 38 41 2210 ord_58201 ...",
  "auditty": {
    "suppressed": 8000,
    "ids":  { "trace_id": ["tr_9f2ab", "..."] },
    "vals": {
      "trace_id":   ["tr_9f2ab", "..."],
      "latency_ms": [38, 41, 2210],
      "order_id":   ["ord_58201", "..."],
      "status":     [200, 503]
    },
    "backfill_url": "https://..."
  }
}

How it works:

  • Faceted search: @auditty.vals.latency_ms:2210 or @auditty.vals.status:503 finds the summary — numeric values are emitted as JSON numbers, so range queries (@auditty.vals.latency_ms:>2000) work too
  • Free-text search: the same tokens ride the summary message, so pasting any bare value into the search bar gets a hit; from there, backfill_url replays the exact originals from the Vault
  • The economics hold: platforms billing per log line (e.g. Datadog) index a large summary line at the same cost as a small one — line-count compression is untouched, and window-level deduplication is the compression (a value repeated 8,000 times contributes one token)
  • Off by default — it changes the size and shape of emitted summaries, so it is an explicit opt-in, cluster-wide or per namespace/workload

Memory is hard-capped, and nothing is ever silently dropped. Each cap has an explicit, lossless overflow behavior:

CapDefaultOn overflow
max_value_len256 bytesLonger values (blobs, stack traces) stay Vault-only
max_bytes_per_window128 KiBBatch rolls to a new summary (flush-then-add) — every value lands in exactly one summary
max_chunk_bytes64 KiB per lineExtra chunks emit as companion search index i/N lines with the same fingerprint and backfill URL
max_total_bytes16 MiB node-wideWindow flushes early; if capture must stop, the summary carries search_partial: true (originals remain in the Vault)

Track what the index costs in egress bytes and billable lines per workload with auditty_search_index_bytes_total and auditty_search_index_spill_lines_total.

Outlier Passthrough

A promoted numeric field is searchable, but a genuinely anomalous value inside an otherwise-repetitive log line — a real latency spike buried in request completed — is still only recoverable by querying the Vault after the fact. Outlier passthrough closes that gap: when a promoted field’s value falls far outside its fingerprint’s own learned normal range, Edge forwards that one event through in full instead of suppressing it. No Vault query needed, no operator action required.

processor:
  fingerprint:
    outlier:
      enabled: true
      z_score_threshold: 4.0   # standard deviations from the learned mean to count as an outlier
      warmup_samples: 30       # observations required before gating activates for a field
      flood_guard_window: 1m   # per-fingerprint passthrough budget window
      flood_guard_max: 10      # max passthroughs per fingerprint per window
      max_tracked_fields: 8    # distinct numeric field names tracked per fingerprint
      categorical_enabled: true    # also gate never-seen values on low-cardinality string fields
      max_categorical_values: 32   # distinct values remembered per categorical field

How it works:

  • Disabled by default: this changes suppression behavior, so it is an explicit opt-in
  • Learned per fingerprint: Edge keeps a running mean and variance per promoted field per fingerprint, in fixed memory and without retaining any raw values. A value beyond z_score_threshold standard deviations bypasses suppression for that one event; the baseline still updates on the outlier itself, so a sustained shift becomes the new normal within a few more events
  • Flood guard: a single fingerprint can trigger at most flood_guard_max passthroughs per flood_guard_window, so a genuinely bimodal or noisy field can’t defeat suppression’s cost-control purpose
  • Categorical fields (categorical_enabled): the same idea for non-numeric fields — a never-before-seen value on a low-cardinality field (a status enum, an error code, a connection state) bypasses suppression once, under the same warmup and flood guard. High-cardinality fields (request ids, UUIDs) exclude themselves automatically via a distinct/total ratio check and stop gating permanently
  • Fails closed: disabled config, no baseline yet, too many distinct numeric fields, or a spent flood-guard budget all simply skip the tag — the event proceeds through normal suppression, unchanged
  • Hot-reloadable — tune thresholds without restarting Edge

Passthroughs are counted in auditty_outlier_detected_total and mirrored in auditty_suppression_skipped_total{reason="outlier_passthrough"} for observability.

Preserving Public IPs & Emails

By default the identity ledger captures private IPs but skips public IPs and email addresses: public IPs are personal data under GDPR and high-cardinality, and emails are PII — so promoting them into summaries is a deliberate, per-stream choice. Opt in per namespace/workload when you need them searchable inline (e.g. in Datadog) rather than only in the Vault:

policy:
  suppression:
    overrides:
      - namespace: edge
        preserve_public_ips: true   # capture routable client IPs inline
        preserve_emails: true       # capture email addresses inline (PII)

Both default to false; private-IP capture is unchanged. Enabling these increases the cardinality of the inline summary, so scope them to the namespaces that need them.

Summary

  • Suppression starts when the same log pattern repeats ~25 times
  • Summaries are emitted when any threshold is hit — 100 events, 10 minutes (once summary_min_count is met), 50 distinct identity rows, or window eviction
  • Suppression stops automatically when log frequency decreases
  • One unified envelope across JSON, logfmt, and plain text — a readable message (the log’s masked template) plus auditty with suppressed, window, ids, and backfill_url
  • Real id names, no noise: every id lists its complete value set inline — a window rolls to a new summary every 50 distinct identity groups, so ids are enumerated in full, never sampled
  • Nothing is lost: every correlation key is searchable inline, and every original line — including context fields — is one click away in the Vault via backfill_url
  • Need to search on anything? The opt-in Total-Recall search index puts every distinct value of every field into the summary itself — faceted and free-text searchable in your platform