Skip to content

Commit 320610e

Browse files
authored
feat: add 'apmIntegration: false' option to disable Elastic APM integration (#62)
Refs: #60
1 parent 839d657 commit 320610e

16 files changed

+604
-25
lines changed

docs/morgan.asciidoc

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,10 @@ const app = require('express')()
134134
const morgan = require('morgan')
135135
const ecsFormat = require('@elastic/ecs-morgan-format')
136136
137-
app.use(morgan(ecsFormat('tiny'))) <1>
137+
app.use(morgan(ecsFormat({ format: 'tiny' }))) <1>
138138
// ...
139139
----
140+
<1> If "format" is the only option you are using, you may pass it as `ecsFormat('tiny')`.
140141

141142
[float]
142143
[[morgan-log-level]]
@@ -194,3 +195,11 @@ For example, running https://github.com/elastic/ecs-logging-nodejs/blob/master/l
194195
----
195196

196197
These IDs match trace data reported by the APM agent.
198+
199+
Integration with Elastic APM can be explicitly disabled via the
200+
`apmIntegration: false` option, for example:
201+
202+
[source,js]
203+
----
204+
app.use(morgan(ecsFormat({ apmIntegration: false })))
205+
----

docs/pino.asciidoc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,14 @@ For example, running https://github.com/elastic/ecs-logging-nodejs/blob/master/l
240240

241241
These IDs match trace data reported by the APM agent.
242242

243+
Integration with Elastic APM can be explicitly disabled via the
244+
`apmIntegration: false` option, for example:
245+
246+
[source,js]
247+
----
248+
const log = pino(ecsFormat({ apmIntegration: false }))
249+
----
250+
243251

244252
[float]
245253
[[pino-considerations]]

docs/winston.asciidoc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,3 +254,14 @@ For example, running https://github.com/elastic/ecs-logging-nodejs/blob/master/l
254254
----
255255

256256
These IDs match trace data reported by the APM agent.
257+
258+
Integration with Elastic APM can be explicitly disabled via the
259+
`apmIntegration: false` option, for example:
260+
261+
[source,js]
262+
----
263+
const logger = winston.createLogger({
264+
format: ecsFormat({ apmIntegration: false }),
265+
// ...
266+
})
267+
----

loggers/morgan/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
- Add `apmIntegration: false` option to all ecs-logging formatters to
6+
enable explicitly disabling Elastic APM integration.
7+
([#62](https://github.com/elastic/ecs-logging-nodejs/pull/62))
8+
59
- Fix "elasticApm.isStarted is not a function" crash on startup.
610
([#60](https://github.com/elastic/ecs-logging-nodejs/issues/60))
711

loggers/morgan/index.js

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,55 @@ try {
3333
// Silently ignore.
3434
}
3535

36-
function ecsFormat (format = morgan.combined) {
37-
// `format` is a format *name* (e.g. 'combined'), format function (e.g.
38-
// `morgan.combined`), or a format string (e.g. ':method :url :status')
39-
// Resolve this to a format function a la morgan's own `getFormatFunction`.
36+
// Return a Morgan formatter function for ecs-logging output.
37+
//
38+
// @param {Object} opts - Optional.
39+
// - {String || Function} opts.format - A format *name* (e.g. 'combined'),
40+
// format function (e.g. `morgan.combined`), or a format string
41+
// (e.g. ':method :url :status'). This is used to format the "message"
42+
// field. Defaults to `morgan.combined`.
43+
// - {Boolean} opts.apmIntegration - Whether to automatically integrate with
44+
// Elastic APM (https://github.com/elastic/apm-agent-nodejs). If a started
45+
// APM agent is detected, then log records will include the following
46+
// fields:
47+
// - "service.name" - the configured serviceName in the agent
48+
// - "event.dataset" - set to "$serviceName.log" for correlation in Kibana
49+
// - "trace.id", "transaction.id", and "span.id" - if there is a current
50+
// active trace when the log call is made
51+
// Default true.
52+
//
53+
// For backwards compatibility, the first argument can be a String or Function
54+
// to specify `opts.format`. For example, the following are equivalent:
55+
// ecsFormat({format: 'combined'})
56+
// ecsFormat('combined')
57+
// The former allows specifying other options.
58+
function ecsFormat (opts) {
59+
let format = morgan.combined
60+
let apmIntegration = true
61+
if (opts && typeof opts === 'object') {
62+
// Usage: ecsFormat({ /* opts */ })
63+
if (hasOwnProperty.call(opts, 'format')) {
64+
format = opts.format
65+
}
66+
if (hasOwnProperty.call(opts, 'apmIntegration')) {
67+
apmIntegration = opts.apmIntegration
68+
}
69+
} else if (opts) {
70+
// Usage: ecsFormat(format)
71+
format = opts
72+
}
73+
74+
// Resolve to a format function a la morgan's own `getFormatFunction`.
4075
let fmt = morgan[format] || format
4176
if (typeof fmt !== 'function') {
4277
fmt = morgan.compile(fmt)
4378
}
4479

45-
// If there is a *started* APM agent, then use it.
46-
const apm = elasticApm && elasticApm.isStarted && elasticApm.isStarted() ? elasticApm : null
80+
let apm = null
81+
if (apmIntegration && elasticApm && elasticApm.isStarted && elasticApm.isStarted()) {
82+
apm = elasticApm
83+
}
84+
4785
let serviceField
4886
let eventField
4987
if (apm) {

loggers/morgan/test/apm.test.js

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,148 @@ test('tracing integration works', t => {
179179
})
180180
})
181181
})
182+
183+
// This is the same as the previous test, but sets `apmIntegration=false`
184+
// and asserts tracing fields are *not* added to log records.
185+
test('apmIntegration=false disables tracing integration', t => {
186+
let apmServer
187+
let app
188+
let appIsClosed = false
189+
const traceObjs = []
190+
const logObjs = []
191+
let stderr = ''
192+
193+
// 1. Setup a mock APM server to accept trace data. Callback when listening.
194+
// Pass intake data to `collectTracesLogsAndCheck()`.
195+
function step1StartMockApmServer (cb) {
196+
apmServer = http.createServer(function apmServerReq (req, res) {
197+
t.equal(req.method, 'POST')
198+
t.equal(req.url, '/intake/v2/events')
199+
let instream = req
200+
if (req.headers['content-encoding'] === 'gzip') {
201+
instream = req.pipe(zlib.createGunzip())
202+
} else {
203+
instream.setEncoding('utf8')
204+
}
205+
instream.pipe(split(JSON.parse)).on('data', function (traceObj) {
206+
collectTracesLogsAndCheck(traceObj, null)
207+
})
208+
req.on('end', function () {
209+
res.end('ok')
210+
})
211+
})
212+
apmServer.listen(0, function () {
213+
cb(null, 'http://localhost:' + apmServer.address().port)
214+
})
215+
}
216+
217+
// 2. Start a test app that uses APM and our mock APM Server.
218+
// Callback on first log line, which includes the app's HTTP address.
219+
// Pass parsed JSON log records to `collectTracesLogsAndCheck()`.
220+
function step2StartApp (apmServerUrl, cb) {
221+
app = spawn(
222+
process.execPath,
223+
[
224+
path.join(__dirname, 'serve-one-http-req-with-apm.js'),
225+
apmServerUrl,
226+
'true' // disableApmIntegration argument
227+
]
228+
)
229+
let handledFirstLogLine = false
230+
app.stdout.pipe(split(JSON.parse)).on('data', function (logObj) {
231+
if (!handledFirstLogLine) {
232+
handledFirstLogLine = true
233+
t.equal(logObj.message, 'listening')
234+
t.ok(logObj.address, 'first listening log line has "address"')
235+
cb(null, logObj.address)
236+
} else {
237+
collectTracesLogsAndCheck(null, logObj)
238+
}
239+
})
240+
app.stderr.on('data', function (chunk) {
241+
stderr += chunk
242+
})
243+
app.on('close', function (code) {
244+
t.equal(stderr, '', 'empty stderr from app')
245+
t.equal(code, 0, 'app exited 0')
246+
appIsClosed = true
247+
})
248+
}
249+
250+
// 3. Call the test app to generate a trace.
251+
function step3CallApp (appUrl, cb) {
252+
const req = http.request(appUrl + '/', function (res) {
253+
res.on('data', function () {})
254+
res.on('end', cb)
255+
})
256+
req.on('error', cb)
257+
req.end()
258+
}
259+
260+
// 4. Collect trace data from the APM Server, log data from the app, and when
261+
// all the expected data is collected, then test it: assert matching tracing
262+
// IDs.
263+
function collectTracesLogsAndCheck (traceObj, logObj) {
264+
if (traceObj) {
265+
traceObjs.push(traceObj)
266+
}
267+
if (logObj) {
268+
t.ok(validate(logObj), 'logObj is ECS valid')
269+
t.equal(ecsLoggingValidate(logObj), null)
270+
logObjs.push(logObj)
271+
}
272+
// Unlike the equivalent apm.test.js for other logging frameworks, we are
273+
// not testing for a custom span and `$logRecord.span.id` because the way
274+
// morgan logs (on the HTTP Response "finished" event), a custom span in
275+
// the request handler is no longer active.
276+
if (traceObjs.length >= 2 && logObjs.length >= 1) {
277+
t.ok(traceObjs[0].metadata, 'traceObjs[0] is metadata')
278+
t.ok(traceObjs[1].transaction, 'traceObjs[1] is transaction')
279+
t.notOk(logObjs[0].trace, 'log record does *not* have "trace" object')
280+
t.notOk(logObjs[0].transaction, 'log record does *not* have "transaction" object')
281+
t.notOk(logObjs[0].span, 'log record does *not* have "span" object')
282+
t.notOk(logObjs[0].service, 'log record does *not* have "service" object')
283+
t.notOk(logObjs[0].event, 'log record does *not* have "event" object')
284+
finish()
285+
}
286+
}
287+
288+
function finish () {
289+
if (appIsClosed) {
290+
apmServer.close(function () {
291+
t.end()
292+
})
293+
} else {
294+
app.on('close', function () {
295+
apmServer.close(function () {
296+
t.end()
297+
})
298+
})
299+
}
300+
}
301+
302+
step1StartMockApmServer(function onListening (apmServerErr, apmServerUrl) {
303+
t.ifErr(apmServerErr)
304+
if (apmServerErr) {
305+
finish()
306+
return
307+
}
308+
t.ok(apmServerUrl, 'apmServerUrl: ' + apmServerUrl)
309+
310+
step2StartApp(apmServerUrl, function onReady (appErr, appUrl) {
311+
t.ifErr(appErr)
312+
if (appErr) {
313+
finish()
314+
return
315+
}
316+
t.ok(appUrl, 'appUrl: ' + appUrl)
317+
318+
step3CallApp(appUrl, function (clientErr) {
319+
t.ifErr(clientErr)
320+
321+
// The thread of control now is expected to be in
322+
// `collectTracesLogsAndCheck()`.
323+
})
324+
})
325+
})
326+
})

loggers/morgan/test/basic.test.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,25 @@ test('"format" argument - format function', t => {
159159
})
160160
})
161161

162+
test('"opts.format" argument', t => {
163+
t.plan(2)
164+
165+
// Example:
166+
// POST /?foo=bar 200 - - 0.073 ms
167+
const format = 'tiny' // https://github.com/expressjs/morgan#tiny
168+
const msgRe = /^POST \/\?foo=bar 200 - - \d+\.\d+ ms$/
169+
const stream = split().on('data', line => {
170+
const rec = JSON.parse(line)
171+
t.match(rec.message, msgRe, 'rec.message')
172+
})
173+
const logger = morgan(ecsFormat({ format: format }), { stream })
174+
175+
makeExpressServerAndRequest(logger, '/?foo=bar', { method: 'POST' }, 'hi', function (err) {
176+
t.ifErr(err)
177+
t.end()
178+
})
179+
})
180+
162181
test('"log.level" for successful response is "info"', t => {
163182
t.plan(2)
164183

loggers/morgan/test/serve-one-http-req-with-apm.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
// - exit
3030

3131
const serverUrl = process.argv[2]
32+
const disableApmIntegration = process.argv[3] === 'true'
3233
/* eslint-disable-next-line no-unused-vars */
3334
const apm = require('elastic-apm-node').start({
3435
serverUrl,
@@ -43,7 +44,11 @@ const http = require('http')
4344
const morgan = require('morgan')
4445
const ecsFormat = require('../') // @elastic/ecs-morgan-format
4546

46-
app.use(morgan(ecsFormat()))
47+
const ecsOpts = {}
48+
if (disableApmIntegration) {
49+
ecsOpts.apmIntegration = false
50+
}
51+
app.use(morgan(ecsFormat(ecsOpts)))
4752

4853
app.get('/', function (req, res) {
4954
res.once('finish', function apmFlushAndExit () {

loggers/pino/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
- Add `apmIntegration: false` option to all ecs-logging formatters to
6+
enable explicitly disabling Elastic APM integration.
7+
([#62](https://github.com/elastic/ecs-logging-nodejs/pull/62))
8+
59
- Fix "elasticApm.isStarted is not a function" crash on startup.
610
([#60](https://github.com/elastic/ecs-logging-nodejs/issues/60))
711

loggers/pino/index.js

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,27 +32,44 @@ try {
3232
// Silently ignore.
3333
}
3434

35+
// Create options for `pino(...)` that configure it for ecs-logging output.
36+
//
37+
// @param {Object} opts - Optional.
38+
// - {Boolean} opts.convertErr - Whether to convert a logged `err` field
39+
// to ECS error fields. Default true, to match Pino's default of having
40+
// an `err` serializer.
41+
// - {Boolean} opts.convertReqRes - Whether to convert logged `req` and `res`
42+
// HTTP request and response fields to ECS HTTP, User agent, and URL
43+
// fields. Default false.
44+
// - {Boolean} opts.apmIntegration - Whether to automatically integrate with
45+
// Elastic APM (https://github.com/elastic/apm-agent-nodejs). If a started
46+
// APM agent is detected, then log records will include the following
47+
// fields:
48+
// - "service.name" - the configured serviceName in the agent
49+
// - "event.dataset" - set to "$serviceName.log" for correlation in Kibana
50+
// - "trace.id", "transaction.id", and "span.id" - if there is a current
51+
// active trace when the log call is made
52+
// Default true.
3553
function createEcsPinoOptions (opts) {
36-
// Boolean options for whether to specially handle some logged field names:
37-
// - `err` to ECS Error fields
38-
// - `req` and `res` to ECS HTTP, User agent, etc. fields
39-
// These intentionally match the common serializers
40-
// (https://getpino.io/#/docs/api?id=serializers-object). If enabled,
41-
// this ECS conversion will take precedence over a serializer for the same
42-
// field name.
4354
let convertErr = true
4455
let convertReqRes = false
56+
let apmIntegration = true
4557
if (opts) {
4658
if (hasOwnProperty.call(opts, 'convertErr')) {
4759
convertErr = opts.convertErr
4860
}
4961
if (hasOwnProperty.call(opts, 'convertReqRes')) {
5062
convertReqRes = opts.convertReqRes
5163
}
64+
if (hasOwnProperty.call(opts, 'apmIntegration')) {
65+
apmIntegration = opts.apmIntegration
66+
}
5267
}
5368

54-
// If there is a *started* APM agent, then use it.
55-
const apm = elasticApm && elasticApm.isStarted && elasticApm.isStarted() ? elasticApm : null
69+
let apm = null
70+
if (apmIntegration && elasticApm && elasticApm.isStarted && elasticApm.isStarted()) {
71+
apm = elasticApm
72+
}
5673

5774
const ecsPinoOptions = {
5875
formatters: {

0 commit comments

Comments
 (0)