Skip to content

Commit

Permalink
feat(config): add captureHeaders config
Browse files Browse the repository at this point in the history
  • Loading branch information
Stephen Belanger committed Jan 18, 2019
1 parent 19fa199 commit 7b6dae6
Show file tree
Hide file tree
Showing 7 changed files with 56 additions and 29 deletions.
10 changes: 10 additions & 0 deletions docs/configuration.asciidoc
Expand Up @@ -218,6 +218,16 @@ the body needs to be available as a property on the incoming HTTP https://nodejs
The agent will look for the body on the following properties:
`req.json || req.body || req.payload`

[[capture-headers]]
==== `captureHeaders`

* *Type:* Boolean
* *Default:* true
* *Env:* `ELASTIC_APM_CAPTURE_HEADERS`

The HTTP headers of incoming HTTP requests are recorded and sent to the APM Server by default.
This can be disabled by setting this option to `false`.

[[error-on-aborted-requests]]
==== `errorOnAbortedRequests`

Expand Down
6 changes: 2 additions & 4 deletions lib/agent.js
Expand Up @@ -273,13 +273,11 @@ Agent.prototype.captureError = function (err, opts, cb) {
}

if (req) {
var config = agent._conf.captureBody
var captureBody = config === 'errors' || config === 'all'
error.context.request = parsers.getContextFromRequest(req, captureBody)
error.context.request = parsers.getContextFromRequest(req, agent._conf, 'errors')
}

if (res) {
error.context.response = parsers.getContextFromResponse(res, true)
error.context.response = parsers.getContextFromResponse(res, agent._conf, true)
}

if (captureLocation) {
Expand Down
9 changes: 6 additions & 3 deletions lib/config.js
Expand Up @@ -57,7 +57,8 @@ var DEFAULTS = {
transactionMaxSpans: 500,
transactionSampleRate: 1.0,
serverTimeout: '30s',
disableInstrumentations: []
disableInstrumentations: [],
captureHeaders: true
}

var ENV_TABLE = {
Expand Down Expand Up @@ -92,7 +93,8 @@ var ENV_TABLE = {
transactionSampleRate: 'ELASTIC_APM_TRANSACTION_SAMPLE_RATE',
serverTimeout: 'ELASTIC_APM_SERVER_TIMEOUT',
disableInstrumentations: 'ELASTIC_APM_DISABLE_INSTRUMENTATIONS',
payloadLogFile: 'ELASTIC_APM_PAYLOAD_LOG_FILE'
payloadLogFile: 'ELASTIC_APM_PAYLOAD_LOG_FILE',
captureHeaders: 'ELASTIC_APM_CAPTURE_HEADERS'
}

var BOOL_OPTS = [
Expand All @@ -103,7 +105,8 @@ var BOOL_OPTS = [
'captureSpanStackTraces',
'errorOnAbortedRequests',
'instrument',
'asyncHooks'
'asyncHooks',
'captureHeaders'
]

var NUM_OPTS = [
Expand Down
7 changes: 3 additions & 4 deletions lib/instrumentation/transaction.js
Expand Up @@ -129,13 +129,12 @@ Transaction.prototype.toJSON = function () {
payload.span_count.dropped = this._droppedSpans
}

var conf = this._agent._conf
if (this.req) {
var config = this._agent._conf.captureBody
var captureBody = config === 'transactions' || config === 'all'
payload.context.request = parsers.getContextFromRequest(this.req, captureBody)
payload.context.request = parsers.getContextFromRequest(this.req, conf, 'transactions')
}
if (this.res) {
payload.context.response = parsers.getContextFromResponse(this.res)
payload.context.response = parsers.getContextFromResponse(this.res, conf)
}
}

Expand Down
18 changes: 14 additions & 4 deletions lib/parsers.js
Expand Up @@ -88,7 +88,9 @@ exports.parseError = function (err, agent, cb) {
})
}

exports.getContextFromRequest = function (req, captureBody) {
exports.getContextFromRequest = function (req, conf, type) {
var captureBody = conf.captureBody === type || conf.captureBody === 'all'

var context = {
http_version: req.httpVersion,
method: req.method,
Expand All @@ -97,7 +99,11 @@ exports.getContextFromRequest = function (req, captureBody) {
remote_address: req.socket.remoteAddress,
encrypted: !!req.socket.encrypted
},
headers: Object.assign({}, req.headers)
headers: undefined
}

if (conf.captureHeaders) {
context.headers = Object.assign({}, req.headers)
}

var contentLength = parseInt(req.headers['content-length'], 10)
Expand All @@ -123,10 +129,14 @@ exports.getContextFromRequest = function (req, captureBody) {
return context
}

exports.getContextFromResponse = function (res, isError) {
exports.getContextFromResponse = function (res, conf, isError) {
var context = {
status_code: res.statusCode,
headers: res.headers || httpHeaders(res, true)
headers: undefined
}

if (conf.captureHeaders) {
context.headers = res.headers || httpHeaders(res, true)
}

if (isError) {
Expand Down
3 changes: 2 additions & 1 deletion test/instrumentation/_agent.js
Expand Up @@ -26,7 +26,8 @@ module.exports = function mockAgent (expected, cb) {
ignoreUserAgentStr: [],
ignoreUserAgentRegExp: [],
transactionSampleRate: 1.0,
disableInstrumentations: []
disableInstrumentations: [],
captureHeaders: true
},
_errorFilters: new Filters(),
_transactionFilters: new Filters(),
Expand Down
32 changes: 19 additions & 13 deletions test/parsers.js
Expand Up @@ -51,7 +51,7 @@ test('#getContextFromResponse()', function (t) {

res.sendDate = false

var context = parsers.getContextFromResponse(res, true)
var context = parsers.getContextFromResponse(res, { captureHeaders: true }, true)
t.deepEqual(context, {
status_code: 200,
headers: {},
Expand All @@ -72,7 +72,7 @@ test('#getContextFromResponse()', function (t) {
res.sendDate = false
res.write('foo')

var context = parsers.getContextFromResponse(res, true)
var context = parsers.getContextFromResponse(res, { captureHeaders: true }, true)
t.deepEqual(context, {
status_code: 200,
headers: { connection: 'close', 'transfer-encoding': 'chunked' },
Expand All @@ -87,7 +87,7 @@ test('#getContextFromResponse()', function (t) {
t.test('for error (request finished)', function (t) {
onRequest(function (req, res) {
req.on('end', function () {
var context = parsers.getContextFromResponse(res, true)
var context = parsers.getContextFromResponse(res, { captureHeaders: true }, true)
t.deepEqual(context, {
status_code: 200,
headers: { connection: 'close', 'content-length': '0' },
Expand All @@ -106,7 +106,7 @@ test('#getContextFromResponse()', function (t) {
t.test('for transaction', function (t) {
onRequest(function (req, res) {
req.on('end', function () {
var context = parsers.getContextFromResponse(res, false)
var context = parsers.getContextFromResponse(res, { captureHeaders: true }, false)
t.deepEqual(context, {
status_code: 200,
headers: { connection: 'close', 'content-length': '0' }
Expand All @@ -121,7 +121,8 @@ test('#getContextFromResponse()', function (t) {

test('#getContextFromRequest()', function (t) {
t.test('should parse a request object', function (t) {
var parsed = parsers.getContextFromRequest(getMockReq())
var conf = { captureHeaders: true, captureBody: 'off' }
var parsed = parsers.getContextFromRequest(getMockReq(), conf)
t.deepEqual(parsed, {
http_version: '1.1',
method: 'GET',
Expand All @@ -148,7 +149,7 @@ test('#getContextFromRequest()', function (t) {
t.test('full URI', function (t) {
var req = getMockReq()
req.url = 'https://www.example.com:8080/some/path?key=value'
var parsed = parsers.getContextFromRequest(req)
var parsed = parsers.getContextFromRequest(req, {})
t.deepEqual(parsed.url, {
pathname: '/some/path',
search: '?key=value',
Expand All @@ -164,7 +165,7 @@ test('#getContextFromRequest()', function (t) {
t.test('port in host header', function (t) {
var req = getMockReq()
req.headers.host = 'example.com:8080'
var parsed = parsers.getContextFromRequest(req)
var parsed = parsers.getContextFromRequest(req, {})
t.deepEqual(parsed.url, {
hostname: 'example.com',
port: 8080,
Expand All @@ -180,7 +181,7 @@ test('#getContextFromRequest()', function (t) {
t.test('empty query string', function (t) {
var req = getMockReq()
req.url = '/some/path?'
var parsed = parsers.getContextFromRequest(req)
var parsed = parsers.getContextFromRequest(req, {})
t.deepEqual(parsed.url, {
hostname: 'example.com',
pathname: '/some/path',
Expand All @@ -193,55 +194,60 @@ test('#getContextFromRequest()', function (t) {
})

t.test('should slice too large body\'s', function (t) {
var conf = { captureBody: 'all' }
var req = getMockReq()
req.body = ''
for (var n = 0; n < parsers._MAX_HTTP_BODY_CHARS + 10; n++) {
req.body += 'x'
}
req.headers['content-length'] = String(req.body.length)
var parsed = parsers.getContextFromRequest(req, true)
var parsed = parsers.getContextFromRequest(req, conf)
t.equal(parsed.body.length, parsers._MAX_HTTP_BODY_CHARS)
t.end()
})

t.test('should not log body if opts.body is false', function (t) {
var conf = { captureBody: 'off' }
var req = getMockReq()
req.body = 'secret stuff'
req.headers['content-length'] = String(req.body.length)
var parsed = parsers.getContextFromRequest(req, false)
var parsed = parsers.getContextFromRequest(req, conf)
t.equal(parsed.body, '[REDACTED]')
t.end()
})

t.test('body is object', function (t) {
var conf = { captureBody: 'all' }
var req = getMockReq()
req.body = { foo: 42 }
req.headers['content-length'] = JSON.stringify(req.body).length
var parsed = parsers.getContextFromRequest(req, true)
var parsed = parsers.getContextFromRequest(req, conf)
t.deepEqual(parsed.body, { foo: 42 })
t.end()
})

t.test('body is object, but too large', function (t) {
var conf = { captureBody: 'all' }
var req = getMockReq()
req.body = { foo: '' }
for (var n = 0; n < parsers._MAX_HTTP_BODY_CHARS + 10; n++) {
req.body.foo += 'x'
}
req.headers['content-length'] = JSON.stringify(req.body).length
var parsed = parsers.getContextFromRequest(req, true)
var parsed = parsers.getContextFromRequest(req, conf)
t.equal(typeof parsed.body, 'string')
t.equal(parsed.body.length, parsers._MAX_HTTP_BODY_CHARS)
t.equal(parsed.body.slice(0, 10), '{"foo":"xx')
t.end()
})

t.test('body is object, but not safe to stringify', function (t) {
var conf = { captureBody: 'all' }
var req = getMockReq()
req.body = { foo: 42 }
req.body.bar = req.body
req.headers['transfer-encoding'] = 'chunked'
var parsed = parsers.getContextFromRequest(req, true)
var parsed = parsers.getContextFromRequest(req, conf)
t.deepEqual(parsed.body, req.body)
t.end()
})
Expand Down

0 comments on commit 7b6dae6

Please sign in to comment.