Reference
Alerting
Threshold rules over logs, traces and metrics, with Prometheus-style pending, firing and resolved states, delivered by email, Slack or a signed webhook.
An alert rule watches one number from your telemetry and tells you, once, when it crosses a line — and once more when it comes back. Rules live on the Alerts page of your team. They are evaluated on a schedule by querying the data Bilis already stores, so creating a rule never slows ingest down, and a service that goes completely silent can still be noticed.
Default alerts
You do not have to build rules to get alerts. Bilis sets every team up automatically — new teams as soon as they are created, existing teams on the next check (every 15 minutes):
- a Team email channel that sends to every member of the team, with issue events switched on, so you hear about new and regressed issues from day one;
- the recommended rules below, each installed once your team sends the kind of data it watches. Every rule covers all of the team's projects, including ones you add later, and splits by service or host, so a new service or server is covered the moment it reports.
New recommended rules send to every enabled channel that has issue events on — the Team email channel, or wherever you moved your issue alerts.
| Rule | Watches | Fires when | Why |
|---|---|---|---|
| Error spike | error and fatal log lines, per service | more than 3× as many as in the same 10 minutes last week, with at least 20 errors (warning) | catches a bad deploy or a failing dependency, whatever your usual error level is |
| High error rate | server spans, per service | more than 5 % of at least 50 requests in 10 minutes failed, for 5 minutes (critical) | your users are seeing errors |
| Slower than last week | p95 duration of server spans, per service | more than 2× last week's p95 over 15 minutes, with at least 50 requests, for 10 minutes (warning) | a slow regression that no fixed threshold would catch |
| Disk almost full | system.filesystem.utilization, per host |
a filesystem is more than 90 % full for 10 minutes (critical) | a full disk stops databases, logs and deploys |
| Memory pressure | system.memory.utilization (used), per host |
more than 95 % used on average for 10 minutes (warning) | the kernel is about to start killing processes |
| Host stopped reporting | host metrics, per host | a host that was reporting has sent nothing for 10 minutes (critical) | the server, or the agent on it, is down |
Error spike needs logs, High error rate and Slower than last week need traces, and the last three need host metrics from the server agent or any OpenTelemetry Collector running the host metrics receiver. The thresholds are deliberately conservative: an alert that pages too often gets ignored.
These rules carry a Set up by Bilis mark and a one-line reason on the
Alerts page, and every notification they send ends with a link to tune or
turn them off. In the webhook payload, data.rule.template_key names the
default (null for a rule you built).
- Tune a rule by editing it like any other. Once you change its name, what it watches, its threshold, its timings or its severity, Bilis stops updating it — your version is kept as is. Changing only its channels, muting it or switching it off and on does not count as an edit.
- Turn off a rule you do not want. A default rule is never deleted, only turned off, and it stays off: Bilis does not set it up again. Switch it back on from the Alerts page at any time.
- Turn one on yourself from the Templates tab on the Alerts page. It lists every default with its status — on, turned off, or not set up yet — and Turn on installs a default straight away, without waiting for Bilis to see its data, or switches a turned-off one back on.
- Rules you have not edited are kept up to date: when Bilis improves a default, they move to the new version automatically.
On a self-hosted install, set BILIS_ALERTING_PROVISIONING=false to stop
Bilis from creating the default channel and rules; teams keep anything already
created.
Comparing with last week, and minimum events
Two settings make the defaults work for any amount of traffic, and you can use them on your own log and trace rules too:
- Compare with the same time yesterday or last week turns the reading into
a multiple of normal:
3means three times what the same window read a day or a week earlier. For a count or a rate, a window that was empty back then counts as one event, so a service that is new this week can still fire. For an error ratio or a percentile there is no sensible "normal" without traffic back then, so the series has no data until there is traffic to compare with. A logratioand metric rules cannot be compared. - Minimum events ignores a series with fewer log lines or spans than that in the window — it is treated as no data. It stops "1 error instead of 0" from looking like an infinite spike.
A series of Host stopped reporting fires on no data. A host that stays silent for more than a day is assumed to be gone for good: its alert is cleared quietly, without a resolved notification, so a decommissioned server does not keep firing.
Needs attention
The dashboard's Needs attention card lists what is firing, then what is pending — most severe and most recent first, up to eight — with each value in words ("4.2× normal", "93 %") and a link to the data. It shows whether or not any channel would deliver the alert, and is hidden when nothing needs you.
What a rule is
A rule is a query, a condition and a few timings:
- Source — logs, traces or metrics, in one or more of the team's projects, or in all of them, including projects added later.
- Filters and an aggregation — what to count or measure (below).
- Group by (optional) — split the result into one series per value, for
example one per
service.name. Each series is alerted on separately. - Condition —
>,>=,<or<=a threshold. - Compare with and minimum events (optional, log and trace rules) — see Comparing with last week.
- Window — how far back each evaluation reads: 1 minute to 24 hours.
- Interval — how often the rule is evaluated: every 1, 5 or 15 minutes.
- For, keep firing for and no data — when a crossing becomes an alert and when it stops being one (below).
- Severity (
info,warning,critical) and channels — carried into the notification and where it is sent.
The rule editor draws the last 24 hours of the rule's reading with the
threshold as a line, so you can see how often it would have fired before you
save it. For a log or span count, the preview's buckets are rescaled to the
rule's window, so the line is comparable with the threshold.
Sources and aggregations
| Source | Filters | Aggregations | Group by |
|---|---|---|---|
| Logs | services, severities, full-text search over the body | count, rate (per minute), ratio (matching lines ÷ every line of the same projects and services) |
service.name |
| Traces | services, span name, span kind | count of spans, error_ratio (share with status Error), p50 / p95 / p99 of span duration in milliseconds |
service.name |
| Metrics | one metric, plus attribute = value filters |
the metric read as the explorer reads it (gauge value, counter as a rate), reduced over the window by avg, max, min or last; histograms by p50 / p95 / p99 |
any attribute key |
A log ratio narrows only the numerator: "error lines from checkout" is read
as a share of everything checkout logged. A percentile only makes sense on a
histogram, and avg does not make sense on one — a rule that asks for either
is marked as failing with that reason rather than quietly reading something
else.
States: pending, firing, resolved
Rules follow the semantics of Prometheus alerting rules, and the no-data setting follows Grafana alerting, so if you have written rules for either, these behave the way you expect.
Each series of a rule is an alert instance in one of three states:
- Inactive — the condition is false.
- Pending — the condition is true, but not yet for long enough.
- Firing — the condition has been true for at least for.
Only two moments are notified: an instance starting to fire, and a firing instance resolving. Going pending and dropping back is silent.
- For (0 to 60 minutes) — how long the condition must hold before a pending
instance fires. With
0it fires on the first evaluation that crosses. - Keep firing for (0 to 60 minutes) — how long a firing instance stays firing after its condition turns false. Use it to stop a value that hovers around the threshold from firing and resolving over and over. If the condition comes back true in that time, nothing is sent.
No data
A series Bilis has seen before can disappear: the service stopped, the host went away, the metric is no longer exported. What that means is the rule's no data setting, per rule:
- Treat as OK (
inactive) — the condition is false; a firing instance resolves. - Fire (
firing) — the condition is true; use this to be told when something goes silent. - Keep the last state (
keep_last) — leave the instance exactly as it was.
Only a series that had data once can be missing, so a brand-new rule that has
never matched anything does not fire on no data. An ungrouped log or span
count never has no data — an empty window counts 0 — so "fewer than 1 error
line in 5 minutes" works as a heartbeat rule. A ratio or a percentile over an
empty window has no value and is no data. A series that has had no data for a
day and is not firing is forgotten.
When Bilis cannot read
If ClickHouse is overloaded or unreachable, the evaluation changes no instance — nothing fires or resolves because of an outage. The rule is marked as failing with the reason, shown on the Alerts page, and one rule error notification is sent when a rule starts failing (not on every failed evaluation, and not when it recovers).
Evaluation timing
Every minute, Bilis dispatches an evaluation for each rule that is due. An
evaluation reads the window that ends at the current time minus one minute,
rounded down to the rule's interval, and the window is closed-open
([end − window, end)), so two evaluations never count a row twice. The
one-minute lag gives batching SDKs and collectors time to deliver.
Expect a notification roughly one to two minutes, plus the rule's interval,
plus its for, after the moment the data crossed the threshold. A missed
evaluation (a restart, a deploy) is skipped, never replayed: after an outage
each rule is evaluated once, on its next slot. The Alerts page shows a banner
when rules are running more than one interval late.
Channels
Channels are set up once per team, under Team settings → Alert notifications (owners and admins), and each rule picks the channels it sends to. A channel can send a test message, and shows its last delivery failure.
- Email — to every team member, or to a list of addresses. On a
self-hosted install it goes through your mailer (
MAIL_*). With Mailgun, the endpoint defaults to the EU region; a US-region account must setMAILGUN_ENDPOINT=api.mailgun.net. - Slack — a Slack
incoming webhook URL. No bot and
no OAuth: create the webhook in Slack, paste its URL. It must be on
hooks.slack.com. - Webhook — a signed
POSTto any public https URL. Use it for anything Bilis does not speak natively, such as an on-call service's inbound integration.
Failed deliveries are retried ten times over about three hours, backing off from 30 seconds to an hour between tries; a URL Bilis refuses to call fails at once. Muting a rule (for an hour, a day or a week) keeps evaluating it and recording history, and sends nothing. A disabled channel sends nothing.
Allowed webhook destinations
Slack and webhook URLs are called from the Bilis server, so they are checked before every send:
https://only, on the default port, without a user name or password.- The host must resolve only to public addresses: loopback, private, link-local, carrier-grade NAT, cloud metadata and other reserved ranges, IPv4 and IPv6, are refused. IP addresses must be written in plain dotted form.
- The request goes to the address that was checked, so a DNS answer that changes between the check and the connection is never used.
- Redirects are not followed, and a request gives up after 5 seconds.
The webhook payload
Each delivery is a JSON body following Standard Webhooks:
{
"type": "alert.firing",
"timestamp": "2026-11-08T11:59:00Z",
"data": {
"event_id": 4812,
"rule": {
"id": 17,
"name": "Checkout error spike",
"severity": "warning",
"template_key": null
},
"labels": { "service.name": "checkout" },
"value": 87.0,
"threshold": 25.0,
"condition": ">",
"state": "firing",
"severity": "warning",
"window": {
"from": "2026-11-08T11:54:00Z",
"to": "2026-11-08T11:59:00Z"
},
"error": null,
"links": {
"explorer": "https://your-bilis-host/acme/logs?from=2026-11-08T11%3A54%3A00Z&to=2026-11-08T11%3A59%3A00Z&service=checkout&severity%5B0%5D=error&severity%5B1%5D=fatal",
"rule": "https://your-bilis-host/acme/alerts/17/edit"
}
}
}
type is one of alert.firing, alert.resolved, alert.rule_error (with
data.error set), alert.issue_new and alert.issue_regressed (see
Issue events; data.rule is null) and alert.test.
links.explorer opens the logs, traces or metrics view filtered to the rule
and the evaluated window — or, for an issue event, the issue's own page.
data.issue is null for rule events; for an issue event it describes the
issue:
"issue": {
"project": "shop",
"fingerprint": "9f2c41d0a7b3e8155c0e6d2b14f8a93e7d61c0b2a45f9e3871d6c2b0e4a7f915",
"type": "TypeError",
"message": "Cannot read properties of undefined (reading 'total')",
"service": "checkout",
"events": 3,
"first_seen": "2026-11-08T11:58:41.512Z",
"resolved_at": null
}
resolved_at is set on alert.issue_regressed — when you had resolved it.
data.state is new or regressed, and rule, value, threshold and
window are null.
Verifying the signature
Every request carries three headers:
| Header | Value |
|---|---|
webhook-id |
msg_<event id> — the same on every retry, so you can de-duplicate |
webhook-timestamp |
Unix seconds when this attempt was sent |
webhook-signature |
v1,<base64 HMAC-SHA256> of <webhook-id>.<webhook-timestamp>.<body> |
When you create a webhook channel, Bilis generates a signing secret
(whsec_…) and shows it once; you can also paste your own. Verify with any
Standard Webhooks library
— they take the whsec_ secret as is. In Node:
import { Webhook } from 'standardwebhooks';
const wh = new Webhook(process.env.BILIS_WEBHOOK_SECRET); // whsec_...
// rawBody: the exact bytes received, before any JSON parsing
const event = wh.verify(rawBody, request.headers); // throws if invalid
In Python:
from standardwebhooks.webhooks import Webhook
wh = Webhook(os.environ["BILIS_WEBHOOK_SECRET"])
event = wh.verify(raw_body, request.headers) # raises if invalid
Without a library: strip whsec_, base64-decode the rest to get the key,
compute HMAC-SHA256 over id.timestamp.body with the raw body, base64 it,
and compare it in constant time with each space-separated v1, entry of
webhook-signature. Reject a timestamp more than five minutes from your
clock. Answer with any 2xx once the event is accepted; anything else is
retried.
Issue events
Besides rules, a channel can opt in to issue events (a switch on the channel): a message when a new issue is first seen, and when a resolved issue regresses — is seen again after you resolved it. These are checked every minute, are sent once per issue and occurrence, and go to every enabled channel of the team that has the switch on, independent of any rule.
Their titles read [NEW ISSUE] TypeError: Cannot read properties of undefined — checkout or [REGRESSED ISSUE] …, and link to the issue's page. Issue
events go only to channels with "issue events" on — never to a rule's
channels. One check sends at most 50 new-issue messages per team; anything
beyond that is still listed on the Issues page, just not announced.
Two limits come from how issues are watched:
- "New" goes by when the issue was first seen according to the timestamp your application sent. An exception that arrives stamped more than about three minutes in the past — a client with a wrong clock, or events buffered offline and sent late — still appears on the Issues page, but never sends a "new issue" message.
- A regression is noticed only if Bilis checks within about three minutes of the event arriving. If the scheduler was down for longer, the regression is visible on the Issues page and is announced on the issue's next event.
Monitoring the alerter
An alerting system that has quietly stopped looks exactly like a quiet night. Point an external uptime check at:
GET https://your-bilis-host/health/alerting
It needs no authentication and returns nothing but the evaluation loop's heartbeat:
| Response | Meaning |
|---|---|
200 {"status":"ok","lag":12} |
evaluations ran 12 seconds ago |
503 {"status":"stale","lag":412} |
no evaluation for more than three minutes |
503 {"status":"missing","lag":null} |
evaluations have never run |
200 {"status":"disabled"} |
alerting is switched off on this instance |
On a self-hosted install, the scheduler container's healthcheck fails on the
same condition, and alerting needs both the scheduler and the queue worker
running — evaluations and notifications run on the alerts queue, which
Horizon always consumes. BILIS_ALERTING_ENABLED=false switches alerting off
entirely.
Limits
| Limit | Value |
|---|---|
| Evaluation interval | 60 s minimum (1, 5 or 15 minutes) |
| Window | 60 s to 24 h; at most 1 h for trace rules |
for and keep firing for |
up to 1 h each |
| Series per rule | 100 — the most extreme values are kept |
| Alert history | 90 days |
| Channel test messages | 10 a minute |
On bilis.app the Free plan counts 10 alert rules per team. Like every Free-plan number it is soft: going over is shown on the plan card, and no rule is ever refused or switched off because of it. See Limits and behavior.
A trace rule reads spans directly, so its window is capped at an hour. A rule
grouped into more than 100 series keeps the 100 furthest past the threshold
(highest for >/>=, lowest for </<=); the rest keep their previous state
until they are back in the top 100.
What alerting deliberately does not do: anomaly detection, rules with several conditions, escalation, on-call schedules, acknowledgements, repeat reminders while something keeps firing, scheduled silences (only "mute until"), or evaluation more often than once a minute. The webhook channel is the way into the tools that do.