Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Added otel compliant spans to external http spans #2169

Merged
merged 3 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 25 additions & 15 deletions lib/instrumentation/core/http-outbound.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const http = require('http')
const synthetics = require('../../synthetics')

const NAMES = require('../../metrics/names')
const { DESTINATIONS } = require('../../config/attribute-filter')

const DEFAULT_HOST = 'localhost'
const DEFAULT_HTTP_PORT = 80
Expand Down Expand Up @@ -88,14 +89,15 @@ function parseOpts(opts) {
module.exports = function instrumentOutbound(agent, opts, makeRequest) {
opts = parseOpts(opts)
const defaultPort = getDefaultPort(opts)
let hostname = getDefaultHostName(opts)
const host = getDefaultHostName(opts)
const port = getPort(opts, defaultPort)

if (!hostname || port < 1) {
logger.warn('Invalid host name (%s) or port (%s) for outbound request.', hostname, port)
if (!host || port < 1) {
logger.warn('Invalid host name (%s) or port (%s) for outbound request.', host, port)
return makeRequest(opts)
}

let hostname = host
if (port && port !== defaultPort) {
hostname += ':' + port
}
Expand All @@ -118,7 +120,7 @@ module.exports = function instrumentOutbound(agent, opts, makeRequest) {
recordExternal(hostname, 'http'),
parent,
false,
instrumentRequest.bind(null, agent, opts, makeRequest, hostname)
instrumentRequest.bind(null, { agent, opts, makeRequest, host, port, hostname })
)
}

Expand All @@ -127,14 +129,17 @@ module.exports = function instrumentOutbound(agent, opts, makeRequest) {
* Instruments the request.emit to properly handle the response of the
* outbound http request
*
* @param {Agent} agent New Relic agent
* @param {string|object} opts a url string or HTTP request options
* @param {Function} makeRequest function to make request
* @param {string} hostname host of outbound request
* @param {object} params to function
* @param {Agent} params.agent New Relic agent
* @param {string|object} params.opts a url string or HTTP request options
* @param {Function} params.makeRequest function to make request
* @param {string} params.hostname host + port of outbound request
* @param {string} params.host host of outbound request
* @param {string} params.port port of outbound request
* @param {TraceSegment} segment outbound http segment
* @returns {http.IncomingMessage} request actual http outbound request
*/
function instrumentRequest(agent, opts, makeRequest, hostname, segment) {
function instrumentRequest({ agent, opts, makeRequest, host, port, hostname }, segment) {
const transaction = segment.transaction
const outboundHeaders = Object.create(null)

Expand All @@ -144,7 +149,7 @@ function instrumentRequest(agent, opts, makeRequest, hostname, segment) {
maybeAddDtCatHeaders(agent, transaction, outboundHeaders, opts?.headers)
opts.headers = assignOutgoingHeaders(opts.headers, outboundHeaders)

const request = applySegment(opts, makeRequest, hostname, segment)
const request = applySegment({ opts, makeRequest, hostname, host, port, segment })

instrumentRequestEmit(agent, hostname, segment, request)

Expand Down Expand Up @@ -205,13 +210,16 @@ function assignOutgoingHeaders(currentHeaders, outboundHeaders) {
/**
* Starts the http outbound segment and attaches relevant attributes to the segment/span.
*
* @param {string|object} opts a url string or HTTP request options
* @param {Function} makeRequest function to make request
* @param {string} hostname host of outbound request
* @param {TraceSegment} segment outbound http segment
* @param {object} params to function
* @param {string|object} params.opts a url string or HTTP request options
* @param {Function} params.makeRequest function to make request
* @param {string} params.host host of outbound request
* @param {string} params.port port of outbound request
* @param {string} params.hostname host + port of outbound request
* @param {TraceSegment} params.segment outbound http segment
* @returns {http.IncomingMessage} request actual http outbound request
*/
function applySegment(opts, makeRequest, hostname, segment) {
function applySegment({ opts, makeRequest, host, port, hostname, segment }) {
segment.start()
const request = makeRequest(opts)
const parsed = urltils.scrubAndParseParameters(request.path)
Expand All @@ -227,6 +235,8 @@ function applySegment(opts, makeRequest, hostname, segment) {
segment.addSpanAttribute(`request.parameters.${key}`, parsed.parameters[key])
}
}
segment.attributes.addAttribute(DESTINATIONS.SPAN_EVENT, 'host', host)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding destination of span event as we don't want these attrs on the segment. the lib/spans/span-event and streaming-span-event will take those and make server.address and server.port. I plan on filling a follow up to encapsulate all the external segment/span attributes into a function on the segment so we can call captureExternalAttributes

segment.attributes.addAttribute(DESTINATIONS.SPAN_EVENT, 'port', port)
segment.addAttribute('url', `${proto}//${hostname}${parsed.path}`)
segment.addAttribute('procedure', opts.method || 'GET')
return request
Expand Down
3 changes: 3 additions & 0 deletions lib/instrumentation/undici.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const symbols = require('../symbols')
const { executionAsyncResource } = require('async_hooks')
const diagnosticsChannel = require('diagnostics_channel')
const synthetics = require('../synthetics')
const { DESTINATIONS } = require('../config/attribute-filter')

const channels = [
{ channel: diagnosticsChannel.channel('undici:request:create'), hook: requestCreateHook },
Expand Down Expand Up @@ -128,6 +129,8 @@ function createExternalSegment({ shim, request, parentSegment }) {
url.searchParams.forEach((value, key) => {
segment.addSpanAttribute(`request.parameters.${key}`, value)
})
segment.attributes.addAttribute(DESTINATIONS.SPAN_EVENT, 'host', url.hostname)
segment.attributes.addAttribute(DESTINATIONS.SPAN_EVENT, 'port', url.port)
segment.addAttribute('procedure', request.method || 'GET')
request[symbols.segment] = segment
}
Expand Down
13 changes: 12 additions & 1 deletion lib/spans/span-event.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ class SpanEvent {
* category of the segment.
*
* @param {TraceSegment} segment - The segment to turn into a span event.
* @param {?string} [parentId=null] - The ID of the segment's parent.
* @param {?string} [parentId] - The ID of the segment's parent.
* @param isRoot
* @returns {SpanEvent} The constructed event.
*/
Expand Down Expand Up @@ -194,8 +194,19 @@ class HttpSpanEvent extends SpanEvent {
attributes.url = null
}

if (attributes.host) {
this.addAttribute('server.address', attributes.host)
attributes.host = null
}

if (attributes.port) {
this.addAttribute('server.port', attributes.port, true)
attributes.port = null
}

if (attributes.procedure) {
this.addAttribute('http.method', attributes.procedure)
this.addAttribute('http.request.method', attributes.procedure)
attributes.procedure = null
}
}
Expand Down
15 changes: 13 additions & 2 deletions lib/spans/streaming-span-event.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class StreamingSpanEvent {
*
* @param {string} key Name of the attribute to be stored.
* @param {string|boolean|number} value Value of the attribute to be stored.
* @param {boolean} [truncateExempt=false] Set to true if attribute should not be truncated.
* @param {boolean} [truncateExempt] Set to true if attribute should not be truncated.
*/
addCustomAttribute(key, value, truncateExempt = false) {
const shouldKeep = this._checkFilter(key)
Expand All @@ -79,7 +79,7 @@ class StreamingSpanEvent {
*
* @param {string} key Name of the attribute to be stored.
* @param {string|boolean|number} value Value of the attribute to be stored.
* @param {boolean} [truncateExempt=false] Set to true if attribute should not be truncated.
* @param {boolean} [truncateExempt] Set to true if attribute should not be truncated.
*/
addAgentAttribute(key, value, truncateExempt = false) {
const shouldKeep = this._checkFilter(key)
Expand Down Expand Up @@ -197,8 +197,19 @@ class StreamingHttpSpanEvent extends StreamingSpanEvent {
agentAttributes.url = null
}

if (agentAttributes.host) {
this.addAgentAttribute('server.address', agentAttributes.host)
agentAttributes.host = null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed the nullifying of these properties isn't doing what we expect. I will file another ticket to deal with that

}

if (agentAttributes.port) {
this.addAgentAttribute('server.port', agentAttributes.port, true)
agentAttributes.port = null
}

if (agentAttributes.procedure) {
this.addAgentAttribute('http.method', agentAttributes.procedure)
this.addAgentAttribute('http.request.method', agentAttributes.procedure)
agentAttributes.procedure = null
}
}
Expand Down
7 changes: 5 additions & 2 deletions test/integration/instrumentation/http-outbound.tap.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,11 @@ tap.test('external requests', function (t) {
res.resume()
res.on('end', () => {
const segment = tx.trace.root.children[0]
t.equal(segment.getAttributes().url, 'http://example.com/')
t.equal(segment.getAttributes().procedure, 'GET')
const attrs = segment.getAttributes()
t.same(attrs, {
url: 'http://example.com/',
procedure: 'GET'
})
t.equal(reqSegment, segment, 'should expose external')
t.end()
})
Expand Down
2 changes: 2 additions & 0 deletions test/unit/instrumentation/http/outbound.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ tap.test('instrumentOutbound', (t) => {
t.same(
transaction.trace.root.children[0].attributes.get(DESTINATIONS.SPAN_EVENT),
{
'host': HOSTNAME,
'port': PORT,
'url': `http://${HOSTNAME}:${PORT}/asdf`,
'procedure': 'GET',
'request.parameters.a': 'b',
Expand Down
12 changes: 12 additions & 0 deletions test/unit/spans/span-event.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,20 @@ tap.test('fromSegment()', (t) => {

// Should have (most) http properties.
t.equal(attributes['http.url'], 'https://example.com/')
t.equal(attributes['server.address'], 'example.com')
t.equal(attributes['server.port'], 443)
t.ok(attributes['http.method'])
t.ok(attributes['http.request.method'])
t.equal(attributes['http.statusCode'], 200)
t.equal(attributes['http.statusText'], 'OK')

// should nullify mapped properties
t.notOk(attributes.library)
t.notOk(attributes.url)
t.notOk(attributes.host)
t.notOk(attributes.port)
t.notOk(attributes.procedure)

// Should have no datastore properties.
const hasOwnAttribute = Object.hasOwnProperty.bind(attributes)
t.notOk(hasOwnAttribute('db.statement'))
Expand Down Expand Up @@ -251,7 +261,9 @@ tap.test('fromSegment()', (t) => {
// Should have not http properties.
const hasOwnAttribute = Object.hasOwnProperty.bind(attributes)
t.notOk(hasOwnAttribute('http.url'))
t.notOk(hasOwnAttribute('server.address'))
t.notOk(hasOwnAttribute('http.method'))
t.notOk(hasOwnAttribute('http.request.method'))

// Should have (most) datastore properties.
t.ok(attributes['db.instance'])
Expand Down
5 changes: 5 additions & 0 deletions test/unit/spans/streaming-span-event.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,10 @@ tap.test('fromSegment()', (t) => {

// Should have (most) http properties.
t.same(agentAttributes['http.url'], { [STRING_TYPE]: 'https://example.com/' })
t.same(agentAttributes['server.address'], { [STRING_TYPE]: 'example.com' })
t.same(agentAttributes['server.port'], { [INT_TYPE]: 443 })
t.ok(agentAttributes['http.method'])
t.ok(agentAttributes['http.request.method'])
t.same(agentAttributes['http.statusCode'], { [INT_TYPE]: 200 })
t.same(agentAttributes['http.statusText'], { [STRING_TYPE]: 'OK' })

Expand Down Expand Up @@ -246,7 +249,9 @@ tap.test('fromSegment()', (t) => {
// Should have not http properties.
const hasOwnAttribute = Object.hasOwnProperty.bind(agentAttributes)
t.notOk(hasOwnAttribute('http.url'))
t.notOk(hasOwnAttribute('server.address'))
t.notOk(hasOwnAttribute('http.method'))
t.notOk(hasOwnAttribute('http.request.method'))

// Should have (most) datastore properties.
t.ok(agentAttributes['db.instance'])
Expand Down
5 changes: 4 additions & 1 deletion test/versioned/undici/requests.tap.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ tap.test('Undici request tests', (t) => {
let server
let REQUEST_URL
let HOST
let PORT

function createServer() {
server = http.createServer((req, res) => {
Expand All @@ -45,6 +46,7 @@ tap.test('Undici request tests', (t) => {

server.listen(0)
const { port } = server.address()
PORT = port
HOST = `localhost:${port}`
REQUEST_URL = `http://${HOST}`
return server
Expand Down Expand Up @@ -146,7 +148,8 @@ tap.test('Undici request tests', (t) => {
t.equal(spanAttrs['http.statusText'], 'OK')
t.equal(spanAttrs['request.parameters.a'], 'b')
t.equal(spanAttrs['request.parameters.c'], 'd')

t.equal(spanAttrs.host, 'localhost')
t.equal(spanAttrs.port, `${PORT}`)
tx.end()
t.end()
})
Expand Down
Loading