diff --git a/lib/otel/segment-synthesis.js b/lib/otel/segment-synthesis.js index bebc9ba287..6619ef37b6 100644 --- a/lib/otel/segment-synthesis.js +++ b/lib/otel/segment-synthesis.js @@ -7,6 +7,7 @@ const { RulesEngine } = require('./rules') const defaultLogger = require('../logger').child({ component: 'segment-synthesizer' }) const { DatabaseSegment, HttpExternalSegment, ServerSegment } = require('./segments') +const createConsumerSegment = require('./segments/consumer') class SegmentSynthesizer { constructor(agent, { logger = defaultLogger } = {}) { @@ -27,14 +28,24 @@ class SegmentSynthesizer { } switch (rule.type) { - case 'external': + case 'external': { return new HttpExternalSegment(this.agent, otelSpan) - case 'db': + } + + case 'db': { return new DatabaseSegment(this.agent, otelSpan) - case 'server': + } + + case 'server': { + if (rule.isConsumer === true) { + return createConsumerSegment({ agent: this.agent, otelSpan }) + } return new ServerSegment(this.agent, otelSpan) - default: + } + + default: { this.logger.debug('Found type: %s, no synthesis rule currently built', rule.type) + } } } } diff --git a/lib/otel/segments/consumer.js b/lib/otel/segments/consumer.js new file mode 100644 index 0000000000..23fb03b422 --- /dev/null +++ b/lib/otel/segments/consumer.js @@ -0,0 +1,47 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +module.exports = createConsumerSegment + +// Notes: +// + https://github.com/open-telemetry/semantic-conventions/blob/v1.24.0/docs/messaging/messaging-spans.md +// + We probably want to inspect `messaging.system` so that we can generate +// attributes according to our own internal specs. + +const Transaction = require('../../transaction/') +const { DESTINATIONS, TYPES } = Transaction + +const { + // SEMATTRS_MESSAGING_SYSTEM, + SEMATTRS_MESSAGING_DESTINATION, + SEMATTRS_MESSAGING_OPERATION +} = require('@opentelemetry/semantic-conventions') + +function createConsumerSegment({ agent, otelSpan }) { + const transaction = new Transaction(agent) + transaction.type = TYPES.BG + + const operation = otelSpan.attributes[SEMATTRS_MESSAGING_OPERATION] + const destination = otelSpan.attributes[SEMATTRS_MESSAGING_DESTINATION] ?? 'unknown' + const segmentName = `OtherTransaction/consumer/${operation}/${destination}` + + transaction.trace.attributes.addAttribute( + DESTINATIONS.TRANS_SCOPE, + 'message.queueName', + destination + ) + transaction.name = segmentName + + const segment = agent.tracer.createSegment({ + name: segmentName, + parent: transaction.trace.root, + transaction + }) + transaction.baseSegment = segment + + return { segment, transaction } +} diff --git a/lib/transaction/index.js b/lib/transaction/index.js index 7d9f20c7c5..fc89a25298 100644 --- a/lib/transaction/index.js +++ b/lib/transaction/index.js @@ -157,6 +157,7 @@ function Transaction(agent) { agent.emit('transactionStarted', this) } +Transaction.DESTINATIONS = DESTS Transaction.TYPES = TYPES Transaction.TYPES_SET = TYPES_SET Transaction.TRANSPORT_TYPES = TRANSPORT_TYPES diff --git a/test/unit/lib/otel/consumer.test.js b/test/unit/lib/otel/consumer.test.js new file mode 100644 index 0000000000..acf84d09c3 --- /dev/null +++ b/test/unit/lib/otel/consumer.test.js @@ -0,0 +1,63 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +// Tests to verify that we can map OTEL "consumer" spans to NR segments. + +const test = require('node:test') +const assert = require('node:assert') + +const { BasicTracerProvider } = require('@opentelemetry/sdk-trace-base') +const { SpanKind } = require('@opentelemetry/api') + +const { DESTINATIONS } = require('../../../../lib/transaction') +const helper = require('../../../lib/agent_helper') +const createSpan = require('./fixtures/span') +const SegmentSynthesizer = require('../../../../lib/otel/segment-synthesis') + +test.beforeEach((ctx) => { + const logs = [] + const logger = { + debug(...args) { + logs.push(args) + } + } + const agent = helper.loadMockedAgent() + const synth = new SegmentSynthesizer(agent, { logger }) + const tracer = new BasicTracerProvider().getTracer('default') + + ctx.nr = { + agent, + logger, + logs, + synth, + tracer + } +}) + +test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) +}) + +test('should create consumer segment from otel span', (t) => { + const { synth, tracer } = t.nr + const span = createSpan({ tracer, kind: SpanKind.CONSUMER }) + span.setAttribute('messaging.operation', 'receive') + + const expectedName = 'OtherTransaction/consumer/receive/unknown' + const { segment, transaction } = synth.synthesize(span) + assert.equal(segment.name, expectedName) + assert.equal(segment.parentId, segment.root.id) + assert.equal(transaction.name, expectedName) + assert.equal(transaction.type, 'bg') + assert.equal(transaction.baseSegment, segment) + assert.equal( + transaction.trace.attributes.get(DESTINATIONS.TRANS_SCOPE)['message.queueName'], + 'unknown' + ) + + transaction.end() +})