Skip to content

export buildkit history and structured lifecycle telemetry at teardown (release/v2) - #128

Merged
piob-io merged 2 commits into
release/v2from
devin/1787595905-docker-build-observability
Sep 4, 2026
Merged

piob-io merged 2 commits into
release/v2from
devin/1787595905-docker-build-observability

Conversation

@piob-io

@piob-io piob-io commented Aug 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Targets release/v2 only. At teardown, the action exports the job's BuildKit build history and ships it — with structured lifecycle telemetry — to the vm-agent's ReportDockerBuild RPC over the shared v2 agent client (src/reporter.ts, BLACKSMITH_AGENT_ADDR discovery). The guest ships raw bytes; all semantic parsing lives host-side in the fa agent so BuildKit format drift is absorbed by an agent deploy, not an action release.

What ships

  • BuildKit history records: Control.ListenBuildHistory({EarlyExit:true}) streams the job's BuildHistoryRecords. Each record is decoded as a typed protobuf-es message only to read Ref (for post-export deletion), CompletedAt (incomplete flag), and the trace descriptor, and to clear logs; it is then re-serialized and shipped opaquely (protobuf-es preserves unknown fields across the round trip, so fields newer than our vendored proto still reach the agent). Exported records are deleted via Control.UpdateBuildHistory(Delete:true) so telemetry state is not committed into the sticky disk.
  • Solve trace attachments: fetched from buildkitd's content store via Content.Read(digest), size-capped (2 MiB per trace; oversized → shipped without trace, marked truncated).
  • Runner step timeline: newest _diag/Worker_*.log read verbatim, tail-capped at 1 MiB; parsing is agent-side.
  • Lifecycle facts: builder mode + fallback reason, commit decision + skip reason, buildctl du -v cache breakdown (per-mount sizes), fs used/size, hotload / buildkitd-ready / buildkitd-shutdown timers, SIGKILL flag, history export-timeout and history-delete-failed flags.

v2 semantics

  • integrityOutcome = SKIPPED, integrityDurationMs = 0: v2 has no bbolt integrity check.
  • pruneTriggered = false, pruneBytes = 0: v2 relies on native GC; there is no explicit cache prune at teardown. History-record deletion is tracked separately (historyPruneFailed).
  • Commit decision follows v2's server-driven flow: STEP_FAILURES / AMBIGUOUS / SIGKILL / CLEANUP_ERROR / NO_EXPOSE map onto the existing CommitSkipReason values; a successful commitStickyDisk call records CommitDecision.REQUESTED. The v2-only outcomes use the dedicated CommitSkipReason values from the bumped @buf/blacksmith_vm-agent.bufbuild_es package: mount-time commitEarlyDenyReason and PrepareCommit shouldCommit=false → SERVER_DECLINED; a pre-commit hook asking to skip → HOOK_SKIPPED. Against an agent that predates these enum values the row still inserts (commit_decision=skipped, empty skip reason); an agent without ReportDockerBuild returns Unimplemented, which is treated as unsupported and skipped.
  • Main-step timings (hotload, buildkitd ready) and builder mode/fallback reason persist through state-helper since main and post are separate processes.

Chunked reporting

A whole-job report can exceed the vm-agent's default 4 MiB gRPC MaxRecvMsgSize (a 24-build job produces a ~6 MB message rejected with resource_exhausted). reportDockerBuild splits builds across multiple unary ReportDockerBuildRequest calls via chunkBuilds, with a ~3 MiB per-request budget covering timeline + record + trace bytes:

  • the runner step timeline ships in every chunk (workflow-step attribution is per-request server-side);
  • the lifecycle message ships only in the first chunk (the agent emits lifecycle metrics per request carrying it);
  • every chunk carries at least one build, so a single build at the per-record/trace caps (~2.5 MiB) can slightly exceed the budget while staying under the 4 MiB limit;
  • each chunk gets its own report timeout; the whole path stays fail-soft (telemetry never fails the customer job, and an agent without the RPC is treated as unsupported and skipped).

Protos

BuildKit control.proto (+ deps) and containerd content.proto are vendored verbatim from pinned upstream releases (buildkit v0.32.2, containerd v2.3.4, googleapis pinned commit) via scripts/fetch-protos.sh and generated with buf generate → src/gen/ (ignored by eslint). stickydisk.proto types come from the @buf/blacksmith_vm-agent.bufbuild_es package release/v2 already depends on (which includes ReportDockerBuild). Generation is dev-time only: pnpm generate → pnpm build → checked-in dist/. Nothing is fetched or compiled during an action run, and no hand-authored proto content or hand-rolled wire parsing remains.

Ordering

Post-step teardown: buildctl du -v snapshot → history export → history-record delete → buildkitd shutdown → fs usage → unmount → commit/skip decision → chunked report.

Identity

The guest sends only expose_id, vm_id, git sha/branch, and build facts; keys, entity, run/job attribution are stamped host-side by the vm-agent.

Companion PRs: agent-side parsing/insertion FastActions/fa#5232 + OTLP JSON trace parser FastActions/fa#5277 (both merged); ClickHouse schema useblacksmith/web#11965 (merged); v2 skip reasons FastActions/fa#5441 (merged).


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Link to Devin session: https://app.devin.ai/sessions/0c6ebc2a6de54933bebe821e21ab8b76
Open in Devin Desktop: https://app.devin.ai/desktop/session/0c6ebc2a6de54933bebe821e21ab8b76?variant=devin
Requested by: @piob-io

@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@piob-io
piob-io marked this pull request as ready for review August 24, 2026 21:15
@piob-io
piob-io marked this pull request as draft August 24, 2026 21:19
@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/1787595905-docker-build-observability branch from 5ce4997 to 303b922 Compare August 24, 2026 21:52
@piob-io
piob-io marked this pull request as ready for review August 24, 2026 21:58
@piob-io
piob-io requested a review from ajwerner August 26, 2026 12:57
Comment thread src/main.ts
Comment on lines +632 to +635
// Export this job's build history (raw record + trace bytes) and delete
// the records afterwards so history.db and its content-store attachments
// never get committed to the sticky disk. Must happen before shutdown.
const builds = await exportBuildHistory(buildkitdAddr, lifecycle);

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.

is this valid? The sticky disk as a block io device doesn't know about which logical files have been deleted, right?

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.

Right — the block device has no notion of file deletion, so residual bytes can remain in freed blocks after commit; we don't claim physical scrubbing. What the delete does guarantee is filesystem-level: it happens via buildkitd's UpdateBuildHistory{Delete} API (BoltDB record delete + content-store blob unref) before buildkitd shutdown → integrity check → unmount → commit, so the committed filesystem no longer references those records. The purpose is (1) history.db doesn't grow unboundedly across runs and (2) the next job's export doesn't re-export prior jobs' records — not data erasure. I've reworded the comment to say exactly that (was misleadingly phrased as "never get committed").

@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/1787595905-docker-build-observability branch from 303b922 to 87c7d43 Compare August 26, 2026 15:07
@ajwerner

Copy link
Copy Markdown
Contributor

@devin do we bound the size of the report we generate in terms of bytes? I think we need a way to hard bound it somewhere around 10s of MiB and under 128 MiB

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Yes — the report is hard-bounded to ~10 MiB worst case, enforced at assembly time in src/build-telemetry.ts:

  • MAX_RECORD_BYTES = 512 KiB per history record (oversized records ship without payload, marked truncated)
  • MAX_TRACE_BYTES = 2 MiB per solve-status trace (dropped + truncated if over)
  • MAX_TOTAL_PAYLOAD_BYTES = 8 MiB running cap across all records+traces combined — once hit, remaining builds ship metadata-only
  • MAX_TIMELINE_BYTES = 1 MiB tail-cap on the runner _diag step timeline

Everything else in the report (lifecycle facts, du totals, per-cache-mount rows) is small fixed-size fields, so the whole ReportDockerBuild request tops out around 9–10 MiB and is typically a few hundred KiB. Well under the 128 MiB ceiling and inside the "10s of MiB" target.

@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/1787595905-docker-build-observability branch from 87c7d43 to b357178 Compare August 26, 2026 16:50
@devin-ai-integration
devin-ai-integration Bot changed the base branch from main to release/v2 September 4, 2026 14:23
@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/1787595905-docker-build-observability branch from bc50154 to 0ccc66c Compare September 4, 2026 14:23
@devin-ai-integration devin-ai-integration Bot changed the title export buildkit history and structured lifecycle telemetry at teardown export buildkit history and structured lifecycle telemetry at teardown (release/v2) Sep 4, 2026
@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/1787595905-docker-build-observability branch from 0ccc66c to 65828a3 Compare September 4, 2026 14:30
piob-io and others added 2 commits September 4, 2026 15:04
At post-step teardown, export the job's raw BuildKit history records (plus
solve-status traces, logs excluded, size-capped) via ListenBuildHistory,
delete them so history.db and its content-store attachments are never
committed to the sticky disk, and ship them with the runner _diag step
timeline and structured lifecycle facts (builder mode/fallback reason,
commit decision/skip reason, buildctl du totals incl. per-cache-mount, fs
usage, setup/shutdown timings) to the vm-agent's ReportDockerBuild RPC over
the shared agent client (BLACKSMITH_AGENT_ADDR discovery). docker_build_ids
are issued host-side; all reporting is fail-soft with bounded timeouts and
never fails the customer job, and is skipped silently when no agent is
reachable or it predates the RPC.

BuildKit control and containerd content protos are vendored verbatim from
pinned upstream releases (scripts/fetch-protos.sh) and compiled with
protoc-gen-es into src/gen; generated code is excluded from eslint.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
A whole-job report can exceed the vm-agent's default 4 MiB gRPC
MaxRecvMsgSize (a 24-build job produces a ~6 MB message that is rejected
with resource_exhausted, dropping all telemetry). Builds are split across
multiple unary ReportDockerBuild calls with a ~3 MiB per-request budget
covering the runner step timeline plus record and trace bytes. The
timeline ships in every chunk (workflow-step attribution is per-request
server-side); the lifecycle message ships only in the first chunk so the
agent never double-emits lifecycle metrics. Per-record/trace caps and
fail-soft behavior are unchanged, and each chunk gets its own report
timeout.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/1787595905-docker-build-observability branch from 65828a3 to fb31cd1 Compare September 4, 2026 15:05
@piob-io
piob-io merged commit e20544e into release/v2 Sep 4, 2026
13 checks passed
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.

2 participants