From 7ac5201e25a362ca91d8699ced91fee31237d36b Mon Sep 17 00:00:00 2001 From: Sohan Kshirsagar Date: Wed, 15 Oct 2025 13:55:14 -0700 Subject: [PATCH 1/4] fixes --- docs/nextjs-initialization.md | 2 +- src/core/tracing/JsonSchemaHelper.ts | 7 +- .../fetch/e2e-tests/cjs-fetch/Dockerfile | 2 + .../fetch/e2e-tests/esm-fetch/Dockerfile | 2 + .../grpc/e2e-tests/cjs-grpc/Dockerfile | 2 + .../grpc/e2e-tests/esm-grpc/Dockerfile | 2 + .../libraries/http/Instrumentation.ts | 6 +- .../http/e2e-tests/cjs-http/Dockerfile | 2 + .../http/e2e-tests/esm-http/Dockerfile | 2 + .../libraries/ioredis/Instrumentation.ts | 11 +- .../ioredis/e2e-tests/cjs-ioredis/Dockerfile | 2 + .../e2e-tests/cjs-ioredis/docker-compose.yml | 2 +- .../ioredis/e2e-tests/esm-ioredis/Dockerfile | 2 + .../libraries/mysql2/Instrumentation.ts | 59 ++ .../mysql2/e2e-tests/cjs-mysql2/Dockerfile | 2 + .../mysql2/e2e-tests/cjs-mysql2/src/index.ts | 526 ++++++++++-------- .../mysql2/e2e-tests/esm-mysql2/Dockerfile | 2 + .../libraries/pg/e2e-tests/cjs-pg/Dockerfile | 2 + .../libraries/pg/e2e-tests/esm-pg/Dockerfile | 2 + .../e2e-tests/cjs-postgres/Dockerfile | 2 + .../e2e-tests/esm-postgres/Dockerfile | 2 + 21 files changed, 391 insertions(+), 250 deletions(-) diff --git a/docs/nextjs-initialization.md b/docs/nextjs-initialization.md index 95094309..ba1adf39 100644 --- a/docs/nextjs-initialization.md +++ b/docs/nextjs-initialization.md @@ -1,4 +1,4 @@ -# Next.js Initialization +# Next.js Initialization (Beta) This guide explains how to set up Tusk Drift in your Next.js application. diff --git a/src/core/tracing/JsonSchemaHelper.ts b/src/core/tracing/JsonSchemaHelper.ts index 76f241d1..718abffa 100644 --- a/src/core/tracing/JsonSchemaHelper.ts +++ b/src/core/tracing/JsonSchemaHelper.ts @@ -169,7 +169,7 @@ export class JsonSchemaHelper { /** * Generate schema from data object using standardized types - * + * * Note: We properties always exists on JsonSchema because proto3 maps cannot be marked optional. * The JSON data is a bit inefficient because of this, but the easiest way to handle this is to keep it for now. */ @@ -188,7 +188,10 @@ export class JsonSchemaHelper { if (Array.isArray(data) && data.length === 0) { return { type: JsonSchemaType.ORDERED_LIST, properties: {} }; } - const items = Array.isArray(data) && data.length > 0 ? JsonSchemaHelper.generateSchema(data[0]) : undefined; + const items = + Array.isArray(data) && data.length > 0 + ? JsonSchemaHelper.generateSchema(data[0]) + : undefined; if (items !== undefined) { return { type: JsonSchemaType.ORDERED_LIST, diff --git a/src/instrumentation/libraries/fetch/e2e-tests/cjs-fetch/Dockerfile b/src/instrumentation/libraries/fetch/e2e-tests/cjs-fetch/Dockerfile index 2a53e57d..55454871 100644 --- a/src/instrumentation/libraries/fetch/e2e-tests/cjs-fetch/Dockerfile +++ b/src/instrumentation/libraries/fetch/e2e-tests/cjs-fetch/Dockerfile @@ -9,6 +9,8 @@ COPY src/instrumentation/libraries/fetch/e2e-tests/cjs-fetch/tsconfig.json ./ # Install dependencies RUN npm install +# Add cache-busting argument to force fresh CLI download +ARG CACHEBUST=1 # Install Tusk Drift CLI RUN curl -fsSL https://raw.githubusercontent.com/Use-Tusk/tusk-drift-cli/main/install.sh | sh diff --git a/src/instrumentation/libraries/fetch/e2e-tests/esm-fetch/Dockerfile b/src/instrumentation/libraries/fetch/e2e-tests/esm-fetch/Dockerfile index 1449ccf4..800bc0c4 100644 --- a/src/instrumentation/libraries/fetch/e2e-tests/esm-fetch/Dockerfile +++ b/src/instrumentation/libraries/fetch/e2e-tests/esm-fetch/Dockerfile @@ -9,6 +9,8 @@ COPY src/instrumentation/libraries/fetch/e2e-tests/esm-fetch/tsconfig.json ./ # Install dependencies RUN npm install +# Add cache-busting argument to force fresh CLI download +ARG CACHEBUST=1 # Install Tusk Drift CLI RUN curl -fsSL https://raw.githubusercontent.com/Use-Tusk/tusk-drift-cli/main/install.sh | sh diff --git a/src/instrumentation/libraries/grpc/e2e-tests/cjs-grpc/Dockerfile b/src/instrumentation/libraries/grpc/e2e-tests/cjs-grpc/Dockerfile index 94fa489d..765a7c47 100644 --- a/src/instrumentation/libraries/grpc/e2e-tests/cjs-grpc/Dockerfile +++ b/src/instrumentation/libraries/grpc/e2e-tests/cjs-grpc/Dockerfile @@ -9,6 +9,8 @@ COPY src/instrumentation/libraries/grpc/e2e-tests/cjs-grpc/tsconfig.json ./ # Install dependencies RUN npm install +# Add cache-busting argument to force fresh CLI download +ARG CACHEBUST=1 # Install Tusk Drift CLI RUN curl -fsSL https://raw.githubusercontent.com/Use-Tusk/tusk-drift-cli/main/install.sh | sh diff --git a/src/instrumentation/libraries/grpc/e2e-tests/esm-grpc/Dockerfile b/src/instrumentation/libraries/grpc/e2e-tests/esm-grpc/Dockerfile index 42cd6d05..cf834f18 100644 --- a/src/instrumentation/libraries/grpc/e2e-tests/esm-grpc/Dockerfile +++ b/src/instrumentation/libraries/grpc/e2e-tests/esm-grpc/Dockerfile @@ -9,6 +9,8 @@ COPY src/instrumentation/libraries/grpc/e2e-tests/esm-grpc/tsconfig.json ./ # Install dependencies RUN npm install +# Add cache-busting argument to force fresh CLI download +ARG CACHEBUST=1 # Install Tusk Drift CLI RUN curl -fsSL https://raw.githubusercontent.com/Use-Tusk/tusk-drift-cli/main/install.sh | sh diff --git a/src/instrumentation/libraries/http/Instrumentation.ts b/src/instrumentation/libraries/http/Instrumentation.ts index c15be4ed..06b2c780 100644 --- a/src/instrumentation/libraries/http/Instrumentation.ts +++ b/src/instrumentation/libraries/http/Instrumentation.ts @@ -91,7 +91,7 @@ export class HttpInstrumentation extends TdInstrumentationBase { } // ESM Support: Detect if this is an ESM module - const isESM = (httpModule as any)[Symbol.toStringTag] === 'Module'; + const isESM = (httpModule as any)[Symbol.toStringTag] === "Module"; if (isESM) { // ESM Case: Also set wrapped methods on the default export @@ -181,6 +181,8 @@ export class HttpInstrumentation extends TdInstrumentationBase { return originalHandler.call(this); } + logger.debug(`[HttpInstrumentation] Setting replay trace id`, replayTraceId); + // Set env vars for current trace const envVars = this.replayHooks.extractEnvVarsFromHeaders(req); if (envVars) { @@ -933,7 +935,7 @@ export class HttpInstrumentation extends TdInstrumentationBase { complete: true, readable: false, // Add error-specific fields - errorName: error.name || 'UNKNOWN', + errorName: error.name || "UNKNOWN", errorMessage: error.message, }; diff --git a/src/instrumentation/libraries/http/e2e-tests/cjs-http/Dockerfile b/src/instrumentation/libraries/http/e2e-tests/cjs-http/Dockerfile index 43f38581..13a9e1e6 100644 --- a/src/instrumentation/libraries/http/e2e-tests/cjs-http/Dockerfile +++ b/src/instrumentation/libraries/http/e2e-tests/cjs-http/Dockerfile @@ -9,6 +9,8 @@ COPY src/instrumentation/libraries/http/e2e-tests/cjs-http/tsconfig.json ./ # Install dependencies RUN npm install +# Add cache-busting argument to force fresh CLI download +ARG CACHEBUST=1 # Install Tusk Drift CLI RUN curl -fsSL https://raw.githubusercontent.com/Use-Tusk/tusk-drift-cli/main/install.sh | sh diff --git a/src/instrumentation/libraries/http/e2e-tests/esm-http/Dockerfile b/src/instrumentation/libraries/http/e2e-tests/esm-http/Dockerfile index 02f21c0a..c8822ae9 100644 --- a/src/instrumentation/libraries/http/e2e-tests/esm-http/Dockerfile +++ b/src/instrumentation/libraries/http/e2e-tests/esm-http/Dockerfile @@ -9,6 +9,8 @@ COPY src/instrumentation/libraries/http/e2e-tests/esm-http/tsconfig.json ./ # Install dependencies RUN npm install +# Add cache-busting argument to force fresh CLI download +ARG CACHEBUST=1 # Install Tusk Drift CLI RUN curl -fsSL https://raw.githubusercontent.com/Use-Tusk/tusk-drift-cli/main/install.sh | sh diff --git a/src/instrumentation/libraries/ioredis/Instrumentation.ts b/src/instrumentation/libraries/ioredis/Instrumentation.ts index f0ed8ed5..96cd37fd 100644 --- a/src/instrumentation/libraries/ioredis/Instrumentation.ts +++ b/src/instrumentation/libraries/ioredis/Instrumentation.ts @@ -17,9 +17,7 @@ import { IORedisOutputValue, BufferMetadata, } from "./types"; -import { - convertValueToJsonable, -} from "./utils"; +import { convertValueToJsonable } from "./utils"; import { PackageType } from "@use-tusk/drift-schemas/core/span"; import { logger } from "../../../core/utils/logger"; @@ -125,7 +123,7 @@ export class IORedisInstrumentation extends TdInstrumentationBase { const actualExports = isESM ? moduleExports.default : moduleExports; if (!actualExports || !actualExports.prototype) { - logger.error(`[IORedisInstrumentation] Invalid Pipeline module exports, cannot patch`); + logger.debug(`[IORedisInstrumentation] Invalid Pipeline module exports, cannot patch`); return moduleExports; } @@ -550,7 +548,10 @@ export class IORedisInstrumentation extends TdInstrumentationBase { return promise; } - private async _handleReplayConnect(spanInfo: SpanInfo, thisContext: IORedisInterface): Promise { + private async _handleReplayConnect( + spanInfo: SpanInfo, + thisContext: IORedisInterface, + ): Promise { logger.debug(`[IORedisInstrumentation] Replaying IORedis connect`); // Connect operations typically don't have meaningful output to replay diff --git a/src/instrumentation/libraries/ioredis/e2e-tests/cjs-ioredis/Dockerfile b/src/instrumentation/libraries/ioredis/e2e-tests/cjs-ioredis/Dockerfile index 2ccb2061..b7d45bac 100644 --- a/src/instrumentation/libraries/ioredis/e2e-tests/cjs-ioredis/Dockerfile +++ b/src/instrumentation/libraries/ioredis/e2e-tests/cjs-ioredis/Dockerfile @@ -9,6 +9,8 @@ COPY src/instrumentation/libraries/ioredis/e2e-tests/cjs-ioredis/tsconfig.json . # Install dependencies RUN npm install +# Add cache-busting argument to force fresh CLI download +ARG CACHEBUST=1 # Install Tusk Drift CLI RUN curl -fsSL https://raw.githubusercontent.com/Use-Tusk/tusk-drift-cli/main/install.sh | sh diff --git a/src/instrumentation/libraries/ioredis/e2e-tests/cjs-ioredis/docker-compose.yml b/src/instrumentation/libraries/ioredis/e2e-tests/cjs-ioredis/docker-compose.yml index 67a59764..4c29c8c4 100644 --- a/src/instrumentation/libraries/ioredis/e2e-tests/cjs-ioredis/docker-compose.yml +++ b/src/instrumentation/libraries/ioredis/e2e-tests/cjs-ioredis/docker-compose.yml @@ -1,4 +1,4 @@ -version: '3.8' +version: "3.8" services: redis: diff --git a/src/instrumentation/libraries/ioredis/e2e-tests/esm-ioredis/Dockerfile b/src/instrumentation/libraries/ioredis/e2e-tests/esm-ioredis/Dockerfile index f329257c..c6b47fc8 100644 --- a/src/instrumentation/libraries/ioredis/e2e-tests/esm-ioredis/Dockerfile +++ b/src/instrumentation/libraries/ioredis/e2e-tests/esm-ioredis/Dockerfile @@ -9,6 +9,8 @@ COPY src/instrumentation/libraries/ioredis/e2e-tests/esm-ioredis/tsconfig.json . # Install dependencies RUN npm install +# Add cache-busting argument to force fresh CLI download +ARG CACHEBUST=1 # Install Tusk Drift CLI RUN curl -fsSL https://raw.githubusercontent.com/Use-Tusk/tusk-drift-cli/main/install.sh | sh diff --git a/src/instrumentation/libraries/mysql2/Instrumentation.ts b/src/instrumentation/libraries/mysql2/Instrumentation.ts index c0876960..e8d40727 100644 --- a/src/instrumentation/libraries/mysql2/Instrumentation.ts +++ b/src/instrumentation/libraries/mysql2/Instrumentation.ts @@ -41,6 +41,11 @@ export class Mysql2Instrumentation extends TdInstrumentationBase { name: "mysql2", supportedVersions: SUPPORTED_VERSIONS, files: [ + new TdInstrumentationNodeModuleFile({ + name: "mysql2/lib/base/connection.js", + supportedVersions: SUPPORTED_VERSIONS, + patch: (moduleExports: any) => this._patchBaseConnection(moduleExports), + }), new TdInstrumentationNodeModuleFile({ name: "mysql2/lib/connection.js", supportedVersions: SUPPORTED_VERSIONS, @@ -71,6 +76,60 @@ export class Mysql2Instrumentation extends TdInstrumentationBase { ]; } + private _patchBaseConnection(BaseConnectionClass: any): any { + logger.debug(`[Mysql2Instrumentation] Patching BaseConnection class`); + + if (this.isModulePatched(BaseConnectionClass)) { + logger.debug(`[Mysql2Instrumentation] BaseConnection class already patched, skipping`); + return BaseConnectionClass; + } + + // Wrap BaseConnection.prototype.query + if (BaseConnectionClass.prototype && BaseConnectionClass.prototype.query) { + if (!isWrapped(BaseConnectionClass.prototype.query)) { + this._wrap(BaseConnectionClass.prototype, "query", this._getQueryPatchFn("connection")); + logger.debug(`[Mysql2Instrumentation] Wrapped BaseConnection.prototype.query`); + } + } + + // Wrap BaseConnection.prototype.execute (prepared statements) + if (BaseConnectionClass.prototype && BaseConnectionClass.prototype.execute) { + if (!isWrapped(BaseConnectionClass.prototype.execute)) { + this._wrap(BaseConnectionClass.prototype, "execute", this._getExecutePatchFn("connection")); + logger.debug(`[Mysql2Instrumentation] Wrapped BaseConnection.prototype.execute`); + } + } + + // Wrap BaseConnection.prototype.connect + if (BaseConnectionClass.prototype && BaseConnectionClass.prototype.connect) { + if (!isWrapped(BaseConnectionClass.prototype.connect)) { + this._wrap(BaseConnectionClass.prototype, "connect", this._getConnectPatchFn("connection")); + logger.debug(`[Mysql2Instrumentation] Wrapped BaseConnection.prototype.connect`); + } + } + + // Wrap BaseConnection.prototype.ping + if (BaseConnectionClass.prototype && BaseConnectionClass.prototype.ping) { + if (!isWrapped(BaseConnectionClass.prototype.ping)) { + this._wrap(BaseConnectionClass.prototype, "ping", this._getPingPatchFn("connection")); + logger.debug(`[Mysql2Instrumentation] Wrapped BaseConnection.prototype.ping`); + } + } + + // Wrap BaseConnection.prototype.end + if (BaseConnectionClass.prototype && BaseConnectionClass.prototype.end) { + if (!isWrapped(BaseConnectionClass.prototype.end)) { + this._wrap(BaseConnectionClass.prototype, "end", this._getEndPatchFn("connection")); + logger.debug(`[Mysql2Instrumentation] Wrapped BaseConnection.prototype.end`); + } + } + + this.markModuleAsPatched(BaseConnectionClass); + logger.debug(`[Mysql2Instrumentation] BaseConnection class patching complete`); + + return BaseConnectionClass; + } + private _patchConnection(ConnectionClass: any): any { logger.debug(`[Mysql2Instrumentation] Patching Connection class`); diff --git a/src/instrumentation/libraries/mysql2/e2e-tests/cjs-mysql2/Dockerfile b/src/instrumentation/libraries/mysql2/e2e-tests/cjs-mysql2/Dockerfile index 7301a163..2fe0c304 100644 --- a/src/instrumentation/libraries/mysql2/e2e-tests/cjs-mysql2/Dockerfile +++ b/src/instrumentation/libraries/mysql2/e2e-tests/cjs-mysql2/Dockerfile @@ -9,6 +9,8 @@ COPY src/instrumentation/libraries/mysql2/e2e-tests/cjs-mysql2/tsconfig.json ./ # Install dependencies RUN npm install +# Add cache-busting argument to force fresh CLI download +ARG CACHEBUST=1 # Install Tusk Drift CLI RUN curl -fsSL https://raw.githubusercontent.com/Use-Tusk/tusk-drift-cli/main/install.sh | sh diff --git a/src/instrumentation/libraries/mysql2/e2e-tests/cjs-mysql2/src/index.ts b/src/instrumentation/libraries/mysql2/e2e-tests/cjs-mysql2/src/index.ts index 768f298e..6760995c 100644 --- a/src/instrumentation/libraries/mysql2/e2e-tests/cjs-mysql2/src/index.ts +++ b/src/instrumentation/libraries/mysql2/e2e-tests/cjs-mysql2/src/index.ts @@ -1,18 +1,18 @@ -import { TuskDrift } from './tdInit'; -import http from 'http'; -import mysql from 'mysql2'; -import { sequelize, User, Product, initializeSequelize } from './sequelizeSetup'; -import { Op, QueryTypes } from 'sequelize'; +import { TuskDrift } from "./tdInit"; +import http from "http"; +import mysql from "mysql2"; +import { sequelize, User, Product, initializeSequelize } from "./sequelizeSetup"; +import { Op, QueryTypes } from "sequelize"; const PORT = process.env.PORT || 3000; // Database configuration const dbConfig = { - host: process.env.MYSQL_HOST || 'mysql', - port: parseInt(process.env.MYSQL_PORT || '3306'), - database: process.env.MYSQL_DB || 'testdb', - user: process.env.MYSQL_USER || 'testuser', - password: process.env.MYSQL_PASSWORD || 'testpass', + host: process.env.MYSQL_HOST || "mysql", + port: parseInt(process.env.MYSQL_PORT || "3306"), + database: process.env.MYSQL_DB || "testdb", + user: process.env.MYSQL_USER || "testuser", + password: process.env.MYSQL_PASSWORD || "testpass", waitForConnections: true, connectionLimit: 10, queueLimit: 0, @@ -52,7 +52,7 @@ async function initializeDatabase() { (err) => { if (err) reject(err); else resolve(); - } + }, ); }); @@ -68,7 +68,7 @@ async function initializeDatabase() { (err) => { if (err) reject(err); else resolve(); - } + }, ); }); @@ -85,7 +85,7 @@ async function initializeDatabase() { (err) => { if (err) reject(err); else resolve(); - } + }, ); }); @@ -98,7 +98,7 @@ async function initializeDatabase() { (err) => { if (err) reject(err); else resolve(); - } + }, ); }); } @@ -108,192 +108,208 @@ async function initializeDatabase() { // Create HTTP server with test endpoints const server = http.createServer(async (req, res) => { - const url = req.url || '/'; - const method = req.method || 'GET'; + const url = req.url || "/"; + const method = req.method || "GET"; console.log(`Received request: ${method} ${url}`); try { // Health check endpoint - if (url === '/health' && method === 'GET') { - res.writeHead(200, { 'Content-Type': 'application/json' }); + if (url === "/health" && method === "GET") { + res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ success: true })); return; } // Test endpoint for connection query - if (url === '/test/connection-query' && method === 'GET') { + if (url === "/test/connection-query" && method === "GET") { connection.query("SELECT * FROM test_users ORDER BY id", (error, results) => { if (error) { - res.writeHead(500, { 'Content-Type': 'application/json' }); + res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ success: false, error: error.message })); return; } - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - success: true, - data: results, - rowCount: Array.isArray(results) ? results.length : 0, - })); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + success: true, + data: results, + rowCount: Array.isArray(results) ? results.length : 0, + }), + ); }); return; } // Test endpoint for connection parameterized query - if (url === '/test/connection-parameterized' && method === 'POST') { - let body = ''; - req.on('data', chunk => body += chunk); - req.on('end', () => { + if (url === "/test/connection-parameterized" && method === "POST") { + let body = ""; + req.on("data", (chunk) => (body += chunk)); + req.on("end", () => { const { userId } = JSON.parse(body); connection.query("SELECT * FROM test_users WHERE id = ?", [userId], (error, results) => { if (error) { - res.writeHead(500, { 'Content-Type': 'application/json' }); + res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ success: false, error: error.message })); return; } - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - success: true, - data: results, - rowCount: Array.isArray(results) ? results.length : 0, - })); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + success: true, + data: results, + rowCount: Array.isArray(results) ? results.length : 0, + }), + ); }); }); return; } // Test endpoint for connection execute (prepared statements) - if (url === '/test/connection-execute' && method === 'GET') { + if (url === "/test/connection-execute" && method === "GET") { connection.execute("SELECT * FROM test_users ORDER BY id", (error, results) => { if (error) { - res.writeHead(500, { 'Content-Type': 'application/json' }); + res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ success: false, error: error.message })); return; } - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - success: true, - data: results, - rowCount: Array.isArray(results) ? results.length : 0, - })); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + success: true, + data: results, + rowCount: Array.isArray(results) ? results.length : 0, + }), + ); }); return; } // Test endpoint for connection execute with params - if (url === '/test/connection-execute-params' && method === 'POST') { - let body = ''; - req.on('data', chunk => body += chunk); - req.on('end', () => { + if (url === "/test/connection-execute-params" && method === "POST") { + let body = ""; + req.on("data", (chunk) => (body += chunk)); + req.on("end", () => { const { userId } = JSON.parse(body); connection.execute("SELECT * FROM test_users WHERE id = ?", [userId], (error, results) => { if (error) { - res.writeHead(500, { 'Content-Type': 'application/json' }); + res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ success: false, error: error.message })); return; } - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - success: true, - data: results, - rowCount: Array.isArray(results) ? results.length : 0, - })); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + success: true, + data: results, + rowCount: Array.isArray(results) ? results.length : 0, + }), + ); }); }); return; } // Pool test endpoint - if (url === '/test/pool-query' && method === 'GET') { + if (url === "/test/pool-query" && method === "GET") { pool.query("SELECT * FROM test_users ORDER BY id LIMIT 5", (error, results) => { if (error) { - res.writeHead(500, { 'Content-Type': 'application/json' }); + res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ success: false, error: error.message })); return; } - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - success: true, - data: results, - rowCount: Array.isArray(results) ? results.length : 0, - queryType: "pool", - })); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + success: true, + data: results, + rowCount: Array.isArray(results) ? results.length : 0, + queryType: "pool", + }), + ); }); return; } // Pool parameterized query - if (url === '/test/pool-parameterized' && method === 'POST') { - let body = ''; - req.on('data', chunk => body += chunk); - req.on('end', () => { + if (url === "/test/pool-parameterized" && method === "POST") { + let body = ""; + req.on("data", (chunk) => (body += chunk)); + req.on("end", () => { const { userId } = JSON.parse(body); pool.query("SELECT * FROM test_users WHERE id = ?", [userId], (error, results) => { if (error) { - res.writeHead(500, { 'Content-Type': 'application/json' }); + res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ success: false, error: error.message })); return; } - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - success: true, - data: results, - rowCount: Array.isArray(results) ? results.length : 0, - queryType: "pool-parameterized", - })); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + success: true, + data: results, + rowCount: Array.isArray(results) ? results.length : 0, + queryType: "pool-parameterized", + }), + ); }); }); return; } // Pool execute test - if (url === '/test/pool-execute' && method === 'GET') { + if (url === "/test/pool-execute" && method === "GET") { pool.execute("SELECT * FROM test_users ORDER BY id", (error, results) => { if (error) { - res.writeHead(500, { 'Content-Type': 'application/json' }); + res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ success: false, error: error.message })); return; } - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - success: true, - data: results, - rowCount: Array.isArray(results) ? results.length : 0, - queryType: "pool-execute", - })); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + success: true, + data: results, + rowCount: Array.isArray(results) ? results.length : 0, + queryType: "pool-execute", + }), + ); }); return; } // Pool execute with params - if (url === '/test/pool-execute-params' && method === 'POST') { - let body = ''; - req.on('data', chunk => body += chunk); - req.on('end', () => { + if (url === "/test/pool-execute-params" && method === "POST") { + let body = ""; + req.on("data", (chunk) => (body += chunk)); + req.on("end", () => { const { userId } = JSON.parse(body); pool.execute("SELECT * FROM test_users WHERE id = ?", [userId], (error, results) => { if (error) { - res.writeHead(500, { 'Content-Type': 'application/json' }); + res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ success: false, error: error.message })); return; } - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - success: true, - data: results, - rowCount: Array.isArray(results) ? results.length : 0, - queryType: "pool-execute-params", - })); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + success: true, + data: results, + rowCount: Array.isArray(results) ? results.length : 0, + queryType: "pool-execute-params", + }), + ); }); }); return; } // Pool getConnection test - if (url === '/test/pool-getConnection' && method === 'GET') { + if (url === "/test/pool-getConnection" && method === "GET") { pool.getConnection((error, poolConnection) => { if (error) { - res.writeHead(500, { 'Content-Type': 'application/json' }); + res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ success: false, error: error.message })); return; } @@ -302,218 +318,242 @@ const server = http.createServer(async (req, res) => { poolConnection.release(); if (queryError) { - res.writeHead(500, { 'Content-Type': 'application/json' }); + res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ success: false, error: queryError.message })); return; } - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - success: true, - data: results, - queryType: "pool-getConnection", - })); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + success: true, + data: results, + queryType: "pool-getConnection", + }), + ); }); }); return; } // Connection connect test - if (url === '/test/connection-connect' && method === 'GET') { + if (url === "/test/connection-connect" && method === "GET") { const newConnection = mysql.createConnection(dbConfig); newConnection.connect((error) => { if (error) { - res.writeHead(500, { 'Content-Type': 'application/json' }); + res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ success: false, error: error.message })); return; } newConnection.end(); - res.writeHead(200, { 'Content-Type': 'application/json' }); + res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ success: true })); }); return; } // Connection ping test - if (url === '/test/connection-ping' && method === 'GET') { + if (url === "/test/connection-ping" && method === "GET") { connection.ping((error) => { if (error) { - res.writeHead(500, { 'Content-Type': 'application/json' }); + res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ success: false, error: error.message })); return; } - res.writeHead(200, { 'Content-Type': 'application/json' }); + res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ success: true })); }); return; } // Stream query test - if (url === '/test/stream-query' && method === 'GET') { + if (url === "/test/stream-query" && method === "GET") { const results: any[] = []; const query = connection.query("SELECT * FROM large_data ORDER BY id"); query - .on('error', (error) => { - res.writeHead(500, { 'Content-Type': 'application/json' }); + .on("error", (error) => { + res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ success: false, error: error.message })); }) - .on('result', (row) => { + .on("result", (row) => { results.push(row); }) - .on('end', () => { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - success: true, - data: results, - rowCount: results.length, - queryType: "stream", - })); + .on("end", () => { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + success: true, + data: results, + rowCount: results.length, + queryType: "stream", + }), + ); }); return; } // Sequelize authenticate test - this triggers internal queries like SELECT VERSION() - if (url === '/test/sequelize-authenticate' && method === 'GET') { + if (url === "/test/sequelize-authenticate" && method === "GET") { try { await sequelize.authenticate(); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - success: true, - message: 'Sequelize authentication successful', - queryType: 'sequelize-authenticate', - })); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + success: true, + message: "Sequelize authentication successful", + queryType: "sequelize-authenticate", + }), + ); } catch (error) { - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - success: false, - error: error instanceof Error ? error.message : String(error), - })); + res.writeHead(500, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + success: false, + error: error instanceof Error ? error.message : String(error), + }), + ); } return; } // Sequelize findAll test - ORM query - if (url === '/test/sequelize-findall' && method === 'GET') { + if (url === "/test/sequelize-findall" && method === "GET") { try { const users = await User.findAll({ - order: [['id', 'ASC']], + order: [["id", "ASC"]], }); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - success: true, - data: users, - rowCount: users.length, - queryType: 'sequelize-findAll', - })); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + success: true, + data: users, + rowCount: users.length, + queryType: "sequelize-findAll", + }), + ); } catch (error) { - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - success: false, - error: error instanceof Error ? error.message : String(error), - })); + res.writeHead(500, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + success: false, + error: error instanceof Error ? error.message : String(error), + }), + ); } return; } // Sequelize findOne test - parameterized ORM query - if (url === '/test/sequelize-findone' && method === 'POST') { + if (url === "/test/sequelize-findone" && method === "POST") { try { - let body = ''; - req.on('data', chunk => body += chunk); - await new Promise((resolve) => req.on('end', () => resolve())); + let body = ""; + req.on("data", (chunk) => (body += chunk)); + await new Promise((resolve) => req.on("end", () => resolve())); const { userId } = JSON.parse(body); const user = await User.findOne({ where: { id: userId }, }); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - success: true, - data: user, - queryType: 'sequelize-findOne', - })); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + success: true, + data: user, + queryType: "sequelize-findOne", + }), + ); } catch (error) { - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - success: false, - error: error instanceof Error ? error.message : String(error), - })); + res.writeHead(500, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + success: false, + error: error instanceof Error ? error.message : String(error), + }), + ); } return; } // Sequelize complex query with joins and aggregations - if (url === '/test/sequelize-complex' && method === 'GET') { + if (url === "/test/sequelize-complex" && method === "GET") { try { // This will trigger multiple internal queries const [users, products] = await Promise.all([ User.findAll({ - attributes: ['id', 'name', 'email'], + attributes: ["id", "name", "email"], limit: 5, }), Product.findAll({ - attributes: ['id', 'name', 'price', 'stock'], + attributes: ["id", "name", "price", "stock"], where: { stock: { [Op.gt]: 0, }, }, - order: [['price', 'DESC']], + order: [["price", "DESC"]], }), ]); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - success: true, - data: { - users, - products, - }, - queryType: 'sequelize-complex', - })); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + success: true, + data: { + users, + products, + }, + queryType: "sequelize-complex", + }), + ); } catch (error) { - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - success: false, - error: error instanceof Error ? error.message : String(error), - })); + res.writeHead(500, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + success: false, + error: error instanceof Error ? error.message : String(error), + }), + ); } return; } // Sequelize raw query test - if (url === '/test/sequelize-raw' && method === 'GET') { + if (url === "/test/sequelize-raw" && method === "GET") { try { const results = await sequelize.query( - 'SELECT * FROM test_users WHERE id <= ? ORDER BY id', + "SELECT * FROM test_users WHERE id <= ? ORDER BY id", { replacements: [3], type: QueryTypes.SELECT, - } + }, ); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - success: true, - data: results, - rowCount: Array.isArray(results) ? results.length : 0, - queryType: 'sequelize-raw', - })); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + success: true, + data: results, + rowCount: Array.isArray(results) ? results.length : 0, + queryType: "sequelize-raw", + }), + ); } catch (error) { - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - success: false, - error: error instanceof Error ? error.message : String(error), - })); + res.writeHead(500, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + success: false, + error: error instanceof Error ? error.message : String(error), + }), + ); } return; } // Sequelize transaction test - this will create multiple queries - if (url === '/test/sequelize-transaction' && method === 'POST') { + if (url === "/test/sequelize-transaction" && method === "POST") { try { const result = await sequelize.transaction(async (t) => { // Multiple queries within a transaction @@ -530,32 +570,38 @@ const server = http.createServer(async (req, res) => { return { user, products }; }); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - success: true, - data: result, - queryType: 'sequelize-transaction', - })); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + success: true, + data: result, + queryType: "sequelize-transaction", + }), + ); } catch (error) { - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - success: false, - error: error instanceof Error ? error.message : String(error), - })); + res.writeHead(500, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + success: false, + error: error instanceof Error ? error.message : String(error), + }), + ); } return; } // 404 for unknown routes - res.writeHead(404, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: 'Not found' })); + res.writeHead(404, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Not found" })); } catch (error) { - console.error('Error handling request:', error); - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - success: false, - error: error instanceof Error ? error.message : String(error), - })); + console.error("Error handling request:", error); + res.writeHead(500, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + success: false, + error: error instanceof Error ? error.message : String(error), + }), + ); } }); @@ -567,26 +613,28 @@ server.listen(PORT, async () => { TuskDrift.markAppAsReady(); console.log(`MySQL2 integration test server running on port ${PORT}`); console.log(`Test mode: ${process.env.TUSK_DRIFT_MODE}`); - console.log('Available endpoints:'); - console.log(' GET /health - Health check'); - console.log(' GET /test/connection-query - Test connection query'); - console.log(' POST /test/connection-parameterized - Test connection parameterized query'); - console.log(' GET /test/connection-execute - Test connection execute (prepared statement)'); - console.log(' POST /test/connection-execute-params - Test connection execute with params'); - console.log(' GET /test/pool-query - Test pool query'); - console.log(' POST /test/pool-parameterized - Test pool parameterized query'); - console.log(' GET /test/pool-execute - Test pool execute (prepared statement)'); - console.log(' POST /test/pool-execute-params - Test pool execute with params'); - console.log(' GET /test/pool-getConnection - Test pool getConnection'); - console.log(' GET /test/connection-connect - Test connection connect'); - console.log(' GET /test/connection-ping - Test connection ping'); - console.log(' GET /test/stream-query - Test stream query'); - console.log(' GET /test/sequelize-authenticate - Test Sequelize authenticate (triggers internal queries)'); - console.log(' GET /test/sequelize-findall - Test Sequelize findAll'); - console.log(' POST /test/sequelize-findone - Test Sequelize findOne'); - console.log(' GET /test/sequelize-complex - Test Sequelize complex queries'); - console.log(' GET /test/sequelize-raw - Test Sequelize raw query'); - console.log(' POST /test/sequelize-transaction - Test Sequelize transaction'); + console.log("Available endpoints:"); + console.log(" GET /health - Health check"); + console.log(" GET /test/connection-query - Test connection query"); + console.log(" POST /test/connection-parameterized - Test connection parameterized query"); + console.log(" GET /test/connection-execute - Test connection execute (prepared statement)"); + console.log(" POST /test/connection-execute-params - Test connection execute with params"); + console.log(" GET /test/pool-query - Test pool query"); + console.log(" POST /test/pool-parameterized - Test pool parameterized query"); + console.log(" GET /test/pool-execute - Test pool execute (prepared statement)"); + console.log(" POST /test/pool-execute-params - Test pool execute with params"); + console.log(" GET /test/pool-getConnection - Test pool getConnection"); + console.log(" GET /test/connection-connect - Test connection connect"); + console.log(" GET /test/connection-ping - Test connection ping"); + console.log(" GET /test/stream-query - Test stream query"); + console.log( + " GET /test/sequelize-authenticate - Test Sequelize authenticate (triggers internal queries)", + ); + console.log(" GET /test/sequelize-findall - Test Sequelize findAll"); + console.log(" POST /test/sequelize-findone - Test Sequelize findOne"); + console.log(" GET /test/sequelize-complex - Test Sequelize complex queries"); + console.log(" GET /test/sequelize-raw - Test Sequelize raw query"); + console.log(" POST /test/sequelize-transaction - Test Sequelize transaction"); } catch (error) { console.error("Failed to start server:", error); process.exit(1); diff --git a/src/instrumentation/libraries/mysql2/e2e-tests/esm-mysql2/Dockerfile b/src/instrumentation/libraries/mysql2/e2e-tests/esm-mysql2/Dockerfile index ff5f0b98..9bba7b34 100644 --- a/src/instrumentation/libraries/mysql2/e2e-tests/esm-mysql2/Dockerfile +++ b/src/instrumentation/libraries/mysql2/e2e-tests/esm-mysql2/Dockerfile @@ -9,6 +9,8 @@ COPY src/instrumentation/libraries/mysql2/e2e-tests/esm-mysql2/tsconfig.json ./ # Install dependencies RUN npm install +# Add cache-busting argument to force fresh CLI download +ARG CACHEBUST=1 # Install Tusk Drift CLI RUN curl -fsSL https://raw.githubusercontent.com/Use-Tusk/tusk-drift-cli/main/install.sh | sh diff --git a/src/instrumentation/libraries/pg/e2e-tests/cjs-pg/Dockerfile b/src/instrumentation/libraries/pg/e2e-tests/cjs-pg/Dockerfile index 961bed28..29139730 100644 --- a/src/instrumentation/libraries/pg/e2e-tests/cjs-pg/Dockerfile +++ b/src/instrumentation/libraries/pg/e2e-tests/cjs-pg/Dockerfile @@ -9,6 +9,8 @@ COPY src/instrumentation/libraries/pg/e2e-tests/cjs-pg/tsconfig.json ./ # Install dependencies RUN npm install +# Add cache-busting argument to force fresh CLI download +ARG CACHEBUST=1 # Install Tusk Drift CLI RUN curl -fsSL https://raw.githubusercontent.com/Use-Tusk/tusk-drift-cli/main/install.sh | sh diff --git a/src/instrumentation/libraries/pg/e2e-tests/esm-pg/Dockerfile b/src/instrumentation/libraries/pg/e2e-tests/esm-pg/Dockerfile index f2b1ed69..33bed608 100644 --- a/src/instrumentation/libraries/pg/e2e-tests/esm-pg/Dockerfile +++ b/src/instrumentation/libraries/pg/e2e-tests/esm-pg/Dockerfile @@ -9,6 +9,8 @@ COPY src/instrumentation/libraries/pg/e2e-tests/esm-pg/tsconfig.json ./ # Install dependencies RUN npm install +# Add cache-busting argument to force fresh CLI download +ARG CACHEBUST=1 # Install Tusk Drift CLI RUN curl -fsSL https://raw.githubusercontent.com/Use-Tusk/tusk-drift-cli/main/install.sh | sh diff --git a/src/instrumentation/libraries/postgres/e2e-tests/cjs-postgres/Dockerfile b/src/instrumentation/libraries/postgres/e2e-tests/cjs-postgres/Dockerfile index 308b71da..82c9ed9b 100644 --- a/src/instrumentation/libraries/postgres/e2e-tests/cjs-postgres/Dockerfile +++ b/src/instrumentation/libraries/postgres/e2e-tests/cjs-postgres/Dockerfile @@ -9,6 +9,8 @@ COPY src/instrumentation/libraries/postgres/e2e-tests/cjs-postgres/tsconfig.json # Install dependencies RUN npm install +# Add cache-busting argument to force fresh CLI download +ARG CACHEBUST=1 # Install Tusk Drift CLI RUN curl -fsSL https://raw.githubusercontent.com/Use-Tusk/tusk-drift-cli/main/install.sh | sh diff --git a/src/instrumentation/libraries/postgres/e2e-tests/esm-postgres/Dockerfile b/src/instrumentation/libraries/postgres/e2e-tests/esm-postgres/Dockerfile index 8f8919c4..f3d32474 100644 --- a/src/instrumentation/libraries/postgres/e2e-tests/esm-postgres/Dockerfile +++ b/src/instrumentation/libraries/postgres/e2e-tests/esm-postgres/Dockerfile @@ -9,6 +9,8 @@ COPY src/instrumentation/libraries/postgres/e2e-tests/esm-postgres/tsconfig.json # Install dependencies RUN npm install +# Add cache-busting argument to force fresh CLI download +ARG CACHEBUST=1 # Install Tusk Drift CLI RUN curl -fsSL https://raw.githubusercontent.com/Use-Tusk/tusk-drift-cli/main/install.sh | sh From c1ff8a5dbdecfe629be6764720bfca791c1f2264 Mon Sep 17 00:00:00 2001 From: Sohan Kshirsagar Date: Wed, 15 Oct 2025 16:14:37 -0700 Subject: [PATCH 2/4] improve mysql2 version handling --- .../libraries/mysql2/Instrumentation.ts | 175 ++++++++++++------ .../libraries/mysql2/README.md | 5 + .../mysql2/e2e-tests/cjs-mysql2/package.json | 2 +- .../mysql2/e2e-tests/cjs-mysql2/run.sh | 4 +- .../mysql2/integration-tests/mysql2.test.ts | 80 ++++---- 5 files changed, 165 insertions(+), 101 deletions(-) diff --git a/src/instrumentation/libraries/mysql2/Instrumentation.ts b/src/instrumentation/libraries/mysql2/Instrumentation.ts index e8d40727..bd7e9c34 100644 --- a/src/instrumentation/libraries/mysql2/Instrumentation.ts +++ b/src/instrumentation/libraries/mysql2/Instrumentation.ts @@ -22,7 +22,10 @@ import { logger } from "../../../core/utils/logger"; import { TdMysql2ConnectionMock } from "./mocks/TdMysql2ConnectionMock"; import { TdMysql2QueryMock } from "./mocks/TdMysql2QueryMock"; -const SUPPORTED_VERSIONS = [">=3.0.0 <4.0.0"]; +// Version ranges for mysql2 +const COMPLETE_SUPPORTED_VERSIONS = ">=2.3.3 <4.0.0"; +const V2_3_3_TO_3_11_4 = ">=2.3.3 <3.11.5"; +const V3_11_5_TO_4_0 = ">=3.11.5 <4.0.0"; export class Mysql2Instrumentation extends TdInstrumentationBase { private readonly INSTRUMENTATION_NAME = "Mysql2Instrumentation"; @@ -39,36 +42,59 @@ export class Mysql2Instrumentation extends TdInstrumentationBase { return [ new TdInstrumentationNodeModule({ name: "mysql2", - supportedVersions: SUPPORTED_VERSIONS, + supportedVersions: [COMPLETE_SUPPORTED_VERSIONS], files: [ + // For v2.3.3-3.11.4: lib/connection.js with prototypes AND class wrapping + new TdInstrumentationNodeModuleFile({ + name: "mysql2/lib/connection.js", + supportedVersions: [V2_3_3_TO_3_11_4], + patch: (moduleExports: any) => this._patchConnectionV2(moduleExports), + }), + // For v3.11.5+: lib/base/connection.js with prototypes only new TdInstrumentationNodeModuleFile({ name: "mysql2/lib/base/connection.js", - supportedVersions: SUPPORTED_VERSIONS, + supportedVersions: [V3_11_5_TO_4_0], patch: (moduleExports: any) => this._patchBaseConnection(moduleExports), }), + // For v3.11.5+: lib/connection.js with class wrapping only new TdInstrumentationNodeModuleFile({ name: "mysql2/lib/connection.js", - supportedVersions: SUPPORTED_VERSIONS, - patch: (moduleExports: any) => this._patchConnection(moduleExports), + supportedVersions: [V3_11_5_TO_4_0], + patch: (moduleExports: any) => this._patchConnectionV3(moduleExports), + }), + // For v2.3.3-3.11.4: lib/pool.js with prototypes AND class wrapping + new TdInstrumentationNodeModuleFile({ + name: "mysql2/lib/pool.js", + supportedVersions: [V2_3_3_TO_3_11_4], + patch: (moduleExports: any) => this._patchPoolV2(moduleExports), }), + // For v3.11.5+: lib/pool.js with class wrapping only new TdInstrumentationNodeModuleFile({ name: "mysql2/lib/pool.js", - supportedVersions: SUPPORTED_VERSIONS, - patch: (moduleExports: any) => this._patchPool(moduleExports), + supportedVersions: [V3_11_5_TO_4_0], + patch: (moduleExports: any) => this._patchPoolV3(moduleExports), }), + // For v2.3.3-3.11.4: lib/pool_connection.js with class wrapping new TdInstrumentationNodeModuleFile({ name: "mysql2/lib/pool_connection.js", - supportedVersions: SUPPORTED_VERSIONS, - patch: (moduleExports: any) => this._patchPoolConnection(moduleExports), + supportedVersions: [V2_3_3_TO_3_11_4], + patch: (moduleExports: any) => this._patchPoolConnectionV2(moduleExports), }), + // For v3.11.5+: lib/pool_connection.js with class wrapping + new TdInstrumentationNodeModuleFile({ + name: "mysql2/lib/pool_connection.js", + supportedVersions: [V3_11_5_TO_4_0], + patch: (moduleExports: any) => this._patchPoolConnectionV3(moduleExports), + }), + // Factory functions (all versions) new TdInstrumentationNodeModuleFile({ name: "mysql2/lib/create_connection.js", - supportedVersions: SUPPORTED_VERSIONS, + supportedVersions: [COMPLETE_SUPPORTED_VERSIONS], patch: (moduleExports: any) => this._patchCreateConnectionFile(moduleExports), }), new TdInstrumentationNodeModuleFile({ name: "mysql2/lib/create_pool.js", - supportedVersions: SUPPORTED_VERSIONS, + supportedVersions: [COMPLETE_SUPPORTED_VERSIONS], patch: (moduleExports: any) => this._patchCreatePoolFile(moduleExports), }), ], @@ -130,14 +156,34 @@ export class Mysql2Instrumentation extends TdInstrumentationBase { return BaseConnectionClass; } - private _patchConnection(ConnectionClass: any): any { - logger.debug(`[Mysql2Instrumentation] Patching Connection class`); + // v2.3.3-3.11.4: Patch prototypes + private _patchConnectionV2(ConnectionClass: any): any { + logger.debug(`[Mysql2Instrumentation] Patching Connection class (v2)`); if (this.isModulePatched(ConnectionClass)) { logger.debug(`[Mysql2Instrumentation] Connection class already patched, skipping`); return ConnectionClass; } + // Patch all connection prototype methods + this._patchConnectionPrototypes(ConnectionClass); + + this.markModuleAsPatched(ConnectionClass); + + logger.debug(`[Mysql2Instrumentation] Connection class (v2) patching complete`); + return ConnectionClass; + } + + // v3.11.5+: No patching needed (prototypes patched in base/connection.js) + private _patchConnectionV3(ConnectionClass: any): any { + logger.debug(`[Mysql2Instrumentation] Connection class (v3) - skipping (base patched)`); + // For v3.11.5+, lib/connection.js extends base/connection.js + // We already patched the prototypes in base/connection.js, so no need to patch again + return ConnectionClass; + } + + // Helper to patch all connection prototype methods (used by both versions) + private _patchConnectionPrototypes(ConnectionClass: any): void { // Wrap Connection.prototype.query if (ConnectionClass.prototype && ConnectionClass.prototype.query) { if (!isWrapped(ConnectionClass.prototype.query)) { @@ -177,21 +223,46 @@ export class Mysql2Instrumentation extends TdInstrumentationBase { logger.debug(`[Mysql2Instrumentation] Wrapped Connection.prototype.end`); } } + } - this.markModuleAsPatched(ConnectionClass); - logger.debug(`[Mysql2Instrumentation] Connection class patching complete`); + // v2.3.3-3.11.4: Patch prototypes for Pool + private _patchPoolV2(PoolClass: any): any { + logger.debug(`[Mysql2Instrumentation] Patching Pool class (v2)`); - return ConnectionClass; + if (this.isModulePatched(PoolClass)) { + logger.debug(`[Mysql2Instrumentation] Pool class already patched, skipping`); + return PoolClass; + } + + // Patch pool prototype methods + this._patchPoolPrototypes(PoolClass); + + this.markModuleAsPatched(PoolClass); + + logger.debug(`[Mysql2Instrumentation] Pool class (v2) patching complete`); + return PoolClass; } - private _patchPool(PoolClass: any): any { - logger.debug(`[Mysql2Instrumentation] Patching Pool class`); + // v3.11.5+: Patch prototypes for Pool (there's no base/pool.js) + private _patchPoolV3(PoolClass: any): any { + logger.debug(`[Mysql2Instrumentation] Patching Pool class (v3)`); if (this.isModulePatched(PoolClass)) { logger.debug(`[Mysql2Instrumentation] Pool class already patched, skipping`); return PoolClass; } + // Patch pool prototype methods + this._patchPoolPrototypes(PoolClass); + + this.markModuleAsPatched(PoolClass); + + logger.debug(`[Mysql2Instrumentation] Pool class (v3) patching complete`); + return PoolClass; + } + + // Helper to patch pool prototype methods + private _patchPoolPrototypes(PoolClass: any): void { // Wrap Pool.prototype.query if (PoolClass.prototype && PoolClass.prototype.query) { if (!isWrapped(PoolClass.prototype.query)) { @@ -216,39 +287,32 @@ export class Mysql2Instrumentation extends TdInstrumentationBase { } } - this.markModuleAsPatched(PoolClass); - logger.debug(`[Mysql2Instrumentation] Pool class patching complete`); - - return PoolClass; - } - - private _patchPoolConnection(PoolConnectionClass: any): any { - logger.debug(`[Mysql2Instrumentation] Patching PoolConnection class`); - - if (this.isModulePatched(PoolConnectionClass)) { - logger.debug(`[Mysql2Instrumentation] PoolConnection class already patched, skipping`); - return PoolConnectionClass; - } - - // Wrap PoolConnection.prototype.query with poolConnection client type - if (PoolConnectionClass.prototype && PoolConnectionClass.prototype.query) { - if (!isWrapped(PoolConnectionClass.prototype.query)) { - this._wrap(PoolConnectionClass.prototype, "query", this._getQueryPatchFn("poolConnection")); - logger.debug(`[Mysql2Instrumentation] Wrapped PoolConnection.prototype.query`); - } - } - - // Wrap PoolConnection.prototype.execute (prepared statements) - if (PoolConnectionClass.prototype && PoolConnectionClass.prototype.execute) { - if (!isWrapped(PoolConnectionClass.prototype.execute)) { - this._wrap(PoolConnectionClass.prototype, "execute", this._getExecutePatchFn("poolConnection")); - logger.debug(`[Mysql2Instrumentation] Wrapped PoolConnection.prototype.execute`); + // Wrap Pool.prototype.end + if (PoolClass.prototype && PoolClass.prototype.end) { + if (!isWrapped(PoolClass.prototype.end)) { + this._wrap(PoolClass.prototype, "end", this._getEndPatchFn("pool")); + logger.debug(`[Mysql2Instrumentation] Wrapped Pool.prototype.end`); } } + } - this.markModuleAsPatched(PoolConnectionClass); - logger.debug(`[Mysql2Instrumentation] PoolConnection class patching complete`); + // v2.3.3-3.11.4: PoolConnection extends Connection, so inherits patched methods + private _patchPoolConnectionV2(PoolConnectionClass: any): any { + logger.debug( + `[Mysql2Instrumentation] PoolConnection class (v2) - skipping (inherits from Connection)`, + ); + // PoolConnection extends Connection, so it inherits the patched methods + // No additional patching needed + return PoolConnectionClass; + } + // v3.11.5+: PoolConnection extends Connection, so inherits patched methods + private _patchPoolConnectionV3(PoolConnectionClass: any): any { + logger.debug( + `[Mysql2Instrumentation] PoolConnection class (v3) - skipping (inherits from Connection)`, + ); + // PoolConnection extends Connection, so it inherits the patched methods + // No additional patching needed return PoolConnectionClass; } @@ -620,12 +684,7 @@ export class Mysql2Instrumentation extends TdInstrumentationBase { isPreAppStart, }, (spanInfo) => { - return self._handleSimpleCallbackMethod( - spanInfo, - originalPing, - callback, - this, - ); + return self._handleSimpleCallbackMethod(spanInfo, originalPing, callback, this); }, ); }, @@ -690,12 +749,7 @@ export class Mysql2Instrumentation extends TdInstrumentationBase { isPreAppStart, }, (spanInfo) => { - return self._handleSimpleCallbackMethod( - spanInfo, - originalEnd, - callback, - this, - ); + return self._handleSimpleCallbackMethod(spanInfo, originalEnd, callback, this); }, ); }, @@ -807,8 +861,7 @@ export class Mysql2Instrumentation extends TdInstrumentationBase { if (typeof firstArg === "string") { const config: Mysql2QueryConfig = { sql: firstArg, - callback: - typeof args[args.length - 1] === "function" ? args[args.length - 1] : undefined, + callback: typeof args[args.length - 1] === "function" ? args[args.length - 1] : undefined, }; if (Array.isArray(args[1])) { config.values = args[1]; diff --git a/src/instrumentation/libraries/mysql2/README.md b/src/instrumentation/libraries/mysql2/README.md index 16c24874..b1a20da5 100644 --- a/src/instrumentation/libraries/mysql2/README.md +++ b/src/instrumentation/libraries/mysql2/README.md @@ -32,6 +32,7 @@ Records and replays MySQL database operations using the `mysql2` library to ensu ## Implementation Details ### File-Level Patching + Patches internal library files to intercept operations: - **`mysql2/lib/connection.js`**: Patches `Connection` class prototype @@ -83,3 +84,7 @@ The instrumentation follows a clean architecture: - Delegates to query mock for actual query execution This separation keeps the instrumentation maintainable and testable. + +## Known Limitations + +This instrumentation does not patch `changeUser`, `beginTransaction`, `commit`, `rollback` diff --git a/src/instrumentation/libraries/mysql2/e2e-tests/cjs-mysql2/package.json b/src/instrumentation/libraries/mysql2/e2e-tests/cjs-mysql2/package.json index f23ea0a6..eb373c00 100644 --- a/src/instrumentation/libraries/mysql2/e2e-tests/cjs-mysql2/package.json +++ b/src/instrumentation/libraries/mysql2/e2e-tests/cjs-mysql2/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@use-tusk/drift-node-sdk": "file:/sdk", - "mysql2": "^3.11.0", + "mysql2": "^3.9.8", "sequelize": "^6.29.0" }, "devDependencies": { diff --git a/src/instrumentation/libraries/mysql2/e2e-tests/cjs-mysql2/run.sh b/src/instrumentation/libraries/mysql2/e2e-tests/cjs-mysql2/run.sh index 0c256ea6..2e683a31 100755 --- a/src/instrumentation/libraries/mysql2/e2e-tests/cjs-mysql2/run.sh +++ b/src/instrumentation/libraries/mysql2/e2e-tests/cjs-mysql2/run.sh @@ -124,8 +124,8 @@ echo "Step 7: Cleaning up docker containers..." docker-compose down # Step 8: Clean up traces and logs -echo "Step 8: Cleaning up traces and logs..." -cleanup_tusk_files +# echo "Step 8: Cleaning up traces and logs..." +# cleanup_tusk_files echo "MySQL2 E2E test run complete." diff --git a/src/instrumentation/libraries/mysql2/integration-tests/mysql2.test.ts b/src/instrumentation/libraries/mysql2/integration-tests/mysql2.test.ts index a69153ed..1cc5d645 100644 --- a/src/instrumentation/libraries/mysql2/integration-tests/mysql2.test.ts +++ b/src/instrumentation/libraries/mysql2/integration-tests/mysql2.test.ts @@ -357,15 +357,19 @@ test.serial("should capture spans for pool queries", async (t) => { // 2. mysql2.poolConnection.query - the actual query execution t.true(mysql2Spans.length >= 2, "Expected at least 2 spans for pool.query"); - const getConnectionSpan = mysql2Spans.find((s: CleanSpanData) => s.name === "mysql2.pool.getConnection"); + const getConnectionSpan = mysql2Spans.find( + (s: CleanSpanData) => s.name === "mysql2.pool.getConnection", + ); t.truthy(getConnectionSpan, "Should have pool.getConnection span"); - const poolConnectionQuerySpan = mysql2Spans.find( - (s: CleanSpanData) => s.name === "mysql2.poolConnection.query", + const connectionQuerySpan = mysql2Spans.find( + (s: CleanSpanData) => s.name === "mysql2.connection.query", ); - t.truthy(poolConnectionQuerySpan, "Should have poolConnection.query span"); + t.truthy(connectionQuerySpan, "Should have connection.query span"); t.true( - (poolConnectionQuerySpan?.inputValue as Mysql2InputValue)?.sql?.includes("SELECT * FROM test_users"), + (connectionQuerySpan?.inputValue as Mysql2InputValue)?.sql?.includes( + "SELECT * FROM test_users", + ), ); }); @@ -378,17 +382,20 @@ test.serial("should capture spans for pool.getConnection", async (t) => { return; } - poolConnection.query("SELECT COUNT(*) as count FROM test_users", (err: any, results: any) => { - poolConnection.release(); + poolConnection.query( + "SELECT COUNT(*) as count FROM test_users", + (err: any, results: any) => { + poolConnection.release(); - if (err) { - reject(err); - return; - } + if (err) { + reject(err); + return; + } - t.true(parseInt(results[0].count) >= 2); - resolve(); - }); + t.true(parseInt(results[0].count) >= 2); + resolve(); + }, + ); }); }); }); @@ -487,19 +494,16 @@ test.serial("should handle all query() overload variations", async (t) => { }); test.serial("should capture spans even for failed queries", async (t) => { - const error = await t.throwsAsync( - async () => { - await new Promise((resolve, reject) => { - withRootSpan(() => { - connection.query("SELECT * FROM nonexistent_table", (err: any) => { - if (err) reject(err); - else resolve(); - }); + const error = await t.throwsAsync(async () => { + await new Promise((resolve, reject) => { + withRootSpan(() => { + connection.query("SELECT * FROM nonexistent_table", (err: any) => { + if (err) reject(err); + else resolve(); }); }); - }, - undefined, - ); + }); + }, undefined); t.truthy(error); const message = error instanceof Error ? error.message : String(error); @@ -517,18 +521,20 @@ test.serial("should capture spans even for failed queries", async (t) => { }); test.serial("should handle concurrent queries", async (t) => { - const queries = Array.from({ length: 5 }, (_, i) => - new Promise((resolve, reject) => { - withRootSpan(() => { - connection.query( - `SELECT ${i} as query_number, name FROM test_users LIMIT 1`, - (err: any, results: any) => { - if (err) reject(err); - else resolve(results); - }, - ); - }); - }), + const queries = Array.from( + { length: 5 }, + (_, i) => + new Promise((resolve, reject) => { + withRootSpan(() => { + connection.query( + `SELECT ${i} as query_number, name FROM test_users LIMIT 1`, + (err: any, results: any) => { + if (err) reject(err); + else resolve(results); + }, + ); + }); + }), ); const results = await Promise.all(queries); From 2b8bfefd7469324ab80e0b97de78d84a86967e70 Mon Sep 17 00:00:00 2001 From: Sohan Kshirsagar Date: Wed, 15 Oct 2025 16:16:54 -0700 Subject: [PATCH 3/4] fix script --- .../libraries/mysql2/e2e-tests/cjs-mysql2/run.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/instrumentation/libraries/mysql2/e2e-tests/cjs-mysql2/run.sh b/src/instrumentation/libraries/mysql2/e2e-tests/cjs-mysql2/run.sh index 2e683a31..0c256ea6 100755 --- a/src/instrumentation/libraries/mysql2/e2e-tests/cjs-mysql2/run.sh +++ b/src/instrumentation/libraries/mysql2/e2e-tests/cjs-mysql2/run.sh @@ -124,8 +124,8 @@ echo "Step 7: Cleaning up docker containers..." docker-compose down # Step 8: Clean up traces and logs -# echo "Step 8: Cleaning up traces and logs..." -# cleanup_tusk_files +echo "Step 8: Cleaning up traces and logs..." +cleanup_tusk_files echo "MySQL2 E2E test run complete." From aa7c5089cf98f770c642fe0a1ba4436ba80bad61 Mon Sep 17 00:00:00 2001 From: Sohan Kshirsagar Date: Wed, 15 Oct 2025 16:23:16 -0700 Subject: [PATCH 4/4] fix integration test --- .../libraries/mysql2/integration-tests/mysql2.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/instrumentation/libraries/mysql2/integration-tests/mysql2.test.ts b/src/instrumentation/libraries/mysql2/integration-tests/mysql2.test.ts index 1cc5d645..03ad0670 100644 --- a/src/instrumentation/libraries/mysql2/integration-tests/mysql2.test.ts +++ b/src/instrumentation/libraries/mysql2/integration-tests/mysql2.test.ts @@ -410,12 +410,12 @@ test.serial("should capture spans for pool.getConnection", async (t) => { ); t.true(getConnectionSpans.length > 0); - const poolConnectionQuerySpans = spans.filter( + const connectionSpans = spans.filter( (input: CleanSpanData) => input.instrumentationName === "Mysql2Instrumentation" && - (input.inputValue as Mysql2InputValue)?.clientType === "poolConnection", + (input.inputValue as Mysql2InputValue)?.clientType === "connection", ); - t.true(poolConnectionQuerySpans.length > 0); + t.true(connectionSpans.length > 0); }); test.serial("should handle all query() overload variations", async (t) => {