Skip to content

Commit

Permalink
feat: support W3C TraceContext traceparent header (#1587)
Browse files Browse the repository at this point in the history
* feat: support W3C TraceContext traceparent header

* test: add tests for useElasticTraceparentHeader config
  • Loading branch information
jahtalab committed Jan 14, 2020
1 parent e7d7c8d commit cde2dde
Show file tree
Hide file tree
Showing 8 changed files with 81 additions and 22 deletions.
2 changes: 2 additions & 0 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ var DEFAULTS = {
stackTraceLimit: 50,
transactionMaxSpans: 500,
transactionSampleRate: 1.0,
useElasticTraceparentHeader: true,
usePathAsTransactionName: false,
verifyServerCert: true
}
Expand Down Expand Up @@ -125,6 +126,7 @@ var ENV_TABLE = {
stackTraceLimit: 'ELASTIC_APM_STACK_TRACE_LIMIT',
transactionMaxSpans: 'ELASTIC_APM_TRANSACTION_MAX_SPANS',
transactionSampleRate: 'ELASTIC_APM_TRANSACTION_SAMPLE_RATE',
useElasticTraceparentHeader: 'ELASTIC_APM_USE_ELASTIC_TRACEPARENT_HEADER',
usePathAsTransactionName: 'ELASTIC_APM_USE_PATH_AS_TRANSACTION_NAME',
verifyServerCert: 'ELASTIC_APM_VERIFY_SERVER_CERT'
}
Expand Down
8 changes: 6 additions & 2 deletions lib/instrumentation/http-shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ exports.instrumentRequest = function (agent, moduleName) {
// don't leak previous transaction
agent._instrumentation.currentTransaction = null
} else {
var traceparent = req.headers['elastic-apm-traceparent']
var traceparent = req.headers['elastic-apm-traceparent'] || req.headers.traceparent
var trans = agent.startTransaction(null, null, {
childOf: traceparent
})
Expand Down Expand Up @@ -150,7 +150,11 @@ exports.traceOutgoingRequest = function (agent, moduleName, method) {
// Use the transaction context as the parent, in this case.
var parent = span || agent.currentTransaction
if (parent && parent._context && shouldPropagateTraceContext(options)) {
options.headers['elastic-apm-traceparent'] = parent._context.toString()
const headerValue = parent._context.toString()
options.headers.traceparent = headerValue
if (agent._conf.useElasticTraceparentHeader) {
options.headers['elastic-apm-traceparent'] = headerValue
}
}

var req = orig.apply(this, newArgs)
Expand Down
9 changes: 8 additions & 1 deletion lib/lambda.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,16 @@ module.exports = function elasticApmAwsLambda (agent) {
let parentId
if (payload.headers !== undefined) {
for (const [key, value] of Object.entries(payload.headers)) {
if (key.toLowerCase() === 'elastic-apm-traceparent') {
const lowerCaseKey = key.toLowerCase()
if (lowerCaseKey === 'elastic-apm-traceparent') {
parentId = value
break
} else if (lowerCaseKey === 'traceparent') {
/**
* We do not break here because we want to make sure to use
* elastic-apm-traceparent if available.
*/
parentId = value
}
}
}
Expand Down
17 changes: 11 additions & 6 deletions test/instrumentation/_agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@ var sharedInstrumentation

module.exports = function mockAgent (expected, cb) {
var agent = {
_conf: config({
abortedErrorThreshold: '250ms',
centralConfig: false,
errorOnAbortedRequests: false,
metricsInterval: 0
}),
_config: function (opts) {
this._conf = config(
Object.assign({
abortedErrorThreshold: '250ms',
centralConfig: false,
errorOnAbortedRequests: false,
metricsInterval: 0
}, opts)
)
},
_errorFilters: new Filters(),
_transactionFilters: new Filters(),
_spanFilters: new Filters(),
Expand All @@ -28,6 +32,7 @@ module.exports = function mockAgent (expected, cb) {
}),
setFramework: function () {}
}
agent._config()

agent._metrics = new Metrics(agent)
agent._metrics.start()
Expand Down
2 changes: 2 additions & 0 deletions test/instrumentation/modules/http/aws.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ AWS.config.update({
test('non aws-sdk request', function (t) {
const server = http.createServer(function (req, res) {
t.equal(req.headers.authorization, undefined, 'no authorization header')
t.ok(req.headers.traceparent.length > 0, 'traceparent header')
t.ok(req.headers['elastic-apm-traceparent'].length > 0, 'elastic-apm-traceparent header')
res.end()
server.close()
Expand All @@ -40,6 +41,7 @@ test('non aws-sdk request', function (t) {
test('aws-sdk request', function (t) {
const server = http.createServer(function (req, res) {
t.equal(req.headers.authorization.substr(0, 5), 'AWS4-', 'AWS authorization header')
t.equal(req.headers.traceparent, undefined, 'no traceparent header')
t.equal(req.headers['elastic-apm-traceparent'], undefined, 'no elastic-apm-traceparent header')
res.end()
server.close()
Expand Down
30 changes: 24 additions & 6 deletions test/instrumentation/modules/http/basic.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,23 +82,41 @@ test('new http.Server', function (t) {
server.on('request', onRequest(t))
sendRequest(server)
})

t.test('support elastic-apm-traceparent header', function (t) {
resetAgent(function (data) {
assert(t, data)
server.close()
t.end()
})

var server = new http.Server()
server.on('request', onRequest(t, true))
sendRequest(server, undefined, true)
})
})

function sendRequest (server, timeout) {
function sendRequest (server, timeout, useElasticHeader) {
server.listen(function () {
var port = server.address().port
var context = TraceParent.startOrResume(null, {
transactionSampleRate: 1.0
})

const headers = {}
const contextValue = context.toString()
if (useElasticHeader) {
headers['elastic-apm-traceparent'] = contextValue
} else {
headers.traceparent = contextValue
}

var req = http.request({
hostname: 'localhost',
port: port,
path: '/',
method: 'GET',
headers: {
'elastic-apm-traceparent': context.toString()
}
headers: headers
}, function (res) {
if (timeout) throw new Error('should not get to here')
res.resume()
Expand All @@ -113,9 +131,9 @@ function sendRequest (server, timeout) {
})
}

function onRequest (t) {
function onRequest (t, useElasticHeader) {
return function onRequestHandler (req, res) {
var traceparent = req.headers['elastic-apm-traceparent']
var traceparent = useElasticHeader ? req.headers['elastic-apm-traceparent'] : req.headers.traceparent
var parent = TraceParent.fromString(traceparent)
var context = agent.currentTransaction._context
t.equal(parent.traceId, context.traceId, 'context trace id matches parent trace id')
Expand Down
31 changes: 26 additions & 5 deletions test/instrumentation/modules/http/outgoing.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ test('http.request(options, callback)', echoTest('http', (port, cb) => {
return http.request(options, cb)
}))

test('http: consider useElasticTraceparentHeader config option', echoTest('http', { useElasticTraceparentHeader: false }, (port, cb) => {
var options = { port }
return http.request(options, cb)
}))

methods.forEach(function (name) {
test(`http.${name}(urlString)`, echoTest('http', (port, cb) => {
var urlString = `http://localhost:${port}`
Expand Down Expand Up @@ -75,6 +80,11 @@ test('https.request(options, callback)', echoTest('https', (port, cb) => {
return https.request(options, cb)
}))

test('https: consider useElasticTraceparentHeader config option', echoTest('https', { useElasticTraceparentHeader: false }, (port, cb) => {
var options = { port, rejectUnauthorized: false }
return https.request(options, cb)
}))

methods.forEach(function (name) {
test(`https.${name}(urlString, options)`, echoTest('https', (port, cb) => {
var urlString = `https://localhost:${port}`
Expand Down Expand Up @@ -111,10 +121,15 @@ methods.forEach(function (name) {
}
})

function echoTest (type, handler) {
function echoTest (type, opts, handler) {
if (arguments.length === 2) {
handler = opts
opts = undefined
}

return function (t) {
echoServer(type, (cp, port) => {
resetAgent(data => {
resetAgent(opts, data => {
t.equal(data.transactions.length, 1, 'has one transaction')
t.equal(data.spans.length, 1, 'has one span')
t.equal(data.spans[0].name, 'GET localhost:' + port + '/', 'has expected span name')
Expand All @@ -135,8 +150,13 @@ function echoTest (type, handler) {
res.resume()
})

var traceparent = req.getHeader('elastic-apm-traceparent')
t.ok(traceparent, 'should have elastic-apm-traceparent header')
var traceparent = req.getHeader('traceparent')
t.ok(traceparent, 'should have traceparent header')
if (opts && opts.useElasticTraceparentHeader === false) {
t.equal(req.getHeader('elastic-apm-traceparent'), undefined)
} else {
t.ok(req.getHeader('elastic-apm-traceparent'), 'should have elastic-apm-traceparent header')
}

var expected = TraceParent.fromString(trans._context.toString())
var received = TraceParent.fromString(traceparent)
Expand All @@ -153,7 +173,8 @@ function echoTest (type, handler) {
}
}

function resetAgent (cb) {
function resetAgent (opts, cb) {
agent._instrumentation.currentTransaction = null
agent._config(opts)
agent._transport = mockClient(2, cb)
}
4 changes: 2 additions & 2 deletions test/lambda/promises.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ test('resolve with parent id header present', function (t) {
const input = {
name: 'world',
headers: {
'Elastic-Apm-Traceparent': 'test'
traceparent: 'test'
}
}
const output = 'Hello, world!'
Expand Down Expand Up @@ -84,7 +84,7 @@ test('resolve with parent id header present', function (t) {
t.equal(agent.transactions.length, 1)
assertTransaction(t, agent.transactions[0], name, context, input, output)

t.equal(input.headers['Elastic-Apm-Traceparent'], agent.transactions[0].opts.childOf, 'context trace id matches parent trace id')
t.equal(input.headers.traceparent, agent.transactions[0].opts.childOf, 'context trace id matches parent trace id')

t.end()
}
Expand Down

0 comments on commit cde2dde

Please sign in to comment.