@prosopo/logger 1.0.4 → 2.0.1

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.
@@ -12,8 +12,24 @@
12
12
  // See the License for the specific language governing permissions and
13
13
  // limitations under the License.
14
14
 
15
+ import { stringifyBigInts } from "@prosopo/util";
15
16
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
16
- import { getLogger, stringifyBigInts } from "../logger.js";
17
+ import {
18
+ getLogger,
19
+ parseDirectives,
20
+ parseLogLevel,
21
+ resolveLevel,
22
+ setGlobalDirectives,
23
+ } from "../logger.js";
24
+
25
+ // print() re-resolves PROSOPO_LOG_LEVEL directives at print time, so a stray
26
+ // env value (e.g. PROSOPO_LOG_LEVEL=fatal) would otherwise suppress output that
27
+ // these tests expect. Reset directives before every test to keep the suite
28
+ // deterministic regardless of the developer's environment.
29
+ beforeEach(() => {
30
+ setGlobalDirectives("");
31
+ });
32
+
17
33
  describe("unpackError", () => {
18
34
  let captured: string[] = [];
19
35
 
@@ -79,7 +95,7 @@ describe("unpackError", () => {
79
95
  });
80
96
 
81
97
  describe("stringifyBigInts", () => {
82
- it("should strigify big int", () => {
98
+ it("should stringify big int", () => {
83
99
  const bigInt = BigInt(12345678901234567890n);
84
100
 
85
101
  const stringified = stringifyBigInts(bigInt);
@@ -132,3 +148,253 @@ describe("stringifyBigInts", () => {
132
148
  ]);
133
149
  });
134
150
  });
151
+
152
+ describe("parseLogLevel", () => {
153
+ it("parses a bare level", () => {
154
+ expect(parseLogLevel("warn")).toBe("warn");
155
+ });
156
+
157
+ it("extracts the global level from a directive string", () => {
158
+ expect(parseLogLevel("warn,database=trace")).toBe("warn");
159
+ });
160
+
161
+ it("returns the fallback for a directives-only string", () => {
162
+ expect(parseLogLevel("database=trace", "info")).toBe("info");
163
+ });
164
+
165
+ it("returns the fallback for undefined", () => {
166
+ expect(parseLogLevel(undefined, "info")).toBe("info");
167
+ });
168
+ });
169
+
170
+ describe("parseDirectives", () => {
171
+ it("parses a bare level as the global default (empty-string key)", () => {
172
+ const directives = parseDirectives("debug");
173
+ expect(directives.get("")).toBe("debug");
174
+ });
175
+
176
+ it("parses scope=level pairs", () => {
177
+ const directives = parseDirectives("provider:db=debug,provider:api=trace");
178
+ expect(directives.get("provider:db")).toBe("debug");
179
+ expect(directives.get("provider:api")).toBe("trace");
180
+ });
181
+
182
+ it("ignores empty and invalid entries", () => {
183
+ const directives = parseDirectives(" , provider:db=debug , bogus=nope ");
184
+ expect(directives.get("provider:db")).toBe("debug");
185
+ expect(directives.has("bogus")).toBe(false);
186
+ });
187
+
188
+ it("ignores an empty-scope entry like '=debug' (only a bare level sets the global default)", () => {
189
+ const directives = parseDirectives("=debug");
190
+ expect(directives.has("")).toBe(false);
191
+ });
192
+
193
+ it("does not let an empty-scope entry override a real scope's level", () => {
194
+ const directives = parseDirectives("=debug,provider:db=warn");
195
+ expect(directives.has("")).toBe(false);
196
+ expect(directives.get("provider:db")).toBe("warn");
197
+ });
198
+
199
+ it("parses per-scope overrides", () => {
200
+ const d = parseDirectives("warn,database=trace");
201
+ expect(d.get("")).toBe("warn");
202
+ expect(d.get("database")).toBe("trace");
203
+ });
204
+
205
+ it("parses directives-only (no global default)", () => {
206
+ const d = parseDirectives("database=trace");
207
+ expect(d.has("")).toBe(false);
208
+ expect(d.get("database")).toBe("trace");
209
+ });
210
+ });
211
+
212
+ describe("resolveLevel", () => {
213
+ it("returns an exact scope match", () => {
214
+ const directives = parseDirectives("provider:db=debug");
215
+ expect(resolveLevel("provider:db", directives, "info")).toBe("debug");
216
+ });
217
+
218
+ it("returns the most specific matching prefix", () => {
219
+ const directives = parseDirectives("provider=warn,provider:db=debug");
220
+ expect(resolveLevel("provider:db:query", directives, "info")).toBe("debug");
221
+ });
222
+
223
+ it("falls back to the global default when no scope matches", () => {
224
+ const directives = parseDirectives("other=trace,error");
225
+ expect(resolveLevel("provider:db", directives, "info")).toBe("error");
226
+ });
227
+
228
+ it("falls back to the supplied fallback when nothing matches", () => {
229
+ const directives = parseDirectives("other=trace");
230
+ expect(resolveLevel("provider:db", directives, "info")).toBe("info");
231
+ });
232
+
233
+ it("matches the exact scope", () => {
234
+ const d = parseDirectives("warn,database:mongo=trace");
235
+ expect(resolveLevel("database:mongo", d, "info")).toBe("trace");
236
+ });
237
+
238
+ it("matches a prefix when exact scope has no entry", () => {
239
+ const d = parseDirectives("warn,database=debug");
240
+ expect(resolveLevel("database:mongo:queries", d, "info")).toBe("debug");
241
+ });
242
+
243
+ it("falls back to global default when no scope matches", () => {
244
+ const d = parseDirectives("warn,database=debug");
245
+ expect(resolveLevel("provider:request", d, "info")).toBe("warn");
246
+ });
247
+
248
+ it("returns fallback when directives are empty", () => {
249
+ const d = parseDirectives("");
250
+ expect(resolveLevel("any:scope", d, "info")).toBe("info");
251
+ });
252
+ });
253
+
254
+ describe("Logger.with subscope + directives", () => {
255
+ let captured: string[] = [];
256
+
257
+ beforeEach(() => {
258
+ captured = [];
259
+ const capture = (...args: unknown[]): void => {
260
+ captured.push(String(args[0]));
261
+ };
262
+ vi.spyOn(console, "debug").mockImplementation(capture);
263
+ vi.spyOn(console, "info").mockImplementation(capture);
264
+ });
265
+
266
+ afterEach(() => {
267
+ vi.restoreAllMocks();
268
+ setGlobalDirectives("");
269
+ });
270
+
271
+ it("concatenates parent scope with subscope", () => {
272
+ setGlobalDirectives("parent:child=debug");
273
+ const child = getLogger("info", "parent").with({}, "child");
274
+ child.debug(() => ({ msg: "hello" }));
275
+ expect(captured.length).toBe(1);
276
+ });
277
+
278
+ it("uses the subscope directly when the parent scope is empty (no leading colon)", () => {
279
+ setGlobalDirectives("admin=debug");
280
+ const child = getLogger("info", "").with({}, "admin");
281
+ child.debug(() => ({ msg: "hello" }));
282
+ // If the scope were ":admin" the "admin" directive would not match and the
283
+ // debug line would be suppressed.
284
+ expect(captured.length).toBe(1);
285
+ });
286
+
287
+ it("re-resolves directives at print time rather than baking them in (no sticky level)", () => {
288
+ setGlobalDirectives("admin=debug");
289
+ const child = getLogger("info", "").with({}, "admin");
290
+ // Directive removed after the child was created.
291
+ setGlobalDirectives("");
292
+ child.debug(() => ({ msg: "should be suppressed" }));
293
+ expect(captured.length).toBe(0);
294
+ // The inherited info level still applies.
295
+ child.info(() => ({ msg: "visible" }));
296
+ expect(captured.length).toBe(1);
297
+ });
298
+ });
299
+
300
+ describe("setGlobalDirectives", () => {
301
+ // Restore the env-derived directives so a failing assertion can't leak state
302
+ // into later tests, without clobbering a non-empty PROSOPO_LOG_LEVEL.
303
+ afterEach(() => {
304
+ setGlobalDirectives(process.env.PROSOPO_LOG_LEVEL ?? "");
305
+ });
306
+
307
+ it("affects an existing logger's filtering at emit time", () => {
308
+ const logger = getLogger("info", "test-scope");
309
+ const debugSpy = vi.spyOn(console, "debug").mockImplementation(() => {});
310
+ try {
311
+ // Globally warn: a debug message from a logger created at info is suppressed.
312
+ setGlobalDirectives("warn");
313
+ logger.debug(() => ({ msg: "hidden" }));
314
+ expect(debugSpy).not.toHaveBeenCalled();
315
+
316
+ // Raising test-scope to trace at runtime lets the same logger emit debug.
317
+ setGlobalDirectives("warn,test-scope=trace");
318
+ logger.debug(() => ({ msg: "shown" }));
319
+ expect(debugSpy).toHaveBeenCalledTimes(1);
320
+ } finally {
321
+ debugSpy.mockRestore();
322
+ }
323
+ });
324
+ });
325
+
326
+ describe("emitted record", () => {
327
+ afterEach(() => {
328
+ setGlobalDirectives(process.env.PROSOPO_LOG_LEVEL ?? "");
329
+ });
330
+
331
+ it("uses the message level rather than the logger's configured level", () => {
332
+ setGlobalDirectives("trace");
333
+ const logger = getLogger("info", "record-scope");
334
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
335
+ try {
336
+ logger.error(() => ({ msg: "boom" }));
337
+ expect(errorSpy).toHaveBeenCalledTimes(1);
338
+ const output = errorSpy.mock.calls[0]?.[0];
339
+ expect(typeof output).toBe("string");
340
+ const record: { level: string; scope: string; msg: string } = JSON.parse(
341
+ output as string,
342
+ );
343
+ expect(record.level).toBe("error");
344
+ expect(record.scope).toBe("record-scope");
345
+ expect(record.msg).toBe("boom");
346
+ } finally {
347
+ errorSpy.mockRestore();
348
+ }
349
+ });
350
+ });
351
+
352
+ describe("Logger.with subscope", () => {
353
+ it("appends subscope to parent scope", () => {
354
+ const parent = getLogger("info", "provider");
355
+ const child = parent.with({}, "request");
356
+ expect(child.getScope()).toBe("provider:request");
357
+ });
358
+
359
+ it("does not produce a leading colon when parent scope is empty", () => {
360
+ const parent = getLogger("info", "");
361
+ const child = parent.with({}, "request");
362
+ expect(child.getScope()).toBe("request");
363
+ });
364
+
365
+ it("trims surrounding whitespace from the subscope", () => {
366
+ const parent = getLogger("info", "provider");
367
+ const child = parent.with({}, " request ");
368
+ expect(child.getScope()).toBe("provider:request");
369
+ });
370
+
371
+ it("treats a whitespace-only subscope as absent", () => {
372
+ const parent = getLogger("info", "provider");
373
+ const child = parent.with({}, " ");
374
+ expect(child.getScope()).toBe("provider");
375
+ });
376
+
377
+ it("merges parent and child default data into emitted records", () => {
378
+ setGlobalDirectives("trace");
379
+ const parent = getLogger("info", "test").with({ requestId: "abc" });
380
+ const child = parent.with({ extra: 1 });
381
+ const infoSpy = vi.spyOn(console, "info").mockImplementation(() => {});
382
+ try {
383
+ child.info(() => ({ data: { perCall: true } }));
384
+ expect(infoSpy).toHaveBeenCalledTimes(1);
385
+ const output = infoSpy.mock.calls[0]?.[0];
386
+ expect(typeof output).toBe("string");
387
+ const record: { scope: string; data: Record<string, unknown> } =
388
+ JSON.parse(output as string);
389
+ expect(record.scope).toBe("test");
390
+ expect(record.data).toMatchObject({
391
+ requestId: "abc",
392
+ extra: 1,
393
+ perCall: true,
394
+ });
395
+ } finally {
396
+ infoSpy.mockRestore();
397
+ setGlobalDirectives(process.env.PROSOPO_LOG_LEVEL ?? "");
398
+ }
399
+ });
400
+ });