diff --git a/src/Instrumentation/PDO/README.md b/src/Instrumentation/PDO/README.md index 7a56fbda4..a2d624e14 100644 --- a/src/Instrumentation/PDO/README.md +++ b/src/Instrumentation/PDO/README.md @@ -33,4 +33,38 @@ otel.instrumentation.pdo.distribute_statement_to_linked_spans = true or environment variable: ```shell OTEL_PHP_INSTRUMENTATION_PDO_DISTRIBUTE_STATEMENT_TO_LINKED_SPANS=true +``` + +### SQL Commenter feature +The [sqlcommenter](https://opentelemetry.io/docs/specs/semconv/database/database-spans/#sql-commenter) feature can be enabled using configuration directive, currently it can be used with `postgresql` and `mysql` drivers only. +``` +otel.instrumentation.pdo.context_propagation = true +``` +or environment variable: +```shell +OTEL_PHP_INSTRUMENTATION_PDO_CONTEXT_PROPAGATION=true +``` + +The context sources from global propagator by default, but it can be configured using the following environment variables: +```shell +OTEL_PHP_INSTRUMENTATION_PDO_CONTEXT_PROPAGATORS=tracecontext +``` + +The modified query statement by default will not update `DbAttributes::DB_QUERY_TEXT` due to high cardinality risk, but it can be configured using the following configuration directive: +``` +otel.instrumentation.pdo.context_propagation.attribute = true +``` +or environment variable: +```shell +OTEL_PHP_INSTRUMENTATION_PDO_CONTEXT_PROPAGATION_ATTRIBUTE=true +``` + +This feature by default will append a SQL comment to the query statement with the information about the code that executed the query. +The SQL comment can be configured to prepend to the query statement using the following configuration directive: +``` +otel.instrumentation.pdo.sql_commenter.prepend = true +``` +or environment variable: +```shell +OTEL_PHP_INSTRUMENTATION_PDO_SQL_COMMENTER_PREPEND=true ``` \ No newline at end of file diff --git a/src/Instrumentation/PDO/src/ContextPropagation.php b/src/Instrumentation/PDO/src/ContextPropagation.php new file mode 100644 index 000000000..ef48b9a7e --- /dev/null +++ b/src/Instrumentation/PDO/src/ContextPropagation.php @@ -0,0 +1,28 @@ +buildPropagator($propagators[0]); + if ($propagator !== null && is_a($propagator, NoopTextMapPropagator::class)) { + return null; + } + + return $propagator; + default: + $props = $this->buildPropagators($propagators); + if ($props) { + return new MultiTextMapPropagator($props); + } + + return null; + } + } + + /** + * @return ?list + */ + private function buildPropagators(array $names): ?array + { + $propagators = []; + foreach ($names as $name) { + $propagator = $this->buildPropagator($name); + if ($propagator !== null && !is_a($propagator, NoopTextMapPropagator::class)) { + $propagators[] = $propagator; + } + } + if (count($propagators) === 0) { + return null; + } + + return $propagators; + } + + private function buildPropagator(string $name): ?TextMapPropagatorInterface + { + try { + return Registry::textMapPropagator($name); + } catch (\RuntimeException $e) { + self::logWarning($e->getMessage()); + } + + return null; + } +} diff --git a/src/Instrumentation/PDO/src/PDOInstrumentation.php b/src/Instrumentation/PDO/src/PDOInstrumentation.php index a739d9e38..a7d97f6ba 100644 --- a/src/Instrumentation/PDO/src/PDOInstrumentation.php +++ b/src/Instrumentation/PDO/src/PDOInstrumentation.php @@ -4,6 +4,7 @@ namespace OpenTelemetry\Contrib\Instrumentation\PDO; +use OpenTelemetry\API\Globals; use OpenTelemetry\API\Instrumentation\CachedInstrumentation; use OpenTelemetry\API\Trace\Span; use OpenTelemetry\API\Trace\SpanBuilderInterface; @@ -22,6 +23,7 @@ class PDOInstrumentation { public const NAME = 'pdo'; + private const UNDEFINED = 'undefined'; public static function register(): void { @@ -31,6 +33,7 @@ public static function register(): void Version::VERSION_1_36_0->url(), ); $pdoTracker = new PDOTracker(); + $contextPropagator = (new ContextPropagatorFactory())->create(); // Hook for the new PDO::connect static method if (method_exists(PDO::class, 'connect')) { @@ -108,12 +111,16 @@ public static function register(): void hook( PDO::class, 'query', - pre: static function (PDO $pdo, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($pdoTracker, $instrumentation) { + pre: static function (PDO $pdo, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($contextPropagator, $pdoTracker, $instrumentation) { /** @psalm-suppress ArgumentTypeCoercion */ $builder = self::makeBuilder($instrumentation, 'PDO::query', $function, $class, $filename, $lineno) ->setSpanKind(SpanKind::KIND_CLIENT); + $sqlStatement = mb_convert_encoding($params[0] ?? self::UNDEFINED, 'UTF-8'); + if (!is_string($sqlStatement)) { + $sqlStatement = self::UNDEFINED; + } if ($class === PDO::class) { - $builder->setAttribute(DbAttributes::DB_QUERY_TEXT, mb_convert_encoding($params[0] ?? 'undefined', 'UTF-8')); + $builder->setAttribute(DbAttributes::DB_QUERY_TEXT, $sqlStatement); } $parent = Context::getCurrent(); $span = $builder->startSpan(); @@ -122,6 +129,39 @@ public static function register(): void $span->setAttributes($attributes); Context::storage()->attach($span->storeInContext($parent)); + if (ContextPropagation::isEnabled() && $sqlStatement !== self::UNDEFINED) { + if (array_key_exists(DbAttributes::DB_SYSTEM_NAME, $attributes)) { + /** @psalm-suppress PossiblyInvalidCast */ + switch ((string) $attributes[DbAttributes::DB_SYSTEM_NAME]) { + case 'postgresql': + case 'mysql': + $comments = []; + if ($contextPropagator !== null) { + // Propagator passed by user + $contextPropagator->inject($comments); + } else { + // fallback to global propagator if user didn't pass one + Globals::propagator()->inject($comments); + } + // Inject comments into SQL statement + $sqlStatement = SqlCommentInjector::inject($sqlStatement, $comments); + if (ContextPropagation::isAttributeEnabled()) { + $span->setAttributes([ + DbAttributes::DB_QUERY_TEXT => $sqlStatement, + ]); + } + + return [ + 0 => $sqlStatement, + ]; + default: + // Do nothing, not a database we want to propagate + break; + } + } + } + + return []; }, post: static function (PDO $pdo, array $params, mixed $statement, ?Throwable $exception) { self::end($exception); @@ -131,12 +171,16 @@ public static function register(): void hook( PDO::class, 'exec', - pre: static function (PDO $pdo, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($pdoTracker, $instrumentation) { + pre: static function (PDO $pdo, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($contextPropagator, $pdoTracker, $instrumentation) { /** @psalm-suppress ArgumentTypeCoercion */ $builder = self::makeBuilder($instrumentation, 'PDO::exec', $function, $class, $filename, $lineno) ->setSpanKind(SpanKind::KIND_CLIENT); + $sqlStatement = mb_convert_encoding($params[0] ?? self::UNDEFINED, 'UTF-8'); + if (!is_string($sqlStatement)) { + $sqlStatement = self::UNDEFINED; + } if ($class === PDO::class) { - $builder->setAttribute(DbAttributes::DB_QUERY_TEXT, mb_convert_encoding($params[0] ?? 'undefined', 'UTF-8')); + $builder->setAttribute(DbAttributes::DB_QUERY_TEXT, $sqlStatement); } $parent = Context::getCurrent(); $span = $builder->startSpan(); @@ -145,6 +189,39 @@ public static function register(): void $span->setAttributes($attributes); Context::storage()->attach($span->storeInContext($parent)); + if (ContextPropagation::isEnabled() && $sqlStatement !== self::UNDEFINED) { + if (array_key_exists(DbAttributes::DB_SYSTEM_NAME, $attributes)) { + /** @psalm-suppress PossiblyInvalidCast */ + switch ((string) $attributes[DbAttributes::DB_SYSTEM_NAME]) { + case 'postgresql': + case 'mysql': + $comments = []; + if ($contextPropagator !== null) { + // Propagator passed by user + $contextPropagator->inject($comments); + } else { + // fallback to global propagator if user didn't pass one + Globals::propagator()->inject($comments); + } + // Inject comments into SQL statement + $sqlStatement = SqlCommentInjector::inject($sqlStatement, $comments); + if (ContextPropagation::isAttributeEnabled()) { + $span->setAttributes([ + DbAttributes::DB_QUERY_TEXT => $sqlStatement, + ]); + } + + return [ + 0 => $sqlStatement, + ]; + default: + // Do nothing, not a database we want to propagate + break; + } + } + } + + return []; }, post: static function (PDO $pdo, array $params, mixed $statement, ?Throwable $exception) { self::end($exception); diff --git a/src/Instrumentation/PDO/src/PDOTracker.php b/src/Instrumentation/PDO/src/PDOTracker.php index 4753a0baa..d458238ec 100644 --- a/src/Instrumentation/PDO/src/PDOTracker.php +++ b/src/Instrumentation/PDO/src/PDOTracker.php @@ -5,7 +5,8 @@ namespace OpenTelemetry\Contrib\Instrumentation\PDO; use OpenTelemetry\API\Trace\SpanContextInterface; -use OpenTelemetry\SemConv\TraceAttributes; +use OpenTelemetry\SemConv\Attributes\DbAttributes; +use OpenTelemetry\SemConv\Attributes\ServerAttributes; use PDO; use PDOStatement; use WeakMap; @@ -74,11 +75,11 @@ public function trackPdoAttributes(PDO $pdo, string $dsn): array /** @var string $dbSystem */ $dbSystem = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME); /** @psalm-suppress InvalidArrayAssignment */ - $attributes[TraceAttributes::DB_SYSTEM_NAME] = self::mapDriverNameToAttribute($dbSystem); + $attributes[DbAttributes::DB_SYSTEM_NAME] = self::mapDriverNameToAttribute($dbSystem); } catch (\Error) { // if we caught an exception, the driver is likely not supporting the operation, default to "other" /** @psalm-suppress PossiblyInvalidArrayAssignment */ - $attributes[TraceAttributes::DB_SYSTEM_NAME] = 'other_sql'; + $attributes[DbAttributes::DB_SYSTEM_NAME] = 'other_sql'; } $this->pdoToAttributesMap[$pdo] = $attributes; @@ -135,18 +136,18 @@ private static function extractAttributesFromDSN(string $dsn): array { $attributes = []; if (str_starts_with($dsn, 'sqlite::memory:')) { - $attributes[TraceAttributes::DB_SYSTEM_NAME] = 'sqlite'; - $attributes[TraceAttributes::DB_NAMESPACE] = 'memory'; + $attributes[DbAttributes::DB_SYSTEM_NAME] = 'sqlite'; + $attributes[DbAttributes::DB_NAMESPACE] = 'memory'; return $attributes; } elseif (str_starts_with($dsn, 'sqlite:')) { - $attributes[TraceAttributes::DB_SYSTEM_NAME] = 'sqlite'; - $attributes[TraceAttributes::DB_NAMESPACE] = substr($dsn, 7); + $attributes[DbAttributes::DB_SYSTEM_NAME] = 'sqlite'; + $attributes[DbAttributes::DB_NAMESPACE] = substr($dsn, 7); return $attributes; } elseif (str_starts_with($dsn, 'sqlite')) { - $attributes[TraceAttributes::DB_SYSTEM_NAME] = 'sqlite'; - $attributes[TraceAttributes::DB_NAMESPACE] = $dsn; + $attributes[DbAttributes::DB_SYSTEM_NAME] = 'sqlite'; + $attributes[DbAttributes::DB_NAMESPACE] = $dsn; return $attributes; } @@ -156,18 +157,18 @@ private static function extractAttributesFromDSN(string $dsn): array if (preg_match('/Server=([^,;]+)(?:,([0-9]+))?/', $dsn, $serverMatches)) { $server = $serverMatches[1]; if ($server !== '') { - $attributes[TraceAttributes::SERVER_ADDRESS] = $server; + $attributes[ServerAttributes::SERVER_ADDRESS] = $server; } if (isset($serverMatches[2]) && $serverMatches[2] !== '') { - $attributes[TraceAttributes::SERVER_PORT] = (int) $serverMatches[2]; + $attributes[ServerAttributes::SERVER_PORT] = (int) $serverMatches[2]; } } if (preg_match('/Database=([^;]*)/', $dsn, $dbMatches)) { $dbname = $dbMatches[1]; if ($dbname !== '') { - $attributes[TraceAttributes::DB_NAMESPACE] = $dbname; + $attributes[DbAttributes::DB_NAMESPACE] = $dbname; } } @@ -186,33 +187,33 @@ private static function extractAttributesFromDSN(string $dsn): array if (preg_match('/host=([^;]*)/', $dsn, $matches)) { $host = $matches[1]; if ($host !== '') { - $attributes[TraceAttributes::SERVER_ADDRESS] = $host; + $attributes[ServerAttributes::SERVER_ADDRESS] = $host; } } elseif (preg_match('/mysql:([^;:]+)/', $dsn, $hostMatches)) { $host = $hostMatches[1]; if ($host !== '' && $host !== 'dbname') { - $attributes[TraceAttributes::SERVER_ADDRESS] = $host; + $attributes[ServerAttributes::SERVER_ADDRESS] = $host; } } // Extract port information if (preg_match('/port=([0-9]+)/', $dsn, $portMatches)) { $port = (int) $portMatches[1]; - $attributes[TraceAttributes::SERVER_PORT] = $port; + $attributes[ServerAttributes::SERVER_PORT] = $port; } elseif (preg_match('/[.0-9]+:([0-9]+)/', $dsn, $portMatches)) { // This pattern matches IP:PORT format like 127.0.0.1:3308 $port = (int) $portMatches[1]; - $attributes[TraceAttributes::SERVER_PORT] = $port; + $attributes[ServerAttributes::SERVER_PORT] = $port; } elseif (preg_match('/:([0-9]+)/', $dsn, $portMatches)) { $port = (int) $portMatches[1]; - $attributes[TraceAttributes::SERVER_PORT] = $port; + $attributes[ServerAttributes::SERVER_PORT] = $port; } // Extract database name if (preg_match('/dbname=([^;]*)/', $dsn, $matches)) { $dbname = $matches[1]; if ($dbname !== '') { - $attributes[TraceAttributes::DB_NAMESPACE] = $dbname; + $attributes[DbAttributes::DB_NAMESPACE] = $dbname; } } diff --git a/src/Instrumentation/PDO/src/SqlCommentInjector.php b/src/Instrumentation/PDO/src/SqlCommentInjector.php new file mode 100644 index 000000000..a26d2b2aa --- /dev/null +++ b/src/Instrumentation/PDO/src/SqlCommentInjector.php @@ -0,0 +1,31 @@ + Utils::customUrlEncode($key) . "='" . Utils::customUrlEncode($value) . "'", + $comments, + array_keys($comments) + ), + ) . '*/'; + } + + private static function customUrlEncode(string $input): string + { + $encodedString = urlencode($input); + + // Since SQL uses '%' as a keyword, '%' is a by-product of url quoting + // e.g. foo,bar --> foo%2Cbar + // thus in our quoting, we need to escape it too to finally give + // foo,bar --> foo%%2Cbar + + return str_replace('%', '%%', $encodedString); + } +} diff --git a/src/Instrumentation/PDO/tests/Integration/PDOInstrumentationTest.php b/src/Instrumentation/PDO/tests/Integration/PDOInstrumentationTest.php index 9398c8701..3f408c013 100644 --- a/src/Instrumentation/PDO/tests/Integration/PDOInstrumentationTest.php +++ b/src/Instrumentation/PDO/tests/Integration/PDOInstrumentationTest.php @@ -14,7 +14,7 @@ use OpenTelemetry\SDK\Trace\SpanExporter\InMemoryExporter; use OpenTelemetry\SDK\Trace\SpanProcessor\SimpleSpanProcessor; use OpenTelemetry\SDK\Trace\TracerProvider; -use OpenTelemetry\SemConv\TraceAttributes; +use OpenTelemetry\SemConv\Attributes\DbAttributes; use OpenTelemetry\TestUtils\TraceStructureAssertionTrait; use PDO; use PHPUnit\Framework\TestCase; @@ -85,7 +85,7 @@ public function test_pdo_construct(): void $this->assertCount(1, $this->storage); $span = $this->storage->offsetGet(0); $this->assertSame('PDO::__construct', $span->getName()); - $this->assertEquals('sqlite', $span->getAttributes()->get(TraceAttributes::DB_SYSTEM_NAME)); + $this->assertEquals('sqlite', $span->getAttributes()->get(DbAttributes::DB_SYSTEM_NAME)); } /** @@ -112,7 +112,7 @@ public function test_pdo_sqlite_subclass(): void $this->assertCount(1, $this->storage); $span = $this->storage->offsetGet(0); $this->assertSame('PDO::connect', $span->getName()); - $this->assertEquals('sqlite', $span->getAttributes()->get(TraceAttributes::DB_SYSTEM_NAME)); + $this->assertEquals('sqlite', $span->getAttributes()->get(DbAttributes::DB_SYSTEM_NAME)); // Test that the subclass-specific methods work $db->createFunction('test_function', static fn ($value) => strtoupper($value)); @@ -121,7 +121,7 @@ public function test_pdo_sqlite_subclass(): void $db->exec($this->fillDB()); $span = $this->storage->offsetGet(1); $this->assertSame('PDO::exec', $span->getName()); - $this->assertEquals('sqlite', $span->getAttributes()->get(TraceAttributes::DB_SYSTEM_NAME)); + $this->assertEquals('sqlite', $span->getAttributes()->get(DbAttributes::DB_SYSTEM_NAME)); $this->assertCount(2, $this->storage); // Test that the custom function works @@ -129,7 +129,7 @@ public function test_pdo_sqlite_subclass(): void $this->assertEquals('HELLO', $result); $span = $this->storage->offsetGet(2); $this->assertSame('PDO::query', $span->getName()); - $this->assertEquals('sqlite', $span->getAttributes()->get(TraceAttributes::DB_SYSTEM_NAME)); + $this->assertEquals('sqlite', $span->getAttributes()->get(DbAttributes::DB_SYSTEM_NAME)); $this->assertCount(3, $this->storage); } @@ -148,32 +148,32 @@ public function test_statement_execution(): void $db->exec($statement); $span = $this->storage->offsetGet(1); $this->assertSame('PDO::exec', $span->getName()); - $this->assertEquals('sqlite', $span->getAttributes()->get(TraceAttributes::DB_SYSTEM_NAME)); + $this->assertEquals('sqlite', $span->getAttributes()->get(DbAttributes::DB_SYSTEM_NAME)); $this->assertFalse($db->inTransaction()); $this->assertCount(2, $this->storage); $sth = $db->prepare('SELECT * FROM `technology`'); $span = $this->storage->offsetGet(2); $this->assertSame('PDO::prepare', $span->getName()); - $this->assertEquals('sqlite', $span->getAttributes()->get(TraceAttributes::DB_SYSTEM_NAME)); + $this->assertEquals('sqlite', $span->getAttributes()->get(DbAttributes::DB_SYSTEM_NAME)); $this->assertCount(3, $this->storage); $sth->execute(); $span = $this->storage->offsetGet(3); $this->assertSame('PDOStatement::execute', $span->getName()); - $this->assertEquals('sqlite', $span->getAttributes()->get(TraceAttributes::DB_SYSTEM_NAME)); + $this->assertEquals('sqlite', $span->getAttributes()->get(DbAttributes::DB_SYSTEM_NAME)); $this->assertCount(4, $this->storage); $sth->fetchAll(); $span = $this->storage->offsetGet(4); $this->assertSame('PDOStatement::fetchAll', $span->getName()); - $this->assertEquals('sqlite', $span->getAttributes()->get(TraceAttributes::DB_SYSTEM_NAME)); + $this->assertEquals('sqlite', $span->getAttributes()->get(DbAttributes::DB_SYSTEM_NAME)); $this->assertCount(5, $this->storage); $db->query('SELECT * FROM `technology`'); $span = $this->storage->offsetGet(5); $this->assertSame('PDO::query', $span->getName()); - $this->assertEquals('sqlite', $span->getAttributes()->get(TraceAttributes::DB_SYSTEM_NAME)); + $this->assertEquals('sqlite', $span->getAttributes()->get(DbAttributes::DB_SYSTEM_NAME)); $this->assertCount(6, $this->storage); } @@ -183,7 +183,7 @@ public function test_transaction(): void $result = $db->beginTransaction(); $span = $this->storage->offsetGet(1); $this->assertSame('PDO::beginTransaction', $span->getName()); - $this->assertEquals('sqlite', $span->getAttributes()->get(TraceAttributes::DB_SYSTEM_NAME)); + $this->assertEquals('sqlite', $span->getAttributes()->get(DbAttributes::DB_SYSTEM_NAME)); $this->assertCount(2, $this->storage); $this->assertSame($result, true); @@ -192,7 +192,7 @@ public function test_transaction(): void $result = $db->commit(); $span = $this->storage->offsetGet(3); $this->assertSame('PDO::commit', $span->getName()); - $this->assertEquals('sqlite', $span->getAttributes()->get(TraceAttributes::DB_SYSTEM_NAME)); + $this->assertEquals('sqlite', $span->getAttributes()->get(DbAttributes::DB_SYSTEM_NAME)); $this->assertCount(4, $this->storage); $this->assertTrue($result); @@ -204,7 +204,7 @@ public function test_transaction(): void $result = $db->rollback(); $span = $this->storage->offsetGet(6); $this->assertSame('PDO::rollBack', $span->getName()); - $this->assertEquals('sqlite', $span->getAttributes()->get(TraceAttributes::DB_SYSTEM_NAME)); + $this->assertEquals('sqlite', $span->getAttributes()->get(DbAttributes::DB_SYSTEM_NAME)); $this->assertCount(7, $this->storage); $this->assertTrue($result); $this->assertFalse($db->inTransaction()); @@ -262,17 +262,17 @@ public function test_encode_db_statement_as_utf8(): void $db->prepare("SELECT id FROM technology WHERE id = '{$non_utf8_id}'"); $span_db_prepare = $this->storage->offsetGet(2); - $this->assertTrue(mb_check_encoding($span_db_prepare->getAttributes()->get(TraceAttributes::DB_QUERY_TEXT), 'UTF-8')); + $this->assertTrue(mb_check_encoding($span_db_prepare->getAttributes()->get(DbAttributes::DB_QUERY_TEXT), 'UTF-8')); $this->assertCount(3, $this->storage); $db->query("SELECT id FROM technology WHERE id = '{$non_utf8_id}'"); $span_db_query = $this->storage->offsetGet(3); - $this->assertTrue(mb_check_encoding($span_db_query->getAttributes()->get(TraceAttributes::DB_QUERY_TEXT), 'UTF-8')); + $this->assertTrue(mb_check_encoding($span_db_query->getAttributes()->get(DbAttributes::DB_QUERY_TEXT), 'UTF-8')); $this->assertCount(4, $this->storage); $db->exec("SELECT id FROM technology WHERE id = '{$non_utf8_id}'"); $span_db_exec = $this->storage->offsetGet(4); - $this->assertTrue(mb_check_encoding($span_db_exec->getAttributes()->get(TraceAttributes::DB_QUERY_TEXT), 'UTF-8')); + $this->assertTrue(mb_check_encoding($span_db_exec->getAttributes()->get(DbAttributes::DB_QUERY_TEXT), 'UTF-8')); $this->assertCount(5, $this->storage); } @@ -364,35 +364,35 @@ public function test_span_hierarchy_with_pdo_operations(): void 'name' => 'PDO::__construct', 'kind' => SpanKind::KIND_CLIENT, 'attributes' => [ - TraceAttributes::DB_SYSTEM_NAME => 'sqlite', + DbAttributes::DB_SYSTEM_NAME => 'sqlite', ], ], [ 'name' => 'PDO::exec', 'kind' => SpanKind::KIND_CLIENT, 'attributes' => [ - TraceAttributes::DB_SYSTEM_NAME => 'sqlite', + DbAttributes::DB_SYSTEM_NAME => 'sqlite', ], ], [ 'name' => 'PDO::prepare', 'kind' => SpanKind::KIND_CLIENT, 'attributes' => [ - TraceAttributes::DB_SYSTEM_NAME => 'sqlite', + DbAttributes::DB_SYSTEM_NAME => 'sqlite', ], ], [ 'name' => 'PDOStatement::execute', 'kind' => SpanKind::KIND_CLIENT, 'attributes' => [ - TraceAttributes::DB_SYSTEM_NAME => 'sqlite', + DbAttributes::DB_SYSTEM_NAME => 'sqlite', ], ], [ 'name' => 'PDOStatement::fetchAll', 'kind' => SpanKind::KIND_CLIENT, 'attributes' => [ - TraceAttributes::DB_SYSTEM_NAME => 'sqlite', + DbAttributes::DB_SYSTEM_NAME => 'sqlite', ], ], ], diff --git a/src/Instrumentation/PDO/tests/Unit/ContextPropagationTest.php b/src/Instrumentation/PDO/tests/Unit/ContextPropagationTest.php new file mode 100644 index 000000000..943c79cc4 --- /dev/null +++ b/src/Instrumentation/PDO/tests/Unit/ContextPropagationTest.php @@ -0,0 +1,39 @@ +assertTrue($result); + } + + public function testIsEnabledReturnsFalse() + { + $_SERVER['OTEL_PHP_INSTRUMENTATION_PDO_CONTEXT_PROPAGATION'] = false; + $result = ContextPropagation::isEnabled(); + $this->assertFalse($result); + } + + public function testIsAttributeEnabledReturnsTrue() + { + $_SERVER['OTEL_PHP_INSTRUMENTATION_PDO_CONTEXT_PROPAGATION_ATTRIBUTE'] = true; + $result = ContextPropagation::isAttributeEnabled(); + $this->assertTrue($result); + } + + public function testIsAttributeEnabledReturnsFalse() + { + $_SERVER['OTEL_PHP_INSTRUMENTATION_PDO_CONTEXT_PROPAGATION_ATTRIBUTE'] = false; + $result = ContextPropagation::isAttributeEnabled(); + $this->assertFalse($result); + } +} diff --git a/src/Instrumentation/PDO/tests/Unit/ContextPropagatorFactoryTest.php b/src/Instrumentation/PDO/tests/Unit/ContextPropagatorFactoryTest.php new file mode 100644 index 000000000..930436c4e --- /dev/null +++ b/src/Instrumentation/PDO/tests/Unit/ContextPropagatorFactoryTest.php @@ -0,0 +1,52 @@ +create(); + if ($expected === null) { + $this->assertNull($propagator); + } else { + $this->assertInstanceOf($expected, $propagator); + } + putenv('OTEL_PHP_INSTRUMENTATION_PDO_CONTEXT_PROPAGATORS'); + } + + public static function propagatorsProvider(): array + { + return [ + [KnownValues::VALUE_BAGGAGE, BaggagePropagator::class], + [KnownValues::VALUE_TRACECONTEXT, TraceContextPropagator::class], + [KnownValues::VALUE_NONE, null], + [sprintf('%s,%s', KnownValues::VALUE_TRACECONTEXT, KnownValues::VALUE_BAGGAGE), MultiTextMapPropagator::class], + ['', null], + ['invalid', null], + ]; + } +} diff --git a/src/Instrumentation/PDO/tests/Unit/PDOAttributeTrackerTest.php b/src/Instrumentation/PDO/tests/Unit/PDOAttributeTrackerTest.php index 44937122e..9516a0af4 100644 --- a/src/Instrumentation/PDO/tests/Unit/PDOAttributeTrackerTest.php +++ b/src/Instrumentation/PDO/tests/Unit/PDOAttributeTrackerTest.php @@ -6,7 +6,7 @@ use OpenTelemetry\API\Trace\Span; use OpenTelemetry\Contrib\Instrumentation\PDO\PDOTracker; -use OpenTelemetry\SemConv\TraceAttributes; +use OpenTelemetry\SemConv\Attributes\DbAttributes; use PHPUnit\Framework\TestCase; class PDOAttributeTrackerTest extends TestCase @@ -22,20 +22,20 @@ public function testPdoCanBeTracked(): void $span = Span::getInvalid(); /** @psalm-suppress InvalidArgument */ - $this->assertContains(TraceAttributes::DB_SYSTEM_NAME, array_keys($attributes)); + $this->assertContains(DbAttributes::DB_SYSTEM_NAME, array_keys($attributes)); /** @psalm-suppress InvalidArgument */ - $this->assertContains(TraceAttributes::DB_NAMESPACE, array_keys($attributes)); + $this->assertContains(DbAttributes::DB_NAMESPACE, array_keys($attributes)); /** @psalm-suppress InvalidArrayAccess */ - $this->assertSame('memory', $attributes[TraceAttributes::DB_NAMESPACE]); + $this->assertSame('memory', $attributes[DbAttributes::DB_NAMESPACE]); $stmt = $pdo->prepare('SELECT NULL LIMIT 0;'); $objectMap->trackStatement($stmt, $pdo, $span->getContext()); $attributes = $objectMap->trackedAttributesForStatement($stmt); /** @psalm-suppress InvalidArgument */ - $this->assertContains(TraceAttributes::DB_SYSTEM_NAME, array_keys($attributes)); + $this->assertContains(DbAttributes::DB_SYSTEM_NAME, array_keys($attributes)); /** @psalm-suppress InvalidArrayAccess */ - $this->assertEquals('sqlite', $attributes[TraceAttributes::DB_SYSTEM_NAME]); + $this->assertEquals('sqlite', $attributes[DbAttributes::DB_SYSTEM_NAME]); $this->assertSame($span->getContext(), $objectMap->getSpanForPreparedStatement($stmt)); } } diff --git a/src/Instrumentation/PDO/tests/Unit/PDOTrackerTest.php b/src/Instrumentation/PDO/tests/Unit/PDOTrackerTest.php index 5049f2039..3407bf5b8 100644 --- a/src/Instrumentation/PDO/tests/Unit/PDOTrackerTest.php +++ b/src/Instrumentation/PDO/tests/Unit/PDOTrackerTest.php @@ -5,7 +5,8 @@ namespace OpenTelemetry\Tests\Instrumentation\PDO\tests\Unit; use OpenTelemetry\Contrib\Instrumentation\PDO\PDOTracker; -use OpenTelemetry\SemConv\TraceAttributes; +use OpenTelemetry\SemConv\Attributes\DbAttributes; +use OpenTelemetry\SemConv\Attributes\ServerAttributes; use PHPUnit\Framework\TestCase; use ReflectionMethod; @@ -39,106 +40,106 @@ public function dsnProvider(): array 'standard format with host and port' => [ 'mysql:host=localhost;port=3306;dbname=test', [ - TraceAttributes::SERVER_ADDRESS => 'localhost', - TraceAttributes::SERVER_PORT => 3306, - TraceAttributes::DB_NAMESPACE => 'test', + ServerAttributes::SERVER_ADDRESS => 'localhost', + ServerAttributes::SERVER_PORT => 3306, + DbAttributes::DB_NAMESPACE => 'test', ], ], 'format with host but no port' => [ 'mysql:host=localhost;dbname=test', [ - TraceAttributes::SERVER_ADDRESS => 'localhost', - TraceAttributes::DB_NAMESPACE => 'test', + ServerAttributes::SERVER_ADDRESS => 'localhost', + DbAttributes::DB_NAMESPACE => 'test', ], ], 'format with port but using alternative format' => [ 'mysql:localhost:3306;dbname=test', [ - TraceAttributes::SERVER_ADDRESS => 'localhost', - TraceAttributes::SERVER_PORT => 3306, - TraceAttributes::DB_NAMESPACE => 'test', + ServerAttributes::SERVER_ADDRESS => 'localhost', + ServerAttributes::SERVER_PORT => 3306, + DbAttributes::DB_NAMESPACE => 'test', ], ], 'format with neither host parameter nor port parameter' => [ 'mysql:localhost;dbname=test', [ - TraceAttributes::SERVER_ADDRESS => 'localhost', - TraceAttributes::DB_NAMESPACE => 'test', + ServerAttributes::SERVER_ADDRESS => 'localhost', + DbAttributes::DB_NAMESPACE => 'test', ], ], 'format with IP address as host' => [ 'mysql:host=127.0.0.1;port=3306;dbname=test', [ - TraceAttributes::SERVER_ADDRESS => '127.0.0.1', - TraceAttributes::SERVER_PORT => 3306, - TraceAttributes::DB_NAMESPACE => 'test', + ServerAttributes::SERVER_ADDRESS => '127.0.0.1', + ServerAttributes::SERVER_PORT => 3306, + DbAttributes::DB_NAMESPACE => 'test', ], ], 'format with domain name as host' => [ 'mysql:host=example.com;port=3306;dbname=test', [ - TraceAttributes::SERVER_ADDRESS => 'example.com', - TraceAttributes::SERVER_PORT => 3306, - TraceAttributes::DB_NAMESPACE => 'test', + ServerAttributes::SERVER_ADDRESS => 'example.com', + ServerAttributes::SERVER_PORT => 3306, + DbAttributes::DB_NAMESPACE => 'test', ], ], 'PostgreSQL format' => [ 'pgsql:host=localhost;port=5432;dbname=test', [ - TraceAttributes::SERVER_ADDRESS => 'localhost', - TraceAttributes::SERVER_PORT => 5432, - TraceAttributes::DB_NAMESPACE => 'test', + ServerAttributes::SERVER_ADDRESS => 'localhost', + ServerAttributes::SERVER_PORT => 5432, + DbAttributes::DB_NAMESPACE => 'test', ], ], 'SQLite format' => [ 'sqlite:/path/to/database.sqlite', [ - TraceAttributes::DB_SYSTEM_NAME => 'sqlite', - TraceAttributes::DB_NAMESPACE => '/path/to/database.sqlite', + DbAttributes::DB_SYSTEM_NAME => 'sqlite', + DbAttributes::DB_NAMESPACE => '/path/to/database.sqlite', ], ], 'SQLite in-memory format' => [ 'sqlite::memory:', [ - TraceAttributes::DB_SYSTEM_NAME => 'sqlite', - TraceAttributes::DB_NAMESPACE => 'memory', + DbAttributes::DB_SYSTEM_NAME => 'sqlite', + DbAttributes::DB_NAMESPACE => 'memory', ], ], 'Oracle format' => [ 'oci:host=localhost;port=1521;dbname=test', [ - TraceAttributes::SERVER_ADDRESS => 'localhost', - TraceAttributes::SERVER_PORT => 1521, - TraceAttributes::DB_NAMESPACE => 'test', + ServerAttributes::SERVER_ADDRESS => 'localhost', + ServerAttributes::SERVER_PORT => 1521, + DbAttributes::DB_NAMESPACE => 'test', ], ], 'SQL Server format' => [ 'sqlsrv:Server=localhost,1433;Database=test', [ - TraceAttributes::SERVER_ADDRESS => 'localhost', - TraceAttributes::SERVER_PORT => 1433, - TraceAttributes::DB_NAMESPACE => 'test', + ServerAttributes::SERVER_ADDRESS => 'localhost', + ServerAttributes::SERVER_PORT => 1433, + DbAttributes::DB_NAMESPACE => 'test', ], ], 'MySQL format with host in DSN prefix' => [ 'mysql:dbname=test;charset=utf8', [ - TraceAttributes::DB_NAMESPACE => 'test', + DbAttributes::DB_NAMESPACE => 'test', ], ], 'MySQL format with host in DSN prefix and port' => [ 'mysql:dbname=test;port=3307;charset=utf8', [ - TraceAttributes::SERVER_PORT => 3307, - TraceAttributes::DB_NAMESPACE => 'test', + ServerAttributes::SERVER_PORT => 3307, + DbAttributes::DB_NAMESPACE => 'test', ], ], 'MySQL format with host in DSN prefix and colon port' => [ 'mysql:127.0.0.1:3308;dbname=test', [ - TraceAttributes::SERVER_ADDRESS => '127.0.0.1', - TraceAttributes::SERVER_PORT => 3308, - TraceAttributes::DB_NAMESPACE => 'test', + ServerAttributes::SERVER_ADDRESS => '127.0.0.1', + ServerAttributes::SERVER_PORT => 3308, + DbAttributes::DB_NAMESPACE => 'test', ], ], ]; diff --git a/src/Instrumentation/PDO/tests/Unit/SqlCommentInjectorTest.php b/src/Instrumentation/PDO/tests/Unit/SqlCommentInjectorTest.php new file mode 100644 index 000000000..dad020751 --- /dev/null +++ b/src/Instrumentation/PDO/tests/Unit/SqlCommentInjectorTest.php @@ -0,0 +1,51 @@ +assertTrue($result); + } + + public function testIsPrependReturnsFalse() + { + $_SERVER['OTEL_PHP_INSTRUMENTATION_PDO_SQL_COMMENTER_PREPEND'] = false; + $result = SqlCommentInjector::isPrepend(); + $this->assertFalse($result); + } + + public function testInjectPrepend() + { + $_SERVER['OTEL_PHP_INSTRUMENTATION_PDO_SQL_COMMENTER_PREPEND'] = true; + $comments = [ + 'key1' => 'value1', + 'key2' => 'value2', + 'key3' => 'value3', + ]; + $query = 'SELECT 1;'; + $result = SqlCommentInjector::inject($query, $comments); + $this->assertEquals("/*key1='value1',key2='value2',key3='value3'*/SELECT 1;", $result); + } + + public function testInjectAppend() + { + $_SERVER['OTEL_PHP_INSTRUMENTATION_PDO_SQL_COMMENTER_PREPEND'] = false; + $comments = [ + 'key1' => 'value1', + 'key2' => 'value2', + 'key3' => 'value3', + ]; + $query = 'SELECT 1;'; + $result = SqlCommentInjector::inject($query, $comments); + $this->assertEquals("SELECT 1/*key1='value1',key2='value2',key3='value3'*/;", $result); + } +} diff --git a/src/Instrumentation/PDO/tests/Unit/UtilsTest.php b/src/Instrumentation/PDO/tests/Unit/UtilsTest.php new file mode 100644 index 000000000..dc1e8ba80 --- /dev/null +++ b/src/Instrumentation/PDO/tests/Unit/UtilsTest.php @@ -0,0 +1,36 @@ +assertEquals("/*key1='value1',key2='value2'*/", Utils::formatComments(['key1' => 'value1', 'key2' => 'value2'])); + } + + public function testFormatCommentsWithoutKeys(): void + { + $this->assertEquals('', Utils::formatComments([])); + } + + public function testFormatCommentsWithSpecialCharKeys(): void + { + $this->assertEquals("/*key1='value1%%40',key2='value2'*/", Utils::formatComments(['key1' => 'value1@', 'key2' => 'value2'])); + } + + public function testFormatCommentsWithPlaceholder(): void + { + $this->assertEquals("/*key1='value1%%3F',key2='value2'*/", Utils::formatComments(['key1' => 'value1?', 'key2' => 'value2'])); + } + + public function testFormatCommentsWithNamedPlaceholder(): void + { + $this->assertEquals("/*key1='%%3Anamed',key2='value2'*/", Utils::formatComments(['key1' => ':named', 'key2' => 'value2'])); + } +}