Skip to content
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

Merged
merged 25 commits into from
Oct 30, 2019

Conversation

markwolff
Copy link
Member

@markwolff markwolff commented Oct 9, 2019

Which problem is this PR solving?

Short description of the changes

  • Adds client.query(...) support. Postgres support will not be complete at this PR. There are also separate packages, pg-pool and pg-cursor, which are separate packages and would require separate plugins as well.

PR TODO

  • Determine what span.status should be set to.
  • End spans on queries with callbacks
  • Test client.query({ plan }).
  • Add additional attributes where appropriate

New TODOs

@markwolff markwolff changed the title Markwolff/impl pg plugin Feature(postgres): Implement Postgres plugin Oct 9, 2019
@markwolff markwolff changed the title Feature(postgres): Implement Postgres plugin feature(plugin): implement postgres plugin Oct 9, 2019
*/

export enum AttributeNames {
COMPONENT = 'component',
Copy link
Contributor

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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-semantic-conventions.md#databases-client-calls

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';
Copy link
Contributor

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?

Copy link
Member Author

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[]) {
Copy link
Contributor

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?

Copy link
Member

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

Copy link
Member Author

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?

Copy link
Member

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 = (
Copy link
Contributor

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) {
Copy link
Contributor

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) => {?

Copy link
Member Author

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?

@draffensperger
Copy link
Contributor

So cool to see this happening!

packages/opentelemetry-plugin-postgres/package.json Outdated Show resolved Hide resolved
*/

export enum AttributeNames {
COMPONENT = 'component',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-semantic-conventions.md#databases-client-calls

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[]) {
Copy link
Member

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

Copy link
Member

@mayurkale22 mayurkale22 left a 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.

@@ -1,5 +1,11 @@
version: 2

test_env: &test_env
TEST_POSTGRES: 1
Copy link
Member

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?

packages/opentelemetry-plugin-postgres/src/pg.ts Outdated Show resolved Hide resolved
attributes: {
[AttributeNames.COMPONENT]: PostgresPlugin.component,
[AttributeNames.PG_HOST]: (this as any).connectionParameters.host,
[AttributeNames.PG_PORT]: (this as any).connectionParameters.port,
Copy link
Member

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,
}

Copy link
Member

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.

Copy link
Member Author

@markwolff markwolff Oct 15, 2019

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(
Copy link
Member

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);
Copy link
Member

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.

packages/opentelemetry-plugin-postgres/test/pg.test.ts Outdated Show resolved Hide resolved
@OlivierAlbertini
Copy link
Member

Nice work!

packages/opentelemetry-plugin-postgres/src/pg.ts Outdated Show resolved Hide resolved
@@ -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",
Copy link
Member

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",
Copy link
Contributor

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?

Copy link
Member

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.

packages/opentelemetry-plugin-postgres/src/pg.ts Outdated Show resolved Hide resolved
packages/opentelemetry-plugin-postgres/src/utils.ts Outdated Show resolved Hide resolved
});
assert.strictEqual(res, undefined, 'No promise is returned');
});
// it('should not return a promise if callback is provided', done => {
Copy link
Contributor

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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"@opentelemetry/tracing": "^0.1.0",
"@opentelemetry/tracing": "^0.1.1",

packages/opentelemetry-plugin-postgres/src/utils.ts Outdated Show resolved Hide resolved

// 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);
Copy link
Member

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 a Span and may lead to re-calculation of sampling or filtering decisions made previously depending on the implementation. Alternatives for the name update may be late Span 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?

Copy link
Member Author

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

Copy link
Member

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 👍

@mayurkale22
Copy link
Member

@open-telemetry/node-approvers and @open-telemetry/node-maintainers Please review when you get a chance.

Copy link
Member

@dyladan dyladan left a 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 {
Copy link
Member

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?

Suggested change
function arrayStringifyHelper<T>(arr: Array<T>): string {
function arrayStringifyHelper(arr: Array<unknown>): string {

span: Span,
cb: PostgresCallback
): PostgresCallback {
const originalCb = cb;
Copy link
Member

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);
Copy link
Member

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?

Suggested change
return originalCb.call(this, err, res);
return cb.call(this, err, res);

Copy link
Member Author

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

Copy link
Member

@OlivierAlbertini OlivierAlbertini left a 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 });
Copy link
Member

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`
Copy link
Member

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

Copy link
Member Author

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(...)', () => {
Copy link
Member

@OlivierAlbertini OlivierAlbertini Oct 28, 2019

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... ?

Copy link
Member Author

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

@OlivierAlbertini
Copy link
Member

Do you know why codecov comment doesn't appear ?

@mayurkale22
Copy link
Member

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.

@mayurkale22 mayurkale22 merged commit 5c49c6c into open-telemetry:master Oct 30, 2019
lukaswelinder pushed a commit to agile-pm/opentelemetry-js that referenced this pull request Jul 24, 2020
pichlermarc pushed a commit to dynatrace-oss-contrib/opentelemetry-js that referenced this pull request Dec 15, 2023
Co-authored-by: Valentin Marchaud <thisismac47@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Plugin]: Add postgres plugin
6 participants