UNPKG

otel-instrumentation-postgres

Version:
653 lines (644 loc) 23.8 kB
'use strict'; var events = require('events'); var api = require('@opentelemetry/api'); var instrumentation = require('@opentelemetry/instrumentation'); var semanticConventions = require('@opentelemetry/semantic-conventions'); // src/constants.ts var PG_TRACER_NAME = "otel-instrumentation-postgres"; var PG_TRACER_VERSION = "1.0.0"; var PG_SERVER_ADDRESS = "net.peer.name"; var PG_SERVER_PORT = "net.peer.port"; var PG_DB_PARAMETER_COUNT = "db.parameter_count"; var PG_DB_DURATION_MS = "db.duration_ms"; var PG_DB_DURATION_SECONDS = "db.duration_seconds"; var PG_DB_QUERY_HAS_WHERE = "db.query.has_where"; var PG_DB_QUERY_HAS_JOIN = "db.query.has_join"; var PG_DB_QUERY_HAS_ORDER_BY = "db.query.has_order_by"; var PG_DB_QUERY_HAS_LIMIT = "db.query.has_limit"; var PG_DB_QUERY_COMPLEXITY = "db.query.complexity"; var PG_DB_QUERY_TYPE = "db.query.type"; var PG_DB_QUERY_PARAMETER_PREFIX = "db.query.parameter."; var PG_DB_RESULT_ROW_COUNT = "db.result.row_count"; var PG_METRIC_REQUESTS = "db.client.requests"; var PG_METRIC_ERRORS = "db.client.errors"; var PG_METRIC_CONNECTIONS = "db.client.connections"; var PG_METRIC_CONNECTION_DURATION = "db.client.connections.duration"; var PG_METRIC_ATTR_ERROR_TYPE = "db.error.type"; var PG_DEFAULT_HISTOGRAM_BUCKETS = [ 1e-3, 0.01, 0.1, 0.5, 1, 2, 5, 10, 30, 60, 120, 300, 600 ]; var PG_EVENT_NAME = "db:query"; var PG_CONNECTION_EVENT_NAME = "db:connection"; var GLOBAL_KEY = "__db_event_emitter_singleton__"; function getDbEventEmitter(logger) { if (global[GLOBAL_KEY]) { return global[GLOBAL_KEY]; } else { logger?.debug?.("[DB-EVENTS] Creating new event emitter singleton"); const dbEventEmitter = new events.EventEmitter(); global[GLOBAL_KEY] = dbEventEmitter; return dbEventEmitter; } } // src/emitter/proxy-handler/query-executor.ts var LOG_PREFIX = "[DB-QUERY-SENDER]"; async function emitAndRunQuery(target, thisArg, argArray, logger) { const sql = argArray[0]; const params = argArray[1] || []; const start = Date.now(); let databaseName; if (thisArg && typeof thisArg === "object" && "options" in thisArg && thisArg.options?.database) { databaseName = thisArg.options.database; } else if (target && typeof target === "object" && "options" in target && target.options?.database) { databaseName = target.options.database; } logger?.debug?.( `${LOG_PREFIX} Intercepted query:`, `${String(sql).substring(0, 100)}...`, databaseName ? `(database: ${databaseName})` : "" ); try { const result = await target.apply( thisArg, argArray ); const event = { sql, params, result, durationMs: Date.now() - start, databaseName }; logger?.debug?.( `${LOG_PREFIX} Emitting success event:`, `${event.durationMs}ms`, databaseName ? `(database: ${databaseName})` : "" ); getDbEventEmitter(logger).emit(PG_EVENT_NAME, event); return result; } catch (error) { const event = { sql, params, error, durationMs: Date.now() - start, databaseName }; logger?.debug?.( `${LOG_PREFIX} Emitting error event:`, `${event.durationMs}ms`, databaseName ? `(database: ${databaseName})` : "" ); getDbEventEmitter(logger).emit(PG_EVENT_NAME, event); throw error; } } // src/emitter/proxy-handler/reserved-connection.ts var LOG_PREFIX2 = "[DB-QUERY-SENDER]"; var sqlRegex = /^(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP)/i; function createEventEmittingReservedConnection(reserved, connectionId, connectionIds, logger) { logger?.debug?.(`${LOG_PREFIX2} Creating event-emitting reserved connection`); if (typeof reserved === "function") { logger?.debug?.( `${LOG_PREFIX2} Reserved is a function, checking for properties` ); const reservedFunc = reserved; if (reservedFunc.release) { logger?.debug?.( `${LOG_PREFIX2} Function has release property, treating as object with methods` ); return new Proxy(reservedFunc, { apply(target, thisArg, argArray) { logger?.debug?.( `${LOG_PREFIX2} Reserved function called directly with args:`, argArray ); if (argArray[0] && typeof argArray[0] === "string" && sqlRegex.test(argArray[0])) { logger?.debug?.( `${LOG_PREFIX2} SQL detected in direct function call!` ); return emitAndRunQuery(target, thisArg, argArray, logger); } return target.apply(thisArg, argArray); }, // eslint-disable-next-line @typescript-eslint/no-unused-vars get(target, prop, _receiver) { const value = target[prop]; if (typeof value === "function") { return function(...args) { logger?.debug?.( `${LOG_PREFIX2} Reserved function property called:`, String(prop), "with args:", args ); if (prop === "release" && connectionId && connectionIds) { logger?.debug?.( `${LOG_PREFIX2} Release method called, emitting disconnect event` ); getDbEventEmitter(logger).emit(PG_CONNECTION_EVENT_NAME, { type: "disconnect", timestamp: Date.now(), connectionId }); connectionIds.delete(reserved); } if (prop !== "release" && prop !== "constructor" && args[0] && typeof args[0] === "string" && sqlRegex.test(args[0])) { logger?.debug?.( `${LOG_PREFIX2} SQL detected in property method call for prop:`, String(prop) ); return emitAndRunQuery(value, this, args, logger); } return value.apply(this, args); }; } return value; } }); } else { logger?.debug?.( `${LOG_PREFIX2} Function has no release property, treating as simple function` ); return function(...args) { logger?.debug?.( `${LOG_PREFIX2} Reserved function called with args:`, args ); if (args[0] && typeof args[0] === "string" && sqlRegex.test(args[0])) { logger?.debug?.(`${LOG_PREFIX2} SQL detected in function call!`); return emitAndRunQuery(reservedFunc, this, args, logger); } return reservedFunc.apply(this, args); }; } } if (typeof reserved !== "object" || reserved === null) { logger?.debug?.( `${LOG_PREFIX2} Reserved is not an object or function, returning as-is` ); return reserved; } logger?.debug?.( `${LOG_PREFIX2} Wrapping reserved connection object with proxy` ); return new Proxy(reserved, { get(target, prop, receiver) { const value = Reflect.get(target, prop, receiver); if (typeof value === "function") { return function(...args) { logger?.debug?.( `${LOG_PREFIX2} Reserved connection method called:`, String(prop), "with args:", args ); if (prop === "release" && connectionId && connectionIds) { logger?.debug?.( `${LOG_PREFIX2} Release method called, emitting disconnect event` ); getDbEventEmitter(logger).emit(PG_CONNECTION_EVENT_NAME, { type: "disconnect", timestamp: Date.now(), connectionId }); connectionIds.delete(reserved); } if (prop !== "release" && prop !== "constructor" && args[0] && typeof args[0] === "string" && sqlRegex.test(args[0])) { logger?.debug?.( `${LOG_PREFIX2} SQL detected in method call for prop:`, String(prop) ); return emitAndRunQuery(value, this, args, logger); } return value.apply(this, args); }; } return value; } }); } // src/emitter/proxy-handler/proxy-handler.ts var LOG_PREFIX3 = "[DB-QUERY-SENDER]"; function createProxyHandler(connectionIds, logger) { return { get(target, prop, receiver) { logger?.debug?.(`${LOG_PREFIX3} Proxy get called for prop:`, String(prop)); const value = Reflect.get(target, prop, receiver); if (prop === "reserve" && typeof value === "function") { logger?.debug?.(`${LOG_PREFIX3} Intercepting reserve method`); return async function(...args) { logger?.debug?.(`${LOG_PREFIX3} Reserve method called`); const reserved = await value.apply(this, args); const connectionId = Math.random().toString(36).substring(2, 15); connectionIds.set(reserved, connectionId); logger?.debug?.(`${LOG_PREFIX3} Emitting connection event`); getDbEventEmitter(logger).emit(PG_CONNECTION_EVENT_NAME, { type: "connect", timestamp: Date.now(), connectionId }); return createEventEmittingReservedConnection( reserved, connectionId, connectionIds, logger ); }; } if (typeof value === "function") { logger?.debug?.(`${LOG_PREFIX3} Intercepting function:`, String(prop)); return function(...args) { logger?.debug?.(`${LOG_PREFIX3} Function called:`, String(prop)); if (args[0] && typeof args[0] === "string" && /^(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP)/i.test(args[0])) { logger?.debug?.(`${LOG_PREFIX3} SQL detected in function call!`); return emitAndRunQuery(value, this, args, logger); } return value.apply(this, args); }; } return value; }, apply(target, thisArg, argArray) { logger?.debug?.(`${LOG_PREFIX3} Proxy apply called`); return emitAndRunQuery(target, thisArg, argArray, logger); } }; } // src/emitter/emitter.ts var LOG_PREFIX4 = "[DB-QUERY-SENDER]"; function createOTELEmitter(client, logger) { logger?.info?.(`${LOG_PREFIX4} Creating event-emitting postgres client`); const connectionIds = /* @__PURE__ */ new Map(); const handler = createProxyHandler(connectionIds, logger); return new Proxy(client, handler); } // src/query-analysis.ts function analyzeQuery(sql) { const cleanSql = sql.replace(/--.*$/gm, "").replace(/\s+/g, " ").trim(); const upperSql = cleanSql.toUpperCase(); let operation = "UNKNOWN"; if (upperSql.startsWith("SELECT")) operation = "SELECT"; else if (upperSql.startsWith("INSERT")) operation = "INSERT"; else if (upperSql.startsWith("UPDATE")) operation = "UPDATE"; else if (upperSql.startsWith("DELETE")) operation = "DELETE"; else if (upperSql.startsWith("CREATE")) operation = "CREATE"; else if (upperSql.startsWith("ALTER")) operation = "ALTER"; else if (upperSql.startsWith("DROP")) operation = "DROP"; const tableMatch = cleanSql.match(/FROM\s+["`]?(\w+)["`]?/i) || cleanSql.match(/INTO\s+["`]?(\w+)["`]?/i) || cleanSql.match(/UPDATE\s+["`]?(\w+)["`]?/i); const table = tableMatch?.[1]; const hasWhere = upperSql.includes("WHERE"); const hasJoin = upperSql.includes("JOIN"); const hasOrderBy = upperSql.includes("ORDER BY"); const hasLimit = upperSql.includes("LIMIT"); const parameterCount = (sql.match(/\?/g) || []).length; let estimatedComplexity = "low" /* LOW */; if (hasJoin || hasOrderBy || parameterCount > 5) estimatedComplexity = "medium" /* MEDIUM */; if (hasJoin && hasOrderBy && hasWhere) estimatedComplexity = "high" /* HIGH */; return { operation, ...table && { table }, hasWhere, hasJoin, hasOrderBy, hasLimit, parameterCount, estimatedComplexity }; } // src/instrumentation.ts var LOG_PREFIX5 = "[POSTGRES-INSTRUMENTATION]"; var PostgresInstrumentation = class _PostgresInstrumentation extends instrumentation.InstrumentationBase { constructor(config = {}) { super("postgres-instrumentation", "1.0.0", config); this.connectionStartTimes = /* @__PURE__ */ new Map(); this.customLogger = config.logger; this.serviceName = config.serviceName; this.enableHistogram = config.enableHistogram ?? true; this.histogramBuckets = config.histogramBuckets ?? PG_DEFAULT_HISTOGRAM_BUCKETS; this.collectQueryParameters = config.collectQueryParameters ?? false; this.serverAddress = config.serverAddress ?? process.env.PGHOST ?? void 0; this.serverPort = config.serverPort ?? (process.env.PGPORT ? Number(process.env.PGPORT) : void 0); this.databaseName = config.databaseName ?? process.env.PGDATABASE ?? void 0; this.parameterSanitizer = config.parameterSanitizer ?? this.defaultParameterSanitizer; this.beforeSpan = config.beforeSpan; this.afterSpan = config.afterSpan; this.responseHook = config.responseHook; this.customLogger?.info?.(`${LOG_PREFIX5} Postgres instrumentation created`); } init() { this.customLogger?.info?.( `${LOG_PREFIX5} Initializing postgres instrumentation` ); return []; } onMeterInitialized() { this.initializeMetrics(); } initializeMetrics() { try { if (!this.meter) { this.customLogger?.debug?.( `${LOG_PREFIX5} Meter not available, skipping metric initialization` ); return; } if (this.enableHistogram) { try { this.queryDurationHistogram = this.meter.createHistogram( semanticConventions.METRIC_DB_CLIENT_OPERATION_DURATION, { description: "Duration of PostgreSQL database queries in seconds", unit: "s", valueType: api.ValueType.DOUBLE, advice: { explicitBucketBoundaries: this.histogramBuckets } } ); } catch (histogramError) { this.customLogger?.debug?.( `${LOG_PREFIX5} Failed to create histogram:`, histogramError ); } } this.queryCounter = this.meter.createCounter(PG_METRIC_REQUESTS, { description: "Number of PostgreSQL database queries executed", valueType: api.ValueType.INT }); this.errorCounter = this.meter.createCounter(PG_METRIC_ERRORS, { description: "Number of PostgreSQL database query errors", valueType: api.ValueType.INT }); this.connectionCounter = this.meter.createCounter(PG_METRIC_CONNECTIONS, { description: "Number of PostgreSQL database connections established", valueType: api.ValueType.INT }); this.connectionDurationHistogram = this.meter.createHistogram( PG_METRIC_CONNECTION_DURATION, { description: "Duration of PostgreSQL database connections in seconds", unit: "s", valueType: api.ValueType.DOUBLE, advice: { explicitBucketBoundaries: this.histogramBuckets } } ); this.customLogger?.info?.( `${LOG_PREFIX5} Metrics initialized successfully` ); } catch (error) { this.customLogger?.debug?.( `${LOG_PREFIX5} Failed to initialize metrics:`, error ); } } enable() { super.enable(); this.customLogger?.debug?.(`${LOG_PREFIX5} Enable method called`); this.customLogger?.info?.( `${LOG_PREFIX5} Enabling postgres instrumentation` ); this.setupEventListeners(); this.customLogger?.debug?.(`${LOG_PREFIX5} Enable method completed`); } disable() { super.disable(); this.customLogger?.info?.( `${LOG_PREFIX5} Disabling postgres instrumentation` ); this.removeEventListeners(); } setupEventListeners() { if (this.listener) { this.removeEventListeners(); } this.customLogger?.info?.(`${LOG_PREFIX5} Setting up event listeners`); this.listener = (event) => { this.handleQueryEvent(event); }; getDbEventEmitter(this.customLogger).on(PG_EVENT_NAME, this.listener); getDbEventEmitter(this.customLogger).on( PG_CONNECTION_EVENT_NAME, (event) => { this.handleConnectionEvent(event); } ); } removeEventListeners() { if (this.listener) { getDbEventEmitter(this.customLogger).off(PG_EVENT_NAME, this.listener); this.listener = void 0; } } handleQueryEvent(event) { this.customLogger?.debug?.( `${LOG_PREFIX5} Processing query:`, `${event.durationMs}ms` ); const tracer = api.trace.getTracer(PG_TRACER_NAME, PG_TRACER_VERSION); const queryAnalysis = analyzeQuery(event.sql); const span = tracer.startSpan( queryAnalysis.operation, { attributes: { ...this.serviceName && { [semanticConventions.ATTR_SERVICE_NAME]: this.serviceName }, [PG_SERVER_ADDRESS]: this.serverAddress, [PG_SERVER_PORT]: this.serverPort, [semanticConventions.ATTR_DB_SYSTEM_NAME]: semanticConventions.DB_SYSTEM_NAME_VALUE_POSTGRESQL, [semanticConventions.ATTR_DB_NAMESPACE]: event.databaseName || this.databaseName, [semanticConventions.ATTR_DB_QUERY_TEXT]: this.sanitizeQuery(event.sql), [PG_DB_QUERY_TYPE]: this.getQueryType(queryAnalysis.operation), [semanticConventions.ATTR_DB_OPERATION_NAME]: queryAnalysis.operation, [semanticConventions.ATTR_DB_COLLECTION_NAME]: queryAnalysis.table || "unknown", [PG_DB_PARAMETER_COUNT]: event.params.length, [PG_DB_QUERY_HAS_WHERE]: queryAnalysis.hasWhere, [PG_DB_QUERY_HAS_JOIN]: queryAnalysis.hasJoin, [PG_DB_QUERY_HAS_ORDER_BY]: queryAnalysis.hasOrderBy, [PG_DB_QUERY_HAS_LIMIT]: queryAnalysis.hasLimit, [PG_DB_QUERY_COMPLEXITY]: queryAnalysis.estimatedComplexity, [PG_DB_DURATION_MS]: event.durationMs, [PG_DB_DURATION_SECONDS]: event.durationMs / 1e3 } }, api.context.active() ); if (this.beforeSpan) { this.beforeSpan(span, event); } if (this.collectQueryParameters && event.params.length > 0) { this.addQueryParameters(span, event.params); } this.recordMetrics(event, queryAnalysis); if (event.error) { span.setStatus({ code: api.SpanStatusCode.ERROR, message: String(event.error) }); span.recordException(event.error); span.setAttribute(semanticConventions.ATTR_EXCEPTION_TYPE, this.getErrorType(event.error)); this.customLogger?.debug?.(`${LOG_PREFIX5} Query failed:`, event.error); } else { span.setStatus({ code: api.SpanStatusCode.OK }); if (event.result) { if (Array.isArray(event.result)) { span.setAttribute(PG_DB_RESULT_ROW_COUNT, event.result.length); } if (this.responseHook) { this.responseHook(span, event.result); } } } if (this.afterSpan) { this.afterSpan(span, event); } span.end(); } handleConnectionEvent(event) { this.customLogger?.debug?.(`${LOG_PREFIX5} Connection event:`, event.type); if (this.connectionCounter) { const attributes = { [semanticConventions.ATTR_DB_SYSTEM_NAME]: semanticConventions.DB_SYSTEM_NAME_VALUE_POSTGRESQL, ...this.serviceName && { [semanticConventions.ATTR_SERVICE_NAME]: this.serviceName } }; this.connectionCounter.add(1, attributes); } if (event.type === "connect") { this.connectionStartTimes.set(event.connectionId, event.timestamp); } else if (event.type === "disconnect") { const startTime = this.connectionStartTimes.get(event.connectionId); if (startTime && this.connectionDurationHistogram) { const durationMs = event.timestamp - startTime; const durationSeconds = durationMs / 1e3; const attributes = { [semanticConventions.ATTR_DB_SYSTEM_NAME]: semanticConventions.DB_SYSTEM_NAME_VALUE_POSTGRESQL, ...this.serviceName && { [semanticConventions.ATTR_SERVICE_NAME]: this.serviceName } }; this.connectionDurationHistogram.record(durationSeconds, attributes); this.connectionStartTimes.delete(event.connectionId); } } } sanitizeQuery(sql) { return sql.replace(/password\s*=\s*['"][^'"]*['"]/gi, "password=***"); } addQueryParameters(span, params) { params.forEach((param, index) => { const paramKey = `${PG_DB_QUERY_PARAMETER_PREFIX}${index}`; const paramValue = this.parameterSanitizer(param); span.setAttribute(paramKey, paramValue); }); } defaultParameterSanitizer(param) { if (typeof param === "string" && param.length > 100) { return `${param.substring(0, 100)}...`; } if (typeof param === "string" && /password|token|secret/i.test(param)) { return "[REDACTED]"; } return String(param); } getErrorType(error) { if (error instanceof Error) { return error.constructor.name; } return "_OTHER"; } recordMetrics(event, queryAnalysis) { try { const durationInSeconds = event.durationMs / 1e3; const attributes = { [semanticConventions.ATTR_DB_SYSTEM_NAME]: semanticConventions.DB_SYSTEM_NAME_VALUE_POSTGRESQL, [semanticConventions.ATTR_DB_OPERATION_NAME]: queryAnalysis.operation, [semanticConventions.ATTR_DB_COLLECTION_NAME]: queryAnalysis.table || "unknown", [PG_DB_QUERY_COMPLEXITY]: queryAnalysis.estimatedComplexity, [PG_DB_QUERY_TYPE]: this.getQueryType(queryAnalysis.operation), ...this.serviceName && { [semanticConventions.ATTR_SERVICE_NAME]: this.serviceName } }; if (!this.queryDurationHistogram && this.enableHistogram) { this.initializeMetrics(); } if (this.queryDurationHistogram) { this.queryDurationHistogram.record(durationInSeconds, attributes); } if (this.queryCounter) { this.queryCounter.add(1, attributes); } if (event.error && this.errorCounter) { this.errorCounter.add(1, { ...attributes, [PG_METRIC_ATTR_ERROR_TYPE]: this.getErrorType(event.error) }); } } catch (error) { this.customLogger?.debug?.( `${LOG_PREFIX5} Failed to record metrics:`, error ); } } getQueryType(operation) { switch (operation.toUpperCase()) { case "SELECT": return "read" /* READ */; case "INSERT": return "write" /* WRITE */; case "UPDATE": return "write" /* WRITE */; case "DELETE": return "write" /* WRITE */; case "CREATE": return "schema" /* SCHEMA */; case "ALTER": return "schema" /* SCHEMA */; case "DROP": return "schema" /* SCHEMA */; default: return "unknown" /* UNKNOWN */; } } static registerDbQueryReceiver(options = {}, logger) { const instrumentation = new _PostgresInstrumentation({ serviceName: options.serviceName, logger, enableHistogram: options.enableHistogram, collectQueryParameters: options.collectQueryParameters, serverAddress: options.serverAddress, serverPort: options.serverPort, parameterSanitizer: options.parameterSanitizer, beforeSpan: options.beforeSpan, afterSpan: options.afterSpan, responseHook: options.responseHook }); instrumentation.enable(); return instrumentation; } }; PostgresInstrumentation.registerDbQueryReceiver; exports.PostgresInstrumentation = PostgresInstrumentation; exports.createOTELEmitter = createOTELEmitter; //# sourceMappingURL=index.cjs.map //# sourceMappingURL=index.cjs.map