-
Notifications
You must be signed in to change notification settings - Fork 823
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
feature(plugin): implement postgres plugin #417
Conversation
*/ | ||
|
||
export enum AttributeNames { | ||
COMPONENT = 'component', |
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.
Is this a standard label in the spec?
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.
Looks like the spec calls for:
Attribute name | Notes and examples | Required? |
---|---|---|
component |
Database driver name or database name (when known) "JDBI" , "jdbc" , "odbc" , "postgreSQL" . |
Yes |
db.type |
Database type. For any SQL database, "sql" . For others, the lower-case database category, e.g. "cassandra" , "hbase" , or "redis" . |
Yes |
db.instance |
Database instance name. E.g., In java, if the jdbc.url="jdbc:mysql://db.example.com:3306/customers" , the instance name is "customers" . |
Yes |
db.statement |
A database statement for the given database type. Note, that the value may be sanitized to exclude sensitive information. E.g., for db.type="sql" , "SELECT * FROM wuser_table" ; for db.type="redis" , "SET mykey 'WuValue'" . |
Yes |
db.user |
Username for accessing database. E.g., "readonly_user" or "reporting_user" |
No |
For database client calls, peer information can be populated and interpreted as
follows:
Attribute name | Notes and examples | Required |
---|---|---|
peer.address |
JDBC substring like "mysql://db.example.com:3306" |
Yes |
peer.hostname |
Remote hostname. "db.example.com" |
Yes |
peer.ipv4 |
Remote IPv4 address as a . -separated tuple. E.g., "127.0.0.1" |
No |
peer.ipv6 |
Remote IPv6 address as a string of colon-separated 4-char hex tuples. E.g., "2001:0db8:85a3:0000:0000:8a2e:0370:7334" |
No |
peer.port |
Remote port. E.g., 80 (integer) |
No |
peer.service |
Remote service name. Can be database friendly name or "db.instance" |
No |
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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
Updated to COMPONENT. SUPPORTED_VERSIONS is specified by BasePlugin
interface, so it should be changed there too. Maybe in a separate PR?
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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
Agree. Would rather see this split into: _textQuery
_parameterizedQuery
and _configQuery
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.
Made a big refactor in dd1a7d3 to use these helper functions. They are outside of the class and add attributes onto the span
based on the query args (and update the span name). WDYT?
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.
great change thanks
} from '@opentelemetry/core'; | ||
import { AttributeNames } from '../src/enums'; | ||
|
||
export const assertSpan = ( |
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.
Would this function be useful to other plugins?
const testPostgresLocally = process.env.TEST_POSTGRES_LOCAL; // For local: spins up local postgres db via docker | ||
const shouldTest = testPostgres || testPostgresLocally; // Skips these tests if false (default) | ||
|
||
before(function(ready) { |
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.
Nit: should this be an arrow function (ready) => {
?
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.
Because I'm using this
as Mocha.Context
in the before
fn to skip the tests, its easier to use a function
. Is there a nicer way to feed mocha my intended this
with an arrow function?
So cool to see this happening! |
*/ | ||
|
||
export enum AttributeNames { | ||
COMPONENT = 'component', |
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.
Looks like the spec calls for:
Attribute name | Notes and examples | Required? |
---|---|---|
component |
Database driver name or database name (when known) "JDBI" , "jdbc" , "odbc" , "postgreSQL" . |
Yes |
db.type |
Database type. For any SQL database, "sql" . For others, the lower-case database category, e.g. "cassandra" , "hbase" , or "redis" . |
Yes |
db.instance |
Database instance name. E.g., In java, if the jdbc.url="jdbc:mysql://db.example.com:3306/customers" , the instance name is "customers" . |
Yes |
db.statement |
A database statement for the given database type. Note, that the value may be sanitized to exclude sensitive information. E.g., for db.type="sql" , "SELECT * FROM wuser_table" ; for db.type="redis" , "SET mykey 'WuValue'" . |
Yes |
db.user |
Username for accessing database. E.g., "readonly_user" or "reporting_user" |
No |
For database client calls, peer information can be populated and interpreted as
follows:
Attribute name | Notes and examples | Required |
---|---|---|
peer.address |
JDBC substring like "mysql://db.example.com:3306" |
Yes |
peer.hostname |
Remote hostname. "db.example.com" |
Yes |
peer.ipv4 |
Remote IPv4 address as a . -separated tuple. E.g., "127.0.0.1" |
No |
peer.ipv6 |
Remote IPv6 address as a string of colon-separated 4-char hex tuples. E.g., "2001:0db8:85a3:0000:0000:8a2e:0370:7334" |
No |
peer.port |
Remote port. E.g., 80 (integer) |
No |
peer.service |
Remote service name. Can be database friendly name or "db.instance" |
No |
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Agree. Would rather see this split into: _textQuery
_parameterizedQuery
and _configQuery
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 is great work! Added a few comments.
.circleci/config.yml
Outdated
@@ -1,5 +1,11 @@ | |||
version: 2 | |||
|
|||
test_env: &test_env | |||
TEST_POSTGRES: 1 |
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?
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 comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should include pg.instance
and pg.user
. https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-semantic-conventions.md#databases-client-calls
const params = (this as any).connectionParameters;
attributes: {
[AttributeNames.COMPONENT]: PostgresPlugin.component,
[AttributeNames.PG_HOST]: params.host,
[AttributeNames.PG_PORT]: params.port,
[AttributeNames.PG_INSTANCE]: params.database,
[AttributeNames.PG_USER]: params.user,
}
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.
Also, I think PG_TEXT
equivalent to db.statement/pg.statement
? If yes, can we use pg.statement
as per the specs.
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.
Updated attribute name to pg.statement
. Still TODO: other attributes called for by spec
} | ||
} | ||
} catch (e) { | ||
plugin._logger.warn( |
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.
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 HTTP
and GRPC
requests.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Although setAttribute
accepts unknown
as a value, do you think we should flattern the array? Otherwise each exporterd needs to implement some logic to handle this.
Nice work! |
@@ -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 comment
The reason will be displayed to describe this comment to others. Learn more.
Please use gts
version ^1.0.0
in order to be consistent with other packages dependencies.
@@ -46,7 +46,7 @@ | |||
"@types/pg": "^7.11.2", | |||
"@types/shimmer": "^1.0.1", | |||
"codecov": "^3.5.0", | |||
"gts": "^1.1.0", | |||
"gts": "^1.0.0", |
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.
Why use the earlier version of gts
?
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.
AFAIK most of the places we are using 1.0.0
, (I think) this change is to make sure we are consistent everywhere. We can update the latest version (1.1.0
) in separate PR.
}); | ||
assert.strictEqual(res, undefined, 'No promise is returned'); | ||
}); | ||
// it('should not return a promise if callback is provided', done => { |
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.
Why is this code being commented out?
@@ -57,6 +61,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 comment
The reason will be displayed to describe this comment to others. Learn more.
"@opentelemetry/tracing": "^0.1.0", | |
"@opentelemetry/tracing": "^0.1.1", |
|
||
// Update span name with query command; prefer plan name, if available | ||
const queryCommand = _getCommandFromText(argsConfig.name || argsConfig.text); | ||
span.updateName(PostgresPlugin.BASE_SPAN_NAME + ':' + queryCommand); |
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.
It is highly discouraged to update the name of a Span
after its creation.
Based on specs:
The method name is called
UpdateName
to differentiate this method from the regular property setter. It emphasizes that this operation signifies a major change for aSpan
and may lead to re-calculation of sampling or filtering decisions made previously depending on the implementation. Alternatives for the name update may be lateSpan
creation, when Span is started with the explicit timestamp from the past at the moment where the final Span name is known, or reporting a Span with the desired name as a child Span.
Is it possible to avoid this? Do we need to update the name with query command?
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.
It can be avoided. The spec calls for DB spans to be named with a low cardinality name describing the query, e.g. insert
or select
. I've moved the span creation to the query handlers so that I can add this information to the span on creation. bfb19d3
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.
Nice! Thanks for doing this 👍
…elemetry-js into markwolff/impl-pg-plugin
@open-telemetry/node-approvers and @open-telemetry/node-maintainers Please review when you get a chance. |
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.
minor nits but otherwise LGTM
import * as pgTypes from 'pg'; | ||
import { PostgresPlugin } from './pg'; | ||
|
||
function arrayStringifyHelper<T>(arr: Array<T>): string { |
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.
nit: Does this need to be generic?
function arrayStringifyHelper<T>(arr: Array<T>): string { | |
function arrayStringifyHelper(arr: Array<unknown>): string { |
span: Span, | ||
cb: PostgresCallback | ||
): PostgresCallback { | ||
const originalCb = cb; |
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.
Why this additional variable? Can this line not be removed and on line 153 just cb.call(...
?
span.setStatus({ code: CanonicalCode.OK }); | ||
} | ||
span.end(); | ||
return originalCb.call(this, err, res); |
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.
Is there a reason to rename cb?
return originalCb.call(this, err, res); | |
return cb.call(this, err, res); |
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.
Nice catch, looks like dead code. Addressed this and your previous comments
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.
Good work! Added few comments.
}) | ||
.catch((error: Error) => { | ||
return new Promise((_, reject) => { | ||
span.setStatus({ code: CanonicalCode.UNKNOWN }); |
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.
Should we set something like span.setStatus({ code: CanonicalCode.UNKNOWN, message: error.message });
@@ -23,6 +23,10 @@ const opentelemetry = require('@opentelemetry/plugin-postgres'); | |||
// TODO: DEMONSTRATE API | |||
``` | |||
|
|||
## Supported Versions | |||
|
|||
- [pg](https://npmjs.com/package/pg): `7.x` |
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.
supported version is currently ^7.12.1
which is different. Please fix here or supportedVersions
field
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.
👍 changed supportedVersions
to 7.*
); | ||
}); | ||
|
||
describe('#client.query(...)', () => { |
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.
Is it possible to add some tests arounds span.status
in order to make sure that status is set to Unknown
, INVALID_ARGUMENT
etc... ?
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.
Good call. I added some span status tests on each call to assertSpan
. I also removed my invalid argument statuses since it would be included in the error message anyways
Do you know why codecov comment doesn't appear ? |
Strange, looks like this happens once in a while (same with #466). But this is un-related to this PR, will merge it now. Maybe we should open an issue to address it separately. |
Bumps [eslint](https://github.com/eslint/eslint) from 3.19.0 to 4.18.2. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/master/CHANGELOG.md) - [Commits](eslint/eslint@v3.19.0...v4.18.2) Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: Valentin Marchaud <[email protected]>
Which problem is this PR solving?
Short description of the changes
client.query(...)
support. Postgres support will not be complete at this PR. There are also separate packages,pg-pool
andpg-cursor
, which are separate packages and would require separate plugins as well.PR TODO
span.status
should be set to.client.query({ plan })
.New TODOs
pg-pool
plugin (docs: https://node-postgres.com/api/pool)pg-cursor
plugin (docs: https://node-postgres.com/api/cursor)