Metrics with Prometheus - ptweezy/cronstable GitHub Wiki
Metrics with Prometheus
cronstable exposes Prometheus metrics at a built-in GET /metrics endpoint on the HTTP web API. This is the pull-side sibling of the per-job statsd push metrics. On demand, a Prometheus server scrapes cumulative counters (run outcomes, durations, retries), live gauges (running instances, next scheduled run, last-run outcome), and the full cluster health surface. The exposition is generated by cronstable itself: no exporter sidecar and no extra dependency, so it works on every architecture and platform cronstable ships for, including Windows.
The endpoint serves both exposition formats: the classic Prometheus text format (text/plain; version=0.0.4) by default, and OpenMetrics 1.0 (application/openmetrics-text) when the scraper advertises it in its Accept header, as modern Prometheus servers do.
Enabling the endpoint
The endpoint is on by default whenever the web API is enabled. A web.listen block is enough:
web:
listen:
- http://127.0.0.1:8080
$ curl http://127.0.0.1:8080/metrics
# HELP cronstable_info cronstable build information.
# TYPE cronstable_info gauge
cronstable_info{version="1.2.60"} 1
...
The web.metrics option tunes or disables it. It accepts a boolean shorthand or a map:
web:
listen:
- http://127.0.0.1:8080
# metrics: false # shorthand: disable the endpoint entirely
metrics:
enabled: true # default
public: false # default; see Authentication below
durationBuckets: # histogram bounds override (seconds)
- 1
- 10
- 60
- 600
Options
The schema for the block is Bool() | Map({Opt("enabled"): Bool(), Opt("public"): Bool(), Opt("durationBuckets"): Seq(Float())}). Every key is optional.
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Serve GET /metrics. metrics: false / metrics: true are shorthands for {enabled: ...}. |
public |
bool | false |
Exempt /metrics (and only /metrics) from web.authToken bearer-token authentication, for scrapers that cannot send credentials. |
durationBuckets |
list of float | 0.1, 0.5, 1, 5, 15, 60, 300, 900, 3600 |
Upper bounds (seconds) of the cronstable_job_duration_seconds histogram. Must be finite, positive, and strictly increasing. A +Inf bucket is always appended automatically. Changing the bounds on a reload restarts the histograms from zero (an ordinary counter reset to Prometheus). |
Scraping and authentication
When web.authToken is configured, /metrics requires the bearer token like every other data endpoint. Prometheus supports this directly in its scrape configuration:
scrape_configs:
- job_name: cronstable
static_configs:
- targets: ["cron-host:8080"]
authorization:
type: Bearer
credentials: "the-secret-token"
If your scraper cannot send credentials, set web.metrics.public: true to exempt /metrics from authentication while everything else stays gated. The metrics reveal job names, schedules, and outcomes, but never command lines, output, or secrets. Treat public accordingly.
Two response details:
web.headersare stamped onto the response as usual, exceptContent-Type: the exposition format's contract always wins.- Content negotiation is on the
Acceptheader. Any request advertisingapplication/openmetrics-textgets OpenMetrics, which ends with# EOF. Everything else gets the text format.
Metric reference
All metrics are prefixed cronstable_. Per-job metrics carry the job name in a job_name label, not job, which would collide with Prometheus's own target label. Counters are cumulative since process start. They reset when the daemon restarts, which Prometheus's rate()/increase() handle as built-in behavior. With a durable state store configured, the per-job counters are re-seeded across restarts; see semantics and guarantees.
Daemon
| Metric | Type | Description |
|---|---|---|
cronstable_info{version} |
gauge (info) | Build information; always 1. |
cronstable_start_time_seconds |
gauge | Unix time this cronstable process started. |
cronstable_job_set_info{job_set_id} |
gauge (info) | The job-set fingerprint of the currently loaded configuration; always 1. Compare across a fleet to spot configuration drift. |
cronstable_jobs{state} |
gauge | Number of configured jobs, state="enabled" / "disabled". |
cronstable_config_last_reload_successful |
gauge | 1 if the most recent configuration parse succeeded, 0 if the configuration on disk is currently broken (the daemon keeps running the previous good one). Absent until the first parse. |
cronstable_config_last_reload_success_timestamp_seconds |
gauge | Unix time of the last successful configuration parse. |
Per job
| Metric | Type | Description |
|---|---|---|
cronstable_job_runs_total{job_name, status} |
counter | Finished runs by outcome: status="success" / "failure" / "cancelled" / "skipped", exactly as recorded in the run history. skipped counts the synthetic ledger rows a paused job writes in place of a launch. Those rows stamp neither last-outcome timestamp. Zero-filled for every configured job from the first scrape. Runs replaced under concurrencyPolicy: Replace are not runs and are not counted, matching the dashboard. |
cronstable_job_duration_seconds |
histogram | Duration of finished runs (bucket bounds from durationBuckets). Cancelled-without-starting records carry no duration and are not observed. |
cronstable_job_retries_total{job_name} |
counter | Retry attempts actually launched (onFailure.retry). Deferred or abandoned retries are not counted. |
cronstable_job_permanent_failures_total{job_name} |
counter | Failures with no retry remaining: exactly when onPermanentFailure reporting fires. |
cronstable_job_start_failures_total{job_name} |
counter | Runs whose command could not be launched at all, also recorded as failures with the conventional exit code 127. |
cronstable_job_last_success_timestamp_seconds{job_name} |
gauge | Unix time of the last successful finish. Absent until the first success. |
cronstable_job_last_failure_timestamp_seconds{job_name} |
gauge | Unix time of the last failed finish. Absent until the first failure. |
cronstable_job_info{job_name, schedule, cluster_policy} |
gauge (info) | Static configuration facts; always 1. |
cronstable_job_enabled{job_name} |
gauge | 1 if the job is enabled in the loaded configuration. |
cronstable_job_running{job_name} |
gauge | Number of currently running instances (can exceed 1 under concurrencyPolicy: Allow). |
cronstable_job_next_run_timestamp_seconds{job_name} |
gauge | Unix time of the next scheduled run. Absent for disabled jobs and @reboot schedules. |
cronstable_job_last_run_timestamp_seconds{job_name} |
gauge | Unix time the most recent run finished. |
cronstable_job_last_run_duration_seconds{job_name} |
gauge | Duration of the most recent finished run. |
cronstable_job_last_run_exit_code{job_name} |
gauge | Exit code of the most recent finished run. |
cronstable_job_last_run_success{job_name} |
gauge | 1 if the most recent finished run succeeded, else 0 (a cancelled run counts as 0). |
cronstable_job_cpu_seconds_total{job_name, mode} |
counter | CPU time consumed by monitorResources runs, mode="user" / "system", summed over the job's monitored runs. Emitted only after the job has a monitored run (an unmonitored job exports no CPU series). |
cronstable_job_peak_rss_bytes{job_name} |
gauge | Highest resident-set size (bytes) observed across all the job's monitored runs. Absent until the first monitored run. |
cronstable_job_last_run_cpu_seconds{job_name} |
gauge | Total CPU time of the most recent monitored run. Absent unless that run had monitorResources on. |
cronstable_job_last_run_max_rss_bytes{job_name} |
gauge | Peak resident-set size (bytes) of the most recent monitored run. Absent unless that run had monitorResources on. |
cronstable_job_paused{job_name} |
gauge | 1 while the job is paused at runtime, else 0. |
cronstable_job_late{job_name, check} |
gauge | 1 while the job's SLA check is breached, per check (maxTimeSinceSuccess / lateAfter / maxRuntime). Emitted from the monitor's first evaluation, only for checks the job configures in its sla: block. Paused jobs are exempt. |
cronstable_job_sla_breaches_total{job_name, check} |
counter | SLA breaches latched for the job, per check. Increments once per breach, not per evaluation. Emitted alongside cronstable_job_late, zero-filled from the first evaluation. |
The four preceding resource families require the job to opt into
monitorResources. They are sampled and
best-effort: see resource monitoring for the sampling
model and every other surface the numbers reach. Like the run counters,
cronstable_job_cpu_seconds_total and cronstable_job_peak_rss_bytes are made
restart-durable by a state: store (see
semantics and guarantees).
State backend
Emitted when a durable state store is configured (a state: section). Without one, cronstable is stateless and these families are absent. They observe the store itself. The preceding job families are unchanged, except that the store makes the per-job counters restart-durable (see semantics and guarantees). Families and label values appear as they first become relevant, so a store that never contends a lock or is never throttled does not export frozen zero series forever.
| Metric | Type | Description |
|---|---|---|
cronstable_state_info{backend, topology} |
gauge (info) | Static store facts: the backend implementation (filesystem) and the effective topology (single-node / shared, after topology: auto resolves); always 1. |
cronstable_state_ops_total{op} |
counter | Store operations by kind, for example op="append" / "list" / "derive-max" / "prune" / "lease-acquire" / "lease-renew" / "lease-release" / "lease-read" / "start" / "gc". Each op value appears after that operation has first run. |
cronstable_state_op_errors_total{op} |
counter | Store operations that raised, same op label. |
cronstable_state_op_seconds_total{op} |
counter | Seconds spent inside store operations. For mean in-store latency per operation, divide by cronstable_state_ops_total, for example rate() over rate(). |
cronstable_state_lock_acquisitions_total |
counter | Advisory-lock acquisitions by the store. Emitted after it goes nonzero. |
cronstable_state_lock_wait_seconds_total |
counter | Seconds spent waiting to acquire the store's advisory locks: the lock-contention signal. Emitted alongside cronstable_state_lock_acquisitions_total. |
cronstable_state_throttled_ops_total |
counter | Store operations delayed by the state.maxOpsPerSecond token bucket. Emitted after it goes nonzero. |
cronstable_state_throttle_wait_seconds_total |
counter | Seconds those operations spent queued behind the rate limit. Emitted alongside cronstable_state_throttled_ops_total. |
cronstable_state_dropped_writes_total{kind} |
counter | Durable writes that failed and were dropped (with a logged warning), by kind="run-record" / "checkpoint" / "retry" / "reboot-marker" / "counters" / "manifest" / "inflight" / "pause" / "overflow" (a write shed because 8192 were already pending). Writes are fire-and-forget under either onStoreUnavailable policy. Emitted after it goes nonzero: the single most alertable state signal. |
cronstable_state_workers_inflight{lane} |
gauge | State worker threads in flight per lane, lane="bulk" / "lease". Sustained at capacity signals a slow or wedged mount. |
cronstable_state_workers_capacity{lane} |
gauge | Worker-thread concurrency cap per lane. |
cronstable_state_workers_peak{lane} |
gauge | Highest number of worker threads in flight per lane. |
If reading the store's stats fails during a scrape, the state families are omitted from that scrape, with the error logged, rather than the scrape failing. The following cluster block follows the same degradation contract. cronstable_state_dropped_writes_total is the exception: the scheduler accumulates it as it drops writes, and it is not read from the store, so it keeps being served even when the store itself cannot be read.
Cluster
cronstable_cluster_enabled is always present (0 with no cluster section or a failed backend). The rest of the block appears while a cluster backend runs. The values mirror the pre-derived fields of GET /cluster, so the same alert rules documented there apply.
| Metric | Type | Description |
|---|---|---|
cronstable_cluster_enabled |
gauge | 1 when a leadership backend is running on this node (0 also covers a configured backend that failed to start). |
cronstable_cluster_info{backend, node_name, distribution} |
gauge (info) | Backend (gossip/kubernetes/etcd/filesystem), this node's name, and the ownership distribution; always 1. |
cronstable_cluster_size / cronstable_cluster_quorum |
gauge | Effective cluster size N and the quorum threshold (lease backends report 1/1). |
cronstable_cluster_quorate |
gauge | Whether this node is part of a quorum: the single most alertable cluster signal. |
cronstable_cluster_is_leader |
gauge | Whether this node holds scheduled-job leadership. Always 0 under distribution: spread (ownership is per job there), mirroring GET /cluster. |
cronstable_cluster_leader_info{leader} |
gauge (info) | The observed leader's name; absent when there is none. |
cronstable_cluster_conflict{kind} |
gauge | The three fail-closed conflict gates, kind="nodename" / "size" / "policy". Any 1 means this node's Leader jobs are standing down. |
cronstable_cluster_peers{status} |
gauge | Gossip backend only: configured peers by observed status (agreed, syncing, drifted, unreachable, untrusted, conflict, self, unknown), zero-filled so alert series never vanish. |
cronstable_cluster_leader_transitions_total |
counter | Times this node acquired or lost leadership. Observed at scheduler cadence (once per minute), so a flap shorter than a scheduler tick may be missed. Emitted only when electLeader is on (an observe-only cluster has no leadership to transition). |
cronstable_cluster_quorum_transitions_total |
counter | Times this node joined or left quorum (same cadence and electLeader caveats). |
If a backend read fails during a scrape, the cluster block degrades to cronstable_cluster_enabled and the transition counters (with the error logged by the prometheus logger) rather than the whole scrape failing.
Semantics and guarantees
-
Metrics never affect scheduling. Every hook is a synchronous in-memory increment on the scheduler's event loop. There is nothing to time out, retry, or crash on. This is the same contract the statsd writer has, without the UDP.
-
Counters agree with the API.
cronstable_job_runs_totalincrements at the exact point a run enters the run history (GET /jobs/{name}/runs), so the two surfaces can never disagree on outcomes. -
Counters survive reloads. With durable state, they survive restarts. The accumulators live on the daemon, not the web app, so even a configuration reload that restarts the web server or the cluster manager keeps them. Without a
state:section, a process restart resets them, which is normal for Prometheus counters. With a durable state store configured, the per-job counters are snapshotted durably and seeded back on boot for the jobs still in the configuration:- Counters covered: runs by outcome, retries, permanent and start failures, the duration histogram, the last success/failure timestamps, and the SLA breach counters.
- Cadence: at most one write per 15 seconds while running, plus a final snapshot on clean shutdown.
- Histograms: seeded only if the
durationBucketsbounds are unchanged.
A restart then reads as, at worst, a small ordinary counter reset covering the events since the last snapshot, and a crash forfeits at most those last few seconds.
cronstable_start_time_secondsstill resets per process by design: that is how you see the restart itself. -
Removed jobs disappear. On every successful reload, the daemon drops series for jobs no longer in the configuration. A renamed job starts over at zero.
-
@rebootkeep-alives look "always running". A long-running@rebootjob showscronstable_job_running 1with zero finished runs. That is the correct reading, not missing data.
Example alerts
groups:
- name: cronstable
rules:
# a job's most recent run failed
- alert: CronstableJobFailed
expr: cronstable_job_last_run_success == 0
for: 5m
# a job that should run hourly has not succeeded for 2 hours
- alert: CronstableJobStale
expr: time() - cronstable_job_last_success_timestamp_seconds > 7200
# the config file on disk is broken; the daemon is running stale config
- alert: CronstableConfigBroken
expr: cronstable_config_last_reload_successful == 0
for: 10m
# this node has lost quorum (gossip) or cannot reach the lease store;
# its Leader jobs are standing down
- alert: CronstableClusterNotQuorate
expr: cronstable_cluster_quorate == 0
for: 5m
# more than one node believes it is the leader (split-brain)
# (inert under distribution: spread; compare clusterOwner instead)
- alert: CronstableMultipleLeaders
expr: sum(cronstable_cluster_is_leader) > 1
The clustering monitoring guide discusses the cluster signals in depth.
The preceding staleness rule (CronstableJobStale) is the external backstop to pair with per-job late-run detection. The in-process sla: monitor gives per-job thresholds and rich onLate notifications, but it stops with the daemon. A Prometheus rule on time() - cronstable_job_last_success_timestamp_seconds, plus up == 0 on the scrape itself, still fires when the daemon, its host, or its network is gone.
See also
- HTTP Control API: the web server
/metricsis part of: listeners,authToken, headers, and reload behavior. - Metrics with statsd: the push-side alternative; both can be enabled at once.
- Resource Monitoring: the
monitorResourcessampling behind the per-job CPU/RSS families. - Clustering and Leader Election: what the cluster gauges mean and when they change.
- Failure Detection and Retries: how run outcomes, retries, and permanent failures are determined.
- Late-Run Detection: the
sla:monitor behindcronstable_job_lateand the breach counter. - Pausing Jobs: the runtime pause behind
cronstable_job_pausedand theskippedrun outcome. - Configuration Reference: the full
websection.