-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
0af4023
commit 6edcfd8
Showing
13 changed files
with
365 additions
and
8 deletions.
There are no files selected for viewing
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import assert from "node:assert"; | ||
|
||
import type mysql from "mysql"; | ||
|
||
import { recording } from "../recorder"; | ||
import { getTime } from "../util/getTime"; | ||
|
||
export default function mysqlHook(mod: typeof mysql) { | ||
// mysql.Connection type is an interface, create a dummy connection | ||
// object to access its prototype. | ||
const connection = mod.createConnection({}); | ||
const prototype: unknown = Object.getPrototypeOf(connection); | ||
assert(prototype != undefined && typeof prototype === "object" && "query" in prototype); | ||
prototype.query = createQueryProxy(prototype.query as mysql.QueryFunction); | ||
|
||
return mod; | ||
} | ||
|
||
mysqlHook.applicable = function (id: string) { | ||
return id === "mysql"; | ||
}; | ||
|
||
function hasStringSqlProperty(o: unknown): o is { sql: string } { | ||
return o !== null && typeof o === "object" && "sql" in o && typeof o.sql === "string"; | ||
} | ||
|
||
function createQueryProxy(query: mysql.QueryFunction) { | ||
return new Proxy(query, { | ||
apply(target, thisArg, argArray: Parameters<typeof query>) { | ||
// We have to cover these Connection.query call variants: | ||
// - query(sql, callback?) | ||
// - query(sql, values, callback?) | ||
// - query(options, callback?) | ||
// - query(options, values, callback?), | ||
// where | ||
// - sql: string | ||
// - options: {sql: string, ...} | ||
// - callback: (error, results, fields) => void | ||
|
||
// Pool.query and PoolNamespace.query methods use Connection.query | ||
// https://github.com/mysqljs/mysql/blob/dc9c152a87ec51a1f647447268917243d2eab1fd/lib/Pool.js#L214 | ||
// https://github.com/mysqljs/mysql/blob/dc9c152a87ec51a1f647447268917243d2eab1fd/lib/PoolNamespace.js#L111 | ||
|
||
const sql: string = hasStringSqlProperty(argArray[0]) ? argArray[0].sql : argArray[0]; | ||
|
||
const call = recording.sqlQuery("mysql", sql); | ||
const start = getTime(); | ||
|
||
const originalCallback = | ||
typeof argArray[argArray.length - 1] === "function" | ||
? (argArray.pop() as mysql.queryCallback) | ||
: undefined; | ||
|
||
const newCallback: mysql.queryCallback = (err, results, fields) => { | ||
if (err) recording.functionException(call.id, err, getTime() - start); | ||
else recording.functionReturn(call.id, undefined, getTime() - start); | ||
|
||
originalCallback?.call(this, err, results, fields); | ||
}; | ||
|
||
argArray.push(newCallback); | ||
|
||
return Reflect.apply(target, thisArg, argArray); | ||
}, | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`mapping MySQL tests 1`] = ` | ||
{ | ||
"classMap": [ | ||
{ | ||
"children": [ | ||
{ | ||
"children": [ | ||
{ | ||
"location": "./index.js:17", | ||
"name": "main", | ||
"static": true, | ||
"type": "function", | ||
}, | ||
], | ||
"name": "index", | ||
"type": "class", | ||
}, | ||
], | ||
"name": "index", | ||
"type": "package", | ||
}, | ||
], | ||
"eventUpdates": { | ||
"3": { | ||
"elapsed": 31.337, | ||
"event": "return", | ||
"id": 3, | ||
"parent_id": 1, | ||
"return_value": { | ||
"class": "Promise", | ||
"object_id": 1, | ||
"value": "Promise { undefined }", | ||
}, | ||
"thread_id": 0, | ||
}, | ||
}, | ||
"events": [ | ||
{ | ||
"defined_class": "", | ||
"event": "call", | ||
"id": 1, | ||
"lineno": 17, | ||
"method_id": "main", | ||
"parameters": [], | ||
"path": "./index.js", | ||
"static": true, | ||
"thread_id": 0, | ||
}, | ||
{ | ||
"event": "call", | ||
"id": 2, | ||
"sql_query": { | ||
"database_type": "mysql", | ||
"sql": "SELECT 'Connection.query'", | ||
}, | ||
"thread_id": 0, | ||
}, | ||
{ | ||
"elapsed": 31.337, | ||
"event": "return", | ||
"id": 3, | ||
"parent_id": 1, | ||
"return_value": { | ||
"class": "Promise", | ||
"object_id": 1, | ||
"value": "Promise { <pending> }", | ||
}, | ||
"thread_id": 0, | ||
}, | ||
{ | ||
"elapsed": 31.337, | ||
"event": "return", | ||
"exceptions": [ | ||
{ | ||
"class": "Error", | ||
"message": "connect ECONNREFUSED 127.0.0.1:3306", | ||
"object_id": 2, | ||
}, | ||
], | ||
"id": 4, | ||
"parent_id": 2, | ||
"thread_id": 0, | ||
}, | ||
{ | ||
"event": "call", | ||
"id": 5, | ||
"sql_query": { | ||
"database_type": "mysql", | ||
"sql": "SELECT 'Connection.query with values', ?, ?", | ||
}, | ||
"thread_id": 0, | ||
}, | ||
{ | ||
"elapsed": 31.337, | ||
"event": "return", | ||
"exceptions": [ | ||
{ | ||
"class": "Error", | ||
"message": "connect ECONNREFUSED 127.0.0.1:3306", | ||
"object_id": 3, | ||
}, | ||
], | ||
"id": 6, | ||
"parent_id": 5, | ||
"thread_id": 0, | ||
}, | ||
{ | ||
"event": "call", | ||
"id": 7, | ||
"sql_query": { | ||
"database_type": "mysql", | ||
"sql": "SELECT 'Connection.query with options'", | ||
}, | ||
"thread_id": 0, | ||
}, | ||
{ | ||
"elapsed": 31.337, | ||
"event": "return", | ||
"exceptions": [ | ||
{ | ||
"class": "Error", | ||
"message": "connect ECONNREFUSED 127.0.0.1:3306", | ||
"object_id": 4, | ||
}, | ||
], | ||
"id": 8, | ||
"parent_id": 7, | ||
"thread_id": 0, | ||
}, | ||
{ | ||
"event": "call", | ||
"id": 9, | ||
"sql_query": { | ||
"database_type": "mysql", | ||
"sql": "SELECT 'Connection.query without a callback", | ||
}, | ||
"thread_id": 0, | ||
}, | ||
{ | ||
"elapsed": 31.337, | ||
"event": "return", | ||
"exceptions": [ | ||
{ | ||
"class": "Error", | ||
"message": "connect ECONNREFUSED 127.0.0.1:3306", | ||
"object_id": 5, | ||
}, | ||
], | ||
"id": 10, | ||
"parent_id": 9, | ||
"thread_id": 0, | ||
}, | ||
], | ||
"metadata": { | ||
"app": "mysql-appmap-node-test", | ||
"client": { | ||
"name": "appmap-node", | ||
"url": "https://github.com/getappmap/appmap-node", | ||
"version": "test node-appmap version", | ||
}, | ||
"language": { | ||
"engine": "Node.js", | ||
"name": "javascript", | ||
"version": "test node version", | ||
}, | ||
"name": "test process recording", | ||
"recorder": { | ||
"name": "process", | ||
"type": "process", | ||
}, | ||
}, | ||
"version": "1.12", | ||
} | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { integrationTest, readAppmap, runAppmapNode } from "./helpers"; | ||
|
||
integrationTest("mapping MySQL tests", () => { | ||
expect(runAppmapNode("index.js").status).toBe(0); | ||
expect(readAppmap()).toMatchSnapshot(); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
/* eslint-disable @typescript-eslint/no-var-requires */ | ||
const mysql = require("mysql"); | ||
|
||
const promisify = async (method, thisArg, ...args) => { | ||
return new Promise((resolve) => { | ||
method.call(thisArg, ...args, (error, result) => { | ||
// Since we don't have a MySQL server, we will | ||
// receive the error in every call. We intentionally | ||
// don't reject here. | ||
resolve(result); | ||
}); | ||
}); | ||
}; | ||
|
||
const newConnection = () => mysql.createConnection({ host: "127.0.0.1" }); | ||
|
||
async function main() { | ||
for (const args of [ | ||
["SELECT 'Connection.query'"], | ||
["SELECT 'Connection.query with values', ?, ?", [1, "ABC"]], | ||
[{ sql: "SELECT 'Connection.query with options'" }], | ||
]) { | ||
// Create new connection for each query because we don't have | ||
// a MySQL server. We get a fatal error after calling the query | ||
// method second time in the same connection and we don't want to | ||
// have different exception events for first query (connect ECONNREFUSED 127.0.0.1:3306) | ||
// and the remaining queries (Cannot enqueue Query after fatal error) | ||
// in the appmap. | ||
const conn = newConnection(); | ||
await promisify(conn.query, conn, ...args); | ||
} | ||
|
||
// We cannot test commands being called without a completion callback with promisify | ||
// because promisify already provides the completion callback to resolve the promise. | ||
// We test this case with no completion callback here without promisifying it. | ||
newConnection().query("SELECT 'Connection.query without a callback"); | ||
} | ||
|
||
main(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"name": "mysql-appmap-node-test", | ||
"packageManager": "yarn@3.6.3", | ||
"private": true, | ||
"dependencies": { | ||
"mysql": "^2.18.1" | ||
} | ||
} |
Oops, something went wrong.