-
Notifications
You must be signed in to change notification settings - Fork 824
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feature(plugin): implement postgres plugin #417
Changes from 6 commits
0212dea
62ecb39
f8d885b
3ed0cf3
9b90fde
c14b33f
dd1a7d3
7a854fe
eafdb3d
7504ba7
9316ff5
bfb19d3
64a2a8b
10fd6ca
d81e4b0
297e699
208d3a9
f986476
644f5f1
b3a83b4
ccefe01
fefb04e
4e24474
0279784
bbdc02d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -7,7 +7,8 @@ | |||||
"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'", | ||||||
"debug": "ts-mocha --inspect-brk --no-timeouts -p tsconfig.json 'test/**/*.test.ts'", | ||||||
markwolff marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
"tdd": "yarn test -- --watch-extensions ts --watch", | ||||||
"clean": "rimraf build/*", | ||||||
"check": "gts check", | ||||||
|
@@ -42,11 +43,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", | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please use |
||||||
"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", | ||||||
|
@@ -56,6 +60,8 @@ | |||||
"dependencies": { | ||||||
"@opentelemetry/core": "^0.1.0", | ||||||
"@opentelemetry/node": "^0.1.0", | ||||||
"@opentelemetry/types": "^0.1.0" | ||||||
"@opentelemetry/tracing": "^0.1.0", | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
"@opentelemetry/types": "^0.1.0", | ||||||
"shimmer": "^1.2.1" | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change | |||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,24 @@ | |||||||||||||||||||||||||||||||||||||||||
/*! | |||||||||||||||||||||||||||||||||||||||||
* 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 { | |||||||||||||||||||||||||||||||||||||||||
COMPONENT = 'component', | |||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this a standard label in the spec? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like the spec calls for:
For database client calls, peer information can be populated and interpreted as
|
|||||||||||||||||||||||||||||||||||||||||
PG_HOST = 'pg.host', | |||||||||||||||||||||||||||||||||||||||||
PG_PORT = 'pg.port', | |||||||||||||||||||||||||||||||||||||||||
PG_TEXT = 'pg.text', | |||||||||||||||||||||||||||||||||||||||||
PG_VALUES = 'pg.values', | |||||||||||||||||||||||||||||||||||||||||
PG_PLAN = 'pg.plan', | |||||||||||||||||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
/*! | ||
* 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 { SpanKind } from '@opentelemetry/types'; | ||
import { AttributeNames } from './enums'; | ||
import { PostgresCallback, PostgresPluginOptions } from './types'; | ||
import * as path from 'path'; | ||
import * as pgTypes from 'pg'; | ||
import * as shimmer from 'shimmer'; | ||
|
||
export class PostgresPlugin extends BasePlugin<typeof pgTypes> { | ||
static readonly component = 'pg'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be COMPONENT since it's immutable? Similar for SUPPORTED_VERIONS? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated to COMPONENT. SUPPORTED_VERSIONS is specified by |
||
readonly supportedVersions = ['^7.12.1']; | ||
markwolff marked this conversation as resolved.
Show resolved
Hide resolved
|
||
protected _config: PostgresPluginOptions; | ||
|
||
constructor(readonly moduleName: string, readonly version: 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, ...args: unknown[]) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function feels pretty complex. Could the different parts be broken up into helper functions? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree. Would rather see this split into: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Made a big refactor in dd1a7d3 to use these helper functions. They are outside of the class and add attributes onto the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. great change thanks |
||
// setup span | ||
let callbackProvided: boolean = | ||
args.length > 1 && typeof args[args.length - 1] === 'function'; | ||
const span = plugin._tracer.startSpan( | ||
`${PostgresPlugin.component}.query`, | ||
{ | ||
kind: SpanKind.CLIENT, | ||
parent: plugin._tracer.getCurrentSpan() || undefined, | ||
attributes: { | ||
[AttributeNames.COMPONENT]: PostgresPlugin.component, | ||
[AttributeNames.PG_HOST]: (this as any).connectionParameters.host, | ||
[AttributeNames.PG_PORT]: (this as any).connectionParameters.port, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should include
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, I think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated attribute name to |
||
}, | ||
} | ||
); | ||
|
||
try { | ||
if (typeof args[0] === 'string') { | ||
span.setAttribute(AttributeNames.PG_TEXT, args[0]); | ||
if (args[1] instanceof Array) { | ||
span.setAttribute(AttributeNames.PG_VALUES, args[1]); | ||
if (callbackProvided) { | ||
args[2] = plugin._tracer.bind(args[2]); | ||
} | ||
} else { | ||
if (callbackProvided) { | ||
args[1] = plugin._tracer.bind(args[1]); | ||
} | ||
} | ||
} else { | ||
const config = args[0] as pgTypes.QueryConfig & { | ||
callback?: PostgresCallback; | ||
}; | ||
if (typeof config.name === 'string') { | ||
span.setAttribute(AttributeNames.PG_PLAN, config.name); | ||
} else { | ||
if (typeof config.text === 'string') { | ||
span.setAttribute(AttributeNames.PG_TEXT, config.text); | ||
} | ||
if (config.values instanceof Array) { | ||
span.setAttribute(AttributeNames.PG_VALUES, config.values); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Although |
||
} | ||
} | ||
|
||
if (callbackProvided) { | ||
if (typeof args[1] === 'function') { | ||
args[1] = plugin._tracer.bind(args[1]); | ||
} else if (typeof args[2] === 'function') { | ||
args[2] = plugin._tracer.bind(args[2]); | ||
} | ||
} else if ( | ||
config.callback && | ||
typeof config.callback === 'function' | ||
) { | ||
callbackProvided = true; | ||
config.callback = plugin._tracer.bind(config.callback); | ||
} | ||
} | ||
} catch (e) { | ||
plugin._logger.warn( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it makes sense to open an issue on https://github.com/open-telemetry/opentelemetry-specification, about what statues to set in case of databases. Looks like most the status codes are applicable to |
||
`pg Plugin failed to trace query: error: ${e.message}` | ||
); | ||
const result = original.apply(this, arguments as any); | ||
span.end(); | ||
return result; | ||
} | ||
|
||
const queryResult = original.apply(this, args as any); | ||
|
||
// No callback was provided, return a promise instead (new as of [email protected]) | ||
if (!callbackProvided) { | ||
const queryResultPromise = (queryResult as unknown) as Promise< | ||
unknown | ||
>; | ||
return plugin._tracer.bind( | ||
markwolff marked this conversation as resolved.
Show resolved
Hide resolved
|
||
queryResultPromise | ||
.then((result: any) => { | ||
// Return a pass-along promise which ends the span and then goes to user's orig resolvers | ||
return new Promise((resolve, _) => { | ||
span.end(); | ||
resolve(result); | ||
}); | ||
}) | ||
.catch((error: Error) => { | ||
return new Promise((_, reject) => { | ||
span.end(); | ||
reject(error); | ||
}); | ||
}) | ||
); | ||
} | ||
|
||
// Else a callback was provided, so just return the result | ||
span.end(); | ||
return queryResult; | ||
}; | ||
}; | ||
} | ||
} | ||
|
||
const basedir = path.dirname(require.resolve('pg')); | ||
const version = require(path.join(basedir, '../', 'package.json')).version; | ||
markwolff marked this conversation as resolved.
Show resolved
Hide resolved
|
||
export const plugin = new PostgresPlugin(PostgresPlugin.component, version); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
/*! | ||
* 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 interface PostgresPluginOptions {} | ||
|
||
export type PostgresCallback = (err: Error, res: object) => unknown; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
/*! | ||
* 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, | ||
TimedEvent, | ||
} 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 = ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would this function be useful to other plugins? |
||
span: ReadableSpan, | ||
kind: SpanKind, | ||
attributes: Attributes, | ||
events: Event[] | ||
) => { | ||
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.strictEqual( | ||
Object.keys(span.attributes).length, | ||
Object.keys(attributes).length, | ||
'Should contain same number of attributes' | ||
); | ||
Object.keys(span.attributes).forEach(attribute => { | ||
assert.deepStrictEqual(span.attributes[attribute], attributes[attribute]); | ||
}); | ||
|
||
// events | ||
assert.strictEqual( | ||
span.events.length, | ||
events.length, | ||
'Should contain same number of events' | ||
); | ||
span.events.forEach((_: TimedEvent, index: number) => { | ||
assert.deepStrictEqual(span.events[index], events[index]); | ||
}); | ||
}; | ||
|
||
// 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); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be more descriptive, something like
RUN_POSTGRES_TESTS
WDYT?