@instana/core
Version:
Core library for Instana's Node.js packages
290 lines (246 loc) • 9.35 kB
JavaScript
/*
* (c) Copyright IBM Corp. 2021
* (c) Copyright Instana Inc. and contributors 2020
*/
;
const { LRUCache } = require('lru-cache');
const shimmer = require('../../shimmer');
const hook = require('../../../util/hook');
const tracingUtil = require('../../tracingUtil');
const constants = require('../../constants');
const cls = require('../../cls');
let isActive = false;
const preparedStatements = new LRUCache({ max: 100000 });
// See https://www.postgresql.org/docs/9.3/libpq-connect.html#AEN39692
// Pattern: postgresql://<user>[:<password>]@<netloc>[:<port>]/<dbname>[?params]
// eslint-disable-next-line max-len
const connectionUriRegex =
/^\s*postgres(?:ql)?:\/\/(?:([^:@]+)?(?::.+)?@)?([^:/?#]+)?(?::(\d+))?(?:\/([^?]+))?(?:\?.*)?$/;
// ^protocol user+pass^ ^user ^pass ^netloc ^port ^db ^params
exports.spanName = 'postgres';
exports.batchable = true;
exports.init = function init() {
hook.onModuleLoad('pg-native', instrumentPgNative);
};
function instrumentPgNative(Client) {
shimmer.wrap(Client.prototype, '_awaitResult', shimAwaitResult);
shimmer.wrap(Client.prototype, 'connect', shimConnect);
shimmer.wrap(Client.prototype, 'connectSync', shimConnect);
shimmer.wrap(Client.prototype, 'query', shimQueryOrExecute.bind(null, instrumentedQuery));
shimmer.wrap(Client.prototype, 'querySync', shimQueryOrExecuteSync.bind(null, false));
shimmer.wrap(Client.prototype, 'prepare', shimPrepare);
shimmer.wrap(Client.prototype, 'prepareSync', shimPrepare);
shimmer.wrap(Client.prototype, 'execute', shimQueryOrExecute.bind(null, instrumentedExecute));
shimmer.wrap(Client.prototype, 'executeSync', shimQueryOrExecuteSync.bind(null, true));
}
function shimConnect(original) {
return function (connectionString) {
const connectionParams = exports.parseConnectionParameters(connectionString);
if (Object.keys(connectionParams).length > 0) {
this._instana = connectionParams;
}
return original.apply(this, arguments);
};
}
function shimAwaitResult(original) {
return function () {
if (cls.skipExitTracing({ isActive }) || typeof arguments[0] !== 'function') {
return original.apply(this, arguments);
}
const originalArgs = new Array(arguments.length);
originalArgs[0] = cls.ns.bind(arguments[0]);
for (let i = 1; i < arguments.length; i++) {
originalArgs[i] = arguments[i];
}
return original.apply(this, originalArgs);
};
}
function shimPrepare(original) {
return function (statementName, text) {
preparedStatements.set(statementName, text);
return original.apply(this, arguments);
};
}
function shimQueryOrExecute(instrumented, original) {
return function () {
if (cls.skipExitTracing({ isActive })) {
return original.apply(this, arguments);
}
const originalArgs = new Array(arguments.length);
for (let i = 0; i < arguments.length; i++) {
originalArgs[i] = arguments[i];
}
return instrumented(this, original, originalArgs);
};
}
function instrumentedQuery(ctx, originalQuery, originalArgs) {
const statement = originalArgs[0];
const stackTraceRef = instrumentedQuery;
return startSpan(ctx, originalQuery, originalArgs, statement, stackTraceRef);
}
function instrumentedExecute(ctx, originalExecute, originalArgs) {
const statement = preparedStatements.get(originalArgs[0]);
const stackTraceRef = instrumentedExecute;
return startSpan(ctx, originalExecute, originalArgs, statement, stackTraceRef);
}
function shimQueryOrExecuteSync(isExecute, original) {
return function () {
if (cls.skipExitTracing({ isActive })) {
return original.apply(this, arguments);
}
const originalArgs = new Array(arguments.length);
for (let i = 0; i < arguments.length; i++) {
originalArgs[i] = arguments[i];
}
const statement = isExecute ? preparedStatements.get(originalArgs[0]) : originalArgs[0];
const resultAndSpan = startSpanBeforeSync(this, original, originalArgs, statement, shimQueryOrExecuteSync);
finishSpan(resultAndSpan.error, resultAndSpan.span);
if (resultAndSpan.error) {
throw resultAndSpan.error;
}
return resultAndSpan.result;
};
}
function startSpan(ctx, originalFn, originalArgs, statement, stackTraceRef) {
return cls.ns.runAndReturn(() => {
const span = cls.startSpan({
spanName: exports.spanName,
kind: constants.EXIT
});
span.stack = tracingUtil.getStackTrace(stackTraceRef);
span.data.pg = {
stmt: tracingUtil.shortenDatabaseStatement(statement),
host: ctx._instana ? ctx._instana.host : undefined,
port: ctx._instana ? ctx._instana.port : undefined,
user: ctx._instana ? ctx._instana.user : undefined,
db: ctx._instana ? ctx._instana.db : undefined
};
let originalCallback;
let callbackIndex = -1;
for (let i = 1; i < originalArgs.length; i++) {
if (typeof originalArgs[i] === 'function') {
originalCallback = originalArgs[i];
callbackIndex = i;
break;
}
}
if (callbackIndex >= 0) {
const wrappedCallback = function (error) {
finishSpan(error, span);
return originalCallback.apply(this, arguments);
};
originalArgs[callbackIndex] = cls.ns.bind(wrappedCallback);
}
return originalFn.apply(ctx, originalArgs);
});
}
function startSpanBeforeSync(ctx, originalFn, originalArgs, statement, stackTraceRef) {
return cls.ns.runAndReturn(() => {
const span = cls.startSpan({
spanName: exports.spanName,
kind: constants.EXIT
});
span.stack = tracingUtil.getStackTrace(stackTraceRef);
span.data.pg = {
stmt: tracingUtil.shortenDatabaseStatement(statement),
host: ctx._instana ? ctx._instana.host : undefined,
port: ctx._instana ? ctx._instana.port : undefined,
user: ctx._instana ? ctx._instana.user : undefined,
db: ctx._instana ? ctx._instana.db : undefined
};
let result;
let error;
try {
result = originalFn.apply(ctx, originalArgs);
} catch (_error) {
error = _error;
}
return {
result,
error,
span
};
});
}
function finishSpan(error, span) {
if (error) {
span.ec = 1;
span.data.pg.error = tracingUtil.getErrorDetails(error);
}
span.d = Date.now() - span.ts;
span.transmit();
}
// exported for testability
exports.parseConnectionParameters = function parseConnectionParameters(connectionString) {
const connectionParams = {};
if (typeof connectionString === 'string') {
connectionString = connectionString.trim();
if (connectionString.indexOf('postgres') === 0) {
parseConnectionUri(connectionString, connectionParams);
} else {
parseKeyValueConnectionString(connectionString, connectionParams);
}
} else {
parseConnectionEnvVars(connectionParams);
}
if (connectionParams.hostaddr && !connectionParams.host) {
connectionParams.host = connectionParams.hostaddr;
}
delete connectionParams.hostaddr;
if (connectionParams.dbname) {
connectionParams.db = connectionParams.dbname;
delete connectionParams.dbname;
}
return connectionParams;
};
function parseConnectionUri(connectionString, connectionParams) {
// See https://www.postgresql.org/docs/9.3/libpq-connect.html#AEN39692
const matchResult = connectionUriRegex.exec(connectionString);
if (matchResult) {
readConnectionParamFromRegexMatch(connectionParams, matchResult, 1, 'user');
readConnectionParamFromRegexMatch(connectionParams, matchResult, 2, 'host');
readConnectionParamFromRegexMatch(connectionParams, matchResult, 3, 'port');
readConnectionParamFromRegexMatch(connectionParams, matchResult, 4, 'db');
}
}
function readConnectionParamFromRegexMatch(connectionParams, matchResult, index, key) {
if (matchResult[index] != null) {
connectionParams[key] = matchResult[index];
}
}
function parseKeyValueConnectionString(connectionString, connectionParams) {
// eslint-disable-next-line max-len
// See https://www.postgresql.org/docs/9.3/libpq-connect.html#LIBPQ-CONNSTRING and https://www.postgresql.org/docs/9.3/libpq-connect.html#LIBPQ-PARAMKEYWORDS
connectionString
.split(' ')
.map(pair => pair.trim())
.forEach(pair =>
['host', 'hostaddr', 'port', 'dbname', 'user'].forEach(key =>
parseConnectStringKeyValuePair(connectionParams, pair, key)
)
);
}
function parseConnectStringKeyValuePair(connectionParams, pair, key) {
if (pair.toLowerCase().indexOf(`${key}=`) === 0) {
connectionParams[key] = pair.split('=')[1];
}
}
function parseConnectionEnvVars(connectionParams) {
// see https://www.postgresql.org/docs/9.3/libpq-envars.html
parseEnvVar(connectionParams, 'PGHOST', 'host');
parseEnvVar(connectionParams, 'PGHOSTADDR', 'hostaddr');
parseEnvVar(connectionParams, 'PGPORT', 'port');
parseEnvVar(connectionParams, 'PGDATABASE', 'db');
parseEnvVar(connectionParams, 'PGUSER', 'user');
}
function parseEnvVar(connectionParams, keyEnvVar, keyConnectionParams) {
if (process.env[keyEnvVar]) {
connectionParams[keyConnectionParams] = process.env[keyEnvVar];
}
}
exports.activate = function activate() {
isActive = true;
};
exports.deactivate = function deactivate() {
isActive = false;
};