Skip to content

Commit 188b173

Browse files
lavarouZNeumann
authored andcommitted
fix PDO instrumentation in PHP 8.4 (#993)
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`.
1 parent a255b18 commit 188b173

File tree

110 files changed

+5810
-225
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

110 files changed

+5810
-225
lines changed

agent/php_internal_instrument.c

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3840,9 +3840,37 @@ void nr_php_generate_internal_wrap_records(void) {
38403840
NR_INTERNAL_WRAPREC("sqlite3::exec", sqlite3_exec, sqlite3, 0, 0)
38413841

38423842
NR_INTERNAL_WRAPREC("pdo::__construct", pdo_construct, pdo_construct, 0, 0)
3843+
#if ZEND_MODULE_API_NO >= ZEND_8_4_X_API_NO
3844+
NR_INTERNAL_WRAPREC("pdo\\firebird::__construct", pdo_construct, pdo_construct, 0, 0)
3845+
NR_INTERNAL_WRAPREC("pdo\\mysql::__construct", pdo_construct, pdo_construct, 0, 0)
3846+
NR_INTERNAL_WRAPREC("pdo\\odbc::__construct", pdo_construct, pdo_construct, 0, 0)
3847+
NR_INTERNAL_WRAPREC("pdo\\pgsql::__construct", pdo_construct, pdo_construct, 0, 0)
3848+
NR_INTERNAL_WRAPREC("pdo\\sqlite::__construct", pdo_construct, pdo_construct, 0, 0)
3849+
#endif
38433850
NR_INTERNAL_WRAPREC("pdo::query", pdo_query, pdo_query, 0, 0)
3851+
#if ZEND_MODULE_API_NO >= ZEND_8_4_X_API_NO
3852+
NR_INTERNAL_WRAPREC("pdo\\firebird::query", pdo_query, pdo_query, 0, 0)
3853+
NR_INTERNAL_WRAPREC("pdo\\mysql::query", pdo_query, pdo_query, 0, 0)
3854+
NR_INTERNAL_WRAPREC("pdo\\odbc::query", pdo_query, pdo_query, 0, 0)
3855+
NR_INTERNAL_WRAPREC("pdo\\pgsql::query", pdo_query, pdo_query, 0, 0)
3856+
NR_INTERNAL_WRAPREC("pdo\\sqlite::query", pdo_query, pdo_query, 0, 0)
3857+
#endif
38443858
NR_INTERNAL_WRAPREC("pdo::exec", pdo_exec, pdo_exec, 0, 0)
3859+
#if ZEND_MODULE_API_NO >= ZEND_8_4_X_API_NO
3860+
NR_INTERNAL_WRAPREC("pdo\\firebird::exec", pdo_exec, pdo_exec, 0, 0)
3861+
NR_INTERNAL_WRAPREC("pdo\\mysql::exec", pdo_exec, pdo_exec, 0, 0)
3862+
NR_INTERNAL_WRAPREC("pdo\\odbc::exec", pdo_exec, pdo_exec, 0, 0)
3863+
NR_INTERNAL_WRAPREC("pdo\\pgsql::exec", pdo_exec, pdo_exec, 0, 0)
3864+
NR_INTERNAL_WRAPREC("pdo\\sqlite::exec", pdo_exec, pdo_exec, 0, 0)
3865+
#endif
38453866
NR_INTERNAL_WRAPREC("pdo::prepare", pdo_prepare, pdo_prepare, 0, 0)
3867+
#if ZEND_MODULE_API_NO >= ZEND_8_4_X_API_NO
3868+
NR_INTERNAL_WRAPREC("pdo\\firebird::prepare", pdo_prepare, pdo_prepare, 0, 0)
3869+
NR_INTERNAL_WRAPREC("pdo\\mysql::prepare", pdo_prepare, pdo_prepare, 0, 0)
3870+
NR_INTERNAL_WRAPREC("pdo\\odbc::prepare", pdo_prepare, pdo_prepare, 0, 0)
3871+
NR_INTERNAL_WRAPREC("pdo\\pgsql::prepare", pdo_prepare, pdo_prepare, 0, 0)
3872+
NR_INTERNAL_WRAPREC("pdo\\sqlite::prepare", pdo_prepare, pdo_prepare, 0, 0)
3873+
#endif
38463874
NR_INTERNAL_WRAPREC("pdostatement::execute", pdostmt_execute,
38473875
pdostatement_execute, 0, 0)
38483876

files/Dockerfile

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

9494
# install redis extension required by test_redis:
9595
RUN \

tests/include/config.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ function isset_or($check, $alternate = NULL)
1313

1414
$MYSQL_USER = isset_or('MYSQL_USER', 'root');
1515
$MYSQL_PASSWD = isset_or('MYSQL_PASSWD', 'root');
16-
$MYSQL_DB = 'information_schema'; // TODO: MSL comment here.
16+
$MYSQL_DB = isset_or('MYSQL_DB', 'information_schema');
1717
$MYSQL_HOST = isset_or('MYSQL_HOST', 'localhost');
1818
$MYSQL_PORT = isset_or('MYSQL_PORT', 3306);
1919
$MYSQL_SOCKET = isset_or('MYSQL_SOCKET', '');
@@ -75,6 +75,10 @@ function make_tracing_url($file)
7575
$PG_PORT = isset_or('PG_PORT', '5433');
7676
$PG_CONNECTION = "host=$PG_HOST port=$PG_PORT user=$PG_USER password=$PG_PW connect_timeout=1";
7777

78+
$PDO_PGSQL_DSN = 'pgsql:';
79+
$PDO_PGSQL_DSN .= 'host=' . $PG_HOST . ';';
80+
$PDO_PGSQL_DSN .= 'port=' . $PG_PORT . ';';
81+
7882
// Header used to track whether or not our CAT instrumentation interferes with
7983
// other existing headers.
8084
define('CUSTOMER_HEADER', 'Customer-Header');

tests/integration/events/test_database_duration.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ function test_slow_sql() {
4949
global $PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD;
5050

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

5555
test_slow_sql();
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
<?php
2+
/*
3+
* Copyright 2020 New Relic Corporation. All rights reserved.
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
/*DESCRIPTION
8+
When PDO base class constructor is used to create connection object
9+
to a database on a remote host via a TCP port,
10+
and database instance reporting is enabled, the agent should
11+
- not generate errors
12+
- record datastore metrics
13+
- record a datastore instance metric
14+
- record a datastore span event with instance information
15+
Moreover, when the query execution time exceeds the explain threshold,
16+
the agent should record a slow sql trace with database instance information.
17+
*/
18+
19+
/*SKIPIF
20+
<?php require(realpath (dirname ( __FILE__ )) . '/../../skipif_mysql.inc');
21+
*/
22+
23+
/*ENVIRONMENT
24+
DATASTORE_PRODUCT=MySQL
25+
DATASTORE_COLLECTION=test
26+
MYSQL_PORT=3306
27+
*/
28+
29+
/*INI
30+
;comment=Set explain_threshold to 0 to ensure that the slow query is recorded.
31+
newrelic.transaction_tracer.explain_threshold = 0
32+
*/
33+
34+
/*EXPECT_ERROR_EVENTS null*/
35+
36+
/*EXPECT
37+
ok - create table
38+
ok - drop table
39+
*/
40+
41+
/*EXPECT_METRICS_EXIST
42+
Datastore/all, 2
43+
Datastore/allOther, 2
44+
Datastore/instance/ENV[DATASTORE_PRODUCT]/ENV[MYSQL_HOST]/ENV[MYSQL_PORT], 2
45+
Datastore/ENV[DATASTORE_PRODUCT]/all, 2
46+
Datastore/ENV[DATASTORE_PRODUCT]/allOther, 2
47+
Datastore/operation/ENV[DATASTORE_PRODUCT]/create, 1
48+
Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create, 1
49+
Datastore/operation/ENV[DATASTORE_PRODUCT]/drop, 1
50+
Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop, 1
51+
Supportability/TxnData/SlowSQL, 1
52+
*/
53+
54+
/*EXPECT_SLOW_SQLS
55+
[
56+
[
57+
[
58+
"OtherTransaction/php__FILE__",
59+
"<unknown>",
60+
"?? SQL id",
61+
"DROP TABLE ENV[DATASTORE_COLLECTION];",
62+
"Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop",
63+
1,
64+
"?? total time",
65+
"?? min time",
66+
"?? max time",
67+
{
68+
"backtrace": [
69+
"/ in PDO::exec called at .*\/",
70+
" in test_instance_reporting called at __FILE__ (??)"
71+
],
72+
"host": "ENV[MYSQL_HOST]",
73+
"port_path_or_id": "ENV[MYSQL_PORT]",
74+
"database_name": "ENV[MYSQL_DB]"
75+
}
76+
],
77+
[
78+
"OtherTransaction/php__FILE__",
79+
"<unknown>",
80+
"?? SQL id",
81+
"CREATE TABLE ENV[DATASTORE_COLLECTION] (id INT, description VARCHAR(?));",
82+
"Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create",
83+
1,
84+
"?? total time",
85+
"?? min time",
86+
"?? max time",
87+
{
88+
"backtrace": [
89+
"/ in PDO::exec called at .*\/",
90+
" in test_instance_reporting called at __FILE__ (??)"
91+
],
92+
"host": "ENV[MYSQL_HOST]",
93+
"port_path_or_id": "ENV[MYSQL_PORT]",
94+
"database_name": "ENV[MYSQL_DB]"
95+
}
96+
]
97+
]
98+
]
99+
*/
100+
101+
/*EXPECT_SPAN_EVENTS_LIKE
102+
[
103+
[
104+
{
105+
"category": "datastore",
106+
"type": "Span",
107+
"guid": "??",
108+
"traceId": "??",
109+
"transactionId": "??",
110+
"name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create",
111+
"timestamp": "??",
112+
"duration": "??",
113+
"priority": "??",
114+
"sampled": true,
115+
"parentId": "??",
116+
"span.kind": "client",
117+
"component": "ENV[DATASTORE_PRODUCT]"
118+
},
119+
{},
120+
{
121+
"peer.hostname": "ENV[MYSQL_HOST]",
122+
"peer.address": "ENV[MYSQL_HOST]:ENV[MYSQL_PORT]",
123+
"db.instance": "ENV[MYSQL_DB]",
124+
"db.statement": "CREATE TABLE ENV[DATASTORE_COLLECTION] (id INT, description VARCHAR(?));"
125+
}
126+
],
127+
[
128+
{
129+
"category": "datastore",
130+
"type": "Span",
131+
"guid": "??",
132+
"traceId": "??",
133+
"transactionId": "??",
134+
"name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop",
135+
"timestamp": "??",
136+
"duration": "??",
137+
"priority": "??",
138+
"sampled": true,
139+
"parentId": "??",
140+
"span.kind": "client",
141+
"component": "ENV[DATASTORE_PRODUCT]"
142+
},
143+
{},
144+
{
145+
"peer.hostname": "ENV[MYSQL_HOST]",
146+
"peer.address": "ENV[MYSQL_HOST]:ENV[MYSQL_PORT]",
147+
"db.instance": "ENV[MYSQL_DB]",
148+
"db.statement": "DROP TABLE ENV[DATASTORE_COLLECTION];"
149+
}
150+
]
151+
]
152+
*/
153+
154+
require_once(realpath (dirname ( __FILE__ )) . '/../../test_instance_reporting.inc');
155+
require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php');
156+
157+
test_instance_reporting(new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD), 0);
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
<?php
2+
/*
3+
* Copyright 2020 New Relic Corporation. All rights reserved.
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
/*DESCRIPTION
8+
When PDO base class constructor is used to create connection object
9+
to a database on a localhost via a unix socket,
10+
and database instance reporting is enabled, the agent should
11+
- not generate errors
12+
- record datastore metrics
13+
- record a datastore instance metric
14+
- record a datastore span event with instance information
15+
Moreover, when the query execution time exceeds the explain threshold,
16+
the agent should record a slow sql trace with database instance information.
17+
*/
18+
19+
/*SKIPIF
20+
<?php require(realpath (dirname ( __FILE__ )) . '/../../skipif_mysql.inc');
21+
*/
22+
23+
/*ENVIRONMENT
24+
DATASTORE_PRODUCT=MySQL
25+
DATASTORE_COLLECTION=test
26+
*/
27+
28+
/*INI
29+
;comment=Set explain_threshold to 0 to ensure that the slow query is recorded.
30+
newrelic.transaction_tracer.explain_threshold = 0
31+
*/
32+
33+
/*EXPECT_ERROR_EVENTS null*/
34+
35+
/*EXPECT
36+
ok - create table
37+
ok - drop table
38+
*/
39+
40+
/*EXPECT_METRICS_EXIST
41+
Datastore/all, 2
42+
Datastore/allOther, 2
43+
Datastore/instance/ENV[DATASTORE_PRODUCT]/__HOST__/ENV[MYSQL_SOCKET], 2
44+
Datastore/ENV[DATASTORE_PRODUCT]/all, 2
45+
Datastore/ENV[DATASTORE_PRODUCT]/allOther, 2
46+
Datastore/operation/ENV[DATASTORE_PRODUCT]/create, 1
47+
Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create, 1
48+
Datastore/operation/ENV[DATASTORE_PRODUCT]/drop, 1
49+
Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop, 1
50+
Supportability/TxnData/SlowSQL, 1
51+
*/
52+
53+
/*EXPECT_SLOW_SQLS
54+
[
55+
[
56+
[
57+
"OtherTransaction/php__FILE__",
58+
"<unknown>",
59+
"?? SQL id",
60+
"DROP TABLE ENV[DATASTORE_COLLECTION];",
61+
"Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop",
62+
1,
63+
"?? total time",
64+
"?? min time",
65+
"?? max time",
66+
{
67+
"backtrace": [
68+
"/ in PDO::exec called at .*\/",
69+
" in test_instance_reporting called at __FILE__ (??)"
70+
],
71+
"host": "__HOST__",
72+
"port_path_or_id": "ENV[MYSQL_SOCKET]",
73+
"database_name": "ENV[MYSQL_DB]"
74+
}
75+
],
76+
[
77+
"OtherTransaction/php__FILE__",
78+
"<unknown>",
79+
"?? SQL id",
80+
"CREATE TABLE ENV[DATASTORE_COLLECTION] (id INT, description VARCHAR(?));",
81+
"Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create",
82+
1,
83+
"?? total time",
84+
"?? min time",
85+
"?? max time",
86+
{
87+
"backtrace": [
88+
"/ in PDO::exec called at .*\/",
89+
" in test_instance_reporting called at __FILE__ (??)"
90+
],
91+
"host": "__HOST__",
92+
"port_path_or_id": "ENV[MYSQL_SOCKET]",
93+
"database_name": "ENV[MYSQL_DB]"
94+
}
95+
]
96+
]
97+
]
98+
*/
99+
100+
/*EXPECT_SPAN_EVENTS_LIKE
101+
[
102+
[
103+
{
104+
"category": "datastore",
105+
"type": "Span",
106+
"guid": "??",
107+
"traceId": "??",
108+
"transactionId": "??",
109+
"name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create",
110+
"timestamp": "??",
111+
"duration": "??",
112+
"priority": "??",
113+
"sampled": true,
114+
"parentId": "??",
115+
"span.kind": "client",
116+
"component": "ENV[DATASTORE_PRODUCT]"
117+
},
118+
{},
119+
{
120+
"peer.hostname": "__HOST__",
121+
"peer.address": "__HOST__:ENV[MYSQL_SOCKET]",
122+
"db.instance": "ENV[MYSQL_DB]",
123+
"db.statement": "CREATE TABLE ENV[DATASTORE_COLLECTION] (id INT, description VARCHAR(?));"
124+
}
125+
],
126+
[
127+
{
128+
"category": "datastore",
129+
"type": "Span",
130+
"guid": "??",
131+
"traceId": "??",
132+
"transactionId": "??",
133+
"name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop",
134+
"timestamp": "??",
135+
"duration": "??",
136+
"priority": "??",
137+
"sampled": true,
138+
"parentId": "??",
139+
"span.kind": "client",
140+
"component": "ENV[DATASTORE_PRODUCT]"
141+
},
142+
{},
143+
{
144+
"peer.hostname": "__HOST__",
145+
"peer.address": "__HOST__:ENV[MYSQL_SOCKET]",
146+
"db.instance": "ENV[MYSQL_DB]",
147+
"db.statement": "DROP TABLE ENV[DATASTORE_COLLECTION];"
148+
}
149+
]
150+
]
151+
*/
152+
153+
require_once(realpath (dirname ( __FILE__ )) . '/../../test_instance_reporting.inc');
154+
require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php');
155+
156+
$DSN = 'mysql:';
157+
$DSN .= 'unix_socket=' . $MYSQL_SOCKET . ';';
158+
$DSN .= 'dbname=' . $MYSQL_DB . ';';
159+
160+
test_instance_reporting(new PDO($DSN, $MYSQL_USER, $MYSQL_PASSWD), 0);

0 commit comments

Comments
 (0)