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

feat: add mysql2 responsehook #915

Merged
merged 5 commits into from
Feb 27, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions plugins/node/opentelemetry-instrumentation-mysql2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ registerInstrumentations({
})
```

### MySQL2 Instrumentation Options

You can set the following instrumentation options:

| Options | Type | Description |
| ------- | ---- | ----------- |
| `responseHook` | `MySQL2InstrumentationExecutionResponseHook` (function) | Function for adding custom attributes from db response |

## Useful links

- For more information on OpenTelemetry, visit: <https://opentelemetry.io/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
InstrumentationBase,
InstrumentationNodeModuleDefinition,
isWrapped,
safeExecuteInTheMiddle,
} from '@opentelemetry/instrumentation';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
import type * as mysqlTypes from 'mysql2';
Expand Down Expand Up @@ -107,13 +108,29 @@ export class MySQL2Instrumentation extends InstrumentationBase<
),
},
});
const endSpan = once((err?: any) => {
const endSpan = once((err?: any, results?: any) => {
if (err) {
span.setStatus({
code: api.SpanStatusCode.ERROR,
message: err.message,
});
} else {
const config: MySQL2InstrumentationConfig = thisPlugin._config;
if (typeof config.responseHook === 'function') {
safeExecuteInTheMiddle(
() => {
config.responseHook!(span, results);
},
err => {
if (err) {
thisPlugin._diag.warn('Failed executing responseHook', err);
}
},
true
);
}
}

span.end();
});

Expand All @@ -136,8 +153,8 @@ export class MySQL2Instrumentation extends InstrumentationBase<
.once('error', err => {
endSpan(err);
})
.once('result', () => {
endSpan();
.once('result', results => {
endSpan(undefined, results);
});

return streamableQuery;
Expand Down Expand Up @@ -169,7 +186,7 @@ export class MySQL2Instrumentation extends InstrumentationBase<
results?: any,
fields?: mysqlTypes.FieldPacket[]
) {
endSpan(err);
endSpan(err, results);
return originalCallback(...arguments);
};
};
Expand Down
15 changes: 14 additions & 1 deletion plugins/node/opentelemetry-instrumentation-mysql2/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,18 @@
*/

import { InstrumentationConfig } from '@opentelemetry/instrumentation';
import type { Span } from '@opentelemetry/api';

export type MySQL2InstrumentationConfig = InstrumentationConfig;
export interface MySQL2InstrumentationExecutionResponseHook {
(span: Span, queryResults: any): void;
blumamir marked this conversation as resolved.
Show resolved Hide resolved
}

export interface MySQL2InstrumentationConfig extends InstrumentationConfig {
/**
* Hook that allows adding custom span attributes based on the data
* returned MySQL2 queries.
*
* @default undefined
*/
responseHook?: MySQL2InstrumentationExecutionResponseHook;
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
SimpleSpanProcessor,
} from '@opentelemetry/sdk-trace-base';
import * as assert from 'assert';
import { MySQL2Instrumentation } from '../src';
import { MySQL2Instrumentation, MySQL2InstrumentationConfig } from '../src';

const LIB_VERSION = testUtils.getPackageVersion('mysql2');
const port = Number(process.env.MYSQL_PORT) || 33306;
Expand Down Expand Up @@ -644,6 +644,126 @@ describe('mysql@2.x', () => {
);
});
});

describe('#responseHook', () => {
const queryResultAttribute = 'query_result';

after(() => {
instrumentation.setConfig({});
});

describe('invalid repsonse hook', () => {
before(() => {
instrumentation.disable();
instrumentation.setTracerProvider(provider);
const config: MySQL2InstrumentationConfig = {
responseHook: (span, queryResults) => {
throw new Error('random failure!');
},
};
instrumentation.setConfig(config);
instrumentation.enable();
});

it('should not affect the behavior of the query', done => {
const span = provider.getTracer('default').startSpan('test span');
context.with(trace.setSpan(context.active(), span), () => {
const sql = 'SELECT 1+1 as solution';
connection.query(sql, (err, res: mysqlTypes.RowDataPacket[]) => {
assert.ifError(err);
assert.ok(res);
assert.strictEqual(res[0].solution, 2);
done();
});
});
});
});

describe('valid response hook', () => {
before(() => {
instrumentation.disable();
instrumentation.setTracerProvider(provider);
const config: MySQL2InstrumentationConfig = {
responseHook: (span, queryResults) => {
span.setAttribute(
queryResultAttribute,
JSON.stringify(queryResults)
);
},
};
instrumentation.setConfig(config);
instrumentation.enable();
});

it('should extract data from responseHook - connection', done => {
const span = provider.getTracer('default').startSpan('test span');
context.with(trace.setSpan(context.active(), span), () => {
const sql = 'SELECT 1+1 as solution';
connection.query(sql, (err, res: mysqlTypes.RowDataPacket[]) => {
assert.ifError(err);
assert.ok(res);
assert.strictEqual(res[0].solution, 2);
const spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 1);
assertSpan(spans[0], sql);
assert.strictEqual(
spans[0].attributes[queryResultAttribute],
JSON.stringify(res)
);
done();
});
});
});

it('should extract data from responseHook - pool', done => {
const span = provider.getTracer('default').startSpan('test span');
context.with(trace.setSpan(context.active(), span), () => {
const sql = 'SELECT 1+1 as solution';
pool.getConnection((err, conn) => {
conn.query(sql, (err, res: mysqlTypes.RowDataPacket[]) => {
assert.ifError(err);
assert.ok(res);
assert.strictEqual(res[0].solution, 2);
const spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 1);
assertSpan(spans[0], sql);
assert.strictEqual(
spans[0].attributes[queryResultAttribute],
JSON.stringify(res)
);
done();
});
});
});
});

it('should extract data from responseHook - poolCluster', done => {
poolCluster.getConnection((err, poolClusterConnection) => {
assert.ifError(err);
const span = provider.getTracer('default').startSpan('test span');
context.with(trace.setSpan(context.active(), span), () => {
const sql = 'SELECT 1+1 as solution';
poolClusterConnection.query(
sql,
(err, res: mysqlTypes.RowDataPacket[]) => {
assert.ifError(err);
assert.ok(res);
assert.strictEqual(res[0].solution, 2);
const spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 1);
assertSpan(spans[0], sql);
assert.strictEqual(
spans[0].attributes[queryResultAttribute],
JSON.stringify(res)
);
done();
}
);
});
});
});
});
});
});

function assertSpan(
Expand Down