UNPKG

@stacksleuth/db-agent

Version:

Universal database performance monitoring agent - Multi-database support, query optimization, connection pool monitoring, transaction tracking, and comprehensive database performance analytics.

196 lines 6.94 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DatabaseAgent = void 0; exports.instrumentPg = instrumentPg; exports.instrumentPgClient = instrumentPgClient; exports.createDatabaseAgent = createDatabaseAgent; const core_1 = require("@stacksleuth/core"); /** * Database agent for PostgreSQL instrumentation */ class DatabaseAgent { constructor(config, options) { this.collector = new core_1.TraceCollector(config); this.options = { enableQueryLogging: true, slowQueryThreshold: 100, logValues: false, ...options }; } /** * Instrument a PostgreSQL Pool */ instrumentPool(pool) { const originalQuery = pool.query.bind(pool); const originalConnect = pool.connect.bind(pool); // Instrument query method pool.query = this.createQueryWrapper(originalQuery); // Instrument connect method to return instrumented clients pool.connect = (callback) => { if (callback) { return originalConnect((err, client, done) => { if (err) return callback(err); if (client) { this.instrumentClient(client); } callback(err, client, done); }); } else { return originalConnect().then((client) => { this.instrumentClient(client); return client; }); } }; return pool; } /** * Instrument a PostgreSQL Client */ instrumentClient(client) { const originalQuery = client.query.bind(client); client.query = this.createQueryWrapper(originalQuery); } /** * Create a query wrapper that traces database operations */ createQueryWrapper(originalQuery) { return (...args) => { const queryText = typeof args[0] === 'string' ? args[0] : args[0]?.text || 'Unknown query'; const queryValues = args[1] || []; // Start tracing const trace = this.collector.startTrace(`DB Query: ${this.getQueryType(queryText)}`, { query: this.options.logValues ? queryText : this.sanitizeQuery(queryText), database: 'postgresql' }); if (!trace) { return originalQuery(...args); } const span = this.collector.startSpan(trace.id, `PostgreSQL ${this.getQueryType(queryText)}`, core_1.SpanType.DB_QUERY, undefined, { query: this.options.logValues ? queryText : this.sanitizeQuery(queryText), database: 'postgresql', table: this.extractTableName(queryText), paramCount: Array.isArray(queryValues) ? queryValues.length : 0 }); const startTime = Date.now(); const result = originalQuery(...args); // Handle both callback and promise-based queries if (result && typeof result.then === 'function') { // Promise-based query return result .then((queryResult) => { const duration = Date.now() - startTime; this.completeQuery(span, trace, duration, queryResult, null); return queryResult; }) .catch((error) => { const duration = Date.now() - startTime; this.completeQuery(span, trace, duration, null, error); throw error; }); } else { // Callback-based query or synchronous result const duration = Date.now() - startTime; this.completeQuery(span, trace, duration, result, null); return result; } }; } /** * Complete query tracing */ completeQuery(span, trace, duration, result, error) { if (span) { const metadata = { duration, rowCount: result?.rowCount || 0, command: result?.command || 'unknown' }; if (error) { this.collector.addSpanError(span.id, error); this.collector.completeSpan(span.id, core_1.TraceStatus.ERROR, metadata); } else { this.collector.completeSpan(span.id, core_1.TraceStatus.SUCCESS, metadata); } // Log slow queries if (this.options.enableQueryLogging && duration > (this.options.slowQueryThreshold || 100)) { console.warn(`🐌 Slow query detected (${duration}ms):`, span.metadata.query); } } if (trace) { const status = error ? core_1.TraceStatus.ERROR : core_1.TraceStatus.SUCCESS; this.collector.completeTrace(trace.id, status); } } /** * Extract the query type (SELECT, INSERT, UPDATE, etc.) */ getQueryType(query) { const match = query.trim().match(/^(\w+)/i); return match ? match[1].toUpperCase() : 'UNKNOWN'; } /** * Extract table name from query */ extractTableName(query) { // Simple regex to extract table name - could be improved const patterns = [ /FROM\s+([a-zA-Z_][a-zA-Z0-9_]*)/i, /INSERT\s+INTO\s+([a-zA-Z_][a-zA-Z0-9_]*)/i, /UPDATE\s+([a-zA-Z_][a-zA-Z0-9_]*)/i, /DELETE\s+FROM\s+([a-zA-Z_][a-zA-Z0-9_]*)/i ]; for (const pattern of patterns) { const match = query.match(pattern); if (match) return match[1]; } return 'unknown'; } /** * Sanitize query by removing parameter values */ sanitizeQuery(query) { // Replace parameter placeholders with generic markers return query .replace(/\$\d+/g, '$?') .replace(/'[^']*'/g, "'?'") .replace(/\s+/g, ' ') .trim(); } /** * Get the trace collector instance */ getCollector() { return this.collector; } } exports.DatabaseAgent = DatabaseAgent; /** * Instrument a PostgreSQL pool with default settings */ function instrumentPg(pool, options) { const agent = new DatabaseAgent(undefined, options); return agent.instrumentPool(pool); } /** * Instrument a PostgreSQL client with default settings */ function instrumentPgClient(client, options) { const agent = new DatabaseAgent(undefined, options); agent.instrumentClient(client); } /** * Create a database agent instance */ function createDatabaseAgent(config, options) { return new DatabaseAgent(config, options); } // Default export exports.default = DatabaseAgent; //# sourceMappingURL=index.js.map