diff options
| author | Claude <noreply@anthropic.com> | 2026-02-07 09:37:48 +0000 |
|---|---|---|
| committer | Mohamed Bassem <me@mbassem.com> | 2026-02-08 00:18:58 +0000 |
| commit | 9e5693c6e4b410d1af05cc3d50c89ff73f21e060 (patch) | |
| tree | 63f18bc726105383b075369511b0eb168e697551 /packages/db/instrumentation.ts | |
| parent | e59fd98b43070898c594c35af1a0bbee604ad160 (diff) | |
| download | karakeep-9e5693c6e4b410d1af05cc3d50c89ff73f21e060.tar.zst | |
feat(db): add OpenTelemetry instrumentation for database queries
Instruments the better-sqlite3 driver so that every prepared statement
execution (run/get/all) produces an OTel span with db.system,
db.statement, and db.operation attributes. The instrumentation is a
no-op when no TracerProvider is registered (i.e. tracing is disabled).
https://claude.ai/code/session_01JZut7LqeHPUKAFbFLfVP8F
Diffstat (limited to 'packages/db/instrumentation.ts')
| -rw-r--r-- | packages/db/instrumentation.ts | 67 |
1 files changed, 67 insertions, 0 deletions
diff --git a/packages/db/instrumentation.ts b/packages/db/instrumentation.ts new file mode 100644 index 00000000..fa5bcf94 --- /dev/null +++ b/packages/db/instrumentation.ts @@ -0,0 +1,67 @@ +import type Database from "better-sqlite3"; +import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api"; + +const TRACER_NAME = "@karakeep/db"; + +function getOperationType(sql: string): string { + return sql.trimStart().split(/\s/, 1)[0]?.toUpperCase() ?? "UNKNOWN"; +} + +/** + * Instruments a better-sqlite3 Database instance with OpenTelemetry tracing. + * + * Wraps `prepare()` so that every `run()`, `get()`, and `all()` call on + * the returned Statement produces an OTel span with db.system, db.statement, + * and db.operation attributes. + * + * The instrumentation is a no-op when no OTel TracerProvider is registered + * (i.e. when tracing is disabled), following standard OTel conventions. + */ +export function instrumentDatabase( + sqlite: Database.Database, +): Database.Database { + const tracer = trace.getTracer(TRACER_NAME); + const origPrepare = sqlite.prepare.bind(sqlite); + + sqlite.prepare = function (sql: string) { + const stmt = origPrepare(sql); + const operation = getOperationType(sql); + const spanName = `db.${operation.toLowerCase()}`; + + for (const method of ["run", "get", "all"] as const) { + type QueryFn = (...args: unknown[]) => unknown; + const original = (stmt[method] as QueryFn).bind(stmt); + (stmt[method] as QueryFn) = function (...args: unknown[]) { + const span = tracer.startSpan(spanName, { + kind: SpanKind.CLIENT, + attributes: { + "db.system": "sqlite", + "db.statement": sql, + "db.operation": operation, + }, + }); + + try { + const result = original(...args); + span.setStatus({ code: SpanStatusCode.OK }); + return result; + } catch (error) { + span.setStatus({ + code: SpanStatusCode.ERROR, + message: error instanceof Error ? error.message : String(error), + }); + span.recordException( + error instanceof Error ? error : new Error(String(error)), + ); + throw error; + } finally { + span.end(); + } + }; + } + + return stmt; + } as typeof sqlite.prepare; + + return sqlite; +} |
