Skip to content

fix(transform): don't panic on absent optional OTLP fields - #17

Open
vito wants to merge 1 commit into
mainfrom
transform-nil-optional-fields
Open

fix(transform): don't panic on absent optional OTLP fields#17
vito wants to merge 1 commit into
mainfrom
transform-nil-optional-fields

Conversation

@vito

@vito vito commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Three functions in transform.go dereferenced a pointer that the OTLP proto marks optional, so converting a perfectly valid payload that simply omits the field panicked with a nil pointer dereference.

  • ResourceFromPB read pb.Attributes directly. resource is optional on ResourceSpans, ResourceLogs and ResourceMetrics — "if this field is not set then no resource info is known" — so a payload without one crashed. This reached users through SpansFromPB (via readOnlySpan.Resource()), ReexportLogsFromPB and ResourceMetricsFromPB. Now uses the generated nil-safe getter, pb.GetAttributes().
  • LogValueFromPB switched on v.Value, which panics when v is nil. body is optional on LogRecord, and an attribute-only record — one carrying its data in attributes rather than log text — is legal and common; ReexportLogsFromPB calls LogValueFromPB(rec.GetBody()) unconditionally. Now returns log.StringValue(""), which is what an absent body means to every consumer and matches what a body explicitly set to "" already produced. This also covers logKeyValue, which passes v.GetValue().
  • attrValue had the identical bug on the span/attribute side, hit by a KeyValue whose value field is absent. Fixed the same way, returning an empty string value rather than falling into the default branch's UNHANDLED ATTR TYPE logging — absent is not unhandled, it is absent.

All three guards sit ahead of the type switch, so no payload that does set these fields changes behaviour. This follows the existing pattern in the file: InstrumentationScopeFromPB and StatusCodeFromPB already return a zero value for nil.

Tests

Three regression tests in transform_test.go, matching the existing testify style:

  • TestSpansFromPBNilResourceResourceSpans{Resource: nil} with one span, asserting .Resource() returns a non-nil resource with no attributes.
  • TestReexportLogsFromPBNilResourceAndBody — nil resource, nil body, two attributes; asserts the exported record comes through with an empty body and both attributes intact.
  • TestAttributesFromProtoNilValue — a KeyValue with a nil Value converts to an empty STRING attribute with its key preserved.

Each one panics on its own with the fix reverted. Reverting only the LogValueFromPB guard still makes the log test panic, confirming it covers the nil-body case independently rather than just tripping the resource bug first.

go test ./... and go vet ./... both pass.

🤖 Generated with Claude Code

ResourceFromPB, LogValueFromPB and attrValue each dereferenced a pointer
that the OTLP proto marks optional, so a valid payload that simply omits
the field crashed the conversion with a nil pointer dereference.

resource is optional on ResourceSpans, ResourceLogs and ResourceMetrics
("if this field is not set then no resource info is known"), reaching
users through SpansFromPB, ReexportLogsFromPB and ResourceMetricsFromPB.
body is optional on LogRecord, so an attribute-only record — one that
carries its data in attributes rather than log text — panicked as well.
value is optional on KeyValue, which hit the span attribute path.

Absent now converts to a zero value rather than crashing: an empty
resource, and an empty string for a missing body or attribute value,
which is what an explicitly empty one already produced. The nil guards
sit ahead of the type switches, so payloads that do set these fields are
unaffected. Absent also no longer falls into attrValue's "UNHANDLED ATTR
TYPE" branch, since absent is not unhandled.

Signed-off-by: Alex Suraci <suraci.alex@gmail.com>
@vito

vito commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Corroboration from the consumer side, in case it's useful for the review: both of the first two were hit as real SIGSEGVs (not hypotheticals) while building a trace-import path in dagger/dagger, feeding a hand-built OTLP capture through SpansFromPB and ReexportLogsFromPB:

  • ResourceFromPB — via readOnlySpan.Resource(), on a ResourceSpans with no resource.
  • LogValueFromPB — via ReexportLogsFromPB, on an attribute-only LogRecord with no body.

Worth noting the second one isn't only reachable from new code: engine/server/telemetry.go's POST /v1/logs handler passes a decoded request straight to ReexportLogsFromPB, so any client posting a body-less record panics that handler today. Every in-repo producer sets an explicit empty-string body, which is presumably why nobody has tripped it.

attrValue is the one I hadn't spotted, and it looks like the sharpest of the three: AttributesFromProto already skips nil elements but passes a.Value through unchecked, so a KeyValue with a key and no value — exactly what a re-encoder emits for a value it can't represent — reaches the deref. Returning an empty value rather than falling into UNHANDLED ATTR TYPE also seems right; "absent" and "unhandled" deserve different answers.

vito added a commit to dagger/dagger that referenced this pull request Aug 13, 2026
The nil Resource / nil Body dereferences the import guards against are fixed
at the decode boundary by dagger/otel-go#17, which also caught a third on the
span-attribute side (attrValue, reachable because AttributesFromProto skips
nil elements but passes a nil Value through). Mark the local guards as a
stopgap to delete with the otel-go bump, so they do not outlive their reason,
and record in §13.4 that the panic was never resume-specific: the engine's own
POST /v1/logs handler feeds ReexportLogsFromPB directly.
vito added a commit to dagger/dagger that referenced this pull request Aug 13, 2026
Records what building the fetch ratified and what it changed: the sink
interface that keeps internal/cloud transport-only, the sequential-streams
decision and the race-detector evidence behind it, the seal's position between
the span and log streams, the three places the reference implementation's
error handling was wrong for a restore, and the two auth/URL bugs fixed on the
way. Also notes that dagger/otel-go#17 has not merged, so slice 4's stopgap
guards stay, and that §12's two curls still could not be run — nothing is
deployed to curl — which leaves the fake server's fidelity as the slice's
whole risk.
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