diff --git a/ext/configuration.h b/ext/configuration.h index 599d2ed4eb8..3f38a2d5ddb 100644 --- a/ext/configuration.h +++ b/ext/configuration.h @@ -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) \ diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index f4a3c4d0644..1e1eca1d96b 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -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", diff --git a/src/DDTrace/Integrations/PDO/PDOIntegration.php b/src/DDTrace/Integrations/PDO/PDOIntegration.php index 7cdd94e90d8..72ca29d56b4 100644 --- a/src/DDTrace/Integrations/PDO/PDOIntegration.php +++ b/src/DDTrace/Integrations/PDO/PDOIntegration.php @@ -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); }); } @@ -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(); @@ -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); diff --git a/tests/Integrations/PDO/PDOTest.php b/tests/Integrations/PDO/PDOTest.php index 3e7261cbbde..7bc58dd111f 100644 --- a/tests/Integrations/PDO/PDOTest.php +++ b/tests/Integrations/PDO/PDOTest.php @@ -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', @@ -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([ @@ -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()), @@ -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()), @@ -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()), @@ -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']); @@ -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]), diff --git a/tests/randomized/config/envs.php b/tests/randomized/config/envs.php index e79466fdd7e..21e0c6fca0f 100644 --- a/tests/randomized/config/envs.php +++ b/tests/randomized/config/envs.php @@ -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'],