Skip to content

Commit 0e873a7

Browse files
authored
nodejs/knex: add support for OpenTelemetry as a trace provider(google#15)
Added OpenTelemetry as a Trace provider
1 parent ab0fbbc commit 0e873a7

10 files changed

Lines changed: 2315 additions & 690 deletions

File tree

nodejs/sqlcommenter-nodejs/packages/sqlcommenter-knex/index.js

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
const {tracer} = require('@opencensus/nodejs');
16-
const {hasComment, toW3CTraceContext} = require('./util');
15+
const {hasComment} = require('./util');
16+
const provider = require('./provider');
1717

1818
const defaultFields = {
1919
'route': true,
@@ -27,10 +27,12 @@ const defaultFields = {
2727
* the commenter output
2828
*
2929
* @param {Object} Knex
30-
* @param {Object} includes a map of values to be optionally included.
30+
* @param {Object} include - A map of values to be optionally included.
31+
* @param {Object} options - A configuration object specifying where to collect trace data from. Accepted fields are:
32+
* TraceProvider: Should be either 'OpenCensus' or 'OpenTelemetry', indicating which library to collect trace data from.
3133
* @return {void}
3234
*/
33-
exports.wrapMainKnex = (Knex, include={}) => {
35+
exports.wrapMainKnex = (Knex, include={}, options={}) => {
3436

3537
/* c8 ignore next 2 */
3638
if (Knex.___alreadySQLCommenterWrapped___)
@@ -73,10 +75,8 @@ exports.wrapMainKnex = (Knex, include={}) => {
7375
// Knex.__req__ = null;
7476
}
7577

76-
if (tracer.active) {
77-
// TODO: check if tracer.currentRootSpan can be null/undefined when tracer is active
78-
toW3CTraceContext(tracer.currentRootSpan, comments);
79-
}
78+
// Add trace context to comments, depending on the current provider.
79+
provider.attachComments(options.TraceProvider, comments);
8080

8181
const filtering = typeof include === 'object' && include && Object.keys(include).length > 0;
8282
// Filter out keys whose values are undefined or aren't to be included by default.
@@ -140,12 +140,14 @@ const getKnexVersion = (Knex) => {
140140
* only being included in the comment.
141141
*
142142
* @param {Object} Knex
143-
* @param {Object} include A map of variables to include. If unset, we'll use default attributes.
143+
* @param {Object} include - A map of variables to include. If unset, we'll use default attributes.
144+
* @param {Object} options - A configuration object specifying where to collect trace data from. Accepted fields are:
145+
* TraceProvider: Should be either 'OpenCensus' or 'OpenTelemetry', indicating which library to collect trace data from.
144146
* @return {Function} A middleware that is compatible with the express framework.
145147
*/
146-
exports.wrapMainKnexAsMiddleware = (Knex, include=null) => {
148+
exports.wrapMainKnexAsMiddleware = (Knex, include=null, options) => {
147149

148-
exports.wrapMainKnex(Knex, include);
150+
exports.wrapMainKnex(Knex, include, options);
149151

150152
return (req, res, next) => {
151153

nodejs/sqlcommenter-nodejs/packages/sqlcommenter-knex/package-lock.json

Lines changed: 2046 additions & 671 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nodejs/sqlcommenter-nodejs/packages/sqlcommenter-knex/package.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,20 @@
77
"test": "test"
88
},
99
"devDependencies": {
10-
"@opencensus/nodejs": "^0.0.15",
10+
"@opencensus/nodejs": "^0.0.22",
1111
"@opencensus/propagation-tracecontext": "^0.0.15",
12+
"@opentelemetry/api": "^0.10.1",
13+
"@opentelemetry/core": "^0.10.1",
14+
"@opentelemetry/node": "^0.10.2",
1215
"chai": "^4.2.0",
1316
"chai-http": "^4.3.0",
1417
"express": "^4.17.0",
1518
"knex": "^0.19.5",
16-
"mocha": "^6.1.4"
19+
"mocha": "^6.1.4",
20+
"rewiremock": "^3.14.3",
21+
"sequelize": "^6.3.3",
22+
"sinon": "^9.0.3",
23+
"sinon-chai": "^3.5.0"
1724
},
1825
"engines": {
1926
"node": ">=6.0.0"
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright 2020 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
const OpenCensus = require('./opencensus');
16+
const OpenTelemetry = require('./opentelemetry');
17+
18+
const providers = {
19+
'opentelemetry': OpenTelemetry,
20+
'opencensus': OpenCensus,
21+
}
22+
23+
exports.attachComments = function attachComments(providerName, comments) {
24+
// Verify we have a comments object to modify
25+
if (!comments || typeof comments !== 'object') return;
26+
27+
// Lookup the provider by name, or use the default.
28+
let provider = providers[String(providerName).toLowerCase()] || OpenCensus;
29+
provider.addW3CTraceContext(comments);
30+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright 2020 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
const {tracer} = require('@opencensus/nodejs');
16+
const {toW3CTraceContext} = require('../util');
17+
18+
exports.addW3CTraceContext = function(comments) {
19+
if (tracer.active) {
20+
toW3CTraceContext(tracer.currentRootSpan, comments);
21+
}
22+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright 2020 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
const {context, defaultSetter} = require('@opentelemetry/api');
16+
const {HttpTraceContext} = require('@opentelemetry/core')
17+
18+
exports.addW3CTraceContext = function(comments) {
19+
let propagator = new HttpTraceContext();
20+
propagator.inject(context.active(), comments, defaultSetter);
21+
};

nodejs/sqlcommenter-nodejs/packages/sqlcommenter-knex/test/comment.test.js

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,13 @@
1515
"use strict";
1616

1717
const {wrapMainKnex} = require('../index');
18-
const tracing = require('@opencensus/nodejs');
18+
const opencensus_tracing = require('@opencensus/nodejs');
1919
const chai = require("chai");
2020
const {fields} = require('../util');
21+
const {context, trace} = require('@opentelemetry/api');
22+
const {NodeTracerProvider} = require('@opentelemetry/node');
23+
const {AsyncHooksContextManager} = require('@opentelemetry/context-async-hooks');
24+
const {InMemorySpanExporter, SimpleSpanProcessor} = require('@opentelemetry/tracing');
2125

2226
const expect = chai.expect;
2327

@@ -112,7 +116,7 @@ describe("Comments for Knex", () => {
112116
});
113117

114118

115-
describe("With tracing", () => {
119+
describe("With OpenCensus tracing", () => {
116120

117121
let fakeKnex = {
118122
Client: {
@@ -130,26 +134,79 @@ describe("With tracing", () => {
130134
};
131135

132136
before(() => {
133-
wrapMainKnex(fakeKnex, {db_driver: true, traceparent: true, tracestate: true});
137+
wrapMainKnex(fakeKnex, {db_driver: true, traceparent: true, tracestate: true}, {TraceProvider: "OpenCensus"});
134138
});
135139

136-
it('Starting a trace should produce `traceparent`', (done) => {
140+
it('Starting an OpenCensus trace should produce `traceparent`', (done) => {
137141
// Let's remember https://github.com/census-instrumentation/opencensus-node/issues/580
138142

139143
const traceOptions = {
140144
samplingRate: 1, // Always sample
141145
};
142-
const tracer = tracing.start(traceOptions).tracer;
146+
const tracer = opencensus_tracing.start(traceOptions).tracer;
143147

144148
tracer.startRootSpan({ name: 'with-tracing' }, rootSpan => {
145149
const obj = {sql: 'SELECT * FROM foo'};
146150
fakeKnex.Client.prototype.query(null, obj).then((got) => {
147151
const augmentedSQL = got.sql;
148152
const wantSQL = `SELECT * FROM foo /*db_driver='knex%3Afake%3A0.0.1',traceparent='00-${rootSpan.traceId}-${rootSpan.id}-01'*/`;
149153
expect(augmentedSQL).equals(wantSQL);
150-
tracing.tracer.stop();
154+
opencensus_tracing.tracer.stop();
151155
done();
152156
});
153157
});
154158
});
155159
});
160+
161+
describe("With OpenTelemetry tracing", () => {
162+
163+
let fakeKnex = {
164+
Client: {
165+
prototype: {
166+
config: { connection: { database: 'fake'}, client: 'fakesql'},
167+
version: 'fake-server:0.0.X',
168+
query: (conn, obj) => {
169+
return Promise.resolve(obj); // simply returns a resolved promise for inspection.
170+
}
171+
}
172+
},
173+
VERSION: () => {
174+
return 'fake:0.0.1';
175+
}
176+
};
177+
178+
// Load OpenTelemetry components
179+
const provider = new NodeTracerProvider();
180+
const memoryExporter = new InMemorySpanExporter();
181+
const spanProcessor = new SimpleSpanProcessor(memoryExporter);
182+
provider.addSpanProcessor(spanProcessor);
183+
const tracer = provider.getTracer('default');
184+
trace.setGlobalTracerProvider(provider);
185+
let contextManager;
186+
187+
before(() => {
188+
contextManager = new AsyncHooksContextManager();
189+
context.setGlobalContextManager(contextManager.enable());
190+
wrapMainKnex(fakeKnex, {db_driver: true, traceparent: true, tracestate: true}, {TraceProvider: "OpenTelemetry"});
191+
});
192+
193+
after(() => {
194+
memoryExporter.reset();
195+
context.disable();
196+
});
197+
198+
it('Starting an OpenTelemetry trace should produce `traceparent`', (done) => {
199+
const rootSpan = tracer.startSpan('rootSpan');
200+
201+
tracer.withSpan(rootSpan, async () => {
202+
const obj = {sql: 'SELECT * FROM foo'};
203+
fakeKnex.Client.prototype.query(null, obj).then((got) => {
204+
const augmentedSQL = got.sql;
205+
const wantSQL = `SELECT * FROM foo /*db_driver='knex%3Afake%3A0.0.1',traceparent='00-${rootSpan.context().traceId}-${rootSpan.context().spanId}-01'*/`;
206+
expect(augmentedSQL).equals(wantSQL);
207+
rootSpan.end();
208+
done();
209+
});
210+
});
211+
});
212+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright 2020 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// This module enables pre-configuring rewiremock for all test cases, rather than repeatedly configuring in each test
16+
17+
const rewiremock = require('rewiremock/node');
18+
// nothng more than `plugins.node`, but it might change how filename resolution works
19+
20+
rewiremock.overrideEntryPoint(module);
21+
module.exports = rewiremock;
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Copyright 2020 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
"use strict";
16+
17+
const chai = require('chai');
18+
const expect = chai.expect;
19+
const sinon = require('sinon');
20+
const sinonChai = require("sinon-chai");
21+
const rewiremock = require('../rewiremock');
22+
chai.use(sinonChai);
23+
24+
// Mock the trace provider implementations
25+
const openCensusMock = sinon.spy();
26+
const openTelemetryMock = sinon.spy();
27+
28+
// Mock the dependencies of provider to make use of faked methods for adding trace context
29+
rewiremock('../../provider/opencensus').with({addW3CTraceContext: openCensusMock});
30+
rewiremock('../../provider/opentelemetry').with({addW3CTraceContext: openTelemetryMock});
31+
32+
// Load the provider module with the appropriate mocks
33+
rewiremock.enable();
34+
const provider = require('../../provider');
35+
36+
// A helper method to test which provider was called
37+
const verifyProviderUsed = function(traceProvider, spy, used) {
38+
provider.attachComments(traceProvider, {});
39+
expect(spy.called).to.equal(used);
40+
};
41+
42+
describe("Provider", () => {
43+
describe("attachComment", () => {
44+
45+
beforeEach(() => {
46+
openCensusMock.resetHistory();
47+
openTelemetryMock.resetHistory();
48+
})
49+
50+
it("should default to OpenCensus when no options are provided", () => {
51+
verifyProviderUsed(undefined, openCensusMock, true);
52+
verifyProviderUsed(undefined, openTelemetryMock, false);
53+
});
54+
55+
it("should default to OpenCensus when null is provided", () => {
56+
verifyProviderUsed(null, openCensusMock, true);
57+
verifyProviderUsed(null, openTelemetryMock, false);
58+
});
59+
60+
it("should default to OpenCensus when invalid options are provided", () => {
61+
verifyProviderUsed("bad trace library name", openCensusMock, true);
62+
verifyProviderUsed("bad trace library name", openTelemetryMock, false);
63+
});
64+
65+
it("should use OpenCensus when the name is provided", () => {
66+
verifyProviderUsed("opencensus", openCensusMock, true);
67+
verifyProviderUsed("opencensus", openTelemetryMock, false);
68+
});
69+
70+
it("should accept an arbitrary capitalization of OpenCensus", () => {
71+
verifyProviderUsed("oPeNceNSus", openCensusMock, true);
72+
verifyProviderUsed("oPeNceNSus", openTelemetryMock, false);
73+
});
74+
75+
it("should use OpenTelemetry when the name is provided", () => {
76+
verifyProviderUsed("opentelemetry", openCensusMock, false);
77+
verifyProviderUsed("opentelemetry", openTelemetryMock, true);
78+
});
79+
80+
it("should accept an arbitrary capitalization of OpenTelemetry", () => {
81+
verifyProviderUsed("OpenTeleMetRY", openCensusMock, false);
82+
verifyProviderUsed("OpenTeleMetRY", openTelemetryMock, true);
83+
});
84+
85+
});
86+
});
87+
88+
// Unload module mocks
89+
rewiremock.disable();

nodejs/sqlcommenter-nodejs/packages/sqlcommenter-knex/util.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ const latestSpan = (span) => {
7878
* @return {void}
7979
*/
8080
exports.toW3CTraceContext = (span, dst) => {
81+
// TODO: check if tracer.currentRootSpan can be null/undefined when tracer is active
8182
const curSpan = latestSpan(span);
8283
if (!curSpan)
8384
return dst;

0 commit comments

Comments
 (0)