Add PgPlugin - PosgreSQL (#31)

diff --git a/README.md b/README.md
index 94c2d0c..f710d45 100644
--- a/README.md
+++ b/README.md
@@ -57,7 +57,7 @@
 | `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_MYSQL_SQL_PARAMETERS_MAX_LENGTH` | The maximum string length of MySQL parameters to log | `512` |
+| `SW_SQL_PARAMETERS_MAX_LENGTH` | The maximum string length of SQL parameters to log | `512` |
 | `SW_AGENT_MAX_BUFFER_SIZE` | The maximum buffer size before sending the segment data to backend | `'1000'` |
 
 ## Supported Libraries
@@ -70,6 +70,7 @@
 | [`express`](https://expressjs.com) | `express` |
 | [`axios`](https://github.com/axios/axios) | `axios` |
 | [`mysql`](https://github.com/mysqljs/mysql) | `mysql` |
+| [`pg`](https://github.com/brianc/node-postgres) | `pg` |
 
 ### Compatible Libraries
 
diff --git a/package.json b/package.json
index f6c9d31..b037c70 100644
--- a/package.json
+++ b/package.json
@@ -46,9 +46,11 @@
     "@types/uuid": "^8.0.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",
+    "mysql": "^2.18.1",
+    "pg": "^8.5.1",
     "prettier": "^2.0.5",
     "testcontainers": "^6.2.0",
     "ts-jest": "^26.4.4",
diff --git a/src/config/AgentConfig.ts b/src/config/AgentConfig.ts
index de68ccb..a28be12 100644
--- a/src/config/AgentConfig.ts
+++ b/src/config/AgentConfig.ts
@@ -27,7 +27,7 @@
   maxBufferSize?: number;
   ignoreSuffix?: string;
   traceIgnorePath?: string;
-  mysql_sql_parameters_max_length?: number;
+  sql_parameters_max_length?: number;
   // the following is internal state computed from config values
   reIgnoreOperation?: RegExp;
 };
@@ -41,7 +41,7 @@
       (s2) => s2.split('*').map(
         (s3) => s3.split('?').map(escapeRegExp).join('[^/]')  // replaces "?"
       ).join('[^/]*')                                         // replaces "*"
-    ).join('(?:(?:[^/]+\.)*[^/]+)?')                          // replaces "**"
+    ).join('(?:(?:[^/]+/)*[^/]+)?')                           // replaces "**"
   ).join('|') + ')$';                                         // replaces ","
 
   config.reIgnoreOperation = RegExp(`${ignoreSuffix}|${ignorePath}`);
@@ -60,6 +60,6 @@
     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 || '',
-  mysql_sql_parameters_max_length: Math.trunc(Math.max(0, Number(process.env.SW_MYSQL_SQL_PARAMETERS_MAX_LENGTH))) || 512,
+  sql_parameters_max_length: Math.trunc(Math.max(0, Number(process.env.SW_SQL_SQL_PARAMETERS_MAX_LENGTH))) || 512,
   reIgnoreOperation: RegExp(''),  // temporary placeholder so Typescript doesn't throw a fit
 };
diff --git a/src/plugins/MySQLPlugin.ts b/src/plugins/MySQLPlugin.ts
index 6a3e6dd..8e87ab6 100644
--- a/src/plugins/MySQLPlugin.ts
+++ b/src/plugins/MySQLPlugin.ts
@@ -43,6 +43,8 @@
     Connection.prototype.query = function(sql: any, values: any, cb: any) {
       const wrapCallback = (_cb: any) => {
         return function(this: any, error: any, results: any, fields: any) {
+          span.resync();
+
           if (error)
             span.error(error);
 
@@ -52,10 +54,19 @@
         }
       };
 
+      let query: any;
+
       const host = `${this.config.host}:${this.config.port}`;
       const span = ContextManager.current.newExitSpan('mysql/query', host).start();
 
       try {
+        span.component = Component.MYSQL;
+        span.layer = SpanLayer.DATABASE;
+        span.peer = host;
+
+        span.tag(Tag.dbType('Mysql'));
+        span.tag(Tag.dbInstance(`${this.config.database}`));
+
         let _sql: any;
         let _values: any;
         let streaming: any;
@@ -103,31 +114,30 @@
           }
         }
 
-        span.component = Component.MYSQL;
-        span.layer = SpanLayer.DATABASE;
-        span.peer = host;
-
-        span.tag(Tag.dbType('mysql'));
-        span.tag(Tag.dbInstance(this.config.database || ''));
-        span.tag(Tag.dbStatement(_sql || ''));
+        span.tag(Tag.dbStatement(`${_sql}`));
 
         if (_values) {
-          let vals = _values.map((v: any) => `${v}`).join(', ');
+          let vals = _values.map((v: any) => v === undefined ? 'undefined' : JSON.stringify(v)).join(', ');
 
-          if (vals.length > config.mysql_sql_parameters_max_length)
-            vals = vals.splice(0, config.mysql_sql_parameters_max_length);
+          if (vals.length > config.sql_parameters_max_length)
+            vals = vals.splice(0, config.sql_parameters_max_length);
 
-            span.tag(Tag.dbSqlParameters(`[${vals}]`));
+          span.tag(Tag.dbSqlParameters(`[${vals}]`));
         }
 
-        const query = _query.call(this, sql, values, cb);
+        query = _query.call(this, sql, values, cb);
 
         if (streaming) {
-          query.on('error', (e: any) => span.error(e));
-          query.on('end', () => span.stop());
-        }
+          query.on('error', (e: any) => {
+            span.resync();
+            span.error(e);
+          });
 
-        return query;
+          query.on('end', () => {
+            span.resync();  // may have already been done in 'error' but safe to do multiple times
+            span.stop()
+          });
+        }
 
       } catch (e) {
         span.error(e);
@@ -135,6 +145,10 @@
 
         throw e;
       }
+
+      span.async();
+
+      return query;
     };
   }
 }
diff --git a/src/plugins/PgPlugin.ts b/src/plugins/PgPlugin.ts
new file mode 100644
index 0000000..e17cf60
--- /dev/null
+++ b/src/plugins/PgPlugin.ts
@@ -0,0 +1,138 @@
+/*!
+ *
+ * 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 Tag from '../Tag';
+import { SpanLayer } from '../proto/language-agent/Tracing_pb';
+import { createLogger } from '../logging';
+import PluginInstaller from '../core/PluginInstaller';
+import agentConfig from '../config/AgentConfig';
+
+const logger = createLogger(__filename);
+
+class MySQLPlugin implements SwPlugin {
+  readonly module = 'pg';
+  readonly versions = '*';
+
+  install(installer: PluginInstaller): void {
+    if (logger.isDebugEnabled()) {
+      logger.debug('installing pg plugin');
+    }
+
+    const Client = installer.require('pg/lib/client');
+    const _query = Client.prototype.query;
+
+    Client.prototype.query = function(config: any, values: any, callback: any) {
+      const wrapCallback = (_cb: any) => {
+        return function(this: any, err: any, res: any) {
+          span.resync();
+
+          if (err)
+            span.error(err);
+
+          span.stop();
+
+          return _cb.call(this, err, res);
+        }
+      };
+
+      let query: any;
+
+      const host = `${this.host}:${this.port}`;
+      const span = ContextManager.current.newExitSpan('pg/query', host).start();
+
+      try {
+        span.component = Component.POSTGRESQL;
+        span.layer = SpanLayer.DATABASE;
+        span.peer = host;
+
+        span.tag(Tag.dbType('PostgreSQL'));
+        span.tag(Tag.dbInstance(`${this.connectionParameters.database}`));
+
+        let _sql: any;
+        let _values: any;
+
+        if (typeof config === 'string')
+          _sql = config;
+
+        else if (config !== null && config !== undefined) {
+          _sql    = config.text;
+          _values = config.values;
+
+          if (typeof config.callback === 'function')
+            config.callback = wrapCallback(config.callback);
+        }
+
+        if (typeof values === 'function')
+          values = wrapCallback(values);
+        else
+          _values = values;
+
+        if (typeof callback === 'function')
+          callback = wrapCallback(callback);
+
+        span.tag(Tag.dbStatement(`${_sql}`));
+
+        if (_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);
+
+            span.tag(Tag.dbSqlParameters(`[${vals}]`));
+        }
+
+        query = _query.call(this, config, values, callback);
+
+        if (query && typeof query.then === 'function' && typeof query.catch === '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);
+            }
+          );
+
+      } catch (e) {
+        span.error(e);
+        span.stop();
+
+        throw e;
+      }
+
+      span.async();
+
+      return query;
+    };
+  }
+}
+
+// noinspection JSUnusedGlobalSymbols
+export default new MySQLPlugin();
diff --git a/src/trace/Component.ts b/src/trace/Component.ts
index 6469900..6c3a69d 100644
--- a/src/trace/Component.ts
+++ b/src/trace/Component.ts
@@ -22,6 +22,7 @@
   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 EXPRESS = new Component(4002);
   static readonly AXIOS = new Component(4005);
diff --git a/tests/plugins/axios/docker-compose.yml b/tests/plugins/axios/docker-compose.yml
index b88cbee..0216550 100644
--- a/tests/plugins/axios/docker-compose.yml
+++ b/tests/plugins/axios/docker-compose.yml
@@ -15,7 +15,7 @@
 # limitations under the License.
 #
 
-version: '3.8'
+version: '2.1'
 
 services:
   collector:
diff --git a/tests/plugins/express/docker-compose.yml b/tests/plugins/express/docker-compose.yml
index 224e4c6..69c9e35 100644
--- a/tests/plugins/express/docker-compose.yml
+++ b/tests/plugins/express/docker-compose.yml
@@ -15,7 +15,7 @@
 # limitations under the License.
 #
 
-version: '3.8'
+version: '2.1'
 
 services:
   collector:
diff --git a/tests/plugins/mysql/client.ts b/tests/plugins/mysql/client.ts
new file mode 100644
index 0000000..2f33af1
--- /dev/null
+++ b/tests/plugins/mysql/client.ts
@@ -0,0 +1,38 @@
+/*!
+ *
+ * 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';
+
+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/mysql/docker-compose.yml b/tests/plugins/mysql/docker-compose.yml
new file mode 100644
index 0000000..e0bb15e
--- /dev/null
+++ b/tests/plugins/mysql/docker-compose.yml
@@ -0,0 +1,89 @@
+#
+# 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
+
+  mysql:
+    container_name: mysql
+    environment:
+      MYSQL_ROOT_PASSWORD: "root"
+      MYSQL_DATABASE: "test"
+    ports:
+      - 3306:3306
+    volumes:
+      - ./init:/docker-entrypoint-initdb.d
+    healthcheck:
+      test: ["CMD", "bash", "-c", "cat < /dev/null > /dev/tcp/127.0.0.1/3306"]
+      interval: 5s
+      timeout: 60s
+      retries: 120
+    image: "docker.io/mysql:5.7.33"
+    networks:
+      - traveling-light
+
+  server:
+    extends:
+      file: ../common/base-compose.yml
+      service: agent
+    ports:
+      - 5000:5000
+    environment:
+      MYSQL_HOST: mysql
+    volumes:
+      - .:/app/tests/plugins/mysql
+    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/mysql/server.ts"]
+    depends_on:
+      collector:
+        condition: service_healthy
+      mysql:
+        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/mysql/client.ts"]
+    depends_on:
+      server:
+        condition: service_healthy
+
+networks:
+  traveling-light:
diff --git a/tests/plugins/mysql/expected.data.yaml b/tests/plugins/mysql/expected.data.yaml
new file mode 100644
index 0000000..bc8bc1e
--- /dev/null
+++ b/tests/plugins/mysql/expected.data.yaml
@@ -0,0 +1,111 @@
+#
+# 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: /mysql
+            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/mysql
+              - key: http.method
+                value: GET
+              - key: http.status.code
+                value: "200"
+            refs:
+              - parentEndpoint: ""
+                networkAddress: server:5000
+                refType: CrossProcess
+                parentSpanId: 1
+                parentTraceSegmentId: not null
+                parentServiceInstance: not null
+                parentService: client
+                traceId: not null
+          - operationName: mysql/query
+            operationId: 0
+            parentSpanId: 0
+            spanId: 1
+            spanLayer: Database
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 5
+            spanType: Exit
+            peer: mysql:3306
+            skipAnalysis: false
+            tags:
+              - key: db.type
+                value: Mysql
+              - key: db.instance
+                value: test
+              - key: db.statement
+                value: SELECT * FROM `user` WHERE `name` = "u1"
+  - serviceName: client
+    segmentSize: 1
+    segments:
+      - segmentId: not null
+        spans:
+          - operationName: /mysql
+            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/mysql
+              - key: http.method
+                value: GET
+              - key: http.status.code
+                value: "200"
+          - operationName: /mysql
+            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/mysql
+              - key: http.method
+                value: GET
+              - key: http.status.code
+                value: "200"
+              - key: http.status.msg
+                value: OK
diff --git a/tests/plugins/mysql/init/init.sql b/tests/plugins/mysql/init/init.sql
new file mode 100644
index 0000000..844112b
--- /dev/null
+++ b/tests/plugins/mysql/init/init.sql
@@ -0,0 +1,22 @@
+-- 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.
+
+use test;
+
+CREATE TABLE IF NOT EXISTS `user`(
+   `id` INT UNSIGNED AUTO_INCREMENT,
+   `name` VARCHAR(100) NOT NULL,
+   PRIMARY KEY( `id` )
+)ENGINE=InnoDB DEFAULT CHARSET=utf8;
diff --git a/tests/plugins/mysql/server.ts b/tests/plugins/mysql/server.ts
new file mode 100644
index 0000000..2d9e20d
--- /dev/null
+++ b/tests/plugins/mysql/server.ts
@@ -0,0 +1,47 @@
+/*!
+ *
+ * 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 mysql from 'mysql';
+import agent from '../../../src';
+
+agent.start({
+  serviceName: 'server',
+  maxBufferSize: 1000,
+})
+
+const server = http.createServer((req, res) => {
+  const connection = mysql.createConnection({
+    host: process.env.MYSQL_HOST || 'mysql',
+    user: 'root',
+    password: 'root',
+    database: 'test'
+  });
+  connection.query(
+    'SELECT * FROM `user` WHERE `name` = "u1"',
+    function (err: any, results: any, fields: any) {
+      res.end(JSON.stringify({
+        results,
+        fields
+      }))
+    }
+  );
+})
+
+server.listen(5000, () => console.info('Listening on port 5000...'));
diff --git a/tests/plugins/mysql/test.ts b/tests/plugins/mysql/test.ts
new file mode 100644
index 0000000..e2b14b5
--- /dev/null
+++ b/tests/plugins/mysql/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('mysql', Wait.forHealthCheck())
+      .up();
+  });
+
+  afterAll(async () => {
+    await compose.down();
+  });
+
+  it(__filename, async () => {
+    await waitForExpect(async () => expect((await axios.get('http://localhost:5001/mysql')).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;
+    }
+  });
+});
diff --git a/tests/plugins/pg/client.ts b/tests/plugins/pg/client.ts
new file mode 100644
index 0000000..25ff2b3
--- /dev/null
+++ b/tests/plugins/pg/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/pg/docker-compose.yml b/tests/plugins/pg/docker-compose.yml
new file mode 100644
index 0000000..be76bbd
--- /dev/null
+++ b/tests/plugins/pg/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
+
+  postgres:
+    container_name: postgres
+    environment:
+      POSTGRES_USER: "root"
+      POSTGRES_PASSWORD: "root"
+      POSTGRES_DB: "test"
+    ports:
+      - 5432:5432
+    volumes:
+      - ./init:/docker-entrypoint-initdb.d
+    healthcheck:
+      test: ["CMD", "bash", "-c", "cat < /dev/null > /dev/tcp/127.0.0.1/5432"]
+      interval: 5s
+      timeout: 60s
+      retries: 120
+    image: "docker.io/postgres:13.2"
+    networks:
+      - traveling-light
+
+  server:
+    extends:
+      file: ../common/base-compose.yml
+      service: agent
+    ports:
+      - 5000:5000
+    environment:
+      POSTGRES_HOST: postgres
+    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
+      postgres:
+        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/pg/expected.data.yaml b/tests/plugins/pg/expected.data.yaml
new file mode 100644
index 0000000..f89d1c8
--- /dev/null
+++ b/tests/plugins/pg/expected.data.yaml
@@ -0,0 +1,98 @@
+#
+# 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: /postgres
+            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/postgres' }
+              - { key: http.method, value: GET }
+              - { key: http.status.code, value: '200' }
+            refs:
+              - parentEndpoint: ""
+                networkAddress: server:5000
+                refType: CrossProcess
+                parentSpanId: 1
+                parentTraceSegmentId: not null
+                parentServiceInstance: not null
+                parentService: client
+                traceId: not null
+          - operationName: pg/query
+            operationId: 0
+            parentSpanId: 0
+            spanId: 1
+            spanLayer: Database
+            startTime: gt 0
+            endTime: gt 0
+            componentId: 22
+            spanType: Exit
+            peer: postgres:5432
+            skipAnalysis: false
+            tags:
+              - { key: db.type, value: PostgreSQL }
+              - { key: db.instance, value: test }
+              - { key: db.statement, value: SELECT * FROM "user" where name = 'u1' }
+  - serviceName: client
+    segmentSize: 1
+    segments:
+      - segmentId: not null
+        spans:
+          - operationName: /postgres
+            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/postgres' }
+              - { key: http.method, value: GET }
+              - { key: http.status.code, value: '200' }
+          - operationName: /postgres
+            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/postgres' }
+              - { key: http.method, value: GET }
+              - { key: http.status.code, value: '200' }
+              - { key: http.status.msg, value: OK }
diff --git a/tests/plugins/pg/init/init.sql b/tests/plugins/pg/init/init.sql
new file mode 100644
index 0000000..84ed399
--- /dev/null
+++ b/tests/plugins/pg/init/init.sql
@@ -0,0 +1,22 @@
+-- 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.
+
+CREATE SEQUENCE user_seq;
+
+CREATE TABLE IF NOT EXISTS "user"(
+   id INT CHECK (id > 0) DEFAULT NEXTVAL ('user_seq'),
+   name VARCHAR(100) NOT NULL,
+   PRIMARY KEY( id )
+);
diff --git a/tests/plugins/pg/server.ts b/tests/plugins/pg/server.ts
new file mode 100644
index 0000000..5538ffc
--- /dev/null
+++ b/tests/plugins/pg/server.ts
@@ -0,0 +1,50 @@
+/*!
+ *
+ * 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 {Client} from 'pg';
+import agent from '../../../src';
+
+process.env.SW_AGENT_LOGGING_LEVEL = 'ERROR';
+
+agent.start({
+  serviceName: 'server',
+  maxBufferSize: 1000,
+})
+
+const server = http.createServer((req, res) => {
+  const client = new Client({
+    host: process.env.POSTGRES_HOST || 'postgres',
+    user: 'root',
+    password: 'root',
+    database: 'test'
+  });
+  client.connect();
+  client.query(`SELECT * FROM "user" where name = 'u1'`).then(
+    (resDB: any) => {
+      res.end(JSON.stringify(resDB.rows));
+      client.end();
+    },
+    (err: any) => {
+      client.end();
+    },
+  );
+})
+
+server.listen(5000, () => console.info('Listening on port 5000...'));
diff --git a/tests/plugins/pg/test.ts b/tests/plugins/pg/test.ts
new file mode 100644
index 0000000..f541e8c
--- /dev/null
+++ b/tests/plugins/pg/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('postgres', Wait.forHealthCheck())
+      .up();
+  });
+
+  afterAll(async () => {
+    await compose.down();
+  });
+
+  it(__filename, async () => {
+    await waitForExpect(async () => expect((await axios.get('http://localhost:5001/postgres')).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;
+    }
+  });
+});