Skip to content

Commit 65c71f6

Browse files
feat(otlp-transformer): add is_remote_parent span flags to OTLP exported Spans and SpanLinks (#5910)
Co-authored-by: Marc Pichler <marc.pichler@dynatrace.com>
1 parent b903af5 commit 65c71f6

4 files changed

Lines changed: 228 additions & 0 deletions

File tree

experimental/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2
1010

1111
### :rocket: Features
1212

13+
* feat(otlp-transformer): add span flags support for isRemote property [#5910](https://github.com/open-telemetry/opentelemetry-js/pull/5910) @nikhilmantri0902
1314
* feat(sampler-composite): Added experimental implementations of draft composite sampling spec [#5839](https://github.com/open-telemetry/opentelemetry-js/pull/5839) @anuraaga
1415

1516
### :bug: Bug Fixes

experimental/packages/otlp-transformer/src/trace/internal-types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ export interface ISpan {
9797

9898
/** Span status */
9999
status: IStatus;
100+
101+
/** Span flags */
102+
flags?: number;
100103
}
101104

102105
/**
@@ -185,4 +188,7 @@ export interface ILink {
185188

186189
/** Link droppedAttributesCount */
187190
droppedAttributesCount: number;
191+
192+
/** Link flags */
193+
flags?: number;
188194
}

experimental/packages/otlp-transformer/src/trace/internal.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,23 @@ import {
3434
import { OtlpEncodingOptions } from '../common/internal-types';
3535
import { getOtlpEncoder } from '../common/utils';
3636

37+
// Span flags constants matching the OTLP specification
38+
const SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK = 0x100;
39+
const SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK = 0x200;
40+
41+
/**
42+
* Builds the 32-bit span flags value combining the low 8-bit W3C TraceFlags
43+
* with the HAS_IS_REMOTE and IS_REMOTE bits according to the OTLP spec.
44+
*/
45+
function buildSpanFlagsFrom(traceFlags: number, isRemote?: boolean): number {
46+
// low 8 bits are W3C TraceFlags (e.g., sampled)
47+
let flags = (traceFlags & 0xff) | SPAN_FLAGS_CONTEXT_HAS_IS_REMOTE_MASK;
48+
if (isRemote) {
49+
flags |= SPAN_FLAGS_CONTEXT_IS_REMOTE_MASK;
50+
}
51+
return flags;
52+
}
53+
3754
export function sdkSpanToOtlpSpan(span: ReadableSpan, encoder: Encoder): ISpan {
3855
const ctx = span.spanContext();
3956
const status = span.status;
@@ -61,6 +78,7 @@ export function sdkSpanToOtlpSpan(span: ReadableSpan, encoder: Encoder): ISpan {
6178
},
6279
links: span.links.map(link => toOtlpLink(link, encoder)),
6380
droppedLinksCount: span.droppedLinksCount,
81+
flags: buildSpanFlagsFrom(ctx.traceFlags, span.parentSpanContext?.isRemote),
6482
};
6583
}
6684

@@ -71,6 +89,7 @@ export function toOtlpLink(link: Link, encoder: Encoder): ILink {
7189
traceId: encoder.encodeSpanContext(link.context.traceId),
7290
traceState: link.context.traceState?.serialize(),
7391
droppedAttributesCount: link.droppedAttributesCount || 0,
92+
flags: buildSpanFlagsFrom(link.context.traceFlags, link.context.isRemote),
7493
};
7594
}
7695

experimental/packages/otlp-transformer/test/trace.test.ts

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ function createExpectedSpanJson(options: OtlpEncodingOptions) {
9797
},
9898
},
9999
],
100+
flags: 0x101, // TraceFlags (0x01) | HAS_IS_REMOTE
100101
},
101102
],
102103
startTimeUnixNano: startTime,
@@ -129,6 +130,7 @@ function createExpectedSpanJson(options: OtlpEncodingOptions) {
129130
code: EStatusCode.STATUS_CODE_OK,
130131
message: undefined,
131132
},
133+
flags: 0x101, // TraceFlags (0x01) | HAS_IS_REMOTE
132134
},
133135
],
134136
schemaUrl: 'http://url.to.schema',
@@ -187,6 +189,7 @@ function createExpectedSpanProtobuf() {
187189
},
188190
},
189191
],
192+
flags: 0x101, // TraceFlags (0x01) | HAS_IS_REMOTE
190193
},
191194
],
192195
startTimeUnixNano: startTime,
@@ -218,6 +221,7 @@ function createExpectedSpanProtobuf() {
218221
status: {
219222
code: EStatusCode.STATUS_CODE_OK,
220223
},
224+
flags: 0x101, // TraceFlags (0x01) | HAS_IS_REMOTE
221225
},
222226
],
223227
schemaUrl: 'http://url.to.schema',
@@ -580,4 +584,202 @@ describe('Trace', () => {
580584
);
581585
});
582586
});
587+
588+
describe('span flags', () => {
589+
it('sets flags to 0x101 for local parent span context', () => {
590+
const exportRequest = createExportTraceServiceRequest([span], {
591+
useHex: true,
592+
});
593+
assert.ok(exportRequest);
594+
const spanFlags =
595+
exportRequest.resourceSpans?.[0].scopeSpans[0].spans?.[0].flags;
596+
assert.strictEqual(spanFlags, 0x101); // TraceFlags (0x01) | HAS_IS_REMOTE
597+
});
598+
599+
it('sets flags to 0x301 for remote parent span context', () => {
600+
// Create a span with a remote parent context
601+
const remoteParentSpanContext = {
602+
spanId: '0000000000000001',
603+
traceId: '00000000000000000000000000000001',
604+
traceFlags: TraceFlags.SAMPLED,
605+
isRemote: true, // This is the key difference
606+
};
607+
608+
const spanWithRemoteParent = {
609+
...span,
610+
parentSpanContext: remoteParentSpanContext,
611+
};
612+
613+
const exportRequest = createExportTraceServiceRequest(
614+
[spanWithRemoteParent],
615+
{
616+
useHex: true,
617+
}
618+
);
619+
assert.ok(exportRequest);
620+
const spanFlags =
621+
exportRequest.resourceSpans?.[0].scopeSpans[0].spans?.[0].flags;
622+
assert.strictEqual(spanFlags, 0x301); // TraceFlags (0x01) | HAS_IS_REMOTE | IS_REMOTE
623+
});
624+
625+
it('sets flags to 0x101 for links with local context', () => {
626+
const exportRequest = createExportTraceServiceRequest([span], {
627+
useHex: true,
628+
});
629+
assert.ok(exportRequest);
630+
const linkFlags =
631+
exportRequest.resourceSpans?.[0].scopeSpans[0].spans?.[0].links?.[0]
632+
.flags;
633+
assert.strictEqual(linkFlags, 0x101); // TraceFlags (0x01) | HAS_IS_REMOTE
634+
});
635+
636+
it('sets flags to 0x301 for links with remote context', () => {
637+
// Create a span with a remote link context
638+
const remoteLinkContext = {
639+
spanId: '0000000000000003',
640+
traceId: '00000000000000000000000000000002',
641+
traceFlags: TraceFlags.SAMPLED,
642+
isRemote: true, // This is the key difference
643+
};
644+
645+
const remoteLink = {
646+
context: remoteLinkContext,
647+
attributes: { 'link-attribute': 'string value' },
648+
droppedAttributesCount: 0,
649+
};
650+
651+
const spanWithRemoteLink = {
652+
...span,
653+
links: [remoteLink],
654+
};
655+
656+
const exportRequest = createExportTraceServiceRequest(
657+
[spanWithRemoteLink],
658+
{
659+
useHex: true,
660+
}
661+
);
662+
assert.ok(exportRequest);
663+
const linkFlags =
664+
exportRequest.resourceSpans?.[0].scopeSpans[0].spans?.[0].links?.[0]
665+
.flags;
666+
assert.strictEqual(linkFlags, 0x301); // TraceFlags (0x01) | HAS_IS_REMOTE | IS_REMOTE
667+
});
668+
});
669+
670+
describe('span/link flags matrix', () => {
671+
const cases = [
672+
{ tf: 0x00, local: 0x100, remote: 0x300 },
673+
{ tf: 0x01, local: 0x101, remote: 0x301 },
674+
{ tf: 0x05, local: 0x105, remote: 0x305 },
675+
{ tf: 0xff, local: 0x1ff, remote: 0x3ff },
676+
];
677+
678+
it('composes span flags with local and remote parent across traceFlags', () => {
679+
const baseCtx = span.spanContext();
680+
for (const c of cases) {
681+
// Local parent
682+
const spanLocal = {
683+
...span,
684+
spanContext: () => ({
685+
spanId: baseCtx.spanId,
686+
traceId: baseCtx.traceId,
687+
traceFlags: c.tf,
688+
isRemote: false,
689+
traceState: baseCtx.traceState,
690+
}),
691+
parentSpanContext: {
692+
...span.parentSpanContext,
693+
isRemote: false,
694+
},
695+
} as unknown as ReadableSpan;
696+
const reqLocal = createExportTraceServiceRequest([spanLocal], {
697+
useHex: true,
698+
});
699+
const spanFlagsLocal =
700+
reqLocal.resourceSpans?.[0].scopeSpans[0].spans?.[0].flags;
701+
assert.strictEqual(spanFlagsLocal, c.local);
702+
703+
// Remote parent
704+
const spanRemote = {
705+
...spanLocal,
706+
parentSpanContext: {
707+
...span.parentSpanContext,
708+
isRemote: true,
709+
},
710+
} as unknown as ReadableSpan;
711+
const reqRemote = createExportTraceServiceRequest([spanRemote], {
712+
useHex: true,
713+
});
714+
const spanFlagsRemote =
715+
reqRemote.resourceSpans?.[0].scopeSpans[0].spans?.[0].flags;
716+
assert.strictEqual(spanFlagsRemote, c.remote);
717+
}
718+
});
719+
720+
it('composes link flags with local and remote context across traceFlags', () => {
721+
for (const c of cases) {
722+
const linkLocal = {
723+
context: {
724+
spanId: '0000000000000003',
725+
traceId: '00000000000000000000000000000002',
726+
traceFlags: c.tf,
727+
isRemote: false,
728+
traceState: new TraceState('link=foo'),
729+
},
730+
attributes: { 'link-attribute': 'string value' },
731+
droppedAttributesCount: 0,
732+
};
733+
const spanWithLocalLink = {
734+
...span,
735+
links: [linkLocal],
736+
} as unknown as ReadableSpan;
737+
const reqLocal = createExportTraceServiceRequest([spanWithLocalLink], {
738+
useHex: true,
739+
});
740+
const linkFlagsLocal =
741+
reqLocal.resourceSpans?.[0].scopeSpans[0].spans?.[0].links?.[0].flags;
742+
assert.strictEqual(linkFlagsLocal, c.local);
743+
744+
const linkRemote = {
745+
...linkLocal,
746+
context: { ...linkLocal.context, isRemote: true },
747+
};
748+
const spanWithRemoteLink = {
749+
...span,
750+
links: [linkRemote],
751+
} as unknown as ReadableSpan;
752+
const reqRemote = createExportTraceServiceRequest(
753+
[spanWithRemoteLink],
754+
{ useHex: true }
755+
);
756+
const linkFlagsRemote =
757+
reqRemote.resourceSpans?.[0].scopeSpans[0].spans?.[0].links?.[0]
758+
.flags;
759+
assert.strictEqual(linkFlagsRemote, c.remote);
760+
}
761+
});
762+
763+
it('composes root span flags across traceFlags (no parent)', () => {
764+
const baseCtx = span.spanContext();
765+
for (const c of cases) {
766+
const rootSpan = {
767+
...span,
768+
spanContext: () => ({
769+
spanId: baseCtx.spanId,
770+
traceId: baseCtx.traceId,
771+
traceFlags: c.tf,
772+
isRemote: false,
773+
traceState: baseCtx.traceState,
774+
}),
775+
parentSpanContext: undefined,
776+
} as unknown as ReadableSpan;
777+
const req = createExportTraceServiceRequest([rootSpan], {
778+
useHex: true,
779+
});
780+
const flags = req.resourceSpans?.[0].scopeSpans[0].spans?.[0].flags;
781+
assert.strictEqual(flags, c.local);
782+
}
783+
});
784+
});
583785
});

0 commit comments

Comments
 (0)