UNPKG

@instana/core

Version:
621 lines (516 loc) 19.5 kB
/* * (c) Copyright IBM Corp. 2022 */ 'use strict'; 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 CLOSE_TIMEOUT_IN_MS = process.env.DB2_CLOSE_TIMEOUT_IN_MS || 1000 * 30; exports.spanName = 'ibmdb2'; exports.init = function init() { hook.onModuleLoad('ibm_db', instrument); }; /** * https://github.com/ibmdb/node-ibm_db/blob/master/APIDocumentation.md * * DB2 is a C++ implementation with a JS interface. * https://github.com/ibmdb/node-ibm_db/blob/master/src/odbc_connection.cpp * https://github.com/ibmdb/node-ibm_db/blob/master/src/odbc_statement.cpp * https://github.com/ibmdb/node-ibm_db/blob/master/lib/odbc.js * * queryStream works out of the box, because they use `queryResult` internally * https://github.com/ibmdb/node-ibm_db/blob/master/lib/odbc.js#L1068 */ function instrument(db2) { shimmer.wrap(db2.Database.prototype, 'open', instrumentOpen); shimmer.wrap(db2.Database.prototype, 'openSync', instrumentOpen); shimmer.wrap(db2.Database.prototype, 'query', instrumentQuery); shimmer.wrap(db2.Database.prototype, 'querySync', instrumentQuerySync); shimmer.wrap(db2.Database.prototype, 'queryResult', instrumentQueryResult); shimmer.wrap(db2.Database.prototype, 'queryResultSync', instrumentQueryResultSync); shimmer.wrap(db2.Database.prototype, 'beginTransaction', instrumentBeginTransaction); shimmer.wrap(db2.Database.prototype, 'prepare', instrumentPrepare); shimmer.wrap(db2.Database.prototype, 'prepareSync', instrumentPrepareSync); } /** * We need to set `skipIsTracing` check for db2, because of e.g. prepare * statements or transactions. We don't always know if we are tracing or not. */ function skipTracing(ignoreClsTracing = false) { const skipExitResult = cls.skipExitTracing({ isActive, skipIsTracing: true, skipParentSpanCheck: true, extendedResponse: true }); const isTracing = cls.isTracing(); // CASE: `cls.skipExitTracing` decided to skip, respect that if (skipExitResult.skip) return true; // CASE: the target parent function wants us to ignore cls.isTracing // because e.g. we don't know if we trace (parent got lost) if (ignoreClsTracing) return false; // CASE: We ignore if tracing or not for `allowRootExitSpan`. See cls file. if (skipExitResult.allowRootExitSpan) return false; return !isTracing; } function instrumentOpen(originalFunction) { return function instanaInstrumentationOpen() { // NOTE: connection.open(fn) will throw an error in the library // we can rely on arguments[0] being the connection string. // There is no other format to pass in the connection. this._instanaConnectionString = arguments[0]; return originalFunction.apply(this, arguments); }; } function instrumentQueryResult(originalFunction) { return function instanaInstrumentQueryResult(stmt) { return instrumentQueryResultHelper(this, arguments, originalFunction, stmt, true); }; } function instrumentQueryResultSync(originalFunction) { return function instanaInstrumentQueryResultSync(stmt) { return instrumentQueryResultHelper(this, arguments, originalFunction, stmt, false); }; } /** * We are losing the parentSpan because of the C++ implementation of `beginTransaction`. * We were unable to figure out why the context get's lost. * * We thought it is related to not having used * https://github.com/nodejs/nan/blob/main/doc/node_misc.md#api_nan_asyncresource * * That's why we have to instrument `beginTransaction` and reactivate * the span. * * We do not loose the parentSpan if the customer uses "beginTransactionSync". */ function instrumentBeginTransaction(originalFunction) { return function instanaInstrumentBeginTransaction() { const parentSpan = cls.getCurrentSpan(); const originalCallback = arguments[0]; if (typeof originalCallback !== 'function') { return originalCallback.apply(this, arguments); } arguments[0] = cls.ns.bind(function instanaInstrumentBeginTransactionCallback() { // NOTE: See function description // NOTE: We need to ensure that there is a parentSpan, see 'allowRootExitSpan: true'. if (parentSpan) { cls.setCurrentSpan(parentSpan); } return originalCallback.apply(this, arguments); }); return originalFunction.apply(this, arguments); }; } function instrumentQuery(originalFunction) { return function (stmt) { return instrumentQueryHelper(this, arguments, originalFunction, stmt, true); }; } function instrumentQuerySync(originalFunction) { return function (stmt) { return instrumentQueryHelper(this, arguments, originalFunction, stmt, false); }; } /** * Main difference of prepare and a normal query call: * - you can call prepare once and re-use the statement (higher performance) * * prepare(stmt, cb) is the only possible usage */ function instrumentPrepare(originalFunction) { return function instanaInstrumentPrepare() { const ctx = this; const originalArgs = arguments; const possibleParentSpan = cls.getCurrentEntrySpan(); const originalCallback = originalArgs[1]; if (typeof originalCallback !== 'function') { return originalFunction.apply(this, originalArgs); } originalArgs[1] = function instanaPrepareCallback(err, stmtObject) { if (err) return originalCallback.apply(this, arguments); instrumentExecuteHelper(ctx, originalArgs, stmtObject, possibleParentSpan); return originalCallback.apply(this, arguments); }; return originalFunction.apply(this, originalArgs); }; } function instrumentPrepareSync(originalFunction) { return function instanaInstrumentPrepareSync() { const ctx = this; const originalArgs = arguments; const possibleParentSpan = cls.getCurrentEntrySpan(); const stmtObject = originalFunction.apply(ctx, originalArgs); if (stmtObject instanceof Error) { return stmtObject; } instrumentExecuteHelper(ctx, originalArgs, stmtObject, possibleParentSpan); return stmtObject; }; } /** * ############################## * ##### HELPERS * ############################## */ /** * Goal of this function is to capture errors * happing when the result is fetched from the database. */ function captureFetchError(result, span) { if (!result) return; const asyncFns = ['fetch', 'fetchAll']; const syncFns = ['fetchSync', 'fetchAllSync']; asyncFns.forEach(fnName => { if (result[fnName]) { const originalFn = result[fnName]; result[fnName] = function instanaFetchOverride() { const argsFetch = arguments; const fetchIndex = // eslint-disable-next-line no-nested-ternary argsFetch.length === 1 && typeof argsFetch[0] === 'function' ? 0 : argsFetch.length === 2 && typeof argsFetch[1] === 'function' ? 1 : null; const fetchCb = argsFetch[fetchIndex]; argsFetch[fetchIndex] = function instanaFetchCb(fetchCbErr) { if (fetchCbErr) { span.ec = 1; tracingUtil.setErrorDetails(span, fetchCbErr, 'db2'); } return fetchCb.apply(this, arguments); }; try { return originalFn.apply(this, arguments); } catch (caughtErr) { span.ec = 1; tracingUtil.setErrorDetails(span, caughtErr, 'db2'); throw caughtErr; } }; } }); syncFns.forEach(fnName => { if (result[fnName]) { const originalFn = result[fnName]; result[fnName] = function instanaSyncOverride() { try { const res = originalFn.apply(this, arguments); if (res instanceof Error) { span.ec = 1; tracingUtil.setErrorDetails(span, res, 'db2'); } return res; } catch (err) { span.ec = 1; tracingUtil.setErrorDetails(span, err, 'db2'); return err; } }; } }); } function instrumentQueryHelper(ctx, originalArgs, originalFunction, stmt, isAsync) { if (skipTracing()) { return originalFunction.apply(ctx, originalArgs); } return cls.ns.runAndReturn(() => { const span = createSpan(stmt, instrumentQueryHelper, ctx._instanaConnectionString); // CASE: querySync if (!isAsync) { try { const result = originalFunction.apply(ctx, originalArgs); if (result instanceof Error) { span.ec = 1; tracingUtil.setErrorDetails(span, result, 'db2'); } finishSpan(ctx, result, span); return result; } catch (e) { span.ec = 1; tracingUtil.setErrorDetails(span, e, 'db2'); finishSpan(ctx, null, span); throw e; } } const customerCallbackIndex = // eslint-disable-next-line no-nested-ternary originalArgs.length === 2 && typeof originalArgs[1] === 'function' ? 1 : originalArgs.length === 3 && typeof originalArgs[2] === 'function' ? 2 : null; const customerCallback = customerCallbackIndex != null ? originalArgs[customerCallbackIndex] : null; if (customerCallback) { originalArgs[customerCallbackIndex] = function instanaCallback(err) { if (err) { span.ec = 1; tracingUtil.setErrorDetails(span, err, 'db2'); } finishSpan(ctx, null, span); return customerCallback.apply(this, arguments); }; return originalFunction.apply(ctx, originalArgs); } const resultPromise = originalFunction.apply(ctx, originalArgs); if (resultPromise && typeof resultPromise.then === 'function' && typeof resultPromise.catch === 'function') { resultPromise .then(result => { finishSpan(ctx, result, span); return result; }) .catch(err => { span.ec = 1; tracingUtil.setErrorDetails(span, err, 'db2'); finishSpan(ctx, null, span); return err; }); return resultPromise; } }); } function instrumentExecuteHelper(ctx, originalArgs, stmtObject, prepareCallParentSpan) { let rememberedParentSpan; const canTrace = () => { // cls.getCurrentSpan() : exists if prepare is called outside of the http context // prepareCallParentSpan : exists if prepare call is called inside of the http context const parentSpan = cls.getCurrentSpan() || prepareCallParentSpan; // CASE: There can be only one exit span, skip if (constants.isExitSpan(parentSpan)) { return false; } // CASE: we need to remember the original parent span for further `execute` calls // ensure we set the remembered span once in the first call by checking rememberedParentSpan if (!parentSpan && !rememberedParentSpan) { rememberedParentSpan = parentSpan; } // CASE 1: `cls.isTracing` is false when `execute` is minimum called twice, because // because there is NO current span and no remembered span. // We skip checking `cls.isTracing` because we want to trace the second call. // // CASE 2: `allowRootExitSpan: true` is set, we might not have an entry span at all on // the first execute call. if (skipTracing(!parentSpan || !!prepareCallParentSpan)) { return false; } /** * If we do not set the parent, the span will have no parent! * * We have to set the parent span here, reasons: * - execute is called twice * - prepare call happens in http context, we need to remember it * * CASE: When setting `allowRootExitSpan: true`, the parentSpan might be null. */ const parentToSetAsCurrent = rememberedParentSpan || parentSpan; if (parentToSetAsCurrent) cls.setCurrentSpan(parentToSetAsCurrent); return true; }; const originalExecuteNonQuerySync = stmtObject.executeNonQuerySync; stmtObject.executeNonQuerySync = function instanaExecuteNonQuerySync() { return cls.ns.runAndReturn(() => { if (!canTrace()) { return originalExecuteNonQuerySync.apply(this, arguments); } const span = createSpan(originalArgs[0], instrumentExecuteHelper, ctx._instanaConnectionString); // NOTE: returns row count try { const result = originalExecuteNonQuerySync.apply(this, arguments); finishSpan(ctx, result, span); return result; } catch (err) { span.ec = 1; tracingUtil.setErrorDetails(span, err, 'db2'); finishSpan(ctx, null, span); return err; } }); }; const originalExecuteSync = stmtObject.executeSync; stmtObject.executeSync = function instanaExecuteSync() { return cls.ns.runAndReturn(() => { if (!canTrace()) { return originalExecuteSync.apply(this, arguments); } // NOTE: start one span per execute! const span = createSpan(originalArgs[0], instrumentExecuteHelper, ctx._instanaConnectionString); const result = originalExecuteSync.apply(this, arguments); finishSpan(ctx, result, span); return result; }); }; const originalExecute = stmtObject.execute; stmtObject.execute = function instanaExecute() { return cls.ns.runAndReturn(() => { if (!canTrace()) { return originalExecute.apply(this, arguments); } // NOTE: start one span per execute! const span = createSpan(originalArgs[0], instrumentExecuteHelper, ctx._instanaConnectionString); const args = arguments; const origCallbackIndex = // eslint-disable-next-line no-nested-ternary args.length === 1 && typeof args[0] === 'function' ? 0 : args.length === 2 && typeof args[1] === 'function' ? 1 : null; const origCallback = args[origCallbackIndex]; args[origCallbackIndex] = function instanaExecuteCallback(executeErr, result) { if (executeErr) { span.ec = 1; tracingUtil.setErrorDetails(span, executeErr, 'db2'); finishSpan(ctx, null, span); return origCallback.apply(this, arguments); } finishSpan(ctx, result, span); return origCallback.apply(this, arguments); }; return originalExecute.apply(this, arguments); }); }; } function instrumentQueryResultHelper(ctx, originalArgs, originalFunction, stmt, isAsync) { if (skipTracing()) { return originalFunction.apply(ctx, originalArgs); } return cls.ns.runAndReturn(() => { const span = createSpan(stmt, instrumentQueryResultHelper, ctx._instanaConnectionString); if (!isAsync) { try { const result = originalFunction.apply(ctx, originalArgs); finishSpan(ctx, result, span); return result; } catch (err) { span.ec = 1; tracingUtil.setErrorDetails(span, err, 'db2'); finishSpan(ctx, null, span); return err; } } const customerCallbackIndex = // eslint-disable-next-line no-nested-ternary originalArgs.length === 2 && typeof originalArgs[1] === 'function' ? 1 : originalArgs.length === 3 && typeof originalArgs[2] === 'function' ? 2 : null; const customerCallback = customerCallbackIndex != null ? originalArgs[customerCallbackIndex] : null; if (customerCallback) { originalArgs[customerCallbackIndex] = function instanaCallback(err) { if (err) { span.ec = 1; tracingUtil.setErrorDetails(span, err, 'db2'); } const result = customerCallback.apply(this, arguments); finishSpan(ctx, result, span); return result; }; return originalFunction.apply(ctx, originalArgs); } }); } function createSpan(stmt, fn, connectionStr) { // eslint-disable-next-line max-len // https://github.ibm.com/instana/backend/blob/develop/forge/src/main/java/com/instana/forge/connection/database/ibmdb2/IbmDb2Span.java const span = cls.startSpan({ spanName: exports.spanName, kind: constants.EXIT }); span.stack = tracingUtil.getStackTrace(fn); span.ts = Date.now(); span.data.db2 = { stmt: tracingUtil.shortenDatabaseStatement(stmt), dsn: tracingUtil.sanitizeConnectionStr(connectionStr) }; return span; } function finishSpan(ctx, result, span) { captureFetchError(result, span); if (ctx.conn.inTransaction) { return handleTransaction(ctx, span); } // NOTE: This signalises us the end of the trace // Because we want to capture errors till the customer finished the operation. // That's why we want to wait for this call. The ibm docs say, it's required to call it! if (result && result.closeSync) { const originalCloseSync = result.closeSync; let closeSyncCalled = false; result.closeSync = function instanaCloseSync() { if (closeSyncCalled) return originalCloseSync.apply(this, arguments); closeSyncCalled = true; span.d = Date.now() - span.ts; span.transmit(); return originalCloseSync.apply(this, arguments); }; // CASE: customer forgets to call `result.closeSync` // search for result.closeSync() in: // https://github.com/ibmdb/node-ibm_db/blob/master/APIDocumentation.md // We need to wait for a long time because any sql query can take long setTimeout(() => { if (!closeSyncCalled) { closeSyncCalled = true; span.ec = 1; span.data.db2.error = `'result.closeSync' was not called within ${CLOSE_TIMEOUT_IN_MS}ms.`; tracingUtil.setErrorDetails( span, new Error(`'result.closeSync' was not called within ${CLOSE_TIMEOUT_IN_MS}ms.`), 'db2' ); span.d = Date.now() - span.ts; span.transmit(); } }, CLOSE_TIMEOUT_IN_MS).unref(); } else { span.d = Date.now() - span.ts; span.transmit(); } } function handleTransaction(ctx, span) { // NOTE: This is an internal fn to avoid instrumenting commit, rollback separately const fns = ['endTransaction', 'endTransactionSync']; fns.forEach(fn => { const originalFn = ctx.conn[fn]; // CASE: does fn exist on the lib? if (typeof originalFn === 'function') { ctx.conn[fn] = function instanaEndTransactionOverride() { const originalOnEndCallback = arguments[1]; // CASE: async if (typeof originalOnEndCallback === 'function') { arguments[1] = function instanaOnEndOverride(onEndErr) { if (onEndErr) { span.ec = 1; tracingUtil.setErrorDetails(span, onEndErr, 'db2'); } span.d = Date.now() - span.ts; span.transmit(); return originalOnEndCallback.apply(this, arguments); }; return originalFn.apply(this, arguments); } // CASE: sync try { const result = originalFn.apply(this, arguments); span.d = Date.now() - span.ts; span.transmit(); return result; } catch (err) { span.ec = 1; tracingUtil.setErrorDetails(span, err, 'db2'); span.transmit(); throw err; } }; } }); } exports.activate = function activate() { isActive = true; }; exports.deactivate = function deactivate() { isActive = false; };