CodeSampleX

샘플

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 spans whose…

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

컨트랙트

  1. assert SpanFromContext returns non-nil noopSpan with IsRecording false and invalid SpanContext on untraced context
  2. assert invalid TraceID and SpanID format to fixed-length zero strings instead of empty strings
  3. assert ContextWithSpan with nil does not clear span and still returns non-nil noopSpan
  4. assert RecordError creates exception event but leaves span status code Unset without explicit SetStatus
  5. assert TraceIDFromHex and SpanIDFromHex return errors on all-zero hex strings
  6. assert NewSpanContext returns invalid empty SpanContext when either TraceID or SpanID is missing or zero
  7. assert ContextWithRemoteSpanContext forces IsRemote true on non-recording span context
  8. 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

소스 아티팩트 내려받기 (tar.gz)

소스

NOTES.md
# 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.
csx.json
{"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"}
go.mod
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
)
go.sum
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=
trace_test.go
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
}

오리진 시더

csx-seed