@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
|
@@ -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
|
|
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": "
|
|
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: "
|
|
46
|
+
model: "example-chat-model",
|
|
47
47
|
status: "ok",
|
|
48
48
|
startedAt: new Date().toISOString(),
|
|
49
49
|
durationMs: 2400,
|
package/src/telemetry.test.ts
CHANGED
|
@@ -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) =>
|