Suppression
How Auditty reduces repetitive log noise
What Is Suppression?
Most of a log stream is the same few lines, over and over. Auditty recognises a repeating pattern after about 25 occurrences, forwards the first ones, and from then on replaces the repeats with one summary per window: how many, over what time, which ids varied, and a link to the originals. The volume your platform indexes falls by 80 to 95%; what you can find in it does not.
Key Benefits:
- 80-95% cost reduction on observability platforms
- Every original one click away: every searchable id is listed inline (a window rolls to a new summary every 50 distinct identity groups, so ids are enumerated in full; a summary lists up to 24 id names), and every original line, context fields included, opens 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 always forwarded by default. Automatic suppression only applies to lower-severity repetitive patterns (info, debug, notice); a line on stderr with no recognisable level counts as an error. The knob is preserve_high_severity (default true); an explicit suppress rule still wins.
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 a 51st distinct combination of ids (e.g. a new
trace_id) arrives, the summary of the first 50 is written and a new window starts - Many patterns at once: more than
summary_max_windows(1,000) patterns open at once and the oldest window is written out early, so you get its summary sooner
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 and capacity closes always emit immediately. 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. Every suppressed line is counted in exactly one summary.
The Summary Envelope
Every suppression summary, in JSON, logfmt, or plain text, carries the same unified envelope. Rather than a copy of one suppressed line, 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 span over which the suppressed events were collected; the backfill link targets the events’ own timestampsids: the search index, one entry per searchable correlation key, each mapping directly to an array of its complete distinct values. 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 (<num>, <decimal>, <str>, <id>, <uuid>, <ts>, …), 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 to 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); 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": "http",
"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 reads as the log does, not as a notice and not as 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 span over which the events were collected (a ten-minute window reads
10m0s) - 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 (
http,db,network,auth,cache,security,k8s, …); absent when nothing matched - 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. 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,internal_ip,public_ip, … when unkeyed), (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), or (4) a numeric field you promoted for the stream withpromote_numeric_fields. A bare generic keyword alone is not enough: a dimension or measurement likeClientRequestMethod(=GET) orClientRequestBytes(=1234) is treated as context, not an id, keeping the index clean; 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 in full to the Vault and replayable viabackfill_url, and left out ofidsso 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; the Vault is the data: every raw line, including the fields excluded from
ids, is stored in the Vault and recoverable viabackfill_url - No misleading counts: values carry no occurrence count, so a unique identifier reads as unique
- Bounded inline, complete in the Vault: when a window accumulates more distinct identity rows than one summary holds (50), Auditty emits another summary. Within a single summary, the inline
idslisting is capped at 24 distinct id names (sorted, deterministic); every value, listed or not, is in the Vault behindbackfill_url
The backfill_url is a signed one-click link 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: 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 masked template of the whole logfmt line under msg= (every key, values as placeholders; msg by 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="host=<ip> level=warn msg=connection timeout after <num> ms rule_uid=<id>" 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 masked template of the whole logfmt line, keys in sorted order (varying spans as <num>/<decimal>/<str>/<id>, …); 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 from JSON and logfmt lines at the highest priority, even a plain word or bare number, even on very wide logs dominated by other fields (dots and dashes in the name become underscores in ids: X-Request-Id is X_Request_Id). (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 every time it appears in a JSON or logfmt line, no value-shape guessing
- Highest priority: on a wide log (many fields) declared keys are captured first and kept ahead of everything else
- 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) 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 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 each stream is summarised on its own. 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, New Relic) 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. The in-line tag is written into JSON log lines; plain-text and logfmt lines are forwarded unchanged, so route those by the summary that follows them - 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 in full 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 extracted 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 (up to 32 fields per line; the raw line is always in the Vault), 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 capped, and every cap has a defined overflow: a value that does not fit inline is in the Vault.
| Cap | Default | On overflow |
|---|---|---|
max_value_len | 256 bytes | Longer values (blobs, stack traces) stay Vault-only |
max_bytes_per_window | 128 KiB | The summary so far is written and a new one begins; 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 | The window is summarised 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 pattern: Edge learns what is normal for each promoted field of each pattern, without keeping 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) are recognised as such and stop gating permanently - Does nothing until it can decide: 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, under the name it has in your logs
- Every original one click away: every correlation key is searchable inline, and every original line, context fields included, opens from the Vault via
backfill_url - Need to search on anything? The opt-in Total-Recall search index puts every distinct value of every extracted field into the summary itself, faceted and free-text searchable in your platform