Пример
go.opentelemetry.io/otel/trace v1.45.0: SpanFromContext never returns nil on untraced contexts but yields invalid non-recording spans whose IDs format to 32-character and 16-character zero strings rather than empty strings, RecordError does not set error status, and NewSpanContext drops all fields if either TraceID or SpanID is zero
Проверенный пример — golang go.opentelemetry.io/otel/trace v1.45.0: SpanFromContext never returns nil on untraced contexts but yields invalid non-recording…
sha256:2def692d5f3bfcef049fe24c06470937e1e7fafec85ed2219eb5c0dd894b2597
Эта сеть предлагает одно: образец, который собирается. Она запустила его в песочнице и сохранила подписанную квитанцию. Она ничего не оценивает и ничего не гарантирует — собирается ли тот же код у вас, она не измеряла.
Сколько различных ключей подписи подали пройденную квитанцию контракта. Один — только автор; больше одного — значит, кто-то ещё тоже собрал. Ключ создаётся сам и не имеет зарегистрированной личности, поэтому считаются ключи, а не люди.
MIT-0
Свидетельства выполнения
Заявленное окружение и подписанные запуски разделены, чтобы вы точно видели, что этот образец запускал и где.
- Основа свидетельства
- Подписанный контракт пройден
- Квитанции проверки
- 3
- Ключи подписи, собравшие его
- 2
Заявленная среда
go linux x64 go go gomod
Среды запусков проверки
| Окружение | Контракт | Этапы | Запуск |
|---|---|---|---|
| go 1.26 · linux alpine/x64 · docker ed25519:d91480838ac982c9 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · golang@1 |
2026-08-16 |
| go 1.26 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · golang@1 |
2026-08-18 |
| go 1.26 · linux alpine/x64 · docker ed25519:c1973797be207ac4 | FAIL | compile:SKIPPED · contract:FAIL · load:SKIPPED · resolve:PASS CONTAINER_RUN · golang@1golang:1.26-alpine@sha256:28d89ee9cc0f… |
2026-09-07 |
Кейс
HOW- Цель
- SpanFromContext never returns nil on untraced contexts but yields invalid non-recording spans whose IDs format to 32-character and 16-character zero strings rather than empty strings, RecordError does not set error status, and NewSpanContext drops all fields if either TraceID or SpanID is zero
- Пакеты
- Символы
-
- go.opentelemetry.io/otel/trace.SpanFromContext
- go.opentelemetry.io/otel/trace.ContextWithSpan
- go.opentelemetry.io/otel/trace.ContextWithRemoteSpanContext
- go.opentelemetry.io/otel/trace.WithNewRoot
- go.opentelemetry.io/otel/trace.NewSpanContext
- go.opentelemetry.io/otel/trace.TraceIDFromHex
- go.opentelemetry.io/otel/trace.SpanIDFromHex
- go.opentelemetry.io/otel/trace.Span.RecordError
- Окружение
- go
- Создан
- 2026-08-16T07:51:56Z
Контракт
- assert SpanFromContext returns non-nil noopSpan with IsRecording false and invalid SpanContext on untraced context
- assert invalid TraceID and SpanID format to fixed-length zero strings instead of empty strings
- assert ContextWithSpan with nil does not clear span and still returns non-nil noopSpan
- assert RecordError creates exception event but leaves span status code Unset without explicit SetStatus
- assert TraceIDFromHex and SpanIDFromHex return errors on all-zero hex strings
- assert NewSpanContext returns invalid empty SpanContext when either TraceID or SpanID is missing or zero
- assert ContextWithRemoteSpanContext forces IsRemote true on non-recording span context
- assert Tracer.Start inherits parent TraceID by default but generates fresh TraceID when WithNewRoot is specified
Файлы
- NOTES.md
- csx.json
- go.mod
- go.sum
- trace_test.go
Исходный код
# CSX Sample Notes: OpenTelemetry Go Trace API Lifecycle, Untraced Contexts, and Error Status Semantics
## Search Known Solution Result
- `search_known_solution` returned existing OpenTelemetry samples:
- `sha256:6dc41c32b8472be7a4589a2a43c284208b012f6e51c2e4bc20a8fb60d0ec1c33` (*"Default global TextMapPropagator silently no-ops cross-boundary trace context injection, while Extract yields remote non-recording span contexts without activating spans and malformed baggage is silently dropped"*)
- `sha256:857b0f9544f5c94ae1950b7dd9d735991244aeea14417acbc0dbd6c17a008fad` (*"BatchSpanProcessor and TracerProvider silently swallow and discard SpanExporter shutdown errors prior to v1.44.0 instead of propagating them to the caller"*)
- `sha256:4e76aea37a5182cdc0ec3afe2892492d79a816a703ab5e9e6186f3a714631bfd` (*"Unconfigured global text map propagator silently drops distributed trace headers and severs multi-service spans without explicit SetTextMapPropagator"*)
- **Why this sample is distinct**: This sample targets the core `go.opentelemetry.io/otel/trace` API surface that callers reach for first. It proves that `trace.SpanFromContext(ctx)` never returns `nil` on untraced contexts (it returns a `noopSpan` with `IsRecording() == false` and invalid `SpanContext`), that invalid Trace/Span IDs format to fixed-length zero hex strings (`"00000000000000000000000000000000"` and `"0000000000000000"`) rather than empty strings `""`, that `ContextWithSpan(ctx, nil)` does not clear the span to `nil`, that `span.RecordError(err)` only emits an exception event without mutating span status code away from `codes.Unset`, and that `trace.WithNewRoot()` creates a new trace while preserving context values.
## What a Model Would Have Written Instead
A model would write `if span := trace.SpanFromContext(ctx); span != nil` to detect if an operation is traced, check `if sc.TraceID().String() != ""` to verify if a trace ID exists, assume `span.RecordError(err)` marks the span as failed with `codes.Error`, and assume `trace.ContextWithSpan(ctx, nil)` removes the span from context.
## How the Wrong Version Fails
It fails silently with a green build: `span != nil` and `sc.TraceID().String() != ""` always evaluate to true even for completely untraced contexts, while spans with captured errors are exported with `codes.Unset` status and rendered as successful in APM backends.
{"case":{"caseId":"case:sha256:2f6314a37ae61d2cb7c4248a18e71691f9af15fdb3bfa4ca721977c2fd71524f","contract":["assert SpanFromContext returns non-nil noopSpan with IsRecording false and invalid SpanContext on untraced context","assert invalid TraceID and SpanID format to fixed-length zero strings instead of empty strings","assert ContextWithSpan with nil does not clear span and still returns non-nil noopSpan","assert RecordError creates exception event but leaves span status code Unset without explicit SetStatus","assert TraceIDFromHex and SpanIDFromHex return errors on all-zero hex strings","assert NewSpanContext returns invalid empty SpanContext when either TraceID or SpanID is missing or zero","assert ContextWithRemoteSpanContext forces IsRemote true on non-recording span context","assert Tracer.Start inherits parent TraceID by default but generates fresh TraceID when WithNewRoot is specified"],"goal":"SpanFromContext never returns nil on untraced contexts but yields invalid non-recording spans whose IDs format to 32-character and 16-character zero strings rather than empty strings, RecordError does not set error status, and NewSpanContext drops all fields if either TraceID or SpanID is zero","kind":"HOW","packages":["pkg:golang/go.opentelemetry.io/otel/trace@v1.45.0","pkg:golang/go.opentelemetry.io/otel@v1.45.0","pkg:golang/go.opentelemetry.io/otel/sdk@v1.45.0"],"schemaVersion":1,"symbols":["go.opentelemetry.io/otel/trace.SpanFromContext","go.opentelemetry.io/otel/trace.ContextWithSpan","go.opentelemetry.io/otel/trace.ContextWithRemoteSpanContext","go.opentelemetry.io/otel/trace.WithNewRoot","go.opentelemetry.io/otel/trace.NewSpanContext","go.opentelemetry.io/otel/trace.TraceIDFromHex","go.opentelemetry.io/otel/trace.SpanIDFromHex","go.opentelemetry.io/otel/trace.Span.RecordError"]},"contractCommand":["go","test","./..."],"environment":{"arch":"x64","ecosystem":"golang","executionContext":"go","language":"go","os":"linux","packageManager":"gomod","runtime":"go","schemaVersion":1},"license":"MIT-0","packages":["pkg:golang/go.opentelemetry.io/otel/trace@v1.45.0","pkg:golang/go.opentelemetry.io/otel@v1.45.0","pkg:golang/go.opentelemetry.io/otel/sdk@v1.45.0"],"schemaVersion":1,"symbols":["go.opentelemetry.io/otel/trace.SpanFromContext","go.opentelemetry.io/otel/trace.ContextWithSpan","go.opentelemetry.io/otel/trace.ContextWithRemoteSpanContext","go.opentelemetry.io/otel/trace.WithNewRoot","go.opentelemetry.io/otel/trace.NewSpanContext","go.opentelemetry.io/otel/trace.TraceIDFromHex","go.opentelemetry.io/otel/trace.SpanIDFromHex","go.opentelemetry.io/otel/trace.Span.RecordError"],"verifierAdapter":"golang@1"}
module example.com/traceapi
go 1.26.5
require (
go.opentelemetry.io/otel v1.45.0
go.opentelemetry.io/otel/sdk v1.45.0
go.opentelemetry.io/otel/trace v1.45.0
)
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/metric v1.45.0 // indirect
golang.org/x/sys v0.47.0 // indirect
)
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=
go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=
go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=
go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw=
go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA=
go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o=
go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA=
go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag=
go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
package traceapi_test
import (
"context"
"errors"
"testing"
"go.opentelemetry.io/otel/codes"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
"go.opentelemetry.io/otel/trace"
)
func TestSpanFromContextUntracedBehavior(t *testing.T) {
ctx := context.Background()
// Trap 1: SpanFromContext NEVER returns nil on an untraced context.
// A naive model writes `if span := trace.SpanFromContext(ctx); span != nil` to detect tracing.
span := trace.SpanFromContext(ctx)
if span == nil {
t.Fatalf("expected SpanFromContext to return non-nil noopSpan, got nil")
}
// Untraced context span is not recording and has invalid SpanContext
if span.IsRecording() {
t.Errorf("expected span.IsRecording() to be false for untraced context")
}
sc := span.SpanContext()
if sc.IsValid() {
t.Errorf("expected SpanContext.IsValid() to be false for untraced context")
}
if sc.TraceID().IsValid() {
t.Errorf("expected TraceID.IsValid() to be false")
}
if sc.SpanID().IsValid() {
t.Errorf("expected SpanID.IsValid() to be false")
}
if sc.IsSampled() {
t.Errorf("expected IsSampled() to be false")
}
// Trap 2: TraceID.String() and SpanID.String() return fixed-width zero strings, NOT empty strings.
// Naive models check `if sc.TraceID().String() != ""` or `len(sc.TraceID().String()) > 0`.
if sc.TraceID().String() != "00000000000000000000000000000000" {
t.Errorf("expected 32-char zero string for invalid TraceID, got %q", sc.TraceID().String())
}
if sc.SpanID().String() != "0000000000000000" {
t.Errorf("expected 16-char zero string for invalid SpanID, got %q", sc.SpanID().String())
}
// Trap 3: ContextWithSpan(ctx, nil) does NOT clear the span to nil; SpanFromContext still returns non-nil noop.
ctxWithNil := trace.ContextWithSpan(ctx, nil)
spanNil := trace.SpanFromContext(ctxWithNil)
if spanNil == nil {
t.Fatalf("expected ContextWithSpan(ctx, nil) to still return non-nil noop span")
}
if spanNil.SpanContext().IsValid() {
t.Errorf("expected invalid SpanContext from ContextWithSpan(ctx, nil)")
}
// Calling methods on noop span must be safe and silently no-op
span.End()
span.RecordError(errors.New("fail"))
span.SetStatus(codes.Error, "failed")
span.AddEvent("noop_event")
}
func TestRecordErrorDoesNotSetErrorStatus(t *testing.T) {
// Trap 4: RecordError records an exception event, but does NOT set the span Status to Error!
// A naive model assumes RecordError(err) sets status to Error.
exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
defer func() { _ = tp.Shutdown(context.Background()) }()
tracer := tp.Tracer("test-tracer")
ctx, span := tracer.Start(context.Background(), "error-op")
_ = ctx
span.RecordError(errors.New("database timeout"))
span.End()
spans := exporter.GetSpans()
if len(spans) != 1 {
t.Fatalf("expected 1 span, got %d", len(spans))
}
snapshot := spans[0]
// The span must have 1 event of name "exception"
if len(snapshot.Events) != 1 || snapshot.Events[0].Name != "exception" {
t.Fatalf("expected 1 'exception' event, got %v", snapshot.Events)
}
// The status code remains Unset (0), NOT Error (1) or Ok (2)!
if snapshot.Status.Code != codes.Unset {
t.Errorf("expected status code to remain codes.Unset (%v), got %v", codes.Unset, snapshot.Status.Code)
}
}
func TestSpanContextConstructorWipeAndHexValidation(t *testing.T) {
tid, err := trace.TraceIDFromHex("4bf92f3577b34da6a3ce929d0e0e4736")
if err != nil {
t.Fatalf("unexpected hex parse error: %v", err)
}
sid, err := trace.SpanIDFromHex("00f067aa0ba902b7")
if err != nil {
t.Fatalf("unexpected hex parse error: %v", err)
}
// Trap 5: TraceIDFromHex rejects all-zero hex string with error
_, errZeroTID := trace.TraceIDFromHex("00000000000000000000000000000000")
if errZeroTID == nil {
t.Errorf("expected TraceIDFromHex to return error on all-zero string")
}
_, errZeroSID := trace.SpanIDFromHex("0000000000000000")
if errZeroSID == nil {
t.Errorf("expected SpanIDFromHex to return error on all-zero string")
}
// Trap 6: NewSpanContext with valid TraceID and SpanID produces valid SpanContext
scValid := trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid,
SpanID: sid,
TraceFlags: trace.FlagsSampled,
})
if !scValid.IsValid() {
t.Errorf("expected scValid to be valid")
}
if !scValid.IsSampled() {
t.Errorf("expected scValid to be sampled")
}
// If TraceID is zero or SpanID is zero, NewSpanContext is not valid
scNoSID := trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid,
TraceFlags: trace.FlagsSampled,
})
if scNoSID.IsValid() {
t.Errorf("expected NewSpanContext without SpanID to be invalid")
}
scNoTID := trace.NewSpanContext(trace.SpanContextConfig{
SpanID: sid,
TraceFlags: trace.FlagsSampled,
})
if scNoTID.IsValid() {
t.Errorf("expected NewSpanContext without TraceID to be invalid")
}
}
func TestContextWithRemoteSpanContextAndWithNewRoot(t *testing.T) {
tid, _ := trace.TraceIDFromHex("4bf92f3577b34da6a3ce929d0e0e4736")
sid, _ := trace.SpanIDFromHex("00f067aa0ba902b7")
sc := trace.NewSpanContext(trace.SpanContextConfig{
TraceID: tid,
SpanID: sid,
TraceFlags: trace.FlagsSampled,
})
// Trap 7: ContextWithRemoteSpanContext forces IsRemote() == true on extracted span context
ctx := trace.ContextWithRemoteSpanContext(context.Background(), sc)
extractedSpan := trace.SpanFromContext(ctx)
if !extractedSpan.SpanContext().IsValid() {
t.Fatalf("expected valid span context")
}
if !extractedSpan.SpanContext().IsRemote() {
t.Errorf("expected span context to be marked remote")
}
// The span itself in context is NOT recording (it is a placeholder nonRecordingSpan)
if extractedSpan.IsRecording() {
t.Errorf("expected nonRecordingSpan in context before Tracer.Start")
}
// Trap 8: Tracer.Start without WithNewRoot creates child inheriting TraceID
exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
defer func() { _ = tp.Shutdown(context.Background()) }()
tracer := tp.Tracer("test-tracer")
childCtx, childSpan := tracer.Start(ctx, "child-span")
childSC := childSpan.SpanContext()
if !childSpan.IsRecording() {
t.Errorf("child span must be recording")
}
if childSC.IsRemote() {
t.Errorf("child span must NOT be remote")
}
if childSC.TraceID() != tid {
t.Errorf("child span must inherit parent TraceID")
}
if childSC.SpanID() == sid {
t.Errorf("child span must generate a new distinct SpanID")
}
childSpan.End()
// Trap 9: Tracer.Start with trace.WithNewRoot() ignores parent TraceID and starts a fresh trace
rootCtx, rootSpan := tracer.Start(ctx, "fresh-root", trace.WithNewRoot())
rootSC := rootSpan.SpanContext()
if rootSC.TraceID() == tid {
t.Errorf("WithNewRoot span must NOT inherit parent TraceID")
}
if !rootSC.TraceID().IsValid() {
t.Errorf("WithNewRoot span must have valid new TraceID")
}
rootSpan.End()
_ = childCtx
_ = rootCtx
}