Skip to content

test(e2e): capture previous container logs and stop killing the snapshot - #3346

Merged
myasnikovdaniil merged 4 commits into
mainfrom
test/e2e-capture-previous-logs
Jul 22, 2026
Merged

myasnikovdaniil merged 4 commits into
mainfrom
test/e2e-capture-previous-logs

Conversation

@lexfrei

@lexfrei Aleksei Sviridkin (lexfrei) commented Jul 18, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does

Root-causing a recent E2E failure was nearly blocked because the diagnostics captured on failure had collected almost nothing. Two independent mechanisms were responsible, and both had been silently in place for a long time.

The failing container's previous logs were never captured. The pod had crash-looped, so the decisive evidence lived in the first container instance, reachable only through kubectl logs --previous. Chainsaw's podLogs action has no previous field, so this could not be expressed per-suite; the conclusion had to be reconstructed from indirect evidence instead. A new capture runs from the global catch and from the BATS failure hook, selecting containers at runtime by restartCount > 0 across both init and regular containers, so a healthy run stays quiet and future suites are covered without per-suite wiring.

The catch was structurally guaranteed to be killed. A Chainsaw script op is bounded by timeouts.exec, which is 5m, while the catch's own inner budgets summed to roughly 960s. It reported SCRIPT | ERROR | signal: killed and produced nothing — and its second leg, the data-plane capture, had therefore never run at all. The kill signature was itself misleading: timeout exits 124 and cannot produce that message, which comes from Go's exec.Cmd killing the process group. Each catch op now carries an explicit timeout above the budgets it contains, so the inner one fires and leaves a partial, usable artifact instead of losing everything.

The snapshot was truncated at 60 seconds. crust-gather --duration defaults to 60s and was never passed, so every snapshot ever taken was cut short, with nothing in the artifact saying so. The API discovery it performs first is not covered by that budget at all, which is where the outer wall-clock was going on a cluster with a degraded aggregated APIService — precisely the state a failed run is in. Both budgets are now set explicitly, and the collector's output goes to a log inside the snapshot with a completeness verdict, so "is this snapshot complete?" is answerable from the artifact.

Failure paths are reported rather than swallowed: an unreachable API says so instead of implying nothing had restarted, a truncated capture is kept and marked, and every dropped item is logged, since silent truncation reads as full coverage.

What is not verified here

The mechanism claims are read from the pinned versions of Chainsaw and crust-gather. Everything cluster-facing is not verified by this PR: the go-template's output shape against a live API server, --previous returning the first instance for a real crash-looper, and whether the new collection budgets suffice on a degraded cluster. The bats suite covers the selection helpers only, and those cases are mutation-tested.

Release note

NONE

Summary by CodeRabbit

  • Diagnostics
    • Improved end-to-end failure evidence by capturing “previous instance” logs for crash-looping pods (including restarted init containers) and prioritizing relevant namespaces.
    • Added time-bounded diagnostics collection with explicit outer timeouts; collector output is saved to per-snapshot log files.
    • Classifies collector outcomes as complete, timeout-truncated, or failed, while preserving the original test failure signal.
  • Documentation
    • Expanded E2E failure-diagnostics guidance with new timeout and evidence-capture conventions.
  • Tests
    • Added unit-style tests for previous-log selection, prioritization, row limiting, and deterministic log artifact naming.

@github-actions github-actions Bot added size/XL This PR changes 500-999 lines, ignoring generated files area/testing Issues or PRs related to testing (e2e, bats, unit tests) labels Jul 18, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request improves the diagnostic capabilities of the E2E testing suite by addressing silent data loss during failure collection. It introduces a mechanism to capture logs from previous container instances, which are often critical for debugging crash-looping pods. Additionally, it refines the timeout and budget management for diagnostic tools like crust-gather to ensure that snapshots are not silently truncated or killed mid-execution, providing more reliable artifacts for investigating test failures.

Highlights

  • Previous Container Log Capture: Added a new mechanism to capture logs from the previous instance of crash-looping containers using kubectl logs --previous, ensuring decisive failure evidence is preserved before kubelet garbage collection.
  • Explicit Collection Budgets: Configured explicit timeouts and collection durations for crust-gather and catch scripts to prevent silent truncation and ensure that collection processes are not prematurely killed by outer orchestrator timeouts.
  • Diagnostic Reliability: Improved failure reporting by logging diagnostic outcomes (complete, truncated, or failed) to files within snapshots, allowing for better post-mortem analysis of E2E failures.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment Gemini (@gemini-code-assist) Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@dosubot dosubot Bot added the area/ci Issues or PRs related to CI workflows, GitHub Actions, automation label Jul 18, 2026
@coderabbitai

coderabbitai Bot commented Jul 18, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d14742cf-604b-4dd0-aaa2-26562717711f

📥 Commits

Reviewing files that changed from the base of the PR and between 76f7267 and f3a0f2d.

📒 Files selected for processing (1)
  • hack/e2e-capture-previous-logs.sh

📝 Walkthrough

Walkthrough

The PR adds previous-instance container log capture to E2E failure handlers, bounds and records crust-gather output, adds helper tests, updates timeout guidance, and clarifies dataplane capture budget comments.

Changes

E2E failure diagnostics

Layer / File(s) Summary
Previous-instance log capture
hack/e2e-capture-previous-logs.sh, hack/capture-previous-logs.bats, hack/cozytest.sh, hack/e2e-chainsaw/.chainsaw.yaml
Discovers restarted containers, captures kubectl logs --previous artifacts, integrates capture into failure paths, and tests selection helpers.
Bounded host snapshot collection
hack/cozytest.sh, hack/e2e-chainsaw/.chainsaw.yaml, hack/e2e-chainsaw/_lib/run-kubernetes.sh, hack/e2e-capture-dataplane.sh
Runs crust-gather with explicit duration and wall-clock limits, stores output, classifies completion states, and documents dataplane timeout budgets.
Diagnostic conventions
docs/agents/e2e-testing.md
Documents previous-container logs, sufficient catch timeouts, explicit crust-gather --duration, and snapshot logging.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant E2E as E2E failure handler
  participant Logs as e2e-capture-previous-logs.sh
  participant K8s as kubectl
  participant Gather as crust-gather
  participant Snapshot as snapshot artifacts
  E2E->>Logs: capture restarted container logs
  Logs->>K8s: list pods and fetch --previous logs
  Logs->>Snapshot: write container log artifacts
  E2E->>Gather: collect with bounded duration
  Gather->>Snapshot: write crust-gather.log
  E2E->>Snapshot: record completion or truncation status
Loading

Possibly related issues

Possibly related PRs

Suggested labels: kind/bug

Suggested reviewers: androndo, kvaps, myasnikovdaniil, sircthulhu, ivanhunters

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the main changes: capturing previous container logs and adjusting snapshot handling to avoid premature killing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test/e2e-capture-previous-logs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a mechanism to capture previous container instance logs for crash-looping pods during E2E test failures. It adds a new helper script hack/e2e-capture-previous-logs.sh along with BATS unit tests, integrates this script into both cozytest.sh and Chainsaw's global catch block, and configures explicit timeouts and durations for crust-gather collections to prevent silent truncation. The review feedback suggests improving robustness and portability by avoiding reliance on the executable bit (-x) when executing the capture script, and handling the 0 limit explicitly in prevlog_cap to prevent failures on BSD-based systems like macOS.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread hack/cozytest.sh
Comment on lines +110 to +113
if command -v kubectl >/dev/null 2>&1 && [ -x "$(dirname "$0")/e2e-capture-previous-logs.sh" ]; then
_prev_rc=0
timeout -k 30 300 "$(dirname "$0")/e2e-capture-previous-logs.sh" \
"$_snap/previous-logs" "${COZY_TEST_NAMESPACE:-tenant-test}" 2>&1 || _prev_rc=$?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Relying on the executable bit ([ -x ...]) can be fragile if file permissions are not preserved during git operations or in certain environments. It is safer to check for file existence ([ -f ...]) and explicitly execute the script using sh.

Suggested change
if command -v kubectl >/dev/null 2>&1 && [ -x "$(dirname "$0")/e2e-capture-previous-logs.sh" ]; then
_prev_rc=0
timeout -k 30 300 "$(dirname "$0")/e2e-capture-previous-logs.sh" \
"$_snap/previous-logs" "${COZY_TEST_NAMESPACE:-tenant-test}" 2>&1 || _prev_rc=$?
if command -v kubectl >/dev/null 2>&1 && [ -f "$(dirname "$0")/e2e-capture-previous-logs.sh" ]; then
_prev_rc=0
timeout -k 30 300 sh "$(dirname "$0")/e2e-capture-previous-logs.sh" \
"$_snap/previous-logs" "${COZY_TEST_NAMESPACE:-tenant-test}" 2>&1 || _prev_rc=$?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping [ -x ] here. The helper is committed with its executable bit (mode 100755) and git preserves mode bits on checkout, so the permission is reliable in the CI environment where this runs. All three call sites of the capture helpers use [ -x ] (hack/cozytest.sh, plus two in hack/e2e-chainsaw/.chainsaw.yaml), so this stays internally consistent; switching two of them to [ -f ] + sh would both diverge from the third and bypass the script's own #!/bin/sh shebang.

Comment on lines +105 to +107
if [ -x ../../e2e-capture-previous-logs.sh ]; then
timeout -k 30 300 ../../e2e-capture-previous-logs.sh \
"${snap}/previous-logs" "${COZY_TEST_NAMESPACE:-tenant-test}" 2>&1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Relying on the executable bit ([ -x ...]) can be fragile. It is safer to check for file existence ([ -f ...]) and explicitly execute the script using sh.

          if [ -f ../../e2e-capture-previous-logs.sh ]; then
            timeout -k 30 300 sh ../../e2e-capture-previous-logs.sh \
              "${snap}/previous-logs" "${COZY_TEST_NAMESPACE:-tenant-test}" 2>&1

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the hack/cozytest.sh call site: keeping [ -x ]. The script is committed executable (mode 100755), git preserves that on checkout, and all three helper invocations use [ -x ] for consistency.

Comment on lines +97 to +107
prevlog_cap() {
_pc_max="$1"
case "$_pc_max" in
'' | *[!0-9]*) _pc_max=12 ;;
esac
# A zero cap emits nothing; the caller still reports the full overflow count,
# so the drop stays visible. (GNU head treats -n 0 as an empty result; BSD
# head rejects it outright, which lands in the same place here -- no rows
# through -- but is why this is not described as a portable no-op.)
head -n "$_pc_max"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

On BSD-based systems (such as macOS), head -n 0 fails with an error (head: illegal line count -- 0). To make the script fully portable and robust across different development environments, we can handle the 0 limit explicitly by consuming the input without calling head.

Suggested change
prevlog_cap() {
_pc_max="$1"
case "$_pc_max" in
'' | *[!0-9]*) _pc_max=12 ;;
esac
# A zero cap emits nothing; the caller still reports the full overflow count,
# so the drop stays visible. (GNU head treats -n 0 as an empty result; BSD
# head rejects it outright, which lands in the same place here -- no rows
# through -- but is why this is not described as a portable no-op.)
head -n "$_pc_max"
}
prevlog_cap() {
_pc_max="$1"
case "$_pc_max" in
'' | *[!0-9]*) _pc_max=12 ;;
esac
# A zero cap emits nothing; the caller still reports the full overflow count,
# so the drop stays visible. (GNU head treats -n 0 as an empty result; BSD
# head rejects it outright, which lands in the same place here -- no rows
# through -- but is why this is not described as a portable no-op.)
if [ "$_pc_max" -eq 0 ]; then
cat >/dev/null
else
head -n "$_pc_max"
fi
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied in f3a0f2d — special-cased a zero cap so it consumes stdin instead of calling head -n 0, which BSD/macOS head rejects. It only bites a local run with COZY_PREVLOG_MAX=0 (CI is GNU coreutils, where -n 0 is already an empty result), but the script targets /bin/sh portability elsewhere, so closing it is consistent.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@hack/e2e-capture-previous-logs.sh`:
- Around line 9-16: The documentation incorrectly describes kubectl logs
--previous as retrieving the first container instance. Update
hack/e2e-capture-previous-logs.sh lines 9-16, hack/cozytest.sh lines 101-109,
hack/e2e-chainsaw/.chainsaw.yaml lines 88-98, and docs/agents/e2e-testing.md
line 92 to consistently state that --previous captures only the immediately or
most recently terminated instance, with each restart replacing the retained
previous instance; remove any claim that it retrieves the first instance.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d7af508e-79ff-4b6b-995f-14f4cdf1a15c

📥 Commits

Reviewing files that changed from the base of the PR and between 28a59da and 7be8ff4.

📒 Files selected for processing (7)
  • docs/agents/e2e-testing.md
  • hack/capture-previous-logs.bats
  • hack/cozytest.sh
  • hack/e2e-capture-dataplane.sh
  • hack/e2e-capture-previous-logs.sh
  • hack/e2e-chainsaw/.chainsaw.yaml
  • hack/e2e-chainsaw/_lib/run-kubernetes.sh

Comment thread hack/e2e-capture-previous-logs.sh Outdated
@myasnikovdaniil

Copy link
Copy Markdown
Contributor

I went looking for evidence to size the tradeoff this PR takes, and ended up auditing e2e diagnostics coverage more broadly — findings in #3368. Two of them bear directly on this PR, so rather than sit on them I want to put them here and let you decide the split.

The premise behind the ceiling increase does not hold. .chainsaw.yaml justifies 5m → 19m with "the report-upload steps are if: always() so a job timeout still yields the artifact". Measured against two real jobs that hit timeout-minutes: 180 (29557003429 / job 87812435947, 29568562337 / job 87848132058):

JOB 87848132058   E2E Tests   conclusion=cancelled   09:11:25 -> 12:16:26
  14 cancelled    Run E2E tests
  15 success      Collect chainsaw report
  16 success      Upload chainsaw report
  17 in_progress  Collect report            <- consumes the grace window
  18 pending      Upload cozyreport.tgz     <- never ran

GitHub's post-cancellation grace measured ~4m48s in both, and step 17 (a full crust-gather) consumes it, so the upload never starts. Complete job does not run either, so the job log itself returns BlobNotFound. Artifact listings confirm it: neither run has a cozyreport. One other run in the window did keep it, only because its tail happened to fit the grace — so the outcome is binary and luck-dependent rather than guaranteed.

This matters more than a stale comment because the same reasoning is being written into docs/agents/e2e-testing.md as guidance.

On the arithmetic, which I think survives but with less margin than it looks. Marginal cost is +14m per failed test, not +19m, since today's cap is 5m. Against a 95.3m median / 106.9m p90 baseline, three failing tests lands at ~152m / 164m — 16m from the cliff before cleanup (delete 5m + cleanup 5m per test), the failing tests' own timeout burn, cozyreport.sh, and upload. That moves break-even from ~4-5 failing tests to ~3. Observed maximum in 400 runs is 3, so it holds — but the ceiling is incidental rather than designed (a broken install short-circuits before chainsaw, 20 of 70 confirmed), and gateway/ alone holds 15 Tests.

What I would suggest, entirely your call on where it lands:

The single cheapest fix for both of the above is an internal deadline (~150m) around the chainsaw step, so the job always retains ~30m to collect and upload. That converts the cancelled case — maximum diagnostics collected, zero delivered — into an ordinary failure with full artifacts, and it removes the risk this PR's ceiling increase adds. It is also the thing that protects every other diagnostics investment.

So: would you rather fold the deadline (and the one-line comment correction) into this PR, or land this as-is and take them in a follow-up? Either works from my side. If you prefer to defer, I would still ask for the comment and the docs bullet to be corrected here, since the claim as written will outlive the PR.

Worth saying plainly: the rest of this PR is well-evidenced and I verified the core claims independently.

  • The catch really was structurally doomed — timeouts.exec: 5m against 330s + 630s of inner budget. In the window, 17 jobs show signal: killed mid-crust-gather having collected nothing, and catch durations are sharply bimodal at 60-62s and exactly 300-301s. The timeout: override is the highest-value line in the diff.
  • The 60s truncation is real: --duration was never passed at any call site, so every crust-gather budget in the repo was really 60s, not the 300s the timeout -k 30 300 implies. On elapse it exits 1, not 124 — which is worth knowing for the verdict case, since the most likely truncation currently lands in the *) FAILED arm. Mitigating: no --encoding is passed and Encoding::default() == Path, so the partial tree stays readable.
  • chainsaw v0.2.15 genuinely has no previous on podLogs, and it is still absent on main upstream, so the local workaround is the right call rather than a version bump.
  • The new script is properly failure-tolerant — no set -e/set -u, failures classified and continued, exit 0 — and its go-template holds up against Pending pods, pods with no status key, and init-container-only restarts. --tail is bounded (200 × max 12 containers), so there is no risk of pushing the real error out of the visible log window.

One genuinely useful thing the audit turned up in this PR's favour: crust-gather never collects init or ephemeral container logs at all (src/scanners/logs.rs:107 iterates pod.spec.containers only, and there are no init_container references anywhere in src/). So a crash-looping init container — our bootstrap-job failure class — is invisible in the snapshot on both clusters. That makes this script strictly additive rather than partly redundant with crust-gather, which is a stronger justification than the PR currently claims for itself.

The tenant-cluster gap (previous-logs is wired to the host cluster only, while run-kubernetes.sh got the --duration fix but no previous-logs against CURRENT_TENANT_KC) is in the issue as a follow-up rather than something I would hold this PR for.

@myasnikovdaniil myasnikovdaniil left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOT LGTM, but narrowly — one false sentence in a comment is the only thing I am asking for. Everything else here I verified and want landed quickly.

Detail and evidence are in my earlier comment and in #3368; I will not repeat them.

Why I want this to land fast. It closes two items #3368 independently identified: every crust-gather budget in the repo was really 60s rather than the 300s it read as (--duration was never passed at any call site), and the global error.catch declared 960s of inner budget inside a 300s outer. That second one is not theoretical — 17 jobs in a 400-run window show signal: killed mid-collection having produced nothing, with catch durations bimodal at 60-62s and exactly 300-301s. Roughly a third of our catches currently collect nothing, and this fixes that. The per-op timeout: override is the highest-value line in the diff.

The new script also has a standalone win the PR undersells: crust-gather never collects init or ephemeral container logs at all (src/scanners/logs.rs:107 iterates pod.spec.containers only), so a crash-looping init container is invisible in the snapshot on both clusters. That makes this strictly additive rather than partly redundant.

The one blocking item is inline: the comment justifying the 5m → 19m ceiling asserts that if: always() upload survives a job timeout. It does not, and I measured it on two real jobs. It blocks only because the same reasoning is being added to docs/agents/e2e-testing.md, where it will outlive this PR and mislead the next reader. Correct both and I will approve.

Explicitly not blocking:

  • The ~150m internal deadline around the chainsaw step. That is the real fix for the risk this ceiling adds, but it lives in pull-requests.yaml and is separate work — I would rather it not gate this.
  • The tenant-cluster gap (previous-logs is host-only, while run-kubernetes.sh gets the --duration fix but no previous-logs against CURRENT_TENANT_KC). Tracked in #3368.
  • The verdict case labelling: crust-gather exits 1 on --duration elapse, never 124, so the most likely truncation currently lands in the *) FAILED arm. Mitigating — no --encoding is passed and Encoding::default() == Path, so the partial tree stays readable and servable.

Two optional fold-ins, entirely your call. You already touch hack/e2e-capture-dataplane.sh here (comment-only), and both of the "blind diagnostics" items in #3368 live in that file:

  1. capture_lb_datapath is called at line 737, but lines 250-253 exit 0 when no NotReady pod exists — while the script's own header (lines 56-64, 472-474) says the LB section exists precisely for that case and is "independent of the NotReady-pod path above". An unreachable LB with all pods Ready is the kubernetes-* / MetalLB signature, and the capture never runs for it.
  2. The affected-pod filter requires a non-empty podIP, but the cilium endpointManager leak manifests as pods that never receive an IP — so the script excludes its own headline failure mode.

Both are small and in a file already open here. Equally fine as a follow-up if you would rather keep this PR tight.

Comment thread hack/e2e-chainsaw/.chainsaw.yaml Outdated
Comment on lines +59 to +60
# the report-upload steps are `if: always()` so a job timeout still yields the
# artifact. If this ever does start truncating runs, bound the number of

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the blocking item, and it is measured rather than argued.

Two real jobs that hit timeout-minutes: 180:

JOB 87848132058   conclusion=cancelled
  15 success      Collect chainsaw report
  16 success      Upload chainsaw report
  17 in_progress  Collect report            <- consumes the grace window
  18 pending      Upload cozyreport.tgz     <- never ran

The post-cancellation grace measured ~4m48s in both, and step 17 (a full crust-gather) consumes all of it. Complete job never runs either, so the job log returns BlobNotFound. Artifact listings confirm neither run kept a cozyreport. One other run in the window did keep it, purely because its tail fitted the grace — so the outcome is luck-dependent, not guaranteed.

So a job timeout loses the artifact and the log, which is the inverse of what this sentence claims, and it is the failure mode the raised ceiling makes more likely.

Suggested change
# the report-upload steps are `if: always()` so a job timeout still yields the
# artifact. If this ever does start truncating runs, bound the number of
# the report-upload steps are `if: always()`. That last point does NOT extend
# to a job timeout: a 180-minute cap ends the job `cancelled`, and the ~4m48s
# grace window is consumed by the earlier `Collect report` step, so both
# cozyreport.tgz and the job log are lost (measured on runs 29557003429 and
# 29568562337). If this ever does start truncating runs, bound the number of

The matching bullet in docs/agents/e2e-testing.md needs the same correction — that is the copy I actually care about, since it becomes guidance.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected the comment to your wording in 1ff83f5 — if: always() upload does not survive a job timeout: the 180-minute cap ends the job cancelled and the grace window is spent on report collection, so both the artifact and the job log are lost.

On the docs side: I checked docs/agents/e2e-testing.md at HEAD and that reasoning isn't there. The bullets this PR adds cover previous-instance capture, sizing a catch's timeout: above its inner budgets, and setting crust-gather --duration — none of them claim a job timeout still yields the artifact. So the corrected comment is the only copy of that claim in the tree. If you'd still like an explicit job-timeout caveat written into the standing guidance, I'm happy to add one — just say the word.

Root-causing a crash-loop failure was blocked twice over by the
diagnostics collected on failure.

`kubectl logs` shows only the current container instance, so for a
crash-looping pod the suites' `podLogs` collector captured the Nth
restart -- a fresh replay of the same startup path -- while the
evidence that explained the failure sat in the first, interrupted
instance. Chainsaw's podLogs collector has no `previous` option, so
add e2e-capture-previous-logs.sh and drive it from the global catch
and from the BATS failure hook. It discovers containers with
restartCount > 0 at runtime (init containers included, where an
interrupted bootstrap lives), orders the app suites' namespace first,
and dumps each to the job log and the snapshot tree. Containers that
never restarted are skipped, so a healthy run emits one line rather
than a page of "previous terminated container not found".

Every way that capture can come up short is named rather than
inferred, because kubectl exits 1 for a garbage-collected container,
an RBAC denial and an unreachable node alike: the container cap, a
pod list that never returned (which is not the same as nothing having
restarted), a read that timed out versus one kubectl refused and the
reason it gave, and a previous instance that produced no output. A
partial capture is kept and marked truncated rather than deleted --
it is the only copy of a log already gone from the cluster.

The crust-gather snapshot was killed by the Chainsaw op containing it.
A script op is bounded by timeouts.exec (5m), and the catch's own
wall-clock wrappers summed to 960s, so the script was SIGKILLed
partway through -- taking out the process group mid-write and skipping
the data-plane capture entirely. Give each catch op an explicit
timeout above the budgets it contains, so an inner timeout fires
instead and leaves a partial, still-servable snapshot behind. The
data-plane leg drops from 600s to 300s at that call site so both legs
fit one envelope; the BATS hook keeps the wider budget, having no
containing op.

Set crust-gather --duration explicitly as well. It is the collection
phase's own budget, it defaults to 60s, and on elapse it abandons the
collection and skips its finish step, leaving a truncated tree with no
indication anything is missing. The outer wall-clock has to exceed it
because the API discovery that runs first is not covered by that
budget at all -- with an unhealthy aggregated APIService, the state a
failed run is usually in, discovery alone can consume the whole
timeout before a single object is written. Send each collector's
output to a log beside its snapshot instead of /dev/null, with a
complete/truncated/failed verdict, so that question is answerable
from the artifact.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
@lexfrei
Aleksei Sviridkin (lexfrei) force-pushed the test/e2e-capture-previous-logs branch from 7be8ff4 to 07c67a8 Compare July 21, 2026 15:37
The catch-timeout comment justified the raised ceiling by asserting that
if: always() upload survives a job timeout. It does not: a 180-minute cap
ends the job cancelled, and the post-cancellation grace window is consumed
by the earlier report-collection step, so both cozyreport.tgz and the job
log are lost. Correct the comment to describe the real failure mode.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
kubectl logs --previous returns the immediately preceding terminated
container instance, not the original first one — after multiple restarts
the first instance is already gone. Reword the helper, the BATS and
Chainsaw failure handlers, and the diagnostics doc to match the actual
--previous semantics.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
A zero container cap reached head -n 0, which GNU head treats as an empty
result but BSD/macOS head rejects ("illegal line count -- 0"), aborting a
local run. Special-case the zero cap to consume stdin instead, so the
helper behaves the same on both coreutils flavours.

Assisted-By: Claude <[email protected]>
Signed-off-by: Aleksei Sviridkin <[email protected]>
@lexfrei

Copy link
Copy Markdown
Contributor Author

Thanks for the measured review and for verifying the core claims independently. Landed the blocking item:

The if: always() / job-timeout comment in .chainsaw.yaml is corrected to your wording (1ff83f5). On the docs bullet: I checked docs/agents/e2e-testing.md at HEAD and that reasoning isn't present — the bullets this PR adds cover previous-instance capture, catch timeout: sizing, and crust-gather --duration, none of which claim a job timeout preserves the artifact. So the corrected comment is the only copy in the tree. If you'd like an explicit job-timeout caveat added to the standing guidance anyway, say the word and I'll add it.

Also corrected the --previous semantics across the helper, both failure handlers, and the doc (76f7267): it captures the immediately preceding terminated instance, not the first.

The explicitly-not-blocking items — the ~150m internal deadline in pull-requests.yaml, the tenant-cluster previous-logs gap, the verdict case labelling, and the two e2e-capture-dataplane.sh fold-ins (capture_lb_datapath early-exit and the podIP filter) — I'm taking as follow-ups to keep this PR tight, tracked against #3368. They're all real; I'd rather land them separately than widen this change.

@myasnikovdaniil myasnikovdaniil left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

Rechecked the current head after f3a0f2d:

  • the blocking timeout comment now correctly states that a job timeout loses both the artifact and job log;
  • previous-instance wording matches kubectl --previous semantics;
  • the BSD head -n 0 case is covered;
  • the focused previous-log Bats suite passes locally;
  • required GitHub CI, including pre-commit and E2E, is green.

This resolves my earlier change request.

@myasnikovdaniil
myasnikovdaniil merged commit 55195de into main Jul 22, 2026
17 of 22 checks passed
@myasnikovdaniil
myasnikovdaniil deleted the test/e2e-capture-previous-logs branch July 22, 2026 12:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/ci Issues or PRs related to CI workflows, GitHub Actions, automation area/testing Issues or PRs related to testing (e2e, bats, unit tests) size/XL This PR changes 500-999 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants