UNPKG

@sap/hana-client

Version:

Official SAP HANA Node.js Driver

360 lines (338 loc) 15.9 kB
// 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 };