Connecting oto to VictoriaMetrics (vmalert)
oto works with a VictoriaMetrics stack the way it works with Prometheus. Alertmanager stays in the middle, and you pair vmalert as the source’s rule source. That is the whole setup. The rest of this page explains why each piece is needed, and which vmalert flags decide how much of each rule oto can record.
Every vmalert flag named here was checked against the
vmalert documentation and its
source (app/vmalert/web.go, app/vmalert/rule/web.go).
1. vmalert → Alertmanager → oto. There is no direct path.
Section titled “1. vmalert → Alertmanager → oto. There is no direct path.”vmalert ── -notifier.url ──▶ Alertmanager ── webhook receiver ──▶ oto ▲ ▲ │ GET /api/v1/rules │ GET /api/v2/alerts (the reconciler) └──────────── oto ─────────────┘Point vmalert’s notifier at your Alertmanager, and Alertmanager’s webhook receiver at the source’s ingest URL with its ingest token, exactly as you would for Prometheus:
vmalert \ -datasource.url=http://victoriametrics:8428 \ -notifier.url=http://alertmanager:9093 \ -rule=/etc/vmalert/rules/*.ymlDo not point -notifier.url at oto. Two things rule it out:
- vmalert’s notifier speaks Alertmanager’s alert-posting API. oto accepts only Alertmanager’s
webhook payload, on
POST /api/v1/ingest/alertmanager/{source_id}, so a vmalert posting directly would be refused. - The reconciler needs an Alertmanager. Alertmanager drops a silenced or inhibited alert
before its webhook, so a webhook never reports suppression. oto reads suppression from
GET /api/v2/alertson the source’s Alertmanager instead (ADR 0006). Without an Alertmanager, a silenced alert looks exactly like a resolved one, and oto would record the wrong one.
The source’s base URL is the Alertmanager, as it is for Prometheus.
2. Set the source’s Prometheus URL to vmalert
Section titled “2. Set the source’s Prometheus URL to vmalert”The source’s prometheus_url is where oto reads a rule’s definition when it fires: for,
keep_firing_for, labels and annotations. That makes it a rule source, and in a
VictoriaMetrics stack the rule source is vmalert, not VictoriaMetrics:
prometheus_url = http://vmalert:8880vmalert serves GET /api/v1/rules and honours the three filters oto sends (type=alert,
rule_name[], exclude_alerts). If vmalert runs behind a path prefix, include the prefix.
Two vmalert differences oto handles, so you don’t have to:
- No
/api/v1/status/buildinfo. vmalert does not serve it (vmselect does). When buildinfo fails with an answer rather than an outage, the source health check asks the rules API instead. A valid answer counts as reachable, so source health shows the rule source as up with no version. That is expected, not a fault. keep_firing_foris spelled differently. vmalert’s rules API writeskeep_firing_for; Prometheus’s writeskeepFiringFor. oto reads both, so a rule snapshot from vmalert records the real value, and the same rule fingerprints the same on either server.
3. Send the expression in the alert’s link: -external.alert.source
Section titled “3. Send the expression in the alert’s link: -external.alert.source”Each alert carries a source link (Alertmanager’s generatorURL). oto recovers a rule
from the link first (SPEC §F.4). This costs no API call and can’t
be ambiguous, and it is the only strategy that still works when the rules API is unreachable.
For that to work, the link has to contain the expression.
vmalert’s default link holds no expression:
http://<vmalert-addr>/vmalert/alert?group_id=<group_id>&alert_id=<alert_id>Even so, oto uses that link to find the vmalert that evaluated the rule and reads the rule from
its /api/v1/rules. But an alert with only this link has no expression to fall back on if that
lookup fails.
We recommend the vmui form from the vmalert docs, so the expression travels with the alert:
vmalert \ -external.url=http://<vmui-addr> \ -external.alert.source='vmui/#/?g0.expr={{.Expr|queryEscape}}' \ ...-external.alert.source is resolved relative to -external.url, so <vmui-addr> is the
VictoriaMetrics instance that serves the UI (single-node :8428, or your vmselect). The link
then reads http://<vmui-addr>/vmui/#/?g0.expr=…. oto reads the expression from after the
#/? route, keeping a + as a +.
One consequence you should know about: that link names the VictoriaMetrics UI, not the
vmalert. When oto looks a rule up, it tries the server the link names first. VictoriaMetrics
returns an empty rules list unless it proxies to vmalert (-vmalert.proxyURL). On an empty
answer, oto falls back to the source’s prometheus_url. So you need to set section 2’s
URL either way: a followed link can only add to what oto finds, never take away.
4. -external.label is part of every alert’s identity
Section titled “4. -external.label is part of every alert’s identity”-external.label=cluster=east-1 adds a label to every alert vmalert sends. oto identifies an
Alert by its label set, so:
- Changing or adding an external label starts new Alerts. The old label sets stop arriving and their Cases close; they are not renamed. Decide on external labels before you rely on oto’s history.
- Rule matching is unaffected. oto scores a candidate rule by how many of the rule’s
labels the alert carries, and ignores labels the alert has beyond them. That one-way
comparison is what makes Prometheus’s
external_labelswork, and it works the same way for vmalert’s.
5. VictoriaLogs rules (type: vlogs)
Section titled “5. VictoriaLogs rules (type: vlogs)”A rule group with type: vlogs evaluates LogsQL against VictoriaLogs. oto records those
rules like any other, with one difference in what you can expect from the history:
- The expression is stored verbatim. A rule snapshot is a capture of what the rules API returned, and oto doesn’t parse the expression to store it. So LogsQL is recorded exactly as written, and drift between two snapshots is detected exactly as it is for PromQL.
- The expression diff is lexical, not LogsQL-aware. oto compares two expressions by
scanning them for numbers (
internal/rules/domain/exprdiff.go). It says “the threshold moved from X to Y” only when it is sure. Otherwise it shows both versions and says the expression changed. Expect the second answer more often for LogsQL than for PromQL, because the scanner refuses to guess at tokens it can’t interpret, such as LogsQL’s_time:5mwindows. - With the recommended
-external.alert.source, a vlogs alert’s link points at the VictoriaMetrics UI with a LogsQL expression in it. oto still reads the expression correctly, but the link opens the metrics UI, which won’t run LogsQL. To get working links, give vlogs groups their own vmalert with a link to the VictoriaLogs UI.
6. Try it locally: just vm
Section titled “6. Try it locally: just vm”The compose file carries this whole shape as an optional profile, vm: a VictoriaMetrics
single-node on :8428 and a vmalert on :8880 that evaluates
deploy/vmalert/rules.yml and notifies the same
Alertmanager the compose Prometheus uses. Nothing starts it unless you ask:
just vm # just infra, then VictoriaMetrics + vmalertvmalert runs with -external.url=http://localhost:8880 and the default alert link, so its
alerts arrive with the expression-less /vmalert/alert?group_id=…&alert_id=… link of section 3,
which oto follows back to this vmalert. Then:
-
Create a source whose base URL is the Alertmanager and whose Prometheus URL is vmalert, and keep its ingest token (it is shown once):
Terminal window curl -sS -X POST http://localhost:8080/api/v1/sources \-H "authorization: Bearer $OTO_PAT" -H 'content-type: application/json' \-d '{"cluster_id":"…","name":"dev-vmalert","kind":"alertmanager","base_url":"http://localhost:9093","prometheus_url":"http://localhost:8880"}' -
Point the Alertmanager at it: set
OTO_AM_LOCAL_WEBHOOK(the source’s ingest URL) andOTO_AM_LOCAL_INGEST_TOKENin.env, thenjust am-wire. Alertmanager reads both files on every send, so it needs no restart. -
Test the source:
prometheus_okistrue, and source health shows the rule source up with no version (section 2). -
Within a minute
VmStackSmokeTest(for: 15s) is a Case in oto.just vm-toggle 1firesVmDevToggleafter its 30 sfor; its rule snapshot recordsfor_seconds: 30andkeep_firing_for_seconds: 120.just vm-toggle 0resolves it two minutes later, which is thekeep_firing_forat work.
just down stops these containers with the rest; the profile only decides what up starts.
Checklist
Section titled “Checklist”- vmalert
-notifier.urlpoints at Alertmanager, not at oto. - Alertmanager’s webhook receiver points at the source’s ingest URL with its ingest token.
- The source’s base URL is the Alertmanager; its
prometheus_urlis vmalert. -
-external.url+-external.alert.source='vmui/#/?g0.expr={{.Expr|queryEscape}}'are set, so alerts carry their expression. - External labels are settled before you rely on the history.
- Source health shows the rule source reachable. A blank version is expected for vmalert.
Related pages
Section titled “Related pages”- configuration.md: oto’s own settings.
- ADR 0006: why the reconciler, and therefore Alertmanager, is required.
- ADR 0009: what a rule snapshot is.