Skip to content

A-1591: Add OTLP job log export - #4121

Merged
zhming0 merged 2 commits into
mainfrom
ming/a-1591-otlp-job-logs
Jul 29, 2026
Merged

A-1591: Add OTLP job log export#4121
zhming0 merged 2 commits into
mainfrom
ming/a-1591-otlp-job-logs

Conversation

@zhming0

@zhming0 zhming0 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Description

Continuation of #3979 (thanks @catkins). That branch is squashed into the first commit here; see the original PR for the full design discussion and verification notes. The second commit reworks the correlation design, per the which span owns a log line design note.

Adds an opt-in job log OTLP sink. When --job-logs-otlp / BUILDKITE_JOB_LOGS_OTLP is enabled, the bootstrap emits job output as OpenTelemetry log records using the existing OTLP exporter environment configuration. This is an additional sink: the normal Buildkite job log stream is unchanged.

What each record carries

  • Body: one job log line, with the same [REDACTED] markers as the customer-facing job log.
  • Trace correlation: the trace ID and the span ID of the nearest enclosing phase or hook span at the time the line is emitted, falling back to the root job span. Records within a phase deliberately carry the phase or hook span, not child op spans such as git.clone. With tracing disabled, records are emitted without correlation.
  • Base attributes: org, pipeline, branch, queue, agent, build and job identifiers.
  • Timestamp: the arrival of the line's first byte, matching the start-of-line semantics of the job log's ANSI timestamper.

We dropped the per-record buildkite.phase and buildkite.hook.* attributes from the earlier iteration. The agent only defines plugin, checkout and command as phases, and inventing phase names for other stages risks exporting wrong metadata. Richer attribute propagation can follow in a dedicated change.

How it differs from the original PR

  • Child-process output and bootstrap narration (section headers, prompts, comments, warnings) are mirrored into OTLP by two tees downstream of the existing job log redactors. Records inherit the job log's redaction by construction, including secrets split across writes and secrets added mid-job via the Job API, so the parallel OTLP redaction pipeline (needle syncing, cross-command partial-match state, flush-boundary alignment) is deleted.
  • The shell OutputInterceptor plumbing is removed entirely.
  • Correlation moved from per-command context plumbing to a single emit context that the executor updates at span boundaries (defer e.otlpLogSpan(ctx)() right after starting a phase or hook span).

Also addresses review feedback from #3979: the job-logs-otlp bootstrap flag is no longer hidden (comment), and teeWriter lives in its own file (comment).

Testing

  • Tests have run locally (with go test ./internal/... ./clicommand/ ./agent/ and go test -race ./internal/job/). Buildkite employees may check this if the pipeline has run automatically.
  • Code is formatted (with go tool gofumpt -extra -w .)
  • golangci-lint run clean on touched packages

Squashed from #3979.

Adds an opt-in job log OTLP sink. When --job-logs-otlp /
BUILDKITE_JOB_LOGS_OTLP is enabled, the bootstrap emits job output as
OpenTelemetry log records using the existing OTLP exporter environment
configuration, in addition to the normal Buildkite job log stream.

- Bootstrap-only architecture: single emitter with structured phase/hook
  metadata and native trace correlation when OTel tracing is enabled.
- Child-process output and bootstrap control output (section headers,
  prompts, comments, warnings) are both mirrored, matching the
  downloadable Buildkite job log.
- OTLP records are redacted with the same live secret needle set as the
  job log, including secrets split across writes and secrets added
  mid-command via the Job API.

Co-authored-by: Chris Atkins <chris.a@buildkite.com>
@socket-security

socket-security Bot commented Jul 27, 2026

Copy link
Copy Markdown

@DrJosh9000 DrJosh9000 added the feature New user-facing feature! label Jul 27, 2026
@zhming0
zhming0 force-pushed the ming/a-1591-otlp-job-logs branch from 0ad8e82 to a61626a Compare July 28, 2026 01:01
Comment thread internal/job/executor.go Outdated
Comment thread internal/job/executor.go Outdated
@zhming0
zhming0 force-pushed the ming/a-1591-otlp-job-logs branch 4 times, most recently from 8e510ef to bf02f2f Compare July 28, 2026 04:30
@zhming0
zhming0 marked this pull request as ready for review July 28, 2026 05:17
@zhming0
zhming0 requested review from a team as code owners July 28, 2026 05:17
@zhming0
zhming0 requested a review from catkins July 28, 2026 05:17

@buildsworth-bk-app buildsworth-bk-app Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This touches the job-log export and secret-redaction path, so I'm leaving this as a comment rather than approving.

Want to dig deeper?

Paste this into your agent to explore the findings from this review's Buildkite build:

Download the buildsworth logs from build 9086, then answer my questions about the findings.

Install the reading-buildsworth-logs skill to run this.

About buildsworth

Model: gpt-5.6-sol with xhigh thinking.

How to request a review: Comment @buildsworth-bk review on the PR, or request buildsworth-bk as a reviewer.

Risk labels (how buildsworth classifies risk) — buildsworth classifies risk itself from the diff. To let it approve, grant L2 approval by mentioning @buildsworth-bk (see L2 approval grant):

  • L1 — Low risk (dep bumps, docs/copy, lockfiles, small presentational fixes). buildsworth may approve by default.
  • L2 — Standard risk (new UI, additive API fields, refactors). Approved only with an L2 grant; otherwise comment-only.
  • L3 — High risk (auth, migrations, payments, secrets, perf-critical paths). Human review always required.

Comment thread internal/job/otlp_job_logger.go
@zhming0
zhming0 force-pushed the ming/a-1591-otlp-job-logs branch from bf02f2f to 32fcbea Compare July 28, 2026 06:16
Comment thread internal/job/otlp_job_logger.go Outdated
), provider, otlpJobAttributes(e)), nil
}

func newOTLPJobLoggerWithLogger(log otellog.Logger, provider *sdklog.LoggerProvider, baseAttrs []otellog.KeyValue) *otlpJobLogger {

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.

Why does newOTLPJobLogger take a context arg, but this function does not?

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.

It's because this function isn't context-aware. The function exist to reduce a bit of duplicated code. Maybe it's not worth the indirection 🤔

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.

It stores context.Background() into the ctx field. That could be passed in?

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.

True, done. I also added a comment to explain that the passed in ctx isn't about lifetime control.

@DrJosh9000 DrJosh9000 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.

Happy to ship it and see how it goes

@buildsworth-bk-app buildsworth-bk-app Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I found no new issues in this pass. This remains on the job-log export and secret-redaction path, so it still needs human sign-off.

Want to dig deeper?

Paste this into your agent to explore the findings from this review's Buildkite build:

Download the buildsworth logs from build 9132, then answer my questions about the findings.

Install the reading-buildsworth-logs skill to run this.

Address review feedback and simplify the correlation design on top of
the squashed base:

- Correlate exported log records with the current phase/hook span only:
  each record carries the trace ID and the span ID of the nearest
  enclosing phase or hook span at emit time. Per-record attributes such
  as buildkite.phase and buildkite.hook.* are dropped for this cut;
  "plugin", "checkout" and "command" are the only phases the agent
  defines, and inventing phase names for other stages risks exporting
  wrong metadata. Richer span/attribute propagation can follow in a
  dedicated refactor.
- OTLP receives the same redacted bytes as the Buildkite job log, teed
  downstream of the existing stdout and logger redactors (stdoutTee and
  stderrTee), replacing the parallel shell output interceptor and
  second redaction pass.
- Unhide the bootstrap --job-logs-otlp flag and align its usage text
  with agent start.
- Extract teeWriter into its own file, remove the flushOutputRedactors
  wrapper.
@zhming0
zhming0 force-pushed the ming/a-1591-otlp-job-logs branch from 32fcbea to 2f6464b Compare July 28, 2026 06:53

@buildsworth-bk-app buildsworth-bk-app Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I found one non-blocking edge case in OTLP body encoding below. This remains on the job-log export and secret-redaction path, so I'm leaving a comment rather than approving.

Want to dig deeper?

Paste this into your agent to explore the findings from this review's Buildkite build:

Download the buildsworth logs from build 9170, then answer my questions about the findings.

Install the reading-buildsworth-logs skill to run this.

record.SetObservedTimestamp(now)
record.SetSeverity(otellog.SeverityInfo)
record.SetSeverityText("INFO")
record.SetBody(otellog.StringValue(line))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Non-blocking: line comes directly from arbitrary process bytes, but StringValue is serialized as an OTLP protobuf string, whose marshaling rejects invalid UTF-8. A command that emits non-UTF-8 bytes—or a valid >64 KiB line split through a multibyte rune—will therefore lose those OTLP records while the Buildkite log keeps the bytes. Could we make the body valid UTF-8 and avoid splitting valid runes before emission?

@zhming0
zhming0 merged commit 60a7c7a into main Jul 29, 2026
4 checks passed
@zhming0
zhming0 deleted the ming/a-1591-otlp-job-logs branch July 29, 2026 00:38
This was referenced Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New user-facing feature!

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants