@void.01/advanced-logger 0.4.1 → 0.5.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/ARCHITECTURE.md +348 -342
- package/CHANGELOG.md +133 -115
- package/README.md +324 -305
- package/dist/actions.d.ts +35 -29
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +45 -33
- package/dist/actions.js.map +1 -1
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +13 -6
- package/dist/core.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/sinks.d.ts +17 -1
- package/dist/sinks.d.ts.map +1 -1
- package/dist/sinks.js +36 -10
- package/dist/sinks.js.map +1 -1
- package/package.json +1 -1
package/ARCHITECTURE.md
CHANGED
|
@@ -1,342 +1,348 @@
|
|
|
1
|
-
# advanced-logger: architecture (A to Z)
|
|
2
|
-
|
|
3
|
-
Package: `@void.01/advanced-logger` (version in `package.json`).
|
|
4
|
-
A TypeScript logger for Playwright test projects. It is a port of the Java `core.logging` package ("VOID") with additions.
|
|
5
|
-
Runtime dependencies: none. `@playwright/test` is an optional peer dependency, needed only by the `/playwright` entry.
|
|
6
|
-
|
|
7
|
-
This document describes the code as it is. File and symbol names are exact. Behavior claims are backed by tests in `test/`.
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## 1. What it does, in one paragraph
|
|
12
|
-
|
|
13
|
-
Test code calls `info.click('Submit')`, `warn.log('x')`, `error.table(rows, 'Failures')` and so on. Every call goes through one function (`dispatch` in `src/core.ts`), which builds a `LogEvent` and hands it to a list of sinks. Sinks write to the console (colored, one color per action), to three plain-text trace files, optionally to a JSONL file, to a CSV and a JSON file per logged table, to an in-memory buffer (used to attach the log to a Playwright report on failure), and to any custom sink. Configuration comes from four layers (defaults, config file, env vars, `configure()` calls). Each Playwright worker is a separate process with its own logger state, writing its own files.
|
|
14
|
-
|
|
15
|
-
## 2. Repository layout
|
|
16
|
-
|
|
17
|
-
```
|
|
18
|
-
src/
|
|
19
|
-
index.ts public API barrel (main entry)
|
|
20
|
-
playwright.ts Playwright entry: `test` (with auto fixture), `expect`, `initRun`
|
|
21
|
-
logger.ts the four level instances: debug, info, warn, error
|
|
22
|
-
actions.ts LogActions class (all action methods), fields(), formatValue()
|
|
23
|
-
core.ts dispatch(): the single funnel; INFO truncation; themed(); test context
|
|
24
|
-
config.ts LogConfig, defaults, layered build, configure(), env parsing, run id
|
|
25
|
-
configFile.ts logger.config.* discovery, loading, validation
|
|
26
|
-
theme.ts LogIntent, the one built-in theme, ACTION_FG (per-action colors), resolveStyle()
|
|
27
|
-
redact.ts maskText(): free-text secret masking (redactText)
|
|
28
|
-
ansi.ts escape-code factory, color catalog, stripAnsi
|
|
29
|
-
caller.ts stack parsing: "caller" suffix and "[CHAIN]"
|
|
30
|
-
sinks.ts Sink interface, built-in sinks, file writing, retention, registry
|
|
31
|
-
test/
|
|
32
|
-
logger.test.ts, logger.aggressive.test.ts core behavior (vitest)
|
|
33
|
-
colors.test.ts fg/bg/ansi/callerColor matrix
|
|
34
|
-
config-file.test.ts discovery, forms, precedence, failures
|
|
35
|
-
truncate.test.ts infoLogsCharLimit + its hint
|
|
36
|
-
retention.test.ts maxRuns, maxLogFiles, recent/missing-run-id guards
|
|
37
|
-
separable.test.ts consoleWorker, consoleTest, missing initRun() warning, configure() validation
|
|
38
|
-
multi-worker.test.ts spawns real Playwright with 2 workers (fixtures/multi-worker/)
|
|
39
|
-
table-files.test.ts CSV/JSON table attachments: names, contents, redaction, retention
|
|
40
|
-
palette.test.ts, redaction.test.ts per-action colors; nested and free-text redaction
|
|
41
|
-
sweep/ npm run sweep: manual check of every console visual and every file, with file checks
|
|
42
|
-
*.spec.ts, playwright*.config.ts, global-setup.ts Playwright e2e/demo specs
|
|
43
|
-
dist/ tsc output (CommonJS, ES2022), gitignored, what npm publishes
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Build: `tsc -p tsconfig.build.json` (module CommonJS, moduleResolution Node10, declarations + source maps, rootDir `src`). `prepare` runs the build on `npm install`/publish. Tests: `npm run test:unit` (vitest, `test/**/*.test.ts`), `npm run test:e2e` (Playwright, 2 workers, `test/*.spec.ts`, fixtures ignored), `npm test` runs both.
|
|
47
|
-
|
|
48
|
-
Published entry points (`package.json` `exports`):
|
|
49
|
-
|
|
50
|
-
| Import path | File | Purpose |
|
|
51
|
-
|---|---|---|
|
|
52
|
-
| `@void.01/advanced-logger` | `dist/index.js` | `info/warn/error/debug`, `configure`, `defineConfig`, helpers |
|
|
53
|
-
| `@void.01/advanced-logger/playwright` | `dist/playwright.js` | drop-in `test`/`expect` with logging fixture |
|
|
54
|
-
|
|
55
|
-
## 3. Module dependency graph
|
|
56
|
-
|
|
57
|
-
```
|
|
58
|
-
index.ts ──► logger ──► actions ──► core ──► caller
|
|
59
|
-
│ │ ├────► sinks
|
|
60
|
-
│ │ └────► theme
|
|
61
|
-
├──► config ◄──────────────┐
|
|
62
|
-
└──► theme, ansi │
|
|
63
|
-
config ──► configFile (runtime) theme ──► config (runtime, reads fg/bg)
|
|
64
|
-
config ──► theme (type only) configFile ──► config (types only)
|
|
65
|
-
sinks ──► config, theme, ansi caller ──► config (type only)
|
|
66
|
-
playwright ──► config, core, sinks, logger
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Two potential cycles are deliberately broken with `import type`: `config.ts` imports only the *type* `LevelName` from `theme.ts`, and `configFile.ts` imports only *types* from `config.ts`. `theme.ts` imports `config()` at runtime (to read `fg`/`bg`). Do not add runtime imports in the other direction.
|
|
70
|
-
|
|
71
|
-
## 4. The call path (the most important flow)
|
|
72
|
-
|
|
73
|
-
```
|
|
74
|
-
info.click('Submit') actions.ts LogActions.click
|
|
75
|
-
└► this.msg(LogIntent.INTERACTION, 'CLICK [>]', m, 'click')
|
|
76
|
-
└► dispatch('INFO', intent, themed(level,intent,'click'), label, str(m)) core.ts
|
|
77
|
-
1. cfg = config() (lazy build on first call, see §6)
|
|
78
|
-
2. redactText: message = maskText(...) (see §3, redaction)
|
|
79
|
-
3. INFO truncation branch (see §11) may emit 3 events and return
|
|
80
|
-
4. emit(...):
|
|
81
|
-
a. sinks = activeSinks().filter(s => s.accepts(level, cfg))
|
|
82
|
-
b. if none accept: return (no stack is ever captured)
|
|
83
|
-
c. resolveCaller(cfg) caller.ts: parse new Error().stack
|
|
84
|
-
d. build LogEvent {time, ts, level, intent, label, message, color,
|
|
85
|
-
caller, chain, worker, test}
|
|
86
|
-
e. for each sink: try { sink.write(ev, cfg) } catch -> one stderr line
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
Key properties:
|
|
90
|
-
|
|
91
|
-
- **Single funnel.** Everything (actions, tables, trees, Playwright fixture banner lines) ends in `dispatch`. Cross-cutting features (free-text masking, truncation) live there.
|
|
92
|
-
- **Cheap rejection first.** `Sink.accepts(level, cfg)` runs before any stack capture, which is the expensive part. If the root level hides the line and file sinks are off, no stack is captured.
|
|
93
|
-
- **A logger never fails a test.** A throwing sink is caught and reported once per call on stderr. The one deliberate exception is a broken config file (see §7), which throws.
|
|
94
|
-
- **Color is baked at call time.** `themed()` computes the escape prefix when the action is called and stores it in `LogEvent.color`. Sinks do not recompute it.
|
|
95
|
-
|
|
96
|
-
### Level instances and actions
|
|
97
|
-
|
|
98
|
-
`logger.ts` exports four `LogActions` instances: `debug`, `info`, `warn`, `error`. `LogActions` (`actions.ts`) has one method per action, each a thin wrapper `this.msg(intent, label, message)`:
|
|
99
|
-
|
|
100
|
-
| Intent | Methods (label) |
|
|
101
|
-
|---|---|
|
|
102
|
-
| INTERACTION | click, checkbox, text, input, dropdown, toggle, upload, hover, clear, key, password (masked) |
|
|
103
|
-
| NAVIGATION | navigate, tab, frame, breadcrumb |
|
|
104
|
-
| OBSERVE | wait, search, result |
|
|
105
|
-
| VERIFY | verifying |
|
|
106
|
-
| SUCCESS | success, complete, resolved |
|
|
107
|
-
| ALERT | error, failed, timeout, validation, fallback, skip |
|
|
108
|
-
| DATA | grid, table, row |
|
|
109
|
-
| BASE | log, tree |
|
|
110
|
-
|
|
111
|
-
Composite methods:
|
|
112
|
-
|
|
113
|
-
- `log(...)` is overloaded: string; object/array/map (table or indexed list); `(heading, fields)` or `(heading, k, v, ...)` (tree); `(heading, value)`.
|
|
114
|
-
- `table(rows, title)` renders an ASCII table through `renderTable`, then calls `dispatch(level, null, color, 'TABLE', ...)` with **intent `null`** (a pre-colored block). `table("text")` is just a TABLE line. The title is required by the type. At runtime a missing or blank title is allowed (plain JS, older code): `warnOnce` asks for one, the table renders without a title row, and its files get the heading `untitled`. `tableInternal(data, title, heading)` separates the console title row (`title`, may be undefined) from the file heading: `log(object)` uses no title row and the heading `log`, a logged list's items `list item N`. A title longer than the table widens the last column instead of being cut.
|
|
115
|
-
- `row
|
|
116
|
-
-
|
|
117
|
-
- Redaction,
|
|
118
|
-
-
|
|
119
|
-
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
| `
|
|
151
|
-
| `
|
|
152
|
-
| `
|
|
153
|
-
| `
|
|
154
|
-
| `
|
|
155
|
-
| `
|
|
156
|
-
| `
|
|
157
|
-
| `
|
|
158
|
-
| `
|
|
159
|
-
| `
|
|
160
|
-
| `
|
|
161
|
-
| `
|
|
162
|
-
| `
|
|
163
|
-
| `
|
|
164
|
-
| `
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
167
|
-
| `
|
|
168
|
-
| `
|
|
169
|
-
| `
|
|
170
|
-
| `
|
|
171
|
-
| `
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
- `
|
|
190
|
-
- `
|
|
191
|
-
- `configure(patch)
|
|
192
|
-
- `configure(
|
|
193
|
-
- `
|
|
194
|
-
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
-
|
|
202
|
-
-
|
|
203
|
-
-
|
|
204
|
-
-
|
|
205
|
-
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
-
|
|
213
|
-
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
- `
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
|
232
|
-
|
|
|
233
|
-
|
|
|
234
|
-
|
|
|
235
|
-
|
|
|
236
|
-
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
- `
|
|
245
|
-
- `ansi`
|
|
246
|
-
- `
|
|
247
|
-
- `
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
- `
|
|
255
|
-
-
|
|
256
|
-
- `
|
|
257
|
-
- `
|
|
258
|
-
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
- `
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
`
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
|
334
|
-
|
|
335
|
-
|
|
|
336
|
-
|
|
|
337
|
-
|
|
|
338
|
-
|
|
|
339
|
-
|
|
|
340
|
-
|
|
|
341
|
-
|
|
342
|
-
|
|
1
|
+
# advanced-logger: architecture (A to Z)
|
|
2
|
+
|
|
3
|
+
Package: `@void.01/advanced-logger` (version in `package.json`).
|
|
4
|
+
A TypeScript logger for Playwright test projects. It is a port of the Java `core.logging` package ("VOID") with additions.
|
|
5
|
+
Runtime dependencies: none. `@playwright/test` is an optional peer dependency, needed only by the `/playwright` entry.
|
|
6
|
+
|
|
7
|
+
This document describes the code as it is. File and symbol names are exact. Behavior claims are backed by tests in `test/`.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. What it does, in one paragraph
|
|
12
|
+
|
|
13
|
+
Test code calls `info.click('Submit')`, `warn.log('x')`, `error.table(rows, 'Failures')` and so on. Every call goes through one function (`dispatch` in `src/core.ts`), which builds a `LogEvent` and hands it to a list of sinks. Sinks write to the console (colored, one color per action), to three plain-text trace files, optionally to a JSONL file, to a CSV and a JSON file per logged table, to an in-memory buffer (used to attach the log to a Playwright report on failure), and to any custom sink. Configuration comes from four layers (defaults, config file, env vars, `configure()` calls). Each Playwright worker is a separate process with its own logger state, writing its own files.
|
|
14
|
+
|
|
15
|
+
## 2. Repository layout
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
src/
|
|
19
|
+
index.ts public API barrel (main entry)
|
|
20
|
+
playwright.ts Playwright entry: `test` (with auto fixture), `expect`, `initRun`
|
|
21
|
+
logger.ts the four level instances: debug, info, warn, error
|
|
22
|
+
actions.ts LogActions class (all action methods), fields(), formatValue()
|
|
23
|
+
core.ts dispatch(): the single funnel; INFO truncation; themed(); test context
|
|
24
|
+
config.ts LogConfig, defaults, layered build, configure(), env parsing, run id
|
|
25
|
+
configFile.ts logger.config.* discovery, loading, validation
|
|
26
|
+
theme.ts LogIntent, the one built-in theme, ACTION_FG (per-action colors), resolveStyle()
|
|
27
|
+
redact.ts maskText(): free-text secret masking (redactText)
|
|
28
|
+
ansi.ts escape-code factory, color catalog, stripAnsi
|
|
29
|
+
caller.ts stack parsing: "caller" suffix and "[CHAIN]"
|
|
30
|
+
sinks.ts Sink interface, built-in sinks, file writing, retention, registry
|
|
31
|
+
test/
|
|
32
|
+
logger.test.ts, logger.aggressive.test.ts core behavior (vitest)
|
|
33
|
+
colors.test.ts fg/bg/ansi/callerColor matrix
|
|
34
|
+
config-file.test.ts discovery, forms, precedence, failures
|
|
35
|
+
truncate.test.ts infoLogsCharLimit + its hint
|
|
36
|
+
retention.test.ts maxRuns, maxLogFiles, recent/missing-run-id guards
|
|
37
|
+
separable.test.ts consoleWorker, consoleTest, missing initRun() warning, configure() validation
|
|
38
|
+
multi-worker.test.ts spawns real Playwright with 2 workers (fixtures/multi-worker/)
|
|
39
|
+
table-files.test.ts CSV/JSON table attachments: names, contents, redaction, retention
|
|
40
|
+
palette.test.ts, redaction.test.ts per-action colors; nested and free-text redaction
|
|
41
|
+
sweep/ npm run sweep: manual check of every console visual and every file, with file checks
|
|
42
|
+
*.spec.ts, playwright*.config.ts, global-setup.ts Playwright e2e/demo specs
|
|
43
|
+
dist/ tsc output (CommonJS, ES2022), gitignored, what npm publishes
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Build: `tsc -p tsconfig.build.json` (module CommonJS, moduleResolution Node10, declarations + source maps, rootDir `src`). `prepare` runs the build on `npm install`/publish. Tests: `npm run test:unit` (vitest, `test/**/*.test.ts`), `npm run test:e2e` (Playwright, 2 workers, `test/*.spec.ts`, fixtures ignored), `npm test` runs both.
|
|
47
|
+
|
|
48
|
+
Published entry points (`package.json` `exports`):
|
|
49
|
+
|
|
50
|
+
| Import path | File | Purpose |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| `@void.01/advanced-logger` | `dist/index.js` | `info/warn/error/debug`, `configure`, `defineConfig`, helpers |
|
|
53
|
+
| `@void.01/advanced-logger/playwright` | `dist/playwright.js` | drop-in `test`/`expect` with logging fixture |
|
|
54
|
+
|
|
55
|
+
## 3. Module dependency graph
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
index.ts ──► logger ──► actions ──► core ──► caller
|
|
59
|
+
│ │ ├────► sinks
|
|
60
|
+
│ │ └────► theme
|
|
61
|
+
├──► config ◄──────────────┐
|
|
62
|
+
└──► theme, ansi │
|
|
63
|
+
config ──► configFile (runtime) theme ──► config (runtime, reads fg/bg)
|
|
64
|
+
config ──► theme (type only) configFile ──► config (types only)
|
|
65
|
+
sinks ──► config, theme, ansi caller ──► config (type only)
|
|
66
|
+
playwright ──► config, core, sinks, logger
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Two potential cycles are deliberately broken with `import type`: `config.ts` imports only the *type* `LevelName` from `theme.ts`, and `configFile.ts` imports only *types* from `config.ts`. `theme.ts` imports `config()` at runtime (to read `fg`/`bg`). Do not add runtime imports in the other direction.
|
|
70
|
+
|
|
71
|
+
## 4. The call path (the most important flow)
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
info.click('Submit') actions.ts LogActions.click
|
|
75
|
+
└► this.msg(LogIntent.INTERACTION, 'CLICK [>]', m, 'click')
|
|
76
|
+
└► dispatch('INFO', intent, themed(level,intent,'click'), label, str(m)) core.ts
|
|
77
|
+
1. cfg = config() (lazy build on first call, see §6)
|
|
78
|
+
2. redactText: message = maskText(...) (see §3, redaction)
|
|
79
|
+
3. INFO truncation branch (see §11) may emit 3 events and return
|
|
80
|
+
4. emit(...):
|
|
81
|
+
a. sinks = activeSinks().filter(s => s.accepts(level, cfg))
|
|
82
|
+
b. if none accept: return (no stack is ever captured)
|
|
83
|
+
c. resolveCaller(cfg) caller.ts: parse new Error().stack
|
|
84
|
+
d. build LogEvent {time, ts, level, intent, label, message, color,
|
|
85
|
+
caller, chain, worker, test}
|
|
86
|
+
e. for each sink: try { sink.write(ev, cfg) } catch -> one stderr line
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Key properties:
|
|
90
|
+
|
|
91
|
+
- **Single funnel.** Everything (actions, tables, trees, Playwright fixture banner lines) ends in `dispatch`. Cross-cutting features (free-text masking, truncation) live there.
|
|
92
|
+
- **Cheap rejection first.** `Sink.accepts(level, cfg)` runs before any stack capture, which is the expensive part. If the root level hides the line and file sinks are off, no stack is captured.
|
|
93
|
+
- **A logger never fails a test.** A throwing sink is caught and reported once per call on stderr. The one deliberate exception is a broken config file (see §7), which throws.
|
|
94
|
+
- **Color is baked at call time.** `themed()` computes the escape prefix when the action is called and stores it in `LogEvent.color`. Sinks do not recompute it.
|
|
95
|
+
|
|
96
|
+
### Level instances and actions
|
|
97
|
+
|
|
98
|
+
`logger.ts` exports four `LogActions` instances: `debug`, `info`, `warn`, `error`. `LogActions` (`actions.ts`) has one method per action, each a thin wrapper `this.msg(intent, label, message)`:
|
|
99
|
+
|
|
100
|
+
| Intent | Methods (label) |
|
|
101
|
+
|---|---|
|
|
102
|
+
| INTERACTION | click, checkbox, text, input, dropdown, toggle, upload, hover, clear, key, password (masked) |
|
|
103
|
+
| NAVIGATION | navigate, tab, frame, breadcrumb |
|
|
104
|
+
| OBSERVE | wait, search, result |
|
|
105
|
+
| VERIFY | verifying |
|
|
106
|
+
| SUCCESS | success, complete, resolved |
|
|
107
|
+
| ALERT | error, failed, timeout, validation, fallback, skip |
|
|
108
|
+
| DATA | grid, table, row |
|
|
109
|
+
| BASE | log, tree |
|
|
110
|
+
|
|
111
|
+
Composite methods:
|
|
112
|
+
|
|
113
|
+
- `log(...)` is overloaded: string; object/array/map (table or indexed list); `(heading, fields)` or `(heading, k, v, ...)` (tree); `(heading, value)`.
|
|
114
|
+
- `table(rows, title)` renders an ASCII table through `renderTable`, then calls `dispatch(level, null, color, 'TABLE', ...)` with **intent `null`** (a pre-colored block). `table("text")` is just a TABLE line. The title is required by the type. At runtime a missing or blank title is allowed (plain JS, older code): `warnOnce` asks for one, the table renders without a title row, and its files get the heading `untitled`. `tableInternal(data, title, heading)` separates the console title row (`title`, may be undefined) from the file heading: `log(object)` uses no title row and the heading `log`, a logged list's items `list item N`. A title longer than the table widens the last column instead of being cut.
|
|
115
|
+
- `row` and `tree` emit several `msg()` lines (one `dispatch` each).
|
|
116
|
+
- Every action except `password` (and `table`/`row`/`tree`) goes through `act(intent, label, action, m, more)`: a lone message is one line; `(heading, rows[])` (an array whose items are all objects/Maps, including empty) is the heading line plus `tableInternal(rows, undefined, heading)` (no title row, files named after the heading); anything else (`toFields`: an object, or pairs) is `treeInternal` with the action's full label (`RESOLVED [ok]`, `CLICK [>]`) and color on every line.
|
|
117
|
+
- Redaction, structured: `redacted(key, v)` prints `***` when the key contains any of `redactKeys` (case-insensitive substring), else returns `redactDeep(v)`: a copy with sensitive keys masked at any depth in plain objects, Maps and arrays (a `toJSON()` result is redacted instead of the object; cycles become `[Circular]`; the caller's object is never touched). Used by tree, table and row.
|
|
118
|
+
- Redaction, free text (`redact.ts`, when `redactText`): `dispatch` runs `maskText(message, redactKeys)` before truncation and before any sink, so every output is masked. It masks the value in `key=value`, `key: value`, `"key":"value"` and `'key': 'value'` when the key contains a sensitive word, plus `Bearer|Basic <credentials>`. The pair regex is built from `redactKeys` and cached per key list. Pattern-based, so it can miss or over-mask (README lists the limits).
|
|
119
|
+
- Colors: `msg(intent, label, m, action?)` passes the action name (`'click'`, ...) so `themed()` can pick the action's own color (§10).
|
|
120
|
+
- Malformed arguments never throw (`safeFields` keeps a dangling key under `(unpaired)`); `fields()` itself throws on an odd count by design.
|
|
121
|
+
|
|
122
|
+
## 5. The event model
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
interface LogEvent {
|
|
126
|
+
time: Date; ts: string; // ts is pre-formatted via cfg.tsFormat
|
|
127
|
+
level: 'DEBUG'|'INFO'|'WARN'|'ERROR';
|
|
128
|
+
intent: LogIntent | null; // null = pre-colored block (table)
|
|
129
|
+
label: string; // e.g. 'CLICK [>]', 'INFO', 'TABLE'
|
|
130
|
+
message: string; // may be multi-line
|
|
131
|
+
color: string; // resolved ANSI prefix; '' means none
|
|
132
|
+
caller: string; // "Callee.method ← Caller.method", plain text
|
|
133
|
+
chain: string; // in-project call chain, leaf first, plain text
|
|
134
|
+
worker: string; // 'w<TEST_WORKER_INDEX>' or 'main'
|
|
135
|
+
test?: string; // full title path, set by the Playwright fixture (jsonl)
|
|
136
|
+
testTitle?: string; // the test's own title (consoleTest prefix)
|
|
137
|
+
table?: TableData; // a rendered table as data: {title, headers, rows, file}
|
|
138
|
+
}
|
|
139
|
+
interface Sink { accepts(level, cfg): boolean; write(ev, cfg): void }
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Rendered line shape: `<ts> │ <label> │ <message line>` where `│` is `cfg.segmentDivider`. Multi-line messages are split on a Java-`\R`-equivalent regex (`LINE_BREAKS`) and each line gets its own prefix.
|
|
143
|
+
|
|
144
|
+
## 6. Configuration
|
|
145
|
+
|
|
146
|
+
### 6.1 `LogConfig` (every option)
|
|
147
|
+
|
|
148
|
+
| Option | Type / default | Meaning |
|
|
149
|
+
|---|---|---|
|
|
150
|
+
| `level` | `'debug'\|'info'\|'warn'\|'error'\|'off'`, `info` | Root threshold for console + partial-trace |
|
|
151
|
+
| `ansi` | bool, auto-detected | Master color switch. `false` overrides `fg`, `bg`, `callerColor` |
|
|
152
|
+
| `fg` | bool, `true` | Foreground colors (intent color; level color on BASE lines; bold) |
|
|
153
|
+
| `bg` | bool, `true` | The theme background (one constant, `BG_BLACK`, for all levels) |
|
|
154
|
+
| `callerColor` | bool, `false` | Color the caller suffix separately (console, debug level). Independent of fg/bg |
|
|
155
|
+
| `infoLogsCharLimit` | number\|null, `null` | INFO truncation limit (§11). null/0 = off |
|
|
156
|
+
| `maxRuns` | number\|null, `null` | Retention per trace kind, in whole runs (§12). null/0 = off |
|
|
157
|
+
| `recentRunMinutes` | number, `60` | Retention never deletes a run touched this recently (§12). 0 = off |
|
|
158
|
+
| `maxLogFiles` | number\|null, `null` | Retention per trace kind, in files (§12). null/0 = off |
|
|
159
|
+
| `consoleWorker` | bool, `false` | Console lines prefixed `w<N> │ ` (§9) |
|
|
160
|
+
| `consoleTest` | bool, `false` | Console lines prefixed with the test title, max 40 chars (§9) |
|
|
161
|
+
| `tsFormat` | string\|fn, `yyyy-MM-dd HH:mm:ss.SSS` | Java-style pattern or `(Date)=>string` |
|
|
162
|
+
| `segmentDivider` | string, `' │ '` | Column separator |
|
|
163
|
+
| `tableCellLimit`, `tableCellLimitEnabled` | 40, false | Truncate table/row cells |
|
|
164
|
+
| `suppressContains`, `suppressMethodPrefixes`, `includeOnlyPrefixes` | Sets | Caller/chain frame filters (§8) |
|
|
165
|
+
| `projectRoots` | string[], `[cwd]` | Frames under these form the `[CHAIN]` |
|
|
166
|
+
| `console` | bool, true | Console sink on/off |
|
|
167
|
+
| `files` | `{debugTrace, partialTrace, fullTrace, jsonl, tables}` | Per-file toggles (jsonl default false; tables, the CSV/JSON table attachments, default true) |
|
|
168
|
+
| `logDir` | string, `./logs` | Output root |
|
|
169
|
+
| `githubAnnotations` | bool, `GITHUB_ACTIONS==='true'` | Emit `::error::`/`::warning::` lines |
|
|
170
|
+
| `redactKeys` | string[] | Keys masked in structured output (any depth) and, with `redactText`, in free text |
|
|
171
|
+
| `redactText` | bool, `true` | Mask `key=value`-style secrets and `Bearer`/`Basic` credentials in message text (§3) |
|
|
172
|
+
| `attach` | `'on-failure'\|'always'\|'never'` | Playwright attachment policy |
|
|
173
|
+
|
|
174
|
+
`LogConfigPatch` = `Partial<LogConfig>` with the Set-typed fields accepting any iterable and `files` partial. It is the type `configure()` and config files accept.
|
|
175
|
+
|
|
176
|
+
### 6.2 Layers and precedence (lowest to highest)
|
|
177
|
+
|
|
178
|
+
1. **Built-in defaults**: `builtinDefaults(env)` in `config.ts`. Only terminal/CI heuristics for `ansi` and `GITHUB_ACTIONS` for annotations read the environment here.
|
|
179
|
+
2. **Config file** (`logger.config.*`, §7).
|
|
180
|
+
3. **Environment variables**: `applyEnv()`.
|
|
181
|
+
4. **`configure()` calls** in code mutate the live object afterwards, so they always win.
|
|
182
|
+
|
|
183
|
+
Environment variables: `VOID_LOG_LEVEL`, `VOID_LOG_DIR`, `VOID_LOG_ANSI`, `VOID_LOG_FG`, `VOID_LOG_BG`, `VOID_LOG_JSONL`, `VOID_LOG_TABLES`, `VOID_LOG_INFO_LOGS_CHAR_LIMIT`, `VOID_LOG_MAX_RUNS`, `VOID_LOG_MAX_FILES`, `VOID_LOG_RECENT_RUN_MINUTES`, `VOID_LOG_REDACT_TEXT`, `VOID_LOG_CONFIG`, `VOID_RUN_ID`, `VOID_RUN_DATE`, plus `NO_COLOR` / `FORCE_COLOR`. Booleans: `1/true/yes/on` true, anything else non-empty false. An invalid level or non-numeric limit is ignored (so it cannot override a lower layer). `NO_COLOR`/`FORCE_COLOR` count as env-level (they beat `ansi` in a file); terminal auto-detection counts as default-level (the file beats it). See `explicitAnsi()` vs `heuristicAnsi()`.
|
|
184
|
+
|
|
185
|
+
### 6.3 Lazy, per-process state
|
|
186
|
+
|
|
187
|
+
`config.ts` holds module state `CURRENT: LogConfig | undefined`.
|
|
188
|
+
|
|
189
|
+
- `config()` returns `CURRENT ?? build()`. The first call in a process builds the config (including reading the config file). Every process, including each Playwright worker, does this independently.
|
|
190
|
+
- `build()` sets `CURRENT = cfg` early as a **re-entrancy guard**: a config file that imports this package and touches `config()` while loading sees the defaults instead of recursing. On failure it sets `CURRENT = undefined` and rethrows, so every later call throws again (no silent fallback to defaults).
|
|
191
|
+
- `configure(patch | fn)`, the `enable*/disable*` mutators, and every internal reader go through `config()`, so they trigger the load first. Therefore a `configure()` call made before any log call still sits above the file.
|
|
192
|
+
- `configure(patch)` runs the patch through `checkPatch` (the config file's validator, §7) with the source label `configure()`. Unlike a file, it never throws: each problem is written to stderr once per process (`WARNED`, cleared by `resetConfig()`) and that key is left out; the rest applies. A non-object patch warns and is ignored.
|
|
193
|
+
- `configure(fn)` snapshots the config (`cloneConfig`), runs the callback on the live object, then `checkLive` turns arrays assigned to the three Set-typed options into Sets and runs `checkPatch` over the whole live config with the label `configure() callback`. Each problem warns once and is undone: a wrong-typed option goes back to its snapshot value, an unknown option or `files` entry is deleted. `checkPatch` reports each problem with its key (`PatchProblem`) for this.
|
|
194
|
+
- `resetConfig()` clears `CURRENT`, forgets the cached config file (`forgetConfigFile()` deletes it from `require.cache`), and rebuilds. Used by tests.
|
|
195
|
+
- A mutable `LIMIT_SOURCE` records which layer set `infoLogsCharLimit` (`default|file|env|code`) for the truncation hint.
|
|
196
|
+
|
|
197
|
+
**Process model consequence** (verified by `multi-worker.test.ts`): `configure()` inside Playwright's `globalSetup` runs in the runner process and does **not** reach workers. Only env vars are inherited. That is why `initRun()` communicates through `process.env.VOID_RUN_ID`/`VOID_RUN_DATE`, and why the config file exists.
|
|
198
|
+
|
|
199
|
+
## 7. Config file loading (`configFile.ts`)
|
|
200
|
+
|
|
201
|
+
- Discovery in `process.cwd()`: `logger.config.ts`, `.js`, `.cjs`, `.json`, first existing wins (`CONFIG_FILE_NAMES`). No file: silently fine, behavior identical to 0.1.0.
|
|
202
|
+
- `VOID_LOG_CONFIG` overrides the path (resolved against cwd). An explicit path that does not exist **throws** (deliberate: a typo should not be silent).
|
|
203
|
+
- Loaded with a lazy `require(file)` inside `loadConfigFile`. Under Playwright, its TypeScript hook transpiles `.ts`; under plain Node, type stripping or CJS handles it. No transpiler dependency. `unwrapDefault` handles `__esModule` interop (`export default`), `module.exports =`, and JSON.
|
|
204
|
+
- Forms: a plain object (`LogConfigPatch`) or `(config: LogConfig) => LogConfigPatch`. The function receives a **clone** of the defaults (`cloneConfig`), so it cannot mutate live state. `defineConfig()` is an identity helper for typing.
|
|
205
|
+
- Validation (table `KEYS`): `checkPatch(value, where)` returns the clean patch plus one message per problem (unknown option name, such as a typo like `levl`; wrong type; bad `files` sub-key), and drops `undefined` values. `validatePatch` throws the first problem for a file; `configure()` warns about each one instead (§6.3). File error messages contain the absolute file path; load failures (`fail()`) wrap the original in `cause`.
|
|
206
|
+
- Loaded once per process (Node's require cache plus `CURRENT`). `loadedConfigFile()` exposes the path for messages.
|
|
207
|
+
|
|
208
|
+
## 8. Caller and chain resolution (`caller.ts`)
|
|
209
|
+
|
|
210
|
+
Parses `new Error().stack` (limit raised to 60 temporarily) with `FRAME_RE`. `displayName()` turns frames into readable names:
|
|
211
|
+
|
|
212
|
+
- `Class.method` when V8 reports a type (instance, static, async, class-field arrow all verified); `Class.(constructor)` for `new`.
|
|
213
|
+
- Free functions and plain-object types (`Object`, `Module`, `Function`, ...) become `<fileBase>.<function>`.
|
|
214
|
+
- Frames inside the logger's own folder (`SELF_DIR`, computed at load from this file's path) are always dropped. This replaces the Java logger's method-name-prefix suppression, so user methods like `login()` are not hidden.
|
|
215
|
+
|
|
216
|
+
Two outputs:
|
|
217
|
+
|
|
218
|
+
- `caller`: first surviving frame is the callee, the next frame with a different display name is the caller: `Callee ← Caller`.
|
|
219
|
+
- `chain`: every surviving frame under `projectRoots` (excluding `node_modules`/`node:`), consecutive duplicates collapsed, leaf first, joined by ` ← `.
|
|
220
|
+
|
|
221
|
+
Filters: `suppressMethodPrefixes`, `includeOnlyPrefixes` (path or display prefix), `suppressContains` (default `node_modules`, `node:`, `<anonymous>`).
|
|
222
|
+
|
|
223
|
+
If the chain comes out empty, `projectRoots` does not cover the test code; set `configure({ projectRoots: [__dirname] })`.
|
|
224
|
+
|
|
225
|
+
## 9. Output sinks (`sinks.ts`)
|
|
226
|
+
|
|
227
|
+
`activeSinks()` returns, in order: console, partial-trace, debug-trace, full-trace, jsonl, capture, then custom sinks (`addSink`/`removeSink`).
|
|
228
|
+
|
|
229
|
+
| Sink | accepts | Content |
|
|
230
|
+
|---|---|---|
|
|
231
|
+
| console | `cfg.console` and level ≥ root | Colored line; caller suffix only when root level is `debug` |
|
|
232
|
+
| partial-trace | file toggle and level ≥ root | Same lines as console, plain text |
|
|
233
|
+
| debug-trace | file toggle, **all levels** | Plain text + caller suffix |
|
|
234
|
+
| full-trace | file toggle, **all levels** | Plain text + `[CHAIN] <chain>` suffix |
|
|
235
|
+
| jsonl | `files.jsonl` | One JSON record per event (opt-in) |
|
|
236
|
+
| table files | `files.tables`, **all levels**; acts only on events with `ev.table.file` | `attachments/{csv,json}/<runId>-<worker>/<NNN>-<heading>.{csv,json}` per table |
|
|
237
|
+
| capture | an active capture exists and level ≥ root | In-memory lines for the Playwright attachment |
|
|
238
|
+
|
|
239
|
+
Files: `<logDir>/<runDate>/<kind>/<prefix>-<runId>-<worker>.log` (`traceFilePath`), where `FILE_PREFIX` maps the kind to `PT`, `DT`, `FT` or `JL` (jsonl): the folder already names the kind, so the file name reads as a trace id. Writes are **synchronous** (`fs.writeSync` on cached fds in `FD_CACHE`), one file per worker, so parallel workers never interleave and a crash loses nothing. Every file line is prefixed `w<N> │ ` and passes through `stripAnsi`, so **plain-text sinks never contain escape codes** regardless of color flags. `closeAllFiles()` closes fds (and clears prune bookkeeping).
|
|
240
|
+
|
|
241
|
+
Table attachments (`tableFilesSink`): `renderTable` (actions.ts) asks `nextTableFile(cfg, heading)` for a name, `<runId>-<worker>/<NNN>-<slug>`. The folder `<runId>-<worker>` is the part all of this worker's trace files share (`<prefix>-<runId>-<worker>.log`), so each parent log has its own folder under `attachments/csv/` and `attachments/json/`. `NNN` comes from a per-process counter that is never reset; a number whose CSV or JSON already exists (another process with the same run id and worker label) is skipped, never overwritten. `headingSlug` lowercases the heading, turns every run of non-letters/digits (Unicode `p{L}p{N}`) into `-`, cuts at 40 chars, and falls back to `table`. The table is dispatched with a `TableData` payload: headers in render order and rows as plain objects, already redacted by key at any depth (`redactDeep`) and never truncated; `file` is the relative name. The sink writes both files with `writeFileSync` (`tableFilePath` joins the folder); with `redactText` it masks each string value and the title (`maskStrings`), never the serialized text, which would break JSON. CSV (`tableToCsv`): BOM, CRLF, RFC 4180 quoting, objects as JSON, a leading `'` on text cells starting with `= + - @` (except plain negative numbers). JSON (`tableToJson`): `{title, time, level, worker, runId, test, columns, rows}`, with a replacer for Map, Set, bigint and Error. After the table, `renderTable` dispatches a DEBUG `TABLE FILES │ attachments/csv/<file>.csv | attachments/json/<file>.json` line, so debug-trace and full-trace point to the files. Custom sinks see `ev.table` too.
|
|
242
|
+
|
|
243
|
+
Console rendering (`consoleSink`):
|
|
244
|
+
- Opt-in prefix (`consolePrefix`), placed before the timestamp and inside the color block: `w<N> │ ` when `consoleWorker`, then `<testTitle> │ ` when `consoleTest` and a test is running (line breaks flattened, cut to `TEST_TAG_MAX` = 40 chars with `...`). The worker part uses a literal ` │ `, like the file prefix, not `segmentDivider`. The GitHub annotation text carries the same prefix. File sinks do not use it: they are already per worker, and tests are bracketed by `TEST START`/`TEST END`.
|
|
245
|
+
- `ansi` off: plain.
|
|
246
|
+
- `ansi` on: `wrap(s) = ev.color ? ev.color + s + RESET : s`. When `fg` and `bg` are both off, `ev.color` is `''` and the body is bare (no stray reset).
|
|
247
|
+
- `callerColor` on and a caller exists: body wrapped, then `div + CALLER_FG + caller + RESET`. Not gated by `fg`/`bg`; only `ansi:false` silences it.
|
|
248
|
+
- `githubAnnotations`: after each line, `::error::`/`::warning::` for ERROR/ALERT/WARN events.
|
|
249
|
+
|
|
250
|
+
## 10. Color model (`theme.ts`, `ansi.ts`)
|
|
251
|
+
|
|
252
|
+
One built-in theme (HIGH_CONTRAST). `resolveStyle(level, intent, action?)` = foreground + background:
|
|
253
|
+
|
|
254
|
+
- Foreground: for `BASE` intent, `LEVEL_FG[level]` (white, amber, red, grey, all bold). Otherwise `ACTION_FG[action] + BOLD` when the line has an action, else `INTENT_FG[intent]` (the family's anchor color, for custom `LogActions` subclasses that call `msg` without an action). Neither depends on the level.
|
|
255
|
+
- `ACTION_FG` gives each of the 31 actions its own 24-bit color inside its intent's hue family: interaction blues, navigation teals, observe pinks, verify gold, data violets, success greens, alert reds and oranges. ERROR and `failed` share `ERROR_RED` (248,75,75). The values were chosen in OKLCH and are enforced by `palette.test.ts`: contrast at least 4.5 on `#1e1e1e`, CIEDE2000 at least 10 within a family and 12 across families (including the INFO and WARN level colors). Change a color only with that test passing.
|
|
256
|
+
- Background: constant `BG_BLACK` for every level (there is no per-level background).
|
|
257
|
+
- `config().fg` false drops the foreground part; `config().bg` false drops the background; both false returns `''`.
|
|
258
|
+
- `themed(level, intent, action?)` in `core.ts` returns `''` when `ansi` is off, else `resolveStyle`. The DEBUG copy made by INFO truncation reuses the original line's color unless it is a BASE line (which turns DEBUG grey).
|
|
259
|
+
- Tables: the title row is wrapped as `TABLE_TITLE_FG + BOLD + line + color` and the column-name row as `TABLE_COLUMNS_FG + BOLD + line + color`, then the table's own style is restored so the background continues. Both are darker than the body and differ from each other: the title is dark and saturated (4.66:1 on `#1e1e1e`, CIEDE2000 25.8 from the body), the columns a muted mid shade of the same hue (6.15:1; 15.6 from the body, 14.9 from the title). A more saturated column color would drift into the observe pinks; `palette.test.ts` checks all of this. Only when a style exists and `fg` is on (these are foregrounds). Borders and data rows use the table color.
|
|
260
|
+
|
|
261
|
+
`ansi.ts` is a catalog and factory (`sgr`, `fg16`, `rgbFg`, ... named `FG_*`, `BG_*`, `RGB_FG`, ...), plus `stripAnsi` and `fgFromStyle`.
|
|
262
|
+
|
|
263
|
+
## 11. INFO truncation (`infoLogsCharLimit`, in `dispatch`)
|
|
264
|
+
|
|
265
|
+
Condition: `level==='INFO'`, `intent !== null` (tables excluded; they have `tableCellLimit`), limit > 0, message longer than the limit, and INFO is visible at the root level (otherwise nothing is truncated or warned, since the traces already hold the full text).
|
|
266
|
+
|
|
267
|
+
Then three events are emitted via `emit()`, each with `ev.truncation = { part, limit }`:
|
|
268
|
+
|
|
269
|
+
1. `cut`: INFO, original label: `message.slice(0, limit) + '... (<total> chars)'`. Also carries `full` (the original text) and `notice` (the general WARN text: `Some INFO logs exceed the <limit> char limit. ... Refer to debug-trace for the full text. <hint>`).
|
|
270
|
+
2. `warn`: WARN, label `WARN`: `INFO log exceeded the <limit> char threshold (<total> chars). Refer to the debug logs to see the full text. <hint>`
|
|
271
|
+
3. `copy`: DEBUG, same label (a plain `log` line's `INFO` label becomes `DEBUG`), full text.
|
|
272
|
+
|
|
273
|
+
Console, full-trace, JSONL, capture and custom sinks write all three. The two trace files below read `ev.truncation`:
|
|
274
|
+
|
|
275
|
+
- `partialTraceSink`: skips `warn`. Before a `cut` line it writes `notice` as a WARN line, once per `<file>|<limit>` (`NOTICED`, reset by `closeAllFiles()`).
|
|
276
|
+
- `debugTraceSink`: skips `warn` and `copy`. Writes the same once-only `notice`, then the `cut` event with `full` as its text.
|
|
277
|
+
|
|
278
|
+
`infoLimitHint()` (in `config.ts`) tells the reader what to edit, depending on `LIMIT_SOURCE`: the config file's absolute path, the env var name, or the `configure()` call.
|
|
279
|
+
|
|
280
|
+
## 12. Log file retention (`maxRuns`, `maxLogFiles`, in `sinks.ts`)
|
|
281
|
+
|
|
282
|
+
`pruneOnce(cfg, kind)` runs after the first write of each kind in a process (guarded by `PRUNED`). It lists `<logDir>/*/<kind>/<prefix>-*.(log|jsonl)` across all date folders (also the pre-0.5.0 names, `<kind>-*`, so old files still age out) and groups files by run id, parsed from `<prefix>-<runId>-<main|wN>.<ext>` (a name without a worker suffix counts as its own run). A run's age is its newest file's mtime.
|
|
283
|
+
|
|
284
|
+
- `maxLogFiles`: of the files outside the current run, the newest `maxLogFiles - currentRunFiles` are kept.
|
|
285
|
+
- `maxRuns`: of the runs other than the current one, the newest `maxRuns - 1` are kept; older runs are marked whole.
|
|
286
|
+
- A file goes if either limit marks it. Then two guards apply: files whose name contains the current run id are never marked (parallel workers share it), and a run whose newest file is younger than `recentRunMinutes` (default 60; 0 = no such guard) is never deleted, because another runner or CI shard may still be writing it. Neither limit is a hard cap.
|
|
287
|
+
- Skipped entirely when `runIdMissing()` (§13): without a shared run id, each worker's "current run" is its own pid-based id, and its siblings' live files would look like an older run.
|
|
288
|
+
|
|
289
|
+
Kinds are described by `pruneSpec(kind)`: folder, the names this logger owns, the run-id regex, and whether files sit in per-worker folders. Besides the four trace kinds there are `table-csv` and `table-json` (folders `attachments/csv`, `attachments/json`), whose files sit one level down in `<runId>-<worker>` folders (`perWorkerFolders`): `listKind` reads the run id from the folder name, so a table heading can never confuse it, and folders or files not named like the logger's own are left alone. Attachments are pruned by `maxRuns` only (`byFileCount: false`): a table count says nothing about runs. Folders left empty are removed upward (worker folder, kind, `attachments`, date), never `logDir` itself. Best effort; all errors swallowed.
|
|
290
|
+
|
|
291
|
+
## 13. Run identity and workers (`config.ts`)
|
|
292
|
+
|
|
293
|
+
- `initRun()` (call from `globalSetup`) sets `process.env.VOID_RUN_ID` and `VOID_RUN_DATE` if absent, so every worker (which inherits env) shares one run id. `getRun()` falls back to a per-process id if unset.
|
|
294
|
+
- `runIdMissing()` is true in a Playwright worker (`TEST_WORKER_INDEX` set) without `VOID_RUN_ID`, meaning `initRun()` was never called in the runner. Then `traceFilePath` writes one stderr warning per process (`RUN_ID_WARNED`, reset by `closeAllFiles()`) and retention is off (§12). A worker cannot fix this itself: env it sets is not seen by its siblings.
|
|
295
|
+
- Only process-based workers are supported. A `worker_threads` thread starts with a copy of its parent's `process.env`, so it gets the same `workerLabel()` and run id, and appends to the same files through its own fds, with no way to tell its lines apart.
|
|
296
|
+
- `workerLabel()` is `w<TEST_WORKER_INDEX>` under Playwright, else `main`. It appears in file names, line prefixes and events.
|
|
297
|
+
|
|
298
|
+
## 14. Playwright integration (`playwright.ts`)
|
|
299
|
+
|
|
300
|
+
`test = base.extend({ voidLog: [fn, { auto: true }] })`: an auto fixture active for every test. Per test it: sets the test context (`setTestContext(titlePath.join(' > '), title)`: the path tags JSONL records, the title feeds `consoleTest`), starts a `captureLogs()` buffer, logs `TEST START`, runs the test, then logs `TEST END | status | ms` (`error.failed` on failure, `info.success` otherwise), and attaches the captured text as `void-log` per `cfg.attach` (`on-failure` default). It also re-exports `expect` and `initRun`. Tests import `test`/`expect` from this entry instead of `@playwright/test`.
|
|
301
|
+
|
|
302
|
+
## 15. Extension points
|
|
303
|
+
|
|
304
|
+
- **Custom sink**: `addSink({ accepts(level,cfg){...}, write(ev,cfg){...} })`. Receives the same `LogEvent`; `ev.color` is ANSI, strip it for plain output.
|
|
305
|
+
- **In-memory capture**: `captureLogs()` returns `{ text(), stop() }`.
|
|
306
|
+
- **New action**: add a method in `LogActions` calling `this.msg(intent, label, m, 'name')`, give `name` a color in `ACTION_FG` (in its intent's hue family), and add it to `FAMILIES` in `palette.test.ts`; that test then checks the color's contrast and distance from the others. For a new intent, add it to `LogIntent` and `INTENT_FG` in `theme.ts`.
|
|
307
|
+
- **New config option**: add to `LogConfig` and `builtinDefaults`; add to the `KEYS` table in `configFile.ts` (validation); optionally an env var in `applyEnv`; document in README; add tests. `LogConfigPatch` updates automatically.
|
|
308
|
+
|
|
309
|
+
## 16. Invariants and gotchas (read before changing code)
|
|
310
|
+
|
|
311
|
+
1. Everything goes through `dispatch`. Do not write to sinks directly.
|
|
312
|
+
2. Do not capture a stack before `accepts()` has approved at least one sink.
|
|
313
|
+
3. `config.ts` <-> `theme.ts`/`configFile.ts` cycles are avoided with `import type`. Keep it that way.
|
|
314
|
+
4. File sinks must stay escape-free: they rely on `stripAnsi` at write time.
|
|
315
|
+
5. `intent === null` means "pre-colored block" (tables). Truncation and `callerColor` logic assume this.
|
|
316
|
+
6. `configure()` must go through `config()` so the config file is loaded first; never touch `CURRENT` directly elsewhere.
|
|
317
|
+
7. A bad config file must keep throwing (no silent fallback). `build()` resets `CURRENT` on failure for this reason.
|
|
318
|
+
8. Files are written synchronously on purpose; do not buffer.
|
|
319
|
+
9. Default console colors changed once, deliberately, in 0.4.0 (per-action colors). Any further change to default colors is a minor version, needs a CHANGELOG entry, and must keep `palette.test.ts` passing. Files never contain colors.
|
|
320
|
+
10. Config is built lazily at first use (not at import). An env var changed between import and the first log call is honored; this is the one intentional difference from 0.1.0.
|
|
321
|
+
11. In regexes written inside template literals, escape backslashes (`\\.`); a past bug came from exactly this.
|
|
322
|
+
12. Retention must never run in a worker without a shared run id (`runIdMissing()`), and must never delete the current run or a run touched within `recentRunMinutes` (unless it is 0).
|
|
323
|
+
13. `configure()` never throws on bad input, in either form; it warns and skips (or reverts). Only a config file fails loudly.
|
|
324
|
+
14. New output options that change line format default to off, so existing console and file output stays byte-identical. The one exception is masking secrets (`redactText`, on since 0.4.0): leaking a secret by default is worse than changing a line.
|
|
325
|
+
15. Free-text masking runs in `dispatch` before truncation and before any sink. Do not move it into a sink, or some outputs would hold the secret.
|
|
326
|
+
|
|
327
|
+
## 17. Differences from the Java logger
|
|
328
|
+
|
|
329
|
+
Dropped: custom themes, `setTheme`, width/arrow settings, file rotation and gzip archives. Changed: caller frames are excluded by path rather than method-name prefix. Added: secret redaction, JSONL sink, custom sinks, in-memory capture, Playwright fixture with attachment, per-worker files, config file, `fg`/`bg`, INFO truncation, file and run retention, console worker/test prefixes, per-action colors, nested and free-text redaction.
|
|
330
|
+
|
|
331
|
+
## 18. Testing map
|
|
332
|
+
|
|
333
|
+
| Behavior | Test file |
|
|
334
|
+
|---|---|
|
|
335
|
+
| Console/file output, levels, caller/chain, redaction, malformed input | `logger.test.ts`, `logger.aggressive.test.ts` |
|
|
336
|
+
| fg/bg/ansi/callerColor combinations across all actions, env parsing | `colors.test.ts` |
|
|
337
|
+
| Discovery order, forms, precedence, `VOID_LOG_CONFIG`, loud failures | `config-file.test.ts` |
|
|
338
|
+
| Truncation text, WARN/DEBUG lines, hint by source | `truncate.test.ts` |
|
|
339
|
+
| Retention by files and runs, protection of current and recent runs, missing run id, foreign files | `retention.test.ts` |
|
|
340
|
+
| consoleWorker/consoleTest prefixes, missing-run-id warning, `configure()` patch and callback validation | `separable.test.ts` |
|
|
341
|
+
| Per-action colors, palette contrast and distance limits | `palette.test.ts` (+ `helpers/color-metrics.ts`) |
|
|
342
|
+
| Nested redaction, free-text masking | `redaction.test.ts` |
|
|
343
|
+
| Table CSV/JSON attachments: names, link line, formatting, redaction, retention | `table-files.test.ts` |
|
|
344
|
+
| Everything at once, by eye, plus automatic file checks | `npm run sweep` (`test/sweep/`; writes to `./logs` like any run, and checks only its own run id's files) |
|
|
345
|
+
| Real Playwright, 2 workers, `.ts` config, `globalSetup` isolation, console prefixes, shared vs split run id | `multi-worker.test.ts` + `fixtures/multi-worker/` |
|
|
346
|
+
| Visual/e2e demos | `*.spec.ts` via `test/playwright.config.ts` |
|
|
347
|
+
|
|
348
|
+
Tests use `resetConfig()` plus `configure({ logDir: tmp, ansi: false, tsFormat: () => 'TS' })` for deterministic output, and spy on `process.stdout.write`. Config-file tests `chdir` into a temp dir so no stray `logger.config.*` leaks in.
|