diff --git a/.circleci/config.yml b/.circleci/config.yml index 931821b9300..238c6427c9e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,5 +1,11 @@ version: 2 +test_env: &test_env + OPENCENSUS_MONGODB_TESTS: 1 + +mongo_service: &mongo_service + image: mongo + node_unit_tests: &node_unit_tests steps: - checkout @@ -68,18 +74,26 @@ jobs: node8: docker: - image: node:8 + environment: *test_env + - *mongo_service <<: *node_unit_tests node10: docker: - image: node:10 + environment: *test_env + - *mongo_service <<: *node_unit_tests node11: docker: - image: node:11 + environment: *test_env + - *mongo_service <<: *node_unit_tests node12: docker: - image: node:12 + environment: *test_env + - *mongo_service <<: *node_unit_tests node12-browsers: docker: diff --git a/packages/opentelemetry-node-tracer/src/NodeTracer.ts b/packages/opentelemetry-node-tracer/src/NodeTracer.ts index ea138470e5c..2befdf8ea67 100644 --- a/packages/opentelemetry-node-tracer/src/NodeTracer.ts +++ b/packages/opentelemetry-node-tracer/src/NodeTracer.ts @@ -14,8 +14,45 @@ * limitations under the License. */ -import { BasicTracer, BasicTracerConfig } from '@opentelemetry/basic-tracer'; +import { BasicTracer } from '@opentelemetry/basic-tracer'; import { AsyncHooksScopeManager } from '@opentelemetry/scope-async-hooks'; +import { ScopeManager } from '@opentelemetry/scope-base'; +import { + Attributes, + BinaryFormat, + HttpTextFormat, + Logger, + Sampler, +} from '@opentelemetry/types'; + +// @todo: Find a way to re-use BasicTracerConfig here +export declare interface NodeTracerConfig { + /** + * Binary formatter which can serialize/deserialize Spans. + */ + binaryFormat?: BinaryFormat; + /** + * Attributed that will be applied on every span created by Tracer. + * Useful to add infrastructure and environment information to your spans. + */ + defaultAttributes?: Attributes; + /** + * HTTP text formatter which can inject/extract Spans. + */ + httpTextFormat?: HttpTextFormat; + /** + * User provided logger. + */ + logger?: Logger; + /** + * Sampler determines if a span should be recorded or should be a NoopSpan. + */ + sampler?: Sampler; + /** + * Scope manager keeps context across in-process operations. + */ + scopeManager?: ScopeManager; +} /** * This class represents a node tracer with `async_hooks` module. @@ -24,7 +61,7 @@ export class NodeTracer extends BasicTracer { /** * Constructs a new Tracer instance. */ - constructor(config: BasicTracerConfig) { + constructor(config: NodeTracerConfig) { super( Object.assign({}, { scopeManager: new AsyncHooksScopeManager() }, config) ); diff --git a/packages/opentelemetry-node-tracer/src/index.ts b/packages/opentelemetry-node-tracer/src/index.ts index 62fb100a201..eaad585d142 100644 --- a/packages/opentelemetry-node-tracer/src/index.ts +++ b/packages/opentelemetry-node-tracer/src/index.ts @@ -13,3 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +export * from './NodeTracer'; +export { Span } from '@opentelemetry/basic-tracer'; diff --git a/packages/opentelemetry-plugin-mongodb/package.json b/packages/opentelemetry-plugin-mongodb/package.json index 03b5cc104aa..d8bcc033eb3 100644 --- a/packages/opentelemetry-plugin-mongodb/package.json +++ b/packages/opentelemetry-plugin-mongodb/package.json @@ -6,7 +6,7 @@ "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/*.ts", "tdd": "yarn test -- --watch-extensions ts --watch", "clean": "rimraf build/*", "check": "gts check", @@ -45,6 +45,7 @@ "codecov": "^3.5.0", "gts": "^1.1.0", "mocha": "^6.2.0", + "mongodb": "^3.3.0", "nyc": "^14.1.1", "ts-mocha": "^6.0.0", "ts-node": "^8.3.0", diff --git a/packages/opentelemetry-plugin-mongodb/src/index.ts b/packages/opentelemetry-plugin-mongodb/src/index.ts index 62fb100a201..734114e7779 100644 --- a/packages/opentelemetry-plugin-mongodb/src/index.ts +++ b/packages/opentelemetry-plugin-mongodb/src/index.ts @@ -13,3 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +export * from './mongodb'; diff --git a/packages/opentelemetry-plugin-mongodb/src/mongodb.ts b/packages/opentelemetry-plugin-mongodb/src/mongodb.ts index 276fa9a6987..fc11966014b 100644 --- a/packages/opentelemetry-plugin-mongodb/src/mongodb.ts +++ b/packages/opentelemetry-plugin-mongodb/src/mongodb.ts @@ -37,9 +37,9 @@ interface MongoInternalCommand { ismaster: boolean; } -/** MongoDB instrumentation plugin for Opencensus */ +/** MongoDB instrumentation plugin for OpenTelemetry */ export class MongoDBPlugin extends BasePlugin { - private readonly SERVER_FNS = ['insert', 'update', 'remove', 'auth']; + private readonly SERVER_FNS = ['insert', 'update', 'remove']; private readonly CURSOR_FNS_FIRST = ['_find', '_getmore']; protected _logger!: Logger; protected readonly _tracer!: Tracer; diff --git a/packages/opentelemetry-plugin-mongodb/test/mongodb.ts b/packages/opentelemetry-plugin-mongodb/test/mongodb.ts new file mode 100644 index 00000000000..c98be9ce98c --- /dev/null +++ b/packages/opentelemetry-plugin-mongodb/test/mongodb.ts @@ -0,0 +1,273 @@ +/** + * Copyright 2018, OpenCensus 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 + * + * http://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 { Span } from '@opentelemetry/node-tracer'; +import * as assert from 'assert'; +import * as mongodb from 'mongodb'; +import { plugin } from '../src/'; +import { ProxyTracer } from './utils/ProxyTracer'; +import { SpanKind } from '@opentelemetry/types'; + +export interface MongoDBAccess { + client: mongodb.MongoClient; + collection: mongodb.Collection; +} + +/** + * Access the mongodb collection. + * @param url The mongodb URL to access. + * @param dbName The mongodb database name. + * @param collectionName The mongodb collection name. + */ +function accessCollection( + url: string, + dbName: string, + collectionName: string +): Promise { + return new Promise((resolve, reject) => { + mongodb.MongoClient.connect(url, function connectedClient(err, client) { + if (err) { + reject(err); + return; + } + const db = client.db(dbName); + const collection = db.collection(collectionName); + resolve({ client, collection }); + }); + }); +} + +/** + * Asserts root spans attributes. + * @param rootSpanVerifier An instance of rootSpanVerifier to analyse RootSpan + * instances from. + * @param expectedName The expected name of the first root span. + * @param expectedKind The expected kind of the first root span. + */ +function assertSpan( + spans: Span[], + expectedName: string, + expectedKind: SpanKind +) { + assert.strictEqual(spans.length, 2); + spans.forEach(span => { + assert.deepStrictEqual(typeof span.endTime, 'number'); + }); + assert.strictEqual(spans[1].name, expectedName); + assert.strictEqual(spans[1].kind, expectedKind); +} + +describe('MongoDBPlugin', () => { + // For these tests, mongo must be running. Add OPENTELEMETRY_MONGODB_TESTS to run + // these tests. + const OPENTELEMETRY_MONGODB_TESTS = process.env + .OPENTELEMETRY_MONGODB_TESTS as string; + let shouldTest = true; + if (!OPENTELEMETRY_MONGODB_TESTS) { + console.log('Skipping test-mongodb. Run MongoDB to test'); + shouldTest = false; + } + + const URL = 'mongodb://localhost:27017'; + const DB_NAME = 'opentelemetry-tests'; + const COLLECTION_NAME = 'test'; + + const tracer = new ProxyTracer({}); + let client: mongodb.MongoClient; + let collection: mongodb.Collection; + + before(done => { + plugin.enable(mongodb, tracer); + accessCollection(URL, DB_NAME, COLLECTION_NAME) + .then(result => { + client = result.client; + collection = result.collection; + done(); + }) + .catch((err: Error) => { + console.log( + 'Skipping test-mongodb. Could not connect. Run MongoDB to test' + ); + shouldTest = false; + done(); + }); + }); + + beforeEach(function mongoBeforeEach(done) { + // Skiping all tests in beforeEach() is a workarround. Mocha does not work + // properly when skiping tests in before() on nested describe() calls. + // https://github.com/mochajs/mocha/issues/2819 + if (!shouldTest) { + this.skip(); + } + tracer.spans = []; + // Non traced insertion of basic data to perform tests + const insertData = [{ a: 1 }, { a: 2 }, { a: 3 }]; + collection.insertMany(insertData, (err, result) => { + done(); + }); + }); + + afterEach(done => { + collection.deleteOne({}, done); + }); + + after(() => { + if (client) { + client.close(); + } + }); + + /** Should intercept query */ + describe('Instrumenting query operations', () => { + it('should create a child span for insert', done => { + const insertData = [{ a: 1 }, { a: 2 }, { a: 3 }]; + + const span = tracer.startSpan(`insertRootSpan`); + collection.insertMany(insertData, (err, result) => { + span.end(); + assert.ifError(err); + + assert.strictEqual(tracer.spans.length, 2); + assertSpan( + tracer.spans, + `${DB_NAME}.${COLLECTION_NAME}.query`, + SpanKind.CLIENT + ); + done(); + }); + }); + + it('should create a child span for update', done => { + const span = tracer.startSpan('updateRootSpan'); + collection.updateOne({ a: 2 }, { $set: { b: 1 } }, (err, result) => { + span.end(); + assert.ifError(err); + + assert.strictEqual(tracer.spans.length, 2); + assertSpan( + tracer.spans, + `${DB_NAME}.${COLLECTION_NAME}.query`, + SpanKind.CLIENT + ); + done(); + }); + }); + + it('should create a child span for remove', done => { + const span = tracer.startSpan('removeRootSpan'); + collection.deleteOne({ a: 3 }, (err, result) => { + span.end(); + assert.ifError(err); + + assert.strictEqual(tracer.spans.length, 2); + assertSpan( + tracer.spans, + `${DB_NAME}.${COLLECTION_NAME}.query`, + SpanKind.CLIENT + ); + done(); + }); + }); + }); + + /** Should intercept cursor */ + describe('Instrumenting cursor operations', () => { + it('should create a child span for find', done => { + const span = tracer.startSpan('findRootSpan'); + collection.find({}).toArray((err, result) => { + span.end(); + assert.ifError(err); + + assert.strictEqual(tracer.spans.length, 2); + assertSpan( + tracer.spans, + `${DB_NAME}.${COLLECTION_NAME}.cursor`, + SpanKind.CLIENT + ); + done(); + }); + }); + }); + + /** Should intercept command */ + describe('Instrumenting command operations', () => { + it('should create a child span for create index', done => { + const span = tracer.startSpan('indexRootSpan'); + collection.createIndex({ a: 1 }, (err, result) => { + span.end(); + assert.ifError(err); + + assert.strictEqual(tracer.spans.length, 2); + assertSpan(tracer.spans, `${DB_NAME}.createIndexes`, SpanKind.CLIENT); + done(); + }); + }); + + it('should create a child span for count', done => { + const span = tracer.startSpan('countRootSpan'); + collection.count({ a: 1 }, (err, result) => { + span.end(); + assert.ifError(err); + + assert.strictEqual(tracer.spans.length, 2); + assertSpan(tracer.spans, `${DB_NAME}.count`, SpanKind.CLIENT); + done(); + }); + }); + }); + + /** Should intercept command */ + describe('Removing Instrumentation', () => { + it('should unpatch plugin', () => { + assert.doesNotThrow(() => { + plugin.unpatch(); + }); + }); + + it('should not create a child span for query', done => { + const insertData = [{ a: 1 }, { a: 2 }, { a: 3 }]; + + const span = tracer.startSpan('insertRootSpan'); + collection.insertMany(insertData, (err, result) => { + span.end(); + assert.ifError(err); + assert.strictEqual(tracer.spans.length, 1); + done(); + }); + }); + + it('should not create a child span for cursor', done => { + const span = tracer.startSpan('findRootSpan'); + collection.find({}).toArray((err, result) => { + span.end(); + assert.ifError(err); + assert.strictEqual(tracer.spans.length, 1); + done(); + }); + }); + + it('should not create a child span for command', done => { + const span = tracer.startSpan('indexRootSpan'); + collection.createIndex({ a: 1 }, (err, result) => { + span.end(); + assert.ifError(err); + assert.strictEqual(tracer.spans.length, 1); + done(); + }); + }); + }); +}); diff --git a/packages/opentelemetry-plugin-mongodb/test/utils/ProxyTracer.ts b/packages/opentelemetry-plugin-mongodb/test/utils/ProxyTracer.ts new file mode 100644 index 00000000000..3cc36a02bc3 --- /dev/null +++ b/packages/opentelemetry-plugin-mongodb/test/utils/ProxyTracer.ts @@ -0,0 +1,28 @@ +/** + * 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 { SpanOptions } from '@opentelemetry/types'; +import { Span, NodeTracer } from '@opentelemetry/node-tracer'; + +export class ProxyTracer extends NodeTracer { + spans: Span[] = []; + + startSpan(name: string, options?: SpanOptions): Span { + const span = super.startSpan(name, options) as Span; + this.spans.push(span); + return span; + } +}