diff --git a/docker/mysql/init.sql b/docker/mysql/init.sql index 72304a89..e1b5c346 100644 --- a/docker/mysql/init.sql +++ b/docker/mysql/init.sql @@ -1,8 +1,9 @@ --- CREATE DATABASE IF NOT EXISTS otel_db; --- DROP USER IF EXISTS 'otel_user'@'%'; --- CREATE USER 'otel_user'@'%' IDENTIFIED BY 'otel_passwd'; +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; diff --git a/src/Instrumentation/MySqli/README.md b/src/Instrumentation/MySqli/README.md index 8d6c2a4b..01a08d87 100644 --- a/src/Instrumentation/MySqli/README.md +++ b/src/Instrumentation/MySqli/README.md @@ -18,8 +18,8 @@ Auto-instrumentation hooks are registered via composer, and client kind spans wi * `mysqli_connect` * `mysqli::__construct` * `mysqli::connect` -* `mysqli::real_connect` * `mysqli_real_connect` +* `mysqli::real_connect` * `mysqli_query` * `mysqli::query` @@ -32,14 +32,17 @@ Auto-instrumentation hooks are registered via composer, and client kind spans wi * `mysqli_next_result` * `mysqli::next_result` -* `mysqli_stmt::execute` +* `mysqli_begin_transaction` +* `mysqli::begin_transaction` +* `mysqli_rollback` +* `mysqli::rollback` +* `mysqli_commit` +* `mysqli::commit` +* * `mysqli_stmt_execute` -* `mysqli_stmt::next_result` +* `mysqli_stmt::execute` * `mysqli_stmt_next_result` - -## Limitations - -Transactions are not fully supported yet +* `mysqli_stmt::next_result` ## Configuration diff --git a/src/Instrumentation/MySqli/src/MySqliInstrumentation.php b/src/Instrumentation/MySqli/src/MySqliInstrumentation.php index 5bf380e2..55e9a892 100644 --- a/src/Instrumentation/MySqli/src/MySqliInstrumentation.php +++ b/src/Instrumentation/MySqli/src/MySqliInstrumentation.php @@ -248,6 +248,69 @@ public static function register(): void } ); + hook( + null, + 'mysqli_begin_transaction', + pre: static function (...$args) use ($instrumentation, $tracker) { + self::beginTransactionPreHook('mysqli_begin_transaction', $instrumentation, $tracker, ...$args); + }, + post: static function (...$args) use ($instrumentation, $tracker) { + self::beginTransactionPostHook($instrumentation, $tracker, ...$args); + } + ); + hook( + mysqli::class, + 'begin_transaction', + pre: static function (...$args) use ($instrumentation, $tracker) { + self::beginTransactionPreHook('mysqli::begin_transaction', $instrumentation, $tracker, ...$args); + }, + post: static function (...$args) use ($instrumentation, $tracker) { + self::beginTransactionPostHook($instrumentation, $tracker, ...$args); + } + ); + + hook( + null, + 'mysqli_rollback', + pre: static function (...$args) use ($instrumentation, $tracker) { + self::transactionPreHook('mysqli_rollback', $instrumentation, $tracker, ...$args); + }, + post: static function (...$args) use ($instrumentation, $tracker) { + self::transactionPostHook($instrumentation, $tracker, ...$args); + } + ); + hook( + mysqli::class, + 'rollback', + pre: static function (...$args) use ($instrumentation, $tracker) { + self::transactionPreHook('mysqli::rollback', $instrumentation, $tracker, ...$args); + }, + post: static function (...$args) use ($instrumentation, $tracker) { + self::transactionPostHook($instrumentation, $tracker, ...$args); + } + ); + + hook( + null, + 'mysqli_commit', + pre: static function (...$args) use ($instrumentation, $tracker) { + self::transactionPreHook('mysqli_commit', $instrumentation, $tracker, ...$args); + }, + post: static function (...$args) use ($instrumentation, $tracker) { + self::transactionPostHook($instrumentation, $tracker, ...$args); + } + ); + hook( + mysqli::class, + 'commit', + pre: static function (...$args) use ($instrumentation, $tracker) { + self::transactionPreHook('mysqli::commit', $instrumentation, $tracker, ...$args); + }, + post: static function (...$args) use ($instrumentation, $tracker) { + self::transactionPostHook($instrumentation, $tracker, ...$args); + } + ); + // Statement hooks hook( @@ -373,7 +436,9 @@ private static function constructPostHook(int $paramsOffset, CachedInstrumentati /** @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 { - self::startSpan($spanName, $instrumentation, $class, $function, $filename, $lineno, []); + $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) @@ -431,6 +496,7 @@ private static function nextResultPreHook(string $spanName, CachedInstrumentatio $span->addLink($spanContext); } + self::addTransactionLink($tracker, $span, $mysqli); } private static function nextResultPostHook(CachedInstrumentation $instrumentation, MySqliTracker $tracker, $obj, array $params, mixed $retVal, ?\Throwable $exception) @@ -509,6 +575,64 @@ private static function preparePostHook(CachedInstrumentation $instrumentation, } + /** @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) { @@ -550,7 +674,8 @@ private static function stmtConstructPostHook(CachedInstrumentation $instrumenta /** @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 { - self::startSpan($spanName, $instrumentation, $class, $function, $filename, $lineno, []); + $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) @@ -580,7 +705,7 @@ private static function stmtNextResultPreHook(string $spanName, CachedInstrument 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) @@ -663,6 +788,19 @@ private static function dropSpan() $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); diff --git a/src/Instrumentation/MySqli/src/MySqliTracker.php b/src/Instrumentation/MySqli/src/MySqliTracker.php index 49b97a11..568a766a 100644 --- a/src/Instrumentation/MySqli/src/MySqliTracker.php +++ b/src/Instrumentation/MySqli/src/MySqliTracker.php @@ -20,6 +20,7 @@ final class MySqliTracker private WeakMap $statementAttributes; private WeakMap $statementSpan; private WeakMap $mySqliSpan; + private WeakMap $mySqliTransaction; public function __construct() { @@ -30,6 +31,7 @@ public function __construct() $this->statementAttributes = new WeakMap(); $this->statementSpan = new WeakMap(); $this->mySqliSpan = new WeakMap(); + $this->mySqliTransaction = new WeakMap(); } public function storeMySqliMultiQuery(mysqli $mysqli, string $query) @@ -77,6 +79,12 @@ public function trackMySqliFromStatement(mysqli $mysqli, mysqli_stmt $mysqli_stm $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(); @@ -132,6 +140,27 @@ public function getMySqliSpan(mysqli $mysqli) : ?SpanContextInterface 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 diff --git a/src/Instrumentation/MySqli/tests/Integration/MySqliInstrumentationTest.php b/src/Instrumentation/MySqli/tests/Integration/MySqliInstrumentationTest.php index 0dcd5d43..4919f3ab 100644 --- a/src/Instrumentation/MySqli/tests/Integration/MySqliInstrumentationTest.php +++ b/src/Instrumentation/MySqli/tests/Integration/MySqliInstrumentationTest.php @@ -8,6 +8,7 @@ use mysqli; use mysqli_result; use mysqli_sql_exception; +use mysqli_stmt; use OpenTelemetry\API\Instrumentation\Configurator; use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator; use OpenTelemetry\API\Trace\StatusCode; @@ -425,4 +426,589 @@ public function test_mysqli_execute_query_procedural(): void $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_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();'); + $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 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->select_db('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 => $this->user, + TraceAttributes::DB_NAMESPACE => 'otel_db2', + TraceAttributes::DB_SYSTEM => 'mysql', + ]); + + mysqli_select_db($mysqli, $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_select_db($mysqli, 'unknown'); + } 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', + ]); + + $offset++; + $this->assertCount($offset, $this->storage); + } + }