7.3 KiB
Alerting
Grafana's built-in unified alerting ties directly into the Prometheus and Loki datasources already running in the Monitoring Stack — no separate Alertmanager container needed. All rules, contact points, and notification policies are provisioned as code in grafana/provisioning/alerting/, so they survive container rebuilds and are version-controlled alongside everything else in this repo.
Architecture
Prometheus ──┐
├──► Grafana Unified Alerting ──► Gmail (SMTP)
Loki ────────┘
Grafana evaluates alert rules against its configured datasources (Prometheus for metrics, Loki for logs). When a rule fires, the notification policy routes it to a contact point, which delivers the notification via the configured channel (email over SMTP in this setup).
No separate Alertmanager instance is required — Grafana handles the full pipeline internally.
Alert rules
All rules live in docker-compose/monitoring-grafana-promethues-cadvisor-node-exporter/grafana/provisioning/alerting/rules.yaml under the Infra folder:
| Rule | UID | Source | Condition | Severity | Duration |
|---|---|---|---|---|---|
| Node exporter target down | node-down |
Prometheus | up{job="node_exporter"} < 1 |
critical |
2 min |
| High CPU usage | high-cpu |
Prometheus | 100 - (avg idle rate) > 85% |
warning |
5 min |
| Error log rate spike | loki-error-spike |
Loki | count_over_time(… |= "error" [5m]) > 20 |
warning |
5 min |
Node exporter target down (node-down)
The most critical alert — if the node exporter scrapes as down for 2 minutes, the host itself may be unreachable or the exporter process crashed. Fires at critical severity because it means all host-level visibility is lost.
PromQL:
up{job="node_exporter"}
Threshold: < 1 (target is down)
High CPU usage (high-cpu)
Watches for sustained CPU usage above 85% across the 5-minute window. The query uses rate(node_cpu_seconds_total{mode="idle"}[5m]) to smooth out momentary spikes — a brief compile or Docker pull won't trigger it, but a runaway process or resource-hungry container will.
PromQL:
100 - (avg by(instance)(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
Threshold: > 85%
Error log rate spike (loki-error-spike)
Queries Loki for log lines containing "error" across all jobs. If the count exceeds 20 within a 5-minute window and stays elevated for 5 minutes, it fires. This catches application-level issues that metrics alone won't surface — a service that's technically "up" but flooding its logs with errors.
LogQL:
sum(count_over_time({job=~".+"} |= "error" [5m])) or vector(0)
Threshold: > 20 error lines in 5 minutes
Contact points
Defined in grafana/provisioning/alerting/contactpoints.yaml:
| Name | Type | Channel |
|---|---|---|
gmail-alerts |
SMTP via Gmail App Password |
The email address is set to a placeholder (YOUR_EMAIL@example.com) in the provisioned file — update it to your actual address after the first Grafana container starts, or edit it directly in the file before starting.
Note: Grafana provisioning files do not expand Docker environment variables (
${...}syntax). The email address must be set as a literal string in the YAML.
Notification policy
Defined in grafana/provisioning/alerting/policies.yaml:
| Setting | Value | Meaning |
|---|---|---|
| Receiver | gmail-alerts |
All alerts route to the email contact point |
| Group by | alertname, instance |
Alerts with the same name and instance are grouped into a single email |
| Group wait | 30s | How long to wait before sending the first notification for a new group |
| Group interval | 5 min | Minimum time between notifications for the same group |
| Repeat interval | 4 hours | How often to re-notify about an ongoing (unresolved) alert |
This means a firing alert sends its first email within ~30 seconds, groups follow-ups every 5 minutes, and you get a reminder every 4 hours until the alert resolves.
SMTP configuration
Grafana sends alert emails through Gmail's SMTP relay. The configuration lives in the grafana service's environment block in docker-compose.yml and pulls credentials from the .env file:
GF_SMTP_ENABLED=true
GF_SMTP_HOST=smtp.gmail.com:587
GF_SMTP_USER=${GMAIL_SMTP_USER}
GF_SMTP_PASSWORD=${GMAIL_APP_PASSWORD}
GF_SMTP_FROM_ADDRESS=${GMAIL_SMTP_USER}
GF_SMTP_FROM_NAME=Grafana Alerts
Setting up Gmail SMTP
- Enable 2-Step Verification on your Google account
- Generate an App Password (requires 2FA)
- Add both values to
.env:GMAIL_SMTP_USER=your-email@gmail.com ALERT_EMAIL=your-email@gmail.com - Set the same email as your Grafana login (
GF_SECURITY_ADMIN_USERin.env, or through the Grafana UI)
How to add new alert rules
Rules are provisioned from YAML — no UI clicks needed:
- Open
grafana/provisioning/alerting/rules.yaml - Add a new entry under the
ruleslist in theinfra-alertsgroup - Give it a unique
uid, a descriptivetitle, and set thedatasourceUidto match your Prometheus (efck3im1w4pvkf) or Loki (P8E80F9AEF21F6940) datasource - Set the
conditionrefId to point to the threshold expression - Set
forto how long the condition must be true before firing - Add
labels.severity(critical,warning,info) and anannotations.summarymessage - Restart the Grafana container:
docker compose restart grafana
Common PromQL examples for new rules
# Memory usage above 90%
100 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes * 100)
# Disk usage above 85%
100 - (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"} * 100)
# Container restart count (from cAdvisor)
increase(container_restart_count[5m])
# Container memory above limit
container_memory_working_set_bytes / container_spec_memory_limit_bytes
How to add new notification channels
Grafana supports Slack, Discord, PagerDuty, webhooks, and more — all configured through provisioning files:
- Add a new receiver in
grafana/provisioning/alerting/contactpoints.yaml - Add the channel to the notification policy in
policies.yaml, or create a new policy that matches specific alert labels - Restart Grafana
Lessons learned
- Provisioning files don't expand env vars. Unlike Docker Compose, Grafana's provisioning YAML is read as-is —
${VARIABLE}syntax is treated as literal text. Email addresses and other non-secret values must be hardcoded in the provisioning files or managed through the Grafana UI after first boot. - Promtail is dead. The
forduration andgroup_intervalin alerting policies interact with log pipeline latency — if using an older Promtail-based setup, log alerts might fire with stale data. Alloy ships logs faster and more reliably, which makes log-based alerts more responsive. See Monitoring Stack for the migration details. - Alert fatigue is real on small setups. Three well-tuned rules cover the important failure modes without spamming. Adding too many rules to a single-node homelab generates noise faster than signal — each new rule should answer the question "would I actually get up at 3 AM for this?"