Skip to content

Set up alerts

The alert engine runs inside the binary and is on by default. It evaluates persisted rules every PITOT_ALERTS_EVALUATION_INTERVAL (30s) against service rollups, and delivers by webhook, by email, or both.

Open Alerts in the sidebar, then choose New rule. Enter a name and expression, plus an optional description, email recipients and webhook URL. A rule with neither email recipients nor a webhook URL goes to the default email destination when one is configured. Without one, the rule records alerts but delivers nothing. Leave Service empty to evaluate every service, or select one service. Choose Start from a template for an example expression, then set the timers and save.

The new-rule form starts enabled, with Hold for (s) 60, Cooldown (s) 300 and Repeat every (s) 3600. Notify when resolved starts off. The name, description, service, expression, email and webhook fields start empty.

Use the Enabled toggle to pause or resume a rule. The rule’s actions menu has Edit, Test against current data and Delete. Testing evaluates the saved expression without changing alert state, sending a webhook or sending email. Deleting removes the rule and its alert history.

Choose Open in Explore to chart a watched measure from raw telemetry. Its values can differ from the rule’s rollups. Explore shows error rate as a percent: 0.05 in a rule is 5 there.

Read current states and delivery results under History. PITOT_ALERTS_HISTORY_DAYS sets resolved-history retention in days (default 7).

Viewers can read rules and history, without a rule’s delivery settings: webhook URL, headers and template, email recipients, and email failure detail are returned only to operators and administrators. Creating, editing, toggling, testing and deleting require an operator or administrator session; see roles.

An expression, an optional service to scope it to, timing, and delivery:

FieldMeans
nameRequired rule name
descriptionOptional text explaining the rule
enabledWhether to evaluate the rule
expressionThe condition, in CEL
serviceEvaluate only for this service; omit for all
for_secondsHow long the condition must hold before firing
cooldown_sMinimum quiet period after resolving before firing again
repeat_interval_sHow often to re-deliver while still firing
email_recipientsArray of up to 10 addresses to email; omit for none
webhook_urlWhere to deliver by webhook
webhook_headersJSON-encoded object of extra headers, supplied as a string
webhook_templateGo text/template payload body; omit for the default
notify_on_resolveAlso deliver when the alert clears

Nonempty expressions supplied when creating or replacing a rule are type-checked against the declared variables and must return a boolean. An unknown variable or a type mistake is rejected at save time. Supply a complete rule body when replacing it; omitted fields take their zero values.

An expression reads one service’s rollup for the current window. The alert expression reference lists every variable, including error_rate, p95, throughput and the baseline deltas.

For example:

error_rate > 0.02 && throughput > 10

throughput is the total span count in the rollup window. A traffic floor prevents a single failure on a quiet service from triggering this rule.

An alert moves through three states:

  • pending: the condition is true but has not held for for_seconds yet. With for_seconds: 0 a rule skips this and fires immediately.
  • firing: it has held. The webhook and email are delivered, and re-delivered every repeat_interval_s while it stays true.
  • resolved: the expression went false. If notify_on_resolve is set, that is delivered too.

cooldown_s then holds the rule quiet for that long after resolving, which is what stops a metric hovering at the threshold from delivering repeatedly.

Each alert records its last delivery status and timestamp in GET /api/alerting/alerts. Setting repeat_interval_s to zero disables repeat delivery. Disabling a rule removes pending alerts and resolves firing alerts, sending a resolve notification when notify_on_resolve is set.

Email uses the SMTP settings Pitot already uses for sign-in codes. Without SMTP the option is disabled in the rule form, and the API rejects email_recipients with the code email_unavailable. GET /api/alerting/email reports whether email is available.

Set PITOT_ALERTS_EMAIL_DEFAULT_TO to a comma-separated list of addresses to give every rule a destination. It applies only to a rule with no webhook and no recipients of its own, including rules created before email existed. A rule with a webhook and no recipients stays webhook-only.

Email follows the rule’s existing timing: for_seconds, cooldown_s, repeat_interval_s and notify_on_resolve apply as they do to webhooks.

Addresses are checked when saved. Each must be a bare address, without a display name. They are stored in lowercase and a rule can have up to 10.

A notification is queued in the control database in the same transaction as the alert’s state change, with one row per recipient. Pitot makes up to five attempts, waiting 1, 5, 15, then 30 minutes between them. Connection failures, including a connection lost partway through a session, and 4xx SMTP replies are retried. A 5xx reply or an unusable address fails at once. Once the server has accepted a message, a failure while closing the session does not count against it. Before a retry, Pitot drops a recipient no longer on the rule, and deleting a rule cancels its queued mail.

Delivery is at least once. A restart resumes queued mail. If Pitot stops or the worker panics while a message is being handed to the mail server, or a connection drops before the server confirms it, Pitot cannot tell whether the server took it, so it sends the message again. The repeat has the same Message-ID. The same happens after a restart if the database could not record an accepted message before Pitot stopped.

PITOT_ALERTS_EMAIL_HOURLY_LIMIT (default 30) caps the alert emails sent to one address in any rolling hour, across all rules. The count is stored apart from rules, so neither a restart nor deleting a rule resets it. The last email within the limit says that the limit has been reached. Notifications over the limit are suppressed: they are dropped, never sent later, and logged as a warning.

A rule whose latest email did not go through shows it in the Rules table, and in email_status and email_status_detail on GET /api/alerting/rules:

email_statusMeans
sentEvery recipient accepted the latest notification
retryingAn attempt failed and another is scheduled
failedAt least one recipient could not be reached
suppressedAt least one recipient was over the hourly limit
unavailableThe rule has recipients but SMTP is not configured

Viewers see email_status only. email_status_detail gives the failure class and SMTP code, not the mail server’s response text.

For a rule with no webhook, the alert’s delivery status under History carries success, failed or suppressed for the email. When a rule has a webhook, that column stays the webhook’s result.

Rule names, descriptions and service names are reduced to a single line of printable text in the subject and body, and escaped in the HTML part, so a hostile service name cannot add mail headers or markup.

Evaluation reads analytical rollups. The rollup pass follows durable telemetry publication; the evaluation interval and for_seconds add further delay. How storage works explains the publication horizon and watermark. A shorter evaluation interval does not increase the rollups’ time resolution.

Leave webhook_template empty to send this body:

{"rule":"{{.Rule.Name}}","service":"{{.Alert.Service}}","state":"{{.Alert.State}}","event":"{{.Event}}","value":{{.Alert.Value}}}

Templates use Go text/template and exported field names, with this context:

ValueFields
.RuleID, Name, Description, Enabled, Service, Namespace, Expression, ForSeconds, CooldownS, RepeatIntervalS, WebhookURL, WebhookHeaders, WebhookTemplate, NotifyOnResolve, CreatedAt, UpdatedAt
.AlertID, RuleID, Service, State, Value, FiredAt, ResolvedAt, RepeatedAt, LastEvaluatedAt, LastDeliveryStatus, LastDeliveryAt, CreatedAt
.Eventfiring, reminder or resolved
.EnvErrorRate, P50, P95, Throughput, LogCount, ZScore, HealthScore, ErrorRateDelta, P95Delta, ThroughputDelta, Service
.TimeDelivery context time as a Go time.Time

.Alert.Value is the first variable the expression reads, at the last evaluation. Timestamp fields on .Rule and .Alert are strings.

For a Slack incoming webhook, set its URL and use this template:

{"text":{{printf "%q" (printf "%s: %s on %s (value %g)" .Event .Rule.Name .Alert.Service .Alert.Value)}}}

This uses the built-in printf function; no JSON template helper is registered. text/template does not automatically escape values for JSON. With this example, use names without control characters other than tabs and line breaks.

Pitot sends a POST with Content-Type: application/json unless custom headers override it. Each attempt has a 10-second total timeout and a 5-second response header timeout. Delivery makes up to three attempts, waiting 2 then 4 seconds before retries; 4xx responses are not retried.

The browser uses these routes. Reads require telemetry:read; mutations and tests require alerts:manage.

EndpointDoes
GET /api/alerting/alertsCurrent alert state
GET /api/alerting/summaryCounts by state
GET /api/alerting/rulesList rules
POST /api/alerting/rulesCreate and compile a rule
PUT /api/alerting/rules/:idReplace a rule and check any nonempty expression
POST /api/alerting/rules/:id/testEvaluate a saved expression against live data without changing alert state or delivering a webhook
DELETE /api/alerting/rules/:idDelete the rule and its alerts

When creating through HTTP, omitted booleans are false, timers are zero and optional strings are empty. In particular, omitted enabled means disabled. These API defaults differ from the new-rule form above.