diff --git a/.tekton/tasks/test-groups/collector-27-postgres-task.yaml b/.tekton/tasks/test-groups/collector-27-postgres-task.yaml index abd686caa0..5f3b524f2d 100644 --- a/.tekton/tasks/test-groups/collector-27-postgres-task.yaml +++ b/.tekton/tasks/test-groups/collector-27-postgres-task.yaml @@ -27,6 +27,35 @@ spec: initialDelaySeconds: 3 periodSeconds: 2 timeoutSeconds: 10 + - name: mysql + image: public.ecr.aws/docker/library/mysql:8.0.26 + imagePullPolicy: IfNotPresent + resources: + requests: + cpu: "500m" + memory: "1Gi" + args: + - "--default-authentication-plugin=mysql_native_password" + env: + - name: "MYSQL_ROOT_PASSWORD" + value: "nodepw" + - name: "MYSQL_DATABASE" + value: "nodedb" + - name: "MYSQL_USER" + value: "node" + - name: "MYSQL_PASSWORD" + value: "nodepw" + - name: "MYSQL_ROOT_HOST" + value: "0.0.0.0" + readinessProbe: + exec: + command: + - "sh" + - "-c" + - "mysql -h 0.0.0.0 -u node -p'nodepw' -e 'SELECT 1'" + initialDelaySeconds: 3 + periodSeconds: 2 + timeoutSeconds: 10 envFrom: - configMapRef: name: environment-properties @@ -102,7 +131,7 @@ spec: memory: "8Gi" script: | #!/bin/bash - AVAILABLE_SIDECARS="postgres" + AVAILABLE_SIDECARS="postgres,mysql" SIDECAR_COUNTS="redis=20,redis-slave=20,redis-sentinel=20,zookeeper=6,kafka=6,kafka-topics=6,postgres=3,elasticsearch=2,mongodb=1,couchbase=1,rabbitmq=1,nats=1,nats-streaming=1,nats-streaming-2=1,mysql=1,localstack=1,memcached=1,oracledb=1" ARTIFACTS_PATH="$(workspaces.output.path)" cd $ARTIFACTS_PATH diff --git a/packages/collector/test/integration/currencies/databases/prisma/app.js b/packages/collector/test/integration/currencies/databases/prisma/app.js index f2b5ebbdc4..173a7120d8 100644 --- a/packages/collector/test/integration/currencies/databases/prisma/app.js +++ b/packages/collector/test/integration/currencies/databases/prisma/app.js @@ -46,6 +46,17 @@ try { const { PrismaPg } = require('@prisma/adapter-pg'); adapter = new PrismaPg({ connectionString: process.env.INSTANA_CONNECT_POSTGRES_PRISMA_URL }); log(`Initialized Prisma ${version} with PostgreSQL adapter`); + } else if (provider === 'mysql') { + const { PrismaMariaDb } = require('@prisma/adapter-mariadb'); + adapter = new PrismaMariaDb({ + host: process.env.INSTANA_CONNECT_MYSQL_HOST, + port: Number(process.env.INSTANA_CONNECT_MYSQL_PORT), + user: process.env.INSTANA_CONNECT_MYSQL_USER, + password: process.env.INSTANA_CONNECT_MYSQL_PW, + database: process.env.INSTANA_CONNECT_MYSQL_DB, + connectionLimit: 5 + }); + log(`Initialized Prisma ${version} with MariaDB adapter`); } else { const { PrismaBetterSqlite3 } = require('@prisma/adapter-better-sqlite3'); const dbPath = path.join(__dirname, 'dev.db'); diff --git a/packages/collector/test/integration/currencies/databases/prisma/app.mjs b/packages/collector/test/integration/currencies/databases/prisma/app.mjs index 516730d605..b38af5a144 100644 --- a/packages/collector/test/integration/currencies/databases/prisma/app.mjs +++ b/packages/collector/test/integration/currencies/databases/prisma/app.mjs @@ -47,6 +47,17 @@ try { const { PrismaPg } = await import('@prisma/adapter-pg'); adapter = new PrismaPg({ connectionString: process.env.INSTANA_CONNECT_POSTGRES_PRISMA_URL }); console.log(`Initialized Prisma ${version} with PostgreSQL adapter`); + } else if (provider === 'mysql') { + const { PrismaMariaDb } = await import('@prisma/adapter-mariadb'); + adapter = new PrismaMariaDb({ + host: process.env.INSTANA_CONNECT_MYSQL_HOST, + port: Number(process.env.INSTANA_CONNECT_MYSQL_PORT), + user: process.env.INSTANA_CONNECT_MYSQL_USER, + password: process.env.INSTANA_CONNECT_MYSQL_PW, + database: process.env.INSTANA_CONNECT_MYSQL_DB, + connectionLimit: 5 + }); + console.log(`Initialized Prisma ${version} with MySQL adapter`); } else { const { PrismaBetterSqlite3 } = await import('@prisma/adapter-better-sqlite3'); const __dirname = path.dirname(fileURLToPath(import.meta.url)); diff --git a/packages/collector/test/integration/currencies/databases/prisma/modes.json b/packages/collector/test/integration/currencies/databases/prisma/modes.json index 5a2c99745d..e791113251 100644 --- a/packages/collector/test/integration/currencies/databases/prisma/modes.json +++ b/packages/collector/test/integration/currencies/databases/prisma/modes.json @@ -1,4 +1,5 @@ [ "sqlite", + "mysql", "postgresql" ] \ No newline at end of file diff --git a/packages/collector/test/integration/currencies/databases/prisma/package.json.template.v7 b/packages/collector/test/integration/currencies/databases/prisma/package.json.template.v7 index c078754374..ca1e4cc49d 100644 --- a/packages/collector/test/integration/currencies/databases/prisma/package.json.template.v7 +++ b/packages/collector/test/integration/currencies/databases/prisma/package.json.template.v7 @@ -2,6 +2,7 @@ "dependencies": { "@prisma/client": "{{CURRENCY_VERSION}}", "@prisma/adapter-pg": "{{CURRENCY_VERSION}}", + "@prisma/adapter-mariadb": "{{CURRENCY_VERSION}}", "@prisma/adapter-better-sqlite3": "{{CURRENCY_VERSION}}" } } \ No newline at end of file diff --git a/packages/collector/test/integration/currencies/databases/prisma/prisma-mysql.config.js b/packages/collector/test/integration/currencies/databases/prisma/prisma-mysql.config.js new file mode 100644 index 0000000000..101eae073d --- /dev/null +++ b/packages/collector/test/integration/currencies/databases/prisma/prisma-mysql.config.js @@ -0,0 +1,19 @@ +/* + * (c) Copyright IBM Corp. 2026 + */ + +'use strict'; + +const { defineConfig } = require('prisma/config'); + +const host = process.env.INSTANA_CONNECT_MYSQL_HOST; +const port = process.env.INSTANA_CONNECT_MYSQL_PORT; +const user = process.env.INSTANA_CONNECT_MYSQL_USER; +const password = process.env.INSTANA_CONNECT_MYSQL_PW; +const database = process.env.INSTANA_CONNECT_MYSQL_DB; + +module.exports = defineConfig({ + datasource: { + url: `mysql://${user}:${password}@${host}:${port}/${database}` + } +}); diff --git a/packages/collector/test/integration/currencies/databases/prisma/prisma/migrations-mysql/20221020125804_init/migration.sql b/packages/collector/test/integration/currencies/databases/prisma/prisma/migrations-mysql/20221020125804_init/migration.sql new file mode 100644 index 0000000000..61d45a06dc --- /dev/null +++ b/packages/collector/test/integration/currencies/databases/prisma/prisma/migrations-mysql/20221020125804_init/migration.sql @@ -0,0 +1,6 @@ +CREATE TABLE `Person` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(191) NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/packages/collector/test/integration/currencies/databases/prisma/prisma/migrations-mysql/migration_lock.toml b/packages/collector/test/integration/currencies/databases/prisma/prisma/migrations-mysql/migration_lock.toml new file mode 100644 index 0000000000..9bee74de58 --- /dev/null +++ b/packages/collector/test/integration/currencies/databases/prisma/prisma/migrations-mysql/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "mysql" diff --git a/packages/collector/test/integration/currencies/databases/prisma/prisma/schema.prisma.mysql b/packages/collector/test/integration/currencies/databases/prisma/prisma/schema.prisma.mysql new file mode 100644 index 0000000000..8306bde5c9 --- /dev/null +++ b/packages/collector/test/integration/currencies/databases/prisma/prisma/schema.prisma.mysql @@ -0,0 +1,17 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + // prisma-client-js provider will be removed in future releases of Prisma ORM. + provider = "prisma-client-js" +} + +datasource db { + provider = "mysql" +} + +model Person { + id Int @id @default(autoincrement()) + name String? +} + diff --git a/packages/collector/test/integration/currencies/databases/prisma/test_base.js b/packages/collector/test/integration/currencies/databases/prisma/test_base.js index 48062c5195..4e4ca41843 100644 --- a/packages/collector/test/integration/currencies/databases/prisma/test_base.js +++ b/packages/collector/test/integration/currencies/databases/prisma/test_base.js @@ -22,9 +22,14 @@ const migrationsTargetDir = path.join(appDir, 'prisma', 'migrations'); module.exports = function (name, version, isLatest, mode) { this.timeout(Math.max(config.getTestTimeout() * 3, 20000)); - const provider = mode; // mode is either 'sqlite' or 'postgresql' + const provider = mode; const majorVersion = parseInt(version, 10); const isV7 = majorVersion >= 7; + + if (provider === 'mysql' && !isV7) { + return; + } + // Getting the URL is not possible between Prisma 4.10 and 5.1 (getConfig was removed) const urlUnavailable = semver.gte(version, '4.10.0') && semver.lt(version, '5.2.0'); @@ -198,6 +203,17 @@ module.exports = function (name, version, isLatest, mode) { case 'postgresql': expectedUrl = process.env.INSTANA_CONNECT_POSTGRES_PRISMA_URL.replace('nodepw', '_redacted_'); break; + case 'mysql': { + const { + INSTANA_CONNECT_MYSQL_HOST: host, + INSTANA_CONNECT_MYSQL_PORT: port, + INSTANA_CONNECT_MYSQL_USER: user, + INSTANA_CONNECT_MYSQL_DB: database + } = process.env; + + expectedUrl = `mysql://${user}:_redacted_@${host}:${port}/${database}`; + break; + } default: throw new Error(`Unknown provider: ${provider}`); } diff --git a/packages/core/src/tracing/instrumentation/databases/prisma.js b/packages/core/src/tracing/instrumentation/databases/prisma.js index e621c0b53a..8243b16485 100644 --- a/packages/core/src/tracing/instrumentation/databases/prisma.js +++ b/packages/core/src/tracing/instrumentation/databases/prisma.js @@ -13,13 +13,63 @@ const cls = require('../../cls'); let logger; let isActive = false; +// Maps database connection info to Prisma instances for span creation +// Key: engine instance (accessible during span creation via ctx._engine) +// Value: { provider: string, dataSourceUrl: string } const providerAndDataSourceUriMap = new WeakMap(); +// Temporary storage for MariaDB adapter connection URLs +// Key: adapter instance +// Value: { provider: string, dataSourceUrl: string } +// Why two maps? Adapter config is only accessible during construction, but spans only have access to the engine. +// Flow: 1) Adapter constructor captures URL → mariadbAdapterConfigMap +// 2) PrismaClient constructor transfers URL → providerAndDataSourceUriMap (keyed by engine) +// 3) Span creation retrieves URL using engine instance +const mariadbAdapterConfigMap = new WeakMap(); + exports.init = function init(config) { logger = config.logger; + + hook.onModuleLoad('@prisma/adapter-mariadb', instrumentMariaDbAdapter); hook.onModuleLoad('@prisma/client', instrumentPrismaClient); }; +function instrumentMariaDbAdapter(mariadbAdapterModule) { + const OriginalPrismaMariaDb = mariadbAdapterModule?.PrismaMariaDb; + + if (typeof OriginalPrismaMariaDb !== 'function') { + return mariadbAdapterModule; + } + + class InstanaPrismaMariaDb extends OriginalPrismaMariaDb { + constructor(...args) { + super(...args); + const [config] = args; + if (!config || typeof config !== 'object') { + return; + } + const { host = 'localhost', port = 3306, user = '', database = '' } = config; + + if (!user || !database) { + return; + } + + const sanitizedUrl = `mysql://${user}:_redacted_@${host}:${port}/${database}`; + + mariadbAdapterConfigMap.set(this, { + // Prisma MariaDB adapter reports mysql as provider + provider: 'mysql', + url: sanitizedUrl + }); + } + } + + return { + ...mariadbAdapterModule, + PrismaMariaDb: InstanaPrismaMariaDb + }; +} + function instrumentPrismaClient(prismaClientModule) { instrumentClientConstructor(prismaClientModule); shimRequest(prismaClientModule); @@ -86,7 +136,11 @@ function instrumentClientConstructor(prismaClientModule) { if (this._engineConfig.adapter) { const adapter = this._engineConfig.adapter; try { - if (adapter?.config?.connectionString) { + // Get URL captured during MariaDB adapter construction + const capturedConfig = mariadbAdapterConfigMap.get(adapter); + if (capturedConfig) { + dataSourceUrl = capturedConfig.url; + } else if (adapter?.config?.connectionString) { dataSourceUrl = redactPassword(provider, adapter.config.connectionString); } else if (adapter?.externalPool?.options?.connectionString) { dataSourceUrl = redactPassword(provider, adapter.externalPool.options.connectionString);