Self-hosting / Prometheus metrics
Prometheus metrics
Scrape the Prometheus metrics of the application and the workers, import the Grafana dashboard and load the ready-made alerting rules.
Uptime Kotlin watches your services, but something has to watch Uptime Kotlin. The application and the workers publish Prometheus metrics: checks run, queues, worker heartbeats, notifications, memory. The repository ships a Grafana dashboard and alerting rules built on them.
Where the metrics are served
The metrics are served at /q/metrics on a separate management port, never on the port of the web interface:
| Image | Management port | Endpoint |
|---|---|---|
ghcr.io/theomeunier/uptime-kotlin |
9000 |
:9000/q/metrics |
ghcr.io/theomeunier/uptime-kotlin/worker |
9002 |
:9002/q/metrics |
The endpoint has no authentication. Do not publish these ports on the internet: let Prometheus reach them through the Docker network, as below, rather than with a ports entry.
Scrape the metrics with Prometheus
Run Prometheus in the same Compose project as Uptime Kotlin, so it reaches the containers by name:
prometheus:
image: prom/prometheus:v3.13.4
container_name: uptime_kotlin_prometheus
restart: unless-stopped
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./alerts.yml:/etc/prometheus/alerts.yml:ro
- prometheus-data:/prometheusAdd prometheus-data: under the top-level volumes key, then create prometheus.yml next to compose.yaml:
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- /etc/prometheus/alerts.yml
scrape_configs:
- job_name: uptime-kotlin-api
metrics_path: /q/metrics
static_configs:
- targets: ['uptime-kotlin:9000']
labels:
role: api
- job_name: uptime-kotlin-worker
metrics_path: /q/metrics
static_configs:
- targets: ['uptime-kotlin-worker:9002']
labels:
role: workerKeep the job names starting with uptime-kotlin and the role label: the dashboard and the alerts select the targets with job=~"uptime-kotlin.*". Without workers, drop the second job. In cluster mode, list every worker under targets, and every replica of the application under the first job.
Import the Grafana dashboard
The dashboard lives in the repository, in deploy/monitoring/grafana/dashboards/uptime-kotlin.json. In Grafana, open Dashboards, then New and Import, upload the file and pick your Prometheus data source.
It is organised in rows:
- Overview: the firing alerts and a status tile for each concern, whether the checks run on schedule, the check queue lag, the workers, the dead-lettered tasks and the failed notifications.
- Probe Checks: checks per minute by result and by protocol, check duration, probes by state.
- Workers and queues: heartbeat age of every worker, worker saturation, queue lag and content.
- Instances · CPU and memory: one line per process, with its memory against its container limit.
- Runtime details, Notifications and API: heap, threads, delivery attempts by channel, request rate and latency of the API.
The Region, Instance and Protocol filters at the top narrow the panels. Two settings of the dashboard are constants: Prometheus URL and Uptime Kotlin URL, used by the links to the alerts and to the application. Set them to your own addresses in the dashboard settings, under Variables.
Alerting rules
deploy/monitoring/alerts.yml holds ready-made rules. Mount it in Prometheus as above: the alerts appear in the Alerts tab of Prometheus and on the dashboard. To receive them by e-mail, Slack or PagerDuty, add an Alertmanager.
| Alert | Severity | Fires when |
|---|---|---|
UptimeKotlinInstanceDown |
critical | An instance does not answer the scrape for 2 minutes. |
UptimeKotlinNoActiveWorker |
critical | No instance has run checks for 30 seconds, for 2 minutes: nothing is monitored. |
UptimeKotlinWorkerSilent |
critical | A worker has sent no heartbeat for more than 60 seconds. |
UptimeKotlinProbeChecksLagging |
critical | Due checks wait more than 60 seconds in the queue, for 5 minutes. |
UptimeKotlinProbeChecksMissed |
warning | More than 10% of the checks the enabled monitors require are not run, for 15 minutes. |
UptimeKotlinNotificationsLagging |
warning | Notifications go out more than 2 minutes late, for 5 minutes. |
UptimeKotlinDeadLetters |
warning | New tasks reached their maximum delivery attempts in the last 15 minutes and will not be retried. |
UptimeKotlinNotificationFailures |
warning | More than 3 deliveries failed on one channel in 15 minutes: check its token or webhook URL. |
UptimeKotlinProbeCheckErrors |
warning | A check threw an error instead of producing a result: see the logs of the workers. |
UptimeKotlinMemoryNearLimit |
warning | A process uses more than 90% of its container memory limit for 10 minutes. |
When checks lag, the workers are often saturated: compare uptime_probe_worker_active with uptime_probe_worker_concurrency, then raise SCHEDULER_WORKER_CONCURRENCY (see Configuration) or add a worker.
A worker stopped on purpose keeps firing UptimeKotlinWorkerSilent until its heartbeat is purged, an hour later.
Metrics reference
Every instance reads the queue, probe and worker metrics from PostgreSQL, so they have the same value on every instance: aggregate them with max(), never sum(). The other metrics count what each process does itself and add up.
| Metric | Type | Labels | Description |
|---|---|---|---|
uptime_probe_checks_total |
counter | protocol, result |
Checks run by this instance. result is success, warning, failure, pause, or error when the check threw. |
uptime_probe_check_duration_seconds |
histogram | protocol |
Wall-clock duration of a check, timeout included. |
uptime_probe_worker_active |
gauge | region |
Checks running right now in this process. |
uptime_probe_worker_concurrency |
gauge | region |
Checks this process may run at once (SCHEDULER_WORKER_CONCURRENCY). |
uptime_probe_expected_checks_per_minute |
gauge | Checks per minute the enabled monitors should produce, retries excluded. Read from PostgreSQL. | |
uptime_probes |
gauge | state |
Monitors by state: success, warning, failure, pause, and disabled apart. Read from PostgreSQL. |
uptime_queue_tasks |
gauge | queue, status |
Tasks in the probe_checks and notifications queues, by status: pending, leased, dead. Read from PostgreSQL. |
uptime_queue_lag_seconds |
gauge | queue |
Age of the oldest due task still waiting to be claimed. Read from PostgreSQL. |
uptime_worker_heartbeat_age_seconds |
gauge | worker, region |
Seconds since the last heartbeat of each instance running checks. Read from PostgreSQL. |
uptime_notification_deliveries_total |
counter | channel, result |
Notification delivery attempts made by this instance. result is sent or failed. |
uptime_build_info |
gauge | version |
Always 1. The version label gives the version of the image this process runs. |
uptime_process_start_time_seconds |
gauge | Start time of the process, in seconds since the epoch. | |
process_resident_memory_bytes |
gauge | Memory used by the process. | |
process_memory_limit_bytes |
gauge | Memory limit of the container, for example mem_limit in Compose. Absent without a limit. |
The region label is the SCHEDULER_WORKER_NAME of the process, and worker adds its host name. The queue and probe gauges are refreshed every 30 seconds, the heartbeats every 15 seconds.
The processes also publish the standard metrics of the runtime and of the HTTP server, such as http_server_requests_seconds for the API, which the dashboard uses.
Try it on your machine
A clone of the repository starts Prometheus and Grafana already wired together, for development:
docker compose --profile monitoring up -dPrometheus answers on http://localhost:9090 and Grafana on http://localhost:3000 (admin / admin), with the Uptime Kotlin dashboard as home page. This setup scrapes an application started on the host, on ports 9000 and 9002: for a deployment, use the configuration above.
