Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Observability

cronoq tells you not only how busy it is, but how well it is scheduling: whether priorities are respected, whether fixed starts are on time, and how long work waits. This guide shows how to collect those signals; Metrics and quality signals explains how to read them.

Prometheus (server)

GET /metrics serves Prometheus text built from the scheduler snapshot at scrape time. On the main HTTP port it requires a token with the metrics (or admin) role:

scrape_configs:
  - job_name: cronoq
    authorization: { credentials_file: /etc/prometheus/cronoq-metrics-token }
    static_configs: [{ targets: ["cronoq:7071"] }]

If your scraper cannot send a token, set server.metrics_addr = "0.0.0.0:9090" and scrape that port instead; it serves /metrics, /healthz and /readyz without authentication or TLS, so keep it on an internal network.

Standbys answer 503; scrape every replica and the leader's series are the live ones. Every metric is listed in the metrics reference.

OpenTelemetry (library)

With the default otel feature, the cronoq crate records metrics through the globally installed OpenTelemetry meter provider and propagates trace context through the global text-map propagator. Configure both once at startup:

use opentelemetry::global;
use opentelemetry_sdk::{metrics::SdkMeterProvider, propagation::TraceContextPropagator};

global::set_text_map_propagator(TraceContextPropagator::new());
let meter_provider = SdkMeterProvider::builder()
    .with_periodic_exporter(opentelemetry_otlp::MetricExporter::builder().with_tonic().build()?)
    .build();
global::set_meter_provider(meter_provider);
// Build the scheduler after this, so its instruments use the provider.

Without a provider the instruments cost almost nothing. Disable the feature (default-features = false) to drop the OpenTelemetry dependencies entirely.

Traces

Each attempt runs inside a tracing span named cronoq.task with fields task.id, task.pool, task.kind and task.attempt. When a task is submitted inside a span and a propagator is installed, its W3C traceparent is stored as a tag; each attempt's span is then parented to the submitter's trace (with tracing-opentelemetry installed), so retries show up under the request that caused them. Remote workers receive the tag in lease.tags["traceparent"] and can continue the trace.

Snapshots and events

For dashboards inside your application, read snapshots directly:

snap = await sched.snapshot()
llm = snap.pool("llm")
print(llm.capacity, llm.busy, llm.wait_p99, llm.priority_adherence, snap.totals["dead_lettered"])

and subscribe to status events for audit logs or user notifications:

async with sched.events() as events:
    async for e in events:
        if e.status == "dead_lettered":
            await alert(e.id, e.message)

The server equivalents are GET /v1/snapshot / Admin.Snapshot and the Tasks.Watch stream.

Key alerts

AlertExpression (Prometheus)Why
Dead lettersincrease(cronoq_tasks_total{status="dead_lettered"}[10m]) > 0Tasks failing every retry.
Queue waitcronoq_pool_wait_seconds{quantile="0.99"} > 30Users or SLAs waiting.
Under-capacitycronoq_pool_desired_workers > cronoq_pool_workers for 15 mThe scaler is not delivering workers.
No workerssum by (pool) (cronoq_pool_pending) > 0 and on (pool) cronoq_pool_workers_connected == 0 for 5 mWork is queued and nothing can run it.
Fixed starts latecronoq_pool_fixed_start_hit_rate < 0.95Scheduled jobs starting late.
Sheddingincrease(cronoq_tasks_total{status="shed"}[5m]) > 0Overload is dropping work.
Lease expiriesincrease(cronoq_lease_expiries_total[10m]) > 0Workers are dying or not heartbeating.
No leadermax(probe_success{job="cronoq-readyz"}) == 0 (blackbox probe of /readyz on every replica)No replica is serving.

Logs

The server logs with tracing to stdout; set RUST_LOG (for example RUST_LOG=info,cronoq_server=debug). Notable events: listening addresses, leadership acquired or lost, worker registration and disconnection (with granted slots), store outages and retries, failed scale requests, and configuration warnings such as short tokens or a database URL without sslmode=verify-full.