From 5feb61d2a92b74c79398a99ebb183f93ff210a1a Mon Sep 17 00:00:00 2001 From: dominic Date: Thu, 3 Oct 2024 09:04:06 +0200 Subject: [PATCH 01/11] feat(#1393): implement integration test and suppress phpstan and psalm errors --- src/Instrumentation/Doctrine/.gitattributes | 12 ++ .../Doctrine/.php-cs-fixer.php | 43 ++++ src/Instrumentation/Doctrine/README.md | 25 +++ src/Instrumentation/Doctrine/_register.php | 18 ++ src/Instrumentation/Doctrine/composer.json | 54 +++++ .../Doctrine/phpstan.neon.dist | 9 + src/Instrumentation/Doctrine/phpunit.xml.dist | 44 +++++ src/Instrumentation/Doctrine/psalm.xml.dist | 17 ++ .../Doctrine/src/DoctrineInstrumentation.php | 186 ++++++++++++++++++ .../DoctrineInstrumentationTest.php | 150 ++++++++++++++ 10 files changed, 558 insertions(+) create mode 100644 src/Instrumentation/Doctrine/.gitattributes create mode 100644 src/Instrumentation/Doctrine/.php-cs-fixer.php create mode 100644 src/Instrumentation/Doctrine/README.md create mode 100644 src/Instrumentation/Doctrine/_register.php create mode 100644 src/Instrumentation/Doctrine/composer.json create mode 100644 src/Instrumentation/Doctrine/phpstan.neon.dist create mode 100644 src/Instrumentation/Doctrine/phpunit.xml.dist create mode 100644 src/Instrumentation/Doctrine/psalm.xml.dist create mode 100644 src/Instrumentation/Doctrine/src/DoctrineInstrumentation.php create mode 100644 src/Instrumentation/Doctrine/tests/Integration/DoctrineInstrumentationTest.php diff --git a/src/Instrumentation/Doctrine/.gitattributes b/src/Instrumentation/Doctrine/.gitattributes new file mode 100644 index 000000000..1676cf825 --- /dev/null +++ b/src/Instrumentation/Doctrine/.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/Doctrine/.php-cs-fixer.php b/src/Instrumentation/Doctrine/.php-cs-fixer.php new file mode 100644 index 000000000..bbfa04e61 --- /dev/null +++ b/src/Instrumentation/Doctrine/.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/Doctrine/README.md b/src/Instrumentation/Doctrine/README.md new file mode 100644 index 000000000..6587063ee --- /dev/null +++ b/src/Instrumentation/Doctrine/README.md @@ -0,0 +1,25 @@ +[![Releases](https://img.shields.io/badge/releases-purple)](https://github.com/opentelemetry-php/contrib-auto-doctrine/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/Doctrine) +[![Mirror](https://img.shields.io/badge/mirror-opentelemetry--php--contrib-blue)](https://github.com/opentelemetry-php/contrib-auto-doctrine) +[![Latest Version](http://poser.pugx.org/open-telemetry/opentelemetry-auto-doctrine/v/unstable)](https://packagist.org/packages/open-telemetry/opentelemetry-auto-doctrine/) +[![Stable](http://poser.pugx.org/open-telemetry/opentelemetry-auto-doctrine/v/stable)](https://packagist.org/packages/open-telemetry/opentelemetry-auto-doctrine/) + +This is a read-only subtree split of https://github.com/open-telemetry/opentelemetry-php-contrib. + +# OpenTelemetry Doctrine 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 spans will automatically be created for +selected `Doctrine\DBAL\Driver` and `Doctrine\DBAL\Driver\Connection` methods. + +## Configuration + +The extension can be disabled via [runtime configuration](https://opentelemetry.io/docs/instrumentation/php/sdk/#configuration): + +```shell +OTEL_PHP_DISABLED_INSTRUMENTATIONS=doctrine +``` diff --git a/src/Instrumentation/Doctrine/_register.php b/src/Instrumentation/Doctrine/_register.php new file mode 100644 index 000000000..5baecbbeb --- /dev/null +++ b/src/Instrumentation/Doctrine/_register.php @@ -0,0 +1,18 @@ + + + + + + + src + + + + + + + + + + + + + tests/Integration + + + + diff --git a/src/Instrumentation/Doctrine/psalm.xml.dist b/src/Instrumentation/Doctrine/psalm.xml.dist new file mode 100644 index 000000000..5a04b34d7 --- /dev/null +++ b/src/Instrumentation/Doctrine/psalm.xml.dist @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/src/Instrumentation/Doctrine/src/DoctrineInstrumentation.php b/src/Instrumentation/Doctrine/src/DoctrineInstrumentation.php new file mode 100644 index 000000000..a87e351d3 --- /dev/null +++ b/src/Instrumentation/Doctrine/src/DoctrineInstrumentation.php @@ -0,0 +1,186 @@ +setSpanKind(SpanKind::KIND_CLIENT); + $builder + ->setAttribute(TraceAttributes::SERVER_ADDRESS, $params[0]['host'] ?? 'unknown') + ->setAttribute(TraceAttributes::SERVER_PORT, $params[0]['port'] ?? 'unknown') + ->setAttribute(TraceAttributes::DB_SYSTEM, $params[0]['driver'] ?? 'unknown') + ->setAttribute(TraceAttributes::DB_NAMESPACE, $params[0]['dbname'] ?? 'unknown'); + $parent = Context::getCurrent(); + $span = $builder->startSpan(); + Context::storage()->attach($span->storeInContext($parent)); + }, + post: static function (\Doctrine\DBAL\Driver $driver, array $params, \Doctrine\DBAL\Driver\Connection $connection, ?Throwable $exception) { + $scope = Context::storage()->scope(); + if (!$scope) { + return; + } + self::end($exception); + } + ); + + hook( + \Doctrine\DBAL\Driver\Connection::class, + 'query', + pre: static function (\Doctrine\DBAL\Driver\Connection $connection, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($instrumentation) { + /** @psalm-suppress ArgumentTypeCoercion */ + $builder = self::makeBuilder($instrumentation, 'Doctrine\DBAL\Driver\Connection::query', $function, $class, $filename, $lineno) + ->setSpanKind(SpanKind::KIND_CLIENT); + $builder->setAttribute(TraceAttributes::DB_QUERY_TEXT, $params[0] ?? 'undefined'); + $parent = Context::getCurrent(); + $span = $builder->startSpan(); + Context::storage()->attach($span->storeInContext($parent)); + }, + post: static function (\Doctrine\DBAL\Driver\Connection $connection, array $params, mixed $statement, ?Throwable $exception) { + self::end($exception); + } + ); + + hook( + \Doctrine\DBAL\Driver\Connection::class, + 'exec', + pre: static function (\Doctrine\DBAL\Driver\Connection $connection, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($instrumentation) { + /** @psalm-suppress ArgumentTypeCoercion */ + $builder = self::makeBuilder($instrumentation, 'Doctrine\DBAL\Driver\Connection::exec', $function, $class, $filename, $lineno) + ->setSpanKind(SpanKind::KIND_CLIENT); + $builder->setAttribute(TraceAttributes::DB_QUERY_TEXT, $params[0] ?? 'undefined'); + $parent = Context::getCurrent(); + $span = $builder->startSpan(); + + Context::storage()->attach($span->storeInContext($parent)); + }, + post: static function (\Doctrine\DBAL\Driver\Connection $connection, array $params, mixed $statement, ?Throwable $exception) { + self::end($exception); + } + ); + + hook( + \Doctrine\DBAL\Driver\Connection::class, + 'prepare', + pre: static function (\Doctrine\DBAL\Driver\Connection $connection, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($instrumentation) { + /** @psalm-suppress ArgumentTypeCoercion */ + $builder = self::makeBuilder($instrumentation, 'Doctrine\DBAL\Driver\Connection::prepare', $function, $class, $filename, $lineno) + ->setSpanKind(SpanKind::KIND_CLIENT); + $builder->setAttribute(TraceAttributes::DB_QUERY_TEXT, $params[0] ?? 'undefined'); + $parent = Context::getCurrent(); + $span = $builder->startSpan(); + + Context::storage()->attach($span->storeInContext($parent)); + }, + post: static function (\Doctrine\DBAL\Driver\Connection $connection, array $params, mixed $statement, ?Throwable $exception) { + self::end($exception); + } + ); + + hook( + \Doctrine\DBAL\Driver\Connection::class, + 'beginTransaction', + pre: static function (\Doctrine\DBAL\Driver\Connection $connection, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($instrumentation) { + /** @psalm-suppress ArgumentTypeCoercion */ + $builder = self::makeBuilder($instrumentation, 'Doctrine\DBAL\Driver\Connection::beginTransaction', $function, $class, $filename, $lineno) + ->setSpanKind(SpanKind::KIND_CLIENT); + $parent = Context::getCurrent(); + $span = $builder->startSpan(); + + Context::storage()->attach($span->storeInContext($parent)); + }, + post: static function (\Doctrine\DBAL\Driver\Connection $connection, array $params, mixed $statement, ?Throwable $exception) { + self::end($exception); + } + ); + + hook( + \Doctrine\DBAL\Driver\Connection::class, + 'commit', + pre: static function (\Doctrine\DBAL\Driver\Connection $connection, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($instrumentation) { + /** @psalm-suppress ArgumentTypeCoercion */ + $builder = self::makeBuilder($instrumentation, 'Doctrine\DBAL\Driver\Connection::commit', $function, $class, $filename, $lineno) + ->setSpanKind(SpanKind::KIND_CLIENT); + $parent = Context::getCurrent(); + $span = $builder->startSpan(); + + Context::storage()->attach($span->storeInContext($parent)); + }, + post: static function (\Doctrine\DBAL\Driver\Connection $connection, array $params, mixed $statement, ?Throwable $exception) { + self::end($exception); + } + ); + + hook( + \Doctrine\DBAL\Driver\Connection::class, + 'rollBack', + pre: static function (\Doctrine\DBAL\Driver\Connection $connection, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($instrumentation) { + /** @psalm-suppress ArgumentTypeCoercion */ + $builder = self::makeBuilder($instrumentation, 'Doctrine\DBAL\Driver\Connection::rollBack', $function, $class, $filename, $lineno) + ->setSpanKind(SpanKind::KIND_CLIENT); + $parent = Context::getCurrent(); + $span = $builder->startSpan(); + Context::storage()->attach($span->storeInContext($parent)); + }, + post: static function (\Doctrine\DBAL\Driver\Connection $connection, array $params, mixed $statement, ?Throwable $exception) { + self::end($exception); + } + ); + } + private static function makeBuilder( + CachedInstrumentation $instrumentation, + string $name, + string $function, + string $class, + ?string $filename, + ?int $lineno + ): SpanBuilderInterface { + /** @psalm-suppress ArgumentTypeCoercion */ + return $instrumentation->tracer() + ->spanBuilder($name) + ->setAttribute(TraceAttributes::CODE_FUNCTION, $function) + ->setAttribute(TraceAttributes::CODE_NAMESPACE, $class) + ->setAttribute(TraceAttributes::CODE_FILEPATH, $filename) + ->setAttribute(TraceAttributes::CODE_LINENO, $lineno); + } + private static function end(?Throwable $exception): void + { + $scope = Context::storage()->scope(); + if (!$scope) { + return; + } + $scope->detach(); + $span = Span::fromContext($scope->context()); + if ($exception) { + $span->recordException($exception, [TraceAttributes::EXCEPTION_ESCAPED => true]); + $span->setStatus(StatusCode::STATUS_ERROR, $exception->getMessage()); + } + + $span->end(); + } +} diff --git a/src/Instrumentation/Doctrine/tests/Integration/DoctrineInstrumentationTest.php b/src/Instrumentation/Doctrine/tests/Integration/DoctrineInstrumentationTest.php new file mode 100644 index 000000000..97554c368 --- /dev/null +++ b/src/Instrumentation/Doctrine/tests/Integration/DoctrineInstrumentationTest.php @@ -0,0 +1,150 @@ + */ + private ArrayObject $storage; + + private function createConnection(): \Doctrine\DBAL\Connection + { + $connectionParams = [ + 'driver' => 'sqlite3', + 'memory' => true, + ]; + + $conn = DriverManager::getConnection($connectionParams); + // Trigger internal connect + $conn->getServerVersion(); + + return $conn; + } + + private function fillDB(): string + { + return <<storage = new ArrayObject(); + $tracerProvider = new TracerProvider( + new SimpleSpanProcessor( + new InMemoryExporter($this->storage) + ) + ); + + $this->scope = Configurator::create() + ->withTracerProvider($tracerProvider) + ->activate(); + } + + public function tearDown(): void + { + $this->scope->detach(); + } + + public function test_connection(): void + { + $this->assertCount(0, $this->storage); + $conn = self::createConnection(); + $this->assertCount(1, $this->storage); + $this->assertTrue($conn->isConnected()); + $span = $this->storage->offsetGet(0); + $this->assertSame('Doctrine\DBAL\Driver::connect', $span->getName()); + $this->assertEquals('sqlite3', $span->getAttributes()->get(TraceAttributes::DB_SYSTEM)); + } + + public function test_connection_exception(): void + { + $this->expectException(\Doctrine\DBAL\Exception::class); + $this->expectExceptionMessageMatches('/The given driver "unknown" is unknown/'); + /** + * @psalm-suppress InvalidArgument + * @phpstan-ignore argument.type + */ + DriverManager::getConnection([ + 'driver' => 'unknown', + ]); + } + + public function test_statement_execution(): void + { + $connection = self::createConnection(); + $statement = self::fillDB(); + + $connection->executeStatement($statement); + $span = $this->storage->offsetGet(1); + $this->assertSame('Doctrine\DBAL\Driver\Connection::exec', $span->getName()); + $this->assertFalse($connection->isTransactionActive()); + $this->assertCount(2, $this->storage); + + $connection->prepare('SELECT * FROM `technology`'); + $span = $this->storage->offsetGet(2); + $this->assertSame('Doctrine\DBAL\Driver\Connection::prepare', $span->getName()); + $this->assertCount(3, $this->storage); + + $connection->executeQuery('SELECT * FROM `technology`'); + $span = $this->storage->offsetGet(3); + $this->assertSame('Doctrine\DBAL\Driver\Connection::query', $span->getName()); + $this->assertCount(4, $this->storage); + } + + public function test_transaction(): void + { + $connection = self::createConnection(); + $connection->beginTransaction(); + $span = $this->storage->offsetGet(1); + $this->assertSame('Doctrine\DBAL\Driver\Connection::beginTransaction', $span->getName()); + $this->assertCount(2, $this->storage); + + $statement = self::fillDB(); + $connection->executeStatement($statement); + $span = $this->storage->offsetGet(2); + $this->assertSame('Doctrine\DBAL\Driver\Connection::exec', $span->getName()); + $connection->commit(); + $span = $this->storage->offsetGet(3); + $this->assertSame('Doctrine\DBAL\Driver\Connection::commit', $span->getName()); + $this->assertCount(4, $this->storage); + + $connection->beginTransaction(); + $this->assertTrue($connection->isTransactionActive()); + + $connection->executeStatement("INSERT INTO technology(`name`, `date`) VALUES('Java', '1995-05-23');"); + $connection->rollback(); + $span = $this->storage->offsetGet(6); + $this->assertSame('Doctrine\DBAL\Driver\Connection::rollBack', $span->getName()); + $this->assertCount(7, $this->storage); + $this->assertFalse($connection->isTransactionActive()); + + $sth = $connection->prepare('SELECT * FROM `technology`'); + $this->assertSame(2, count($sth->executeQuery()->fetchAllAssociative())); + } +} From 42e52c46d3c6f319763bb5da6d3e4f1677369958 Mon Sep 17 00:00:00 2001 From: dominic Date: Thu, 3 Oct 2024 10:16:16 +0200 Subject: [PATCH 02/11] feat(#1393): add split readonly repo and github workflows --- .github/workflows/php.yml | 1 + .gitsplit.yml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index aaa66b315..8d3be290e 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -42,6 +42,7 @@ jobs: 'Instrumentation/MongoDB', 'Instrumentation/CodeIgniter', 'Instrumentation/Yii', + 'Instrumentation/Doctrine', 'Logs/Monolog', 'Propagation/ServerTiming', 'Propagation/TraceResponse', diff --git a/.gitsplit.yml b/.gitsplit.yml index db0a0dd02..4b2cfbe9d 100644 --- a/.gitsplit.yml +++ b/.gitsplit.yml @@ -54,6 +54,8 @@ splits: target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-auto-cakephp.git" - prefix: "src/Instrumentation/Yii" target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-auto-yii.git" + - prefix: "src/Instrumentation/Doctrine" + target: "https://${GH_TOKEN}@github.com/opentelemetry-php/contrib-auto-doctrine.git" - prefix: "src/Context/Swoole" target: "https://${GH_TOKEN}@github.com/opentelemetry-php/context-swoole.git" - prefix: "src/AutoInstrumentationInstaller" From 2e6c142d7ad231804839124535bffe9740269b22 Mon Sep 17 00:00:00 2001 From: dominic Date: Fri, 4 Oct 2024 09:59:56 +0200 Subject: [PATCH 03/11] ci: drop workflows PHP 7.4 version --- .github/workflows/php.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 8d3be290e..2a793fbd0 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - php-version: ['7.4', '8.0', '8.1', '8.2', '8.3'] + php-version: ['8.0', '8.1', '8.2', '8.3'] project: [ 'Aws', 'Context/Swoole', From 011365ef4cc50f353a8bc84d261872ea0d5a7db8 Mon Sep 17 00:00:00 2001 From: dominic Date: Fri, 4 Oct 2024 13:58:46 +0200 Subject: [PATCH 04/11] ci: exclude pre-8.2 versions from github workflows --- .github/workflows/php.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 2a793fbd0..c5fb4f6e2 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - php-version: ['8.0', '8.1', '8.2', '8.3'] + php-version: ['7.4', '8.0', '8.1', '8.2', '8.3'] project: [ 'Aws', 'Context/Swoole', @@ -109,6 +109,12 @@ jobs: php-version: 8.0 - project: 'Instrumentation/CakePHP' php-version: 7.4 + - project: 'Instrumentation/Doctrine' + php-version: 7.4 + - project: 'Instrumentation/Doctrine' + php-version: 8.0 + - project: 'Instrumentation/Doctrine' + php-version: 8.1 - project: 'Propagation/ServerTiming' php-version: 7.4 - project: 'ResourceDetectors/Container' From 046e81092b35f646e52763a492858255b567177b Mon Sep 17 00:00:00 2001 From: domaz Date: Mon, 7 Oct 2024 15:48:31 +0200 Subject: [PATCH 05/11] Update src/Instrumentation/Doctrine/src/DoctrineInstrumentation.php Co-authored-by: Tobias Bachert --- src/Instrumentation/Doctrine/src/DoctrineInstrumentation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Instrumentation/Doctrine/src/DoctrineInstrumentation.php b/src/Instrumentation/Doctrine/src/DoctrineInstrumentation.php index a87e351d3..b49da2899 100644 --- a/src/Instrumentation/Doctrine/src/DoctrineInstrumentation.php +++ b/src/Instrumentation/Doctrine/src/DoctrineInstrumentation.php @@ -40,7 +40,7 @@ public static function register(): void $span = $builder->startSpan(); Context::storage()->attach($span->storeInContext($parent)); }, - post: static function (\Doctrine\DBAL\Driver $driver, array $params, \Doctrine\DBAL\Driver\Connection $connection, ?Throwable $exception) { + post: static function (\Doctrine\DBAL\Driver $driver, array $params, ?\Doctrine\DBAL\Driver\Connection $connection, ?Throwable $exception) { $scope = Context::storage()->scope(); if (!$scope) { return; From 32d028b4249213336dace1fe95cf336ec67ce915 Mon Sep 17 00:00:00 2001 From: domaz Date: Mon, 7 Oct 2024 15:53:14 +0200 Subject: [PATCH 06/11] Update src/Instrumentation/Doctrine/src/DoctrineInstrumentation.php Co-authored-by: Tobias Bachert --- src/Instrumentation/Doctrine/src/DoctrineInstrumentation.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Instrumentation/Doctrine/src/DoctrineInstrumentation.php b/src/Instrumentation/Doctrine/src/DoctrineInstrumentation.php index b49da2899..23238f9fb 100644 --- a/src/Instrumentation/Doctrine/src/DoctrineInstrumentation.php +++ b/src/Instrumentation/Doctrine/src/DoctrineInstrumentation.php @@ -41,10 +41,6 @@ public static function register(): void Context::storage()->attach($span->storeInContext($parent)); }, post: static function (\Doctrine\DBAL\Driver $driver, array $params, ?\Doctrine\DBAL\Driver\Connection $connection, ?Throwable $exception) { - $scope = Context::storage()->scope(); - if (!$scope) { - return; - } self::end($exception); } ); From 8229200337e26227949751a42dda7b5deed1686b Mon Sep 17 00:00:00 2001 From: dominic Date: Thu, 6 Mar 2025 10:29:01 +0100 Subject: [PATCH 07/11] fix: apply semantic conventions on db attributes --- src/Instrumentation/Doctrine/composer.json | 5 +- .../Doctrine/src/AttributesResolver.php | 148 ++++++++++++++++++ .../Doctrine/src/DoctrineInstrumentation.php | 27 ++-- .../DoctrineInstrumentationTest.php | 2 +- 4 files changed, 165 insertions(+), 17 deletions(-) create mode 100644 src/Instrumentation/Doctrine/src/AttributesResolver.php diff --git a/src/Instrumentation/Doctrine/composer.json b/src/Instrumentation/Doctrine/composer.json index e1c2d64ba..044a07f62 100644 --- a/src/Instrumentation/Doctrine/composer.json +++ b/src/Instrumentation/Doctrine/composer.json @@ -48,7 +48,8 @@ }, "config": { "allow-plugins": { - "php-http/discovery": false + "php-http/discovery": false, + "tbachert/spi": true } } -} \ No newline at end of file +} diff --git a/src/Instrumentation/Doctrine/src/AttributesResolver.php b/src/Instrumentation/Doctrine/src/AttributesResolver.php new file mode 100644 index 000000000..d844bfac2 --- /dev/null +++ b/src/Instrumentation/Doctrine/src/AttributesResolver.php @@ -0,0 +1,148 @@ + 'db2', + 'derby', + 'edb', + 'firebird', + 'h2', + 'hsqldb', + 'ingres', + 'interbase', + 'mariadb', + 'maxdb', + 'sqlsrv' => 'mssql', + 'mssqlcompact', + 'mysqli' => 'mysql', + 'oci8' => 'oracle', + 'pervasive', + 'pgsql' => 'postgresql', + 'sqlite3' => 'sqlite', + 'trino', + ]; + + public static function get(string $attributeName, array $params): string + { + $method = 'get' . str_replace('.', '', ucwords($attributeName, '.')); + + if (!method_exists(AttributesResolver::class, $method)) { + throw new Exception(sprintf('Attribute %s not supported by Doctrine', $attributeName)); + } + + return self::{$method}($params); + } + + /** + * Resolve attribute `server.address` + */ + private static function getServerAddress(array $params): string + { + return $params[1][0]['host'] ?? 'unknown'; + } + + /** + * Resolve attribute `server.port` + */ + private static function getServerPort(array $params): string + { + return $params[1][0]['port'] ?? 'unknown'; + } + + /** + * Resolve attribute `db.system` + */ + private static function getDbSystem(array $params): string + { + $dbSystem = $params[1][0]['driver'] ?? null; + + if (strpos($dbSystem, 'pdo_') !== false) { + // Remove pdo_ word to ignore it while searching well-known db.system + $dbSystem = ltrim($dbSystem, 'pdo_'); + } + + if (in_array($dbSystem, self::DB_SYSTEMS_KNOWN)) { + return $dbSystem; + } + + // Fetch the db system using the alias if exists + if (isset(self::DB_SYSTEMS_KNOWN[$dbSystem])) { + return self::DB_SYSTEMS_KNOWN[$dbSystem]; + } + return 'other_sql'; + } + + /** + * Resolve attribute `db.collection.name` + */ + private static function getDbCollectionName(array $params): string + { + return $params[1][0]['dbname'] ?? 'unknown'; + } + + /** + * Resolve attribute `db.query.text` + * No sanitization is implemented because implicitly the query is expected to be expressed as a preparated statement + * which happen automatically in Doctrine if parameters are bound to the query. + */ + private static function getDbQueryText(array $params): string + { + return $params[1][0] ?? 'undefined'; + } + + private static function getDbNamespace(array $params): string + { + return $params[1][0]['dbname'] ?? 'unknown'; + } + + /** + * Resolve attribute `db.query.summary` + * See https://opentelemetry.io/docs/specs/semconv/database/database-spans/#generating-a-summary-of-the-query-text + */ + public static function getDbQuerySummary(array $params): string + { + $query = $params[0] ?? null; + + if (!$query) { + return ''; + } + + // Fetch operation name + $operationName = explode(' ', $query); + $operationName = $operationName[0]; + + // Fetch target name + $matches = []; + preg_match_all('/(from|into|update|join)\s*([a-zA-Z0-9[\]_]+)/i', $query, $matches); + + $targetName = null; + if (strtolower($operationName) == 'select') { + if ($matches && isset($matches[2]) && $matches[2]) { + $targetName = implode(' ', $matches[2]); + } + } elseif ($matches) { + $targetName = $matches[2][0] ?? ''; + } + return $operationName . ($targetName ? ' ' . $targetName : ''); + } +} diff --git a/src/Instrumentation/Doctrine/src/DoctrineInstrumentation.php b/src/Instrumentation/Doctrine/src/DoctrineInstrumentation.php index 23238f9fb..5534c6100 100644 --- a/src/Instrumentation/Doctrine/src/DoctrineInstrumentation.php +++ b/src/Instrumentation/Doctrine/src/DoctrineInstrumentation.php @@ -30,12 +30,11 @@ public static function register(): void pre: static function (\Doctrine\DBAL\Driver $driver, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($instrumentation) { /** @psalm-suppress ArgumentTypeCoercion */ $builder = self::makeBuilder($instrumentation, 'Doctrine\DBAL\Driver::connect', $function, $class, $filename, $lineno) - ->setSpanKind(SpanKind::KIND_CLIENT); - $builder - ->setAttribute(TraceAttributes::SERVER_ADDRESS, $params[0]['host'] ?? 'unknown') - ->setAttribute(TraceAttributes::SERVER_PORT, $params[0]['port'] ?? 'unknown') - ->setAttribute(TraceAttributes::DB_SYSTEM, $params[0]['driver'] ?? 'unknown') - ->setAttribute(TraceAttributes::DB_NAMESPACE, $params[0]['dbname'] ?? 'unknown'); + ->setSpanKind(SpanKind::KIND_CLIENT) + ->setAttribute(TraceAttributes::SERVER_ADDRESS, AttributesResolver::get(TraceAttributes::SERVER_ADDRESS, func_get_args())) + ->setAttribute(TraceAttributes::SERVER_PORT, AttributesResolver::get(TraceAttributes::SERVER_PORT, func_get_args())) + ->setAttribute(TraceAttributes::DB_SYSTEM, AttributesResolver::get(TraceAttributes::DB_SYSTEM, func_get_args())) + ->setAttribute(TraceAttributes::DB_NAMESPACE, AttributesResolver::get(TraceAttributes::DB_NAMESPACE, func_get_args())); $parent = Context::getCurrent(); $span = $builder->startSpan(); Context::storage()->attach($span->storeInContext($parent)); @@ -50,9 +49,9 @@ public static function register(): void 'query', pre: static function (\Doctrine\DBAL\Driver\Connection $connection, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($instrumentation) { /** @psalm-suppress ArgumentTypeCoercion */ - $builder = self::makeBuilder($instrumentation, 'Doctrine\DBAL\Driver\Connection::query', $function, $class, $filename, $lineno) + $builder = self::makeBuilder($instrumentation, AttributesResolver::getDbQuerySummary($params), $function, $class, $filename, $lineno) ->setSpanKind(SpanKind::KIND_CLIENT); - $builder->setAttribute(TraceAttributes::DB_QUERY_TEXT, $params[0] ?? 'undefined'); + $builder->setAttribute(TraceAttributes::DB_QUERY_TEXT, AttributesResolver::get(TraceAttributes::DB_QUERY_TEXT, func_get_args())); $parent = Context::getCurrent(); $span = $builder->startSpan(); Context::storage()->attach($span->storeInContext($parent)); @@ -67,9 +66,9 @@ public static function register(): void 'exec', pre: static function (\Doctrine\DBAL\Driver\Connection $connection, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($instrumentation) { /** @psalm-suppress ArgumentTypeCoercion */ - $builder = self::makeBuilder($instrumentation, 'Doctrine\DBAL\Driver\Connection::exec', $function, $class, $filename, $lineno) - ->setSpanKind(SpanKind::KIND_CLIENT); - $builder->setAttribute(TraceAttributes::DB_QUERY_TEXT, $params[0] ?? 'undefined'); + $builder = self::makeBuilder($instrumentation, AttributesResolver::getDbQuerySummary($params), $function, $class, $filename, $lineno) + ->setSpanKind(SpanKind::KIND_CLIENT) + ->setAttribute(TraceAttributes::DB_QUERY_TEXT, AttributesResolver::get(TraceAttributes::DB_QUERY_TEXT, func_get_args())); $parent = Context::getCurrent(); $span = $builder->startSpan(); @@ -85,9 +84,9 @@ public static function register(): void 'prepare', pre: static function (\Doctrine\DBAL\Driver\Connection $connection, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($instrumentation) { /** @psalm-suppress ArgumentTypeCoercion */ - $builder = self::makeBuilder($instrumentation, 'Doctrine\DBAL\Driver\Connection::prepare', $function, $class, $filename, $lineno) - ->setSpanKind(SpanKind::KIND_CLIENT); - $builder->setAttribute(TraceAttributes::DB_QUERY_TEXT, $params[0] ?? 'undefined'); + $builder = self::makeBuilder($instrumentation, AttributesResolver::getDbQuerySummary($params), $function, $class, $filename, $lineno) + ->setSpanKind(SpanKind::KIND_CLIENT) + ->setAttribute(TraceAttributes::DB_QUERY_TEXT, AttributesResolver::get(TraceAttributes::DB_QUERY_TEXT, func_get_args())); $parent = Context::getCurrent(); $span = $builder->startSpan(); diff --git a/src/Instrumentation/Doctrine/tests/Integration/DoctrineInstrumentationTest.php b/src/Instrumentation/Doctrine/tests/Integration/DoctrineInstrumentationTest.php index 97554c368..ad31a2cc7 100644 --- a/src/Instrumentation/Doctrine/tests/Integration/DoctrineInstrumentationTest.php +++ b/src/Instrumentation/Doctrine/tests/Integration/DoctrineInstrumentationTest.php @@ -79,7 +79,7 @@ public function test_connection(): void $this->assertTrue($conn->isConnected()); $span = $this->storage->offsetGet(0); $this->assertSame('Doctrine\DBAL\Driver::connect', $span->getName()); - $this->assertEquals('sqlite3', $span->getAttributes()->get(TraceAttributes::DB_SYSTEM)); + $this->assertEquals('sqlite', $span->getAttributes()->get(TraceAttributes::DB_SYSTEM)); } public function test_connection_exception(): void From ff6dd7f4a70ce4555e5c0d825508f71929fe032d Mon Sep 17 00:00:00 2001 From: dominic Date: Thu, 6 Mar 2025 10:38:06 +0100 Subject: [PATCH 08/11] fix: resolve conflict github workflows --- .github/workflows/php.yml | 102 ++++++++++---------------------------- 1 file changed, 27 insertions(+), 75 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index c5fb4f6e2..f7bf1597c 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -1,4 +1,3 @@ - name: PHP QA on: @@ -18,29 +17,33 @@ jobs: strategy: fail-fast: false matrix: - php-version: ['7.4', '8.0', '8.1', '8.2', '8.3'] + php-version: ['8.1', '8.2', '8.3', '8.4'] + # Sorted alphabetically to ease finding the desired run in the GitHub Workflow UI. project: [ 'Aws', 'Context/Swoole', + 'Instrumentation/CakePHP', + 'Instrumentation/CodeIgniter', + 'Instrumentation/Curl', 'Instrumentation/ExtAmqp', 'Instrumentation/ExtRdKafka', 'Instrumentation/Guzzle', 'Instrumentation/HttpAsyncClient', - 'Instrumentation/Slim', - 'Instrumentation/CakePHP', + 'Instrumentation/IO', + 'Instrumentation/Laravel', + 'Instrumentation/MongoDB', + 'Instrumentation/MySqli', + 'Instrumentation/OpenAIPHP', + 'Instrumentation/PDO', + # Sort PSRs numerically. 'Instrumentation/Psr3', 'Instrumentation/Psr6', 'Instrumentation/Psr14', 'Instrumentation/Psr15', 'Instrumentation/Psr16', 'Instrumentation/Psr18', - 'Instrumentation/IO', - 'Instrumentation/PDO', + 'Instrumentation/Slim', 'Instrumentation/Symfony', - 'Instrumentation/OpenAIPHP', - 'Instrumentation/Laravel', - 'Instrumentation/MongoDB', - 'Instrumentation/CodeIgniter', 'Instrumentation/Yii', 'Instrumentation/Doctrine', 'Logs/Monolog', @@ -50,79 +53,23 @@ jobs: 'ResourceDetectors/Container', 'Sampler/RuleBased', 'Shims/OpenTracing', - 'Symfony' + 'Symfony', ] exclude: - - project: 'Instrumentation/Guzzle' - php-version: 7.4 - - project: 'Instrumentation/HttpAsyncClient' - php-version: 7.4 - - project: 'Instrumentation/Slim' - php-version: 7.4 - - project: 'Instrumentation/Psr3' - php-version: 7.4 - - project: 'Instrumentation/Psr6' - php-version: 7.4 - - project: 'Instrumentation/Psr14' - php-version: 7.4 - - project: 'Instrumentation/Psr15' - php-version: 7.4 - - project: 'Instrumentation/Psr16' - php-version: 7.4 - - project: 'Instrumentation/Psr18' - php-version: 7.4 - - project: 'Instrumentation/IO' - php-version: 7.4 - - project: 'Instrumentation/Symfony' - php-version: 7.4 - - project: 'Instrumentation/Laravel' - php-version: 7.4 - - project: 'Instrumentation/CodeIgniter' - php-version: 7.4 - - project: 'Instrumentation/Yii' - php-version: 7.4 - - project: 'Instrumentation/IO' - php-version: 8.0 - project: 'Instrumentation/IO' php-version: 8.1 - - project: 'Instrumentation/PDO' - php-version: 7.4 - - project: 'Instrumentation/PDO' - php-version: 8.0 + - project: 'Instrumentation/Curl' + php-version: 8.1 + - project: 'Instrumentation/MySqli' + php-version: 8.1 - project: 'Instrumentation/PDO' php-version: 8.1 - - project: 'Instrumentation/ExtAmqp' - php-version: 7.4 - - project: 'Instrumentation/ExtAmqp' - php-version: 8.0 - project: 'Instrumentation/ExtAmqp' php-version: 8.1 - - project: 'Instrumentation/ExtRdKafka' - php-version: 7.4 - - project: 'Instrumentation/ExtRdKafka' - php-version: 8.0 - project: 'Instrumentation/ExtRdKafka' php-version: 8.1 - - project: 'Instrumentation/OpenAIPHP' - php-version: 7.4 - - project: 'Instrumentation/OpenAIPHP' - php-version: 8.0 - - project: 'Instrumentation/CakePHP' - php-version: 7.4 - - project: 'Instrumentation/Doctrine' - php-version: 7.4 - - project: 'Instrumentation/Doctrine' - php-version: 8.0 - project: 'Instrumentation/Doctrine' php-version: 8.1 - - project: 'Propagation/ServerTiming' - php-version: 7.4 - - project: 'ResourceDetectors/Container' - php-version: 7.4 - - project: 'Sampler/RuleBased' - php-version: 7.4 - - project: 'Sampler/RuleBased' - php-version: 8.0 steps: - uses: actions/checkout@v4 @@ -131,7 +78,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 @@ -158,7 +105,7 @@ jobs: - name: Check Style working-directory: src/${{ matrix.project }} - run: vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php -v --dry-run --stop-on-violation --using-cache=no -vvv + run: PHP_CS_FIXER_IGNORE_ENV=1 vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php -v --dry-run --stop-on-violation --using-cache=no -vvv - name: Run Phan working-directory: src/${{ matrix.project }} @@ -191,6 +138,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 @@ -202,10 +154,10 @@ jobs: - name: Code Coverage uses: codecov/codecov-action@v4 # only generate coverage against the latest PHP version - if: ${{ matrix.php-version == '8.3' }} + if: ${{ matrix.php-version == '8.4' }} with: token: ${{ secrets.CODECOV_TOKEN }} directory: src/${{ matrix.project }} files: ./coverage.clover flags: ${{ matrix.project }} - verbose: false + verbose: false \ No newline at end of file From 8ce859c4a8e4b0e1b4cedc4e4acfc704bd31c8e9 Mon Sep 17 00:00:00 2001 From: dominic Date: Thu, 6 Mar 2025 11:14:01 +0100 Subject: [PATCH 09/11] fix: apply insights from test integrations --- .../Doctrine/src/AttributesResolver.php | 11 ++++++----- .../tests/Integration/DoctrineInstrumentationTest.php | 8 ++++---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Instrumentation/Doctrine/src/AttributesResolver.php b/src/Instrumentation/Doctrine/src/AttributesResolver.php index d844bfac2..f6fd2858f 100644 --- a/src/Instrumentation/Doctrine/src/AttributesResolver.php +++ b/src/Instrumentation/Doctrine/src/AttributesResolver.php @@ -4,7 +4,6 @@ namespace OpenTelemetry\Contrib\Instrumentation\Doctrine; -use Doctrine\DBAL\SQL\Parser; use Exception; final class AttributesResolver @@ -72,11 +71,11 @@ private static function getServerPort(array $params): string /** * Resolve attribute `db.system` */ - private static function getDbSystem(array $params): string + private static function getDbSystem(array $params) { $dbSystem = $params[1][0]['driver'] ?? null; - if (strpos($dbSystem, 'pdo_') !== false) { + if ($dbSystem && strpos($dbSystem, 'pdo_') !== false) { // Remove pdo_ word to ignore it while searching well-known db.system $dbSystem = ltrim($dbSystem, 'pdo_'); } @@ -89,6 +88,7 @@ private static function getDbSystem(array $params): string if (isset(self::DB_SYSTEMS_KNOWN[$dbSystem])) { return self::DB_SYSTEMS_KNOWN[$dbSystem]; } + return 'other_sql'; } @@ -133,16 +133,17 @@ public static function getDbQuerySummary(array $params): string // Fetch target name $matches = []; - preg_match_all('/(from|into|update|join)\s*([a-zA-Z0-9[\]_]+)/i', $query, $matches); + preg_match_all('/(from|into|update|join)\s*([a-zA-Z0-9`"[\]_]+)/i', $query, $matches); $targetName = null; if (strtolower($operationName) == 'select') { - if ($matches && isset($matches[2]) && $matches[2]) { + if ($matches[2]) { $targetName = implode(' ', $matches[2]); } } elseif ($matches) { $targetName = $matches[2][0] ?? ''; } + return $operationName . ($targetName ? ' ' . $targetName : ''); } } diff --git a/src/Instrumentation/Doctrine/tests/Integration/DoctrineInstrumentationTest.php b/src/Instrumentation/Doctrine/tests/Integration/DoctrineInstrumentationTest.php index ad31a2cc7..36219490c 100644 --- a/src/Instrumentation/Doctrine/tests/Integration/DoctrineInstrumentationTest.php +++ b/src/Instrumentation/Doctrine/tests/Integration/DoctrineInstrumentationTest.php @@ -102,18 +102,18 @@ public function test_statement_execution(): void $connection->executeStatement($statement); $span = $this->storage->offsetGet(1); - $this->assertSame('Doctrine\DBAL\Driver\Connection::exec', $span->getName()); + $this->assertSame('CREATE technology', $span->getName()); $this->assertFalse($connection->isTransactionActive()); $this->assertCount(2, $this->storage); $connection->prepare('SELECT * FROM `technology`'); $span = $this->storage->offsetGet(2); - $this->assertSame('Doctrine\DBAL\Driver\Connection::prepare', $span->getName()); + $this->assertSame('SELECT `technology`', $span->getName()); $this->assertCount(3, $this->storage); $connection->executeQuery('SELECT * FROM `technology`'); $span = $this->storage->offsetGet(3); - $this->assertSame('Doctrine\DBAL\Driver\Connection::query', $span->getName()); + $this->assertSame('SELECT `technology`', $span->getName()); $this->assertCount(4, $this->storage); } @@ -128,7 +128,7 @@ public function test_transaction(): void $statement = self::fillDB(); $connection->executeStatement($statement); $span = $this->storage->offsetGet(2); - $this->assertSame('Doctrine\DBAL\Driver\Connection::exec', $span->getName()); + $this->assertSame('CREATE technology', $span->getName()); $connection->commit(); $span = $this->storage->offsetGet(3); $this->assertSame('Doctrine\DBAL\Driver\Connection::commit', $span->getName()); From 099b86ac8b4f3504835c5c091e1793cfe42264c3 Mon Sep 17 00:00:00 2001 From: dominic Date: Thu, 6 Mar 2025 11:21:32 +0100 Subject: [PATCH 10/11] fix: regexp --- src/Instrumentation/Doctrine/src/AttributesResolver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Instrumentation/Doctrine/src/AttributesResolver.php b/src/Instrumentation/Doctrine/src/AttributesResolver.php index f6fd2858f..b3731d2be 100644 --- a/src/Instrumentation/Doctrine/src/AttributesResolver.php +++ b/src/Instrumentation/Doctrine/src/AttributesResolver.php @@ -133,7 +133,7 @@ public static function getDbQuerySummary(array $params): string // Fetch target name $matches = []; - preg_match_all('/(from|into|update|join)\s*([a-zA-Z0-9`"[\]_]+)/i', $query, $matches); + preg_match_all('/( from| into| update| join)\s*([a-zA-Z0-9`"[\]_]+)/i', $query, $matches); $targetName = null; if (strtolower($operationName) == 'select') { From 829157c35bc6b03b542ad3685c6118f70102893e Mon Sep 17 00:00:00 2001 From: dominic Date: Mon, 10 Mar 2025 10:03:26 +0100 Subject: [PATCH 11/11] chore: reorder lines of instrumentation Doctrine and add newline EOF --- .github/workflows/php.yml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index f7bf1597c..cfc53004f 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -25,6 +25,7 @@ jobs: 'Instrumentation/CakePHP', 'Instrumentation/CodeIgniter', 'Instrumentation/Curl', + 'Instrumentation/Doctrine', 'Instrumentation/ExtAmqp', 'Instrumentation/ExtRdKafka', 'Instrumentation/Guzzle', @@ -45,7 +46,6 @@ jobs: 'Instrumentation/Slim', 'Instrumentation/Symfony', 'Instrumentation/Yii', - 'Instrumentation/Doctrine', 'Logs/Monolog', 'Propagation/ServerTiming', 'Propagation/TraceResponse', @@ -56,19 +56,20 @@ jobs: 'Symfony', ] exclude: - - project: 'Instrumentation/IO' - php-version: 8.1 + - project: 'Instrumentation/Curl' php-version: 8.1 - - project: 'Instrumentation/MySqli' - php-version: 8.1 - - project: 'Instrumentation/PDO' + - project: 'Instrumentation/Doctrine' php-version: 8.1 - project: 'Instrumentation/ExtAmqp' php-version: 8.1 - project: 'Instrumentation/ExtRdKafka' php-version: 8.1 - - project: 'Instrumentation/Doctrine' + - project: 'Instrumentation/IO' + php-version: 8.1 + - project: 'Instrumentation/MySqli' + php-version: 8.1 + - project: 'Instrumentation/PDO' php-version: 8.1 steps: - uses: actions/checkout@v4 @@ -160,4 +161,4 @@ jobs: directory: src/${{ matrix.project }} files: ./coverage.clover flags: ${{ matrix.project }} - verbose: false \ No newline at end of file + verbose: false