UNPKG

autotel

Version:
1 lines 16.9 kB
{"version":3,"file":"db.cjs","names":["getConfig","SpanStatusCode"],"sources":["../src/db.ts"],"sourcesContent":["/**\n * Database Instrumentation Helpers\n *\n * Optional import: Not included in main bundle\n * Import from: 'autotel/db'\n *\n * Provides functional utilities for database query instrumentation.\n * Works with Prisma, Drizzle, TypeORM, raw SQL, and more.\n *\n * @example\n * ```typescript\n * import { instrumentDatabase } from 'autotel/db'\n *\n * const db = drizzle(pool)\n * instrumentDatabase(db, { dbSystem: 'postgresql', dbName: 'myapp' })\n *\n * // Now all queries are automatically trace\n * await db.select().from(users)\n * ```\n */\n\nimport { SpanStatusCode } from '@opentelemetry/api';\nimport { getConfig } from './config';\n\n/**\n * Helper: Trace a single database query\n *\n * @example\n * ```typescript\n * import { tracebQuery } from 'autotel/db'\n *\n * const users = await tracebQuery(\n * 'postgresql',\n * 'SELECT',\n * () => db.query('SELECT * FROM users WHERE active = true')\n * )\n * ```\n */\nexport async function tracebQuery<T>(\n dbSystem: string,\n operation: string,\n fn: () => Promise<T>,\n attributes?: Record<string, string | number>,\n): Promise<T> {\n const config = getConfig();\n const tracer = config.tracer;\n\n const spanName = `${dbSystem}.${operation}`;\n\n return tracer.startActiveSpan(spanName, async (span) => {\n const startTime = performance.now();\n\n try {\n span.setAttributes({\n 'db.system': dbSystem,\n 'db.operation': operation,\n ...attributes,\n });\n\n const result = await fn();\n\n const duration = performance.now() - startTime;\n span.setStatus({ code: SpanStatusCode.OK });\n span.setAttribute('db.duration_ms', duration);\n\n if (Array.isArray(result)) {\n span.setAttribute('db.result_count', result.length);\n }\n\n return result;\n } catch (error) {\n const duration = performance.now() - startTime;\n\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : 'Unknown error',\n });\n\n span.setAttributes({\n 'db.duration_ms': duration,\n 'error.type':\n error instanceof Error ? error.constructor.name : 'Unknown',\n 'error.message':\n error instanceof Error ? error.message : 'Unknown error',\n });\n\n throw error;\n } finally {\n span.end();\n }\n });\n}\n\n// Helper functions\n\nfunction inferDbOperation(methodName: string): string {\n const lower = methodName.toLowerCase();\n if (lower.includes('find') || lower.includes('get') || lower.includes('list'))\n return 'SELECT';\n if (lower.includes('create') || lower.includes('insert')) return 'INSERT';\n if (lower.includes('update') || lower.includes('modify')) return 'UPDATE';\n if (lower.includes('delete') || lower.includes('remove')) return 'DELETE';\n if (lower.includes('count')) return 'COUNT';\n return 'QUERY';\n}\n\nfunction inferTableName(methodName: string): string | undefined {\n // Extract table name from method patterns like:\n // findUser -> user\n // listUsers -> users\n // createOrder -> order\n\n const patterns = [\n /find([A-Z][a-zA-Z]+)/,\n /get([A-Z][a-zA-Z]+)/,\n /list([A-Z][a-zA-Z]+)/,\n /create([A-Z][a-zA-Z]+)/,\n /update([A-Z][a-zA-Z]+)/,\n /delete([A-Z][a-zA-Z]+)/,\n /remove([A-Z][a-zA-Z]+)/,\n ];\n\n for (const pattern of patterns) {\n const match = methodName.match(pattern);\n if (match && match[1]) {\n return match[1].toLowerCase();\n }\n }\n\n return undefined;\n}\n\nfunction sanitizeSqlQuery(query: string): string {\n // Remove string literals and sensitive values (PII, credentials, etc.)\n // Preserves query structure for debugging while protecting data\n return query\n .replaceAll(/'[^']*'/g, \"'?'\")\n .replaceAll(/\"[^\"]*\"/g, '\"?\"')\n .replaceAll(/\\b\\d+\\b/g, '?') // Replace literal numbers\n .trim();\n}\n\n/**\n * Common database operation metrics\n */\nexport const DB_OPERATIONS = {\n SELECT: 'SELECT',\n INSERT: 'INSERT',\n UPDATE: 'UPDATE',\n DELETE: 'DELETE',\n COUNT: 'COUNT',\n AGGREGATE: 'AGGREGATE',\n} as const;\n\n/**\n * Common database systems\n */\nexport const DB_SYSTEMS = {\n POSTGRESQL: 'postgresql',\n MYSQL: 'mysql',\n MONGODB: 'mongodb',\n REDIS: 'redis',\n SQLITE: 'sqlite',\n MSSQL: 'mssql',\n} as const;\n\n// Symbol for idempotency - prevents double-instrumentation\nconst INSTRUMENTED_SYMBOL = Symbol.for('autotel.db.instrumented');\n\n/**\n * Options for instrumentDatabase\n */\nexport interface InstrumentDatabaseOptions {\n /** Database system (e.g., 'postgresql', 'mysql') */\n dbSystem: string;\n /** Database name (optional) */\n dbName?: string;\n /** Method names to instrument (if not provided, instruments common patterns) */\n methods?: string[];\n /** Method names to skip */\n skipMethods?: string[];\n /** Sanitize queries (remove sensitive data) - default: true */\n sanitizeQuery?: boolean;\n /** Slow query threshold in milliseconds - default: 1000ms */\n slowQueryThresholdMs?: number;\n}\n\n/**\n * Instrument a database client instance with OpenTelemetry tracing\n *\n * This is a function-based alternative to @DbInstrumented decorator.\n * Modifies the client in-place and returns it (idempotent - safe to call multiple times).\n *\n * Inspired by otel-drizzle and other otel instrumentation packages.\n *\n * @example Drizzle ORM\n * ```typescript\n * import { drizzle } from 'drizzle-orm/node-postgres'\n * import { instrumentDatabase } from 'autotel/db'\n *\n * const db = drizzle(pool)\n * instrumentDatabase(db, { dbSystem: 'postgresql', dbName: 'myapp' })\n *\n * // Now all db queries are automatically trace\n * await db.select().from(users)\n * ```\n *\n * @example Prisma\n * ```typescript\n * import { PrismaClient } from '@prisma/client'\n * import { instrumentDatabase } from 'autotel/db'\n *\n * const prisma = new PrismaClient()\n * instrumentDatabase(prisma, {\n * dbSystem: 'postgresql',\n * methods: ['findMany', 'findUnique', 'create', 'update', 'delete']\n * })\n *\n * // All specified methods are trace\n * await prisma.user.findMany()\n * ```\n *\n * @example Generic database client\n * ```typescript\n * import { instrumentDatabase } from 'autotel/db'\n *\n * const db = createDatabaseClient()\n * instrumentDatabase(db, {\n * dbSystem: 'mongodb',\n * methods: ['find', 'findOne', 'insertOne', 'updateOne', 'deleteOne']\n * })\n * ```\n */\nexport function instrumentDatabase<T extends object>(\n client: T,\n options: InstrumentDatabaseOptions,\n): T {\n // Idempotency check - if already instrumented, return as-is\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n if ((client as any)[INSTRUMENTED_SYMBOL]) {\n return client;\n }\n\n const {\n dbSystem,\n dbName,\n methods,\n skipMethods = [],\n sanitizeQuery = true,\n slowQueryThresholdMs = 1000,\n } = options;\n\n const config = getConfig();\n const tracer = config.tracer;\n\n // Determine which methods to instrument\n const methodsToInstrument = methods || extractDatabaseMethods(client);\n const skipSet = new Set(skipMethods);\n\n for (const methodName of methodsToInstrument) {\n if (skipSet.has(methodName)) continue;\n if (methodName.startsWith('_')) continue; // Skip private methods\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const method = (client as any)[methodName];\n if (typeof method !== 'function') continue;\n\n // Preserve the original method\n const originalMethod = method;\n\n // Wrap the method\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (client as any)[methodName] = async function (this: T, ...args: any[]) {\n const operation = inferDbOperation(methodName);\n const table = inferTableName(methodName);\n\n const spanName = table\n ? `${dbSystem}.${operation} ${table}`\n : `${dbSystem}.${operation}`;\n\n return tracer.startActiveSpan(spanName, async (span) => {\n const startTime = performance.now();\n\n try {\n span.setAttributes({\n 'db.system': dbSystem,\n 'db.operation': operation,\n });\n\n if (dbName) {\n span.setAttribute('db.name', dbName);\n }\n\n if (table) {\n span.setAttribute('db.sql.table', table);\n }\n\n // Try to extract query from arguments (common patterns)\n const query = extractQueryFromArgs(args);\n if (query) {\n span.setAttribute(\n 'db.statement',\n sanitizeQuery ? sanitizeSqlQuery(query) : query,\n );\n }\n\n // Execute original method\n const result = await originalMethod.apply(this, args);\n\n const duration = performance.now() - startTime;\n\n span.setStatus({ code: SpanStatusCode.OK });\n span.setAttributes({\n 'db.duration_ms': duration,\n });\n\n // Mark slow queries\n if (duration > slowQueryThresholdMs) {\n span.setAttribute('db.slow_query', true);\n span.setAttribute(\n 'db.slow_query_threshold_ms',\n slowQueryThresholdMs,\n );\n }\n\n // Track result count if it's an array\n if (Array.isArray(result)) {\n span.setAttribute('db.result_count', result.length);\n }\n\n return result;\n } catch (error) {\n const duration = performance.now() - startTime;\n\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : 'Unknown error',\n });\n\n span.setAttributes({\n 'db.duration_ms': duration,\n 'error.type':\n error instanceof Error ? error.constructor.name : 'Unknown',\n 'error.message':\n error instanceof Error ? error.message : 'Unknown error',\n });\n\n span.recordException(\n error instanceof Error ? error : new Error(String(error)),\n );\n\n throw error;\n } finally {\n span.end();\n }\n });\n };\n\n // Preserve function name\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n Object.defineProperty((client as any)[methodName], 'name', {\n value: methodName,\n configurable: true,\n });\n }\n\n // Mark as instrumented\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (client as any)[INSTRUMENTED_SYMBOL] = true;\n\n return client;\n}\n\n/**\n * Extract method names from a database client that should be instrumented\n */\nfunction extractDatabaseMethods(client: object): string[] {\n const methods: string[] = [];\n const proto = Object.getPrototypeOf(client);\n\n // Get own methods\n for (const key of Object.getOwnPropertyNames(client)) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n if (typeof (client as any)[key] === 'function' && !key.startsWith('_')) {\n methods.push(key);\n }\n }\n\n // Get prototype methods\n if (proto) {\n for (const key of Object.getOwnPropertyNames(proto)) {\n if (\n typeof proto[key] === 'function' &&\n !key.startsWith('_') &&\n key !== 'constructor'\n ) {\n methods.push(key);\n }\n }\n }\n\n return [...new Set(methods)]; // Deduplicate\n}\n\n/**\n * Try to extract SQL query from common argument patterns\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction extractQueryFromArgs(args: any[]): string | undefined {\n if (args.length === 0) return undefined;\n\n const firstArg = args[0];\n\n // String query (raw SQL)\n if (typeof firstArg === 'string') {\n return firstArg;\n }\n\n // Object with sql property\n if (firstArg && typeof firstArg === 'object') {\n if ('sql' in firstArg && typeof firstArg.sql === 'string') {\n return firstArg.sql;\n }\n // PostgreSQL-style query object\n if ('text' in firstArg && typeof firstArg.text === 'string') {\n return firstArg.text;\n }\n // Query builder pattern\n if ('toQuery' in firstArg && typeof firstArg.toQuery === 'function') {\n try {\n const queryResult = firstArg.toQuery();\n if (typeof queryResult === 'string') return queryResult;\n if (\n queryResult &&\n typeof queryResult === 'object' &&\n 'sql' in queryResult\n ) {\n return queryResult.sql as string;\n }\n } catch {\n // Ignore errors from toQuery()\n }\n }\n }\n\n return undefined;\n}\n"],"mappings":"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"}