@prosopo/logger 2.0.3 → 2.0.5

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.
@@ -0,0 +1,132 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi, } from "vitest";
2
+ import { NativeLogger, getLogger, setGlobalDirectives } from "../logger.js";
3
+ const parseLast = (spy) => {
4
+ const calls = spy.mock.calls;
5
+ const last = calls[calls.length - 1];
6
+ return JSON.parse(String(last?.[0]));
7
+ };
8
+ let info;
9
+ let debug;
10
+ let trace;
11
+ let warn;
12
+ let error;
13
+ beforeEach(() => {
14
+ setGlobalDirectives("");
15
+ info = vi.fn();
16
+ debug = vi.fn();
17
+ trace = vi.fn();
18
+ warn = vi.fn();
19
+ error = vi.fn();
20
+ vi.spyOn(console, "info").mockImplementation(info);
21
+ vi.spyOn(console, "debug").mockImplementation(debug);
22
+ vi.spyOn(console, "trace").mockImplementation(trace);
23
+ vi.spyOn(console, "warn").mockImplementation(warn);
24
+ vi.spyOn(console, "error").mockImplementation(error);
25
+ });
26
+ afterEach(() => {
27
+ vi.restoreAllMocks();
28
+ });
29
+ describe("NativeLogger format", () => {
30
+ it("accepts the json format and reports it back", () => {
31
+ const logger = new NativeLogger("scope");
32
+ logger.setFormat("json");
33
+ expect(logger.getFormat()).toBe("json");
34
+ });
35
+ it("rejects the plain format, which is not implemented", () => {
36
+ const logger = new NativeLogger("scope");
37
+ expect(() => logger.setFormat("plain")).toThrow("Only JSON format implemented for now");
38
+ });
39
+ });
40
+ describe("NativeLogger.log level dispatch", () => {
41
+ const cases = [
42
+ { level: "trace", dest: () => trace },
43
+ { level: "debug", dest: () => debug },
44
+ { level: "info", dest: () => info },
45
+ { level: "warn", dest: () => warn },
46
+ { level: "error", dest: () => error },
47
+ { level: "fatal", dest: () => error },
48
+ ];
49
+ for (const { level, dest } of cases) {
50
+ it(`routes ${level} to its own console method`, () => {
51
+ const logger = getLogger("trace", "scope");
52
+ logger.log(level, () => ({ msg: `${level} message` }));
53
+ expect(parseLast(dest()).msg).toBe(`${level} message`);
54
+ });
55
+ }
56
+ it("throws on an unknown log level", () => {
57
+ const logger = getLogger("trace", "scope");
58
+ expect(() => logger.log("verbose", () => ({ msg: "x" }))).toThrow("Unknown log level: verbose");
59
+ });
60
+ });
61
+ describe("NativeLogger direct level methods", () => {
62
+ it("emits trace records at the trace threshold", () => {
63
+ const logger = getLogger("trace", "scope");
64
+ logger.trace(() => ({ msg: "traced" }));
65
+ expect(parseLast(trace)).toMatchObject({ msg: "traced", level: "trace" });
66
+ });
67
+ it("emits warn records", () => {
68
+ const logger = getLogger("info", "scope");
69
+ logger.warn(() => ({ msg: "warned" }));
70
+ expect(parseLast(warn)).toMatchObject({ msg: "warned", level: "warn" });
71
+ });
72
+ it("emits fatal records via console.error", () => {
73
+ const logger = getLogger("info", "scope");
74
+ logger.fatal(() => ({ msg: "fatal" }));
75
+ expect(parseLast(error)).toMatchObject({ msg: "fatal", level: "fatal" });
76
+ });
77
+ it("suppresses a record below the threshold", () => {
78
+ const logger = getLogger("warn", "scope");
79
+ logger.debug(() => ({ msg: "hidden" }));
80
+ expect(debug).not.toHaveBeenCalled();
81
+ });
82
+ });
83
+ describe("error unpacking edge cases", () => {
84
+ it("passes a primitive cause through unchanged", () => {
85
+ const logger = getLogger("info", "scope");
86
+ const err = new Error("boom");
87
+ err.cause = "a string cause";
88
+ logger.error(() => ({ err }));
89
+ expect(parseLast(error).errData).toMatchObject({ cause: "a string cause" });
90
+ });
91
+ it("keeps a null cause out of the unpacked object", () => {
92
+ const logger = getLogger("info", "scope");
93
+ const err = new Error("boom");
94
+ err.cause = null;
95
+ logger.error(() => ({ err }));
96
+ expect(parseLast(error).errData).toBeDefined();
97
+ });
98
+ it("stringifies a primitive err field", () => {
99
+ const logger = getLogger("info", "scope");
100
+ logger.error(() => ({ err: 42 }));
101
+ expect(parseLast(error).err).toBe("42");
102
+ });
103
+ it("stringifies a string err field", () => {
104
+ const logger = getLogger("info", "scope");
105
+ logger.error(() => ({ err: "plain failure" }));
106
+ expect(parseLast(error).err).toBe("plain failure");
107
+ });
108
+ });
109
+ describe("Logger.with default data reuse", () => {
110
+ it("reuses the parent default data when the child adds none", () => {
111
+ const parent = getLogger("info", "scope").with({ requestId: "r1" });
112
+ const child = parent.with({}, "sub");
113
+ child.info(() => ({ msg: "hello" }));
114
+ expect(parseLast(info)).toMatchObject({
115
+ msg: "hello",
116
+ req_id: "r1",
117
+ data: { requestId: "r1" },
118
+ scope: "scope:sub",
119
+ });
120
+ });
121
+ it("omits default data entirely when neither side supplies any", () => {
122
+ const child = getLogger("info", "scope").with({}, "sub");
123
+ child.info(() => ({ msg: "hello" }));
124
+ expect(parseLast(info)).toMatchObject({ msg: "hello", scope: "scope:sub" });
125
+ });
126
+ it("merges the child data over the parent data", () => {
127
+ const parent = getLogger("info", "scope").with({ a: 1, b: 2 });
128
+ parent.with({ b: 3 }).info(() => ({ msg: "hello" }));
129
+ expect(parseLast(info)).toMatchObject({ data: { a: 1, b: 3 } });
130
+ });
131
+ });
132
+ //# sourceMappingURL=loggerLevels.unit.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loggerLevels.unit.test.js","sourceRoot":"","sources":["../../src/tests/loggerLevels.unit.test.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prosopo/logger",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "description": "Prosopo logger library",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -28,22 +28,23 @@
28
28
  "author": "Prosopo Limited",
29
29
  "license": "Apache-2.0",
30
30
  "dependencies": {
31
- "@prosopo/util": "3.3.4",
31
+ "@prosopo/util": "3.3.5",
32
32
  "zod": "3.23.8"
33
33
  },
34
34
  "devDependencies": {
35
- "@prosopo/config": "3.3.4",
35
+ "@prosopo/config": "3.3.7",
36
36
  "@types/node": "22.10.2",
37
- "@vitest/coverage-v8": "3.2.4",
37
+ "@vitest/coverage-v8": "4.1.10",
38
38
  "concurrently": "9.0.1",
39
39
  "del-cli": "6.0.0",
40
40
  "dotenv": "16.4.5",
41
+ "jsdom": "25.0.0",
41
42
  "npm-run-all": "4.1.5",
42
43
  "tslib": "2.7.0",
43
44
  "tsx": "4.20.3",
44
45
  "typescript": "5.6.2",
45
- "vite": "6.4.3",
46
- "vitest": "3.2.4"
46
+ "vite": "8.1.5",
47
+ "vitest": "4.1.10"
47
48
  },
48
49
  "repository": {
49
50
  "type": "git",
@@ -0,0 +1,85 @@
1
+ // Copyright 2021-2026 Prosopo (UK) Ltd.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ // @vitest-environment jsdom
16
+
17
+ import {
18
+ type Mock,
19
+ afterEach,
20
+ beforeEach,
21
+ describe,
22
+ expect,
23
+ it,
24
+ vi,
25
+ } from "vitest";
26
+ import type { LogRecord } from "../logger.js";
27
+ import { getLogger, setGlobalDirectives } from "../logger.js";
28
+
29
+ // `inBrowser` is resolved once at module load, so this whole file runs under
30
+ // jsdom to exercise the dev-tools branch of print() (objects are passed to the
31
+ // console verbatim rather than being serialised to JSON).
32
+
33
+ type ConsoleMock = Mock<(...args: unknown[]) => void>;
34
+
35
+ let info: ConsoleMock;
36
+ let error: ConsoleMock;
37
+
38
+ beforeEach(() => {
39
+ setGlobalDirectives("");
40
+ info = vi.fn<(...args: unknown[]) => void>();
41
+ error = vi.fn<(...args: unknown[]) => void>();
42
+ vi.spyOn(console, "info").mockImplementation(info);
43
+ vi.spyOn(console, "error").mockImplementation(error);
44
+ });
45
+
46
+ afterEach(() => {
47
+ vi.restoreAllMocks();
48
+ });
49
+
50
+ describe("NativeLogger in a browser", () => {
51
+ it("logs the message and the record separately so dev tools can expand it", () => {
52
+ getLogger("info", "scope").info((): LogRecord => ({ msg: "hello" }));
53
+
54
+ expect(info).toHaveBeenCalledWith(
55
+ "hello",
56
+ expect.objectContaining({ msg: "hello", scope: "scope", level: "info" }),
57
+ );
58
+ });
59
+
60
+ it("falls back to the error message when there is no msg", () => {
61
+ getLogger("info", "scope").error(
62
+ (): LogRecord => ({ err: new Error("boom") }),
63
+ );
64
+
65
+ expect(error).toHaveBeenCalledWith(
66
+ "boom",
67
+ expect.objectContaining({ err: "boom" }),
68
+ );
69
+ });
70
+
71
+ it("logs the bare record when there is neither a message nor an error", () => {
72
+ getLogger("info", "scope").info((): LogRecord => ({ data: { a: 1 } }));
73
+
74
+ expect(info).toHaveBeenCalledWith(
75
+ expect.objectContaining({ data: { a: 1 }, scope: "scope" }),
76
+ );
77
+ expect(info.mock.calls[0]).toHaveLength(1);
78
+ });
79
+
80
+ it("does not serialise the record to a string", () => {
81
+ getLogger("info", "scope").info((): LogRecord => ({ msg: "hello" }));
82
+
83
+ expect(typeof info.mock.calls[0]?.[1]).toBe("object");
84
+ });
85
+ });
@@ -0,0 +1,215 @@
1
+ // Copyright 2021-2026 Prosopo (UK) Ltd.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ import {
16
+ type Mock,
17
+ afterEach,
18
+ beforeEach,
19
+ describe,
20
+ expect,
21
+ it,
22
+ vi,
23
+ } from "vitest";
24
+ import type { LogLevel, LogRecord } from "../logger.js";
25
+ import { NativeLogger, getLogger, setGlobalDirectives } from "../logger.js";
26
+
27
+ type Record_ = Record<string, unknown>;
28
+
29
+ type ConsoleMock = Mock<(...args: unknown[]) => void>;
30
+
31
+ const parseLast = (spy: ConsoleMock): Record_ => {
32
+ const calls = spy.mock.calls;
33
+ const last = calls[calls.length - 1];
34
+ return JSON.parse(String(last?.[0])) as Record_;
35
+ };
36
+
37
+ let info: ConsoleMock;
38
+ let debug: ConsoleMock;
39
+ let trace: ConsoleMock;
40
+ let warn: ConsoleMock;
41
+ let error: ConsoleMock;
42
+
43
+ beforeEach(() => {
44
+ setGlobalDirectives("");
45
+ info = vi.fn<(...args: unknown[]) => void>();
46
+ debug = vi.fn<(...args: unknown[]) => void>();
47
+ trace = vi.fn<(...args: unknown[]) => void>();
48
+ warn = vi.fn<(...args: unknown[]) => void>();
49
+ error = vi.fn<(...args: unknown[]) => void>();
50
+ vi.spyOn(console, "info").mockImplementation(info);
51
+ vi.spyOn(console, "debug").mockImplementation(debug);
52
+ vi.spyOn(console, "trace").mockImplementation(trace);
53
+ vi.spyOn(console, "warn").mockImplementation(warn);
54
+ vi.spyOn(console, "error").mockImplementation(error);
55
+ });
56
+
57
+ afterEach(() => {
58
+ vi.restoreAllMocks();
59
+ });
60
+
61
+ describe("NativeLogger format", () => {
62
+ it("accepts the json format and reports it back", () => {
63
+ const logger = new NativeLogger("scope");
64
+
65
+ logger.setFormat("json");
66
+
67
+ expect(logger.getFormat()).toBe("json");
68
+ });
69
+
70
+ it("rejects the plain format, which is not implemented", () => {
71
+ const logger = new NativeLogger("scope");
72
+
73
+ expect(() => logger.setFormat("plain")).toThrow(
74
+ "Only JSON format implemented for now",
75
+ );
76
+ });
77
+ });
78
+
79
+ describe("NativeLogger.log level dispatch", () => {
80
+ const cases: Array<{
81
+ level: LogLevel;
82
+ dest: () => ConsoleMock;
83
+ }> = [
84
+ { level: "trace", dest: (): ConsoleMock => trace },
85
+ { level: "debug", dest: (): ConsoleMock => debug },
86
+ { level: "info", dest: (): ConsoleMock => info },
87
+ { level: "warn", dest: (): ConsoleMock => warn },
88
+ { level: "error", dest: (): ConsoleMock => error },
89
+ { level: "fatal", dest: (): ConsoleMock => error },
90
+ ];
91
+
92
+ for (const { level, dest } of cases) {
93
+ it(`routes ${level} to its own console method`, () => {
94
+ const logger = getLogger("trace", "scope");
95
+
96
+ logger.log(level, (): LogRecord => ({ msg: `${level} message` }));
97
+
98
+ expect(parseLast(dest()).msg).toBe(`${level} message`);
99
+ });
100
+ }
101
+
102
+ it("throws on an unknown log level", () => {
103
+ const logger = getLogger("trace", "scope");
104
+
105
+ expect(() =>
106
+ // the switch guards against a level smuggled past the type system
107
+ logger.log("verbose" as LogLevel, (): LogRecord => ({ msg: "x" })),
108
+ ).toThrow("Unknown log level: verbose");
109
+ });
110
+ });
111
+
112
+ describe("NativeLogger direct level methods", () => {
113
+ it("emits trace records at the trace threshold", () => {
114
+ const logger = getLogger("trace", "scope");
115
+
116
+ logger.trace((): LogRecord => ({ msg: "traced" }));
117
+
118
+ expect(parseLast(trace)).toMatchObject({ msg: "traced", level: "trace" });
119
+ });
120
+
121
+ it("emits warn records", () => {
122
+ const logger = getLogger("info", "scope");
123
+
124
+ logger.warn((): LogRecord => ({ msg: "warned" }));
125
+
126
+ expect(parseLast(warn)).toMatchObject({ msg: "warned", level: "warn" });
127
+ });
128
+
129
+ it("emits fatal records via console.error", () => {
130
+ const logger = getLogger("info", "scope");
131
+
132
+ logger.fatal((): LogRecord => ({ msg: "fatal" }));
133
+
134
+ expect(parseLast(error)).toMatchObject({ msg: "fatal", level: "fatal" });
135
+ });
136
+
137
+ it("suppresses a record below the threshold", () => {
138
+ const logger = getLogger("warn", "scope");
139
+
140
+ logger.debug((): LogRecord => ({ msg: "hidden" }));
141
+
142
+ expect(debug).not.toHaveBeenCalled();
143
+ });
144
+ });
145
+
146
+ describe("error unpacking edge cases", () => {
147
+ it("passes a primitive cause through unchanged", () => {
148
+ const logger = getLogger("info", "scope");
149
+ const err: Error & { cause?: unknown } = new Error("boom");
150
+ err.cause = "a string cause";
151
+
152
+ logger.error((): LogRecord => ({ err }));
153
+
154
+ expect(parseLast(error).errData).toMatchObject({ cause: "a string cause" });
155
+ });
156
+
157
+ it("keeps a null cause out of the unpacked object", () => {
158
+ const logger = getLogger("info", "scope");
159
+ const err: Error & { cause?: unknown } = new Error("boom");
160
+ err.cause = null;
161
+
162
+ logger.error((): LogRecord => ({ err }));
163
+
164
+ // null is an object, so it takes the object branch and unpacks to nothing
165
+ expect(parseLast(error).errData).toBeDefined();
166
+ });
167
+
168
+ it("stringifies a primitive err field", () => {
169
+ const logger = getLogger("info", "scope");
170
+
171
+ logger.error((): LogRecord => ({ err: 42 }));
172
+
173
+ expect(parseLast(error).err).toBe("42");
174
+ });
175
+
176
+ it("stringifies a string err field", () => {
177
+ const logger = getLogger("info", "scope");
178
+
179
+ logger.error((): LogRecord => ({ err: "plain failure" }));
180
+
181
+ expect(parseLast(error).err).toBe("plain failure");
182
+ });
183
+ });
184
+
185
+ describe("Logger.with default data reuse", () => {
186
+ it("reuses the parent default data when the child adds none", () => {
187
+ const parent = getLogger("info", "scope").with({ requestId: "r1" });
188
+
189
+ const child = parent.with({}, "sub");
190
+ child.info((): LogRecord => ({ msg: "hello" }));
191
+
192
+ expect(parseLast(info)).toMatchObject({
193
+ msg: "hello",
194
+ req_id: "r1",
195
+ data: { requestId: "r1" },
196
+ scope: "scope:sub",
197
+ });
198
+ });
199
+
200
+ it("omits default data entirely when neither side supplies any", () => {
201
+ const child = getLogger("info", "scope").with({}, "sub");
202
+
203
+ child.info((): LogRecord => ({ msg: "hello" }));
204
+
205
+ expect(parseLast(info)).toMatchObject({ msg: "hello", scope: "scope:sub" });
206
+ });
207
+
208
+ it("merges the child data over the parent data", () => {
209
+ const parent = getLogger("info", "scope").with({ a: 1, b: 2 });
210
+
211
+ parent.with({ b: 3 }).info((): LogRecord => ({ msg: "hello" }));
212
+
213
+ expect(parseLast(info)).toMatchObject({ data: { a: 1, b: 3 } });
214
+ });
215
+ });