@zenvia/logger 1.6.1 → 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 +47 -31
- package/src/middleware/trace.d.ts +3 -2
- package/src/middleware/trace.js +5 -1
- package/test/lib/logger.spec.js +100 -44
|
@@ -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,28 +7,13 @@ 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
|
|
|
13
|
-
const
|
|
14
|
-
const sanitizeCRLFInjection = (str) => str
|
|
15
|
-
.replace(/\n|\r/g, (x) => (x === '\n' ? '#n' : '#r'));
|
|
16
|
-
|
|
17
|
-
Object.keys(info).forEach((key) => {
|
|
18
|
-
if (typeof info[key] === 'string') {
|
|
19
|
-
info[key] = sanitizeCRLFInjection(info[key]);
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (info[key] instanceof Function) {
|
|
24
|
-
delete info[key];
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
};
|
|
14
|
+
const loggerStorage = new AsyncLocalStorage();
|
|
28
15
|
|
|
29
16
|
const customFormatJson = winston.format((info) => {
|
|
30
|
-
sanitizeInfo(info);
|
|
31
|
-
|
|
32
17
|
let stack;
|
|
33
18
|
|
|
34
19
|
if (info.stack) {
|
|
@@ -49,6 +34,12 @@ const customFormatJson = winston.format((info) => {
|
|
|
49
34
|
traceId: rTrace.id(),
|
|
50
35
|
};
|
|
51
36
|
|
|
37
|
+
Object.keys(info).forEach((key) => {
|
|
38
|
+
if (info[key] instanceof Function) {
|
|
39
|
+
delete info[key];
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
52
43
|
return info;
|
|
53
44
|
});
|
|
54
45
|
|
|
@@ -67,7 +58,7 @@ const createConsoleTransport = () => new winston.transports.Console({
|
|
|
67
58
|
stderrLevels: ['fatal', 'error'],
|
|
68
59
|
});
|
|
69
60
|
|
|
70
|
-
const
|
|
61
|
+
const baseLogger = winston.createLogger({
|
|
71
62
|
levels: {
|
|
72
63
|
fatal: 0,
|
|
73
64
|
error: 1,
|
|
@@ -86,6 +77,11 @@ const logger = winston.createLogger({
|
|
|
86
77
|
exitOnError: false,
|
|
87
78
|
});
|
|
88
79
|
|
|
80
|
+
function getActiveLogger() {
|
|
81
|
+
const store = loggerStorage.getStore();
|
|
82
|
+
return store ? store.current : baseLogger;
|
|
83
|
+
}
|
|
84
|
+
|
|
89
85
|
function leveledLogFn(level) {
|
|
90
86
|
return function log(...args) {
|
|
91
87
|
return logFn(level, ...args);
|
|
@@ -93,8 +89,9 @@ function leveledLogFn(level) {
|
|
|
93
89
|
}
|
|
94
90
|
|
|
95
91
|
function logFn(level, msg) {
|
|
92
|
+
const currentLogger = getActiveLogger();
|
|
96
93
|
if (arguments.length === 1 && typeof level !== 'object') {
|
|
97
|
-
return
|
|
94
|
+
return currentLogger;
|
|
98
95
|
}
|
|
99
96
|
if (arguments.length === 2) {
|
|
100
97
|
if (msg && typeof msg === 'object') {
|
|
@@ -105,24 +102,43 @@ function logFn(level, msg) {
|
|
|
105
102
|
};
|
|
106
103
|
}
|
|
107
104
|
}
|
|
108
|
-
return
|
|
105
|
+
return currentLogger.log(...arguments);
|
|
109
106
|
}
|
|
110
107
|
|
|
111
108
|
function isLevelEnabledFn(level) {
|
|
112
109
|
return function isLevelEnabled() {
|
|
113
|
-
return
|
|
110
|
+
return getActiveLogger().isLevelEnabled(level);
|
|
114
111
|
};
|
|
115
112
|
}
|
|
116
113
|
|
|
117
|
-
logger
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
+
};
|
|
127
142
|
|
|
128
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
|
@@ -70,6 +70,21 @@ describe('Logger test', () => {
|
|
|
70
70
|
JSON.parse(actualOutput).should.be.deep.equal(expectedOutput);
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
+
it('should remove attributes that are log functions, leaving only the @timestamp, application, message and level fields', () => {
|
|
74
|
+
logger.info('some message', { field1: () => {} });
|
|
75
|
+
const expectedOutput = {
|
|
76
|
+
'@timestamp': '2018-06-05T18:20:42.345Z',
|
|
77
|
+
'@version': 1,
|
|
78
|
+
application: 'application-name',
|
|
79
|
+
host: os.hostname(),
|
|
80
|
+
message: 'some message',
|
|
81
|
+
level: 'INFO',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const actualOutput = stdMocks.flush().stdout[0];
|
|
85
|
+
JSON.parse(actualOutput).should.be.deep.equal(expectedOutput);
|
|
86
|
+
});
|
|
87
|
+
|
|
73
88
|
it('should log @timestamp, application, message, level and environment fields', () => {
|
|
74
89
|
process.env.NODE_ENV = 'test';
|
|
75
90
|
logger.info('some message');
|
|
@@ -241,50 +256,6 @@ describe('Logger test', () => {
|
|
|
241
256
|
});
|
|
242
257
|
|
|
243
258
|
describe('Logging format', () => {
|
|
244
|
-
it('should replace LF characters from log (POSIX systems)', () => {
|
|
245
|
-
logger.debug(`some message
|
|
246
|
-
other CRLF injection message`);
|
|
247
|
-
const expectedOutput = {
|
|
248
|
-
'@timestamp': '2018-06-05T18:20:42.345Z',
|
|
249
|
-
'@version': 1,
|
|
250
|
-
application: 'application-name',
|
|
251
|
-
host: os.hostname(),
|
|
252
|
-
message: 'some message#nother CRLF injection message',
|
|
253
|
-
level: 'DEBUG',
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
const actualOutput = stdMocks.flush().stdout[0];
|
|
257
|
-
JSON.parse(actualOutput).should.be.deep.equal(expectedOutput);
|
|
258
|
-
|
|
259
|
-
logger.debug('some\n CRLF\n injection\n message');
|
|
260
|
-
const expectedOutput2 = {
|
|
261
|
-
'@timestamp': '2018-06-05T18:20:42.345Z',
|
|
262
|
-
'@version': 1,
|
|
263
|
-
application: 'application-name',
|
|
264
|
-
host: os.hostname(),
|
|
265
|
-
message: 'some#n CRLF#n injection#n message',
|
|
266
|
-
level: 'DEBUG',
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
const actualOutput2 = stdMocks.flush().stdout[0];
|
|
270
|
-
JSON.parse(actualOutput2).should.be.deep.equal(expectedOutput2);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it('should replace CRLF characters from log (Windows systems)', () => {
|
|
274
|
-
logger.debug('some\r\n CRLF\r\n injection\r\n message');
|
|
275
|
-
const expectedOutput = {
|
|
276
|
-
'@timestamp': '2018-06-05T18:20:42.345Z',
|
|
277
|
-
'@version': 1,
|
|
278
|
-
application: 'application-name',
|
|
279
|
-
host: os.hostname(),
|
|
280
|
-
message: 'some#r#n CRLF#r#n injection#r#n message',
|
|
281
|
-
level: 'DEBUG',
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
const actualOutput = stdMocks.flush().stdout[0];
|
|
285
|
-
JSON.parse(actualOutput).should.be.deep.equal(expectedOutput);
|
|
286
|
-
});
|
|
287
|
-
|
|
288
259
|
it('should get not format when LOGGING_FORMATTER_DISABLED environment is true', () => {
|
|
289
260
|
delete require.cache[require.resolve('../../src/lib/logger')];
|
|
290
261
|
process.env.LOGGING_FORMATTER_DISABLED = 'true';
|
|
@@ -328,4 +299,89 @@ other CRLF injection message`);
|
|
|
328
299
|
obj.should.not.have.property('level');
|
|
329
300
|
});
|
|
330
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
|
+
});
|
|
331
387
|
});
|