Skip to content

Commit ed2e095

Browse files
committed
feat: Added segment synthesis for otel producer spans (#2839)
1 parent e044b40 commit ed2e095

File tree

9 files changed

+142
-11
lines changed

9 files changed

+142
-11
lines changed

lib/otel/rules.js

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const srcJson = require('./rules.json')
4545
class Rule {
4646
static OTEL_SPAN_KIND_SERVER = 'server'
4747
static OTEL_SPAN_KIND_CLIENT = 'client'
48+
static OTEL_SPAN_KIND_PRODUCER = 'producer'
4849

4950
#name
5051
#spanKinds
@@ -82,15 +83,15 @@ class Rule {
8283
}
8384

8485
get isClientRule() {
85-
return this.#spanKinds.includes(Rule.OTEL_SPAN_KIND_CLIENT) || this.isProducer
86+
return this.#spanKinds.includes(Rule.OTEL_SPAN_KIND_CLIENT)
8687
}
8788

8889
get isConsumer() {
8990
return this.#spanKinds.includes('consumer')
9091
}
9192

92-
get isProducer() {
93-
return this.#spanKinds.includes('producer')
93+
get isProducerRule() {
94+
return this.#spanKinds.includes(Rule.OTEL_SPAN_KIND_PRODUCER)
9495
}
9596

9697
get isServerRule() {
@@ -126,6 +127,7 @@ class RulesEngine {
126127
#fallbackServerRules = new Map()
127128
#clientRules = new Map()
128129
#fallbackClientRules = new Map()
130+
#fallbackProducerRules = new Map()
129131

130132
constructor() {
131133
for (const inputRule of srcJson) {
@@ -136,6 +138,8 @@ class RulesEngine {
136138
this.#fallbackServerRules.set(rule.name, rule)
137139
} else if (rule.isClientRule === true) {
138140
this.#fallbackClientRules.set(rule.name, rule)
141+
} else if (rule.isProducerRule === true) {
142+
this.#fallbackProducerRules.set(rule.name, rule)
139143
}
140144
continue
141145
}
@@ -178,8 +182,7 @@ class RulesEngine {
178182
break
179183
}
180184

181-
case SpanKind.CLIENT:
182-
case SpanKind.PRODUCER: {
185+
case SpanKind.CLIENT: {
183186
for (const rule of this.#clientRules.values()) {
184187
if (rule.matches(otelSpan) === true) {
185188
result = rule
@@ -194,6 +197,18 @@ class RulesEngine {
194197
}
195198
break
196199
}
200+
201+
// there currently are no producer rules, just fallback
202+
// if we add new rules they will have to be wired up
203+
case SpanKind.PRODUCER: {
204+
for (const rule of this.#fallbackProducerRules.values()) {
205+
if (rule.matches(otelSpan) === true) {
206+
result = rule
207+
break
208+
}
209+
}
210+
break
211+
}
197212
}
198213

199214
return result

lib/otel/rules.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,7 @@
455455
},
456456
{
457457
"name": "FallbackProducer",
458+
"type": "producer",
458459
"matcher": {
459460
"required_span_kinds": [
460461
"producer"

lib/otel/segment-synthesis.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
'use strict'
77
const { RulesEngine } = require('./rules')
88
const defaultLogger = require('../logger').child({ component: 'segment-synthesizer' })
9-
const { createDbSegment, createHttpExternalSegment, createServerSegment } = require('./segments')
9+
const {
10+
createDbSegment,
11+
createHttpExternalSegment,
12+
createServerSegment,
13+
createProducerSegment
14+
} = require('./segments')
1015

1116
class SegmentSynthesizer {
1217
constructor(agent, { logger = defaultLogger } = {}) {
@@ -27,10 +32,12 @@ class SegmentSynthesizer {
2732
}
2833

2934
switch (rule.type) {
30-
case 'external':
31-
return createHttpExternalSegment(this.agent, otelSpan)
3235
case 'db':
3336
return createDbSegment(this.agent, otelSpan)
37+
case 'external':
38+
return createHttpExternalSegment(this.agent, otelSpan)
39+
case 'producer':
40+
return createProducerSegment(this.agent, otelSpan)
3441
case 'server':
3542
return createServerSegment(this.agent, otelSpan)
3643
default:

lib/otel/segments/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
const createHttpExternalSegment = require('./http-external')
88
const createDbSegment = require('./database')
99
const createServerSegment = require('./server')
10+
const createProducerSegment = require('./producer')
1011

1112
module.exports = {
1213
createDbSegment,
1314
createHttpExternalSegment,
15+
createProducerSegment,
1416
createServerSegment
1517
}

lib/otel/segments/producer.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2024 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
const {
8+
SEMATTRS_MESSAGING_SYSTEM,
9+
SEMATTRS_MESSAGING_DESTINATION,
10+
SEMATTRS_MESSAGING_DESTINATION_KIND
11+
} = require('@opentelemetry/semantic-conventions')
12+
13+
module.exports = function createProducerSegment(agent, otelSpan) {
14+
const context = agent.tracer.getContext()
15+
const name = setName(otelSpan)
16+
const segment = agent.tracer.createSegment({
17+
name,
18+
parent: context.segment,
19+
transaction: context.transaction
20+
})
21+
return { segment, transaction: context.transaction }
22+
}
23+
24+
function setName(otelSpan) {
25+
const system = otelSpan.attributes[SEMATTRS_MESSAGING_SYSTEM] || 'Unknown'
26+
const destKind = otelSpan.attributes[SEMATTRS_MESSAGING_DESTINATION_KIND] || 'Unknown'
27+
const destination = otelSpan.attributes[SEMATTRS_MESSAGING_DESTINATION] || 'Unknown'
28+
return `MessageBroker/${system}/${destKind}/Produce/Named/${destination}`
29+
}

test/unit/lib/otel/fixtures/index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const {
1414
const createSpan = require('./span')
1515
const createHttpClientSpan = require('./http-client')
1616
const { createRpcServerSpan, createHttpServerSpan, createBaseHttpSpan } = require('./server')
17+
const { createQueueProducerSpan, createTopicProducerSpan } = require('./producer')
1718

1819
module.exports = {
1920
createBaseHttpSpan,
@@ -23,7 +24,9 @@ module.exports = {
2324
createHttpServerSpan,
2425
createMemcachedDbSpan,
2526
createMongoDbSpan,
27+
createQueueProducerSpan,
2628
createRedisDbSpan,
2729
createRpcServerSpan,
28-
createSpan
30+
createSpan,
31+
createTopicProducerSpan
2932
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2024 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
const {
8+
MessagingDestinationKindValues,
9+
SEMATTRS_MESSAGING_SYSTEM,
10+
SEMATTRS_MESSAGING_DESTINATION,
11+
SEMATTRS_MESSAGING_DESTINATION_KIND
12+
} = require('@opentelemetry/semantic-conventions')
13+
const { SpanKind } = require('@opentelemetry/api')
14+
const createSpan = require('./span')
15+
16+
function createTopicProducerSpan({ parentId, tracer, tx, name = 'test-span' }) {
17+
const span = createSpan({ name, kind: SpanKind.PRODUCER, parentId, tracer, tx })
18+
span.setAttribute(SEMATTRS_MESSAGING_SYSTEM, 'messaging-lib')
19+
span.setAttribute(SEMATTRS_MESSAGING_DESTINATION_KIND, MessagingDestinationKindValues.TOPIC)
20+
span.setAttribute(SEMATTRS_MESSAGING_DESTINATION, 'test-topic')
21+
return span
22+
}
23+
24+
function createQueueProducerSpan({ parentId, tracer, tx, name = 'test-span' }) {
25+
const span = createSpan({ name, kind: SpanKind.PRODUCER, parentId, tracer, tx })
26+
span.setAttribute(SEMATTRS_MESSAGING_SYSTEM, 'messaging-lib')
27+
span.setAttribute(SEMATTRS_MESSAGING_DESTINATION_KIND, MessagingDestinationKindValues.QUEUE)
28+
span.setAttribute(SEMATTRS_MESSAGING_DESTINATION, 'test-queue')
29+
return span
30+
}
31+
32+
module.exports = {
33+
createQueueProducerSpan,
34+
createTopicProducerSpan
35+
}

test/unit/lib/otel/rules.test.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,22 @@ test('fallback server rule is met', () => {
5757

5858
test('fallback client rule is met', () => {
5959
const engine = new RulesEngine()
60-
const span = new Span(tracer, ROOT_CONTEXT, 'test-span', spanContext, SpanKind.PRODUCER, parentId)
60+
const span = new Span(tracer, ROOT_CONTEXT, 'test-span', spanContext, SpanKind.CLIENT, parentId)
6161
span.setAttribute('foo.bar', 'baz')
6262
span.end()
6363

6464
const rule = engine.test(span)
6565
assert.notEqual(rule, undefined)
6666
assert.equal(rule.name, 'FallbackClient')
6767
})
68+
69+
test('fallback producer rule is met', () => {
70+
const engine = new RulesEngine()
71+
const span = new Span(tracer, ROOT_CONTEXT, 'test-span', spanContext, SpanKind.PRODUCER, parentId)
72+
span.setAttribute('foo.bar', 'baz')
73+
span.end()
74+
75+
const rule = engine.test(span)
76+
assert.notEqual(rule, undefined)
77+
assert.equal(rule.name, 'FallbackProducer')
78+
})

test/unit/lib/otel/segment-synthesizer.test.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ const {
2121
createMongoDbSpan,
2222
createRedisDbSpan,
2323
createRpcServerSpan,
24-
createMemcachedDbSpan
24+
createMemcachedDbSpan,
25+
createTopicProducerSpan,
26+
createQueueProducerSpan
2527
} = require('./fixtures')
2628
const { SEMATTRS_DB_SYSTEM } = require('@opentelemetry/semantic-conventions')
2729
const { SpanKind } = require('@opentelemetry/api')
@@ -186,6 +188,32 @@ test('should create base http server segment', (t) => {
186188
assert.equal(transaction.name, expectedName)
187189
})
188190

191+
test('should create topic producer segment', (t, end) => {
192+
const { agent, synthesizer, parentId, tracer } = t.nr
193+
helper.runInTransaction(agent, (tx) => {
194+
const span = createTopicProducerSpan({ tx, parentId, tracer })
195+
const { segment, transaction } = synthesizer.synthesize(span)
196+
assert.equal(tx.id, transaction.id)
197+
assert.equal(segment.name, 'MessageBroker/messaging-lib/topic/Produce/Named/test-topic')
198+
assert.equal(segment.parentId, tx.trace.root.id)
199+
tx.end()
200+
end()
201+
})
202+
})
203+
204+
test('should create queue producer segment', (t, end) => {
205+
const { agent, synthesizer, parentId, tracer } = t.nr
206+
helper.runInTransaction(agent, (tx) => {
207+
const span = createQueueProducerSpan({ tx, parentId, tracer })
208+
const { segment, transaction } = synthesizer.synthesize(span)
209+
assert.equal(tx.id, transaction.id)
210+
assert.equal(segment.name, 'MessageBroker/messaging-lib/queue/Produce/Named/test-queue')
211+
assert.equal(segment.parentId, tx.trace.root.id)
212+
tx.end()
213+
end()
214+
})
215+
})
216+
189217
test('should log warning span does not match a rule', (t, end) => {
190218
const { agent, synthesizer, loggerMock, parentId, tracer } = t.nr
191219

0 commit comments

Comments
 (0)