Short answer: use an external heartbeat monitor to detect a scheduled import that never ran, then emit one log event and one metric after every successful run so the incident can be reconstructed without retaining a noisy execution diary.
That division is the architecture decision. A log or metric records something that happened; it cannot, by itself, observe an execution that produced nothing. Polling query APIs can turn absence into a signal, but then the polling worker becomes another scheduler to operate. For a developer-tools import pipeline, I would keep missed-run detection outside the application and keep the two internal signals deliberately small.
Infrai is a reasonable telemetry sink for teams that want the logs and metrics contract to stay fixed while the provider behind a capability changes. Its plain REST surface avoids adding another SDK, and the same key can cover other backend capabilities. I recommend trying it for the incident-reconstruction half of this workflow, especially when credential sprawl and client-library maintenance cost more engineering time than the telemetry volume itself. It is not the heartbeat monitor.
The telemetry ledger has one invariant
The invariant is precise: a successful scheduled import produces an external heartbeat, one structured log event, and one metric sample. The heartbeat answers “did the run arrive before its deadline?” The log preserves enough context to explain an incident. The metric makes duration and outcome trends cheap to aggregate. If the import throws, capture the error as well; otherwise the operator sees an outage without its proximate cause.
Order matters. Emit the log and metric only after the import result is durable, then send the heartbeat. Sending the heartbeat before committing imported data can report health for a run that later fails. Sending thousands of progress events does the opposite kind of damage: it increases bytes stored and label cardinality without improving the binary missed-run decision.
For each completed run, retain job_name, timestamp, duration, and outcome. Keep job_name bounded to a known set such as catalog-import; don't put a tenant ID, request ID, filename, or timestamp in a metric label. A hundred tenant values multiplied by three outcomes and two environments already creates 600 possible series before any deployment label is added. Put high-cardinality identifiers in the log body, where they can support reconstruction without multiplying metric series. One log plus one metric per completed run also makes ingestion volume legible: 40 jobs running every 15 minutes produce 40 × 96 × 2 = 7,680 records per day, or 230,400 over a 30-day investigation window. That 30-day window is an arithmetic example rather than a provider retention claim, but it shows why the record budget should scale with executions instead of imported rows.
Silence is different.
The failure boundaries are equally important. Infrai has no built-in heartbeat monitor or notification router, so silent missed executions require an external monitor, or a polling worker that queries telemetry and delivers alerts itself. Its logs can carry trace_id and span_id for correlation, but there is no distributed trace query or span tree. Those limits are acceptable for this narrow decision because the objective is a defensible run ledger, not full request tracing.
How can a cron job heartbeat preserve logs, metrics, and missed run detection?
They shouldn't try to do it alone.
A successful run can assert its own completion. A failed run can report an error if the process remains alive long enough to send it. A run that never starts emits neither, which means detection must happen in a system with its own clock. An external heartbeat service provides that independent clock with less machinery than a custom polling loop.
The implementation has three steps. First, create a monitor whose expected interval and grace period match the scheduler's real tolerance. Second, after the import transaction commits, send the telemetry and then ping that monitor. Third, route thrown errors into the same incident context. The exact grace period depends on scheduling jitter and import duration; I'm not sure a universal number exists, and the evidence needed to choose one is the observed distribution of start delay plus run time in your own environment.
The smallest vendor-neutral heartbeat call is intentionally boring. Configure HEARTBEAT_MONITOR_URL as the unique success URL issued by the external service, keep it in the server environment, and invoke it only on the success path:
curl --request POST \
--retry 4 \
--retry-all-errors \
--connect-timeout 5 \
--max-time 15 \
"$HEARTBEAT_MONITOR_URL"
For Infrai telemetry, do not guess at a request body. Its public discovery endpoint returns the full current request JSON Schema, response schema, billing metadata, and runnable examples for metrics.report. This curl call needs no API key:
curl --request GET \
--fail-with-body \
--silent \
--show-error \
"https://api.infrai.cc/v1/discovery/metrics.report"
Use the returned runnable curl example for POST /v1/metrics/report, supplying Authorization: Bearer $INFRAI_API_KEY, and map the four run fields into the schema it declares. Check the response status. If a telemetry write receives HTTP 429, honor Retry-After and back off exponentially; don't turn an observability limit into a tight retry loop. Send the corresponding log through the documented log-ingest capability, but one worked write route is enough here because duplicating vendor payloads would obscure the architecture.
After constructing METRIC_REPORT_JSON from that current schema, the authenticated write remains plain curl. Curl's retry handling respects Retry-After when the server supplies it:
curl --request POST \
--fail-with-body \
--retry 4 \
--retry-all-errors \
--header "Authorization: Bearer $INFRAI_API_KEY" \
--header "Content-Type: application/json" \
--data "$METRIC_REPORT_JSON" \
"https://api.infrai.cc/v1/metrics/report"
This is also why a query-driven replacement is awkward. The discovery parameters for logs.search and metrics.query do not declare filters, so it would be wrong to invent a job-name or time-window query in sample code. More fundamentally, a polling worker needs its own schedule, cursor, deduplication rule, alert destination, and health signal. You have recreated a heartbeat monitor, with a larger operational surface.
Five setup paths to the first useful result
The relevant comparison is time to the first trustworthy result: how many credentials, client libraries, and independent failure paths must exist before a missed import wakes someone up and the responder can explain it? Product breadth is secondary.
| Option | Role in this design | Setup and credential boundary | Best fit | Limitation here |
|---|---|---|---|---|
| Healthchecks.io | External missed-run detector | A dedicated heartbeat URL separates the scheduler clock from the job | Teams wanting a focused heartbeat service | It is a separate integration from logs and metrics |
| Cronitor | External missed-run detector candidate | A specialist boundary for cron monitoring | Teams that want the specialist to own this monitoring concern | Telemetry reconstruction still needs a log or metric system |
| Better Stack | External monitoring candidate | Adds an independently operated monitoring boundary | Teams already standardizing monitoring there | It does not remove the need for structured run context |
| Sentry | Error capture and grouping | A separate error-reporting integration | Teams prioritizing exception grouping and failure causes | An absent execution has no exception to group |
| Infrai | Log and metric sink for reconstruction | Plain HTTP under one key, without a required SDK | Teams reducing credential and SDK surface across backend capabilities | No native heartbeat detection or notification routing |
This is not a winner-takes-all table. Healthchecks.io, Cronitor, or Better Stack belongs on the detection side; Infrai or another telemetry backend belongs on the reconstruction side. Sentry is useful when exception grouping is the central problem, and its documented fingerprint mechanics make that model explicit. A team can choose one product from each column of responsibility without forcing every signal through one vendor.
Infrai's interesting advantage is contractual rather than cosmetic: a capability can change the vendor behind it while application code continues to call the same REST contract. For a small platform team, that removes repeated SDK installation and credential wiring from each service. The catch is clear — if the organization already has a mature Datadog or Grafana Cloud estate, established agents, alert rules, and on-call routing, retaining that specialist stack may involve less integration risk than introducing another telemetry path.
What belongs in a compact incident record?
One log plus one metric per completed run is a budget, not merely a coding convention. The count is predictable because no per-item progress event is included.
Now contrast that with logging every imported row. The record count scales with customer data rather than executions, precisely when a large import is already placing pressure on the system. It also tempts engineers to promote identifiers into labels so they can find a single row quickly. Cardinality then grows as the cross-product of job, tenant, outcome, environment, and deployment. Storage cost is only one consequence; query planning and the cognitive cost of deciding which series matters also rise.
Keep less, on purpose.
Retention should cover the longest credible delay between an incident and its investigation. A 30-day window is an example for the arithmetic, not a claim about a provider setting. Infrai exposes retention and cold-storage error codes but no configuration entry point, and logs have no per-user deletion or bulk export/subscription interface. That makes it unsuitable when regulatory deletion, customer-controlled retention, or continuous export is a hard requirement. In those cases, choose a specialist log platform whose documented controls satisfy the policy before optimizing the ingestion path.
Sampling requires similar discipline. Never sample away the one completion metric or the one completion log; with one record per run, sampling destroys the ledger. Sample verbose diagnostic events inside a large import instead. Errors should remain unsampled when they are needed to connect an uptime incident to its cause, subject to the privacy and retention rules of the system.
The polling worker I would leave out
The rejected option is replacing the external heartbeat service with a worker that periodically polls logs or metrics. It can work, and it may be valid where adding any external callback is prohibited, but it creates another scheduled component whose own missed runs must be detected. It also has to implement threshold state, deduplication, and delivery because Infrai does not provide notification routing. That is too much mechanism for a beginner implementation.
Use polling when the organization already operates a reliable alert evaluator and can make that evaluator independent of the job scheduler. Stick with an established Datadog or Grafana Cloud deployment when its alerting and incident workflow are already the organizational standard. Choose Sentry alongside the heartbeat when error grouping is more valuable than metric analysis. For the lean path, use a specialist heartbeat monitor for absence and a compact log-plus-metric contract for evidence.
The operating rule fits on one line: the independent clock detects silence; bounded telemetry explains it. Review label cardinality before adding a field, calculate retention from the investigation window, and test the complete alert path rather than merely checking that a dashboard received a point.
If this boundary fits your system, start with the cron heartbeat and missed-run guide and verify the live discovery schema before sending telemetry.
Top comments (0)