From e971b0f09df5a751d9e92f30c05286c7bac4a762 Mon Sep 17 00:00:00 2001 From: Pawel Filipczak Date: Mon, 2 Dec 2024 16:03:16 +0100 Subject: [PATCH] Auto instrumentation for mysqli --- src/Instrumentation/MySqli/.gitattributes | 12 + src/Instrumentation/MySqli/.gitignore | 1 + src/Instrumentation/MySqli/.php-cs-fixer.php | 43 ++ src/Instrumentation/MySqli/README.md | 53 ++ src/Instrumentation/MySqli/_register.php | 18 + src/Instrumentation/MySqli/composer.json | 59 ++ src/Instrumentation/MySqli/phpstan.neon.dist | 9 + src/Instrumentation/MySqli/phpunit.xml.dist | 47 ++ src/Instrumentation/MySqli/psalm.xml.dist | 17 + .../MySqli/src/MySqliInstrumentation.php | 671 ++++++++++++++++++ .../MySqli/src/MySqliTracker.php | 184 +++++ .../Integration/MySqliInstrumentationTest.php | 48 ++ .../MySqli/tests/Unit/.gitkeep | 0 13 files changed, 1162 insertions(+) create mode 100644 src/Instrumentation/MySqli/.gitattributes create mode 100644 src/Instrumentation/MySqli/.gitignore create mode 100644 src/Instrumentation/MySqli/.php-cs-fixer.php create mode 100644 src/Instrumentation/MySqli/README.md create mode 100644 src/Instrumentation/MySqli/_register.php create mode 100644 src/Instrumentation/MySqli/composer.json create mode 100644 src/Instrumentation/MySqli/phpstan.neon.dist create mode 100644 src/Instrumentation/MySqli/phpunit.xml.dist create mode 100644 src/Instrumentation/MySqli/psalm.xml.dist create mode 100644 src/Instrumentation/MySqli/src/MySqliInstrumentation.php create mode 100644 src/Instrumentation/MySqli/src/MySqliTracker.php create mode 100644 src/Instrumentation/MySqli/tests/Integration/MySqliInstrumentationTest.php create mode 100644 src/Instrumentation/MySqli/tests/Unit/.gitkeep 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..8d6c2a4b --- /dev/null +++ b/src/Instrumentation/MySqli/README.md @@ -0,0 +1,53 @@ +[![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_stmt::execute` +* `mysqli_stmt_execute` +* `mysqli_stmt::next_result` +* `mysqli_stmt_next_result` + +## Limitations + +Transactions are not fully supported yet + +## 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..9c1582f2 --- /dev/null +++ b/src/Instrumentation/MySqli/src/MySqliInstrumentation.php @@ -0,0 +1,671 @@ +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); + + } + + 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, []); + } + + 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); + + } + + 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); + } + + } + + 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; //TODO create error span? + } + + $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; //TODO create error span? + } + $tracker->addMySqliAttribute($obj ? $obj : $params[0], TraceAttributes::DB_NAMESPACE, $params[$obj ? 0 : 1]); + } + + private static function preparePostHook(CachedInstrumentation $instrumentation, MySqliTracker $tracker, $obj, array $params, mixed $stmtRetVal, ?\Throwable $exception) + { + + if ($exception || !$stmtRetVal instanceof mysqli_stmt) { + self::logDebug('mysqli::prepare failed', ['exception' => $exception, 'obj' => $obj, 'retVal' => $stmtRetVal, 'params' => $params]); + + return; + } + + $mysqli = $obj ? $obj : $params[0]; + $query = $params[$obj ? 0 : 1]; + + $tracker->trackMySqliFromStatement($mysqli, $stmtRetVal); + + $tracker->addStatementAttribute($stmtRetVal, TraceAttributes::DB_STATEMENT, mb_convert_encoding($query, 'UTF-8')); + $tracker->addStatementAttribute($stmtRetVal, TraceAttributes::DB_OPERATION_NAME, self::extractQueryCommand($query)); + + } + + 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])); + } + } + + 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, []); + } + + 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); + + } + + 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); + } + + } + + 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); + } + + 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(array $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 extractQueryCommand($query) : ?string + { + $query = preg_replace("/\r\n|\n\r|\r/", "\n", $query); + 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..5a9f4ce3 --- /dev/null +++ b/src/Instrumentation/MySqli/src/MySqliTracker.php @@ -0,0 +1,184 @@ +mySqliToAttributes = new WeakMap(); + $this->mySqliToMultiQueries = new WeakMap(); + $this->statementToMySqli = new WeakMap(); + $this->statementAttributes = new WeakMap(); + $this->statementSpan = new WeakMap(); + $this->mySqliSpan = 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[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) : iterable + { + return $this->mySqliToAttributes[$mysqli] ?? []; + } + + public function trackMySqliFromStatement(mysqli $mysqli, mysqli_stmt $mysqli_stmt) + { + $this->statementToMySqli[$mysqli_stmt] = WeakReference::create($mysqli); + } + + public function getMySqliAttributesFromStatement(mysqli_stmt $stmt) : iterable + { + $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) : iterable + { + 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(); + } + + 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..88dda9e6 --- /dev/null +++ b/src/Instrumentation/MySqli/tests/Integration/MySqliInstrumentationTest.php @@ -0,0 +1,48 @@ + */ + private ArrayObject $storage; + + 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(); + } + + public function tearDown(): void + { + $this->scope->detach(); + } + + public function test_mysqli_connect(): void + { + } + + // to be continued +} diff --git a/src/Instrumentation/MySqli/tests/Unit/.gitkeep b/src/Instrumentation/MySqli/tests/Unit/.gitkeep new file mode 100644 index 00000000..e69de29b