@kitlangton/motel 0.2.7 → 0.2.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitlangton/motel",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "A local OpenTelemetry ingest + TUI viewer for development, backed by SQLite.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -614,6 +614,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
614
614
  CREATE INDEX IF NOT EXISTS idx_logs_trace_time ON logs(trace_id, timestamp_ms DESC);
615
615
  CREATE INDEX IF NOT EXISTS idx_logs_span_time ON logs(span_id, timestamp_ms DESC);
616
616
  CREATE INDEX IF NOT EXISTS idx_logs_severity_time ON logs(severity_text, timestamp_ms DESC);
617
+ CREATE INDEX IF NOT EXISTS idx_logs_severity_nocase_time ON logs(severity_text COLLATE NOCASE, timestamp_ms DESC);
617
618
 
618
619
  CREATE TABLE IF NOT EXISTS trace_summaries (
619
620
  trace_id TEXT PRIMARY KEY,
@@ -1673,7 +1674,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
1673
1674
  params.push(input.serviceName)
1674
1675
  }
1675
1676
  if (input.severity) {
1676
- clauses.push(`severity_text = ?`)
1677
+ clauses.push(`severity_text = ? COLLATE NOCASE`)
1677
1678
  params.push(input.severity.toUpperCase())
1678
1679
  }
1679
1680
  if (input.traceId) {
@@ -1886,7 +1887,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
1886
1887
  const group = input.groupBy === "service"
1887
1888
  ? log.serviceName
1888
1889
  : input.groupBy === "severity"
1889
- ? log.severityText
1890
+ ? log.severityText.toUpperCase()
1890
1891
  : input.groupBy === "scope"
1891
1892
  ? log.scopeName ?? "unknown"
1892
1893
  : isAttrGroupBy
@@ -1932,7 +1933,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
1932
1933
  const groupExpr = input.groupBy === "service"
1933
1934
  ? "service_name"
1934
1935
  : input.groupBy === "severity"
1935
- ? "severity_text"
1936
+ ? "UPPER(severity_text)"
1936
1937
  : input.groupBy === "scope"
1937
1938
  ? "COALESCE(scope_name, 'unknown')"
1938
1939
  : "'unknown'"
@@ -1976,11 +1977,11 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
1976
1977
  }
1977
1978
  if (input.field === "severity") {
1978
1979
  const rows = db.query(`
1979
- SELECT severity_text AS value, COUNT(*) AS count
1980
+ SELECT UPPER(severity_text) AS value, COUNT(*) AS count
1980
1981
  FROM logs
1981
1982
  WHERE timestamp_ms >= ?
1982
1983
  ${input.serviceName ? "AND service_name = ?" : ""}
1983
- GROUP BY severity_text
1984
+ GROUP BY value
1984
1985
  ORDER BY count DESC, value ASC
1985
1986
  LIMIT ?
1986
1987
  `).all(...(input.serviceName ? [cutoff, input.serviceName, limit] : [cutoff, limit])) as Array<{ value: string; count: number }>
@@ -27,7 +27,7 @@ export const makeSpan = (overrides: Partial<TraceSpanItem> = {}): TraceSpanItem
27
27
  depth: 0,
28
28
  tags: {
29
29
  "ai.operationId": "ai.streamText",
30
- "ai.model.id": "claude-opus-4-7",
30
+ "ai.model.id": "example-chat-model",
31
31
  "ai.model.provider": "anthropic",
32
32
  "ai.prompt.messages": "[]",
33
33
  },
@@ -43,7 +43,7 @@ export const makeDetail = (overrides: Partial<AiCallDetail> = {}): AiCallDetail
43
43
  service: "storybook",
44
44
  functionId: "story.demo",
45
45
  provider: "anthropic",
46
- model: "claude-opus-4-7",
46
+ model: "example-chat-model",
47
47
  status: "ok",
48
48
  startedAt: new Date().toISOString(),
49
49
  durationMs: 2400,
@@ -668,6 +668,81 @@ describe("motel telemetry store", () => {
668
668
  expect(result[0]?.severityText).toBe("ERROR")
669
669
  })
670
670
 
671
+ describe("mixed-case stored log severities", () => {
672
+ const serviceName = "severity-case-test"
673
+ const severities = ["Info", "INFO", "info", "Error"]
674
+
675
+ beforeAll(async () => {
676
+ await storeRuntime.runPromise(Effect.flatMap(TelemetryStore, (store) =>
677
+ store.ingestLogs({
678
+ resourceLogs: [{
679
+ resource: { attributes: [{ key: "service.name", value: { stringValue: serviceName } }] },
680
+ scopeLogs: [{ logRecords: severities.map((severityText, index) => ({
681
+ timeUnixNano: String(BigInt(Date.now()) * 1_000_000n),
682
+ severityText,
683
+ body: { stringValue: `severity-case-${index}` },
684
+ attributes: [{ key: "batch", value: { stringValue: index < 2 ? "included" : "excluded" } }],
685
+ })) }],
686
+ }],
687
+ }),
688
+ ).pipe(Effect.provideService(References.MinimumLogLevel, "None")))
689
+ })
690
+
691
+ for (const severity of ["INFO", "Info", "info"]) {
692
+ it(`filters ${severity} without changing returned severity text`, async () => {
693
+ const logs = await storeRuntime.runPromise(Effect.flatMap(TelemetryStore, (store) =>
694
+ store.searchLogs({ serviceName, severity }),
695
+ ))
696
+ expect(logs.map((log) => log.severityText).sort()).toEqual(["INFO", "Info", "info"])
697
+ })
698
+ }
699
+
700
+ it("groups severity variants in SQL stats", async () => {
701
+ const stats = await storeRuntime.runPromise(Effect.flatMap(TelemetryStore, (store) =>
702
+ store.logStats({ serviceName, groupBy: "severity", agg: "count" }),
703
+ ))
704
+ expect(stats).toEqual([
705
+ { group: "INFO", value: 3, count: 3 },
706
+ { group: "ERROR", value: 1, count: 1 },
707
+ ])
708
+ })
709
+
710
+ it("groups severity variants in attribute-filtered stats", async () => {
711
+ const stats = await storeRuntime.runPromise(Effect.flatMap(TelemetryStore, (store) =>
712
+ store.logStats({ serviceName, groupBy: "severity", agg: "count", attributeFilters: { batch: "included" } }),
713
+ ))
714
+ expect(stats).toEqual([{ group: "INFO", value: 2, count: 2 }])
715
+ })
716
+
717
+ it("indexes case-insensitive severity searches", () => {
718
+ const probe = new Database(dbPath, { readonly: true })
719
+ try {
720
+ const plan = probe.query<{ detail: string }, [string, number, number]>(`
721
+ EXPLAIN QUERY PLAN SELECT * FROM logs
722
+ WHERE severity_text = ? COLLATE NOCASE AND timestamp_ms >= ?
723
+ ORDER BY timestamp_ms DESC, id DESC LIMIT ?
724
+ `).all("INFO", 0, 80)
725
+ expect(plan.some((row) => row.detail.includes("idx_logs_severity_nocase_time (severity_text=?"))).toBe(true)
726
+ } finally {
727
+ probe.close()
728
+ }
729
+ })
730
+
731
+ it("groups severity facets without changing persisted text", async () => {
732
+ const facets = await storeRuntime.runPromise(Effect.flatMap(TelemetryStore, (store) =>
733
+ store.listFacets({ serviceName, type: "logs", field: "severity" }),
734
+ ))
735
+ expect(facets).toEqual([{ value: "INFO", count: 3 }, { value: "ERROR", count: 1 }])
736
+ const probe = new Database(dbPath, { readonly: true })
737
+ try {
738
+ expect(probe.query("SELECT severity_text FROM logs WHERE service_name = ? ORDER BY id").all(serviceName))
739
+ .toEqual(severities.map((severity_text) => ({ severity_text })))
740
+ } finally {
741
+ probe.close()
742
+ }
743
+ })
744
+ })
745
+
671
746
  it("searches log body case-insensitively", async () => {
672
747
  const result = await storeRuntime.runPromise(
673
748
  Effect.flatMap(TelemetryStore, (store) =>