Skip to main content

Buildpacks RFC 0131: What Build Observability Actually Requires Beyond a Streaming Log

9 min readDora NodaDora Noda
Share
On this page

A build fails at 2am, and the only artifact you have is a wall of scrollback text. Somewhere in there is the answer to "which buildpack broke, on which line, after how long" — but finding it means eyeballing a log stream, not querying a fact. Cloud Native Buildpacks' RFC 0131, targeted for Q3 2026 as part of the project's push toward a 1.0 release, proposes to replace that eyeballing with structured, queryable data: OpenTelemetry spans written to the build filesystem, one per lifecycle phase and one per buildpack.

The short version: RFC 0131 turns a build into a set of named, timed spans — detect, build, export, and one per buildpack — instead of a stream of text a human has to read start to finish. An operator (or an agent debugging a failed deploy) gets back "the build phase for heroku/nodejs-engine@2.1.0 took 41 seconds and the restore phase before it took only 0.3 seconds" instead of scrolling for the line that says so. That distinction — a fact you can query versus a transcript you have to read — is the entire proposal, and it's worth being precise about what it actually specifies before getting to what it would take to run it on a Cluster API-managed Paketo/kpack pipeline today, ahead of the spec even shipping.

What a Build Pipeline Gives You Today

Take kpack, the Kubernetes-native controller that runs Cloud Native Buildpacks builds as pods — the piece a Cluster API-managed fleet would use to turn a git push into a container image. kpack implements each lifecycle phase (detect, analyze, restore, build, export) as its own init container inside the build pod. Visibility into what happened is kubectl logs -c <container>, or the kp build logs command that wraps it — a text stream, one container at a time, in the order the phases ran.

That's fine for a human watching a build in real time. It falls apart for three things a git-push PaaS actually needs:

  • "Which phase took the time?" — you can time it yourself by watching timestamps scroll past, but nothing hands you {phase: "build", duration_ms: 41230} as data.
  • "Did the cache help?" — the restore phase either pulls forward cached layers from the previous build or it doesn't, and today the only way to know is to read its log output and infer from what it printed (or didn't).
  • "Can an agent act on this?" — an MCP tool backing a deploy-debugging agent can pipe log text into a model and ask it to summarize, but that's paying an LLM to parse structure that should have existed in the first place.

None of that requires a new build system. It requires the build to hand back facts instead of a transcript.

What RFC 0131 Actually Specifies

RFC 0131 doesn't add a new build phase or a network dependency — no OTel collector for lifecycle or a buildpack to talk to. Instead, when the lifecycle binary is invoked with an opt-in --telemetry flag, each phase writes its own trace to a fixed path on the build filesystem, in OpenTelemetry's File Exporter Format — one JSON Lines file per phase:

text
<layers>
└── tracing
    ├── buildpacks
    │   ├── some-id@some-version-detect.jsonl
    │   └── some-id@some-version-build.jsonl
    ├── extensions
    │   └── some-id@some-version-detect.jsonl
    └── lifecycle
        ├── analyze.jsonl
        ├── build.jsonl
        ├── detect.jsonl
        ├── export.jsonl
        ├── extend.jsonl
        └── restore.jsonl

Two kinds of trace live here, and they answer different questions:

  • Lifecycle traces (/layers/tracing/lifecycle/*.jsonl) are buildpack-agnostic: how long did detect, restore, build, and export each take, which buildpacks were detected, in what order. This is the RFC's own example span:
json
{
  "name": "buildpack-detect",
  "startTimeUnixNano": "1581452772000000321",
  "endTimeUnixNano": "1581452773000000789",
  "events": [{ "timeUnixNano": "1581452773000000123", "name": "detect-pass" }],
  "attributes": [{ "key": "buildpack-id", "value": { "stringValue": "heroku/nodejs-engine" } }]
}
  • Buildpack traces (/layers/tracing/buildpacks/{id}@{version}-{phase}.jsonl) are buildpack-specific, emitted by the buildpack's own code during detect or build — how long it took to download a language runtime, which version got selected, and anything else the buildpack author chooses to attach as a span attribute or event.

That second category is where the RFC's motivating questions live verbatim — the RFC itself lists "How long does it take to download node_modules?" and "Which versions of Go are being installed?" as the kind of thing this is for. It does not define a built-in cache-hit/cache-miss boolean field; that's not in the spec text. But the mechanism gets you there two ways, and it's worth being precise about which is which:

  1. Timing inference, no buildpack changes needed. The restore phase in the CNB lifecycle is specifically the step that reuses cached layers from a previous build when a layer's metadata still matches — it's already one of the six lifecycle phases with its own restore.jsonl trace in the file tree above. A restore phase that took 300ms restored layers from cache; one that took 30 seconds rebuilt them from scratch. You don't get a boolean, but you get the timing signal a dashboard or an agent can threshold on, for free, from lifecycle-level data alone.
  2. Explicit signal, buildpack-author opt-in. Because a buildpack's own spans support arbitrary events (the RFC's example already shows a detect-pass event attached to a span), a buildpack author can emit a named event like layer-restored or layer-rebuilt directly from their build logic — turning inference into an explicit fact, at the cost of the buildpack needing to instrument it.

Two more things matter for a platform operator deciding whether to wire this up. First, CNB_OTEL_TRACEPARENT lets a platform pass in a W3C traceparent so generated spans inherit the platform's own trace-id — a build shows up as a child span in whatever tracing backend already ingests the rest of the platform's telemetry, not an island. Second, the RFC is explicit about what these files must never contain: no PII (usernames, emails, IP addresses), and no business-sensitive data (passwords, access keys, the resulting image name, or the source repo name). It's opt-in, local-file-only telemetry — nothing "phones home" unless the platform operator chooses to ship the .jsonl files somewhere.

Where This Actually Stands

"Q3 2026" is a roadmap target, not a shipped feature, and it's worth saying plainly: as of this writing, RFC 0131 is marked Approved in the RFC repository, but the tracking issue that closes when implementation lands is still open, and so is the lifecycle implementation issue that would add the --telemetry flag and the trace-writing behavior to the lifecycle binary itself. Nothing here has shipped in pack or lifecycle yet — the spec text exists, the code doesn't.

One more thing worth clearing up, because it's a genuine source of confusion: Paketo already ships an opentelemetry buildpack, and it has nothing to do with RFC 0131. That buildpack instruments the deployed application's runtime — it contributes a Java/Node/PHP OpenTelemetry agent so the running app emits APM traces once it's live. RFC 0131 instruments the build itself — the minutes between git push and a runnable image, before the application has started a single request. Same underlying tracing standard, two completely different phases of a deploy's lifecycle, and easy to conflate if you only skim the name.

What a Cluster API-Managed Paketo/kpack Pipeline Would Need to Expose This Before 1.0 Ships

Here's the part that doesn't require waiting on upstream: RFC 0131's entire design is "an opt-in flag plus files on a shared filesystem," specifically so that a pack build running on a laptop with no OTel collector in sight still works. That design choice is what makes it implementable ahead of lifecycle shipping --telemetry natively — because kpack already has the shared filesystem RFC 0131 needs, for a different reason.

kpack's Build custom resource already runs detect, analyze, restore, build, and export as separate init containers sharing one pod, which means they already share one filesystem. Getting RFC 0131-shaped data out of that pipeline today, without the flag existing yet, means:

  1. A wrapper around each lifecycle invocation that times the phase and, for now, hand-writes the same span shape the RFC specifies — {name, startTimeUnixNano, endTimeUnixNano, attributes: [{key: "buildpack-id", ...}]} — to /layers/tracing/lifecycle/{phase}.jsonl. This is a stopgap that goes away the day lifecycle --telemetry ships and does it natively; the point is the shape of the output doesn't have to change when that happens, because it's copying the RFC's own format.
  2. A sidecar or final init container that reads the accumulated .jsonl files off the shared volume before the build pod terminates, and either forwards them to an OTel backend or writes a summarized status onto the kpack Build resource's own status fields — turning "read kubectl logs -c build" into "read a structured field on a Kubernetes object."
  3. An MCP tool surface on top of that structured data, so a deploy-debugging agent asks a specific question — "why did the last build fail, and where did the time go" — and gets back {phase: "build", buildpack: "heroku/nodejs-engine@2.1.0", duration_ms: 41230, restore_duration_ms: 30412} instead of a blob of log text it has to parse itself. The restore_duration_ms field is the cache signal from the section above, already shaped for a threshold check — 30 seconds of restore time on a small dependency set is a cache miss whether or not any buildpack bothered to emit an explicit event for it.

None of this needs a network path to an OTel collector, no firewall rules between the build pod and anywhere else, and no coordination with the seven-plus repos (spec, lifecycle, pack, libcnb, docs) the tracking issue lists as needing to ship before the RFC counts as "Implemented." It needs a shared volume kpack already has, and a reader for a file format the RFC has already published in full — which is exactly the gap between "the spec exists" and "the spec ships everywhere," and exactly where a platform operator running its own build pipeline has more room to move than a spec that has to get seven projects to agree on a release timeline.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, with the build pipeline running Cluster API and kpack/Paketo underneath instead of a black box. Star the repo on GitHub or deploy your first app today.

Sources

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex