You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by ke...@apache.org on 2021/03/28 12:09:06 UTC
[skywalking-nodejs] branch master updated: Node Mongoose Plugin
(#44)
This is an automated email from the ASF dual-hosted git repository.
kezhenxu94 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/skywalking-nodejs.git
The following commit(s) were added to refs/heads/master by this push:
new 4670a45 Node Mongoose Plugin (#44)
4670a45 is described below
commit 4670a45509e187aac77248eec5da720347830552
Author: Tomasz Pytel <to...@gmail.com>
AuthorDate: Sun Mar 28 09:08:57 2021 -0300
Node Mongoose Plugin (#44)
---
README.md | 2 +-
package-lock.json | 125 +++++++++++++++++
package.json | 3 +-
src/config/AgentConfig.ts | 2 +-
src/core/PluginInstaller.ts | 4 +-
src/plugins/MongoDBPlugin.ts | 9 +-
src/plugins/MongoosePlugin.ts | 155 +++++++++++++++++++++
src/trace/Component.ts | 1 +
src/trace/context/Context.ts | 2 -
src/trace/context/ContextManager.ts | 6 +
src/trace/context/DummyContext.ts | 4 -
src/trace/context/SpanContext.ts | 4 -
src/trace/span/Span.ts | 5 +
.../plugins/mongoose/client.ts | 33 +++--
tests/plugins/mongoose/docker-compose.yml | 90 ++++++++++++
tests/plugins/mongoose/expected.data.yaml | 129 +++++++++++++++++
tests/plugins/mongoose/init/init.js | 1 +
tests/plugins/mongoose/server.ts | 66 +++++++++
tests/plugins/mongoose/test.ts | 57 ++++++++
19 files changed, 667 insertions(+), 31 deletions(-)
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/src/trace/Component.ts b/tests/plugins/mongoose/client.ts
similarity index 59%
copy from src/trace/Component.ts
copy to tests/plugins/mongoose/client.ts
index 2a78e67..25ff2b3 100644
--- a/src/trace/Component.ts
+++ b/tests/plugins/mongoose/client.ts
@@ -17,17 +17,24 @@
*
*/
-export class Component {
- static readonly UNKNOWN = new Component(0);
- static readonly HTTP = new Component(2);
- static readonly MYSQL = new Component(5);
- static readonly MONGODB = new Component(9);
- static readonly POSTGRESQL = new Component(22);
- static readonly HTTP_SERVER = new Component(49);
- static readonly RABBITMQ_PRODUCER = new Component(52);
- static readonly RABBITMQ_CONSUMER = new Component(53);
- static readonly EXPRESS = new Component(4002);
- static readonly AXIOS = new Component(4005);
+import * as http from 'http';
+import agent from '../../../src';
- constructor(public readonly id: number) {}
-}
+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;
+ }
+ });
+});