From fa8e48ba88056b3de77d4733a74a2347bcd9bcae Mon Sep 17 00:00:00 2001 From: Nikhil Mantri Date: Mon, 13 Oct 2025 13:22:27 +0530 Subject: [PATCH] =?UTF-8?q?OTLP=20trace=20exporter=20include=20W3C=20trace?= =?UTF-8?q?=20flags=20(bits=200=E2=80=937)=20in=20Span.Flags=20=20(#7438)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #7436 Span.Flags should include: Bits 0–7: span’s W3C TraceFlags (e.g., sampled) Bits 8–9: “has parent isRemote” and “parent isRemote” per OTLP spec Update the trace exporter to include the span’s W3C trace flags in the lower 8 bits and keep the existing 8–9 isRemote logic. Conceptually: For spans: flags := uint32(sd.SpanContext().TraceFlags() & 0xff) Always set SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK If sd.Parent().IsRemote(), also set SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK Assign s.Flags = flags Apply the same for links using the link’s SpanContext.TraceFlags() for bits 0–7 and the link’s SpanContext.IsRemote() for bits 8–9. --------- Co-authored-by: Damien Mathieu <42@dmathieu.com> --- CHANGELOG.md | 1 + .../otlptrace/internal/tracetransform/span.go | 16 ++--- .../internal/tracetransform/span_test.go | 59 ++++++++++++++++++- 3 files changed, 67 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9903d131a..c6fe673a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm Now, when translation would drop data (e.g., invalid label/value), the exporter emits a `NewInvalidMetric`, and Prometheus scrapes **fail with HTTP 500** by default. To preserve the prior behavior (scrapes succeed while errors are logged), configure your Prometheus HTTP handler with: `promhttp.HandlerOpts{ ErrorHandling: promhttp.ContinueOnError }`. (#7363) - The default `TranslationStrategy` in `go.opentelemetry.io/exporters/prometheus` is changed from `otlptranslator.NoUTF8EscapingWithSuffixes` to `otlptranslator.UnderscoreEscapingWithSuffixes`. (#7421) +- Include W3C TraceFlags (bits 0–7) in the OTLP `Span.Flags` field in `go.opentelemetry.io/exporters/otlp/otlptrace/otlptracehttp` and `go.opentelemetry.io/exporters/otlp/otlptrace/otlptracegrpc`. (#7438) - The `ErrorType` function in `go.opentelemetry.io/otel/semconv/v1.37.0` now handles custom error types. If an error implements an `ErrorType() string` method, the return value of that method will be used as the error type. (#7442) - Improve performance of concurrent measurements in `go.opentelemetry.io/otel/sdk/metric`. (#7427) diff --git a/exporters/otlp/otlptrace/internal/tracetransform/span.go b/exporters/otlp/otlptrace/internal/tracetransform/span.go index 379bc8170..d431fc451 100644 --- a/exporters/otlp/otlptrace/internal/tracetransform/span.go +++ b/exporters/otlp/otlptrace/internal/tracetransform/span.go @@ -113,7 +113,7 @@ func span(sd tracesdk.ReadOnlySpan) *tracepb.Span { if psid := sd.Parent().SpanID(); psid.IsValid() { s.ParentSpanId = psid[:] } - s.Flags = buildSpanFlags(sd.Parent()) + s.Flags = buildSpanFlagsWith(sd.SpanContext().TraceFlags(), sd.Parent()) return s } @@ -159,7 +159,7 @@ func links(links []tracesdk.Link) []*tracepb.Span_Link { tid := otLink.SpanContext.TraceID() sid := otLink.SpanContext.SpanID() - flags := buildSpanFlags(otLink.SpanContext) + flags := buildSpanFlagsWith(otLink.SpanContext.TraceFlags(), otLink.SpanContext) sl = append(sl, &tracepb.Span_Link{ TraceId: tid[:], @@ -172,13 +172,15 @@ func links(links []tracesdk.Link) []*tracepb.Span_Link { return sl } -func buildSpanFlags(sc trace.SpanContext) uint32 { - flags := tracepb.SpanFlags_SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK - if sc.IsRemote() { - flags |= tracepb.SpanFlags_SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK +func buildSpanFlagsWith(tf trace.TraceFlags, parent trace.SpanContext) uint32 { + // Lower 8 bits are the W3C TraceFlags; always indicate that we know whether the parent is remote + flags := uint32(tf) | uint32(tracepb.SpanFlags_SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK) + // Set the parent-is-remote bit when applicable + if parent.IsRemote() { + flags |= uint32(tracepb.SpanFlags_SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK) } - return uint32(flags) // nolint:gosec // Flags is a bitmask and can't be negative + return flags // nolint:gosec // Flags is a bitmask and can't be negative } // spanEvents transforms span Events to an OTLP span events. diff --git a/exporters/otlp/otlptrace/internal/tracetransform/span_test.go b/exporters/otlp/otlptrace/internal/tracetransform/span_test.go index d420925fd..7a57ffda0 100644 --- a/exporters/otlp/otlptrace/internal/tracetransform/span_test.go +++ b/exporters/otlp/otlptrace/internal/tracetransform/span_test.go @@ -207,11 +207,66 @@ func TestBuildSpanFlags(t *testing.T) { }, } { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.wantFlags, buildSpanFlags(tt.spanContext)) + assert.Equal(t, tt.wantFlags, buildSpanFlagsWith(tt.spanContext.TraceFlags(), tt.spanContext)) }) } } +func TestSpanFlagsLower8BitsFromTraceFlags(t *testing.T) { + for _, tc := range []struct { + name string + traceFlags trace.TraceFlags + parentRemote bool + wantLow8 uint32 + wantMask uint32 + }{ + {name: "unsampled root", traceFlags: 0x00, parentRemote: false, wantLow8: 0x00, wantMask: 0x100}, + {name: "sampled root", traceFlags: 0x01, parentRemote: false, wantLow8: 0x01, wantMask: 0x100}, + {name: "custom bits root", traceFlags: 0x05, parentRemote: false, wantLow8: 0x05, wantMask: 0x100}, + {name: "unsampled remote parent", traceFlags: 0x00, parentRemote: true, wantLow8: 0x00, wantMask: 0x300}, + {name: "sampled remote parent", traceFlags: 0x01, parentRemote: true, wantLow8: 0x01, wantMask: 0x300}, + } { + t.Run(tc.name, func(t *testing.T) { + parent := trace.NewSpanContext(trace.SpanContextConfig{Remote: tc.parentRemote}) + got := buildSpanFlagsWith(tc.traceFlags, parent) + assert.Equal(t, tc.wantLow8, got&0xff) + assert.Equal(t, tc.wantMask, got&0x300) + // Ensure higher bits are not set beyond 0-9 + assert.Equal(t, uint32(0), got&^uint32(0x3ff)) + }) + } +} + +func TestSpanAndLinkExportLower8Bits(t *testing.T) { + // Span: sampled child with local parent + spanData := tracetest.SpanStub{ + SpanContext: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: trace.TraceID{0x1}, + SpanID: trace.SpanID{0x2}, + TraceFlags: trace.TraceFlags(0x01), + }), + Parent: trace.NewSpanContext(trace.SpanContextConfig{}), + Name: "flags-test", + } + rss := Spans(tracetest.SpanStubs{spanData}.Snapshots()) + require.Len(t, rss, 1) + scopeSpans := rss[0].GetScopeSpans() + require.Len(t, scopeSpans, 1) + require.Len(t, scopeSpans[0].Spans, 1) + s := scopeSpans[0].Spans[0] + assert.Equal(t, uint32(0x01), s.Flags&0xff) + assert.Equal(t, uint32(0x100), s.Flags&0x300) + + // Link: sampled link local + l := []tracesdk.Link{ + {SpanContext: trace.NewSpanContext(trace.SpanContextConfig{TraceFlags: 0x01})}, + } + gotLinks := links(l) + require.Len(t, gotLinks, 1) + assert.Equal(t, uint32(0x01), gotLinks[0].Flags&0xff) + assert.Equal(t, uint32(0x100), gotLinks[0].Flags&0x300) +} + func TestNilSpan(t *testing.T) { assert.Nil(t, span(nil)) } @@ -331,7 +386,7 @@ func TestSpanData(t *testing.T) { SpanId: []byte{0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8}, ParentSpanId: []byte{0xEF, 0xEE, 0xED, 0xEC, 0xEB, 0xEA, 0xE9, 0xE8}, TraceState: "key1=val1,key2=val2", - Flags: 0x300, + Flags: 0x300, // lower 8 bits (trace flags) are 0x00 in this fixture; update in new tests below Name: spanData.Name, Kind: tracepb.Span_SPAN_KIND_SERVER, StartTimeUnixNano: uint64(startTime.UnixNano()),