autotel
Version:
Write Once, Observe Anywhere
267 lines (265 loc) • 8.95 kB
JavaScript
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
const require_config = require('./config.cjs');
let _opentelemetry_api = require("@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 = require_config.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: _opentelemetry_api.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: _opentelemetry_api.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 = require_config.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: _opentelemetry_api.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: _opentelemetry_api.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
exports.DB_OPERATIONS = DB_OPERATIONS;
exports.DB_SYSTEMS = DB_SYSTEMS;
exports.instrumentDatabase = instrumentDatabase;
exports.tracebQuery = tracebQuery;
//# sourceMappingURL=db.cjs.map