Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions ext/configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ enum ddtrace_sidecar_connection_mode {
CONFIG(BOOL, DD_TRACE_HEALTH_METRICS_ENABLED, "false", .ini_change = zai_config_system_ini_change) \
CONFIG(DOUBLE, DD_TRACE_HEALTH_METRICS_HEARTBEAT_SAMPLE_RATE, "0.001") \
CONFIG(BOOL, DD_TRACE_DB_CLIENT_SPLIT_BY_INSTANCE, "false") \
CONFIG(BOOL, DD_TRACE_PDO_PREPARED_STATEMENTS_ENABLED, "true") \
CONFIG(BOOL, DD_TRACE_PDO_LIFECYCLE_COMMANDS_ENABLED, "true") \
CONFIG(BOOL, DD_TRACE_HTTP_CLIENT_SPLIT_BY_DOMAIN, "false") \
CONFIG(BOOL, DD_TRACE_REDIS_CLIENT_SPLIT_BY_HOST, "false") \
CONFIG(BOOL, DD_EXCEPTION_REPLAY_ENABLED, "false", .ini_change = ddtrace_alter_DD_EXCEPTION_REPLAY_ENABLED) \
Expand Down
14 changes: 14 additions & 0 deletions metadata/supported-configurations.json
Original file line number Diff line number Diff line change
Expand Up @@ -1878,6 +1878,20 @@
"default": "true"
}
],
"DD_TRACE_PDO_LIFECYCLE_COMMANDS_ENABLED": [
{
"implementation": "A",
"type": "boolean",
"default": "true"
}
],
"DD_TRACE_PDO_PREPARED_STATEMENTS_ENABLED": [
{
"implementation": "A",
"type": "boolean",
"default": "true"
}
],
"DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED": [
{
"implementation": "A",
Expand Down
86 changes: 70 additions & 16 deletions src/DDTrace/Integrations/PDO/PDOIntegration.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,24 +42,42 @@ public static function init(): int
}

// public PDO::__construct ( string $dsn [, string $username [, string $passwd [, array $options ]]] )
\DDTrace\trace_method('PDO', '__construct', function (SpanData $span, array $args) {
Integration::handleOrphan($span);
$span->name = $span->resource = 'PDO.__construct';
$connectionMetadata = PDOIntegration::extractConnectionMetadata($args);
ObjectKVStore::put($this, PDOIntegration::CONNECTION_TAGS_KEY, $connectionMetadata);
// We have to use $connectionMetadata as a medium, instead of $this (aka the PDO instance) because in
// PHP 5.* $this is NULL in this callback when there is a connection error.
PDOIntegration::setCommonSpanInfo($connectionMetadata, $span);
});
\DDTrace\install_hook(
'PDO::__construct',
static function (HookData $hook) {
if (\dd_trace_env_config("DD_TRACE_PDO_LIFECYCLE_COMMANDS_ENABLED")) {
$hook->span();
$hook->data = true;
}
},
static function (HookData $hook) {
$connectionMetadata = PDOIntegration::extractConnectionMetadata($hook->args);
// $hook->instance may be NULL on connection error (PHP 5.* compat note still applies)
if ($hook->instance !== null) {
ObjectKVStore::put($hook->instance, PDOIntegration::CONNECTION_TAGS_KEY, $connectionMetadata);
}
if (!isset($hook->data)) {
return;
}
$span = $hook->span();
Integration::handleOrphan($span);
$span->name = $span->resource = 'PDO.__construct';
PDOIntegration::setCommonSpanInfo($connectionMetadata, $span);
}
);

if (PHP_VERSION_ID >= 80400) {
// public PDO::connect ( string $dsn [, string $username [, string $passwd [, array $options ]]] )
\DDTrace\trace_method('PDO', 'connect', static function (SpanData $span, array $args, $pdo) {
\DDTrace\install_hook('PDO::connect', static function (HookData $hook) {
if (!\dd_trace_env_config("DD_TRACE_PDO_LIFECYCLE_COMMANDS_ENABLED")) {
return;
}
$span = $hook->span();
Integration::handleOrphan($span);
$span->name = $span->resource = 'PDO.connect';
$connectionMetadata = self::extractConnectionMetadata($args);
ObjectKVStore::put($pdo, self::CONNECTION_TAGS_KEY, $connectionMetadata);
self::setCommonSpanInfo($connectionMetadata, $span);
$connectionMetadata = PDOIntegration::extractConnectionMetadata($hook->args);
ObjectKVStore::put($hook->instance, PDOIntegration::CONNECTION_TAGS_KEY, $connectionMetadata);
PDOIntegration::setCommonSpanInfo($connectionMetadata, $span);
});
}

Expand Down Expand Up @@ -117,6 +135,11 @@ public static function init(): int
// public PDOStatement PDO::prepare ( string $statement [, array $driver_options = array() ] )
\DDTrace\install_hook('PDO::prepare', static function (HookData $hook) {
list($query) = $hook->args;

if (!\dd_trace_env_config("DD_TRACE_PDO_PREPARED_STATEMENTS_ENABLED")) {
return; // No span; post-hook still propagates connection metadata
}

$hook->data = $query;

$span = $hook->span();
Expand All @@ -131,26 +154,57 @@ public static function init(): int
}, static function (HookData $hook) {
$pdo = $hook->returned;
ObjectKVStore::propagate($hook->instance, $pdo, PDOIntegration::CONNECTION_TAGS_KEY);
if ($pdo instanceof \PDOStatement) {
\dd_trace_internal_fn("force_overwrite_property", $pdo, "queryString", $hook->data); // Restore the query string minus the DBM injected stuff
if ($pdo instanceof \PDOStatement && isset($hook->data)) {
\dd_trace_internal_fn("force_overwrite_property", $pdo, "queryString", $hook->data); // Only reached when span was created (flag enabled) and DBM may have modified queryString; restores the original query
}
});

// public bool PDO::beginTransaction ( void )
\DDTrace\install_hook('PDO::beginTransaction', static function (HookData $hook) {
if (!\dd_trace_env_config("DD_TRACE_PDO_LIFECYCLE_COMMANDS_ENABLED")) {
return;
}
$span = $hook->span();
Integration::handleOrphan($span);
$span->name = $span->resource = 'PDO.beginTransaction';
PDOIntegration::setCommonSpanInfo($hook->instance, $span);
});

// public bool PDO::commit ( void )
\DDTrace\install_hook('PDO::commit', static function (HookData $hook) {
if (!\dd_trace_env_config("DD_TRACE_PDO_LIFECYCLE_COMMANDS_ENABLED")) {
return;
}
$span = $hook->span();
Integration::handleOrphan($span);
$span->name = $span->resource = 'PDO.commit';
PDOIntegration::setCommonSpanInfo($hook->instance, $span);
});

// public bool PDO::rollBack ( void )
\DDTrace\install_hook('PDO::rollBack', static function (HookData $hook) {
if (!\dd_trace_env_config("DD_TRACE_PDO_LIFECYCLE_COMMANDS_ENABLED")) {
return;
}
$span = $hook->span();
Integration::handleOrphan($span);
$span->name = $span->resource = 'PDO.rollBack';
PDOIntegration::setCommonSpanInfo($hook->instance, $span);
});

// public bool PDOStatement::execute ([ array $input_parameters ] )
\DDTrace\install_hook(
'PDOStatement::execute',
static function (HookData $hook) {
$hook->span();
if (\dd_trace_env_config("DD_TRACE_PDO_PREPARED_STATEMENTS_ENABLED")) {
$hook->data = true;
$hook->span();
}
},
static function (HookData $hook) {
if (!isset($hook->data)) {
return;
}
$span = $hook->span();
$instance = $hook->instance;
Integration::handleOrphan($span);
Expand Down
86 changes: 86 additions & 0 deletions tests/Integrations/PDO/PDOTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ protected function envsToCleanUpAtTearDown()
{
return [
'DD_TRACE_DB_CLIENT_SPLIT_BY_INSTANCE',
'DD_TRACE_PDO_PREPARED_STATEMENTS_ENABLED',
'DD_TRACE_PDO_LIFECYCLE_COMMANDS_ENABLED',
'DD_TRACE_PEER_SERVICE_DEFAULTS_ENABLED',
'DD_TRACE_REMOVE_INTEGRATION_SERVICE_NAMES_ENABLED',
'DD_SERVICE_MAPPING',
Expand Down Expand Up @@ -232,6 +234,7 @@ public function testPDOExecOk()
});
$this->assertSpans($traces, [
SpanAssertion::exists('PDO.__construct'),
SpanAssertion::exists('PDO.beginTransaction'),
SpanAssertion::build('PDO.exec', 'pdo', 'sql', $query)
->withExactTags($this->baseTags())
->withExactMetrics([
Expand Down Expand Up @@ -259,6 +262,7 @@ public function testPDOExecError()
});
$this->assertSpans($traces, [
SpanAssertion::exists('PDO.__construct'),
SpanAssertion::exists('PDO.beginTransaction'),
SpanAssertion::build('PDO.exec', 'pdo', 'sql', $query)
->setError('PDO error', 'SQL error: 42000. Driver error: 1064. Driver-specific error data: You have an error in your SQL syntax')
->withExactTags($this->baseTags()),
Expand All @@ -283,6 +287,7 @@ public function testPDOExecException()
});
$this->assertSpans($traces, [
SpanAssertion::exists('PDO.__construct'),
SpanAssertion::exists('PDO.beginTransaction'),
SpanAssertion::build('PDO.exec', 'pdo', 'sql', $query)
->setError('PDOException', static::ERROR_EXEC, true)
->withExactTags($this->baseTags()),
Expand Down Expand Up @@ -407,6 +412,7 @@ public function testPDOCommit()
});
$this->assertSpans($traces, [
SpanAssertion::exists('PDO.__construct'),
SpanAssertion::exists('PDO.beginTransaction'),
SpanAssertion::exists('PDO.exec'),
SpanAssertion::build('PDO.commit', 'pdo', 'sql', 'PDO.commit')
->withExactTags($this->baseTags()),
Expand Down Expand Up @@ -489,6 +495,85 @@ public function testPDOStatementOkPeerServiceEnabled()
]);
}

public function testPDOPreparedStatementsDisabled()
{
$this->putEnvAndReloadConfig(['DD_TRACE_PDO_PREPARED_STATEMENTS_ENABLED=false']);

$query = "SELECT * FROM tests WHERE id = ?";
$traces = $this->isolateTracer(function () use ($query) {
$pdo = $this->pdoInstance();
$stmt = $pdo->prepare($query);
$stmt->execute([1]);
$results = $stmt->fetchAll();
$this->assertEquals('Tom', $results[0]['name']);
$stmt->closeCursor();
$stmt = null;
$pdo = null;
});
// PDO.prepare and PDOStatement.execute spans must NOT appear
$this->assertSpans($traces, [
SpanAssertion::exists('PDO.__construct'),
]);
}

public function testPDOPreparedStatementsDisabledDoesNotAffectExec()
{
$this->putEnvAndReloadConfig(['DD_TRACE_PDO_PREPARED_STATEMENTS_ENABLED=false']);

$traces = $this->isolateTracer(function () {
$pdo = $this->pdoInstance();
$pdo->exec("SELECT * FROM tests WHERE id = 1");
$pdo = null;
});
// PDO.exec span must still appear when prepared statements are disabled
$this->assertSpans($traces, [
SpanAssertion::exists('PDO.__construct'),
SpanAssertion::build('PDO.exec', 'pdo', 'sql', 'SELECT * FROM tests WHERE id = 1')
->withExactTags($this->baseTags()),
]);
}

public function testPDOLifecycleCommandsDisabled()
{
$this->putEnvAndReloadConfig(['DD_TRACE_PDO_LIFECYCLE_COMMANDS_ENABLED=false']);

$traces = $this->isolateTracer(function () {
$pdo = $this->pdoInstance();
$pdo->exec("SELECT * FROM tests WHERE id = 1");
$pdo = null;
});
// PDO.__construct and PDO.commit spans must NOT appear; PDO.exec must still appear
$this->assertSpans($traces, [
SpanAssertion::build('PDO.exec', 'pdo', 'sql', 'SELECT * FROM tests WHERE id = 1')
->withExactTags($this->baseTags()),
]);
}

public function testPDOLifecycleCommandsDisabledTransactions()
{
$this->putEnvAndReloadConfig(['DD_TRACE_PDO_LIFECYCLE_COMMANDS_ENABLED=false']);

$query = "INSERT INTO tests (id, name) VALUES (1000, 'Sam')";
$traces = $this->isolateTracer(function () use ($query) {
$pdo = $this->pdoInstance();
$pdo->beginTransaction();
$pdo->exec($query);
$pdo->commit();
$pdo = null;
});
// PDO.beginTransaction, PDO.commit must NOT appear; PDO.exec must still appear
$this->assertSpans($traces, [
SpanAssertion::build('PDO.exec', 'pdo', 'sql', $query)
->withExactTags($this->baseTags())
->withExactMetrics([
Tag::DB_ROW_COUNT => 1.0,
Tag::ANALYTICS_KEY => 1.0,
'_dd.agent_psr' => 1.0,
'_sampling_priority_v1' => 1.0,
]),
]);
}

public function testPDOStatementSplitByDomain()
{
$this->putEnvAndReloadConfig(['DD_TRACE_DB_CLIENT_SPLIT_BY_INSTANCE=true']);
Expand Down Expand Up @@ -819,6 +904,7 @@ public function testNoFakeServices()
});
$this->assertSpans($traces, [
SpanAssertion::exists('PDO.__construct'),
SpanAssertion::exists('PDO.beginTransaction'),
SpanAssertion::build('PDO.exec', 'configured_service', 'sql', $query)
->withExactTags($this->baseTags())
->withExactMetrics([Tag::DB_ROW_COUNT => 1.0, Tag::ANALYTICS_KEY => 1.0]),
Expand Down
2 changes: 2 additions & 0 deletions tests/randomized/config/envs.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
'DD_TRACE_AUTO_FLUSH_ENABLED' => ['true'],
'DD_TAGS' => ['tag_1:hi,tag_2:hello'],
'DD_TRACE_DB_CLIENT_SPLIT_BY_INSTANCE' => ['true'],
'DD_TRACE_PDO_PREPARED_STATEMENTS_ENABLED' => ['false'],
'DD_TRACE_PDO_LIFECYCLE_COMMANDS_ENABLED' => ['false'],
'DD_TRACE_HTTP_CLIENT_SPLIT_BY_DOMAIN' => ['true'],
'DD_TRACE_REDIS_CLIENT_SPLIT_BY_HOST' => ['true'],
'DD_TRACE_MEASURE_COMPILE_TIME' => ['false'],
Expand Down