Alert Deduplication and Ingestion#
Keep ingests alerts through two webhook endpoints, routes them through an async processing pipeline, fingerprints them per-provider, and deduplicates before writing to the database. The two key axes of behavior are (1) whether a provider_type is specified in the request and (2) whether a custom deduplication rule exists for that provider.
Ingestion Endpoints#
Both routes are defined in keep/api/routes/alerts.py and immediately return HTTP 202, handing off processing to a background thread pool or ARQ job queue (when Redis is enabled).
| Endpoint | Handler | provider_type passed downstream |
|---|---|---|
POST /alerts/event | receive_generic_event | None |
POST /alerts/event/{provider_type} | receive_event | The URL path segment |
- Generic endpoint accepts
AlertDto,list[AlertDto], or a rawdictβ no provider-specific body parsing. It passesprovider_type=Nonetoprocess_event. - Provider-specific endpoint resolves a provider class, calls
provider_class.parse_event_raw_body(event)before dispatch, and passes the resolvedprovider_typestring .
Processing Pipeline (process_event)#
process_event in keep/api/tasks/process_event_task.py orchestrates the full flow:
- Pre-format extraction rules β tenant-level enrichment rules run against the raw payload before any formatting .
- Provider formatting β if
provider_type is not Noneand the event is a rawdict,FormData, orlist, the provider class'sformat_alert()producesAlertDto(s) . If the event is already anAlertDto, formatting is skipped. - Internal preparation β
__internal_prepartionstamps each alert: defaultssourceto["keep"]if absent, overridesfingerprintif the caller supplied one, and records theapi_key_nameused . __handle_formatted_eventsβ runs maintenance-window checks, deduplication, DB save, Elasticsearch indexing, workflow triggers, rules engine, and Pusher notifications .
Silent failures:
process_eventruns after a 202 has already been returned to the caller. Any exception caught in the outer try/except is logged and retried via ARQ, but the caller is not notified .
Fingerprinting#
Each provider subclass declares FINGERPRINT_FIELDS: list[str] = [] at the class level. BaseProvider.get_alert_fingerprint(alert, fingerprint_fields) computes a SHA-256 over the values of those fields in order. If fingerprint_fields is empty, it falls back to alert.name .
After the provider's _format_alert() runs, BaseProvider.format_alert() checks for a custom deduplication rule and, if found, re-computes the fingerprint using custom_deduplication_rule.fingerprint_fields . This override happens inside format_alert() itself, so it applies consistently across all ingestion paths.
Deduplication#
Rule Resolution (AlertDeduplicator.get_deduplication_rules)#
- If
provider_typeisNone, it is normalized to"keep"so custom rules can match . - A custom rule is looked up by
(tenant_id, provider_id, provider_type). If found andfull_deduplication=True, it is used as-is . - If no custom rule exists, the default full-deduplication rule is returned:
ignore_fields=["lastReceived"],full_deduplication=Trueβ effectively hashing the entire alert payload minuslastReceived. - If a custom rule exists but
full_deduplication=False, the default rule'signore_fieldsare merged into it .
Hash Comparison (_apply_deduplication_rule)#
For each alert, ignore_fields are stripped from a deep copy, and SHA-256 is taken over the remaining JSON . The result is compared against the last stored hash for that fingerprint:
| Outcome | Flag set | DB behavior |
|---|---|---|
| Same hash, same fingerprint | isFullDuplicate = True | Dropped; only lastReceived enrichment is updated |
| Different hash, same fingerprint | isPartialDuplicate = True | Saved as a new occurrence |
| No prior hash | β | Saved normally as a new alert |
provider_type=None Behavior#
When alerts arrive via POST /alerts/event (no provider type), the deduplicator normalizes provider_type to "keep". Custom deduplication rules configured for the keep provider β including field-based fingerprint_fields β are therefore correctly applied to these alerts. Without a custom rule, they fall back to the whole-payload hash (minus lastReceived).
Key Files#
| File | Purpose |
|---|---|
keep/api/routes/alerts.py | Route handlers, 202 dispatch to thread pool or ARQ |
keep/api/tasks/process_event_task.py | End-to-end event processing orchestration |
keep/api/alert_deduplicator/alert_deduplicator.py | Rule resolution, SHA-256 hash comparison, dedup stats |
keep/providers/base/base_provider.py | FINGERPRINT_FIELDS, format_alert, get_alert_fingerprint |