Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
181 changes: 181 additions & 0 deletions extension/Dynatrace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// 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)
&& !Array.isArray(resultSet)) {
resultSet = args[1];
}

if (err) {
tracer.error(err);
} 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,
// this happens if the result set is larger than 32 rows)
if(rowCount >= 0) {
tracer.setResultData({rowsReturned: rowCount});
}
} else if (resultSet !== undefined) {
tracer.setResultData({
rowsReturned: (resultSet && resultSet.length) || resultSet
});
}
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];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be an args.length > 0 check here, similar to _ExecuteWrapperFn. This also applies to hana-client and Ian McH will fix it there

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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment isn't relevant here, should just remove it

// 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 };
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
27 changes: 26 additions & 1 deletion lib/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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];
Expand All @@ -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);
}
}
5 changes: 4 additions & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,7 @@ exports.createJSONStringifier = function createJSONStringifier() {
seperator: ',',
stringify: JSON.stringify
});
};
};

// Dynatrace support should not change unless there are source code modifications
exports.isDynatraceSupported = true;
21 changes: 21 additions & 0 deletions lib/protocol/ResultSet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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);
}

Expand All @@ -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"');
Expand Down
33 changes: 33 additions & 0 deletions lib/util/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Loading