@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
@@ -0,0 +1,505 @@
1
+ ---
2
+ title: Telemetry
3
+ description: Trace requests and operations across your app with OTLP-compatible spans and zero SDK dependencies.
4
+ ---
5
+
6
+ # Telemetry
7
+
8
+ Distributed tracing for Zerotal applications: a self-contained tracer with an
9
+ OTLP-compatible span model, `AsyncLocalStorage` context propagation, and pluggable
10
+ exporters — no OpenTelemetry SDK dependency. Wrap any operation in `withSpan()` and
11
+ it becomes a timed, attributed node in a trace.
12
+
13
+ ## Getting Started
14
+
15
+ ```bash
16
+ # in your project root
17
+ bun add @zerotal/telemetry
18
+ ```
19
+
20
+ ## Register the provider
21
+
22
+ Add `TelemetryProvider` to the providers array in `bootstrap/providers.ts`. List it
23
+ **before** other providers so the global tracer is ready when the rest of the
24
+ application boots:
25
+
26
+ ```typescript
27
+ // bootstrap/providers.ts
28
+ import { TelemetryProvider } from "@zerotal/telemetry";
29
+ import { DatabaseProvider } from "@zerotal/orm";
30
+
31
+ const providers = [
32
+ TelemetryProvider,
33
+ DatabaseProvider,
34
+ // …your other providers
35
+ ];
36
+
37
+ export default providers;
38
+ ```
39
+
40
+ Registering the provider switches on the following hooks (in lifecycle order):
41
+
42
+ - `onRegister` — binds the `Tracer` as a lazy singleton under the `telemetry`
43
+ container key, built from `config/telemetry.ts`.
44
+ - `onBooted` — resolves that tracer and installs it as the global tracer used by
45
+ `withSpan()` and `currentSpan()`.
46
+ - `onStopping` — shuts the tracer down (flushing the exporter) and clears the
47
+ global reference, so nothing leaks between boots or test suites.
48
+
49
+ > **Note** — The provider is active in the `web`, `console`, `test`, and `repl`
50
+ > environments. Until `onBooted` runs, `withSpan()` falls back to a no-op span, so
51
+ > early call sites never throw.
52
+
53
+ ## Configuration
54
+
55
+ Create `config/telemetry.ts`. Use the `TelemetryConfig()` helper (or `satisfies
56
+ TelemetryConfigShape`) so every field stays type-checked. The `otlp` block is only
57
+ read when `exporter` is `'otlp'`:
58
+
59
+ ```typescript
60
+ // config/telemetry.ts
61
+ import { TelemetryConfig } from "@zerotal/telemetry";
62
+ import { env } from "zerotal";
63
+
64
+ export default TelemetryConfig({
65
+ exporter: "noop", // 'noop' | 'console' | 'otlp'
66
+ serviceName: env("APP_NAME", "zerotal-app"),
67
+ serviceVersion: env("APP_VERSION", "0.0.0"),
68
+ minDurationMs: 0, // drop spans shorter than this (0 = keep all)
69
+
70
+ // Only used when exporter is 'otlp':
71
+ otlp: {
72
+ endpoint: env("OTLP_ENDPOINT", "http://localhost:4318/v1/traces"),
73
+ headers: { "x-honeycomb-team": env("HONEYCOMB_API_KEY", "") },
74
+ },
75
+ });
76
+ ```
77
+
78
+ | Field | Required | Default | Description |
79
+ | ---------------- | -------- | ----------------------------------- | ------------------------------------------------------------------ |
80
+ | `exporter` | no | `'noop'` | Where spans go: `'noop'`, `'console'`, or `'otlp'`. |
81
+ | `serviceName` | no | `'zerotal-app'` | Service name sent as a resource attribute. |
82
+ | `serviceVersion` | no | `'0.0.0'` | Service version sent as a resource attribute. |
83
+ | `minDurationMs` | no | `0` | Drop spans whose callback ran for fewer ms than this (`0` = keep). |
84
+ | `otlp.endpoint` | no | `'http://localhost:4318/v1/traces'` | OTLP HTTP/JSON endpoint. Only read when `exporter` is `'otlp'`. |
85
+ | `otlp.headers` | no | `{}` | Extra request headers, e.g. an API key. |
86
+
87
+ ### Which exporter should I use?
88
+
89
+ - **`noop`** — production default until you wire up a backend, and the value in
90
+ tests. Spans are created but discarded, so call sites stay cheap.
91
+ - **`console`** — local development. Prints readable span data to stdout so you can
92
+ see traces without a collector.
93
+ - **`otlp`** — staging and production. Ships spans to any OTLP HTTP/JSON backend
94
+ (Honeycomb, Grafana Tempo, Jaeger, an OpenTelemetry Collector, …).
95
+
96
+ ## Basic usage
97
+
98
+ `withSpan()` is the primary call-site API. It uses the global tracer registered by
99
+ `TelemetryProvider`; if no tracer is registered the callback still runs with a no-op
100
+ span — no error is thrown, no import guard needed.
101
+
102
+ ```typescript
103
+ // in a service or controller
104
+ import { withSpan } from "@zerotal/telemetry";
105
+
106
+ async function processOrder(id: string) {
107
+ return withSpan("process-order", async (span) => {
108
+ span.setAttribute("order.id", id);
109
+ span.setAttribute("order.source", "web");
110
+
111
+ const order = await Order.findOrFail(id);
112
+ await notifyWarehouse(order);
113
+ });
114
+ }
115
+ ```
116
+
117
+ The span is automatically:
118
+
119
+ - Ended when the callback resolves or throws.
120
+ - Exported to the configured backend.
121
+ - Set to `status: ok` on success.
122
+ - Set to `status: error` with an exception event on throw (the error is re-thrown).
123
+
124
+ Pass `kind` and initial `attributes` as a third options argument:
125
+
126
+ ```typescript
127
+ // in a service
128
+ await withSpan(
129
+ "http.outbound",
130
+ async (span) => {
131
+ /* … */
132
+ },
133
+ { kind: "client", attributes: { "http.method": "POST" } },
134
+ );
135
+ ```
136
+
137
+ ## Working with spans
138
+
139
+ The `Span` passed to your callback is fluent — every mutator returns the span.
140
+
141
+ ### Attributes
142
+
143
+ Attribute values must be `string | number | boolean`.
144
+
145
+ ```typescript
146
+ // inside a withSpan() callback
147
+ span.setAttribute("db.table", "orders"); // single
148
+ span.setAttributes({ "db.rows": 5, "cache.hit": true }); // batch
149
+ ```
150
+
151
+ ### Status
152
+
153
+ The default status is `'unset'`; the exporter treats it the same as `'ok'`.
154
+ `setStatus()` accepts only `'ok'` or `'error'`:
155
+
156
+ ```typescript
157
+ // inside a withSpan() callback
158
+ span.setStatus("ok");
159
+ span.setStatus("error", "payment gateway timeout");
160
+ ```
161
+
162
+ ### Events
163
+
164
+ Events are timestamped log lines attached to a span — useful for recording
165
+ significant moments inside a long operation.
166
+
167
+ ```typescript
168
+ // inside a withSpan() callback
169
+ span.addEvent("cache.miss");
170
+ span.addEvent("db.query", { table: "orders", rows: 5 });
171
+ ```
172
+
173
+ ### Recording exceptions
174
+
175
+ `recordException()` adds an `exception` event carrying the error's type, message,
176
+ and stack:
177
+
178
+ ```typescript
179
+ // inside a withSpan() callback
180
+ try {
181
+ await chargeCard(amount);
182
+ } catch (err) {
183
+ span.recordException(err as Error);
184
+ span.setStatus("error", (err as Error).message);
185
+ throw err;
186
+ }
187
+ ```
188
+
189
+ > **Tip** — `withSpan()` already records the exception and sets `error` status when
190
+ > your callback throws. Only call `recordException()` yourself when you catch and
191
+ > swallow the error inside the callback.
192
+
193
+ ## Context propagation
194
+
195
+ Child spans created inside a `withSpan()` callback automatically inherit the parent
196
+ trace ID and attach as children — no manual wiring. Context flows through the async
197
+ call stack via `AsyncLocalStorage`.
198
+
199
+ ```typescript
200
+ // in a request handler
201
+ await withSpan("handle-request", async () => {
202
+ // These nested spans automatically become children:
203
+ await withSpan("validate-input", async () => {
204
+ /* … */
205
+ });
206
+ await withSpan("query-db", async () => {
207
+ /* … */
208
+ });
209
+ await withSpan("render-response", async () => {
210
+ /* … */
211
+ });
212
+ });
213
+ ```
214
+
215
+ The active span is reachable anywhere in the async stack via `currentSpan()`, which
216
+ returns `undefined` when no span is active:
217
+
218
+ ```typescript
219
+ // in any helper called within a span
220
+ import { currentSpan } from "@zerotal/telemetry";
221
+
222
+ function logWithTrace(message: string) {
223
+ const span = currentSpan();
224
+ console.log({ message, traceId: span?.data.traceId });
225
+ }
226
+ ```
227
+
228
+ ## Observability model — one substrate, many readers
229
+
230
+ Zerotal has a single source of truth for "what happened and how long it took": the
231
+ [`FrameworkEvents`](/docs/events) bus in core. Every subsystem emits timed events
232
+ there — `RequestHandled`, `QueryExecuted`, `JobRan`, `TaskRan`, and the
233
+ once-per-boot `AppBooted` (which carries the wall-clock boot time, also surfaced in
234
+ the [Health](/docs/health) report's `app.bootMs`). The [devtools panel](/docs/devtools)
235
+ and the admin Health page read those events directly; core's `HttpMetrics` rolls
236
+ them into aggregate p95/throughput.
237
+
238
+ Telemetry is a **reader** of that substrate, not a parallel one. When
239
+ `TelemetryProvider` boots it installs an event bridge that translates each timed
240
+ event into a completed span and exports it to your OTLP backend — so external
241
+ tracing shows the same signal the panel does, with no extra instrumentation. Core
242
+ never depends on telemetry; telemetry subscribes to core. You can install the bridge
243
+ manually onto any tracer:
244
+
245
+ ```typescript
246
+ import { installEventBridge } from "@zerotal/telemetry";
247
+
248
+ const dispose = installEventBridge(tracer); // returns an unsubscribe fn
249
+ ```
250
+
251
+ Spans you create yourself with `withSpan()` (below) compose on top of this — use them
252
+ to trace work that _isn't_ already a framework event.
253
+
254
+ ## Tracing HTTP requests
255
+
256
+ `TelemetryMiddleware` creates a `server`-kind root span for every incoming HTTP
257
+ request. Register it near the top of the middleware stack, after
258
+ `LoggerMiddleware`:
259
+
260
+ ```typescript
261
+ // bootstrap/middleware.ts (or where the stack is declared)
262
+ import { TelemetryMiddleware } from "@zerotal/telemetry";
263
+
264
+ app.use([
265
+ LoggerMiddleware,
266
+ TelemetryMiddleware,
267
+ /* … */
268
+ ]);
269
+ ```
270
+
271
+ Attributes set automatically:
272
+
273
+ | Attribute | Source |
274
+ | ------------------ | ----------------------------------------------- |
275
+ | `http.method` | Uppercased request method, e.g. `'GET'` |
276
+ | `http.url` | `http.url.href` |
277
+ | `http.request_id` | `http.requestId` |
278
+ | `http.status_code` | Response status, set after the handler runs |
279
+ | `http.route` | Set only if the pathname changed during routing |
280
+
281
+ A status of `500` or higher sets the span status to `error`. The span name defaults
282
+ to `"METHOD /pathname"` (e.g. `"GET /api/users"`). Override it with a custom
283
+ function:
284
+
285
+ ```typescript
286
+ // where you register the middleware
287
+ TelemetryMiddleware.with({
288
+ spanName: (ctx) => `${ctx.request.method} ${ctx.params.route ?? ctx.url.pathname}`,
289
+ });
290
+ ```
291
+
292
+ Any `withSpan()` calls inside route handlers or downstream middleware automatically
293
+ attach to this root span as children.
294
+
295
+ ## Exporters
296
+
297
+ An exporter decides where completed spans go. Select one with the `exporter` config
298
+ field; the provider wires up the matching class for you.
299
+
300
+ ### NoopExporter
301
+
302
+ Discards all spans. Active when `exporter` is `'noop'` or unset.
303
+
304
+ ### ConsoleExporter
305
+
306
+ Prints human-readable span data to stdout. Use during development.
307
+
308
+ ```typescript
309
+ // config/telemetry.ts
310
+ import { TelemetryConfig } from "@zerotal/telemetry";
311
+
312
+ export default TelemetryConfig({ exporter: "console" });
313
+ ```
314
+
315
+ Output:
316
+
317
+ ```text
318
+ # stdout
319
+ [telemetry] GET /api/users
320
+ trace=a1b2c3d4e5f6a7b8 span=c3d4e5f6 (root)
321
+ status=ok duration=12ms
322
+ http.method: GET
323
+ http.status_code: 200
324
+ ```
325
+
326
+ ### OtlpExporter
327
+
328
+ Sends spans to any OTLP HTTP/JSON endpoint. Compatible backends:
329
+
330
+ - [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/) (self-hosted)
331
+ - [Honeycomb](https://honeycomb.io)
332
+ - [Grafana Tempo](https://grafana.com/oss/tempo/)
333
+ - [Jaeger](https://www.jaegertracing.io/)
334
+ - Any vendor that accepts OTLP
335
+
336
+ ```typescript
337
+ // config/telemetry.ts
338
+ import { TelemetryConfig } from "@zerotal/telemetry";
339
+
340
+ export default TelemetryConfig({
341
+ exporter: "otlp",
342
+ serviceName: "my-api",
343
+ otlp: {
344
+ endpoint: "https://api.honeycomb.io/v1/traces",
345
+ headers: { "x-honeycomb-team": Bun.env.HONEYCOMB_API_KEY ?? "" },
346
+ },
347
+ });
348
+ ```
349
+
350
+ > **Note** — Export errors (network failures, etc.) are silently swallowed —
351
+ > telemetry failures never affect the application. Set `rethrowExportErrors: true`
352
+ > on a manual `Tracer` if you need them surfaced (for example, in a test).
353
+
354
+ ## Manual tracer
355
+
356
+ For advanced scenarios — testing, multi-tracer setups, or libraries — create and
357
+ use a `Tracer` instance directly instead of the global one.
358
+
359
+ ```typescript
360
+ // in a library or test
361
+ import { Tracer, ConsoleExporter } from "@zerotal/telemetry";
362
+
363
+ const tracer = new Tracer({
364
+ exporter: new ConsoleExporter(),
365
+ minDurationMs: 5, // drop spans shorter than 5 ms
366
+ rethrowExportErrors: false, // swallow exporter errors (default)
367
+ });
368
+
369
+ await tracer.withSpan("my-op", async (span) => {
370
+ span.setAttribute("foo", "bar");
371
+ await doWork();
372
+ });
373
+ ```
374
+
375
+ ### startSpan — manual lifetime management
376
+
377
+ Use `startSpan()` for fire-and-forget work, or when the span lifetime doesn't map
378
+ neatly to a single async function. The caller owns the lifecycle:
379
+
380
+ ```typescript
381
+ // in a stream/queue worker
382
+ const span = tracer.startSpan("stream-processor", { kind: "consumer" });
383
+ span.setAttribute("queue", "orders");
384
+
385
+ try {
386
+ await processStream(span);
387
+ span.setStatus("ok");
388
+ } catch (err) {
389
+ span.recordException(err as Error);
390
+ span.setStatus("error", (err as Error).message);
391
+ throw err;
392
+ } finally {
393
+ span.end(); // ends the span — but does NOT export it
394
+ }
395
+ ```
396
+
397
+ > **Warning** — `startSpan()` does **not** export automatically, and `span.end()`
398
+ > only stamps `endMs` — it does not flush to the exporter. Use `startSpan()` for
399
+ > attaching to an existing trace or wrapping a `SpanContext.run()`; prefer
400
+ > `withSpan()` for the common case, which exports for you.
401
+
402
+ ## Testing
403
+
404
+ Implement the `SpanExporter` interface to capture spans in memory and assert on
405
+ them. The global-tracer setters live on the `withSpan` module and are internal —
406
+ import them from the package entry only where they are re-exported, or prefer
407
+ passing your `Tracer` explicitly:
408
+
409
+ ```typescript
410
+ // in a test
411
+ import { Tracer } from "@zerotal/telemetry";
412
+ import type { SpanExporter, SpanData } from "@zerotal/telemetry";
413
+
414
+ class MemoryExporter implements SpanExporter {
415
+ readonly spans: SpanData[] = [];
416
+ async export(span: SpanData) {
417
+ this.spans.push(span);
418
+ }
419
+ }
420
+
421
+ const mem = new MemoryExporter();
422
+ const tracer = new Tracer({ exporter: mem });
423
+
424
+ await tracer.withSpan("test-op", async (span) => {
425
+ span.setAttribute("x", 1);
426
+ });
427
+
428
+ console.log(mem.spans[0]?.name); // 'test-op'
429
+ console.log(mem.spans[0]?.attributes["x"]); // 1
430
+ ```
431
+
432
+ > **Note** — To exercise the global `withSpan()` helper in a test, register your
433
+ > tracer via `TelemetryProvider` on a booted test app rather than reaching for the
434
+ > internal global setters.
435
+
436
+ ## References
437
+
438
+ ### withSpan / currentSpan
439
+
440
+ | Member | Signature | Description |
441
+ | ------------- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
442
+ | `withSpan` | `withSpan<T>(name: string, fn: (span: Span) => Promise<T>, options?: SpanOptions): Promise<T>` | Run `fn` in a span on the global tracer; no-op span if none is registered. |
443
+ | `currentSpan` | `currentSpan(): Span \| undefined` | The active span in the current async context, or `undefined`. |
444
+
445
+ ### Tracer
446
+
447
+ | Method | Signature | Description |
448
+ | ----------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
449
+ | constructor | `new Tracer(options: TracerOptions)` | `exporter` required; `minDurationMs`, `rethrowExportErrors` opt. |
450
+ | `withSpan` | `withSpan<T>(name, fn: (span: Span) => Promise<T>, options?: SpanOptions): Promise<T>` | Create, run, end, and export a span around `fn`. |
451
+ | `startSpan` | `startSpan(name: string, options?: SpanOptions): Span` | Start an un-ended span; caller must call `end()` (no export). |
452
+ | `shutdown` | `shutdown(): Promise<void>` | Flush and tear down the exporter. |
453
+
454
+ ### Span
455
+
456
+ | Method | Signature | Description |
457
+ | ------------------ | ------------------------------------------------------------------------- | --------------------------------------------------- |
458
+ | `setAttribute` | `setAttribute(key: string, value: string \| number \| boolean): this` | Set one attribute. |
459
+ | `setAttributes` | `setAttributes(attrs: Record<string, string \| number \| boolean>): this` | Merge a batch of attributes. |
460
+ | `setStatus` | `setStatus(code: "ok" \| "error", message?: string): this` | Set the span status and optional message. |
461
+ | `addEvent` | `addEvent(name: string, attributes?: Record<string, unknown>): this` | Append a timestamped event. |
462
+ | `recordException` | `recordException(error: Error): this` | Add an `exception` event with type, message, stack. |
463
+ | `end` | `end(): void` | Stamp `endMs` (idempotent); does not export. |
464
+ | `durationMs` (get) | `durationMs: number` | Elapsed ms since start (uses now if not yet ended). |
465
+ | `isEnded` (get) | `isEnded: boolean` | Whether `end()` has been called. |
466
+
467
+ ### SpanData
468
+
469
+ | Field | Type | Description |
470
+ | ------------ | -------------------------------------------------------- | ------------------------------------------------------------------------ |
471
+ | `traceId` | `string` | 32-char hex — shared by all spans in the same trace. |
472
+ | `spanId` | `string` | 16-char hex — unique to this span. |
473
+ | `parentId` | `string \| undefined` | Parent's `spanId`, or `undefined` for root spans. |
474
+ | `name` | `string` | Operation name. |
475
+ | `kind` | `SpanKind` | `'internal'` \| `'server'` \| `'client'` \| `'producer'` \| `'consumer'` |
476
+ | `startMs` | `number` | Unix timestamp (ms) when the span started. |
477
+ | `endMs` | `number \| undefined` | Unix timestamp (ms) when the span ended. |
478
+ | `attributes` | `Record<string, string \| number \| boolean>` | Key/value metadata. |
479
+ | `status` | `{ code: 'unset' \| 'ok' \| 'error'; message?: string }` | Span status. |
480
+ | `events` | `SpanEvent[]` | Timestamped log lines attached to the span. |
481
+
482
+ ### SpanEvent
483
+
484
+ | Field | Type |
485
+ | ------------ | -------------------------------------- |
486
+ | `name` | `string` |
487
+ | `timeMs` | `number` |
488
+ | `attributes` | `Record<string, unknown> \| undefined` |
489
+
490
+ ### SpanKind
491
+
492
+ | Value | Use |
493
+ | ------------ | ----------------------------------------------------------- |
494
+ | `'internal'` | Default — intra-process operation. |
495
+ | `'server'` | Handling an inbound request (set by `TelemetryMiddleware`). |
496
+ | `'client'` | Outbound HTTP call or DB query. |
497
+ | `'producer'` | Enqueuing a message. |
498
+ | `'consumer'` | Processing a queued message. |
499
+
500
+ ## Next steps
501
+
502
+ - [Logger](/docs/logger) — pair traces with structured logs.
503
+ - [Health](/docs/health) — expose readiness and liveness checks.
504
+ - [Middleware](/docs/middleware) — where `TelemetryMiddleware` fits in the stack.
505
+ - [Queue](/docs/queue) — trace background jobs with producer/consumer spans.