From 528b4a2dd16610c59978f291c0cd70b3742f1eb4 Mon Sep 17 00:00:00 2001 From: he-is-harry Date: Fri, 14 Feb 2025 12:28:56 -0500 Subject: [PATCH 1/4] Added Dynatrace support (WORK IN PROGRESS) - Added extension/Dynatrace.js to wrap exec / execute / prepare functions with dynatrace tracing - Modified the Client to automatically wrap with dynatrace tracing when isDynatraceEnabled() is true (@dynatrace/oneagent-sdk is installed and HDB_NODEJS_SKIP_DYNATRACE is off) and the dynatrace connect option is true - Added the dynatrace and dynatraceTenant connect options - Modified ResultSet.js to have the getRowCount function - Added 2 unit tests for getRowCount - Added the isDynatraceSupported field to the driver (Hana class) - Added the MockDynatraceSDK for testing and added integration tests for dynatrace Testing Notes The Makefile was modified so that `make test-dynatrace` will test only the dynatrace integration tests To install the MockDynatraceSDK, copy the contents of the MockDynatraceSDK folder into node_modules/@dynatrace/oneagent-sdk The integration tests were designed to be able to dynamically run on the mock dynatrace and the real dynatrace depending on the node_modules import --- Makefile | 4 + extension/Dynatrace.js | 176 +++++++++ index.js | 1 + lib/Client.js | 27 +- lib/index.js | 5 +- lib/protocol/ResultSet.js | 21 ++ lib/util/index.js | 33 ++ test/MockDynatraceSDK/index.js | 132 +++++++ test/MockDynatraceSDK/package.json | 6 + test/acceptance/db.Dynatrace.js | 548 +++++++++++++++++++++++++++++ test/lib.ResultSet.js | 100 ++++++ 11 files changed, 1051 insertions(+), 2 deletions(-) create mode 100644 extension/Dynatrace.js create mode 100644 test/MockDynatraceSDK/index.js create mode 100644 test/MockDynatraceSDK/package.json create mode 100644 test/acceptance/db.Dynatrace.js diff --git a/Makefile b/Makefile index b00c620..9ed2671 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,10 @@ test-acceptance: @NODE_ENV=test ./node_modules/.bin/mocha \ -R $(REPORTER) -b test/acceptance/*.js +test-dynatrace: + @NODE_ENV=test ./node_modules/.bin/mocha \ + -R $(REPORTER) -b test/acceptance/db.Dynatrace.js + test-mock: @HDB_MOCK=1 $(MAKE) -s test diff --git a/extension/Dynatrace.js b/extension/Dynatrace.js new file mode 100644 index 0000000..ce7fb0f --- /dev/null +++ b/extension/Dynatrace.js @@ -0,0 +1,176 @@ +// Copyright 2013 SAP AG. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http: //www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +// either express or implied. See the License for the specific +// language governing permissions and limitations under the License. +'use strict'; + +var ResultSet = require('../lib/protocol/ResultSet'); +const dynatrace = {}; +try { + // @dynatrace/oneagent-sdk must be installed by the application in order for + // the client to use it. + dynatrace.sdk = require('@dynatrace/oneagent-sdk'); + dynatrace.api = dynatrace.sdk.createInstance(); +} catch (err) { + // If module was not found, do not do anything +} + +function isDynatraceEnabled() { + if(dynatrace.api === undefined) { + return false; + } + const envVar = process.env.HDB_NODEJS_SKIP_DYNATRACE; + if(envVar && envVar != '0' && envVar.toLowerCase() != 'false') { + return false; + } + return true; +} + +function _dynatraceResultCallback(tracer, cb) { + return function (err, ...args) { + var results = args[0]; + + // With DB calls, the first argument can potentially be output parameters + // In that case, we consider the next parameter + if (typeof results === 'object' && results !== null && !Array.isArray(results)) { + results = args[1]; + } + + if (err) { + tracer.error(err); + } else if(results !== undefined) { + tracer.setResultData({ + rowsReturned: (results && results.length) || results + }); + } + tracer.end(cb, err, ...args); + }; +} + +function _dynatraceResultSetCallback(tracer, cb) { + return function (err, ...args) { + var resultSet = args[0]; + + // With DB calls, the first argument can potentially be output parameters + // In that case, we consider the next parameter + if (typeof resultSet === 'object' && resultSet !== null && !(resultSet instanceof ResultSet)) { + resultSet = args[1]; + } + + if (err) { + tracer.error(err); + } else if(resultSet) { + const rowCount = resultSet.getRowCount(); + // A negative rowCount means the number of rows is unknown. + // This happens if the client hasn't received the last fetch chunk yet (with default server configuration, + // this happens if the result set is larger than 32 rows) + if(rowCount >= 0) { + tracer.setResultData({rowsReturned: rowCount}); + } + } + tracer.end(cb, err, ...args); + }; +} + +function _ExecuteWrapperFn(stmtOrConn, conn, execFn, resultCB, sql) { + // connection exec args = [sql, options, callback] --> options is optional + // stmt exec args = [values, options, callback] --> options is optional + return function (...args) { + if(stmtOrConn === conn && args.length > 0) { + sql = args[0]; + } + if(typeof(sql) !== 'string') { + sql = ''; // execute will fail, but need sql for when the error is traced + } + // get dbInfo from the conn in case it changes since the first time dynatraceConnection was called + const tracer = dynatrace.api.traceSQLDatabaseRequest(conn._dbInfo, {statement: sql}); + var cb; + if (args.length > 0 && typeof args[args.length - 1] === 'function') { + cb = args[args.length - 1]; + } + // async execute + // cb can potentially be undefined but the execute will still go through, so we log but throw an error + // when cb tries to be run + tracer.startWithContext(execFn, stmtOrConn, ...args.slice(0, args.length - 1), resultCB(tracer, cb)); + } +} + +// modify stmt for Dynatrace after a successful prepare +function _DynatraceStmt(stmt, conn, sql) { + const originalExecFn = stmt.exec; + stmt.exec = _ExecuteWrapperFn(stmt, conn, originalExecFn, _dynatraceResultCallback, sql); + const originalExecuteFn = stmt.execute; + stmt.execute = _ExecuteWrapperFn(stmt, conn, originalExecuteFn, _dynatraceResultSetCallback, sql); +} + +function _prepareStmtUsingDynatrace(conn, prepareFn) { + // args = [sql, options, callback] --> options is optional + return function (...args) { + const cb = args[args.length - 1]; + var sql = args[0]; + if(typeof(sql) !== 'string') { + sql = ''; // prepare will fail, but need sql for when the error is traced + } + + // same as before, cb can be undefined / not a function but we still log, but throw an error after + prepareFn.call(conn, ...args.slice(0, args.length - 1), dynatrace.api.passContext(function prepare_handler(err, stmt) { + if (err) { + // The prepare failed, so trace the SQL and the error + // We didn't start the tracer yet, so the trace start time will be inaccurate. + const tracer = dynatrace.api.traceSQLDatabaseRequest(conn._dbInfo, {statement: sql}); + tracer.start(function prepare_error_handler() { + tracer.error(err); + tracer.end(cb, err); + }); + } else { + _DynatraceStmt(stmt, conn, sql); + cb(err, stmt); + } + })); + } +} + +function _createDbInfo(destinationInfo) { + const dbInfo = { + name: `SAPHANA${destinationInfo.tenant ? `-${destinationInfo.tenant}` : ''}`, + vendor: dynatrace.sdk.DatabaseVendor.HANADB, + host: destinationInfo.host, + port: Number(destinationInfo.port) + }; + return dbInfo; +} + +function dynatraceConnection(conn, destinationInfo) { + if(dynatrace.api === undefined) { + return conn; + } + const dbInfo = _createDbInfo(destinationInfo); + if(conn._dbInfo) { + // dynatraceConnection has already been called on conn, use new destinationInfo + // in case it changed, but don't wrap conn again + conn._dbInfo = dbInfo; + return conn; + } + conn._dbInfo = dbInfo; + // hana-client does not like decorating. + // because of that, we need to override the fn and pass the original fn for execution + const originalExecFn = conn.exec; + conn.exec = _ExecuteWrapperFn(conn, conn, originalExecFn, _dynatraceResultCallback); + const originalExecuteFn = conn.execute; + conn.execute = _ExecuteWrapperFn(conn, conn, originalExecuteFn, _dynatraceResultSetCallback); + const originalPrepareFn = conn.prepare; + conn.prepare = _prepareStmtUsingDynatrace(conn, originalPrepareFn); + + return conn; +} + +module.exports = { dynatraceConnection, isDynatraceEnabled }; diff --git a/index.js b/index.js index 5061d29..65ae754 100644 --- a/index.js +++ b/index.js @@ -19,3 +19,4 @@ exports.createClient = lib.createClient; exports.Stringifier = lib.Stringifier; exports.createJSONStringifier = lib.createJSONStringifier; exports.iconv = require('iconv-lite'); +exports.isDynatraceSupported = lib.isDynatraceSupported; diff --git a/lib/Client.js b/lib/Client.js index 3f367cc..ad4d374 100644 --- a/lib/Client.js +++ b/lib/Client.js @@ -20,6 +20,7 @@ var Connection = protocol.Connection; var Result = protocol.Result; var Statement = protocol.Statement; var ConnectionManager = protocol.ConnectionManager; +var hanaDynatrace = require('../extension/Dynatrace'); module.exports = Client; @@ -31,9 +32,11 @@ function Client(options) { this._settings = util.extend({ fetchSize: 1024, holdCursorsOverCommit: true, - scrollableCursor: true + scrollableCursor: true, + dynatrace: true }, options); this._settings.useCesu8 = (this._settings.useCesu8 !== false); + normalizeSettings(this._settings); this._connection = this._createConnection(this._settings); } @@ -120,6 +123,9 @@ Client.prototype.connect = function connect(options, cb) { options = {}; } var connectOptions = util.extend({}, this._settings, options); + normalizeSettings(connectOptions); + addDynatraceWrapper(this, {host: this._settings.host, port: this._settings.port, + tenant: this._settings.dynatraceTenant}, connectOptions.dynatrace); var connManager = new ConnectionManager(connectOptions); // SAML assertion can only be used once @@ -302,6 +308,19 @@ Client.prototype._addListeners = function _addListeners(connection) { connection.on('close', onclose); }; +function normalizeSettings(settings) { + for (var key in settings) { + if (key.toUpperCase() === 'SPATIALTYPES') { + settings['spatialTypes'] = util.getBooleanProperty(settings[key]) ? 1 : 0; + } else if (key.toUpperCase() === 'DYNATRACE') { + var {value, isValid} = util.validateAndGetBoolProperty(settings[key]); + settings['dynatrace'] = isValid ? value : true; + } else if (key.toUpperCase() === 'DYNATRACETENANT') { + settings['dynatraceTenant'] = settings[key]; + } + } +} + function normalizeArguments(args, defaults) { var command = args[0]; var options = args[1]; @@ -326,3 +345,9 @@ function normalizeArguments(args, defaults) { } return [command, options, cb]; } + +function addDynatraceWrapper(client, destinationInfo, dynatraceOn) { + if (hanaDynatrace && hanaDynatrace.isDynatraceEnabled() && dynatraceOn) { + hanaDynatrace.dynatraceConnection(client, destinationInfo); + } +} diff --git a/lib/index.js b/lib/index.js index 5a712ca..9144945 100644 --- a/lib/index.js +++ b/lib/index.js @@ -34,4 +34,7 @@ exports.createJSONStringifier = function createJSONStringifier() { seperator: ',', stringify: JSON.stringify }); -}; \ No newline at end of file +}; + +// Dynatrace support should not change unless there are source code modifications +exports.isDynatraceSupported = true; \ No newline at end of file diff --git a/lib/protocol/ResultSet.js b/lib/protocol/ResultSet.js index 026f49b..292df79 100644 --- a/lib/protocol/ResultSet.js +++ b/lib/protocol/ResultSet.js @@ -49,6 +49,10 @@ function ResultSet(connection, rsd, options) { readSize: Lob.DEFAULT_READ_SIZE, columnNameProperty: 'columnDisplayName' }, options); + + // Rows in result set from packets so far + this._curRows = rsd.data ? rsd.data.argumentCount : 0; + this._resultSetRowsKnown = rsd.data ? isLast(rsd.data) : false; } ResultSet.create = function createResultSet(connection, rsd, options) { @@ -168,6 +172,13 @@ ResultSet.prototype.getLobColumnIndexes = function getLobColumnIndexes() { return indexes; }; +ResultSet.prototype.getRowCount = function getRowCount() { + if (this._resultSetRowsKnown) { + return this._curRows; + } + return -1; +} + ResultSet.prototype.fetch = function fetch(cb) { var stream = this.createArrayStream(); var collector = new util.stream.Writable({ @@ -340,9 +351,12 @@ function handleData(data) { this.emit('data', data.buffer); } + addResultSetRows.call(this, data); + if (isLast(data)) { this.finished = true; this._running = false; + this._resultSetRowsKnown = true; this.closed = isClosed(data); } @@ -361,6 +375,13 @@ function handleData(data) { } } +function addResultSetRows(data) { + // Stored data is already included in the number of rows + if (this._data !== data) { + this._curRows += data.argumentCount; + } +} + function emitEnd() { /* jshint validthis:true */ debug('emit "end"'); diff --git a/lib/util/index.js b/lib/util/index.js index 870d301..cdbb323 100644 --- a/lib/util/index.js +++ b/lib/util/index.js @@ -275,8 +275,41 @@ function getBooleanProperty(arg) { return false; } else if (arg === 1) { return true; + } else if (arg === true) { + return true; } else { return false; } } exports.getBooleanProperty = getBooleanProperty; + +function validateAndGetBoolProperty(arg) { + var value = false; + var isValid = false; + if (isString(arg)) { + var upper = arg.toUpperCase(); + if (upper === 'TRUE' || upper === 'YES' || upper === 'ON' || upper === '1') { + value = true; + isValid = true; + } else if (upper === 'FALSE' || upper === 'NO' || upper === 'OFF' || upper === '0') { + value = false; + isValid = true; + } + } else if (isNumber(arg)) { + if (arg === 1) { + value = true; + isValid = true; + } else if (arg === 0) { + value = false; + isValid = true; + } + } else if (arg === true) { + value = true; + isValid = true; + } else if (arg === false) { + value = false; + isValid = true; + } + return { value: value, isValid: isValid }; +} +exports.validateAndGetBoolProperty = validateAndGetBoolProperty; diff --git a/test/MockDynatraceSDK/index.js b/test/MockDynatraceSDK/index.js new file mode 100644 index 0000000..3502211 --- /dev/null +++ b/test/MockDynatraceSDK/index.js @@ -0,0 +1,132 @@ +// Copyright 2013 SAP AG. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http: //www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +// either express or implied. See the License for the specific +// language governing permissions and limitations under the License. +'use strict'; + +// This module is a mock of the @dynatrace/oneagent-sdk module for +// HANA client testing purposes + +// To "install" this mock, copy this index.js and the package.json in the +// same directory to the node_modules/@dynatrace/oneagent-sdk directory. + +// traceData is an object with keys that are numbers (traceNum) and +// values that are objects with keys: +// {dbInfo, sql, startTime, endTime, error, rowsReturned} +var traceData = {}; +var traceEnabled = false; +var lastTraceNum = 0; + +// matching drop-in replacement for a subset of the @dynatrace/oneagent-sdk interface +class DBTracer { + constructor(api, dbinfo, sql, traceNum) { + this.traceNum = traceNum; + if(traceNum) { + traceData[traceNum] = {dbInfo: dbinfo, sql: sql}; + } + } + + // trace start and call cb(...params) + start(cb, ...params) { + if(this.traceNum) { + if(traceData[this.traceNum].startTime) { + console.log("Error: DBTracer.start or startWithContext called more than once"); + } + traceData[this.traceNum].startTime = new Date(); + } + cb(...params); + } + + // trace start and call obj.fn(...params) + startWithContext(fn, obj, ...params) { + if(this.traceNum) { + if(traceData[this.traceNum].startTime) { + console.log("Error: DBTracer.startWithContext or start called more than once"); + } + traceData[this.traceNum].startTime = new Date(); + } + fn.apply(obj, params); + } + + // trace result set data (only interested in prop.rowsReturned) + setResultData(prop) { + if(this.traceNum) { + traceData[this.traceNum].rowsReturned = prop.rowsReturned; + } + } + + // trace error + error(err) { + if(this.traceNum) { + if(traceData[this.traceNum].error) { + console.log("Error: DBTracer.error called more than once"); + } + traceData[this.traceNum].error = err; + } + } + + // end of trace object, so trace end and call cb(...params) if cb is passed in + end(cb, ...params) { + if(this.traceNum) { + if(traceData[this.traceNum].endTime) { + console.log("Error: DBTracer.end called more than once"); + } + traceData[this.traceNum].endTime = new Date(); + } + if(cb) { + cb(...params); + } + } + + // data members: traceNum (undefined if not tracing) +} + +class API { + constructor() { + //console.log('in API constructor'); + } + + traceSQLDatabaseRequest(dbinfo, prop) { + var traceNum; // undefined if trace is not enabled + if(traceEnabled) { + traceNum = ++lastTraceNum; + } + return new DBTracer(this, dbinfo, prop.statement, traceNum); + } + + passContext(fn) { + return fn; + } +} + +exports.createInstance = function() { + return new API(); +} + +exports.DatabaseVendor = {HANADB: 'HANADB'}; + +// functions so tests can get and clear the mocked trace data +exports.enableTrace = function() { + traceEnabled = true; +} +exports.disableTrace = function() { + traceEnabled = false; +} +exports.getTraceData = function() { + return traceData; +} +exports.getLastTraceNum = function() { + return lastTraceNum; +} +exports.clearTraceData = function() { + traceData = {}; +} diff --git a/test/MockDynatraceSDK/package.json b/test/MockDynatraceSDK/package.json new file mode 100644 index 0000000..5e8b196 --- /dev/null +++ b/test/MockDynatraceSDK/package.json @@ -0,0 +1,6 @@ +{ + "author": "SAP SE", + "name": "@dynatrace/oneagent-sdk", + "description": "mock of @dynatrace/oneagent-sdk for HANA Client testing", + "main": "./index.js" +} diff --git a/test/acceptance/db.Dynatrace.js b/test/acceptance/db.Dynatrace.js new file mode 100644 index 0000000..11db1c6 --- /dev/null +++ b/test/acceptance/db.Dynatrace.js @@ -0,0 +1,548 @@ +// Copyright 2013 SAP AG. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http: //www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +// either express or implied. See the License for the specific +// language governing permissions and limitations under the License. +'use strict'; +/* jshint undef:false, expr:true */ + +var async = require('async'); +var db = require('../db')(); +var RemoteDB = require('../db/RemoteDB'); +var util = require('../../lib/util'); +var hanaDynatrace = require('../../extension/Dynatrace'); +var dynatraceSDK; // either the real @dynatrace/oneagent-sdk Dynatrace SDK or the mock one +var mockDynatraceSDK; // our mock @dynatrace/oneagent-sdk for testing +var http, server, request; +try { + dynatraceSDK = require('@dynatrace/oneagent-sdk'); + if (dynatraceSDK.getTraceData !== undefined) { + // Using mock @dynatrace/oneagent-sdk + mockDynatraceSDK = dynatraceSDK; + } else { + // Using real @dynatrace/oneagent-sdk, so setup web request + http = require('http'); + } +} catch (err) { + // No @dynatrace/oneagent-sdk, skip this test, see MockDynatraceSDK to "install" the mock + // to run these tests +} + +var describeDynatrace = db instanceof RemoteDB && dynatraceSDK !== undefined ? describe : describe.skip; + +function isMockDynatraceEnabled() { + return mockDynatraceSDK && hanaDynatrace.isDynatraceEnabled(); +} + +describeDynatrace('db', function () { + before(function (done) { + if (isMockDynatraceEnabled()) { + mockDynatraceSDK.enableTrace(); + } + if (mockDynatraceSDK) { + db.init.bind(db)(done); + } else { + // Real dynatrace, create an inbound web request + server = http.createServer(function onRequest(req, res) { + request = res; + db.init.bind(db)(done); + }).listen(8001).on("listening", () => http.get("http://localhost:" + server.address().port));; + } + + }); + after(function (done) { + if (isMockDynatraceEnabled()) { + mockDynatraceSDK.disableTrace(); + } + if (mockDynatraceSDK) { + db.end.bind(db)(done); + } else { + // Real dynatrace, stop the web request + request.end(); + server.close(); + db.end.bind(db)(done); + } + + }) + var client = db.client; + + describeDynatrace('Dynatrace', function () { + it('should trace a prepared statement exec', function (done) { + var sql = 'SELECT 1 FROM DUMMY'; + var destInfo = getDestInfoForDynatrace(); + client.prepare(sql, function (err, stmt) { + if (err) done(err); + stmt.exec([], function (err, rows) { + if (err) done(err); + rows.should.have.length(1); + rows[0]['1'].should.equal(1); + verifyDynatraceData(destInfo, sql, 1); + cleanup(stmt, done); + }); + }); + }); + + it('should trace a client exec', function (done) { + var destInfo = getDestInfoForDynatrace(); + var sql = 'SELECT TOP 10 * FROM OBJECTS'; + client.exec(sql, function (err, rows) { + if (err) done(err); + rows.should.have.length(10); + verifyDynatraceData(destInfo, sql, 10); + done(); + }); + }); + + it('should trace exec / prepare errors', function (done) { + var destInfo = getDestInfoForDynatrace(); + function testExecSqlSyntaxError(input) { + return function execError(cb) { + client.exec(input, function (err) { + err.should.be.an.instanceOf(Error); + // When the sql input is not a string, we log an empty string in dynatrace + verifyDynatraceData(destInfo, typeof input === 'string' ? input : '', undefined, { code: 257 }); + cb(); + }) + } + } + function testPrepareSqlSyntaxError(input) { + return function prepareError(cb) { + client.prepare(input, function (err, statement) { + (!!statement).should.not.be.ok; + err.should.be.an.instanceOf(Error); + verifyDynatraceData(destInfo, typeof input === 'string' ? input : '', undefined, { code: 257 }); + cb(); + }) + } + } + function castError(cb) { + var sql = 'SELECT CAST(? AS INT) FROM DUMMY'; + client.prepare(sql, function (err, statement) { + if (err) cb(err); + statement.exec(['string to int cast'], function (err) { + err.should.be.an.instanceOf(Error); + verifyDynatraceData(destInfo, sql, undefined, { + message: 'Cannot set parameter at row: 1. Wrong input for INT type' + }); + cleanup(statement, cb); + }) + }); + } + + async.series([testExecSqlSyntaxError('SELECT 2 SYNTAX ERROR'), testExecSqlSyntaxError([2]), + testExecSqlSyntaxError('SELECT * FROM /* SYNTAX ERROR */'), + testPrepareSqlSyntaxError('SELECT 3 SYNTAX ERROR'), testPrepareSqlSyntaxError([3]), + testPrepareSqlSyntaxError('SELECT /* SYNTAX ERROR */ FROM DUMMY'), castError], done); + }); + + it('should trace a statement exec unbound parameters error', function (done) { + var destInfo = getDestInfoForDynatrace(); + var sql = 'SELECT ? FROM DUMMY'; + client.prepare(sql, function (err, stmt) { + if (err) done(err); + stmt.exec([], function (err, rows) { + err.should.be.an.instanceOf(Error); + verifyDynatraceData(destInfo, sql, undefined, { message: "Unbound parameters found." }); + cleanup(stmt, done); + }); + }); + }); + + it('should time an exec', function (done) { + var destInfo = getDestInfoForDynatrace(); + var sql = 'SELECT 2 FROM DUMMY'; + client.prepare(sql, function (err, stmt) { + if (err) done(err); + var beforeExecTime = new Date(); + stmt.exec([], function (err, rows) { + var afterExecTime = new Date(); + if (err) done(err); + rows.should.have.length(1); + rows[0]['2'].should.equal(2); + var elapsedExecTime = afterExecTime - beforeExecTime; + verifyDynatraceRequestTime(Math.max(0, elapsedExecTime - 1000), elapsedExecTime); + verifyDynatraceData(destInfo, sql, 1); + cleanup(stmt, done); + }) + }); + }); + + it('should time a 2 second procedure', function (done) { + this.timeout(3000); + var destInfo = getDestInfoForDynatrace(); + var sql = 'DO BEGIN CALL SQLSCRIPT_SYNC:SLEEP_SECONDS(2); END'; + client.prepare(sql, function (err, stmt) { + if (err) done(err); + var beforeExecTime = new Date(); + stmt.exec([], function (err, params) { + var afterExecTime = new Date(); + if (err) done(err); + var elapsedExecTime = afterExecTime - beforeExecTime; + elapsedExecTime.should.be.aboveOrEqual(1900); + verifyDynatraceRequestTime(Math.max(1900, elapsedExecTime - 1000), elapsedExecTime); + // This db call does not return any rows, so the behaviour is to not log any rowsReturned + verifyDynatraceData(destInfo, sql, undefined); + cleanup(stmt, done); + }); + }); + }); + + it('should trace multiple exec with a statement', function (done) { + var destInfo = getDestInfoForDynatrace(); + var sql = 'SELECT ? FROM DUMMY'; + var statement; + function prepare (cb) { + client.prepare(sql, function (err, ps) { + if (err) done(err); + statement = ps; + cb(err); + }); + } + function testExecStatement(input) { + return function execStatement(cb) { + var beforeExecTime = new Date(); + statement.exec(input, function (err, rows) { + var afterExecTime = new Date(); + if (err) done(err); + var elapsedExecTime = afterExecTime - beforeExecTime; + verifyDynatraceRequestTime(Math.max(0, elapsedExecTime - 1000), elapsedExecTime); + verifyDynatraceData(destInfo, sql, 1); + rows.should.have.length(1); + rows[0][':1'].should.equal(input[0]); + cb(); + }); + }; + } + function dropStatement(cb) { + cleanup(statement, cb); + } + + async.waterfall([prepare, testExecStatement(['1']), testExecStatement(['2']), dropStatement], done); + }); + + it('should trace a client execute', function (done) { + var destInfo = getDestInfoForDynatrace(); + var sql = 'SELECT TOP 10 * FROM OBJECTS'; + client.execute(sql, function (err, rs) { + if (err) done(err); + verifyDynatraceData(destInfo, sql, 10); + rs.fetch(function (err, rows) { + if (err) done(err); + rows.should.have.length(10); + if (!rs.closed) { + rs.close(); + } + done(); + }); + }); + }); + + function testDynatraceExecuteNRows(numRows, cb) { + var destInfo = getDestInfoForDynatrace(); + var sql = 'SELECT TOP ? * FROM OBJECTS'; + client.prepare(sql, function (err, stmt) { + if (err) cb(err); + stmt.execute([numRows], function (err, rs) { + if (err) cb(err); + // FYI, we define the Dynatrace end as when the execQuery callback is called + // If there are more than 32 rows, we don't know the number of rows returned + // because we only know the actual number of rows when we've received the last + // fetch chunk. + const expectedNumRows = (numRows > 32) ? undefined : numRows; + verifyDynatraceData(destInfo, sql, expectedNumRows); + rs.fetch(function (err, rows) { + if (err) cb(err); + rows.should.have.length(numRows); + if (!rs.closed) { + rs.close(); + } + cleanup(stmt, cb); + }); + }) + }); + } + + it('should trace a execute with a result set with 1 row', function (done) { + testDynatraceExecuteNRows(1, done); + }); + it('should trace a execute with a result set with 32 rows', function (done) { + testDynatraceExecuteNRows(32, done); + }); + it('should trace a execute with a result set with 33 rows', function (done) { + testDynatraceExecuteNRows(33, done); + }); + it('should trace a execute with a result set with 0 rows', function (done) { + var destInfo = getDestInfoForDynatrace(); + var sql = 'SELECT 3 FROM DUMMY WHERE 1 = 0'; + client.prepare(sql, function (err, stmt) { + if (err) done(err); + stmt.execute([], function (err, rs) { + if (err) done(err); + verifyDynatraceData(destInfo, sql, 0); + rs.fetch(function (err, rows) { + if (err) done(err); + rows.should.have.length(0); + if (!rs.closed) { + rs.close(); + } + cleanup(stmt, done); + }); + }) + }); + }); + + it('should trace multiple execute with a statement', function (done) { + var destInfo = getDestInfoForDynatrace(); + var sql = 'SELECT 1 FROM DUMMY WHERE 1 = ?'; + var statement; + function clearTraceData(cb) { + if (isMockDynatraceEnabled()) { + mockDynatraceSDK.clearTraceData(); + mockDynatraceSDK.getTraceData().should.be.empty; + } + cb(); + } + function prepare (cb) { + client.prepare(sql, function (err, ps) { + if (err) done(err); + statement = ps; + cb(err); + }); + } + function testExecuteStatement(input, expectedRows) { + return function executeStatement(cb) { + var beforeExecTime = new Date(); + statement.execute(input, function (err, rs) { + var afterExecTime = new Date(); + if (err) done(err); + var elapsedExecTime = afterExecTime - beforeExecTime; + if (isMockDynatraceEnabled()) { + Object.keys(mockDynatraceSDK.getTraceData()).should.have.length(1); + } + verifyDynatraceRequestTime(Math.max(0, elapsedExecTime - 1000), elapsedExecTime); + verifyDynatraceData(destInfo, sql, expectedRows.length); + rs.fetch(function (err, rows) { + rows.should.eql(expectedRows); + if (!rs.closed) { + rs.close(); + } + cb(); + }) + }); + }; + } + function dropStatement(cb) { + cleanup(statement, cb); + } + + async.waterfall([clearTraceData, prepare, testExecuteStatement(['1'], [{ '1': 1 }]), + testExecuteStatement(['2'], []), dropStatement], done); + }); + + it('should disable dynatrace with environment variable', function (done) { + var skipDynatrace = process.env.HDB_NODEJS_SKIP_DYNATRACE; + process.env.HDB_NODEJS_SKIP_DYNATRACE = true; + hanaDynatrace.isDynatraceEnabled().should.equal(false); + if (skipDynatrace) { + process.env.HDB_NODEJS_SKIP_DYNATRACE = skipDynatrace; + } else { + delete process.env.HDB_NODEJS_SKIP_DYNATRACE; + } + done(); + }); + + it('should disable dynatrace with dynatrace connect option (only tested on mock)', function (done) { + if (!isMockDynatraceEnabled()) { + // The disabling of dynatrace using the dynatrace connect option can only be validated + // when dynatrace is enabled (no skip env and oneagent-sdk exists) and we are using the mock + this.skip(); + } else { + var destInfo = getDestInfoForDynatrace(); + var sql = 'SELECT 1 FROM DUMMY'; + mockDynatraceSDK.clearTraceData(); + var nonDynatraceDB = require('../db')({dynatrace: false}); + nonDynatraceDB.init(function (err) { + if (err) done(err); + var nonDynatraceClient = nonDynatraceDB.client; + nonDynatraceClient.exec(sql, function (err, rows) { + if (err) done(err); + rows.should.have.length(1); + Object.keys(mockDynatraceSDK.getTraceData()).length.should.equal(0); + + // Manually re-enable the Dynatrace extension + hanaDynatrace.dynatraceConnection(nonDynatraceClient, destInfo); + nonDynatraceClient.exec(sql, function (err, rows) { + if (err) done(err); + Object.keys(mockDynatraceSDK.getTraceData()).length.should.equal(1); + verifyDynatraceData(destInfo, sql, 1); + nonDynatraceDB.end(done); + }); + }); + }); + } + }); + + it('should configure a dynatraceTenant option', function (done) { + var tenantName = 'DynatraceTenant'; + var destInfo = getDestInfoForDynatrace(tenantName); + var sql = 'SELECT 1 FROM DUMMY'; + if (isMockDynatraceEnabled()) { + mockDynatraceSDK.clearTraceData(); + } + var tenantDB = require('../db')({dynatrace: true, dynatraceTenant: tenantName}); + tenantDB.init(function (err) { + if (err) done(err); + var tenantClient = tenantDB.client; + tenantClient.exec(sql, function (err, rows) { + if (err) done(err); + verifyDynatraceData(destInfo, sql, 1); + tenantDB.end(done); + }); + }); + }); + + describeDynatrace('using table', function () { + beforeEach(db.createTable.bind(db, 'TEST_DYNATRACE', ['ID INT UNIQUE NOT NULL'], null)); + afterEach(db.dropTable.bind(db, 'TEST_DYNATRACE')); + + it('should trace a client insert', function (done) { + var destInfo = getDestInfoForDynatrace(); + var sql = 'INSERT INTO TEST_DYNATRACE VALUES(1)'; + client.exec(sql, function (err, rowsAffected) { + if (err) done(err); + rowsAffected.should.equal(1); + // Trace rows affected as rows returned + verifyDynatraceData(destInfo, sql, 1); + client.exec('SELECT COUNT(*) FROM TEST_DYNATRACE', {rowsAsArray: true}, function (err, rows) { + if (err) done(err); + rows[0][0].should.equal(1); + done(); + }); + }); + }); + + it('should trace a prepared statement delete', function (done) { + var destInfo = getDestInfoForDynatrace(); + var sql = 'DELETE FROM TEST_DYNATRACE'; + client.exec('INSERT INTO TEST_DYNATRACE VALUES(1)', function (err, rowsAffected) { + if (err) done(err); + client.exec('INSERT INTO TEST_DYNATRACE VALUES(2)', function (err, rowsAffected) { + if (err) done(err); + client.prepare(sql, function (err, stmt) { + if (err) done(err); + stmt.exec([], function (err, rowsAffected) { + rowsAffected.should.equal(2); + verifyDynatraceData(destInfo, sql, 2); + client.exec('SELECT COUNT(*) FROM TEST_DYNATRACE', {rowsAsArray: true}, function (err, rows) { + if (err) done(err); + rows[0][0].should.equal(0); + cleanup(stmt, done); + }); + }); + }); + }); + }); + }); + + it('should trace a statement batch exec', function (done) { + var destInfo = getDestInfoForDynatrace(); + var sql = 'INSERT INTO TEST_DYNATRACE VALUES(?)'; + client.prepare(sql, function (err, stmt) { + if (err) done(err); + stmt.exec([[1], [2], [3], [4]], function (err, rowsAffected) { + if (err) done(err); + rowsAffected.should.eql([1, 1, 1, 1]); + verifyDynatraceData(destInfo, sql, 4); + client.exec('SELECT COUNT(*) FROM TEST_DYNATRACE', {rowsAsArray: true}, function (err, rows) { + if (err) done(err); + rows[0][0].should.equal(4); + cleanup(stmt, done); + }); + }); + }); + }); + + it('should trace a statement batch exec error', function (done) { + var destInfo = getDestInfoForDynatrace(); + var sql = 'INSERT INTO TEST_DYNATRACE VALUES(?)'; + client.prepare(sql, function (err, stmt) { + if (err) done(err); + stmt.exec([['string going to int column'], ['2']], function (err, rowsAffected) { + err.should.be.an.instanceOf(Error); + (!!rowsAffected).should.not.be.ok; + verifyDynatraceData(destInfo, sql, undefined, { + message: 'Cannot set parameter at row: 1. Wrong input for INT type' + }); + cleanup(stmt, done); + }); + }); + }); + }); + }); + + function getDestInfoForDynatrace(tenant) { + return { host: client.get('host'), port: client.get('port'), tenant: tenant }; + } +}); + +function verifyDynatraceData(destInfo, sql, expectedRowsReturned, expectedError) { + if(isMockDynatraceEnabled()) { // Only validate the data on the mock dynatrace sdk + var got = mockDynatraceSDK.getTraceData()[mockDynatraceSDK.getLastTraceNum()]; + got.should.not.be.undefined; + if(got) { + if (expectedError) { + (!!got.error).should.be.ok; + if (expectedError.code) { + got.error.code.should.equal(expectedError.code); + } + if (expectedError.message) { + got.error.message.should.equal(expectedError.message); + } + } else { + (!!got.error).should.be.not.ok; + } + got.dbInfo.name.should.eql(destInfo.tenant ? 'SAPHANA-' + destInfo.tenant : 'SAPHANA'); + got.dbInfo.vendor.should.eql('HANADB'); + got.dbInfo.host.should.eql(destInfo.host); + got.dbInfo.port.should.eql(Number(destInfo.port)); + got.sql.should.eql(sql); + got.startTime.should.not.be.undefined; + if (expectedRowsReturned !== undefined) { + got.rowsReturned.should.equal(expectedRowsReturned); + } else { + (got.rowsReturned === undefined).should.be.ok; + } + got.endTime.should.not.be.undefined; + got.endTime.should.be.aboveOrEqual(got.startTime); + } + mockDynatraceSDK.clearTraceData(); + } +} + +// must be called before verifyDynatraceData since that clears the trace data +function verifyDynatraceRequestTime(minAllowedMS, maxAllowedMS) { + if(isMockDynatraceEnabled()) { + var got = mockDynatraceSDK.getTraceData()[mockDynatraceSDK.getLastTraceNum()]; + got.should.not.be.undefined; + if(got) { + var gotElapsedTime = got.endTime - got.startTime; + gotElapsedTime.should.be.aboveOrEqual(minAllowedMS); + gotElapsedTime.should.be.belowOrEqual(maxAllowedMS); + } + } +} + +function cleanup(stmt, cb) { + stmt.drop(function (err) { + // ignore error + cb(); + }) +} \ No newline at end of file diff --git a/test/lib.ResultSet.js b/test/lib.ResultSet.js index e81f512..4314343 100644 --- a/test/lib.ResultSet.js +++ b/test/lib.ResultSet.js @@ -254,6 +254,63 @@ function createResultSetWithoutLob(options) { return createResultSet(rsd, chunks, options); } +function createOneChunkResultSet(options) { + var rsd = { + id: new Buffer([1, 0, 0, 0, 0, 0, 0, 0]), + metadata: [{ + dataType: TypeCode.SMALLINT, + columnDisplayName: 'SMALLINT' + }], + data: { + argumentCount: 5, + attributes: ResultSetAttributes.LAST | + ResultSetAttributes.CLOSED, + kind: 5, + buffer: Buffer.concat([ + writeInt(1), writeInt(2), writeInt(3), + writeInt(4), writeInt(5) + ]) + } + }; + return createResultSet(rsd, undefined, options); +} + +function createResultSetWithRepeats(options) { + // Buffer with small int encodings for 1 - 32 + var buffer32 = Buffer.concat(Array.from(Array(32).keys()).map(function (value) { + return writeInt(value + 1); + })); + var rsd = { + id: new Buffer([1, 0, 0, 0, 0, 0, 0, 0]), + metadata: [{ + dataType: TypeCode.SMALLINT, + columnDisplayName: 'SMALLINT' + }], + data: { + argumentCount: 32, + attributes: 0, + buffer: buffer32 + } + }; + var chunks = [{ + argumentCount: 32, + attributes: 0, + buffer: buffer32 + }, { + argumentCount: 32, + attributes: 0, + buffer: buffer32 + }, { + argumentCount: 4, + attributes: ResultSetAttributes.LAST | + ResultSetAttributes.CLOSED, + buffer: Buffer.concat([ + writeInt(97), writeInt(98), writeInt(99), writeInt(100) + ]) + }]; + return createResultSet(rsd, chunks, options); +} + describe('Lib', function () { describe('#ResultSet', function () { @@ -549,5 +606,48 @@ describe('Lib', function () { }); }); + it('should get row count without fetch all for one chunk result set', function (done) { + var rs = createOneChunkResultSet(); + rs.getRowCount().should.equal(5); + rs.fetch(function (err, rows) { + if (err) done(err); + rows.should.eql([{ + SMALLINT: 1, + }, { + SMALLINT: 2, + }, { + SMALLINT: 3, + }, { + SMALLINT: 4, + }, { + SMALLINT: 5, + }]); + rs.finished.should.be.true; + rs.closed.should.be.true; + done(); + }); + }); + + it('should not get row count until last chunk is received', function (done) { + var rs = createResultSetWithRepeats(); + rs.getRowCount().should.equal(-1); + rs.fetch(function (err, rows) { + if (err) done(err); + var expectedRows = []; + for (var i = 0; i < 100; i++) { + if (i >= 96) { + expectedRows.push({SMALLINT: i + 1}); + } else { + expectedRows.push({SMALLINT: (i % 32) + 1}); + } + } + rows.should.eql(expectedRows); + rs.finished.should.be.true; + rs.closed.should.be.true; + rs.getRowCount().should.equal(100); + done(); + }); + }); + }); }); From ce8a44cc08a0c2b9c1834cbe2021414524be9c5d Mon Sep 17 00:00:00 2001 From: he-is-harry Date: Fri, 14 Feb 2025 15:14:07 -0500 Subject: [PATCH 2/4] Fixed dynatrace integration test - Updated the create table hook to allow the tests to be run without a config.json --- test/acceptance/db.Dynatrace.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/test/acceptance/db.Dynatrace.js b/test/acceptance/db.Dynatrace.js index 11db1c6..457f2e9 100644 --- a/test/acceptance/db.Dynatrace.js +++ b/test/acceptance/db.Dynatrace.js @@ -17,6 +17,7 @@ var async = require('async'); var db = require('../db')(); var RemoteDB = require('../db/RemoteDB'); +var isRemoteDB = db instanceof RemoteDB; var util = require('../../lib/util'); var hanaDynatrace = require('../../extension/Dynatrace'); var dynatraceSDK; // either the real @dynatrace/oneagent-sdk Dynatrace SDK or the mock one @@ -410,8 +411,21 @@ describeDynatrace('db', function () { }); describeDynatrace('using table', function () { - beforeEach(db.createTable.bind(db, 'TEST_DYNATRACE', ['ID INT UNIQUE NOT NULL'], null)); - afterEach(db.dropTable.bind(db, 'TEST_DYNATRACE')); + beforeEach(function (done) { + if (isRemoteDB) { + db.createTable.bind(db)('TEST_DYNATRACE', ['ID INT UNIQUE NOT NULL'], null, done); + } else { + this.skip(); + done(); + } + }); + afterEach(function (done) { + if (isRemoteDB) { + db.dropTable.bind(db)('TEST_DYNATRACE', done); + } else { + done(); + } + }); it('should trace a client insert', function (done) { var destInfo = getDestInfoForDynatrace(); From 7744a9f4760b3d449881850724a31373f022e365 Mon Sep 17 00:00:00 2001 From: he-is-harry Date: Fri, 21 Feb 2025 10:26:21 -0500 Subject: [PATCH 3/4] Added rowsReturned dynatrace for inserts with execute - Modified Dynatrace.js result set callback to allow array results to indicate the rows returned not just result sets like before - Added an integration test to check that an insert with execute would trace the number of rows affected --- extension/Dynatrace.js | 9 ++++++-- test/acceptance/db.Dynatrace.js | 37 ++++++++++++++++++++++++--------- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/extension/Dynatrace.js b/extension/Dynatrace.js index ce7fb0f..2697b4b 100644 --- a/extension/Dynatrace.js +++ b/extension/Dynatrace.js @@ -62,13 +62,14 @@ function _dynatraceResultSetCallback(tracer, cb) { // With DB calls, the first argument can potentially be output parameters // In that case, we consider the next parameter - if (typeof resultSet === 'object' && resultSet !== null && !(resultSet instanceof ResultSet)) { + if (typeof resultSet === 'object' && resultSet !== null && !(resultSet instanceof ResultSet) + && !Array.isArray(resultSet)) { resultSet = args[1]; } if (err) { tracer.error(err); - } else if(resultSet) { + } else if(resultSet instanceof ResultSet) { const rowCount = resultSet.getRowCount(); // A negative rowCount means the number of rows is unknown. // This happens if the client hasn't received the last fetch chunk yet (with default server configuration, @@ -76,6 +77,10 @@ function _dynatraceResultSetCallback(tracer, cb) { if(rowCount >= 0) { tracer.setResultData({rowsReturned: rowCount}); } + } else if (resultSet !== undefined) { + tracer.setResultData({ + rowsReturned: (resultSet && resultSet.length) || resultSet + }); } tracer.end(cb, err, ...args); }; diff --git a/test/acceptance/db.Dynatrace.js b/test/acceptance/db.Dynatrace.js index 457f2e9..e834330 100644 --- a/test/acceptance/db.Dynatrace.js +++ b/test/acceptance/db.Dynatrace.js @@ -466,22 +466,39 @@ describeDynatrace('db', function () { }); }); - it('should trace a statement batch exec', function (done) { + function testStatementBatchInsert(useExec, done) { var destInfo = getDestInfoForDynatrace(); var sql = 'INSERT INTO TEST_DYNATRACE VALUES(?)'; - client.prepare(sql, function (err, stmt) { + var statement; + + function validateInsert(err, rowsAffected) { if (err) done(err); - stmt.exec([[1], [2], [3], [4]], function (err, rowsAffected) { + rowsAffected.should.eql([1, 1, 1, 1]); + verifyDynatraceData(destInfo, sql, 4); + client.exec('SELECT COUNT(*) FROM TEST_DYNATRACE', {rowsAsArray: true}, function (err, rows) { if (err) done(err); - rowsAffected.should.eql([1, 1, 1, 1]); - verifyDynatraceData(destInfo, sql, 4); - client.exec('SELECT COUNT(*) FROM TEST_DYNATRACE', {rowsAsArray: true}, function (err, rows) { - if (err) done(err); - rows[0][0].should.equal(4); - cleanup(stmt, done); - }); + rows[0][0].should.equal(4); + cleanup(statement, done); }); + } + + client.prepare(sql, function (err, stmt) { + if (err) done(err); + statement = stmt; + if (useExec) { + statement.exec([[1], [2], [3], [4]], validateInsert); + } else { + statement.execute([[1], [2], [3], [4]], validateInsert); + } }); + } + + it('should trace a statement batch exec', function (done) { + testStatementBatchInsert(true, done); + }); + + it('should trace a statement batch execute', function (done) { + testStatementBatchInsert(false, done); }); it('should trace a statement batch exec error', function (done) { From f8e5a6653a147fef6a35359e87f46d8ebb4aefd0 Mon Sep 17 00:00:00 2001 From: he-is-harry Date: Thu, 27 Feb 2025 10:45:24 -0500 Subject: [PATCH 4/4] Added Dynatrace prepare index check - Added an index check to ensure that the callback when preparing statements for dynatrace exists and is a function - Improved some test naming to better indicate client execute tests vs. client exec and statement execute tests - Added a comment to indicate when numbers are returned for rows affected --- extension/Dynatrace.js | 11 ++++++++--- test/acceptance/db.Dynatrace.js | 10 +++++----- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/extension/Dynatrace.js b/extension/Dynatrace.js index 2697b4b..0878395 100644 --- a/extension/Dynatrace.js +++ b/extension/Dynatrace.js @@ -48,6 +48,9 @@ function _dynatraceResultCallback(tracer, cb) { if (err) { tracer.error(err); } else if(results !== undefined) { + // In 0.19.12, results will typically be an array, but for non-batch insert / + // delete / update, results will be a number. This may be changed later to + // match hana-client which returns rows affected as a number even for batches. tracer.setResultData({ rowsReturned: (results && results.length) || results }); @@ -78,6 +81,7 @@ function _dynatraceResultSetCallback(tracer, cb) { tracer.setResultData({rowsReturned: rowCount}); } } else if (resultSet !== undefined) { + // Same as above, sometimes resultSet can be a number for non-batch insert / delete / update tracer.setResultData({ rowsReturned: (resultSet && resultSet.length) || resultSet }); @@ -120,7 +124,10 @@ function _DynatraceStmt(stmt, conn, sql) { function _prepareStmtUsingDynatrace(conn, prepareFn) { // args = [sql, options, callback] --> options is optional return function (...args) { - const cb = args[args.length - 1]; + var cb; + if (args.length > 0 && typeof args[args.length - 1] === 'function') { + cb = args[args.length - 1]; + } var sql = args[0]; if(typeof(sql) !== 'string') { sql = ''; // prepare will fail, but need sql for when the error is traced @@ -166,8 +173,6 @@ function dynatraceConnection(conn, destinationInfo) { return conn; } conn._dbInfo = dbInfo; - // hana-client does not like decorating. - // because of that, we need to override the fn and pass the original fn for execution const originalExecFn = conn.exec; conn.exec = _ExecuteWrapperFn(conn, conn, originalExecFn, _dynatraceResultCallback); const originalExecuteFn = conn.execute; diff --git a/test/acceptance/db.Dynatrace.js b/test/acceptance/db.Dynatrace.js index e834330..8ec2899 100644 --- a/test/acceptance/db.Dynatrace.js +++ b/test/acceptance/db.Dynatrace.js @@ -229,7 +229,7 @@ describeDynatrace('db', function () { async.waterfall([prepare, testExecStatement(['1']), testExecStatement(['2']), dropStatement], done); }); - it('should trace a client execute', function (done) { + it('should trace a client execute with a result set with 10 rows', function (done) { var destInfo = getDestInfoForDynatrace(); var sql = 'SELECT TOP 10 * FROM OBJECTS'; client.execute(sql, function (err, rs) { @@ -271,16 +271,16 @@ describeDynatrace('db', function () { }); } - it('should trace a execute with a result set with 1 row', function (done) { + it('should trace a statement execute with a result set with 1 row', function (done) { testDynatraceExecuteNRows(1, done); }); - it('should trace a execute with a result set with 32 rows', function (done) { + it('should trace a statement execute with a result set with 32 rows', function (done) { testDynatraceExecuteNRows(32, done); }); - it('should trace a execute with a result set with 33 rows', function (done) { + it('should trace a statement execute with a result set with 33 rows', function (done) { testDynatraceExecuteNRows(33, done); }); - it('should trace a execute with a result set with 0 rows', function (done) { + it('should trace a statement execute with a result set with 0 rows', function (done) { var destInfo = getDestInfoForDynatrace(); var sql = 'SELECT 3 FROM DUMMY WHERE 1 = 0'; client.prepare(sql, function (err, stmt) {