Skip to content

Commit ef697a5

Browse files
chore: Added http external span attributes (#2955)
1 parent 1b5ed2c commit ef697a5

File tree

6 files changed

+183
-13
lines changed

6 files changed

+183
-13
lines changed

lib/otel/constants.js

+7
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,13 @@ module.exports = {
259259
*/
260260
ATTR_URL_SCHEME: 'url.scheme',
261261

262+
/**
263+
* The URI query string.
264+
*
265+
* @example q=foo
266+
*/
267+
ATTR_URL_QUERY: 'url.query',
268+
262269
/* !!! Miscellaneous !!! */
263270
/**
264271
* Database system names.

lib/otel/segments/http-external.js

+32-3
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,50 @@
77

88
const NAMES = require('../../metrics/names')
99
const recordExternal = require('../../metrics/recorders/http_external')
10+
const urltils = require('../../util/urltils')
1011

1112
const {
12-
ATTR_HTTP_HOST
13+
ATTR_FULL_URL,
14+
ATTR_HTTP_URL,
15+
ATTR_HTTP_METHOD,
16+
ATTR_HTTP_REQUEST_METHOD,
17+
ATTR_NET_PEER_NAME,
18+
ATTR_SERVER_ADDRESS
1319
} = require('../constants')
1420

1521
module.exports = function createHttpExternalSegment(agent, otelSpan) {
1622
const context = agent.tracer.getContext()
17-
const host = otelSpan.attributes[ATTR_HTTP_HOST] || 'Unknown'
18-
const name = NAMES.EXTERNAL.PREFIX + host
23+
const method = otelSpan.attributes[ATTR_HTTP_REQUEST_METHOD] || otelSpan.attributes[ATTR_HTTP_METHOD]
24+
const host = otelSpan.attributes[ATTR_SERVER_ADDRESS] || otelSpan.attributes[ATTR_NET_PEER_NAME] || 'Unknown'
25+
26+
const url = otelSpan.attributes[ATTR_FULL_URL] || otelSpan.attributes[ATTR_HTTP_URL]
27+
let name = NAMES.EXTERNAL.PREFIX + host
28+
let parsedUrl
29+
let obfuscatedPath
30+
if (url) {
31+
parsedUrl = new URL(url)
32+
obfuscatedPath = urltils.obfuscatePath(agent.config, parsedUrl.pathname)
33+
name += obfuscatedPath
34+
}
35+
1936
const segment = agent.tracer.createSegment({
2037
id: otelSpan?.spanContext()?.spanId,
2138
name,
2239
recorder: recordExternal(host, 'http'),
2340
parent: context.segment,
2441
transaction: context.transaction
2542
})
43+
44+
if (parsedUrl && segment) {
45+
segment.captureExternalAttributes({
46+
protocol: parsedUrl.protocol,
47+
hostname: parsedUrl.hostname,
48+
host: parsedUrl.host,
49+
method,
50+
port: parsedUrl.port,
51+
path: obfuscatedPath,
52+
queryParams: Object.fromEntries(parsedUrl.searchParams.entries())
53+
})
54+
}
2655
return { segment, transaction: context.transaction }
2756
}

lib/otel/span-processor.js

+31-4
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,27 @@ const {
1616
ATTR_DB_NAME,
1717
ATTR_DB_STATEMENT,
1818
ATTR_DB_SYSTEM,
19+
ATTR_FULL_URL,
1920
ATTR_GRPC_STATUS_CODE,
21+
ATTR_HTTP_METHOD,
22+
ATTR_HTTP_REQUEST_METHOD,
2023
ATTR_HTTP_ROUTE,
2124
ATTR_HTTP_RES_STATUS_CODE,
2225
ATTR_HTTP_STATUS_CODE,
2326
ATTR_HTTP_STATUS_TEXT,
27+
ATTR_HTTP_URL,
2428
ATTR_MESSAGING_DESTINATION,
2529
ATTR_MESSAGING_DESTINATION_NAME,
2630
ATTR_MESSAGING_MESSAGE_CONVERSATION_ID,
2731
ATTR_MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY,
28-
ATTR_NET_PEER_NAME,
29-
ATTR_NET_PEER_PORT,
3032
ATTR_NET_HOST_NAME,
3133
ATTR_NET_HOST_PORT,
34+
ATTR_NET_PEER_NAME,
35+
ATTR_NET_PEER_PORT,
3236
ATTR_RPC_SYSTEM,
37+
ATTR_SERVER_ADDRESS,
3338
ATTR_SERVER_PORT,
34-
ATTR_SERVER_ADDRESS
39+
ATTR_URL_QUERY,
3540
} = require('./constants')
3641
const { DESTINATIONS } = require('../config/attribute-filter')
3742

@@ -85,8 +90,30 @@ module.exports = class NrSpanProcessor {
8590
this.reconcileConsumerAttributes({ segment, span, transaction })
8691
} else if (span.kind === SpanKind.PRODUCER) {
8792
this.reconcileProducerAttributes({ segment, span })
93+
} else if (span.kind === SpanKind.CLIENT && (span.attributes[ATTR_HTTP_METHOD] || span.attributes[ATTR_HTTP_REQUEST_METHOD])) {
94+
this.reconcileHttpExternalAttributes({ segment, span })
8895
}
89-
// TODO: add http external checks
96+
}
97+
98+
reconcileHttpExternalAttributes({ segment, span }) {
99+
const noOpMapper = () => {}
100+
const statusCode = (value) => segment.addSpanAttribute('http.statusCode', value)
101+
const statusText = (value) => segment.addSpanAttribute('http.statusText', value)
102+
const mapper = {
103+
[ATTR_HTTP_REQUEST_METHOD]: noOpMapper,
104+
[ATTR_HTTP_METHOD]: noOpMapper,
105+
[ATTR_SERVER_ADDRESS]: noOpMapper,
106+
[ATTR_NET_PEER_NAME]: noOpMapper,
107+
[ATTR_SERVER_PORT]: noOpMapper,
108+
[ATTR_NET_PEER_PORT]: noOpMapper,
109+
[ATTR_HTTP_RES_STATUS_CODE]: statusCode,
110+
[ATTR_HTTP_STATUS_CODE]: statusCode,
111+
[ATTR_HTTP_STATUS_TEXT]: statusText,
112+
[ATTR_FULL_URL]: noOpMapper,
113+
[ATTR_HTTP_URL]: noOpMapper,
114+
[ATTR_URL_QUERY]: noOpMapper
115+
}
116+
this.#reconciler.reconcile({ segment, otelSpan: span, mapper })
90117
}
91118

92119
/**

test/unit/lib/otel/fixtures/http-client.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ const { SpanKind } = require('@opentelemetry/api')
99
const createSpan = require('./span')
1010

1111
const {
12-
ATTR_HTTP_HOST,
12+
ATTR_SERVER_ADDRESS,
1313
ATTR_HTTP_METHOD
1414
} = require('#agentlib/otel/constants.js')
1515

1616
module.exports = function createHttpClientSpan({ parentId, tracer, tx }) {
1717
const span = createSpan({ name: 'test-span', kind: SpanKind.CLIENT, parentId, tracer, tx })
1818
span.setAttribute(ATTR_HTTP_METHOD, 'GET')
19-
span.setAttribute(ATTR_HTTP_HOST, 'newrelic.com')
19+
span.setAttribute(ATTR_SERVER_ADDRESS, 'newrelic.com')
2020
return span
2121
}

test/unit/lib/otel/segment-synthesizer.test.js

+25-2
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,14 @@ const {
2727
} = require('./fixtures')
2828
const {
2929
ATTR_DB_SYSTEM,
30+
ATTR_FULL_URL,
31+
ATTR_HTTP_REQUEST_METHOD,
3032
ATTR_MESSAGING_DESTINATION,
3133
ATTR_MESSAGING_DESTINATION_KIND,
32-
ATTR_MESSAGING_SYSTEM
34+
ATTR_MESSAGING_SYSTEM,
35+
ATTR_SERVER_ADDRESS,
36+
ATTR_SERVER_PORT,
37+
ATTR_URL_QUERY,
3338
} = require('#agentlib/otel/constants.js')
3439
const { SpanKind, TraceFlags } = require('@opentelemetry/api')
3540
const { DESTINATIONS } = require('#agentlib/config/attribute-filter.js')
@@ -56,13 +61,31 @@ test.afterEach((ctx) => {
5661

5762
test('should create http external segment from otel http client span', (t, end) => {
5863
const { agent, synthesizer, parentId, tracer } = t.nr
64+
65+
const attributes = {
66+
[ATTR_SERVER_ADDRESS]: 'www.newrelic.com',
67+
[ATTR_HTTP_REQUEST_METHOD]: 'GET',
68+
[ATTR_SERVER_PORT]: 8080,
69+
[ATTR_URL_QUERY]: 'q=test',
70+
[ATTR_FULL_URL]: 'https://www.newrelic.com:8080/search?q=test'
71+
}
72+
5973
helper.runInTransaction(agent, (tx) => {
6074
const span = createHttpClientSpan({ tx, parentId, tracer })
75+
span.setAttribute('http.url', attributes[ATTR_FULL_URL])
76+
span.setAttribute('url.query', attributes[ATTR_URL_QUERY])
6177
const { segment, transaction } = synthesizer.synthesize(span)
78+
const attrs = segment.getAttributes()
79+
const spanAttributes = segment.attributes.get(DESTINATIONS.SPAN_EVENT)
6280
assert.equal(tx.id, transaction.id)
6381
assert.equal(segment.id, span.spanContext().spanId)
64-
assert.equal(segment.name, 'External/newrelic.com')
82+
assert.equal(segment.name, 'External/newrelic.com/search')
6583
assert.equal(segment.parentId, tx.trace.root.id)
84+
assert.equal(attrs.procedure, attributes[ATTR_HTTP_REQUEST_METHOD])
85+
assert.equal(attrs.url, 'https://www.newrelic.com:8080/search')
86+
assert.equal(spanAttributes.hostname, attributes[ATTR_SERVER_ADDRESS])
87+
assert.equal(spanAttributes.port, attributes[ATTR_SERVER_PORT])
88+
assert.equal(spanAttributes['request.parameters.q'], 'test')
6689
tx.end()
6790
end()
6891
})

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

+86-2
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,19 @@ const { hrTimeToMilliseconds } = require('@opentelemetry/core')
1212

1313
const helper = require('../../lib/agent_helper')
1414
const { otelSynthesis } = require('../../../lib/symbols')
15+
const { DESTINATIONS: ATTR_DESTINATION } = require('../../../lib/config/attribute-filter')
1516

1617
const { DESTINATIONS } = require('../../../lib/transaction')
1718
const {
1819
ATTR_DB_NAME,
1920
ATTR_DB_STATEMENT,
2021
ATTR_DB_SYSTEM,
2122
ATTR_GRPC_STATUS_CODE,
23+
ATTR_FULL_URL,
2224
ATTR_HTTP_HOST,
2325
ATTR_HTTP_METHOD,
26+
ATTR_HTTP_REQUEST_METHOD,
27+
ATTR_HTTP_RES_STATUS_CODE,
2428
ATTR_HTTP_ROUTE,
2529
ATTR_HTTP_STATUS_CODE,
2630
ATTR_HTTP_STATUS_TEXT,
@@ -40,6 +44,7 @@ const {
4044
ATTR_SERVER_ADDRESS,
4145
ATTR_SERVER_PORT,
4246
ATTR_URL_PATH,
47+
ATTR_URL_QUERY,
4348
ATTR_URL_SCHEME,
4449
DB_SYSTEM_VALUES,
4550
MESSAGING_SYSTEM_KIND_VALUES
@@ -121,7 +126,7 @@ test('client span(http) is bridge accordingly', (t, end) => {
121126
const { agent, tracer } = t.nr
122127
helper.runInTransaction(agent, (tx) => {
123128
tx.name = 'http-external-test'
124-
tracer.startActiveSpan('http-outbound', { kind: otel.SpanKind.CLIENT, attributes: { [ATTR_HTTP_HOST]: 'newrelic.com', [ATTR_HTTP_METHOD]: 'GET' } }, (span) => {
129+
tracer.startActiveSpan('http-outbound', { kind: otel.SpanKind.CLIENT, attributes: { [ATTR_HTTP_HOST]: 'newrelic.com', [ATTR_NET_PEER_NAME]: 'newrelic.com', [ATTR_HTTP_METHOD]: 'GET' } }, (span) => {
125130
const segment = agent.tracer.getSegment()
126131
assert.equal(segment.name, 'External/newrelic.com')
127132
assert.equal(tx.traceId, span.spanContext().traceId)
@@ -141,6 +146,85 @@ test('client span(http) is bridge accordingly', (t, end) => {
141146
})
142147
})
143148

149+
test('Http external span is bridged accordingly', (t, end) => {
150+
const attributes = {
151+
[ATTR_SERVER_ADDRESS]: 'www.newrelic.com',
152+
[ATTR_HTTP_REQUEST_METHOD]: 'GET',
153+
[ATTR_SERVER_PORT]: 8080,
154+
[ATTR_URL_PATH]: '/search',
155+
[ATTR_URL_QUERY]: 'q=test',
156+
[ATTR_URL_SCHEME]: 'https',
157+
[ATTR_HTTP_HOST]: 'www.newrelic.com',
158+
[ATTR_FULL_URL]: 'https://www.newrelic.com:8080/search?q=test'
159+
}
160+
161+
const { agent, tracer } = t.nr
162+
helper.runInTransaction(agent, (tx) => {
163+
tx.name = 'undici-external-test'
164+
tracer.startActiveSpan('unidic-outbound', { kind: otel.SpanKind.CLIENT, attributes }, (span) => {
165+
span.setAttribute(ATTR_HTTP_RES_STATUS_CODE, 200)
166+
const segment = agent.tracer.getSegment()
167+
assert.equal(segment.name, 'External/www.newrelic.com/search')
168+
assert.equal(tx.traceId, span.spanContext().traceId)
169+
span.end()
170+
const duration = hrTimeToMilliseconds(span.duration)
171+
assert.equal(duration, segment.getDurationInMillis())
172+
tx.end()
173+
174+
const attrs = segment.getAttributes()
175+
const spanAttributes = segment.attributes.get(ATTR_DESTINATION.SPAN_EVENT)
176+
assert.equal(attrs.procedure, attributes[ATTR_HTTP_REQUEST_METHOD])
177+
assert.equal(attrs['url.scheme'], attrs[ATTR_URL_SCHEME])
178+
// attributes.url shouldn't include the query
179+
assert.equal(attrs.url, `https://${attributes[ATTR_SERVER_ADDRESS]}:8080/search`)
180+
assert.equal(spanAttributes['http.statusCode'], 200)
181+
assert.equal(spanAttributes.hostname, attributes[ATTR_SERVER_ADDRESS])
182+
assert.equal(spanAttributes.port, attributes[ATTR_SERVER_PORT])
183+
assert.equal(spanAttributes['request.parameters.q'], 'test')
184+
end()
185+
})
186+
})
187+
})
188+
189+
test('Http external span is bridged accordingly(legacy attributes test)', (t, end) => {
190+
const attributes = {
191+
[ATTR_NET_PEER_NAME]: 'www.newrelic.com',
192+
[ATTR_HTTP_METHOD]: 'GET',
193+
[ATTR_NET_PEER_PORT]: 8080,
194+
[ATTR_URL_QUERY]: 'q=test',
195+
[ATTR_HTTP_HOST]: 'www.newrelic.com',
196+
[ATTR_HTTP_URL]: 'https://www.newrelic.com:8080/search?q=test'
197+
}
198+
199+
const { agent, tracer } = t.nr
200+
helper.runInTransaction(agent, (tx) => {
201+
tx.name = 'http-external-test'
202+
tracer.startActiveSpan('http-outbound', { kind: otel.SpanKind.CLIENT, attributes }, (span) => {
203+
span.setAttribute(ATTR_HTTP_RES_STATUS_CODE, 200)
204+
span.setAttribute(ATTR_HTTP_STATUS_TEXT, 'OK')
205+
const segment = agent.tracer.getSegment()
206+
assert.equal(segment.name, 'External/www.newrelic.com/search')
207+
assert.equal(tx.traceId, span.spanContext().traceId)
208+
span.end()
209+
const duration = hrTimeToMilliseconds(span.duration)
210+
assert.equal(duration, segment.getDurationInMillis())
211+
tx.end()
212+
213+
const attrs = segment.getAttributes()
214+
const spanAttributes = segment.attributes.get(ATTR_DESTINATION.SPAN_EVENT)
215+
assert.equal(attrs.procedure, attributes[ATTR_HTTP_METHOD])
216+
// attributes.url shouldn't include the query
217+
assert.equal(attrs.url, `https://${attributes[ATTR_NET_PEER_NAME]}:8080/search`)
218+
assert.equal(spanAttributes['http.statusCode'], 200)
219+
assert.equal(spanAttributes['http.statusText'], 'OK')
220+
assert.equal(spanAttributes.hostname, attributes[ATTR_NET_PEER_NAME])
221+
assert.equal(spanAttributes.port, attributes[ATTR_NET_PEER_PORT])
222+
assert.equal(spanAttributes['request.parameters.q'], 'test')
223+
end()
224+
})
225+
})
226+
})
227+
144228
test('client span(db) is bridge accordingly(statement test)', (t, end) => {
145229
const { agent, tracer } = t.nr
146230
const attributes = {
@@ -247,7 +331,7 @@ test('server span is bridged accordingly', (t, end) => {
247331
tracer.startActiveSpan('http-test', { kind: otel.SpanKind.SERVER, attributes }, (span) => {
248332
const tx = agent.getTransaction()
249333
assert.equal(tx.traceId, span.spanContext().traceId)
250-
span.setAttribute(ATTR_HTTP_STATUS_CODE, 200)
334+
span.setAttribute(ATTR_HTTP_RES_STATUS_CODE, 200)
251335
span.setAttribute(ATTR_HTTP_STATUS_TEXT, 'OK')
252336
span.end()
253337
assert.ok(!tx.isDistributedTrace)

0 commit comments

Comments
 (0)