UNPKG

@opentelemetry/instrumentation-pg

Version:
295 lines 12.9 kB
"use strict"; /* * 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. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.isObjectWithTextString = exports.getErrorMessage = exports.patchClientConnectCallback = exports.patchCallbackPGPool = exports.updateCounter = exports.getPoolName = exports.patchCallback = exports.handleExecutionResult = exports.handleConfigQuery = exports.shouldSkipInstrumentation = exports.getSemanticAttributesFromPool = exports.getSemanticAttributesFromConnection = exports.getConnectionString = exports.parseAndMaskConnectionString = exports.parseNormalizedOperationName = exports.getQuerySpanName = void 0; const api_1 = require("@opentelemetry/api"); const AttributeNames_1 = require("./enums/AttributeNames"); const semantic_conventions_1 = require("@opentelemetry/semantic-conventions"); const semconv_1 = require("./semconv"); const instrumentation_1 = require("@opentelemetry/instrumentation"); const SpanNames_1 = require("./enums/SpanNames"); /** * Helper function to get a low cardinality span name from whatever info we have * about the query. * * This is tricky, because we don't have most of the information (table name, * operation name, etc) the spec recommends using to build a low-cardinality * value w/o parsing. So, we use db.name and assume that, if the query's a named * prepared statement, those `name` values will be low cardinality. If we don't * have a named prepared statement, we try to parse an operation (despite the * spec's warnings). * * @params dbName The name of the db against which this query is being issued, * which could be missing if no db name was given at the time that the * connection was established. * @params queryConfig Information we have about the query being issued, typed * to reflect only the validation we've actually done on the args to * `client.query()`. This will be undefined if `client.query()` was called * with invalid arguments. */ function getQuerySpanName(dbName, queryConfig) { // NB: when the query config is invalid, we omit the dbName too, so that // someone (or some tool) reading the span name doesn't misinterpret the // dbName as being a prepared statement or sql commit name. if (!queryConfig) return SpanNames_1.SpanNames.QUERY_PREFIX; // Either the name of a prepared statement; or an attempted parse // of the SQL command, normalized to uppercase; or unknown. const command = typeof queryConfig.name === 'string' && queryConfig.name ? queryConfig.name : parseNormalizedOperationName(queryConfig.text); return `${SpanNames_1.SpanNames.QUERY_PREFIX}:${command}${dbName ? ` ${dbName}` : ''}`; } exports.getQuerySpanName = getQuerySpanName; function parseNormalizedOperationName(queryText) { const indexOfFirstSpace = queryText.indexOf(' '); let sqlCommand = indexOfFirstSpace === -1 ? queryText : queryText.slice(0, indexOfFirstSpace); sqlCommand = sqlCommand.toUpperCase(); // Handle query text being "COMMIT;", which has an extra semicolon before the space. return sqlCommand.endsWith(';') ? sqlCommand.slice(0, -1) : sqlCommand; } exports.parseNormalizedOperationName = parseNormalizedOperationName; function parseAndMaskConnectionString(connectionString) { try { // Parse the connection string const url = new URL(connectionString); // Remove all auth information (username and password) url.username = ''; url.password = ''; return url.toString(); } catch (e) { // If parsing fails, return a generic connection string return 'postgresql://localhost:5432/'; } } exports.parseAndMaskConnectionString = parseAndMaskConnectionString; function getConnectionString(params) { if ('connectionString' in params && params.connectionString) { return parseAndMaskConnectionString(params.connectionString); } const host = params.host || 'localhost'; const port = params.port || 5432; const database = params.database || ''; return `postgresql://${host}:${port}/${database}`; } exports.getConnectionString = getConnectionString; function getPort(port) { // Port may be NaN as parseInt() is used on the value, passing null will result in NaN being parsed. // https://github.com/brianc/node-postgres/blob/2a8efbee09a284be12748ed3962bc9b816965e36/packages/pg/lib/connection-parameters.js#L66 if (Number.isInteger(port)) { return port; } // Unable to find the default used in pg code, so falling back to 'undefined'. return undefined; } function getSemanticAttributesFromConnection(params) { return { [semantic_conventions_1.SEMATTRS_DB_SYSTEM]: semantic_conventions_1.DBSYSTEMVALUES_POSTGRESQL, [semantic_conventions_1.SEMATTRS_DB_NAME]: params.database, [semantic_conventions_1.SEMATTRS_DB_CONNECTION_STRING]: getConnectionString(params), [semantic_conventions_1.SEMATTRS_NET_PEER_NAME]: params.host, [semantic_conventions_1.SEMATTRS_NET_PEER_PORT]: getPort(params.port), [semantic_conventions_1.SEMATTRS_DB_USER]: params.user, }; } exports.getSemanticAttributesFromConnection = getSemanticAttributesFromConnection; function getSemanticAttributesFromPool(params) { let url; try { url = params.connectionString ? new URL(params.connectionString) : undefined; } catch (e) { url = undefined; } return { [semantic_conventions_1.SEMATTRS_DB_SYSTEM]: semantic_conventions_1.DBSYSTEMVALUES_POSTGRESQL, [semantic_conventions_1.SEMATTRS_DB_NAME]: url?.pathname.slice(1) ?? params.database, [semantic_conventions_1.SEMATTRS_DB_CONNECTION_STRING]: getConnectionString(params), [semantic_conventions_1.SEMATTRS_NET_PEER_NAME]: url?.hostname ?? params.host, [semantic_conventions_1.SEMATTRS_NET_PEER_PORT]: Number(url?.port) || getPort(params.port), [semantic_conventions_1.SEMATTRS_DB_USER]: url?.username ?? params.user, [AttributeNames_1.AttributeNames.IDLE_TIMEOUT_MILLIS]: params.idleTimeoutMillis, [AttributeNames_1.AttributeNames.MAX_CLIENT]: params.maxClient, }; } exports.getSemanticAttributesFromPool = getSemanticAttributesFromPool; function shouldSkipInstrumentation(instrumentationConfig) { return (instrumentationConfig.requireParentSpan === true && api_1.trace.getSpan(api_1.context.active()) === undefined); } exports.shouldSkipInstrumentation = shouldSkipInstrumentation; // Create a span from our normalized queryConfig object, // or return a basic span if no queryConfig was given/could be created. function handleConfigQuery(tracer, instrumentationConfig, queryConfig) { // Create child span. const { connectionParameters } = this; const dbName = connectionParameters.database; const spanName = getQuerySpanName(dbName, queryConfig); const span = tracer.startSpan(spanName, { kind: api_1.SpanKind.CLIENT, attributes: getSemanticAttributesFromConnection(connectionParameters), }); if (!queryConfig) { return span; } // Set attributes if (queryConfig.text) { span.setAttribute(semantic_conventions_1.SEMATTRS_DB_STATEMENT, queryConfig.text); } if (instrumentationConfig.enhancedDatabaseReporting && Array.isArray(queryConfig.values)) { try { const convertedValues = queryConfig.values.map(value => { if (value == null) { return 'null'; } else if (value instanceof Buffer) { return value.toString(); } else if (typeof value === 'object') { if (typeof value.toPostgres === 'function') { return value.toPostgres(); } return JSON.stringify(value); } else { //string, number return value.toString(); } }); span.setAttribute(AttributeNames_1.AttributeNames.PG_VALUES, convertedValues); } catch (e) { api_1.diag.error('failed to stringify ', queryConfig.values, e); } } // Set plan name attribute, if present if (typeof queryConfig.name === 'string') { span.setAttribute(AttributeNames_1.AttributeNames.PG_PLAN, queryConfig.name); } return span; } exports.handleConfigQuery = handleConfigQuery; function handleExecutionResult(config, span, pgResult) { if (typeof config.responseHook === 'function') { (0, instrumentation_1.safeExecuteInTheMiddle)(() => { config.responseHook(span, { data: pgResult, }); }, err => { if (err) { api_1.diag.error('Error running response hook', err); } }, true); } } exports.handleExecutionResult = handleExecutionResult; function patchCallback(instrumentationConfig, span, cb, attributes, recordDuration) { return function patchedCallback(err, res) { if (err) { if (Object.prototype.hasOwnProperty.call(err, 'code')) { attributes[semantic_conventions_1.ATTR_ERROR_TYPE] = err['code']; } span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: err.message, }); } else { handleExecutionResult(instrumentationConfig, span, res); } recordDuration(); span.end(); cb.call(this, err, res); }; } exports.patchCallback = patchCallback; function getPoolName(pool) { let poolName = ''; poolName += (pool?.host ? `${pool.host}` : 'unknown_host') + ':'; poolName += (pool?.port ? `${pool.port}` : 'unknown_port') + '/'; poolName += pool?.database ? `${pool.database}` : 'unknown_database'; return poolName.trim(); } exports.getPoolName = getPoolName; function updateCounter(poolName, pool, connectionCount, connectionPendingRequests, latestCounter) { const all = pool.totalCount; const pending = pool.waitingCount; const idle = pool.idleCount; const used = all - idle; connectionCount.add(used - latestCounter.used, { [semconv_1.ATTR_DB_CLIENT_CONNECTION_STATE]: semconv_1.DB_CLIENT_CONNECTION_STATE_VALUE_USED, [semconv_1.ATTR_DB_CLIENT_CONNECTION_POOL_NAME]: poolName, }); connectionCount.add(idle - latestCounter.idle, { [semconv_1.ATTR_DB_CLIENT_CONNECTION_STATE]: semconv_1.DB_CLIENT_CONNECTION_STATE_VALUE_IDLE, [semconv_1.ATTR_DB_CLIENT_CONNECTION_POOL_NAME]: poolName, }); connectionPendingRequests.add(pending - latestCounter.pending, { [semconv_1.ATTR_DB_CLIENT_CONNECTION_POOL_NAME]: poolName, }); return { used: used, idle: idle, pending: pending }; } exports.updateCounter = updateCounter; function patchCallbackPGPool(span, cb) { return function patchedCallback(err, res, done) { if (err) { span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: err.message, }); } span.end(); cb.call(this, err, res, done); }; } exports.patchCallbackPGPool = patchCallbackPGPool; function patchClientConnectCallback(span, cb) { return function patchedClientConnectCallback(err) { if (err) { span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: err.message, }); } span.end(); cb.apply(this, arguments); }; } exports.patchClientConnectCallback = patchClientConnectCallback; /** * Attempt to get a message string from a thrown value, while being quite * defensive, to recognize the fact that, in JS, any kind of value (even * primitives) can be thrown. */ function getErrorMessage(e) { return typeof e === 'object' && e !== null && 'message' in e ? String(e.message) : undefined; } exports.getErrorMessage = getErrorMessage; function isObjectWithTextString(it) { return (typeof it === 'object' && typeof it?.text === 'string'); } exports.isObjectWithTextString = isObjectWithTextString; //# sourceMappingURL=utils.js.map