support AsyncLocalStorage stored object merged into logs, breaking changes to constru...
[squeep-logger-json-console] / test / lib / logger.js
1 /* eslint-env mocha */
2 'use strict';
3
4 const assert = require('node:assert');
5 const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
6 const Logger = require('../../lib/logger');
7 const http = require('node:http');
8 const { AsyncLocalStorage } = require('node:async_hooks');
9 const asyncLocalStorage = new AsyncLocalStorage();
10
11 describe('Logger', function () {
12 let config, logger, commonObject, scope, message;
13
14 beforeEach(function () {
15 config = {};
16 commonObject = {
17 nodeId: '3c100e84-9a7f-11ec-9b4e-0025905f714a',
18 };
19 logger = new Logger(config, commonObject);
20 Object.keys(Logger.nullLogger).forEach((level) => sinon.stub(logger.backend, level));
21 scope = 'testScope';
22 message = 'message';
23 });
24 this.afterEach(function () {
25 sinon.restore();
26 });
27
28 it('logs a message', function () {
29 logger.info(scope, message, { baz: 'quux' }, { foo: 1 }, 'more other');
30 assert(logger.backend.info.called);
31 assert(logger.backend.info.args[0][0].includes(message));
32 });
33
34 it('stubs missing levels', function () {
35 const backend = {
36 debug: () => {},
37 }
38 logger = new Logger(config, commonObject, undefined, backend);
39 assert.strictEqual(typeof logger.info, 'function');
40 });
41
42 it('logs BigInts', function () {
43 logger.info(scope, message, { aBigInteger: BigInt(2) });
44 assert(logger.backend.info.called);
45 assert(logger.backend.info.args[0][0].includes('"2"'));
46 });
47
48 it('logs Errors', function () {
49 logger.error(scope, message, { e: new Error('an error') });
50 assert(logger.backend.error.called);
51 assert(logger.backend.error.args[0][0].includes('an error'));
52 });
53
54 it('covers config settings', function () {
55 config.ignoreBelowLevel = 'info';
56 logger = new Logger(config);
57 logger.debug(scope, message, {});
58 assert(logger.backend.debug.notCalled);
59 });
60
61 it('covers config error', function () {
62 config.ignoreBelowLevel = 'not a level';
63 try {
64 logger = new Logger(config);
65 assert.fail('expected RangeError here');
66 } catch (e) {
67 assert(e instanceof RangeError);
68 }
69 });
70
71 it('covers empty fields', function () {
72 logger.info();
73 assert(logger.backend.info.called);
74 assert(logger.backend.info.args[0][0].includes('[unknown]'));
75 });
76
77 it('sanitizes', function () {
78 logger.dataSanitizers.push((data, sanitize = true) => {
79 let unclean = false;
80 const credentialLength = data?.ctx?.parsedBody?.credential?.length;
81 if (credentialLength) {
82 unclean = true;
83 }
84 if (unclean && sanitize) {
85 data.ctx.parsedBody.credential = '*'.repeat(credentialLength);
86 }
87 return unclean;
88 });
89 logger.info(scope, message, {
90 ctx: {
91 parsedBody: {
92 credential: 'secret',
93 },
94 },
95 });
96 assert(logger.backend.info.called);
97 assert(logger.backend.info.args[0][0].includes('******'));
98 });
99
100 it('logs http client requests', function () {
101 const incomingMessage = new http.IncomingMessage();
102 incomingMessage.method = 'GET';
103 incomingMessage.url = new URL('http://example.com/');
104 logger.info(scope, message, { incomingMessage });
105 assert(logger.backend.info.called);
106 assert(logger.backend.info.args[0][0].includes('GET'));
107 assert(logger.backend.info.args[0][0].includes('http://example.com/'));
108 });
109
110 it('logs http client requests with scrubbed auth', function () {
111 const incomingMessage = Object.create(http.IncomingMessage.prototype);
112 incomingMessage.headers = {
113 authorization: 'Basic eW8=',
114 };
115 logger.info(scope, message, { incomingMessage });
116 assert(logger.backend.info.called);
117 assert(logger.backend.info.args[0][0].includes('****'));
118 });
119
120 it('logs http server responses', function () {
121 const serverResponse = Object.create(http.ServerResponse.prototype);
122 logger.info(scope, message, { serverResponse });
123 assert(logger.backend.info.args[0][0].includes('"statusCode":200'));
124 });
125
126 it('follows expected level ordering', function () {
127 const levels = Object.keys(Logger.nullLogger);
128 const expected = ['error', 'warn', 'info', 'log', 'debug'];
129 assert.deepStrictEqual(levels, expected);
130 });
131
132 it('logs async storage data', async function () {
133 logger = new Logger(config, commonObject, asyncLocalStorage);
134 const data = { foo: 'bar' };
135 asyncLocalStorage.run(data, async () => {
136 logger.info(scope, message, { baz: 3 });
137 });
138 assert(logger.backend.info.called);
139 assert(logger.backend.info.args[0][0].includes('"foo":"bar"'));
140 });
141
142 it('covers no async storage', function () {
143 logger = new Logger(config);
144 const data = { foo: 'bar' };
145 asyncLocalStorage.run(data, async () => {
146 logger.info(scope, message, { baz: 3 });
147 });
148 assert(logger.backend.info.called);
149 assert(!logger.backend.info.args[0][0].includes('"foo":"bar"'));
150 });
151 }); // Logger