Skip to content

Commit 697b17e

Browse files
authored
refactor: Updated span event generation to assign the appropriate span.kind based on the segment name (#2976)
1 parent 06b7884 commit 697b17e

36 files changed

+427
-87
lines changed

api.js

+2
Original file line numberDiff line numberDiff line change
@@ -992,6 +992,7 @@ API.prototype.startWebTransaction = function startWebTransaction(url, handle) {
992992
transaction: tx,
993993
parent
994994
})
995+
tx.baseSegment.spanKind = 'server'
995996
const newContext = context.enterSegment({ transaction: tx, segment: tx.baseSegment })
996997
tx.baseSegment.start()
997998

@@ -1101,6 +1102,7 @@ function startBackgroundTransaction(name, group, handle) {
11011102
parent
11021103
})
11031104
const newContext = context.enterSegment({ transaction: tx, segment: tx.baseSegment })
1105+
tx.baseSegment.spanKind = 'server'
11041106
tx.baseSegment.partialName = group
11051107
tx.baseSegment.start()
11061108

lib/context-manager/context.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ module.exports = class Context {
2929
* @returns {Context} a newly constructed context
3030
*/
3131
enterSegment({ segment, transaction = this._transaction }) {
32-
return new this.constructor(transaction, segment, this._otelCtx)
32+
return new this.constructor(transaction, segment)
3333
}
3434

3535
/**

lib/instrumentation/kafkajs/consumer.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ function wrapConsumer(shim, orig) {
4242
consumer,
4343
'run',
4444
new MessageSubscribeSpec({
45-
name: `${SEGMENT_PREFIX}#run`,
45+
name: `${SEGMENT_PREFIX}run`,
4646
destinationType: shim.TOPIC,
4747
promise: true,
4848
consumer: shim.FIRST,

lib/spans/helpers.js

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2025 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
const HTTP_LIBRARY = 'http'
7+
const CATEGORIES = {
8+
HTTP: 'http',
9+
DATASTORE: 'datastore',
10+
GENERIC: 'generic'
11+
}
12+
13+
const SPAN_KIND = {
14+
CONSUMER: 'consumer',
15+
CLIENT: 'client',
16+
INTERNAL: 'internal',
17+
PRODUCER: 'producer',
18+
SERVER: 'server'
19+
}
20+
21+
const REGEXS = {
22+
CONSUMER: /^(?:Truncated\/)?OtherTransaction\/Message\//,
23+
CLIENT: {
24+
EXTERNAL: /^(?:Truncated\/)?External\//,
25+
DATASTORE: /^(?:Truncated\/)?Datastore\//,
26+
},
27+
PRODUCER: /^(?:Truncated\/)?MessageBroker\//,
28+
SERVER: /^(?:Truncated\/)?(WebTransaction)\//
29+
}
30+
31+
/**
32+
* Assigns the appropriate span kind based on the segment name.
33+
* Does not handle client kind as this is done in the `HttpSpanEvent` and `DatastoreSpanEvent`
34+
* Our agent has conventions for naming all types of segments.
35+
* The only place this convention does not exist is within the `api.startWebTransaction`
36+
* and `api.startBackgroundTransaction`. For those, we have assigned a `spanKind` property
37+
* on the segment. We default to `internal` if it cannot match a regex.
38+
*
39+
* @param {object} params to function
40+
* @param {TraceSegment} params.segment segment that is creating span
41+
* @param {object} params.span span to add `intrinsics['span.kind']`
42+
*/
43+
function addSpanKind({ segment, span }) {
44+
const intrinsics = span.getIntrinsicAttributes()
45+
if (!intrinsics['span.kind']) {
46+
let spanKind
47+
if (segment.spanKind) {
48+
spanKind = segment.spanKind
49+
} else if (REGEXS.CONSUMER.test(segment.name)) {
50+
spanKind = SPAN_KIND.CONSUMER
51+
} else if (REGEXS.PRODUCER.test(segment.name)) {
52+
spanKind = SPAN_KIND.PRODUCER
53+
} else if (REGEXS.SERVER.test(segment.name)) {
54+
spanKind = SPAN_KIND.SERVER
55+
} else {
56+
spanKind = SPAN_KIND.INTERNAL
57+
}
58+
59+
span.addIntrinsicAttribute('span.kind', spanKind)
60+
}
61+
}
62+
63+
module.exports = {
64+
HTTP_LIBRARY,
65+
CATEGORIES,
66+
SPAN_KIND,
67+
REGEXS,
68+
addSpanKind
69+
}

lib/spans/span-event.js

+14-16
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,7 @@ const Config = require('../config')
99
const { truncate } = require('../util/byte-limit')
1010

1111
const { DESTINATIONS } = require('../config/attribute-filter')
12-
13-
const HTTP_LIBRARY = 'http'
14-
const CLIENT_KIND = 'client'
15-
const CATEGORIES = {
16-
HTTP: 'http',
17-
DATASTORE: 'datastore',
18-
GENERIC: 'generic'
19-
}
20-
21-
const EXTERNAL_REGEX = /^(?:Truncated\/)?External\//
22-
const DATASTORE_REGEX = /^(?:Truncated\/)?Datastore\//
23-
12+
const { addSpanKind, HTTP_LIBRARY, REGEXS, SPAN_KIND, CATEGORIES } = require('./helpers')
2413
const EMPTY_USER_ATTRS = Object.freeze(Object.create(null))
2514
const SERVER_ADDRESS = 'server.address'
2615

@@ -74,6 +63,14 @@ class SpanEvent {
7463
}
7564
}
7665

66+
getIntrinsicAttributes() {
67+
return this.intrinsics
68+
}
69+
70+
addIntrinsicAttribute(key, value) {
71+
this.intrinsics[key] = value
72+
}
73+
7774
static get CATEGORIES() {
7875
return CATEGORIES
7976
}
@@ -153,6 +150,7 @@ class SpanEvent {
153150
span.intrinsics.timestamp = segment.timer.start
154151
span.intrinsics.duration = segment.timer.getDurationInMillis() / 1000
155152

153+
addSpanKind({ segment, span })
156154
return span
157155
}
158156

@@ -193,7 +191,7 @@ class HttpSpanEvent extends SpanEvent {
193191

194192
this.intrinsics.category = CATEGORIES.HTTP
195193
this.intrinsics.component = attributes.library || HTTP_LIBRARY
196-
this.intrinsics['span.kind'] = CLIENT_KIND
194+
this.intrinsics['span.kind'] = SPAN_KIND.CLIENT
197195

198196
if (attributes.library) {
199197
attributes.library = null
@@ -217,7 +215,7 @@ class HttpSpanEvent extends SpanEvent {
217215
}
218216

219217
static testSegment(segment) {
220-
return EXTERNAL_REGEX.test(segment.name)
218+
return REGEXS.CLIENT.EXTERNAL.test(segment.name)
221219
}
222220
}
223221

@@ -232,7 +230,7 @@ class DatastoreSpanEvent extends SpanEvent {
232230
super(attributes, customAttributes)
233231

234232
this.intrinsics.category = CATEGORIES.DATASTORE
235-
this.intrinsics['span.kind'] = CLIENT_KIND
233+
this.intrinsics['span.kind'] = SPAN_KIND.CLIENT
236234

237235
if (attributes.product) {
238236
this.intrinsics.component = attributes.product
@@ -279,7 +277,7 @@ class DatastoreSpanEvent extends SpanEvent {
279277
}
280278

281279
static testSegment(segment) {
282-
return DATASTORE_REGEX.test(segment.name)
280+
return REGEXS.CLIENT.DATASTORE.test(segment.name)
283281
}
284282
}
285283

lib/spans/streaming-span-event.js

+11-16
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,7 @@ const { truncate } = require('../util/byte-limit')
1010
const Config = require('../config')
1111

1212
const { DESTINATIONS } = require('../config/attribute-filter')
13-
14-
const HTTP_LIBRARY = 'http'
15-
const CLIENT_KIND = 'client'
16-
const CATEGORIES = {
17-
HTTP: 'http',
18-
DATASTORE: 'datastore',
19-
GENERIC: 'generic'
20-
}
21-
22-
const EXTERNAL_REGEX = /^(?:Truncated\/)?External\//
23-
const DATASTORE_REGEX = /^(?:Truncated\/)?Datastore\//
13+
const { addSpanKind, HTTP_LIBRARY, REGEXS, SPAN_KIND, CATEGORIES } = require('./helpers')
2414

2515
/**
2616
* Specialized span event class for use with infinite streaming.
@@ -59,7 +49,7 @@ class StreamingSpanEvent {
5949
}
6050

6151
/**
62-
* Add a key/value pair to the Span's instrinisics collection.
52+
* Add a key/value pair to the Span's intrinsics collection.
6353
*
6454
* @param {string} key Name of the attribute to be stored.
6555
* @param {string|boolean|number} value Value of the attribute to be stored.
@@ -68,6 +58,10 @@ class StreamingSpanEvent {
6858
this._intrinsicAttributes.addAttribute(key, value)
6959
}
7060

61+
getIntrinsicAttributes() {
62+
return this._intrinsicAttributes
63+
}
64+
7165
/**
7266
* Add a key/value pair to the Span's custom/user attributes collection.
7367
*
@@ -169,6 +163,7 @@ class StreamingSpanEvent {
169163
// Timestamp in milliseconds, duration in seconds. Yay consistency!
170164
span.addIntrinsicAttribute('timestamp', segment.timer.start)
171165
span.addIntrinsicAttribute('duration', segment.timer.getDurationInMillis() / 1000)
166+
addSpanKind({ segment, span })
172167

173168
return span
174169
}
@@ -196,7 +191,7 @@ class StreamingHttpSpanEvent extends StreamingSpanEvent {
196191

197192
this.addIntrinsicAttribute('category', CATEGORIES.HTTP)
198193
this.addIntrinsicAttribute('component', library || HTTP_LIBRARY)
199-
this.addIntrinsicAttribute('span.kind', CLIENT_KIND)
194+
this.addIntrinsicAttribute('span.kind', SPAN_KIND.CLIENT)
200195

201196
if (url) {
202197
this.addAgentAttribute('http.url', url)
@@ -213,7 +208,7 @@ class StreamingHttpSpanEvent extends StreamingSpanEvent {
213208
}
214209

215210
static isHttpSegment(segment) {
216-
return EXTERNAL_REGEX.test(segment.name)
211+
return REGEXS.CLIENT.EXTERNAL.test(segment.name)
217212
}
218213
}
219214

@@ -248,7 +243,7 @@ class StreamingDatastoreSpanEvent extends StreamingSpanEvent {
248243
super(traceId, agentAttrs, customAttributes)
249244

250245
this.addIntrinsicAttribute('category', CATEGORIES.DATASTORE)
251-
this.addIntrinsicAttribute('span.kind', CLIENT_KIND)
246+
this.addIntrinsicAttribute('span.kind', SPAN_KIND.CLIENT)
252247

253248
if (product) {
254249
this.addIntrinsicAttribute('component', product)
@@ -288,7 +283,7 @@ class StreamingDatastoreSpanEvent extends StreamingSpanEvent {
288283
/* eslint-enable camelcase */
289284

290285
static isDatastoreSegment(segment) {
291-
return DATASTORE_REGEX.test(segment.name)
286+
return REGEXS.CLIENT.DATASTORE.test(segment.name)
292287
}
293288
}
294289

lib/transaction/trace/segment.js

+3
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ function TraceSegment({ id, config, name, collect, parentId, root, isRoot = fals
6464
this.state = STATE.EXTERNAL
6565
this.async = true
6666
this.ignore = false
67+
// only use to specify spanKind on segments created with `api.startBackgroundTransaction` and `api.startWebTransaction`
68+
// we typically determine the span kind based on the segment name
69+
this.spanKind = null
6770
}
6871

6972
TraceSegment.prototype.getSpanContext = function getSpanContext() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2025 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
8+
/**
9+
* Gets all spans created during transaction and iterates over to assert the span kind
10+
* for each segment in the transaction matches the provided expected `segments`.
11+
*
12+
* @example
13+
* assertSpanKind({
14+
* agent,
15+
* segments: [{ name: 'expectedSegment', kind: 'server' }]
16+
* })
17+
*
18+
* @param {object} params to function
19+
* @param {Agent} params.agent instance of agent
20+
* @param {segments} params.segments collection of span names and their respective span kind
21+
* @param {object} params.assert assertion library
22+
*/
23+
module.exports = function assertSpanKind({ agent, segments, assert = require('node:assert') }) {
24+
const spans = agent.spanEventAggregator.getEvents()
25+
if (segments) {
26+
segments.forEach((segment) => {
27+
const span = spans.find((s) => s.intrinsics.name === segment.name)
28+
if (!span) {
29+
assert.fail(`Could not find span: ${segment.name}`)
30+
}
31+
assert.equal(span.intrinsics['span.kind'], segment.kind)
32+
})
33+
} else {
34+
assert.fail('Custom assertion must either pass in an array of span kinds or a collection of segment names to span kind')
35+
}
36+
}

test/unit/api/api-start-background-transaction.test.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const test = require('node:test')
88
const assert = require('node:assert')
99
const API = require('../../../api')
1010
const helper = require('../../lib/agent_helper')
11-
const { assertCLMAttrs } = require('../../lib/custom-assertions')
11+
const { assertCLMAttrs, assertSpanKind } = require('../../lib/custom-assertions')
1212

1313
function nested({ api }) {
1414
api.startBackgroundTransaction('nested', function nestedHandler() {})
@@ -72,6 +72,7 @@ test('Agent API - startBackgroundTransaction', async (t) => {
7272
})
7373

7474
assert.ok(!transaction.isActive())
75+
assertSpanKind({ agent, segments: [{ name: transaction.name, kind: 'server' }] })
7576

7677
end()
7778
})

test/unit/api/api-start-web-transaction.test.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const test = require('node:test')
88
const assert = require('node:assert')
99
const API = require('../../../api')
1010
const helper = require('../../lib/agent_helper')
11-
const { assertCLMAttrs } = require('../../lib/custom-assertions')
11+
const { assertCLMAttrs, assertSpanKind } = require('../../lib/custom-assertions')
1212
function nested({ api }) {
1313
api.startWebTransaction('nested', function nestedHandler() {})
1414
}
@@ -70,6 +70,7 @@ test('Agent API - startWebTransaction', async (t) => {
7070
})
7171

7272
assert.ok(!transaction.isActive())
73+
assertSpanKind({ agent, segments: [{ name: transaction.name, kind: 'server' }] })
7374
end()
7475
})
7576

test/unit/spans/span-event.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ test('fromSegment()', async (t) => {
9393
assert.ok(span.intrinsics.duration >= 0.03 && span.intrinsics.duration <= 0.3)
9494

9595
// Generic should not have 'span.kind' or 'component'
96-
assert.equal(span.intrinsics['span.kind'], null)
96+
assert.equal(span.intrinsics['span.kind'], 'internal')
9797
assert.equal(span.intrinsics.component, null)
9898

9999
assert.ok(span.customAttributes)

test/unit/spans/streaming-span-event.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,13 @@ test('fromSegment()', async (t) => {
8585
assert.deepEqual(span._intrinsicAttributes.priority, { [INT_TYPE]: 42 })
8686
assert.deepEqual(span._intrinsicAttributes.name, { [STRING_TYPE]: 'timers.setTimeout' })
8787
assert.deepEqual(span._intrinsicAttributes.timestamp, { [INT_TYPE]: segment.timer.start })
88+
assert.deepEqual(span._intrinsicAttributes['span.kind'], { [STRING_TYPE]: 'internal' })
8889

8990
assert.ok(span._intrinsicAttributes.duration)
9091
assert.ok(span._intrinsicAttributes.duration[DOUBLE_TYPE])
9192

9293
// Generic should not have 'span.kind' or 'component'
9394
const hasIntrinsic = Object.hasOwnProperty.bind(span._intrinsicAttributes)
94-
assert.ok(!hasIntrinsic('span.kind'))
9595
assert.ok(!hasIntrinsic('component'))
9696

9797
const customAttributes = span._customAttributes

0 commit comments

Comments
 (0)