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