Skip to content

Commit c884fc9

Browse files
authored
feat: support OTel span.addLink, span.addLinks; add similar APIs to the APM agent API (#4078)
This also updates a few places for the new otel/api v1.9.0. Refs: #4071 (the dependabot update doesn't get everything) Refs: #4070 Refs: #4069 Refs: #4077 (a separate issue for this other new feature in otel/[email protected])
1 parent e312779 commit c884fc9

20 files changed

+283
-77
lines changed

docs/span-api.asciidoc

+30
Original file line numberDiff line numberDiff line change
@@ -242,3 +242,33 @@ If false-y values (e.g. `null`) are given for both `type` and `name`, then `serv
242242
If this method is not called, the service target values are inferred from other span fields (https://github.com/elastic/apm/blob/main/specs/agents/tracing-spans-service-target.md#field-values[spec]).
243243

244244
`service.target.*` fields are ignored for APM Server before v8.3.
245+
246+
[[span-addlink]]
247+
==== `span.addLink(link)`
248+
249+
[small]#Added in: REPLACEME#
250+
251+
* `link` +{type-link}+
252+
253+
A span can refer to zero or more other transactions or spans (separate
254+
from its parent). Span links will be shown in the Kibana APM app trace view. The
255+
`link` argument is an object with a single "context" field that is a
256+
`Transaction`, `Span`, OpenTelemetry `SpanContext` object, or W3C trace-context
257+
'traceparent' string.
258+
For example: `span.addLink({ context: anotherSpan })`.
259+
260+
[[span-addlinks]]
261+
==== `span.addLinks([links])`
262+
263+
[small]#Added in: REPLACEME#
264+
265+
* `links` +{type-array}+ Span links.
266+
267+
Add span links to this span.
268+
269+
A span can refer to zero or more other transactions or spans (separate
270+
from its parent). Span links will be shown in the Kibana APM app trace view. The
271+
`link` argument is an object with a single "context" field that is a
272+
`Transaction`, `Span`, OpenTelemetry `SpanContext` object, or W3C trace-context
273+
'traceparent' string.
274+
For example: `span.addLinks([{ context: anotherSpan }])`.

docs/supported-technologies.asciidoc

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ Metrics API and Metrics SDK to allow
8484
[options="header"]
8585
|=======================================================================
8686
| Framework | Version
87-
| <<opentelemetry-bridge,@opentelemetry/api>> | >=1.0.0 <1.9.0
87+
| <<opentelemetry-bridge,@opentelemetry/api>> | >=1.0.0 <1.10.0
8888
| https://www.npmjs.com/package/@opentelemetry/sdk-metrics[@opentelemetry/sdk-metrics] | >=1.11.0 <2
8989
|=======================================================================
9090

docs/transaction-api.asciidoc

+30
Original file line numberDiff line numberDiff line change
@@ -308,3 +308,33 @@ Non-HTTP transactions will begin with an outcome of `unknown`.
308308
* `outcome` +{type-string}+
309309

310310
The `setOutcome` method allows an end user to override the Node.js agent's default setting of a transaction's `outcome` property. The `setOutcome` method accepts a string of either `success`, `failure`, or `unknown`, and will force the agent to report this value for a specific span.
311+
312+
[[transaction-addlink]]
313+
==== `transaction.addLink(link)`
314+
315+
[small]#Added in: REPLACEME#
316+
317+
* `link` +{type-link}+
318+
319+
A transaction can refer to zero or more other transactions or spans (separate
320+
from its parent). Span links will be shown in the Kibana APM app trace view. The
321+
`link` argument is an object with a single "context" field that is a
322+
`Transaction`, `Span`, OpenTelemetry `SpanContext` object, or W3C trace-context
323+
'traceparent' string.
324+
For example: `transaction.addLink({ context: anotherSpan })`.
325+
326+
[[transaction-addlinks]]
327+
==== `transaction.addLinks([links])`
328+
329+
[small]#Added in: REPLACEME#
330+
331+
* `links` +{type-array}+ Span links.
332+
333+
Add span links to this transaction.
334+
335+
A transaction can refer to zero or more other transactions or spans (separate
336+
from its parent). Span links will be shown in the Kibana APM app trace view. The
337+
`link` argument is an object with a single "context" field that is a
338+
`Transaction`, `Span`, OpenTelemetry `SpanContext` object, or W3C trace-context
339+
'traceparent' string.
340+
For example: `transaction.addLinks([{ context: anotherSpan }])`.

index.d.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ declare namespace apm {
150150
setLabel (name: string, value: LabelValue, stringify?: boolean): boolean;
151151
addLabels (labels: Labels, stringify?: boolean): boolean;
152152
setOutcome(outcome: Outcome): void;
153+
addLink (link: Link): void;
154+
addLinks (links: Link[]): void;
153155

154156
startSpan(
155157
name?: string | null,
@@ -201,6 +203,8 @@ declare namespace apm {
201203
addLabels (labels: Labels, stringify?: boolean): boolean;
202204
setOutcome(outcome: Outcome): void;
203205
setServiceTarget(type?: string | null, name?: string | null): void;
206+
addLink (link: Link): void;
207+
addLinks (links: Link[]): void;
204208
end (endTime?: number): void;
205209
}
206210

@@ -349,8 +353,8 @@ declare namespace apm {
349353
// equivalent APIs in "opentelemetry-js-api/src/trace/link.ts". Currently
350354
// span link attributes are not supported.
351355
export interface Link {
352-
/** A W3C trace-context 'traceparent' string, Transaction, or Span. */
353-
context: Transaction | Span | string; // This is a SpanContext in OTel.
356+
/** A W3C trace-context 'traceparent' string, Transaction, Span, or OTel SpanContext. */
357+
context: Transaction | Span | {traceId: string, spanId: string} | string;
354358
}
355359

356360
export interface TransactionOptions {

lib/instrumentation/generic-span.js

+27-18
Original file line numberDiff line numberDiff line change
@@ -170,28 +170,31 @@ GenericSpan.prototype.addLabels = function (labels, stringify) {
170170
return true;
171171
};
172172

173-
// This method is private because the APM agents spec says that (for OTel
174-
// compat), adding links after span creation should not be allowed.
175-
// https://github.com/elastic/apm/blob/main/specs/agents/span-links.md
176-
//
177-
// To support adding span links for SQS ReceiveMessage and equivalent, the
178-
// message data isn't known until the *response*, after the span has been
179-
// created.
173+
// Add span links.
180174
//
181175
// @param {Array} links - An array of objects with a `context` property that is
182-
// a Transaction, Span, or TraceParent instance, or a W3C trace-context
183-
// 'traceparent' string.
184-
GenericSpan.prototype._addLinks = function (links) {
176+
// a Transaction, Span, or TraceParent instance; an OTel SpanContext object;
177+
// or a W3C trace-context 'traceparent' string.
178+
GenericSpan.prototype.addLinks = function (links) {
185179
if (links) {
186180
for (let i = 0; i < links.length; i++) {
187-
const link = linkFromLinkArg(links[i]);
188-
if (link) {
189-
this._links.push(link);
190-
}
181+
this.addLink(links[i]);
191182
}
192183
}
193184
};
194185

186+
// Add a span link.
187+
//
188+
// @param {Link} link - An object with a `context` property that is
189+
// a Transaction, Span, or TraceParent instance; an OTel SpanContext object;
190+
// or a W3C trace-context 'traceparent' string.
191+
GenericSpan.prototype.addLink = function (linkArg) {
192+
const link = linkFromLinkArg(linkArg);
193+
if (link) {
194+
this._links.push(link);
195+
}
196+
};
197+
195198
GenericSpan.prototype._freezeOutcome = function () {
196199
this._isOutcomeFrozen = true;
197200
};
@@ -278,9 +281,9 @@ GenericSpan.prototype._serializeOTel = function (payload) {
278281
// span link as it will be serialized and sent to APM server. If the linkArg is
279282
// invalid, this will return null.
280283
//
281-
// @param {Object} linkArg - An object with a `context` property that is a
282-
// Transaction, Span, or TraceParent instance, or a W3C trace-context
283-
// 'traceparent' string.
284+
// @param {Object} linkArg - An object with a `context` property that is
285+
// a Transaction, Span, or TraceParent instance; an OTel SpanContext object;
286+
// or a W3C trace-context 'traceparent' string.
284287
function linkFromLinkArg(linkArg) {
285288
if (!linkArg || !linkArg.context) {
286289
return null;
@@ -290,7 +293,13 @@ function linkFromLinkArg(linkArg) {
290293
let traceId;
291294
let spanId;
292295

293-
if (ctx._context instanceof TraceContext) {
296+
if (ctx.traceId && ctx.spanId) {
297+
// Duck-typing for an OTel SpanContext. APM intake v2 only supports the
298+
// trace id and span id fields for span links, so we only need care about
299+
// those attributes.
300+
traceId = ctx.traceId;
301+
spanId = ctx.spanId;
302+
} else if (ctx._context instanceof TraceContext) {
294303
// Transaction or Span
295304
traceId = ctx._context.traceparent.traceId;
296305
spanId = ctx._context.traceparent.id;

lib/instrumentation/modules/@aws-sdk/client-sqs.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ function sqsMiddlewareFactory(client, agent) {
160160
// Links
161161
const links = getSpanLinksFromResponseData(result && result.output);
162162
if (links) {
163-
span._addLinks(links);
163+
span.addLinks(links);
164164
}
165165

166166
// Metrics

lib/instrumentation/modules/aws-sdk/sqs.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ function instrumentationSqs(
320320
if (receiveMsgData) {
321321
const links = getSpanLinksFromResponseData(receiveMsgData);
322322
if (links) {
323-
span._addLinks(links);
323+
span.addLinks(links);
324324
}
325325
}
326326

lib/instrumentation/modules/kafkajs.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ module.exports = function (mod, agent, { version, enabled }) {
238238
}
239239
}
240240
}
241-
trans._addLinks(links);
241+
trans.addLinks(links);
242242
}
243243

244244
let result, err;

lib/lambda.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ function setSqsData(agent, trans, event, context, faasId, isColdStart) {
383383
trans.setCloudContext(cloudContext);
384384

385385
const links = spanLinksFromSqsRecords(event.Records);
386-
trans._addLinks(links);
386+
trans.addLinks(links);
387387
}
388388

389389
function setSnsData(agent, trans, event, context, faasId, isColdStart) {
@@ -424,7 +424,7 @@ function setSnsData(agent, trans, event, context, faasId, isColdStart) {
424424
trans.setCloudContext(cloudContext);
425425

426426
const links = spanLinksFromSnsRecords(event.Records);
427-
trans._addLinks(links);
427+
trans.addLinks(links);
428428
}
429429

430430
function setS3SingleData(trans, event, context, faasId, isColdStart) {

lib/opentelemetry-bridge/OTelBridgeNonRecordingSpan.js

+8
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,14 @@ class OTelBridgeNonRecordingSpan {
127127
return this;
128128
}
129129

130+
addLink(_link) {
131+
return this;
132+
}
133+
134+
addLinks(_links) {
135+
return this;
136+
}
137+
130138
end(_endTime) {}
131139

132140
// isRecording always returns false for NonRecordingSpan.

lib/opentelemetry-bridge/OTelSpan.js

+10
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,16 @@ class OTelSpan {
167167
return this;
168168
}
169169

170+
addLink(link) {
171+
this._span.addLink(link);
172+
return this;
173+
}
174+
175+
addLinks(links) {
176+
this._span.addLinks(links);
177+
return this;
178+
}
179+
170180
end(otelEndTime) {
171181
oblog.apicall('%s.end(endTime=%s)', this, otelEndTime);
172182
const endTime =

test/instrumentation/span.test.js

+36
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,38 @@ test('#addLabels', function (t) {
263263
t.end();
264264
});
265265

266+
test('#addLink, #addLinks', function (t) {
267+
var trans = new Transaction(agent);
268+
var span = new Span(trans);
269+
270+
const theTraceId = '00000000000000000000000000000001';
271+
span.addLink({
272+
context: { traceId: theTraceId, spanId: '0000000000000002' },
273+
});
274+
t.deepEqual(span._links, [
275+
{
276+
trace_id: theTraceId,
277+
span_id: '0000000000000002',
278+
},
279+
]);
280+
281+
span.addLinks([
282+
{
283+
context: { traceId: theTraceId, spanId: '0000000000000003' },
284+
},
285+
{
286+
context: { traceId: theTraceId, spanId: '0000000000000004' },
287+
},
288+
]);
289+
t.deepEqual(span._links, [
290+
{ trace_id: theTraceId, span_id: '0000000000000002' },
291+
{ trace_id: theTraceId, span_id: '0000000000000003' },
292+
{ trace_id: theTraceId, span_id: '0000000000000004' },
293+
]);
294+
295+
t.end();
296+
});
297+
266298
test('span.sync', function (t) {
267299
var trans = agent.startTransaction();
268300

@@ -528,6 +560,10 @@ test('Span API on ended span', function (t) {
528560
t.pass('span.addLabels(...) does not blow up');
529561
span.setOutcome('failure');
530562
t.pass('span.setOutcome(...) does not blow up');
563+
span.addLink({ context: { traceId: '001', spanId: '002' } });
564+
t.pass('span.addLink(...) does not blow up');
565+
span.addLinks([{ context: { traceId: '001', spanId: '002' } }]);
566+
t.pass('span.addLinks(...) does not blow up');
531567
span.end(42);
532568
t.pass('span.end(...) does not blow up');
533569

test/instrumentation/transaction.test.js

+31
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,37 @@ test('#addLabels', function (t) {
226226
t.end();
227227
});
228228

229+
test('#addLink, #addLinks', function (t) {
230+
var trans = new Transaction(agent);
231+
232+
const theTraceId = '00000000000000000000000000000001';
233+
trans.addLink({
234+
context: { traceId: theTraceId, spanId: '0000000000000002' },
235+
});
236+
t.deepEqual(trans._links, [
237+
{
238+
trace_id: theTraceId,
239+
span_id: '0000000000000002',
240+
},
241+
]);
242+
243+
trans.addLinks([
244+
{
245+
context: { traceId: theTraceId, spanId: '0000000000000003' },
246+
},
247+
{
248+
context: { traceId: theTraceId, spanId: '0000000000000004' },
249+
},
250+
]);
251+
t.deepEqual(trans._links, [
252+
{ trace_id: theTraceId, span_id: '0000000000000002' },
253+
{ trace_id: theTraceId, span_id: '0000000000000003' },
254+
{ trace_id: theTraceId, span_id: '0000000000000004' },
255+
]);
256+
257+
t.end();
258+
});
259+
229260
test('#startSpan()', function (t) {
230261
t.test('basic', function (t) {
231262
var trans = new Transaction(agent);

test/opentelemetry-bridge/.tav.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"@opentelemetry/api":
2-
versions: '>=1.0.0 <1.9.0'
2+
versions: '>=1.0.0 <1.10.0'
33
node: '>=8.0.0'
44
commands:
55
- node OTelBridgeNonRecordingSpan.test.js

test/opentelemetry-bridge/OTelBridgeNonRecordingSpan.test.js

+7
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ tape.test('OTelBridgeNonRecordingSpan', (suite) => {
7878
'setStatus',
7979
);
8080
t.equal(nrsOTelSpan.updateName('anotherName'), nrsOTelSpan, 'updateName');
81+
const linkContext = {
82+
traceId: '8b46594050c89c3d87248476ed8e0c57',
83+
spanId: 'ffe4cfa94865ee2a',
84+
traceFlags: otel.TraceFlags.SAMPLED,
85+
};
86+
t.equal(nrsOTelSpan.addLink(linkContext), nrsOTelSpan, 'addLink');
87+
t.equal(nrsOTelSpan.addLinks([linkContext]), nrsOTelSpan, 'addLinks');
8188
t.equal(nrsOTelSpan.end(), undefined, 'end');
8289
t.equal(nrsOTelSpan.isRecording(), false, 'isRecording');
8390
t.equal(

test/opentelemetry-bridge/fixtures.test.js

+18
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@ const cases = [
376376
'sSetStatusChildERROR.outcome',
377377
);
378378

379+
// Span#updateName
379380
t.strictEqual(
380381
findObjInArray(
381382
events,
@@ -386,6 +387,23 @@ const cases = [
386387
'sUpdateName',
387388
);
388389

390+
// Span#addLink, Span#addLinks
391+
t.deepEqual(
392+
findObjInArray(events, 'transaction.name', 'sAddLinks').transaction
393+
.links,
394+
[
395+
{
396+
trace_id: '8b46594050c89c3d87248476ed8e0c57',
397+
span_id: 'ffe4cfa94865ee2a',
398+
},
399+
{
400+
trace_id: '8b46594050c89c3d87248476ed8e0c57',
401+
span_id: 'ffe4cfa94865ee2a',
402+
},
403+
],
404+
'sAddLinks links',
405+
);
406+
389407
// Span#end
390408
function spanEndTimeIsApprox(transOrSpanName, t = Date.now()) {
391409
const foundTrans = findObjInArray(

0 commit comments

Comments
 (0)