UNPKG

@instana/core

Version:
290 lines (246 loc) 9.35 kB
/* * (c) Copyright IBM Corp. 2021 * (c) Copyright Instana Inc. and contributors 2020 */ 'use strict'; 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; tracingUtil.setErrorDetails(span, error, 'pg'); } 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; };