@aryasaatvik/motel 0.5.1 → 0.5.2
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 +1 -1
- package/src/services/TelemetryStore.ts +23 -10
- package/src/services/retention.test.ts +150 -1
- package/src/services/retention.ts +44 -1
- package/src/telemetry.test.ts +224 -0
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@ 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"
|
|
5
|
+
import { installRetentionSchema, checkpointPassive, mergeFts, retainBatch, repairSearchRows, SPAN_SEARCH_MAPPING_READY_KEY } from "./retention.js"
|
|
6
6
|
import { WriterDiagnostics } from "../ingestReadiness.ts"
|
|
7
7
|
import { config } from "../config.js"
|
|
8
8
|
import type { AiCallDetail, AiCallSummary, FacetItem, LogItem, SpanItem, StatsItem, TraceItem, TraceSummaryItem, TraceSpanEvent, TraceSpanItem } from "../domain.js"
|
|
@@ -872,14 +872,21 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
872
872
|
query.run(...entries.flatMap(([key, value]) => [traceId, spanId, key, value]))
|
|
873
873
|
}
|
|
874
874
|
const deleteSpanOperationSearch = db.query(`DELETE FROM span_operation_fts WHERE trace_id = ? AND span_id = ?`)
|
|
875
|
+
const spanSearchMappingReady = hasRetention ? db.query("SELECT 1 FROM motel_maintenance WHERE key = ? AND value = 'complete'") : null
|
|
876
|
+
const deleteMappedSpanOperationSearch = hasRetention ? db.query(`DELETE FROM span_operation_fts WHERE rowid IN (
|
|
877
|
+
SELECT id FROM span_search_rows WHERE trace_id = ? AND span_id = ?
|
|
878
|
+
)`) : null
|
|
875
879
|
const insertSpanOperationSearch = db.query(`INSERT INTO span_operation_fts (trace_id, span_id, operation_name) VALUES (?, ?, ?)`)
|
|
876
880
|
const deleteSpanOperationSearchManyByCount = new Map<number, ReturnType<Database["query"]>>()
|
|
877
881
|
const insertSpanOperationSearchManyByCount = new Map<number, ReturnType<Database["query"]>>()
|
|
878
882
|
const updateSpanOperationSearchMany = (operations: ReadonlyArray<readonly [string, string, string]>) => {
|
|
879
883
|
if (operations.length === 0) return
|
|
884
|
+
const mappedDelete = spanSearchMappingReady !== null && deleteMappedSpanOperationSearch !== null
|
|
885
|
+
&& spanSearchMappingReady.get(SPAN_SEARCH_MAPPING_READY_KEY) !== null ? deleteMappedSpanOperationSearch : null
|
|
880
886
|
if (operations.length === 1) {
|
|
881
887
|
const [traceId, spanId, operationName] = operations[0]!
|
|
882
|
-
|
|
888
|
+
if (mappedDelete !== null) mappedDelete.run(traceId, spanId)
|
|
889
|
+
else deleteSpanOperationSearch.run(traceId, spanId)
|
|
883
890
|
db.query("DELETE FROM span_search_rows WHERE trace_id = ? AND span_id = ?").run(traceId, spanId)
|
|
884
891
|
const result = insertSpanOperationSearch.run(traceId, spanId, operationName)
|
|
885
892
|
mapSpanSearch?.run(Number(result.lastInsertRowid), traceId, spanId)
|
|
@@ -891,7 +898,9 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
891
898
|
deleteQuery = db.query(`DELETE FROM span_operation_fts WHERE ${operations.map(() => "(trace_id = ? AND span_id = ?)").join(" OR ")}`)
|
|
892
899
|
deleteSpanOperationSearchManyByCount.set(operations.length, deleteQuery)
|
|
893
900
|
}
|
|
894
|
-
|
|
901
|
+
if (mappedDelete !== null) {
|
|
902
|
+
for (const [traceId, spanId] of operations) mappedDelete.run(traceId, spanId)
|
|
903
|
+
} else deleteQuery.run(...operations.flatMap(([traceId, spanId]) => [traceId, spanId]))
|
|
895
904
|
for (const [traceId, spanId] of operations) db.query("DELETE FROM span_search_rows WHERE trace_id = ? AND span_id = ?").run(traceId, spanId)
|
|
896
905
|
|
|
897
906
|
let insertQuery = insertSpanOperationSearchManyByCount.get(operations.length)
|
|
@@ -904,6 +913,13 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
904
913
|
const firstId = Number(result.lastInsertRowid) - operations.length + 1
|
|
905
914
|
for (const [index, [traceId, spanId]] of operations.entries()) mapSpanSearch?.run(firstId + index, traceId, spanId)
|
|
906
915
|
}
|
|
916
|
+
// An optional search-index failure must leave every FTS row mapped.
|
|
917
|
+
const updateSpanOperationSearch = db.transaction((operations: Parameters<typeof updateSpanOperationSearchMany>[0]) => {
|
|
918
|
+
const BATCH_SIZE = 500
|
|
919
|
+
for (let offset = 0; offset < operations.length; offset += BATCH_SIZE) {
|
|
920
|
+
updateSpanOperationSearchMany(operations.slice(offset, offset + BATCH_SIZE))
|
|
921
|
+
}
|
|
922
|
+
})
|
|
907
923
|
const insertLogAttribute = db.query(`INSERT INTO log_attributes (log_id, key, value) VALUES (?, ?, ?)`)
|
|
908
924
|
const logAttributeInsertManyByCount = new Map<number, ReturnType<Database["query"]>>()
|
|
909
925
|
const insertLogAttributesMany = (logId: number, attributes: Readonly<Record<string, string>>) => {
|
|
@@ -979,8 +995,8 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
979
995
|
logs: config.otel.retentionLogBatch,
|
|
980
996
|
rows: config.otel.retentionRowBatch,
|
|
981
997
|
})
|
|
982
|
-
|
|
983
|
-
|
|
998
|
+
const spanMappingPending = repairSearchRows(db, Math.min(100, config.otel.retentionRowBatch))
|
|
999
|
+
maintenancePending = (result.pending && result.rows > 0) || spanMappingPending
|
|
984
1000
|
mergeFts(db, 100)
|
|
985
1001
|
checkpoint()
|
|
986
1002
|
})
|
|
@@ -1114,12 +1130,9 @@ const makeTelemetryStoreEffect = (opts: TelemetryStoreOptions) =>
|
|
|
1114
1130
|
}
|
|
1115
1131
|
}
|
|
1116
1132
|
try {
|
|
1117
|
-
|
|
1118
|
-
for (let offset = 0; offset < touchedOperations.length; offset += BATCH_SIZE) {
|
|
1119
|
-
updateSpanOperationSearchMany(touchedOperations.slice(offset, offset + BATCH_SIZE))
|
|
1120
|
-
}
|
|
1133
|
+
updateSpanOperationSearch(touchedOperations)
|
|
1121
1134
|
} catch {
|
|
1122
|
-
// FTS is optional.
|
|
1135
|
+
// FTS is optional; its nested transaction restores the index and mapping together.
|
|
1123
1136
|
}
|
|
1124
1137
|
for (const traceId of touchedTraceIds) {
|
|
1125
1138
|
upsertTraceSummary.run(traceId)
|
|
@@ -3,7 +3,7 @@ import { expect, test } from "bun:test"
|
|
|
3
3
|
import { mkdtempSync, rmSync } from "node:fs"
|
|
4
4
|
import { tmpdir } from "node:os"
|
|
5
5
|
import { join } from "node:path"
|
|
6
|
-
import { checkpointPassive, installRetentionSchema, mergeFts, repairSearchRows, retainBatch } from "./retention.ts"
|
|
6
|
+
import { checkpointPassive, installRetentionSchema, mergeFts, repairSearchRows, retainBatch, SPAN_SEARCH_MAPPING_READY_KEY } from "./retention.ts"
|
|
7
7
|
|
|
8
8
|
const schema = (db: Database) => {
|
|
9
9
|
db.exec(`PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 15000;
|
|
@@ -168,3 +168,152 @@ test("stale upgraded summaries cannot evict active spans under age or size reten
|
|
|
168
168
|
} finally { db.close() }
|
|
169
169
|
}
|
|
170
170
|
})
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
test("operation mapping readiness waits for complete legacy repair and stays idle after reopen", () => {
|
|
174
|
+
const root = mkdtempSync(join(tmpdir(), "motel-mapping-ready-"))
|
|
175
|
+
const path = join(root, "test.sqlite")
|
|
176
|
+
let db = new Database(path)
|
|
177
|
+
try {
|
|
178
|
+
schema(db)
|
|
179
|
+
expect(db.query("SELECT value FROM motel_maintenance WHERE key = ?").get(SPAN_SEARCH_MAPPING_READY_KEY)).toEqual({ value: "complete" })
|
|
180
|
+
db.query("DELETE FROM motel_maintenance WHERE key = ?").run(SPAN_SEARCH_MAPPING_READY_KEY)
|
|
181
|
+
db.exec("INSERT INTO spans(trace_id,span_id) VALUES ('a','s'), ('b','s'); INSERT INTO span_operation_fts(trace_id,span_id,operation_name) VALUES ('a','s','alpha'), ('b','s','beta'), ('orphan','s','orphan')")
|
|
182
|
+
// All three unmapped historical rows are below the pre-upgrade durable cursor.
|
|
183
|
+
db.exec("INSERT OR REPLACE INTO motel_maintenance VALUES ('retention_repair_span_operation_fts', '999')")
|
|
184
|
+
db.exec("CREATE TRIGGER refuse_mapping_marker BEFORE INSERT ON motel_maintenance WHEN NEW.key = 'span_search_mapping_complete' BEGIN SELECT RAISE(ABORT, 'marker failure'); END")
|
|
185
|
+
expect(() => installRetentionSchema(db)).toThrow("marker failure")
|
|
186
|
+
expect(db.query("SELECT value FROM motel_maintenance WHERE key = 'retention_repair_span_operation_fts'").get()).toEqual({ value: "999" })
|
|
187
|
+
expect(db.query("SELECT value FROM motel_maintenance WHERE key = ?").get(SPAN_SEARCH_MAPPING_READY_KEY)).toBeNull()
|
|
188
|
+
db.exec("DROP TRIGGER refuse_mapping_marker")
|
|
189
|
+
installRetentionSchema(db)
|
|
190
|
+
expect(db.query("SELECT value FROM motel_maintenance WHERE key = 'retention_repair_span_operation_fts'").get()).toBeNull()
|
|
191
|
+
expect(repairSearchRows(db, 1)).toBe(true)
|
|
192
|
+
expect(count(db, "span_search_rows")).toBe(1)
|
|
193
|
+
expect(db.query("SELECT value FROM motel_maintenance WHERE key = ?").get(SPAN_SEARCH_MAPPING_READY_KEY)).toEqual({ value: "repairing:3" })
|
|
194
|
+
db.close()
|
|
195
|
+
db = new Database(path)
|
|
196
|
+
installRetentionSchema(db)
|
|
197
|
+
expect(db.query("SELECT value FROM motel_maintenance WHERE key = 'retention_repair_span_operation_fts'").get()).toEqual({ value: "1" })
|
|
198
|
+
expect(repairSearchRows(db, 1)).toBe(true)
|
|
199
|
+
expect(count(db, "span_search_rows")).toBe(2)
|
|
200
|
+
expect(db.query("SELECT value FROM motel_maintenance WHERE key = ?").get(SPAN_SEARCH_MAPPING_READY_KEY)).toEqual({ value: "repairing:3" })
|
|
201
|
+
expect(repairSearchRows(db, 1)).toBe(false)
|
|
202
|
+
expect(count(db, "span_operation_fts")).toBe(2)
|
|
203
|
+
expect(db.query("SELECT value FROM motel_maintenance WHERE key = ?").get(SPAN_SEARCH_MAPPING_READY_KEY)).toEqual({ value: "complete" })
|
|
204
|
+
expect(repairSearchRows(db, 1)).toBe(false)
|
|
205
|
+
expect(db.query("SELECT value FROM motel_maintenance WHERE key = ?").get(SPAN_SEARCH_MAPPING_READY_KEY)).toEqual({ value: "complete" })
|
|
206
|
+
db.close()
|
|
207
|
+
db = new Database(path)
|
|
208
|
+
installRetentionSchema(db)
|
|
209
|
+
expect(db.query("SELECT value FROM motel_maintenance WHERE key = 'retention_repair_span_operation_fts'").get()).toEqual({ value: "3" })
|
|
210
|
+
expect(db.query("SELECT value FROM motel_maintenance WHERE key = ?").get(SPAN_SEARCH_MAPPING_READY_KEY)).toEqual({ value: "complete" })
|
|
211
|
+
const before = db.query("SELECT total_changes() AS n").get()
|
|
212
|
+
expect(repairSearchRows(db, 1)).toBe(false)
|
|
213
|
+
expect(db.query("SELECT total_changes() AS n").get()).toEqual(before)
|
|
214
|
+
} finally { db.close(); rmSync(root, { recursive: true, force: true }) }
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
test("a final partial span repair returns idle despite remaining log repair", () => {
|
|
219
|
+
const db = new Database(":memory:")
|
|
220
|
+
try {
|
|
221
|
+
schema(db)
|
|
222
|
+
db.query("DELETE FROM motel_maintenance WHERE key = ?").run(SPAN_SEARCH_MAPPING_READY_KEY)
|
|
223
|
+
db.exec("INSERT INTO spans(trace_id, span_id) VALUES ('a', 's'), ('b', 's'); INSERT INTO span_operation_fts(trace_id, span_id, operation_name) VALUES ('a', 's', 'alpha'), ('b', 's', 'beta')")
|
|
224
|
+
for (let id = 1; id <= 7; id++) {
|
|
225
|
+
db.query("INSERT INTO logs VALUES (?, NULL, 1000, 'INFO')").run(id)
|
|
226
|
+
db.query("INSERT INTO log_attributes VALUES (?, 'key', 'value')").run(id)
|
|
227
|
+
db.query("INSERT INTO log_body_fts(log_id, body) VALUES (?, 'searchable')").run(String(id))
|
|
228
|
+
}
|
|
229
|
+
installRetentionSchema(db)
|
|
230
|
+
expect(repairSearchRows(db, 3)).toBe(false)
|
|
231
|
+
expect(count(db, "span_search_rows")).toBe(2)
|
|
232
|
+
expect(db.query("SELECT value FROM motel_maintenance WHERE key = ?").get(SPAN_SEARCH_MAPPING_READY_KEY)).toEqual({ value: "complete" })
|
|
233
|
+
expect(count(db, "log_body_fts")).toBe(7)
|
|
234
|
+
expect(count(db, "log_search_rows")).toBe(3)
|
|
235
|
+
} finally { db.close() }
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
test("an unavailable optional span index does not request fast repair retries", () => {
|
|
239
|
+
const db = new Database(":memory:")
|
|
240
|
+
try {
|
|
241
|
+
schema(db)
|
|
242
|
+
db.query("UPDATE motel_maintenance SET value = 'repairing:3' WHERE key = ?").run(SPAN_SEARCH_MAPPING_READY_KEY)
|
|
243
|
+
db.exec("DROP TABLE span_operation_fts")
|
|
244
|
+
expect(repairSearchRows(db, 3)).toBe(false)
|
|
245
|
+
expect(db.query("SELECT value FROM motel_maintenance WHERE key = ?").get(SPAN_SEARCH_MAPPING_READY_KEY)).toEqual({ value: "repairing:3" })
|
|
246
|
+
} finally { db.close() }
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
test("legacy span mapping finishes its fixed range despite mapped arrivals and reopen", () => {
|
|
251
|
+
const root = mkdtempSync(join(tmpdir(), "motel-mapping-target-"))
|
|
252
|
+
const path = join(root, "test.sqlite")
|
|
253
|
+
let db = new Database(path)
|
|
254
|
+
const state = () => db.query("SELECT value FROM motel_maintenance WHERE key = ?").get(SPAN_SEARCH_MAPPING_READY_KEY)
|
|
255
|
+
const append = (id: number, mapped: boolean) => {
|
|
256
|
+
db.query("INSERT INTO spans(trace_id, span_id) VALUES (?, 's')").run(String(id))
|
|
257
|
+
db.query("INSERT INTO span_operation_fts(rowid, trace_id, span_id, operation_name) VALUES (?, ?, 's', 'operation')").run(id, String(id))
|
|
258
|
+
if (mapped) db.query("INSERT INTO span_search_rows VALUES (?, ?, 's')").run(id, String(id))
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
schema(db)
|
|
262
|
+
db.query("DELETE FROM motel_maintenance WHERE key = ?").run(SPAN_SEARCH_MAPPING_READY_KEY)
|
|
263
|
+
for (let id = 1; id <= 4; id++) append(id, false)
|
|
264
|
+
db.exec("INSERT OR REPLACE INTO motel_maintenance VALUES ('retention_repair_span_operation_fts', '999')")
|
|
265
|
+
installRetentionSchema(db)
|
|
266
|
+
const captured = state()
|
|
267
|
+
expect(repairSearchRows(db, 2)).toBe(true)
|
|
268
|
+
for (let id = 5; id <= 8; id++) append(id, true)
|
|
269
|
+
db.close()
|
|
270
|
+
db = new Database(path)
|
|
271
|
+
installRetentionSchema(db)
|
|
272
|
+
expect(state()).toEqual(captured)
|
|
273
|
+
// Complete the exact full-sized final batch without scanning the mapped arrivals.
|
|
274
|
+
expect(repairSearchRows(db, 2)).toBe(false)
|
|
275
|
+
expect(state()).toEqual({ value: "complete" })
|
|
276
|
+
expect(captured).toEqual({ value: "repairing:4" })
|
|
277
|
+
expect(count(db, "span_search_rows")).toBe(8)
|
|
278
|
+
expect(db.query("SELECT value FROM motel_maintenance WHERE key = 'retention_repair_span_operation_fts'").get()).toEqual({ value: "4" })
|
|
279
|
+
// Ordinary incremental repair still visits later rows, without requesting fast cadence.
|
|
280
|
+
expect(repairSearchRows(db, 2)).toBe(false)
|
|
281
|
+
expect(db.query("SELECT value FROM motel_maintenance WHERE key = 'retention_repair_span_operation_fts'").get()).toEqual({ value: "6" })
|
|
282
|
+
expect(repairSearchRows(db, 2)).toBe(false)
|
|
283
|
+
const before = db.query("SELECT total_changes() AS n").get()
|
|
284
|
+
expect(repairSearchRows(db, 2)).toBe(false)
|
|
285
|
+
expect(db.query("SELECT total_changes() AS n").get()).toEqual(before)
|
|
286
|
+
} finally { db.close(); rmSync(root, { recursive: true, force: true }) }
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
test("fixed-range repair rolls back failures and completes after its target row is removed", () => {
|
|
290
|
+
const db = new Database(":memory:")
|
|
291
|
+
try {
|
|
292
|
+
schema(db)
|
|
293
|
+
db.query("DELETE FROM motel_maintenance WHERE key = ?").run(SPAN_SEARCH_MAPPING_READY_KEY)
|
|
294
|
+
db.exec("INSERT INTO spans(trace_id,span_id) VALUES ('a','s'),('b','s'); INSERT INTO span_operation_fts(trace_id,span_id,operation_name) VALUES ('a','s','alpha'),('b','s','beta')")
|
|
295
|
+
installRetentionSchema(db)
|
|
296
|
+
expect(repairSearchRows(db, 1)).toBe(true)
|
|
297
|
+
db.exec("CREATE TRIGGER refuse_repair BEFORE INSERT ON span_search_rows WHEN NEW.trace_id = 'b' BEGIN SELECT RAISE(ABORT, 'repair failure'); END")
|
|
298
|
+
expect(() => repairSearchRows(db, 1)).toThrow("repair failure")
|
|
299
|
+
expect(db.query("SELECT value FROM motel_maintenance WHERE key = ?").get(SPAN_SEARCH_MAPPING_READY_KEY)).toEqual({ value: "repairing:2" })
|
|
300
|
+
expect(db.query("SELECT value FROM motel_maintenance WHERE key = 'retention_repair_span_operation_fts'").get()).toEqual({ value: "1" })
|
|
301
|
+
db.exec("DROP TRIGGER refuse_repair; DELETE FROM span_operation_fts WHERE rowid = 2")
|
|
302
|
+
expect(repairSearchRows(db, 1)).toBe(false)
|
|
303
|
+
expect(db.query("SELECT value FROM motel_maintenance WHERE key = ?").get(SPAN_SEARCH_MAPPING_READY_KEY)).toEqual({ value: "complete" })
|
|
304
|
+
} finally { db.close() }
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
test("malformed persistent span migration state fails without resetting its cursor", () => {
|
|
308
|
+
for (const state of ["repairing", "repairing:0", "repairing:1.5", "repairing:9007199254740992", "unknown"]) {
|
|
309
|
+
const db = new Database(":memory:")
|
|
310
|
+
try {
|
|
311
|
+
schema(db)
|
|
312
|
+
db.query("UPDATE motel_maintenance SET value = ? WHERE key = ?").run(state, SPAN_SEARCH_MAPPING_READY_KEY)
|
|
313
|
+
db.exec("INSERT OR REPLACE INTO motel_maintenance VALUES ('retention_repair_span_operation_fts', '7')")
|
|
314
|
+
expect(() => installRetentionSchema(db)).toThrow("Invalid span search mapping migration state")
|
|
315
|
+
expect(() => repairSearchRows(db, 1)).toThrow("Invalid span search mapping migration state")
|
|
316
|
+
expect(db.query("SELECT value FROM motel_maintenance WHERE key = 'retention_repair_span_operation_fts'").get()).toEqual({ value: "7" })
|
|
317
|
+
} finally { db.close() }
|
|
318
|
+
}
|
|
319
|
+
})
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite"
|
|
2
2
|
|
|
3
|
+
export const SPAN_SEARCH_MAPPING_READY_KEY = "span_search_mapping_complete"
|
|
4
|
+
|
|
5
|
+
const spanMappingTarget = (state: string): number | null => {
|
|
6
|
+
if (state === "complete") return null
|
|
7
|
+
const match = /^repairing:([1-9][0-9]*)$/.exec(state)
|
|
8
|
+
const target = match === null ? NaN : Number(match[1])
|
|
9
|
+
if (!Number.isSafeInteger(target)) throw new Error("Invalid span search mapping migration state")
|
|
10
|
+
return target
|
|
11
|
+
}
|
|
12
|
+
|
|
3
13
|
/** Additive schema: unfinished eviction is hidden from every public query. */
|
|
4
14
|
export const installRetentionSchema = (db: Database) => {
|
|
5
15
|
db.exec(`
|
|
@@ -22,6 +32,21 @@ export const installRetentionSchema = (db: Database) => {
|
|
|
22
32
|
WHERE NOT EXISTS (SELECT 1 FROM retention_traces d WHERE d.trace_id = logs.trace_id)
|
|
23
33
|
AND NOT EXISTS (SELECT 1 FROM retention_logs d WHERE d.id = logs.id);
|
|
24
34
|
`)
|
|
35
|
+
if (db.query("SELECT 1 FROM sqlite_master WHERE name = 'span_operation_fts' AND type = 'table'").get()) {
|
|
36
|
+
db.transaction(() => {
|
|
37
|
+
const marker = db.query("SELECT value FROM motel_maintenance WHERE key = ?").get(SPAN_SEARCH_MAPPING_READY_KEY) as { value: string } | null
|
|
38
|
+
if (marker !== null) {
|
|
39
|
+
spanMappingTarget(marker.value)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
// Earlier optional index failures can strand unmapped rows behind the old cursor.
|
|
43
|
+
db.query("DELETE FROM motel_maintenance WHERE key = 'retention_repair_span_operation_fts'").run()
|
|
44
|
+
const last = db.query("SELECT rowid AS target FROM span_operation_fts ORDER BY rowid DESC LIMIT 1").get() as { target: number } | null
|
|
45
|
+
const state = last === null ? "complete" : `repairing:${last.target}`
|
|
46
|
+
spanMappingTarget(state)
|
|
47
|
+
db.query("INSERT INTO motel_maintenance(key, value) VALUES (?, ?)").run(SPAN_SEARCH_MAPPING_READY_KEY, state)
|
|
48
|
+
}).immediate()
|
|
49
|
+
}
|
|
25
50
|
}
|
|
26
51
|
|
|
27
52
|
export type Checkpoint = { busy: number; log: number; checkpointed: number; deferred: boolean }
|
|
@@ -133,13 +158,23 @@ export const retainBatch = (db: Database, options: RetentionOptions) => db.trans
|
|
|
133
158
|
|
|
134
159
|
/** Bounded repair scans only rowids beyond its durable high-water mark; idle passes do not write. */
|
|
135
160
|
export const repairSearchRows = (db: Database, limit: number) => db.transaction(() => {
|
|
161
|
+
let spanMappingPending = false
|
|
136
162
|
for (const table of ["log_attributes", "log_body_fts", "span_operation_fts"] as const) {
|
|
137
163
|
if (!db.query("SELECT 1 FROM sqlite_master WHERE name = ?").get(table)) continue
|
|
138
164
|
const key = `retention_repair_${table}`
|
|
139
165
|
const marker = (db.query("SELECT value FROM motel_maintenance WHERE key = ?").get(key) as { value: string } | null)?.value
|
|
140
166
|
const cursor = Number(marker ?? 0)
|
|
141
167
|
const columns = table === "span_operation_fts" ? "trace_id, span_id" : "log_id"
|
|
142
|
-
|
|
168
|
+
let target: number | null = null
|
|
169
|
+
if (table === "span_operation_fts") {
|
|
170
|
+
const state = db.query("SELECT value FROM motel_maintenance WHERE key = ?").get(SPAN_SEARCH_MAPPING_READY_KEY) as { value: string } | null
|
|
171
|
+
if (state === null) throw new Error("Missing span search mapping migration state")
|
|
172
|
+
target = spanMappingTarget(state.value)
|
|
173
|
+
}
|
|
174
|
+
const rows = (target === null
|
|
175
|
+
? db.query(`SELECT rowid AS repair_id, ${columns} FROM ${table} WHERE rowid > ? ORDER BY rowid LIMIT ?`).all(cursor, limit)
|
|
176
|
+
: db.query(`SELECT rowid AS repair_id, ${columns} FROM ${table} WHERE rowid > ? AND rowid <= ? ORDER BY rowid LIMIT ?`).all(cursor, target, limit)
|
|
177
|
+
) as { repair_id: number; log_id?: number | string; trace_id?: string; span_id?: string }[]
|
|
143
178
|
for (const row of rows) {
|
|
144
179
|
const exists = table === "span_operation_fts"
|
|
145
180
|
? db.query("SELECT 1 FROM spans WHERE trace_id = ? AND span_id = ?").get(row.trace_id!, row.span_id!)
|
|
@@ -154,5 +189,13 @@ export const repairSearchRows = (db: Database, limit: number) => db.transaction(
|
|
|
154
189
|
}
|
|
155
190
|
}
|
|
156
191
|
if (rows.length > 0) db.query("INSERT OR REPLACE INTO motel_maintenance VALUES (?, ?)").run(key, String(rows.at(-1)!.repair_id))
|
|
192
|
+
if (target !== null) {
|
|
193
|
+
const nextCursor = rows.at(-1)?.repair_id ?? cursor
|
|
194
|
+
spanMappingPending = db.query("SELECT 1 FROM span_operation_fts WHERE rowid > ? AND rowid <= ? LIMIT 1").get(nextCursor, target) !== null
|
|
195
|
+
if (!spanMappingPending) {
|
|
196
|
+
db.query("UPDATE motel_maintenance SET value = 'complete' WHERE key = ?").run(SPAN_SEARCH_MAPPING_READY_KEY)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
157
199
|
}
|
|
200
|
+
return spanMappingPending
|
|
158
201
|
})()
|
package/src/telemetry.test.ts
CHANGED
|
@@ -407,6 +407,194 @@ describe("motel telemetry store", () => {
|
|
|
407
407
|
}
|
|
408
408
|
})
|
|
409
409
|
|
|
410
|
+
|
|
411
|
+
it("keeps operation search and mappings atomic across updates, legacy repair and ingest rollback", async () => {
|
|
412
|
+
const isolatedPath = join(tempDir, "operation-savepoint.sqlite")
|
|
413
|
+
const script = `
|
|
414
|
+
const assert = (await import('node:assert/strict')).default
|
|
415
|
+
const { Database } = await import('bun:sqlite')
|
|
416
|
+
const { Effect, ManagedRuntime } = await import('effect')
|
|
417
|
+
const { TelemetryStore, makeTelemetryStoreLayer } = await import('./src/services/TelemetryStore.ts')
|
|
418
|
+
const { repairSearchRows, installRetentionSchema, SPAN_SEARCH_MAPPING_READY_KEY } = await import('./src/services/retention.ts')
|
|
419
|
+
const runtime = ManagedRuntime.make(makeTelemetryStoreLayer({ readonly: false, runRetention: false }))
|
|
420
|
+
let db
|
|
421
|
+
const now = BigInt(Date.now()) * 1000000n
|
|
422
|
+
const span = (traceId, spanId, name) => ({ traceId, spanId, name, startTimeUnixNano: String(now), endTimeUnixNano: String(now + 1000000n) })
|
|
423
|
+
const ingest = (spans) => runtime.runPromise(Effect.flatMap(TelemetryStore, (store) => store.ingestTraces({ resourceSpans: [{ resource: { attributes: [{ key: 'service.name', value: { stringValue: 'fts-regression' } }] }, scopeSpans: [{ spans }] }] })))
|
|
424
|
+
const names = (traceId) => db.query('SELECT operation_name FROM span_operation_fts WHERE trace_id = ? ORDER BY operation_name').all(traceId).map((row) => row.operation_name)
|
|
425
|
+
const ready = () => db.query('SELECT value FROM motel_maintenance WHERE key = ?').get(SPAN_SEARCH_MAPPING_READY_KEY)?.value
|
|
426
|
+
const mappingCounts = () => db.query('SELECT (SELECT count(*) FROM span_operation_fts) AS fts, (SELECT count(*) FROM span_search_rows) AS mapping, (SELECT count(*) FROM span_operation_fts f LEFT JOIN span_search_rows m ON m.id = f.rowid WHERE m.id IS NULL) AS unmapped').get()
|
|
427
|
+
try {
|
|
428
|
+
await ingest([span('normal', 'root', 'oldoperation')])
|
|
429
|
+
db = new Database(process.env.MOTEL_OTEL_DB_PATH)
|
|
430
|
+
assert.equal(ready(), 'complete')
|
|
431
|
+
await ingest([span('normal', 'root', 'newoperation')])
|
|
432
|
+
assert.deepEqual(names('normal'), ['newoperation'])
|
|
433
|
+
assert.equal((await runtime.runPromise(Effect.flatMap(TelemetryStore, (store) => store.searchSpans({ operation: 'oldoperation' })))).length, 0)
|
|
434
|
+
assert.equal((await runtime.runPromise(Effect.flatMap(TelemetryStore, (store) => store.searchSpans({ operation: 'newoperation' })))).length, 1)
|
|
435
|
+
|
|
436
|
+
await ingest(Array.from({ length: 32 }, (_, i) => span('mixed-' + i, 'root', 'mixedold')))
|
|
437
|
+
await ingest(Array.from({ length: 64 }, (_, i) => span('mixed-' + i, 'root', 'mixednew')))
|
|
438
|
+
assert.equal(db.query("SELECT count(*) AS n FROM span_operation_fts WHERE operation_name = 'mixedold'").get().n, 0)
|
|
439
|
+
assert.equal(db.query("SELECT count(*) AS n FROM span_operation_fts WHERE operation_name = 'mixednew'").get().n, 64)
|
|
440
|
+
|
|
441
|
+
await ingest([span('duplicate', 'root', 'first'), span('duplicate', 'root', 'second')])
|
|
442
|
+
assert.deepEqual(names('duplicate'), ['first', 'second'])
|
|
443
|
+
const acrossBoundary = [span('duplicate', 'root', 'early')]
|
|
444
|
+
for (let i = 1; i < 499; i++) acrossBoundary.push(span('filler-' + i, 'root', 'filler'))
|
|
445
|
+
acrossBoundary.push(span('duplicate', 'root', 'middle'), span('duplicate', 'root', 'lateone'), span('duplicate', 'root', 'latetwo'))
|
|
446
|
+
await ingest(acrossBoundary)
|
|
447
|
+
assert.deepEqual(names('duplicate'), ['lateone', 'latetwo'])
|
|
448
|
+
assert.equal(db.query("SELECT operation_name FROM spans WHERE trace_id = 'duplicate' AND span_id = 'root'").get().operation_name, 'latetwo')
|
|
449
|
+
|
|
450
|
+
// Simulate an upgraded database with unmapped historical FTS rows.
|
|
451
|
+
db.query("DELETE FROM motel_maintenance WHERE key IN (?, 'retention_repair_span_operation_fts')").run(SPAN_SEARCH_MAPPING_READY_KEY)
|
|
452
|
+
db.exec("DELETE FROM span_search_rows WHERE trace_id = 'normal'")
|
|
453
|
+
db.query("INSERT OR REPLACE INTO motel_maintenance VALUES ('retention_repair_span_operation_fts', '999999')").run()
|
|
454
|
+
installRetentionSchema(db)
|
|
455
|
+
await ingest([span('normal', 'root', 'legacyupdated')])
|
|
456
|
+
assert.deepEqual(names('normal'), ['legacyupdated'])
|
|
457
|
+
assert.match(ready(), /^repairing:[1-9][0-9]*$/)
|
|
458
|
+
for (let pass = 0; ready() !== 'complete' && pass < 20; pass++) repairSearchRows(db, 100)
|
|
459
|
+
assert.equal(ready(), 'complete')
|
|
460
|
+
|
|
461
|
+
// Fail mapping after FTS insertion; every chunk of the optional unit must roll back.
|
|
462
|
+
const before = names('normal')
|
|
463
|
+
const beforeCounts = mappingCounts()
|
|
464
|
+
db.exec("CREATE TRIGGER reject_mapping BEFORE INSERT ON span_search_rows WHEN NEW.trace_id = 'normal' BEGIN SELECT RAISE(ABORT, 'mapping failure'); END")
|
|
465
|
+
const failedChunks = Array.from({ length: 500 }, (_, i) => span('failed-chunk-' + i, 'root', 'mustrollback'))
|
|
466
|
+
failedChunks.push(span('normal', 'root', 'committedspan'))
|
|
467
|
+
await ingest(failedChunks)
|
|
468
|
+
assert.deepEqual(names('normal'), before)
|
|
469
|
+
assert.deepEqual(mappingCounts(), beforeCounts)
|
|
470
|
+
assert.equal(db.query("SELECT count(*) AS n FROM span_operation_fts WHERE operation_name = 'mustrollback'").get().n, 0)
|
|
471
|
+
assert.equal(db.query("SELECT operation_name FROM spans WHERE trace_id = 'normal' AND span_id = 'root'").get().operation_name, 'committedspan')
|
|
472
|
+
assert.equal(ready(), 'complete')
|
|
473
|
+
db.exec('DROP TRIGGER reject_mapping')
|
|
474
|
+
await ingest([span('normal', 'root', 'recovered')])
|
|
475
|
+
assert.deepEqual(names('normal'), ['recovered'])
|
|
476
|
+
|
|
477
|
+
const stableNames = names('normal')
|
|
478
|
+
const stableCounts = mappingCounts()
|
|
479
|
+
db.exec("CREATE TRIGGER reject_summary BEFORE INSERT ON trace_summaries WHEN NEW.trace_id = 'normal' BEGIN SELECT RAISE(ABORT, 'summary failure'); END")
|
|
480
|
+
await assert.rejects(ingest([span('normal', 'root', 'outerrollback')]))
|
|
481
|
+
assert.deepEqual(names('normal'), stableNames)
|
|
482
|
+
assert.deepEqual(mappingCounts(), stableCounts)
|
|
483
|
+
assert.equal(db.query("SELECT operation_name FROM spans WHERE trace_id = 'normal' AND span_id = 'root'").get().operation_name, 'recovered')
|
|
484
|
+
assert.equal(ready(), 'complete')
|
|
485
|
+
db.exec('DROP TRIGGER reject_summary')
|
|
486
|
+
} finally {
|
|
487
|
+
db?.close()
|
|
488
|
+
await runtime.dispose()
|
|
489
|
+
}
|
|
490
|
+
`
|
|
491
|
+
const child = Bun.spawn({
|
|
492
|
+
cmd: [process.execPath, "--no-env-file", "-e", script], cwd: process.cwd(),
|
|
493
|
+
env: { ...process.env, MOTEL_OTEL_DB_PATH: isolatedPath, MOTEL_OTEL_ENABLED: "false" },
|
|
494
|
+
stdout: "ignore", stderr: "pipe",
|
|
495
|
+
})
|
|
496
|
+
const stderr = await new Response(child.stderr).text()
|
|
497
|
+
expect(await child.exited, stderr).toBe(0)
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
it("repairs legacy span mappings at the pending cadence and idles after completion", async () => {
|
|
502
|
+
const isolatedPath = join(tempDir, "mapping-repair-pacing.sqlite")
|
|
503
|
+
const script = `
|
|
504
|
+
const assert = (await import('node:assert/strict')).default
|
|
505
|
+
const { Database } = await import('bun:sqlite')
|
|
506
|
+
const { Clock, Deferred, Effect, Layer, ManagedRuntime } = await import('effect')
|
|
507
|
+
const { TestClock } = await import('effect/testing')
|
|
508
|
+
const { TelemetryStore, makeTelemetryStoreLayer } = await import('./src/services/TelemetryStore.ts')
|
|
509
|
+
const { WriterDiagnostics } = await import('./src/ingestReadiness.ts')
|
|
510
|
+
const { SPAN_SEARCH_MAPPING_READY_KEY } = await import('./src/services/retention.ts')
|
|
511
|
+
const now = Date.now()
|
|
512
|
+
const nanos = BigInt(now) * 1000000n
|
|
513
|
+
const seed = ManagedRuntime.make(makeTelemetryStoreLayer({ readonly: false, runRetention: false }))
|
|
514
|
+
try {
|
|
515
|
+
await seed.runPromise(Effect.flatMap(TelemetryStore, (store) => store.ingestTraces({
|
|
516
|
+
resourceSpans: [{
|
|
517
|
+
resource: { attributes: [{ key: 'service.name', value: { stringValue: 'mapping-pacing' } }] },
|
|
518
|
+
scopeSpans: [{ spans: Array.from({ length: 250 }, (_, id) => ({
|
|
519
|
+
traceId: 'pacing-' + id, spanId: 'root', name: 'legacyoperation',
|
|
520
|
+
startTimeUnixNano: String(nanos), endTimeUnixNano: String(nanos + 1000000n),
|
|
521
|
+
})) }],
|
|
522
|
+
}],
|
|
523
|
+
})))
|
|
524
|
+
} finally { await seed.dispose() }
|
|
525
|
+
const db = new Database(process.env.MOTEL_OTEL_DB_PATH)
|
|
526
|
+
try {
|
|
527
|
+
db.exec('DELETE FROM span_search_rows')
|
|
528
|
+
db.query('DELETE FROM motel_maintenance WHERE key = ?').run(SPAN_SEARCH_MAPPING_READY_KEY)
|
|
529
|
+
db.exec("INSERT OR REPLACE INTO motel_maintenance VALUES ('retention_repair_span_operation_fts', '999999')")
|
|
530
|
+
await Effect.runPromise(Effect.scoped(Effect.gen(function* () {
|
|
531
|
+
const clock = yield* TestClock.make()
|
|
532
|
+
yield* clock.setTime(now + 2)
|
|
533
|
+
const firstPass = Deferred.makeUnsafe()
|
|
534
|
+
const passes = []
|
|
535
|
+
const publish = (event) => {
|
|
536
|
+
if (event._tag !== 'maintenance' || event.value.operation !== 'retention' || event.value.outcome === 'running') return
|
|
537
|
+
passes.push({
|
|
538
|
+
outcome: event.value.outcome,
|
|
539
|
+
mapped: db.query('SELECT count(*) AS n FROM span_search_rows').get().n,
|
|
540
|
+
state: db.query('SELECT value FROM motel_maintenance WHERE key = ?').get(SPAN_SEARCH_MAPPING_READY_KEY)?.value,
|
|
541
|
+
})
|
|
542
|
+
Deferred.doneUnsafe(firstPass, Effect.void)
|
|
543
|
+
}
|
|
544
|
+
const runtime = ManagedRuntime.make(makeTelemetryStoreLayer({ readonly: false, runRetention: true }).pipe(
|
|
545
|
+
Layer.provide(Layer.succeed(Clock.Clock, clock)),
|
|
546
|
+
Layer.provide(Layer.succeed(WriterDiagnostics, publish)),
|
|
547
|
+
))
|
|
548
|
+
yield* Effect.addFinalizer(() => runtime.disposeEffect)
|
|
549
|
+
yield* Effect.promise(() => runtime.runPromise(Effect.asVoid(TelemetryStore)))
|
|
550
|
+
yield* Deferred.await(firstPass).pipe(Effect.timeout('5 seconds'))
|
|
551
|
+
assert.deepEqual(passes, [{ outcome: 'completed', mapped: 100, state: 'repairing:250' }])
|
|
552
|
+
yield* clock.adjust('100 millis')
|
|
553
|
+
assert.deepEqual(passes, [
|
|
554
|
+
{ outcome: 'completed', mapped: 100, state: 'repairing:250' },
|
|
555
|
+
{ outcome: 'completed', mapped: 200, state: 'repairing:250' },
|
|
556
|
+
])
|
|
557
|
+
yield* clock.adjust('100 millis')
|
|
558
|
+
assert.deepEqual(passes, [
|
|
559
|
+
{ outcome: 'completed', mapped: 100, state: 'repairing:250' },
|
|
560
|
+
{ outcome: 'completed', mapped: 200, state: 'repairing:250' },
|
|
561
|
+
{ outcome: 'completed', mapped: 250, state: 'complete' },
|
|
562
|
+
])
|
|
563
|
+
yield* clock.adjust('100 millis')
|
|
564
|
+
assert.equal(passes.length, 3)
|
|
565
|
+
yield* clock.adjust('9900 millis')
|
|
566
|
+
assert.deepEqual(passes.at(-1), { outcome: 'completed', mapped: 250, state: 'complete' })
|
|
567
|
+
assert.equal(passes.length, 4)
|
|
568
|
+
yield* runtime.disposeEffect
|
|
569
|
+
yield* clock.adjust('30 seconds')
|
|
570
|
+
assert.equal(passes.length, 4)
|
|
571
|
+
})))
|
|
572
|
+
} finally { db.close() }
|
|
573
|
+
`
|
|
574
|
+
const child = Bun.spawn({
|
|
575
|
+
cmd: [process.execPath, "--no-env-file", "-e", script], cwd: process.cwd(),
|
|
576
|
+
env: {
|
|
577
|
+
...process.env,
|
|
578
|
+
MOTEL_OTEL_DB_PATH: isolatedPath, MOTEL_OTEL_ENABLED: "false",
|
|
579
|
+
MOTEL_OTEL_RETENTION_ROW_BATCH: "1000", MOTEL_OTEL_RETENTION_INTERVAL_SECONDS: "10",
|
|
580
|
+
MOTEL_OTEL_RETENTION_TRACE_BATCH: "100", MOTEL_OTEL_RETENTION_HOURS: "168",
|
|
581
|
+
MOTEL_OTEL_MAX_DB_SIZE_MB: "1024",
|
|
582
|
+
},
|
|
583
|
+
stdout: "ignore", stderr: "pipe",
|
|
584
|
+
})
|
|
585
|
+
let timedOut = false
|
|
586
|
+
const deadline = setTimeout(() => { timedOut = true; child.kill() }, 15_000)
|
|
587
|
+
try {
|
|
588
|
+
const [exitCode, stderr] = await Promise.all([child.exited, new Response(child.stderr).text()])
|
|
589
|
+
expect(timedOut, stderr).toBe(false)
|
|
590
|
+
expect(exitCode, stderr).toBe(0)
|
|
591
|
+
} finally {
|
|
592
|
+
clearTimeout(deadline)
|
|
593
|
+
if (child.exitCode === null) child.kill()
|
|
594
|
+
await child.exited
|
|
595
|
+
}
|
|
596
|
+
}, 20_000)
|
|
597
|
+
|
|
410
598
|
it("incrementally backfills historical AI FTS content", async () => {
|
|
411
599
|
const historicalPath = join(tempDir, "historical-fts.sqlite")
|
|
412
600
|
const seedScript = `
|
|
@@ -468,6 +656,42 @@ describe("motel telemetry store", () => {
|
|
|
468
656
|
expect(result?.span.depth).toBe(1)
|
|
469
657
|
})
|
|
470
658
|
|
|
659
|
+
|
|
660
|
+
it("opens and searches a read-only pre-retention database without creating mappings", async () => {
|
|
661
|
+
const legacyPath = join(tempDir, "readonly-pre-retention.sqlite")
|
|
662
|
+
const script = `
|
|
663
|
+
const assert = (await import('node:assert/strict')).default
|
|
664
|
+
const { Database } = await import('bun:sqlite')
|
|
665
|
+
const { Effect, ManagedRuntime } = await import('effect')
|
|
666
|
+
const { TelemetryStore, makeTelemetryStoreLayer, TelemetryStoreReadonly, TelemetryStoreReadonlyLive } = await import('./src/services/TelemetryStore.ts')
|
|
667
|
+
const writer = ManagedRuntime.make(makeTelemetryStoreLayer({ readonly: false, runRetention: false }))
|
|
668
|
+
try {
|
|
669
|
+
const now = BigInt(Date.now()) * 1000000n
|
|
670
|
+
await writer.runPromise(Effect.flatMap(TelemetryStore, (store) => store.ingestTraces({ resourceSpans: [{ resource: { attributes: [{ key: 'service.name', value: { stringValue: 'legacy-readonly' } }] }, scopeSpans: [{ spans: [{ traceId: 'legacy-read', spanId: 'root', name: 'legacyoperation', startTimeUnixNano: String(now), endTimeUnixNano: String(now + 1000000n) }] }] }] })))
|
|
671
|
+
} finally { await writer.dispose() }
|
|
672
|
+
const legacy = new Database(process.env.MOTEL_OTEL_DB_PATH)
|
|
673
|
+
legacy.exec('DROP VIEW retained_spans; DROP VIEW retained_logs; DROP VIEW retained_trace_summaries; DROP TABLE span_search_rows; DROP TABLE log_search_rows; DROP TABLE retention_traces; DROP TABLE retention_logs;')
|
|
674
|
+
const version = legacy.query('PRAGMA schema_version').get().schema_version
|
|
675
|
+
legacy.close()
|
|
676
|
+
const reader = ManagedRuntime.make(TelemetryStoreReadonlyLive)
|
|
677
|
+
try {
|
|
678
|
+
const trace = await reader.runPromise(Effect.flatMap(TelemetryStoreReadonly, (store) => store.getTrace('legacy-read')))
|
|
679
|
+
assert.equal(trace?.spans.length, 1)
|
|
680
|
+
const found = await reader.runPromise(Effect.flatMap(TelemetryStoreReadonly, (store) => store.searchSpans({ operation: 'legacyoperation' })))
|
|
681
|
+
assert.equal(found.length, 1)
|
|
682
|
+
assert.equal(found[0].traceId, 'legacy-read')
|
|
683
|
+
} finally { await reader.dispose() }
|
|
684
|
+
const verify = new Database(process.env.MOTEL_OTEL_DB_PATH, { readonly: true })
|
|
685
|
+
try {
|
|
686
|
+
assert.equal(verify.query('PRAGMA schema_version').get().schema_version, version)
|
|
687
|
+
assert.equal(verify.query("SELECT count(*) AS n FROM sqlite_master WHERE name IN ('span_search_rows', 'log_search_rows', 'retained_spans')").get().n, 0)
|
|
688
|
+
} finally { verify.close() }
|
|
689
|
+
`
|
|
690
|
+
const child = Bun.spawn({ cmd: [process.execPath, "--no-env-file", "-e", script], cwd: process.cwd(), env: { ...process.env, MOTEL_OTEL_DB_PATH: legacyPath }, stdout: "ignore", stderr: "pipe" })
|
|
691
|
+
const [exitCode, stderr] = await Promise.all([child.exited, new Response(child.stderr).text()])
|
|
692
|
+
expect(exitCode, stderr).toBe(0)
|
|
693
|
+
})
|
|
694
|
+
|
|
471
695
|
it("returns trace details through the query worker when spans share empty event arrays", async () => {
|
|
472
696
|
const workerDbPath = join(tempDir, "query-worker-shared-events.sqlite")
|
|
473
697
|
const script = `
|