@sap/hana-client
Version:
Official SAP HANA Node.js Driver
360 lines (338 loc) • 15.9 kB
JavaScript
// The @sap/hana-client driver will automatically
// use this to add OpenTelemetry support when @opentelemetry/api
// is already installed.
var OTel = {};
try {
// @opentelemetry/api must be installed by the application in order for
// the client to use it.
OTel.API = require('@opentelemetry/api');
OTel.Tracer = OTel.API.trace.getTracer('@sap/hana-client', '2.25.31');
// 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
// console.log("Failed to load @opentelemetry/api: ", err); // for debugging
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 attempt to follow:
// https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/database-spans.md
// (above page was updated May 2, 2025 I think to version v1.33.0 to mark most DB attributes as stable)
// The specs says the span name could be <db.operation.name> <db.collection.name>
// but instead follow CAP and postgresql which roughly use "<operation> - <sql>"
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.name' : 'sap.hana',
'db.operation.name': op, // the method name, which is roughly the operation being executed
'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
// TODO: this should be sanitized to remove literal strings, numbers, etc.
var sql_text = sql.length > 1000 ? sql.substring(0, 999) + '…' : sql;
spanOptions.attributes['db.query.text'] = sql_text;
}
if(conn._destinationInfo.tenant) {
spanOptions.attributes['db.namespace'] = 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);
const results = args[0];
if(results !== undefined) {
span.setAttribute('db.response.returned_rows', (results && results.length) || results);
}
span.end();
if(cb) {
// 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) {
const resultSet = args[0];
_setSpanStatus(span, err);
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) {
span.setAttribute('db.response.returned_rows', rowCount);
}
// modify resultSet for OpenTelemetry after a successful executeQuery
// async methods that do not trace to OpenTelemetry
_setPropagateContextWrapper(resultSet, resultSet.close, "close");
_setPropagateContextWrapper(resultSet, resultSet.getData, "getData");
_setPropagateContextWrapper(resultSet, resultSet.getValue, "getValue");
_setPropagateContextWrapper(resultSet, resultSet.getValues, "getValues");
_setPropagateContextWrapper(resultSet, resultSet.next, "next");
_setPropagateContextWrapper(resultSet, resultSet.nextResult, "nextResult");
}
span.end();
if(cb) {
// 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.getData)
// 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];
}
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 {
// sync method call
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 can end with a callback
return function (...args) {
var cb;
var activeCtx;
if (args.length > 0 && typeof args[args.length - 1] === 'function') {
cb = args[args.length - 1];
activeCtx = OTel.API.context.active();
}
const {spanName, spanOptions} = _getSpanNameAndStatus(op, undefined, conn);
return OTel.Tracer.startActiveSpan(spanName, spanOptions, function(span) {
if(cb) {
// async method call
return origFn.call(thisArg, ...args.slice(0, args.length - 1), function (...cbArgs) {
// if cbArgs is empty, cbArbs[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);
});
});
} else {
// sync method call
var result;
try {
result = origFn.call(thisArg, ...args);
_setSpanStatus(span);
span.end();
} catch (err) {
_setSpanStatus(span, err);
span.end();
throw err;
}
return result;
}
});
}
}
// wrapper for execute, executeBatch, and executeQuery
function _executeWrapperFn(thisArg, conn, execFn, op, resultCB, sql) {
// connection exec args = [sql, options, callback] --> options and callback is optional
// stmt exec args = [options, callback] --> options and callback 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;
if (args.length > 0 && typeof args[args.length - 1] === 'function') {
cb = args[args.length - 1];
activeCtx = OTel.API.context.active();
}
const {spanName, spanOptions} = _getSpanNameAndStatus(op, sql, conn);
return OTel.Tracer.startActiveSpan(spanName, spanOptions, function(span) {
if(cb) {
// async execute
return execFn.call(thisArg, ...args.slice(0, args.length - 1), resultCB(span, activeCtx, cb));
} else {
// sync execute
var result;
try {
result = execFn.call(thisArg, ...args);
resultCB(span)(undefined, result);
} catch (err) {
_setSpanStatus(span, err);
span.end();
throw err;
}
return result;
}
});
}
}
// modify stmt for OpenTelemetry after a successful prepare
function _modifyStmt(stmt, conn, sql) {
const originalExecFn = stmt.exec;
stmt.exec = _executeWrapperFn(stmt, conn, originalExecFn, "execute", _openTelemetryResultCallback, sql);
stmt.execute = stmt.exec;
const originalExecBatchFn = stmt.execBatch;
stmt.execBatch = _executeWrapperFn(stmt, conn, originalExecBatchFn, "executeBatch", _openTelemetryResultCallback, sql);
stmt.executeBatch = stmt.execBatch;
const originalExecQueryFn = stmt.execQuery;
stmt.execQuery = _executeWrapperFn(stmt, conn, originalExecQueryFn, "executeQuery", _openTelemetryResultSetCallback, sql);
stmt.executeQuery = stmt.execQuery;
// async methods that do not trace to OpenTelemetry
_setPropagateContextWrapper(stmt, stmt.drop, "drop");
_setPropagateContextWrapper(stmt, stmt.getData, "getData");
_setPropagateContextWrapper(stmt, stmt.getParameterValue, "getParameterValue");
_setPropagateContextWrapper(stmt, stmt.sendParameterData, "sendParameterData");
}
function _prepareWrapperFn(conn, prepareFn) {
// args = [sql, options, callback] --> options is optional
return function (...args) {
var cb;
var activeCtx;
if(args.length > 0 && typeof args[args.length - 1] === 'function') {
cb = args[args.length - 1];
activeCtx = OTel.API.context.active();
}
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) {
if(cb) {
// async prepare
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);
}
});
});
} else {
// sync prepare
try {
var stmt = prepareFn.call(conn, ...args);
_setSpanStatus(span);
span.end();
_modifyStmt(stmt, conn, sql);
return stmt;
} catch (err) {
_setSpanStatus(span, err);
span.end();
throw err;
}
}
});
}
}
// 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;
// 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, "execute", _openTelemetryResultCallback);
conn.execute = conn.exec;
const originalPrepareFn = conn.prepare;
// Must use defineProperty and not just conn.<method> = ... for N-API methods.
// (The execute methods are set to non-N-API JavaScript methods by
// index.js dynamicallyAddConnExecMethod)
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.abort, "abort");
_setPropagateContextWrapper(conn, conn.cancel, "cancel");
_setPropagateContextWrapper(conn, conn.clean, "clean");
_setPropagateContextWrapper(conn, conn.clearPool, "clearPool");
_setPropagateContextWrapper(conn, conn.disconnect, "disconnect");
_setPropagateContextWrapper(conn, conn.close, "close");
_setPropagateContextWrapper(conn, conn.end, "end");
_setPropagateContextWrapper(conn, conn.switchUser, "switchUser");
return conn;
}
module.exports = { openTelemetryConnection, isOpenTelemetryEnabled };