Skip to main content

Alerts

Alerts tell you when something in the cluster needs attention: a node went offline, a disk is nearly full, a container keeps crashing, a certificate is about to expire. They appear on the Alerts page and can also be sent to Discord, to any URL that accepts a JSON POST (a generic webhook), or by email.

The cluster watches about 38 kinds of event (catalog below). Each event has a severity (Critical, Warning or Info), and each destination chooses which severities it wants.

How it works​

Only the raft leader (glossary) runs the alert engine. Every node can run the log watcher and the disk and memory checks, and they hand their findings to the leader. Alert settings are replicated through raft, so every node sees the same configuration. Every change to an alert is written to raft before a message is sent, so a new leader never re-sends what the old leader already sent. A newly elected leader also takes a silent snapshot first, so it does not re-announce things that already happened.

Lifecycle of one alert​

  1. Opened. A condition holds. A new Critical alert is sent at once. A new Warning waits out the debounce window (default 5 minutes) first.
  2. Dropped silently. A warning that stops holding inside its debounce window is dropped and no message is ever sent. Nobody gets paged by a flapping problem.
  3. Escalated. A warning that becomes critical is sent again as Escalated to <severity>: <title> (setting renotify_on_escalation, on by default).
  4. Resolved. When the condition has been absent for two checks in a row, the alert resolves. A "Resolved" follow-up goes to every destination that heard the opening (setting notify_resolved, on by default). Discord can edit the original message instead of posting a new one (edit_resolved).
  5. One-shot events. Some events are announced once and not held open: each OOM kill and each log match, plus leader changes, nodes joining or leaving, finished updates and self-update outcomes, stack deploys and removals, API key creation and revocation, secret rotation, finished volume migrations and backups that succeeded with a warning.

Severity and routing​

  • A Critical or Warning alert goes immediately to every enabled destination whose severities list contains that severity (and whose category and stack filters match).
  • An Info alert is not sent one by one. Destinations with digest on collect Info alerts into one daily digest at digest_at in the settings time zone (default 09:00).
  • A per-event route overrides all of that: muted drops the event, in_app records it on the Alerts page without sending, and a list of destinations sends to exactly those destinations, ignoring their severity filters.
  • There are no quiet hours. The nearest tools are the digest time (Info only) and muting events.

Timing​

WhatValue
Cluster conditions are re-evaluatedevery 15 s on the leader
Disk free space checkevery 60 s
Memory checkevery 15 s
Warning debounce5 min (setting warning_debounce)
Alert absent before it resolves2 consecutive checks
Quorum lost messageafter 60 s without quorum, once per outage
Delivery attempt timeout10 s (email: 20 s overall)
Delivery retriesup to 5 attempts, waiting 10 s, 30 s, 60 s, 120 s
Rate limit30 messages per destination per 10 min; extra messages are held back
History kept30 days (retention), at most 2000 entries; the Recent tab shows up to 300

A message held back by the rate limit is noted as held back: too many messages to this destination in 10 minutes and does not count as a delivery failure. A delivery that still fails after 5 attempts is recorded as failed for that destination and appears on the Destinations tab.

Events​

Defaults can be changed per event (severity and route) on the Events tab or in the events setting. The test event used by test messages is not a real alert and is not listed.

Event keyCategoryDefaultRaised when
cluster.quorum_lostClusterCriticalA manager has had no quorum for 60 s. Sent once per outage; the resolved message follows when quorum returns.
cluster.emergency_modeClusterCriticalThe cluster is in emergency mode.
cluster.leader_changedClusterInfoThe raft leader changed. Warning when it changed more than 3 times in 10 minutes ("Check the network between the managers").
cluster.node_joinedClusterInfoA node joined.
cluster.node_removedClusterInfoA node was removed.
node.unreachableNodesCriticalA node is unreachable. Warning instead of Critical for a worker.
node.disk_lowNodesWarningA drive is at or above disk_warning_percent (90). Critical at disk_critical_percent (95). Checked every 60 s. A band is entered at or above the threshold and left only when usage drops 2 points below it.
node.memory_pressureNodesWarningMemory stays above memory_percent (92) for memory_for (10 min).
instance.crash_loopWorkloadsCriticalA container keeps crashing.
instance.oom_killedWorkloadsWarningA container was killed for using too much memory. Critical on the third kill of the same instance within one hour.
instance.exitedWorkloadsWarningA container is not in its desired state (exited or missing).
instance.unhealthyWorkloadsWarningA container fails its healthcheck.
instance.unplaceableWorkloadsWarningNo node can run the container (see Placement).
instance.unreachable_by_proxyWorkloadsWarningThe proxy cannot reach a container.
instance.pin_driftWorkloadsInfoA container runs on another node than its pin.
log.matchWorkloadsWarningA log rule matched a line. Severity comes from the rule.
volume.sync_refusedStorageCriticalA sync was refused because it would wipe data.
volume.migration_failedStorageCriticalA volume migration failed.
volume.emptyStorageCriticalA volume that should hold data is empty.
volume.replica_errorStorageWarningA replica has an error or is behind its primary for at least 30 min.
volume.migration_completedStorageInfoA volume migration completed.
restore.failedBackupsCriticalA restore failed. Stays open for 3 days.
backup.failedBackupsWarningA backup failed. Critical once failures have lasted 24 h.
backup.overdueBackupsWarningNo backup succeeded within twice its interval (at least 2 h).
backup.succeeded_with_warningBackupsInfoA backup succeeded with a warning.
cert.expiringEdgeWarningA certificate expires within cert_warning_days (14). Critical within cert_critical_days (7).
cert.issuance_failedEdgeWarningCertificate issuance keeps failing.
dns.reconcile_failedEdgeWarningDNS records could not be updated.
self_update.rolled_backDeploymentsCriticalA node went back to its previous binary (see Updates).
self_update.completedDeploymentsInfoClouduler updated on a node (sent after the new binary clears probation).
update.failedDeploymentsWarningAn image update failed.
update.stuckDeploymentsWarningAn image update made no progress for over 15 minutes.
update.completedDeploymentsInfoAn image update completed.
stack.deployedDeploymentsInfoA stack was deployed ("First deploy." or "Redeployed, revision N.").
stack.removedDeploymentsInfoA stack was removed.
auth.api_key_createdSecurityInfoAn API key was created.
auth.api_key_revokedSecurityInfoAn API key was revoked.
secret.rotatedSecurityInfoA user secret was overwritten (reserved _clouduler_ secrets are not reported).

Destinations​

A destination is a named place to send alerts. Three kinds exist.

KindUI labelSends
discordDiscordA Discord webhook message: a rich embed or one plain line. See Discord alerts.
webhookGenericA POST with a JSON body (version 1, cluster, type of alert, digest or test, the alert fields and ui_url). Headers: X-Clouduler-Event, X-Clouduler-Severity, User-Agent: clouduler/<version> and an optional Authorization. Retried on 408, 429 and 5xx; other 4xx responses are not retried.
emailEmailA multipart text and HTML message through the SMTP server in smtp. See Email through SMTP.

Email subjects look like [homelab] CRITICAL: <title>, [homelab] Resolved: <title>, [homelab] Escalated to <severity>: <title>, [homelab] Daily digest: N events and [homelab] Test message from Clouduler.

Webhook and Discord URLs are stored as sealed secrets (_clouduler_alert_<name>_url, and _clouduler_alert_<name>_auth for a generic webhook's Authorization value), never in the settings. The SMTP password is _clouduler_alert_smtp_password. After saving, the UI and API show a URL only as scheme and host (https://discord.com/…).

Tasks​

Add a destination and send a test message​

Open Alerts (/alerts), then the Destinations tab, and click Add destination (admins only). Choose a type chip: Discord, Generic or Email. Enter a Name (it cannot be changed later), the Webhook URL (or URL, or Recipients for email), choose Send these severities (Critical, Warning, Info, as a daily digest), and click Send test message.

For email, set up the server first in the SMTP panel on the same tab: Host, Port, Security, Username, Password and From, then use Send a test email to.

/alerts/destinations
The Destinations tab shows each destination's status and what it receives.

Each destination shows a status: Disabled, Failing, Working or Not used yet. A banner reads "NAME has failed N deliveries in a row" when sends keep failing.

How a test works. A test sends one message with a 10 second limit through the saved destination, or through what you typed if you changed it. Stored credentials only ever go to the address they were saved for: a stored Authorization value is used only when the test URL equals the stored URL, and the stored SMTP password only when host, port and username match the stored server. If you change the URL while testing, enter the Authorization value again.

Route one event differently​

On the Events tab, open an event. Set Severity (Default, Critical, Warning, Info) and Goes to (Default, In-app only, Pick destinations, Muted). Reset to default undoes it.

Change timing and thresholds​

On the Alerts page, the timing panel has Warning debounce, Daily digest at, Time zone, Keep history for and Public URL, the toggles Send notifications, Notify again when an alert gets worse, Tell me when it is resolved and Display warnings during debounce, and a Thresholds panel for disk, memory and certificate numbers. Changes apply on the next pass, no restart needed.

Read the Recent tab​

The Recent tab (/alerts/recent) shows the metrics Critical now, Warnings now and Destinations, severity filter chips, Active now (worst first) and Earlier (grouped as Today, Yesterday, Earlier this week, Earlier). Row badges: Critical, Warning, Info, Resolved, Waiting (a warning still inside its debounce), Was followed by the old severity (escalated) and New (since you last pressed Mark all as read, which is stored in your browser). Show N log lines expands a log excerpt.

/alerts/recent
Recent alerts while boreas is down.

Log watcher​

Add logs rules to a stack's x-clouduler.alerts block to raise an alert when a line appears in a container's output. The watcher runs on every node, follows only containers that have rules, and starts at the end of the log (docker logs --follow from zero lines), so lines written before it started are never matched.

stack.yaml (excerpt)
x-clouduler:
alerts:
enabled: true
mute:
- instance.oom_killed
- backup.*
severity:
instance.exited: critical
logs:
- match: "database is shutting down"
severity: critical
title: "Database shutting down"
cooldown: 10m
stream: both
- regex: "panic|FATAL"
ignore_case: true
severity: warning
services: [app]
services:
app:
image: example/app:1.0
x-clouduler:
alerts:
mute: [log.match]
  • A rule sets exactly one of match (literal text) or regex.
  • Cooldown. After a rule fires for a container it stays quiet for cooldown (default 5 minutes). Matches inside the cooldown are counted, and the next message says It matched N more times in the last ….
  • Excerpt. The watcher waits 1 second after a match so nearby lines join the excerpt. It keeps at most 3 lines, each cut to 500 characters. Lines over 8 KB are cut.
  • Caps. At most 30 log signals per minute per node; the queue holds 64. A flood of matches can silently lose some.
  • Rules are only accepted in the stack-level block. A service-level block can set enabled, mute and severity; it is merged over the stack block.
  • mute accepts event keys or a prefix.* pattern that covers a whole category. enabled: false mutes every event for the stack, including its log rules.
  • The container is only watched when its stack spec contains the text logs.

Options​

Alert settings (_clouduler_alerts)​

All settings apply live.

Option
Type / default
Reload
Description
booldefault true
live reload
Master switch. When false nothing is recorded or sent.
durationdefault 5m
live reload
How long a warning must last before it is sent. Zero is allowed.
Watch out: A warning that clears inside the window is dropped without any message.
HH:MMdefault 09:00
live reload
Time of the daily digest, in the settings time zone.
Watch out: Error: use HH:MM, for example 09:00
IANA nameno default
live reload
Time zone for the digest time.
Watch out: Error: not an IANA time zone like Europe/Paris.
booldefault true
live reload
Send again when a warning becomes critical.
booldefault true
live reload
Send a resolved follow-up to destinations that heard the opening.
booldefault false
live reload
Show warnings still inside their debounce window in the active list (badge Waiting).
durationdefault 30d
live reload
How long resolved alerts stay in history. Also capped at 2000 entries.
URLno default
live reload
Base URL for links in messages. Without it links use https://<fqdn> from the cluster config.
Watch out: Set it if the UI is only reachable through another address, or links will not open.
listdefault []
live reload
Where alerts go. See the destination tables.
object or nullno default
live reload
Email server. Required before an email destination can be added.
Watch out: Error: set up the email server before adding an email destination
mapdefault {}
live reload
Per-event overrides: severity and route.
Watch out: Error: unknown event
objectno default
live reload
Numbers behind disk, memory and certificate alerts.

Destination (each item of destinations)​

Option
Type / default
Reload
Description
namerequired
stringno default
live reload
1 to 40 lowercase letters, digits or dashes; unique. Cannot be renamed in the UI.
Watch out: Errors: use 1 to 40 lowercase letters, digits or dashes; another destination has this name
booldefault true
live reload
A disabled destination gets nothing.
kindrequired
webhook | discord | emailno default
live reload
Delivery method. The UI calls webhook "Generic".
setdefault critical, warning
live reload
Severities sent immediately. Needs at least one unless digest is on.
booldefault false
live reload
Receive Info alerts as the daily digest.
listdefault all
live reload
Only alerts of these categories.
Watch out: Error: unknown category
listdefault all
live reload
Only alerts for these stacks.
URL (secret)required
URLno default
live reload
Webhook or Discord URL, stored as _clouduler_alert_<name>_url and shown masked.
Watch out: Errors: a URL is required; must be an http or https URL; not a Discord webhook URL (https://discord.com/api/webhooks/...)
stringno default
live reload
Generic webhook only. Value of the Authorization header, stored as _clouduler_alert_<name>_auth and sent as a sensitive header.
embed | shortdefault embed
live reload
Discord: rich embed or one plain line.
stringno default
live reload
Discord: text used for a Ping. One of <@&roleId>, <@userId>, <@!userId>, @here, @everyone (ids are digits only).
Watch out: A Ping without a mention pings nobody. Normal and Silent messages never mention.
ping | normal | silentdefault ping
live reload
Discord: how a critical alert announces itself.
ping | normal | silentdefault normal
live reload
Discord: how a warning announces itself.
ping | normal | silentdefault silent
live reload
Discord: how an info alert announces itself. Digests use this level.
ping | normal | silentdefault silent
live reload
Discord: how a resolved message announces itself.
booldefault true
live reload
Discord: link buttons.
Watch out: Digests get the "Open alerts" button only when `buttons` is on.
booldefault false
live reload
Discord: edit the original message when resolved instead of posting a new one.
Watch out: Discord drops the username when editing. If the original was deleted, a new message is posted.
stringno default
live reload
Discord: display name the webhook posts under.
torequired
list of addressesno default
live reload
Email: recipients, at least one.
Watch out: Errors: add at least one recipient; not an email address

SMTP server (smtp)​

Option
Type / default
Reload
Description
smtp.hostrequired
stringno default
live reload
SMTP server.
integerdefault 587
live reload
SMTP port, 0 to 65535.
Watch out: The UI fills 587 for STARTTLS, 465 for TLS and 25 for None. A port typed by hand wins.
starttls | tls | nonedefault starttls
live reload
Transport security.
stringno default
live reload
Login name. Credentials are used only when this is set.
smtp.fromrequired
addressno default
live reload
Sender address.
stringno default
live reload
Stored sealed as _clouduler_alert_smtp_password. Write-only.

Routing override (events.<key>) and thresholds​

Option
Type / default
Reload
Description
info | warning | criticaldefault catalog default
live reload
Overrides the event severity.
muted | in_app | {destinations: [...]}no default
live reload
muted drops the event; in_app records it without sending; a list sends to exactly those destinations.
Watch out: Error: no destination named NAME
integer 0-100default 90
live reload
Drive usage that raises node.disk_low as a warning.
integer 0-100default 95
live reload
Drive usage that makes it critical.
integer 0-100default 92
live reload
Memory use that starts the pressure timer.
durationdefault 10m
live reload
How long memory must stay above memory_percent.
integerdefault 14
live reload
Days before expiry for cert.expiring as a warning.
integerdefault 7
live reload
Days before expiry for cert.expiring as critical.

Stack alerts (x-clouduler.alerts)​

Take effect when the stack is deployed. x-clouduler.alerts: false turns all alerts off for the stack.

Option
Type / default
Reload
Description
booldefault true
needs restart
Alerts on or off for the stack (takes effect on deploy).
listdefault empty
needs restart
Event keys or prefix.* patterns this stack should not raise.
Watch out: Error: x-clouduler.alerts.<field>: <key> is not an alert event (see docs/alerts.md). The path in that message is out of date.
mapdefault empty
needs restart
Per-event severity for this stack. An exact key wins over the longest matching prefix.
list of rulesdefault empty
needs restart
Log-line rules, up to 32. Stack-level block only.
Watch out: Error: at most 32 log rules; only allowed on the stack-level alerts block
stringno default
needs restart
Exactly one of the two; up to 512 characters.
Watch out: Errors: set exactly one of match and regex; the pattern is empty
booldefault false
needs restart
Case-insensitive matching.
info | warning | criticaldefault warning
needs restart
Severity of the alert.
stringdefault <service> logged "<pattern>"
needs restart
Title of the alert.
durationdefault 5m
needs restart
Minimum time between alerts for one rule and container.
Watch out: Error: at least 1s
both | stdout | stderrdefault both
needs restart
Which output stream is watched.
listdefault all
needs restart
Limit the rule to these services.

Quirks and gotchas​