@kitlangton/motel 0.1.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/AGENTS.md +142 -0
- package/LICENSE +21 -0
- package/README.md +199 -0
- package/package.json +92 -0
- package/src/App.tsx +217 -0
- package/src/cli.ts +258 -0
- package/src/config.ts +39 -0
- package/src/daemon.test.ts +59 -0
- package/src/daemon.ts +398 -0
- package/src/domain.ts +233 -0
- package/src/httpApi.ts +384 -0
- package/src/index.tsx +18 -0
- package/src/instructions.ts +72 -0
- package/src/localServer.ts +699 -0
- package/src/locator.ts +138 -0
- package/src/mcp.ts +260 -0
- package/src/motel.ts +86 -0
- package/src/motelClient.ts +201 -0
- package/src/otlp.ts +142 -0
- package/src/queryFilters.ts +39 -0
- package/src/registry.ts +86 -0
- package/src/runtime.ts +38 -0
- package/src/server.ts +10 -0
- package/src/services/LogQueryService.ts +43 -0
- package/src/services/TelemetryStore.ts +1821 -0
- package/src/services/TraceQueryService.ts +71 -0
- package/src/telemetry.test.ts +726 -0
- package/src/ui/ServiceLogs.tsx +112 -0
- package/src/ui/SpanDetail.tsx +134 -0
- package/src/ui/SpanDetailFull.tsx +224 -0
- package/src/ui/SpanDetailPane.tsx +91 -0
- package/src/ui/TraceDetailsPane.tsx +169 -0
- package/src/ui/TraceList.tsx +128 -0
- package/src/ui/Waterfall.tsx +412 -0
- package/src/ui/app/TraceListPane.tsx +34 -0
- package/src/ui/app/TraceWorkspace.tsx +254 -0
- package/src/ui/app/useAppLayout.ts +79 -0
- package/src/ui/app/useTraceScreenData.ts +411 -0
- package/src/ui/format.ts +119 -0
- package/src/ui/primitives.tsx +170 -0
- package/src/ui/state.ts +137 -0
- package/src/ui/theme.ts +153 -0
- package/src/ui/traceDetailsWidth.repro.test.ts +115 -0
- package/src/ui/traceSortNav.repro.seed.ts +62 -0
- package/src/ui/traceSortNav.repro.test.ts +220 -0
- package/src/ui/useKeyboardNav.ts +532 -0
- package/src/ui/waterfallNav.repro.seed.ts +86 -0
- package/src/ui/waterfallNav.repro.test.ts +263 -0
- package/src/ui/waterfallNav.test.ts +422 -0
- package/src/ui/waterfallNav.ts +75 -0
- package/web/dist/assets/index-BEKIiisE.js +27 -0
- package/web/dist/assets/index-DzuHNBGV.css +2 -0
- package/web/dist/index.html +13 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
## Commands
|
|
4
|
+
- Install deps: `bun install`
|
|
5
|
+
- Run the TUI: `bun run dev` or `bun run start`
|
|
6
|
+
- Run the local server only: `bun run server`
|
|
7
|
+
- Run tests: `bun run test`
|
|
8
|
+
- Query services via CLI: `bun run cli services`
|
|
9
|
+
- Query traces via CLI: `bun run cli traces <service> [limit]`
|
|
10
|
+
- Query a span via CLI: `bun run cli span <span-id>`
|
|
11
|
+
- Query spans for one trace: `bun run cli trace-spans <trace-id>`
|
|
12
|
+
- Search spans via CLI: `bun run cli search-spans [service] [operation] [parent=<operation>] [attr.key=value ...]`
|
|
13
|
+
- Search traces via CLI: `bun run cli search-traces <service> [operation] [attr.key=value ...]`
|
|
14
|
+
- Query trace stats via CLI: `bun run cli trace-stats <groupBy> <agg> [service] [attr.key=value ...]`
|
|
15
|
+
- Query logs via CLI: `bun run cli logs <service>`
|
|
16
|
+
- Search logs via CLI: `bun run cli search-logs <service> [body] [attr.key=value ...]`
|
|
17
|
+
- Query log stats via CLI: `bun run cli log-stats <groupBy> [service] [attr.key=value ...]`
|
|
18
|
+
- Query logs for one trace: `bun run cli trace-logs <trace-id>`
|
|
19
|
+
- Query logs for one span: `bun run cli span-logs <span-id>`
|
|
20
|
+
- Query facets via CLI: `bun run cli facets <traces|logs> <field>`
|
|
21
|
+
- Print Effect setup instructions: `bun run instructions`
|
|
22
|
+
- Build the web UI: `bun run web:build`
|
|
23
|
+
- Dev the web UI (with hot reload): `bun run web:dev`
|
|
24
|
+
- Typecheck: `bun run typecheck`
|
|
25
|
+
|
|
26
|
+
## Verification
|
|
27
|
+
- The built-in verification step is `bun run typecheck`.
|
|
28
|
+
- For runtime verification, start the TUI or server once, then query `http://127.0.0.1:27686/api/services`, `http://127.0.0.1:27686/api/spans/<span-id>`, `http://127.0.0.1:27686/openapi.json`, and `bun run cli logs motel-otel-tui`.
|
|
29
|
+
- For span-centric debugging, use `http://127.0.0.1:27686/api/spans/search?...`, `http://127.0.0.1:27686/api/spans/<span-id>/logs`, and `http://127.0.0.1:27686/api/traces/<trace-id>/spans`.
|
|
30
|
+
|
|
31
|
+
## API Notes
|
|
32
|
+
- List and search endpoints return a `meta` object with `limit`, `lookback`, `returned`, `truncated`, and `nextCursor`.
|
|
33
|
+
- `/api/traces` and `/api/traces/search` return summaries by default. Use `/api/traces/<trace-id>` for the full trace tree.
|
|
34
|
+
- `/api/logs` and `/api/logs/search` support `severity` (e.g. `?severity=ERROR`), case-insensitive body search, and `attrContains.<key>=<substring>` for substring search inside attribute values.
|
|
35
|
+
- `/api/spans/search` supports `traceId` to scope to one trace, `attr.<key>=<value>` for exact match, and `attrContains.<key>=<substring>` for case-insensitive substring search inside attribute values.
|
|
36
|
+
- `/api/ai/calls` searches AI SDK calls (streamText, generateText, etc.) with first-class filters for `model`, `provider`, `sessionId`, `functionId`, `operation`, `status`, `text` (cross-field search), and returns compact summaries with previews and token usage.
|
|
37
|
+
- `/api/ai/calls/<span-id>` returns the full detail of a single AI call including complete prompt messages, response text, tool calls, timing, and correlated logs.
|
|
38
|
+
- `/api/ai/stats` aggregates AI call statistics by `provider`, `model`, `functionId`, `sessionId`, or `status` with aggregations: `count`, `avg_duration`, `p95_duration`, `total_input_tokens`, `total_output_tokens`.
|
|
39
|
+
- `/api/docs` lists available documentation; `/api/docs/debug` and `/api/docs/effect` return the full skill content.
|
|
40
|
+
|
|
41
|
+
## Architecture
|
|
42
|
+
- `src/index.tsx` creates the OpenTUI renderer and mounts the app.
|
|
43
|
+
- `src/App.tsx` composes the top-level screen: header, footer, and the
|
|
44
|
+
drill-in workspace. Heavy logic is delegated to the modules below.
|
|
45
|
+
- `src/ui/app/useTraceScreenData.ts` owns the atoms and data-loading
|
|
46
|
+
effects for traces, logs, services, and cache warming.
|
|
47
|
+
- `src/ui/app/useAppLayout.ts` is the single source for layout math
|
|
48
|
+
(pane widths, body lines, viewport rows, drill-in level).
|
|
49
|
+
- `src/ui/app/TraceWorkspace.tsx` renders the drill-in state machine:
|
|
50
|
+
L0 (trace list), L1 (waterfall), L2 (span detail), plus the service
|
|
51
|
+
logs side mode.
|
|
52
|
+
- `src/ui/app/TraceListPane.tsx` wraps `TraceList` in a scrollbox with
|
|
53
|
+
the filter bar and list header.
|
|
54
|
+
- `src/ui/TraceList.tsx` renders trace rows (trace id, duration, span
|
|
55
|
+
count, relative age).
|
|
56
|
+
- `src/ui/Waterfall.tsx` renders the waterfall timeline with a
|
|
57
|
+
virtualised scroll viewport; `src/ui/waterfallNav.ts` is the pure
|
|
58
|
+
collapse/expand/walk resolver (unit-tested).
|
|
59
|
+
- `src/ui/TraceDetailsPane.tsx` is the L1 body: header + waterfall.
|
|
60
|
+
- `src/ui/SpanDetailPane.tsx` is the L2 body; renders
|
|
61
|
+
`src/ui/SpanDetail.tsx` below a header that owns the span identity.
|
|
62
|
+
- `src/ui/useKeyboardNav.ts` centralises the keyboard handlers and
|
|
63
|
+
cross-pane navigation state transitions.
|
|
64
|
+
- `src/cli.ts` exposes trace and log queries through a small local CLI wrapper.
|
|
65
|
+
- `src/runtime.ts` wires the Effect beta runtime and OTEL trace + log exporters.
|
|
66
|
+
- `src/localServer.ts` starts the local Bun OTLP/query server.
|
|
67
|
+
- `src/httpApi.ts` defines the typed Effect HttpApi surface and OpenAPI spec for the local server.
|
|
68
|
+
- `src/server.ts` runs the local server without the TUI.
|
|
69
|
+
- `src/instructions.ts` contains the copied setup instructions for other Effect apps.
|
|
70
|
+
- `src/services/TelemetryStore.ts` persists traces and logs in SQLite and exposes indexed queries.
|
|
71
|
+
- `src/services/TraceQueryService.ts` reads traces from the local store.
|
|
72
|
+
- `src/services/LogQueryService.ts` reads logs from the local store.
|
|
73
|
+
- `src/config.ts` is the source of truth for ports and env-driven OTEL settings.
|
|
74
|
+
- `web/` is a Vite + React SPA for the browser-based UI (Tailwind CSS, `@effect/atom-react`, `AtomHttpApi`).
|
|
75
|
+
- `web/src/api.ts` creates the typed `AtomHttpApi.Service` client from `src/httpApi.ts`.
|
|
76
|
+
- `web/src/pages/` contains route pages: TracesPage, TraceDetailPage, LogsPage, AiCallsPage.
|
|
77
|
+
- `web/src/components/` contains Waterfall and SpanDetail components.
|
|
78
|
+
- The server in `src/localServer.ts` serves `web/dist/` as static files with SPA fallback for non-API routes.
|
|
79
|
+
|
|
80
|
+
## Tests
|
|
81
|
+
- `bun test` runs the suite. Three kinds of tests live in the repo:
|
|
82
|
+
- `src/telemetry.test.ts` exercises the SQLite TelemetryStore with
|
|
83
|
+
OTLP payloads end-to-end.
|
|
84
|
+
- `src/ui/waterfallNav.test.ts` unit-tests the pure collapse/expand
|
|
85
|
+
resolver (no UI).
|
|
86
|
+
- `src/ui/*.repro.test.ts` drive the real TUI under `tuistory` to
|
|
87
|
+
reproduce regressions; each has a sibling `*.repro.seed.ts` that
|
|
88
|
+
seeds a deterministic trace into SQLite in a child process. These
|
|
89
|
+
are auto-skipped when `tuistory` isn't installed.
|
|
90
|
+
|
|
91
|
+
## Effect Observability Guidance
|
|
92
|
+
- Inspect the target repo’s existing Effect runtime and observability wiring before adding anything new.
|
|
93
|
+
- Prefer the repo’s existing Effect-native observability APIs if available.
|
|
94
|
+
- If `effect/unstable/observability` is already the best fit, prefer it over adding `@effect/opentelemetry`.
|
|
95
|
+
- Only add new OpenTelemetry SDK packages when the repo already uses them or they are clearly required.
|
|
96
|
+
- Merge telemetry into the main runtime once, not per-feature.
|
|
97
|
+
- Prefer structured log annotations so fields like `sessionID`, `modelID`, `providerID`, and `tool` are queryable.
|
|
98
|
+
|
|
99
|
+
## Local OTEL Ports
|
|
100
|
+
- Local API / UI base: `http://127.0.0.1:27686`
|
|
101
|
+
- OTLP HTTP traces: `http://127.0.0.1:27686/v1/traces`
|
|
102
|
+
- OTLP HTTP logs: `http://127.0.0.1:27686/v1/logs`
|
|
103
|
+
- Health: `http://127.0.0.1:27686/api/health`
|
|
104
|
+
|
|
105
|
+
## Env Vars
|
|
106
|
+
- `MOTEL_OTEL_ENABLED`: defaults to `false` (set to `true` to emit self-traces for debugging motel itself)
|
|
107
|
+
- `MOTEL_OTEL_SERVICE_NAME`: defaults to `motel-otel-tui`
|
|
108
|
+
- `MOTEL_OTEL_BASE_URL`: defaults to `http://127.0.0.1:27686`
|
|
109
|
+
- `MOTEL_OTEL_HOST`: defaults to `127.0.0.1`
|
|
110
|
+
- `MOTEL_OTEL_PORT`: defaults to `27686`
|
|
111
|
+
- `MOTEL_OTEL_EXPORTER_URL`: defaults to `http://127.0.0.1:27686/v1/traces`
|
|
112
|
+
- `MOTEL_OTEL_LOGS_EXPORTER_URL`: defaults to `http://127.0.0.1:27686/v1/logs`
|
|
113
|
+
- `MOTEL_OTEL_QUERY_URL`: defaults to `http://127.0.0.1:27686`
|
|
114
|
+
- `MOTEL_OTEL_DB_PATH`: defaults to `.motel-data/telemetry.sqlite`
|
|
115
|
+
- `MOTEL_OTEL_TRACE_LOOKBACK_MINUTES`: defaults to `1440` (24h)
|
|
116
|
+
- `MOTEL_OTEL_TRACE_LIMIT`: defaults to `100`
|
|
117
|
+
- `MOTEL_OTEL_LOG_LIMIT`: defaults to `80`
|
|
118
|
+
- `MOTEL_OTEL_RETENTION_HOURS`: defaults to `168` (7d)
|
|
119
|
+
- `MOTEL_OTEL_MAX_DB_SIZE_MB`: defaults to `256` (size-based retention cap)
|
|
120
|
+
|
|
121
|
+
## TUI Keys
|
|
122
|
+
- `?`: toggle shortcut help
|
|
123
|
+
- `j` / `k` or `up` / `down`: move trace or span selection
|
|
124
|
+
- `h` / `left`: collapse current span, or step to parent
|
|
125
|
+
- `l` / `right`: expand current span, or step to first child
|
|
126
|
+
- `ctrl-n` / `ctrl-p`: switch traces while staying in the details area
|
|
127
|
+
- `gg` / `home`: jump to the first trace or span
|
|
128
|
+
- `G` / `end`: jump to the last trace or span
|
|
129
|
+
- `ctrl-u` / `pageup`: page up
|
|
130
|
+
- `ctrl-d` / `pagedown`: page down
|
|
131
|
+
- `enter`: drill in one level (list → waterfall → span detail)
|
|
132
|
+
- `esc`: back out one level
|
|
133
|
+
- `tab`: toggle service logs view
|
|
134
|
+
- `[` / `]`: switch services
|
|
135
|
+
- `s`: cycle sort mode (recent → slowest → errors)
|
|
136
|
+
- `/`: enter filter mode (type to match on root operation name; `:error` restricts to failing traces)
|
|
137
|
+
- `a`: pause or resume auto-refresh
|
|
138
|
+
- `r`: refresh now
|
|
139
|
+
- `c`: copy setup instructions for another Effect app
|
|
140
|
+
- `o`: open selected trace in the browser
|
|
141
|
+
- `y`: copy selected trace or span id
|
|
142
|
+
- `q`: quit
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kit Langton
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# motel
|
|
2
|
+
|
|
3
|
+
A local OpenTelemetry ingest + TUI viewer for development, backed by SQLite.
|
|
4
|
+
Point your app's OTLP/HTTP exporters at the local motel server and browse
|
|
5
|
+
traces, spans, and logs from a terminal or the built-in web UI.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun add -g @kitlangton/motel
|
|
11
|
+
motel
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
(or `bunx @kitlangton/motel` for a one-off run without installing.)
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
- [Bun](https://bun.sh/) — v1.1 or newer
|
|
19
|
+
|
|
20
|
+
## Install the motel-debug skill
|
|
21
|
+
|
|
22
|
+
`motel` ships a companion skill that teaches agents (Claude Code, OpenCode,
|
|
23
|
+
Cursor, Codex, and 40+ others) how to debug with runtime evidence by
|
|
24
|
+
querying motel's local OTLP store. Install it with a one-liner via
|
|
25
|
+
[`npx skills`](https://github.com/vercel-labs/skills):
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Project-local (adds to .claude/skills, .agents/skills, etc.)
|
|
29
|
+
npx skills add kitlangton/motel --skill motel-debug
|
|
30
|
+
|
|
31
|
+
# Globally, available in every project
|
|
32
|
+
npx skills add kitlangton/motel --skill motel-debug -g
|
|
33
|
+
|
|
34
|
+
# Target a specific agent only
|
|
35
|
+
npx skills add kitlangton/motel --skill motel-debug -a claude-code
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The skill lives at [`skills/motel-debug/`](skills/motel-debug/) in this repo.
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
bun install
|
|
44
|
+
bun run dev
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`bun run dev` starts the local OTLP ingest server (on `http://127.0.0.1:27686`)
|
|
48
|
+
and launches the TUI. Press `?` once inside for the keyboard cheat sheet, or
|
|
49
|
+
`c` to copy paste-ready setup instructions for another Effect/OTEL app.
|
|
50
|
+
|
|
51
|
+
If you just want the server without the TUI (for example, to run it in the
|
|
52
|
+
background and browse the web UI):
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
bun run server
|
|
56
|
+
# then in another terminal
|
|
57
|
+
bun run web:dev
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Commands
|
|
61
|
+
|
|
62
|
+
- `bun install`
|
|
63
|
+
- `bun run server`
|
|
64
|
+
- `bun run dev`
|
|
65
|
+
- `bun run test`
|
|
66
|
+
- `bun run cli services`
|
|
67
|
+
- `bun run cli traces <service>`
|
|
68
|
+
- `bun run cli span <span-id>`
|
|
69
|
+
- `bun run cli search-traces <service> [operation]`
|
|
70
|
+
- `bun run cli trace-stats <groupBy> <agg> [service]`
|
|
71
|
+
- `bun run cli logs <service>`
|
|
72
|
+
- `bun run cli search-logs <service> [body]`
|
|
73
|
+
- `bun run cli log-stats <groupBy> [service]`
|
|
74
|
+
- `bun run cli trace-logs <trace-id>`
|
|
75
|
+
- `bun run cli facets <traces|logs> <field>`
|
|
76
|
+
- `bun run instructions`
|
|
77
|
+
- `bun run typecheck`
|
|
78
|
+
|
|
79
|
+
## Local ports
|
|
80
|
+
|
|
81
|
+
This repo uses one local Bun server with SQLite storage. No Docker is required.
|
|
82
|
+
|
|
83
|
+
- motel local API / UI base: `http://127.0.0.1:27686`
|
|
84
|
+
- OTLP HTTP traces: `http://127.0.0.1:27686/v1/traces`
|
|
85
|
+
- OTLP HTTP logs: `http://127.0.0.1:27686/v1/logs`
|
|
86
|
+
- health: `http://127.0.0.1:27686/api/health`
|
|
87
|
+
|
|
88
|
+
Other local apps can send telemetry to:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
http://127.0.0.1:27686/v1/traces
|
|
92
|
+
http://127.0.0.1:27686/v1/logs
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Agents and scripts can query traces and logs from the local API:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
http://127.0.0.1:27686/api/services
|
|
99
|
+
http://127.0.0.1:27686/api/traces?service=<service>&limit=20&lookback=1h
|
|
100
|
+
http://127.0.0.1:27686/api/traces/search?service=<service>&operation=proxy&status=error&attr.sessionID=<session-id>
|
|
101
|
+
http://127.0.0.1:27686/api/traces/stats?groupBy=operation&agg=p95_duration&service=<service>
|
|
102
|
+
http://127.0.0.1:27686/api/spans/<span-id>
|
|
103
|
+
http://127.0.0.1:27686/api/spans/<span-id>/logs
|
|
104
|
+
http://127.0.0.1:27686/api/spans/search?service=<service>&operation=Format.file&parentOperation=Tool.write&attr.sessionID=<session-id>
|
|
105
|
+
http://127.0.0.1:27686/api/traces/<trace-id>/spans
|
|
106
|
+
http://127.0.0.1:27686/api/logs?service=<service>&body=proxy_request
|
|
107
|
+
http://127.0.0.1:27686/api/logs?service=<service>&attr.service.name=<service>
|
|
108
|
+
http://127.0.0.1:27686/api/logs/stats?groupBy=severity&agg=count&service=<service>
|
|
109
|
+
http://127.0.0.1:27686/api/facets?type=logs&field=severity
|
|
110
|
+
http://127.0.0.1:27686/openapi.json
|
|
111
|
+
http://127.0.0.1:27686/docs
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## TUI keys
|
|
115
|
+
|
|
116
|
+
- `?`: show or hide keyboard shortcut help
|
|
117
|
+
- `j` / `k` or `up` / `down`: move selection
|
|
118
|
+
- `ctrl-n` / `ctrl-p`: switch traces even while in trace details
|
|
119
|
+
- `gg` or `home`: jump to the first trace or first span
|
|
120
|
+
- `G` or `end`: jump to the last trace or last span
|
|
121
|
+
- `ctrl-u` / `pageup`: move up by one page
|
|
122
|
+
- `ctrl-d` / `pagedown`: move down by one page
|
|
123
|
+
- `l`: toggle service logs mode
|
|
124
|
+
- `[` / `]`: switch service
|
|
125
|
+
- `enter`: enter span navigation or open selected span detail
|
|
126
|
+
- `esc`: leave span detail or span navigation
|
|
127
|
+
- `r`: refresh
|
|
128
|
+
- `c`: copy a paste-ready Effect setup prompt for another app
|
|
129
|
+
- `o`: open selected trace in browser
|
|
130
|
+
- `q`: quit
|
|
131
|
+
|
|
132
|
+
## How It Works
|
|
133
|
+
|
|
134
|
+
`motel` now has one local service process:
|
|
135
|
+
|
|
136
|
+
- the local Bun server receives OTLP traces and logs on `http://127.0.0.1:27686`
|
|
137
|
+
- it stores telemetry in SQLite at `.motel-data/telemetry.sqlite`
|
|
138
|
+
- it exposes query endpoints on the same base URL
|
|
139
|
+
|
|
140
|
+
So yes: another service has to point its OTEL exporters at this local motel instance.
|
|
141
|
+
|
|
142
|
+
## Privacy Note
|
|
143
|
+
|
|
144
|
+
`motel` is a local observability tool, and it can store sensitive telemetry content if the upstream app emits it.
|
|
145
|
+
|
|
146
|
+
- correlated logs may include secrets, tokens, or PII if your app logs them
|
|
147
|
+
- AI call data may include prompt previews, response previews, full prompt content, response text, tool metadata, and provider metadata
|
|
148
|
+
- treat the local SQLite store as sensitive development data when using motel against real workloads
|
|
149
|
+
|
|
150
|
+
The easiest flow is:
|
|
151
|
+
|
|
152
|
+
1. Run `bun run dev` here. That starts the local server if needed and then launches the TUI.
|
|
153
|
+
2. In `motel`, press `c`.
|
|
154
|
+
3. Paste the copied instructions into an agent working in the other service.
|
|
155
|
+
4. Have that service export OTEL traces to `http://127.0.0.1:27686/v1/traces` and OTEL logs to `http://127.0.0.1:27686/v1/logs`.
|
|
156
|
+
5. Refresh `motel`, switch to that service with `[` / `]`, and use `l` or `enter` to inspect logs under a trace or span.
|
|
157
|
+
|
|
158
|
+
## For Agents
|
|
159
|
+
|
|
160
|
+
An agent does not need to talk to the TUI.
|
|
161
|
+
|
|
162
|
+
List and search endpoints now return a `meta` object with `limit`, `lookback`, `returned`, `truncated`, and `nextCursor` so callers can page safely instead of assuming they received all results.
|
|
163
|
+
|
|
164
|
+
Use one of these:
|
|
165
|
+
|
|
166
|
+
1. motel HTTP API directly
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
curl http://127.0.0.1:27686/api/services
|
|
170
|
+
curl "http://127.0.0.1:27686/api/traces?service=my-service&limit=20&lookback=1h"
|
|
171
|
+
curl http://127.0.0.1:27686/api/traces/<trace-id>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
2. The local CLI wrapper in this repo
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
bun run cli services
|
|
178
|
+
bun run cli traces my-service 20
|
|
179
|
+
bun run cli span <span-id>
|
|
180
|
+
bun run cli trace-spans <trace-id>
|
|
181
|
+
bun run cli search-spans my-service Format.file parent=Tool.write attr.sessionID=sess_123
|
|
182
|
+
bun run cli search-traces my-service proxy attr.sessionID=sess_123
|
|
183
|
+
bun run cli trace-stats operation p95_duration my-service attr.modelID=gpt-5.4
|
|
184
|
+
bun run cli trace <trace-id>
|
|
185
|
+
bun run cli logs my-service
|
|
186
|
+
bun run cli search-logs my-service timeout attr.tool=search
|
|
187
|
+
bun run cli log-stats severity my-service attr.tool=search
|
|
188
|
+
bun run cli trace-logs <trace-id>
|
|
189
|
+
bun run cli span-logs <span-id>
|
|
190
|
+
bun run cli facets logs severity
|
|
191
|
+
bun run instructions
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Recommended shape going forward:
|
|
195
|
+
|
|
196
|
+
1. Keep motel as the single ingest point for apps.
|
|
197
|
+
2. Keep SQLite as the local source of truth.
|
|
198
|
+
3. Keep `motel` as the interactive viewer.
|
|
199
|
+
4. Keep the CLI and HTTP API as the agent/script interfaces.
|
package/package.json
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kitlangton/motel",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A local OpenTelemetry ingest + TUI viewer for development, backed by SQLite.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Kit Langton",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/kitlangton/motel.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/kitlangton/motel#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/kitlangton/motel/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"opentelemetry",
|
|
18
|
+
"otel",
|
|
19
|
+
"tracing",
|
|
20
|
+
"observability",
|
|
21
|
+
"tui",
|
|
22
|
+
"sqlite",
|
|
23
|
+
"effect",
|
|
24
|
+
"opentui"
|
|
25
|
+
],
|
|
26
|
+
"workspaces": ["web"],
|
|
27
|
+
"engines": {
|
|
28
|
+
"bun": ">=1.1.0"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"src",
|
|
35
|
+
"web/dist",
|
|
36
|
+
"LICENSE",
|
|
37
|
+
"README.md",
|
|
38
|
+
"AGENTS.md"
|
|
39
|
+
],
|
|
40
|
+
"bin": {
|
|
41
|
+
"motel": "src/motel.ts",
|
|
42
|
+
"motel-mcp": "src/mcp.ts"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"clear-motel-debug": "bun run skills/motel-debug/scripts/clear-motel-debug.ts",
|
|
46
|
+
"dev": "bun run src/motel.ts tui",
|
|
47
|
+
"start": "bun run src/motel.ts tui",
|
|
48
|
+
"daemon": "bun run src/motel.ts daemon",
|
|
49
|
+
"status": "bun run src/motel.ts status",
|
|
50
|
+
"stop": "bun run src/motel.ts stop",
|
|
51
|
+
"server": "bun run src/motel.ts server",
|
|
52
|
+
"mcp": "bun run src/mcp.ts",
|
|
53
|
+
"test": "bun test",
|
|
54
|
+
"cli": "bun run src/cli.ts",
|
|
55
|
+
"instructions": "bun run src/cli.ts instructions",
|
|
56
|
+
"services": "bun run src/cli.ts services",
|
|
57
|
+
"logs": "bun run src/cli.ts logs",
|
|
58
|
+
"trace-logs": "bun run src/cli.ts trace-logs",
|
|
59
|
+
"span-logs": "bun run src/cli.ts span-logs",
|
|
60
|
+
"span": "bun run src/cli.ts span",
|
|
61
|
+
"trace-spans": "bun run src/cli.ts trace-spans",
|
|
62
|
+
"search-spans": "bun run src/cli.ts search-spans",
|
|
63
|
+
"trace-stats": "bun run src/cli.ts trace-stats",
|
|
64
|
+
"log-stats": "bun run src/cli.ts log-stats",
|
|
65
|
+
"web:dev": "cd web && npx vite",
|
|
66
|
+
"web:build": "cd web && npx vite build",
|
|
67
|
+
"typecheck": "tsc --noEmit",
|
|
68
|
+
"prepublishOnly": "bun run web:build"
|
|
69
|
+
},
|
|
70
|
+
"devDependencies": {
|
|
71
|
+
"@types/bun": "^1.3.12",
|
|
72
|
+
"@types/react": "^19.2.14",
|
|
73
|
+
"typescript": "^6.0.2"
|
|
74
|
+
},
|
|
75
|
+
"dependencies": {
|
|
76
|
+
"@effect/atom-react": "^4.0.0-beta.49",
|
|
77
|
+
"@effect/opentelemetry": "^4.0.0-beta.49",
|
|
78
|
+
"@effect/platform-bun": "^4.0.0-beta.49",
|
|
79
|
+
"@opentelemetry/api": "^1.9.0",
|
|
80
|
+
"@opentelemetry/exporter-logs-otlp-http": "^0.214.0",
|
|
81
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.211.0",
|
|
82
|
+
"@opentelemetry/sdk-logs": "^0.214.0",
|
|
83
|
+
"@opentelemetry/sdk-node": "^0.214.0",
|
|
84
|
+
"@opentelemetry/sdk-trace-base": "^2.5.0",
|
|
85
|
+
"@opentelemetry/sdk-trace-node": "^2.6.1",
|
|
86
|
+
"@opentui/core": "^0.1.99",
|
|
87
|
+
"@opentui/react": "^0.1.99",
|
|
88
|
+
"effect": "^4.0.0-beta.49",
|
|
89
|
+
"react": "^19.2.5",
|
|
90
|
+
"scheduler": "^0.27.0"
|
|
91
|
+
}
|
|
92
|
+
}
|
package/src/App.tsx
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { RGBA, TextAttributes, type ScrollBoxRenderable } from "@opentui/core"
|
|
2
|
+
import { useAtom } from "@effect/atom-react"
|
|
3
|
+
import { useTerminalDimensions } from "@opentui/react"
|
|
4
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from "react"
|
|
5
|
+
import { formatTimestamp, traceRowId } from "./ui/format.ts"
|
|
6
|
+
import { Divider, FooterHints, HelpModal, PlainLine, SplitDivider, TextLine } from "./ui/primitives.tsx"
|
|
7
|
+
import { useAppLayout } from "./ui/app/useAppLayout.ts"
|
|
8
|
+
import { useTraceScreenData } from "./ui/app/useTraceScreenData.ts"
|
|
9
|
+
import { TraceWorkspace } from "./ui/app/TraceWorkspace.tsx"
|
|
10
|
+
import { noticeAtom, persistSelectedTheme, selectedThemeAtom } from "./ui/state.ts"
|
|
11
|
+
import { applyTheme, colors, SEPARATOR, themeLabel } from "./ui/theme.ts"
|
|
12
|
+
import { getVisibleSpans } from "./ui/Waterfall.tsx"
|
|
13
|
+
import { useKeyboardNav } from "./ui/useKeyboardNav.ts"
|
|
14
|
+
|
|
15
|
+
export const App = () => {
|
|
16
|
+
const { width, height } = useTerminalDimensions()
|
|
17
|
+
const [notice, setNotice] = useAtom(noticeAtom)
|
|
18
|
+
const [selectedTheme] = useAtom(selectedThemeAtom)
|
|
19
|
+
applyTheme(selectedTheme)
|
|
20
|
+
const {
|
|
21
|
+
traceState,
|
|
22
|
+
traceDetailState,
|
|
23
|
+
logState,
|
|
24
|
+
serviceLogState,
|
|
25
|
+
selectedServiceLogIndex,
|
|
26
|
+
setSelectedServiceLogIndex,
|
|
27
|
+
selectedTraceIndex,
|
|
28
|
+
setSelectedTraceIndex,
|
|
29
|
+
selectedTraceService,
|
|
30
|
+
selectedSpanIndex,
|
|
31
|
+
setSelectedSpanIndex,
|
|
32
|
+
detailView,
|
|
33
|
+
showHelp,
|
|
34
|
+
setShowHelp,
|
|
35
|
+
collapsedSpanIds,
|
|
36
|
+
autoRefresh,
|
|
37
|
+
filterMode,
|
|
38
|
+
filterText,
|
|
39
|
+
traceSort,
|
|
40
|
+
selectedTraceSummary,
|
|
41
|
+
selectedTrace,
|
|
42
|
+
filteredTraces,
|
|
43
|
+
} = useTraceScreenData()
|
|
44
|
+
|
|
45
|
+
const layout = useAppLayout({ width, height, notice, detailView, selectedSpanIndex })
|
|
46
|
+
const {
|
|
47
|
+
contentWidth,
|
|
48
|
+
isWideLayout,
|
|
49
|
+
viewLevel,
|
|
50
|
+
footerNotice,
|
|
51
|
+
footerHeight,
|
|
52
|
+
leftPaneWidth,
|
|
53
|
+
rightPaneWidth,
|
|
54
|
+
leftContentWidth,
|
|
55
|
+
headerFooterWidth,
|
|
56
|
+
wideBodyLines,
|
|
57
|
+
narrowBodyLines,
|
|
58
|
+
traceViewportRows,
|
|
59
|
+
tracePageSize,
|
|
60
|
+
spanPageSize,
|
|
61
|
+
} = layout
|
|
62
|
+
|
|
63
|
+
const noticeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
64
|
+
const traceListScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
65
|
+
|
|
66
|
+
const flashNotice = (message: string) => {
|
|
67
|
+
if (noticeTimeoutRef.current !== null) {
|
|
68
|
+
clearTimeout(noticeTimeoutRef.current)
|
|
69
|
+
}
|
|
70
|
+
setNotice(message)
|
|
71
|
+
noticeTimeoutRef.current = globalThis.setTimeout(() => {
|
|
72
|
+
setNotice((current) => (current === message ? null : current))
|
|
73
|
+
}, 2500)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
useEffect(() => () => {
|
|
77
|
+
if (noticeTimeoutRef.current !== null) {
|
|
78
|
+
clearTimeout(noticeTimeoutRef.current)
|
|
79
|
+
}
|
|
80
|
+
}, [setNotice])
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
persistSelectedTheme(selectedTheme)
|
|
84
|
+
}, [selectedTheme])
|
|
85
|
+
|
|
86
|
+
useLayoutEffect(() => {
|
|
87
|
+
const box = traceListScrollRef.current
|
|
88
|
+
const traceId = selectedTraceSummary?.traceId
|
|
89
|
+
if (!box || !traceId) return
|
|
90
|
+
const indexInList = filteredTraces.findIndex((trace) => trace.traceId === traceId)
|
|
91
|
+
if (indexInList < 0) return
|
|
92
|
+
const currentTop = box.scrollTop
|
|
93
|
+
const viewportRows = Math.max(1, traceViewportRows)
|
|
94
|
+
let nextTop = currentTop
|
|
95
|
+
if (indexInList < currentTop) {
|
|
96
|
+
nextTop = indexInList
|
|
97
|
+
} else if (indexInList >= currentTop + viewportRows) {
|
|
98
|
+
nextTop = indexInList - viewportRows + 1
|
|
99
|
+
}
|
|
100
|
+
const maxTop = Math.max(0, filteredTraces.length - viewportRows)
|
|
101
|
+
nextTop = Math.max(0, Math.min(nextTop, maxTop))
|
|
102
|
+
if (nextTop !== currentTop) {
|
|
103
|
+
box.scrollTop = nextTop
|
|
104
|
+
}
|
|
105
|
+
}, [filteredTraces, selectedTraceIndex, selectedTraceSummary?.traceId, traceSort, traceViewportRows])
|
|
106
|
+
|
|
107
|
+
const { spanNavActive } = useKeyboardNav({
|
|
108
|
+
selectedTrace,
|
|
109
|
+
filteredTraces,
|
|
110
|
+
isWideLayout,
|
|
111
|
+
wideBodyLines,
|
|
112
|
+
narrowBodyLines,
|
|
113
|
+
tracePageSize,
|
|
114
|
+
spanPageSize,
|
|
115
|
+
flashNotice,
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const headerServiceLabel = selectedTraceService ?? "none"
|
|
119
|
+
const autoLabel = autoRefresh ? "● live" : "○ paused"
|
|
120
|
+
const headerRight = traceState.fetchedAt
|
|
121
|
+
? `${autoLabel} ${formatTimestamp(traceState.fetchedAt)}`
|
|
122
|
+
: traceState.status === "loading"
|
|
123
|
+
? "loading traces..."
|
|
124
|
+
: ""
|
|
125
|
+
const headerLeftLen = "MOTEL".length + SEPARATOR.length + headerServiceLabel.length
|
|
126
|
+
const headerGap = Math.max(2, headerFooterWidth - headerLeftLen - headerRight.length)
|
|
127
|
+
const visibleFooterNotice = footerNotice
|
|
128
|
+
|
|
129
|
+
const selectTraceById = useCallback((traceId: string) => {
|
|
130
|
+
const index = traceState.data.findIndex((trace) => trace.traceId === traceId)
|
|
131
|
+
if (index >= 0) setSelectedTraceIndex(index)
|
|
132
|
+
}, [setSelectedTraceIndex, traceState.data])
|
|
133
|
+
|
|
134
|
+
const selectSpan = useCallback((index: number) => {
|
|
135
|
+
if (!selectedTrace) return
|
|
136
|
+
const visibleCount = getVisibleSpans(selectedTrace.spans, collapsedSpanIds).length
|
|
137
|
+
setSelectedSpanIndex(Math.max(0, Math.min(index, visibleCount - 1)))
|
|
138
|
+
}, [collapsedSpanIds, selectedTrace, setSelectedSpanIndex])
|
|
139
|
+
|
|
140
|
+
const traceListProps = useMemo(() => ({
|
|
141
|
+
traces: filteredTraces,
|
|
142
|
+
selectedTraceId: selectedTraceSummary?.traceId ?? null,
|
|
143
|
+
status: traceState.status,
|
|
144
|
+
error: traceState.error,
|
|
145
|
+
contentWidth: leftContentWidth,
|
|
146
|
+
services: traceState.services,
|
|
147
|
+
selectedService: selectedTraceService,
|
|
148
|
+
focused: !spanNavActive,
|
|
149
|
+
filterText: filterText || undefined,
|
|
150
|
+
sortMode: traceSort,
|
|
151
|
+
totalCount: filterText ? traceState.data.length : undefined,
|
|
152
|
+
onSelectTrace: selectTraceById,
|
|
153
|
+
} as const), [filteredTraces, selectedTraceSummary?.traceId, traceState.status, traceState.error, leftContentWidth, traceState.services, selectedTraceService, spanNavActive, filterText, traceSort, traceState.data.length, selectTraceById])
|
|
154
|
+
|
|
155
|
+
const filteredSpans = selectedTrace ? getVisibleSpans(selectedTrace.spans, collapsedSpanIds) : []
|
|
156
|
+
const selectedSpan = selectedSpanIndex !== null ? filteredSpans[selectedSpanIndex] ?? null : null
|
|
157
|
+
const selectedSpanLogs = useMemo(
|
|
158
|
+
() => selectedSpan ? logState.data.filter((log) => log.spanId === selectedSpan.spanId) : [],
|
|
159
|
+
[selectedSpan, logState.data],
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
const showSplit = isWideLayout
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<box width={width ?? 100} height={height ?? 24} flexGrow={1} flexDirection="column" backgroundColor={RGBA.fromHex(colors.screenBg)}>
|
|
166
|
+
<box paddingLeft={1} paddingRight={1} flexDirection="column">
|
|
167
|
+
<TextLine>
|
|
168
|
+
<span fg={colors.muted} attributes={TextAttributes.BOLD}>MOTEL</span>
|
|
169
|
+
<span fg={colors.separator}>{SEPARATOR}</span>
|
|
170
|
+
<span fg={colors.muted}>{headerServiceLabel}</span>
|
|
171
|
+
<span fg={colors.muted}>{" ".repeat(headerGap)}</span>
|
|
172
|
+
<span fg={colors.muted} attributes={TextAttributes.BOLD}>{headerRight}</span>
|
|
173
|
+
</TextLine>
|
|
174
|
+
</box>
|
|
175
|
+
{showSplit
|
|
176
|
+
? <SplitDivider leftWidth={leftPaneWidth} junction={"┬"} rightWidth={rightPaneWidth} />
|
|
177
|
+
: <Divider width={contentWidth} />}
|
|
178
|
+
<TraceWorkspace
|
|
179
|
+
layout={layout}
|
|
180
|
+
detailView={detailView}
|
|
181
|
+
filterMode={filterMode}
|
|
182
|
+
filterText={filterText}
|
|
183
|
+
traceListProps={traceListProps}
|
|
184
|
+
traceListScrollRef={traceListScrollRef}
|
|
185
|
+
selectedTraceService={selectedTraceService}
|
|
186
|
+
serviceLogState={serviceLogState}
|
|
187
|
+
selectedServiceLogIndex={selectedServiceLogIndex}
|
|
188
|
+
setSelectedServiceLogIndex={setSelectedServiceLogIndex}
|
|
189
|
+
traceDetailState={traceDetailState}
|
|
190
|
+
selectedTrace={selectedTrace}
|
|
191
|
+
selectedTraceSummary={selectedTraceSummary}
|
|
192
|
+
logState={logState}
|
|
193
|
+
selectedSpanIndex={selectedSpanIndex}
|
|
194
|
+
collapsedSpanIds={collapsedSpanIds}
|
|
195
|
+
viewLevel={viewLevel}
|
|
196
|
+
selectedSpan={selectedSpan}
|
|
197
|
+
selectedSpanLogs={selectedSpanLogs}
|
|
198
|
+
selectSpan={selectSpan}
|
|
199
|
+
/>
|
|
200
|
+
{footerHeight > 0 ? (
|
|
201
|
+
<>
|
|
202
|
+
{showSplit
|
|
203
|
+
? <SplitDivider leftWidth={leftPaneWidth} junction={"┴"} rightWidth={rightPaneWidth} />
|
|
204
|
+
: <Divider width={contentWidth} />}
|
|
205
|
+
<box paddingLeft={1} paddingRight={1} flexDirection="column" height={footerHeight}>
|
|
206
|
+
{visibleFooterNotice ? (
|
|
207
|
+
<PlainLine text={visibleFooterNotice} fg={colors.count} />
|
|
208
|
+
) : (
|
|
209
|
+
<FooterHints spanNavActive={spanNavActive} detailView={detailView} autoRefresh={autoRefresh} width={headerFooterWidth} />
|
|
210
|
+
)}
|
|
211
|
+
</box>
|
|
212
|
+
</>
|
|
213
|
+
) : null}
|
|
214
|
+
{showHelp ? <HelpModal width={width ?? 100} height={height ?? 24} autoRefresh={autoRefresh} themeLabel={themeLabel(selectedTheme)} onClose={() => setShowHelp(false)} /> : null}
|
|
215
|
+
</box>
|
|
216
|
+
)
|
|
217
|
+
}
|