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