UNPKG

autotel

Version:
277 lines (275 loc) 9.15 kB
import { _ as readProperty, h as isFunction, l as asString, r as asFunction, s as asRecord } from "./values-xBtdXtjA.js"; 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.name": dbSystem, "db.operation.name": 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) { const target = clientMembers(client); if (target[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 = asFunction(target[methodName]); if (!method) continue; const originalMethod = method; target[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.name": dbSystem, "db.operation.name": operation }); if (dbName) span.setAttribute("db.namespace", dbName); if (table) span.setAttribute("db.collection.name", table); const query = extractQueryFromArgs(args); if (query) span.setAttribute("db.query.text", 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(target[methodName], "name", { value: methodName, configurable: true }); } target[INSTRUMENTED_SYMBOL] = true; return client; } /** * A client's members, so the wrapper can read a method off it and write the * wrapped one back. * * SAFETY: instrumentDatabase's whole job is to replace named methods on a * client it was handed. TypeScript describes that client by the methods the * caller declared, which is precisely the set being rewritten - so this says * once that the client is being addressed by name. */ function clientMembers(client) { return client; } /** * Extract method names from a database client that should be instrumented */ function extractDatabaseMethods(client) { const methods = []; const own = clientMembers(client); const proto = clientMembers(Object.getPrototypeOf(client) ?? {}); for (const key of Object.getOwnPropertyNames(client)) if (isFunction(own[key]) && !key.startsWith("_")) methods.push(key); for (const key of Object.getOwnPropertyNames(proto)) if (isFunction(proto[key]) && !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]; const rawSql = asString(firstArg); if (rawSql !== void 0) return rawSql; const query = asRecord(firstArg); if (query) { const stated = asString(query.sql) ?? asString(query.text); if (stated !== void 0) return stated; if (isFunction(query.toQuery)) try { const queryResult = query.toQuery(); const built = asString(queryResult) ?? asString(readProperty(queryResult, "sql")); if (built !== void 0) return built; } catch {} } } //#endregion export { DB_OPERATIONS, DB_SYSTEMS, instrumentDatabase, tracebQuery };