샘플
go.opentelemetry.io/otel v1.45.0: SpanFromContext never returns nil on uninstrumented contexts, ContextWithRemoteSpanContext produces non-recording spans that silently drop in-place mutations, TraceIDFromHex rejects uppercase hex, and TraceState mutations are strictly immutable
검증된 샘플 — golang go.opentelemetry.io/otel v1.45.0: SpanFromContext never returns nil on uninstrumented contexts, ContextWithRemoteSpanContext produces…
sha256:6ce5d5513105a8ec6185c85a379474fc0c264a342723cf6da8ea4fa0c4591c49
이 네트워크가 제공하는 것은 하나입니다. 빌드되는 샘플. 샌드박스에서 돌리고 서명된 영수증을 보관합니다. 등급을 매기지 않고 무엇도 보증하지 않습니다 — 같은 코드가 당신 환경에서 빌드되는지는 측정한 적이 없습니다.
통과한 계약 영수증을 낸 서로 다른 서명 키의 수입니다. 하나면 작성자 혼자이고, 둘 이상이면 다른 사람도 빌드했다는 뜻입니다. 키는 스스로 만드는 것이고 뒤에 등록된 신원이 없으므로, 세는 것은 사람이 아니라 키입니다.
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 uninstrumented contexts, ContextWithRemoteSpanContext produces non-recording spans that silently drop in-place mutations, TraceIDFromHex rejects uppercase hex, and TraceState mutations are strictly immutable
- 패키지
- 심벌
-
- go.opentelemetry.io/otel/trace.SpanFromContext
- go.opentelemetry.io/otel/trace.ContextWithRemoteSpanContext
- go.opentelemetry.io/otel/trace.WithNewRoot
- go.opentelemetry.io/otel/trace.TraceIDFromHex
- go.opentelemetry.io/otel/trace.SpanIDFromHex
- go.opentelemetry.io/otel/trace.TraceState
- 환경
- go
- 생성일
- 2026-08-16T07:51:55Z
컨트랙트
- assert SpanFromContext returns non-nil noopSpan with invalid span context on nil or empty context
- assert ContextWithRemoteSpanContext produces non-recording span that silently drops SetAttributes and SetStatus
- assert Tracer Start on remote context inherits trace ID and parent span ID for local recording child
- assert WithNewRoot severs remote parent context generating fresh trace ID with zero parent ID
- assert TraceIDFromHex and SpanIDFromHex reject uppercase hex strings and all-zeros per W3C specification
- assert TraceState Insert is immutable, prepends keys to index 0, and rejects uppercase key names
파일
- NOTES.md
- csx.json
- go.mod
- go.sum
- trace_context_test.go
소스
# Notes
- **search_known_solution result**: The network returned sample `sha256:1bf17bde93ae26bd833bf9e707662cf033b05798d2ab0c1289d2d691da1b1942` ("RecordError records an exception event leaving span status as Unset, while SetStatus Ok irrevocably locks the span status against subsequent Error updates"). We selected a different set of behavioral traps in `go.opentelemetry.io/otel/trace`: `SpanFromContext` never returns `nil` (returning a non-nil `noopSpan`), `ContextWithRemoteSpanContext` yields a `nonRecordingSpan` that silently drops in-place `SetAttributes`/`SetStatus` calls without error, `WithNewRoot()` severs inherited remote trace IDs, `TraceIDFromHex`/`SpanIDFromHex` strictly reject uppercase hex per W3C specification unlike standard Go hex decoders, and `TraceState.Insert` is value-immutable and fails on uppercase keys.
- **What a model would write instead**: A model writes `if span := trace.SpanFromContext(ctx); span != nil` to detect tracing, attempts to mutate remote spans directly with `span.SetAttributes(...)` after remote context extraction, parses uppercase hex strings via `TraceIDFromHex` assuming standard hex tolerance, and ignores the return value of `ts.Insert(...)` assuming in-place mutation.
- **How the wrong version fails**: Fails silently with a green build: the application compiles and runs without error, but presence checks (`span != nil`) always evaluate true, attributes attached to extracted remote contexts are dropped silently without recording, traces become disconnected or orphan when `WithNewRoot` is misused, and TraceState modifications are lost.
{"case":{"caseId":"case:sha256:941d72b77be63276d05f78fd013510e9a0be4841138a4fcdf10768585866feb5","contract":["assert SpanFromContext returns non-nil noopSpan with invalid span context on nil or empty context","assert ContextWithRemoteSpanContext produces non-recording span that silently drops SetAttributes and SetStatus","assert Tracer Start on remote context inherits trace ID and parent span ID for local recording child","assert WithNewRoot severs remote parent context generating fresh trace ID with zero parent ID","assert TraceIDFromHex and SpanIDFromHex reject uppercase hex strings and all-zeros per W3C specification","assert TraceState Insert is immutable, prepends keys to index 0, and rejects uppercase key names"],"goal":"SpanFromContext never returns nil on uninstrumented contexts, ContextWithRemoteSpanContext produces non-recording spans that silently drop in-place mutations, TraceIDFromHex rejects uppercase hex, and TraceState mutations are strictly immutable","kind":"HOW","packages":["pkg:golang/go.opentelemetry.io/otel@v1.45.0","pkg:golang/go.opentelemetry.io/otel/trace@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.ContextWithRemoteSpanContext","go.opentelemetry.io/otel/trace.WithNewRoot","go.opentelemetry.io/otel/trace.TraceIDFromHex","go.opentelemetry.io/otel/trace.SpanIDFromHex","go.opentelemetry.io/otel/trace.TraceState"]},"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@v1.45.0","pkg:golang/go.opentelemetry.io/otel/trace@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.ContextWithRemoteSpanContext","go.opentelemetry.io/otel/trace.WithNewRoot","go.opentelemetry.io/otel/trace.TraceIDFromHex","go.opentelemetry.io/otel/trace.SpanIDFromHex","go.opentelemetry.io/otel/trace.TraceState"],"verifierAdapter":"golang@1"}
module sample
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 sample_test
import (
"context"
"testing"
"go.opentelemetry.io/otel/attribute"
"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 TestContextAndSpanContextSemantics(t *testing.T) {
ctx := context.Background()
t.Run("SpanFromContext never returns nil even on empty or nil context", func(t *testing.T) {
// Trap: A naive developer checks `if span != nil` to test if a context is traced.
// SpanFromContext returns a non-nil noopSpanInstance with IsValid() == false.
spanNilCtx := trace.SpanFromContext(nil)
if spanNilCtx == nil {
t.Fatal("expected non-nil Span from nil context, got nil")
}
if spanNilCtx.SpanContext().IsValid() {
t.Errorf("expected invalid SpanContext from nil context")
}
if spanNilCtx.IsRecording() {
t.Errorf("expected non-recording span from nil context")
}
spanEmptyCtx := trace.SpanFromContext(ctx)
if spanEmptyCtx == nil {
t.Fatal("expected non-nil Span from empty context, got nil")
}
if spanEmptyCtx.SpanContext().IsValid() {
t.Errorf("expected invalid SpanContext from empty context")
}
if spanEmptyCtx.IsRecording() {
t.Errorf("expected non-recording span from empty context")
}
// Calling methods on noopSpan is silently ignored (does not panic)
spanEmptyCtx.SetAttributes(attribute.String("key", "val"))
spanEmptyCtx.SetStatus(codes.Error, "failed")
spanEmptyCtx.End()
})
t.Run("ContextWithRemoteSpanContext wraps remote context in non-recording span", func(t *testing.T) {
traceID, err := trace.TraceIDFromHex("4bf92f3577b34da6a3ce929d0e0e4736")
if err != nil {
t.Fatalf("unexpected error parsing traceID: %v", err)
}
spanID, err := trace.SpanIDFromHex("00f067aa0ba902b7")
if err != nil {
t.Fatalf("unexpected error parsing spanID: %v", err)
}
remoteSC := trace.NewSpanContext(trace.SpanContextConfig{
TraceID: traceID,
SpanID: spanID,
TraceFlags: trace.FlagsSampled,
})
// Injected as remote span context
remoteCtx := trace.ContextWithRemoteSpanContext(ctx, remoteSC)
extractedSpan := trace.SpanFromContext(remoteCtx)
// The span extracted is a nonRecordingSpan: valid and remote, but NOT recording!
if !extractedSpan.SpanContext().IsValid() {
t.Errorf("expected valid SpanContext from remote context")
}
if !extractedSpan.SpanContext().IsRemote() {
t.Errorf("expected IsRemote() == true on remote span context")
}
if extractedSpan.IsRecording() {
t.Errorf("expected IsRecording() == false on remote non-recording span")
}
// Mutating the extracted non-recording span is silently dropped
extractedSpan.SetAttributes(attribute.String("dropped.attr", "true"))
extractedSpan.SetStatus(codes.Error, "dropped error")
extractedSpan.End()
// Starting a child span with TracerProvider propagates the remote parent
exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
tracer := tp.Tracer("server-tracer")
childCtx, childSpan := tracer.Start(remoteCtx, "handle-request")
childSpan.SetAttributes(attribute.String("handler.status", "ok"))
childSpan.End()
spans := exporter.GetSpans()
if len(spans) != 1 {
t.Fatalf("expected 1 exported child span, got %d", len(spans))
}
s := spans[0]
// Child inherits TraceID and Parent SpanID
if s.SpanContext.TraceID() != traceID {
t.Errorf("expected child traceID %s, got %s", traceID, s.SpanContext.TraceID())
}
if s.Parent.SpanID() != spanID {
t.Errorf("expected child parent spanID %s, got %s", spanID, s.Parent.SpanID())
}
if s.SpanContext.IsRemote() {
t.Errorf("expected child span IsRemote() == false")
}
_ = childCtx
})
t.Run("WithNewRoot severs parent trace context and starts fresh trace", func(t *testing.T) {
traceID, _ := trace.TraceIDFromHex("4bf92f3577b34da6a3ce929d0e0e4736")
spanID, _ := trace.SpanIDFromHex("00f067aa0ba902b7")
remoteSC := trace.NewSpanContext(trace.SpanContextConfig{
TraceID: traceID,
SpanID: spanID,
TraceFlags: trace.FlagsSampled,
})
remoteCtx := trace.ContextWithRemoteSpanContext(ctx, remoteSC)
exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
tracer := tp.Tracer("boundary-tracer")
// Starting span with WithNewRoot
_, rootSpan := tracer.Start(remoteCtx, "isolated-root", trace.WithNewRoot())
rootSpan.End()
spans := exporter.GetSpans()
if len(spans) != 1 {
t.Fatalf("expected 1 exported span, got %d", len(spans))
}
s := spans[0]
// Trap: WithNewRoot ignores parent in context: assigns fresh TraceID and no parent
if s.SpanContext.TraceID() == traceID {
t.Errorf("expected new TraceID different from parent %s", traceID)
}
if s.Parent.IsValid() {
t.Errorf("expected invalid parent for new root, got %v", s.Parent)
}
})
t.Run("TraceIDFromHex rejects uppercase hex and all-zeros per W3C specification", func(t *testing.T) {
// Valid lowercase hex succeeds
validID, err := trace.TraceIDFromHex("4bf92f3577b34da6a3ce929d0e0e4736")
if err != nil || !validID.IsValid() {
t.Fatalf("expected valid TraceID, got err=%v", err)
}
// Trap: Standard hex decoders allow uppercase, but W3C trace context & OTel reject uppercase
_, errUpper := trace.TraceIDFromHex("4BF92F3577B34DA6A3CE929D0E0E4736")
if errUpper == nil {
t.Errorf("expected error for uppercase TraceID hex, got nil")
}
// Trap: All-zero hex is rejected with errNilTraceID
_, errZeros := trace.TraceIDFromHex("00000000000000000000000000000000")
if errZeros == nil {
t.Errorf("expected error for all-zero TraceID hex, got nil")
}
// SpanIDFromHex also rejects uppercase and all-zeros
_, errSpanUpper := trace.SpanIDFromHex("00F067AA0BA902B7")
if errSpanUpper == nil {
t.Errorf("expected error for uppercase SpanID hex, got nil")
}
_, errSpanZeros := trace.SpanIDFromHex("0000000000000000")
if errSpanZeros == nil {
t.Errorf("expected error for all-zero SpanID hex, got nil")
}
})
t.Run("TraceState is immutable and enforces lowercase W3C keys and capacity", func(t *testing.T) {
ts, err := trace.ParseTraceState("vendor1=alpha,vendor2=beta")
if err != nil {
t.Fatalf("unexpected error parsing tracestate: %v", err)
}
if ts.Len() != 2 {
t.Fatalf("expected 2 members, got %d", ts.Len())
}
// Trap 1: Immutability - calling Insert returns a new copy and does NOT mutate receiver
tsNew, err := ts.Insert("vendor3", "gamma")
if err != nil {
t.Fatalf("unexpected error on valid Insert: %v", err)
}
if ts.Len() != 2 {
t.Errorf("original TraceState was mutated: expected len 2, got %d", ts.Len())
}
if ts.Get("vendor3") != "" {
t.Errorf("original TraceState should not contain vendor3")
}
if tsNew.Len() != 3 {
t.Errorf("expected new TraceState len 3, got %d", tsNew.Len())
}
// Trap 2: Insert prepends new member to the front (index 0)
if tsNew.String() != "vendor3=gamma,vendor1=alpha,vendor2=beta" {
t.Errorf("expected vendor3 at front, got %q", tsNew.String())
}
// Trap 3: Updating an existing key moves it to the front
tsUpdated, err := tsNew.Insert("vendor2", "updated_beta")
if err != nil {
t.Fatalf("unexpected error updating existing key: %v", err)
}
if tsUpdated.String() != "vendor2=updated_beta,vendor3=gamma,vendor1=alpha" {
t.Errorf("expected updated vendor2 at front, got %q", tsUpdated.String())
}
// Trap 4: Uppercase key fails W3C validation and returns errInvalidKey
_, errUpperKey := ts.Insert("VendorKey", "val")
if errUpperKey == nil {
t.Errorf("expected error when inserting uppercase key into TraceState")
}
// Trap 5: Delete is also immutable
tsDeleted := tsNew.Delete("vendor1")
if tsNew.Len() != 3 {
t.Errorf("expected original tsNew to remain len 3, got %d", tsNew.Len())
}
if tsDeleted.Len() != 2 || tsDeleted.Get("vendor1") != "" {
t.Errorf("expected tsDeleted len 2 without vendor1, got %q", tsDeleted.String())
}
})
}