@zerotal/arch 1.7.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.
Files changed (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +67 -0
  4. package/api-surface.md +283 -0
  5. package/docs/about.md +676 -0
  6. package/docs/admin/actions.md +183 -0
  7. package/docs/admin/auth.md +89 -0
  8. package/docs/admin/dashboard.md +155 -0
  9. package/docs/admin/extending-ui.md +229 -0
  10. package/docs/admin/extending.md +205 -0
  11. package/docs/admin/forms.md +155 -0
  12. package/docs/admin/index.md +144 -0
  13. package/docs/admin/operations.md +224 -0
  14. package/docs/admin/references.md +174 -0
  15. package/docs/admin/resources.md +132 -0
  16. package/docs/admin/structure.md +158 -0
  17. package/docs/admin/tables.md +185 -0
  18. package/docs/admin/testing.md +138 -0
  19. package/docs/ai.md +373 -0
  20. package/docs/application.md +435 -0
  21. package/docs/arch.md +263 -0
  22. package/docs/assets.md +755 -0
  23. package/docs/audit.md +367 -0
  24. package/docs/authentication.md +1000 -0
  25. package/docs/authorization.md +529 -0
  26. package/docs/broadcasting/channels.md +170 -0
  27. package/docs/broadcasting/client.md +108 -0
  28. package/docs/broadcasting/events.md +190 -0
  29. package/docs/broadcasting/index.md +163 -0
  30. package/docs/broadcasting/references.md +84 -0
  31. package/docs/broadcasting/testing.md +132 -0
  32. package/docs/cache.md +387 -0
  33. package/docs/carbon.md +830 -0
  34. package/docs/changelog.md +465 -0
  35. package/docs/client/auth.md +113 -0
  36. package/docs/client/errors.md +139 -0
  37. package/docs/client/files.md +118 -0
  38. package/docs/client/index.md +128 -0
  39. package/docs/client/references.md +58 -0
  40. package/docs/client/requests.md +131 -0
  41. package/docs/client/resilience.md +141 -0
  42. package/docs/client/testing.md +146 -0
  43. package/docs/commands.md +450 -0
  44. package/docs/components.md +2427 -0
  45. package/docs/config-system.md +258 -0
  46. package/docs/container.md +596 -0
  47. package/docs/context.md +579 -0
  48. package/docs/contributing.md +164 -0
  49. package/docs/controllers.md +354 -0
  50. package/docs/conventions.md +466 -0
  51. package/docs/cookies.md +101 -0
  52. package/docs/csrf.md +206 -0
  53. package/docs/database.md +476 -0
  54. package/docs/deployment.md +434 -0
  55. package/docs/devtools.md +1013 -0
  56. package/docs/email-verification.md +320 -0
  57. package/docs/encryption.md +380 -0
  58. package/docs/errors.md +317 -0
  59. package/docs/events.md +420 -0
  60. package/docs/flow/components.md +846 -0
  61. package/docs/flow/decorators.md +458 -0
  62. package/docs/flow/events.md +420 -0
  63. package/docs/flow/forms.md +551 -0
  64. package/docs/flow/index.md +568 -0
  65. package/docs/flow/layouts.md +793 -0
  66. package/docs/flow/lifecycle.md +336 -0
  67. package/docs/flow/pagination.md +402 -0
  68. package/docs/flow/performance.md +262 -0
  69. package/docs/flow/references.md +317 -0
  70. package/docs/flow/routing.md +550 -0
  71. package/docs/flow/testing.md +447 -0
  72. package/docs/getting-started.md +286 -0
  73. package/docs/health.md +269 -0
  74. package/docs/helpers.md +423 -0
  75. package/docs/i18n.md +414 -0
  76. package/docs/index.md +65 -0
  77. package/docs/inertia/build.md +95 -0
  78. package/docs/inertia/devtools.md +197 -0
  79. package/docs/inertia/index.md +267 -0
  80. package/docs/inertia/middleware.md +81 -0
  81. package/docs/inertia/props.md +507 -0
  82. package/docs/inertia/references.md +144 -0
  83. package/docs/inertia/rendering.md +189 -0
  84. package/docs/inertia/ssr.md +102 -0
  85. package/docs/inspirations.md +257 -0
  86. package/docs/lifecycle.md +331 -0
  87. package/docs/lock.md +454 -0
  88. package/docs/logger.md +517 -0
  89. package/docs/media.md +582 -0
  90. package/docs/middleware.md +509 -0
  91. package/docs/migrations.md +529 -0
  92. package/docs/monitor.md +377 -0
  93. package/docs/notifications.md +848 -0
  94. package/docs/orm/casts.md +365 -0
  95. package/docs/orm/factories.md +231 -0
  96. package/docs/orm/index.md +663 -0
  97. package/docs/orm/lifecycle.md +421 -0
  98. package/docs/orm/queries.md +692 -0
  99. package/docs/orm/relationships.md +440 -0
  100. package/docs/orm/serialization.md +251 -0
  101. package/docs/package-development.md +536 -0
  102. package/docs/pagination.md +340 -0
  103. package/docs/password-reset.md +335 -0
  104. package/docs/providers.md +447 -0
  105. package/docs/query-builder.md +465 -0
  106. package/docs/queue.md +549 -0
  107. package/docs/rate-limiting.md +253 -0
  108. package/docs/responses.md +336 -0
  109. package/docs/roles-and-2fa.md +418 -0
  110. package/docs/routing.md +1008 -0
  111. package/docs/scaffolding.md +335 -0
  112. package/docs/scheduler.md +533 -0
  113. package/docs/seeding.md +229 -0
  114. package/docs/session.md +451 -0
  115. package/docs/social.md +524 -0
  116. package/docs/storage.md +622 -0
  117. package/docs/structure.md +230 -0
  118. package/docs/support-policy.md +114 -0
  119. package/docs/telemetry.md +505 -0
  120. package/docs/tenancy.md +608 -0
  121. package/docs/testing/browser.md +153 -0
  122. package/docs/testing/console.md +130 -0
  123. package/docs/testing/database.md +238 -0
  124. package/docs/testing/flow-browser.md +216 -0
  125. package/docs/testing/http.md +441 -0
  126. package/docs/testing/index.md +240 -0
  127. package/docs/testing/mocking.md +410 -0
  128. package/docs/upgrade.md +192 -0
  129. package/docs/validator.md +549 -0
  130. package/docs/view.md +453 -0
  131. package/package.json +60 -0
  132. package/src/bin/mcp.ts +73 -0
  133. package/src/config.ts +63 -0
  134. package/src/errors.ts +27 -0
  135. package/src/index.ts +56 -0
  136. package/src/install/ArchInstallCommand.ts +189 -0
  137. package/src/install/detect.ts +98 -0
  138. package/src/install/guidelines.ts +290 -0
  139. package/src/install/markers.ts +77 -0
  140. package/src/install/mcpConfig.ts +81 -0
  141. package/src/mcp/index.ts +27 -0
  142. package/src/mcp/jsonrpc.ts +133 -0
  143. package/src/mcp/server.ts +307 -0
  144. package/src/mcp/stdio.ts +111 -0
  145. package/src/mcp/types.ts +146 -0
  146. package/src/probe/ArchProbeCommand.ts +48 -0
  147. package/src/probe/sentinel.ts +13 -0
  148. package/src/probe/topics.ts +291 -0
  149. package/src/provider/ArchProvider.ts +101 -0
  150. package/src/tools/_probe.ts +164 -0
  151. package/src/tools/apiSurface.ts +241 -0
  152. package/src/tools/appInfo.ts +88 -0
  153. package/src/tools/baselines.ts +210 -0
  154. package/src/tools/context.ts +37 -0
  155. package/src/tools/doctor.ts +93 -0
  156. package/src/tools/index.ts +50 -0
  157. package/src/tools/logs.ts +265 -0
  158. package/src/tools/routes.ts +133 -0
  159. package/src/tools/schema.ts +119 -0
  160. package/src/tools/searchDocs.ts +345 -0
package/docs/logger.md ADDED
@@ -0,0 +1,517 @@
1
+ ---
2
+ title: Logger
3
+ description: Record structured, multi-channel application logs and auto-capture framework signals like slow queries and request errors.
4
+ ---
5
+
6
+ # Logger
7
+
8
+ `zerotal/logger` is Zerotal's structured logger. It gives you a multi-channel
9
+ `LogManager` (console, file, daily-rotating, stack, null), a global `Log` facade
10
+ you can call anywhere, HTTP access logging via `LoggerMiddleware`, and automatic
11
+ logging of framework signals (slow queries, N+1, failed jobs, auth events,
12
+ request errors) by subscribing to the [FrameworkEvents](/docs/events)
13
+ instrumentation bus.
14
+
15
+ ## Getting Started
16
+
17
+ The logger ships as part of `@zerotal/core` — there is nothing extra to install. Import it from the `zerotal/logger` subpath:
18
+
19
+ ```ts
20
+ import { Log, LogProvider } from "zerotal/logger";
21
+ ```
22
+
23
+ ## Register the provider
24
+
25
+ Add `LogProvider` to the providers array in `bootstrap/providers.ts`:
26
+
27
+ ```ts
28
+ // bootstrap/providers.ts
29
+ import { LogProvider } from "zerotal/logger";
30
+
31
+ const providers = [
32
+ // …your other providers
33
+ LogProvider,
34
+ ];
35
+
36
+ export default providers;
37
+ ```
38
+
39
+ Registering the provider switches on the following, in lifecycle order:
40
+
41
+ - `onRegister` — binds the `LogManager` under the `log` container token as a lazy singleton.
42
+ - `onBooting` — wires the singleton into `LoggerMiddleware` (`setManager`) and registers it via `app.useOnce()`, so request access logging is active with no manual `.use()`.
43
+ - `onBooted` — subscribes to the [FrameworkEvents](/docs/events) bus (slow queries, N+1, failed jobs, scheduled tasks, auth events, request errors).
44
+ - `onStopping` — unsubscribes every handler, so nothing leaks between boots or test suites.
45
+
46
+ > **Note** — `LogProvider` runs in every environment (`web`, `console`,
47
+ > `worker`, `test`, `repl`), so `Log` is available in HTTP requests, commands,
48
+ > queue workers, and the REPL alike.
49
+
50
+ ## Where logs go by default
51
+
52
+ Two places, always, with no config file at all:
53
+
54
+ - **The terminal**, so you can watch what is happening.
55
+ - **A dated file** — `storage/logs/YYYY-MM-DD.log`, one per day, kept for 14 days.
56
+
57
+ They are independent on purpose. Quietening the terminal must not cost you the
58
+ record, because the record is what you read after the process exited, after the
59
+ scrollback rolled over, on a machine nobody was watching. Both are properties of
60
+ the logger rather than channels you route to, so pointing `default` at a file
61
+ channel no longer silences your terminal.
62
+
63
+ > **Note** — The file trail is off when `APP_ENV=test`. The path is relative to
64
+ > the working directory, so a suite that boots an app would otherwise grow a
65
+ > `storage/logs` directory wherever it happened to run. Ask for it explicitly
66
+ > (`file: { path: "./tmp/logs" }`) if a test needs it.
67
+
68
+ ## Configuration
69
+
70
+ Create `config/logging.ts` and use the `LoggingConfig()` helper, which
71
+ deep-merges your options over the defaults so you only specify what you change:
72
+
73
+ ```ts
74
+ // config/logging.ts
75
+ import { LoggingConfig } from "zerotal/logger";
76
+
77
+ export default LoggingConfig({
78
+ // Quiet terminal, full trail on disk, a month of history.
79
+ console: { level: "warn" },
80
+ file: { days: 30 },
81
+ });
82
+ ```
83
+
84
+ | Field | Required | Default | Description |
85
+ | ------------- | -------- | ------------------------- | ------------------------------------------------------------------ |
86
+ | `console` | no | `{ format: "pretty" }` | The terminal sink. `false` silences it. |
87
+ | `file` | no | `./storage/logs`, 14 days | The dated file trail. `false` turns it off. |
88
+ | `default` | no | `"app"` | Name of the channel that `Log.info()` (and friends) also write to. |
89
+ | `channels` | no | `{}` | Named map of extra destinations, discriminated by `driver`. |
90
+ | `slowQueryMs` | no | `1000` | Queries slower than this (ms) are auto-logged at `warn`. |
91
+
92
+ ### The two sinks
93
+
94
+ ```ts
95
+ // config/logging.ts
96
+ export default LoggingConfig({
97
+ console: { level: "info", format: "json" }, // or false
98
+ file: { path: "./var/log/app", days: 30, level: "debug" }, // or false
99
+ });
100
+ ```
101
+
102
+ `console` takes `level` (default `debug`) and `format` (`"pretty"` or `"json"`).
103
+ `file` takes `path`, `days`, and `level` — which also defaults to `debug`, so the
104
+ trail records everything even when the console is filtered. That asymmetry is the
105
+ point: you decide what to _watch_ without deciding what to _keep_.
106
+
107
+ Turning the file off is reasonable in a container that ships stdout to a
108
+ collector:
109
+
110
+ ```ts
111
+ // config/logging.ts
112
+ export default LoggingConfig({ file: false });
113
+ ```
114
+
115
+ ### Named channels
116
+
117
+ Channels are _extra_ destinations layered on top of the two sinks, for routing a
118
+ subsystem somewhere specific:
119
+
120
+ ```ts
121
+ // config/logging.ts
122
+ export default LoggingConfig({
123
+ channels: {
124
+ audit: { driver: "daily", path: "./storage/logs/audit", days: 90 },
125
+ },
126
+ });
127
+
128
+ // elsewhere
129
+ Log.channel("audit").info("permission granted", { userId, ability });
130
+ ```
131
+
132
+ An entry routed to a channel still reaches the console and the trail — with one
133
+ exception that keeps output honest: a channel that already covers a sink
134
+ suppresses that baseline for its own entries. A `console` channel does not print
135
+ twice; a `single`/`daily` channel does not write the same entry to two files. A
136
+ `stack` inherits the coverage of its members.
137
+
138
+ `null` is not an exception. It discards its own writes; it does not suppress the
139
+ record.
140
+
141
+ ## Channel drivers
142
+
143
+ Each channel is a `{ driver: … }` entry under `channels`, discriminated by its
144
+ `driver` field:
145
+
146
+ | Driver | Config fields | Behaviour |
147
+ | --------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
148
+ | `console` | `level?`, `format?` | Writes to `process.stdout`. `format: 'pretty'` (default) for coloured terminal lines; `format: 'json'` for one JSON object per line. |
149
+ | `single` | `level?`, `path` | Appends JSON lines to one file at `path`. Creates parent directories as needed; write errors are swallowed. |
150
+ | `daily` | `level?`, `path`, `days?` | One date-stamped JSON file (`YYYY-MM-DD.log`) per day under `path/`. When `days` is set, files older than that many days are pruned. |
151
+ | `stack` | `level?`, `channels` | Fan-out: each entry is written to every listed child channel simultaneously. |
152
+ | `null` | — | Discards everything. Use in test environments. |
153
+
154
+ > **Warning** — The `daily` driver only prunes old files when you set `days`. Omit
155
+ > `days` and files accumulate indefinitely. A `stack` referencing an unknown child
156
+ > channel throws at construction time.
157
+
158
+ ### Switching destinations per environment
159
+
160
+ The two sinks already cover the common case, so this is for the extras:
161
+
162
+ ```ts
163
+ // config/logging.ts
164
+ export default LoggingConfig({
165
+ // Machine-readable terminal output in production, human-readable locally.
166
+ console: { format: process.env.APP_ENV === "production" ? "json" : "pretty" },
167
+ // Keep a year of audit history in production only.
168
+ ...(process.env.APP_ENV === "production"
169
+ ? { channels: { audit: { driver: "daily", path: "./storage/logs/audit", days: 365 } } }
170
+ : {}),
171
+ });
172
+ ```
173
+
174
+ ## The Log facade
175
+
176
+ `Log` is a static proxy over the `LogManager` singleton. Use it anywhere —
177
+ controllers, services, commands, event listeners:
178
+
179
+ ```ts
180
+ // in a controller or service
181
+ import { Log } from "zerotal/logger";
182
+
183
+ Log.debug("User lookup started", { userId: 42 });
184
+ Log.info("Order created", { orderId: order.id, total: order.total });
185
+ Log.warn("Rate limit close", { ip, remaining: 2 });
186
+ Log.error("Payment failed", { orderId }, err);
187
+ Log.fatal("Database unreachable", { host: dbHost }, err);
188
+ ```
189
+
190
+ Every level shares the same signature:
191
+
192
+ ```ts
193
+ // signature — all five levels
194
+ Log.info(message: string, context?: Record<string, unknown>, err?: unknown): void
195
+ ```
196
+
197
+ - `context` — arbitrary key/value data merged into the structured log entry.
198
+ - `err` — an `Error` instance (message + stack captured) or any value (stringified).
199
+
200
+ ## Targeting a specific channel
201
+
202
+ `Log.channel(name)` returns a `BoundLogger` that writes to the named channel
203
+ instead of the configured default:
204
+
205
+ ```ts
206
+ // in a controller or service
207
+ import { Log } from "zerotal/logger";
208
+
209
+ const auditLog = Log.channel("daily");
210
+ auditLog.info("Admin action", { adminId: user.id, action: "delete_user", targetId });
211
+ ```
212
+
213
+ This lets specific modules write to a dedicated channel without changing
214
+ `default` for the whole application.
215
+
216
+ ## Adding persistent context
217
+
218
+ `Log.withContext(extra)` returns a `BoundLogger` (writing to the default channel)
219
+ that merges `extra` into every entry. Useful in long-running jobs or to tag a
220
+ group of log lines with a shared identifier:
221
+
222
+ ```ts
223
+ // in a queue job
224
+ import { Log } from "zerotal/logger";
225
+
226
+ const logger = Log.withContext({ jobId: job.id, queue: job.queue });
227
+
228
+ logger.info("Job started");
229
+ logger.warn("Retrying step", { step: "charge_card", attempt: 2 });
230
+ logger.info("Job finished", { durationMs });
231
+ // All three entries carry jobId and queue automatically
232
+ ```
233
+
234
+ ## Scopes: which subsystem is talking
235
+
236
+ `Log.scope(name)` returns a `BoundLogger` that tags every entry with the part of
237
+ the system it came from. The console channel renders the tag as a padded column,
238
+ so a boot log reads down the page instead of across it:
239
+
240
+ ```text
241
+ 01:27:48.682 INFO [APP] Application booted {"durationMs":486,"environment":"web","providers":2}
242
+ 01:27:50.310 INFO [FLOW] Compiled 4 page(s), 2 from cache, 3 bind-injected, 8 using runtime
243
+ 01:27:50.314 INFO [ROUTER] Registered 2 static asset routes {"dir":"…/public"}
244
+ 01:27:50.320 INFO [APP] Server listening on http://localhost:3000 {"port":3000}
245
+ ```
246
+
247
+ The tag is a real field, not decoration — file and JSON channels keep it as
248
+ `scope`, so a log file can be filtered down to one subsystem.
249
+
250
+ The framework uses this for its own output. Everything a running Zerotal app
251
+ reports — boot, routing, the Flow compiler, the queue, the scheduler, page
252
+ registry generation — is an ordinary log entry on your configured channels, at a
253
+ level you can filter and in a file you can ship. Nothing writes to the terminal
254
+ behind the logger's back, which means a `daily` channel captures the framework's
255
+ own account of a boot, not just your application's.
256
+
257
+ The one deliberate exception is the CLI itself. `serve --dev`'s banner and its
258
+ `[zerotal:dev]` build/restart lines come from the supervisor process that watches
259
+ your files, not from the application, and they are interactive terminal output
260
+ rather than a record of anything. They stay on stdout.
261
+
262
+ ## How context is rendered
263
+
264
+ On the console, context is written as `key=value` pairs rather than a JSON blob.
265
+ The keys are dimmed so the values carry the eye, and strings stay exactly as you
266
+ passed them:
267
+
268
+ ```text
269
+ 03:19:56.531 INFO [ROUTER] Registered 2 static asset routes dir=C:\Projects\app\public
270
+ 03:19:56.537 INFO [APP] Server listening on http://localhost:3000 port=3000
271
+ ```
272
+
273
+ Literal strings matter more than they sound. `JSON.stringify` escapes every
274
+ backslash, so the same line used to read `{"dir":"C:\\Projects\\app\\public"}` —
275
+ a path you cannot copy, in a format that spends its width on punctuation.
276
+
277
+ A value containing whitespace is quoted (`dir="C:\Program Files\app"`) so the
278
+ pairs stay separable; nothing else is escaped. Nested objects and arrays fall
279
+ back to compact JSON, which is what they are.
280
+
281
+ When the pairs would run past the edge of your terminal, they fold onto a
282
+ continuation line indented under the message instead:
283
+
284
+ ```text
285
+ 07:02:37.464 INFO [FLOW] Compiled 0 page(s), 0 from cache, 3 bind-injected, 8 using runtime
286
+ ↳ compiled=0 cached=0 injected=3 runtime=8 ms=82
287
+ 07:02:37.469 INFO [ROUTER] Registered 2 static asset routes dir=C:\Projects\app\public
288
+ ```
289
+
290
+ A short bag stays on the message line — one event reading as one line is worth
291
+ keeping. A long one is laid out here rather than left to the terminal, which
292
+ would break it mid-pair at whatever column it ran out of room and bury the
293
+ message it belongs to. Lines are filled greedily with whole pairs, so a
294
+ `key=value` is never split across two of them, and only the first continuation
295
+ carries the `↳`. The width comes from the terminal; piped or redirected output
296
+ assumes a page.
297
+
298
+ This applies to the console only. File and JSON channels are unchanged — they
299
+ write the full `LogEntry` as one JSON line, which is what a collector wants.
300
+
301
+ ## Tables: structured data you can actually read
302
+
303
+ Past three or four keys, inline context stops being readable — `{"compiled":0,"cached":0,"injected":3,"runtime":8,"ms":124}`
304
+ is a wall the eye slides off. `table()` logs the same data and asks the console
305
+ to draw it in columns:
306
+
307
+ ```ts
308
+ Log.table("Compile summary", { compiled: 0, cached: 2, injected: 3, runtime: 8, ms: 124 });
309
+ ```
310
+
311
+ ```text
312
+ 02:03:19.263 INFO [FLOW] Compile summary
313
+ ┌──────────┬─────┐
314
+ │ compiled │ 0 │
315
+ │ cached │ 2 │
316
+ │ injected │ 3 │
317
+ │ runtime │ 8 │
318
+ │ ms │ 124 │
319
+ └──────────┴─────┘
320
+ ```
321
+
322
+ Pass a list of objects instead and each key becomes a column, with a header. A
323
+ third argument sets the level, which defaults to `info`:
324
+
325
+ ```ts
326
+ Log.table(
327
+ "Pages rendering through the runtime",
328
+ [
329
+ { page: "ListsPage", blocker: "<Demo> is a component", at: "lists.tsx:83:10" },
330
+ { page: "IndexPage", blocker: "key={d.href} is not static", at: "index.tsx:96:15" },
331
+ ],
332
+ "warn",
333
+ );
334
+ ```
335
+
336
+ ```text
337
+ 02:03:19.263 WARN [FLOW] Pages rendering through the runtime
338
+ ┌───────────┬────────────────────────────┬─────────────────┐
339
+ │ page │ blocker │ at │
340
+ ├───────────┼────────────────────────────┼─────────────────┤
341
+ │ ListsPage │ <Demo> is a component │ lists.tsx:83:10 │
342
+ │ IndexPage │ key={d.href} is not static │ index.tsx:96:15 │
343
+ └───────────┴────────────────────────────┴─────────────────┘
344
+ ```
345
+
346
+ Numeric columns right-align so digits line up, a row missing a key gets a blank
347
+ cell rather than shifting the table, and an over-long cell is truncated instead
348
+ of wrapping into noise. Strings render literally, so a Windows path reads as
349
+ `C:\Projects\app\public` rather than the double-escaped form `JSON.stringify`
350
+ produces.
351
+
352
+ The table is presentation only. The rows are ordinary context, so a `daily` or
353
+ `single` channel writes exactly what any other level method would have written —
354
+ a list ends up under a `rows` key — and a JSON collector never sees a box-drawing
355
+ character. `renderTable(data)` is exported if you want the same lines somewhere
356
+ other than a log.
357
+
358
+ ## Log entry shape
359
+
360
+ Every structured entry written by any channel has this shape:
361
+
362
+ ```ts
363
+ // zerotal/logger — LogEntry
364
+ interface LogEntry {
365
+ level: "debug" | "info" | "warn" | "error" | "fatal";
366
+ channel: string;
367
+ scope?: string; // subsystem tag, e.g. "app", "flow", "queue"
368
+ display?: "table"; // rendering hint for human-facing channels; ignored by JSON
369
+ message: string;
370
+ timestamp: string; // ISO 8601
371
+ app?: string;
372
+ env?: string;
373
+ hostname: string;
374
+ pid: number;
375
+ requestId?: string; // set automatically inside HTTP requests
376
+ context?: Record<string, unknown>;
377
+ error?: string;
378
+ stack?: string;
379
+ }
380
+ ```
381
+
382
+ `requestId` is populated automatically when the log call is made within a request
383
+ context (i.e. inside a middleware or controller) — no manual threading required.
384
+
385
+ ## Log levels
386
+
387
+ Entries below a channel's configured `level` are silently dropped (a channel with
388
+ no `level` defaults to `debug`, letting everything through):
389
+
390
+ | Level | Numeric | Typical use |
391
+ | ------- | ------- | ------------------------------------------------------ |
392
+ | `debug` | 0 | Verbose diagnostic detail; development only |
393
+ | `info` | 1 | Normal operational events |
394
+ | `warn` | 2 | Recoverable issues, rate-limit proximity, slow queries |
395
+ | `error` | 3 | Caught exceptions, request errors |
396
+ | `fatal` | 4 | Unrecoverable failures, process about to exit |
397
+
398
+ ## HTTP access logging
399
+
400
+ `LoggerMiddleware` is registered automatically by `LogProvider`. It times every
401
+ request, sets an `X-Request-Id` response header, and emits one line per request.
402
+ By default it writes a coloured text line straight to `process.stdout`:
403
+
404
+ ```text
405
+ GET /posts ......................................... 200 42ms
406
+ ```
407
+
408
+ Set the format to `json` (via the `LOG_FORMAT` env var) to route the entry through
409
+ the configured `LogManager` channels instead, at `info`/`warn`/`error` by status:
410
+
411
+ ```bash
412
+ # in your project root
413
+ LOG_FORMAT=json bun run start
414
+ ```
415
+
416
+ > **Note** — In text mode the access line goes directly to stdout, not through the
417
+ > default channel. Use `LOG_FORMAT=json` when you want request lines persisted to
418
+ > your file/daily channels. To drop access logging entirely, omit `LogProvider`
419
+ > and register your own middleware.
420
+
421
+ ## Automatic framework signals
422
+
423
+ Once `LogProvider.onBooted()` runs, it subscribes to the
424
+ [FrameworkEvents](/docs/events) bus and logs these signals automatically:
425
+
426
+ | Event | Level | Condition |
427
+ | ----------------------- | -------------- | --------------------------------------------------- |
428
+ | `QueryExecuted` | `warn` | Query duration ≥ `slowQueryMs` |
429
+ | `NPlusOneDetected` | `warn` | An N+1 access pattern is detected |
430
+ | `TransactionRolledBack` | `warn` | A database transaction is rolled back |
431
+ | `MigrationRan` | `info`/`error` | A migration completes (or fails) |
432
+ | `JobRan` | `warn`/`error` | A queued job is retried (`warn`) or fails (`error`) |
433
+ | `MessageFailed` | `error` | A mail send fails |
434
+ | `TaskRan` | `info`/`error` | A scheduled task ran (or failed its run) |
435
+ | `TaskFailed` | `error` | A scheduled task throws |
436
+ | `LoginSucceeded` | `info` | A user authenticates |
437
+ | `LoginFailed` | `warn` | An authentication attempt fails |
438
+ | `LoggedOut` | `info` | A user logs out |
439
+ | `AuthorizationDenied` | `warn` | An ability check is denied |
440
+ | `RequestHandled` | `warn`/`error` | A request returns a 4xx (`warn`) or 5xx (`error`) |
441
+ | `RequestFailed` | `error` | The request pipeline throws |
442
+
443
+ > **Tip** — Requests to internal paths (`/__zerotal/`, `/__flow/`, `/__dev/`)
444
+ > are skipped, so the dev tooling doesn't flood your logs.
445
+
446
+ ## Testing
447
+
448
+ Set your suite up once as described in [Testing](/docs/testing). Asserting on a
449
+ log usually means asserting on a side-effect nobody else observes, so the logger
450
+ gives you a tap.
451
+
452
+ **`LogManager.tap()` receives every entry** after enrichment and before it
453
+ reaches a channel, and returns an unsubscribe function:
454
+
455
+ ```typescript
456
+ // tests/logging/AuditTrail.test.ts
457
+ import { test, expect, afterEach } from "bun:test";
458
+ import { LogManager, type LogEntry } from "zerotal/logger";
459
+
460
+ let stop: (() => void) | undefined;
461
+ afterEach(() => stop?.());
462
+
463
+ test("a failed charge is logged with the order id", async () => {
464
+ const entries: LogEntry[] = [];
465
+ stop = LogManager.tap((entry) => entries.push(entry));
466
+
467
+ await chargeOrder(order); // fails internally
468
+
469
+ const failure = entries.find((e) => e.level === "error");
470
+ expect(failure?.message).toContain("Charge failed");
471
+ expect(failure?.context?.orderId).toBe(order.id);
472
+ });
473
+ ```
474
+
475
+ **Unsubscribe in `afterEach`, not at the end of the test.** Taps are static and
476
+ process-wide, so one left installed keeps collecting entries from every later
477
+ test in the run — the array grows, the assertions get slower, and a `find()`
478
+ starts matching something from a different test.
479
+
480
+ **Silence the logger in the suite** so a passing run stays readable. The `null`
481
+ driver discards everything:
482
+
483
+ ```typescript
484
+ // tests/helpers.ts
485
+ .useConfig({
486
+ logging: { default: "null", channels: { null: { driver: "null" } } },
487
+ })
488
+ ```
489
+
490
+ The tap still fires with `null` as the default channel — it runs before the
491
+ channel write — so you get quiet output and assertable logs at the same time.
492
+
493
+ > **Note** — Assert on `entry.context` rather than on the formatted message.
494
+ > The message is prose and will be reworded; the context is structured data and
495
+ > is what a log search actually queries.
496
+
497
+ ## References
498
+
499
+ `Log` and every `BoundLogger` returned by `channel()`/`withContext()` share the
500
+ same surface:
501
+
502
+ | Method | Signature | Description |
503
+ | ------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------- |
504
+ | `debug` | `(message: string, context?: Record<string, unknown>, err?: unknown) => void` | Write a `debug`-level entry to the bound channel. |
505
+ | `info` | `(message: string, context?: Record<string, unknown>, err?: unknown) => void` | Write an `info`-level entry. |
506
+ | `warn` | `(message: string, context?: Record<string, unknown>, err?: unknown) => void` | Write a `warn`-level entry. |
507
+ | `error` | `(message: string, context?: Record<string, unknown>, err?: unknown) => void` | Write an `error`-level entry. |
508
+ | `fatal` | `(message: string, context?: Record<string, unknown>, err?: unknown) => void` | Write a `fatal`-level entry. |
509
+ | `channel` | `(name: string) => BoundLogger` | Bind a logger to a specific channel (`Log` only). |
510
+ | `withContext` | `(extra: Record<string, unknown>) => BoundLogger` | Bind a logger that merges `extra` into every entry (`Log` only). |
511
+
512
+ ## Next steps
513
+
514
+ - [Events](/docs/events) — the FrameworkEvents bus the logger subscribes to.
515
+ - [DevTools](/docs/devtools) — per-request log capture in the debug panel.
516
+ - [Commands](/docs/commands) — using `Log` inside scheduled commands.
517
+ - [Telemetry](/docs/telemetry) — distributed tracing alongside structured logs.