Concepts
Architecture
One self-hosted binary sits between your monitoring stack and your AI agent, and turns raw alerts into context worth investigating.
Everything below runs inside a single alertint serve process with local
SQLite state — one binary, one config file, no external dependencies to
install.
Two feedback loops close on the triage step, and both are why the same condition doesn't get the same wrong answer twice: the verification round makes each analysis falsify its own draft before the finding persists, and an operator verdict captured over MCP steers the next triage of that failure group.
Phase 1 — Ingest and triage#
1. Webhook transmission#
Alerts arrive over HTTP(S) from any of the first-class alert sources, each on
its own receiver, all mounted on the same listen address
(receivers.address, default
:9911) and each authenticated with its own bearer token:
| Source | Route | Payload |
|---|---|---|
| Alertmanager | POST /webhook/alertmanager |
Alertmanager webhook JSON (version 4) |
| Zabbix | POST /webhook/zabbix |
Zabbix PROBLEM / RESOLVED events |
| Change events | POST /webhook/change |
Deploys, config edits, flag flips |
The first two are alert sources and feed the same pipeline end to end — a Zabbix shop gets correlated, triaged incidents exactly the way an Alertmanager shop does. Change events are not alerts: they feed the what changed right before this? plane that triage reads at analysis time, and can also be polled from Sentry releases and deploys instead of pushed.
- Auth: Bearer token per receiver (env vars named by
alertmanager.webhook_token_env,zabbix.ingress.webhook_token_env,changes.ingress.webhook_token_env) - No custom format required on any of them
2. Persistence and deduplication#
Received alerts are written to local state (SQLite). Duplicate firings of the same alert fingerprint are collapsed — one record per logical alert.
- Storage: local SQLite, configurable path
- Dedup key: alert fingerprint
3. Correlation#
Alerts that fire within a configurable time window and share a Receiver
grouping identity are grouped into one Incident. Alertmanager supplies its v4
webhook groupLabels, rendered deterministically as sorted key=value pairs;
Zabbix supplies the technical host. A non-empty
correlator.group_labels list overrides that Receiver identity for operators
who want AlertINT to group on a different label set. If the selected identity
has no value, AlertINT falls back to alertname and then fingerprint, so the
Incident group key is never empty. The rule engine runs
here too: storm collapse, known-issue short-circuits, and prompt selection.
- Grouping identity: Receiver default; optional
correlator.group_labelsoverride - Window:
correlator.window_seconds, default 90 s
4. Memory#
Before spending an analysis, AlertINT checks whether it has seen this condition before. A re-fire of an already-analyzed group key inside the collapse horizon attaches as an occurrence — the Slack card edits in place, no second LLM call. A genuinely new incident whose key matches a past analysis gets the prior finding recalled into its prompt as a past hypothesis, never as evidence. See incident memory.
- Collapse: occurrence attach, no LLM call; re-judgment only on an escalation trigger
- Recall: distilled prior findings, recurrence count, cadence
5. Evidence pack#
Once an incident's window closes, AlertINT builds an evidence pack:
shared labels, timeline, and annotations, enriched — where the matching
connector is configured — with live Prometheus metric values, recent
Loki log lines, overlapping change events,
Sentry exceptions with file:line, and
Zabbix operator context (runbook, trigger
dependencies, flap history, host CMDB and maintenance state, other open
problems). Every connector is read-only and optional; the pack degrades to
labels and annotations when none is configured.
6. AI triage — the draft#
The evidence pack goes to the configured LLM. The model returns a structured draft: probable cause, severity assessment, confidence, and suggested next checks — plus a short list of read-only checks it thinks would challenge its own conclusion.
- Model: Anthropic Claude (
claude-sonnet-5by default;claude-haiku-4-5as the lower-cost option), or any OpenAI-compatible endpoint you host - Auth:
ANTHROPIC_API_KEYenv var (or the local endpoint's key)
7. Verification round#
The draft is not the finding. AlertINT gathers contrast evidence —
facts chosen to disprove the draft — and asks the model to re-judge against
what it finds. A deterministic floor runs on every judged triage regardless
of what the model asked for (peer-scope up ratio and other incidents in
window on Prometheus installs; host reachability and host-group neighbors on
Zabbix installs), plus up to max_queries model-chosen read-only checks. A
round that can't finish marks the finding ⚠ unverified and can never raise
confidence. See verification round.
- Cost: two LLM calls per judged incident, the second reading the first's prompt-cache prefix at ~0.10× input price
- Kill-switch:
triage.verification.enabled: falserestores single-call triage
8. Outbound notification#
The final finding — the post-verification judgment, not the draft — is emitted as one JSON line on stdout and, when configured, posted to a Slack channel. When all alerts recover, AlertINT updates the original Slack message in-place (🔴 → ✅) and posts a short resolution note in the thread.
- Method: stdout (always available) and Slack Bot Token API
(
chat.postMessage/chat.update)
Phase 2 — Investigate#
9. Agent entry via MCP#
An engineer opens their MCP-capable AI client (Claude Code, Cursor, Windsurf, or any MCP-compatible tool) pointed at the AlertINT MCP server, which runs as part of the same binary — no separate daemon.
- Transport: Streamable HTTP,
http://host:9912/mcp - Auth: Bearer token (env var named by
mcp.token_env)
10. Evidence query#
The agent calls AlertINT MCP tools to list recent incidents, retrieve alert payloads, evidence packs, correlated change events, and stored findings. All data is served from local state — no external calls at this stage.
- MCP tools:
alertint_list_incidents,alertint_get_incident,alertint_search_alerts,alertint_get_evidence_pack,alertint_recent_changes,alertint_verify_audit
11. Telemetry context#
The agent queries the same backends the evidence pack drew from, scoped to the incident window — metrics, logs, and Zabbix history — through AlertINT, which proxies each query and returns the result. Queries only: nothing here writes, tails, or mutates.
- MCP tools:
prometheus_query,prometheus_query_range,loki_query_range,zabbix_metric_history,zabbix_host_problems - Backends: Prometheus HTTP API, Loki, Zabbix JSON-RPC — read paths only
12. Capture a verdict — closing the loop#
When the investigation lands somewhere the machine didn't, the agent writes
it back. alertint_incident_capture_verdict records a confirmation or a
correction against the incident; alertint_incident_annotate leaves a
note for the next investigator. Both are additive and audit-chained — they
never edit or delete what came before.
A captured correction is not taken as fact: on the next triage of that
failure group its evidence runs as verification checks and the model must
rule supported, contradicted, or unverifiable before the corrected
cause is adopted. Live evidence can retire a stale correction; the calendar
can't. See operator verdicts steer the next
triage.
- MCP tools:
alertint_incident_capture_verdict,alertint_incident_annotate - Effect: steering on the next triage of the same group key — the only write path in the product, and it writes to AlertINT's own state, never to your infrastructure
13. Decision point#
The agent synthesizes alert payloads, the stored finding, and live context into a response. The engineer decides the next action — re-query, escalate, or begin remediation — with full context already in the conversation. AlertINT's role ends at providing context; the next step is engineer-controlled.
MCP-first investigation#
The MCP server is the primary way you and your agent interact with AlertINT — there is no web UI. Typical prompts:
List recent AlertINT incidents.
Open the latest critical incident and summarize the evidence.
Show the alert labels and annotations for this incident.
Query Prometheus for CPU and memory around the incident window.
Compare the finding with the metric trend and suggest next checks.
That root cause is wrong — it was the cache rollout. Capture that as a correction.
Incident lifecycle#
collecting → ready → (skill running) → analyzed
→ failed
collecting: window is open, alerts arrivingready: window expired, incident dispatched to the triage skillanalyzed: LLM output persistedfailed: LLM call or persistence error (logged; retry is on the roadmap)
A recurrence of an analyzed incident attaches as an occurrence rather than
minting a new row — the lifecycle above describes one incident, not one
firing.
Audit log#
Every action appends a hash-chained row to the local audit log:
hash = SHA256( ts FS actor FS kind FS canonical_json(payload) FS prev_hash )
FS is the ASCII unit separator 0x1f. Each row's hash covers the
previous row, so any tampering is detectable with alertint verify-audit
or the alertint_verify_audit MCP tool.
Design constraints#
- No silent config drift — unknown YAML keys are rejected at load time.
- No inline secrets — all secret values come from env vars named by config fields.
- No 5xx to a sender — ingress always returns 2xx or 4xx; errors are logged, not propagated upstream to Alertmanager or Zabbix.
- Single binary, SQLite state — no external dependencies to install.
- Read-only outward — every connector issues queries only. The single write path is an operator verdict, and it writes to AlertINT's own state.
- MCP-first investigation — local context is exposed through the MCP server; there is no web UI.