Skip to content

Commit cac0902

Browse files
committed
feat: Added segment synthesizer and provided ability to convert http client otel spans to external http trace segments (#2745)
1 parent 18a5f2b commit cac0902

File tree

13 files changed

+476
-85
lines changed

13 files changed

+476
-85
lines changed

THIRD_PARTY_NOTICES.md

Lines changed: 223 additions & 13 deletions
Large diffs are not rendered by default.

lib/metrics/recorders/database.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,10 @@ function recordQueryMetrics(segment, scope, transaction) {
6161

6262
if (this.raw) {
6363
transaction.agent.queries.add({
64-
segment,
64+
segment,
6565
transaction,
66-
type: this.type.toLowerCase(),
67-
query: this.raw,
66+
type: this.type.toLowerCase(),
67+
query: this.raw,
6868
trace: this.trace
6969
})
7070
}

lib/otel/rules.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class Rule {
4949
#name
5050
#spanKinds
5151
#requiredAttributes
52+
#type
5253
#mappings
5354

5455
/**
@@ -63,6 +64,7 @@ class Rule {
6364
}
6465

6566
this.#name = input.name
67+
this.#type = input.type
6668
this.#spanKinds = input.matcher.required_span_kinds?.map((v) => v.toLowerCase()) ?? []
6769
this.#requiredAttributes = input.matcher.required_attribute_keys ?? []
6870
this.#mappings = input.target.attribute_mappings ?? []
@@ -72,6 +74,10 @@ class Rule {
7274
return this.#name
7375
}
7476

77+
get type() {
78+
return this.#type
79+
}
80+
7581
get isServerRule() {
7682
return this.#spanKinds.includes(Rule.OTEL_SPAN_KIND_SERVER)
7783
}

lib/otel/rules.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@
229229
},
230230
{
231231
"name": "OtelDbClientRedis1_24",
232+
"type": "db",
232233
"matcher": {
233234
"required_span_kinds": [
234235
"client"
@@ -267,6 +268,7 @@
267268
},
268269
{
269270
"name": "OtelDbClient1_24",
271+
"type": "db",
270272
"matcher": {
271273
"required_span_kinds": [
272274
"client"
@@ -302,6 +304,7 @@
302304
},
303305
{
304306
"name": "OtelHttpClient1_23",
307+
"type": "external",
305308
"matcher": {
306309
"required_metric_names": [
307310
"http.client.request.duration"
@@ -338,6 +341,7 @@
338341
},
339342
{
340343
"name": "OtelHttpClient1_20",
344+
"type": "external",
341345
"matcher": {
342346
"required_metric_names": [
343347
"http.client.duration"
@@ -410,6 +414,7 @@
410414
},
411415
{
412416
"name": "FallbackClient",
417+
"type": "external",
413418
"matcher": {
414419
"required_metric_names": [
415420
"rpc.client.duration",

lib/otel/segment-synthesis.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2024 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
const { RulesEngine } = require('./rules')
8+
const defaultLogger = require('../logger').child({ component: 'segment-synthesizer' })
9+
const NAMES = require('../metrics/names')
10+
const { SEMATTRS_HTTP_HOST } = require('@opentelemetry/semantic-conventions')
11+
12+
class SegmentSynthesizer {
13+
constructor(agent, { logger = defaultLogger } = {}) {
14+
this.agent = agent
15+
this.logger = logger
16+
this.engine = new RulesEngine()
17+
}
18+
19+
synthesize(otelSpan) {
20+
const rule = this.engine.test(otelSpan)
21+
if (!rule?.type) {
22+
this.logger.debug(
23+
'Cannot match a rule to span name: %s, kind %s',
24+
otelSpan?.name,
25+
otelSpan?.kind
26+
)
27+
return
28+
}
29+
30+
if (rule?.type === 'external') {
31+
return this.createExternalSegment(otelSpan)
32+
}
33+
this.logger.debug('Found type: %s, no synthesize rule currently built', rule.type)
34+
}
35+
36+
// TODO: should we move these to somewhere else and use in the places
37+
// where external segments are created in our agent
38+
createExternalSegment(otelSpan) {
39+
const context = this.agent.tracer.getContext()
40+
const host = otelSpan.attributes[SEMATTRS_HTTP_HOST] || 'Unknown'
41+
const name = NAMES.EXTERNAL.PREFIX + host
42+
return this.agent.tracer.createSegment({
43+
name,
44+
parent: context.segment,
45+
transaction: context.transaction
46+
})
47+
}
48+
}
49+
50+
module.exports = SegmentSynthesizer

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@
198198
"@grpc/proto-loader": "^0.7.5",
199199
"@newrelic/security-agent": "^2.0.0",
200200
"@opentelemetry/api": "^1.9.0",
201+
"@opentelemetry/semantic-conventions": "^1.27.0",
201202
"@tyriar/fibonacci-heap": "^2.0.7",
202203
"concat-stream": "^2.0.0",
203204
"https-proxy-agent": "^7.0.1",
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright 2024 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
const test = require('node:test')
8+
const assert = require('node:assert')
9+
10+
const helper = require('../../../lib/agent_helper')
11+
const { ROOT_CONTEXT, SpanKind, TraceFlags } = require('@opentelemetry/api')
12+
const { BasicTracerProvider, Span } = require('@opentelemetry/sdk-trace-base')
13+
const SegmentSynthesizer = require('../../../../lib/otel/segment-synthesis')
14+
const {
15+
SEMATTRS_DB_SYSTEM,
16+
SEMATTRS_HTTP_HOST,
17+
SEMATTRS_HTTP_METHOD
18+
} = require('@opentelemetry/semantic-conventions')
19+
const createMockLogger = require('../../mocks/logger')
20+
21+
test.beforeEach((ctx) => {
22+
const loggerMock = createMockLogger()
23+
const agent = helper.loadMockedAgent()
24+
const synthesizer = new SegmentSynthesizer(agent, { logger: loggerMock })
25+
const tracer = new BasicTracerProvider().getTracer('default')
26+
const parentId = '5c1c63257de34c67'
27+
ctx.nr = {
28+
agent,
29+
loggerMock,
30+
parentId,
31+
synthesizer,
32+
tracer
33+
}
34+
})
35+
36+
test.afterEach((ctx) => {
37+
helper.unloadAgent(ctx.nr.agent)
38+
})
39+
40+
test('should create http external segment from otel http client span', (t, end) => {
41+
const { agent, synthesizer, parentId, tracer } = t.nr
42+
helper.runInTransaction(agent, (tx) => {
43+
const spanContext = {
44+
traceId: tx.trace.id,
45+
spanId: tx.trace.root.id,
46+
traceFlags: TraceFlags.SAMPLED
47+
}
48+
const span = new Span(tracer, ROOT_CONTEXT, 'test-span', spanContext, SpanKind.CLIENT, parentId)
49+
span.setAttribute(SEMATTRS_HTTP_METHOD, 'GET')
50+
span.setAttribute(SEMATTRS_HTTP_HOST, 'newrelic.com')
51+
const segment = synthesizer.synthesize(span)
52+
assert.equal(segment.name, 'External/newrelic.com')
53+
assert.equal(segment.parentId, tx.trace.root.id)
54+
tx.end()
55+
end()
56+
})
57+
})
58+
59+
test('should log warning if a rule does have a synthesis for the given type', (t, end) => {
60+
const { agent, synthesizer, loggerMock, parentId, tracer } = t.nr
61+
62+
helper.runInTransaction(agent, (tx) => {
63+
const spanContext = {
64+
traceId: tx.trace.id,
65+
spanId: tx.trace.root.id,
66+
traceFlags: TraceFlags.SAMPLED
67+
}
68+
const span = new Span(tracer, ROOT_CONTEXT, 'test-span', spanContext, SpanKind.CLIENT, parentId)
69+
span.setAttribute(SEMATTRS_DB_SYSTEM, 'postgres')
70+
const segment = synthesizer.synthesize(span)
71+
assert.ok(!segment)
72+
assert.deepEqual(loggerMock.debug.args[0], [
73+
'Found type: %s, no synthesize rule currently built',
74+
'db'
75+
])
76+
tx.end()
77+
end()
78+
})
79+
})
80+
81+
test('should log warning span does not match a rule', (t, end) => {
82+
const { agent, synthesizer, loggerMock, parentId, tracer } = t.nr
83+
84+
helper.runInTransaction(agent, (tx) => {
85+
const spanContext = {
86+
traceId: tx.trace.id,
87+
spanId: tx.trace.root.id,
88+
traceFlags: TraceFlags.SAMPLED
89+
}
90+
91+
const span = new Span(tracer, ROOT_CONTEXT, 'test-span', spanContext, 'bogus', parentId)
92+
const segment = synthesizer.synthesize(span)
93+
assert.ok(!segment)
94+
assert.deepEqual(loggerMock.debug.args[0], [
95+
'Cannot match a rule to span name: %s, kind %s',
96+
'test-span',
97+
'bogus'
98+
])
99+
tx.end()
100+
end()
101+
})
102+
})

test/versioned/aws-sdk-v3/client-dynamodb.test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,9 @@ test('DynamoDB', async (t) => {
116116
}
117117
tx.end()
118118
const root = tx.trace.root
119-
const segments = common.checkAWSAttributes({
120-
trace: tx.trace,
121-
segment: root,
119+
const segments = common.checkAWSAttributes({
120+
trace: tx.trace,
121+
segment: root,
122122
pattern: common.DATASTORE_PATTERN
123123
})
124124

test/versioned/koa/code-level-metrics.test.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,12 @@ test('vanilla koa, no router', async (t) => {
102102
{
103103
segments: [
104104
{
105-
segment: one,
105+
segment: one,
106106
name: 'one',
107107
filepath: 'code-level-metrics.test.js'
108108
},
109109
{
110-
segment: two,
110+
segment: two,
111111
name: 'two',
112112
filepath: 'code-level-metrics.test.js'
113113
}
@@ -168,17 +168,17 @@ test('using koa-router', async (t) => {
168168
{
169169
segments: [
170170
{
171-
segment: dispatch,
171+
segment: dispatch,
172172
name: 'dispatch',
173173
filepath: 'koa-router/lib/router.js'
174174
},
175175
{
176-
segment: appLevel,
176+
segment: appLevel,
177177
name: 'appLevelMiddleware',
178178
filepath: 'code-level-metrics.test.js'
179179
},
180180
{
181-
segment: secondMw,
181+
segment: secondMw,
182182
name: 'secondMiddleware',
183183
filepath: 'code-level-metrics.test.js'
184184
}
@@ -239,17 +239,17 @@ test('using @koa/router', async (t) => {
239239
{
240240
segments: [
241241
{
242-
segment: dispatch,
242+
segment: dispatch,
243243
name: 'dispatch',
244244
filepath: '@koa/router/lib/router.js'
245245
},
246246
{
247-
segment: appLevel,
247+
segment: appLevel,
248248
name: 'appLevelMiddleware',
249249
filepath: 'code-level-metrics.test.js'
250250
},
251251
{
252-
segment: secondMw,
252+
segment: secondMw,
253253
name: 'secondMiddleware',
254254
filepath: 'code-level-metrics.test.js'
255255
}

test/versioned/memcached/memcached.test.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -874,7 +874,11 @@ test('memcached instrumentation', { timeout: 5000 }, async function (t) {
874874
assert.ok(!err)
875875
transaction.end()
876876
checkParams(firstSegment, 'server1', '1111')
877-
checkParams(transaction.trace.getParent(agent.tracer.getSegment().parentId), 'server2', '2222')
877+
checkParams(
878+
transaction.trace.getParent(agent.tracer.getSegment().parentId),
879+
'server2',
880+
'2222'
881+
)
878882
end()
879883
})
880884
})

test/versioned/nextjs/attributes.test.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,8 @@ test('Next.js', async (t) => {
258258
enabled,
259259
skipFull: true
260260
})
261-
})
261+
}
262+
)
262263

263264
await t.test('should not add CLM attrs to static page segment', async (t) => {
264265
agent.config.code_level_metrics = { enabled }

test/versioned/pg/pg.common.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ module.exports = function runTests(name, clientFactory) {
309309
assert.ok(agent.getTransaction(), 'transaction should still still be visible')
310310
assert.equal(selectResults.rows[0][COL], colVal, 'Postgres client should still work')
311311
transaction.end()
312-
verify(assert, transaction)
312+
verify(assert, transaction)
313313
end()
314314
} catch (err) {
315315
assert.ifError(err)
@@ -465,7 +465,7 @@ module.exports = function runTests(name, clientFactory) {
465465

466466
transaction.end()
467467
pool.end()
468-
verify(plan, transaction)
468+
verify(plan, transaction)
469469
})
470470
})
471471
})
@@ -514,7 +514,7 @@ module.exports = function runTests(name, clientFactory) {
514514
}
515515

516516
done(true)
517-
verify(plan, transaction)
517+
verify(plan, transaction)
518518
})
519519
})
520520
})

0 commit comments

Comments
 (0)