@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/src/localServer.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { promises as fs } from "node:fs"
|
|
2
1
|
import path from "node:path"
|
|
3
|
-
import { Effect, Layer } from "effect"
|
|
4
|
-
import { config
|
|
2
|
+
import { Effect, FileSystem, Layer } from "effect"
|
|
3
|
+
import { config } from "./config.js"
|
|
5
4
|
import { HttpApiBuilder, HttpApiScalar } from "effect/unstable/httpapi"
|
|
6
5
|
import * as HttpMiddleware from "effect/unstable/http/HttpMiddleware"
|
|
7
6
|
import * as HttpRouter from "effect/unstable/http/HttpRouter"
|
|
@@ -9,43 +8,46 @@ import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"
|
|
|
9
8
|
import * as HttpStaticServer from "effect/unstable/http/HttpStaticServer"
|
|
10
9
|
import * as BunHttpServer from "@effect/platform-bun/BunHttpServer"
|
|
11
10
|
import { MotelHttpApi } from "./httpApi.js"
|
|
12
|
-
import {
|
|
13
|
-
import { MOTEL_SERVICE_ID, MOTEL_VERSION, removeRegistryEntry, writeRegistryEntry } from "./registry.js"
|
|
11
|
+
import { AI_LIST, LOG_LIST, LOG_STATS, SPAN_LIST, TRACE_LIST, TRACE_STATS, listMeta, logCursorArgs, paginateLogs, paginateSummaries, parseLimit, parseListParams as decodeListParams, parseLookbackMinutes, requestUrl as decodeRequestUrl, traceCursorArgs, type ListBounds, type ListParams } from "./httpListPolicy.js"
|
|
12
|
+
import { MOTEL_SERVICE_ID, MOTEL_VERSION, processIdentity, removeRegistryEntry, writeRegistryEntry } from "./registry.js"
|
|
14
13
|
import { AsyncIngest, AsyncIngestLive } from "./services/AsyncIngest.js"
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import type { LogItem, TraceItem, TraceSummaryItem } from "./domain.js"
|
|
14
|
+
import { TelemetryStoreReadonly } from "./services/TelemetryStore.js"
|
|
15
|
+
import { TelemetryQueryLive } from "./services/TelemetryQuery.js"
|
|
16
|
+
import type { LogItem, TraceItem } from "./domain.js"
|
|
19
17
|
import { lifecycleLabel } from "./ui/format.js"
|
|
18
|
+
import { decodeProtobufLogs, decodeProtobufTraces } from "./otlpProtobuf.js"
|
|
19
|
+
import type { OtlpLogExportRequest, OtlpTraceExportRequest } from "./otlp.js"
|
|
20
20
|
|
|
21
21
|
// Set by the RegistryLayer acquisition once the Bun socket has bound.
|
|
22
22
|
// Both /api/health and the registry entry read from here so they agree
|
|
23
23
|
// on a single server-start timestamp, and the value reflects actual
|
|
24
24
|
// listen time rather than module-evaluation time.
|
|
25
|
-
let serverStartedAt: string = new Date(
|
|
25
|
+
let serverStartedAt: string = new Date().toISOString()
|
|
26
26
|
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const TRACE_DEFAULT_LOOKBACK = 60
|
|
30
|
-
const TRACE_MAX_LOOKBACK = 24 * 60
|
|
31
|
-
const SPAN_DEFAULT_LIMIT = 100
|
|
32
|
-
const SPAN_MAX_LIMIT = 500
|
|
33
|
-
const LOG_DEFAULT_LIMIT = 100
|
|
34
|
-
const LOG_MAX_LIMIT = 500
|
|
35
|
-
const LOG_DEFAULT_LOOKBACK = 60
|
|
36
|
-
const LOG_MAX_LOOKBACK = 24 * 60
|
|
27
|
+
const requestUrl = (request: { readonly url: string }) => decodeRequestUrl(request, config.otel.baseUrl)
|
|
28
|
+
const parseListParams = (request: { readonly url: string }, bounds: ListBounds) => decodeListParams(request, bounds, config.otel.baseUrl)
|
|
37
29
|
|
|
38
30
|
const jsonResponse = (value: unknown, status = 200) => HttpServerResponse.jsonUnsafe(value, { status })
|
|
39
31
|
const textResponse = (value: string) => HttpServerResponse.text(value)
|
|
40
32
|
const htmlResponse = (value: string) => HttpServerResponse.html(value)
|
|
41
33
|
const notFoundResponse = (message = "Not found") => jsonResponse({ error: message }, 404)
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
34
|
+
const healthPayload = () => ({
|
|
35
|
+
ok: true,
|
|
36
|
+
service: MOTEL_SERVICE_ID,
|
|
37
|
+
databasePath: config.otel.databasePath,
|
|
38
|
+
pid: process.pid,
|
|
39
|
+
url: config.otel.baseUrl,
|
|
40
|
+
workdir: process.cwd(),
|
|
41
|
+
startedAt: serverStartedAt,
|
|
42
|
+
version: MOTEL_VERSION,
|
|
43
|
+
instanceId: process.env.MOTEL_DAEMON_INSTANCE_ID?.trim(),
|
|
44
|
+
})
|
|
45
|
+
// Query handlers resolve against the readonly store identifier so they
|
|
46
|
+
// don't contend with the writer connection that owns ingest/retention.
|
|
47
|
+
const withRead = <A>(f: (store: TelemetryStoreReadonly["Service"]) => Effect.Effect<A, Error>) => Effect.flatMap(TelemetryStoreReadonly, f)
|
|
46
48
|
// Response-building helpers are generic in R so a handler can depend
|
|
47
|
-
// on
|
|
48
|
-
// without forcing every handler onto the same service surface.
|
|
49
|
+
// on AsyncIngest (worker-RPC path) or TelemetryStoreReadonly (query
|
|
50
|
+
// path) without forcing every handler onto the same service surface.
|
|
49
51
|
const respondJson = <A, R>(effect: Effect.Effect<A, unknown, R>) =>
|
|
50
52
|
Effect.match(effect, {
|
|
51
53
|
onFailure: (error) => jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 500),
|
|
@@ -57,144 +59,50 @@ const respondRaw = <R>(effect: Effect.Effect<ReturnType<typeof jsonResponse>, un
|
|
|
57
59
|
onSuccess: (value) => value,
|
|
58
60
|
})
|
|
59
61
|
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
if (unit === "d") return amount * 1440
|
|
72
|
-
if (unit === "h") return amount * 60
|
|
73
|
-
return amount
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const parseBoundedLookbackMinutes = (value: string | null, fallback: number, max: number) => clamp(parseLookbackMinutes(value, fallback), 1, max)
|
|
77
|
-
|
|
78
|
-
const attributeFiltersFromQuery = (url: URL) =>
|
|
79
|
-
attributeFiltersFromEntries(url.searchParams.entries())
|
|
80
|
-
|
|
81
|
-
const attributeContainsFiltersFromQuery = (url: URL) =>
|
|
82
|
-
attributeContainsFiltersFromEntries(url.searchParams.entries())
|
|
83
|
-
|
|
84
|
-
type CursorShape =
|
|
85
|
-
| { readonly kind: "trace"; readonly startedAt: number; readonly id: string }
|
|
86
|
-
| { readonly kind: "log"; readonly timestamp: number; readonly id: string }
|
|
87
|
-
|
|
88
|
-
const encodeCursor = (cursor: CursorShape) => Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url")
|
|
89
|
-
|
|
90
|
-
const decodeCursor = (value: string | null): CursorShape | null => {
|
|
91
|
-
if (!value) return null
|
|
92
|
-
try {
|
|
93
|
-
return JSON.parse(Buffer.from(value, "base64url").toString("utf8")) as CursorShape
|
|
94
|
-
} catch {
|
|
95
|
-
return null
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const formatLookback = (minutes: number) => {
|
|
100
|
-
if (minutes % 1440 === 0) return `${minutes / 1440}d`
|
|
101
|
-
if (minutes % 60 === 0) return `${minutes / 60}h`
|
|
102
|
-
return `${minutes}m`
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const listMeta = (input: { readonly limit: number; readonly lookbackMinutes: number; readonly returned: number; readonly truncated: boolean; readonly nextCursor: string | null }) => ({
|
|
106
|
-
limit: input.limit,
|
|
107
|
-
lookback: formatLookback(input.lookbackMinutes),
|
|
108
|
-
returned: input.returned,
|
|
109
|
-
truncated: input.truncated,
|
|
110
|
-
nextCursor: input.nextCursor,
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
const paginateSummaries = (summaries: readonly TraceSummaryItem[], options: { readonly limit: number; readonly lookbackMinutes: number; readonly cursor: CursorShape | null }) => {
|
|
114
|
-
const page = summaries.slice(0, options.limit)
|
|
115
|
-
const last = page.at(-1)
|
|
116
|
-
return {
|
|
117
|
-
data: page,
|
|
118
|
-
meta: listMeta({
|
|
119
|
-
limit: options.limit,
|
|
120
|
-
lookbackMinutes: options.lookbackMinutes,
|
|
121
|
-
returned: page.length,
|
|
122
|
-
truncated: summaries.length > page.length,
|
|
123
|
-
nextCursor: last ? encodeCursor({ kind: "trace", startedAt: last.startedAt.getTime(), id: last.traceId }) : null,
|
|
124
|
-
}),
|
|
62
|
+
const readOtlpBody = <T>(
|
|
63
|
+
request: {
|
|
64
|
+
readonly json: Effect.Effect<unknown, unknown>
|
|
65
|
+
readonly arrayBuffer: Effect.Effect<ArrayBuffer, unknown>
|
|
66
|
+
readonly headers: Readonly<Record<string, string | undefined>>
|
|
67
|
+
},
|
|
68
|
+
decodeProtobuf: (bytes: Uint8Array) => T,
|
|
69
|
+
): Effect.Effect<T, unknown> => {
|
|
70
|
+
const contentType = (request.headers["content-type"] ?? "").toLowerCase()
|
|
71
|
+
if (contentType.includes("application/x-protobuf") || contentType.includes("application/protobuf")) {
|
|
72
|
+
return Effect.map(request.arrayBuffer, (buffer) => decodeProtobuf(new Uint8Array(buffer)))
|
|
125
73
|
}
|
|
74
|
+
return Effect.map(request.json, (payload) => payload as T)
|
|
126
75
|
}
|
|
127
76
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
readonly severity?: string | null
|
|
147
|
-
readonly traceId?: string | null
|
|
148
|
-
readonly spanId?: string | null
|
|
149
|
-
readonly body?: string | null
|
|
150
|
-
readonly attributeFilters?: Readonly<Record<string, string>>
|
|
151
|
-
readonly attributeContainsFilters?: Readonly<Record<string, string>>
|
|
152
|
-
readonly limit: number
|
|
153
|
-
readonly lookbackMinutes: number
|
|
154
|
-
readonly cursor: CursorShape | null
|
|
155
|
-
}) =>
|
|
156
|
-
Effect.flatMap(LogQueryService.asEffect(), (store) =>
|
|
157
|
-
Effect.map(
|
|
158
|
-
store.searchLogs({
|
|
159
|
-
serviceName: input.serviceName,
|
|
160
|
-
severity: input.severity,
|
|
161
|
-
traceId: input.traceId,
|
|
162
|
-
spanId: input.spanId,
|
|
163
|
-
body: input.body,
|
|
164
|
-
lookbackMinutes: input.lookbackMinutes,
|
|
165
|
-
limit: input.limit + 1,
|
|
166
|
-
cursorTimestampMs: input.cursor?.kind === "log" ? input.cursor.timestamp : undefined,
|
|
167
|
-
cursorId: input.cursor?.kind === "log" ? input.cursor.id : undefined,
|
|
168
|
-
attributeFilters: input.attributeFilters,
|
|
169
|
-
attributeContainsFilters: input.attributeContainsFilters,
|
|
170
|
-
}),
|
|
171
|
-
(logs) => paginateLogs(logs, {
|
|
172
|
-
limit: input.limit,
|
|
173
|
-
lookbackMinutes: input.lookbackMinutes,
|
|
174
|
-
cursor: input.cursor,
|
|
175
|
-
}),
|
|
176
|
-
),
|
|
77
|
+
// Log page loader: takes the parsed list params + any resource-specific
|
|
78
|
+
// filter values (service, severity, traceId, spanId, body), runs the
|
|
79
|
+
// store query with limit+1 to detect a next page, and shapes the
|
|
80
|
+
// paginated response.
|
|
81
|
+
const loadLogsPage = (
|
|
82
|
+
params: ListParams,
|
|
83
|
+
filters: { readonly serviceName?: string | null; readonly severity?: string | null; readonly traceId?: string | null; readonly spanId?: string | null; readonly body?: string | null },
|
|
84
|
+
) =>
|
|
85
|
+
Effect.map(
|
|
86
|
+
withRead((store) => store.searchLogs({
|
|
87
|
+
...filters,
|
|
88
|
+
...logCursorArgs(params.cursor),
|
|
89
|
+
attributeFilters: params.attributeFilters,
|
|
90
|
+
attributeContainsFilters: params.attributeContainsFilters,
|
|
91
|
+
lookbackMinutes: params.lookbackMinutes,
|
|
92
|
+
limit: params.limit + 1,
|
|
93
|
+
})),
|
|
94
|
+
(logs) => paginateLogs(logs, params),
|
|
177
95
|
)
|
|
178
96
|
|
|
179
97
|
const handleLogSearch = (request: { readonly url: string }) =>
|
|
180
98
|
respondRaw(Effect.gen(function*() {
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
serviceName: url.searchParams.get("service"),
|
|
189
|
-
severity: url.searchParams.get("severity"),
|
|
190
|
-
traceId: url.searchParams.get("traceId"),
|
|
191
|
-
spanId: url.searchParams.get("spanId"),
|
|
192
|
-
body: url.searchParams.get("body"),
|
|
193
|
-
attributeFilters,
|
|
194
|
-
attributeContainsFilters,
|
|
195
|
-
limit,
|
|
196
|
-
lookbackMinutes,
|
|
197
|
-
cursor,
|
|
99
|
+
const params = parseListParams(request, LOG_LIST)
|
|
100
|
+
return jsonResponse(yield* loadLogsPage(params, {
|
|
101
|
+
serviceName: params.url.searchParams.get("service"),
|
|
102
|
+
severity: params.url.searchParams.get("severity"),
|
|
103
|
+
traceId: params.url.searchParams.get("traceId"),
|
|
104
|
+
spanId: params.url.searchParams.get("spanId"),
|
|
105
|
+
body: params.url.searchParams.get("body"),
|
|
198
106
|
}))
|
|
199
107
|
}))
|
|
200
108
|
|
|
@@ -281,103 +189,84 @@ const TelemetryGroupLive = HttpApiBuilder.group(
|
|
|
281
189
|
Effect.succeed(textResponse("motel local telemetry server\n\nPOST /v1/traces\nPOST /v1/logs\nGET /api/services\nGET /api/traces\nGET /api/traces/search\nGET /api/traces/stats\nGET /api/traces/<trace-id>\nGET /api/traces/<trace-id>/spans\nGET /api/traces/<trace-id>/logs\nGET /api/spans/search\nGET /api/spans/<span-id>\nGET /api/spans/<span-id>/logs\nGET /api/logs\nGET /api/logs/search\nGET /api/logs/stats\nGET /api/ai/calls\nGET /api/ai/calls/<span-id>\nGET /api/ai/stats\nGET /api/facets?type=logs&field=severity\nGET /api/docs\nGET /api/docs/<name>\nGET /openapi.json\nGET /docs\nGET /trace/<trace-id>\n")),
|
|
282
190
|
)
|
|
283
191
|
.handle("health", () =>
|
|
284
|
-
Effect.succeed(
|
|
285
|
-
ok: true,
|
|
286
|
-
service: MOTEL_SERVICE_ID,
|
|
287
|
-
databasePath: config.otel.databasePath,
|
|
288
|
-
pid: process.pid,
|
|
289
|
-
url: config.otel.baseUrl,
|
|
290
|
-
workdir: process.cwd(),
|
|
291
|
-
startedAt: serverStartedAt,
|
|
292
|
-
version: MOTEL_VERSION,
|
|
293
|
-
}),
|
|
192
|
+
HttpMiddleware.withLoggerDisabled(Effect.succeed(healthPayload())),
|
|
294
193
|
)
|
|
295
194
|
// OTLP ingest is routed to the worker thread via AsyncIngest
|
|
296
195
|
// so the main event loop stays free during heavy SQLite writes.
|
|
297
|
-
//
|
|
298
|
-
//
|
|
196
|
+
// Read queries use a separate query worker so synchronous SQLite
|
|
197
|
+
// work cannot block the HTTP event loop.
|
|
299
198
|
.handleRaw("ingestTraces", ({ request }) =>
|
|
300
|
-
respondRaw(
|
|
301
|
-
Effect.flatMap(
|
|
302
|
-
|
|
303
|
-
|
|
199
|
+
HttpMiddleware.withLoggerDisabled(respondRaw(
|
|
200
|
+
Effect.flatMap(
|
|
201
|
+
readOtlpBody<OtlpTraceExportRequest>(request, decodeProtobufTraces),
|
|
202
|
+
(payload) => Effect.map(
|
|
203
|
+
Effect.flatMap(AsyncIngest, (ingest) => ingest.ingestTraces({ payload })),
|
|
304
204
|
(result) => jsonResponse(result),
|
|
305
205
|
),
|
|
306
206
|
),
|
|
307
|
-
),
|
|
207
|
+
)),
|
|
308
208
|
)
|
|
309
209
|
.handleRaw("ingestLogs", ({ request }) =>
|
|
310
|
-
respondRaw(
|
|
311
|
-
Effect.flatMap(
|
|
312
|
-
|
|
313
|
-
|
|
210
|
+
HttpMiddleware.withLoggerDisabled(respondRaw(
|
|
211
|
+
Effect.flatMap(
|
|
212
|
+
readOtlpBody<OtlpLogExportRequest>(request, decodeProtobufLogs),
|
|
213
|
+
(payload) => Effect.map(
|
|
214
|
+
Effect.flatMap(AsyncIngest, (ingest) => ingest.ingestLogs({ payload })),
|
|
314
215
|
(result) => jsonResponse(result),
|
|
315
216
|
),
|
|
316
217
|
),
|
|
317
|
-
),
|
|
218
|
+
)),
|
|
318
219
|
)
|
|
319
|
-
.handleRaw("services", () => respondJson(Effect.map(
|
|
220
|
+
.handleRaw("services", () => respondJson(Effect.map(withRead((store) => store.listServices), (data) => ({ data }))))
|
|
320
221
|
.handleRaw("traces", ({ request }) =>
|
|
321
222
|
respondRaw(Effect.gen(function*() {
|
|
322
|
-
const
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const data = yield* withTraceQuery((store) => store.listTraceSummaries(service, {
|
|
328
|
-
limit: limit + 1,
|
|
329
|
-
lookbackMinutes,
|
|
330
|
-
cursorStartedAtMs: cursor?.kind === "trace" ? cursor.startedAt : undefined,
|
|
331
|
-
cursorTraceId: cursor?.kind === "trace" ? cursor.id : undefined,
|
|
223
|
+
const params = parseListParams(request, TRACE_LIST)
|
|
224
|
+
const data = yield* withRead((store) => store.listTraceSummaries(params.url.searchParams.get("service"), {
|
|
225
|
+
limit: params.limit + 1,
|
|
226
|
+
lookbackMinutes: params.lookbackMinutes,
|
|
227
|
+
...traceCursorArgs(params.cursor),
|
|
332
228
|
}))
|
|
333
|
-
return jsonResponse(paginateSummaries(data,
|
|
229
|
+
return jsonResponse(paginateSummaries(data, params))
|
|
334
230
|
})),
|
|
335
231
|
)
|
|
336
232
|
.handleRaw("searchTraces", ({ request }) =>
|
|
337
233
|
respondRaw(Effect.gen(function*() {
|
|
338
|
-
const
|
|
339
|
-
const
|
|
340
|
-
const limit = parseBoundedLimit(url.searchParams.get("limit"), TRACE_DEFAULT_LIMIT, TRACE_MAX_LIMIT)
|
|
341
|
-
const lookbackMinutes = parseBoundedLookbackMinutes(url.searchParams.get("lookback"), TRACE_DEFAULT_LOOKBACK, TRACE_MAX_LOOKBACK)
|
|
342
|
-
const cursor = decodeCursor(url.searchParams.get("cursor"))
|
|
343
|
-
const data = yield* withTraceQuery((store) =>
|
|
234
|
+
const params = parseListParams(request, TRACE_LIST)
|
|
235
|
+
const data = yield* withRead((store) =>
|
|
344
236
|
store.searchTraceSummaries({
|
|
345
|
-
serviceName: url.searchParams.get("service"),
|
|
346
|
-
operation: url.searchParams.get("operation"),
|
|
347
|
-
status: (url.searchParams.get("status") as "ok" | "error" | null) ?? null,
|
|
348
|
-
minDurationMs: url.searchParams.get("minDurationMs") ? Number.parseFloat(url.searchParams.get("minDurationMs") ?? "") : null,
|
|
349
|
-
attributeFilters,
|
|
350
|
-
aiText: url.searchParams.get("aiText"),
|
|
351
|
-
limit: limit + 1,
|
|
352
|
-
lookbackMinutes,
|
|
353
|
-
|
|
354
|
-
cursorTraceId: cursor?.kind === "trace" ? cursor.id : undefined,
|
|
237
|
+
serviceName: params.url.searchParams.get("service"),
|
|
238
|
+
operation: params.url.searchParams.get("operation"),
|
|
239
|
+
status: (params.url.searchParams.get("status") as "ok" | "error" | null) ?? null,
|
|
240
|
+
minDurationMs: params.url.searchParams.get("minDurationMs") ? Number.parseFloat(params.url.searchParams.get("minDurationMs") ?? "") : null,
|
|
241
|
+
attributeFilters: params.attributeFilters,
|
|
242
|
+
aiText: params.url.searchParams.get("aiText"),
|
|
243
|
+
limit: params.limit + 1,
|
|
244
|
+
lookbackMinutes: params.lookbackMinutes,
|
|
245
|
+
...traceCursorArgs(params.cursor),
|
|
355
246
|
}),
|
|
356
247
|
)
|
|
357
|
-
return jsonResponse(paginateSummaries(data,
|
|
248
|
+
return jsonResponse(paginateSummaries(data, params))
|
|
358
249
|
})),
|
|
359
250
|
)
|
|
360
251
|
.handleRaw("traceStats", ({ request }) =>
|
|
361
252
|
respondRaw(Effect.gen(function*() {
|
|
362
|
-
const
|
|
363
|
-
const
|
|
364
|
-
const
|
|
365
|
-
const agg = url.searchParams.get("agg")
|
|
366
|
-
const lookbackMinutes = parseBoundedLookbackMinutes(url.searchParams.get("lookback"), TRACE_DEFAULT_LOOKBACK, TRACE_MAX_LOOKBACK)
|
|
253
|
+
const params = parseListParams(request, TRACE_STATS)
|
|
254
|
+
const groupBy = params.url.searchParams.get("groupBy")
|
|
255
|
+
const agg = params.url.searchParams.get("agg")
|
|
367
256
|
if (!groupBy || (agg !== "count" && agg !== "avg_duration" && agg !== "p95_duration" && agg !== "error_rate")) {
|
|
368
257
|
return jsonResponse({ error: "Expected groupBy and agg=count|avg_duration|p95_duration|error_rate" }, 400)
|
|
369
258
|
}
|
|
370
|
-
const data = yield*
|
|
259
|
+
const data = yield* withRead((store) =>
|
|
371
260
|
store.traceStats({
|
|
372
261
|
groupBy,
|
|
373
262
|
agg,
|
|
374
|
-
serviceName: url.searchParams.get("service"),
|
|
375
|
-
operation: url.searchParams.get("operation"),
|
|
376
|
-
status: (url.searchParams.get("status") as "ok" | "error" | null) ?? null,
|
|
377
|
-
minDurationMs: url.searchParams.get("minDurationMs") ? Number.parseFloat(url.searchParams.get("minDurationMs") ?? "") : null,
|
|
378
|
-
attributeFilters,
|
|
379
|
-
limit:
|
|
380
|
-
lookbackMinutes,
|
|
263
|
+
serviceName: params.url.searchParams.get("service"),
|
|
264
|
+
operation: params.url.searchParams.get("operation"),
|
|
265
|
+
status: (params.url.searchParams.get("status") as "ok" | "error" | null) ?? null,
|
|
266
|
+
minDurationMs: params.url.searchParams.get("minDurationMs") ? Number.parseFloat(params.url.searchParams.get("minDurationMs") ?? "") : null,
|
|
267
|
+
attributeFilters: params.attributeFilters,
|
|
268
|
+
limit: params.limit,
|
|
269
|
+
lookbackMinutes: params.lookbackMinutes,
|
|
381
270
|
}),
|
|
382
271
|
)
|
|
383
272
|
return jsonResponse({ data })
|
|
@@ -385,31 +274,27 @@ const TelemetryGroupLive = HttpApiBuilder.group(
|
|
|
385
274
|
)
|
|
386
275
|
.handleRaw("searchSpans", ({ request }) =>
|
|
387
276
|
respondRaw(Effect.gen(function*() {
|
|
388
|
-
const
|
|
389
|
-
const
|
|
390
|
-
const attributeContainsFilters = attributeContainsFiltersFromQuery(url)
|
|
391
|
-
const limit = parseBoundedLimit(url.searchParams.get("limit"), SPAN_DEFAULT_LIMIT, SPAN_MAX_LIMIT)
|
|
392
|
-
const lookbackMinutes = parseBoundedLookbackMinutes(url.searchParams.get("lookback"), TRACE_DEFAULT_LOOKBACK, TRACE_MAX_LOOKBACK)
|
|
393
|
-
const data = yield* withTraceQuery((store) =>
|
|
277
|
+
const params = parseListParams(request, SPAN_LIST)
|
|
278
|
+
const data = yield* withRead((store) =>
|
|
394
279
|
store.searchSpans({
|
|
395
|
-
serviceName: url.searchParams.get("service"),
|
|
396
|
-
traceId: url.searchParams.get("traceId"),
|
|
397
|
-
operation: url.searchParams.get("operation"),
|
|
398
|
-
parentOperation: url.searchParams.get("parentOperation"),
|
|
399
|
-
status: (url.searchParams.get("status") as "ok" | "error" | null) ?? null,
|
|
400
|
-
attributeFilters,
|
|
401
|
-
attributeContainsFilters,
|
|
402
|
-
limit: limit + 1,
|
|
403
|
-
lookbackMinutes,
|
|
280
|
+
serviceName: params.url.searchParams.get("service"),
|
|
281
|
+
traceId: params.url.searchParams.get("traceId"),
|
|
282
|
+
operation: params.url.searchParams.get("operation"),
|
|
283
|
+
parentOperation: params.url.searchParams.get("parentOperation"),
|
|
284
|
+
status: (params.url.searchParams.get("status") as "ok" | "error" | null) ?? null,
|
|
285
|
+
attributeFilters: params.attributeFilters,
|
|
286
|
+
attributeContainsFilters: params.attributeContainsFilters,
|
|
287
|
+
limit: params.limit + 1,
|
|
288
|
+
lookbackMinutes: params.lookbackMinutes,
|
|
404
289
|
}),
|
|
405
290
|
)
|
|
406
|
-
const truncated = data.length > limit
|
|
407
|
-
const page = truncated ? data.slice(0, limit) : data
|
|
291
|
+
const truncated = data.length > params.limit
|
|
292
|
+
const page = truncated ? data.slice(0, params.limit) : data
|
|
408
293
|
return jsonResponse({
|
|
409
294
|
data: page,
|
|
410
295
|
meta: listMeta({
|
|
411
|
-
limit,
|
|
412
|
-
lookbackMinutes,
|
|
296
|
+
limit: params.limit,
|
|
297
|
+
lookbackMinutes: params.lookbackMinutes,
|
|
413
298
|
returned: page.length,
|
|
414
299
|
truncated,
|
|
415
300
|
nextCursor: null,
|
|
@@ -417,37 +302,31 @@ const TelemetryGroupLive = HttpApiBuilder.group(
|
|
|
417
302
|
})
|
|
418
303
|
})),
|
|
419
304
|
)
|
|
420
|
-
.handleRaw("traceLogs", ({ params, request }) =>
|
|
305
|
+
.handleRaw("traceLogs", ({ params: route, request }) =>
|
|
421
306
|
respondRaw(Effect.gen(function*() {
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
const limit = parseBoundedLimit(url.searchParams.get("limit"), LOG_DEFAULT_LIMIT, LOG_MAX_LIMIT)
|
|
425
|
-
const cursor = decodeCursor(url.searchParams.get("cursor"))
|
|
426
|
-
return jsonResponse(yield* loadLogsPage({ traceId: params.traceId, limit, lookbackMinutes, cursor }))
|
|
307
|
+
const params = parseListParams(request, LOG_LIST)
|
|
308
|
+
return jsonResponse(yield* loadLogsPage(params, { traceId: route.traceId }))
|
|
427
309
|
})),
|
|
428
310
|
)
|
|
429
311
|
.handleRaw("traceSpans", ({ params }) =>
|
|
430
|
-
respondJson(Effect.map(
|
|
312
|
+
respondJson(Effect.map(withRead((store) => store.listTraceSpans(params.traceId)), (data) => ({ data }))),
|
|
431
313
|
)
|
|
432
|
-
.handleRaw("spanLogs", ({ params, request }) =>
|
|
314
|
+
.handleRaw("spanLogs", ({ params: route, request }) =>
|
|
433
315
|
respondRaw(Effect.gen(function*() {
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
const limit = parseBoundedLimit(url.searchParams.get("limit"), LOG_DEFAULT_LIMIT, LOG_MAX_LIMIT)
|
|
437
|
-
const cursor = decodeCursor(url.searchParams.get("cursor"))
|
|
438
|
-
return jsonResponse(yield* loadLogsPage({ spanId: params.spanId, limit, lookbackMinutes, cursor }))
|
|
316
|
+
const params = parseListParams(request, LOG_LIST)
|
|
317
|
+
return jsonResponse(yield* loadLogsPage(params, { spanId: route.spanId }))
|
|
439
318
|
})),
|
|
440
319
|
)
|
|
441
320
|
.handleRaw("span", ({ params }) =>
|
|
442
321
|
respondRaw(
|
|
443
|
-
Effect.flatMap(
|
|
322
|
+
Effect.flatMap(withRead((store) => store.getSpan(params.spanId)), (data) =>
|
|
444
323
|
Effect.succeed(data ? jsonResponse({ data }) : notFoundResponse("Span not found")),
|
|
445
324
|
),
|
|
446
325
|
),
|
|
447
326
|
)
|
|
448
327
|
.handleRaw("trace", ({ params }) =>
|
|
449
328
|
respondRaw(
|
|
450
|
-
Effect.flatMap(
|
|
329
|
+
Effect.flatMap(withRead((store) => store.getTrace(params.traceId)), (data) =>
|
|
451
330
|
Effect.succeed(data ? jsonResponse({ data }) : notFoundResponse("Trace not found")),
|
|
452
331
|
),
|
|
453
332
|
),
|
|
@@ -456,25 +335,23 @@ const TelemetryGroupLive = HttpApiBuilder.group(
|
|
|
456
335
|
.handleRaw("searchLogs", ({ request }) => handleLogSearch(request))
|
|
457
336
|
.handleRaw("logStats", ({ request }) =>
|
|
458
337
|
respondRaw(Effect.gen(function*() {
|
|
459
|
-
const
|
|
460
|
-
const
|
|
461
|
-
const
|
|
462
|
-
const agg = url.searchParams.get("agg")
|
|
463
|
-
const lookbackMinutes = parseBoundedLookbackMinutes(url.searchParams.get("lookback"), LOG_DEFAULT_LOOKBACK, LOG_MAX_LOOKBACK)
|
|
338
|
+
const params = parseListParams(request, LOG_STATS)
|
|
339
|
+
const groupBy = params.url.searchParams.get("groupBy")
|
|
340
|
+
const agg = params.url.searchParams.get("agg")
|
|
464
341
|
if (!groupBy || agg !== "count") {
|
|
465
342
|
return jsonResponse({ error: "Expected groupBy and agg=count" }, 400)
|
|
466
343
|
}
|
|
467
|
-
const data = yield*
|
|
344
|
+
const data = yield* withRead((store) =>
|
|
468
345
|
store.logStats({
|
|
469
346
|
groupBy,
|
|
470
347
|
agg: "count",
|
|
471
|
-
serviceName: url.searchParams.get("service"),
|
|
472
|
-
traceId: url.searchParams.get("traceId"),
|
|
473
|
-
spanId: url.searchParams.get("spanId"),
|
|
474
|
-
body: url.searchParams.get("body"),
|
|
475
|
-
attributeFilters,
|
|
476
|
-
limit:
|
|
477
|
-
lookbackMinutes,
|
|
348
|
+
serviceName: params.url.searchParams.get("service"),
|
|
349
|
+
traceId: params.url.searchParams.get("traceId"),
|
|
350
|
+
spanId: params.url.searchParams.get("spanId"),
|
|
351
|
+
body: params.url.searchParams.get("body"),
|
|
352
|
+
attributeFilters: params.attributeFilters,
|
|
353
|
+
limit: params.limit,
|
|
354
|
+
lookbackMinutes: params.lookbackMinutes,
|
|
478
355
|
}),
|
|
479
356
|
)
|
|
480
357
|
return jsonResponse({ data })
|
|
@@ -490,18 +367,17 @@ const TelemetryGroupLive = HttpApiBuilder.group(
|
|
|
490
367
|
)
|
|
491
368
|
.handleRaw("doc", ({ params }) =>
|
|
492
369
|
respondRaw(Effect.gen(function*() {
|
|
370
|
+
const fileSystem = yield* FileSystem.FileSystem
|
|
493
371
|
const docFiles: Record<string, string> = {
|
|
494
372
|
debug: path.resolve(import.meta.dir, "../skills/motel-debug/SKILL.md"),
|
|
495
373
|
effect: path.resolve(import.meta.dir, "../skills/motel-debug/references/effect.md"),
|
|
496
374
|
}
|
|
497
375
|
const filePath = docFiles[params.name]
|
|
498
376
|
if (!filePath) return notFoundResponse(`Unknown doc: ${params.name}. Available: ${Object.keys(docFiles).join(", ")}`)
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
return notFoundResponse(`Doc file not found: ${params.name}`)
|
|
504
|
-
}
|
|
377
|
+
return yield* fileSystem.readFileString(filePath).pipe(
|
|
378
|
+
Effect.map(textResponse),
|
|
379
|
+
Effect.catch(() => Effect.succeed(notFoundResponse(`Doc file not found: ${params.name}`))),
|
|
380
|
+
)
|
|
505
381
|
})),
|
|
506
382
|
)
|
|
507
383
|
.handleRaw("facets", ({ request }) =>
|
|
@@ -512,7 +388,7 @@ const TelemetryGroupLive = HttpApiBuilder.group(
|
|
|
512
388
|
if ((type !== "traces" && type !== "logs") || !field) {
|
|
513
389
|
return jsonResponse({ error: "Expected type=traces|logs and field=<name>" }, 400)
|
|
514
390
|
}
|
|
515
|
-
const data = yield*
|
|
391
|
+
const data = yield* withRead((store) =>
|
|
516
392
|
store.listFacets({
|
|
517
393
|
type,
|
|
518
394
|
field,
|
|
@@ -527,62 +403,59 @@ const TelemetryGroupLive = HttpApiBuilder.group(
|
|
|
527
403
|
)
|
|
528
404
|
.handleRaw("aiCalls", ({ request }) =>
|
|
529
405
|
respondRaw(Effect.gen(function*() {
|
|
530
|
-
const
|
|
531
|
-
const
|
|
532
|
-
const lookbackMinutes = parseBoundedLookbackMinutes(url.searchParams.get("lookback"), TRACE_DEFAULT_LOOKBACK, TRACE_MAX_LOOKBACK)
|
|
533
|
-
const data = yield* withStore((store) =>
|
|
406
|
+
const params = parseListParams(request, AI_LIST)
|
|
407
|
+
const data = yield* withRead((store) =>
|
|
534
408
|
store.searchAiCalls({
|
|
535
|
-
service: url.searchParams.get("service"),
|
|
536
|
-
traceId: url.searchParams.get("traceId"),
|
|
537
|
-
sessionId: url.searchParams.get("sessionId"),
|
|
538
|
-
functionId: url.searchParams.get("functionId"),
|
|
539
|
-
provider: url.searchParams.get("provider"),
|
|
540
|
-
model: url.searchParams.get("model"),
|
|
541
|
-
operation: url.searchParams.get("operation"),
|
|
542
|
-
status: (url.searchParams.get("status") as "ok" | "error" | null) ?? null,
|
|
543
|
-
minDurationMs: url.searchParams.get("minDurationMs") ? Number(url.searchParams.get("minDurationMs")) : null,
|
|
544
|
-
text: url.searchParams.get("text"),
|
|
545
|
-
lookbackMinutes,
|
|
546
|
-
limit,
|
|
409
|
+
service: params.url.searchParams.get("service"),
|
|
410
|
+
traceId: params.url.searchParams.get("traceId"),
|
|
411
|
+
sessionId: params.url.searchParams.get("sessionId"),
|
|
412
|
+
functionId: params.url.searchParams.get("functionId"),
|
|
413
|
+
provider: params.url.searchParams.get("provider"),
|
|
414
|
+
model: params.url.searchParams.get("model"),
|
|
415
|
+
operation: params.url.searchParams.get("operation"),
|
|
416
|
+
status: (params.url.searchParams.get("status") as "ok" | "error" | null) ?? null,
|
|
417
|
+
minDurationMs: params.url.searchParams.get("minDurationMs") ? Number(params.url.searchParams.get("minDurationMs")) : null,
|
|
418
|
+
text: params.url.searchParams.get("text"),
|
|
419
|
+
lookbackMinutes: params.lookbackMinutes,
|
|
420
|
+
limit: params.limit,
|
|
547
421
|
}),
|
|
548
422
|
)
|
|
549
423
|
return jsonResponse({
|
|
550
424
|
data,
|
|
551
|
-
meta: listMeta({ limit, lookbackMinutes, returned: data.length, truncated: false, nextCursor: null }),
|
|
425
|
+
meta: listMeta({ limit: params.limit, lookbackMinutes: params.lookbackMinutes, returned: data.length, truncated: false, nextCursor: null }),
|
|
552
426
|
})
|
|
553
427
|
})),
|
|
554
428
|
)
|
|
555
429
|
.handleRaw("aiCall", ({ params }) =>
|
|
556
430
|
respondRaw(Effect.gen(function*() {
|
|
557
|
-
const data = yield*
|
|
431
|
+
const data = yield* withRead((store) => store.getAiCall(params.spanId))
|
|
558
432
|
if (!data) return notFoundResponse("AI call not found")
|
|
559
433
|
return jsonResponse({ data })
|
|
560
434
|
})),
|
|
561
435
|
)
|
|
562
436
|
.handleRaw("aiStats", ({ request }) =>
|
|
563
437
|
respondRaw(Effect.gen(function*() {
|
|
564
|
-
const
|
|
565
|
-
const groupBy = url.searchParams.get("groupBy") as "provider" | "model" | "functionId" | "sessionId" | "status" | null
|
|
566
|
-
const agg = url.searchParams.get("agg") as "count" | "avg_duration" | "p95_duration" | "total_input_tokens" | "total_output_tokens" | null
|
|
438
|
+
const params = parseListParams(request, AI_LIST)
|
|
439
|
+
const groupBy = params.url.searchParams.get("groupBy") as "provider" | "model" | "functionId" | "sessionId" | "status" | null
|
|
440
|
+
const agg = params.url.searchParams.get("agg") as "count" | "avg_duration" | "p95_duration" | "total_input_tokens" | "total_output_tokens" | null
|
|
567
441
|
if (!groupBy || !agg) {
|
|
568
442
|
return jsonResponse({ error: "Expected groupBy and agg parameters" }, 400)
|
|
569
443
|
}
|
|
570
|
-
const
|
|
571
|
-
const data = yield* withStore((store) =>
|
|
444
|
+
const data = yield* withRead((store) =>
|
|
572
445
|
store.aiCallStats({
|
|
573
446
|
groupBy,
|
|
574
447
|
agg,
|
|
575
|
-
service: url.searchParams.get("service"),
|
|
576
|
-
traceId: url.searchParams.get("traceId"),
|
|
577
|
-
sessionId: url.searchParams.get("sessionId"),
|
|
578
|
-
functionId: url.searchParams.get("functionId"),
|
|
579
|
-
provider: url.searchParams.get("provider"),
|
|
580
|
-
model: url.searchParams.get("model"),
|
|
581
|
-
operation: url.searchParams.get("operation"),
|
|
582
|
-
status: (url.searchParams.get("status") as "ok" | "error" | null) ?? null,
|
|
583
|
-
minDurationMs: url.searchParams.get("minDurationMs") ? Number(url.searchParams.get("minDurationMs")) : null,
|
|
584
|
-
lookbackMinutes,
|
|
585
|
-
limit:
|
|
448
|
+
service: params.url.searchParams.get("service"),
|
|
449
|
+
traceId: params.url.searchParams.get("traceId"),
|
|
450
|
+
sessionId: params.url.searchParams.get("sessionId"),
|
|
451
|
+
functionId: params.url.searchParams.get("functionId"),
|
|
452
|
+
provider: params.url.searchParams.get("provider"),
|
|
453
|
+
model: params.url.searchParams.get("model"),
|
|
454
|
+
operation: params.url.searchParams.get("operation"),
|
|
455
|
+
status: (params.url.searchParams.get("status") as "ok" | "error" | null) ?? null,
|
|
456
|
+
minDurationMs: params.url.searchParams.get("minDurationMs") ? Number(params.url.searchParams.get("minDurationMs")) : null,
|
|
457
|
+
lookbackMinutes: params.lookbackMinutes,
|
|
458
|
+
limit: params.limit,
|
|
586
459
|
}),
|
|
587
460
|
)
|
|
588
461
|
return jsonResponse({ data })
|
|
@@ -590,9 +463,9 @@ const TelemetryGroupLive = HttpApiBuilder.group(
|
|
|
590
463
|
)
|
|
591
464
|
.handleRaw("tracePage", ({ params }) =>
|
|
592
465
|
respondRaw(
|
|
593
|
-
Effect.flatMap(
|
|
466
|
+
Effect.flatMap(withRead((store) => store.getTrace(params.traceId)), (trace) =>
|
|
594
467
|
trace
|
|
595
|
-
? Effect.map(
|
|
468
|
+
? Effect.map(withRead((store) => store.listTraceLogs(params.traceId)), (logs) => htmlResponse(renderTracePage(trace, logs)))
|
|
596
469
|
: Effect.succeed(notFoundResponse("Trace not found")),
|
|
597
470
|
),
|
|
598
471
|
),
|
|
@@ -610,10 +483,6 @@ const ApiLayer = HttpApiBuilder.layer(MotelHttpApi, { openapiPath: "/openapi.jso
|
|
|
610
483
|
Layer.provide(HttpApiScalar.layer(MotelHttpApi, { scalar: { forceDarkModeState: "dark", showOperationId: true } })),
|
|
611
484
|
)
|
|
612
485
|
|
|
613
|
-
const QueryServicesLive = Layer.mergeAll(TraceQueryServiceLive, LogQueryServiceLive).pipe(
|
|
614
|
-
Layer.provideMerge(TelemetryStoreReadonlyLive),
|
|
615
|
-
)
|
|
616
|
-
|
|
617
486
|
// Web UI: Vite-built SPA served from web/dist. HttpStaticServer.layer
|
|
618
487
|
// handles GET /*, filesystem lookup under `root`, and SPA fallback to
|
|
619
488
|
// index.html for unknown paths — replacing the hand-rolled serveWebUi
|
|
@@ -643,6 +512,8 @@ const RegistryLayer = Layer.effectDiscard(
|
|
|
643
512
|
startedAt: serverStartedAt,
|
|
644
513
|
version: MOTEL_VERSION,
|
|
645
514
|
databasePath: config.otel.databasePath,
|
|
515
|
+
instanceId: process.env.MOTEL_DAEMON_INSTANCE_ID?.trim(),
|
|
516
|
+
processIdentity: processIdentity(process.pid) ?? undefined,
|
|
646
517
|
})
|
|
647
518
|
} catch (err) {
|
|
648
519
|
console.warn(`motel: failed to write registry entry: ${(err as Error).message}`)
|
|
@@ -680,18 +551,18 @@ export const ServerLive = HttpRouter.serve(
|
|
|
680
551
|
// POSTs again on the next flush. This also shaves ~1 KB of header
|
|
681
552
|
// attributes off every ingest request that would have been written
|
|
682
553
|
// to the spans table as noise.
|
|
683
|
-
Layer.provide(HttpMiddleware.layerTracerDisabledForUrls(["/v1/traces", "/v1/logs"])),
|
|
684
|
-
//
|
|
685
|
-
//
|
|
686
|
-
//
|
|
687
|
-
// resolve through readonly TraceQueryService / LogQueryService so
|
|
688
|
-
// reads do not contend with the writer connection.
|
|
554
|
+
Layer.provide(HttpMiddleware.layerTracerDisabledForUrls(["/api/health", "/v1/traces", "/v1/logs"])),
|
|
555
|
+
// The telemetry worker owns ingest, migrations, and bounded maintenance.
|
|
556
|
+
// The HTTP thread only opens an existing database read-only (or bootstraps
|
|
557
|
+
// a brand-new empty one), keeping health independent of writer work.
|
|
689
558
|
Layer.provideMerge(AsyncIngestLive),
|
|
690
|
-
Layer.provideMerge(
|
|
691
|
-
Layer.provideMerge(TelemetryStoreLive),
|
|
559
|
+
Layer.provideMerge(TelemetryQueryLive),
|
|
692
560
|
Layer.provideMerge(BunHttpServer.layer({
|
|
693
561
|
port: config.otel.port,
|
|
694
562
|
hostname: config.otel.host,
|
|
695
563
|
reusePort: true,
|
|
564
|
+
routes: {
|
|
565
|
+
"/api/health": () => Response.json(healthPayload()),
|
|
566
|
+
},
|
|
696
567
|
})),
|
|
697
568
|
)
|