@kitlangton/motel 0.1.0 → 0.1.2
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 +22 -8
- package/README.md +70 -163
- package/package.json +5 -2
- package/src/App.tsx +38 -28
- package/src/config.ts +1 -1
- package/src/httpApi.ts +5 -2
- package/src/localServer.ts +1 -0
- package/src/motel.ts +12 -0
- package/src/services/TelemetryStore.ts +99 -23
- package/src/services/TraceQueryService.ts +4 -0
- package/src/ui/AttrFilterModal.tsx +120 -0
- package/src/ui/SpanDetailPane.tsx +1 -2
- package/src/ui/TraceDetailsPane.tsx +14 -22
- package/src/ui/TraceList.tsx +166 -40
- package/src/ui/Waterfall.tsx +104 -49
- package/src/ui/app/TraceListPane.tsx +19 -14
- package/src/ui/app/TraceWorkspace.tsx +60 -31
- package/src/ui/app/useAppLayout.ts +22 -3
- package/src/ui/app/useTraceScreenData.ts +13 -2
- package/src/ui/format.ts +14 -5
- package/src/ui/primitives.tsx +3 -1
- package/src/ui/state.ts +32 -0
- package/src/ui/theme.ts +24 -19
- package/src/ui/traceSortNav.repro.test.ts +3 -2
- package/src/ui/useAttrFilterPicker.ts +47 -0
- package/src/ui/useKeyboardNav.ts +114 -15
- package/src/ui/waterfallNav.test.ts +22 -7
- package/web/dist/assets/{index-BEKIiisE.js → index-DKinj-OE.js} +1 -1
- package/web/dist/index.html +1 -1
package/AGENTS.md
CHANGED
|
@@ -2,8 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## Commands
|
|
4
4
|
- Install deps: `bun install`
|
|
5
|
-
- Run the TUI: `bun run dev` or `bun run start`
|
|
6
|
-
|
|
5
|
+
- Run the TUI: `bun run dev` or `bun run start` (auto-ensures a managed
|
|
6
|
+
OTLP daemon is running in the background so traces ingest while the TUI
|
|
7
|
+
is up)
|
|
8
|
+
- Start the background daemon only: `bun run daemon` (same as `motel start`)
|
|
9
|
+
- Stop the managed daemon: `bun run stop`
|
|
10
|
+
- Daemon status JSON: `bun run status`
|
|
11
|
+
- Restart daemon + relaunch TUI: `bun run restart`
|
|
12
|
+
- Run the local server in the foreground (no daemon, no TUI): `bun run server`
|
|
7
13
|
- Run tests: `bun run test`
|
|
8
14
|
- Query services via CLI: `bun run cli services`
|
|
9
15
|
- Query traces via CLI: `bun run cli traces <service> [limit]`
|
|
@@ -36,6 +42,7 @@
|
|
|
36
42
|
- `/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
43
|
- `/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
44
|
- `/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`.
|
|
45
|
+
- `/api/facets?type=traces&field=attribute_keys&service=<svc>` lists span-attribute keys for a service, ranked by discriminating power (keys with many distinct values first). Pair with `field=attribute_values&key=<key>` to list values for a specific key. Used by the TUI `f` attribute filter.
|
|
39
46
|
- `/api/docs` lists available documentation; `/api/docs/debug` and `/api/docs/effect` return the full skill content.
|
|
40
47
|
|
|
41
48
|
## Architecture
|
|
@@ -48,11 +55,16 @@
|
|
|
48
55
|
(pane widths, body lines, viewport rows, drill-in level).
|
|
49
56
|
- `src/ui/app/TraceWorkspace.tsx` renders the drill-in state machine:
|
|
50
57
|
L0 (trace list), L1 (waterfall), L2 (span detail), plus the service
|
|
51
|
-
logs side mode.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
logs side mode. When drilled in the list is hidden entirely and the
|
|
59
|
+
detail pane(s) expand to fill.
|
|
60
|
+
- `src/ui/app/TraceListPane.tsx` hosts the trace list: header + optional
|
|
61
|
+
filter bar + virtual-windowed body (no opentui scrollbox — that had a
|
|
62
|
+
race with Yoga layout timing).
|
|
63
|
+
- `src/ui/TraceList.tsx` exports `TraceListHeader` (the `TRACES 100 · ...`
|
|
64
|
+
strip) and `TraceListBody` (virtual-windowed rows with mouse-wheel
|
|
65
|
+
scrolling). The body owns its own scrollOffset state, preserves the
|
|
66
|
+
selected row's visual position across auto-refresh shifts, and snaps
|
|
67
|
+
the window to follow selection that moves off-screen.
|
|
56
68
|
- `src/ui/Waterfall.tsx` renders the waterfall timeline with a
|
|
57
69
|
virtualised scroll viewport; `src/ui/waterfallNav.ts` is the pure
|
|
58
70
|
collapse/expand/walk resolver (unit-tested).
|
|
@@ -116,7 +128,7 @@
|
|
|
116
128
|
- `MOTEL_OTEL_TRACE_LIMIT`: defaults to `100`
|
|
117
129
|
- `MOTEL_OTEL_LOG_LIMIT`: defaults to `80`
|
|
118
130
|
- `MOTEL_OTEL_RETENTION_HOURS`: defaults to `168` (7d)
|
|
119
|
-
- `MOTEL_OTEL_MAX_DB_SIZE_MB`: defaults to `
|
|
131
|
+
- `MOTEL_OTEL_MAX_DB_SIZE_MB`: defaults to `1024` (size-based retention cap)
|
|
120
132
|
|
|
121
133
|
## TUI Keys
|
|
122
134
|
- `?`: toggle shortcut help
|
|
@@ -133,7 +145,9 @@
|
|
|
133
145
|
- `tab`: toggle service logs view
|
|
134
146
|
- `[` / `]`: switch services
|
|
135
147
|
- `s`: cycle sort mode (recent → slowest → errors)
|
|
148
|
+
- `t`: cycle theme (motel-default → tokyo-night → catppuccin)
|
|
136
149
|
- `/`: enter filter mode (type to match on root operation name; `:error` restricts to failing traces)
|
|
150
|
+
- `f`: open attribute filter picker (browse span-attribute keys → values for the current service; `backspace` walks back to keys; `esc` in the trace list clears the active filter)
|
|
137
151
|
- `a`: pause or resume auto-refresh
|
|
138
152
|
- `r`: refresh now
|
|
139
153
|
- `c`: copy setup instructions for another Effect app
|
package/README.md
CHANGED
|
@@ -1,199 +1,106 @@
|
|
|
1
1
|
# motel
|
|
2
2
|
|
|
3
|
-
A local OpenTelemetry ingest + TUI viewer for development, backed by
|
|
4
|
-
Point your app's OTLP/HTTP exporters at the local motel server
|
|
5
|
-
|
|
3
|
+
A local OpenTelemetry ingest + TUI viewer for development, backed by
|
|
4
|
+
SQLite. Point your app's OTLP/HTTP exporters at the local motel server
|
|
5
|
+
and debug with real runtime evidence — from a terminal, the built-in web
|
|
6
|
+
UI, or directly from an AI coding agent.
|
|
6
7
|
|
|
7
|
-
##
|
|
8
|
+
## For agents: install the motel-debug skill
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
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):
|
|
10
|
+
`motel` ships a companion skill that teaches Claude Code, OpenCode,
|
|
11
|
+
Cursor, Codex, and 40+ other agents how to debug with runtime evidence
|
|
12
|
+
by querying motel's local OTLP store. Install it once and any future
|
|
13
|
+
agent session in the project will know how to use it.
|
|
26
14
|
|
|
27
15
|
```bash
|
|
28
16
|
# Project-local (adds to .claude/skills, .agents/skills, etc.)
|
|
29
17
|
npx skills add kitlangton/motel --skill motel-debug
|
|
30
18
|
|
|
31
|
-
#
|
|
19
|
+
# Or globally, for every project
|
|
32
20
|
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
21
|
```
|
|
37
22
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
## Quick start
|
|
23
|
+
See the full skill at [`skills/motel-debug/SKILL.md`](skills/motel-debug/SKILL.md).
|
|
41
24
|
|
|
42
|
-
|
|
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.
|
|
25
|
+
## For humans: install and run the TUI
|
|
50
26
|
|
|
51
|
-
|
|
52
|
-
|
|
27
|
+
motel is distributed on npm as `@kitlangton/motel`. The binary is a Bun
|
|
28
|
+
script, so Bun must be on your `PATH` at runtime:
|
|
53
29
|
|
|
54
30
|
```bash
|
|
55
|
-
|
|
56
|
-
|
|
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`
|
|
31
|
+
# one-off (no install)
|
|
32
|
+
bunx @kitlangton/motel
|
|
78
33
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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:
|
|
34
|
+
# or install globally
|
|
35
|
+
bun add -g @kitlangton/motel
|
|
36
|
+
motel
|
|
89
37
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
http://127.0.0.1:27686/v1/logs
|
|
38
|
+
# npm also works (Bun still required to run it)
|
|
39
|
+
npm install -g @kitlangton/motel
|
|
93
40
|
```
|
|
94
41
|
|
|
95
|
-
|
|
42
|
+
Don't have Bun?
|
|
96
43
|
|
|
97
44
|
```bash
|
|
98
|
-
|
|
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
|
|
45
|
+
curl -fsSL https://bun.sh/install | bash
|
|
112
46
|
```
|
|
113
47
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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.
|
|
48
|
+
`motel` starts the local OTLP ingest server on
|
|
49
|
+
`http://127.0.0.1:27686` and launches the TUI. Press `?` once inside for
|
|
50
|
+
the keyboard cheat sheet, or `c` to copy paste-ready setup instructions
|
|
51
|
+
for any Effect/OTEL app you want to trace.
|
|
145
52
|
|
|
146
|
-
|
|
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
|
|
53
|
+
Requirements: [Bun](https://bun.sh/) v1.1 or newer.
|
|
149
54
|
|
|
150
|
-
|
|
55
|
+
## How your app connects
|
|
151
56
|
|
|
152
|
-
|
|
153
|
-
|
|
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.
|
|
57
|
+
Once motel is running, point your app's OTLP/HTTP exporters at these
|
|
58
|
+
local endpoints:
|
|
157
59
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
60
|
+
```
|
|
61
|
+
http://127.0.0.1:27686/v1/traces
|
|
62
|
+
http://127.0.0.1:27686/v1/logs
|
|
63
|
+
```
|
|
161
64
|
|
|
162
|
-
|
|
65
|
+
Motel keeps everything in a local SQLite database at
|
|
66
|
+
`.motel-data/telemetry.sqlite`. No Docker, no cloud account.
|
|
163
67
|
|
|
164
|
-
|
|
68
|
+
## How agents connect
|
|
165
69
|
|
|
166
|
-
|
|
70
|
+
Agents with the `motel-debug` skill installed will automatically use
|
|
71
|
+
motel's HTTP API. The full OpenAPI spec is at
|
|
72
|
+
`http://127.0.0.1:27686/openapi.json` — the key endpoints are:
|
|
167
73
|
|
|
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
74
|
```
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
75
|
+
GET /api/health liveness check
|
|
76
|
+
GET /api/services services reporting telemetry
|
|
77
|
+
GET /api/traces?service=<service> recent traces for a service
|
|
78
|
+
GET /api/traces/<trace-id> full trace tree
|
|
79
|
+
GET /api/spans/<span-id> single span + logs
|
|
80
|
+
GET /api/logs?service=<service> recent logs
|
|
81
|
+
GET /api/traces/search?... structured trace search
|
|
82
|
+
GET /api/logs/search?... structured log search
|
|
83
|
+
GET /api/ai/calls AI SDK call inspector
|
|
192
84
|
```
|
|
193
85
|
|
|
194
|
-
|
|
86
|
+
## TUI keys
|
|
195
87
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
88
|
+
- `?` — keyboard cheat sheet
|
|
89
|
+
- `j` / `k` or `↑` / `↓` — move selection
|
|
90
|
+
- `enter` / `esc` — drill in / back out (trace → waterfall → span detail)
|
|
91
|
+
- `[` / `]` — switch service
|
|
92
|
+
- `tab` — toggle service logs
|
|
93
|
+
- `/` — filter traces
|
|
94
|
+
- `s` — cycle sort (recent → slowest → errors)
|
|
95
|
+
- `t` — cycle theme
|
|
96
|
+
- `c` — copy paste-ready setup instructions for another app
|
|
97
|
+
- `o` — open selected trace in the browser
|
|
98
|
+
- `q` — quit
|
|
99
|
+
|
|
100
|
+
## Privacy note
|
|
101
|
+
|
|
102
|
+
motel is a local development tool, but your app can emit sensitive
|
|
103
|
+
telemetry. Correlated logs may include secrets, tokens, or PII if your
|
|
104
|
+
app logs them; AI call traces may include full prompt content and
|
|
105
|
+
response text. Treat the local SQLite store as sensitive development
|
|
106
|
+
data when pointing motel at real workloads.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kitlangton/motel",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "A local OpenTelemetry ingest + TUI viewer for development, backed by SQLite.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
"effect",
|
|
24
24
|
"opentui"
|
|
25
25
|
],
|
|
26
|
-
"workspaces": [
|
|
26
|
+
"workspaces": [
|
|
27
|
+
"web"
|
|
28
|
+
],
|
|
27
29
|
"engines": {
|
|
28
30
|
"bun": ">=1.1.0"
|
|
29
31
|
},
|
|
@@ -48,6 +50,7 @@
|
|
|
48
50
|
"daemon": "bun run src/motel.ts daemon",
|
|
49
51
|
"status": "bun run src/motel.ts status",
|
|
50
52
|
"stop": "bun run src/motel.ts stop",
|
|
53
|
+
"restart": "bun run src/motel.ts restart",
|
|
51
54
|
"server": "bun run src/motel.ts server",
|
|
52
55
|
"mcp": "bun run src/mcp.ts",
|
|
53
56
|
"test": "bun test",
|
package/src/App.tsx
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
|
-
import { RGBA, TextAttributes
|
|
1
|
+
import { RGBA, TextAttributes } from "@opentui/core"
|
|
2
2
|
import { useAtom } from "@effect/atom-react"
|
|
3
3
|
import { useTerminalDimensions } from "@opentui/react"
|
|
4
|
-
import { useCallback, useEffect,
|
|
5
|
-
import { formatTimestamp
|
|
4
|
+
import { useCallback, useEffect, useMemo, useRef } from "react"
|
|
5
|
+
import { formatTimestamp } from "./ui/format.ts"
|
|
6
6
|
import { Divider, FooterHints, HelpModal, PlainLine, SplitDivider, TextLine } from "./ui/primitives.tsx"
|
|
7
7
|
import { useAppLayout } from "./ui/app/useAppLayout.ts"
|
|
8
8
|
import { useTraceScreenData } from "./ui/app/useTraceScreenData.ts"
|
|
9
9
|
import { TraceWorkspace } from "./ui/app/TraceWorkspace.tsx"
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
attrPickerIndexAtom,
|
|
12
|
+
attrPickerInputAtom,
|
|
13
|
+
attrPickerModeAtom,
|
|
14
|
+
attrFacetStateAtom,
|
|
15
|
+
noticeAtom,
|
|
16
|
+
persistSelectedTheme,
|
|
17
|
+
selectedThemeAtom,
|
|
18
|
+
} from "./ui/state.ts"
|
|
11
19
|
import { applyTheme, colors, SEPARATOR, themeLabel } from "./ui/theme.ts"
|
|
12
20
|
import { getVisibleSpans } from "./ui/Waterfall.tsx"
|
|
13
21
|
import { useKeyboardNav } from "./ui/useKeyboardNav.ts"
|
|
22
|
+
import { AttrFilterModal } from "./ui/AttrFilterModal.tsx"
|
|
23
|
+
import { useAttrFilterPicker } from "./ui/useAttrFilterPicker.ts"
|
|
14
24
|
|
|
15
25
|
export const App = () => {
|
|
16
26
|
const { width, height } = useTerminalDimensions()
|
|
@@ -36,11 +46,18 @@ export const App = () => {
|
|
|
36
46
|
autoRefresh,
|
|
37
47
|
filterMode,
|
|
38
48
|
filterText,
|
|
49
|
+
activeAttrKey,
|
|
50
|
+
activeAttrValue,
|
|
39
51
|
traceSort,
|
|
40
52
|
selectedTraceSummary,
|
|
41
53
|
selectedTrace,
|
|
42
54
|
filteredTraces,
|
|
43
55
|
} = useTraceScreenData()
|
|
56
|
+
const [pickerMode] = useAtom(attrPickerModeAtom)
|
|
57
|
+
const [pickerInput] = useAtom(attrPickerInputAtom)
|
|
58
|
+
const [pickerIndex] = useAtom(attrPickerIndexAtom)
|
|
59
|
+
const [attrFacets] = useAtom(attrFacetStateAtom)
|
|
60
|
+
useAttrFilterPicker(activeAttrKey)
|
|
44
61
|
|
|
45
62
|
const layout = useAppLayout({ width, height, notice, detailView, selectedSpanIndex })
|
|
46
63
|
const {
|
|
@@ -61,7 +78,6 @@ export const App = () => {
|
|
|
61
78
|
} = layout
|
|
62
79
|
|
|
63
80
|
const noticeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
64
|
-
const traceListScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
65
81
|
|
|
66
82
|
const flashNotice = (message: string) => {
|
|
67
83
|
if (noticeTimeoutRef.current !== null) {
|
|
@@ -83,27 +99,6 @@ export const App = () => {
|
|
|
83
99
|
persistSelectedTheme(selectedTheme)
|
|
84
100
|
}, [selectedTheme])
|
|
85
101
|
|
|
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
102
|
const { spanNavActive } = useKeyboardNav({
|
|
108
103
|
selectedTrace,
|
|
109
104
|
filteredTraces,
|
|
@@ -117,12 +112,15 @@ export const App = () => {
|
|
|
117
112
|
|
|
118
113
|
const headerServiceLabel = selectedTraceService ?? "none"
|
|
119
114
|
const autoLabel = autoRefresh ? "● live" : "○ paused"
|
|
115
|
+
const attrFilterLabel = activeAttrKey && activeAttrValue
|
|
116
|
+
? ` [${activeAttrKey}=${activeAttrValue.length > 20 ? `${activeAttrValue.slice(0, 19)}…` : activeAttrValue}]`
|
|
117
|
+
: ""
|
|
120
118
|
const headerRight = traceState.fetchedAt
|
|
121
119
|
? `${autoLabel} ${formatTimestamp(traceState.fetchedAt)}`
|
|
122
120
|
: traceState.status === "loading"
|
|
123
121
|
? "loading traces..."
|
|
124
122
|
: ""
|
|
125
|
-
const headerLeftLen = "MOTEL".length + SEPARATOR.length + headerServiceLabel.length
|
|
123
|
+
const headerLeftLen = "MOTEL".length + SEPARATOR.length + headerServiceLabel.length + attrFilterLabel.length
|
|
126
124
|
const headerGap = Math.max(2, headerFooterWidth - headerLeftLen - headerRight.length)
|
|
127
125
|
const visibleFooterNotice = footerNotice
|
|
128
126
|
|
|
@@ -168,6 +166,7 @@ export const App = () => {
|
|
|
168
166
|
<span fg={colors.muted} attributes={TextAttributes.BOLD}>MOTEL</span>
|
|
169
167
|
<span fg={colors.separator}>{SEPARATOR}</span>
|
|
170
168
|
<span fg={colors.muted}>{headerServiceLabel}</span>
|
|
169
|
+
{attrFilterLabel ? <span fg={colors.accent} attributes={TextAttributes.BOLD}>{attrFilterLabel}</span> : null}
|
|
171
170
|
<span fg={colors.muted}>{" ".repeat(headerGap)}</span>
|
|
172
171
|
<span fg={colors.muted} attributes={TextAttributes.BOLD}>{headerRight}</span>
|
|
173
172
|
</TextLine>
|
|
@@ -181,7 +180,6 @@ export const App = () => {
|
|
|
181
180
|
filterMode={filterMode}
|
|
182
181
|
filterText={filterText}
|
|
183
182
|
traceListProps={traceListProps}
|
|
184
|
-
traceListScrollRef={traceListScrollRef}
|
|
185
183
|
selectedTraceService={selectedTraceService}
|
|
186
184
|
serviceLogState={serviceLogState}
|
|
187
185
|
selectedServiceLogIndex={selectedServiceLogIndex}
|
|
@@ -212,6 +210,18 @@ export const App = () => {
|
|
|
212
210
|
</>
|
|
213
211
|
) : null}
|
|
214
212
|
{showHelp ? <HelpModal width={width ?? 100} height={height ?? 24} autoRefresh={autoRefresh} themeLabel={themeLabel(selectedTheme)} onClose={() => setShowHelp(false)} /> : null}
|
|
213
|
+
{pickerMode !== "off" ? (
|
|
214
|
+
<AttrFilterModal
|
|
215
|
+
width={width ?? 100}
|
|
216
|
+
height={height ?? 24}
|
|
217
|
+
mode={pickerMode}
|
|
218
|
+
input={pickerInput}
|
|
219
|
+
selectedIndex={pickerIndex}
|
|
220
|
+
selectedKey={activeAttrKey}
|
|
221
|
+
state={attrFacets}
|
|
222
|
+
onClose={() => { /* handled via keyboard */ }}
|
|
223
|
+
/>
|
|
224
|
+
) : null}
|
|
215
225
|
</box>
|
|
216
226
|
)
|
|
217
227
|
}
|
package/src/config.ts
CHANGED
|
@@ -34,6 +34,6 @@ export const config = {
|
|
|
34
34
|
traceFetchLimit: parsePositiveInt(process.env.MOTEL_OTEL_TRACE_LIMIT, 100),
|
|
35
35
|
logFetchLimit: parsePositiveInt(process.env.MOTEL_OTEL_LOG_LIMIT, 80),
|
|
36
36
|
retentionHours: parsePositiveInt(process.env.MOTEL_OTEL_RETENTION_HOURS, 168),
|
|
37
|
-
maxDbSizeMb: parsePositiveInt(process.env.MOTEL_OTEL_MAX_DB_SIZE_MB,
|
|
37
|
+
maxDbSizeMb: parsePositiveInt(process.env.MOTEL_OTEL_MAX_DB_SIZE_MB, 1024),
|
|
38
38
|
},
|
|
39
39
|
} as const
|
package/src/httpApi.ts
CHANGED
|
@@ -310,7 +310,10 @@ export const MotelHttpApi = HttpApi.make("MotelTelemetry")
|
|
|
310
310
|
Schema.annotateKey({ description: "Data source to facet: 'traces' facets span columns, 'logs' facets log columns" }),
|
|
311
311
|
),
|
|
312
312
|
field: Schema.String.pipe(
|
|
313
|
-
Schema.annotateKey({ description: "Column to facet. Traces: service, operation, status. Logs: service, severity, scope" }),
|
|
313
|
+
Schema.annotateKey({ description: "Column to facet. Traces: service, operation, status, attribute_keys, attribute_values. Logs: service, severity, scope. For attribute_values, also pass key=<attribute-name>." }),
|
|
314
|
+
),
|
|
315
|
+
key: Schema.optionalKey(Schema.String).pipe(
|
|
316
|
+
Schema.annotateKey({ description: "Attribute key to get values for (required when field=attribute_values)." }),
|
|
314
317
|
),
|
|
315
318
|
service: ServiceParam,
|
|
316
319
|
lookback: LookbackParam,
|
|
@@ -320,7 +323,7 @@ export const MotelHttpApi = HttpApi.make("MotelTelemetry")
|
|
|
320
323
|
error: ErrorResponse,
|
|
321
324
|
})
|
|
322
325
|
.annotate(OpenApi.Summary, "Get facet value counts")
|
|
323
|
-
.annotate(OpenApi.Description, "Returns distinct values and their counts for a given field, useful for discovering what data exists before querying.
|
|
326
|
+
.annotate(OpenApi.Description, "Returns distinct values and their counts for a given field, useful for discovering what data exists before querying. Examples: ?type=logs&field=severity returns log level distribution; ?type=traces&field=attribute_keys&service=opencode lists top span attribute keys; ?type=traces&field=attribute_values&key=ai.model.id lists values seen for that key."),
|
|
324
327
|
|
|
325
328
|
// AI Call endpoints
|
|
326
329
|
HttpApiEndpoint.get("aiCalls", "/api/ai/calls", {
|
package/src/localServer.ts
CHANGED
|
@@ -504,6 +504,7 @@ const TelemetryGroupLive = HttpApiBuilder.group(
|
|
|
504
504
|
type,
|
|
505
505
|
field,
|
|
506
506
|
serviceName: url.searchParams.get("service"),
|
|
507
|
+
key: url.searchParams.get("key"),
|
|
507
508
|
lookbackMinutes: parseLookbackMinutes(url.searchParams.get("lookback"), config.otel.traceLookbackMinutes),
|
|
508
509
|
limit: parseLimit(url.searchParams.get("limit"), 20),
|
|
509
510
|
}),
|
package/src/motel.ts
CHANGED
|
@@ -36,6 +36,17 @@ case "stop": {
|
|
|
36
36
|
break
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
case "restart": {
|
|
40
|
+
// Stop any running managed daemon, then start a fresh one + launch the
|
|
41
|
+
// TUI. Handy during local development when you've rebuilt the server
|
|
42
|
+
// and want the TUI to reconnect to the new binary in one command.
|
|
43
|
+
await run(stopManagedDaemon)
|
|
44
|
+
await run(applyManagedDaemonEnv)
|
|
45
|
+
await run(ensureManagedDaemon)
|
|
46
|
+
await import("./index.js")
|
|
47
|
+
break
|
|
48
|
+
}
|
|
49
|
+
|
|
39
50
|
case "server": {
|
|
40
51
|
await run(applyManagedDaemonEnv)
|
|
41
52
|
await import("./server.js")
|
|
@@ -56,6 +67,7 @@ case "-h": {
|
|
|
56
67
|
motel daemon
|
|
57
68
|
motel status
|
|
58
69
|
motel stop
|
|
70
|
+
motel restart
|
|
59
71
|
motel server
|
|
60
72
|
motel mcp
|
|
61
73
|
motel services
|