Skip to content

Commit f404585

Browse files
authored
feat: Added http timeslice metrics from otel (#2924)
1 parent 965c41b commit f404585

File tree

4 files changed

+202
-15
lines changed

4 files changed

+202
-15
lines changed

lib/otel/constants.js

+16
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,15 @@ module.exports = {
166166
*/
167167
ATTR_RPC_SYSTEM: 'rpc.system',
168168

169+
/**
170+
* Server domain name, IP address, or Unix domain socket.
171+
*
172+
* @example example.com
173+
* @example 10.1.2.80
174+
* @example /tmp/my.sock
175+
*/
176+
ATTR_SERVER_ADDRESS: 'server.address',
177+
169178
/**
170179
* Logical name of the local service being instrumented.
171180
*/
@@ -178,6 +187,13 @@ module.exports = {
178187
*/
179188
ATTR_URL_PATH: 'url.path',
180189

190+
/**
191+
* The scheme value for the URL.
192+
*
193+
* @example https
194+
*/
195+
ATTR_URL_SCHEME: 'url.scheme',
196+
181197
/* !!! Miscellaneous !!! */
182198
/**
183199
* Database system names.

lib/otel/segments/server.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
'use strict'
77

88
const Transaction = require('../../transaction')
9+
const httpRecorder = require('../../metrics/recorders/http')
910
const urltils = require('../../util/urltils')
10-
const url = require('url')
11+
const url = require('node:url')
1112

1213
const DESTINATION = Transaction.DESTINATIONS.TRANS_COMMON
1314
const {
1415
ATTR_HTTP_METHOD,
16+
ATTR_HTTP_REQUEST_METHOD,
1517
ATTR_HTTP_ROUTE,
1618
ATTR_HTTP_URL,
1719
ATTR_RPC_METHOD,
@@ -23,7 +25,7 @@ module.exports = function createServerSegment(agent, otelSpan) {
2325
const transaction = new Transaction(agent)
2426
transaction.type = 'web'
2527
const rpcSystem = otelSpan.attributes[ATTR_RPC_SYSTEM]
26-
const httpMethod = otelSpan.attributes[ATTR_HTTP_METHOD]
28+
const httpMethod = otelSpan.attributes[ATTR_HTTP_METHOD] ?? otelSpan.attributes[ATTR_HTTP_REQUEST_METHOD]
2729
let segment
2830
if (rpcSystem) {
2931
segment = rpcSegment({ agent, otelSpan, transaction, rpcSystem })
@@ -46,6 +48,7 @@ function rpcSegment({ agent, otelSpan, transaction, rpcSystem }) {
4648
transaction.url = name
4749
const segment = agent.tracer.createSegment({
4850
name,
51+
recorder: httpRecorder,
4952
parent: transaction.trace.root,
5053
transaction
5154
})
@@ -69,6 +72,7 @@ function httpSegment({ agent, otelSpan, transaction, httpMethod }) {
6972
transaction.trace.attributes.addAttribute(DESTINATION, 'request.method', httpMethod)
7073
return agent.tracer.createSegment({
7174
name,
75+
recorder: httpRecorder,
7276
parent: transaction.trace.root,
7377
transaction
7478
})
@@ -79,6 +83,7 @@ function genericHttpSegment({ agent, transaction }) {
7983
transaction.name = name
8084
return agent.tracer.createSegment({
8185
name,
86+
recorder: httpRecorder,
8287
parent: transaction.trace.root,
8388
transaction
8489
})

lib/otel/span-processor.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ const {
1212
ATTR_DB_NAME,
1313
ATTR_DB_STATEMENT,
1414
ATTR_DB_SYSTEM,
15+
ATTR_HTTP_HOST,
1516
ATTR_NET_PEER_NAME,
1617
ATTR_NET_PEER_PORT,
18+
ATTR_SERVER_ADDRESS,
1719
} = require('./constants')
1820

1921
module.exports = class NrSpanProcessor {
@@ -60,7 +62,7 @@ module.exports = class NrSpanProcessor {
6062
let sanitized = value
6163
if (key === ATTR_NET_PEER_PORT) {
6264
key = 'port_path_or_id'
63-
} else if (prop === ATTR_NET_PEER_NAME) {
65+
} else if (prop === ATTR_NET_PEER_NAME || prop === ATTR_SERVER_ADDRESS || prop === ATTR_HTTP_HOST) {
6466
key = 'host'
6567
if (urltils.isLocalhost(sanitized)) {
6668
sanitized = this.agent.config.getHostnameSafe(sanitized)

test/versioned/otel-bridge/span.test.js

+176-12
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,33 @@
44
*/
55

66
'use strict'
7+
78
const assert = require('node:assert')
89
const test = require('node:test')
9-
const helper = require('../../lib/agent_helper')
1010
const otel = require('@opentelemetry/api')
1111
const { hrTimeToMilliseconds } = require('@opentelemetry/core')
12+
13+
const helper = require('../../lib/agent_helper')
1214
const { otelSynthesis } = require('../../../lib/symbols')
13-
const { SEMATTRS_HTTP_HOST, SEMATTRS_HTTP_METHOD, SEMATTRS_DB_NAME, SEMATTRS_DB_STATEMENT, SEMATTRS_DB_SYSTEM, SEMATTRS_NET_PEER_PORT, SEMATTRS_NET_PEER_NAME, DbSystemValues } = require('@opentelemetry/semantic-conventions')
15+
16+
const {
17+
ATTR_DB_NAME,
18+
ATTR_DB_STATEMENT,
19+
ATTR_DB_SYSTEM,
20+
ATTR_HTTP_HOST,
21+
ATTR_HTTP_METHOD,
22+
ATTR_HTTP_REQUEST_METHOD,
23+
ATTR_HTTP_ROUTE,
24+
ATTR_NET_PEER_NAME,
25+
ATTR_NET_PEER_PORT,
26+
ATTR_RPC_METHOD,
27+
ATTR_RPC_SERVICE,
28+
ATTR_RPC_SYSTEM,
29+
ATTR_SERVER_ADDRESS,
30+
ATTR_URL_PATH,
31+
ATTR_URL_SCHEME,
32+
DB_SYSTEM_VALUES
33+
} = require('../../../lib/otel/constants.js')
1434

1535
test.beforeEach((ctx) => {
1636
const agent = helper.instrumentMockedAgent({
@@ -85,7 +105,7 @@ test('Otel http external span test', (t, end) => {
85105
const { agent, tracer } = t.nr
86106
helper.runInTransaction(agent, (tx) => {
87107
tx.name = 'http-external-test'
88-
tracer.startActiveSpan('http-outbound', { kind: otel.SpanKind.CLIENT, attributes: { [SEMATTRS_HTTP_HOST]: 'newrelic.com', [SEMATTRS_HTTP_METHOD]: 'GET' } }, (span) => {
108+
tracer.startActiveSpan('http-outbound', { kind: otel.SpanKind.CLIENT, attributes: { [ATTR_HTTP_HOST]: 'newrelic.com', [ATTR_HTTP_METHOD]: 'GET' } }, (span) => {
89109
const segment = agent.tracer.getSegment()
90110
assert.equal(segment.name, 'External/newrelic.com')
91111
span.end()
@@ -107,11 +127,11 @@ test('Otel http external span test', (t, end) => {
107127
test('Otel db client span statement test', (t, end) => {
108128
const { agent, tracer } = t.nr
109129
const attributes = {
110-
[SEMATTRS_DB_NAME]: 'test-db',
111-
[SEMATTRS_DB_SYSTEM]: 'postgresql',
112-
[SEMATTRS_DB_STATEMENT]: "select foo from test where foo = 'bar';",
113-
[SEMATTRS_NET_PEER_PORT]: 5436,
114-
[SEMATTRS_NET_PEER_NAME]: '127.0.0.1'
130+
[ATTR_DB_NAME]: 'test-db',
131+
[ATTR_DB_SYSTEM]: 'postgresql',
132+
[ATTR_DB_STATEMENT]: "select foo from test where foo = 'bar';",
133+
[ATTR_NET_PEER_PORT]: 5436,
134+
[ATTR_NET_PEER_NAME]: '127.0.0.1'
115135
}
116136
const expectedHost = agent.config.getHostnameSafe('127.0.0.1')
117137
helper.runInTransaction(agent, (tx) => {
@@ -152,10 +172,10 @@ test('Otel db client span statement test', (t, end) => {
152172
test('Otel db client span operation test', (t, end) => {
153173
const { agent, tracer } = t.nr
154174
const attributes = {
155-
[SEMATTRS_DB_SYSTEM]: DbSystemValues.REDIS,
156-
[SEMATTRS_DB_STATEMENT]: 'hset has random random',
157-
[SEMATTRS_NET_PEER_PORT]: 5436,
158-
[SEMATTRS_NET_PEER_NAME]: '127.0.0.1'
175+
[ATTR_DB_SYSTEM]: DB_SYSTEM_VALUES.REDIS,
176+
[ATTR_DB_STATEMENT]: 'hset has random random',
177+
[ATTR_NET_PEER_PORT]: 5436,
178+
[ATTR_NET_PEER_NAME]: '127.0.0.1'
159179
}
160180
const expectedHost = agent.config.getHostnameSafe('127.0.0.1')
161181
helper.runInTransaction(agent, (tx) => {
@@ -189,3 +209,147 @@ test('Otel db client span operation test', (t, end) => {
189209
})
190210
})
191211
})
212+
213+
test('http metrics are bridged correctly', (t, end) => {
214+
const { agent, tracer } = t.nr
215+
216+
// Required span attributes for incoming HTTP server spans as defined by:
217+
// https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-server-semantic-conventions
218+
const attributes = {
219+
[ATTR_URL_SCHEME]: 'http',
220+
[ATTR_SERVER_ADDRESS]: 'newrelic.com',
221+
[ATTR_HTTP_REQUEST_METHOD]: 'GET',
222+
[ATTR_URL_PATH]: '/foo/bar',
223+
[ATTR_HTTP_ROUTE]: '/foo/:param'
224+
}
225+
226+
tracer.startActiveSpan('http-test', { kind: otel.SpanKind.SERVER, attributes }, (span) => {
227+
const tx = agent.getTransaction()
228+
const segment = agent.tracer.getSegment()
229+
assert.equal(segment.name, 'WebTransaction/Nodejs/GET//foo/:param')
230+
span.end()
231+
232+
const duration = hrTimeToMilliseconds(span.duration)
233+
assert.equal(duration, segment.getDurationInMillis())
234+
tx.end()
235+
236+
const attrs = segment.getAttributes()
237+
assert.equal(attrs.host, 'newrelic.com')
238+
assert.equal(attrs['http.request.method'], 'GET')
239+
assert.equal(attrs['http.route'], '/foo/:param')
240+
assert.equal(attrs['url.path'], '/foo/bar')
241+
assert.equal(attrs['url.scheme'], 'http')
242+
assert.equal(attrs.nr_exclusive_duration_millis, duration)
243+
244+
const unscopedMetrics = tx.metrics.unscoped
245+
const expectedMetrics = [
246+
'HttpDispatcher',
247+
'WebTransaction',
248+
'WebTransaction/Nodejs/GET//foo/:param',
249+
'WebTransactionTotalTime',
250+
'WebTransactionTotalTime/null',
251+
segment.name
252+
]
253+
for (const expectedMetric of expectedMetrics) {
254+
assert.equal(unscopedMetrics[expectedMetric].callCount, 1, `${expectedMetric} has correct callCount`)
255+
}
256+
assert.equal(unscopedMetrics.Apdex.apdexT, 0.1)
257+
assert.equal(unscopedMetrics['Apdex/null'].apdexT, 0.1)
258+
259+
end()
260+
})
261+
})
262+
263+
test('rpc server metrics are bridged correctly', (t, end) => {
264+
const { agent, tracer } = t.nr
265+
266+
// Required span attributes for incoming HTTP server spans as defined by:
267+
// https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans/#client-attributes
268+
const attributes = {
269+
[ATTR_RPC_SYSTEM]: 'foo',
270+
[ATTR_RPC_METHOD]: 'getData',
271+
[ATTR_RPC_SERVICE]: 'test.service',
272+
[ATTR_SERVER_ADDRESS]: 'newrelic.com',
273+
[ATTR_URL_PATH]: '/foo/bar'
274+
}
275+
276+
tracer.startActiveSpan('http-test', { kind: otel.SpanKind.SERVER, attributes }, (span) => {
277+
const tx = agent.getTransaction()
278+
const segment = agent.tracer.getSegment()
279+
assert.equal(segment.name, 'WebTransaction/WebFrameworkUri/foo/test.service.getData')
280+
span.end()
281+
282+
const duration = hrTimeToMilliseconds(span.duration)
283+
assert.equal(duration, segment.getDurationInMillis())
284+
tx.end()
285+
286+
const attrs = segment.getAttributes()
287+
assert.equal(attrs.host, 'newrelic.com')
288+
assert.equal(attrs['rpc.system'], 'foo')
289+
assert.equal(attrs['rpc.method'], 'getData')
290+
assert.equal(attrs['rpc.service'], 'test.service')
291+
assert.equal(attrs['url.path'], '/foo/bar')
292+
assert.equal(attrs.nr_exclusive_duration_millis, duration)
293+
294+
const unscopedMetrics = tx.metrics.unscoped
295+
const expectedMetrics = [
296+
'HttpDispatcher',
297+
'WebTransaction',
298+
'WebTransaction/WebFrameworkUri/foo/test.service.getData',
299+
'WebTransactionTotalTime',
300+
'WebTransactionTotalTime/null',
301+
segment.name
302+
]
303+
for (const expectedMetric of expectedMetrics) {
304+
assert.equal(unscopedMetrics[expectedMetric].callCount, 1, `${expectedMetric} has correct callCount`)
305+
}
306+
assert.equal(unscopedMetrics.Apdex.apdexT, 0.1)
307+
assert.equal(unscopedMetrics['Apdex/null'].apdexT, 0.1)
308+
309+
end()
310+
})
311+
})
312+
313+
test('fallback metrics are bridged correctly', (t, end) => {
314+
const { agent, tracer } = t.nr
315+
316+
const attributes = {
317+
[ATTR_URL_SCHEME]: 'gopher',
318+
[ATTR_SERVER_ADDRESS]: 'newrelic.com',
319+
[ATTR_URL_PATH]: '/foo/bar',
320+
}
321+
322+
tracer.startActiveSpan('http-test', { kind: otel.SpanKind.SERVER, attributes }, (span) => {
323+
const tx = agent.getTransaction()
324+
const segment = agent.tracer.getSegment()
325+
assert.equal(segment.name, 'WebTransaction/NormalizedUri/*')
326+
span.end()
327+
328+
const duration = hrTimeToMilliseconds(span.duration)
329+
assert.equal(duration, segment.getDurationInMillis())
330+
tx.end()
331+
332+
const attrs = segment.getAttributes()
333+
assert.equal(attrs.host, 'newrelic.com')
334+
assert.equal(attrs['url.path'], '/foo/bar')
335+
assert.equal(attrs['url.scheme'], 'gopher')
336+
assert.equal(attrs.nr_exclusive_duration_millis, duration)
337+
338+
const unscopedMetrics = tx.metrics.unscoped
339+
const expectedMetrics = [
340+
'HttpDispatcher',
341+
'WebTransaction',
342+
'WebTransaction/NormalizedUri/*',
343+
'WebTransactionTotalTime',
344+
'WebTransactionTotalTime/null',
345+
segment.name
346+
]
347+
for (const expectedMetric of expectedMetrics) {
348+
assert.equal(unscopedMetrics[expectedMetric].callCount, 1, `${expectedMetric} has correct callCount`)
349+
}
350+
assert.equal(unscopedMetrics.Apdex.apdexT, 0.1)
351+
assert.equal(unscopedMetrics['Apdex/null'].apdexT, 0.1)
352+
353+
end()
354+
})
355+
})

0 commit comments

Comments
 (0)