lib/instrumentation/modules/mysql2.js (113 lines of code) (raw):

/* * Copyright Elasticsearch B.V. and other contributors where applicable. * Licensed under the BSD 2-Clause License; you may not use this file except in * compliance with the BSD 2-Clause License. */ 'use strict'; var semver = require('semver'); var sqlSummary = require('sql-summary'); var shimmer = require('../shimmer'); var symbols = require('../../symbols'); var { getDBDestination } = require('../context'); module.exports = function (mysql2, agent, { version, enabled }) { if (!enabled) { return mysql2; } if (!semver.satisfies(version, '>=1 <4')) { agent.logger.debug( 'mysql2 version %s not supported - aborting...', version, ); return mysql2; } var ins = agent._instrumentation; // mysql2@3.11.5 added BaseConnection class which is extended by Connection // but is not in the public API so we need to extract it via prototype chain // ref: https://github.com/sidorares/node-mysql2/pull/3081 const baseClass = Object.getPrototypeOf(mysql2.Connection); const baseProto = baseClass.prototype; const hasQuery = typeof baseProto?.query === 'function'; const hasExec = typeof baseProto?.execute === 'function'; const shouldPatchBase = hasQuery && hasExec; if (shouldPatchBase) { shimmer.wrap(baseProto, 'query', wrapQuery); shimmer.wrap(baseProto, 'execute', wrapQuery); } else { shimmer.wrap(mysql2.Connection.prototype, 'query', wrapQuery); shimmer.wrap(mysql2.Connection.prototype, 'execute', wrapQuery); } return mysql2; function wrapQuery(original) { return function wrappedQuery(sql, values, cb) { agent.logger.debug('intercepted call to mysql2.%s', original.name); var span = ins.createSpan(null, 'db', 'mysql', 'query', { exitSpan: true, }); if (!span) { return original.apply(this, arguments); } if (this[symbols.knexStackObj]) { span.customStackTrace(this[symbols.knexStackObj]); this[symbols.knexStackObj] = null; } let hasCallback = false; const wrapCallback = function (origCallback) { hasCallback = true; return ins.bindFunction(function wrappedCallback(_err) { span.end(); return origCallback.apply(this, arguments); }); }; let host, port, user, database; if (typeof this.config === 'object') { ({ host, port, user, database } = this.config); } span._setDestinationContext(getDBDestination(host, port)); let sqlStr; switch (typeof sql) { case 'string': sqlStr = sql; break; case 'object': // `mysql2.{query,execute}` support the only arg being an instance // of the internal mysql2 `Command` object. if (typeof sql.onResult === 'function') { sql.onResult = wrapCallback(sql.onResult); } sqlStr = sql.sql; break; case 'function': arguments[0] = wrapCallback(sql); break; } if (sqlStr) { span.setDbContext({ type: 'sql', instance: database, user, statement: sqlStr, }); span.name = sqlSummary(sqlStr); } else { span.setDbContext({ type: 'sql', instance: database, user }); } if (typeof values === 'function') { arguments[1] = wrapCallback(values); } else if (typeof cb === 'function') { arguments[2] = wrapCallback(cb); } const spanRunContext = ins.currRunContext().enterSpan(span); const result = ins.withRunContext( spanRunContext, original, this, ...arguments, ); if (result && !hasCallback) { ins.bindEmitter(result); shimmer.wrap(result, 'emit', function (origEmit) { return function (event) { switch (event) { case 'error': case 'close': case 'end': span.end(); } return origEmit.apply(this, arguments); }; }); } return result; }; } };