Skip to content

Commit

Permalink
feat: Added segment synthesizer and provided ability to convert http …
Browse files Browse the repository at this point in the history
…client otel spans to external http trace segments
  • Loading branch information
bizob2828 committed Nov 15, 2024
1 parent 8b082c7 commit e81adc9
Show file tree
Hide file tree
Showing 7 changed files with 452 additions and 66 deletions.
236 changes: 223 additions & 13 deletions THIRD_PARTY_NOTICES.md

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions lib/otel/rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class Rule {
#name
#spanKinds
#requiredAttributes
#type
#mappings

/**
Expand All @@ -63,6 +64,7 @@ class Rule {
}

this.#name = input.name
this.#type = input.type
this.#spanKinds = input.matcher.required_span_kinds?.map((v) => v.toLowerCase()) ?? []
this.#requiredAttributes = input.matcher.required_attribute_keys ?? []
this.#mappings = input.target.attribute_mappings ?? []
Expand All @@ -72,6 +74,10 @@ class Rule {
return this.#name
}

get type() {
return this.#type
}

get isServerRule() {
return this.#spanKinds.includes(Rule.OTEL_SPAN_KIND_SERVER)
}
Expand Down
5 changes: 5 additions & 0 deletions lib/otel/rules.json
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@
},
{
"name": "OtelDbClientRedis1_24",
"type": "db",
"matcher": {
"required_span_kinds": [
"client"
Expand Down Expand Up @@ -267,6 +268,7 @@
},
{
"name": "OtelDbClient1_24",
"type": "db",
"matcher": {
"required_span_kinds": [
"client"
Expand Down Expand Up @@ -302,6 +304,7 @@
},
{
"name": "OtelHttpClient1_23",
"type": "external",
"matcher": {
"required_metric_names": [
"http.client.request.duration"
Expand Down Expand Up @@ -338,6 +341,7 @@
},
{
"name": "OtelHttpClient1_20",
"type": "external",
"matcher": {
"required_metric_names": [
"http.client.duration"
Expand Down Expand Up @@ -410,6 +414,7 @@
},
{
"name": "FallbackClient",
"type": "external",
"matcher": {
"required_metric_names": [
"rpc.client.duration",
Expand Down
50 changes: 50 additions & 0 deletions lib/otel/segment-synthesis.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2024 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

'use strict'
const { RulesEngine } = require('./rules')
const defaultLogger = require('../logger').child({ component: 'segment-synthesizer' })
const NAMES = require('../metrics/names')
const { SEMATTRS_HTTP_HOST } = require('@opentelemetry/semantic-conventions')

class SegmentSynthesizer {
constructor(agent, { logger = defaultLogger } = {}) {
this.agent = agent
this.logger = logger
this.engine = new RulesEngine()
}

synthesize(otelSpan) {
const rule = this.engine.test(otelSpan)
if (!rule?.type) {
this.logger.debug(
'Cannot match a rule to span name: %s, kind %s',
otelSpan?.name,
otelSpan?.kind
)
return
}

if (rule?.type === 'external') {
return this.createExternalSegment(otelSpan)
}
this.logger.debug('Found type: %s, no synthesize rule currently built', rule.type)
}

// TODO: should we move these to somewhere else and use in the places
// where external segments are created in our agent
createExternalSegment(otelSpan) {
const context = this.agent.tracer.getContext()
const host = otelSpan.attributes[SEMATTRS_HTTP_HOST] || 'Unknown'
const name = NAMES.EXTERNAL.PREFIX + host
return this.agent.tracer.createSegment({
name,
parent: context.segment,
transaction: context.transaction
})
}
}

module.exports = SegmentSynthesizer
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@
"@grpc/proto-loader": "^0.7.5",
"@newrelic/security-agent": "^2.0.0",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/semantic-conventions": "^1.27.0",
"@tyriar/fibonacci-heap": "^2.0.7",
"concat-stream": "^2.0.0",
"https-proxy-agent": "^7.0.1",
Expand Down
102 changes: 102 additions & 0 deletions test/unit/lib/otel/segment-synthesizer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright 2024 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

'use strict'
const test = require('node:test')
const assert = require('node:assert')

const helper = require('../../../lib/agent_helper')
const { ROOT_CONTEXT, SpanKind, TraceFlags } = require('@opentelemetry/api')
const { BasicTracerProvider, Span } = require('@opentelemetry/sdk-trace-base')
const SegmentSynthesizer = require('../../../../lib/otel/segment-synthesis')
const {
SEMATTRS_DB_SYSTEM,
SEMATTRS_HTTP_HOST,
SEMATTRS_HTTP_METHOD
} = require('@opentelemetry/semantic-conventions')
const createMockLogger = require('../../mocks/logger')

test.beforeEach((ctx) => {
const loggerMock = createMockLogger()
const agent = helper.loadMockedAgent()
const synthesizer = new SegmentSynthesizer(agent, { logger: loggerMock })
const tracer = new BasicTracerProvider().getTracer('default')
const parentId = '5c1c63257de34c67'
ctx.nr = {
agent,
loggerMock,
parentId,
synthesizer,
tracer
}
})

test.afterEach((ctx) => {
helper.unloadAgent(ctx.nr.agent)
})

test('should create http external segment from otel http client span', (t, end) => {
const { agent, synthesizer, parentId, tracer } = t.nr
helper.runInTransaction(agent, (tx) => {
const spanContext = {
traceId: tx.trace.id,
spanId: tx.trace.root.id,
traceFlags: TraceFlags.SAMPLED
}
const span = new Span(tracer, ROOT_CONTEXT, 'test-span', spanContext, SpanKind.CLIENT, parentId)
span.setAttribute(SEMATTRS_HTTP_METHOD, 'GET')
span.setAttribute(SEMATTRS_HTTP_HOST, 'newrelic.com')
const segment = synthesizer.synthesize(span)
assert.equal(segment.name, 'External/newrelic.com')
assert.equal(segment.parentId, tx.trace.root.id)
tx.end()
end()
})
})

test('should log warning if a rule does have a synthesis for the given type', (t, end) => {
const { agent, synthesizer, loggerMock, parentId, tracer } = t.nr

helper.runInTransaction(agent, (tx) => {
const spanContext = {
traceId: tx.trace.id,
spanId: tx.trace.root.id,
traceFlags: TraceFlags.SAMPLED
}
const span = new Span(tracer, ROOT_CONTEXT, 'test-span', spanContext, SpanKind.CLIENT, parentId)
span.setAttribute(SEMATTRS_DB_SYSTEM, 'postgres')
const segment = synthesizer.synthesize(span)
assert.ok(!segment)
assert.deepEqual(loggerMock.debug.args[0], [
'Found type: %s, no synthesize rule currently built',
'db'
])
tx.end()
end()
})
})

test('should log warning span does not match a rule', (t, end) => {
const { agent, synthesizer, loggerMock, parentId, tracer } = t.nr

helper.runInTransaction(agent, (tx) => {
const spanContext = {
traceId: tx.trace.id,
spanId: tx.trace.root.id,
traceFlags: TraceFlags.SAMPLED
}

const span = new Span(tracer, ROOT_CONTEXT, 'test-span', spanContext, 'bogus', parentId)
const segment = synthesizer.synthesize(span)
assert.ok(!segment)
assert.deepEqual(loggerMock.debug.args[0], [
'Cannot match a rule to span name: %s, kind %s',
'test-span',
'bogus'
])
tx.end()
end()
})
})
Loading

0 comments on commit e81adc9

Please sign in to comment.