@zenvia/logger 1.6.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/npm-publish.yml +11 -10
- package/README.md +68 -8
- package/package.json +7 -4
- package/src/index.d.ts +12 -2
- package/src/index.js +7 -7
- package/src/lib/logger.d.ts +3 -1
- package/src/lib/logger.js +42 -14
- package/src/middleware/trace.d.ts +3 -2
- package/src/middleware/trace.js +5 -1
- package/test/lib/logger.spec.js +85 -0
|
@@ -11,10 +11,10 @@ jobs:
|
|
|
11
11
|
build:
|
|
12
12
|
runs-on: ubuntu-latest
|
|
13
13
|
steps:
|
|
14
|
-
- uses: actions/checkout@
|
|
15
|
-
- uses: actions/setup-node@
|
|
14
|
+
- uses: actions/checkout@v6
|
|
15
|
+
- uses: actions/setup-node@v6
|
|
16
16
|
with:
|
|
17
|
-
node-version:
|
|
17
|
+
node-version: '24'
|
|
18
18
|
- name: Install dependencies
|
|
19
19
|
run: npm ci
|
|
20
20
|
- name: Run lint
|
|
@@ -30,13 +30,14 @@ jobs:
|
|
|
30
30
|
publish-npm:
|
|
31
31
|
needs: build
|
|
32
32
|
runs-on: ubuntu-latest
|
|
33
|
+
permissions:
|
|
34
|
+
id-token: write
|
|
35
|
+
contents: read
|
|
33
36
|
steps:
|
|
34
|
-
- uses: actions/checkout@
|
|
35
|
-
- uses: actions/setup-node@
|
|
37
|
+
- uses: actions/checkout@v6
|
|
38
|
+
- uses: actions/setup-node@v6
|
|
36
39
|
with:
|
|
37
|
-
node-version:
|
|
38
|
-
registry-url: https://registry.npmjs.org
|
|
40
|
+
node-version: '24'
|
|
41
|
+
registry-url: 'https://registry.npmjs.org'
|
|
39
42
|
- run: npm ci
|
|
40
|
-
- run: npm publish
|
|
41
|
-
env:
|
|
42
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
43
|
+
- run: NODE_AUTH_TOKEN="" npm publish --provenance
|
package/README.md
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
# Zenvia Logger for Node.js
|
|
2
2
|
|
|
3
|
-
A wrapper for [Winston](https://github.com/winstonjs/winston) Logging [Node.js](https://nodejs.org/) library that formats the output on STDOUT as [Logstash](https://www.elastic.co/logstash) JSON format.
|
|
3
|
+
A wrapper for [Winston](https://github.com/winstonjs/winston) Logging [Node.js](https://nodejs.org/) library that formats the output on STDOUT as [Logstash](https://www.elastic.co/logstash) JSON format.
|
|
4
|
+
|
|
5
|
+
**Main features:**
|
|
6
|
+
- **Dual Package Support:** Native support for both **ES Modules (ESM)** and **CommonJS (CJS)**.
|
|
7
|
+
- **Node 24 Ready:** Optimized for native TypeScript execution (Type Stripping).
|
|
8
|
+
- **Distributed Tracing:** Automated request tracking using [cls-rtracer](https://www.npmjs.com/package/cls-rtracer).
|
|
4
9
|
|
|
5
10
|
[](LICENSE.md)
|
|
6
|
-
[](https://www.npmjs.com/package/@zenvia/logger)
|
|
7
12
|
[](https://coveralls.io/github/zenvia/zenvia-logger-node?branch=master)
|
|
8
|
-
[](https://david-dm.org/zenvia/zenvia-logger-node)
|
|
9
13
|
|
|
10
|
-
[](https://nodei.co/npm/@zenvia/logger/)
|
|
11
15
|
|
|
16
|
+
[](https://twitter.com/intent/follow?screen_name=ZENVIA_)
|
|
12
17
|
|
|
13
18
|
|
|
14
19
|
## Installation
|
|
@@ -101,7 +106,7 @@ Output:
|
|
|
101
106
|
}
|
|
102
107
|
```
|
|
103
108
|
|
|
104
|
-
|
|
109
|
+
## Available logging levels
|
|
105
110
|
|
|
106
111
|
The log levels are as follows.
|
|
107
112
|
|
|
@@ -115,7 +120,7 @@ For backward compatibility purposes, **"verbose"** and **"silly"** levels will b
|
|
|
115
120
|
|
|
116
121
|
|
|
117
122
|
|
|
118
|
-
|
|
123
|
+
## Adding extra key/value fields
|
|
119
124
|
|
|
120
125
|
```js
|
|
121
126
|
logger.debug('Some text message', { keyA: 'value A', keyB: 'value B' });
|
|
@@ -135,7 +140,7 @@ Output:
|
|
|
135
140
|
}
|
|
136
141
|
```
|
|
137
142
|
|
|
138
|
-
|
|
143
|
+
## Logging errors
|
|
139
144
|
|
|
140
145
|
```js
|
|
141
146
|
logger.error('Ops!', new Error('Something goes wrong'));
|
|
@@ -174,7 +179,7 @@ Output:
|
|
|
174
179
|
}
|
|
175
180
|
```
|
|
176
181
|
|
|
177
|
-
|
|
182
|
+
## Using trace logs
|
|
178
183
|
From version 1.5.0 it is possible to track logs. To do traceability, the cls-rTrace package is used. To use it, just add the middleware in the framework you are using. In this way it is possible to propagate the traceId received in a request to the logs throughout your project. If no traceId is passed in the request, Zenvia Logger generates a random traceId for the request being processed.
|
|
179
184
|
|
|
180
185
|
**Request example sending traceId:**
|
|
@@ -218,6 +223,61 @@ Log Output
|
|
|
218
223
|
}
|
|
219
224
|
```
|
|
220
225
|
|
|
226
|
+
## Contextual Logging (Async Context)
|
|
227
|
+
|
|
228
|
+
You can use `runWithContext` and `addContext` to manage metadata context across asynchronous flows (like Kafka consumers or complex background jobs). This ensures all logs within a specific execution thread share the same context without passing a logger instance manually.
|
|
229
|
+
|
|
230
|
+
### Basic Usage with runWithContext
|
|
231
|
+
|
|
232
|
+
Useful for isolating logs from different event sources or partitions.
|
|
233
|
+
|
|
234
|
+
```js
|
|
235
|
+
import logger from '@zenvia/logger';
|
|
236
|
+
|
|
237
|
+
async function handleEvent(event) {
|
|
238
|
+
await logger.runWithContext({
|
|
239
|
+
partition: event.partition,
|
|
240
|
+
topic: 'orders-topic'
|
|
241
|
+
}, async () => {
|
|
242
|
+
// All logs here will include partition and topic automatically
|
|
243
|
+
logger.info('Processing started');
|
|
244
|
+
await processOrder(event.data);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Enriching Context with addContext
|
|
250
|
+
|
|
251
|
+
You can inject new metadata into the current context at any point during the execution.
|
|
252
|
+
|
|
253
|
+
```js
|
|
254
|
+
import logger from '@zenvia/logger';
|
|
255
|
+
|
|
256
|
+
async function processOrder(data) {
|
|
257
|
+
logger.info('Validating...');
|
|
258
|
+
|
|
259
|
+
const provider = await api.getProvider(data.id);
|
|
260
|
+
|
|
261
|
+
// Mutates the current context to include providerId in all subsequent logs
|
|
262
|
+
logger.addContext({ providerId: provider.id });
|
|
263
|
+
|
|
264
|
+
logger.info('Validation complete');
|
|
265
|
+
// Output JSON will contain: partition, topic AND providerId
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Nested Contexts
|
|
270
|
+
|
|
271
|
+
Contexts can be nested. The inner context will inherit metadata from the parent.
|
|
272
|
+
|
|
273
|
+
```js
|
|
274
|
+
await logger.runWithContext({ level1: 'A' }, async () => {
|
|
275
|
+
await logger.runWithContext({ level2: 'B' }, async () => {
|
|
276
|
+
logger.info('Log with A and B');
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
221
281
|
## License
|
|
222
282
|
|
|
223
283
|
[MIT](LICENSE.md)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenvia/logger",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "A wrapper for Winston Logging Node.js library that formats the output on STDOUT as Logstash JSON format.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "./src/index",
|
|
@@ -31,19 +31,22 @@
|
|
|
31
31
|
"Zenvia",
|
|
32
32
|
"Winston"
|
|
33
33
|
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=16.4.0"
|
|
36
|
+
},
|
|
34
37
|
"dependencies": {
|
|
35
38
|
"app-root-dir": "^1.0.2",
|
|
36
39
|
"cls-rtracer": "^2.6.3",
|
|
37
|
-
"winston": "^3.
|
|
40
|
+
"winston": "^3.19.0"
|
|
38
41
|
},
|
|
39
42
|
"devDependencies": {
|
|
40
43
|
"chai": "^4.2.0",
|
|
41
44
|
"chai-as-promised": "^7.1.1",
|
|
42
|
-
"coveralls": "^
|
|
45
|
+
"coveralls-next": "^6.0.1",
|
|
43
46
|
"eslint": "^7.10.0",
|
|
44
47
|
"eslint-config-airbnb-base": "^14.2.0",
|
|
45
48
|
"eslint-plugin-import": "^2.22.1",
|
|
46
|
-
"mocha": "^
|
|
49
|
+
"mocha": "^11.7.5",
|
|
47
50
|
"nyc": "^15.1.0",
|
|
48
51
|
"sinon": "^9.2.0",
|
|
49
52
|
"sinon-chai": "^3.5.0",
|
package/src/index.d.ts
CHANGED
|
@@ -1,2 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
|
|
2
|
+
import { ZenviaLogger } from './lib/logger';
|
|
3
|
+
import traceMiddlewareFactory from './middleware/trace';
|
|
4
|
+
|
|
5
|
+
type TraceMiddlewareInstance = ReturnType<typeof traceMiddlewareFactory>;
|
|
6
|
+
type ZenviaLoggerModule = ZenviaLogger & {
|
|
7
|
+
default: ZenviaLogger;
|
|
8
|
+
traceMiddleware: TraceMiddlewareInstance;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
declare const loggerModule: ZenviaLoggerModule;
|
|
12
|
+
export = loggerModule;
|
package/src/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
Object.defineProperty(exports, '__esModule', {
|
|
2
|
-
value: true,
|
|
3
|
-
});
|
|
4
|
-
|
|
5
1
|
const logger = require('./lib/logger');
|
|
6
|
-
const
|
|
2
|
+
const traceMiddlewareFactory = require('./middleware/trace');
|
|
3
|
+
|
|
4
|
+
const traceMiddleware = traceMiddlewareFactory();
|
|
7
5
|
|
|
8
|
-
exports
|
|
9
|
-
exports.
|
|
6
|
+
module.exports = logger;
|
|
7
|
+
module.exports.logger = logger;
|
|
8
|
+
module.exports.traceMiddleware = traceMiddleware;
|
|
9
|
+
module.exports.default = logger;
|
package/src/lib/logger.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Logger, LeveledLogMethod } from 'winston';
|
|
2
2
|
|
|
3
|
-
interface ZenviaLogger extends Logger {
|
|
3
|
+
export interface ZenviaLogger extends Logger {
|
|
4
4
|
fatal: LeveledLogMethod;
|
|
5
5
|
isFatalEnabled(): boolean;
|
|
6
|
+
runWithContext<T>(context: object, fn: () => Promise<T> | T): Promise<T>;
|
|
7
|
+
addContext(context: object): void;
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
declare const logger: ZenviaLogger;
|
package/src/lib/logger.js
CHANGED
|
@@ -7,9 +7,12 @@ const winston = require('winston');
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const appRootDir = require('app-root-dir').get();
|
|
9
9
|
const rTrace = require('cls-rtracer');
|
|
10
|
+
const { AsyncLocalStorage } = require('node:async_hooks');
|
|
10
11
|
|
|
11
12
|
const appPackage = require(path.join(appRootDir, 'package'));
|
|
12
13
|
|
|
14
|
+
const loggerStorage = new AsyncLocalStorage();
|
|
15
|
+
|
|
13
16
|
const customFormatJson = winston.format((info) => {
|
|
14
17
|
let stack;
|
|
15
18
|
|
|
@@ -55,7 +58,7 @@ const createConsoleTransport = () => new winston.transports.Console({
|
|
|
55
58
|
stderrLevels: ['fatal', 'error'],
|
|
56
59
|
});
|
|
57
60
|
|
|
58
|
-
const
|
|
61
|
+
const baseLogger = winston.createLogger({
|
|
59
62
|
levels: {
|
|
60
63
|
fatal: 0,
|
|
61
64
|
error: 1,
|
|
@@ -74,6 +77,11 @@ const logger = winston.createLogger({
|
|
|
74
77
|
exitOnError: false,
|
|
75
78
|
});
|
|
76
79
|
|
|
80
|
+
function getActiveLogger() {
|
|
81
|
+
const store = loggerStorage.getStore();
|
|
82
|
+
return store ? store.current : baseLogger;
|
|
83
|
+
}
|
|
84
|
+
|
|
77
85
|
function leveledLogFn(level) {
|
|
78
86
|
return function log(...args) {
|
|
79
87
|
return logFn(level, ...args);
|
|
@@ -81,8 +89,9 @@ function leveledLogFn(level) {
|
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
function logFn(level, msg) {
|
|
92
|
+
const currentLogger = getActiveLogger();
|
|
84
93
|
if (arguments.length === 1 && typeof level !== 'object') {
|
|
85
|
-
return
|
|
94
|
+
return currentLogger;
|
|
86
95
|
}
|
|
87
96
|
if (arguments.length === 2) {
|
|
88
97
|
if (msg && typeof msg === 'object') {
|
|
@@ -93,24 +102,43 @@ function logFn(level, msg) {
|
|
|
93
102
|
};
|
|
94
103
|
}
|
|
95
104
|
}
|
|
96
|
-
return
|
|
105
|
+
return currentLogger.log(...arguments);
|
|
97
106
|
}
|
|
98
107
|
|
|
99
108
|
function isLevelEnabledFn(level) {
|
|
100
109
|
return function isLevelEnabled() {
|
|
101
|
-
return
|
|
110
|
+
return getActiveLogger().isLevelEnabled(level);
|
|
102
111
|
};
|
|
103
112
|
}
|
|
104
113
|
|
|
105
|
-
logger
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
const logger = {
|
|
115
|
+
...baseLogger,
|
|
116
|
+
|
|
117
|
+
fatal: leveledLogFn('fatal'),
|
|
118
|
+
error: leveledLogFn('error'),
|
|
119
|
+
warn: leveledLogFn('warn'),
|
|
120
|
+
info: leveledLogFn('info'),
|
|
121
|
+
debug: leveledLogFn('debug'),
|
|
122
|
+
verbose: leveledLogFn('verbose'),
|
|
123
|
+
silly: leveledLogFn('silly'),
|
|
124
|
+
log: logFn,
|
|
125
|
+
isFatalEnabled: isLevelEnabledFn('fatal'),
|
|
126
|
+
|
|
127
|
+
runWithContext: (context, fn) => {
|
|
128
|
+
const parentLogger = loggerStorage.getStore()?.current || baseLogger;
|
|
129
|
+
const child = parentLogger.child(context);
|
|
130
|
+
return loggerStorage.run({ current: child }, fn);
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
addContext: (context) => {
|
|
134
|
+
const store = loggerStorage.getStore();
|
|
135
|
+
if (store) {
|
|
136
|
+
store.current = store.current.child(context);
|
|
137
|
+
} else {
|
|
138
|
+
baseLogger.warn('Attempted to call addContext outside of an AsyncLocalStorage context');
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
};
|
|
115
142
|
|
|
116
143
|
module.exports = logger;
|
|
144
|
+
module.exports.default = logger;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import rTracer from 'cls-rtracer';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
export type TracerMiddlewareResult =
|
|
4
4
|
ReturnType<typeof rTracer.expressMiddleware> |
|
|
5
5
|
ReturnType<typeof rTracer.fastifyPlugin> |
|
|
6
6
|
typeof rTracer.hapiPlugin |
|
|
7
7
|
ReturnType<typeof rTracer.koaMiddleware>;
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
declare const traceMiddlewareFactory: () => TracerMiddlewareResult;
|
|
10
|
+
export default traceMiddlewareFactory;
|
package/src/middleware/trace.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const rTracer = require('cls-rtracer');
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const traceMiddlewareFactory = () => {
|
|
4
4
|
const frameworkMiddleware = process.env.LOGGING_FRAMEWORK_MIDDLEWARE;
|
|
5
5
|
|
|
6
6
|
const RTRACER_OPTIONS = {
|
|
@@ -23,3 +23,7 @@ module.exports = () => {
|
|
|
23
23
|
|
|
24
24
|
return middleware[frameworkMiddleware || 'EXPRESS'];
|
|
25
25
|
};
|
|
26
|
+
|
|
27
|
+
module.exports = traceMiddlewareFactory;
|
|
28
|
+
module.exports.traceMiddleware = traceMiddlewareFactory;
|
|
29
|
+
module.exports.default = traceMiddlewareFactory;
|
package/test/lib/logger.spec.js
CHANGED
|
@@ -299,4 +299,89 @@ describe('Logger test', () => {
|
|
|
299
299
|
obj.should.not.have.property('level');
|
|
300
300
|
});
|
|
301
301
|
});
|
|
302
|
+
|
|
303
|
+
describe('Contextual Logging (AsyncLocalStorage)', () => {
|
|
304
|
+
it('should maintain context metadata across async calls using runWithContext', async () => {
|
|
305
|
+
await logger.runWithContext({ partition: 1024, topic: 'my-topic' }, async () => {
|
|
306
|
+
await Promise.resolve();
|
|
307
|
+
|
|
308
|
+
logger.info('message inside context');
|
|
309
|
+
|
|
310
|
+
const output = JSON.parse(stdMocks.flush().stdout[0]);
|
|
311
|
+
output.should.have.property('partition').and.be.equal(1024);
|
|
312
|
+
output.should.have.property('topic').and.be.equal('my-topic');
|
|
313
|
+
output.should.have.property('message').and.be.equal('message inside context');
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should update context metadata using addContext (mutation)', async () => {
|
|
318
|
+
await logger.runWithContext({ requestId: 'initial-id' }, async () => {
|
|
319
|
+
logger.info('first log');
|
|
320
|
+
const output1 = JSON.parse(stdMocks.flush().stdout[0]);
|
|
321
|
+
output1.should.not.have.property('providerId');
|
|
322
|
+
|
|
323
|
+
logger.addContext({ providerId: 'zenvia-123' });
|
|
324
|
+
|
|
325
|
+
logger.info('second log');
|
|
326
|
+
const output2 = JSON.parse(stdMocks.flush().stdout[0]);
|
|
327
|
+
output2.should.have.property('requestId').and.be.equal('initial-id');
|
|
328
|
+
output2.should.have.property('providerId').and.be.equal('zenvia-123');
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('should warn when addContext is called outside of an active context', () => {
|
|
333
|
+
logger.addContext({ some: 'data' });
|
|
334
|
+
|
|
335
|
+
const actualOutput = stdMocks.flush().stdout[0];
|
|
336
|
+
const parsedOutput = JSON.parse(actualOutput);
|
|
337
|
+
|
|
338
|
+
parsedOutput.should.have.property('level').and.be.equal('WARN');
|
|
339
|
+
parsedOutput.should.have.property('message').and.be.equal('Attempted to call addContext outside of an AsyncLocalStorage context');
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('should isolate contexts between concurrent executions', async () => {
|
|
343
|
+
const flow1 = logger.runWithContext({ flow: 1 }, async () => {
|
|
344
|
+
await Promise.resolve();
|
|
345
|
+
logger.info('log flow 1');
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const flow2 = logger.runWithContext({ flow: 2 }, async () => {
|
|
349
|
+
await Promise.resolve();
|
|
350
|
+
logger.info('log flow 2');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
await Promise.all([flow1, flow2]);
|
|
354
|
+
|
|
355
|
+
const logs = stdMocks.flush().stdout.map((line) => JSON.parse(line));
|
|
356
|
+
|
|
357
|
+
const logFlow2 = logs.find((l) => l.message === 'log flow 2');
|
|
358
|
+
const logFlow1 = logs.find((l) => l.message === 'log flow 1');
|
|
359
|
+
|
|
360
|
+
logFlow2.should.have.property('flow').and.be.equal(2);
|
|
361
|
+
logFlow1.should.have.property('flow').and.be.equal(1);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('should not leak metadata to logs outside of context', async () => {
|
|
365
|
+
await logger.runWithContext({ internal: 'secret' }, async () => {
|
|
366
|
+
logger.info('inside');
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
logger.info('outside');
|
|
370
|
+
|
|
371
|
+
const outputs = stdMocks.flush().stdout.map((line) => JSON.parse(line));
|
|
372
|
+
outputs[0].should.have.property('internal').and.be.equal('secret');
|
|
373
|
+
outputs[1].should.not.have.property('internal');
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('should allow nested contexts (re-nesting via runWithContext)', async () => {
|
|
377
|
+
await logger.runWithContext({ level1: 'a' }, async () => {
|
|
378
|
+
await logger.runWithContext({ level2: 'b' }, async () => {
|
|
379
|
+
logger.info('nested log');
|
|
380
|
+
const output = JSON.parse(stdMocks.flush().stdout[0]);
|
|
381
|
+
output.should.have.property('level1').and.be.equal('a');
|
|
382
|
+
output.should.have.property('level2').and.be.equal('b');
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
});
|
|
302
387
|
});
|