Alerting is a control-plane feature. A rule watches one signal from the
request log, compares it with a threshold every minute, and moves between ok
and firing. A channel is the webhook a rule reports to: each change of
state is POSTed to it as JSON. The history records each change and whether
it was delivered.
In the dashboard it is the Alerting group, with one screen for each:
Channels, Alert Rules and History. The API is /api/v1/alert-channels,
/api/v1/alert-rules and /api/v1/alert-notifications.
Requirements#
- A control plane with a database. The alerting API and the evaluator are only
mounted when the control plane runs with
ROLTER_DATABASE_URL. - ClickHouse on the control plane (
CLICKHOUSE_URL), with the gateway writing request logs to it. Every signal is a query over therequest_logsorprovider_health_eventstable; without ClickHouse, evaluation fails withalert evaluation requires CLICKHOUSE_URL. ROLTER_KEK, only if a channel stores a secret. The same key is needed at delivery time to unseal it.- A route from the control plane to each channel endpoint that the egress policy permits.
- An organization, team or project admin for the scope you want to watch, or a superadmin account, an operator (a deployment role) or the deployment admin token for the whole deployment (see Scopes and ownership and Permissions).
Rules read the request log, and the log can be sampled. With a
sample_rate below 1, each kept row
carries the number of real requests it stands for, and request_volume and
spend_velocity add those up, so a threshold set against real traffic still
means real traffic. error_rate is the weighted share of failing requests, a
ratio of weighted counts. p95_latency_ms is an estimate from the sample.
Rows written before the sample weight was recorded count as one request each.
How a rule is evaluated#
Every 60 seconds the control plane loads the enabled rules and evaluates each one. The first pass runs a minute after the control plane starts.
An evaluation is one ClickHouse query over the trailing window, window_secs
seconds back from now. The result is the rule’s last_value. With the default
comparison of above, a value greater than or equal to threshold makes the
rule firing, otherwise ok; with below, a value less than or equal to it
does. A window with nothing to measure is handled by the rule’s
no_data policy. Windows
longer than a minute overlap from one pass to the next, so a 5-minute rule
looks at a sliding 5 minutes each time.
A rule without a scope reads the whole deployment. To watch a slice, give it a scope. A rule still cannot be narrowed to one provider, model or route.
For alerts that need that, use the gateway’s Prometheus metrics, which carry per-model and per-provider labels.
Signals#
Each signal’s value is in its own unit, and a rule’s threshold is in the same
unit. The API stores and returns the value as the query computes it; the
dashboard shows it under a translated name and in that unit.
| Signal | Value | Unit (API) | Dashboard | Table |
|---|---|---|---|---|
error_rate | Share of logged requests in the window with an HTTP status of 500 or above. 4xx responses, 429 included, do not count. | a fraction from 0 to 1 | Error rate, a percentage: 0.05 shows as 5% | request_logs |
p95_latency_ms | 95th-percentile request latency, from ClickHouse’s approximate quantile. | milliseconds | p95 latency: 840 ms | request_logs |
spend_velocity | Spend in the window scaled to an hourly rate, sum(cost_usd) × 3600 / window_secs, so a 5-minute and a 1-hour window give comparable figures. | settlement currency per hour | Spend rate: €12.50/h | request_logs |
request_volume | Number of logged requests in the window. A count, not a rate: the same traffic reads twice as high over a window twice as long. | requests per window | Request volume: 340 requests in 5m | request_logs |
provider_health_flaps | Number of provider health observations in the window whose outcome was not ok (an error or a timeout), across every target and every source, active probes and passive request results alike. Despite the name it counts failures, not flaps. | failed health events per window | Provider health failures: 10 failed health events in 5m | provider_health_events |
Spend is in the deployment’s settlement currency, the base of the
[currency] table
(USD unless configured). The cost_usd column name is historic and does not
mean dollars.
An empty window reads 0 for every signal, except that error_rate and
p95_latency_ms have no value at all over zero requests (a rate or a
percentile of nothing is not 0): the evaluation reports no data, stored as
a last_value of null, and the rule’s no_data
policy decides what that means. The other three signals have a true zero to
compare.
No data and below rules#
Two rule fields cover what a high-only threshold cannot:
| Field | Values | Default | Meaning |
|---|---|---|---|
comparison | above, below | above | above fires at value >= threshold, below at value <= threshold. Both are inclusive, so a value exactly at the threshold fires either way. |
no_data | ignore, fire, ok | ignore | What a window with no data does. ignore keeps the rule’s state. fire treats it as a breach. ok treats it as healthy, resolving a firing rule. |
Rules saved before these fields existed read as above and ignore, which is
how they always behaved. no_data other than ignore is accepted only on
error_rate and p95_latency_ms, the signals that can lack data; the API
rejects it elsewhere rather than store a policy that could never apply.
A rule that fires on no data records why: its history row’s detail ends in
no data in window, and the delivery carries "no_data": true, a null
value and a text saying so (see Delivery). A rule that
was already firing when a window has no data is not reported again.
Traffic stopped. To be paged when the gateway, or everything in front of
it, goes quiet, use request_volume with comparison: below. A count of zero
requests is a real reading for that signal, so threshold: 0 fires when the
window holds no requests at all:
For an error-rate rule that should not stay silent when there are no
requests to count is signal: error_rate, no_data: fire. A rule on the
request log also stops when the gateway stops logging, so a dead log pipeline
pages too; it cannot tell the two apart.
States and transitions#
A new rule starts as unknown. Each evaluation compares the reading with what
the rule last reported, which is the state of its newest history row, and
records a row when the two differ:
| Reading | Last reported | History row and delivery |
|---|---|---|
| at or over | nothing yet, or resolved | firing |
| under | firing | resolved |
| at or over | firing | nothing, or a retry of firing |
| under | resolved | nothing, or a retry of resolved |
| under | nothing yet | nothing |
“At or over” and “under” are for an above rule; a below rule reads them
the other way round, and a window with no data is one more reading, resolved
by no_data into firing, resolved or no change.
So a rule created or enabled while its condition already holds reports
firing on its first evaluation, and a rule whose first reading is under the
threshold records nothing until it first fires. A rule that stays firing for
an hour produces one row and one delivery, not sixty, once that delivery
succeeds.
Disabling a rule stops the scheduled evaluation and keeps its state as it was. Editing a rule keeps its state too, so a new threshold takes effect on the next pass and reports only if the result differs from what the rule last reported.
When an evaluation fails, the rule’s state becomes error and last_error
says why:
last_error | Cause |
|---|---|
alert evaluation requires CLICKHOUSE_URL | The control plane has no ClickHouse configured. |
could not connect to ClickHouse | ClickHouse did not accept the connection. |
analytics query timed out after 30s | The query ran past the 30-second bound. |
clickhouse query failed (<status>): <exception> | ClickHouse answered with an error, for example a missing request_logs table. Cut at 512 bytes. |
ClickHouse returned a response that is not JSON | Something other than ClickHouse answered at CLICKHOUSE_URL. |
Beside last_error the rule carries last_error_code, a stable code a client
can translate, and last_error_params, the numbers the text interpolates.
last_error stays as the English fallback. Both are null while the last
evaluation succeeded. A failed Evaluate now returns the same code as
error.code.
last_error_code | last_error_params | Cause |
|---|---|---|
clickhouse_not_configured | The control plane has no ClickHouse configured. | |
clickhouse_unreachable | ClickHouse did not accept the connection. | |
clickhouse_timeout | The request to ClickHouse timed out. | |
query_timeout | seconds | The query ran past the bound. |
clickhouse_error | ClickHouse answered with an error. | |
clickhouse_bad_response | Something other than ClickHouse answered. | |
query_failed | The query failed for another transport reason. | |
unknown_signal | signal | The stored rule names a signal this build does not know. |
An error never carries the ClickHouse URL, which can hold a password; the full
error is in the control-plane log under alert signal query failed.
last_value and last_evaluated_at keep the last successful reading, and an
error writes no history row and sends nothing. The next successful evaluation
clears last_error and compares with the last reported state as usual, so a
rule that was firing before the error is not reported again, and one whose
condition cleared during it still reports resolved.
Every control-plane process runs its own evaluator, so with more than one
replica each rule is evaluated by several of them. An evaluation holds the
rule’s row lock only while it decides and writes the history, and the
scheduled pass skips a rule another replica is holding, so one replica delivers
and records each change of state. The delivery itself runs after that, in the
background, so a receiver that is slow or not answering delays only its own
rule’s history row, never the evaluation of the others. If another replica records a newer reading
while one replica’s query is still running, the older reading is dropped rather
than applied, so a slow query cannot undo a newer result. last_evaluated_at
is the time the reading was taken.
Evaluate now#
Each rule card has an Evaluate now button, backed by
POST /api/v1/alert-rules/{id}/evaluate. It runs one evaluation immediately,
whether or not the rule is enabled, and can record and deliver a transition
exactly as the scheduled pass would. It also retries a state that was never
delivered straight away, without waiting for the retry delay. If a
scheduled pass is evaluating the same rule at that moment, it waits for it to
finish first.
The response has three fields: rule, the updated rule; notified, which is
true only when a transition was delivered; and notification, the history
row the evaluation wrote, or null when it wrote none: the reading was not a
transition and nothing was retried. A transition the endpoint refused looks
like this, with rule left out:
In the dashboard, the toast after Evaluate now says what became of a transition: delivered with the endpoint’s status, failed with the reason, or recorded without delivery, followed by the reading and the threshold it crossed in the signal’s unit. A failed delivery turns the toast red.
A failed evaluation sets the rule to error and comes back as an error toast
carrying the same reason as last_error, which makes this the quickest way to
check that ClickHouse is reachable from the control plane.
Channels#
A channel is a named webhook endpoint. webhook is the only kind.
| Field | Meaning |
|---|---|
name | Unique, 1 to 128 characters after trimming surrounding whitespace. |
kind | Always webhook, set by the server. |
endpoint | An http or https URL with a host. A URL carrying user:password@ is rejected; put a credential in managed_secret. |
enabled | A disabled channel receives nothing; its rules’ transitions are recorded as skipped. Defaults to false in the API. |
managed_secret | Optional bearer secret. Write-only, sealed before storage. |
The endpoint is stored as the HTTP client parses it, so
HTTPS://Alerts.Example.com is saved as https://alerts.example.com/.
The Add channel sheet asks for a name, the endpoint URL and the optional secret, and creates the channel enabled. Each channel card shows its kind, a secret set badge when a secret is stored, an enable switch, an edit button and a delete button.
Delivery#
When a rule’s state changes and its channel is enabled, the control plane sends
one POST to the endpoint with a JSON body:
| Field | Meaning |
|---|---|
id | The history row this delivery is recorded as, so a request can be matched to the History screen. |
state | firing or resolved. |
rule | The rule’s id and name. |
signal | The signal the rule reads. |
value | The reading that caused the change, in the signal’s unit. null when no_data is true. |
no_data | true when the change came from a window with no data, under the rule’s no_data policy. |
comparison | The rule’s comparison, above or below. |
threshold | The rule’s threshold. firing means value >= threshold, or <= for a below rule. |
unit | The unit of value and threshold: fraction (0-1), ms, currency_per_hour or count (over the window). |
currency | The settlement currency code when unit is currency_per_hour, otherwise null. |
window_secs | The trailing window the value was read over. |
evaluated_at | When the evaluation that took the reading ran, RFC 3339 in UTC. |
text | The same facts as one line, for a relay or a chat tool to display as-is. |
value and threshold are in the signal’s API unit, which unit names, so an
error_rate of 7% arrives as 0.07. text words them for a person: a
percentage for error_rate, ms for p95_latency_ms, the settlement currency
per hour for spend_velocity (EUR 12.5 per hour), and a count with its noun
for request_volume and provider_health_flaps. The window is a duration
(over the last 5 minutes), not a second count.
The request carries Content-Type: application/json, a User-Agent of
rolter/<version>, and, when the channel stores a secret,
Authorization: Bearer <secret>. The body has the same shape whichever
service receives it, so a destination that wants its own format, such as a
paging or chat service, needs a small relay in front of it that reads this
body and posts its own.
A delivery succeeds on any 2xx response. It is bounded to 10 seconds
including a 5-second connect, and redirects are not followed, so a 3xx
counts as a failure. The response body is never read or stored. At most 16
deliveries run at once per control-plane process.
Retries#
A failed delivery is recorded as failed and the rule still moves to its new
state. While that state holds, the scheduled pass sends it again: on the next
pass, then 2, 4, 8, 16 and 32 minutes after the attempt before, then hourly,
each attempt a new history row with a new id. Retries stop at the first delivered row, or when
the state changes, and the new state is delivered as usual. A rule that
resolves before its firing got through sends resolved.
A transition recorded as skipped because the channel was disabled or the rule
had none is sent on the first pass after the rule has an enabled channel. A
rule without one writes no more rows while its state holds.
A receiver can see the same firing or resolved more than once, from these
retries, and should treat each request’s id as the delivery, not the alert.
Check the History screen, or GET /api/v1/alert-notifications, for
failed rows after changing an endpoint or its secret, and use
Evaluate now to retry one without waiting.
Endpoints and the egress policy#
A channel endpoint is a URL the control plane will POST to, so it goes through
the deployment’s egress policy (the [egress] table of the bootstrap config)
when the channel is saved and again before every delivery. The default policy
denies link-local addresses, which is where cloud instance metadata lives, and
can be tightened to deny loopback or private ranges too. A channel whose
endpoint the policy denies is rejected with 400 when saved, and a delivery to
an endpoint the policy has come to deny since is recorded as failed without a
request being sent. Such a channel can still be switched off, renamed or
deleted, but not switched back on or pointed at another denied address.
Only an endpoint whose host is an IP address is checked, in whatever spelling
the URL uses: http://2852039166/, http://169.254.169.254./ and
http://[::ffff:169.254.169.254]/ are all checked as 169.254.169.254. An
endpoint with a hostname is not checked against the policy at all, neither
when it is saved nor before a delivery, so a hostname that resolves to a
private or loopback address is reached even with block_private or
block_loopback on. Checking the address a hostname resolves to when the
request connects is tracked in
#1949.
The secret is write-only#
A managed_secret is encrypted with the deployment KEK before it reaches
Postgres. With ROLTER_KEK unset, storing one fails with
storing channel credentials requires ROLTER_KEK. It is never returned: responses carry
secret_configured, a boolean. The secret is sent as an HTTP header, so a
value with a control character or a line break, such as a trailing newline
read from a file, is rejected with 400.
An update that leaves managed_secret out keeps the stored secret, so flipping
the enable switch cannot drop it, unless the update moves the endpoint to
another origin: a different scheme, host or port. That clears the stored
secret, because it was given for the receiver at the old endpoint. Send the new
receiver’s managed_secret in the same request to keep the channel
authenticated.
The secret is unsealed only to build the Authorization header of a delivery.
If it cannot be unsealed, because ROLTER_KEK is unset or differs from the key
it was sealed with, the delivery is recorded as failed and nothing is sent:
the control plane never sends a request without a credential the channel was
configured with.
The sealed column is covered by rolter kek verify and listed in
Backup and restore.
Deleting a channel#
Rules pointing at a deleted channel stay in place with their channel_id
cleared, so their next transitions are recorded as skipped with
no channel configured. History rows that named the channel keep their other
fields. A delivery already in flight when the channel is deleted still
finishes and is recorded.
Rules#
| Field | Meaning |
|---|---|
name | Unique, 1 to 128 characters after trimming surrounding whitespace. |
signal | One of the signals above. |
threshold | A finite number, 0 or above, in the signal’s own unit. See comparison. |
comparison | above (default) fires at value >= threshold, below at value <= threshold. |
no_data | ignore (default), fire or ok: what a window with no data does. |
window_secs | Trailing window in seconds, from 60 to 86400 (one minute to one day). |
channel_id | The channel to deliver to, or null to record transitions without delivering them. |
enabled | Only enabled rules are evaluated on schedule. Defaults to false in the API. |
comparison and no_data may be left out of a request, and read as their
defaults. The server also returns state, last_value (null after a window
with no data), last_evaluated_at and
last_error.
The Add rule sheet lists each signal by its dashboard name, with a line
under the picker saying what it counts and over what. The threshold field’s
label names the unit it is typed in, such as Threshold (%) or
Threshold (EUR per hour), and the field starts from that signal’s own
default: 5 (%) for error_rate, 8000 ms for p95_latency_ms, 50 per
hour for spend_velocity, 1000 requests for request_volume and 10 failed
health events for provider_health_flaps. Choosing another signal resets the
threshold to that signal’s default, since a number typed in one unit means
nothing in another.
error_rate is typed as a percentage from 0 to 100 and sent as the fraction
the API stores: typing 5 sends 0.05. The sheet will not save a percentage
over 100, which the fraction can never reach, a negative threshold, or a window
outside 60 to 86400 seconds. The window starts at 300 seconds and the first
channel is selected; pick none (record only) for a rule without one. The
rule is created enabled.
Each rule card shows its state, signal, threshold, window, last value, when it
was last evaluated and its channel, with an enable switch, Evaluate now, an
edit button and a delete button. The threshold and last value are in the signal’s unit
(5%, 840 ms, €12.50/h, 340 requests in 5m) and the window is a
duration (5m). When it was last evaluated reads as how long ago (3 min. ago),
with the full date and time on hover. A rule in error shows its last_error
under the figures, in the same amber as its state. A rule that could not be
evaluated has no reading to compare, so it does not borrow the red of firing.
The state, like the delivery statuses below, is shown in the dashboard’s
language rather than as the stored identifier.
Some starting points, with the threshold as the API takes it and as the dashboard shows it:
| Signal | Threshold (API) | Dashboard | Window | Fires when |
|---|---|---|---|---|
error_rate | 0.05 | 5% | 300 | 5% or more of the last five minutes’ requests are 5xx |
p95_latency_ms | 8000 | 8,000 ms | 600 | p95 latency over ten minutes reaches 8 seconds |
spend_velocity | 50 | $50.00/h | 3600 | the last hour spent 50 or more in the settlement currency |
request_volume | 1000 | 1,000 requests in 5m | 300 | a thousand or more requests in five minutes, about three a second |
provider_health_flaps | 10 | 10 failed health events in 5m | 300 | ten or more failed health observations in 5 minutes |
Tune request_volume to your own traffic. With the default above it catches a
surge; for a drop, use comparison: below (see
No data and below rules).
Changing a rule or channel#
The edit button on a rule or channel card opens the sheet that adds one, filled in from the row. Change the threshold, window, signal, channel or endpoint and choose Save. Do not delete and recreate instead: deleting a rule also deletes its history, and deleting a channel clears it from every rule that delivers through it. An edited rule keeps its state and history, and the change applies from its next evaluation.
Saving sends the enable switch back as it was, so editing a rule or channel that
is switched off leaves it off. The edit button follows the same permission as
the switch, alert_rule or alert_channel update.
An existing rule opens on its own threshold, in the signal’s unit: a stored
0.05 for error_rate opens as 5. Only choosing another signal resets it to
that signal’s default.
A channel’s secret field opens empty, because a stored secret is never read back. Leave it blank to keep the stored secret, or type one to replace it. A secret belongs to the endpoint’s origin, so pointing the channel at another scheme, host or port without typing a new secret drops the stored one. The sheet says so beside the field before you save.
The same change through the API is a PUT, which replaces the whole row, so
send every field. A body without enabled turns the rule or channel off.
A channel update takes name, endpoint, enabled and, only to replace it,
managed_secret.
History#
Alerting → History lists the rows 100 at a time, newest first, with the
state (firing or resolved), delivery status, time, rule, reading and detail.
The state and delivery columns come first so they stay in view on a phone. Every
change of state has a row, including those of rules with no channel.
A full page may be followed by older rows, and Load older transitions under
the list reads the next page from the last row shown (the before cursor below).
A page that comes back short is the end, and the button goes away. The toolbar
narrows the list, and each picker is sent to the API, so it reaches its own
newest rows rather than searching the page already loaded:
- Rule is sent as
rule_id. - State is sent as
state. - Delivery is sent as
delivery_status.
Changing a picker starts the list again from the newest matching row.
The Reading column shows the value the rule read when the row was written and,
under it, the threshold it was held against, both in the signal’s unit (11%
above 5%, 840 ms above 2,000 ms). A transition written for a window with no
data reads No data, and a row from before readings were recorded shows —.
The Detail column and the rule card’s last_error are translated from the
detail_code and last_error_code below, with their detail_params /
last_error_params filling in the numbers. A code this build does not know, a
row written before codes existed, and a code whose numbers did not arrive show
the control plane’s English detail / last_error as stored. A long detail wraps
onto further lines inside its column rather than being cut, so a diagnosis such
as channel secret could not be unsealed; check ROLTER_KEK reads in full. On a
phone the detail is the last column, so scroll the list sideways to reach it.
| Delivery status | Detail |
|---|---|
pending | delivery in progress. The transition is recorded and the request is being sent; the row becomes delivered or failed when it ends. A row still pending after 5 minutes is taken as lost (the control plane stopped before sending) and the state is sent again. |
delivered | The endpoint’s status, for example HTTP 200. |
failed | The endpoint’s status when it answered outside 2xx (HTTP 401, HTTP 503, HTTP 302), or what stopped the request: timed out after 10s, could not connect to the endpoint, delivery failed, endpoint denied by the egress policy, channel secret could not be unsealed; check ROLTER_KEK, or channel secret is not a valid header value. |
skipped | no channel configured or channel disabled. |
A detail never contains the endpoint’s response body, which can echo the
credential it rejected. sent_at is when the transition was recorded, just before the
delivery attempt started. A retried state has one row per attempt.
The history is append-only: the API has no update or delete for it. Deleting a rule deletes that rule’s rows along with it.
Each row also carries detail_code, a stable code for the outcome, and
detail_params, the numbers the text interpolates. detail stays as the English
fallback. A row written before codes existed has null for both.
detail_code | detail_params | Outcome |
|---|---|---|
delivery_pending | pending: the request is being sent. | |
http_ok | status | delivered: the endpoint answered 2xx. |
http_error | status | failed: the endpoint answered outside 2xx. |
timeout | seconds | failed: the delivery ran past the bound. |
connect_failed | failed: the endpoint did not accept the connection. | |
delivery_failed | failed: another transport failure. | |
egress_denied | failed: the egress policy refuses the endpoint. | |
secret_unsealable | failed: the secret cannot be unsealed; check ROLTER_KEK. | |
secret_invalid | failed: the secret is not a valid header value. | |
client_unavailable | failed: the webhook client could not be built. | |
no_channel | skipped: the rule has no channel. | |
channel_disabled | skipped: the channel is disabled. |
A row written for a window with no data adds "no_data": true to its
detail_params.
value and threshold record the reading the row was written for and the rule’s
threshold at that moment, in the signal’s API unit (see Delivery).
value is null for a no-data transition and on older rows. The Evaluate
now response carries the same fields on its notification.
The API takes these optional query parameters:
| Parameter | Meaning |
|---|---|
rule_id | One rule’s rows. Leave out for every rule. |
state | firing or resolved. |
delivery_status | delivered, failed or skipped. |
limit | Page size, default 100, clamped to 1–500. |
before | Cursor <sent_at>,<id>: the rows strictly older than that row. |
The filters combine. The list is newest first, ordered by sent_at then id, so
rows written in the same instant keep a stable order. To read the next page, pass
the last row’s sent_at and id as before; an empty page, or one shorter than
limit, is the end. A value outside the lists above, or a malformed before,
answers 400 with code invalid_field.
Scopes and ownership#
A rule can watch one scope instead of the deployment: an organization, a
team, a project, a virtual key, a business unit or a customer. It then
evaluates only the request-log rows of that scope, so a team lead’s error-rate
rule sees their team’s traffic and nothing else. The scope is set when the rule
is created (scope_type and scope_id in the body, given together) and cannot
be changed afterwards; create a new rule to watch another scope.
provider_health_flaps is recorded per organization, so it can only watch an
organization or the whole deployment. An organization’s rule counts the health
events of the providers that organization owns; providers defined in the
gateway’s own config file belong to no organization and are counted only by a
deployment-wide rule. A rule whose team, project or key has
since been deleted reports a scope error (scope_gone) and does not fire;
delete it.
Creating, editing, deleting and evaluating a rule takes admin at its scope: an organization admin for an organization rule, a team admin for a team’s, and so on. A rule for a business unit or a customer is the organization admin’s. A rule with no scope, which reads every tenant, stays superadmin-only.
A channel belongs to one organization or to the deployment. An organization
admin owns the channels of their organization (org_id in the create body); a
deployment channel is created by a superadmin and can be named by any rule. A
rule can notify only a channel of its own organization or a deployment channel,
and a deployment-wide rule only a deployment channel. The control plane refuses
anything else with a 400 on channel_id, answering an unknown channel the
same way so the id cannot be probed, and it checks again at delivery. Names
are unique per owner, so two organizations can each have a rule called
prod errors.
The lists are narrowed to what the caller may use. Rules and notification
history show only the scopes the caller administers. Channels show the
caller’s organizations’ channels and the deployment’s, but a deployment
channel’s address is withheld (endpoint is empty and manageable is false),
as it may carry a token. A caller who administers nothing gets 403.
Permissions and audit#
Every operation is decided at the owner of the row, as described above, and the dashboard shows the Alerting group to anyone who administers a scope. Deployment rows and channels are for a superadmin or an operator (a deployment role), like the other deployment-wide settings in RBAC.
| Endpoint | Capability |
|---|---|
GET, POST /api/v1/alert-channels | alert_channel read, create |
PUT, DELETE /api/v1/alert-channels/{id} | alert_channel update, delete |
GET, POST /api/v1/alert-rules | alert_rule read, create |
PUT, DELETE /api/v1/alert-rules/{id} | alert_rule update, delete |
POST /api/v1/alert-rules/{id}/evaluate | alert_history create |
GET /api/v1/alert-notifications | alert_history read |
Evaluating a rule by hand counts as creating history, because it can write a row.
Every create, update and delete of a channel or rule writes an audit entry
(alert.channel.create, alert.rule.delete and so on). Create and update
entries carry the name, the enabled flag and, for a rule, the signal and scope. A channel
update entry also records endpoint_changed, secret_replaced and
secret_cleared, as booleans. Neither a channel secret nor its endpoint ever
appears in one, since an endpoint’s query string can carry a token.
All of these operations are in the control plane’s
OpenAPI document under the alerting tag.