UNPKG

autotel

Version:
263 lines (261 loc) 8.67 kB
import { getConfig } from "./config.js"; import { SpanStatusCode } from "@opentelemetry/api"; //#region src/db.ts /** * Database Instrumentation Helpers * * Optional import: Not included in main bundle * Import from: 'autotel/db' * * Provides functional utilities for database query instrumentation. * Works with Prisma, Drizzle, TypeORM, raw SQL, and more. * * @example * ```typescript * import { instrumentDatabase } from 'autotel/db' * * const db = drizzle(pool) * instrumentDatabase(db, { dbSystem: 'postgresql', dbName: 'myapp' }) * * // Now all queries are automatically trace * await db.select().from(users) * ``` */ /** * Helper: Trace a single database query * * @example * ```typescript * import { tracebQuery } from 'autotel/db' * * const users = await tracebQuery( * 'postgresql', * 'SELECT', * () => db.query('SELECT * FROM users WHERE active = true') * ) * ``` */ async function tracebQuery(dbSystem, operation, fn, attributes) { const tracer = getConfig().tracer; const spanName = `${dbSystem}.${operation}`; return tracer.startActiveSpan(spanName, async (span) => { const startTime = performance.now(); try { span.setAttributes({ "db.system": dbSystem, "db.operation": operation, ...attributes }); const result = await fn(); const duration = performance.now() - startTime; span.setStatus({ code: SpanStatusCode.OK }); span.setAttribute("db.duration_ms", duration); if (Array.isArray(result)) span.setAttribute("db.result_count", result.length); return result; } catch (error) { const duration = performance.now() - startTime; span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : "Unknown error" }); span.setAttributes({ "db.duration_ms": duration, "error.type": error instanceof Error ? error.constructor.name : "Unknown", "error.message": error instanceof Error ? error.message : "Unknown error" }); throw error; } finally { span.end(); } }); } function inferDbOperation(methodName) { const lower = methodName.toLowerCase(); if (lower.includes("find") || lower.includes("get") || lower.includes("list")) return "SELECT"; if (lower.includes("create") || lower.includes("insert")) return "INSERT"; if (lower.includes("update") || lower.includes("modify")) return "UPDATE"; if (lower.includes("delete") || lower.includes("remove")) return "DELETE"; if (lower.includes("count")) return "COUNT"; return "QUERY"; } function inferTableName(methodName) { for (const pattern of [ /find([A-Z][a-zA-Z]+)/, /get([A-Z][a-zA-Z]+)/, /list([A-Z][a-zA-Z]+)/, /create([A-Z][a-zA-Z]+)/, /update([A-Z][a-zA-Z]+)/, /delete([A-Z][a-zA-Z]+)/, /remove([A-Z][a-zA-Z]+)/ ]) { const match = methodName.match(pattern); if (match && match[1]) return match[1].toLowerCase(); } } function sanitizeSqlQuery(query) { return query.replaceAll(/'[^']*'/g, "'?'").replaceAll(/"[^"]*"/g, "\"?\"").replaceAll(/\b\d+\b/g, "?").trim(); } /** * Common database operation metrics */ const DB_OPERATIONS = { SELECT: "SELECT", INSERT: "INSERT", UPDATE: "UPDATE", DELETE: "DELETE", COUNT: "COUNT", AGGREGATE: "AGGREGATE" }; /** * Common database systems */ const DB_SYSTEMS = { POSTGRESQL: "postgresql", MYSQL: "mysql", MONGODB: "mongodb", REDIS: "redis", SQLITE: "sqlite", MSSQL: "mssql" }; const INSTRUMENTED_SYMBOL = Symbol.for("autotel.db.instrumented"); /** * Instrument a database client instance with OpenTelemetry tracing * * This is a function-based alternative to @DbInstrumented decorator. * Modifies the client in-place and returns it (idempotent - safe to call multiple times). * * Inspired by otel-drizzle and other otel instrumentation packages. * * @example Drizzle ORM * ```typescript * import { drizzle } from 'drizzle-orm/node-postgres' * import { instrumentDatabase } from 'autotel/db' * * const db = drizzle(pool) * instrumentDatabase(db, { dbSystem: 'postgresql', dbName: 'myapp' }) * * // Now all db queries are automatically trace * await db.select().from(users) * ``` * * @example Prisma * ```typescript * import { PrismaClient } from '@prisma/client' * import { instrumentDatabase } from 'autotel/db' * * const prisma = new PrismaClient() * instrumentDatabase(prisma, { * dbSystem: 'postgresql', * methods: ['findMany', 'findUnique', 'create', 'update', 'delete'] * }) * * // All specified methods are trace * await prisma.user.findMany() * ``` * * @example Generic database client * ```typescript * import { instrumentDatabase } from 'autotel/db' * * const db = createDatabaseClient() * instrumentDatabase(db, { * dbSystem: 'mongodb', * methods: ['find', 'findOne', 'insertOne', 'updateOne', 'deleteOne'] * }) * ``` */ function instrumentDatabase(client, options) { if (client[INSTRUMENTED_SYMBOL]) return client; const { dbSystem, dbName, methods, skipMethods = [], sanitizeQuery = true, slowQueryThresholdMs = 1e3 } = options; const tracer = getConfig().tracer; const methodsToInstrument = methods || extractDatabaseMethods(client); const skipSet = new Set(skipMethods); for (const methodName of methodsToInstrument) { if (skipSet.has(methodName)) continue; if (methodName.startsWith("_")) continue; const method = client[methodName]; if (typeof method !== "function") continue; const originalMethod = method; client[methodName] = async function(...args) { const operation = inferDbOperation(methodName); const table = inferTableName(methodName); const spanName = table ? `${dbSystem}.${operation} ${table}` : `${dbSystem}.${operation}`; return tracer.startActiveSpan(spanName, async (span) => { const startTime = performance.now(); try { span.setAttributes({ "db.system": dbSystem, "db.operation": operation }); if (dbName) span.setAttribute("db.name", dbName); if (table) span.setAttribute("db.sql.table", table); const query = extractQueryFromArgs(args); if (query) span.setAttribute("db.statement", sanitizeQuery ? sanitizeSqlQuery(query) : query); const result = await originalMethod.apply(this, args); const duration = performance.now() - startTime; span.setStatus({ code: SpanStatusCode.OK }); span.setAttributes({ "db.duration_ms": duration }); if (duration > slowQueryThresholdMs) { span.setAttribute("db.slow_query", true); span.setAttribute("db.slow_query_threshold_ms", slowQueryThresholdMs); } if (Array.isArray(result)) span.setAttribute("db.result_count", result.length); return result; } catch (error) { const duration = performance.now() - startTime; span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : "Unknown error" }); span.setAttributes({ "db.duration_ms": duration, "error.type": error instanceof Error ? error.constructor.name : "Unknown", "error.message": error instanceof Error ? error.message : "Unknown error" }); span.recordException(error instanceof Error ? error : new Error(String(error))); throw error; } finally { span.end(); } }); }; Object.defineProperty(client[methodName], "name", { value: methodName, configurable: true }); } client[INSTRUMENTED_SYMBOL] = true; return client; } /** * Extract method names from a database client that should be instrumented */ function extractDatabaseMethods(client) { const methods = []; const proto = Object.getPrototypeOf(client); for (const key of Object.getOwnPropertyNames(client)) if (typeof client[key] === "function" && !key.startsWith("_")) methods.push(key); if (proto) { for (const key of Object.getOwnPropertyNames(proto)) if (typeof proto[key] === "function" && !key.startsWith("_") && key !== "constructor") methods.push(key); } return [...new Set(methods)]; } /** * Try to extract SQL query from common argument patterns */ function extractQueryFromArgs(args) { if (args.length === 0) return void 0; const firstArg = args[0]; if (typeof firstArg === "string") return firstArg; if (firstArg && typeof firstArg === "object") { if ("sql" in firstArg && typeof firstArg.sql === "string") return firstArg.sql; if ("text" in firstArg && typeof firstArg.text === "string") return firstArg.text; if ("toQuery" in firstArg && typeof firstArg.toQuery === "function") try { const queryResult = firstArg.toQuery(); if (typeof queryResult === "string") return queryResult; if (queryResult && typeof queryResult === "object" && "sql" in queryResult) return queryResult.sql; } catch {} } } //#endregion export { DB_OPERATIONS, DB_SYSTEMS, instrumentDatabase, tracebQuery }; //# sourceMappingURL=db.js.map