otel-instrumentation-postgres
Version:
OpenTelemetry instrumentation for postgres.js
653 lines (644 loc) • 23.8 kB
JavaScript
'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