Getting started
Configuration
Complete YAML configuration reference for the AlertINT agent.
- One YAML file. Start from
config.example.yamlat the repo root, or the minimal config in the Quickstart. - Secrets stay out of it. A field ending in
_envholds the name of an environment variable; the value is read at startup. - Strict. Unknown keys are rejected, so a typo fails loudly instead of being ignored.
Before you deploy#
Validate#
alertint validate config.yaml
- Checks every key and cross-field rule; exits
0or1with actionable errors. - Skips checks that depend on the machine (the SQLite directory, the local rule-pack directory), so a pod config validates on a laptop or in CI.
- Doesn't read environment variables. Missing secrets are caught at
servestartup.
Secrets#
A starting .env:
# Required
ALERTINT_WEBHOOK_TOKEN=<openssl rand -hex 32> # Alertmanager webhook
ALERTINT_CHANGES_WEBHOOK_TOKEN=<openssl rand -hex 32> # change webhook
ALERTINT_MCP_TOKEN=<openssl rand -hex 32> # MCP clients
ANTHROPIC_API_KEY=sk-ant-... # console.anthropic.com
# Optional integrations
# PROMETHEUS_BEARER_TOKEN=...
# SLACK_BOT_TOKEN=xoxb-...
# NTFY_TOKEN=tk_...
# LOKI_BEARER_TOKEN=...
# SENTRY_AUTH_TOKEN=...
- Any long random string works for the
ALERTINT_*tokens. - Save
ALERTINT_MCP_TOKENin your team's secret store. Webhook tokens are machine-to-machine, but every teammate pastes the MCP token into each new client.
Sections at a glance#
Sections follow the pipeline: inputs, processing, outputs, then operations.
| Stage | Section | Configures | Needed? |
|---|---|---|---|
| Inputs | receivers |
Webhook listen address | When any webhook is on |
alertmanager |
Alertmanager webhook | On by default | |
zabbix |
Zabbix webhook and API | Optional | |
changes |
Change-event webhook | Optional | |
| Processing | correlator |
Grouping alerts into Incidents | Defaults work |
memory |
Recurrence collapse and recall | Defaults work | |
rules |
Local rule pack | Optional | |
llm |
Model, provider, spend limits | Required | |
triage |
Verification round, query labels | Defaults work | |
situations |
Situation timing and Slack cooldown | Defaults work | |
| Evidence | prometheus |
Metric connector | Optional |
logs, sentry |
See Loki, Sentry | Optional | |
| Outputs | notify |
stdout and Slack | stdout on by default |
mcp |
MCP server | On when its token is set | |
| Operations | storage |
SQLite path, upgrades | Defaults work |
health |
LLM health timers | Defaults work | |
telemetry |
OpenTelemetry traces | Off by default | |
log_level, log_format |
Logging | Defaults work |
At least one of alertmanager, changes.ingress, or mcp must be enabled.
Every integration has an enabled flag; once enabled, its required fields must
be set or the agent won't start.
Inputs#
receivers#
All webhooks share one listen address.
| Field | Type | Default | Description |
|---|---|---|---|
address |
string | ":9911" |
Address the webhook server binds to. Also serves GET /health. |
alertmanager#
The Alertmanager webhook. The only integration enabled by default.
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Run the receiver |
webhook_token_env |
string | — | Required. Variable holding the bearer token. Wrong or missing token → 401. |
zabbix#
ingress receives Zabbix problems; api reads context from Zabbix. See
Zabbix.
| Field | Type | Default | Description |
|---|---|---|---|
instance_id |
string | — | Stable, non-secret ID for this Zabbix installation (up to 64 letters, digits, ., _, -). Needed to trust rule versions. Changing it creates a new source identity. |
ingress.enabled |
bool | false |
Mount POST /webhook/zabbix |
ingress.webhook_token_env |
string | — | Required when enabled. Variable holding the bearer token |
api.enabled |
bool | auto | On when api.base_url is set; false forces off |
api.base_url |
string | — | Required for api. Zabbix frontend root |
api.api_token_env |
string | — | Required for api. Variable holding a read-only API token |
api.timeout_seconds |
int | 10 |
Per-request timeout |
api.default_range_minutes |
int | 60 |
Default look-back for zabbix_metric_history |
api.history_retention_days |
int | 7 |
Older windows use hourly trends instead of raw history |
api.flap_window_hours |
int | 24 |
Look-back for the trigger flap count |
api.host_label |
string | host |
Alert label holding the Zabbix host name |
changes#
Deploys, config edits, and flag flips. ingress receives them; enrichment
adds matching changes to triage and the alertint_recent_changes MCP tool.
See Change events.
| Field | Type | Default | Description |
|---|---|---|---|
ingress.enabled |
bool | false |
Mount POST /webhook/change |
ingress.webhook_token_env |
string | — | Required when enabled. Variable holding the bearer token |
enrichment.enabled |
bool | auto | On when a change source exists (the webhook or Sentry releases); false forces off |
enrichment.window_minutes |
int | 120 |
How far before the first alert to look for changes |
enrichment.max_events |
int | 10 |
Max changes added to a prompt |
retention_days |
int | 30 |
Delete changes older than this |
Processing#
correlator#
How alerts become Incidents.
| Field | Type | Default | Description |
|---|---|---|---|
window_seconds |
int | 90 |
Alerts with the same group key inside this window form one Incident |
min_alerts |
int | 1 |
Fewer alerts than this → no triage. Keep 1; control noise with notify.slack.min_severity. |
group_labels |
list | — | Build the group key from these labels instead of the source's own grouping |
- Default grouping: Alertmanager's
groupLabels(as sortedkey=valuepairs); Zabbix'shost. - Never empty: with no value for the chosen labels, the key falls back to
alertname, then the fingerprint. - Reserved: label keys starting
alertint_are rejected. - Choosing the window: shorter gives a faster first Finding with fewer alerts grouped; longer gives more context, later.
memory#
Repeats and recall. See Incident memory.
| Field | Type | Default | Description |
|---|---|---|---|
attach_window_minutes |
int | 30 |
A re-fire within this time of the last occurrence attaches instead of re-triaging |
judgment_ceiling_hours |
int | 4 |
Marks an escalation this long after the last analysis |
occurrence_cap |
int | 100 |
Marks an escalation after this many occurrences |
lookback_days |
int | 90 |
How far back recall and recurrence counts reach |
classifier.mode |
string | off |
Near-match classifier: off, shadow, or on. Quote it: bare off/on are YAML booleans. |
classifier.timeout_seconds |
int | 10 |
Classifier call timeout |
Avoid volatile labels (pod, instance, job_name) in group_labels: the
key rarely repeats, so memory rarely matches. alertint validate warns.
rules#
The built-in rule pack always loads. A local pack overrides entries with the
same id or name. Layout: pack.yaml, rules/*.yaml, templates/*.md (see
examples/rules/).
| Field | Type | Default | Description |
|---|---|---|---|
local_pack_dir |
string | — | Path to a local pack; must contain pack.yaml |
llm#
The model behind triage and Situation assessments.
| Field | Type | Default | Description |
|---|---|---|---|
provider |
string | anthropic |
anthropic or openai-compatible (see OpenAI-compatible endpoint) |
api_key_env |
string | — | Variable holding the API key. Required for anthropic. |
model |
string | claude-sonnet-5 |
Model ID, or the name your endpoint serves |
max_tokens |
int | 4096 |
Reply ceiling. Raise it if a large Incident truncates; the log says raise llm.max_tokens. |
timeout_seconds |
int | 120 |
Whole-request timeout. Local endpoints under load often need ~300. |
base_url |
string | — | openai-compatible only, required: endpoint root (a trailing /v1 is fine) |
response_format |
string | json_object |
openai-compatible only: json_object or off |
thinking |
bool | false |
openai-compatible only: enable thinking on hybrid-reasoning models. Needs max_tokens 8000–16000. |
reasoning_effort |
string | — | openai-compatible only, with thinking: true. Passed to the model unchecked (e.g. low, medium, xhigh). |
budget.calls_per_hour |
int | 0 |
Max LLM calls, retries included, in any rolling 60 minutes. 0 = unlimited. |
budget.total_tokens |
int | 0 |
Lifetime token ceiling, never reset. 0 = unlimited. |
- Choosing a model:
claude-sonnet-5gives the strongest Findings;claude-haiku-4-5costs less per Incident. - Every triaged Incident uses tokens. Watch spend in your provider console, or set a budget.
- Hitting the hourly budget delays Situation assessments until the window frees up; nothing is lost.
- Hitting the token budget, or unknown usage, stops token-budgeted calls. Affected Situations stay parked until you reconcile the budget with the agent stopped; raising the limit or restarting doesn't release them.
config.example.yamlhas the steps. - A declined request (4xx, 503, 529) counts its reported usage, or zero, and stops nothing. A budget stopped before v0.14.0 still needs the reconcile above.
- Budgets are approximate for compatible servers that report usage inaccurately.
triage#
The verification round and which alert labels may enter evidence queries.
| Field | Type | Default | Description |
|---|---|---|---|
verification.enabled |
bool | true |
false = single-call triage, no verification |
verification.max_queries |
int | 4 |
Max model-proposed checks; the floor always runs |
verification.query_timeout_seconds |
int | 10 |
Time budget for all checks, split per query |
verification.max_rounds |
int | 1 |
Only 1 is accepted |
extra_selector_labels |
list | — | Topology labels (cluster, region) to add to the built-in list (namespace, service, job, pod, container, instance) used in metric and log queries. See Prometheus. |
situations#
How often Situations are re-checked, how long recovery must hold, and how often Slack may be interrupted. The defaults suit most installs. See the Situation workflow.
Most likely to tune:
| Field | Type | Default | Description |
|---|---|---|---|
webhook_recovery_grace_seconds |
int | 120 |
How long a Situation waits after its last firing alert before it can count as recovered, or end as unconfirmed |
slack.repage_cooldown_seconds |
int | 900 |
Minimum gap before a changed required action may post to the channel again. First posts, escalations, and hand-offs skip it. |
cadence.fast_seconds |
int | 60 |
Re-check interval for urgent Situations, those confirming recovery, and those with work running |
cadence.normal_seconds |
int | 300 |
Re-check interval for Situations under investigation |
cadence.slow_seconds |
int | 900 |
Re-check interval for quiet, waiting, or parked Situations. Must be fast < normal < slow. |
Capacity and limits:
| Field | Type | Default | Description |
|---|---|---|---|
workers |
int | 2 |
Situations processed in parallel |
llm_concurrency |
int | 2 |
Max Situation-assessment LLM calls at once |
attempt_wall_seconds |
int | 180 |
Time limit for one Situation update |
preparation.max_source_calls_per_cycle |
int | 8 |
Max requests to your sources while gathering evidence for one update (1–32) |
preparation.max_wall_seconds |
int | 20 |
Time limit for gathering evidence (1–30, below attempt_wall_seconds) |
preparation.refresh_seconds |
int | 300 |
Minimum time before the same check runs again (60–3600) |
semantic_profiles.workers |
int | 1 |
Workers building advisory source profiles (1–4). Only one profile call runs at a time. |
semantic_profiles.max_attempts |
int | 3 |
Attempts per profile (1–5) |
semantic_profiles.attempt_wall_seconds |
int | 30 |
Time limit per profile call (1–60) |
Advanced, leave at default:
| Field | Type | Default | Description |
|---|---|---|---|
reconcile_poll_seconds |
int | 1 |
How often the controller wakes on its own |
lease_seconds |
int | 300 |
How long a claimed Situation or triage attempt stays claimed before another worker may take it |
heartbeat_seconds |
int | 30 |
How often a claim is renewed while work runs |
retry.min_seconds |
int | 5 |
First retry delay after a transient failure |
retry.max_seconds |
int | 300 |
Longest retry delay |
retry.jitter_percent |
int | 20 |
Random spread on retry delays |
max_l2_calls_per_attempt |
int | 2 |
Fixed: one assessment call plus one retry for a malformed reply. Other values are rejected. |
max_work_attempts_per_input |
int | 5 |
Fixed: attempts on an unchanged Situation before it's parked. Other values are rejected. |
Not configurable: triage attempts (five, see Reliability); Slack retry timing and the delivery-gap threshold (see Slack).
Evidence#
prometheus#
Adds live metric values to triage and PromQL tools to MCP. See Prometheus.
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | auto | On when base_url is set; false forces off |
base_url |
string | — | Prometheus API root, e.g. http://localhost:9090 |
bearer_token_env |
string | — | Only if your Prometheus needs a bearer token |
org_id |
string | — | Tenant ID sent as X-Scope-OrgID (Mimir, Cortex) |
timeout_seconds |
int | 10 |
Time budget for one Incident's metric fetch, split across queries |
default_range_minutes |
int | 60 |
Default look-back for range queries |
max_series |
int | 1000 |
Max series per query, so a broad selector can't flood a storm |
A slow Prometheus shows as degraded (slow), not unreachable, and doesn't
lower the Finding's confidence.
Outputs#
notify#
See ntfy setup for server, topic and credentials.
| Field | Type | Default | Description |
|---|---|---|---|
stdout |
bool | true |
Write events to stdout. Leave on. |
ntfy.enabled |
bool | false |
Send selected Situation changes to one ntfy topic, independently of Slack |
ntfy.base_url |
string | https://ntfy.sh |
Hosted or self-hosted server; no embedded credentials, query or fragment |
ntfy.topic |
string | — | Required when enabled; 1–64 letters, digits, underscores or hyphens |
ntfy.token_env |
string | — | Optional variable holding a publisher token; must be set when named |
ntfy.events |
list | five defaults | Omitted uses defaults; an explicit list replaces them; [] selects none. Event list |
slack.enabled |
bool | false |
Post Situations to Slack |
slack.bot_token_env |
string | — | Required for Slack. Variable holding the xoxb- token (chat:write scope) |
slack.channel |
string | — | Required for Slack. Channel name or ID |
slack.min_severity |
string | low |
Lowest interruption priority that may post to the channel. low posts everything; critical always passes. |
slack.recurrence_mode |
string | change-gated |
change-gated: one quiet thread reply at recurrence milestones. off: count in the message only. |
What stdout writes:
| Line | When |
|---|---|
finding JSON |
Each Finding, only at --log-level=debug. At info, a one-line summary instead. |
occurrence |
A re-fire attached to an Incident |
annotation |
An operator note was added |
triage_exhausted |
Triage failed on every attempt |
situation.transition |
Every Situation change. Means saved, not sent to Slack. Deduplicate by transition_id. |
The log also shows one notifiers ready line at startup and one notified
line per delivery (notified · stdout=ok slack=ok), so a failed send is never
silent. Slack setup: Slack.
The independent ntfy worker logs ntfy notified or retained delivery failures.
A saved transition does not prove external delivery. See
ntfy setup and troubleshooting.
mcp#
The MCP server your agent connects to. Setting the token variable turns it on.
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | auto | On when the token_env variable holds a token; false forces off |
addr |
string | "0.0.0.0:9912" |
Listen address. Endpoint: http://host:9912/mcp. |
token_env |
string | ALERTINT_MCP_TOKEN |
Variable holding the bearer token |
Lost the token? Set a new value, restart, and update every client. Client setup: MCP clients.
Operations#
storage#
| Field | Type | Default | Description |
|---|---|---|---|
sqlite_path |
string | ./alertint-agent.db |
Database file. Its directory must be writable. |
- Upgrades: every start applies pending schema migrations before accepting traffic.
- Back up first: take a live backup before upgrading (Backup & restore).
- Rollback means restore: there is no schema downgrade, and an older binary can't run on a newer database.
health#
Timers for LLM dependency health.
| Field | Type | Default | Description |
|---|---|---|---|
llm_idle_probe_after_seconds |
int | 300 |
Idle time without a real LLM call before a metadata probe runs (at most once a minute, never during a call) |
broadcast_after_seconds |
int | 300 |
How long the LLM must stay unhealthy before the Slack system message is posted. State changes within one outage don't reset it. |
telemetry#
OpenTelemetry traces for the Situation controller and triage worker. Off by default: nothing leaves the process. Spans carry IDs, counts, result classes, and durations, never prompts, responses, or secrets.
| Field | Type | Default | Description |
|---|---|---|---|
otlp.enabled |
bool | false |
Export traces |
otlp.endpoint |
string | — | Required when enabled. host:port (TLS unless insecure) or a URL (the scheme decides TLS; HTTP without a path uses /v1/traces) |
otlp.protocol |
string | grpc |
grpc or http |
otlp.insecure |
bool | false |
Plaintext for a host:port endpoint |
otlp.service_name |
string | alertint-agent |
service.name for your backend. OTEL_RESOURCE_ATTRIBUTES is merged in. |
otlp.timeout_seconds |
int | 10 |
Per export batch, and the final flush at shutdown |
telemetry:
otlp:
enabled: true
endpoint: otel-collector:4317
insecure: true
- Never blocks work. An unreachable collector is a log warning, not a startup failure.
- Logs match spans. Each span also writes a log line with the same IDs plus
trace_idandspan_id.
log_level#
debug, info (default), warn, or error.
log_format#
| Value | Output |
|---|---|
auto (default) |
console when stderr is a terminal, json otherwise |
console |
One readable line per record; colour only on a terminal with NO_COLOR unset |
json |
One JSON object per record, for log shipping |
- Precedence:
--log-level/--log-formatflags → config → defaults. - Findings as JSON: run with
--log-level=debug(seenotify). - Colour in
docker logs: setCLICOLOR_FORCE=1.NO_COLORstill wins.
Full example#
receivers:
address: "0.0.0.0:9911"
alertmanager:
enabled: true
webhook_token_env: ALERTINT_WEBHOOK_TOKEN
storage:
sqlite_path: /var/lib/alertint/alertint.db
llm:
provider: anthropic
api_key_env: ANTHROPIC_API_KEY
model: claude-sonnet-5
correlator:
window_seconds: 90
min_alerts: 1
# group_labels: [cluster, namespace, service] # optional override
notify:
stdout: true
slack:
enabled: true
bot_token_env: SLACK_BOT_TOKEN
channel: "#alerts"
mcp:
# on automatically when ALERTINT_MCP_TOKEN is set
addr: "0.0.0.0:9912"
token_env: ALERTINT_MCP_TOKEN
rules:
local_pack_dir: /etc/alertint/rules
prometheus:
base_url: http://localhost:9090
bearer_token_env: PROMETHEUS_BEARER_TOKEN
log_level: info
log_format: auto
Integration health#
At startup, each enabled integration is probed (Prometheus with a trivial
query, Slack with auth.test) and logged:
15:04:05 INFO integration health: OK · integration=prometheus detail=http://prometheus:9090
15:04:05 WARN integration health: FAILED · integration=slack detail=#alerts err=invalid_auth
GET /health returns the same, cached for 60 s:
statusreflects only whether the agent is alive. A failing integration never makes an orchestrator restart it.llmcarries the LLM dependency state:healthy,degraded, orunavailable, with per-capability detail.
{"status":"ok",
"integrations":[{"name":"prometheus","detail":"http://prometheus:9090","ok":true,"checked_at":"..."}],
"llm":{"state":"unavailable","reason":"provider_unavailable","detail":"HTTP 503","unhealthy_since":"2026-08-27T12:00:00Z","in_flight":0,
"capabilities":[{"capability":"triage_draft","healthy":false,"reason":"provider_unavailable","detail":"HTTP 503"}]}}