Skip to content

Commit 18b6ec5

Browse files
committed
Updated PDO
1 parent 22b57d0 commit 18b6ec5

File tree

4 files changed

+211
-3
lines changed

4 files changed

+211
-3
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
/*
4+
* Copyright 2022 Google LLC
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
* http:*www.apache.org/licenses/LICENSE-2.0
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
declare(strict_types=1);
17+
18+
namespace OpenTelemetry\Contrib\Instrumentation\PDO;
19+
20+
use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator;
21+
22+
class Opentelemetry
23+
{
24+
public static function getTraceContextValues()
25+
{
26+
$carrier = [];
27+
28+
$trace = TraceContextPropagator::getInstance();
29+
$trace->inject($carrier);
30+
31+
return $carrier;
32+
}
33+
34+
public static function getServiceNameValues()
35+
{
36+
$carrier = [];
37+
38+
if (class_exists('OpenTelemetry\Contrib\Propagation\ServiceName\ServiceNamePropagator')) {
39+
/** @phan-suppress-next-line PhanUndeclaredClassMethod */
40+
$trace = new \OpenTelemetry\Contrib\Propagation\ServiceName\ServiceNamePropagator();
41+
/** @phan-suppress-next-line PhanAccessMethodInternal,PhanUndeclaredClassMethod */
42+
$trace->inject($carrier);
43+
}
44+
45+
return $carrier;
46+
}
47+
}

src/Instrumentation/PDO/src/PDOInstrumentation.php

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,9 @@ public static function register(): void
111111
/** @psalm-suppress ArgumentTypeCoercion */
112112
$builder = self::makeBuilder($instrumentation, 'PDO::query', $function, $class, $filename, $lineno)
113113
->setSpanKind(SpanKind::KIND_CLIENT);
114+
$sqlStatement = mb_convert_encoding($params[0] ?? 'undefined', 'UTF-8');
114115
if ($class === PDO::class) {
115-
$builder->setAttribute(TraceAttributes::DB_QUERY_TEXT, mb_convert_encoding($params[0] ?? 'undefined', 'UTF-8'));
116+
$builder->setAttribute(TraceAttributes::DB_QUERY_TEXT, $sqlStatement);
116117
}
117118
$parent = Context::getCurrent();
118119
$span = $builder->startSpan();
@@ -121,6 +122,18 @@ public static function register(): void
121122
$span->setAttributes($attributes);
122123

123124
Context::storage()->attach($span->storeInContext($parent));
125+
if (self::isSqlCommenterEnabled() && $sqlStatement !== 'undefined') {
126+
$sqlStatement = self::appendSqlComments($sqlStatement);
127+
$span->setAttributes([
128+
TraceAttributes::DB_QUERY_TEXT => $sqlStatement,
129+
]);
130+
131+
return [
132+
0 => $sqlStatement,
133+
];
134+
}
135+
136+
return [];
124137
},
125138
post: static function (PDO $pdo, array $params, mixed $statement, ?Throwable $exception) {
126139
self::end($exception);
@@ -134,8 +147,9 @@ public static function register(): void
134147
/** @psalm-suppress ArgumentTypeCoercion */
135148
$builder = self::makeBuilder($instrumentation, 'PDO::exec', $function, $class, $filename, $lineno)
136149
->setSpanKind(SpanKind::KIND_CLIENT);
150+
$sqlStatement = mb_convert_encoding($params[0] ?? 'undefined', 'UTF-8');
137151
if ($class === PDO::class) {
138-
$builder->setAttribute(TraceAttributes::DB_QUERY_TEXT, mb_convert_encoding($params[0] ?? 'undefined', 'UTF-8'));
152+
$builder->setAttribute(TraceAttributes::DB_QUERY_TEXT, $sqlStatement);
139153
}
140154
$parent = Context::getCurrent();
141155
$span = $builder->startSpan();
@@ -144,6 +158,18 @@ public static function register(): void
144158
$span->setAttributes($attributes);
145159

146160
Context::storage()->attach($span->storeInContext($parent));
161+
if (self::isSqlCommenterEnabled() && $sqlStatement !== 'undefined') {
162+
$sqlStatement = self::appendSqlComments($sqlStatement);
163+
$span->setAttributes([
164+
TraceAttributes::DB_QUERY_TEXT => $sqlStatement,
165+
]);
166+
167+
return [
168+
0 => $sqlStatement,
169+
];
170+
}
171+
172+
return [];
147173
},
148174
post: static function (PDO $pdo, array $params, mixed $statement, ?Throwable $exception) {
149175
self::end($exception);
@@ -157,8 +183,9 @@ public static function register(): void
157183
/** @psalm-suppress ArgumentTypeCoercion */
158184
$builder = self::makeBuilder($instrumentation, 'PDO::prepare', $function, $class, $filename, $lineno)
159185
->setSpanKind(SpanKind::KIND_CLIENT);
186+
$sqlStatement = mb_convert_encoding($params[0] ?? 'undefined', 'UTF-8');
160187
if ($class === PDO::class) {
161-
$builder->setAttribute(TraceAttributes::DB_QUERY_TEXT, mb_convert_encoding($params[0] ?? 'undefined', 'UTF-8'));
188+
$builder->setAttribute(TraceAttributes::DB_QUERY_TEXT, $sqlStatement);
162189
}
163190
$parent = Context::getCurrent();
164191
$span = $builder->startSpan();
@@ -167,6 +194,18 @@ public static function register(): void
167194
$span->setAttributes($attributes);
168195

169196
Context::storage()->attach($span->storeInContext($parent));
197+
if (self::isSqlCommenterEnabled() && $sqlStatement !== 'undefined') {
198+
$sqlStatement = self::appendSqlComments($sqlStatement, false);
199+
$span->setAttributes([
200+
TraceAttributes::DB_QUERY_TEXT => $sqlStatement,
201+
]);
202+
203+
return [
204+
0 => $sqlStatement,
205+
];
206+
}
207+
208+
return [];
170209
},
171210
post: static function (PDO $pdo, array $params, mixed $statement, ?Throwable $exception) use ($pdoTracker) {
172211
if ($statement instanceof PDOStatement) {
@@ -329,4 +368,29 @@ private static function isDistributeStatementToLinkedSpansEnabled(): bool
329368

330369
return filter_var(get_cfg_var('otel.instrumentation.pdo.distribute_statement_to_linked_spans'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? false;
331370
}
371+
372+
private static function isSqlCommenterEnabled(): bool
373+
{
374+
if (class_exists('OpenTelemetry\SDK\Common\Configuration\Configuration')) {
375+
return Configuration::getBoolean('OTEL_PHP_INSTRUMENTATION_PDO_SQL_COMMENTER', false);
376+
}
377+
378+
return filter_var(get_cfg_var('otel.instrumentation.pdo.sql_commenter'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? false;
379+
}
380+
381+
private static function appendSqlComments(string $query, bool $withTraceContext = true): string
382+
{
383+
$comments = [];
384+
if ($withTraceContext) {
385+
$comments = Opentelemetry::getTraceContextValues();
386+
}
387+
foreach (Opentelemetry::getServiceNameValues() as $key => $value) {
388+
$comments[$key] = $value;
389+
}
390+
$query = trim($query);
391+
$hasSemicolon = $query[-1] === ';';
392+
$query = rtrim($query, ';');
393+
394+
return $query . Utils::formatComments(array_filter($comments)) . ($hasSemicolon ? ';' : '');
395+
}
332396
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
/*
4+
* Copyright 2022 Google LLC
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
* http:*www.apache.org/licenses/LICENSE-2.0
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
declare(strict_types=1);
17+
18+
namespace OpenTelemetry\Contrib\Instrumentation\PDO;
19+
20+
class Utils
21+
{
22+
public static function formatComments(array $comments): string
23+
{
24+
if (empty($comments)) {
25+
return '';
26+
}
27+
28+
return '/*' . implode(
29+
',',
30+
array_map(
31+
static fn (string $value, string $key) => Utils::customUrlEncode($key) . "='" . Utils::customUrlEncode($value) . "'",
32+
$comments,
33+
array_keys($comments)
34+
),
35+
) . '*/';
36+
}
37+
38+
private static function customUrlEncode(string $input): string
39+
{
40+
$encodedString = urlencode($input);
41+
42+
// Since SQL uses '%' as a keyword, '%' is a by-product of url quoting
43+
// e.g. foo,bar --> foo%2Cbar
44+
// thus in our quoting, we need to escape it too to finally give
45+
// foo,bar --> foo%%2Cbar
46+
47+
return str_replace('%', '%%', $encodedString);
48+
}
49+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
/*
4+
* Copyright 2022 Google LLC
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
* http:*www.apache.org/licenses/LICENSE-2.0
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
declare(strict_types=1);
16+
17+
namespace OpenTelemetry\Tests\Instrumentation\PDO\tests\Unit;
18+
19+
use OpenTelemetry\Contrib\Instrumentation\PDO\Utils;
20+
use PHPUnit\Framework\TestCase;
21+
22+
class UtilsTest extends TestCase
23+
{
24+
public function testFormatCommentsWithKeys(): void
25+
{
26+
$this->assertEquals("/*key1='value1',key2='value2'*/", Utils::formatComments(['key1' => 'value1', 'key2' => 'value2']));
27+
}
28+
29+
public function testFormatCommentsWithoutKeys(): void
30+
{
31+
$this->assertEquals('', Utils::formatComments([]));
32+
}
33+
34+
public function testFormatCommentsWithSpecialCharKeys(): void
35+
{
36+
$this->assertEquals("/*key1='value1%%40',key2='value2'*/", Utils::formatComments(['key1' => 'value1@', 'key2' => 'value2']));
37+
}
38+
39+
public function testFormatCommentsWithPlaceholder(): void
40+
{
41+
$this->assertEquals("/*key1='value1%%3F',key2='value2'*/", Utils::formatComments(['key1' => 'value1?', 'key2' => 'value2']));
42+
}
43+
44+
public function testFormatCommentsWithNamedPlaceholder(): void
45+
{
46+
$this->assertEquals("/*key1='%%3Anamed',key2='value2'*/", Utils::formatComments(['key1' => ':named', 'key2' => 'value2']));
47+
}
48+
}

0 commit comments

Comments
 (0)