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 2020/08/07 03:12:46 UTC

[skywalking-python] branch master updated: [Plugin] Add plugin for PyMongo(support version 3.7.0 or above) (#60)

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-python.git


The following commit(s) were added to refs/heads/master by this push:
     new 9bc64ad  [Plugin] Add plugin for PyMongo(support version 3.7.0 or above) (#60)
9bc64ad is described below

commit 9bc64ad07018de981619702ce3a73ff7cf7b9722
Author: Humbertzhang <50...@qq.com>
AuthorDate: Fri Aug 7 11:12:36 2020 +0800

    [Plugin] Add plugin for PyMongo(support version 3.7.0 or above) (#60)
---
 docs/EnvVars.md                              |   2 +
 docs/Plugins.md                              |   1 +
 requirements.txt                             |   1 +
 skywalking/__init__.py                       |   1 +
 skywalking/config.py                         |   3 +
 skywalking/plugins/sw_pymongo.py             | 193 +++++++++++++++++++++
 tests/plugin/sw_pymongo/__init__.py          |  16 ++
 tests/plugin/sw_pymongo/docker-compose.yml   |  75 +++++++++
 tests/plugin/sw_pymongo/expected.data.yml    | 239 +++++++++++++++++++++++++++
 tests/plugin/sw_pymongo/services/__init__.py |  16 ++
 tests/plugin/sw_pymongo/services/consumer.py |  39 +++++
 tests/plugin/sw_pymongo/services/provider.py |  66 ++++++++
 tests/plugin/sw_pymongo/test_pymongo.py      |  42 +++++
 13 files changed, 694 insertions(+)

diff --git a/docs/EnvVars.md b/docs/EnvVars.md
index babd9b1..4f59807 100644
--- a/docs/EnvVars.md
+++ b/docs/EnvVars.md
@@ -11,6 +11,8 @@ Environment Variable | Description | Default
 | `SW_AGENT_DISABLE_PLUGINS` | The name patterns in CSV pattern, plugins whose name matches one of the pattern won't be installed | `''` |
 | `SW_MYSQL_TRACE_SQL_PARAMETERS` | Indicates whether to collect the sql parameters or not | `False` |
 | `SW_MYSQL_SQL_PARAMETERS_MAX_LENGTH` | The maximum length of the collected parameter, parameters longer than the specified length will be truncated | `512` |
+| `SW_PYMONGO_TRACE_PARAMETERS` | Indicates whether to collect the filters of pymongo | `False` | 
+| `SW_PYMONGO_PARAMETERS_MAX_LENGTH` | The maximum length of the collected filters, filters longer than the specified length will be truncated |  `512` |
 | `SW_IGNORE_SUFFIX` | If the operation name of the first span is included in this set, this segment should be ignored. | `.jpg,.jpeg,.js,.css,.png,.bmp,.gif,.ico,.mp3,.mp4,.html,.svg` |
 | `SW_FLASK_COLLECT_HTTP_PARAMS`| This config item controls that whether the Flask plugin should collect the parameters of the request.| `false` |
 | `SW_DJANGO_COLLECT_HTTP_PARAMS`| This config item controls that whether the Django plugin should collect the parameters of the request.| `false` |
diff --git a/docs/Plugins.md b/docs/Plugins.md
index f8c3d2b..dded9bb 100644
--- a/docs/Plugins.md
+++ b/docs/Plugins.md
@@ -12,3 +12,4 @@ Library | Plugin Name
 | [kafka-python](https://kafka-python.readthedocs.io/en/master/) | `sw_kafka` |
 | [tornado](https://www.tornadoweb.org/en/stable/) | `sw_tornado` |
 | [pika](https://pika.readthedocs.io/en/stable/) | `sw_rabbitmq` |
+| [pymongo](https://pymongo.readthedocs.io/en/stable/) | `sw_pymongo` |
diff --git a/requirements.txt b/requirements.txt
index 4732bc5..facf167 100755
--- a/requirements.txt
+++ b/requirements.txt
@@ -19,6 +19,7 @@ MarkupSafe==1.1.1
 packaging==20.4
 pika==1.1.0
 protobuf==3.12.4
+pymongo==3.11.0
 PyMySQL==0.10.0
 pyparsing==2.4.7
 pytz==2020.1
diff --git a/skywalking/__init__.py b/skywalking/__init__.py
index 67ac3ac..153ff5c 100644
--- a/skywalking/__init__.py
+++ b/skywalking/__init__.py
@@ -30,6 +30,7 @@ class Component(Enum):
     Django = 7004
     Tornado = 7005
     Redis = 7
+    MongoDB = 9
     KafkaProducer = 40
     KafkaConsumer = 41
     RabbitmqProducer = 52
diff --git a/skywalking/config.py b/skywalking/config.py
index fe73943..fb89ba8 100644
--- a/skywalking/config.py
+++ b/skywalking/config.py
@@ -29,6 +29,9 @@ disable_plugins = (os.getenv('SW_AGENT_DISABLE_PLUGINS') or '').split(',')  # ty
 mysql_trace_sql_parameters = True if os.getenv('SW_MYSQL_TRACE_SQL_PARAMETERS') and \
                                      os.getenv('SW_MYSQL_TRACE_SQL_PARAMETERS') == 'True' else False  # type: bool
 mysql_sql_parameters_max_length = int(os.getenv('SW_MYSQL_SQL_PARAMETERS_MAX_LENGTH') or '512')  # type: int
+pymongo_trace_parameters = True if os.getenv('SW_PYMONGO_TRACE_PARAMETERS') and \
+                                   os.getenv('SW_PYMONGO_TRACE_PARAMETERS') == 'True' else False  # type: bool
+pymongo_parameters_max_length = int(os.getenv('SW_PYMONGO_PARAMETERS_MAX_LENGTH') or '512')  # type: int
 ignore_suffix = os.getenv('SW_IGNORE_SUFFIX') or '.jpg,.jpeg,.js,.css,.png,.bmp,.gif,.ico,.mp3,' \
                                                  '.mp4,.html,.svg '  # type: str
 flask_collect_http_params = True if os.getenv('SW_FLASK_COLLECT_HTTP_PARAMS') and \
diff --git a/skywalking/plugins/sw_pymongo.py b/skywalking/plugins/sw_pymongo.py
new file mode 100644
index 0000000..06a8dcb
--- /dev/null
+++ b/skywalking/plugins/sw_pymongo.py
@@ -0,0 +1,193 @@
+#
+# 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 logging
+import pkg_resources
+from packaging import version
+
+from skywalking import Layer, Component, config
+from skywalking.trace import tags
+from skywalking.trace.carrier import Carrier
+from skywalking.trace.context import get_context
+from skywalking.trace.tags import Tag
+
+logger = logging.getLogger(__name__)
+
+
+def install():
+    try:
+        from pymongo.bulk import _Bulk
+        from pymongo.cursor import Cursor
+        from pymongo.pool import SocketInfo
+
+        # check pymongo version
+        pymongo_version = pkg_resources.get_distribution("pymongo").version
+        if version.parse(pymongo_version) < version.parse("3.7.0"):
+            logger.warning("support pymongo version 3.7.0 or above, current version:" + pymongo_version)
+            raise Exception
+
+        bulk_op_map = {
+                0: "insert",
+                1: "update",
+                2: "delete"
+            }
+        # handle insert_many and bulk write
+        inject_bulk_write(_Bulk, bulk_op_map)
+
+        # handle find() & find_one()
+        inject_cursor(Cursor)
+
+        # handle other commands
+        inject_socket_info(SocketInfo)
+
+    except Exception:
+        logger.warning('failed to install plugin %s', __name__)
+
+
+def inject_socket_info(SocketInfo):
+    _command = SocketInfo.command
+
+    def _sw_command(this: SocketInfo, dbname, spec, *args, **kwargs):
+        # pymongo sends `ismaster` command continuously. ignore it.
+        if spec.get("ismaster") is None:
+            address = this.sock.getpeername()
+            peer = "%s:%s" % address
+            context = get_context()
+            carrier = Carrier()
+
+            operation = list(spec.keys())[0]
+            sw_op = operation.capitalize() + "Operation"
+            with context.new_exit_span(op="MongoDB/" + sw_op, peer=peer, carrier=carrier) as span:
+                try:
+                    result = _command(this, dbname, spec, *args, **kwargs)
+
+                    span.layer = Layer.Database
+                    span.component = Component.MongoDB
+                    span.tag(Tag(key=tags.DbType, val="MongoDB"))
+                    span.tag(Tag(key=tags.DbInstance, val=dbname))
+
+                    if config.pymongo_trace_parameters:
+                        # get filters
+                        filters = _get_filter(operation, spec)
+                        max_len = config.pymongo_parameters_max_length
+                        filters = filters[0:max_len] + "..." if len(filters) > max_len else filters
+                        span.tag(Tag(key=tags.DbStatement, val=filters))
+                except BaseException as e:
+                    span.raised()
+                    raise e
+
+        else:
+            result = _command(this, dbname, spec, *args, **kwargs)
+
+        return result
+
+    SocketInfo.command = _sw_command
+
+
+def _get_filter(request_type, spec):
+    """
+    :param request_type: the request param send to MongoDB
+    :param spec: maybe a bson.SON class or a dict
+    :return: filter string
+    """
+    from bson import SON
+
+    if isinstance(spec, SON):
+        spec = spec.to_dict()
+        spec.pop(request_type)
+    elif isinstance(spec, dict):
+        spec = dict(spec)
+        spec.pop(request_type)
+
+    return request_type + " " + str(spec)
+
+
+def inject_bulk_write(_Bulk, bulk_op_map):
+    _execute = _Bulk.execute
+
+    def _sw_execute(this: _Bulk, *args, **kwargs):
+        address = this.collection.database.client.address
+        peer = "%s:%s" % address
+        context = get_context()
+        carrier = Carrier()
+
+        sw_op = "MixedBulkWriteOperation"
+        with context.new_exit_span(op="MongoDB/"+sw_op, peer=peer, carrier=carrier) as span:
+            span.layer = Layer.Database
+            span.component = Component.MongoDB
+
+            try:
+                bulk_result = _execute(this, *args, **kwargs)
+
+                span.tag(Tag(key=tags.DbType, val="MongoDB"))
+                span.tag(Tag(key=tags.DbInstance, val=this.collection.database.name))
+                if config.pymongo_trace_parameters:
+                    filters = ""
+                    bulk_ops = this.ops
+                    for bulk_op in bulk_ops:
+                        opname = bulk_op_map.get(bulk_op[0])
+                        _filter = opname + " " + str(bulk_op[1])
+                        filters = filters + _filter + " "
+
+                    max_len = config.pymongo_parameters_max_length
+                    filters = filters[0:max_len] + "..." if len(filters) > max_len else filters
+                    span.tag(Tag(key=tags.DbStatement, val=filters))
+
+            except BaseException as e:
+                span.raised()
+                raise e
+
+            return bulk_result
+
+    _Bulk.execute = _sw_execute
+
+
+def inject_cursor(Cursor):
+    __send_message = Cursor._Cursor__send_message
+
+    def _sw_send_message(this: Cursor, operation):
+        address = this.collection.database.client.address
+        peer = "%s:%s" % address
+
+        context = get_context()
+        carrier = Carrier()
+        op = "FindOperation"
+
+        with context.new_exit_span(op="MongoDB/"+op, peer=peer, carrier=carrier) as span:
+            span.layer = Layer.Database
+            span.component = Component.MongoDB
+
+            try:
+                # __send_message return nothing
+                __send_message(this, operation)
+
+                span.tag(Tag(key=tags.DbType, val="MongoDB"))
+                span.tag(Tag(key=tags.DbInstance, val=this.collection.database.name))
+
+                if config.pymongo_trace_parameters:
+                    filters = "find " + str(operation.spec)
+                    max_len = config.pymongo_parameters_max_length
+                    filters = filters[0:max_len] + "..." if len(filters) > max_len else filters
+                    span.tag(Tag(key=tags.DbStatement, val=filters))
+
+            except BaseException as e:
+                span.raised()
+                raise e
+
+            return
+
+    Cursor._Cursor__send_message = _sw_send_message
diff --git a/tests/plugin/sw_pymongo/__init__.py b/tests/plugin/sw_pymongo/__init__.py
new file mode 100644
index 0000000..b1312a0
--- /dev/null
+++ b/tests/plugin/sw_pymongo/__init__.py
@@ -0,0 +1,16 @@
+#
+# 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.
+#
diff --git a/tests/plugin/sw_pymongo/docker-compose.yml b/tests/plugin/sw_pymongo/docker-compose.yml
new file mode 100644
index 0000000..596f5be
--- /dev/null
+++ b/tests/plugin/sw_pymongo/docker-compose.yml
@@ -0,0 +1,75 @@
+#
+# 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:
+      service: collector
+      file: ../docker/docker-compose.base.yml
+
+  mongo:
+    image: mongo:4.2
+    hostname: mongo
+    ports:
+      - 27017:27017
+    healthcheck:
+      test: echo 'db.runCommand("ping").ok' | mongo mongo:27017/test --quiet
+      interval: 5s
+      timeout: 60s
+      retries: 120
+    networks:
+      - beyond
+
+  provider:
+    extends:
+      service: agent
+      file: ../docker/docker-compose.base.yml
+    ports:
+      - 9091:9091
+    volumes:
+      - ./services/provider.py:/app/provider.py
+    command: ['bash', '-c', 'pip install flask && pip install pymongo && python3 /app/provider.py']
+    depends_on:
+      collector:
+        condition: service_healthy
+      mongo:
+        condition: service_healthy
+    healthcheck:
+      test: ["CMD", "bash", "-c", "cat < /dev/null > /dev/tcp/127.0.0.1/9091"]
+      interval: 5s
+      timeout: 60s
+      retries: 120
+
+  consumer:
+    extends:
+      service: agent
+      file: ../docker/docker-compose.base.yml
+    ports:
+      - 9090:9090
+    volumes:
+      - ./services/consumer.py:/app/consumer.py
+    command: ['bash', '-c', 'pip install flask && python3 /app/consumer.py']
+    depends_on:
+      collector:
+        condition: service_healthy
+      provider:
+        condition: service_healthy
+
+networks:
+  beyond:
diff --git a/tests/plugin/sw_pymongo/expected.data.yml b/tests/plugin/sw_pymongo/expected.data.yml
new file mode 100644
index 0000000..b96bf8f
--- /dev/null
+++ b/tests/plugin/sw_pymongo/expected.data.yml
@@ -0,0 +1,239 @@
+#
+# 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: provider
+    segmentSize: 3
+    segments:
+      - segmentId: not null
+        spans:
+          - operationName: MongoDB/MixedBulkWriteOperation
+            operationId: 0
+            parentSpanId: 0
+            spanId: 1
+            spanLayer: Database
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 9
+            spanType: Exit
+            peer: not null
+            skipAnalysis: false
+            tags:
+              - key: db.type
+                value: MongoDB
+              - key: db.instance
+                value: test-database
+              - key: db.statement
+                value: not null
+          - operationName: /insert_many
+            operationId: 0
+            parentSpanId: -1
+            spanId: 0
+            spanLayer: Http
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 7001
+            spanType: Entry
+            peer: not null
+            skipAnalysis: false
+            tags:
+              - key: http.method
+                value: GET
+              - key: url
+                value: http://provider:9091/insert_many
+              - key: status.code
+                value: '200'
+            refs:
+              - parentEndpoint: /insert_many
+                networkAddress: provider:9091
+                refType: CrossProcess
+                parentSpanId: 1
+                parentTraceSegmentId: not null
+                parentServiceInstance: not null
+                parentService: consumer
+                traceId: not null
+      - segmentId: not null
+        spans:
+          - operationName: MongoDB/FindOperation
+            operationId: 0
+            parentSpanId: 0
+            spanId: 1
+            spanLayer: Database
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 9
+            spanType: Exit
+            peer: not null
+            skipAnalysis: false
+            tags:
+              - key: db.type
+                value: MongoDB
+              - key: db.instance
+                value: test-database
+              - key: db.statement
+                value: not null
+          - operationName: /find_one
+            operationId: 0
+            parentSpanId: -1
+            spanId: 0
+            spanLayer: Http
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 7001
+            spanType: Entry
+            peer: not null
+            skipAnalysis: false
+            tags:
+              - key: http.method
+                value: GET
+              - key: url
+                value: http://provider:9091/find_one
+              - key: status.code
+                value: '200'
+            refs:
+              - parentEndpoint: /find_one
+                networkAddress: provider:9091
+                refType: CrossProcess
+                parentSpanId: 2
+                parentTraceSegmentId: not null
+                parentServiceInstance: not null
+                parentService: consumer
+                traceId: not null
+      - segmentId: not null
+        spans:
+          - operationName: MongoDB/DeleteOperation
+            operationId: 0
+            parentSpanId: 0
+            spanId: 1
+            spanLayer: Database
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 9
+            spanType: Exit
+            peer: not null
+            skipAnalysis: false
+            tags:
+              - key: db.type
+                value: MongoDB
+              - key: db.instance
+                value: test-database
+              - key: db.statement
+                value: not null
+          - operationName: /delete_one
+            operationId: 0
+            parentSpanId: -1
+            spanId: 0
+            spanLayer: Http
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 7001
+            spanType: Entry
+            peer: not null
+            skipAnalysis: false
+            tags:
+              - key: http.method
+                value: GET
+              - key: url
+                value: http://provider:9091/delete_one
+              - key: status.code
+                value: '200'
+            refs:
+              - parentEndpoint: /delete_one
+                networkAddress: provider:9091
+                refType: CrossProcess
+                parentSpanId: 3
+                parentTraceSegmentId: not null
+                parentServiceInstance: not null
+                parentService: consumer
+                traceId: not null
+  - serviceName: consumer
+    segmentSize: 1
+    segments:
+      - segmentId: not null
+        spans:
+          - operationName: /insert_many
+            operationId: 0
+            parentSpanId: 0
+            spanId: 1
+            spanLayer: Http
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 7002
+            spanType: Exit
+            peer: provider:9091
+            skipAnalysis: false
+            tags:
+              - key: http.method
+                value: GET
+              - key: url
+                value: http://provider:9091/insert_many
+              - key: status.code
+                value: '200'
+          - operationName: /find_one
+            operationId: 0
+            parentSpanId: 0
+            spanId: 2
+            spanLayer: Http
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 7002
+            spanType: Exit
+            peer: provider:9091
+            skipAnalysis: false
+            tags:
+              - key: http.method
+                value: GET
+              - key: url
+                value: http://provider:9091/find_one
+              - key: status.code
+                value: '200'
+          - operationName: /delete_one
+            operationId: 0
+            parentSpanId: 0
+            spanId: 3
+            spanLayer: Http
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 7002
+            spanType: Exit
+            peer: provider:9091
+            skipAnalysis: false
+            tags:
+              - key: http.method
+                value: GET
+              - key: url
+                value: http://provider:9091/delete_one
+              - key: status.code
+                value: '200'
+          - operationName: /users
+            operationId: 0
+            parentSpanId: -1
+            spanId: 0
+            spanLayer: Http
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 7001
+            spanType: Entry
+            peer: not null
+            skipAnalysis: false
+            tags:
+              - key: http.method
+                value: GET
+              - key: url
+                value: http://0.0.0.0:9090/users
+              - key: status.code
+                value: '200'
diff --git a/tests/plugin/sw_pymongo/services/__init__.py b/tests/plugin/sw_pymongo/services/__init__.py
new file mode 100644
index 0000000..b1312a0
--- /dev/null
+++ b/tests/plugin/sw_pymongo/services/__init__.py
@@ -0,0 +1,16 @@
+#
+# 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.
+#
diff --git a/tests/plugin/sw_pymongo/services/consumer.py b/tests/plugin/sw_pymongo/services/consumer.py
new file mode 100644
index 0000000..271985a
--- /dev/null
+++ b/tests/plugin/sw_pymongo/services/consumer.py
@@ -0,0 +1,39 @@
+#
+# 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 requests
+
+from skywalking import agent, config
+
+if __name__ == '__main__':
+    config.service_name = 'consumer'
+    config.logging_level = 'DEBUG'
+    agent.start()
+
+    from flask import Flask, jsonify
+
+    app = Flask(__name__)
+
+    @app.route("/users", methods=["POST", "GET"])
+    def application():
+        requests.get("http://provider:9091/insert_many")
+        requests.get("http://provider:9091/find_one")
+        res = requests.get("http://provider:9091/delete_one")
+        return jsonify(res.json())
+
+    PORT = 9090
+    app.run(host='0.0.0.0', port=PORT, debug=True)
diff --git a/tests/plugin/sw_pymongo/services/provider.py b/tests/plugin/sw_pymongo/services/provider.py
new file mode 100644
index 0000000..153e26b
--- /dev/null
+++ b/tests/plugin/sw_pymongo/services/provider.py
@@ -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 time
+
+from skywalking import agent, config
+from flask import Flask, jsonify
+from pymongo import MongoClient
+
+
+config.service_name = "provider"
+config.logging_level = "DEBUG"
+config.pymongo_trace_parameters = True
+agent.start()
+
+
+client = MongoClient('mongodb://mongo:27017/')
+db = client['test-database']
+collection = db['test-collection']
+
+
+app = Flask(__name__)
+
+
+@app.route("/insert_many", methods=["GET"])
+def test_insert_many():
+    time.sleep(0.5)
+    new_posts = [{"song": "Despacito"},
+                 {"artist": "Luis Fonsi"}]
+    result = collection.insert_many(new_posts)
+    return jsonify({"ok": result.acknowledged})
+
+
+@app.route("/find_one", methods=["GET"])
+def test_find_one():
+    time.sleep(0.5)
+    result = collection.find_one({"song": "Despacito"})
+    # have to get the result and use it. if not lint will report error
+    print(result)
+    return jsonify({"song": "Despacito"})
+
+
+@app.route("/delete_one", methods=["GET"])
+def test_delete_one():
+    time.sleep(0.5)
+    result = collection.delete_one({"song": "Despacito"})
+    return jsonify({"ok": result.acknowledged})
+
+
+if __name__ == '__main__':
+    PORT = 9091
+    app.run(host="0.0.0.0", port=PORT, debug=True)
diff --git a/tests/plugin/sw_pymongo/test_pymongo.py b/tests/plugin/sw_pymongo/test_pymongo.py
new file mode 100644
index 0000000..f7eb788
--- /dev/null
+++ b/tests/plugin/sw_pymongo/test_pymongo.py
@@ -0,0 +1,42 @@
+#
+# 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 inspect
+import time
+import unittest
+from os.path import dirname
+
+from testcontainers.compose import DockerCompose
+
+from tests.plugin import BasePluginTest
+
+
+class TestPlugin(BasePluginTest):
+    @classmethod
+    def setUpClass(cls):
+        cls.compose = DockerCompose(filepath=dirname(inspect.getfile(cls)))
+        cls.compose.start()
+
+        cls.compose.wait_for(cls.url(('consumer', '9090'), 'users'))
+
+    def test_plugin(self):
+        time.sleep(10)
+
+        self.validate()
+
+
+if __name__ == '__main__':
+    unittest.main()