Skip to content

Commit

Permalink
feature(plugin): implement postgres plugin (#417)
Browse files Browse the repository at this point in the history
* feat(pg): implement postgres plugin

* fix: linting

* fix: docker starting not locally

* fix: compile errors from merge

* fix: linting

* refactor: use helper functions for span building

* fix: add callback patching to end span

* fix: add required attributes, address comments

* fix: lint errors

* refactor: start named spans in query handlers

* fix: linting errors

* fix: circleci config, make pg helpers nonexported

* fix: linting

* docs: add supported versions

* fix: pass PG env to spawned container

* fix: remove hardcoded shouldTest

* test: add span tests for pg driver errors

* chore: remove hardcode shouldTest
  • Loading branch information
markwolff authored and mayurkale22 committed Oct 30, 2019
1 parent ff907cf commit 5c49c6c
Show file tree
Hide file tree
Showing 11 changed files with 1,025 additions and 3 deletions.
21 changes: 21 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions packages/opentelemetry-plugin-postgres/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <https://opentelemetry.io/>
- For more about OpenTelemetry JavaScript: <https://github.com/open-telemetry/opentelemetry-js>
Expand Down
14 changes: 11 additions & 3 deletions packages/opentelemetry-plugin-postgres/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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"
}
}
36 changes: 36 additions & 0 deletions packages/opentelemetry-plugin-postgres/src/enums.ts
Original file line number Diff line number Diff line change
@@ -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',
}
2 changes: 2 additions & 0 deletions packages/opentelemetry-plugin-postgres/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export * from './pg';
161 changes: 161 additions & 0 deletions packages/opentelemetry-plugin-postgres/src/pg.ts
Original file line number Diff line number Diff line change
@@ -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<typeof pgTypes> {
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);
38 changes: 38 additions & 0 deletions packages/opentelemetry-plugin-postgres/src/types.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading

0 comments on commit 5c49c6c

Please sign in to comment.