diff --git a/package-lock.json b/package-lock.json index 2ae8e72eea..46dcf2d004 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25994,6 +25994,13 @@ } } }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "dev": true, + "optional": true + }, "node_modules/pg-connection-string": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", @@ -35465,6 +35472,7 @@ "knex": "0.95.9", "mocha": "7.2.0", "nyc": "15.1.0", + "pg": "8.11.3", "rimraf": "5.0.5", "sqlite3": "5.1.6", "ts-mocha": "10.0.0", @@ -35477,6 +35485,50 @@ "@opentelemetry/api": "^1.3.0" } }, + "plugins/node/opentelemetry-instrumentation-knex/node_modules/pg": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", + "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", + "dev": true, + "dependencies": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.6.2", + "pg-pool": "^3.6.1", + "pg-protocol": "^1.6.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "plugins/node/opentelemetry-instrumentation-knex/node_modules/pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==", + "dev": true + }, + "plugins/node/opentelemetry-instrumentation-knex/node_modules/pg-pool": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "dev": true, + "peerDependencies": { + "pg": ">=8.0" + } + }, "plugins/node/opentelemetry-instrumentation-koa": { "name": "@opentelemetry/instrumentation-koa", "version": "0.37.0", @@ -44517,10 +44569,42 @@ "knex": "0.95.9", "mocha": "7.2.0", "nyc": "15.1.0", + "pg": "8.11.3", "rimraf": "5.0.5", "sqlite3": "5.1.6", "ts-mocha": "10.0.0", "typescript": "4.4.4" + }, + "dependencies": { + "pg": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", + "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", + "dev": true, + "requires": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-cloudflare": "^1.1.1", + "pg-connection-string": "^2.6.2", + "pg-pool": "^3.6.1", + "pg-protocol": "^1.6.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + } + }, + "pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==", + "dev": true + }, + "pg-pool": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "dev": true, + "requires": {} + } } }, "@opentelemetry/instrumentation-koa": { @@ -61091,6 +61175,13 @@ "pgpass": "1.x" } }, + "pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "dev": true, + "optional": true + }, "pg-connection-string": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", diff --git a/plugins/node/opentelemetry-instrumentation-knex/package.json b/plugins/node/opentelemetry-instrumentation-knex/package.json index 9291a89717..47fbdd0756 100644 --- a/plugins/node/opentelemetry-instrumentation-knex/package.json +++ b/plugins/node/opentelemetry-instrumentation-knex/package.json @@ -51,6 +51,7 @@ "knex": "0.95.9", "mocha": "7.2.0", "nyc": "15.1.0", + "pg": "8.11.3", "rimraf": "5.0.5", "sqlite3": "5.1.6", "ts-mocha": "10.0.0", diff --git a/plugins/node/opentelemetry-instrumentation-knex/src/instrumentation.ts b/plugins/node/opentelemetry-instrumentation-knex/src/instrumentation.ts index 257232f278..b19094541e 100644 --- a/plugins/node/opentelemetry-instrumentation-knex/src/instrumentation.ts +++ b/plugins/node/opentelemetry-instrumentation-knex/src/instrumentation.ts @@ -131,13 +131,19 @@ export class KnexInstrumentation extends InstrumentationBase { return function wrapQuery(original: () => any) { return function wrapped_logging_method(this: any, query: any) { const config = this.client.config; + const connection = utils.parseConnectionString( + config?.connection?.connectionString + ); const table = utils.extractTableName(this.builder); // `method` actually refers to the knex API method - Not exactly "operation" // in the spec sense, but matches most of the time. const operation = query?.method; const name = - config?.connection?.filename || config?.connection?.database; + connection?.database || + config?.connection?.filename || + config?.connection?.database; + const maxLen = ( instrumentation._config as types.KnexInstrumentationConfig ).maxQueryLength!; @@ -147,10 +153,13 @@ export class KnexInstrumentation extends InstrumentationBase { [SemanticAttributes.DB_SYSTEM]: utils.mapSystem(config.client), [SemanticAttributes.DB_SQL_TABLE]: table, [SemanticAttributes.DB_OPERATION]: operation, - [SemanticAttributes.DB_USER]: config?.connection?.user, + [SemanticAttributes.DB_USER]: + connection?.user || config?.connection?.user, [SemanticAttributes.DB_NAME]: name, - [SemanticAttributes.NET_PEER_NAME]: config?.connection?.host, - [SemanticAttributes.NET_PEER_PORT]: config?.connection?.port, + [SemanticAttributes.NET_PEER_NAME]: + connection?.host || config?.connection?.host, + [SemanticAttributes.NET_PEER_PORT]: + connection?.port || config?.connection?.port, [SemanticAttributes.NET_TRANSPORT]: config?.connection?.filename === ':memory:' ? 'inproc' : undefined, }; diff --git a/plugins/node/opentelemetry-instrumentation-knex/src/utils.ts b/plugins/node/opentelemetry-instrumentation-knex/src/utils.ts index 6c7e6fbdba..a92b31c179 100644 --- a/plugins/node/opentelemetry-instrumentation-knex/src/utils.ts +++ b/plugins/node/opentelemetry-instrumentation-knex/src/utils.ts @@ -88,3 +88,16 @@ export const extractTableName = (builder: any): string => { } return table; }; + +export const parseConnectionString = (connectionString: string | undefined) => { + if (!connectionString) { + return undefined; + } + const url = new URL(connectionString); + return { + host: url.hostname, + port: parseInt(url.port || '5432', 10), + user: url.username, + database: url.pathname.replace(/^\//, ''), + }; +}; diff --git a/plugins/node/opentelemetry-instrumentation-knex/test/index.test.ts b/plugins/node/opentelemetry-instrumentation-knex/test/index.test.ts index 1f660cf273..d0835b7c92 100644 --- a/plugins/node/opentelemetry-instrumentation-knex/test/index.test.ts +++ b/plugins/node/opentelemetry-instrumentation-knex/test/index.test.ts @@ -431,6 +431,61 @@ describe('Knex instrumentation', () => { ); }); }); + + describe('connectionString', () => { + const user = process.env.POSTGRES_USER || 'postgres'; + const password = process.env.POSTGRES_PASSWORD || 'postgres'; + const database = process.env.POSTGRES_DB || 'postgres'; + const host = process.env.POSTGRES_HOST || 'localhost'; + const port = process.env.POSTGRES_PORT + ? parseInt(process.env.POSTGRES_PORT, 10) + : 54320; + let pgClient: any; + + beforeEach(() => { + pgClient = knex({ + client: 'pg', + connection: { + connectionString: `postgres://${user}:${password}@${host}:${port}/${database}`, + // connectionString takes precedence over other connection options in knex + host: 'ignored', + user: 'ignored', + port: 1111, + db: 'ignored', + }, + }); + }); + + afterEach(async () => { + await pgClient.destroy(); + }); + + it('should extract connection attributes from connectionString when available', async () => { + const parentSpan = tracer.startSpan('parentSpan'); + const statement = "select date('now')"; + + await context.with( + trace.setSpan(context.active(), parentSpan), + async () => { + await pgClient.raw(statement); + parentSpan.end(); + + const instrumentationSpans = memoryExporter.getFinishedSpans(); + + const span = instrumentationSpans[0]; + assertMatch(span.name, new RegExp('raw')); + assertMatch(span.name, new RegExp(database)); + assert.strictEqual(span.attributes['db.system'], 'postgresql'); + assert.strictEqual(span.attributes['db.name'], database); + assert.strictEqual(span.attributes['db.user'], user); + assert.strictEqual(span.attributes['db.operation'], 'raw'); + assert.strictEqual(span.attributes['db.statement'], statement); + assert.strictEqual(span.attributes['net.peer.name'], host); + assert.strictEqual(span.attributes['net.peer.port'], port); + } + ); + }); + }); }); describe('Disabling instrumentation', () => { diff --git a/plugins/node/opentelemetry-instrumentation-knex/test/utils.test.ts b/plugins/node/opentelemetry-instrumentation-knex/test/utils.test.ts new file mode 100644 index 0000000000..4728213121 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-knex/test/utils.test.ts @@ -0,0 +1,54 @@ +/* + * 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. + */ + +import assert = require('assert'); +import { parseConnectionString } from '../src/utils'; + +describe('utils', () => { + describe('parseConnectionString', () => { + it('should return undefined if connectionString is undefined', () => { + const connection = parseConnectionString(undefined); + + assert.strictEqual(connection, undefined); + }); + + it('should return object with connection properties', () => { + const connection = parseConnectionString( + 'postgres://user:password@localhost:5555/mydb' + ); + + assert.deepEqual(connection, { + host: 'localhost', + port: 5555, + user: 'user', + database: 'mydb', + }); + }); + + it('should assume default port of 5432 if not provided', () => { + const connection = parseConnectionString( + 'postgres://user@localhost/mydb' + ); + + assert.deepEqual(connection, { + host: 'localhost', + port: 5432, + user: 'user', + database: 'mydb', + }); + }); + }); +});