@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 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/) v1.1 or newer.
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.4.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.1",
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.112",
102
- "@effect/opentelemetry": "4.0.0-rc.112",
103
- "@effect/platform-bun": "4.0.0-rc.112",
104
- "@effect/platform-node-shared": "4.0.0-rc.112",
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.112",
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.string("service-or-filter").pipe(
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.string("service").pipe(Argument.optional),
38
- limit: Argument.integer("limit").pipe(Argument.optional),
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.string("trace-id"),
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.string("span-id"),
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.string("trace-id"),
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.string("groupBy"),
106
- agg: Argument.choice("aggregation", ["count", "avg_duration", "p95_duration", "error_rate"]),
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.string("service").pipe(Argument.optional),
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.string("groupBy"),
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.string("trace-id"),
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.string("span-id"),
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.choice("type", ["traces", "logs"]),
175
- field: Argument.string("field"),
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
- Layer.provide(Logger.layer([Logger.consolePretty({ stderr: true })])),
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.boolean("replace").pipe(Flag.withDefault(false)),
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
- worker.addEventListener("error", () => progress.fail())
62
- worker.addEventListener("close", () => progress.fail())
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