UNPKG

@opentelemetry/instrumentation-pg

Version:
396 lines 20.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PgInstrumentation = void 0; /* * Copyright The OpenTelemetry Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const instrumentation_1 = require("@opentelemetry/instrumentation"); const api_1 = require("@opentelemetry/api"); const internal_types_1 = require("./internal-types"); const utils = require("./utils"); const sql_common_1 = require("@opentelemetry/sql-common"); /** @knipignore */ const version_1 = require("./version"); const SpanNames_1 = require("./enums/SpanNames"); const core_1 = require("@opentelemetry/core"); const semantic_conventions_1 = require("@opentelemetry/semantic-conventions"); const semconv_1 = require("./semconv"); function extractModuleExports(module) { return module[Symbol.toStringTag] === 'Module' ? module.default // ESM : module; // CommonJS } class PgInstrumentation extends instrumentation_1.InstrumentationBase { // 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. _connectionsCounter = { used: 0, idle: 0, pending: 0, }; constructor(config = {}) { super(version_1.PACKAGE_NAME, version_1.PACKAGE_VERSION, config); } _updateMetricInstruments() { this._operationDuration = this.meter.createHistogram(semconv_1.METRIC_DB_CLIENT_OPERATION_DURATION, { description: 'Duration of database client operations.', unit: 's', valueType: api_1.ValueType.DOUBLE, advice: { explicitBucketBoundaries: [ 0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10, ], }, }); this._connectionsCounter = { idle: 0, pending: 0, used: 0, }; this._connectionsCount = this.meter.createUpDownCounter(semconv_1.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(semconv_1.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 modulePgNativeClient = new instrumentation_1.InstrumentationNodeModuleFile('pg/lib/native/client.js', SUPPORTED_PG_VERSIONS, this._patchPgClient.bind(this), this._unpatchPgClient.bind(this)); const modulePgClient = new instrumentation_1.InstrumentationNodeModuleFile('pg/lib/client.js', SUPPORTED_PG_VERSIONS, this._patchPgClient.bind(this), this._unpatchPgClient.bind(this)); const modulePG = new instrumentation_1.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 instrumentation_1.InstrumentationNodeModuleDefinition('pg-pool', ['>=2.0.0 <4'], (moduleExports) => { if ((0, instrumentation_1.isWrapped)(moduleExports.prototype.connect)) { this._unwrap(moduleExports.prototype, 'connect'); } this._wrap(moduleExports.prototype, 'connect', this._getPoolConnectPatch()); return moduleExports; }, (moduleExports) => { if ((0, instrumentation_1.isWrapped)(moduleExports.prototype.connect)) { this._unwrap(moduleExports.prototype, 'connect'); } }); return [modulePG, modulePGPool]; } _patchPgClient(module) { if (!module) { return; } const moduleExports = extractModuleExports(module); if ((0, instrumentation_1.isWrapped)(moduleExports.prototype.query)) { this._unwrap(moduleExports.prototype, 'query'); } if ((0, instrumentation_1.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 ((0, instrumentation_1.isWrapped)(moduleExports.prototype.query)) { this._unwrap(moduleExports.prototype, 'query'); } if ((0, instrumentation_1.isWrapped)(moduleExports.prototype.connect)) { this._unwrap(moduleExports.prototype, 'connect'); } return module; } _getClientConnectPatch() { const plugin = this; return (original) => { return function connect(callback) { if (utils.shouldSkipInstrumentation(plugin.getConfig())) { return original.call(this, callback); } const span = plugin.tracer.startSpan(SpanNames_1.SpanNames.CONNECT, { kind: api_1.SpanKind.CLIENT, attributes: utils.getSemanticAttributesFromConnection(this), }); if (callback) { const parentSpan = api_1.trace.getSpan(api_1.context.active()); callback = utils.patchClientConnectCallback(span, callback); if (parentSpan) { callback = api_1.context.bind(api_1.context.active(), callback); } } const connectResult = api_1.context.with(api_1.trace.setSpan(api_1.context.active(), span), () => { return original.call(this, callback); }); return handleConnectResult(span, connectResult); }; }; } recordOperationDuration(attributes, startTime) { const metricsAttributes = {}; const keysToCopy = [ semantic_conventions_1.SEMATTRS_DB_SYSTEM, semconv_1.ATTR_DB_NAMESPACE, semantic_conventions_1.ATTR_ERROR_TYPE, semantic_conventions_1.ATTR_SERVER_PORT, semantic_conventions_1.ATTR_SERVER_ADDRESS, semconv_1.ATTR_DB_OPERATION_NAME, ]; keysToCopy.forEach(key => { if (key in attributes) { metricsAttributes[key] = attributes[key]; } }); const durationSeconds = (0, core_1.hrTimeToMilliseconds)((0, core_1.hrTimeDuration)(startTime, (0, core_1.hrTime)())) / 1000; this._operationDuration.record(durationSeconds, metricsAttributes); } _getClientQueryPatch() { const plugin = this; return (original) => { this._diag.debug('Patching pg.Client.prototype.query'); return function query(...args) { if (utils.shouldSkipInstrumentation(plugin.getConfig())) { return original.apply(this, args); } const startTime = (0, core_1.hrTime)(); // client.query(text, cb?), client.query(text, values, cb?), and // client.query(configObj, cb?) are all valid signatures. We construct // a queryConfig obj from all (valid) signatures to build the span in a // unified way. We verify that we at least have query text, and code // defensively when dealing with `queryConfig` after that (to handle all // the other invalid cases, like a non-array for values being provided). // The type casts here reflect only what we've actually validated. const arg0 = args[0]; const firstArgIsString = typeof arg0 === 'string'; const firstArgIsQueryObjectWithText = utils.isObjectWithTextString(arg0); // TODO: remove the `as ...` casts below when the TS version is upgraded. // Newer TS versions will use the result of firstArgIsQueryObjectWithText // to properly narrow arg0, but TS 4.3.5 does not. const queryConfig = firstArgIsString ? { text: arg0, values: Array.isArray(args[1]) ? args[1] : undefined, } : firstArgIsQueryObjectWithText ? arg0 : undefined; const attributes = { [semantic_conventions_1.SEMATTRS_DB_SYSTEM]: semantic_conventions_1.DBSYSTEMVALUES_POSTGRESQL, [semconv_1.ATTR_DB_NAMESPACE]: this.database, [semantic_conventions_1.ATTR_SERVER_PORT]: this.connectionParameters.port, [semantic_conventions_1.ATTR_SERVER_ADDRESS]: this.connectionParameters.host, }; if (queryConfig?.text) { attributes[semconv_1.ATTR_DB_OPERATION_NAME] = utils.parseNormalizedOperationName(queryConfig?.text); } const recordDuration = () => { plugin.recordOperationDuration(attributes, startTime); }; const instrumentationConfig = plugin.getConfig(); const span = utils.handleConfigQuery.call(this, plugin.tracer, instrumentationConfig, queryConfig); // Modify query text w/ a tracing comment before invoking original for // tracing, but only if args[0] has one of our expected shapes. if (instrumentationConfig.addSqlCommenterCommentToQueries) { if (firstArgIsString) { args[0] = (0, sql_common_1.addSqlCommenterComment)(span, arg0); } else if (firstArgIsQueryObjectWithText && !('name' in arg0)) { // In the case of a query object, we need to ensure there's no name field // as this indicates a prepared query, where the comment would remain the same // for every invocation and contain an outdated trace context. args[0] = { ...arg0, text: (0, sql_common_1.addSqlCommenterComment)(span, arg0.text), }; } } // Bind callback (if any) to parent span (if any) if (args.length > 0) { const parentSpan = api_1.trace.getSpan(api_1.context.active()); if (typeof args[args.length - 1] === 'function') { // Patch ParameterQuery callback args[args.length - 1] = utils.patchCallback(instrumentationConfig, span, args[args.length - 1], // nb: not type safe. attributes, recordDuration); // If a parent span exists, bind the callback if (parentSpan) { args[args.length - 1] = api_1.context.bind(api_1.context.active(), args[args.length - 1]); } } else if (typeof queryConfig?.callback === 'function') { // Patch ConfigQuery callback let callback = utils.patchCallback(plugin.getConfig(), span, queryConfig.callback, // nb: not type safe. attributes, recordDuration); // If a parent span existed, bind the callback if (parentSpan) { callback = api_1.context.bind(api_1.context.active(), callback); } args[0].callback = callback; } } const { requestHook } = instrumentationConfig; if (typeof requestHook === 'function' && queryConfig) { (0, instrumentation_1.safeExecuteInTheMiddle)(() => { // pick keys to expose explicitly, so we're not leaking pg package // internals that are subject to change 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) { span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: utils.getErrorMessage(e), }); span.end(); throw e; } // Bind promise to parent span and end the span if (result instanceof Promise) { return result .then((result) => { // Return a pass-along promise which ends the span and then goes to user's orig resolvers return new Promise(resolve => { utils.handleExecutionResult(plugin.getConfig(), span, result); recordDuration(); span.end(); resolve(result); }); }) .catch((error) => { return new Promise((_, reject) => { span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error.message, }); recordDuration(); span.end(); reject(error); }); }); } // else returns void return result; // void }; }; } _setPoolConnectEventListeners(pgPool) { if (pgPool[internal_types_1.EVENT_LISTENERS_SET]) return; const poolName = utils.getPoolName(pgPool.options); pgPool.on('connect', () => { this._connectionsCounter = utils.updateCounter(poolName, pgPool, this._connectionsCount, this._connectionPendingRequests, this._connectionsCounter); }); pgPool.on('acquire', () => { this._connectionsCounter = utils.updateCounter(poolName, pgPool, this._connectionsCount, this._connectionPendingRequests, this._connectionsCounter); }); pgPool.on('remove', () => { this._connectionsCounter = utils.updateCounter(poolName, pgPool, this._connectionsCount, this._connectionPendingRequests, this._connectionsCounter); }); pgPool.on('release', () => { this._connectionsCounter = utils.updateCounter(poolName, pgPool, this._connectionsCount, this._connectionPendingRequests, this._connectionsCounter); }); pgPool[internal_types_1.EVENT_LISTENERS_SET] = true; } _getPoolConnectPatch() { const plugin = this; return (originalConnect) => { return function connect(callback) { if (utils.shouldSkipInstrumentation(plugin.getConfig())) { return originalConnect.call(this, callback); } // setup span const span = plugin.tracer.startSpan(SpanNames_1.SpanNames.POOL_CONNECT, { kind: api_1.SpanKind.CLIENT, attributes: utils.getSemanticAttributesFromPool(this.options), }); plugin._setPoolConnectEventListeners(this); if (callback) { const parentSpan = api_1.trace.getSpan(api_1.context.active()); callback = utils.patchCallbackPGPool(span, callback); // If a parent span exists, bind the callback if (parentSpan) { callback = api_1.context.bind(api_1.context.active(), callback); } } const connectResult = api_1.context.with(api_1.trace.setSpan(api_1.context.active(), span), () => { return originalConnect.call(this, callback); }); return handleConnectResult(span, connectResult); }; }; } } exports.PgInstrumentation = PgInstrumentation; function handleConnectResult(span, connectResult) { if (!(connectResult instanceof Promise)) { return connectResult; } const connectResultPromise = connectResult; return api_1.context.bind(api_1.context.active(), connectResultPromise .then(result => { span.end(); return result; }) .catch((error) => { span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: utils.getErrorMessage(error), }); span.end(); return Promise.reject(error); })); } //# sourceMappingURL=instrumentation.js.map