Hi @pocketcolin,
It is not obvious at all, but sanitization/redaction of fields in a captured incoming HTTP request body will be done only for form data -- i.e. requests with Content-Type: application/x-www-form-urlencoded
. This is mentioned at Configuration options | APM Node.js Agent Reference [3.x] | Elastic. I'm not sure if it is discussed elsewhere in the docs. I've opened `captureBody` docs don't mention the conditions for sanitization/redaction · Issue #3426 · elastic/apm-agent-nodejs · GitHub to add mention of this in the captureBody
docs.
Take this example. (I'm using the apm.addTransactionFilter()
to conveniently dump the transaction data before it is sent on the the APM server. This is also foreshadowing.
// capturebody.example.js
const apm = require('elastic-apm-node').start({
// serverUrl: '...',
// secretToken: '...',
serviceName: 'capturebody-example',
apiRequestTime: '2s',
metricsInterval: '0s',
captureBody: 'all'
})
apm.addTransactionFilter(trans => {
console.log('about to send this transaction: ', trans)
return trans
})
const http = require('http')
const bodyParser = require('body-parser')
const express = require('express')
const app = express()
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({extended: false}))
app.post('/ping', function (req, reply) {
console.log(`received request: ${req.method} ${req.url}, content-type:${req.headers['content-type']}`)
reply.send({ping: 'pong'})
})
app.listen({ port: 3000 }, async () => {
console.log('listening at <http://127.0.0.1:3000/ping>')
})
If we run that and then POST to it with form data:
% curl -v http://127.0.0.1:3000/ping -X POST -d foo=bar -d passwd=secret
...
> POST /ping HTTP/1.1
> Content-Type: application/x-www-form-urlencoded
...
Then the transaction data shows that the "secret" field -- which matches one of the default patterns in Configuration options | APM Node.js Agent Reference [3.x] | Elastic -- is redacted:
% node capturebody.example.js
...
received request: POST /ping, content-type:application/x-www-form-urlencoded
about to send this transaction: {
id: 'a26c10c366d34a8e',
trace_id: '841e9449f6dd84158bd459baaeb6c3bf',
parent_id: undefined,
name: 'POST /ping',
type: 'request',
duration: 3.551,
timestamp: 1686610832961007,
result: 'HTTP 2xx',
sampled: true,
context: {
user: {},
tags: {},
custom: {},
service: {},
cloud: {},
message: {},
request: {
http_version: '1.1',
method: 'POST',
url: [Object],
headers: [Object],
socket: [Object],
body: '{"foo":"bar","passwd":"[REDACTED]"}'
},
response: { status_code: 200, headers: [Object] }
},
span_count: { started: 0 },
outcome: 'success',
faas: undefined,
sample_rate: 1
}
However, if we POST with a JSON content-type:
% curl http://127.0.0.1:3000/ping -X POST -H content-type:application/json -d '{"foo":"bar","passwd":"secret"}'
Then there is no redaction:
received request: POST /ping, content-type:application/json
about to send this transaction: {
id: 'dabeae3b13849dd7',
trace_id: '764730e22d8a60f4428aeba274681f59',
parent_id: undefined,
name: 'POST /ping',
type: 'request',
duration: 14.57,
timestamp: 1686610819372043,
result: 'HTTP 2xx',
sampled: true,
context: {
user: {},
tags: {},
custom: {},
service: {},
cloud: {},
message: {},
request: {
http_version: '1.1',
method: 'POST',
url: [Object],
headers: [Object],
socket: [Object],
body: '{"foo":"bar","passwd":"secret"}'
},
response: { status_code: 200, headers: [Object] }
},
span_count: { started: 0 },
outcome: 'success',
faas: undefined,
sample_rate: 1
}
As I hinted above, you could use apm.addTransactionFilter(fn)
to filter as you require for your application. Something like this:
apm.addTransactionFilter(trans => {
if (trans?.context?.request?.body) {
try {
const body = JSON.parse(trans.context.request.body)
if ('passwd' in body) {
body.passwd = '[REDACTED]'
}
trans.context.request.body = JSON.stringify(body)
} catch (_err) {
// pass
}
}
// console.log('about to send this transaction: ', trans)
return trans
})