Skip to content

Commit 5c49c6c

Browse files
markwolffmayurkale22
authored andcommitted
feature(plugin): implement postgres plugin (#417)
* 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
1 parent ff907cf commit 5c49c6c

File tree

11 files changed

+1025
-3
lines changed

11 files changed

+1025
-3
lines changed

.circleci/config.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
version: 2
22

3+
test_env: &test_env
4+
RUN_POSTGRES_TESTS: 1
5+
POSTGRES_USER: postgres
6+
POSTGRES_DB: circle_database
7+
POSTGRES_HOST: localhost
8+
POSTGRES_PORT: 5432
9+
10+
postgres_service: &postgres_service
11+
image: circleci/postgres:9.6-alpine
12+
environment: # env to pass to CircleCI, specified values must match test_env
13+
POSTGRES_USER: postgres
14+
POSTGRES_DB: circle_database
15+
316
node_unit_tests: &node_unit_tests
417
steps:
518
- checkout
@@ -71,18 +84,26 @@ jobs:
7184
node8:
7285
docker:
7386
- image: node:8
87+
environment: *test_env
88+
- *postgres_service
7489
<<: *node_unit_tests
7590
node10:
7691
docker:
7792
- image: node:10
93+
environment: *test_env
94+
- *postgres_service
7895
<<: *node_unit_tests
7996
node11:
8097
docker:
8198
- image: node:11
99+
environment: *test_env
100+
- *postgres_service
82101
<<: *node_unit_tests
83102
node12:
84103
docker:
85104
- image: node:12
105+
environment: *test_env
106+
- *postgres_service
86107
<<: *node_unit_tests
87108
node12-browsers:
88109
docker:

packages/opentelemetry-plugin-postgres/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ const opentelemetry = require('@opentelemetry/plugin-postgres');
2323
// TODO: DEMONSTRATE API
2424
```
2525

26+
## Supported Versions
27+
28+
- [pg](https://npmjs.com/package/pg): `7.x`
29+
2630
## Useful links
2731
- For more information on OpenTelemetry, visit: <https://opentelemetry.io/>
2832
- For more about OpenTelemetry JavaScript: <https://github.com/open-telemetry/opentelemetry-js>

packages/opentelemetry-plugin-postgres/package.json

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77
"types": "build/src/index.d.ts",
88
"repository": "open-telemetry/opentelemetry-js",
99
"scripts": {
10-
"test": "nyc ts-mocha -p tsconfig.json 'test/**/*.ts'",
10+
"test": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts'",
11+
"test:debug": "ts-mocha --inspect-brk --no-timeouts -p tsconfig.json 'test/**/*.test.ts'",
12+
"test:local": "cross-env RUN_POSTGRES_TESTS_LOCAL=true yarn test",
1113
"tdd": "yarn test -- --watch-extensions ts --watch",
1214
"clean": "rimraf build/*",
15+
"codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../",
1316
"check": "gts check",
1417
"compile": "tsc -p .",
1518
"fix": "gts fix",
@@ -43,11 +46,14 @@
4346
"devDependencies": {
4447
"@types/mocha": "^5.2.7",
4548
"@types/node": "^12.6.9",
49+
"@types/pg": "^7.11.2",
50+
"@types/shimmer": "^1.0.1",
4651
"codecov": "^3.5.0",
47-
"gts": "^1.1.0",
52+
"gts": "^1.0.0",
4853
"mocha": "^6.2.0",
4954
"nyc": "^14.1.1",
5055
"rimraf": "^3.0.0",
56+
"pg": "^7.12.1",
5157
"tslint-microsoft-contrib": "^6.2.0",
5258
"tslint-consistent-codestyle": "^1.15.1",
5359
"ts-mocha": "^6.0.0",
@@ -57,6 +63,8 @@
5763
"dependencies": {
5864
"@opentelemetry/core": "^0.1.1",
5965
"@opentelemetry/node": "^0.1.1",
60-
"@opentelemetry/types": "^0.1.1"
66+
"@opentelemetry/tracing": "^0.1.1",
67+
"@opentelemetry/types": "^0.1.1",
68+
"shimmer": "^1.2.1"
6169
}
6270
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*!
2+
* Copyright 2019, OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
export enum AttributeNames {
18+
// required by https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-semantic-conventions.md#databases-client-calls
19+
COMPONENT = 'component',
20+
DB_TYPE = 'db.type',
21+
DB_INSTANCE = 'db.instance',
22+
DB_STATEMENT = 'db.statement',
23+
PEER_ADDRESS = 'peer.address',
24+
PEER_HOSTNAME = 'peer.host',
25+
26+
// optional
27+
DB_USER = 'db.user',
28+
PEER_PORT = 'peer.port',
29+
PEER_IPV4 = 'peer.ipv4',
30+
PEER_IPV6 = 'peer.ipv6',
31+
PEER_SERVICE = 'peer.service',
32+
33+
// PG specific -- not specified by spec
34+
PG_VALUES = 'pg.values',
35+
PG_PLAN = 'pg.plan',
36+
}

packages/opentelemetry-plugin-postgres/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16+
17+
export * from './pg';
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*!
2+
* Copyright 2019, OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { BasePlugin } from '@opentelemetry/core';
18+
import { CanonicalCode, Span } from '@opentelemetry/types';
19+
import {
20+
PostgresPluginOptions,
21+
PgClientExtended,
22+
PgPluginQueryConfig,
23+
PostgresCallback,
24+
} from './types';
25+
import * as pgTypes from 'pg';
26+
import * as shimmer from 'shimmer';
27+
import * as utils from './utils';
28+
29+
export class PostgresPlugin extends BasePlugin<typeof pgTypes> {
30+
protected _config: PostgresPluginOptions;
31+
32+
static readonly COMPONENT = 'pg';
33+
static readonly DB_TYPE = 'sql';
34+
35+
static readonly BASE_SPAN_NAME = PostgresPlugin.COMPONENT + '.query';
36+
37+
readonly supportedVersions = ['7.*'];
38+
39+
constructor(readonly moduleName: string) {
40+
super();
41+
this._config = {};
42+
}
43+
44+
protected patch(): typeof pgTypes {
45+
if (this._moduleExports.Client.prototype.query) {
46+
shimmer.wrap(
47+
this._moduleExports.Client.prototype,
48+
'query',
49+
this._getClientQueryPatch() as never
50+
);
51+
}
52+
return this._moduleExports;
53+
}
54+
55+
protected unpatch(): void {
56+
if (this._moduleExports.Client.prototype.query) {
57+
shimmer.unwrap(this._moduleExports.Client.prototype, 'query');
58+
}
59+
}
60+
61+
private _getClientQueryPatch() {
62+
const plugin = this;
63+
return (original: typeof pgTypes.Client.prototype.query) => {
64+
plugin._logger.debug(
65+
`Patching ${PostgresPlugin.COMPONENT}.Client.prototype.query`
66+
);
67+
return function query(
68+
this: pgTypes.Client & PgClientExtended,
69+
...args: unknown[]
70+
) {
71+
let span: Span;
72+
73+
// Handle different client.query(...) signatures
74+
if (typeof args[0] === 'string') {
75+
if (args.length > 1 && args[1] instanceof Array) {
76+
span = utils.handleParameterizedQuery.call(
77+
this,
78+
plugin._tracer,
79+
...args
80+
);
81+
} else {
82+
span = utils.handleTextQuery.call(this, plugin._tracer, ...args);
83+
}
84+
} else if (typeof args[0] === 'object') {
85+
span = utils.handleConfigQuery.call(this, plugin._tracer, ...args);
86+
} else {
87+
return utils.handleInvalidQuery.call(
88+
this,
89+
plugin._tracer,
90+
original,
91+
...args
92+
);
93+
}
94+
95+
// Bind callback to parent span
96+
if (args.length > 0) {
97+
const parentSpan = plugin._tracer.getCurrentSpan();
98+
if (typeof args[args.length - 1] === 'function') {
99+
// Patch ParameterQuery callback
100+
args[args.length - 1] = utils.patchCallback(span, args[
101+
args.length - 1
102+
] as PostgresCallback);
103+
// If a parent span exists, bind the callback
104+
if (parentSpan) {
105+
args[args.length - 1] = plugin._tracer.bind(
106+
args[args.length - 1]
107+
);
108+
}
109+
} else if (
110+
typeof (args[0] as PgPluginQueryConfig).callback === 'function'
111+
) {
112+
// Patch ConfigQuery callback
113+
let callback = utils.patchCallback(
114+
span,
115+
(args[0] as PgPluginQueryConfig).callback!
116+
);
117+
// If a parent span existed, bind the callback
118+
if (parentSpan) {
119+
callback = plugin._tracer.bind(callback);
120+
}
121+
122+
// Copy the callback instead of writing to args.callback so that we don't modify user's
123+
// original callback reference
124+
args[0] = { ...(args[0] as object), callback };
125+
}
126+
}
127+
128+
// Perform the original query
129+
const result: unknown = original.apply(this, args as never);
130+
131+
// Bind promise to parent span and end the span
132+
if (result instanceof Promise) {
133+
return result
134+
.then((result: unknown) => {
135+
// Return a pass-along promise which ends the span and then goes to user's orig resolvers
136+
return new Promise((resolve, _) => {
137+
span.setStatus({ code: CanonicalCode.OK });
138+
span.end();
139+
resolve(result);
140+
});
141+
})
142+
.catch((error: Error) => {
143+
return new Promise((_, reject) => {
144+
span.setStatus({
145+
code: CanonicalCode.UNKNOWN,
146+
message: error.message,
147+
});
148+
span.end();
149+
reject(error);
150+
});
151+
});
152+
}
153+
154+
// else returns void
155+
return result; // void
156+
};
157+
};
158+
}
159+
}
160+
161+
export const plugin = new PostgresPlugin(PostgresPlugin.COMPONENT);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*!
2+
* Copyright 2019, OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as pgTypes from 'pg';
18+
19+
export interface PostgresPluginOptions {}
20+
21+
export type PostgresCallback = (err: Error, res: object) => unknown;
22+
23+
// These are not included in @types/pg, so manually define them.
24+
// https://github.com/brianc/node-postgres/blob/fde5ec586e49258dfc4a2fcd861fcdecb4794fc3/lib/client.js#L25
25+
export interface PgClientConnectionParams {
26+
database: string;
27+
host: string;
28+
port: number;
29+
user: string;
30+
}
31+
32+
export interface PgClientExtended {
33+
connectionParameters: PgClientConnectionParams;
34+
}
35+
36+
export interface PgPluginQueryConfig extends pgTypes.QueryConfig {
37+
callback?: PostgresCallback;
38+
}

0 commit comments

Comments
 (0)