@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.
Files changed (66) hide show
  1. package/AGENTS.md +23 -8
  2. package/README.md +13 -2
  3. package/package.json +35 -19
  4. package/skills/motel-debug/SKILL.md +203 -0
  5. package/skills/motel-debug/references/effect.md +38 -0
  6. package/src/App.tsx +12 -5
  7. package/src/StartupGate.tsx +289 -0
  8. package/src/cli.ts +15 -16
  9. package/src/config.ts +7 -1
  10. package/src/daemon.test.ts +332 -51
  11. package/src/daemon.ts +105 -153
  12. package/src/httpApi.ts +1 -0
  13. package/src/httpListPolicy.test.ts +76 -0
  14. package/src/httpListPolicy.ts +129 -0
  15. package/src/index.tsx +9 -2
  16. package/src/localServer.ts +194 -313
  17. package/src/mcp.ts +2 -1
  18. package/src/motel.ts +0 -2
  19. package/src/opentui-jsx.d.ts +11 -0
  20. package/src/otlp.test.ts +65 -0
  21. package/src/otlp.ts +20 -0
  22. package/src/otlpProtobuf.ts +35 -0
  23. package/src/registry.ts +37 -11
  24. package/src/runtime.ts +2 -6
  25. package/src/services/AsyncIngest.ts +22 -8
  26. package/src/services/LogQueryService.ts +13 -27
  27. package/src/services/TelemetryQuery.ts +62 -0
  28. package/src/services/TelemetryStore.ts +546 -231
  29. package/src/services/TraceQueryService.ts +22 -56
  30. package/src/services/ingestRpc.ts +2 -4
  31. package/src/services/queryRpc.ts +15 -0
  32. package/src/services/telemetryQueryWorker.ts +32 -0
  33. package/src/services/telemetryWorker.ts +5 -8
  34. package/src/startupBench.ts +19 -0
  35. package/src/storybook/aiChatStory.tsx +1 -1
  36. package/src/telemetry.test.ts +307 -41
  37. package/src/ui/AiChatView.tsx +1 -1
  38. package/src/ui/AttrFilterModal.tsx +1 -1
  39. package/src/ui/ServiceLogs.tsx +10 -7
  40. package/src/ui/SpanContentView.tsx +24 -21
  41. package/src/ui/TraceDetailsPane.tsx +1 -1
  42. package/src/ui/TraceList.tsx +1 -1
  43. package/src/ui/aiState.ts +10 -22
  44. package/src/ui/app/TraceWorkspace.tsx +2 -1
  45. package/src/ui/app/useAppLayout.ts +1 -1
  46. package/src/ui/app/useTraceScreenData.ts +35 -23
  47. package/src/ui/atoms.ts +1 -1
  48. package/src/ui/cachedLoader.test.ts +23 -0
  49. package/src/ui/cachedLoader.ts +60 -0
  50. package/src/ui/loaders.ts +34 -53
  51. package/src/ui/persistence.ts +3 -3
  52. package/src/ui/primitives.tsx +1 -1
  53. package/src/ui/state.ts +2 -0
  54. package/src/ui/theme.ts +7 -5
  55. package/src/ui/traceDetailsWidth.repro.test.ts +12 -1
  56. package/src/ui/traceSortNav.repro.seed.ts +1 -1
  57. package/src/ui/traceSortNav.repro.test.ts +12 -2
  58. package/src/ui/useAttrFilterPicker.ts +10 -8
  59. package/src/ui/useKeyboardNav.ts +28 -5
  60. package/src/ui/waterfallNav.repro.seed.ts +1 -1
  61. package/src/ui/waterfallNav.repro.test.ts +16 -8
  62. package/web/dist/assets/index-B01z9BaO.css +2 -0
  63. package/web/dist/assets/index-M86tcih5.js +22 -0
  64. package/web/dist/index.html +2 -2
  65. package/web/dist/assets/index-DnyVo03x.js +0 -27
  66. package/web/dist/assets/index-DzuHNBGV.css +0 -2
@@ -0,0 +1,289 @@
1
+ import { Effect } from "effect"
2
+ import { RGBA, TextAttributes } from "@opentui/core"
3
+ import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react"
4
+ import { useEffect, useMemo, useState } from "react"
5
+ import { App } from "./App.js"
6
+ import { createDaemonManager, ensureManagedDaemon, getManagedDaemonStatus, type DaemonStatus } from "./daemon.js"
7
+ import { MOTEL_SERVICE_ID } from "./registry.js"
8
+ import { Divider, PlainLine, TextLine } from "./ui/primitives.tsx"
9
+ import { colors } from "./ui/theme.ts"
10
+
11
+ type ConflictStatus = DaemonStatus & {
12
+ readonly service: typeof MOTEL_SERVICE_ID
13
+ readonly pid: number
14
+ readonly workdir: string
15
+ readonly reason: string
16
+ }
17
+
18
+ type ConflictScreenState = {
19
+ kind: "conflict"
20
+ message: string
21
+ status: ConflictStatus
22
+ busy: boolean
23
+ notice: string | null
24
+ }
25
+
26
+ type ErrorScreenState = {
27
+ kind: "error"
28
+ message: string
29
+ busy: boolean
30
+ notice: string | null
31
+ }
32
+
33
+ type StartupState =
34
+ | { kind: "loading"; message: string }
35
+ | { kind: "ready" }
36
+ | ConflictScreenState
37
+ | ErrorScreenState
38
+
39
+ type RecoveryAction = {
40
+ readonly key: string
41
+ readonly label: string
42
+ readonly run: () => Promise<void>
43
+ readonly disabled?: boolean
44
+ }
45
+
46
+ const readStatus = () => Effect.runPromise(getManagedDaemonStatus)
47
+ const startDaemon = () => Effect.runPromise(ensureManagedDaemon)
48
+
49
+ const parsePort = (url: string) => {
50
+ try {
51
+ const port = Number(new URL(url).port)
52
+ return Number.isFinite(port) && port > 0 ? port : undefined
53
+ } catch {
54
+ return undefined
55
+ }
56
+ }
57
+
58
+ const isRecoverableConflict = (status: DaemonStatus | null): status is ConflictStatus =>
59
+ status !== null &&
60
+ status.service === MOTEL_SERVICE_ID &&
61
+ status.pid !== null &&
62
+ status.workdir !== null &&
63
+ status.reason !== null
64
+
65
+ const stopConflictingDaemon = async (status: ConflictStatus) => {
66
+ const port = parsePort(status.url)
67
+ const manager = createDaemonManager({
68
+ workdir: status.workdir ?? undefined,
69
+ databasePath: status.databasePath,
70
+ port,
71
+ })
72
+ await Effect.runPromise(manager.stop)
73
+ }
74
+
75
+ const LoadingScreen = ({ width, height, message }: { width: number; height: number; message: string }) => {
76
+ const panelWidth = Math.min(76, Math.max(50, width - 8))
77
+ const left = Math.max(0, Math.floor((width - panelWidth) / 2))
78
+ const top = Math.max(0, Math.floor((height - 5) / 2))
79
+
80
+ return (
81
+ <box width={width} height={height} backgroundColor={RGBA.fromHex(colors.screenBg)}>
82
+ <box position="absolute" left={left} top={top} width={panelWidth} flexDirection="column">
83
+ <TextLine>
84
+ <span fg={colors.accent} attributes={TextAttributes.BOLD}>MOTEL</span>
85
+ <span fg={colors.separator}>{" · "}</span>
86
+ <span fg={colors.muted}>starting up...</span>
87
+ </TextLine>
88
+ <Divider width={panelWidth} />
89
+ <box paddingTop={1}>
90
+ <PlainLine text={message} fg={colors.count} />
91
+ </box>
92
+ </box>
93
+ </box>
94
+ )
95
+ }
96
+
97
+ const RecoveryScreen = ({
98
+ title,
99
+ message,
100
+ width,
101
+ height,
102
+ detailLines,
103
+ actions,
104
+ selectedIndex,
105
+ notice,
106
+ busy,
107
+ }: {
108
+ readonly title: string
109
+ readonly message: string
110
+ readonly width: number
111
+ readonly height: number
112
+ readonly detailLines: readonly string[]
113
+ readonly actions: readonly RecoveryAction[]
114
+ readonly selectedIndex: number
115
+ readonly notice: string | null
116
+ readonly busy: boolean
117
+ }) => {
118
+ const panelWidth = Math.min(96, Math.max(64, width - 8))
119
+ const left = Math.max(0, Math.floor((width - panelWidth) / 2))
120
+ const bodyHeight = 9 + detailLines.length + actions.length + (notice ? 2 : 0)
121
+ const top = Math.max(0, Math.floor((height - bodyHeight) / 2))
122
+
123
+ return (
124
+ <box width={width} height={height} backgroundColor={RGBA.fromHex(colors.screenBg)}>
125
+ <box position="absolute" left={left} top={top} width={panelWidth} flexDirection="column">
126
+ <TextLine>
127
+ <span fg={colors.accent} attributes={TextAttributes.BOLD}>MOTEL</span>
128
+ <span fg={colors.separator}>{" · "}</span>
129
+ <span fg={colors.error} attributes={TextAttributes.BOLD}>{title}</span>
130
+ </TextLine>
131
+ <Divider width={panelWidth} />
132
+ <box paddingTop={1} paddingBottom={1} flexDirection="column">
133
+ <PlainLine text={message} fg={colors.text} />
134
+ </box>
135
+ {detailLines.map((line, index) => (
136
+ <PlainLine key={`${index}:${line}`} text={line} fg={colors.muted} />
137
+ ))}
138
+ <box paddingTop={1} flexDirection="column">
139
+ {actions.map((action, index) => {
140
+ const selected = index === selectedIndex
141
+ const prefix = selected ? ">" : " "
142
+ const text = `${prefix} [${action.key}] ${action.label}${action.disabled ? " (unavailable)" : ""}`
143
+ return (
144
+ <TextLine key={action.key} bg={selected ? colors.selectedBg : undefined}>
145
+ <span fg={selected ? colors.selectedText : colors.text}>{text}</span>
146
+ </TextLine>
147
+ )
148
+ })}
149
+ </box>
150
+ <box paddingTop={1} flexDirection="column">
151
+ {notice ? <PlainLine text={notice} fg={busy ? colors.warning : colors.count} /> : null}
152
+ <PlainLine text={busy ? "Working..." : "j/k or ↑↓ select · enter run · r retry · k stop incompatible daemon · q quit"} fg={colors.count} />
153
+ </box>
154
+ </box>
155
+ </box>
156
+ )
157
+ }
158
+
159
+ export const StartupGate = () => {
160
+ const renderer = useRenderer()
161
+ const { width = 100, height = 24 } = useTerminalDimensions()
162
+ const [startupState, setStartupState] = useState<StartupState>({ kind: "loading", message: "Checking managed daemon..." })
163
+ const [selectedIndex, setSelectedIndex] = useState(0)
164
+
165
+ const attemptStart = async () => {
166
+ setStartupState({ kind: "loading", message: "Checking managed daemon..." })
167
+ try {
168
+ await startDaemon()
169
+ setStartupState({ kind: "ready" })
170
+ } catch (error) {
171
+ const message = error instanceof Error ? error.message : String(error)
172
+ const status = await readStatus().catch(() => null)
173
+ if (isRecoverableConflict(status)) {
174
+ setSelectedIndex(0)
175
+ setStartupState({ kind: "conflict", message, status, busy: false, notice: null })
176
+ return
177
+ }
178
+ setSelectedIndex(0)
179
+ setStartupState({ kind: "error", message, busy: false, notice: null })
180
+ }
181
+ }
182
+
183
+ useEffect(() => {
184
+ void attemptStart()
185
+ }, [])
186
+
187
+ const actions = useMemo<readonly RecoveryAction[]>(() => {
188
+ if (startupState.kind === "conflict") {
189
+ return [
190
+ { key: "r", label: "Retry startup", run: attemptStart },
191
+ {
192
+ key: "k",
193
+ label: `Stop incompatible daemon (${startupState.status.pid})`,
194
+ run: async () => {
195
+ setStartupState((current) => current.kind === "conflict"
196
+ ? { ...current, busy: true, notice: `Stopping daemon ${current.status.pid}...` }
197
+ : current)
198
+ try {
199
+ await stopConflictingDaemon(startupState.status)
200
+ await attemptStart()
201
+ } catch (error) {
202
+ const message = error instanceof Error ? error.message : String(error)
203
+ setStartupState((current) => current.kind === "conflict"
204
+ ? { ...current, busy: false, notice: message }
205
+ : current)
206
+ }
207
+ },
208
+ },
209
+ { key: "q", label: "Quit", run: async () => { renderer.destroy() } },
210
+ ]
211
+ }
212
+ if (startupState.kind === "error") {
213
+ return [
214
+ { key: "r", label: "Retry startup", run: attemptStart },
215
+ { key: "q", label: "Quit", run: async () => { renderer.destroy() } },
216
+ ]
217
+ }
218
+ return []
219
+ }, [renderer, startupState])
220
+
221
+ useKeyboard((key) => {
222
+ if (startupState.kind === "ready" || startupState.kind === "loading" || key.repeated) return
223
+ if (key.name === "q" || (key.ctrl && key.name === "c")) {
224
+ renderer.destroy()
225
+ return
226
+ }
227
+ if (key.name === "up" || key.name === "k") {
228
+ setSelectedIndex((current) => (current + actions.length - 1) % actions.length)
229
+ return
230
+ }
231
+ if (key.name === "down" || key.name === "j") {
232
+ setSelectedIndex((current) => (current + 1) % actions.length)
233
+ return
234
+ }
235
+ if (key.name === "r") {
236
+ void actions.find((action) => action.key === "r")?.run()
237
+ return
238
+ }
239
+ if (key.name === "k") {
240
+ void actions.find((action) => action.key === "k")?.run()
241
+ return
242
+ }
243
+ if (key.name === "return" || key.name === "enter") {
244
+ void actions[selectedIndex]?.run()
245
+ }
246
+ })
247
+
248
+ if (startupState.kind === "ready") return <App />
249
+ if (startupState.kind === "loading") return <LoadingScreen width={width} height={height} message={startupState.message} />
250
+ if (startupState.kind === "conflict") {
251
+ const status = startupState.status
252
+ const detailLines = [
253
+ `Port: ${status.url}`,
254
+ `Daemon workdir: ${status.workdir}`,
255
+ `Daemon pid: ${status.pid}`,
256
+ `Incompatible database: ${status.databasePath}`,
257
+ status.workdir.startsWith("/tmp") || status.workdir.startsWith("/private/tmp")
258
+ ? "This looks like a temp/test daemon."
259
+ : "This daemon is using a different database configuration.",
260
+ ]
261
+ return (
262
+ <RecoveryScreen
263
+ title="Daemon Configuration Conflict"
264
+ message={startupState.message}
265
+ width={width}
266
+ height={height}
267
+ detailLines={detailLines}
268
+ actions={actions}
269
+ selectedIndex={selectedIndex}
270
+ notice={startupState.notice}
271
+ busy={startupState.busy}
272
+ />
273
+ )
274
+ }
275
+
276
+ return (
277
+ <RecoveryScreen
278
+ title="Startup Error"
279
+ message={startupState.message}
280
+ width={width}
281
+ height={height}
282
+ detailLines={["Retry startup or quit the TUI."]}
283
+ actions={actions}
284
+ selectedIndex={selectedIndex}
285
+ notice={startupState.notice}
286
+ busy={startupState.busy}
287
+ />
288
+ )
289
+ }
package/src/cli.ts CHANGED
@@ -3,18 +3,17 @@ import { config } from "./config.js"
3
3
  import { otelServerInstructions } from "./instructions.js"
4
4
  import { attributeFiltersFromArgs, isAttributeFilterToken } from "./queryFilters.js"
5
5
  import { queryRuntime } from "./runtime.js"
6
- import { LogQueryService } from "./services/LogQueryService.js"
7
- import { TraceQueryService } from "./services/TraceQueryService.js"
6
+ import { TelemetryStoreReadonly } from "./services/TelemetryStore.js"
8
7
 
9
8
  const [command, ...args] = process.argv.slice(2)
10
9
 
11
- const runQuiet = <A, E, R extends TraceQueryService | LogQueryService | never>(effect: Effect.Effect<A, E, R>) =>
10
+ const runQuiet = <A, E, R extends TelemetryStoreReadonly | never>(effect: Effect.Effect<A, E, R>) =>
12
11
  queryRuntime.runPromise(effect.pipe(Effect.provideService(References.MinimumLogLevel, "None")))
13
12
 
14
13
  try {
15
14
  switch (command) {
16
15
  case "services": {
17
- const result = await runQuiet(Effect.flatMap(TraceQueryService.asEffect(), (query) => query.listServices))
16
+ const result = await runQuiet(Effect.flatMap(TelemetryStoreReadonly, (query) => query.listServices))
18
17
  console.log(JSON.stringify(result, null, 2))
19
18
  break
20
19
  }
@@ -22,7 +21,7 @@ try {
22
21
  case "traces": {
23
22
  const service = args[0] ?? config.otel.serviceName
24
23
  const limit = args[1] ? Number.parseInt(args[1], 10) : config.otel.traceFetchLimit
25
- const result = await runQuiet(Effect.flatMap(TraceQueryService.asEffect(), (query) => query.listRecentTraces(service, { limit })))
24
+ const result = await runQuiet(Effect.flatMap(TelemetryStoreReadonly, (query) => query.listRecentTraces(service, { limit })))
26
25
  console.log(JSON.stringify(result, null, 2))
27
26
  break
28
27
  }
@@ -33,7 +32,7 @@ try {
33
32
  throw new Error("Usage: bun run cli trace <trace-id>")
34
33
  }
35
34
 
36
- const result = await runQuiet(Effect.flatMap(TraceQueryService.asEffect(), (query) => query.getTrace(traceId)))
35
+ const result = await runQuiet(Effect.flatMap(TelemetryStoreReadonly, (query) => query.getTrace(traceId)))
37
36
  console.log(JSON.stringify(result, null, 2))
38
37
  break
39
38
  }
@@ -55,7 +54,7 @@ try {
55
54
  throw new Error("Usage: bun run cli trace-spans <trace-id>")
56
55
  }
57
56
 
58
- const result = await runQuiet(Effect.flatMap(TraceQueryService.asEffect(), (query) => query.listTraceSpans(traceId)))
57
+ const result = await runQuiet(Effect.flatMap(TelemetryStoreReadonly, (query) => query.listTraceSpans(traceId)))
59
58
  console.log(JSON.stringify(result, null, 2))
60
59
  break
61
60
  }
@@ -68,7 +67,7 @@ try {
68
67
  const attributeStartIndex = operation ? 2 : 1
69
68
  const attributeFilters = attributeFiltersFromArgs(args.slice(attributeStartIndex))
70
69
  const result = await runQuiet(
71
- Effect.flatMap(TraceQueryService.asEffect(), (query) =>
70
+ Effect.flatMap(TelemetryStoreReadonly, (query) =>
72
71
  query.searchSpans({
73
72
  serviceName: service,
74
73
  operation,
@@ -87,7 +86,7 @@ try {
87
86
  const operation = args[1] && !isAttributeFilterToken(args[1]) ? args[1] : undefined
88
87
  const attributeFilters = attributeFiltersFromArgs(args.slice(operation ? 2 : 1))
89
88
  const result = await runQuiet(
90
- Effect.flatMap(TraceQueryService.asEffect(), (query) =>
89
+ Effect.flatMap(TelemetryStoreReadonly, (query) =>
91
90
  query.searchTraces({
92
91
  serviceName: service,
93
92
  operation,
@@ -110,7 +109,7 @@ try {
110
109
  }
111
110
 
112
111
  const result = await runQuiet(
113
- Effect.flatMap(TraceQueryService.asEffect(), (query) =>
112
+ Effect.flatMap(TelemetryStoreReadonly, (query) =>
114
113
  query.traceStats({
115
114
  groupBy,
116
115
  agg,
@@ -131,7 +130,7 @@ try {
131
130
 
132
131
  case "logs": {
133
132
  const service = args[0] ?? config.otel.serviceName
134
- const result = await runQuiet(Effect.flatMap(LogQueryService.asEffect(), (query) => query.listRecentLogs(service)))
133
+ const result = await runQuiet(Effect.flatMap(TelemetryStoreReadonly, (query) => query.listRecentLogs(service)))
135
134
  console.log(JSON.stringify(result, null, 2))
136
135
  break
137
136
  }
@@ -141,7 +140,7 @@ try {
141
140
  const body = args[1] && !isAttributeFilterToken(args[1]) ? args[1] : undefined
142
141
  const attributeFilters = attributeFiltersFromArgs(args.slice(body ? 2 : 1))
143
142
  const result = await runQuiet(
144
- Effect.flatMap(LogQueryService.asEffect(), (query) =>
143
+ Effect.flatMap(TelemetryStoreReadonly, (query) =>
145
144
  query.searchLogs({
146
145
  serviceName: service,
147
146
  body,
@@ -163,7 +162,7 @@ try {
163
162
  }
164
163
 
165
164
  const result = await runQuiet(
166
- Effect.flatMap(LogQueryService.asEffect(), (query) =>
165
+ Effect.flatMap(TelemetryStoreReadonly, (query) =>
167
166
  query.logStats({
168
167
  groupBy,
169
168
  agg: "count",
@@ -183,7 +182,7 @@ try {
183
182
  throw new Error("Usage: bun run cli trace-logs <trace-id>")
184
183
  }
185
184
 
186
- const result = await runQuiet(Effect.flatMap(LogQueryService.asEffect(), (query) => query.listTraceLogs(traceId)))
185
+ const result = await runQuiet(Effect.flatMap(TelemetryStoreReadonly, (query) => query.listTraceLogs(traceId)))
187
186
  console.log(JSON.stringify(result, null, 2))
188
187
  break
189
188
  }
@@ -195,7 +194,7 @@ try {
195
194
  }
196
195
 
197
196
  const result = await runQuiet(
198
- Effect.flatMap(LogQueryService.asEffect(), (query) =>
197
+ Effect.flatMap(TelemetryStoreReadonly, (query) =>
199
198
  query.searchLogs({
200
199
  spanId,
201
200
  limit: config.otel.logFetchLimit,
@@ -214,7 +213,7 @@ try {
214
213
  }
215
214
 
216
215
  const result = await runQuiet(
217
- Effect.flatMap(LogQueryService.asEffect(), (query) =>
216
+ Effect.flatMap(TelemetryStoreReadonly, (query) =>
218
217
  query.listFacets({ type, field, limit: 20 }),
219
218
  ),
220
219
  )
package/src/config.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import { motelStateDir } from "./registry.js"
2
+ import * as path from "node:path"
3
+
1
4
  const parseBoolean = (value: string | undefined, defaultValue: boolean) => {
2
5
  const normalized = value?.trim().toLowerCase()
3
6
  if (!normalized) return defaultValue
@@ -29,11 +32,14 @@ export const config = {
29
32
  queryUrl: baseUrl,
30
33
  exporterUrl: process.env.MOTEL_OTEL_EXPORTER_URL?.trim() || resolveOtelUrl("/v1/traces"),
31
34
  logsExporterUrl: process.env.MOTEL_OTEL_LOGS_EXPORTER_URL?.trim() || resolveOtelUrl("/v1/logs"),
32
- databasePath: process.env.MOTEL_OTEL_DB_PATH?.trim() || `${import.meta.dir}/../.motel-data/telemetry.sqlite`,
35
+ databasePath: process.env.MOTEL_OTEL_DB_PATH?.trim() || path.join(motelStateDir(), "telemetry.sqlite"),
33
36
  traceLookbackMinutes: parsePositiveInt(process.env.MOTEL_OTEL_TRACE_LOOKBACK_MINUTES, 1440),
34
37
  traceFetchLimit: parsePositiveInt(process.env.MOTEL_OTEL_TRACE_LIMIT, 100),
35
38
  logFetchLimit: parsePositiveInt(process.env.MOTEL_OTEL_LOG_LIMIT, 80),
36
39
  retentionHours: parsePositiveInt(process.env.MOTEL_OTEL_RETENTION_HOURS, 168),
37
40
  maxDbSizeMb: parsePositiveInt(process.env.MOTEL_OTEL_MAX_DB_SIZE_MB, 1024),
41
+ retentionTraceBatch: parsePositiveInt(process.env.MOTEL_OTEL_RETENTION_TRACE_BATCH, 100),
42
+ retentionLogBatch: parsePositiveInt(process.env.MOTEL_OTEL_RETENTION_LOG_BATCH, 5_000),
43
+ retentionIntervalSeconds: parsePositiveInt(process.env.MOTEL_OTEL_RETENTION_INTERVAL_SECONDS, 10),
38
44
  },
39
45
  } as const