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/6] 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/6] 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/6] 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/6] 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) { From d6a6926c627abd25c930592d9f7ec7b3757fa33f Mon Sep 17 00:00:00 2001 From: he-is-harry Date: Tue, 11 Mar 2025 11:20:20 -0400 Subject: [PATCH 5/6] Added OpenTelemetry support - Added extension/OpenTelemetry.js to automatically wrap exec / execute / prepare / commit / rollback with OpenTelemetry tracing - Client will automatically wrap with OpenTelemetry tracing when isOpenTelemetryEnabled() is true - Most async functions will propagate context (except connect to match hana-client) - Added openTelemetry and openTelemetryTenant connect options - Added isOpenTelemetrySupported property to the driver (Hana class) - Added MockOpenTelemetrySDK and integration tests Testing Notes OpenTelemetry is only tested when the ENABLE_NODE_OPENTEL_TESTS environment variable is set and not 0 or false. So the Makefile now has `make test-opentelemetry` which will run the OpenTelemetry integration tests with this environment variable. To install the MockOpenTelemetrySDK, copy the contents of the MockOpenTelemetrySDK into node_modules/@opentelemetry/api --- Makefile | 10 +- extension/OpenTelemetry.js | 322 ++++++++++ index.js | 1 + lib/Client.js | 23 +- lib/index.js | 5 +- test/MockOpenTelemetryAPI/index.js | 146 +++++ test/MockOpenTelemetryAPI/package.json | 6 + test/acceptance/db.Dynatrace.js | 579 ------------------ test/acceptance/db.ExtTrace.js | 794 +++++++++++++++++++++++++ 9 files changed, 1299 insertions(+), 587 deletions(-) create mode 100644 extension/OpenTelemetry.js create mode 100644 test/MockOpenTelemetryAPI/index.js create mode 100644 test/MockOpenTelemetryAPI/package.json delete mode 100644 test/acceptance/db.Dynatrace.js create mode 100644 test/acceptance/db.ExtTrace.js diff --git a/Makefile b/Makefile index 9ed2671..5583726 100644 --- a/Makefile +++ b/Makefile @@ -13,8 +13,14 @@ test-acceptance: -R $(REPORTER) -b test/acceptance/*.js test-dynatrace: - @NODE_ENV=test ./node_modules/.bin/mocha \ - -R $(REPORTER) -b test/acceptance/db.Dynatrace.js + @ENABLE_NODE_OPENTEL_TESTS=false NODE_ENV=test \ + ./node_modules/.bin/mocha -R $(REPORTER) -b \ + test/acceptance/db.ExtTrace.js + +test-opentelemetry: + @ENABLE_NODE_OPENTEL_TESTS=true NODE_ENV=test \ + ./node_modules/.bin/mocha -R $(REPORTER) -b \ + test/acceptance/db.ExtTrace.js test-mock: @HDB_MOCK=1 $(MAKE) -s test diff --git a/extension/OpenTelemetry.js b/extension/OpenTelemetry.js new file mode 100644 index 0000000..ee1fce7 --- /dev/null +++ b/extension/OpenTelemetry.js @@ -0,0 +1,322 @@ +// 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'; + +// The hdb driver will automatically use this to add OpenTelemetry +// support when @opentelemetry/api is already installed. + +var ResultSet = require('../lib/protocol/ResultSet'); +var OTel = {}; +try { + // @opentelemetry/api must be installed by the application in order for + // the client to use it. + OTel.API = require('@opentelemetry/api'); + var pjson = require('../package.json'); + OTel.Tracer = OTel.API.trace.getTracer('hdb', pjson.version); + // Future: ideally use SEMATTRS_ values from OTel.semConv = require('@opentelemetry/semantic-conventions') + // Currently do not to avoid the problem of what if @opentelemtry/api is installed but not + // @opentelemetry/semantic-conventions? +} catch (err) { + // If module was not found, do not do anything + if(OTel.Tracer) { + OTel.Tracer = undefined; + } +} + +function isOpenTelemetryEnabled() { + if(OTel.Tracer === undefined) { + return false; + } + const envVar = process.env.HDB_NODEJS_SKIP_OPENTELEMETRY; + if(envVar && envVar != '0' && envVar.toLowerCase() != 'false') { + return false; + } + return true; +} + +function _getSpanNameAndStatus(op, sql, conn) { + // spanName and attributes roughly follow: + // https://github.tools.sap/CPA/telemetry-semantic-conventions/blob/main/docs/database/database-spans.md + // Note the above SAP copy differs from the OpenTelemetry spec of: + // https://opentelemetry.io/docs/specs/semconv/database/database-span + + // The specs says the span name could be ., + // but instead follow CAP and postgresql which roughly use " - " + var spanName = op; + if(sql) { + spanName = op + " - " + sql; + if(spanName.length > 80) { + spanName = spanName.substring(0, 79) + '…'; // based on what CAP used + } + } + // Future: consider using OTel.semConv.SEMATTRS_DB_ values instead of hardcoding attribute names + // FYI CAP and postgresql use net.peer.name, and net.peer.port instead of server.address and server.port + var spanOptions = {kind: OTel.API.SpanKind.CLIENT, + attributes: {'db.system' : 'hanadb', // FYI OT spec says sap.hana + 'server.address': conn._destinationInfo.host,}}; + if(conn._destinationInfo.port) { + try { + spanOptions.attributes['server.port'] = Number(conn._destinationInfo.port); + } catch (err) { + // ignore conversion error + } + } + if(typeof(sql) === 'string') { + // Follow Dynatrace which limits SQL to 1000 characters + var sql_text = sql.length > 1000 ? sql.substring(0, 999) + '…' : sql; + spanOptions.attributes['db.statement'] = sql_text; + } + if(conn._destinationInfo.tenant) { + spanOptions.attributes['db.name'] = conn._destinationInfo.tenant; + } + return {spanName: spanName, spanOptions: spanOptions}; +} + +function _setSpanStatus(span, err) { + if(err) { + span.setStatus(Object.assign({code: OTel.API.SpanStatusCode.ERROR }, err.message ? { message: err.message } : undefined)); + if(err.code) { + // https://opentelemetry.io/docs/specs/semconv/attributes-registry/db/ says this value should be a string + span.setAttribute('db.response.status_code', err.code.toString()); + } + } else { + span.setStatus({code: OTel.API.SpanStatusCode.OK}); + } +} + +function _openTelemetryResultCallback(span, activeCtx, cb) { + return function (err, ...args) { + _setSpanStatus(span, err); + 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(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. + span.setAttribute('db.response.returned_rows', (results && results.length) || results); + } + span.end(); + // propagate the active context for async calls + // (otherwise spans started within cb will not know the parent span) + return OTel.API.context.with(activeCtx, function() { + return cb(err, ...args); + }); + }; +} + +function _openTelemetryResultSetCallback(span, activeCtx, 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) + && !Array.isArray(resultSet)) { + resultSet = args[1]; + } + _setSpanStatus(span, err); + 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, + // this happens if the result set is larger than 32 rows) + if(rowCount >= 0) { + span.setAttribute('db.response.returned_rows', rowCount); + } + // modify resultSet for OpenTelemetry after a successful execute + // async methods that do not trace to OpenTelemetry + _setPropagateContextWrapper(resultSet, resultSet.close, "close"); + _setPropagateContextWrapper(resultSet, resultSet.fetch, "fetch"); + } else if (resultSet !== undefined) { + // Same as above, sometimes resultSet can be a number for non-batch insert / delete / update + span.setAttribute('db.response.returned_rows', (resultSet && resultSet.length) || resultSet); + } + span.end(); + // propagate the active context for async calls + // (otherwise spans started within cb will not know the parent span) + return OTel.API.context.with(activeCtx, function() { + return cb(err, ...args); + }); + }; +} + +// Wrapper for thisArg.origFn that we do NOT want to create a span for (eg stmt.drop) +// but we still want to propagate the active context on an async call. +// The method's callback first parameter must the error object. +function _propagateContextWrapperFn(thisArg, origFn) { + // args can end with a callback + return function (...args) { + var cb; + if (args.length > 0 && typeof args[args.length - 1] === 'function') { + cb = args[args.length - 1]; + } + // Sometimes cb can be undefined for disconnect and drop + if(cb) { + const activeCtx = OTel.API.context.active(); + origFn.call(thisArg, ...args.slice(0, args.length - 1), function (...cbArgs) { + // propagate the active context for async calls + // (otherwise spans started within cb will not know the parent span) + OTel.API.context.with(activeCtx, function() { + cb(...cbArgs); + }); + }); + } else { + // No callback so no need to pass context + return origFn.call(thisArg, ...args); + } + } +} + +// thisArg is the class, origFn is the method, fnName is a string (name of method) +function _setPropagateContextWrapper(thisArg, origFn, fnName) { + Object.defineProperty(thisArg, fnName, {value: _propagateContextWrapperFn(thisArg, origFn)}); +} + +// Wrapper for thisArg.origFn that is not a prepare or execute method (eg conn.commit) +// to create a span for the operation. +// The method's callback first parameter must the error object. +function _generalWrapperFn(thisArg, origFn, op, conn) { + // args should end with a callback + return function (...args) { + var cb; + var activeCtx = OTel.API.context.active(); + if (args.length > 0 && typeof args[args.length - 1] === 'function') { + cb = args[args.length - 1]; + } + const {spanName, spanOptions} = _getSpanNameAndStatus(op, undefined, conn); + return OTel.Tracer.startActiveSpan(spanName, spanOptions, function(span) { + // async method call + // cb can potentially be undefined but the function will still go through, so we log but throw an error + // when cb tries to be run + return origFn.call(thisArg, ...args.slice(0, args.length - 1), function (...cbArgs) { + // if cbArgs is empty, cbArgs[0] is undefined, so this is safe + _setSpanStatus(span, cbArgs[0]); + span.end(); + // propagate the active context for async calls + // (otherwise spans started within cb will not know the parent span) + OTel.API.context.with(activeCtx, function() { + cb(...cbArgs); + }); + }); + }); + } +} + +// wrapper for exec and execute +function _executeWrapperFn(thisArg, conn, execFn, op, resultCB, sql) { + // connection exec args = [sql, options, callback] --> options is optional + // stmt exec args = [options, callback] --> options is optional + return function (...args) { + if(thisArg === conn && args.length > 0) { + sql = args[0]; + } + if(typeof(sql) !== 'string') { + sql = ''; // execute will fail, but need sql for when the error is traced + } + var cb; + var activeCtx = OTel.API.context.active(); + if (args.length > 0 && typeof args[args.length - 1] === 'function') { + cb = args[args.length - 1]; + } + const {spanName, spanOptions} = _getSpanNameAndStatus(op, sql, conn); + return OTel.Tracer.startActiveSpan(spanName, spanOptions, function(span) { + // 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 + return execFn.call(thisArg, ...args.slice(0, args.length - 1), resultCB(span, activeCtx, cb)); + }); + } +} + +// modify stmt for OpenTelemetry after a successful prepare +function _modifyStmt(stmt, conn, sql) { + const originalExecFn = stmt.exec; + stmt.exec = _executeWrapperFn(stmt, conn, originalExecFn, "exec", _openTelemetryResultCallback, sql); + + const originalExecuteFn = stmt.execute; + stmt.execute = _executeWrapperFn(stmt, conn, originalExecuteFn, "execute", _openTelemetryResultSetCallback, sql); + + // async methods that do not trace to OpenTelemetry + _setPropagateContextWrapper(stmt, stmt.drop, "drop"); +} + +function _prepareWrapperFn(conn, prepareFn) { + // args = [sql, options, callback] --> options is optional + return function (...args) { + var cb; + var activeCtx = OTel.API.context.active(); + 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 + } + const {spanName, spanOptions} = _getSpanNameAndStatus("prepare", sql, conn); + return OTel.Tracer.startActiveSpan(spanName, spanOptions, function(span) { + // async prepare + // same as before, cb can be undefined but we still log, but throw an error after + prepareFn.call(conn, ...args.slice(0, args.length - 1), function prepare_handler(err, stmt) { + _setSpanStatus(span, err); + span.end(); + // propagate the active context for async calls + // (otherwise spans started within cb will not know the parent span) + OTel.API.context.with(activeCtx, function() { + if (err) { + cb(err); + } else { + _modifyStmt(stmt, conn, sql); + cb(err, stmt); + } + }); + }); + }); + } +} + +// destinationInfo is an object with host, port and optionally tenant keys +function openTelemetryConnection(conn, destinationInfo) { + if(OTel.Tracer === undefined) { + return conn; + } + if(conn._destinationInfo) { + // openTelemetryConnection has already been called on conn, use new destinationInfo + // in case it changed, but don't wrap conn again + conn._destinationInfo = destinationInfo; + return conn; + } + conn._destinationInfo = destinationInfo; + + const originalExecFn = conn.exec; + conn.exec = _executeWrapperFn(conn, conn, originalExecFn, "exec", _openTelemetryResultCallback); + const originalExecuteFn = conn.execute; + conn.execute = _executeWrapperFn(conn, conn, originalExecuteFn, "execute", _openTelemetryResultSetCallback); + const originalPrepareFn = conn.prepare; + Object.defineProperty(conn, 'prepare', {value: _prepareWrapperFn(conn, originalPrepareFn)}); + + const originalCommitFn = conn.commit; + Object.defineProperty(conn, 'commit', {value: _generalWrapperFn(conn, originalCommitFn, "commit", conn)}); + const originalRollbackFn = conn.rollback; + Object.defineProperty(conn, 'rollback', {value: _generalWrapperFn(conn, originalRollbackFn, "rollback", conn)}); + + // async methods that do not trace to OpenTelemetry + _setPropagateContextWrapper(conn, conn.disconnect, "disconnect"); + + return conn; +} + +module.exports = { openTelemetryConnection, isOpenTelemetryEnabled }; diff --git a/index.js b/index.js index 65ae754..83e18bd 100644 --- a/index.js +++ b/index.js @@ -20,3 +20,4 @@ exports.Stringifier = lib.Stringifier; exports.createJSONStringifier = lib.createJSONStringifier; exports.iconv = require('iconv-lite'); exports.isDynatraceSupported = lib.isDynatraceSupported; +exports.isOpenTelemetrySupported = lib.isOpenTelemetrySupported; diff --git a/lib/Client.js b/lib/Client.js index ad4d374..e38dd5d 100644 --- a/lib/Client.js +++ b/lib/Client.js @@ -21,6 +21,7 @@ var Result = protocol.Result; var Statement = protocol.Statement; var ConnectionManager = protocol.ConnectionManager; var hanaDynatrace = require('../extension/Dynatrace'); +var hanaOpenTelemetry = require('../extension/OpenTelemetry'); module.exports = Client; @@ -33,7 +34,8 @@ function Client(options) { fetchSize: 1024, holdCursorsOverCommit: true, scrollableCursor: true, - dynatrace: true + dynatrace: true, + openTelemetry: true }, options); this._settings.useCesu8 = (this._settings.useCesu8 !== false); normalizeSettings(this._settings); @@ -125,7 +127,8 @@ Client.prototype.connect = function connect(options, cb) { 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); + dynatraceTenant: this._settings.dynatraceTenant, + openTelemetryTenant: this._settings.openTelemetryTenant}, connectOptions); var connManager = new ConnectionManager(connectOptions); // SAML assertion can only be used once @@ -317,6 +320,11 @@ function normalizeSettings(settings) { settings['dynatrace'] = isValid ? value : true; } else if (key.toUpperCase() === 'DYNATRACETENANT') { settings['dynatraceTenant'] = settings[key]; + } else if (key.toUpperCase() === 'OPENTELEMETRY') { + var {value, isValid} = util.validateAndGetBoolProperty(settings[key]); + settings['openTelemetry'] = isValid ? value : true; + } else if (key.toUpperCase() === 'OPENTELEMETRYTENANT') { + settings['openTelemetryTenant'] = settings[key]; } } } @@ -346,8 +354,15 @@ function normalizeArguments(args, defaults) { return [command, options, cb]; } -function addDynatraceWrapper(client, destinationInfo, dynatraceOn) { - if (hanaDynatrace && hanaDynatrace.isDynatraceEnabled() && dynatraceOn) { +function addDynatraceWrapper(client, destinationInfo, connectOptions) { + // Prefer OpenTelemetry over Dynatrace if both are enabled + if (hanaOpenTelemetry && hanaOpenTelemetry.isOpenTelemetryEnabled() + && connectOptions.openTelemetry) { + destinationInfo.tenant = destinationInfo.openTelemetryTenant; + hanaOpenTelemetry.openTelemetryConnection(client, destinationInfo); + } else if (hanaDynatrace && hanaDynatrace.isDynatraceEnabled() + && connectOptions.dynatrace) { + destinationInfo.tenant = destinationInfo.dynatraceTenant; hanaDynatrace.dynatraceConnection(client, destinationInfo); } } diff --git a/lib/index.js b/lib/index.js index 9144945..815472b 100644 --- a/lib/index.js +++ b/lib/index.js @@ -36,5 +36,6 @@ exports.createJSONStringifier = function createJSONStringifier() { }); }; -// Dynatrace support should not change unless there are source code modifications -exports.isDynatraceSupported = true; \ No newline at end of file +// External trace support should not change unless there are source code modifications +exports.isDynatraceSupported = true; +exports.isOpenTelemetrySupported = true; diff --git a/test/MockOpenTelemetryAPI/index.js b/test/MockOpenTelemetryAPI/index.js new file mode 100644 index 0000000..bfda797 --- /dev/null +++ b/test/MockOpenTelemetryAPI/index.js @@ -0,0 +1,146 @@ +// 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 @opentelemetry/api 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/@opentelemetry/api directory. +// OR run `npm install /path/to/MockOpenTelemetryAPI` + +// traceData is an object with keys that are numbers (traceNum) and +// values that are objects with keys: +// {spanName, dbInfo, sql, startTime, endTime, error, rowsReturned} +// (traceData values are defined this way so the contents are the same as for MockDynatraceSDK) +var traceData = {}; +var traceEnabled = false; +var lastTraceNum = 0; + + +// matching drop-in replacement for a subset of the @opentelemetry/api interface +const StatusCodeEnum = {OK: 1, ERROR: 2}; +const ActiveContext = "Active Context"; + +class Span { + constructor(name, options, traceNum) { + this.traceNum = traceNum; + if(traceNum) { + var dbInfo = {host: options.attributes['server.address'], + port: options.attributes['server.port'], + name: 'SAPHANA', + vendor: 'HANADB'}; + if(options.attributes['db.name']) { + dbInfo.name = 'SAPHANA-' + options.attributes['db.name']; + } + // TODO consider validating the other attributes + // console.log("spanName: " + name + ", options: " + JSON.stringify(options)); // for debugging + traceData[traceNum] = {spanName: name, dbInfo: dbInfo, sql: options.attributes['db.statement'], startTime: new Date()}; + } + } + + setStatus(status) { + if(this.traceNum && status.code == StatusCodeEnum.ERROR) { + var err = {statusCode: status.code}; + if(status.message) { + err.message = status.message; + } + traceData[this.traceNum].error = err; + } + } + + // trace returned_rows and status_code + setAttribute(name, value) { + if(this.traceNum) { + if(name === 'db.response.returned_rows') { + traceData[this.traceNum].rowsReturned = value; + } else if(name === 'db.response.status_code' && traceData[this.traceNum].error) { + traceData[this.traceNum].error.code = Number(value); + } else { + console.log("Error: Span.setAttribute called with unexpected name: " + name); + } + } + } + + end() { + if(this.traceNum) { + if(traceData[this.traceNum].endTime) { + console.log("Error: Span.end called more than once"); + } + traceData[this.traceNum].endTime = new Date(); + } + } + + // data members: traceNum (undefined if not tracing) +} + +class Tracer { + constructor(name, version) { + // console.log('in Tracer constructor name: ' + name + ', version:' + version); + } + + startSpan(name, options) { + var traceNum; // undefined if trace is not enabled + if(traceEnabled) { + traceNum = ++lastTraceNum; + } + return new Span(name, options, traceNum); + } + + startActiveSpan(name, options, fn) { + var traceNum; // undefined if trace is not enabled + if(traceEnabled) { + traceNum = ++lastTraceNum; + } + return fn(new Span(name, options, traceNum)); + } +} + +// @opentelemetry/api interface replacement +exports.trace = { + getTracer: function getTracer(name, version) { + return new Tracer(name, version); + } +} +exports.context = { + active: function () { + return ActiveContext; + }, + + with: function (context, fn) { + if(context !== ActiveContext) { + console.log("Error: context.with called with unexpected context"); + } + return fn(); + } +} +exports.SpanKind = {CLIENT: 2}; +exports.SpanStatusCode = StatusCodeEnum; + +// 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/MockOpenTelemetryAPI/package.json b/test/MockOpenTelemetryAPI/package.json new file mode 100644 index 0000000..3415ee7 --- /dev/null +++ b/test/MockOpenTelemetryAPI/package.json @@ -0,0 +1,6 @@ +{ + "author": "SAP SE", + "name": "@opentelemetry/api", + "description": "mock of @opentelemetry/api for HANA Client testing", + "main": "./index.js" +} diff --git a/test/acceptance/db.Dynatrace.js b/test/acceptance/db.Dynatrace.js deleted file mode 100644 index 8ec2899..0000000 --- a/test/acceptance/db.Dynatrace.js +++ /dev/null @@ -1,579 +0,0 @@ -// 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 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 -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 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) { - 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 statement execute with a result set with 1 row', function (done) { - testDynatraceExecuteNRows(1, done); - }); - it('should trace a statement execute with a result set with 32 rows', function (done) { - testDynatraceExecuteNRows(32, done); - }); - it('should trace a statement execute with a result set with 33 rows', function (done) { - testDynatraceExecuteNRows(33, 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) { - 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(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(); - 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); - }); - }); - }); - }); - }); - }); - - function testStatementBatchInsert(useExec, done) { - var destInfo = getDestInfoForDynatrace(); - var sql = 'INSERT INTO TEST_DYNATRACE VALUES(?)'; - var statement; - - function validateInsert(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(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) { - 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/acceptance/db.ExtTrace.js b/test/acceptance/db.ExtTrace.js new file mode 100644 index 0000000..7788bd2 --- /dev/null +++ b/test/acceptance/db.ExtTrace.js @@ -0,0 +1,794 @@ +// 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 RemoteDB = require('../db/RemoteDB'); +var util = require('../../lib/util'); +// Both Dynatrace and OpenTelemetry are considered External tracers (ExtTrace) +var hanaDynatrace = require('../../extension/Dynatrace'); +var dynatraceSDK; // either the real @dynatrace/oneagent-sdk Dynatrace SDK or the mock one +var hanaOpenTel = require('../../extension/OpenTelemetry'); +var openTelAPI; // either the real @opentelemetry/api OpenTelemetry API or the mock one +var mockExtTrace; // either mock @dynatrace/oneagent-sdk or mock @opentelemetry/api +var http, server, request; +var db, isRemoteDB; +const openTelTestVar = process.env.ENABLE_NODE_OPENTEL_TESTS; +const testOpenTel = openTelTestVar && openTelTestVar != '0' && openTelTestVar.toLowerCase() != 'false'; +try { + if (testOpenTel) { + // Avoid getting dynatrace tracing (should be redundant since OpenTelemetry is preferred + // over dynatrace) + db = require('../db')({dynatrace: false}); + openTelAPI = require('@opentelemetry/api'); + if (openTelAPI.getTraceData !== undefined) { + // Using mock @opentelemetry/api + mockExtTrace = openTelAPI; + } else { + // Using real @opentelemetry/api, so setup web request + http = require('http'); + } + } else { + dynatraceSDK = require('@dynatrace/oneagent-sdk'); + // When testing dynatrace, disable open telemetry so that the tracing will go to dynatrace + db = require('../db')({openTelemetry: false}); + if (dynatraceSDK.getTraceData !== undefined) { + // Using mock @dynatrace/oneagent-sdk + mockExtTrace = dynatraceSDK; + } else { + // Using real @dynatrace/oneagent-sdk, so setup web request + http = require('http'); + } + } + isRemoteDB = db instanceof RemoteDB; +} catch (err) { + // No @dynatrace/oneagent-sdk / @opentelemetry/api, skip this test, see + // MockDynatraceSDK / MockOpenTelemetryAPI to "install" the mock to run + // these tests +} + +var describeExtTrace = (db instanceof RemoteDB && ((!testOpenTel && dynatraceSDK !== undefined) || + (testOpenTel && openTelAPI !== undefined))) ? describe : describe.skip; + +function isMockExtTraceEnabled() { + if (testOpenTel) { + return mockExtTrace && hanaOpenTel.isOpenTelemetryEnabled(); + } else { + return mockExtTrace && hanaDynatrace.isDynatraceEnabled(); + } +} + +describeExtTrace('db', function () { + before(function (done) { + if (isMockExtTraceEnabled()) { + mockExtTrace.enableTrace(); + } + if (mockExtTrace) { + db.init.bind(db)(done); + } else { + // Real external trace, 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 (isMockExtTraceEnabled()) { + mockExtTrace.disableTrace(); + } + if (mockExtTrace) { + db.end.bind(db)(done); + } else { + // Real external trace, stop the web request + request.end(); + server.close(); + db.end.bind(db)(done); + } + }); + // If the db is undefined, we will skip the tests + var client = db ? db.client : undefined; + + describeExtTrace('external trace', function () { + it('should trace a prepared statement exec', function (done) { + var sql = 'SELECT 1 FROM DUMMY'; + var destInfo = getDestInfoForExtTrace(); + client.prepare(sql, function (err, stmt) { + if (err) done(err); + verifyOpenTelData('prepare - ' + sql, destInfo, sql); + stmt.exec([], function (err, rows) { + if (err) done(err); + rows.should.have.length(1); + rows[0]['1'].should.equal(1); + verifyOpenTelSpan('exec - ' + sql); + verifyExtTraceData(destInfo, sql, 1); + cleanup(stmt, done); + }); + }); + }); + + it('should trace a client exec', function (done) { + var destInfo = getDestInfoForExtTrace(); + var sql = 'SELECT TOP 10 * FROM OBJECTS'; + client.exec(sql, function (err, rows) { + if (err) done(err); + rows.should.have.length(10); + verifyOpenTelSpan('exec - ' + sql); + verifyExtTraceData(destInfo, sql, 10); + done(); + }); + }); + + it('should trace exec / prepare errors', function (done) { + var destInfo = getDestInfoForExtTrace(); + 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 for the sql in the external trace + verifyOpenTelSpan((typeof input === 'string' ? 'exec - ' + input : 'exec')); + verifyExtTraceData(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); + verifyOpenTelSpan((typeof input === 'string' ? 'prepare - ' + input : 'prepare')); + verifyExtTraceData(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); + verifyOpenTelData('prepare - ' + sql, destInfo, sql); + // Check server version since HANA 2 SPS05 gives a server error + var version = db.getHANAFullVersion(); + var versionSplit = version.split("."); + var major = Number(versionSplit[0]); + var revision = Number(versionSplit[2]); + if (!(major == 2 && revision < 70)) { + statement.exec(['string to int cast'], function (err) { + err.should.be.an.instanceOf(Error); + verifyOpenTelSpan('exec - ' + sql); + verifyExtTraceData(destInfo, sql, undefined, { + message: 'Cannot set parameter at row: 1. Wrong input for INT type' + }); + cleanup(statement, cb); + }); + } else { + // Skip part of test on pre HANA2sp7 + 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 = getDestInfoForExtTrace(); + var sql = 'SELECT ? FROM DUMMY'; + client.prepare(sql, function (err, stmt) { + if (err) done(err); + verifyOpenTelData('prepare - ' + sql, destInfo, sql); + stmt.exec([], function (err, rows) { + err.should.be.an.instanceOf(Error); + verifyOpenTelSpan('exec - ' + sql); + verifyExtTraceData(destInfo, sql, undefined, { message: "Unbound parameters found." }); + cleanup(stmt, done); + }); + }); + }); + + it('should time an exec', function (done) { + var destInfo = getDestInfoForExtTrace(); + var sql = 'SELECT 2 FROM DUMMY'; + client.prepare(sql, function (err, stmt) { + if (err) done(err); + verifyOpenTelData('prepare - ' + sql, destInfo, sql); + 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; + verifyExtTraceRequestTime(Math.max(0, elapsedExecTime - 1000), elapsedExecTime); + verifyOpenTelSpan('exec - ' + sql); + verifyExtTraceData(destInfo, sql, 1); + cleanup(stmt, done); + }) + }); + }); + + it('should time a 2 second procedure', function (done) { + this.timeout(3000); + var destInfo = getDestInfoForExtTrace(); + var sql = 'DO BEGIN CALL SQLSCRIPT_SYNC:SLEEP_SECONDS(2); END'; + client.prepare(sql, function (err, stmt) { + if (err) done(err); + verifyOpenTelData('prepare - ' + sql, destInfo, sql); + 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); + verifyExtTraceRequestTime(Math.max(1900, elapsedExecTime - 1000), elapsedExecTime); + verifyOpenTelSpan('exec - ' + sql); + // This db call does not return any rows, so the behaviour is to not log any rowsReturned + verifyExtTraceData(destInfo, sql, undefined); + cleanup(stmt, done); + }); + }); + }); + + it('should trace multiple exec with a statement', function (done) { + var destInfo = getDestInfoForExtTrace(); + var sql = 'SELECT ? FROM DUMMY'; + var statement; + function prepare (cb) { + client.prepare(sql, function (err, ps) { + if (err) done(err); + statement = ps; + verifyOpenTelData('prepare - ' + sql, destInfo, sql); + 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; + verifyExtTraceRequestTime(Math.max(0, elapsedExecTime - 1000), elapsedExecTime); + verifyOpenTelSpan('exec - ' + sql); + verifyExtTraceData(destInfo, sql, 1); + rows.should.have.length(1); + rows[0][':1'].should.equal(input[0]); + cb(); + }); + }; + } + // Test that commits are traced for OpenTelemetry, since auto commit is on + // the commit has no effect + function commit(cb) { + client.commit(function (err) { + if (err) done(err); + verifyOpenTelData('commit', destInfo); + cb(); + }); + } + function dropStatement(cb) { + cleanup(statement, cb); + } + + async.waterfall([prepare, testExecStatement(['1']), testExecStatement(['2']), commit, dropStatement], done); + }); + + it('should trace a client exec with 2k length sql', function (done) { + var destInfo = getDestInfoForExtTrace(); + var sql = "SELECT '" + 'A'.repeat(2000) + "' FROM DUMMY"; + client.exec(sql, function (err, rows) { + if (err) done(err); + rows.should.have.length(1); + // Our extension/OpenTelemetry.js truncates the span name to 80 characters + // and sql to 1000 chars. + // Our extension/Dynatrace.js does not truncate sql since Dynatrace itself + // sanitizes SQL and limits it to 1000 characters. + var sqlTrunc1000 = sql.substring(0, 999) + '…'; + var spanNameTrunc80 = ("exec - " + sql).substring(0, 79) + '…'; + verifyOpenTelSpan(spanNameTrunc80); + verifyExtTraceData(destInfo, testOpenTel ? sqlTrunc1000 : sql, 1); + done(); + }); + }); + + it('should trace a client execute with a result set with 10 rows', function (done) { + var destInfo = getDestInfoForExtTrace(); + var sql = 'SELECT TOP 10 * FROM OBJECTS'; + client.execute(sql, function (err, rs) { + if (err) done(err); + verifyOpenTelSpan('execute - ' + sql); + verifyExtTraceData(destInfo, sql, 10); + rs.fetch(function (err, rows) { + if (err) done(err); + rows.should.have.length(10); + if (!rs.closed) { + rs.close(); + } + done(); + }); + }); + }); + + function testExtTraceExecuteNRows(numRows, cb) { + var destInfo = getDestInfoForExtTrace(); + var sql = 'SELECT TOP ? * FROM OBJECTS'; + client.prepare(sql, function (err, stmt) { + if (err) cb(err); + verifyOpenTelData('prepare - ' + sql, destInfo, sql); + stmt.execute([numRows], function (err, rs) { + if (err) cb(err); + // FYI, we define the ExtTrace 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; + verifyOpenTelSpan('execute - ' + sql); + verifyExtTraceData(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 statement execute with a result set with 1 row', function (done) { + testExtTraceExecuteNRows(1, done); + }); + it('should trace a statement execute with a result set with 32 rows', function (done) { + testExtTraceExecuteNRows(32, done); + }); + it('should trace a statement execute with a result set with 33 rows', function (done) { + testExtTraceExecuteNRows(33, done); + }); + it('should trace a statement execute with a result set with 0 rows', function (done) { + var destInfo = getDestInfoForExtTrace(); + var sql = 'SELECT 3 FROM DUMMY WHERE 1 = 0'; + client.prepare(sql, function (err, stmt) { + if (err) done(err); + verifyOpenTelData('prepare - ' + sql, destInfo, sql); + stmt.execute([], function (err, rs) { + if (err) done(err); + verifyOpenTelSpan('execute - ' + sql); + verifyExtTraceData(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 = getDestInfoForExtTrace(); + var sql = 'SELECT 1 FROM DUMMY WHERE 1 = ?'; + var statement; + function clearTraceData(cb) { + if (isMockExtTraceEnabled()) { + mockExtTrace.clearTraceData(); + mockExtTrace.getTraceData().should.be.empty; + } + cb(); + } + function prepare (cb) { + client.prepare(sql, function (err, ps) { + if (err) done(err); + statement = ps; + verifyOpenTelData('prepare - ' + sql, destInfo, sql); + 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 (isMockExtTraceEnabled()) { + Object.keys(mockExtTrace.getTraceData()).should.have.length(1); + } + verifyExtTraceRequestTime(Math.max(0, elapsedExecTime - 1000), elapsedExecTime); + verifyOpenTelSpan('execute - ' + sql); + verifyExtTraceData(destInfo, sql, expectedRows.length); + rs.fetch(function (err, rows) { + rows.should.eql(expectedRows); + if (!rs.closed) { + rs.close(); + } + cb(); + }) + }); + }; + } + // Test that rollbacks are traced for OpenTelemetry, since auto commit is on + // the rollback has no effect + function rollback(cb) { + client.rollback(function (err) { + if (err) done(err); + verifyOpenTelData('rollback', destInfo); + cb(); + }); + } + function dropStatement(cb) { + cleanup(statement, cb); + } + + async.waterfall([clearTraceData, prepare, testExecuteStatement(['1'], [{ '1': 1 }]), + testExecuteStatement(['2'], []), rollback, dropStatement], done); + }); + + it('should trace the first rows result in a DB call', function (done) { + var destInfo = getDestInfoForExtTrace(); + var sql = `DO (IN P1 INTEGER => ?, OUT P2 INTEGER => ?, OUT P3 INTEGER => ?) + BEGIN + P2 = :P1 + 1; + P3 = :P1 + 2; + SELECT TOP 10 * FROM OBJECTS; + SELECT 'A' AS A FROM DUMMY; + END`; + client.prepare(sql, function (err, stmt) { + if (err) done(err); + // Our extension/OpenTelemetry.js truncates the span name to 80 characters + var spanPrepName80 = ("prepare - " + sql).substring(0, 79) + '…'; + verifyOpenTelData(spanPrepName80, destInfo, sql); + stmt.exec([1], function (err, params, objRows, dummyRows) { + if (err) done(err); + params.should.eql({ P2: 2, P3: 3 }); + objRows.should.have.length(10); + dummyRows.should.have.length(1); + var spanExecName80 = ("exec - " + sql).substring(0, 79) + '…'; + verifyOpenTelSpan(spanExecName80); + // The current behaviour is that the rows returned is traced as the length + // of the first rows result (not object parameters) + verifyExtTraceData(destInfo, sql, 10); + cleanup(stmt, done); + }); + }); + }); + + it('should disable Dynatrace / OpenTelemetry with environment variable', function (done) { + if (testOpenTel) { + var skipOpenTelemetry = process.env.HDB_NODEJS_SKIP_OPENTELEMETRY; + process.env.HDB_NODEJS_SKIP_OPENTELEMETRY = true; + hanaOpenTel.isOpenTelemetryEnabled().should.equal(false); + if (skipOpenTelemetry) { + process.env.HDB_NODEJS_SKIP_OPENTELEMETRY = skipOpenTelemetry; + } else { + delete process.env.HDB_NODEJS_SKIP_OPENTELEMETRY; + } + } else { + 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 / OpenTelemetry with dynatrace / openTelemetry connect ' + + 'option (only tested on mock)', function (done) { + if (!isMockExtTraceEnabled()) { + // The disabling of Dynatrace / OpenTelemetry using the dynatrace / openTelemetry connect + // option can only be validated when Dynatrace / OpenTelemetry is enabled (no skip env + // and oneagent-sdk / opentelemetry/api exists) and we are using the mock + this.skip(); + } else { + var destInfo = getDestInfoForExtTrace(); + var sql = 'SELECT 1 FROM DUMMY'; + mockExtTrace.clearTraceData(); + var nonExtTraceDB = require('../db')({dynatrace: false, openTelemetry: false}); + nonExtTraceDB.init(function (err) { + if (err) done(err); + var nonExtTraceClient = nonExtTraceDB.client; + nonExtTraceClient.exec(sql, function (err, rows) { + if (err) done(err); + rows.should.have.length(1); + Object.keys(mockExtTrace.getTraceData()).length.should.equal(0); + + // Manually re-enable the Dynatrace / OpenTelemetry extension + if (testOpenTel) { + hanaOpenTel.openTelemetryConnection(nonExtTraceClient, destInfo); + } else { + hanaDynatrace.dynatraceConnection(nonExtTraceClient, destInfo); + } + nonExtTraceClient.exec(sql, function (err, rows) { + if (err) done(err); + Object.keys(mockExtTrace.getTraceData()).length.should.equal(1); + verifyOpenTelSpan('exec - ' + sql); + verifyExtTraceData(destInfo, sql, 1); + nonExtTraceDB.end(done); + }); + }); + }); + } + }); + + it('should configure a dynatraceTenant / openTelemetryTenant option', function (done) { + var tenantName = 'ExternalTraceTenant'; + var destInfo = getDestInfoForExtTrace(tenantName); + var sql = 'SELECT 1 FROM DUMMY'; + if (isMockExtTraceEnabled()) { + mockExtTrace.clearTraceData(); + } + var tenantDB; + if (testOpenTel) { + tenantDB = require('../db')({openTelemetry: true}); + // The db module will save settings, so we set the tenant settings directly in the client + // as they cannot be easily rewritten once saved + tenantDB.client.set('openTelemetryTenant', tenantName); + } else { + tenantDB = require('../db')({dynatrace: true}); + tenantDB.client.set('dynatraceTenant', tenantName); + } + tenantDB.init(function (err) { + if (err) done(err); + var tenantClient = tenantDB.client; + tenantClient.exec(sql, function (err, rows) { + if (err) done(err); + verifyOpenTelSpan('exec - ' + sql); + verifyExtTraceData(destInfo, sql, 1); + tenantDB.end(done); + }); + }); + }); + + it('should prefer OpenTelemetry over Dynatrace', function (done) { + if (!(isMockExtTraceEnabled() && hanaOpenTel.isOpenTelemetryEnabled() + && hanaDynatrace.isDynatraceEnabled())) { + // Preferring OpenTelemetry can only be tested when OpenTelemetry and Dynatrace + // are enabled and we are using the mock of Dynatrace or OpenTelemetry + this.skip(); + } else { + var destInfo = getDestInfoForExtTrace(); + var sql = 'SELECT 1 FROM DUMMY'; + var noPreferenceDB = require('../db')({dynatrace: true, openTelemetry: true}); + noPreferenceDB.init(function (err) { + if (err) done(err); + var noPrefClient = noPreferenceDB.client; + noPrefClient.exec(sql, function (err, rows) { + if (err) done(err); + rows.should.have.length(1); + if (testOpenTel) { + // ensure we traced to OpenTelemetry even though Dynatrace was avaliable + Object.keys(mockExtTrace.getTraceData()).length.should.equal(1); + verifyOpenTelData('exec - ' + sql, destInfo, sql, 1); + } else { + // since OpenTelemetry was avaliable, we should have no Dynatrace data + Object.keys(mockExtTrace.getTraceData()).length.should.equal(0); + } + noPreferenceDB.end(done); + }); + }); + } + }); + + describeExtTrace('using table', function () { + beforeEach(function (done) { + if (isRemoteDB) { + db.createTable.bind(db)('TEST_EXT_TRACE', ['ID INT UNIQUE NOT NULL'], null, done); + } else { + this.skip(); + done(); + } + }); + afterEach(function (done) { + if (isRemoteDB) { + db.dropTable.bind(db)('TEST_EXT_TRACE', done); + } else { + done(); + } + }); + + it('should trace a client insert', function (done) { + var destInfo = getDestInfoForExtTrace(); + var sql = 'INSERT INTO TEST_EXT_TRACE VALUES(1)'; + client.exec(sql, function (err, rowsAffected) { + if (err) done(err); + rowsAffected.should.equal(1); + verifyOpenTelSpan('exec - ' + sql); + // Trace rows affected as rows returned + verifyExtTraceData(destInfo, sql, 1); + client.exec('SELECT COUNT(*) FROM TEST_EXT_TRACE', {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 = getDestInfoForExtTrace(); + var sql = 'DELETE FROM TEST_EXT_TRACE'; + client.exec('INSERT INTO TEST_EXT_TRACE VALUES(1)', function (err, rowsAffected) { + if (err) done(err); + client.exec('INSERT INTO TEST_EXT_TRACE VALUES(2)', function (err, rowsAffected) { + if (err) done(err); + client.prepare(sql, function (err, stmt) { + if (err) done(err); + verifyOpenTelData('prepare - ' + sql, destInfo, sql); + stmt.exec([], function (err, rowsAffected) { + rowsAffected.should.equal(2); + verifyOpenTelSpan('exec - ' + sql); + verifyExtTraceData(destInfo, sql, 2); + client.exec('SELECT COUNT(*) FROM TEST_EXT_TRACE', {rowsAsArray: true}, function (err, rows) { + if (err) done(err); + rows[0][0].should.equal(0); + cleanup(stmt, done); + }); + }); + }); + }); + }); + }); + + function testStatementBatchInsert(useExec, done) { + var destInfo = getDestInfoForExtTrace(); + var sql = 'INSERT INTO TEST_EXT_TRACE VALUES(?)'; + var statement; + + function validateInsert(err, rowsAffected) { + if (err) done(err); + rowsAffected.should.eql([1, 1, 1, 1]); + verifyOpenTelSpan((useExec ? 'exec' : 'execute') + ' - ' + sql); + verifyExtTraceData(destInfo, sql, 4); + client.exec('SELECT COUNT(*) FROM TEST_EXT_TRACE', {rowsAsArray: true}, function (err, rows) { + if (err) done(err); + rows[0][0].should.equal(4); + cleanup(statement, done); + }); + } + + client.prepare(sql, function (err, stmt) { + if (err) done(err); + verifyOpenTelData('prepare - ' + sql, destInfo, sql); + 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) { + var destInfo = getDestInfoForExtTrace(); + var sql = 'INSERT INTO TEST_EXT_TRACE VALUES(?)'; + client.prepare(sql, function (err, stmt) { + if (err) done(err); + verifyOpenTelData('prepare - ' + sql, destInfo, sql); + stmt.exec([['string going to int column'], ['2']], function (err, rowsAffected) { + err.should.be.an.instanceOf(Error); + (!!rowsAffected).should.not.be.ok; + verifyOpenTelSpan('exec - ' + sql); + verifyExtTraceData(destInfo, sql, undefined, { + message: 'Cannot set parameter at row: 1. Wrong input for INT type' + }); + cleanup(stmt, done); + }); + }); + }); + }); + }); + + function getDestInfoForExtTrace(tenant) { + return { host: client.get('host'), port: client.get('port'), tenant: tenant }; + } +}); + +function verifyExtTraceData(destInfo, sql, expectedRowsReturned, expectedError) { + if(isMockExtTraceEnabled()) { // Only validate the data on the mock + var got = mockExtTrace.getTraceData()[mockExtTrace.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)); + if (sql !== undefined) { + got.sql.should.eql(sql); + } else { + (got.sql === undefined).should.be.ok; + } + + 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); + } + mockExtTrace.clearTraceData(); + } +} + +// must be called before verifyExtTraceData since that clears the trace data +function verifyExtTraceRequestTime(minAllowedMS, maxAllowedMS) { + if(isMockExtTraceEnabled()) { + var got = mockExtTrace.getTraceData()[mockExtTrace.getLastTraceNum()]; + got.should.not.be.undefined; + if(got) { + var gotElapsedTime = got.endTime - got.startTime; + gotElapsedTime.should.be.aboveOrEqual(minAllowedMS); + gotElapsedTime.should.be.belowOrEqual(maxAllowedMS); + } + } +} + +// Verifies span name when open telemetry is running, does not clear trace data +function verifyOpenTelSpan(spanName) { + // Only validate the data on the mock opentelemetry + if(testOpenTel && isMockExtTraceEnabled()) { + var got = mockExtTrace.getTraceData()[mockExtTrace.getLastTraceNum()]; + got.should.not.be.undefined; + if(got) { + got.spanName.should.eql(spanName); + } + } +} + +// Verifies all data (including span name) when open telemetry is running, and +// does clear trace data +function verifyOpenTelData(spanName, destInfo, sql, expectedRowsReturned, expectedError) { + // Only validate the data on the mock opentelemetry + if(testOpenTel && isMockExtTraceEnabled()) { + verifyOpenTelSpan(spanName); + verifyExtTraceData(destInfo, sql, expectedRowsReturned, expectedError); + } +} + +function cleanup(stmt, cb) { + stmt.drop(function (err) { + // ignore error + cb(); + }) +} \ No newline at end of file From 64d2b1680704b0ef46df0af3becb5aa04de8d82e Mon Sep 17 00:00:00 2001 From: he-is-harry Date: Tue, 11 Mar 2025 14:18:00 -0400 Subject: [PATCH 6/6] Added delay to real external trace test end - Added 10 seconds of delay before external traces tests are done when testing with real tracing backends to give them time to log --- test/acceptance/db.ExtTrace.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/acceptance/db.ExtTrace.js b/test/acceptance/db.ExtTrace.js index 7788bd2..1ce0afe 100644 --- a/test/acceptance/db.ExtTrace.js +++ b/test/acceptance/db.ExtTrace.js @@ -92,10 +92,15 @@ describeExtTrace('db', function () { if (mockExtTrace) { db.end.bind(db)(done); } else { + this.timeout(15000); // Real external trace, stop the web request request.end(); - server.close(); - db.end.bind(db)(done); + // When using real external trace, give some time for the trace backend + // to log results + setTimeout(function () { + server.close(); + db.end.bind(db)(done); + }, 10000); } }); // If the db is undefined, we will skip the tests