Skip to content

Commit bb811ed

Browse files
committed
feat: more correlation fields: service.version, service.environment, service.node.name
These are retrieved from an active APM agent, if apmIntegration is enabled. As well, config options for overriding these (and service.name) have been added. Closes: #121 Closes: #87 Refs: elastic/apm-agent-nodejs#3195
1 parent 0897078 commit bb811ed

File tree

8 files changed

+171
-57
lines changed

8 files changed

+171
-57
lines changed

docs/winston.asciidoc

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const winston = require('winston')
3030
const ecsFormat = require('@elastic/ecs-winston-format')
3131
3232
const logger = winston.createLogger({
33-
format: ecsFormat(), <1>
33+
format: ecsFormat(/* options */), <1>
3434
transports: [
3535
new winston.transports.Console()
3636
]
@@ -63,7 +63,7 @@ const ecsFormat = require('@elastic/ecs-winston-format')
6363
6464
const logger = winston.createLogger({
6565
level: 'info',
66-
format: ecsFormat(), <1>
66+
format: ecsFormat(/* options */), <1>
6767
transports: [
6868
new winston.transports.Console()
6969
]
@@ -72,6 +72,7 @@ const logger = winston.createLogger({
7272
logger.info('hi')
7373
logger.error('oops there is a problem', { foo: 'bar' })
7474
----
75+
<1> See available options <<winston-ref,below>>.
7576

7677
Running this script (available https://github.com/elastic/ecs-logging-nodejs/blob/main/loggers/winston/examples/basic.js[here]) will produce log output similar to the following:
7778

@@ -226,14 +227,21 @@ For https://github.com/elastic/ecs-logging-nodejs/blob/main/loggers/winston/exam
226227

227228
[float]
228229
[[winston-apm]]
229-
=== Integration with APM Tracing
230+
=== Log Correlation with APM
230231

231232
This ECS log formatter integrates with https://www.elastic.co/apm[Elastic APM].
232233
If your Node app is using the {apm-node-ref}/intro.html[Node.js Elastic APM Agent],
233-
then fields are added to log records that {ecs-ref}/ecs-tracing.html[identify an active trace] and the configured service name
234-
({ecs-ref}/ecs-service.html["service.name"] and {ecs-ref}/ecs-event.html["event.dataset"]).
235-
These fields allow cross linking between traces and logs in Kibana and support
236-
log anomaly detection.
234+
then a number of fields are added to log records to correlate between APM
235+
services or traces and logging data:
236+
237+
- Log statements (e.g. `logger.info(...)`) called when there is a current
238+
tracing span will include {ecs-ref}/ecs-tracing.html[tracing fields] --
239+
`trace.id`, `transaction.id`, `span.id`.
240+
- A number of service identifier fields determined by or configured on the APM
241+
agent allow cross-linking between services and logs in Kibana --
242+
`service.name`, `service.version`, `service.environment`, `service.node.name`.
243+
- `event.dataset` enables {observability-guide}/inspect-log-anomalies.html[log
244+
rate anomaly detection] in the Elastic Observability app.
237245

238246
For example, running https://github.com/elastic/ecs-logging-nodejs/blob/main/loggers/winston/examples/http-with-elastic-apm.js[examples/http-with-elastic-apm.js] and `curl -i localhost:3000/` results in a log record with the following:
239247

@@ -242,7 +250,9 @@ For example, running https://github.com/elastic/ecs-logging-nodejs/blob/main/log
242250
% node examples/http-with-elastic-apm.js | jq .
243251
...
244252
"service.name": "http-with-elastic-apm",
245-
"event.dataset": "http-with-elastic-apm",
253+
"service.version": "1.4.0",
254+
"service.environment": "development",
255+
"event.dataset": "http-with-elastic-apm"
246256
"trace.id": "7fd75f0f33ff49aba85d060b46dcad7e",
247257
"transaction.id": "6c97c7c1b468fa05"
248258
}
@@ -261,3 +271,22 @@ const logger = winston.createLogger({
261271
})
262272
----
263273

274+
[float]
275+
[[winston-ref]]
276+
=== Reference
277+
278+
[float]
279+
[[winston-ref-ecsFormat]]
280+
==== `ecsFormat([options])`
281+
282+
* `options` +{type-object}+ The following options are supported:
283+
** `convertErr` +{type-boolean}+ Whether to convert a logged `err` field to ECS error fields. *Default:* `true`.
284+
** `convertReqRes` +{type-boolean}+ Whether to logged `req` and `res` HTTP request and response fields to ECS HTTP, User agent, and URL fields. *Default:* `false`.
285+
** `apmIntegration` +{type-boolean}+ Whether to enable APM agent integration. *Default:* `true`.
286+
** `serviceName` +{type-string}+ A "service.name" value. If specified this overrides any value from an active APM agent.
287+
** `serviceVersion` +{type-string}+ A "service.version" value. If specified this overrides any value from an active APM agent.
288+
** `serviceEnvironment` +{type-string}+ A "service.environment" value. If specified this overrides any value from an active APM agent.
289+
** `serviceNodeName` +{type-string}+ A "service.node.name" value. If specified this overrides any value from an active APM agent.
290+
** `eventDataset` +{type-string}+ A "event.dataset" value. If specified this overrides the default of using `${serviceVersion}`.
291+
292+
Create a formatter for winston that emits in ECS Logging format.

loggers/winston/CHANGELOG.md

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

33
## Unreleased
44

5+
- Add `service.version`, `service.environment`, and `service.node.name` log
6+
correlation fields, automatically inferred from an active APM agent. As
7+
well, the following `ecsFormat` configuration options have been added for
8+
overriding these and existing correlation fields: `serviceName`,
9+
`serviceVersion`, `serviceEnvironment`, `serviceNodeName`.
10+
(https://github.com/elastic/apm-agent-nodejs/issues/3195,
11+
https://github.com/elastic/ecs-logging-nodejs/issues/121,
12+
https://github.com/elastic/ecs-logging-nodejs/issues/87)
13+
514
- Change to adding dotted field names (`"ecs.version": "1.6.0"`), rather than
615
namespaced fields (`"ecs": {"version": "1.6.0"}`) for most fields. This is
716
supported by the ecs-logging spec, and arguably preferred in the ECS logging
817
docs. It is also what the ecs-logging-java libraries do. The resulting output
918
is slightly shorter, and accidental collisions with user fields is less
1019
likely.
20+
1121
- Stop adding ".log" suffix to `event.dataset` field.
1222
([#95](https://github.com/elastic/ecs-logging-nodejs/issues/95))
1323

loggers/winston/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const ecsFormat = require('@elastic/ecs-winston-format')
2929

3030
const logger = winston.createLogger({
3131
level: 'info',
32-
format: ecsFormat(),
32+
format: ecsFormat(/* options */),
3333
transports: [
3434
new winston.transports.Console()
3535
]

loggers/winston/index.d.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,44 @@ interface Config {
66
* Default true.
77
*/
88
convertErr?: boolean;
9+
910
/**
1011
* Whether to convert logged `req` and `res` HTTP request and response fields
1112
* to ECS HTTP, User agent, and URL fields. Default false.
1213
*/
1314
convertReqRes?: boolean;
15+
1416
/**
1517
* Whether to automatically integrate with
1618
* Elastic APM (https://github.com/elastic/apm-agent-nodejs). If a started
1719
* APM agent is detected, then log records will include the following
1820
* fields:
1921
*
20-
* - "service.name" - the configured serviceName in the agent
21-
* - "event.dataset" - set to "$serviceName" for correlation in Kibana
2222
* - "trace.id", "transaction.id", and "span.id" - if there is a current
2323
* active trace when the log call is made
2424
*
25+
* and also the following fields, if not already specified in this config:
26+
*
27+
* - "service.name" - the configured `serviceName` in the agent
28+
* - "service.version" - the configured `serviceVersion` in the agent
29+
* - "service.environment" - the configured `environment` in the agent
30+
* - "service.node.name" - the configured `serviceNodeName` in the agent
31+
* - "event.dataset" - set to `${serviceName}` for correlation in Kibana
32+
*
2533
* Default true.
2634
*/
2735
apmIntegration?: boolean;
36+
37+
/** Specify "service.name" field. Defaults to a value from the APM agent, if available. */
38+
serviceName?: string;
39+
/** Specify "service.version" field. Defaults to a value from the APM agent, if available. */
40+
serviceVersion?: string;
41+
/** Specify "service.environment" field. Defaults to a value from the APM agent, if available. */
42+
serviceEnvironment?: string;
43+
/** Specify "service.node.name" field. Defaults to a value from the APM agent, if available. */
44+
serviceNodeName?: string;
45+
/** Specify "event.dataset" field. Defaults `${serviceName}`. */
46+
eventDataset?: string;
2847
}
2948

3049
declare function ecsFormat(opts?: Config): Logform.Format;

loggers/winston/index.js

Lines changed: 64 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -45,39 +45,18 @@ const reservedFields = {
4545
res: true
4646
}
4747

48-
// Create a Winston format for ecs-logging output.
49-
//
50-
// @param {Object} opts - Optional.
51-
// - {Boolean} opts.convertErr - Whether to convert a logged `err` field
52-
// to ECS error fields. Default true.
53-
// - {Boolean} opts.convertReqRes - Whether to convert logged `req` and `res`
54-
// HTTP request and response fields to ECS HTTP, User agent, and URL
55-
// fields. Default false.
56-
// - {Boolean} opts.apmIntegration - Whether to automatically integrate with
57-
// Elastic APM (https://github.com/elastic/apm-agent-nodejs). If a started
58-
// APM agent is detected, then log records will include the following
59-
// fields:
60-
// - "service.name" - the configured serviceName in the agent
61-
// - "event.dataset" - set to "$serviceName" for correlation in Kibana
62-
// - "trace.id", "transaction.id", and "span.id" - if there is a current
63-
// active trace when the log call is made
64-
// Default true.
48+
/**
49+
* Create a Winston format for ecs-logging output.
50+
*
51+
* @param {import('logform').TransformableInfo} info
52+
* @param {Config} opts - See index.d.ts.
53+
*/
6554
function ecsTransform (info, opts) {
66-
let convertErr = true
67-
let convertReqRes = false
68-
let apmIntegration = true
69-
// istanbul ignore else
70-
if (opts) {
71-
if (hasOwnProperty.call(opts, 'convertErr')) {
72-
convertErr = opts.convertErr
73-
}
74-
if (hasOwnProperty.call(opts, 'convertReqRes')) {
75-
convertReqRes = opts.convertReqRes
76-
}
77-
if (hasOwnProperty.call(opts, 'apmIntegration')) {
78-
apmIntegration = opts.apmIntegration
79-
}
80-
}
55+
// istanbul ignore next
56+
opts = opts || {}
57+
const convertErr = opts.convertErr != null ? opts.convertErr : true
58+
const convertReqRes = opts.convertReqRes != null ? opts.convertReqRes : false
59+
const apmIntegration = opts.apmIntegration != null ? opts.apmIntegration : true
8160

8261
const ecsFields = {
8362
'@timestamp': new Date().toISOString(),
@@ -100,19 +79,62 @@ function ecsTransform (info, opts) {
10079
apm = elasticApm
10180
}
10281

103-
// istanbul ignore else
104-
if (apm) {
105-
// Set "service.name" and "event.dataset" from APM conf.
82+
// Set a number of correlation fields from (a) the given options or (b) an
83+
// APM agent, if there is one running.
84+
let serviceName = opts.serviceName
85+
if (serviceName == null && apm) {
10686
// istanbul ignore next
107-
const serviceName = apm.getServiceName
87+
serviceName = (apm.getServiceName
10888
? apm.getServiceName() // added in [email protected]
109-
: apm._conf.serviceName // fallback to private `_conf`
110-
// A mis-configured APM Agent can be "started" but not have a "serviceName".
111-
if (serviceName) {
112-
ecsFields['service.name'] = serviceName
113-
ecsFields['event.dataset'] = serviceName
114-
}
89+
: apm._conf.serviceName) // fallback to private `_conf`
90+
}
91+
if (serviceName) {
92+
ecsFields['service.name'] = serviceName
93+
}
94+
95+
let serviceVersion = opts.serviceVersion
96+
if (serviceVersion == null && apm) {
97+
// istanbul ignore next
98+
serviceVersion = (apm.getServiceVersion
99+
? apm.getServiceVersion() // added in elastic-apm-node@...
100+
: apm._conf.serviceVersion) // fallback to private `_conf`
101+
}
102+
if (serviceVersion) {
103+
ecsFields['service.version'] = serviceVersion
104+
}
105+
106+
let serviceEnvironment = opts.serviceEnvironment
107+
if (serviceEnvironment == null && apm) {
108+
// istanbul ignore next
109+
serviceEnvironment = (apm.getServiceEnvironment
110+
? apm.getServiceEnvironment() // added in elastic-apm-node@...
111+
: apm._conf.environment) // fallback to private `_conf`
112+
}
113+
if (serviceEnvironment) {
114+
ecsFields['service.environment'] = serviceEnvironment
115+
}
116+
117+
let serviceNodeName = opts.serviceNodeName
118+
if (serviceNodeName == null && apm) {
119+
// istanbul ignore next
120+
serviceNodeName = (apm.getServiceNodeName
121+
? apm.getServiceNodeName() // added in elastic-apm-node@...
122+
: apm._conf.serviceNodeName) // fallback to private `_conf`
123+
}
124+
if (serviceNodeName) {
125+
ecsFields['service.node.name'] = serviceNodeName
126+
}
115127

128+
let eventDataset = opts.eventDataset
129+
if (eventDataset == null && serviceName) {
130+
eventDataset = serviceName
131+
}
132+
if (eventDataset) {
133+
ecsFields['event.dataset'] = eventDataset
134+
}
135+
136+
// istanbul ignore else
137+
if (apm) {
116138
// https://www.elastic.co/guide/en/ecs/current/ecs-tracing.html
117139
const tx = apm.currentTransaction
118140
if (tx) {

loggers/winston/test/apm.test.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,12 @@ test('tracing integration works', t => {
7979
[
8080
path.join(__dirname, 'serve-one-http-req-with-apm.js'),
8181
apmServerUrl
82-
]
82+
],
83+
{
84+
env: Object.assign({}, process.env, {
85+
ELASTIC_APM_SERVICE_NODE_NAME: 'serviceNodeNameFromEnv'
86+
})
87+
}
8388
)
8489
let handledFirstLogLine = false
8590
app.stdout.pipe(split(JSON.parse)).on('data', function (logObj) {
@@ -135,8 +140,11 @@ test('tracing integration works', t => {
135140
t.equal(logObjs[0]['trace.id'], span.trace_id, 'trace.id matches')
136141
t.equal(logObjs[0]['transaction.id'], span.transaction_id, 'transaction.id matches')
137142
t.equal(logObjs[0]['span.id'], span.id, 'span.id matches')
138-
t.equal(logObjs[0]['service.name'], 'test-apm', 'service.name matches')
139-
t.equal(logObjs[0]['event.dataset'], 'test-apm', 'event.dataset matches')
143+
t.equal(logObjs[0]['service.name'], 'test-apm', 'service.name')
144+
t.equal(logObjs[0]['service.version'], 'override-serviceVersion', 'service.version')
145+
t.equal(logObjs[0]['service.environment'], 'development', 'service.environment')
146+
t.equal(logObjs[0]['service.node.name'], 'serviceNodeNameFromEnv', 'service.node.name')
147+
t.equal(logObjs[0]['event.dataset'], 'test-apm', 'event.dataset')
140148
finish()
141149
}
142150
}

loggers/winston/test/basic.test.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,3 +277,26 @@ test('convertErr=false allows passing through err=<non-Error>', t => {
277277
t.equal(rec.error, undefined, 'no rec.error is set')
278278
t.end()
279279
})
280+
281+
test('can configure correlation fields', t => {
282+
const cap = new CaptureTransport()
283+
const logger = winston.createLogger({
284+
format: ecsFormat({
285+
serviceName: 'override-serviceName',
286+
serviceVersion: 'override-serviceVersion',
287+
serviceEnvironment: 'override-serviceEnvironment',
288+
serviceNodeName: 'override-serviceNodeName',
289+
eventDataset: 'override-eventDataset'
290+
}),
291+
transports: [cap]
292+
})
293+
logger.info('hi')
294+
295+
const rec = cap.records[0]
296+
t.equal(rec['service.name'], 'override-serviceName')
297+
t.equal(rec['service.version'], 'override-serviceVersion')
298+
t.equal(rec['service.environment'], 'override-serviceEnvironment')
299+
t.equal(rec['service.node.name'], 'override-serviceNodeName')
300+
t.equal(rec['event.dataset'], 'override-eventDataset')
301+
t.end()
302+
})

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ const http = require('http')
4444
const ecsFormat = require('../') // @elastic/ecs-winston-format
4545
const winston = require('winston')
4646

47-
const ecsOpts = { convertReqRes: true }
47+
const ecsOpts = {
48+
convertReqRes: true,
49+
serviceVersion: 'override-serviceVersion'
50+
}
4851
if (disableApmIntegration) {
4952
ecsOpts.apmIntegration = false
5053
}

0 commit comments

Comments
 (0)