Skip to content

feat: infrahub-collect troubleshooting-bundle tool (INFP-415)#140

Merged
fatih-acar merged 20 commits into
mainfrom
fac/collect-tool-implem-opip6
Jul 10, 2026
Merged

feat: infrahub-collect troubleshooting-bundle tool (INFP-415)#140
fatih-acar merged 20 commits into
mainfrom
fac/collect-tool-implem-opip6

Conversation

@fatih-acar

@fatih-acar fatih-acar commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds infrahub-collect, the third CLI binary in this repo, implementing the troubleshooting-bundle collection tool from Jira INFP-415 (spec 003-collect-tool). It replaces the Python invoke bundle collect script and adds first-class Kubernetes support. Collection is strictly read-only — it never stops, restarts, or scales any workload — so it is safe to run against a degraded production instance.

Stacked on top of fac/collect-tool-docs (the user docs + spec + constitution amendment, reviewed separately). This PR is the implementation only; base it on the docs branch so the diff stays focused.

infrahub-collect create produces a single support_bundle_<timestamp>.tar.gz with an identical internal layout on Docker Compose and Kubernetes, plus a bundle_information.json manifest recording an explicit outcome (success / failed / skipped) for every collector.

What it collects

  • Service logs for every Infrahub service, all replicas, plus previous-container logs for restarted pods on Kubernetes
  • Parity diagnostics: Neo4j server logs, RabbitMQ status, Redis status, Prefect worker/manager state, server info (version, packages, API info/config/schema, env)
  • Container metrics (docker stats / kubectl top)
  • Opt-in: --include-backup (delegates to the existing backup logic unmodified), --benchmark, --include-queries
  • Secret masking (key-name based) on all env/config dumps before they are written

Design

  • Thin Cobra entry point at src/cmd/infrahub-collect; all logic in src/internal/app (constitution Principle I). Reuses the existing environment abstraction, CommandExecutor, tarball helper, and CNPG-primary detection.
  • Collector framework: each collector is time-bounded and non-fatal — a failure is recorded in the manifest and the run continues, still exiting 0 with a partial bundle (FR-009). A degraded instance is the primary use case.
  • No new Go module dependencies; Docker/Kubernetes accessed by shelling out to docker/kubectl like the existing backends.

Testing

  • 69 new Go unit test functions (89 incl. subtests), all passing; make fmt / vet / lint (0 issues) / test green.
  • Docker e2e (tests/e2e/test_docker_collect.py, 5 tests) — passed locally against a real compose stack (Infrahub 1.8.2): full bundle, project selection, log-lines flag/env precedence, degraded-cache (exit 0 + cache-status: failed), include-backup.
  • Kubernetes e2e (tests/e2e/test_k8s_collect.py) — passed locally against a real vcluster + Helm stack (Infrahub 1.10.2): multi-replica logs, induced-restart .previous.log, manifest schema, and a before/after read-only assertion (SC-003).
  • Collect e2e tests are wired into the existing docker/k8s CI jobs (they carry the -m docker/-m k8s markers).

Review

A multi-lens review (correctness, error handling, test coverage, types/comments/simplify) ran across the full diff. All HIGH and MEDIUM findings were fixed and re-verified, notably:

  • Bounded every collector subprocess (CopyFrom, server-info/version execs, k8s pod resolution) so a wedged container can't hang the run — collect-only bounded variants added without touching the shared helpers backup depends on.
  • Hardened Kubernetes replica enumeration so a cluster/API/RBAC failure records failed instead of masquerading as "service not deployed" (which would have produced a misleadingly-empty bundle).
  • Added panic recovery around collectors, fixed timeout-reason fidelity in the manifest, and added unit coverage for --include-queries and the read-only guarantee.

Lower-severity items (key-name masking not catching inline connection-string credentials; double-SIGINT archive window) are documented as by-design/deferred in specs/003-collect-tool/opsmill-implement-report.md.

🤖 Generated with Claude Code


Summary by cubic

Add infrahub-collect, a read-only troubleshooting-bundle CLI for Infrahub on Docker Compose and Kubernetes, with logs, diagnostics, metrics, and opt-in backup/benchmark. Implements INFP-415, integrates full build/CI, and adds developer ADRs/guidelines with the spec archived.

  • New Features

    • create writes support_bundle_<timestamp>.tar.gz with a uniform layout and bundle_information.json manifest across Docker and Kubernetes.
    • Collects per-service logs (all replicas; .previous.log on Kubernetes), diagnostics (Neo4j, RabbitMQ, Redis, Prefect, server info/config/schema/env), and container metrics; secrets masked; per-collector timeouts.
    • Opt-in: --include-backup (delegates to existing backup; artifact path recorded) and --benchmark (uses INFRAHUB_BENCHMARK_IMAGE; writes benchmark.log/json; skipped if the image can’t be pulled).
    • Docker: per-replica exec/logs/stats and compose project discovery. Kubernetes: multi-replica and previous-container logs, metrics via kubectl top.
    • Build/CI: added to Makefile and flake.nix; CI builds all binaries and runs Docker/K8s e2e; no new Go module deps. Docs/dev: extracted ADRs/guidelines/knowledge under dev/, archived spec under specs/archive/, updated guides (Go 1.25; benchmark measures host resources; documented INFRAHUB_BENCHMARK_IMAGE).
  • Bug Fixes

    • Bounded exec/copy and pod resolution with precise timeout reasons; collector panics recovered so runs don’t abort.
    • Kubernetes replica discovery distinguishes “not deployed” from API/RBAC failures.
    • Fixed Docker combined-log pipe leak; partial failures recorded without losing collected data.
    • Safe fallbacks: Neo4j query-log enumeration when empty, Prefect events pagination capped; read-only guard enforced.
    • --include-backup: run with force=true to avoid unbounded running-tasks wait on busy instances; records a clear outcome.

Written for commit 5fd8210. Summary will update on new commits.

Review in cubic

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 3, 2026

Copy link
Copy Markdown

Deploying infrahub-ops-cli with  Cloudflare Pages  Cloudflare Pages

Latest commit: 5fd8210
Status:⚡️  Build in progress...

View logs

@fatih-acar
fatih-acar force-pushed the fac/collect-tool-docs branch 2 times, most recently from 5b773ed to 17b532f Compare July 9, 2026 08:19
Base automatically changed from fac/collect-tool-docs to main July 9, 2026 09:41
An error occurred while trying to automatically change base from fac/collect-tool-docs to main July 9, 2026 09:41
fatih-acar and others added 18 commits July 9, 2026 11:19
…ect-tool

Plan, Phase 0 research decisions, data model, CLI/bundle/manifest
contracts, and quickstart validation guide for the infrahub-collect
troubleshooting-bundle binary. Records agent-context tech entries in
AGENTS.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dual-lens critique report (verdict: proceed with updates). Applied
fixes: per-collector subprocess timeouts (E1), per-container pod log
handling (E2), normative masked-output list (E3), collect e2e CI
wiring (E4), disk-full/bundle-size behavior (P1), human-meaningful
replica log filenames (P2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
40 tasks across setup, foundational collector framework, three user
stories (k8s MVP, docker parity, include-backup), opt-in benchmark,
and polish/CI wiring.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 1 (Setup) of 003-collect-tool: thin Cobra entry point at
src/cmd/infrahub-collect mirroring infrahub-taskmanager, with the full
published CLI surface — persistent --output-dir (INFRAHUB_OUTPUT_DIR,
default ./infrahub_bundles) and create-local --log-lines /
--include-backup / --include-queries / --benchmark, all viper-bound.
The create RunE is a stub (validates --log-lines, returns a clear
not-implemented error) until the orchestrator lands (T013).

Build surface: infrahub-collect added to BINARIES in the Makefile and
as a third buildGoModule package + symlinkJoin entry in flake.nix
(go.mod untouched, vendorHash unchanged).

Tasks: T001-T004 (specs/003-collect-tool/tasks.md).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rator

Phase 2 (Foundational) of the 003-collect-tool feature (T005-T013):

- masking.go: key-name secret masking (password|secret|token|key,
  case-insensitive substring) for env dumps, Redis config pairs and
  RabbitMQ Erlang-term dumps (research R5)
- collect_manifest.go: BundleManifest/CollectorResult with normative
  JSON field names, manifestVersion 2026070200, bundle_information.json
  writer mirroring backup_metadata.go conventions (research R6)
- collect.go: Replica struct and narrow collectBackend seam (resolved
  from the active EnvironmentBackend via runtime type assertion so
  backend primitives can land in later tasks), CollectOptions, and the
  CollectBundle orchestrator: staging via os.MkdirTemp under the output
  dir, ordered collector loop with skip preconditions and non-fatal
  failures (FR-009), manifest finalized last, createTarball packaging,
  staging cleanup on success/failure/SIGINT/SIGTERM, final log with
  archive path and size (research R7)
- command_executor.go: context/timeout-bounded runCommandContext and
  runCommandPipeContext; timeouts surface as a distinguishable
  timeoutError whose message is exactly "timed out after <duration>"
  (research R2; 60s exec dumps, 5 min transfers)
- infrahub-collect create now builds CollectOptions from viper and
  calls CollectBundle (empty run plan until T025)
- table-driven tests for masking, manifest schema semantics, executor
  timeouts, and orchestrator outcomes with a fake collectBackend

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implement the collect-side primitives on KubernetesBackend (T014-T016):
ServiceReplicas enumerates one Replica per pod container with per-container
restart counts via kubectl jsonpath, ReplicaLogs streams kubectl logs through
the 5-minute timeout-bounded pipe, and Metrics captures kubectl top pods
under the 60s exec timeout with metrics-server absence surfacing as a normal
collector failure.

Add the environment-agnostic service-log collector (T017) in collect_logs.go:
one collector per canonical service writing per-replica files named per
data-model.md, .previous.log for restarted containers, skipped entries for
services that are not deployed, and partial-failure aggregation that keeps
already-written logs in the bundle.

Cover filename derivation, kubectl argument construction, jsonpath replica
parsing, and collector skip/failure semantics with table-driven unit tests
(T026).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implement the US1 diagnostics collectors (T018-T025):

- database-logs: CopyFrom Neo4j neo4j.log/debug.log; --include-queries
  copies the enumerated /logs directory including rotated query logs
- message-queue-status: rabbitmqctl queues/exchanges/bindings/connections/
  channels/status/environment dumps, Erlang-config masking on status and
  environment
- cache-status: redis-cli info/clients/config (masked)/slowlog/dbsize
- task-worker-state: prefect version/config (masked)/work-pool ls per
  replica; failed only when nothing could be collected
- task-manager-state: prefect work-pool/work-queue/flow-run/automation ls
  with a PREFECT_API_URL localhost fallback, plus recent events via the
  embedded collect_prefect_events.py script (paginates the events API,
  which caps pages at 50)
- server-info: version (also populates manifest infrahub_version),
  pip list, masked env, and /api/info|config|schema fetched inside the
  server container against INFRAHUB_INTERNAL_ADDRESS (config masked via
  a new JSON-aware masker)
- metrics: backend Metrics primitive into bundle/metrics/metrics.txt
- register the full ordered run plan in collectPlan with TODO hooks for
  the T033/T036 opt-in extras

Supporting changes:

- masking: widen the sensitive-key list from "password" to "pass"
  (catches requirepass, default_pass, anonymous_login_pass; FR-008
  allows widening) and add maskJSON for the API configuration dump
- backends: optional ExecContext (timeout-bounded exec, both backends)
  and ExecReplica (per-replica exec, Kubernetes) capabilities consumed
  by the diagnostics collectors with plain-Exec fallback
- collect_logs: extract replicaBaseName, shared with the per-replica
  task-worker directories

Smoke-tested against a live Docker Compose deployment: database,
message-queue, cache and task-manager collectors succeed end to end;
log/metrics/task-worker collectors correctly report the Docker backend's
missing collect primitives until US2 lands them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Helm chart labels infrahub-server pods with infrahub/service=server,
so none of the podSelectors match the canonical service name and
ServiceReplicas reported the service as not deployed, silently skipping
logs/infrahub-server in the bundle. Mirror the pod-name substring
fallback getPodForService already uses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add the collect_binary session fixture and run_collect helper (T027) and
the Kubernetes e2e test (T028): full collect against the vcluster+Helm
stack, asserting archive integrity, manifest conformance with
contracts/manifest.schema.json (manual schema checks; jsonschema is not
a test dependency), one log file per replica with task-worker scaled to
two, a .previous.log for an induced in-place container restart, bundle
layout per contracts/bundle-layout.md, and zero pod restarts or scale
events caused by the run (SC-003).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implement the collect-side backend seam on DockerBackend (US2, T029-T031):

- ServiceReplicas enumerates compose containers per service via
  `docker compose ps -a --format json` (NDJSON and array forms parsed by a
  pure helper), one Replica per container carrying the container name;
  stopped containers are included so a deployed-but-stopped service surfaces
  as a failed collector with its logs still captured (FR-009), and one-off
  `compose run` containers are excluded
- ReplicaLogs streams `docker logs --tail <n> <container>` through a new
  timeout-bounded combined-output pipe primitive: the Docker daemon demuxes
  container output onto stdout and stderr (Infrahub services log to stderr),
  so both streams merge into the log file; previous logs are rejected —
  Restarted is always false on Docker
- Metrics captures `docker stats --no-stream` over the project's running
  containers, names included
- ExecReplica targets one container by name so per-replica task-worker
  state works on scaled compose services

E2E (T032, tests/e2e/test_docker_collect.py, -m docker): full parity bundle
with schema-validated manifest against a compose stack scaled to two
task-workers, per-replica logs and state dirs, no .previous.log on Docker,
masked env dump with no plaintext secrets (FR-008), project selection with
and without --project mirroring the backup tool (US2 scenario 2),
--log-lines/INFRAHUB_LOG_LINES precedence in the manifest (FR-011), and the
degraded case: stopped cache -> exit 0 + failed cache-status entry while
its logs are still collected (FR-009/SC-005). Shared bundle assertions move
to tests/helpers/bundle.py, now used by the k8s e2e as well (SC-004 parity
asserted with the same code on both sides).

Tick T029-T032 in specs/003-collect-tool/tasks.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implement US3 of spec 003-collect-tool (T033-T035):

- New include-backup collector in collect_extras.go delegates to the
  existing CreateBackup unmodified with non-interactive
  `infrahub-backup create` defaults (no --force, all Neo4j metadata,
  task-manager DB included, no S3 upload, no sleep, no redaction, no
  encryption — research R10, Principle II). The collector runs last in
  the plan because the inherited backup behavior may stop/restart
  application containers.
- The produced archive stays a standalone file in the standard backup
  directory; its path is recorded in the manifest entry via a new
  optional CollectorResult field `artifact` (documented in
  contracts/manifest.schema.json). Backup failure records a failed
  manifest entry while the bundle is still produced (US3 scenario 2);
  an unset flag records skipped/"not requested".
- Unit tests cover success/failure/skipped outcome recording through a
  fake backup runner seam, the artifact JSON shape, and the
  directory-diff helper that locates the produced archive.
- Docker e2e gains an --include-backup case asserting the backup
  artifact next to the bundle, valid backup metadata inside it, the
  manifest artifact reference, exit 0, and that the backup is never
  embedded in the bundle archive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implement T036 (FR-013, research R11): the benchmark collector runs the
OpsMill benchmark image — registry.opsmill.io/opsmill/bench, ported from
the Python invoke bundle collect implementation (opsmill/infrahub
tasks/container_ops.py collect_benchmark) — as a transient resource the
tool creates and always removes: a named `docker run --pull always --rm`
container attached to the compose project network on Docker, a one-off
attached `kubectl run` pod in the namespace on Kubernetes, both bounded
by a dedicated 10-minute timeout and force-removed even when the run
times out or is interrupted.

Output is captured into bundle/benchmark/ as benchmark.log (raw) and
benchmark.json (parsed measures, shape-compatible with the Python
tool's report, with ANSI codes stripped). The image reference is
overridable via INFRAHUB_BENCHMARK_IMAGE for private registries.

An image pull or run failure is recorded in the manifest as skipped
with a warning reason — never failed — via a new collectSkipError seam
in the run loop, so air-gapped runs stay clean; the flag left unset
records skipped/"not requested" as documented. The e2e helpers now
expect the benchmark entry in every manifest.

Docs: add the INFRAHUB_BENCHMARK_IMAGE row to the configuration
reference (Vale and rumdl pass).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add infrahub-collect (troubleshooting-bundle collection) to the project
overview, architecture core-components, key design patterns, and Docker
Compose dependency notes, per constitution v1.1.0 Sync Impact Report.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The docker and k8s e2e jobs run pytest by marker (-m docker / -m k8s)
over tests/e2e, so test_docker_collect.py and test_k8s_collect.py are
already selected. Rename the misleading 'Build infrahub-backup binary'
step to reflect that make build produces all three binaries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- install-collect: bump build-from-source prerequisite Go 1.21 -> 1.25
  to match go.mod (two occurrences).
- collect-troubleshooting-bundle: correct the benchmark description; the
  OpsMill benchmark measures host resource requirements (disk IOPS,
  memory, single-core CPU) rather than generating load against the
  instance. Image-download and air-gapped-skip wording kept intact.
- Tick T037-T040 complete in tasks.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion

Address review findings that could hang a collect run or produce a
misleading empty bundle:

- Add a bounded contextCopier (CopyFromContext) on both backends and use
  it for the Neo4j log collector, so a wedged container/daemon can no
  longer hang on the context-free CopyFrom (FIX-1). The shared CopyFrom is
  left untouched for the backup tool.
- Distinguish "no pods matched" (errNoPodsMatched sentinel) from a real
  kubectl/cluster failure in GetAllPods, and map only the sentinel to a
  skipped collector in ServiceReplicas; cluster outages now surface as
  failed instead of masquerading as "service not deployed" (FIX-2).
- Recover collector run/skip panics into a recorded failure so one bad
  collector never aborts the run (FR-009, FIX-3).
- Wrap command timeouts with %w through collector aggregation so the
  manifest reason is the bare "timed out after <duration>" contract string
  rather than a verbose composite (FIX-4).
- Bound the collect path's version/internal-address lookups and the k8s
  pod-resolution kubectl calls the collect primitives depend on, via
  collect-local bounded variants; the shared getPodForService/GetAllPods
  stay unbounded for backup (FIX-5).
- Close the ReplicaLogs reader to fix a Docker combined-pipe fd leak
  (FIX-6); fall back to the default Neo4j log set when --include-queries
  enumeration is empty (FIX-7); write the Prefect events script's failure
  output to a .err.txt sibling instead of a mislabeled events.json (FIX-8);
  cap the events pagination loop so a truthy next_page cannot spin (FIX-9).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Drop the brittle exact top-level field-count assertion in the manifest
  JSON-shape test; the schema allows additionalProperties, so it would
  break on the next legal field. Keep the required-field presence checks
  (FIX-T1).
- Add a real-aggregation timeout test that drives the message-queue
  collector against a backend whose exec times out and asserts the manifest
  reason is exactly "timed out after 60s" (FIX-4 coverage).
- Cover the FR-004 --include-queries path (default vs enumerated set) and
  the empty-enumeration fallback via a CopyFromContext-recording backend
  (FIX-T2/FIX-7).
- Add a read-only guard test that runs the full plan through a backend
  failing the test on any Start/Stop, proving FR-010/SC-003 without a
  cluster (FIX-T3).
- Add collector panic-recovery tests and getAllPodsWith branch tests
  (sentinel vs real cluster failure) for FIX-3 and FIX-2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@fatih-acar
fatih-acar force-pushed the fac/collect-tool-implem-opip6 branch from 17e0d7c to ba7fba0 Compare July 9, 2026 11:21
…g-tasks wait

CreateBackup(force=false) runs waitForRunningTasks, an unbounded loop that
returns only when no Prefect tasks are running/pending. On a busy instance
(notably the Enterprise edition, with recurring background tasks and
automations that never fully drain) this hangs or aborts the backup, so
--include-backup produced no archive and the e2e failed at find_latest_backup.
The standalone backup e2e passes on Enterprise because it uses --force.

Collection is non-interactive and designed never to hang, so the
troubleshooting backup now uses force=true — skipping only the consistency
gate, not the backup's integrity guarantees.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…uidelines

Bootstrap the dev/ documentation system (adr/, knowledge/, guidelines/,
README index) and extract durable decisions from the completed
infrahub-collect spec:

- 7 ADRs (CLI shell-out, non-fatal timeout-bounded collectors, read-only
  collection, key-name masking choke-point, uniform bundle+manifest,
  --include-backup reuse, offline-by-default/opt-in benchmark)
- knowledge/infrahub-collect.md (CLI, bundle layout, manifest, backend seam)
- guidelines/collectors.md (conventions for future collectors)

Archive specs/003-collect-tool under specs/archive/ and mark it Extracted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@fatih-acar
fatih-acar marked this pull request as ready for review July 10, 2026 07:09
@fatih-acar
fatih-acar requested a review from a team as a code owner July 10, 2026 07:09
@fatih-acar
fatih-acar merged commit 2606ef0 into main Jul 10, 2026
7 of 8 checks passed
@fatih-acar
fatih-acar deleted the fac/collect-tool-implem-opip6 branch July 10, 2026 07:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant