Skip to content
cronpad
Observability / Logging

Logging

cronpad writes structured logs to standard output, in JSON (default) or logfmt. Every line about a run automatically carries the job ID, run ID, occurrence ID, attempt, workspace and trace ID, however deep in the code it was written, so you can filter one job’s or one run’s entire story with a single query. Secrets, tokens and authorization headers are redacted.

Format

[logging]
format = "logfmt"   # or "json"
level = "info"      # debug | info | warn | error
add_source = false  # add source=file.go:123

The same fields are emitted in both formats, so switching never breaks your queries.

logfmt:

time=2026-11-10T03:30:00.118Z level=INFO msg="run finished" service=cronpad version=1.0.0 job_id=job_01J9ZK3Q run_id=run_01J9ZM1A occ="job_01J9ZK3Q|1794281400000|0|3|s" attempt=0 trigger=s workspace_id=ws_01J9X sink=webhook outcome=success status_code=200 duration_ms=104 start_lag_ms=6 sla_breached=false trace_id=4bf92f3577b34da6a3ce929d0e0e4736

json:

{"time":"2026-11-10T03:30:00.118Z","level":"INFO","msg":"run finished","service":"cronpad","version":"1.0.0","job_id":"job_01J9ZK3Q","run_id":"run_01J9ZM1A","occ":"job_01J9ZK3Q|1794281400000|0|3|s","attempt":0,"trigger":"s","workspace_id":"ws_01J9X","sink":"webhook","outcome":"success","status_code":200,"duration_ms":104,"start_lag_ms":6,"sla_breached":false,"trace_id":"4bf92f3577b34da6a3ce929d0e0e4736"}

Standard fields

ContextFields
Every linetime, level, msg, service, version; trace_id and span_id when inside a traced operation
Anything during a runjob_id, run_id, occ, attempt, trigger, scheduled_at, workspace_id, sink
Run finishedoutcome, status_code, duration_ms, start_lag_ms, sla_breached, error
API requestsrequest_id, method, route (the route pattern, e.g. /api/v1/jobs/{id}), status, duration_ms, workspace_id, actor

occ is the occurrence ID: it identifies one fire of one job, including the attempt, which is exactly what you want when tracing a duplicate delivery.

Useful queries

# Everything that happened to one run (logfmt + grep)
grep 'run_id=run_01J9ZM1A' cronpad.log

# Loki: failed attempts for one job
{app="cronpad"} | logfmt | job_id="job_01J9ZK3Q" and outcome!="success"

# jq: slowest starts
jq -c 'select(.msg=="run finished" and .start_lag_ms > 1000)' cronpad.json.log

What’s logged at each level

LevelContents
errorThings an operator must act on: database or queue failures, panics recovered in workers, failed alert deliveries after all retries
warnDegraded but handled: lease lost, queue entry repaired, queue data loss detected, dev login enabled, missing Litestream
infoOne line per finished run, one per API request, startup/shutdown, janitor passes, job state changes
debugClaim-loop iterations, retries scheduled, cache misses

At high volume, info is about three lines per run. Set level = "warn" to log only problems; run history is still recorded in the database.

Redaction

Values of fields named like authorization, cookie, token, secret, password, api_key or signature are replaced with [REDACTED]. Webhook and command configs are never logged whole; only a request summary (POST https://api.example.com/reports (231 bytes)). Secret values substituted from {{secret.NAME}} never appear in logs.

Shipping logs elsewhere

Logs go to stdout for your container runtime or journald to collect. To also send them over OTLP to an OpenTelemetry Collector, set telemetry.otlp_logs = true; the trace and run fields become OTLP log attributes.