@aryasaatvik/motel 0.3.1 → 0.3.3
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/package.json +6 -5
- package/src/daemon.test.ts +29 -0
- package/src/launchAgent.test.ts +7 -3
- package/src/launchAgent.ts +33 -4
- package/src/mcp.ts +7 -1
- package/src/motel.test.ts +1 -1
- package/src/motel.ts +3 -1
- package/src/services/AsyncIngest.ts +11 -13
- package/src/services/TelemetryQuery.ts +7 -11
- package/src/services/TelemetryStore.ts +5 -7
- package/src/services/ingestRpc.ts +1 -1
- package/src/services/queryRpc.ts +1 -1
- package/web/dist/assets/index-DesEgJu7.js +21 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/index-CYlBaY9O.js +0 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aryasaatvik/motel",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "A local OpenTelemetry ingest + TUI viewer for development, backed by SQLite.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -98,9 +98,10 @@
|
|
|
98
98
|
"typescript": "^6.0.3"
|
|
99
99
|
},
|
|
100
100
|
"dependencies": {
|
|
101
|
-
"@effect/atom-react": "4.0.0-
|
|
102
|
-
"@effect/opentelemetry": "4.0.0-
|
|
103
|
-
"@effect/platform-bun": "4.0.0-
|
|
101
|
+
"@effect/atom-react": "4.0.0-rc.110",
|
|
102
|
+
"@effect/opentelemetry": "4.0.0-rc.110",
|
|
103
|
+
"@effect/platform-bun": "4.0.0-rc.110",
|
|
104
|
+
"@effect/platform-node-shared": "4.0.0-rc.110",
|
|
104
105
|
"@opentelemetry/api": "^1.9.0",
|
|
105
106
|
"@opentelemetry/exporter-logs-otlp-http": "^0.219.0",
|
|
106
107
|
"@opentelemetry/exporter-trace-otlp-http": "^0.219.0",
|
|
@@ -111,7 +112,7 @@
|
|
|
111
112
|
"@opentelemetry/sdk-trace-node": "^2.8.0",
|
|
112
113
|
"@opentui/core": "0.4.2",
|
|
113
114
|
"@opentui/react": "0.4.2",
|
|
114
|
-
"effect": "4.0.0-
|
|
115
|
+
"effect": "4.0.0-rc.110",
|
|
115
116
|
"protobufjs": "7.6.4",
|
|
116
117
|
"react": "^19.2.7",
|
|
117
118
|
"scheduler": "^0.27.0"
|
package/src/daemon.test.ts
CHANGED
|
@@ -226,6 +226,35 @@ describe("daemon manager", () => {
|
|
|
226
226
|
expect(finalStatus.running).toBe(false)
|
|
227
227
|
})
|
|
228
228
|
|
|
229
|
+
test("keeps one ingest generation usable after concurrent client aborts", async () => {
|
|
230
|
+
const harness = makeHarness()
|
|
231
|
+
activeHarnesses.push(harness)
|
|
232
|
+
const started = await Effect.runPromise(harness.manager.ensure)
|
|
233
|
+
|
|
234
|
+
await Promise.all(Array.from({ length: 50 }, async () => {
|
|
235
|
+
const controller = new AbortController()
|
|
236
|
+
const request = fetch(`http://127.0.0.1:${harness.port}/v1/traces`, {
|
|
237
|
+
method: "POST",
|
|
238
|
+
headers: { "content-type": "application/json" },
|
|
239
|
+
body: "{}",
|
|
240
|
+
signal: controller.signal,
|
|
241
|
+
}).catch(() => undefined)
|
|
242
|
+
controller.abort()
|
|
243
|
+
await request
|
|
244
|
+
}))
|
|
245
|
+
|
|
246
|
+
for (const path of ["/v1/traces", "/v1/logs"]) {
|
|
247
|
+
const response = await fetch(`http://127.0.0.1:${harness.port}${path}`, {
|
|
248
|
+
method: "POST",
|
|
249
|
+
headers: { "content-type": "application/json" },
|
|
250
|
+
body: "{}",
|
|
251
|
+
})
|
|
252
|
+
expect(response.ok).toBe(true)
|
|
253
|
+
}
|
|
254
|
+
const status = await Effect.runPromise(harness.manager.ensure)
|
|
255
|
+
expect(status.pid).toBe(started.pid)
|
|
256
|
+
})
|
|
257
|
+
|
|
229
258
|
test("health responds while ingest readiness waits for a write lock", async () => {
|
|
230
259
|
const harness = makeHarness()
|
|
231
260
|
activeHarnesses.push(harness)
|
package/src/launchAgent.test.ts
CHANGED
|
@@ -45,12 +45,13 @@ const daemonStatus = (overrides: Partial<DaemonStatus> = {}): DaemonStatus => ({
|
|
|
45
45
|
...overrides,
|
|
46
46
|
})
|
|
47
47
|
|
|
48
|
-
const makeHarness = (options: { readonly installed?: boolean; readonly missingExecutables?: readonly string[]; readonly plist?: Record<string, unknown>; readonly plutilExitCode?: number; readonly launchctlPrintExitCode?: number; readonly launchctlPrintStderr?: string; readonly bootoutExitCode?: number; readonly health?: DaemonStatus } = {}) => {
|
|
48
|
+
const makeHarness = (options: { readonly installed?: boolean; readonly missingExecutables?: readonly string[]; readonly plist?: Record<string, unknown>; readonly plutilExitCode?: number; readonly launchctlPrintExitCode?: number; readonly launchctlPrintStderr?: string; readonly bootoutExitCode?: number; readonly health?: DaemonStatus; readonly readyAfter?: number } = {}) => {
|
|
49
49
|
let installed = options.installed ?? false
|
|
50
50
|
let plist = options.plist ?? plistValue()
|
|
51
51
|
let launchctlPrintExitCode = options.launchctlPrintExitCode ?? 113
|
|
52
52
|
const calls: Array<readonly string[]> = []
|
|
53
53
|
const fileEvents: string[] = []
|
|
54
|
+
let probeCalls = 0
|
|
54
55
|
const files = new Map<string, string>()
|
|
55
56
|
const operations: LaunchAgentOperations = {
|
|
56
57
|
platform: "darwin",
|
|
@@ -73,9 +74,10 @@ const makeHarness = (options: { readonly installed?: boolean; readonly missingEx
|
|
|
73
74
|
return { exitCode: 0, stdout: "", stderr: "" }
|
|
74
75
|
},
|
|
75
76
|
getDaemonStatus: async () => options.health ?? daemonStatus(),
|
|
77
|
+
probeDaemonIngest: async () => ++probeCalls > (options.readyAfter ?? 0),
|
|
76
78
|
version: "0.2.6",
|
|
77
79
|
}
|
|
78
|
-
return { calls, fileEvents, manager: createLaunchAgentManager(spec, operations) }
|
|
80
|
+
return { calls, fileEvents, get probeCalls() { return probeCalls }, manager: createLaunchAgentManager(spec, operations) }
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
describe("LaunchAgent specification", () => {
|
|
@@ -211,8 +213,9 @@ describe("LaunchAgent lifecycle", () => {
|
|
|
211
213
|
})
|
|
212
214
|
|
|
213
215
|
test("bootstraps an unloaded service before restarting it", async () => {
|
|
214
|
-
const harness = makeHarness({ installed: true, launchctlPrintExitCode: 113 })
|
|
216
|
+
const harness = makeHarness({ installed: true, launchctlPrintExitCode: 113, readyAfter: 2 })
|
|
215
217
|
await Effect.runPromise(harness.manager.restart)
|
|
218
|
+
expect(harness.probeCalls).toBe(3)
|
|
216
219
|
expect(harness.calls.filter(([command]) => command === "launchctl")).toEqual([
|
|
217
220
|
["launchctl", "print", spec.target],
|
|
218
221
|
["launchctl", "bootstrap", spec.domain, spec.plistPath],
|
|
@@ -276,6 +279,7 @@ describe("LaunchAgent lifecycle", () => {
|
|
|
276
279
|
return { exitCode: 0, stdout: "", stderr: "" }
|
|
277
280
|
},
|
|
278
281
|
getDaemonStatus: async () => daemonStatus(),
|
|
282
|
+
probeDaemonIngest: async () => true,
|
|
279
283
|
version: "0.2.6",
|
|
280
284
|
} satisfies LaunchAgentOperations),
|
|
281
285
|
})
|
package/src/launchAgent.ts
CHANGED
|
@@ -50,6 +50,7 @@ export type LaunchAgentOperations = {
|
|
|
50
50
|
readonly unlink: (file: string) => Promise<void>
|
|
51
51
|
readonly run: (command: string, args: readonly string[]) => Promise<CommandResult>
|
|
52
52
|
readonly getDaemonStatus: () => Promise<DaemonStatus>
|
|
53
|
+
readonly probeDaemonIngest: () => Promise<boolean>
|
|
53
54
|
readonly version: string
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -206,6 +207,20 @@ export const compareLaunchAgent = (inspection: LaunchAgentInspection, spec: Laun
|
|
|
206
207
|
|
|
207
208
|
const defaultOperations = (): LaunchAgentOperations => {
|
|
208
209
|
const daemon = createDaemonManager()
|
|
210
|
+
const probeDaemonIngest = async () => {
|
|
211
|
+
try {
|
|
212
|
+
const post = (path: string) => fetch(`http://127.0.0.1:27686${path}`, {
|
|
213
|
+
method: "POST",
|
|
214
|
+
headers: { "content-type": "application/json" },
|
|
215
|
+
body: "{}",
|
|
216
|
+
signal: AbortSignal.timeout(3_000),
|
|
217
|
+
})
|
|
218
|
+
const [traces, logs] = await Promise.all([post("/v1/traces"), post("/v1/logs")])
|
|
219
|
+
return traces.ok && logs.ok
|
|
220
|
+
} catch {
|
|
221
|
+
return false
|
|
222
|
+
}
|
|
223
|
+
}
|
|
209
224
|
return {
|
|
210
225
|
exists: async (file) => fs.access(file).then(() => true).catch(() => false),
|
|
211
226
|
readFile: (file) => fs.readFile(file, "utf8"),
|
|
@@ -219,6 +234,7 @@ const defaultOperations = (): LaunchAgentOperations => {
|
|
|
219
234
|
return { exitCode, stdout, stderr }
|
|
220
235
|
},
|
|
221
236
|
getDaemonStatus: () => Effect.runPromise(daemon.getStatus),
|
|
237
|
+
probeDaemonIngest,
|
|
222
238
|
version: MOTEL_VERSION,
|
|
223
239
|
platform: process.platform,
|
|
224
240
|
}
|
|
@@ -299,6 +315,19 @@ export const createLaunchAgentManager = (spec = buildLaunchAgentSpec(), supplied
|
|
|
299
315
|
}
|
|
300
316
|
}
|
|
301
317
|
}
|
|
318
|
+
const waitForReady = async () => {
|
|
319
|
+
const deadline = Date.now() + 30_000
|
|
320
|
+
while (Date.now() < deadline) {
|
|
321
|
+
const health = await operations.getDaemonStatus()
|
|
322
|
+
if (health.running && health.managed && await operations.probeDaemonIngest()) return health
|
|
323
|
+
await Bun.sleep(100)
|
|
324
|
+
}
|
|
325
|
+
throw new LaunchAgentError("Motel LaunchAgent did not become ingest-ready within 30 seconds.")
|
|
326
|
+
}
|
|
327
|
+
const readyStatus = async () => {
|
|
328
|
+
await waitForReady()
|
|
329
|
+
return Effect.runPromise(status)
|
|
330
|
+
}
|
|
302
331
|
const inspect = Effect.tryPromise({ try: () => readInspection(operations, spec), catch: (error) => error instanceof LaunchAgentError ? error : new LaunchAgentError(error instanceof Error ? error.message : String(error)) })
|
|
303
332
|
const status = Effect.tryPromise({
|
|
304
333
|
try: async (): Promise<LaunchAgentStatus> => {
|
|
@@ -332,7 +361,7 @@ export const createLaunchAgentManager = (spec = buildLaunchAgentSpec(), supplied
|
|
|
332
361
|
await required(operations, "launchctl", ["bootstrap", spec.domain, spec.plistPath])
|
|
333
362
|
await required(operations, "launchctl", ["enable", spec.target])
|
|
334
363
|
await required(operations, "launchctl", ["kickstart", "-k", spec.target])
|
|
335
|
-
return
|
|
364
|
+
return readyStatus()
|
|
336
365
|
}
|
|
337
366
|
await requireExecutables()
|
|
338
367
|
if (manager === "loaded" && !replace) throw new LaunchAgentError("Motel has a loaded LaunchAgent job that does not match the requested install state. Re-run with --replace to boot it out before installing.")
|
|
@@ -342,7 +371,7 @@ export const createLaunchAgentManager = (spec = buildLaunchAgentSpec(), supplied
|
|
|
342
371
|
await required(operations, "launchctl", ["bootstrap", spec.domain, spec.plistPath])
|
|
343
372
|
await required(operations, "launchctl", ["enable", spec.target])
|
|
344
373
|
await required(operations, "launchctl", ["kickstart", "-k", spec.target])
|
|
345
|
-
return
|
|
374
|
+
return readyStatus()
|
|
346
375
|
},
|
|
347
376
|
catch: (error) => error instanceof LaunchAgentError ? error : new LaunchAgentError(error instanceof Error ? error.message : String(error)),
|
|
348
377
|
})
|
|
@@ -353,7 +382,7 @@ export const createLaunchAgentManager = (spec = buildLaunchAgentSpec(), supplied
|
|
|
353
382
|
const manager = requireKnownLaunchctlState(launchctlState(await optional(operations, "launchctl", ["print", spec.target])))
|
|
354
383
|
if (manager === "not-loaded") await required(operations, "launchctl", ["bootstrap", spec.domain, spec.plistPath])
|
|
355
384
|
await required(operations, "launchctl", ["kickstart", "-k", spec.target])
|
|
356
|
-
return
|
|
385
|
+
return readyStatus()
|
|
357
386
|
},
|
|
358
387
|
catch: (error) => error instanceof LaunchAgentError ? error : new LaunchAgentError(error instanceof Error ? error.message : String(error)),
|
|
359
388
|
})
|
|
@@ -374,7 +403,7 @@ export const createLaunchAgentManager = (spec = buildLaunchAgentSpec(), supplied
|
|
|
374
403
|
const manager = requireKnownLaunchctlState(launchctlState(await optional(operations, "launchctl", ["print", spec.target])))
|
|
375
404
|
if (manager === "not-loaded") await required(operations, "launchctl", ["bootstrap", spec.domain, spec.plistPath])
|
|
376
405
|
await required(operations, "launchctl", ["kickstart", "-k", spec.target])
|
|
377
|
-
return
|
|
406
|
+
return readyStatus()
|
|
378
407
|
},
|
|
379
408
|
catch: (error) => error instanceof LaunchAgentError ? error : new LaunchAgentError(error instanceof Error ? error.message : String(error)),
|
|
380
409
|
})
|
package/src/mcp.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { BunRuntime, BunStdio } from "@effect/platform-bun"
|
|
3
3
|
import { Effect, Layer, Logger, Schema } from "effect"
|
|
4
4
|
import { MOTEL_VERSION } from "./registry.js"
|
|
5
|
-
import { McpServer, Tool, Toolkit } from "effect/unstable/ai"
|
|
5
|
+
import { McpProtocol, McpServer, Tool, Toolkit } from "effect/unstable/ai"
|
|
6
6
|
import { TraceSpanStatus } from "./domain.js"
|
|
7
7
|
import { MotelClient, MotelClientLive } from "./motelClient.js"
|
|
8
8
|
import { Locator, LocatorLive } from "./locator.js"
|
|
@@ -424,6 +424,12 @@ const ServerLayer = McpServer.toolkit(MotelToolkit).pipe(
|
|
|
424
424
|
McpServer.layerStdio({
|
|
425
425
|
name: "motel",
|
|
426
426
|
version: MOTEL_VERSION,
|
|
427
|
+
protocols: [
|
|
428
|
+
McpProtocol.v2025_11_25,
|
|
429
|
+
McpProtocol.v2025_06_18,
|
|
430
|
+
McpProtocol.v2025_03_26,
|
|
431
|
+
McpProtocol.v2024_11_05,
|
|
432
|
+
],
|
|
427
433
|
}),
|
|
428
434
|
),
|
|
429
435
|
Layer.provide(BunStdio.layer),
|
package/src/motel.test.ts
CHANGED
package/src/motel.ts
CHANGED
|
@@ -39,7 +39,9 @@ const restart = Command.make("restart", {}, () =>
|
|
|
39
39
|
).pipe(Command.withDescription("Restart only the managed telemetry daemon"))
|
|
40
40
|
|
|
41
41
|
const serviceManager = createLaunchAgentManager()
|
|
42
|
-
const serviceInstall = Command.make("install", {
|
|
42
|
+
const serviceInstall = Command.make("install", {
|
|
43
|
+
replace: Flag.boolean("replace").pipe(Flag.withDefault(false)),
|
|
44
|
+
}, ({ replace }) =>
|
|
43
45
|
serviceManager.install(replace).pipe(Effect.andThen(json)),
|
|
44
46
|
).pipe(Command.withDescription("Install the per-user Motel LaunchAgent"))
|
|
45
47
|
const serviceStatus = Command.make("status", {}, () =>
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import * as BunWorker from "@effect/platform-bun/BunWorker"
|
|
23
|
-
import { Context,
|
|
23
|
+
import { Context, Effect, Layer, Scope } from "effect"
|
|
24
24
|
import * as RpcClient from "effect/unstable/rpc/RpcClient"
|
|
25
25
|
import type { RpcClientError } from "effect/unstable/rpc/RpcClientError"
|
|
26
26
|
import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"
|
|
@@ -55,28 +55,26 @@ export const AsyncIngestLive = Layer.effect(
|
|
|
55
55
|
// spawn the worker and make /api/health wait on the worker's SQLite
|
|
56
56
|
// bootstrap. Cache a lazy initializer instead so the worker only starts
|
|
57
57
|
// on the first ingest request, but is still shared thereafter.
|
|
58
|
-
|
|
58
|
+
// The server scope owns exactly one lazy writer generation. Request
|
|
59
|
+
// failures must not replace it: concurrent stale failures can otherwise
|
|
60
|
+
// invalidate a newer cached generation and orphan its SQLite writer and
|
|
61
|
+
// maintenance fibers. A poisoned worker stays failed until the managed
|
|
62
|
+
// daemon is restarted, which the external ingest readiness probe handles.
|
|
63
|
+
const getClient = yield* Effect.cached(Effect.gen(function*() {
|
|
59
64
|
const clientScope = yield* Scope.fork(scope, "sequential")
|
|
60
65
|
const protocolContext = yield* Layer.buildWithScope(WorkerProtocol, clientScope)
|
|
61
|
-
|
|
66
|
+
return yield* RpcClient.make(IngestRpcs).pipe(
|
|
62
67
|
Effect.provide(protocolContext),
|
|
63
68
|
Effect.provideService(Scope.Scope, clientScope),
|
|
64
69
|
)
|
|
65
|
-
|
|
66
|
-
}), Duration.infinity)
|
|
70
|
+
}))
|
|
67
71
|
// Start the sole writer/maintenance worker immediately, but do not make
|
|
68
72
|
// HTTP health wait for SQLite bootstrap. Managed readiness still verifies
|
|
69
73
|
// the worker through explicit ingest probes.
|
|
70
74
|
yield* Effect.forkScoped(getClient.pipe(Effect.ignore))
|
|
71
75
|
return {
|
|
72
|
-
ingestTraces: (input, options) =>
|
|
73
|
-
|
|
74
|
-
Effect.onError(() => Effect.andThen(Scope.close(clientScope, Exit.void), invalidateClient)),
|
|
75
|
-
)),
|
|
76
|
-
ingestLogs: (input, options) =>
|
|
77
|
-
Effect.flatMap(getClient, ({ client, clientScope }) => client.ingestLogs(input, options).pipe(
|
|
78
|
-
Effect.onError(() => Effect.andThen(Scope.close(clientScope, Exit.void), invalidateClient)),
|
|
79
|
-
)),
|
|
76
|
+
ingestTraces: (input, options) => Effect.flatMap(getClient, (client) => client.ingestTraces(input, options)),
|
|
77
|
+
ingestLogs: (input, options) => Effect.flatMap(getClient, (client) => client.ingestLogs(input, options)),
|
|
80
78
|
}
|
|
81
79
|
}),
|
|
82
80
|
)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as BunWorker from "@effect/platform-bun/BunWorker"
|
|
2
|
-
import {
|
|
2
|
+
import { Effect, Layer, Scope } from "effect"
|
|
3
3
|
import * as RpcClient from "effect/unstable/rpc/RpcClient"
|
|
4
4
|
import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"
|
|
5
5
|
import type { WorkerError } from "effect/unstable/workers/WorkerError"
|
|
@@ -9,17 +9,14 @@ import { QueryRpcs } from "./queryRpc.js"
|
|
|
9
9
|
|
|
10
10
|
type QueryMethod = keyof TelemetryStoreReader
|
|
11
11
|
type QueryClient = RpcClient.FromGroup<typeof QueryRpcs, RpcClientError | WorkerError>
|
|
12
|
-
type QueryClientEntry = { readonly client: QueryClient; readonly clientScope: Scope.Scope }
|
|
13
12
|
|
|
14
13
|
const WorkerProtocol = RpcClient.layerProtocolWorker({ size: 1 }).pipe(
|
|
15
14
|
Layer.provide(RpcSerialization.layerMsgPack),
|
|
16
15
|
Layer.provide(BunWorker.layer(() => new Worker(new URL("./telemetryQueryWorker.ts", import.meta.url)))),
|
|
17
16
|
)
|
|
18
17
|
|
|
19
|
-
const query = <A>(getClient: Effect.Effect<
|
|
20
|
-
Effect.flatMap(getClient, (
|
|
21
|
-
Effect.onError(() => Effect.andThen(Scope.close(clientScope, Exit.void), invalidateClient)),
|
|
22
|
-
)).pipe(
|
|
18
|
+
const query = <A>(getClient: Effect.Effect<QueryClient, unknown>, method: QueryMethod, args: readonly unknown[] = []) =>
|
|
19
|
+
Effect.flatMap(getClient, (client) => client.query({ method, args })).pipe(
|
|
23
20
|
Effect.map((result) => result as A),
|
|
24
21
|
Effect.mapError((error) => error instanceof Error ? error : new Error(String(error))),
|
|
25
22
|
)
|
|
@@ -28,16 +25,15 @@ export const TelemetryQueryLive = Layer.effect(
|
|
|
28
25
|
TelemetryStoreReadonly,
|
|
29
26
|
Effect.gen(function*() {
|
|
30
27
|
const scope = yield* Scope.Scope
|
|
31
|
-
const
|
|
28
|
+
const getClient = yield* Effect.cached(Effect.gen(function*() {
|
|
32
29
|
const clientScope = yield* Scope.fork(scope, "sequential")
|
|
33
30
|
const protocolContext = yield* Layer.buildWithScope(WorkerProtocol, clientScope)
|
|
34
|
-
|
|
31
|
+
return yield* RpcClient.make(QueryRpcs).pipe(
|
|
35
32
|
Effect.provide(protocolContext),
|
|
36
33
|
Effect.provideService(Scope.Scope, clientScope),
|
|
37
34
|
)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const run = <A>(method: QueryMethod, args: readonly unknown[] = []) => query<A>(getClient, invalidateClient, method, args)
|
|
35
|
+
}))
|
|
36
|
+
const run = <A>(method: QueryMethod, args: readonly unknown[] = []) => query<A>(getClient, method, args)
|
|
41
37
|
return TelemetryStoreReadonly.of({
|
|
42
38
|
listServices: run("listServices"),
|
|
43
39
|
listRecentTraces: (serviceName, options) => run("listRecentTraces", [serviceName, options]),
|
|
@@ -539,6 +539,9 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
539
539
|
`)
|
|
540
540
|
} else {
|
|
541
541
|
db.exec(`
|
|
542
|
+
-- Wait for an existing writer to release startup locks before any
|
|
543
|
+
-- pragma that may need to update the database header.
|
|
544
|
+
PRAGMA busy_timeout = 15000;
|
|
542
545
|
-- Bump cache above the 2MB default. 64MB fits most hot index pages
|
|
543
546
|
-- (trace_summaries, spans, span_attributes indexes) in RAM even on
|
|
544
547
|
-- multi-GB databases, cutting cold-read latency meaningfully on
|
|
@@ -557,8 +560,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
557
560
|
// existing DBs that predate this setting keep their current mode;
|
|
558
561
|
// Motel never performs a surprise full-file VACUUM at startup.
|
|
559
562
|
try { db.exec(`PRAGMA auto_vacuum = INCREMENTAL;`) } catch { /* ignore */ }
|
|
560
|
-
|
|
561
|
-
db.exec(`
|
|
563
|
+
db.exec(`
|
|
562
564
|
PRAGMA journal_mode = WAL;
|
|
563
565
|
PRAGMA synchronous = NORMAL;
|
|
564
566
|
PRAGMA temp_store = MEMORY;
|
|
@@ -656,11 +658,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
656
658
|
key TEXT PRIMARY KEY,
|
|
657
659
|
value TEXT NOT NULL
|
|
658
660
|
);
|
|
659
|
-
|
|
660
|
-
} catch (err) {
|
|
661
|
-
if (!isSqliteLockError(err)) throw err
|
|
662
|
-
console.warn(`motel: writer bootstrap skipped during startup: ${(err as Error).message}`)
|
|
663
|
-
}
|
|
661
|
+
`)
|
|
664
662
|
}
|
|
665
663
|
|
|
666
664
|
// Tables detected at runtime. For writer connections these flags are
|
|
@@ -21,7 +21,7 @@ import { Schema } from "effect"
|
|
|
21
21
|
import * as Rpc from "effect/unstable/rpc/Rpc"
|
|
22
22
|
import * as RpcGroup from "effect/unstable/rpc/RpcGroup"
|
|
23
23
|
|
|
24
|
-
export class IngestError extends Schema.
|
|
24
|
+
export class IngestError extends Schema.TaggedError<IngestError>()("IngestError", {
|
|
25
25
|
message: Schema.String,
|
|
26
26
|
}) {}
|
|
27
27
|
|
package/src/services/queryRpc.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Schema } from "effect"
|
|
|
2
2
|
import * as Rpc from "effect/unstable/rpc/Rpc"
|
|
3
3
|
import * as RpcGroup from "effect/unstable/rpc/RpcGroup"
|
|
4
4
|
|
|
5
|
-
export class QueryError extends Schema.
|
|
5
|
+
export class QueryError extends Schema.TaggedError<QueryError>()("QueryError", {
|
|
6
6
|
message: Schema.String,
|
|
7
7
|
}) {}
|
|
8
8
|
|