Skip to content

Commit b85111c

Browse files
authored
feat: Propagate agent root context when opentelemetry ROOT_CONTEXT is passed in to trace propagator. Added logic to handle properly naming and ending transactions for server spans. (#2940)
1 parent 6832637 commit b85111c

File tree

10 files changed

+355
-73
lines changed

10 files changed

+355
-73
lines changed

lib/otel/constants.js

+28
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ module.exports = {
6868
*/
6969
ATTR_FULL_URL: 'url.full',
7070

71+
/**
72+
* The [numeric status code](https://github.com/grpc/grpc/blob/v1.33.2/doc/statuscodes.md) of the gRPC request.
73+
*/
74+
ATTR_GRPC_STATUS_CODE: 'rpc.grpc.status_code',
75+
7176
/**
7277
* Value of the HTTP `host` header.
7378
*
@@ -101,6 +106,20 @@ module.exports = {
101106
*/
102107
ATTR_HTTP_URL: 'http.url',
103108

109+
/**
110+
* The http response status code
111+
*
112+
* @example 200
113+
*/
114+
ATTR_HTTP_STATUS_CODE: 'http.response.status_code',
115+
116+
/**
117+
* The http response status text
118+
*
119+
* @example OK
120+
*/
121+
ATTR_HTTP_STATUS_TEXT: 'http.status_text',
122+
104123
/**
105124
* The message destination name.
106125
*
@@ -174,6 +193,15 @@ module.exports = {
174193
* @example /tmp/my.sock
175194
*/
176195
ATTR_SERVER_ADDRESS: 'server.address',
196+
ATTR_NET_HOST_NAME: 'net.host.name',
197+
198+
/**
199+
* Poort of the local HTTP server that received the request.
200+
*
201+
* @example 80
202+
*/
203+
ATTR_SERVER_PORT: 'server.port',
204+
ATTR_NET_HOST_PORT: 'net.host.port',
177205

178206
/**
179207
* Logical name of the local service being instrumented.

lib/otel/segments/server.js

+18-29
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ const Transaction = require('../../transaction')
99
const httpRecorder = require('../../metrics/recorders/http')
1010
const urltils = require('../../util/urltils')
1111
const url = require('node:url')
12+
const { NODEJS, ACTION_DELIMITER } = require('../../metrics/names')
1213

1314
const DESTINATION = Transaction.DESTINATIONS.TRANS_COMMON
1415
const {
1516
ATTR_HTTP_METHOD,
1617
ATTR_HTTP_REQUEST_METHOD,
17-
ATTR_HTTP_ROUTE,
1818
ATTR_HTTP_URL,
1919
ATTR_RPC_METHOD,
2020
ATTR_RPC_SERVICE,
@@ -24,15 +24,15 @@ const {
2424
module.exports = function createServerSegment(agent, otelSpan) {
2525
const transaction = new Transaction(agent)
2626
transaction.type = 'web'
27+
transaction.nameState.setPrefix(NODEJS.PREFIX)
28+
transaction.nameState.setPrefix(ACTION_DELIMITER)
2729
const rpcSystem = otelSpan.attributes[ATTR_RPC_SYSTEM]
2830
const httpMethod = otelSpan.attributes[ATTR_HTTP_METHOD] ?? otelSpan.attributes[ATTR_HTTP_REQUEST_METHOD]
2931
let segment
3032
if (rpcSystem) {
3133
segment = rpcSegment({ agent, otelSpan, transaction, rpcSystem })
32-
} else if (httpMethod) {
33-
segment = httpSegment({ agent, otelSpan, transaction, httpMethod })
3434
} else {
35-
segment = genericHttpSegment({ agent, transaction })
35+
segment = httpSegment({ agent, otelSpan, transaction, httpMethod })
3636
}
3737
transaction.baseSegment = segment
3838
return { segment, transaction }
@@ -41,11 +41,12 @@ module.exports = function createServerSegment(agent, otelSpan) {
4141
function rpcSegment({ agent, otelSpan, transaction, rpcSystem }) {
4242
const rpcService = otelSpan.attributes[ATTR_RPC_SERVICE] || 'Unknown'
4343
const rpcMethod = otelSpan.attributes[ATTR_RPC_METHOD] || 'Unknown'
44-
const name = `WebTransaction/WebFrameworkUri/${rpcSystem}/${rpcService}.${rpcMethod}`
45-
transaction.name = name
46-
transaction.trace.attributes.addAttribute(DESTINATION, 'request.method', rpcMethod)
47-
transaction.trace.attributes.addAttribute(DESTINATION, 'request.uri', name)
44+
const name = `${rpcService}/${rpcMethod}`
4845
transaction.url = name
46+
transaction.trace.attributes.addAttribute(DESTINATION, 'request.method', rpcMethod)
47+
transaction.trace.attributes.addAttribute(DESTINATION, 'request.uri', transaction.url)
48+
transaction.nameState.setPrefix(rpcSystem)
49+
transaction.nameState.appendPath(transaction.url)
4950
const segment = agent.tracer.createSegment({
5051
name,
5152
recorder: httpRecorder,
@@ -56,34 +57,22 @@ function rpcSegment({ agent, otelSpan, transaction, rpcSystem }) {
5657
return segment
5758
}
5859

59-
// most instrumentation will hit this case
60-
// I find that if the request is in a web framework, the web framework instrumentation
61-
// sets `http.route` and when the span closes it pulls that attribute in
62-
// we'll most likely need to wire up some naming reconciliation
63-
// to handle this use case.
6460
function httpSegment({ agent, otelSpan, transaction, httpMethod }) {
65-
const httpRoute = otelSpan.attributes[ATTR_HTTP_ROUTE] || 'Unknown'
6661
const httpUrl = otelSpan.attributes[ATTR_HTTP_URL] || '/Unknown'
62+
transaction.nameState.setVerb(httpMethod)
6763
const requestUrl = url.parse(httpUrl, true)
68-
const name = `WebTransaction/Nodejs/${httpMethod}/${httpRoute}`
69-
transaction.name = name
64+
transaction.parsedUrl = requestUrl
7065
transaction.url = urltils.obfuscatePath(agent.config, requestUrl.pathname)
7166
transaction.trace.attributes.addAttribute(DESTINATION, 'request.uri', transaction.url)
72-
transaction.trace.attributes.addAttribute(DESTINATION, 'request.method', httpMethod)
73-
return agent.tracer.createSegment({
74-
name,
75-
recorder: httpRecorder,
76-
parent: transaction.trace.root,
77-
transaction
78-
})
79-
}
80-
81-
function genericHttpSegment({ agent, transaction }) {
82-
const name = 'WebTransaction/NormalizedUri/*'
83-
transaction.name = name
67+
if (httpMethod) {
68+
transaction.trace.attributes.addAttribute(DESTINATION, 'request.method', httpMethod)
69+
}
70+
transaction.applyUserNamingRules(requestUrl.pathname)
71+
// accept dt headers?
72+
// synthetics.assignHeadersToTransaction(agent.config, transaction, )
8473
return agent.tracer.createSegment({
85-
name,
8674
recorder: httpRecorder,
75+
name: requestUrl.pathname,
8776
parent: transaction.trace.root,
8877
transaction
8978
})

lib/otel/setup.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const NrSpanProcessor = require('./span-processor')
1010
const ContextManager = require('./context-manager')
1111
const defaultLogger = require('../logger').child({ component: 'opentelemetry-bridge' })
1212
const createOtelLogger = require('./logger')
13+
const TracePropagator = require('./trace-propagator')
1314

1415
const { ATTR_SERVICE_NAME } = require('./constants')
1516

@@ -34,8 +35,8 @@ module.exports = function setupOtel(agent, logger = defaultLogger) {
3435

3536
})
3637
provider.register({
37-
contextManager: new ContextManager(agent)
38-
// propagator: // todo: https://github.com/newrelic/node-newrelic/issues/2662
38+
contextManager: new ContextManager(agent),
39+
propagator: new TracePropagator(agent)
3940
})
4041

4142
agent.metrics

lib/otel/span-processor.js

+81-7
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,25 @@
77
const SegmentSynthesizer = require('./segment-synthesis')
88
const { otelSynthesis } = require('../symbols')
99
const { hrTimeToMilliseconds } = require('@opentelemetry/core')
10+
const { SpanKind } = require('@opentelemetry/api')
1011
const urltils = require('../util/urltils')
1112
const {
1213
ATTR_DB_NAME,
1314
ATTR_DB_STATEMENT,
1415
ATTR_DB_SYSTEM,
15-
ATTR_HTTP_HOST,
16+
ATTR_GRPC_STATUS_CODE,
17+
ATTR_HTTP_ROUTE,
18+
ATTR_HTTP_STATUS_CODE,
19+
ATTR_HTTP_STATUS_TEXT,
1620
ATTR_NET_PEER_NAME,
1721
ATTR_NET_PEER_PORT,
18-
ATTR_SERVER_ADDRESS,
22+
ATTR_NET_HOST_NAME,
23+
ATTR_NET_HOST_PORT,
24+
ATTR_RPC_SYSTEM,
25+
ATTR_SERVER_PORT,
26+
ATTR_SERVER_ADDRESS
1927
} = require('./constants')
28+
const { DESTINATIONS } = require('../config/attribute-filter')
2029

2130
module.exports = class NrSpanProcessor {
2231
constructor(agent) {
@@ -42,9 +51,9 @@ module.exports = class NrSpanProcessor {
4251
*/
4352
onEnd(span) {
4453
if (span[otelSynthesis] && span[otelSynthesis].segment) {
45-
const { segment } = span[otelSynthesis]
54+
const { segment, transaction } = span[otelSynthesis]
4655
this.updateDuration(segment, span)
47-
this.reconcileAttributes(segment, span)
56+
this.reconcileAttributes({ segment, span, transaction })
4857
delete span[otelSynthesis]
4958
}
5059
}
@@ -55,14 +64,79 @@ module.exports = class NrSpanProcessor {
5564
segment.overwriteDurationInMillis(duration)
5665
}
5766

58-
// TODO: clean this up and break out by span.kind
59-
reconcileAttributes(segment, span) {
67+
reconcileAttributes({ segment, span, transaction }) {
68+
if (span.kind === SpanKind.SERVER) {
69+
this.reconcileServerAttributes({ segment, span, transaction })
70+
} else if (span.kind === SpanKind.CLIENT && span.attributes[ATTR_DB_SYSTEM]) {
71+
this.reconcileDbAttributes({ segment, span })
72+
}
73+
// TODO: add http external checks
74+
}
75+
76+
reconcileServerAttributes({ segment, span, transaction }) {
77+
if (span.attributes[ATTR_RPC_SYSTEM]) {
78+
this.reconcileRpcAttributes({ segment, span, transaction })
79+
} else {
80+
this.reconcileHttpAttributes({ segment, span, transaction })
81+
}
82+
83+
// End the corresponding transaction for the entry point server span.
84+
// We do then when the span ends to ensure all data has been processed
85+
// for the correspondig server span.
86+
transaction.end()
87+
}
88+
89+
reconcileHttpAttributes({ segment, span, transaction }) {
90+
for (const [prop, value] of Object.entries(span.attributes)) {
91+
let key = prop
92+
let sanitized = value
93+
if (key === ATTR_HTTP_ROUTE) {
94+
// TODO: can we get the route params?
95+
transaction.nameState.appendPath(sanitized)
96+
} else if (key === ATTR_HTTP_STATUS_CODE) {
97+
transaction.finalizeNameFromUri(transaction.parsedUrl, sanitized)
98+
transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_COMMON, 'http.statusCode', sanitized)
99+
key = 'http.statusCode'
100+
// Not using const as it is not in semantic-conventions
101+
} else if (key === ATTR_HTTP_STATUS_TEXT) {
102+
transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_COMMON, 'http.statusText', sanitized)
103+
key = 'http.statusText'
104+
} else if (key === ATTR_SERVER_PORT || key === ATTR_NET_HOST_PORT) {
105+
key = 'port'
106+
} else if (key === ATTR_SERVER_ADDRESS || key === ATTR_NET_HOST_NAME) {
107+
key = 'host'
108+
if (urltils.isLocalhost(sanitized)) {
109+
sanitized = this.agent.config.getHostnameSafe(sanitized)
110+
}
111+
}
112+
113+
// TODO: otel instrumentation does not collect headers
114+
// a customer can specify which ones, we also specify this
115+
// so i think we'd have to cross reference our list
116+
// it also looks like we add all headers to the trace
117+
// this isn't doing that
118+
segment.addAttribute(key, sanitized)
119+
}
120+
}
121+
122+
// TODO: our grpc instrumentation handles errors when the status code is not 0
123+
// we should prob do this here too
124+
reconcileRpcAttributes({ segment, span, transaction }) {
125+
for (const [prop, value] of Object.entries(span.attributes)) {
126+
if (prop === ATTR_GRPC_STATUS_CODE) {
127+
transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_COMMON, 'response.status', value)
128+
}
129+
segment.addAttribute(prop, value)
130+
}
131+
}
132+
133+
reconcileDbAttributes({ segment, span }) {
60134
for (const [prop, value] of Object.entries(span.attributes)) {
61135
let key = prop
62136
let sanitized = value
63137
if (key === ATTR_NET_PEER_PORT) {
64138
key = 'port_path_or_id'
65-
} else if (prop === ATTR_NET_PEER_NAME || prop === ATTR_SERVER_ADDRESS || prop === ATTR_HTTP_HOST) {
139+
} else if (prop === ATTR_NET_PEER_NAME) {
66140
key = 'host'
67141
if (urltils.isLocalhost(sanitized)) {
68142
sanitized = this.agent.config.getHostnameSafe(sanitized)

0 commit comments

Comments
 (0)