Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
d077d3b
Added ServiceNamePropagator
jerrytfleung Jul 11, 2025
4859d6b
Updated PDO
jerrytfleung Jul 12, 2025
6145de2
Updated
jerrytfleung Jul 12, 2025
62f8ebd
Updated
jerrytfleung Jul 12, 2025
a337245
Fix
jerrytfleung Jul 12, 2025
f29504c
Added tests
jerrytfleung Jul 14, 2025
0c54465
Added opentelemetry test
jerrytfleung Jul 14, 2025
bb12ed3
Update src/Instrumentation/PDO/src/Utils.php
jerrytfleung Jul 15, 2025
c8deb29
Update src/Instrumentation/PDO/src/Opentelemetry.php
jerrytfleung Jul 15, 2025
c5836f2
Addressed service.name issue
jerrytfleung Jul 15, 2025
586df3f
Update src/Instrumentation/PDO/tests/Unit/UtilsTest.php
jerrytfleung Jul 15, 2025
84ff7cd
fixed style
jerrytfleung Jul 16, 2025
0a603a1
Making undefined as constant
jerrytfleung Jul 16, 2025
1c2c49b
Added opt in for sql commenter attribute and update READMD.md
jerrytfleung Jul 16, 2025
3b2871b
Added prepend vs append opt-in
jerrytfleung Jul 16, 2025
b434b42
Removed Opentelemetry class
jerrytfleung Jul 16, 2025
8e63332
Added database opt-in configuration
jerrytfleung Jul 17, 2025
db6a915
nits
jerrytfleung Jul 17, 2025
569e24d
Added suppress
jerrytfleung Jul 17, 2025
11bab35
Removed 'all' by default
jerrytfleung Jul 17, 2025
d280ff8
Revert
jerrytfleung Jul 18, 2025
574e7fc
nits
jerrytfleung Jul 18, 2025
0a61fe2
Used Globals
jerrytfleung Jul 21, 2025
91556b5
limited the opt-in to postgresql & mysql only
jerrytfleung Jul 21, 2025
44a74c3
nits
jerrytfleung Jul 21, 2025
7ad124a
Moved to another class and added unit tests
jerrytfleung Jul 23, 2025
2ee3590
Fixed psalm complaints
jerrytfleung Jul 23, 2025
0ce7b73
Updated
jerrytfleung Jul 25, 2025
bb2d5fd
Fixed tests
jerrytfleung Jul 25, 2025
7aee9ec
Address comments
jerrytfleung Aug 29, 2025
ee41b2a
Fixed
jerrytfleung Aug 29, 2025
df60ca8
Updated attributes
jerrytfleung Aug 29, 2025
e3ad9ca
Corrected the reference in the README.md
jerrytfleung Sep 2, 2025
b1686cd
Updated
jerrytfleung Sep 4, 2025
44bb5ed
Fixed
jerrytfleung Sep 4, 2025
4c0d9cd
updated
jerrytfleung Sep 4, 2025
ef7c00f
Updated
jerrytfleung Sep 4, 2025
ee94639
Updated
jerrytfleung Sep 4, 2025
97aff4e
Updated
jerrytfleung Sep 4, 2025
a98eb56
nits
jerrytfleung Sep 5, 2025
d540582
Merge pull request #2 from jerrytfleung/service_name_propagator_extend
jerrytfleung Sep 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/Instrumentation/PDO/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
28 changes: 28 additions & 0 deletions src/Instrumentation/PDO/src/ContextPropagation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\Contrib\Instrumentation\PDO;

use OpenTelemetry\SDK\Common\Configuration\Configuration;

class ContextPropagation
{
public static function isEnabled(): bool
{
if (class_exists('OpenTelemetry\SDK\Common\Configuration\Configuration') && Configuration::getBoolean('OTEL_PHP_INSTRUMENTATION_PDO_CONTEXT_PROPAGATION', false)) {
return true;
}

return filter_var(get_cfg_var('otel.instrumentation.pdo.context_propagation'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? false;
}

public static function isAttributeEnabled(): bool
{
if (class_exists('OpenTelemetry\SDK\Common\Configuration\Configuration') && Configuration::getBoolean('OTEL_PHP_INSTRUMENTATION_PDO_CONTEXT_PROPAGATION_ATTRIBUTE', false)) {
return true;
}

return filter_var(get_cfg_var('otel.instrumentation.pdo.context_propagation.attribute'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? false;
}
}
74 changes: 74 additions & 0 deletions src/Instrumentation/PDO/src/ContextPropagatorFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\Contrib\Instrumentation\PDO;

use OpenTelemetry\API\Behavior\LogsMessagesTrait;
use OpenTelemetry\Context\Propagation\MultiTextMapPropagator;
use OpenTelemetry\Context\Propagation\NoopTextMapPropagator;
use OpenTelemetry\Context\Propagation\TextMapPropagatorInterface;
use OpenTelemetry\SDK\Common\Configuration\Configuration;
use OpenTelemetry\SDK\Registry;

class ContextPropagatorFactory
{
use LogsMessagesTrait;

public function create(): TextMapPropagatorInterface | null
{
$propagators = [];
if (class_exists('OpenTelemetry\SDK\Common\Configuration\Configuration')) {
$propagators = Configuration::getList('OTEL_PHP_INSTRUMENTATION_PDO_CONTEXT_PROPAGATORS', []);
}

switch (count($propagators)) {
case 0:
return null;
case 1:
$propagator = $this->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<TextMapPropagatorInterface>
*/
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;
}
}
85 changes: 81 additions & 4 deletions src/Instrumentation/PDO/src/PDOInstrumentation.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -22,6 +23,7 @@
class PDOInstrumentation
{
public const NAME = 'pdo';
private const UNDEFINED = 'undefined';

public static function register(): void
{
Expand All @@ -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')) {
Expand Down Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -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();
Expand All @@ -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);
Expand Down
37 changes: 19 additions & 18 deletions src/Instrumentation/PDO/src/PDOTracker.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
}

Expand All @@ -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;
}
}

Expand Down
Loading
Loading