diff --git a/.ci/tav.json b/.ci/tav.json index 8ffb8edce0..ebc48ee4d6 100644 --- a/.ci/tav.json +++ b/.ci/tav.json @@ -31,6 +31,7 @@ { "name": "mongodb-core", "minMajorVersion": 8 }, { "name": "mysql", "minMajorVersion": 8 }, { "name": "mysql2", "minMajorVersion": 8 }, + { "name": "mariadb", "minMajorVersion": 14 }, { "name": "next", "minMajorVersion": 14 }, { "name": "pg", "minMajorVersion": 8 }, { "name": "redis", "minMajorVersion": 8 }, diff --git a/.tav.yml b/.tav.yml index 7fd1ae6e54..af8c1677df 100644 --- a/.tav.yml +++ b/.tav.yml @@ -54,6 +54,13 @@ mysql2: - node test/instrumentation/modules/mysql2/mysql.test.js - node test/instrumentation/modules/mysql2/pool-release-1.test.js +mariadb: + - versions: '>=3.1.0 <4' + node: '>=14.0.0' + commands: + - node test/instrumentation/modules/mariadb/mariadb.test.js + - node test/instrumentation/modules/mariadb/pool-release-1.test.js + redis: - versions: '>=2.0.0 <4.0.0' commands: node test/instrumentation/modules/redis-2-3.test.js diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index fde29a4ea2..136dc86e73 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -33,7 +33,6 @@ Notes: See the <> guide. - ==== Unreleased [float] @@ -41,6 +40,7 @@ See the <> guide. [float] ===== Features +Add support for mariadb >=3.1.0 instrumentation [float] ===== Bug fixes @@ -56,7 +56,6 @@ compatible runtimes (which is advisory) was not updated until now. [float] ===== Chores - [[release-notes-4.5.4]] ==== 4.5.4 - 2024/05/13 diff --git a/docs/supported-technologies.asciidoc b/docs/supported-technologies.asciidoc index 5461eaec9b..b4e56667c0 100644 --- a/docs/supported-technologies.asciidoc +++ b/docs/supported-technologies.asciidoc @@ -137,6 +137,7 @@ so those should be supported as well. |https://www.npmjs.com/package/mongoose[mongoose] |>=5.7.0 <8 |Supported via mongodb |https://www.npmjs.com/package/mysql[mysql] |^2.0.0 |Will instrument all queries |https://www.npmjs.com/package/mysql2[mysql2] |>=1.0.0 <4.0.0 |Will instrument all queries +|https://www.npmjs.com/package/mariadb[mariadb] |>=3.1.0 <4.0.0 |Will instrument all queries |https://www.npmjs.com/package/pg[pg] |>=4.0.0 <9.0.0 |Will instrument all queries |https://www.npmjs.com/package/redis[redis] |>=2.0.0 <5.0.0 |Will instrument all queries |https://www.npmjs.com/package/tedious[tedious] |>=1.9 <19.0.0 | (Excluding v4.0.0.) Will instrument all queries diff --git a/lib/instrumentation/index.js b/lib/instrumentation/index.js index 8af059cff2..c291ef2bcb 100644 --- a/lib/instrumentation/index.js +++ b/lib/instrumentation/index.js @@ -99,6 +99,9 @@ var MODULE_PATCHERS = [ }, { modPath: 'mysql' }, { modPath: 'mysql2' }, + { modPath: 'mariadb' }, + { modPath: 'mariadb/callback', patcher: './modules/mariadb.js' }, + { modPath: 'mariadb/callback.js', patcher: './modules/mariadb.js' }, { modPath: 'next' }, { modPath: 'next/dist/server/api-utils/node.js' }, { modPath: 'next/dist/server/dev/next-dev-server.js' }, diff --git a/lib/instrumentation/modules/mariadb.js b/lib/instrumentation/modules/mariadb.js new file mode 100644 index 0000000000..9dfb012398 --- /dev/null +++ b/lib/instrumentation/modules/mariadb.js @@ -0,0 +1,339 @@ +/* + * 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 { getDBDestination } = require('../context'); + +module.exports = function ( + mariadb, + agent, + { version, enabled, name: pkgName }, +) { + if (!enabled) { + return mariadb; + } + + if (!semver.satisfies(version, '>=3.1.0')) { + agent.logger.debug( + 'mariab version %s not supported - aborting...', + version, + ); + return mariadb; + } + + var ins = agent._instrumentation; + let defaultConfig = mariadb.defaultOptions(); + let config = {}; + + shimmer.wrap(mariadb, 'createConnection', wrapConnection); + shimmer.wrap(mariadb, 'createPool', wrapPool); + shimmer.wrap(mariadb, 'createPoolCluster', wrapPool); + + return mariadb; + + function wrapPool(original) { + return function wrappedPool() { + let result = original.apply(this, arguments); + shimmer.wrap( + result, + 'getConnection', + function wrapGetConnection(...args) { + if (typeof arguments[0] === 'object') { + config = { + ...defaultConfig, + ...arguments[0], + }; + } + + return wrapConnection.apply(result, args); + }, + ); + + if (typeof arguments[0] === 'object') { + config = { + ...defaultConfig, + ...arguments[0], + }; + } + + if (typeof result.add === 'function') { + shimmer.wrap(result, 'add', wrapAdd); + } + if (typeof result.of === 'function') { + shimmer.wrap(result, 'of', wrapPool); + } + + if (typeof result.query === 'function') { + shimmer.wrap(result, 'query', wrapQuery); + } + if (typeof result.execute === 'function') { + shimmer.wrap(result, 'execute', wrapQuery); + } + + if (typeof result.queryStream === 'function') { + shimmer.wrap(result, 'queryStream', wrapQuery); + } + if (typeof result.batch === 'function') { + shimmer.wrap(result, 'batch', wrapQuery); + } + return result; + }; + } + + function wrapAdd(original) { + return function wrappedAdd() { + config = { + ...defaultConfig, + ...arguments[1], + }; + return original.apply(this, arguments); + }; + } + + function wrapConnection(original) { + return function wrappedConnection() { + if (typeof arguments[0] === 'object') { + config = { + ...defaultConfig, + ...arguments[0], + }; + } + + if (!pkgName.includes('callback')) { + return original.apply(this, arguments).then((res) => { + if (typeof res.query === 'function') { + shimmer.wrap(res, 'query', wrapQuery); + } + if (typeof res.execute === 'function') { + shimmer.wrap(res, 'execute', wrapQuery); + } + + if (typeof res.queryStream === 'function') { + shimmer.wrap(res, 'queryStream', wrapQuery); + } + + if (typeof res.batch === 'function') { + shimmer.wrap(res, 'batch', wrapQuery); + } + + if (typeof res.prepare === 'function') { + shimmer.wrap(res, 'prepare', wrapPrepare); + } + + return Promise.resolve(res); + }); + } + + if (typeof arguments[arguments.length - 1] === 'function') { + return original.apply(this, [ + ...[...arguments].slice(0, arguments.length - 1), + (err, conn) => { + if (err) return arguments[0](err); + + if (typeof conn.query === 'function') { + shimmer.wrap(conn, 'query', wrapQuery); + } + if (typeof conn.execute === 'function') { + shimmer.wrap(conn, 'execute', wrapQuery); + } + + if (typeof conn.queryStream === 'function') { + shimmer.wrap(conn, 'queryStream', wrapQuery); + } + if (typeof conn.batch === 'function') { + shimmer.wrap(conn, 'batch', wrapQuery); + } + + return arguments[0](err, conn); + }, + ]); + } else { + let result = original.apply(this, arguments); + + if (typeof result.query === 'function') { + shimmer.wrap(result, 'query', wrapQuery); + } + if (typeof result.execute === 'function') { + shimmer.wrap(result, 'execute', wrapQuery); + } + + if (typeof result.queryStream === 'function') { + shimmer.wrap(result, 'queryStream', wrapQuery); + } + if (typeof result.batch === 'function') { + shimmer.wrap(result, 'batch', wrapQuery); + } + + return result; + } + }; + } + function wrapQuery(original, name) { + return function wrappedQuery(sql, values, cb) { + agent.logger.debug('intercepted call to mariadb.%s', original.name); + var span = ins.createSpan(null, 'db', 'mariadb', 'query', { + exitSpan: true, + }); + if (!span) { + return original.apply(this, arguments); + } + + 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 config === 'object') { + ({ host, port, user, database } = config); + } + + span._setDestinationContext(getDBDestination(host, port)); + let sqlStr; + switch (typeof sql) { + case 'string': + sqlStr = sql; + break; + case 'object': + 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); + } + + if ( + !hasCallback && + name !== 'queryStream' && + (name !== 'query' || !pkgName.includes('callback')) + ) { + return original.apply(this, arguments).then((awaitedResult) => { + span.end(); + return awaitedResult; + }); + } + + if ( + name === 'queryStream' || + (name === 'query' && pkgName.includes('callback') && !hasCallback) + ) { + let newResult = original.apply(this, arguments); + + ins.bindEmitter(newResult); + shimmer.wrap(newResult, 'emit', function (origEmit) { + return function (event) { + switch (event) { + case 'error': + case 'end': + span.end(); + } + return origEmit.apply(this, arguments); + }; + }); + return newResult; + } else { + return original.apply(this, arguments); + } + }; + } + function wrapPrepare(original, name) { + return function wrappedPrepare(sql) { + return original.apply(this, arguments).then((newResult) => { + function wrapPreparedExecute(original, name) { + return function wrappedQuery(values) { + agent.logger.debug('intercepted call to mariadb.%s', original.name); + var span = ins.createSpan(null, 'db', 'mariadb', 'query', { + exitSpan: true, + }); + if (!span) { + return original.apply(this, arguments); + } + + let host, port, user, database; + if (typeof config === 'object') { + ({ host, port, user, database } = config); + } + + span._setDestinationContext(getDBDestination(host, port)); + let sqlStr; + switch (typeof sql) { + case 'string': + sqlStr = sql; + break; + case 'object': + sqlStr = sql.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 (name === 'executeStream') { + let newResult = original.apply(this, arguments); + + ins.bindEmitter(newResult); + shimmer.wrap(newResult, 'emit', function (origEmit) { + return function (event) { + switch (event) { + case 'error': + case 'end': + span.end(); + } + return origEmit.apply(this, arguments); + }; + }); + return newResult; + } else { + return original.apply(this, arguments).then((awaitedResult) => { + span.end(); + return awaitedResult; + }); + } + }; + } + shimmer.wrap(newResult, 'execute', wrapPreparedExecute); + shimmer.wrap(newResult, 'executeStream', wrapPreparedExecute); + + return newResult; + }); + }; + } +}; diff --git a/package-lock.json b/package-lock.json index ab1853b7e3..023183c312 100644 --- a/package-lock.json +++ b/package-lock.json @@ -103,6 +103,7 @@ "koa-bodyparser": "^4.3.0", "koa-router": "^12.0.0", "lambda-local": "^2.0.2", + "mariadb": "^3.3.0", "memcached": "^2.2.2", "mimic-response": "1.0.0", "mkdirp": "^3.0.1", @@ -6534,6 +6535,12 @@ "@types/range-parser": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==", + "dev": true + }, "node_modules/@types/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", @@ -13242,9 +13249,9 @@ } }, "node_modules/lru-cache": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", - "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", "engines": { "node": "14 || >=16.14" } @@ -13276,6 +13283,34 @@ "resolved": "https://registry.npmjs.org/mapcap/-/mapcap-1.0.0.tgz", "integrity": "sha512-KcNlZSlFPx+r1jYZmxEbTVymG+dIctf10WmWkuhrhrblM+KMoF77HelwihL5cxYlORye79KoR4IlOOk99lUJ0g==" }, + "node_modules/mariadb": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/mariadb/-/mariadb-3.3.0.tgz", + "integrity": "sha512-sAL4bJgbfCAtXcE8bXI+NAMzVaPNkIU8hRZUXYfgNFoWB9U57G3XQiMeCx/A6IrS6y7kGwBLylrwgsZQ8kUYlw==", + "dev": true, + "dependencies": { + "@types/geojson": "^7946.0.14", + "@types/node": "^20.11.17", + "denque": "^2.1.0", + "iconv-lite": "^0.6.3", + "lru-cache": "^10.2.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/mariadb/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/measured-core": { "version": "1.51.1", "resolved": "https://registry.npmjs.org/measured-core/-/measured-core-1.51.1.tgz", @@ -23404,6 +23439,12 @@ "@types/range-parser": "*" } }, + "@types/geojson": { + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==", + "dev": true + }, "@types/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", @@ -28480,9 +28521,9 @@ "dev": true }, "lru-cache": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", - "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==" + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==" }, "make-dir": { "version": "2.1.0", @@ -28507,6 +28548,30 @@ "resolved": "https://registry.npmjs.org/mapcap/-/mapcap-1.0.0.tgz", "integrity": "sha512-KcNlZSlFPx+r1jYZmxEbTVymG+dIctf10WmWkuhrhrblM+KMoF77HelwihL5cxYlORye79KoR4IlOOk99lUJ0g==" }, + "mariadb": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/mariadb/-/mariadb-3.3.0.tgz", + "integrity": "sha512-sAL4bJgbfCAtXcE8bXI+NAMzVaPNkIU8hRZUXYfgNFoWB9U57G3XQiMeCx/A6IrS6y7kGwBLylrwgsZQ8kUYlw==", + "dev": true, + "requires": { + "@types/geojson": "^7946.0.14", + "@types/node": "^20.11.17", + "denque": "^2.1.0", + "iconv-lite": "^0.6.3", + "lru-cache": "^10.2.0" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, "measured-core": { "version": "1.51.1", "resolved": "https://registry.npmjs.org/measured-core/-/measured-core-1.51.1.tgz", diff --git a/package.json b/package.json index 66bc724c92..1c085e4cf8 100644 --- a/package.json +++ b/package.json @@ -184,6 +184,7 @@ "koa-bodyparser": "^4.3.0", "koa-router": "^12.0.0", "lambda-local": "^2.0.2", + "mariadb": "^3.3.0", "memcached": "^2.2.2", "mimic-response": "1.0.0", "mkdirp": "^3.0.1", @@ -214,4 +215,4 @@ "wait-on": "^7.0.1", "ws": "^7.2.1" } -} +} \ No newline at end of file diff --git a/test/docker-compose.yml b/test/docker-compose.yml index a87e4435c3..a480fd5ce2 100644 --- a/test/docker-compose.yml +++ b/test/docker-compose.yml @@ -61,6 +61,20 @@ services: timeout: 10s retries: 30 + mariadb: + image: bitnami/mariadb:11.2 + environment: + ALLOW_EMPTY_PASSWORD: 1 + ports: + - "3306:3306" + volumes: + - nodemariadata:/bitnami/mariadb + healthcheck: + test: ["CMD", "mariadb" ,"-h", "mariadb", "-P", "3306", "-u", "root", "-e", "SELECT 1"] + interval: 1s + timeout: 10s + retries: 30 + redis: image: redis ports: @@ -185,6 +199,8 @@ volumes: driver: local nodemysqldata: driver: local + nodemariadata: + driver: local nodeesdata: driver: local nodecassandradata: diff --git a/test/instrumentation/modules/mariadb/_utils.js b/test/instrumentation/modules/mariadb/_utils.js new file mode 100644 index 0000000000..e68efa5746 --- /dev/null +++ b/test/instrumentation/modules/mariadb/_utils.js @@ -0,0 +1,40 @@ +/* + * 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 mariadb = require('mariadb/callback'); + +exports.reset = reset; +exports.credentials = credentials; + +var DEFAULTS = { + host: process.env.MYSQL_HOST || 'localhost', + user: process.env.MYSQL_USER || 'root', + password: process.env.MYSQL_PASSWORD, + database: process.env.MYSQL_DATABASE || 'test_elastic_apm', +}; + +function credentials(conf) { + return Object.assign({}, DEFAULTS, conf); +} + +function reset(cb) { + var client = mariadb.createConnection( + credentials({ database: 'test_elastic_apm' }), + ); + + client.connect(function (err) { + if (err) throw err; + client.query('DROP DATABASE IF EXISTS test_elastic_apm', function (err) { + if (err) throw err; + client.query('CREATE DATABASE test_elastic_apm', function (err) { + if (err) throw err; + client.end(cb); + }); + }); + }); +} diff --git a/test/instrumentation/modules/mariadb/mariadb.test.js b/test/instrumentation/modules/mariadb/mariadb.test.js new file mode 100644 index 0000000000..027bf41f7a --- /dev/null +++ b/test/instrumentation/modules/mariadb/mariadb.test.js @@ -0,0 +1,846 @@ +/* + * 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'; + +if (process.env.GITHUB_ACTIONS === 'true' && process.platform === 'win32') { + console.log('# SKIP: GH Actions do not support docker services on Windows'); + process.exit(0); +} + +const semver = require('semver'); +const { safeGetPackageVersion, findObjInArray } = require('../../../_utils'); +const mariadbVer = safeGetPackageVersion('mariadb'); +if (semver.gte(mariadbVer, '3.0.0') && semver.lt(process.version, '14.0.0')) { + console.log( + `# SKIP mariadb@${mariadbVer} does not support node ${process.version}`, + ); + process.exit(); +} + +var agent = require('../../../..').start({ + serviceName: 'test', + secretToken: 'test', + captureExceptions: false, + metricsInterval: 0, + centralConfig: false, + spanCompressionEnabled: false, +}); + +var mariadb = require('mariadb/callback'); +var mariadbPromise = require('mariadb'); +var test = require('tape'); + +var utils = require('./_utils'); +var mockClient = require('../../../_mock_http_client'); + +var connectionOptions = utils.credentials(); +var queryable; +var queryablePromise; +var factories = [ + [createConnection, 'connection', true], + [createPool, 'pool', true], + [createPoolAndGetConnection, 'pool > connection', true], + [createPoolClusterAndGetConnection, 'poolCluster > connection', true], + [ + createPoolClusterAndGetConnectionViaOf, + 'poolCluster > of > connection', + false, + ], + [ + createPoolClusterAndGetConnectionViaOfDirect, + 'poolCluster > ofConnection', + false, + ], +]; +// var executors = ['execute']; +// var executors = ['queryStream']; +var executors = ['query', 'execute', 'queryStream', 'batch', 'prepare']; + +var universalArgumentSets = [ + { + names: ['sql'], + query: 'SELECT 1 + 1 AS solution', + values: (query, cb) => [query, cb], + }, + { + names: ['sql', 'values'], + query: 'SELECT 1 + ? AS solution', + values: (query, cb) => [query, ['1'], cb], + }, + { + names: ['options'], + query: 'SELECT 1 + 1 AS solution', + values: (query, cb) => [{ sql: query }, cb], + }, + { + names: ['options', 'values'], + query: 'SELECT 1 + ? AS solution', + values: (query, cb) => [{ sql: query }, ['1'], cb], + }, +]; + +var batchArgumentSets = [ + { + names: ['sql', 'values'], + query: 'INSERT INTO users (name) VALUES (?)', + values: (query, cb) => [query, [['test'], ['test2']], cb], + }, + { + names: ['options', 'values'], + query: 'INSERT INTO users (name) VALUES (?)', + values: (query, cb) => [{ sql: query }, [['test'], ['test2']], cb], + }, +]; + +factories.forEach(function (f) { + var factory = f[0]; + var type = f[1]; + var hasCallback = f[2]; + + test('mariadb.' + factory.name, function (t) { + t.on('end', teardown); + executors.forEach(function (executor) { + t.test(executor, function (t) { + var argumentSets = + executor === 'batch' ? batchArgumentSets : universalArgumentSets; + + if (executor === 'queryStream') { + if (!['pool'].includes(type)) { + t.test('streaming promise', function (t) { + argumentSets.forEach(function (argumentSet) { + var query = argumentSet.query; + var names = argumentSet.names; + var values = argumentSet.values; + var name = `${type}.${executor}(${names.join(', ')})`; + var args = values(query); + t.test(name, function (t) { + resetAgent(function (data) { + assertBasicQuery(t, query, data); + t.end(); + }); + factory(function () { + agent.startTransaction('foo'); + var stream = queryablePromise[executor].apply( + queryablePromise, + args, + ); + t.ok( + agent.currentSpan === null, + 'mariadb span should not spill into calling code', + ); + basicQueryStream(stream, t); + }); + }); + }); + }); + if (hasCallback) { + t.test('streaming callback', function (t) { + argumentSets.forEach(function (argumentSet) { + var query = argumentSet.query; + var names = argumentSet.names; + var values = argumentSet.values; + var name = `${type}.${executor}(${names.join(', ')})`; + var args = values(query); + t.test(name, function (t) { + resetAgent(function (data) { + assertBasicQuery(t, query, data); + t.end(); + }); + factory(function () { + agent.startTransaction('foo'); + var stream = queryable.query.apply(queryable, args); + t.ok( + agent.currentSpan === null, + 'mariadb span should not spill into calling code', + ); + basicQueryStream(stream, t); + }); + }); + }); + }); + } + } else { + t.end(); + } + } else if (executor === 'prepare') { + if (!['pool'].includes(type)) { + t.test('prepare', function (t) { + argumentSets.forEach(function (argumentSet) { + var query = argumentSet.query; + var names = argumentSet.names; + var values = argumentSet.values; + var name = `${type}.${executor}(${names.join(', ')})`; + var args = values(query); + t.test(name, function (t) { + resetAgent(function (data) { + assertBasicQuery(t, query, data); + t.end(); + }); + factory(function () { + agent.startTransaction('foo'); + queryablePromise[executor] + .apply(queryablePromise, [args[0]]) + .then((preparedStatement) => { + var result = preparedStatement.execute(args[1]); + + t.ok( + agent.currentSpan === null, + 'mariadb span should not spill into calling code', + ); + basicQueryPromise(t, result); + }); + }); + }); + }); + }); + t.test('prepare stream', function (t) { + argumentSets.forEach(function (argumentSet) { + var query = argumentSet.query; + var names = argumentSet.names; + var values = argumentSet.values; + var name = `${type}.${executor}(${names.join(', ')})`; + var args = values(query); + t.test(name, function (t) { + resetAgent(function (data) { + assertBasicQuery(t, query, data); + t.end(); + }); + factory(function () { + agent.startTransaction('foo'); + queryablePromise[executor] + .apply(queryablePromise, [args[0]]) + .then((preparedStatement) => { + var result = preparedStatement.executeStream(args[1]); + + t.ok( + agent.currentSpan === null, + 'mariadb span should not spill into calling code', + ); + basicQueryStream(result, t); + }); + }); + }); + }); + }); + } else { + t.end(); + } + } else { + if (hasCallback) { + t.test('callback', function (t) { + argumentSets.forEach(function (argumentSet) { + var query = argumentSet.query; + var names = argumentSet.names; + var values = argumentSet.values; + var name = `${type}.${executor}(${names.join(', ')}, callback)`; + var args = + executor === 'batch' + ? values(query, batchQueryCallback(t)) + : values(query, basicQueryCallback(t)); + t.test(name, function (t) { + resetAgent(function (data) { + assertBasicQuery( + t, + query, + data, + executor === 'batch' ? 'INSERT INTO users' : 'SELECT', + ); + t.end(); + }); + factory(function () { + agent.startTransaction('foo'); + queryable[executor].apply(queryable, args); + t.ok( + agent.currentSpan === null, + 'mariadb span should not spill into calling code', + ); + }); + }); + }); + }); + } + t.test('promise', function (t) { + argumentSets.forEach(function (argumentSet) { + var query = argumentSet.query; + var names = argumentSet.names; + var values = argumentSet.values; + var name = `${type}.${executor}(${names.join(', ')})`; + var args = values(query); + t.test(name, function (t) { + resetAgent(function (data) { + assertBasicQuery( + t, + query, + data, + executor === 'batch' ? 'INSERT INTO users' : 'SELECT', + ); + t.end(); + }); + factory(function () { + agent.startTransaction('foo'); + var promise = queryablePromise[executor].apply( + queryablePromise, + args, + ); + t.ok( + agent.currentSpan === null, + 'mariadb span should not spill into calling code', + ); + if (executor === 'batch') { + batchQueryPromise(t, promise); + } else { + basicQueryPromise(t, promise); + } + }); + }); + }); + }); + } + }); + }); + + t.test('simultaneous queries', function (t) { + t.test('on same connection', function (t) { + resetAgent(4, function (data) { + t.strictEqual(data.transactions.length, 1); + t.strictEqual(data.spans.length, 3); + + var trans = data.transactions[0]; + + t.strictEqual(trans.name, 'foo'); + + data.spans.forEach(function (span) { + assertSpan(t, span, sql); + t.equal( + span.parent_id, + trans.id, + 'each mariadb span is a child of the transaction', + ); + }); + + t.end(); + }); + + var sql = 'SELECT 1 + ? AS solution'; + + factory(function () { + var n = 0; + var trans = agent.startTransaction('foo'); + + queryablePromise.query(sql, [1]).then((rows) => { + t.strictEqual(rows[0].solution, 2); + if (++n === 3) done(); + }); + queryablePromise.query(sql, [2]).then((rows) => { + t.strictEqual(rows[0].solution, 3); + if (++n === 3) done(); + }); + queryablePromise.query(sql, [3]).then((rows) => { + t.strictEqual(rows[0].solution, 4); + if (++n === 3) done(); + }); + + function done() { + trans.end(); + } + }); + }); + + t.test('on different connections', function (t) { + resetAgent(4, function (data) { + t.strictEqual(data.transactions.length, 1); + t.strictEqual(data.spans.length, 3); + + var trans = data.transactions[0]; + + t.strictEqual(trans.name, 'foo'); + + data.spans.forEach(function (span) { + assertSpan(t, span, sql); + t.equal( + span.parent_id, + trans.id, + 'each mariadb span is a child of the transaction', + ); + }); + + t.end(); + }); + + var sql = 'SELECT 1 + ? AS solution'; + + createPool(function () { + var n = 0; + var trans = agent.startTransaction('foo'); + + queryablePromise.getConnection().then(function (conn) { + conn.query(sql, [1]).then(function (rows) { + t.strictEqual(rows[0].solution, 2); + if (++n === 3) done(); + }); + }); + queryablePromise.getConnection().then(function (conn) { + conn.query(sql, [2]).then(function (rows) { + t.strictEqual(rows[0].solution, 3); + if (++n === 3) done(); + }); + }); + queryablePromise.getConnection().then(function (conn) { + conn.query(sql, [3]).then(function (rows) { + t.strictEqual(rows[0].solution, 4); + if (++n === 3) done(); + }); + }); + + function done() { + trans.end(); + } + }); + }); + }); + + t.test('simultaneous transactions', function (t) { + resetAgent(6, function (data) { + t.strictEqual(data.transactions.length, 3); + t.strictEqual(data.spans.length, 3); + var names = data.transactions + .map(function (trans) { + return trans.name; + }) + .sort(); + t.deepEqual(names, ['bar', 'baz', 'foo']); + + data.transactions.forEach(function (trans) { + const span = findObjInArray(data.spans, 'transaction_id', trans.id); + t.ok(span, 'transaction should have span'); + assertSpan(t, span, sql); + }); + + t.end(); + }); + + var sql = 'SELECT 1 + ? AS solution'; + + factory(function () { + setImmediate(function () { + var trans = agent.startTransaction('foo'); + queryablePromise.query(sql, [1]).then(function (rows) { + t.strictEqual(rows[0].solution, 2); + trans.end(); + }); + }); + + setImmediate(function () { + var trans = agent.startTransaction('bar'); + queryablePromise.query(sql, [2]).then(function (rows) { + t.strictEqual(rows[0].solution, 3); + trans.end(); + }); + }); + + setImmediate(function () { + var trans = agent.startTransaction('baz'); + queryablePromise.query(sql, [3]).then(function (rows) { + t.strictEqual(rows[0].solution, 4); + trans.end(); + }); + }); + }); + }); + + // Only pools have a getConnection function + if (type === 'pool') { + t.test('connection.release()', function (t) { + resetAgent(function (data) { + assertBasicQuery(t, sql, data); + t.end(); + }); + + var sql = 'SELECT 1 + 1 AS solution'; + + factory(function () { + agent.startTransaction('foo'); + + queryablePromise.getConnection().then(function (conn) { + conn.release(); + + queryablePromise.getConnection().then(function (conn) { + basicQueryPromise(t, conn.query(sql)); + t.ok( + agent.currentSpan === null, + 'mariadb span should not spill into calling code', + ); + }); + }); + }); + }); + } + }); +}); + +function basicQueryPromise(t, p) { + function done() { + agent.endTransaction(); + } + + p.then( + function (response) { + var rows = response[0]; + t.strictEqual(rows.solution, 2); + done(); + }, + function (error) { + t.error(error); + done(); + }, + ); +} +function batchQueryPromise(t, p) { + function done() { + agent.endTransaction(); + } + + p.then( + function (response) { + var rows = response; + t.strictEqual(rows.affectedRows, 2); + done(); + }, + function (error) { + t.error(error); + done(); + }, + ); +} + +function basicQueryCallback(t) { + return function (err, rows, fields) { + t.ok( + agent.currentSpan === null, + 'mariadb span should not spill into calling code', + ); + t.error(err); + + t.strictEqual(rows[0].solution, 2); + agent.endTransaction(); + }; +} +function batchQueryCallback(t) { + return function (err, rows, fields) { + t.ok( + agent.currentSpan === null, + 'mariadb span should not spill into calling code', + ); + t.error(err); + t.strictEqual(rows.affectedRows, 2); + agent.endTransaction(); + }; +} + +function basicQueryStream(stream, t) { + var results = 0; + stream.on('error', function (err) { + t.ok( + agent.currentSpan === null, + 'mariadb span should not be active in user code', + ); + t.error(err); + }); + stream.on('data', function (row) { + t.ok( + agent.currentSpan === null, + 'mariadb span should not be active in user code', + ); + results++; + t.strictEqual(row.solution, 2); + }); + stream.on('end', function () { + t.ok( + agent.currentSpan === null, + 'mariadb span should not be active in user code', + ); + t.strictEqual(results, 1); + agent.endTransaction(); + }); +} + +function assertBasicQuery(t, sql, data, spanName = 'SELECT') { + t.strictEqual(data.transactions.length, 1); + t.strictEqual(data.spans.length, 1); + + var trans = data.transactions[0]; + var span = data.spans[0]; + + t.strictEqual(trans.name, 'foo'); + assertSpan(t, span, sql, spanName); +} + +function assertSpan(t, span, sql, spanName = 'SELECT') { + t.strictEqual(span.name, spanName, 'span.name'); + t.strictEqual(span.type, 'db', 'span.type'); + t.strictEqual(span.subtype, 'mariadb', 'span.subtype'); + t.strictEqual(span.action, 'query', 'span.action'); + t.deepEqual( + span.context.db, + { + type: 'sql', + instance: connectionOptions.database, + user: connectionOptions.user, + statement: sql, + }, + 'span.context.db', + ); + t.deepEqual( + span.context.service.target, + { + type: 'mariadb', + name: connectionOptions.database, + }, + 'span.context.service.target', + ); + t.deepEqual( + span.context.destination, + { + address: connectionOptions.host, + port: 3306, + service: { + type: '', + name: '', + resource: `mariadb/${connectionOptions.database}`, + }, + }, + 'span.context.destination', + ); +} + +function createConnection(cb) { + setup(function () { + _teardown = function teardown() { + if (queryable) { + queryable.end(); + queryable = undefined; + } + if (queryablePromise) { + queryablePromise.end(); + queryablePromise = undefined; + } + }; + + queryable = mariadb.createConnection(connectionOptions); + + queryable.connect((err) => { + if (err) throw err; + + mariadbPromise + .createConnection(connectionOptions) + .then(async function (conn2) { + queryablePromise = conn2; + + await queryablePromise.execute('DROP TABLE IF EXISTS users'); + + await queryablePromise.execute( + 'CREATE TABLE users (id INTEGER PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255))', + ); + cb(); + }); + }); + }); +} + +function createPool(cb) { + setup(function () { + _teardown = function teardown() { + if (queryable) { + queryable.end(); + queryable = undefined; + } + if (queryablePromise) { + queryablePromise.end(); + queryablePromise = undefined; + } + }; + + queryable = mariadb.createPool(connectionOptions); + queryablePromise = mariadbPromise.createPool(connectionOptions); + + queryablePromise.execute('DROP TABLE IF EXISTS users').then(() => { + queryablePromise + .execute( + 'CREATE TABLE users (id INTEGER PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255))', + ) + .then(() => { + cb(); + }); + }); + }); +} + +function createPoolAndGetConnection(cb) { + setup(function () { + _teardown = function teardown() { + if (pool) { + queryable.end(); + pool.end(); + pool = undefined; + queryable = undefined; + } + if (poolPromise) { + queryablePromise.end(); + poolPromise.end(); + poolPromise = undefined; + queryablePromise = undefined; + } + }; + + var pool = mariadb.createPool(connectionOptions); + var poolPromise = mariadbPromise.createPool(connectionOptions); + + pool.getConnection(function (err, conn) { + if (err) throw err; + queryable = conn; + + poolPromise.getConnection().then(function (conn) { + queryablePromise = conn; + + queryablePromise.execute('DROP TABLE IF EXISTS users').then(() => { + queryablePromise + .execute( + 'CREATE TABLE users (id INTEGER PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255))', + ) + .then(() => { + cb(); + }); + }); + }); + }); + }); +} + +function createPoolClusterAndGetConnection(cb) { + setup(function () { + _teardown = function teardown() { + if (cluster) { + queryable.end(); + cluster.end(); + cluster = undefined; + queryable = undefined; + } + if (clusterPromise) { + queryablePromise.end(); + clusterPromise.end(); + clusterPromise = undefined; + queryablePromise = undefined; + } + }; + + var cluster = mariadb.createPoolCluster(); + var clusterPromise = mariadbPromise.createPoolCluster(); + + cluster.add('master', connectionOptions); + clusterPromise.add('master', connectionOptions); + + cluster.getConnection(function (err, conn) { + if (err) throw err; + queryable = conn; + + clusterPromise.getConnection().then(function (conn2) { + queryablePromise = conn2; + + queryablePromise.execute('DROP TABLE IF EXISTS users').then(() => { + queryablePromise + .execute( + 'CREATE TABLE users (id INTEGER PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255))', + ) + .then(() => { + cb(); + }); + }); + }); + }); + }); +} + +function createPoolClusterAndGetConnectionViaOf(cb) { + setup(function () { + _teardown = function teardown() { + if (clusterPromise) { + queryablePromise.end(); + clusterPromise.end(); + clusterPromise = undefined; + queryablePromise = undefined; + } + }; + + var clusterPromise = mariadbPromise.createPoolCluster(); + clusterPromise.add('master-test', connectionOptions); + clusterPromise + .of('.*', 'RANDOM') + .getConnection() + .then((conn) => { + queryablePromise = conn; + + queryablePromise.execute('DROP TABLE IF EXISTS users').then(() => { + queryablePromise + .execute( + 'CREATE TABLE users (id INTEGER PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255))', + ) + .then(() => { + cb(); + }); + }); + }); + }); +} +function createPoolClusterAndGetConnectionViaOfDirect(cb) { + setup(function () { + _teardown = function teardown() { + if (clusterPromise) { + queryablePromise.end(); + clusterPromise.end(); + clusterPromise = undefined; + queryablePromise = undefined; + } + }; + + var clusterPromise = mariadbPromise.createPoolCluster(); + clusterPromise.add('master-test', connectionOptions); + clusterPromise.getConnection('.*', 'RANDOM').then((conn) => { + queryablePromise = conn; + + queryablePromise.execute('DROP TABLE IF EXISTS users').then(() => { + queryablePromise + .execute( + 'CREATE TABLE users (id INTEGER PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255))', + ) + .then(() => { + cb(); + }); + }); + }); + }); +} + +function setup(cb) { + teardown(); // just in case it didn't happen at the end of the previous test + utils.reset(cb); +} + +// placeholder variable to hold the teardown function created by the setup function +var _teardown = function () { }; +var teardown = function () { + _teardown(); +}; + +function resetAgent(expected, cb) { + if (typeof expected === 'function') return resetAgent(2, expected); + // first time this function is called, the real client will be present - so + // let's just destroy it before creating the mock + + if (agent._apmClient.destroy) agent._apmClient.destroy(); + agent._instrumentation.testReset(); + agent._apmClient = mockClient(expected, cb); +} diff --git a/test/instrumentation/modules/mariadb/pool-release-1.test.js b/test/instrumentation/modules/mariadb/pool-release-1.test.js new file mode 100644 index 0000000000..2edf07ccbd --- /dev/null +++ b/test/instrumentation/modules/mariadb/pool-release-1.test.js @@ -0,0 +1,66 @@ +/* + * 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'; + +if (process.env.GITHUB_ACTIONS === 'true' && process.platform === 'win32') { + console.log('# SKIP: GH Actions do not support docker services on Windows'); + process.exit(0); +} + +const semver = require('semver'); +const { safeGetPackageVersion } = require('../../../_utils'); +const mariadbVer = safeGetPackageVersion('mariadb'); +if (semver.gte(mariadbVer, '3.0.0') && semver.lt(process.version, '14.0.0')) { + console.log( + `# SKIP mariadb@${mariadbVer} does not support node ${process.version}`, + ); + process.exit(); +} + +var agent = require('../../../..').start({ + serviceName: 'test', + secretToken: 'test', + captureExceptions: false, + metricsInterval: 0, + centralConfig: false, +}); + +var mariadb = require('mariadb/callback'); +var test = require('tape'); + +var utils = require('./_utils'); + +test('release connection prior to transaction', function (t) { + createPool(function (pool) { + pool.getConnection(function (err, conn) { + t.error(err); + console.log('Test1'); + conn.release(); // important to release connection before starting the transaction + console.log('Test2'); + + agent.startTransaction('foo'); + t.ok(agent.currentTransaction); + + pool.getConnection(function (err, conn) { + t.error(err); + t.ok(agent.currentTransaction); + pool.end(); + t.end(); + }); + }); + }); +}); + +function createPool(cb) { + setup(function () { + cb(mariadb.createPool(utils.credentials())); + }); +} + +function setup(cb) { + utils.reset(cb); +}