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_idvalues) — 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 summarywindow.start/window.end: the exact time range the suppressed events spannedids: 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 herebackfill_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 — theaudittyobject 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 viabackfill_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 bareid/arn/urn, or a name ending inid(sessionid,traceid,requestid). A bare generic keyword alone is not enough: a dimension or measurement likeClientRequestMethod(=GET) orClientRequestBytes(=1234) is treated as context, not an id, so it never clutters the index — declare it inidentity_keysif 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 viabackfill_url, but are excluded fromidsso it stays a clean search index. To index a bare numeric id that has no id-ish key name, declare its key inidentity_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 viabackfill_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
idslisting 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 viabackfill_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, andRAYIDall 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.
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})?\]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.
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>[^|\]]+)"index=your_index "auditty.suppressed="
| kvElastic / Kibana
Use an ingest pipeline with a Grok processor (plain text) or KV processor (logfmt). Elastic uses uppercase matchers.
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
}
}]
}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).
{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>[^ |\]]+)`{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.
%{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})?\]%{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: 10How 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, andsummary_min_countcan 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: 10Suppression 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_logssource already tails, so pod labels/annotations are intact — no enrichment table needed - Proxy mode: tagged events are emitted over OTLP with
suppressed=trueas 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.
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 onlyEvery summary then carries vals next to ids — the same tokens are also appended to the summary message when channel includes text:
{
"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:2210or@auditty.vals.status:503finds 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_urlreplays 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:
| Cap | Default | On overflow |
|---|---|---|
max_value_len | 256 bytes | Longer values (blobs, stack traces) stay Vault-only |
max_bytes_per_window | 128 KiB | Batch rolls to a new summary (flush-then-add) — every value lands in exactly one summary |
max_chunk_bytes | 64 KiB per line | Extra chunks emit as companion search index i/N lines with the same fingerprint and backfill URL |
max_total_bytes | 16 MiB node-wide | Window 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 fieldHow 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_thresholdstandard 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_maxpassthroughs perflood_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_countis 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) plusaudittywithsuppressed,window,ids, andbackfill_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