@sentry/node
Version:
Sentry Node SDK using OpenTelemetry for performance instrumentation
435 lines (432 loc) • 16.6 kB
JavaScript
import { InstrumentationBase, semconvStabilityFromStr, InstrumentationNodeModuleFile, InstrumentationNodeModuleDefinition, isWrapped, SemconvStability, safeExecuteInTheMiddle } from '@opentelemetry/instrumentation';
import { ValueType, SpanKind, trace, context, SpanStatusCode } from '@opentelemetry/api';
import { EVENT_LISTENERS_SET } from './internal-types.js';
import { shouldSkipInstrumentation, getSemanticAttributesFromConnection, patchClientConnectCallback, isObjectWithTextString, parseNormalizedOperationName, handleConfigQuery, patchCallback, sanitizedErrorMessage, getErrorMessage, handleExecutionResult, getPoolName, updateCounter, getSemanticAttributesFromPoolConnection, patchCallbackPGPool } from './utils.js';
import { addSqlCommenterComment } from '../../utils/sql-common.js';
import { SDK_VERSION } from '@sentry/core';
import { SpanNames } from './enums/SpanNames.js';
import { hrTimeToMilliseconds, hrTimeDuration, hrTime } from '@opentelemetry/core';
import { METRIC_DB_CLIENT_OPERATION_DURATION, ATTR_DB_NAMESPACE, ATTR_ERROR_TYPE, ATTR_SERVER_PORT, ATTR_SERVER_ADDRESS, ATTR_DB_OPERATION_NAME, ATTR_DB_SYSTEM_NAME } from '@opentelemetry/semantic-conventions';
import { METRIC_DB_CLIENT_CONNECTION_COUNT, METRIC_DB_CLIENT_CONNECTION_PENDING_REQUESTS, ATTR_DB_SYSTEM, DB_SYSTEM_VALUE_POSTGRESQL } from './semconv.js';
const PACKAGE_NAME = "@sentry/instrumentation-pg";
function extractModuleExports(module) {
return module[Symbol.toStringTag] === "Module" ? module.default : module;
}
class PgInstrumentation extends InstrumentationBase {
constructor(config = {}) {
super(PACKAGE_NAME, SDK_VERSION, config);
// Pool events connect, acquire, release and remove can be called
// multiple times without changing the values of total, idle and waiting
// connections. The _connectionsCounter is used to keep track of latest
// values and only update the metrics _connectionsCount and _connectionPendingRequests
// when the value change.
this._connectionsCounter = {
used: 0,
idle: 0,
pending: 0
};
this._semconvStability = semconvStabilityFromStr("database", process.env.OTEL_SEMCONV_STABILITY_OPT_IN);
}
_updateMetricInstruments() {
this._operationDuration = this.meter.createHistogram(METRIC_DB_CLIENT_OPERATION_DURATION, {
description: "Duration of database client operations.",
unit: "s",
valueType: ValueType.DOUBLE,
advice: {
explicitBucketBoundaries: [1e-3, 5e-3, 0.01, 0.05, 0.1, 0.5, 1, 5, 10]
}
});
this._connectionsCounter = {
idle: 0,
pending: 0,
used: 0
};
this._connectionsCount = this.meter.createUpDownCounter(METRIC_DB_CLIENT_CONNECTION_COUNT, {
description: "The number of connections that are currently in state described by the state attribute.",
unit: "{connection}"
});
this._connectionPendingRequests = this.meter.createUpDownCounter(METRIC_DB_CLIENT_CONNECTION_PENDING_REQUESTS, {
description: "The number of current pending requests for an open connection.",
unit: "{connection}"
});
}
init() {
const SUPPORTED_PG_VERSIONS = [">=8.0.3 <9"];
const SUPPORTED_PG_POOL_VERSIONS = [">=2.0.0 <4"];
const modulePgNativeClient = new InstrumentationNodeModuleFile(
"pg/lib/native/client.js",
SUPPORTED_PG_VERSIONS,
this._patchPgClient.bind(this),
this._unpatchPgClient.bind(this)
);
const modulePgClient = new InstrumentationNodeModuleFile(
"pg/lib/client.js",
SUPPORTED_PG_VERSIONS,
this._patchPgClient.bind(this),
this._unpatchPgClient.bind(this)
);
const modulePG = new InstrumentationNodeModuleDefinition(
"pg",
SUPPORTED_PG_VERSIONS,
(module) => {
const moduleExports = extractModuleExports(module);
this._patchPgClient(moduleExports.Client);
return module;
},
(module) => {
const moduleExports = extractModuleExports(module);
this._unpatchPgClient(moduleExports.Client);
return module;
},
[modulePgClient, modulePgNativeClient]
);
const modulePGPool = new InstrumentationNodeModuleDefinition(
"pg-pool",
SUPPORTED_PG_POOL_VERSIONS,
(module) => {
const moduleExports = extractModuleExports(module);
if (isWrapped(moduleExports.prototype.connect)) {
this._unwrap(moduleExports.prototype, "connect");
}
this._wrap(moduleExports.prototype, "connect", this._getPoolConnectPatch());
return moduleExports;
},
(module) => {
const moduleExports = extractModuleExports(module);
if (isWrapped(moduleExports.prototype.connect)) {
this._unwrap(moduleExports.prototype, "connect");
}
}
);
return [modulePG, modulePGPool];
}
_patchPgClient(module) {
if (!module) {
return;
}
const moduleExports = extractModuleExports(module);
if (isWrapped(moduleExports.prototype.query)) {
this._unwrap(moduleExports.prototype, "query");
}
if (isWrapped(moduleExports.prototype.connect)) {
this._unwrap(moduleExports.prototype, "connect");
}
this._wrap(moduleExports.prototype, "query", this._getClientQueryPatch());
this._wrap(moduleExports.prototype, "connect", this._getClientConnectPatch());
return module;
}
_unpatchPgClient(module) {
const moduleExports = extractModuleExports(module);
if (isWrapped(moduleExports.prototype.query)) {
this._unwrap(moduleExports.prototype, "query");
}
if (isWrapped(moduleExports.prototype.connect)) {
this._unwrap(moduleExports.prototype, "connect");
}
return module;
}
_getClientConnectPatch() {
const plugin = this;
return (original) => {
return function connect(callback) {
const config = plugin.getConfig();
if (shouldSkipInstrumentation(config) || config.ignoreConnectSpans) {
return original.call(this, callback);
}
const span = plugin.tracer.startSpan(SpanNames.CONNECT, {
kind: SpanKind.CLIENT,
attributes: getSemanticAttributesFromConnection(this, plugin._semconvStability)
});
if (callback) {
const parentSpan = trace.getSpan(context.active());
callback = patchClientConnectCallback(span, callback);
if (parentSpan) {
callback = context.bind(context.active(), callback);
}
}
const connectResult = context.with(trace.setSpan(context.active(), span), () => {
return original.call(this, callback);
});
return handleConnectResult(span, connectResult);
};
};
}
recordOperationDuration(attributes, startTime) {
const metricsAttributes = {};
const keysToCopy = [
ATTR_DB_NAMESPACE,
ATTR_ERROR_TYPE,
ATTR_SERVER_PORT,
ATTR_SERVER_ADDRESS,
ATTR_DB_OPERATION_NAME
];
if (this._semconvStability & SemconvStability.OLD) {
keysToCopy.push(ATTR_DB_SYSTEM);
}
if (this._semconvStability & SemconvStability.STABLE) {
keysToCopy.push(ATTR_DB_SYSTEM_NAME);
}
keysToCopy.forEach((key) => {
if (key in attributes) {
metricsAttributes[key] = attributes[key];
}
});
const durationSeconds = hrTimeToMilliseconds(hrTimeDuration(startTime, hrTime())) / 1e3;
this._operationDuration.record(durationSeconds, metricsAttributes);
}
_getClientQueryPatch() {
const plugin = this;
return (original) => {
this._diag.debug("Patching pg.Client.prototype.query");
return function query(...args) {
if (shouldSkipInstrumentation(plugin.getConfig())) {
return original.apply(this, args);
}
const startTime = hrTime();
const arg0 = args[0];
const firstArgIsString = typeof arg0 === "string";
const firstArgIsQueryObjectWithText = isObjectWithTextString(arg0);
const queryConfig = firstArgIsString ? {
text: arg0,
values: Array.isArray(args[1]) ? args[1] : void 0
} : firstArgIsQueryObjectWithText ? {
...arg0,
name: arg0.name,
text: arg0.text,
values: arg0.values ?? (Array.isArray(args[1]) ? args[1] : void 0)
} : void 0;
const attributes = {
[ATTR_DB_SYSTEM]: DB_SYSTEM_VALUE_POSTGRESQL,
[ATTR_DB_NAMESPACE]: this.database,
[ATTR_SERVER_PORT]: this.connectionParameters.port,
[ATTR_SERVER_ADDRESS]: this.connectionParameters.host
};
if (queryConfig?.text) {
attributes[ATTR_DB_OPERATION_NAME] = parseNormalizedOperationName(queryConfig?.text);
}
const recordDuration = () => {
plugin.recordOperationDuration(attributes, startTime);
};
const instrumentationConfig = plugin.getConfig();
const span = handleConfigQuery.call(
this,
plugin.tracer,
instrumentationConfig,
plugin._semconvStability,
queryConfig
);
if (instrumentationConfig.addSqlCommenterCommentToQueries) {
if (firstArgIsString) {
args[0] = addSqlCommenterComment(span, arg0);
} else if (firstArgIsQueryObjectWithText && !("name" in arg0)) {
args[0] = {
...arg0,
text: addSqlCommenterComment(span, arg0.text)
};
}
}
if (args.length > 0) {
const parentSpan = trace.getSpan(context.active());
if (typeof args[args.length - 1] === "function") {
args[args.length - 1] = patchCallback(
instrumentationConfig,
span,
args[args.length - 1],
// nb: not type safe.
attributes,
recordDuration
);
if (parentSpan) {
args[args.length - 1] = context.bind(context.active(), args[args.length - 1]);
}
} else if (typeof queryConfig?.callback === "function") {
let callback = patchCallback(
plugin.getConfig(),
span,
queryConfig.callback,
// nb: not type safe.
attributes,
recordDuration
);
if (parentSpan) {
callback = context.bind(context.active(), callback);
}
args[0].callback = callback;
}
}
const { requestHook } = instrumentationConfig;
if (typeof requestHook === "function" && queryConfig) {
safeExecuteInTheMiddle(
() => {
const { database, host, port, user } = this.connectionParameters;
const connection = { database, host, port, user };
requestHook(span, {
connection,
query: {
text: queryConfig.text,
// nb: if `client.query` is called with illegal arguments
// (e.g., if `queryConfig.values` is passed explicitly, but a
// non-array is given), then the type casts will be wrong. But
// we leave it up to the queryHook to handle that, and we
// catch and swallow any errors it throws. The other options
// are all worse. E.g., we could leave `queryConfig.values`
// and `queryConfig.name` as `unknown`, but then the hook body
// would be forced to validate (or cast) them before using
// them, which seems incredibly cumbersome given that these
// casts will be correct 99.9% of the time -- and pg.query
// will immediately throw during development in the other .1%
// of cases. Alternatively, we could simply skip calling the
// hook when `values` or `name` don't have the expected type,
// but that would add unnecessary validation overhead to every
// hook invocation and possibly be even more confusing/unexpected.
values: queryConfig.values,
name: queryConfig.name
}
});
},
(err) => {
if (err) {
plugin._diag.error("Error running query hook", err);
}
},
true
);
}
let result;
try {
result = original.apply(this, args);
} catch (e) {
if (e instanceof Error) {
span.recordException(sanitizedErrorMessage(e));
}
span.setStatus({
code: SpanStatusCode.ERROR,
message: getErrorMessage(e)
});
span.end();
throw e;
}
if (result instanceof Promise) {
return result.then((result2) => {
return new Promise((resolve) => {
handleExecutionResult(plugin.getConfig(), span, result2);
recordDuration();
span.end();
resolve(result2);
});
}).catch((error) => {
return new Promise((_, reject) => {
if (error instanceof Error) {
span.recordException(sanitizedErrorMessage(error));
}
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message
});
recordDuration();
span.end();
reject(error);
});
});
}
return result;
};
};
}
_setPoolConnectEventListeners(pgPool) {
if (pgPool[EVENT_LISTENERS_SET]) return;
const poolName = getPoolName(pgPool.options);
pgPool.on("connect", () => {
this._connectionsCounter = updateCounter(
poolName,
pgPool,
this._connectionsCount,
this._connectionPendingRequests,
this._connectionsCounter
);
});
pgPool.on("acquire", () => {
this._connectionsCounter = updateCounter(
poolName,
pgPool,
this._connectionsCount,
this._connectionPendingRequests,
this._connectionsCounter
);
});
pgPool.on("remove", () => {
this._connectionsCounter = updateCounter(
poolName,
pgPool,
this._connectionsCount,
this._connectionPendingRequests,
this._connectionsCounter
);
});
pgPool.on("release", () => {
this._connectionsCounter = updateCounter(
poolName,
pgPool,
this._connectionsCount,
this._connectionPendingRequests,
this._connectionsCounter
);
});
pgPool[EVENT_LISTENERS_SET] = true;
}
_getPoolConnectPatch() {
const plugin = this;
return (originalConnect) => {
return function connect(callback) {
const config = plugin.getConfig();
if (shouldSkipInstrumentation(config)) {
return originalConnect.call(this, callback);
}
plugin._setPoolConnectEventListeners(this);
if (config.ignoreConnectSpans) {
return originalConnect.call(this, callback);
}
const span = plugin.tracer.startSpan(SpanNames.POOL_CONNECT, {
kind: SpanKind.CLIENT,
attributes: getSemanticAttributesFromPoolConnection(this.options, plugin._semconvStability)
});
if (callback) {
const parentSpan = trace.getSpan(context.active());
callback = patchCallbackPGPool(span, callback);
if (parentSpan) {
callback = context.bind(context.active(), callback);
}
}
const connectResult = context.with(trace.setSpan(context.active(), span), () => {
return originalConnect.call(this, callback);
});
return handleConnectResult(span, connectResult);
};
};
}
}
function handleConnectResult(span, connectResult) {
if (!(connectResult instanceof Promise)) {
return connectResult;
}
const connectResultPromise = connectResult;
return context.bind(
context.active(),
connectResultPromise.then((result) => {
span.end();
return result;
}).catch((error) => {
if (error instanceof Error) {
span.recordException(sanitizedErrorMessage(error));
}
span.setStatus({
code: SpanStatusCode.ERROR,
message: getErrorMessage(error)
});
span.end();
return Promise.reject(error);
})
);
}
export { PgInstrumentation };
//# sourceMappingURL=instrumentation.js.map