Skip to content

Commit f1bfdcf

Browse files
committed
add Doctrine span links and more SemConv
track doctrine prepared statements, and when they are executed create a span link back to the span that prepared. add some more SemConv to db spans, particularly db.operation.name align span names with how PDO does it (eg Doctrine::prepare)
1 parent 3f003f3 commit f1bfdcf

File tree

4 files changed

+169
-41
lines changed

4 files changed

+169
-41
lines changed

src/Instrumentation/Doctrine/src/AttributesResolver.php

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ public static function get(string $attributeName, array $params): string|int|nul
5555
/**
5656
* Resolve attribute `server.address`
5757
*/
58-
private static function getServerAddress(array $params): string
58+
private static function getServerAddress(array $params): ?string
5959
{
60-
return $params[1][0]['host'] ?? 'unknown';
60+
return $params[1][0]['host'] ?? null;
6161
}
6262

6363
/**
@@ -107,24 +107,54 @@ private static function getDbCollectionName(array $params): string
107107

108108
/**
109109
* Resolve attribute `db.query.text`
110-
* No sanitization is implemented because implicitly the query is expected to be expressed as a preparated statement
110+
* No sanitization is implemented because implicitly the query is expected to be expressed as a prepared statement
111111
* which happen automatically in Doctrine if parameters are bound to the query.
112112
*/
113113
private static function getDbQueryText(array $params): string
114114
{
115115
return $params[1][0] ?? 'undefined';
116116
}
117117

118-
private static function getDbNamespace(array $params): string
118+
private static function getDbNamespace(array $params): ?string
119119
{
120-
return $params[1][0]['dbname'] ?? 'unknown';
120+
return $params[1][0]['dbname'] ?? null;
121+
}
122+
123+
public static function getTarget(array $params): ?string
124+
{
125+
$query = $params[0] ?? null;
126+
127+
if (!$query) {
128+
return null;
129+
}
130+
131+
// Fetch target name
132+
$matches = [];
133+
preg_match_all('/( from| into| update| join)\s*([a-zA-Z0-9`"[\]_]+)/i', $query, $matches);
134+
135+
$targetName = null;
136+
if ($matches !== []) {
137+
$targetName = $matches[2][0] ?? '';
138+
}
139+
//strip quotes and backticks from the target name
140+
$targetName = str_replace(['`', '"', '[', ']'], '', $targetName);
141+
142+
return $targetName;
121143
}
122144

123145
/**
124146
* Resolve attribute `db.query.summary`
125147
* See https://opentelemetry.io/docs/specs/semconv/database/database-spans/#generating-a-summary-of-the-query-text
126148
*/
127149
public static function getDbQuerySummary(array $params): string
150+
{
151+
$operationName = self::getDbOperationName($params);
152+
$targetName = self::getTarget($params);
153+
154+
return $operationName . ($targetName ? ' ' . $targetName : '');
155+
}
156+
157+
public static function getDbOperationName(array $params): string
128158
{
129159
$query = $params[0] ?? null;
130160

@@ -136,19 +166,6 @@ public static function getDbQuerySummary(array $params): string
136166
$operationName = explode(' ', $query);
137167
$operationName = $operationName[0];
138168

139-
// Fetch target name
140-
$matches = [];
141-
preg_match_all('/( from| into| update| join)\s*([a-zA-Z0-9`"[\]_]+)/i', $query, $matches);
142-
143-
$targetName = null;
144-
if (strtolower($operationName) == 'select') {
145-
if ($matches[2]) {
146-
$targetName = implode(' ', $matches[2]);
147-
}
148-
} elseif ($matches) {
149-
$targetName = $matches[2][0] ?? '';
150-
}
151-
152-
return $operationName . ($targetName ? ' ' . $targetName : '');
169+
return $operationName;
153170
}
154171
}

src/Instrumentation/Doctrine/src/DoctrineInstrumentation.php

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44

55
namespace OpenTelemetry\Contrib\Instrumentation\Doctrine;
66

7+
use Doctrine\DBAL\Driver\Result as ResultInterface;
8+
use Doctrine\DBAL\Driver\Statement;
79
use OpenTelemetry\API\Instrumentation\CachedInstrumentation;
810
use OpenTelemetry\API\Trace\Span;
911
use OpenTelemetry\API\Trace\SpanBuilderInterface;
1012
use OpenTelemetry\API\Trace\SpanKind;
1113
use OpenTelemetry\API\Trace\StatusCode;
1214
use OpenTelemetry\Context\Context;
13-
1415
use function OpenTelemetry\Instrumentation\hook;
15-
1616
use OpenTelemetry\SemConv\TraceAttributes;
1717
use Throwable;
1818

@@ -23,18 +23,19 @@ class DoctrineInstrumentation
2323
public static function register(): void
2424
{
2525
$instrumentation = new CachedInstrumentation('io.opentelemetry.contrib.php.doctrine');
26+
$tracker = new DoctrineTracker();
2627

2728
hook(
2829
\Doctrine\DBAL\Driver::class,
2930
'connect',
3031
pre: static function (\Doctrine\DBAL\Driver $driver, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($instrumentation) {
3132
/** @psalm-suppress ArgumentTypeCoercion */
3233
$builder = self::makeBuilder($instrumentation, 'Doctrine\DBAL\Driver::connect', $function, $class, $filename, $lineno)
33-
->setSpanKind(SpanKind::KIND_CLIENT)
34-
->setAttribute(TraceAttributes::SERVER_ADDRESS, AttributesResolver::get(TraceAttributes::SERVER_ADDRESS, func_get_args()))
35-
->setAttribute(TraceAttributes::SERVER_PORT, AttributesResolver::get(TraceAttributes::SERVER_PORT, func_get_args()))
36-
->setAttribute(TraceAttributes::DB_SYSTEM_NAME, AttributesResolver::get(TraceAttributes::DB_SYSTEM_NAME, func_get_args()))
37-
->setAttribute(TraceAttributes::DB_NAMESPACE, AttributesResolver::get(TraceAttributes::DB_NAMESPACE, func_get_args()));
34+
->setSpanKind(SpanKind::KIND_CLIENT)
35+
->setAttribute(TraceAttributes::SERVER_ADDRESS, AttributesResolver::get(TraceAttributes::SERVER_ADDRESS, func_get_args()))
36+
->setAttribute(TraceAttributes::SERVER_PORT, AttributesResolver::get(TraceAttributes::SERVER_PORT, func_get_args()))
37+
->setAttribute(TraceAttributes::DB_SYSTEM_NAME, AttributesResolver::get(TraceAttributes::DB_SYSTEM_NAME, func_get_args()))
38+
->setAttribute(TraceAttributes::DB_NAMESPACE, AttributesResolver::get(TraceAttributes::DB_NAMESPACE, func_get_args()));
3839
$parent = Context::getCurrent();
3940
$span = $builder->startSpan();
4041
Context::storage()->attach($span->storeInContext($parent));
@@ -52,6 +53,8 @@ public static function register(): void
5253
$builder = self::makeBuilder($instrumentation, AttributesResolver::getDbQuerySummary($params), $function, $class, $filename, $lineno)
5354
->setSpanKind(SpanKind::KIND_CLIENT);
5455
$builder->setAttribute(TraceAttributes::DB_QUERY_TEXT, AttributesResolver::get(TraceAttributes::DB_QUERY_TEXT, func_get_args()));
56+
$builder->setAttribute(TraceAttributes::DB_OPERATION_NAME, AttributesResolver::getDbOperationName($params));
57+
$builder->setAttribute(TraceAttributes::DB_COLLECTION_NAME, AttributesResolver::getTarget($params));
5558
$parent = Context::getCurrent();
5659
$span = $builder->startSpan();
5760
Context::storage()->attach($span->storeInContext($parent));
@@ -68,7 +71,9 @@ public static function register(): void
6871
/** @psalm-suppress ArgumentTypeCoercion */
6972
$builder = self::makeBuilder($instrumentation, AttributesResolver::getDbQuerySummary($params), $function, $class, $filename, $lineno)
7073
->setSpanKind(SpanKind::KIND_CLIENT)
71-
->setAttribute(TraceAttributes::DB_QUERY_TEXT, AttributesResolver::get(TraceAttributes::DB_QUERY_TEXT, func_get_args()));
74+
->setAttribute(TraceAttributes::DB_QUERY_TEXT, AttributesResolver::get(TraceAttributes::DB_QUERY_TEXT, func_get_args()))
75+
->setAttribute(TraceAttributes::DB_OPERATION_NAME, AttributesResolver::getDbOperationName($params))
76+
->setAttribute(TraceAttributes::DB_COLLECTION_NAME, AttributesResolver::getTarget($params));
7277
$parent = Context::getCurrent();
7378
$span = $builder->startSpan();
7479

@@ -86,13 +91,22 @@ public static function register(): void
8691
/** @psalm-suppress ArgumentTypeCoercion */
8792
$builder = self::makeBuilder($instrumentation, AttributesResolver::getDbQuerySummary($params), $function, $class, $filename, $lineno)
8893
->setSpanKind(SpanKind::KIND_CLIENT)
89-
->setAttribute(TraceAttributes::DB_QUERY_TEXT, AttributesResolver::get(TraceAttributes::DB_QUERY_TEXT, func_get_args()));
94+
->setAttribute(TraceAttributes::DB_QUERY_TEXT, AttributesResolver::get(TraceAttributes::DB_QUERY_TEXT, func_get_args()))
95+
->setAttribute(TraceAttributes::DB_OPERATION_NAME, 'prepare');
9096
$parent = Context::getCurrent();
9197
$span = $builder->startSpan();
9298

9399
Context::storage()->attach($span->storeInContext($parent));
94100
},
95-
post: static function (\Doctrine\DBAL\Driver\Connection $connection, array $params, mixed $statement, ?Throwable $exception) {
101+
post: static function (\Doctrine\DBAL\Driver\Connection $connection, array $params, ?Statement $statement, ?Throwable $exception) use ($tracker) {
102+
if ($statement) {
103+
$scope = Context::storage()->scope();
104+
$context = $scope?->context();
105+
if ($context) {
106+
$span = Span::fromContext($context);
107+
$tracker->trackStatement($statement, $span->getContext());
108+
}
109+
}
96110
self::end($exception);
97111
}
98112
);
@@ -102,8 +116,9 @@ public static function register(): void
102116
'beginTransaction',
103117
pre: static function (\Doctrine\DBAL\Driver\Connection $connection, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($instrumentation) {
104118
/** @psalm-suppress ArgumentTypeCoercion */
105-
$builder = self::makeBuilder($instrumentation, 'Doctrine\DBAL\Driver\Connection::beginTransaction', $function, $class, $filename, $lineno)
106-
->setSpanKind(SpanKind::KIND_CLIENT);
119+
$builder = self::makeBuilder($instrumentation, 'Doctrine::beginTransaction', $function, $class, $filename, $lineno)
120+
->setSpanKind(SpanKind::KIND_CLIENT)
121+
->setAttribute(TraceAttributes::DB_OPERATION_NAME, 'begin');
107122
$parent = Context::getCurrent();
108123
$span = $builder->startSpan();
109124

@@ -119,8 +134,9 @@ public static function register(): void
119134
'commit',
120135
pre: static function (\Doctrine\DBAL\Driver\Connection $connection, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($instrumentation) {
121136
/** @psalm-suppress ArgumentTypeCoercion */
122-
$builder = self::makeBuilder($instrumentation, 'Doctrine\DBAL\Driver\Connection::commit', $function, $class, $filename, $lineno)
123-
->setSpanKind(SpanKind::KIND_CLIENT);
137+
$builder = self::makeBuilder($instrumentation, 'Doctrine::commit', $function, $class, $filename, $lineno)
138+
->setSpanKind(SpanKind::KIND_CLIENT)
139+
->setAttribute(TraceAttributes::DB_OPERATION_NAME, 'commit');
124140
$parent = Context::getCurrent();
125141
$span = $builder->startSpan();
126142

@@ -136,8 +152,9 @@ public static function register(): void
136152
'rollBack',
137153
pre: static function (\Doctrine\DBAL\Driver\Connection $connection, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($instrumentation) {
138154
/** @psalm-suppress ArgumentTypeCoercion */
139-
$builder = self::makeBuilder($instrumentation, 'Doctrine\DBAL\Driver\Connection::rollBack', $function, $class, $filename, $lineno)
140-
->setSpanKind(SpanKind::KIND_CLIENT);
155+
$builder = self::makeBuilder($instrumentation, 'Doctrine::rollBack', $function, $class, $filename, $lineno)
156+
->setSpanKind(SpanKind::KIND_CLIENT)
157+
->setAttribute(TraceAttributes::DB_OPERATION_NAME, 'rollback');
141158
$parent = Context::getCurrent();
142159
$span = $builder->startSpan();
143160
Context::storage()->attach($span->storeInContext($parent));
@@ -146,6 +163,27 @@ public static function register(): void
146163
self::end($exception);
147164
}
148165
);
166+
167+
hook(
168+
\Doctrine\DBAL\Driver\Statement::class,
169+
'execute',
170+
pre: static function (\Doctrine\DBAL\Driver\Statement $statement, array $params, string $class, string $function, ?string $filename, ?int $lineno) use ($instrumentation, $tracker) {
171+
/** @psalm-suppress ArgumentTypeCoercion */
172+
$builder = self::makeBuilder($instrumentation, 'Doctrine::execute', $function, $class, $filename, $lineno)
173+
->setSpanKind(SpanKind::KIND_CLIENT)
174+
->setAttribute(TraceAttributes::DB_OPERATION_NAME, 'execute');
175+
if ($ctx = $tracker->getSpanContextForStatement($statement)) {
176+
$builder->addLink($ctx);
177+
}
178+
$parent = Context::getCurrent();
179+
$span = $builder->startSpan();
180+
181+
Context::storage()->attach($span->storeInContext($parent));
182+
},
183+
post: static function (\Doctrine\DBAL\Driver\Statement $statement, array $params, ResultInterface $result, ?Throwable $exception) {
184+
self::end($exception);
185+
}
186+
);
149187
}
150188
private static function makeBuilder(
151189
CachedInstrumentation $instrumentation,
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenTelemetry\Contrib\Instrumentation\Doctrine;
6+
7+
use Doctrine\DBAL\Driver\Statement;
8+
use OpenTelemetry\API\Trace\SpanContextInterface;
9+
use WeakMap;
10+
use WeakReference;
11+
12+
/**
13+
* @internal
14+
*/
15+
class DoctrineTracker
16+
{
17+
private WeakMap $statementToSpanContextMap;
18+
19+
public function __construct()
20+
{
21+
$this->statementToSpanContextMap = new WeakMap();
22+
}
23+
24+
public function trackStatement(Statement $statement, SpanContextInterface $context): void
25+
{
26+
$this->statementToSpanContextMap[$statement] = WeakReference::create($context);
27+
}
28+
29+
public function getSpanContextForStatement(Statement $statement): ?SpanContextInterface
30+
{
31+
return $this->statementToSpanContextMap[$statement]?->get();
32+
}
33+
}

src/Instrumentation/Doctrine/tests/Integration/DoctrineInstrumentationTest.php

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,49 @@ public function test_statement_execution(): void
108108

109109
$connection->prepare('SELECT * FROM `technology`');
110110
$span = $this->storage->offsetGet(2);
111-
$this->assertSame('SELECT `technology`', $span->getName());
112-
$this->assertCount(3, $this->storage);
111+
$this->assertSame('SELECT technology', $span->getName());
112+
$this->assertSame('prepare', $span->getAttributes()->get(TraceAttributes::DB_OPERATION_NAME));
113113

114114
$connection->executeQuery('SELECT * FROM `technology`');
115115
$span = $this->storage->offsetGet(3);
116-
$this->assertSame('SELECT `technology`', $span->getName());
116+
$this->assertSame('SELECT technology', $span->getName());
117+
$this->assertSame('SELECT', $span->getAttributes()->get(TraceAttributes::DB_OPERATION_NAME));
118+
$this->assertCount(4, $this->storage);
119+
}
120+
121+
public function test_prepare_then_execute_statement(): void
122+
{
123+
$connection = self::createConnection();
124+
$statement = self::fillDB();
125+
$connection->executeStatement($statement);
126+
127+
$stmt = $connection->prepare('SELECT * FROM `technology` WHERE name = :name');
128+
$prepare = $this->storage->offsetGet(2);
129+
$this->assertSame('prepare', $prepare->getAttributes()->get(TraceAttributes::DB_OPERATION_NAME));
130+
$this->assertSame('SELECT technology', $prepare->getName());
131+
132+
$stmt->bindValue('name', 'PHP');
133+
$stmt->executeQuery();
134+
$execute = $this->storage->offsetGet(3);
135+
$this->assertSame('Doctrine::execute', $execute->getName());
136+
$this->assertSame('execute', $execute->getAttributes()->get(TraceAttributes::DB_OPERATION_NAME));
137+
$this->assertCount(1, $execute->getLinks());
138+
$this->assertEquals($prepare->getContext(), $execute->getLinks()[0]->getSpanContext(), 'execute span is linked to prepare span');
139+
}
140+
141+
public function test_query_with_bind_variables(): void
142+
{
143+
$connection = self::createConnection();
144+
$statement = self::fillDB();
145+
$connection->executeStatement($statement);
146+
147+
$connection->executeQuery('SELECT * FROM `technology` WHERE name = :name', ['name' => 'PHP']);
148+
$prepare = $this->storage->offsetGet(2);
149+
$this->assertSame('SELECT technology', $prepare->getName());
150+
$execute = $this->storage->offsetGet(3);
151+
$this->assertSame('Doctrine::execute', $execute->getName());
152+
$this->assertCount(1, $execute->getLinks());
153+
$this->assertEquals($prepare->getContext(), $execute->getLinks()[0]->getSpanContext(), 'execute span is linked to prepare span');
117154
$this->assertCount(4, $this->storage);
118155
}
119156

@@ -122,7 +159,8 @@ public function test_transaction(): void
122159
$connection = self::createConnection();
123160
$connection->beginTransaction();
124161
$span = $this->storage->offsetGet(1);
125-
$this->assertSame('Doctrine\DBAL\Driver\Connection::beginTransaction', $span->getName());
162+
$this->assertSame('Doctrine::beginTransaction', $span->getName());
163+
$this->assertSame('begin', $span->getAttributes()->get(TraceAttributes::DB_OPERATION_NAME));
126164
$this->assertCount(2, $this->storage);
127165

128166
$statement = self::fillDB();
@@ -131,7 +169,8 @@ public function test_transaction(): void
131169
$this->assertSame('CREATE technology', $span->getName());
132170
$connection->commit();
133171
$span = $this->storage->offsetGet(3);
134-
$this->assertSame('Doctrine\DBAL\Driver\Connection::commit', $span->getName());
172+
$this->assertSame('Doctrine::commit', $span->getName());
173+
$this->assertSame('commit', $span->getAttributes()->get(TraceAttributes::DB_OPERATION_NAME));
135174
$this->assertCount(4, $this->storage);
136175

137176
$connection->beginTransaction();
@@ -140,7 +179,8 @@ public function test_transaction(): void
140179
$connection->executeStatement("INSERT INTO technology(`name`, `date`) VALUES('Java', '1995-05-23');");
141180
$connection->rollback();
142181
$span = $this->storage->offsetGet(6);
143-
$this->assertSame('Doctrine\DBAL\Driver\Connection::rollBack', $span->getName());
182+
$this->assertSame('Doctrine::rollBack', $span->getName());
183+
$this->assertSame('rollback', $span->getAttributes()->get(TraceAttributes::DB_OPERATION_NAME));
144184
$this->assertCount(7, $this->storage);
145185
$this->assertFalse($connection->isTransactionActive());
146186

0 commit comments

Comments
 (0)