diff --git a/.circleci/config.yml b/.circleci/config.yml
index 8138c5e300..bc61c37361 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,5 +1,18 @@
version: 2
+test_env: &test_env
+ RUN_POSTGRES_TESTS: 1
+ POSTGRES_USER: postgres
+ POSTGRES_DB: circle_database
+ POSTGRES_HOST: localhost
+ POSTGRES_PORT: 5432
+
+postgres_service: &postgres_service
+ image: circleci/postgres:9.6-alpine
+ environment: # env to pass to CircleCI, specified values must match test_env
+ POSTGRES_USER: postgres
+ POSTGRES_DB: circle_database
+
node_unit_tests: &node_unit_tests
steps:
- checkout
@@ -71,18 +84,26 @@ jobs:
node8:
docker:
- image: node:8
+ environment: *test_env
+ - *postgres_service
<<: *node_unit_tests
node10:
docker:
- image: node:10
+ environment: *test_env
+ - *postgres_service
<<: *node_unit_tests
node11:
docker:
- image: node:11
+ environment: *test_env
+ - *postgres_service
<<: *node_unit_tests
node12:
docker:
- image: node:12
+ environment: *test_env
+ - *postgres_service
<<: *node_unit_tests
node12-browsers:
docker:
diff --git a/packages/opentelemetry-plugin-postgres/README.md b/packages/opentelemetry-plugin-postgres/README.md
index db2d211a1d..f18233a45e 100644
--- a/packages/opentelemetry-plugin-postgres/README.md
+++ b/packages/opentelemetry-plugin-postgres/README.md
@@ -23,6 +23,10 @@ const opentelemetry = require('@opentelemetry/plugin-postgres');
// TODO: DEMONSTRATE API
```
+## Supported Versions
+
+- [pg](https://npmjs.com/package/pg): `7.x`
+
## Useful links
- For more information on OpenTelemetry, visit:
- For more about OpenTelemetry JavaScript:
diff --git a/packages/opentelemetry-plugin-postgres/package.json b/packages/opentelemetry-plugin-postgres/package.json
index 44d9402c53..1a785a4f7f 100644
--- a/packages/opentelemetry-plugin-postgres/package.json
+++ b/packages/opentelemetry-plugin-postgres/package.json
@@ -7,9 +7,12 @@
"types": "build/src/index.d.ts",
"repository": "open-telemetry/opentelemetry-js",
"scripts": {
- "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.ts'",
+ "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts'",
+ "test:debug": "ts-mocha --inspect-brk --no-timeouts -p tsconfig.json 'test/**/*.test.ts'",
+ "test:local": "cross-env RUN_POSTGRES_TESTS_LOCAL=true yarn test",
"tdd": "yarn test -- --watch-extensions ts --watch",
"clean": "rimraf build/*",
+ "codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../",
"check": "gts check",
"compile": "tsc -p .",
"fix": "gts fix",
@@ -43,11 +46,14 @@
"devDependencies": {
"@types/mocha": "^5.2.7",
"@types/node": "^12.6.9",
+ "@types/pg": "^7.11.2",
+ "@types/shimmer": "^1.0.1",
"codecov": "^3.5.0",
- "gts": "^1.1.0",
+ "gts": "^1.0.0",
"mocha": "^6.2.0",
"nyc": "^14.1.1",
"rimraf": "^3.0.0",
+ "pg": "^7.12.1",
"tslint-microsoft-contrib": "^6.2.0",
"tslint-consistent-codestyle": "^1.15.1",
"ts-mocha": "^6.0.0",
@@ -57,6 +63,8 @@
"dependencies": {
"@opentelemetry/core": "^0.1.1",
"@opentelemetry/node": "^0.1.1",
- "@opentelemetry/types": "^0.1.1"
+ "@opentelemetry/tracing": "^0.1.1",
+ "@opentelemetry/types": "^0.1.1",
+ "shimmer": "^1.2.1"
}
}
diff --git a/packages/opentelemetry-plugin-postgres/src/enums.ts b/packages/opentelemetry-plugin-postgres/src/enums.ts
new file mode 100644
index 0000000000..2df81cef3f
--- /dev/null
+++ b/packages/opentelemetry-plugin-postgres/src/enums.ts
@@ -0,0 +1,36 @@
+/*!
+ * Copyright 2019, 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.
+ */
+
+export enum AttributeNames {
+ // required by https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-semantic-conventions.md#databases-client-calls
+ COMPONENT = 'component',
+ DB_TYPE = 'db.type',
+ DB_INSTANCE = 'db.instance',
+ DB_STATEMENT = 'db.statement',
+ PEER_ADDRESS = 'peer.address',
+ PEER_HOSTNAME = 'peer.host',
+
+ // optional
+ DB_USER = 'db.user',
+ PEER_PORT = 'peer.port',
+ PEER_IPV4 = 'peer.ipv4',
+ PEER_IPV6 = 'peer.ipv6',
+ PEER_SERVICE = 'peer.service',
+
+ // PG specific -- not specified by spec
+ PG_VALUES = 'pg.values',
+ PG_PLAN = 'pg.plan',
+}
diff --git a/packages/opentelemetry-plugin-postgres/src/index.ts b/packages/opentelemetry-plugin-postgres/src/index.ts
index ae225f6b52..33dab806f8 100644
--- a/packages/opentelemetry-plugin-postgres/src/index.ts
+++ b/packages/opentelemetry-plugin-postgres/src/index.ts
@@ -13,3 +13,5 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
+export * from './pg';
diff --git a/packages/opentelemetry-plugin-postgres/src/pg.ts b/packages/opentelemetry-plugin-postgres/src/pg.ts
new file mode 100644
index 0000000000..e41ceee08a
--- /dev/null
+++ b/packages/opentelemetry-plugin-postgres/src/pg.ts
@@ -0,0 +1,161 @@
+/*!
+ * Copyright 2019, 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 { BasePlugin } from '@opentelemetry/core';
+import { CanonicalCode, Span } from '@opentelemetry/types';
+import {
+ PostgresPluginOptions,
+ PgClientExtended,
+ PgPluginQueryConfig,
+ PostgresCallback,
+} from './types';
+import * as pgTypes from 'pg';
+import * as shimmer from 'shimmer';
+import * as utils from './utils';
+
+export class PostgresPlugin extends BasePlugin {
+ protected _config: PostgresPluginOptions;
+
+ static readonly COMPONENT = 'pg';
+ static readonly DB_TYPE = 'sql';
+
+ static readonly BASE_SPAN_NAME = PostgresPlugin.COMPONENT + '.query';
+
+ readonly supportedVersions = ['7.*'];
+
+ constructor(readonly moduleName: string) {
+ super();
+ this._config = {};
+ }
+
+ protected patch(): typeof pgTypes {
+ if (this._moduleExports.Client.prototype.query) {
+ shimmer.wrap(
+ this._moduleExports.Client.prototype,
+ 'query',
+ this._getClientQueryPatch() as never
+ );
+ }
+ return this._moduleExports;
+ }
+
+ protected unpatch(): void {
+ if (this._moduleExports.Client.prototype.query) {
+ shimmer.unwrap(this._moduleExports.Client.prototype, 'query');
+ }
+ }
+
+ private _getClientQueryPatch() {
+ const plugin = this;
+ return (original: typeof pgTypes.Client.prototype.query) => {
+ plugin._logger.debug(
+ `Patching ${PostgresPlugin.COMPONENT}.Client.prototype.query`
+ );
+ return function query(
+ this: pgTypes.Client & PgClientExtended,
+ ...args: unknown[]
+ ) {
+ let span: Span;
+
+ // Handle different client.query(...) signatures
+ if (typeof args[0] === 'string') {
+ if (args.length > 1 && args[1] instanceof Array) {
+ span = utils.handleParameterizedQuery.call(
+ this,
+ plugin._tracer,
+ ...args
+ );
+ } else {
+ span = utils.handleTextQuery.call(this, plugin._tracer, ...args);
+ }
+ } else if (typeof args[0] === 'object') {
+ span = utils.handleConfigQuery.call(this, plugin._tracer, ...args);
+ } else {
+ return utils.handleInvalidQuery.call(
+ this,
+ plugin._tracer,
+ original,
+ ...args
+ );
+ }
+
+ // Bind callback to parent span
+ if (args.length > 0) {
+ const parentSpan = plugin._tracer.getCurrentSpan();
+ if (typeof args[args.length - 1] === 'function') {
+ // Patch ParameterQuery callback
+ args[args.length - 1] = utils.patchCallback(span, args[
+ args.length - 1
+ ] as PostgresCallback);
+ // If a parent span exists, bind the callback
+ if (parentSpan) {
+ args[args.length - 1] = plugin._tracer.bind(
+ args[args.length - 1]
+ );
+ }
+ } else if (
+ typeof (args[0] as PgPluginQueryConfig).callback === 'function'
+ ) {
+ // Patch ConfigQuery callback
+ let callback = utils.patchCallback(
+ span,
+ (args[0] as PgPluginQueryConfig).callback!
+ );
+ // If a parent span existed, bind the callback
+ if (parentSpan) {
+ callback = plugin._tracer.bind(callback);
+ }
+
+ // Copy the callback instead of writing to args.callback so that we don't modify user's
+ // original callback reference
+ args[0] = { ...(args[0] as object), callback };
+ }
+ }
+
+ // Perform the original query
+ const result: unknown = original.apply(this, args as never);
+
+ // Bind promise to parent span and end the span
+ if (result instanceof Promise) {
+ return result
+ .then((result: unknown) => {
+ // Return a pass-along promise which ends the span and then goes to user's orig resolvers
+ return new Promise((resolve, _) => {
+ span.setStatus({ code: CanonicalCode.OK });
+ span.end();
+ resolve(result);
+ });
+ })
+ .catch((error: Error) => {
+ return new Promise((_, reject) => {
+ span.setStatus({
+ code: CanonicalCode.UNKNOWN,
+ message: error.message,
+ });
+ span.end();
+ reject(error);
+ });
+ });
+ }
+
+ // else returns void
+ return result; // void
+ };
+ };
+ }
+}
+
+export const plugin = new PostgresPlugin(PostgresPlugin.COMPONENT);
diff --git a/packages/opentelemetry-plugin-postgres/src/types.ts b/packages/opentelemetry-plugin-postgres/src/types.ts
new file mode 100644
index 0000000000..c9d1f70974
--- /dev/null
+++ b/packages/opentelemetry-plugin-postgres/src/types.ts
@@ -0,0 +1,38 @@
+/*!
+ * Copyright 2019, 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 * as pgTypes from 'pg';
+
+export interface PostgresPluginOptions {}
+
+export type PostgresCallback = (err: Error, res: object) => unknown;
+
+// These are not included in @types/pg, so manually define them.
+// https://github.com/brianc/node-postgres/blob/fde5ec586e49258dfc4a2fcd861fcdecb4794fc3/lib/client.js#L25
+export interface PgClientConnectionParams {
+ database: string;
+ host: string;
+ port: number;
+ user: string;
+}
+
+export interface PgClientExtended {
+ connectionParameters: PgClientConnectionParams;
+}
+
+export interface PgPluginQueryConfig extends pgTypes.QueryConfig {
+ callback?: PostgresCallback;
+}
diff --git a/packages/opentelemetry-plugin-postgres/src/utils.ts b/packages/opentelemetry-plugin-postgres/src/utils.ts
new file mode 100644
index 0000000000..5e74204b3b
--- /dev/null
+++ b/packages/opentelemetry-plugin-postgres/src/utils.ts
@@ -0,0 +1,181 @@
+/*!
+ * Copyright 2019, 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 { Span, CanonicalCode, Tracer, SpanKind } from '@opentelemetry/types';
+import { AttributeNames } from './enums';
+import {
+ PgClientExtended,
+ PgPluginQueryConfig,
+ PostgresCallback,
+ PgClientConnectionParams,
+} from './types';
+import * as pgTypes from 'pg';
+import { PostgresPlugin } from './pg';
+
+function arrayStringifyHelper(arr: Array): string {
+ return '[' + arr.toString() + ']';
+}
+
+// Helper function to get a low cardinality command name from the full text query
+function getCommandFromText(text?: string): string {
+ if (!text) return 'unknown';
+ const words = text.split(' ');
+ return words[0].length > 0 ? words[0] : 'unknown';
+}
+
+function getJDBCString(params: PgClientConnectionParams) {
+ const host = params.host || 'localhost'; // postgres defaults to localhost
+ const port = params.port || 5432; // postgres defaults to port 5432
+ const database = params.database || '';
+ return `jdbc:postgresql://${host}:${port}/${database}`;
+}
+
+// Private helper function to start a span
+function pgStartSpan(
+ tracer: Tracer,
+ client: pgTypes.Client & PgClientExtended,
+ name: string
+) {
+ const jdbcString = getJDBCString(client.connectionParameters);
+ return tracer.startSpan(name, {
+ kind: SpanKind.CLIENT,
+ parent: tracer.getCurrentSpan() || undefined,
+ attributes: {
+ [AttributeNames.COMPONENT]: PostgresPlugin.COMPONENT, // required
+ [AttributeNames.DB_INSTANCE]: client.connectionParameters.database, // required
+ [AttributeNames.DB_TYPE]: PostgresPlugin.DB_TYPE, // required
+ [AttributeNames.PEER_ADDRESS]: jdbcString, // required
+ [AttributeNames.PEER_HOSTNAME]: client.connectionParameters.host, // required
+ [AttributeNames.PEER_PORT]: client.connectionParameters.port,
+ [AttributeNames.DB_USER]: client.connectionParameters.user,
+ },
+ });
+}
+
+// Queries where args[0] is a QueryConfig
+export function handleConfigQuery(
+ this: pgTypes.Client & PgClientExtended,
+ tracer: Tracer,
+ ...args: unknown[]
+) {
+ const argsConfig = args[0] as PgPluginQueryConfig;
+
+ // Set child span name
+ const queryCommand = getCommandFromText(argsConfig.name || argsConfig.text);
+ const name = PostgresPlugin.BASE_SPAN_NAME + ':' + queryCommand;
+ const span = pgStartSpan(tracer, this, name);
+
+ // Set attributes
+ if (argsConfig.text) {
+ span.setAttribute(AttributeNames.DB_STATEMENT, argsConfig.text);
+ }
+
+ if (argsConfig.values instanceof Array) {
+ span.setAttribute(
+ AttributeNames.PG_VALUES,
+ arrayStringifyHelper(argsConfig.values)
+ );
+ }
+ // Set plan name attribute, if present
+ if (argsConfig.name) {
+ span.setAttribute(AttributeNames.PG_PLAN, argsConfig.name);
+ }
+
+ return span;
+}
+
+// Queries where args[1] is a 'values' array
+export function handleParameterizedQuery(
+ this: pgTypes.Client & PgClientExtended,
+ tracer: Tracer,
+ ...args: unknown[]
+) {
+ // Set child span name
+ const queryCommand = getCommandFromText(args[0] as string);
+ const name = PostgresPlugin.BASE_SPAN_NAME + ':' + queryCommand;
+ const span = pgStartSpan(tracer, this, name);
+
+ // Set attributes
+ span.setAttribute(AttributeNames.DB_STATEMENT, args[0]);
+ if (args[1] instanceof Array) {
+ span.setAttribute(AttributeNames.PG_VALUES, arrayStringifyHelper(args[1]));
+ }
+
+ return span;
+}
+
+// Queries where args[0] is a text query and 'values' was not specified
+export function handleTextQuery(
+ this: pgTypes.Client & PgClientExtended,
+ tracer: Tracer,
+ ...args: unknown[]
+) {
+ // Set child span name
+ const queryCommand = getCommandFromText(args[0] as string);
+ const name = PostgresPlugin.BASE_SPAN_NAME + ':' + queryCommand;
+ const span = pgStartSpan(tracer, this, name);
+
+ // Set attributes
+ span.setAttribute(AttributeNames.DB_STATEMENT, args[0]);
+
+ return span;
+}
+
+/**
+ * Invalid query handler. We should never enter this function unless invalid args were passed to the driver.
+ * Create and immediately end a new span
+ */
+export function handleInvalidQuery(
+ this: pgTypes.Client & PgClientExtended,
+ tracer: Tracer,
+ originalQuery: typeof pgTypes.Client.prototype.query,
+ ...args: unknown[]
+) {
+ let result;
+ const span = pgStartSpan(tracer, this, PostgresPlugin.BASE_SPAN_NAME);
+ try {
+ result = originalQuery.apply(this, args as never);
+ span.setStatus({ code: CanonicalCode.OK }); // this will never happen, but set a status anyways
+ } catch (e) {
+ span.setStatus({ code: CanonicalCode.UNKNOWN, message: e.message });
+ throw e;
+ } finally {
+ span.end();
+ }
+ return result;
+}
+
+export function patchCallback(
+ span: Span,
+ cb: PostgresCallback
+): PostgresCallback {
+ return function patchedCallback(
+ this: pgTypes.Client & PgClientExtended,
+ err: Error,
+ res: object
+ ) {
+ if (err) {
+ span.setStatus({
+ code: CanonicalCode.UNKNOWN,
+ message: err.message,
+ });
+ } else if (res) {
+ span.setStatus({ code: CanonicalCode.OK });
+ }
+ span.end();
+ cb.call(this, err, res);
+ };
+}
diff --git a/packages/opentelemetry-plugin-postgres/test/assertionUtils.ts b/packages/opentelemetry-plugin-postgres/test/assertionUtils.ts
new file mode 100644
index 0000000000..2c81918305
--- /dev/null
+++ b/packages/opentelemetry-plugin-postgres/test/assertionUtils.ts
@@ -0,0 +1,82 @@
+/*!
+ * Copyright 2019, 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 {
+ SpanKind,
+ Attributes,
+ Event,
+ Span,
+ Status,
+} from '@opentelemetry/types';
+import * as assert from 'assert';
+import { PostgresPlugin } from '../src';
+import { ReadableSpan } from '@opentelemetry/tracing';
+import {
+ hrTimeToMilliseconds,
+ hrTimeToMicroseconds,
+} from '@opentelemetry/core';
+import { AttributeNames } from '../src/enums';
+
+export const assertSpan = (
+ span: ReadableSpan,
+ kind: SpanKind,
+ attributes: Attributes,
+ events: Event[],
+ status: Status
+) => {
+ assert.strictEqual(span.spanContext.traceId.length, 32);
+ assert.strictEqual(span.spanContext.spanId.length, 16);
+ assert.strictEqual(span.kind, kind);
+
+ assert.strictEqual(
+ span.attributes[AttributeNames.COMPONENT],
+ PostgresPlugin.COMPONENT
+ );
+ assert.ok(span.endTime);
+ assert.strictEqual(span.links.length, 0);
+
+ assert.ok(
+ hrTimeToMicroseconds(span.startTime) < hrTimeToMicroseconds(span.endTime)
+ );
+ assert.ok(hrTimeToMilliseconds(span.endTime) > 0);
+
+ // attributes
+ assert.deepStrictEqual(span.attributes, attributes);
+
+ // events
+ assert.deepStrictEqual(span.events, events);
+
+ assert.strictEqual(span.status.code, status.code);
+ if (status.message) {
+ assert.strictEqual(span.status.message, status.message);
+ }
+};
+
+// Check if sourceSpan was propagated to targetSpan
+export const assertPropagation = (
+ childSpan: ReadableSpan,
+ parentSpan: Span
+) => {
+ const targetSpanContext = childSpan.spanContext;
+ const sourceSpanContext = parentSpan.context();
+ assert.strictEqual(targetSpanContext.traceId, sourceSpanContext.traceId);
+ assert.strictEqual(childSpan.parentSpanId, sourceSpanContext.spanId);
+ assert.strictEqual(
+ targetSpanContext.traceFlags,
+ sourceSpanContext.traceFlags
+ );
+ assert.notStrictEqual(targetSpanContext.spanId, sourceSpanContext.spanId);
+};
diff --git a/packages/opentelemetry-plugin-postgres/test/pg.test.ts b/packages/opentelemetry-plugin-postgres/test/pg.test.ts
new file mode 100644
index 0000000000..d30ad3a325
--- /dev/null
+++ b/packages/opentelemetry-plugin-postgres/test/pg.test.ts
@@ -0,0 +1,435 @@
+/*!
+ * Copyright 2019, 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 { NoopLogger } from '@opentelemetry/core';
+import { NodeTracer } from '@opentelemetry/node';
+import {
+ InMemorySpanExporter,
+ SimpleSpanProcessor,
+} from '@opentelemetry/tracing';
+import {
+ SpanKind,
+ Attributes,
+ TimedEvent,
+ Span,
+ CanonicalCode,
+ Status,
+} from '@opentelemetry/types';
+import { plugin, PostgresPlugin } from '../src';
+import { AttributeNames } from '../src/enums';
+import * as assert from 'assert';
+import * as pg from 'pg';
+import * as assertionUtils from './assertionUtils';
+import * as testUtils from './testUtils';
+
+const memoryExporter = new InMemorySpanExporter();
+
+const CONFIG = {
+ user: process.env.POSTGRES_USER || 'postgres',
+ database: process.env.POSTGRES_DB || 'postgres',
+ host: process.env.POSTGRES_HOST || 'localhost',
+ port: process.env.POSTGRES_PORT
+ ? parseInt(process.env.POSTGRES_PORT, 10)
+ : 54320,
+};
+
+const DEFAULT_ATTRIBUTES = {
+ [AttributeNames.COMPONENT]: PostgresPlugin.COMPONENT,
+ [AttributeNames.DB_INSTANCE]: CONFIG.database,
+ [AttributeNames.DB_TYPE]: PostgresPlugin.DB_TYPE,
+ [AttributeNames.PEER_HOSTNAME]: CONFIG.host,
+ [AttributeNames.PEER_ADDRESS]: `jdbc:postgresql://${CONFIG.host}:${CONFIG.port}/${CONFIG.database}`,
+ [AttributeNames.PEER_PORT]: CONFIG.port,
+ [AttributeNames.DB_USER]: CONFIG.user,
+};
+
+const okStatus: Status = {
+ code: CanonicalCode.OK,
+};
+const unknownStatus: Status = {
+ code: CanonicalCode.UNKNOWN,
+};
+
+const runCallbackTest = (
+ span: Span | null,
+ attributes: Attributes,
+ events: TimedEvent[],
+ status: Status = okStatus,
+ spansLength = 1,
+ spansIndex = 0
+) => {
+ const spans = memoryExporter.getFinishedSpans();
+ assert.strictEqual(spans.length, spansLength);
+ const pgSpan = spans[spansIndex];
+ assertionUtils.assertSpan(
+ pgSpan,
+ SpanKind.CLIENT,
+ attributes,
+ events,
+ status
+ );
+ if (span) {
+ assertionUtils.assertPropagation(pgSpan, span);
+ }
+};
+
+describe('pg@7.x', () => {
+ let client: pg.Client;
+ const tracer = new NodeTracer();
+ const logger = new NoopLogger();
+ const testPostgres = process.env.RUN_POSTGRES_TESTS; // For CI: assumes local postgres db is already available
+ const testPostgresLocally = process.env.RUN_POSTGRES_TESTS_LOCAL; // For local: spins up local postgres db via docker
+ const shouldTest = testPostgres || testPostgresLocally; // Skips these tests if false (default)
+
+ before(async function() {
+ if (!shouldTest) {
+ // this.skip() workaround
+ // https://github.com/mochajs/mocha/issues/2683#issuecomment-375629901
+ this.test!.parent!.pending = true;
+ this.skip();
+ }
+ tracer.addSpanProcessor(new SimpleSpanProcessor(memoryExporter));
+ if (testPostgresLocally) {
+ testUtils.startDocker();
+ }
+
+ client = new pg.Client(CONFIG);
+ try {
+ await client.connect();
+ } catch (e) {
+ throw e;
+ }
+ });
+
+ after(async () => {
+ if (testPostgresLocally) {
+ testUtils.cleanUpDocker();
+ }
+ await client.end();
+ });
+
+ beforeEach(function() {
+ plugin.enable(pg, tracer, logger);
+ });
+
+ afterEach(() => {
+ memoryExporter.reset();
+ plugin.disable();
+ });
+
+ it('should return a plugin', () => {
+ assert.ok(plugin instanceof PostgresPlugin);
+ });
+
+ it('should have correct moduleName', () => {
+ assert.strictEqual(plugin.moduleName, 'pg');
+ });
+
+ it('should maintain pg module error throwing behavior with bad arguments', () => {
+ const assertPgError = (e: Error) => {
+ const src = e.stack!.split('\n').map(line => line.trim())[1];
+ return /node_modules[/\\]pg/.test(src);
+ };
+
+ assert.throws(
+ () => {
+ (client as any).query();
+ },
+ assertPgError,
+ 'pg should throw when no args provided'
+ );
+ runCallbackTest(null, DEFAULT_ATTRIBUTES, [], unknownStatus);
+ memoryExporter.reset();
+
+ assert.doesNotThrow(
+ () =>
+ (client as any).query({ foo: 'bar' }, undefined, () => {
+ runCallbackTest(
+ null,
+ {
+ ...DEFAULT_ATTRIBUTES,
+ },
+ [],
+ unknownStatus
+ );
+ }),
+ 'pg should not throw when invalid config args are provided'
+ );
+ });
+
+ describe('#client.query(...)', () => {
+ it('should not return a promise if callback is provided', done => {
+ const res = client.query('SELECT NOW()', (err, res) => {
+ assert.strictEqual(err, null);
+ done();
+ });
+ assert.strictEqual(res, undefined, 'No promise is returned');
+ });
+
+ it('should return a promise if callback is provided', done => {
+ const resPromise = client.query('SELECT NOW()');
+ resPromise
+ .then(res => {
+ assert.ok(res);
+ done();
+ })
+ .catch((err: Error) => {
+ assert.ok(false, err.message);
+ });
+ });
+
+ it('should intercept client.query(text, callback)', done => {
+ const attributes = {
+ ...DEFAULT_ATTRIBUTES,
+ [AttributeNames.DB_STATEMENT]: 'SELECT NOW()',
+ };
+ const events: TimedEvent[] = [];
+ const span = tracer.startSpan('test span');
+ tracer.withSpan(span, () => {
+ const res = client.query('SELECT NOW()', (err, res) => {
+ assert.strictEqual(err, null);
+ assert.ok(res);
+ runCallbackTest(span, attributes, events);
+ done();
+ });
+ assert.strictEqual(res, undefined, 'No promise is returned');
+ });
+ });
+
+ it('should intercept client.query(text, values, callback)', done => {
+ const query = 'SELECT $1::text';
+ const values = ['0'];
+ const attributes = {
+ ...DEFAULT_ATTRIBUTES,
+ [AttributeNames.DB_STATEMENT]: query,
+ [AttributeNames.PG_VALUES]: '[0]',
+ };
+ const events: TimedEvent[] = [];
+ const span = tracer.startSpan('test span');
+ tracer.withSpan(span, () => {
+ const resNoPromise = client.query(query, values, (err, res) => {
+ assert.strictEqual(err, null);
+ assert.ok(res);
+ runCallbackTest(span, attributes, events);
+ done();
+ });
+ assert.strictEqual(resNoPromise, undefined, 'No promise is returned');
+ });
+ });
+
+ it('should intercept client.query({text, callback})', done => {
+ const query = 'SELECT NOW()';
+ const attributes = {
+ ...DEFAULT_ATTRIBUTES,
+ [AttributeNames.DB_STATEMENT]: query,
+ };
+ const events: TimedEvent[] = [];
+ const span = tracer.startSpan('test span');
+ tracer.withSpan(span, () => {
+ const resNoPromise = client.query({
+ text: query,
+ callback: (err: Error, res: pg.QueryResult) => {
+ assert.strictEqual(err, null);
+ assert.ok(res);
+ runCallbackTest(span, attributes, events);
+ done();
+ },
+ } as pg.QueryConfig);
+ assert.strictEqual(resNoPromise, undefined, 'No promise is returned');
+ });
+ });
+
+ it('should intercept client.query({text}, callback)', done => {
+ const query = 'SELECT NOW()';
+ const attributes = {
+ ...DEFAULT_ATTRIBUTES,
+ [AttributeNames.DB_STATEMENT]: query,
+ };
+ const events: TimedEvent[] = [];
+ const span = tracer.startSpan('test span');
+ tracer.withSpan(span, () => {
+ const resNoPromise = client.query({ text: query }, (err, res) => {
+ assert.strictEqual(err, null);
+ assert.ok(res);
+ runCallbackTest(span, attributes, events);
+ done();
+ });
+ assert.strictEqual(resNoPromise, undefined, 'No promise is returned');
+ });
+ });
+
+ it('should intercept client.query(text, values)', async () => {
+ const query = 'SELECT $1::text';
+ const values = ['0'];
+ const attributes = {
+ ...DEFAULT_ATTRIBUTES,
+ [AttributeNames.DB_STATEMENT]: query,
+ [AttributeNames.PG_VALUES]: '[0]',
+ };
+ const events: TimedEvent[] = [];
+ const span = tracer.startSpan('test span');
+ await tracer.withSpan(span, async () => {
+ const resPromise = await client.query(query, values);
+ try {
+ assert.ok(resPromise);
+ runCallbackTest(span, attributes, events);
+ } catch (e) {
+ assert.ok(false, e.message);
+ }
+ });
+ });
+
+ it('should intercept client.query({text, values})', async () => {
+ const query = 'SELECT $1::text';
+ const values = ['0'];
+ const attributes = {
+ ...DEFAULT_ATTRIBUTES,
+ [AttributeNames.DB_STATEMENT]: query,
+ [AttributeNames.PG_VALUES]: '[0]',
+ };
+ const events: TimedEvent[] = [];
+ const span = tracer.startSpan('test span');
+ await tracer.withSpan(span, async () => {
+ const resPromise = await client.query({
+ text: query,
+ values: values,
+ });
+ try {
+ assert.ok(resPromise);
+ runCallbackTest(span, attributes, events);
+ } catch (e) {
+ assert.ok(false, e.message);
+ }
+ });
+ });
+
+ it('should intercept client.query(plan)', async () => {
+ const name = 'fetch-text';
+ const query = 'SELECT $1::text';
+ const values = ['0'];
+ const attributes = {
+ ...DEFAULT_ATTRIBUTES,
+ [AttributeNames.PG_PLAN]: name,
+ [AttributeNames.DB_STATEMENT]: query,
+ [AttributeNames.PG_VALUES]: '[0]',
+ };
+ const events: TimedEvent[] = [];
+ const span = tracer.startSpan('test span');
+
+ await tracer.withSpan(span, async () => {
+ try {
+ const resPromise = await client.query({
+ name: name,
+ text: query,
+ values: values,
+ });
+ assert.strictEqual(resPromise.command, 'SELECT');
+ runCallbackTest(span, attributes, events);
+ } catch (e) {
+ assert.ok(false, e.message);
+ }
+ });
+ });
+
+ it('should intercept client.query(text)', async () => {
+ const query = 'SELECT NOW()';
+ const attributes = {
+ ...DEFAULT_ATTRIBUTES,
+ [AttributeNames.DB_STATEMENT]: query,
+ };
+ const events: TimedEvent[] = [];
+ const span = tracer.startSpan('test span');
+ await tracer.withSpan(span, async () => {
+ try {
+ const resPromise = await client.query(query);
+ assert.ok(resPromise);
+ runCallbackTest(span, attributes, events);
+ } catch (e) {
+ assert.ok(false, e.message);
+ }
+ });
+ });
+
+ it('should handle the same callback being given to multiple client.query()s', done => {
+ let events = 0;
+
+ const queryHandler = (err: Error, res: pg.QueryResult) => {
+ const span = tracer.getCurrentSpan();
+ assert.ok(span);
+ assert.strictEqual((span as any)['_ended'], false);
+ if (err) {
+ throw err;
+ }
+ events += 1;
+ };
+
+ const config = {
+ text: 'SELECT NOW()',
+ callback: queryHandler,
+ };
+
+ client.query(config.text, config.callback); // 1
+ client.query(config); // 2
+ client.query(config.text, queryHandler); // 3
+ client.query(config.text, queryHandler); // 4
+ client.query(config.text); // Not using queryHandler
+ client.query(config); // 5
+ client.query(config); // 6
+ client.query(config.text, (err, res) => {
+ assert.strictEqual(events, 6);
+ done();
+ });
+ });
+
+ it('should preserve correct context even when using the same callback in client.query()', done => {
+ const spans = [tracer.startSpan('span 1'), tracer.startSpan('span 2')];
+ const currentSpans: (Span | null)[] = [];
+ const queryHandler = () => {
+ currentSpans.push(tracer.getCurrentSpan());
+ if (currentSpans.length === 2) {
+ assert.deepStrictEqual(currentSpans, spans);
+ done();
+ }
+ };
+
+ tracer.withSpan(spans[0], () => {
+ client.query('SELECT NOW()', queryHandler);
+ });
+ tracer.withSpan(spans[1], () => {
+ client.query('SELECT NOW()', queryHandler);
+ });
+ });
+
+ it('should preserve correct context even when using the same promise resolver in client.query()', done => {
+ const spans = [tracer.startSpan('span 1'), tracer.startSpan('span 2')];
+ const currentSpans: (Span | null)[] = [];
+ const queryHandler = () => {
+ currentSpans.push(tracer.getCurrentSpan());
+ if (currentSpans.length === 2) {
+ assert.deepStrictEqual(currentSpans, spans);
+ done();
+ }
+ };
+
+ tracer.withSpan(spans[0], () => {
+ client.query('SELECT NOW()').then(queryHandler);
+ });
+ tracer.withSpan(spans[1], () => {
+ client.query('SELECT NOW()').then(queryHandler);
+ });
+ });
+ });
+});
diff --git a/packages/opentelemetry-plugin-postgres/test/testUtils.ts b/packages/opentelemetry-plugin-postgres/test/testUtils.ts
new file mode 100644
index 0000000000..eec866fc41
--- /dev/null
+++ b/packages/opentelemetry-plugin-postgres/test/testUtils.ts
@@ -0,0 +1,54 @@
+/*!
+ * Copyright 2019, 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 * as childProcess from 'child_process';
+export function startDocker() {
+ const tasks = [
+ run('docker run -d -p 54320:5432 --name otpostgres postgres:alpine'),
+ ];
+
+ for (let i = 0; i < tasks.length; i++) {
+ const task = tasks[i];
+ if (task && task.code !== 0) {
+ console.error('Failed to start container!');
+ console.error(task.output);
+ return false;
+ }
+ }
+ return true;
+}
+
+export function cleanUpDocker() {
+ run('docker stop otpostgres');
+ run('docker rm otpostgres');
+}
+
+function run(cmd: string) {
+ try {
+ const proc = childProcess.spawnSync(cmd, {
+ shell: true,
+ });
+ return {
+ code: proc.status,
+ output: proc.output
+ .map(v => String.fromCharCode.apply(null, v as any))
+ .join(''),
+ };
+ } catch (e) {
+ console.log(e);
+ return;
+ }
+}