Skip to content

Commit e6272aa

Browse files
authored
add Doctrine span links and more SemConv (#374)
* 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) * phan * allow doctrine + php 8.1 * apply review feedback - tidy constructor - do not use a WeakMap to track prepared statements, as the SpanContext is lost when prepare span flushed/exported * revert SplObjectStorage usage
1 parent e0c6ed6 commit e6272aa

File tree

6 files changed

+186
-44
lines changed

6 files changed

+186
-44
lines changed

.github/workflows/php.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,6 @@ jobs:
6464

6565
- project: 'Instrumentation/Curl'
6666
php-version: 8.1
67-
- project: 'Instrumentation/Doctrine'
68-
php-version: 8.1
6967
- project: 'Instrumentation/ExtAmqp'
7068
php-version: 8.1
7169
- project: 'Instrumentation/ExtRdKafka'

src/Instrumentation/Doctrine/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"minimum-stability": "dev",
1717
"prefer-stable": true,
1818
"require": {
19-
"php": "^8.2",
19+
"php": "^8.1",
2020
"ext-opentelemetry": "*",
2121
"doctrine/dbal": "^3 || ^4 || ^5",
2222
"open-telemetry/api": "^1.0",

src/Instrumentation/Doctrine/src/AttributesResolver.php

Lines changed: 39 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,57 @@ 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] ?? null;
138+
}
139+
if ($targetName === null) {
140+
return null;
141+
}
142+
//strip quotes and backticks from the target name
143+
$targetName = str_replace(['`', '"', '[', ']'], '', $targetName);
144+
145+
return $targetName;
121146
}
122147

123148
/**
124149
* Resolve attribute `db.query.summary`
125150
* See https://opentelemetry.io/docs/specs/semconv/database/database-spans/#generating-a-summary-of-the-query-text
126151
*/
127152
public static function getDbQuerySummary(array $params): string
153+
{
154+
$operationName = self::getDbOperationName($params);
155+
$targetName = self::getTarget($params);
156+
157+
return $operationName . ($targetName ? ' ' . $targetName : '');
158+
}
159+
160+
public static function getDbOperationName(array $params): string
128161
{
129162
$query = $params[0] ?? null;
130163

@@ -136,19 +169,6 @@ public static function getDbQuerySummary(array $params): string
136169
$operationName = explode(' ', $query);
137170
$operationName = $operationName[0];
138171

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 : '');
172+
return $operationName;
153173
}
154174
}

src/Instrumentation/Doctrine/src/DoctrineInstrumentation.php

Lines changed: 55 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,7 +163,29 @@ 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
}
188+
150189
private static function makeBuilder(
151190
CachedInstrumentation $instrumentation,
152191
string $name,
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
11+
/**
12+
* @internal
13+
*/
14+
class DoctrineTracker
15+
{
16+
public function __construct(
17+
private WeakMap $statementToSpanContextMap = new WeakMap(),
18+
) {
19+
}
20+
21+
public function trackStatement(Statement $statement, SpanContextInterface $context): void
22+
{
23+
$this->statementToSpanContextMap[$statement] = $context;
24+
}
25+
26+
public function getSpanContextForStatement(Statement $statement): ?SpanContextInterface
27+
{
28+
return $this->statementToSpanContextMap[$statement] ?? null;
29+
}
30+
}

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

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,64 @@ 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_tracked_span_context_when_prepare_span_flushed(): void
142+
{
143+
$connection = self::createConnection();
144+
$statement = self::fillDB();
145+
$connection->executeStatement($statement);
146+
147+
$stmt = $connection->prepare('SELECT * FROM `technology` WHERE name = :name');
148+
$this->storage->exchangeArray([]); //removes the reference to prepared span, including the tracked SpanContext
149+
$stmt->bindValue('name', 'PHP');
150+
$stmt->executeQuery();
151+
152+
$execute = $this->storage->offsetGet(0);
153+
$this->assertCount(1, $execute->getLinks());
154+
}
155+
156+
public function test_query_with_bind_variables(): void
157+
{
158+
$connection = self::createConnection();
159+
$statement = self::fillDB();
160+
$connection->executeStatement($statement);
161+
162+
$connection->executeQuery('SELECT * FROM `technology` WHERE name = :name', ['name' => 'PHP']);
163+
$prepare = $this->storage->offsetGet(2);
164+
$this->assertSame('SELECT technology', $prepare->getName());
165+
$execute = $this->storage->offsetGet(3);
166+
$this->assertSame('Doctrine::execute', $execute->getName());
167+
$this->assertCount(1, $execute->getLinks());
168+
$this->assertEquals($prepare->getContext(), $execute->getLinks()[0]->getSpanContext(), 'execute span is linked to prepare span');
117169
$this->assertCount(4, $this->storage);
118170
}
119171

@@ -122,7 +174,8 @@ public function test_transaction(): void
122174
$connection = self::createConnection();
123175
$connection->beginTransaction();
124176
$span = $this->storage->offsetGet(1);
125-
$this->assertSame('Doctrine\DBAL\Driver\Connection::beginTransaction', $span->getName());
177+
$this->assertSame('Doctrine::beginTransaction', $span->getName());
178+
$this->assertSame('begin', $span->getAttributes()->get(TraceAttributes::DB_OPERATION_NAME));
126179
$this->assertCount(2, $this->storage);
127180

128181
$statement = self::fillDB();
@@ -131,7 +184,8 @@ public function test_transaction(): void
131184
$this->assertSame('CREATE technology', $span->getName());
132185
$connection->commit();
133186
$span = $this->storage->offsetGet(3);
134-
$this->assertSame('Doctrine\DBAL\Driver\Connection::commit', $span->getName());
187+
$this->assertSame('Doctrine::commit', $span->getName());
188+
$this->assertSame('commit', $span->getAttributes()->get(TraceAttributes::DB_OPERATION_NAME));
135189
$this->assertCount(4, $this->storage);
136190

137191
$connection->beginTransaction();
@@ -140,7 +194,8 @@ public function test_transaction(): void
140194
$connection->executeStatement("INSERT INTO technology(`name`, `date`) VALUES('Java', '1995-05-23');");
141195
$connection->rollback();
142196
$span = $this->storage->offsetGet(6);
143-
$this->assertSame('Doctrine\DBAL\Driver\Connection::rollBack', $span->getName());
197+
$this->assertSame('Doctrine::rollBack', $span->getName());
198+
$this->assertSame('rollback', $span->getAttributes()->get(TraceAttributes::DB_OPERATION_NAME));
144199
$this->assertCount(7, $this->storage);
145200
$this->assertFalse($connection->isTransactionActive());
146201

0 commit comments

Comments
 (0)