@aryasaatvik/motel 0.3.5 → 0.4.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/AGENTS.md +4 -1
- package/README.md +51 -0
- package/package.json +1 -1
- package/src/config.ts +3 -0
- package/src/daemon.ts +69 -35
- package/src/httpApi.ts +28 -9
- package/src/ingestReadiness.test.ts +160 -0
- package/src/ingestReadiness.ts +100 -0
- package/src/localServer.ts +29 -14
- package/src/queryHttp.test.ts +34 -0
- package/src/runtime.ts +4 -2
- package/src/services/AsyncIngest.ts +36 -16
- package/src/services/TelemetryQuery.ts +18 -31
- package/src/services/TelemetryStore.ts +160 -259
- package/src/services/ingestRpc.ts +2 -2
- package/src/services/queryRpc.ts +12 -12
- package/src/services/queryScheduler.test.ts +102 -0
- package/src/services/queryScheduler.ts +157 -0
- package/src/services/retention.test.ts +170 -0
- package/src/services/retention.ts +158 -0
- package/src/services/telemetryQueryProcess.ts +10 -0
- package/src/services/telemetryQueryWorker.ts +25 -26
- package/src/services/telemetryWorker.ts +9 -1
- package/src/telemetry.test.ts +34 -1
- package/src/ui/app/useTraceScreenData.ts +11 -17
- package/src/ui/cachedLoader.test.ts +20 -0
- package/src/ui/cachedLoader.ts +1 -2
- package/src/ui/loaders.ts +17 -6
- package/web/dist/assets/index-CN7ClqOl.js +21 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/index-5mTXbtNw.js +0 -21
|
@@ -2,6 +2,8 @@ import { Database } from "bun:sqlite"
|
|
|
2
2
|
import * as BunFileSystem from "@effect/platform-bun/BunFileSystem"
|
|
3
3
|
import { dirname } from "node:path"
|
|
4
4
|
import { Cause, Clock, Effect, FileSystem, Layer, Schedule, Context } from "effect"
|
|
5
|
+
import { installRetentionSchema, checkpointPassive, mergeFts, retainBatch, repairSearchRows } from "./retention.js"
|
|
6
|
+
import { WriterDiagnostics } from "../ingestReadiness.ts"
|
|
5
7
|
import { config } from "../config.js"
|
|
6
8
|
import type { AiCallDetail, AiCallSummary, FacetItem, LogItem, SpanItem, StatsItem, TraceItem, TraceSummaryItem, TraceSpanEvent, TraceSpanItem } from "../domain.js"
|
|
7
9
|
import { AI_ATTR_MAP, AI_FTS_KEYS, AI_TEXT_SEARCH_KEYS, truncatePreview } from "../domain.js"
|
|
@@ -568,12 +570,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
568
570
|
-- this the WAL happily runs into the hundreds of MB and queries
|
|
569
571
|
-- start paying the cost of walking the WAL on every read.
|
|
570
572
|
PRAGMA wal_autocheckpoint = 4000;
|
|
571
|
-
--
|
|
572
|
-
-- pages move out of the WAL; size_limit controls how much the
|
|
573
|
-
-- WAL file is allowed to grow on disk. 128MB is generous enough
|
|
574
|
-
-- to absorb a long write burst without blocking on truncation,
|
|
575
|
-
-- tight enough that a wedged retention loop can't hide a 20GB
|
|
576
|
-
-- WAL the way a default no-limit configuration can.
|
|
573
|
+
-- Retained WAL size after a reset; not a cap while readers hold snapshots.
|
|
577
574
|
PRAGMA journal_size_limit = 134217728;
|
|
578
575
|
|
|
579
576
|
CREATE TABLE IF NOT EXISTS spans (
|
|
@@ -787,6 +784,18 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
787
784
|
try { db.exec(`PRAGMA busy_timeout = 15000;`) } catch { /* ignore */ }
|
|
788
785
|
} // end: if (!opts.readonly) writer init
|
|
789
786
|
|
|
787
|
+
if (!opts.readonly) {
|
|
788
|
+
installRetentionSchema(db)
|
|
789
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_spans_roots ON spans(trace_id, start_time_ms) WHERE parent_span_id IS NULL")
|
|
790
|
+
}
|
|
791
|
+
const hasRetention = db.query("SELECT 1 FROM sqlite_master WHERE name = 'retained_spans'").get() !== null
|
|
792
|
+
const spanSource = hasRetention ? "retained_spans" : "spans"
|
|
793
|
+
const logSource = hasRetention ? "retained_logs" : "logs"
|
|
794
|
+
const traceSource = hasRetention ? "retained_trace_summaries" : "trace_summaries"
|
|
795
|
+
const evictingTrace = hasRetention ? db.query("SELECT 1 FROM retention_traces WHERE trace_id = ?") : null
|
|
796
|
+
const mapSpanSearch = hasRetention ? db.query("INSERT OR REPLACE INTO span_search_rows VALUES (?, ?, ?)") : null
|
|
797
|
+
const mapLogSearch = hasRetention ? db.query("INSERT OR REPLACE INTO log_search_rows VALUES (?, ?)") : null
|
|
798
|
+
|
|
790
799
|
const insertSpan = db.query(`
|
|
791
800
|
INSERT INTO spans (
|
|
792
801
|
trace_id, span_id, parent_span_id, service_name, scope_name, operation_name, kind,
|
|
@@ -834,7 +843,11 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
834
843
|
return
|
|
835
844
|
}
|
|
836
845
|
const transaction = db.transaction(() => {
|
|
837
|
-
for (const traceId of new Set(rows.map((row) => row.trace_id)))
|
|
846
|
+
for (const traceId of new Set(rows.map((row) => row.trace_id))) {
|
|
847
|
+
// Ingest updates existing summaries transactionally. Rebuilding them on every
|
|
848
|
+
// maintenance tick repeatedly scans large traces and delays unrelated writes.
|
|
849
|
+
if (!evictingTrace?.get(traceId) && !db.query("SELECT 1 FROM trace_summaries WHERE trace_id = ?").get(traceId)) upsertTraceSummary.run(traceId)
|
|
850
|
+
}
|
|
838
851
|
db.query(`INSERT OR REPLACE INTO motel_maintenance(key, value) VALUES ('trace_summary_cursor', ?)`).run(String(rows.at(-1)!.rowid))
|
|
839
852
|
})
|
|
840
853
|
transaction()
|
|
@@ -867,7 +880,9 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
867
880
|
if (operations.length === 1) {
|
|
868
881
|
const [traceId, spanId, operationName] = operations[0]!
|
|
869
882
|
deleteSpanOperationSearch.run(traceId, spanId)
|
|
870
|
-
|
|
883
|
+
db.query("DELETE FROM span_search_rows WHERE trace_id = ? AND span_id = ?").run(traceId, spanId)
|
|
884
|
+
const result = insertSpanOperationSearch.run(traceId, spanId, operationName)
|
|
885
|
+
mapSpanSearch?.run(Number(result.lastInsertRowid), traceId, spanId)
|
|
871
886
|
return
|
|
872
887
|
}
|
|
873
888
|
|
|
@@ -877,13 +892,17 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
877
892
|
deleteSpanOperationSearchManyByCount.set(operations.length, deleteQuery)
|
|
878
893
|
}
|
|
879
894
|
deleteQuery.run(...operations.flatMap(([traceId, spanId]) => [traceId, spanId]))
|
|
895
|
+
for (const [traceId, spanId] of operations) db.query("DELETE FROM span_search_rows WHERE trace_id = ? AND span_id = ?").run(traceId, spanId)
|
|
880
896
|
|
|
881
897
|
let insertQuery = insertSpanOperationSearchManyByCount.get(operations.length)
|
|
882
898
|
if (!insertQuery) {
|
|
883
899
|
insertQuery = db.query(`INSERT INTO span_operation_fts (trace_id, span_id, operation_name) VALUES ${operations.map(() => "(?, ?, ?)").join(", ")}`)
|
|
884
900
|
insertSpanOperationSearchManyByCount.set(operations.length, insertQuery)
|
|
885
901
|
}
|
|
886
|
-
insertQuery.run(...operations.flatMap(([traceId, spanId, operationName]) => [traceId, spanId, operationName]))
|
|
902
|
+
const result = insertQuery.run(...operations.flatMap(([traceId, spanId, operationName]) => [traceId, spanId, operationName]))
|
|
903
|
+
// FTS allocates consecutive rowids within this trigger-free multi-row INSERT.
|
|
904
|
+
const firstId = Number(result.lastInsertRowid) - operations.length + 1
|
|
905
|
+
for (const [index, [traceId, spanId]] of operations.entries()) mapSpanSearch?.run(firstId + index, traceId, spanId)
|
|
887
906
|
}
|
|
888
907
|
const insertLogAttribute = db.query(`INSERT INTO log_attributes (log_id, key, value) VALUES (?, ?, ?)`)
|
|
889
908
|
const logAttributeInsertManyByCount = new Map<number, ReturnType<Database["query"]>>()
|
|
@@ -908,7 +927,8 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
908
927
|
if (entries.length === 0) return
|
|
909
928
|
if (entries.length === 1) {
|
|
910
929
|
const [logId, body] = entries[0]!
|
|
911
|
-
insertLogBodySearch.run(logId, body)
|
|
930
|
+
const result = insertLogBodySearch.run(logId, body)
|
|
931
|
+
mapLogSearch?.run(Number(result.lastInsertRowid), Number(logId))
|
|
912
932
|
return
|
|
913
933
|
}
|
|
914
934
|
let query = insertLogBodySearchManyByCount.get(entries.length)
|
|
@@ -916,161 +936,53 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
916
936
|
query = db.query(`INSERT INTO log_body_fts (log_id, body) VALUES ${entries.map(() => "(?, ?)").join(", ")}`)
|
|
917
937
|
insertLogBodySearchManyByCount.set(entries.length, query)
|
|
918
938
|
}
|
|
919
|
-
query.run(...entries.flatMap(([logId, body]) => [logId, body]))
|
|
939
|
+
const result = query.run(...entries.flatMap(([logId, body]) => [logId, body]))
|
|
940
|
+
const firstId = Number(result.lastInsertRowid) - entries.length + 1
|
|
941
|
+
for (const [index, [logId]] of entries.entries()) mapLogSearch?.run(firstId + index, Number(logId))
|
|
920
942
|
}
|
|
921
943
|
|
|
922
944
|
const maxDbSizeBytes = config.otel.maxDbSizeMb * 1024 * 1024
|
|
923
945
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
const VACUUM_PAGES_PANIC = 50000 // ~200MB/pass — only when ratio > 50%
|
|
934
|
-
|
|
935
|
-
const ftsTableNames = ["span_attr_fts", "log_body_fts", "span_operation_fts"] as const
|
|
936
|
-
|
|
937
|
-
const incrementalFtsMerge = (pages: number) => {
|
|
938
|
-
// FTS5 segment merges drop tombstone rows that DELETE leaves behind.
|
|
939
|
-
// Without periodic merges, deleted FTS rows stay on disk indefinitely
|
|
940
|
-
// — a major source of freelist pages on a heavy-deletion workload.
|
|
941
|
-
// `merge=N` is a bounded, online operation: it merges at most N
|
|
942
|
-
// pages of work and returns. Per FTS5 docs, missing tables silently
|
|
943
|
-
// throw; we swallow because not every DB has every FTS table.
|
|
944
|
-
for (const name of ftsTableNames) {
|
|
945
|
-
try { db.query(`INSERT INTO ${name}(${name}) VALUES (?)`).run(`merge=${pages}`) } catch { /* table absent or older schema */ }
|
|
946
|
-
}
|
|
947
|
-
}
|
|
946
|
+
const publishDiagnostic = yield* WriterDiagnostics
|
|
947
|
+
const observeMaintenance = <A, E, R>(operation: string, effect: Effect.Effect<A, E, R>) =>
|
|
948
|
+
Effect.suspend(() => {
|
|
949
|
+
const startedAt = Date.now()
|
|
950
|
+
publishDiagnostic({ _tag: "maintenance", value: { operation, startedAt, durationMs: null, outcome: "running" } })
|
|
951
|
+
return effect.pipe(Effect.onExit((exit) => Effect.sync(() => publishDiagnostic({
|
|
952
|
+
_tag: "maintenance", value: { operation, startedAt, durationMs: Date.now() - startedAt, outcome: exit._tag === "Success" ? "completed" : "failed" },
|
|
953
|
+
}))))
|
|
954
|
+
})
|
|
948
955
|
|
|
956
|
+
const checkpoint = () => {
|
|
957
|
+
const value = checkpointPassive(db)
|
|
958
|
+
publishDiagnostic({ _tag: "checkpoint", value })
|
|
959
|
+
return value
|
|
960
|
+
}
|
|
949
961
|
const reclaimSpace = Effect.fn("motel/TelemetryStore.reclaimSpace")(function* () {
|
|
950
962
|
yield* Effect.sync(() => {
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
if (
|
|
954
|
-
const
|
|
955
|
-
if (
|
|
956
|
-
|
|
957
|
-
// Adaptive vacuum sizing — fixed 2000 pages/min could not keep
|
|
958
|
-
// up with sustained deletions, leaking 10GB of freelist over
|
|
959
|
-
// time. Scale the per-pass work to the size of the backlog so
|
|
960
|
-
// we stay roughly proportional to the deficit.
|
|
961
|
-
const pages =
|
|
962
|
-
ratio >= FREELIST_HIGH_RATIO ? VACUUM_PAGES_PANIC :
|
|
963
|
-
ratio >= FREELIST_MID_RATIO ? VACUUM_PAGES_BUSY :
|
|
964
|
-
VACUUM_PAGES_NORMAL
|
|
965
|
-
|
|
966
|
-
try { db.exec(`PRAGMA incremental_vacuum(${pages});`) } catch { /* ignore */ }
|
|
967
|
-
|
|
968
|
-
// In WAL mode incremental_vacuum only moves pages — the file
|
|
969
|
-
// shrinks on the next checkpoint. PASSIVE silently skips when
|
|
970
|
-
// readers are active (the failure mode the agent's research
|
|
971
|
-
// flagged: checkpoint starvation). Use RESTART normally and
|
|
972
|
-
// TRUNCATE in panic mode to physically shrink the WAL when it
|
|
973
|
-
// has grown.
|
|
974
|
-
const mode = ratio >= FREELIST_HIGH_RATIO ? "TRUNCATE" : "RESTART"
|
|
975
|
-
try { db.exec(`PRAGMA wal_checkpoint(${mode});`) } catch { /* ignore */ }
|
|
963
|
+
// Never wait out busy_timeout for a WAL reader. Defer physical reclaim until
|
|
964
|
+
// all frames are checkpointed; a held snapshot can grow WAL beyond size_limit.
|
|
965
|
+
if (checkpoint().deferred) return
|
|
966
|
+
const freePages = (db.query("PRAGMA freelist_count").get() as { freelist_count: number }).freelist_count
|
|
967
|
+
if (freePages > 0) db.exec("PRAGMA incremental_vacuum(256)")
|
|
976
968
|
})
|
|
977
969
|
})
|
|
978
970
|
|
|
971
|
+
let maintenancePending = false
|
|
979
972
|
const cleanupExpired = Effect.fn("motel/TelemetryStore.cleanupExpired")(function* () {
|
|
980
973
|
const now = yield* Clock.currentTimeMillis
|
|
981
|
-
|
|
982
974
|
yield* Effect.sync(() => {
|
|
983
|
-
const
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
`SELECT trace_id FROM trace_summaries WHERE active_span_count = 0 AND ended_at_ms > 0 AND ended_at_ms < ? ORDER BY ended_at_ms ASC LIMIT ?`,
|
|
995
|
-
).all(cutoff, config.otel.retentionTraceBatch) as readonly { trace_id: string }[]
|
|
996
|
-
for (const row of timeExpired) toEvict.add(row.trace_id)
|
|
997
|
-
|
|
998
|
-
// Size-based: if actual data exceeds the target, drop one bounded
|
|
999
|
-
// batch of the oldest completed traces. `(page_count - freelist_count)`
|
|
1000
|
-
// ignores freed-but-not-vacuumed pages so a large freelist doesn't
|
|
1001
|
-
// trigger a deletion death spiral.
|
|
1002
|
-
const pageCount = (db.query(`PRAGMA page_count`).get() as { page_count: number }).page_count
|
|
1003
|
-
const freePages = (db.query(`PRAGMA freelist_count`).get() as { freelist_count: number }).freelist_count
|
|
1004
|
-
const pageSize = (db.query(`PRAGMA page_size`).get() as { page_size: number }).page_size
|
|
1005
|
-
const dbSize = (pageCount - freePages) * pageSize
|
|
1006
|
-
if (dbSize > maxDbSizeBytes) {
|
|
1007
|
-
const oldest = db.query(
|
|
1008
|
-
`SELECT trace_id FROM trace_summaries WHERE active_span_count = 0 ORDER BY started_at_ms ASC LIMIT ?`,
|
|
1009
|
-
).all(config.otel.retentionTraceBatch) as readonly { trace_id: string }[]
|
|
1010
|
-
// Set.add dedupes overlap with the time-expired batch above.
|
|
1011
|
-
for (const row of oldest) toEvict.add(row.trace_id)
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
// Logs have their own retention boundary. A correlated log may refer
|
|
1015
|
-
// to a trace that was sampled elsewhere or never reached Motel, so
|
|
1016
|
-
// tying log eviction to trace_summaries lets those rows grow forever.
|
|
1017
|
-
const expiredLogs = db.query(`DELETE FROM logs WHERE id IN (SELECT id FROM logs WHERE timestamp_ms < ? ORDER BY timestamp_ms ASC LIMIT ?)`).run(cutoff, config.otel.retentionLogBatch)
|
|
1018
|
-
let deletedLogs = Number(expiredLogs.changes) > 0
|
|
1019
|
-
if (dbSize > maxDbSizeBytes) {
|
|
1020
|
-
const oversizedLogs = db.query(`DELETE FROM logs WHERE id IN (SELECT id FROM logs ORDER BY timestamp_ms ASC LIMIT ?)`).run(config.otel.retentionLogBatch)
|
|
1021
|
-
deletedLogs = deletedLogs || Number(oversizedLogs.changes) > 0
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
// Batch the trace-id list so the IN placeholders stay under
|
|
1025
|
-
// SQLite's default limit (~999). Each batch wipes every row
|
|
1026
|
-
// reachable from those trace_ids across the cascade tables.
|
|
1027
|
-
const traceIds = Array.from(toEvict)
|
|
1028
|
-
const BATCH_SIZE = 500
|
|
1029
|
-
for (let offset = 0; offset < traceIds.length; offset += BATCH_SIZE) {
|
|
1030
|
-
const batch = traceIds.slice(offset, offset + BATCH_SIZE)
|
|
1031
|
-
const placeholders = batch.map(() => "?").join(",")
|
|
1032
|
-
db.query(`DELETE FROM span_attributes WHERE trace_id IN (${placeholders})`).run(...batch)
|
|
1033
|
-
try {
|
|
1034
|
-
db.query(`DELETE FROM span_operation_fts WHERE trace_id IN (${placeholders})`).run(...batch)
|
|
1035
|
-
} catch {
|
|
1036
|
-
// FTS table may not exist on old DBs.
|
|
1037
|
-
}
|
|
1038
|
-
db.query(`DELETE FROM spans WHERE trace_id IN (${placeholders})`).run(...batch)
|
|
1039
|
-
db.query(`DELETE FROM logs WHERE trace_id IN (${placeholders})`).run(...batch)
|
|
1040
|
-
db.query(`DELETE FROM trace_summaries WHERE trace_id IN (${placeholders})`).run(...batch)
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
// Log-side orphans (log_attributes + FTS) are keyed by log.id,
|
|
1044
|
-
// so prune what no longer has a parent log row.
|
|
1045
|
-
const orphanAttributes = db.query(`DELETE FROM log_attributes WHERE rowid IN (SELECT log_attributes.rowid FROM log_attributes WHERE NOT EXISTS (SELECT 1 FROM logs WHERE logs.id = log_attributes.log_id) LIMIT ?)`).run(config.otel.retentionLogBatch)
|
|
1046
|
-
let deletedOrphans = Number(orphanAttributes.changes) > 0
|
|
1047
|
-
try {
|
|
1048
|
-
const orphanFts = db.query(`DELETE FROM log_body_fts WHERE rowid IN (SELECT rowid FROM log_body_fts WHERE NOT EXISTS (SELECT 1 FROM logs WHERE logs.id = CAST(log_body_fts.log_id AS INTEGER)) LIMIT ?)`).run(config.otel.retentionLogBatch)
|
|
1049
|
-
deletedOrphans = deletedOrphans || Number(orphanFts.changes) > 0
|
|
1050
|
-
} catch {
|
|
1051
|
-
// FTS table may not exist on old DBs.
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
// Checkpoint after a big delete pass so the freed pages land
|
|
1055
|
-
// in the main DB file and become eligible for incremental
|
|
1056
|
-
// vacuum. Use RESTART (not PASSIVE): PASSIVE silently no-ops
|
|
1057
|
-
// when readers are active, which is the documented mechanism
|
|
1058
|
-
// behind WAL/freelist starvation when ingest is busy.
|
|
1059
|
-
if (toEvict.size === 0 && !deletedLogs && !deletedOrphans) return
|
|
1060
|
-
try { db.exec(`PRAGMA wal_checkpoint(RESTART);`) } catch { /* ignore */ }
|
|
1061
|
-
|
|
1062
|
-
// Incremental FTS5 merge — DELETE on an FTS5-indexed row
|
|
1063
|
-
// leaves a tombstone in the segment tree that only `merge`
|
|
1064
|
-
// reclaims. Skipping this is the second compounding cause
|
|
1065
|
-
// (after fixed-size vacuum) of the slow freelist accretion
|
|
1066
|
-
// that took the DB to 17GB. 100 pages of merge work per
|
|
1067
|
-
// retention tick is bounded and runs in milliseconds.
|
|
1068
|
-
incrementalFtsMerge(100)
|
|
1069
|
-
|
|
1070
|
-
// Actual page reclamation lives in `reclaimSpace`, which
|
|
1071
|
-
// runs on its own faster cadence so the file shrinks even
|
|
1072
|
-
// when no traces are evicted in a given retention tick (e.g.
|
|
1073
|
-
// after a large historical eviction has already happened).
|
|
975
|
+
const result = retainBatch(db, {
|
|
976
|
+
cutoff: now - config.otel.retentionHours * 60 * 60 * 1000,
|
|
977
|
+
maxBytes: maxDbSizeBytes,
|
|
978
|
+
traces: config.otel.retentionTraceBatch,
|
|
979
|
+
logs: config.otel.retentionLogBatch,
|
|
980
|
+
rows: config.otel.retentionRowBatch,
|
|
981
|
+
})
|
|
982
|
+
maintenancePending = result.pending && result.rows > 0
|
|
983
|
+
repairSearchRows(db, Math.min(100, config.otel.retentionRowBatch))
|
|
984
|
+
mergeFts(db, 100)
|
|
985
|
+
checkpoint()
|
|
1074
986
|
})
|
|
1075
987
|
})
|
|
1076
988
|
|
|
@@ -1078,18 +990,20 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
1078
990
|
// the HTTP event loop and no second writer duplicates cleanup work.
|
|
1079
991
|
if (opts.runRetention) {
|
|
1080
992
|
// Cleanup runs on the telemetry worker, never the HTTP event loop.
|
|
1081
|
-
|
|
1082
|
-
Effect.andThen(reconcileTraceSummaries, cleanupExpired()).pipe(
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
993
|
+
const retentionLoop: Effect.Effect<void> = Effect.suspend(() =>
|
|
994
|
+
observeMaintenance("retention", Effect.andThen(reconcileTraceSummaries, cleanupExpired())).pipe(
|
|
995
|
+
Effect.catchCause((cause) => {
|
|
996
|
+
maintenancePending = false
|
|
997
|
+
return Effect.logWarning(`motel: maintenance pass failed: ${Cause.pretty(cause)}`)
|
|
998
|
+
}),
|
|
999
|
+
Effect.andThen(Effect.suspend(() => Effect.sleep(maintenancePending ? "100 millis" : `${config.otel.retentionIntervalSeconds} seconds`))),
|
|
1000
|
+
Effect.andThen(Effect.suspend(() => retentionLoop)),
|
|
1001
|
+
),
|
|
1002
|
+
)
|
|
1003
|
+
yield* Effect.forkScoped(retentionLoop)
|
|
1004
|
+
|
|
1005
|
+
// Physical reclamation has a small fixed page budget and defers for readers.
|
|
1006
|
+
yield* Effect.forkScoped(Effect.repeat(observeMaintenance("reclaim", reclaimSpace()).pipe(Effect.catchCause((cause) => Effect.logWarning(`motel: reclaim failed: ${Cause.pretty(cause)}`))), Schedule.spaced("10 seconds")))
|
|
1093
1007
|
|
|
1094
1008
|
// Periodically refresh query planner stats. `PRAGMA optimize` is a
|
|
1095
1009
|
// no-op when nothing has changed, so this is essentially free on idle
|
|
@@ -1099,7 +1013,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
1099
1013
|
const refreshPlannerStats = Effect.sync(() => {
|
|
1100
1014
|
try { db.exec(`PRAGMA optimize;`) } catch { /* ignore */ }
|
|
1101
1015
|
})
|
|
1102
|
-
yield* Effect.forkScoped(Effect.repeat(refreshPlannerStats, Schedule.spaced("15 minutes")))
|
|
1016
|
+
yield* Effect.forkScoped(Effect.repeat(observeMaintenance("planner", refreshPlannerStats), Schedule.spaced("15 minutes")))
|
|
1103
1017
|
}
|
|
1104
1018
|
|
|
1105
1019
|
// Incrementally rebuild historical AI attributes in bounded batches.
|
|
@@ -1163,7 +1077,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
1163
1077
|
for (const span of scopeSpans.spans ?? []) {
|
|
1164
1078
|
const traceId = normalizeOtlpBinaryId(span.traceId, 16)
|
|
1165
1079
|
const spanId = normalizeOtlpBinaryId(span.spanId, 8)
|
|
1166
|
-
if (!traceId || !spanId) continue
|
|
1080
|
+
if (!traceId || !spanId || evictingTrace?.get(traceId)) continue
|
|
1167
1081
|
const parentSpanId = normalizeOtlpBinaryId(span.parentSpanId, 8)
|
|
1168
1082
|
const spanAttributes = attributeMap(span.attributes)
|
|
1169
1083
|
const mergedAttributes = { ...resourceAttributes, ...spanAttributes }
|
|
@@ -1236,8 +1150,10 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
1236
1150
|
const body = stringifyValue(parseAnyValue(record.body))
|
|
1237
1151
|
const rawTraceId = attributes.traceId || attributes.trace_id || record.traceId || null
|
|
1238
1152
|
const rawSpanId = attributes.spanId || attributes.span_id || record.spanId || null
|
|
1153
|
+
const traceId = normalizeOtlpBinaryId(rawTraceId, 16)
|
|
1154
|
+
if (traceId && evictingTrace?.get(traceId)) continue
|
|
1239
1155
|
const result = insertLog.run(
|
|
1240
|
-
|
|
1156
|
+
traceId,
|
|
1241
1157
|
normalizeOtlpBinaryId(rawSpanId, 8),
|
|
1242
1158
|
serviceName,
|
|
1243
1159
|
scopeName,
|
|
@@ -1276,9 +1192,9 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
1276
1192
|
// long-running trace can emit a current child after its root ages
|
|
1277
1193
|
// outside the lookback window.
|
|
1278
1194
|
const rows = db.query(`
|
|
1279
|
-
SELECT service_name FROM
|
|
1195
|
+
SELECT service_name FROM ${spanSource} WHERE start_time_ms >= ?
|
|
1280
1196
|
UNION
|
|
1281
|
-
SELECT service_name FROM
|
|
1197
|
+
SELECT service_name FROM ${logSource} WHERE timestamp_ms >= ?
|
|
1282
1198
|
ORDER BY service_name ASC
|
|
1283
1199
|
`).all(cutoff, cutoff) as Array<{ service_name: string }>
|
|
1284
1200
|
return rows.map((row) => row.service_name)
|
|
@@ -1291,7 +1207,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
1291
1207
|
if (traceIds.length === 0) return [] as readonly TraceItem[]
|
|
1292
1208
|
const placeholders = traceIds.map(() => "?").join(", ")
|
|
1293
1209
|
const rows = db.query(`
|
|
1294
|
-
SELECT * FROM
|
|
1210
|
+
SELECT * FROM ${spanSource}
|
|
1295
1211
|
WHERE trace_id IN (${placeholders})
|
|
1296
1212
|
ORDER BY start_time_ms ASC
|
|
1297
1213
|
`).all(...traceIds) as SpanRow[]
|
|
@@ -1338,7 +1254,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
1338
1254
|
|
|
1339
1255
|
return db.query(`
|
|
1340
1256
|
SELECT trace_id, service_name, root_operation_name, started_at_ms, ended_at_ms, active_span_count, duration_ms, span_count, error_count
|
|
1341
|
-
FROM
|
|
1257
|
+
FROM ${traceSource}
|
|
1342
1258
|
WHERE ${clauses.join(" AND ")}
|
|
1343
1259
|
ORDER BY started_at_ms DESC, trace_id DESC
|
|
1344
1260
|
LIMIT ?
|
|
@@ -1381,7 +1297,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
1381
1297
|
clauses.push("trace_id IN (SELECT DISTINCT trace_id FROM span_operation_fts WHERE span_operation_fts MATCH ?)")
|
|
1382
1298
|
params.push(ftsQuery)
|
|
1383
1299
|
} else {
|
|
1384
|
-
clauses.push(
|
|
1300
|
+
clauses.push(`trace_id IN (SELECT DISTINCT trace_id FROM ${spanSource} WHERE operation_name LIKE ? COLLATE NOCASE)`)
|
|
1385
1301
|
params.push(`%${input.operation}%`)
|
|
1386
1302
|
}
|
|
1387
1303
|
}
|
|
@@ -1413,7 +1329,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
1413
1329
|
|
|
1414
1330
|
const rows = db.query(`
|
|
1415
1331
|
SELECT trace_id, service_name, root_operation_name, started_at_ms, ended_at_ms, active_span_count, duration_ms, span_count, error_count
|
|
1416
|
-
FROM
|
|
1332
|
+
FROM ${traceSource}
|
|
1417
1333
|
WHERE ${clauses.join(" AND ")}
|
|
1418
1334
|
ORDER BY started_at_ms DESC, trace_id DESC
|
|
1419
1335
|
LIMIT ?
|
|
@@ -1427,7 +1343,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
1427
1343
|
yield* Effect.annotateCurrentSpan("trace.trace_id", traceId)
|
|
1428
1344
|
return yield* Effect.sync(() => {
|
|
1429
1345
|
const rows = db.query(`
|
|
1430
|
-
SELECT * FROM
|
|
1346
|
+
SELECT * FROM ${spanSource} WHERE trace_id = ? ORDER BY start_time_ms ASC
|
|
1431
1347
|
`).all(traceId) as SpanRow[]
|
|
1432
1348
|
return rows.length === 0 ? null : buildTrace(traceId, rows)
|
|
1433
1349
|
})
|
|
@@ -1437,7 +1353,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
1437
1353
|
yield* Effect.annotateCurrentSpan("trace.span_id", spanId)
|
|
1438
1354
|
return yield* Effect.sync(() => {
|
|
1439
1355
|
// Fetch only the target span row (uses idx_spans_span_id)
|
|
1440
|
-
const spanRow = db.query(`SELECT * FROM
|
|
1356
|
+
const spanRow = db.query(`SELECT * FROM ${spanSource} WHERE span_id = ? LIMIT 1`).get(spanId) as SpanRow | null
|
|
1441
1357
|
if (!spanRow) return null
|
|
1442
1358
|
|
|
1443
1359
|
const traceId = spanRow.trace_id
|
|
@@ -1451,11 +1367,11 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
1451
1367
|
const ancestors = db.query(`
|
|
1452
1368
|
WITH RECURSIVE ancestors(span_id, parent_span_id, operation_name, hop) AS (
|
|
1453
1369
|
SELECT span_id, parent_span_id, operation_name, 1
|
|
1454
|
-
FROM
|
|
1370
|
+
FROM ${spanSource} WHERE trace_id = ? AND span_id = ?
|
|
1455
1371
|
UNION ALL
|
|
1456
1372
|
SELECT s.span_id, s.parent_span_id, s.operation_name, a.hop + 1
|
|
1457
1373
|
FROM ancestors a
|
|
1458
|
-
JOIN
|
|
1374
|
+
JOIN ${spanSource} s ON s.trace_id = ? AND s.span_id = a.parent_span_id
|
|
1459
1375
|
)
|
|
1460
1376
|
SELECT span_id, parent_span_id, operation_name, hop FROM ancestors ORDER BY hop ASC
|
|
1461
1377
|
`).all(traceId, spanRow.parent_span_id, traceId) as Array<{ span_id: string; parent_span_id: string | null; operation_name: string; hop: number }>
|
|
@@ -1465,7 +1381,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
1465
1381
|
}
|
|
1466
1382
|
|
|
1467
1383
|
const rootRow = db.query(`
|
|
1468
|
-
SELECT operation_name FROM
|
|
1384
|
+
SELECT operation_name FROM ${spanSource}
|
|
1469
1385
|
WHERE trace_id = ? AND parent_span_id IS NULL
|
|
1470
1386
|
ORDER BY start_time_ms ASC LIMIT 1
|
|
1471
1387
|
`).get(traceId) as { operation_name: string } | null
|
|
@@ -1483,7 +1399,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
1483
1399
|
|
|
1484
1400
|
const listTraceSpans = Effect.fn("motel/TelemetryStore.listTraceSpans")(function* (traceId: string) {
|
|
1485
1401
|
return yield* Effect.sync(() => {
|
|
1486
|
-
const rows = db.query(`SELECT * FROM
|
|
1402
|
+
const rows = db.query(`SELECT * FROM ${spanSource} WHERE trace_id = ? ORDER BY start_time_ms ASC`).all(traceId) as SpanRow[]
|
|
1487
1403
|
return rows.length === 0 ? [] as readonly SpanItem[] : buildSpanItems(traceId, rows)
|
|
1488
1404
|
})
|
|
1489
1405
|
})
|
|
@@ -1508,7 +1424,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
1508
1424
|
// to drive the parent-context lookup. Parsing the heavy
|
|
1509
1425
|
// `*_json` blobs is deferred until after we've sliced down
|
|
1510
1426
|
// to the final `limit`.
|
|
1511
|
-
let fromSql =
|
|
1427
|
+
let fromSql = `FROM ${spanSource} AS s`
|
|
1512
1428
|
const joinParams: Array<string | number> = []
|
|
1513
1429
|
const clauses: string[] = ["s.start_time_ms >= ?"]
|
|
1514
1430
|
const params: Array<string | number> = [cutoff]
|
|
@@ -1549,75 +1465,60 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
1549
1465
|
}
|
|
1550
1466
|
|
|
1551
1467
|
const candidateRows = db.query(`
|
|
1552
|
-
SELECT s.trace_id, s.span_id, s.parent_span_id, s.operation_name, s.start_time_ms
|
|
1468
|
+
SELECT s.trace_id, s.span_id, s.parent_span_id, s.operation_name, s.start_time_ms,
|
|
1469
|
+
(SELECT p.operation_name FROM ${spanSource} p WHERE p.trace_id = s.trace_id AND p.span_id = s.parent_span_id) AS parent_operation_name
|
|
1553
1470
|
${fromSql}
|
|
1554
1471
|
WHERE ${clauses.join(" AND ")}
|
|
1555
1472
|
ORDER BY s.start_time_ms DESC
|
|
1556
1473
|
LIMIT ?
|
|
1557
|
-
`).all(...joinParams, ...params, candidateLimit) as Array<{ trace_id: string; span_id: string; parent_span_id: string | null; operation_name: string; start_time_ms: number }>
|
|
1558
|
-
|
|
1559
|
-
const traceIds = [...new Set(candidateRows.map((row) => row.trace_id))]
|
|
1560
|
-
if (traceIds.length === 0) return [] as readonly SpanItem[]
|
|
1474
|
+
`).all(...joinParams, ...params, candidateLimit) as Array<{ trace_id: string; span_id: string; parent_span_id: string | null; operation_name: string; start_time_ms: number; parent_operation_name: string | null }>
|
|
1561
1475
|
|
|
1476
|
+
// Keep the public Unicode substring matching and candidate-window semantics.
|
|
1477
|
+
// Parent metadata is an indexed point lookup, not whole-trace hydration.
|
|
1478
|
+
const parentOperationNeedle = input.parentOperation?.toLowerCase() ?? null
|
|
1479
|
+
const filteredLite = candidateRows.filter((row) => !parentOperationNeedle || row.parent_operation_name?.toLowerCase().includes(parentOperationNeedle)).slice(0, limit)
|
|
1480
|
+
if (filteredLite.length === 0) return [] as readonly SpanItem[]
|
|
1562
1481
|
const keyOf = (traceId: string, spanId: string) => `${traceId}:${spanId}`
|
|
1563
|
-
const
|
|
1564
|
-
|
|
1565
|
-
//
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
`).all(...
|
|
1575
|
-
|
|
1576
|
-
const rootOperationByTraceId = new Map<string, { operationName: string; startTimeMs: number }>()
|
|
1577
|
-
for (const row of allSpanRows) {
|
|
1578
|
-
spanContextById.set(keyOf(row.trace_id, row.span_id), {
|
|
1579
|
-
parentSpanId: row.parent_span_id,
|
|
1580
|
-
operationName: row.operation_name,
|
|
1581
|
-
})
|
|
1582
|
-
if (row.parent_span_id === null) {
|
|
1583
|
-
const existing = rootOperationByTraceId.get(row.trace_id)
|
|
1584
|
-
if (!existing || row.start_time_ms < existing.startTimeMs) {
|
|
1585
|
-
rootOperationByTraceId.set(row.trace_id, { operationName: row.operation_name, startTimeMs: row.start_time_ms })
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1482
|
+
const anchorValues = filteredLite.map(() => "(?, ?)").join(", ")
|
|
1483
|
+
// UNION deduplicates cycles. Only ancestors of returned spans are loaded;
|
|
1484
|
+
// unrelated siblings and their large metadata never enter this traversal.
|
|
1485
|
+
const ancestors = db.query(`
|
|
1486
|
+
WITH RECURSIVE ancestors(trace_id, span_id, parent_span_id, operation_name) AS (
|
|
1487
|
+
SELECT trace_id, span_id, parent_span_id, operation_name FROM ${spanSource}
|
|
1488
|
+
WHERE (trace_id, span_id) IN (VALUES ${anchorValues})
|
|
1489
|
+
UNION
|
|
1490
|
+
SELECT s.trace_id, s.span_id, s.parent_span_id, s.operation_name
|
|
1491
|
+
FROM ${spanSource} s JOIN ancestors a ON s.trace_id = a.trace_id AND s.span_id = a.parent_span_id
|
|
1492
|
+
) SELECT * FROM ancestors
|
|
1493
|
+
`).all(...filteredLite.flatMap((row) => [row.trace_id, row.span_id])) as Array<{ trace_id: string; span_id: string; parent_span_id: string | null; operation_name: string }>
|
|
1494
|
+
const spanContextById = new Map(ancestors.map((row) => [keyOf(row.trace_id, row.span_id), { parentSpanId: row.parent_span_id, operationName: row.operation_name }]))
|
|
1590
1495
|
const getSpanContext = (traceId: string, spanId: string) => spanContextById.get(keyOf(traceId, spanId)) ?? null
|
|
1591
|
-
|
|
1592
|
-
const
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
const cached = depthById.get(key)
|
|
1596
|
-
if (cached !== undefined) return cached
|
|
1597
|
-
if (visiting.has(key)) return 0
|
|
1598
|
-
visiting.add(key)
|
|
1599
|
-
const context = getSpanContext(traceId, spanId)
|
|
1600
|
-
const depth = context?.parentSpanId ? getDepth(traceId, context.parentSpanId, visiting) + 1 : 0
|
|
1601
|
-
depthById.set(key, depth)
|
|
1602
|
-
return depth
|
|
1496
|
+
const rootOperationByTraceId = new Map<string, { operationName: string }>()
|
|
1497
|
+
for (const traceId of new Set(filteredLite.map((row) => row.trace_id))) {
|
|
1498
|
+
const root = db.query(`SELECT operation_name FROM ${spanSource} WHERE trace_id = ? AND parent_span_id IS NULL ORDER BY start_time_ms LIMIT 1`).get(traceId) as { operation_name: string } | null
|
|
1499
|
+
if (root) rootOperationByTraceId.set(traceId, { operationName: root.operation_name })
|
|
1603
1500
|
}
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
const
|
|
1613
|
-
if (
|
|
1501
|
+
const depthById = new Map<string, number>()
|
|
1502
|
+
const getDepth = (traceId: string, spanId: string): number => {
|
|
1503
|
+
const path: string[] = []
|
|
1504
|
+
const visiting = new Set<string>()
|
|
1505
|
+
let current: string | null = spanId
|
|
1506
|
+
let depth = 0
|
|
1507
|
+
while (current) {
|
|
1508
|
+
const key = keyOf(traceId, current)
|
|
1509
|
+
const cached = depthById.get(key)
|
|
1510
|
+
if (cached !== undefined) { depth = cached; break }
|
|
1511
|
+
if (visiting.has(key)) break
|
|
1512
|
+
const context = getSpanContext(traceId, current)
|
|
1513
|
+
if (!context?.parentSpanId) { depthById.set(key, 0); break }
|
|
1514
|
+
visiting.add(key)
|
|
1515
|
+
path.push(key)
|
|
1516
|
+
current = context.parentSpanId
|
|
1614
1517
|
}
|
|
1615
|
-
|
|
1616
|
-
|
|
1518
|
+
for (const key of path.reverse()) depthById.set(key, ++depth)
|
|
1519
|
+
return depthById.get(keyOf(traceId, spanId)) ?? 0
|
|
1617
1520
|
}
|
|
1618
1521
|
|
|
1619
|
-
if (filteredLite.length === 0) return [] as readonly SpanItem[]
|
|
1620
|
-
|
|
1621
1522
|
// Hydrate only the kept rows: one batched fetch of the full
|
|
1622
1523
|
// SpanRow (with resource_json / attributes_json / events_json)
|
|
1623
1524
|
// using SQLite's row-value `IN` syntax, then parseSpanRow per
|
|
@@ -1625,7 +1526,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
1625
1526
|
// sees the same ordering the candidate scan produced.
|
|
1626
1527
|
const keptValues = filteredLite.map(() => "(?, ?)").join(", ")
|
|
1627
1528
|
const fullRows = db.query(`
|
|
1628
|
-
SELECT * FROM
|
|
1529
|
+
SELECT * FROM ${spanSource} WHERE (trace_id, span_id) IN (VALUES ${keptValues})
|
|
1629
1530
|
`).all(...filteredLite.flatMap((row) => [row.trace_id, row.span_id])) as SpanRow[]
|
|
1630
1531
|
const fullRowByKey = new Map<string, SpanRow>()
|
|
1631
1532
|
for (const row of fullRows) {
|
|
@@ -1718,7 +1619,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
1718
1619
|
const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : ""
|
|
1719
1620
|
const limit = input.limit ?? config.otel.logFetchLimit
|
|
1720
1621
|
const rows = db.query(`
|
|
1721
|
-
SELECT * FROM
|
|
1622
|
+
SELECT * FROM ${logSource}
|
|
1722
1623
|
${where}
|
|
1723
1624
|
ORDER BY timestamp_ms DESC, id DESC
|
|
1724
1625
|
LIMIT ?
|
|
@@ -1832,7 +1733,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
1832
1733
|
if (input.agg === "p95_duration") {
|
|
1833
1734
|
const rows = db.query(`
|
|
1834
1735
|
SELECT ${groupExpr} AS grp, duration_ms
|
|
1835
|
-
FROM
|
|
1736
|
+
FROM ${traceSource}
|
|
1836
1737
|
WHERE ${whereClauses.join(" AND ")}
|
|
1837
1738
|
`).all(...whereParams) as Array<{ grp: string; duration_ms: number }>
|
|
1838
1739
|
|
|
@@ -1851,7 +1752,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
1851
1752
|
|
|
1852
1753
|
const rows = db.query(`
|
|
1853
1754
|
SELECT ${groupExpr} AS grp, ${aggExpr} AS value, COUNT(*) AS count
|
|
1854
|
-
FROM
|
|
1755
|
+
FROM ${traceSource}
|
|
1855
1756
|
WHERE ${whereClauses.join(" AND ")}
|
|
1856
1757
|
GROUP BY grp
|
|
1857
1758
|
ORDER BY value DESC
|
|
@@ -1938,7 +1839,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
1938
1839
|
|
|
1939
1840
|
const rows = db.query(`
|
|
1940
1841
|
SELECT ${groupExpr} AS grp, COUNT(*) AS count
|
|
1941
|
-
FROM
|
|
1842
|
+
FROM ${logSource}
|
|
1942
1843
|
${where}
|
|
1943
1844
|
GROUP BY grp
|
|
1944
1845
|
ORDER BY count DESC
|
|
@@ -1965,7 +1866,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
1965
1866
|
if (input.field === "service") {
|
|
1966
1867
|
const rows = db.query(`
|
|
1967
1868
|
SELECT service_name AS value, COUNT(*) AS count
|
|
1968
|
-
FROM
|
|
1869
|
+
FROM ${logSource}
|
|
1969
1870
|
WHERE timestamp_ms >= ?
|
|
1970
1871
|
GROUP BY service_name
|
|
1971
1872
|
ORDER BY count DESC, value ASC
|
|
@@ -1976,7 +1877,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
1976
1877
|
if (input.field === "severity") {
|
|
1977
1878
|
const rows = db.query(`
|
|
1978
1879
|
SELECT UPPER(severity_text) AS value, COUNT(*) AS count
|
|
1979
|
-
FROM
|
|
1880
|
+
FROM ${logSource}
|
|
1980
1881
|
WHERE timestamp_ms >= ?
|
|
1981
1882
|
${input.serviceName ? "AND service_name = ?" : ""}
|
|
1982
1883
|
GROUP BY value
|
|
@@ -1988,7 +1889,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
1988
1889
|
if (input.field === "scope") {
|
|
1989
1890
|
const rows = db.query(`
|
|
1990
1891
|
SELECT COALESCE(scope_name, 'unknown') AS value, COUNT(*) AS count
|
|
1991
|
-
FROM
|
|
1892
|
+
FROM ${logSource}
|
|
1992
1893
|
WHERE timestamp_ms >= ?
|
|
1993
1894
|
${input.serviceName ? "AND service_name = ?" : ""}
|
|
1994
1895
|
GROUP BY COALESCE(scope_name, 'unknown')
|
|
@@ -2003,7 +1904,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
2003
1904
|
if (input.field === "service") {
|
|
2004
1905
|
const rows = db.query(`
|
|
2005
1906
|
SELECT service_name AS value, COUNT(*) AS count
|
|
2006
|
-
FROM
|
|
1907
|
+
FROM ${traceSource}
|
|
2007
1908
|
WHERE started_at_ms >= ?
|
|
2008
1909
|
GROUP BY service_name
|
|
2009
1910
|
ORDER BY count DESC, value ASC
|
|
@@ -2014,7 +1915,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
2014
1915
|
if (input.field === "operation") {
|
|
2015
1916
|
const rows = db.query(`
|
|
2016
1917
|
SELECT root_operation_name AS value, COUNT(*) AS count
|
|
2017
|
-
FROM
|
|
1918
|
+
FROM ${traceSource}
|
|
2018
1919
|
WHERE started_at_ms >= ?
|
|
2019
1920
|
${input.serviceName ? "AND service_name = ?" : ""}
|
|
2020
1921
|
GROUP BY root_operation_name
|
|
@@ -2026,7 +1927,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
2026
1927
|
if (input.field === "status") {
|
|
2027
1928
|
const rows = db.query(`
|
|
2028
1929
|
SELECT CASE WHEN error_count > 0 THEN 'error' ELSE 'ok' END AS value, COUNT(*) AS count
|
|
2029
|
-
FROM
|
|
1930
|
+
FROM ${traceSource}
|
|
2030
1931
|
WHERE started_at_ms >= ?
|
|
2031
1932
|
${input.serviceName ? "AND service_name = ?" : ""}
|
|
2032
1933
|
GROUP BY value
|
|
@@ -2055,10 +1956,10 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
2055
1956
|
const params: Array<string | number> = []
|
|
2056
1957
|
let traceFilter: string
|
|
2057
1958
|
if (input.serviceName) {
|
|
2058
|
-
traceFilter = `(SELECT trace_id FROM
|
|
1959
|
+
traceFilter = `(SELECT trace_id FROM ${traceSource} WHERE started_at_ms >= ? AND service_name = ?)`
|
|
2059
1960
|
params.push(cutoff, input.serviceName)
|
|
2060
1961
|
} else {
|
|
2061
|
-
traceFilter = `(SELECT trace_id FROM
|
|
1962
|
+
traceFilter = `(SELECT trace_id FROM ${traceSource} WHERE started_at_ms >= ?)`
|
|
2062
1963
|
params.push(cutoff)
|
|
2063
1964
|
}
|
|
2064
1965
|
params.push(FACET_VALUE_MAX_LEN, limit)
|
|
@@ -2089,7 +1990,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
2089
1990
|
const rows = db.query(`
|
|
2090
1991
|
SELECT sa.value AS value, COUNT(DISTINCT sa.trace_id) AS count
|
|
2091
1992
|
FROM span_attributes sa
|
|
2092
|
-
JOIN
|
|
1993
|
+
JOIN ${spanSource} s ON s.trace_id = sa.trace_id AND s.span_id = sa.span_id
|
|
2093
1994
|
WHERE sa.key = ? AND LENGTH(sa.value) < ?
|
|
2094
1995
|
AND s.start_time_ms >= ?
|
|
2095
1996
|
${input.serviceName ? "AND s.service_name = ?" : ""}
|
|
@@ -2226,7 +2127,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
2226
2127
|
|
|
2227
2128
|
const rows = db.query(`
|
|
2228
2129
|
SELECT s.trace_id, s.span_id, s.service_name, s.operation_name, s.start_time_ms, s.duration_ms, s.status
|
|
2229
|
-
FROM
|
|
2130
|
+
FROM ${spanSource} AS s
|
|
2230
2131
|
WHERE ${clauses.join(" AND ")}
|
|
2231
2132
|
ORDER BY s.start_time_ms DESC
|
|
2232
2133
|
LIMIT ?
|
|
@@ -2252,7 +2153,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
2252
2153
|
const spanParams = rows.flatMap((r) => [r.trace_id, r.span_id])
|
|
2253
2154
|
const toolCountRows = db.query(`
|
|
2254
2155
|
SELECT parent_span_id, COUNT(*) AS cnt
|
|
2255
|
-
FROM
|
|
2156
|
+
FROM ${spanSource}
|
|
2256
2157
|
WHERE (trace_id, parent_span_id) IN (VALUES ${spanPlaceholders})
|
|
2257
2158
|
AND operation_name LIKE 'ai.toolCall%'
|
|
2258
2159
|
GROUP BY trace_id, parent_span_id
|
|
@@ -2302,7 +2203,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
2302
2203
|
const getAiCall = Effect.fn("motel/TelemetryStore.getAiCall")(function* (spanId: string) {
|
|
2303
2204
|
return yield* Effect.sync(() => {
|
|
2304
2205
|
const row = db.query(`
|
|
2305
|
-
SELECT * FROM
|
|
2206
|
+
SELECT * FROM ${spanSource} WHERE span_id = ? AND operation_name LIKE 'ai.%' LIMIT 1
|
|
2306
2207
|
`).get(spanId) as SpanRow | null
|
|
2307
2208
|
if (!row) return null
|
|
2308
2209
|
|
|
@@ -2321,7 +2222,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
2321
2222
|
// Load tool call child spans
|
|
2322
2223
|
const toolCallRows = db.query(`
|
|
2323
2224
|
SELECT span_id, operation_name, duration_ms, status, attributes_json
|
|
2324
|
-
FROM
|
|
2225
|
+
FROM ${spanSource}
|
|
2325
2226
|
WHERE trace_id = ? AND parent_span_id = ? AND operation_name LIKE 'ai.toolCall%'
|
|
2326
2227
|
ORDER BY start_time_ms ASC
|
|
2327
2228
|
`).all(row.trace_id, row.span_id) as SpanRow[]
|
|
@@ -2338,7 +2239,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
2338
2239
|
|
|
2339
2240
|
// Load correlated logs
|
|
2340
2241
|
const logRows = db.query(`
|
|
2341
|
-
SELECT * FROM
|
|
2242
|
+
SELECT * FROM ${logSource} WHERE span_id = ? ORDER BY timestamp_ms ASC
|
|
2342
2243
|
`).all(row.span_id) as LogRow[]
|
|
2343
2244
|
const logs = logRows.map(parseLogRow)
|
|
2344
2245
|
|
|
@@ -2410,7 +2311,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
2410
2311
|
if (input.groupBy === "status") {
|
|
2411
2312
|
const rows = db.query(`
|
|
2412
2313
|
SELECT s.status AS grp, COUNT(*) AS count, AVG(s.duration_ms) AS avg_dur
|
|
2413
|
-
FROM
|
|
2314
|
+
FROM ${spanSource} AS s
|
|
2414
2315
|
WHERE ${clauses.join(" AND ")}
|
|
2415
2316
|
GROUP BY s.status
|
|
2416
2317
|
ORDER BY count DESC
|
|
@@ -2437,7 +2338,7 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
2437
2338
|
s.span_id,
|
|
2438
2339
|
s.duration_ms,
|
|
2439
2340
|
s.status
|
|
2440
|
-
FROM
|
|
2341
|
+
FROM ${spanSource} AS s
|
|
2441
2342
|
LEFT JOIN span_attributes AS ga
|
|
2442
2343
|
ON ga.trace_id = s.trace_id AND ga.span_id = s.span_id AND ga.key = ?
|
|
2443
2344
|
WHERE ${clauses.join(" AND ")}
|