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;
+    }
+  });
+});