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/01 13:16:01 UTC

[skywalking-nodejs] branch master updated: Add MongoDB plugin (first working version( (#33)

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 0f946b9  Add MongoDB plugin (first working version( (#33)
0f946b9 is described below

commit 0f946b9e3bde988de09b4d2a58c2147b99da1b54
Author: Tomasz Pytel <to...@gmail.com>
AuthorDate: Mon Mar 1 10:15:53 2021 -0300

    Add MongoDB plugin (first working version( (#33)
---
 README.md                                |   5 +
 package.json                             |   3 +-
 src/Tag.ts                               |   8 +
 src/config/AgentConfig.ts                |   6 +
 src/plugins/MongoDBPlugin.ts             | 304 +++++++++++++++++++++++++++++++
 src/plugins/MySQLPlugin.ts               |   8 +-
 src/plugins/PgPlugin.ts                  |  55 ++++--
 tests/plugins/mongodb/client.ts          |  40 ++++
 tests/plugins/mongodb/docker-compose.yml |  90 +++++++++
 tests/plugins/mongodb/expected.data.yaml |  99 ++++++++++
 tests/plugins/mongodb/init/init.js       |   1 +
 tests/plugins/mongodb/server.ts          |  55 ++++++
 tests/plugins/mongodb/test.ts            |  57 ++++++
 13 files changed, 709 insertions(+), 22 deletions(-)

diff --git a/README.md b/README.md
index f710d45..e6b4a6e 100644
--- a/README.md
+++ b/README.md
@@ -57,7 +57,10 @@ Environment Variable | Description | Default
 | `SW_AGENT_LOGGING_LEVEL` | The logging level, could be one of `CRITICAL`, `FATAL`, `ERROR`, `WARN`(`WARNING`), `INFO`, `DEBUG` | `INFO` |
 | `SW_IGNORE_SUFFIX` | The suffices of endpoints that will be ignored (not traced), comma separated | `.jpg,.jpeg,.js,.css,.png,.bmp,.gif,.ico,.mp3,.mp4,.html,.svg` |
 | `SW_TRACE_IGNORE_PATH` | The paths of endpoints that will be ignored (not traced), comma separated | `` |
+| `SW_SQL_TRACE_PARAMETERS` | If set to 'true' then SQL query parameters will be included | `false` |
 | `SW_SQL_PARAMETERS_MAX_LENGTH` | The maximum string length of SQL parameters to log | `512` |
+| `SW_MONGO_TRACE_PARAMETERS` | If set to 'true' then mongodb query parameters will be included | `false` |
+| `SW_MONGO_PARAMETERS_MAX_LENGTH` | The maximum string length of mongodb parameters to log | `512` |
 | `SW_AGENT_MAX_BUFFER_SIZE` | The maximum buffer size before sending the segment data to backend | `'1000'` |
 
 ## Supported Libraries
@@ -71,6 +74,8 @@ Library | Plugin Name
 | [`axios`](https://github.com/axios/axios) | `axios` |
 | [`mysql`](https://github.com/mysqljs/mysql) | `mysql` |
 | [`pg`](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` |
 
 ### Compatible Libraries
 
diff --git a/package.json b/package.json
index b037c70..2bfe29c 100644
--- a/package.json
+++ b/package.json
@@ -46,9 +46,10 @@
     "@types/uuid": "^8.0.0",
     "axios": "^0.21.0",
     "express": "^4.17.1",
-    "grpc_tools_node_protoc_ts": "^4.0.0",
     "grpc-tools": "^1.10.0",
+    "grpc_tools_node_protoc_ts": "^4.0.0",
     "jest": "^26.6.3",
+    "mongodb": "^3.6.4",
     "mysql": "^2.18.1",
     "pg": "^8.5.1",
     "prettier": "^2.0.5",
diff --git a/src/Tag.ts b/src/Tag.ts
index 305107c..06daf24 100644
--- a/src/Tag.ts
+++ b/src/Tag.ts
@@ -32,6 +32,7 @@ export default {
   dbInstanceKey: 'db.instance',
   dbStatementKey: 'db.statement',
   dbSqlParametersKey: 'db.sql.parameters',
+  dbMongoParametersKey: 'db.mongo.parameters',
 
   httpStatusCode(val: string | number | undefined): Tag {
     return {
@@ -89,4 +90,11 @@ export default {
       val: `${val}`,
     } as Tag;
   },
+  dbMongoParameters(val: string | undefined): Tag {
+    return {
+      key: this.dbMongoParametersKey,
+      overridable: false,
+      val: `${val}`,
+    } as Tag;
+  },
 };
diff --git a/src/config/AgentConfig.ts b/src/config/AgentConfig.ts
index 94d5f26..349e13a 100644
--- a/src/config/AgentConfig.ts
+++ b/src/config/AgentConfig.ts
@@ -27,7 +27,10 @@ export type AgentConfig = {
   maxBufferSize?: number;
   ignoreSuffix?: string;
   traceIgnorePath?: string;
+  sql_trace_parameters?: boolean;
   sql_parameters_max_length?: number;
+  mongo_trace_parameters?: boolean;
+  mongo_parameters_max_length?: number;
   // the following is internal state computed from config values
   reIgnoreOperation?: RegExp;
 };
@@ -60,6 +63,9 @@ export default {
     Number.parseInt(process.env.SW_AGENT_MAX_BUFFER_SIZE as string, 10) : 1000,
   ignoreSuffix: process.env.SW_IGNORE_SUFFIX ?? '.jpg,.jpeg,.js,.css,.png,.bmp,.gif,.ico,.mp3,.mp4,.html,.svg',
   traceIgnorePath: process.env.SW_TRACE_IGNORE_PATH || '',
+  sql_trace_parameters: (process.env.SW_SQL_TRACE_PARAMETERS || '').toLowerCase() === 'true',
   sql_parameters_max_length: Math.trunc(Math.max(0, Number(process.env.SW_SQL_PARAMETERS_MAX_LENGTH))) || 512,
+  mongo_trace_parameters: (process.env.SW_MONGO_TRACE_PARAMETERS || '').toLowerCase() === 'true',
+  mongo_parameters_max_length: Math.trunc(Math.max(0, Number(process.env.SW_MONGO_PARAMETERS_MAX_LENGTH))) || 512,
   reIgnoreOperation: RegExp(''),  // temporary placeholder so Typescript doesn't throw a fit
 };
diff --git a/src/plugins/MongoDBPlugin.ts b/src/plugins/MongoDBPlugin.ts
new file mode 100644
index 0000000..60b4534
--- /dev/null
+++ b/src/plugins/MongoDBPlugin.ts
@@ -0,0 +1,304 @@
+/*!
+ *
+ * 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 from '../core/SwPlugin';
+import ContextManager from '../trace/context/ContextManager';
+import { Component } from '../trace/Component';
+import ExitSpan from '../trace/span/ExitSpan';
+import Tag from '../Tag';
+import { SpanLayer } from '../proto/language-agent/Tracing_pb';
+import PluginInstaller from '../core/PluginInstaller';
+import agentConfig from '../config/AgentConfig';
+
+class MongoDBPlugin implements SwPlugin {
+  readonly module = 'mongodb';
+  readonly versions = '*';
+
+  Cursor: any;
+
+  // Experimental method to determine proper end time of cursor DB operation, we stop the span when the cursor is closed.
+  // Problematic because other exit spans may be created during processing, for this reason we do not .resync() this
+  // span to the span list until it is closed. If the cursor is never closed then the span will not be sent.
+
+  maybeHookCursor(span: any, cursor: any): boolean {
+    if (!(cursor instanceof this.Cursor))
+      return false;
+
+    cursor.on('error', (err: any) => {
+      span.resync();  // this may precede 'close' .resync() but its fine
+      span.error(err);
+      span.stop();
+    });
+
+    cursor.on('close', () => {
+      span.resync();  // cursor does not .resync() until it is closed because maybe other exit spans will be opened during processing
+      span.stop();
+    });
+
+    return true;
+  }
+
+  install(installer: PluginInstaller): void {
+    const plugin     = this;
+    const Collection = installer.require('mongodb/lib/collection');
+    this.Cursor      = installer.require('mongodb/lib/cursor');
+
+    const wrapCallback = (span: any, args: any[], idx: number): boolean => {
+      const callback = args.length > idx && typeof args[idx = args.length - 1] === 'function' ? args[idx] : null;
+
+      if (!callback)
+        return false;
+
+      args[idx] = function(this: any, error: any, result: any) {
+        if (error || !plugin.maybeHookCursor(span, result)) {
+          span.resync();
+
+          if (error)
+            span.error(error);
+
+          span.stop();
+        }
+
+        return callback.call(this, error, result);
+      }
+
+      return true;
+    };
+
+    const stringify = (params: any) => {
+      if (params === undefined)
+        return '';
+
+      let str = JSON.stringify(params);
+
+      if (str.length > agentConfig.mongo_parameters_max_length)
+        str = str.slice(0, agentConfig.mongo_parameters_max_length) + ' ...';
+
+      return str;
+    }
+
+    const insertFunc = function(this: any, operation: string, span: any, args: any[]): boolean {  // args = [doc(s), options, callback]
+      span.tag(Tag.dbStatement(`${this.s.namespace.collection}.${operation}()`));
+
+      if (agentConfig.mongo_trace_parameters)
+        span.tag(Tag.dbMongoParameters(stringify(args[0])));
+
+      return wrapCallback(span, args, 1);
+    };
+
+    const deleteFunc = function(this: any, operation: string, span: any, args: any[]): boolean {  // args = [filter, options, callback]
+      span.tag(Tag.dbStatement(`${this.s.namespace.collection}.${operation}(${stringify(args[0])})`));
+
+      return wrapCallback(span, args, 1);
+    };
+
+    const updateFunc = function(this: any, operation: string, span: any, args: any[]): boolean {  // args = [filter, update, options, callback]
+      span.tag(Tag.dbStatement(`${this.s.namespace.collection}.${operation}(${stringify(args[0])})`));
+
+      if (agentConfig.mongo_trace_parameters)
+        span.tag(Tag.dbMongoParameters(stringify(args[1])));
+
+      return wrapCallback(span, args, 2);
+    };
+
+    const findOneFunc = function(this: any, operation: string, span: any, args: any[]): boolean {  // args = [query, options, callback]
+      span.tag(Tag.dbStatement(`${this.s.namespace.collection}.${operation}(${typeof args[0] !== 'function' ? stringify(args[0]) : ''})`));
+
+      return wrapCallback(span, args, 0);
+    };
+
+    const findAndRemoveFunc = function(this: any, operation: string, span: any, args: any[]): boolean {  // args = [query, sort, options, callback]
+      span.tag(Tag.dbStatement(`${this.s.namespace.collection}.${operation}(${stringify(args[0])}${typeof args[1] !== 'function' && args[1] !== undefined ? ', ' + stringify(args[1]) : ''})`));
+
+      return wrapCallback(span, args, 1);
+    };
+
+    const findAndModifyFunc = function(this: any, operation: string, span: any, args: any[]): boolean {  // args = [query, sort, doc, options, callback]
+      let params = stringify(args[0]);
+
+      if (typeof args[1] !== 'function' && args[1] !== undefined) {
+        params += ', ' + stringify(args[1]);
+
+        if (typeof args[2] !== 'function' && args[2] !== undefined) {
+          if (agentConfig.mongo_trace_parameters)
+            span.tag(Tag.dbMongoParameters(stringify(args[2])));
+        }
+      }
+
+      span.tag(Tag.dbStatement(`${this.s.namespace.collection}.${operation}(${params})`));
+
+      return wrapCallback(span, args, 1);
+    };
+
+    const mapReduceFunc = function(this: any, operation: string, span: any, args: any[]): boolean {  // args = [map, reduce, options, callback]
+      span.tag(Tag.dbStatement(`${this.s.namespace.collection}.${operation}(${args[0]}, ${args[1]})`));
+
+      return wrapCallback(span, args, 2);
+    };
+
+    const dropFunc = function(this: any, operation: string, span: any, args: any[]): boolean {  // args = [options, callback]
+      span.tag(Tag.dbStatement(`${this.s.namespace.collection}.${operation}()`));
+
+      return wrapCallback(span, args, 0);
+    };
+
+    this.interceptOperation(Collection, 'insert', insertFunc);
+    this.interceptOperation(Collection, 'insertOne', insertFunc);
+    this.interceptOperation(Collection, 'insertMany', insertFunc);
+    this.interceptOperation(Collection, 'save', insertFunc);
+    this.interceptOperation(Collection, 'deleteOne', deleteFunc);
+    this.interceptOperation(Collection, 'deleteMany', deleteFunc);
+    this.interceptOperation(Collection, 'remove', deleteFunc);
+    this.interceptOperation(Collection, 'removeOne', deleteFunc);
+    this.interceptOperation(Collection, 'removeMany', deleteFunc);
+    this.interceptOperation(Collection, 'update', updateFunc);
+    this.interceptOperation(Collection, 'updateOne', updateFunc);
+    this.interceptOperation(Collection, 'updateMany', updateFunc);
+    this.interceptOperation(Collection, 'replaceOne', updateFunc);
+    this.interceptOperation(Collection, 'find', findOneFunc);  // cursor
+    this.interceptOperation(Collection, 'findOne', findOneFunc);
+    this.interceptOperation(Collection, 'findOneAndDelete', deleteFunc);
+    this.interceptOperation(Collection, 'findOneAndReplace', updateFunc);
+    this.interceptOperation(Collection, 'findOneAndUpdate', updateFunc);
+    this.interceptOperation(Collection, 'findAndRemove', findAndRemoveFunc);
+    this.interceptOperation(Collection, 'findAndModify', findAndModifyFunc);
+
+    this.interceptOperation(Collection, 'bulkWrite', insertFunc);
+    this.interceptOperation(Collection, 'mapReduce', mapReduceFunc);
+    this.interceptOperation(Collection, 'aggregate', deleteFunc);  // cursor
+    this.interceptOperation(Collection, 'distinct', findAndRemoveFunc);
+    this.interceptOperation(Collection, 'count', findOneFunc);
+    this.interceptOperation(Collection, 'estimatedDocumentCount', dropFunc);
+    this.interceptOperation(Collection, 'countDocuments', findOneFunc);
+
+    this.interceptOperation(Collection, 'rename', deleteFunc);
+    this.interceptOperation(Collection, 'drop', dropFunc);
+
+
+    // TODO?
+
+    //   createIndex
+    //   createIndexes
+    //   dropIndex
+    //   dropIndexes
+    //   dropAllIndexes
+    //   ensureIndex
+    //   indexExists
+    //   indexInformation
+    //   indexes
+    //   listIndexes
+    //   reIndex
+
+    //   stats
+    //   options
+    //   isCapped
+    //   initializeUnorderedBulkOp
+    //   initializeOrderedBulkOp
+    //   watch
+
+
+    // NODO:
+
+    //   group
+    //   parallelCollectionScan
+    //   geoHaystackSearch
+  }
+
+  interceptOperation(Collection: any, operation: string, operationFunc: any): void {
+    const plugin    = this;
+    const _original = Collection.prototype[operation];
+
+    if (!_original)
+        return;
+
+    Collection.prototype[operation] = function(...args: any[]) {
+      const spans = ContextManager.spans;
+      let   span = spans[spans.length - 1];
+
+      if (span && span.component === Component.MONGODB && span instanceof ExitSpan)  // mongodb has called into itself internally
+        return _original.apply(this, args);
+
+      let ret: any;
+      let host: string;
+
+      try {
+        host = this.s.db.serverConfig.s.options.servers.map((s: any) => `${s.host}:${s.port}`).join(',');  // will this work for non-NativeTopology?
+      } catch {
+        host = '???';
+      }
+
+      span = ContextManager.current.newExitSpan('/' + this.s.namespace.db, host).start();  // or this.s.db.databaseName
+
+      try {
+        span.component = Component.MONGODB;
+        span.layer = SpanLayer.DATABASE;
+        span.peer = host;
+
+        span.tag(Tag.dbType('MongoDB'));
+        span.tag(Tag.dbInstance(`${this.s.namespace.db}`));
+
+        const hasCB = operationFunc.call(this, operation, span, args);
+
+        ret = _original.apply(this, args);
+
+        if (!hasCB) {
+          if (plugin.maybeHookCursor(span, ret)) {
+            // NOOP
+
+          } else if (!ret || typeof ret.then !== 'function') {  // generic Promise check
+            span.stop();  // no callback passed in and no Promise or Cursor returned, play it safe
+
+            return ret;
+
+          } else {
+            ret = ret.then(
+              (res: any) => {
+                span.resync();
+                span.stop();
+
+                return res;
+              },
+
+              (err: any) => {
+                span.resync();
+                span.error(err);
+                span.stop();
+
+                return Promise.reject(err);
+              }
+            );
+          }
+        }
+
+      } catch (e) {
+        span.error(e);
+        span.stop();
+
+        throw e;
+      }
+
+      span.async();
+
+      return ret;
+    };
+  }
+}
+
+// noinspection JSUnusedGlobalSymbols
+export default new MongoDBPlugin();
diff --git a/src/plugins/MySQLPlugin.ts b/src/plugins/MySQLPlugin.ts
index 8e191fc..35ebee0 100644
--- a/src/plugins/MySQLPlugin.ts
+++ b/src/plugins/MySQLPlugin.ts
@@ -23,7 +23,7 @@ import { Component } from '../trace/Component';
 import Tag from '../Tag';
 import { SpanLayer } from '../proto/language-agent/Tracing_pb';
 import PluginInstaller from '../core/PluginInstaller';
-import config from '../config/AgentConfig';
+import agentConfig from '../config/AgentConfig';
 
 class MySQLPlugin implements SwPlugin {
   readonly module = 'mysql';
@@ -109,11 +109,11 @@ class MySQLPlugin implements SwPlugin {
 
         span.tag(Tag.dbStatement(`${_sql}`));
 
-        if (_values) {
+        if (agentConfig.sql_trace_parameters && _values) {
           let vals = _values.map((v: any) => v === undefined ? 'undefined' : JSON.stringify(v)).join(', ');
 
-          if (vals.length > config.sql_parameters_max_length)
-            vals = vals.splice(0, config.sql_parameters_max_length);
+          if (vals.length > agentConfig.sql_parameters_max_length)
+            vals = vals.slice(0, agentConfig.sql_parameters_max_length) + ' ...';
 
           span.tag(Tag.dbSqlParameters(`[${vals}]`));
         }
diff --git a/src/plugins/PgPlugin.ts b/src/plugins/PgPlugin.ts
index e8f4435..0d8f4e9 100644
--- a/src/plugins/PgPlugin.ts
+++ b/src/plugins/PgPlugin.ts
@@ -31,6 +31,13 @@ class MySQLPlugin implements SwPlugin {
 
   install(installer: PluginInstaller): void {
     const Client = installer.require('pg/lib/client');
+
+    let Cursor: any;
+
+    try {
+      Cursor = installer.require('pg-cursor');
+    } catch { /* Linter food */ }
+
     const _query = Client.prototype.query;
 
     Client.prototype.query = function(config: any, values: any, callback: any) {
@@ -76,7 +83,7 @@ class MySQLPlugin implements SwPlugin {
 
         if (typeof values === 'function')
           values = wrapCallback(values);
-        else
+        else if (_values !== undefined)
           _values = values;
 
         if (typeof callback === 'function')
@@ -84,34 +91,48 @@ class MySQLPlugin implements SwPlugin {
 
         span.tag(Tag.dbStatement(`${_sql}`));
 
-        if (_values) {
+        if (agentConfig.sql_trace_parameters && _values) {
           let vals = _values.map((v: any) => v === undefined ? 'undefined' : JSON.stringify(v)).join(', ');
 
           if (vals.length > agentConfig.sql_parameters_max_length)
-            vals = vals.splice(0, agentConfig.sql_parameters_max_length);
+            vals = vals.slice(0, agentConfig.sql_parameters_max_length) + ' ...';
 
             span.tag(Tag.dbSqlParameters(`[${vals}]`));
         }
 
         query = _query.call(this, config, values, callback);
 
-        if (query && typeof query.then === 'function') {  // generic Promise check
-          query = query.then(
-            (res: any) => {
-              span.resync();
-              span.stop();
-
-              return res;
-            },
-
-            (err: any) => {
-              span.resync();
+        if (query) {
+          if (Cursor && query instanceof Cursor) {
+            query.on('error', (err: any) => {
+              span.resync();  // this may precede 'end' .resync() but its fine
               span.error(err);
               span.stop();
+            });
 
-              return Promise.reject(err);
-            }
-          );
+            query.on('end', () => {
+              span.resync();  // cursor does not .resync() until it is closed because maybe other exit spans will be opened during processing
+              span.stop();
+            });
+
+          } else if (typeof query.then === 'function') {  // generic Promise check
+            query = query.then(
+              (res: any) => {
+                span.resync();
+                span.stop();
+
+                return res;
+              },
+
+              (err: any) => {
+                span.resync();
+                span.error(err);
+                span.stop();
+
+                return Promise.reject(err);
+              }
+            );
+          } // else we assume there was a callback
         }
 
       } catch (e) {
diff --git a/tests/plugins/mongodb/client.ts b/tests/plugins/mongodb/client.ts
new file mode 100644
index 0000000..25ff2b3
--- /dev/null
+++ b/tests/plugins/mongodb/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/mongodb/docker-compose.yml b/tests/plugins/mongodb/docker-compose.yml
new file mode 100644
index 0000000..305351b
--- /dev/null
+++ b/tests/plugins/mongodb/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/mongodb/expected.data.yaml b/tests/plugins/mongodb/expected.data.yaml
new file mode 100644
index 0000000..37061e2
--- /dev/null
+++ b/tests/plugins/mongodb/expected.data.yaml
@@ -0,0 +1,99 @@
+#
+# 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: /admin
+            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: docs.findOne() }
+          - operationName: /mongo
+            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/mongo' }
+              - { 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: /mongo
+            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/mongo' }
+              - { key: http.method, value: GET }
+              - { key: http.status.code, value: '200' }
+          - operationName: /mongo
+            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: 'server:5000/mongo' }
+              - { key: http.method, value: GET }
+              - { key: http.status.code, value: '200' }
+              - { key: http.status.msg, value: OK }
diff --git a/tests/plugins/mongodb/init/init.js b/tests/plugins/mongodb/init/init.js
new file mode 100644
index 0000000..48ac0d3
--- /dev/null
+++ b/tests/plugins/mongodb/init/init.js
@@ -0,0 +1 @@
+db.createCollection('docs');
diff --git a/tests/plugins/mongodb/server.ts b/tests/plugins/mongodb/server.ts
new file mode 100644
index 0000000..5870227
--- /dev/null
+++ b/tests/plugins/mongodb/server.ts
@@ -0,0 +1,55 @@
+/*!
+ *
+ * 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 {MongoClient} from 'mongodb';
+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) => {
+    MongoClient.connect(`mongodb://root:root@${process.env.MONGO_HOST}:27017`, {useUnifiedTopology: true}, function(err: any, client: any) {
+      if (err) {
+          res.end(`${err}`);
+          resolve(null);
+      } else {
+        client.db('admin').collection('docs').findOne().then(
+          (resDB: any) => {
+            res.end(`${resDB}`);
+            resolve(null);
+            client.close();
+          },
+          (err: any) => {
+            res.end(`${err}`);
+            resolve(null);
+            client.close();
+          },
+        );
+      }
+    });
+  });
+});
+
+server.listen(5000, () => console.info('Listening on port 5000...'));
diff --git a/tests/plugins/mongodb/test.ts b/tests/plugins/mongodb/test.ts
new file mode 100644
index 0000000..787e57d
--- /dev/null
+++ b/tests/plugins/mongodb/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/mongo')).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;
+    }
+  });
+});