CronGuard

SLO-style observability for Kubernetes CronJobs.
CronGuard is a small Kubernetes operator that wraps any existing batch/v1.CronJob with a CronJobMonitor custom resource. It watches child Jobs, tracks execution history, and exposes Prometheus metrics so you can alert on missed runs, duration overruns, and consecutive failures without hand-rolling PromQL against kube-state-metrics.
Why
Kubernetes CronJobs fail silently in several common ways: a skipped run under concurrencyPolicy: Forbid, a Job that completes with no succeeded pods, control-plane drift pushing starts minutes late. Teams keep re-inventing the same fragile PromQL:
time() - kube_cronjob_status_last_successful_time{cronjob="..."} > 86400
CronGuard replaces that with a declarative SLO per CronJob.
Quickstart
Helm (OCI)
helm install cronguard oci://ghcr.io/dmazhukov/charts/cronguard \
--version 0.3.3 \
--namespace cronguard-system --create-namespace
Helm (GitHub Pages)
helm repo add cronguard https://dmazhukov.github.io/cronguard/
helm repo update
helm install cronguard cronguard/cronguard --version 0.3.3 \
--namespace cronguard-system --create-namespace
Raw manifests
kubectl apply -f https://github.com/dmazhukov/cronguard/releases/download/v0.3.3/install.yaml
Apply a sample monitor:
kubectl apply -f config/samples/cronjob_example.yaml
kubectl apply -f config/samples/monitoring_v1alpha1_cronjobmonitor.yaml
kubectl get cronjobmonitors
# NAME SCHEDULE LASTSUCCESS CONSECFAILS MISSED READY AGE
# nightly-settlement 0 2 * * * 5m 0 0 True 10m
See docs/distribution.md for the full installation reference and all configuration knobs.
Example
apiVersion: monitoring.cronguard.io/v1alpha1
kind: CronJobMonitor
metadata:
name: nightly-settlement
spec:
cronJobRef:
name: nightly-settlement
schedule: "0 2 * * *"
timeZone: "Europe/Moscow" # optional; falls back to CronJob.spec.timeZone, then UTC
maxDurationSeconds: 1800 # SLO: finish within 30 minutes
maxConsecutiveFailures: 2 # alert after 2 failures in a row
alertAfterMissedRuns: 2 # alert after 2 missed starts
gracePeriodSeconds: 60
historyLimit: 10
Walkthrough

Recorded against a real kind cluster. Replay locally with:
asciinema play docs/cast/install.cast
Or open the .cast file directly.
Metrics
The per-CronJob gauge family is labelled {namespace, name, cronjob} (name is the CronJobMonitor; cronjob is the referenced CronJob):
| Metric |
Type |
Meaning |
cronguard_last_success_timestamp_seconds |
gauge |
Unix time of last successful Job |
cronguard_last_failure_timestamp_seconds |
gauge |
Unix time of last failed Job |
cronguard_last_schedule_timestamp_seconds |
gauge |
Unix time of last Job start (success or failure) |
cronguard_running_jobs |
gauge |
Currently-running Jobs owned by the watched CronJob |
cronguard_next_expected_timestamp_seconds |
gauge |
Unix time of next expected run |
cronguard_consecutive_failures |
gauge |
Consecutive failed runs |
cronguard_missed_runs |
gauge |
Consecutive missed runs |
cronguard_schedule_drift_seconds |
gauge |
Drift of most recent run |
cronguard_last_duration_seconds |
gauge |
Duration of last completed run |
cronguard_condition |
gauge |
1/0/-1 per condition type/reason (extra labels type, reason) |
Counters and operator self-metrics (different label sets):
| Metric |
Type |
Labels |
Meaning |
cronguard_missed_runs_total |
counter |
namespace, name |
Cumulative missed runs observed; drives the burn-rate alerts |
cronguard_reconcile_total |
counter |
namespace, name, result |
Reconcile invocations per outcome (success/error/requeue) |
cronguard_reconcile_duration_seconds |
histogram |
namespace, name |
Reconcile latency |
cronguard_build_info |
gauge |
version, commit, build_date |
Always 1; carries build metadata (also the operator-up signal) |
Example alerts (PromQL):
# No success in 25 hours for a daily CronJob
(time() - cronguard_last_success_timestamp_seconds) > 90000
# Execution SLO breached
cronguard_condition{type="ExecutionHealthy"} == 0
Dashboards
A pre-built Grafana dashboard ships under config/grafana/ — six panels covering the core CronGuard metrics. See the dashboards README for import instructions.
Architecture
User applies CronJobMonitor YAML
│
▼
apiserver ──► Reconciler ──► computes SLO ──► patches .status
▲ │
└──── Job events (informer) ─┘
/metrics scrape ──► Collector reads cache ──► emits cronguard_*
Roadmap
Shipped (v0.3.x, v0.4.0): CEL admission validation, burn-rate SLO alerts (cronguard_missed_runs_total), HA metrics deduplication (leader-only scrape), drift annotation re-stamping, bounded schedule math with proven DST semantics, the missed-run floor from CronJob.status (ADR 0001), attested release artifacts. See CHANGELOG.md for details.
Shipped (v0.2.x): operator core, CRD with timezone-aware schedules (spec.timeZone with fallback to CronJob.spec.timeZone), envtest suite, raw kubectl apply manifests, Prometheus metrics, Helm chart (OCI + GitHub Pages), Grafana dashboard, default PrometheusRule, ServiceMonitor, kind-based e2e, Artifact Hub listing.
v0.5 — Data-Freshness SLO (planned):
spec.freshness probe — a PromQL query that returns the timestamp of the last downstream data update, and a window. A job can exit 0 while writing nothing; this axis asks whether the data moved, not whether the Job ran. Surfaces as a DataFresh condition, a cronguard_data_freshness_seconds gauge and a CronGuardDataStale alert. It is the first outbound call the operator makes (to Prometheus), so it ships behind an ADR and is off unless a monitor asks for it.
Later, gated on adoption signal:
ClusterCronJobMonitor resource — cluster-scoped CRD that selects CronJobs by label/regex and applies a SLO template to all of them. Per-CronJob CronJobMonitor resources still override the cluster template when present.
- API promotion v1alpha1 → v1beta1 — multi-version serving with
v1beta1 as the storage version, after the spec fields above settle.
Considered, not yet planned:
- Operator self-observability metrics (reconcile queue depth, leader-election state, status-patch conflicts)
- Backfill of execution history from existing Jobs at first install
- OLM bundle on OperatorHub (deferred until real OpenShift demand)
- CNCF Sandbox application (gated on real-user adoption)
Development
# Run unit + envtest
make test
# Run the manager against your current kube-context
make run
# Build a local Docker image
make docker-build IMG=cronguard:dev
Contributing
See CONTRIBUTING.md.
Security
See SECURITY.md.
License
Apache 2.0 — see LICENSE.