Repository navigation
fix(timeline): warn once per export failure streak and report it in health - #2835
Merged
Birfy merged 1 commit intoNov 11, 2026
Merged
Conversation
…ealth (ThinkFlowLab#2834) a dead collector warned on every tick: at the default 5s interval that is 17,280 warnings a day, and the operator cannot tell one outage from many. the cursor now counts consecutive failures, warns when the streak opens and when it recovers, and timeline/health carries failures/lastOkAt/lastError.
Collaborator
|
Verdict: PASS at 825eadd Ran at this tip: Review, on tip 825eadd (base 1e27f10), read 2026-11-11T05:2xZ:
Not run here: |
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.
fix(timeline): warn once per export failure streak and report it in health
Fixes #2834
Owner: Chief
Reviewer: Reviewer
Written-by: Dev-5
Bot: Dev-5
Risk tier: T2
Base: 1e27f10
Claim-Id: dba7a50fe8974d6db882fe7b5b720c99
What changed
A dead OTLP collector was warned about on every failed attempt. At the default 5 s
interval that is 17,280 warnings a day for one outage, and nothing in the output says
whether the operator is looking at one collector that is down or ninety separate faults.
timeline/health— the endpoint an operator or an automation polls — had no export sectionat all, so the only evidence was that repeated warning.
After this change one failure streak produces one warning when it opens and one
when it recovers, and
timeline/healthcarries the streak so the count is readable ondemand instead of in the log.
plugins/timeline/src/otel.js— the cursor (createExporter) counts the streak. It gainsfailures,lastOkAt,lastErrorand an injectablenow, and exposesstats().The first failure of a streak warns
flybot-timeline: OTLP export failed: <msg>; later failures stay quiet until it recovers (timeline/health carries the count);every later failure only increments. The first success after failures warns
flybot-timeline: OTLP export recovered after N failed attempt(s) (last error: <msg>)and clears the streak, so a later outage is a new streak and warns again.
plugins/timeline/src/health.js—buildHealth(window, exportState): the second argumentis optional. With an exporter's
stats()the answer carriesexport: { enabled: true, failing, failures, lastOkAt, lastError }; withnullit carriesexport: { enabled: false }. Left out entirely there is noexportkey, so everyexisting one-argument caller (
digest.jsand the older health tests) is unchanged.plugins/timeline/src/timeline.js— three lines: keep the cursor, hand itsstats()(ornullwhen no exporter is configured) tobuildHealth.plugins/timeline/test/export-failure-streak.test.js(new, 6 cases) — the streak, therecovery line, the health section, the no-exporter case, three pushes behind the timer,
and a real 560 ms run at
exportMs: 100against a dead collector.docs/subsystems/timeline.md— thehealth.jsrow, thetimeline/healthendpointparagraph (field meanings), invariant 11 (one warning per streak, with the 17,280/day
figure) and the Tests paragraph name the new file.
changelog.d/2834-timeline-export-failure-streak.fixed.md.Nothing else moves: the cursor still keeps its position on a failed send and retries the
same closed spans on the next tick, the OTLP payload, the span-id dedupe, the redirect
refusal and the JSON export's 8 MiB cap are untouched, and
plugins/timeline/index.jsisnot in the diff.
Red first
Base
1e27f10e(this branch's parent). The three source files were put back to their basecontent while the new test stayed, and the run was made twice. Both runs have the same
shape,
6 tests / 0 pass / 6 fail, with the same six sentences — only the timing differs.Case 6 is Product's measurement shape: a real timer at
exportMs: 100, asendthat throws,and a 560 ms wait — the base source warns five times in that window, the fix warns once.
After the fix the same file is
6/6and the whole plugin is172/172.Why dedupe the report, not the work
The issue asks for one warning per outage, not for fewer attempts. So the exporter still
tries on every tick and still keeps its cursor: a collector that comes back receives the
spans that were waiting, and the recovery line names exactly how many attempts failed. A
backoff would have changed delivery behaviour in the same PR and is out of scope.
Scope — what this does not cover
exportMswith no backoff or jitter; only the warning is folded into one streak.timeline.jsstays per attempt. It is the outercatcharoundcursor.push(a throw outside the send path, e.g. a record whose shape breaks the mapping,or the JSON export throwing). That is a different and rarer class and this PR does not
touch it.
separately each keep their own streak.
plugins/timeline/client.jsdraws the export state;timeline/healthis where it is readable.failuresis a count of failed sends, not of dropped spans. The cursor keeps theunexported spans and retries them, so nothing is lost by the failures being counted.
different warning and not part of this streak.
Duplicate check
Open:
is:issue is:openforexport timeline,health warning,dedupe warning streakand"OTLP export failed"each return only this issue (#2834) and nothing else.Closed:
"export failed" timelinereturns#1507(workflow retry duplicate rows — unrelated);"timer ceiling"returns#2827/#2811(this family's interval ceilings, already fixed, adifferent defect: the interval value, not the warning).
timeline/health+exportreturnsonly #2834.
Generated artifacts (not smuggling)
node scripts/ds-bot-patches.mjs generaterewritespatches/ds-bot/deterministically andleft no change (
git status --porcelainshows only the untracked.cache/,.local/and
tmp/of this worktree — no tracked file).checkreportsok core/ equals ds-bot/ + patches/ds-bot/(10 patches apply cleanly in order), andcanonicalreportsgenerate is a no-op.git diff --checkis clean.plugins/**,docs/**andchangelog.d/**are not part of the patch set, so there is no mirror torewrite.
Ran at this tip
Checks ran at the local commit
68bb8d051bd5e2f0b3ef752818fb92cea7b9ccbe, based on1e27f10e(the currentmain). The fork head is the same tree, parent and message rebuiltthrough the Git Data API, so its sha differs; the two trees are identical.
TZ=UTCthroughout, with
npm_config_cacheinside the worktree and a pnpm-free tool directory infront of
PATH(see the environment note below).verifythen stops on the.github/scriptsface — the#749machine-only red below,673/672/1— so every face after it was run on its own at the same tip:The branch was rebased from the previous main (
f0f2a1ef) onto1e27f10e(#2829,Project-domain refactor) before any reading was taken.
plugins/timeline/**,docs/subsystems/timeline.mdandchangelog.d/are byte-identical between the two mains,so the red-first sentences above are the same implementation; the plugin counts do move
because
plugins/projectjoined the tree in #2829.Local reading that is environment, not this change
.github/scriptssuite,testing-layers.test.mjs(#749) — it walks worktrees and finds.cache/wt-2547, a nested worktree this machine has and CI does not. It reproduces on thebase
1e27f10etoo and names only.cache/wt-2547andtmp/gatespaths.core/test/bench-preflight.test.js— on this machine pnpm lives in/usr/bin, and thefixture drops every
PATHdirectory containing pnpm, taking/usr/binand/binwith it;with a pnpm-free tool directory in front of
PATHthe file is green. The file isbyte-identical between the base and this tip.
Not run
npm run pack, typecheck and actionlint: not part ofnpm run verify, andnothing here reaches
index.jsorsrc/host/module loading.sendis a function that throws, which is thefailure path this change touches;
otel.test.jsandotlp-redirect.test.jsalready coverthe real send path and are green at this tip.
Checkpoint
{ "pr": 2835, "claim": "Chief", "issue": 2834, "summary": "One dead OTLP collector used to write one warning per failed export attempt: 17,280 lines a day at the default 5 s interval, with nothing to say whether that was one outage or many. The cursor now counts the streak, warns once when it opens and once when it recovers, and `timeline/health` carries `failures`, `lastOkAt` and `lastError` so the count is readable without the log. The retry behaviour, payload, dedupe, redirect refusal and JSON size cap are unchanged, and callers that pass no exporter state get no `export` key at all.", "files": [ "plugins/timeline/src/otel.js", "plugins/timeline/src/health.js", "plugins/timeline/src/timeline.js", "plugins/timeline/test/export-failure-streak.test.js", "docs/subsystems/timeline.md", "changelog.d/2834-timeline-export-failure-streak.fixed.md" ], "checks": { "ran": "export-failure-streak 6/6; plugins/timeline 172/172; core 3079/3079; social 619, tree 149, workflow 528, project 147; core lint 88/0; arch-docs OK; plugin lint 0 errors; ds-bot-patches check + canonical; interop 37/37; live-e2e 34/34; changelog 8 fragments", "result": "green", "at": "825eadd19eb4f7e33a3442cdc4a81f66cc5d671a" }, "next": "none — ready for review" }