@sentry/node
Version:
Sentry Node SDK using OpenTelemetry for performance instrumentation
153 lines (150 loc) • 6.48 kB
JavaScript
import { EventEmitter } from 'events';
import { InstrumentationBase, InstrumentationNodeModuleDefinition, isWrapped } from '@opentelemetry/instrumentation';
import { NET_PEER_PORT, NET_PEER_NAME, DB_USER, DB_STATEMENT, DB_NAME, DB_SYSTEM } from '@sentry/conventions/attributes';
import { ATTR_DB_SQL_TABLE, DB_SYSTEM_VALUE_MSSQL } from './semconv.js';
import { getSpanName, once } from './utils.js';
import { SDK_VERSION, startInactiveSpan, SPAN_KIND, withActiveSpan, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR } from '@sentry/core';
const PACKAGE_NAME = "@sentry/instrumentation-tedious";
const CURRENT_DATABASE = /* @__PURE__ */ Symbol("opentelemetry.instrumentation-tedious.current-database");
const PATCHED_METHODS = ["callProcedure", "execSql", "execSqlBatch", "execBulkLoad", "prepare", "execute"];
function setDatabase(databaseName) {
Object.defineProperty(this, CURRENT_DATABASE, {
value: databaseName,
writable: true
});
}
const _TediousInstrumentation = class _TediousInstrumentation extends InstrumentationBase {
constructor(config = {}) {
super(PACKAGE_NAME, SDK_VERSION, config);
}
init() {
return [
new InstrumentationNodeModuleDefinition(
_TediousInstrumentation.COMPONENT,
[">=1.11.0 <20"],
(moduleExports) => {
const ConnectionPrototype = moduleExports.Connection.prototype;
for (const method of PATCHED_METHODS) {
if (isWrapped(ConnectionPrototype[method])) {
this._unwrap(ConnectionPrototype, method);
}
this._wrap(ConnectionPrototype, method, this._patchQuery(method));
}
if (isWrapped(ConnectionPrototype.connect)) {
this._unwrap(ConnectionPrototype, "connect");
}
this._wrap(ConnectionPrototype, "connect", this._patchConnect);
return moduleExports;
},
(moduleExports) => {
if (moduleExports === void 0) return;
const ConnectionPrototype = moduleExports.Connection.prototype;
for (const method of PATCHED_METHODS) {
this._unwrap(ConnectionPrototype, method);
}
this._unwrap(ConnectionPrototype, "connect");
}
)
];
}
_patchConnect(original) {
return function patchedConnect() {
setDatabase.call(this, this.config?.options?.database);
this.removeListener("databaseChange", setDatabase);
this.on("databaseChange", setDatabase);
this.once("end", () => {
this.removeListener("databaseChange", setDatabase);
});
return original.apply(this, arguments);
};
}
_patchQuery(operation) {
return (originalMethod) => {
const thisPlugin = this;
function patchedMethod(request) {
if (!(request instanceof EventEmitter)) {
thisPlugin._diag.warn(`Unexpected invocation of patched ${operation} method. Span not recorded`);
return originalMethod.apply(this, arguments);
}
let procCount = 0;
let statementCount = 0;
const incrementStatementCount = () => statementCount++;
const incrementProcCount = () => procCount++;
const databaseName = this[CURRENT_DATABASE];
const sql = ((request2) => {
if (request2.sqlTextOrProcedure === "sp_prepare" && request2.parametersByName?.stmt?.value) {
return request2.parametersByName.stmt.value;
}
return request2.sqlTextOrProcedure;
})(request);
const attributes = {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: "auto.db.otel.tedious",
// eslint-disable-next-line typescript/no-deprecated
[DB_SYSTEM]: DB_SYSTEM_VALUE_MSSQL,
// eslint-disable-next-line typescript/no-deprecated
[DB_NAME]: databaseName,
// >=4 uses `authentication` object; older versions just userName and password pair
// eslint-disable-next-line typescript/no-deprecated
[DB_USER]: this.config?.userName ?? this.config?.authentication?.options?.userName,
// eslint-disable-next-line typescript/no-deprecated
[DB_STATEMENT]: sql,
// eslint-disable-next-line typescript/no-deprecated
[ATTR_DB_SQL_TABLE]: request.table,
// eslint-disable-next-line typescript/no-deprecated
[NET_PEER_NAME]: this.config?.server,
// eslint-disable-next-line typescript/no-deprecated
[NET_PEER_PORT]: this.config?.options?.port
};
const span = startInactiveSpan({
name: getSpanName(operation, databaseName, sql, request.table),
kind: SPAN_KIND.CLIENT,
attributes
});
const endSpan = once((err) => {
request.removeListener("done", incrementStatementCount);
request.removeListener("doneInProc", incrementStatementCount);
request.removeListener("doneProc", incrementProcCount);
request.removeListener("error", endSpan);
this.removeListener("end", endSpan);
span.setAttribute("tedious.procedure_count", procCount);
span.setAttribute("tedious.statement_count", statementCount);
if (err) {
span.setStatus({
code: SPAN_STATUS_ERROR,
message: err.message
});
}
span.end();
});
request.on("done", incrementStatementCount);
request.on("doneInProc", incrementStatementCount);
request.on("doneProc", incrementProcCount);
request.once("error", endSpan);
this.on("end", endSpan);
if (typeof request.callback === "function") {
thisPlugin._wrap(request, "callback", thisPlugin._patchCallbackQuery(endSpan));
} else {
thisPlugin._diag.error("Expected request.callback to be a function");
}
return withActiveSpan(span, () => originalMethod.apply(this, arguments));
}
Object.defineProperty(patchedMethod, "length", {
value: originalMethod.length,
writable: false
});
return patchedMethod;
};
}
_patchCallbackQuery(endSpan) {
return (originalCallback) => {
return function(err, _rowCount, _rows) {
endSpan(err);
return originalCallback.apply(this, arguments);
};
};
}
};
_TediousInstrumentation.COMPONENT = "tedious";
let TediousInstrumentation = _TediousInstrumentation;
export { TediousInstrumentation };
//# sourceMappingURL=instrumentation.js.map