UNPKG

@sentry/node

Version:

Sentry Node SDK using OpenTelemetry for performance instrumentation

279 lines (239 loc) 9.6 kB
import { trace, context } from '@opentelemetry/api'; import { InstrumentationBase, InstrumentationNodeModuleDefinition, InstrumentationNodeModuleFile, safeExecuteInTheMiddle } from '@opentelemetry/instrumentation'; import { ATTR_DB_RESPONSE_STATUS_CODE, ATTR_ERROR_TYPE, ATTR_DB_OPERATION_NAME, ATTR_DB_SYSTEM_NAME, ATTR_DB_NAMESPACE, ATTR_SERVER_ADDRESS, ATTR_SERVER_PORT, ATTR_DB_QUERY_TEXT } from '@opentelemetry/semantic-conventions'; import { SDK_VERSION, SPAN_STATUS_ERROR, startSpanManual, getCurrentScope, debug, defineIntegration } from '@sentry/core'; import { generateInstrumentOnce, addOriginToSpan } from '@sentry/node-core'; // Instrumentation for https://github.com/porsager/postgres const INTEGRATION_NAME = 'PostgresJs'; const SUPPORTED_VERSIONS = ['>=3.0.0 <4']; const instrumentPostgresJs = generateInstrumentOnce( INTEGRATION_NAME, (options) => new PostgresJsInstrumentation({ requireParentSpan: options?.requireParentSpan ?? true, requestHook: options?.requestHook, }), ); /** * Instrumentation for the [postgres](https://www.npmjs.com/package/postgres) library. * This instrumentation captures postgresjs queries and their attributes, */ class PostgresJsInstrumentation extends InstrumentationBase { constructor(config) { super('sentry-postgres-js', SDK_VERSION, config); } /** * Initializes the instrumentation. */ init() { const instrumentationModule = new InstrumentationNodeModuleDefinition('postgres', SUPPORTED_VERSIONS); ['src', 'cf/src', 'cjs/src'].forEach(path => { instrumentationModule.files.push( new InstrumentationNodeModuleFile( `postgres/${path}/connection.js`, ['*'], this._patchConnection.bind(this), this._unwrap.bind(this), ), ); instrumentationModule.files.push( new InstrumentationNodeModuleFile( `postgres/${path}/query.js`, SUPPORTED_VERSIONS, this._patchQuery.bind(this), this._unwrap.bind(this), ), ); }); return [instrumentationModule]; } /** * Determines whether a span should be created based on the current context. * If `requireParentSpan` is set to true in the configuration, a span will * only be created if there is a parent span available. */ _shouldCreateSpans() { const config = this.getConfig(); const hasParentSpan = trace.getSpan(context.active()) !== undefined; return hasParentSpan || !config.requireParentSpan; } /** * Patches the reject method of the Query class to set the span status and end it */ _patchReject(rejectTarget, span) { return new Proxy(rejectTarget, { apply: ( rejectTarget, rejectThisArg, rejectArgs , ) => { span.setStatus({ code: SPAN_STATUS_ERROR, // This message is the error message from the rejectArgs, when available // e.g "relation 'User' does not exist" message: rejectArgs?.[0]?.message || 'unknown_error', }); const result = Reflect.apply(rejectTarget, rejectThisArg, rejectArgs); // This status code is PG error code, e.g. '42P01' for "relation does not exist" // https://www.postgresql.org/docs/current/errcodes-appendix.html span.setAttribute(ATTR_DB_RESPONSE_STATUS_CODE, rejectArgs?.[0]?.code || 'Unknown error'); // This is the error type, e.g. 'PostgresError' for a Postgres error span.setAttribute(ATTR_ERROR_TYPE, rejectArgs?.[0]?.name || 'Unknown error'); span.end(); return result; }, }); } /** * Patches the resolve method of the Query class to end the span when the query is resolved. */ _patchResolve(resolveTarget, span) { return new Proxy(resolveTarget, { apply: (resolveTarget, resolveThisArg, resolveArgs) => { const result = Reflect.apply(resolveTarget, resolveThisArg, resolveArgs); const sqlCommand = resolveArgs?.[0]?.command; if (sqlCommand) { // SQL command is only available when the query is resolved successfully span.setAttribute(ATTR_DB_OPERATION_NAME, sqlCommand); } span.end(); return result; }, }); } /** * Patches the Query class to instrument the handle method. */ _patchQuery(moduleExports ) { moduleExports.Query.prototype.handle = new Proxy(moduleExports.Query.prototype.handle, { apply: async ( handleTarget, handleThisArg , handleArgs, ) => { if (!this._shouldCreateSpans()) { // If we don't need to create spans, just call the original method return Reflect.apply(handleTarget, handleThisArg, handleArgs); } const sanitizedSqlQuery = this._sanitizeSqlQuery(handleThisArg.strings?.[0]); return startSpanManual( { name: sanitizedSqlQuery || 'postgresjs.query', op: 'db', }, (span) => { const scope = getCurrentScope(); const postgresConnectionContext = scope.getScopeData().contexts['postgresjsConnection'] ; addOriginToSpan(span, 'auto.db.otel.postgres'); const { requestHook } = this.getConfig(); if (requestHook) { safeExecuteInTheMiddle( () => requestHook(span, sanitizedSqlQuery, postgresConnectionContext), error => { if (error) { debug.error(`Error in requestHook for ${INTEGRATION_NAME} integration:`, error); } }, ); } // ATTR_DB_NAMESPACE is used to indicate the database name and the schema name // It's only the database name as we don't have the schema information const databaseName = postgresConnectionContext?.ATTR_DB_NAMESPACE || '<unknown database>'; const databaseHost = postgresConnectionContext?.ATTR_SERVER_ADDRESS || '<unknown host>'; const databasePort = postgresConnectionContext?.ATTR_SERVER_PORT || '<unknown port>'; span.setAttribute(ATTR_DB_SYSTEM_NAME, 'postgres'); span.setAttribute(ATTR_DB_NAMESPACE, databaseName); span.setAttribute(ATTR_SERVER_ADDRESS, databaseHost); span.setAttribute(ATTR_SERVER_PORT, databasePort); span.setAttribute(ATTR_DB_QUERY_TEXT, sanitizedSqlQuery); handleThisArg.resolve = this._patchResolve(handleThisArg.resolve, span); handleThisArg.reject = this._patchReject(handleThisArg.reject, span); try { return Reflect.apply(handleTarget, handleThisArg, handleArgs); } catch (error) { span.setStatus({ code: SPAN_STATUS_ERROR, }); span.end(); throw error; // Re-throw the error to propagate it } }, ); }, }); return moduleExports; } /** * Patches the Connection class to set the database, host, and port attributes * when a new connection is created. */ _patchConnection(Connection) { return new Proxy(Connection, { apply: (connectionTarget, thisArg, connectionArgs) => { const databaseName = connectionArgs[0]?.database || '<unknown database>'; const databaseHost = connectionArgs[0]?.host?.[0] || '<unknown host>'; const databasePort = connectionArgs[0]?.port?.[0] || '<unknown port>'; const scope = getCurrentScope(); scope.setContext('postgresjsConnection', { ATTR_DB_NAMESPACE: databaseName, ATTR_SERVER_ADDRESS: databaseHost, ATTR_SERVER_PORT: databasePort, }); return Reflect.apply(connectionTarget, thisArg, connectionArgs); }, }); } /** * Sanitize SQL query as per the OTEL semantic conventions * https://opentelemetry.io/docs/specs/semconv/database/database-spans/#sanitization-of-dbquerytext */ _sanitizeSqlQuery(sqlQuery) { if (!sqlQuery) { return 'Unknown SQL Query'; } return ( sqlQuery .replace(/\s+/g, ' ') .trim() // Remove extra spaces including newlines and trim .substring(0, 1024) // Truncate to 1024 characters .replace(/--.*?(\r?\n|$)/g, '') // Single line comments .replace(/\/\*[\s\S]*?\*\//g, '') // Multi-line comments .replace(/;\s*$/, '') // Remove trailing semicolons .replace(/\b\d+\b/g, '?') // Replace standalone numbers // Collapse whitespace to a single space .replace(/\s+/g, ' ') // Collapse IN and in clauses // eg. IN (?, ?, ?, ?) to IN (?) .replace(/\bIN\b\s*\(\s*\?(?:\s*,\s*\?)*\s*\)/g, 'IN (?)') ); } } const _postgresJsIntegration = (() => { return { name: INTEGRATION_NAME, setupOnce() { instrumentPostgresJs(); }, }; }) ; /** * Adds Sentry tracing instrumentation for the [postgres](https://www.npmjs.com/package/postgres) library. * * For more information, see the [`postgresIntegration` documentation](https://docs.sentry.io/platforms/javascript/guides/node/configuration/integrations/postgres/). * * @example * ```javascript * const Sentry = require('@sentry/node'); * * Sentry.init({ * integrations: [Sentry.postgresJsIntegration()], * }); * ``` */ const postgresJsIntegration = defineIntegration(_postgresJsIntegration); export { PostgresJsInstrumentation, instrumentPostgresJs, postgresJsIntegration }; //# sourceMappingURL=postgresjs.js.map