@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.
- package/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +67 -0
- package/api-surface.md +283 -0
- package/docs/about.md +676 -0
- package/docs/admin/actions.md +183 -0
- package/docs/admin/auth.md +89 -0
- package/docs/admin/dashboard.md +155 -0
- package/docs/admin/extending-ui.md +229 -0
- package/docs/admin/extending.md +205 -0
- package/docs/admin/forms.md +155 -0
- package/docs/admin/index.md +144 -0
- package/docs/admin/operations.md +224 -0
- package/docs/admin/references.md +174 -0
- package/docs/admin/resources.md +132 -0
- package/docs/admin/structure.md +158 -0
- package/docs/admin/tables.md +185 -0
- package/docs/admin/testing.md +138 -0
- package/docs/ai.md +373 -0
- package/docs/application.md +435 -0
- package/docs/arch.md +263 -0
- package/docs/assets.md +755 -0
- package/docs/audit.md +367 -0
- package/docs/authentication.md +1000 -0
- package/docs/authorization.md +529 -0
- package/docs/broadcasting/channels.md +170 -0
- package/docs/broadcasting/client.md +108 -0
- package/docs/broadcasting/events.md +190 -0
- package/docs/broadcasting/index.md +163 -0
- package/docs/broadcasting/references.md +84 -0
- package/docs/broadcasting/testing.md +132 -0
- package/docs/cache.md +387 -0
- package/docs/carbon.md +830 -0
- package/docs/changelog.md +465 -0
- package/docs/client/auth.md +113 -0
- package/docs/client/errors.md +139 -0
- package/docs/client/files.md +118 -0
- package/docs/client/index.md +128 -0
- package/docs/client/references.md +58 -0
- package/docs/client/requests.md +131 -0
- package/docs/client/resilience.md +141 -0
- package/docs/client/testing.md +146 -0
- package/docs/commands.md +450 -0
- package/docs/components.md +2427 -0
- package/docs/config-system.md +258 -0
- package/docs/container.md +596 -0
- package/docs/context.md +579 -0
- package/docs/contributing.md +164 -0
- package/docs/controllers.md +354 -0
- package/docs/conventions.md +466 -0
- package/docs/cookies.md +101 -0
- package/docs/csrf.md +206 -0
- package/docs/database.md +476 -0
- package/docs/deployment.md +434 -0
- package/docs/devtools.md +1013 -0
- package/docs/email-verification.md +320 -0
- package/docs/encryption.md +380 -0
- package/docs/errors.md +317 -0
- package/docs/events.md +420 -0
- package/docs/flow/components.md +846 -0
- package/docs/flow/decorators.md +458 -0
- package/docs/flow/events.md +420 -0
- package/docs/flow/forms.md +551 -0
- package/docs/flow/index.md +568 -0
- package/docs/flow/layouts.md +793 -0
- package/docs/flow/lifecycle.md +336 -0
- package/docs/flow/pagination.md +402 -0
- package/docs/flow/performance.md +262 -0
- package/docs/flow/references.md +317 -0
- package/docs/flow/routing.md +550 -0
- package/docs/flow/testing.md +447 -0
- package/docs/getting-started.md +286 -0
- package/docs/health.md +269 -0
- package/docs/helpers.md +423 -0
- package/docs/i18n.md +414 -0
- package/docs/index.md +65 -0
- package/docs/inertia/build.md +95 -0
- package/docs/inertia/devtools.md +197 -0
- package/docs/inertia/index.md +267 -0
- package/docs/inertia/middleware.md +81 -0
- package/docs/inertia/props.md +507 -0
- package/docs/inertia/references.md +144 -0
- package/docs/inertia/rendering.md +189 -0
- package/docs/inertia/ssr.md +102 -0
- package/docs/inspirations.md +257 -0
- package/docs/lifecycle.md +331 -0
- package/docs/lock.md +454 -0
- package/docs/logger.md +517 -0
- package/docs/media.md +582 -0
- package/docs/middleware.md +509 -0
- package/docs/migrations.md +529 -0
- package/docs/monitor.md +377 -0
- package/docs/notifications.md +848 -0
- package/docs/orm/casts.md +365 -0
- package/docs/orm/factories.md +231 -0
- package/docs/orm/index.md +663 -0
- package/docs/orm/lifecycle.md +421 -0
- package/docs/orm/queries.md +692 -0
- package/docs/orm/relationships.md +440 -0
- package/docs/orm/serialization.md +251 -0
- package/docs/package-development.md +536 -0
- package/docs/pagination.md +340 -0
- package/docs/password-reset.md +335 -0
- package/docs/providers.md +447 -0
- package/docs/query-builder.md +465 -0
- package/docs/queue.md +549 -0
- package/docs/rate-limiting.md +253 -0
- package/docs/responses.md +336 -0
- package/docs/roles-and-2fa.md +418 -0
- package/docs/routing.md +1008 -0
- package/docs/scaffolding.md +335 -0
- package/docs/scheduler.md +533 -0
- package/docs/seeding.md +229 -0
- package/docs/session.md +451 -0
- package/docs/social.md +524 -0
- package/docs/storage.md +622 -0
- package/docs/structure.md +230 -0
- package/docs/support-policy.md +114 -0
- package/docs/telemetry.md +505 -0
- package/docs/tenancy.md +608 -0
- package/docs/testing/browser.md +153 -0
- package/docs/testing/console.md +130 -0
- package/docs/testing/database.md +238 -0
- package/docs/testing/flow-browser.md +216 -0
- package/docs/testing/http.md +441 -0
- package/docs/testing/index.md +240 -0
- package/docs/testing/mocking.md +410 -0
- package/docs/upgrade.md +192 -0
- package/docs/validator.md +549 -0
- package/docs/view.md +453 -0
- package/package.json +60 -0
- package/src/bin/mcp.ts +73 -0
- package/src/config.ts +63 -0
- package/src/errors.ts +27 -0
- package/src/index.ts +56 -0
- package/src/install/ArchInstallCommand.ts +189 -0
- package/src/install/detect.ts +98 -0
- package/src/install/guidelines.ts +290 -0
- package/src/install/markers.ts +77 -0
- package/src/install/mcpConfig.ts +81 -0
- package/src/mcp/index.ts +27 -0
- package/src/mcp/jsonrpc.ts +133 -0
- package/src/mcp/server.ts +307 -0
- package/src/mcp/stdio.ts +111 -0
- package/src/mcp/types.ts +146 -0
- package/src/probe/ArchProbeCommand.ts +48 -0
- package/src/probe/sentinel.ts +13 -0
- package/src/probe/topics.ts +291 -0
- package/src/provider/ArchProvider.ts +101 -0
- package/src/tools/_probe.ts +164 -0
- package/src/tools/apiSurface.ts +241 -0
- package/src/tools/appInfo.ts +88 -0
- package/src/tools/baselines.ts +210 -0
- package/src/tools/context.ts +37 -0
- package/src/tools/doctor.ts +93 -0
- package/src/tools/index.ts +50 -0
- package/src/tools/logs.ts +265 -0
- package/src/tools/routes.ts +133 -0
- package/src/tools/schema.ts +119 -0
- 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.
|