diff --git a/README.md b/README.md index 94c2d0c..f710d45 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Environment Variable | Description | Default | `SW_AGENT_LOGGING_LEVEL` | The logging level, could be one of `CRITICAL`, `FATAL`, `ERROR`, `WARN`(`WARNING`), `INFO`, `DEBUG` | `INFO` | | `SW_IGNORE_SUFFIX` | The suffices of endpoints that will be ignored (not traced), comma separated | `.jpg,.jpeg,.js,.css,.png,.bmp,.gif,.ico,.mp3,.mp4,.html,.svg` | | `SW_TRACE_IGNORE_PATH` | The paths of endpoints that will be ignored (not traced), comma separated | `` | -| `SW_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 @@ Library | Plugin Name | [`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 @@ export type AgentConfig = { 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 @@ export function finalizeConfig(config: AgentConfig): void { (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 @@ export default { Number.parseInt(process.env.SW_AGENT_MAX_BUFFER_SIZE as string, 10) : 1000, ignoreSuffix: process.env.SW_IGNORE_SUFFIX ?? '.jpg,.jpeg,.js,.css,.png,.bmp,.gif,.ico,.mp3,.mp4,.html,.svg', traceIgnorePath: process.env.SW_TRACE_IGNORE_PATH || '', - 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 @@ class MySQLPlugin implements SwPlugin { 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 @@ class MySQLPlugin implements SwPlugin { } }; + 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,38 +114,41 @@ class MySQLPlugin implements SwPlugin { } } - 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); + }); + + query.on('end', () => { + span.resync(); // may have already been done in 'error' but safe to do multiple times + span.stop() + }); } - return query; - } catch (e) { span.error(e); span.stop(); 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 @@ export class Component { 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; + } + }); +});