fix(otel): alert on Cloud delivery failures only while they are happening - #1147
Merged
Merged
Conversation
…ning The telemetry storage page raised a red "N spans were never delivered to Temps Cloud ... re-enroll this instance" alert whenever any dead-lettered span existed for the project. Dead letters never change state, so an instance that had recovered kept showing the alert, and kept telling the operator to re-enroll, indefinitely. "Failing now" is now derived from spans that failed an attempt and are still pending (`state = 'pending' AND last_error IS NOT NULL`). That set appears on the first refused attempt and empties on its own once Cloud accepts again. The alert names the fix only when there is one: re-enroll after a credential rejection, re-link an unlinked instance, or turn telemetry export back on. Cloud-side failures get no invented fix. Past losses move to Storage history as dated gaps: dead letters grouped by span time (a new gap after an hour without one), each with its count, when retries ran out and why. The query walks the existing (entity_type, project_id, state, enqueued_at) index in order, so memory stays constant: about 310 ms and a 32 kB hash aggregate at 520k dead letters. The CLI (`cloud telemetry write-mode get`) shows the same split. Signed-off-by: David Viejo <dviejo@kfs.es>
📓 Changelog previewThis is what your commits will add to the generated ## [Unreleased]
### Fixed
- **otel:** Alert on Cloud delivery failures only while they are happening
- **otel:** Keep delivery alert and history accurate after review |
|
- Web: poll project telemetry (15 s while spans are failing or queued, 60 s otherwise), so the failing alert clears by itself and a new failure appears without a reload. The draft-reset effect now depends only on the saved write mode, fidelity and allowlist. Keyed on the whole response, every poll would have overwritten unsaved edits. - CLI: when the server predates dated delivery gaps, fall back to its dead-letter total and last reason instead of showing nothing. - A gap's give-up time and reason now come from the same row (its latest span), so they always describe the same failure. - The current failure reason is read from the front of the FIFO queue. Claims are strictly oldest-first, so the oldest failing row was in the most recent attempt; the highest id can carry an older reason. - The API reports `delivery_gaps_truncated` when more than 100 gaps exist. Web and CLI then say how many older undelivered spans are not listed. Signed-off-by: David Viejo <dviejo@kfs.es>
Contributor
Author
|
@greptileai review |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The project Telemetry storage page showed a red alert whenever any dead-lettered span existed:
Dead letters are a permanent record and never change state. So once an instance recovered (credential re-issued, Cloud outage over), the alert stayed red and kept telling a healthy instance to re-enroll, with no way to clear it. Operators learn to ignore red banners, and "re-enroll" is the wrong instruction once the link works.
What changes
The alert only shows while delivery is failing now. "Failing now" comes from spans that failed at least one attempt and are still pending (
state = 'pending' AND last_error IS NOT NULL). That set appears on the first refused attempt, hours before anything is dead-lettered, and it empties on its own once Cloud accepts again. Nothing needs dismissing.The alert names the fix only when there is one, and links to Temps Cloud settings:
Past losses become history, not an alert. Dead letters are grouped by span time into separate gaps: a new gap starts after
DELIVERY_GAP_SEPARATION(1 h) without one. They are listed under Storage history → Spans never delivered to Temps Cloud with the time range, the count, when retries ran out, and why. Two outages days apart are two rows, not one range claiming the days in between were lost.API (
GET /otel/cloud-telemetry/projects/{project_id}), additive onlydelivery_failing,retrying_spans,delivery_failing_since,delivery_failure_error,delivery_failure_action,delivery_failure_setup_pathdelivery_gaps: CloudDeliveryGapResponse[](first_span_at,last_span_at,undelivered_spans,gave_up_at,last_error)dead_lettered_spans/last_dead_letter_*fields are unchanged, for older clients.CLI parity
bunx @temps-sdk/cli cloud telemetry write-mode getprints the same split: a warning only while failing, and a "Spans never delivered to Temps Cloud" history section. It uses?? []so it doesn't break against an older server.Load / efficiency
Control-plane only: one settings-page read, nothing on the ingest path.
delivery_failure_for_projectis a filtered count over this project's pending rows (the pending set is bounded by the outbox byte cap).delivery_gaps_for_projectwalksidx_cloud_telemetry_outbox_entity_projectinenqueued_atorder, so both window functions run without a sort. Gaps are hash-aggregated (a handful of rows), and each gap's reason is one index probe (at most 100). Measured at 520,300 dead letters for one project: ~280 ms end-to-end, 32 kB aggregate memory, no full sort. An earlier version with an orderedARRAY_AGGsorted all 520k rows (69 MB, ~600 ms); that was replaced before this PR.Evidence
Tests
crates/temps-otel/tests/cloud_primary_write_mode_test.rs::past_delivery_failures_are_history_and_only_retrying_spans_mean_failing_nowruns against real Postgres (not skipped). It covers:cloud_telemetry_handlerunit tests cover the four link states in the table above:apps/temps-clidescribeDeliverytests (recovered → no alert + history; failing → alert + fix; no fix → none invented):45 pass, 0 failacrosssrc/commands/cloud/.web/e2e/authenticated/cloud-delivery-failure.spec.ts(recovered / failing / never-failed), run against a local server built from this branch:cargo clippy -p temps-cloud-client -p temps-otel --all-targets -- -D warnings: clean.cargo fmt --check: clean. Webtsc --noEmit, CLItsc --noEmit,spec:check,source_attribution.py check: clean.Live, against a local server built from this branch
Seeded a project's outbox with 520k dead letters from three outages, and later 1,200 retrying spans.
Recovered (nothing retrying):
With spans retrying:
CLI against the same server (recovered):
I checked the Playwright run's screenshots by eye (they're written to
/tmp/cloud-delivery-{failing,recovered}.png; not attached here). Failing: red alert with count, reason, and an Open Temps Cloud settings link. Recovered: no alert; the gap is a neutral row under Storage history.Not verified live
Notes
apps/temps-cli/src/api/sdk.gen.tsgains a 3-line doc comment onupdateSettings. That's pre-existing generator drift against the committed spec onmain, not part of this change's API.Review follow-up (bcb4a40)
gave_up_atandlast_errorcome from the same row.delivery_gaps_truncatedis set past 100 gaps; web and CLI then state how many older spans are not listed.Evidence after the follow-up: integration tests
2 passed(delivery + dead-letter),temps-otellib802 passed,temps-cloud-clientlib147 passed, CLI47 pass, Playwright6 passed, clippy--all-targets -D warningsclean.