@warlock.js/logger 5.24.0 → 5.26.0
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.
- package/CHANGELOG.md +12 -0
- package/README.md +153 -153
- package/cjs/index.cjs.map +1 -1
- package/esm/channels/console-log.mjs.map +1 -1
- package/esm/channels/file-log.mjs.map +1 -1
- package/esm/channels/json-file-log.mjs.map +1 -1
- package/esm/channels/sentry-log.mjs.map +1 -1
- package/esm/log-channel.mjs.map +1 -1
- package/esm/logger.mjs.map +1 -1
- package/esm/utils/capture-unhandled-errors.mjs.map +1 -1
- package/esm/utils/clear-message.mjs.map +1 -1
- package/llms-full.txt +1413 -1413
- package/package.json +2 -2
- package/skills/capture-unhandled-errors/SKILL.md +111 -111
- package/skills/configure-logger/SKILL.md +105 -105
- package/skills/filter-log-entries/SKILL.md +120 -120
- package/skills/flush-logs-on-shutdown/SKILL.md +117 -117
- package/skills/overview/SKILL.md +93 -93
- package/skills/pick-log-channel/SKILL.md +155 -155
- package/skills/redact-sensitive-log-fields/SKILL.md +156 -156
- package/skills/ship-logs-to-sentry/SKILL.md +118 -118
- package/skills/test-logging-code/SKILL.md +177 -177
- package/skills/use-log-helpers/SKILL.md +66 -66
- package/skills/write-custom-log-channel/SKILL.md +190 -190
|
@@ -1,177 +1,177 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: test-logging-code
|
|
3
|
-
description: 'Test code that touches the logger — silence globally via log.setChannels([]) in setupFiles, assert specific log lines via a capturing LogChannel subclass (prefer it over vi.spyOn — it asserts on delivered entries, not just method calls, and isolates the shared singleton cleanly). Triggers: `log.setChannels`, `LogChannel`, `LoggingData`, `Logger`, `log.channels`; "silence logger in vitest", "assert a log line was emitted", "capture log output in tests", "test code that logs"; typical import `import { log, Logger, LogChannel, type LoggingData } from "@warlock.js/logger"`. Skip: custom sinks — `@warlock.js/logger/write-custom-log-channel/SKILL.md`; filtering — `@warlock.js/logger/filter-log-entries/SKILL.md`; competing `vi.spyOn(console)`, `jest.spyOn`.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Testing — code that logs, and asserting on log output
|
|
7
|
-
|
|
8
|
-
Two scenarios: **silencing the logger during tests** (most common) and **asserting that a specific log line was emitted**.
|
|
9
|
-
|
|
10
|
-
## Silence the logger during tests
|
|
11
|
-
|
|
12
|
-
Clear every channel once, globally. No output, no file handles, no noise.
|
|
13
|
-
|
|
14
|
-
```ts title="src/setupTests.ts"
|
|
15
|
-
import { log } from "@warlock.js/logger";
|
|
16
|
-
|
|
17
|
-
log.setChannels([]);
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
Wire it in Vitest:
|
|
21
|
-
|
|
22
|
-
```ts title="vitest.config.ts"
|
|
23
|
-
import { defineConfig } from "vitest/config";
|
|
24
|
-
|
|
25
|
-
export default defineConfig({
|
|
26
|
-
test: {
|
|
27
|
-
setupFiles: ["src/setupTests.ts"],
|
|
28
|
-
},
|
|
29
|
-
});
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## Assert on log output — use a capturing channel
|
|
33
|
-
|
|
34
|
-
Don't spy on `console.log` and don't mock `log.info` — assert on what a channel actually received instead (see "Why not spy on `log.info`?" below). The cleanest pattern is a tiny channel that records what it sees:
|
|
35
|
-
|
|
36
|
-
```ts
|
|
37
|
-
import { LogChannel } from "@warlock.js/logger";
|
|
38
|
-
import type { LoggingData } from "@warlock.js/logger";
|
|
39
|
-
|
|
40
|
-
class CapturingChannel extends LogChannel {
|
|
41
|
-
public name = "capture";
|
|
42
|
-
public terminal = false;
|
|
43
|
-
public received: LoggingData[] = [];
|
|
44
|
-
public log(data: LoggingData) { this.received.push({ ...data }); }
|
|
45
|
-
}
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
### Test against the singleton
|
|
49
|
-
|
|
50
|
-
```ts
|
|
51
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
52
|
-
import { log } from "@warlock.js/logger";
|
|
53
|
-
import { createUser } from "./users";
|
|
54
|
-
|
|
55
|
-
describe("createUser", () => {
|
|
56
|
-
let capture: CapturingChannel;
|
|
57
|
-
let originalChannels: typeof log.channels;
|
|
58
|
-
|
|
59
|
-
beforeEach(() => {
|
|
60
|
-
capture = new CapturingChannel();
|
|
61
|
-
originalChannels = log.channels;
|
|
62
|
-
log.channels = [capture];
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
afterEach(() => {
|
|
66
|
-
log.channels = originalChannels;
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("logs a success entry when the user is created", async () => {
|
|
70
|
-
await createUser({ email: "a@b.com" });
|
|
71
|
-
|
|
72
|
-
expect(capture.received).toContainEqual(
|
|
73
|
-
expect.objectContaining({
|
|
74
|
-
type: "success",
|
|
75
|
-
module: "users",
|
|
76
|
-
action: "create",
|
|
77
|
-
}),
|
|
78
|
-
);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
### Test an isolated logger (avoid touching the singleton)
|
|
84
|
-
|
|
85
|
-
If the code under test accepts a logger via injection, create one per test:
|
|
86
|
-
|
|
87
|
-
```ts
|
|
88
|
-
import { Logger } from "@warlock.js/logger";
|
|
89
|
-
|
|
90
|
-
const testLogger = new Logger();
|
|
91
|
-
const capture = new CapturingChannel();
|
|
92
|
-
testLogger.addChannel(capture);
|
|
93
|
-
|
|
94
|
-
await createUser({ email: "a@b.com" }, testLogger);
|
|
95
|
-
|
|
96
|
-
expect(capture.received[0]!.type).toBe("success");
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
No cleanup needed — the local `Logger` is garbage-collected.
|
|
100
|
-
|
|
101
|
-
## Why not spy on `log.info`?
|
|
102
|
-
|
|
103
|
-
`log` is a plain `Logger` instance (`export const log = new Logger()`) and every level method lives on the prototype, so `vi.spyOn(log, "info")` *does* technically work. Prefer the capturing channel anyway:
|
|
104
|
-
|
|
105
|
-
- A spy on `log.info` proves the method was **called**, not that an entry was **delivered** — it skips the whole pipeline (`minLevel` floor, redaction, per-channel `levels` / `filter`). A capturing channel asserts on the entry your code under test actually produced after all of that ran.
|
|
106
|
-
- The `log` singleton is shared global state. A spy you forget to `mockRestore()` leaks into the next test; swapping `log.channels` and restoring it in `afterEach` is the same amount of code and isolates cleanly.
|
|
107
|
-
- Code that logs through `log.error(...)` and the bare object form `log.log({ type, ... })` both land in channels, but only the level shortcut goes through `log.info` — a channel catches both.
|
|
108
|
-
|
|
109
|
-
So capture through a channel as shown above; reach for a method spy only when you specifically want to assert "this exact shortcut was invoked".
|
|
110
|
-
|
|
111
|
-
## Testing a custom channel
|
|
112
|
-
|
|
113
|
-
Write specs against the channel directly; don't route through `Logger`:
|
|
114
|
-
|
|
115
|
-
```ts
|
|
116
|
-
import { describe, it, expect, vi } from "vitest";
|
|
117
|
-
import { SlackLog } from "./slack-log";
|
|
118
|
-
|
|
119
|
-
describe("SlackLog", () => {
|
|
120
|
-
it("skips non-error levels", async () => {
|
|
121
|
-
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response());
|
|
122
|
-
|
|
123
|
-
const channel = new SlackLog({ webhookUrl: "https://test", levels: ["error"] });
|
|
124
|
-
await channel.log({ type: "info", module: "x", action: "y", message: "z" });
|
|
125
|
-
|
|
126
|
-
expect(fetchSpy).not.toHaveBeenCalled();
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
## Testing `FileLog` and `JSONFileLog`
|
|
132
|
-
|
|
133
|
-
Use real temp directories — it's the only way to exercise file IO, rotation, chunking, and JSON I/O with fidelity:
|
|
134
|
-
|
|
135
|
-
```ts
|
|
136
|
-
import fs from "fs";
|
|
137
|
-
import os from "os";
|
|
138
|
-
import path from "path";
|
|
139
|
-
import { randomUUID } from "node:crypto";
|
|
140
|
-
|
|
141
|
-
function tempDir() {
|
|
142
|
-
const dir = path.join(os.tmpdir(), "logger-test", randomUUID());
|
|
143
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
144
|
-
return dir;
|
|
145
|
-
}
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
Clean up in `afterEach(() => fs.rmSync(dir, { recursive: true, force: true }))`.
|
|
149
|
-
|
|
150
|
-
## Waiting for async init
|
|
151
|
-
|
|
152
|
-
`LogChannel.init()` runs inside a `setTimeout(0)`. Before asserting on post-init behavior, yield once:
|
|
153
|
-
|
|
154
|
-
```ts
|
|
155
|
-
const channel = new FileLog({ storagePath: tempDir() });
|
|
156
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
157
|
-
// Now `channel.isInitialized` is true and it's safe to call `channel.log(...)` for real I/O.
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
## Testing `captureAnyUnhandledRejection`
|
|
161
|
-
|
|
162
|
-
Don't actually throw unhandled rejections in tests — emit the listener directly:
|
|
163
|
-
|
|
164
|
-
```ts
|
|
165
|
-
captureAnyUnhandledRejection();
|
|
166
|
-
process.emit("unhandledRejection", new Error("test"), Promise.resolve());
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
The `uncaughtException` path additionally calls `process.exit(1)`, so stub it (or pass `{ exitOnUncaughtException: false }`) before emitting — otherwise the emitted exception tears the test runner down:
|
|
170
|
-
|
|
171
|
-
```ts
|
|
172
|
-
vi.spyOn(process, "exit").mockImplementation(() => undefined as never);
|
|
173
|
-
captureAnyUnhandledRejection();
|
|
174
|
-
process.emit("uncaughtException", new Error("test"), "uncaughtException");
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
See [`@warlock.js/logger/capture-unhandled-errors/SKILL.md`](@warlock.js/logger/capture-unhandled-errors/SKILL.md) for a full example.
|
|
1
|
+
---
|
|
2
|
+
name: test-logging-code
|
|
3
|
+
description: 'Test code that touches the logger — silence globally via log.setChannels([]) in setupFiles, assert specific log lines via a capturing LogChannel subclass (prefer it over vi.spyOn — it asserts on delivered entries, not just method calls, and isolates the shared singleton cleanly). Triggers: `log.setChannels`, `LogChannel`, `LoggingData`, `Logger`, `log.channels`; "silence logger in vitest", "assert a log line was emitted", "capture log output in tests", "test code that logs"; typical import `import { log, Logger, LogChannel, type LoggingData } from "@warlock.js/logger"`. Skip: custom sinks — `@warlock.js/logger/write-custom-log-channel/SKILL.md`; filtering — `@warlock.js/logger/filter-log-entries/SKILL.md`; competing `vi.spyOn(console)`, `jest.spyOn`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Testing — code that logs, and asserting on log output
|
|
7
|
+
|
|
8
|
+
Two scenarios: **silencing the logger during tests** (most common) and **asserting that a specific log line was emitted**.
|
|
9
|
+
|
|
10
|
+
## Silence the logger during tests
|
|
11
|
+
|
|
12
|
+
Clear every channel once, globally. No output, no file handles, no noise.
|
|
13
|
+
|
|
14
|
+
```ts title="src/setupTests.ts"
|
|
15
|
+
import { log } from "@warlock.js/logger";
|
|
16
|
+
|
|
17
|
+
log.setChannels([]);
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Wire it in Vitest:
|
|
21
|
+
|
|
22
|
+
```ts title="vitest.config.ts"
|
|
23
|
+
import { defineConfig } from "vitest/config";
|
|
24
|
+
|
|
25
|
+
export default defineConfig({
|
|
26
|
+
test: {
|
|
27
|
+
setupFiles: ["src/setupTests.ts"],
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Assert on log output — use a capturing channel
|
|
33
|
+
|
|
34
|
+
Don't spy on `console.log` and don't mock `log.info` — assert on what a channel actually received instead (see "Why not spy on `log.info`?" below). The cleanest pattern is a tiny channel that records what it sees:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { LogChannel } from "@warlock.js/logger";
|
|
38
|
+
import type { LoggingData } from "@warlock.js/logger";
|
|
39
|
+
|
|
40
|
+
class CapturingChannel extends LogChannel {
|
|
41
|
+
public name = "capture";
|
|
42
|
+
public terminal = false;
|
|
43
|
+
public received: LoggingData[] = [];
|
|
44
|
+
public log(data: LoggingData) { this.received.push({ ...data }); }
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Test against the singleton
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
52
|
+
import { log } from "@warlock.js/logger";
|
|
53
|
+
import { createUser } from "./users";
|
|
54
|
+
|
|
55
|
+
describe("createUser", () => {
|
|
56
|
+
let capture: CapturingChannel;
|
|
57
|
+
let originalChannels: typeof log.channels;
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
capture = new CapturingChannel();
|
|
61
|
+
originalChannels = log.channels;
|
|
62
|
+
log.channels = [capture];
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
log.channels = originalChannels;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("logs a success entry when the user is created", async () => {
|
|
70
|
+
await createUser({ email: "a@b.com" });
|
|
71
|
+
|
|
72
|
+
expect(capture.received).toContainEqual(
|
|
73
|
+
expect.objectContaining({
|
|
74
|
+
type: "success",
|
|
75
|
+
module: "users",
|
|
76
|
+
action: "create",
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Test an isolated logger (avoid touching the singleton)
|
|
84
|
+
|
|
85
|
+
If the code under test accepts a logger via injection, create one per test:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { Logger } from "@warlock.js/logger";
|
|
89
|
+
|
|
90
|
+
const testLogger = new Logger();
|
|
91
|
+
const capture = new CapturingChannel();
|
|
92
|
+
testLogger.addChannel(capture);
|
|
93
|
+
|
|
94
|
+
await createUser({ email: "a@b.com" }, testLogger);
|
|
95
|
+
|
|
96
|
+
expect(capture.received[0]!.type).toBe("success");
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
No cleanup needed — the local `Logger` is garbage-collected.
|
|
100
|
+
|
|
101
|
+
## Why not spy on `log.info`?
|
|
102
|
+
|
|
103
|
+
`log` is a plain `Logger` instance (`export const log = new Logger()`) and every level method lives on the prototype, so `vi.spyOn(log, "info")` *does* technically work. Prefer the capturing channel anyway:
|
|
104
|
+
|
|
105
|
+
- A spy on `log.info` proves the method was **called**, not that an entry was **delivered** — it skips the whole pipeline (`minLevel` floor, redaction, per-channel `levels` / `filter`). A capturing channel asserts on the entry your code under test actually produced after all of that ran.
|
|
106
|
+
- The `log` singleton is shared global state. A spy you forget to `mockRestore()` leaks into the next test; swapping `log.channels` and restoring it in `afterEach` is the same amount of code and isolates cleanly.
|
|
107
|
+
- Code that logs through `log.error(...)` and the bare object form `log.log({ type, ... })` both land in channels, but only the level shortcut goes through `log.info` — a channel catches both.
|
|
108
|
+
|
|
109
|
+
So capture through a channel as shown above; reach for a method spy only when you specifically want to assert "this exact shortcut was invoked".
|
|
110
|
+
|
|
111
|
+
## Testing a custom channel
|
|
112
|
+
|
|
113
|
+
Write specs against the channel directly; don't route through `Logger`:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { describe, it, expect, vi } from "vitest";
|
|
117
|
+
import { SlackLog } from "./slack-log";
|
|
118
|
+
|
|
119
|
+
describe("SlackLog", () => {
|
|
120
|
+
it("skips non-error levels", async () => {
|
|
121
|
+
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response());
|
|
122
|
+
|
|
123
|
+
const channel = new SlackLog({ webhookUrl: "https://test", levels: ["error"] });
|
|
124
|
+
await channel.log({ type: "info", module: "x", action: "y", message: "z" });
|
|
125
|
+
|
|
126
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Testing `FileLog` and `JSONFileLog`
|
|
132
|
+
|
|
133
|
+
Use real temp directories — it's the only way to exercise file IO, rotation, chunking, and JSON I/O with fidelity:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import fs from "fs";
|
|
137
|
+
import os from "os";
|
|
138
|
+
import path from "path";
|
|
139
|
+
import { randomUUID } from "node:crypto";
|
|
140
|
+
|
|
141
|
+
function tempDir() {
|
|
142
|
+
const dir = path.join(os.tmpdir(), "logger-test", randomUUID());
|
|
143
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
144
|
+
return dir;
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Clean up in `afterEach(() => fs.rmSync(dir, { recursive: true, force: true }))`.
|
|
149
|
+
|
|
150
|
+
## Waiting for async init
|
|
151
|
+
|
|
152
|
+
`LogChannel.init()` runs inside a `setTimeout(0)`. Before asserting on post-init behavior, yield once:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
const channel = new FileLog({ storagePath: tempDir() });
|
|
156
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
157
|
+
// Now `channel.isInitialized` is true and it's safe to call `channel.log(...)` for real I/O.
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Testing `captureAnyUnhandledRejection`
|
|
161
|
+
|
|
162
|
+
Don't actually throw unhandled rejections in tests — emit the listener directly:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
captureAnyUnhandledRejection();
|
|
166
|
+
process.emit("unhandledRejection", new Error("test"), Promise.resolve());
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The `uncaughtException` path additionally calls `process.exit(1)`, so stub it (or pass `{ exitOnUncaughtException: false }`) before emitting — otherwise the emitted exception tears the test runner down:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
vi.spyOn(process, "exit").mockImplementation(() => undefined as never);
|
|
173
|
+
captureAnyUnhandledRejection();
|
|
174
|
+
process.emit("uncaughtException", new Error("test"), "uncaughtException");
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
See [`@warlock.js/logger/capture-unhandled-errors/SKILL.md`](@warlock.js/logger/capture-unhandled-errors/SKILL.md) for a full example.
|
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: use-log-helpers
|
|
3
|
-
description: 'Two DX shortcuts on every Logger — log.assert(condition, module, action, message, context?) logs an error when condition is falsy (free on the happy path), log.timer(module, action) returns an end-function emitting an info entry with measured duration. Triggers: `log.assert`, `log.timer`, `durationMs`; "assert an invariant via logger", "measure how long an operation took", "time a request", "log operation duration"; typical import `import { log } from "@warlock.js/logger"`. Skip: basics — `@warlock.js/logger/logger-basics/SKILL.md`; filtering — `@warlock.js/logger/filter-log-entries/SKILL.md`; competing `console.assert`, `console.time`, `console.timeEnd`, `perf_hooks.performance.now`.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Helpers — `assert`, `timer`
|
|
7
|
-
|
|
8
|
-
Two small DX shortcuts on every `Logger` (and the bound `log` helper). They route through the normal log pipeline — every channel sees what they emit.
|
|
9
|
-
|
|
10
|
-
## `log.assert(condition, module, action, message, context?)`
|
|
11
|
-
|
|
12
|
-
Logs an `error` entry when `condition` is falsy. Genuinely free in the happy path: when the condition is truthy, the entry is never built and channels are never invoked.
|
|
13
|
-
|
|
14
|
-
```ts
|
|
15
|
-
log.assert(user !== null, "auth", "session", "user vanished mid-flight", {
|
|
16
|
-
sessionId,
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
// truthy → no log call
|
|
20
|
-
// falsy → equivalent to log.error("auth", "session", "user vanished...", { sessionId })
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
The level is implicitly `error` — assertions express failures, not warnings. If you need a non-error level, use `log.error` / `log.warn` directly with your own `if`.
|
|
24
|
-
|
|
25
|
-
### Why not `console.assert`?
|
|
26
|
-
|
|
27
|
-
`console.assert` writes to stderr only and bypasses your file/JSON channels. `log.assert` runs through the logger pipeline, so a failed assertion is captured by every persistent channel you've configured. See [`@warlock.js/logger/pick-log-channel/SKILL.md`](@warlock.js/logger/pick-log-channel/SKILL.md).
|
|
28
|
-
|
|
29
|
-
## `log.timer(module, action)`
|
|
30
|
-
|
|
31
|
-
Returns an end-function. Calling it emits an `info` entry with `completed in <ms>ms` and a `durationMs` field in `context`.
|
|
32
|
-
|
|
33
|
-
```ts
|
|
34
|
-
const end = log.timer("db", "users.findById");
|
|
35
|
-
const user = await usersRepo.findById(id);
|
|
36
|
-
end({ id, found: !!user });
|
|
37
|
-
// ℹ info [db] [users.findById] completed in 12ms
|
|
38
|
-
// ↳ { durationMs: 12, id: "abc", found: true } (when ConsoleLog has showContext: true)
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Common patterns:
|
|
42
|
-
|
|
43
|
-
```ts
|
|
44
|
-
// Around an HTTP handler
|
|
45
|
-
async function handle(req) {
|
|
46
|
-
const end = log.timer("http", `${req.method} ${req.url}`);
|
|
47
|
-
try {
|
|
48
|
-
return await runHandler(req);
|
|
49
|
-
} finally {
|
|
50
|
-
end({ status: res.statusCode });
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Around a job
|
|
55
|
-
const end = log.timer("jobs", "nightly-report");
|
|
56
|
-
await report.run();
|
|
57
|
-
end({ rowsProcessed: report.rowCount });
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
`end()` can be called more than once if you want intermediate checkpoints — each call emits a fresh entry with the duration measured from the original `timer()` call.
|
|
61
|
-
|
|
62
|
-
### Caveats
|
|
63
|
-
|
|
64
|
-
- The duration is `Date.now()` based — millisecond resolution. For sub-millisecond profiling, reach for `performance.now()` directly.
|
|
65
|
-
- The end-function captures `this` at construction; calling it after the logger is reconfigured still routes through the same `Logger` instance.
|
|
66
|
-
- `log.timer` shorthand binds to the singleton — see [`@warlock.js/logger/test-logging-code/SKILL.md`](@warlock.js/logger/test-logging-code/SKILL.md) for how to swap channels per test.
|
|
1
|
+
---
|
|
2
|
+
name: use-log-helpers
|
|
3
|
+
description: 'Two DX shortcuts on every Logger — log.assert(condition, module, action, message, context?) logs an error when condition is falsy (free on the happy path), log.timer(module, action) returns an end-function emitting an info entry with measured duration. Triggers: `log.assert`, `log.timer`, `durationMs`; "assert an invariant via logger", "measure how long an operation took", "time a request", "log operation duration"; typical import `import { log } from "@warlock.js/logger"`. Skip: basics — `@warlock.js/logger/logger-basics/SKILL.md`; filtering — `@warlock.js/logger/filter-log-entries/SKILL.md`; competing `console.assert`, `console.time`, `console.timeEnd`, `perf_hooks.performance.now`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Helpers — `assert`, `timer`
|
|
7
|
+
|
|
8
|
+
Two small DX shortcuts on every `Logger` (and the bound `log` helper). They route through the normal log pipeline — every channel sees what they emit.
|
|
9
|
+
|
|
10
|
+
## `log.assert(condition, module, action, message, context?)`
|
|
11
|
+
|
|
12
|
+
Logs an `error` entry when `condition` is falsy. Genuinely free in the happy path: when the condition is truthy, the entry is never built and channels are never invoked.
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
log.assert(user !== null, "auth", "session", "user vanished mid-flight", {
|
|
16
|
+
sessionId,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// truthy → no log call
|
|
20
|
+
// falsy → equivalent to log.error("auth", "session", "user vanished...", { sessionId })
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The level is implicitly `error` — assertions express failures, not warnings. If you need a non-error level, use `log.error` / `log.warn` directly with your own `if`.
|
|
24
|
+
|
|
25
|
+
### Why not `console.assert`?
|
|
26
|
+
|
|
27
|
+
`console.assert` writes to stderr only and bypasses your file/JSON channels. `log.assert` runs through the logger pipeline, so a failed assertion is captured by every persistent channel you've configured. See [`@warlock.js/logger/pick-log-channel/SKILL.md`](@warlock.js/logger/pick-log-channel/SKILL.md).
|
|
28
|
+
|
|
29
|
+
## `log.timer(module, action)`
|
|
30
|
+
|
|
31
|
+
Returns an end-function. Calling it emits an `info` entry with `completed in <ms>ms` and a `durationMs` field in `context`.
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
const end = log.timer("db", "users.findById");
|
|
35
|
+
const user = await usersRepo.findById(id);
|
|
36
|
+
end({ id, found: !!user });
|
|
37
|
+
// ℹ info [db] [users.findById] completed in 12ms
|
|
38
|
+
// ↳ { durationMs: 12, id: "abc", found: true } (when ConsoleLog has showContext: true)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Common patterns:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
// Around an HTTP handler
|
|
45
|
+
async function handle(req) {
|
|
46
|
+
const end = log.timer("http", `${req.method} ${req.url}`);
|
|
47
|
+
try {
|
|
48
|
+
return await runHandler(req);
|
|
49
|
+
} finally {
|
|
50
|
+
end({ status: res.statusCode });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Around a job
|
|
55
|
+
const end = log.timer("jobs", "nightly-report");
|
|
56
|
+
await report.run();
|
|
57
|
+
end({ rowsProcessed: report.rowCount });
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`end()` can be called more than once if you want intermediate checkpoints — each call emits a fresh entry with the duration measured from the original `timer()` call.
|
|
61
|
+
|
|
62
|
+
### Caveats
|
|
63
|
+
|
|
64
|
+
- The duration is `Date.now()` based — millisecond resolution. For sub-millisecond profiling, reach for `performance.now()` directly.
|
|
65
|
+
- The end-function captures `this` at construction; calling it after the logger is reconfigured still routes through the same `Logger` instance.
|
|
66
|
+
- `log.timer` shorthand binds to the singleton — see [`@warlock.js/logger/test-logging-code/SKILL.md`](@warlock.js/logger/test-logging-code/SKILL.md) for how to swap channels per test.
|