@aryasaatvik/motel 0.4.0 → 0.5.0
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/README.md +1 -1
- package/package.json +7 -7
- package/src/cli.ts +14 -14
- package/src/mcp.ts +2 -1
- package/src/motel.ts +1 -1
- package/src/services/AsyncIngest.ts +16 -5
- package/src/services/telemetryWorker.ts +0 -2
- package/web/dist/assets/index-BuWb6t2M.js +21 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/index-CN7ClqOl.js +0 -21
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ curl -fsSL https://bun.sh/install | bash
|
|
|
50
50
|
the keyboard cheat sheet, or `c` to copy paste-ready setup instructions
|
|
51
51
|
for any Effect/OTEL app you want to trace.
|
|
52
52
|
|
|
53
|
-
Requirements: [Bun](https://bun.sh/)
|
|
53
|
+
Requirements: [Bun](https://bun.sh/) matching `package.json` `engines.bun` (`>=1.3.0`).
|
|
54
54
|
|
|
55
55
|
### macOS service from the maintained checkout
|
|
56
56
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aryasaatvik/motel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "A local OpenTelemetry ingest + TUI viewer for development, backed by SQLite.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -91,17 +91,17 @@
|
|
|
91
91
|
"prepublishOnly": "bun run web:build"
|
|
92
92
|
},
|
|
93
93
|
"devDependencies": {
|
|
94
|
-
"@effect/language-service": "0.87.
|
|
94
|
+
"@effect/language-service": "0.87.2",
|
|
95
95
|
"@types/bun": "^1.3.14",
|
|
96
96
|
"@types/react": "^19.2.17",
|
|
97
97
|
"tegami": "1.4.0",
|
|
98
98
|
"typescript": "^6.0.3"
|
|
99
99
|
},
|
|
100
100
|
"dependencies": {
|
|
101
|
-
"@effect/atom-react": "4.0.0-rc.
|
|
102
|
-
"@effect/opentelemetry": "4.0.0-rc.
|
|
103
|
-
"@effect/platform-bun": "4.0.0-rc.
|
|
104
|
-
"@effect/platform-node-shared": "4.0.0-rc.
|
|
101
|
+
"@effect/atom-react": "4.0.0-rc.117",
|
|
102
|
+
"@effect/opentelemetry": "4.0.0-rc.117",
|
|
103
|
+
"@effect/platform-bun": "4.0.0-rc.117",
|
|
104
|
+
"@effect/platform-node-shared": "4.0.0-rc.117",
|
|
105
105
|
"@opentelemetry/api": "^1.9.0",
|
|
106
106
|
"@opentelemetry/exporter-logs-otlp-http": "^0.219.0",
|
|
107
107
|
"@opentelemetry/exporter-trace-otlp-http": "^0.219.0",
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
"@opentelemetry/sdk-trace-node": "^2.8.0",
|
|
113
113
|
"@opentui/core": "0.4.2",
|
|
114
114
|
"@opentui/react": "0.4.2",
|
|
115
|
-
"effect": "4.0.0-rc.
|
|
115
|
+
"effect": "4.0.0-rc.117",
|
|
116
116
|
"protobufjs": "7.6.4",
|
|
117
117
|
"react": "^19.2.7",
|
|
118
118
|
"scheduler": "^0.27.0"
|
package/src/cli.ts
CHANGED
|
@@ -16,7 +16,7 @@ const query = <A>(effect: Effect.Effect<A, unknown, TelemetryStoreReadonly>) =>
|
|
|
16
16
|
|
|
17
17
|
const optional = (value: Option.Option<string>, fallback?: string) => Option.getOrElse(value, () => fallback)
|
|
18
18
|
|
|
19
|
-
const searchOperand = Argument.
|
|
19
|
+
const searchOperand = Argument.String("service-or-filter").pipe(
|
|
20
20
|
Argument.mapTryCatch(
|
|
21
21
|
(value) => {
|
|
22
22
|
if (value.startsWith("attr.") && !isAttributeFilterToken(value)) {
|
|
@@ -34,8 +34,8 @@ const services = Command.make("services", {}, () =>
|
|
|
34
34
|
).pipe(Command.withDescription("List observed telemetry services"))
|
|
35
35
|
|
|
36
36
|
const traces = Command.make("traces", {
|
|
37
|
-
service: Argument.
|
|
38
|
-
limit: Argument.
|
|
37
|
+
service: Argument.String("service").pipe(Argument.optional),
|
|
38
|
+
limit: Argument.Int("limit").pipe(Argument.optional),
|
|
39
39
|
}, ({ service, limit }) =>
|
|
40
40
|
query(Effect.flatMap(TelemetryStoreReadonly, (store) =>
|
|
41
41
|
store.listRecentTraces(optional(service, config.otel.serviceName)!, {
|
|
@@ -45,13 +45,13 @@ const traces = Command.make("traces", {
|
|
|
45
45
|
).pipe(Command.withDescription("List recent traces"))
|
|
46
46
|
|
|
47
47
|
const trace = Command.make("trace", {
|
|
48
|
-
traceId: Argument.
|
|
48
|
+
traceId: Argument.String("trace-id"),
|
|
49
49
|
}, ({ traceId }) =>
|
|
50
50
|
query(Effect.flatMap(TelemetryStoreReadonly, (store) => store.getTrace(traceId))),
|
|
51
51
|
).pipe(Command.withDescription("Get one trace"))
|
|
52
52
|
|
|
53
53
|
const span = Command.make("span", {
|
|
54
|
-
spanId: Argument.
|
|
54
|
+
spanId: Argument.String("span-id"),
|
|
55
55
|
}, ({ spanId }) =>
|
|
56
56
|
Effect.promise(() => fetch(`${config.otel.queryUrl}/api/spans/${encodeURIComponent(spanId)}`).then((response) => response.json())).pipe(
|
|
57
57
|
Effect.andThen(json),
|
|
@@ -59,7 +59,7 @@ const span = Command.make("span", {
|
|
|
59
59
|
).pipe(Command.withDescription("Get one span"))
|
|
60
60
|
|
|
61
61
|
const traceSpans = Command.make("trace-spans", {
|
|
62
|
-
traceId: Argument.
|
|
62
|
+
traceId: Argument.String("trace-id"),
|
|
63
63
|
}, ({ traceId }) =>
|
|
64
64
|
query(Effect.flatMap(TelemetryStoreReadonly, (store) => store.listTraceSpans(traceId))),
|
|
65
65
|
).pipe(Command.withDescription("List spans in one trace"))
|
|
@@ -102,8 +102,8 @@ const searchTraces = Command.make("search-traces", {
|
|
|
102
102
|
}).pipe(Command.withDescription("Search trace summaries by service, operation, and attributes"))
|
|
103
103
|
|
|
104
104
|
const traceStats = Command.make("trace-stats", {
|
|
105
|
-
groupBy: Argument.
|
|
106
|
-
agg: Argument.
|
|
105
|
+
groupBy: Argument.String("groupBy"),
|
|
106
|
+
agg: Argument.Literals("aggregation", ["count", "avg_duration", "p95_duration", "error_rate"]),
|
|
107
107
|
args: searchOperand,
|
|
108
108
|
}, ({ groupBy, agg, args }) => {
|
|
109
109
|
const values = args as ReadonlyArray<string>
|
|
@@ -121,7 +121,7 @@ const instructions = Command.make("instructions", {}, () => Console.log(otelServ
|
|
|
121
121
|
.pipe(Command.withDescription("Print Effect telemetry setup instructions"))
|
|
122
122
|
|
|
123
123
|
const logs = Command.make("logs", {
|
|
124
|
-
service: Argument.
|
|
124
|
+
service: Argument.String("service").pipe(Argument.optional),
|
|
125
125
|
}, ({ service }) =>
|
|
126
126
|
query(Effect.flatMap(TelemetryStoreReadonly, (store) => store.listRecentLogs(optional(service, config.otel.serviceName)!))),
|
|
127
127
|
).pipe(Command.withDescription("List recent logs"))
|
|
@@ -141,7 +141,7 @@ const searchLogs = Command.make("search-logs", {
|
|
|
141
141
|
}).pipe(Command.withDescription("Search logs by service, body, and attributes"))
|
|
142
142
|
|
|
143
143
|
const logStats = Command.make("log-stats", {
|
|
144
|
-
groupBy: Argument.
|
|
144
|
+
groupBy: Argument.String("groupBy"),
|
|
145
145
|
args: searchOperand,
|
|
146
146
|
}, ({ groupBy, args }) => {
|
|
147
147
|
const values = args as ReadonlyArray<string>
|
|
@@ -156,13 +156,13 @@ const logStats = Command.make("log-stats", {
|
|
|
156
156
|
}).pipe(Command.withDescription("Aggregate log metrics"))
|
|
157
157
|
|
|
158
158
|
const traceLogs = Command.make("trace-logs", {
|
|
159
|
-
traceId: Argument.
|
|
159
|
+
traceId: Argument.String("trace-id"),
|
|
160
160
|
}, ({ traceId }) =>
|
|
161
161
|
query(Effect.flatMap(TelemetryStoreReadonly, (store) => store.listTraceLogs(traceId))),
|
|
162
162
|
).pipe(Command.withDescription("List logs for one trace"))
|
|
163
163
|
|
|
164
164
|
const spanLogs = Command.make("span-logs", {
|
|
165
|
-
spanId: Argument.
|
|
165
|
+
spanId: Argument.String("span-id"),
|
|
166
166
|
}, ({ spanId }) =>
|
|
167
167
|
query(Effect.flatMap(TelemetryStoreReadonly, (store) => store.searchLogs({
|
|
168
168
|
spanId,
|
|
@@ -171,8 +171,8 @@ const spanLogs = Command.make("span-logs", {
|
|
|
171
171
|
).pipe(Command.withDescription("List logs for one span"))
|
|
172
172
|
|
|
173
173
|
const facets = Command.make("facets", {
|
|
174
|
-
type: Argument.
|
|
175
|
-
field: Argument.
|
|
174
|
+
type: Argument.Literals("type", ["traces", "logs"]),
|
|
175
|
+
field: Argument.String("field"),
|
|
176
176
|
}, ({ type, field }) =>
|
|
177
177
|
query(Effect.flatMap(TelemetryStoreReadonly, (store) => store.listFacets({ type, field, limit: 20 }))),
|
|
178
178
|
).pipe(Command.withDescription("List facet values"))
|
package/src/mcp.ts
CHANGED
|
@@ -433,7 +433,8 @@ const ServerLayer = McpServer.toolkit(MotelToolkit).pipe(
|
|
|
433
433
|
}),
|
|
434
434
|
),
|
|
435
435
|
Layer.provide(BunStdio.layer),
|
|
436
|
-
|
|
436
|
+
// stdout carries the MCP stdio protocol, so logs must go to stderr.
|
|
437
|
+
Layer.provide(Layer.merge(Logger.layer([Logger.consolePretty()]), Layer.succeed(Logger.LogToStderr, true))),
|
|
437
438
|
)
|
|
438
439
|
|
|
439
440
|
Layer.launch(ServerLayer).pipe(BunRuntime.runMain)
|
package/src/motel.ts
CHANGED
|
@@ -40,7 +40,7 @@ const restart = Command.make("restart", {}, () =>
|
|
|
40
40
|
|
|
41
41
|
const serviceManager = createLaunchAgentManager()
|
|
42
42
|
const serviceInstall = Command.make("install", {
|
|
43
|
-
replace: Flag.
|
|
43
|
+
replace: Flag.Boolean("replace").pipe(Flag.withDefault(false)),
|
|
44
44
|
}, ({ replace }) =>
|
|
45
45
|
serviceManager.install(replace).pipe(Effect.andThen(json)),
|
|
46
46
|
).pipe(Command.withDescription("Install the per-user Motel LaunchAgent"))
|
|
@@ -20,10 +20,9 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import * as BunWorker from "@effect/platform-bun/BunWorker"
|
|
23
|
-
import { Context, Effect, Layer, Scope, Schema } from "effect"
|
|
23
|
+
import { Context, Effect, Latch, Layer, Scope, Schema } from "effect"
|
|
24
24
|
import * as RpcClient from "effect/unstable/rpc/RpcClient"
|
|
25
25
|
import type { RpcClientError } from "effect/unstable/rpc/RpcClientError"
|
|
26
|
-
import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"
|
|
27
26
|
import type { WorkerError } from "effect/unstable/workers/WorkerError"
|
|
28
27
|
import { IngestProgress, WriterEvent, type IngestReadiness } from "../ingestReadiness.ts"
|
|
29
28
|
import { IngestRpcs } from "./ingestRpc.ts"
|
|
@@ -52,14 +51,22 @@ export const AsyncIngestLive = Layer.effect(
|
|
|
52
51
|
const event = Schema.decodeUnknownSync(WriterEvent)(data)
|
|
53
52
|
progress.receive(event)
|
|
54
53
|
}
|
|
54
|
+
// Opens once the current worker has answered the platform handshake (or
|
|
55
|
+
// died). Effect's worker close message is only received after that
|
|
56
|
+
// handshake; one posted earlier is dropped and the platform finalizer
|
|
57
|
+
// then waits five seconds before terminating the worker.
|
|
58
|
+
let handshake = Latch.makeUnsafe(true)
|
|
55
59
|
const WorkerProtocol = RpcClient.layerProtocolWorker({ size: 1 }).pipe(
|
|
56
|
-
Layer.provide(RpcSerialization.layerMsgPack),
|
|
57
60
|
Layer.provide(BunWorker.layer(() => {
|
|
58
61
|
const worker = new Worker(new URL("./telemetryWorker.ts", import.meta.url), {
|
|
59
62
|
env: { ...process.env, MOTEL_INGEST_DIAGNOSTICS_CHANNEL: channelName },
|
|
60
63
|
})
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
const spawned = Latch.makeUnsafe(false)
|
|
65
|
+
handshake = spawned
|
|
66
|
+
const settle = () => spawned.openUnsafe()
|
|
67
|
+
worker.addEventListener("message", settle, { once: true })
|
|
68
|
+
worker.addEventListener("error", () => { settle(); progress.fail() })
|
|
69
|
+
worker.addEventListener("close", () => { settle(); progress.fail() })
|
|
63
70
|
return worker
|
|
64
71
|
})),
|
|
65
72
|
)
|
|
@@ -76,6 +83,10 @@ export const AsyncIngestLive = Layer.effect(
|
|
|
76
83
|
const getClient = yield* Effect.cached(Effect.gen(function*() {
|
|
77
84
|
const clientScope = yield* Scope.fork(scope, "sequential")
|
|
78
85
|
const protocolContext = yield* Layer.buildWithScope(WorkerProtocol, clientScope)
|
|
86
|
+
// Added after the protocol's finalizers, so it runs before them: a
|
|
87
|
+
// shutdown during worker startup waits for the handshake and then
|
|
88
|
+
// closes the writer gracefully instead of timing out and terminating it.
|
|
89
|
+
yield* Scope.addFinalizer(clientScope, Effect.suspend(() => handshake.await).pipe(Effect.timeoutOption("5 seconds")))
|
|
79
90
|
return yield* RpcClient.make(IngestRpcs).pipe(
|
|
80
91
|
Effect.provide(protocolContext),
|
|
81
92
|
Effect.provideService(Scope.Scope, clientScope),
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
import { BunRuntime } from "@effect/platform-bun"
|
|
18
18
|
import * as BunWorkerRunner from "@effect/platform-bun/BunWorkerRunner"
|
|
19
19
|
import { Effect, Layer } from "effect"
|
|
20
|
-
import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization"
|
|
21
20
|
import * as RpcServer from "effect/unstable/rpc/RpcServer"
|
|
22
21
|
import type { OtlpLogExportRequest, OtlpTraceExportRequest } from "../otlp.ts"
|
|
23
22
|
import { WriterDiagnostics, type WriterEvent } from "../ingestReadiness.ts"
|
|
@@ -57,7 +56,6 @@ const WorkerLive = RpcServer.layer(IngestRpcs).pipe(
|
|
|
57
56
|
Layer.provide(IngestHandlers),
|
|
58
57
|
Layer.provide(TelemetryStoreWorkerLive),
|
|
59
58
|
Layer.provide(RpcServer.layerProtocolWorkerRunner),
|
|
60
|
-
Layer.provide(RpcSerialization.layerMsgPack),
|
|
61
59
|
Layer.provide(BunWorkerRunner.layer),
|
|
62
60
|
)
|
|
63
61
|
|