Skip to content

Commit cfb344f

Browse files
authored
feat(winston): separate winston ecsFormat to composable ecsFields and ecsStringify (take 2) (#161)
This adds `ecsFields` and `ecsStringify` formats. `ecsFormat` is now just a composition of the two. Named import is now preferred, but the old default export remains for now for backwards compat. The single downside here is that the order of serialized fields no longer follows the spec (https://github.com/elastic/ecs-logging/blob/main/spec/spec.json) suggestion of having `@timestamp` then `log.level` then `message` then the rest ordering. This was a trade-off with avoiding creating an additional object for each log record. Fixes: #57 Obsoletes: #65
1 parent c6ac65d commit cfb344f

19 files changed

+427
-139
lines changed

docs/winston.asciidoc

Lines changed: 82 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,18 @@ $ npm install @elastic/ecs-winston-format
2626

2727
[source,js]
2828
----
29-
const winston = require('winston')
30-
const ecsFormat = require('@elastic/ecs-winston-format')
29+
const winston = require('winston');
30+
const { ecsFormat } = require('@elastic/ecs-winston-format');
3131
3232
const logger = winston.createLogger({
3333
format: ecsFormat(/* options */), <1>
3434
transports: [
3535
new winston.transports.Console()
3636
]
37-
})
37+
});
3838
39-
logger.info('hi')
40-
logger.error('oops there is a problem', { err: new Error('boom') })
39+
logger.info('hi');
40+
logger.error('oops there is a problem', { err: new Error('boom') });
4141
----
4242
<1> Pass the ECS formatter to winston here.
4343

@@ -58,19 +58,19 @@ NOTE: You might like to try out our tutorial using Node.js ECS logging with wins
5858

5959
[source,js]
6060
----
61-
const winston = require('winston')
62-
const ecsFormat = require('@elastic/ecs-winston-format')
61+
const winston = require('winston');
62+
const { ecsFormat } = require('@elastic/ecs-winston-format');
6363
6464
const logger = winston.createLogger({
6565
level: 'info',
6666
format: ecsFormat(/* options */), <1>
6767
transports: [
6868
new winston.transports.Console()
6969
]
70-
})
70+
});
7171
72-
logger.info('hi')
73-
logger.error('oops there is a problem', { foo: 'bar' })
72+
logger.info('hi');
73+
logger.error('oops there is a problem', { foo: 'bar' });
7474
----
7575
<1> See available options <<winston-ref,below>>.
7676

@@ -99,17 +99,17 @@ For https://github.com/elastic/ecs-logging-nodejs/blob/main/packages/ecs-winston
9999

100100
[source,js]
101101
----
102-
const winston = require('winston')
103-
const ecsFormat = require('@elastic/ecs-winston-format')
102+
const winston = require('winston');
103+
const { ecsFormat } = require('@elastic/ecs-winston-format');
104104
const logger = winston.createLogger({
105105
format: ecsFormat(), <1>
106106
transports: [
107107
new winston.transports.Console()
108108
]
109-
})
109+
});
110110
111-
const myErr = new Error('boom')
112-
logger.info('oops', { err: myErr }) <2>
111+
const myErr = new Error('boom');
112+
logger.info('oops', { err: myErr }); <2>
113113
----
114114

115115
will yield (pretty-printed for readability):
@@ -153,27 +153,27 @@ objects when passed as the `req` and `res` meta fields, respectively.
153153

154154
[source,js]
155155
----
156-
const http = require('http')
157-
const winston = require('winston')
158-
const ecsFormat = require('@elastic/ecs-winston-format')
156+
const http = require('http');
157+
const winston = require('winston');
158+
const { ecsFormat } = require('@elastic/ecs-winston-format');
159159
160160
const logger = winston.createLogger({
161161
level: 'info',
162162
format: ecsFormat({ convertReqRes: true }), <1>
163163
transports: [
164164
new winston.transports.Console()
165165
]
166-
})
166+
});
167167
168-
const server = http.createServer(handler)
168+
const server = http.createServer(handler);
169169
server.listen(3000, () => {
170170
logger.info('listening at http://localhost:3000')
171-
})
171+
});
172172
173173
function handler (req, res) {
174-
res.setHeader('Foo', 'Bar')
175-
res.end('ok')
176-
logger.info('handled request', { req, res }) <2>
174+
res.setHeader('Foo', 'Bar');
175+
res.end('ok');
176+
logger.info('handled request', { req, res }); <2>
177177
}
178178
----
179179
<1> use `convertReqRes` option
@@ -271,6 +271,18 @@ const logger = winston.createLogger({
271271
})
272272
----
273273

274+
[float]
275+
[[winston-limitations]]
276+
=== Limitations and Considerations
277+
278+
The https://github.com/elastic/ecs-logging/tree/main/spec[ecs-logging spec]
279+
suggests that the first three fields in log records should be `@timestamp`,
280+
`log.level`, and `message`. As of version 1.5.0, this formatter does *not*
281+
follow this suggestion. It would be possible but would require creating a new
282+
Object in `ecsFields` for each log record. Given that ordering of ecs-logging
283+
fields is for *human readability* and does not affect interoperability, the
284+
decision was made to prefer performance.
285+
274286
[float]
275287
[[winston-ref]]
276288
=== Reference
@@ -289,4 +301,49 @@ const logger = winston.createLogger({
289301
** `serviceNodeName` +{type-string}+ A "service.node.name" value. If specified this overrides any value from an active APM agent.
290302
** `eventDataset` +{type-string}+ A "event.dataset" value. If specified this overrides the default of using `${serviceVersion}`.
291303

292-
Create a formatter for winston that emits in ECS Logging format.
304+
Create a formatter for winston that emits in ECS Logging format. This is a
305+
single format that handles both <<winston-ref-ecsFields>> and <<winston-ref-ecsStringify>>.
306+
The following two are equivalent:
307+
308+
[source,js]
309+
----
310+
const { ecsFormat, ecsFields, ecsStringify } = require('@elastic/ecs-winston-format');
311+
const winston = require('winston');
312+
313+
const logger = winston.createLogger({
314+
format: ecsFormat(/* options */),
315+
// ...
316+
});
317+
318+
const logger = winston.createLogger({
319+
format: winston.format.combine(
320+
ecsFields(/* options */),
321+
ecsStringify()
322+
),
323+
// ...
324+
});
325+
----
326+
327+
[float]
328+
[[winston-ref-ecsFields]]
329+
==== `ecsFields([options])`
330+
331+
* `options` +{type-object}+ The following options are supported:
332+
** `convertErr` +{type-boolean}+ Whether to convert a logged `err` field to ECS error fields. *Default:* `true`.
333+
** `convertReqRes` +{type-boolean}+ Whether to logged `req` and `res` HTTP request and response fields to ECS HTTP, User agent, and URL fields. *Default:* `false`.
334+
** `apmIntegration` +{type-boolean}+ Whether to enable APM agent integration. *Default:* `true`.
335+
** `serviceName` +{type-string}+ A "service.name" value. If specified this overrides any value from an active APM agent.
336+
** `serviceVersion` +{type-string}+ A "service.version" value. If specified this overrides any value from an active APM agent.
337+
** `serviceEnvironment` +{type-string}+ A "service.environment" value. If specified this overrides any value from an active APM agent.
338+
** `serviceNodeName` +{type-string}+ A "service.node.name" value. If specified this overrides any value from an active APM agent.
339+
** `eventDataset` +{type-string}+ A "event.dataset" value. If specified this overrides the default of using `${serviceVersion}`.
340+
341+
Create a formatter for winston that converts fields on the log record info
342+
objecct to ECS Logging format.
343+
344+
[float]
345+
[[winston-ref-ecsStringify]]
346+
==== `ecsStringify([options])`
347+
348+
Create a formatter for winston that stringifies/serializes the log record to
349+
JSON. (This is very similar to `logform.json()`.)

packages/ecs-winston-format/CHANGELOG.md

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

33
## Unreleased
44

5+
- Add `ecsFields` and `ecsStringify` exports that are winston formatters
6+
that separate the gathering of ECS fields (`ecsFields`) and the
7+
stringification of a log record to an ecs-logging JSON object
8+
(`ecsStringify`). This allows for better composability using
9+
[`winston.format.combine`](https://github.com/winstonjs/logform#combine).
10+
11+
The preferred way to import now changes to:
12+
13+
```js
14+
const { ecsFormat } = require('@elastic/ecs-winston-format'); // NEW
15+
```
16+
17+
The old way will be deprecated and removed in the future:
18+
19+
```js
20+
const ecsFormat = require('@elastic/ecs-winston-format'); // OLD
21+
```
22+
23+
Common usage will still use `ecsFormat` in the same way:
24+
25+
```js
26+
const { ecsFormat } = require('@elastic/ecs-winston-format');
27+
const log = winston.createLogger({
28+
format: ecsFormat(<options>),
29+
// ...
30+
```
31+
32+
However, one can use the separated formatters as follows:
33+
34+
```js
35+
const { ecsFields, ecsStringify } = require('@elastic/ecs-winston-format');
36+
const log = winston.createLogger({
37+
format: winston.format.combine(
38+
ecsFields(<options>),
39+
// Add a custom formatter to redact fields here.
40+
ecsStringify()
41+
),
42+
// ...
43+
```
44+
45+
One good use case is for redaction of sensitive fields in the log record
46+
as in https://github.com/elastic/ecs-logging-nodejs/issues/57. See a
47+
complete example at [examples/redact-fields.js](./examples/redact-fields.js).
48+
549
- Fix/improve serialization of error details to `error.*` fields for the
650
various ways a Winston logger handles `Error` instances.
751

packages/ecs-winston-format/README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,19 @@ npm install @elastic/ecs-winston-format
2727
## Usage
2828

2929
```js
30-
const winston = require('winston')
31-
const ecsFormat = require('@elastic/ecs-winston-format')
30+
const winston = require('winston');
31+
const { ecsFormat } = require('@elastic/ecs-winston-format');
3232

3333
const logger = winston.createLogger({
3434
level: 'info',
3535
format: ecsFormat(/* options */),
3636
transports: [
3737
new winston.transports.Console()
3838
]
39-
})
39+
});
4040

41-
logger.info('hi')
42-
logger.error('oops there is a problem', { foo: 'bar' })
41+
logger.info('hi');
42+
logger.error('oops there is a problem', { foo: 'bar' });
4343
```
4444

4545
Running this script will produce log output similar to the following:

packages/ecs-winston-format/examples/basic-without-ecs-format.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const logger = winston.createLogger({
2828
// winston format:
2929
format: winston.format.combine(
3030
winston.format.timestamp(),
31+
winston.format.errors({ stack: true, cause: true }),
3132
winston.format.json()
3233
),
3334
transports: [
@@ -36,4 +37,8 @@ const logger = winston.createLogger({
3637
})
3738

3839
logger.info('hi')
39-
logger.error('oops there is a problem', { foo: 'bar' })
40+
logger.warn('look out', { foo: 'bar' })
41+
42+
const err = new Error('boom', { cause: new Error('the cause') })
43+
err.code = 42
44+
logger.error('here is an exception', err)

packages/ecs-winston-format/examples/basic.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@
1818
'use strict'
1919

2020
const winston = require('winston')
21-
const ecsFormat = require('../') // @elastic/ecs-winston-format
21+
const { ecsFormat } = require('../') // @elastic/ecs-winston-format
2222

2323
const logger = winston.createLogger({
2424
level: 'info',
2525
format: ecsFormat(),
26-
// Compare to:
26+
// Compare to the following (see "basic-without-ecs-format.js"):
2727
// format: winston.format.combine(
28+
// winston.format.timestamp(),
2829
// winston.format.errors({stack: true, cause: true}),
2930
// winston.format.json()
3031
// ),

packages/ecs-winston-format/examples/error.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
'use strict'
1919

2020
const winston = require('winston')
21-
const ecsFormat = require('../') // @elastic/ecs-winston-format
21+
const { ecsFormat } = require('../') // @elastic/ecs-winston-format
2222

2323
const logger = winston.createLogger({
2424
level: 'info',

packages/ecs-winston-format/examples/express.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
// This shows how one could use @elastic/ecs-winston-format with Express.
2121
// This implements simple Express middleware to do so.
2222

23-
const ecsFormat = require('../') // @elastic/ecs-winston-format
23+
const { ecsFormat } = require('../') // @elastic/ecs-winston-format
2424
const express = require('express')
2525
const winston = require('winston')
2626

packages/ecs-winston-format/examples/http-with-elastic-apm.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const apm = require('elastic-apm-node').start({
3838

3939
const http = require('http')
4040
const winston = require('winston')
41-
const ecsFormat = require('../') // @elastic/ecs-winston-format
41+
const { ecsFormat } = require('../') // @elastic/ecs-winston-format
4242

4343
const logger = winston.createLogger({
4444
level: 'info',

packages/ecs-winston-format/examples/http.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
const http = require('http')
2121
const winston = require('winston')
22-
const ecsFormat = require('../') // @elastic/ecs-winston-format
22+
const { ecsFormat } = require('../') // @elastic/ecs-winston-format
2323

2424
const logger = winston.createLogger({
2525
level: 'info',

0 commit comments

Comments
 (0)