diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index 7e800b4936e..519ef6ee225 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -10,6 +10,7 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2 ### :rocket: Features +* feat(otlp-transformer): add span flags support for isRemote property [#5910](https://github.com/open-telemetry/opentelemetry-js/pull/5910) @nikhilmantri0902 * feat(sampler-composite): Added experimental implementations of draft composite sampling spec [#5839](https://github.com/open-telemetry/opentelemetry-js/pull/5839) @anuraaga ### :bug: Bug Fixes diff --git a/experimental/packages/otlp-transformer/src/trace/internal-types.ts b/experimental/packages/otlp-transformer/src/trace/internal-types.ts index 75b9c21e260..cafd2ba820d 100644 --- a/experimental/packages/otlp-transformer/src/trace/internal-types.ts +++ b/experimental/packages/otlp-transformer/src/trace/internal-types.ts @@ -97,6 +97,9 @@ export interface ISpan { /** Span status */ status: IStatus; + + /** Span flags */ + flags?: number; } /** @@ -185,4 +188,7 @@ export interface ILink { /** Link droppedAttributesCount */ droppedAttributesCount: number; + + /** Link flags */ + flags?: number; } diff --git a/experimental/packages/otlp-transformer/src/trace/internal.ts b/experimental/packages/otlp-transformer/src/trace/internal.ts index d9a517401d6..cbfef6f28d1 100644 --- a/experimental/packages/otlp-transformer/src/trace/internal.ts +++ b/experimental/packages/otlp-transformer/src/trace/internal.ts @@ -34,6 +34,23 @@ import { import { OtlpEncodingOptions } from '../common/internal-types'; import { getOtlpEncoder } from '../common/utils'; +// Span flags constants matching the OTLP specification +const SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK = 0x100; +const SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK = 0x200; + +/** + * Builds the 32-bit span flags value combining the low 8-bit W3C TraceFlags + * with the HAS_IS_REMOTE and IS_REMOTE bits according to the OTLP spec. + */ +function buildSpanFlagsFrom(traceFlags: number, isRemote?: boolean): number { + // low 8 bits are W3C TraceFlags (e.g., sampled) + let flags = (traceFlags & 0xff) | SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK; + if (isRemote) { + flags |= SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK; + } + return flags; +} + export function sdkSpanToOtlpSpan(span: ReadableSpan, encoder: Encoder): ISpan { const ctx = span.spanContext(); const status = span.status; @@ -61,6 +78,7 @@ export function sdkSpanToOtlpSpan(span: ReadableSpan, encoder: Encoder): ISpan { }, links: span.links.map(link => toOtlpLink(link, encoder)), droppedLinksCount: span.droppedLinksCount, + flags: buildSpanFlagsFrom(ctx.traceFlags, span.parentSpanContext?.isRemote), }; } @@ -71,6 +89,7 @@ export function toOtlpLink(link: Link, encoder: Encoder): ILink { traceId: encoder.encodeSpanContext(link.context.traceId), traceState: link.context.traceState?.serialize(), droppedAttributesCount: link.droppedAttributesCount || 0, + flags: buildSpanFlagsFrom(link.context.traceFlags, link.context.isRemote), }; } diff --git a/experimental/packages/otlp-transformer/test/trace.test.ts b/experimental/packages/otlp-transformer/test/trace.test.ts index 9fad977170f..5a1aaf13a23 100644 --- a/experimental/packages/otlp-transformer/test/trace.test.ts +++ b/experimental/packages/otlp-transformer/test/trace.test.ts @@ -97,6 +97,7 @@ function createExpectedSpanJson(options: OtlpEncodingOptions) { }, }, ], + flags: 0x101, // TraceFlags (0x01) | HAS_IS_REMOTE }, ], startTimeUnixNano: startTime, @@ -129,6 +130,7 @@ function createExpectedSpanJson(options: OtlpEncodingOptions) { code: EStatusCode.STATUS_CODE_OK, message: undefined, }, + flags: 0x101, // TraceFlags (0x01) | HAS_IS_REMOTE }, ], schemaUrl: 'http://url.to.schema', @@ -187,6 +189,7 @@ function createExpectedSpanProtobuf() { }, }, ], + flags: 0x101, // TraceFlags (0x01) | HAS_IS_REMOTE }, ], startTimeUnixNano: startTime, @@ -218,6 +221,7 @@ function createExpectedSpanProtobuf() { status: { code: EStatusCode.STATUS_CODE_OK, }, + flags: 0x101, // TraceFlags (0x01) | HAS_IS_REMOTE }, ], schemaUrl: 'http://url.to.schema', @@ -580,4 +584,202 @@ describe('Trace', () => { ); }); }); + + describe('span flags', () => { + it('sets flags to 0x101 for local parent span context', () => { + const exportRequest = createExportTraceServiceRequest([span], { + useHex: true, + }); + assert.ok(exportRequest); + const spanFlags = + exportRequest.resourceSpans?.[0].scopeSpans[0].spans?.[0].flags; + assert.strictEqual(spanFlags, 0x101); // TraceFlags (0x01) | HAS_IS_REMOTE + }); + + it('sets flags to 0x301 for remote parent span context', () => { + // Create a span with a remote parent context + const remoteParentSpanContext = { + spanId: '0000000000000001', + traceId: '00000000000000000000000000000001', + traceFlags: TraceFlags.SAMPLED, + isRemote: true, // This is the key difference + }; + + const spanWithRemoteParent = { + ...span, + parentSpanContext: remoteParentSpanContext, + }; + + const exportRequest = createExportTraceServiceRequest( + [spanWithRemoteParent], + { + useHex: true, + } + ); + assert.ok(exportRequest); + const spanFlags = + exportRequest.resourceSpans?.[0].scopeSpans[0].spans?.[0].flags; + assert.strictEqual(spanFlags, 0x301); // TraceFlags (0x01) | HAS_IS_REMOTE | IS_REMOTE + }); + + it('sets flags to 0x101 for links with local context', () => { + const exportRequest = createExportTraceServiceRequest([span], { + useHex: true, + }); + assert.ok(exportRequest); + const linkFlags = + exportRequest.resourceSpans?.[0].scopeSpans[0].spans?.[0].links?.[0] + .flags; + assert.strictEqual(linkFlags, 0x101); // TraceFlags (0x01) | HAS_IS_REMOTE + }); + + it('sets flags to 0x301 for links with remote context', () => { + // Create a span with a remote link context + const remoteLinkContext = { + spanId: '0000000000000003', + traceId: '00000000000000000000000000000002', + traceFlags: TraceFlags.SAMPLED, + isRemote: true, // This is the key difference + }; + + const remoteLink = { + context: remoteLinkContext, + attributes: { 'link-attribute': 'string value' }, + droppedAttributesCount: 0, + }; + + const spanWithRemoteLink = { + ...span, + links: [remoteLink], + }; + + const exportRequest = createExportTraceServiceRequest( + [spanWithRemoteLink], + { + useHex: true, + } + ); + assert.ok(exportRequest); + const linkFlags = + exportRequest.resourceSpans?.[0].scopeSpans[0].spans?.[0].links?.[0] + .flags; + assert.strictEqual(linkFlags, 0x301); // TraceFlags (0x01) | HAS_IS_REMOTE | IS_REMOTE + }); + }); + + describe('span/link flags matrix', () => { + const cases = [ + { tf: 0x00, local: 0x100, remote: 0x300 }, + { tf: 0x01, local: 0x101, remote: 0x301 }, + { tf: 0x05, local: 0x105, remote: 0x305 }, + { tf: 0xff, local: 0x1ff, remote: 0x3ff }, + ]; + + it('composes span flags with local and remote parent across traceFlags', () => { + const baseCtx = span.spanContext(); + for (const c of cases) { + // Local parent + const spanLocal = { + ...span, + spanContext: () => ({ + spanId: baseCtx.spanId, + traceId: baseCtx.traceId, + traceFlags: c.tf, + isRemote: false, + traceState: baseCtx.traceState, + }), + parentSpanContext: { + ...span.parentSpanContext, + isRemote: false, + }, + } as unknown as ReadableSpan; + const reqLocal = createExportTraceServiceRequest([spanLocal], { + useHex: true, + }); + const spanFlagsLocal = + reqLocal.resourceSpans?.[0].scopeSpans[0].spans?.[0].flags; + assert.strictEqual(spanFlagsLocal, c.local); + + // Remote parent + const spanRemote = { + ...spanLocal, + parentSpanContext: { + ...span.parentSpanContext, + isRemote: true, + }, + } as unknown as ReadableSpan; + const reqRemote = createExportTraceServiceRequest([spanRemote], { + useHex: true, + }); + const spanFlagsRemote = + reqRemote.resourceSpans?.[0].scopeSpans[0].spans?.[0].flags; + assert.strictEqual(spanFlagsRemote, c.remote); + } + }); + + it('composes link flags with local and remote context across traceFlags', () => { + for (const c of cases) { + const linkLocal = { + context: { + spanId: '0000000000000003', + traceId: '00000000000000000000000000000002', + traceFlags: c.tf, + isRemote: false, + traceState: new TraceState('link=foo'), + }, + attributes: { 'link-attribute': 'string value' }, + droppedAttributesCount: 0, + }; + const spanWithLocalLink = { + ...span, + links: [linkLocal], + } as unknown as ReadableSpan; + const reqLocal = createExportTraceServiceRequest([spanWithLocalLink], { + useHex: true, + }); + const linkFlagsLocal = + reqLocal.resourceSpans?.[0].scopeSpans[0].spans?.[0].links?.[0].flags; + assert.strictEqual(linkFlagsLocal, c.local); + + const linkRemote = { + ...linkLocal, + context: { ...linkLocal.context, isRemote: true }, + }; + const spanWithRemoteLink = { + ...span, + links: [linkRemote], + } as unknown as ReadableSpan; + const reqRemote = createExportTraceServiceRequest( + [spanWithRemoteLink], + { useHex: true } + ); + const linkFlagsRemote = + reqRemote.resourceSpans?.[0].scopeSpans[0].spans?.[0].links?.[0] + .flags; + assert.strictEqual(linkFlagsRemote, c.remote); + } + }); + + it('composes root span flags across traceFlags (no parent)', () => { + const baseCtx = span.spanContext(); + for (const c of cases) { + const rootSpan = { + ...span, + spanContext: () => ({ + spanId: baseCtx.spanId, + traceId: baseCtx.traceId, + traceFlags: c.tf, + isRemote: false, + traceState: baseCtx.traceState, + }), + parentSpanContext: undefined, + } as unknown as ReadableSpan; + const req = createExportTraceServiceRequest([rootSpan], { + useHex: true, + }); + const flags = req.resourceSpans?.[0].scopeSpans[0].spans?.[0].flags; + assert.strictEqual(flags, c.local); + } + }); + }); });