@kitlangton/motel 0.2.5 → 0.2.6
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 +11 -8
- package/README.md +13 -2
- package/package.json +31 -19
- package/skills/motel-debug/SKILL.md +203 -0
- package/skills/motel-debug/references/effect.md +38 -0
- package/src/App.tsx +3 -5
- package/src/StartupGate.tsx +8 -10
- package/src/cli.ts +15 -16
- package/src/config.ts +7 -1
- package/src/daemon.test.ts +332 -51
- package/src/daemon.ts +103 -152
- package/src/httpApi.ts +1 -0
- package/src/httpListPolicy.test.ts +76 -0
- package/src/httpListPolicy.ts +129 -0
- package/src/localServer.ts +194 -323
- package/src/mcp.ts +2 -1
- package/src/opentui-jsx.d.ts +11 -0
- package/src/otlp.test.ts +65 -0
- package/src/otlp.ts +20 -0
- package/src/otlpProtobuf.ts +35 -0
- package/src/registry.ts +37 -11
- package/src/runtime.ts +2 -6
- package/src/services/AsyncIngest.ts +20 -8
- package/src/services/LogQueryService.ts +11 -25
- package/src/services/TelemetryQuery.ts +62 -0
- package/src/services/TelemetryStore.ts +433 -249
- package/src/services/TraceQueryService.ts +18 -52
- package/src/services/ingestRpc.ts +2 -4
- package/src/services/queryRpc.ts +15 -0
- package/src/services/telemetryQueryWorker.ts +32 -0
- package/src/services/telemetryWorker.ts +5 -8
- package/src/storybook/aiChatStory.tsx +1 -1
- package/src/telemetry.test.ts +307 -41
- package/src/ui/AiChatView.tsx +1 -1
- package/src/ui/AttrFilterModal.tsx +1 -1
- package/src/ui/ServiceLogs.tsx +10 -7
- package/src/ui/SpanContentView.tsx +24 -21
- package/src/ui/TraceDetailsPane.tsx +1 -1
- package/src/ui/TraceList.tsx +1 -1
- package/src/ui/aiState.ts +10 -22
- package/src/ui/app/TraceWorkspace.tsx +2 -1
- package/src/ui/app/useAppLayout.ts +1 -1
- package/src/ui/app/useTraceScreenData.ts +22 -18
- package/src/ui/cachedLoader.test.ts +23 -0
- package/src/ui/cachedLoader.ts +60 -0
- package/src/ui/loaders.ts +34 -53
- package/src/ui/primitives.tsx +1 -1
- package/src/ui/state.ts +2 -0
- package/src/ui/traceDetailsWidth.repro.test.ts +12 -1
- package/src/ui/traceSortNav.repro.seed.ts +1 -1
- package/src/ui/traceSortNav.repro.test.ts +12 -2
- package/src/ui/useAttrFilterPicker.ts +10 -8
- package/src/ui/useKeyboardNav.ts +3 -6
- package/src/ui/waterfallNav.repro.seed.ts +1 -1
- package/src/ui/waterfallNav.repro.test.ts +16 -8
- package/web/dist/assets/index-B01z9BaO.css +2 -0
- package/web/dist/assets/index-M86tcih5.js +22 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-DnyVo03x.js +0 -27
- package/web/dist/assets/index-DzuHNBGV.css +0 -2
package/AGENTS.md
CHANGED
|
@@ -94,11 +94,11 @@ The repo is wired up with `@effect/language-service` as a `tsconfig.json` `plugi
|
|
|
94
94
|
- `src/runtime.ts` wires the Effect beta runtime and OTEL trace + log exporters.
|
|
95
95
|
- `src/localServer.ts` starts the local Bun OTLP/query server.
|
|
96
96
|
- `src/httpApi.ts` defines the typed Effect HttpApi surface and OpenAPI spec for the local server.
|
|
97
|
+
- `src/httpListPolicy.ts` owns pure HTTP list/search parameter decoding, bounds, cursors, and pagination metadata shaping.
|
|
97
98
|
- `src/server.ts` runs the local server without the TUI.
|
|
98
99
|
- `src/instructions.ts` contains the copied setup instructions for other Effect apps.
|
|
99
|
-
- `src/services/TelemetryStore.ts` persists traces and logs in SQLite and exposes indexed queries.
|
|
100
|
-
- `src/services/
|
|
101
|
-
- `src/services/LogQueryService.ts` reads logs from the local store.
|
|
100
|
+
- `src/services/TelemetryStore.ts` persists traces and logs in SQLite and exposes indexed queries through writer and read-only service identifiers.
|
|
101
|
+
- `src/services/TelemetryQuery.ts` proxies read-only store calls to `src/services/telemetryQueryWorker.ts`, keeping synchronous Bun SQLite queries off the HTTP event loop.
|
|
102
102
|
- `src/config.ts` is the source of truth for ports and env-driven OTEL settings.
|
|
103
103
|
- `web/` is a Vite + React SPA for the browser-based UI (Tailwind CSS, `@effect/atom-react`, `AtomHttpApi`).
|
|
104
104
|
- `web/src/api.ts` creates the typed `AtomHttpApi.Service` client from `src/httpApi.ts`.
|
|
@@ -107,15 +107,14 @@ The repo is wired up with `@effect/language-service` as a `tsconfig.json` `plugi
|
|
|
107
107
|
- The server in `src/localServer.ts` serves `web/dist/` as static files with SPA fallback for non-API routes.
|
|
108
108
|
|
|
109
109
|
## Tests
|
|
110
|
-
- `bun test` runs the suite. Three kinds of tests live in the repo:
|
|
110
|
+
- `bun run test` runs the suite. Three kinds of tests live in the repo:
|
|
111
111
|
- `src/telemetry.test.ts` exercises the SQLite TelemetryStore with
|
|
112
112
|
OTLP payloads end-to-end.
|
|
113
113
|
- `src/ui/waterfallNav.test.ts` unit-tests the pure collapse/expand
|
|
114
114
|
resolver (no UI).
|
|
115
115
|
- `src/ui/*.repro.test.ts` drive the real TUI under `tuistory` to
|
|
116
|
-
reproduce regressions
|
|
117
|
-
|
|
118
|
-
are auto-skipped when `tuistory` isn't installed.
|
|
116
|
+
reproduce regressions, using deterministic traces seeded into SQLite
|
|
117
|
+
by child processes. They become no-op passes when `tuistory` is absent.
|
|
119
118
|
|
|
120
119
|
## Effect Observability Guidance
|
|
121
120
|
- Inspect the target repo’s existing Effect runtime and observability wiring before adding anything new.
|
|
@@ -140,12 +139,16 @@ The repo is wired up with `@effect/language-service` as a `tsconfig.json` `plugi
|
|
|
140
139
|
- `MOTEL_OTEL_EXPORTER_URL`: defaults to `http://127.0.0.1:27686/v1/traces`
|
|
141
140
|
- `MOTEL_OTEL_LOGS_EXPORTER_URL`: defaults to `http://127.0.0.1:27686/v1/logs`
|
|
142
141
|
- `MOTEL_OTEL_QUERY_URL`: defaults to `http://127.0.0.1:27686`
|
|
143
|
-
- `MOTEL_OTEL_DB_PATH`: defaults to
|
|
142
|
+
- `MOTEL_OTEL_DB_PATH`: defaults to `${XDG_STATE_HOME:-~/.local/state}/motel/telemetry.sqlite` (one shared DB per machine; daemon log + lock + instance registry live in the same directory)
|
|
143
|
+
- `MOTEL_RUNTIME_DIR`: overrides the daemon log, lock, and instance-registry directory (primarily for isolated tests and custom managed instances)
|
|
144
144
|
- `MOTEL_OTEL_TRACE_LOOKBACK_MINUTES`: defaults to `1440` (24h)
|
|
145
145
|
- `MOTEL_OTEL_TRACE_LIMIT`: defaults to `100`
|
|
146
146
|
- `MOTEL_OTEL_LOG_LIMIT`: defaults to `80`
|
|
147
147
|
- `MOTEL_OTEL_RETENTION_HOURS`: defaults to `168` (7d)
|
|
148
148
|
- `MOTEL_OTEL_MAX_DB_SIZE_MB`: defaults to `1024` (size-based retention cap)
|
|
149
|
+
- `MOTEL_OTEL_RETENTION_TRACE_BATCH`: defaults to `100` completed traces per cleanup pass
|
|
150
|
+
- `MOTEL_OTEL_RETENTION_LOG_BATCH`: defaults to `5000` logs per cleanup pass
|
|
151
|
+
- `MOTEL_OTEL_RETENTION_INTERVAL_SECONDS`: defaults to `10`
|
|
149
152
|
|
|
150
153
|
## TUI Keys
|
|
151
154
|
- `?`: toggle shortcut help
|
package/README.md
CHANGED
|
@@ -62,8 +62,19 @@ http://127.0.0.1:27686/v1/traces
|
|
|
62
62
|
http://127.0.0.1:27686/v1/logs
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
Motel keeps everything in a local SQLite database at
|
|
66
|
-
|
|
65
|
+
Motel keeps everything in a machine-global local SQLite database at
|
|
66
|
+
`${XDG_STATE_HOME:-~/.local/state}/motel/telemetry.sqlite`. One managed
|
|
67
|
+
daemon is shared across local projects. No Docker, no cloud account.
|
|
68
|
+
|
|
69
|
+
The store retains seven days of telemetry by default and targets a 1 GB
|
|
70
|
+
active-data ceiling using bounded background batches. Recent data is preserved while
|
|
71
|
+
the oldest completed traces and logs are removed first. Configure the policy
|
|
72
|
+
with `MOTEL_OTEL_RETENTION_HOURS`, `MOTEL_OTEL_MAX_DB_SIZE_MB`,
|
|
73
|
+
`MOTEL_OTEL_RETENTION_TRACE_BATCH`, `MOTEL_OTEL_RETENTION_LOG_BATCH`, and
|
|
74
|
+
`MOTEL_OTEL_RETENTION_INTERVAL_SECONDS`. Existing databases created without
|
|
75
|
+
incremental auto-vacuum are never silently rewritten at startup; deleted pages
|
|
76
|
+
are reused, but shrinking such a historical file requires an explicit offline
|
|
77
|
+
SQLite `VACUUM` chosen by the user.
|
|
67
78
|
|
|
68
79
|
## How agents connect
|
|
69
80
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kitlangton/motel",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "A local OpenTelemetry ingest + TUI viewer for development, backed by SQLite.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,14 +27,22 @@
|
|
|
27
27
|
"web"
|
|
28
28
|
],
|
|
29
29
|
"engines": {
|
|
30
|
-
"bun": ">=1.
|
|
30
|
+
"bun": ">=1.3.0"
|
|
31
31
|
},
|
|
32
32
|
"publishConfig": {
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
|
+
"overrides": {
|
|
36
|
+
"@grpc/grpc-js": "1.14.4",
|
|
37
|
+
"protobufjs": "7.6.4",
|
|
38
|
+
"shell-quote": "1.9.0",
|
|
39
|
+
"ws": "8.21.0"
|
|
40
|
+
},
|
|
35
41
|
"files": [
|
|
36
42
|
"src",
|
|
37
43
|
"web/dist",
|
|
44
|
+
"skills/motel-debug/SKILL.md",
|
|
45
|
+
"skills/motel-debug/references/effect.md",
|
|
38
46
|
"LICENSE",
|
|
39
47
|
"README.md",
|
|
40
48
|
"AGENTS.md"
|
|
@@ -72,30 +80,34 @@
|
|
|
72
80
|
"bench:ingest-logs": "bun run scripts/bench-ingest-logs.ts",
|
|
73
81
|
"bench:ingest-traces": "bun run scripts/bench-ingest-traces.ts",
|
|
74
82
|
"bench:search-spans": "bun run scripts/bench-search-spans.ts",
|
|
83
|
+
"release:validate": "bun run scripts/validate-release.ts",
|
|
75
84
|
"typecheck": "tsc --noEmit",
|
|
85
|
+
"web:typecheck": "tsc --noEmit -p web/tsconfig.json",
|
|
76
86
|
"prepublishOnly": "bun run web:build"
|
|
77
87
|
},
|
|
78
88
|
"devDependencies": {
|
|
79
|
-
"@effect/language-service": "^0.
|
|
80
|
-
"@types/bun": "^1.3.
|
|
81
|
-
"@types/react": "^19.2.
|
|
82
|
-
"typescript": "^6.0.
|
|
89
|
+
"@effect/language-service": "^0.86.2",
|
|
90
|
+
"@types/bun": "^1.3.14",
|
|
91
|
+
"@types/react": "^19.2.17",
|
|
92
|
+
"typescript": "^6.0.3"
|
|
83
93
|
},
|
|
84
94
|
"dependencies": {
|
|
85
|
-
"@effect/atom-react": "
|
|
86
|
-
"@effect/opentelemetry": "
|
|
87
|
-
"@effect/platform-bun": "
|
|
95
|
+
"@effect/atom-react": "4.0.0-beta.90",
|
|
96
|
+
"@effect/opentelemetry": "4.0.0-beta.90",
|
|
97
|
+
"@effect/platform-bun": "4.0.0-beta.90",
|
|
88
98
|
"@opentelemetry/api": "^1.9.0",
|
|
89
|
-
"@opentelemetry/exporter-logs-otlp-http": "^0.
|
|
90
|
-
"@opentelemetry/exporter-trace-otlp-http": "^0.
|
|
91
|
-
"@opentelemetry/
|
|
92
|
-
"@opentelemetry/sdk-
|
|
93
|
-
"@opentelemetry/sdk-
|
|
94
|
-
"@opentelemetry/sdk-trace-
|
|
95
|
-
"@
|
|
96
|
-
"@opentui/
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
+
"@opentelemetry/exporter-logs-otlp-http": "^0.219.0",
|
|
100
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.219.0",
|
|
101
|
+
"@opentelemetry/otlp-transformer": "0.214.0",
|
|
102
|
+
"@opentelemetry/sdk-logs": "^0.219.0",
|
|
103
|
+
"@opentelemetry/sdk-node": "^0.219.0",
|
|
104
|
+
"@opentelemetry/sdk-trace-base": "^2.8.0",
|
|
105
|
+
"@opentelemetry/sdk-trace-node": "^2.8.0",
|
|
106
|
+
"@opentui/core": "0.4.2",
|
|
107
|
+
"@opentui/react": "0.4.2",
|
|
108
|
+
"effect": "4.0.0-beta.90",
|
|
109
|
+
"protobufjs": "7.6.4",
|
|
110
|
+
"react": "^19.2.7",
|
|
99
111
|
"scheduler": "^0.27.0"
|
|
100
112
|
}
|
|
101
113
|
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: motel-debug
|
|
3
|
+
description: Debug applications with motel, a local OpenTelemetry ingest and query server. Use when the user wants runtime-evidence debugging with traces or logs, wants temporary debug instrumentation that can be removed later, or needs a repo wired to send OTLP/HTTP telemetry to a local motel server. If the target repo uses Effect or @effect/*, also read references/effect.md.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Motel Debug
|
|
7
|
+
|
|
8
|
+
You are in **debug mode**. Debug with runtime evidence, not guesswork.
|
|
9
|
+
|
|
10
|
+
Agents guess based on code alone. You need actual runtime data. Motel is the local OpenTelemetry server that collects traces and logs — use it as your evidence loop.
|
|
11
|
+
|
|
12
|
+
Default local server details:
|
|
13
|
+
|
|
14
|
+
- Base URL: `http://127.0.0.1:27686`
|
|
15
|
+
- OTLP traces: `POST /v1/traces`
|
|
16
|
+
- OTLP logs: `POST /v1/logs`
|
|
17
|
+
- Query API: `GET /api/*`
|
|
18
|
+
- OpenAPI: `GET /openapi.json`
|
|
19
|
+
- Header: `Content-Type: application/json`
|
|
20
|
+
- Auth: none by default
|
|
21
|
+
|
|
22
|
+
If the user provides a different motel URL, use that instead of the default.
|
|
23
|
+
|
|
24
|
+
## Workflow
|
|
25
|
+
|
|
26
|
+
### 1. Verify motel is running — and start it if not
|
|
27
|
+
|
|
28
|
+
Check `GET /api/health`. If it returns 200, continue.
|
|
29
|
+
|
|
30
|
+
If it fails (connection refused, timeout, non-200), motel isn't running.
|
|
31
|
+
Start it as a background daemon — **do not** launch the TUI, which is
|
|
32
|
+
interactive and will block your shell:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
motel start
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`motel start` ensures the machine-global managed daemon is running, writes
|
|
39
|
+
runtime files under `${XDG_STATE_HOME:-~/.local/state}/motel/`, and returns a
|
|
40
|
+
JSON status blob. It is idempotent and shared across local projects. If motel isn't on `PATH`, fall
|
|
41
|
+
back to `bunx @kitlangton/motel start`.
|
|
42
|
+
|
|
43
|
+
After starting, re-check `GET /api/health` (may take 1–2s to become
|
|
44
|
+
ready). If it still fails, read `${XDG_STATE_HOME:-~/.local/state}/motel/daemon.log` for the error
|
|
45
|
+
and surface it to the user.
|
|
46
|
+
|
|
47
|
+
Other lifecycle commands, for reference:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
motel status # JSON status (running? pid? originating workdir?)
|
|
51
|
+
motel stop # stop the shared managed daemon for all local projects
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Discover reporting services with `GET /api/services` when needed.
|
|
55
|
+
|
|
56
|
+
### 2. Generate hypotheses
|
|
57
|
+
|
|
58
|
+
Before touching any code, generate **3-5 specific hypotheses** about why the bug occurs. Be precise — "the cache key doesn't include the user ID" is better than "something is wrong with caching."
|
|
59
|
+
|
|
60
|
+
### 3. Instrument with tagged debug blocks
|
|
61
|
+
|
|
62
|
+
Add the minimum instrumentation needed to confirm or reject **all** hypotheses in parallel. Every debug block must:
|
|
63
|
+
|
|
64
|
+
- Be wrapped in `#region motel debug` / `#endregion motel debug` markers
|
|
65
|
+
- Include a `debug.hypothesis` attribute linking it to a specific hypothesis
|
|
66
|
+
- Use whatever tracing/logging mechanism the codebase already has (spans, structured logs, annotations — not raw `fetch` calls)
|
|
67
|
+
|
|
68
|
+
Tag every piece of debug instrumentation with structured attributes so you can query it later. Reuse these keys:
|
|
69
|
+
|
|
70
|
+
| Key | Purpose |
|
|
71
|
+
|-----|---------|
|
|
72
|
+
| `debug.session` | Groups all instrumentation for this debug session |
|
|
73
|
+
| `debug.hypothesis` | Links to a specific hypothesis (e.g. `"cache-miss"`, `"A"`) |
|
|
74
|
+
| `debug.step` | Position in the flow (e.g. `"entry"`, `"before-write"`, `"after-read"`) |
|
|
75
|
+
| `debug.label` | Human-readable description of what this point captures |
|
|
76
|
+
|
|
77
|
+
Choose log placements based on your hypotheses:
|
|
78
|
+
|
|
79
|
+
- Function entry with parameters
|
|
80
|
+
- Function exit with return values
|
|
81
|
+
- Values before and after critical operations
|
|
82
|
+
- Branch execution paths (which if/else ran)
|
|
83
|
+
- State mutations and intermediate values
|
|
84
|
+
- Suspected error or edge-case values
|
|
85
|
+
|
|
86
|
+
Guidelines:
|
|
87
|
+
|
|
88
|
+
- At least 1 instrumentation point is required; never skip instrumentation
|
|
89
|
+
- Do not exceed 10 — if you think you need more, narrow your hypotheses
|
|
90
|
+
- Typical range is 2-6
|
|
91
|
+
|
|
92
|
+
### 4. Reproduce the issue
|
|
93
|
+
|
|
94
|
+
- If a failing test exists, run it directly
|
|
95
|
+
- If reproduction is straightforward (CLI command, curl, simple script), write and run it yourself
|
|
96
|
+
- Otherwise, ask the user to reproduce — provide clear numbered steps and remind them to restart if needed
|
|
97
|
+
- Once a reproduction pathway is established, reuse it for all subsequent iterations
|
|
98
|
+
|
|
99
|
+
### 5. Analyze evidence
|
|
100
|
+
|
|
101
|
+
Query motel for the debug instrumentation:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
curl "http://127.0.0.1:27686/api/spans/search?service=<service>&attr.debug.hypothesis=<id>"
|
|
105
|
+
curl "http://127.0.0.1:27686/api/logs/search?service=<service>&attr.debug.session=<session>"
|
|
106
|
+
curl "http://127.0.0.1:27686/api/traces/search?service=<service>&attr.debug.hypothesis=<id>"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
For each hypothesis, evaluate: **CONFIRMED**, **REJECTED**, or **INCONCLUSIVE** — cite specific spans, logs, or attribute values as evidence.
|
|
110
|
+
|
|
111
|
+
### 6. Fix only with evidence
|
|
112
|
+
|
|
113
|
+
Do **not** fix without runtime evidence. When you fix:
|
|
114
|
+
|
|
115
|
+
- Keep all debug instrumentation in place — do not remove it yet
|
|
116
|
+
- Make the fix as small and targeted as possible
|
|
117
|
+
- Reuse existing architecture and patterns; do not overengineer
|
|
118
|
+
|
|
119
|
+
### 7. Verify the fix
|
|
120
|
+
|
|
121
|
+
Reproduce the issue again with instrumentation still active. Compare before/after evidence:
|
|
122
|
+
|
|
123
|
+
- Cite specific log lines or span attributes that prove the fix works
|
|
124
|
+
- If the fix failed: **revert code changes from rejected hypotheses** (do not let speculative fixes accumulate), generate new hypotheses from different subsystems, add more instrumentation, and iterate
|
|
125
|
+
- Iteration is expected. Taking longer with more data yields better fixes.
|
|
126
|
+
|
|
127
|
+
### 8. Clean up
|
|
128
|
+
|
|
129
|
+
Only after the fix is verified **and** the user confirms there are no remaining issues:
|
|
130
|
+
|
|
131
|
+
- Run the cleanup script or remove blocks manually (see Cleanup section below)
|
|
132
|
+
- Run `git diff` to confirm only the intentional fix remains
|
|
133
|
+
|
|
134
|
+
## Instrumentation Rules
|
|
135
|
+
|
|
136
|
+
Wrap every temporary debug block in these exact markers:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
// #region motel debug
|
|
140
|
+
// temporary debug instrumentation
|
|
141
|
+
// #endregion motel debug
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Use whatever the codebase already provides for tracing and logging. The markers are language-comment wrappers — adapt the comment syntax for non-JS/TS files (e.g. `# #region motel debug` for Python).
|
|
145
|
+
|
|
146
|
+
**Do not:**
|
|
147
|
+
- Log secrets, tokens, passwords, or raw PII
|
|
148
|
+
- Remove instrumentation before post-fix verification succeeds
|
|
149
|
+
- Use `setTimeout`, `sleep`, or artificial delays as a "fix"
|
|
150
|
+
- Let code changes from rejected hypotheses accumulate — revert them
|
|
151
|
+
|
|
152
|
+
## Query Patterns
|
|
153
|
+
|
|
154
|
+
Two filter prefixes for attribute search:
|
|
155
|
+
|
|
156
|
+
| Prefix | Match type | Example |
|
|
157
|
+
|--------|-----------|---------|
|
|
158
|
+
| `attr.<key>=<value>` | Exact match | `attr.debug.hypothesis=cache-miss` |
|
|
159
|
+
| `attrContains.<key>=<substring>` | Case-insensitive substring | `attrContains.ai.prompt.messages=hello world` |
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
curl http://127.0.0.1:27686/api/health
|
|
163
|
+
curl http://127.0.0.1:27686/api/services
|
|
164
|
+
|
|
165
|
+
# Trace search
|
|
166
|
+
curl "http://127.0.0.1:27686/api/traces/search?service=<service>&operation=<text>&attr.debug.session=<session>"
|
|
167
|
+
|
|
168
|
+
# Span search (supports traceId to scope to one trace)
|
|
169
|
+
curl "http://127.0.0.1:27686/api/spans/search?service=<service>&traceId=<trace-id>&attr.debug.hypothesis=<id>"
|
|
170
|
+
curl "http://127.0.0.1:27686/api/spans/search?service=<service>&attrContains.ai.prompt.messages=<phrase>"
|
|
171
|
+
|
|
172
|
+
# Log search (supports severity filter, case-insensitive body search)
|
|
173
|
+
curl "http://127.0.0.1:27686/api/logs/search?service=<service>&severity=ERROR&body=<text>"
|
|
174
|
+
curl "http://127.0.0.1:27686/api/logs/search?service=<service>&attrContains.debug.label=<substring>"
|
|
175
|
+
|
|
176
|
+
# AI call search (compact summaries with previews)
|
|
177
|
+
curl "http://127.0.0.1:27686/api/ai/calls?model=gpt-5.4&sessionId=<session>"
|
|
178
|
+
curl "http://127.0.0.1:27686/api/ai/calls?text=<phrase>&status=error"
|
|
179
|
+
|
|
180
|
+
# AI call detail (full prompt/response payloads)
|
|
181
|
+
curl "http://127.0.0.1:27686/api/ai/calls/<span-id>"
|
|
182
|
+
|
|
183
|
+
# AI stats
|
|
184
|
+
curl "http://127.0.0.1:27686/api/ai/stats?groupBy=model&agg=total_input_tokens"
|
|
185
|
+
|
|
186
|
+
curl http://127.0.0.1:27686/openapi.json
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
List and search responses include `meta.nextCursor` when more data is available.
|
|
190
|
+
|
|
191
|
+
Motel gives you trace-correlated data — you can see which span a debug log belongs to, the parent operation, timing, and the full trace tree. Use `GET /api/traces/<trace-id>/spans` and `GET /api/spans/<span-id>/logs` to navigate the correlation.
|
|
192
|
+
|
|
193
|
+
For AI/LLM calls, use `/api/ai/calls` for compact searchable summaries (with prompt/response previews and token usage), and `/api/ai/calls/<span-id>` for full payloads.
|
|
194
|
+
|
|
195
|
+
## Effect
|
|
196
|
+
|
|
197
|
+
If the target repo uses Effect, read `references/effect.md` before changing runtime wiring or adding instrumentation.
|
|
198
|
+
|
|
199
|
+
## Cleanup
|
|
200
|
+
|
|
201
|
+
Use the bundled script at `scripts/clear-motel-debug.ts` when you want deterministic cleanup. It removes every block between `#region motel debug` and `#endregion motel debug` in JS/TS files and fails on unmatched markers.
|
|
202
|
+
|
|
203
|
+
If you cannot run the script, delete every marked block manually and then grep for `#region motel debug` to confirm none remain.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Effect Notes
|
|
2
|
+
|
|
3
|
+
Apply these only when the target repo already uses Effect or `@effect/*`.
|
|
4
|
+
|
|
5
|
+
## Runtime
|
|
6
|
+
|
|
7
|
+
- Inspect the existing runtime and observability wiring before adding anything new.
|
|
8
|
+
- Prefer the repo's existing Effect-native observability APIs if they already exist.
|
|
9
|
+
- If `effect/unstable/observability` is already the best fit, prefer it over adding new OTEL packages.
|
|
10
|
+
- Merge telemetry into the main runtime once, not per feature or per request path.
|
|
11
|
+
|
|
12
|
+
## Instrumentation
|
|
13
|
+
|
|
14
|
+
- Prefer `Effect.fn("...")` for meaningful workflow spans.
|
|
15
|
+
- Add a few child spans around boundaries that are likely to fail or add latency.
|
|
16
|
+
- Emit `Effect.logInfo`, `Effect.logWarning`, and `Effect.logError` with structured fields.
|
|
17
|
+
- Put searchable values in annotations/attributes, not only in the free-form log body.
|
|
18
|
+
- Reuse stable debug keys such as `debug.session`, `debug.hypothesis`, `debug.step`, and `debug.label`.
|
|
19
|
+
|
|
20
|
+
## Debug Blocks
|
|
21
|
+
|
|
22
|
+
Wrap temporary debug-only Effect instrumentation in removable markers.
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
// #region motel debug
|
|
26
|
+
const program = Effect.fn("feature/doThing")(function*() {
|
|
27
|
+
yield* Effect.logInfo("entered doThing", {
|
|
28
|
+
debug: {
|
|
29
|
+
session: "abc123",
|
|
30
|
+
hypothesis: "cache-miss",
|
|
31
|
+
step: "entry",
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
// #endregion motel debug
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Keep those blocks until the fix is verified, then remove them with the cleanup script.
|
package/src/App.tsx
CHANGED
|
@@ -14,16 +14,14 @@ import {
|
|
|
14
14
|
attrPickerInputAtom,
|
|
15
15
|
attrPickerModeAtom,
|
|
16
16
|
attrFacetStateAtom,
|
|
17
|
-
chatDetailChunkIdAtom,
|
|
18
|
-
chatDetailScrollOffsetAtom,
|
|
19
17
|
noticeAtom,
|
|
20
|
-
persistSelectedTheme,
|
|
21
18
|
selectedAttrIndexAtom,
|
|
22
|
-
selectedChatChunkIdAtom,
|
|
23
19
|
selectedThemeAtom,
|
|
24
20
|
waterfallFilterModeAtom,
|
|
25
21
|
waterfallFilterTextAtom,
|
|
26
|
-
} from "./ui/
|
|
22
|
+
} from "./ui/atoms.ts"
|
|
23
|
+
import { chatDetailChunkIdAtom, chatDetailScrollOffsetAtom, selectedChatChunkIdAtom } from "./ui/aiState.ts"
|
|
24
|
+
import { persistSelectedTheme } from "./ui/persistence.ts"
|
|
27
25
|
import type { ThemeName } from "./ui/theme.ts"
|
|
28
26
|
import { applyTheme, colors, SEPARATOR, themeLabel } from "./ui/theme.ts"
|
|
29
27
|
import { useKeyboardNav } from "./ui/useKeyboardNav.ts"
|
package/src/StartupGate.tsx
CHANGED
|
@@ -13,7 +13,6 @@ type ConflictStatus = DaemonStatus & {
|
|
|
13
13
|
readonly pid: number
|
|
14
14
|
readonly workdir: string
|
|
15
15
|
readonly reason: string
|
|
16
|
-
readonly sameWorkdir: false
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
type ConflictScreenState = {
|
|
@@ -61,8 +60,7 @@ const isRecoverableConflict = (status: DaemonStatus | null): status is ConflictS
|
|
|
61
60
|
status.service === MOTEL_SERVICE_ID &&
|
|
62
61
|
status.pid !== null &&
|
|
63
62
|
status.workdir !== null &&
|
|
64
|
-
status.reason !== null
|
|
65
|
-
!status.sameWorkdir
|
|
63
|
+
status.reason !== null
|
|
66
64
|
|
|
67
65
|
const stopConflictingDaemon = async (status: ConflictStatus) => {
|
|
68
66
|
const port = parsePort(status.url)
|
|
@@ -151,7 +149,7 @@ const RecoveryScreen = ({
|
|
|
151
149
|
</box>
|
|
152
150
|
<box paddingTop={1} flexDirection="column">
|
|
153
151
|
{notice ? <PlainLine text={notice} fg={busy ? colors.warning : colors.count} /> : null}
|
|
154
|
-
<PlainLine text={busy ? "Working..." : "j/k or ↑↓ select · enter run · r retry · k
|
|
152
|
+
<PlainLine text={busy ? "Working..." : "j/k or ↑↓ select · enter run · r retry · k stop incompatible daemon · q quit"} fg={colors.count} />
|
|
155
153
|
</box>
|
|
156
154
|
</box>
|
|
157
155
|
</box>
|
|
@@ -192,7 +190,7 @@ export const StartupGate = () => {
|
|
|
192
190
|
{ key: "r", label: "Retry startup", run: attemptStart },
|
|
193
191
|
{
|
|
194
192
|
key: "k",
|
|
195
|
-
label: `Stop
|
|
193
|
+
label: `Stop incompatible daemon (${startupState.status.pid})`,
|
|
196
194
|
run: async () => {
|
|
197
195
|
setStartupState((current) => current.kind === "conflict"
|
|
198
196
|
? { ...current, busy: true, notice: `Stopping daemon ${current.status.pid}...` }
|
|
@@ -253,16 +251,16 @@ export const StartupGate = () => {
|
|
|
253
251
|
const status = startupState.status
|
|
254
252
|
const detailLines = [
|
|
255
253
|
`Port: ${status.url}`,
|
|
256
|
-
`
|
|
257
|
-
`
|
|
258
|
-
`
|
|
254
|
+
`Daemon workdir: ${status.workdir}`,
|
|
255
|
+
`Daemon pid: ${status.pid}`,
|
|
256
|
+
`Incompatible database: ${status.databasePath}`,
|
|
259
257
|
status.workdir.startsWith("/tmp") || status.workdir.startsWith("/private/tmp")
|
|
260
258
|
? "This looks like a temp/test daemon."
|
|
261
|
-
: "This
|
|
259
|
+
: "This daemon is using a different database configuration.",
|
|
262
260
|
]
|
|
263
261
|
return (
|
|
264
262
|
<RecoveryScreen
|
|
265
|
-
title="Daemon Conflict"
|
|
263
|
+
title="Daemon Configuration Conflict"
|
|
266
264
|
message={startupState.message}
|
|
267
265
|
width={width}
|
|
268
266
|
height={height}
|
package/src/cli.ts
CHANGED
|
@@ -3,18 +3,17 @@ import { config } from "./config.js"
|
|
|
3
3
|
import { otelServerInstructions } from "./instructions.js"
|
|
4
4
|
import { attributeFiltersFromArgs, isAttributeFilterToken } from "./queryFilters.js"
|
|
5
5
|
import { queryRuntime } from "./runtime.js"
|
|
6
|
-
import {
|
|
7
|
-
import { TraceQueryService } from "./services/TraceQueryService.js"
|
|
6
|
+
import { TelemetryStoreReadonly } from "./services/TelemetryStore.js"
|
|
8
7
|
|
|
9
8
|
const [command, ...args] = process.argv.slice(2)
|
|
10
9
|
|
|
11
|
-
const runQuiet = <A, E, R extends
|
|
10
|
+
const runQuiet = <A, E, R extends TelemetryStoreReadonly | never>(effect: Effect.Effect<A, E, R>) =>
|
|
12
11
|
queryRuntime.runPromise(effect.pipe(Effect.provideService(References.MinimumLogLevel, "None")))
|
|
13
12
|
|
|
14
13
|
try {
|
|
15
14
|
switch (command) {
|
|
16
15
|
case "services": {
|
|
17
|
-
const result = await runQuiet(Effect.flatMap(
|
|
16
|
+
const result = await runQuiet(Effect.flatMap(TelemetryStoreReadonly, (query) => query.listServices))
|
|
18
17
|
console.log(JSON.stringify(result, null, 2))
|
|
19
18
|
break
|
|
20
19
|
}
|
|
@@ -22,7 +21,7 @@ try {
|
|
|
22
21
|
case "traces": {
|
|
23
22
|
const service = args[0] ?? config.otel.serviceName
|
|
24
23
|
const limit = args[1] ? Number.parseInt(args[1], 10) : config.otel.traceFetchLimit
|
|
25
|
-
const result = await runQuiet(Effect.flatMap(
|
|
24
|
+
const result = await runQuiet(Effect.flatMap(TelemetryStoreReadonly, (query) => query.listRecentTraces(service, { limit })))
|
|
26
25
|
console.log(JSON.stringify(result, null, 2))
|
|
27
26
|
break
|
|
28
27
|
}
|
|
@@ -33,7 +32,7 @@ try {
|
|
|
33
32
|
throw new Error("Usage: bun run cli trace <trace-id>")
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
const result = await runQuiet(Effect.flatMap(
|
|
35
|
+
const result = await runQuiet(Effect.flatMap(TelemetryStoreReadonly, (query) => query.getTrace(traceId)))
|
|
37
36
|
console.log(JSON.stringify(result, null, 2))
|
|
38
37
|
break
|
|
39
38
|
}
|
|
@@ -55,7 +54,7 @@ try {
|
|
|
55
54
|
throw new Error("Usage: bun run cli trace-spans <trace-id>")
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
const result = await runQuiet(Effect.flatMap(
|
|
57
|
+
const result = await runQuiet(Effect.flatMap(TelemetryStoreReadonly, (query) => query.listTraceSpans(traceId)))
|
|
59
58
|
console.log(JSON.stringify(result, null, 2))
|
|
60
59
|
break
|
|
61
60
|
}
|
|
@@ -68,7 +67,7 @@ try {
|
|
|
68
67
|
const attributeStartIndex = operation ? 2 : 1
|
|
69
68
|
const attributeFilters = attributeFiltersFromArgs(args.slice(attributeStartIndex))
|
|
70
69
|
const result = await runQuiet(
|
|
71
|
-
Effect.flatMap(
|
|
70
|
+
Effect.flatMap(TelemetryStoreReadonly, (query) =>
|
|
72
71
|
query.searchSpans({
|
|
73
72
|
serviceName: service,
|
|
74
73
|
operation,
|
|
@@ -87,7 +86,7 @@ try {
|
|
|
87
86
|
const operation = args[1] && !isAttributeFilterToken(args[1]) ? args[1] : undefined
|
|
88
87
|
const attributeFilters = attributeFiltersFromArgs(args.slice(operation ? 2 : 1))
|
|
89
88
|
const result = await runQuiet(
|
|
90
|
-
Effect.flatMap(
|
|
89
|
+
Effect.flatMap(TelemetryStoreReadonly, (query) =>
|
|
91
90
|
query.searchTraces({
|
|
92
91
|
serviceName: service,
|
|
93
92
|
operation,
|
|
@@ -110,7 +109,7 @@ try {
|
|
|
110
109
|
}
|
|
111
110
|
|
|
112
111
|
const result = await runQuiet(
|
|
113
|
-
Effect.flatMap(
|
|
112
|
+
Effect.flatMap(TelemetryStoreReadonly, (query) =>
|
|
114
113
|
query.traceStats({
|
|
115
114
|
groupBy,
|
|
116
115
|
agg,
|
|
@@ -131,7 +130,7 @@ try {
|
|
|
131
130
|
|
|
132
131
|
case "logs": {
|
|
133
132
|
const service = args[0] ?? config.otel.serviceName
|
|
134
|
-
const result = await runQuiet(Effect.flatMap(
|
|
133
|
+
const result = await runQuiet(Effect.flatMap(TelemetryStoreReadonly, (query) => query.listRecentLogs(service)))
|
|
135
134
|
console.log(JSON.stringify(result, null, 2))
|
|
136
135
|
break
|
|
137
136
|
}
|
|
@@ -141,7 +140,7 @@ try {
|
|
|
141
140
|
const body = args[1] && !isAttributeFilterToken(args[1]) ? args[1] : undefined
|
|
142
141
|
const attributeFilters = attributeFiltersFromArgs(args.slice(body ? 2 : 1))
|
|
143
142
|
const result = await runQuiet(
|
|
144
|
-
Effect.flatMap(
|
|
143
|
+
Effect.flatMap(TelemetryStoreReadonly, (query) =>
|
|
145
144
|
query.searchLogs({
|
|
146
145
|
serviceName: service,
|
|
147
146
|
body,
|
|
@@ -163,7 +162,7 @@ try {
|
|
|
163
162
|
}
|
|
164
163
|
|
|
165
164
|
const result = await runQuiet(
|
|
166
|
-
Effect.flatMap(
|
|
165
|
+
Effect.flatMap(TelemetryStoreReadonly, (query) =>
|
|
167
166
|
query.logStats({
|
|
168
167
|
groupBy,
|
|
169
168
|
agg: "count",
|
|
@@ -183,7 +182,7 @@ try {
|
|
|
183
182
|
throw new Error("Usage: bun run cli trace-logs <trace-id>")
|
|
184
183
|
}
|
|
185
184
|
|
|
186
|
-
const result = await runQuiet(Effect.flatMap(
|
|
185
|
+
const result = await runQuiet(Effect.flatMap(TelemetryStoreReadonly, (query) => query.listTraceLogs(traceId)))
|
|
187
186
|
console.log(JSON.stringify(result, null, 2))
|
|
188
187
|
break
|
|
189
188
|
}
|
|
@@ -195,7 +194,7 @@ try {
|
|
|
195
194
|
}
|
|
196
195
|
|
|
197
196
|
const result = await runQuiet(
|
|
198
|
-
Effect.flatMap(
|
|
197
|
+
Effect.flatMap(TelemetryStoreReadonly, (query) =>
|
|
199
198
|
query.searchLogs({
|
|
200
199
|
spanId,
|
|
201
200
|
limit: config.otel.logFetchLimit,
|
|
@@ -214,7 +213,7 @@ try {
|
|
|
214
213
|
}
|
|
215
214
|
|
|
216
215
|
const result = await runQuiet(
|
|
217
|
-
Effect.flatMap(
|
|
216
|
+
Effect.flatMap(TelemetryStoreReadonly, (query) =>
|
|
218
217
|
query.listFacets({ type, field, limit: 20 }),
|
|
219
218
|
),
|
|
220
219
|
)
|
package/src/config.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { motelStateDir } from "./registry.js"
|
|
2
|
+
import * as path from "node:path"
|
|
3
|
+
|
|
1
4
|
const parseBoolean = (value: string | undefined, defaultValue: boolean) => {
|
|
2
5
|
const normalized = value?.trim().toLowerCase()
|
|
3
6
|
if (!normalized) return defaultValue
|
|
@@ -29,11 +32,14 @@ export const config = {
|
|
|
29
32
|
queryUrl: baseUrl,
|
|
30
33
|
exporterUrl: process.env.MOTEL_OTEL_EXPORTER_URL?.trim() || resolveOtelUrl("/v1/traces"),
|
|
31
34
|
logsExporterUrl: process.env.MOTEL_OTEL_LOGS_EXPORTER_URL?.trim() || resolveOtelUrl("/v1/logs"),
|
|
32
|
-
databasePath: process.env.MOTEL_OTEL_DB_PATH?.trim() ||
|
|
35
|
+
databasePath: process.env.MOTEL_OTEL_DB_PATH?.trim() || path.join(motelStateDir(), "telemetry.sqlite"),
|
|
33
36
|
traceLookbackMinutes: parsePositiveInt(process.env.MOTEL_OTEL_TRACE_LOOKBACK_MINUTES, 1440),
|
|
34
37
|
traceFetchLimit: parsePositiveInt(process.env.MOTEL_OTEL_TRACE_LIMIT, 100),
|
|
35
38
|
logFetchLimit: parsePositiveInt(process.env.MOTEL_OTEL_LOG_LIMIT, 80),
|
|
36
39
|
retentionHours: parsePositiveInt(process.env.MOTEL_OTEL_RETENTION_HOURS, 168),
|
|
37
40
|
maxDbSizeMb: parsePositiveInt(process.env.MOTEL_OTEL_MAX_DB_SIZE_MB, 1024),
|
|
41
|
+
retentionTraceBatch: parsePositiveInt(process.env.MOTEL_OTEL_RETENTION_TRACE_BATCH, 100),
|
|
42
|
+
retentionLogBatch: parsePositiveInt(process.env.MOTEL_OTEL_RETENTION_LOG_BATCH, 5_000),
|
|
43
|
+
retentionIntervalSeconds: parsePositiveInt(process.env.MOTEL_OTEL_RETENTION_INTERVAL_SECONDS, 10),
|
|
38
44
|
},
|
|
39
45
|
} as const
|