Skip to content

Metrics and alerts

GET /metrics serves Prometheus metrics about every deploy. It follows the same rule as /status: with api_token_env set, it needs the Bearer token.

prometheus.yml
scrape_configs:
  - job_name: nimdeploy
    authorization:
      credentials_file: /etc/prometheus/nimdeploy.token   # the NIMDEPLOY_API_TOKEN value
    static_configs:
      - targets: ["127.0.0.1:9000"]

nimdeploy listens on localhost: scrape it from an agent on the same host (a local Prometheus or Grafana Alloy), or publish /metrics through your proxy only to your monitoring network.

Metrics

Metric Type
nimdeploy_build_info{version,goversion} gauge always 1
nimdeploy_start_time_seconds gauge when the service started
nimdeploy_deploys_total{deploy,status} counter finished runs: success, failed, skipped, interrupted
nimdeploy_deploy_duration_seconds{deploy} histogram duration of runs that ran (buckets 5 s … 1 h)
nimdeploy_deploy_running{deploy} gauge runs in progress
nimdeploy_deploy_queued{deploy} gauge runs waiting
nimdeploy_deploy_last_success{deploy} gauge 1 if the last run succeeded, 0 if it failed
nimdeploy_deploy_last_finished_timestamp_seconds{deploy} gauge
nimdeploy_deploy_last_success_timestamp_seconds{deploy} gauge
nimdeploy_deploy_last_failure_timestamp_seconds{deploy} gauge
nimdeploy_deploy_next_run_timestamp_seconds{deploy} gauge scheduled deploys
nimdeploy_webhook_requests_total{deploy,code} counter webhook answers: 202 accepted, 200 ignored/duplicate, 401 bad signature, 403 client certificate, 400 invalid, 409/503 busy
nimdeploy_deploy_info{deploy,<labels>} gauge always 1, with the deploy's labels: join on deploy
nimdeploy_queue_wait_seconds{deploy} histogram from the request to the run starting (0 when it started at once)
nimdeploy_phase_duration_seconds{deploy,phase} summary time in each phase: command, health, ci, before, after_success, after_failure, cloudflare
nimdeploy_ansible_hosts_total{deploy,result} counter Ansible hosts by outcome: ok, changed, unreachable, failed
nimdeploy_ansible_last_run_hosts{deploy,result} gauge the same for the last run
nimdeploy_runs_running gauge runs in progress across every deploy
nimdeploy_runs_queued gauge runs waiting across every deploy
nimdeploy_runs_oldest_queued_seconds gauge how long the oldest waiting run has waited: the service is saturated when it grows
nimdeploy_tls_cert_expiry_timestamp_seconds{cert} gauge expiry of each certificate in use: server, client_ca, hub_client, hub_ca, otel_client, otel_ca (also on the hub)
nimdeploy_otel_dropped_total counter spans and log records the OTLP endpoint couldn't take

No label holds a per-run value (commit, delivery, run id): those are in the logs and traces, where they don't multiply the series.

Counters start at zero when the service starts; Prometheus' rate() and increase() handle that.

Alerts

nimdeploy.rules.yml
groups:
  - name: nimdeploy
    rules:
      - alert: DeployFailing
        expr: nimdeploy_deploy_last_success == 0
        for: 5m
        annotations:
          summary: "{{ $labels.deploy }}: last deploy failed"

      - alert: DeploySlow
        expr: histogram_quantile(0.9, sum by (deploy, le) (rate(nimdeploy_deploy_duration_seconds_bucket[1d]))) > 900
        annotations:
          summary: "{{ $labels.deploy }}: deploys take over 15 min"

      - alert: DeployQueueGrowing
        expr: nimdeploy_deploy_queued > 10
        for: 15m
        annotations:
          summary: "{{ $labels.deploy }}: {{ $value }} runs waiting"

      - alert: ScheduledJobLate
        expr: time() - nimdeploy_deploy_last_success_timestamp_seconds{deploy="backup"} > 26 * 3600
        annotations:
          summary: "backup has not succeeded in over a day"

      - alert: WebhookSignatureFailures
        expr: increase(nimdeploy_webhook_requests_total{code="401"}[1h]) > 5
        annotations:
          summary: "{{ $labels.deploy }}: webhooks with a wrong secret (rotated? attack?)"

      - alert: AnsibleHostsFailing
        expr: nimdeploy_ansible_last_run_hosts{result=~"failed|unreachable"} > 0
        annotations:
          summary: "{{ $labels.deploy }}: {{ $value }} hosts {{ $labels.result }} in the last run"

      - alert: DeploysWaitingLong
        expr: histogram_quantile(0.9, rate(nimdeploy_queue_wait_seconds_bucket[1h])) > 600
        annotations:
          summary: "{{ $labels.deploy }}: runs wait over 10 minutes for their turn"

      - alert: CertificateExpiresSoon
        expr: nimdeploy_tls_cert_expiry_timestamp_seconds - time() < 14 * 86400
        annotations:
          summary: "{{ $labels.cert }} certificate expires in less than 14 days"

      - alert: DeploysSaturated
        expr: nimdeploy_runs_oldest_queued_seconds > 1800
        annotations:
          summary: "a run has been waiting over 30 minutes for its turn"

      - alert: NimdeployDown
        expr: up{job="nimdeploy"} == 0
        for: 5m

up == 0 covers nimdeploy itself; for "no deploys arriving at all" use the webhook counters, e.g. increase(nimdeploy_webhook_requests_total{code="202"}[7d]) == 0.