autotel-devtools 21.0.1 → 23.0.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/README.md +183 -28
- package/dist/cli.cjs +80 -13
- package/dist/cli.js +80 -13
- package/dist/compile-JDFHUCo5.d.cts +177 -0
- package/dist/compile-JDFHUCo5.d.ts +177 -0
- package/dist/{error-aggregator-D8VKciLY.d.ts → error-aggregator-B52JI8jL.d.ts} +1 -1
- package/dist/{error-aggregator-DCEKMOm3.d.cts → error-aggregator-DSwUvgFF.d.cts} +1 -1
- package/dist/exporter-BrEcnzoT.d.ts +546 -0
- package/dist/exporter-C4uKgo7l.d.cts +546 -0
- package/dist/fullpage.global.js +39 -0
- package/dist/grpc-CSWjMRiB.cjs +82 -0
- package/dist/grpc-CZEDUYCM.js +77 -0
- package/dist/http-Cqm_MAtc.cjs +3942 -0
- package/dist/http-Dbd9TIYU.js +3776 -0
- package/dist/index.cjs +25 -7
- package/dist/index.d.cts +30 -3
- package/dist/index.d.ts +30 -3
- package/dist/index.js +25 -7
- package/dist/{listen-DBfsfcdd.js → listen-D-lLgfro.js} +5 -2
- package/dist/{listen-CEJ3nYJf.cjs → listen-l09RRHht.cjs} +5 -2
- package/dist/parse-BRlosZft.cjs +642 -0
- package/dist/parse-D_RmPPQs.js +612 -0
- package/dist/query/index.cjs +8 -0
- package/dist/query/index.d.cts +16 -0
- package/dist/query/index.d.ts +16 -0
- package/dist/query/index.js +3 -0
- package/dist/server/exporter.d.cts +1 -1
- package/dist/server/exporter.d.ts +1 -1
- package/dist/server/index.cjs +6 -2
- package/dist/server/index.d.cts +19 -6
- package/dist/server/index.d.ts +18 -5
- package/dist/server/index.js +3 -2
- package/dist/types-B0tjwFqj.d.cts +107 -0
- package/dist/types-DM8y4A9Z.d.ts +107 -0
- package/dist/widget.global.js +15 -23
- package/dist/wire/index.cjs +7 -0
- package/dist/wire/index.d.cts +26 -0
- package/dist/wire/index.d.ts +26 -0
- package/dist/wire/index.js +3 -0
- package/dist/wire-2Rmfg6IT.js +51 -0
- package/dist/wire-CHU1PkMo.cjs +75 -0
- package/package.json +22 -6
- package/dist/exporter-Dt4kx128.d.cts +0 -207
- package/dist/exporter-Due9Rd4s.d.ts +0 -207
- package/dist/http-CNZMrnzv.js +0 -1453
- package/dist/http-CXSzX4ee.cjs +0 -1607
package/README.md
CHANGED
|
@@ -24,11 +24,26 @@ Standalone OTLP receiver with web UI for local development. Think TanStack Devto
|
|
|
24
24
|
│ │ ├── GET /healthz → Health check │ │
|
|
25
25
|
│ │ └── WS /ws ←→ WebSocket │ │
|
|
26
26
|
│ └───────────────────────────────────────┘ │
|
|
27
|
+
│ OTLP/gRPC receiver (port 4317) │
|
|
27
28
|
└─────────────────────────────────────────────┘
|
|
28
29
|
```
|
|
29
30
|
|
|
30
31
|
## Quick Start
|
|
31
32
|
|
|
33
|
+
### Installing with pnpm
|
|
34
|
+
|
|
35
|
+
This package pulls in `esbuild` and `protobufjs`, both of which run install
|
|
36
|
+
scripts. pnpm 10+ blocks those by default and then refuses to run anything else
|
|
37
|
+
until you decide about them, so `pnpm install && pnpm start` fails on a fresh
|
|
38
|
+
clone with a deps-status error rather than an obvious one. Approve them once in
|
|
39
|
+
`pnpm-workspace.yaml`:
|
|
40
|
+
|
|
41
|
+
```yaml
|
|
42
|
+
allowBuilds:
|
|
43
|
+
esbuild: true
|
|
44
|
+
protobufjs: true
|
|
45
|
+
```
|
|
46
|
+
|
|
32
47
|
### Standalone Mode
|
|
33
48
|
|
|
34
49
|
```bash
|
|
@@ -42,17 +57,23 @@ node app.js
|
|
|
42
57
|
```
|
|
43
58
|
|
|
44
59
|
The endpoints accept **both OTLP/JSON and OTLP/protobuf** (`application/x-protobuf`),
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
60
|
+
chosen from the request `Content-Type`. SDKs that default to protobuf over OTLP
|
|
61
|
+
HTTP, including the Python, Java, and Go OpenTelemetry SDKs, work with no extra
|
|
62
|
+
configuration:
|
|
48
63
|
|
|
49
64
|
```bash
|
|
50
|
-
# Python / Java / Go SDKs default to http/protobuf
|
|
65
|
+
# Python / Java / Go SDKs default to http/protobuf; point them at the receiver
|
|
51
66
|
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 python app.py
|
|
52
67
|
```
|
|
53
68
|
|
|
54
69
|
Open http://localhost:4318 to see traces, logs, and metrics.
|
|
55
70
|
|
|
71
|
+
SDKs configured for OTLP/gRPC can use the standard endpoint directly:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 python app.py
|
|
75
|
+
```
|
|
76
|
+
|
|
56
77
|
### Embedded Widget
|
|
57
78
|
|
|
58
79
|
Add the widget to your web app:
|
|
@@ -111,6 +132,7 @@ const myFunction = withTracing({ name: 'example.run' })((ctx) => async () => {
|
|
|
111
132
|
|
|
112
133
|
- **DevtoolsServer** - WebSocket server + in-memory data store
|
|
113
134
|
- **HTTP Routes** - OTLP receivers for traces/logs/metrics (JSON + protobuf)
|
|
135
|
+
- **gRPC Receiver** - canonical OTLP trace, log, and metric services on port 4317
|
|
114
136
|
|
|
115
137
|
#### Detecting the receiver
|
|
116
138
|
|
|
@@ -145,14 +167,18 @@ process. Point them at the bound port, or free the original.
|
|
|
145
167
|
|
|
146
168
|
- ✅ Real-time OTLP ingestion (traces, logs, metrics)
|
|
147
169
|
- ✅ WebSocket streaming with history replay
|
|
148
|
-
- ✅
|
|
149
|
-
- ✅
|
|
170
|
+
- ✅ **Query language** over traces and logs, run as SQL against a sqlite store (see below)
|
|
171
|
+
- ✅ **Persistence**: `--db` keeps telemetry across restarts; retention applies count and byte caps
|
|
172
|
+
- ✅ Traces view with waterfall (tree connectors, event markers, critical path) + flame graph
|
|
173
|
+
- ✅ Logs view with a query bar and severity filtering
|
|
174
|
+
- ✅ **Metrics view**: server-reduced time series, merged histogram distributions, interpolated quantiles, sparklines, and capped exemplars that open the trace behind a spike
|
|
150
175
|
- ✅ Error aggregation and grouping
|
|
151
176
|
- ✅ Service map visualization
|
|
152
177
|
- ✅ Resources view (derived from telemetry)
|
|
153
178
|
- ✅ GenAI run summaries + narrated walkthrough
|
|
154
179
|
- ✅ **Agents view**: observe coding agents (Claude Code, opencode) from their OTel metrics + log events
|
|
155
|
-
- ✅
|
|
180
|
+
- ✅ Global time window shared by every tab
|
|
181
|
+
- ✅ Live tail that freezes while you read, with a "N new" pill to catch up
|
|
156
182
|
- ✅ Configurable telemetry limits (env vars)
|
|
157
183
|
- ✅ Widget position persistence (localStorage)
|
|
158
184
|
- ✅ Export traces as JSON
|
|
@@ -171,8 +197,8 @@ on top of the per-span detail:
|
|
|
171
197
|
- An **Explain run** button steps through the run in chronological order with
|
|
172
198
|
plain-language narration of each step. Auto-play or step manually with the
|
|
173
199
|
arrow keys / Space (Esc exits); clicking a span jumps the tour to that step.
|
|
174
|
-
|
|
175
|
-
|
|
200
|
+
Use it to show a teammate or a client what the agent did, which tools it
|
|
201
|
+
called, and where the cost went.
|
|
176
202
|
|
|
177
203
|
### Agents: observe Claude Code (and other coding agents)
|
|
178
204
|
|
|
@@ -187,9 +213,9 @@ One command starts the receiver _and_ launches Claude Code wired to it:
|
|
|
187
213
|
npx autotel-devtools claude
|
|
188
214
|
```
|
|
189
215
|
|
|
190
|
-
This sets the telemetry env for a live local view
|
|
191
|
-
|
|
192
|
-
|
|
216
|
+
This sets the telemetry env for a live local view: OTLP **`http/protobuf`**, 1s
|
|
217
|
+
export intervals, and `session.id` kept on metrics. The receiver also accepts
|
|
218
|
+
standard OTLP/gRPC on `:4317`. Then open the UI
|
|
193
219
|
and switch to **Agents**.
|
|
194
220
|
|
|
195
221
|
- `--print-env`: print the env block instead of launching (for managed-settings
|
|
@@ -201,17 +227,116 @@ decisions), a **rollup** (cost, tokens, requests, lines changed), and breakdowns
|
|
|
201
227
|
by **tool category**, **MCP server** (`mcp__server__tool`), **sub-agent** (`Task`)
|
|
202
228
|
and **skill** (`Skill`). Plus an aggregate strip across all sessions. Cost uses
|
|
203
229
|
the agent's reported `cost_usd`, falling back to a token estimate (badged). MCP
|
|
204
|
-
protocol internals
|
|
230
|
+
protocol internals live in `autotel-mcp-instrumentation`.
|
|
231
|
+
|
|
232
|
+
## Querying
|
|
233
|
+
|
|
234
|
+
The Traces and Logs tabs take a query, which the server compiles to SQL and runs
|
|
235
|
+
against everything it has kept, rather than only what is on screen.
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
service = api AND duration > 100
|
|
239
|
+
name CONTAINS checkout
|
|
240
|
+
http.status_code = 500
|
|
241
|
+
service IN [api, web, worker]
|
|
242
|
+
name REGEXP "^GET /users/[0-9]+$"
|
|
243
|
+
severity_number >= 17 # logs: error and above
|
|
244
|
+
parent_span_id = NULL # root spans only
|
|
245
|
+
"user id" = "u-42" # quote a key a bare word can't spell
|
|
246
|
+
checkout # bare words are free text
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**Fields.** Traces: `service`, `name`, `kind`, `duration`, `status`, `trace_id`,
|
|
250
|
+
`span_id`, `parent_span_id`. Logs: `service`, `severity`, `severity_number`,
|
|
251
|
+
`trace_id`, `span_id`, `body`. **Anything else is looked up as an attribute**, so
|
|
252
|
+
every attribute your services emit is queryable without being declared.
|
|
253
|
+
|
|
254
|
+
`severity` and `severity_number` both exist on purpose: the text is what you
|
|
255
|
+
read, but "error and above" is a numeric comparison and string ordering cannot
|
|
256
|
+
express it.
|
|
257
|
+
|
|
258
|
+
**Operators.** `=` `!=` `>` `<` `>=` `<=` for comparison; `CONTAINS`,
|
|
259
|
+
`NOT CONTAINS`, `^` (starts with), `$` (ends with), `REGEXP` / `=~`,
|
|
260
|
+
`NOT REGEXP` / `!~` for text; `IN` / `NOT IN` with `[…]`; `= NULL` and
|
|
261
|
+
`!= NULL` for presence. Combine with `AND`, `OR` and parentheses. Two
|
|
262
|
+
conditions side by side mean `AND`.
|
|
263
|
+
|
|
264
|
+
Values are always sent as bound parameters and field names always come from a
|
|
265
|
+
schema, so nothing you type reaches the SQL string.
|
|
266
|
+
|
|
267
|
+
### The HTTP API
|
|
268
|
+
|
|
269
|
+
The same query the UI runs is available over HTTP, so a test can assert on what
|
|
270
|
+
a run actually emitted instead of on a log line. Requests to the `/api/*`
|
|
271
|
+
endpoints must come from loopback, or the server answers `403 Forbidden`.
|
|
272
|
+
|
|
273
|
+
| Endpoint | Body / result |
|
|
274
|
+
| ------------------------------ | ---------------------------------------------------------------- |
|
|
275
|
+
| `POST /api/query/traces` | `{ query, window?, limit?, cursor? }` → `{ traces, nextCursor }` |
|
|
276
|
+
| `POST /api/query/logs` | same shape, over logs |
|
|
277
|
+
| `POST /api/query/errors` | same shape, error spans only |
|
|
278
|
+
| `POST /api/query/metrics` | same shape, over metric series |
|
|
279
|
+
| `GET /api/query/traces/fields` | `{ fields }` - the bare field names a query can use |
|
|
280
|
+
| `GET /api/stats` | `{ traceCount, spanCount, logCount, bytesUsed, maxBytes, … }` |
|
|
281
|
+
| `GET /v1/traces` | every trace held, no query |
|
|
282
|
+
| `DELETE /v1/traces` | `{ cleared: true }` - empty the store between tests |
|
|
283
|
+
| `GET /healthz` | `{ ok, service, version, clients }` |
|
|
284
|
+
|
|
285
|
+
`query` takes the language above, so anything the Traces tab can filter on, a
|
|
286
|
+
script can too. Each returned trace carries `traceId`, `service`, `rootSpan`,
|
|
287
|
+
`spans`, `duration` and `status`.
|
|
288
|
+
|
|
289
|
+
### Asserting on traces in CI
|
|
290
|
+
|
|
291
|
+
Clear the store, run the thing, then ask what it emitted. A query count is a far
|
|
292
|
+
steadier assertion than a duration:
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
curl -sX DELETE http://127.0.0.1:4318/v1/traces
|
|
296
|
+
node app.js
|
|
297
|
+
|
|
298
|
+
curl -s -X POST http://127.0.0.1:4318/api/query/traces \
|
|
299
|
+
-H 'content-type: application/json' \
|
|
300
|
+
-d '{"query":"name = \"GET /feed\"","limit":1}' \
|
|
301
|
+
| jq -e '.traces[0].spans | length == 2'
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
That fails the moment someone reintroduces an N+1 on `/feed`, and it does not
|
|
305
|
+
care how fast the machine running CI happens to be.
|
|
306
|
+
|
|
307
|
+
### Live tail and the time window
|
|
308
|
+
|
|
309
|
+
The list follows new data by default. It **freezes** the moment you type a
|
|
310
|
+
query, scroll back, select a row, or bound the time window, and a "N new" pill
|
|
311
|
+
counts the matches instead of reordering rows under you. Click the pill to catch
|
|
312
|
+
up. There is no mode to manage: freezing follows from what you did.
|
|
313
|
+
|
|
314
|
+
The time window is one control shared by every tab. Presets track _now_ rather
|
|
315
|
+
than freezing when clicked, and **"All time" means "I haven't said"**: a view
|
|
316
|
+
may fit itself to its own data then, but a window you choose is never widened or
|
|
317
|
+
cropped, because an empty 15-minute window is the answer.
|
|
318
|
+
|
|
319
|
+
### Reading it from a CLI or an agent
|
|
320
|
+
|
|
321
|
+
The same store backs `autotel-mcp`'s `devtools` backend, so `autotel diagnose`,
|
|
322
|
+
`autotel query` and the MCP tools read what the viewer shows:
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
autotel diagnose errors --backend devtools --devtools-base-url http://localhost:4318
|
|
326
|
+
```
|
|
205
327
|
|
|
206
328
|
## Configuration
|
|
207
329
|
|
|
208
330
|
### Environment Variables
|
|
209
331
|
|
|
210
332
|
```bash
|
|
211
|
-
AUTOTEL_MAX_TRACE_COUNT=10000 #
|
|
212
|
-
AUTOTEL_MAX_LOG_COUNT=10000 #
|
|
213
|
-
|
|
333
|
+
AUTOTEL_MAX_TRACE_COUNT=10000 # Live-tail buffer size (default: 100)
|
|
334
|
+
AUTOTEL_MAX_LOG_COUNT=10000 # Live-tail buffer size (default: 100)
|
|
335
|
+
AUTOTEL_DEVTOOLS_DB=./tel.db # sqlite file for the store (default: in-memory)
|
|
336
|
+
AUTOTEL_DEVTOOLS_MAX_TRACES=100000 # Traces retained in the store before pruning
|
|
214
337
|
AUTOTEL_DEVTOOLS_PORT=4318 # Server port (default: 4318)
|
|
338
|
+
AUTOTEL_DEVTOOLS_GRPC_PORT=4317 # OTLP/gRPC port (default: 4317)
|
|
339
|
+
AUTOTEL_DEVTOOLS_DB_MAX_SIZE=2gb # Logical sqlite retention cap
|
|
215
340
|
AUTOTEL_DEVTOOLS_HOST=127.0.0.1 # Bind host (default: 127.0.0.1)
|
|
216
341
|
AUTOTEL_DEVTOOLS_TITLE="My App" # Dashboard title (optional)
|
|
217
342
|
AUTOTEL_DEVTOOLS_SOURCE_ROOT=. # Root GET /source may read (default: cwd on a loopback bind, else off; `false` disables)
|
|
@@ -222,6 +347,7 @@ AUTOTEL_DEVTOOLS_SOURCE_ROOT=. # Root GET /source may read (default: cwd on a
|
|
|
222
347
|
```bash
|
|
223
348
|
npx autotel-devtools 4319 # port as a bare positional
|
|
224
349
|
npx autotel-devtools --port 4319 --host 0.0.0.0
|
|
350
|
+
npx autotel-devtools --db ./telemetry.db # keep telemetry across restarts
|
|
225
351
|
npx autotel-devtools claude # receiver + launch Claude Code wired to it
|
|
226
352
|
npx autotel-devtools claude --print-env # print the telemetry env, don't launch
|
|
227
353
|
```
|
|
@@ -239,17 +365,22 @@ Options:
|
|
|
239
365
|
- `--port, -p` - Port to listen on (default: 4318). If the port is taken, the receiver walks forward to the next free port and prints a warning.
|
|
240
366
|
- `--host, -H` - Host to bind to (default: 127.0.0.1)
|
|
241
367
|
- `--title, -t` - Dashboard title, used for the startup banner and the browser tab (default: `autotel-devtools`). Useful when several dashboards are open at once.
|
|
368
|
+
- `--db, -d` - sqlite file backing the store, so telemetry survives a restart (default: in-memory, which keeps everything until the process exits)
|
|
369
|
+
- `--max-traces` - Traces retained in the store before the oldest are pruned (default: 100000). Pruning runs every 30s.
|
|
370
|
+
- `--grpc-port` - OTLP/gRPC port (default: 4317). If busy, the receiver reports the fallback port.
|
|
371
|
+
- `--db-max-size` - Logical sqlite retention cap (`512mb`, `2gb`, etc.). Defaults to 512 MiB in memory and 2 GiB on disk.
|
|
242
372
|
|
|
243
373
|
When bound to a loopback host, the receiver listens on **both** `127.0.0.1`
|
|
244
374
|
and `::1`, so a client connecting via `localhost` reaches it regardless of how
|
|
245
375
|
the OS resolves `localhost` (macOS prefers IPv6 `::1`). The startup banner
|
|
246
|
-
prints every address it bound
|
|
247
|
-
|
|
376
|
+
prints every address it bound, and warns when it cannot bind a family rather
|
|
377
|
+
than failing silently. Both listeners serve the HTTP routes _and_ the `/ws`
|
|
378
|
+
live tail, so the widget streams whichever form of `localhost` it resolved.
|
|
248
379
|
|
|
249
380
|
## Behind a dev-server proxy
|
|
250
381
|
|
|
251
|
-
If your app's dev server proxies `/v1/traces` to the receiver, two
|
|
252
|
-
|
|
382
|
+
If your app's dev server proxies `/v1/traces` to the receiver, two bugs make
|
|
383
|
+
spans vanish with **no error**:
|
|
253
384
|
|
|
254
385
|
```ts
|
|
255
386
|
// Express / http-proxy-middleware
|
|
@@ -270,15 +401,15 @@ app.use(
|
|
|
270
401
|
```
|
|
271
402
|
|
|
272
403
|
> Symptom of either bug: the browser shows the request leaving (200/no error),
|
|
273
|
-
> but the receiver stays empty. Always verify on the **receiver** side (below)
|
|
274
|
-
>
|
|
404
|
+
> but the receiver stays empty. Always verify on the **receiver** side (below).
|
|
405
|
+
> The browser trying to send proves nothing.
|
|
275
406
|
|
|
276
407
|
## Verifying ingestion in tests
|
|
277
408
|
|
|
278
409
|
The receiver exposes an HTTP read-back so a test can assert the collector
|
|
279
|
-
**
|
|
280
|
-
|
|
281
|
-
it reaches any server
|
|
410
|
+
**received** spans, rather than that the client tried to send them. A
|
|
411
|
+
browser-level route intercept can fake the second one by fulfilling the request
|
|
412
|
+
before it reaches any server:
|
|
282
413
|
|
|
283
414
|
```bash
|
|
284
415
|
GET /v1/traces # → { traces: [...], count: N } what the receiver has
|
|
@@ -286,7 +417,7 @@ DELETE /v1/traces # clear captured telemetry (reset between tests)
|
|
|
286
417
|
```
|
|
287
418
|
|
|
288
419
|
```ts
|
|
289
|
-
// Playwright / integration test
|
|
420
|
+
// Playwright / integration test: bypass any page.route() intercept and ask
|
|
290
421
|
// the collector directly.
|
|
291
422
|
await fetch(`${RECEIVER}/v1/traces`, { method: 'DELETE' }); // reset
|
|
292
423
|
await runTheUserFlow(); // app emits spans
|
|
@@ -295,8 +426,32 @@ await expect
|
|
|
295
426
|
.toBeGreaterThan(0);
|
|
296
427
|
```
|
|
297
428
|
|
|
298
|
-
These read-back calls run from Node
|
|
299
|
-
|
|
429
|
+
These read-back calls run from Node with no `Origin` header, so the origin guard
|
|
430
|
+
below leaves them alone.
|
|
431
|
+
|
|
432
|
+
## Trace payload shape
|
|
433
|
+
|
|
434
|
+
The server **answers in full and streams compact**. Every HTTP response carries
|
|
435
|
+
a complete trace, so `GET /v1/traces`, `POST /api/query/traces` and anything
|
|
436
|
+
else you can `curl` need no decoding.
|
|
437
|
+
|
|
438
|
+
The `/ws` stream is the one exception. It leaves off any span `endTime` that
|
|
439
|
+
`startTime + duration` already reproduces, which is 32% of the compressed
|
|
440
|
+
payload on a large trace. Writing your own WebSocket client? Rehydrate in one
|
|
441
|
+
call:
|
|
442
|
+
|
|
443
|
+
```ts
|
|
444
|
+
import { decodeTraces } from 'autotel-devtools/wire';
|
|
445
|
+
|
|
446
|
+
ws.addEventListener('message', (event) => {
|
|
447
|
+
const data = JSON.parse(event.data);
|
|
448
|
+
const traces = decodeTraces(data.traces ?? []);
|
|
449
|
+
});
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
Responses over 1 KiB are gzipped when the client accepts it, and the WebSocket
|
|
453
|
+
negotiates `permessage-deflate`. Browser `fetch`, browser `WebSocket` and
|
|
454
|
+
undici all handle that transparently, so this is normally invisible.
|
|
300
455
|
|
|
301
456
|
## Read-surface origin guard
|
|
302
457
|
|
package/dist/cli.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const require_http = require('./http-
|
|
3
|
-
const require_listen = require('./listen-
|
|
2
|
+
const require_http = require('./http-Cqm_MAtc.cjs');
|
|
3
|
+
const require_listen = require('./listen-l09RRHht.cjs');
|
|
4
|
+
const require_grpc = require('./grpc-CSWjMRiB.cjs');
|
|
4
5
|
let node_http = require("node:http");
|
|
5
6
|
let node_fs = require("node:fs");
|
|
6
7
|
let node_path = require("node:path");
|
|
@@ -9,7 +10,7 @@ let node_child_process = require("node:child_process");
|
|
|
9
10
|
|
|
10
11
|
//#region src/cli.ts
|
|
11
12
|
function printHelp() {
|
|
12
|
-
process.stdout.write("autotel-devtools - Standalone OTLP receiver with web devtools UI\n\nUsage: autotel-devtools [port] [options]\n autotel-devtools claude [claude args] [--print-env] [--log-prompts]\n\nSubcommands:\n claude Start the receiver AND launch Claude Code wired to it\n (HTTP/protobuf → this receiver, 1s intervals, session id on).\n Open the \"Agents\" tab to watch sessions, tokens, cost,\n tool / MCP / sub-agent / skill usage live.\n --print-env Print the telemetry env block and exit (don't launch)\n --log-prompts Capture prompt text (default: length only / private)\n\nArguments:\n port Port to listen on (shorthand for --port; must be a positive integer)\n\nOptions:\n -p, --port <port> Port to listen on (default: 4318, env: AUTOTEL_DEVTOOLS_PORT).\n If the port is taken, the next free port is used and a warning is shown.\n -H, --host <host> Host to bind to (default: 127.0.0.1, env: AUTOTEL_DEVTOOLS_HOST)\n -t, --title <title> Dashboard title, shown in the banner and browser tab\n (env: AUTOTEL_DEVTOOLS_TITLE)\n Env limits: AUTOTEL_MAX_TRACE_COUNT, AUTOTEL_MAX_LOG_COUNT
|
|
13
|
+
process.stdout.write("autotel-devtools - Standalone OTLP receiver with web devtools UI\n\nUsage: autotel-devtools [port] [options]\n autotel-devtools claude [claude args] [--print-env] [--log-prompts]\n\nSubcommands:\n claude Start the receiver AND launch Claude Code wired to it\n (HTTP/protobuf → this receiver, 1s intervals, session id on).\n Open the \"Agents\" tab to watch sessions, tokens, cost,\n tool / MCP / sub-agent / skill usage live.\n --print-env Print the telemetry env block and exit (don't launch)\n --log-prompts Capture prompt text (default: length only / private)\n\nArguments:\n port Port to listen on (shorthand for --port; must be a positive integer)\n\nOptions:\n -p, --port <port> Port to listen on (default: 4318, env: AUTOTEL_DEVTOOLS_PORT).\n If the port is taken, the next free port is used and a warning is shown.\n -H, --host <host> Host to bind to (default: 127.0.0.1, env: AUTOTEL_DEVTOOLS_HOST)\n -t, --title <title> Dashboard title, shown in the banner and browser tab\n -d, --db <path> Keep telemetry in a sqlite file so it survives restarts\n (default: in-memory, env: AUTOTEL_DEVTOOLS_DB)\n --max-traces <n> Traces retained before the oldest are pruned\n (default: 100000, env: AUTOTEL_DEVTOOLS_MAX_TRACES)\n --grpc-port <n> OTLP/gRPC port (default: 4317, env: AUTOTEL_DEVTOOLS_GRPC_PORT)\n --db-max-size <size> Logical store cap, e.g. 512mb or 2gb\n (default: 512mb memory / 2gb disk, env: AUTOTEL_DEVTOOLS_DB_MAX_SIZE)\n (env: AUTOTEL_DEVTOOLS_TITLE)\n Env limits: AUTOTEL_MAX_TRACE_COUNT, AUTOTEL_MAX_LOG_COUNT\n -h, --help Show this help message\n -v, --version Show version number\n\nEndpoints:\n GET / Web devtools UI (fullpage)\n GET /widget.js Widget bundle (embed in your app)\n POST /v1/traces Receive OTLP JSON trace data\n GET /v1/traces Read back received traces (verify ingestion in tests)\n DELETE /v1/traces Clear captured telemetry (test reset)\n POST /v1/logs Receive OTLP JSON log data\n POST /v1/metrics Receive OTLP JSON metric data\n gRPC :4317 Receive OTLP protobuf traces, logs and metrics\n WS /ws WebSocket stream for real-time updates\n GET /healthz Health check\n\nExamples:\n npx autotel-devtools\n npx autotel-devtools 4319\n npx autotel-devtools -p 4319 -H 0.0.0.0\n npx autotel-devtools claude # watch Claude Code in the Agents tab\n npx autotel-devtools claude --print-env # just print the env (for MDM / VS Code)\n\nThen point your app:\n OTEL_EXPORTER_OTLP_PROTOCOL=http/json OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 node app.js\n\nView in browser:\n http://localhost:4318\n\nOr embed widget in your app:\n <script src=\"http://localhost:4318/widget.js\"><\/script>\n\n");
|
|
13
14
|
}
|
|
14
15
|
function printVersion() {
|
|
15
16
|
try {
|
|
@@ -24,7 +25,11 @@ function parseArgs(argv) {
|
|
|
24
25
|
const options = {
|
|
25
26
|
port: parsePort(process.env.AUTOTEL_DEVTOOLS_PORT || "4318"),
|
|
26
27
|
host: process.env.AUTOTEL_DEVTOOLS_HOST || "127.0.0.1",
|
|
27
|
-
title: process.env.AUTOTEL_DEVTOOLS_TITLE
|
|
28
|
+
title: process.env.AUTOTEL_DEVTOOLS_TITLE,
|
|
29
|
+
dbPath: process.env.AUTOTEL_DEVTOOLS_DB,
|
|
30
|
+
maxTraces: parseCount(process.env.AUTOTEL_DEVTOOLS_MAX_TRACES),
|
|
31
|
+
maxDbBytes: parseBytes(process.env.AUTOTEL_DEVTOOLS_DB_MAX_SIZE),
|
|
32
|
+
grpcPort: parsePort(process.env.AUTOTEL_DEVTOOLS_GRPC_PORT || "4317")
|
|
28
33
|
};
|
|
29
34
|
let portWasExplicit = false;
|
|
30
35
|
let positionalPortConsumed = false;
|
|
@@ -55,6 +60,26 @@ function parseArgs(argv) {
|
|
|
55
60
|
i++;
|
|
56
61
|
continue;
|
|
57
62
|
}
|
|
63
|
+
if ((arg === "--db" || arg === "-d") && next) {
|
|
64
|
+
options.dbPath = next;
|
|
65
|
+
i++;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (arg === "--max-traces" && next) {
|
|
69
|
+
options.maxTraces = parseCount(next);
|
|
70
|
+
i++;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (arg === "--grpc-port" && next) {
|
|
74
|
+
options.grpcPort = parsePort(next);
|
|
75
|
+
i++;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (arg === "--db-max-size" && next) {
|
|
79
|
+
options.maxDbBytes = parseBytes(next);
|
|
80
|
+
i++;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
58
83
|
if (/^\d+$/.test(arg) && !positionalPortConsumed) {
|
|
59
84
|
if (!portWasExplicit) options.port = parsePort(arg);
|
|
60
85
|
positionalPortConsumed = true;
|
|
@@ -63,6 +88,28 @@ function parseArgs(argv) {
|
|
|
63
88
|
}
|
|
64
89
|
return options;
|
|
65
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* A positive integer option, or undefined when unset.
|
|
93
|
+
*
|
|
94
|
+
* Unlike `parsePort` this does not exit on a bad value: a nonsense retention
|
|
95
|
+
* cap should fall back to the default rather than refuse to start a viewer.
|
|
96
|
+
*/
|
|
97
|
+
function parseCount(value) {
|
|
98
|
+
if (!value) return void 0;
|
|
99
|
+
const n = Number(value);
|
|
100
|
+
return Number.isInteger(n) && n > 0 ? n : void 0;
|
|
101
|
+
}
|
|
102
|
+
function parseBytes(value) {
|
|
103
|
+
if (!value) return void 0;
|
|
104
|
+
const match = value.trim().toLowerCase().match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)?$/);
|
|
105
|
+
if (!match) return void 0;
|
|
106
|
+
return Math.floor(Number(match[1]) * {
|
|
107
|
+
b: 1,
|
|
108
|
+
kb: 1024,
|
|
109
|
+
mb: 1024 ** 2,
|
|
110
|
+
gb: 1024 ** 3
|
|
111
|
+
}[match[2] || "b"]);
|
|
112
|
+
}
|
|
66
113
|
function parsePort(value) {
|
|
67
114
|
const n = Number(value);
|
|
68
115
|
if (!Number.isInteger(n) || n < 0 || n > 65535) {
|
|
@@ -77,7 +124,10 @@ async function startReceiver(options) {
|
|
|
77
124
|
const wsServer = new require_http.DevtoolsServer({
|
|
78
125
|
server: httpServer,
|
|
79
126
|
host: options.host,
|
|
80
|
-
verbose: true
|
|
127
|
+
verbose: true,
|
|
128
|
+
dbPath: options.dbPath,
|
|
129
|
+
maxTraces: options.maxTraces,
|
|
130
|
+
maxDbBytes: options.maxDbBytes
|
|
81
131
|
});
|
|
82
132
|
const sourceRoot = require_http.resolveSourceRoot(process.env.AUTOTEL_DEVTOOLS_SOURCE_ROOT, process.cwd(), loopbackOnly);
|
|
83
133
|
require_http.attachDevtoolsRoutes(httpServer, wsServer, {
|
|
@@ -89,22 +139,36 @@ async function startReceiver(options) {
|
|
|
89
139
|
primary: httpServer,
|
|
90
140
|
port: options.port,
|
|
91
141
|
host: options.host,
|
|
92
|
-
attachSecondary: (s) =>
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
142
|
+
attachSecondary: (s) => {
|
|
143
|
+
require_http.attachDevtoolsRoutes(s, wsServer, {
|
|
144
|
+
loopbackOnly,
|
|
145
|
+
title: options.title,
|
|
146
|
+
sourceRoot
|
|
147
|
+
});
|
|
148
|
+
wsServer.attachWebSocket(s);
|
|
149
|
+
}
|
|
97
150
|
});
|
|
98
151
|
const { addresses, warnings, port: boundPort } = await listeners.ready;
|
|
152
|
+
const grpc = await require_grpc.startOtlpGrpcReceiver({
|
|
153
|
+
devtools: wsServer,
|
|
154
|
+
host: options.host,
|
|
155
|
+
port: options.grpcPort
|
|
156
|
+
});
|
|
157
|
+
if (grpc.port !== options.grpcPort) warnings.push(`OTLP/gRPC port ${options.grpcPort} was busy; using ${grpc.port}.`);
|
|
99
158
|
if (boundPort !== options.port) if (await require_http.probePortHolder(options.host, options.port) === "autotel-devtools") warnings.push(`another autotel-devtools is already running on port ${options.port}; this instance is on ${boundPort}. Use the existing one, or stop it and restart here.`);
|
|
100
159
|
else warnings.push(`port ${options.port} is held by another process that is NOT autotel-devtools. Anything exporting OTLP to :${options.port} is reaching that process, not this devtools. Point your exporter at :${boundPort}, or free :${options.port} and restart.`);
|
|
101
160
|
return {
|
|
102
161
|
wsServer,
|
|
103
|
-
closeAll: () => Promise.all([
|
|
162
|
+
closeAll: () => Promise.all([
|
|
163
|
+
wsServer.close(),
|
|
164
|
+
listeners.closeSibling(),
|
|
165
|
+
grpc.close()
|
|
166
|
+
]).then(() => void 0),
|
|
104
167
|
addresses,
|
|
105
168
|
warnings,
|
|
106
169
|
boundPort,
|
|
107
|
-
uiBase: `http://${options.host === "localhost" ? "127.0.0.1" : options.host}:${boundPort}
|
|
170
|
+
uiBase: `http://${options.host === "localhost" ? "127.0.0.1" : options.host}:${boundPort}`,
|
|
171
|
+
grpcAddress: grpc.address
|
|
108
172
|
};
|
|
109
173
|
}
|
|
110
174
|
function buildAgentEnv(uiBase, logPrompts) {
|
|
@@ -169,7 +233,8 @@ async function runClaudeSubcommand(argv) {
|
|
|
169
233
|
}
|
|
170
234
|
const receiver = await startReceiver({
|
|
171
235
|
port: opts.port,
|
|
172
|
-
host: opts.host
|
|
236
|
+
host: opts.host,
|
|
237
|
+
grpcPort: 4317
|
|
173
238
|
});
|
|
174
239
|
const env = buildAgentEnv(receiver.uiBase, opts.logPrompts);
|
|
175
240
|
process.stdout.write(`\n autotel-devtools — Claude Code\n\n`);
|
|
@@ -214,6 +279,7 @@ async function main() {
|
|
|
214
279
|
process.stdout.write(` Listening: ${addresses.join(" + ")}\n`);
|
|
215
280
|
process.stdout.write(` UI: ${uiBase} (open in a browser)\n`);
|
|
216
281
|
process.stdout.write(` OTLP: ${uiBase}/v1/traces\n`);
|
|
282
|
+
process.stdout.write(` OTLP/gRPC: ${receiver.grpcAddress}\n`);
|
|
217
283
|
process.stdout.write(` WebSocket: ${uiBase.replace("http", "ws")}/ws\n\n`);
|
|
218
284
|
process.stdout.write(` Embed in your app — paste into your HTML; a floating panel appears automatically:\n`);
|
|
219
285
|
process.stdout.write(` <script src="${uiBase}/widget.js"><\/script>\n\n`);
|
|
@@ -222,6 +288,7 @@ async function main() {
|
|
|
222
288
|
process.stdout.write(` Or point any OTLP exporter at this receiver:\n`);
|
|
223
289
|
process.stdout.write(` OTEL_EXPORTER_OTLP_PROTOCOL=http/json\n`);
|
|
224
290
|
process.stdout.write(` OTEL_EXPORTER_OTLP_ENDPOINT=${uiBase}\n\n`);
|
|
291
|
+
process.stdout.write(` Or use gRPC: OTEL_EXPORTER_OTLP_ENDPOINT=http://${receiver.grpcAddress}\n\n`);
|
|
225
292
|
process.stdout.write(` Verify ingestion: curl -s ${uiBase}/v1/traces\n\n`);
|
|
226
293
|
for (const w of warnings) process.stdout.write(` ⚠ ${w}\n`);
|
|
227
294
|
if (warnings.length > 0) process.stdout.write("\n");
|