diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml
index 2fe06941..2d8cb27a 100644
--- a/.github/workflows/php.yml
+++ b/.github/workflows/php.yml
@@ -32,6 +32,7 @@ jobs:
'Instrumentation/IO',
'Instrumentation/Laravel',
'Instrumentation/MongoDB',
+ 'Instrumentation/MySqli',
'Instrumentation/OpenAIPHP',
'Instrumentation/PDO',
# Sort PSRs numerically.
@@ -92,6 +93,12 @@ jobs:
php-version: 8.0
- project: 'Instrumentation/Curl'
php-version: 8.1
+ - project: 'Instrumentation/MySqli'
+ php-version: 7.4
+ - project: 'Instrumentation/MySqli'
+ php-version: 8.0
+ - project: 'Instrumentation/MySqli'
+ php-version: 8.1
- project: 'Instrumentation/PDO'
php-version: 7.4
- project: 'Instrumentation/PDO'
@@ -132,7 +139,7 @@ jobs:
with:
php-version: ${{ matrix.php-version }}
coverage: xdebug
- extensions: ast, amqp, grpc, opentelemetry, rdkafka
+ extensions: ast, amqp, grpc, opentelemetry, rdkafka, mysqli
- name: Validate composer.json and composer.lock
run: composer validate
@@ -192,6 +199,11 @@ jobs:
run: |
KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://localhost:29092,PLAINTEXT_HOST://localhost:9092 docker compose up kafka -d --wait
+ - name: Start Mysql
+ if: ${{ matrix.project == 'Instrumentation/MySqli' }}
+ run: |
+ docker compose up mysql -d --wait
+
- name: Run PHPUnit
working-directory: src/${{ matrix.project }}
run: vendor/bin/phpunit
diff --git a/.gitsplit.yml b/.gitsplit.yml
index 06edf178..23ee30cc 100644
--- a/.gitsplit.yml
+++ b/.gitsplit.yml
@@ -30,6 +30,8 @@ splits:
target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-auto-laravel.git"
- prefix: "src/Instrumentation/MongoDB"
target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-auto-mongodb.git"
+ - prefix: "src/Instrumentation/MySqli"
+ target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-auto-mysqli.git"
- prefix: "src/Instrumentation/OpenAIPHP"
target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-auto-openai.git"
- prefix: "src/Instrumentation/PDO"
diff --git a/composer.json b/composer.json
index c60c5fd2..7db6e192 100644
--- a/composer.json
+++ b/composer.json
@@ -24,6 +24,7 @@
"OpenTelemetry\\Contrib\\Instrumentation\\HttpAsyncClient\\": "src/Instrumentation/HttpAsyncClient/src",
"OpenTelemetry\\Contrib\\Instrumentation\\IO\\": "src/Instrumentation/IO/src",
"OpenTelemetry\\Contrib\\Instrumentation\\MongoDB\\": "src/Instrumentation/MongoDB/src",
+ "OpenTelemetry\\Contrib\\Instrumentation\\MySqli\\": "src/Instrumentation/MySqli/src",
"OpenTelemetry\\Contrib\\Instrumentation\\PDO\\": "src/Instrumentation/PDO/src",
"OpenTelemetry\\Contrib\\Instrumentation\\Psr3\\": "src/Instrumentation/Psr3/src",
"OpenTelemetry\\Contrib\\Instrumentation\\Psr15\\": "src/Instrumentation/Psr15/src",
@@ -47,6 +48,7 @@
"src/Instrumentation/IO/_register.php",
"src/Instrumentation/Laravel/_register.php",
"src/Instrumentation/MongoDB/_register.php",
+ "src/Instrumentation/MySqli/_register.php",
"src/Instrumentation/PDO/_register.php",
"src/Instrumentation/Psr3/_register.php",
"src/Instrumentation/Psr15/_register.php",
@@ -66,6 +68,7 @@
"open-telemetry/opentelemetry-auto-http-async": "self.version",
"open-telemetry/opentelemetry-auto-io": "self.version",
"open-telemetry/opentelemetry-auto-mongodb": "self.version",
+ "open-telemetry/opentelemetry-auto-mysqli": "self.version",
"open-telemetry/opentelemetry-auto-pdo": "self.version",
"open-telemetry/opentelemetry-auto-psr3": "self.version",
"open-telemetry/opentelemetry-auto-psr15": "self.version",
diff --git a/docker-compose.yaml b/docker-compose.yaml
index e9d99f4a..2d3ad109 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -14,6 +14,8 @@ services:
PHP_IDE_CONFIG: ${PHP_IDE_CONFIG:-''}
RABBIT_HOST: ${RABBIT_HOST:-rabbitmq}
KAFKA_HOST: ${KAFKA_HOST:-kafka}
+ MYSQL_HOST: ${MYSQL_HOST:-mysql}
+
zipkin:
image: openzipkin/zipkin-slim
@@ -61,4 +63,20 @@ services:
volumes:
- ./docker/kafka/update_run.sh:/tmp/update_run.sh
-
+ mysql:
+ image: mysql:8.0
+ hostname: mysql
+ ports:
+ - "3306:3306/tcp"
+ environment:
+ MYSQL_ROOT_PASSWORD: root_password
+ MYSQL_DATABASE: otel_db
+ MYSQL_USER: otel_user
+ MYSQL_PASSWORD: otel_passwd
+ healthcheck:
+ test: mysql -uotel_user -potel_passwd -e "USE otel_db;"
+ interval: 30s
+ timeout: 30s
+ retries: 3
+ volumes:
+ - ./docker/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 9a5951e6..ebfc9b25 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -7,6 +7,7 @@ RUN install-php-extensions \
opentelemetry \
mongodb \
amqp \
- rdkafka
+ rdkafka \
+ mysqli
USER php
diff --git a/docker/mysql/init.sql b/docker/mysql/init.sql
new file mode 100644
index 00000000..e1b5c346
--- /dev/null
+++ b/docker/mysql/init.sql
@@ -0,0 +1,34 @@
+CREATE DATABASE IF NOT EXISTS otel_db2;
+CREATE USER 'otel_user2'@'%' IDENTIFIED BY 'otel_passwd';
+
+
+GRANT ALL PRIVILEGES ON *.* TO 'otel_user'@'%';
+GRANT ALL PRIVILEGES ON *.* TO 'otel_user2'@'%';
+FLUSH PRIVILEGES;
+
+
+USE otel_db;
+
+CREATE TABLE users (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ email VARCHAR(255) UNIQUE NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+INSERT INTO users (name, email) VALUES
+('John Doe', 'john.doe@example.com'),
+('Jane Smith', 'jane.smith@example.com'),
+('Bob Johnson', 'bob.johnson@example.com');
+
+CREATE TABLE products (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ price DECIMAL(10, 2) NOT NULL,
+ stock INT NOT NULL DEFAULT 0
+);
+
+INSERT INTO products (name, price, stock) VALUES
+('Laptop', 999.99, 10),
+('Smartphone', 499.99, 25),
+('Headphones', 49.99, 50);
diff --git a/src/Instrumentation/MySqli/.gitattributes b/src/Instrumentation/MySqli/.gitattributes
new file mode 100644
index 00000000..1676cf82
--- /dev/null
+++ b/src/Instrumentation/MySqli/.gitattributes
@@ -0,0 +1,12 @@
+* text=auto
+
+*.md diff=markdown
+*.php diff=php
+
+/.gitattributes export-ignore
+/.gitignore export-ignore
+/.php-cs-fixer.php export-ignore
+/phpstan.neon.dist export-ignore
+/phpunit.xml.dist export-ignore
+/psalm.xml.dist export-ignore
+/tests export-ignore
diff --git a/src/Instrumentation/MySqli/.gitignore b/src/Instrumentation/MySqli/.gitignore
new file mode 100644
index 00000000..57872d0f
--- /dev/null
+++ b/src/Instrumentation/MySqli/.gitignore
@@ -0,0 +1 @@
+/vendor/
diff --git a/src/Instrumentation/MySqli/.php-cs-fixer.php b/src/Instrumentation/MySqli/.php-cs-fixer.php
new file mode 100644
index 00000000..e35fa078
--- /dev/null
+++ b/src/Instrumentation/MySqli/.php-cs-fixer.php
@@ -0,0 +1,43 @@
+exclude('vendor')
+ ->exclude('var/cache')
+ ->in(__DIR__);
+
+$config = new PhpCsFixer\Config();
+return $config->setRules([
+ 'concat_space' => ['spacing' => 'one'],
+ 'declare_equal_normalize' => ['space' => 'none'],
+ 'is_null' => true,
+ 'modernize_types_casting' => true,
+ 'ordered_imports' => true,
+ 'php_unit_construct' => true,
+ 'single_line_comment_style' => true,
+ 'yoda_style' => false,
+ '@PSR2' => true,
+ 'array_syntax' => ['syntax' => 'short'],
+ 'blank_line_after_opening_tag' => true,
+ 'blank_line_before_statement' => true,
+ 'cast_spaces' => true,
+ 'declare_strict_types' => true,
+ 'type_declaration_spaces' => true,
+ 'include' => true,
+ 'lowercase_cast' => true,
+ 'new_with_parentheses' => true,
+ 'no_extra_blank_lines' => true,
+ 'no_leading_import_slash' => true,
+ 'echo_tag_syntax' => true,
+ 'no_unused_imports' => true,
+ 'no_useless_else' => true,
+ 'no_useless_return' => true,
+ 'phpdoc_order' => true,
+ 'phpdoc_scalar' => true,
+ 'phpdoc_types' => true,
+ 'short_scalar_cast' => true,
+ 'blank_lines_before_namespace' => true,
+ 'single_quote' => true,
+ 'trailing_comma_in_multiline' => true,
+ ])
+ ->setRiskyAllowed(true)
+ ->setFinder($finder);
+
diff --git a/src/Instrumentation/MySqli/README.md b/src/Instrumentation/MySqli/README.md
new file mode 100644
index 00000000..01a08d87
--- /dev/null
+++ b/src/Instrumentation/MySqli/README.md
@@ -0,0 +1,56 @@
+[![Releases](https://img.shields.io/badge/releases-purple)](https://github.com/opentelemetry-php/contrib-auto-mysqli/releases)
+[![Issues](https://img.shields.io/badge/issues-pink)](https://github.com/open-telemetry/opentelemetry-php/issues)
+[![Source](https://img.shields.io/badge/source-contrib-green)](https://github.com/open-telemetry/opentelemetry-php-contrib/tree/main/src/Instrumentation/MySqli)
+[![Mirror](https://img.shields.io/badge/mirror-opentelemetry--php--contrib-blue)](https://github.com/opentelemetry-php/contrib-auto-mysqli)
+[![Latest Version](http://poser.pugx.org/open-telemetry/opentelemetry-auto-mysqli/v/unstable)](https://packagist.org/packages/open-telemetry/opentelemetry-auto-mysqli/)
+[![Stable](http://poser.pugx.org/open-telemetry/opentelemetry-auto-mysqli/v/stable)](https://packagist.org/packages/open-telemetry/opentelemetry-auto-mysqli/)
+
+This is a read-only subtree split of https://github.com/open-telemetry/opentelemetry-php-contrib.
+
+# OpenTelemetry mysqli auto-instrumentation
+
+Please read https://opentelemetry.io/docs/instrumentation/php/automatic/ for instructions on how to
+install and configure the extension and SDK.
+
+## Overview
+Auto-instrumentation hooks are registered via composer, and client kind spans will automatically be created when calling following functions or methods:
+
+* `mysqli_connect`
+* `mysqli::__construct`
+* `mysqli::connect`
+* `mysqli_real_connect`
+* `mysqli::real_connect`
+
+* `mysqli_query`
+* `mysqli::query`
+* `mysqli_real_query`
+* `mysqli::real_query`
+* `mysqli_execute_query`
+* `mysqli::execute_query`
+* `mysqli_multi_query`
+* `mysqli::multi_query`
+* `mysqli_next_result`
+* `mysqli::next_result`
+
+* `mysqli_begin_transaction`
+* `mysqli::begin_transaction`
+* `mysqli_rollback`
+* `mysqli::rollback`
+* `mysqli_commit`
+* `mysqli::commit`
+*
+* `mysqli_stmt_execute`
+* `mysqli_stmt::execute`
+* `mysqli_stmt_next_result`
+* `mysqli_stmt::next_result`
+
+## Configuration
+
+### Disabling mysqli instrumentation
+
+The extension can be disabled via [runtime configuration](https://opentelemetry.io/docs/instrumentation/php/sdk/#configuration):
+
+```shell
+OTEL_PHP_DISABLED_INSTRUMENTATIONS=mysqli
+```
+
diff --git a/src/Instrumentation/MySqli/_register.php b/src/Instrumentation/MySqli/_register.php
new file mode 100644
index 00000000..10cb9823
--- /dev/null
+++ b/src/Instrumentation/MySqli/_register.php
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ src
+
+
+
+
+
+
+
+
+
+
+
+
+ tests/Unit
+
+
+ tests/Integration
+
+
+
+
diff --git a/src/Instrumentation/MySqli/psalm.xml.dist b/src/Instrumentation/MySqli/psalm.xml.dist
new file mode 100644
index 00000000..5a04b34d
--- /dev/null
+++ b/src/Instrumentation/MySqli/psalm.xml.dist
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Instrumentation/MySqli/src/MySqliInstrumentation.php b/src/Instrumentation/MySqli/src/MySqliInstrumentation.php
new file mode 100644
index 00000000..4ee60abc
--- /dev/null
+++ b/src/Instrumentation/MySqli/src/MySqliInstrumentation.php
@@ -0,0 +1,832 @@
+storeMySqliAttributes($mysqliObject, $params[$paramsOffset + 0] ?? null, $params[$paramsOffset + 1] ?? null, $params[$paramsOffset + 3] ?? null, $params[$paramsOffset + 4] ?? null, null);
+ }
+
+ self::endSpan([], $exception, ($retVal === false && !$exception) ? mysqli_connect_error() : null);
+ }
+
+ /** @param non-empty-string $spanName */
+ private static function queryPreHook(string $spanName, CachedInstrumentation $instrumentation, MySqliTracker $tracker, $obj, array $params, ?string $class, ?string $function, ?string $filename, ?int $lineno): void
+ {
+ $span = self::startSpan($spanName, $instrumentation, $class, $function, $filename, $lineno, []);
+ $mysqli = $obj ? $obj : $params[0];
+ self::addTransactionLink($tracker, $span, $mysqli);
+ }
+
+ private static function queryPostHook(CachedInstrumentation $instrumentation, MySqliTracker $tracker, $obj, array $params, mixed $retVal, ?\Throwable $exception)
+ {
+ $mysqli = $obj ? $obj : $params[0];
+ $query = $obj ? $params[0] : $params[1];
+
+ $attributes = $tracker->getMySqliAttributes($mysqli);
+
+ $attributes[TraceAttributes::DB_STATEMENT] = mb_convert_encoding($query, 'UTF-8');
+ $attributes[TraceAttributes::DB_OPERATION_NAME] = self::extractQueryCommand($query);
+
+ if ($retVal === false || $exception) {
+ //TODO use constant from comment after sem-conv update
+ $attributes[/*TraceAttributes::DB_RESPONSE_STATUS_CODE*/ 'db.response.status_code'] = $mysqli->errno;
+ }
+
+ $errorStatus = ($retVal === false && !$exception) ? $mysqli->error : null;
+ self::endSpan($attributes, $exception, $errorStatus);
+
+ }
+
+ private static function multiQueryPostHook(CachedInstrumentation $instrumentation, MySqliTracker $tracker, $obj, array $params, mixed $retVal, ?\Throwable $exception)
+ {
+
+ $mysqli = $obj ? $obj : $params[0];
+ $query = $obj ? $params[0] : $params[1];
+
+ $attributes = $tracker->getMySqliAttributes($mysqli);
+
+ $tracker->storeMySqliMultiQuery($mysqli, $query);
+ if ($currentQuery = $tracker->getNextMySqliMultiQuery($mysqli)) {
+ $attributes[TraceAttributes::DB_STATEMENT] = mb_convert_encoding($currentQuery, 'UTF-8');
+ $attributes[TraceAttributes::DB_OPERATION_NAME] = self::extractQueryCommand($currentQuery);
+ }
+
+ if ($retVal === false || $exception) {
+ //TODO use constant from comment after sem-conv update
+ $attributes[/*TraceAttributes::DB_RESPONSE_STATUS_CODE*/ 'db.response.status_code'] = $mysqli->errno;
+ } else {
+ $tracker->trackMySqliSpan($mysqli, Span::getCurrent()->getContext());
+ }
+
+ $errorStatus = ($retVal === false && !$exception) ? $mysqli->error : null;
+ self::endSpan($attributes, $exception, $errorStatus);
+
+ }
+
+ /** @param non-empty-string $spanName */
+ private static function nextResultPreHook(string $spanName, CachedInstrumentation $instrumentation, MySqliTracker $tracker, $obj, array $params, ?string $class, ?string $function, ?string $filename, ?int $lineno): void
+ {
+ $span = self::startSpan($spanName, $instrumentation, $class, $function, $filename, $lineno, []);
+ $mysqli = $obj ? $obj : $params[0];
+ if ($mysqli instanceof mysqli && ($spanContext = $tracker->getMySqliSpan($mysqli))) {
+ $span->addLink($spanContext);
+ }
+
+ self::addTransactionLink($tracker, $span, $mysqli);
+ }
+
+ private static function nextResultPostHook(CachedInstrumentation $instrumentation, MySqliTracker $tracker, $obj, array $params, mixed $retVal, ?\Throwable $exception)
+ {
+
+ $mysqli = $obj ? $obj : $params[0];
+
+ $errorStatus = ($retVal === false && !$exception) ? (strlen($mysqli->error) > 0 ? $mysqli->error : null) : null;
+
+ $attributes = $tracker->getMySqliAttributes($mysqli);
+
+ $currentQuery = $tracker->getNextMySqliMultiQuery($mysqli);
+
+ // it was just a call to check if there is a pending query
+ if ($currentQuery === null || ($retVal === false && !$errorStatus && !$exception)) {
+ self::logDebug('nextResultPostHook span dropped', ['exception' => $exception, 'obj' => $obj, 'retVal' => $retVal, 'params' => $params, 'currentQuery' => $currentQuery]);
+ self::dropSpan();
+
+ return;
+ }
+
+ if ($currentQuery) {
+ $attributes[TraceAttributes::DB_STATEMENT] = mb_convert_encoding($currentQuery, 'UTF-8');
+ $attributes[TraceAttributes::DB_OPERATION_NAME] = self::extractQueryCommand($currentQuery);
+ }
+
+ if ($retVal === false || $exception) {
+ //TODO use constant from comment after sem-conv update
+ $attributes[/*TraceAttributes::DB_RESPONSE_STATUS_CODE*/ 'db.response.status_code'] = $mysqli->errno;
+ }
+
+ self::endSpan($attributes, $exception, $errorStatus);
+ }
+
+ private static function changeUserPostHook(CachedInstrumentation $instrumentation, MySqliTracker $tracker, $obj, array $params, mixed $retVal, ?\Throwable $exception)
+ {
+ if ($retVal != true) {
+ return;
+ }
+
+ $mysqli = $obj ? $obj : $params[0];
+
+ $tracker->addMySqliAttribute($mysqli, TraceAttributes::DB_USER, $params[$obj ? 0 : 1]);
+ if (($database = $params[$obj ? 2 : 3] ?? null) !== null) {
+ $tracker->addMySqliAttribute($mysqli, TraceAttributes::DB_NAMESPACE, $database);
+ }
+
+ }
+
+ private static function selectDbPostHook(CachedInstrumentation $instrumentation, MySqliTracker $tracker, $obj, array $params, mixed $retVal, ?\Throwable $exception)
+ {
+ if ($retVal != true) {
+ return;
+ }
+ $tracker->addMySqliAttribute($obj ? $obj : $params[0], TraceAttributes::DB_NAMESPACE, $params[$obj ? 0 : 1]);
+ }
+
+ /** @param non-empty-string $spanName */
+ private static function preparePreHook(string $spanName, CachedInstrumentation $instrumentation, MySqliTracker $tracker, $obj, array $params, ?string $class, ?string $function, ?string $filename, ?int $lineno): void
+ {
+ $span = self::startSpan($spanName, $instrumentation, $class, $function, $filename, $lineno, []);
+ $mysqli = $obj ? $obj : $params[0];
+ self::addTransactionLink($tracker, $span, $mysqli);
+ }
+
+ private static function preparePostHook(CachedInstrumentation $instrumentation, MySqliTracker $tracker, $obj, array $params, mixed $stmtRetVal, ?\Throwable $exception)
+ {
+ $mysqli = $obj ? $obj : $params[0];
+ $query = $params[$obj ? 0 : 1];
+
+ $errorStatus = null;
+
+ $query = mb_convert_encoding($query, 'UTF-8');
+ $operation = self::extractQueryCommand($query);
+
+ $attributes = $tracker->getMySqliAttributes($mysqli);
+ $attributes[TraceAttributes::DB_STATEMENT] = $query;
+ $attributes[TraceAttributes::DB_OPERATION_NAME] = $operation;
+
+ if (!$exception && $stmtRetVal instanceof mysqli_stmt) {
+ $tracker->trackMySqliFromStatement($mysqli, $stmtRetVal);
+ $tracker->addStatementAttribute($stmtRetVal, TraceAttributes::DB_STATEMENT, $query);
+ $tracker->addStatementAttribute($stmtRetVal, TraceAttributes::DB_OPERATION_NAME, $operation);
+
+ } else {
+ //TODO use constant from comment after sem-conv update
+ $attributes[/*TraceAttributes::DB_RESPONSE_STATUS_CODE*/ 'db.response.status_code'] = $mysqli->errno;
+ $errorStatus = !$exception ? $mysqli->error : null;
+ }
+
+ self::endSpan($attributes, $exception, $errorStatus);
+ }
+
+ /** @param non-empty-string $spanName */
+ private static function beginTransactionPreHook(string $spanName, CachedInstrumentation $instrumentation, MySqliTracker $tracker, $obj, array $params, ?string $class, ?string $function, ?string $filename, ?int $lineno): void
+ {
+ self::startSpan($spanName, $instrumentation, $class, $function, $filename, $lineno, []);
+ }
+
+ private static function beginTransactionPostHook(CachedInstrumentation $instrumentation, MySqliTracker $tracker, $obj, array $params, mixed $retVal, ?\Throwable $exception)
+ {
+ $mysqli = $obj ? $obj : $params[0];
+ $transactionName = $params[$obj ? 1 : 2] ?? null;
+
+ $attributes = $tracker->getMySqliAttributes($mysqli);
+
+ if ($transactionName) {
+ $attributes['db.transaction.name'] = $transactionName;
+ }
+
+ if ($retVal === false || $exception) {
+ //TODO use constant from comment after sem-conv update
+ $attributes[/*TraceAttributes::DB_RESPONSE_STATUS_CODE*/ 'db.response.status_code'] = $mysqli->errno;
+ } else {
+ $tracker->trackMySqliTransaction($mysqli, Span::getCurrent()->getContext());
+ }
+
+ $errorStatus = ($retVal === false && !$exception) ? $mysqli->error : null;
+ self::endSpan($attributes, $exception, $errorStatus);
+ }
+
+ /** @param non-empty-string $spanName */
+ private static function transactionPreHook(string $spanName, CachedInstrumentation $instrumentation, MySqliTracker $tracker, $obj, array $params, ?string $class, ?string $function, ?string $filename, ?int $lineno): void
+ {
+ $span = self::startSpan($spanName, $instrumentation, $class, $function, $filename, $lineno, []);
+ $mysqli = $obj ? $obj : $params[0];
+ self::addTransactionLink($tracker, $span, $mysqli);
+ }
+
+ private static function transactionPostHook(CachedInstrumentation $instrumentation, MySqliTracker $tracker, $obj, array $params, mixed $retVal, ?\Throwable $exception)
+ {
+ $mysqli = $obj ? $obj : $params[0];
+ $transactionName = $params[$obj ? 1 : 2] ?? null;
+
+ $attributes = $tracker->getMySqliAttributes($mysqli);
+
+ if ($transactionName) {
+ $attributes['db.transaction.name'] = $transactionName;
+ }
+
+ if ($retVal === false || $exception) {
+ //TODO use constant from comment after sem-conv update
+ $attributes[/*TraceAttributes::DB_RESPONSE_STATUS_CODE*/ 'db.response.status_code'] = $mysqli->errno;
+ }
+
+ $tracker->untrackMySqliTransaction($mysqli);
+
+ $errorStatus = ($retVal === false && !$exception) ? $mysqli->error : null;
+ self::endSpan($attributes, $exception, $errorStatus);
+ }
+
+ private static function stmtInitPostHook(CachedInstrumentation $instrumentation, MySqliTracker $tracker, $mySqliObj, array $params, mixed $retVal, ?\Throwable $exception)
+ {
+ if ($retVal !== false) {
+ $tracker->trackMySqliFromStatement($mySqliObj ? $mySqliObj : $params[0], $retVal);
+ }
+ }
+
+ private static function stmtPreparePostHook(CachedInstrumentation $instrumentation, MySqliTracker $tracker, $obj, array $params, mixed $retVal, ?\Throwable $exception)
+ {
+ // There is no need to create a span for prepare. It is a partial operation that is not executed on the database, so we do not need to measure its execution time.
+ if ($retVal != true) {
+ self::logDebug('mysqli::prepare failed', ['exception' => $exception, 'obj' => $obj, 'retVal' => $retVal, 'params' => $params]);
+
+ return;
+ }
+
+ $query = $obj ? $params[0] : $params[1];
+ $tracker->addStatementAttribute($obj ? $obj : $params[0], TraceAttributes::DB_STATEMENT, mb_convert_encoding($query, 'UTF-8'));
+ $tracker->addStatementAttribute($obj ? $obj : $params[0], TraceAttributes::DB_OPERATION_NAME, self::extractQueryCommand($query));
+ }
+
+ private static function stmtConstructPostHook(CachedInstrumentation $instrumentation, MySqliTracker $tracker, $stmt, array $params, mixed $retVal, ?\Throwable $exception)
+ {
+
+ if ($exception) {
+ self::logDebug('stmt::__construct failed', ['exception' => $exception, 'stmt' => $stmt, 'retVal' => $retVal, 'params' => $params]);
+
+ return;
+ }
+
+ $tracker->trackMySqliFromStatement($params[0], $stmt);
+
+ if ($params[1] ?? null) {
+ $tracker->addStatementAttribute($stmt, TraceAttributes::DB_STATEMENT, mb_convert_encoding($params[1], 'UTF-8'));
+ $tracker->addStatementAttribute($stmt, TraceAttributes::DB_OPERATION_NAME, self::extractQueryCommand($params[1]));
+ }
+ }
+
+ /** @param non-empty-string $spanName */
+ private static function stmtExecutePreHook(string $spanName, CachedInstrumentation $instrumentation, MySqliTracker $tracker, $obj, array $params, ?string $class, ?string $function, ?string $filename, ?int $lineno): void
+ {
+ $span = self::startSpan($spanName, $instrumentation, $class, $function, $filename, $lineno, []);
+ self::addTransactionLink($tracker, $span, $obj ? $obj : $params[0]);
+ }
+
+ private static function stmtExecutePostHook(CachedInstrumentation $instrumentation, MySqliTracker $tracker, $obj, array $params, mixed $retVal, ?\Throwable $exception)
+ {
+ $stmt = $obj ? $obj : $params[0];
+ $attributes = array_merge($tracker->getMySqliAttributesFromStatement($stmt), $tracker->getStatementAttributes($stmt));
+
+ if ($retVal === false || $exception) {
+ //TODO use constant from comment after sem-conv update
+ $attributes[/*TraceAttributes::DB_RESPONSE_STATUS_CODE*/ 'db.response.status_code'] = $stmt->errno;
+ }
+
+ $errorStatus = ($retVal === false && !$exception) ? $stmt->error : null;
+
+ $tracker->trackStatementSpan($stmt, Span::getCurrent()->getContext());
+
+ self::endSpan($attributes, $exception, $errorStatus);
+
+ }
+
+ /** @param non-empty-string $spanName */
+ private static function stmtNextResultPreHook(string $spanName, CachedInstrumentation $instrumentation, MySqliTracker $tracker, $obj, array $params, ?string $class, ?string $function, ?string $filename, ?int $lineno): void
+ {
+ $span = self::startSpan($spanName, $instrumentation, $class, $function, $filename, $lineno, []);
+
+ $stmt = $obj ? $obj : $params[0];
+ if ($spanContext = $tracker->getStatementSpan($stmt)) {
+ $span->addLink($spanContext);
+ }
+ self::addTransactionLink($tracker, $span, $stmt);
+ }
+
+ private static function stmtNextResultPostHook(CachedInstrumentation $instrumentation, MySqliTracker $tracker, $obj, array $params, mixed $retVal, ?\Throwable $exception)
+ {
+ $stmt = $obj ? $obj : $params[0];
+ $attributes = array_merge($tracker->getMySqliAttributesFromStatement($stmt), $tracker->getStatementAttributes($stmt));
+
+ if ($retVal === false && $stmt->errno == 0 && !$exception) {
+ // it was just a call to check if there is a pending result
+ self::logDebug('stmtNextResultPostHook span dropped', ['exception' => $exception, 'obj' => $obj, 'retVal' => $retVal, 'params' => $params]);
+
+ self::dropSpan();
+
+ return;
+ }
+
+ if ($retVal === false || $exception) {
+ //TODO use constant from comment after sem-conv update
+ $attributes[/*TraceAttributes::DB_RESPONSE_STATUS_CODE*/ 'db.response.status_code'] = $stmt->errno;
+ }
+
+ $errorStatus = ($retVal === false && !$exception) ? $stmt->error : null;
+
+ self::endSpan($attributes, $exception, $errorStatus);
+ }
+
+ /** @param non-empty-string $spanName */
+ private static function startSpan(string $spanName, CachedInstrumentation $instrumentation, ?string $class, ?string $function, ?string $filename, ?int $lineno, iterable $attributes) : SpanInterface
+ {
+ $parent = Context::getCurrent();
+ $builder = $instrumentation->tracer()
+ ->spanBuilder($spanName)
+ ->setParent($parent)
+ ->setSpanKind(SpanKind::KIND_CLIENT)
+ ->setAttribute(TraceAttributes::CODE_FUNCTION, $function)
+ ->setAttribute(TraceAttributes::CODE_NAMESPACE, $class)
+ ->setAttribute(TraceAttributes::CODE_FILEPATH, $filename)
+ ->setAttribute(TraceAttributes::CODE_LINENO, $lineno)
+ ->setAttributes($attributes);
+
+ $span = $builder->startSpan();
+ $context = $span->storeInContext($parent);
+
+ Context::storage()->attach($context);
+
+ return $span;
+ }
+
+ private static function endSpan(iterable $attributes, ?\Throwable $exception, ?string $errorStatus)
+ {
+ $scope = Context::storage()->scope();
+ if (!$scope) {
+ return;
+ }
+ $scope->detach();
+ $span = Span::fromContext($scope->context());
+
+ $span->setAttributes($attributes);
+
+ if ($errorStatus !== null) {
+ $span->setAttribute(TraceAttributes::EXCEPTION_MESSAGE, $errorStatus);
+ $span->setStatus(StatusCode::STATUS_ERROR, $errorStatus);
+ }
+
+ if ($exception) {
+ $span->recordException($exception, [TraceAttributes::EXCEPTION_ESCAPED => true]);
+ $span->setAttribute(TraceAttributes::EXCEPTION_TYPE, $exception::class);
+ $span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage());
+ }
+
+ $span->end();
+ }
+
+ private static function dropSpan()
+ {
+ $scope = Context::storage()->scope();
+ if (!$scope) {
+ return;
+ }
+ $scope->detach();
+ }
+
+ private static function addTransactionLink(MySqliTracker $tracker, SpanInterface $span, $mysqliOrStatement)
+ {
+ $mysqli = $mysqliOrStatement;
+
+ if ($mysqli instanceof mysqli_stmt) {
+ $mysqli = $tracker->getMySqliFromStatement($mysqli);
+ }
+
+ if ($mysqli instanceof mysqli && ($spanContext = $tracker->getMySqliTransaction($mysqli))) {
+ $span->addLink($spanContext);
+ }
+ }
+
+ private static function extractQueryCommand($query) : ?string
+ {
+ $query = preg_replace("/\r\n|\n\r|\r/", "\n", $query);
+ /** @psalm-suppress PossiblyInvalidArgument */
+ if (preg_match('/^\s*(?:--[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*([a-zA-Z_][a-zA-Z0-9_]*)/i', $query, $matches)) {
+ return strtoupper($matches[1]);
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/Instrumentation/MySqli/src/MySqliTracker.php b/src/Instrumentation/MySqli/src/MySqliTracker.php
new file mode 100644
index 00000000..568a766a
--- /dev/null
+++ b/src/Instrumentation/MySqli/src/MySqliTracker.php
@@ -0,0 +1,214 @@
+mySqliToAttributes = new WeakMap();
+ $this->mySqliToMultiQueries = new WeakMap();
+ $this->statementToMySqli = new WeakMap();
+ $this->statementAttributes = new WeakMap();
+ $this->statementSpan = new WeakMap();
+ $this->mySqliSpan = new WeakMap();
+ $this->mySqliTransaction = new WeakMap();
+ }
+
+ public function storeMySqliMultiQuery(mysqli $mysqli, string $query)
+ {
+ $this->mySqliToMultiQueries[$mysqli] = $this->splitQueries($query);
+ }
+
+ public function getNextMySqliMultiQuery(mysqli $mysqli) : ?string
+ {
+ if (!$this->mySqliToMultiQueries->offsetExists($mysqli)) {
+ return null;
+ }
+
+ return array_shift($this->mySqliToMultiQueries[$mysqli]);
+ }
+
+ public function storeMySqliAttributes(mysqli $mysqli, ?string $hostname = null, ?string $username = null, ?string $database = null, ?int $port = null, ?string $socket = null)
+ {
+ $attributes = [];
+ $attributes[TraceAttributes::DB_SYSTEM] = 'mysql';
+ $attributes[TraceAttributes::SERVER_ADDRESS] = $hostname ?? get_cfg_var('mysqli.default_host');
+ $attributes[TraceAttributes::SERVER_PORT] = $port ?? get_cfg_var('mysqli.default_port');
+ $attributes[TraceAttributes::DB_USER] = $username ?? get_cfg_var('mysqli.default_user');
+ if ($database) {
+ $attributes[TraceAttributes::DB_NAMESPACE] = $database;
+ }
+ $this->mySqliToAttributes[$mysqli] = $attributes;
+ }
+
+ public function addMySqliAttribute($mysqli, string $attribute, bool|int|float|string|array|null $value)
+ {
+ if (!$this->mySqliToAttributes->offsetExists($mysqli)) {
+ $this->mySqliToAttributes[$mysqli] = [];
+ }
+ $this->mySqliToAttributes[$mysqli][$attribute] = $value;
+ }
+
+ public function getMySqliAttributes(mysqli $mysqli) : array
+ {
+ return $this->mySqliToAttributes[$mysqli] ?? [];
+ }
+
+ public function trackMySqliFromStatement(mysqli $mysqli, mysqli_stmt $mysqli_stmt)
+ {
+ $this->statementToMySqli[$mysqli_stmt] = WeakReference::create($mysqli);
+ }
+
+ public function getMySqliFromStatement(mysqli_stmt $mysqli_stmt) : ?mysqli
+ {
+ return ($this->statementToMySqli[$mysqli_stmt] ?? null)?->get();
+ ;
+ }
+
+ public function getMySqliAttributesFromStatement(mysqli_stmt $stmt) : array
+ {
+ $mysqli = ($this->statementToMySqli[$stmt] ?? null)?->get();
+ if (!$mysqli) {
+ return [];
+ }
+
+ return $this->getMySqliAttributes($mysqli);
+ }
+
+ public function addStatementAttribute(mysqli_stmt $stmt, string $attribute, bool|int|float|string|array|null $value)
+ {
+ if (!$this->statementAttributes->offsetExists($stmt)) {
+ $this->statementAttributes[$stmt] = [];
+ }
+ $this->statementAttributes[$stmt][$attribute] = $value;
+ }
+
+ public function getStatementAttributes(mysqli_stmt $stmt) : array
+ {
+ if (!$this->statementAttributes->offsetExists($stmt)) {
+ return [];
+ }
+
+ return $this->statementAttributes[$stmt];
+ }
+
+ public function trackStatementSpan(mysqli_stmt $stmt, SpanContextInterface $spanContext)
+ {
+ $this->statementSpan[$stmt] = WeakReference::create($spanContext);
+ }
+
+ public function getStatementSpan(mysqli_stmt $stmt) : ?SpanContextInterface
+ {
+ if (!$this->statementSpan->offsetExists($stmt)) {
+ return null;
+ }
+
+ return $this->statementSpan[$stmt]->get();
+ }
+
+ public function trackMysqliSpan(mysqli $mysqli, SpanContextInterface $spanContext)
+ {
+ $this->mySqliSpan[$mysqli] = WeakReference::create($spanContext);
+ }
+
+ public function getMySqliSpan(mysqli $mysqli) : ?SpanContextInterface
+ {
+ if (!$this->mySqliSpan->offsetExists($mysqli)) {
+ return null;
+ }
+
+ return $this->mySqliSpan[$mysqli]->get();
+ }
+
+ public function trackMySqliTransaction(mysqli $mysqli, SpanContextInterface $spanContext)
+ {
+ $this->mySqliTransaction[$mysqli] = WeakReference::create($spanContext);
+ }
+
+ public function getMySqliTransaction(mysqli $mysqli) : ?SpanContextInterface
+ {
+ if (!$this->mySqliTransaction->offsetExists($mysqli)) {
+ return null;
+ }
+
+ return $this->mySqliTransaction[$mysqli]->get();
+ }
+
+ public function untrackMySqliTransaction(mysqli $mysqli)
+ {
+ if ($this->mySqliTransaction->offsetExists($mysqli)) {
+ unset($this->mySqliTransaction[$mysqli]);
+ }
+ }
+
+ private function splitQueries(string $sql)
+ {
+ // Normalize line endings to \n
+ $sql = preg_replace("/\r\n|\n\r|\r/", "\n", $sql);
+
+ $queries = [];
+ $buffer = '';
+ $blockDepth = 0;
+ $tokens = preg_split('/(;)/', $sql, -1, PREG_SPLIT_DELIM_CAPTURE); // Keep semicolons as separate tokens
+
+ foreach ($tokens as $token) {
+ if ($token === '') {
+ continue;
+ }
+
+ if ($blockDepth === 0) {
+ $token = trim($token);
+ }
+
+ $buffer .= $token;
+
+ // Detect BEGIN with optional label
+ if (preg_match('/(^|\s|[)])\bBEGIN\b/i', $token)) {
+ $blockDepth++;
+ }
+
+ // Detect END with optional label
+ if (preg_match('/\bEND\b(\s+[a-zA-Z0-9_]+)?\s*$/i', $token)) {
+ $blockDepth--;
+ }
+
+ // If we are outside a block and encounter a semicolon, split the query
+ if ($blockDepth === 0 && $token === ';') {
+ $trimmedQuery = trim($buffer);
+ if ($trimmedQuery !== ';') { // Ignore empty queries
+ $queries[] = $trimmedQuery;
+ //substr($trimmedQuery, 0, -1); // Remove the trailing semicolon
+ }
+ $buffer = '';
+ }
+ }
+
+ // Add any remaining buffer as a query
+ if (!empty(trim($buffer))) {
+ $queries[] = trim($buffer);
+ }
+
+ return $queries;
+ }
+
+}
diff --git a/src/Instrumentation/MySqli/tests/Integration/MySqliInstrumentationTest.php b/src/Instrumentation/MySqli/tests/Integration/MySqliInstrumentationTest.php
new file mode 100644
index 00000000..35a8d66b
--- /dev/null
+++ b/src/Instrumentation/MySqli/tests/Integration/MySqliInstrumentationTest.php
@@ -0,0 +1,1424 @@
+ */
+ private ArrayObject $storage;
+
+ private string $mysqlHost;
+
+ private string $user;
+ private string $passwd;
+ private string $database;
+
+ public function setUp(): void
+ {
+ $this->storage = new ArrayObject();
+ $tracerProvider = new TracerProvider(
+ new SimpleSpanProcessor(
+ new InMemoryExporter($this->storage)
+ )
+ );
+
+ $this->scope = Configurator::create()
+ ->withTracerProvider($tracerProvider)
+ ->withPropagator(TraceContextPropagator::getInstance())
+ ->activate();
+
+ $this->mysqlHost = getenv('MYSQL_HOST') ?: '127.0.0.1';
+
+ $this->user = 'otel_user';
+ $this->passwd = 'otel_passwd';
+ $this->database = 'otel_db';
+ }
+
+ public function tearDown(): void
+ {
+ $this->scope->detach();
+ }
+
+ private function assertDatabaseAttributes(int $offset)
+ {
+ $span = $this->storage->offsetGet($offset);
+ $this->assertEquals($this->mysqlHost, $span->getAttributes()->get(TraceAttributes::SERVER_ADDRESS));
+ $this->assertEquals($this->user, $span->getAttributes()->get(TraceAttributes::DB_USER));
+ $this->assertEquals($this->database, $span->getAttributes()->get(TraceAttributes::DB_NAMESPACE));
+ $this->assertEquals('mysql', $span->getAttributes()->get(TraceAttributes::DB_SYSTEM));
+ }
+
+ private function assertDatabaseAttributesForAllSpans(int $offsets)
+ {
+ for ($offset = 0; $offset < $offsets; $offset++) {
+ $this->assertDatabaseAttributes($offset);
+ }
+ }
+
+ private function assertAttributes(int $offset, iterable $attributes)
+ {
+ foreach ($attributes as $attribute => $expected) {
+ $this->assertSame($expected, $this->storage->offsetGet($offset)->getAttributes()->get($attribute));
+ }
+ }
+
+ public function test_mysqli_connect(): void
+ {
+ $mysqli = new mysqli($this->mysqlHost, $this->user, $this->passwd, $this->database);
+ $mysqli->connect($this->mysqlHost, $this->user, $this->passwd, $this->database);
+ mysqli_connect($this->mysqlHost, $this->user, $this->passwd, $this->database);
+
+ $mysqli->real_connect($this->mysqlHost, $this->user, $this->passwd, $this->database);
+ mysqli_real_connect($mysqli, $this->mysqlHost, $this->user, $this->passwd, $this->database);
+
+ $offset = 0;
+ $this->assertSame('mysqli::__construct', $this->storage->offsetGet($offset++)->getName());
+ $this->assertSame('mysqli::connect', $this->storage->offsetGet($offset++)->getName());
+ $this->assertSame('mysqli_connect', $this->storage->offsetGet($offset++)->getName());
+ $this->assertSame('mysqli::real_connect', $this->storage->offsetGet($offset++)->getName());
+ $this->assertSame('mysqli_real_connect', $this->storage->offsetGet($offset++)->getName());
+
+ $this->assertCount($offset, $this->storage);
+
+ $this->assertDatabaseAttributesForAllSpans($offset);
+ }
+
+ public function test_mysqli_query_objective(): void
+ {
+ mysqli_report(MYSQLI_REPORT_ERROR| MYSQLI_REPORT_STRICT);
+
+ $mysqli = new mysqli($this->mysqlHost, $this->user, $this->passwd, $this->database);
+
+ $offset = 0;
+ $this->assertSame('mysqli::__construct', $this->storage->offsetGet($offset)->getName());
+
+ $offset++;
+ $res = $mysqli->query('SELECT * FROM otel_db.users');
+ if ($res instanceof mysqli_result) {
+ while ($res->fetch_object()) {
+ }
+ }
+
+ $this->assertSame('mysqli::query', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT * FROM otel_db.users',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ ]);
+
+ $offset++;
+ if ($mysqli->real_query('SELECT * FROM otel_db.users')) {
+ $mysqli->store_result();
+ }
+ $this->assertSame('mysqli::real_query', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT * FROM otel_db.users',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ ]);
+
+ $offset++;
+
+ try {
+ $mysqli->query('SELECT * FROM unknown_db.users');
+ } catch (Throwable) {
+ }
+
+ $this->assertSame('mysqli::query', $this->storage->offsetGet($offset)->getName());
+ $this->assertStringContainsString('Unknown database', $this->storage->offsetGet($offset)->getStatus()->getDescription());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT * FROM unknown_db.users',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ TraceAttributes::EXCEPTION_TYPE => mysqli_sql_exception::class,
+ ]);
+
+ $offset++;
+
+ try {
+ $mysqli->real_query('SELECT * FROM unknown_db.users');
+ } catch (Throwable) {
+ }
+
+ $this->assertSame('mysqli::real_query', $this->storage->offsetGet($offset)->getName());
+ $this->assertSame(StatusCode::STATUS_ERROR, $this->storage->offsetGet($offset)->getStatus()->getCode());
+ $this->assertStringContainsString('Unknown database', $this->storage->offsetGet($offset)->getStatus()->getDescription());
+
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT * FROM unknown_db.users',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ TraceAttributes::EXCEPTION_TYPE => mysqli_sql_exception::class,
+ ]);
+
+ // disabling exceptions - test error capturing
+ mysqli_report(MYSQLI_REPORT_ERROR);
+ $offset++;
+
+ try {
+
+ $mysqli->query('SELECT * FROM unknown_db.users');
+ } catch (Throwable) {
+ }
+
+ $this->assertSame('mysqli::query', $this->storage->offsetGet($offset)->getName());
+ $this->assertStringContainsString('Unknown database', $this->storage->offsetGet($offset)->getStatus()->getDescription());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT * FROM unknown_db.users',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ TraceAttributes::EXCEPTION_TYPE => \PHPUnit\Framework\Error\Warning::class,
+ ]);
+
+ $offset++;
+
+ try {
+ $mysqli->real_query('SELECT * FROM unknown_db.users');
+ } catch (Throwable) {
+ }
+
+ $this->assertSame('mysqli::real_query', $this->storage->offsetGet($offset)->getName());
+ $this->assertStringContainsString('Unknown database', $this->storage->offsetGet($offset)->getStatus()->getDescription());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT * FROM unknown_db.users',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ TraceAttributes::EXCEPTION_TYPE => \PHPUnit\Framework\Error\Warning::class,
+ ]);
+
+ $offset++;
+ $this->assertCount($offset, $this->storage);
+ $this->assertDatabaseAttributesForAllSpans($offset);
+ }
+
+ public function test_mysqli_query_procedural(): void
+ {
+ mysqli_report(MYSQLI_REPORT_ERROR| MYSQLI_REPORT_STRICT);
+
+ $mysqli = mysqli_connect($this->mysqlHost, $this->user, $this->passwd, $this->database);
+
+ $this->assertTrue($mysqli instanceof mysqli);
+
+ $offset = 0;
+ $this->assertSame('mysqli_connect', $this->storage->offsetGet($offset)->getName());
+
+ $offset++;
+ $res = mysqli_query($mysqli, 'SELECT * FROM otel_db.users');
+ if ($res instanceof mysqli_result) {
+ while ($res->fetch_object()) {
+ }
+ }
+
+ $this->assertSame('mysqli_query', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT * FROM otel_db.users',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ ]);
+
+ $offset++;
+ if (mysqli_real_query($mysqli, 'SELECT * FROM otel_db.users')) {
+ $mysqli->store_result();
+ }
+ $this->assertSame('mysqli_real_query', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT * FROM otel_db.users',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ ]);
+
+ $offset++;
+
+ try {
+ mysqli_query($mysqli, 'SELECT * FROM unknown_db.users');
+ } catch (Throwable) {
+ }
+
+ $this->assertSame('mysqli_query', $this->storage->offsetGet($offset)->getName());
+ $this->assertStringContainsString('Unknown database', $this->storage->offsetGet($offset)->getStatus()->getDescription());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT * FROM unknown_db.users',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ TraceAttributes::EXCEPTION_TYPE => mysqli_sql_exception::class,
+ ]);
+
+ $offset++;
+
+ try {
+ mysqli_real_query($mysqli, 'SELECT * FROM unknown_db.users');
+ } catch (Throwable) {
+ }
+
+ $this->assertSame('mysqli_real_query', $this->storage->offsetGet($offset)->getName());
+ $this->assertSame(StatusCode::STATUS_ERROR, $this->storage->offsetGet($offset)->getStatus()->getCode());
+ $this->assertStringContainsString('Unknown database', $this->storage->offsetGet($offset)->getStatus()->getDescription());
+
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT * FROM unknown_db.users',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ TraceAttributes::EXCEPTION_TYPE => mysqli_sql_exception::class,
+ ]);
+
+ // disabling exceptions - test error capturing
+ mysqli_report(MYSQLI_REPORT_ERROR);
+
+ $offset++;
+
+ try {
+ mysqli_query($mysqli, 'SELECT * FROM unknown_db.users');
+ } catch (Throwable) {
+ }
+
+ $this->assertSame('mysqli_query', $this->storage->offsetGet($offset)->getName());
+ $this->assertStringContainsString('Unknown database', $this->storage->offsetGet($offset)->getStatus()->getDescription());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT * FROM unknown_db.users',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ TraceAttributes::EXCEPTION_TYPE => \PHPUnit\Framework\Error\Warning::class,
+ ]);
+
+ $offset++;
+
+ try {
+ mysqli_real_query($mysqli, 'SELECT * FROM unknown_db.users');
+ } catch (Throwable) {
+ }
+
+ $this->assertSame('mysqli_real_query', $this->storage->offsetGet($offset)->getName());
+ $this->assertStringContainsString('Unknown database', $this->storage->offsetGet($offset)->getStatus()->getDescription());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT * FROM unknown_db.users',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ TraceAttributes::EXCEPTION_TYPE => \PHPUnit\Framework\Error\Warning::class,
+ ]);
+
+ $offset++;
+ $this->assertCount($offset, $this->storage);
+ $this->assertDatabaseAttributesForAllSpans($offset);
+
+ }
+
+ public function test_mysqli_execute_query_objective(): void
+ {
+ mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
+
+ $mysqli = new mysqli($this->mysqlHost, $this->user, $this->passwd, $this->database);
+
+ $offset = 0;
+ $this->assertSame('mysqli::__construct', $this->storage->offsetGet($offset)->getName());
+
+ $offset++;
+ $result = $mysqli->execute_query('SELECT * FROM otel_db.users');
+ if ($result instanceof mysqli_result) {
+ $this->assertCount(3, $result->fetch_all(), 'Result should contain 3 elements');
+ }
+
+ $this->assertSame('mysqli::execute_query', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT * FROM otel_db.users',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ ]);
+
+ $offset++;
+
+ try {
+ $result = $mysqli->execute_query('SELECT * FROM unknown_db.users');
+ } catch (mysqli_sql_exception) {
+ }
+
+ $this->assertSame('mysqli::execute_query', $this->storage->offsetGet($offset)->getName());
+ $this->assertStringContainsString('Unknown database', $this->storage->offsetGet($offset)->getStatus()->getDescription());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT * FROM unknown_db.users',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ TraceAttributes::EXCEPTION_TYPE => mysqli_sql_exception::class,
+ ]);
+
+ mysqli_report(MYSQLI_REPORT_ERROR);
+
+ $offset++;
+
+ try {
+ $result = $mysqli->execute_query('SELECT * FROM unknown_db.users');
+ } catch (Throwable) {
+ }
+
+ $this->assertSame('mysqli::execute_query', $this->storage->offsetGet($offset)->getName());
+ $this->assertStringContainsString('Unknown database', $this->storage->offsetGet($offset)->getStatus()->getDescription());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT * FROM unknown_db.users',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ TraceAttributes::EXCEPTION_TYPE => \PHPUnit\Framework\Error\Warning::class,
+ ]);
+
+ $offset++;
+ $this->assertCount($offset, $this->storage);
+ $this->assertDatabaseAttributesForAllSpans($offset);
+ }
+
+ public function test_mysqli_execute_query_procedural(): void
+ {
+ mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
+
+ $mysqli = mysqli_connect($this->mysqlHost, $this->user, $this->passwd, $this->database);
+
+ $offset = 0;
+ $this->assertSame('mysqli_connect', $this->storage->offsetGet($offset)->getName());
+
+ $offset++;
+ $result = mysqli_execute_query($mysqli, 'SELECT * FROM otel_db.users');
+ if ($result instanceof mysqli_result) {
+ $this->assertCount(3, $result->fetch_all(), 'Result should contain 3 elements');
+ }
+
+ $this->assertSame('mysqli_execute_query', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT * FROM otel_db.users',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ ]);
+
+ $offset++;
+
+ try {
+ $result = mysqli_execute_query($mysqli, 'SELECT * FROM unknown_db.users');
+ } catch (mysqli_sql_exception) {
+ }
+
+ $this->assertSame('mysqli_execute_query', $this->storage->offsetGet($offset)->getName());
+ $this->assertStringContainsString('Unknown database', $this->storage->offsetGet($offset)->getStatus()->getDescription());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT * FROM unknown_db.users',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ TraceAttributes::EXCEPTION_TYPE => mysqli_sql_exception::class,
+ ]);
+
+ mysqli_report(MYSQLI_REPORT_ERROR);
+
+ $offset++;
+
+ try {
+ $result = mysqli_execute_query($mysqli, 'SELECT * FROM unknown_db.users');
+ } catch (Throwable) {
+ }
+
+ $this->assertSame('mysqli_execute_query', $this->storage->offsetGet($offset)->getName());
+ $this->assertStringContainsString('Unknown database', $this->storage->offsetGet($offset)->getStatus()->getDescription());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT * FROM unknown_db.users',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ TraceAttributes::EXCEPTION_TYPE => \PHPUnit\Framework\Error\Warning::class,
+ ]);
+
+ $offset++;
+ $this->assertCount($offset, $this->storage);
+ $this->assertDatabaseAttributesForAllSpans($offset);
+ }
+
+ public function test_mysqli_multi_query_objective(): void
+ {
+ mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
+
+ $mysqli = new mysqli($this->mysqlHost, $this->user, $this->passwd, $this->database);
+
+ $offset = 0;
+ $this->assertSame('mysqli::__construct', $this->storage->offsetGet($offset)->getName());
+
+ $query = 'SELECT CURRENT_USER();';
+ $query .= 'SELECT email FROM users ORDER BY id;';
+ $query .= 'SELECT name FROM products ORDER BY stock;';
+ $query .= 'SELECT test FROM unknown ORDER BY nothing;';
+
+ $result = $mysqli->multi_query($query);
+ do {
+ try {
+ if ($result = $mysqli->store_result()) {
+ $result->free_result();
+ }
+
+ if (!$mysqli->next_result()) {
+ break;
+ }
+ } catch (Throwable) {
+ break;
+ }
+ } while (true);
+
+ $offset++;
+ $this->assertSame('mysqli::multi_query', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT CURRENT_USER();',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ ]);
+
+ $offset++;
+ $this->assertSame('mysqli::next_result', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT email FROM users ORDER BY id;',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ ]);
+
+ $offset++;
+ $this->assertSame('mysqli::next_result', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT name FROM products ORDER BY stock;',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ ]);
+
+ $offset++;
+ $this->assertSame('mysqli::next_result', $this->storage->offsetGet($offset)->getName());
+ $this->assertStringContainsString("Table 'otel_db.unknown' doesn't exist", $this->storage->offsetGet($offset)->getStatus()->getDescription());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT test FROM unknown ORDER BY nothing;',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ TraceAttributes::EXCEPTION_TYPE => mysqli_sql_exception::class,
+ ]);
+
+ mysqli_report(MYSQLI_REPORT_ERROR);
+
+ $result = $mysqli->multi_query($query);
+ do {
+ try {
+ if ($result = $mysqli->store_result()) {
+ $result->free_result();
+ }
+
+ if (!$mysqli->next_result()) {
+ break;
+ }
+ } catch (Throwable) {
+ break;
+ }
+ } while (true);
+
+ $offset++;
+ $this->assertSame('mysqli::multi_query', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT CURRENT_USER();',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ ]);
+
+ $offset++;
+ $this->assertSame('mysqli::next_result', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT email FROM users ORDER BY id;',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ ]);
+
+ $offset++;
+ $this->assertSame('mysqli::next_result', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT name FROM products ORDER BY stock;',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ ]);
+
+ $offset++;
+ $this->assertSame('mysqli::next_result', $this->storage->offsetGet($offset)->getName());
+ $this->assertStringContainsString("Table 'otel_db.unknown' doesn't exist", $this->storage->offsetGet($offset)->getStatus()->getDescription());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT test FROM unknown ORDER BY nothing;',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ TraceAttributes::EXCEPTION_TYPE => \PHPUnit\Framework\Error\Warning::class,
+ ]);
+
+ $offset++;
+ $this->assertCount($offset, $this->storage);
+ $this->assertDatabaseAttributesForAllSpans($offset);
+ }
+
+ public function test_mysqli_multi_query_procedural(): void
+ {
+ mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
+
+ $mysqli = mysqli_connect($this->mysqlHost, $this->user, $this->passwd, $this->database);
+
+ $offset = 0;
+ $this->assertSame('mysqli_connect', $this->storage->offsetGet($offset)->getName());
+
+ $query = 'SELECT CURRENT_USER();';
+ $query .= 'SELECT email FROM users ORDER BY id;';
+ $query .= 'SELECT name FROM products ORDER BY stock;';
+ $query .= 'SELECT test FROM unknown ORDER BY nothing;';
+
+ $result = mysqli_multi_query($mysqli, $query);
+ do {
+ try {
+ if ($result = mysqli_store_result($mysqli)) {
+ mysqli_free_result($result);
+ }
+
+ if (!mysqli_next_result($mysqli)) {
+ break;
+ }
+ } catch (Throwable) {
+ break;
+ }
+ } while (true);
+
+ $offset++;
+ $this->assertSame('mysqli_multi_query', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT CURRENT_USER();',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ ]);
+
+ $offset++;
+ $this->assertSame('mysqli_next_result', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT email FROM users ORDER BY id;',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ ]);
+
+ $offset++;
+ $this->assertSame('mysqli_next_result', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT name FROM products ORDER BY stock;',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ ]);
+
+ $offset++;
+ $this->assertSame('mysqli_next_result', $this->storage->offsetGet($offset)->getName());
+ $this->assertStringContainsString("Table 'otel_db.unknown' doesn't exist", $this->storage->offsetGet($offset)->getStatus()->getDescription());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT test FROM unknown ORDER BY nothing;',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ TraceAttributes::EXCEPTION_TYPE => mysqli_sql_exception::class,
+ ]);
+
+ mysqli_report(MYSQLI_REPORT_ERROR);
+
+ $result = mysqli_multi_query($mysqli, $query);
+ do {
+ try {
+ if ($result = mysqli_store_result($mysqli)) {
+ mysqli_free_result($result);
+ }
+
+ if (!mysqli_next_result($mysqli)) {
+ break;
+ }
+ } catch (Throwable) {
+ break;
+ }
+ } while (true);
+
+ $offset++;
+ $this->assertSame('mysqli_multi_query', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT CURRENT_USER();',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ ]);
+
+ $offset++;
+ $this->assertSame('mysqli_next_result', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT email FROM users ORDER BY id;',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ ]);
+
+ $offset++;
+ $this->assertSame('mysqli_next_result', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT name FROM products ORDER BY stock;',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ ]);
+
+ $offset++;
+ $this->assertSame('mysqli_next_result', $this->storage->offsetGet($offset)->getName());
+ $this->assertStringContainsString("Table 'otel_db.unknown' doesn't exist", $this->storage->offsetGet($offset)->getStatus()->getDescription());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT test FROM unknown ORDER BY nothing;',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ TraceAttributes::EXCEPTION_TYPE => \PHPUnit\Framework\Error\Warning::class,
+ ]);
+
+ $offset++;
+ $this->assertCount($offset, $this->storage);
+ $this->assertDatabaseAttributesForAllSpans($offset);
+ }
+
+ public function test_mysqli_prepare_objective(): void
+ {
+ mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
+
+ $mysqli = new mysqli($this->mysqlHost, $this->user, $this->passwd, $this->database);
+
+ $offset = 0;
+ $this->assertSame('mysqli::__construct', $this->storage->offsetGet($offset)->getName());
+
+ try {
+ $stmt = $mysqli->prepare('SELECT * FROM otel_db.users');
+
+ $offset++;
+ $this->assertSame('mysqli::prepare', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT * FROM otel_db.users',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ ]);
+
+ $stmt->execute();
+ $offset++;
+
+ $this->assertSame('mysqli_stmt::execute', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT * FROM otel_db.users',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ ]);
+
+ $stmt->fetch();
+ $stmt->close();
+
+ } catch (mysqli_sql_exception $exception) {
+ $this->fail('Unexpected exception was thrown: ' . $exception->getMessage());
+ }
+
+ try {
+ $stmt = $mysqli->prepare('SELECT * FROM unknown_db.users');
+
+ $this->fail('Should never reach this point');
+ } catch (mysqli_sql_exception $exception) {
+ $offset++;
+
+ $this->assertSame('mysqli::prepare', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT * FROM unknown_db.users',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ TraceAttributes::EXCEPTION_TYPE => mysqli_sql_exception::class,
+ ]);
+
+ }
+
+ $offset++;
+ $this->assertCount($offset, $this->storage);
+ $this->assertDatabaseAttributesForAllSpans($offset);
+ }
+
+ public function test_mysqli_prepare_procedural(): void
+ {
+ mysqli_report(MYSQLI_REPORT_ERROR);
+
+ $mysqli = new mysqli($this->mysqlHost, $this->user, $this->passwd, $this->database);
+
+ $offset = 0;
+ $this->assertSame('mysqli::__construct', $this->storage->offsetGet($offset)->getName());
+
+ try {
+ $stmt = mysqli_prepare($mysqli, 'SELECT * FROM otel_db.users');
+
+ $offset++;
+ $this->assertSame('mysqli_prepare', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT * FROM otel_db.users',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ ]);
+
+ mysqli_stmt_execute($stmt);
+ $offset++;
+
+ $this->assertSame('mysqli_stmt_execute', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT * FROM otel_db.users',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ ]);
+
+ mysqli_stmt_fetch($stmt);
+ mysqli_stmt_close($stmt);
+ } catch (mysqli_sql_exception $exception) {
+ $this->fail('Unexpected exception was thrown: ' . $exception->getMessage());
+ }
+
+ try {
+ $stmt = mysqli_prepare($mysqli, 'SELECT * FROM unknown_db.users');
+
+ $this->fail('Should never reach this point');
+ } catch (\Throwable $exception) {
+ $offset++;
+
+ $this->assertSame('mysqli_prepare', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT * FROM unknown_db.users',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ TraceAttributes::EXCEPTION_TYPE => \PHPUnit\Framework\Error\Warning::class,
+ ]);
+ }
+
+ $offset++;
+ $this->assertCount($offset, $this->storage);
+ $this->assertDatabaseAttributesForAllSpans($offset);
+ }
+
+ public function test_mysqli_transaction_rollback_objective(): void
+ {
+ mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
+
+ $mysqli = new mysqli($this->mysqlHost, $this->user, $this->passwd, $this->database);
+
+ $offset = 0;
+ $this->assertSame('mysqli::__construct', $this->storage->offsetGet($offset)->getName());
+
+ $mysqli->query('DROP TABLE IF EXISTS language;');
+ $offset++;
+
+ $mysqli->query('CREATE TABLE IF NOT EXISTS language ( Code text NOT NULL, Speakers int(11) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;');
+ $offset++;
+
+ $mysqli->begin_transaction(name: 'supertransaction');
+ $offset++;
+ $this->assertSame('mysqli::begin_transaction', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ 'db.transaction.name' => 'supertransaction',
+ ]);
+
+ try {
+ // Insert some values
+ $mysqli->query("INSERT INTO language(Code, Speakers) VALUES ('DE', 42000123)");
+
+ $offset++;
+ $this->assertSame('mysqli::query', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => "INSERT INTO language(Code, Speakers) VALUES ('DE', 42000123)",
+ TraceAttributes::DB_OPERATION_NAME => 'INSERT',
+ ]);
+
+ // Try to insert invalid values
+ $language_code = 'FR';
+ $native_speakers = 'Unknown';
+ $stmt = $mysqli->prepare('INSERT INTO language(Code, Speakers) VALUES (?,?)');
+ $offset++;
+ $this->assertSame('mysqli::prepare', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'INSERT INTO language(Code, Speakers) VALUES (?,?)',
+ TraceAttributes::DB_OPERATION_NAME => 'INSERT',
+ ]);
+
+ $stmt->bind_param('ss', $language_code, $native_speakers);
+ $stmt->execute(); // THROWS HERE
+
+ $this->fail('Should never reach this point');
+ } catch (mysqli_sql_exception $exception) {
+ $offset++;
+ $this->assertSame('mysqli_stmt::execute', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'INSERT INTO language(Code, Speakers) VALUES (?,?)',
+ TraceAttributes::DB_OPERATION_NAME => 'INSERT',
+ ]);
+
+ $mysqli->rollback(name: 'supertransaction');
+ $offset++;
+ $this->assertSame('mysqli::rollback', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ 'db.transaction.name' => 'supertransaction',
+ ]);
+
+ }
+
+ $offset++;
+ $this->assertCount($offset, $this->storage);
+ $this->assertDatabaseAttributesForAllSpans($offset);
+ }
+
+ public function test_mysqli_transaction_rollback_procedural(): void
+ {
+ mysqli_report(MYSQLI_REPORT_ERROR);
+
+ $mysqli = mysqli_connect($this->mysqlHost, $this->user, $this->passwd, $this->database);
+
+ $offset = 0;
+ $this->assertSame('mysqli_connect', $this->storage->offsetGet($offset)->getName());
+
+ mysqli_query($mysqli, 'DROP TABLE IF EXISTS language;');
+ $offset++;
+
+ mysqli_query($mysqli, 'CREATE TABLE IF NOT EXISTS language ( Code text NOT NULL, Speakers int(11) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;');
+ $offset++;
+
+ mysqli_begin_transaction($mysqli, name: 'supertransaction');
+ $offset++;
+ $this->assertSame('mysqli_begin_transaction', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ 'db.transaction.name' => 'supertransaction',
+ ]);
+
+ try {
+ // Insert some values
+ mysqli_query($mysqli, "INSERT INTO language(Code, Speakers) VALUES ('DE', 76000001)");
+
+ $offset++;
+ $this->assertSame('mysqli_query', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => "INSERT INTO language(Code, Speakers) VALUES ('DE', 76000001)",
+ TraceAttributes::DB_OPERATION_NAME => 'INSERT',
+ ]);
+
+ // Try to insert invalid values
+ $language_code = 'FR';
+ $native_speakers = 'Unknown';
+ $stmt = mysqli_prepare($mysqli, 'INSERT INTO language(Code, Speakers) VALUES (?,?)');
+ $offset++;
+ $this->assertSame('mysqli_prepare', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'INSERT INTO language(Code, Speakers) VALUES (?,?)',
+ TraceAttributes::DB_OPERATION_NAME => 'INSERT',
+ ]);
+
+ mysqli_stmt_bind_param($stmt, 'ss', $language_code, $native_speakers);
+
+ try {
+ mysqli_stmt_execute($stmt);
+ } catch (\PHPUnit\Framework\Error\Warning $e) {
+ $offset++;
+ $this->assertSame('mysqli_stmt_execute', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'INSERT INTO language(Code, Speakers) VALUES (?,?)',
+ TraceAttributes::DB_OPERATION_NAME => 'INSERT',
+ ]);
+
+ mysqli_rollback($mysqli, name: 'supertransaction');
+ $offset++;
+ $this->assertSame('mysqli_rollback', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ 'db.transaction.name' => 'supertransaction',
+ ]);
+ }
+ } catch (mysqli_sql_exception $exception) {
+ $this->fail('Should never reach this point');
+ }
+
+ $offset++;
+ $this->assertCount($offset, $this->storage);
+ $this->assertDatabaseAttributesForAllSpans($offset);
+ }
+
+ public function test_mysqli_transaction_commit_objective(): void
+ {
+ mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
+
+ $mysqli = new mysqli($this->mysqlHost, $this->user, $this->passwd, $this->database);
+
+ $offset = 0;
+ $this->assertSame('mysqli::__construct', $this->storage->offsetGet($offset)->getName());
+
+ $mysqli->query('DROP TABLE IF EXISTS language;');
+ $offset++;
+
+ $mysqli->query('CREATE TABLE IF NOT EXISTS language ( Code text NOT NULL, Speakers int(11) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;');
+ $offset++;
+
+ $mysqli->begin_transaction(name: 'supertransaction');
+ $offset++;
+ $this->assertSame('mysqli::begin_transaction', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ 'db.transaction.name' => 'supertransaction',
+ ]);
+
+ try {
+ // Insert some values
+ $mysqli->query("INSERT INTO language(Code, Speakers) VALUES ('DE', 76000001)");
+
+ $offset++;
+ $this->assertSame('mysqli::query', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => "INSERT INTO language(Code, Speakers) VALUES ('DE', 76000001)",
+ TraceAttributes::DB_OPERATION_NAME => 'INSERT',
+ ]);
+
+ // Try to insert invalid values
+ $language_code = 'FR';
+ $native_speakers = 66000002;
+ $stmt = $mysqli->prepare('INSERT INTO language(Code, Speakers) VALUES (?,?)');
+
+ $offset++;
+ $this->assertSame('mysqli::prepare', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'INSERT INTO language(Code, Speakers) VALUES (?,?)',
+ TraceAttributes::DB_OPERATION_NAME => 'INSERT',
+ ]);
+
+ $stmt->bind_param('ss', $language_code, $native_speakers);
+ $stmt->execute();
+
+ $offset++;
+ $this->assertSame('mysqli_stmt::execute', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'INSERT INTO language(Code, Speakers) VALUES (?,?)',
+ TraceAttributes::DB_OPERATION_NAME => 'INSERT',
+ ]);
+
+ $mysqli->commit(name: 'supertransaction');
+
+ $offset++;
+ $this->assertSame('mysqli::commit', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ 'db.transaction.name' => 'supertransaction',
+ ]);
+ } catch (mysqli_sql_exception $exception) {
+ $this->fail('Unexpected exception was thrown: ' . $exception->getMessage());
+ }
+
+ $offset++;
+ $this->assertCount($offset, $this->storage);
+ $this->assertDatabaseAttributesForAllSpans($offset);
+ }
+
+ public function test_mysqli_transaction_commit_procedural(): void
+ {
+ mysqli_report(MYSQLI_REPORT_ERROR);
+
+ $mysqli = mysqli_connect($this->mysqlHost, $this->user, $this->passwd, $this->database);
+
+ $offset = 0;
+ $this->assertSame('mysqli_connect', $this->storage->offsetGet($offset)->getName());
+
+ mysqli_query($mysqli, 'DROP TABLE IF EXISTS language;');
+ $offset++;
+
+ mysqli_query($mysqli, 'CREATE TABLE IF NOT EXISTS language ( Code text NOT NULL, Speakers int(11) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;');
+ $offset++;
+
+ mysqli_begin_transaction($mysqli, name: 'supertransaction');
+ $offset++;
+ $this->assertSame('mysqli_begin_transaction', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ 'db.transaction.name' => 'supertransaction',
+ ]);
+
+ try {
+ // Insert some values
+ mysqli_query($mysqli, "INSERT INTO language(Code, Speakers) VALUES ('DE', 76000001)");
+
+ $offset++;
+ $this->assertSame('mysqli_query', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => "INSERT INTO language(Code, Speakers) VALUES ('DE', 76000001)",
+ TraceAttributes::DB_OPERATION_NAME => 'INSERT',
+ ]);
+
+ // Try to insert invalid values
+ $language_code = 'FR';
+ $native_speakers = 66000002;
+ $stmt = mysqli_prepare($mysqli, 'INSERT INTO language(Code, Speakers) VALUES (?,?)');
+ $offset++;
+ $this->assertSame('mysqli_prepare', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'INSERT INTO language(Code, Speakers) VALUES (?,?)',
+ TraceAttributes::DB_OPERATION_NAME => 'INSERT',
+ ]);
+
+ mysqli_stmt_bind_param($stmt, 'ss', $language_code, $native_speakers);
+ mysqli_stmt_execute($stmt);
+
+ $offset++;
+ $this->assertSame('mysqli_stmt_execute', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'INSERT INTO language(Code, Speakers) VALUES (?,?)',
+ TraceAttributes::DB_OPERATION_NAME => 'INSERT',
+ ]);
+
+ mysqli_commit($mysqli, name: 'supertransaction');
+
+ $offset++;
+ $this->assertSame('mysqli_commit', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ 'db.transaction.name' => 'supertransaction',
+ ]);
+ } catch (mysqli_sql_exception $exception) {
+ $this->fail('Unexpected exception was thrown: ' . $exception->getMessage());
+ }
+
+ $offset++;
+ $this->assertCount($offset, $this->storage);
+ $this->assertDatabaseAttributesForAllSpans($offset);
+ }
+
+ public function test_mysqli_stmt_execute_objective(): void
+ {
+ mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
+
+ $mysqli = new mysqli($this->mysqlHost, $this->user, $this->passwd, $this->database);
+
+ $offset = 0;
+ $this->assertSame('mysqli::__construct', $this->storage->offsetGet($offset)->getName());
+
+ $stmt = new mysqli_stmt($mysqli, "SELECT email FROM users WHERE name='John Doe'");
+ $stmt->execute();
+ $stmt->fetch();
+ $stmt->close();
+
+ $offset++;
+ $this->assertSame('mysqli_stmt::execute', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => "SELECT email FROM users WHERE name='John Doe'",
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ ]);
+
+ $stmt = $mysqli->stmt_init();
+ $stmt->prepare("SELECT email FROM users WHERE name='John Doe'");
+
+ $stmt->execute();
+ $stmt->fetch();
+ $stmt->close();
+
+ $offset++;
+ $this->assertSame('mysqli_stmt::execute', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => "SELECT email FROM users WHERE name='John Doe'",
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ ]);
+
+ $offset++;
+ $this->assertCount($offset, $this->storage);
+ $this->assertDatabaseAttributesForAllSpans($offset);
+ }
+
+ public function test_mysqli_stmt_execute_procedural(): void
+ {
+ mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
+
+ $mysqli = new mysqli($this->mysqlHost, $this->user, $this->passwd, $this->database);
+
+ $offset = 0;
+ $this->assertSame('mysqli::__construct', $this->storage->offsetGet($offset)->getName());
+
+ $stmt = mysqli_stmt_init($mysqli);
+ mysqli_stmt_prepare($stmt, "SELECT email FROM users WHERE name='John Doe'");
+ mysqli_stmt_execute($stmt);
+ mysqli_stmt_fetch($stmt);
+ mysqli_stmt_close($stmt);
+
+ $offset++;
+ $this->assertSame('mysqli_stmt_execute', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => "SELECT email FROM users WHERE name='John Doe'",
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ ]);
+
+ $offset++;
+ $this->assertCount($offset, $this->storage);
+ $this->assertDatabaseAttributesForAllSpans($offset);
+ }
+
+ public function test_mysqli_multiquery_with_calls(): void
+ {
+ mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
+
+ $mysqli = new mysqli($this->mysqlHost, $this->user, $this->passwd, $this->database);
+
+ $offset = 0;
+ $this->assertSame('mysqli::__construct', $this->storage->offsetGet($offset)->getName());
+
+ $createProcedureSQL = "
+ DROP PROCEDURE IF EXISTS get_message;
+ CREATE PROCEDURE get_message()
+ BEGIN
+ -- first result
+ SELECT 'Result 1' AS message;
+ -- second result
+ SELECT 'Result 2' AS message;
+ END;
+ ";
+
+ $mysqli->multi_query($createProcedureSQL);
+
+ $offset++;
+ $this->assertSame('mysqli::multi_query', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'DROP PROCEDURE IF EXISTS get_message;',
+ TraceAttributes::DB_OPERATION_NAME => 'DROP',
+ ]);
+
+ while ($mysqli->next_result()) {
+ if ($result = $mysqli->store_result()) {
+ $result->free();
+ }
+ }
+
+ $offset++;
+ $this->assertSame('mysqli::next_result', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_OPERATION_NAME => 'CREATE',
+ ]);
+ $span = $this->storage->offsetGet($offset);
+ $this->assertStringStartsWith('CREATE PROCEDURE', $span->getAttributes()->get(TraceAttributes::DB_STATEMENT));
+ $this->assertStringEndsWith('END;', $span->getAttributes()->get(TraceAttributes::DB_STATEMENT));
+
+ $stmt = $mysqli->prepare('CALL get_message();');
+ $offset++;
+ $this->assertSame('mysqli::prepare', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'CALL get_message();',
+ TraceAttributes::DB_OPERATION_NAME => 'CALL',
+ ]);
+
+ $stmt->execute();
+
+ $offset++;
+ $this->assertSame('mysqli_stmt::execute', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'CALL get_message();',
+ TraceAttributes::DB_OPERATION_NAME => 'CALL',
+ ]);
+
+ do {
+ $result = $stmt->get_result();
+ if ($result) {
+ while ($row = $result->fetch_assoc()) {
+ // echo 'Result: ' . str_replace(PHP_EOL, '', print_r($row, true)) . PHP_EOL;
+ }
+ $result->free();
+ }
+ } while ($stmt->next_result());
+
+ $offset++;
+ $this->assertSame('mysqli_stmt::next_result', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'CALL get_message();',
+ TraceAttributes::DB_OPERATION_NAME => 'CALL',
+ ]);
+
+ $offset++;
+ $this->assertSame('mysqli_stmt::next_result', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'CALL get_message();',
+ TraceAttributes::DB_OPERATION_NAME => 'CALL',
+ ]);
+
+ // the same but procedural
+
+ mysqli_stmt_execute($stmt);
+
+ $offset++;
+ $this->assertSame('mysqli_stmt_execute', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'CALL get_message();',
+ TraceAttributes::DB_OPERATION_NAME => 'CALL',
+ ]);
+
+ do {
+ $result = mysqli_stmt_get_result($stmt);
+ if ($result) {
+ while ($row = mysqli_fetch_assoc($result)) {
+ // echo 'Result: ' . str_replace(PHP_EOL, '', print_r($row, true)) . PHP_EOL;
+ }
+ mysqli_free_result($result);
+ }
+ } while (mysqli_stmt_next_result($stmt));
+
+ $offset++;
+ $this->assertSame('mysqli_stmt_next_result', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'CALL get_message();',
+ TraceAttributes::DB_OPERATION_NAME => 'CALL',
+ ]);
+
+ $offset++;
+ $this->assertSame('mysqli_stmt_next_result', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'CALL get_message();',
+ TraceAttributes::DB_OPERATION_NAME => 'CALL',
+ ]);
+
+ $offset++;
+ $this->assertCount($offset, $this->storage);
+ $this->assertDatabaseAttributesForAllSpans($offset);
+ }
+
+ public function test_mysqli_change_user(): void
+ {
+ mysqli_report(MYSQLI_REPORT_ERROR| MYSQLI_REPORT_STRICT);
+
+ $mysqli = new mysqli($this->mysqlHost, $this->user, $this->passwd, $this->database);
+
+ $offset = 0;
+ $this->assertSame('mysqli::__construct', $this->storage->offsetGet($offset)->getName());
+
+ $offset++;
+ $res = $mysqli->query('SELECT CURRENT_USER();');
+ if ($res instanceof mysqli_result) {
+ while ($res->fetch_object()) {
+ }
+ }
+
+ $this->assertSame('mysqli::query', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT CURRENT_USER();',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ TraceAttributes::SERVER_ADDRESS => $this->mysqlHost,
+ TraceAttributes::DB_USER => $this->user,
+ TraceAttributes::DB_NAMESPACE => $this->database,
+ TraceAttributes::DB_SYSTEM => 'mysql',
+ ]);
+
+ $mysqli->change_user('otel_user2', $this->passwd, 'otel_db2');
+
+ $offset++;
+ $res = $mysqli->query('SELECT CURRENT_USER();');
+ if ($res instanceof mysqli_result) {
+ while ($res->fetch_object()) {
+ }
+ }
+
+ $this->assertSame('mysqli::query', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT CURRENT_USER();',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ TraceAttributes::SERVER_ADDRESS => $this->mysqlHost,
+ TraceAttributes::DB_USER => 'otel_user2',
+ TraceAttributes::DB_NAMESPACE => 'otel_db2',
+ TraceAttributes::DB_SYSTEM => 'mysql',
+ ]);
+
+ mysqli_change_user($mysqli, $this->user, $this->passwd, $this->database);
+
+ $offset++;
+ $res = $mysqli->query('SELECT CURRENT_USER();');
+ if ($res instanceof mysqli_result) {
+ while ($res->fetch_object()) {
+ }
+ }
+
+ $this->assertSame('mysqli::query', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT CURRENT_USER();',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ TraceAttributes::SERVER_ADDRESS => $this->mysqlHost,
+ TraceAttributes::DB_USER => $this->user,
+ TraceAttributes::DB_NAMESPACE => $this->database,
+ TraceAttributes::DB_SYSTEM => 'mysql',
+ ]);
+
+ try {
+ mysqli_change_user($mysqli, 'blahh', $this->passwd, 'unknowndb');
+ } catch (Throwable) {
+ }
+
+ $offset++;
+
+ try {
+ $res = $mysqli->query('SELECT CURRENT_USER();');
+ if ($res instanceof mysqli_result) {
+ while ($res->fetch_object()) {
+ }
+ }
+ } catch (Throwable) {
+ }
+
+ $this->assertSame('mysqli::query', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT CURRENT_USER();',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ TraceAttributes::SERVER_ADDRESS => $this->mysqlHost,
+ TraceAttributes::DB_USER => $this->user,
+ TraceAttributes::DB_NAMESPACE => $this->database,
+ TraceAttributes::DB_SYSTEM => 'mysql',
+ TraceAttributes::EXCEPTION_TYPE => mysqli_sql_exception::class,
+ ]);
+
+ $offset++;
+ $this->assertCount($offset, $this->storage);
+ }
+
+ public function test_mysqli_select_db(): void
+ {
+ mysqli_report(MYSQLI_REPORT_ERROR| MYSQLI_REPORT_STRICT);
+
+ $mysqli = new mysqli($this->mysqlHost, $this->user, $this->passwd, $this->database);
+
+ $offset = 0;
+ $this->assertSame('mysqli::__construct', $this->storage->offsetGet($offset)->getName());
+
+ $offset++;
+ $res = $mysqli->query('SELECT * FROM users;');
+ if ($res instanceof mysqli_result) {
+ while ($res->fetch_object()) {
+ }
+ }
+
+ $this->assertSame('mysqli::query', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT * FROM users;',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ TraceAttributes::SERVER_ADDRESS => $this->mysqlHost,
+ TraceAttributes::DB_USER => $this->user,
+ TraceAttributes::DB_NAMESPACE => $this->database,
+ TraceAttributes::DB_SYSTEM => 'mysql',
+ ]);
+
+ $mysqli->select_db('otel_db2');
+
+ try {
+ $res = $mysqli->query('SELECT * FROM users;');
+ $this->fail('Should never reach this point');
+ } catch (\Throwable $e) {
+ $offset++;
+
+ $this->assertSame('mysqli::query', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT * FROM users;',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ TraceAttributes::SERVER_ADDRESS => $this->mysqlHost,
+ TraceAttributes::DB_USER => $this->user,
+ TraceAttributes::DB_NAMESPACE => 'otel_db2',
+ TraceAttributes::DB_SYSTEM => 'mysql',
+ TraceAttributes::EXCEPTION_TYPE => mysqli_sql_exception::class,
+ ]);
+ }
+
+
+ mysqli_select_db($mysqli, $this->database);
+
+ $res = $mysqli->query('SELECT * FROM users;');
+ if ($res instanceof mysqli_result) {
+ while ($res->fetch_object()) {
+ }
+ }
+
+ $offset++;
+ $this->assertSame('mysqli::query', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT * FROM users;',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ TraceAttributes::SERVER_ADDRESS => $this->mysqlHost,
+ TraceAttributes::DB_USER => $this->user,
+ TraceAttributes::DB_NAMESPACE => $this->database,
+ TraceAttributes::DB_SYSTEM => 'mysql',
+ ]);
+
+ try {
+ mysqli_select_db($mysqli, 'unknown');
+ } catch (Throwable) {
+
+ }
+
+ $res = $mysqli->query('SELECT * FROM users;');
+ if ($res instanceof mysqli_result) {
+ while ($res->fetch_object()) {
+ }
+ }
+
+ $offset++;
+ $this->assertSame('mysqli::query', $this->storage->offsetGet($offset)->getName());
+ $this->assertAttributes($offset, [
+ TraceAttributes::DB_STATEMENT => 'SELECT * FROM users;',
+ TraceAttributes::DB_OPERATION_NAME => 'SELECT',
+ TraceAttributes::SERVER_ADDRESS => $this->mysqlHost,
+ TraceAttributes::DB_USER => $this->user,
+ TraceAttributes::DB_NAMESPACE => $this->database,
+ TraceAttributes::DB_SYSTEM => 'mysql',
+ ]);
+
+ $offset++;
+ $this->assertCount($offset, $this->storage);
+ }
+
+}
diff --git a/src/Instrumentation/MySqli/tests/Integration/MySqliTrackerTest.php b/src/Instrumentation/MySqli/tests/Integration/MySqliTrackerTest.php
new file mode 100644
index 00000000..3d0587ca
--- /dev/null
+++ b/src/Instrumentation/MySqli/tests/Integration/MySqliTrackerTest.php
@@ -0,0 +1,163 @@
+tracker = new MySqliTracker();
+
+ $this->splitQueries = new ReflectionMethod(MySqliTracker::class, 'splitQueries');
+ $this->splitQueries->setAccessible(true);
+ }
+
+ public function tearDown(): void
+ {
+ unset($this->tracker, $this->splitQueries);
+ }
+
+ public function test_split_queries(): void
+ {
+ $query = "SELECT * FROM users; INSERT INTO logs (message) VALUES ('test');";
+
+ $result = $this->splitQueries->invoke($this->tracker, $query);
+
+ $expected = [
+ 'SELECT * FROM users;',
+ "INSERT INTO logs (message) VALUES ('test');",
+ ];
+
+ $this->assertEquals($expected, $result);
+ }
+
+ public function test_split_queries_whitespaces(): void
+ {
+ $query = " SELECT * FROM users;\n\t INSERT INTO logs (message) VALUES ('test');SELECT * from test\n\n";
+
+ $result = $this->splitQueries->invoke($this->tracker, $query);
+
+ $expected = [
+ 'SELECT * FROM users;',
+ "INSERT INTO logs (message) VALUES ('test');",
+ 'SELECT * from test',
+ ];
+
+ $this->assertEquals($expected, $result);
+ }
+
+ public function test_split_queries_with_begin_end(): void
+ {
+ $query = "
+ DROP PROCEDURE IF EXISTS get_data_with_delay;
+ CREATE PROCEDURE get_data_with_delay()
+ BEGIN
+ -- first result
+ SELECT SLEEP(1);
+ -- second result
+ SELECT 'Result 1' AS message;
+ -- third result
+ SELECT SLEEP(1);
+ -- fourth result
+ SELECT 'Result 2' AS message;
+ END;
+
+ SELECT * FROM users
+ ";
+
+ $result = $this->splitQueries->invoke($this->tracker, $query);
+
+ $expected = [
+ 'DROP PROCEDURE IF EXISTS get_data_with_delay;',
+ "CREATE PROCEDURE get_data_with_delay()
+ BEGIN
+ -- first result
+ SELECT SLEEP(1);
+ -- second result
+ SELECT 'Result 1' AS message;
+ -- third result
+ SELECT SLEEP(1);
+ -- fourth result
+ SELECT 'Result 2' AS message;
+ END;",
+ 'SELECT * FROM users',
+ ];
+
+ $this->assertEquals($expected, $result);
+ }
+
+ public function test_split_queries_with_labeled_begin_end(): void
+ {
+ $query = "
+ DROP PROCEDURE IF EXISTS get_data_with_delay;
+ CREATE PROCEDURE get_data_with_delay()
+ BEGIN label;
+ -- first result
+ SELECT SLEEP(1);
+ -- second result
+ SELECT 'Result 1' AS message;
+ -- third result
+ SELECT SLEEP(1);
+ -- fourth result
+ SELECT 'Result 2' AS message;
+ END label;
+
+ SELECT * FROM users
+ ";
+
+ $result = $this->splitQueries->invoke($this->tracker, $query);
+
+ $expected = [
+ 'DROP PROCEDURE IF EXISTS get_data_with_delay;',
+ "CREATE PROCEDURE get_data_with_delay()
+ BEGIN label;
+ -- first result
+ SELECT SLEEP(1);
+ -- second result
+ SELECT 'Result 1' AS message;
+ -- third result
+ SELECT SLEEP(1);
+ -- fourth result
+ SELECT 'Result 2' AS message;
+ END label;",
+ 'SELECT * FROM users',
+ ];
+
+ $this->assertEquals($expected, $result);
+ }
+
+ public function test_split_queries_with_transaction(): void
+ {
+ $query = "
+ SELECT * FROM users;
+ BEGIN TRANSACTION;
+ INSERT INTO users (name) VALUES ('Alice');
+ INSERT INTO users (name) VALUES ('Bob');
+ END TRANSACTION;
+ SELECT * FROM users2;
+ ";
+
+ $result = $this->splitQueries->invoke($this->tracker, $query);
+
+ $expected = [
+ 'SELECT * FROM users;',
+ "BEGIN TRANSACTION;
+ INSERT INTO users (name) VALUES ('Alice');
+ INSERT INTO users (name) VALUES ('Bob');
+ END TRANSACTION;",
+ 'SELECT * FROM users2;',
+ ];
+
+ $this->assertEquals($expected, $result);
+ }
+
+}
diff --git a/src/Instrumentation/MySqli/tests/Unit/.gitkeep b/src/Instrumentation/MySqli/tests/Unit/.gitkeep
new file mode 100644
index 00000000..e69de29b