@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
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,39 +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
|
|
14
|
+
import { TelemetryStoreReadonly } from "./services/TelemetryStore.js"
|
|
15
|
+
import { TelemetryQueryLive } from "./services/TelemetryQuery.js"
|
|
16
|
+
import type { LogItem, TraceItem } from "./domain.js"
|
|
17
17
|
import { lifecycleLabel } from "./ui/format.js"
|
|
18
|
+
import { decodeProtobufLogs, decodeProtobufTraces } from "./otlpProtobuf.js"
|
|
19
|
+
import type { OtlpLogExportRequest, OtlpTraceExportRequest } from "./otlp.js"
|
|
18
20
|
|
|
19
21
|
// Set by the RegistryLayer acquisition once the Bun socket has bound.
|
|
20
22
|
// Both /api/health and the registry entry read from here so they agree
|
|
21
23
|
// on a single server-start timestamp, and the value reflects actual
|
|
22
24
|
// listen time rather than module-evaluation time.
|
|
23
|
-
let serverStartedAt: string = new Date(
|
|
25
|
+
let serverStartedAt: string = new Date().toISOString()
|
|
24
26
|
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const TRACE_DEFAULT_LOOKBACK = 60
|
|
28
|
-
const TRACE_MAX_LOOKBACK = 24 * 60
|
|
29
|
-
const SPAN_DEFAULT_LIMIT = 100
|
|
30
|
-
const SPAN_MAX_LIMIT = 500
|
|
31
|
-
const LOG_DEFAULT_LIMIT = 100
|
|
32
|
-
const LOG_MAX_LIMIT = 500
|
|
33
|
-
const LOG_DEFAULT_LOOKBACK = 60
|
|
34
|
-
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)
|
|
35
29
|
|
|
36
30
|
const jsonResponse = (value: unknown, status = 200) => HttpServerResponse.jsonUnsafe(value, { status })
|
|
37
31
|
const textResponse = (value: string) => HttpServerResponse.text(value)
|
|
38
32
|
const htmlResponse = (value: string) => HttpServerResponse.html(value)
|
|
39
33
|
const notFoundResponse = (message = "Not found") => jsonResponse({ error: message }, 404)
|
|
40
|
-
const
|
|
41
|
-
|
|
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)
|
|
42
48
|
// Response-building helpers are generic in R so a handler can depend
|
|
43
|
-
// on
|
|
44
|
-
// 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.
|
|
45
51
|
const respondJson = <A, R>(effect: Effect.Effect<A, unknown, R>) =>
|
|
46
52
|
Effect.match(effect, {
|
|
47
53
|
onFailure: (error) => jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 500),
|
|
@@ -53,144 +59,50 @@ const respondRaw = <R>(effect: Effect.Effect<ReturnType<typeof jsonResponse>, un
|
|
|
53
59
|
onSuccess: (value) => value,
|
|
54
60
|
})
|
|
55
61
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
if (unit === "d") return amount * 1440
|
|
68
|
-
if (unit === "h") return amount * 60
|
|
69
|
-
return amount
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const parseBoundedLookbackMinutes = (value: string | null, fallback: number, max: number) => clamp(parseLookbackMinutes(value, fallback), 1, max)
|
|
73
|
-
|
|
74
|
-
const attributeFiltersFromQuery = (url: URL) =>
|
|
75
|
-
attributeFiltersFromEntries(url.searchParams.entries())
|
|
76
|
-
|
|
77
|
-
const attributeContainsFiltersFromQuery = (url: URL) =>
|
|
78
|
-
attributeContainsFiltersFromEntries(url.searchParams.entries())
|
|
79
|
-
|
|
80
|
-
type CursorShape =
|
|
81
|
-
| { readonly kind: "trace"; readonly startedAt: number; readonly id: string }
|
|
82
|
-
| { readonly kind: "log"; readonly timestamp: number; readonly id: string }
|
|
83
|
-
|
|
84
|
-
const encodeCursor = (cursor: CursorShape) => Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url")
|
|
85
|
-
|
|
86
|
-
const decodeCursor = (value: string | null): CursorShape | null => {
|
|
87
|
-
if (!value) return null
|
|
88
|
-
try {
|
|
89
|
-
return JSON.parse(Buffer.from(value, "base64url").toString("utf8")) as CursorShape
|
|
90
|
-
} catch {
|
|
91
|
-
return null
|
|
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)))
|
|
92
73
|
}
|
|
74
|
+
return Effect.map(request.json, (payload) => payload as T)
|
|
93
75
|
}
|
|
94
76
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
data: page,
|
|
114
|
-
meta: listMeta({
|
|
115
|
-
limit: options.limit,
|
|
116
|
-
lookbackMinutes: options.lookbackMinutes,
|
|
117
|
-
returned: page.length,
|
|
118
|
-
truncated: summaries.length > page.length,
|
|
119
|
-
nextCursor: last ? encodeCursor({ kind: "trace", startedAt: last.startedAt.getTime(), id: last.traceId }) : null,
|
|
120
|
-
}),
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const paginateLogs = (logs: readonly LogItem[], options: { readonly limit: number; readonly lookbackMinutes: number; readonly cursor: CursorShape | null }) => {
|
|
125
|
-
const page = logs.slice(0, options.limit)
|
|
126
|
-
const last = page.at(-1)
|
|
127
|
-
|
|
128
|
-
return {
|
|
129
|
-
data: page,
|
|
130
|
-
meta: listMeta({
|
|
131
|
-
limit: options.limit,
|
|
132
|
-
lookbackMinutes: options.lookbackMinutes,
|
|
133
|
-
returned: page.length,
|
|
134
|
-
truncated: logs.length > page.length,
|
|
135
|
-
nextCursor: last ? encodeCursor({ kind: "log", timestamp: last.timestamp.getTime(), id: last.id }) : null,
|
|
136
|
-
}),
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const loadLogsPage = (input: {
|
|
141
|
-
readonly serviceName?: string | null
|
|
142
|
-
readonly severity?: string | null
|
|
143
|
-
readonly traceId?: string | null
|
|
144
|
-
readonly spanId?: string | null
|
|
145
|
-
readonly body?: string | null
|
|
146
|
-
readonly attributeFilters?: Readonly<Record<string, string>>
|
|
147
|
-
readonly attributeContainsFilters?: Readonly<Record<string, string>>
|
|
148
|
-
readonly limit: number
|
|
149
|
-
readonly lookbackMinutes: number
|
|
150
|
-
readonly cursor: CursorShape | null
|
|
151
|
-
}) =>
|
|
152
|
-
Effect.flatMap(TelemetryStore.asEffect(), (store) =>
|
|
153
|
-
Effect.map(
|
|
154
|
-
store.searchLogs({
|
|
155
|
-
serviceName: input.serviceName,
|
|
156
|
-
severity: input.severity,
|
|
157
|
-
traceId: input.traceId,
|
|
158
|
-
spanId: input.spanId,
|
|
159
|
-
body: input.body,
|
|
160
|
-
lookbackMinutes: input.lookbackMinutes,
|
|
161
|
-
limit: input.limit + 1,
|
|
162
|
-
cursorTimestampMs: input.cursor?.kind === "log" ? input.cursor.timestamp : undefined,
|
|
163
|
-
cursorId: input.cursor?.kind === "log" ? input.cursor.id : undefined,
|
|
164
|
-
attributeFilters: input.attributeFilters,
|
|
165
|
-
attributeContainsFilters: input.attributeContainsFilters,
|
|
166
|
-
}),
|
|
167
|
-
(logs) => paginateLogs(logs, {
|
|
168
|
-
limit: input.limit,
|
|
169
|
-
lookbackMinutes: input.lookbackMinutes,
|
|
170
|
-
cursor: input.cursor,
|
|
171
|
-
}),
|
|
172
|
-
),
|
|
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),
|
|
173
95
|
)
|
|
174
96
|
|
|
175
97
|
const handleLogSearch = (request: { readonly url: string }) =>
|
|
176
98
|
respondRaw(Effect.gen(function*() {
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
serviceName: url.searchParams.get("service"),
|
|
185
|
-
severity: url.searchParams.get("severity"),
|
|
186
|
-
traceId: url.searchParams.get("traceId"),
|
|
187
|
-
spanId: url.searchParams.get("spanId"),
|
|
188
|
-
body: url.searchParams.get("body"),
|
|
189
|
-
attributeFilters,
|
|
190
|
-
attributeContainsFilters,
|
|
191
|
-
limit,
|
|
192
|
-
lookbackMinutes,
|
|
193
|
-
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"),
|
|
194
106
|
}))
|
|
195
107
|
}))
|
|
196
108
|
|
|
@@ -277,103 +189,84 @@ const TelemetryGroupLive = HttpApiBuilder.group(
|
|
|
277
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")),
|
|
278
190
|
)
|
|
279
191
|
.handle("health", () =>
|
|
280
|
-
Effect.succeed(
|
|
281
|
-
ok: true,
|
|
282
|
-
service: MOTEL_SERVICE_ID,
|
|
283
|
-
databasePath: config.otel.databasePath,
|
|
284
|
-
pid: process.pid,
|
|
285
|
-
url: config.otel.baseUrl,
|
|
286
|
-
workdir: process.cwd(),
|
|
287
|
-
startedAt: serverStartedAt,
|
|
288
|
-
version: MOTEL_VERSION,
|
|
289
|
-
}),
|
|
192
|
+
HttpMiddleware.withLoggerDisabled(Effect.succeed(healthPayload())),
|
|
290
193
|
)
|
|
291
194
|
// OTLP ingest is routed to the worker thread via AsyncIngest
|
|
292
195
|
// so the main event loop stays free during heavy SQLite writes.
|
|
293
|
-
//
|
|
294
|
-
//
|
|
196
|
+
// Read queries use a separate query worker so synchronous SQLite
|
|
197
|
+
// work cannot block the HTTP event loop.
|
|
295
198
|
.handleRaw("ingestTraces", ({ request }) =>
|
|
296
|
-
respondRaw(
|
|
297
|
-
Effect.flatMap(
|
|
298
|
-
|
|
299
|
-
|
|
199
|
+
HttpMiddleware.withLoggerDisabled(respondRaw(
|
|
200
|
+
Effect.flatMap(
|
|
201
|
+
readOtlpBody<OtlpTraceExportRequest>(request, decodeProtobufTraces),
|
|
202
|
+
(payload) => Effect.map(
|
|
203
|
+
Effect.flatMap(AsyncIngest, (ingest) => ingest.ingestTraces({ payload })),
|
|
300
204
|
(result) => jsonResponse(result),
|
|
301
205
|
),
|
|
302
206
|
),
|
|
303
|
-
),
|
|
207
|
+
)),
|
|
304
208
|
)
|
|
305
209
|
.handleRaw("ingestLogs", ({ request }) =>
|
|
306
|
-
respondRaw(
|
|
307
|
-
Effect.flatMap(
|
|
308
|
-
|
|
309
|
-
|
|
210
|
+
HttpMiddleware.withLoggerDisabled(respondRaw(
|
|
211
|
+
Effect.flatMap(
|
|
212
|
+
readOtlpBody<OtlpLogExportRequest>(request, decodeProtobufLogs),
|
|
213
|
+
(payload) => Effect.map(
|
|
214
|
+
Effect.flatMap(AsyncIngest, (ingest) => ingest.ingestLogs({ payload })),
|
|
310
215
|
(result) => jsonResponse(result),
|
|
311
216
|
),
|
|
312
217
|
),
|
|
313
|
-
),
|
|
218
|
+
)),
|
|
314
219
|
)
|
|
315
|
-
.handleRaw("services", () => respondJson(Effect.map(
|
|
220
|
+
.handleRaw("services", () => respondJson(Effect.map(withRead((store) => store.listServices), (data) => ({ data }))))
|
|
316
221
|
.handleRaw("traces", ({ request }) =>
|
|
317
222
|
respondRaw(Effect.gen(function*() {
|
|
318
|
-
const
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const data = yield* withStore((store) => store.listTraceSummaries(service, {
|
|
324
|
-
limit: limit + 1,
|
|
325
|
-
lookbackMinutes,
|
|
326
|
-
cursorStartedAtMs: cursor?.kind === "trace" ? cursor.startedAt : undefined,
|
|
327
|
-
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),
|
|
328
228
|
}))
|
|
329
|
-
return jsonResponse(paginateSummaries(data,
|
|
229
|
+
return jsonResponse(paginateSummaries(data, params))
|
|
330
230
|
})),
|
|
331
231
|
)
|
|
332
232
|
.handleRaw("searchTraces", ({ request }) =>
|
|
333
233
|
respondRaw(Effect.gen(function*() {
|
|
334
|
-
const
|
|
335
|
-
const
|
|
336
|
-
const limit = parseBoundedLimit(url.searchParams.get("limit"), TRACE_DEFAULT_LIMIT, TRACE_MAX_LIMIT)
|
|
337
|
-
const lookbackMinutes = parseBoundedLookbackMinutes(url.searchParams.get("lookback"), TRACE_DEFAULT_LOOKBACK, TRACE_MAX_LOOKBACK)
|
|
338
|
-
const cursor = decodeCursor(url.searchParams.get("cursor"))
|
|
339
|
-
const data = yield* withStore((store) =>
|
|
234
|
+
const params = parseListParams(request, TRACE_LIST)
|
|
235
|
+
const data = yield* withRead((store) =>
|
|
340
236
|
store.searchTraceSummaries({
|
|
341
|
-
serviceName: url.searchParams.get("service"),
|
|
342
|
-
operation: url.searchParams.get("operation"),
|
|
343
|
-
status: (url.searchParams.get("status") as "ok" | "error" | null) ?? null,
|
|
344
|
-
minDurationMs: url.searchParams.get("minDurationMs") ? Number.parseFloat(url.searchParams.get("minDurationMs") ?? "") : null,
|
|
345
|
-
attributeFilters,
|
|
346
|
-
aiText: url.searchParams.get("aiText"),
|
|
347
|
-
limit: limit + 1,
|
|
348
|
-
lookbackMinutes,
|
|
349
|
-
|
|
350
|
-
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),
|
|
351
246
|
}),
|
|
352
247
|
)
|
|
353
|
-
return jsonResponse(paginateSummaries(data,
|
|
248
|
+
return jsonResponse(paginateSummaries(data, params))
|
|
354
249
|
})),
|
|
355
250
|
)
|
|
356
251
|
.handleRaw("traceStats", ({ request }) =>
|
|
357
252
|
respondRaw(Effect.gen(function*() {
|
|
358
|
-
const
|
|
359
|
-
const
|
|
360
|
-
const
|
|
361
|
-
const agg = url.searchParams.get("agg")
|
|
362
|
-
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")
|
|
363
256
|
if (!groupBy || (agg !== "count" && agg !== "avg_duration" && agg !== "p95_duration" && agg !== "error_rate")) {
|
|
364
257
|
return jsonResponse({ error: "Expected groupBy and agg=count|avg_duration|p95_duration|error_rate" }, 400)
|
|
365
258
|
}
|
|
366
|
-
const data = yield*
|
|
259
|
+
const data = yield* withRead((store) =>
|
|
367
260
|
store.traceStats({
|
|
368
261
|
groupBy,
|
|
369
262
|
agg,
|
|
370
|
-
serviceName: url.searchParams.get("service"),
|
|
371
|
-
operation: url.searchParams.get("operation"),
|
|
372
|
-
status: (url.searchParams.get("status") as "ok" | "error" | null) ?? null,
|
|
373
|
-
minDurationMs: url.searchParams.get("minDurationMs") ? Number.parseFloat(url.searchParams.get("minDurationMs") ?? "") : null,
|
|
374
|
-
attributeFilters,
|
|
375
|
-
limit:
|
|
376
|
-
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,
|
|
377
270
|
}),
|
|
378
271
|
)
|
|
379
272
|
return jsonResponse({ data })
|
|
@@ -381,31 +274,27 @@ const TelemetryGroupLive = HttpApiBuilder.group(
|
|
|
381
274
|
)
|
|
382
275
|
.handleRaw("searchSpans", ({ request }) =>
|
|
383
276
|
respondRaw(Effect.gen(function*() {
|
|
384
|
-
const
|
|
385
|
-
const
|
|
386
|
-
const attributeContainsFilters = attributeContainsFiltersFromQuery(url)
|
|
387
|
-
const limit = parseBoundedLimit(url.searchParams.get("limit"), SPAN_DEFAULT_LIMIT, SPAN_MAX_LIMIT)
|
|
388
|
-
const lookbackMinutes = parseBoundedLookbackMinutes(url.searchParams.get("lookback"), TRACE_DEFAULT_LOOKBACK, TRACE_MAX_LOOKBACK)
|
|
389
|
-
const data = yield* withStore((store) =>
|
|
277
|
+
const params = parseListParams(request, SPAN_LIST)
|
|
278
|
+
const data = yield* withRead((store) =>
|
|
390
279
|
store.searchSpans({
|
|
391
|
-
serviceName: url.searchParams.get("service"),
|
|
392
|
-
traceId: url.searchParams.get("traceId"),
|
|
393
|
-
operation: url.searchParams.get("operation"),
|
|
394
|
-
parentOperation: url.searchParams.get("parentOperation"),
|
|
395
|
-
status: (url.searchParams.get("status") as "ok" | "error" | null) ?? null,
|
|
396
|
-
attributeFilters,
|
|
397
|
-
attributeContainsFilters,
|
|
398
|
-
limit: limit + 1,
|
|
399
|
-
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,
|
|
400
289
|
}),
|
|
401
290
|
)
|
|
402
|
-
const truncated = data.length > limit
|
|
403
|
-
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
|
|
404
293
|
return jsonResponse({
|
|
405
294
|
data: page,
|
|
406
295
|
meta: listMeta({
|
|
407
|
-
limit,
|
|
408
|
-
lookbackMinutes,
|
|
296
|
+
limit: params.limit,
|
|
297
|
+
lookbackMinutes: params.lookbackMinutes,
|
|
409
298
|
returned: page.length,
|
|
410
299
|
truncated,
|
|
411
300
|
nextCursor: null,
|
|
@@ -413,37 +302,31 @@ const TelemetryGroupLive = HttpApiBuilder.group(
|
|
|
413
302
|
})
|
|
414
303
|
})),
|
|
415
304
|
)
|
|
416
|
-
.handleRaw("traceLogs", ({ params, request }) =>
|
|
305
|
+
.handleRaw("traceLogs", ({ params: route, request }) =>
|
|
417
306
|
respondRaw(Effect.gen(function*() {
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
const limit = parseBoundedLimit(url.searchParams.get("limit"), LOG_DEFAULT_LIMIT, LOG_MAX_LIMIT)
|
|
421
|
-
const cursor = decodeCursor(url.searchParams.get("cursor"))
|
|
422
|
-
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 }))
|
|
423
309
|
})),
|
|
424
310
|
)
|
|
425
311
|
.handleRaw("traceSpans", ({ params }) =>
|
|
426
|
-
respondJson(Effect.map(
|
|
312
|
+
respondJson(Effect.map(withRead((store) => store.listTraceSpans(params.traceId)), (data) => ({ data }))),
|
|
427
313
|
)
|
|
428
|
-
.handleRaw("spanLogs", ({ params, request }) =>
|
|
314
|
+
.handleRaw("spanLogs", ({ params: route, request }) =>
|
|
429
315
|
respondRaw(Effect.gen(function*() {
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
const limit = parseBoundedLimit(url.searchParams.get("limit"), LOG_DEFAULT_LIMIT, LOG_MAX_LIMIT)
|
|
433
|
-
const cursor = decodeCursor(url.searchParams.get("cursor"))
|
|
434
|
-
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 }))
|
|
435
318
|
})),
|
|
436
319
|
)
|
|
437
320
|
.handleRaw("span", ({ params }) =>
|
|
438
321
|
respondRaw(
|
|
439
|
-
Effect.flatMap(
|
|
322
|
+
Effect.flatMap(withRead((store) => store.getSpan(params.spanId)), (data) =>
|
|
440
323
|
Effect.succeed(data ? jsonResponse({ data }) : notFoundResponse("Span not found")),
|
|
441
324
|
),
|
|
442
325
|
),
|
|
443
326
|
)
|
|
444
327
|
.handleRaw("trace", ({ params }) =>
|
|
445
328
|
respondRaw(
|
|
446
|
-
Effect.flatMap(
|
|
329
|
+
Effect.flatMap(withRead((store) => store.getTrace(params.traceId)), (data) =>
|
|
447
330
|
Effect.succeed(data ? jsonResponse({ data }) : notFoundResponse("Trace not found")),
|
|
448
331
|
),
|
|
449
332
|
),
|
|
@@ -452,25 +335,23 @@ const TelemetryGroupLive = HttpApiBuilder.group(
|
|
|
452
335
|
.handleRaw("searchLogs", ({ request }) => handleLogSearch(request))
|
|
453
336
|
.handleRaw("logStats", ({ request }) =>
|
|
454
337
|
respondRaw(Effect.gen(function*() {
|
|
455
|
-
const
|
|
456
|
-
const
|
|
457
|
-
const
|
|
458
|
-
const agg = url.searchParams.get("agg")
|
|
459
|
-
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")
|
|
460
341
|
if (!groupBy || agg !== "count") {
|
|
461
342
|
return jsonResponse({ error: "Expected groupBy and agg=count" }, 400)
|
|
462
343
|
}
|
|
463
|
-
const data = yield*
|
|
344
|
+
const data = yield* withRead((store) =>
|
|
464
345
|
store.logStats({
|
|
465
346
|
groupBy,
|
|
466
347
|
agg: "count",
|
|
467
|
-
serviceName: url.searchParams.get("service"),
|
|
468
|
-
traceId: url.searchParams.get("traceId"),
|
|
469
|
-
spanId: url.searchParams.get("spanId"),
|
|
470
|
-
body: url.searchParams.get("body"),
|
|
471
|
-
attributeFilters,
|
|
472
|
-
limit:
|
|
473
|
-
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,
|
|
474
355
|
}),
|
|
475
356
|
)
|
|
476
357
|
return jsonResponse({ data })
|
|
@@ -486,18 +367,17 @@ const TelemetryGroupLive = HttpApiBuilder.group(
|
|
|
486
367
|
)
|
|
487
368
|
.handleRaw("doc", ({ params }) =>
|
|
488
369
|
respondRaw(Effect.gen(function*() {
|
|
370
|
+
const fileSystem = yield* FileSystem.FileSystem
|
|
489
371
|
const docFiles: Record<string, string> = {
|
|
490
372
|
debug: path.resolve(import.meta.dir, "../skills/motel-debug/SKILL.md"),
|
|
491
373
|
effect: path.resolve(import.meta.dir, "../skills/motel-debug/references/effect.md"),
|
|
492
374
|
}
|
|
493
375
|
const filePath = docFiles[params.name]
|
|
494
376
|
if (!filePath) return notFoundResponse(`Unknown doc: ${params.name}. Available: ${Object.keys(docFiles).join(", ")}`)
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
return notFoundResponse(`Doc file not found: ${params.name}`)
|
|
500
|
-
}
|
|
377
|
+
return yield* fileSystem.readFileString(filePath).pipe(
|
|
378
|
+
Effect.map(textResponse),
|
|
379
|
+
Effect.catch(() => Effect.succeed(notFoundResponse(`Doc file not found: ${params.name}`))),
|
|
380
|
+
)
|
|
501
381
|
})),
|
|
502
382
|
)
|
|
503
383
|
.handleRaw("facets", ({ request }) =>
|
|
@@ -508,7 +388,7 @@ const TelemetryGroupLive = HttpApiBuilder.group(
|
|
|
508
388
|
if ((type !== "traces" && type !== "logs") || !field) {
|
|
509
389
|
return jsonResponse({ error: "Expected type=traces|logs and field=<name>" }, 400)
|
|
510
390
|
}
|
|
511
|
-
const data = yield*
|
|
391
|
+
const data = yield* withRead((store) =>
|
|
512
392
|
store.listFacets({
|
|
513
393
|
type,
|
|
514
394
|
field,
|
|
@@ -523,62 +403,59 @@ const TelemetryGroupLive = HttpApiBuilder.group(
|
|
|
523
403
|
)
|
|
524
404
|
.handleRaw("aiCalls", ({ request }) =>
|
|
525
405
|
respondRaw(Effect.gen(function*() {
|
|
526
|
-
const
|
|
527
|
-
const
|
|
528
|
-
const lookbackMinutes = parseBoundedLookbackMinutes(url.searchParams.get("lookback"), TRACE_DEFAULT_LOOKBACK, TRACE_MAX_LOOKBACK)
|
|
529
|
-
const data = yield* withStore((store) =>
|
|
406
|
+
const params = parseListParams(request, AI_LIST)
|
|
407
|
+
const data = yield* withRead((store) =>
|
|
530
408
|
store.searchAiCalls({
|
|
531
|
-
service: url.searchParams.get("service"),
|
|
532
|
-
traceId: url.searchParams.get("traceId"),
|
|
533
|
-
sessionId: url.searchParams.get("sessionId"),
|
|
534
|
-
functionId: url.searchParams.get("functionId"),
|
|
535
|
-
provider: url.searchParams.get("provider"),
|
|
536
|
-
model: url.searchParams.get("model"),
|
|
537
|
-
operation: url.searchParams.get("operation"),
|
|
538
|
-
status: (url.searchParams.get("status") as "ok" | "error" | null) ?? null,
|
|
539
|
-
minDurationMs: url.searchParams.get("minDurationMs") ? Number(url.searchParams.get("minDurationMs")) : null,
|
|
540
|
-
text: url.searchParams.get("text"),
|
|
541
|
-
lookbackMinutes,
|
|
542
|
-
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,
|
|
543
421
|
}),
|
|
544
422
|
)
|
|
545
423
|
return jsonResponse({
|
|
546
424
|
data,
|
|
547
|
-
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 }),
|
|
548
426
|
})
|
|
549
427
|
})),
|
|
550
428
|
)
|
|
551
429
|
.handleRaw("aiCall", ({ params }) =>
|
|
552
430
|
respondRaw(Effect.gen(function*() {
|
|
553
|
-
const data = yield*
|
|
431
|
+
const data = yield* withRead((store) => store.getAiCall(params.spanId))
|
|
554
432
|
if (!data) return notFoundResponse("AI call not found")
|
|
555
433
|
return jsonResponse({ data })
|
|
556
434
|
})),
|
|
557
435
|
)
|
|
558
436
|
.handleRaw("aiStats", ({ request }) =>
|
|
559
437
|
respondRaw(Effect.gen(function*() {
|
|
560
|
-
const
|
|
561
|
-
const groupBy = url.searchParams.get("groupBy") as "provider" | "model" | "functionId" | "sessionId" | "status" | null
|
|
562
|
-
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
|
|
563
441
|
if (!groupBy || !agg) {
|
|
564
442
|
return jsonResponse({ error: "Expected groupBy and agg parameters" }, 400)
|
|
565
443
|
}
|
|
566
|
-
const
|
|
567
|
-
const data = yield* withStore((store) =>
|
|
444
|
+
const data = yield* withRead((store) =>
|
|
568
445
|
store.aiCallStats({
|
|
569
446
|
groupBy,
|
|
570
447
|
agg,
|
|
571
|
-
service: url.searchParams.get("service"),
|
|
572
|
-
traceId: url.searchParams.get("traceId"),
|
|
573
|
-
sessionId: url.searchParams.get("sessionId"),
|
|
574
|
-
functionId: url.searchParams.get("functionId"),
|
|
575
|
-
provider: url.searchParams.get("provider"),
|
|
576
|
-
model: url.searchParams.get("model"),
|
|
577
|
-
operation: url.searchParams.get("operation"),
|
|
578
|
-
status: (url.searchParams.get("status") as "ok" | "error" | null) ?? null,
|
|
579
|
-
minDurationMs: url.searchParams.get("minDurationMs") ? Number(url.searchParams.get("minDurationMs")) : null,
|
|
580
|
-
lookbackMinutes,
|
|
581
|
-
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,
|
|
582
459
|
}),
|
|
583
460
|
)
|
|
584
461
|
return jsonResponse({ data })
|
|
@@ -586,9 +463,9 @@ const TelemetryGroupLive = HttpApiBuilder.group(
|
|
|
586
463
|
)
|
|
587
464
|
.handleRaw("tracePage", ({ params }) =>
|
|
588
465
|
respondRaw(
|
|
589
|
-
Effect.flatMap(
|
|
466
|
+
Effect.flatMap(withRead((store) => store.getTrace(params.traceId)), (trace) =>
|
|
590
467
|
trace
|
|
591
|
-
? Effect.map(
|
|
468
|
+
? Effect.map(withRead((store) => store.listTraceLogs(params.traceId)), (logs) => htmlResponse(renderTracePage(trace, logs)))
|
|
592
469
|
: Effect.succeed(notFoundResponse("Trace not found")),
|
|
593
470
|
),
|
|
594
471
|
),
|
|
@@ -635,6 +512,8 @@ const RegistryLayer = Layer.effectDiscard(
|
|
|
635
512
|
startedAt: serverStartedAt,
|
|
636
513
|
version: MOTEL_VERSION,
|
|
637
514
|
databasePath: config.otel.databasePath,
|
|
515
|
+
instanceId: process.env.MOTEL_DAEMON_INSTANCE_ID?.trim(),
|
|
516
|
+
processIdentity: processIdentity(process.pid) ?? undefined,
|
|
638
517
|
})
|
|
639
518
|
} catch (err) {
|
|
640
519
|
console.warn(`motel: failed to write registry entry: ${(err as Error).message}`)
|
|
@@ -672,16 +551,18 @@ export const ServerLive = HttpRouter.serve(
|
|
|
672
551
|
// POSTs again on the next flush. This also shaves ~1 KB of header
|
|
673
552
|
// attributes off every ingest request that would have been written
|
|
674
553
|
// to the spans table as noise.
|
|
675
|
-
Layer.provide(HttpMiddleware.layerTracerDisabledForUrls(["/v1/traces", "/v1/logs"])),
|
|
676
|
-
//
|
|
677
|
-
//
|
|
678
|
-
//
|
|
679
|
-
// their dependency directly.
|
|
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.
|
|
680
558
|
Layer.provideMerge(AsyncIngestLive),
|
|
681
|
-
Layer.provideMerge(
|
|
559
|
+
Layer.provideMerge(TelemetryQueryLive),
|
|
682
560
|
Layer.provideMerge(BunHttpServer.layer({
|
|
683
561
|
port: config.otel.port,
|
|
684
562
|
hostname: config.otel.host,
|
|
685
563
|
reusePort: true,
|
|
564
|
+
routes: {
|
|
565
|
+
"/api/health": () => Response.json(healthPayload()),
|
|
566
|
+
},
|
|
686
567
|
})),
|
|
687
568
|
)
|