From 9e21345ee4e832960a71416a6d143322196d31f3 Mon Sep 17 00:00:00 2001 From: Alexandre Rulleau Date: Mon, 18 May 2026 16:36:35 +0200 Subject: [PATCH 1/3] fix(PDO): strip libpq-style paired quotes from dbname DSN value Laravel's Postgres connector wraps the dbname in single quotes when building the DSN (`dbname='milk'`). libpq strips those at connect time so the database name on the wire is `milk`, but the tracer's DSN regex captured the value verbatim and tagged `db.name='milk'`. The agent's tag-value sanitizer then rewrote the disallowed quote to `_`, emitting `peer.db.name=_milk` and splitting the inferred-entity peer.* tuple from other tracers that emit the unquoted name. Match libpq's behavior: peel paired wrapping single quotes from the captured dbname; leave unpaired quotes visible so real config typos still surface. Fixes APMS-19464. --- .../Integrations/PDO/PDOIntegration.php | 7 +++ .../Integrations/PDO/PDOIntegrationTest.php | 43 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 tests/Unit/Integrations/PDO/PDOIntegrationTest.php diff --git a/src/DDTrace/Integrations/PDO/PDOIntegration.php b/src/DDTrace/Integrations/PDO/PDOIntegration.php index f731bdc3df1..6a1a5bb4c14 100644 --- a/src/DDTrace/Integrations/PDO/PDOIntegration.php +++ b/src/DDTrace/Integrations/PDO/PDOIntegration.php @@ -247,6 +247,13 @@ private static function parseDsn($dsn) $tags[Tag::DB_SYSTEM] = $dbSystem; $tags[Tag::DB_TYPE] = $dbSystem; // db.type is DD equivalent to db.system in OpenTelemetry, used for SQL spans obfuscation + // libpq strips wrapping single quotes from key=value pairs, so `dbname='milk'` + // connects to `milk`. Match that here — otherwise the agent's tag sanitizer + // rewrites the captured `'milk'` as `_milk`, which splits the inferred-entity + // peer.* tuple from other tracers that emit the unquoted name (APMS-19464). + if (strlen($db) >= 2 && $db[0] === "'" && substr($db, -1) === "'") { + $db = substr($db, 1, -1); + } if ($db !== "") { $tags[Tag::DB_NAME] = $db; } diff --git a/tests/Unit/Integrations/PDO/PDOIntegrationTest.php b/tests/Unit/Integrations/PDO/PDOIntegrationTest.php new file mode 100644 index 00000000000..522f5aaf1c9 --- /dev/null +++ b/tests/Unit/Integrations/PDO/PDOIntegrationTest.php @@ -0,0 +1,43 @@ +setAccessible(true); + $tags = $method->invoke(null, $dsn); + + if ($expectedDbName === null) { + $this->assertArrayNotHasKey(Tag::DB_NAME, $tags); + } else { + $this->assertSame($expectedDbName, $tags[Tag::DB_NAME]); + } + } + + public function dsnDbNameCases() + { + // libpq strips paired wrapping single quotes from key=value pairs, so the tracer must too — + // otherwise `dbname='milk'` produces `db.name='milk'`, which the agent sanitizer rewrites + // as `_milk`, splitting the inferred-entity peer.* tuple from other tracers (APMS-19464). + return [ + 'unquoted dbname' => ['pgsql:host=h;dbname=milk', 'milk'], + 'paired single quotes stripped' => ["pgsql:host=h;dbname='milk'", 'milk'], + 'double quotes preserved as-is' => ['pgsql:host=h;dbname="milk"', '"milk"'], + 'unpaired leading quote preserved' => ["pgsql:host=h;dbname='milk", "'milk"], + 'unpaired trailing quote preserved' => ["pgsql:host=h;dbname=milk'", "milk'"], + 'empty quoted dbname omitted' => ["pgsql:host=h;dbname=''", null], + 'database= alias also stripped' => ["mysql:host=h;database='foo'", 'foo'], + ]; + } +} From 11b382f9b93915e9e3bd80b1502703a14e66f2d8 Mon Sep 17 00:00:00 2001 From: Alexandre Rulleau Date: Mon, 18 May 2026 16:40:23 +0200 Subject: [PATCH 2/3] chore: shorten inline comment --- src/DDTrace/Integrations/PDO/PDOIntegration.php | 5 +---- tests/Unit/Integrations/PDO/PDOIntegrationTest.php | 3 --- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/DDTrace/Integrations/PDO/PDOIntegration.php b/src/DDTrace/Integrations/PDO/PDOIntegration.php index 6a1a5bb4c14..73e646c8f98 100644 --- a/src/DDTrace/Integrations/PDO/PDOIntegration.php +++ b/src/DDTrace/Integrations/PDO/PDOIntegration.php @@ -247,10 +247,7 @@ private static function parseDsn($dsn) $tags[Tag::DB_SYSTEM] = $dbSystem; $tags[Tag::DB_TYPE] = $dbSystem; // db.type is DD equivalent to db.system in OpenTelemetry, used for SQL spans obfuscation - // libpq strips wrapping single quotes from key=value pairs, so `dbname='milk'` - // connects to `milk`. Match that here — otherwise the agent's tag sanitizer - // rewrites the captured `'milk'` as `_milk`, which splits the inferred-entity - // peer.* tuple from other tracers that emit the unquoted name (APMS-19464). + // Match libpq: strip paired wrapping single quotes from dbname (APMS-19464). if (strlen($db) >= 2 && $db[0] === "'" && substr($db, -1) === "'") { $db = substr($db, 1, -1); } diff --git a/tests/Unit/Integrations/PDO/PDOIntegrationTest.php b/tests/Unit/Integrations/PDO/PDOIntegrationTest.php index 522f5aaf1c9..7172e00c039 100644 --- a/tests/Unit/Integrations/PDO/PDOIntegrationTest.php +++ b/tests/Unit/Integrations/PDO/PDOIntegrationTest.php @@ -27,9 +27,6 @@ public function testParseDsnDbNameQuoteHandling($dsn, $expectedDbName) public function dsnDbNameCases() { - // libpq strips paired wrapping single quotes from key=value pairs, so the tracer must too — - // otherwise `dbname='milk'` produces `db.name='milk'`, which the agent sanitizer rewrites - // as `_milk`, splitting the inferred-entity peer.* tuple from other tracers (APMS-19464). return [ 'unquoted dbname' => ['pgsql:host=h;dbname=milk', 'milk'], 'paired single quotes stripped' => ["pgsql:host=h;dbname='milk'", 'milk'], From 272f0f07375384a780555a878ae7110788ba46a2 Mon Sep 17 00:00:00 2001 From: Alexandre Rulleau Date: Mon, 18 May 2026 16:45:59 +0200 Subject: [PATCH 3/3] test(PDO): move parseDsn quote-handling cases into PDOTest --- tests/Integrations/PDO/PDOTest.php | 31 ++++++++++++++ .../Integrations/PDO/PDOIntegrationTest.php | 40 ------------------- 2 files changed, 31 insertions(+), 40 deletions(-) delete mode 100644 tests/Unit/Integrations/PDO/PDOIntegrationTest.php diff --git a/tests/Integrations/PDO/PDOTest.php b/tests/Integrations/PDO/PDOTest.php index a4bfe816ab5..dd77c790471 100644 --- a/tests/Integrations/PDO/PDOTest.php +++ b/tests/Integrations/PDO/PDOTest.php @@ -2,9 +2,11 @@ namespace DDTrace\Tests\Integrations\PDO; +use DDTrace\Integrations\PDO\PDOIntegration; use DDTrace\Tag; use DDTrace\Tests\Common\IntegrationTestCase; use DDTrace\Tests\Common\SpanAssertion; +use ReflectionMethod; use function DDTrace\close_span; use function DDTrace\start_trace_span; @@ -907,4 +909,33 @@ protected function baseTags($expectPeerService = false) return $tags; } + + /** + * @dataProvider dsnDbNameCases + */ + public function testParseDsnDbNameQuoteHandling($dsn, $expectedDbName) + { + $method = new ReflectionMethod(PDOIntegration::class, 'parseDsn'); + $method->setAccessible(true); + $tags = $method->invoke(null, $dsn); + + if ($expectedDbName === null) { + $this->assertArrayNotHasKey(Tag::DB_NAME, $tags); + } else { + $this->assertSame($expectedDbName, $tags[Tag::DB_NAME]); + } + } + + public function dsnDbNameCases() + { + return [ + 'unquoted dbname' => ['pgsql:host=h;dbname=milk', 'milk'], + 'paired single quotes stripped' => ["pgsql:host=h;dbname='milk'", 'milk'], + 'double quotes preserved as-is' => ['pgsql:host=h;dbname="milk"', '"milk"'], + 'unpaired leading quote preserved' => ["pgsql:host=h;dbname='milk", "'milk"], + 'unpaired trailing quote preserved' => ["pgsql:host=h;dbname=milk'", "milk'"], + 'empty quoted dbname omitted' => ["pgsql:host=h;dbname=''", null], + 'database= alias also stripped' => ["mysql:host=h;database='foo'", 'foo'], + ]; + } } diff --git a/tests/Unit/Integrations/PDO/PDOIntegrationTest.php b/tests/Unit/Integrations/PDO/PDOIntegrationTest.php deleted file mode 100644 index 7172e00c039..00000000000 --- a/tests/Unit/Integrations/PDO/PDOIntegrationTest.php +++ /dev/null @@ -1,40 +0,0 @@ -setAccessible(true); - $tags = $method->invoke(null, $dsn); - - if ($expectedDbName === null) { - $this->assertArrayNotHasKey(Tag::DB_NAME, $tags); - } else { - $this->assertSame($expectedDbName, $tags[Tag::DB_NAME]); - } - } - - public function dsnDbNameCases() - { - return [ - 'unquoted dbname' => ['pgsql:host=h;dbname=milk', 'milk'], - 'paired single quotes stripped' => ["pgsql:host=h;dbname='milk'", 'milk'], - 'double quotes preserved as-is' => ['pgsql:host=h;dbname="milk"', '"milk"'], - 'unpaired leading quote preserved' => ["pgsql:host=h;dbname='milk", "'milk"], - 'unpaired trailing quote preserved' => ["pgsql:host=h;dbname=milk'", "milk'"], - 'empty quoted dbname omitted' => ["pgsql:host=h;dbname=''", null], - 'database= alias also stripped' => ["mysql:host=h;database='foo'", 'foo'], - ]; - } -}