Skip to content
1 change: 1 addition & 0 deletions experimental/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ export interface ISpan {

/** Span status */
status: IStatus;

/** Span flags */
flags?: number;
}

/**
Expand Down Expand Up @@ -185,4 +188,7 @@ export interface ILink {

/** Link droppedAttributesCount */
droppedAttributesCount: number;

/** Link flags */
flags?: number;
}
19 changes: 19 additions & 0 deletions experimental/packages/otlp-transformer/src/trace/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
};
}

Expand All @@ -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),
};
}

Expand Down
202 changes: 202 additions & 0 deletions experimental/packages/otlp-transformer/test/trace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ function createExpectedSpanJson(options: OtlpEncodingOptions) {
},
},
],
flags: 0x101, // TraceFlags (0x01) | HAS_IS_REMOTE
},
],
startTimeUnixNano: startTime,
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -187,6 +189,7 @@ function createExpectedSpanProtobuf() {
},
},
],
flags: 0x101, // TraceFlags (0x01) | HAS_IS_REMOTE
},
],
startTimeUnixNano: startTime,
Expand Down Expand Up @@ -218,6 +221,7 @@ function createExpectedSpanProtobuf() {
status: {
code: EStatusCode.STATUS_CODE_OK,
},
flags: 0x101, // TraceFlags (0x01) | HAS_IS_REMOTE
},
],
schemaUrl: 'http://url.to.schema',
Expand Down Expand Up @@ -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);
}
});
});
});
Loading