@logtape/logtape 2.1.0-dev.517 → 2.1.0-dev.518

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logtape/logtape",
3
- "version": "2.1.0-dev.517+0bef54f3",
3
+ "version": "2.1.0-dev.518+08c09b1d",
4
4
  "description": "Unobtrusive logging library with zero dependencies—library-first design for Deno/Node.js/Bun/browsers/edge",
5
5
  "keywords": [
6
6
  "logging",
@@ -66,8 +66,17 @@
66
66
  },
67
67
  "sideEffects": false,
68
68
  "files": [
69
- "dist/"
69
+ "dist/",
70
+ "skills/"
70
71
  ],
72
+ "agents": {
73
+ "skills": [
74
+ {
75
+ "name": "logtape",
76
+ "path": "./skills/logtape"
77
+ }
78
+ ]
79
+ },
71
80
  "devDependencies": {
72
81
  "@alinea/suite": "^0.6.3",
73
82
  "@std/assert": "npm:@jsr/std__assert@^1.0.13",
@@ -0,0 +1,670 @@
1
+ ---
2
+ name: logtape
3
+ description: >
4
+ Use this skill when writing any code that uses LogTape for logging in
5
+ JavaScript or TypeScript. Covers getting loggers, the structured message
6
+ syntax, configuration, library author rules, context, lazy evaluation,
7
+ testing, and common mistakes to avoid. Trigger whenever the user is
8
+ adding logging to a project, debugging log output, or integrating LogTape
9
+ with a framework.
10
+ license: MIT
11
+ ---
12
+
13
+ LogTape skill for AI coding assistants
14
+ ======================================
15
+
16
+ LogTape is a zero-dependency, library-first logging framework for JavaScript
17
+ and TypeScript that works across Deno, Node.js, Bun, browsers, and edge
18
+ functions.
19
+
20
+ Full documentation: <https://logtape.org/>
21
+
22
+
23
+ Getting a logger
24
+ ----------------
25
+
26
+ Always use `getLogger()` with an **array** category to enable hierarchical
27
+ filtering. The hierarchy works like a path: a parent category's configuration
28
+ applies to all children.
29
+
30
+ ~~~~ typescript
31
+ import { getLogger } from "@logtape/logtape";
32
+
33
+ // Good: array form enables hierarchical filtering
34
+ const logger = getLogger(["my-app", "users", "auth"]);
35
+
36
+ // Acceptable shorthand for a single-segment category
37
+ const rootLogger = getLogger("my-app");
38
+ ~~~~
39
+
40
+ Choose category segments that reflect your module structure so that operators
41
+ can selectively enable/disable logging per subsystem.
42
+
43
+ Use `logger.getChild("sub")` to derive a child logger without repeating the
44
+ full category:
45
+
46
+ ~~~~ typescript
47
+ const dbLogger = logger.getChild("database");
48
+ // category = ["my-app", "users", "auth", "database"]
49
+ ~~~~
50
+
51
+ See <https://logtape.org/manual/categories.md> for details.
52
+
53
+
54
+ Structured messages
55
+ -------------------
56
+
57
+ Use **named placeholders** with a properties object. This keeps messages
58
+ parseable and properties searchable:
59
+
60
+ ~~~~ typescript
61
+ // Correct: structured message with named placeholders
62
+ logger.info("User {userId} logged in from {ip}", { userId, ip });
63
+
64
+ // Correct: structured data without a message
65
+ logger.info({ userId, ip, action: "login" });
66
+
67
+ // Nested property access (since 1.2.0)
68
+ logger.info("Name: {user.name}", { user: { name: "Alice" } });
69
+ ~~~~
70
+
71
+ Template literal syntax is available for quick debug logging but does **not**
72
+ produce structured data:
73
+
74
+ ~~~~ typescript
75
+ // Template literal: convenient but not structured
76
+ logger.debug`User ${userId} logged in`;
77
+ ~~~~
78
+
79
+ See <https://logtape.org/manual/struct.md> for full structured logging details.
80
+
81
+
82
+ Severity levels
83
+ ---------------
84
+
85
+ LogTape provides six levels, from most to least verbose:
86
+
87
+ | Level | Use for |
88
+ | --------- | ----------------------------------------------------- |
89
+ | `trace` | Very fine-grained diagnostic output |
90
+ | `debug` | Developer-facing diagnostic messages |
91
+ | `info` | Normal operational events (startup, shutdown, etc.) |
92
+ | `warning` | Unexpected but recoverable situations |
93
+ | `error` | Errors that affect a single operation |
94
+ | `fatal` | Unrecoverable errors that require process termination |
95
+
96
+ Use the lowest appropriate level. Reserve `error`/`fatal` for actual failures;
97
+ avoid using them for expected conditions like validation errors.
98
+
99
+ See <https://logtape.org/manual/levels.md> for details.
100
+
101
+
102
+ Configuration
103
+ -------------
104
+
105
+ ### Async configuration (most common)
106
+
107
+ `configure()` is **application-only**. It must be `await`ed and called
108
+ **exactly once** at startup (e.g., in your entry point):
109
+
110
+ ~~~~ typescript
111
+ import { configure, getConsoleSink } from "@logtape/logtape";
112
+
113
+ await configure({
114
+ sinks: {
115
+ console: getConsoleSink(),
116
+ },
117
+ loggers: [
118
+ {
119
+ category: "my-app",
120
+ lowestLevel: "debug",
121
+ sinks: ["console"],
122
+ },
123
+ ],
124
+ });
125
+ ~~~~
126
+
127
+ ### Synchronous configuration
128
+
129
+ Use `configureSync()` when you cannot use `await` (e.g., top-level in CommonJS,
130
+ or in a synchronous startup path):
131
+
132
+ ~~~~ typescript
133
+ import { configureSync, getConsoleSink } from "@logtape/logtape";
134
+
135
+ configureSync({
136
+ sinks: {
137
+ console: getConsoleSink(),
138
+ },
139
+ loggers: [
140
+ {
141
+ category: "my-app",
142
+ lowestLevel: "debug",
143
+ sinks: ["console"],
144
+ },
145
+ ],
146
+ });
147
+ ~~~~
148
+
149
+ > **Limitation:** `configureSync()` cannot use `AsyncDisposable` sinks such as
150
+ > `getStreamSink()` or sinks created with `fromAsyncSink()`.
151
+
152
+ ### Key rules
153
+
154
+ - `configure()` returns a `Promise`; always `await` it.
155
+ - `configureSync()` returns `void`; do not `await` it.
156
+ - Call either one **once**. Calling again without resetting first throws
157
+ `ConfigError`.
158
+ - Do **not** mix async and sync: if you used `configure()`, reset with
159
+ `await reset()`; if you used `configureSync()`, reset with `resetSync()`.
160
+ - For tests, call `await reset()` (or `resetSync()`) in teardown.
161
+
162
+ ### Framework-specific patterns
163
+
164
+ - **React**: configure **before** `createRoot()`.
165
+ - **Vue**: configure **before** `app.mount()`.
166
+ - **Next.js**: use *instrumentation.js* (server) or
167
+ *instrumentation-client.js* (client).
168
+ - **SvelteKit**: configure in *hooks.server.ts*.
169
+
170
+ See <https://logtape.org/manual/config.md> for all options.
171
+
172
+
173
+ Library author rule
174
+ -------------------
175
+
176
+ **Never call `configure()` or `configureSync()` in library code.** Libraries
177
+ should only call `getLogger()` and log messages. The application that depends
178
+ on your library decides how (or whether) to configure sinks and levels.
179
+
180
+ ~~~~ typescript
181
+ // my-lib/src/client.ts — library code
182
+ import { getLogger } from "@logtape/logtape";
183
+
184
+ // Good: just get a logger, don't configure
185
+ const logger = getLogger(["my-lib", "client"]);
186
+
187
+ export function fetchData(url: string) {
188
+ logger.debug("Fetching {url}", { url });
189
+ // ...
190
+ }
191
+ ~~~~
192
+
193
+ If your library wraps other LogTape-using libraries, use `withCategoryPrefix()`
194
+ to nest their logs under your category:
195
+
196
+ ~~~~ typescript
197
+ import { withCategoryPrefix } from "@logtape/logtape";
198
+
199
+ export function myOperation() {
200
+ return withCategoryPrefix(["my-lib"], () => {
201
+ // Logs from inner libraries appear as ["my-lib", ...their-category]
202
+ innerLib.doWork();
203
+ });
204
+ }
205
+ ~~~~
206
+
207
+ > **Note:** `withCategoryPrefix()` requires `contextLocalStorage` to be
208
+ > configured by the application.
209
+
210
+ See <https://logtape.org/manual/library.md> for the full guide.
211
+
212
+
213
+ Context with `with()` and lazy evaluation
214
+ -----------------------------------------
215
+
216
+ ### Adding explicit context
217
+
218
+ Use `logger.with()` to create a child logger that attaches properties to every
219
+ subsequent log call:
220
+
221
+ ~~~~ typescript
222
+ const reqLogger = logger.with({ requestId, userId });
223
+ reqLogger.info("Processing order {orderId}", { orderId });
224
+ // Log record will contain requestId, userId, AND orderId
225
+ ~~~~
226
+
227
+ ### Implicit context (request tracing)
228
+
229
+ Use `withContext()` to propagate context across an entire call stack without
230
+ threading loggers manually. Requires `contextLocalStorage` in configuration:
231
+
232
+ ~~~~ typescript
233
+ import { configure, getConsoleSink, withContext } from "@logtape/logtape";
234
+ import { AsyncLocalStorage } from "node:async_hooks";
235
+
236
+ await configure({
237
+ sinks: { console: getConsoleSink() },
238
+ loggers: [{ category: "app", sinks: ["console"] }],
239
+ contextLocalStorage: new AsyncLocalStorage(),
240
+ });
241
+
242
+ function handleRequest(req: Request) {
243
+ withContext({ requestId: crypto.randomUUID() }, () => {
244
+ // All logs inside this callback automatically include requestId
245
+ processRequest(req);
246
+ });
247
+ }
248
+ ~~~~
249
+
250
+ > **Note:** Implicit contexts are not available in browsers yet.
251
+
252
+ ### Lazy evaluation
253
+
254
+ Wrap expensive computations with `lazy()` so they only run when the level is
255
+ enabled:
256
+
257
+ ~~~~ typescript
258
+ import { getLogger, lazy } from "@logtape/logtape";
259
+
260
+ const logger = getLogger(["my-app"]);
261
+ logger.debug("System state: {state}", {
262
+ state: lazy(() => JSON.stringify(getExpensiveState())),
263
+ });
264
+ ~~~~
265
+
266
+ For structured data, pass a callback as the second argument:
267
+
268
+ ~~~~ typescript
269
+ logger.debug("Diagnostics", () => ({
270
+ heap: process.memoryUsage().heapUsed,
271
+ uptime: process.uptime(),
272
+ }));
273
+ ~~~~
274
+
275
+ For **async** lazy evaluation, pass an async callback and `await` the result:
276
+
277
+ ~~~~ typescript
278
+ await logger.info("User details", async () => ({
279
+ user: await fetchUserDetails(),
280
+ }));
281
+ ~~~~
282
+
283
+ For multiple expensive log calls, use `isEnabledFor()`:
284
+
285
+ ~~~~ typescript
286
+ if (logger.isEnabledFor("debug")) {
287
+ const snapshot = await captureExpensiveSnapshot();
288
+ logger.debug("Snapshot: {data}", { data: snapshot });
289
+ }
290
+ ~~~~
291
+
292
+ See <https://logtape.org/manual/lazy.md> and
293
+ <https://logtape.org/manual/contexts.md> for details.
294
+
295
+
296
+ Logging errors
297
+ --------------
298
+
299
+ Pass `Error` objects directly to `error()` or `fatal()`. You can attach
300
+ extra properties as a second argument:
301
+
302
+ ~~~~ typescript
303
+ try {
304
+ await riskyOperation();
305
+ } catch (error) {
306
+ logger.error(error, { operation: "riskyOperation", userId });
307
+ }
308
+ ~~~~
309
+
310
+
311
+ Sinks and formatters
312
+ --------------------
313
+
314
+ ### Built-in sinks
315
+
316
+ ~~~~ typescript
317
+ import {
318
+ configure,
319
+ getConsoleSink,
320
+ getStreamSink,
321
+ } from "@logtape/logtape";
322
+
323
+ await configure({
324
+ sinks: {
325
+ console: getConsoleSink(),
326
+ stderr: getStreamSink(Writable.toWeb(process.stderr)),
327
+ },
328
+ loggers: [{ category: "app", sinks: ["console"] }],
329
+ });
330
+ ~~~~
331
+
332
+ ### Sink filters
333
+
334
+ Use `withFilter()` to route different levels to different sinks:
335
+
336
+ ~~~~ typescript
337
+ import { configure, getConsoleSink, withFilter } from "@logtape/logtape";
338
+
339
+ await configure({
340
+ sinks: {
341
+ errorsOnly: withFilter(getConsoleSink(), "error"),
342
+ allLevels: getConsoleSink(),
343
+ },
344
+ loggers: [
345
+ { category: "app", sinks: ["allLevels", "errorsOnly"] },
346
+ ],
347
+ });
348
+ ~~~~
349
+
350
+ ### Formatters
351
+
352
+ ~~~~ typescript
353
+ import {
354
+ configure,
355
+ getAnsiColorFormatter,
356
+ getConsoleSink,
357
+ getJsonLinesFormatter,
358
+ } from "@logtape/logtape";
359
+
360
+ // Pretty ANSI-colored output for development
361
+ getConsoleSink({ formatter: getAnsiColorFormatter() });
362
+
363
+ // JSON Lines for production / log aggregation
364
+ getConsoleSink({ formatter: getJsonLinesFormatter() });
365
+ ~~~~
366
+
367
+ For even nicer development output, use `@logtape/pretty`:
368
+
369
+ ~~~~ typescript
370
+ import { getPrettyFormatter } from "@logtape/pretty";
371
+
372
+ getConsoleSink({ formatter: getPrettyFormatter() });
373
+ ~~~~
374
+
375
+ ### Disposal
376
+
377
+ Non-blocking sinks and stream sinks hold resources. In **edge functions** or
378
+ short-lived processes, explicitly dispose before exit:
379
+
380
+ ~~~~ typescript
381
+ import { dispose } from "@logtape/logtape";
382
+
383
+ // At shutdown
384
+ await dispose();
385
+ ~~~~
386
+
387
+ See <https://logtape.org/manual/sinks.md> and
388
+ <https://logtape.org/manual/formatters.md> for details.
389
+
390
+
391
+ Data redaction
392
+ --------------
393
+
394
+ Use `@logtape/redaction` to prevent sensitive data from reaching log output.
395
+
396
+ ### Pattern-based redaction (scans formatted text)
397
+
398
+ Wrap a formatter with `redactByPattern()` to catch data like emails, credit
399
+ card numbers, or JWTs anywhere in the log output:
400
+
401
+ ~~~~ typescript
402
+ import { defaultConsoleFormatter, getConsoleSink } from "@logtape/logtape";
403
+ import {
404
+ EMAIL_ADDRESS_PATTERN,
405
+ JWT_PATTERN,
406
+ redactByPattern,
407
+ } from "@logtape/redaction";
408
+
409
+ const sink = getConsoleSink({
410
+ formatter: redactByPattern(defaultConsoleFormatter, [
411
+ EMAIL_ADDRESS_PATTERN,
412
+ JWT_PATTERN,
413
+ ]),
414
+ });
415
+ ~~~~
416
+
417
+ Built-in patterns: `EMAIL_ADDRESS_PATTERN`, `CREDIT_CARD_NUMBER_PATTERN`,
418
+ `JWT_PATTERN`, `US_SSN_PATTERN`, `KR_RRN_PATTERN`.
419
+
420
+ ### Field-based redaction (removes/replaces properties by name)
421
+
422
+ Wrap a sink with `redactByField()` to strip sensitive fields from structured
423
+ log data before it reaches the sink:
424
+
425
+ ~~~~ typescript
426
+ import { getConsoleSink } from "@logtape/logtape";
427
+ import { redactByField } from "@logtape/redaction";
428
+
429
+ // Uses DEFAULT_REDACT_FIELDS (password, secret, token, etc.)
430
+ const sink = redactByField(getConsoleSink());
431
+
432
+ // Or customize with replacement instead of removal
433
+ const sink2 = redactByField(getConsoleSink(), {
434
+ fieldPatterns: [/password/i, /secret/i, /api[-_]?key/i],
435
+ action: () => "[REDACTED]",
436
+ });
437
+ ~~~~
438
+
439
+ ### Combining both for maximum security
440
+
441
+ ~~~~ typescript
442
+ const sink = redactByField(
443
+ getConsoleSink({
444
+ formatter: redactByPattern(defaultConsoleFormatter, [
445
+ EMAIL_ADDRESS_PATTERN,
446
+ JWT_PATTERN,
447
+ ]),
448
+ }),
449
+ );
450
+ ~~~~
451
+
452
+ See <https://logtape.org/manual/redaction.md> for details.
453
+
454
+
455
+ Adaptors for existing loggers
456
+ -----------------------------
457
+
458
+ If the project already uses winston, Pino, or log4js, use an adaptor instead
459
+ of `configure()`:
460
+
461
+ ~~~~ typescript
462
+ import { install } from "@logtape/adaptor-winston";
463
+ import winston from "winston";
464
+
465
+ const winstonLogger = winston.createLogger({ /* ... */ });
466
+ install(winstonLogger);
467
+ // All LogTape logs now route through winston
468
+ ~~~~
469
+
470
+ Available: `@logtape/adaptor-winston`, `@logtape/adaptor-pino`,
471
+ `@logtape/adaptor-log4js`.
472
+
473
+ See <https://logtape.org/manual/adaptors.md> for details.
474
+
475
+
476
+ Testing
477
+ -------
478
+
479
+ For tests, configure a buffer sink and assert on collected records:
480
+
481
+ ~~~~ typescript
482
+ import { configure, getLogger, reset, type LogRecord } from "@logtape/logtape";
483
+
484
+ const buffer: LogRecord[] = [];
485
+
486
+ await configure({
487
+ sinks: { buffer: buffer.push.bind(buffer) },
488
+ loggers: [{ category: "test", sinks: ["buffer"] }],
489
+ });
490
+
491
+ const logger = getLogger(["test"]);
492
+ logger.info("hello");
493
+
494
+ // Assert
495
+ assert(buffer.length === 1);
496
+ assert(buffer[0].level === "info");
497
+
498
+ await reset();
499
+ ~~~~
500
+
501
+ If using `configureSync()`, reset with `resetSync()`:
502
+
503
+ ~~~~ typescript
504
+ import { configureSync, resetSync } from "@logtape/logtape";
505
+
506
+ configureSync({ /* ... */ });
507
+ // ... test ...
508
+ resetSync();
509
+ ~~~~
510
+
511
+ Always call `reset()` / `resetSync()` in test teardown so that each test can
512
+ configure independently.
513
+
514
+ See <https://logtape.org/manual/testing.md> for more patterns.
515
+
516
+
517
+ Available packages
518
+ ------------------
519
+
520
+ ### Sink packages
521
+
522
+ | Package | Description |
523
+ | --------------------------- | ---------------------- |
524
+ | `@logtape/file` | File and rotating file |
525
+ | `@logtape/otel` | OpenTelemetry |
526
+ | `@logtape/sentry` | Sentry |
527
+ | `@logtape/syslog` | Syslog |
528
+ | `@logtape/cloudwatch-logs` | AWS CloudWatch Logs |
529
+ | `@logtape/windows-eventlog` | Windows Event Log |
530
+
531
+ ### Framework integrations
532
+
533
+ | Package | Description |
534
+ | ---------------------- | ------------------------- |
535
+ | `@logtape/express` | Express HTTP logging |
536
+ | `@logtape/fastify` | Fastify HTTP logging |
537
+ | `@logtape/hono` | Hono HTTP logging |
538
+ | `@logtape/koa` | Koa HTTP logging |
539
+ | `@logtape/elysia` | Elysia HTTP logging |
540
+ | `@logtape/drizzle-orm` | Drizzle ORM query logging |
541
+
542
+ ### Formatters
543
+
544
+ | Package | Description |
545
+ | -------------------- | ------------------------ |
546
+ | `@logtape/pretty` | Pretty console formatter |
547
+ | `@logtape/redaction` | Sensitive data redaction |
548
+
549
+ ### Adaptors (for existing loggers)
550
+
551
+ | Package | Description |
552
+ | -------------------------- | --------------------- |
553
+ | `@logtape/adaptor-pino` | Pino compatibility |
554
+ | `@logtape/adaptor-winston` | Winston compatibility |
555
+ | `@logtape/adaptor-log4js` | log4js compatibility |
556
+
557
+
558
+ Common mistakes
559
+ ---------------
560
+
561
+ ### Using template literals for structured data
562
+
563
+ ~~~~ typescript
564
+ // WRONG: template literals don't produce structured data
565
+ logger.info`User ${userId} performed ${action}`;
566
+
567
+ // CORRECT: use placeholders for structured, searchable logs
568
+ logger.info("User {userId} performed {action}", { userId, action });
569
+ ~~~~
570
+
571
+ ### String concatenation
572
+
573
+ ~~~~ typescript
574
+ // WRONG: loses structure, always evaluates
575
+ logger.info("User " + userId + " logged in from " + ip);
576
+
577
+ // CORRECT
578
+ logger.info("User {userId} logged in from {ip}", { userId, ip });
579
+ ~~~~
580
+
581
+ ### Calling `configure()` in library code
582
+
583
+ ~~~~ typescript
584
+ // WRONG: library must never configure LogTape
585
+ import { configure } from "@logtape/logtape";
586
+ await configure({ /* ... */ }); // Don't do this in a library!
587
+
588
+ // CORRECT: just use getLogger()
589
+ import { getLogger } from "@logtape/logtape";
590
+ const logger = getLogger(["my-lib"]);
591
+ ~~~~
592
+
593
+ ### Forgetting to `await configure()`
594
+
595
+ ~~~~ typescript
596
+ // WRONG: configure() returns a Promise; without await, logging may
597
+ // not work when you expect it to
598
+ configure({ /* ... */ });
599
+
600
+ // CORRECT
601
+ await configure({ /* ... */ });
602
+
603
+ // Or use the synchronous variant if you can't await
604
+ configureSync({ /* ... */ });
605
+ ~~~~
606
+
607
+ ### Mixing async/sync configure and reset
608
+
609
+ ~~~~ typescript
610
+ // WRONG: mismatched pair causes errors
611
+ await configure({ /* ... */ });
612
+ resetSync(); // Can't sync-reset an async config!
613
+
614
+ // CORRECT: match configure and reset variants
615
+ await configure({ /* ... */ });
616
+ await reset();
617
+
618
+ // Or:
619
+ configureSync({ /* ... */ });
620
+ resetSync();
621
+ ~~~~
622
+
623
+ ### Using `console.log` alongside LogTape
624
+
625
+ ~~~~ typescript
626
+ // WRONG: bypasses LogTape's filtering, formatting, and routing
627
+ console.log("User logged in:", userId);
628
+
629
+ // CORRECT: use the logger so the message goes through configured sinks
630
+ logger.info("User {userId} logged in", { userId });
631
+ ~~~~
632
+
633
+ ### Flat categories
634
+
635
+ ~~~~ typescript
636
+ // WRONG: single flat string prevents granular filtering
637
+ const logger = getLogger("myapp");
638
+
639
+ // CORRECT: hierarchical array enables per-module control
640
+ const logger = getLogger(["myapp", "auth", "oauth"]);
641
+ ~~~~
642
+
643
+ ### Logging sensitive data without redaction
644
+
645
+ ~~~~ typescript
646
+ // WRONG: password ends up in log files
647
+ logger.info("Login attempt for {email} with {password}", { email, password });
648
+
649
+ // CORRECT: never log secrets; use @logtape/redaction if needed
650
+ logger.info("Login attempt for {email}", { email });
651
+ ~~~~
652
+
653
+
654
+ Best practices summary
655
+ ----------------------
656
+
657
+ - One `configure()` or `configureSync()` call at startup
658
+ - Always `await configure()`; never `await configureSync()`
659
+ - Match reset variant to configure variant
660
+ - Hierarchical array categories (e.g., `["app", "module", "sub"]`)
661
+ - Structured messages with named placeholders, not string interpolation
662
+ - `lazy()` or callback for expensive computations
663
+ - `logger.with()` for request-scoped context
664
+ - `withContext()` + `AsyncLocalStorage` for implicit context propagation
665
+ - `error`/`fatal` only for real failures
666
+ - Libraries: never configure, just `getLogger()`
667
+ - Tests: `reset()` / `resetSync()` in teardown, buffer sink for assertions
668
+ - Never log sensitive data (passwords, tokens, PII)
669
+ - Use `dispose()` / `disposeSync()` in edge functions or short-lived
670
+ processes