You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by to...@apache.org on 2021/06/06 17:05:35 UTC

[skywalking-python] branch master updated: psycopg plugin (#122)

This is an automated email from the ASF dual-hosted git repository.

tompytel 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 89b4c20  psycopg plugin (#122)
89b4c20 is described below

commit 89b4c2091f53704e654be124519545488d1834fe
Author: Tomasz Pytel <to...@gmail.com>
AuthorDate: Sun Jun 6 14:05:25 2021 -0300

    psycopg plugin (#122)
    
    * WIP initial psycopg plugin
    
    * sql_parameters_length option
    
    * converted to PostgreSQL component
---
 Makefile                                           |   2 +-
 docs/EnvVars.md                                    |   7 +-
 docs/Plugins.md                                    |   3 +-
 requirements.txt                                   |   1 +
 skywalking/__init__.py                             |   1 +
 skywalking/config.py                               |   4 +-
 skywalking/plugins/sw_psycopg2.py                  | 122 +++++++++++++++++++++
 skywalking/plugins/sw_pymysql.py                   |   4 +-
 tests/plugin/sw_psycopg2/__init__.py               |  16 +++
 tests/plugin/sw_psycopg2/docker-compose.yml        |  79 +++++++++++++
 tests/plugin/sw_psycopg2/expected.data.yml         | 114 +++++++++++++++++++
 tests/plugin/sw_psycopg2/services/__init__.py      |  16 +++
 .../services/consumer.py}                          |  19 +---
 .../services/provider.py                           |   6 +-
 tests/plugin/sw_psycopg2/test_psycopg2.py          |  36 ++++++
 tests/plugin/sw_pymysql/services/provider.py       |   2 +-
 16 files changed, 403 insertions(+), 29 deletions(-)

diff --git a/Makefile b/Makefile
index 58590fa..e12c92e 100644
--- a/Makefile
+++ b/Makefile
@@ -32,7 +32,7 @@ gen:
 lint: clean
 	flake8 --version || python3 -m pip install flake8
 	flake8 . --count --select=E9,F63,F7,F82 --show-source
-	flake8 . --count --max-complexity=13 --max-line-length=120
+	flake8 . --count --max-complexity=15 --max-line-length=120
 
 license: clean
 	python3 tools/check-license-header.py skywalking tests tools
diff --git a/docs/EnvVars.md b/docs/EnvVars.md
index 6172dbf..cc23edd 100644
--- a/docs/EnvVars.md
+++ b/docs/EnvVars.md
@@ -9,9 +9,8 @@ Environment Variable | Description | Default
 | `SW_AGENT_AUTHENTICATION` | The authentication token to verify that the agent is trusted by the backend OAP, as for how to configure the backend, refer to [the yaml](https://github.com/apache/skywalking/blob/4f0f39ffccdc9b41049903cc540b8904f7c9728e/oap-server/server-bootstrap/src/main/resources/application.yml#L155-L158). | unset |
 | `SW_AGENT_LOGGING_LEVEL` | The logging level, could be one of `CRITICAL`, `FATAL`, `ERROR`, `WARN`(`WARNING`), `INFO`, `DEBUG` | `INFO` |
 | `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_SQL_PARAMETERS_LENGTH` | The maximum length of the collected parameter, parameters longer than the specified length will be truncated, length 0 turns off parameter tracing | `0` |
+| `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` |
@@ -23,6 +22,6 @@ Environment Variable | Description | Default
 | `SW_TRACE_IGNORE_PATH`| You can setup multiple URL path patterns, The endpoints match these patterns wouldn't be traced. the current matching rules follow Ant Path match style , like /path/*, /path/**, /path/?.| `''` |
 | `SW_ELASTICSEARCH_TRACE_DSL`| If true, trace all the DSL(Domain Specific Language) in ElasticSearch access, default is false | `false` |
 | `SW_KAFKA_REPORTER_BOOTSTRAP_SERVERS` | A list of host/port pairs to use for establishing the initial connection to the Kafka cluster. It is in the form host1:port1,host2:port2,... | `localhost:9092` |
-| `SW_KAFKA_REPORTER_TOPIC_MANAGEMENT` | Specifying Kafka topic name for service instance reporting and registering. | `skywalking-managements` | 
+| `SW_KAFKA_REPORTER_TOPIC_MANAGEMENT` | Specifying Kafka topic name for service instance reporting and registering. | `skywalking-managements` |
 | `SW_KAFKA_REPORTER_TOPIC_SEGMENT` | Specifying Kafka topic name for Tracing data. | `skywalking-segments` |
 | `SW_KAFKA_REPORTER_CONFIG_key` | The configs to init KafkaProducer. it support the basic arguments (whose type is either `str`, `bool`, or `int`) listed [here](https://kafka-python.readthedocs.io/en/master/apidoc/KafkaProducer.html#kafka.KafkaProducer) | unset |
diff --git a/docs/Plugins.md b/docs/Plugins.md
index fc7f1d6..28688e0 100644
--- a/docs/Plugins.md
+++ b/docs/Plugins.md
@@ -2,7 +2,7 @@
 
 Library | Versions | Plugin Name
 | :--- | :--- | :--- |
-| [http.server](https://docs.python.org/3/library/http.server.html) | Python 3.5 ~ 3.8 | `sw_http_server` |
+| [http.server](https://docs.python.org/3/library/http.server.html) | Python 3.5 ~ 3.9 | `sw_http_server` |
 | [urllib.request](https://docs.python.org/3/library/urllib.request.html) | Python 3.5 ~ 3.8 | `sw_urllib_request` |
 | [requests](https://requests.readthedocs.io/en/master/) | >= 2.9.0 < 2.15.0, >= 2.17.0 <= 2.24.0 | `sw_requests` |
 | [Flask](https://flask.palletsprojects.com/en/1.1.x/) | >=1.0.4 <= 1.1.2 | `sw_flask` |
@@ -18,5 +18,6 @@ Library | Versions | Plugin Name
 | [sanic](https://sanic.readthedocs.io/en/latest/) | >= 20.3.0 <= 20.9.1 | `sw_sanic` |
 | [aiohttp](https://sanic.readthedocs.io/en/latest/) | >= 3.7.3 | `sw_aiohttp` |
 | [pyramid](https://trypyramid.com) | >= 1.9 | `sw_pyramid` |
+| [psycopg2](https://www.psycopg.org/) | 2.8.6 | `sw_psycopg2` |
 
 The column `Versions` only indicates that the versions are tested, if you found the newer versions are also supported, welcome to add the newer version into the table.
diff --git a/requirements.txt b/requirements.txt
index c0ccf07..91a06d2 100755
--- a/requirements.txt
+++ b/requirements.txt
@@ -34,6 +34,7 @@ parameterized==0.7.4
 pika==1.1.0
 pluggy==0.13.1
 protobuf==3.12.4
+psycopg2-binary==2.8.6
 py==1.9.0
 pymongo==3.11.0
 PyMySQL==0.10.0
diff --git a/skywalking/__init__.py b/skywalking/__init__.py
index eb180f1..482d39e 100644
--- a/skywalking/__init__.py
+++ b/skywalking/__init__.py
@@ -31,6 +31,7 @@ class Component(Enum):
     Tornado = 7005
     Redis = 7
     MongoDB = 9
+    PostgreSQL = 22
     KafkaProducer = 40
     KafkaConsumer = 41
     RabbitmqProducer = 52
diff --git a/skywalking/config.py b/skywalking/config.py
index d952381..60c2419 100644
--- a/skywalking/config.py
+++ b/skywalking/config.py
@@ -37,9 +37,7 @@ protocol = (os.getenv('SW_AGENT_PROTOCOL') or 'grpc').lower()  # type: str
 authentication = os.getenv('SW_AGENT_AUTHENTICATION')  # type: str
 logging_level = os.getenv('SW_AGENT_LOGGING_LEVEL') or 'INFO'  # type: str
 disable_plugins = (os.getenv('SW_AGENT_DISABLE_PLUGINS') or '').split(',')  # type: List[str]
-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
+sql_parameters_length = int(os.getenv('SW_SQL_PARAMETERS_LENGTH') or '0')  # 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
diff --git a/skywalking/plugins/sw_psycopg2.py b/skywalking/plugins/sw_psycopg2.py
new file mode 100644
index 0000000..1d5d137
--- /dev/null
+++ b/skywalking/plugins/sw_psycopg2.py
@@ -0,0 +1,122 @@
+#
+# 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.
+#
+
+from skywalking import Layer, Component, config
+from skywalking.trace import tags
+from skywalking.trace.context import get_context
+from skywalking.trace.tags import Tag
+
+
+def install():
+    import wrapt  # psycopg2 is read-only C extension objects so they need to be proxied
+    import psycopg2
+
+    class ProxyCursor(wrapt.ObjectProxy):
+        def __init__(self, cur):
+            wrapt.ObjectProxy.__init__(self, cur)
+
+            self._self_cur = cur
+
+        def __enter__(self):
+            return ProxyCursor(wrapt.ObjectProxy.__enter__(self))
+
+        def execute(self, query, vars=None):
+            dsn = self.connection.get_dsn_parameters()
+            peer = dsn['host'] + ':' + dsn['port']
+
+            with get_context().new_exit_span(op="PostgreSLQ/Psycopg/execute", peer=peer) as span:
+                span.layer = Layer.Database
+                span.component = Component.PostgreSQL
+
+                span.tag(Tag(key=tags.DbType, val="PostgreSQL"))
+                span.tag(Tag(key=tags.DbInstance, val=dsn['dbname']))
+                span.tag(Tag(key=tags.DbStatement, val=query))
+
+                if config.sql_parameters_length and vars is not None:
+                    text = ','.join(str(v) for v in vars)
+
+                    if len(text) > config.sql_parameters_length:
+                        text = text[:config.sql_parameters_length] + '...'
+
+                    span.tag(Tag(key=tags.DbSqlParameters, val='[' + text + ']'))
+
+                return self._self_cur.execute(query, vars)
+
+        def executemany(self, query, vars_list):
+            dsn = self.connection.get_dsn_parameters()
+            peer = dsn['host'] + ':' + dsn['port']
+
+            with get_context().new_exit_span(op="PostgreSLQ/Psycopg/executemany", peer=peer) as span:
+                span.layer = Layer.Database
+                span.component = Component.PostgreSQL
+
+                span.tag(Tag(key=tags.DbType, val="PostgreSQL"))
+                span.tag(Tag(key=tags.DbInstance, val=dsn['dbname']))
+                span.tag(Tag(key=tags.DbStatement, val=query))
+
+                if config.sql_parameters_length:
+                    max_len = config.sql_parameters_length
+                    total_len = 0
+                    text_list = []
+
+                    for vars in vars_list:
+                        text = '[' + ','.join(str(v) for v in vars) + ']'
+                        total_len += len(text)
+
+                        if total_len > max_len:
+                            text_list.append(text[:max_len - total_len] + '...')
+
+                            break
+
+                        text_list.append(text)
+
+                    span.tag(Tag(key=tags.DbSqlParameters, val='[' + ','.join(text_list) + ']'))
+
+                return self._self_cur.executemany(query, vars_list)
+
+        def callproc(self, procname, parameters=None):
+            dsn = self.connection.get_dsn_parameters()
+            peer = dsn['host'] + ':' + dsn['port']
+
+            with get_context().new_exit_span(op="PostgreSLQ/Psycopg/callproc", peer=peer) as span:
+                span.layer = Layer.Database
+                span.component = Component.PostgreSQL
+                args = '(' + ('' if not parameters else ','.join(parameters)) + ')'
+
+                span.tag(Tag(key=tags.DbType, val="PostgreSQL"))
+                span.tag(Tag(key=tags.DbInstance, val=dsn['dbname']))
+                span.tag(Tag(key=tags.DbStatement, val=procname + args))
+
+                return self._self_cur.callproc(procname, parameters)
+
+    class ProxyConnection(wrapt.ObjectProxy):
+        def __init__(self, conn):
+            wrapt.ObjectProxy.__init__(self, conn)
+
+            self._self_conn = conn
+
+        def __enter__(self):
+            return ProxyConnection(wrapt.ObjectProxy.__enter__(self))
+
+        def cursor(self, *args, **kwargs):
+            return ProxyCursor(self._self_conn.cursor(*args, **kwargs))
+
+    def connect(*args, **kwargs):
+        return ProxyConnection(_connect(*args, **kwargs))
+
+    _connect = psycopg2.connect
+    psycopg2.connect = connect
diff --git a/skywalking/plugins/sw_pymysql.py b/skywalking/plugins/sw_pymysql.py
index 15acdb5..715f918 100644
--- a/skywalking/plugins/sw_pymysql.py
+++ b/skywalking/plugins/sw_pymysql.py
@@ -39,9 +39,9 @@ def install():
             span.tag(Tag(key=tags.DbInstance, val=(this.connection.db or b'').decode("utf-8")))
             span.tag(Tag(key=tags.DbStatement, val=query))
 
-            if config.mysql_trace_sql_parameters and args:
+            if config.sql_parameters_length and args:
                 parameter = ",".join([str(arg) for arg in args])
-                max_len = config.mysql_sql_parameters_max_length
+                max_len = config.sql_parameters_length
                 parameter = parameter[0:max_len] + "..." if len(parameter) > max_len else parameter
                 span.tag(Tag(key=tags.DbSqlParameters, val='[' + parameter + ']'))
 
diff --git a/tests/plugin/sw_psycopg2/__init__.py b/tests/plugin/sw_psycopg2/__init__.py
new file mode 100644
index 0000000..b1312a0
--- /dev/null
+++ b/tests/plugin/sw_psycopg2/__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_psycopg2/docker-compose.yml b/tests/plugin/sw_psycopg2/docker-compose.yml
new file mode 100644
index 0000000..dfcf6c0
--- /dev/null
+++ b/tests/plugin/sw_psycopg2/docker-compose.yml
@@ -0,0 +1,79 @@
+#
+# 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
+
+  postgres:
+    image: postgres:11
+    hostname: postgres
+    ports:
+      - 5432:5432
+    environment:
+      - POSTGRES_USER=root
+      - POSTGRES_PASSWORD=root
+      - POSTGRES_DB=admin
+    healthcheck:
+      test: ["CMD", "bash", "-c", "cat < /dev/null > /dev/tcp/127.0.0.1/5432"]
+      interval: 5s
+      timeout: 60s
+      retries: 120
+    networks:
+      - beyond
+
+  provider:
+    extends:
+      service: agent
+      file: ../docker/docker-compose.base.yml
+    ports:
+      - 9091:9091
+    volumes:
+      - .:/app
+    command: ['bash', '-c', 'pip install flask && pip install -r /app/requirements.txt && python3 /app/services/provider.py']
+    depends_on:
+      collector:
+        condition: service_healthy
+      postgres:
+        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:
+      - .:/app
+    command: ['bash', '-c', 'pip install flask && python3 /app/services/consumer.py']
+    depends_on:
+      collector:
+        condition: service_healthy
+      provider:
+        condition: service_healthy
+
+networks:
+  beyond:
diff --git a/tests/plugin/sw_psycopg2/expected.data.yml b/tests/plugin/sw_psycopg2/expected.data.yml
new file mode 100644
index 0000000..6ff4ea5
--- /dev/null
+++ b/tests/plugin/sw_psycopg2/expected.data.yml
@@ -0,0 +1,114 @@
+#
+# 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: 1
+    segments:
+      - segmentId: not null
+        spans:
+          - operationName: PostgreSLQ/Psycopg/execute
+            operationId: 0
+            parentSpanId: 0
+            spanId: 1
+            spanLayer: Database
+            tags:
+              - key: db.type
+                value: PostgreSQL
+              - key: db.instance
+                value: admin
+              - key: db.statement
+                value: 'select * from user where user = %s'
+              - key: db.sql.parameters
+                value: '[root]'
+            startTime: gt 0
+            endTime: gt 0
+            isError: false
+            componentId: 22
+            spanType: Exit
+            peer: postgres:5432
+            skipAnalysis: false
+          - operationName: /users
+            operationId: 0
+            parentSpanId: -1
+            spanId: 0
+            spanLayer: Http
+            tags:
+              - key: http.method
+                value: POST
+              - key: url
+                value: http://provider:9091/users
+              - key: status.code
+                value: '200'
+            refs:
+              - parentEndpoint: /users
+                networkAddress: provider:9091
+                refType: CrossProcess
+                parentSpanId: 1
+                parentTraceSegmentId: not null
+                parentServiceInstance: not null
+                parentService: consumer
+                traceId: not null
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 7001
+            spanType: Entry
+            peer: not null
+            skipAnalysis: false
+  - serviceName: consumer
+    segmentSize: 1
+    segments:
+      - segmentId: not null
+        spans:
+          - operationName: /users
+            operationId: 0
+            parentSpanId: 0
+            spanId: 1
+            spanLayer: Http
+            tags:
+              - key: http.method
+                value: POST
+              - key: url
+                value: http://provider:9091/users
+              - key: status.code
+                value: '200'
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 7002
+            isError: false
+            spanType: Exit
+            peer: provider:9091
+            skipAnalysis: false
+          - operationName: /users
+            operationId: 0
+            parentSpanId: -1
+            spanId: 0
+            spanLayer: Http
+            tags:
+              - key: http.method
+                value: GET
+              - key: url
+                value: http://0.0.0.0:9090/users
+              - key: status.code
+                value: '200'
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 7001
+            isError: false
+            spanType: Entry
+            peer: not null
+            skipAnalysis: false
\ No newline at end of file
diff --git a/tests/plugin/sw_psycopg2/services/__init__.py b/tests/plugin/sw_psycopg2/services/__init__.py
new file mode 100644
index 0000000..b1312a0
--- /dev/null
+++ b/tests/plugin/sw_psycopg2/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_pymysql/services/provider.py b/tests/plugin/sw_psycopg2/services/consumer.py
similarity index 68%
copy from tests/plugin/sw_pymysql/services/provider.py
copy to tests/plugin/sw_psycopg2/services/consumer.py
index 0e9f86f..c942f91 100644
--- a/tests/plugin/sw_pymysql/services/provider.py
+++ b/tests/plugin/sw_psycopg2/services/consumer.py
@@ -15,32 +15,23 @@
 # limitations under the License.
 #
 
-import time
+import requests
 
 from skywalking import agent, config
 
 if __name__ == '__main__':
-    config.service_name = 'provider'
+    config.service_name = 'consumer'
     config.logging_level = 'DEBUG'
-    config.mysql_trace_sql_parameters = True
     agent.start()
 
     from flask import Flask, jsonify
-    import pymysql.cursors
 
     app = Flask(__name__)
 
     @app.route("/users", methods=["POST", "GET"])
     def application():
-        time.sleep(0.5)
-        connection = pymysql.connect(host='mysql', user='root', password='root', db='mysql', charset='utf8mb4')
-        with connection.cursor() as cursor:
-            sql = "select * from user where user = %s"
-            cursor.execute(sql, ("root",))
+        res = requests.post("http://provider:9091/users")
+        return jsonify(res.json())
 
-        connection.close()
-
-        return jsonify({"song": "Despacito", "artist": "Luis Fonsi"})
-
-    PORT = 9091
+    PORT = 9090
     app.run(host='0.0.0.0', port=PORT, debug=True)
diff --git a/tests/plugin/sw_pymysql/services/provider.py b/tests/plugin/sw_psycopg2/services/provider.py
similarity index 88%
copy from tests/plugin/sw_pymysql/services/provider.py
copy to tests/plugin/sw_psycopg2/services/provider.py
index 0e9f86f..bf03a88 100644
--- a/tests/plugin/sw_pymysql/services/provider.py
+++ b/tests/plugin/sw_psycopg2/services/provider.py
@@ -22,18 +22,18 @@ from skywalking import agent, config
 if __name__ == '__main__':
     config.service_name = 'provider'
     config.logging_level = 'DEBUG'
-    config.mysql_trace_sql_parameters = True
+    config.sql_parameters_length = 512
     agent.start()
 
     from flask import Flask, jsonify
-    import pymysql.cursors
+    import psycopg2
 
     app = Flask(__name__)
 
     @app.route("/users", methods=["POST", "GET"])
     def application():
         time.sleep(0.5)
-        connection = pymysql.connect(host='mysql', user='root', password='root', db='mysql', charset='utf8mb4')
+        connection = psycopg2.connect(host='postgres', user='root', password='root', dbname='admin')
         with connection.cursor() as cursor:
             sql = "select * from user where user = %s"
             cursor.execute(sql, ("root",))
diff --git a/tests/plugin/sw_psycopg2/test_psycopg2.py b/tests/plugin/sw_psycopg2/test_psycopg2.py
new file mode 100644
index 0000000..a21cb72
--- /dev/null
+++ b/tests/plugin/sw_psycopg2/test_psycopg2.py
@@ -0,0 +1,36 @@
+#
+# 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.
+#
+from typing import Callable
+
+import pytest
+import requests
+
+from tests.plugin.base import TestPluginBase
+
+
+@pytest.fixture
+def prepare():
+    # type: () -> Callable
+    return lambda *_: requests.get('http://0.0.0.0:9090/users')
+
+
+class TestPlugin(TestPluginBase):
+    @pytest.mark.parametrize('version', [
+        'psycopg2-binary==2.8.6',
+    ])
+    def test_plugin(self, docker_compose, version):
+        self.validate()
diff --git a/tests/plugin/sw_pymysql/services/provider.py b/tests/plugin/sw_pymysql/services/provider.py
index 0e9f86f..8bb9cc5 100644
--- a/tests/plugin/sw_pymysql/services/provider.py
+++ b/tests/plugin/sw_pymysql/services/provider.py
@@ -22,7 +22,7 @@ from skywalking import agent, config
 if __name__ == '__main__':
     config.service_name = 'provider'
     config.logging_level = 'DEBUG'
-    config.mysql_trace_sql_parameters = True
+    config.sql_parameters_length = 512
     agent.start()
 
     from flask import Flask, jsonify