@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.
Files changed (53) hide show
  1. package/AGENTS.md +142 -0
  2. package/LICENSE +21 -0
  3. package/README.md +199 -0
  4. package/package.json +92 -0
  5. package/src/App.tsx +217 -0
  6. package/src/cli.ts +258 -0
  7. package/src/config.ts +39 -0
  8. package/src/daemon.test.ts +59 -0
  9. package/src/daemon.ts +398 -0
  10. package/src/domain.ts +233 -0
  11. package/src/httpApi.ts +384 -0
  12. package/src/index.tsx +18 -0
  13. package/src/instructions.ts +72 -0
  14. package/src/localServer.ts +699 -0
  15. package/src/locator.ts +138 -0
  16. package/src/mcp.ts +260 -0
  17. package/src/motel.ts +86 -0
  18. package/src/motelClient.ts +201 -0
  19. package/src/otlp.ts +142 -0
  20. package/src/queryFilters.ts +39 -0
  21. package/src/registry.ts +86 -0
  22. package/src/runtime.ts +38 -0
  23. package/src/server.ts +10 -0
  24. package/src/services/LogQueryService.ts +43 -0
  25. package/src/services/TelemetryStore.ts +1821 -0
  26. package/src/services/TraceQueryService.ts +71 -0
  27. package/src/telemetry.test.ts +726 -0
  28. package/src/ui/ServiceLogs.tsx +112 -0
  29. package/src/ui/SpanDetail.tsx +134 -0
  30. package/src/ui/SpanDetailFull.tsx +224 -0
  31. package/src/ui/SpanDetailPane.tsx +91 -0
  32. package/src/ui/TraceDetailsPane.tsx +169 -0
  33. package/src/ui/TraceList.tsx +128 -0
  34. package/src/ui/Waterfall.tsx +412 -0
  35. package/src/ui/app/TraceListPane.tsx +34 -0
  36. package/src/ui/app/TraceWorkspace.tsx +254 -0
  37. package/src/ui/app/useAppLayout.ts +79 -0
  38. package/src/ui/app/useTraceScreenData.ts +411 -0
  39. package/src/ui/format.ts +119 -0
  40. package/src/ui/primitives.tsx +170 -0
  41. package/src/ui/state.ts +137 -0
  42. package/src/ui/theme.ts +153 -0
  43. package/src/ui/traceDetailsWidth.repro.test.ts +115 -0
  44. package/src/ui/traceSortNav.repro.seed.ts +62 -0
  45. package/src/ui/traceSortNav.repro.test.ts +220 -0
  46. package/src/ui/useKeyboardNav.ts +532 -0
  47. package/src/ui/waterfallNav.repro.seed.ts +86 -0
  48. package/src/ui/waterfallNav.repro.test.ts +263 -0
  49. package/src/ui/waterfallNav.test.ts +422 -0
  50. package/src/ui/waterfallNav.ts +75 -0
  51. package/web/dist/assets/index-BEKIiisE.js +27 -0
  52. package/web/dist/assets/index-DzuHNBGV.css +2 -0
  53. 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
+ }