change replacer function signatures to match that of stringify, detect circular refer...
[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 const logWrapper = process.env['VERBOSE_TESTS'] ? sinon.spy : sinon.stub;
21 Object.keys(Logger.nullLogger).forEach((level) => logWrapper(logger.backend, level));
22 scope = 'testScope';
23 message = 'message';
24 });
25 this.afterEach(function () {
26 sinon.restore();
27 });
28
29 it('logs a message', function () {
30 logger.info(scope, message, { baz: 'quux' }, { foo: 1 }, 'more other');
31 assert(logger.backend.info.called);
32 assert(logger.backend.info.args[0][0].includes(message));
33 });
34
35 it('stubs missing levels', function () {
36 const backend = {
37 debug: () => {},
38 }
39 logger = new Logger(config, commonObject, undefined, backend);
40 assert.strictEqual(typeof logger.info, 'function');
41 });
42
43 it('logs BigInts', function () {
44 logger.info(scope, message, { aBigInteger: BigInt(2) });
45 assert(logger.backend.info.called);
46 assert(logger.backend.info.args[0][0].includes('"2"'));
47 });
48
49 it('logs Errors', function () {
50 logger.error(scope, message, { e: new Error('an error') });
51 assert(logger.backend.error.called);
52 assert(logger.backend.error.args[0][0].includes('an error'), logger.backend.error.args[0][0]);
53 });
54
55 it('covers config settings', function () {
56 config.ignoreBelowLevel = 'info';
57 logger = new Logger(config);
58 logger.debug(scope, message, {});
59 assert(logger.backend.debug.notCalled);
60 });
61
62 it('covers config error', function () {
63 config.ignoreBelowLevel = 'not a level';
64 try {
65 logger = new Logger(config);
66 assert.fail('expected RangeError here');
67 } catch (e) {
68 assert(e instanceof RangeError);
69 }
70 });
71
72 it('covers empty fields', function () {
73 logger.info();
74 assert(logger.backend.info.called);
75 assert(logger.backend.info.args[0][0].includes('[unknown]'));
76 });
77
78 it('sanitizes', function () {
79 logger.dataSanitizers.push((data, sanitize = true) => {
80 let unclean = false;
81 const credentialLength = data?.ctx?.parsedBody?.credential?.length;
82 if (credentialLength) {
83 unclean = true;
84 }
85 if (unclean && sanitize) {
86 data.ctx.parsedBody.credential = '*'.repeat(credentialLength);
87 }
88 return unclean;
89 });
90 logger.info(scope, message, {
91 ctx: {
92 parsedBody: {
93 credential: 'secret',
94 },
95 },
96 });
97 assert(logger.backend.info.called);
98 assert(logger.backend.info.args[0][0].includes('******'));
99 });
100
101 it('logs http client requests', function () {
102 const incomingMessage = new http.IncomingMessage();
103 incomingMessage.method = 'GET';
104 incomingMessage.url = new URL('http://example.com/');
105 logger.info(scope, message, { incomingMessage });
106 assert(logger.backend.info.called);
107 assert(logger.backend.info.args[0][0].includes('GET'));
108 assert(logger.backend.info.args[0][0].includes('http://example.com/'));
109 });
110
111 it('logs http client requests with scrubbed auth', function () {
112 const incomingMessage = Object.create(http.IncomingMessage.prototype);
113 incomingMessage.headers = {
114 authorization: 'Basic eW8=',
115 };
116 logger.info(scope, message, { incomingMessage });
117 assert(logger.backend.info.called);
118 assert(logger.backend.info.args[0][0].includes('****'));
119 });
120
121 it('logs http server responses', function () {
122 const serverResponse = Object.create(http.ServerResponse.prototype);
123 logger.info(scope, message, { serverResponse });
124 assert(logger.backend.info.args[0][0].includes('"statusCode":200'));
125 });
126
127 it('follows expected level ordering', function () {
128 const levels = Object.keys(Logger.nullLogger);
129 const expected = ['error', 'warn', 'info', 'log', 'debug'];
130 assert.deepStrictEqual(levels, expected);
131 });
132
133 it('logs async storage data', async function () {
134 logger = new Logger(config, commonObject, asyncLocalStorage);
135 const data = { foo: 'bar' };
136 asyncLocalStorage.run(data, async () => {
137 logger.info(scope, message, { baz: 3 });
138 });
139 assert(logger.backend.info.called);
140 assert(logger.backend.info.args[0][0].includes('"foo":"bar"'));
141 });
142
143 it('covers no async storage', function () {
144 logger = new Logger(config);
145 const data = { foo: 'bar' };
146 asyncLocalStorage.run(data, async () => {
147 logger.info(scope, message, { baz: 3 });
148 });
149 assert(logger.backend.info.called);
150 assert(!logger.backend.info.args[0][0].includes('"foo":"bar"'));
151 });
152
153 it('covers circular objects', function () {
154 const data = { foo: 'bar' };
155 data.self = data;
156 logger.info(scope, message, data);
157 assert(logger.backend.info.called);
158 assert(logger.backend.info.args[0][0].includes('[Circular]'));
159 });
160 }); // Logger