Skip to content

Commit

Permalink
fix PDO instrumentation in PHP 8.4 (#993)
Browse files Browse the repository at this point in the history
PHP 8.4 includes implementation of PDO driver specific sub-classes RFC
(see https://wiki.php.net/rfc/pdo_driver_specific_subclasses). This
means that when database access code uses new factory method
`PDO::connect` to create PDO object, an instance of driver specific
sub-class of `PDO` is returned instead of an instance of generic `PDO`
class. This means that instrumentation of generic `PDO` class is not
enough to provide instrumentation of datastores. Add wrappers for driver
specific subclasses of `PDO` supported by the agent: `Pdo\Firebird`,
`Pdo\Mysql`, `Pdo\Odbc`, `Pdo\Pgsql`, `Pdo\Sqlite`.
  • Loading branch information
lavarou authored and ZNeumann committed Jan 7, 2025
1 parent a255b18 commit 188b173
Show file tree
Hide file tree
Showing 110 changed files with 5,810 additions and 225 deletions.
28 changes: 28 additions & 0 deletions agent/php_internal_instrument.c
Original file line number Diff line number Diff line change
Expand Up @@ -3840,9 +3840,37 @@ void nr_php_generate_internal_wrap_records(void) {
NR_INTERNAL_WRAPREC("sqlite3::exec", sqlite3_exec, sqlite3, 0, 0)

NR_INTERNAL_WRAPREC("pdo::__construct", pdo_construct, pdo_construct, 0, 0)
#if ZEND_MODULE_API_NO >= ZEND_8_4_X_API_NO
NR_INTERNAL_WRAPREC("pdo\\firebird::__construct", pdo_construct, pdo_construct, 0, 0)
NR_INTERNAL_WRAPREC("pdo\\mysql::__construct", pdo_construct, pdo_construct, 0, 0)
NR_INTERNAL_WRAPREC("pdo\\odbc::__construct", pdo_construct, pdo_construct, 0, 0)
NR_INTERNAL_WRAPREC("pdo\\pgsql::__construct", pdo_construct, pdo_construct, 0, 0)
NR_INTERNAL_WRAPREC("pdo\\sqlite::__construct", pdo_construct, pdo_construct, 0, 0)
#endif
NR_INTERNAL_WRAPREC("pdo::query", pdo_query, pdo_query, 0, 0)
#if ZEND_MODULE_API_NO >= ZEND_8_4_X_API_NO
NR_INTERNAL_WRAPREC("pdo\\firebird::query", pdo_query, pdo_query, 0, 0)
NR_INTERNAL_WRAPREC("pdo\\mysql::query", pdo_query, pdo_query, 0, 0)
NR_INTERNAL_WRAPREC("pdo\\odbc::query", pdo_query, pdo_query, 0, 0)
NR_INTERNAL_WRAPREC("pdo\\pgsql::query", pdo_query, pdo_query, 0, 0)
NR_INTERNAL_WRAPREC("pdo\\sqlite::query", pdo_query, pdo_query, 0, 0)
#endif
NR_INTERNAL_WRAPREC("pdo::exec", pdo_exec, pdo_exec, 0, 0)
#if ZEND_MODULE_API_NO >= ZEND_8_4_X_API_NO
NR_INTERNAL_WRAPREC("pdo\\firebird::exec", pdo_exec, pdo_exec, 0, 0)
NR_INTERNAL_WRAPREC("pdo\\mysql::exec", pdo_exec, pdo_exec, 0, 0)
NR_INTERNAL_WRAPREC("pdo\\odbc::exec", pdo_exec, pdo_exec, 0, 0)
NR_INTERNAL_WRAPREC("pdo\\pgsql::exec", pdo_exec, pdo_exec, 0, 0)
NR_INTERNAL_WRAPREC("pdo\\sqlite::exec", pdo_exec, pdo_exec, 0, 0)
#endif
NR_INTERNAL_WRAPREC("pdo::prepare", pdo_prepare, pdo_prepare, 0, 0)
#if ZEND_MODULE_API_NO >= ZEND_8_4_X_API_NO
NR_INTERNAL_WRAPREC("pdo\\firebird::prepare", pdo_prepare, pdo_prepare, 0, 0)
NR_INTERNAL_WRAPREC("pdo\\mysql::prepare", pdo_prepare, pdo_prepare, 0, 0)
NR_INTERNAL_WRAPREC("pdo\\odbc::prepare", pdo_prepare, pdo_prepare, 0, 0)
NR_INTERNAL_WRAPREC("pdo\\pgsql::prepare", pdo_prepare, pdo_prepare, 0, 0)
NR_INTERNAL_WRAPREC("pdo\\sqlite::prepare", pdo_prepare, pdo_prepare, 0, 0)
#endif
NR_INTERNAL_WRAPREC("pdostatement::execute", pdostmt_execute,
pdostatement_execute, 0, 0)

Expand Down
2 changes: 1 addition & 1 deletion files/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ COPY --from=composer ["/usr/bin/composer", "/usr/bin/composer"]
# and 8.0 has problems with how the explanation for informational_schema
# work (refer to bug https://bugs.mysql.com/bug.php?id=102536) so to run
# the mysql tests a separate machine running mysql server 5.6 is required.
RUN docker-php-ext-install pdo pdo_mysql
RUN docker-php-ext-install pdo pdo_mysql pdo_pgsql

# install redis extension required by test_redis:
RUN \
Expand Down
6 changes: 5 additions & 1 deletion tests/include/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function isset_or($check, $alternate = NULL)

$MYSQL_USER = isset_or('MYSQL_USER', 'root');
$MYSQL_PASSWD = isset_or('MYSQL_PASSWD', 'root');
$MYSQL_DB = 'information_schema'; // TODO: MSL comment here.
$MYSQL_DB = isset_or('MYSQL_DB', 'information_schema');
$MYSQL_HOST = isset_or('MYSQL_HOST', 'localhost');
$MYSQL_PORT = isset_or('MYSQL_PORT', 3306);
$MYSQL_SOCKET = isset_or('MYSQL_SOCKET', '');
Expand Down Expand Up @@ -75,6 +75,10 @@ function make_tracing_url($file)
$PG_PORT = isset_or('PG_PORT', '5433');
$PG_CONNECTION = "host=$PG_HOST port=$PG_PORT user=$PG_USER password=$PG_PW connect_timeout=1";

$PDO_PGSQL_DSN = 'pgsql:';
$PDO_PGSQL_DSN .= 'host=' . $PG_HOST . ';';
$PDO_PGSQL_DSN .= 'port=' . $PG_PORT . ';';

// Header used to track whether or not our CAT instrumentation interferes with
// other existing headers.
define('CUSTOMER_HEADER', 'Customer-Header');
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/events/test_database_duration.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ function test_slow_sql() {
global $PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD;

$conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD);
$result = $conn->query('select * from tables limit 1;');
$result = $conn->query('select * from information_schema.tables limit 1;');
}

test_slow_sql();
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
<?php
/*
* Copyright 2020 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

/*DESCRIPTION
When PDO base class constructor is used to create connection object
to a database on a remote host via a TCP port,
and database instance reporting is enabled, the agent should
- not generate errors
- record datastore metrics
- record a datastore instance metric
- record a datastore span event with instance information
Moreover, when the query execution time exceeds the explain threshold,
the agent should record a slow sql trace with database instance information.
*/

/*SKIPIF
<?php require(realpath (dirname ( __FILE__ )) . '/../../skipif_mysql.inc');
*/

/*ENVIRONMENT
DATASTORE_PRODUCT=MySQL
DATASTORE_COLLECTION=test
MYSQL_PORT=3306
*/

/*INI
;comment=Set explain_threshold to 0 to ensure that the slow query is recorded.
newrelic.transaction_tracer.explain_threshold = 0
*/

/*EXPECT_ERROR_EVENTS null*/

/*EXPECT
ok - create table
ok - drop table
*/

/*EXPECT_METRICS_EXIST
Datastore/all, 2
Datastore/allOther, 2
Datastore/instance/ENV[DATASTORE_PRODUCT]/ENV[MYSQL_HOST]/ENV[MYSQL_PORT], 2
Datastore/ENV[DATASTORE_PRODUCT]/all, 2
Datastore/ENV[DATASTORE_PRODUCT]/allOther, 2
Datastore/operation/ENV[DATASTORE_PRODUCT]/create, 1
Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create, 1
Datastore/operation/ENV[DATASTORE_PRODUCT]/drop, 1
Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop, 1
Supportability/TxnData/SlowSQL, 1
*/

/*EXPECT_SLOW_SQLS
[
[
[
"OtherTransaction/php__FILE__",
"<unknown>",
"?? SQL id",
"DROP TABLE ENV[DATASTORE_COLLECTION];",
"Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop",
1,
"?? total time",
"?? min time",
"?? max time",
{
"backtrace": [
"/ in PDO::exec called at .*\/",
" in test_instance_reporting called at __FILE__ (??)"
],
"host": "ENV[MYSQL_HOST]",
"port_path_or_id": "ENV[MYSQL_PORT]",
"database_name": "ENV[MYSQL_DB]"
}
],
[
"OtherTransaction/php__FILE__",
"<unknown>",
"?? SQL id",
"CREATE TABLE ENV[DATASTORE_COLLECTION] (id INT, description VARCHAR(?));",
"Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create",
1,
"?? total time",
"?? min time",
"?? max time",
{
"backtrace": [
"/ in PDO::exec called at .*\/",
" in test_instance_reporting called at __FILE__ (??)"
],
"host": "ENV[MYSQL_HOST]",
"port_path_or_id": "ENV[MYSQL_PORT]",
"database_name": "ENV[MYSQL_DB]"
}
]
]
]
*/

/*EXPECT_SPAN_EVENTS_LIKE
[
[
{
"category": "datastore",
"type": "Span",
"guid": "??",
"traceId": "??",
"transactionId": "??",
"name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create",
"timestamp": "??",
"duration": "??",
"priority": "??",
"sampled": true,
"parentId": "??",
"span.kind": "client",
"component": "ENV[DATASTORE_PRODUCT]"
},
{},
{
"peer.hostname": "ENV[MYSQL_HOST]",
"peer.address": "ENV[MYSQL_HOST]:ENV[MYSQL_PORT]",
"db.instance": "ENV[MYSQL_DB]",
"db.statement": "CREATE TABLE ENV[DATASTORE_COLLECTION] (id INT, description VARCHAR(?));"
}
],
[
{
"category": "datastore",
"type": "Span",
"guid": "??",
"traceId": "??",
"transactionId": "??",
"name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop",
"timestamp": "??",
"duration": "??",
"priority": "??",
"sampled": true,
"parentId": "??",
"span.kind": "client",
"component": "ENV[DATASTORE_PRODUCT]"
},
{},
{
"peer.hostname": "ENV[MYSQL_HOST]",
"peer.address": "ENV[MYSQL_HOST]:ENV[MYSQL_PORT]",
"db.instance": "ENV[MYSQL_DB]",
"db.statement": "DROP TABLE ENV[DATASTORE_COLLECTION];"
}
]
]
*/

require_once(realpath (dirname ( __FILE__ )) . '/../../test_instance_reporting.inc');
require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php');

test_instance_reporting(new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD), 0);
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<?php
/*
* Copyright 2020 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

/*DESCRIPTION
When PDO base class constructor is used to create connection object
to a database on a localhost via a unix socket,
and database instance reporting is enabled, the agent should
- not generate errors
- record datastore metrics
- record a datastore instance metric
- record a datastore span event with instance information
Moreover, when the query execution time exceeds the explain threshold,
the agent should record a slow sql trace with database instance information.
*/

/*SKIPIF
<?php require(realpath (dirname ( __FILE__ )) . '/../../skipif_mysql.inc');
*/

/*ENVIRONMENT
DATASTORE_PRODUCT=MySQL
DATASTORE_COLLECTION=test
*/

/*INI
;comment=Set explain_threshold to 0 to ensure that the slow query is recorded.
newrelic.transaction_tracer.explain_threshold = 0
*/

/*EXPECT_ERROR_EVENTS null*/

/*EXPECT
ok - create table
ok - drop table
*/

/*EXPECT_METRICS_EXIST
Datastore/all, 2
Datastore/allOther, 2
Datastore/instance/ENV[DATASTORE_PRODUCT]/__HOST__/ENV[MYSQL_SOCKET], 2
Datastore/ENV[DATASTORE_PRODUCT]/all, 2
Datastore/ENV[DATASTORE_PRODUCT]/allOther, 2
Datastore/operation/ENV[DATASTORE_PRODUCT]/create, 1
Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create, 1
Datastore/operation/ENV[DATASTORE_PRODUCT]/drop, 1
Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop, 1
Supportability/TxnData/SlowSQL, 1
*/

/*EXPECT_SLOW_SQLS
[
[
[
"OtherTransaction/php__FILE__",
"<unknown>",
"?? SQL id",
"DROP TABLE ENV[DATASTORE_COLLECTION];",
"Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop",
1,
"?? total time",
"?? min time",
"?? max time",
{
"backtrace": [
"/ in PDO::exec called at .*\/",
" in test_instance_reporting called at __FILE__ (??)"
],
"host": "__HOST__",
"port_path_or_id": "ENV[MYSQL_SOCKET]",
"database_name": "ENV[MYSQL_DB]"
}
],
[
"OtherTransaction/php__FILE__",
"<unknown>",
"?? SQL id",
"CREATE TABLE ENV[DATASTORE_COLLECTION] (id INT, description VARCHAR(?));",
"Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create",
1,
"?? total time",
"?? min time",
"?? max time",
{
"backtrace": [
"/ in PDO::exec called at .*\/",
" in test_instance_reporting called at __FILE__ (??)"
],
"host": "__HOST__",
"port_path_or_id": "ENV[MYSQL_SOCKET]",
"database_name": "ENV[MYSQL_DB]"
}
]
]
]
*/

/*EXPECT_SPAN_EVENTS_LIKE
[
[
{
"category": "datastore",
"type": "Span",
"guid": "??",
"traceId": "??",
"transactionId": "??",
"name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create",
"timestamp": "??",
"duration": "??",
"priority": "??",
"sampled": true,
"parentId": "??",
"span.kind": "client",
"component": "ENV[DATASTORE_PRODUCT]"
},
{},
{
"peer.hostname": "__HOST__",
"peer.address": "__HOST__:ENV[MYSQL_SOCKET]",
"db.instance": "ENV[MYSQL_DB]",
"db.statement": "CREATE TABLE ENV[DATASTORE_COLLECTION] (id INT, description VARCHAR(?));"
}
],
[
{
"category": "datastore",
"type": "Span",
"guid": "??",
"traceId": "??",
"transactionId": "??",
"name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop",
"timestamp": "??",
"duration": "??",
"priority": "??",
"sampled": true,
"parentId": "??",
"span.kind": "client",
"component": "ENV[DATASTORE_PRODUCT]"
},
{},
{
"peer.hostname": "__HOST__",
"peer.address": "__HOST__:ENV[MYSQL_SOCKET]",
"db.instance": "ENV[MYSQL_DB]",
"db.statement": "DROP TABLE ENV[DATASTORE_COLLECTION];"
}
]
]
*/

require_once(realpath (dirname ( __FILE__ )) . '/../../test_instance_reporting.inc');
require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php');

$DSN = 'mysql:';
$DSN .= 'unix_socket=' . $MYSQL_SOCKET . ';';
$DSN .= 'dbname=' . $MYSQL_DB . ';';

test_instance_reporting(new PDO($DSN, $MYSQL_USER, $MYSQL_PASSWD), 0);
Loading

0 comments on commit 188b173

Please sign in to comment.