@kitlangton/motel 0.2.4 → 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 +23 -8
- package/README.md +13 -2
- package/package.json +35 -19
- package/skills/motel-debug/SKILL.md +203 -0
- package/skills/motel-debug/references/effect.md +38 -0
- package/src/App.tsx +12 -5
- package/src/StartupGate.tsx +289 -0
- package/src/cli.ts +15 -16
- package/src/config.ts +7 -1
- package/src/daemon.test.ts +332 -51
- package/src/daemon.ts +105 -153
- package/src/httpApi.ts +1 -0
- package/src/httpListPolicy.test.ts +76 -0
- package/src/httpListPolicy.ts +129 -0
- package/src/index.tsx +9 -2
- package/src/localServer.ts +194 -313
- package/src/mcp.ts +2 -1
- package/src/motel.ts +0 -2
- 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 +22 -8
- package/src/services/LogQueryService.ts +13 -27
- package/src/services/TelemetryQuery.ts +62 -0
- package/src/services/TelemetryStore.ts +546 -231
- package/src/services/TraceQueryService.ts +22 -56
- 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/startupBench.ts +19 -0
- 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 +35 -23
- package/src/ui/atoms.ts +1 -1
- 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/persistence.ts +3 -3
- package/src/ui/primitives.tsx +1 -1
- package/src/ui/state.ts +2 -0
- package/src/ui/theme.ts +7 -5
- 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 +28 -5
- 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
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
import { RGBA, TextAttributes } from "@opentui/core"
|
|
3
|
+
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react"
|
|
4
|
+
import { useEffect, useMemo, useState } from "react"
|
|
5
|
+
import { App } from "./App.js"
|
|
6
|
+
import { createDaemonManager, ensureManagedDaemon, getManagedDaemonStatus, type DaemonStatus } from "./daemon.js"
|
|
7
|
+
import { MOTEL_SERVICE_ID } from "./registry.js"
|
|
8
|
+
import { Divider, PlainLine, TextLine } from "./ui/primitives.tsx"
|
|
9
|
+
import { colors } from "./ui/theme.ts"
|
|
10
|
+
|
|
11
|
+
type ConflictStatus = DaemonStatus & {
|
|
12
|
+
readonly service: typeof MOTEL_SERVICE_ID
|
|
13
|
+
readonly pid: number
|
|
14
|
+
readonly workdir: string
|
|
15
|
+
readonly reason: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type ConflictScreenState = {
|
|
19
|
+
kind: "conflict"
|
|
20
|
+
message: string
|
|
21
|
+
status: ConflictStatus
|
|
22
|
+
busy: boolean
|
|
23
|
+
notice: string | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type ErrorScreenState = {
|
|
27
|
+
kind: "error"
|
|
28
|
+
message: string
|
|
29
|
+
busy: boolean
|
|
30
|
+
notice: string | null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type StartupState =
|
|
34
|
+
| { kind: "loading"; message: string }
|
|
35
|
+
| { kind: "ready" }
|
|
36
|
+
| ConflictScreenState
|
|
37
|
+
| ErrorScreenState
|
|
38
|
+
|
|
39
|
+
type RecoveryAction = {
|
|
40
|
+
readonly key: string
|
|
41
|
+
readonly label: string
|
|
42
|
+
readonly run: () => Promise<void>
|
|
43
|
+
readonly disabled?: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const readStatus = () => Effect.runPromise(getManagedDaemonStatus)
|
|
47
|
+
const startDaemon = () => Effect.runPromise(ensureManagedDaemon)
|
|
48
|
+
|
|
49
|
+
const parsePort = (url: string) => {
|
|
50
|
+
try {
|
|
51
|
+
const port = Number(new URL(url).port)
|
|
52
|
+
return Number.isFinite(port) && port > 0 ? port : undefined
|
|
53
|
+
} catch {
|
|
54
|
+
return undefined
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const isRecoverableConflict = (status: DaemonStatus | null): status is ConflictStatus =>
|
|
59
|
+
status !== null &&
|
|
60
|
+
status.service === MOTEL_SERVICE_ID &&
|
|
61
|
+
status.pid !== null &&
|
|
62
|
+
status.workdir !== null &&
|
|
63
|
+
status.reason !== null
|
|
64
|
+
|
|
65
|
+
const stopConflictingDaemon = async (status: ConflictStatus) => {
|
|
66
|
+
const port = parsePort(status.url)
|
|
67
|
+
const manager = createDaemonManager({
|
|
68
|
+
workdir: status.workdir ?? undefined,
|
|
69
|
+
databasePath: status.databasePath,
|
|
70
|
+
port,
|
|
71
|
+
})
|
|
72
|
+
await Effect.runPromise(manager.stop)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const LoadingScreen = ({ width, height, message }: { width: number; height: number; message: string }) => {
|
|
76
|
+
const panelWidth = Math.min(76, Math.max(50, width - 8))
|
|
77
|
+
const left = Math.max(0, Math.floor((width - panelWidth) / 2))
|
|
78
|
+
const top = Math.max(0, Math.floor((height - 5) / 2))
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<box width={width} height={height} backgroundColor={RGBA.fromHex(colors.screenBg)}>
|
|
82
|
+
<box position="absolute" left={left} top={top} width={panelWidth} flexDirection="column">
|
|
83
|
+
<TextLine>
|
|
84
|
+
<span fg={colors.accent} attributes={TextAttributes.BOLD}>MOTEL</span>
|
|
85
|
+
<span fg={colors.separator}>{" · "}</span>
|
|
86
|
+
<span fg={colors.muted}>starting up...</span>
|
|
87
|
+
</TextLine>
|
|
88
|
+
<Divider width={panelWidth} />
|
|
89
|
+
<box paddingTop={1}>
|
|
90
|
+
<PlainLine text={message} fg={colors.count} />
|
|
91
|
+
</box>
|
|
92
|
+
</box>
|
|
93
|
+
</box>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const RecoveryScreen = ({
|
|
98
|
+
title,
|
|
99
|
+
message,
|
|
100
|
+
width,
|
|
101
|
+
height,
|
|
102
|
+
detailLines,
|
|
103
|
+
actions,
|
|
104
|
+
selectedIndex,
|
|
105
|
+
notice,
|
|
106
|
+
busy,
|
|
107
|
+
}: {
|
|
108
|
+
readonly title: string
|
|
109
|
+
readonly message: string
|
|
110
|
+
readonly width: number
|
|
111
|
+
readonly height: number
|
|
112
|
+
readonly detailLines: readonly string[]
|
|
113
|
+
readonly actions: readonly RecoveryAction[]
|
|
114
|
+
readonly selectedIndex: number
|
|
115
|
+
readonly notice: string | null
|
|
116
|
+
readonly busy: boolean
|
|
117
|
+
}) => {
|
|
118
|
+
const panelWidth = Math.min(96, Math.max(64, width - 8))
|
|
119
|
+
const left = Math.max(0, Math.floor((width - panelWidth) / 2))
|
|
120
|
+
const bodyHeight = 9 + detailLines.length + actions.length + (notice ? 2 : 0)
|
|
121
|
+
const top = Math.max(0, Math.floor((height - bodyHeight) / 2))
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<box width={width} height={height} backgroundColor={RGBA.fromHex(colors.screenBg)}>
|
|
125
|
+
<box position="absolute" left={left} top={top} width={panelWidth} flexDirection="column">
|
|
126
|
+
<TextLine>
|
|
127
|
+
<span fg={colors.accent} attributes={TextAttributes.BOLD}>MOTEL</span>
|
|
128
|
+
<span fg={colors.separator}>{" · "}</span>
|
|
129
|
+
<span fg={colors.error} attributes={TextAttributes.BOLD}>{title}</span>
|
|
130
|
+
</TextLine>
|
|
131
|
+
<Divider width={panelWidth} />
|
|
132
|
+
<box paddingTop={1} paddingBottom={1} flexDirection="column">
|
|
133
|
+
<PlainLine text={message} fg={colors.text} />
|
|
134
|
+
</box>
|
|
135
|
+
{detailLines.map((line, index) => (
|
|
136
|
+
<PlainLine key={`${index}:${line}`} text={line} fg={colors.muted} />
|
|
137
|
+
))}
|
|
138
|
+
<box paddingTop={1} flexDirection="column">
|
|
139
|
+
{actions.map((action, index) => {
|
|
140
|
+
const selected = index === selectedIndex
|
|
141
|
+
const prefix = selected ? ">" : " "
|
|
142
|
+
const text = `${prefix} [${action.key}] ${action.label}${action.disabled ? " (unavailable)" : ""}`
|
|
143
|
+
return (
|
|
144
|
+
<TextLine key={action.key} bg={selected ? colors.selectedBg : undefined}>
|
|
145
|
+
<span fg={selected ? colors.selectedText : colors.text}>{text}</span>
|
|
146
|
+
</TextLine>
|
|
147
|
+
)
|
|
148
|
+
})}
|
|
149
|
+
</box>
|
|
150
|
+
<box paddingTop={1} flexDirection="column">
|
|
151
|
+
{notice ? <PlainLine text={notice} fg={busy ? colors.warning : colors.count} /> : null}
|
|
152
|
+
<PlainLine text={busy ? "Working..." : "j/k or ↑↓ select · enter run · r retry · k stop incompatible daemon · q quit"} fg={colors.count} />
|
|
153
|
+
</box>
|
|
154
|
+
</box>
|
|
155
|
+
</box>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const StartupGate = () => {
|
|
160
|
+
const renderer = useRenderer()
|
|
161
|
+
const { width = 100, height = 24 } = useTerminalDimensions()
|
|
162
|
+
const [startupState, setStartupState] = useState<StartupState>({ kind: "loading", message: "Checking managed daemon..." })
|
|
163
|
+
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
164
|
+
|
|
165
|
+
const attemptStart = async () => {
|
|
166
|
+
setStartupState({ kind: "loading", message: "Checking managed daemon..." })
|
|
167
|
+
try {
|
|
168
|
+
await startDaemon()
|
|
169
|
+
setStartupState({ kind: "ready" })
|
|
170
|
+
} catch (error) {
|
|
171
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
172
|
+
const status = await readStatus().catch(() => null)
|
|
173
|
+
if (isRecoverableConflict(status)) {
|
|
174
|
+
setSelectedIndex(0)
|
|
175
|
+
setStartupState({ kind: "conflict", message, status, busy: false, notice: null })
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
setSelectedIndex(0)
|
|
179
|
+
setStartupState({ kind: "error", message, busy: false, notice: null })
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
void attemptStart()
|
|
185
|
+
}, [])
|
|
186
|
+
|
|
187
|
+
const actions = useMemo<readonly RecoveryAction[]>(() => {
|
|
188
|
+
if (startupState.kind === "conflict") {
|
|
189
|
+
return [
|
|
190
|
+
{ key: "r", label: "Retry startup", run: attemptStart },
|
|
191
|
+
{
|
|
192
|
+
key: "k",
|
|
193
|
+
label: `Stop incompatible daemon (${startupState.status.pid})`,
|
|
194
|
+
run: async () => {
|
|
195
|
+
setStartupState((current) => current.kind === "conflict"
|
|
196
|
+
? { ...current, busy: true, notice: `Stopping daemon ${current.status.pid}...` }
|
|
197
|
+
: current)
|
|
198
|
+
try {
|
|
199
|
+
await stopConflictingDaemon(startupState.status)
|
|
200
|
+
await attemptStart()
|
|
201
|
+
} catch (error) {
|
|
202
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
203
|
+
setStartupState((current) => current.kind === "conflict"
|
|
204
|
+
? { ...current, busy: false, notice: message }
|
|
205
|
+
: current)
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
{ key: "q", label: "Quit", run: async () => { renderer.destroy() } },
|
|
210
|
+
]
|
|
211
|
+
}
|
|
212
|
+
if (startupState.kind === "error") {
|
|
213
|
+
return [
|
|
214
|
+
{ key: "r", label: "Retry startup", run: attemptStart },
|
|
215
|
+
{ key: "q", label: "Quit", run: async () => { renderer.destroy() } },
|
|
216
|
+
]
|
|
217
|
+
}
|
|
218
|
+
return []
|
|
219
|
+
}, [renderer, startupState])
|
|
220
|
+
|
|
221
|
+
useKeyboard((key) => {
|
|
222
|
+
if (startupState.kind === "ready" || startupState.kind === "loading" || key.repeated) return
|
|
223
|
+
if (key.name === "q" || (key.ctrl && key.name === "c")) {
|
|
224
|
+
renderer.destroy()
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
if (key.name === "up" || key.name === "k") {
|
|
228
|
+
setSelectedIndex((current) => (current + actions.length - 1) % actions.length)
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
if (key.name === "down" || key.name === "j") {
|
|
232
|
+
setSelectedIndex((current) => (current + 1) % actions.length)
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
if (key.name === "r") {
|
|
236
|
+
void actions.find((action) => action.key === "r")?.run()
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
if (key.name === "k") {
|
|
240
|
+
void actions.find((action) => action.key === "k")?.run()
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
if (key.name === "return" || key.name === "enter") {
|
|
244
|
+
void actions[selectedIndex]?.run()
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
if (startupState.kind === "ready") return <App />
|
|
249
|
+
if (startupState.kind === "loading") return <LoadingScreen width={width} height={height} message={startupState.message} />
|
|
250
|
+
if (startupState.kind === "conflict") {
|
|
251
|
+
const status = startupState.status
|
|
252
|
+
const detailLines = [
|
|
253
|
+
`Port: ${status.url}`,
|
|
254
|
+
`Daemon workdir: ${status.workdir}`,
|
|
255
|
+
`Daemon pid: ${status.pid}`,
|
|
256
|
+
`Incompatible database: ${status.databasePath}`,
|
|
257
|
+
status.workdir.startsWith("/tmp") || status.workdir.startsWith("/private/tmp")
|
|
258
|
+
? "This looks like a temp/test daemon."
|
|
259
|
+
: "This daemon is using a different database configuration.",
|
|
260
|
+
]
|
|
261
|
+
return (
|
|
262
|
+
<RecoveryScreen
|
|
263
|
+
title="Daemon Configuration Conflict"
|
|
264
|
+
message={startupState.message}
|
|
265
|
+
width={width}
|
|
266
|
+
height={height}
|
|
267
|
+
detailLines={detailLines}
|
|
268
|
+
actions={actions}
|
|
269
|
+
selectedIndex={selectedIndex}
|
|
270
|
+
notice={startupState.notice}
|
|
271
|
+
busy={startupState.busy}
|
|
272
|
+
/>
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
<RecoveryScreen
|
|
278
|
+
title="Startup Error"
|
|
279
|
+
message={startupState.message}
|
|
280
|
+
width={width}
|
|
281
|
+
height={height}
|
|
282
|
+
detailLines={["Retry startup or quit the TUI."]}
|
|
283
|
+
actions={actions}
|
|
284
|
+
selectedIndex={selectedIndex}
|
|
285
|
+
notice={startupState.notice}
|
|
286
|
+
busy={startupState.busy}
|
|
287
|
+
/>
|
|
288
|
+
)
|
|
289
|
+
}
|
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
|