diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f42753dd3..14ab32e70a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,7 @@ For experimental package changes, see the [experimental CHANGELOG](experimental/ * feat(instrumentation): Make `init()` method public [#4418](https://github.com/open-telemetry/opentelemetry-js/pull/4418) * feat(context-zone-peer-dep, context-zone): support zone.js 0.13.x, 0.14.x [#4469](https://github.com/open-telemetry/opentelemetry-js/pull/4469) @pichlermarc * chore: Semantic Conventions export individual strings [4185](https://github.com/open-telemetry/opentelemetry-js/issues/4185) +* feat(sdk-trace-base): allow adding span links after span creation [#4536](https://github.com/open-telemetry/opentelemetry-js/pull/4536) @seemk ### :bug: (Bug Fix) diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index fbc6f264c1..946d39be86 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -8,6 +8,8 @@ All notable changes to this project will be documented in this file. ### :rocket: (Enhancement) +* feat(api): allow adding span links after span creation [#4536](https://github.com/open-telemetry/opentelemetry-js/pull/4536) @seemk + * This change is non-breaking for end-users, but breaking for Trace SDK implmentations in accordance with the [specification](https://github.com/open-telemetry/opentelemetry-specification/blob/a03382ada8afa9415266a84dafac0510ec8c160f/specification/upgrading.md?plain=1#L97-L122) as new features need to be implemented. * feat: support node 22 [#4666](https://github.com/open-telemetry/opentelemetry-js/pull/4666) @dyladan ### :bug: (Bug Fix) diff --git a/api/src/trace/NonRecordingSpan.ts b/api/src/trace/NonRecordingSpan.ts index a9e5bcaf9b..9ee3d28837 100644 --- a/api/src/trace/NonRecordingSpan.ts +++ b/api/src/trace/NonRecordingSpan.ts @@ -21,6 +21,7 @@ import { INVALID_SPAN_CONTEXT } from './invalid-span-constants'; import { Span } from './span'; import { SpanContext } from './span_context'; import { SpanStatus } from './status'; +import { Link } from './link'; /** * The NonRecordingSpan is the default {@link Span} that is used when no Span @@ -52,6 +53,14 @@ export class NonRecordingSpan implements Span { return this; } + addLink(_link: Link): this { + return this; + } + + addLinks(_links: Link[]): this { + return this; + } + // By default does nothing setStatus(_status: SpanStatus): this { return this; diff --git a/api/src/trace/span.ts b/api/src/trace/span.ts index d80b8c2626..e3b563f103 100644 --- a/api/src/trace/span.ts +++ b/api/src/trace/span.ts @@ -19,6 +19,7 @@ import { TimeInput } from '../common/Time'; import { SpanAttributes, SpanAttributeValue } from './attributes'; import { SpanContext } from './span_context'; import { SpanStatus } from './status'; +import { Link } from './link'; /** * An interface that represents a span. A span represents a single operation @@ -76,6 +77,26 @@ export interface Span { startTime?: TimeInput ): this; + /** + * Adds a single link to the span. + * + * Links added after the creation will not affect the sampling decision. + * It is preferred span links be added at span creation. + * + * @param link the link to add. + */ + addLink(link: Link): this; + + /** + * Adds multiple links to the span. + * + * Links added after the creation will not affect the sampling decision. + * It is preferred span links be added at span creation. + * + * @param links the links to add. + */ + addLinks(links: Link[]): this; + /** * Sets a status to the span. If used, this will override the default Span * status. Default is {@link SpanStatusCode.UNSET}. SetStatus overrides the value diff --git a/api/test/common/noop-implementations/noop-span.test.ts b/api/test/common/noop-implementations/noop-span.test.ts index 5bc341f31a..05020716de 100644 --- a/api/test/common/noop-implementations/noop-span.test.ts +++ b/api/test/common/noop-implementations/noop-span.test.ts @@ -36,6 +36,14 @@ describe('NonRecordingSpan', () => { my_number_attribute: 123, }); + const linkContext = { + traceId: 'e4cda95b652f4a1592b449d5929fda1b', + spanId: '7e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + }; + span.addLink({ context: linkContext }); + span.addLinks([{ context: linkContext }]); + span.addEvent('sent'); span.addEvent('sent', { id: '42', key: 'value' }); diff --git a/packages/opentelemetry-sdk-trace-base/src/Span.ts b/packages/opentelemetry-sdk-trace-base/src/Span.ts index 06b7056a17..e2af2ad5ee 100644 --- a/packages/opentelemetry-sdk-trace-base/src/Span.ts +++ b/packages/opentelemetry-sdk-trace-base/src/Span.ts @@ -210,6 +210,16 @@ export class Span implements APISpan, ReadableSpan { return this; } + addLink(link: Link): this { + this.links.push(link); + return this; + } + + addLinks(links: Link[]): this { + this.links.push(...links); + return this; + } + setStatus(status: SpanStatus): this { if (this._isSpanEnded()) return this; this.status = status; diff --git a/packages/opentelemetry-sdk-trace-base/test/common/Span.test.ts b/packages/opentelemetry-sdk-trace-base/test/common/Span.test.ts index 5dd7ec79e5..aa36407466 100644 --- a/packages/opentelemetry-sdk-trace-base/test/common/Span.test.ts +++ b/packages/opentelemetry-sdk-trace-base/test/common/Span.test.ts @@ -771,30 +771,6 @@ describe('Span', () => { }); }); - it('should set a link', () => { - const spanContext: SpanContext = { - traceId: 'a3cda95b652f4a1592b449d5929fda1b', - spanId: '5e0c63257de34c92', - traceFlags: TraceFlags.SAMPLED, - }; - const linkContext: SpanContext = { - traceId: 'b3cda95b652f4a1592b449d5929fda1b', - spanId: '6e0c63257de34c92', - traceFlags: TraceFlags.SAMPLED, - }; - const attributes = { attr1: 'value', attr2: 123, attr3: true }; - const span = new Span( - tracer, - ROOT_CONTEXT, - name, - spanContext, - SpanKind.CLIENT, - '12345', - [{ context: linkContext }, { context: linkContext, attributes }] - ); - span.end(); - }); - it('should drop extra events', () => { const span = new Span( tracer, @@ -959,6 +935,58 @@ describe('Span', () => { span.end(); }); + it('should be possible to add a link after span creation', () => { + const span = new Span( + tracer, + ROOT_CONTEXT, + 'my-span', + spanContext, + SpanKind.CONSUMER + ); + + span.addLink({ context: linkContext }); + + span.end(); + + assert.strictEqual(span.links.length, 1); + assert.deepStrictEqual(span.links, [ + { + context: linkContext, + }, + ]); + }); + + it('should be possible to add multiple links after span creation', () => { + const span = new Span( + tracer, + ROOT_CONTEXT, + 'my-span', + spanContext, + SpanKind.CONSUMER + ); + + span.addLinks([ + { context: linkContext }, + { + context: linkContext, + attributes: { attr1: 'value', attr2: 123, attr3: true }, + }, + ]); + + span.end(); + + assert.strictEqual(span.links.length, 2); + assert.deepStrictEqual(span.links, [ + { + context: linkContext, + }, + { + attributes: { attr1: 'value', attr2: 123, attr3: true }, + context: linkContext, + }, + ]); + }); + it('should return ReadableSpan with events', () => { const span = new Span( tracer,