Skip to content

Commit 1b5ed2c

Browse files
authored
chore: Refactored otel attribute reconciling (#2964)
1 parent dfeec5a commit 1b5ed2c

File tree

2 files changed

+139
-117
lines changed

2 files changed

+139
-117
lines changed

lib/otel/attr-reconciler.js

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2025 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
8+
const urltils = require('../util/urltils')
9+
const constants = require('./constants')
10+
11+
const hostKeys = [
12+
constants.ATTR_NET_HOST_NAME,
13+
constants.ATTR_NET_PEER_NAME,
14+
constants.ATTR_SERVER_ADDRESS
15+
]
16+
17+
class AttributeReconciler {
18+
#agent
19+
20+
constructor({ agent }) {
21+
this.#agent = agent
22+
}
23+
24+
#resolveHost(hostname) {
25+
if (urltils.isLocalhost(hostname)) {
26+
return this.#agent.config.getHostnameSafe(hostname)
27+
}
28+
return hostname
29+
}
30+
31+
#isHostnameKey(key) {
32+
return hostKeys.includes(key)
33+
}
34+
35+
reconcile({ segment, otelSpan, mapper = {} }) {
36+
for (const [key, srcValue] of Object.entries(otelSpan.attributes)) {
37+
let value = srcValue
38+
39+
if (this.#isHostnameKey(key) === true) {
40+
value = this.#resolveHost(srcValue)
41+
}
42+
43+
if (Object.prototype.hasOwnProperty.call(mapper, key) === true) {
44+
mapper[key](value)
45+
} else {
46+
segment.addAttribute(key, value)
47+
}
48+
}
49+
}
50+
}
51+
52+
module.exports = AttributeReconciler

lib/otel/span-processor.js

+87-117
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
*/
55

66
'use strict'
7-
const SegmentSynthesizer = require('./segment-synthesis')
8-
const { otelSynthesis } = require('../symbols')
7+
98
const { hrTimeToMilliseconds } = require('@opentelemetry/core')
109
const { SpanKind } = require('@opentelemetry/api')
11-
const urltils = require('../util/urltils')
10+
11+
const AttributeReconciler = require('./attr-reconciler')
12+
const SegmentSynthesizer = require('./segment-synthesis')
13+
const { otelSynthesis } = require('../symbols')
14+
1215
const {
1316
ATTR_DB_NAME,
1417
ATTR_DB_STATEMENT,
@@ -33,10 +36,14 @@ const {
3336
const { DESTINATIONS } = require('../config/attribute-filter')
3437

3538
module.exports = class NrSpanProcessor {
39+
#reconciler
40+
3641
constructor(agent) {
3742
this.agent = agent
3843
this.synthesizer = new SegmentSynthesizer(agent)
3944
this.tracer = agent.tracer
45+
46+
this.#reconciler = new AttributeReconciler({ agent })
4047
}
4148

4249
/**
@@ -93,49 +100,27 @@ module.exports = class NrSpanProcessor {
93100
* @param {Transaction} params.transaction The NR transaction to attach
94101
* the found attributes to.
95102
*/
96-
reconcileConsumerAttributes({ span, transaction }) { // eslint-disable-line sonarjs/cognitive-complexity
103+
reconcileConsumerAttributes({ span, transaction }) {
97104
const baseSegment = transaction.baseSegment
98105
const trace = transaction.trace
99106
const isHighSecurity = this.agent.config.high_security ?? false
100107

101-
for (const [key, value] of Object.entries(span.attributes)) {
102-
switch (key) {
103-
case ATTR_SERVER_ADDRESS: {
104-
if (value) {
105-
let serverAddress = value
106-
if (urltils.isLocalhost(value)) {
107-
serverAddress = this.agent.config.getHostnameSafe(value)
108-
}
109-
baseSegment.addAttribute('host', serverAddress)
110-
}
111-
break
112-
}
113-
114-
case ATTR_SERVER_PORT: {
115-
baseSegment.addAttribute('port', value)
116-
break
117-
}
118-
119-
case ATTR_MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY: {
120-
if (isHighSecurity === true || !value) break
121-
trace.attributes.addAttribute(DESTINATIONS.TRANS_COMMON, 'message.routingKey', value)
122-
baseSegment.addAttribute('message.routingKey', value)
123-
break
124-
}
125-
126-
case ATTR_MESSAGING_DESTINATION_NAME:
127-
case ATTR_MESSAGING_DESTINATION: {
128-
if (isHighSecurity === true || !value) break
129-
trace.attributes.addAttribute(DESTINATIONS.TRANS_COMMON, 'message.queueName', value)
130-
baseSegment.addAttribute('message.queueName', value)
131-
break
132-
}
133-
134-
default: {
135-
baseSegment.addAttribute(key, value)
136-
}
137-
}
108+
const queueNameMapper = (value) => {
109+
if (isHighSecurity === true) return
110+
trace.attributes.addAttribute(DESTINATIONS.TRANS_COMMON, 'message.queueName', value)
111+
baseSegment.addAttribute('message.queueName', value)
112+
}
113+
const mapper = {
114+
[ATTR_SERVER_ADDRESS]: (value) => baseSegment.addAttribute('host', value),
115+
[ATTR_SERVER_PORT]: (value) => baseSegment.addAttribute('port', value),
116+
[ATTR_MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY]: (value) => {
117+
if (isHighSecurity === true) return
118+
trace.attributes.addAttribute(DESTINATIONS.TRANS_COMMON, 'message.routingKey', value)
119+
},
120+
[ATTR_MESSAGING_DESTINATION_NAME]: queueNameMapper,
121+
[ATTR_MESSAGING_DESTINATION]: queueNameMapper
138122
}
123+
this.#reconciler.reconcile({ segment: baseSegment, otelSpan: span, mapper })
139124

140125
transaction.end()
141126
}
@@ -157,98 +142,83 @@ module.exports = class NrSpanProcessor {
157142
}
158143

159144
reconcileHttpAttributes({ segment, span, transaction }) {
160-
for (const [prop, value] of Object.entries(span.attributes)) {
161-
let key = prop
162-
let sanitized = value
163-
if (key === ATTR_HTTP_ROUTE) {
164-
// TODO: can we get the route params?
165-
transaction.nameState.appendPath(sanitized)
166-
} else if (key === ATTR_HTTP_STATUS_CODE || key === ATTR_HTTP_RES_STATUS_CODE) {
167-
key = 'http.statusCode'
168-
transaction.statusCode = sanitized
169-
transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_COMMON, key, sanitized)
170-
// Not using const as it is not in semantic-conventions
171-
} else if (key === ATTR_HTTP_STATUS_TEXT) {
172-
key = 'http.statusText'
173-
transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_COMMON, key, sanitized)
174-
} else if (key === ATTR_SERVER_PORT || key === ATTR_NET_HOST_PORT) {
175-
key = 'port'
176-
} else if (key === ATTR_SERVER_ADDRESS || key === ATTR_NET_HOST_NAME) {
177-
key = 'host'
178-
if (urltils.isLocalhost(sanitized)) {
179-
sanitized = this.agent.config.getHostnameSafe(sanitized)
180-
}
181-
}
182-
183-
// TODO: otel instrumentation does not collect headers
184-
// a customer can specify which ones, we also specify this
185-
// so i think we'd have to cross reference our list
186-
// it also looks like we add all headers to the trace
187-
// this isn't doing that
188-
segment.addAttribute(key, sanitized)
145+
const status = (value) => {
146+
transaction.statusCode = value
147+
transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_COMMON, 'http.statusCode', value)
148+
}
149+
const port = (value) => segment.addAttribute('port', value)
150+
const host = (value) => segment.addAttribute('host', value)
151+
const mapper = {
152+
// TODO: if route params are available, assign them as well
153+
[ATTR_HTTP_ROUTE]: (value) => {
154+
transaction.nameState.appendPath(value)
155+
segment.addAttribute('http.route', value)
156+
},
157+
[ATTR_HTTP_STATUS_CODE]: status,
158+
[ATTR_HTTP_RES_STATUS_CODE]: status,
159+
[ATTR_HTTP_STATUS_TEXT]: (value) => {
160+
transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_COMMON, 'http.statusText', value)
161+
},
162+
[ATTR_SERVER_PORT]: port,
163+
[ATTR_NET_HOST_PORT]: port,
164+
[ATTR_SERVER_ADDRESS]: host,
165+
[ATTR_NET_HOST_NAME]: host
189166
}
167+
this.#reconciler.reconcile({ segment, otelSpan: span, mapper })
168+
169+
// TODO: otel instrumentation does not collect headers
170+
// a customer can specify which ones, we also specify this
171+
// so i think we'd have to cross reference our list
172+
// it also looks like we add all headers to the trace
173+
// this isn't doing that
190174
}
191175

192176
// TODO: our grpc instrumentation handles errors when the status code is not 0
193177
// we should prob do this here too
194178
reconcileRpcAttributes({ segment, span, transaction }) {
195-
for (const [prop, value] of Object.entries(span.attributes)) {
196-
if (prop === ATTR_GRPC_STATUS_CODE) {
179+
const mapper = {
180+
[ATTR_GRPC_STATUS_CODE]: (value) => {
197181
transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_COMMON, 'response.status', value)
182+
segment.addAttribute(ATTR_GRPC_STATUS_CODE, value)
198183
}
199-
segment.addAttribute(prop, value)
200184
}
185+
this.#reconciler.reconcile({ segment, otelSpan: span, mapper })
201186
}
202187

203188
reconcileDbAttributes({ segment, span }) {
204-
for (const [prop, value] of Object.entries(span.attributes)) {
205-
let key = prop
206-
let sanitized = value
207-
if (key === ATTR_NET_PEER_PORT) {
208-
key = 'port_path_or_id'
209-
} else if (prop === ATTR_NET_PEER_NAME) {
210-
key = 'host'
211-
if (urltils.isLocalhost(sanitized)) {
212-
sanitized = this.agent.config.getHostnameSafe(sanitized)
213-
}
214-
} else if (prop === ATTR_DB_NAME) {
215-
key = 'database_name'
216-
} else if (prop === ATTR_DB_SYSTEM) {
217-
key = 'product'
218-
/**
219-
* This attribute was collected in `onStart`
220-
* and was passed to `ParsedStatement`. It adds
221-
* this segment attribute as `sql` or `sql_obfuscated`
222-
* and then when the span is built from segment
223-
* re-assigns to `db.statement`. This needs
224-
* to be skipped because it will be the raw value.
225-
*/
226-
} else if (prop === ATTR_DB_STATEMENT) {
227-
continue
228-
}
229-
segment.addAttribute(key, sanitized)
189+
const mapper = {
190+
[ATTR_NET_PEER_PORT]: (value) => {
191+
segment.addAttribute('port_path_or_id', value)
192+
},
193+
[ATTR_NET_PEER_NAME]: (value) => {
194+
segment.addAttribute('host', value)
195+
},
196+
[ATTR_DB_NAME]: (value) => {
197+
segment.addAttribute('database_name', value)
198+
},
199+
[ATTR_DB_SYSTEM]: (value) => {
200+
segment.addAttribute('product', value)
201+
/*
202+
* This attribute was collected in `onStart`
203+
* and was passed to `ParsedStatement`. It adds
204+
* this segment attribute as `sql` or `sql_obfuscated`
205+
* and then when the span is built from segment
206+
* re-assigns to `db.statement`. This needs
207+
* to be skipped because it will be the raw value.
208+
*/
209+
},
210+
[ATTR_DB_STATEMENT]: () => {}
230211
}
212+
this.#reconciler.reconcile({ segment, otelSpan: span, mapper })
231213
}
232214

233215
reconcileProducerAttributes({ segment, span }) {
234-
for (const [prop, value] of Object.entries(span.attributes)) {
235-
let key = prop
236-
let sanitized = value
237-
238-
if (prop === ATTR_SERVER_ADDRESS) {
239-
key = 'host'
240-
if (urltils.isLocalhost(sanitized)) {
241-
sanitized = this.agent.config.getHostnameSafe(sanitized)
242-
}
243-
} else if (prop === ATTR_SERVER_PORT) {
244-
key = 'port'
245-
} else if (prop === ATTR_MESSAGING_MESSAGE_CONVERSATION_ID) {
246-
key = 'correlation_id'
247-
} else if (prop === ATTR_MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY) {
248-
key = 'routing_key'
249-
}
250-
251-
segment.addAttribute(key, sanitized)
216+
const mapper = {
217+
[ATTR_SERVER_ADDRESS]: (value) => segment.addAttribute('host', value),
218+
[ATTR_SERVER_PORT]: (value) => segment.addAttribute('port', value),
219+
[ATTR_MESSAGING_MESSAGE_CONVERSATION_ID]: (value) => segment.addAttribute('correlation_id', value),
220+
[ATTR_MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY]: (value) => segment.addAttribute('routing_key', value)
252221
}
222+
this.#reconciler.reconcile({ segment, otelSpan: span, mapper })
253223
}
254224
}

0 commit comments

Comments
 (0)