diff --git a/README.md b/README.md index 2f9e595..a78aac3 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ Library | Plugin Name | [`PostgreSQL`](https://github.com/brianc/node-postgres) | `pg` | | [`pg-cursor`](https://github.com/brianc/node-postgres) | `pg-cursor` | | [`MongoDB`](https://github.com/mongodb/node-mongodb-native) | `mongodb` | +| [`Mongoose`](https://github.com/Automattic/mongoose) | `mongoose` | | [`RabbitMQ`](https://github.com/squaremo/amqp.node) | `amqplib` | ### Compatible Libraries @@ -88,7 +89,6 @@ Library | Underlying Plugin Name | [`request`](https://github.com/request/request) | `http` / `https` | | [`request-promise`](https://github.com/request/request-promise) | `http` / `https` | | [`koa`](https://github.com/koajs/koa) | `http` / `https` | -| [`mongoose`](https://github.com/Automattic/mongoose) | `mongodb` | ## Contact Us * Submit [an issue](https://github.com/apache/skywalking/issues/new) by using [Nodejs] as title prefix. diff --git a/package-lock.json b/package-lock.json index 124737b..62a9e49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -879,6 +879,15 @@ "@types/node": "*" } }, + "@types/bson": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.3.tgz", + "integrity": "sha512-mVRvYnTOZJz3ccpxhr3wgxVmSeiYinW+zlzQz3SXWaJmD1DuL05Jeq7nKw3SnbKmbleW5qrLG5vdyWe/A9sXhw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/bytebuffer": { "version": "5.0.42", "resolved": "https://registry.npmjs.org/@types/bytebuffer/-/bytebuffer-5.0.42.tgz", @@ -995,6 +1004,16 @@ "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", "dev": true }, + "@types/mongodb": { + "version": "3.6.10", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.6.10.tgz", + "integrity": "sha512-BkwAHFiZSSWdTIqbUVGmgvIsiXXjqAketeK7Izy7oSs6G3N8Bn993tK9eq6QEovQDx6OQ2FGP2KWDDxBzdlJ6Q==", + "dev": true, + "requires": { + "@types/bson": "*", + "@types/node": "*" + } + }, "@types/node": { "version": "14.14.36", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.36.tgz", @@ -5107,6 +5126,12 @@ "verror": "1.10.0" } }, + "kareem": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.2.tgz", + "integrity": "sha512-STHz9P7X2L4Kwn72fA4rGyqyXdmrMSdxqHx9IXon/FXluXieaFA6KJ2upcHAHxQPQ0LeM/OjLrhFxifHewOALQ==", + "dev": true + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -5551,6 +5576,88 @@ "saslprep": "^1.0.0" } }, + "mongoose": { + "version": "5.12.2", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.12.2.tgz", + "integrity": "sha512-kT9t6Nvu9WPsfssn7Gzke446Il8UdMilY7Sa5vALtwoOoNOGtZEVjekZBFwsBFzTWtBA/x5gBmJoYFP+1LeDlg==", + "dev": true, + "requires": { + "@types/mongodb": "^3.5.27", + "bson": "^1.1.4", + "kareem": "2.3.2", + "mongodb": "3.6.5", + "mongoose-legacy-pluralize": "1.0.2", + "mpath": "0.8.3", + "mquery": "3.2.4", + "ms": "2.1.2", + "regexp-clone": "1.0.0", + "safe-buffer": "5.2.1", + "sift": "7.0.1", + "sliced": "1.0.1" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "mongoose-legacy-pluralize": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz", + "integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ==", + "dev": true + }, + "mpath": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.8.3.tgz", + "integrity": "sha512-eb9rRvhDltXVNL6Fxd2zM9D4vKBxjVVQNLNijlj7uoXUy19zNDsIif5zR+pWmPCWNKwAtqyo4JveQm4nfD5+eA==", + "dev": true + }, + "mquery": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.4.tgz", + "integrity": "sha512-uOLpp7iRX0BV1Uu6YpsqJ5b42LwYnmu0WeF/f8qgD/On3g0XDaQM6pfn0m6UxO6SM8DioZ9Bk6xxbWIGHm2zHg==", + "dev": true, + "requires": { + "bluebird": "3.5.1", + "debug": "3.1.0", + "regexp-clone": "^1.0.0", + "safe-buffer": "5.1.2", + "sliced": "1.0.1" + }, + "dependencies": { + "bluebird": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6422,6 +6529,12 @@ } } }, + "regexp-clone": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-1.0.0.tgz", + "integrity": "sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw==", + "dev": true + }, "relative": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/relative/-/relative-3.0.2.tgz", @@ -6854,6 +6967,12 @@ "dev": true, "optional": true }, + "sift": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/sift/-/sift-7.0.1.tgz", + "integrity": "sha512-oqD7PMJ+uO6jV9EQCl0LrRw1OwsiPsiFQR5AR30heR+4Dl7jBBbDLnNvWiak20tzZlSE1H7RB30SX/1j/YYT7g==", + "dev": true + }, "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", @@ -6879,6 +6998,12 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, + "sliced": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", + "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=", + "dev": true + }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", diff --git a/package.json b/package.json index 671b37d..7fccce8 100644 --- a/package.json +++ b/package.json @@ -47,10 +47,11 @@ "amqplib": "^0.7.0", "axios": "^0.21.0", "express": "^4.17.1", - "grpc-tools": "^1.10.0", "grpc_tools_node_protoc_ts": "^4.0.0", + "grpc-tools": "^1.10.0", "jest": "^26.6.3", "mongodb": "^3.6.4", + "mongoose": "^5.12.2", "mysql": "^2.18.1", "pg": "^8.5.1", "prettier": "^2.0.5", diff --git a/src/config/AgentConfig.ts b/src/config/AgentConfig.ts index 2b631b8..c92afea 100644 --- a/src/config/AgentConfig.ts +++ b/src/config/AgentConfig.ts @@ -40,7 +40,7 @@ export type AgentConfig = { export function finalizeConfig(config: AgentConfig): void { const escapeRegExp = (s: string) => s.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); - config.reDisablePlugins = RegExp(`^(?:${config.disablePlugins!.split(',').map((s) => escapeRegExp(s.trim()) + 'Plugin\\.js').join('|')})$`, 'i'); + config.reDisablePlugins = RegExp(`^(?:${config.disablePlugins!.split(',').map((s) => escapeRegExp(s.trim())).join('|')})$`, 'i'); const ignoreSuffix =`^.+(?:${config.ignoreSuffix!.split(',').map((s) => escapeRegExp(s.trim())).join('|')})$`; const ignorePath = '^(?:' + config.traceIgnorePath!.split(',').map( diff --git a/src/core/PluginInstaller.ts b/src/core/PluginInstaller.ts index c42dfcc..07f3e57 100644 --- a/src/core/PluginInstaller.ts +++ b/src/core/PluginInstaller.ts @@ -73,11 +73,13 @@ export default class PluginInstaller { }; }; + isPluginEnabled = (name: string): boolean => !name.match(config.reDisablePlugins); + install(): void { fs.readdirSync(this.pluginDir) .filter((file) => !(file.endsWith('.d.ts') || file.endsWith('.js.map'))) .forEach((file) => { - if (file.match(config.reDisablePlugins)) { + if (file.replace(/(?:Plugin)?\.js$/i, '').match(config.reDisablePlugins)) { logger.info(`Plugin ${file} not installed because it is disabled`); return; } diff --git a/src/plugins/MongoDBPlugin.ts b/src/plugins/MongoDBPlugin.ts index dae59f6..923a683 100644 --- a/src/plugins/MongoDBPlugin.ts +++ b/src/plugins/MongoDBPlugin.ts @@ -275,14 +275,13 @@ class MongoDBPlugin implements SwPlugin { return; Cls.prototype[operation] = function(...args: any[]) { - const spans = ContextManager.spans; - let span = spans[spans.length - 1]; + let span = ContextManager.currentSpan; // XXX: mongodb calls back into itself at this level in several places, for this reason we just do a normal call // if this is detected instead of opening a new span. This should not affect secondary db calls being recorded // from a cursor since this span is kept async until the cursor is closed, at which point it is stoppped. - if (span?.component === Component.MONGODB && (span as any).mongodbInCall) // mongodb has called into itself internally, span instanceof ExitSpan assumed + if ((span as any)?.mongodbInCall) // mongodb has called into itself internally return _original.apply(this, args); let host = '???'; @@ -297,7 +296,9 @@ class MongoDBPlugin implements SwPlugin { span.start(); try { - span.component = Component.MONGODB; + if (span.component === Component.UNKNOWN) // in case mongoose sitting on top + span.component = Component.MONGODB; + span.layer = SpanLayer.DATABASE; span.peer = host; diff --git a/src/plugins/MongoosePlugin.ts b/src/plugins/MongoosePlugin.ts new file mode 100644 index 0000000..d8f5fd2 --- /dev/null +++ b/src/plugins/MongoosePlugin.ts @@ -0,0 +1,155 @@ +/*! + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 SwPlugin, {wrapCallback, wrapPromise} from '../core/SwPlugin'; +import ContextManager from '../trace/context/ContextManager'; +import { Component } from '../trace/Component'; +import Tag from '../Tag'; +import { SpanLayer } from '../proto/language-agent/Tracing_pb'; +import PluginInstaller from '../core/PluginInstaller'; + +class MongoosePlugin implements SwPlugin { + readonly module = 'mongoose'; + readonly versions = '*'; + mongodbEnabled?: boolean; + + install(installer: PluginInstaller): void { + const {Model} = installer.require('mongoose'); + + this.interceptOperation(Model, 'aggregate'); + this.interceptOperation(Model, 'bulkWrite'); + this.interceptOperation(Model, 'cleanIndexes'); + this.interceptOperation(Model, 'count'); + this.interceptOperation(Model, 'countDocuments'); + this.interceptOperation(Model, 'create'); + this.interceptOperation(Model, 'createCollection'); + this.interceptOperation(Model, 'createIndexes'); + this.interceptOperation(Model, 'deleteMany'); + this.interceptOperation(Model, 'deleteOne'); + this.interceptOperation(Model, 'distinct'); + this.interceptOperation(Model, 'ensureIndexes'); + this.interceptOperation(Model, 'estimatedDocumentCount'); + this.interceptOperation(Model, 'exists'); + + this.interceptOperation(Model, 'find'); + this.interceptOperation(Model, 'findById'); + this.interceptOperation(Model, 'findByIdAndDelete'); + this.interceptOperation(Model, 'findByIdAndRemove'); + this.interceptOperation(Model, 'findByIdAndUpdate'); + this.interceptOperation(Model, 'findOne'); + this.interceptOperation(Model, 'findOneAndDelete'); + this.interceptOperation(Model, 'findOneAndRemove'); + this.interceptOperation(Model, 'findOneAndReplace'); + this.interceptOperation(Model, 'findOneAndUpdate'); + + this.interceptOperation(Model, 'geoSearch'); + this.interceptOperation(Model, 'insertMany'); + this.interceptOperation(Model, 'listIndexes'); + this.interceptOperation(Model, 'mapReduce'); + this.interceptOperation(Model, 'populate'); + this.interceptOperation(Model, 'remove'); + this.interceptOperation(Model, 'replaceOne'); + this.interceptOperation(Model, 'syncIndexes'); + this.interceptOperation(Model, 'update'); + this.interceptOperation(Model, 'updateMany'); + this.interceptOperation(Model, 'updateOne'); + this.interceptOperation(Model, 'validate'); + + this.interceptOperation(Model.prototype, 'delete'); + this.interceptOperation(Model.prototype, 'deleteOne'); + this.interceptOperation(Model.prototype, 'remove'); + this.interceptOperation(Model.prototype, 'save'); + + // TODO: + // discriminator? + // startSession? + // where? + + // NODO: + // hydrate + } + + interceptOperation(Container: any, operation: string): void { + const _original = Container[operation]; + + if (!_original) + return; + + Container[operation] = function() { + let span = ContextManager.currentSpan; + + if ((span as any)?.mongooseInCall) // mongoose has called into itself internally + return _original.apply(this, arguments); + + const host = `${this.db.host}:${this.db.port}`; + span = ContextManager.current.newExitSpan('Mongoose/' + operation, host, Component.MONGOOSE, Component.MONGODB); + + span.start(); + + try { + span.component = Component.MONGOOSE; + span.layer = SpanLayer.DATABASE; // mongodb may not actually be called so we set these here in case + span.peer = host; + + span.tag(Tag.dbType('MongoDB')); + span.tag(Tag.dbInstance(this.db.name)); + + const hasCB = typeof arguments[arguments.length - 1] === 'function'; + + if (hasCB) { + const wrappedCallback = wrapCallback(span, arguments[arguments.length - 1], 0); + + arguments[arguments.length - 1] = function() { // in case of immediate synchronous callback from mongoose + (span as any).mongooseInCall = false; + + wrappedCallback.apply(this, arguments as any); + }; + } + + (span as any).mongooseInCall = true; // if mongoose calls into itself while executing this operation then ignore it + let ret = _original.apply(this, arguments); + (span as any).mongooseInCall = false; + + if (!hasCB) { + if (ret && typeof ret.then === 'function') { // generic Promise check + ret = wrapPromise(span, ret); + + } else { // no callback passed in and no Promise or Cursor returned, play it safe + span.stop(); + + return ret; + } + } + + span.async(); + + return ret; + + } catch (err) { + span.error(err); + span.stop(); + + throw err; + } + }; + } +} + +// noinspection JSUnusedGlobalSymbols +export default new MongoosePlugin(); diff --git a/src/trace/Component.ts b/src/trace/Component.ts index 2a78e67..64d47bf 100644 --- a/src/trace/Component.ts +++ b/src/trace/Component.ts @@ -28,6 +28,7 @@ export class Component { static readonly RABBITMQ_CONSUMER = new Component(53); static readonly EXPRESS = new Component(4002); static readonly AXIOS = new Component(4005); + static readonly MONGOOSE = new Component(4006); constructor(public readonly id: number) {} } diff --git a/src/trace/context/Context.ts b/src/trace/context/Context.ts index af0b678..fdb6258 100644 --- a/src/trace/context/Context.ts +++ b/src/trace/context/Context.ts @@ -49,6 +49,4 @@ export default interface Context { /* This should be called upon entering the new async context for a span that has previously executed .async(), it should be the first thing the callback function belonging to the span does. */ resync(span: Span): void; - - currentSpan(): Span | undefined; } diff --git a/src/trace/context/ContextManager.ts b/src/trace/context/ContextManager.ts index 96fd23e..2d88d30 100644 --- a/src/trace/context/ContextManager.ts +++ b/src/trace/context/ContextManager.ts @@ -69,6 +69,12 @@ class ContextManager { return asyncState; } + get currentSpan(): Span { + const spans = store.getStore()?.spans; + + return spans?.[spans.length - 1] as Span; + }; + get hasContext(): boolean | undefined { return store.getStore()?.valid; } diff --git a/src/trace/context/DummyContext.ts b/src/trace/context/DummyContext.ts index 50d802a..99a722f 100644 --- a/src/trace/context/DummyContext.ts +++ b/src/trace/context/DummyContext.ts @@ -62,8 +62,4 @@ export default class DummyContext implements Context { resync(span: Span) { return; } - - currentSpan(): Span { - throw new Error('DummyContext.currentSpan() should never be called!'); - } } diff --git a/src/trace/context/SpanContext.ts b/src/trace/context/SpanContext.ts index 8c39072..5afa513 100644 --- a/src/trace/context/SpanContext.ts +++ b/src/trace/context/SpanContext.ts @@ -230,8 +230,4 @@ export default class SpanContext implements Context { ContextManager.spans.push(span); } } - - currentSpan(): Span | undefined { - return ContextManager.spans[ContextManager.spans.length - 1]; - } } diff --git a/src/trace/span/Span.ts b/src/trace/span/Span.ts index e6a1b56..2899a45 100644 --- a/src/trace/span/Span.ts +++ b/src/trace/span/Span.ts @@ -59,6 +59,7 @@ export default abstract class Span { startTime = 0; endTime = 0; errored = false; + lastError: Error | null = null; constructor(options: SpanCtorOptions & { type: SpanType }) { this.context = options.context; @@ -139,7 +140,11 @@ export default abstract class Span { } error(error: Error): this { + if (error === this.lastError) // don't store duplicate identical error twice + return this; + this.errored = true; + this.lastError = error; this.log('Stack', error?.stack || ''); return this; diff --git a/tests/plugins/mongoose/client.ts b/tests/plugins/mongoose/client.ts new file mode 100644 index 0000000..25ff2b3 --- /dev/null +++ b/tests/plugins/mongoose/client.ts @@ -0,0 +1,40 @@ +/*! + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 * as http from 'http'; +import agent from '../../../src'; + +process.env.SW_AGENT_LOGGING_LEVEL = 'ERROR'; + +agent.start({ + serviceName: 'client', + maxBufferSize: 1000, +}) + +const server = http.createServer((req, res) => { + http + .request(`http://${process.env.SERVER || 'localhost:5000'}${req.url}`, (r) => { + let data = ''; + r.on('data', (chunk) => (data += chunk)); + r.on('end', () => res.end(data)); + }) + .end(); +}); + +server.listen(5001, () => console.info('Listening on port 5001...')); diff --git a/tests/plugins/mongoose/docker-compose.yml b/tests/plugins/mongoose/docker-compose.yml new file mode 100644 index 0000000..305351b --- /dev/null +++ b/tests/plugins/mongoose/docker-compose.yml @@ -0,0 +1,90 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +version: "2.1" + +services: + collector: + extends: + file: ../common/base-compose.yml + service: collector + networks: + - traveling-light + + mongo: + container_name: mongo + environment: + MONGO_INITDB_ROOT_USERNAME: "root" + MONGO_INITDB_ROOT_PASSWORD: "root" + MONGO_INITDB_DATABASE: "admin" + ports: + - 27017:27017 + volumes: + - ./init:/docker-entrypoint-initdb.d + healthcheck: + test: ["CMD", "bash", "-c", "cat < /dev/null > /dev/tcp/127.0.0.1/27017"] + interval: 5s + timeout: 60s + retries: 120 + image: "mongo:latest" + networks: + - traveling-light + + server: + extends: + file: ../common/base-compose.yml + service: agent + ports: + - 5000:5000 + environment: + MONGO_HOST: mongo + volumes: + - .:/app/tests/plugins/pg + healthcheck: + test: ["CMD", "bash", "-c", "cat < /dev/null > /dev/tcp/127.0.0.1/5000"] + interval: 5s + timeout: 60s + retries: 120 + entrypoint: + ["bash", "-c", "npx ts-node /app/tests/plugins/pg/server.ts"] + depends_on: + collector: + condition: service_healthy + mongo: + condition: service_healthy + + client: + extends: + file: ../common/base-compose.yml + service: agent + ports: + - 5001:5001 + environment: + SERVER: server:5000 + healthcheck: + test: ["CMD", "bash", "-c", "cat < /dev/null > /dev/tcp/127.0.0.1/5001"] + interval: 5s + timeout: 60s + retries: 120 + entrypoint: + ["bash", "-c", "npx ts-node /app/tests/plugins/pg/client.ts"] + depends_on: + server: + condition: service_healthy + +networks: + traveling-light: diff --git a/tests/plugins/mongoose/expected.data.yaml b/tests/plugins/mongoose/expected.data.yaml new file mode 100644 index 0000000..029e3fa --- /dev/null +++ b/tests/plugins/mongoose/expected.data.yaml @@ -0,0 +1,129 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +segmentItems: + - serviceName: server + segmentSize: 1 + segments: + - segmentId: not null + spans: + - operationName: MongoDB/collection + operationId: 0 + parentSpanId: 0 + spanId: 1 + spanLayer: Database + startTime: gt 0 + endTime: gt 0 + componentId: 9 + spanType: Exit + peer: mongo:27017 + skipAnalysis: false + tags: + - { key: db.type, value: MongoDB } + - { key: db.instance, value: admin } + - { key: db.statement, value: 'collection("tests")' } + - operationName: Mongoose/ensureIndexes + operationId: 0 + parentSpanId: 0 + spanId: 2 + spanLayer: Database + startTime: gt 0 + endTime: gt 0 + componentId: 4006 + spanType: Exit + peer: mongo:27017 + skipAnalysis: false + tags: + - { key: db.type, value: MongoDB } + - { key: db.instance, value: admin } + - operationName: Mongoose/find + operationId: 0 + parentSpanId: 0 + spanId: 3 + spanLayer: Database + startTime: gt 0 + endTime: gt 0 + componentId: 4006 + spanType: Exit + peer: mongo:27017 + skipAnalysis: false + tags: + - { key: db.type, value: MongoDB } + - { key: db.instance, value: admin } + - { key: db.statement, value: 'tests.find({})' } + - operationName: /mongoose + operationId: 0 + parentSpanId: -1 + spanId: 0 + spanLayer: Http + startTime: gt 0 + endTime: gt 0 + componentId: 49 + spanType: Entry + peer: not null + skipAnalysis: false + tags: + - { key: http.url, value: 'http://server:5000/mongoose' } + - { key: http.method, value: GET } + - { key: http.status.code, value: '200' } + - { key: http.status.msg, value: OK } + refs: + - parentEndpoint: "" + networkAddress: server:5000 + refType: CrossProcess + parentSpanId: 1 + parentTraceSegmentId: not null + parentServiceInstance: not null + parentService: client + traceId: not null + - serviceName: client + segmentSize: 1 + segments: + - segmentId: not null + spans: + - operationName: /mongoose + operationId: 0 + parentSpanId: -1 + spanId: 0 + spanLayer: Http + startTime: gt 0 + endTime: gt 0 + componentId: 49 + spanType: Entry + peer: not null + skipAnalysis: false + tags: + - { key: http.url, value: 'http://localhost:5001/mongoose' } + - { key: http.method, value: GET } + - { key: http.status.code, value: '200' } + - { key: http.status.msg, value: OK } + - operationName: /mongoose + operationId: 0 + parentSpanId: 0 + spanId: 1 + spanLayer: Http + startTime: gt 0 + endTime: gt 0 + componentId: 2 + spanType: Exit + peer: server:5000 + skipAnalysis: false + tags: + - { key: http.url, value: 'http://server:5000/mongoose' } + - { key: http.method, value: GET } + - { key: http.status.code, value: '200' } + - { key: http.status.msg, value: OK } diff --git a/tests/plugins/mongoose/init/init.js b/tests/plugins/mongoose/init/init.js new file mode 100644 index 0000000..48ac0d3 --- /dev/null +++ b/tests/plugins/mongoose/init/init.js @@ -0,0 +1 @@ +db.createCollection('docs'); diff --git a/tests/plugins/mongoose/server.ts b/tests/plugins/mongoose/server.ts new file mode 100644 index 0000000..2e6d37b --- /dev/null +++ b/tests/plugins/mongoose/server.ts @@ -0,0 +1,66 @@ +/*! + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 * as http from 'http'; +import mongoose from 'mongoose'; +import agent from '../../../src'; + +process.env.SW_AGENT_LOGGING_LEVEL = 'ERROR'; + +agent.start({ + serviceName: 'server', + maxBufferSize: 1000, +}); + +const server = http.createServer(async (req, res) => { + await new Promise((resolve, reject) => { + mongoose.connect(`mongodb://root:root@${process.env.MONGO_HOST}:27017/admin`, { + useNewUrlParser: true, + useUnifiedTopology: true, + useFindAndModify: false, + + }).then(() => { + const Test = new mongoose.Schema({ + title: String + }); + + const modelTest = mongoose.model('Test', Test); + + modelTest.find().then( + (result: any) => { + res.end(`${result}`); + resolve(null); + mongoose.connection.close(); + }, + + (err: Error) => { + res.end(`${err}`); + resolve(null); + mongoose.connection.close(); + }, + ); + + }).catch((err: Error) => { + res.end(`${err}`); + resolve(null); + }); + }); +}); + +server.listen(5000, () => console.info('Listening on port 5000...')); diff --git a/tests/plugins/mongoose/test.ts b/tests/plugins/mongoose/test.ts new file mode 100644 index 0000000..805d109 --- /dev/null +++ b/tests/plugins/mongoose/test.ts @@ -0,0 +1,57 @@ +/*! + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 * as path from 'path'; +import { DockerComposeEnvironment, StartedDockerComposeEnvironment, Wait } from 'testcontainers'; +import axios from 'axios'; +import waitForExpect from 'wait-for-expect'; +import { promises as fs } from 'fs'; + +const rootDir = path.resolve(__dirname); + +describe('plugin tests', () => { + let compose: StartedDockerComposeEnvironment; + + beforeAll(async () => { + compose = await new DockerComposeEnvironment(rootDir, 'docker-compose.yml') + .withWaitStrategy('client', Wait.forHealthCheck()) + .withWaitStrategy('mongo', Wait.forHealthCheck()) + .up(); + }); + + afterAll(async () => { + await compose.down(); + }); + + it(__filename, async () => { + await waitForExpect(async () => expect((await axios.get('http://localhost:5001/mongoose')).status).toBe(200)); + + const expectedData = await fs.readFile(path.join(rootDir, 'expected.data.yaml'), 'utf8'); + + try { + await waitForExpect(async () => + expect((await axios.post('http://localhost:12800/dataValidate', expectedData)).status).toBe(200), + ); + } catch (e) { + const actualData = (await axios.get('http://localhost:12800/receiveData')).data; + console.info({ actualData }); + throw e; + } + }); +});