From c887fd600f15bd424bd74db81f810e87bba95693 Mon Sep 17 00:00:00 2001 From: tyk Date: Wed, 19 May 2021 11:19:36 +0800 Subject: [PATCH 1/3] add mysql2 plugin --- src/plugins/MySQL2Plugin.ts | 130 ++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 src/plugins/MySQL2Plugin.ts diff --git a/src/plugins/MySQL2Plugin.ts b/src/plugins/MySQL2Plugin.ts new file mode 100644 index 0000000..320ba73 --- /dev/null +++ b/src/plugins/MySQL2Plugin.ts @@ -0,0 +1,130 @@ +/*! + * + * 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, { wrapEmit, wrapCallback } 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 PluginInstaller from '../core/PluginInstaller'; +import agentConfig from '../config/AgentConfig'; + +class MySQL2Plugin implements SwPlugin { + readonly module = 'mysql2'; + readonly versions = '*'; + + install(installer: PluginInstaller): void { + const Connection = installer.require('mysql2/lib/connection'); + const _query = Connection.prototype.query; + + Connection.prototype.query = function (sql: any, values: any, cb: any) { + let query: any; + + const host = `${this.config.host}:${this.config.port}`; + const span = ContextManager.current.newExitSpan('mysql/query', host, Component.MYSQL); + + span.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; + + if (typeof sql === 'function') { + sql = wrapCallback(span, sql, 0); + + } else if (typeof sql === 'object') { + _sql = sql.sql; + + if (typeof values === 'function') { + values = wrapCallback(span, values, 0); + _values = sql.values; + + } else if (values !== undefined) { + _values = values; + + if (typeof cb === 'function') { + cb = wrapCallback(span, cb, 0); + } else { + streaming = true; + } + + } else { + streaming = true; + } + + } else { + _sql = sql; + + if (typeof values === 'function') { + values = wrapCallback(span, values, 0); + + } else if (values !== undefined) { + _values = values; + + if (typeof cb === 'function') { + cb = wrapCallback(span, cb, 0); + } else { + streaming = true; + } + + } else { + streaming = true; + } + } + + span.tag(Tag.dbStatement(`${_sql}`)); + + if (agentConfig.sqlTraceParameters && _values) { + let vals = _values.map((v: any) => v === undefined ? 'undefined' : JSON.stringify(v)).join(', '); + + if (vals.length > agentConfig.sqlParametersMaxLength) + vals = vals.slice(0, agentConfig.sqlParametersMaxLength) + ' ...'; + + span.tag(Tag.dbSqlParameters(`[${vals}]`)); + } + + query = _query.call(this, sql, values, cb); + + if (streaming) + wrapEmit(span, query, true, 'end'); + + } catch (e) { + span.error(e); + span.stop(); + + throw e; + } + + span.async(); + + return query; + }; + } +} + +// noinspection JSUnusedGlobalSymbols +export default new MySQL2Plugin(); From 387043fec0dca1aef22c48ce64fe05993c49020f Mon Sep 17 00:00:00 2001 From: tyk Date: Wed, 19 May 2021 11:25:29 +0800 Subject: [PATCH 2/3] add mysql2 test --- package-lock.json | 93 +++++++++++++++++++ package.json | 3 +- tests/plugins/mysql2/client.ts | 38 ++++++++ tests/plugins/mysql2/docker-compose.yml | 89 ++++++++++++++++++ tests/plugins/mysql2/expected.data.yaml | 115 ++++++++++++++++++++++++ tests/plugins/mysql2/init/init.sql | 22 +++++ tests/plugins/mysql2/server.ts | 47 ++++++++++ tests/plugins/mysql2/test.ts | 57 ++++++++++++ 8 files changed, 463 insertions(+), 1 deletion(-) create mode 100644 tests/plugins/mysql2/client.ts create mode 100644 tests/plugins/mysql2/docker-compose.yml create mode 100644 tests/plugins/mysql2/expected.data.yaml create mode 100644 tests/plugins/mysql2/init/init.sql create mode 100644 tests/plugins/mysql2/server.ts create mode 100644 tests/plugins/mysql2/test.ts diff --git a/package-lock.json b/package-lock.json index 62a9e49..844dadd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3224,6 +3224,15 @@ "wide-align": "^1.1.0" } }, + "generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dev": true, + "requires": { + "is-property": "^1.0.2" + } + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -4032,6 +4041,12 @@ "integrity": "sha1-DFLlS8yjkbssSUsh6GJtczbG45c=", "dev": true }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true + }, "is-self-closing": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-self-closing/-/is-self-closing-1.0.1.tgz", @@ -5675,6 +5690,72 @@ "sqlstring": "2.3.1" } }, + "mysql2": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-2.2.5.tgz", + "integrity": "sha512-XRqPNxcZTpmFdXbJqb+/CtYVLCx14x1RTeNMD4954L331APu75IC74GDqnZMEt1kwaXy6TySo55rF2F3YJS78g==", + "dev": true, + "requires": { + "denque": "^1.4.1", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.2", + "long": "^4.0.0", + "lru-cache": "^6.0.0", + "named-placeholders": "^1.1.2", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz", + "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "dev": true + }, + "sqlstring": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.2.tgz", + "integrity": "sha512-vF4ZbYdKS8OnoJAWBmMxCQDkiEBkGQYU7UZPtL8flbDRSNkhaXvRJ279ZtI6M+zDaQovVU4tuRgzK5fVhvFAhg==", + "dev": true + } + } + }, + "named-placeholders": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.2.tgz", + "integrity": "sha512-wiFWqxoLL3PGVReSZpjLVxyJ1bRqe+KKJVbr4hGs1KWfTZTQyezHFBbuKj9hsizHyGV2ne7EMjHdxEGAybD5SA==", + "dev": true, + "requires": { + "lru-cache": "^4.1.3" + }, + "dependencies": { + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + } + } + }, "nan": { "version": "2.14.2", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", @@ -6384,6 +6465,12 @@ "ipaddr.js": "1.9.1" } }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, "psl": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", @@ -6901,6 +6988,12 @@ } } }, + "seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha1-1WgS4cAXpuTnw+Ojeh2m143TyT4=", + "dev": true + }, "serve-static": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", diff --git a/package.json b/package.json index 7fccce8..a05e96f 100644 --- a/package.json +++ b/package.json @@ -47,12 +47,13 @@ "amqplib": "^0.7.0", "axios": "^0.21.0", "express": "^4.17.1", - "grpc_tools_node_protoc_ts": "^4.0.0", "grpc-tools": "^1.10.0", + "grpc_tools_node_protoc_ts": "^4.0.0", "jest": "^26.6.3", "mongodb": "^3.6.4", "mongoose": "^5.12.2", "mysql": "^2.18.1", + "mysql2": "^2.2.5", "pg": "^8.5.1", "prettier": "^2.0.5", "testcontainers": "^6.2.0", diff --git a/tests/plugins/mysql2/client.ts b/tests/plugins/mysql2/client.ts new file mode 100644 index 0000000..2f33af1 --- /dev/null +++ b/tests/plugins/mysql2/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/mysql2/docker-compose.yml b/tests/plugins/mysql2/docker-compose.yml new file mode 100644 index 0000000..c53a9e6 --- /dev/null +++ b/tests/plugins/mysql2/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/mysql2 + 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/mysql2/expected.data.yaml b/tests/plugins/mysql2/expected.data.yaml new file mode 100644 index 0000000..221f4e9 --- /dev/null +++ b/tests/plugins/mysql2/expected.data.yaml @@ -0,0 +1,115 @@ +# +# 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/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" + - 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" + - key: http.status.msg + value: OK + refs: + - parentEndpoint: "" + networkAddress: server:5000 + refType: CrossProcess + parentSpanId: 1 + parentTraceSegmentId: not null + parentServiceInstance: not null + parentService: client + traceId: not null + - 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" + - key: http.status.msg + value: OK + - 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: http://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/mysql2/init/init.sql b/tests/plugins/mysql2/init/init.sql new file mode 100644 index 0000000..844112b --- /dev/null +++ b/tests/plugins/mysql2/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/mysql2/server.ts b/tests/plugins/mysql2/server.ts new file mode 100644 index 0000000..2381ca8 --- /dev/null +++ b/tests/plugins/mysql2/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 'mysql2'; +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/mysql2/test.ts b/tests/plugins/mysql2/test.ts new file mode 100644 index 0000000..e2b14b5 --- /dev/null +++ b/tests/plugins/mysql2/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; + } + }); +}); From 2cb2eeb49e264cc6302d605446d61afa0f4f8f2b Mon Sep 17 00:00:00 2001 From: tyk Date: Wed, 19 May 2021 14:23:31 +0800 Subject: [PATCH 3/3] fix newExitSpan --- src/plugins/MySQL2Plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/MySQL2Plugin.ts b/src/plugins/MySQL2Plugin.ts index 320ba73..4488c96 100644 --- a/src/plugins/MySQL2Plugin.ts +++ b/src/plugins/MySQL2Plugin.ts @@ -37,7 +37,7 @@ class MySQL2Plugin implements SwPlugin { let query: any; const host = `${this.config.host}:${this.config.port}`; - const span = ContextManager.current.newExitSpan('mysql/query', host, Component.MYSQL); + const span = ContextManager.current.newExitSpan('mysql/query', Component.MYSQL); span.start();