This guide explains how the StellarKraal backend produces production alerts, how alert rules are structured, and how to add a new rule.
Alerts flow from backend/src/utils/alertRules.ts (rule definitions) through backend/src/utils/alerting.ts (dispatch logic) to Slack and PagerDuty.
Application code
β
βΌ
fireAlert(rule, message, meta) β backend/src/utils/alerting.ts
β
ββββΊ Slack webhook β SLACK_WEBHOOK_URL env var
β
ββββΊ PagerDuty Events v2 API β PAGERDUTY_ROUTING_KEY env var (only if rule.pagerduty = true)
Alerts are deduplicated using an in-memory cooldown: once a rule fires, it cannot fire again until its cooldownMs window expires. This prevents alert fatigue during sustained incidents.
Each rule is defined in backend/src/utils/alertRules.ts as an object conforming to the AlertRule interface:
export interface AlertRule {
id: string; // Unique identifier, used as the dedup key
name: string; // Human-readable name shown in Slack / PagerDuty
severity: AlertSeverity; // "warning" | "critical"
cooldownMs: number; // Minimum ms between successive firings of this rule
runbook: string; // Filename (relative to RUNBOOK_BASE_URL) for on-call runbook
pagerduty?: boolean; // If true, escalates to PagerDuty in addition to Slack
}| Rule key | ID | Severity | Cooldown | PagerDuty | Runbook |
|---|---|---|---|---|---|
rpcFailure |
rpc-failure |
critical | 5 min | No | rpc-failure.md |
rpcCircuitOpen |
rpc-circuit-open |
critical | 10 min | No | rpc-failure.md |
dbError |
db-error |
critical | 5 min | No | db-error.md |
liquidationFailure |
liquidation-failure |
critical | 2 min | No | liquidation-failure.md |
fivexxSpike |
5xx-spike |
critical | 1 min | Yes | 5xx-spike.md |
backupFailure |
backup-failure |
critical | 1 hour | Yes | restore-procedure.md |
Runbook files are in docs/runbooks/.
Cooldown is enforced in-process using a Map<ruleId, lastFiredTimestamp>. When fireAlert is called:
- The rule's
idis looked up in the map. - If
now - lastFired < cooldownMs, the alert is silently dropped. - Otherwise the timestamp is updated and the alert is dispatched.
This means a 5-minute cooldown rule that fires at 10:00 will not fire again until 10:05, even if the underlying condition is continuously triggered.
Important: The cooldown state is in-process and resets on restart. A restart during an incident will allow the next occurrence to fire immediately, regardless of recent history.
Slack alerts are sent to the webhook URL configured in SLACK_WEBHOOK_URL. If the variable is not set, Slack delivery is silently skipped.
The Slack message format uses the Attachments API:
- Colour: Red (
#dc2626) forcritical, amber (#ca8a04) forwarning. - Title:
[CRITICAL] Rule Name(or[WARNING]). - Fields: Runbook link + all metadata key-value pairs passed via
meta.
To configure:
# In .env or CI/CD secrets:
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T.../B.../...curl -X POST "$SLACK_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d '{"text": "StellarKraal alerting test"}'PagerDuty is only contacted when rule.pagerduty === true and PAGERDUTY_ROUTING_KEY is set. The integration uses the PagerDuty Events API v2.
Payload fields:
| Field | Value |
|---|---|
event_action |
"trigger" |
dedup_key |
The rule's id (used by PagerDuty for alert grouping) |
payload.severity |
Always "critical" |
payload.source |
"stellarkraal-backend" |
payload.custom_details |
The meta object plus the runbook URL |
To configure:
# In .env or CI/CD secrets:
PAGERDUTY_ROUTING_KEY=<integration-key-from-pagerduty-service>The routing key is found under Services β Integrations β Events API v2 in your PagerDuty account.
Both Slack and PagerDuty include a runbook link. The base URL defaults to the public GitHub runbooks path and can be overridden:
RUNBOOK_BASE_URL=https://github.com/teslims2/StellarKraal-/blob/main/docs/runbooksimport { fireAlert } from './utils/alerting';
import { rules } from './utils/alertRules';
// Example: fire the rpcFailure alert with metadata
await fireAlert(rules.rpcFailure, 'Soroban RPC call timed out after 10 s', {
endpoint: RPC_URL,
retryCount: 3,
});fireAlert is async and resolves after all channel deliveries are attempted (or failed gracefully). It will not throw even if Slack or PagerDuty are unreachable β delivery failures are logged as warnings.
Open backend/src/utils/alertRules.ts and add a new entry to the rules object:
export const rules = {
// ... existing rules ...
oracleStale: {
id: "oracle-stale",
name: "Oracle Price Feed Stale",
severity: "critical",
cooldownMs: 15 * 60 * 1000, // 15-minute cooldown
runbook: "oracle-stale.md",
pagerduty: false,
},
} satisfies Record<string, AlertRule>;Rules that affect financial correctness (e.g., oracle feeds, liquidation) should have short cooldowns and consider pagerduty: true.
Create docs/runbooks/oracle-stale.md with:
- A description of the alert condition.
- Immediate mitigation steps.
- Escalation path if mitigation fails.
import { fireAlert } from '../utils/alerting';
import { rules } from '../utils/alertRules';
// Inside your oracle polling logic:
if (priceAge > STALE_THRESHOLD_MS) {
await fireAlert(rules.oracleStale, 'Oracle price feed has not updated', {
lastUpdatedAt: new Date(lastUpdate).toISOString(),
ageMs: priceAge,
});
}Add a test in the relevant *.test.ts file to assert that fireAlert is called with the correct rule and message when the condition is triggered:
import * as alerting from '../utils/alerting';
jest.spyOn(alerting, 'fireAlert').mockResolvedValue();
// ... trigger the condition ...
expect(alerting.fireAlert).toHaveBeenCalledWith(
rules.oracleStale,
expect.stringContaining('not updated'),
expect.objectContaining({ ageMs: expect.any(Number) })
);| Variable | Required | Description |
|---|---|---|
SLACK_WEBHOOK_URL |
No | Slack Incoming Webhook URL. Alerts are skipped if unset. |
PAGERDUTY_ROUTING_KEY |
No | PagerDuty Events v2 integration key. Required for pagerduty: true rules. |
RUNBOOK_BASE_URL |
No | Base URL prefix for runbook links. Defaults to the GitHub docs/runbooks path. |
- Observability β Loki & Grafana
- Runbooks
- Source:
backend/src/utils/alerting.ts,backend/src/utils/alertRules.ts