From fa73a84a77ecd69b24a4abdb5088fb29bd4f6da1 Mon Sep 17 00:00:00 2001 From: Amber Sistla Date: Wed, 24 Aug 2022 10:03:51 -0700 Subject: [PATCH 01/56] chore: Update branches that the GHA PR runner monitors. (#509) --- .github/workflows/agent-build.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/agent-build.yml b/.github/workflows/agent-build.yml index 26876327b..29ef76213 100644 --- a/.github/workflows/agent-build.yml +++ b/.github/workflows/agent-build.yml @@ -18,13 +18,15 @@ on: push: branches: - main - - 'php8' - 'dev' + - 'dev2' + - 'oapi' pull_request: branches: - main - - 'php8' - 'dev' + - 'dev2' + - 'oapi' jobs: agent_pr: From a3c4cb58b1add69c1ca566e74c760ed18747b3de Mon Sep 17 00:00:00 2001 From: Amber Sistla Date: Mon, 12 Sep 2022 06:48:09 -0700 Subject: [PATCH 02/56] feat(agent): Registered function begin/end and error handlers with Observer API (#501) * feat(agent): Registered function begin/end and error handlers with Observer API 1) Registered function begin/end handlers with Observer API 2) Created the function begin/end handler stubs. Full functionality is schedule for another ticket. 3) Registered currently existing error handler with Observer API 4) created php_observer.c/h module to contain the observer api logic. Testing: 1) Verified new function begin/end handlers were registered correctly and received zend_execute_data from PHP engine. 2) Current test cases verified that registering our current error handler directly caused no change in functionality. Additional * feat(agent): Don't call handlers for internal functions. Currently internal functions are not handled by OAPI, but they will be in 8.2. These functions are tailored to USER functions (similar to nr_php_execute) and we don't want internal functions filtered to these handlers. This will default to INTERNAL functions being handled by the current implementation. To change in the future, it's possible we'd need to implement handlers specific for internal functions (similar to nr_php_execute_internal). --- agent/config.m4 | 2 +- agent/php_error.c | 25 +++++++----- agent/php_execute.c | 42 ++++++++++++++++++- agent/php_execute.h | 2 + agent/php_hooks.h | 10 ++--- agent/php_includes.h | 4 ++ agent/php_minit.c | 5 +++ agent/php_observer.c | 97 ++++++++++++++++++++++++++++++++++++++++++++ agent/php_observer.h | 68 +++++++++++++++++++++++++++++++ 9 files changed, 236 insertions(+), 19 deletions(-) create mode 100644 agent/php_observer.c create mode 100644 agent/php_observer.h diff --git a/agent/config.m4 b/agent/config.m4 index 9547c8044..1de342c03 100644 --- a/agent/config.m4 +++ b/agent/config.m4 @@ -190,7 +190,7 @@ if test "$PHP_NEWRELIC" = "yes"; then php_explain_pdo_mysql.c php_extension.c php_file_get_contents.c \ php_globals.c php_hash.c php_header.c php_httprequest_send.c \ php_internal_instrument.c php_minit.c php_mshutdown.c php_mysql.c \ - php_mysqli.c php_newrelic.c php_nrini.c php_output.c php_pdo.c \ + php_mysqli.c php_newrelic.c php_nrini.c php_observer.c php_output.c php_pdo.c \ php_pdo_mysql.c php_pdo_pgsql.c php_pgsql.c php_psr7.c php_redis.c \ php_rinit.c php_rshutdown.c php_samplers.c php_stack.c \ php_stacked_segment.c php_txn.c php_user_instrument.c \ diff --git a/agent/php_error.c b/agent/php_error.c index 918518c63..0de933b3b 100644 --- a/agent/php_error.c +++ b/agent/php_error.c @@ -449,14 +449,20 @@ static int nr_php_should_record_error(int type, const char* format TSRMLS_DC) { } #if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO +/* Prior to PHP8 these error_filename and error_lineno were only used to pass + * on to the error handler that the agent overwrote. With PHP8+, these values + * are currently unused since the agent is already recording the stack trace. + * HOWEVER, when code level metrics(CLM) are incorporated, these values can be + * used to add lineno and filename to error traces. + */ void nr_php_error_cb(int type, - zend_string* error_filename, - uint error_lineno, + zend_string* error_filename NRUNUSED, + uint error_lineno NRUNUSED, zend_string* message) { #elif ZEND_MODULE_API_NO == ZEND_8_0_X_API_NO void nr_php_error_cb(int type, - const char* error_filename, - uint error_lineno, + const char* error_filename NRUNUSED, + uint error_lineno NRUNUSED, zend_string* message) { #else void nr_php_error_cb(int type, @@ -502,17 +508,16 @@ void nr_php_error_cb(int type, } /* - * Call through to the actual error handler. + * Call through to the actual error handler for PHP 7.4 and below. + * For PHP 8+ we have registered our error handler with the Observer + * API so there is no need to callback to the original. */ - if (0 != NR_PHP_PROCESS_GLOBALS(orig_error_cb)) { #if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO + if (0 != NR_PHP_PROCESS_GLOBALS(orig_error_cb)) { NR_PHP_PROCESS_GLOBALS(orig_error_cb) (type, error_filename, error_lineno, format, args); -#else - NR_PHP_PROCESS_GLOBALS(orig_error_cb) - (type, error_filename, error_lineno, message); -#endif /* PHP < 8.0 */ } +#endif /* PHP < 8.0 */ } nr_status_t nr_php_error_record_exception(nrtxn_t* txn, diff --git a/agent/php_execute.c b/agent/php_execute.c index 5703b2f8d..b367c3c14 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -29,9 +29,9 @@ #include "util_url.h" #include "util_metrics.h" #include "util_number_converter.h" -#include "php_execute.h" #include "fw_support.h" #include "fw_hooks.h" +#include "php_observer.h" /* * This wall of text is important. Read it. Understand it. Really. @@ -1557,3 +1557,43 @@ void nr_php_user_instrumentation_from_opcache(TSRMLS_D) { end: nr_php_zval_free(&status); } + +/* + * nr_php_observer_fcall_begin and nr_php_observer_fcall_end + * are Observer API function handlers that are the entry point to instrumenting + * userland code and should replicate the functionality of + * nr_php_execute_enabled, nr_php_execute, and nr_php_execute_show that are used + * when hooking in via zend_execute_ex. + * + * Observer API functionality was added with PHP 8.0. + * See nr_php_observer.h/c for more information. + */ +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO /* PHP8+ */ +void nr_php_observer_fcall_begin(zend_execute_data* execute_data) { + /* + * Instrument the function. + * This and any other needed helper functions will replace: + * nr_php_execute_enabled + * nr_php_execute + * nr_php_execute_show + */ + + if (NULL == execute_data) { + return; + } +} + +void nr_php_observer_fcall_end(zend_execute_data* execute_data, + zval* return_value) { + /* + * Instrument the function. + * This and any other needed helper functions will replace: + * nr_php_execute_enabled + * nr_php_execute + * nr_php_execute_show + */ + if ((NULL == execute_data) || (NULL == return_value)) { + return; + } +} +#endif diff --git a/agent/php_execute.h b/agent/php_execute.h index dfefdc8c8..a594a28a2 100644 --- a/agent/php_execute.h +++ b/agent/php_execute.h @@ -64,6 +64,8 @@ extern void nr_framework_create_metric(TSRMLS_D); * This is necessary to correctly instrument frameworks and libraries * that are preloaded. */ + extern void nr_php_user_instrumentation_from_opcache(TSRMLS_D); +#include "php_observer.h" #endif /* PHP_EXECUTE_HDR */ diff --git a/agent/php_hooks.h b/agent/php_hooks.h index 5f21f6f98..6f1e1bd47 100644 --- a/agent/php_hooks.h +++ b/agent/php_hooks.h @@ -38,13 +38,9 @@ extern void nr_php_execute(NR_EXECUTE_PROTO TSRMLS_DC); * are strongly encouraged to use the new error notification API instead. * Error notification callbacks are guaranteed to be called regardless of the * users error_reporting setting or userland error handler return values. - * Register notification callbacks during MINIT of an extension: -void my_error_notify_cb(int type, - const char *error_filename, - uint32_t error_lineno, - zend_string *message) { - } - zend_register_error_notify_callback(my_error_notify_cb); + * + * The register notification callbacks during MINIT of an extension are done in + * `php_observer.c/h`. */ #if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO extern void nr_php_error_cb(int type, diff --git a/agent/php_includes.h b/agent/php_includes.h index b53b26abc..764a846fd 100644 --- a/agent/php_includes.h +++ b/agent/php_includes.h @@ -53,6 +53,10 @@ #define ZEND_8_0_X_API_NO 20200930 #define ZEND_8_1_X_API_NO 20210902 +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO /* PHP8+ */ +#include "Zend/zend_observer.h" +#endif + #if ZEND_MODULE_API_NO >= ZEND_5_6_X_API_NO #include "Zend/zend_virtual_cwd.h" #else /* PHP < 5.6 */ diff --git a/agent/php_minit.c b/agent/php_minit.c index 66dc70e1e..7081c927c 100644 --- a/agent/php_minit.c +++ b/agent/php_minit.c @@ -38,6 +38,8 @@ #include "fw_laravel.h" #include "lib_guzzle4.h" +#include "php_observer.h" + static void php_newrelic_init_globals(zend_newrelic_globals* nrg) { if (nrunlikely(NULL == nrg)) { return; @@ -408,6 +410,7 @@ PHP_MINIT_FUNCTION(newrelic) { zend_extension dummy; #else char dummy[] = "newrelic"; + nr_php_observer_minit(); #endif (void)type; @@ -723,6 +726,7 @@ void nr_php_late_initialization(void) { * forward the errors, so if a user has Xdebug loaded, we do not install * our own error callback handler. Otherwise, we do. */ +#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO /* < PHP8 */ if (0 == zend_get_extension("Xdebug")) { NR_PHP_PROCESS_GLOBALS(orig_error_cb) = zend_error_cb; zend_error_cb = nr_php_error_cb; @@ -731,6 +735,7 @@ void nr_php_late_initialization(void) { "the Xdebug extension prevents the New Relic agent from " "gathering errors. No errors will be recorded."); } +#endif /* end of < PHP8 */ /* * Install our signal handler, unless the user has set a special flag diff --git a/agent/php_observer.c b/agent/php_observer.c new file mode 100644 index 000000000..70fee1793 --- /dev/null +++ b/agent/php_observer.c @@ -0,0 +1,97 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * This file handles the initialization that happens once per module load. + */ +#include "php_agent.h" + +#include +#include + +#include "php_api_distributed_trace.h" +#include "php_environment.h" +#include "php_error.h" +#include "php_extension.h" +#include "php_globals.h" +#include "php_header.h" +#include "php_hooks.h" +#include "php_internal_instrument.h" +#include "php_samplers.h" +#include "php_user_instrument.h" +#include "php_vm.h" +#include "php_wrapper.h" +#include "fw_laravel.h" +#include "lib_guzzle4.h" +#include "lib_guzzle6.h" +#include "nr_agent.h" +#include "nr_app.h" +#include "nr_banner.h" +#include "nr_daemon_spawn.h" +#include "util_logging.h" +#include "util_memory.h" +#include "util_signals.h" +#include "util_strings.h" +#include "util_syscalls.h" +#include "util_threads.h" + +#include "php_observer.h" +#include "php_execute.h" + +/* + * Observer API functionality was added with PHP 8.0. + * + * The Observer API provide function handlers that trigger on every userland + * function begin and end. The handlers provide all zend_execute_data and the + * end handler provides the return value pointer. The previous way to hook into + * PHP was via zend_execute_ex which will hook all userland function calls with + * significant overhead for doing the call. However, depending on user stack + * size settings, it could potentially generate an extremely deep call stack in + * PHP because zend_execute_ex limits stack size to whatever user settings + * are. Observer API bypasses the stack overflow issue that an agent could run + * into when intercepting userland calls. Additionally, with PHP 8.0, JIT + * optimizations could optimize out a call to zend_execute_ex and the agent + * would not be able to overwite that call properly as the agent wouldn't have + * access to the JITed information. This could lead to segfaults and caused PHP + * to decide to disable JIT when detecting extensions that overwrote + * zend_execute_ex. + * + * It only provides ZEND_USER_FUNCTIONS yet as it was assumed mechanisms already + * exist to monitor internal functions by overwriting internal function + * handlers. This will be included in PHP 8.2: Registered + * zend_observer_fcall_init handlers are now also called for internal functions. + * + * Without overwriting the execute function and therefore being responsible for + * continuing the execution of ALL functions that we intercepted, the agent is + * provided zend_execute_data on each function start/end and is then able to use + * it with our currently existing logic and instrumentation. + */ + +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO /* PHP8+ */ +/* + * Register the begin and end function handlers with the Observer API. + */ +static zend_observer_fcall_handlers nr_php_fcall_register_handlers( + zend_execute_data* execute_data) { + zend_observer_fcall_handlers handlers = {NULL, NULL}; + if (NULL == execute_data) { + return handlers; + } + if ((NULL == execute_data->func) + || (ZEND_INTERNAL_FUNCTION == execute_data->func->type)) { + return handlers; + } + handlers.begin = nr_php_observer_fcall_begin; + handlers.end = nr_php_observer_fcall_end; + return handlers; +} + +void nr_php_observer_minit() { + /* + * Register the Observer API handlers. + */ + zend_observer_fcall_register(nr_php_fcall_register_handlers); + zend_observer_error_register(nr_php_error_cb); +} + +#endif diff --git a/agent/php_observer.h b/agent/php_observer.h new file mode 100644 index 000000000..d9e693140 --- /dev/null +++ b/agent/php_observer.h @@ -0,0 +1,68 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* + * This file is the wrapper for PHP 8+ Observer API (OAPI) functionality. + * + * The registered function handlers are the entry points of instrumentation and + * are implemented in php_execute.c which contains the brains/helper functions + * required to monitor PHP. + */ + +#ifndef NEWRELIC_PHP_AGENT_PHP_OBSERVER_H +#define NEWRELIC_PHP_AGENT_PHP_OBSERVER_H + +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO /* PHP8+ */ + +#include "Zend/zend_observer.h" + +/* + * Purpose : Register the OAPI function handlers and any other minit actions. + * + * Params : None + * + * Returns : Void. + */ +void nr_php_observer_minit(); + +/* + * Purpose : Call the necessary functions needed to instrument a function by + * updating a transaction or segment for a function that has just + * started. This function is registered via the Observer API and will be called + * by the zend engine every time a function begins. The zend engine directly + * provides the zend_execute_data which has all details we need to know about + * the function. This and nr_php_execute_observer_fcall_end sum to provide all + * the functionality of nr_php_execute and nr_php _execute_enabled and as such + * will use all the helper functions they also used. + * + * + * Params : 1. zend_execute_data: everything we need to know about the + * function. + * + * Returns : Void. + */ +void nr_php_observer_fcall_begin(zend_execute_data* execute_data); +/* + * Purpose : Call the necessary functions needed to instrument a function when + * updating a transaction or segment for a function that has just + * ended. This function is registered via the Observer API and will be called by + * the zend engine every time a function ends. The zend engine directly + * provides the zend_execute_data and the return_value pointer, both of which + * have all details that the agent needs to know about the function. This and + * nr_php_execute_observer_fcall_start sum to provide all the functionality of + * nr_php_execute and nr_php_execute_enabled and as such will use all the helper + * functions they also used. + * + * + * Params : 1. zend_execute_data: everything to know about the function. + * 2. return_value: function return value information + * + * Returns : Void. + */ +void nr_php_observer_fcall_end(zend_execute_data* execute_data, + zval* return_value); +#endif /* PHP8+ */ + +#endif // NEWRELIC_PHP_AGENT_PHP_OBSERVER_H From 73d28169dad41d93819263b4ced48a8cccd2ff66 Mon Sep 17 00:00:00 2001 From: Amber Sistla Date: Mon, 3 Oct 2022 06:26:18 -0700 Subject: [PATCH 03/56] feat(agent): Add/Update functions to utilize the OAPI zend_execute_data. (#505) * feat(agent): Add/Update functions to utilize the OAPI zend_execute_data. 1) Added 4 new functions for use with OAPI instrumentation: extern const char* nr_php_zend_execute_data_function_name( const zend_execute_data* execute_data); extern const char* nr_php_zend_execute_data_filename( const zend_execute_data* execute_data); extern const char* nr_php_zend_execute_data_scope_name( const zend_execute_data* execute_data); extern uint32_t nr_php_zend_execute_data_lineno( const zend_execute_data* execute_data); 2) Added test cases to test new functionality. 3) Updated the following to reflect new OAPI paradigm and add additional checks for robustness: * #define NR_PHP_USER_FN_THIS() * nr_php_execute_scope 3) Added unit test cases to test new functions 4) Also verified functionality via integration tests. * refactor(agent): If called correctly, new functions should work with PHP 7+. --- agent/php_agent.c | 64 +++++++++++++++++ agent/php_agent.h | 75 +++++++++++++++++++- agent/tests/test_agent.c | 146 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 283 insertions(+), 2 deletions(-) diff --git a/agent/php_agent.c b/agent/php_agent.c index 0e8882d1d..f2ec80ba3 100644 --- a/agent/php_agent.c +++ b/agent/php_agent.c @@ -1141,3 +1141,67 @@ bool nr_php_function_is_static_method(const zend_function* func) { return (func->common.fn_flags & ZEND_ACC_STATIC); } + +#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ +const char* nr_php_zend_execute_data_function_name( + const zend_execute_data* execute_data) { + zend_string* function_name = NULL; + + if ((NULL == execute_data) || (NULL == execute_data->func)) { + return NULL; + } + + function_name = execute_data->func->common.function_name; + + if ((NULL == function_name) + && (ZEND_USER_FUNCTION == execute_data->func->type)) { + return "main"; + } + return function_name ? ZSTR_VAL(function_name) : NULL; +} + +const char* nr_php_zend_execute_data_filename( + const zend_execute_data* execute_data) { + zend_string* filename = NULL; + while ( + execute_data + && (!execute_data->func || !ZEND_USER_CODE(execute_data->func->type))) { + execute_data = execute_data->prev_execute_data; + } + if (execute_data) { + filename = execute_data->func->op_array.filename; + } + return filename ? ZSTR_VAL(filename) : NULL; +} + +const char* nr_php_zend_execute_data_scope_name( + const zend_execute_data* execute_data) { + zend_class_entry* ce = NULL; + + while (execute_data) { + if (execute_data->func + && (ZEND_USER_CODE(execute_data->func->type) + || execute_data->func->common.scope)) { + ce = execute_data->func->common.scope; + execute_data = NULL; + } else { + execute_data = execute_data->prev_execute_data; + } + } + return ce ? ZSTR_VAL(ce->name) : NULL; +} + +uint32_t nr_php_zend_execute_data_lineno( + const zend_execute_data* execute_data) { + while ( + execute_data + && (!execute_data->func || !ZEND_USER_CODE(execute_data->func->type))) { + execute_data = execute_data->prev_execute_data; + } + if (execute_data) { + return execute_data->opline ? execute_data->opline->lineno : 0; + } + return 0; +} + +#endif /* PHP 7+ */ diff --git a/agent/php_agent.h b/agent/php_agent.h index 0d5415a99..879c4c0b6 100644 --- a/agent/php_agent.h +++ b/agent/php_agent.h @@ -404,7 +404,23 @@ static inline zval* nr_php_execute_scope(zend_execute_data* execute_data) { return NULL; } -#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP 7.0+ */ +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO /* PHP 8.0+ */ + while (execute_data) { + if ((Z_TYPE(execute_data->This) == IS_OBJECT) + || (Z_CE(execute_data->This))) { + return &execute_data->This; + } else if (execute_data->func) { + if (execute_data->func->type != ZEND_INTERNAL_FUNCTION + || execute_data->func->common.scope) { + return NULL; + } + } + execute_data = execute_data->prev_execute_data; + } + return NULL; + +#elif ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO \ + && ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO /* PHP 7.0 - 7.4 */ return &execute_data->This; #else return execute_data->object; @@ -715,7 +731,10 @@ nr_php_ini_entry_name_length(const zend_ini_entry* entry) { #define NR_PHP_INTERNAL_FN_THIS() getThis() -#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP 7.0+ */ +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO /* PHP 8.0+ */ +#define NR_PHP_USER_FN_THIS() nr_php_execute_scope(execute_data) +#elif ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO \ + && ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO /* PHP 7.0 - 7.4 */ #define NR_PHP_USER_FN_THIS() getThis() #else #define NR_PHP_USER_FN_THIS() EG(This) @@ -791,6 +810,58 @@ extern bool nr_php_function_is_static_method(const zend_function* func); */ extern zend_execute_data* nr_get_zend_execute_data(NR_EXECUTE_PROTO TSRMLS_DC); +#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ +/* + * Purpose : Return a pointer to the function name of zend_execute_data. + * + * Params : 1. zend_execute_data. + * + * Returns : A pointer to string, ownership of does NOT pass to the caller and + * string must be dupped if it needs to persist, + * or NULL if the zend_execute_data is invalid. + * + */ +extern const char* nr_php_zend_execute_data_function_name( + const zend_execute_data* execute_data); +/* + * Purpose : Return a pointer to the filename of zend_execute_data. + * + * Params : 1. zend_execute_data. + * + * Returns : A pointer to string, ownership of does NOT pass to the caller and + * string must be dupped if it needs to persist, + * or NULL if the zend_execute_data is invalid. + * + */ +extern const char* nr_php_zend_execute_data_filename( + const zend_execute_data* execute_data); + +/* + * Purpose : Return a pointer to the scope(i.e., class) name of + * zend_execute_data. + * + * Params : 1. zend_execute_data. + * + * Returns : A pointer to string, ownership of does NOT pass to the caller and + * string must be dupped if it needs to persist, + * or NULL if the zend_execute_data is invalid. + * + */ +extern const char* nr_php_zend_execute_data_scope_name( + const zend_execute_data* execute_data); + +/* + * Purpose : Return a uint32_t line number value of zend_execute_data. + * + * Params : 1. zend_execute_data. + * + * Returns : uint32_t lineno value + * + */ +extern uint32_t nr_php_zend_execute_data_lineno( + const zend_execute_data* execute_data); +#endif /* PHP 7+ */ + #if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ /* diff --git a/agent/tests/test_agent.c b/agent/tests/test_agent.c index a2b7bdfa5..6ad98df4d 100644 --- a/agent/tests/test_agent.c +++ b/agent/tests/test_agent.c @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ #include "tlib_php.h" +#include "tlib_main.h" #include "php_agent.h" #include "php_call.h" @@ -559,6 +560,144 @@ static void test_default_address() { #if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ +static void test_nr_php_zend_execute_data_function_name() { + zend_function* func; + zend_execute_data execute_data = {0}; + + /* + * Test : Invalid arguments, NULL zend_execute_data + */ + tlib_pass_if_null("NULL zend_execute_data should return NULL", + nr_php_zend_execute_data_function_name(NULL)); + + /* + * Test : Invalid arguments. + */ + tlib_pass_if_null("NULL zend_function should return NULL", + nr_php_zend_execute_data_function_name(&execute_data)); + + /* + * Test : Normal operation. + */ + func = nr_php_find_function("newrelic_get_request_metadata"); + execute_data.func = func; + tlib_pass_if_str_equal( + "Unexpected function name", "newrelic_get_request_metadata", + nr_php_zend_execute_data_function_name(&execute_data TSRMLS_CC)); +} + +static void test_nr_php_zend_execute_data_filename() { + zend_function func = {0}; + zend_string* filename = NULL; + zend_execute_data execute_data = {0}; + + /* + * Test : Invalid arguments, NULL zend_execute_data + */ + tlib_pass_if_null("NULL zend_execute_data should return NULL", + nr_php_zend_execute_data_filename(NULL TSRMLS_CC)); + + /* + * Test : Null function. + */ + tlib_pass_if_null("NULL zend_function should return NULL", + nr_php_zend_execute_data_filename(&execute_data TSRMLS_CC)); + + /* + * Test : Function exists, op_array doesn't. + */ + execute_data.func = &func; + tlib_pass_if_null("NULL op_array should return NULL", + nr_php_zend_execute_data_filename(&execute_data TSRMLS_CC)); + + /* + * Test : Function exists, op_array exists. + */ + filename = zend_string_init("myfilename\\is\\here", + strlen("myfilename\\is\\here"), 0); + func.op_array.filename = filename; + execute_data.func = &func; + tlib_pass_if_str_equal( + "Filename should be displayed", ZSTR_VAL(filename), + nr_php_zend_execute_data_filename(&execute_data TSRMLS_CC)); + zend_string_release(filename); +} + +static void test_nr_php_zend_execute_data_scope_name() { + zend_function func = {0}; + zend_string* scope_name = NULL; + zend_execute_data execute_data = {0}; + zend_class_entry ce = {0}; + + /* + * Test : Invalid arguments, NULL zend_execute_data + */ + tlib_pass_if_null("NULL zend_execute_data should return NULL", + nr_php_zend_execute_data_scope_name(NULL TSRMLS_CC)); + + /* + * Test : Invalid arguments. + */ + tlib_pass_if_null( + "NULL zend_function should return NULL", + nr_php_zend_execute_data_scope_name(&execute_data TSRMLS_CC)); + + /* + * Test : Function exists, but no class scope. + */ + execute_data.func = &func; + tlib_pass_if_null( + "NULL op_array should return NULL", + nr_php_zend_execute_data_scope_name(&execute_data TSRMLS_CC)); + + /* + * Test : Function exists, class scope exists. + */ + execute_data.func = &func; + scope_name = zend_string_init("NewRelic\\Integration", + strlen("NewRelic\\Integration"), 0); + ce.name = scope_name; + execute_data.func->common.scope = &ce; + tlib_pass_if_str_equal( + "Unexpected scope name", ZSTR_VAL(scope_name), + nr_php_zend_execute_data_scope_name(&execute_data TSRMLS_CC)); + zend_string_release(scope_name); +} + +static void test_nr_php_zend_execute_data_lineno() { + zend_function func = {0}; + zend_op opline = {0}; + zend_execute_data execute_data = {0}; + + /* + * Test : Invalid arguments, NULL zend_execute_data + */ + tlib_pass_if_uint32_t_equal("NULL zend_execute_data should return 0", 0, + nr_php_zend_execute_data_lineno(NULL TSRMLS_CC)); + + /* + * Test : Invalid arguments. + */ + tlib_pass_if_uint32_t_equal( + "NULL zend_function should return 0", 0, + nr_php_zend_execute_data_lineno(&execute_data TSRMLS_CC)); + + /* + * Test : Normal operation. + */ + execute_data.func = &func; + + opline.lineno = 4; + execute_data.opline = &opline; + tlib_pass_if_uint32_t_equal( + "Unexpected lineno name", 4, + nr_php_zend_execute_data_lineno(&execute_data TSRMLS_CC)); +} + +#endif /* PHP 7+ */ + +#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ + static void test_nr_php_zend_function_lineno() { zend_function func = {0}; @@ -608,6 +747,13 @@ void test_main(void* p NRUNUSED) { * Tests that require state and will handle their own request startup and * shutdown. */ +#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ + test_nr_php_zend_execute_data_function_name(); + test_nr_php_zend_execute_data_filename(); + test_nr_php_zend_execute_data_lineno(); + test_nr_php_zend_execute_data_scope_name(); +#endif /* PHP 7+ */ + #if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ test_nr_php_zend_function_lineno(); #endif /* PHP 7+ */ From 53658788c18c18aac2e386ec13f00b4c5f48f6f4 Mon Sep 17 00:00:00 2001 From: Amber Sistla Date: Thu, 6 Oct 2022 06:59:20 -0700 Subject: [PATCH 04/56] feat(agent): Propagate OAPI return values and update return value functions. (#516) * feat(agent): Propagate OAPI return values and update return value functions. 1) updated macros to pass the OAPI given return value throughout the userland system. 2) changed nr_php_get_return_value to return oapi given pointer 3) Added the OVERWRITE_ZEND_EXECUTE_DATA to allow us to toggle off during feature addition, but toggle off to maintain CI as long as possible. It also gives the flexibility to revert to the instrumentation prior to OAPI. 4) macro to toggle between just using the OAPI return value when OAPI is enabled or calling nr_php_get_return_value_ptr when it is not OAPI. --- agent/fw_drupal.c | 2 +- agent/fw_drupal8.c | 10 +++---- agent/fw_laravel.c | 2 +- agent/fw_laravel_queue.c | 2 +- agent/fw_magento2.c | 10 +++---- agent/fw_mediawiki.c | 2 +- agent/fw_slim.c | 2 +- agent/fw_wordpress.c | 2 +- agent/lib_predis.c | 2 +- agent/lib_zend_http.c | 7 ++--- agent/php_agent.c | 7 +++-- agent/php_agent.h | 18 +++++++++++- agent/php_call.c | 5 +++- agent/php_execute.c | 26 +++++++++++++---- agent/php_hooks.h | 2 +- agent/php_internal_instrument.c | 11 +++++--- agent/php_newrelic.h | 50 +++++++++++++++++++++++++++++++-- agent/php_user_instrument.c | 8 ++++-- agent/php_wrapper.c | 3 +- 19 files changed, 130 insertions(+), 41 deletions(-) diff --git a/agent/fw_drupal.c b/agent/fw_drupal.c index 13fcc68a9..83daef226 100644 --- a/agent/fw_drupal.c +++ b/agent/fw_drupal.c @@ -216,7 +216,7 @@ NR_PHP_WRAPPER(nr_drupal_http_request_exec) { goto end; } - return_value = nr_php_get_return_value_ptr(TSRMLS_C); + return_value = NR_GET_RETURN_VALUE_PTR; /* * We only want to create a metric here if this isn't a recursive call to diff --git a/agent/fw_drupal8.c b/agent/fw_drupal8.c index 2239dcc9c..d37941545 100644 --- a/agent/fw_drupal8.c +++ b/agent/fw_drupal8.c @@ -156,7 +156,7 @@ static int nr_drupal8_is_function_in_call_stack(const char* function, * ControllerResolver::getControllerFromDefinition(). */ NR_PHP_WRAPPER(nr_drupal8_name_the_wt) { - zval** retval_ptr = nr_php_get_return_value_ptr(TSRMLS_C); + zval** retval_ptr = NR_GET_RETURN_VALUE_PTR; NR_UNUSED_SPECIALFN; (void)wraprec; @@ -205,7 +205,7 @@ NR_PHP_WRAPPER_END NR_PHP_WRAPPER(nr_drupal8_name_the_wt_cached) { const char* name = "page_cache"; - zval** retval_ptr = nr_php_get_return_value_ptr(TSRMLS_C); + zval** retval_ptr = NR_GET_RETURN_VALUE_PTR; (void)wraprec; @@ -304,7 +304,7 @@ static int nr_drupal8_apply_hook(zval* element, */ NR_PHP_WRAPPER(nr_drupal8_post_get_implementations) { zval* hook = NULL; - zval** retval_ptr = nr_php_get_return_value_ptr(TSRMLS_C); + zval** retval_ptr = NR_GET_RETURN_VALUE_PTR; (void)wraprec; @@ -338,7 +338,7 @@ NR_PHP_WRAPPER_END NR_PHP_WRAPPER(nr_drupal8_post_implements_hook) { zval* hook = NULL; zval* module = NULL; - zval** retval_ptr = nr_php_get_return_value_ptr(TSRMLS_C); + zval** retval_ptr = NR_GET_RETURN_VALUE_PTR; (void)wraprec; @@ -371,7 +371,7 @@ NR_PHP_WRAPPER_END */ NR_PHP_WRAPPER(nr_drupal8_module_handler) { zend_class_entry* ce = NULL; - zval** retval_ptr = nr_php_get_return_value_ptr(TSRMLS_C); + zval** retval_ptr = NR_GET_RETURN_VALUE_PTR; NR_UNUSED_SPECIALFN; (void)wraprec; diff --git a/agent/fw_laravel.c b/agent/fw_laravel.c index 925b72a55..23e4b4840 100644 --- a/agent/fw_laravel.c +++ b/agent/fw_laravel.c @@ -1073,7 +1073,7 @@ NR_PHP_WRAPPER(nr_laravel_routes_get_route_for_methods) { * Start by calling the original method, and if it doesn't return a * route then we don't need to do any extra work. */ - route = nr_php_get_return_value_ptr(TSRMLS_C); + route = NR_GET_RETURN_VALUE_PTR; NR_PHP_WRAPPER_CALL; /* If the method did not return a route, then end gracefully. */ diff --git a/agent/fw_laravel_queue.c b/agent/fw_laravel_queue.c index d038936a6..94792429d 100644 --- a/agent/fw_laravel_queue.c +++ b/agent/fw_laravel_queue.c @@ -695,7 +695,7 @@ static char* nr_laravel_get_payload_header_mq(char* header) { NR_PHP_WRAPPER(nr_laravel_queue_queue_createpayload) { zval* json = NULL; zval* payload = NULL; - zval** retval_ptr = nr_php_get_return_value_ptr(TSRMLS_C); + zval** retval_ptr = NR_GET_RETURN_VALUE_PTR; nr_hashmap_t* outbound_headers = NULL; nr_vector_t* header_keys = NULL; char* header = NULL; diff --git a/agent/fw_magento2.c b/agent/fw_magento2.c index b8678fe1d..3f56e6158 100644 --- a/agent/fw_magento2.c +++ b/agent/fw_magento2.c @@ -129,7 +129,7 @@ NR_PHP_WRAPPER(nr_magento2_pagecache_kernel_load) { NR_PHP_WRAPPER_REQUIRE_FRAMEWORK(NR_FW_MAGENTO2); - response = nr_php_get_return_value_ptr(TSRMLS_C); + response = NR_GET_RETURN_VALUE_PTR; NR_PHP_WRAPPER_CALL; @@ -167,7 +167,7 @@ NR_PHP_WRAPPER(nr_magento2_objectmanager_get) { */ goto leave; } - retval_ptr = nr_php_get_return_value_ptr(TSRMLS_C); + retval_ptr = NR_GET_RETURN_VALUE_PTR; NR_PHP_WRAPPER_CALL; if ((NULL == retval_ptr) || !nr_php_is_zval_valid_object(*retval_ptr)) { @@ -251,7 +251,7 @@ NR_PHP_WRAPPER(nr_magento2_soap_iswsdlrequest) { NR_PHP_WRAPPER_REQUIRE_FRAMEWORK(NR_FW_MAGENTO2); - retval_ptr = nr_php_get_return_value_ptr(TSRMLS_C); + retval_ptr = NR_GET_RETURN_VALUE_PTR; NR_PHP_WRAPPER_CALL; if (retval_ptr && nr_php_is_zval_true(*retval_ptr)) { @@ -268,7 +268,7 @@ NR_PHP_WRAPPER(nr_magento2_soap_iswsdllistrequest) { NR_PHP_WRAPPER_REQUIRE_FRAMEWORK(NR_FW_MAGENTO2); - retval_ptr = nr_php_get_return_value_ptr(TSRMLS_C); + retval_ptr = NR_GET_RETURN_VALUE_PTR; NR_PHP_WRAPPER_CALL; if (retval_ptr && nr_php_is_zval_true(*retval_ptr)) { @@ -322,7 +322,7 @@ NR_PHP_WRAPPER(nr_magento2_soap_handler_prepareoperationinput) { svc_class = nr_php_arg_get(1, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); method_metadata = nr_php_arg_get(2, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); - /* + /* * We expect method_metadata to be an array. At index 'method', if we see * a method name, we'll pass it to the transaction naming. * See: diff --git a/agent/fw_mediawiki.c b/agent/fw_mediawiki.c index 95fcdc1cf..8872e2090 100644 --- a/agent/fw_mediawiki.c +++ b/agent/fw_mediawiki.c @@ -142,7 +142,7 @@ NR_PHP_WRAPPER(nr_mediawiki_getaction) { NR_PHP_WRAPPER_REQUIRE_FRAMEWORK(NR_FW_MEDIAWIKI); - return_value = nr_php_get_return_value_ptr(TSRMLS_C); + return_value = NR_GET_RETURN_VALUE_PTR; NR_PHP_WRAPPER_CALL; diff --git a/agent/fw_slim.c b/agent/fw_slim.c index 2dea82343..841703036 100644 --- a/agent/fw_slim.c +++ b/agent/fw_slim.c @@ -54,7 +54,7 @@ NR_PHP_WRAPPER(nr_slim2_route_dispatch) { txn_name = nr_slim_path_from_route(this_var TSRMLS_CC); nr_php_scope_release(&this_var); - retval_ptr = nr_php_get_return_value_ptr(TSRMLS_C); + retval_ptr = NR_GET_RETURN_VALUE_PTR; NR_PHP_WRAPPER_CALL; diff --git a/agent/fw_wordpress.c b/agent/fw_wordpress.c index 4f9544636..84c440209 100644 --- a/agent/fw_wordpress.c +++ b/agent/fw_wordpress.c @@ -543,7 +543,7 @@ static void nr_wordpress_name_the_wt(const zval* tag, */ NR_PHP_WRAPPER(nr_wordpress_apply_filters) { zval* tag = NULL; - zval** retval_ptr = nr_php_get_return_value_ptr(TSRMLS_C); + zval** retval_ptr = NR_GET_RETURN_VALUE_PTR; NR_UNUSED_SPECIALFN; (void)wraprec; diff --git a/agent/lib_predis.c b/agent/lib_predis.c index 9bf9966aa..1a194f70a 100644 --- a/agent/lib_predis.c +++ b/agent/lib_predis.c @@ -596,7 +596,7 @@ NR_PHP_WRAPPER(nr_predis_connection_writeRequest) { NR_PHP_WRAPPER_END NR_PHP_WRAPPER(nr_predis_aggregateconnection_getConnection) { - zval** retval_ptr = nr_php_get_return_value_ptr(TSRMLS_C); + zval** retval_ptr = NR_GET_RETURN_VALUE_PTR; (void)wraprec; diff --git a/agent/lib_zend_http.c b/agent/lib_zend_http.c index 16ef42560..ef8a416b8 100644 --- a/agent/lib_zend_http.c +++ b/agent/lib_zend_http.c @@ -33,8 +33,8 @@ typedef enum _nr_zend_http_adapter { */ #define LIB_NAME_Z "Zend" #define CURL_ADAPTER_Z "Zend_Http_Client_Adapter_Curl" -#define URI_HTTP_Z "Zend_Uri_Http" -#define HTTP_CLIENT_Z "Zend_Http_Client" +#define URI_HTTP_Z "Zend_Uri_Http" +#define HTTP_CLIENT_Z "Zend_Http_Client" #define HTTP_CLIENT_REQUEST_Z "Zend_Http_Client::request" #define LIB_NAME_L "Laminas" @@ -338,7 +338,7 @@ NR_PHP_WRAPPER_START(nr_zend_http_client_request) { (void)wraprec; this_var = nr_php_scope_get(NR_EXECUTE_ORIG_ARGS TSRMLS_CC); - retval_ptr = nr_php_get_return_value_ptr(TSRMLS_C); + retval_ptr = NR_GET_RETURN_VALUE_PTR; /* Avoid double counting if CURL is used. */ adapter = nr_zend_check_adapter(this_var TSRMLS_CC); @@ -456,4 +456,3 @@ void nr_laminas_http_enable(TSRMLS_D) { nr_zend_http_client_request TSRMLS_CC); } } - diff --git a/agent/php_agent.c b/agent/php_agent.c index f2ec80ba3..0fe087bea 100644 --- a/agent/php_agent.c +++ b/agent/php_agent.c @@ -277,15 +277,15 @@ zend_function* nr_php_zval_to_function(zval* zv TSRMLS_DC) { zend_execute_data* nr_get_zend_execute_data(NR_EXECUTE_PROTO TSRMLS_DC) { zend_execute_data* ptrg = EG(current_execute_data); /* via zend engine global data structure */ - NR_UNUSED_SPECIALFN; + NR_UNUSED_FUNC_RETURN_VALUE; #if ZEND_MODULE_API_NO >= ZEND_5_5_X_API_NO { /* * ptra is argument passed in to us, it might be NULL if the caller doesn't * have that info. */ - zend_execute_data* ptra = NR_EXECUTE_ORIG_ARGS; + zend_execute_data* ptra = execute_data; if (NULL != ptra) { return ptra; } else { @@ -419,6 +419,8 @@ zval* nr_php_get_user_func_arg(size_t requested_arg_index, zval* arg_via_h = 0; int arg_count_via_h = -1; + NR_UNUSED_FUNC_RETURN_VALUE; + if (requested_arg_index < 1) { return NULL; } @@ -441,6 +443,7 @@ zval* nr_php_get_user_func_arg(size_t requested_arg_index, size_t nr_php_get_user_func_arg_count(NR_EXECUTE_PROTO TSRMLS_DC) { #if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP 7.0+ */ + NR_UNUSED_FUNC_RETURN_VALUE; return (size_t)ZEND_CALL_NUM_ARGS(execute_data); #else int arg_count_via_h = -1; diff --git a/agent/php_agent.h b/agent/php_agent.h index 879c4c0b6..dc9620827 100644 --- a/agent/php_agent.h +++ b/agent/php_agent.h @@ -330,7 +330,22 @@ extern zend_function* nr_php_zval_to_function(zval* zv TSRMLS_DC); * won't help you! */ static inline zval* nr_php_get_return_value(NR_EXECUTE_PROTO TSRMLS_DC) { -#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP 7.0+ */ +#if (ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA) /* PHP 8.0+ and OAPI */ + /* + * If the agent is still overwriting zend_execute_data extract oldfashioned + * way; otherwise, pass the observer given return value. + */ + if (nrunlikely(NULL == execute_data)) { + /* + * Shouldn't theoretically ever have a NULL execute_data with valid + * return_value. + */ + return NULL; + } + return func_return_value; +#elif ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP 7.0+ */ + NR_UNUSED_FUNC_RETURN_VALUE; if (NULL == execute_data) { /* * This function shouldn't be called from outside a function context, so @@ -378,6 +393,7 @@ extern size_t nr_php_get_user_func_arg_count(NR_EXECUTE_PROTO TSRMLS_DC); static inline zend_function* nr_php_execute_function( NR_EXECUTE_PROTO TSRMLS_DC) { NR_UNUSED_TSRMLS; + NR_UNUSED_FUNC_RETURN_VALUE; #if ZEND_MODULE_API_NO >= ZEND_5_5_X_API_NO if (NULL == execute_data) { diff --git a/agent/php_call.c b/agent/php_call.c index f268fc59a..1bcd08a4e 100644 --- a/agent/php_call.c +++ b/agent/php_call.c @@ -259,7 +259,10 @@ void nr_php_call_user_func_array_handler(nrphpcufafn_t handler, caller = prev_execute_data->function_state.function; #endif /* PHP7 */ } else { -#if ZEND_MODULE_API_NO >= ZEND_5_5_X_API_NO +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + caller = nr_php_get_caller(EG(current_execute_data), NULL, 1 TSRMLS_CC); +#elif ZEND_MODULE_API_NO >= ZEND_5_5_X_API_NO caller = nr_php_get_caller(EG(current_execute_data), 1 TSRMLS_CC); #else caller = nr_php_get_caller(NULL, 1 TSRMLS_CC); diff --git a/agent/php_execute.c b/agent/php_execute.c index b367c3c14..69e07913d 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -945,6 +945,8 @@ static void nr_php_execute_file(const zend_op_array* op_array, NR_EXECUTE_PROTO TSRMLS_DC) { const char* filename = nr_php_op_array_file_name(op_array); + NR_UNUSED_FUNC_RETURN_VALUE; + if (nrunlikely(NR_PHP_PROCESS_GLOBALS(special_flags).show_loaded_files)) { nrl_debug(NRL_AGENT, "loaded file=" NRP_FMT, NRP_FILENAME(filename)); } @@ -956,7 +958,8 @@ static void nr_php_execute_file(const zend_op_array* op_array, nr_txn_match_file(NRPRG(txn), filename); - NR_PHP_PROCESS_GLOBALS(orig_execute)(NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + NR_PHP_PROCESS_GLOBALS(orig_execute) + (NR_EXECUTE_ORIG_ARGS_OVERWRITE TSRMLS_CC); if (0 == nr_php_recording(TSRMLS_C)) { return; @@ -1299,7 +1302,8 @@ static void nr_php_execute_enabled(NR_EXECUTE_PROTO TSRMLS_DC) { /* * This is the case for New Relic is enabled, but we're not recording. */ - NR_PHP_PROCESS_GLOBALS(orig_execute)(NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + NR_PHP_PROCESS_GLOBALS(orig_execute) + (NR_EXECUTE_ORIG_ARGS_OVERWRITE TSRMLS_CC); } } @@ -1352,7 +1356,7 @@ static void nr_php_max_nesting_level_reached(TSRMLS_D) { * the presence of longjmp as from zend_bailout when processing zend internal * errors, as for example when calling php_error. */ -void nr_php_execute(NR_EXECUTE_PROTO TSRMLS_DC) { +void nr_php_execute(NR_EXECUTE_PROTO_OVERWRITE TSRMLS_DC) { /* * We do not use zend_try { ... } mechanisms here because zend_try * involves a setjmp, and so may be too expensive along this oft-used @@ -1369,6 +1373,10 @@ void nr_php_execute(NR_EXECUTE_PROTO TSRMLS_DC) { * zend_catch is called to avoid catastrophe on the way to a premature * exit, maintaining this counter perfectly is not a necessity. */ +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ + zval* func_return_value = NULL; +#endif NRPRG(php_cur_stack_depth) += 1; @@ -1378,7 +1386,8 @@ void nr_php_execute(NR_EXECUTE_PROTO TSRMLS_DC) { } if (nrunlikely(0 == nr_php_recording(TSRMLS_C))) { - NR_PHP_PROCESS_GLOBALS(orig_execute)(NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + NR_PHP_PROCESS_GLOBALS(orig_execute) + (NR_EXECUTE_ORIG_ARGS_OVERWRITE TSRMLS_CC); } else { int show_executes = NR_PHP_PROCESS_GLOBALS(special_flags).show_executes @@ -1395,12 +1404,17 @@ void nr_php_execute(NR_EXECUTE_PROTO TSRMLS_DC) { return; } -static void nr_php_show_exec_internal(NR_EXECUTE_PROTO, +static void nr_php_show_exec_internal(NR_EXECUTE_PROTO_OVERWRITE, const zend_function* func TSRMLS_DC) { char argstr[NR_EXECUTE_DEBUG_STRBUFSZ] = {'\0'}; const char* name = nr_php_function_debug_name(func); +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ + zval* func_return_value = NULL; +#endif nr_show_execute_params(NR_EXECUTE_ORIG_ARGS, argstr TSRMLS_CC); + nrl_verbosedebug( NRL_AGENT, "execute: %.*s function={" NRP_FMT_UQ "} params={" NRP_FMT_UQ "}", @@ -1468,7 +1482,7 @@ void nr_php_execute_internal(zend_execute_data* execute_data, */ if (nrunlikely(NR_PHP_PROCESS_GLOBALS(special_flags).show_executes)) { #if ZEND_MODULE_API_NO >= ZEND_5_5_X_API_NO - nr_php_show_exec_internal(NR_EXECUTE_ORIG_ARGS, func TSRMLS_CC); + nr_php_show_exec_internal(NR_EXECUTE_ORIG_ARGS_OVERWRITE, func TSRMLS_CC); #else /* * We're passing the same pointer twice. This is inefficient. However, no diff --git a/agent/php_hooks.h b/agent/php_hooks.h index 6f1e1bd47..63a81eb34 100644 --- a/agent/php_hooks.h +++ b/agent/php_hooks.h @@ -16,7 +16,7 @@ * See http://www.php.net/manual/en/migration55.internals.php * for a discussion of what/how to override. */ -extern void nr_php_execute(NR_EXECUTE_PROTO TSRMLS_DC); +extern void nr_php_execute(NR_EXECUTE_PROTO_OVERWRITE TSRMLS_DC); /* * Purpose : Our own error callback function, used to capture the PHP stack diff --git a/agent/php_internal_instrument.c b/agent/php_internal_instrument.c index 741f6e725..461372ddf 100644 --- a/agent/php_internal_instrument.c +++ b/agent/php_internal_instrument.c @@ -1169,7 +1169,11 @@ NR_INNER_WRAPPER(mysqli_stmt_bind_param) { argc = (size_t)ZEND_NUM_ARGS(); argv = (zval**)nr_calloc(argc, sizeof(zval*)); for (i = 0; i < argc; i++) { -#if ZEND_MODULE_API_NO >= ZEND_5_5_X_API_NO +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + argv[i] = nr_php_get_user_func_arg(i + 1, EG(current_execute_data), + NULL TSRMLS_CC); +#elif ZEND_MODULE_API_NO >= ZEND_5_5_X_API_NO argv[i] = nr_php_get_user_func_arg(i + 1, EG(current_execute_data) TSRMLS_CC); #else /* PHP < 5.5 */ @@ -1560,9 +1564,8 @@ static char* nr_php_prepared_statement_make_pgsql_key( */ #if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO /* PHP 8.1+ */ if (nr_php_is_zval_valid_object(conn)) { - key = nr_formatf("type=pgsql id=%ld name=%.*s", - nr_php_zval_object_id(conn), NRSAFELEN(stmtname_len), - stmtname); + key = nr_formatf("type=pgsql id=%ld name=%.*s", nr_php_zval_object_id(conn), + NRSAFELEN(stmtname_len), stmtname); } #else if (nr_php_is_zval_valid_resource(conn)) { diff --git a/agent/php_newrelic.h b/agent/php_newrelic.h index 8c3dcba81..72f204a0a 100644 --- a/agent/php_newrelic.h +++ b/agent/php_newrelic.h @@ -38,7 +38,35 @@ extern zend_module_entry newrelic_module_entry; * majority of those changes so the rest of the code can simply use the macros * and not have to take the different APIs into account. */ -#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP 7.0+ */ + +/* OVERWRITE_ZEND_EXECUTE_DATA allows testing of components with the previous + * method of overwriting until the handler functions are complete. + * Additionally, gives us flexibility of toggling back to previous method of + * instrumentation. When checking in, leave this toggled on to have the CI work + * as long as possible until the handler functionality is implemented.*/ +#define OVERWRITE_ZEND_EXECUTE_DATA true +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ +#define NR_SPECIALFNPTR_PROTO \ + struct _nruserfn_t *wraprec, nr_segment_t *auto_segment, \ + zend_execute_data *execute_data, zval *func_return_value +#define NR_SPECIALFNPTR_ORIG_ARGS \ + wraprec, auto_segment, execute_data, func_return_value +#define NR_SPECIALFN_PROTO \ + nruserfn_t *wraprec, zend_execute_data *execute_data, , \ + zval *func_return_value +#define NR_OP_ARRAY (&execute_data->func->op_array) +#define NR_EXECUTE_PROTO \ + zend_execute_data *execute_data, zval *func_return_value +#define NR_EXECUTE_PROTO_OVERWRITE zend_execute_data* execute_data +#define NR_EXECUTE_ORIG_ARGS_OVERWRITE execute_data +#define NR_EXECUTE_ORIG_ARGS execute_data, func_return_value +#define NR_UNUSED_SPECIALFN (void)execute_data +#define NR_UNUSED_FUNC_RETURN_VALUE (void)func_return_value +/* NR_ZEND_EXECUTE_HOOK to be removed in future ticket */ +#define NR_ZEND_EXECUTE_HOOK zend_execute_ex + +#elif ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP 7.0+ and overwrite hook*/ #define NR_SPECIALFNPTR_PROTO \ struct _nruserfn_t *wraprec, nr_segment_t *auto_segment, \ zend_execute_data *execute_data @@ -46,9 +74,13 @@ extern zend_module_entry newrelic_module_entry; #define NR_SPECIALFN_PROTO nruserfn_t *wraprec, zend_execute_data *execute_data #define NR_OP_ARRAY (&execute_data->func->op_array) #define NR_EXECUTE_PROTO zend_execute_data* execute_data +#define NR_EXECUTE_PROTO_OVERWRITE zend_execute_data* execute_data +#define NR_EXECUTE_ORIG_ARGS_OVERWRITE execute_data #define NR_EXECUTE_ORIG_ARGS execute_data #define NR_UNUSED_SPECIALFN (void)execute_data +#define NR_UNUSED_FUNC_RETURN_VALUE #define NR_ZEND_EXECUTE_HOOK zend_execute_ex + #elif ZEND_MODULE_API_NO >= ZEND_5_5_X_API_NO #define NR_SPECIALFNPTR_PROTO \ struct _nruserfn_t *wraprec, nr_segment_t *auto_segment, \ @@ -57,9 +89,13 @@ extern zend_module_entry newrelic_module_entry; #define NR_SPECIALFN_PROTO nruserfn_t *wraprec, zend_execute_data *execute_data #define NR_OP_ARRAY (execute_data->op_array) #define NR_EXECUTE_PROTO zend_execute_data* execute_data +#define NR_EXECUTE_PROTO_OVERWRITE zend_execute_data* execute_data +#define NR_EXECUTE_ORIG_ARGS_OVERWRITE execute_data #define NR_EXECUTE_ORIG_ARGS execute_data #define NR_UNUSED_SPECIALFN (void)execute_data +#define NR_UNUSED_FUNC_RETURN_VALUE #define NR_ZEND_EXECUTE_HOOK zend_execute_ex + #else /* PHP < 5.5 */ #define NR_SPECIALFNPTR_PROTO \ struct _nruserfn_t *wraprec, nr_segment_t *auto_segment, \ @@ -68,11 +104,21 @@ extern zend_module_entry newrelic_module_entry; #define NR_SPECIALFN_PROTO nruserfn_t *wraprec, zend_op_array *op_array_arg #define NR_OP_ARRAY (op_array_arg) #define NR_EXECUTE_PROTO zend_op_array* op_array_arg +#define NR_EXECUTE_PROTO_OVERWRITE zend_op_array* op_array_arg +#define NR_EXECUTE_ORIG_ARGS_OVERWRITE op_array_arg #define NR_EXECUTE_ORIG_ARGS op_array_arg #define NR_UNUSED_SPECIALFN (void)op_array_arg +#define NR_UNUSED_FUNC_RETURN_VALUE #define NR_ZEND_EXECUTE_HOOK zend_execute #endif +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA +#define NR_GET_RETURN_VALUE_PTR &func_return_value +#else +#define NR_GET_RETURN_VALUE_PTR nr_php_get_return_value_ptr(TSRMLS_C) +#endif + #if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP 7.0+ */ #define NR_UNUSED_EXECUTE_DATA (void)execute_data; #define NR_UNUSED_HT @@ -202,7 +248,7 @@ typedef zend_op_array* (*nrphpcfile_t)(zend_file_handle* file_handle, int type TSRMLS_DC); typedef zend_op_array* (*nrphpcstr_t)(zval* source_string, char* filename TSRMLS_DC); -typedef void (*nrphpexecfn_t)(NR_EXECUTE_PROTO TSRMLS_DC); +typedef void (*nrphpexecfn_t)(NR_EXECUTE_PROTO_OVERWRITE TSRMLS_DC); typedef void (*nrphpcufafn_t)(zend_function* func, const zend_function* caller TSRMLS_DC); typedef int (*nrphphdrfn_t)(sapi_header_struct* sapi_header, diff --git a/agent/php_user_instrument.c b/agent/php_user_instrument.c index f6005193a..17f8a47fb 100644 --- a/agent/php_user_instrument.c +++ b/agent/php_user_instrument.c @@ -51,8 +51,10 @@ */ int nr_zend_call_orig_execute(NR_EXECUTE_PROTO TSRMLS_DC) { volatile int zcaught = 0; + NR_UNUSED_FUNC_RETURN_VALUE; zend_try { - NR_PHP_PROCESS_GLOBALS(orig_execute)(NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + NR_PHP_PROCESS_GLOBALS(orig_execute) + (NR_EXECUTE_ORIG_ARGS_OVERWRITE TSRMLS_CC); } zend_catch { zcaught = 1; } zend_end_try(); @@ -63,12 +65,14 @@ int nr_zend_call_orig_execute_special(nruserfn_t* wraprec, nr_segment_t* segment, NR_EXECUTE_PROTO TSRMLS_DC) { volatile int zcaught = 0; + NR_UNUSED_FUNC_RETURN_VALUE; zend_try { if (wraprec && wraprec->special_instrumentation) { wraprec->special_instrumentation(wraprec, segment, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); } else { - NR_PHP_PROCESS_GLOBALS(orig_execute)(NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + NR_PHP_PROCESS_GLOBALS(orig_execute) + (NR_EXECUTE_ORIG_ARGS_OVERWRITE TSRMLS_CC); } } zend_catch { zcaught = 1; } diff --git a/agent/php_wrapper.c b/agent/php_wrapper.c index 44c5c9e4d..6ce1ea4be 100644 --- a/agent/php_wrapper.c +++ b/agent/php_wrapper.c @@ -77,7 +77,7 @@ inline static void release_zval(zval** ppzv) { zval* nr_php_arg_get(ssize_t index, NR_EXECUTE_PROTO TSRMLS_DC) { zval* arg; - + NR_UNUSED_FUNC_RETURN_VALUE; #ifdef PHP7 { zval* orig; @@ -203,6 +203,7 @@ void nr_php_arg_release(zval** ppzv) { zval* nr_php_scope_get(NR_EXECUTE_PROTO TSRMLS_DC) { zval* this_obj; zval* this_copy; + NR_UNUSED_FUNC_RETURN_VALUE; this_obj = NR_PHP_USER_FN_THIS(); if (NULL == this_obj) { From 2871e2fcac20df0e9c6a9a658172324045dde722 Mon Sep 17 00:00:00 2001 From: Amber Sistla Date: Thu, 6 Oct 2022 07:00:52 -0700 Subject: [PATCH 05/56] feat(agent): update nr_get_zend_execute_data to only use OAPI provided zed. (#517) * feat(agent): Propagate OAPI return values and update return value functions. 1) updated macros to pass the OAPI given return value throughout the userland system. 2) changed nr_php_get_return_value to return oapi given pointer 3) Added the OVERWRITE_ZEND_EXECUTE_DATA to allow us to toggle off during feature addition, but toggle off to maintain CI as long as possible. It also gives the flexibility to revert to the instrumentation prior to OAPI. 4) macro to toggle between just using the OAPI return value when OAPI is enabled or calling nr_php_get_return_value_ptr when it is not OAPI. --- agent/php_agent.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/agent/php_agent.c b/agent/php_agent.c index 0fe087bea..469435427 100644 --- a/agent/php_agent.c +++ b/agent/php_agent.c @@ -275,6 +275,18 @@ zend_function* nr_php_zval_to_function(zval* zv TSRMLS_DC) { } zend_execute_data* nr_get_zend_execute_data(NR_EXECUTE_PROTO TSRMLS_DC) { + NR_UNUSED_FUNC_RETURN_VALUE; + +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ + + /* + * There is no other recourse. We must return what OAPI gave us. This should + * theoretically never be NULL since we check for NULL before calling the + * handlers; however, if it was NULL, there is nothing we can do about it. + */ + return execute_data; +#endif zend_execute_data* ptrg = EG(current_execute_data); /* via zend engine global data structure */ NR_UNUSED_SPECIALFN; From ecd977cf5bde8dc1b94805e8e9f1a2e11b518f61 Mon Sep 17 00:00:00 2001 From: Amber Sistla Date: Fri, 7 Oct 2022 16:00:52 -0700 Subject: [PATCH 06/56] feat(agent): Add code level metrics functionality. (#506) * feat(agent): Add code level metrics functionality. 1) Add INI value to disable/enable code level metrics. 2) Create new function `nr_php_txn_add_code_level_metrics` that uses nr_php_zend_execute_data_* family of functions to extract CLM and add it as an `agent attribute`. 3) Added new NR_TXN_ATTRIBUTES for `code.namespace`, `code.lineno`, `code.filepath`, and `code.function`. 4) Added call to php_execute_enabled to call the new CLM function 4) Added new integration tests to exercise the new functionality. Note: CLM functionality is only compatible with PHP 7+. * feat(agent): Code level metrics functionality. 1) updated `nr_php_execute_metadata_t` to hold additional information and moved the definition of the struct to php_execute.h. 2) `nr_php_execute_enabled` uses the metadata at the beginning so now we can simply release at the end. 3) In the case of a super short segment that would have been ignored, we do NOT add CLM (otherwise it wouldn't be ignored). We wait until after we decide to ignore or not to add the CLM data. * feat(agent): Updated test scripts. * fix(agent): check clm string lengths will not be truncated * fix(agent): check for empty/null CLM attributes * style(agent): clang-format php_execute * fix(agent): fix logic handling CLM string length * refactor(agent): abstract out CLM while-loop code for readability --- agent/php_agent.c | 17 +- agent/php_agent.h | 28 ++- agent/php_execute.c | 165 ++++++++++--- agent/php_execute.h | 42 ++++ agent/php_newrelic.h | 6 + agent/php_nrini.c | 21 +- agent/php_txn.c | 50 ++++ axiom/nr_segment.c | 12 +- axiom/nr_segment_traces.c | 10 +- axiom/nr_txn.c | 43 +++- axiom/nr_txn.h | 13 +- .../test_transaction_namespace2_clm.php | 142 +++++++++++ .../test_transaction_namespace_clm.php | 140 +++++++++++ ..._transaction_nested_user_functions_clm.php | 231 ++++++++++++++++++ .../test_transaction_non_web_clm.php | 158 ++++++++++++ .../attributes/test_transaction_web_clm.php | 227 +++++++++++++++++ 16 files changed, 1246 insertions(+), 59 deletions(-) create mode 100644 tests/integration/attributes/test_transaction_namespace2_clm.php create mode 100644 tests/integration/attributes/test_transaction_namespace_clm.php create mode 100644 tests/integration/attributes/test_transaction_nested_user_functions_clm.php create mode 100644 tests/integration/attributes/test_transaction_non_web_clm.php create mode 100644 tests/integration/attributes/test_transaction_web_clm.php diff --git a/agent/php_agent.c b/agent/php_agent.c index 469435427..322049db9 100644 --- a/agent/php_agent.c +++ b/agent/php_agent.c @@ -1158,6 +1158,7 @@ bool nr_php_function_is_static_method(const zend_function* func) { } #if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ + const char* nr_php_zend_execute_data_function_name( const zend_execute_data* execute_data) { zend_string* function_name = NULL; @@ -1170,7 +1171,12 @@ const char* nr_php_zend_execute_data_function_name( if ((NULL == function_name) && (ZEND_USER_FUNCTION == execute_data->func->type)) { - return "main"; + /* + * This is the case of a filename being called so there is no function name. + * It is broken out separately here in case we need to do something special + * with it in the future. + */ + return NULL; } return function_name ? ZSTR_VAL(function_name) : NULL; } @@ -1178,9 +1184,8 @@ const char* nr_php_zend_execute_data_function_name( const char* nr_php_zend_execute_data_filename( const zend_execute_data* execute_data) { zend_string* filename = NULL; - while ( - execute_data - && (!execute_data->func || !ZEND_USER_CODE(execute_data->func->type))) { + + while (NR_ZEND_USER_FUNC_EXISTS(execute_data)) { execute_data = execute_data->prev_execute_data; } if (execute_data) { @@ -1208,9 +1213,7 @@ const char* nr_php_zend_execute_data_scope_name( uint32_t nr_php_zend_execute_data_lineno( const zend_execute_data* execute_data) { - while ( - execute_data - && (!execute_data->func || !ZEND_USER_CODE(execute_data->func->type))) { + while (NR_ZEND_USER_FUNC_EXISTS(execute_data)) { execute_data = execute_data->prev_execute_data; } if (execute_data) { diff --git a/agent/php_agent.h b/agent/php_agent.h index dc9620827..10c7599d0 100644 --- a/agent/php_agent.h +++ b/agent/php_agent.h @@ -41,6 +41,7 @@ #include "php_zval.h" #include "util_memory.h" #include "util_strings.h" +#include "php_execute.h" /* * The default connection mechanism to the daemon is: @@ -79,6 +80,7 @@ * Returns : A newly allocated JSON stack trace string or NULL on error. */ #define NR_PHP_STACKTRACE_LIMIT 300 + extern char* nr_php_backtrace_to_json(zval* itrace TSRMLS_DC); /* @@ -426,7 +428,7 @@ static inline zval* nr_php_execute_scope(zend_execute_data* execute_data) { || (Z_CE(execute_data->This))) { return &execute_data->This; } else if (execute_data->func) { - if (execute_data->func->type != ZEND_INTERNAL_FUNCTION + if (ZEND_USER_CODE(execute_data->func->type) || execute_data->func->common.scope) { return NULL; } @@ -826,7 +828,31 @@ extern bool nr_php_function_is_static_method(const zend_function* func); */ extern zend_execute_data* nr_get_zend_execute_data(NR_EXECUTE_PROTO TSRMLS_DC); +/* + * Purpose : If code level metrics are enabled, extract the data from the OAPI + * given zend_execute_data. Add the CLM as agent attributes to the + * attributes data structure. + * + * Params : 1. attributes data structure to add the CLM to + * 2. The zend_execute_data given by OAPI + * + * Returns : void + * + * Note: PHP has a concept of calling files with no function names. In the + * case of a file being called when there is no function name, the agent + * instruments the file. In this case, we provide the filename to CLM + * as the "function" name. + * Current CLM functionality only works with PHP 7+ + */ +extern void nr_php_txn_add_code_level_metrics( + nr_attributes_t* attributes, + const nr_php_execute_metadata_t* metadata); + #if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ + +#define NR_ZEND_USER_FUNC_EXISTS(x) \ + (x && (!x->func || !ZEND_USER_CODE(x->func->type))) + /* * Purpose : Return a pointer to the function name of zend_execute_data. * diff --git a/agent/php_execute.c b/agent/php_execute.c index 69e07913d..b51c262b7 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -72,8 +72,8 @@ * This too is erroneous. The cost of calling a function is about 4 assembler * instructions. This is negligible. Therefore, as a means of reducing stack * usage, if you need stack space it is better to put that usage into a static - * function and call it from the main function, because then that stack space - * in genuinely only allocated when needed. + * function and call it from the main function, because then that stack space is + * genuinely only allocated when needed. * * A not-insignificant performance boost comes from accurate branch hinting * using the nrlikely() and nrunlikely() macros. This prevents pipeline stalls @@ -969,40 +969,115 @@ static void nr_php_execute_file(const zend_op_array* op_array, } /* - * Version specific metadata that we have to gather before we call the original - * execute_ex handler, as different versions of PHP behave differently in terms - * of what you can do with the op array after making that call. This structure - * and the functions immediately below are helpers for - * nr_php_execute_enabled(), which is the user function execution function. + * Purpose : Add Code Level Metrics (CLM) to a metadata structure from + * zend_execute_data. * - * In PHP 7, it is possible that the op array will be destroyed if the function - * being called is a __call() magic method (in which case a trampoline is - * created and destroyed). We increment the reference counts on the scope and - * function strings and keep pointers to them in this structure, then release - * them once we've named the trace node and/or metric (if required). - * - * In PHP 5, execute_data->op_array may be set to NULL if we make a subsequent - * user function call in an exec callback (which occurs before we decide - * whether to create a metric and/or trace node), so we keep a copy of the - * pointer here. The op array itself won't be destroyed from under us, as it's - * owned by the request and not the specific function call (unlike the - * trampoline case above). + * Params : 1. A pointer to a metadata structure. + * 2. The zend_execute_data * - * Note that, while op arrays are theoretically reference counted themselves, - * we cannot take the simple approach of incrementing that reference count due - * to not all Zend Engine functions using init_op_array() and - * destroy_op_array(): one example is that PHP 7 trampoline op arrays are - * simply emalloc() and efree()'d without even setting the reference count. - * Therefore we have to be more selective in our approach. + * Note : It is the responsibility of the caller to allocate the metadata + * structure. In general, it's expected that this will be a pointer + * to a stack variable. */ -typedef struct { -#ifdef PHP7 - zend_string* scope; - zend_string* function; +static void nr_php_execute_metadata_add_code_level_metrics( + nr_php_execute_metadata_t* metadata, + NR_EXECUTE_PROTO) { +#if ZEND_MODULE_API_NO < ZEND_7_0_X_API_NO /* PHP7+ */ + (void)metadata; + NR_UNUSED_SPECIALFN; + return; #else - zend_op_array* op_array; + const char* filepath = NULL; + const char* namespace = NULL; + const char* function = NULL; + uint32_t lineno = 1; + + if (NULL == metadata) { + return; + } + + if (NULL == execute_data) { + return; + } + + metadata->function_name = NULL; + metadata->function_filepath = NULL; + metadata->function_namespace = NULL; + + /* + * Check if code level metrics are enabled in the ini. + * If they aren't, exit and don't update CLM. + */ + if (!NRINI(code_level_metrics_enabled)) { + return; + } + /* + * At a minimum, at least one of the following attribute combinations MUST be + * implemented in order for customers to be able to accurately identify their + * instrumented functions: + * - code.filepath AND code.function + * - code.namespace AND code.function + * + * If we don't have the minimum requirements, exit and don't add any + * attributes. + */ + +#define CHK_CLM_STRLEN(s) \ + if (CLM_STRLEN_MAX < NRSAFELEN(sizeof(s) - 1)) { \ + s = NULL; \ + } + + filepath = nr_php_zend_execute_data_filename(execute_data); + CHK_CLM_STRLEN(filepath) + + namespace = nr_php_zend_execute_data_scope_name(execute_data); + CHK_CLM_STRLEN(namespace) + + function = nr_php_zend_execute_data_function_name(execute_data); + CHK_CLM_STRLEN(function) + +#undef CHK_CLM_STRLEN + + lineno = nr_php_zend_execute_data_lineno(execute_data); + + /* + * Check if we are getting CLM for a file. + */ + if (nrunlikely(OP_ARRAY_IS_A_FILE(NR_OP_ARRAY))) { + /* + * If instrumenting a file, the filename is the "function" and the + * lineno is 1 (i.e., start of the file). + */ + function = filepath; + lineno = 1; + } else { + /* + * We are getting CLM for a function. + */ + lineno = nr_php_zend_execute_data_lineno(execute_data); + } + +#define CHK_CLM_EMPTY(s) ((NULL == s || nr_strempty(s)) ? true : false) + + if (CHK_CLM_EMPTY(function)) { + return; + } + if (CHK_CLM_EMPTY(namespace) && CHK_CLM_EMPTY(filepath)) { + /* + * CLM MUST have either function+namespace or function+filepath. + */ + return; + } + +#undef CHK_CLM_EMPTY + + metadata->function_lineno = lineno; + metadata->function_name = nr_strdup(function); + metadata->function_namespace = nr_strdup(namespace); + metadata->function_filepath = nr_strdup(filepath); + #endif /* PHP7 */ -} nr_php_execute_metadata_t; +} /* * Purpose : Initialise a metadata structure from an op array. @@ -1082,6 +1157,9 @@ static void nr_php_execute_metadata_release( zend_string_release(metadata->function); metadata->function = NULL; } + nr_free(metadata->function_name); + nr_free(metadata->function_namespace); + nr_free(metadata->function_filepath); #else metadata->op_array = NULL; #endif /* PHP7 */ @@ -1129,6 +1207,10 @@ static inline void nr_php_execute_segment_end( || stacked->error) { nr_segment_t* s = nr_php_stacked_segment_move_to_heap(stacked TSRMLS_CC); nr_php_execute_segment_add_metric(s, metadata, create_metric); + if (NULL == s->attributes) { + s->attributes = nr_attributes_create(s->txn->attribute_config); + } + nr_php_txn_add_code_level_metrics(s->attributes, metadata); nr_segment_end(&s); } else { nr_php_stacked_segment_deinit(stacked TSRMLS_CC); @@ -1145,15 +1227,22 @@ static inline void nr_php_execute_segment_end( static void nr_php_execute_enabled(NR_EXECUTE_PROTO TSRMLS_DC) { int zcaught = 0; nrtime_t txn_start_time; - nr_php_execute_metadata_t metadata; + nr_php_execute_metadata_t metadata = {0}; nr_segment_t stacked = {0}; - nr_segment_t* segment; - nruserfn_t* wraprec; + nr_segment_t* segment = NULL; + nruserfn_t* wraprec = NULL; NRTXNGLOBAL(execute_count) += 1; + nr_php_execute_metadata_add_code_level_metrics(&metadata, + NR_EXECUTE_ORIG_ARGS); + if (nrunlikely(OP_ARRAY_IS_A_FILE(NR_OP_ARRAY))) { + if (NRPRG(txn)) { + nr_php_txn_add_code_level_metrics(NRPRG(txn)->attributes, &metadata); + } nr_php_execute_file(NR_OP_ARRAY, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + nr_php_execute_metadata_release(&metadata); return; } @@ -1210,7 +1299,6 @@ static void nr_php_execute_enabled(NR_EXECUTE_PROTO TSRMLS_DC) { txn_start_time = nr_txn_start_time(NRPRG(txn)); segment = nr_php_stacked_segment_init(&stacked TSRMLS_CC); - zcaught = nr_zend_call_orig_execute_special(wraprec, segment, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); @@ -1230,8 +1318,6 @@ static void nr_php_execute_enabled(NR_EXECUTE_PROTO TSRMLS_DC) { nr_php_execute_segment_end(segment, &metadata, create_metric TSRMLS_CC); - nr_php_execute_metadata_release(&metadata); - if (nrunlikely(zcaught)) { zend_bailout(); } @@ -1293,8 +1379,6 @@ static void nr_php_execute_enabled(NR_EXECUTE_PROTO TSRMLS_DC) { nr_php_execute_segment_end(segment, &metadata, false TSRMLS_CC); - nr_php_execute_metadata_release(&metadata); - if (nrunlikely(zcaught)) { zend_bailout(); } @@ -1305,6 +1389,7 @@ static void nr_php_execute_enabled(NR_EXECUTE_PROTO TSRMLS_DC) { NR_PHP_PROCESS_GLOBALS(orig_execute) (NR_EXECUTE_ORIG_ARGS_OVERWRITE TSRMLS_CC); } + nr_php_execute_metadata_release(&metadata); } static void nr_php_execute_show(NR_EXECUTE_PROTO TSRMLS_DC) { @@ -1502,7 +1587,7 @@ void nr_php_execute_internal(zend_execute_data* execute_data, nr_segment_set_timing(segment, segment->start_time, duration); if (duration >= NR_PHP_PROCESS_GLOBALS(expensive_min)) { - nr_php_execute_metadata_t metadata; + nr_php_execute_metadata_t metadata = {0}; nr_php_execute_metadata_init(&metadata, (zend_op_array*)func); diff --git a/agent/php_execute.h b/agent/php_execute.h index a594a28a2..3ae1192f6 100644 --- a/agent/php_execute.h +++ b/agent/php_execute.h @@ -24,6 +24,48 @@ #define OP_ARRAY_IS_METHOD(OP, FNAME) \ (0 == nr_strcmp(nr_php_op_array_function_name(OP), (FNAME))) +#define CLM_STRLEN_MAX (255) + +/* + * Version specific metadata that we have to gather before we call the original + * execute_ex handler, as different versions of PHP behave differently in terms + * of what you can do with the op array after making that call. This structure + * and the functions immediately below are helpers for + * nr_php_execute_enabled(), which is the user function execution function. + * + * In PHP 7, it is possible that the op array will be destroyed if the function + * being called is a __call() magic method (in which case a trampoline is + * created and destroyed). We increment the reference counts on the scope and + * function strings and keep pointers to them in this structure, then release + * them once we've named the trace node and/or metric (if required). + * + * In PHP 5, execute_data->op_array may be set to NULL if we make a subsequent + * user function call in an exec callback (which occurs before we decide + * whether to create a metric and/or trace node), so we keep a copy of the + * pointer here. The op array itself won't be destroyed from under us, as it's + * owned by the request and not the specific function call (unlike the + * trampoline case above). + * + * Note that, while op arrays are theoretically reference counted themselves, + * we cannot take the simple approach of incrementing that reference count due + * to not all Zend Engine functions using init_op_array() and + * destroy_op_array(): one example is that PHP 7 trampoline op arrays are + * simply emalloc() and efree()'d without even setting the reference count. + * Therefore we have to be more selective in our approach. + */ +typedef struct { +#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ + zend_string* scope; + zend_string* function; + char* function_name; + char* function_filepath; + char* function_namespace; + uint32_t function_lineno; +#else + zend_op_array* op_array; +#endif /* PHP7 */ +} nr_php_execute_metadata_t; + /* * Purpose: Look through the PHP symbol table for special names or symbols * that provide additional hints that a specific framework has been loaded. diff --git a/agent/php_newrelic.h b/agent/php_newrelic.h index 72f204a0a..bb3ffacb8 100644 --- a/agent/php_newrelic.h +++ b/agent/php_newrelic.h @@ -524,6 +524,12 @@ nrinibool_t nriniuint_t log_forwarding_log_level; /* newrelic.application_logging.forwarding.log_level */ +/* + * Configuration option to toggle code level metrics collection. + */ +nrinibool_t + code_level_metrics_enabled; /* newrelic.code_level_metrics.enabled */ + /* * pid and user_function_wrappers are used to store user function wrappers. * Storing this on a request level (as opposed to storing it on transaction diff --git a/agent/php_nrini.c b/agent/php_nrini.c index 14fedcb16..001932b3f 100644 --- a/agent/php_nrini.c +++ b/agent/php_nrini.c @@ -2173,6 +2173,7 @@ STD_PHP_INI_ENTRY_EX("newrelic.framework", zend_newrelic_globals, newrelic_globals, nr_framework_dh) + /* DEPRECATED */ STD_PHP_INI_ENTRY_EX("newrelic.cross_application_tracer.enabled", "0", @@ -2863,6 +2864,17 @@ STD_PHP_INI_ENTRY_EX( newrelic_globals, 0) +/* + * Code Level Metrics, initially off by default + */ +STD_PHP_INI_ENTRY_EX("newrelic.code_level_metrics.enabled", + "0", + NR_PHP_REQUEST, + nr_boolean_mh, + code_level_metrics_enabled, + zend_newrelic_globals, + newrelic_globals, + nr_enabled_disabled_dh) /* * Logging */ @@ -3093,10 +3105,11 @@ void zm_info_newrelic(void); /* ctags landing pad only */ PHP_MINFO_FUNCTION(newrelic) { php_info_print_table_start(); php_info_print_table_header(2, "New Relic RPM Monitoring", - NR_PHP_PROCESS_GLOBALS(enabled) ? "enabled" - : NR_PHP_PROCESS_GLOBALS(mpm_bad) - ? "disabled due to threaded MPM" - : "disabled"); + NR_PHP_PROCESS_GLOBALS(enabled) + ? "enabled" + : NR_PHP_PROCESS_GLOBALS(mpm_bad) + ? "disabled due to threaded MPM" + : "disabled"); php_info_print_table_row(2, "New Relic Version", nr_version_verbose()); php_info_print_table_end(); diff --git a/agent/php_txn.c b/agent/php_txn.c index d99585f11..250620a3d 100644 --- a/agent/php_txn.c +++ b/agent/php_txn.c @@ -1109,3 +1109,53 @@ nr_status_t nr_php_txn_end(int ignoretxn, int in_post_deactivate TSRMLS_DC) { return NR_SUCCESS; } + +extern void nr_php_txn_add_code_level_metrics( + nr_attributes_t* attributes, + const nr_php_execute_metadata_t* metadata) { +#if ZEND_MODULE_API_NO < ZEND_7_0_X_API_NO /* PHP7+ */ + (void)attributes; + (void)metadata; + return; +} +#else + /* Current CLM functionality only works with PHP 7+ */ + + if (NULL == metadata) { + return; + } + + /* + * Check if code level metrics are enabled in the ini. + * If they aren't, exit and don't add any attributes. + */ + if (!NRINI(code_level_metrics_enabled)) { + return; + } + +#define CHK_CLM_EMPTY(s) ((NULL == s || nr_strempty(s)) ? true : false) + + if (CHK_CLM_EMPTY(metadata->function_name)) { + /* + * CLM aren't set so don't do anything + */ + return; + } + + nr_txn_attributes_set_string_attribute(attributes, nr_txn_clm_code_function, + metadata->function_name); + if (!CHK_CLM_EMPTY(metadata->function_filepath)) { + nr_txn_attributes_set_string_attribute(attributes, nr_txn_clm_code_filepath, + metadata->function_filepath); + } + if (!CHK_CLM_EMPTY(metadata->function_namespace)) { + nr_txn_attributes_set_string_attribute( + attributes, nr_txn_clm_code_namespace, metadata->function_namespace); + } + +#undef CHK_CLM_EMPTY + + nr_txn_attributes_set_long_attribute(attributes, nr_txn_clm_code_lineno, + metadata->function_lineno); +} +#endif diff --git a/axiom/nr_segment.c b/axiom/nr_segment.c index 34ea61540..4aa546c1c 100644 --- a/axiom/nr_segment.c +++ b/axiom/nr_segment.c @@ -404,7 +404,6 @@ nr_span_event_t* nr_segment_to_span_event(nr_segment_t* segment) { } trace_id = nr_txn_get_current_trace_id(segment->txn); - event = nr_span_event_create(); nr_span_event_set_guid(event, segment->id); nr_span_event_set_trace_id(event, trace_id); @@ -463,9 +462,7 @@ nr_span_event_t* nr_segment_to_span_event(nr_segment_t* segment) { agent_attributes = nr_attributes_agent_to_obj( segment->txn->attributes, NR_ATTRIBUTE_DESTINATION_TXN_EVENT); - nro_iteratehash(agent_attributes, add_agent_attribute_to_span_event, event); - nro_delete(agent_attributes); } @@ -505,6 +502,13 @@ nr_span_event_t* nr_segment_to_span_event(nr_segment_t* segment) { &event_and_counter); nro_delete(user_attributes); + /* + * Add segment agent attributes to span + */ + agent_attributes = nr_attributes_agent_to_obj( + segment->attributes, NR_ATTRIBUTE_DESTINATION_SPAN); + nro_iteratehash(agent_attributes, add_agent_attribute_to_span_event, event); + nro_delete(agent_attributes); } if (segment->attributes_txn_event) { user_attributes = nr_attributes_user_to_obj(segment->attributes_txn_event, @@ -1230,4 +1234,4 @@ bool nr_segment_attributes_user_txn_event_add(nr_segment_t* segment, nr_segment_set_priority_flag(segment, NR_SEGMENT_PRIORITY_ATTR); return (NR_SUCCESS == status); -} \ No newline at end of file +} diff --git a/axiom/nr_segment_traces.c b/axiom/nr_segment_traces.c index 93b63d537..846d89a55 100644 --- a/axiom/nr_segment_traces.c +++ b/axiom/nr_segment_traces.c @@ -228,7 +228,8 @@ static void nr_segment_iteration_pass_trace(nr_segment_t* segment, nrbuf_t* buf = userdata->trace.buf; int idx; nr_segment_t* parent = NULL; - nrobj_t* user_attributes; + nrobj_t* user_attributes = NULL; + nrobj_t* agent_attributes = NULL; uint64_t start_ms; uint64_t stop_ms; @@ -298,6 +299,13 @@ static void nr_segment_iteration_pass_trace(nr_segment_t* segment, segment->attributes, NR_ATTRIBUTE_DESTINATION_TXN_TRACE); add_attribute_hash_to_buffer(buf, user_attributes); nro_delete(user_attributes); + /* + * Add segment attributes to transaction trace. + */ + agent_attributes = nr_attributes_agent_to_obj( + segment->attributes, NR_ATTRIBUTE_DESTINATION_TXN_TRACE); + add_attribute_hash_to_buffer(buf, agent_attributes); + nro_delete(agent_attributes); } nr_buffer_add(buf, "}", 1); diff --git a/axiom/nr_txn.c b/axiom/nr_txn.c index d5b9fa45e..a0a681112 100644 --- a/axiom/nr_txn.c +++ b/axiom/nr_txn.c @@ -48,6 +48,10 @@ struct _nr_txn_attribute_t { uint32_t destinations; }; +#define NR_TXN_ATTRIBUTE_SPAN_TRACE_ERROR_EVENT \ + (NR_ATTRIBUTE_DESTINATION_TXN_TRACE | NR_ATTRIBUTE_DESTINATION_ERROR \ + | NR_ATTRIBUTE_DESTINATION_TXN_EVENT | NR_ATTRIBUTE_DESTINATION_SPAN) + #define NR_TXN_ATTRIBUTE_TRACE_ERROR_EVENT \ (NR_ATTRIBUTE_DESTINATION_TXN_TRACE | NR_ATTRIBUTE_DESTINATION_ERROR \ | NR_ATTRIBUTE_DESTINATION_TXN_EVENT) @@ -103,6 +107,18 @@ NR_TXN_ATTR(nr_txn_http_statuscode, NR_TXN_ATTR(nr_txn_request_user_agent, "request.headers.userAgent", NR_TXN_ATTRIBUTE_TRACE_ERROR); +NR_TXN_ATTR(nr_txn_clm_code_function, + "code.function", + NR_TXN_ATTRIBUTE_SPAN_TRACE_ERROR_EVENT); +NR_TXN_ATTR(nr_txn_clm_code_filepath, + "code.filepath", + NR_TXN_ATTRIBUTE_SPAN_TRACE_ERROR_EVENT); +NR_TXN_ATTR(nr_txn_clm_code_namespace, + "code.namespace", + NR_TXN_ATTRIBUTE_SPAN_TRACE_ERROR_EVENT); +NR_TXN_ATTR(nr_txn_clm_code_lineno, + "code.lineno", + NR_TXN_ATTRIBUTE_SPAN_TRACE_ERROR_EVENT); /* * Deprecated per December 2019 @@ -157,6 +173,32 @@ void nr_txn_set_long_attribute(nrtxn_t* txn, attribute->name, value); } +void nr_txn_attributes_set_string_attribute(nr_attributes_t* attributes, + const nr_txn_attribute_t* attribute, + const char* value) { + if (NULL == attribute) { + return; + } + if (NULL == value) { + return; + } + if ('\0' == value[0]) { + return; + } + nr_attributes_agent_add_string(attributes, attribute->destinations, + attribute->name, value); +} + +void nr_txn_attributes_set_long_attribute(nr_attributes_t* attributes, + const nr_txn_attribute_t* attribute, + long value) { + if (NULL == attribute) { + return; + } + nr_attributes_agent_add_long(attributes, attribute->destinations, + attribute->name, value); +} + /* These sample options are provided for tests. */ const nrtxnopt_t nr_txn_test_options = { .custom_events_enabled = 0, @@ -2504,7 +2546,6 @@ nr_analytics_event_t* nr_error_to_event(const nrtxn_t* txn) { nro_set_hash_string(params, "spanId", nr_error_get_span_id(txn->error)); } } - agent_attributes = nr_attributes_agent_to_obj(txn->attributes, NR_ATTRIBUTE_DESTINATION_ERROR); user_attributes = nr_attributes_user_to_obj(txn->attributes, diff --git a/axiom/nr_txn.h b/axiom/nr_txn.h index 6e6c5fcc6..e95dbe94d 100644 --- a/axiom/nr_txn.h +++ b/axiom/nr_txn.h @@ -575,13 +575,24 @@ extern const nr_txn_attribute_t* nr_txn_request_user_agent; extern const nr_txn_attribute_t* nr_txn_server_name; extern const nr_txn_attribute_t* nr_txn_response_content_type; extern const nr_txn_attribute_t* nr_txn_response_content_length; +extern const nr_txn_attribute_t* nr_txn_clm_code_filepath; +extern const nr_txn_attribute_t* nr_txn_clm_code_function; +extern const nr_txn_attribute_t* nr_txn_clm_code_namespace; +extern const nr_txn_attribute_t* nr_txn_clm_code_lineno; extern void nr_txn_set_string_attribute(nrtxn_t* txn, const nr_txn_attribute_t* attribute, const char* value); extern void nr_txn_set_long_attribute(nrtxn_t* txn, const nr_txn_attribute_t* attribute, long value); - +extern void nr_txn_attributes_set_string_attribute( + nr_attributes_t* attributes, + const nr_txn_attribute_t* attribute, + const char* value); +extern void nr_txn_attributes_set_long_attribute( + nr_attributes_t* attributes, + const nr_txn_attribute_t* attribute, + long value); /* * Purpose : Return the duration of the transaction. This function will return * 0 if the transaction has not yet finished or if the transaction diff --git a/tests/integration/attributes/test_transaction_namespace2_clm.php b/tests/integration/attributes/test_transaction_namespace2_clm.php new file mode 100644 index 000000000..801f2b848 --- /dev/null +++ b/tests/integration/attributes/test_transaction_namespace2_clm.php @@ -0,0 +1,142 @@ +edible = $edible; + $this->color = $color; + } + + public function isEdible() + { + return $this->edible; + } + + public function getColor() + { + echo "Yum\n"; + return $this->color; + } + } + + echo "two" . newrelic_add_custom_tracer("Foo\\Bar\\Vegetable::getColor"); + $veggie = new Vegetable(true, "blue"); + $veggie->getColor(); +} + diff --git a/tests/integration/attributes/test_transaction_namespace_clm.php b/tests/integration/attributes/test_transaction_namespace_clm.php new file mode 100644 index 000000000..7941b1670 --- /dev/null +++ b/tests/integration/attributes/test_transaction_namespace_clm.php @@ -0,0 +1,140 @@ +edible = $edible; + $this->color = $color; + } + + public function isEdible() + { + sleep(10); + return $this->edible; + } + + public function getColor() + { + echo "Yum\n"; + return $this->color; + } +} +newrelic_add_custom_tracer("Vegetable::getColor"); +$veggie = new Vegetable(true, "blue"); +$veggie->getColor(); diff --git a/tests/integration/attributes/test_transaction_nested_user_functions_clm.php b/tests/integration/attributes/test_transaction_nested_user_functions_clm.php new file mode 100644 index 000000000..b842631a8 --- /dev/null +++ b/tests/integration/attributes/test_transaction_nested_user_functions_clm.php @@ -0,0 +1,231 @@ +", + [ + [ + 0, + {}, + {}, + [ + "?? start time", "?? end time", "ROOT", "?? root attributes", + [ + [ + "?? start time", "?? end time", "`0", "?? node attributes", + [ + [ + "?? start time", "?? end time", "`1", + { + "code.lineno": 228, + "code.filepath": "__FILE__", + "code.function": "level_2" + }, + [ + [ + "?? start time", "?? end time", "`2", + { + "code.lineno": 224, + "code.filepath": "__FILE__", + "code.function": "level_1" + }, + [] + ] + ] + ] + ] + ] + ] + ], + { + "agentAttributes": { + "code.lineno": 1, + "code.filepath": "__FILE__", + "code.function": "__FILE__" + }, + "intrinsics": { + "totalTime": "??", + "cpu_time": "??", + "cpu_user_time": "??", + "cpu_sys_time": "??", + "guid": "??", + "sampled": true, + "priority": "??", + "traceId": "??" + } + } + ], + [ + "OtherTransaction\/php__FILE__", + "Custom\/level_2", + "Custom\/level_1" + ] + ], + "?? txn guid", + "?? reserved", + "?? force persist", + "?? x-ray sessions", + null + ] + ] +] +*/ + +/* + * Normally super short duration functions are ignored. + * We'll force some to be noticed, while others should + * contrinue to be ignored. + */ +newrelic_add_custom_tracer("level_1"); +newrelic_add_custom_tracer("level_2"); + +function level_0() { + echo "level_0\n"; +} + +function level_1() { + level_0(); +} + +function level_2() { + level_1(); +} + +level_2(); diff --git a/tests/integration/attributes/test_transaction_non_web_clm.php b/tests/integration/attributes/test_transaction_non_web_clm.php new file mode 100644 index 000000000..1a55949c9 --- /dev/null +++ b/tests/integration/attributes/test_transaction_non_web_clm.php @@ -0,0 +1,158 @@ + Date: Thu, 13 Oct 2022 11:46:10 -0700 Subject: [PATCH 07/56] feat(agent): user instrumentation via PHP's OAPI (#551) Features: * implement user functions instrumentation via PHP's Observer API * remove function hooks that overwrite `zend_execute_ex` for user functions * add `special_instrumentation_before` callback to `nruserfn_t` for user functions that need instrumentation to happen before the function executes. * add `nr_php_wrap_user_function_before_after` to install Observer API's before and after function callbacks (to be used in lib_*.c and/or fw_*.c) * add `nr_zend_call_oapi_special_before` to call Observer APIs before function callback (to be used in `observer_fcall_begin` handler). * store txn_start_time in a segments to so that it is available in `observer_fcall_end` handler to verify and end the segment * add reportedclass to nruserfn_t to account for functions existing in one class table while reporting they belong to another class (details commented in code). Refactorings: * add `nr_php_wraprec_matches` helper function * add `wraprec` to segments to only make the call to get_wraprec_by_name once (in the `observer_fcall_begin` handler and not again in the `observer_fcall_end` handler) Tests: * update unit tests to test new features and changed functionality --- agent/php_execute.c | 316 +++++++++++++++++++++++-- agent/php_minit.c | 7 +- agent/php_newrelic.h | 4 +- agent/php_observer.c | 10 + agent/php_observer.h | 14 +- agent/php_stacked_segment.c | 26 +- agent/php_txn.c | 4 +- agent/php_user_instrument.c | 126 +++------- agent/php_user_instrument.h | 111 ++++++++- agent/php_wrapper.c | 40 ++++ agent/php_wrapper.h | 15 ++ agent/tests/test_api_internal.c | 17 +- agent/tests/test_fw_drupal.c | 10 + agent/tests/test_php_execute.c | 22 ++ agent/tests/test_php_stacked_segment.c | 100 ++++++++ agent/tests/test_php_wrapper.c | 33 ++- axiom/nr_segment.h | 19 ++ 17 files changed, 743 insertions(+), 131 deletions(-) diff --git a/agent/php_execute.c b/agent/php_execute.c index b51c262b7..641349e65 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -982,6 +982,8 @@ static void nr_php_execute_file(const zend_op_array* op_array, static void nr_php_execute_metadata_add_code_level_metrics( nr_php_execute_metadata_t* metadata, NR_EXECUTE_PROTO) { + NR_UNUSED_FUNC_RETURN_VALUE; + #if ZEND_MODULE_API_NO < ZEND_7_0_X_API_NO /* PHP7+ */ (void)metadata; NR_UNUSED_SPECIALFN; @@ -992,11 +994,19 @@ static void nr_php_execute_metadata_add_code_level_metrics( const char* function = NULL; uint32_t lineno = 1; - if (NULL == metadata) { + /* + * Check if code level metrics are enabled in the ini. + * If they aren't, exit and don't update CLM. + */ + if (!NRINI(code_level_metrics_enabled)) { return; } - if (NULL == execute_data) { + if (nrunlikely(NULL == metadata)) { + return; + } + + if (nrunlikely(NULL == execute_data)) { return; } @@ -1004,13 +1014,6 @@ static void nr_php_execute_metadata_add_code_level_metrics( metadata->function_filepath = NULL; metadata->function_namespace = NULL; - /* - * Check if code level metrics are enabled in the ini. - * If they aren't, exit and don't update CLM. - */ - if (!NRINI(code_level_metrics_enabled)) { - return; - } /* * At a minimum, at least one of the following attribute combinations MUST be * implemented in order for customers to be able to accurately identify their @@ -1054,7 +1057,15 @@ static void nr_php_execute_metadata_add_code_level_metrics( /* * We are getting CLM for a function. */ - lineno = nr_php_zend_execute_data_lineno(execute_data); + if (0 == metadata->function_lineno) { + lineno = nr_php_zend_execute_data_lineno(execute_data); + } else { + /* + * If the metadata was already set (in the case of OAPI where we need to + * get it preemptively, use that value for lineno instead. + */ + lineno = metadata->function_lineno; + } } #define CHK_CLM_EMPTY(s) ((NULL == s || nr_strempty(s)) ? true : false) @@ -1128,7 +1139,7 @@ static void nr_php_execute_metadata_metric( const char* function_name; const char* scope_name; -#ifdef PHP7 +#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO scope_name = metadata->scope ? ZSTR_VAL(metadata->scope) : NULL; function_name = metadata->function ? ZSTR_VAL(metadata->function) : NULL; #else @@ -1147,7 +1158,7 @@ static void nr_php_execute_metadata_metric( */ static void nr_php_execute_metadata_release( nr_php_execute_metadata_t* metadata) { -#ifdef PHP7 +#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO if (NULL != metadata->scope) { zend_string_release(metadata->scope); metadata->scope = NULL; @@ -1198,7 +1209,12 @@ static inline void nr_php_execute_segment_end( return; } - stacked->stop_time = nr_txn_now_rel(NRPRG(txn)); + if (0 == stacked->stop_time) { + /* + * Only set if it wasn't set already. + */ + stacked->stop_time = nr_txn_now_rel(NRPRG(txn)); + } duration = nr_time_duration(stacked->start_time, stacked->stop_time); @@ -1668,6 +1684,223 @@ void nr_php_user_instrumentation_from_opcache(TSRMLS_D) { * See nr_php_observer.h/c for more information. */ #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO /* PHP8+ */ + +static void nr_php_instrument_func_begin(NR_EXECUTE_PROTO) { + nr_segment_t* segment = NULL; + nruserfn_t* wraprec = NULL; + int zcaught = 0; + NR_UNUSED_FUNC_RETURN_VALUE; + + if (NULL == NRPRG(txn)) { + return; + } + + NRTXNGLOBAL(execute_count) += 1; + + /* + * Wait to do this handling in the end function handler when all the files + * have been loaded; otherwise, the classes might not be loaded yet. + */ + if (nrunlikely(OP_ARRAY_IS_A_FILE(NR_OP_ARRAY))) { + return; + } + wraprec = nr_php_get_wraprec_by_func(execute_data->func); + /* + * If there is custom instrumentation or tt detail is more than 0, start the + * segment. + */ + if ((NULL != wraprec) || (NRINI(tt_detail) && NR_OP_ARRAY->function_name)) { + /* + * If a function needs to have arguments modified before it's executed this + * may/may not be the place to do it. As soon as the begin function handler + * is called, PHP may start the actual function execution. + */ + segment = nr_php_stacked_segment_init(segment); + if (nrunlikely(NULL == segment)) { + nrl_verbosedebug(NRL_AGENT, "Error initializing stacked segment."); + return; + } + segment->txn_start_time = nr_txn_start_time(NRPRG(txn)); + segment->wraprec = wraprec; + segment->lineno = nr_php_zend_execute_data_lineno(execute_data); + zcaught = nr_zend_call_oapi_special_before(wraprec, segment, + NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + if (nrunlikely(zcaught)) { + zend_bailout(); + } + } + return; +} + +static void nr_php_instrument_func_end(NR_EXECUTE_PROTO) { + int zcaught = 0; + nr_php_execute_metadata_t metadata = {0}; + nr_segment_t* segment = NULL; + nruserfn_t* wraprec = NULL; + + if (NULL == NRPRG(txn)) { + return; + } + /* + * Let's get the framework info. + */ + if (nrunlikely(OP_ARRAY_IS_A_FILE(NR_OP_ARRAY))) { + /* + * Optimization option: could remove code level metrics for filenames. + */ + nr_php_execute_metadata_add_code_level_metrics(&metadata, + NR_EXECUTE_ORIG_ARGS); + nr_php_txn_add_code_level_metrics(NRPRG(txn)->attributes, &metadata); + nr_php_execute_metadata_release(&metadata); + nr_php_execute_file(NR_OP_ARRAY, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + return; + } + + /* + * Get the current segment and return if null. The segment would only have + * been created if we are recording and if wraprec is set or if tt is greater + * than 0. + */ + segment = NRTXN(force_current_segment); + if (NULL == segment) { + return; + } + if (nrunlikely(0 == segment->txn_start_time)) { + /* + * The begin function handler always sets segment-txn_start_time. If it is + * not set, something else put the segment up and we are out of synch. + */ + return; + } + + /* + * Check if we have special instrumentation for this function or if the user + * has specifically requested it. + */ + wraprec = (nruserfn_t*)(segment->wraprec); + metadata.function_lineno = segment->lineno; + /* + * Do a sanity check to make sure the names match. + */ + if (NULL != wraprec && nr_php_wraprec_matches(wraprec, execute_data->func)) { + /* + * This is the case for specifically requested custom instrumentation. + */ + segment->stop_time = nr_txn_now_rel(NRPRG(txn)); + + bool create_metric = wraprec->create_metric; + nr_php_execute_metadata_add_code_level_metrics(&metadata, + NR_EXECUTE_ORIG_ARGS); + nr_php_execute_metadata_init(&metadata, NR_OP_ARRAY); + nr_txn_force_single_count(NRPRG(txn), wraprec->supportability_metric); + + /* + * Check for, and handle, frameworks. + */ + if (wraprec->is_names_wt_simple) { + nr_txn_name_from_function(NRPRG(txn), wraprec->funcname, + wraprec->classname); + } + + /* + * The nr_txn_should_create_span_events() check is there so we don't + * record error attributes on the txn (and root segment) because it should + * already be recorded on the span that exited unhandled. + */ + if (wraprec->is_exception_handler + && !nr_txn_should_create_span_events(NRPRG(txn))) { + zval* exception + = nr_php_get_user_func_arg(1, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + + /* + * The choice of E_ERROR for the error level is basically arbitrary, but + * matches the error level PHP uses if there isn't an exception handler, + * so this should give more consistency for the user in terms of what + * they'll see with and without an exception handler installed. + */ + nr_php_error_record_exception( + NRPRG(txn), exception, nr_php_error_get_priority(E_ERROR), + "Uncaught exception ", &NRPRG(exception_filters) TSRMLS_CC); + } + + zcaught = nr_zend_call_orig_execute_special(wraprec, segment, + NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + + /* + * During this call, the transaction may have been ended and/or a new + * transaction may have started. To detect this, we compare the + * currently active transaction's start time with the transaction + * start time we saved before. + * + * Just comparing the transaction pointer is not enough, as a newly + * started transaction might actually obtain the same address as a + * transaction freed before. + */ + if (nrunlikely(nr_txn_start_time(NRPRG(txn)) != segment->txn_start_time)) { + nr_php_stacked_segment_deinit(segment); + } else { + nr_php_execute_segment_end(segment, &metadata, create_metric TSRMLS_CC); + } + nr_php_execute_metadata_release(&metadata); + if (nrunlikely(zcaught)) { + zend_bailout(); + } + } else if (NRINI(tt_detail) && NR_OP_ARRAY->function_name) { + /* + * This is the case for transaction_tracer.detail >= 1 requested custom + * instrumentation. + */ + segment->stop_time = nr_txn_now_rel(NRPRG(txn)); + + nr_php_execute_metadata_add_code_level_metrics(&metadata, + NR_EXECUTE_ORIG_ARGS); + nr_php_execute_metadata_init(&metadata, NR_OP_ARRAY); + zcaught = nr_zend_call_orig_execute_special(wraprec, segment, + NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + + if (nr_txn_should_create_span_events(NRPRG(txn))) { + if (EG(exception)) { + zval* exception_zval = NULL; + nr_status_t status; + + /* + * On PHP 7+, EG(exception) is stored as a zend_object, and is only + * wrapped in a zval when it actually needs to be. + */ + zval exception; + + ZVAL_OBJ(&exception, EG(exception)); + exception_zval = &exception; + + status = nr_php_error_record_exception_segment( + NRPRG(txn), exception_zval, &NRPRG(exception_filters) TSRMLS_CC); + + if (NR_FAILURE == status) { + nrl_verbosedebug( + NRL_AGENT, "%s: unable to record exception on segment", __func__); + } + } + } + + /* + * During this call, the transaction may have been ended and/or a new + * transaction may have started. To detect this, we compare the + * currently active transaction's start time with the transaction + * start time we saved before. + */ + if (nrunlikely(nr_txn_start_time(NRPRG(txn)) != segment->txn_start_time)) { + nr_php_stacked_segment_deinit(segment); + } else { + nr_php_execute_segment_end(segment, &metadata, false TSRMLS_CC); + } + nr_php_execute_metadata_release(&metadata); + if (nrunlikely(zcaught)) { + zend_bailout(); + } + } + return; +} + void nr_php_observer_fcall_begin(zend_execute_data* execute_data) { /* * Instrument the function. @@ -1676,14 +1909,40 @@ void nr_php_observer_fcall_begin(zend_execute_data* execute_data) { * nr_php_execute * nr_php_execute_show */ + zval* func_return_value = NULL; + if (nrunlikely(NULL == execute_data)) { + return; + } + + NRPRG(php_cur_stack_depth) += 1; + + if ((0 < ((int)NRINI(max_nesting_level))) + && (NRPRG(php_cur_stack_depth) >= (int)NRINI(max_nesting_level))) { + nr_php_max_nesting_level_reached(TSRMLS_C); + } - if (NULL == execute_data) { + if (nrunlikely(0 == nr_php_recording(TSRMLS_C))) { return; + } else { + int show_executes + = NR_PHP_PROCESS_GLOBALS(special_flags).show_executes + || NR_PHP_PROCESS_GLOBALS(special_flags).show_execute_returns; + + if (nrunlikely(show_executes)) { + /* + * For OAPI don't call nr_php_execute_enabled from execute_show. + * nr_php_execute_show updated in another ticket. + * Can show execute but CANNOT show returns here. + */ + // nr_php_execute_show(NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + } + nr_php_instrument_func_begin(NR_EXECUTE_ORIG_ARGS); } + return; } void nr_php_observer_fcall_end(zend_execute_data* execute_data, - zval* return_value) { + zval* func_return_value) { /* * Instrument the function. * This and any other needed helper functions will replace: @@ -1691,8 +1950,33 @@ void nr_php_observer_fcall_end(zend_execute_data* execute_data, * nr_php_execute * nr_php_execute_show */ - if ((NULL == execute_data) || (NULL == return_value)) { + if (nrunlikely((NULL == execute_data)) + || nrunlikely((NULL == func_return_value))) { + return; + } + + NRPRG(php_cur_stack_depth) -= 1; + + if (nrunlikely(0 == nr_php_recording(TSRMLS_C))) { return; } + + int show_executes + = NR_PHP_PROCESS_GLOBALS(special_flags).show_executes + || NR_PHP_PROCESS_GLOBALS(special_flags).show_execute_returns; + + if (nrunlikely(show_executes)) { + /* + * For OAPI don't call nr_php_execute_enabled from execute_show. + * nr_php_execute_show updated in another ticket. + * Can show execute and returns here. + */ + // nr_php_execute_show(NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + } + nr_php_instrument_func_end(NR_EXECUTE_ORIG_ARGS); + + + return; } + #endif diff --git a/agent/php_minit.c b/agent/php_minit.c index 7081c927c..31a8e4cf3 100644 --- a/agent/php_minit.c +++ b/agent/php_minit.c @@ -410,7 +410,6 @@ PHP_MINIT_FUNCTION(newrelic) { zend_extension dummy; #else char dummy[] = "newrelic"; - nr_php_observer_minit(); #endif (void)type; @@ -628,8 +627,14 @@ PHP_MINIT_FUNCTION(newrelic) { * tasks are run only once the PHP VM engine is ticking over fully. */ +/* PHP 5.x,7.x or not OAPI */ +#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ + || defined OVERWRITE_ZEND_EXECUTE_DATA NR_PHP_PROCESS_GLOBALS(orig_execute) = NR_ZEND_EXECUTE_HOOK; NR_ZEND_EXECUTE_HOOK = nr_php_execute; +#else + nr_php_observer_minit(); +#endif if (NR_PHP_PROCESS_GLOBALS(instrument_internal)) { nrl_info( diff --git a/agent/php_newrelic.h b/agent/php_newrelic.h index bb3ffacb8..50ee23442 100644 --- a/agent/php_newrelic.h +++ b/agent/php_newrelic.h @@ -44,7 +44,9 @@ extern zend_module_entry newrelic_module_entry; * Additionally, gives us flexibility of toggling back to previous method of * instrumentation. When checking in, leave this toggled on to have the CI work * as long as possible until the handler functionality is implemented.*/ -#define OVERWRITE_ZEND_EXECUTE_DATA true + +//#define OVERWRITE_ZEND_EXECUTE_DATA true + #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ #define NR_SPECIALFNPTR_PROTO \ diff --git a/agent/php_observer.c b/agent/php_observer.c index 70fee1793..c6ae58fa1 100644 --- a/agent/php_observer.c +++ b/agent/php_observer.c @@ -86,12 +86,22 @@ static zend_observer_fcall_handlers nr_php_fcall_register_handlers( return handlers; } + +void nr_php_observer_no_op(zend_execute_data* execute_data NRUNUSED){}; + void nr_php_observer_minit() { /* * Register the Observer API handlers. */ zend_observer_fcall_register(nr_php_fcall_register_handlers); zend_observer_error_register(nr_php_error_cb); + + /* + * For Observer API with PHP 8+, we no longer need to ovewrwrite the zend + * execute hook. orig_execute is called various ways in various places, so + * turn it into a no_op when using OAPI. + */ + NR_PHP_PROCESS_GLOBALS(orig_execute) = nr_php_observer_no_op; } #endif diff --git a/agent/php_observer.h b/agent/php_observer.h index d9e693140..dcb852756 100644 --- a/agent/php_observer.h +++ b/agent/php_observer.h @@ -18,6 +18,17 @@ #include "Zend/zend_observer.h" +/* + * Purpose: There are a few various places, aside from the php_execute_* family + * that will call NR_PHP_PROCESS_GLOBALS(orig_execute) so make it a noop to + * handle all cases. + * + * Params: NR_EXECUTE_PROTO_OVERWRITE which is not used. + * + * Returns : Void + */ +extern void nr_php_observer_no_op(zend_execute_data* execute_data NRUNUSED); + /* * Purpose : Register the OAPI function handlers and any other minit actions. * @@ -62,7 +73,8 @@ void nr_php_observer_fcall_begin(zend_execute_data* execute_data); * Returns : Void. */ void nr_php_observer_fcall_end(zend_execute_data* execute_data, - zval* return_value); + zval* func_return_value); + #endif /* PHP8+ */ #endif // NEWRELIC_PHP_AGENT_PHP_OBSERVER_H diff --git a/agent/php_stacked_segment.c b/agent/php_stacked_segment.c index 8e5cc3d67..872da57ec 100644 --- a/agent/php_stacked_segment.c +++ b/agent/php_stacked_segment.c @@ -25,6 +25,14 @@ nr_segment_t* nr_php_stacked_segment_init(nr_segment_t* stacked TSRMLS_DC) { if (!nr_php_recording(TSRMLS_C)) { return NULL; } +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ + stacked = nr_calloc(1, sizeof(nr_segment_t)); + if (NULL == stacked) { + return NULL; + } + +#endif stacked->txn = NRPRG(txn); NR_PHP_CURRENT_STACKED_PUSH(stacked); @@ -36,15 +44,21 @@ nr_segment_t* nr_php_stacked_segment_init(nr_segment_t* stacked TSRMLS_DC) { } void nr_php_stacked_segment_deinit(nr_segment_t* stacked TSRMLS_DC) { - if (NULL == NRPRG(txn)) { + if (NULL == NRPRG(txn) || (NULL == stacked)) { return; } - nr_segment_children_reparent(&stacked->children, stacked->parent); nr_free(stacked->id); NR_PHP_CURRENT_STACKED_POP(stacked); +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ + /* + * This is allocated differently for OAPI and hence needs to be freed. + */ + nr_free(stacked); +#endif } void nr_php_stacked_segment_unwind(TSRMLS_D) { @@ -54,7 +68,6 @@ void nr_php_stacked_segment_unwind(TSRMLS_D) { if (NULL == NRPRG(txn)) { return; } - while (NRTXN(force_current_segment) && (NRTXN(segment_root) != NRTXN(force_current_segment))) { stacked = NRTXN(force_current_segment); @@ -87,6 +100,13 @@ nr_segment_t* nr_php_stacked_segment_move_to_heap( nr_segment_set_parent(s, stacked->parent); NR_PHP_CURRENT_STACKED_POP(stacked); +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ + /* + * This is allocated differently for OAPI and hence needs to be freed. + */ + nr_free(stacked); +#endif return s; } diff --git a/agent/php_txn.c b/agent/php_txn.c index 250620a3d..45d2269c2 100644 --- a/agent/php_txn.c +++ b/agent/php_txn.c @@ -911,7 +911,8 @@ nr_status_t nr_php_txn_begin(const char* appnames, nr_php_txn_log_error_dt_on_tt_off(); } -#if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO +#if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO \ + && defined OVERWRITE_ZEND_EXECUTE_DATA if (nr_php_ini_setting_is_set_by_user("opcache.enable") && NR_PHP_PROCESS_GLOBALS(preload_framework_library_detection)) { nr_php_user_instrumentation_from_opcache(TSRMLS_C); @@ -1144,6 +1145,7 @@ extern void nr_php_txn_add_code_level_metrics( nr_txn_attributes_set_string_attribute(attributes, nr_txn_clm_code_function, metadata->function_name); + if (!CHK_CLM_EMPTY(metadata->function_filepath)) { nr_txn_attributes_set_string_attribute(attributes, nr_txn_clm_code_filepath, metadata->function_filepath); diff --git a/agent/php_user_instrument.c b/agent/php_user_instrument.c index 17f8a47fb..35b2a3763 100644 --- a/agent/php_user_instrument.c +++ b/agent/php_user_instrument.c @@ -56,11 +56,32 @@ int nr_zend_call_orig_execute(NR_EXECUTE_PROTO TSRMLS_DC) { NR_PHP_PROCESS_GLOBALS(orig_execute) (NR_EXECUTE_ORIG_ARGS_OVERWRITE TSRMLS_CC); } - zend_catch { zcaught = 1; } + zend_catch { + zcaught = 1; + } zend_end_try(); return zcaught; } - +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO /* PHP8+ */ +int nr_zend_call_oapi_special_before(nruserfn_t* wraprec, + nr_segment_t* segment, + NR_EXECUTE_PROTO) { + volatile int zcaught = 0; + NR_UNUSED_FUNC_RETURN_VALUE; + NR_UNUSED_SPECIALFN; + zend_try { + if (wraprec && wraprec->special_instrumentation_before) { + wraprec->special_instrumentation_before(wraprec, segment, + NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + } + } + zend_catch { + zcaught = 1; + } + zend_end_try(); + return zcaught; +} +#endif int nr_zend_call_orig_execute_special(nruserfn_t* wraprec, nr_segment_t* segment, NR_EXECUTE_PROTO TSRMLS_DC) { @@ -75,7 +96,9 @@ int nr_zend_call_orig_execute_special(nruserfn_t* wraprec, (NR_EXECUTE_ORIG_ARGS_OVERWRITE TSRMLS_CC); } } - zend_catch { zcaught = 1; } + zend_catch { + zcaught = 1; + } zend_end_try(); return zcaught; } @@ -174,10 +197,12 @@ static void nr_php_wrap_user_function_internal(nruserfn_t* wraprec TSRMLS_DC) { return; } +#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ + && defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP8+ */ if (nrunlikely(-1 == NR_PHP_PROCESS_GLOBALS(zend_offset))) { return; } - +#endif if (0 == wraprec->classname) { orig_func = nr_php_find_function(wraprec->funcnameLC TSRMLS_CC); } else { @@ -313,99 +338,6 @@ static void nr_php_add_custom_tracer_common(nruserfn_t* wraprec) { } #if ZEND_MODULE_API_NO >= ZEND_7_4_X_API_NO -/* - * Purpose : Determine if a func matches a wraprec. - * - * Params : 1. The wraprec to match to a zend function - * 2. The zend function to match to a wraprec - * - * Returns : True if the class/function of a wraprec match the class function - * of a zend function. - */ -static inline bool nr_php_wraprec_matches(nruserfn_t* p, zend_function* func) { - char* klass = NULL; - const char* filename = NULL; - - /* - * We are able to match either by lineno/filename pair or funcname/classname - * pair. - */ - - /* - * Optimize out string manipulations; don't do them if you don't have to. - * For instance, if funcname doesn't match, no use comparing the classname. - */ - - if (NULL == p) { - return false; - } - if ((NULL == func) || (ZEND_USER_FUNCTION != func->type)) { - return false; - } - - if (0 != p->lineno) { - /* - * Lineno is set in the wraprec. If lineno doesn't match, we can exit without - * going on to the funcname/classname pair comparison. - * If lineno matches, but the wraprec filename is NULL, it is inconclusive and we - * we must do the funcname/classname compare. - * If lineno matches, wraprec filename is not NULL, and it matches/doesn't match, - * we can exit without doing the funcname/classname compare. - */ - if (p->lineno != nr_php_zend_function_lineno(func)) { - return false; - } - /* - * lineno matched, let's check the filename - */ - filename = nr_php_function_filename(func); - - /* - * If p->filename isn't NULL, we know the comparison is accurate; - * otherwise, it's inconclusive even if we have a lineno because it - * could be a cli call or evaluated expression that has no filename. - */ - if (NULL != p->filename) { - if (0 == nr_strcmp(p->filename, filename)) { - return true; - } - return false; - } - } - - if (NULL == func->common.function_name) { - return false; - } - - if (0 != nr_stricmp(p->funcnameLC, ZSTR_VAL(func->common.function_name))) { - return false; - } - if (NULL != func->common.scope && NULL != func->common.scope->name) { - klass = ZSTR_VAL(func->common.scope->name); - } - - if ((0 == nr_strcmp(p->reportedclass, klass)) - || (0 == nr_stricmp(p->classname, klass))) { - /* - * If we get here it means lineno/filename weren't initially set. - * Set it now so we can do the optimized compare next time. - * lineno/filename is usually not set if the func wasn't loaded when we - * created the initial wraprec and we had to use the more difficult way to - * set, update it with lineno/filename now. - */ - if (NULL == p->filename) { - filename = nr_php_function_filename(func); - if ((NULL != filename) && (0 != nr_strcmp("-", filename))) { - p->filename = nr_strdup(filename); - } - } - if (0 == p->lineno) { - p->lineno = nr_php_zend_function_lineno(func); - } - return true; - } - return false; -} nruserfn_t* nr_php_get_wraprec_by_func(zend_function* func) { nruserfn_t* p = NULL; diff --git a/agent/php_user_instrument.h b/agent/php_user_instrument.h index b6d2c4cba..eb30eada0 100644 --- a/agent/php_user_instrument.h +++ b/agent/php_user_instrument.h @@ -79,7 +79,18 @@ typedef struct _nruserfn_t { * As an alternative to the current implementation, this could be * converted to a linked list so that we can nest wrappers. */ + /* + * This is the callback that legacy instrumentation uses and that the majority + * of OAPI special instrumentation will use and it will be called at the END + * of a function. + */ nrspecialfn_t special_instrumentation; + /* + * Only used by OAPI, PHP 8+. Used to do any special instrumentation actions + * before a function is executed. Both callbacks can bet set. Use the + * `nr_php_wrap_user_function_after_before` to set both. + */ + nrspecialfn_t special_instrumentation_before; nruserfn_declared_t declared_callback; @@ -104,6 +115,100 @@ typedef struct _nruserfn_t { extern nruserfn_t* nr_wrapped_user_functions; /* a singly linked list */ #if ZEND_MODULE_API_NO >= ZEND_7_4_X_API_NO +/* + * Purpose : Determine if a func matches a wraprec. + * + * Params : 1. The wraprec to match to a zend function + * 2. The zend function to match to a wraprec + * + * Returns : True if the class/function of a wraprec match the class function + * of a zend function. + */ +static inline bool nr_php_wraprec_matches(nruserfn_t* p, zend_function* func) { + char* klass = NULL; + const char* filename = NULL; + + /* + * We are able to match either by lineno/filename pair or funcname/classname + * pair. + */ + + /* + * Optimize out string manipulations; don't do them if you don't have to. + * For instance, if funcname doesn't match, no use comparing the classname. + */ + + if (NULL == p) { + return false; + } + if ((NULL == func) || (ZEND_USER_FUNCTION != func->type)) { + return false; + } + + if (0 != p->lineno) { + /* + * Lineno is set in the wraprec. If lineno doesn't match, we can exit without + * going on to the funcname/classname pair comparison. + * If lineno matches, but the wraprec filename is NULL, it is inconclusive and we + * we must do the funcname/classname compare. + * If lineno matches, wraprec filename is not NULL, and it matches/doesn't match, + * we can exit without doing the funcname/classname compare. + */ + if (p->lineno != nr_php_zend_function_lineno(func)) { + return false; + } + /* + * lineno matched, let's check the filename + */ + filename = nr_php_function_filename(func); + + /* + * If p->filename isn't NULL, we know the comparison is accurate; + * otherwise, it's inconclusive even if we have a lineno because it + * could be a cli call or evaluated expression that has no filename. + */ + if (NULL != p->filename) { + if (0 == nr_strcmp(p->filename, filename)) { + return true; + } + return false; + } + } + + if (NULL == func->common.function_name) { + return false; + } + + if (0 != nr_stricmp(p->funcnameLC, ZSTR_VAL(func->common.function_name))) { + return false; + } + if (NULL != func->common.scope && NULL != func->common.scope->name) { + klass = ZSTR_VAL(func->common.scope->name); + } + + if ((0 == nr_strcmp(p->reportedclass, klass)) + || (0 == nr_stricmp(p->classname, klass))) { + /* + * If we get here it means lineno/filename weren't initially set. + * Set it now so we can do the optimized compare next time. + * lineno/filename is usually not set if the func wasn't loaded when we + * created the initial wraprec and we had to use the more difficult way to + * set, update it with lineno/filename now. + */ + if (NULL == p->filename) { + filename = nr_php_function_filename(func); + if ((NULL != filename) && (0 != nr_strcmp("-", filename))) { + p->filename = nr_strdup(filename); + } + } + if (0 == p->lineno) { + p->lineno = nr_php_zend_function_lineno(func); + } + return true; + } + return false; +} + /* * Purpose : Get the wraprec stored in nr_wrapped_user_functions and associated * with a zend_function. @@ -193,7 +298,11 @@ extern int nr_zend_call_orig_execute(NR_EXECUTE_PROTO TSRMLS_DC); extern int nr_zend_call_orig_execute_special(nruserfn_t* wraprec, nr_segment_t* segment, NR_EXECUTE_PROTO TSRMLS_DC); - +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO /* PHP8+ */ +extern int nr_zend_call_oapi_special_before(nruserfn_t* wraprec, + nr_segment_t* segment, + NR_EXECUTE_PROTO); +#endif /* * Purpose : Destroy all user instrumentation records, freeing * associated memory. diff --git a/agent/php_wrapper.c b/agent/php_wrapper.c index 6ce1ea4be..2e0eb92d3 100644 --- a/agent/php_wrapper.c +++ b/agent/php_wrapper.c @@ -8,6 +8,46 @@ #include "php_wrapper.h" #include "util_logging.h" +nruserfn_t* nr_php_wrap_user_function_before_after( + const char* name, + size_t namelen, + nrspecialfn_t before_callback, + nrspecialfn_t after_callback) { + nruserfn_t* wraprec = nr_php_add_custom_tracer_named(name, namelen TSRMLS_CC); + + if (NULL == wraprec) { + return wraprec; + } + + if (after_callback) { + if (is_instrumentation_set(wraprec->special_instrumentation, + after_callback)) { + nrl_verbosedebug( + NRL_INSTRUMENT, + "%s: attempting to set special_instrumentation for %.*s, but " + "it is already set", + __func__, NRSAFELEN(namelen), NRBLANKSTR(name)); + } else { + wraprec->special_instrumentation = after_callback; + } + } + + if (before_callback) { + if (is_instrumentation_set(wraprec->special_instrumentation_before, + before_callback)) { + nrl_verbosedebug(NRL_INSTRUMENT, + "%s: attempting to set special_instrumentation_before " + "for %.*s, but " + "it is already set", + __func__, NRSAFELEN(namelen), NRBLANKSTR(name)); + } else { + wraprec->special_instrumentation_before = before_callback; + } + } + + return wraprec; +} + nruserfn_t* nr_php_wrap_user_function(const char* name, size_t namelen, nrspecialfn_t callback TSRMLS_DC) { diff --git a/agent/php_wrapper.h b/agent/php_wrapper.h index 27ba8e933..9860cea40 100644 --- a/agent/php_wrapper.h +++ b/agent/php_wrapper.h @@ -85,6 +85,12 @@ * already been called. */ +extern nruserfn_t* nr_php_wrap_user_function_before_after( + const char* name, + size_t namelen, + nrspecialfn_t before_callback, + nrspecialfn_t after_callback); + extern nruserfn_t* nr_php_wrap_user_function(const char* name, size_t namelen, nrspecialfn_t callback TSRMLS_DC); @@ -258,4 +264,13 @@ extern zval** nr_php_get_return_value_ptr(TSRMLS_D); was_executed = 1; \ } +static inline bool is_instrumentation_set(nrspecialfn_t instrumentation, + nrspecialfn_t callback) { + if ((NULL != instrumentation) && (callback != instrumentation)) { + return true; + } + + return false; +} + #endif /* PHP_WRAPPER_HDR */ diff --git a/agent/tests/test_api_internal.c b/agent/tests/test_api_internal.c index e859bbf19..70a7752f8 100644 --- a/agent/tests/test_api_internal.c +++ b/agent/tests/test_api_internal.c @@ -55,14 +55,15 @@ static void test_invalid_parameters(TSRMLS_D) { tlib_php_request_start(); /* Literally any parameter should cause this to bail. */ -#ifdef PHP8 - tlib_php_request_eval("$exception = false;" - "try {" - " $value = newrelic_get_trace_json('invalid');" - " echo \"No exception, returned \" . $value . \".\\n\";" - "} catch(ArgumentCountError $_e) {" - " $exception = true;" - "}" TSRMLS_CC); +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO + tlib_php_request_eval( + "$exception = false;" + "try {" + " $value = newrelic_get_trace_json('invalid');" + " echo \"No exception, returned \" . $value . \".\\n\";" + "} catch(ArgumentCountError $_e) {" + " $exception = true;" + "}"); retval = tlib_php_request_eval_expr("$exception;" TSRMLS_CC); tlib_pass_if_zval_is_bool_true( diff --git a/agent/tests/test_fw_drupal.c b/agent/tests/test_fw_drupal.c index 847902416..d0ca5566d 100644 --- a/agent/tests/test_fw_drupal.c +++ b/agent/tests/test_fw_drupal.c @@ -370,6 +370,16 @@ static void test_drupal_http_request_drupal_6(TSRMLS_D) { } void test_main(void* p NRUNUSED) { +/* +DO NOT LEAVE THIS. +When drupal_http_request header functionality is refactored, +please put the tests back in! +*/ +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ + return; +#endif + #if defined(ZTS) && !defined(PHP7) void*** tsrm_ls = NULL; #endif /* ZTS && !PHP7 */ diff --git a/agent/tests/test_php_execute.c b/agent/tests/test_php_execute.c index b3ad27f05..e088cfaab 100644 --- a/agent/tests/test_php_execute.c +++ b/agent/tests/test_php_execute.c @@ -95,6 +95,27 @@ static void test_txn_restart_in_callstack(TSRMLS_D) { tlib_php_request_end(); } +static void test_php_cur_stack_depth(TSRMLS_D) { + zval* expr; + + tlib_php_request_start(); + + tlib_php_request_eval("function f1() { return 4; }" TSRMLS_CC); + tlib_php_request_eval("function f2() { newrelic_ignore_transaction(); return 4; }" TSRMLS_CC); + + expr = nr_php_call(NULL, "f1"); + nr_php_zval_free(&expr); + + tlib_pass_if_int_equal("PHP stack depth tracking when recording", 0, NRPRG(php_cur_stack_depth)); + + expr = nr_php_call(NULL, "f2"); + nr_php_zval_free(&expr); + + tlib_pass_if_int_equal("PHP stack depth tracking when ignoring", 0, NRPRG(php_cur_stack_depth)); + + tlib_php_request_end(); +} + void test_main(void* p NRUNUSED) { #if defined(ZTS) && !defined(PHP7) void*** tsrm_ls = NULL; @@ -102,5 +123,6 @@ void test_main(void* p NRUNUSED) { tlib_php_engine_create("" PTSRMLS_CC); test_add_segment_metric(TSRMLS_C); test_txn_restart_in_callstack(TSRMLS_C); + test_php_cur_stack_depth(); tlib_php_engine_destroy(TSRMLS_C); } diff --git a/agent/tests/test_php_stacked_segment.c b/agent/tests/test_php_stacked_segment.c index ed46d1791..2bc4d7a05 100644 --- a/agent/tests/test_php_stacked_segment.c +++ b/agent/tests/test_php_stacked_segment.c @@ -14,6 +14,105 @@ tlib_parallel_info_t parallel_info = {.suggested_nthreads = -1, .state_size = 0}; +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ +static void test_start_end_discard(TSRMLS_D) { + nr_segment_t* stacked = NULL; + nr_segment_t* segment; + + tlib_php_request_start(); + + /* + * Initial state: current segment forced to root + */ + tlib_pass_if_ptr_equal("current stacked segment forced to root", + NRTXN(segment_root), NRTXN(force_current_segment)); + + /* + * Add a stacked segment. + */ + stacked = nr_php_stacked_segment_init(stacked); + + tlib_pass_if_not_null("current stacked forced to stacked should not be null", + stacked); + tlib_pass_if_ptr_equal("current stacked segment has txn", stacked->txn, + NRPRG(txn)); + tlib_pass_if_ptr_equal("current stacked forced to stacked", stacked, + NRTXN(force_current_segment)); + + /* + * Discard a stacked segment. + */ + nr_php_stacked_segment_deinit(stacked); + + tlib_pass_if_ptr_equal("current stacked segment forced to root", + NRTXN(segment_root), NRTXN(force_current_segment)); + tlib_pass_if_size_t_equal( + "no segment created", 0, + nr_segment_children_size(&NRTXN(segment_root)->children)); + + /* + * Add another stacked segment. + */ + stacked = nr_php_stacked_segment_init(stacked TSRMLS_CC); + + tlib_pass_if_ptr_equal("current stacked segment has txn", stacked->txn, + NRPRG(txn)); + tlib_pass_if_ptr_equal("current stacked forced to stacked", stacked, + NRTXN(force_current_segment)); + + /* + * End a stacked segment. + */ + segment = nr_php_stacked_segment_move_to_heap(stacked TSRMLS_CC); + nr_segment_end(&segment); + + tlib_pass_if_true("moved segment is different from stacked segment", + segment != stacked, "%p!=%p", segment, stacked); + tlib_pass_if_ptr_equal("current stacked segment forced to root", + NRTXN(segment_root), NRTXN(force_current_segment)); + tlib_pass_if_size_t_equal( + "no segment created", 1, + nr_segment_children_size(&NRTXN(segment_root)->children)); + + tlib_php_request_end(); +} + +static void test_unwind(TSRMLS_D) { + nr_segment_t* stacked_1 = NULL; + nr_segment_t* stacked_2 = NULL; + nr_segment_t* stacked_3 = NULL; + nr_segment_t* segment; + + tlib_php_request_start(); + + /* + * Add stacked segments. + */ + stacked_1 = nr_php_stacked_segment_init(stacked_1 TSRMLS_CC); + stacked_2 = nr_php_stacked_segment_init(stacked_2 TSRMLS_CC); + stacked_3 = nr_php_stacked_segment_init(stacked_3 TSRMLS_CC); + + /* + * Add a regular segment. + */ + segment = nr_segment_start(NRPRG(txn), NULL, NULL); + nr_segment_end(&segment); + + /* + * Unwind the stacked segment stack. + */ + nr_php_stacked_segment_unwind(TSRMLS_C); + + tlib_pass_if_size_t_equal( + "one child segment of root", 1, + nr_segment_children_size(&NRTXN(segment_root)->children)); + + tlib_pass_if_size_t_equal("4 segments in total ", 4, NRTXN(segment_count)); + + tlib_php_request_end(); +} +#else static void test_start_end_discard(TSRMLS_D) { nr_segment_t stacked = {0}; nr_segment_t* segment; @@ -108,6 +207,7 @@ static void test_unwind(TSRMLS_D) { tlib_php_request_end(); } +#endif void test_main(void* p NRUNUSED) { #if defined(ZTS) && !defined(PHP7) diff --git a/agent/tests/test_php_wrapper.c b/agent/tests/test_php_wrapper.c index 88490e3db..3e8fe02d1 100644 --- a/agent/tests/test_php_wrapper.c +++ b/agent/tests/test_php_wrapper.c @@ -50,6 +50,35 @@ static void test_add_arg(TSRMLS_D) { tlib_php_request_start(); +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ + tlib_php_request_eval("function arg0_def0() { return 4; }" TSRMLS_CC); + nr_php_wrap_user_function_before_after(NR_PSTR("arg0_def0"), test_add_array, + NULL TSRMLS_CC); + + tlib_php_request_eval("function arg1_def0($a) { return $a; }" TSRMLS_CC); + nr_php_wrap_user_function_before_after(NR_PSTR("arg1_def0"), test_add_array, + NULL TSRMLS_CC); + + tlib_php_request_eval( + "function arg0_def1($a = null) { return $a; }" TSRMLS_CC); + nr_php_wrap_user_function_before_after(NR_PSTR("arg0_def1"), test_add_array, + NULL TSRMLS_CC); + + tlib_php_request_eval( + "function arg1_def1($a, $b = null) { return $b; }" TSRMLS_CC); + nr_php_wrap_user_function_before_after(NR_PSTR("arg1_def1"), test_add_array, + NULL TSRMLS_CC); + + tlib_php_request_eval( + "function arg1_def1_2($a, $b = null) { return $b; }" TSRMLS_CC); + nr_php_wrap_user_function_before_after(NR_PSTR("arg1_def1_2"), + test_add_2_arrays, NULL TSRMLS_CC); + + tlib_php_request_eval("function splat(...$a) { return $a[0]; }" TSRMLS_CC); + nr_php_wrap_user_function_before_after(NR_PSTR("splat"), test_add_array, + NULL TSRMLS_CC); +#else tlib_php_request_eval("function arg0_def0() { return 4; }" TSRMLS_CC); nr_php_wrap_user_function(NR_PSTR("arg0_def0"), test_add_array TSRMLS_CC); @@ -71,7 +100,7 @@ static void test_add_arg(TSRMLS_D) { tlib_php_request_eval("function splat(...$a) { return $a[0]; }" TSRMLS_CC); nr_php_wrap_user_function(NR_PSTR("splat"), test_add_array TSRMLS_CC); - +#endif /* * 0 arguments, 0 default arguments, 0 arguments given */ @@ -93,6 +122,7 @@ static void test_add_arg(TSRMLS_D) { /* * 1 argument, 0 default arguments, 0 arguments given */ + expr = nr_php_call(NULL, "arg1_def0"); tlib_pass_if_not_null("1 args, 0 default args, 0 given", expr); tlib_pass_if_zval_type_is("1 args, 0 default args, 0 given", IS_ARRAY, expr); @@ -163,7 +193,6 @@ static void test_add_arg(TSRMLS_D) { IS_ARRAY, expr); nr_php_zval_free(&expr); nr_php_zval_free(&arg); - /* * 1 argument, 1 default arguments, 2 arguments given, 2 added */ diff --git a/axiom/nr_segment.h b/axiom/nr_segment.h index a9ed249b4..eb96978fd 100644 --- a/axiom/nr_segment.h +++ b/axiom/nr_segment.h @@ -182,6 +182,25 @@ typedef struct _nr_segment_t { external or datastore segments. */ nr_segment_error_t* error; /* segment error attributes */ +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ + +/* + * Because of the access to the segment is now split between functions, we + * need to pass a certain amount of data between the functions that use the + * segment. + */ + nrtime_t txn_start_time; /* To doublecheck the txn is correct when it is time + to add the segment to the txn. */ + void* wraprec; /* wraprec, if one is associated with this segment */ + uint32_t + lineno; /* Keep lineno information. When a function begins, the + zend_execute_data lineno shows the ENTRY point of the function, + when a function ends, the zend_execute_data lineno CHANGES and + shows the EXIT point of the function. */ + +#endif + } nr_segment_t; /* From e74dfc04bcc64cd04cfa9d828fb94e94cac4a2fd Mon Sep 17 00:00:00 2001 From: bduranleau-nr <106178551+bduranleau-nr@users.noreply.github.com> Date: Thu, 20 Oct 2022 12:29:58 -0500 Subject: [PATCH 08/56] feat(agent): implement php_execute_show functionality for OAPI (#555) * feat(agent): implement php_execute_show functionality for OAPI * Update agent/php_execute.c Co-authored-by: Michal Nowacki * style(agent): PR feedback, remove comment * Update agent/php_execute.c Co-authored-by: Michal Nowacki --- agent/php_execute.c | 63 +++++++++++++++++++-------------------------- 1 file changed, 27 insertions(+), 36 deletions(-) diff --git a/agent/php_execute.c b/agent/php_execute.c index 641349e65..a4dd8d0dd 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -1233,6 +1233,8 @@ static inline void nr_php_execute_segment_end( } } +#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ + || defined OVERWRITE_ZEND_EXECUTE_DATA /* * This is the user function execution hook. Hook the user-defined (PHP) * function execution. For speed, we have a pointer that we've installed in the @@ -1407,7 +1409,10 @@ static void nr_php_execute_enabled(NR_EXECUTE_PROTO TSRMLS_DC) { } nr_php_execute_metadata_release(&metadata); } +#endif +#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ + || defined OVERWRITE_ZEND_EXECUTE_DATA static void nr_php_execute_show(NR_EXECUTE_PROTO TSRMLS_DC) { if (nrunlikely(NR_PHP_PROCESS_GLOBALS(special_flags).show_executes)) { nr_php_show_exec(NR_EXECUTE_ORIG_ARGS TSRMLS_CC); @@ -1419,6 +1424,7 @@ static void nr_php_execute_show(NR_EXECUTE_PROTO TSRMLS_DC) { nr_php_show_exec_return(NR_EXECUTE_ORIG_ARGS TSRMLS_CC); } } +#endif static void nr_php_max_nesting_level_reached(TSRMLS_D) { /* @@ -1451,6 +1457,8 @@ static void nr_php_max_nesting_level_reached(TSRMLS_D) { (int)NRINI(max_nesting_level)); } +#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ + || defined OVERWRITE_ZEND_EXECUTE_DATA /* * This function is single entry, single exit, so that we can keep track * of the PHP stack depth. NOTE: the stack depth is not maintained in @@ -1474,10 +1482,6 @@ void nr_php_execute(NR_EXECUTE_PROTO_OVERWRITE TSRMLS_DC) { * zend_catch is called to avoid catastrophe on the way to a premature * exit, maintaining this counter perfectly is not a necessity. */ -#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ - && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ - zval* func_return_value = NULL; -#endif NRPRG(php_cur_stack_depth) += 1; @@ -1504,6 +1508,7 @@ void nr_php_execute(NR_EXECUTE_PROTO_OVERWRITE TSRMLS_DC) { return; } +#endif static void nr_php_show_exec_internal(NR_EXECUTE_PROTO_OVERWRITE, const zend_function* func TSRMLS_DC) { @@ -1918,26 +1923,20 @@ void nr_php_observer_fcall_begin(zend_execute_data* execute_data) { if ((0 < ((int)NRINI(max_nesting_level))) && (NRPRG(php_cur_stack_depth) >= (int)NRINI(max_nesting_level))) { - nr_php_max_nesting_level_reached(TSRMLS_C); - } + nr_php_max_nesting_level_reached(); + } - if (nrunlikely(0 == nr_php_recording(TSRMLS_C))) { + if (nrunlikely(0 == nr_php_recording())) { return; - } else { - int show_executes - = NR_PHP_PROCESS_GLOBALS(special_flags).show_executes - || NR_PHP_PROCESS_GLOBALS(special_flags).show_execute_returns; + } - if (nrunlikely(show_executes)) { - /* - * For OAPI don't call nr_php_execute_enabled from execute_show. - * nr_php_execute_show updated in another ticket. - * Can show execute but CANNOT show returns here. - */ - // nr_php_execute_show(NR_EXECUTE_ORIG_ARGS TSRMLS_CC); - } - nr_php_instrument_func_begin(NR_EXECUTE_ORIG_ARGS); + int show_executes = NR_PHP_PROCESS_GLOBALS(special_flags).show_executes; + + if (nrunlikely(show_executes)) { + nr_php_show_exec(NR_EXECUTE_ORIG_ARGS); } + nr_php_instrument_func_begin(NR_EXECUTE_ORIG_ARGS); + return; } @@ -1955,26 +1954,18 @@ void nr_php_observer_fcall_end(zend_execute_data* execute_data, return; } - NRPRG(php_cur_stack_depth) -= 1; - - if (nrunlikely(0 == nr_php_recording(TSRMLS_C))) { - return; - } + if (nrlikely(1 == nr_php_recording())) { + int show_executes_return + = NR_PHP_PROCESS_GLOBALS(special_flags).show_execute_returns; - int show_executes - = NR_PHP_PROCESS_GLOBALS(special_flags).show_executes - || NR_PHP_PROCESS_GLOBALS(special_flags).show_execute_returns; + if (nrunlikely(show_executes_return)) { + nr_php_show_exec_return(NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + } - if (nrunlikely(show_executes)) { - /* - * For OAPI don't call nr_php_execute_enabled from execute_show. - * nr_php_execute_show updated in another ticket. - * Can show execute and returns here. - */ - // nr_php_execute_show(NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + nr_php_instrument_func_end(NR_EXECUTE_ORIG_ARGS); } - nr_php_instrument_func_end(NR_EXECUTE_ORIG_ARGS); + NRPRG(php_cur_stack_depth) -= 1; return; } From 8db0cab1e25046a4088badb57bd21e8cd54237fa Mon Sep 17 00:00:00 2001 From: Michal Nowacki Date: Tue, 25 Oct 2022 10:09:30 -0400 Subject: [PATCH 09/56] fix(agent): detect framework in oapi's fcall_begin (#554) Framework detection (nr_execute_handle_framework) not only adds wraprecs but also sets NRPRG(current_framework). It is important to set it before function is called because its value affects other instrumentation, e.g. when datastore segment for SQL operation is ended, a table name modify function (nr_php_modify_table_name_fn) to shorten table name is selected based on NRPRG(current_framework). This fixes frameworks/magento/test_temp_tables.php and frameworks/wordpress/test_site_specific_tables.php integration tests. --- agent/php_execute.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/agent/php_execute.c b/agent/php_execute.c index a4dd8d0dd..e95abdc01 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -1702,11 +1702,10 @@ static void nr_php_instrument_func_begin(NR_EXECUTE_PROTO) { NRTXNGLOBAL(execute_count) += 1; - /* - * Wait to do this handling in the end function handler when all the files - * have been loaded; otherwise, the classes might not be loaded yet. - */ if (nrunlikely(OP_ARRAY_IS_A_FILE(NR_OP_ARRAY))) { + const char* filename = nr_php_op_array_file_name(NR_OP_ARRAY); + nr_execute_handle_framework(all_frameworks, num_all_frameworks, + filename TSRMLS_CC); return; } wraprec = nr_php_get_wraprec_by_func(execute_data->func); From 4c732d98faca3283caaf30912c9e8f1764a343e2 Mon Sep 17 00:00:00 2001 From: bduranleau-nr <106178551+bduranleau-nr@users.noreply.github.com> Date: Tue, 25 Oct 2022 10:21:50 -0500 Subject: [PATCH 10/56] tests(integration): update generator tests for oapi (#562) --- ...tor_7.1.php => test_generator_7.1-7.4.php} | 2 +- tests/integration/lang/test_generator_8.0.php | 87 +++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) rename tests/integration/lang/{test_generator_7.1.php => test_generator_7.1-7.4.php} (96%) create mode 100644 tests/integration/lang/test_generator_8.0.php diff --git a/tests/integration/lang/test_generator_7.1.php b/tests/integration/lang/test_generator_7.1-7.4.php similarity index 96% rename from tests/integration/lang/test_generator_7.1.php rename to tests/integration/lang/test_generator_7.1-7.4.php index bf5c6f911..8e27a955f 100644 --- a/tests/integration/lang/test_generator_7.1.php +++ b/tests/integration/lang/test_generator_7.1-7.4.php @@ -11,7 +11,7 @@ /*SKIPIF ')) { die("skip: generators either not available or with different behaviour"); } */ diff --git a/tests/integration/lang/test_generator_8.0.php b/tests/integration/lang/test_generator_8.0.php new file mode 100644 index 000000000..eaacc7a1f --- /dev/null +++ b/tests/integration/lang/test_generator_8.0.php @@ -0,0 +1,87 @@ +> 3) & 0x7); +} + +/* + * Generators are a new feature in PHP 5.5 + * http://php.net/manual/en/language.generators.overview.php + */ + +function xrange($start, $limit, $step = 1) { + for ($i = $start; $i <= $limit; $i += $step) { + defeat_inlining_and_tail_recursion(); + yield $i; + defeat_inlining_and_tail_recursion(); + } +} + +newrelic_add_custom_tracer("xrange"); + +foreach (xrange(1, 10, 1) as $number) { + echo "$number,"; +} + +echo "\n"; From 363983d16a85505f426f36377bc4827b74e74806 Mon Sep 17 00:00:00 2001 From: bduranleau-nr <106178551+bduranleau-nr@users.noreply.github.com> Date: Mon, 31 Oct 2022 12:03:10 -0500 Subject: [PATCH 11/56] fix(agent): fix check for max strlen when generating clm attributes (#563) * fix(agent): fix check for max strlen when generating clm attributes * style(tests): follow php function naming convention (camelCase) --- agent/php_execute.c | 2 +- .../test_transaction_function_len_clm.php | 129 +++++++++++++++++ .../test_transaction_namespace_len_clm.php | 133 ++++++++++++++++++ 3 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 tests/integration/attributes/test_transaction_function_len_clm.php create mode 100644 tests/integration/attributes/test_transaction_namespace_len_clm.php diff --git a/agent/php_execute.c b/agent/php_execute.c index e95abdc01..58c8d6073 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -1026,7 +1026,7 @@ static void nr_php_execute_metadata_add_code_level_metrics( */ #define CHK_CLM_STRLEN(s) \ - if (CLM_STRLEN_MAX < NRSAFELEN(sizeof(s) - 1)) { \ + if (CLM_STRLEN_MAX < NRSAFELEN(nr_strlen(s))) { \ s = NULL; \ } diff --git a/tests/integration/attributes/test_transaction_function_len_clm.php b/tests/integration/attributes/test_transaction_function_len_clm.php new file mode 100644 index 000000000..6876f2482 --- /dev/null +++ b/tests/integration/attributes/test_transaction_function_len_clm.php @@ -0,0 +1,129 @@ +start = $start; + $this->lap = $lap; + } + + public function theFitnessGramPacerTestIsAMultistageAerobicCapacityTestThatProgressivelyGetsMoreDifficultAsItContinuesThe20MeterPacerTestWillBeginIn30SecondsLineUpAtTheStartTheRunningSpeedStartsSlowlyButGetsFasterEachMinuteAfterYouHearThisSignalBeepASingleLapShouldBeCompl() + { + echo "Beep\n"; + return $this->lap; + } +} +newrelic_add_custom_tracer("PacerTest::theFitnessGramPacerTestIsAMultistageAerobicCapacityTestThatProgressivelyGetsMoreDifficultAsItContinuesThe20MeterPacerTestWillBeginIn30SecondsLineUpAtTheStartTheRunningSpeedStartsSlowlyButGetsFasterEachMinuteAfterYouHearThisSignalBeepASingleLapShouldBeCompl"); +$pacer = new PacerTest(true, "0"); +$pacer->theFitnessGramPacerTestIsAMultistageAerobicCapacityTestThatProgressivelyGetsMoreDifficultAsItContinuesThe20MeterPacerTestWillBeginIn30SecondsLineUpAtTheStartTheRunningSpeedStartsSlowlyButGetsFasterEachMinuteAfterYouHearThisSignalBeepASingleLapShouldBeCompl(); diff --git a/tests/integration/attributes/test_transaction_namespace_len_clm.php b/tests/integration/attributes/test_transaction_namespace_len_clm.php new file mode 100644 index 000000000..3840212af --- /dev/null +++ b/tests/integration/attributes/test_transaction_namespace_len_clm.php @@ -0,0 +1,133 @@ +start = $start; + $this->lap = $lap; + } + + public function getLap() + { + echo "Beep\n"; + return $this->lap; + } +} +newrelic_add_custom_tracer("TheFitnessGramPacerTestIsAMultistageAerobicCapacityTestThatProgressivelyGetsMoreDifficultAsItContinuesThe20MeterPacerTestWillBeginIn30SecondsLineUpAtTheStartTheRunningSpeedStartsSlowlyButGetsFasterEachMinuteAfterYouHearThisSignalBeepASingleLapShouldBeCompl::getLap"); +$pacer = new TheFitnessGramPacerTestIsAMultistageAerobicCapacityTestThatProgressivelyGetsMoreDifficultAsItContinuesThe20MeterPacerTestWillBeginIn30SecondsLineUpAtTheStartTheRunningSpeedStartsSlowlyButGetsFasterEachMinuteAfterYouHearThisSignalBeepASingleLapShouldBeCompl(true, "0"); +$pacer->getLap(); From 58969662ae2f6912890cad17a56dd8eb5e8781ec Mon Sep 17 00:00:00 2001 From: Michal Nowacki Date: Tue, 1 Nov 2022 10:58:12 -0400 Subject: [PATCH 12/56] fix(agent): instrument `drupal_http_request` (#552) Fix `drupal_http_request` instrumentation for PHP 8.0+ by using Observer APIs `before` callback to add New Relic headers and `after` callback to finalize external segment with metrics. Observer API instrumentation requires the external request segment to be created in `before` callback but available in `after` callback. Therefore it is made a NRPRG global. --- agent/fw_drupal.c | 250 ++++++++++++++++++++++++++--------- agent/php_newrelic.h | 5 +- agent/php_rinit.c | 4 + agent/php_rshutdown.c | 4 + agent/tests/test_fw_drupal.c | 9 -- 5 files changed, 200 insertions(+), 72 deletions(-) diff --git a/agent/fw_drupal.c b/agent/fw_drupal.c index 83daef226..87dae8741 100644 --- a/agent/fw_drupal.c +++ b/agent/fw_drupal.c @@ -67,6 +67,90 @@ NR_PHP_WRAPPER(nr_drupal_qdrupal_name_the_wt) { } NR_PHP_WRAPPER_END +#if ZEND_MODULE_API_NO >= ZEND_7_3_X_API_NO +static void nr_drupal_http_request_ensure_second_arg(NR_EXECUTE_PROTO) { + zval* arg = NULL; + + /* + * If only one argument is given, an empty list is inserted as second + * argument. NR headers are added during a later step. + */ + if (ZEND_NUM_ARGS() == 1) { + arg = nr_php_zval_alloc(); + array_init(arg); + + nr_php_arg_add(NR_EXECUTE_ORIG_ARGS, arg); + + nr_php_zval_free(&arg); + } +} + +static zval* nr_drupal_http_request_add_headers(NR_EXECUTE_PROTO TSRMLS_DC) { + bool is_drupal_7; + zval* second_arg = NULL; + zend_execute_data* ex = nr_get_zend_execute_data(NR_EXECUTE_ORIG_ARGS); + + if (nrunlikely(NULL == ex)) { + return NULL; + } + + /* Ensure second argument exists in the call frame */ + nr_drupal_http_request_ensure_second_arg(NR_EXECUTE_ORIG_ARGS); + + is_drupal_7 = (ex->func->common.num_args == 2); + + /* + * nr_php_get_user_func_arg is used, as nr_php_arg_get calls ZVAL_DUP + * on the argument zval and thus doesn't allow us to change the + * original argument. + */ + second_arg = nr_php_get_user_func_arg(2, NR_EXECUTE_ORIG_ARGS); + + /* + * Add NR headers. + */ + nr_drupal_headers_add(second_arg, is_drupal_7 TSRMLS_CC); + + return second_arg; +} +#endif + +static char* nr_drupal_http_request_get_method(NR_EXECUTE_PROTO TSRMLS_DC) { + zval* arg2 = NULL; + zval* arg3 = NULL; + zval* method = NULL; + char* http_request_method = NULL; + + /* + * Drupal 6 will have a third argument with the method, Drupal 7 will not + * have a third argument it must be parsed from the second. + */ + arg3 = nr_php_arg_get(3, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + // There is no third arg, this is drupal 7 + if (NULL == arg3) { + arg2 = nr_php_arg_get(2, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + if (NULL != arg2) { + method = nr_php_zend_hash_find(Z_ARRVAL_P(arg2), "method"); + if (nr_php_is_zval_valid_string(method)) { + http_request_method + = nr_strndup(Z_STRVAL_P(method), Z_STRLEN_P(method)); + } + } + } else if (nr_php_is_zval_valid_string(arg3)) { + // This is drupal 6, the method is the third arg. + http_request_method = nr_strndup(Z_STRVAL_P(arg3), Z_STRLEN_P(arg3)); + } + // If the method is not set, Drupal will default to GET + if (NULL == http_request_method) { + http_request_method = nr_strdup("GET"); + } + + nr_php_arg_release(&arg2); + nr_php_arg_release(&arg3); + + return http_request_method; +} + static uint64_t nr_drupal_http_request_get_response_code( zval** return_value TSRMLS_DC) { zval* code = NULL; @@ -129,23 +213,11 @@ static char* nr_drupal_http_request_get_response_header( return NULL; } -/* - * Drupal 6: - * drupal_http_request ($url, $headers = array(), $method = 'GET', - * $data = NULL, $retry = 3, $timeout = 30.0) - * - * Drupal 7: - * drupal_http_request ($url, array $options = array()) - * - */ -NR_PHP_WRAPPER(nr_drupal_http_request_exec) { - zval* arg1 = NULL; - zval* arg2 = NULL; - zval* arg3 = NULL; - zval* method = NULL; - zval** return_value = NULL; +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA -#if ZEND_MODULE_API_NO >= ZEND_7_3_X_API_NO +NR_PHP_WRAPPER(nr_drupal_http_request_before) { + (void)wraprec; /* * For PHP 7.3 and newer, New Relic headers are added here. * For older versions, New Relic headers are added via the proxy function @@ -155,40 +227,109 @@ NR_PHP_WRAPPER(nr_drupal_http_request_exec) { * (nr_php_swap_user_functions), which breaks as since PHP 7.3 user * functions are stored in shared memory. */ - bool is_drupal_7; - zval* arg = NULL; - zend_execute_data* ex = nr_get_zend_execute_data(NR_EXECUTE_ORIG_ARGS); + nr_drupal_http_request_add_headers(NR_EXECUTE_ORIG_ARGS); - if (NULL == ex) { - NR_PHP_WRAPPER_LEAVE - } + NR_PHP_WRAPPER_REQUIRE_FRAMEWORK(NR_FW_DRUPAL); - is_drupal_7 = (ex->func->common.num_args == 2); + NRPRG(drupal_http_request_depth) += 1; /* - * If only one argument is given, an empty list is inserted as second - * argument. NR headers are added during a later step. + * We only want to create a metric here if this isn't a recursive call to + * drupal_http_request() caused by the original call returning a redirect. + * We can check how many drupal_http_request() calls are on the stack by + * checking a counter. */ - if (ZEND_NUM_ARGS() == 1) { - arg = nr_php_zval_alloc(); - array_init(arg); + if (1 == NRPRG(drupal_http_request_depth)) { + NRPRG(drupal_http_request_segment) + = nr_segment_start(NRPRG(txn), NULL, NULL); + } +} +NR_PHP_WRAPPER_END - nr_php_arg_add(NR_EXECUTE_ORIG_ARGS, arg); +NR_PHP_WRAPPER(nr_drupal_http_request_after) { + zval* arg1 = NULL; - nr_php_zval_free(&arg); + (void)wraprec; + + NR_PHP_WRAPPER_REQUIRE_FRAMEWORK(NR_FW_DRUPAL); + + /* + * Grab the URL for the external metric, which is the first parameter in all + * versions of Drupal. + */ + arg1 = nr_php_arg_get(1, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + if (0 == nr_php_is_zval_non_empty_string(arg1)) { + goto end; } /* - * nr_php_get_user_func_arg is used, as nr_php_arg_get calls ZVAL_DUP - * on the argument zval and thus doesn't allow us to change the - * original argument. + * We only want to create a metric here if this isn't a recursive call to + * drupal_http_request() caused by the original call returning a redirect. + * We can check how many drupal_http_request() calls are on the stack by + * checking a counter. */ - arg = nr_php_get_user_func_arg(2, NR_EXECUTE_ORIG_ARGS); + if (1 == NRPRG(drupal_http_request_depth)) { + nr_segment_external_params_t external_params + = {.library = "Drupal", + .uri = nr_strndup(Z_STRVAL_P(arg1), Z_STRLEN_P(arg1))}; + + external_params.procedure + = nr_drupal_http_request_get_method(NR_EXECUTE_ORIG_ARGS); + + external_params.encoded_response_header + = nr_drupal_http_request_get_response_header(&func_return_value); + + external_params.status + = nr_drupal_http_request_get_response_code(&func_return_value); + if (NRPRG(txn) && NRTXN(special_flags.debug_cat)) { + nrl_verbosedebug( + NRL_CAT, "CAT: outbound response: transport='Drupal 6-7' %s=" NRP_FMT, + X_NEWRELIC_APP_DATA, + NRP_CAT(external_params.encoded_response_header)); + } + + nr_segment_external_end(&NRPRG(drupal_http_request_segment), + &external_params); + NRPRG(drupal_http_request_segment) = NULL; + + nr_free(external_params.encoded_response_header); + nr_free(external_params.procedure); + nr_free(external_params.uri); + } + +end: + nr_php_arg_release(&arg1); + NRPRG(drupal_http_request_depth) -= 1; +} +NR_PHP_WRAPPER_END + +#else +/* + * Drupal 6: + * drupal_http_request ($url, $headers = array(), $method = 'GET', + * $data = NULL, $retry = 3, $timeout = 30.0) + * + * Drupal 7: + * drupal_http_request ($url, array $options = array()) + * + */ +NR_PHP_WRAPPER(nr_drupal_http_request_exec) { + zval* arg1 = NULL; + zval** return_value = NULL; + +#if ZEND_MODULE_API_NO >= ZEND_7_3_X_API_NO /* - * Add NR headers. + * For PHP 7.3 and newer, New Relic headers are added here. + * For older versions, New Relic headers are added via the proxy function + * nr_drupal_replace_http_request. + * + * Reason: using the proxy function involves swizzling + * (nr_php_swap_user_functions), which breaks as since PHP 7.3 user + * functions are stored in shared memory. */ - nr_drupal_headers_add(arg, is_drupal_7 TSRMLS_CC); + zval* arg + = nr_drupal_http_request_add_headers(NR_EXECUTE_ORIG_ARGS TSRMLS_CC); /* * If an invalid argument was given for the second argument ($headers @@ -230,30 +371,8 @@ NR_PHP_WRAPPER(nr_drupal_http_request_exec) { = {.library = "Drupal", .uri = nr_strndup(Z_STRVAL_P(arg1), Z_STRLEN_P(arg1))}; - /* - * Drupal 6 will have a third argument with the method, Drupal 7 will not - * have a third argument it must be parsed from the second. - */ - arg3 = nr_php_arg_get(3, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); - // There is no third arg, this is drupal 7 - if (0 == arg3) { - arg2 = nr_php_arg_get(2, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); - if (NULL != arg2) { - method = nr_php_zend_hash_find(Z_ARRVAL_P(arg2), "method"); - if (nr_php_is_zval_valid_string(method)) { - external_params.procedure - = nr_strndup(Z_STRVAL_P(method), Z_STRLEN_P(method)); - } - } - } else if (nr_php_is_zval_valid_string(arg3)) { - // This is drupal 6, the method is the third arg. - external_params.procedure - = nr_strndup(Z_STRVAL_P(arg3), Z_STRLEN_P(arg3)); - } - // If the method is not set, Drupal will default to GET - if (NULL == external_params.procedure) { - external_params.procedure = nr_strdup("GET"); - } + external_params.procedure + = nr_drupal_http_request_get_method(NR_EXECUTE_ORIG_ARGS TSRMLS_CC); segment = nr_segment_start(NRPRG(txn), NULL, NULL); @@ -287,12 +406,12 @@ NR_PHP_WRAPPER(nr_drupal_http_request_exec) { end: nr_php_arg_release(&arg1); - nr_php_arg_release(&arg2); - nr_php_arg_release(&arg3); NRPRG(drupal_http_request_depth) -= 1; } NR_PHP_WRAPPER_END +#endif + static void nr_drupal_name_the_wt(const zend_function* func TSRMLS_DC) { char* action = NULL; @@ -632,8 +751,15 @@ void nr_drupal_enable(TSRMLS_D) { nr_drupal_name_wt_as_cached_page TSRMLS_CC); nr_php_wrap_user_function(NR_PSTR("drupal_cron_run"), nr_drupal_cron_run TSRMLS_CC); +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + nr_php_wrap_user_function_before_after( + NR_PSTR("drupal_http_request"), nr_drupal_http_request_before, + nr_drupal_http_request_after TSRMLS_CC); +#else nr_php_wrap_user_function(NR_PSTR("drupal_http_request"), nr_drupal_http_request_exec TSRMLS_CC); +#endif /* * The drupal_modules config setting controls instrumentation of modules, diff --git a/agent/php_newrelic.h b/agent/php_newrelic.h index 50ee23442..dfe3d3863 100644 --- a/agent/php_newrelic.h +++ b/agent/php_newrelic.h @@ -409,7 +409,10 @@ size_t drupal_module_invoke_all_hook_len; /* The length of the current Drupal hook */ size_t drupal_http_request_depth; /* The current depth of drupal_http_request() calls */ - +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA +nr_segment_t* drupal_http_request_segment; +#endif int symfony1_in_dispatch; /* Whether we are currently within a sfFrontWebController::dispatch() frame */ int symfony1_in_error404; /* Whether we are currently within a diff --git a/agent/php_rinit.c b/agent/php_rinit.c index e42465f5a..4c3c96e7b 100644 --- a/agent/php_rinit.c +++ b/agent/php_rinit.c @@ -37,6 +37,10 @@ PHP_RINIT_FUNCTION(newrelic) { NRPRG(sapi_headers) = NULL; NRPRG(pid) = getpid(); NRPRG(user_function_wrappers) = nr_vector_create(64, NULL, NULL); +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + NRPRG(drupal_http_request_segment) = NULL; +#endif if ((0 == NR_PHP_PROCESS_GLOBALS(enabled)) || (0 == NRINI(enabled))) { return SUCCESS; diff --git a/agent/php_rshutdown.c b/agent/php_rshutdown.c index 439791374..cab121f7e 100644 --- a/agent/php_rshutdown.c +++ b/agent/php_rshutdown.c @@ -119,6 +119,10 @@ int nr_php_post_deactivate(void) { NRPRG(current_framework) = NR_FW_UNSET; NRPRG(framework_version) = 0; +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + NRPRG(drupal_http_request_segment) = NULL; +#endif nrl_verbosedebug(NRL_INIT, "post-deactivate processing done"); return SUCCESS; diff --git a/agent/tests/test_fw_drupal.c b/agent/tests/test_fw_drupal.c index d0ca5566d..f2f383110 100644 --- a/agent/tests/test_fw_drupal.c +++ b/agent/tests/test_fw_drupal.c @@ -370,15 +370,6 @@ static void test_drupal_http_request_drupal_6(TSRMLS_D) { } void test_main(void* p NRUNUSED) { -/* -DO NOT LEAVE THIS. -When drupal_http_request header functionality is refactored, -please put the tests back in! -*/ -#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ - && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ - return; -#endif #if defined(ZTS) && !defined(PHP7) void*** tsrm_ls = NULL; From 48932777baf7758c4ee07a82a373c07306998974 Mon Sep 17 00:00:00 2001 From: Amber Sistla Date: Thu, 19 Jan 2023 21:36:02 -0700 Subject: [PATCH 13/56] feat(agent): OAPI exception handling (#580) 1. Renamed nr_php_execute_metadata_add_code_level_metrics to nr_php_observer_metadata_init as this will be the way we populate the metadata. We don't need to do duplicate effort with `nr_php_execute_metadata_init` anymore. 2. Since nr_php_execute_metadata_init is now for populating metadata, moved all CLM checks out nr_php_observer_metadata_init and into nr_php_txn_add_code_level_metrics where it is more appropriate. 3. Modified stacked segments to have a pointer to the metadata so if they are closed off by an exception, they will properly propogate the information. Added initializations/deninitializations. 4. For php_execute_enabled, only call nr_php_observer_metadata_init if we need to. 5. Added nr_php_observer_exception_segment_end To handled closing off observer segments if an exception occurs. 6. Now use zend_throw_exception_hook (more info: https://www.phpinternalsbook.com/php7/extensions_design/hooks.html Note: This ONLY notifies when an exception is thrown. It gives no indication if that exception was subsequently caught or not. 8. OAPI leaves dangling segments in the case of exceptions. These need to be cleaned up for functions that rely on the current segment (includes begin/end functions, stacked segment unwinding, and API calls) 9. New inline function re`nr_php_api_ensure_current_segment` to account for dangling segments when calling our API. 10. TXN globals to keep track of the exception 11. Change in php_txn turn off recording after unwind the segments to give timer to attach exception to dangling segment(s). 12. Modify stacked segments init/denit to handle additional segment metadata variable. 13. Functions to clear/set TXN uncaught_exception variables. 14. Update metadata struct to retain more context of the segment. 15. Removed legacy exception code that wasn't getting called anymore. 16. Added tests. 17. Added a `clean`callback to the wraprec functionality and associated unit tests. With OAPI exceptions, the registered function handler (nr_php_observer_fcall_end) doesn't get called when an uncaught exception occurs, and therefore doesn't decrement the stack_depth counter. All OAPI unhandled exception cleanup filters through: nr_php_observer_segment_end so we decrement php_cur_stack_depth there when we cleanup orphaned segments. Additionally, since nr_php_observer_segment_end is an exit path, also call nr_php_show_oapi_metadata New function nr_php_show_oapi_metadata called via the segment exception handling exit path (to correspond to nr_php_show_exec_return) to show the all the available function details when the special_flags.show_execute_* is toggled on. This will help when debugging. Added additional test cases to ensure proper php_cur_stack_depth counting --- agent/fw_drupal.c | 14 +- agent/php_agent.c | 67 -- agent/php_agent.h | 114 +-- agent/php_api.c | 9 + agent/php_api.h | 22 + agent/php_api_internal.c | 2 +- agent/php_call.c | 14 +- agent/php_error.c | 25 +- agent/php_execute.c | 744 ++++++++++++------ agent/php_execute.h | 77 +- agent/php_minit.c | 6 +- agent/php_newrelic.h | 10 + agent/php_observer.c | 36 +- agent/php_observer.h | 26 + agent/php_rinit.c | 1 + agent/php_rshutdown.c | 11 +- agent/php_stacked_segment.c | 48 +- agent/php_stacked_segment.h | 404 +++++++++- agent/php_txn.c | 60 +- agent/php_user_instrument.c | 38 +- agent/php_user_instrument.h | 24 +- agent/php_wrapper.c | 23 +- agent/php_wrapper.h | 17 +- agent/scripts/newrelic.ini.template | 11 + agent/tests/test_agent.c | 145 +--- agent/tests/test_php_execute.c | 232 +++++- agent/tests/test_php_stacked_segment.c | 11 + agent/tests/test_php_wrapper.c | 393 ++++++++- axiom/nr_segment.h | 23 +- axiom/nr_txn.c | 44 +- axiom/nr_txn.h | 13 +- ...stom_parameter_nested_caught_exception.php | 198 +++++ ...test_add_custom_parameter_nested_happy.php | 192 +++++ ...om_parameter_nested_uncaught_exception.php | 225 ++++++ ...vent_parameter_nested_caught_exception.php | 198 +++++ ...test_span_event_parameter_nested_happy.php | 192 +++++ ...nt_parameter_nested_uncaught_exception.php | 221 ++++++ ...create_payload_nested_caught_exception.php | 158 ++++ .../test_create_payload_nested_happy.php | 130 +++ ...eate_payload_nested_uncaught_exception.php | 163 ++++ ...t_notice_error_nested_caught_exception.php | 214 +++++ .../test_notice_error_nested_happy.php | 208 +++++ ...notice_error_nested_uncaught_exception.php | 212 +++++ .../api/other/test_end_transaction_nested.php | 7 + .../test_end_transaction_nested.php8.php | 106 +++ ...ser_attributes_nested_caught_exception.php | 190 +++++ .../test_set_user_attributes_nested_happy.php | 184 +++++ ...r_attributes_nested_uncaught_exception.php | 217 +++++ .../test_transaction_closure_clm.php | 166 ++++ .../test_transaction_function_len_clm.php | 12 +- .../test_transaction_namespace2_clm.php | 14 +- .../test_transaction_namespace_clm.php | 14 +- .../test_transaction_namespace_len_clm.php | 14 +- ..._transaction_nested_user_functions_clm.php | 25 +- .../test_transaction_non_web_clm.php | 25 +- .../attributes/test_transaction_web_clm.php | 18 +- .../monolog3/test_monolog_basic_clm.php | 159 ++++ 57 files changed, 5280 insertions(+), 846 deletions(-) create mode 100644 tests/integration/api/add_custom_parameter/test_add_custom_parameter_nested_caught_exception.php create mode 100644 tests/integration/api/add_custom_parameter/test_add_custom_parameter_nested_happy.php create mode 100644 tests/integration/api/add_custom_parameter/test_add_custom_parameter_nested_uncaught_exception.php create mode 100644 tests/integration/api/add_custom_span_parameter/test_span_event_parameter_nested_caught_exception.php create mode 100644 tests/integration/api/add_custom_span_parameter/test_span_event_parameter_nested_happy.php create mode 100644 tests/integration/api/add_custom_span_parameter/test_span_event_parameter_nested_uncaught_exception.php create mode 100644 tests/integration/api/distributed_trace/newrelic/test_create_payload_nested_caught_exception.php create mode 100644 tests/integration/api/distributed_trace/newrelic/test_create_payload_nested_happy.php create mode 100644 tests/integration/api/distributed_trace/newrelic/test_create_payload_nested_uncaught_exception.php create mode 100644 tests/integration/api/notice_error/test_notice_error_nested_caught_exception.php create mode 100644 tests/integration/api/notice_error/test_notice_error_nested_happy.php create mode 100644 tests/integration/api/notice_error/test_notice_error_nested_uncaught_exception.php create mode 100644 tests/integration/api/other/test_end_transaction_nested.php8.php create mode 100644 tests/integration/api/other/test_set_user_attributes_nested_caught_exception.php create mode 100644 tests/integration/api/other/test_set_user_attributes_nested_happy.php create mode 100644 tests/integration/api/other/test_set_user_attributes_nested_uncaught_exception.php create mode 100644 tests/integration/attributes/test_transaction_closure_clm.php create mode 100644 tests/integration/logging/monolog3/test_monolog_basic_clm.php diff --git a/agent/fw_drupal.c b/agent/fw_drupal.c index 87dae8741..3563639a8 100644 --- a/agent/fw_drupal.c +++ b/agent/fw_drupal.c @@ -303,6 +303,16 @@ NR_PHP_WRAPPER(nr_drupal_http_request_after) { } NR_PHP_WRAPPER_END +NR_PHP_WRAPPER(nr_drupal_http_request_clean) { + NR_UNUSED_SPECIALFN; + NR_UNUSED_FUNC_RETURN_VALUE; + (void)wraprec; + + NR_PHP_WRAPPER_REQUIRE_FRAMEWORK(NR_FW_DRUPAL); + + NRPRG(drupal_http_request_depth) -= 1; +} +NR_PHP_WRAPPER_END #else /* @@ -753,9 +763,9 @@ void nr_drupal_enable(TSRMLS_D) { nr_drupal_cron_run TSRMLS_CC); #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA - nr_php_wrap_user_function_before_after( + nr_php_wrap_user_function_before_after_clean( NR_PSTR("drupal_http_request"), nr_drupal_http_request_before, - nr_drupal_http_request_after TSRMLS_CC); + nr_drupal_http_request_after, nr_drupal_http_request_clean); #else nr_php_wrap_user_function(NR_PSTR("drupal_http_request"), nr_drupal_http_request_exec TSRMLS_CC); diff --git a/agent/php_agent.c b/agent/php_agent.c index 322049db9..2502000c7 100644 --- a/agent/php_agent.c +++ b/agent/php_agent.c @@ -1156,70 +1156,3 @@ bool nr_php_function_is_static_method(const zend_function* func) { return (func->common.fn_flags & ZEND_ACC_STATIC); } - -#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ - -const char* nr_php_zend_execute_data_function_name( - const zend_execute_data* execute_data) { - zend_string* function_name = NULL; - - if ((NULL == execute_data) || (NULL == execute_data->func)) { - return NULL; - } - - function_name = execute_data->func->common.function_name; - - if ((NULL == function_name) - && (ZEND_USER_FUNCTION == execute_data->func->type)) { - /* - * This is the case of a filename being called so there is no function name. - * It is broken out separately here in case we need to do something special - * with it in the future. - */ - return NULL; - } - return function_name ? ZSTR_VAL(function_name) : NULL; -} - -const char* nr_php_zend_execute_data_filename( - const zend_execute_data* execute_data) { - zend_string* filename = NULL; - - while (NR_ZEND_USER_FUNC_EXISTS(execute_data)) { - execute_data = execute_data->prev_execute_data; - } - if (execute_data) { - filename = execute_data->func->op_array.filename; - } - return filename ? ZSTR_VAL(filename) : NULL; -} - -const char* nr_php_zend_execute_data_scope_name( - const zend_execute_data* execute_data) { - zend_class_entry* ce = NULL; - - while (execute_data) { - if (execute_data->func - && (ZEND_USER_CODE(execute_data->func->type) - || execute_data->func->common.scope)) { - ce = execute_data->func->common.scope; - execute_data = NULL; - } else { - execute_data = execute_data->prev_execute_data; - } - } - return ce ? ZSTR_VAL(ce->name) : NULL; -} - -uint32_t nr_php_zend_execute_data_lineno( - const zend_execute_data* execute_data) { - while (NR_ZEND_USER_FUNC_EXISTS(execute_data)) { - execute_data = execute_data->prev_execute_data; - } - if (execute_data) { - return execute_data->opline ? execute_data->opline->lineno : 0; - } - return 0; -} - -#endif /* PHP 7+ */ diff --git a/agent/php_agent.h b/agent/php_agent.h index 10c7599d0..84325a037 100644 --- a/agent/php_agent.h +++ b/agent/php_agent.h @@ -39,6 +39,7 @@ #include "php_compat.h" #include "php_newrelic.h" #include "php_zval.h" +#include "util_logging.h" #include "util_memory.h" #include "util_strings.h" #include "php_execute.h" @@ -765,6 +766,42 @@ nr_php_ini_entry_name_length(const zend_ini_entry* entry) { #define ZVAL_OR_ZEND_OBJECT(x) x #endif /* PHP8+ */ +/* + * Purpose : Ensure all dangling segments caused by an OAPI exception are closed + * before having an API act on the calling segment. + * + * Params : + * + * Returns : + */ +static inline void nr_php_api_ensure_current_segment() { +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + /* + * Before we call an API that depends on current segment, we need to ensure + * there isn't an outstanding uncaught exception that needs to be applied to + * dangling segments. If so, we need to apply the exception and close the + * stacked segments until we get to the segment that called the API. To do + * this, we detect the execute_data that called the API which is guaranteed to + * be what the current segment should be (otherwise, fcall_end would have + * closed normal segments and we would have taken care of any dangling + * segments already) and any segments stacked above it need to be closed due + * to an exception. + */ + + /* + * Get the function that called the API. prev_execute_data + * should never be null, but doublecheck for it anyway. + */ + if (NULL != EG(current_execute_data)->prev_execute_data) { + zval* prev_this = &EG(current_execute_data)->prev_execute_data->This; + + nr_php_observer_handle_uncaught_exception(prev_this); + } + +#endif +} + /* * Purpose : Wrap the native PHP json_decode function for those times when we * need a more robust JSON decoder than nro_create_from_json. @@ -828,84 +865,11 @@ extern bool nr_php_function_is_static_method(const zend_function* func); */ extern zend_execute_data* nr_get_zend_execute_data(NR_EXECUTE_PROTO TSRMLS_DC); -/* - * Purpose : If code level metrics are enabled, extract the data from the OAPI - * given zend_execute_data. Add the CLM as agent attributes to the - * attributes data structure. - * - * Params : 1. attributes data structure to add the CLM to - * 2. The zend_execute_data given by OAPI - * - * Returns : void - * - * Note: PHP has a concept of calling files with no function names. In the - * case of a file being called when there is no function name, the agent - * instruments the file. In this case, we provide the filename to CLM - * as the "function" name. - * Current CLM functionality only works with PHP 7+ - */ -extern void nr_php_txn_add_code_level_metrics( - nr_attributes_t* attributes, - const nr_php_execute_metadata_t* metadata); - #if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ -#define NR_ZEND_USER_FUNC_EXISTS(x) \ +#define NR_NOT_ZEND_USER_FUNC(x) \ (x && (!x->func || !ZEND_USER_CODE(x->func->type))) -/* - * Purpose : Return a pointer to the function name of zend_execute_data. - * - * Params : 1. zend_execute_data. - * - * Returns : A pointer to string, ownership of does NOT pass to the caller and - * string must be dupped if it needs to persist, - * or NULL if the zend_execute_data is invalid. - * - */ -extern const char* nr_php_zend_execute_data_function_name( - const zend_execute_data* execute_data); -/* - * Purpose : Return a pointer to the filename of zend_execute_data. - * - * Params : 1. zend_execute_data. - * - * Returns : A pointer to string, ownership of does NOT pass to the caller and - * string must be dupped if it needs to persist, - * or NULL if the zend_execute_data is invalid. - * - */ -extern const char* nr_php_zend_execute_data_filename( - const zend_execute_data* execute_data); - -/* - * Purpose : Return a pointer to the scope(i.e., class) name of - * zend_execute_data. - * - * Params : 1. zend_execute_data. - * - * Returns : A pointer to string, ownership of does NOT pass to the caller and - * string must be dupped if it needs to persist, - * or NULL if the zend_execute_data is invalid. - * - */ -extern const char* nr_php_zend_execute_data_scope_name( - const zend_execute_data* execute_data); - -/* - * Purpose : Return a uint32_t line number value of zend_execute_data. - * - * Params : 1. zend_execute_data. - * - * Returns : uint32_t lineno value - * - */ -extern uint32_t nr_php_zend_execute_data_lineno( - const zend_execute_data* execute_data); -#endif /* PHP 7+ */ - -#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ - /* * Purpose : Return a uint32_t (zend_uint) line number value of zend_function. * @@ -915,7 +879,7 @@ extern uint32_t nr_php_zend_execute_data_lineno( * */ static inline uint32_t nr_php_zend_function_lineno(const zend_function* func) { - if (NULL != func) { + if (NULL != func && ZEND_USER_FUNCTION == func->op_array.type) { return func->op_array.line_start; } return 0; diff --git a/agent/php_api.c b/agent/php_api.c index 91d308116..6d11fd56b 100644 --- a/agent/php_api.c +++ b/agent/php_api.c @@ -97,6 +97,8 @@ PHP_FUNCTION(newrelic_notice_error) { priority = nr_php_error_get_priority(E_ERROR); } + nr_php_api_ensure_current_segment(); + if (NR_SUCCESS != nr_txn_record_error_worthy(NRPRG(txn), priority)) { nrl_debug(NRL_API, "newrelic_notice_error: a higher severity error has already been " @@ -304,6 +306,8 @@ PHP_FUNCTION(newrelic_end_transaction) { } } + nr_php_api_ensure_current_segment(); + ret = nr_php_txn_end((0 != ignore), 0 TSRMLS_CC); if (NR_SUCCESS == ret) { nrl_debug(NRL_API, "transaction completed by API"); @@ -740,6 +744,7 @@ PHP_FUNCTION(newrelic_add_custom_parameter) { obj = nr_php_api_zval_to_attribute_obj(zzvalue TSRMLS_CC); if (obj) { + nr_php_api_ensure_current_segment(); rv = nr_txn_add_user_custom_parameter(NRPRG(txn), key, obj); } @@ -1233,6 +1238,7 @@ PHP_FUNCTION(newrelic_set_user_attributes) { RETURN_FALSE; } + nr_php_api_ensure_current_segment(); rv = nr_php_api_add_custom_parameter_string(NRPRG(txn), "user", userstr, userlen); if (NR_FAILURE == rv) { @@ -1260,6 +1266,7 @@ static nr_status_t nr_php_api_add_custom_span_attribute(const char* keystr, char* key = NULL; nr_segment_t* current; + nr_php_api_ensure_current_segment(); current = nr_txn_get_current_segment(NRPRG(txn), NULL); if (!current) { return NR_FAILURE; @@ -1523,6 +1530,7 @@ PHP_FUNCTION(newrelic_get_linking_metadata) { } if (nrlikely(NRPRG(txn))) { + nr_php_api_ensure_current_segment(); trace_id = nr_txn_get_current_trace_id(NRPRG(txn)); span_id = nr_txn_get_current_span_id(NRPRG(txn)); @@ -1565,6 +1573,7 @@ PHP_FUNCTION(newrelic_get_trace_metadata) { } if (nrlikely(NRPRG(txn))) { + nr_php_api_ensure_current_segment(); trace_id = nr_txn_get_current_trace_id(NRPRG(txn)); if (trace_id) { nr_php_add_assoc_string(return_value, "trace_id", trace_id); diff --git a/agent/php_api.h b/agent/php_api.h index 358b8934e..adb09729d 100644 --- a/agent/php_api.h +++ b/agent/php_api.h @@ -7,6 +7,28 @@ #ifndef PHP_API_HDR #define PHP_API_HDR +/* + * Recommendations for API calls when using OAPI instrumentation and PHP 8+ + * + * Dangling segments: + * With the use of Observer API we have the possibility of dangling segments + * that can occur due to an exception occurring. In the normal course of + * events, nr_php_observer_fcall_begin starts segments and + * nr_php_observer_fcall_end keeps/discards/ends segments. However, in the case + * of an uncaught exception, nr_php_observer_fcall_end is never called and + * therefore, the logic to keep/discard/end the segment doesn't automatically + * get initiated which can lead to dangling stacked segments. + * + * However, certain agent API calls need to be associated with particular + * segments. + * + * To handle this , dangling exception cleanup is initiated by the following + * call: nr_php_api_ensure_current_segment(); + * + * ANY API call that depends on the current segment needs to use this function + * to ensure the API uses the correct segment. + */ + extern void nr_php_api_add_supportability_metric(const char* name TSRMLS_DC); extern void nr_php_api_error(const char* format, ...) NRPRINTFMT(1); diff --git a/agent/php_api_internal.c b/agent/php_api_internal.c index aaa2b4832..3ad4a70bf 100644 --- a/agent/php_api_internal.c +++ b/agent/php_api_internal.c @@ -49,7 +49,7 @@ PHP_FUNCTION(newrelic_get_request_metadata) { } array_init(return_value); - + nr_php_api_ensure_current_segment(); outbound_headers = nr_header_outbound_request_create( NRPRG(txn), nr_txn_get_current_segment(NRPRG(txn), NULL)); diff --git a/agent/php_call.c b/agent/php_call.c index 1bcd08a4e..fa8a7c813 100644 --- a/agent/php_call.c +++ b/agent/php_call.c @@ -52,10 +52,16 @@ zval* nr_php_call_user_func(zval* object_ptr, /* * With PHP8, `call_user_function_ex` was removed and `call_user_function` - * became the recommended function. + * became the recommended function. This does't return a FAILURE for + * exceptions and needs to be in a try/catch block in order to clean up + * properly. */ - zend_result = call_user_function(EG(function_table), object_ptr, fname, - retval, param_count, param_values); + zend_try { + zend_result = call_user_function(EG(function_table), object_ptr, fname, + retval, param_count, param_values); + } + zend_catch { zend_result = FAILURE; } + zend_end_try(); #else zend_result = call_user_function_ex(EG(function_table), object_ptr, fname, @@ -65,11 +71,9 @@ zval* nr_php_call_user_func(zval* object_ptr, nr_php_zval_free(&fname); nr_free(param_values); - if (SUCCESS == zend_result) { return retval; } - nr_php_zval_free(&retval); return NULL; #else /* PHP < 7 */ diff --git a/agent/php_error.c b/agent/php_error.c index 0de933b3b..1142d85a4 100644 --- a/agent/php_error.c +++ b/agent/php_error.c @@ -172,10 +172,10 @@ PHP_FUNCTION(newrelic_exception_handler) { * _including_ API noticed errors (in case the user uses newrelic_notice_error * as their error handler with prioritize_api_errors enabled). */ + nr_php_error_record_exception( NRPRG(txn), exception, NR_PHP_ERROR_PRIORITY_UNCAUGHT_EXCEPTION, "Uncaught exception ", &NRPRG(exception_filters) TSRMLS_CC); - /* * Finally, we need to generate an E_ERROR to match what PHP would have done * if this handler wasn't installed. Happily, PHP exposes an API function @@ -452,17 +452,15 @@ static int nr_php_should_record_error(int type, const char* format TSRMLS_DC) { /* Prior to PHP8 these error_filename and error_lineno were only used to pass * on to the error handler that the agent overwrote. With PHP8+, these values * are currently unused since the agent is already recording the stack trace. - * HOWEVER, when code level metrics(CLM) are incorporated, these values can be - * used to add lineno and filename to error traces. */ void nr_php_error_cb(int type, - zend_string* error_filename NRUNUSED, - uint error_lineno NRUNUSED, + zend_string* error_filename, + uint error_lineno, zend_string* message) { #elif ZEND_MODULE_API_NO == ZEND_8_0_X_API_NO void nr_php_error_cb(int type, - const char* error_filename NRUNUSED, - uint error_lineno NRUNUSED, + const char* error_filename, + uint error_lineno, zend_string* message) { #else void nr_php_error_cb(int type, @@ -471,6 +469,11 @@ void nr_php_error_cb(int type, const char* format, va_list args) { #endif /* PHP >= 8.1 */ +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ + (void)error_filename; + (void)error_lineno; +#endif TSRMLS_FETCH(); char* stack_json = NULL; const char* errclass = NULL; @@ -506,12 +509,14 @@ void nr_php_error_cb(int type, nr_free(msg); nr_free(stack_json); } - /* * Call through to the actual error handler for PHP 7.4 and below. * For PHP 8+ we have registered our error handler with the Observer * API so there is no need to callback to the original. */ + /* + * Call through to the actual error handler. + */ #if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO if (0 != NR_PHP_PROCESS_GLOBALS(orig_error_cb)) { NR_PHP_PROCESS_GLOBALS(orig_error_cb) @@ -607,10 +612,10 @@ nr_status_t nr_php_error_record_exception_segment(nrtxn_t* txn, char* message = NULL; char* prefix = "Uncaught exception "; long line = 0; - zend_class_entry* ce; + zend_class_entry* ce = NULL; zval* zend_err_message = NULL; zval* zend_err_file = NULL; - zval* zend_err_line; + zval* zend_err_line = NULL; if ((NULL == txn) || (0 == nr_php_error_zval_is_exception(exception TSRMLS_CC))) { diff --git a/agent/php_execute.c b/agent/php_execute.c index 58c8d6073..e595913ae 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -96,6 +96,11 @@ static void nr_php_show_exec_return(NR_EXECUTE_PROTO TSRMLS_DC); static int nr_php_show_exec_indentation(TSRMLS_D); static void nr_php_show_exec(NR_EXECUTE_PROTO TSRMLS_DC); +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ +static void nr_php_show_oapi_metadata(nr_php_execute_metadata_t* metadata, + bool wraprec_exists); +#endif /* * Purpose: Enable monitoring on specific functions in the framework. @@ -150,7 +155,7 @@ static int nr_format_zval_for_debug(zval* arg, break; } -#ifdef PHP7 +#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ if (NULL == Z_STR_P(arg)) { safe_append("invalid string", 14); break; @@ -194,7 +199,7 @@ static int nr_format_zval_for_debug(zval* arg, safe_append(tmp, len); break; -#ifdef PHP7 +#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ case IS_TRUE: safe_append("true", 4); break; @@ -222,7 +227,7 @@ static int nr_format_zval_for_debug(zval* arg, break; case IS_OBJECT: -#ifdef PHP7 +#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ if (NULL == Z_OBJ_P(arg)) { safe_append("invalid object", 14); break; @@ -688,6 +693,27 @@ static void nr_php_show_exec(NR_EXECUTE_PROTO TSRMLS_DC) { } } +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ +/* + * Show the metadata values associated with a dangling segment. + * This is called only with OAPI/PHP8+ when an exception leaves a stacked + * segment dangling due to nr_php_observer_fcall_end not getting called when an + * unhandled exception occurs. + */ +static void nr_php_show_oapi_metadata(nr_php_execute_metadata_t* metadata, + bool wraprec_exists) { + char* function_name = metadata->function ? ZSTR_VAL(metadata->function) : "?"; + char* class_name = metadata->scope ? ZSTR_VAL(metadata->scope) : "?"; + char* file_name = metadata->filepath ? ZSTR_VAL(metadata->filepath) : "?"; + char* wraprec_indicator = wraprec_exists ? "exists" : ""; + nrl_verbosedebug(NRL_AGENT, + "oapi metadata: scope={%s} function={%s} filename={%s} " + "lineno={%d} wraprec={%s}", + class_name, function_name, file_name, + metadata->function_lineno, wraprec_indicator); +} +#endif /* * Show the return value, assuming that there is one. * The return value is an attribute[sic] of the caller site, @@ -968,52 +994,41 @@ static void nr_php_execute_file(const zend_op_array* op_array, nr_php_add_user_instrumentation(TSRMLS_C); } +#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ /* - * Purpose : Add Code Level Metrics (CLM) to a metadata structure from - * zend_execute_data. + * Purpose : If code level metrics are enabled, use the metadata to create agent + * attributes in the segment with code level metrics. * - * Params : 1. A pointer to a metadata structure. - * 2. The zend_execute_data + * Params : 1. segment to create and add agent attributes to + * 2. metadata that will populate the CLM attributes * - * Note : It is the responsibility of the caller to allocate the metadata - * structure. In general, it's expected that this will be a pointer - * to a stack variable. + * Returns : void + * + * Note: PHP has a concept of calling files with no function names. In the + * case of a file being called when there is no function name, the agent + * instruments the file. In this case, we provide the filename to CLM + * as the "function" name. + * Current CLM functionality only works with PHP 7+ */ -static void nr_php_execute_metadata_add_code_level_metrics( - nr_php_execute_metadata_t* metadata, - NR_EXECUTE_PROTO) { - NR_UNUSED_FUNC_RETURN_VALUE; - -#if ZEND_MODULE_API_NO < ZEND_7_0_X_API_NO /* PHP7+ */ - (void)metadata; - NR_UNUSED_SPECIALFN; - return; -#else - const char* filepath = NULL; - const char* namespace = NULL; - const char* function = NULL; - uint32_t lineno = 1; - +static inline void nr_php_execute_segment_add_code_level_metrics( + nr_segment_t* segment, + const nr_php_execute_metadata_t* metadata) { /* * Check if code level metrics are enabled in the ini. - * If they aren't, exit and don't update CLM. + * If they aren't, exit and don't add any attributes. */ if (!NRINI(code_level_metrics_enabled)) { return; } - if (nrunlikely(NULL == metadata)) { + if (NULL == metadata) { return; } - if (nrunlikely(NULL == execute_data)) { + if (NULL == segment) { return; } - metadata->function_name = NULL; - metadata->function_filepath = NULL; - metadata->function_namespace = NULL; - /* * At a minimum, at least one of the following attribute combinations MUST be * implemented in order for customers to be able to accurately identify their @@ -1023,73 +1038,103 @@ static void nr_php_execute_metadata_add_code_level_metrics( * * If we don't have the minimum requirements, exit and don't add any * attributes. + * + * Additionally, none of the needed attributes can exceed 255 characters. */ -#define CHK_CLM_STRLEN(s) \ - if (CLM_STRLEN_MAX < NRSAFELEN(nr_strlen(s))) { \ - s = NULL; \ +#define CLM_STRLEN_MAX (255) + +#define CHK_CLM_STRLEN(s, zstr_len) \ + if (CLM_STRLEN_MAX < zstr_len) { \ + s = NULL; \ } - filepath = nr_php_zend_execute_data_filename(execute_data); - CHK_CLM_STRLEN(filepath) + const char* namespace = NULL; + const char* function = NULL; + const char* filepath = NULL; + + if (NULL != metadata->scope) { + namespace = ZSTR_VAL(metadata->scope); + CHK_CLM_STRLEN(namespace, ZSTR_LEN(metadata->scope)); + } - namespace = nr_php_zend_execute_data_scope_name(execute_data); - CHK_CLM_STRLEN(namespace) + if (NULL != metadata->function) { + function = ZSTR_VAL(metadata->function); + CHK_CLM_STRLEN(function, ZSTR_LEN(metadata->function)); + } - function = nr_php_zend_execute_data_function_name(execute_data); - CHK_CLM_STRLEN(function) + if (NULL != metadata->filepath) { + filepath = ZSTR_VAL(metadata->filepath); + CHK_CLM_STRLEN(filepath, ZSTR_LEN(metadata->filepath)); + } #undef CHK_CLM_STRLEN - lineno = nr_php_zend_execute_data_lineno(execute_data); - - /* - * Check if we are getting CLM for a file. - */ - if (nrunlikely(OP_ARRAY_IS_A_FILE(NR_OP_ARRAY))) { + if (1 == metadata->function_lineno) { /* - * If instrumenting a file, the filename is the "function" and the - * lineno is 1 (i.e., start of the file). + * It's a file. For CLM purposes, the "function" name is the filepath. */ function = filepath; - lineno = 1; - } else { - /* - * We are getting CLM for a function. - */ - if (0 == metadata->function_lineno) { - lineno = nr_php_zend_execute_data_lineno(execute_data); - } else { - /* - * If the metadata was already set (in the case of OAPI where we need to - * get it preemptively, use that value for lineno instead. - */ - lineno = metadata->function_lineno; - } } -#define CHK_CLM_EMPTY(s) ((NULL == s || nr_strempty(s)) ? true : false) - - if (CHK_CLM_EMPTY(function)) { + if (nr_strempty(function)) { + /* + * Name isn't set so don't do anything + */ return; } - if (CHK_CLM_EMPTY(namespace) && CHK_CLM_EMPTY(filepath)) { + if (nr_strempty(namespace) && nr_strempty(filepath)) { /* * CLM MUST have either function+namespace or function+filepath. */ return; } -#undef CHK_CLM_EMPTY + /* + * Only go through the trouble of actually allocating agent attributes if we + * know we have valid values to turn into attributes. + */ - metadata->function_lineno = lineno; - metadata->function_name = nr_strdup(function); - metadata->function_namespace = nr_strdup(namespace); - metadata->function_filepath = nr_strdup(filepath); + if (NULL == segment->attributes) { + segment->attributes = nr_attributes_create(segment->txn->attribute_config); + } -#endif /* PHP7 */ + if (nrunlikely(NULL == segment->attributes)) { + return; + } + +#define CLM_ATTRIBUTE_DESTINATION \ + (NR_ATTRIBUTE_DESTINATION_TXN_TRACE | NR_ATTRIBUTE_DESTINATION_ERROR \ + | NR_ATTRIBUTE_DESTINATION_TXN_EVENT | NR_ATTRIBUTE_DESTINATION_SPAN) + + /* + * If the string is empty, CLM specs say don't add it. + * nr_attributes_agent_add_string is okay with an empty string attribute. + * Already checked function for strempty no need to check again, but will need + * to check filepath and namespace. + */ + + nr_attributes_agent_add_string(segment->attributes, CLM_ATTRIBUTE_DESTINATION, + "code.function", function); + + if (!nr_strempty(filepath)) { + nr_attributes_agent_add_string(segment->attributes, + CLM_ATTRIBUTE_DESTINATION, "code.filepath", + filepath); + } + + if (!nr_strempty(namespace)) { + nr_attributes_agent_add_string(segment->attributes, + CLM_ATTRIBUTE_DESTINATION, "code.namespace", + namespace); + } + + nr_attributes_agent_add_long(segment->attributes, CLM_ATTRIBUTE_DESTINATION, + "code.lineno", metadata->function_lineno); } +#endif + /* * Purpose : Initialise a metadata structure from an op array. * @@ -1102,7 +1147,7 @@ static void nr_php_execute_metadata_add_code_level_metrics( */ static void nr_php_execute_metadata_init(nr_php_execute_metadata_t* metadata, zend_op_array* op_array) { -#ifdef PHP7 +#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ if (op_array->scope && op_array->scope->name && op_array->scope->name->len) { metadata->scope = op_array->scope->name; zend_string_addref(metadata->scope); @@ -1116,6 +1161,20 @@ static void nr_php_execute_metadata_init(nr_php_execute_metadata_t* metadata, } else { metadata->function = NULL; } + if (!NRINI(code_level_metrics_enabled) + || ZEND_USER_FUNCTION != op_array->type) { + metadata->filepath = NULL; + return; + } + if (op_array->filename && op_array->filename->len) { + metadata->filepath = op_array->filename; + zend_string_addref(metadata->filepath); + } else { + metadata->filepath = NULL; + } + + metadata->function_lineno = op_array->line_start; + #else metadata->op_array = op_array; #endif /* PHP7 */ @@ -1152,13 +1211,19 @@ static void nr_php_execute_metadata_metric( } /* - * Purpose : Release any cached op array metadata. + * Purpose : Release any cached metadata. * * Params : 1. A pointer to the metadata. */ -static void nr_php_execute_metadata_release( +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ +void nr_php_execute_metadata_release(nr_php_execute_metadata_t* metadata) { +#else +static inline void nr_php_execute_metadata_release( nr_php_execute_metadata_t* metadata) { +#endif #if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO + if (NULL != metadata->scope) { zend_string_release(metadata->scope); metadata->scope = NULL; @@ -1168,9 +1233,12 @@ static void nr_php_execute_metadata_release( zend_string_release(metadata->function); metadata->function = NULL; } - nr_free(metadata->function_name); - nr_free(metadata->function_namespace); - nr_free(metadata->function_filepath); + + if (NULL != metadata->filepath) { + zend_string_release(metadata->filepath); + metadata->filepath = NULL; + } + #else metadata->op_array = NULL; #endif /* PHP7 */ @@ -1221,20 +1289,52 @@ static inline void nr_php_execute_segment_end( if (create_metric || (duration >= NR_PHP_PROCESS_GLOBALS(expensive_min)) || nr_vector_size(stacked->metrics) || stacked->id || stacked->attributes || stacked->error) { + /* + * Non-OAPI segments are able to utilize metadata that is declared in the + * call stack. OAPI doesn't have this luxury since we have to handle begin + * and end func calls separately. Because of this, metadata now resides as + * a pointer in the stacked segment. We must extract data from it BEFORE we + * move the stacked segment to the heap; otherwise, it gets deallocated + * before we can use it. + */ +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + + nr_php_execute_segment_add_metric(stacked, metadata, create_metric); + + /* + * Check if code level metrics are enabled in the ini. + * If they aren't, exit and don't create any metrics. + */ +#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP >= PHP7 */ + if (NRINI(code_level_metrics_enabled)) { + nr_php_execute_segment_add_code_level_metrics(stacked, metadata); + } +#endif + nr_segment_t* s = nr_php_stacked_segment_move_to_heap(stacked TSRMLS_CC); + +#else + nr_segment_t* s = nr_php_stacked_segment_move_to_heap(stacked TSRMLS_CC); nr_php_execute_segment_add_metric(s, metadata, create_metric); - if (NULL == s->attributes) { - s->attributes = nr_attributes_create(s->txn->attribute_config); + + /* + * Check if code level metrics are enabled in the ini. + * If they aren't, don't create any CLM. + */ +#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP >= PHP7 */ + if (NRINI(code_level_metrics_enabled)) { + nr_php_execute_segment_add_code_level_metrics(s, metadata); } - nr_php_txn_add_code_level_metrics(s->attributes, metadata); +#endif + +#endif nr_segment_end(&s); } else { nr_php_stacked_segment_deinit(stacked TSRMLS_CC); } } -#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ - || defined OVERWRITE_ZEND_EXECUTE_DATA /* * This is the user function execution hook. Hook the user-defined (PHP) * function execution. For speed, we have a pointer that we've installed in the @@ -1252,15 +1352,8 @@ static void nr_php_execute_enabled(NR_EXECUTE_PROTO TSRMLS_DC) { NRTXNGLOBAL(execute_count) += 1; - nr_php_execute_metadata_add_code_level_metrics(&metadata, - NR_EXECUTE_ORIG_ARGS); - if (nrunlikely(OP_ARRAY_IS_A_FILE(NR_OP_ARRAY))) { - if (NRPRG(txn)) { - nr_php_txn_add_code_level_metrics(NRPRG(txn)->attributes, &metadata); - } nr_php_execute_file(NR_OP_ARRAY, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); - nr_php_execute_metadata_release(&metadata); return; } @@ -1335,6 +1428,7 @@ static void nr_php_execute_enabled(NR_EXECUTE_PROTO TSRMLS_DC) { } nr_php_execute_segment_end(segment, &metadata, create_metric TSRMLS_CC); + nr_php_execute_metadata_release(&metadata); if (nrunlikely(zcaught)) { zend_bailout(); @@ -1359,7 +1453,7 @@ static void nr_php_execute_enabled(NR_EXECUTE_PROTO TSRMLS_DC) { zval* exception_zval = NULL; nr_status_t status; -#ifdef PHP7 +#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ /* * On PHP 7, EG(exception) is stored as a zend_object, and is only * wrapped in a zval when it actually needs to be. @@ -1396,6 +1490,7 @@ static void nr_php_execute_enabled(NR_EXECUTE_PROTO TSRMLS_DC) { } nr_php_execute_segment_end(segment, &metadata, false TSRMLS_CC); + nr_php_execute_metadata_release(&metadata); if (nrunlikely(zcaught)) { zend_bailout(); @@ -1407,12 +1502,8 @@ static void nr_php_execute_enabled(NR_EXECUTE_PROTO TSRMLS_DC) { NR_PHP_PROCESS_GLOBALS(orig_execute) (NR_EXECUTE_ORIG_ARGS_OVERWRITE TSRMLS_CC); } - nr_php_execute_metadata_release(&metadata); } -#endif -#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ - || defined OVERWRITE_ZEND_EXECUTE_DATA static void nr_php_execute_show(NR_EXECUTE_PROTO TSRMLS_DC) { if (nrunlikely(NR_PHP_PROCESS_GLOBALS(special_flags).show_executes)) { nr_php_show_exec(NR_EXECUTE_ORIG_ARGS TSRMLS_CC); @@ -1424,7 +1515,6 @@ static void nr_php_execute_show(NR_EXECUTE_PROTO TSRMLS_DC) { nr_php_show_exec_return(NR_EXECUTE_ORIG_ARGS TSRMLS_CC); } } -#endif static void nr_php_max_nesting_level_reached(TSRMLS_D) { /* @@ -1457,8 +1547,6 @@ static void nr_php_max_nesting_level_reached(TSRMLS_D) { (int)NRINI(max_nesting_level)); } -#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ - || defined OVERWRITE_ZEND_EXECUTE_DATA /* * This function is single entry, single exit, so that we can keep track * of the PHP stack depth. NOTE: the stack depth is not maintained in @@ -1482,6 +1570,10 @@ void nr_php_execute(NR_EXECUTE_PROTO_OVERWRITE TSRMLS_DC) { * zend_catch is called to avoid catastrophe on the way to a premature * exit, maintaining this counter perfectly is not a necessity. */ +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ + zval* func_return_value = NULL; +#endif NRPRG(php_cur_stack_depth) += 1; @@ -1508,7 +1600,6 @@ void nr_php_execute(NR_EXECUTE_PROTO_OVERWRITE TSRMLS_DC) { return; } -#endif static void nr_php_show_exec_internal(NR_EXECUTE_PROTO_OVERWRITE, const zend_function* func TSRMLS_DC) { @@ -1566,7 +1657,7 @@ void nr_php_execute_internal(zend_execute_data* execute_data, return; } -#ifdef PHP7 +#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ func = execute_data->func; #else func = execute_data->function_state.function; @@ -1600,7 +1691,6 @@ void nr_php_execute_internal(zend_execute_data* execute_data, nr_php_show_exec_internal((zend_op_array*)func, func TSRMLS_CC); #endif /* PHP >= 5.5 */ } - segment = nr_segment_start(NRPRG(txn), NULL, NULL); CALL_ORIGINAL; @@ -1688,12 +1778,148 @@ void nr_php_user_instrumentation_from_opcache(TSRMLS_D) { * Observer API functionality was added with PHP 8.0. * See nr_php_observer.h/c for more information. */ -#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO /* PHP8+ */ +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP8+ */ + +static inline void nr_php_observer_exception_segments_end( + zval* exception, + zval* execute_data_this) { + nr_segment_t* segment = NULL; + + if (NULL == exception || NULL == execute_data_this) { + return; + } + segment = NRTXN(force_current_segment); + while ((NULL != segment) + && (NRTXN(segment_root) != NRTXN(force_current_segment))) { + nr_php_execute_metadata_t* metadata = segment->metadata; + if (metadata->execute_data_this == execute_data_this) { + break; + } + nr_php_observer_segment_end(NRPRG(uncaught_exception)); + segment = NRTXN(force_current_segment); + } +} + +void nr_php_observer_segment_end(zval* exception) { + nr_segment_t* segment = NULL; + nruserfn_t* wraprec = NULL; + /* + * If we have a stacked segment that missed an OAPI func_end call, add an + * exception (if not null) and close then get the current segment and return + * if null. The segment would only have been created if we are recording and + * if wraprec is set or if tt is greater than 0. + */ + + if (NULL != exception) { + nr_status_t status; + + status = nr_php_error_record_exception_segment( + NRPRG(txn), exception, &NRPRG(exception_filters) TSRMLS_CC); + + if (NR_FAILURE == status) { + nrl_verbosedebug(NRL_AGENT, "%s: unable to record exception on segment", + __func__); + } + } + segment = NRTXN(force_current_segment); + if (NULL != segment) { + bool create_metric = false; + wraprec = (nruserfn_t*)(segment->wraprec); + if (NULL != wraprec) { + create_metric = wraprec->create_metric; + int zcaught + = nr_zend_call_oapi_special_clean(wraprec, segment, NULL, NULL); + if (nrunlikely(zcaught)) { + zend_bailout(); + } + } + /* + * We are only here because there is a dangling segment which means + * nr_php_observer_fcall_end didn't get called due to unhandled + * exception(s). Decrement the php_cur_stack_depth counter properly. + */ + if (nrunlikely( + NR_PHP_PROCESS_GLOBALS(special_flags).show_execute_returns)) { + nrl_verbosedebug(NRL_AGENT, + "Stack depth: %d before OAPI function exiting via %s", + NRPRG(php_cur_stack_depth), __func__); + nr_php_show_oapi_metadata(segment->metadata, (NULL != wraprec)); + } + NRPRG(php_cur_stack_depth) -= 1; + nr_php_execute_segment_end(segment, segment->metadata, create_metric); + } + return; +} + +void nr_php_observer_handle_uncaught_exception(zval* current_this) { + if (NULL == NRPRG(uncaught_exeption_execute_data_this)) { + return; + } + /* + * A pending uncaught exception for this txn exists, so we need to close + * stacked segments to get to the correct stacked segment to add the noticed + * error to. + */ + if (current_this != NRPRG(uncaught_exeption_execute_data_this)) { + nr_php_observer_exception_segments_end(NRPRG(uncaught_exception), + current_this); + + php_observer_clear_uncaught_exception_globals(); + } +} + +void php_observer_handle_exception_hook(zval* exception, zval* exception_this) { + /* + * The issue is, with OAPI, only the most recent exception is exposed in the + * error handler. If function `a` calls function `b` calls function `c` calls + * function `d` which throws an exception that `c` catches and that `c' then + * throws an exception that `b` catches but then b throws an exception that is + * uncaught, only the latest exception thrown by `b` gets passed to the error + * handler. Additonally, the fcall_end handler does not get called for + * functions which have uncaught exceptions. + * + * To solve this, this function gets called with every exception regardless of + * whether it is caught or not. We save the most recent exception and the + * unique `this` pointer of the execute_data it is associated with so we can + * use it if we need to end stacked segments. If another exception is + * triggered while our saved exception is not null, we check if we need to end + * stacked segments and then save the new exception. + */ + + if (nrunlikely(NULL == exception || NULL == exception_this)) { + return; + } + + if (NULL != NRPRG(uncaught_exeption_execute_data_this)) { + /* + * A pending uncaught exception for this txn exists, see if we need to close + * segments. We determine this by comparing the `execute_data_this` pointer + * in the `metadata` of the top stacked segment with the `This` pointer of + * the currently executing segment. If the pointers match, then the + * execute_data is still executing and could theoretically still catch it. + * If the pointers don't match, then the previous exception caused the + * fcall_end to be skipped, so we need to close those stacked segments + * manually until we arrive at the correct stacked segment that corresponds + * to exception we just recieved. This will close all necessary stacked + * segments. If the previous exception had been caught anywhere along the + * calling chain (by an fcall_end happening for a function) the segments + * would have been closed and the exception cleared. + */ + if (exception_this != NRPRG(uncaught_exeption_execute_data_this)) { + nr_php_observer_exception_segments_end(NRPRG(uncaught_exception), + exception_this); + } + php_observer_clear_uncaught_exception_globals(); + } + php_observer_set_uncaught_exception_globals(exception, exception_this); +} static void nr_php_instrument_func_begin(NR_EXECUTE_PROTO) { nr_segment_t* segment = NULL; nruserfn_t* wraprec = NULL; int zcaught = 0; + nr_php_execute_metadata_t* metadata = NULL; NR_UNUSED_FUNC_RETURN_VALUE; if (NULL == NRPRG(txn)) { @@ -1702,10 +1928,13 @@ static void nr_php_instrument_func_begin(NR_EXECUTE_PROTO) { NRTXNGLOBAL(execute_count) += 1; + /* + * Handle here, but be aware the classes might not be loaded yet. + */ if (nrunlikely(OP_ARRAY_IS_A_FILE(NR_OP_ARRAY))) { const char* filename = nr_php_op_array_file_name(NR_OP_ARRAY); nr_execute_handle_framework(all_frameworks, num_all_frameworks, - filename TSRMLS_CC); + filename TSRMLS_CC); return; } wraprec = nr_php_get_wraprec_by_func(execute_data->func); @@ -1713,50 +1942,139 @@ static void nr_php_instrument_func_begin(NR_EXECUTE_PROTO) { * If there is custom instrumentation or tt detail is more than 0, start the * segment. */ - if ((NULL != wraprec) || (NRINI(tt_detail) && NR_OP_ARRAY->function_name)) { + if ((NULL == wraprec) && !(NRINI(tt_detail) && NR_OP_ARRAY->function_name)) { + return; + } + /* + * Check if it's a custom error handler. Even with some custom error + * handlers, fcall might not get called. But we don't need to wait for + * fcall_end to put the error anyway. It can be done earlier in + * fcall_begin. Here, we are doing before the segment call so the error gets + * on the correct stacked segment. + */ + if (NULL != wraprec && wraprec->is_exception_handler) { /* - * If a function needs to have arguments modified before it's executed this - * may/may not be the place to do it. As soon as the begin function handler - * is called, PHP may start the actual function execution. + * Before starting the error handler segment, put the error it handled on + * the segment that called it. The choice of E_ERROR for the error level + * is basically arbitrary, but matches the error level PHP uses if there + * isn't an exception handler, so this should give more consistency for + * the user in terms of what they'll see with and without an exception + * handler installed. */ - segment = nr_php_stacked_segment_init(segment); - if (nrunlikely(NULL == segment)) { - nrl_verbosedebug(NRL_AGENT, "Error initializing stacked segment."); - return; + nr_status_t status; + if (NULL != NRPRG(uncaught_exception)) { + status = nr_php_error_record_exception_segment( + NRPRG(txn), NRPRG(uncaught_exception), + &NRPRG(exception_filters) TSRMLS_CC); + + if (NR_FAILURE == status) { + nrl_verbosedebug(NRL_AGENT, "%s: unable to record exception on segment", + __func__); + } + zval* exception + = nr_php_get_user_func_arg(1, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + nr_php_error_record_exception( + NRPRG(txn), exception, nr_php_error_get_priority(E_ERROR), + "Uncaught exception ", &NRPRG(exception_filters) TSRMLS_CC); + php_observer_clear_uncaught_exception_globals(); } - segment->txn_start_time = nr_txn_start_time(NRPRG(txn)); - segment->wraprec = wraprec; - segment->lineno = nr_php_zend_execute_data_lineno(execute_data); - zcaught = nr_zend_call_oapi_special_before(wraprec, segment, - NR_EXECUTE_ORIG_ARGS TSRMLS_CC); - if (nrunlikely(zcaught)) { - zend_bailout(); + } else { + /* + * Check if NRPRG(uncaught_exception) exists because if it's not handled, + * we'll parent the new segment on the wrong stacked segment. Close off + * all dangling segments caused by an exception before starting a new + * segment. + */ + + if (nrunlikely(NULL != NRPRG(uncaught_exception))) { + /* + * First check if it's the root because obviously, prev_execute won't + * exist. + */ + if (NRTXN(segment_root) != NRTXN(force_current_segment)) { + /* + * Get the current segment if it exists. + */ + nr_segment_t* exception_segment = NRTXN(force_current_segment); + if (NULL != exception_segment) { + /* + * If the metadata info doesn't match the previous callers This, + * then we know the uncaught exception occurred which caused the + * fcall_end function to not be called. Clean up dangling stacked + * segments. + */ + nr_php_execute_metadata_t* md = exception_segment->metadata; + if ((NULL != md) + && (md->execute_data_this + != &execute_data->prev_execute_data->This)) { + /* + * Close all previous segments, attaching the uncaught exception + * as necessary. + */ + nr_php_observer_exception_segments_end( + NRPRG(uncaught_exception), + &execute_data->prev_execute_data->This); + } + } + php_observer_clear_uncaught_exception_globals(); + } } } - return; + + segment = nr_php_stacked_segment_init(segment); + if (nrunlikely(NULL == segment)) { + nrl_verbosedebug(NRL_AGENT, "Error initializing stacked segment."); + return; + } + + nr_php_execute_metadata_init(segment->metadata, NR_OP_ARRAY); + metadata = segment->metadata; + metadata->execute_data_this = &execute_data->This; + /* + * Metadata deinit is handled when the segment is destroyed. + */ + + if (NULL == wraprec) { + return; + } + /* + * If a function needs to have arguments modified, do so in + * nr_zend_call_oapi_special_before. + */ + segment->wraprec = wraprec; + zcaught = nr_zend_call_oapi_special_before(wraprec, segment, + NR_EXECUTE_ORIG_ARGS); + if (nrunlikely(zcaught)) { + zend_bailout(); + } + + nr_txn_force_single_count(NRPRG(txn), wraprec->supportability_metric); + /* + * Check for, and handle, frameworks. + */ + if (wraprec->is_names_wt_simple) { + nr_txn_name_from_function(NRPRG(txn), wraprec->funcname, + wraprec->classname); + } } static void nr_php_instrument_func_end(NR_EXECUTE_PROTO) { int zcaught = 0; - nr_php_execute_metadata_t metadata = {0}; nr_segment_t* segment = NULL; nruserfn_t* wraprec = NULL; + bool create_metric = false; + nr_php_execute_metadata_t* metadata = NULL; if (NULL == NRPRG(txn)) { return; } + /* * Let's get the framework info. */ if (nrunlikely(OP_ARRAY_IS_A_FILE(NR_OP_ARRAY))) { - /* - * Optimization option: could remove code level metrics for filenames. - */ - nr_php_execute_metadata_add_code_level_metrics(&metadata, - NR_EXECUTE_ORIG_ARGS); - nr_php_txn_add_code_level_metrics(NRPRG(txn)->attributes, &metadata); - nr_php_execute_metadata_release(&metadata); nr_php_execute_file(NR_OP_ARRAY, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + php_observer_clear_uncaught_exception_globals(); return; } @@ -1766,142 +2084,82 @@ static void nr_php_instrument_func_end(NR_EXECUTE_PROTO) { * than 0. */ segment = NRTXN(force_current_segment); - if (NULL == segment) { + if (nrunlikely(NULL == segment)) { + /* + * Most likely caused by txn ending prematurely and closing all segments. We + * can only exit since the segments were already closed. + */ return; } - if (nrunlikely(0 == segment->txn_start_time)) { + if (nrunlikely(NULL == segment->metadata)) { /* - * The begin function handler always sets segment-txn_start_time. If it is - * not set, something else put the segment up and we are out of synch. + * If this value isn't set, it is either the root segment not a stacked + * segment set or not set by the instrument_begin_func, but in all we we + * should only ignore it. */ return; } - - /* - * Check if we have special instrumentation for this function or if the user - * has specifically requested it. - */ - wraprec = (nruserfn_t*)(segment->wraprec); - metadata.function_lineno = segment->lineno; /* - * Do a sanity check to make sure the names match. + * If the metadata info doesn't match, an uncaught exception occurred which + * doesn't call fcall_end. */ - if (NULL != wraprec && nr_php_wraprec_matches(wraprec, execute_data->func)) { - /* - * This is the case for specifically requested custom instrumentation. - */ - segment->stop_time = nr_txn_now_rel(NRPRG(txn)); - - bool create_metric = wraprec->create_metric; - nr_php_execute_metadata_add_code_level_metrics(&metadata, - NR_EXECUTE_ORIG_ARGS); - nr_php_execute_metadata_init(&metadata, NR_OP_ARRAY); - nr_txn_force_single_count(NRPRG(txn), wraprec->supportability_metric); - + metadata = segment->metadata; + if ((metadata->execute_data_this != &execute_data->This)) { /* - * Check for, and handle, frameworks. + * Close all previous segments, attaching the uncaught exception as + * necessary. */ - if (wraprec->is_names_wt_simple) { - nr_txn_name_from_function(NRPRG(txn), wraprec->funcname, - wraprec->classname); + nr_php_observer_exception_segments_end(NRPRG(uncaught_exception), + &execute_data->This); + php_observer_clear_uncaught_exception_globals(); + segment = NRTXN(force_current_segment); + if (NULL == segment) { + return; } - - /* - * The nr_txn_should_create_span_events() check is there so we don't - * record error attributes on the txn (and root segment) because it should - * already be recorded on the span that exited unhandled. - */ - if (wraprec->is_exception_handler - && !nr_txn_should_create_span_events(NRPRG(txn))) { - zval* exception - = nr_php_get_user_func_arg(1, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); - + metadata = segment->metadata; + if (nrunlikely(metadata->execute_data_this != &execute_data->This)) { /* - * The choice of E_ERROR for the error level is basically arbitrary, but - * matches the error level PHP uses if there isn't an exception handler, - * so this should give more consistency for the user in terms of what - * they'll see with and without an exception handler installed. + * Sanity check. + * If the pointers still aren't equal, let's exit. + * */ - nr_php_error_record_exception( - NRPRG(txn), exception, nr_php_error_get_priority(E_ERROR), - "Uncaught exception ", &NRPRG(exception_filters) TSRMLS_CC); + return; } + } - zcaught = nr_zend_call_orig_execute_special(wraprec, segment, - NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + /* + * Stop the segment time now so we don't add our additional processing on to + * the segment's time. + */ + segment->stop_time = nr_txn_now_rel(NRPRG(txn)); + /* + * Check if we have special instrumentation for this function or if the user + * has specifically requested it. + */ + wraprec = segment->wraprec; + + if (NULL != wraprec) { /* - * During this call, the transaction may have been ended and/or a new - * transaction may have started. To detect this, we compare the - * currently active transaction's start time with the transaction - * start time we saved before. - * - * Just comparing the transaction pointer is not enough, as a newly - * started transaction might actually obtain the same address as a - * transaction freed before. - */ - if (nrunlikely(nr_txn_start_time(NRPRG(txn)) != segment->txn_start_time)) { - nr_php_stacked_segment_deinit(segment); - } else { - nr_php_execute_segment_end(segment, &metadata, create_metric TSRMLS_CC); - } - nr_php_execute_metadata_release(&metadata); - if (nrunlikely(zcaught)) { - zend_bailout(); - } - } else if (NRINI(tt_detail) && NR_OP_ARRAY->function_name) { - /* - * This is the case for transaction_tracer.detail >= 1 requested custom - * instrumentation. + * This is the case for specifically requested custom instrumentation. */ - segment->stop_time = nr_txn_now_rel(NRPRG(txn)); + create_metric = wraprec->create_metric; - nr_php_execute_metadata_add_code_level_metrics(&metadata, - NR_EXECUTE_ORIG_ARGS); - nr_php_execute_metadata_init(&metadata, NR_OP_ARRAY); zcaught = nr_zend_call_orig_execute_special(wraprec, segment, - NR_EXECUTE_ORIG_ARGS TSRMLS_CC); - - if (nr_txn_should_create_span_events(NRPRG(txn))) { - if (EG(exception)) { - zval* exception_zval = NULL; - nr_status_t status; - - /* - * On PHP 7+, EG(exception) is stored as a zend_object, and is only - * wrapped in a zval when it actually needs to be. - */ - zval exception; - - ZVAL_OBJ(&exception, EG(exception)); - exception_zval = &exception; - - status = nr_php_error_record_exception_segment( - NRPRG(txn), exception_zval, &NRPRG(exception_filters) TSRMLS_CC); - - if (NR_FAILURE == status) { - nrl_verbosedebug( - NRL_AGENT, "%s: unable to record exception on segment", __func__); - } - } - } - - /* - * During this call, the transaction may have been ended and/or a new - * transaction may have started. To detect this, we compare the - * currently active transaction's start time with the transaction - * start time we saved before. - */ - if (nrunlikely(nr_txn_start_time(NRPRG(txn)) != segment->txn_start_time)) { - nr_php_stacked_segment_deinit(segment); - } else { - nr_php_execute_segment_end(segment, &metadata, false TSRMLS_CC); - } - nr_php_execute_metadata_release(&metadata); + NR_EXECUTE_ORIG_ARGS); if (nrunlikely(zcaught)) { zend_bailout(); } } + + nr_php_execute_segment_end(segment, segment->metadata, create_metric); + + /* + * Clear the uncaught exception globals. This will also take care of the case + * of an exception that was thrown for this segment but then was caught as + * evidenced by the fact that we got to fcall_end. + */ + php_observer_clear_uncaught_exception_globals(); return; } @@ -1932,6 +2190,9 @@ void nr_php_observer_fcall_begin(zend_execute_data* execute_data) { int show_executes = NR_PHP_PROCESS_GLOBALS(special_flags).show_executes; if (nrunlikely(show_executes)) { + nrl_verbosedebug(NRL_AGENT, + "Stack depth: %d after OAPI function beginning via %s", + NRPRG(php_cur_stack_depth), __func__); nr_php_show_exec(NR_EXECUTE_ORIG_ARGS); } nr_php_instrument_func_begin(NR_EXECUTE_ORIG_ARGS); @@ -1958,6 +2219,9 @@ void nr_php_observer_fcall_end(zend_execute_data* execute_data, = NR_PHP_PROCESS_GLOBALS(special_flags).show_execute_returns; if (nrunlikely(show_executes_return)) { + nrl_verbosedebug(NRL_AGENT, + "Stack depth: %d before OAPI function exiting via %s", + NRPRG(php_cur_stack_depth), __func__); nr_php_show_exec_return(NR_EXECUTE_ORIG_ARGS TSRMLS_CC); } diff --git a/agent/php_execute.h b/agent/php_execute.h index 3ae1192f6..2fc3edb3b 100644 --- a/agent/php_execute.h +++ b/agent/php_execute.h @@ -9,12 +9,15 @@ #ifndef PHP_EXECUTE_HDR #define PHP_EXECUTE_HDR +#include "php_observer.h" +#include "util_logging.h" + /* * An op_array is for a file rather than a function if it has a file name * and no function name. */ #define OP_ARRAY_IS_A_FILE(OP) \ - ((0 == nr_php_op_array_function_name(OP)) && nr_php_op_array_file_name(OP)) + ((NULL == nr_php_op_array_function_name(OP)) && nr_php_op_array_file_name(OP)) #define OP_ARRAY_IS_A_FUNCTION(OP) \ (nr_php_op_array_function_name(OP) && (0 == (OP)->scope)) #define OP_ARRAY_IS_FUNCTION(OP, FNAME) \ @@ -24,7 +27,19 @@ #define OP_ARRAY_IS_METHOD(OP, FNAME) \ (0 == nr_strcmp(nr_php_op_array_function_name(OP), (FNAME))) -#define CLM_STRLEN_MAX (255) +/* + * Purpose: Look through the PHP symbol table for special names or symbols + * that provide additional hints that a specific framework has been loaded. + * + * Returns: a nr_framework_classification + */ +typedef enum { + FRAMEWORK_IS_NORMAL, /* the framework isn't special, but is treated normally + */ + FRAMEWORK_IS_SPECIAL, /* the framework is special */ +} nr_framework_classification_t; +typedef nr_framework_classification_t (*nr_framework_special_fn_t)( + const char* filename TSRMLS_DC); /* * Version specific metadata that we have to gather before we call the original @@ -57,29 +72,14 @@ typedef struct { #if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ zend_string* scope; zend_string* function; - char* function_name; - char* function_filepath; - char* function_namespace; + zend_string* filepath; uint32_t function_lineno; + zval* execute_data_this; #else zend_op_array* op_array; #endif /* PHP7 */ } nr_php_execute_metadata_t; -/* - * Purpose: Look through the PHP symbol table for special names or symbols - * that provide additional hints that a specific framework has been loaded. - * - * Returns: a nr_framework_classification - */ -typedef enum { - FRAMEWORK_IS_NORMAL, /* the framework isn't special, but is treated normally - */ - FRAMEWORK_IS_SPECIAL, /* the framework is special */ -} nr_framework_classification_t; -typedef nr_framework_classification_t (*nr_framework_special_fn_t)( - const char* filename TSRMLS_DC); - extern nrframework_t nr_php_framework_from_config(const char* config_name); /* @@ -109,5 +109,42 @@ extern void nr_framework_create_metric(TSRMLS_D); extern void nr_php_user_instrumentation_from_opcache(TSRMLS_D); -#include "php_observer.h" +extern void nr_php_observer_handle_uncaught_exception(zval* exception_this); + +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP8+ */ +static inline void php_observer_clear_uncaught_exception_globals() { + /* + * Clear the uncaught exception global variables. + */ + if (NULL != NRPRG(uncaught_exception)) { + nr_php_zval_free(&NRPRG(uncaught_exception)); + } + NRPRG(uncaught_exeption_execute_data_this) = NULL; +} + +static inline void php_observer_set_uncaught_exception_globals( + zval* exception, + zval* exception_this) { + /* + * Set the uncaught exception global variables + */ + if (nrunlikely(NULL != NRPRG(uncaught_exception))) { + return; + } + NRPRG(uncaught_exception) = nr_php_zval_alloc(); + ZVAL_DUP(NRPRG(uncaught_exception), exception); + NRPRG(uncaught_exeption_execute_data_this) = exception_this; +} + +/* + * Purpose : Release any cached metadata. + * + * Params : 1. A pointer to the metadata. + */ +extern void nr_php_execute_metadata_release( + nr_php_execute_metadata_t* metadata); + +#endif + #endif /* PHP_EXECUTE_HDR */ diff --git a/agent/php_minit.c b/agent/php_minit.c index 31a8e4cf3..a8c341a33 100644 --- a/agent/php_minit.c +++ b/agent/php_minit.c @@ -731,7 +731,9 @@ void nr_php_late_initialization(void) { * forward the errors, so if a user has Xdebug loaded, we do not install * our own error callback handler. Otherwise, we do. */ -#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO /* < PHP8 */ + +#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ + || defined OVERWRITE_ZEND_EXECUTE_DATA /* < PHP8 */ if (0 == zend_get_extension("Xdebug")) { NR_PHP_PROCESS_GLOBALS(orig_error_cb) = zend_error_cb; zend_error_cb = nr_php_error_cb; @@ -740,7 +742,7 @@ void nr_php_late_initialization(void) { "the Xdebug extension prevents the New Relic agent from " "gathering errors. No errors will be recorded."); } -#endif /* end of < PHP8 */ +#endif /* end of < PHP8 or not using OAPI*/ /* * Install our signal handler, unless the user has set a special flag diff --git a/agent/php_newrelic.h b/agent/php_newrelic.h index dfe3d3863..a97eb8ed4 100644 --- a/agent/php_newrelic.h +++ b/agent/php_newrelic.h @@ -435,6 +435,16 @@ int php_cur_stack_depth; /* Total current depth of PHP stack, measured in PHP nrphpcufafn_t cufa_callback; /* The current call_user_func_array callback, if any */ +/* + * The exception happens whether the exception was caught or not. Keep track of + * the execute_data frame to determine if it was uncaught and so we can compare + * to determine if we need to propagate the exception or not. */ +zval* uncaught_exception; /* The last exception that occurred. Does need to be + freed. */ +zval* uncaught_exeption_execute_data_this; /* Keep track of the execute data + that the last exception occurred + on. Does not need to be freed. */ + /* * We instrument database connection constructors and store the instance * information in a hash keyed by a string containing the connection resource diff --git a/agent/php_observer.c b/agent/php_observer.c index c6ae58fa1..23d0878d1 100644 --- a/agent/php_observer.c +++ b/agent/php_observer.c @@ -9,14 +9,18 @@ #include #include +#include + #include "php_api_distributed_trace.h" #include "php_environment.h" #include "php_error.h" +#include "php_execute.h" #include "php_extension.h" #include "php_globals.h" #include "php_header.h" #include "php_hooks.h" #include "php_internal_instrument.h" +#include "php_observer.h" #include "php_samplers.h" #include "php_user_instrument.h" #include "php_vm.h" @@ -35,9 +39,6 @@ #include "util_syscalls.h" #include "util_threads.h" -#include "php_observer.h" -#include "php_execute.h" - /* * Observer API functionality was added with PHP 8.0. * @@ -86,9 +87,10 @@ static zend_observer_fcall_handlers nr_php_fcall_register_handlers( return handlers; } - void nr_php_observer_no_op(zend_execute_data* execute_data NRUNUSED){}; +static void (*original_zend_throw_exception_hook)(zend_object* ex); + void nr_php_observer_minit() { /* * Register the Observer API handlers. @@ -96,6 +98,13 @@ void nr_php_observer_minit() { zend_observer_fcall_register(nr_php_fcall_register_handlers); zend_observer_error_register(nr_php_error_cb); + /* + * Overwrite the exception_hook. Note: This ONLY notifies when an exception + * is thrown. It gives no indication if that exception was subsequently + * caught or not. + */ + original_zend_throw_exception_hook = zend_throw_exception_hook; + zend_throw_exception_hook = nr_throw_exception_hook; /* * For Observer API with PHP 8+, we no longer need to ovewrwrite the zend * execute hook. orig_execute is called various ways in various places, so @@ -104,4 +113,23 @@ void nr_php_observer_minit() { NR_PHP_PROCESS_GLOBALS(orig_execute) = nr_php_observer_no_op; } +void nr_throw_exception_hook(zend_object* exception) { + zval new_exception; + zval* exception_zval = NULL; + + /* + * Since PHP 7, EG(exception) is stored as a zend_object, and is therefore + * only wrapped in a zval when it actually needs to be. + */ + ZVAL_OBJ(&new_exception, exception); + exception_zval = &new_exception; + + php_observer_handle_exception_hook(exception_zval, + &(EG(current_execute_data)->This)); + + if (original_zend_throw_exception_hook != NULL) { + original_zend_throw_exception_hook(exception); + } +} + #endif diff --git a/agent/php_observer.h b/agent/php_observer.h index dcb852756..4225c4678 100644 --- a/agent/php_observer.h +++ b/agent/php_observer.h @@ -75,6 +75,32 @@ void nr_php_observer_fcall_begin(zend_execute_data* execute_data); void nr_php_observer_fcall_end(zend_execute_data* execute_data, zval* func_return_value); +/* + * Purpose : Overwrite the php exception hook. + * + * Params : zend_object* exception : The exception to monitor. + */ +void nr_throw_exception_hook(zend_object* exception); + +/* + * Purpose : Monitor the exception to take care of dangling segments, if needed. + * + * Params : 1) zval* exception : The exception to monitor. + * 2) zval* execute_data_this: The pointer to the unique execute data + * that the exception was thrown from. + */ +void php_observer_handle_exception_hook(zval* exception_zval, + zval* execute_data_this); + +/* + * Purpose : End a stacked segment. If an exception is provided, add it before + * exiting. + * + * Params : zval* exception : The exception to add to the segment. If NULL, no + * exception is recorded on the segment. + */ +extern void nr_php_observer_segment_end(zval* exception); + #endif /* PHP8+ */ #endif // NEWRELIC_PHP_AGENT_PHP_OBSERVER_H diff --git a/agent/php_rinit.c b/agent/php_rinit.c index 4c3c96e7b..8a96c4fb4 100644 --- a/agent/php_rinit.c +++ b/agent/php_rinit.c @@ -40,6 +40,7 @@ PHP_RINIT_FUNCTION(newrelic) { #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA NRPRG(drupal_http_request_segment) = NULL; + NRPRG(drupal_http_request_depth) = 0; #endif if ((0 == NR_PHP_PROCESS_GLOBALS(enabled)) || (0 == NRINI(enabled))) { diff --git a/agent/php_rshutdown.c b/agent/php_rshutdown.c index cab121f7e..8619c6170 100644 --- a/agent/php_rshutdown.c +++ b/agent/php_rshutdown.c @@ -93,6 +93,13 @@ int nr_php_post_deactivate(void) { EG(trampoline).op_array.reserved[NR_PHP_PROCESS_GLOBALS(zend_offset)] = NULL; #endif /* PHP7 */ #endif + /* + * End the txn before we clean up all the globals it might need. + */ + if (nrlikely(0 != NRPRG(txn))) { + (void)nr_php_txn_end(0, 1 TSRMLS_CC); + } + nr_php_remove_transient_user_instrumentation(); nr_php_exception_filters_destroy(&NRPRG(exception_filters)); @@ -113,10 +120,6 @@ int nr_php_post_deactivate(void) { NRPRG(cufa_callback) = NULL; - if (nrlikely(0 != NRPRG(txn))) { - (void)nr_php_txn_end(0, 1 TSRMLS_CC); - } - NRPRG(current_framework) = NR_FW_UNSET; NRPRG(framework_version) = 0; #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ diff --git a/agent/php_stacked_segment.c b/agent/php_stacked_segment.c index 872da57ec..1c1c42c06 100644 --- a/agent/php_stacked_segment.c +++ b/agent/php_stacked_segment.c @@ -4,6 +4,9 @@ */ #include "php_stacked_segment.h" +#include "util_logging.h" +#include "php_execute.h" +#include "php_error.h" /* * Purpose : Add a stacked segment to the stacked segment stack. The top @@ -32,6 +35,12 @@ nr_segment_t* nr_php_stacked_segment_init(nr_segment_t* stacked TSRMLS_DC) { return NULL; } + stacked->metadata = nr_calloc(1, sizeof(nr_php_execute_metadata_t)); + if (NULL == stacked->metadata) { + nr_free(stacked); + return NULL; + } + #endif stacked->txn = NRPRG(txn); @@ -57,23 +66,50 @@ void nr_php_stacked_segment_deinit(nr_segment_t* stacked TSRMLS_DC) { /* * This is allocated differently for OAPI and hence needs to be freed. */ + nr_php_execute_metadata_release(stacked->metadata); + nr_free(stacked->metadata); nr_free(stacked); #endif } void nr_php_stacked_segment_unwind(TSRMLS_D) { - nr_segment_t* stacked; - nr_segment_t* segment; - if (NULL == NRPRG(txn)) { return; } + while (NRTXN(force_current_segment) && (NRTXN(segment_root) != NRTXN(force_current_segment))) { - stacked = NRTXN(force_current_segment); - segment = nr_php_stacked_segment_move_to_heap(stacked TSRMLS_CC); +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + /* + * With OAPI, we need to gracefully close off the stacked segments with + * their naming contexts. + */ + nr_php_observer_segment_end(NRPRG(uncaught_exception)); + +#else + nr_segment_t* stacked = NRTXN(force_current_segment); + nr_segment_t* segment + = nr_php_stacked_segment_move_to_heap(stacked TSRMLS_CC); nr_segment_end(&segment); + +#endif } +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + /* + * If OAPI we need to record the uncaught exception (if it exists) on the root + * segment as well. + */ + if (NULL != NRPRG(uncaught_exception)) { + if (NRTXN(segment_root) == NRTXN(force_current_segment)) { + nr_php_error_record_exception_segment( + NRPRG(txn), NRPRG(uncaught_exception), + &NRPRG(exception_filters) TSRMLS_CC); + } + } + php_observer_clear_uncaught_exception_globals(); +#endif } nr_segment_t* nr_php_stacked_segment_move_to_heap( @@ -105,6 +141,8 @@ nr_segment_t* nr_php_stacked_segment_move_to_heap( /* * This is allocated differently for OAPI and hence needs to be freed. */ + nr_php_execute_metadata_release(stacked->metadata); + nr_free(stacked->metadata); nr_free(stacked); #endif diff --git a/agent/php_stacked_segment.h b/agent/php_stacked_segment.h index e46659c81..c990a9c72 100644 --- a/agent/php_stacked_segment.h +++ b/agent/php_stacked_segment.h @@ -48,8 +48,10 @@ * - nr_slab_release * - zero-out segment * - * As a comparision, here's what happens when using stacked segments. - * Also for the best case. + * As a comparision, here's the basic outline of what happens when using stacked + * segments. Stacked segment alloc/dealloc for overwite execute paradigm handled + * by c stack behind the scenes Stacked segment alloc/dealloc for OAPI paradigm + * handled manually. Also for the best case. * * - nr_php_stacked_segment_init - nr_php_stacked_segment_discard * - 3 value changes - reparent children (3 if checks) @@ -61,6 +63,14 @@ * are immediately discarded. Speeding up the segment init/discard cycle * is crucial for improving the performance of the agent. * + * Additionally the ordered nature of the stack segment provides additional + * benefits when dealing with the increased likelihood of dangling segments in + * OAPI. + * + * There are some additional functionalities/checks added for OAPI however those + * would need to be done regardless of where the segment is located so are not + * added for comparison. + * * What enables us to eliminate much of the work done in the * nr_segment_start/nr_segment_discard cycle: * @@ -155,6 +165,396 @@ * Stacked segments cannot be used to model async segments. */ +// clang-format off +/* + * Observer API paradigm. + * + * + * The workflow of using stacked segments in connection with regular + * segments is complicated. It's best illustrated by a short ASCII + * cartoon. + * + * root < root root + * | | + * *A < *A + * | + * *B + * + * We start out with a root segment, OAPI calls nr_php_observer_fcall_begin for + * A, and it starts stacked segment *A and then nr_php_observer_fcall_begin(B) + * starts stacked segment *B as a child of *A. + * + * root root root + * | | | + * *A < *A *A < + * | + * *C < + * + * *nr_php_observer_fcall_end(B) decides to discard *B, and *A is the current + * segment again. nr_php_observer_fcall_begin(C) starts *C gets started as child + * of *A and when nr_php_observer_fcall_end(C) is called, *C gets discarded too. + * Note that up to this point, no segment except the root segment ever was + * allocated via the slab; however, stacked segments are being calloced in + * stacked_segment_init. + * + * root root root + * | | | + * *A < *A *A + * | | + * *D < *D + * | + * *E < + * + * In a next exciting step, nr_php_observer_fcall_begin(D) starts stacked + * segment *D as child of *A and nr_php_observer_fcall_begin(E) *E is started as + * child of *D. + * + * root root + * | | + * *A *A < + * | | + * *D < e + * | + * e + * + * Now something new happens. nr_php_observer_fcall_end(E) decides to keep the + * stacked segment *E. We copy the contents of the stacked segment *E into a + * segment e we obtained from the slab allocator, and we make e a child of the + * stacked segment *D. nr_php_observer_fcall_end(D) discards stacked segment *D + * and its child e is made a child of *D's parent *A. + * + * root root root + * | | | + * *A *A < *A + * / \ | / \ + * e *F < e e *G < + * + * More of the same. nr_php_observer_fcall_begin(F) creates a stacked segment *F + * as child of A and nr_php_observer_fcall_end(F) eventually discards it. + * nr_php_observer_fcall_begin(G) then creates a stacked segment *G. + * + * root root < root + * | | / \ + * *A < a a *H + * / \ / \ / \ + * e g e g e g + * + * Finally nr_php_observer_fcall_end(G) also decides to keep *G. Again, it is + * turned into a regular segment g and made a child of *A. Then we decide to + * keep *A, turning it into regular segment a and making it a child of the root + * segment. Afterward a nr_php_observer_fcall_begin(H) starts stacked segment *H + * as child of the root segment. + * + * Note that with this workflow, we went through the + * nr_segment_start/nr_segment_discard cycle for only 3 times, + * although we used 8 different segments. For the remaining 5 segments, we + * went through the stacked segment cycle. + * + * Also note that this only works with segments on the default parent stack. + * Stacked segments cannot be used to model async segments. + * + * Dangling segments: + * With the use of Observer API we have the possibility of dangling segments. In + * the normal course of events, the above scenario shows + * nr_php_observer_fcall_begin starting segments and nr_php_observer_fcall_end + * keeping/discarding/ending segments. However, in the case of an uncaught + * exception, nr_php_observer_fcall_end is never called and therefore, the logic + * to keep/discard/end the segment doesn't automatically get initiated. + * Additionally, PHP only provides the last exception (meaning if exceptions + * were thrown then rethrown or another exception thrown, nothing gets + * communicated except for the last exception. PHP has a hook that can be used + * to notify whenever an exception is triggered but it doesn't give any + * indication if that exception was ever caught. + * + * To handle this, dangling exception sweeps occur in + * nr_php_observer_exception_segments_end and is called from 5 different places: + * 1) nr_php_observer_fcall_begin - before a new segment starts + * 2) nr_php_observer_fcall_end - before a segment is ended(kept/discarded) + * 3) nr_php_stacked_segment_unwind - when a txn ends and we are closing up shop + * 4) php_observer_handle_exception_hook - when a new exception is noticed + * 5) in newrelic APIs that depend on having the current segment + * + * + * The workflow of using stacked segments in connection with regular + * segments when an exception occurs is complicated. + * These cases are illustrated by a series of short ASCII cartoons. + * + * case 1 nr_php_observer_fcall_begin - before a new segment starts + * root < root root + * | | + * *A < *A + * | + * *B + * + * We start out with a root segment, OAPI calls nr_php_observer_fcall_begin(A), + * and it starts stacked segment *A and then nr_php_observer_fcall_begin(B) + * starts stacked segment *B as a child of *A. + * + * root root + * | | + * *A *A + * | | + * *B *B < + * | | + * *C < c + * + * nr_php_observer_fcall_begin(C) starts *C gets started as child + * of *B. Function C throws an uncaught exception which B does not catch so + * neither nr_php_observer_fcall_end(B) nor nr_php_observer_fcall_end(C) is + * called and *C remains the current segment. A catches the exception and calls + * function D, so nr_php_observer_fcall_begin(D) is triggered. At this point we + * realize the current stacked_segment->metadata->This value and the + * execute_data->prev_execute_data->This don't match so we don't want to parent + * *D to the wrong segment. We check the global exception hook and see it + * has a value and that the global uncaught_exception_this also matches the + * current segment `this`. Time to apply the exception and clean up dangling + * segments. We pop the current segment *C and apply the exception. + * Because it has an exception, the segment is kept so we copy the contents of + * the stacked segment *C into a segment c we obtained from the slab allocator, + * and we make c a child of the stacked segment *B which becomes the current + * segment. + * + * root root root + * | | | + * *A < *A < *A + * | | / \ + * b b b *D < + * | | | + * c c c + * + * + * But we aren't done yet. + * current stacked_segment->metadata->this still doesn't equal the + * execute_data->prev_execute_data->This provided by + * nr_php_observer_fcall_begin(D). We pop the current segment *B and apply the + * exception. Because it has an exception, the segment is kept so we copy the + * contents of the stacked segment *B into a segment b we obtained from the + * slab allocator, and we make b a child of the stacked segment *A which + * becomes the current segment. Now current stacked_segment->metadata->this + * DOES equal the execute_data->prev_execute_data->This provided by + * nr_php_observer_fcall_begin(D) so we proceed and create stacked segment *D + * correctly parented as a child of *A and *D becomes the current segment. + * + * case 2 nr_php_observer_fcall_end - before a segment is ended(kept/discarded) + * root < root root + * | | + * *A < *A + * | + * *B + * + * We start out with a root segment, OAPI calls nr_php_observer_fcall_begin(A), + * and it starts stacked segment *A and then nr_php_observer_fcall_begin(B) + * starts stacked segment *B as a child of *A. + * + * root root + * | | + * *A *A + * | | + * *B *B < + * | | + * *C < c + * + * nr_php_observer_fcall_begin(C) starts segment *C as child + * of *B. Function C throws an uncaught exception which B does not catch so + * neither nr_php_observer_fcall_end(B) nor nr_php_observer_fcall_end(C) is + * called and *C remains the current segment. A catches the exception and + * nr_php_observer_fcall_end(A) is triggered. At this point we compare the + * current stacked_segment->metadata->This value with the execute_data->This and + * realize the two don't match. We check the global exception hook and see it + * has a value and that the global uncaught_exception_this also matches the + * current segment `this`. Time to apply the exception and clean up dangling + * segments. We pop the current segment *C and apply the exception. + * Because it has an exception, the segment is kept so we copy the contents of + * the stacked segment *C into a segment c we obtained from the slab allocator, + * and we make c a child of the stacked segment *B which becomes the current + * segment. + * + * root root < + * | | + * *A < a + * | | + * b b + * | | + * c c + * + * + * But we aren't done yet. + * current stacked_segment->metadata->this still doesn't equal the + * execute_data-> this provided by nr_php_observer_fcall_end(A). We pop the + * current segment *B and apply the exception. Because it has an exception, the + * segment is kept so we copy the contents of the stacked segment *B into a + * segment b we obtained from the slab allocator, and we make b a child of the + * stacked segment *A which becomes the current segment. Now current + * stacked_segment->metadata->this DOES equal the execute_data-> this + * provided by nr_php_observer_fcall_end(A) so it proceeds, decides to keep the + * segment and we copy the contents of the stacked segment *A into a segment a + * we obtained from the slab allocator, and we make a a child of the stacked + * segment root and root becomes the current segment. + * + * case 3 nr_php_stacked_segment_unwind - when a txn ends but stacked segments + * still exist + * + * root < root root + * | | + * *A < *A + * | + * *B < + * + * We start out with a root segment, OAPI calls nr_php_observer_fcall_begin(A), + * and it starts stacked segment *A and then nr_php_observer_fcall_begin(B) + * starts stacked segment *B as a child of *A. + * + * root root + * | | + * *A *A + * | | + * *B *B < + * | | + * *C < c + * + * nr_php_observer_fcall_begin(C) starts *C gets started as child + * of *B. Function C throws an uncaught exception which A and B do not catch so + * nr_php_observer_fcall_end(A), nr_php_observer_fcall_end(B), + * nr_php_observer_fcall_end(C) are not called and *C remains the current + * segment. The root segment ends and rshutdown calls `nr_php_txn_end` which + * calls `nr_php_stacked_segment_unwind`. Because we didn't get any + * nr_php_observer_fcall_end we know no segment caught the exception that + * triggered the exception hook. We'll apply the exception and + * keep/close stacked segments all the way down the stack to clean up dangling + * segments. We pop the current segment *C and apply the exception. Because it + * has an exception, the segment is kept so we copy the contents of the stacked + * segment *C into a segment c we obtained from the slab allocator, and we make + * c a child of the stacked segment *B which becomes the current segment. + * + * root root < root + * | | | + * *A < a a + * | | | + * b b b + * | | | + * c c c + * + * We pop the current segment *B and apply the exception. Because it has an + * exception, the segment is kept so we copy the contents of the stacked segment + * *B into a segment b we obtained from the slab allocator, and we make b a + * child of the stacked segment *A which becomes the current segment. Then we + * pop the current segment *A and apply the exception. Because it has an + * exception, the segment is kept so we copy the contents of the stacked segment + * *A into a segment a we obtained from the slab allocator, and we make a a + * child of the root. The exception is applied to the root and the rshutdown + * completes. + * + * case 4 php_observer_handle_exception_hook - when a new exception is noticed + * root < root root + * | | + * *A < *A + * | + * *B + * + * We start out with a root segment, OAPI calls nr_php_observer_fcall_begin(A), + * and it starts stacked segment *A and then nr_php_observer_fcall_begin(B) + * starts stacked segment *B as a child of *A. + * + * root root root + * | | | + * *A < *A *A + * | | + * *B *B < + * | | + * *C < c + * + * nr_php_observer_fcall_begin(C) starts *C gets started as child + * of *B. Function C throws an exception which B catches but + * nr_php_observer_fcall_end(C) is not called so *C remains the + * current segment. B catches the exception and throws another + * exception which triggers the exception hook. At this point we realize the current + * exception->This value indicates another function is active. Because we + * received no nr_php_observer_fcall_end up to that point, we know the exception + * was uncaught until the exception->This function. We check the global + * exception hook and see it has a value and that the global + * uncaught_exception_this also matches the current segment `this`. Time to + * apply the exception and clean up dangling segments. We pop the current + * segment *C and apply the exception. Because it has an exception, the segment + * is kept so we copy the contents of the stacked segment *C into a segment c we + * obtained from the slab allocator, and we make c a child of the stacked + * segment *B which becomes the current segment. + * + * root + * | + * *A + * | + * *B < + * | + * c + * + * current stacked_segment->metadata->this now equals the exception->This. so we + * reserve judgement on what eventually happens to segment *B and *B becomes the + * current segment with the new active exception stored. Any subsequent dangling + * segments are cleaned when the next scenario 1-5 occurs. + * + * case 5 in newrelic APIs that depend on having the current segment + * root < root root + * | | + * *A < *A + * | + * *B + * + * We start out with a root segment, OAPI calls nr_php_observer_fcall_begin(A), + * and it starts stacked segment *A and then nr_php_observer_fcall_begin(B) + * starts stacked segment *B as a child of *A. + * + * root root + * | | + * *A *A + * | | + * *B *B < + * | | + * *C < c + * + * nr_php_observer_fcall_begin(C) starts *C gets started as child + * of *B. Function C throws an uncaught exception which B does not catch so + * neither nr_php_observer_fcall_end(B) nor nr_php_observer_fcall_end(C) is + * called and *C remains the current segment. A catches the exception and makes + * an API call `newrelic_notice_error`. All API functions that rely on segments + * call `nr_php_api_ensure_current_segment` before doing any segment related + * operation. `nr_php_api_ensure_current_segment` eventually calls + * `nr_php_observer_handle_uncaught_exception` where we check the `this` value + * of the function that called newrelic_notice_error and see it is not the same. + * Because we received no nr_php_observer_fcall_end up to that point, we know + * the exception was uncaught until the Function A. We check the global + * exception hook and see it has a value and that the global + * uncaught_exception_this also matches the current segment `this`. Time to + * apply the exception and clean up dangling segments as we don't want to apply + * the notice_error to the wrong segment. We pop the current segment *C and + * apply the exception. Because it has an exception, the segment is kept so we + * copy the contents of the stacked segment *C into a segment c we obtained + * from the slab allocator, and we make c a child of the stacked segment *B + * which becomes the current segment. + * + * root + * | + * *A < + * | + * b + * | + * c + * + * + * But we aren't done yet. + * We check the `this` value of the function that called + * newrelic_notice_error and see it is not the same as the current segment + * `this`. We pop the current segment *B and apply the exception. Because it has + * an exception, the segment is kept so we copy the contents of the stacked + * segment *B into a segment b we obtained from the slab allocator, and we make + * b a child of the stacked segment *A which becomes the current segment. We + * check the this` value of the function that called newrelic_notice_error and see it is + * the same as the current segment `this` so newrelic_notice_error proceeds and applies the + * notice error to the current segment *A. + * Note that this only works with segments on the default parent stack. + * Stacked segments cannot be used to model async segments. + */ +// clang-format on + #include "php_agent.h" /* diff --git a/agent/php_txn.c b/agent/php_txn.c index 45d2269c2..f0b8c8247 100644 --- a/agent/php_txn.c +++ b/agent/php_txn.c @@ -1033,9 +1033,6 @@ nr_status_t nr_php_txn_end(int ignoretxn, int in_post_deactivate TSRMLS_DC) { return NR_SUCCESS; } - /* Stop all recording although we shouldn't be getting anything */ - NRTXN(status.recording) = 0; - /* * If a transaction is ended while stacked segments are active (e. g. * by calling newrelic_end_transaction inside nested function scopes) @@ -1043,6 +1040,12 @@ nr_status_t nr_php_txn_end(int ignoretxn, int in_post_deactivate TSRMLS_DC) { */ nr_php_stacked_segment_unwind(TSRMLS_C); + nrl_verbosedebug(NRL_TXN, "%s: Ending the transaction and stack depth = %d", + __func__, NRPRG(php_cur_stack_depth)); + + /* Stop all recording although we shouldn't be getting anything */ + NRTXN(status.recording) = 0; + ignoretxn = nr_php_txn_should_ignore(ignoretxn TSRMLS_CC); /* Add the remaining metrics that aren't added on shutdown. */ @@ -1110,54 +1113,3 @@ nr_status_t nr_php_txn_end(int ignoretxn, int in_post_deactivate TSRMLS_DC) { return NR_SUCCESS; } - -extern void nr_php_txn_add_code_level_metrics( - nr_attributes_t* attributes, - const nr_php_execute_metadata_t* metadata) { -#if ZEND_MODULE_API_NO < ZEND_7_0_X_API_NO /* PHP7+ */ - (void)attributes; - (void)metadata; - return; -} -#else - /* Current CLM functionality only works with PHP 7+ */ - - if (NULL == metadata) { - return; - } - - /* - * Check if code level metrics are enabled in the ini. - * If they aren't, exit and don't add any attributes. - */ - if (!NRINI(code_level_metrics_enabled)) { - return; - } - -#define CHK_CLM_EMPTY(s) ((NULL == s || nr_strempty(s)) ? true : false) - - if (CHK_CLM_EMPTY(metadata->function_name)) { - /* - * CLM aren't set so don't do anything - */ - return; - } - - nr_txn_attributes_set_string_attribute(attributes, nr_txn_clm_code_function, - metadata->function_name); - - if (!CHK_CLM_EMPTY(metadata->function_filepath)) { - nr_txn_attributes_set_string_attribute(attributes, nr_txn_clm_code_filepath, - metadata->function_filepath); - } - if (!CHK_CLM_EMPTY(metadata->function_namespace)) { - nr_txn_attributes_set_string_attribute( - attributes, nr_txn_clm_code_namespace, metadata->function_namespace); - } - -#undef CHK_CLM_EMPTY - - nr_txn_attributes_set_long_attribute(attributes, nr_txn_clm_code_lineno, - metadata->function_lineno); -} -#endif diff --git a/agent/php_user_instrument.c b/agent/php_user_instrument.c index 35b2a3763..2bef25045 100644 --- a/agent/php_user_instrument.c +++ b/agent/php_user_instrument.c @@ -56,9 +56,7 @@ int nr_zend_call_orig_execute(NR_EXECUTE_PROTO TSRMLS_DC) { NR_PHP_PROCESS_GLOBALS(orig_execute) (NR_EXECUTE_ORIG_ARGS_OVERWRITE TSRMLS_CC); } - zend_catch { - zcaught = 1; - } + zend_catch { zcaught = 1; } zend_end_try(); return zcaught; } @@ -67,18 +65,32 @@ int nr_zend_call_oapi_special_before(nruserfn_t* wraprec, nr_segment_t* segment, NR_EXECUTE_PROTO) { volatile int zcaught = 0; - NR_UNUSED_FUNC_RETURN_VALUE; - NR_UNUSED_SPECIALFN; - zend_try { - if (wraprec && wraprec->special_instrumentation_before) { + + if (wraprec && wraprec->special_instrumentation_before) { + zend_try { wraprec->special_instrumentation_before(wraprec, segment, - NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + NR_EXECUTE_ORIG_ARGS); } + zend_catch { zcaught = 1; } + zend_end_try(); } - zend_catch { - zcaught = 1; + + return zcaught; +} + +int nr_zend_call_oapi_special_clean(nruserfn_t* wraprec, + nr_segment_t* segment, + NR_EXECUTE_PROTO) { + volatile int zcaught = 0; + + if (wraprec && wraprec->special_instrumentation_clean) { + zend_try { + wraprec->special_instrumentation_clean(wraprec, segment, + NR_EXECUTE_ORIG_ARGS); + } + zend_catch { zcaught = 1; } + zend_end_try(); } - zend_end_try(); return zcaught; } #endif @@ -96,9 +108,7 @@ int nr_zend_call_orig_execute_special(nruserfn_t* wraprec, (NR_EXECUTE_ORIG_ARGS_OVERWRITE TSRMLS_CC); } } - zend_catch { - zcaught = 1; - } + zend_catch { zcaught = 1; } zend_end_try(); return zcaught; } diff --git a/agent/php_user_instrument.h b/agent/php_user_instrument.h index eb30eada0..6ff6f955d 100644 --- a/agent/php_user_instrument.h +++ b/agent/php_user_instrument.h @@ -87,10 +87,13 @@ typedef struct _nruserfn_t { nrspecialfn_t special_instrumentation; /* * Only used by OAPI, PHP 8+. Used to do any special instrumentation actions - * before a function is executed. Both callbacks can bet set. Use the - * `nr_php_wrap_user_function_after_before` to set both. + * before a function is executed. special_instrumentation_clean will clean up + * any variables that were set in the before calledback but didn't get cleaned + * up when an exception circumvents the end callback. All callbacks can be + * set. Use the `nr_php_wrap_user_function_after_before_clean` to set. */ nrspecialfn_t special_instrumentation_before; + nrspecialfn_t special_instrumentation_clean; nruserfn_declared_t declared_callback; @@ -147,16 +150,16 @@ static inline bool nr_php_wraprec_matches(nruserfn_t* p, zend_function* func) { if (0 != p->lineno) { /* - * Lineno is set in the wraprec. If lineno doesn't match, we can exit without - * going on to the funcname/classname pair comparison. - * If lineno matches, but the wraprec filename is NULL, it is inconclusive and we - * we must do the funcname/classname compare. - * If lineno matches, wraprec filename is not NULL, and it matches/doesn't match, - * we can exit without doing the funcname/classname compare. + * Lineno is set in the wraprec. If lineno doesn't match, we can exit + * without going on to the funcname/classname pair comparison. If lineno + * matches, but the wraprec filename is NULL, it is inconclusive and we we + * must do the funcname/classname compare. If lineno matches, wraprec + * filename is not NULL, and it matches/doesn't match, we can exit without + * doing the funcname/classname compare. */ if (p->lineno != nr_php_zend_function_lineno(func)) { return false; - } + } /* * lineno matched, let's check the filename */ @@ -302,6 +305,9 @@ extern int nr_zend_call_orig_execute_special(nruserfn_t* wraprec, extern int nr_zend_call_oapi_special_before(nruserfn_t* wraprec, nr_segment_t* segment, NR_EXECUTE_PROTO); +extern int nr_zend_call_oapi_special_clean(nruserfn_t* wraprec, + nr_segment_t* segment, + NR_EXECUTE_PROTO); #endif /* * Purpose : Destroy all user instrumentation records, freeing diff --git a/agent/php_wrapper.c b/agent/php_wrapper.c index 2e0eb92d3..9df81924b 100644 --- a/agent/php_wrapper.c +++ b/agent/php_wrapper.c @@ -8,12 +8,14 @@ #include "php_wrapper.h" #include "util_logging.h" -nruserfn_t* nr_php_wrap_user_function_before_after( +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO +nruserfn_t* nr_php_wrap_user_function_before_after_clean( const char* name, size_t namelen, nrspecialfn_t before_callback, - nrspecialfn_t after_callback) { - nruserfn_t* wraprec = nr_php_add_custom_tracer_named(name, namelen TSRMLS_CC); + nrspecialfn_t after_callback, + nrspecialfn_t clean_callback) { + nruserfn_t* wraprec = nr_php_add_custom_tracer_named(name, namelen); if (NULL == wraprec) { return wraprec; @@ -45,9 +47,22 @@ nruserfn_t* nr_php_wrap_user_function_before_after( } } + if (clean_callback) { + if (is_instrumentation_set(wraprec->special_instrumentation_clean, + clean_callback)) { + nrl_verbosedebug(NRL_INSTRUMENT, + "%s: attempting to set special_instrumentation_clean " + "for %.*s, but " + "it is already set", + __func__, NRSAFELEN(namelen), NRBLANKSTR(name)); + } else { + wraprec->special_instrumentation_clean = clean_callback; + } + } + return wraprec; } - +#endif nruserfn_t* nr_php_wrap_user_function(const char* name, size_t namelen, nrspecialfn_t callback TSRMLS_DC) { diff --git a/agent/php_wrapper.h b/agent/php_wrapper.h index 9860cea40..cc20b7967 100644 --- a/agent/php_wrapper.h +++ b/agent/php_wrapper.h @@ -83,14 +83,23 @@ * 3. Delegation: you can delegate from any wrapper to another wrapper with * NR_PHP_WRAPPER_DELEGATE (foo), provided the original function hasn't * already been called. + * + * OAPI updates: + * There are now before, after, and clean callbacks. + * 1) before_callback gets called when OAPI triggers the begin function hook. + * 2) after_callback gets called when OAPI triggers the end function hook. + * 3) clean_callback gets called in the case of dangling segments that occur + * because an exception causes the end function hook to NOT be called and thus + * the clean function resets any variables. */ - -extern nruserfn_t* nr_php_wrap_user_function_before_after( +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO +extern nruserfn_t* nr_php_wrap_user_function_before_after_clean( const char* name, size_t namelen, nrspecialfn_t before_callback, - nrspecialfn_t after_callback); - + nrspecialfn_t after_callback, + nrspecialfn_t clean_callback); +#endif extern nruserfn_t* nr_php_wrap_user_function(const char* name, size_t namelen, nrspecialfn_t callback TSRMLS_DC); diff --git a/agent/scripts/newrelic.ini.template b/agent/scripts/newrelic.ini.template index 71720f82d..05fd3de57 100644 --- a/agent/scripts/newrelic.ini.template +++ b/agent/scripts/newrelic.ini.template @@ -1206,3 +1206,14 @@ newrelic.daemon.logfile = "/var/log/newrelic/newrelic-daemon.log" ; on the APM Summary page. ; ;newrelic.application_logging.metrics.enabled = true + + +; Setting: newrelic.code_level_metrics.enabled +; Type : boolean +; Scope : per-directory +; Default: false +; Info : Toggles whether the agent provides function name, function +; filepath, function namespace, and function lineno as +; attributes on reported spans +; +;newrelic.code_level_metrics.enabled = false diff --git a/agent/tests/test_agent.c b/agent/tests/test_agent.c index 6ad98df4d..12d3437e4 100644 --- a/agent/tests/test_agent.c +++ b/agent/tests/test_agent.c @@ -560,144 +560,6 @@ static void test_default_address() { #if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ -static void test_nr_php_zend_execute_data_function_name() { - zend_function* func; - zend_execute_data execute_data = {0}; - - /* - * Test : Invalid arguments, NULL zend_execute_data - */ - tlib_pass_if_null("NULL zend_execute_data should return NULL", - nr_php_zend_execute_data_function_name(NULL)); - - /* - * Test : Invalid arguments. - */ - tlib_pass_if_null("NULL zend_function should return NULL", - nr_php_zend_execute_data_function_name(&execute_data)); - - /* - * Test : Normal operation. - */ - func = nr_php_find_function("newrelic_get_request_metadata"); - execute_data.func = func; - tlib_pass_if_str_equal( - "Unexpected function name", "newrelic_get_request_metadata", - nr_php_zend_execute_data_function_name(&execute_data TSRMLS_CC)); -} - -static void test_nr_php_zend_execute_data_filename() { - zend_function func = {0}; - zend_string* filename = NULL; - zend_execute_data execute_data = {0}; - - /* - * Test : Invalid arguments, NULL zend_execute_data - */ - tlib_pass_if_null("NULL zend_execute_data should return NULL", - nr_php_zend_execute_data_filename(NULL TSRMLS_CC)); - - /* - * Test : Null function. - */ - tlib_pass_if_null("NULL zend_function should return NULL", - nr_php_zend_execute_data_filename(&execute_data TSRMLS_CC)); - - /* - * Test : Function exists, op_array doesn't. - */ - execute_data.func = &func; - tlib_pass_if_null("NULL op_array should return NULL", - nr_php_zend_execute_data_filename(&execute_data TSRMLS_CC)); - - /* - * Test : Function exists, op_array exists. - */ - filename = zend_string_init("myfilename\\is\\here", - strlen("myfilename\\is\\here"), 0); - func.op_array.filename = filename; - execute_data.func = &func; - tlib_pass_if_str_equal( - "Filename should be displayed", ZSTR_VAL(filename), - nr_php_zend_execute_data_filename(&execute_data TSRMLS_CC)); - zend_string_release(filename); -} - -static void test_nr_php_zend_execute_data_scope_name() { - zend_function func = {0}; - zend_string* scope_name = NULL; - zend_execute_data execute_data = {0}; - zend_class_entry ce = {0}; - - /* - * Test : Invalid arguments, NULL zend_execute_data - */ - tlib_pass_if_null("NULL zend_execute_data should return NULL", - nr_php_zend_execute_data_scope_name(NULL TSRMLS_CC)); - - /* - * Test : Invalid arguments. - */ - tlib_pass_if_null( - "NULL zend_function should return NULL", - nr_php_zend_execute_data_scope_name(&execute_data TSRMLS_CC)); - - /* - * Test : Function exists, but no class scope. - */ - execute_data.func = &func; - tlib_pass_if_null( - "NULL op_array should return NULL", - nr_php_zend_execute_data_scope_name(&execute_data TSRMLS_CC)); - - /* - * Test : Function exists, class scope exists. - */ - execute_data.func = &func; - scope_name = zend_string_init("NewRelic\\Integration", - strlen("NewRelic\\Integration"), 0); - ce.name = scope_name; - execute_data.func->common.scope = &ce; - tlib_pass_if_str_equal( - "Unexpected scope name", ZSTR_VAL(scope_name), - nr_php_zend_execute_data_scope_name(&execute_data TSRMLS_CC)); - zend_string_release(scope_name); -} - -static void test_nr_php_zend_execute_data_lineno() { - zend_function func = {0}; - zend_op opline = {0}; - zend_execute_data execute_data = {0}; - - /* - * Test : Invalid arguments, NULL zend_execute_data - */ - tlib_pass_if_uint32_t_equal("NULL zend_execute_data should return 0", 0, - nr_php_zend_execute_data_lineno(NULL TSRMLS_CC)); - - /* - * Test : Invalid arguments. - */ - tlib_pass_if_uint32_t_equal( - "NULL zend_function should return 0", 0, - nr_php_zend_execute_data_lineno(&execute_data TSRMLS_CC)); - - /* - * Test : Normal operation. - */ - execute_data.func = &func; - - opline.lineno = 4; - execute_data.opline = &opline; - tlib_pass_if_uint32_t_equal( - "Unexpected lineno name", 4, - nr_php_zend_execute_data_lineno(&execute_data TSRMLS_CC)); -} - -#endif /* PHP 7+ */ - -#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ - static void test_nr_php_zend_function_lineno() { zend_function func = {0}; @@ -718,6 +580,7 @@ static void test_nr_php_zend_function_lineno() { */ func.op_array.line_start = 4; + func.op_array.type = ZEND_USER_FUNCTION; tlib_pass_if_uint32_t_equal("Unexpected lineno name", 4, nr_php_zend_function_lineno(&func)); } @@ -747,12 +610,6 @@ void test_main(void* p NRUNUSED) { * Tests that require state and will handle their own request startup and * shutdown. */ -#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ - test_nr_php_zend_execute_data_function_name(); - test_nr_php_zend_execute_data_filename(); - test_nr_php_zend_execute_data_lineno(); - test_nr_php_zend_execute_data_scope_name(); -#endif /* PHP 7+ */ #if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP7+ */ test_nr_php_zend_function_lineno(); diff --git a/agent/tests/test_php_execute.c b/agent/tests/test_php_execute.c index e088cfaab..bbff5a7df 100644 --- a/agent/tests/test_php_execute.c +++ b/agent/tests/test_php_execute.c @@ -101,20 +101,238 @@ static void test_php_cur_stack_depth(TSRMLS_D) { tlib_php_request_start(); tlib_php_request_eval("function f1() { return 4; }" TSRMLS_CC); - tlib_php_request_eval("function f2() { newrelic_ignore_transaction(); return 4; }" TSRMLS_CC); + tlib_php_request_eval( + "function f2() { newrelic_ignore_transaction(); return 4; }" TSRMLS_CC); expr = nr_php_call(NULL, "f1"); nr_php_zval_free(&expr); - tlib_pass_if_int_equal("PHP stack depth tracking when recording", 0, NRPRG(php_cur_stack_depth)); + tlib_pass_if_int_equal("PHP stack depth tracking when recording", 0, + NRPRG(php_cur_stack_depth)); expr = nr_php_call(NULL, "f2"); nr_php_zval_free(&expr); - tlib_pass_if_int_equal("PHP stack depth tracking when ignoring", 0, NRPRG(php_cur_stack_depth)); + tlib_pass_if_int_equal("PHP stack depth tracking when ignoring", 0, + NRPRG(php_cur_stack_depth)); + + tlib_php_request_end(); +} + +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ + +static void populate_functions() { + tlib_php_request_eval( + "function three($a) { if (0 == $a) { throw new " + "RuntimeException('Division by zero'); } else return $a; }"); + tlib_php_request_eval("function two($a) { return three($a); }"); + tlib_php_request_eval("function uncaught($a) { return two($a); }"); + tlib_php_request_eval( + "function caught($a) { try {two($a);} catch (Exception $e) { return 1;} " + "return 1; }"); + tlib_php_request_eval( + "function followup($a) { try {two($a);} catch (Exception $e) { return " + "three(1);} return three(1); }"); + tlib_php_request_eval( + "function followup_uncaught($a) { try {two($a);} catch (Exception $e) { " + "return three(0);} return three(1); }"); + tlib_php_request_eval( + "function rethrow($a) { try {two($a);} catch (Exception $e) { throw new " + "RuntimeException('Rethrown caught exception: '. $e->getMessage());} " + "return three(1); }"); +} + +static void test_stack_depth_after_exception() { + zval* expr = NULL; + zval* arg = NULL; + + /* + * call a function and trigger an exception and cause two segments to dangle + * because it was caught, even though two functions don't get the oapi end + * func call, they are still cleaned up and stack_depth is appropriately + * decremented. the valid function end will trigger a cleanup dangling + * segments, stack_depth should be zero again + */ + + /* + * stack depth should increment on function call and decrement on function + * end. + */ + tlib_php_request_start(); + populate_functions(); + /* + * pass argument that will not throw exception. + * stack depth should be 0 before calling and 1 before ending. + */ + tlib_pass_if_int_equal( + "PHP stack depth tracking should be 0 before function call", 0, + NRPRG(php_cur_stack_depth)); + arg = tlib_php_request_eval_expr("1" TSRMLS_CC); + expr = nr_php_call(NULL, "uncaught", arg); + tlib_pass_if_not_null("Runs fine with no exception.", expr); + tlib_pass_if_zval_type_is("Should have received the arg value.", IS_LONG, + expr); + tlib_pass_if_int_equal( + "PHP stack depth tracking should be 0 after successful function call", 0, + NRPRG(php_cur_stack_depth)); + + nr_php_zval_free(&expr); + nr_php_zval_free(&arg); + + /* + * call a function and trigger an exception and cause three segments to dangle + * stack_depth should be initially stuck at 3 + * after triggering the unwind, stack_depth should be zero again + */ + tlib_pass_if_int_equal( + "PHP stack depth tracking should be 0 before function call", 0, + NRPRG(php_cur_stack_depth)); + arg = tlib_php_request_eval_expr("0"); + expr = nr_php_call(NULL, "uncaught", arg); + tlib_pass_if_int_equal( + "PHP stack depth tracking should be 3 after function call", 3, + NRPRG(php_cur_stack_depth)); + tlib_pass_if_null("Exception so expr should be null.", expr); + + /* + * Trigger the unwind. + */ + tlib_php_request_eval("newrelic_end_transaction(); "); + tlib_pass_if_int_equal( + "PHP stack depth tracking should be 0 after transaction ends", 0, + NRPRG(php_cur_stack_depth)); + nr_php_zval_free(&expr); + nr_php_zval_free(&arg); + tlib_php_request_end(); + + /* + * call a function and trigger an exception that is caught but causes two + * segments to dangle. + * the function that caught the exception will successfully call the + * registered oapi function end which will trigger a cleanup of dangling + * segments, stack_depth should be zero + */ + + tlib_php_request_start(); + populate_functions(); + + tlib_pass_if_int_equal( + "PHP stack depth tracking should be 0 before function call", 0, + NRPRG(php_cur_stack_depth)); + arg = tlib_php_request_eval_expr("0"); + expr = nr_php_call(NULL, "caught", arg); + tlib_pass_if_int_equal( + "PHP stack depth tracking should be 0 after function call", 0, + NRPRG(php_cur_stack_depth)); + tlib_pass_if_not_null("Exception caught so expr should not be null.", expr); + + /* + * Trigger the unwind. + */ + tlib_php_request_eval("newrelic_end_transaction(); "); + tlib_pass_if_int_equal( + "PHP stack depth tracking should be 0 after transaction ends", 0, + NRPRG(php_cur_stack_depth)); + nr_php_zval_free(&expr); + nr_php_zval_free(&arg); + tlib_php_request_end(); + + /* + * call a function and trigger an exception that is caught but the initial + * exception caused two segments to dangle. immediately call another function + * that will trigger cleanup of segments, and stack_depth should be zero. + */ + + tlib_php_request_start(); + populate_functions(); + + tlib_pass_if_int_equal( + "PHP stack depth tracking should be 0 before function call", 0, + NRPRG(php_cur_stack_depth)); + arg = tlib_php_request_eval_expr("0"); + expr = nr_php_call(NULL, "followup", arg); + tlib_pass_if_int_equal( + "PHP stack depth tracking should be 0 after function call", 0, + NRPRG(php_cur_stack_depth)); + tlib_pass_if_not_null("Exception caught so expr should not be null.", expr); + tlib_pass_if_int_equal( + "PHP stack depth tracking should be 0 after transaction", 0, + NRPRG(php_cur_stack_depth)); + + /* + * Trigger the unwind. + */ + tlib_php_request_eval("newrelic_end_transaction(); "); + tlib_pass_if_int_equal( + "PHP stack depth tracking should be 0 after transaction ends", 0, + NRPRG(php_cur_stack_depth)); + nr_php_zval_free(&expr); + nr_php_zval_free(&arg); + tlib_php_request_end(); + + /* + * call a function and trigger an exception that is caught but another + * uncaught exception is thrown and causes two segments to dangle stack_depth + * should be initially stuck at 2 but after unwind, stack_depth should be zero + * again + */ + + tlib_php_request_start(); + populate_functions(); + + tlib_pass_if_int_equal( + "PHP stack depth tracking should be 0 before function call", 0, + NRPRG(php_cur_stack_depth)); + arg = tlib_php_request_eval_expr("0"); + expr = nr_php_call(NULL, "followup_uncaught", arg); + tlib_pass_if_int_equal( + "PHP stack depth tracking should be 2 after function call", 2, + NRPRG(php_cur_stack_depth)); + tlib_pass_if_null("Exception so expr should not be null.", expr); + + /* + * Trigger the unwind. + */ + tlib_php_request_eval("newrelic_end_transaction(); "); + tlib_pass_if_int_equal( + "PHP stack depth tracking should be 0 after transaction ends", 0, + NRPRG(php_cur_stack_depth)); + nr_php_zval_free(&expr); + nr_php_zval_free(&arg); + tlib_php_request_end(); + + /* + * call a function and trigger an exception that is caught then rethrown + * stack_depth should be initially stuck at 2 + * but after unwind, stack_depth should be zero again + */ + + tlib_php_request_start(); + populate_functions(); + tlib_pass_if_int_equal( + "PHP stack depth tracking should be 0 before function call", 0, + NRPRG(php_cur_stack_depth)); + arg = tlib_php_request_eval_expr("0"); + expr = nr_php_call(NULL, "rethrow", arg); + tlib_pass_if_int_equal( + "PHP stack depth tracking should be 2 after function call", 1, + NRPRG(php_cur_stack_depth)); + tlib_pass_if_null("Exception so expr should not be null.", expr); + + /* + * Trigger the unwind. + */ + tlib_php_request_eval("newrelic_end_transaction(); "); + tlib_pass_if_int_equal( + "PHP stack depth tracking should be 0 after transaction ends", 0, + NRPRG(php_cur_stack_depth)); + nr_php_zval_free(&expr); + nr_php_zval_free(&arg); tlib_php_request_end(); } +#endif void test_main(void* p NRUNUSED) { #if defined(ZTS) && !defined(PHP7) @@ -123,6 +341,12 @@ void test_main(void* p NRUNUSED) { tlib_php_engine_create("" PTSRMLS_CC); test_add_segment_metric(TSRMLS_C); test_txn_restart_in_callstack(TSRMLS_C); - test_php_cur_stack_depth(); + test_php_cur_stack_depth(TSRMLS_C); + +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ + test_stack_depth_after_exception(); +#endif + tlib_php_engine_destroy(TSRMLS_C); } diff --git a/agent/tests/test_php_stacked_segment.c b/agent/tests/test_php_stacked_segment.c index 2bc4d7a05..785b9d625 100644 --- a/agent/tests/test_php_stacked_segment.c +++ b/agent/tests/test_php_stacked_segment.c @@ -10,6 +10,7 @@ #include "php_stacked_segment.h" #include "php_globals.h" #include "php_wrapper.h" +#include "util_sleep.h" tlib_parallel_info_t parallel_info = {.suggested_nthreads = -1, .state_size = 0}; @@ -99,6 +100,11 @@ static void test_unwind(TSRMLS_D) { segment = nr_segment_start(NRPRG(txn), NULL, NULL); nr_segment_end(&segment); + /* + * Sleep; otherwise, unwind will dump the short segments and fail. + */ + nr_msleep(500); + /* * Unwind the stacked segment stack. */ @@ -194,6 +200,11 @@ static void test_unwind(TSRMLS_D) { segment = nr_segment_start(NRPRG(txn), NULL, NULL); nr_segment_end(&segment); + /* + * Sleep; otherwise, unwind will dump the short segments and fail. + */ + nr_msleep(500); + /* * Unwind the stacked segment stack. */ diff --git a/agent/tests/test_php_wrapper.c b/agent/tests/test_php_wrapper.c index 3e8fe02d1..3e59714d9 100644 --- a/agent/tests/test_php_wrapper.c +++ b/agent/tests/test_php_wrapper.c @@ -13,6 +13,47 @@ tlib_parallel_info_t parallel_info = {.suggested_nthreads = -1, .state_size = 0}; #if ZEND_MODULE_API_NO >= ZEND_7_3_X_API_NO + +/* + * Set test_before, test_after, test_clean to use a Newrelic global variable. + * Randomly picking NRPRG(drupal_http_request_depth) because it is easy to use, + * a variable, and not used in any other way in this test. + * + */ +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + +NR_PHP_WRAPPER(test_before) { + (void)wraprec; + NRPRG(drupal_http_request_depth) = 10; + + NR_PHP_WRAPPER_CALL; +} +NR_PHP_WRAPPER_END + +NR_PHP_WRAPPER(test_after) { + (void)wraprec; + NRPRG(drupal_http_request_depth) = 20; + + NR_PHP_WRAPPER_CALL; +} +NR_PHP_WRAPPER_END + +NR_PHP_WRAPPER(test_clean) { + (void)wraprec; + if (20 != NRPRG(drupal_http_request_depth)) { + NRPRG(drupal_http_request_depth) = 30; + } + /* + * If 20 = NRPRG(drupal_http_request_depth) it means the after callback was + * called. We should never call clean if the after callback was called. + */ + + NR_PHP_WRAPPER_CALL; +} +NR_PHP_WRAPPER_END +#endif + NR_PHP_WRAPPER(test_add_array) { zval* arg = nr_php_zval_alloc(); @@ -53,31 +94,31 @@ static void test_add_arg(TSRMLS_D) { #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ tlib_php_request_eval("function arg0_def0() { return 4; }" TSRMLS_CC); - nr_php_wrap_user_function_before_after(NR_PSTR("arg0_def0"), test_add_array, - NULL TSRMLS_CC); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("arg0_def0"), test_add_array, NULL, NULL TSRMLS_CC); tlib_php_request_eval("function arg1_def0($a) { return $a; }" TSRMLS_CC); - nr_php_wrap_user_function_before_after(NR_PSTR("arg1_def0"), test_add_array, - NULL TSRMLS_CC); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("arg1_def0"), test_add_array, NULL, NULL TSRMLS_CC); tlib_php_request_eval( "function arg0_def1($a = null) { return $a; }" TSRMLS_CC); - nr_php_wrap_user_function_before_after(NR_PSTR("arg0_def1"), test_add_array, - NULL TSRMLS_CC); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("arg0_def1"), test_add_array, NULL, NULL TSRMLS_CC); tlib_php_request_eval( "function arg1_def1($a, $b = null) { return $b; }" TSRMLS_CC); - nr_php_wrap_user_function_before_after(NR_PSTR("arg1_def1"), test_add_array, - NULL TSRMLS_CC); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("arg1_def1"), test_add_array, NULL, NULL TSRMLS_CC); tlib_php_request_eval( "function arg1_def1_2($a, $b = null) { return $b; }" TSRMLS_CC); - nr_php_wrap_user_function_before_after(NR_PSTR("arg1_def1_2"), - test_add_2_arrays, NULL TSRMLS_CC); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("arg1_def1_2"), test_add_2_arrays, NULL, NULL TSRMLS_CC); tlib_php_request_eval("function splat(...$a) { return $a[0]; }" TSRMLS_CC); - nr_php_wrap_user_function_before_after(NR_PSTR("splat"), test_add_array, - NULL TSRMLS_CC); + nr_php_wrap_user_function_before_after_clean(NR_PSTR("splat"), test_add_array, + NULL, NULL TSRMLS_CC); #else tlib_php_request_eval("function arg0_def0() { return 4; }" TSRMLS_CC); nr_php_wrap_user_function(NR_PSTR("arg0_def0"), test_add_array TSRMLS_CC); @@ -229,12 +270,340 @@ static void test_add_arg(TSRMLS_D) { tlib_php_request_end(); } +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ +static void test_before_after_clean() { + zval* expr = NULL; + zval* arg = NULL; + + /* + * before, after, clean callbacks are all set. + */ + tlib_php_request_start(); + + tlib_php_request_eval( + "function all_set($a) { if (0 == $a) { throw new " + "RuntimeException('Division by zero'); } else return $a; }" TSRMLS_CC); + nr_php_wrap_user_function_before_after_clean(NR_PSTR("all_set"), test_before, + test_after, test_clean); + /* + * pass argument that will not throw exception. + * before/after should be called. + * clean callback should not be called. + */ + arg = tlib_php_request_eval_expr("1" TSRMLS_CC); + expr = nr_php_call(NULL, "all_set", arg); + tlib_pass_if_not_null("Runs fine with no exception.", expr); + tlib_pass_if_zval_type_is("Should have received the arg value.", IS_LONG, + expr); + tlib_pass_if_int_equal("After callback should set value", 20, + NRPRG(drupal_http_request_depth)); + NRPRG(drupal_http_request_depth) = 0; + nr_php_zval_free(&expr); + nr_php_zval_free(&arg); + + /* + * pass argument that will throw exception. + * before should be called after should not be called. + * clean callback should be called. + */ + + arg = tlib_php_request_eval_expr("0" TSRMLS_CC); + expr = nr_php_call(NULL, "all_set", arg); + + tlib_pass_if_null("Exception so expr should be null.", expr); + /* + * Trigger the unwind. + */ + tlib_php_request_eval("newrelic_end_transaction(); "); + tlib_pass_if_int_equal("Clean callback should set value", 30, + NRPRG(drupal_http_request_depth)); + NRPRG(drupal_http_request_depth) = 0; + nr_php_zval_free(&expr); + nr_php_zval_free(&arg); + tlib_php_request_end(); + + /* + * before, after, callbacks are set + */ + + tlib_php_request_start(); + tlib_php_request_eval( + "function before_after($a) { if (0 == $a) { throw new " + "RuntimeException('Division by zero'); } else return $a; }" TSRMLS_CC); + nr_php_wrap_user_function_before_after_clean(NR_PSTR("before_after"), + test_before, test_after, NULL); + + /* + * pass argument that will not throw exception. + * before/after should be called. + * no clean callback + */ + + arg = tlib_php_request_eval_expr("1" TSRMLS_CC); + expr = nr_php_call(NULL, "before_after", arg); + tlib_pass_if_not_null("Runs fine with no exception.", expr); + tlib_pass_if_zval_type_is("Should have received the arg value.", IS_LONG, + expr); + tlib_pass_if_int_equal("After callback should set value", 20, + NRPRG(drupal_http_request_depth)); + NRPRG(drupal_http_request_depth) = 0; + nr_php_zval_free(&expr); + nr_php_zval_free(&arg); + + /* + * pass argument that will throw exception. + * before should be called after should not be called. + * clean should not be called. + */ + arg = tlib_php_request_eval_expr("0" TSRMLS_CC); + expr = nr_php_call(NULL, "before_after", arg); + tlib_pass_if_null("Exception so does not evaluate.", expr); + tlib_php_request_eval("newrelic_end_transaction(); "); + tlib_pass_if_int_equal("Clean callback should not set value", 10, + NRPRG(drupal_http_request_depth)); + tlib_pass_if_int_equal( + "Since there is no clean and after doesn't get called, only the before " + "value persists and does not get cleaned up.", + 10, NRPRG(drupal_http_request_depth)); + NRPRG(drupal_http_request_depth) = 0; + nr_php_zval_free(&expr); + nr_php_zval_free(&arg); + + tlib_php_request_end(); + + /* + * before, clean callbacks are set + */ + + tlib_php_request_start(); + + tlib_php_request_eval( + "function before_clean($a) { if (0 == $a) { throw new " + "RuntimeException('Division by zero'); } else return $a; }" TSRMLS_CC); + nr_php_wrap_user_function_before_after_clean(NR_PSTR("before_clean"), + test_before, NULL, test_clean); + + /* + * pass argument that will not throw exception. + * before should be called. + * clean callback should not be called. + */ + + arg = tlib_php_request_eval_expr("1" TSRMLS_CC); + expr = nr_php_call(NULL, "before_clean", arg); + tlib_pass_if_not_null("Runs fine with no exception.", expr); + tlib_pass_if_zval_type_is("Should have received the arg value.", IS_LONG, + expr); + tlib_pass_if_int_equal("Only before callback should set value", 10, + NRPRG(drupal_http_request_depth)); + NRPRG(drupal_http_request_depth) = 0; + nr_php_zval_free(&expr); + nr_php_zval_free(&arg); + + /* + * pass argument that will throw exception. + * no before callback. + * clean callback should be called. + */ + arg = tlib_php_request_eval_expr("0" TSRMLS_CC); + expr = nr_php_call(NULL, "before_clean", arg); + tlib_pass_if_null("Exception so func does not evaluate.", expr); + /* + * Trigger the unwind. + */ + tlib_php_request_eval("newrelic_end_transaction(); "); + tlib_pass_if_int_equal("Clean callback should set value", 30, + NRPRG(drupal_http_request_depth)); + NRPRG(drupal_http_request_depth) = 0; + nr_php_zval_free(&expr); + nr_php_zval_free(&arg); + tlib_php_request_end(); + + /* + * after, clean callbacks are set + */ + tlib_php_request_start(); + + tlib_php_request_eval( + "function after_clean($a) { if (0 == $a) { throw new " + "RuntimeException('Division by zero'); } else return $a; }" TSRMLS_CC); + nr_php_wrap_user_function_before_after_clean(NR_PSTR("after_clean"), NULL, + test_after, test_clean); + /* + * pass argument that will not throw exception. + * after should be called. + * clean callback should not be called. + */ + arg = tlib_php_request_eval_expr("1" TSRMLS_CC); + expr = nr_php_call(NULL, "after_clean", arg); + tlib_pass_if_not_null("Runs fine with no exception.", expr); + tlib_pass_if_zval_type_is("Should have received the arg value.", IS_LONG, + expr); + tlib_pass_if_int_equal("After callback should set value", 20, + NRPRG(drupal_http_request_depth)); + NRPRG(drupal_http_request_depth) = 0; + nr_php_zval_free(&expr); + nr_php_zval_free(&arg); + + /* + * pass argument that will throw exception. + * before should be called after should not be called. + * clean callback should be called. + */ + arg = tlib_php_request_eval_expr("0" TSRMLS_CC); + expr = nr_php_call(NULL, "after_clean", arg); + tlib_pass_if_null("Exception so returns null.", expr); + tlib_php_request_eval("newrelic_end_transaction(); "); + tlib_pass_if_int_equal( + "After callback should not be called and clean callback should set value", + 30, NRPRG(drupal_http_request_depth)); + NRPRG(drupal_http_request_depth) = 0; + nr_php_zval_free(&expr); + nr_php_zval_free(&arg); + + tlib_php_request_end(); + + /* + * before only callback + */ + tlib_php_request_start(); + + tlib_php_request_eval( + "function before_only($a) { if (0 == $a) { throw new " + "RuntimeException('Division by zero'); } else return $a; }" TSRMLS_CC); + nr_php_wrap_user_function_before_after_clean(NR_PSTR("before_only"), + test_before, NULL, NULL); + /* + * pass argument that will not throw exception. + * before should be called. + * no other callbacks should be called. + */ + arg = tlib_php_request_eval_expr("1" TSRMLS_CC); + expr = nr_php_call(NULL, "before_only", arg); + tlib_pass_if_not_null("Runs fine with no exception.", expr); + tlib_pass_if_zval_type_is("Should have received the arg value.", IS_LONG, + expr); + tlib_pass_if_int_equal("Before callback should set value", 10, + NRPRG(drupal_http_request_depth)); + NRPRG(drupal_http_request_depth) = 0; + nr_php_zval_free(&expr); + nr_php_zval_free(&arg); + + /* + * pass argument that will throw exception. + * before should be called after should not be called. + * clean callback should be called. + */ + arg = tlib_php_request_eval_expr("0" TSRMLS_CC); + expr = nr_php_call(NULL, "before_only", arg); + tlib_pass_if_null("Exception so does not evaluate.", expr); + tlib_php_request_eval("newrelic_end_transaction(); "); + tlib_pass_if_int_equal("Only before would set the value", 10, + NRPRG(drupal_http_request_depth)); + NRPRG(drupal_http_request_depth) = 0; + nr_php_zval_free(&expr); + nr_php_zval_free(&arg); + + tlib_php_request_end(); + + /* + * after only callback + */ + tlib_php_request_start(); + + tlib_php_request_eval( + "function after_only($a) { if (0 == $a) { throw new " + "RuntimeException('Division by zero'); } else return $a; }" TSRMLS_CC); + nr_php_wrap_user_function_before_after_clean(NR_PSTR("after_only"), NULL, + test_after, NULL); + /* + * pass argument that will not throw exception. + * after should be called. + * no other callbacks should be called. + */ + arg = tlib_php_request_eval_expr("1" TSRMLS_CC); + expr = nr_php_call(NULL, "after_only", arg); + tlib_pass_if_not_null("Runs fine with no exception.", expr); + tlib_pass_if_zval_type_is("Should have received the arg value.", IS_LONG, + expr); + tlib_pass_if_int_equal("After callback should set value", 20, + NRPRG(drupal_http_request_depth)); + NRPRG(drupal_http_request_depth) = 0; + nr_php_zval_free(&expr); + nr_php_zval_free(&arg); + + /* + * pass argument that will throw exception. + * before should be called after should not be called. + * clean callback should be called. + */ + arg = tlib_php_request_eval_expr("0" TSRMLS_CC); + expr = nr_php_call(NULL, "after_only", arg); + tlib_pass_if_null("Exception so should be null.", expr); + tlib_php_request_eval("newrelic_end_transaction(); "); + tlib_pass_if_int_equal("No callbacks triggered to set the value", 0, + NRPRG(drupal_http_request_depth)); + NRPRG(drupal_http_request_depth) = 0; + nr_php_zval_free(&expr); + nr_php_zval_free(&arg); + + tlib_php_request_end(); + + /* + * clean only callback + */ + tlib_php_request_start(); + + tlib_php_request_eval( + "function clean_only($a) { if (0 == $a) { throw new " + "RuntimeException('Division by zero'); } else return $a; }" TSRMLS_CC); + nr_php_wrap_user_function_before_after_clean(NR_PSTR("clean_only"), NULL, + NULL, test_clean); + /* + * pass argument that will not throw exception. + * clean should be called. + * no other callbacks should be called. + */ + arg = tlib_php_request_eval_expr("1" TSRMLS_CC); + expr = nr_php_call(NULL, "clean_only", arg); + tlib_pass_if_not_null("Runs fine with no exception.", expr); + tlib_pass_if_zval_type_is("Should have received the arg value.", IS_LONG, + expr); + tlib_pass_if_int_equal("No callback to set value", 0, + NRPRG(drupal_http_request_depth)); + NRPRG(drupal_http_request_depth) = 0; + nr_php_zval_free(&expr); + nr_php_zval_free(&arg); + + /* + * pass argument that will throw exception. + * clean callback should be called. + */ + arg = tlib_php_request_eval_expr("0" TSRMLS_CC); + expr = nr_php_call(NULL, "clean_only", arg); + tlib_pass_if_null("Exception so should be null.", expr); + tlib_php_request_eval("newrelic_end_transaction(); "); + tlib_pass_if_int_equal("Only clean would set the value", 30, + NRPRG(drupal_http_request_depth)); + NRPRG(drupal_http_request_depth) = 0; + nr_php_zval_free(&expr); + nr_php_zval_free(&arg); + + tlib_php_request_end(); +} +#endif + void test_main(void* p NRUNUSED) { #if defined(ZTS) && !defined(PHP7) void*** tsrm_ls = NULL; #endif /* ZTS && !PHP7 */ tlib_php_engine_create("" PTSRMLS_CC); test_add_arg(); +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + test_before_after_clean(); +#endif tlib_php_engine_destroy(TSRMLS_C); } #else /* PHP 7.3 */ diff --git a/axiom/nr_segment.h b/axiom/nr_segment.h index eb96978fd..c611b0748 100644 --- a/axiom/nr_segment.h +++ b/axiom/nr_segment.h @@ -185,19 +185,16 @@ typedef struct _nr_segment_t { #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ -/* - * Because of the access to the segment is now split between functions, we - * need to pass a certain amount of data between the functions that use the - * segment. - */ - nrtime_t txn_start_time; /* To doublecheck the txn is correct when it is time - to add the segment to the txn. */ - void* wraprec; /* wraprec, if one is associated with this segment */ - uint32_t - lineno; /* Keep lineno information. When a function begins, the - zend_execute_data lineno shows the ENTRY point of the function, - when a function ends, the zend_execute_data lineno CHANGES and - shows the EXIT point of the function. */ + /* + * Because of the access to the segment is now split between functions, we + * need to pass a certain amount of data between the functions that use the + * segment. + */ + void* wraprec; /* wraprec, if one is associated with this segment, to reduce + wraprec lookups */ + + void* metadata; /* Persist data for OAPI for when exceptions prevent fcall_end + from being called */ #endif diff --git a/axiom/nr_txn.c b/axiom/nr_txn.c index a0a681112..e739f8912 100644 --- a/axiom/nr_txn.c +++ b/axiom/nr_txn.c @@ -59,10 +59,8 @@ struct _nr_txn_attribute_t { #define NR_TXN_ATTRIBUTE_TRACE_ERROR \ (NR_ATTRIBUTE_DESTINATION_TXN_TRACE | NR_ATTRIBUTE_DESTINATION_ERROR) -#define NR_TXN_ATTR(X, NAME, DESTS) \ - const nr_txn_attribute_t* X = &(nr_txn_attribute_t) { \ - (NAME), (DESTS) \ - } +#define NR_TXN_ATTR(X, NAME, DESTS) \ + const nr_txn_attribute_t* X = &(nr_txn_attribute_t) { (NAME), (DESTS) } NR_TXN_ATTR(nr_txn_request_uri, "request.uri", @@ -107,18 +105,6 @@ NR_TXN_ATTR(nr_txn_http_statuscode, NR_TXN_ATTR(nr_txn_request_user_agent, "request.headers.userAgent", NR_TXN_ATTRIBUTE_TRACE_ERROR); -NR_TXN_ATTR(nr_txn_clm_code_function, - "code.function", - NR_TXN_ATTRIBUTE_SPAN_TRACE_ERROR_EVENT); -NR_TXN_ATTR(nr_txn_clm_code_filepath, - "code.filepath", - NR_TXN_ATTRIBUTE_SPAN_TRACE_ERROR_EVENT); -NR_TXN_ATTR(nr_txn_clm_code_namespace, - "code.namespace", - NR_TXN_ATTRIBUTE_SPAN_TRACE_ERROR_EVENT); -NR_TXN_ATTR(nr_txn_clm_code_lineno, - "code.lineno", - NR_TXN_ATTRIBUTE_SPAN_TRACE_ERROR_EVENT); /* * Deprecated per December 2019 @@ -173,32 +159,6 @@ void nr_txn_set_long_attribute(nrtxn_t* txn, attribute->name, value); } -void nr_txn_attributes_set_string_attribute(nr_attributes_t* attributes, - const nr_txn_attribute_t* attribute, - const char* value) { - if (NULL == attribute) { - return; - } - if (NULL == value) { - return; - } - if ('\0' == value[0]) { - return; - } - nr_attributes_agent_add_string(attributes, attribute->destinations, - attribute->name, value); -} - -void nr_txn_attributes_set_long_attribute(nr_attributes_t* attributes, - const nr_txn_attribute_t* attribute, - long value) { - if (NULL == attribute) { - return; - } - nr_attributes_agent_add_long(attributes, attribute->destinations, - attribute->name, value); -} - /* These sample options are provided for tests. */ const nrtxnopt_t nr_txn_test_options = { .custom_events_enabled = 0, diff --git a/axiom/nr_txn.h b/axiom/nr_txn.h index e95dbe94d..6e6c5fcc6 100644 --- a/axiom/nr_txn.h +++ b/axiom/nr_txn.h @@ -575,24 +575,13 @@ extern const nr_txn_attribute_t* nr_txn_request_user_agent; extern const nr_txn_attribute_t* nr_txn_server_name; extern const nr_txn_attribute_t* nr_txn_response_content_type; extern const nr_txn_attribute_t* nr_txn_response_content_length; -extern const nr_txn_attribute_t* nr_txn_clm_code_filepath; -extern const nr_txn_attribute_t* nr_txn_clm_code_function; -extern const nr_txn_attribute_t* nr_txn_clm_code_namespace; -extern const nr_txn_attribute_t* nr_txn_clm_code_lineno; extern void nr_txn_set_string_attribute(nrtxn_t* txn, const nr_txn_attribute_t* attribute, const char* value); extern void nr_txn_set_long_attribute(nrtxn_t* txn, const nr_txn_attribute_t* attribute, long value); -extern void nr_txn_attributes_set_string_attribute( - nr_attributes_t* attributes, - const nr_txn_attribute_t* attribute, - const char* value); -extern void nr_txn_attributes_set_long_attribute( - nr_attributes_t* attributes, - const nr_txn_attribute_t* attribute, - long value); + /* * Purpose : Return the duration of the transaction. This function will return * 0 if the transaction has not yet finished or if the transaction diff --git a/tests/integration/api/add_custom_parameter/test_add_custom_parameter_nested_caught_exception.php b/tests/integration/api/add_custom_parameter/test_add_custom_parameter_nested_caught_exception.php new file mode 100644 index 000000000..217fc10b2 --- /dev/null +++ b/tests/integration/api/add_custom_parameter/test_add_custom_parameter_nested_caught_exception.php @@ -0,0 +1,198 @@ +getMessage() . "\n"); + } + newrelic_add_custom_parameter("string", "b_str"); + newrelic_add_custom_parameter("int", 7); + newrelic_add_custom_parameter("bool", false); + newrelic_add_custom_parameter("double", 1.5); +} + +function fraction($x) { + time_nanosleep(0, 100000000); + if (!$x) { + throw new RuntimeException('Division by zero'); + } + return 1/$x; +} + +function a() +{ + time_nanosleep(0, 100000000); + echo "Hello\n"; + newrelic_add_custom_parameter("string", "a_str"); + newrelic_add_custom_parameter("int", 7); + newrelic_add_custom_parameter("bool", false); + newrelic_add_custom_parameter("double", 1.5); +}; + +a(); +b(); diff --git a/tests/integration/api/add_custom_parameter/test_add_custom_parameter_nested_happy.php b/tests/integration/api/add_custom_parameter/test_add_custom_parameter_nested_happy.php new file mode 100644 index 000000000..7cd08af99 --- /dev/null +++ b/tests/integration/api/add_custom_parameter/test_add_custom_parameter_nested_happy.php @@ -0,0 +1,192 @@ +getMessage() . "\n"); + } + newrelic_add_custom_parameter("string", "b_str"); + newrelic_add_custom_parameter("int", 7); + newrelic_add_custom_parameter("bool", false); + newrelic_add_custom_parameter("double", 1.5); +} + +function fraction($x) { + time_nanosleep(0, 100000000); + if (!$x) { + return 0; + } + return 1/$x; +} + +function a() +{ + time_nanosleep(0, 100000000); + echo "Hello\n"; + newrelic_add_custom_parameter("string", "a_str"); + newrelic_add_custom_parameter("int", 7); + newrelic_add_custom_parameter("bool", false); + newrelic_add_custom_parameter("double", 1.5); +}; + +a(); +b(); diff --git a/tests/integration/api/add_custom_parameter/test_add_custom_parameter_nested_uncaught_exception.php b/tests/integration/api/add_custom_parameter/test_add_custom_parameter_nested_uncaught_exception.php new file mode 100644 index 000000000..fd64988e4 --- /dev/null +++ b/tests/integration/api/add_custom_parameter/test_add_custom_parameter_nested_uncaught_exception.php @@ -0,0 +1,225 @@ +getMessage() . "\n"); + } + newrelic_add_custom_span_parameter("string", "b_str"); + newrelic_add_custom_span_parameter("int", 7); + newrelic_add_custom_span_parameter("bool", false); + newrelic_add_custom_span_parameter("double", 1.5); +} + +function fraction($x) { + time_nanosleep(0, 100000000); + if (!$x) { + throw new RuntimeException('Division by zero'); + } + return 1/$x; +} + +function a() +{ + time_nanosleep(0, 100000000); + echo "Hello\n"; + newrelic_add_custom_span_parameter("string", "a_str"); + newrelic_add_custom_span_parameter("int", 7); + newrelic_add_custom_span_parameter("bool", false); + newrelic_add_custom_span_parameter("double", 1.5); +}; + +a(); +b(); diff --git a/tests/integration/api/add_custom_span_parameter/test_span_event_parameter_nested_happy.php b/tests/integration/api/add_custom_span_parameter/test_span_event_parameter_nested_happy.php new file mode 100644 index 000000000..2fa9a3561 --- /dev/null +++ b/tests/integration/api/add_custom_span_parameter/test_span_event_parameter_nested_happy.php @@ -0,0 +1,192 @@ +getMessage() . "\n"); + } + newrelic_add_custom_span_parameter("string", "b_str"); + newrelic_add_custom_span_parameter("int", 7); + newrelic_add_custom_span_parameter("bool", false); + newrelic_add_custom_span_parameter("double", 1.5); +} + +function fraction($x) { + time_nanosleep(0, 100000000); + if (!$x) { + return 0; + } + return 1/$x; +} + +function a() +{ + time_nanosleep(0, 100000000); + echo "Hello\n"; + newrelic_add_custom_span_parameter("string", "a_str"); + newrelic_add_custom_span_parameter("int", 7); + newrelic_add_custom_span_parameter("bool", false); + newrelic_add_custom_span_parameter("double", 1.5); +}; + +a(); +b(); diff --git a/tests/integration/api/add_custom_span_parameter/test_span_event_parameter_nested_uncaught_exception.php b/tests/integration/api/add_custom_span_parameter/test_span_event_parameter_nested_uncaught_exception.php new file mode 100644 index 000000000..8f0207c79 --- /dev/null +++ b/tests/integration/api/add_custom_span_parameter/test_span_event_parameter_nested_uncaught_exception.php @@ -0,0 +1,221 @@ +getMessage() . "\n"); + } + newrelic_create_distributed_trace_payload(); +} + +function fraction($x) { + time_nanosleep(0, 100000000); + if (!$x) { + throw new RuntimeException('Division by zero'); + } + return 1/$x; +} + +function a() +{ + time_nanosleep(0, 100000000); + echo "Hello\n"; + newrelic_create_distributed_trace_payload(); +}; + +a(); +b(); diff --git a/tests/integration/api/distributed_trace/newrelic/test_create_payload_nested_happy.php b/tests/integration/api/distributed_trace/newrelic/test_create_payload_nested_happy.php new file mode 100644 index 000000000..283c75fca --- /dev/null +++ b/tests/integration/api/distributed_trace/newrelic/test_create_payload_nested_happy.php @@ -0,0 +1,130 @@ +getMessage() . "\n"); + } + newrelic_create_distributed_trace_payload(); +} + +function fraction($x) { + time_nanosleep(0, 100000000); + if (!$x) { + return 0; + } + return 1/$x; +} + +function a() +{ + time_nanosleep(0, 100000000); + echo "Hello\n"; + newrelic_create_distributed_trace_payload(); +}; + +a(); +b(); diff --git a/tests/integration/api/distributed_trace/newrelic/test_create_payload_nested_uncaught_exception.php b/tests/integration/api/distributed_trace/newrelic/test_create_payload_nested_uncaught_exception.php new file mode 100644 index 000000000..fd47fc64a --- /dev/null +++ b/tests/integration/api/distributed_trace/newrelic/test_create_payload_nested_uncaught_exception.php @@ -0,0 +1,163 @@ +getMessage() . "\n"); + } +newrelic_notice_error(new Exception('Sample Exception b')); +} + +function fraction($x) { + time_nanosleep(0, 100000000); + if (!$x) { + throw new RuntimeException('Division by zero'); + } + return 1/$x; +} + +function a() +{ + time_nanosleep(0, 100000000); + echo "Hello\n"; + newrelic_notice_error(new Exception('Sample Exception a')); +}; + +a(); +b(); diff --git a/tests/integration/api/notice_error/test_notice_error_nested_happy.php b/tests/integration/api/notice_error/test_notice_error_nested_happy.php new file mode 100644 index 000000000..83477e011 --- /dev/null +++ b/tests/integration/api/notice_error/test_notice_error_nested_happy.php @@ -0,0 +1,208 @@ +getMessage() . "\n"); + } + newrelic_notice_error(new Exception('Sample Exception b')); +} + +function fraction($x) { + time_nanosleep(0, 100000000); + if (!$x) { + return 0; + } + return 1/$x; +} + +function a() +{ + time_nanosleep(0, 100000000); + echo "Hello\n"; + newrelic_notice_error(new Exception('Sample Exception a')); +}; + +a(); +b(); diff --git a/tests/integration/api/notice_error/test_notice_error_nested_uncaught_exception.php b/tests/integration/api/notice_error/test_notice_error_nested_uncaught_exception.php new file mode 100644 index 000000000..0dac1b7d1 --- /dev/null +++ b/tests/integration/api/notice_error/test_notice_error_nested_uncaught_exception.php @@ -0,0 +1,212 @@ +=")) { + die("skip: PHP >= 8.0.0 not supported\n"); +} +*/ + /*INI newrelic.transaction_tracer.threshold = 0 */ diff --git a/tests/integration/api/other/test_end_transaction_nested.php8.php b/tests/integration/api/other/test_end_transaction_nested.php8.php new file mode 100644 index 000000000..ee637afb7 --- /dev/null +++ b/tests/integration/api/other/test_end_transaction_nested.php8.php @@ -0,0 +1,106 @@ +", + [ + [ + 0, {}, {}, + [ + "?? start time", "?? end time", "ROOT", "?? root attributes", + [ + [ + "?? start time", "?? end time", "`0", "?? node attributes", + [ + [ + "?? start time", "?? end time", "`1", "?? node attributes", + [ + [ + "?? start time", "?? end time", "`2", "?? node attributes", + [ + [ + "?? start time", "?? end time", "`3", "?? node attributes", + [] + ] + ] + ] + ] + ] + ] + ] + ] + ], + { + "intrinsics": { + "totalTime": "??", + "cpu_time": "??", + "cpu_user_time": "??", + "cpu_sys_time": "??", + "guid": "??", + "sampled": true, + "priority": "??", + "traceId": "??" + } + } + ], + [ + "OtherTransaction\/php__FILE__", + "Custom\/level_2", + "Custom\/level_1", + "Custom\/level_0" + ] + ], + "?? txn guid", + "?? reserved", + "?? force persist", + "?? x-ray sessions", + null + ] + ] +] +*/ +require_once(realpath(dirname(__FILE__)) . '/../../../include/helpers.php'); + +newrelic_add_custom_tracer("level_0"); + +function level_0() { + echo "level_0\n"; +} + +function level_1() { + level_0(); + newrelic_end_transaction(); +} + +function level_2() { + level_1(); +} + +level_2(); diff --git a/tests/integration/api/other/test_set_user_attributes_nested_caught_exception.php b/tests/integration/api/other/test_set_user_attributes_nested_caught_exception.php new file mode 100644 index 000000000..5d6a4e38f --- /dev/null +++ b/tests/integration/api/other/test_set_user_attributes_nested_caught_exception.php @@ -0,0 +1,190 @@ +getMessage() . "\n"); + } + newrelic_set_user_attributes("b_user", "b_account", "b_product"); +} + +function fraction($x) { + time_nanosleep(0, 100000000); + if (!$x) { + throw new RuntimeException('Division by zero'); + } + return 1/$x; +} + +function a() +{ + time_nanosleep(0, 100000000); + echo "Hello\n"; + newrelic_set_user_attributes("a_user", "a_account", "a_product"); +}; + +a(); +b(); diff --git a/tests/integration/api/other/test_set_user_attributes_nested_happy.php b/tests/integration/api/other/test_set_user_attributes_nested_happy.php new file mode 100644 index 000000000..9f2410079 --- /dev/null +++ b/tests/integration/api/other/test_set_user_attributes_nested_happy.php @@ -0,0 +1,184 @@ +getMessage() . "\n"); + } + newrelic_set_user_attributes("b_user", "b_account", "b_product"); +} + +function fraction($x) { + time_nanosleep(0, 100000000); + if (!$x) { + return 0; + } + return 1/$x; +} + +function a() +{ + time_nanosleep(0, 100000000); + echo "Hello\n"; + newrelic_set_user_attributes("a_user", "a_account", "a_product"); +}; + +a(); +b(); diff --git a/tests/integration/api/other/test_set_user_attributes_nested_uncaught_exception.php b/tests/integration/api/other/test_set_user_attributes_nested_uncaught_exception.php new file mode 100644 index 000000000..819ede304 --- /dev/null +++ b/tests/integration/api/other/test_set_user_attributes_nested_uncaught_exception.php @@ -0,0 +1,217 @@ +setFormatter($formatter); + + $logger->pushHandler($stdoutHandler); + + // insert delays between log messages to allow priority sampling + // to resolve that later messages have higher precedence + // since timestamps are only millisecond resolution + // without delays sometimes order in output will reflect + // all having the same timestamp. + $logger->debug("debug"); + usleep(10000); + +} + +test_logging(); \ No newline at end of file From 8764ea62e1ca92be20df6eea97f9a567fceeaf52 Mon Sep 17 00:00:00 2001 From: ZNeumann Date: Thu, 26 Jan 2023 09:32:38 -0700 Subject: [PATCH 14/56] fix(agent): move globals that utilized the call stack into a global stack when using OAPI (#582) Symfony1 instrumentation utilizes the call stack, but should never be run with PHP8+. --- agent/lib_doctrine2.c | 29 ++++++++++ agent/lib_predis.c | 67 +++++++++++++++++++++- agent/php_newrelic.h | 7 +++ agent/php_rinit.c | 16 ++++++ agent/php_rshutdown.c | 5 ++ tests/integration/predis/test_pipeline.php | 23 +++++--- 6 files changed, 137 insertions(+), 10 deletions(-) diff --git a/agent/lib_doctrine2.c b/agent/lib_doctrine2.c index 05bdf54a0..6c7ef7d7e 100644 --- a/agent/lib_doctrine2.c +++ b/agent/lib_doctrine2.c @@ -49,10 +49,31 @@ NR_PHP_WRAPPER(nr_doctrine2_cache_dql) { NR_PHP_WRAPPER_CALL; +/* If not using OAPI, we can simply free the value after the NR_PHP_WRAPPER_CALL. + * Otherwise, we need an "after function" to do the freeing */ +#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ + || defined OVERWRITE_ZEND_EXECUTE_DATA nr_free(NRPRG(doctrine_dql)); +#endif /* not OAPI */ } NR_PHP_WRAPPER_END +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA +NR_PHP_WRAPPER(nr_doctrine2_cache_dql_clean) { + (void)wraprec; + nr_free(NRPRG(doctrine_dql)); +} +NR_PHP_WRAPPER_END + +NR_PHP_WRAPPER(nr_doctrine2_cache_dql_after) { + (void)wraprec; + nr_free(NRPRG(doctrine_dql)); +} +NR_PHP_WRAPPER_END +#endif /* OAPI */ + + nr_slowsqls_labelled_query_t* nr_doctrine2_lookup_input_query(TSRMLS_D) { nr_slowsqls_labelled_query_t* query = NULL; const char* dql = NRPRG(doctrine_dql); @@ -74,6 +95,14 @@ nr_slowsqls_labelled_query_t* nr_doctrine2_lookup_input_query(TSRMLS_D) { } void nr_doctrine2_enable(TSRMLS_D) { +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + nr_php_wrap_user_function_before_after_clean(NR_PSTR("Doctrine\\ORM\\Query::_doExecute"), + nr_doctrine2_cache_dql, + nr_doctrine2_cache_dql_after, + nr_doctrine2_cache_dql_clean); +#else nr_php_wrap_user_function(NR_PSTR("Doctrine\\ORM\\Query::_doExecute"), nr_doctrine2_cache_dql TSRMLS_CC); +#endif /* OAPI */ } diff --git a/agent/lib_predis.c b/agent/lib_predis.c index 1a194f70a..b412d212f 100644 --- a/agent/lib_predis.c +++ b/agent/lib_predis.c @@ -544,13 +544,19 @@ NR_PHP_WRAPPER(nr_predis_connection_readResponse) { * have set predis_ctx to a non-NULL async context, so we use that to add an * async context to the datastore node. */ - if (NRPRG(predis_ctx)) { +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + char* ctx = (char *)nr_stack_get_top(&NRPRG(predis_ctxs)); +#else + char* ctx = NRPRG(predis_ctx); +#endif /* OAPI */ + if (ctx) { /* * Since we need a unique async context for each element within the * pipeline, we'll concatenate the object ID onto the base context name * generated in the executePipeline() instrumentation. */ - async_context = nr_formatf("%s." NR_UINT64_FMT, NRPRG(predis_ctx), index); + async_context = nr_formatf("%s." NR_UINT64_FMT, ctx, index); } segment = nr_segment_start(NRPRG(txn), NULL, async_context); @@ -684,7 +690,6 @@ NR_PHP_WRAPPER(nr_predis_client_construct) { NR_PHP_WRAPPER_END NR_PHP_WRAPPER(nr_predis_pipeline_executePipeline) { - char* prev_predis_ctx; (void)wraprec; @@ -698,19 +703,51 @@ NR_PHP_WRAPPER(nr_predis_pipeline_executePipeline) { * We'll save any existing context just in case this is a nested pipeline. */ +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + nr_stack_push(&NRPRG(predis_ctxs), nr_formatf("Predis #" NR_TIME_FMT, nr_get_time())); +#else + char* prev_predis_ctx; prev_predis_ctx = NRPRG(predis_ctx); NRPRG(predis_ctx) = nr_formatf("Predis #" NR_TIME_FMT, nr_get_time()); +#endif /* OAPI */ NR_PHP_WRAPPER_CALL; /* * Restore any previous context on the way out. + * + * If not using OAPI, we can simply free the value after the NR_PHP_WRAPPER_CALL. + * Otherwise, we need an "after function" to do the freeing */ +#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ + || defined OVERWRITE_ZEND_EXECUTE_DATA nr_free(NRPRG(predis_ctx)); NRPRG(predis_ctx) = prev_predis_ctx; +#endif /* not OAPI */ +} +NR_PHP_WRAPPER_END + +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA +static void predis_executePipeline_handle_stack() { + char* predis_ctx = (char *)nr_stack_pop(&NRPRG(predis_ctxs)); + nr_free(predis_ctx); +} + +NR_PHP_WRAPPER(nr_predis_pipeline_executePipeline_after) { + (void)wraprec; + predis_executePipeline_handle_stack(); } NR_PHP_WRAPPER_END +NR_PHP_WRAPPER(nr_predis_pipeline_executePipeline_clean) { + (void)wraprec; + predis_executePipeline_handle_stack(); +} +NR_PHP_WRAPPER_END +#endif /* OAPI */ + NR_PHP_WRAPPER(nr_predis_webdisconnection_executeCommand) { char* operation = NULL; zval* command_obj = NULL; @@ -755,6 +792,29 @@ void nr_predis_enable(TSRMLS_D) { * Instrument the pipeline classes that are bundled with Predis so that we * correctly set up async contexts. */ +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("Predis\\Pipeline\\Pipeline::executePipeline"), + nr_predis_pipeline_executePipeline, + nr_predis_pipeline_executePipeline_after, + nr_predis_pipeline_executePipeline_clean); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("Predis\\Pipeline\\Atomic::executePipeline"), + nr_predis_pipeline_executePipeline, + nr_predis_pipeline_executePipeline_after, + nr_predis_pipeline_executePipeline_clean); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("Predis\\Pipeline\\ConnectionErrorProof::executePipeline"), + nr_predis_pipeline_executePipeline, + nr_predis_pipeline_executePipeline_after, + nr_predis_pipeline_executePipeline_clean); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("Predis\\Pipeline\\FireAndForget::executePipeline"), + nr_predis_pipeline_executePipeline, + nr_predis_pipeline_executePipeline_after, + nr_predis_pipeline_executePipeline_clean); +#else nr_php_wrap_user_function( NR_PSTR("Predis\\Pipeline\\Pipeline::executePipeline"), nr_predis_pipeline_executePipeline TSRMLS_CC); @@ -767,6 +827,7 @@ void nr_predis_enable(TSRMLS_D) { nr_php_wrap_user_function( NR_PSTR("Predis\\Pipeline\\FireAndForget::executePipeline"), nr_predis_pipeline_executePipeline TSRMLS_CC); +#endif /* OAPI */ /* * Instrument Webdis connections, since they don't use the same diff --git a/agent/php_newrelic.h b/agent/php_newrelic.h index 63a3872b7..87c4860a2 100644 --- a/agent/php_newrelic.h +++ b/agent/php_newrelic.h @@ -561,7 +561,14 @@ nrapp_t* app; /* The application used in the last attempt to initialize a nrtxn_t* txn; /* The all-important transaction pointer */ +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA +nr_stack_t predis_ctxs; /* Without OAPI, we are able to utilize the call + stack to keep track of the current predis_ctx. + WIth OAPI, we must track this manually */ +#else char* predis_ctx; /* The current Predis pipeline context name, if any */ +#endif nr_hashmap_t* predis_commands; /* diff --git a/agent/php_rinit.c b/agent/php_rinit.c index 158569d9e..434ff17f0 100644 --- a/agent/php_rinit.c +++ b/agent/php_rinit.c @@ -24,6 +24,17 @@ static void nr_php_datastore_instance_destroy( nr_datastore_instance_destroy(&instance); } +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA +/* OAPI global stacks (as opposed to call stack used previously) + * need to have a dtor set so that when we free it + * during rshutdown, all elements are properly freed */ +static void str_stack_dtor(void* e, NRUNUSED void* d) { + char* str = (char*)e; + nr_free(str); +} +#endif + #ifdef TAGS void zm_activate_newrelic(void); /* ctags landing pad only */ #endif @@ -101,6 +112,11 @@ PHP_RINIT_FUNCTION(newrelic) { "(^([a-z_-]+[_-])([0-9a-f_.]+[0-9][0-9a-f.]+)(_{0,1}.*)$|(.*))", NR_REGEX_CASELESS, 0); +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + nr_stack_init(&NRPRG(predis_ctxs), NR_STACK_DEFAULT_CAPACITY); + NRPRG(predis_ctxs).dtor = str_stack_dtor; +#endif NRPRG(mysql_last_conn) = NULL; NRPRG(pgsql_last_conn) = NULL; NRPRG(datastore_connections) = nr_hashmap_create( diff --git a/agent/php_rshutdown.c b/agent/php_rshutdown.c index 1ba94bfad..fec40b3d2 100644 --- a/agent/php_rshutdown.c +++ b/agent/php_rshutdown.c @@ -113,7 +113,12 @@ int nr_php_post_deactivate(void) { nr_free(NRPRG(pgsql_last_conn)); nr_hashmap_destroy(&NRPRG(datastore_connections)); +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + nr_stack_destroy_fields(&NRPRG(predis_ctxs)); +#else nr_free(NRPRG(predis_ctx)); +#endif /* OAPI */ nr_hashmap_destroy(&NRPRG(predis_commands)); #if ZEND_MODULE_API_NO >= ZEND_7_4_X_API_NO diff --git a/tests/integration/predis/test_pipeline.php b/tests/integration/predis/test_pipeline.php index ce7eda678..5ef06dcb3 100644 --- a/tests/integration/predis/test_pipeline.php +++ b/tests/integration/predis/test_pipeline.php @@ -20,11 +20,11 @@ [ [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/all"}, [1, "??", "??", "??", "??", "??"]], [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/all"}, [12, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/allOther"}, [12, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/Redis/all"}, [12, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/Redis/allOther"}, [12, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/instance/Redis/__HOST__/6379"}, [12, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/all"}, [13, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/allOther"}, [13, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/Redis/all"}, [13, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/Redis/allOther"}, [13, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/instance/Redis/__HOST__/6379"}, [13, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Redis/del"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Redis/del", "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], @@ -40,9 +40,9 @@ [{"name":"Datastore/operation/Redis/mget"}, [2, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Redis/mget", "scope":"OtherTransaction/php__FILE__"}, [2, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/operation/Redis/ping"}, [2, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/operation/Redis/ping"}, [3, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Redis/ping", - "scope":"OtherTransaction/php__FILE__"}, [2, "??", "??", "??", "??", "??"]], + "scope":"OtherTransaction/php__FILE__"}, [3, "??", "??", "??", "??", "??"]], [{"name":"OtherTransaction/all"}, [1, "??", "??", "??", "??", "??"]], [{"name":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], [{"name":"OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], @@ -102,6 +102,15 @@ function test_pipeline() { $pipe->mget('does_not_exist', $key); $replies = $pipe->execute(); + /* method 3 (exception) */ + $replies = $client->pipeline(function($pipe) { + try { + $pipe->execute(); + } catch (Exception $e) { + $pipe->ping(); + } + }); + $client->del($key); $client->quit(); } From 0051e67c19a421f338097a52ca13a79d0f35d0d2 Mon Sep 17 00:00:00 2001 From: ZNeumann Date: Thu, 26 Jan 2023 10:18:44 -0700 Subject: [PATCH 15/56] feat(agent): Remove php_vm.c overwriting opcodes when using OAPI (#559) Occasionally, we want to instrument functions called with call_user_func_array, when we otherwise wouldn't instrument that function. To do this, we instrument the internal function call_user_func_array and check for contexts in which we want to begin instrumentation. However, call_user_func_array can be inlined by the zend compiler. Previously, we were detecting inlined calls by overwriting the DO_FCALL opcode. With OAPI, we no longer want to touch the opcodes and are instead checking for the inlined calls during observer_do_fcall. --- agent/fw_drupal.c | 77 +++++++++- agent/fw_wordpress.c | 133 +++++++++++++++++- agent/php_execute.c | 74 ++++++++++ agent/php_newrelic.h | 22 +++ agent/php_rinit.c | 15 ++ agent/php_rshutdown.c | 11 ++ agent/php_vm.c | 9 +- axiom/util_stack.h | 2 +- .../integration/frameworks/drupal/skipif.inc | 9 ++ .../drupal/test_module_invoke_all.php | 90 ++++++++++++ .../drupal/test_module_invoke_all.php8.php | 90 ++++++++++++ .../test_wordpress_apply_filters.php | 82 +++++++++++ .../test_wordpress_apply_filters.php8.php | 79 +++++++++++ .../wordpress/test_wordpress_do_action.php | 76 ++++++++++ .../test_wordpress_do_action.php8.php | 73 ++++++++++ 15 files changed, 829 insertions(+), 13 deletions(-) create mode 100644 tests/integration/frameworks/drupal/skipif.inc create mode 100644 tests/integration/frameworks/drupal/test_module_invoke_all.php create mode 100644 tests/integration/frameworks/drupal/test_module_invoke_all.php8.php create mode 100644 tests/integration/frameworks/wordpress/test_wordpress_apply_filters.php create mode 100644 tests/integration/frameworks/wordpress/test_wordpress_apply_filters.php8.php create mode 100644 tests/integration/frameworks/wordpress/test_wordpress_do_action.php create mode 100644 tests/integration/frameworks/wordpress/test_wordpress_do_action.php8.php diff --git a/agent/fw_drupal.c b/agent/fw_drupal.c index 3563639a8..3b3535cff 100644 --- a/agent/fw_drupal.c +++ b/agent/fw_drupal.c @@ -454,7 +454,22 @@ static void nr_drupal_wrap_hook_within_module_invoke_all( * module_invoke_all(), the drupal_module_invoke_all_hook global should be * available. */ - if (NULL == NRPRG(drupal_module_invoke_all_hook)) { +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + zval* curr_hook = (zval*)nr_stack_get_top(&NRPRG(drupal_module_invoke_all_hooks)); + if (!nr_php_is_zval_non_empty_string(curr_hook)) { + nrl_verbosedebug(NRL_FRAMEWORK, + "%s: cannot extract hook name from global stack", + __func__); + return; + } + char* hook_name = Z_STRVAL_P(curr_hook); + size_t hook_len = Z_STRLEN_P(curr_hook); +#else + char* hook_name = NRPRG(drupal_module_invoke_all_hook); + size_t hook_len = NRPRG(drupal_module_invoke_all_hook_len); +#endif + if (NULL == hook_name) { nrl_verbosedebug(NRL_FRAMEWORK, "%s: cannot extract module name without knowing the hook", __func__); @@ -462,8 +477,8 @@ static void nr_drupal_wrap_hook_within_module_invoke_all( } rv = module_invoke_all_parse_module_and_hook( - &module, &module_len, NRPRG(drupal_module_invoke_all_hook), - NRPRG(drupal_module_invoke_all_hook_len), func); + &module, &module_len, hook_name, + hook_len, func); if (NR_SUCCESS != rv) { return; @@ -471,8 +486,8 @@ static void nr_drupal_wrap_hook_within_module_invoke_all( nr_php_wrap_user_function_drupal( nr_php_function_name(func), nr_php_function_name_length(func), module, - module_len, NRPRG(drupal_module_invoke_all_hook), - NRPRG(drupal_module_invoke_all_hook_len) TSRMLS_CC); + module_len, hook_name, + hook_len TSRMLS_CC); nr_free(module); } @@ -717,6 +732,49 @@ NR_PHP_WRAPPER(nr_drupal_wrap_module_invoke) { } NR_PHP_WRAPPER_END +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA +NR_PHP_WRAPPER(nr_drupal_wrap_module_invoke_all_before) { + (void)wraprec; + zval* hook_copy = NULL; + + NR_PHP_WRAPPER_REQUIRE_FRAMEWORK(NR_FW_DRUPAL); + + hook_copy = nr_php_arg_get(1, NR_EXECUTE_ORIG_ARGS); + if (nr_php_is_zval_non_empty_string(hook_copy)) { + nr_stack_push(&NRPRG(drupal_module_invoke_all_hooks), + hook_copy); + nr_stack_push(&NRPRG(drupal_module_invoke_all_states), + (void*)!NULL); + } else { + nr_stack_push(&NRPRG(drupal_module_invoke_all_states), + NULL); + } +} +NR_PHP_WRAPPER_END + +static void module_invoke_all_clean_stacks() { + if((bool)nr_stack_pop(&NRPRG(drupal_module_invoke_all_states))) { + zval* hook_copy = nr_stack_pop(&NRPRG(drupal_module_invoke_all_hooks)); + nr_php_arg_release(&hook_copy); + } +} + +NR_PHP_WRAPPER(nr_drupal_wrap_module_invoke_all_after) { + (void)wraprec; + module_invoke_all_clean_stacks(); +} +NR_PHP_WRAPPER_END + +NR_PHP_WRAPPER(nr_drupal_wrap_module_invoke_all_clean) { + NR_UNUSED_SPECIALFN; + NR_UNUSED_FUNC_RETURN_VALUE; + (void)wraprec; + module_invoke_all_clean_stacks(); +} +NR_PHP_WRAPPER_END + +#else NR_PHP_WRAPPER(nr_drupal_wrap_module_invoke_all) { zval* hook = NULL; char* prev_hook = NULL; @@ -748,6 +806,7 @@ NR_PHP_WRAPPER(nr_drupal_wrap_module_invoke_all) { nr_php_arg_release(&hook); } NR_PHP_WRAPPER_END +#endif /* OAPI */ /* * Enable the drupal instrumentation. @@ -778,8 +837,16 @@ void nr_drupal_enable(TSRMLS_D) { if (NRINI(drupal_modules)) { nr_php_wrap_user_function(NR_PSTR("module_invoke"), nr_drupal_wrap_module_invoke TSRMLS_CC); +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + nr_php_wrap_user_function_before_after_clean(NR_PSTR("module_invoke_all"), + nr_drupal_wrap_module_invoke_all_before, + nr_drupal_wrap_module_invoke_all_after, + nr_drupal_wrap_module_invoke_all_clean); +#else nr_php_wrap_user_function(NR_PSTR("module_invoke_all"), nr_drupal_wrap_module_invoke_all TSRMLS_CC); +#endif /* OAPI */ nr_php_wrap_user_function(NR_PSTR("view::execute"), nr_drupal_wrap_view_execute TSRMLS_CC); } diff --git a/agent/fw_wordpress.c b/agent/fw_wordpress.c index 84c440209..4236414ef 100644 --- a/agent/fw_wordpress.c +++ b/agent/fw_wordpress.c @@ -285,7 +285,13 @@ static char* nr_wordpress_plugin_from_function(zend_function* func TSRMLS_DC) { nrl_verbosedebug(NRL_FRAMEWORK, "Wordpress: cannot determine plugin name:" " missing filename, tag=" NRP_FMT, - NRP_PHP(NRPRG(wordpress_tag))); +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + NRP_PHP((char *)nr_stack_get_top(&NRPRG(wordpress_tags))) +#else + NRP_PHP(NRPRG(wordpress_tag)) +#endif /* OAPI */ + ); return NULL; } filename_len = nr_strlen(filename); @@ -327,13 +333,25 @@ static char* nr_wordpress_plugin_from_function(zend_function* func TSRMLS_DC) { "Wordpress: detected Wordpress Core filename, functions " "will be anonymized:" "tag=" NRP_FMT " filename=" NRP_FMT, - NRP_PHP(NRPRG(wordpress_tag)), NRP_FILENAME(filename)); +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + NRP_PHP((char *)nr_stack_get_top(&NRPRG(wordpress_tags))), +#else + NRP_PHP(NRPRG(wordpress_tag)), +#endif /* OAPI */ + NRP_FILENAME(filename)); /* We can discard the plugin value, since these functions are anonymous. */ } else { nrl_verbosedebug(NRL_FRAMEWORK, "Wordpress: cannot determine plugin name:" " unexpected format, tag=" NRP_FMT " filename=" NRP_FMT, - NRP_PHP(NRPRG(wordpress_tag)), NRP_FILENAME(filename)); +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + NRP_PHP((char *)nr_stack_get_top(&NRPRG(wordpress_tags))), +#else + NRP_PHP(NRPRG(wordpress_tag)), +#endif /* OAPI */ + NRP_FILENAME(filename)); } nr_free(plugin); @@ -361,7 +379,13 @@ NR_PHP_WRAPPER(nr_wordpress_wrap_hook) { */ NR_PHP_WRAPPER_REQUIRE_FRAMEWORK(NR_FW_WORDPRESS); +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + char* tag = nr_stack_get_top(&NRPRG(wordpress_tags)); + if ((0 == NRINI(wordpress_hooks)) || (NULL == tag)) { +#else if ((0 == NRINI(wordpress_hooks)) || (NULL == NRPRG(wordpress_tag))) { +#endif /* OAPI */ NR_PHP_WRAPPER_LEAVE; } func = nr_php_execute_function(NR_EXECUTE_ORIG_ARGS TSRMLS_CC); @@ -369,8 +393,13 @@ NR_PHP_WRAPPER(nr_wordpress_wrap_hook) { NR_PHP_WRAPPER_CALL; +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + nr_wordpress_create_metric(auto_segment, NR_WORDPRESS_HOOK_PREFIX, tag); +#else nr_wordpress_create_metric(auto_segment, NR_WORDPRESS_HOOK_PREFIX, NRPRG(wordpress_tag)); +#endif /* OAPI */ nr_wordpress_create_metric(auto_segment, NR_WORDPRESS_PLUGIN_PREFIX, plugin); } NR_PHP_WRAPPER_END @@ -387,8 +416,14 @@ static void nr_wordpress_call_user_func_array(zend_function* func, * function, we're instrumenting hooks, and WordPress is currently executing * hooks (denoted by the wordpress_tag being set). */ +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + if ((NR_FW_WORDPRESS != NRPRG(current_framework)) + || (0 == NRINI(wordpress_hooks)) || (NULL == nr_stack_get_top(&NRPRG(wordpress_tags)))) { +#else if ((NR_FW_WORDPRESS != NRPRG(current_framework)) || (0 == NRINI(wordpress_hooks)) || (NULL == NRPRG(wordpress_tag))) { +#endif /* OAPI */ return; } @@ -471,12 +506,20 @@ NR_PHP_WRAPPER(nr_wordpress_exec_handle_tag) { tag = nr_php_arg_get(1, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); if (1 == nr_php_is_zval_non_empty_string(tag) - || (0 != NRINI(wordpress_hooks))) { + && (0 != NRINI(wordpress_hooks))) { /* * Our general approach here is to set the wordpress_tag global, then let * the call_user_func_array instrumentation take care of actually timing * the hooks by checking if it's set. */ +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + nr_stack_push(&NRPRG(wordpress_tags), nr_wordpress_clean_tag(tag TSRMLS_CC)); + nr_stack_push(&NRPRG(wordpress_tag_states), (void*)!NULL); + } else { + nr_stack_push(&NRPRG(wordpress_tag_states), NULL); + } +#else char* old_tag = NRPRG(wordpress_tag); NRPRG(wordpress_tag) = nr_wordpress_clean_tag(tag TSRMLS_CC); @@ -486,6 +529,7 @@ NR_PHP_WRAPPER(nr_wordpress_exec_handle_tag) { } else { NR_PHP_WRAPPER_CALL; } +#endif /* OAPI */ nr_php_arg_release(&tag); } @@ -543,7 +587,6 @@ static void nr_wordpress_name_the_wt(const zval* tag, */ NR_PHP_WRAPPER(nr_wordpress_apply_filters) { zval* tag = NULL; - zval** retval_ptr = NR_GET_RETURN_VALUE_PTR; NR_UNUSED_SPECIALFN; (void)wraprec; @@ -559,6 +602,18 @@ NR_PHP_WRAPPER(nr_wordpress_apply_filters) { * the call_user_func_array instrumentation take care of actually timing * the hooks by checking if it's set. */ +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + nr_stack_push(&NRPRG(wordpress_tags), nr_wordpress_clean_tag(tag TSRMLS_CC)); + nr_stack_push(&NRPRG(wordpress_tag_states), (void*)!NULL); + } else { + // Keep track of whether we pushed to NRPRG(wordpress_tags) + nr_stack_push(&NRPRG(wordpress_tag_states), NULL); + } + } else { + nr_stack_push(&NRPRG(wordpress_tag_states), NULL); + } +#else char* old_tag = NRPRG(wordpress_tag); NRPRG(wordpress_tag) = nr_wordpress_clean_tag(tag TSRMLS_CC); @@ -569,16 +624,83 @@ NR_PHP_WRAPPER(nr_wordpress_apply_filters) { NR_PHP_WRAPPER_CALL; } + zval** retval_ptr = NR_GET_RETURN_VALUE_PTR; nr_wordpress_name_the_wt(tag, retval_ptr TSRMLS_CC); } else { NR_PHP_WRAPPER_CALL; } +#endif /* OAPI */ nr_php_arg_release(&tag); } NR_PHP_WRAPPER_END +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA +static void clean_wordpress_tag_stack() { + if ((bool)nr_stack_pop(&NRPRG(wordpress_tag_states))) { + char* cleaned_tag = nr_stack_pop(&NRPRG(wordpress_tags)); + nr_free(cleaned_tag); + } +} + +NR_PHP_WRAPPER(nr_wordpress_handle_tag_stack_after) { + (void)wraprec; + if (0 != NRINI(wordpress_hooks)) { + clean_wordpress_tag_stack(); + } +} +NR_PHP_WRAPPER_END + +NR_PHP_WRAPPER(nr_wordpress_handle_tag_stack_clean) { + NR_UNUSED_SPECIALFN; + NR_UNUSED_FUNC_RETURN_VALUE; + (void)wraprec; + if (0 != NRINI(wordpress_hooks)) { + clean_wordpress_tag_stack(); + } +} +NR_PHP_WRAPPER_END + +NR_PHP_WRAPPER(nr_wordpress_apply_filters_after) { + /* using nr_php_get_user_func_arg() so that we don't perform another copy + * when all we want to do is check the string length */ + zval* tag = nr_php_get_user_func_arg(1, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + if (1 == nr_php_is_zval_non_empty_string(tag)) { + zval** retval_ptr = NR_GET_RETURN_VALUE_PTR; + nr_wordpress_name_the_wt(tag, retval_ptr TSRMLS_CC); + } + + nr_wordpress_handle_tag_stack_after(NR_SPECIALFNPTR_ORIG_ARGS); +} +NR_PHP_WRAPPER_END + +#endif /* OAPI */ + void nr_wordpress_enable(TSRMLS_D) { +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + nr_php_wrap_user_function_before_after_clean(NR_PSTR("apply_filters"), + nr_wordpress_apply_filters, + nr_wordpress_apply_filters_after, + nr_wordpress_handle_tag_stack_clean); + + nr_php_wrap_user_function_before_after_clean(NR_PSTR("apply_filters_ref_array"), + nr_wordpress_exec_handle_tag, + nr_wordpress_handle_tag_stack_after, + nr_wordpress_handle_tag_stack_clean); + + nr_php_wrap_user_function_before_after_clean(NR_PSTR("do_action"), + nr_wordpress_exec_handle_tag, + nr_wordpress_handle_tag_stack_after, + nr_wordpress_handle_tag_stack_clean); + + nr_php_wrap_user_function_before_after_clean(NR_PSTR("do_action_ref_array"), + nr_wordpress_exec_handle_tag, + nr_wordpress_handle_tag_stack_after, + nr_wordpress_handle_tag_stack_clean); + +#else nr_php_wrap_user_function(NR_PSTR("apply_filters"), nr_wordpress_apply_filters TSRMLS_CC); @@ -590,6 +712,7 @@ void nr_wordpress_enable(TSRMLS_D) { nr_php_wrap_user_function(NR_PSTR("do_action_ref_array"), nr_wordpress_exec_handle_tag TSRMLS_CC); +#endif /* OAPI */ nr_php_add_call_user_func_array_pre_callback( nr_wordpress_call_user_func_array TSRMLS_CC); diff --git a/agent/php_execute.c b/agent/php_execute.c index 65dcd9b5d..e1c5493ff 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -1919,6 +1919,68 @@ void php_observer_handle_exception_hook(zval* exception, zval* exception_this) { php_observer_set_uncaught_exception_globals(exception, exception_this); } +static void nr_php_observer_attempt_call_cufa_handler(NR_EXECUTE_PROTO) { + NR_UNUSED_FUNC_RETURN_VALUE; + if (NULL == execute_data->prev_execute_data) { + nrl_verbosedebug(NRL_AGENT, "%s: cannot get previous execute data", __func__); + return; + } + if (NULL == execute_data->prev_execute_data->opline) { + nrl_verbosedebug(NRL_AGENT, "%s: cannot get previous opline", __func__); + return; + } + + /* + * COPIED Comment from php_vm.c: + * To actually determine whether this is a call_user_func_array() call we + * have to look at one of the previous opcodes. ZEND_DO_FCALL will never be + * the first opcode in an op array -- minimally, there is always at least a + * ZEND_INIT_FCALL before it -- so this is safe. + * + * When PHP 7+ flattens a call_user_func_array() call into direct opcodes, it + * uses ZEND_SEND_ARRAY to send the arguments in a single opline, and that + * opcode is the opcode before the ZEND_DO_FCALL. Therefore, if we see + * ZEND_SEND_ARRAY, we know it's call_user_func_array(). The relevant code + * can be found at: + * https://github.com/php/php-src/blob/php-7.0.19/Zend/zend_compile.c#L3082-L3098 + * https://github.com/php/php-src/blob/php-7.1.5/Zend/zend_compile.c#L3564-L3580 + * + * In PHP 8, sometimes a ZEND_CHECK_UNDEF_ARGS opcode is added after the call + * to ZEND_SEND_ARRAY and before ZEND_DO_FCALL so we need to sometimes look + * back two opcodes instead of just one. + * + * Note that this heuristic will fail if the Zend Engine ever starts + * compiling inlined call_user_func_array() calls differently. PHP 7.2 made + * a change, but it only optimized array_slice() calls, which as an internal + * function won't get this far anyway.) We can disable this behaviour by + * setting the ZEND_COMPILE_NO_BUILTINS compiler flag, but since that will + * cause additional performance overhead, this should be considered a last + * resort. + */ + const zend_op* prev_opline = execute_data->prev_execute_data->opline - 1; + if (ZEND_CHECK_UNDEF_ARGS == prev_opline->opcode) { + prev_opline = execute_data->prev_execute_data->opline - 2; + } + if (ZEND_SEND_ARRAY == prev_opline->opcode) { + if (UNEXPECTED((NULL == execute_data->func))) { + nrl_verbosedebug(NRL_AGENT, "%s: cannot get current function", __func__); + return; + } + if (UNEXPECTED(NULL == execute_data->prev_execute_data->func)) { + nrl_verbosedebug(NRL_AGENT, "%s: cannot get previous function", __func__); + return; + } + + if (UNEXPECTED(NULL == execute_data->prev_execute_data->func->common.function_name)) { + nrl_verbosedebug(NRL_AGENT, "%s: cannot get previous function name", __func__); + return; + } + + nr_php_call_user_func_array_handler(NRPRG(cufa_callback), execute_data->func, + execute_data->prev_execute_data); + } +} + static void nr_php_instrument_func_begin(NR_EXECUTE_PROTO) { nr_segment_t* segment = NULL; nruserfn_t* wraprec = NULL; @@ -1941,6 +2003,18 @@ static void nr_php_instrument_func_begin(NR_EXECUTE_PROTO) { filename TSRMLS_CC); return; } + if (UNEXPECTED(NULL != NRPRG(cufa_callback))) { + /* + * For PHP 7+, call_user_func_array() is flattened into an inline by default. Because + * of this, we must check the opcodes set to see whether we are calling it flattened. + * If we have a cufa callback, we want to call that here. This will create the wraprec + * for the user function we want to instrument and thus must be called before we search + * the wraprecs + * + * For non-OAPI, this is handled in php_vm.c by overwriting the ZEND_DO_FCALL opcode. + */ + nr_php_observer_attempt_call_cufa_handler(NR_EXECUTE_ORIG_ARGS); + } wraprec = nr_php_get_wraprec(execute_data->func); /* * If there is custom instrumentation or tt detail is more than 0, start the diff --git a/agent/php_newrelic.h b/agent/php_newrelic.h index 87c4860a2..49584bab1 100644 --- a/agent/php_newrelic.h +++ b/agent/php_newrelic.h @@ -405,9 +405,20 @@ nrframework_t current_framework; /* Current request framework (forced or detected) */ int framework_version; /* Current framework version */ +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA +/* Without OAPI, we are able to utilize the call stack to keep track + * of the previous hooks. With OAPI, we can no longer do this so + * we track the stack manually */ +nr_stack_t drupal_module_invoke_all_hooks; /* stack of Drupal hooks */ +nr_stack_t drupal_module_invoke_all_states; /* stack of bools indicating + whether the current hook + needs to be released */ +#else char* drupal_module_invoke_all_hook; /* The current Drupal hook */ size_t drupal_module_invoke_all_hook_len; /* The length of the current Drupal hook */ +#endif //OAPI size_t drupal_http_request_depth; /* The current depth of drupal_http_request() calls */ #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ @@ -419,7 +430,18 @@ int symfony1_in_dispatch; /* Whether we are currently within a int symfony1_in_error404; /* Whether we are currently within a sfError404Exception::printStackTrace() frame */ +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA +/* Without OAPI, we are able to utilize the call stack to keep track + * of the previous tags. With OAPI, we can no longer do this so + * we track the stack manually */ +nr_stack_t wordpress_tags; +nr_stack_t wordpress_tag_states; /* stack of bools indicating + whether the current tag + needs to be released */ +#else char* wordpress_tag; /* The current WordPress tag */ +#endif //OAPI nr_regex_t* wordpress_hook_regex; /* Regex to sanitize hook names */ nr_regex_t* wordpress_plugin_regex; /* Regex for plugin filenames */ nr_regex_t* wordpress_theme_regex; /* Regex for theme filenames */ diff --git a/agent/php_rinit.c b/agent/php_rinit.c index 434ff17f0..376cf1d19 100644 --- a/agent/php_rinit.c +++ b/agent/php_rinit.c @@ -33,6 +33,10 @@ static void str_stack_dtor(void* e, NRUNUSED void* d) { char* str = (char*)e; nr_free(str); } +static void zval_stack_dtor(void* e, NRUNUSED void* d) { + zval* zv = (zval*)e; + nr_php_zval_free(&zv); +} #endif #ifdef TAGS @@ -112,11 +116,22 @@ PHP_RINIT_FUNCTION(newrelic) { "(^([a-z_-]+[_-])([0-9a-f_.]+[0-9][0-9a-f.]+)(_{0,1}.*)$|(.*))", NR_REGEX_CASELESS, 0); + /* + * Pre-OAPI, this variables were kept on the call stack and + * therefore had no need to be in an nr_stack + */ #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA nr_stack_init(&NRPRG(predis_ctxs), NR_STACK_DEFAULT_CAPACITY); + nr_stack_init(&NRPRG(wordpress_tags), NR_STACK_DEFAULT_CAPACITY); + nr_stack_init(&NRPRG(wordpress_tag_states), NR_STACK_DEFAULT_CAPACITY); + nr_stack_init(&NRPRG(drupal_module_invoke_all_hooks), NR_STACK_DEFAULT_CAPACITY); + nr_stack_init(&NRPRG(drupal_module_invoke_all_states), NR_STACK_DEFAULT_CAPACITY); NRPRG(predis_ctxs).dtor = str_stack_dtor; + NRPRG(wordpress_tags).dtor = str_stack_dtor; + NRPRG(drupal_module_invoke_all_hooks).dtor = zval_stack_dtor; #endif + NRPRG(mysql_last_conn) = NULL; NRPRG(pgsql_last_conn) = NULL; NRPRG(datastore_connections) = nr_hashmap_create( diff --git a/agent/php_rshutdown.c b/agent/php_rshutdown.c index fec40b3d2..8bfc739dd 100644 --- a/agent/php_rshutdown.c +++ b/agent/php_rshutdown.c @@ -112,6 +112,17 @@ int nr_php_post_deactivate(void) { nr_free(NRPRG(mysql_last_conn)); nr_free(NRPRG(pgsql_last_conn)); nr_hashmap_destroy(&NRPRG(datastore_connections)); +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + /* + * Pre-OAPI, this variables were kept on the call stack and + * therefore had no need to be in an nr_stack + */ + nr_stack_destroy_fields(&NRPRG(wordpress_tags)); + nr_stack_destroy_fields(&NRPRG(wordpress_tag_states)); + nr_stack_destroy_fields(&NRPRG(drupal_module_invoke_all_hooks)); + nr_stack_destroy_fields(&NRPRG(drupal_module_invoke_all_states)); +#endif #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA diff --git a/agent/php_vm.c b/agent/php_vm.c index 19f4bb215..0681af32c 100644 --- a/agent/php_vm.c +++ b/agent/php_vm.c @@ -8,7 +8,12 @@ #include "php_call.h" #include "util_logging.h" -#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO /* PHP 7.0+ */ +/* + * If we are using OAPI, we do not want to modify any opcodes + */ +#if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO \ + && !(ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA) /* PHP 7.0+ and not OAPI */ /* * An entry in the previous_opcode_handlers table. @@ -228,4 +233,4 @@ void nr_php_set_opcode_handlers(void) {} void nr_php_remove_opcode_handlers(void) {} -#endif /* PHP 7.0+ */ +#endif /* PHP 7.0+ and not OAPI */ diff --git a/axiom/util_stack.h b/axiom/util_stack.h index d509b1b51..dd166a780 100644 --- a/axiom/util_stack.h +++ b/axiom/util_stack.h @@ -70,7 +70,7 @@ void* nr_stack_get_top(nr_stack_t* s); void nr_stack_push(nr_stack_t* s, void* new_element); /* - * Purpose : Peek at the top of the stack without removing the top element. + * Purpose : Remove and return the top of the stack * * Params : 1. A pointer to a stack, s. * diff --git a/tests/integration/frameworks/drupal/skipif.inc b/tests/integration/frameworks/drupal/skipif.inc new file mode 100644 index 000000000..502ee78c9 --- /dev/null +++ b/tests/integration/frameworks/drupal/skipif.inc @@ -0,0 +1,9 @@ +=")) { + die("skip: PHP >= 8.2 not supported\n"); +} diff --git a/tests/integration/frameworks/drupal/test_module_invoke_all.php b/tests/integration/frameworks/drupal/test_module_invoke_all.php new file mode 100644 index 000000000..57936a6d3 --- /dev/null +++ b/tests/integration/frameworks/drupal/test_module_invoke_all.php @@ -0,0 +1,90 @@ +=")) { + die("skip: PHP >= 8.0 uses other test\n"); +} +*/ + +/*EXPECT +module_hook_with_arg(arg=[arg_value]) +g +h +f +h +f +*/ + +/*EXPECT_METRICS +[ + "?? agent run id", + "?? timeframe start", + "?? timeframe stop", + [ + [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/all"}, + [1, "??", "??", "??", "??", "??"]], + [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther"}, + [1, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/Logging/Forwarding/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/Logging/Metrics/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Framework/Drupal/Hook/hook_with_arg"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Framework/Drupal/Hook/f"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Framework/Drupal/Hook/g"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Framework/Drupal/Hook/h"}, [2, "??", "??", "??", "??", "??"]], + [{"name":"Framework/Drupal/Module/module"}, [5, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransaction/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/framework/Drupal/forced"}, [1, 0, 0, 0, 0, 0]] + ] +] +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../../include/config.php'); + +function module_invoke_all($f) { + $args = func_get_args(); + unset($args[0]); + call_user_func_array("module_" . $f, $args); +} + +function module_h() { + echo "h\n"; + throw new Exception("Test Exception"); +} + +function module_f() { + try { + module_invoke_all("h"); + } catch (Exception $e) { + echo "f\n"; + } +} + +function module_g() { + echo "g\n"; + module_f(); +} + +function module_hook_with_arg($arg) { + echo "module_hook_with_arg(arg=[$arg])\n"; +} + +module_invoke_all("hook_with_arg", "arg_value"); +module_invoke_all("g"); +module_invoke_all("f"); diff --git a/tests/integration/frameworks/drupal/test_module_invoke_all.php8.php b/tests/integration/frameworks/drupal/test_module_invoke_all.php8.php new file mode 100644 index 000000000..c14b92fd1 --- /dev/null +++ b/tests/integration/frameworks/drupal/test_module_invoke_all.php8.php @@ -0,0 +1,90 @@ +=")) { + die("skip: PHP >= 8.0 uses other test\n"); +} +*/ + +/*INI +newrelic.framework = wordpress +*/ + +/*EXPECT +f: string1 +h: string3 +g: string2 +*/ + +/*EXPECT_METRICS +[ + "?? agent run id", + "?? start time", + "?? stop time", + [ + [{"name": "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name": "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther"}, [1, "??", "??", "??", "??", "??"]], + [{"name": "Framework/WordPress/Hook/f"}, [1, "??", "??", "??", "??", "??"]], + [{"name": "Framework/WordPress/Hook/g"}, [1, "??", "??", "??", "??", "??"]], + [{"name": "Framework/WordPress/Hook/h"}, [1, "??", "??", "??", "??", "??"]], + [{"name": "OtherTransaction/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name": "OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name": "OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], + [{"name": "OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/framework/WordPress/forced"}, [1, "??", "??", "??", "??", "??"]] + ] +] +*/ + +// Simple mock of wordpress's apply_filter() +function apply_filters($tag, ...$args) { + call_user_func_array($tag, $args); +} + +function h($str) { + echo "h: "; + echo $str; + echo "\n"; + throw new Exception("Test Exception"); +} + +function g($str) { + echo "g: "; + echo $str; + echo "\n"; +} + +function f($str) { + echo "f: "; + echo $str; + echo "\n"; + try { + apply_filters("h", "string3"); + } catch (Exception $e) { + apply_filters("g", "string2"); + } +} + +apply_filters("f", "string1"); diff --git a/tests/integration/frameworks/wordpress/test_wordpress_apply_filters.php8.php b/tests/integration/frameworks/wordpress/test_wordpress_apply_filters.php8.php new file mode 100644 index 000000000..07266ffa7 --- /dev/null +++ b/tests/integration/frameworks/wordpress/test_wordpress_apply_filters.php8.php @@ -0,0 +1,79 @@ +=")) { + die("skip: PHP >= 8.0 uses other test\n"); +} +*/ + +/*INI +newrelic.framework = wordpress +*/ + +/*EXPECT +f +h +g +*/ + +/*EXPECT_METRICS +[ + "?? agent run id", + "?? start time", + "?? stop time", + [ + [{"name": "DurationByCaller/Unknown/Unknown/Unknown/Unknown/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name": "DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther"}, [1, "??", "??", "??", "??", "??"]], + [{"name": "Framework/WordPress/Hook/f"}, [1, "??", "??", "??", "??", "??"]], + [{"name": "Framework/WordPress/Hook/g"}, [1, "??", "??", "??", "??", "??"]], + [{"name": "Framework/WordPress/Hook/h"}, [1, "??", "??", "??", "??", "??"]], + [{"name": "OtherTransaction/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name": "OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name": "OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], + [{"name": "OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/framework/WordPress/forced"}, [1, "??", "??", "??", "??", "??"]] + ] +] +*/ + +// Simple mock of wordpress's do_action() +function do_action($tag, ...$args) { + call_user_func_array($tag, $args); +} + +function h() { + echo "h\n"; + throw new Exception("Test Exception"); +} + +function g() { + echo "g\n"; +} + +function f() { + echo "f\n"; + try { + do_action("h"); + } catch (Exception $e){ + do_action("g"); + } +} + +do_action("f"); diff --git a/tests/integration/frameworks/wordpress/test_wordpress_do_action.php8.php b/tests/integration/frameworks/wordpress/test_wordpress_do_action.php8.php new file mode 100644 index 000000000..a24492dff --- /dev/null +++ b/tests/integration/frameworks/wordpress/test_wordpress_do_action.php8.php @@ -0,0 +1,73 @@ + Date: Thu, 26 Jan 2023 15:56:15 -0700 Subject: [PATCH 16/56] fix(agent): Ensure txn exists before we try to reference it. (#600) Consistent with other functions in php_execute, check if txn exists before trying to use it. The issue occurred during the first request, and because it is the first, the appname is unknown and we don't create a txn. However, an exception happened in that first request, our exception_hook handler got triggered, and if that is triggered, we always try to close off dangling segments. In this case, because we didn't check for txn==null, it segfaulted as we tried to access txn elements. Pr fixes that in two ways, 1) check for txn==null before closing off segments. 2) don't record uncaught exception info generated from our exception_hook handler if a txn is null. ``` 2023-01-26 21:03:16.816 +0000 (29813 29813) verbosedebug: RINIT processing started 2023-01-26 21:03:16.816 +0000 (29813 29813) debug: added app='PHP Test Apps Laravel ads v2' license='07...4a' 2023-01-26 21:03:16.816 +0000 (29813 29813) verbosedebug: querying app='PHP Test Apps Laravel ads v2' from parent=4 2023-01-26 21:03:16.816 +0000 (29813 29813) verbosedebug: sending appinfo message, len=6492 2023-01-26 21:03:16.817 +0000 (29813 29813) debug: APPINFO reply unknown app='PHP Test Apps Laravel ads v2' 2023-01-26 21:03:16.817 +0000 (29813 29813) debug: unable to begin transaction: app 'PHP Test Apps Laravel ads v2' is unknown ``` note the last line: debug: unable to begin transaction: app 'PHP Test Apps Laravel ads v2' is unknown --- agent/php_execute.c | 11 ++++++++++- agent/php_observer.c | 18 +++++++++++------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/agent/php_execute.c b/agent/php_execute.c index e1c5493ff..18465ceb0 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -1793,6 +1793,11 @@ static inline void nr_php_observer_exception_segments_end( if (NULL == exception || NULL == execute_data_this) { return; } + + if (NULL == NRPRG(txn)) { + return; + } + segment = NRTXN(force_current_segment); while ((NULL != segment) && (NRTXN(segment_root) != NRTXN(force_current_segment))) { @@ -1815,6 +1820,10 @@ void nr_php_observer_segment_end(zval* exception) { * if wraprec is set or if tt is greater than 0. */ + if (NULL == NRPRG(txn)) { + return; + } + if (NULL != exception) { nr_status_t status; @@ -2056,7 +2065,7 @@ static void nr_php_instrument_func_begin(NR_EXECUTE_PROTO) { "Uncaught exception ", &NRPRG(exception_filters) TSRMLS_CC); php_observer_clear_uncaught_exception_globals(); } - } else { + } else if (NULL != NRPRG(txn)) { /* * Check if NRPRG(uncaught_exception) exists because if it's not handled, * we'll parent the new segment on the wrong stacked segment. Close off diff --git a/agent/php_observer.c b/agent/php_observer.c index 23d0878d1..a157d45b2 100644 --- a/agent/php_observer.c +++ b/agent/php_observer.c @@ -118,15 +118,19 @@ void nr_throw_exception_hook(zend_object* exception) { zval* exception_zval = NULL; /* - * Since PHP 7, EG(exception) is stored as a zend_object, and is therefore - * only wrapped in a zval when it actually needs to be. + * Don't track the exception if we don't have a valid txn. */ - ZVAL_OBJ(&new_exception, exception); - exception_zval = &new_exception; - - php_observer_handle_exception_hook(exception_zval, - &(EG(current_execute_data)->This)); + if (NULL != NRPRG(txn)) { + /* + * Since PHP 7, EG(exception) is stored as a zend_object, and is therefore + * only wrapped in a zval when it actually needs to be. + */ + ZVAL_OBJ(&new_exception, exception); + exception_zval = &new_exception; + php_observer_handle_exception_hook(exception_zval, + &(EG(current_execute_data)->This)); + } if (original_zend_throw_exception_hook != NULL) { original_zend_throw_exception_hook(exception); } From 947f42a3582a2ca305b81e40302e263800c6a2b3 Mon Sep 17 00:00:00 2001 From: ZNeumann Date: Fri, 27 Jan 2023 14:45:12 -0700 Subject: [PATCH 17/56] fix(wordpress): fix unbound memory allocation when wordpress tags off (#601) move NR_GET_RETURN_VALUE_PTR before NR_PHP_WRAPPER_CALL Cleanup conditional compilation to be more readable --- agent/fw_wordpress.c | 45 +++++++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/agent/fw_wordpress.c b/agent/fw_wordpress.c index 4236414ef..078132249 100644 --- a/agent/fw_wordpress.c +++ b/agent/fw_wordpress.c @@ -505,6 +505,23 @@ NR_PHP_WRAPPER(nr_wordpress_exec_handle_tag) { tag = nr_php_arg_get(1, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + /* We will ignore the global wordpress_tag_states stack if wodpress_hooks is off */ + if (0 != NRINI(wordpress_hooks)) { + if (1 == nr_php_is_zval_non_empty_string(tag)) { + /* + * Our general approach here is to set the wordpress_tag global, then let + * the call_user_func_array instrumentation take care of actually timing + * the hooks by checking if it's set. + */ + nr_stack_push(&NRPRG(wordpress_tags), nr_wordpress_clean_tag(tag TSRMLS_CC)); + nr_stack_push(&NRPRG(wordpress_tag_states), (void*)!NULL); + } else { + nr_stack_push(&NRPRG(wordpress_tag_states), NULL); + } + } +#else if (1 == nr_php_is_zval_non_empty_string(tag) && (0 != NRINI(wordpress_hooks))) { /* @@ -512,14 +529,6 @@ NR_PHP_WRAPPER(nr_wordpress_exec_handle_tag) { * the call_user_func_array instrumentation take care of actually timing * the hooks by checking if it's set. */ -#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ - && !defined OVERWRITE_ZEND_EXECUTE_DATA - nr_stack_push(&NRPRG(wordpress_tags), nr_wordpress_clean_tag(tag TSRMLS_CC)); - nr_stack_push(&NRPRG(wordpress_tag_states), (void*)!NULL); - } else { - nr_stack_push(&NRPRG(wordpress_tag_states), NULL); - } -#else char* old_tag = NRPRG(wordpress_tag); NRPRG(wordpress_tag) = nr_wordpress_clean_tag(tag TSRMLS_CC); @@ -595,25 +604,32 @@ NR_PHP_WRAPPER(nr_wordpress_apply_filters) { tag = nr_php_arg_get(1, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); - if (1 == nr_php_is_zval_non_empty_string(tag)) { - if (0 != NRINI(wordpress_hooks)) { +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + /* We will ignore the global wordpress_tag_states stack if wodpress_hooks is off */ + if (0 != NRINI(wordpress_hooks)) { + if (1 == nr_php_is_zval_non_empty_string(tag)) { /* * Our general approach here is to set the wordpress_tag global, then let * the call_user_func_array instrumentation take care of actually timing * the hooks by checking if it's set. */ -#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ - && !defined OVERWRITE_ZEND_EXECUTE_DATA nr_stack_push(&NRPRG(wordpress_tags), nr_wordpress_clean_tag(tag TSRMLS_CC)); nr_stack_push(&NRPRG(wordpress_tag_states), (void*)!NULL); } else { // Keep track of whether we pushed to NRPRG(wordpress_tags) nr_stack_push(&NRPRG(wordpress_tag_states), NULL); } - } else { - nr_stack_push(&NRPRG(wordpress_tag_states), NULL); } #else + zval** retval_ptr = NR_GET_RETURN_VALUE_PTR; + if (1 == nr_php_is_zval_non_empty_string(tag)) { + if (0 != NRINI(wordpress_hooks)) { + /* + * Our general approach here is to set the wordpress_tag global, then let + * the call_user_func_array instrumentation take care of actually timing + * the hooks by checking if it's set. + */ char* old_tag = NRPRG(wordpress_tag); NRPRG(wordpress_tag) = nr_wordpress_clean_tag(tag TSRMLS_CC); @@ -624,7 +640,6 @@ NR_PHP_WRAPPER(nr_wordpress_apply_filters) { NR_PHP_WRAPPER_CALL; } - zval** retval_ptr = NR_GET_RETURN_VALUE_PTR; nr_wordpress_name_the_wt(tag, retval_ptr TSRMLS_CC); } else { NR_PHP_WRAPPER_CALL; From 7648b1a85be5bd618585c8aa58a917fe3ca52362 Mon Sep 17 00:00:00 2001 From: Amber Sistla Date: Thu, 16 Feb 2023 11:07:24 -0700 Subject: [PATCH 18/56] fix(agent): Explicitly call some special function instrumentation in oapi func_begin for txn naming. (#615) For OAPI, wrapping default makes all wrapped special functions execute during func_end. This caused issues since to customize txn naming per framework, PHP agent uses the order in which functions are processed, NR_NOT_OK_TO_OVERWRITE/NR_OK_TO_OVERWRITE, and whether it is called either before or after NR_PHP_WRAPPER_CALL (for pre PHP 8+) or whether it is called in func_begin or func_end (for PHP 8+ / OAPI). By defaulting to calling all callbacks "after" during func_end, it changed the order in which nr_txn_set_path was being set. To fix this, we need to verify which callback functions set the txn before NR_PHP_WRAPPER_CALL and which set it after, and have the wrapped functions act similarly to call before/after. This PR 1) reviewed all frameworks (exceptions listed below) and use of nr_txn_set_path on a case by case basis. 2) Explicitly commented to detail the naming schemes in use with each use of nr_txn_set_path in all frameworks 3) used the before/after/clean wrapping function as needed when txn naming needed to occur before function execution to match what is done in legacy instrumentation. 4) for functions not needing to be called before, comments were still added noting that it was being called by the default (after callback/func_end) Calling the wrapper special function by default before a function executes was a solution initially considered; however, after investigation/testing, it was deemed inappropriate for multiple reasons: 1)too invasive 2)some wrapped functions do not have all the values they need before a function is executed, for example functions that need the return value (I.e. all those special functions that continue to do things after `NR_PHP_WRAPPER_CALL` 3) Additionally, each framework has individualized, specialized ways of setting the txn name via `nr_txn_set_path`. As such, calling a wrapper function in func_end as default is still the most appropriate choice. These frameworks are either unsupported, do not support PHP 8+, or both and no change to the txn naming scheme: fw_kohana.c, fw_joomla.c, fw_magneto.c(magento 1.x), fw_silex.c, fw_symfony.c, fw_symfony2.c, fw_zend.c fw_zend2.c Unit tests that run on all versions of PHP ensure the txn naming behavior is identical. --- agent/fw_cakephp.c | 37 +++ agent/fw_drupal.c | 66 +++-- agent/fw_drupal8.c | 32 ++ agent/fw_laminas3.c | 8 + agent/fw_laravel.c | 65 +++- agent/fw_lumen.c | 25 +- agent/fw_magento2.c | 91 +++++- agent/fw_mediawiki.c | 31 ++ agent/fw_slim.c | 12 + agent/fw_symfony4.c | 67 +++-- agent/fw_wordpress.c | 5 + agent/fw_yii.c | 17 ++ agent/php_wrapper.h | 43 +++ agent/tests/test_php_wrapper.c | 528 +++++++++++++++++++++++++++++++++ 14 files changed, 974 insertions(+), 53 deletions(-) diff --git a/agent/fw_cakephp.c b/agent/fw_cakephp.c index 84a01deb1..80c8a6b9b 100644 --- a/agent/fw_cakephp.c +++ b/agent/fw_cakephp.c @@ -39,6 +39,11 @@ nr_framework_classification_t nr_cakephp_special_2( * Component::initialize(). This function takes a controller as a parameter * and we look into the params array of that controller object, and pick up * the controller and action out of that array. + * + * CakePHP 1.x is end-of-life and no longer supported by the agent. + * Cake PHP 1.x does not support PHP 8+ and this wrapper is not updated for OAPI + * compatibility. + * */ NR_PHP_WRAPPER(nr_cakephp_name_the_wt_pre20) { zval* arg1 = 0; @@ -130,6 +135,17 @@ NR_PHP_WRAPPER_END * and we get the action from the params array in that object. The * controller object ($this) has a name, and that name is used (along * with the word "Controller" appended which is what the CakePHP code does). + * + * CakePHP 2.x is end-of-life and in maintenance mode (critical bugfixes only). + * As such, functionality added in PHP 7.1+ is not well supported. + * + * txn naming scheme: + * In this case, `nr_txn_set_path` is called after `NR_PHP_WRAPPER_CALL` with + * `NR_NOT_OK_TO_OVERWRITE` + * This entails that the last wrapped call gets to name the txn. + * No changes required to ensure OAPI compatibility this corresponds to the + * default way of calling the wrapped function in func_end. + * */ NR_PHP_WRAPPER(nr_cakephp_name_the_wt_2) { zval* arg1 = 0; @@ -243,6 +259,11 @@ NR_PHP_WRAPPER_END * * Dispatch::cakeError will be called if there is a problem during dispatch * (action or controller not found). + * + * CakePHP 1.x is end-of-life and no longer supported by the agent. + * Cake PHP 1.x does not support PHP 8+ and this wrapper is not updated for OAPI + * compatibility. + * */ NR_PHP_WRAPPER(nr_cakephp_problem_1) { const char* name = "Dispatcher::cakeError"; @@ -266,6 +287,16 @@ NR_PHP_WRAPPER_END * appropriate Exception will be created and thrown. We wrap the CakeException * constructor instead of the Exception handler, since CakePHP allows for the * handler to be completely replaced. + * + * CakePHP 2.x is end-of-life and in maintenance mode (critical bugfixes only). + * As such, functionality added in PHP 7.1+ is not well supported. + * + * txn naming scheme: + * In this case, `nr_txn_set_path` is called before `NR_PHP_WRAPPER_CALL` with + * `NR_NOT_OK_TO_OVERWRITE` and as this corresponds to calling the wrapped function + * in func_begin it needs to be explicitly set as a before_callback to ensure OAPI + * compatibility. + * This entails that the first wrapped call gets to name the txn. */ NR_PHP_WRAPPER(nr_cakephp_problem_2) { const char* name = "Exception"; @@ -298,6 +329,12 @@ void nr_cakephp_enable_1(TSRMLS_D) { void nr_cakephp_enable_2(TSRMLS_D) { nr_php_wrap_user_function(NR_PSTR("Controller::invokeAction"), nr_cakephp_name_the_wt_2 TSRMLS_CC); +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("CakeException::__construct"), nr_cakephp_problem_2, NULL, NULL); +#else nr_php_wrap_user_function(NR_PSTR("CakeException::__construct"), nr_cakephp_problem_2 TSRMLS_CC); +#endif } diff --git a/agent/fw_drupal.c b/agent/fw_drupal.c index 3b3535cff..4513d930a 100644 --- a/agent/fw_drupal.c +++ b/agent/fw_drupal.c @@ -22,6 +22,13 @@ /* * Set the Web Transaction (WT) name to "(cached page)" + * + * txn naming scheme: + * In this case, `nr_txn_set_path` is called before `NR_PHP_WRAPPER_CALL` with + * `NR_NOT_OK_TO_OVERWRITE` and as this corresponds to calling the wrapped + * function in func_begin it needs to be explicitly set as a before_callback to + * ensure OAPI compatibility. This combination entails that the first wrapped + * call gets to name the txn. */ NR_PHP_WRAPPER(nr_drupal_name_wt_as_cached_page) { const char* buf = "(cached page)"; @@ -39,6 +46,12 @@ NR_PHP_WRAPPER_END /* * Name the WT based on the QDrupal QForm name. + * txn naming scheme: + * In this case, `nr_txn_set_path` is called before `NR_PHP_WRAPPER_CALL` with + * `NR_NOT_OK_TO_OVERWRITE` and as this corresponds to calling the wrapped + * function in func_begin it needs to be explicitly set as a before_callback to + * ensure OAPI compatibility. This combination entails that the first wrapped + * call gets to name the txn. */ NR_PHP_WRAPPER(nr_drupal_qdrupal_name_the_wt) { zval* arg1 = NULL; @@ -455,8 +468,9 @@ static void nr_drupal_wrap_hook_within_module_invoke_all( * available. */ #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ - && !defined OVERWRITE_ZEND_EXECUTE_DATA - zval* curr_hook = (zval*)nr_stack_get_top(&NRPRG(drupal_module_invoke_all_hooks)); + && !defined OVERWRITE_ZEND_EXECUTE_DATA + zval* curr_hook + = (zval*)nr_stack_get_top(&NRPRG(drupal_module_invoke_all_hooks)); if (!nr_php_is_zval_non_empty_string(curr_hook)) { nrl_verbosedebug(NRL_FRAMEWORK, "%s: cannot extract hook name from global stack", @@ -476,18 +490,16 @@ static void nr_drupal_wrap_hook_within_module_invoke_all( return; } - rv = module_invoke_all_parse_module_and_hook( - &module, &module_len, hook_name, - hook_len, func); + rv = module_invoke_all_parse_module_and_hook(&module, &module_len, hook_name, + hook_len, func); if (NR_SUCCESS != rv) { return; } - nr_php_wrap_user_function_drupal( - nr_php_function_name(func), nr_php_function_name_length(func), module, - module_len, hook_name, - hook_len TSRMLS_CC); + nr_php_wrap_user_function_drupal(nr_php_function_name(func), + nr_php_function_name_length(func), module, + module_len, hook_name, hook_len TSRMLS_CC); nr_free(module); } @@ -733,7 +745,7 @@ NR_PHP_WRAPPER(nr_drupal_wrap_module_invoke) { NR_PHP_WRAPPER_END #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ - && !defined OVERWRITE_ZEND_EXECUTE_DATA + && !defined OVERWRITE_ZEND_EXECUTE_DATA NR_PHP_WRAPPER(nr_drupal_wrap_module_invoke_all_before) { (void)wraprec; zval* hook_copy = NULL; @@ -742,19 +754,16 @@ NR_PHP_WRAPPER(nr_drupal_wrap_module_invoke_all_before) { hook_copy = nr_php_arg_get(1, NR_EXECUTE_ORIG_ARGS); if (nr_php_is_zval_non_empty_string(hook_copy)) { - nr_stack_push(&NRPRG(drupal_module_invoke_all_hooks), - hook_copy); - nr_stack_push(&NRPRG(drupal_module_invoke_all_states), - (void*)!NULL); + nr_stack_push(&NRPRG(drupal_module_invoke_all_hooks), hook_copy); + nr_stack_push(&NRPRG(drupal_module_invoke_all_states), (void*)!NULL); } else { - nr_stack_push(&NRPRG(drupal_module_invoke_all_states), - NULL); + nr_stack_push(&NRPRG(drupal_module_invoke_all_states), NULL); } } NR_PHP_WRAPPER_END static void module_invoke_all_clean_stacks() { - if((bool)nr_stack_pop(&NRPRG(drupal_module_invoke_all_states))) { + if ((bool)nr_stack_pop(&NRPRG(drupal_module_invoke_all_states))) { zval* hook_copy = nr_stack_pop(&NRPRG(drupal_module_invoke_all_hooks)); nr_php_arg_release(&hook_copy); } @@ -814,18 +823,23 @@ NR_PHP_WRAPPER_END void nr_drupal_enable(TSRMLS_D) { nr_php_add_call_user_func_array_pre_callback( nr_drupal_call_user_func_array_callback TSRMLS_CC); - nr_php_wrap_user_function(NR_PSTR("QFormBase::Run"), - nr_drupal_qdrupal_name_the_wt TSRMLS_CC); - nr_php_wrap_user_function(NR_PSTR("drupal_page_cache_header"), - nr_drupal_name_wt_as_cached_page TSRMLS_CC); nr_php_wrap_user_function(NR_PSTR("drupal_cron_run"), nr_drupal_cron_run TSRMLS_CC); #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("QFormBase::Run"), nr_drupal_qdrupal_name_the_wt, NULL, NULL); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("drupal_page_cache_header"), nr_drupal_name_wt_as_cached_page, + NULL, NULL); nr_php_wrap_user_function_before_after_clean( NR_PSTR("drupal_http_request"), nr_drupal_http_request_before, nr_drupal_http_request_after, nr_drupal_http_request_clean); #else + nr_php_wrap_user_function(NR_PSTR("QFormBase::Run"), + nr_drupal_qdrupal_name_the_wt TSRMLS_CC); + nr_php_wrap_user_function(NR_PSTR("drupal_page_cache_header"), + nr_drupal_name_wt_as_cached_page TSRMLS_CC); nr_php_wrap_user_function(NR_PSTR("drupal_http_request"), nr_drupal_http_request_exec TSRMLS_CC); #endif @@ -838,11 +852,11 @@ void nr_drupal_enable(TSRMLS_D) { nr_php_wrap_user_function(NR_PSTR("module_invoke"), nr_drupal_wrap_module_invoke TSRMLS_CC); #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ - && !defined OVERWRITE_ZEND_EXECUTE_DATA - nr_php_wrap_user_function_before_after_clean(NR_PSTR("module_invoke_all"), - nr_drupal_wrap_module_invoke_all_before, - nr_drupal_wrap_module_invoke_all_after, - nr_drupal_wrap_module_invoke_all_clean); + && !defined OVERWRITE_ZEND_EXECUTE_DATA + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("module_invoke_all"), nr_drupal_wrap_module_invoke_all_before, + nr_drupal_wrap_module_invoke_all_after, + nr_drupal_wrap_module_invoke_all_clean); #else nr_php_wrap_user_function(NR_PSTR("module_invoke_all"), nr_drupal_wrap_module_invoke_all TSRMLS_CC); diff --git a/agent/fw_drupal8.c b/agent/fw_drupal8.c index a010edfc3..8cfd13fd7 100644 --- a/agent/fw_drupal8.c +++ b/agent/fw_drupal8.c @@ -154,6 +154,14 @@ static int nr_drupal8_is_function_in_call_stack(const char* function, /* * Purpose : Name the Drupal 8 transaction based on the return value of * ControllerResolver::getControllerFromDefinition(). + * + * txn naming scheme: + * In this case, `nr_txn_set_path` is called after `NR_PHP_WRAPPER_CALL` with + * `NR_NOT_OK_TO_OVERWRITE` and as this corresponds to calling the wrapped + * function in func_end no change is needed to ensure OAPI compatibility as it + * will use the default func_end after callback. This entails that the last + * wrapped call gets to name the txn which corresponds to the naming details + * within the function. */ NR_PHP_WRAPPER(nr_drupal8_name_the_wt) { zval** retval_ptr = NR_GET_RETURN_VALUE_PTR; @@ -203,6 +211,14 @@ NR_PHP_WRAPPER(nr_drupal8_name_the_wt) { } NR_PHP_WRAPPER_END +/* + * txn naming scheme: + * In this case, `nr_txn_set_path` is called after `NR_PHP_WRAPPER_CALL` with + * `NR_OK_TO_OVERWRITE` and as this corresponds to calling the wrapped function + * in func_end no change is needed to ensure OAPI compatibility as it will use + * the default func_end after callback. This entails that the last wrapped + * function call of this type gets to name the txn. + */ NR_PHP_WRAPPER(nr_drupal8_name_the_wt_cached) { const char* name = "page_cache"; zval** retval_ptr = NR_GET_RETURN_VALUE_PTR; @@ -400,6 +416,14 @@ NR_PHP_WRAPPER(nr_drupal8_module_handler) { } NR_PHP_WRAPPER_END +/* + * txn naming scheme: + * In this case, `nr_txn_set_path` is called before `NR_PHP_WRAPPER_CALL` with + * `NR_OK_TO_OVERWRITE` and as this corresponds to calling the wrapped function + * in func_begin it needs to be explicitly set as a before_callback to ensure + * OAPI compatibility. This entails that the last wrapped call gets to name the + * txn but it is overwritable if another better name comes along. + */ NR_PHP_WRAPPER(nr_drupal8_name_the_wt_via_symfony) { zval* event = NULL; zval* request = NULL; @@ -467,9 +491,17 @@ void nr_drupal8_enable(TSRMLS_D) { * the Symfony RouterListener to determine the main controller this * request is routed through. */ +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("Symfony\\Component\\HttpKernel\\EventListe" + "ner\\RouterListener::onKernelRequest"), + nr_drupal8_name_the_wt_via_symfony, NULL, NULL); +#else nr_php_wrap_user_function(NR_PSTR("Symfony\\Component\\HttpKernel\\EventListe" "ner\\RouterListener::onKernelRequest"), nr_drupal8_name_the_wt_via_symfony TSRMLS_CC); +#endif /* * The ControllerResolver is the legacy way to name diff --git a/agent/fw_laminas3.c b/agent/fw_laminas3.c index dbfb744f6..e7ce0e899 100644 --- a/agent/fw_laminas3.c +++ b/agent/fw_laminas3.c @@ -67,6 +67,14 @@ * presumably that was some optimization due to the return value not being used. */ +/* + * txn naming scheme: + * In this case, `nr_txn_set_path` is called after `NR_PHP_WRAPPER_CALL` with + * `NR_OK_TO_OVERWRITE` and as this corresponds to calling the wrapped function + * in func_end no change is needed to ensure OAPI compatibility as it will use + * the default func_end after callback. This entails that the first wrapped + * function call of this type gets to name the txn. + */ NR_PHP_WRAPPER(nr_laminas3_name_the_wt) { zval* path = NULL; zval* this_var = NULL; diff --git a/agent/fw_laravel.c b/agent/fw_laravel.c index 23e4b4840..2302a803c 100644 --- a/agent/fw_laravel.c +++ b/agent/fw_laravel.c @@ -529,6 +529,7 @@ static char* nr_laravel_version(zval* app TSRMLS_DC) { * render() method. * * See: http://laravel.com/docs/5.0/errors#handling-errors + * */ NR_PHP_WRAPPER(nr_laravel5_exception_render) { #if ZEND_MODULE_API_NO >= ZEND_5_4_X_API_NO @@ -642,6 +643,9 @@ NR_PHP_WRAPPER(nr_laravel5_exception_report) { } NR_PHP_WRAPPER_END +/* + * Not applicable to OAPI. + */ static void nr_laravel_register_after_filter(zval* app TSRMLS_DC) { zval* filter = NULL; zval* retval = NULL; @@ -668,6 +672,7 @@ static void nr_laravel_register_after_filter(zval* app TSRMLS_DC) { /* * Only install our filter if this version of Laravel supports them. * Filters were deprecated in Laravel 5.0 and removed in version 5.2. + * As such, not applicable to OAPI. */ if (0 == nr_php_object_has_concrete_method(router, "after" TSRMLS_CC)) { nrl_verbosedebug(NRL_FRAMEWORK, "%s: Router does not support filters", @@ -698,7 +703,9 @@ static void nr_laravel_register_after_filter(zval* app TSRMLS_DC) { nr_php_zval_free(&filter); nr_php_zval_free(&retval); } - +/* + * Not applicable to OAPI. + */ NR_PHP_WRAPPER(nr_laravel4_application_run) { zval* this_var = NULL; @@ -728,6 +735,13 @@ NR_PHP_WRAPPER_END * the transaction name. This ensures the transaction is named if * the middleware short-circuits request processing by returning * a response instead of invoking its successor. + * + * txn naming scheme: + * In this case, `nr_txn_set_path` is called before `NR_PHP_WRAPPER_CALL` with + * `NR_OK_TO_OVERWRITE` and as this corresponds to calling the wrapped function + * in func_begin it needs to be explicitly set as a before_callback to ensure + * OAPI compatibility. This entails that the last wrapped call gets to name the + * txn (as detailed in the purpose above). */ NR_PHP_WRAPPER(nr_laravel5_middleware_handle) { NR_UNUSED_SPECIALFN; @@ -797,8 +811,14 @@ static void nr_laravel5_wrap_middleware(zval* app TSRMLS_DC) { name = nr_formatf("%.*s::handle", (int)Z_STRLEN_P(classname), Z_STRVAL_P(classname)); +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + nr_php_wrap_user_function_before_after_clean( + name, nr_strlen(name), nr_laravel5_middleware_handle, NULL, NULL); +#else nr_php_wrap_user_function(name, nr_strlen(name), nr_laravel5_middleware_handle TSRMLS_CC); +#endif nr_free(name); } } @@ -819,6 +839,11 @@ static void nr_laravel5_wrap_middleware(zval* app TSRMLS_DC) { * 2. The method name. * 3. The length of the method name. * 4. The post callback. + * + * Note: In this case, all functions utilized this execute before calling + * `NR_PHP_WRAPPER_CALL` and as this corresponds to calling the wrapped function + * in func_begin it needs to be explicitly set as a before_callback to ensure + * OAPI compatibility. */ static void nr_laravel_add_callback_method(const zend_class_entry* ce, const char* method, @@ -847,9 +872,14 @@ static void nr_laravel_add_callback_method(const zend_class_entry* ce, char* class_method = nr_formatf("%.*s::%.*s", NRSAFELEN(class_name_len), class_name, NRSAFELEN(method_len), method); +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + nr_php_wrap_user_function_before_after_clean( + class_method, nr_strlen(class_method), callback, NULL, NULL); +#else nr_php_wrap_user_function(class_method, nr_strlen(class_method), callback TSRMLS_CC); - +#endif nr_free(class_method); } @@ -905,6 +935,13 @@ NR_PHP_WRAPPER_END /* * This is a generic callback for any post hook on an Illuminate\Routing\Router * method where the method receives a request object as its first parameter. + * + * txn naming scheme: + * In this case, `nr_txn_set_path` is called after `NR_PHP_WRAPPER_CALL` with + * `NR_OK_TO_OVERWRITE` and as this corresponds to the OAPI default of calling + * the wrapped function callback in func_end, there are no changes required to + * ensure OAPI compatibility. This entails that the first call to this function + * gets to name the txn. */ NR_PHP_WRAPPER(nr_laravel_router_method_with_request) { zval* request = NULL; @@ -1012,6 +1049,14 @@ NR_PHP_WRAPPER(nr_laravel_application_construct) { } NR_PHP_WRAPPER_END +/* + * * txn naming scheme: + * In this case, `nr_txn_set_path` is called before `NR_PHP_WRAPPER_CALL` with + * `NR_OK_TO_OVERWRITE` and as this corresponds to calling the wrapped function + * in func_begin it needs to be explicitly set as a before_callback to ensure + * OAPI compatibility. + * This entails that the last wrapped call gets to name the txn. + */ NR_PHP_WRAPPER(nr_laravel_console_application_dorun) { zval* command = NULL; zval* input = NULL; @@ -1059,6 +1104,14 @@ NR_PHP_WRAPPER(nr_laravel_console_application_dorun) { } NR_PHP_WRAPPER_END +/* + * txn naming scheme: + * In this case, `nr_txn_set_path` is called after `NR_PHP_WRAPPER_CALL` with + * `NR_OK_TO_OVERWRITE` and as this corresponds the OAPI default of calling the + * wrapped function callback in func_end, there are no changes required to + * ensure OAPI compatibility. This entails that the first call to this function + * gets to name the txn. + */ NR_PHP_WRAPPER(nr_laravel_routes_get_route_for_methods) { zval* arg_request = NULL; zval* http_method = NULL; @@ -1205,9 +1258,15 @@ void nr_laravel_enable(TSRMLS_D) { /* * Listen for Artisan commands so we can name those appropriately. */ +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("Illuminate\\Console\\Application::doRun"), + nr_laravel_console_application_dorun, NULL, NULL); +#else nr_php_wrap_user_function(NR_PSTR("Illuminate\\Console\\Application::doRun"), nr_laravel_console_application_dorun TSRMLS_CC); - +#endif /* * Start Laravel queue instrumentation, provided it's not disabled. */ diff --git a/agent/fw_lumen.c b/agent/fw_lumen.c index 4d1de24dd..36330fca2 100644 --- a/agent/fw_lumen.c +++ b/agent/fw_lumen.c @@ -66,6 +66,13 @@ static int nr_lumen_name_the_wt_from_zval(const zval* name TSRMLS_DC, /* * Core transaction naming logic. Wraps the function that correlates * requests to routes + * + * txn naming scheme: + * In this case, `nr_txn_set_path` is called after `NR_PHP_WRAPPER_CALL` with + * `NR_OK_TO_OVERWRITE` and as this corresponds to calling the wrapped function + * in func_end no change is needed to ensure OAPI compatibility as it will use + * the default func_end after callback. This entails that the last wrapped + * function call of this type gets to name the txn. */ NR_PHP_WRAPPER(nr_lumen_handle_found_route) { zval* route_info = NULL; @@ -105,7 +112,8 @@ NR_PHP_WRAPPER(nr_lumen_handle_found_route) { if (NULL != route_name) { if (NR_SUCCESS - != nr_lumen_name_the_wt_from_zval(route_name TSRMLS_CC, "Lumen", false)) { + != nr_lumen_name_the_wt_from_zval(route_name TSRMLS_CC, "Lumen", + false)) { nrl_verbosedebug(NRL_TXN, "Lumen: located route name is a non-string"); } } else { @@ -139,6 +147,13 @@ NR_PHP_WRAPPER_END /* * Exception handling logic. Wraps the function that routes * exceptions to their respective handlers + * + * txn naming scheme: + * In this case, `nr_txn_set_path` is called before `NR_PHP_WRAPPER_CALL` with + * `NR_OK_TO_OVERWRITE` and as this corresponds to calling the wrapped function + * in func_begin it needs to be explicitly set as a before_callback to ensure + * OAPI compatibility. This entails that the last wrapped call gets to name the + * txn which in this case is the one that generated the exception. */ NR_PHP_WRAPPER(nr_lumen_exception) { zval* exception = NULL; @@ -204,8 +219,14 @@ void nr_lumen_enable(TSRMLS_D) { nr_php_wrap_user_function( NR_PSTR("Laravel\\Lumen\\Application::handleFoundRoute"), nr_lumen_handle_found_route TSRMLS_CC); - +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("Laravel\\Lumen\\Application::sendExceptionToHandler"), + nr_lumen_exception, NULL, NULL); +#else nr_php_wrap_user_function( NR_PSTR("Laravel\\Lumen\\Application::sendExceptionToHandler"), nr_lumen_exception TSRMLS_CC); +#endif } diff --git a/agent/fw_magento2.c b/agent/fw_magento2.c index 3f56e6158..87994936a 100644 --- a/agent/fw_magento2.c +++ b/agent/fw_magento2.c @@ -103,6 +103,14 @@ static void nr_magento2_name_transaction_from_service(const char* module, nr_free(name); } +/* + * txn naming scheme: + * In this case, `nr_txn_set_path` is called before `NR_PHP_WRAPPER_CALL` with + * `NR_OK_TO_OVERWRITE` and as this corresponds to calling the wrapped function + * in func_begin it needs to be explicitly set as a before_callback to ensure + * OAPI compatibility. This entails that the last wrapped call gets to name the + * txn but it is overwritable if another better name comes along. + */ NR_PHP_WRAPPER(nr_magento2_action_dispatch) { zval* this_var = NULL; @@ -120,6 +128,14 @@ NR_PHP_WRAPPER(nr_magento2_action_dispatch) { } NR_PHP_WRAPPER_END +/* + * txn naming scheme: + * In this case, `nr_txn_set_path` is called after `NR_PHP_WRAPPER_CALL` with + * `NR_OK_TO_OVERWRITE` and as this corresponds to calling the wrapped function + * in func_end no change is needed to ensure OAPI compatibility as it will use + * the default func_end after callback. This entails that the first wrapped + * function call of this type gets to name the txn. + */ NR_PHP_WRAPPER(nr_magento2_pagecache_kernel_load) { const char* name = "page_cache"; zval** response = NULL; @@ -145,6 +161,14 @@ NR_PHP_WRAPPER(nr_magento2_pagecache_kernel_load) { } NR_PHP_WRAPPER_END +/* + * txn naming scheme: + * In this case, `nr_txn_set_path` is called after `NR_PHP_WRAPPER_CALL` with + * `NR_OK_TO_OVERWRITE` and as this corresponds to calling the wrapped function + * in func_end no change is needed to ensure OAPI compatibility as it will use + * the default func_end after callback. This entails that the first wrapped + * function call of this type gets to name the txn. + */ NR_PHP_WRAPPER(nr_magento2_objectmanager_get) { const char* fci_class = "Magento\\Framework\\App\\FrontControllerInterface"; zval** retval_ptr = NULL; @@ -201,6 +225,14 @@ NR_PHP_WRAPPER(nr_magento2_objectmanager_get) { } NR_PHP_WRAPPER_END +/* + * * txn naming scheme: + * In this case, `nr_txn_set_path` is called before `NR_PHP_WRAPPER_CALL` with + * `NR_OK_TO_OVERWRITE` and as this corresponds to calling the wrapped function + * in func_begin it needs to be explicitly set as a before_callback to ensure + * OAPI compatibility. This combination entails that the last wrapped function + * call gets to name the txn. + */ NR_PHP_WRAPPER(nr_magento2_inputparamsresolver_resolve) { const char* this_klass = "Magento\\Webapi\\Controller\\Rest\\InputParamsResolver"; @@ -244,6 +276,14 @@ NR_PHP_WRAPPER(nr_magento2_inputparamsresolver_resolve) { } NR_PHP_WRAPPER_END +/* + * txn naming scheme: + * In this case, `nr_txn_set_path` is called after `NR_PHP_WRAPPER_CALL` with + * `NR_OK_TO_OVERWRITE` and as this corresponds to calling the wrapped function + * in func_end no change is needed to ensure as it will use the default func_end + * after callback. OAPI compatibility. This entails that the first wrapped + * function call of this type gets to name the txn. + */ NR_PHP_WRAPPER(nr_magento2_soap_iswsdlrequest) { zval** retval_ptr = NULL; @@ -261,6 +301,14 @@ NR_PHP_WRAPPER(nr_magento2_soap_iswsdlrequest) { } NR_PHP_WRAPPER_END +/* + * txn naming scheme: + * In this case, `nr_txn_set_path` is called after `NR_PHP_WRAPPER_CALL` with + * `NR_OK_TO_OVERWRITE` and as this corresponds to calling the wrapped function + * in func_end no change is needed to ensure as it will use the default func_end + * after callback. OAPI compatibility. This entails that the first wrapped + * function call of this type gets to name the txn. + */ NR_PHP_WRAPPER(nr_magento2_soap_iswsdllistrequest) { zval** retval_ptr = NULL; @@ -283,6 +331,14 @@ NR_PHP_WRAPPER_END * string $serviceClass * string $serviceMethod * array $arguments + * + * + * txn naming scheme: + * In this case, `nr_txn_set_path` is called before `NR_PHP_WRAPPER_CALL` with + * `NR_OK_TO_OVERWRITE` and as this corresponds to calling the wrapped function + * in func_begin it needs to be explicitly set as a before_callback to ensure + * OAPI compatibility. This combination entails that the last wrapped function + * call gets to name the txn. */ NR_PHP_WRAPPER(nr_magento2_soap_handler_preparerequestdata) { zval* svc_class = NULL; @@ -311,6 +367,13 @@ NR_PHP_WRAPPER_END * string $serviceClass * array $methodMetadata * array $arguments + * + * * txn naming scheme: + * In this case, `nr_txn_set_path` is called before `NR_PHP_WRAPPER_CALL` with + * `NR_OK_TO_OVERWRITE` and as this corresponds to calling the wrapped function + * in func_begin it needs to be explicitly set as a before_callback to ensure + * OAPI compatibility. This combination entails that the last wrapped function + * call gets to name the txn. */ NR_PHP_WRAPPER(nr_magento2_soap_handler_prepareoperationinput) { zval* svc_class = NULL; @@ -375,9 +438,16 @@ void nr_magento2_enable(TSRMLS_D) { * dispatch() is overridden and the original method is never invoked, this * hook will not fire. */ +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("Magento\\Framework\\App\\Action\\Action::dispatch"), + nr_magento2_action_dispatch, NULL, NULL); +#else nr_php_wrap_user_function( NR_PSTR("Magento\\Framework\\App\\Action\\Action::dispatch"), nr_magento2_action_dispatch TSRMLS_CC); +#endif /* * Kernel is Magento's built-in cache processor. @@ -400,11 +470,18 @@ void nr_magento2_enable(TSRMLS_D) { * entirely separate routing. We'll access the current route as the input * params are resolved. */ +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + nr_php_wrap_user_function_before_after_clean( + NR_PSTR( + "Magento\\Webapi\\Controller\\Rest\\InputParamsResolver::resolve"), + nr_magento2_inputparamsresolver_resolve, NULL, NULL); +#else nr_php_wrap_user_function( NR_PSTR( "Magento\\Webapi\\Controller\\Rest\\InputParamsResolver::resolve"), nr_magento2_inputparamsresolver_resolve TSRMLS_CC); - +#endif /* * The SOAP controller also implements its own routing logic. There are * effectively three cases in Magento\Webapi\Controller\Soap::dispatch(): @@ -418,7 +495,18 @@ void nr_magento2_enable(TSRMLS_D) { nr_php_wrap_user_function( NR_PSTR("Magento\\Webapi\\Controller\\Soap::_isWsdlListRequest"), nr_magento2_soap_iswsdllistrequest TSRMLS_CC); +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("Magento\\Webapi\\Controller\\Soap\\Request\\Handler::_" + "prepareRequestData"), + nr_magento2_soap_handler_preparerequestdata, NULL, NULL); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("Magento\\Webapi\\Controller\\Soap\\Request\\Handler::" + "prepareOperationInput"), + nr_magento2_soap_handler_prepareoperationinput, NULL, NULL); +#else nr_php_wrap_user_function( NR_PSTR("Magento\\Webapi\\Controller\\Soap\\Request\\Handler::_" "prepareRequestData"), @@ -433,6 +521,7 @@ void nr_magento2_enable(TSRMLS_D) { NR_PSTR("Magento\\Webapi\\Controller\\Soap\\Request\\Handler::" "prepareOperationInput"), nr_magento2_soap_handler_prepareoperationinput TSRMLS_CC); +#endif /* * The Magento_Ui render controllers will, if sent a json Accepts diff --git a/agent/fw_mediawiki.c b/agent/fw_mediawiki.c index 8872e2090..12a1a3a16 100644 --- a/agent/fw_mediawiki.c +++ b/agent/fw_mediawiki.c @@ -24,6 +24,14 @@ * done by trapping ApiMain::__construct. This takes as its first argument a * WebRequest object. That object has an array called 'data'. That array will * contain a member named 'action'. + * + * + * txn naming scheme: + * In this case, `nr_txn_set_path` is called before `NR_PHP_WRAPPER_CALL` with + * `NR_NOT_OK_TO_OVERWRITE`. This entails that the first wrapped call gets to + * name the txn. Corresponds to mediawiki version less than 1.18 and is not + * applicable to OAPI/PHP8+ since they recommend if using PHP8 to use + * MediaWiki 1.38.4+ or 1.39.0+. */ NR_PHP_WRAPPER(nr_mediawiki_name_the_wt_non_api) { char* name = NULL; @@ -70,6 +78,14 @@ NR_PHP_WRAPPER(nr_mediawiki_name_the_wt_non_api) { } NR_PHP_WRAPPER_END +/* + * * txn naming scheme: + * In this case, `nr_txn_set_path` is called before `NR_PHP_WRAPPER_CALL` with + * `NR_NOT_OK_TO_OVERWRITE`. This entails that the first wrapped call gets to + * name the txn. Corresponds to mediawiki version less than 1.18 and is not + * applicable to OAPI/PHP8+ since they recommend if using PHP8 to use + * MediaWiki 1.38.4+ or 1.39.0+. + */ NR_PHP_WRAPPER(nr_mediawiki_name_the_wt_api) { zval* data = NULL; zval* arg1 = NULL; @@ -132,6 +148,14 @@ NR_PHP_WRAPPER_END * custom actions are supported by either adding a listener to the * UnknownAction hook (in 1.18 and older) or by adding to the $wgActions * global. + * + * txn naming scheme: + * In this case, `nr_txn_set_path` is called after `NR_PHP_WRAPPER_CALL` with + * `NR_OK_TO_OVERWRITE` and as this corresponds to calling the wrapped function + * in func_end no change is needed to ensure OAPI compatibility as it will use + * the default func_end after callback. This entails that the first wrapped call + * gets to name the txn. + * */ NR_PHP_WRAPPER(nr_mediawiki_getaction) { char* name = NULL; @@ -175,6 +199,13 @@ NR_PHP_WRAPPER_END * ApiMain object. The action name is kept in the mAction property on that * object, but that property isn't set until ApiMain::setupExecuteAction() is * called, so we'll wait until after that's done. + * + * * txn naming scheme: + * In this case, `nr_txn_set_path` is called after `NR_PHP_WRAPPER_CALL` with + * `NR_NOT_OK_TO_OVERWRITE` and as this corresponds to calling the wrapped + * function in func_end no change is needed to ensure OAPI compatibility as it + * will use the default func_end after callback. This entails that the last + * wrapped call gets to name the txn. */ NR_PHP_WRAPPER(nr_mediawiki_apimain_setupexecuteaction) { zval* action = NULL; diff --git a/agent/fw_slim.c b/agent/fw_slim.c index 841703036..29b4f7aaa 100644 --- a/agent/fw_slim.c +++ b/agent/fw_slim.c @@ -41,6 +41,12 @@ static char* nr_slim_path_from_route(zval* route TSRMLS_DC) { * Wrap the \Slim\Route::dispatch method, which is the happy path for Slim 2.x * routing. i.e. The router has succesfully matched the URL and dispatched the * request to a route. + * + * In this case, `nr_txn_set_path` is called after `NR_PHP_WRAPPER_CALL` with + * `NR_OK_TO_OVERWRITE` and as this corresponds to calling the wrapped function + * in func_end no change is needed to ensure OAPI compatibility as it will use + * the default func_end after callback. This entails that the first wrapped + * function call of this type gets to name the txn. */ NR_PHP_WRAPPER(nr_slim2_route_dispatch) { zval* this_var = NULL; @@ -75,6 +81,12 @@ NR_PHP_WRAPPER_END * Wrap the \Slim\Route::run method, which is the happy path for Slim routing. * i.e. The router has succesfully matched the URL and dispatched the request * to a route. + * + * In this case, `nr_txn_set_path` is called after `NR_PHP_WRAPPER_CALL` with + * `NR_OK_TO_OVERWRITE` and as this corresponds to calling the wrapped function + * in func_end no change is needed to ensure OAPI compatibility as it will use + * the default func_end after callback. This entails that the first wrapped + * function call of this type gets to name the txn. */ NR_PHP_WRAPPER(nr_slim3_4_route_run) { zval* this_var = NULL; diff --git a/agent/fw_symfony4.c b/agent/fw_symfony4.c index 36ba3e98d..904668e0c 100644 --- a/agent/fw_symfony4.c +++ b/agent/fw_symfony4.c @@ -28,7 +28,7 @@ NR_PHP_WRAPPER(nr_symfony4_exception) { /* Get the event that was given. */ event = nr_php_arg_get(1, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); - + /* Call the original function. */ NR_PHP_WRAPPER_CALL; @@ -41,8 +41,9 @@ NR_PHP_WRAPPER(nr_symfony4_exception) { /* * Get the exception from the event. - * Firstly, we check if ExceptionEvent is available - if yes, that means we are using Symfony 5 - */ + * Firstly, we check if ExceptionEvent is available - if yes, that means we + * are using Symfony 5 + */ exception = nr_php_call(event, "getThrowable"); if (!nr_php_is_zval_valid_object(exception)) { exception = nr_php_call(event, "getException"); @@ -55,8 +56,8 @@ NR_PHP_WRAPPER(nr_symfony4_exception) { } if (NR_SUCCESS - != nr_php_error_record_exception( NRPRG(txn), exception, priority, NULL, - &NRPRG(exception_filters) TSRMLS_CC)) { + != nr_php_error_record_exception(NRPRG(txn), exception, priority, NULL, + &NRPRG(exception_filters) TSRMLS_CC)) { nrl_verbosedebug(NRL_TXN, "Symfony 4: unable to record exception"); } @@ -66,6 +67,14 @@ NR_PHP_WRAPPER(nr_symfony4_exception) { } NR_PHP_WRAPPER_END +/* + * txn naming scheme: + * In this case, `nr_txn_set_path` is called before `NR_PHP_WRAPPER_CALL` with + * `NR_OK_TO_OVERWRITE` and as this corresponds to calling the wrapped function + * in func_begin it needs to be explicitly set as a before_callback to ensure + * OAPI compatibility. This entails that the last wrapped call gets to name the + * txn but it is overwritable if another better name comes along. + */ NR_PHP_WRAPPER(nr_symfony4_console_application_run) { zval* command = NULL; zval* input = NULL; @@ -83,15 +92,15 @@ NR_PHP_WRAPPER(nr_symfony4_console_application_run) { */ input = nr_php_arg_get(1, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); if (!nr_php_object_instanceof_class( - input, - "Symfony\\Component\\Console\\Input\\InputInterface" TSRMLS_CC)) { + input, + "Symfony\\Component\\Console\\Input\\InputInterface" TSRMLS_CC)) { goto leave; } command = nr_php_call(input, "getFirstArgument"); if (nr_php_is_zval_non_empty_string(command)) { - nr_txn_set_path("Symfony4", NRPRG(txn), Z_STRVAL_P(command), NR_PATH_TYPE_ACTION, - NR_OK_TO_OVERWRITE); + nr_txn_set_path("Symfony4", NRPRG(txn), Z_STRVAL_P(command), + NR_PATH_TYPE_ACTION, NR_OK_TO_OVERWRITE); } else { /* * Not having any arguments will result in the same behaviour as @@ -108,6 +117,14 @@ NR_PHP_WRAPPER(nr_symfony4_console_application_run) { } NR_PHP_WRAPPER_END +/* + * * In this case, `nr_txn_set_path` is called after `NR_PHP_WRAPPER_CALL` with + * `NR_OK_TO_OVERWRITE` and as this corresponds to calling the wrapped function + * in func_end no change is needed to ensure OAPI compatibility as it will use + * the default func_end after callback. This entails that the first wrapped + * function call of this type gets to name the txn. See more naming logic + * details within the function. + */ NR_PHP_WRAPPER(nr_symfony4_name_the_wt) { zval* event = NULL; zval* request = NULL; @@ -121,11 +138,11 @@ NR_PHP_WRAPPER(nr_symfony4_name_the_wt) { /* * A high level overview of the logic: * - * RouterListener::onKernelRequest() receives a GetResponseEvent (RequestEvent in Symfony 5) parameter, - * which includes the request object accessible via the getRequest() method. - * We want to get the request, then access its attributes: the request - * matcher will create a number of internal attributes prefixed by - * underscores as part of resolving the controller action. + * RouterListener::onKernelRequest() receives a GetResponseEvent (RequestEvent + * in Symfony 5) parameter, which includes the request object accessible via + * the getRequest() method. We want to get the request, then access its + * attributes: the request matcher will create a number of internal attributes + * prefixed by underscores as part of resolving the controller action. * * If the user has given their action method a friendly name via an * annotation or controller option, then this is available in _route. This is @@ -161,7 +178,7 @@ NR_PHP_WRAPPER(nr_symfony4_name_the_wt) { if (route_rval) { if (NR_SUCCESS != nr_symfony_name_the_wt_from_zval(route_rval TSRMLS_CC, - "Symfony 4")) { + "Symfony 4")) { nrl_verbosedebug( NRL_TXN, "Symfony 4: Request::get('_route') returned a non-string"); } @@ -174,7 +191,7 @@ NR_PHP_WRAPPER(nr_symfony4_name_the_wt) { if (controller_rval) { if (NR_SUCCESS != nr_symfony_name_the_wt_from_zval(controller_rval TSRMLS_CC, - "Symfony 4")) { + "Symfony 4")) { nrl_verbosedebug( NRL_TXN, "Symfony 4: Request::get('_controller') returned a non-string"); @@ -219,9 +236,10 @@ void nr_symfony4_enable(TSRMLS_D) { * listener service, which is a pretty deep customisation: chances are a user * who's doing that is quite capable of naming a transaction by hand. */ - nr_php_wrap_user_function(NR_PSTR("Symfony\\Component\\HttpKernel\\" - "EventListener\\RouterListener::onKernelRequest"), - nr_symfony4_name_the_wt TSRMLS_CC); + nr_php_wrap_user_function( + NR_PSTR("Symfony\\Component\\HttpKernel\\" + "EventListener\\RouterListener::onKernelRequest"), + nr_symfony4_name_the_wt TSRMLS_CC); /* * Symfony does a pretty good job of catching errors but that means we @@ -247,7 +265,14 @@ void nr_symfony4_enable(TSRMLS_D) { /* * Listen for Symfony commands so we can name those appropriately. */ +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("Symfony\\Component\\Console\\Command\\Command::run"), + nr_symfony4_console_application_run, NULL, NULL); +#else nr_php_wrap_user_function( - NR_PSTR("Symfony\\Component\\Console\\Command\\Command::run"), - nr_symfony4_console_application_run TSRMLS_CC); + NR_PSTR("Symfony\\Component\\Console\\Command\\Command::run"), + nr_symfony4_console_application_run TSRMLS_CC); +#endif } diff --git a/agent/fw_wordpress.c b/agent/fw_wordpress.c index 078132249..55f9527bb 100644 --- a/agent/fw_wordpress.c +++ b/agent/fw_wordpress.c @@ -593,6 +593,11 @@ static void nr_wordpress_name_the_wt(const zval* tag, /* * apply_filters() is special, since we're interested in it both for * WordPress hook/plugin metrics and for transaction naming. + * + * * txn naming scheme: +* In this case, `nr_txn_set_path` is called after `NR_PHP_WRAPPER_CALL` with +* `NR_NOT_OK_TO_OVERWRITE`. There is an explicit after function `nr_wordpress_apply_filters_after`. This entails that the last wrapped call gets to name the +* txn. */ NR_PHP_WRAPPER(nr_wordpress_apply_filters) { zval* tag = NULL; diff --git a/agent/fw_yii.c b/agent/fw_yii.c index d71dc5796..c6ff7cef8 100644 --- a/agent/fw_yii.c +++ b/agent/fw_yii.c @@ -15,6 +15,13 @@ /* * Set the web transaction name from the action. + * + * * txn naming scheme: + * In this case, `nr_txn_set_path` is called before `NR_PHP_WRAPPER_CALL` with + * `NR_NOT_OK_TO_OVERWRITE` and as this corresponds to calling the wrapped + * function in func_begin it needs to be explicitly set as a before_callback to + * ensure OAPI compatibility. This entails that the first wrapped call gets to + * name the txn. */ NR_PHP_WRAPPER(nr_yii_runWithParams_wrapper) { @@ -82,8 +89,18 @@ NR_PHP_WRAPPER_END * Enable Yii instrumentation. */ void nr_yii_enable(TSRMLS_D) { +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("CAction::runWithParams"), nr_yii_runWithParams_wrapper, NULL, + NULL); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("CInlineAction::runWithParams"), nr_yii_runWithParams_wrapper, + NULL, NULL); +#else nr_php_wrap_user_function(NR_PSTR("CAction::runWithParams"), nr_yii_runWithParams_wrapper TSRMLS_CC); nr_php_wrap_user_function(NR_PSTR("CInlineAction::runWithParams"), nr_yii_runWithParams_wrapper TSRMLS_CC); +#endif } diff --git a/agent/php_wrapper.h b/agent/php_wrapper.h index cc20b7967..cdc3abb8b 100644 --- a/agent/php_wrapper.h +++ b/agent/php_wrapper.h @@ -91,6 +91,49 @@ * 3) clean_callback gets called in the case of dangling segments that occur * because an exception causes the end function hook to NOT be called and thus * the clean function resets any variables. + * 4) unless explicitly setting any of the above callbacks, the default + * callback is set to after_callback. + * + * TXN Naming schemes and understanding how it is affected by function order, + * NR_PHP_WRAPPER_CALL, NR_NOT_OK_TO_OVERWRITE/NR_OK_TO_OVERWRITE + * + * txn naming has been configured to take into account order in which functions + * are processed, NR_NOT_OK_TO_OVERWRITE/NR_OK_TO_OVERWRITE, and whether it is + * called either before or after NR_PHP_WRAPPER_CALL (for pre PHP 8+) or whether + * it is called in func_begin or func_end (for PHP 8+ / OAPI). Txn naming scheme + * is customized per framework according to its requirements and pecularities. + * To determine the txn naming winner in the case of nested functions wrapped + * functions: + * + * 1) IF wrapper function is called before NR_PHP_WRAPPER_CALL or called in + * func_begin AND NR_NOT_OK_TO_OVERWRITE is set for all THEN the FIRST wrapped + * function encountered determines the txn name. + * + * 2) IF wrapper function is called before NR_PHP_WRAPPER_CALL or called in + * func_begin then the LAST wrapped function with NR_OK_TO_OVERWRITE determines + * the txn name. + * + * 3) IF wrapper function is called after NR_PHP_WRAPPER_CALL or called in + * func_end AND NR_NOT_OK_TO_OVERWRITE is set for all THEN the LAST wrapped + * function encountered determines the txn name. + * + * 4) IF wrapper function is called after NR_PHP_WRAPPER_CALL or called in + * func_end then the FIRST wrapped function with NR_OK_TO_OVERWRITE determines + * the txn name. + * + * 5) If there are nested functions that have wrapped functions called before + * NR_PHP_WRAPPER_CALL or called in func_begin AND that also have called after + * NR_PHP_WRAPPER_CALL or called in func_end if the after call uses + * NR_NOT_OK_TO_OVERWRITE, then rule 1 or 2 applies depending on whether a + * before_func used NR_NOT_OK_TO_OVERWRITE or NR_NOT_TO_OVERWRITE. + * + * 6) If there are nested functions that have wrapped functions called before + * NR_PHP_WRAPPER_CALL or called in func_begin AND that also have called after + * NR_PHP_WRAPPER_CALL or called in func_end if the after call uses + * NR_OK_TO_OVERWRITE, then rule 4 applies. + * + * See agent/tests/test_php_wrapper.c `function test_framework_txn_naming` to + * see how it works with frameworks. */ #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO extern nruserfn_t* nr_php_wrap_user_function_before_after_clean( diff --git a/agent/tests/test_php_wrapper.c b/agent/tests/test_php_wrapper.c index 3e59714d9..9e5296ad1 100644 --- a/agent/tests/test_php_wrapper.c +++ b/agent/tests/test_php_wrapper.c @@ -53,6 +53,11 @@ NR_PHP_WRAPPER(test_clean) { } NR_PHP_WRAPPER_END #endif +/* + * endif to match: + * ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + * && !defined OVERWRITE_ZEND_EXECUTE_DATA + */ NR_PHP_WRAPPER(test_add_array) { zval* arg = nr_php_zval_alloc(); @@ -85,6 +90,520 @@ NR_PHP_WRAPPER(test_add_2_arrays) { NR_PHP_WRAPPER_CALL; } NR_PHP_WRAPPER_END + +#if ZEND_MODULE_API_NO >= ZEND_7_4_X_API_NO +NR_PHP_WRAPPER(test_name_txn_before_not_ok) { + nr_txn_set_path("UnitTest", NRPRG(txn), wraprec->funcname, + NR_PATH_TYPE_ACTION, NR_NOT_OK_TO_OVERWRITE); + + NR_PHP_WRAPPER_CALL; +} +NR_PHP_WRAPPER_END + +NR_PHP_WRAPPER(test_name_txn_before_ok) { + nr_txn_set_path("UnitTest", NRPRG(txn), wraprec->funcname, + NR_PATH_TYPE_ACTION, NR_OK_TO_OVERWRITE); + + NR_PHP_WRAPPER_CALL; +} +NR_PHP_WRAPPER_END + +NR_PHP_WRAPPER(test_name_txn_after_not_ok) { + NR_PHP_WRAPPER_CALL; + nr_txn_set_path("UnitTest", NRPRG(txn), wraprec->funcname, + NR_PATH_TYPE_ACTION, NR_NOT_OK_TO_OVERWRITE); +} +NR_PHP_WRAPPER_END + +NR_PHP_WRAPPER(test_name_txn_after_ok) { + NR_PHP_WRAPPER_CALL; + nr_txn_set_path("UnitTest", NRPRG(txn), wraprec->funcname, + NR_PATH_TYPE_ACTION, NR_OK_TO_OVERWRITE); +} +NR_PHP_WRAPPER_END + +static void populate_functions() { + tlib_php_request_eval("function three($a) { return $a; }" TSRMLS_CC); + tlib_php_request_eval("function two($a) { return three($a); }" TSRMLS_CC); + tlib_php_request_eval("function one($a) { return two($a); }" TSRMLS_CC); +} +/* + * This function is meant to wrap/test when only ONE before/after special + * callback is chosen for one, two, and three. If one_before is configured, + * one_after must be NULL. + */ +static void execute_nested_framework_calls(nrspecialfn_t one_before, + nrspecialfn_t one_after, + nrspecialfn_t two_before, + nrspecialfn_t two_after, + nrspecialfn_t three_before, + nrspecialfn_t three_after, + char* expected_name, + char* message) { + zval* expr = NULL; + zval* arg = NULL; + + tlib_php_engine_create("" PTSRMLS_CC); + tlib_php_request_start(); + populate_functions(); + +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + nr_php_wrap_user_function_before_after_clean(NR_PSTR("one"), one_before, + one_after, NULL); + nr_php_wrap_user_function_before_after_clean(NR_PSTR("two"), two_before, + two_after, NULL); + nr_php_wrap_user_function_before_after_clean(NR_PSTR("three"), three_before, + three_after, NULL); +#else + /* + * This will pick up whichever one isn't null. + */ + nr_php_wrap_user_function(NR_PSTR("one"), one_before TSRMLS_CC); + nr_php_wrap_user_function(NR_PSTR("two"), two_before TSRMLS_CC); + nr_php_wrap_user_function(NR_PSTR("three"), three_before TSRMLS_CC); + nr_php_wrap_user_function(NR_PSTR("one"), one_after TSRMLS_CC); + nr_php_wrap_user_function(NR_PSTR("two"), two_after TSRMLS_CC); + nr_php_wrap_user_function(NR_PSTR("three"), three_after TSRMLS_CC); +#endif + + arg = tlib_php_request_eval_expr("1" TSRMLS_CC); + expr = nr_php_call(NULL, "one", arg); + tlib_pass_if_not_null("Runs fine.", expr); + tlib_pass_if_zval_type_is("Should have received the arg value.", IS_LONG, + expr); + tlib_pass_if_str_equal(message, expected_name, NRTXN(path)); + + nr_php_zval_free(&expr); + nr_php_zval_free(&arg); + tlib_php_request_end(); + tlib_php_engine_destroy(TSRMLS_C); +} + +/* + * to customize txn naming per framework, PHP agent uses the order in which + * (functions are processed, NR_NOT_OK_TO_OVERWRITE/NR_OK_TO_OVERWRITE, and + * whether it is called either before or after NR_PHP_WRAPPER_CALL (for pre PHP + * 8+) or whether it is called in func_begin or func_end (for PHP 8+ / OAPI). + * + * This test serves to illustrate and test *framework* txn naming conventions + * and how they are affected by calling nr_tx_set path either 1) before + * nr_wrapper_call for preoapi and in func_begin as a before callback in oapi 2) + * after nr_wrapper_call for preoapi and in func_end as an after callback in + * oapi + * + * The execution order is as follows: + * 1) oapi: `one` before callback OR legacy:any statements before the `one` + * PHP_CALL_WRAPPER + * + * 2) `one` begins execution and calls `two` + * + * 3) oapi: `two` before callback OR legacy: any statements before the + * `two` PHP_CALL_WRAPPER + * + * 4) `two` begins execution and calls `three` + * + * 5) oapi: `three` before callback OR legacy: any statements + * before the `three` PHP_CALL_WRAPPER + * + * 6) `three` begins execution + * + * 7) `three` ends + * + * 8) oapi: `three` after callback OR legacy: any statements + * after the `three` PHP_CALL_WRAPPER + * + * 9) `two` ends + * + * 10) oapi:`two` after callback OR legacy: any statements after the + * `two` PHP_CALL_WRAPPER + * + * 11) `one` ends + * + * 12) oapi: `one` after callback OR legacy: any statements after the `one` + * PHP_CALL_WRAPPER + */ +static void test_framework_txn_naming() { + /* + * This function both tests and illustrates how to use the wrapped function + * special callbacks in the various framework scenarios for naming the txn + * when there are multiple routes for naming (i.e. nested calls that have + * wrapper special functions that all call nr_set_txn_path in different ways. + * Each test case can be considered a "framework" with three different ways of + * naming the txn called by the three nested functions. For all cases, + * function `one` calls `two` calls `three`. + */ + + /* + * case 1) IF wrapper function is called before NR_PHP_WRAPPER_CALL or called + * in func_begin AND NR_NOT_OK_TO_OVERWRITE is set THEN the FIRST wrapped + * function encountered determines the txn name. + * + * All functions set the txn before NR_PHP_WRAPPER_CALL and/or in OAPI + * func_begin and use NR_NOT_OK_TO_OVERWRITE. + * + * Expecting `one` to name txn. + */ + execute_nested_framework_calls( + test_name_txn_before_not_ok, NULL, test_name_txn_before_not_ok, NULL, + test_name_txn_before_not_ok, NULL, "one", + "one:name_before_call:will_not_overwrite,two:name_before_call:will_not_" + "overwrite,three:name_before_call:will_not_overwrite"); + + /* + * + * 2) IF wrapper function is called before NR_PHP_WRAPPER_CALL or called in + * func_begin AND NR_OK_TO_OVERWRITE is set THEN the LAST wrapped function + * encountered determines the txn name. + * + * All functions set the txn before NR_PHP_WRAPPER_CALL and/or in OAPI + * func_begin and use NR_OK_TO_OVERWRITE. + * + * Expecting `three` to name txn. + */ + execute_nested_framework_calls( + test_name_txn_before_ok, NULL, test_name_txn_before_ok, NULL, + test_name_txn_before_ok, NULL, "three", + "one:name_before_call:will_overwrite,two:name_before_call:will_overwrite," + "three:name_before_call:will_overwrite"); + /* + * + * 3) IF wrapper function is called after NR_PHP_WRAPPER_CALL or called in + * func_end AND NR_NOT_OK_TO_OVERWRITE is set THEN the LAST wrapped function + * encountered determines the txn name. + * All functions set the txn after NR_PHP_WRAPPER_CALL and/or in OAPI + * func_begin and use NR_NOT_OK_TO_OVERWRITE. + * + * Expecting `three` to name txn. + */ + execute_nested_framework_calls( + NULL, test_name_txn_after_not_ok, NULL, test_name_txn_after_not_ok, NULL, + test_name_txn_after_not_ok, "three", + "one:name_after_call:will_not_overwrite,two:name_after_call:will_not_" + "overwrite,three:name_after_call:will_not_overwrite"); + + /* + * 4) IF wrapper function is called after NR_PHP_WRAPPER_CALL or called in + * func_end AND NR_OK_TO_OVERWRITE is set THEN the FIRST wrapped function + * encountered determines the txn name. + * + * All functions set the txn before NR_PHP_WRAPPER_CALL and/or in OAPI + * func_begin and use NR_OK_TO_OVERWRITE. + * + * Expecting `one` to name txn. + */ + execute_nested_framework_calls( + NULL, test_name_txn_after_ok, NULL, test_name_txn_after_ok, NULL, + test_name_txn_after_ok, "one", + "one:name_after_call:will_overwrite,two:name_after_call:will_overwrite," + "three:name_after_call:will_overwrite"); + + /* + * 5) If there are nested functions that have wrapped functions called before + * NR_PHP_WRAPPER_CALL or called in func_begin AND that also have called after + * NR_PHP_WRAPPER_CALL or called in func_end if the after call uses + * NR_NOT_OK_TO_OVERWRITE, then rule 1 or 2 applies depending on whether a + * before_func used NR_NOT_OK_TO_OVERWRITE or NR_NOT_TO_OVERWRITE. + * + * 6) If there are nested functions that have wrapped functions called before + * NR_PHP_WRAPPER_CALL or called in func_begin AND that also have called after + * NR_PHP_WRAPPER_CALL or called in func_end if the after call uses + * NR_OK_TO_OVERWRITE, then rule 4 applies. + * + * Basically a mix and match situation where we have some best tries at naming + * a nested function transaction via one txn naming wrapped function, but if + * something better comes along via a DIFFERENT wrapped function, we'd prefer + * that. Special functions are mixed with regards to calling before/after and + * NR_NOT_OK_TO_OVERWRITE/NR_OK_TO_OVERWRITE + */ + + /* + * After only mixes of NR_NOT_OK_TO_OVERWRITE/NR_NOT_OK_TO_OVERWRITE + */ + + /* + * special function for one is called after and uses NR_NOT_OK_TO_OVERWRITE + * special function for two is called after and uses NR_OK_TO_OVERWRITE + * special function for three is called after and uses NR_OK_TO_OVERWRITE + * + * expect txn to be named `two` + */ + execute_nested_framework_calls( + NULL, test_name_txn_after_not_ok, NULL, test_name_txn_after_ok, NULL, + test_name_txn_after_ok, "two", + "one:name_after_call:will_not_overwrite,two:name_after_call:will_" + "overwrite,three:name_after_call:will_overwrite"); + + /* + * special function for one is called after and uses NR_NOT_OK_TO_OVERWRITE + * special function for two is called after and uses NR_NOT_OK_TO_OVERWRITE + * special function for three is called after and uses NR_OK_TO_OVERWRITE + * + * expect txn to be named `three` + */ + execute_nested_framework_calls( + NULL, test_name_txn_after_not_ok, NULL, test_name_txn_after_not_ok, NULL, + test_name_txn_after_ok, "three", + "one:name_after_call:will_not_overwrite,two:name_after_call:will_not_" + "overwrite,three:name_after_call:will_overwrite"); + + /* + * special function for one is called after and uses NR_NOT_OK_TO_OVERWRITE + * special function for two is called after and uses NR_OK_TO_OVERWRITE + * special function for three is called after and uses NR_NOT_OK_TO_OVERWRITE + * + * expect txn to be named `two` + */ + execute_nested_framework_calls( + NULL, test_name_txn_after_not_ok, NULL, test_name_txn_after_ok, NULL, + test_name_txn_after_not_ok, "two", + "one:name_after_call:will_not_overwrite,two:name_after_call:will_" + "overwrite,three:name_after_call:will_not_overwrite"); + + /* + * special function for one is called after and uses NR_OK_TO_OVERWRITE + * special function for two is called after and uses NR_NOT_OK_TO_OVERWRITE + * special function for three is called after and uses NR_OK_TO_OVERWRITE + * + * expect txn to be named `one` + */ + execute_nested_framework_calls( + NULL, test_name_txn_after_ok, NULL, test_name_txn_after_not_ok, NULL, + test_name_txn_after_ok, "one", + "one:name_after_call:will_overwrite,two:name_after_call:will_not_" + "overwrite,three:name_after_call:will_overwrite"); + + /* + * special function for one is called after and uses NR_OK_TO_OVERWRITE + * special function for two is called after and uses NR_NOT_OK_TO_OVERWRITE + * special function for three is called after and uses NR_NOT_OK_TO_OVERWRITE + * + * expect txn to be named `one` + */ + execute_nested_framework_calls( + NULL, test_name_txn_after_ok, NULL, test_name_txn_after_not_ok, NULL, + test_name_txn_after_not_ok, "one", + "one:name_after_call:will_overwrite,two:name_after_call:will_not_" + "overwrite,three:name_after_call:will_not_overwrite"); + + /* + * special function for one is called after and uses NR_OK_TO_OVERWRITE + * special function for two is called after and uses NR_OK_TO_OVERWRITE + * special function for three is called after and uses NR_NOT_OK_TO_OVERWRITE + * + * expect txn to be named `one` + */ + execute_nested_framework_calls( + NULL, test_name_txn_after_ok, NULL, test_name_txn_after_ok, NULL, + test_name_txn_after_not_ok, "one", + "one:name_after_call:will_overwrite,two:name_after_call:will_overwrite," + "three:name_after_call:will_not_overwrite"); + + /* + * Before only mixes of NR_NOT_OK_TO_OVERWRITE/NR_NOT_OK_TO_OVERWRITE + */ + + /* + * special function for one is called before and uses NR_NOT_OK_TO_OVERWRITE + * special function for two is called before and uses NR_OK_TO_OVERWRITE + * special function for three is called before and uses NR_OK_TO_OVERWRITE + * + * expect txn to be named `three` + */ + execute_nested_framework_calls( + test_name_txn_before_not_ok, NULL, test_name_txn_before_ok, NULL, + test_name_txn_before_ok, NULL, "three", + "one:name_before_call:will_not_overwrite,two:name_before_call:will_" + "overwrite,three:name_before_call:will_overwrite"); + + /* + * special function for one is called before and uses NR_NOT_OK_TO_OVERWRITE + * special function for two is called before and uses NR_NOT_OK_TO_OVERWRITE + * special function for three is called before and uses NR_OK_TO_OVERWRITE + * + * expect txn to be named `three` + */ + execute_nested_framework_calls( + test_name_txn_before_not_ok, NULL, test_name_txn_before_not_ok, NULL, + test_name_txn_before_ok, NULL, "three", + "one:name_before_call:will_not_overwrite,two:name_before_call:will_not_" + "overwrite,three:name_before_call:will_overwrite"); + + /* + * special function for one is called before and uses NR_NOT_OK_TO_OVERWRITE + * special function for two is called before and uses NR_OK_TO_OVERWRITE + * special function for three is called before and uses NR_NOT_OK_TO_OVERWRITE + * + * expect txn to be named `two` + */ + execute_nested_framework_calls( + test_name_txn_before_not_ok, NULL, test_name_txn_before_ok, NULL, + test_name_txn_before_not_ok, NULL, "two", + "one:name_before_call:will_not_overwrite,two:name_before_call:will_" + "overwrite,three:name_before_call:will_not_overwrite"); + + /* + * special function for one is called before and uses NR_OK_TO_OVERWRITE + * special function for two is called before and uses NR_NOT_OK_TO_OVERWRITE + * special function for three is called before and uses NR_OK_TO_OVERWRITE + * + * expect txn to be named `three` + */ + execute_nested_framework_calls( + test_name_txn_before_ok, NULL, test_name_txn_before_not_ok, NULL, + test_name_txn_before_ok, NULL, "three", + "one:name_before_call:will_overwrite,two:name_before_call:will_not_" + "overwrite,three:name_before_call:will_overwrite"); + + /* + * special function for one is called before and uses NR_OK_TO_OVERWRITE + * special function for two is called before and uses NR_NOT_OK_TO_OVERWRITE + * special function for three is called before and uses NR_NOT_OK_TO_OVERWRITE + * + * expect txn to be named `one` + */ + execute_nested_framework_calls( + test_name_txn_before_ok, NULL, test_name_txn_before_not_ok, NULL, + test_name_txn_before_not_ok, NULL, "one", + "one:name_before_call:will_overwrite,two:name_before_call:will_not_" + "overwrite,three:name_before_call:will_not_overwrite"); + + /* + * special function for one is called before and uses NR_OK_TO_OVERWRITE + * special function for two is called before and uses NR_OK_TO_OVERWRITE + * special function for three is called before and uses NR_NOT_OK_TO_OVERWRITE + * + * expect txn to be named `two` + */ + execute_nested_framework_calls( + test_name_txn_before_ok, NULL, test_name_txn_before_ok, NULL, + test_name_txn_before_not_ok, NULL, "two", + "one:name_before_call:will_overwrite,two:name_before_call:will_overwrite," + "three:name_before_call:will_not_overwrite"); + + /* + * Before/After and NR_NOT_OK_TO_OVERWRITE/NR_NOT_OK_TO_OVERWRITE mixes + */ + + /* + * special function for one is called after and uses NR_NOT_OK_TO_OVERWRITE + * special function for two is called before and uses NR_OK_TO_OVERWRITE + * special function for three is called before and uses NR_OK_TO_OVERWRITE + * + * expect txn to be named `three` + */ + execute_nested_framework_calls( + NULL, test_name_txn_after_not_ok, test_name_txn_before_ok, NULL, + test_name_txn_before_ok, NULL, "three", + "one:name_after_call:will_not_overwrite,two:name_before_call:will_" + "overwrite,three:name_before_call:will_overwrite"); + + /* + * special function for one is called after and uses NR_OK_TO_OVERWRITE + * special function for two is called before and uses NR_OK_TO_OVERWRITE + * special function for three is called before and uses NR_OK_TO_OVERWRITE + * + * expect txn to be named `one` + */ + execute_nested_framework_calls( + NULL, test_name_txn_after_ok, test_name_txn_before_ok, NULL, + test_name_txn_before_ok, NULL, "one", + "one:name_after_call:will_overwrite,two:name_before_call:will_overwrite," + "three:name_before_call:will_overwrite"); + + /* + * special function for one is called before and uses NR_NOT_OK_TO_OVERWRITE + * special function for two is called after and uses NR_NOT_OK_TO_OVERWRITE + * special function for three is called before and uses NR_OK_TO_OVERWRITE + * + * expect txn to be named `three` + */ + execute_nested_framework_calls( + test_name_txn_before_not_ok, NULL, NULL, test_name_txn_after_not_ok, + test_name_txn_before_ok, NULL, "three", + "one:name_before_call:will_not_overwrite,two:name_after_call:will_not_" + "overwrite,three:name_before_call:will_overwrite"); + + /* + * special function for one is called before and uses NR_NOT_OK_TO_OVERWRITE + * special function for two is called after and uses NR_NOT_OK_TO_OVERWRITE + * special function for three is called before and uses NR_OK_TO_OVERWRITE + * + * expect txn to be named `two` + */ + execute_nested_framework_calls( + test_name_txn_before_not_ok, NULL, NULL, test_name_txn_after_ok, + test_name_txn_before_ok, NULL, "two", + "one:name_before_call:will_not_overwrite,two:name_after_call:will_" + "overwrite,three:name_before_call:will_overwrite"); + + /* + * special function for one is called before and uses NR_NOT_OK_TO_OVERWRITE + * special function for two is called before and uses NR_OK_TO_OVERWRITE + * special function for three is called after and uses NR_NOT_OK_TO_OVERWRITE + * + * expect txn to be named `two` + */ + execute_nested_framework_calls( + test_name_txn_before_not_ok, NULL, test_name_txn_before_ok, NULL, NULL, + test_name_txn_after_not_ok, "two", + "one:name_before_call:will_not_overwrite,two:name_before_call:will_" + "overwrite,three:name_after_call:will_not_overwrite"); + + /* + * special function for one is called before and uses NR_NOT_OK_TO_OVERWRITE + * special function for two is called before and uses NR_OK_TO_OVERWRITE + * special function for three is called after and uses NR_OK_TO_OVERWRITE + * + * expect txn to be named `three` + */ + execute_nested_framework_calls( + test_name_txn_before_not_ok, NULL, test_name_txn_before_ok, NULL, NULL, + test_name_txn_after_ok, "three", + "one:name_before_call:will_not_overwrite,two:name_before_call:will_" + "overwrite,three:name_after_call:will_overwrite"); + + /* + * special function for one is called after and uses NR_OK_TO_OVERWRITE + * special function for two is called after and uses NR_NOT_OK_TO_OVERWRITE + * special function for three is called before and uses NR_OK_TO_OVERWRITE + * + * expect txn to be named `one` + */ + execute_nested_framework_calls( + NULL, test_name_txn_after_ok, NULL, test_name_txn_after_not_ok, + test_name_txn_before_ok, NULL, "one", + "one:name_after_call:will_overwrite,two:name_after_call:will_not_" + "overwrite,three:name_before_call:will_overwrite"); + + /* + * special function for one is called after and uses NR_OK_TO_OVERWRITE + * special function for two is called before and uses NR_NOT_OK_TO_OVERWRITE + * special function for three is called after and uses NR_NOT_OK_TO_OVERWRITE + * + * expect txn to be named `two` + */ + execute_nested_framework_calls( + NULL, test_name_txn_after_not_ok, test_name_txn_before_not_ok, NULL, NULL, + test_name_txn_after_not_ok, "two", + "one:name_after_call:will_overwrite,two:name_before_call:will_not_" + "overwrite,three:name_after_call:will_not_overwrite"); + + /* + * special function for one is called before and uses NR_OK_TO_OVERWRITE + * special function for two is called after and uses NR_OK_TO_OVERWRITE + * special function for three is called after and uses NR_NOT_OK_TO_OVERWRITE + * + * expect txn to be named `two` + */ + execute_nested_framework_calls( + test_name_txn_before_ok, NULL, NULL, test_name_txn_after_ok, NULL, + test_name_txn_after_not_ok, "two", + "one:name_before_call:will_overwrite,two:name_after_call:will_overwrite," + "three:name_after_call:will_not_overwrite"); +} +#endif /* ZEND_MODULE_API_NO >= ZEND_7_4_X_API_NO */ + static void test_add_arg(TSRMLS_D) { zval* expr = NULL; zval* arg = NULL; @@ -605,6 +1124,15 @@ void test_main(void* p NRUNUSED) { test_before_after_clean(); #endif tlib_php_engine_destroy(TSRMLS_C); + /* + * The Jenkins PHP 7.3 nodes are unable to handle the multiple + * create/destroys, but works on more recent OSs. + */ +#if ZEND_MODULE_API_NO >= ZEND_7_4_X_API_NO + if (PHP_VERSION_ID < 80000 && PHP_VERSION_ID > 80002) { + test_framework_txn_naming(); + } +#endif } #else /* PHP 7.3 */ void test_main(void* p NRUNUSED) {} From f3c38a307279639e1899e3d12a1436e3987293a8 Mon Sep 17 00:00:00 2001 From: Michael Fulbright <89205663+mfulb@users.noreply.github.com> Date: Fri, 10 Mar 2023 08:39:27 -0500 Subject: [PATCH 19/56] test(agent): Fix tests to CLM on (#637) Modify newly added oapi tests to conform with CLM on by default. --- ...stom_parameter_nested_caught_exception.php | 35 ++++++++++++++--- ...test_add_custom_parameter_nested_happy.php | 37 +++++++++++++++--- ...om_parameter_nested_uncaught_exception.php | 38 ++++++++++++++---- ...vent_parameter_nested_caught_exception.php | 39 +++++++++++++++---- ...test_span_event_parameter_nested_happy.php | 37 +++++++++++++++--- ...nt_parameter_nested_uncaught_exception.php | 36 ++++++++++++++--- ...create_payload_nested_caught_exception.php | 19 +++++++-- .../test_create_payload_nested_happy.php | 19 ++++++++- ...eate_payload_nested_uncaught_exception.php | 11 +++++- ...t_notice_error_nested_caught_exception.php | 33 +++++++++++++--- .../test_notice_error_nested_happy.php | 35 ++++++++++++++--- ...notice_error_nested_uncaught_exception.php | 33 +++++++++++++--- ...ser_attributes_nested_caught_exception.php | 35 ++++++++++++++--- .../test_set_user_attributes_nested_happy.php | 37 +++++++++++++++--- ...r_attributes_nested_uncaught_exception.php | 34 +++++++++++++--- 15 files changed, 405 insertions(+), 73 deletions(-) diff --git a/tests/integration/api/add_custom_parameter/test_add_custom_parameter_nested_caught_exception.php b/tests/integration/api/add_custom_parameter/test_add_custom_parameter_nested_caught_exception.php index 217fc10b2..f66452c15 100644 --- a/tests/integration/api/add_custom_parameter/test_add_custom_parameter_nested_caught_exception.php +++ b/tests/integration/api/add_custom_parameter/test_add_custom_parameter_nested_caught_exception.php @@ -7,6 +7,13 @@ Tests newrelic_add_custom_parameter() on a nested path that includes a caught exception. */ +/*SKIPIF + Date: Fri, 10 Mar 2023 12:04:04 -0500 Subject: [PATCH 20/56] chore(install): Disable ZTS installs (#636) Removes support for ZTS installs in `newrelic-install.sh` as disables building ZTS artifacts. --------- Co-authored-by: Amber Sistla --- .github/workflows/agent-build.yml | 11 +---------- agent/newrelic-install.sh | 30 ++++++++++++++---------------- make/release.mk | 23 ----------------------- 3 files changed, 15 insertions(+), 49 deletions(-) diff --git a/.github/workflows/agent-build.yml b/.github/workflows/agent-build.yml index c98e77e08..f1576d278 100644 --- a/.github/workflows/agent-build.yml +++ b/.github/workflows/agent-build.yml @@ -40,7 +40,7 @@ jobs: strategy: matrix: os: [linux] - php_ver: ['7.0', '7.1-zts', '7.2', '7.3', '7.4', '8.0', '8.0-zts', '8.1', '8.1-zts', '8.2'] + php_ver: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2'] arch: ['x64', 'x86'] exclude: # Excludes PHP 5.x, 7.x on linux 32-bit @@ -48,9 +48,6 @@ jobs: - os: linux php_ver: '7.0' arch: 'x86' - - os: linux - php_ver: '7.1-zts' - arch: 'x86' - os: linux php_ver: '7.2' arch: 'x86' @@ -63,15 +60,9 @@ jobs: - os: linux php_ver: '8.0' arch: 'x86' - - os: linux - php_ver: '8.0-zts' - arch: 'x86' - os: linux php_ver: '8.1' arch: 'x86' - - os: linux - php_ver: '8.1-zts' - arch: 'x86' - os: linux php_ver: '8.2' arch: 'x86' diff --git a/agent/newrelic-install.sh b/agent/newrelic-install.sh index 58f33180d..ddb2d068f 100755 --- a/agent/newrelic-install.sh +++ b/agent/newrelic-install.sh @@ -343,8 +343,6 @@ check_file "${ilibdir}/scripts/newrelic.ini.template" for pmv in "20121212" "20131226" "20151012" "20160303" "20170718" \ "20180731" "20190902" "20200930" "20210902" "20220829"; do check_file "${ilibdir}/agent/${arch}/newrelic-${pmv}.so" - # remove following line when ZTS removed from releases - check_file "${ilibdir}/agent/${arch}/newrelic-${pmv}-zts.so" done if [ -n "${fmissing}" ]; then @@ -1291,20 +1289,20 @@ does not exist. This particular instance of PHP will be skipped. fi log "${pdir}: pi_zts=${pi_zts}" -# uncomment when ZTS binaries are removed -# if [ "${pi_zts}" = "yes" ]; then -# msg=$( -# cat << EOF -# -#An unsupported PHP ZTS build has been detected. Please refer to this link: -# https://docs.newrelic.com/docs/apm/agents/php-agent/getting-started/php-agent-compatibility-requirements/ -#to view compatibilty requirements for the the New Relic PHP agent. -#The install will now exit. -#EOF -#) -# error "${msg}" -# exit 1 -# fi +# zts installs are no longer supported + if [ "${pi_zts}" = "yes" ]; then + msg=$( + cat << EOF + +An unsupported PHP ZTS build has been detected. Please refer to this link: + https://docs.newrelic.com/docs/apm/agents/php-agent/getting-started/php-agent-compatibility-requirements/ +to view compatibilty requirements for the the New Relic PHP agent. +The install will now exit. +EOF +) + error "${msg}" + exit 1 + fi # # This is where we figure out where to put the ini file, if at all. We only do diff --git a/make/release.mk b/make/release.mk index 814f4a207..a17828b70 100644 --- a/make/release.mk +++ b/make/release.mk @@ -95,14 +95,6 @@ release-agent: Makefile | releases/$(RELEASE_OS)/agent/$(RELEASE_ARCH)/ for PHP in $(SUPPORTED_PHP) ; do \ $(MAKE) agent-clean; $(MAKE) release-$$PHP-no-zts; \ done -# -# Next build ZTS binaries of the PHP versions requested that are supported -# on this OS. -# - for PHP in $(SUPPORTED_PHP) ; do \ - $(MAKE) agent-clean; $(MAKE) release-$$PHP-zts; \ - done - # # Add a new target to build the agent against build machines. @@ -121,27 +113,12 @@ release-$1-gha: Makefile agent | releases/$$(RELEASE_OS)/agent/$$(RELEASE_ARCH)/ @cp agent/modules/newrelic.so "releases/$$(RELEASE_OS)/agent/$$(RELEASE_ARCH)/newrelic-$2.so" @test -e agent/newrelic.map && cp agent/newrelic.map "releases/$$(RELEASE_OS)/agent/$$(RELEASE_ARCH)/newrelic-$2.map" || true -# -# Target for zts GHA releases. -# -release-$1-zts-gha: PHPIZE := /usr/local/bin/phpize -release-$1-zts-gha: PHP_CONFIG := /usr/local/bin/php-config -release-$1-zts-gha: Makefile agent | releases/$$(RELEASE_OS)/agent/$$(RELEASE_ARCH)/ - @cp agent/modules/newrelic.so "releases/$$(RELEASE_OS)/agent/$$(RELEASE_ARCH)/newrelic-$2-zts.so" - @test -e agent/newrelic.map && cp agent/newrelic.map "releases/$$(RELEASE_OS)/agent/$$(RELEASE_ARCH)/newrelic-$2-zts.map" || true - release-$1-no-zts: PHPIZE := /opt/nr/lamp/bin/phpize-$1-no-zts release-$1-no-zts: PHP_CONFIG := /opt/nr/lamp/bin/php-config-$1-no-zts release-$1-no-zts: Makefile agent | releases/$$(RELEASE_OS)/agent/$$(RELEASE_ARCH)/ @cp agent/modules/newrelic.so "releases/$$(RELEASE_OS)/agent/$$(RELEASE_ARCH)/newrelic-$2.so" @test -e agent/newrelic.map && cp agent/newrelic.map "releases/$$(RELEASE_OS)/agent/$$(RELEASE_ARCH)/newrelic-$2.map" || true -release-$1-zts: PHPIZE := /opt/nr/lamp/bin/phpize-$1-zts -release-$1-zts: PHP_CONFIG := /opt/nr/lamp/bin/php-config-$1-zts -release-$1-zts: Makefile agent | releases/$$(RELEASE_OS)/agent/$$(RELEASE_ARCH)/ - @cp agent/modules/newrelic.so "releases/$$(RELEASE_OS)/agent/$$(RELEASE_ARCH)/newrelic-$2-zts.so" - @test -e agent/newrelic.map && cp agent/newrelic.map "releases/$$(RELEASE_OS)/agent/$$(RELEASE_ARCH)/newrelic-$2-zts.map" || true - endef $(eval $(call RELEASE_AGENT_TARGET,8.2,20220829)) From cd99428b100a03564f12c0feb54e2a7b62395cdd Mon Sep 17 00:00:00 2001 From: Michal Nowacki Date: Fri, 28 Apr 2023 12:05:26 -0400 Subject: [PATCH 21/56] Revert "chore(install): Disable ZTS installs" (#662) Reverts newrelic/newrelic-php-agent#636. This change is obsoleted by #661. Revert is needed to avoid conflicts when dev is merged into oapi. --- .github/workflows/agent-build.yml | 11 ++++++++++- agent/newrelic-install.sh | 30 ++++++++++++++++-------------- make/release.mk | 23 +++++++++++++++++++++++ 3 files changed, 49 insertions(+), 15 deletions(-) diff --git a/.github/workflows/agent-build.yml b/.github/workflows/agent-build.yml index f1576d278..c98e77e08 100644 --- a/.github/workflows/agent-build.yml +++ b/.github/workflows/agent-build.yml @@ -40,7 +40,7 @@ jobs: strategy: matrix: os: [linux] - php_ver: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2'] + php_ver: ['7.0', '7.1-zts', '7.2', '7.3', '7.4', '8.0', '8.0-zts', '8.1', '8.1-zts', '8.2'] arch: ['x64', 'x86'] exclude: # Excludes PHP 5.x, 7.x on linux 32-bit @@ -48,6 +48,9 @@ jobs: - os: linux php_ver: '7.0' arch: 'x86' + - os: linux + php_ver: '7.1-zts' + arch: 'x86' - os: linux php_ver: '7.2' arch: 'x86' @@ -60,9 +63,15 @@ jobs: - os: linux php_ver: '8.0' arch: 'x86' + - os: linux + php_ver: '8.0-zts' + arch: 'x86' - os: linux php_ver: '8.1' arch: 'x86' + - os: linux + php_ver: '8.1-zts' + arch: 'x86' - os: linux php_ver: '8.2' arch: 'x86' diff --git a/agent/newrelic-install.sh b/agent/newrelic-install.sh index ddb2d068f..58f33180d 100755 --- a/agent/newrelic-install.sh +++ b/agent/newrelic-install.sh @@ -343,6 +343,8 @@ check_file "${ilibdir}/scripts/newrelic.ini.template" for pmv in "20121212" "20131226" "20151012" "20160303" "20170718" \ "20180731" "20190902" "20200930" "20210902" "20220829"; do check_file "${ilibdir}/agent/${arch}/newrelic-${pmv}.so" + # remove following line when ZTS removed from releases + check_file "${ilibdir}/agent/${arch}/newrelic-${pmv}-zts.so" done if [ -n "${fmissing}" ]; then @@ -1289,20 +1291,20 @@ does not exist. This particular instance of PHP will be skipped. fi log "${pdir}: pi_zts=${pi_zts}" -# zts installs are no longer supported - if [ "${pi_zts}" = "yes" ]; then - msg=$( - cat << EOF - -An unsupported PHP ZTS build has been detected. Please refer to this link: - https://docs.newrelic.com/docs/apm/agents/php-agent/getting-started/php-agent-compatibility-requirements/ -to view compatibilty requirements for the the New Relic PHP agent. -The install will now exit. -EOF -) - error "${msg}" - exit 1 - fi +# uncomment when ZTS binaries are removed +# if [ "${pi_zts}" = "yes" ]; then +# msg=$( +# cat << EOF +# +#An unsupported PHP ZTS build has been detected. Please refer to this link: +# https://docs.newrelic.com/docs/apm/agents/php-agent/getting-started/php-agent-compatibility-requirements/ +#to view compatibilty requirements for the the New Relic PHP agent. +#The install will now exit. +#EOF +#) +# error "${msg}" +# exit 1 +# fi # # This is where we figure out where to put the ini file, if at all. We only do diff --git a/make/release.mk b/make/release.mk index a17828b70..814f4a207 100644 --- a/make/release.mk +++ b/make/release.mk @@ -95,6 +95,14 @@ release-agent: Makefile | releases/$(RELEASE_OS)/agent/$(RELEASE_ARCH)/ for PHP in $(SUPPORTED_PHP) ; do \ $(MAKE) agent-clean; $(MAKE) release-$$PHP-no-zts; \ done +# +# Next build ZTS binaries of the PHP versions requested that are supported +# on this OS. +# + for PHP in $(SUPPORTED_PHP) ; do \ + $(MAKE) agent-clean; $(MAKE) release-$$PHP-zts; \ + done + # # Add a new target to build the agent against build machines. @@ -113,12 +121,27 @@ release-$1-gha: Makefile agent | releases/$$(RELEASE_OS)/agent/$$(RELEASE_ARCH)/ @cp agent/modules/newrelic.so "releases/$$(RELEASE_OS)/agent/$$(RELEASE_ARCH)/newrelic-$2.so" @test -e agent/newrelic.map && cp agent/newrelic.map "releases/$$(RELEASE_OS)/agent/$$(RELEASE_ARCH)/newrelic-$2.map" || true +# +# Target for zts GHA releases. +# +release-$1-zts-gha: PHPIZE := /usr/local/bin/phpize +release-$1-zts-gha: PHP_CONFIG := /usr/local/bin/php-config +release-$1-zts-gha: Makefile agent | releases/$$(RELEASE_OS)/agent/$$(RELEASE_ARCH)/ + @cp agent/modules/newrelic.so "releases/$$(RELEASE_OS)/agent/$$(RELEASE_ARCH)/newrelic-$2-zts.so" + @test -e agent/newrelic.map && cp agent/newrelic.map "releases/$$(RELEASE_OS)/agent/$$(RELEASE_ARCH)/newrelic-$2-zts.map" || true + release-$1-no-zts: PHPIZE := /opt/nr/lamp/bin/phpize-$1-no-zts release-$1-no-zts: PHP_CONFIG := /opt/nr/lamp/bin/php-config-$1-no-zts release-$1-no-zts: Makefile agent | releases/$$(RELEASE_OS)/agent/$$(RELEASE_ARCH)/ @cp agent/modules/newrelic.so "releases/$$(RELEASE_OS)/agent/$$(RELEASE_ARCH)/newrelic-$2.so" @test -e agent/newrelic.map && cp agent/newrelic.map "releases/$$(RELEASE_OS)/agent/$$(RELEASE_ARCH)/newrelic-$2.map" || true +release-$1-zts: PHPIZE := /opt/nr/lamp/bin/phpize-$1-zts +release-$1-zts: PHP_CONFIG := /opt/nr/lamp/bin/php-config-$1-zts +release-$1-zts: Makefile agent | releases/$$(RELEASE_OS)/agent/$$(RELEASE_ARCH)/ + @cp agent/modules/newrelic.so "releases/$$(RELEASE_OS)/agent/$$(RELEASE_ARCH)/newrelic-$2-zts.so" + @test -e agent/newrelic.map && cp agent/newrelic.map "releases/$$(RELEASE_OS)/agent/$$(RELEASE_ARCH)/newrelic-$2-zts.map" || true + endef $(eval $(call RELEASE_AGENT_TARGET,8.2,20220829)) From 21a383b1c329649c04cd51be6452626f232ee4c0 Mon Sep 17 00:00:00 2001 From: Michal Nowacki Date: Wed, 26 Jul 2023 11:16:50 -0400 Subject: [PATCH 22/56] tests: 'enhance' logging libraries mocks (#699) Empty files don't get executed by PHP 8.2 when the agent uses Observer API to hook into Zend engine, therefore mocks needs to do something - enhance them with a 'noop' statement - `echo "";` --- .../analog/vendor/analog/analog/lib/Analog/Analog.php | 5 ++++- .../logging/cakephp-log/vendor/cakephp/log/Log.php | 5 ++++- .../vendor/consolidation/log/src/Logger.php | 5 ++++- .../laminas-log/vendor/laminas/laminas-log/src/Logger.php | 5 ++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/integration/logging/analog/vendor/analog/analog/lib/Analog/Analog.php b/tests/integration/logging/analog/vendor/analog/analog/lib/Analog/Analog.php index 20ba2c149..403713fd2 100644 --- a/tests/integration/logging/analog/vendor/analog/analog/lib/Analog/Analog.php +++ b/tests/integration/logging/analog/vendor/analog/analog/lib/Analog/Analog.php @@ -1,3 +1,6 @@ Date: Tue, 1 Aug 2023 16:10:01 -0400 Subject: [PATCH 23/56] tests: fix predis/test_pipeline expectation (#704) restore expected values updated in 329acaad706ca14c3e3678681c86f97b98eee359 which got overwritten with c974a2e11293c10739e7ff60ffb229979dd563b6. --- tests/integration/predis/test_pipeline.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/integration/predis/test_pipeline.php b/tests/integration/predis/test_pipeline.php index da3fb697d..2e3646d13 100644 --- a/tests/integration/predis/test_pipeline.php +++ b/tests/integration/predis/test_pipeline.php @@ -20,11 +20,11 @@ [ [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/all"}, [1, "??", "??", "??", "??", "??"]], [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/all"}, [12, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/allOther"}, [12, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/Redis/all"}, [12, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/Redis/allOther"}, [12, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/instance/Redis/redisdb/6379"}, [12, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/all"}, [13, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/allOther"}, [13, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/Redis/all"}, [13, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/Redis/allOther"}, [13, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/instance/Redis/redisdb/6379"}, [13, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Redis/del"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Redis/del", "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], From fdb79fdcf4179c7bda4c1d20b5804456fb56ee07 Mon Sep 17 00:00:00 2001 From: Michal Nowacki Date: Tue, 8 Aug 2023 11:49:24 -0400 Subject: [PATCH 24/56] suppress `Invalid read` error in test_redis (#707) A call to `zend_map_ptr_reset` in `zend_deactivate` triggers `Invalid read of size 8` valgrind error in test_redis during `tlib_php_request_eval_expr` and `nr_php_zval_free` in the last php request lifecycle when Observer API is used to hook into Zend engine. This error is triggered on all PHPs with this patch: https://github.com/php/php-src/commit/ff62d117a35509699f8bac8b0750a956914da1b7 --- agent/tests/valgrind-suppressions | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/agent/tests/valgrind-suppressions b/agent/tests/valgrind-suppressions index 2fb7f2504..2f701865c 100644 --- a/agent/tests/valgrind-suppressions +++ b/agent/tests/valgrind-suppressions @@ -128,3 +128,25 @@ ... fun:zend_string_equal_val } + +{ + + Memcheck:Addr8 + fun:_zend_observe_fcall_begin + ... + fun:tlib_php_request_eval_expr + fun:test_*_datastore_instance + fun:test_main + ... +} + +{ + + Memcheck:Addr8 + fun:_zend_observe_fcall_begin + ... + fun:nr_php_zval_free + fun:test_*_datastore_instance + fun:test_main + ... +} From 33fed1dcc129a4c1b09131bc91de5f4c841040ee Mon Sep 17 00:00:00 2001 From: ZNeumann Date: Tue, 8 Aug 2023 12:44:31 -0600 Subject: [PATCH 25/56] fix(agent): set oapi callbacks the right way (#700) When wrapping a user function do not add special instrumentation if ANY special instrumentation (before/after/cleanup) has already been set. This approach matches pre-oapi instrumentation. --- agent/php_wrapper.c | 63 +++++++++++++++++++++------------------------ agent/php_wrapper.h | 5 ++-- 2 files changed, 33 insertions(+), 35 deletions(-) diff --git a/agent/php_wrapper.c b/agent/php_wrapper.c index 7d618863a..6a3ee536d 100644 --- a/agent/php_wrapper.c +++ b/agent/php_wrapper.c @@ -23,45 +23,42 @@ nruserfn_t* nr_php_wrap_user_function_before_after_clean_with_transience( return wraprec; } - if (after_callback) { - if (is_instrumentation_set(wraprec->special_instrumentation, - after_callback)) { - nrl_verbosedebug( - NRL_INSTRUMENT, - "%s: attempting to set special_instrumentation for %.*s, but " - "it is already set", - __func__, NRSAFELEN(namelen), NRBLANKSTR(name)); - } else { - wraprec->special_instrumentation = after_callback; - } + /* If any of the callbacks we are attempting to set are already set to + * something else, we want to exit without setting new callbacks */ + if (is_instrumentation_set_and_not_equal(wraprec->special_instrumentation, + after_callback)) { + nrl_verbosedebug( + NRL_INSTRUMENT, + "%s: attempting to set special_instrumentation for %.*s, but " + "it is already set", + __func__, NRSAFELEN(namelen), NRBLANKSTR(name)); + return wraprec; } - if (before_callback) { - if (is_instrumentation_set(wraprec->special_instrumentation_before, - before_callback)) { - nrl_verbosedebug(NRL_INSTRUMENT, - "%s: attempting to set special_instrumentation_before " - "for %.*s, but " - "it is already set", - __func__, NRSAFELEN(namelen), NRBLANKSTR(name)); - } else { - wraprec->special_instrumentation_before = before_callback; - } + if (is_instrumentation_set_and_not_equal(wraprec->special_instrumentation_before, + before_callback)) { + nrl_verbosedebug(NRL_INSTRUMENT, + "%s: attempting to set special_instrumentation_before " + "for %.*s, but " + "it is already set", + __func__, NRSAFELEN(namelen), NRBLANKSTR(name)); + return wraprec; } - if (clean_callback) { - if (is_instrumentation_set(wraprec->special_instrumentation_clean, - clean_callback)) { - nrl_verbosedebug(NRL_INSTRUMENT, - "%s: attempting to set special_instrumentation_clean " - "for %.*s, but " - "it is already set", - __func__, NRSAFELEN(namelen), NRBLANKSTR(name)); - } else { - wraprec->special_instrumentation_clean = clean_callback; - } + if (is_instrumentation_set_and_not_equal(wraprec->special_instrumentation_clean, + clean_callback)) { + nrl_verbosedebug(NRL_INSTRUMENT, + "%s: attempting to set special_instrumentation_clean " + "for %.*s, but " + "it is already set", + __func__, NRSAFELEN(namelen), NRBLANKSTR(name)); + return wraprec; } + wraprec->special_instrumentation = after_callback; + wraprec->special_instrumentation_before = before_callback; + wraprec->special_instrumentation_clean = clean_callback; + return wraprec; } #endif diff --git a/agent/php_wrapper.h b/agent/php_wrapper.h index 06e5c8e08..5e109efcd 100644 --- a/agent/php_wrapper.h +++ b/agent/php_wrapper.h @@ -327,8 +327,9 @@ extern zval** nr_php_get_return_value_ptr(TSRMLS_D); was_executed = 1; \ } -static inline bool is_instrumentation_set(nrspecialfn_t instrumentation, - nrspecialfn_t callback) { +static inline bool is_instrumentation_set_and_not_equal( + nrspecialfn_t instrumentation, + nrspecialfn_t callback) { if ((NULL != instrumentation) && (callback != instrumentation)) { return true; } From 6846ba47d98fe2d461906287f7de4dade37716a3 Mon Sep 17 00:00:00 2001 From: ZNeumann Date: Wed, 9 Aug 2023 08:03:11 -0600 Subject: [PATCH 26/56] feat(agent): instrument Drupal 9.4 hooks with oapi (#701) In addition to OAPI Drupal 9.4 support, this PR does some refactoring: 1. The hook stacks have been renamed from `module_invoke_all` to `invoke_all` to be more aligned with the name of Drupal functions that can now push to those stacks. 2. The generic wrapper function (originally added when Drupal 9.4 instrumentation was introduced) has been updated to support OAPI. 3. `nr_php_wrap_callable_before_after_clean` has been introduced as a parallel for `nr_php_wrap_user_function_before_after_clean`. To facilitate this, many of the internals have been extracted to a common function. 4. Speaking of, lots of the maintenance of the Drupal's hook stacks has been extracted to common functions to reduce code reuse --------- Co-authored-by: Michal Nowacki --- agent/fw_drupal.c | 38 ++++------ agent/fw_drupal8.c | 149 +++++++++++++++++++++++++++++---------- agent/fw_drupal_common.c | 19 +++++ agent/fw_drupal_common.h | 19 +++++ agent/php_newrelic.h | 8 +-- agent/php_rinit.c | 6 +- agent/php_rshutdown.c | 4 +- agent/php_wrapper.c | 103 ++++++++++++++++++++------- agent/php_wrapper.h | 6 ++ 9 files changed, 257 insertions(+), 95 deletions(-) diff --git a/agent/fw_drupal.c b/agent/fw_drupal.c index d599fb2d5..d2ecb61a8 100644 --- a/agent/fw_drupal.c +++ b/agent/fw_drupal.c @@ -470,7 +470,7 @@ static void nr_drupal_wrap_hook_within_module_invoke_all( #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA zval* curr_hook - = (zval*)nr_stack_get_top(&NRPRG(drupal_module_invoke_all_hooks)); + = (zval*)nr_stack_get_top(&NRPRG(drupal_invoke_all_hooks)); if (!nr_php_is_zval_non_empty_string(curr_hook)) { nrl_verbosedebug(NRL_FRAMEWORK, "%s: cannot extract hook name from global stack", @@ -480,8 +480,8 @@ static void nr_drupal_wrap_hook_within_module_invoke_all( char* hook_name = Z_STRVAL_P(curr_hook); size_t hook_len = Z_STRLEN_P(curr_hook); #else - char* hook_name = NRPRG(drupal_module_invoke_all_hook); - size_t hook_len = NRPRG(drupal_module_invoke_all_hook_len); + char* hook_name = NRPRG(drupal_invoke_all_hook); + size_t hook_len = NRPRG(drupal_invoke_all_hook_len); #endif if (NULL == hook_name) { nrl_verbosedebug(NRL_FRAMEWORK, @@ -753,25 +753,13 @@ NR_PHP_WRAPPER(nr_drupal_wrap_module_invoke_all_before) { NR_PHP_WRAPPER_REQUIRE_FRAMEWORK(NR_FW_DRUPAL); hook_copy = nr_php_arg_get(1, NR_EXECUTE_ORIG_ARGS); - if (nr_php_is_zval_non_empty_string(hook_copy)) { - nr_stack_push(&NRPRG(drupal_module_invoke_all_hooks), hook_copy); - nr_stack_push(&NRPRG(drupal_module_invoke_all_states), (void*)!NULL); - } else { - nr_stack_push(&NRPRG(drupal_module_invoke_all_states), NULL); - } + nr_drupal_invoke_all_hook_stacks_push(hook_copy); } NR_PHP_WRAPPER_END -static void module_invoke_all_clean_stacks() { - if ((bool)nr_stack_pop(&NRPRG(drupal_module_invoke_all_states))) { - zval* hook_copy = nr_stack_pop(&NRPRG(drupal_module_invoke_all_hooks)); - nr_php_arg_release(&hook_copy); - } -} - NR_PHP_WRAPPER(nr_drupal_wrap_module_invoke_all_after) { (void)wraprec; - module_invoke_all_clean_stacks(); + nr_drupal_invoke_all_hook_stacks_pop(); } NR_PHP_WRAPPER_END @@ -779,7 +767,7 @@ NR_PHP_WRAPPER(nr_drupal_wrap_module_invoke_all_clean) { NR_UNUSED_SPECIALFN; NR_UNUSED_FUNC_RETURN_VALUE; (void)wraprec; - module_invoke_all_clean_stacks(); + nr_drupal_invoke_all_hook_stacks_pop(); } NR_PHP_WRAPPER_END @@ -799,17 +787,17 @@ NR_PHP_WRAPPER(nr_drupal_wrap_module_invoke_all) { goto leave; } - prev_hook = NRPRG(drupal_module_invoke_all_hook); - prev_hook_len = NRPRG(drupal_module_invoke_all_hook_len); - NRPRG(drupal_module_invoke_all_hook) + prev_hook = NRPRG(drupal_invoke_all_hook); + prev_hook_len = NRPRG(drupal_invoke_all_hook_len); + NRPRG(drupal_invoke_all_hook) = nr_strndup(Z_STRVAL_P(hook), Z_STRLEN_P(hook)); - NRPRG(drupal_module_invoke_all_hook_len) = Z_STRLEN_P(hook); + NRPRG(drupal_invoke_all_hook_len) = Z_STRLEN_P(hook); NR_PHP_WRAPPER_CALL; - nr_free(NRPRG(drupal_module_invoke_all_hook)); - NRPRG(drupal_module_invoke_all_hook) = prev_hook; - NRPRG(drupal_module_invoke_all_hook_len) = prev_hook_len; + nr_free(NRPRG(drupal_invoke_all_hook)); + NRPRG(drupal_invoke_all_hook) = prev_hook; + NRPRG(drupal_invoke_all_hook_len) = prev_hook_len; leave: nr_php_arg_release(&hook); diff --git a/agent/fw_drupal8.c b/agent/fw_drupal8.c index 044fc8ed5..2041d14d9 100644 --- a/agent/fw_drupal8.c +++ b/agent/fw_drupal8.c @@ -71,6 +71,48 @@ static void nr_drupal8_add_method_callback(const zend_class_entry* ce, } } +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA +static void nr_drupal8_add_method_callback_before_after_clean( + const zend_class_entry* ce, + const char* method, + size_t method_len, + nrspecialfn_t before_callback, + nrspecialfn_t after_callback, + nrspecialfn_t clean_callback) { + zend_function* function = NULL; + + if (NULL == ce) { + nrl_verbosedebug(NRL_FRAMEWORK, "Drupal 8: got NULL class entry in %s", + __func__); + return; + } + + function = nr_php_find_class_method(ce, method); + if (NULL == function) { + nrl_verbosedebug(NRL_FRAMEWORK, + "Drupal 8+: cannot get zend_function entry for %.*s::%.*s", + NRSAFELEN(nr_php_class_entry_name_length(ce)), + nr_php_class_entry_name(ce), NRSAFELEN(method_len), + method); + return; + } + + if (NULL == nr_php_get_wraprec(function)) { + char* class_method = nr_formatf( + "%.*s::%.*s", NRSAFELEN(nr_php_class_entry_name_length(ce)), + nr_php_class_entry_name(ce), NRSAFELEN(method_len), method); + + nr_php_wrap_user_function_before_after_clean_with_transience( + class_method, nr_strlen(class_method), + before_callback, after_callback, clean_callback, + NR_WRAPREC_NOT_TRANSIENT); + + nr_free(class_method); + } +} +#endif // OAPI + /* * Purpose : Check if the given function or method is in the current call * stack. @@ -383,18 +425,10 @@ NR_PHP_WRAPPER(nr_drupal8_post_implements_hook) { } NR_PHP_WRAPPER_END -/* - * Temporary exclusion via #if defined OVERWRITE_ZEND_EXECUTE_DATA - * This needs to be excluded for now because the hooks handling was changed - * for oapi so the hook vars this is referencing do not exist. - * - */ -#if defined OVERWRITE_ZEND_EXECUTE_DATA /* * Purpose : Handles ModuleHandlerInterface::invokeAllWith()'s callback * and ensure that the relevant module_hook function is instrumented. */ - NR_PHP_WRAPPER(nr_drupal94_invoke_all_with_callback) { zval* module = NULL; @@ -406,9 +440,24 @@ NR_PHP_WRAPPER(nr_drupal94_invoke_all_with_callback) { goto leave; } +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + zval* curr_hook + = (zval*)nr_stack_get_top(&NRPRG(drupal_invoke_all_hooks)); + if (UNEXPECTED(!nr_php_is_zval_non_empty_string(curr_hook))) { + nrl_verbosedebug(NRL_FRAMEWORK, + "%s: cannot extract hook name from global stack", + __func__); + goto leave; + } nr_drupal_hook_instrument(Z_STRVAL_P(module), Z_STRLEN_P(module), - NRPRG(drupal_module_invoke_all_hook), - NRPRG(drupal_module_invoke_all_hook_len) TSRMLS_CC); + Z_STRVAL_P(curr_hook), Z_STRLEN_P(curr_hook)); +#else + nr_drupal_hook_instrument(Z_STRVAL_P(module), Z_STRLEN_P(module), + NRPRG(drupal_invoke_all_hook), + NRPRG(drupal_invoke_all_hook_len) TSRMLS_CC); +#endif // OAPI + leave: NR_PHP_WRAPPER_CALL; nr_php_arg_release(&module); @@ -424,8 +473,11 @@ NR_PHP_WRAPPER_END NR_PHP_WRAPPER(nr_drupal94_invoke_all_with) { zval* callback = NULL; zval* hook = NULL; +#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ + || defined OVERWRITE_ZEND_EXECUTE_DATA char* prev_hook = NULL; int prev_hook_len; +#endif // not OAPI (void)wraprec; @@ -433,43 +485,65 @@ NR_PHP_WRAPPER(nr_drupal94_invoke_all_with) { hook = nr_php_arg_get(1, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); if (!nr_php_is_zval_non_empty_string(hook)) { +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + nr_php_arg_release(&hook); +#endif // OAPI goto leave; } - prev_hook = NRPRG(drupal_module_invoke_all_hook); - prev_hook_len = NRPRG(drupal_module_invoke_all_hook_len); - NRPRG(drupal_module_invoke_all_hook) +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + nr_drupal_invoke_all_hook_stacks_push(hook); +#else + prev_hook = NRPRG(drupal_invoke_all_hook); + prev_hook_len = NRPRG(drupal_invoke_all_hook_len); + NRPRG(drupal_invoke_all_hook) = nr_strndup(Z_STRVAL_P(hook), Z_STRLEN_P(hook)); - NRPRG(drupal_module_invoke_all_hook_len) = Z_STRLEN_P(hook); - + NRPRG(drupal_invoke_all_hook_len) = Z_STRLEN_P(hook); +#endif // OAPI callback = nr_php_arg_get(2, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + /* This instrumentation will fail if callback has already been wrapped * with a special instrumentation callback in a different context. * In this scenario, we will be unable to instrument hooks and modules for * this particular call */ - - /* - * Temporary exclusion via #if defined OVERWRITE_ZEND_EXECUTE_DATA - * This needs to be excluded for now because the hooks handling was changed - * for oapi so the hook vars this is referencing do not exist. - * - */ -#if defined OVERWRITE_ZEND_EXECUTE_DATA nr_php_wrap_generic_callable(callback, nr_drupal94_invoke_all_with_callback TSRMLS_CC); -#endif NR_PHP_WRAPPER_CALL; nr_php_arg_release(&callback); - nr_free(NRPRG(drupal_module_invoke_all_hook)); - NRPRG(drupal_module_invoke_all_hook) = prev_hook; - NRPRG(drupal_module_invoke_all_hook_len) = prev_hook_len; - -leave: +#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ + || defined OVERWRITE_ZEND_EXECUTE_DATA + nr_free(NRPRG(drupal_invoke_all_hook)); + NRPRG(drupal_invoke_all_hook) = prev_hook; + NRPRG(drupal_invoke_all_hook_len) = prev_hook_len; +#endif // not OAPI + +leave: ; +#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ + || defined OVERWRITE_ZEND_EXECUTE_DATA + /* for OAPI, the _after callback handles this free */ nr_php_arg_release(&hook); +#endif // not OAPI +} +NR_PHP_WRAPPER_END + +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA +NR_PHP_WRAPPER(nr_drupal94_invoke_all_with_after) { + (void)wraprec; + nr_drupal_invoke_all_hook_stacks_pop(); +} +NR_PHP_WRAPPER_END + +NR_PHP_WRAPPER(nr_drupal94_invoke_all_with_clean) { + (void)wraprec; + nr_drupal_invoke_all_hook_stacks_pop(); } NR_PHP_WRAPPER_END -#endif /* temporary: defined OVERWRITE_ZEND_EXECUTE_DATA */ +#endif // OAPI + /* * Purpose : Wrap the invoke() method of the module handler instance in use. */ @@ -502,13 +576,14 @@ NR_PHP_WRAPPER(nr_drupal8_module_handler) { nr_drupal8_add_method_callback(ce, NR_PSTR("implementshook"), nr_drupal8_post_implements_hook TSRMLS_CC); /* Drupal 9.4 introduced a replacement method for getImplentations */ - /* - * Temporary exclusion via #if defined OVERWRITE_ZEND_EXECUTE_DATA - * This needs to be excluded for now because the hooks handling was changed - * for oapi so the hook vars this is referencing do not exist. - * - */ -#if defined OVERWRITE_ZEND_EXECUTE_DATA +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + nr_drupal8_add_method_callback_before_after_clean( + ce, NR_PSTR("invokeallwith"), + nr_drupal94_invoke_all_with, + nr_drupal94_invoke_all_with_after, + nr_drupal94_invoke_all_with_clean); +#else nr_drupal8_add_method_callback(ce, NR_PSTR("invokeallwith"), nr_drupal94_invoke_all_with TSRMLS_CC); #endif diff --git a/agent/fw_drupal_common.c b/agent/fw_drupal_common.c index 27d55ea42..9759bd41a 100644 --- a/agent/fw_drupal_common.c +++ b/agent/fw_drupal_common.c @@ -294,3 +294,22 @@ void nr_drupal_headers_add(zval* arg, bool is_drupal_7 TSRMLS_DC) { nr_php_zval_free(&newrelic_headers); } + +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA +void nr_drupal_invoke_all_hook_stacks_push(zval* hook_copy) { + if (nr_php_is_zval_non_empty_string(hook_copy)) { + nr_stack_push(&NRPRG(drupal_invoke_all_hooks), hook_copy); + nr_stack_push(&NRPRG(drupal_invoke_all_states), (void*)!NULL); + } else { + nr_stack_push(&NRPRG(drupal_invoke_all_states), NULL); + } +} + +void nr_drupal_invoke_all_hook_stacks_pop() { + if ((bool)nr_stack_pop(&NRPRG(drupal_invoke_all_states))) { + zval* hook_copy = nr_stack_pop(&NRPRG(drupal_invoke_all_hooks)); + nr_php_arg_release(&hook_copy); + } +} +#endif diff --git a/agent/fw_drupal_common.h b/agent/fw_drupal_common.h index 3481d0a39..d94097a07 100644 --- a/agent/fw_drupal_common.h +++ b/agent/fw_drupal_common.h @@ -147,4 +147,23 @@ nr_status_t module_invoke_all_parse_module_and_hook_from_strings( */ void nr_drupal_headers_add(zval* arg, bool is_drupal_7 TSRMLS_DC); + +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA +/* + * Purpose: Before an invoke_all style call, adds the hook to that hook states stacks + * + * Params : 1. A zval holding a copy of the hook invoked, to be managed by the hook + * states stacks and freed by nr_drupal_invoke_all_hook_stacks_pop() after the + * invoke_all call completes + */ +void nr_drupal_invoke_all_hook_stacks_push(zval* hook_copy); + +/* + * Purpose: After an invoke_all style call, pops that states stack and conditionally + * pops the hook stack based on the previously popped state + */ +void nr_drupal_invoke_all_hook_stacks_pop(); +#endif // OAPI + #endif /* FW_DRUPAL_COMMON_HDR */ diff --git a/agent/php_newrelic.h b/agent/php_newrelic.h index 49584bab1..d74505663 100644 --- a/agent/php_newrelic.h +++ b/agent/php_newrelic.h @@ -410,13 +410,13 @@ int framework_version; /* Current framework version */ /* Without OAPI, we are able to utilize the call stack to keep track * of the previous hooks. With OAPI, we can no longer do this so * we track the stack manually */ -nr_stack_t drupal_module_invoke_all_hooks; /* stack of Drupal hooks */ -nr_stack_t drupal_module_invoke_all_states; /* stack of bools indicating +nr_stack_t drupal_invoke_all_hooks; /* stack of Drupal hooks */ +nr_stack_t drupal_invoke_all_states; /* stack of bools indicating whether the current hook needs to be released */ #else -char* drupal_module_invoke_all_hook; /* The current Drupal hook */ -size_t drupal_module_invoke_all_hook_len; /* The length of the current Drupal +char* drupal_invoke_all_hook; /* The current Drupal hook */ +size_t drupal_invoke_all_hook_len; /* The length of the current Drupal hook */ #endif //OAPI size_t drupal_http_request_depth; /* The current depth of drupal_http_request() diff --git a/agent/php_rinit.c b/agent/php_rinit.c index 376cf1d19..bcd45bf96 100644 --- a/agent/php_rinit.c +++ b/agent/php_rinit.c @@ -125,11 +125,11 @@ PHP_RINIT_FUNCTION(newrelic) { nr_stack_init(&NRPRG(predis_ctxs), NR_STACK_DEFAULT_CAPACITY); nr_stack_init(&NRPRG(wordpress_tags), NR_STACK_DEFAULT_CAPACITY); nr_stack_init(&NRPRG(wordpress_tag_states), NR_STACK_DEFAULT_CAPACITY); - nr_stack_init(&NRPRG(drupal_module_invoke_all_hooks), NR_STACK_DEFAULT_CAPACITY); - nr_stack_init(&NRPRG(drupal_module_invoke_all_states), NR_STACK_DEFAULT_CAPACITY); + nr_stack_init(&NRPRG(drupal_invoke_all_hooks), NR_STACK_DEFAULT_CAPACITY); + nr_stack_init(&NRPRG(drupal_invoke_all_states), NR_STACK_DEFAULT_CAPACITY); NRPRG(predis_ctxs).dtor = str_stack_dtor; NRPRG(wordpress_tags).dtor = str_stack_dtor; - NRPRG(drupal_module_invoke_all_hooks).dtor = zval_stack_dtor; + NRPRG(drupal_invoke_all_hooks).dtor = zval_stack_dtor; #endif NRPRG(mysql_last_conn) = NULL; diff --git a/agent/php_rshutdown.c b/agent/php_rshutdown.c index 8bfc739dd..b1de6962c 100644 --- a/agent/php_rshutdown.c +++ b/agent/php_rshutdown.c @@ -120,8 +120,8 @@ int nr_php_post_deactivate(void) { */ nr_stack_destroy_fields(&NRPRG(wordpress_tags)); nr_stack_destroy_fields(&NRPRG(wordpress_tag_states)); - nr_stack_destroy_fields(&NRPRG(drupal_module_invoke_all_hooks)); - nr_stack_destroy_fields(&NRPRG(drupal_module_invoke_all_states)); + nr_stack_destroy_fields(&NRPRG(drupal_invoke_all_hooks)); + nr_stack_destroy_fields(&NRPRG(drupal_invoke_all_states)); #endif #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ diff --git a/agent/php_wrapper.c b/agent/php_wrapper.c index 6a3ee536d..8d5dcc12c 100644 --- a/agent/php_wrapper.c +++ b/agent/php_wrapper.c @@ -9,18 +9,14 @@ #include "util_logging.h" #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO -nruserfn_t* nr_php_wrap_user_function_before_after_clean_with_transience( - const char* name, - size_t namelen, +static void nr_php_wraprec_add_before_after_clean_callbacks( + const char* name, size_t namelen, + nruserfn_t* wraprec, nrspecialfn_t before_callback, nrspecialfn_t after_callback, - nrspecialfn_t clean_callback, - nr_transience_t transience) { - nruserfn_t* wraprec - = nr_php_add_custom_tracer_named(name, namelen, transience); - + nrspecialfn_t clean_callback) { if (NULL == wraprec) { - return wraprec; + return; } /* If any of the callbacks we are attempting to set are already set to @@ -32,7 +28,7 @@ nruserfn_t* nr_php_wrap_user_function_before_after_clean_with_transience( "%s: attempting to set special_instrumentation for %.*s, but " "it is already set", __func__, NRSAFELEN(namelen), NRBLANKSTR(name)); - return wraprec; + return; } if (is_instrumentation_set_and_not_equal(wraprec->special_instrumentation_before, @@ -42,7 +38,7 @@ nruserfn_t* nr_php_wrap_user_function_before_after_clean_with_transience( "for %.*s, but " "it is already set", __func__, NRSAFELEN(namelen), NRBLANKSTR(name)); - return wraprec; + return; } if (is_instrumentation_set_and_not_equal(wraprec->special_instrumentation_clean, @@ -52,12 +48,56 @@ nruserfn_t* nr_php_wrap_user_function_before_after_clean_with_transience( "for %.*s, but " "it is already set", __func__, NRSAFELEN(namelen), NRBLANKSTR(name)); - return wraprec; + return; } wraprec->special_instrumentation = after_callback; wraprec->special_instrumentation_before = before_callback; wraprec->special_instrumentation_clean = clean_callback; +} + +nruserfn_t* nr_php_wrap_user_function_before_after_clean_with_transience( + const char* name, + size_t namelen, + nrspecialfn_t before_callback, + nrspecialfn_t after_callback, + nrspecialfn_t clean_callback, + nr_transience_t transience) { + + nruserfn_t* wraprec + = nr_php_add_custom_tracer_named(name, namelen, transience); + + nr_php_wraprec_add_before_after_clean_callbacks(name, namelen, wraprec, + before_callback, + after_callback, + clean_callback); + + return wraprec; +} + +nruserfn_t* nr_php_wrap_callable_before_after_clean( + zend_function* callable, + nrspecialfn_t before_callback, + nrspecialfn_t after_callback, + nrspecialfn_t clean_callback) { + char* name = NULL; + + /* creates a transient wraprec */ + nruserfn_t* wraprec = nr_php_add_custom_tracer_callable(callable TSRMLS_CC); + + /* + * For logging purposes, let's create a name if we're logging at verbosedebug. + */ + if (nrl_should_print(NRL_VERBOSEDEBUG, NRL_INSTRUMENT)) { + name = nr_php_function_debug_name(callable); + } + nr_php_wraprec_add_before_after_clean_callbacks(name, nr_strlen(name), wraprec, + before_callback, + after_callback, + clean_callback); + if (nrl_should_print(NRL_VERBOSEDEBUG, NRL_INSTRUMENT) && NULL != name) { + nr_free(name); + } return wraprec; } @@ -106,6 +146,7 @@ nruserfn_t* nr_php_wrap_user_function_extra(const char* name, nruserfn_t* nr_php_wrap_callable(zend_function* callable, nrspecialfn_t callback TSRMLS_DC) { + /* creates a transient wraprec */ nruserfn_t* wraprec = nr_php_add_custom_tracer_callable(callable TSRMLS_CC); if (wraprec && callback) { @@ -123,6 +164,14 @@ nruserfn_t* nr_php_wrap_callable(zend_function* callable, return wraprec; } +/* + * When wrapping a generic callable, it is currently only desired that a + * wraprec's internals be evaluated BEFORE for the callable's. As such, + * for OAPI, this creates "before" wrappers, where normally the default + * is to create "after" wrappers (see nr_php_wrap_user_function). Should + * "after"/"clean" wrappers ever be desired, it is suggested to create a + * separate nr_php_wrap_generic_callable_before_after_clean() function. + */ nruserfn_t* nr_php_wrap_generic_callable(zval* callable, nrspecialfn_t callback TSRMLS_DC) { #if ZEND_MODULE_API_NO < ZEND_7_0_X_API_NO @@ -136,17 +185,19 @@ nruserfn_t* nr_php_wrap_generic_callable(zval* callable, /* not calling nr_zend_is_callable because we want to additionally populate * name */ if (zend_is_callable(callable, 0, &name TSRMLS_CC)) { - /* see php source code's zend_is_callable_at_frame function to see from - * where these switch cases are derived */ #if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO again: #endif + /* see php source code's zend_is_callable_at_frame function to see from + * where these switch cases are derived */ switch (Z_TYPE_P(callable)) { /* wrapping a string name of a callable */ case IS_STRING: -#if ZEND_MODULE_API_NO < ZEND_7_0_X_API_NO - return nr_php_wrap_user_function_with_transience( - name, nr_strlen(name), callback, NR_WRAPREC_IS_TRANSIENT TSRMLS_CC); +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + return nr_php_wrap_user_function_before_after_clean_with_transience( + ZEND_STRING_VALUE(name), ZEND_STRING_LEN(name), callback, + NULL, NULL, NR_WRAPREC_IS_TRANSIENT); #else return nr_php_wrap_user_function_with_transience( ZEND_STRING_VALUE(name), ZEND_STRING_LEN(name), callback, @@ -157,9 +208,11 @@ nruserfn_t* nr_php_wrap_generic_callable(zval* callable, * invoke the previous zend_is_callable has created the commbined * object::method name for us to wrap */ case IS_ARRAY: -#if ZEND_MODULE_API_NO < ZEND_7_0_X_API_NO - return nr_php_wrap_user_function_with_transience( - name, nr_strlen(name), callback, NR_WRAPREC_IS_TRANSIENT TSRMLS_CC); +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + return nr_php_wrap_user_function_before_after_clean_with_transience( + ZEND_STRING_VALUE(name), ZEND_STRING_LEN(name), callback, + NULL, NULL, NR_WRAPREC_IS_TRANSIENT); #else return nr_php_wrap_user_function_with_transience( ZEND_STRING_VALUE(name), ZEND_STRING_LEN(name), callback, @@ -173,7 +226,13 @@ nruserfn_t* nr_php_wrap_generic_callable(zval* callable, == zend_fcall_info_init(callable, 0, &fci, &fcc, NULL, NULL TSRMLS_CC)) { /* nr_php_wrap_callable already sets the transience field for us */ +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + return nr_php_wrap_callable_before_after_clean( + fcc.function_handler, callback, NULL, NULL); +#else return nr_php_wrap_callable(fcc.function_handler, callback TSRMLS_CC); +#endif } nrl_verbosedebug(NRL_INSTRUMENT, "Failed to initialize fcall info when wrapping"); @@ -189,9 +248,6 @@ nruserfn_t* nr_php_wrap_generic_callable(zval* callable, #endif } } -#if ZEND_MODULE_API_NO < ZEND_7_0_X_API_NO - nrl_verbosedebug(NRL_INSTRUMENT, "Failed to wrap callable: %s", name); -#else if (NULL != name) { nrl_verbosedebug(NRL_INSTRUMENT, "Failed to wrap callable: %s", ZEND_STRING_VALUE(name)); @@ -199,7 +255,6 @@ nruserfn_t* nr_php_wrap_generic_callable(zval* callable, nrl_verbosedebug(NRL_INSTRUMENT, "Failed to wrap callable with unknown name"); } -#endif return NULL; } diff --git a/agent/php_wrapper.h b/agent/php_wrapper.h index 5e109efcd..3e7ccd694 100644 --- a/agent/php_wrapper.h +++ b/agent/php_wrapper.h @@ -143,6 +143,12 @@ extern nruserfn_t* nr_php_wrap_user_function_before_after_clean_with_transience( nrspecialfn_t after_callback, nrspecialfn_t clean_callback, nr_transience_t transience); + +extern nruserfn_t* nr_php_wrap_callable_before_after_clean( + zend_function* callable, + nrspecialfn_t before_callback, + nrspecialfn_t after_callback, + nrspecialfn_t clean_callback); #endif extern nruserfn_t* nr_php_wrap_user_function(const char* name, size_t namelen, From dca53bd512bab306e81910b64c332a0d09e4b49e Mon Sep 17 00:00:00 2001 From: ZNeumann Date: Wed, 9 Aug 2023 08:57:11 -0600 Subject: [PATCH 27/56] fix: add guards to oapi cufa opcode check (#708) When instrumenting wordpress/drupal hooks, we want to instrument the hook function calls, which are dispatched by php's call_user_function_array. This php function (cufa for short) is flattened/inlined, so to determine if we are in a function called by a cufa call, we need to check the opcodes of the caller to the current function. If the caller is a php internal function, checking these opcodes was not safe and lead to invalid reads. As such, before reading the opcodes, we first check if the caller is a user function. We only want to instrument cufa calls from user functions anyway. --------- Co-authored-by: Michal Nowacki --- agent/php_execute.c | 38 +++++++++++++++---- .../test_wordpress_do_action.php8.php | 10 +++++ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/agent/php_execute.c b/agent/php_execute.c index 18465ceb0..a0ee83ed2 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -1934,6 +1934,7 @@ static void nr_php_observer_attempt_call_cufa_handler(NR_EXECUTE_PROTO) { nrl_verbosedebug(NRL_AGENT, "%s: cannot get previous execute data", __func__); return; } + if (NULL == execute_data->prev_execute_data->opline) { nrl_verbosedebug(NRL_AGENT, "%s: cannot get previous opline", __func__); return; @@ -1966,20 +1967,43 @@ static void nr_php_observer_attempt_call_cufa_handler(NR_EXECUTE_PROTO) { * cause additional performance overhead, this should be considered a last * resort. */ - const zend_op* prev_opline = execute_data->prev_execute_data->opline - 1; + + /* + * When Observer API is used, this code executes in the context of + * zend_execute and not in the context of VM (as was the case pre-OAPI), + * therefore we need to ensure we're dealing with a user function. We cannot + * safely access the opline of internal functions, and we only want to + * instrument cufa calls from user functions anyway. + */ + if (UNEXPECTED(NULL == execute_data->prev_execute_data->func)) { + nrl_verbosedebug(NRL_AGENT, "%s: cannot get previous function", __func__); + return; + } + if (!ZEND_USER_CODE(execute_data->prev_execute_data->func->type)) { + nrl_verbosedebug(NRL_AGENT, "%s: caller is php internal function", __func__); + return; + } + + const zend_op* prev_opline = execute_data->prev_execute_data->opline; + + /* + * Extra safety check. Previously, we instrumented by overwritting ZEND_DO_FCALL. + * Within OAPI, for consistency's sake, we will ensure the same + */ + if (ZEND_DO_FCALL != prev_opline->opcode) { + nrl_verbosedebug(NRL_AGENT, "%s: cannot get previous function name", __func__); + return; + } + prev_opline -= 1; // Checks previous opcode if (ZEND_CHECK_UNDEF_ARGS == prev_opline->opcode) { - prev_opline = execute_data->prev_execute_data->opline - 2; + prev_opline -= 1; // Checks previous opcode } if (ZEND_SEND_ARRAY == prev_opline->opcode) { + if (UNEXPECTED((NULL == execute_data->func))) { nrl_verbosedebug(NRL_AGENT, "%s: cannot get current function", __func__); return; } - if (UNEXPECTED(NULL == execute_data->prev_execute_data->func)) { - nrl_verbosedebug(NRL_AGENT, "%s: cannot get previous function", __func__); - return; - } - if (UNEXPECTED(NULL == execute_data->prev_execute_data->func->common.function_name)) { nrl_verbosedebug(NRL_AGENT, "%s: cannot get previous function name", __func__); return; diff --git a/tests/integration/frameworks/wordpress/test_wordpress_do_action.php8.php b/tests/integration/frameworks/wordpress/test_wordpress_do_action.php8.php index a24492dff..56280eaad 100644 --- a/tests/integration/frameworks/wordpress/test_wordpress_do_action.php8.php +++ b/tests/integration/frameworks/wordpress/test_wordpress_do_action.php8.php @@ -21,6 +21,7 @@ */ /*EXPECT +g f h g @@ -70,4 +71,13 @@ function f() { } } +/* + * Initiates a non-flattened call stack of internal->user_code + * to ensure that cufa instrumentation properly handles skipping + * opline lookups of internal functions + */ +$function = new ReflectionFunction('g'); +$function->invoke(); + + do_action("f"); From 1e7c861b791ab0fe212723965eaee062a57e9b5d Mon Sep 17 00:00:00 2001 From: ZNeumann Date: Wed, 9 Aug 2023 08:58:09 -0600 Subject: [PATCH 28/56] fix(agent): wrap zend_try in a func to avoid clobbering (#703) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Calls to `call_user_function` (in PHP 8.0 and 8.1) and `zend_call_method_if_exists` (in PHP 8.2+) need to be wrapped by the `zend_try`/`zend_catch`/`zend_end_try` block, which use [`setjmp`](https://linux.die.net/man/3/setjmp) and [`longjmp`](https://linux.die.net/man/3/longjmp), because according to [`call_user_func()`](https://www.php.net/manual/en/function.call-user-func.php): > Callbacks registered with functions such as `call_user_func()` and [`call_user_func_array()`](https://www.php.net/manual/en/function.call-user-func-array.php) will not be called if there is an uncaught exception thrown in a previous callback. So if we call something that causes an exception, it will block us from future calls that use `call_user_func` or `call_user_func_array`. Valgrind showed the agent and/or the Zend engine wasn’t properly cleaning up after such cases and newer compilers had issues with this when compiling with any optimization and generated the following error: ``` error: variable ‘retval’ might be clobbered by ‘longjmp’ or ‘vfork’) [-Werror=clobbered] ``` PHP developers solve this problem by using an automatic variable to store user function call result - see [here](https://github.com/php/php-src/blob/master/main/streams/userspace.c#L335-L340) for an example in PHP source code how zend_call_method_if_exists is called. This solution wraps `zend_try`/`zend_catch`/`zend_end_try` constructs in a function to localize the `retval` and avoid variable clobbering. --- agent/php_call.c | 85 ++++++++++++++----- .../test_wordpress_apply_filters.php8.php | 4 + .../test_wordpress_do_action.php8.php | 4 + 3 files changed, 70 insertions(+), 23 deletions(-) diff --git a/agent/php_call.c b/agent/php_call.c index 38bd792f4..c8e0c874d 100644 --- a/agent/php_call.c +++ b/agent/php_call.c @@ -10,6 +10,56 @@ #include "Zend/zend_exceptions.h" +/* + * zend_try family of macros entail the use of setjmp and longjmp, which can cause clobbering issues with + * non-primitive local variables. Abstracting these constructs into separate functions protects from this. + */ +#if ZEND_MODULE_API_NO >= ZEND_8_2_X_API_NO +static int nr_php_call_try_catch(zend_object* object, + zend_string* method_name, + zval* retval, + zend_uint param_count, + zval* param_values) { + /* + * With PHP 8.2, functions that do not exist will cause a fatal error to + * be thrown. `zend_call_method_if_exists` will attempt to call a function and + * silently fail if it does not exist + */ + int zend_result = FAILURE; + zend_try { + zend_result = zend_call_method_if_exists(object, method_name, retval, + param_count, param_values); + } + zend_catch { zend_result = FAILURE; } + zend_end_try(); + return zend_result; +} +#elif ZEND_MODULE_API_NO < ZEND_8_2_X_API_NO \ + && ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO +static int nr_php_call_try_catch(zval* object_ptr, + zval* fname, + zval* retval, + zend_uint param_count, + zval* param_values) { + /* + * With PHP8, `call_user_function_ex` was removed and `call_user_function` + * became the recommended function. + * According to zend internals documentation: + * As of PHP 7.1.0, the function_table argument is not used and should + * always be NULL. See for more details: + * https://www.phpinternalsbook.com/php7/internal_types/functions/callables.html + */ + int zend_result = FAILURE; + zend_try { + zend_result = call_user_function(EG(function_table), object_ptr, fname, + retval, param_count, param_values); + } + zend_catch { zend_result = FAILURE; } + zend_end_try(); + return zend_result; +} +#endif + zval* nr_php_call_user_func(zval* object_ptr, const char* function_name, zend_uint param_count, @@ -53,11 +103,6 @@ zval* nr_php_call_user_func(zval* object_ptr, fname = nr_php_zval_alloc(); nr_php_zval_str(fname, function_name); #if ZEND_MODULE_API_NO >= ZEND_8_2_X_API_NO /* PHP 8.2+ */ - /* - * With PHP 8.2, functions that do not exist will cause a fatal error to - * be thrown. `zend_call_method_if_exists` will attempt to call a function and - * silently fail if it does not exist - */ if (NULL != object_ptr) { object = Z_OBJ_P(object_ptr); } else { @@ -69,26 +114,20 @@ zval* nr_php_call_user_func(zval* object_ptr, } else { return NULL; } - zend_try { - zend_result = zend_call_method_if_exists(object, method_name, retval, - param_count, param_values); - } - zend_catch { zend_result = FAILURE; } - zend_end_try(); -#elif ZEND_MODULE_API_NO < ZEND_8_2_X_API_NO \ - && ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO + /* - * With PHP8, `call_user_function_ex` was removed and `call_user_function` - * became the recommended function. This does't return a FAILURE for - * exceptions and needs to be in a try/catch block in order to clean up - * properly. + * For PHP 8+, in the case of exceptions according to: + * https://www.php.net/manual/en/function.call-user-func.php + * Callbacks registered with functions such as call_user_func() and + * call_user_func_array() will not be called if there is an uncaught exception + * thrown in a previous callback. So if we call something that causes an + * exception, it will block us from future calls that use call_user_func or + * call_user_func_array and hence the need for a try/catch block. */ - zend_try { - zend_result = call_user_function(EG(function_table), object_ptr, fname, - retval, param_count, param_values); - } - zend_catch { zend_result = FAILURE; } - zend_end_try(); + zend_result = nr_php_call_try_catch(object, method_name, retval, param_count, param_values); +#elif ZEND_MODULE_API_NO < ZEND_8_2_X_API_NO \ + && ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO + zend_result = nr_php_call_try_catch(object_ptr, fname, retval, param_count, param_values); #else zend_result = call_user_function_ex(EG(function_table), object_ptr, fname, diff --git a/tests/integration/frameworks/wordpress/test_wordpress_apply_filters.php8.php b/tests/integration/frameworks/wordpress/test_wordpress_apply_filters.php8.php index 07266ffa7..299616114 100644 --- a/tests/integration/frameworks/wordpress/test_wordpress_apply_filters.php8.php +++ b/tests/integration/frameworks/wordpress/test_wordpress_apply_filters.php8.php @@ -52,6 +52,10 @@ function apply_filters($tag, ...$args) { call_user_func_array($tag, $args); } +//Simple mock of wordpress's get_theme_roots +function get_theme_roots() { +} + function h($str) { echo "h: "; echo $str; diff --git a/tests/integration/frameworks/wordpress/test_wordpress_do_action.php8.php b/tests/integration/frameworks/wordpress/test_wordpress_do_action.php8.php index 56280eaad..0483b2511 100644 --- a/tests/integration/frameworks/wordpress/test_wordpress_do_action.php8.php +++ b/tests/integration/frameworks/wordpress/test_wordpress_do_action.php8.php @@ -53,6 +53,10 @@ function do_action($tag, ...$args) { call_user_func_array($tag, $args); } +//Simple mock of wordpress's get_theme_roots +function get_theme_roots() { +} + function h() { echo "h\n"; throw new Exception("Test Exception"); From 7d0a7685ee54839dea4a37e2849277fac30874a5 Mon Sep 17 00:00:00 2001 From: Michal Nowacki Date: Mon, 21 Aug 2023 15:40:10 -0400 Subject: [PATCH 29/56] tests: 'enhance' laravel framework mock (#716) Bypass PHP 8.2 optimization of not executing files without executable statements by forcing the execution of the file with mock of Laravel used by the test, so when wrapper for `Illuminate\Routing\RouteCollection::getRouteForMethods` is installed, the `Illuminate\Routing\RouteCollection` class and `getRouteForMethods` can be found. --- tests/integration/frameworks/laravel/mock_http_options.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/integration/frameworks/laravel/mock_http_options.php b/tests/integration/frameworks/laravel/mock_http_options.php index 98777344b..85df12f2f 100644 --- a/tests/integration/frameworks/laravel/mock_http_options.php +++ b/tests/integration/frameworks/laravel/mock_http_options.php @@ -57,3 +57,7 @@ public function setMockedRoute($route) { } } } + +namespace Illuminate\Routing { + echo ""; +} From f582376d090c110bf43678d8efed19bbee5e7cc8 Mon Sep 17 00:00:00 2001 From: Michal Nowacki Date: Wed, 6 Sep 2023 13:07:57 -0400 Subject: [PATCH 30/56] tests: fix metrics expectation in drupal tests (#726) External metrics should not be generated when call to `drupal_http_request` fails due to uncaught error. This works correctly in PHPs 8+, when Observer API is used to hook into Zend Engine but doesn't work correctly for previous method of hooking into Zend Engine hence two flavors of tests. --- .../test_bad_params_integer_headers.php | 3 ++ .../test_bad_params_integer_headers.php8.php | 48 +++++++++++++++++++ .../drupal7/test_bad_params_null_headers.php | 3 ++ .../test_bad_params_null_headers.php8.php | 48 +++++++++++++++++++ 4 files changed, 102 insertions(+) create mode 100644 tests/integration/external/drupal7/test_bad_params_integer_headers.php8.php create mode 100644 tests/integration/external/drupal7/test_bad_params_null_headers.php8.php diff --git a/tests/integration/external/drupal7/test_bad_params_integer_headers.php b/tests/integration/external/drupal7/test_bad_params_integer_headers.php index b717584c4..c7f793ad1 100644 --- a/tests/integration/external/drupal7/test_bad_params_integer_headers.php +++ b/tests/integration/external/drupal7/test_bad_params_integer_headers.php @@ -14,6 +14,9 @@ /*SKIPIF =")) { + die("skip: PHP >= 8.0 not supported\n"); +} */ /*EXPECT_METRICS diff --git a/tests/integration/external/drupal7/test_bad_params_integer_headers.php8.php b/tests/integration/external/drupal7/test_bad_params_integer_headers.php8.php new file mode 100644 index 000000000..8781876b3 --- /dev/null +++ b/tests/integration/external/drupal7/test_bad_params_integer_headers.php8.php @@ -0,0 +1,48 @@ + 22)); diff --git a/tests/integration/external/drupal7/test_bad_params_null_headers.php b/tests/integration/external/drupal7/test_bad_params_null_headers.php index 6ace16115..86a105fdb 100644 --- a/tests/integration/external/drupal7/test_bad_params_null_headers.php +++ b/tests/integration/external/drupal7/test_bad_params_null_headers.php @@ -14,6 +14,9 @@ /*SKIPIF =")) { + die("skip: PHP >= 8.0 not supported\n"); +} */ /*EXPECT_METRICS diff --git a/tests/integration/external/drupal7/test_bad_params_null_headers.php8.php b/tests/integration/external/drupal7/test_bad_params_null_headers.php8.php new file mode 100644 index 000000000..e5e2a5558 --- /dev/null +++ b/tests/integration/external/drupal7/test_bad_params_null_headers.php8.php @@ -0,0 +1,48 @@ + NULL)); From 7b142fc34a8a8873152452710eeb6b9a95c48e7f Mon Sep 17 00:00:00 2001 From: Michal Nowacki Date: Wed, 6 Sep 2023 14:11:24 -0400 Subject: [PATCH 31/56] refactor(agent): improve phpunit instrumentation (#725) * improve phpunit detection Use file that is loaded also on PHP 8.2 with include/require optimizations that don't execute files without executable statements. * improve skipped tests handling PHPUnit passes skipped tests to `addError` as well as `endTest` regardless ofthe version. The difference is that in PHPUnit 5.x it is not possible to obtain test outcome for skipped test - `$test.getStatus()` returns 'null'. Because of that, `endTest` instrumentation bails out and does not generate test event and therefore `addError` needs to be instrumented. It is possible to obtain test outcome for skipped tests in PHPUnit 9.x. However, the `$time` for skipped test is not received in `endTest` as a valid double and therefore it needs to be 'fixed'. Moreover testcase.getStatusMessage returns 'null' for tests skipped due to dependency failure and therefore it is not possible to expect it in the test event. --- agent/lib_phpunit.c | 16 +++++++++------- agent/php_execute.c | 7 +++---- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/agent/lib_phpunit.c b/agent/lib_phpunit.c index 3495fa4fb..90bd711d5 100644 --- a/agent/lib_phpunit.c +++ b/agent/lib_phpunit.c @@ -472,8 +472,8 @@ NR_PHP_WRAPPER(nr_phpunit_instrument_testresult_endtest) { } /* - * PHPUnit 6+ started passing "tests skipped due to dependency failures" - * to the endTest method -- however, we already catch these tests in + * PHPUnit passes "tests skipped due to dependency failures" + * to the endTest method. For PHPUnit 5.x, we already catch these tests in * our nr_phpunit_instrument_testresult_adderror wrapper. This check * ensures these skipped tests aren't double counted by bailing if * a test's status isn't set. @@ -490,8 +490,13 @@ NR_PHP_WRAPPER(nr_phpunit_instrument_testresult_endtest) { duration = nr_php_arg_get(2, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); if (!nr_php_is_zval_valid_double(duration)) { nrl_verbosedebug(NRL_INSTRUMENT, "%s: invalid test duration", __func__); - NR_PHP_WRAPPER_CALL; - goto end; + /* + * When PHPUnit 6.x+ passes "tests skipped due to dependency failures" + * to the endTest method the second argument - $time - has the value of 0. + * However Zend Engine does not correctly set the type for this argument + * in this case and therefore we need to fix the type of duration here: + */ + ZVAL_DOUBLE(duration, 0.0); } NR_PHP_WRAPPER_CALL; @@ -689,7 +694,4 @@ void nr_phpunit_enable(TSRMLS_D) { nr_php_wrap_user_function( NR_PSTR("PHPUnit_Framework_TestResult::addError"), nr_phpunit_instrument_testresult_adderror TSRMLS_CC); - nr_php_wrap_user_function( - NR_PSTR("PHPUnit\\Framework\\TestResult::addError"), - nr_phpunit_instrument_testresult_adderror TSRMLS_CC); } diff --git a/agent/php_execute.c b/agent/php_execute.c index 096423663..2f31f7fac 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -488,11 +488,10 @@ static nr_library_table_t libraries[] = { /* * The first path is for Composer installs, the second is for - * /usr/local/bin. While BaseTestRunner isn't the very first file to load, - * it contains the test status constants and loads before tests can run. + * /usr/local/bin. */ - {"PHPUnit", "phpunit/src/runner/basetestrunner.php", nr_phpunit_enable}, - {"PHPUnit", "phpunit/runner/basetestrunner.php", nr_phpunit_enable}, + {"PHPUnit", "phpunit/src/framework/test.php", nr_phpunit_enable}, + {"PHPUnit", "phpunit/framework/test.php", nr_phpunit_enable}, {"Predis", "predis/src/client.php", nr_predis_enable}, {"Predis", "predis/client.php", nr_predis_enable}, From f9fa14afac8333d0bcf0434bbf0ddfab136697a7 Mon Sep 17 00:00:00 2001 From: ZNeumann Date: Thu, 21 Sep 2023 13:19:02 -0600 Subject: [PATCH 32/56] feat: optimize oapi cufa detection (#729) --- agent/fw_drupal_common.c | 4 ++++ agent/fw_wordpress.c | 11 +++++++++-- agent/php_execute.c | 12 ++++++------ agent/php_newrelic.h | 1 + agent/php_rinit.c | 1 + agent/tests/test_internal_instrument.c | 8 ++++++++ 6 files changed, 29 insertions(+), 8 deletions(-) diff --git a/agent/fw_drupal_common.c b/agent/fw_drupal_common.c index 9759bd41a..894ff2270 100644 --- a/agent/fw_drupal_common.c +++ b/agent/fw_drupal_common.c @@ -301,6 +301,7 @@ void nr_drupal_invoke_all_hook_stacks_push(zval* hook_copy) { if (nr_php_is_zval_non_empty_string(hook_copy)) { nr_stack_push(&NRPRG(drupal_invoke_all_hooks), hook_copy); nr_stack_push(&NRPRG(drupal_invoke_all_states), (void*)!NULL); + NRPRG(check_cufa) = true; } else { nr_stack_push(&NRPRG(drupal_invoke_all_states), NULL); } @@ -311,5 +312,8 @@ void nr_drupal_invoke_all_hook_stacks_pop() { zval* hook_copy = nr_stack_pop(&NRPRG(drupal_invoke_all_hooks)); nr_php_arg_release(&hook_copy); } + if (nr_stack_is_empty(&NRPRG(drupal_invoke_all_hooks))) { + NRPRG(check_cufa) = false; + } } #endif diff --git a/agent/fw_wordpress.c b/agent/fw_wordpress.c index 094a83fc5..96a24ad11 100644 --- a/agent/fw_wordpress.c +++ b/agent/fw_wordpress.c @@ -517,6 +517,7 @@ NR_PHP_WRAPPER(nr_wordpress_exec_handle_tag) { * the call_user_func_array instrumentation take care of actually timing * the hooks by checking if it's set. */ + NRPRG(check_cufa) = true; nr_stack_push(&NRPRG(wordpress_tags), nr_wordpress_clean_tag(tag TSRMLS_CC)); nr_stack_push(&NRPRG(wordpress_tag_states), (void*)!NULL); @@ -624,6 +625,7 @@ NR_PHP_WRAPPER(nr_wordpress_apply_filters) { * the call_user_func_array instrumentation take care of actually timing * the hooks by checking if it's set. */ + NRPRG(check_cufa) = true; nr_stack_push(&NRPRG(wordpress_tags), nr_wordpress_clean_tag(tag TSRMLS_CC)); nr_stack_push(&NRPRG(wordpress_tag_states), (void*)!NULL); @@ -668,6 +670,9 @@ static void clean_wordpress_tag_stack() { char* cleaned_tag = nr_stack_pop(&NRPRG(wordpress_tags)); nr_free(cleaned_tag); } + if (nr_stack_is_empty(&NRPRG(wordpress_tags))) { + NRPRG(check_cufa) = false; + } } NR_PHP_WRAPPER(nr_wordpress_handle_tag_stack_after) { @@ -740,6 +745,8 @@ void nr_wordpress_enable(TSRMLS_D) { nr_wordpress_exec_handle_tag TSRMLS_CC); #endif /* OAPI */ - nr_php_add_call_user_func_array_pre_callback( - nr_wordpress_call_user_func_array TSRMLS_CC); + if (0 != NRINI(wordpress_hooks)) { + nr_php_add_call_user_func_array_pre_callback( + nr_wordpress_call_user_func_array TSRMLS_CC); + } } diff --git a/agent/php_execute.c b/agent/php_execute.c index 2f31f7fac..d5ce67b31 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -1930,11 +1930,6 @@ static void nr_php_observer_attempt_call_cufa_handler(NR_EXECUTE_PROTO) { return; } - if (NULL == execute_data->prev_execute_data->opline) { - nrl_verbosedebug(NRL_AGENT, "%s: cannot get previous opline", __func__); - return; - } - /* * COPIED Comment from php_vm.c: * To actually determine whether this is a call_user_func_array() call we @@ -1979,6 +1974,11 @@ static void nr_php_observer_attempt_call_cufa_handler(NR_EXECUTE_PROTO) { return; } + if (UNEXPECTED(NULL == execute_data->prev_execute_data->opline)) { + nrl_verbosedebug(NRL_AGENT, "%s: cannot get previous opline", __func__); + return; + } + const zend_op* prev_opline = execute_data->prev_execute_data->opline; /* @@ -2031,7 +2031,7 @@ static void nr_php_instrument_func_begin(NR_EXECUTE_PROTO) { filename TSRMLS_CC); return; } - if (UNEXPECTED(NULL != NRPRG(cufa_callback))) { + if (NULL != NRPRG(cufa_callback) && NRPRG(check_cufa)) { /* * For PHP 7+, call_user_func_array() is flattened into an inline by default. Because * of this, we must check the opcodes set to see whether we are calling it flattened. diff --git a/agent/php_newrelic.h b/agent/php_newrelic.h index d74505663..958725c2d 100644 --- a/agent/php_newrelic.h +++ b/agent/php_newrelic.h @@ -432,6 +432,7 @@ int symfony1_in_error404; /* Whether we are currently within a #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA +bool check_cufa; /* Without OAPI, we are able to utilize the call stack to keep track * of the previous tags. With OAPI, we can no longer do this so * we track the stack manually */ diff --git a/agent/php_rinit.c b/agent/php_rinit.c index bcd45bf96..37fd26451 100644 --- a/agent/php_rinit.c +++ b/agent/php_rinit.c @@ -122,6 +122,7 @@ PHP_RINIT_FUNCTION(newrelic) { */ #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA + NRPRG(check_cufa) = false; nr_stack_init(&NRPRG(predis_ctxs), NR_STACK_DEFAULT_CAPACITY); nr_stack_init(&NRPRG(wordpress_tags), NR_STACK_DEFAULT_CAPACITY); nr_stack_init(&NRPRG(wordpress_tag_states), NR_STACK_DEFAULT_CAPACITY); diff --git a/agent/tests/test_internal_instrument.c b/agent/tests/test_internal_instrument.c index de3f19619..a80d4bf1c 100644 --- a/agent/tests/test_internal_instrument.c +++ b/agent/tests/test_internal_instrument.c @@ -48,6 +48,10 @@ static void test_cufa_direct(TSRMLS_D) { zval* retval = NULL; tlib_php_request_start(); +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + NRPRG(check_cufa) = true; +#endif define_cufa_function_f(TSRMLS_C); tlib_php_request_eval( @@ -73,6 +77,10 @@ static void test_cufa_indirect(TSRMLS_D) { zval* retval = NULL; tlib_php_request_start(); +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + NRPRG(check_cufa) = true; +#endif define_cufa_function_f(TSRMLS_C); tlib_php_request_eval( From 54a23d4155102d9000c24865737148177db17cd5 Mon Sep 17 00:00:00 2001 From: ZNeumann Date: Thu, 21 Sep 2023 14:00:27 -0600 Subject: [PATCH 33/56] feat: optional instrumentated function metric (#730) There still exists a default wrapper, `nr_php_wrap_user_function`, which (for OAPI) creates an 'after' wraprec that is non-transient and creates an `InstrumentedFunction` metric. Before this PR, there was the option to create 'before', 'after', and 'clean' wrappers and define the transience. This PR extends the options available when creating a wraprec to no longer create the InstrumentedFunction metric. It wraps this option with the other bool option (transience) into an options struct. Additionally, if the top-most default, `nr_php_wrap_user_function`, is not used, every single option must be defined ('before', 'after', 'clean', transience, and instrumented metric). This takes the concept that if a single one of these fields is non-default, it is much more likely that other fields will also be non-default. The code writer will be forced to explicitly consider each option available. This does not add the option to create the InstrumentedFunction metric when calling `nr_wrap_callable`. --- agent/fw_cakephp.c | 5 +- agent/fw_drupal.c | 18 ++++--- agent/fw_drupal8.c | 9 ++-- agent/fw_laravel.c | 15 +++--- agent/fw_lumen.c | 4 +- agent/fw_magento2.c | 19 ++++---- agent/fw_symfony4.c | 5 +- agent/fw_wordpress.c | 20 ++++---- agent/fw_yii.c | 8 ++-- agent/lib_doctrine2.c | 5 +- agent/lib_predis.c | 16 +++---- agent/php_user_instrument.c | 35 ++++++++++---- agent/php_user_instrument.h | 15 +++++- agent/php_wrapper.c | 65 +++++++++++++++++-------- agent/php_wrapper.h | 15 ++++-- agent/tests/test_php_wrapper.c | 76 +++++++++++++----------------- agent/tests/test_user_instrument.c | 6 ++- 17 files changed, 188 insertions(+), 148 deletions(-) diff --git a/agent/fw_cakephp.c b/agent/fw_cakephp.c index 989f3f09b..1dfe88231 100644 --- a/agent/fw_cakephp.c +++ b/agent/fw_cakephp.c @@ -331,9 +331,8 @@ void nr_cakephp_enable_2(TSRMLS_D) { nr_cakephp_name_the_wt_2 TSRMLS_CC); #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA - nr_php_wrap_user_function_before_after_clean_with_transience( - NR_PSTR("CakeException::__construct"), nr_cakephp_problem_2, NULL, NULL, - NR_WRAPREC_NOT_TRANSIENT); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("CakeException::__construct"), nr_cakephp_problem_2, NULL, NULL); #else nr_php_wrap_user_function(NR_PSTR("CakeException::__construct"), nr_cakephp_problem_2 TSRMLS_CC); diff --git a/agent/fw_drupal.c b/agent/fw_drupal.c index d2ecb61a8..bb7d851b9 100644 --- a/agent/fw_drupal.c +++ b/agent/fw_drupal.c @@ -815,16 +815,14 @@ void nr_drupal_enable(TSRMLS_D) { nr_drupal_cron_run TSRMLS_CC); #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA - nr_php_wrap_user_function_before_after_clean_with_transience( - NR_PSTR("QFormBase::Run"), nr_drupal_qdrupal_name_the_wt, NULL, NULL, - NR_WRAPREC_NOT_TRANSIENT); - nr_php_wrap_user_function_before_after_clean_with_transience( + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("QFormBase::Run"), nr_drupal_qdrupal_name_the_wt, NULL, NULL); + nr_php_wrap_user_function_before_after_clean( NR_PSTR("drupal_page_cache_header"), nr_drupal_name_wt_as_cached_page, - NULL, NULL, NR_WRAPREC_NOT_TRANSIENT); - nr_php_wrap_user_function_before_after_clean_with_transience( + NULL, NULL); + nr_php_wrap_user_function_before_after_clean( NR_PSTR("drupal_http_request"), nr_drupal_http_request_before, - nr_drupal_http_request_after, nr_drupal_http_request_clean, - NR_WRAPREC_NOT_TRANSIENT); + nr_drupal_http_request_after, nr_drupal_http_request_clean); #else nr_php_wrap_user_function(NR_PSTR("QFormBase::Run"), nr_drupal_qdrupal_name_the_wt TSRMLS_CC); @@ -843,10 +841,10 @@ void nr_drupal_enable(TSRMLS_D) { nr_drupal_wrap_module_invoke TSRMLS_CC); #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA - nr_php_wrap_user_function_before_after_clean_with_transience( + nr_php_wrap_user_function_before_after_clean( NR_PSTR("module_invoke_all"), nr_drupal_wrap_module_invoke_all_before, nr_drupal_wrap_module_invoke_all_after, - nr_drupal_wrap_module_invoke_all_clean, NR_WRAPREC_NOT_TRANSIENT); + nr_drupal_wrap_module_invoke_all_clean); #else nr_php_wrap_user_function(NR_PSTR("module_invoke_all"), nr_drupal_wrap_module_invoke_all TSRMLS_CC); diff --git a/agent/fw_drupal8.c b/agent/fw_drupal8.c index 2041d14d9..b8ab99296 100644 --- a/agent/fw_drupal8.c +++ b/agent/fw_drupal8.c @@ -103,10 +103,9 @@ static void nr_drupal8_add_method_callback_before_after_clean( "%.*s::%.*s", NRSAFELEN(nr_php_class_entry_name_length(ce)), nr_php_class_entry_name(ce), NRSAFELEN(method_len), method); - nr_php_wrap_user_function_before_after_clean_with_transience( + nr_php_wrap_user_function_before_after_clean( class_method, nr_strlen(class_method), - before_callback, after_callback, clean_callback, - NR_WRAPREC_NOT_TRANSIENT); + before_callback, after_callback, clean_callback); nr_free(class_method); } @@ -667,10 +666,10 @@ void nr_drupal8_enable(TSRMLS_D) { */ #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA - nr_php_wrap_user_function_before_after_clean_with_transience( + nr_php_wrap_user_function_before_after_clean( NR_PSTR("Symfony\\Component\\HttpKernel\\EventListe" "ner\\RouterListener::onKernelRequest"), - nr_drupal8_name_the_wt_via_symfony, NULL, NULL, NR_WRAPREC_NOT_TRANSIENT); + nr_drupal8_name_the_wt_via_symfony, NULL, NULL); #else nr_php_wrap_user_function(NR_PSTR("Symfony\\Component\\HttpKernel\\EventListe" "ner\\RouterListener::onKernelRequest"), diff --git a/agent/fw_laravel.c b/agent/fw_laravel.c index 7ad9bd883..2302a803c 100644 --- a/agent/fw_laravel.c +++ b/agent/fw_laravel.c @@ -813,9 +813,8 @@ static void nr_laravel5_wrap_middleware(zval* app TSRMLS_DC) { Z_STRVAL_P(classname)); #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA - nr_php_wrap_user_function_before_after_clean_with_transience( - name, nr_strlen(name), nr_laravel5_middleware_handle, NULL, NULL, - NR_WRAPREC_NOT_TRANSIENT); + nr_php_wrap_user_function_before_after_clean( + name, nr_strlen(name), nr_laravel5_middleware_handle, NULL, NULL); #else nr_php_wrap_user_function(name, nr_strlen(name), nr_laravel5_middleware_handle TSRMLS_CC); @@ -875,9 +874,8 @@ static void nr_laravel_add_callback_method(const zend_class_entry* ce, #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA - nr_php_wrap_user_function_before_after_clean_with_transience( - class_method, nr_strlen(class_method), callback, NULL, NULL, - NR_WRAPREC_NOT_TRANSIENT); + nr_php_wrap_user_function_before_after_clean( + class_method, nr_strlen(class_method), callback, NULL, NULL); #else nr_php_wrap_user_function(class_method, nr_strlen(class_method), callback TSRMLS_CC); @@ -1262,10 +1260,9 @@ void nr_laravel_enable(TSRMLS_D) { */ #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA - nr_php_wrap_user_function_before_after_clean_with_transience( + nr_php_wrap_user_function_before_after_clean( NR_PSTR("Illuminate\\Console\\Application::doRun"), - nr_laravel_console_application_dorun, NULL, NULL, - NR_WRAPREC_NOT_TRANSIENT); + nr_laravel_console_application_dorun, NULL, NULL); #else nr_php_wrap_user_function(NR_PSTR("Illuminate\\Console\\Application::doRun"), nr_laravel_console_application_dorun TSRMLS_CC); diff --git a/agent/fw_lumen.c b/agent/fw_lumen.c index 8510c55a8..36330fca2 100644 --- a/agent/fw_lumen.c +++ b/agent/fw_lumen.c @@ -221,9 +221,9 @@ void nr_lumen_enable(TSRMLS_D) { nr_lumen_handle_found_route TSRMLS_CC); #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA - nr_php_wrap_user_function_before_after_clean_with_transience( + nr_php_wrap_user_function_before_after_clean( NR_PSTR("Laravel\\Lumen\\Application::sendExceptionToHandler"), - nr_lumen_exception, NULL, NULL, NR_WRAPREC_NOT_TRANSIENT); + nr_lumen_exception, NULL, NULL); #else nr_php_wrap_user_function( NR_PSTR("Laravel\\Lumen\\Application::sendExceptionToHandler"), diff --git a/agent/fw_magento2.c b/agent/fw_magento2.c index dafcbcc9e..87994936a 100644 --- a/agent/fw_magento2.c +++ b/agent/fw_magento2.c @@ -440,9 +440,9 @@ void nr_magento2_enable(TSRMLS_D) { */ #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA - nr_php_wrap_user_function_before_after_clean_with_transience( + nr_php_wrap_user_function_before_after_clean( NR_PSTR("Magento\\Framework\\App\\Action\\Action::dispatch"), - nr_magento2_action_dispatch, NULL, NULL, NR_WRAPREC_NOT_TRANSIENT); + nr_magento2_action_dispatch, NULL, NULL); #else nr_php_wrap_user_function( NR_PSTR("Magento\\Framework\\App\\Action\\Action::dispatch"), @@ -472,11 +472,10 @@ void nr_magento2_enable(TSRMLS_D) { */ #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA - nr_php_wrap_user_function_before_after_clean_with_transience( + nr_php_wrap_user_function_before_after_clean( NR_PSTR( "Magento\\Webapi\\Controller\\Rest\\InputParamsResolver::resolve"), - nr_magento2_inputparamsresolver_resolve, NULL, NULL, - NR_WRAPREC_NOT_TRANSIENT); + nr_magento2_inputparamsresolver_resolve, NULL, NULL); #else nr_php_wrap_user_function( NR_PSTR( @@ -498,16 +497,14 @@ void nr_magento2_enable(TSRMLS_D) { nr_magento2_soap_iswsdllistrequest TSRMLS_CC); #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA - nr_php_wrap_user_function_before_after_clean_with_transience( + nr_php_wrap_user_function_before_after_clean( NR_PSTR("Magento\\Webapi\\Controller\\Soap\\Request\\Handler::_" "prepareRequestData"), - nr_magento2_soap_handler_preparerequestdata, NULL, NULL, - NR_WRAPREC_NOT_TRANSIENT); - nr_php_wrap_user_function_before_after_clean_with_transience( + nr_magento2_soap_handler_preparerequestdata, NULL, NULL); + nr_php_wrap_user_function_before_after_clean( NR_PSTR("Magento\\Webapi\\Controller\\Soap\\Request\\Handler::" "prepareOperationInput"), - nr_magento2_soap_handler_prepareoperationinput, NULL, NULL, - NR_WRAPREC_NOT_TRANSIENT); + nr_magento2_soap_handler_prepareoperationinput, NULL, NULL); #else nr_php_wrap_user_function( diff --git a/agent/fw_symfony4.c b/agent/fw_symfony4.c index 9704fd191..904668e0c 100644 --- a/agent/fw_symfony4.c +++ b/agent/fw_symfony4.c @@ -267,10 +267,9 @@ void nr_symfony4_enable(TSRMLS_D) { */ #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA - nr_php_wrap_user_function_before_after_clean_with_transience( + nr_php_wrap_user_function_before_after_clean( NR_PSTR("Symfony\\Component\\Console\\Command\\Command::run"), - nr_symfony4_console_application_run, NULL, NULL, - NR_WRAPREC_NOT_TRANSIENT); + nr_symfony4_console_application_run, NULL, NULL); #else nr_php_wrap_user_function( NR_PSTR("Symfony\\Component\\Console\\Command\\Command::run"), diff --git a/agent/fw_wordpress.c b/agent/fw_wordpress.c index 96a24ad11..0ff0cffed 100644 --- a/agent/fw_wordpress.c +++ b/agent/fw_wordpress.c @@ -711,25 +711,21 @@ NR_PHP_WRAPPER_END void nr_wordpress_enable(TSRMLS_D) { #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA - nr_php_wrap_user_function_before_after_clean_with_transience( + nr_php_wrap_user_function_before_after_clean( NR_PSTR("apply_filters"), nr_wordpress_apply_filters, - nr_wordpress_apply_filters_after, nr_wordpress_handle_tag_stack_clean, - NR_WRAPREC_NOT_TRANSIENT); + nr_wordpress_apply_filters_after, nr_wordpress_handle_tag_stack_clean); - nr_php_wrap_user_function_before_after_clean_with_transience( + nr_php_wrap_user_function_before_after_clean( NR_PSTR("apply_filters_ref_array"), nr_wordpress_exec_handle_tag, - nr_wordpress_handle_tag_stack_after, nr_wordpress_handle_tag_stack_clean, - NR_WRAPREC_NOT_TRANSIENT); + nr_wordpress_handle_tag_stack_after, nr_wordpress_handle_tag_stack_clean); - nr_php_wrap_user_function_before_after_clean_with_transience( + nr_php_wrap_user_function_before_after_clean( NR_PSTR("do_action"), nr_wordpress_exec_handle_tag, - nr_wordpress_handle_tag_stack_after, nr_wordpress_handle_tag_stack_clean, - NR_WRAPREC_NOT_TRANSIENT); + nr_wordpress_handle_tag_stack_after, nr_wordpress_handle_tag_stack_clean); - nr_php_wrap_user_function_before_after_clean_with_transience( + nr_php_wrap_user_function_before_after_clean( NR_PSTR("do_action_ref_array"), nr_wordpress_exec_handle_tag, - nr_wordpress_handle_tag_stack_after, nr_wordpress_handle_tag_stack_clean, - NR_WRAPREC_NOT_TRANSIENT); + nr_wordpress_handle_tag_stack_after, nr_wordpress_handle_tag_stack_clean); #else nr_php_wrap_user_function(NR_PSTR("apply_filters"), diff --git a/agent/fw_yii.c b/agent/fw_yii.c index a336f2401..c6ff7cef8 100644 --- a/agent/fw_yii.c +++ b/agent/fw_yii.c @@ -91,12 +91,12 @@ NR_PHP_WRAPPER_END void nr_yii_enable(TSRMLS_D) { #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA - nr_php_wrap_user_function_before_after_clean_with_transience( + nr_php_wrap_user_function_before_after_clean( NR_PSTR("CAction::runWithParams"), nr_yii_runWithParams_wrapper, NULL, - NULL, NR_WRAPREC_NOT_TRANSIENT); - nr_php_wrap_user_function_before_after_clean_with_transience( + NULL); + nr_php_wrap_user_function_before_after_clean( NR_PSTR("CInlineAction::runWithParams"), nr_yii_runWithParams_wrapper, - NULL, NULL, NR_WRAPREC_NOT_TRANSIENT); + NULL, NULL); #else nr_php_wrap_user_function(NR_PSTR("CAction::runWithParams"), nr_yii_runWithParams_wrapper TSRMLS_CC); diff --git a/agent/lib_doctrine2.c b/agent/lib_doctrine2.c index f146d781f..d6132a3c4 100644 --- a/agent/lib_doctrine2.c +++ b/agent/lib_doctrine2.c @@ -97,10 +97,9 @@ nr_slowsqls_labelled_query_t* nr_doctrine2_lookup_input_query(TSRMLS_D) { void nr_doctrine2_enable(TSRMLS_D) { #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA - nr_php_wrap_user_function_before_after_clean_with_transience( + nr_php_wrap_user_function_before_after_clean( NR_PSTR("Doctrine\\ORM\\Query::_doExecute"), nr_doctrine2_cache_dql, - nr_doctrine2_cache_dql_after, nr_doctrine2_cache_dql_clean, - NR_WRAPREC_NOT_TRANSIENT); + nr_doctrine2_cache_dql_after, nr_doctrine2_cache_dql_clean); #else nr_php_wrap_user_function(NR_PSTR("Doctrine\\ORM\\Query::_doExecute"), nr_doctrine2_cache_dql TSRMLS_CC); diff --git a/agent/lib_predis.c b/agent/lib_predis.c index d7339482c..cbbc76104 100644 --- a/agent/lib_predis.c +++ b/agent/lib_predis.c @@ -795,26 +795,26 @@ void nr_predis_enable(TSRMLS_D) { */ #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA - nr_php_wrap_user_function_before_after_clean_with_transience( + nr_php_wrap_user_function_before_after_clean( NR_PSTR("Predis\\Pipeline\\Pipeline::executePipeline"), nr_predis_pipeline_executePipeline, nr_predis_pipeline_executePipeline_after, - nr_predis_pipeline_executePipeline_clean, NR_WRAPREC_NOT_TRANSIENT); - nr_php_wrap_user_function_before_after_clean_with_transience( + nr_predis_pipeline_executePipeline_clean); + nr_php_wrap_user_function_before_after_clean( NR_PSTR("Predis\\Pipeline\\Atomic::executePipeline"), nr_predis_pipeline_executePipeline, nr_predis_pipeline_executePipeline_after, - nr_predis_pipeline_executePipeline_clean, NR_WRAPREC_NOT_TRANSIENT); - nr_php_wrap_user_function_before_after_clean_with_transience( + nr_predis_pipeline_executePipeline_clean); + nr_php_wrap_user_function_before_after_clean( NR_PSTR("Predis\\Pipeline\\ConnectionErrorProof::executePipeline"), nr_predis_pipeline_executePipeline, nr_predis_pipeline_executePipeline_after, - nr_predis_pipeline_executePipeline_clean, NR_WRAPREC_NOT_TRANSIENT); - nr_php_wrap_user_function_before_after_clean_with_transience( + nr_predis_pipeline_executePipeline_clean); + nr_php_wrap_user_function_before_after_clean( NR_PSTR("Predis\\Pipeline\\FireAndForget::executePipeline"), nr_predis_pipeline_executePipeline, nr_predis_pipeline_executePipeline_after, - nr_predis_pipeline_executePipeline_clean, NR_WRAPREC_NOT_TRANSIENT); + nr_predis_pipeline_executePipeline_clean); #else nr_php_wrap_user_function( NR_PSTR("Predis\\Pipeline\\Pipeline::executePipeline"), diff --git a/agent/php_user_instrument.c b/agent/php_user_instrument.c index 5da7484be..6b8aeb0ea 100644 --- a/agent/php_user_instrument.c +++ b/agent/php_user_instrument.c @@ -295,7 +295,8 @@ static nruserfn_t* nr_php_user_wraprec_create(void) { } static nruserfn_t* nr_php_user_wraprec_create_named(const char* full_name, - int full_name_len) { + int full_name_len, + nr_instrumented_function_metric_t ifm) { int i; const char* name; const char* klass; @@ -338,8 +339,10 @@ static nruserfn_t* nr_php_user_wraprec_create_named(const char* full_name, wraprec->is_method = 1; } - wraprec->supportability_metric = nr_txn_create_fn_supportability_metric( - wraprec->funcname, wraprec->classname); + if (NR_WRAPREC_CREATE_INSTRUMENTED_FUNCTION_METRIC == ifm) { + wraprec->supportability_metric = nr_txn_create_fn_supportability_metric( + wraprec->funcname, wraprec->classname); + } return wraprec; } @@ -437,11 +440,13 @@ nruserfn_t* nr_php_add_custom_tracer_callable(zend_function* func TSRMLS_DC) { nruserfn_t* nr_php_add_custom_tracer_named(const char* namestr, size_t namestrlen, - nr_transience_t transience TSRMLS_DC) { + const nr_wrap_user_function_options_t* options + TSRMLS_DC) { nruserfn_t* wraprec; nruserfn_t* p; - wraprec = nr_php_user_wraprec_create_named(namestr, namestrlen); + wraprec = nr_php_user_wraprec_create_named(namestr, namestrlen, + options->instrumented_function_metric); if (0 == wraprec) { return 0; } @@ -470,7 +475,7 @@ nruserfn_t* nr_php_add_custom_tracer_named(const char* namestr, (0 == wraprec->classname) ? "" : "::", NRP_PHP(wraprec->funcname)); nr_php_wrap_user_function_internal(wraprec TSRMLS_CC); - if (transience == NR_WRAPREC_IS_TRANSIENT) { + if (NR_WRAPREC_IS_TRANSIENT == options->transience) { wraprec->transience = NR_WRAPREC_IS_TRANSIENT; } else { /* non-transient wraprecs are added to both the hashmap and linked list. @@ -558,9 +563,13 @@ void nr_php_add_user_instrumentation(TSRMLS_D) { void nr_php_add_transaction_naming_function(const char* namestr, int namestrlen TSRMLS_DC) { + nr_wrap_user_function_options_t options = { + NR_WRAPREC_NOT_TRANSIENT, + NR_WRAPREC_CREATE_INSTRUMENTED_FUNCTION_METRIC + }; nruserfn_t* wraprec = nr_php_add_custom_tracer_named(namestr, namestrlen, - NR_WRAPREC_NOT_TRANSIENT TSRMLS_CC); + &options TSRMLS_CC); if (NULL != wraprec) { wraprec->is_names_wt_simple = 1; @@ -568,9 +577,13 @@ void nr_php_add_transaction_naming_function(const char* namestr, } void nr_php_add_custom_tracer(const char* namestr, int namestrlen TSRMLS_DC) { + nr_wrap_user_function_options_t options = { + NR_WRAPREC_NOT_TRANSIENT, + NR_WRAPREC_CREATE_INSTRUMENTED_FUNCTION_METRIC + }; nruserfn_t* wraprec = nr_php_add_custom_tracer_named(namestr, namestrlen, - NR_WRAPREC_NOT_TRANSIENT TSRMLS_CC); + &options TSRMLS_CC); if (NULL != wraprec) { wraprec->create_metric = 1; @@ -627,9 +640,13 @@ void nr_php_user_function_add_declared_callback(const char* namestr, int namestrlen, nruserfn_declared_t callback TSRMLS_DC) { + nr_wrap_user_function_options_t options = { + NR_WRAPREC_NOT_TRANSIENT, + NR_WRAPREC_CREATE_INSTRUMENTED_FUNCTION_METRIC + }; nruserfn_t* wraprec = nr_php_add_custom_tracer_named(namestr, namestrlen, - NR_WRAPREC_NOT_TRANSIENT TSRMLS_CC); + &options TSRMLS_CC); if (0 != wraprec) { wraprec->declared_callback = callback; diff --git a/agent/php_user_instrument.h b/agent/php_user_instrument.h index d1a9373c1..911476c50 100644 --- a/agent/php_user_instrument.h +++ b/agent/php_user_instrument.h @@ -27,10 +27,22 @@ typedef struct nrspecialfn_return_t (*nrspecialfn_t)( typedef void (*nruserfn_declared_t)(TSRMLS_D); +/* Options for wrapping a user function */ typedef enum { NR_WRAPREC_NOT_TRANSIENT = 0, NR_WRAPREC_IS_TRANSIENT = 1 } nr_transience_t; + +typedef enum { + NR_WRAPREC_CREATE_INSTRUMENTED_FUNCTION_METRIC = 0, + NR_WRAPREC_NO_INSTRUMENTED_FUNCTION_METRIC = 1 +} nr_instrumented_function_metric_t; + +typedef struct _nr_wrap_user_function_options_t { + nr_transience_t transience; + nr_instrumented_function_metric_t instrumented_function_metric; +} nr_wrap_user_function_options_t; + /* * An equivalent data structure for user functions. * @@ -177,7 +189,8 @@ extern nruserfn_t* nr_php_add_custom_tracer_callable( zend_function* func TSRMLS_DC); extern nruserfn_t* nr_php_add_custom_tracer_named(const char* namestr, size_t namestrlen, - nr_transience_t transience TSRMLS_DC); + const nr_wrap_user_function_options_t* options + TSRMLS_DC); extern void nr_php_reset_user_instrumentation(void); extern void nr_php_remove_transient_user_instrumentation(void); extern void nr_php_add_user_instrumentation(TSRMLS_D); diff --git a/agent/php_wrapper.c b/agent/php_wrapper.c index 8d5dcc12c..8bb734b52 100644 --- a/agent/php_wrapper.c +++ b/agent/php_wrapper.c @@ -56,16 +56,31 @@ static void nr_php_wraprec_add_before_after_clean_callbacks( wraprec->special_instrumentation_clean = clean_callback; } -nruserfn_t* nr_php_wrap_user_function_before_after_clean_with_transience( +nruserfn_t* nr_php_wrap_user_function_before_after_clean( + const char* name, + size_t namelen, + nrspecialfn_t before_callback, + nrspecialfn_t after_callback, + nrspecialfn_t clean_callback) { + nr_wrap_user_function_options_t options = { + NR_WRAPREC_NOT_TRANSIENT, + NR_WRAPREC_CREATE_INSTRUMENTED_FUNCTION_METRIC + }; + return nr_php_wrap_user_function_before_after_clean_with_options( + name, namelen, before_callback, after_callback, + clean_callback, &options); +} + +nruserfn_t* nr_php_wrap_user_function_before_after_clean_with_options( const char* name, size_t namelen, nrspecialfn_t before_callback, nrspecialfn_t after_callback, nrspecialfn_t clean_callback, - nr_transience_t transience) { + const nr_wrap_user_function_options_t* options) { nruserfn_t* wraprec - = nr_php_add_custom_tracer_named(name, namelen, transience); + = nr_php_add_custom_tracer_named(name, namelen, options); nr_php_wraprec_add_before_after_clean_callbacks(name, namelen, wraprec, before_callback, @@ -105,17 +120,21 @@ nruserfn_t* nr_php_wrap_callable_before_after_clean( nruserfn_t* nr_php_wrap_user_function(const char* name, size_t namelen, nrspecialfn_t callback TSRMLS_DC) { - return nr_php_wrap_user_function_with_transience( - name, namelen, callback, NR_WRAPREC_NOT_TRANSIENT TSRMLS_CC); + nr_wrap_user_function_options_t options = { + NR_WRAPREC_NOT_TRANSIENT, + NR_WRAPREC_CREATE_INSTRUMENTED_FUNCTION_METRIC + }; + return nr_php_wrap_user_function_with_options( + name, namelen, callback, &options TSRMLS_CC); } -nruserfn_t* nr_php_wrap_user_function_with_transience(const char* name, - size_t namelen, - nrspecialfn_t callback, - nr_transience_t transience - TSRMLS_DC) { +nruserfn_t* nr_php_wrap_user_function_with_options(const char* name, + size_t namelen, + nrspecialfn_t callback, + const nr_wrap_user_function_options_t* options + TSRMLS_DC) { nruserfn_t* wraprec - = nr_php_add_custom_tracer_named(name, namelen, transience TSRMLS_CC); + = nr_php_add_custom_tracer_named(name, namelen, options TSRMLS_CC); if (wraprec && callback) { if ((NULL != wraprec->special_instrumentation) @@ -171,6 +190,9 @@ nruserfn_t* nr_php_wrap_callable(zend_function* callable, * is to create "after" wrappers (see nr_php_wrap_user_function). Should * "after"/"clean" wrappers ever be desired, it is suggested to create a * separate nr_php_wrap_generic_callable_before_after_clean() function. + * + * This creates a transient wraprec that does NOT produce an + * "InstrumentedFunction" metric. */ nruserfn_t* nr_php_wrap_generic_callable(zval* callable, nrspecialfn_t callback TSRMLS_DC) { @@ -182,6 +204,11 @@ nruserfn_t* nr_php_wrap_generic_callable(zval* callable, zend_fcall_info_cache fcc; zend_fcall_info fci; + nr_wrap_user_function_options_t options = { + NR_WRAPREC_IS_TRANSIENT, + NR_WRAPREC_NO_INSTRUMENTED_FUNCTION_METRIC + }; + /* not calling nr_zend_is_callable because we want to additionally populate * name */ if (zend_is_callable(callable, 0, &name TSRMLS_CC)) { @@ -195,13 +222,13 @@ nruserfn_t* nr_php_wrap_generic_callable(zval* callable, case IS_STRING: #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA - return nr_php_wrap_user_function_before_after_clean_with_transience( + return nr_php_wrap_user_function_before_after_clean_with_options( ZEND_STRING_VALUE(name), ZEND_STRING_LEN(name), callback, - NULL, NULL, NR_WRAPREC_IS_TRANSIENT); + NULL, NULL, &options); #else - return nr_php_wrap_user_function_with_transience( + return nr_php_wrap_user_function_with_options( ZEND_STRING_VALUE(name), ZEND_STRING_LEN(name), callback, - NR_WRAPREC_IS_TRANSIENT TSRMLS_CC); + &options TSRMLS_CC); #endif /* wrapping an array where [0] is an object and [1] is the method to @@ -210,13 +237,13 @@ nruserfn_t* nr_php_wrap_generic_callable(zval* callable, case IS_ARRAY: #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA - return nr_php_wrap_user_function_before_after_clean_with_transience( + return nr_php_wrap_user_function_before_after_clean_with_options( ZEND_STRING_VALUE(name), ZEND_STRING_LEN(name), callback, - NULL, NULL, NR_WRAPREC_IS_TRANSIENT); + NULL, NULL, &options); #else - return nr_php_wrap_user_function_with_transience( + return nr_php_wrap_user_function_with_options( ZEND_STRING_VALUE(name), ZEND_STRING_LEN(name), callback, - NR_WRAPREC_IS_TRANSIENT TSRMLS_CC); + &options TSRMLS_CC); #endif /* wrapping a closure. Need to initialize fcall info in order to wrap the diff --git a/agent/php_wrapper.h b/agent/php_wrapper.h index 3e7ccd694..04633f98c 100644 --- a/agent/php_wrapper.h +++ b/agent/php_wrapper.h @@ -136,13 +136,20 @@ * see how it works with frameworks. */ #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO -extern nruserfn_t* nr_php_wrap_user_function_before_after_clean_with_transience( +extern nruserfn_t* nr_php_wrap_user_function_before_after_clean( + const char* name, + size_t namelen, + nrspecialfn_t before_callback, + nrspecialfn_t after_callback, + nrspecialfn_t clean_callback); + +extern nruserfn_t* nr_php_wrap_user_function_before_after_clean_with_options( const char* name, size_t namelen, nrspecialfn_t before_callback, nrspecialfn_t after_callback, nrspecialfn_t clean_callback, - nr_transience_t transience); + const nr_wrap_user_function_options_t* options); extern nruserfn_t* nr_php_wrap_callable_before_after_clean( zend_function* callable, @@ -154,11 +161,11 @@ extern nruserfn_t* nr_php_wrap_user_function(const char* name, size_t namelen, nrspecialfn_t callback TSRMLS_DC); -extern nruserfn_t* nr_php_wrap_user_function_with_transience( +extern nruserfn_t* nr_php_wrap_user_function_with_options( const char* name, size_t namelen, nrspecialfn_t callback, - nr_transience_t TSRMLS_DC); + const nr_wrap_user_function_options_t* TSRMLS_DC); extern nruserfn_t* nr_php_wrap_user_function_extra(const char* name, size_t namelen, diff --git a/agent/tests/test_php_wrapper.c b/agent/tests/test_php_wrapper.c index 0e816b135..513654b2b 100644 --- a/agent/tests/test_php_wrapper.c +++ b/agent/tests/test_php_wrapper.c @@ -149,13 +149,12 @@ static void execute_nested_framework_calls(nrspecialfn_t one_before, #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA - nr_php_wrap_user_function_before_after_clean_with_transience( - NR_PSTR("one"), one_before, one_after, NULL, NR_WRAPREC_NOT_TRANSIENT); - nr_php_wrap_user_function_before_after_clean_with_transience( - NR_PSTR("two"), two_before, two_after, NULL, NR_WRAPREC_NOT_TRANSIENT); - nr_php_wrap_user_function_before_after_clean_with_transience( - NR_PSTR("three"), three_before, three_after, NULL, - NR_WRAPREC_NOT_TRANSIENT); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("one"), one_before, one_after, NULL); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("two"), two_before, two_after, NULL); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("three"), three_before, three_after, NULL); #else /* * This will pick up whichever one isn't null. @@ -614,37 +613,31 @@ static void test_add_arg(TSRMLS_D) { #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ tlib_php_request_eval("function arg0_def0() { return 4; }" TSRMLS_CC); - nr_php_wrap_user_function_before_after_clean_with_transience( - NR_PSTR("arg0_def0"), test_add_array, NULL, NULL, - NR_WRAPREC_NOT_TRANSIENT TSRMLS_CC); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("arg0_def0"), test_add_array, NULL, NULL TSRMLS_CC); tlib_php_request_eval("function arg1_def0($a) { return $a; }" TSRMLS_CC); - nr_php_wrap_user_function_before_after_clean_with_transience( - NR_PSTR("arg1_def0"), test_add_array, NULL, NULL, - NR_WRAPREC_NOT_TRANSIENT TSRMLS_CC); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("arg1_def0"), test_add_array, NULL, NULL TSRMLS_CC); tlib_php_request_eval( "function arg0_def1($a = null) { return $a; }" TSRMLS_CC); - nr_php_wrap_user_function_before_after_clean_with_transience( - NR_PSTR("arg0_def1"), test_add_array, NULL, NULL, - NR_WRAPREC_NOT_TRANSIENT TSRMLS_CC); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("arg0_def1"), test_add_array, NULL, NULL TSRMLS_CC); tlib_php_request_eval( "function arg1_def1($a, $b = null) { return $b; }" TSRMLS_CC); - nr_php_wrap_user_function_before_after_clean_with_transience( - NR_PSTR("arg1_def1"), test_add_array, NULL, NULL, - NR_WRAPREC_NOT_TRANSIENT TSRMLS_CC); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("arg1_def1"), test_add_array, NULL, NULL TSRMLS_CC); tlib_php_request_eval( "function arg1_def1_2($a, $b = null) { return $b; }" TSRMLS_CC); - nr_php_wrap_user_function_before_after_clean_with_transience( - NR_PSTR("arg1_def1_2"), test_add_2_arrays, NULL, NULL, - NR_WRAPREC_NOT_TRANSIENT TSRMLS_CC); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("arg1_def1_2"), test_add_2_arrays, NULL, NULL TSRMLS_CC); tlib_php_request_eval("function splat(...$a) { return $a[0]; }" TSRMLS_CC); - nr_php_wrap_user_function_before_after_clean_with_transience( - NR_PSTR("splat"), test_add_array, NULL, NULL, - NR_WRAPREC_NOT_TRANSIENT TSRMLS_CC); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("splat"), test_add_array, NULL, NULL TSRMLS_CC); #else tlib_php_request_eval("function arg0_def0() { return 4; }" TSRMLS_CC); nr_php_wrap_user_function(NR_PSTR("arg0_def0"), test_add_array TSRMLS_CC); @@ -810,9 +803,8 @@ static void test_before_after_clean() { tlib_php_request_eval( "function all_set($a) { if (0 == $a) { throw new " "RuntimeException('Division by zero'); } else return $a; }" TSRMLS_CC); - nr_php_wrap_user_function_before_after_clean_with_transience( - NR_PSTR("all_set"), test_before, test_after, test_clean, - NR_WRAPREC_NOT_TRANSIENT); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("all_set"), test_before, test_after, test_clean); /* * pass argument that will not throw exception. * before/after should be called. @@ -858,9 +850,8 @@ static void test_before_after_clean() { tlib_php_request_eval( "function before_after($a) { if (0 == $a) { throw new " "RuntimeException('Division by zero'); } else return $a; }" TSRMLS_CC); - nr_php_wrap_user_function_before_after_clean_with_transience( - NR_PSTR("before_after"), test_before, test_after, NULL, - NR_WRAPREC_NOT_TRANSIENT); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("before_after"), test_before, test_after, NULL); /* * pass argument that will not throw exception. @@ -909,9 +900,8 @@ static void test_before_after_clean() { tlib_php_request_eval( "function before_clean($a) { if (0 == $a) { throw new " "RuntimeException('Division by zero'); } else return $a; }" TSRMLS_CC); - nr_php_wrap_user_function_before_after_clean_with_transience( - NR_PSTR("before_clean"), test_before, NULL, test_clean, - NR_WRAPREC_NOT_TRANSIENT); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("before_clean"), test_before, NULL, test_clean); /* * pass argument that will not throw exception. @@ -957,9 +947,8 @@ static void test_before_after_clean() { tlib_php_request_eval( "function after_clean($a) { if (0 == $a) { throw new " "RuntimeException('Division by zero'); } else return $a; }" TSRMLS_CC); - nr_php_wrap_user_function_before_after_clean_with_transience( - NR_PSTR("after_clean"), NULL, test_after, test_clean, - NR_WRAPREC_NOT_TRANSIENT); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("after_clean"), NULL, test_after, test_clean); /* * pass argument that will not throw exception. * after should be called. @@ -1002,9 +991,8 @@ static void test_before_after_clean() { tlib_php_request_eval( "function before_only($a) { if (0 == $a) { throw new " "RuntimeException('Division by zero'); } else return $a; }" TSRMLS_CC); - nr_php_wrap_user_function_before_after_clean_with_transience( - NR_PSTR("before_only"), test_before, NULL, NULL, - NR_WRAPREC_NOT_TRANSIENT); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("before_only"), test_before, NULL, NULL); /* * pass argument that will not throw exception. * before should be called. @@ -1046,8 +1034,8 @@ static void test_before_after_clean() { tlib_php_request_eval( "function after_only($a) { if (0 == $a) { throw new " "RuntimeException('Division by zero'); } else return $a; }" TSRMLS_CC); - nr_php_wrap_user_function_before_after_clean_with_transience( - NR_PSTR("after_only"), NULL, test_after, NULL, NR_WRAPREC_NOT_TRANSIENT); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("after_only"), NULL, test_after, NULL); /* * pass argument that will not throw exception. * after should be called. @@ -1089,8 +1077,8 @@ static void test_before_after_clean() { tlib_php_request_eval( "function clean_only($a) { if (0 == $a) { throw new " "RuntimeException('Division by zero'); } else return $a; }" TSRMLS_CC); - nr_php_wrap_user_function_before_after_clean_with_transience( - NR_PSTR("clean_only"), NULL, NULL, test_clean, NR_WRAPREC_NOT_TRANSIENT); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("clean_only"), NULL, NULL, test_clean); /* * pass argument that will not throw exception. * clean should be called. diff --git a/agent/tests/test_user_instrument.c b/agent/tests/test_user_instrument.c index 3f2484c0e..a465922ee 100644 --- a/agent/tests/test_user_instrument.c +++ b/agent/tests/test_user_instrument.c @@ -96,8 +96,12 @@ static void test_hashmap_wraprec() { user_func1_wraprec); /* instrument user function */ + nr_wrap_user_function_options_t options = { + NR_WRAPREC_NOT_TRANSIENT, + NR_WRAPREC_CREATE_INSTRUMENTED_FUNCTION_METRIC + }; user_func1_wraprec = nr_php_add_custom_tracer_named( - user_func1_name, nr_strlen(user_func1_name), NR_WRAPREC_NOT_TRANSIENT ); + user_func1_name, nr_strlen(user_func1_name), &options ); wraprec_found = nr_php_get_wraprec(user_func1_zf); tlib_pass_if_ptr_equal("lookup instrumented user function succeeds", wraprec_found, user_func1_wraprec); From b6c47aaf994947bacec3bed065963ca17cbe9fe1 Mon Sep 17 00:00:00 2001 From: ZNeumann Date: Thu, 21 Sep 2023 14:45:33 -0600 Subject: [PATCH 34/56] feat: redo wordpress hooks instrumentation for OAPI (#731) Bypass the need to check for cufa calls during do_action/filter by instrumenting add_action/filter and wrapping the desired calls at hook time, rather than call time. Substantially improves performance and stability. --- agent/fw_wordpress.c | 31 +++++-- .../frameworks/wordpress/mock_hooks.php | 27 ++++++ .../test_wordpress_apply_filters.php | 16 ++-- .../test_wordpress_apply_filters.php8.php | 83 ------------------ .../wordpress/test_wordpress_do_action.php | 29 +++++-- .../test_wordpress_do_action.php8.php | 87 ------------------- 6 files changed, 81 insertions(+), 192 deletions(-) create mode 100644 tests/integration/frameworks/wordpress/mock_hooks.php delete mode 100644 tests/integration/frameworks/wordpress/test_wordpress_apply_filters.php8.php delete mode 100644 tests/integration/frameworks/wordpress/test_wordpress_do_action.php8.php diff --git a/agent/fw_wordpress.c b/agent/fw_wordpress.c index 0ff0cffed..de530657d 100644 --- a/agent/fw_wordpress.c +++ b/agent/fw_wordpress.c @@ -404,6 +404,8 @@ NR_PHP_WRAPPER(nr_wordpress_wrap_hook) { } NR_PHP_WRAPPER_END +#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ + || defined OVERWRITE_ZEND_EXECUTE_DATA /* * A call_user_func_array() callback to ensure that we wrap each hook function. */ @@ -416,15 +418,8 @@ static void nr_wordpress_call_user_func_array(zend_function* func, * function, we're instrumenting hooks, and WordPress is currently executing * hooks (denoted by the wordpress_tag being set). */ -#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ - && !defined OVERWRITE_ZEND_EXECUTE_DATA - if ((NR_FW_WORDPRESS != NRPRG(current_framework)) - || (0 == NRINI(wordpress_hooks)) - || (NULL == nr_stack_get_top(&NRPRG(wordpress_tags)))) { -#else if ((NR_FW_WORDPRESS != NRPRG(current_framework)) || (0 == NRINI(wordpress_hooks)) || (NULL == NRPRG(wordpress_tag))) { -#endif /* OAPI */ return; } @@ -445,6 +440,7 @@ static void nr_wordpress_call_user_func_array(zend_function* func, */ nr_php_wrap_callable(func, nr_wordpress_wrap_hook TSRMLS_CC); } +#endif /* not OAPI */ /* * Some plugins generate transient tag names. We can detect these by checking @@ -706,6 +702,22 @@ NR_PHP_WRAPPER(nr_wordpress_apply_filters_after) { } NR_PHP_WRAPPER_END +NR_PHP_WRAPPER(nr_wordpress_add_filter) { + /* Wordpress's add_action() is just a wrapper around add_filter(), + * so we only need to instrument this function */ + NR_UNUSED_SPECIALFN; + (void)wraprec; + + NR_PHP_WRAPPER_REQUIRE_FRAMEWORK(NR_FW_WORDPRESS); + + zval* callback = nr_php_arg_get(2, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + /* the callback here can be any PHP callable. nr_php_wrap_generic_callable + * checks that a valid callable is passed */ + nr_php_wrap_generic_callable(callback, nr_wordpress_wrap_hook); + nr_php_arg_release(&callback); +} +NR_PHP_WRAPPER_END + #endif /* OAPI */ void nr_wordpress_enable(TSRMLS_D) { @@ -727,6 +739,9 @@ void nr_wordpress_enable(TSRMLS_D) { NR_PSTR("do_action_ref_array"), nr_wordpress_exec_handle_tag, nr_wordpress_handle_tag_stack_after, nr_wordpress_handle_tag_stack_clean); + nr_php_wrap_user_function(NR_PSTR("add_filter"), + nr_wordpress_add_filter); + #else nr_php_wrap_user_function(NR_PSTR("apply_filters"), nr_wordpress_apply_filters TSRMLS_CC); @@ -739,10 +754,10 @@ void nr_wordpress_enable(TSRMLS_D) { nr_php_wrap_user_function(NR_PSTR("do_action_ref_array"), nr_wordpress_exec_handle_tag TSRMLS_CC); -#endif /* OAPI */ if (0 != NRINI(wordpress_hooks)) { nr_php_add_call_user_func_array_pre_callback( nr_wordpress_call_user_func_array TSRMLS_CC); } +#endif /* OAPI */ } diff --git a/tests/integration/frameworks/wordpress/mock_hooks.php b/tests/integration/frameworks/wordpress/mock_hooks.php new file mode 100644 index 000000000..7419bbc65 --- /dev/null +++ b/tests/integration/frameworks/wordpress/mock_hooks.php @@ -0,0 +1,27 @@ +=")) { - die("skip: PHP >= 8.0 uses other test\n"); -} */ /*INI @@ -23,6 +20,9 @@ */ /*EXPECT +add filter +add filter +add filter f: string1 h: string3 g: string2 @@ -50,10 +50,7 @@ ] */ -// Simple mock of wordpress's apply_filter() -function apply_filters($tag, ...$args) { - call_user_func_array($tag, $args); -} +require_once __DIR__.'/mock_hooks.php'; function h($str) { echo "h: "; @@ -79,4 +76,9 @@ function f($str) { } } +// Due to the mock simplification described above, the hook +// is not used in this test, and the callback is treated as the hook +add_filter("hook", "f"); +add_filter("hook", "g"); +add_filter("hook", "h"); apply_filters("f", "string1"); diff --git a/tests/integration/frameworks/wordpress/test_wordpress_apply_filters.php8.php b/tests/integration/frameworks/wordpress/test_wordpress_apply_filters.php8.php deleted file mode 100644 index 299616114..000000000 --- a/tests/integration/frameworks/wordpress/test_wordpress_apply_filters.php8.php +++ /dev/null @@ -1,83 +0,0 @@ -=")) { - die("skip: PHP >= 8.0 uses other test\n"); -} */ /*INI @@ -23,6 +20,10 @@ */ /*EXPECT +add filter +add filter +add filter +g f h g @@ -50,10 +51,7 @@ ] */ -// Simple mock of wordpress's do_action() -function do_action($tag, ...$args) { - call_user_func_array($tag, $args); -} +require_once __DIR__.'/mock_hooks.php'; function h() { echo "h\n"; @@ -73,4 +71,21 @@ function f() { } } +// Due to the mock simplification described above, the hook +// is not used in this test, and the callback is treated as the hook +add_action("hook", "f"); +add_action("hook", "g"); +add_action("hook", "h"); +/* + * pre-OAPI: Initiates a non-flattened call stack of internal->user_code + * to ensure that cufa instrumentation properly handles skipping + * opline lookups of internal functions + * + * OAPI: Initiates a call to an added action outside + * the context of do_action, to ensure we only instrument + * with an active hook + */ +$function = new ReflectionFunction('g'); +$function->invoke(); + do_action("f"); diff --git a/tests/integration/frameworks/wordpress/test_wordpress_do_action.php8.php b/tests/integration/frameworks/wordpress/test_wordpress_do_action.php8.php deleted file mode 100644 index 0483b2511..000000000 --- a/tests/integration/frameworks/wordpress/test_wordpress_do_action.php8.php +++ /dev/null @@ -1,87 +0,0 @@ -user_code - * to ensure that cufa instrumentation properly handles skipping - * opline lookups of internal functions - */ -$function = new ReflectionFunction('g'); -$function->invoke(); - - -do_action("f"); From 3eb2abe81abb20b6eab8397a881f413a1d33e237 Mon Sep 17 00:00:00 2001 From: ZNeumann Date: Fri, 29 Sep 2023 07:44:05 -0600 Subject: [PATCH 35/56] fix(agent): fix wrapping transient user functions (#735) Prevent adding transient wraprecs for already wrapped transient functions by checking if wraprec already exists in the hashmap. Additionally prevent adding transient wraprecs for if the transient function is already wrapped as non-transient function by checking if wraprec already exists in the linked list. For non-transient wrappers (standard instrumentation), the wrapper is stored in both the hashmap and linked-list. For transient wrappers, the wrapper is only stored in the hashmap. HOWEVER! non-transient wrappers MAY only be in the linked-list. This normally happens if it is wrapping a function that hasn't been loaded by PHP yet. Once the function is loaded, there are many hooks to ensure the wrapper is also added to the hashmap. The implications of the above are: For non-transient wrappers, we only need to check the linked-list to ensure that we are not duplicating a wrapper. If a wrapper is only in the hashmap, it is transient and will be overwritten by the non-transient wrapper. For transient wrappers, however, we must check both the linked list and the hashmap. This is because it is possible to attempt to create a transient wrapper around an already non-transiently wrapped function. This will return the non-transient wrapper and will attempt to set the transient callbacks if that wrapper has those callbacks free. This means that it is possible for callbacks intended to be transient are attached onto a wrapper that isn't. --- agent/php_user_instrument.c | 72 ++++++++++++++++--- .../drupal/test_invoke_all_with.php | 20 +++++- .../test_wordpress_apply_filters.php | 3 + 3 files changed, 84 insertions(+), 11 deletions(-) diff --git a/agent/php_user_instrument.c b/agent/php_user_instrument.c index 6b8aeb0ea..2fd77eb65 100644 --- a/agent/php_user_instrument.c +++ b/agent/php_user_instrument.c @@ -244,21 +244,22 @@ static void nr_php_wrap_zend_function(zend_function* func, } } -static void nr_php_wrap_user_function_internal(nruserfn_t* wraprec TSRMLS_DC) { +// Returns whether wraprec ownership was transfered to the hashmap +static bool nr_php_wrap_user_function_internal(nruserfn_t* wraprec TSRMLS_DC) { zend_function* orig_func = 0; if (0 == NR_PHP_PROCESS_GLOBALS(done_instrumentation)) { - return; + return false; } if (wraprec->is_wrapped) { - return; + return false; } #if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ && defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP8+ */ if (nrunlikely(-1 == NR_PHP_PROCESS_GLOBALS(zend_offset))) { - return; + return false; } #endif if (0 == wraprec->classname) { @@ -272,7 +273,7 @@ static void nr_php_wrap_user_function_internal(nruserfn_t* wraprec TSRMLS_DC) { if (NULL == orig_func) { /* It could be in a file not yet loaded, no reason to log anything. */ - return; + return false; } if (ZEND_USER_FUNCTION != orig_func->type) { @@ -285,9 +286,10 @@ static void nr_php_wrap_user_function_internal(nruserfn_t* wraprec TSRMLS_DC) { * logs with this message. */ wraprec->is_disabled = 1; - return; + return false; } nr_php_wrap_zend_function(orig_func, wraprec TSRMLS_CC); + return true; } static nruserfn_t* nr_php_user_wraprec_create(void) { @@ -451,7 +453,28 @@ nruserfn_t* nr_php_add_custom_tracer_named(const char* namestr, return 0; } - /* Make sure that we are not duplicating an existing wraprecord */ + /* Make sure that we are not duplicating an existing wraprecord. + * + * For non-transient wrappers (standard instrumentation), the wrapper + * is stored in both the hashmap and linked-list. For transient wrappers, + * the wrapper is only stored in the hashmap. HOWEVER! non-transient + * wrappers MAY only be in the linked-list. This normally happens if it + * is wrapping a function that hasn't been loaded by PHP yet. Once the + * function is loaded, there are many hooks to ensure the wrapper is also + * added to the hashmap. + * + * The implications of the above are: For non-transient wrappers, we only + * need to check the linked-list to ensure that we are not duplicating a + * wrapper. If a wrapper is only in the hashmap, it is transient and will + * be overwritten by the non-transient wrapper. + * + * For transient wrappers, however, we must check both the linked + * list and the hashmap. This is because it is possible to attempt to + * create a transient wrapper around an already non-transiently wrapped + * function. This will return the non-transient wrapper and will attempt + * to set the transient callbacks if that wrapper has those callbacks free. + * This means that it is possible for callbacks intended to be transient + * are attached onto a wrapper that isn't. */ p = nr_wrapped_user_functions; while (0 != p) { @@ -468,15 +491,48 @@ nruserfn_t* nr_php_add_custom_tracer_named(const char* namestr, } p = p->next; } + if (NR_WRAPREC_IS_TRANSIENT == options->transience) { + zend_function* orig_func = 0; + if (0 == wraprec->classname) { + orig_func = nr_php_find_function(wraprec->funcnameLC TSRMLS_CC); + } else { + zend_class_entry* orig_class = 0; + + orig_class = nr_php_find_class(wraprec->classnameLC TSRMLS_CC); + orig_func = nr_php_find_class_method(orig_class, wraprec->funcnameLC); + } + + if (NULL != orig_func) { +#if ZEND_MODULE_API_NO < ZEND_7_4_X_API_NO + p = nr_php_op_array_get_wraprec(&orig_func->op_array TSRMLS_CC); +#else + p = nr_php_wraprec_lookup_get(orig_func); +#endif + + if (p) { + nrl_verbosedebug(NRL_INSTRUMENT, "reusing custom wrapper for callable '%s'", + namestr); + nr_php_user_wraprec_destroy(&wraprec); + nr_php_wrap_user_function_internal(p TSRMLS_CC); + return p; + } + } + } nrl_verbosedebug( NRL_INSTRUMENT, "adding custom for '" NRP_FMT_UQ "%.5s" NRP_FMT_UQ "'", NRP_PHP(wraprec->classname), (0 == wraprec->classname) ? "" : "::", NRP_PHP(wraprec->funcname)); - nr_php_wrap_user_function_internal(wraprec TSRMLS_CC); + bool added = nr_php_wrap_user_function_internal(wraprec TSRMLS_CC); if (NR_WRAPREC_IS_TRANSIENT == options->transience) { wraprec->transience = NR_WRAPREC_IS_TRANSIENT; + /* If the wraprec (for one reason or another) was not added to the hashmap + * and will not be added to the linked list, it needs to be destroyed */ + if (!added) { + nr_php_user_wraprec_destroy(&wraprec); + return NULL; + } } else { /* non-transient wraprecs are added to both the hashmap and linked list. * At request shutdown, the hashmap will free transients, but leave diff --git a/tests/integration/frameworks/drupal/test_invoke_all_with.php b/tests/integration/frameworks/drupal/test_invoke_all_with.php index 87afb932e..107e20a98 100644 --- a/tests/integration/frameworks/drupal/test_invoke_all_with.php +++ b/tests/integration/frameworks/drupal/test_invoke_all_with.php @@ -26,6 +26,7 @@ a3 b4 b4 +b2 */ /*EXPECT_METRICS @@ -37,11 +38,11 @@ [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/all"}, [1, "??", "??", "??", "??", "??"]], [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Framework/Drupal/Hook/hook_1"}, [2, "??", "??", "??", "??", "??"]], - [{"name":"Framework/Drupal/Hook/hook_2"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Framework/Drupal/Hook/hook_2"}, [2, "??", "??", "??", "??", "??"]], [{"name":"Framework/Drupal/Hook/hook_3"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Framework/Drupal/Hook/hook_4"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Framework/Drupal/Module/module_a"}, [2, "??", "??", "??", "??", "??"]], - [{"name":"Framework/Drupal/Module/module_b"}, [3, "??", "??", "??", "??", "??"]], + [{"name":"Framework/Drupal/Module/module_b"}, [4, "??", "??", "??", "??", "??"]], [{"name":"OtherTransaction/all"}, [1, "??", "??", "??", "??", "??"]], [{"name":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], [{"name":"OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], @@ -49,9 +50,12 @@ [{"name":"Supportability/framework/Drupal8/forced"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Supportability/api/add_custom_tracer"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/api/add_custom_tracer"}, [2, "??", "??", "??", "??", "??"]], [{"name":"Custom/invoke_callback_instrumented"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Custom/invoke_callback"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Custom/invoke_callback_instrumented", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Custom/invoke_callback", "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]] ] ] @@ -125,5 +129,15 @@ public function invoke(callable $hook, string $module) { /* At this point, module_b_hook_4 should NOT be instrumented */ // test string callback; function already instrumented +// This will reuse the existing wraprec and successfully +// add instrumentation because the "before" callback is unset $func_name = "invoke_callback_instrumented"; $handler->invokeallwith("hook_4", $func_name); + +// test non-transiently wrapping an already transiently instrumented function +// This will overwrite the existing transient wrapper +$func_name = "invoke_callback"; +newrelic_add_custom_tracer($func_name); +// Now this test will function the same as above: adding special instrumentation +// to an already existing wrapper +$handler->invokeallwith("hook_2", $func_name); diff --git a/tests/integration/frameworks/wordpress/test_wordpress_apply_filters.php b/tests/integration/frameworks/wordpress/test_wordpress_apply_filters.php index 8a42f433c..65a280fe3 100644 --- a/tests/integration/frameworks/wordpress/test_wordpress_apply_filters.php +++ b/tests/integration/frameworks/wordpress/test_wordpress_apply_filters.php @@ -24,6 +24,7 @@ add filter add filter f: string1 +add filter h: string3 g: string2 */ @@ -69,6 +70,8 @@ function f($str) { echo "f: "; echo $str; echo "\n"; + // For OAPI: attempt to overwrite the currently executing transient wrapper + add_filter("hook", "f"); try { apply_filters("h", "string3"); } catch (Exception $e) { From 2b9e3b4195b7f61cab4e9252a802957193ee2d27 Mon Sep 17 00:00:00 2001 From: Michal Nowacki Date: Fri, 29 Sep 2023 10:07:32 -0400 Subject: [PATCH 36/56] add new metric to oapi integration tests Add log decorating metric to integration tests added in oapi. --- .../external/drupal7/test_bad_params_integer_headers.php8.php | 1 + .../external/drupal7/test_bad_params_null_headers.php8.php | 1 + tests/integration/frameworks/drupal/test_module_invoke_all.php | 1 + .../frameworks/drupal/test_module_invoke_all.php8.php | 1 + .../frameworks/wordpress/test_wordpress_apply_filters.php | 1 + .../frameworks/wordpress/test_wordpress_do_action.php | 1 + tests/integration/lang/test_generator_8.0.php | 1 + 7 files changed, 7 insertions(+) diff --git a/tests/integration/external/drupal7/test_bad_params_integer_headers.php8.php b/tests/integration/external/drupal7/test_bad_params_integer_headers.php8.php index 8781876b3..36cf6b0f1 100644 --- a/tests/integration/external/drupal7/test_bad_params_integer_headers.php8.php +++ b/tests/integration/external/drupal7/test_bad_params_integer_headers.php8.php @@ -27,6 +27,7 @@ [ [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Errors/OtherTransaction/php__FILE__"}, [1, 0, 0, 0, 0, 0]], [{"name":"Errors/all"}, [1, 0, 0, 0, 0, 0]], [{"name":"Errors/allOther"}, [1, 0, 0, 0, 0, 0]], diff --git a/tests/integration/external/drupal7/test_bad_params_null_headers.php8.php b/tests/integration/external/drupal7/test_bad_params_null_headers.php8.php index e5e2a5558..1189821b1 100644 --- a/tests/integration/external/drupal7/test_bad_params_null_headers.php8.php +++ b/tests/integration/external/drupal7/test_bad_params_null_headers.php8.php @@ -27,6 +27,7 @@ [ [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Errors/OtherTransaction/php__FILE__"}, [1, 0, 0, 0, 0, 0]], [{"name":"Errors/all"}, [1, 0, 0, 0, 0, 0]], [{"name":"Errors/allOther"}, [1, 0, 0, 0, 0, 0]], diff --git a/tests/integration/frameworks/drupal/test_module_invoke_all.php b/tests/integration/frameworks/drupal/test_module_invoke_all.php index 57936a6d3..c7f832e48 100644 --- a/tests/integration/frameworks/drupal/test_module_invoke_all.php +++ b/tests/integration/frameworks/drupal/test_module_invoke_all.php @@ -41,6 +41,7 @@ [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Forwarding/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Metrics/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Framework/Drupal/Hook/hook_with_arg"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Framework/Drupal/Hook/f"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Framework/Drupal/Hook/g"}, [1, "??", "??", "??", "??", "??"]], diff --git a/tests/integration/frameworks/drupal/test_module_invoke_all.php8.php b/tests/integration/frameworks/drupal/test_module_invoke_all.php8.php index c14b92fd1..fc694e039 100644 --- a/tests/integration/frameworks/drupal/test_module_invoke_all.php8.php +++ b/tests/integration/frameworks/drupal/test_module_invoke_all.php8.php @@ -42,6 +42,7 @@ [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Forwarding/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Metrics/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Framework/Drupal/Hook/hook_with_arg"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Framework/Drupal/Hook/f"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Framework/Drupal/Hook/g"}, [1, "??", "??", "??", "??", "??"]], diff --git a/tests/integration/frameworks/wordpress/test_wordpress_apply_filters.php b/tests/integration/frameworks/wordpress/test_wordpress_apply_filters.php index 65a280fe3..654171af7 100644 --- a/tests/integration/frameworks/wordpress/test_wordpress_apply_filters.php +++ b/tests/integration/frameworks/wordpress/test_wordpress_apply_filters.php @@ -46,6 +46,7 @@ [{"name": "OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/framework/WordPress/forced"}, [1, "??", "??", "??", "??", "??"]] ] ] diff --git a/tests/integration/frameworks/wordpress/test_wordpress_do_action.php b/tests/integration/frameworks/wordpress/test_wordpress_do_action.php index 4c394f324..058a24e6a 100644 --- a/tests/integration/frameworks/wordpress/test_wordpress_do_action.php +++ b/tests/integration/frameworks/wordpress/test_wordpress_do_action.php @@ -46,6 +46,7 @@ [{"name": "OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/framework/WordPress/forced"}, [1, "??", "??", "??", "??", "??"]] ] ] diff --git a/tests/integration/lang/test_generator_8.0.php b/tests/integration/lang/test_generator_8.0.php index eaacc7a1f..62139de98 100644 --- a/tests/integration/lang/test_generator_8.0.php +++ b/tests/integration/lang/test_generator_8.0.php @@ -34,6 +34,7 @@ [ [{"name": "Supportability/Logging/Forwarding/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Metrics/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/all"}, [1, "??", "??", "??", "??", "??"]], [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther"}, From 8bcd5ca7f8e0f53d44bdca43ea16e15abbdb7966 Mon Sep 17 00:00:00 2001 From: Amber Sistla Date: Sun, 22 Oct 2023 14:34:36 -0700 Subject: [PATCH 37/56] tests(agent): Add tests that specifically exercise JIT. (#715) Tests to exercise JIT. Please see original forked PR here: https://github.com/newrelic/newrelic-php-agent/pull/583 --------- Co-authored-by: Michal Nowacki --- src/newrelic/integration/parse.go | 30 ++ src/newrelic/integration/test.go | 11 +- tests/integration/jit/function/skipif.inc | 15 + .../jit/function/test_computations.php | 78 +++++ .../jit/function/test_even_odd_count.php | 84 ++++++ .../function/test_recursion_no_segfault.php | 55 ++++ .../jit/function/test_span_class_function.php | 278 ++++++++++++++++++ ..._span_events_are_created_from_segments.php | 118 ++++++++ ...n_events_are_created_upon_caught_error.php | 194 ++++++++++++ ...test_span_events_are_created_upon_exit.php | 123 ++++++++ ...events_are_created_upon_uncaught_error.php | 133 +++++++++ ...reated_upon_uncaught_handled_exception.php | 196 ++++++++++++ ...ught_handled_exception_invalid_handler.php | 210 +++++++++++++ ...ated_upon_uncaught_unhandled_exception.php | 170 +++++++++++ ...t_span_events_error_collector_disabled.php | 130 ++++++++ ...st_span_events_exception_caught_nested.php | 217 ++++++++++++++ ...vents_exception_caught_nested_rethrown.php | 196 ++++++++++++ ...n_events_exception_caught_notice_error.php | 169 +++++++++++ ...s_exception_caught_notice_error_nested.php | 245 +++++++++++++++ ...span_events_exception_caught_same_span.php | 139 +++++++++ ..._span_events_exception_uncaught_nested.php | 224 ++++++++++++++ ...est_span_events_exist_when_no_segments.php | 65 ++++ .../function/test_span_events_hsm_error.php | 137 +++++++++ .../test_span_events_notice_error.php | 157 ++++++++++ .../test_span_events_on_dt_off_cat_off.php | 50 ++++ .../test_span_events_on_dt_off_cat_on.php | 50 ++++ .../function/test_span_events_root_parent.php | 140 +++++++++ tests/integration/jit/tracing/skipif.inc | 16 + .../jit/tracing/test_computations.php | 77 +++++ .../jit/tracing/test_even_odd_count.php | 84 ++++++ .../tracing/test_recursion_no_segfault.php | 55 ++++ .../jit/tracing/test_span_class_function.php | 276 +++++++++++++++++ ..._span_events_are_created_from_segments.php | 118 ++++++++ ...n_events_are_created_upon_caught_error.php | 193 ++++++++++++ ...test_span_events_are_created_upon_exit.php | 123 ++++++++ ...events_are_created_upon_uncaught_error.php | 133 +++++++++ ...reated_upon_uncaught_handled_exception.php | 193 ++++++++++++ ...ught_handled_exception_invalid_handler.php | 205 +++++++++++++ ...ated_upon_uncaught_unhandled_exception.php | 170 +++++++++++ ...t_span_events_error_collector_disabled.php | 131 +++++++++ ...st_span_events_exception_caught_nested.php | 217 ++++++++++++++ ...vents_exception_caught_nested_rethrown.php | 196 ++++++++++++ ...n_events_exception_caught_notice_error.php | 169 +++++++++++ ...s_exception_caught_notice_error_nested.php | 246 ++++++++++++++++ ...span_events_exception_caught_same_span.php | 139 +++++++++ ..._span_events_exception_uncaught_nested.php | 225 ++++++++++++++ ...est_span_events_exist_when_no_segments.php | 65 ++++ .../tracing/test_span_events_hsm_error.php | 137 +++++++++ .../tracing/test_span_events_notice_error.php | 157 ++++++++++ .../test_span_events_on_dt_off_cat_off.php | 50 ++++ .../test_span_events_on_dt_off_cat_on.php | 50 ++++ .../tracing/test_span_events_root_parent.php | 140 +++++++++ 52 files changed, 7175 insertions(+), 4 deletions(-) create mode 100644 tests/integration/jit/function/skipif.inc create mode 100644 tests/integration/jit/function/test_computations.php create mode 100644 tests/integration/jit/function/test_even_odd_count.php create mode 100644 tests/integration/jit/function/test_recursion_no_segfault.php create mode 100644 tests/integration/jit/function/test_span_class_function.php create mode 100644 tests/integration/jit/function/test_span_events_are_created_from_segments.php create mode 100644 tests/integration/jit/function/test_span_events_are_created_upon_caught_error.php create mode 100644 tests/integration/jit/function/test_span_events_are_created_upon_exit.php create mode 100644 tests/integration/jit/function/test_span_events_are_created_upon_uncaught_error.php create mode 100644 tests/integration/jit/function/test_span_events_are_created_upon_uncaught_handled_exception.php create mode 100644 tests/integration/jit/function/test_span_events_are_created_upon_uncaught_handled_exception_invalid_handler.php create mode 100644 tests/integration/jit/function/test_span_events_are_created_upon_uncaught_unhandled_exception.php create mode 100644 tests/integration/jit/function/test_span_events_error_collector_disabled.php create mode 100644 tests/integration/jit/function/test_span_events_exception_caught_nested.php create mode 100644 tests/integration/jit/function/test_span_events_exception_caught_nested_rethrown.php create mode 100644 tests/integration/jit/function/test_span_events_exception_caught_notice_error.php create mode 100644 tests/integration/jit/function/test_span_events_exception_caught_notice_error_nested.php create mode 100644 tests/integration/jit/function/test_span_events_exception_caught_same_span.php create mode 100644 tests/integration/jit/function/test_span_events_exception_uncaught_nested.php create mode 100644 tests/integration/jit/function/test_span_events_exist_when_no_segments.php create mode 100644 tests/integration/jit/function/test_span_events_hsm_error.php create mode 100644 tests/integration/jit/function/test_span_events_notice_error.php create mode 100644 tests/integration/jit/function/test_span_events_on_dt_off_cat_off.php create mode 100644 tests/integration/jit/function/test_span_events_on_dt_off_cat_on.php create mode 100644 tests/integration/jit/function/test_span_events_root_parent.php create mode 100644 tests/integration/jit/tracing/skipif.inc create mode 100644 tests/integration/jit/tracing/test_computations.php create mode 100644 tests/integration/jit/tracing/test_even_odd_count.php create mode 100644 tests/integration/jit/tracing/test_recursion_no_segfault.php create mode 100644 tests/integration/jit/tracing/test_span_class_function.php create mode 100644 tests/integration/jit/tracing/test_span_events_are_created_from_segments.php create mode 100644 tests/integration/jit/tracing/test_span_events_are_created_upon_caught_error.php create mode 100644 tests/integration/jit/tracing/test_span_events_are_created_upon_exit.php create mode 100644 tests/integration/jit/tracing/test_span_events_are_created_upon_uncaught_error.php create mode 100644 tests/integration/jit/tracing/test_span_events_are_created_upon_uncaught_handled_exception.php create mode 100644 tests/integration/jit/tracing/test_span_events_are_created_upon_uncaught_handled_exception_invalid_handler.php create mode 100644 tests/integration/jit/tracing/test_span_events_are_created_upon_uncaught_unhandled_exception.php create mode 100644 tests/integration/jit/tracing/test_span_events_error_collector_disabled.php create mode 100644 tests/integration/jit/tracing/test_span_events_exception_caught_nested.php create mode 100644 tests/integration/jit/tracing/test_span_events_exception_caught_nested_rethrown.php create mode 100644 tests/integration/jit/tracing/test_span_events_exception_caught_notice_error.php create mode 100644 tests/integration/jit/tracing/test_span_events_exception_caught_notice_error_nested.php create mode 100644 tests/integration/jit/tracing/test_span_events_exception_caught_same_span.php create mode 100644 tests/integration/jit/tracing/test_span_events_exception_uncaught_nested.php create mode 100644 tests/integration/jit/tracing/test_span_events_exist_when_no_segments.php create mode 100644 tests/integration/jit/tracing/test_span_events_hsm_error.php create mode 100644 tests/integration/jit/tracing/test_span_events_notice_error.php create mode 100644 tests/integration/jit/tracing/test_span_events_on_dt_off_cat_off.php create mode 100644 tests/integration/jit/tracing/test_span_events_on_dt_off_cat_on.php create mode 100644 tests/integration/jit/tracing/test_span_events_root_parent.php diff --git a/src/newrelic/integration/parse.go b/src/newrelic/integration/parse.go index 554972556..36745dfa7 100644 --- a/src/newrelic/integration/parse.go +++ b/src/newrelic/integration/parse.go @@ -23,6 +23,7 @@ var ( "HEADERS": parseHeaders, "SKIPIF": parseRawSkipIf, "INI": parseSettings, + "PHPMODULES": parsePHPModules, "CONFIG": parseConfig, "DESCRIPTION": parseDescription, "EXPECT_ANALYTICS_EVENTS": parseAnalyticEvents, @@ -196,6 +197,35 @@ func parseSettings(t *Test, content []byte) error { return nil } +func parsePHPModules(t *Test, content []byte) error { + trimmed := bytes.TrimSpace(content) + settings := make(map[string]string) + scanner := bufio.NewScanner(bytes.NewReader(trimmed)) + delim := []byte("=") + + for scanner.Scan() { + parts := bytes.SplitN(scanner.Bytes(), delim, 2) + switch len(parts) { + case 2: + key, value := bytes.TrimSpace(parts[0]), bytes.TrimSpace(parts[1]) + if len(key) > 0 { + settings[string(key)] = string(value) + } else { + return errBadSetting + } + case 1: + return errBadSetting + } + } + + if err := scanner.Err(); err != nil { + return err + } + t.PhpModules = settings + return nil +} + + func parseAnalyticEvents(test *Test, content []byte) error { test.analyticEvents = content return nil diff --git a/src/newrelic/integration/test.go b/src/newrelic/integration/test.go index 4ce126a33..f5dfd9b8f 100644 --- a/src/newrelic/integration/test.go +++ b/src/newrelic/integration/test.go @@ -51,10 +51,11 @@ type Test struct { // Raw parsed test information used to construct the Tx. // The settings and env do not include global env and // global settings. - rawSkipIf []byte - Env map[string]string - Settings map[string]string - headers http.Header + rawSkipIf []byte + Env map[string]string + Settings map[string]string + PhpModules map[string]string + headers http.Header // When non-empty describes why failed should be true after the test // is run. This field may be set in the test definition to indicate @@ -195,6 +196,8 @@ func (t *Test) MakeRun(ctx *Context) (Tx, error) { } } + settings = merge(settings, t.PhpModules) + if t.IsC() { return CTx(ScriptFile(t.Path), env, settings, headers, ctx) } diff --git a/tests/integration/jit/function/skipif.inc b/tests/integration/jit/function/skipif.inc new file mode 100644 index 000000000..ac88b510a --- /dev/null +++ b/tests/integration/jit/function/skipif.inc @@ -0,0 +1,15 @@ +functionName(); + } +} + +for($i = 0; $i < 5; ++$i){ + new LittleClass; +} +echo "OK\n"; diff --git a/tests/integration/jit/function/test_span_events_are_created_from_segments.php b/tests/integration/jit/function/test_span_events_are_created_from_segments.php new file mode 100644 index 000000000..b543794bd --- /dev/null +++ b/tests/integration/jit/function/test_span_events_are_created_from_segments.php @@ -0,0 +1,118 @@ + 'FakeDB', +)); diff --git a/tests/integration/jit/function/test_span_events_are_created_upon_caught_error.php b/tests/integration/jit/function/test_span_events_are_created_upon_caught_error.php new file mode 100644 index 000000000..bca70be7e --- /dev/null +++ b/tests/integration/jit/function/test_span_events_are_created_upon_caught_error.php @@ -0,0 +1,194 @@ + 'FakeDB', + ) +); +a(); + +echo 'this should never be printed'; diff --git a/tests/integration/jit/function/test_span_events_are_created_upon_exit.php b/tests/integration/jit/function/test_span_events_are_created_upon_exit.php new file mode 100644 index 000000000..3274bf154 --- /dev/null +++ b/tests/integration/jit/function/test_span_events_are_created_upon_exit.php @@ -0,0 +1,123 @@ + 'FakeDB', + ) +); +a(); diff --git a/tests/integration/jit/function/test_span_events_are_created_upon_uncaught_error.php b/tests/integration/jit/function/test_span_events_are_created_upon_uncaught_error.php new file mode 100644 index 000000000..c3f064b6d --- /dev/null +++ b/tests/integration/jit/function/test_span_events_are_created_upon_uncaught_error.php @@ -0,0 +1,133 @@ + 'FakeDB', + ) +); +a(); + +echo 'this should never be printed'; diff --git a/tests/integration/jit/function/test_span_events_are_created_upon_uncaught_handled_exception.php b/tests/integration/jit/function/test_span_events_are_created_upon_uncaught_handled_exception.php new file mode 100644 index 000000000..2381e2045 --- /dev/null +++ b/tests/integration/jit/function/test_span_events_are_created_upon_uncaught_handled_exception.php @@ -0,0 +1,196 @@ + 'FakeDB', + ) +); +a(); + +echo 'this should never be printed'; diff --git a/tests/integration/jit/function/test_span_events_are_created_upon_uncaught_handled_exception_invalid_handler.php b/tests/integration/jit/function/test_span_events_are_created_upon_uncaught_handled_exception_invalid_handler.php new file mode 100644 index 000000000..fd00e971c --- /dev/null +++ b/tests/integration/jit/function/test_span_events_are_created_upon_uncaught_handled_exception_invalid_handler.php @@ -0,0 +1,210 @@ +=")) { + die("skip: PHP > 8.1 not supported\n"); +} + +require('skipif.inc'); + + +*/ + +/*INI +newrelic.distributed_tracing_enabled=1 +newrelic.transaction_tracer.threshold = 0 +newrelic.span_events_enabled=1 +newrelic.cross_application_tracer.enabled = false +display_errors=1 +log_errors=0 +error_reporting = E_ALL +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_update_protection=0 +opcache.jit_buffer_size=32M +opcache.jit=function +*/ + + +/*PHPMODULES +zend_extension=opcache.so +*/ + +/*EXPECT_ERROR_EVENTS +[ + "?? agent run id", + { + "reservoir_size": "??", + "events_seen": 1 + }, + [ + [ + { + "type": "TransactionError", + "timestamp": "??", + "error.class": "RuntimeException", + "error.message": "Uncaught exception 'RuntimeException' with message 'oops' in __FILE__:??", + "transactionName": "OtherTransaction\/php__FILE__", + "duration": "??", + "databaseDuration": "??", + "databaseCallCount": "??", + "nr.transactionGuid": "??", + "guid": "??", + "sampled": true, + "priority": "??", + "traceId": "??", + "spanId": "??" + }, + {}, + {} + ] + ] +] +*/ + + +/*EXPECT_SPAN_EVENTS +[ + "?? agent run id", + { + "reservoir_size": 10000, + "events_seen": 4 + }, + [ + [ + { + "traceId": "??", + "duration": "??", + "transactionId": "??", + "name": "OtherTransaction\/php__FILE__", + "guid": "??", + "type": "Span", + "category": "generic", + "priority": "??", + "sampled": true, + "nr.entryPoint": true, + "timestamp": "??", + "transaction.name": "OtherTransaction\/php__FILE__" + }, + {}, + {} + ], + [ + { + "type": "Span", + "traceId": "??", + "transactionId": "??", + "sampled": true, + "priority": "??", + "name": "Datastore\/statement\/FakeDB\/other\/other", + "guid": "??", + "timestamp": "??", + "duration": "??", + "category": "datastore", + "parentId": "??", + "span.kind": "client", + "component": "FakeDB" + }, + {}, + { + "db.instance": "unknown", + "peer.hostname": "unknown", + "peer.address": "unknown:unknown" + } + ], + [ + { + "type": "Span", + "traceId": "??", + "transactionId": "??", + "sampled": true, + "priority": "??", + "name": "Custom\/a", + "guid": "??", + "timestamp": "??", + "duration": "??", + "category": "generic", + "parentId": "??" + }, + {}, + { + "error.message": "Uncaught exception 'RuntimeException' with message 'oops' in __FILE__:??", + "error.class": "RuntimeException", + "code.lineno": "??", + "code.filepath": "__FILE__", + "code.function": "a" + } + ], + [ + { + "type": "Span", + "traceId": "??", + "transactionId": "??", + "sampled": true, + "priority": "??", + "name": "Custom\/{closure}", + "guid": "??", + "timestamp": "??", + "duration": "??", + "category": "generic", + "parentId": "??" + }, + {}, + { + "code.lineno": "??", + "code.filepath": "__FILE__", + "code.function": "{closure}" + } + ] + ] +] +*/ + +/*EXPECT +*/ + +set_exception_handler( + function () { + time_nanosleep(0, 100000000); + exit(0); + } +); + +function a() +{ + time_nanosleep(0, 100000000); + throw new RuntimeException('oops'); +} + +newrelic_record_datastore_segment( + function () { + time_nanosleep(0, 100000000); + }, array( + 'product' => 'FakeDB', + ) +); +a(); + +echo 'this should never be printed'; diff --git a/tests/integration/jit/function/test_span_events_are_created_upon_uncaught_unhandled_exception.php b/tests/integration/jit/function/test_span_events_are_created_upon_uncaught_unhandled_exception.php new file mode 100644 index 000000000..f619cab45 --- /dev/null +++ b/tests/integration/jit/function/test_span_events_are_created_upon_uncaught_unhandled_exception.php @@ -0,0 +1,170 @@ + 'FakeDB', + ) +); +a(); + +echo 'this should never be printed'; diff --git a/tests/integration/jit/function/test_span_events_error_collector_disabled.php b/tests/integration/jit/function/test_span_events_error_collector_disabled.php new file mode 100644 index 000000000..09a26cd9c --- /dev/null +++ b/tests/integration/jit/function/test_span_events_error_collector_disabled.php @@ -0,0 +1,130 @@ + 'FakeDB', + ) +); +a(); + +echo 'this should never be printed'; diff --git a/tests/integration/jit/function/test_span_events_exception_caught_nested.php b/tests/integration/jit/function/test_span_events_exception_caught_nested.php new file mode 100644 index 000000000..2433f1289 --- /dev/null +++ b/tests/integration/jit/function/test_span_events_exception_caught_nested.php @@ -0,0 +1,217 @@ +getMessage() . "\n"); + } +} + +function fraction($x) { + time_nanosleep(0, 100000000); + if (!$x) { + throw new RuntimeException('Division by zero'); + } + return 1/$x; +} + +function a() +{ + time_nanosleep(0, 100000000); + echo "Hello\n"; +}; + +a(); +b(); diff --git a/tests/integration/jit/function/test_span_events_exception_caught_nested_rethrown.php b/tests/integration/jit/function/test_span_events_exception_caught_nested_rethrown.php new file mode 100644 index 000000000..036b6a55e --- /dev/null +++ b/tests/integration/jit/function/test_span_events_exception_caught_nested_rethrown.php @@ -0,0 +1,196 @@ +getMessage()); + } +} + +function fraction($x) { + time_nanosleep(0, 100000000); + if (!$x) { + throw new RuntimeException('Division by zero'); + } + return 1/$x; +} + +b(); diff --git a/tests/integration/jit/function/test_span_events_exception_caught_notice_error.php b/tests/integration/jit/function/test_span_events_exception_caught_notice_error.php new file mode 100644 index 000000000..202479b19 --- /dev/null +++ b/tests/integration/jit/function/test_span_events_exception_caught_notice_error.php @@ -0,0 +1,169 @@ +getMessage() . "\n"); + } + newrelic_notice_error(new Exception('Notice me')); +} + +function a() +{ + time_nanosleep(0, 100000000); + echo "Hello\n"; +}; + +a(); +fraction(); diff --git a/tests/integration/jit/function/test_span_events_exception_caught_notice_error_nested.php b/tests/integration/jit/function/test_span_events_exception_caught_notice_error_nested.php new file mode 100644 index 000000000..f182090d3 --- /dev/null +++ b/tests/integration/jit/function/test_span_events_exception_caught_notice_error_nested.php @@ -0,0 +1,245 @@ +getMessage() . "\n"); + } + newrelic_notice_error(new Exception('Notice me')); +} + +function fraction($x) { + time_nanosleep(0, 100000000); + if (!$x) { + throw new RuntimeException('Division by zero'); + } + return 1/$x; +} + +function a() +{ + time_nanosleep(0, 100000000); + echo "Hello\n"; +}; + +a(); +b(); diff --git a/tests/integration/jit/function/test_span_events_exception_caught_same_span.php b/tests/integration/jit/function/test_span_events_exception_caught_same_span.php new file mode 100644 index 000000000..943b908ba --- /dev/null +++ b/tests/integration/jit/function/test_span_events_exception_caught_same_span.php @@ -0,0 +1,139 @@ +getMessage() . "\n"); + } +} + +function a() +{ + time_nanosleep(0, 100000000); + echo "Hello\n"; +}; + +a(); +fraction(); diff --git a/tests/integration/jit/function/test_span_events_exception_uncaught_nested.php b/tests/integration/jit/function/test_span_events_exception_uncaught_nested.php new file mode 100644 index 000000000..d3c317994 --- /dev/null +++ b/tests/integration/jit/function/test_span_events_exception_uncaught_nested.php @@ -0,0 +1,224 @@ + 'FakeDB', + ) +); +a(); + +echo 'this should never be printed'; diff --git a/tests/integration/jit/function/test_span_events_exist_when_no_segments.php b/tests/integration/jit/function/test_span_events_exist_when_no_segments.php new file mode 100644 index 000000000..ce7cb296f --- /dev/null +++ b/tests/integration/jit/function/test_span_events_exist_when_no_segments.php @@ -0,0 +1,65 @@ +functionName(); + } +} + +for($i = 0; $i < 5; ++$i){ + new LittleClass; +} +echo "OK\n"; diff --git a/tests/integration/jit/tracing/test_span_events_are_created_from_segments.php b/tests/integration/jit/tracing/test_span_events_are_created_from_segments.php new file mode 100644 index 000000000..cc2a52753 --- /dev/null +++ b/tests/integration/jit/tracing/test_span_events_are_created_from_segments.php @@ -0,0 +1,118 @@ + 'FakeDB', +)); diff --git a/tests/integration/jit/tracing/test_span_events_are_created_upon_caught_error.php b/tests/integration/jit/tracing/test_span_events_are_created_upon_caught_error.php new file mode 100644 index 000000000..ebde4a26b --- /dev/null +++ b/tests/integration/jit/tracing/test_span_events_are_created_upon_caught_error.php @@ -0,0 +1,193 @@ + 'FakeDB', + ) +); +a(); + +echo 'this should never be printed'; diff --git a/tests/integration/jit/tracing/test_span_events_are_created_upon_exit.php b/tests/integration/jit/tracing/test_span_events_are_created_upon_exit.php new file mode 100644 index 000000000..176730a7f --- /dev/null +++ b/tests/integration/jit/tracing/test_span_events_are_created_upon_exit.php @@ -0,0 +1,123 @@ + 'FakeDB', + ) +); +a(); diff --git a/tests/integration/jit/tracing/test_span_events_are_created_upon_uncaught_error.php b/tests/integration/jit/tracing/test_span_events_are_created_upon_uncaught_error.php new file mode 100644 index 000000000..ee1a578ae --- /dev/null +++ b/tests/integration/jit/tracing/test_span_events_are_created_upon_uncaught_error.php @@ -0,0 +1,133 @@ + 'FakeDB', + ) +); +a(); + +echo 'this should never be printed'; diff --git a/tests/integration/jit/tracing/test_span_events_are_created_upon_uncaught_handled_exception.php b/tests/integration/jit/tracing/test_span_events_are_created_upon_uncaught_handled_exception.php new file mode 100644 index 000000000..395b1231f --- /dev/null +++ b/tests/integration/jit/tracing/test_span_events_are_created_upon_uncaught_handled_exception.php @@ -0,0 +1,193 @@ + 'FakeDB', + ) +); +a(); + +echo 'this should never be printed'; diff --git a/tests/integration/jit/tracing/test_span_events_are_created_upon_uncaught_handled_exception_invalid_handler.php b/tests/integration/jit/tracing/test_span_events_are_created_upon_uncaught_handled_exception_invalid_handler.php new file mode 100644 index 000000000..bd99855b8 --- /dev/null +++ b/tests/integration/jit/tracing/test_span_events_are_created_upon_uncaught_handled_exception_invalid_handler.php @@ -0,0 +1,205 @@ +=")) { + die("skip: PHP > 8.1 not supported\n"); +} + +require('skipif.inc'); + + +*/ + +/*INI +newrelic.distributed_tracing_enabled=1 +newrelic.transaction_tracer.threshold = 0 +newrelic.span_events_enabled=1 +newrelic.cross_application_tracer.enabled = false +error_reporting = E_ALL +opcache.enable=1 +opcache.enable_cli=1 +opcache.file_update_protection=0 +opcache.jit_buffer_size=32M +opcache.jit=tracing +*/ + +/*PHPMODULES +zend_extension=opcache.so +*/ + +/*EXPECT_ERROR_EVENTS +[ + "?? agent run id", + { + "reservoir_size": "??", + "events_seen": 1 + }, + [ + [ + { + "type": "TransactionError", + "timestamp": "??", + "error.class": "RuntimeException", + "error.message": "Uncaught exception 'RuntimeException' with message 'oops' in __FILE__:??", + "transactionName": "OtherTransaction\/php__FILE__", + "duration": "??", + "databaseDuration": "??", + "databaseCallCount": "??", + "nr.transactionGuid": "??", + "guid": "??", + "sampled": true, + "priority": "??", + "traceId": "??", + "spanId": "??" + }, + {}, + {} + ] + ] +] +*/ + +/*EXPECT_SPAN_EVENTS +[ + "?? agent run id", + { + "reservoir_size": 10000, + "events_seen": 4 + }, + [ + [ + { + "traceId": "??", + "duration": "??", + "transactionId": "??", + "name": "OtherTransaction\/php__FILE__", + "guid": "??", + "type": "Span", + "category": "generic", + "priority": "??", + "sampled": true, + "nr.entryPoint": true, + "timestamp": "??", + "transaction.name": "OtherTransaction\/php__FILE__" + }, + {}, + {} + ], + [ + { + "type": "Span", + "traceId": "??", + "transactionId": "??", + "sampled": true, + "priority": "??", + "name": "Datastore\/statement\/FakeDB\/other\/other", + "guid": "??", + "timestamp": "??", + "duration": "??", + "category": "datastore", + "parentId": "??", + "span.kind": "client", + "component": "FakeDB" + }, + {}, + { + "db.instance": "unknown", + "peer.hostname": "unknown", + "peer.address": "unknown:unknown" + } + ], + [ + { + "type": "Span", + "traceId": "??", + "transactionId": "??", + "sampled": true, + "priority": "??", + "name": "Custom\/a", + "guid": "??", + "timestamp": "??", + "duration": "??", + "category": "generic", + "parentId": "??" + }, + {}, + { + "error.message": "Uncaught exception 'RuntimeException' with message 'oops' in __FILE__:??", + "error.class": "RuntimeException", + "code.lineno": "??", + "code.filepath": "__FILE__", + "code.function": "a" + } + ], + [ + { + "type": "Span", + "traceId": "??", + "transactionId": "??", + "sampled": true, + "priority": "??", + "name": "Custom\/{closure}", + "guid": "??", + "timestamp": "??", + "duration": "??", + "category": "generic", + "parentId": "??" + }, + {}, + { + "code.lineno": "??", + "code.filepath": "__FILE__", + "code.function": "{closure}" + } + ] + ] +] +*/ + +/*EXPECT +*/ + +set_exception_handler( + function () { + time_nanosleep(0, 100000000); + exit(0); + } +); + +function a() +{ + time_nanosleep(0, 100000000); + throw new RuntimeException('oops'); +} + +newrelic_record_datastore_segment( + function () { + time_nanosleep(0, 100000000); + }, array( + 'product' => 'FakeDB', + ) +); +a(); + +echo 'this should never be printed'; diff --git a/tests/integration/jit/tracing/test_span_events_are_created_upon_uncaught_unhandled_exception.php b/tests/integration/jit/tracing/test_span_events_are_created_upon_uncaught_unhandled_exception.php new file mode 100644 index 000000000..54269f072 --- /dev/null +++ b/tests/integration/jit/tracing/test_span_events_are_created_upon_uncaught_unhandled_exception.php @@ -0,0 +1,170 @@ + 'FakeDB', + ) +); +a(); + +echo 'this should never be printed'; diff --git a/tests/integration/jit/tracing/test_span_events_error_collector_disabled.php b/tests/integration/jit/tracing/test_span_events_error_collector_disabled.php new file mode 100644 index 000000000..0e6fb6db1 --- /dev/null +++ b/tests/integration/jit/tracing/test_span_events_error_collector_disabled.php @@ -0,0 +1,131 @@ + 'FakeDB', + ) +); +a(); + +echo 'this should never be printed'; diff --git a/tests/integration/jit/tracing/test_span_events_exception_caught_nested.php b/tests/integration/jit/tracing/test_span_events_exception_caught_nested.php new file mode 100644 index 000000000..4ff854cf2 --- /dev/null +++ b/tests/integration/jit/tracing/test_span_events_exception_caught_nested.php @@ -0,0 +1,217 @@ +getMessage() . "\n"); + } +} + +function fraction($x) { + time_nanosleep(0, 100000000); + if (!$x) { + throw new RuntimeException('Division by zero'); + } + return 1/$x; +} + +function a() +{ + time_nanosleep(0, 100000000); + echo "Hello\n"; +}; + +a(); +b(); diff --git a/tests/integration/jit/tracing/test_span_events_exception_caught_nested_rethrown.php b/tests/integration/jit/tracing/test_span_events_exception_caught_nested_rethrown.php new file mode 100644 index 000000000..1ec6df51b --- /dev/null +++ b/tests/integration/jit/tracing/test_span_events_exception_caught_nested_rethrown.php @@ -0,0 +1,196 @@ +getMessage()); + } +} + +function fraction($x) { + time_nanosleep(0, 100000000); + if (!$x) { + throw new RuntimeException('Division by zero'); + } + return 1/$x; +} + +b(); diff --git a/tests/integration/jit/tracing/test_span_events_exception_caught_notice_error.php b/tests/integration/jit/tracing/test_span_events_exception_caught_notice_error.php new file mode 100644 index 000000000..5b42d5280 --- /dev/null +++ b/tests/integration/jit/tracing/test_span_events_exception_caught_notice_error.php @@ -0,0 +1,169 @@ +getMessage() . "\n"); + } + newrelic_notice_error(new Exception('Notice me')); +} + +function a() +{ + time_nanosleep(0, 100000000); + echo "Hello\n"; +}; + +a(); +fraction(); diff --git a/tests/integration/jit/tracing/test_span_events_exception_caught_notice_error_nested.php b/tests/integration/jit/tracing/test_span_events_exception_caught_notice_error_nested.php new file mode 100644 index 000000000..aaeb5a278 --- /dev/null +++ b/tests/integration/jit/tracing/test_span_events_exception_caught_notice_error_nested.php @@ -0,0 +1,246 @@ +getMessage() . "\n"); + } + newrelic_notice_error(new Exception('Notice me')); +} + +function fraction($x) { + time_nanosleep(0, 100000000); + if (!$x) { + throw new RuntimeException('Division by zero'); + } + return 1/$x; +} + +function a() +{ + time_nanosleep(0, 100000000); + echo "Hello\n"; +}; + +a(); +b(); diff --git a/tests/integration/jit/tracing/test_span_events_exception_caught_same_span.php b/tests/integration/jit/tracing/test_span_events_exception_caught_same_span.php new file mode 100644 index 000000000..d92e22d16 --- /dev/null +++ b/tests/integration/jit/tracing/test_span_events_exception_caught_same_span.php @@ -0,0 +1,139 @@ +getMessage() . "\n"); + } +} + +function a() +{ + time_nanosleep(0, 100000000); + echo "Hello\n"; +}; + +a(); +fraction(); diff --git a/tests/integration/jit/tracing/test_span_events_exception_uncaught_nested.php b/tests/integration/jit/tracing/test_span_events_exception_uncaught_nested.php new file mode 100644 index 000000000..d352b174e --- /dev/null +++ b/tests/integration/jit/tracing/test_span_events_exception_uncaught_nested.php @@ -0,0 +1,225 @@ + 'FakeDB', + ) +); +a(); + +echo 'this should never be printed'; diff --git a/tests/integration/jit/tracing/test_span_events_exist_when_no_segments.php b/tests/integration/jit/tracing/test_span_events_exist_when_no_segments.php new file mode 100644 index 000000000..a3c7c6a11 --- /dev/null +++ b/tests/integration/jit/tracing/test_span_events_exist_when_no_segments.php @@ -0,0 +1,65 @@ + Date: Mon, 23 Oct 2023 15:15:45 -0400 Subject: [PATCH 38/56] tests: fix user exception and error handler implementation (#748) * fix user exception handler implementation User exception handler, set with set_exception_handler is a callable that needs to accept one parameter, which will be the Throwable object that was thrown. If that callable is not defined according to these rules, PHP's behavior, and therefore agent's behavior, is undefined. E.g. in PHPs 8.0, 8.1 and 8.2, when opcache is disabled Zend Engine gives the agent a zval which is an object of Throwable type. The same thing happens in PHPs 8.0 and 8.1 when opcache is enabled. However, when opcache is enabled in PHP 8.2, Zend Engine no longer gives the agent a zval which is an object of Throwable type. * fix user error handler implementation User error handler, set with `set_error_handler` is a callable that needs to accept at least two parameters: int errno and string errstr set to error level and error message respectively. If that callable is not defined according to these rules, PHP's behavior, and therefore agent's behavior, is undefined. * split integration test An agent hooking into Zend Engine using zend_execute (pre-oapi) hook behaves differently than the agent hooking into Zend Engine using observer API (oapi). Namely pre-oapi agent does not generate error events when an exception is handled with user exception handler wheras oapi agent generates error events in such case. Therefore the test testing agent's behavior when an exception is handled by user exception handler must be split depending on what method is used to hook into Zend Engine: zend_execute hook is used for PHPs < 8.0 and observer API is used for PHPs >= 8.0. --- ...xception_restored_no_exception_handler.php | 2 +- ...n_events_are_created_upon_caught_error.php | 2 +- ...eated_upon_uncaught_handled_exception.php} | 55 +++++++++++++++++-- ..._upon_uncaught_handled_exception.php7.php} | 9 ++- ...ted_upon_uncaught_unhandled_exception.php} | 0 ...pon_uncaught_unhandled_exception.php5.php} | 0 6 files changed, 57 insertions(+), 11 deletions(-) rename tests/integration/span_events/{test_span_events_are_created_upon_caught_exception.php5.php => test_span_events_are_created_upon_uncaught_handled_exception.php} (68%) rename tests/integration/span_events/{test_span_events_are_created_upon_caught_exception.php => test_span_events_are_created_upon_uncaught_handled_exception.php7.php} (94%) rename tests/integration/span_events/{test_span_events_are_created_upon_uncaught_exception.php => test_span_events_are_created_upon_uncaught_unhandled_exception.php} (100%) rename tests/integration/span_events/{test_span_events_are_created_upon_uncaught_exception.php5.php => test_span_events_are_created_upon_uncaught_unhandled_exception.php5.php} (100%) diff --git a/tests/integration/errors/test_exception_restored_no_exception_handler.php b/tests/integration/errors/test_exception_restored_no_exception_handler.php index 900a5b191..8360b189a 100644 --- a/tests/integration/errors/test_exception_restored_no_exception_handler.php +++ b/tests/integration/errors/test_exception_restored_no_exception_handler.php @@ -26,7 +26,7 @@ function alpha() { throw new Exception('Sample Exception'); } -function beta() { +function beta(Throwable $ex) { alpha(); } diff --git a/tests/integration/span_events/test_span_events_are_created_upon_caught_error.php b/tests/integration/span_events/test_span_events_are_created_upon_caught_error.php index 9b1c75cfb..91b1420be 100644 --- a/tests/integration/span_events/test_span_events_are_created_upon_caught_error.php +++ b/tests/integration/span_events/test_span_events_are_created_upon_caught_error.php @@ -127,7 +127,7 @@ */ set_error_handler( - function () { + function (int $errno, string $errst) { time_nanosleep(0, 100000000); return false; } diff --git a/tests/integration/span_events/test_span_events_are_created_upon_caught_exception.php5.php b/tests/integration/span_events/test_span_events_are_created_upon_uncaught_handled_exception.php similarity index 68% rename from tests/integration/span_events/test_span_events_are_created_upon_caught_exception.php5.php rename to tests/integration/span_events/test_span_events_are_created_upon_uncaught_handled_exception.php index ad7e3a94d..7670f8a27 100644 --- a/tests/integration/span_events/test_span_events_are_created_upon_caught_exception.php5.php +++ b/tests/integration/span_events/test_span_events_are_created_upon_uncaught_handled_exception.php @@ -6,7 +6,16 @@ /*DESCRIPTION Test that span events are correctly created from any eligible segment, even -when an exception is handled by the exception handler. +when an uncaught exception is handled by the user exception handler. The +span that generated the exception should have error attributes. Additionally +error events should be created. +*/ + +/*SKIPIF +=")) { + die("skip: test for non-oapi agent only\n"); +} */ /*INI @@ -94,7 +97,7 @@ { "error.message": "Uncaught exception 'RuntimeException' with message 'oops' in __FILE__:??", "error.class": "RuntimeException", - "code.lineno": 137, + "code.lineno": "??", "code.filepath": "__FILE__", "code.function": "a" } @@ -115,7 +118,7 @@ }, {}, { - "code.lineno": 131, + "code.lineno": "??", "code.filepath": "__FILE__", "code.function": "{closure}" } @@ -128,7 +131,7 @@ */ set_exception_handler( - function () { + function (Throwable $ex) { time_nanosleep(0, 100000000); exit(0); } diff --git a/tests/integration/span_events/test_span_events_are_created_upon_uncaught_exception.php b/tests/integration/span_events/test_span_events_are_created_upon_uncaught_unhandled_exception.php similarity index 100% rename from tests/integration/span_events/test_span_events_are_created_upon_uncaught_exception.php rename to tests/integration/span_events/test_span_events_are_created_upon_uncaught_unhandled_exception.php diff --git a/tests/integration/span_events/test_span_events_are_created_upon_uncaught_exception.php5.php b/tests/integration/span_events/test_span_events_are_created_upon_uncaught_unhandled_exception.php5.php similarity index 100% rename from tests/integration/span_events/test_span_events_are_created_upon_uncaught_exception.php5.php rename to tests/integration/span_events/test_span_events_are_created_upon_uncaught_unhandled_exception.php5.php From 58662ebfcdfe658457c4938c04a0cecb7b6375b6 Mon Sep 17 00:00:00 2001 From: Amber Sistla Date: Mon, 13 Nov 2023 10:21:12 -0700 Subject: [PATCH 39/56] fix(agent): Correctly handle laravel queues (#746) When run via artisan queue:listen or artisan queue:work, Laravel queue jobs are generated as a separate background transaction for each job that is processed. These transactions are named with the class name of the job (or IlluminateQueueClosure if a closure was queued) and the connection type which is stored in the application's config/queue.php configuration file that includes connection configurations for each of the queue drivers that are included with the framework(sync--for dev env to execute immediately, database, [Amazon SQS](https://aws.amazon.com/sqs/), [Redis](https://redis.io/), and [Beanstalkd](https://beanstalkd.github.io/) drivers, and null --discards queued jobs). For example, current implementation shows a job like: `OtherTransaction\/Custom\/App\\Http\\Controllers\\JobOne (database)` `OtherTransaction` shows it is a background job `App\\Http\\Controllers\\JobOne` is the class name of the job `database` is the connection type This PR: * Removed WorkCommand::handle wrapper * removed worker::process wrapper * removed all non-supported Laravel 4.0 instrumentation. * updated link * removed references to "fire" * removed unneeded job parsing code since the laravel Job class already provides the info * Added handling to detect when a txn is ended within a wrapped function (that could result in segfaults when transactions were restarted (either directly through newrelic_start_transaction() or indirectly through newrelic_set_appname(), or with laravel queue instrumentation). 1)With laravel updates, there are better functions to instrument queue jobs that also result in fewer extraneous segments. For OAPI/PHP 8+ and moving forward, we can use: * Illuminate\\Queue\\Worker::raiseBeforeJobEvent(string $connectionName, Job $job):void * Illuminate\\Queue\\SyncQueue::raiseBeforeJobEvent(Job $job):void * Illuminate\\Queue\\Worker::raiseAfterJobEvent(string $connectionName, Job $job):void * Illuminate\\Queue\\SyncQueue::raiseAfterJobEvent(Job $job):void 2)Using updated laravel functionality, we can additionally simplify the txn name decoding quite a bit and add additional information. Current naming is: "job_name (connection_type)" New naming is: "job_name (connection_type:job_queue)" 3) git diff got confused. For the purpose of reviews, despite git saying anything else, in fw_laravel_queue.c all the code above line 159 and the #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA Block is the same code/functionality as currently exists. Multiverse PR that tests both sync and async (database) queue workers is forthcoming. --- agent/fw_laravel_queue.c | 693 ++++++++++++++++++++++----------------- agent/php_execute.c | 111 +++++-- 2 files changed, 484 insertions(+), 320 deletions(-) diff --git a/agent/fw_laravel_queue.c b/agent/fw_laravel_queue.c index 94792429d..e1618f0ab 100644 --- a/agent/fw_laravel_queue.c +++ b/agent/fw_laravel_queue.c @@ -17,11 +17,10 @@ #include "fw_laravel_queue.h" /* - * This file includes functions for instrumenting Laravel's Queue component. As - * with our primary Laravel instrumentation, all 4.x, 5.x and 6.x versions are - * supported. + * This file includes functions for instrumenting Laravel's Queue component. + * Supports the same versions as our our primary Laravel instrumentation. * - * Userland docs for this can be found at: http://laravel.com/docs/5.0/queues + * Userland docs for this can be found at: https://laravel.com/docs/10.x/queues * (use the dropdown to change to other versions) * * As with most of our framework files, the entry point is in the last @@ -40,7 +39,318 @@ static int nr_laravel_queue_is_sync_job(zval* job TSRMLS_DC) { return nr_php_object_instanceof_class( job, "Illuminate\\Queue\\Jobs\\SyncJob" TSRMLS_CC); } +/* + * Iterator function and supporting struct to walk an nrobj_t hash to extract + * CATMQ headers in a case insensitive manner. + */ +typedef struct { + const char* id; + const char* synthetics; + const char* transaction; + const char* dt_payload; + const char* traceparent; + const char* tracestate; +} nr_laravel_queue_headers_t; + +static nr_status_t nr_laravel_queue_iterate_headers( + const char* key, + const nrobj_t* val, + nr_laravel_queue_headers_t* headers) { + char* key_lc; + + if (NULL == headers) { + return NR_SUCCESS; + } + + key_lc = nr_string_to_lowercase(key); + if (NULL == key_lc) { + return NR_SUCCESS; + } + + if (0 == nr_strcmp(key_lc, X_NEWRELIC_ID_MQ_LOWERCASE)) { + headers->id = nro_get_string(val, NULL); + } else if (0 == nr_strcmp(key_lc, X_NEWRELIC_SYNTHETICS_MQ_LOWERCASE)) { + headers->synthetics = nro_get_string(val, NULL); + } else if (0 == nr_strcmp(key_lc, X_NEWRELIC_TRANSACTION_MQ_LOWERCASE)) { + headers->transaction = nro_get_string(val, NULL); + } else if (0 == nr_strcmp(key_lc, X_NEWRELIC_DT_PAYLOAD_MQ_LOWERCASE)) { + headers->dt_payload = nro_get_string(val, NULL); + } else if (0 == nr_strcmp(key_lc, X_NEWRELIC_W3C_TRACEPARENT_MQ_LOWERCASE)) { + headers->traceparent = nro_get_string(val, NULL); + } else if (0 == nr_strcmp(key_lc, X_NEWRELIC_W3C_TRACESTATE_MQ_LOWERCASE)) { + headers->tracestate = nro_get_string(val, NULL); + } + + nr_free(key_lc); + return NR_SUCCESS; +} + +/* + * Purpose : Parse a Laravel 4.1+ job object for CATMQ metadata and update the + * transaction type accordingly. + * + * Params : 1. The job object. + */ +static void nr_laravel_queue_set_cat_txn(zval* job TSRMLS_DC) { + zval* json = NULL; + nrobj_t* payload = NULL; + nr_laravel_queue_headers_t headers = {NULL, NULL, NULL, NULL, NULL, NULL}; + + /* + * We're not interested in SyncJob instances, since they don't run in a + * separate queue worker and hence don't need to be linked via CATMQ. + */ + if (nr_laravel_queue_is_sync_job(job TSRMLS_CC)) { + return; + } + + /* + * Let's see if we can access the payload. + */ + if (!nr_php_object_has_method(job, "getRawBody" TSRMLS_CC)) { + return; + } + + json = nr_php_call(job, "getRawBody"); + if (!nr_php_is_zval_non_empty_string(json)) { + goto end; + } + + /* + * We've got it. Let's decode the payload and extract our CATMQ properties. + * + * Our nro code doesn't handle NULLs particularly gracefully, but it doesn't + * matter here, as we're not turning this back into JSON and aren't + * interested in the properties that could include NULLs. + */ + payload = nro_create_from_json(Z_STRVAL_P(json)); + nro_iteratehash(payload, (nrhashiter_t)nr_laravel_queue_iterate_headers, + (void*)&headers); + + if (headers.id && headers.transaction) { + nr_header_set_cat_txn(NRPRG(txn), headers.id, headers.transaction); + } + + if (headers.synthetics) { + nr_header_set_synthetics_txn(NRPRG(txn), headers.synthetics); + } + + if (headers.dt_payload || headers.traceparent) { + + nr_hashmap_t* header_map = nr_header_create_distributed_trace_map( + headers.dt_payload, headers.traceparent, headers.tracestate); + + nr_php_api_accept_distributed_trace_payload_httpsafe(NRPRG(txn), header_map, + "Other"); + nr_hashmap_destroy(&header_map); + } + +end: + nr_php_zval_free(&json); + nro_delete(payload); +} + +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA +/* + * Purpose : Retrieve the txn name for a job which consists of: + * 1. The job name + * 2. The job connection type + * 3. The job queue + * + * Formatting is "job_name (connection_type:job_queue)" + * + * Params : 1. The job object. + * + * Returns : The txn name for the job, which is owned by the caller, or NULL if + * it cannot be found. + */ +static char* nr_laravel_queue_job_txn_name(zval* job TSRMLS_DC) { + char* name = NULL; + char* resolve_name = NULL; + char* connection_name = NULL; + char* queue_name = NULL; + zval* resolve_name_zval = NULL; + zval* connection_name_zval = NULL; + zval* queue_name_zval = NULL; + + /* + * Laravel 7+ includes following methods for Job + * https://laravel.com/api/7.x/Illuminate/Queue/Jobs/Job.html + * Job::getName(): this is not needed as sometimes it will provide a + * CallQueuedHandler job with the actual job wrapped inside + * + * Job::resolveName(): inside "resolveName" method, there are actually three + * methods being called (resolve, getName and payload) so we will use it + * instead of getName. This provides us the wrapped name when jobname is + * Illuminate\Queue\CallQueuedHandler@call Job::getConnectionName() + * + * Job::getQueue() + * + */ + + connection_name_zval = nr_php_call(job, "getConnectionName"); + + if (nr_php_is_zval_non_empty_string(connection_name_zval)) { + connection_name = nr_strndup(Z_STRVAL_P(connection_name_zval), + Z_STRLEN_P(connection_name_zval)); + } else { + connection_name = nr_strdup("unknown"); + } + + nr_php_zval_free(&connection_name_zval); + + queue_name_zval = nr_php_call(job, "getQueue"); + + if (nr_php_is_zval_non_empty_string(queue_name_zval)) { + queue_name + = nr_strndup(Z_STRVAL_P(queue_name_zval), Z_STRLEN_P(queue_name_zval)); + } else { + queue_name = nr_strdup("default"); + } + + nr_php_zval_free(&queue_name_zval); + + resolve_name_zval = nr_php_call(job, "resolveName"); + + if (nr_php_is_zval_non_empty_string(resolve_name_zval)) { + resolve_name = nr_strndup(Z_STRVAL_P(resolve_name_zval), + Z_STRLEN_P(resolve_name_zval)); + } else { + resolve_name = nr_strdup("unknown"); + } + + nr_php_zval_free(&resolve_name_zval); + + name = nr_formatf("%s (%s:%s)", resolve_name, connection_name, queue_name); + + return name; +} + +/* + * Handle: + * Illuminate\\Queue\\SyncQueue::raiseBeforeJobEvent(Job $job):void + */ +NR_PHP_WRAPPER(nr_laravel_queue_syncqueue_raiseBeforeJobEvent_before) { + zval* job = NULL; + + NR_UNUSED_SPECIALFN; + (void)wraprec; + + NR_PHP_WRAPPER_REQUIRE_FRAMEWORK(NR_FW_LARAVEL); + + /* + * End the current txn in preparation for the Job txn. + */ + nr_php_txn_end(1, 0); + + /* + * Laravel 7+ passes Job as the first parameter. + */ + char* txn_name = NULL; + + job = nr_php_arg_get(1, NR_EXECUTE_ORIG_ARGS); + + txn_name = nr_laravel_queue_job_txn_name(job); + + /* + * Begin the transaction we'll actually record. + */ + + if (NR_SUCCESS == nr_php_txn_begin(NULL, NULL)) { + nr_txn_set_as_background_job(NRPRG(txn), "Laravel job"); + + if (NULL == txn_name) { + txn_name = nr_strdup("unknown"); + } + + nr_laravel_queue_set_cat_txn(job TSRMLS_CC); + + nr_txn_set_path("Laravel", NRPRG(txn), txn_name, NR_PATH_TYPE_CUSTOM, + NR_OK_TO_OVERWRITE); + } + nr_php_arg_release(&job); + NR_PHP_WRAPPER_CALL; +} +NR_PHP_WRAPPER_END + +/* + * Handle: + * Illuminate\\Queue\\Worker::raiseBeforeJobEvent(string $connectionName, Job + * $job):void + */ +NR_PHP_WRAPPER(nr_laravel_queue_worker_raiseBeforeJobEvent_after) { + zval* job = NULL; + + NR_UNUSED_SPECIALFN; + (void)wraprec; + + NR_PHP_WRAPPER_REQUIRE_FRAMEWORK(NR_FW_LARAVEL); + + /* + * End the current txn to prepare for the Job txn. + */ + nr_php_txn_end(1, 0 TSRMLS_CC); + + /* + * Laravel 7 and later passes Job as the second parameter. + */ + char* txn_name = NULL; + + job = nr_php_arg_get(2, NR_EXECUTE_ORIG_ARGS); + txn_name = nr_laravel_queue_job_txn_name(job); + + /* + * Begin the transaction we'll actually record. + */ + + if (NR_SUCCESS == nr_php_txn_begin(NULL, NULL)) { + nr_txn_set_as_background_job(NRPRG(txn), "Laravel job"); + + if (NULL == txn_name) { + txn_name = nr_strdup("unknown"); + } + + nr_laravel_queue_set_cat_txn(job TSRMLS_CC); + + nr_txn_set_path("Laravel", NRPRG(txn), txn_name, NR_PATH_TYPE_CUSTOM, + NR_OK_TO_OVERWRITE); + } + nr_php_arg_release(&job); + NR_PHP_WRAPPER_CALL; +} +NR_PHP_WRAPPER_END + +/* + * Handle: + * Illuminate\\Queue\\Worker::raiseAfterJobEvent(string $connectionName, Job + * $job):void Illuminate\\Queue\\SyncQueue::raiseAfterJobEvent(Job $job):void + */ +NR_PHP_WRAPPER(nr_laravel_queue_worker_raiseAfterJobEvent_before) { + NR_UNUSED_SPECIALFN; + (void)wraprec; + + NR_PHP_WRAPPER_REQUIRE_FRAMEWORK(NR_FW_LARAVEL); + + /* + * If we made it here, we are assured there are no uncaught exceptions (as it + * would be noticed with the oapi exception handling before calling this + * callback so no need to check before ending the txn. + */ + + /* + * End the real transaction and then start a new transaction so our + * instrumentation continues to fire, knowing that we'll ignore that + * transaction either when Worker::process() is called again or when + * WorkCommand::handle() exits. + */ + nr_php_txn_end(0, 0 TSRMLS_CC); + nr_php_txn_begin(NULL, NULL TSRMLS_CC); +} +NR_PHP_WRAPPER_END + +#else /* * Purpose : Extract the actual job name from a job that used CallQueuedHandler * to enqueue a serialised object. @@ -233,237 +543,8 @@ static char* nr_laravel_queue_job_name(zval* job TSRMLS_DC) { return name; } -/* - * Iterator function and supporting struct to walk an nrobj_t hash to extract - * CATMQ headers in a case insensitive manner. - */ -typedef struct { - const char* id; - const char* synthetics; - const char* transaction; - const char* dt_payload; - const char* traceparent; - const char* tracestate; -} nr_laravel_queue_headers_t; - -static nr_status_t nr_laravel_queue_iterate_headers( - const char* key, - const nrobj_t* val, - nr_laravel_queue_headers_t* headers) { - char* key_lc; - - if (NULL == headers) { - return NR_SUCCESS; - } - - key_lc = nr_string_to_lowercase(key); - if (NULL == key_lc) { - return NR_SUCCESS; - } - - if (0 == nr_strcmp(key_lc, X_NEWRELIC_ID_MQ_LOWERCASE)) { - headers->id = nro_get_string(val, NULL); - } else if (0 == nr_strcmp(key_lc, X_NEWRELIC_SYNTHETICS_MQ_LOWERCASE)) { - headers->synthetics = nro_get_string(val, NULL); - } else if (0 == nr_strcmp(key_lc, X_NEWRELIC_TRANSACTION_MQ_LOWERCASE)) { - headers->transaction = nro_get_string(val, NULL); - } else if (0 == nr_strcmp(key_lc, X_NEWRELIC_DT_PAYLOAD_MQ_LOWERCASE)) { - headers->dt_payload = nro_get_string(val, NULL); - } else if (0 == nr_strcmp(key_lc, X_NEWRELIC_W3C_TRACEPARENT_MQ_LOWERCASE)) { - headers->traceparent = nro_get_string(val, NULL); - } else if (0 == nr_strcmp(key_lc, X_NEWRELIC_W3C_TRACESTATE_MQ_LOWERCASE)) { - headers->tracestate = nro_get_string(val, NULL); - } - - nr_free(key_lc); - return NR_SUCCESS; -} - -/* - * Purpose : Parse a Laravel 4.1+ job object for CATMQ metadata and update the - * transaction type accordingly. - * - * Params : 1. The job object. - */ -static void nr_laravel_queue_set_cat_txn(zval* job TSRMLS_DC) { - zval* json = NULL; - nrobj_t* payload = NULL; - nr_laravel_queue_headers_t headers = {NULL, NULL, NULL, NULL}; - - /* - * We're not interested in SyncJob instances, since they don't run in a - * separate queue worker and hence don't need to be linked via CATMQ. - */ - if (nr_laravel_queue_is_sync_job(job TSRMLS_CC)) { - return; - } - - /* - * Let's see if we can access the payload. - */ - if (!nr_php_object_has_method(job, "getRawBody" TSRMLS_CC)) { - return; - } - - json = nr_php_call(job, "getRawBody"); - if (!nr_php_is_zval_non_empty_string(json)) { - goto end; - } - - /* - * We've got it. Let's decode the payload and extract our CATMQ properties. - * - * Our nro code doesn't handle NULLs particularly gracefully, but it doesn't - * matter here, as we're not turning this back into JSON and aren't - * interested in the properties that could include NULLs. - */ - payload = nro_create_from_json(Z_STRVAL_P(json)); - nro_iteratehash(payload, (nrhashiter_t)nr_laravel_queue_iterate_headers, - (void*)&headers); - - if (headers.id && headers.transaction) { - nr_header_set_cat_txn(NRPRG(txn), headers.id, headers.transaction); - } - - if (headers.synthetics) { - nr_header_set_synthetics_txn(NRPRG(txn), headers.synthetics); - } - - if (headers.dt_payload || headers.traceparent) { - char* dt_payload = nr_strdup(headers.dt_payload); - char* tracestate = nr_strdup(headers.tracestate); - char* traceparent = nr_strdup(headers.traceparent); - - nr_hashmap_t* header_map = nr_header_create_distributed_trace_map( - dt_payload, traceparent, tracestate); - - nr_php_api_accept_distributed_trace_payload_httpsafe(NRPRG(txn), header_map, - "Other"); - nr_free(dt_payload); - nr_free(tracestate); - nr_free(traceparent); - nr_hashmap_destroy(&header_map); - } - -end: - nr_php_zval_free(&json); - nro_delete(payload); -} - -/* - * Purpose : Parse the decoded payload array from a Laravel 4.0 job and set the - * transaction name accordingly. - * - * Params : 1. The payload array. - */ -static void nr_laravel_queue_name_from_payload_array( - const zval* payload TSRMLS_DC) { - const zval* job = nr_php_zend_hash_find(Z_ARRVAL_P(payload), "job"); - - /* - * If the payload contains a "job" entry, we'll - * use that for the name. Otherwise, there's no standard entry we can look - * at, so we'll just bail. - */ - if (!nr_php_is_zval_non_empty_string(job)) { - return; - } - - nr_txn_set_path("Laravel", NRPRG(txn), Z_STRVAL_P(job), NR_PATH_TYPE_CUSTOM, - NR_OK_TO_OVERWRITE); -} - -/* - * Purpose : Parse the decoded payload array from a Laravel 4.0 job and set the - * transaction type accordingly. - * - * Params : 1. The payload array. - */ -static void nr_laravel_queue_set_cat_txn_from_payload_array( - const zval* payload TSRMLS_DC) { - const zval* id = NULL; - const zval* synthetics = NULL; - const zval* transaction = NULL; - const zval* dt_payload = NULL; - const zval* traceparent = NULL; - const zval* tracestate = NULL; - - /* - * This is ugly, but actually very simple: we want to get the array values - * for the metadata keys, and if they're set, we'll call - * nr_header_set_cat_txn and (optionally) nr_header_set_synthetics_txn to set - * the transaction type and attributes. - */ - id = nr_php_zend_hash_find(Z_ARRVAL_P(payload), X_NEWRELIC_ID_MQ); - synthetics - = nr_php_zend_hash_find(Z_ARRVAL_P(payload), X_NEWRELIC_SYNTHETICS_MQ); - transaction - = nr_php_zend_hash_find(Z_ARRVAL_P(payload), X_NEWRELIC_TRANSACTION_MQ); - dt_payload - = nr_php_zend_hash_find(Z_ARRVAL_P(payload), X_NEWRELIC_DT_PAYLOAD_MQ); - traceparent = nr_php_zend_hash_find(Z_ARRVAL_P(payload), - X_NEWRELIC_W3C_TRACEPARENT_MQ); - tracestate = nr_php_zend_hash_find(Z_ARRVAL_P(payload), - X_NEWRELIC_W3C_TRACEPARENT_MQ); - - if (!nr_php_is_zval_non_empty_string(id) - || !nr_php_is_zval_non_empty_string(transaction)) { - return; - } - - nr_header_set_cat_txn(NRPRG(txn), Z_STRVAL_P(id), Z_STRVAL_P(transaction)); - - if (nr_php_is_zval_non_empty_string(synthetics)) { - nr_header_set_synthetics_txn(NRPRG(txn), Z_STRVAL_P(synthetics)); - } - - if (nr_php_is_zval_non_empty_string(dt_payload)) { - nr_hashmap_t* header_map = nr_header_create_distributed_trace_map( - Z_STRVAL_P(dt_payload), Z_STRVAL_P(traceparent), - Z_STRVAL_P(tracestate)); - - nr_php_api_accept_distributed_trace_payload_httpsafe(NRPRG(txn), header_map, - "Other"); - nr_hashmap_destroy(&header_map); - } -} - -/* - * Handle: - * Illuminate\Queue\Jobs\Job::resolveAndFire (array $payload): void - * - * Although this function exists on all versions of Laravel, we only hook this - * on Laravel 4.0, as we have better ways of getting the job name directly in - * the Worker::process() callback on other versions. - */ -NR_PHP_WRAPPER(nr_laravel_queue_job_resolveandfire) { - zval* payload = NULL; - - NR_UNUSED_SPECIALFN; - (void)wraprec; - - NR_PHP_WRAPPER_REQUIRE_FRAMEWORK(NR_FW_LARAVEL); - - payload = nr_php_arg_get(1, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); - if (!nr_php_is_zval_valid_array(payload)) { - goto end; - } - - nr_laravel_queue_name_from_payload_array(payload TSRMLS_CC); - nr_laravel_queue_set_cat_txn_from_payload_array(payload TSRMLS_CC); - -end: - NR_PHP_WRAPPER_CALL; - nr_php_arg_release(&payload); -} -NR_PHP_WRAPPER_END - /* * Handle: - * (Laravel 4.0) - * Illuminate\Queue\Worker::process (Job $job, int $delay): void - * - * (Laravel 4.1+) * Illuminate\Queue\Worker::process (string $connection, Job $job, int * $maxTries = 0, int $delay = 0): void */ @@ -488,64 +569,38 @@ NR_PHP_WRAPPER(nr_laravel_queue_worker_process) { if (NR_SUCCESS == nr_php_txn_begin(NULL, NULL TSRMLS_CC)) { nr_txn_set_as_background_job(NRPRG(txn), "Laravel job"); + /* + * Laravel passed the name of the connection + * as the first parameter. + */ + char* connection_name = NULL; + char* job_name; + char* txn_name = NULL; + connection = nr_php_arg_get(1, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); if (nr_php_is_zval_non_empty_string(connection)) { - /* - * Laravel 4.1 and later (including 5.x) pass the name of the connection - * as the first parameter. - */ - char* connection_name = NULL; - char* job_name; - char* txn_name = NULL; - - if (nr_php_is_zval_non_empty_string(connection)) { - connection_name - = nr_strndup(Z_STRVAL_P(connection), Z_STRLEN_P(connection)); - } else { - connection_name = nr_strdup("unknown"); - } - - job = nr_php_arg_get(2, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); - job_name = nr_laravel_queue_job_name(job TSRMLS_CC); - if (NULL == job_name) { - job_name = nr_strdup("unknown job"); - } - - txn_name = nr_formatf("%s (%s)", job_name, connection_name); - - nr_laravel_queue_set_cat_txn(job TSRMLS_CC); + connection_name + = nr_strndup(Z_STRVAL_P(connection), Z_STRLEN_P(connection)); + } else { + connection_name = nr_strdup("unknown"); + } - nr_txn_set_path("Laravel", NRPRG(txn), txn_name, NR_PATH_TYPE_CUSTOM, - NR_OK_TO_OVERWRITE); + job = nr_php_arg_get(2, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + job_name = nr_laravel_queue_job_name(job TSRMLS_CC); + if (NULL == job_name) { + job_name = nr_strdup("unknown job"); + } - nr_free(connection_name); - nr_free(job_name); - nr_free(txn_name); - } else { - /* - * Laravel 4.0 only provides the job to this method, and the Job class - * doesn't provide a getRawBody method. We'll hook the resolveAndFire - * method on the job argument (which is the first argument, so is - * normally the connection on newer versions), since that gets the - * payload, and then use that to name if we can. - */ - if (nr_php_is_zval_valid_object(connection)) { - const char* klass = nr_php_class_entry_name(Z_OBJCE_P(connection)); - char* method = nr_formatf("%s::resolveAndFire", klass); + txn_name = nr_formatf("%s (%s)", job_name, connection_name); - nr_php_wrap_user_function( - method, nr_strlen(method), - nr_laravel_queue_job_resolveandfire TSRMLS_CC); + nr_laravel_queue_set_cat_txn(job TSRMLS_CC); - nr_free(method); - } + nr_txn_set_path("Laravel", NRPRG(txn), txn_name, NR_PATH_TYPE_CUSTOM, + NR_OK_TO_OVERWRITE); - /* - * We'll set a fallback name just in case. - */ - nr_txn_set_path("Laravel", NRPRG(txn), "unknown job", NR_PATH_TYPE_CUSTOM, - NR_OK_TO_OVERWRITE); - } + nr_free(connection_name); + nr_free(job_name); + nr_free(txn_name); } NR_PHP_WRAPPER_CALL; @@ -598,18 +653,27 @@ NR_PHP_WRAPPER(nr_laravel_queue_worker_process) { * End the real transaction and then start a new transaction so our * instrumentation continues to fire, knowing that we'll ignore that * transaction either when Worker::process() is called again or when - * WorkCommand::fire() exits. + * WorkCommand::handle() exits. */ nr_php_txn_end(0, 0 TSRMLS_CC); nr_php_txn_begin(NULL, NULL TSRMLS_CC); } NR_PHP_WRAPPER_END +#endif + +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + +#else + /* * Handle: - * Illuminate\Queue\Console\WorkCommand::fire (): void + * This changed in Laravel 5.5+ to be: + * Illuminate\Queue\Console\WorkCommand::handle (): void + * */ -NR_PHP_WRAPPER(nr_laravel_queue_workcommand_fire) { +NR_PHP_WRAPPER(nr_laravel_queue_workcommand_handle) { NR_UNUSED_SPECIALFN; (void)wraprec; @@ -624,8 +688,8 @@ NR_PHP_WRAPPER(nr_laravel_queue_workcommand_fire) { * aren't executed if we're not actually in a transaction. * * So instead, what we'll do is to keep recording, but ensure that we ignore - * the transaction after WorkCommand::fire() has finished executing, at which - * point no more jobs can be run. + * the transaction after WorkCommand::handle() has finished executing, at + * which point no more jobs can be run. */ /* @@ -635,7 +699,7 @@ NR_PHP_WRAPPER(nr_laravel_queue_workcommand_fire) { nr_laravel_queue_worker_process TSRMLS_CC); /* - * Actually execute the command's fire() method. + * Actually execute the command's handle() method. */ NR_PHP_WRAPPER_CALL; @@ -646,6 +710,8 @@ NR_PHP_WRAPPER(nr_laravel_queue_workcommand_fire) { } NR_PHP_WRAPPER_END +#endif + /* * This supports the mapping of outbound payload headers to their * message queue variants @@ -784,17 +850,52 @@ void nr_laravel_queue_enable(TSRMLS_D) { * that we can disable the default transaction and add listeners to generate * appropriate background transactions when handling jobs. */ - nr_php_wrap_user_function( - NR_PSTR("Illuminate\\Queue\\Console\\WorkCommand::fire"), - nr_laravel_queue_workcommand_fire TSRMLS_CC); + +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA /* - * Laravel 5.5 renamed the methods on all its command classes from `fire` - * to `handle`. As a result, we also need to hook the following. + * Here's the problem: we want to record individual transactions for each job + * that is executed, but don't want to record a transaction for the actual + * queue:work command, since it spends most of its time sleeping. + * + * We use the raiseBeforeJobEvent and raiseAfterJobEvent listeners which we + * can use to name the Laravel Job and capture the true time that the job + * took. */ + + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("Illuminate\\Queue\\Worker::raiseBeforeJobEvent"), NULL, + nr_laravel_queue_worker_raiseBeforeJobEvent_after, NULL); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("Illuminate\\Queue\\Worker::raiseAfterJobEvent"), + nr_laravel_queue_worker_raiseAfterJobEvent_before, NULL, NULL); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("Illuminate\\Queue\\SyncQueue::raiseBeforeJobEvent"), + nr_laravel_queue_syncqueue_raiseBeforeJobEvent_before, NULL, NULL); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("Illuminate\\Queue\\SyncQueue::raiseAfterJobEvent"), + nr_laravel_queue_worker_raiseAfterJobEvent_before, NULL, NULL); + +#else + + /* + * Here's the problem: we want to record individual transactions for each job + * that is executed, but don't want to record a transaction for the actual + * queue:work command, since it spends most of its time sleeping. The naive + * approach would be to end the transaction immediately and instrument + * Worker::process(). The issue with that is that instrumentation hooks + * aren't executed if we're not actually in a transaction. + * + * So instead, what we'll do is to keep recording, but ensure that we ignore + * the transaction after WorkCommand::handle() has finished executing, at + * which point no more jobs can be run. + */ + nr_php_wrap_user_function( NR_PSTR("Illuminate\\Queue\\Console\\WorkCommand::handle"), - nr_laravel_queue_workcommand_fire TSRMLS_CC); + nr_laravel_queue_workcommand_handle TSRMLS_CC); +#endif /* * Hook the method that creates the JSON payloads for queued jobs so that we diff --git a/agent/php_execute.c b/agent/php_execute.c index d5ce67b31..49c56b0d1 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -1278,7 +1278,6 @@ static inline void nr_php_execute_segment_end( } duration = nr_time_duration(stacked->start_time, stacked->stop_time); - if (create_metric || (duration >= NR_PHP_PROCESS_GLOBALS(expensive_min)) || nr_vector_size(stacked->metrics) || stacked->id || stacked->attributes || stacked->error) { @@ -1808,6 +1807,8 @@ static inline void nr_php_observer_exception_segments_end( void nr_php_observer_segment_end(zval* exception) { nr_segment_t* segment = NULL; nruserfn_t* wraprec = NULL; + nrtime_t txn_start_time = 0; + /* * If we have a stacked segment that missed an OAPI func_end call, add an * exception (if not null) and close then get the current segment and return @@ -1818,7 +1819,7 @@ void nr_php_observer_segment_end(zval* exception) { if (NULL == NRPRG(txn)) { return; } - + txn_start_time = nr_txn_start_time(NRPRG(txn)); if (NULL != exception) { nr_status_t status; @@ -1842,6 +1843,24 @@ void nr_php_observer_segment_end(zval* exception) { zend_bailout(); } } + + /* + * During nr_zend_call_oapi_special_clean, the transaction may have been + * ended and/or a new transaction may have started. To detect this, we + * compare the currently active transaction's start time with the + * transaction start time we saved before. + * + * Just comparing the transaction pointer is not enough, as a newly + * started transaction might actually obtain the same address as a + * transaction freed before. + */ + if (nrunlikely(nr_txn_start_time(NRPRG(txn)) != txn_start_time)) { + nrl_verbosedebug( + NRL_AGENT, "%s txn ended and/or started while in a wrapped function", + __func__); + return; + } + /* * We are only here because there is a dangling segment which means * nr_php_observer_fcall_end didn't get called due to unhandled @@ -1926,7 +1945,8 @@ void php_observer_handle_exception_hook(zval* exception, zval* exception_this) { static void nr_php_observer_attempt_call_cufa_handler(NR_EXECUTE_PROTO) { NR_UNUSED_FUNC_RETURN_VALUE; if (NULL == execute_data->prev_execute_data) { - nrl_verbosedebug(NRL_AGENT, "%s: cannot get previous execute data", __func__); + nrl_verbosedebug(NRL_AGENT, "%s: cannot get previous execute data", + __func__); return; } @@ -1958,7 +1978,7 @@ static void nr_php_observer_attempt_call_cufa_handler(NR_EXECUTE_PROTO) { * resort. */ - /* + /* * When Observer API is used, this code executes in the context of * zend_execute and not in the context of VM (as was the case pre-OAPI), * therefore we need to ensure we're dealing with a user function. We cannot @@ -1970,7 +1990,8 @@ static void nr_php_observer_attempt_call_cufa_handler(NR_EXECUTE_PROTO) { return; } if (!ZEND_USER_CODE(execute_data->prev_execute_data->func->type)) { - nrl_verbosedebug(NRL_AGENT, "%s: caller is php internal function", __func__); + nrl_verbosedebug(NRL_AGENT, "%s: caller is php internal function", + __func__); return; } @@ -1981,30 +2002,34 @@ static void nr_php_observer_attempt_call_cufa_handler(NR_EXECUTE_PROTO) { const zend_op* prev_opline = execute_data->prev_execute_data->opline; - /* - * Extra safety check. Previously, we instrumented by overwritting ZEND_DO_FCALL. - * Within OAPI, for consistency's sake, we will ensure the same + /* + * Extra safety check. Previously, we instrumented by overwritting + * ZEND_DO_FCALL. Within OAPI, for consistency's sake, we will ensure the same */ if (ZEND_DO_FCALL != prev_opline->opcode) { - nrl_verbosedebug(NRL_AGENT, "%s: cannot get previous function name", __func__); + nrl_verbosedebug(NRL_AGENT, "%s: cannot get previous function name", + __func__); return; } - prev_opline -= 1; // Checks previous opcode + prev_opline -= 1; // Checks previous opcode if (ZEND_CHECK_UNDEF_ARGS == prev_opline->opcode) { - prev_opline -= 1; // Checks previous opcode + prev_opline -= 1; // Checks previous opcode } if (ZEND_SEND_ARRAY == prev_opline->opcode) { - if (UNEXPECTED((NULL == execute_data->func))) { nrl_verbosedebug(NRL_AGENT, "%s: cannot get current function", __func__); return; } - if (UNEXPECTED(NULL == execute_data->prev_execute_data->func->common.function_name)) { - nrl_verbosedebug(NRL_AGENT, "%s: cannot get previous function name", __func__); + if (UNEXPECTED( + NULL + == execute_data->prev_execute_data->func->common.function_name)) { + nrl_verbosedebug(NRL_AGENT, "%s: cannot get previous function name", + __func__); return; } - nr_php_call_user_func_array_handler(NRPRG(cufa_callback), execute_data->func, + nr_php_call_user_func_array_handler(NRPRG(cufa_callback), + execute_data->func, execute_data->prev_execute_data); } } @@ -2012,6 +2037,7 @@ static void nr_php_observer_attempt_call_cufa_handler(NR_EXECUTE_PROTO) { static void nr_php_instrument_func_begin(NR_EXECUTE_PROTO) { nr_segment_t* segment = NULL; nruserfn_t* wraprec = NULL; + nrtime_t txn_start_time = 0; int zcaught = 0; nr_php_execute_metadata_t* metadata = NULL; NR_UNUSED_FUNC_RETURN_VALUE; @@ -2021,7 +2047,7 @@ static void nr_php_instrument_func_begin(NR_EXECUTE_PROTO) { } NRTXNGLOBAL(execute_count) += 1; - + txn_start_time = nr_txn_start_time(NRPRG(txn)); /* * Handle here, but be aware the classes might not be loaded yet. */ @@ -2033,15 +2059,16 @@ static void nr_php_instrument_func_begin(NR_EXECUTE_PROTO) { } if (NULL != NRPRG(cufa_callback) && NRPRG(check_cufa)) { /* - * For PHP 7+, call_user_func_array() is flattened into an inline by default. Because - * of this, we must check the opcodes set to see whether we are calling it flattened. - * If we have a cufa callback, we want to call that here. This will create the wraprec - * for the user function we want to instrument and thus must be called before we search - * the wraprecs + * For PHP 7+, call_user_func_array() is flattened into an inline by + * default. Because of this, we must check the opcodes set to see whether we + * are calling it flattened. If we have a cufa callback, we want to call + * that here. This will create the wraprec for the user function we want to + * instrument and thus must be called before we search the wraprecs * - * For non-OAPI, this is handled in php_vm.c by overwriting the ZEND_DO_FCALL opcode. + * For non-OAPI, this is handled in php_vm.c by overwriting the + * ZEND_DO_FCALL opcode. */ - nr_php_observer_attempt_call_cufa_handler(NR_EXECUTE_ORIG_ARGS); + nr_php_observer_attempt_call_cufa_handler(NR_EXECUTE_ORIG_ARGS); } wraprec = nr_php_get_wraprec(execute_data->func); /* @@ -2154,6 +2181,24 @@ static void nr_php_instrument_func_begin(NR_EXECUTE_PROTO) { zend_bailout(); } + /* + * During nr_zend_call_oapi_special_before, the transaction may have been + * ended and/or a new transaction may have started. To detect this, we + * compare the currently active transaction's start time with the transaction + * start time we saved before. + * + * Just comparing the transaction pointer is not enough, as a newly + * started transaction might actually obtain the same address as a + * transaction freed before. + */ + if (nrunlikely(nr_txn_start_time(NRPRG(txn)) != txn_start_time)) { + nrl_verbosedebug(NRL_AGENT, + "%s txn ended and/or started while in a wrapped function", + __func__); + + return; + } + nr_txn_force_single_count(NRPRG(txn), wraprec->supportability_metric); /* * Check for, and handle, frameworks. @@ -2170,10 +2215,12 @@ static void nr_php_instrument_func_end(NR_EXECUTE_PROTO) { nruserfn_t* wraprec = NULL; bool create_metric = false; nr_php_execute_metadata_t* metadata = NULL; + nrtime_t txn_start_time = 0; if (NULL == NRPRG(txn)) { return; } + txn_start_time = nr_txn_start_time(NRPRG(txn)); /* * Let's get the framework info. @@ -2257,9 +2304,25 @@ static void nr_php_instrument_func_end(NR_EXECUTE_PROTO) { zend_bailout(); } } + /* + * During nr_zend_call_orig_execute_special, the transaction may have been + * ended and/or a new transaction may have started. To detect this, we + * compare the currently active transaction's start time with the transaction + * start time we saved before. + * + * Just comparing the transaction pointer is not enough, as a newly + * started transaction might actually obtain the same address as a + * transaction freed before. + */ + if (nrunlikely(nr_txn_start_time(NRPRG(txn)) != txn_start_time)) { + nrl_verbosedebug(NRL_AGENT, + "%s txn ended and/or started while in a wrapped function", + __func__); - nr_php_execute_segment_end(segment, segment->metadata, create_metric); + return; + } + nr_php_execute_segment_end(segment, segment->metadata, create_metric); /* * Clear the uncaught exception globals. This will also take care of the case * of an exception that was thrown for this segment but then was caught as From b4e3e5f61f810b4dd94ebd1aa1407c41a93a0c97 Mon Sep 17 00:00:00 2001 From: Michal Nowacki Date: Fri, 22 Dec 2023 10:48:18 -0500 Subject: [PATCH 40/56] fixup! 24c1c656540a742d9d0466b79d33808ca2507ed7 After #766 `nr_execute_handle_framework` needs filename length. --- agent/php_execute.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/agent/php_execute.c b/agent/php_execute.c index 84b526bc6..7a0692d22 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -2076,8 +2076,9 @@ static void nr_php_instrument_func_begin(NR_EXECUTE_PROTO) { */ if (nrunlikely(OP_ARRAY_IS_A_FILE(NR_OP_ARRAY))) { const char* filename = nr_php_op_array_file_name(NR_OP_ARRAY); + size_t filename_len = nr_php_op_array_file_name_len(NR_OP_ARRAY); nr_execute_handle_framework(all_frameworks, num_all_frameworks, - filename TSRMLS_CC); + filename, filename_len TSRMLS_CC); return; } if (NULL != NRPRG(cufa_callback) && NRPRG(check_cufa)) { From bdb1174ac0ed60cff52306583b3e9cc1cb03a2cb Mon Sep 17 00:00:00 2001 From: Michal Nowacki Date: Thu, 21 Dec 2023 18:21:14 -0500 Subject: [PATCH 41/56] fixup! 0150c096dc959dad60f820defb95d031d692aef9 After #778 wordpress cleaned tags need not to be freed - they're memoized in a hashmap that persists through the whole request and is destroyed in rshutdown. --- agent/fw_wordpress.c | 3 +-- agent/php_rinit.c | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/agent/fw_wordpress.c b/agent/fw_wordpress.c index 8c90b60fd..cda022423 100644 --- a/agent/fw_wordpress.c +++ b/agent/fw_wordpress.c @@ -632,8 +632,7 @@ NR_PHP_WRAPPER_END && !defined OVERWRITE_ZEND_EXECUTE_DATA static void clean_wordpress_tag_stack() { if ((bool)nr_stack_pop(&NRPRG(wordpress_tag_states))) { - char* cleaned_tag = nr_stack_pop(&NRPRG(wordpress_tags)); - nr_free(cleaned_tag); + nr_stack_pop(&NRPRG(wordpress_tags)); } if (nr_stack_is_empty(&NRPRG(wordpress_tags))) { NRPRG(check_cufa) = false; diff --git a/agent/php_rinit.c b/agent/php_rinit.c index 922e4a884..d704f935e 100644 --- a/agent/php_rinit.c +++ b/agent/php_rinit.c @@ -124,7 +124,6 @@ PHP_RINIT_FUNCTION(newrelic) { nr_stack_init(&NRPRG(drupal_invoke_all_hooks), NR_STACK_DEFAULT_CAPACITY); nr_stack_init(&NRPRG(drupal_invoke_all_states), NR_STACK_DEFAULT_CAPACITY); NRPRG(predis_ctxs).dtor = str_stack_dtor; - NRPRG(wordpress_tags).dtor = str_stack_dtor; NRPRG(drupal_invoke_all_hooks).dtor = zval_stack_dtor; #endif From 99bd0efcfdc3424d19fee65e89edf3d8c1736da0 Mon Sep 17 00:00:00 2001 From: bduranleau-nr <106178551+bduranleau-nr@users.noreply.github.com> Date: Thu, 25 Jan 2024 09:38:43 -0600 Subject: [PATCH 42/56] tests: remove broken exception handling unit tests (#817) PHP embed SAPI, used by unit tests, demonstrates memory issues when PHP code executed by way of `nr_php_call` or `tlib_php_request_eval` throws a PHP Exception. This functionality is best suited to be exercised via integration test. --- agent/tests/test_php_execute.c | 220 -------------------- agent/tests/test_php_wrapper.c | 368 +-------------------------------- 2 files changed, 1 insertion(+), 587 deletions(-) diff --git a/agent/tests/test_php_execute.c b/agent/tests/test_php_execute.c index bbff5a7df..de770bc7d 100644 --- a/agent/tests/test_php_execute.c +++ b/agent/tests/test_php_execute.c @@ -119,221 +119,6 @@ static void test_php_cur_stack_depth(TSRMLS_D) { tlib_php_request_end(); } -#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ - && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ - -static void populate_functions() { - tlib_php_request_eval( - "function three($a) { if (0 == $a) { throw new " - "RuntimeException('Division by zero'); } else return $a; }"); - tlib_php_request_eval("function two($a) { return three($a); }"); - tlib_php_request_eval("function uncaught($a) { return two($a); }"); - tlib_php_request_eval( - "function caught($a) { try {two($a);} catch (Exception $e) { return 1;} " - "return 1; }"); - tlib_php_request_eval( - "function followup($a) { try {two($a);} catch (Exception $e) { return " - "three(1);} return three(1); }"); - tlib_php_request_eval( - "function followup_uncaught($a) { try {two($a);} catch (Exception $e) { " - "return three(0);} return three(1); }"); - tlib_php_request_eval( - "function rethrow($a) { try {two($a);} catch (Exception $e) { throw new " - "RuntimeException('Rethrown caught exception: '. $e->getMessage());} " - "return three(1); }"); -} - -static void test_stack_depth_after_exception() { - zval* expr = NULL; - zval* arg = NULL; - - /* - * call a function and trigger an exception and cause two segments to dangle - * because it was caught, even though two functions don't get the oapi end - * func call, they are still cleaned up and stack_depth is appropriately - * decremented. the valid function end will trigger a cleanup dangling - * segments, stack_depth should be zero again - */ - - /* - * stack depth should increment on function call and decrement on function - * end. - */ - tlib_php_request_start(); - populate_functions(); - /* - * pass argument that will not throw exception. - * stack depth should be 0 before calling and 1 before ending. - */ - tlib_pass_if_int_equal( - "PHP stack depth tracking should be 0 before function call", 0, - NRPRG(php_cur_stack_depth)); - arg = tlib_php_request_eval_expr("1" TSRMLS_CC); - expr = nr_php_call(NULL, "uncaught", arg); - tlib_pass_if_not_null("Runs fine with no exception.", expr); - tlib_pass_if_zval_type_is("Should have received the arg value.", IS_LONG, - expr); - tlib_pass_if_int_equal( - "PHP stack depth tracking should be 0 after successful function call", 0, - NRPRG(php_cur_stack_depth)); - - nr_php_zval_free(&expr); - nr_php_zval_free(&arg); - - /* - * call a function and trigger an exception and cause three segments to dangle - * stack_depth should be initially stuck at 3 - * after triggering the unwind, stack_depth should be zero again - */ - tlib_pass_if_int_equal( - "PHP stack depth tracking should be 0 before function call", 0, - NRPRG(php_cur_stack_depth)); - arg = tlib_php_request_eval_expr("0"); - expr = nr_php_call(NULL, "uncaught", arg); - tlib_pass_if_int_equal( - "PHP stack depth tracking should be 3 after function call", 3, - NRPRG(php_cur_stack_depth)); - tlib_pass_if_null("Exception so expr should be null.", expr); - - /* - * Trigger the unwind. - */ - tlib_php_request_eval("newrelic_end_transaction(); "); - tlib_pass_if_int_equal( - "PHP stack depth tracking should be 0 after transaction ends", 0, - NRPRG(php_cur_stack_depth)); - nr_php_zval_free(&expr); - nr_php_zval_free(&arg); - tlib_php_request_end(); - - /* - * call a function and trigger an exception that is caught but causes two - * segments to dangle. - * the function that caught the exception will successfully call the - * registered oapi function end which will trigger a cleanup of dangling - * segments, stack_depth should be zero - */ - - tlib_php_request_start(); - populate_functions(); - - tlib_pass_if_int_equal( - "PHP stack depth tracking should be 0 before function call", 0, - NRPRG(php_cur_stack_depth)); - arg = tlib_php_request_eval_expr("0"); - expr = nr_php_call(NULL, "caught", arg); - tlib_pass_if_int_equal( - "PHP stack depth tracking should be 0 after function call", 0, - NRPRG(php_cur_stack_depth)); - tlib_pass_if_not_null("Exception caught so expr should not be null.", expr); - - /* - * Trigger the unwind. - */ - tlib_php_request_eval("newrelic_end_transaction(); "); - tlib_pass_if_int_equal( - "PHP stack depth tracking should be 0 after transaction ends", 0, - NRPRG(php_cur_stack_depth)); - nr_php_zval_free(&expr); - nr_php_zval_free(&arg); - tlib_php_request_end(); - - /* - * call a function and trigger an exception that is caught but the initial - * exception caused two segments to dangle. immediately call another function - * that will trigger cleanup of segments, and stack_depth should be zero. - */ - - tlib_php_request_start(); - populate_functions(); - - tlib_pass_if_int_equal( - "PHP stack depth tracking should be 0 before function call", 0, - NRPRG(php_cur_stack_depth)); - arg = tlib_php_request_eval_expr("0"); - expr = nr_php_call(NULL, "followup", arg); - tlib_pass_if_int_equal( - "PHP stack depth tracking should be 0 after function call", 0, - NRPRG(php_cur_stack_depth)); - tlib_pass_if_not_null("Exception caught so expr should not be null.", expr); - tlib_pass_if_int_equal( - "PHP stack depth tracking should be 0 after transaction", 0, - NRPRG(php_cur_stack_depth)); - - /* - * Trigger the unwind. - */ - tlib_php_request_eval("newrelic_end_transaction(); "); - tlib_pass_if_int_equal( - "PHP stack depth tracking should be 0 after transaction ends", 0, - NRPRG(php_cur_stack_depth)); - nr_php_zval_free(&expr); - nr_php_zval_free(&arg); - tlib_php_request_end(); - - /* - * call a function and trigger an exception that is caught but another - * uncaught exception is thrown and causes two segments to dangle stack_depth - * should be initially stuck at 2 but after unwind, stack_depth should be zero - * again - */ - - tlib_php_request_start(); - populate_functions(); - - tlib_pass_if_int_equal( - "PHP stack depth tracking should be 0 before function call", 0, - NRPRG(php_cur_stack_depth)); - arg = tlib_php_request_eval_expr("0"); - expr = nr_php_call(NULL, "followup_uncaught", arg); - tlib_pass_if_int_equal( - "PHP stack depth tracking should be 2 after function call", 2, - NRPRG(php_cur_stack_depth)); - tlib_pass_if_null("Exception so expr should not be null.", expr); - - /* - * Trigger the unwind. - */ - tlib_php_request_eval("newrelic_end_transaction(); "); - tlib_pass_if_int_equal( - "PHP stack depth tracking should be 0 after transaction ends", 0, - NRPRG(php_cur_stack_depth)); - nr_php_zval_free(&expr); - nr_php_zval_free(&arg); - tlib_php_request_end(); - - /* - * call a function and trigger an exception that is caught then rethrown - * stack_depth should be initially stuck at 2 - * but after unwind, stack_depth should be zero again - */ - - tlib_php_request_start(); - populate_functions(); - - tlib_pass_if_int_equal( - "PHP stack depth tracking should be 0 before function call", 0, - NRPRG(php_cur_stack_depth)); - arg = tlib_php_request_eval_expr("0"); - expr = nr_php_call(NULL, "rethrow", arg); - tlib_pass_if_int_equal( - "PHP stack depth tracking should be 2 after function call", 1, - NRPRG(php_cur_stack_depth)); - tlib_pass_if_null("Exception so expr should not be null.", expr); - - /* - * Trigger the unwind. - */ - tlib_php_request_eval("newrelic_end_transaction(); "); - tlib_pass_if_int_equal( - "PHP stack depth tracking should be 0 after transaction ends", 0, - NRPRG(php_cur_stack_depth)); - nr_php_zval_free(&expr); - nr_php_zval_free(&arg); - tlib_php_request_end(); -} -#endif - void test_main(void* p NRUNUSED) { #if defined(ZTS) && !defined(PHP7) void*** tsrm_ls = NULL; @@ -343,10 +128,5 @@ void test_main(void* p NRUNUSED) { test_txn_restart_in_callstack(TSRMLS_C); test_php_cur_stack_depth(TSRMLS_C); -#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ - && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ - test_stack_depth_after_exception(); -#endif - tlib_php_engine_destroy(TSRMLS_C); } diff --git a/agent/tests/test_php_wrapper.c b/agent/tests/test_php_wrapper.c index 513654b2b..975a46904 100644 --- a/agent/tests/test_php_wrapper.c +++ b/agent/tests/test_php_wrapper.c @@ -14,45 +14,6 @@ tlib_parallel_info_t parallel_info #if ZEND_MODULE_API_NO >= ZEND_7_3_X_API_NO -/* - * Set test_before, test_after, test_clean to use a Newrelic global variable. - * Randomly picking NRPRG(drupal_http_request_depth) because it is easy to use, - * a variable, and not used in any other way in this test. - * - */ -#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ - && !defined OVERWRITE_ZEND_EXECUTE_DATA - -NR_PHP_WRAPPER(test_before) { - (void)wraprec; - NRPRG(drupal_http_request_depth) = 10; - - NR_PHP_WRAPPER_CALL; -} -NR_PHP_WRAPPER_END - -NR_PHP_WRAPPER(test_after) { - (void)wraprec; - NRPRG(drupal_http_request_depth) = 20; - - NR_PHP_WRAPPER_CALL; -} -NR_PHP_WRAPPER_END - -NR_PHP_WRAPPER(test_clean) { - (void)wraprec; - if (20 != NRPRG(drupal_http_request_depth)) { - NRPRG(drupal_http_request_depth) = 30; - } - /* - * If 20 = NRPRG(drupal_http_request_depth) it means the after callback was - * called. We should never call clean if the after callback was called. - */ - - NR_PHP_WRAPPER_CALL; -} -NR_PHP_WRAPPER_END -#endif /* * endif to match: * ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ @@ -789,340 +750,13 @@ static void test_add_arg(TSRMLS_D) { tlib_php_request_end(); } -#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ - && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ -static void test_before_after_clean() { - zval* expr = NULL; - zval* arg = NULL; - - /* - * before, after, clean callbacks are all set. - */ - tlib_php_request_start(); - - tlib_php_request_eval( - "function all_set($a) { if (0 == $a) { throw new " - "RuntimeException('Division by zero'); } else return $a; }" TSRMLS_CC); - nr_php_wrap_user_function_before_after_clean( - NR_PSTR("all_set"), test_before, test_after, test_clean); - /* - * pass argument that will not throw exception. - * before/after should be called. - * clean callback should not be called. - */ - arg = tlib_php_request_eval_expr("1" TSRMLS_CC); - expr = nr_php_call(NULL, "all_set", arg); - tlib_pass_if_not_null("Runs fine with no exception.", expr); - tlib_pass_if_zval_type_is("Should have received the arg value.", IS_LONG, - expr); - tlib_pass_if_int_equal("After callback should set value", 20, - NRPRG(drupal_http_request_depth)); - NRPRG(drupal_http_request_depth) = 0; - nr_php_zval_free(&expr); - nr_php_zval_free(&arg); - - /* - * pass argument that will throw exception. - * before should be called after should not be called. - * clean callback should be called. - */ - - arg = tlib_php_request_eval_expr("0" TSRMLS_CC); - expr = nr_php_call(NULL, "all_set", arg); - - tlib_pass_if_null("Exception so expr should be null.", expr); - /* - * Trigger the unwind. - */ - tlib_php_request_eval("newrelic_end_transaction(); "); - tlib_pass_if_int_equal("Clean callback should set value", 30, - NRPRG(drupal_http_request_depth)); - NRPRG(drupal_http_request_depth) = 0; - nr_php_zval_free(&expr); - nr_php_zval_free(&arg); - tlib_php_request_end(); - - /* - * before, after, callbacks are set - */ - - tlib_php_request_start(); - tlib_php_request_eval( - "function before_after($a) { if (0 == $a) { throw new " - "RuntimeException('Division by zero'); } else return $a; }" TSRMLS_CC); - nr_php_wrap_user_function_before_after_clean( - NR_PSTR("before_after"), test_before, test_after, NULL); - - /* - * pass argument that will not throw exception. - * before/after should be called. - * no clean callback - */ - - arg = tlib_php_request_eval_expr("1" TSRMLS_CC); - expr = nr_php_call(NULL, "before_after", arg); - tlib_pass_if_not_null("Runs fine with no exception.", expr); - tlib_pass_if_zval_type_is("Should have received the arg value.", IS_LONG, - expr); - tlib_pass_if_int_equal("After callback should set value", 20, - NRPRG(drupal_http_request_depth)); - NRPRG(drupal_http_request_depth) = 0; - nr_php_zval_free(&expr); - nr_php_zval_free(&arg); - - /* - * pass argument that will throw exception. - * before should be called after should not be called. - * clean should not be called. - */ - arg = tlib_php_request_eval_expr("0" TSRMLS_CC); - expr = nr_php_call(NULL, "before_after", arg); - tlib_pass_if_null("Exception so does not evaluate.", expr); - tlib_php_request_eval("newrelic_end_transaction(); "); - tlib_pass_if_int_equal("Clean callback should not set value", 10, - NRPRG(drupal_http_request_depth)); - tlib_pass_if_int_equal( - "Since there is no clean and after doesn't get called, only the before " - "value persists and does not get cleaned up.", - 10, NRPRG(drupal_http_request_depth)); - NRPRG(drupal_http_request_depth) = 0; - nr_php_zval_free(&expr); - nr_php_zval_free(&arg); - - tlib_php_request_end(); - - /* - * before, clean callbacks are set - */ - - tlib_php_request_start(); - - tlib_php_request_eval( - "function before_clean($a) { if (0 == $a) { throw new " - "RuntimeException('Division by zero'); } else return $a; }" TSRMLS_CC); - nr_php_wrap_user_function_before_after_clean( - NR_PSTR("before_clean"), test_before, NULL, test_clean); - - /* - * pass argument that will not throw exception. - * before should be called. - * clean callback should not be called. - */ - - arg = tlib_php_request_eval_expr("1" TSRMLS_CC); - expr = nr_php_call(NULL, "before_clean", arg); - tlib_pass_if_not_null("Runs fine with no exception.", expr); - tlib_pass_if_zval_type_is("Should have received the arg value.", IS_LONG, - expr); - tlib_pass_if_int_equal("Only before callback should set value", 10, - NRPRG(drupal_http_request_depth)); - NRPRG(drupal_http_request_depth) = 0; - nr_php_zval_free(&expr); - nr_php_zval_free(&arg); - - /* - * pass argument that will throw exception. - * no before callback. - * clean callback should be called. - */ - arg = tlib_php_request_eval_expr("0" TSRMLS_CC); - expr = nr_php_call(NULL, "before_clean", arg); - tlib_pass_if_null("Exception so func does not evaluate.", expr); - /* - * Trigger the unwind. - */ - tlib_php_request_eval("newrelic_end_transaction(); "); - tlib_pass_if_int_equal("Clean callback should set value", 30, - NRPRG(drupal_http_request_depth)); - NRPRG(drupal_http_request_depth) = 0; - nr_php_zval_free(&expr); - nr_php_zval_free(&arg); - tlib_php_request_end(); - - /* - * after, clean callbacks are set - */ - tlib_php_request_start(); - - tlib_php_request_eval( - "function after_clean($a) { if (0 == $a) { throw new " - "RuntimeException('Division by zero'); } else return $a; }" TSRMLS_CC); - nr_php_wrap_user_function_before_after_clean( - NR_PSTR("after_clean"), NULL, test_after, test_clean); - /* - * pass argument that will not throw exception. - * after should be called. - * clean callback should not be called. - */ - arg = tlib_php_request_eval_expr("1" TSRMLS_CC); - expr = nr_php_call(NULL, "after_clean", arg); - tlib_pass_if_not_null("Runs fine with no exception.", expr); - tlib_pass_if_zval_type_is("Should have received the arg value.", IS_LONG, - expr); - tlib_pass_if_int_equal("After callback should set value", 20, - NRPRG(drupal_http_request_depth)); - NRPRG(drupal_http_request_depth) = 0; - nr_php_zval_free(&expr); - nr_php_zval_free(&arg); - - /* - * pass argument that will throw exception. - * before should be called after should not be called. - * clean callback should be called. - */ - arg = tlib_php_request_eval_expr("0" TSRMLS_CC); - expr = nr_php_call(NULL, "after_clean", arg); - tlib_pass_if_null("Exception so returns null.", expr); - tlib_php_request_eval("newrelic_end_transaction(); "); - tlib_pass_if_int_equal( - "After callback should not be called and clean callback should set value", - 30, NRPRG(drupal_http_request_depth)); - NRPRG(drupal_http_request_depth) = 0; - nr_php_zval_free(&expr); - nr_php_zval_free(&arg); - - tlib_php_request_end(); - - /* - * before only callback - */ - tlib_php_request_start(); - - tlib_php_request_eval( - "function before_only($a) { if (0 == $a) { throw new " - "RuntimeException('Division by zero'); } else return $a; }" TSRMLS_CC); - nr_php_wrap_user_function_before_after_clean( - NR_PSTR("before_only"), test_before, NULL, NULL); - /* - * pass argument that will not throw exception. - * before should be called. - * no other callbacks should be called. - */ - arg = tlib_php_request_eval_expr("1" TSRMLS_CC); - expr = nr_php_call(NULL, "before_only", arg); - tlib_pass_if_not_null("Runs fine with no exception.", expr); - tlib_pass_if_zval_type_is("Should have received the arg value.", IS_LONG, - expr); - tlib_pass_if_int_equal("Before callback should set value", 10, - NRPRG(drupal_http_request_depth)); - NRPRG(drupal_http_request_depth) = 0; - nr_php_zval_free(&expr); - nr_php_zval_free(&arg); - - /* - * pass argument that will throw exception. - * before should be called after should not be called. - * clean callback should be called. - */ - arg = tlib_php_request_eval_expr("0" TSRMLS_CC); - expr = nr_php_call(NULL, "before_only", arg); - tlib_pass_if_null("Exception so does not evaluate.", expr); - tlib_php_request_eval("newrelic_end_transaction(); "); - tlib_pass_if_int_equal("Only before would set the value", 10, - NRPRG(drupal_http_request_depth)); - NRPRG(drupal_http_request_depth) = 0; - nr_php_zval_free(&expr); - nr_php_zval_free(&arg); - - tlib_php_request_end(); - - /* - * after only callback - */ - tlib_php_request_start(); - - tlib_php_request_eval( - "function after_only($a) { if (0 == $a) { throw new " - "RuntimeException('Division by zero'); } else return $a; }" TSRMLS_CC); - nr_php_wrap_user_function_before_after_clean( - NR_PSTR("after_only"), NULL, test_after, NULL); - /* - * pass argument that will not throw exception. - * after should be called. - * no other callbacks should be called. - */ - arg = tlib_php_request_eval_expr("1" TSRMLS_CC); - expr = nr_php_call(NULL, "after_only", arg); - tlib_pass_if_not_null("Runs fine with no exception.", expr); - tlib_pass_if_zval_type_is("Should have received the arg value.", IS_LONG, - expr); - tlib_pass_if_int_equal("After callback should set value", 20, - NRPRG(drupal_http_request_depth)); - NRPRG(drupal_http_request_depth) = 0; - nr_php_zval_free(&expr); - nr_php_zval_free(&arg); - - /* - * pass argument that will throw exception. - * before should be called after should not be called. - * clean callback should be called. - */ - arg = tlib_php_request_eval_expr("0" TSRMLS_CC); - expr = nr_php_call(NULL, "after_only", arg); - tlib_pass_if_null("Exception so should be null.", expr); - tlib_php_request_eval("newrelic_end_transaction(); "); - tlib_pass_if_int_equal("No callbacks triggered to set the value", 0, - NRPRG(drupal_http_request_depth)); - NRPRG(drupal_http_request_depth) = 0; - nr_php_zval_free(&expr); - nr_php_zval_free(&arg); - - tlib_php_request_end(); - - /* - * clean only callback - */ - tlib_php_request_start(); - - tlib_php_request_eval( - "function clean_only($a) { if (0 == $a) { throw new " - "RuntimeException('Division by zero'); } else return $a; }" TSRMLS_CC); - nr_php_wrap_user_function_before_after_clean( - NR_PSTR("clean_only"), NULL, NULL, test_clean); - /* - * pass argument that will not throw exception. - * clean should be called. - * no other callbacks should be called. - */ - arg = tlib_php_request_eval_expr("1" TSRMLS_CC); - expr = nr_php_call(NULL, "clean_only", arg); - tlib_pass_if_not_null("Runs fine with no exception.", expr); - tlib_pass_if_zval_type_is("Should have received the arg value.", IS_LONG, - expr); - tlib_pass_if_int_equal("No callback to set value", 0, - NRPRG(drupal_http_request_depth)); - NRPRG(drupal_http_request_depth) = 0; - nr_php_zval_free(&expr); - nr_php_zval_free(&arg); - - /* - * pass argument that will throw exception. - * clean callback should be called. - */ - arg = tlib_php_request_eval_expr("0" TSRMLS_CC); - expr = nr_php_call(NULL, "clean_only", arg); - tlib_pass_if_null("Exception so should be null.", expr); - tlib_php_request_eval("newrelic_end_transaction(); "); - tlib_pass_if_int_equal("Only clean would set the value", 30, - NRPRG(drupal_http_request_depth)); - NRPRG(drupal_http_request_depth) = 0; - nr_php_zval_free(&expr); - nr_php_zval_free(&arg); - - tlib_php_request_end(); -} -#endif - void test_main(void* p NRUNUSED) { #if defined(ZTS) && !defined(PHP7) void*** tsrm_ls = NULL; #endif /* ZTS && !PHP7 */ tlib_php_engine_create("" PTSRMLS_CC); test_add_arg(); -#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ - && !defined OVERWRITE_ZEND_EXECUTE_DATA - test_before_after_clean(); -#endif + tlib_php_engine_destroy(TSRMLS_C); /* * The Jenkins PHP 7.3 nodes are unable to handle the multiple From bfc5ac2376b8f6cda379f7e53c3d96f917d12729 Mon Sep 17 00:00:00 2001 From: Michal Nowacki Date: Thu, 25 Jan 2024 16:02:35 -0500 Subject: [PATCH 43/56] fixup! 43590ae2dfe0a86ca12d699e4406ad9d831d04bb After #798 there's no need to fix wrapping of transient user functions in nr_php_add_custom_tracer_named because it is no longer used for those. --- agent/php_user_instrument.c | 64 ++++--------------------------------- 1 file changed, 7 insertions(+), 57 deletions(-) diff --git a/agent/php_user_instrument.c b/agent/php_user_instrument.c index a148db4bb..a0ed0600e 100644 --- a/agent/php_user_instrument.c +++ b/agent/php_user_instrument.c @@ -244,22 +244,21 @@ static void nr_php_wrap_zend_function(zend_function* func, } } -// Returns whether wraprec ownership was transfered to the hashmap -static bool nr_php_wrap_user_function_internal(nruserfn_t* wraprec TSRMLS_DC) { +static void nr_php_wrap_user_function_internal(nruserfn_t* wraprec TSRMLS_DC) { zend_function* orig_func = 0; if (0 == NR_PHP_PROCESS_GLOBALS(done_instrumentation)) { - return false; + return; } if (wraprec->is_wrapped) { - return false; + return; } #if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ && defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP8+ */ if (nrunlikely(-1 == NR_PHP_PROCESS_GLOBALS(zend_offset))) { - return false; + return; } #endif if (0 == wraprec->classname) { @@ -273,7 +272,7 @@ static bool nr_php_wrap_user_function_internal(nruserfn_t* wraprec TSRMLS_DC) { if (NULL == orig_func) { /* It could be in a file not yet loaded, no reason to log anything. */ - return false; + return; } if (ZEND_USER_FUNCTION != orig_func->type) { @@ -286,10 +285,9 @@ static bool nr_php_wrap_user_function_internal(nruserfn_t* wraprec TSRMLS_DC) { * logs with this message. */ wraprec->is_disabled = 1; - return false; + return; } nr_php_wrap_zend_function(orig_func, wraprec TSRMLS_CC); - return true; } static nruserfn_t* nr_php_user_wraprec_create(void) { @@ -451,28 +449,7 @@ nruserfn_t* nr_php_add_custom_tracer_named(const char* namestr, return 0; } - /* Make sure that we are not duplicating an existing wraprecord. - * - * For non-transient wrappers (standard instrumentation), the wrapper - * is stored in both the hashmap and linked-list. For transient wrappers, - * the wrapper is only stored in the hashmap. HOWEVER! non-transient - * wrappers MAY only be in the linked-list. This normally happens if it - * is wrapping a function that hasn't been loaded by PHP yet. Once the - * function is loaded, there are many hooks to ensure the wrapper is also - * added to the hashmap. - * - * The implications of the above are: For non-transient wrappers, we only - * need to check the linked-list to ensure that we are not duplicating a - * wrapper. If a wrapper is only in the hashmap, it is transient and will - * be overwritten by the non-transient wrapper. - * - * For transient wrappers, however, we must check both the linked - * list and the hashmap. This is because it is possible to attempt to - * create a transient wrapper around an already non-transiently wrapped - * function. This will return the non-transient wrapper and will attempt - * to set the transient callbacks if that wrapper has those callbacks free. - * This means that it is possible for callbacks intended to be transient - * are attached onto a wrapper that isn't. */ + /* Make sure that we are not duplicating an existing wraprecord */ p = nr_wrapped_user_functions; while (0 != p) { @@ -489,33 +466,6 @@ nruserfn_t* nr_php_add_custom_tracer_named(const char* namestr, } p = p->next; } - if (NR_WRAPREC_IS_TRANSIENT == options->transience) { - zend_function* orig_func = 0; - if (0 == wraprec->classname) { - orig_func = nr_php_find_function(wraprec->funcnameLC TSRMLS_CC); - } else { - zend_class_entry* orig_class = 0; - - orig_class = nr_php_find_class(wraprec->classnameLC TSRMLS_CC); - orig_func = nr_php_find_class_method(orig_class, wraprec->funcnameLC); - } - - if (NULL != orig_func) { -#if ZEND_MODULE_API_NO < ZEND_7_4_X_API_NO - p = nr_php_op_array_get_wraprec(&orig_func->op_array TSRMLS_CC); -#else - p = nr_php_wraprec_lookup_get(orig_func); -#endif - - if (p) { - nrl_verbosedebug(NRL_INSTRUMENT, "reusing custom wrapper for callable '%s'", - namestr); - nr_php_user_wraprec_destroy(&wraprec); - nr_php_wrap_user_function_internal(p TSRMLS_CC); - return p; - } - } - } nrl_verbosedebug( NRL_INSTRUMENT, "adding custom for '" NRP_FMT_UQ "%.5s" NRP_FMT_UQ "'", From 50352981a76afba4a57f48140a4cf62e14919d6d Mon Sep 17 00:00:00 2001 From: Michal Nowacki Date: Thu, 25 Jan 2024 17:10:25 -0500 Subject: [PATCH 44/56] fixup! 43590ae2dfe0a86ca12d699e4406ad9d831d04bb After #798 there's no need to pass any wraprec options when wraprecs are created with nr_php_add_custom_tracer_named because it is no longer used to create different types of wraprecs. It always creates non-transient wraprecs and therefore always needs to generate instrumented function metrics. --- agent/php_user_instrument.c | 12 ++++-------- agent/php_wrapper.c | 19 +------------------ agent/php_wrapper.h | 8 -------- 3 files changed, 5 insertions(+), 34 deletions(-) diff --git a/agent/php_user_instrument.c b/agent/php_user_instrument.c index a0ed0600e..65b078b71 100644 --- a/agent/php_user_instrument.c +++ b/agent/php_user_instrument.c @@ -295,8 +295,7 @@ static nruserfn_t* nr_php_user_wraprec_create(void) { } static nruserfn_t* nr_php_user_wraprec_create_named(const char* full_name, - int full_name_len, - nr_instrumented_function_metric_t ifm) { + int full_name_len) { int i; const char* name; const char* klass; @@ -339,10 +338,8 @@ static nruserfn_t* nr_php_user_wraprec_create_named(const char* full_name, wraprec->is_method = 1; } - if (NR_WRAPREC_CREATE_INSTRUMENTED_FUNCTION_METRIC == ifm) { - wraprec->supportability_metric = nr_txn_create_fn_supportability_metric( - wraprec->funcname, wraprec->classname); - } + wraprec->supportability_metric = nr_txn_create_fn_supportability_metric( + wraprec->funcname, wraprec->classname); return wraprec; } @@ -443,8 +440,7 @@ nruserfn_t* nr_php_add_custom_tracer_named(const char* namestr, nruserfn_t* wraprec; nruserfn_t* p; - wraprec = nr_php_user_wraprec_create_named(namestr, namestrlen, - options->instrumented_function_metric); + wraprec = nr_php_user_wraprec_create_named(namestr, namestrlen); if (0 == wraprec) { return 0; } diff --git a/agent/php_wrapper.c b/agent/php_wrapper.c index f80b4d696..daa01bb4f 100644 --- a/agent/php_wrapper.c +++ b/agent/php_wrapper.c @@ -62,25 +62,8 @@ nruserfn_t* nr_php_wrap_user_function_before_after_clean( nrspecialfn_t before_callback, nrspecialfn_t after_callback, nrspecialfn_t clean_callback) { - nr_wrap_user_function_options_t options = { - NR_WRAPREC_NOT_TRANSIENT, - NR_WRAPREC_CREATE_INSTRUMENTED_FUNCTION_METRIC - }; - return nr_php_wrap_user_function_before_after_clean_with_options( - name, namelen, before_callback, after_callback, - clean_callback, &options); -} - -nruserfn_t* nr_php_wrap_user_function_before_after_clean_with_options( - const char* name, - size_t namelen, - nrspecialfn_t before_callback, - nrspecialfn_t after_callback, - nrspecialfn_t clean_callback, - const nr_wrap_user_function_options_t* options) { - nruserfn_t* wraprec - = nr_php_add_custom_tracer_named(name, namelen, options); + nruserfn_t* wraprec = nr_php_add_custom_tracer_named(name, namelen); nr_php_wraprec_add_before_after_clean_callbacks(name, namelen, wraprec, before_callback, diff --git a/agent/php_wrapper.h b/agent/php_wrapper.h index d11456762..d89a1ab5a 100644 --- a/agent/php_wrapper.h +++ b/agent/php_wrapper.h @@ -143,14 +143,6 @@ extern nruserfn_t* nr_php_wrap_user_function_before_after_clean( nrspecialfn_t after_callback, nrspecialfn_t clean_callback); -extern nruserfn_t* nr_php_wrap_user_function_before_after_clean_with_options( - const char* name, - size_t namelen, - nrspecialfn_t before_callback, - nrspecialfn_t after_callback, - nrspecialfn_t clean_callback, - const nr_wrap_user_function_options_t* options); - extern nruserfn_t* nr_php_wrap_callable_before_after_clean( zend_function* callable, nrspecialfn_t before_callback, From a20529bf1a99a30f8834c391e6de0220895113e2 Mon Sep 17 00:00:00 2001 From: Michal Nowacki Date: Mon, 29 Jan 2024 17:49:28 -0500 Subject: [PATCH 45/56] fixup! 0fa3083c32c65abf8d48b28f76e96ee17679440a After #768, filter hook's callback function instrumentation needs to create wordpress metrics when hook callback function throws an exception. Therefore its 'after' wrappers (after and clean) need to be set to do it on hook's callback function either normal return or when it returns via an exception. --- agent/fw_wordpress.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/agent/fw_wordpress.c b/agent/fw_wordpress.c index a46f0a62e..0164ca5c3 100644 --- a/agent/fw_wordpress.c +++ b/agent/fw_wordpress.c @@ -757,7 +757,13 @@ NR_PHP_WRAPPER(nr_wordpress_add_filter) { if (NULL != zf) { char* wordpress_plugin_theme = nr_wordpress_plugin_from_function(zf); if (NULL != wordpress_plugin_theme || NRPRG(wordpress_core)) { +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + callback_wraprec = nr_php_wrap_callable_before_after_clean( + zf, NULL, nr_wordpress_wrap_hook, nr_wordpress_wrap_hook); +#else callback_wraprec = nr_php_wrap_callable(zf, nr_wordpress_wrap_hook); +#endif // We can cheat here: wraprecs on callables are always transient, so if // there's a wordpress_plugin_theme set we know it's from this // transaction, and we don't have any issues around a possible @@ -796,9 +802,9 @@ void nr_wordpress_enable(TSRMLS_D) { nr_php_wrap_user_function_before_after_clean( NR_PSTR("do_action_ref_array"), nr_wordpress_exec_handle_tag, nr_wordpress_handle_tag_stack_after, nr_wordpress_handle_tag_stack_clean); - - nr_php_wrap_user_function(NR_PSTR("add_filter"), - nr_wordpress_add_filter); + if (0 != NRPRG(wordpress_plugins)) { + nr_php_wrap_user_function(NR_PSTR("add_filter"), nr_wordpress_add_filter); + } } #else From e99f7cbb71d1391886cccc6d46aba1980da6b6d2 Mon Sep 17 00:00:00 2001 From: Michal Nowacki Date: Mon, 29 Jan 2024 17:58:20 -0500 Subject: [PATCH 46/56] fixup! ac78511ede0e96d59610b798b6a41b53a202364b After #799, wordpress hooks stack handlers need to be able to generate hooks metrics too when hooks are to be monitored but not hooks' callback functions, i.e. when newrelic.framework.wordpress.hooks.options=threshold. --- agent/fw_wordpress.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/agent/fw_wordpress.c b/agent/fw_wordpress.c index 0164ca5c3..cb61141bb 100644 --- a/agent/fw_wordpress.c +++ b/agent/fw_wordpress.c @@ -661,9 +661,12 @@ NR_PHP_WRAPPER_END #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA -static void clean_wordpress_tag_stack() { +static void clean_wordpress_tag_stack(nr_segment_t* segment) { if ((bool)nr_stack_pop(&NRPRG(wordpress_tag_states))) { - nr_stack_pop(&NRPRG(wordpress_tags)); + char* tag = nr_stack_pop(&NRPRG(wordpress_tags)); + if (0 == NRPRG(wordpress_plugins)) { + nr_wordpress_hooks_create_metric(segment, tag); + } } if (nr_stack_is_empty(&NRPRG(wordpress_tags))) { NRPRG(check_cufa) = false; @@ -673,7 +676,7 @@ static void clean_wordpress_tag_stack() { NR_PHP_WRAPPER(nr_wordpress_handle_tag_stack_after) { (void)wraprec; if (0 != NRINI(wordpress_hooks)) { - clean_wordpress_tag_stack(); + clean_wordpress_tag_stack(auto_segment); } } NR_PHP_WRAPPER_END @@ -683,7 +686,7 @@ NR_PHP_WRAPPER(nr_wordpress_handle_tag_stack_clean) { NR_UNUSED_FUNC_RETURN_VALUE; (void)wraprec; if (0 != NRINI(wordpress_hooks)) { - clean_wordpress_tag_stack(); + clean_wordpress_tag_stack(auto_segment); } } NR_PHP_WRAPPER_END From 53d10e0870955520e2c020af1194a7733486932a Mon Sep 17 00:00:00 2001 From: Michal Nowacki Date: Mon, 29 Jan 2024 18:21:55 -0500 Subject: [PATCH 47/56] fixup! 43590ae2dfe0a86ca12d699e4406ad9d831d04bb After #798, the tests expect Drupal\page_cache\StackMiddleware\PageCache::get to be instrumented so that transient instrumentation will not get installed. But empty files don't get executed by PHP 8.2+ when the agent uses Observer API to hook into Zend engine, therefore mocks needs to do something in order for the non-transient instrumentation to be installed (classes and class' methods need to be available at the time non-transient instrumentation is installed). Enhance Drupal\page_cache\StackMiddleware\PageCache with a 'noop' statement - echo "";` --- tests/integration/frameworks/drupal/mock_page_cache_get.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/frameworks/drupal/mock_page_cache_get.php b/tests/integration/frameworks/drupal/mock_page_cache_get.php index c5a59c66e..bddb52493 100644 --- a/tests/integration/frameworks/drupal/mock_page_cache_get.php +++ b/tests/integration/frameworks/drupal/mock_page_cache_get.php @@ -13,4 +13,5 @@ public function get(callable $hook, string $module) { $hook(); } } + echo ""; } From 4da12116f2d9fb87d9cd61c5ff0465ace9980b0a Mon Sep 17 00:00:00 2001 From: bduranleau-nr <106178551+bduranleau-nr@users.noreply.github.com> Date: Wed, 31 Jan 2024 12:29:38 -0600 Subject: [PATCH 48/56] fix(agent): revert zend_try/catch php call logic (#811) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit zend_try/zend_catch is to handle Zend Exceptions not PHP exceptions - see [here](https://github.com/newrelic/newrelic-php-agent/blob/320ea571a11bc469d7d8179dfe51577b54df11df/agent/php_user_instrument.c#L17-L50) for more details. They were added in this [commit](https://github.com/newrelic/newrelic-php-agent/pull/580/commits/66eccf7d58d979737b7859308a95cddeb4f08f2e) to handle misbehaving PHP embed SAPI that threw Zend Exception when PHP code that was called via nr_php_call threw PHP Exception. PHP CLI or CGI SAPIs don’t throw Zend Exception when PHP code that was called threw PHP Exception and therefore zend_try/zend_catch is effectively a dead code. --- agent/php_call.c | 74 ++++++++++-------------------------------------- 1 file changed, 15 insertions(+), 59 deletions(-) diff --git a/agent/php_call.c b/agent/php_call.c index c8e0c874d..119271f66 100644 --- a/agent/php_call.c +++ b/agent/php_call.c @@ -10,56 +10,6 @@ #include "Zend/zend_exceptions.h" -/* - * zend_try family of macros entail the use of setjmp and longjmp, which can cause clobbering issues with - * non-primitive local variables. Abstracting these constructs into separate functions protects from this. - */ -#if ZEND_MODULE_API_NO >= ZEND_8_2_X_API_NO -static int nr_php_call_try_catch(zend_object* object, - zend_string* method_name, - zval* retval, - zend_uint param_count, - zval* param_values) { - /* - * With PHP 8.2, functions that do not exist will cause a fatal error to - * be thrown. `zend_call_method_if_exists` will attempt to call a function and - * silently fail if it does not exist - */ - int zend_result = FAILURE; - zend_try { - zend_result = zend_call_method_if_exists(object, method_name, retval, - param_count, param_values); - } - zend_catch { zend_result = FAILURE; } - zend_end_try(); - return zend_result; -} -#elif ZEND_MODULE_API_NO < ZEND_8_2_X_API_NO \ - && ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO -static int nr_php_call_try_catch(zval* object_ptr, - zval* fname, - zval* retval, - zend_uint param_count, - zval* param_values) { - /* - * With PHP8, `call_user_function_ex` was removed and `call_user_function` - * became the recommended function. - * According to zend internals documentation: - * As of PHP 7.1.0, the function_table argument is not used and should - * always be NULL. See for more details: - * https://www.phpinternalsbook.com/php7/internal_types/functions/callables.html - */ - int zend_result = FAILURE; - zend_try { - zend_result = call_user_function(EG(function_table), object_ptr, fname, - retval, param_count, param_values); - } - zend_catch { zend_result = FAILURE; } - zend_end_try(); - return zend_result; -} -#endif - zval* nr_php_call_user_func(zval* object_ptr, const char* function_name, zend_uint param_count, @@ -116,18 +66,24 @@ zval* nr_php_call_user_func(zval* object_ptr, } /* - * For PHP 8+, in the case of exceptions according to: - * https://www.php.net/manual/en/function.call-user-func.php - * Callbacks registered with functions such as call_user_func() and - * call_user_func_array() will not be called if there is an uncaught exception - * thrown in a previous callback. So if we call something that causes an - * exception, it will block us from future calls that use call_user_func or - * call_user_func_array and hence the need for a try/catch block. + * With PHP 8.2, functions that do not exist will cause a fatal error to + * be thrown. `zend_call_method_if_exists` will attempt to call a function and + * silently fail if it does not exist */ - zend_result = nr_php_call_try_catch(object, method_name, retval, param_count, param_values); + zend_result = zend_call_method_if_exists(object, method_name, retval, param_count, param_values); + #elif ZEND_MODULE_API_NO < ZEND_8_2_X_API_NO \ && ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO - zend_result = nr_php_call_try_catch(object_ptr, fname, retval, param_count, param_values); + /* + * With PHP8, `call_user_function_ex` was removed and `call_user_function` + * became the recommended function. + * According to zend internals documentation: + * As of PHP 7.1.0, the function_table argument is not used and should + * always be NULL. See for more details: + * https://www.phpinternalsbook.com/php7/internal_types/functions/callables.html + */ + zend_result = call_user_function(EG(function_table), object_ptr, fname, + retval, param_count, param_values); #else zend_result = call_user_function_ex(EG(function_table), object_ptr, fname, From 5640be38fdc8693efb0214029b3df0f418ea234b Mon Sep 17 00:00:00 2001 From: ZNeumann Date: Fri, 2 Feb 2024 14:46:24 -0700 Subject: [PATCH 49/56] feat(oapi): rework exception handling (#767) Removing the need to manual handling of dangling segments, because Zend calls all of the hooks we need. - adds a boolean argument to nr_php_error_record_exception to control whether we add the error to the current segment. This is needed because the OAPI context of when the above is called is no longer during a segment with an uncaught exception and was incorrectly adding the error to the root segment. - removes all language of "dangling segments". These no longer exist. Zend calls all of the necessary `fcall_end`'s, even when an exception is thrown. When this happens,` func_return_value` is a C `NULL` pointer which is distinct from a `NULL` (but valid) zval when there is no return value from a function. We use this `NULL` value to determine the presence of an uncaught exception. - no longer overwrites the exception hook; no longer stores a copy of exceptions locally - replace storing metadata in the segment, which was used to pair segments from func_begin with func_end, with logic that always creates segment in func_begin Mostly undoes the work of https://github.com/newrelic/newrelic-php-agent/pull/580 --------- Co-authored-by: Michal Nowacki Co-authored-by: bduranleau-nr <106178551+bduranleau-nr@users.noreply.github.com> Co-authored-by: Amber Sistla --- agent/fw_laravel.c | 1 + agent/fw_laravel_queue.c | 2 +- agent/fw_lumen.c | 1 + agent/fw_symfony4.c | 2 +- agent/php_agent.h | 36 -- agent/php_api.c | 13 +- agent/php_api.h | 22 - agent/php_api_internal.c | 1 - agent/php_error.c | 11 +- agent/php_error.h | 6 +- agent/php_execute.c | 409 +++--------------- agent/php_execute.h | 39 -- agent/php_internal_instrument.c | 2 +- agent/php_newrelic.h | 11 - agent/php_observer.c | 32 -- agent/php_observer.h | 25 -- agent/php_stacked_segment.c | 37 -- agent/php_stacked_segment.h | 300 ------------- axiom/nr_segment.h | 4 - axiom/nr_txn.c | 19 +- axiom/nr_txn.h | 8 +- axiom/tests/test_txn.c | 72 +-- ...create_payload_nested_caught_exception.php | 2 +- ...eate_payload_nested_uncaught_exception.php | 70 ++- .../api/other/test_end_transaction_nested.php | 7 - .../test_end_transaction_nested.php8.php | 106 ----- 26 files changed, 205 insertions(+), 1033 deletions(-) delete mode 100644 tests/integration/api/other/test_end_transaction_nested.php8.php diff --git a/agent/fw_laravel.c b/agent/fw_laravel.c index 2302a803c..921a8e027 100644 --- a/agent/fw_laravel.c +++ b/agent/fw_laravel.c @@ -619,6 +619,7 @@ NR_PHP_WRAPPER(nr_laravel5_exception_report) { nr_status_t st; st = nr_php_error_record_exception(NRPRG(txn), exception, priority, + true /* add to segment */, NULL /* use default prefix */, &NRPRG(exception_filters) TSRMLS_CC); diff --git a/agent/fw_laravel_queue.c b/agent/fw_laravel_queue.c index e1618f0ab..b4d5576e7 100644 --- a/agent/fw_laravel_queue.c +++ b/agent/fw_laravel_queue.c @@ -642,7 +642,7 @@ NR_PHP_WRAPPER(nr_laravel_queue_worker_process) { nr_php_error_record_exception( NRPRG(txn), exception_zval, NR_PHP_ERROR_PRIORITY_UNCAUGHT_EXCEPTION, - "Unhandled exception within Laravel Queue job: ", + true, "Unhandled exception within Laravel Queue job: ", &NRPRG(exception_filters) TSRMLS_CC); } diff --git a/agent/fw_lumen.c b/agent/fw_lumen.c index 36330fca2..4ea50e500 100644 --- a/agent/fw_lumen.c +++ b/agent/fw_lumen.c @@ -196,6 +196,7 @@ NR_PHP_WRAPPER(nr_lumen_exception) { int priority = nr_php_error_get_priority(E_ERROR); st = nr_php_error_record_exception(NRPRG(txn), exception, priority, + true /* add to segment */, NULL /* use default prefix */, &NRPRG(exception_filters) TSRMLS_CC); diff --git a/agent/fw_symfony4.c b/agent/fw_symfony4.c index 904668e0c..5b41a0c1b 100644 --- a/agent/fw_symfony4.c +++ b/agent/fw_symfony4.c @@ -56,7 +56,7 @@ NR_PHP_WRAPPER(nr_symfony4_exception) { } if (NR_SUCCESS - != nr_php_error_record_exception(NRPRG(txn), exception, priority, NULL, + != nr_php_error_record_exception(NRPRG(txn), exception, priority, true, NULL, &NRPRG(exception_filters) TSRMLS_CC)) { nrl_verbosedebug(NRL_TXN, "Symfony 4: unable to record exception"); } diff --git a/agent/php_agent.h b/agent/php_agent.h index 44c878d3d..deeabac79 100644 --- a/agent/php_agent.h +++ b/agent/php_agent.h @@ -776,42 +776,6 @@ nr_php_ini_entry_name_length(const zend_ini_entry* entry) { #define ZVAL_OR_ZEND_OBJECT(x) x #endif /* PHP8+ */ -/* - * Purpose : Ensure all dangling segments caused by an OAPI exception are closed - * before having an API act on the calling segment. - * - * Params : - * - * Returns : - */ -static inline void nr_php_api_ensure_current_segment() { -#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ - && !defined OVERWRITE_ZEND_EXECUTE_DATA - /* - * Before we call an API that depends on current segment, we need to ensure - * there isn't an outstanding uncaught exception that needs to be applied to - * dangling segments. If so, we need to apply the exception and close the - * stacked segments until we get to the segment that called the API. To do - * this, we detect the execute_data that called the API which is guaranteed to - * be what the current segment should be (otherwise, fcall_end would have - * closed normal segments and we would have taken care of any dangling - * segments already) and any segments stacked above it need to be closed due - * to an exception. - */ - - /* - * Get the function that called the API. prev_execute_data - * should never be null, but doublecheck for it anyway. - */ - if (NULL != EG(current_execute_data)->prev_execute_data) { - zval* prev_this = &EG(current_execute_data)->prev_execute_data->This; - - nr_php_observer_handle_uncaught_exception(prev_this); - } - -#endif -} - /* * Purpose : Wrap the native PHP json_decode function for those times when we * need a more robust JSON decoder than nro_create_from_json. diff --git a/agent/php_api.c b/agent/php_api.c index 723cd712d..f9dc5c903 100644 --- a/agent/php_api.c +++ b/agent/php_api.c @@ -97,8 +97,6 @@ PHP_FUNCTION(newrelic_notice_error) { priority = nr_php_error_get_priority(E_ERROR); } - nr_php_api_ensure_current_segment(); - if (NR_SUCCESS != nr_txn_record_error_worthy(NRPRG(txn), priority)) { nrl_debug(NRL_API, "newrelic_notice_error: a higher severity error has already been " @@ -162,7 +160,7 @@ PHP_FUNCTION(newrelic_notice_error) { if (exc) { if (NR_SUCCESS == nr_php_error_record_exception( - NRPRG(txn), exc, priority, "Noticed exception ", NULL TSRMLS_CC)) { + NRPRG(txn), exc, priority, true, "Noticed exception ", NULL TSRMLS_CC)) { RETURN_TRUE; } else { nrl_debug(NRL_API, "newrelic_notice_error: invalid exception argument"); @@ -174,7 +172,7 @@ PHP_FUNCTION(newrelic_notice_error) { char* buf = nr_strndup(errormsgstr, errormsglen); char* stack_json = nr_php_backtrace_to_json(NULL TSRMLS_CC); - nr_txn_record_error(NRPRG(txn), priority, buf, errclass, stack_json); + nr_txn_record_error(NRPRG(txn), priority, true, buf, errclass, stack_json); nr_free(buf); nr_free(stack_json); @@ -306,8 +304,6 @@ PHP_FUNCTION(newrelic_end_transaction) { } } - nr_php_api_ensure_current_segment(); - ret = nr_php_txn_end((0 != ignore), 0 TSRMLS_CC); if (NR_SUCCESS == ret) { nrl_debug(NRL_API, "transaction completed by API"); @@ -744,7 +740,6 @@ PHP_FUNCTION(newrelic_add_custom_parameter) { obj = nr_php_api_zval_to_attribute_obj(zzvalue TSRMLS_CC); if (obj) { - nr_php_api_ensure_current_segment(); rv = nr_txn_add_user_custom_parameter(NRPRG(txn), key, obj); } @@ -1238,7 +1233,6 @@ PHP_FUNCTION(newrelic_set_user_attributes) { RETURN_FALSE; } - nr_php_api_ensure_current_segment(); rv = nr_php_api_add_custom_parameter_string(NRPRG(txn), "user", userstr, userlen); if (NR_FAILURE == rv) { @@ -1266,7 +1260,6 @@ static nr_status_t nr_php_api_add_custom_span_attribute(const char* keystr, char* key = NULL; nr_segment_t* current; - nr_php_api_ensure_current_segment(); current = nr_txn_get_current_segment(NRPRG(txn), NULL); if (!current) { return NR_FAILURE; @@ -1530,7 +1523,6 @@ PHP_FUNCTION(newrelic_get_linking_metadata) { } if (nrlikely(NRPRG(txn))) { - nr_php_api_ensure_current_segment(); trace_id = nr_txn_get_current_trace_id(NRPRG(txn)); span_id = nr_txn_get_current_span_id(NRPRG(txn)); @@ -1573,7 +1565,6 @@ PHP_FUNCTION(newrelic_get_trace_metadata) { } if (nrlikely(NRPRG(txn))) { - nr_php_api_ensure_current_segment(); trace_id = nr_txn_get_current_trace_id(NRPRG(txn)); if (trace_id) { nr_php_add_assoc_string(return_value, "trace_id", trace_id); diff --git a/agent/php_api.h b/agent/php_api.h index 34fc29063..eacf96ce7 100644 --- a/agent/php_api.h +++ b/agent/php_api.h @@ -7,28 +7,6 @@ #ifndef PHP_API_HDR #define PHP_API_HDR -/* - * Recommendations for API calls when using OAPI instrumentation and PHP 8+ - * - * Dangling segments: - * With the use of Observer API we have the possibility of dangling segments - * that can occur due to an exception occurring. In the normal course of - * events, nr_php_observer_fcall_begin starts segments and - * nr_php_observer_fcall_end keeps/discards/ends segments. However, in the case - * of an uncaught exception, nr_php_observer_fcall_end is never called and - * therefore, the logic to keep/discard/end the segment doesn't automatically - * get initiated which can lead to dangling stacked segments. - * - * However, certain agent API calls need to be associated with particular - * segments. - * - * To handle this , dangling exception cleanup is initiated by the following - * call: nr_php_api_ensure_current_segment(); - * - * ANY API call that depends on the current segment needs to use this function - * to ensure the API uses the correct segment. - */ - extern void nr_php_api_add_supportability_metric(const char* name TSRMLS_DC); extern void nr_php_api_error(const char* format, ...) NRPRINTFMT(1); diff --git a/agent/php_api_internal.c b/agent/php_api_internal.c index 3ad4a70bf..86776d029 100644 --- a/agent/php_api_internal.c +++ b/agent/php_api_internal.c @@ -49,7 +49,6 @@ PHP_FUNCTION(newrelic_get_request_metadata) { } array_init(return_value); - nr_php_api_ensure_current_segment(); outbound_headers = nr_header_outbound_request_create( NRPRG(txn), nr_txn_get_current_segment(NRPRG(txn), NULL)); diff --git a/agent/php_error.c b/agent/php_error.c index bf44d1516..d59858147 100644 --- a/agent/php_error.c +++ b/agent/php_error.c @@ -273,7 +273,7 @@ PHP_FUNCTION(newrelic_exception_handler) { nr_php_error_record_exception( NRPRG(txn), exception, NR_PHP_ERROR_PRIORITY_UNCAUGHT_EXCEPTION, - "Uncaught exception ", &NRPRG(exception_filters) TSRMLS_CC); + true, "Uncaught exception ", &NRPRG(exception_filters) TSRMLS_CC); /* * Finally, we need to generate an E_ERROR to match what PHP would have done * if this handler wasn't installed. Happily, PHP exposes an API function @@ -603,8 +603,8 @@ void nr_php_error_cb(int type, stack_json = nr_php_backtrace_to_json(0 TSRMLS_CC); errclass = get_error_type_string(type); - nr_txn_record_error(NRPRG(txn), nr_php_error_get_priority(type), msg, - errclass, stack_json); + nr_txn_record_error(NRPRG(txn), nr_php_error_get_priority(type), true, + msg, errclass, stack_json); /* * Error Fingerprinting Callback @@ -645,6 +645,7 @@ void nr_php_error_cb(int type, nr_status_t nr_php_error_record_exception(nrtxn_t* txn, zval* exception, int priority, + bool add_to_current_segment, const char* prefix, zend_llist* filters TSRMLS_DC) { zend_class_entry* ce; @@ -715,7 +716,9 @@ nr_status_t nr_php_error_record_exception(nrtxn_t* txn, stack_json); } - nr_txn_record_error(NRPRG(txn), priority, error_message, klass, stack_json); + nr_txn_record_error(NRPRG(txn), priority, + add_to_current_segment, + error_message, klass, stack_json); nr_free(error_message); nr_free(file); diff --git a/agent/php_error.h b/agent/php_error.h index af3726dda..98c06708a 100644 --- a/agent/php_error.h +++ b/agent/php_error.h @@ -124,9 +124,10 @@ extern void nr_php_error_install_exception_handler(TSRMLS_D); * Params : 1. The transaction to record the error in. * 2. The exception to record an error for. * 3. The error priority to use. - * 4. A prefix to prepend to the error message before the class name. + * 4. Whether we want to add the error to the current segment. + * 5. A prefix to prepend to the error message before the class name. * If NULL, then the default "Exception " will be used. - * 5. The exception filters to apply. + * 6. The exception filters to apply. * Typically, &NRPRG (exception_filters) or NULL to disable * exception filtering. * @@ -137,6 +138,7 @@ extern void nr_php_error_install_exception_handler(TSRMLS_D); extern nr_status_t nr_php_error_record_exception(nrtxn_t* txn, zval* exception, int priority, + bool add_to_current_segment, const char* prefix, zend_llist* filters TSRMLS_DC); diff --git a/agent/php_execute.c b/agent/php_execute.c index fbd69b82e..c02a033f2 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -96,11 +96,6 @@ static void nr_php_show_exec_return(NR_EXECUTE_PROTO TSRMLS_DC); static int nr_php_show_exec_indentation(TSRMLS_D); static void nr_php_show_exec(NR_EXECUTE_PROTO TSRMLS_DC); -#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ - && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ -static void nr_php_show_oapi_metadata(nr_php_execute_metadata_t* metadata, - bool wraprec_exists); -#endif /* * Purpose: Enable monitoring on specific functions in the framework. @@ -696,27 +691,6 @@ static void nr_php_show_exec(NR_EXECUTE_PROTO TSRMLS_DC) { } } -#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ - && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ -/* - * Show the metadata values associated with a dangling segment. - * This is called only with OAPI/PHP8+ when an exception leaves a stacked - * segment dangling due to nr_php_observer_fcall_end not getting called when an - * unhandled exception occurs. - */ -static void nr_php_show_oapi_metadata(nr_php_execute_metadata_t* metadata, - bool wraprec_exists) { - char* function_name = metadata->function ? ZSTR_VAL(metadata->function) : "?"; - char* class_name = metadata->scope ? ZSTR_VAL(metadata->scope) : "?"; - char* file_name = metadata->filepath ? ZSTR_VAL(metadata->filepath) : "?"; - char* wraprec_indicator = wraprec_exists ? "exists" : ""; - nrl_verbosedebug(NRL_AGENT, - "oapi metadata: scope={%s} function={%s} filename={%s} " - "lineno={%d} wraprec={%s}", - class_name, function_name, file_name, - metadata->function_lineno, wraprec_indicator); -} -#endif /* * Show the return value, assuming that there is one. * The return value is an attribute[sic] of the caller site, @@ -1229,13 +1203,8 @@ static void nr_php_execute_metadata_metric( * * Params : 1. A pointer to the metadata. */ -#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ - && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ -void nr_php_execute_metadata_release(nr_php_execute_metadata_t* metadata) { -#else static inline void nr_php_execute_metadata_release( nr_php_execute_metadata_t* metadata) { -#endif #if ZEND_MODULE_API_NO >= ZEND_7_0_X_API_NO if (NULL != metadata->scope) { @@ -1302,32 +1271,6 @@ static inline void nr_php_execute_segment_end( if (create_metric || (duration >= NR_PHP_PROCESS_GLOBALS(expensive_min)) || nr_vector_size(stacked->metrics) || stacked->id || stacked->attributes || stacked->error) { - /* - * Non-OAPI segments are able to utilize metadata that is declared in the - * call stack. OAPI doesn't have this luxury since we have to handle begin - * and end func calls separately. Because of this, metadata now resides as - * a pointer in the stacked segment. We must extract data from it BEFORE we - * move the stacked segment to the heap; otherwise, it gets deallocated - * before we can use it. - */ -#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ - && !defined OVERWRITE_ZEND_EXECUTE_DATA - - nr_php_execute_segment_add_metric(stacked, metadata, create_metric); - - /* - * Check if code level metrics are enabled in the ini. - * If they aren't, exit and don't create any metrics. - * We need to get the CLM from metadata before we move it to the heap - * because once it is moved to the heap, the metadata on the segment is - * freed. - */ - if (NRINI(code_level_metrics_enabled)) { - nr_php_execute_segment_add_code_level_metrics(stacked, metadata); - } - nr_segment_t* s = nr_php_stacked_segment_move_to_heap(stacked TSRMLS_CC); - -#else nr_segment_t* s = nr_php_stacked_segment_move_to_heap(stacked TSRMLS_CC); nr_php_execute_segment_add_metric(s, metadata, create_metric); @@ -1340,8 +1283,6 @@ static inline void nr_php_execute_segment_end( if (NRINI(code_level_metrics_enabled)) { nr_php_execute_segment_add_code_level_metrics(s, metadata); } -#endif - #endif nr_segment_end(&s); @@ -1418,7 +1359,7 @@ static void nr_php_execute_enabled(NR_EXECUTE_PROTO TSRMLS_DC) { * they'll see with and without an exception handler installed. */ nr_php_error_record_exception( - NRPRG(txn), exception, nr_php_error_get_priority(E_ERROR), + NRPRG(txn), exception, nr_php_error_get_priority(E_ERROR), true, "Uncaught exception ", &NRPRG(exception_filters) TSRMLS_CC); } @@ -1802,169 +1743,6 @@ void nr_php_user_instrumentation_from_opcache(TSRMLS_D) { #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP8+ */ -static inline void nr_php_observer_exception_segments_end( - zval* exception, - zval* execute_data_this) { - nr_segment_t* segment = NULL; - - if (NULL == exception || NULL == execute_data_this) { - return; - } - - if (NULL == NRPRG(txn)) { - return; - } - - segment = NRTXN(force_current_segment); - while ((NULL != segment) - && (NRTXN(segment_root) != NRTXN(force_current_segment))) { - nr_php_execute_metadata_t* metadata = segment->metadata; - if (metadata->execute_data_this == execute_data_this) { - break; - } - nr_php_observer_segment_end(NRPRG(uncaught_exception)); - segment = NRTXN(force_current_segment); - } -} - -void nr_php_observer_segment_end(zval* exception) { - nr_segment_t* segment = NULL; - nruserfn_t* wraprec = NULL; - nrtime_t txn_start_time = 0; - - /* - * If we have a stacked segment that missed an OAPI func_end call, add an - * exception (if not null) and close then get the current segment and return - * if null. The segment would only have been created if we are recording and - * if wraprec is set or if tt is greater than 0. - */ - - if (NULL == NRPRG(txn)) { - return; - } - txn_start_time = nr_txn_start_time(NRPRG(txn)); - if (NULL != exception) { - nr_status_t status; - - status = nr_php_error_record_exception_segment( - NRPRG(txn), exception, &NRPRG(exception_filters) TSRMLS_CC); - - if (NR_FAILURE == status) { - nrl_verbosedebug(NRL_AGENT, "%s: unable to record exception on segment", - __func__); - } - } - segment = NRTXN(force_current_segment); - if (NULL != segment) { - bool create_metric = false; - wraprec = (nruserfn_t*)(segment->wraprec); - if (NULL != wraprec) { - create_metric = wraprec->create_metric; - int zcaught - = nr_zend_call_oapi_special_clean(wraprec, segment, NULL, NULL); - if (nrunlikely(zcaught)) { - zend_bailout(); - } - } - - /* - * During nr_zend_call_oapi_special_clean, the transaction may have been - * ended and/or a new transaction may have started. To detect this, we - * compare the currently active transaction's start time with the - * transaction start time we saved before. - * - * Just comparing the transaction pointer is not enough, as a newly - * started transaction might actually obtain the same address as a - * transaction freed before. - */ - if (nrunlikely(nr_txn_start_time(NRPRG(txn)) != txn_start_time)) { - nrl_verbosedebug( - NRL_AGENT, "%s txn ended and/or started while in a wrapped function", - __func__); - return; - } - - /* - * We are only here because there is a dangling segment which means - * nr_php_observer_fcall_end didn't get called due to unhandled - * exception(s). Decrement the php_cur_stack_depth counter properly. - */ - if (nrunlikely( - NR_PHP_PROCESS_GLOBALS(special_flags).show_execute_returns)) { - nrl_verbosedebug(NRL_AGENT, - "Stack depth: %d before OAPI function exiting via %s", - NRPRG(php_cur_stack_depth), __func__); - nr_php_show_oapi_metadata(segment->metadata, (NULL != wraprec)); - } - NRPRG(php_cur_stack_depth) -= 1; - nr_php_execute_segment_end(segment, segment->metadata, create_metric); - } - return; -} - -void nr_php_observer_handle_uncaught_exception(zval* current_this) { - if (NULL == NRPRG(uncaught_exeption_execute_data_this)) { - return; - } - /* - * A pending uncaught exception for this txn exists, so we need to close - * stacked segments to get to the correct stacked segment to add the noticed - * error to. - */ - if (current_this != NRPRG(uncaught_exeption_execute_data_this)) { - nr_php_observer_exception_segments_end(NRPRG(uncaught_exception), - current_this); - - php_observer_clear_uncaught_exception_globals(); - } -} - -void php_observer_handle_exception_hook(zval* exception, zval* exception_this) { - /* - * The issue is, with OAPI, only the most recent exception is exposed in the - * error handler. If function `a` calls function `b` calls function `c` calls - * function `d` which throws an exception that `c` catches and that `c' then - * throws an exception that `b` catches but then b throws an exception that is - * uncaught, only the latest exception thrown by `b` gets passed to the error - * handler. Additonally, the fcall_end handler does not get called for - * functions which have uncaught exceptions. - * - * To solve this, this function gets called with every exception regardless of - * whether it is caught or not. We save the most recent exception and the - * unique `this` pointer of the execute_data it is associated with so we can - * use it if we need to end stacked segments. If another exception is - * triggered while our saved exception is not null, we check if we need to end - * stacked segments and then save the new exception. - */ - - if (nrunlikely(NULL == exception || NULL == exception_this)) { - return; - } - - if (NULL != NRPRG(uncaught_exeption_execute_data_this)) { - /* - * A pending uncaught exception for this txn exists, see if we need to close - * segments. We determine this by comparing the `execute_data_this` pointer - * in the `metadata` of the top stacked segment with the `This` pointer of - * the currently executing segment. If the pointers match, then the - * execute_data is still executing and could theoretically still catch it. - * If the pointers don't match, then the previous exception caused the - * fcall_end to be skipped, so we need to close those stacked segments - * manually until we arrive at the correct stacked segment that corresponds - * to exception we just recieved. This will close all necessary stacked - * segments. If the previous exception had been caught anywhere along the - * calling chain (by an fcall_end happening for a function) the segments - * would have been closed and the exception cleared. - */ - if (exception_this != NRPRG(uncaught_exeption_execute_data_this)) { - nr_php_observer_exception_segments_end(NRPRG(uncaught_exception), - exception_this); - } - php_observer_clear_uncaught_exception_globals(); - } - php_observer_set_uncaught_exception_globals(exception, exception_this); -} - static void nr_php_observer_attempt_call_cufa_handler(NR_EXECUTE_PROTO) { NR_UNUSED_FUNC_RETURN_VALUE; if (NULL == execute_data->prev_execute_data) { @@ -2062,7 +1840,6 @@ static void nr_php_instrument_func_begin(NR_EXECUTE_PROTO) { nruserfn_t* wraprec = NULL; nrtime_t txn_start_time = 0; int zcaught = 0; - nr_php_execute_metadata_t* metadata = NULL; NR_UNUSED_FUNC_RETURN_VALUE; if (NULL == NRPRG(txn)) { @@ -2095,87 +1872,28 @@ static void nr_php_instrument_func_begin(NR_EXECUTE_PROTO) { nr_php_observer_attempt_call_cufa_handler(NR_EXECUTE_ORIG_ARGS); } wraprec = nr_php_get_wraprec(execute_data->func); + /* - * If there is custom instrumentation or tt detail is more than 0, start the - * segment. - */ - if ((NULL == wraprec) && !(NRINI(tt_detail) && NR_OP_ARRAY->function_name)) { - return; - } - /* - * Check if it's a custom error handler. Even with some custom error - * handlers, fcall might not get called. But we don't need to wait for - * fcall_end to put the error anyway. It can be done earlier in - * fcall_begin. Here, we are doing before the segment call so the error gets - * on the correct stacked segment. + * Check if it's a custom error handler. We don't need to wait for + * fcall_end to record the error to the transaction; it can be done earlier + * in fcall_begin. However, we must wait until fcall_end to add the error + * to any possible segment(s), as at this point we do not know when it + * will be caught. */ if (NULL != wraprec && wraprec->is_exception_handler) { /* * Before starting the error handler segment, put the error it handled on - * the segment that called it. The choice of E_ERROR for the error level - * is basically arbitrary, but matches the error level PHP uses if there + * the transaction. The choice of E_ERROR for the error level is + * basically arbitrary, but matches the error level PHP uses if there * isn't an exception handler, so this should give more consistency for * the user in terms of what they'll see with and without an exception * handler installed. */ - nr_status_t status; - if (NULL != NRPRG(uncaught_exception)) { - status = nr_php_error_record_exception_segment( - NRPRG(txn), NRPRG(uncaught_exception), - &NRPRG(exception_filters) TSRMLS_CC); - - if (NR_FAILURE == status) { - nrl_verbosedebug(NRL_AGENT, "%s: unable to record exception on segment", - __func__); - } - zval* exception - = nr_php_get_user_func_arg(1, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); - nr_php_error_record_exception( - NRPRG(txn), exception, nr_php_error_get_priority(E_ERROR), - "Uncaught exception ", &NRPRG(exception_filters) TSRMLS_CC); - php_observer_clear_uncaught_exception_globals(); - } - } else if (NULL != NRPRG(txn)) { - /* - * Check if NRPRG(uncaught_exception) exists because if it's not handled, - * we'll parent the new segment on the wrong stacked segment. Close off - * all dangling segments caused by an exception before starting a new - * segment. - */ - - if (nrunlikely(NULL != NRPRG(uncaught_exception))) { - /* - * First check if it's the root because obviously, prev_execute won't - * exist. - */ - if (NRTXN(segment_root) != NRTXN(force_current_segment)) { - /* - * Get the current segment if it exists. - */ - nr_segment_t* exception_segment = NRTXN(force_current_segment); - if (NULL != exception_segment) { - /* - * If the metadata info doesn't match the previous callers This, - * then we know the uncaught exception occurred which caused the - * fcall_end function to not be called. Clean up dangling stacked - * segments. - */ - nr_php_execute_metadata_t* md = exception_segment->metadata; - if ((NULL != md) - && (md->execute_data_this - != &execute_data->prev_execute_data->This)) { - /* - * Close all previous segments, attaching the uncaught exception - * as necessary. - */ - nr_php_observer_exception_segments_end( - NRPRG(uncaught_exception), - &execute_data->prev_execute_data->This); - } - } - php_observer_clear_uncaught_exception_globals(); - } - } + zval* exception + = nr_php_get_user_func_arg(1, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + nr_php_error_record_exception( + NRPRG(txn), exception, nr_php_error_get_priority(E_ERROR), false, + "Uncaught exception ", &NRPRG(exception_filters) TSRMLS_CC); } segment = nr_php_stacked_segment_init(segment); @@ -2184,13 +1902,6 @@ static void nr_php_instrument_func_begin(NR_EXECUTE_PROTO) { return; } - nr_php_execute_metadata_init(segment->metadata, NR_OP_ARRAY); - metadata = segment->metadata; - metadata->execute_data_this = &execute_data->This; - /* - * Metadata deinit is handled when the segment is destroyed. - */ - if (NULL == wraprec) { return; } @@ -2238,7 +1949,7 @@ static void nr_php_instrument_func_end(NR_EXECUTE_PROTO) { nr_segment_t* segment = NULL; nruserfn_t* wraprec = NULL; bool create_metric = false; - nr_php_execute_metadata_t* metadata = NULL; + nr_php_execute_metadata_t metadata = {0}; nrtime_t txn_start_time = 0; if (NULL == NRPRG(txn)) { @@ -2251,14 +1962,11 @@ static void nr_php_instrument_func_end(NR_EXECUTE_PROTO) { */ if (nrunlikely(OP_ARRAY_IS_A_FILE(NR_OP_ARRAY))) { nr_php_execute_file(NR_OP_ARRAY, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); - php_observer_clear_uncaught_exception_globals(); return; } /* - * Get the current segment and return if null. The segment would only have - * been created if we are recording and if wraprec is set or if tt is greater - * than 0. + * Get the current segment and return if null. */ segment = NRTXN(force_current_segment); if (nrunlikely(NULL == segment)) { @@ -2268,39 +1976,34 @@ static void nr_php_instrument_func_end(NR_EXECUTE_PROTO) { */ return; } - if (nrunlikely(NULL == segment->metadata)) { - /* - * If this value isn't set, it is either the root segment not a stacked - * segment set or not set by the instrument_begin_func, but in all we we - * should only ignore it. - */ + if (nrunlikely(NRPRG(txn)->segment_root == segment)) { return; } - /* - * If the metadata info doesn't match, an uncaught exception occurred which - * doesn't call fcall_end. - */ - metadata = segment->metadata; - if ((metadata->execute_data_this != &execute_data->This)) { + + wraprec = segment->wraprec; + + if (wraprec && wraprec->is_exception_handler) { /* - * Close all previous segments, attaching the uncaught exception as - * necessary. + * An exception handler sets the transaction exception in fcall_begin + * and does not have a return value, like an fcall_end during an + * uncaught exception. This code path is here to simplify and + * explicitly enumerate the possible cases. */ - nr_php_observer_exception_segments_end(NRPRG(uncaught_exception), - &execute_data->This); - php_observer_clear_uncaught_exception_globals(); - segment = NRTXN(force_current_segment); - if (NULL == segment) { - return; - } - metadata = segment->metadata; - if (nrunlikely(metadata->execute_data_this != &execute_data->This)) { - /* - * Sanity check. - * If the pointers still aren't equal, let's exit. - * - */ - return; + } else if (NULL == nr_php_get_return_value(NR_EXECUTE_ORIG_ARGS)) { + /* + * Having no return value (and not being an exception handler) indicates + * that this segment had an uncaught exception. We want to add that + * exception to the segment. + */ + zval exception; + ZVAL_OBJ(&exception, EG(exception)); + nr_status_t status = nr_php_error_record_exception_segment( + NRPRG(txn), &exception, + &NRPRG(exception_filters)); + + if (NR_FAILURE == status) { + nrl_verbosedebug(NRL_AGENT, "%s: unable to record exception on segment", + __func__); } } @@ -2314,7 +2017,6 @@ static void nr_php_instrument_func_end(NR_EXECUTE_PROTO) { * Check if we have special instrumentation for this function or if the user * has specifically requested it. */ - wraprec = segment->wraprec; if (NULL != wraprec) { /* @@ -2322,11 +2024,27 @@ static void nr_php_instrument_func_end(NR_EXECUTE_PROTO) { */ create_metric = wraprec->create_metric; - zcaught = nr_zend_call_orig_execute_special(wraprec, segment, + /* + * A NULL return value ptr means that there was an uncaught exception + * and therefore we want to call the 'clean' function type + */ + if (NULL != nr_php_get_return_value(NR_EXECUTE_ORIG_ARGS)) { + zcaught = nr_zend_call_orig_execute_special(wraprec, segment, + NR_EXECUTE_ORIG_ARGS); + } else { + zcaught = nr_zend_call_oapi_special_clean(wraprec, segment, NR_EXECUTE_ORIG_ARGS); + } if (nrunlikely(zcaught)) { zend_bailout(); } + } else if (!NRINI(tt_detail) || !(NR_OP_ARRAY->function_name)) { + /* + * If there is no custom instrumentation and tt detail is not more than 0, + * do not record the segment + */ + nr_php_stacked_segment_deinit(segment); + return; } /* * During nr_zend_call_orig_execute_special, the transaction may have been @@ -2346,13 +2064,9 @@ static void nr_php_instrument_func_end(NR_EXECUTE_PROTO) { return; } - nr_php_execute_segment_end(segment, segment->metadata, create_metric); - /* - * Clear the uncaught exception globals. This will also take care of the case - * of an exception that was thrown for this segment but then was caught as - * evidenced by the fact that we got to fcall_end. - */ - php_observer_clear_uncaught_exception_globals(); + nr_php_execute_metadata_init(&metadata, NR_OP_ARRAY); + nr_php_execute_segment_end(segment, &metadata, create_metric); + nr_php_execute_metadata_release(&metadata); return; } @@ -2402,8 +2116,7 @@ void nr_php_observer_fcall_end(zend_execute_data* execute_data, * nr_php_execute * nr_php_execute_show */ - if (nrunlikely((NULL == execute_data)) - || nrunlikely((NULL == func_return_value))) { + if (nrunlikely(NULL == execute_data)) { return; } diff --git a/agent/php_execute.h b/agent/php_execute.h index 2fc3edb3b..dd446a594 100644 --- a/agent/php_execute.h +++ b/agent/php_execute.h @@ -74,7 +74,6 @@ typedef struct { zend_string* function; zend_string* filepath; uint32_t function_lineno; - zval* execute_data_this; #else zend_op_array* op_array; #endif /* PHP7 */ @@ -109,42 +108,4 @@ extern void nr_framework_create_metric(TSRMLS_D); extern void nr_php_user_instrumentation_from_opcache(TSRMLS_D); -extern void nr_php_observer_handle_uncaught_exception(zval* exception_this); - -#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ - && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP8+ */ -static inline void php_observer_clear_uncaught_exception_globals() { - /* - * Clear the uncaught exception global variables. - */ - if (NULL != NRPRG(uncaught_exception)) { - nr_php_zval_free(&NRPRG(uncaught_exception)); - } - NRPRG(uncaught_exeption_execute_data_this) = NULL; -} - -static inline void php_observer_set_uncaught_exception_globals( - zval* exception, - zval* exception_this) { - /* - * Set the uncaught exception global variables - */ - if (nrunlikely(NULL != NRPRG(uncaught_exception))) { - return; - } - NRPRG(uncaught_exception) = nr_php_zval_alloc(); - ZVAL_DUP(NRPRG(uncaught_exception), exception); - NRPRG(uncaught_exeption_execute_data_this) = exception_this; -} - -/* - * Purpose : Release any cached metadata. - * - * Params : 1. A pointer to the metadata. - */ -extern void nr_php_execute_metadata_release( - nr_php_execute_metadata_t* metadata); - -#endif - #endif /* PHP_EXECUTE_HDR */ diff --git a/agent/php_internal_instrument.c b/agent/php_internal_instrument.c index f6733669f..15c43dcdd 100644 --- a/agent/php_internal_instrument.c +++ b/agent/php_internal_instrument.c @@ -331,7 +331,7 @@ static void record_mysql_error(TSRMLS_D) { errdup = nr_strndup(errormsgstr, errormsglen); stack_json = nr_php_backtrace_to_json(0 TSRMLS_CC); - nr_txn_record_error(NRPRG(txn), errprio, errdup, "MysqlError", stack_json); + nr_txn_record_error(NRPRG(txn), errprio, true, errdup, "MysqlError", stack_json); nr_free(errdup); nr_free(stack_json); diff --git a/agent/php_newrelic.h b/agent/php_newrelic.h index 28f28199e..6df58317c 100644 --- a/agent/php_newrelic.h +++ b/agent/php_newrelic.h @@ -480,17 +480,6 @@ int php_cur_stack_depth; /* Total current depth of PHP stack, measured in PHP nrphpcufafn_t cufa_callback; /* The current call_user_func_array callback, if any */ - -/* - * The exception happens whether the exception was caught or not. Keep track of - * the execute_data frame to determine if it was uncaught and so we can compare - * to determine if we need to propagate the exception or not. */ -zval* uncaught_exception; /* The last exception that occurred. Does need to be - freed. */ -zval* uncaught_exeption_execute_data_this; /* Keep track of the execute data - that the last exception occurred - on. Does not need to be freed. */ - /* * We instrument database connection constructors and store the instance * information in a hash keyed by a string containing the connection resource diff --git a/agent/php_observer.c b/agent/php_observer.c index a157d45b2..e0d651fc0 100644 --- a/agent/php_observer.c +++ b/agent/php_observer.c @@ -89,8 +89,6 @@ static zend_observer_fcall_handlers nr_php_fcall_register_handlers( void nr_php_observer_no_op(zend_execute_data* execute_data NRUNUSED){}; -static void (*original_zend_throw_exception_hook)(zend_object* ex); - void nr_php_observer_minit() { /* * Register the Observer API handlers. @@ -98,13 +96,6 @@ void nr_php_observer_minit() { zend_observer_fcall_register(nr_php_fcall_register_handlers); zend_observer_error_register(nr_php_error_cb); - /* - * Overwrite the exception_hook. Note: This ONLY notifies when an exception - * is thrown. It gives no indication if that exception was subsequently - * caught or not. - */ - original_zend_throw_exception_hook = zend_throw_exception_hook; - zend_throw_exception_hook = nr_throw_exception_hook; /* * For Observer API with PHP 8+, we no longer need to ovewrwrite the zend * execute hook. orig_execute is called various ways in various places, so @@ -113,27 +104,4 @@ void nr_php_observer_minit() { NR_PHP_PROCESS_GLOBALS(orig_execute) = nr_php_observer_no_op; } -void nr_throw_exception_hook(zend_object* exception) { - zval new_exception; - zval* exception_zval = NULL; - - /* - * Don't track the exception if we don't have a valid txn. - */ - if (NULL != NRPRG(txn)) { - /* - * Since PHP 7, EG(exception) is stored as a zend_object, and is therefore - * only wrapped in a zval when it actually needs to be. - */ - ZVAL_OBJ(&new_exception, exception); - exception_zval = &new_exception; - - php_observer_handle_exception_hook(exception_zval, - &(EG(current_execute_data)->This)); - } - if (original_zend_throw_exception_hook != NULL) { - original_zend_throw_exception_hook(exception); - } -} - #endif diff --git a/agent/php_observer.h b/agent/php_observer.h index 4225c4678..6a80873cb 100644 --- a/agent/php_observer.h +++ b/agent/php_observer.h @@ -75,31 +75,6 @@ void nr_php_observer_fcall_begin(zend_execute_data* execute_data); void nr_php_observer_fcall_end(zend_execute_data* execute_data, zval* func_return_value); -/* - * Purpose : Overwrite the php exception hook. - * - * Params : zend_object* exception : The exception to monitor. - */ -void nr_throw_exception_hook(zend_object* exception); - -/* - * Purpose : Monitor the exception to take care of dangling segments, if needed. - * - * Params : 1) zval* exception : The exception to monitor. - * 2) zval* execute_data_this: The pointer to the unique execute data - * that the exception was thrown from. - */ -void php_observer_handle_exception_hook(zval* exception_zval, - zval* execute_data_this); - -/* - * Purpose : End a stacked segment. If an exception is provided, add it before - * exiting. - * - * Params : zval* exception : The exception to add to the segment. If NULL, no - * exception is recorded on the segment. - */ -extern void nr_php_observer_segment_end(zval* exception); #endif /* PHP8+ */ diff --git a/agent/php_stacked_segment.c b/agent/php_stacked_segment.c index 1c1c42c06..f200e0eb3 100644 --- a/agent/php_stacked_segment.c +++ b/agent/php_stacked_segment.c @@ -34,13 +34,6 @@ nr_segment_t* nr_php_stacked_segment_init(nr_segment_t* stacked TSRMLS_DC) { if (NULL == stacked) { return NULL; } - - stacked->metadata = nr_calloc(1, sizeof(nr_php_execute_metadata_t)); - if (NULL == stacked->metadata) { - nr_free(stacked); - return NULL; - } - #endif stacked->txn = NRPRG(txn); @@ -66,8 +59,6 @@ void nr_php_stacked_segment_deinit(nr_segment_t* stacked TSRMLS_DC) { /* * This is allocated differently for OAPI and hence needs to be freed. */ - nr_php_execute_metadata_release(stacked->metadata); - nr_free(stacked->metadata); nr_free(stacked); #endif } @@ -79,37 +70,11 @@ void nr_php_stacked_segment_unwind(TSRMLS_D) { while (NRTXN(force_current_segment) && (NRTXN(segment_root) != NRTXN(force_current_segment))) { -#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ - && !defined OVERWRITE_ZEND_EXECUTE_DATA - /* - * With OAPI, we need to gracefully close off the stacked segments with - * their naming contexts. - */ - nr_php_observer_segment_end(NRPRG(uncaught_exception)); - -#else nr_segment_t* stacked = NRTXN(force_current_segment); nr_segment_t* segment = nr_php_stacked_segment_move_to_heap(stacked TSRMLS_CC); nr_segment_end(&segment); - -#endif } -#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ - && !defined OVERWRITE_ZEND_EXECUTE_DATA - /* - * If OAPI we need to record the uncaught exception (if it exists) on the root - * segment as well. - */ - if (NULL != NRPRG(uncaught_exception)) { - if (NRTXN(segment_root) == NRTXN(force_current_segment)) { - nr_php_error_record_exception_segment( - NRPRG(txn), NRPRG(uncaught_exception), - &NRPRG(exception_filters) TSRMLS_CC); - } - } - php_observer_clear_uncaught_exception_globals(); -#endif } nr_segment_t* nr_php_stacked_segment_move_to_heap( @@ -141,8 +106,6 @@ nr_segment_t* nr_php_stacked_segment_move_to_heap( /* * This is allocated differently for OAPI and hence needs to be freed. */ - nr_php_execute_metadata_release(stacked->metadata); - nr_free(stacked->metadata); nr_free(stacked); #endif diff --git a/agent/php_stacked_segment.h b/agent/php_stacked_segment.h index c990a9c72..b85a6c3c9 100644 --- a/agent/php_stacked_segment.h +++ b/agent/php_stacked_segment.h @@ -252,306 +252,6 @@ * * Also note that this only works with segments on the default parent stack. * Stacked segments cannot be used to model async segments. - * - * Dangling segments: - * With the use of Observer API we have the possibility of dangling segments. In - * the normal course of events, the above scenario shows - * nr_php_observer_fcall_begin starting segments and nr_php_observer_fcall_end - * keeping/discarding/ending segments. However, in the case of an uncaught - * exception, nr_php_observer_fcall_end is never called and therefore, the logic - * to keep/discard/end the segment doesn't automatically get initiated. - * Additionally, PHP only provides the last exception (meaning if exceptions - * were thrown then rethrown or another exception thrown, nothing gets - * communicated except for the last exception. PHP has a hook that can be used - * to notify whenever an exception is triggered but it doesn't give any - * indication if that exception was ever caught. - * - * To handle this, dangling exception sweeps occur in - * nr_php_observer_exception_segments_end and is called from 5 different places: - * 1) nr_php_observer_fcall_begin - before a new segment starts - * 2) nr_php_observer_fcall_end - before a segment is ended(kept/discarded) - * 3) nr_php_stacked_segment_unwind - when a txn ends and we are closing up shop - * 4) php_observer_handle_exception_hook - when a new exception is noticed - * 5) in newrelic APIs that depend on having the current segment - * - * - * The workflow of using stacked segments in connection with regular - * segments when an exception occurs is complicated. - * These cases are illustrated by a series of short ASCII cartoons. - * - * case 1 nr_php_observer_fcall_begin - before a new segment starts - * root < root root - * | | - * *A < *A - * | - * *B - * - * We start out with a root segment, OAPI calls nr_php_observer_fcall_begin(A), - * and it starts stacked segment *A and then nr_php_observer_fcall_begin(B) - * starts stacked segment *B as a child of *A. - * - * root root - * | | - * *A *A - * | | - * *B *B < - * | | - * *C < c - * - * nr_php_observer_fcall_begin(C) starts *C gets started as child - * of *B. Function C throws an uncaught exception which B does not catch so - * neither nr_php_observer_fcall_end(B) nor nr_php_observer_fcall_end(C) is - * called and *C remains the current segment. A catches the exception and calls - * function D, so nr_php_observer_fcall_begin(D) is triggered. At this point we - * realize the current stacked_segment->metadata->This value and the - * execute_data->prev_execute_data->This don't match so we don't want to parent - * *D to the wrong segment. We check the global exception hook and see it - * has a value and that the global uncaught_exception_this also matches the - * current segment `this`. Time to apply the exception and clean up dangling - * segments. We pop the current segment *C and apply the exception. - * Because it has an exception, the segment is kept so we copy the contents of - * the stacked segment *C into a segment c we obtained from the slab allocator, - * and we make c a child of the stacked segment *B which becomes the current - * segment. - * - * root root root - * | | | - * *A < *A < *A - * | | / \ - * b b b *D < - * | | | - * c c c - * - * - * But we aren't done yet. - * current stacked_segment->metadata->this still doesn't equal the - * execute_data->prev_execute_data->This provided by - * nr_php_observer_fcall_begin(D). We pop the current segment *B and apply the - * exception. Because it has an exception, the segment is kept so we copy the - * contents of the stacked segment *B into a segment b we obtained from the - * slab allocator, and we make b a child of the stacked segment *A which - * becomes the current segment. Now current stacked_segment->metadata->this - * DOES equal the execute_data->prev_execute_data->This provided by - * nr_php_observer_fcall_begin(D) so we proceed and create stacked segment *D - * correctly parented as a child of *A and *D becomes the current segment. - * - * case 2 nr_php_observer_fcall_end - before a segment is ended(kept/discarded) - * root < root root - * | | - * *A < *A - * | - * *B - * - * We start out with a root segment, OAPI calls nr_php_observer_fcall_begin(A), - * and it starts stacked segment *A and then nr_php_observer_fcall_begin(B) - * starts stacked segment *B as a child of *A. - * - * root root - * | | - * *A *A - * | | - * *B *B < - * | | - * *C < c - * - * nr_php_observer_fcall_begin(C) starts segment *C as child - * of *B. Function C throws an uncaught exception which B does not catch so - * neither nr_php_observer_fcall_end(B) nor nr_php_observer_fcall_end(C) is - * called and *C remains the current segment. A catches the exception and - * nr_php_observer_fcall_end(A) is triggered. At this point we compare the - * current stacked_segment->metadata->This value with the execute_data->This and - * realize the two don't match. We check the global exception hook and see it - * has a value and that the global uncaught_exception_this also matches the - * current segment `this`. Time to apply the exception and clean up dangling - * segments. We pop the current segment *C and apply the exception. - * Because it has an exception, the segment is kept so we copy the contents of - * the stacked segment *C into a segment c we obtained from the slab allocator, - * and we make c a child of the stacked segment *B which becomes the current - * segment. - * - * root root < - * | | - * *A < a - * | | - * b b - * | | - * c c - * - * - * But we aren't done yet. - * current stacked_segment->metadata->this still doesn't equal the - * execute_data-> this provided by nr_php_observer_fcall_end(A). We pop the - * current segment *B and apply the exception. Because it has an exception, the - * segment is kept so we copy the contents of the stacked segment *B into a - * segment b we obtained from the slab allocator, and we make b a child of the - * stacked segment *A which becomes the current segment. Now current - * stacked_segment->metadata->this DOES equal the execute_data-> this - * provided by nr_php_observer_fcall_end(A) so it proceeds, decides to keep the - * segment and we copy the contents of the stacked segment *A into a segment a - * we obtained from the slab allocator, and we make a a child of the stacked - * segment root and root becomes the current segment. - * - * case 3 nr_php_stacked_segment_unwind - when a txn ends but stacked segments - * still exist - * - * root < root root - * | | - * *A < *A - * | - * *B < - * - * We start out with a root segment, OAPI calls nr_php_observer_fcall_begin(A), - * and it starts stacked segment *A and then nr_php_observer_fcall_begin(B) - * starts stacked segment *B as a child of *A. - * - * root root - * | | - * *A *A - * | | - * *B *B < - * | | - * *C < c - * - * nr_php_observer_fcall_begin(C) starts *C gets started as child - * of *B. Function C throws an uncaught exception which A and B do not catch so - * nr_php_observer_fcall_end(A), nr_php_observer_fcall_end(B), - * nr_php_observer_fcall_end(C) are not called and *C remains the current - * segment. The root segment ends and rshutdown calls `nr_php_txn_end` which - * calls `nr_php_stacked_segment_unwind`. Because we didn't get any - * nr_php_observer_fcall_end we know no segment caught the exception that - * triggered the exception hook. We'll apply the exception and - * keep/close stacked segments all the way down the stack to clean up dangling - * segments. We pop the current segment *C and apply the exception. Because it - * has an exception, the segment is kept so we copy the contents of the stacked - * segment *C into a segment c we obtained from the slab allocator, and we make - * c a child of the stacked segment *B which becomes the current segment. - * - * root root < root - * | | | - * *A < a a - * | | | - * b b b - * | | | - * c c c - * - * We pop the current segment *B and apply the exception. Because it has an - * exception, the segment is kept so we copy the contents of the stacked segment - * *B into a segment b we obtained from the slab allocator, and we make b a - * child of the stacked segment *A which becomes the current segment. Then we - * pop the current segment *A and apply the exception. Because it has an - * exception, the segment is kept so we copy the contents of the stacked segment - * *A into a segment a we obtained from the slab allocator, and we make a a - * child of the root. The exception is applied to the root and the rshutdown - * completes. - * - * case 4 php_observer_handle_exception_hook - when a new exception is noticed - * root < root root - * | | - * *A < *A - * | - * *B - * - * We start out with a root segment, OAPI calls nr_php_observer_fcall_begin(A), - * and it starts stacked segment *A and then nr_php_observer_fcall_begin(B) - * starts stacked segment *B as a child of *A. - * - * root root root - * | | | - * *A < *A *A - * | | - * *B *B < - * | | - * *C < c - * - * nr_php_observer_fcall_begin(C) starts *C gets started as child - * of *B. Function C throws an exception which B catches but - * nr_php_observer_fcall_end(C) is not called so *C remains the - * current segment. B catches the exception and throws another - * exception which triggers the exception hook. At this point we realize the current - * exception->This value indicates another function is active. Because we - * received no nr_php_observer_fcall_end up to that point, we know the exception - * was uncaught until the exception->This function. We check the global - * exception hook and see it has a value and that the global - * uncaught_exception_this also matches the current segment `this`. Time to - * apply the exception and clean up dangling segments. We pop the current - * segment *C and apply the exception. Because it has an exception, the segment - * is kept so we copy the contents of the stacked segment *C into a segment c we - * obtained from the slab allocator, and we make c a child of the stacked - * segment *B which becomes the current segment. - * - * root - * | - * *A - * | - * *B < - * | - * c - * - * current stacked_segment->metadata->this now equals the exception->This. so we - * reserve judgement on what eventually happens to segment *B and *B becomes the - * current segment with the new active exception stored. Any subsequent dangling - * segments are cleaned when the next scenario 1-5 occurs. - * - * case 5 in newrelic APIs that depend on having the current segment - * root < root root - * | | - * *A < *A - * | - * *B - * - * We start out with a root segment, OAPI calls nr_php_observer_fcall_begin(A), - * and it starts stacked segment *A and then nr_php_observer_fcall_begin(B) - * starts stacked segment *B as a child of *A. - * - * root root - * | | - * *A *A - * | | - * *B *B < - * | | - * *C < c - * - * nr_php_observer_fcall_begin(C) starts *C gets started as child - * of *B. Function C throws an uncaught exception which B does not catch so - * neither nr_php_observer_fcall_end(B) nor nr_php_observer_fcall_end(C) is - * called and *C remains the current segment. A catches the exception and makes - * an API call `newrelic_notice_error`. All API functions that rely on segments - * call `nr_php_api_ensure_current_segment` before doing any segment related - * operation. `nr_php_api_ensure_current_segment` eventually calls - * `nr_php_observer_handle_uncaught_exception` where we check the `this` value - * of the function that called newrelic_notice_error and see it is not the same. - * Because we received no nr_php_observer_fcall_end up to that point, we know - * the exception was uncaught until the Function A. We check the global - * exception hook and see it has a value and that the global - * uncaught_exception_this also matches the current segment `this`. Time to - * apply the exception and clean up dangling segments as we don't want to apply - * the notice_error to the wrong segment. We pop the current segment *C and - * apply the exception. Because it has an exception, the segment is kept so we - * copy the contents of the stacked segment *C into a segment c we obtained - * from the slab allocator, and we make c a child of the stacked segment *B - * which becomes the current segment. - * - * root - * | - * *A < - * | - * b - * | - * c - * - * - * But we aren't done yet. - * We check the `this` value of the function that called - * newrelic_notice_error and see it is not the same as the current segment - * `this`. We pop the current segment *B and apply the exception. Because it has - * an exception, the segment is kept so we copy the contents of the stacked - * segment *B into a segment b we obtained from the slab allocator, and we make - * b a child of the stacked segment *A which becomes the current segment. We - * check the this` value of the function that called newrelic_notice_error and see it is - * the same as the current segment `this` so newrelic_notice_error proceeds and applies the - * notice error to the current segment *A. - * Note that this only works with segments on the default parent stack. - * Stacked segments cannot be used to model async segments. */ // clang-format on diff --git a/axiom/nr_segment.h b/axiom/nr_segment.h index c611b0748..591612c1f 100644 --- a/axiom/nr_segment.h +++ b/axiom/nr_segment.h @@ -192,10 +192,6 @@ typedef struct _nr_segment_t { */ void* wraprec; /* wraprec, if one is associated with this segment, to reduce wraprec lookups */ - - void* metadata; /* Persist data for OAPI for when exceptions prevent fcall_end - from being called */ - #endif } nr_segment_t; diff --git a/axiom/nr_txn.c b/axiom/nr_txn.c index 27f326d16..a4f5773a4 100644 --- a/axiom/nr_txn.c +++ b/axiom/nr_txn.c @@ -1549,6 +1549,7 @@ nr_status_t nr_txn_record_error_worthy(const nrtxn_t* txn, int priority) { void nr_txn_record_error(nrtxn_t* txn, int priority, + bool add_to_current_segment, const char* errmsg, const char* errclass, const char* stacktrace_json) { @@ -1592,15 +1593,17 @@ void nr_txn_record_error(nrtxn_t* txn, return; } - current_segment = nr_txn_get_current_segment(txn, NULL); + if (add_to_current_segment) { + current_segment = nr_txn_get_current_segment(txn, NULL); - if (current_segment) { - nr_segment_set_error(current_segment, errmsg, errclass); - nrl_verbosedebug(NRL_TXN, - "recording segment error: msg='%.48s' cls='%.48s'" - "span_id='%.48s'", - NRSAFESTR(errmsg), NRSAFESTR(errclass), - NRSAFESTR(span_id)); + if (current_segment) { + nr_segment_set_error(current_segment, errmsg, errclass); + nrl_verbosedebug(NRL_TXN, + "recording segment error: msg='%.48s' cls='%.48s'" + "span_id='%.48s'", + NRSAFESTR(errmsg), NRSAFESTR(errclass), + NRSAFESTR(span_id)); + } } } error = nr_error_create(priority, errmsg, errclass, stacktrace_json, span_id, diff --git a/axiom/nr_txn.h b/axiom/nr_txn.h index e3e063214..60d5d5e82 100644 --- a/axiom/nr_txn.h +++ b/axiom/nr_txn.h @@ -451,9 +451,10 @@ extern nr_status_t nr_txn_record_error_worthy(const nrtxn_t* txn, int priority); * Params : 1. The transaction pointer. * 2. The priority of the error. A higher number indicates a more * serious error. - * 3. The error message. - * 4. The error class. - * 5. Stack trace in JSON format. + * 3. Whether to add the error to the current segment. + * 4. The error message. + * 5. The error class. + * 6. Stack trace in JSON format. * * Returns : Nothing. * @@ -468,6 +469,7 @@ extern nr_status_t nr_txn_record_error_worthy(const nrtxn_t* txn, int priority); extern void nr_txn_record_error(nrtxn_t* txn, int priority, + bool add_to_segment, const char* errmsg, const char* errclass, const char* stacktrace_json); diff --git a/axiom/tests/test_txn.c b/axiom/tests/test_txn.c index 37d9a8784..5574f431b 100644 --- a/axiom/tests/test_txn.c +++ b/axiom/tests/test_txn.c @@ -1301,43 +1301,43 @@ static void test_record_error(void) { * Nothing to test after these calls since no txn is provided. * However, we want to ensure that the stack parameter is freed. */ - nr_txn_record_error(NULL, 0, NULL, NULL, NULL); - nr_txn_record_error(0, 2, "msg", "class", "[\"A\",\"B\"]"); + nr_txn_record_error(NULL, 0, true, NULL, NULL, NULL); + nr_txn_record_error(0, 2, true, "msg", "class", "[\"A\",\"B\"]"); txn->options.err_enabled = 0; - nr_txn_record_error(txn, 2, "msg", "class", "[\"A\",\"B\"]"); + nr_txn_record_error(txn, 2, true, "msg", "class", "[\"A\",\"B\"]"); tlib_pass_if_true("nr_txn_record_error no err_enabled", 0 == txn->error, "txn->error=%p", txn->error); txn->options.err_enabled = 1; txn->status.recording = 0; - nr_txn_record_error(txn, 2, "msg", "class", "[\"A\",\"B\"]"); + nr_txn_record_error(txn, 2, true, "msg", "class", "[\"A\",\"B\"]"); tlib_pass_if_true("nr_txn_record_error no recording", 0 == txn->error, "txn->error=%p", txn->error); txn->status.recording = 1; - nr_txn_record_error(txn, 2, 0, "class", "[\"A\",\"B\"]"); + nr_txn_record_error(txn, 2, true, 0, "class", "[\"A\",\"B\"]"); tlib_pass_if_true("nr_txn_record_error no errmsg", 0 == txn->error, "txn->error=%p", txn->error); - nr_txn_record_error(txn, 2, "msg", 0, "[\"A\",\"B\"]"); + nr_txn_record_error(txn, 2, true, "msg", 0, "[\"A\",\"B\"]"); tlib_pass_if_true("nr_txn_record_error no class", 0 == txn->error, "txn->error=%p", txn->error); - nr_txn_record_error(txn, 2, "", "class", "[\"A\",\"B\"]"); + nr_txn_record_error(txn, 2, true, "", "class", "[\"A\",\"B\"]"); tlib_pass_if_true("nr_txn_record_error empty errmsg", 0 == txn->error, "txn->error=%p", txn->error); - nr_txn_record_error(txn, 2, "msg", "", "[\"A\",\"B\"]"); + nr_txn_record_error(txn, 2, true, "msg", "", "[\"A\",\"B\"]"); tlib_pass_if_true("nr_txn_record_error empty class", 0 == txn->error, "txn->error=%p", txn->error); - nr_txn_record_error(txn, 2, "msg", "class", 0); + nr_txn_record_error(txn, 2, true, "msg", "class", 0); tlib_pass_if_true("nr_txn_record_error no stack", 0 == txn->error, "txn->error=%p", txn->error); /* Success when no previous error */ - nr_txn_record_error(txn, 2, "msg", "class", "[\"A\",\"B\"]"); + nr_txn_record_error(txn, 2, true, "msg", "class", "[\"A\",\"B\"]"); tlib_pass_if_true("no previous error", 0 != txn->error, "txn->error=%p", txn->error); tlib_pass_if_true("no previous error", 2 == nr_error_priority(txn->error), @@ -1349,7 +1349,7 @@ static void test_record_error(void) { NRSAFESTR(nr_error_get_message(txn->error))); /* Failure with lower priority error than existing */ - nr_txn_record_error(txn, 1, "newmsg", "newclass", "[]"); + nr_txn_record_error(txn, 1, true, "newmsg", "newclass", "[]"); tlib_pass_if_true("lower priority", 0 != txn->error, "txn->error=%p", txn->error); tlib_pass_if_true("lower priority", 2 == nr_error_priority(txn->error), @@ -1361,7 +1361,7 @@ static void test_record_error(void) { NRSAFESTR(nr_error_get_message(txn->error))); /* Replace error when higher priority than existing */ - nr_txn_record_error(txn, 3, "newmsg", "newclass", "[\"C\",\"D\"]"); + nr_txn_record_error(txn, 3, true, "newmsg", "newclass", "[\"C\",\"D\"]"); tlib_pass_if_true("higher priority", 0 != txn->error, "txn->error=%p", txn->error); tlib_pass_if_true("higher priority", 3 == nr_error_priority(txn->error), @@ -1373,7 +1373,7 @@ static void test_record_error(void) { NRSAFESTR(nr_error_get_message(txn->error))); txn->high_security = 1; - nr_txn_record_error(txn, 4, "don't show me", "high_security", + nr_txn_record_error(txn, 4, true, "don't show me", "high_security", "[\"C\",\"D\"]"); tlib_pass_if_true("high security error message stripped", 0 != txn->error, "txn->error=%p", txn->error); @@ -1397,7 +1397,7 @@ static void test_record_error(void) { txn->distributed_trace = nr_distributed_trace_create(); nr_distributed_trace_set_sampled(txn->distributed_trace, true); - nr_txn_record_error(txn, 2, "msg", "class", "[\"A\",\"B\"]"); + nr_txn_record_error(txn, 2, true, "msg", "class", "[\"A\",\"B\"]"); tlib_pass_if_null("nr_txn_record_error no span_id for error", txn->error); txn->options.distributed_tracing_enabled = 0; txn->options.span_events_enabled = 0; @@ -1419,7 +1419,7 @@ static void test_record_error(void) { */ nr_error_destroy(&txn->error); txn->error = 0; - nr_txn_record_error(txn, 3, "oldmsg", "oldclass", "[\"C\",\"D\"]"); + nr_txn_record_error(txn, 3, true, "oldmsg", "oldclass", "[\"C\",\"D\"]"); /* Change the environment to create an error condition. */ txn->options.distributed_tracing_enabled = 1; txn->options.span_events_enabled = 1; @@ -1427,7 +1427,7 @@ static void test_record_error(void) { nr_distributed_trace_set_sampled(txn->distributed_trace, true); /*Even though it is higher priority, it should not replace the existing error * because of the error condition.*/ - nr_txn_record_error(txn, 5, "newmsg", "newclass", "[\"A\",\"B\"]"); + nr_txn_record_error(txn, 5, true, "newmsg", "newclass", "[\"A\",\"B\"]"); tlib_pass_if_not_null("nr_txn_record_error previous error is not destroyed", txn->error); tlib_pass_if_not_null("previous error is not destroyed", txn->error); @@ -1972,7 +1972,7 @@ static nrtxn_t* create_full_txn_and_reset(nrapp_t* app) { /* * Add an Error */ - nr_txn_record_error(txn, 1, "my_errmsg", "my_errclass", + nr_txn_record_error(txn, 1, true, "my_errmsg", "my_errclass", "[\"Zink called on line 123 of script.php\"," "\"Zonk called on line 456 of hack.php\"]"); tlib_pass_if_true("error added", 0 != txn->error, "txn->error=%p", @@ -4641,7 +4641,7 @@ static void test_txn_dt_cross_agent_testcase(nrapp_t* app, if (raises_exception) { txn->options.err_enabled = 1; txn->error = NULL; - nr_txn_record_error(txn, 2, "msg", "class", "[\"A\",\"B\"]"); + nr_txn_record_error(txn, 2, true, "msg", "class", "[\"A\",\"B\"]"); } /* @@ -4708,7 +4708,7 @@ static void test_txn_dt_cross_agent_testcase(nrapp_t* app, nrobj_t* data; nr_analytics_event_t* error_event_analytics; - nr_txn_record_error(txn, 100, "error", "class", "{}"); + nr_txn_record_error(txn, 100, true, "error", "class", "{}"); error_event_analytics = nr_error_to_event(txn); data = nro_create_from_json(nr_analytics_event_json(error_event_analytics)); error_event = nro_copy(nro_get_array_hash(data, 1, NULL)); @@ -4904,7 +4904,7 @@ static void test_txn_trace_context_cross_agent_testcase(nrapp_t* app, if (raises_exception) { txn->options.err_enabled = 1; txn->error = NULL; - nr_txn_record_error(txn, 2, "msg", "class", "[\"A\",\"B\"]"); + nr_txn_record_error(txn, 2, true, "msg", "class", "[\"A\",\"B\"]"); } /* @@ -4995,7 +4995,7 @@ static void test_txn_trace_context_cross_agent_testcase(nrapp_t* app, nrobj_t* error_data; nr_analytics_event_t* error_event_analytics; - nr_txn_record_error(txn, 100, "error", "class", "{}"); + nr_txn_record_error(txn, 100, true, "error", "class", "{}"); error_event_analytics = nr_error_to_event(txn); error_data = nro_create_from_json(nr_analytics_event_json(error_event_analytics)); @@ -5588,7 +5588,7 @@ static void test_allow_raw_messages_lasp(void) { nro_set_hash_boolean(security_policies, "allow_raw_exception_messages", true); nr_txn_enforce_security_settings(&txn->options, connect_reply, security_policies); - nr_txn_record_error(txn, 2, "", "class", "[\"A\",\"B\"]"); + nr_txn_record_error(txn, 2, true, "", "class", "[\"A\",\"B\"]"); tlib_pass_if_true("nr_txn_record_error empty errmsg", 0 == txn->error, "txn->error=%p", txn->error); @@ -5600,7 +5600,7 @@ static void test_allow_raw_messages_lasp(void) { false); nr_txn_enforce_security_settings(&txn->options, connect_reply, security_policies); - nr_txn_record_error(txn, 4, "don't show", "class", "[\"A\",\"B\"]"); + nr_txn_record_error(txn, 4, true, "don't show", "class", "[\"A\",\"B\"]"); tlib_pass_if_true("security setting error message stripped", 0 != txn->error, "txn->error=%p", txn->error); tlib_pass_if_true("security setting error message stripped", @@ -5617,7 +5617,7 @@ static void test_allow_raw_messages_lasp(void) { nro_set_hash_boolean(security_policies, "allow_raw_exception_messages", true); nr_txn_enforce_security_settings(&txn->options, connect_reply, security_policies); - nr_txn_record_error(txn, 4, "don't show", "class", "[\"A\",\"B\"]"); + nr_txn_record_error(txn, 4, true, "don't show", "class", "[\"A\",\"B\"]"); tlib_pass_if_true("security setting error message stripped", 0 != txn->error, "txn->error=%p", txn->error); tlib_pass_if_true("security setting error message stripped", @@ -5635,7 +5635,7 @@ static void test_allow_raw_messages_lasp(void) { false); nr_txn_enforce_security_settings(&txn->options, connect_reply, security_policies); - nr_txn_record_error(txn, 4, "don't show", "class", "[\"A\",\"B\"]"); + nr_txn_record_error(txn, 4, true, "don't show", "class", "[\"A\",\"B\"]"); tlib_pass_if_true("security setting error message stripped", 0 != txn->error, "txn->error=%p", txn->error); tlib_pass_if_true("security setting error message stripped", @@ -8009,12 +8009,22 @@ static void test_segment_record_error(void) { /* No error attributes added if error collection isn't enabled */ txn->options.err_enabled = 0; - nr_txn_record_error(txn, 1, "msg", "class", "[\"A\",\"B\"]"); + nr_txn_record_error(txn, 1, true, "msg", "class", "[\"A\",\"B\"]"); tlib_pass_if_null("No segment error created", segment->error); txn->options.err_enabled = 1; - /* Normal operation */ - nr_txn_record_error(txn, 1, "error message", "error class", "[\"A\",\"B\"]"); + /* Do not add to current segment */ + nr_txn_record_error(txn, 0.5, false /* do not add to current segment*/, + "low priority message", "low priority class", "[\"A\",\"B\"]"); + tlib_pass_if_not_null("Txn error event created", txn->error); + tlib_pass_if_null("Segment error NOT created", segment->error); + tlib_pass_if_str_equal("Correct txn error.message", "low priority message", + nr_error_get_message(txn->error)); + tlib_pass_if_str_equal("Correct txn error.class", "low priority class", + nr_error_get_klass(txn->error)); + + /* Normal operation: txn error prioritized over previous */ + nr_txn_record_error(txn, 1, true, "error message", "error class", "[\"A\",\"B\"]"); tlib_pass_if_not_null("Txn error event created", txn->error); tlib_pass_if_not_null("Segment error created", segment->error); @@ -8030,7 +8040,7 @@ static void test_segment_record_error(void) { nr_error_get_klass(txn->error)); /* Multiple errors on the same segment */ - nr_txn_record_error(txn, 1, "error message 2", "error class 2", + nr_txn_record_error(txn, 1, true, "error message 2", "error class 2", "[\"A\",\"B\"]"); tlib_pass_if_str_equal("Segment error.message overwritten", "error message 2", @@ -8049,7 +8059,7 @@ static void test_segment_record_error(void) { /* High_security */ txn->high_security = 1; - nr_txn_record_error(txn, 1, "Highly secure message", "error class", + nr_txn_record_error(txn, 1, true, "Highly secure message", "error class", "[\"A\",\"B\"]"); tlib_pass_if_not_null("Segment error created", segment->error); tlib_pass_if_str_equal("Secure error.message", @@ -8067,7 +8077,7 @@ static void test_segment_record_error(void) { /* allow_raw_exception_messages */ txn->options.allow_raw_exception_messages = 0; - nr_txn_record_error(txn, 1, "Another highly secure message", + nr_txn_record_error(txn, 1, true, "Another highly secure message", "another error class", "[\"A\",\"B\"]"); tlib_pass_if_not_null("Segment error created", segment->error); tlib_pass_if_str_equal("Secure error message", diff --git a/tests/integration/api/distributed_trace/newrelic/test_create_payload_nested_caught_exception.php b/tests/integration/api/distributed_trace/newrelic/test_create_payload_nested_caught_exception.php index 3e4fb3e79..351f20c88 100644 --- a/tests/integration/api/distributed_trace/newrelic/test_create_payload_nested_caught_exception.php +++ b/tests/integration/api/distributed_trace/newrelic/test_create_payload_nested_caught_exception.php @@ -106,7 +106,7 @@ "transactionId": "??", "sampled": true, "priority": "??", - "name": "Custom\/fraction", + "name": "Custom\/c", "guid": "??", "timestamp": "??", "duration": "??", diff --git a/tests/integration/api/distributed_trace/newrelic/test_create_payload_nested_uncaught_exception.php b/tests/integration/api/distributed_trace/newrelic/test_create_payload_nested_uncaught_exception.php index af710bbc7..433e0f8ab 100644 --- a/tests/integration/api/distributed_trace/newrelic/test_create_payload_nested_uncaught_exception.php +++ b/tests/integration/api/distributed_trace/newrelic/test_create_payload_nested_uncaught_exception.php @@ -21,7 +21,6 @@ newrelic.distributed_tracing_enabled = true newrelic.transaction_tracer.detail = 1 newrelic.transaction_tracer.threshold = 0 -newrelic.transaction_tracer.max_segments_cli = 3 newrelic.special.expensive_node_min = 0 */ @@ -60,7 +59,7 @@ "?? agent run id", { "reservoir_size": 10000, - "events_seen": 3 + "events_seen": 6 }, [ [ @@ -105,6 +104,73 @@ "code.function": "??" } ], + [ + { + "type": "Span", + "traceId": "??", + "transactionId": "??", + "sampled": true, + "priority": "??", + "name": "Custom\/b", + "guid": "??", + "timestamp": "??", + "duration": "??", + "category": "generic", + "parentId": "??" + }, + {}, + { + "error.message": "Uncaught exception 'RuntimeException' with message 'Division by zero' in __FILE__:??", + "error.class": "RuntimeException", + "code.lineno": "??", + "code.filepath": "__FILE__", + "code.function": "??" + } + ], + [ + { + "type": "Span", + "traceId": "??", + "transactionId": "??", + "sampled": true, + "priority": "??", + "name": "Custom\/c", + "guid": "??", + "timestamp": "??", + "duration": "??", + "category": "generic", + "parentId": "??" + }, + {}, + { + "error.message": "Uncaught exception 'RuntimeException' with message 'Division by zero' in __FILE__:??", + "error.class": "RuntimeException", + "code.lineno": "??", + "code.filepath": "__FILE__", + "code.function": "??" + } + ], + [ + { + "type": "Span", + "traceId": "??", + "transactionId": "??", + "sampled": true, + "priority": "??", + "name": "Custom\/fraction", + "guid": "??", + "timestamp": "??", + "duration": "??", + "category": "generic", + "parentId": "??" + }, + {}, + { + "code.lineno": "??", + "code.filepath": "__FILE__", + "code.function": "??" + } + ], [ { "type": "Span", diff --git a/tests/integration/api/other/test_end_transaction_nested.php b/tests/integration/api/other/test_end_transaction_nested.php index f8eaf24b8..30bc19a32 100644 --- a/tests/integration/api/other/test_end_transaction_nested.php +++ b/tests/integration/api/other/test_end_transaction_nested.php @@ -7,13 +7,6 @@ Test that newrelic_end_transaction() ends all unended segments in the stack. */ -/*SKIPIF -=")) { - die("skip: PHP >= 8.0.0 not supported\n"); -} -*/ - /*INI newrelic.transaction_tracer.threshold = 0 */ diff --git a/tests/integration/api/other/test_end_transaction_nested.php8.php b/tests/integration/api/other/test_end_transaction_nested.php8.php deleted file mode 100644 index ee637afb7..000000000 --- a/tests/integration/api/other/test_end_transaction_nested.php8.php +++ /dev/null @@ -1,106 +0,0 @@ -", - [ - [ - 0, {}, {}, - [ - "?? start time", "?? end time", "ROOT", "?? root attributes", - [ - [ - "?? start time", "?? end time", "`0", "?? node attributes", - [ - [ - "?? start time", "?? end time", "`1", "?? node attributes", - [ - [ - "?? start time", "?? end time", "`2", "?? node attributes", - [ - [ - "?? start time", "?? end time", "`3", "?? node attributes", - [] - ] - ] - ] - ] - ] - ] - ] - ] - ], - { - "intrinsics": { - "totalTime": "??", - "cpu_time": "??", - "cpu_user_time": "??", - "cpu_sys_time": "??", - "guid": "??", - "sampled": true, - "priority": "??", - "traceId": "??" - } - } - ], - [ - "OtherTransaction\/php__FILE__", - "Custom\/level_2", - "Custom\/level_1", - "Custom\/level_0" - ] - ], - "?? txn guid", - "?? reserved", - "?? force persist", - "?? x-ray sessions", - null - ] - ] -] -*/ -require_once(realpath(dirname(__FILE__)) . '/../../../include/helpers.php'); - -newrelic_add_custom_tracer("level_0"); - -function level_0() { - echo "level_0\n"; -} - -function level_1() { - level_0(); - newrelic_end_transaction(); -} - -function level_2() { - level_1(); -} - -level_2(); From 49a6c1a4fdc5031d1b631738a60a0c566c45396d Mon Sep 17 00:00:00 2001 From: ZNeumann Date: Mon, 5 Feb 2024 13:25:35 -0700 Subject: [PATCH 50/56] refactor: condense exception handling instrumentation (#825) Move exception handler instrumentation from fcall_begin to fcall_end. This requires a change of how we are obtaining the fields of the exception, because calling `nr_php_call` during the handling of an exception does not play nicely with PHP. --------- Co-authored-by: Michal Nowacki --- agent/php_agent.c | 45 +++++++++++++++++++++++++++++++++++---------- agent/php_agent.h | 18 +++++++++++++++++- agent/php_error.c | 30 +++++++++++++----------------- agent/php_execute.c | 38 +++++++++++--------------------------- 4 files changed, 76 insertions(+), 55 deletions(-) diff --git a/agent/php_agent.c b/agent/php_agent.c index 2502000c7..e064afc76 100644 --- a/agent/php_agent.c +++ b/agent/php_agent.c @@ -4,6 +4,7 @@ */ #include "php_agent.h" #include "php_call.h" +#include "php_error.h" #include "php_globals.h" #include "php_hash.h" #include "nr_rum.h" @@ -11,6 +12,8 @@ #include "util_memory.h" #include "util_strings.h" +#include + #include "php_variables.h" static zval* nr_php_get_zval_object_property_with_class_internal( @@ -63,28 +66,50 @@ static zval* nr_php_get_zval_object_property_with_class_internal( zval* nr_php_get_zval_object_property(zval* object, const char* cname TSRMLS_DC) { - char* name; - - if ((0 == object) || (0 == cname) || (0 == cname[0])) { - return 0; + if ((NULL == object) || (NULL == cname) || (0 == cname[0])) { + return NULL; } - name = (char*)nr_alloca(nr_strlen(cname) + 1); - nr_strcpy(name, cname); - if (nr_php_is_zval_valid_object(object)) { return nr_php_get_zval_object_property_with_class_internal( - object, Z_OBJCE_P(object), name TSRMLS_CC); + object, Z_OBJCE_P(object), cname TSRMLS_CC); } else if (IS_ARRAY == Z_TYPE_P(object)) { zval* data; - data = nr_php_zend_hash_find(Z_ARRVAL_P(object), name); + data = nr_php_zend_hash_find(Z_ARRVAL_P(object), cname); if (data) { return data; } } - return 0; + return NULL; +} + +zval* nr_php_get_zval_base_exception_property(zval* exception, + const char* cname) { + zend_class_entry* ce; + + if ((NULL == exception) || (NULL == cname) || (0 == cname[0])) { + return NULL; + } + + if (nr_php_is_zval_valid_object(exception)) { + if (nr_php_error_zval_is_exception(exception)) { + /* + * This is inline with what the php source code does to extract properties from + * errors and exceptions. Without getting the base class entry, certain values + * are incorrect for either errors/exceptions. + */ +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO + ce = zend_get_exception_base(Z_OBJ_P(exception)); +#else + ce = zend_get_exception_base(exception); +#endif + return nr_php_get_zval_object_property_with_class_internal( + exception, ce, cname TSRMLS_CC); + } + } + return NULL; } zval* nr_php_get_zval_object_property_with_class(zval* object, diff --git a/agent/php_agent.h b/agent/php_agent.h index deeabac79..2d061ea00 100644 --- a/agent/php_agent.h +++ b/agent/php_agent.h @@ -133,13 +133,29 @@ typedef unsigned int nr_php_object_handle_t; * Purpose : Extract the named entity from a PHP object zval. * * Params : 1. The object to extract the property from. - * 2. The name of the object. + * 2. The name of the object property. * * Returns : The specified element or NULL if it was not found. */ extern zval* nr_php_get_zval_object_property(zval* object, const char* cname TSRMLS_DC); +/* + * Purpose : Extract the named entity from a PHP exception zval. + * This determines if the passed exception is a PHP error + * or a PHP exception, and extracts accordingly. + * + * Params : 1. The exception to extract the property from. + * 2. The name of the exception property. + * + * Returns : The specified element or NULL if it was not found. + * This returns the zval owned by the Zend engine, so + * a refernce incremenet should take place if the return + * value is to be kept around beyond the caller's scope. + */ +extern zval* nr_php_get_zval_base_exception_property(zval* exception, + const char* cname); + /* * Purpose : Extract the named property from a PHP object zval in a particular * class context. (Useful for getting private properties from diff --git a/agent/php_error.c b/agent/php_error.c index d59858147..3702870a3 100644 --- a/agent/php_error.c +++ b/agent/php_error.c @@ -381,18 +381,22 @@ void nr_php_error_install_exception_handler(TSRMLS_D) { * checked in any way, and is assumed to be a valid Exception * object. * - * Returns : A zval for the stack trace, which the caller will need to destroy, - * or NULL if no trace is available. + * Returns : A zval for the stack trace, which the caller will need to + * delete reference to, or NULL if no trace is available. */ static zval* nr_php_error_exception_stack_trace(zval* exception TSRMLS_DC) { zval* trace; - trace = nr_php_call(exception, "getTrace"); - if (!nr_php_is_zval_valid_array(trace)) { - nr_php_zval_free(&trace); + trace = nr_php_get_zval_base_exception_property(exception, "trace"); + if (NULL == trace || nr_php_is_zval_null(trace)) { return NULL; } + Z_ADDREF_P(trace); + if (!nr_php_is_zval_valid_array(trace)) { + Z_DELREF_P(trace); + return NULL; + } return trace; } @@ -407,14 +411,13 @@ static zval* nr_php_error_exception_stack_trace(zval* exception TSRMLS_DC) { * will need to free, or NULL if no file name is available. */ static char* nr_php_error_exception_file(zval* exception TSRMLS_DC) { - zval* file_zv = nr_php_call(exception, "getFile"); + zval* file_zv = nr_php_get_zval_object_property(exception, "file"); char* file = NULL; if (nr_php_is_zval_valid_string(file_zv)) { file = nr_strndup(Z_STRVAL_P(file_zv), Z_STRLEN_P(file_zv)); } - nr_php_zval_free(&file_zv); return file; } @@ -429,7 +432,7 @@ static char* nr_php_error_exception_file(zval* exception TSRMLS_DC) { */ static long nr_php_error_exception_line(zval* exception TSRMLS_DC) { long line = 0; - zval* line_zv = nr_php_call(exception, "getLine"); + zval* line_zv = nr_php_get_zval_object_property(exception, "line"); /* * All scalar types can be coerced to IS_LONG. @@ -439,7 +442,6 @@ static long nr_php_error_exception_line(zval* exception TSRMLS_DC) { line = Z_LVAL_P(line_zv); } - nr_php_zval_free(&line_zv); return line; } @@ -454,19 +456,13 @@ static long nr_php_error_exception_line(zval* exception TSRMLS_DC) { * will need to free, or NULL if no message could be extracted. */ static char* nr_php_error_exception_message(zval* exception TSRMLS_DC) { - /* - * This intentionally prefers getMessage(): __toString() can include stack - * dumps generated by PHP, which can include user data that we don't want to - * send up and for which it isn't obvious that it would be sent. - */ - zval* message_zv = nr_php_call(exception, "getMessage"); + zval* message_zv = nr_php_get_zval_base_exception_property(exception, "message"); char* message = NULL; if (nr_php_is_zval_valid_string(message_zv)) { message = nr_strndup(Z_STRVAL_P(message_zv), Z_STRLEN_P(message_zv)); } - nr_php_zval_free(&message_zv); return message; } @@ -725,7 +721,7 @@ nr_status_t nr_php_error_record_exception(nrtxn_t* txn, nr_free(klass); nr_free(message); nr_free(stack_json); - nr_php_zval_free(&stack_trace); + Z_DELREF_P(stack_trace); return NR_SUCCESS; } diff --git a/agent/php_execute.c b/agent/php_execute.c index c02a033f2..628f5244a 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -1873,29 +1873,6 @@ static void nr_php_instrument_func_begin(NR_EXECUTE_PROTO) { } wraprec = nr_php_get_wraprec(execute_data->func); - /* - * Check if it's a custom error handler. We don't need to wait for - * fcall_end to record the error to the transaction; it can be done earlier - * in fcall_begin. However, we must wait until fcall_end to add the error - * to any possible segment(s), as at this point we do not know when it - * will be caught. - */ - if (NULL != wraprec && wraprec->is_exception_handler) { - /* - * Before starting the error handler segment, put the error it handled on - * the transaction. The choice of E_ERROR for the error level is - * basically arbitrary, but matches the error level PHP uses if there - * isn't an exception handler, so this should give more consistency for - * the user in terms of what they'll see with and without an exception - * handler installed. - */ - zval* exception - = nr_php_get_user_func_arg(1, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); - nr_php_error_record_exception( - NRPRG(txn), exception, nr_php_error_get_priority(E_ERROR), false, - "Uncaught exception ", &NRPRG(exception_filters) TSRMLS_CC); - } - segment = nr_php_stacked_segment_init(segment); if (nrunlikely(NULL == segment)) { nrl_verbosedebug(NRL_AGENT, "Error initializing stacked segment."); @@ -1984,11 +1961,18 @@ static void nr_php_instrument_func_end(NR_EXECUTE_PROTO) { if (wraprec && wraprec->is_exception_handler) { /* - * An exception handler sets the transaction exception in fcall_begin - * and does not have a return value, like an fcall_end during an - * uncaught exception. This code path is here to simplify and - * explicitly enumerate the possible cases. + * After running the exception handler segment, create an error from + * the exception it handled, and save the error in the transaction. + * The choice of E_ERROR for the error level is basically arbitrary, + * but matches the error level PHP uses if there isn't an exception + * handler, so this should give more consistency for the user in terms + * of what they'll see with and without an exception handler installed. */ + zval* exception + = nr_php_get_user_func_arg(1, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + nr_php_error_record_exception( + NRPRG(txn), exception, nr_php_error_get_priority(E_ERROR), false, + "Uncaught exception ", &NRPRG(exception_filters) TSRMLS_CC); } else if (NULL == nr_php_get_return_value(NR_EXECUTE_ORIG_ARGS)) { /* * Having no return value (and not being an exception handler) indicates From 26648fe70429c3c40c5bdf8a895deaef93ae0ec5 Mon Sep 17 00:00:00 2001 From: ZNeumann Date: Tue, 6 Feb 2024 09:43:24 -0700 Subject: [PATCH 51/56] feat(oapi): use segment pool instead of stacked segments (#776) Because OAPI doesn't create a nice callstack that replicates the PHP callstack, stacked segments make little/no sense in its context. Instead, we want to utilize the segment pool available in txn's. force_current_segment was how stacked segments were able to maintain their context in a txn while not being on the segment stack. For OAPI, there is no reason to use this field, as the current segment will just be the top of the txn's segment stack. --------- Co-authored-by: bduranleau-nr <106178551+bduranleau-nr@users.noreply.github.com> Co-authored-by: Michal Nowacki --- agent/fw_drupal.c | 11 +++ agent/lib_zend_http.c | 11 ++- agent/php_api_datastore.c | 8 +- agent/php_execute.c | 45 ++++++++-- agent/php_stacked_segment.c | 24 +----- agent/php_stacked_segment.h | 91 ++------------------ agent/php_txn.c | 12 +++ agent/tests/test_php_stacked_segment.c | 112 ++----------------------- 8 files changed, 89 insertions(+), 225 deletions(-) diff --git a/agent/fw_drupal.c b/agent/fw_drupal.c index 02a932fd5..67efc1dab 100644 --- a/agent/fw_drupal.c +++ b/agent/fw_drupal.c @@ -253,8 +253,19 @@ NR_PHP_WRAPPER(nr_drupal_http_request_before) { * checking a counter. */ if (1 == NRPRG(drupal_http_request_depth)) { + /* + * Parent this segment to the txn root so as to not interfere with + * the OAPI default segment stack, which is used to dispatch to the + * after function properly + */ NRPRG(drupal_http_request_segment) = nr_segment_start(NRPRG(txn), NULL, NULL); + /* + * The new segment needs to have the wraprec data attached, so that + * fcall_end is able to properly dispatch to the after wrapper, as + * this new segment is now at the top of the segment stack. + */ + NRPRG(drupal_http_request_segment)->wraprec = auto_segment->wraprec; } } NR_PHP_WRAPPER_END diff --git a/agent/lib_zend_http.c b/agent/lib_zend_http.c index ef8a416b8..136244c1a 100644 --- a/agent/lib_zend_http.c +++ b/agent/lib_zend_http.c @@ -361,7 +361,8 @@ NR_PHP_WRAPPER_START(nr_zend_http_client_request) { * We have to manually force this segment as the current segment on * the transaction, otherwise the previously forced stacked segment * will be used as parent for segments that should rather be - * parented to this segment. + * parented to this segment. For OAPI, no stacked segments are used + * and therefore parenting is automatically handled. * * This solution of purely for Zend_Http_Client issues related to older * versions of the Zend framework. @@ -379,7 +380,10 @@ NR_PHP_WRAPPER_START(nr_zend_http_client_request) { * Implement a solution just for `Zend_Http_Client` (e. g. deleting all child * segments from the `request` related segment). */ +#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ + || defined OVERWRITE_ZEND_EXECUTE_DATA /* not OAPI */ NRTXN(force_current_segment) = segment; +#endif nr_zend_http_client_request_add_request_headers(this_var, segment TSRMLS_CC); @@ -410,7 +414,7 @@ NR_PHP_WRAPPER_START(nr_zend_http_client_request) { * * Therefore we delete all children of the segment. Afterwards we set * the forced current of the transaction back to the segments parent, - * thus restoring the stacked segment stack. + * thus restoring the stacked segment stack (non-OAPI only). */ if (segment) { @@ -421,7 +425,10 @@ NR_PHP_WRAPPER_START(nr_zend_http_client_request) { nr_segment_discard(&child); } +#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ + || defined OVERWRITE_ZEND_EXECUTE_DATA /* not OAPI */ NRTXN(force_current_segment) = segment->parent; +#endif } nr_segment_external_end(&segment, &external_params); diff --git a/agent/php_api_datastore.c b/agent/php_api_datastore.c index 731723d11..2cddcf4bd 100644 --- a/agent/php_api_datastore.c +++ b/agent/php_api_datastore.c @@ -146,6 +146,8 @@ PHP_FUNCTION(newrelic_record_datastore_segment) { if (instrument) { segment = nr_segment_start(NRPRG(txn), NULL, NULL); +#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ + || defined OVERWRITE_ZEND_EXECUTE_DATA /* not OAPI */ /* * We have to manually force this segment as the current segment on * the transaction, otherwise the previously forced stacked segment @@ -153,6 +155,7 @@ PHP_FUNCTION(newrelic_record_datastore_segment) { * parented to this segment. */ NRTXN(force_current_segment) = segment; +#endif } retval = nr_php_call_fcall_info(fci, fcc); ZVAL_ZVAL(return_value, retval, 0, 1); @@ -198,14 +201,17 @@ PHP_FUNCTION(newrelic_record_datastore_segment) { * * Therefore we delete all children of the segment. Afterwards we set * the forced current of the transaction back to the segments parent, - * thus restoring the stacked segment stack. + * thus restoring the stacked segment stack (for non-OAPI). */ if (segment) { for (size_t i = 0; i < nr_segment_children_size(&segment->children); i++) { nr_segment_t* child = nr_segment_children_get(&segment->children, i); nr_segment_discard(&child); } +#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ + || defined OVERWRITE_ZEND_EXECUTE_DATA /* not OAPI */ NRTXN(force_current_segment) = segment->parent; +#endif } nr_segment_datastore_end(&segment, &node_params); diff --git a/agent/php_execute.c b/agent/php_execute.c index 628f5244a..c3a16fbf4 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -1247,6 +1247,8 @@ static inline void nr_php_execute_segment_add_metric( * custom metric? * * Params : 1. The stacked segment to end. + * OAPI does not use stacked segments, and should pass + * a heap allocated segment instead * 2. The function naming metadata. * 3. Whether to create a metric. */ @@ -1272,7 +1274,13 @@ static inline void nr_php_execute_segment_end( || nr_vector_size(stacked->metrics) || stacked->id || stacked->attributes || stacked->error) { +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + // There are no stacked segments for OAPI. + nr_segment_t* s = stacked; +#else nr_segment_t* s = nr_php_stacked_segment_move_to_heap(stacked TSRMLS_CC); +#endif // OAPI nr_php_execute_segment_add_metric(s, metadata, create_metric); /* @@ -1287,7 +1295,13 @@ static inline void nr_php_execute_segment_end( nr_segment_end(&s); } else { +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + // There are no stacked segments for OAPI. + nr_segment_discard(&stacked); +#else nr_php_stacked_segment_deinit(stacked TSRMLS_CC); +#endif // OAPI } } @@ -1298,6 +1312,8 @@ static inline void nr_php_execute_segment_end( * If the flag is NULL, then we've only added a couple of CPU instructions to * the call path and thus the overhead is (hopefully) very low. */ +#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ + || defined OVERWRITE_ZEND_EXECUTE_DATA /* not OAPI */ static void nr_php_execute_enabled(NR_EXECUTE_PROTO TSRMLS_DC) { int zcaught = 0; nrtime_t txn_start_time; @@ -1471,6 +1487,7 @@ static void nr_php_execute_show(NR_EXECUTE_PROTO TSRMLS_DC) { nr_php_show_exec_return(NR_EXECUTE_ORIG_ARGS TSRMLS_CC); } } +#endif // not OAPI static void nr_php_max_nesting_level_reached(TSRMLS_D) { /* @@ -1509,6 +1526,8 @@ static void nr_php_max_nesting_level_reached(TSRMLS_D) { * the presence of longjmp as from zend_bailout when processing zend internal * errors, as for example when calling php_error. */ +#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ + || defined OVERWRITE_ZEND_EXECUTE_DATA /* not OAPI */ void nr_php_execute(NR_EXECUTE_PROTO_OVERWRITE TSRMLS_DC) { /* * We do not use zend_try { ... } mechanisms here because zend_try @@ -1526,10 +1545,6 @@ void nr_php_execute(NR_EXECUTE_PROTO_OVERWRITE TSRMLS_DC) { * zend_catch is called to avoid catastrophe on the way to a premature * exit, maintaining this counter perfectly is not a necessity. */ -#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ - && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ - zval* func_return_value = NULL; -#endif NRPRG(php_cur_stack_depth) += 1; @@ -1556,6 +1571,7 @@ void nr_php_execute(NR_EXECUTE_PROTO_OVERWRITE TSRMLS_DC) { return; } +#endif // not OAPI static void nr_php_show_exec_internal(NR_EXECUTE_PROTO_OVERWRITE, const zend_function* func TSRMLS_DC) { @@ -1741,7 +1757,7 @@ void nr_php_user_instrumentation_from_opcache(TSRMLS_D) { * See nr_php_observer.h/c for more information. */ #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ - && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP8+ */ + && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP8+ and OAPI */ static void nr_php_observer_attempt_call_cufa_handler(NR_EXECUTE_PROTO) { NR_UNUSED_FUNC_RETURN_VALUE; @@ -1873,9 +1889,10 @@ static void nr_php_instrument_func_begin(NR_EXECUTE_PROTO) { } wraprec = nr_php_get_wraprec(execute_data->func); - segment = nr_php_stacked_segment_init(segment); + segment = nr_segment_start(NRPRG(txn), NULL, NULL); + if (nrunlikely(NULL == segment)) { - nrl_verbosedebug(NRL_AGENT, "Error initializing stacked segment."); + nrl_verbosedebug(NRL_AGENT, "Error starting segment."); return; } @@ -1945,7 +1962,7 @@ static void nr_php_instrument_func_end(NR_EXECUTE_PROTO) { /* * Get the current segment and return if null. */ - segment = NRTXN(force_current_segment); + segment = nr_txn_get_current_segment(NRPRG(txn), NULL); if (nrunlikely(NULL == segment)) { /* * Most likely caused by txn ending prematurely and closing all segments. We @@ -1954,6 +1971,10 @@ static void nr_php_instrument_func_end(NR_EXECUTE_PROTO) { return; } if (nrunlikely(NRPRG(txn)->segment_root == segment)) { + /* + * There should be no fcall_end associated with the segment root, If we are + * here, it is most likely due to an API call to newrelic_end_transaction + */ return; } @@ -2027,7 +2048,7 @@ static void nr_php_instrument_func_end(NR_EXECUTE_PROTO) { * If there is no custom instrumentation and tt detail is not more than 0, * do not record the segment */ - nr_php_stacked_segment_deinit(segment); + nr_segment_discard(&segment); return; } /* @@ -2048,6 +2069,12 @@ static void nr_php_instrument_func_end(NR_EXECUTE_PROTO) { return; } + /* + * Reassign segment to the current segment, as some before/after wraprecs + * start and then stop a segment. If that happened, we want to ensure we + * get the now-current segment + */ + segment = nr_txn_get_current_segment(NRPRG(txn), NULL); nr_php_execute_metadata_init(&metadata, NR_OP_ARRAY); nr_php_execute_segment_end(segment, &metadata, create_metric); nr_php_execute_metadata_release(&metadata); diff --git a/agent/php_stacked_segment.c b/agent/php_stacked_segment.c index f200e0eb3..d285296ad 100644 --- a/agent/php_stacked_segment.c +++ b/agent/php_stacked_segment.c @@ -7,6 +7,8 @@ #include "util_logging.h" #include "php_execute.h" #include "php_error.h" +#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ + || defined OVERWRITE_ZEND_EXECUTE_DATA /* not OAPI */ /* * Purpose : Add a stacked segment to the stacked segment stack. The top @@ -28,13 +30,6 @@ nr_segment_t* nr_php_stacked_segment_init(nr_segment_t* stacked TSRMLS_DC) { if (!nr_php_recording(TSRMLS_C)) { return NULL; } -#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ - && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ - stacked = nr_calloc(1, sizeof(nr_segment_t)); - if (NULL == stacked) { - return NULL; - } -#endif stacked->txn = NRPRG(txn); NR_PHP_CURRENT_STACKED_PUSH(stacked); @@ -54,13 +49,6 @@ void nr_php_stacked_segment_deinit(nr_segment_t* stacked TSRMLS_DC) { nr_free(stacked->id); NR_PHP_CURRENT_STACKED_POP(stacked); -#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ - && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ - /* - * This is allocated differently for OAPI and hence needs to be freed. - */ - nr_free(stacked); -#endif } void nr_php_stacked_segment_unwind(TSRMLS_D) { @@ -101,13 +89,7 @@ nr_segment_t* nr_php_stacked_segment_move_to_heap( nr_segment_set_parent(s, stacked->parent); NR_PHP_CURRENT_STACKED_POP(stacked); -#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ - && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ - /* - * This is allocated differently for OAPI and hence needs to be freed. - */ - nr_free(stacked); -#endif return s; } +#endif /* not OAPI */ diff --git a/agent/php_stacked_segment.h b/agent/php_stacked_segment.h index b85a6c3c9..b32f8afe9 100644 --- a/agent/php_stacked_segment.h +++ b/agent/php_stacked_segment.h @@ -165,97 +165,15 @@ * Stacked segments cannot be used to model async segments. */ -// clang-format off /* - * Observer API paradigm. - * - * - * The workflow of using stacked segments in connection with regular - * segments is complicated. It's best illustrated by a short ASCII - * cartoon. - * - * root < root root - * | | - * *A < *A - * | - * *B - * - * We start out with a root segment, OAPI calls nr_php_observer_fcall_begin for - * A, and it starts stacked segment *A and then nr_php_observer_fcall_begin(B) - * starts stacked segment *B as a child of *A. - * - * root root root - * | | | - * *A < *A *A < - * | - * *C < - * - * *nr_php_observer_fcall_end(B) decides to discard *B, and *A is the current - * segment again. nr_php_observer_fcall_begin(C) starts *C gets started as child - * of *A and when nr_php_observer_fcall_end(C) is called, *C gets discarded too. - * Note that up to this point, no segment except the root segment ever was - * allocated via the slab; however, stacked segments are being calloced in - * stacked_segment_init. - * - * root root root - * | | | - * *A < *A *A - * | | - * *D < *D - * | - * *E < - * - * In a next exciting step, nr_php_observer_fcall_begin(D) starts stacked - * segment *D as child of *A and nr_php_observer_fcall_begin(E) *E is started as - * child of *D. - * - * root root - * | | - * *A *A < - * | | - * *D < e - * | - * e - * - * Now something new happens. nr_php_observer_fcall_end(E) decides to keep the - * stacked segment *E. We copy the contents of the stacked segment *E into a - * segment e we obtained from the slab allocator, and we make e a child of the - * stacked segment *D. nr_php_observer_fcall_end(D) discards stacked segment *D - * and its child e is made a child of *D's parent *A. - * - * root root root - * | | | - * *A *A < *A - * / \ | / \ - * e *F < e e *G < - * - * More of the same. nr_php_observer_fcall_begin(F) creates a stacked segment *F - * as child of A and nr_php_observer_fcall_end(F) eventually discards it. - * nr_php_observer_fcall_begin(G) then creates a stacked segment *G. - * - * root root < root - * | | / \ - * *A < a a *H - * / \ / \ / \ - * e g e g e g - * - * Finally nr_php_observer_fcall_end(G) also decides to keep *G. Again, it is - * turned into a regular segment g and made a child of *A. Then we decide to - * keep *A, turning it into regular segment a and making it a child of the root - * segment. Afterward a nr_php_observer_fcall_begin(H) starts stacked segment *H - * as child of the root segment. - * - * Note that with this workflow, we went through the - * nr_segment_start/nr_segment_discard cycle for only 3 times, - * although we used 8 different segments. For the remaining 5 segments, we - * went through the stacked segment cycle. - * - * Also note that this only works with segments on the default parent stack. - * Stacked segments cannot be used to model async segments. + * Observer API paradigm: OAPI cannot make use of stacked segments and + * therefore uses the txn segment stack API. */ // clang-format on #include "php_agent.h" +#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ + || defined OVERWRITE_ZEND_EXECUTE_DATA /* not OAPI */ /* * Purpose : Initialize a stacked segment. @@ -315,4 +233,5 @@ extern void nr_php_stacked_segment_unwind(TSRMLS_D); extern nr_segment_t* nr_php_stacked_segment_move_to_heap( nr_segment_t* stacked TSRMLS_DC); +#endif /* not OAPI */ #endif /* PHP_STACKED_SEGMENT_HDR */ diff --git a/agent/php_txn.c b/agent/php_txn.c index 34e332c0d..a3cc2fe82 100644 --- a/agent/php_txn.c +++ b/agent/php_txn.c @@ -844,11 +844,14 @@ nr_status_t nr_php_txn_begin(const char* appnames, nr_php_txn_send_metrics_once(NRPRG(txn) TSRMLS_CC); +#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ + || defined OVERWRITE_ZEND_EXECUTE_DATA /* not OAPI */ /* * Disable automated parenting for the default parent context. See * php_stacked_segment.h for further details. */ nr_txn_force_current_segment(NRPRG(txn), NRTXN(segment_root)); +#endif nr_php_collect_x_request_start(TSRMLS_C); nr_php_set_initial_path(NRPRG(txn) TSRMLS_CC); @@ -1067,7 +1070,16 @@ nr_status_t nr_php_txn_end(int ignoretxn, int in_post_deactivate TSRMLS_DC) { * by calling newrelic_end_transaction inside nested function scopes) * the stack of stacked segments has to be cleaned up. */ +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + nr_segment_t* segment = nr_txn_get_current_segment(NRPRG(txn), NULL); + while(NULL != segment && segment != NRTXN(segment_root)) { + nr_segment_end(&segment); + segment = nr_txn_get_current_segment(NRPRG(txn), NULL); + } +#else nr_php_stacked_segment_unwind(TSRMLS_C); +#endif nrl_verbosedebug(NRL_TXN, "%s: Ending the transaction and stack depth = %d", __func__, NRPRG(php_cur_stack_depth)); diff --git a/agent/tests/test_php_stacked_segment.c b/agent/tests/test_php_stacked_segment.c index 785b9d625..7d71f5ecd 100644 --- a/agent/tests/test_php_stacked_segment.c +++ b/agent/tests/test_php_stacked_segment.c @@ -15,110 +15,8 @@ tlib_parallel_info_t parallel_info = {.suggested_nthreads = -1, .state_size = 0}; -#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ - && !defined OVERWRITE_ZEND_EXECUTE_DATA /* PHP 8.0+ and OAPI */ -static void test_start_end_discard(TSRMLS_D) { - nr_segment_t* stacked = NULL; - nr_segment_t* segment; - - tlib_php_request_start(); - - /* - * Initial state: current segment forced to root - */ - tlib_pass_if_ptr_equal("current stacked segment forced to root", - NRTXN(segment_root), NRTXN(force_current_segment)); - - /* - * Add a stacked segment. - */ - stacked = nr_php_stacked_segment_init(stacked); - - tlib_pass_if_not_null("current stacked forced to stacked should not be null", - stacked); - tlib_pass_if_ptr_equal("current stacked segment has txn", stacked->txn, - NRPRG(txn)); - tlib_pass_if_ptr_equal("current stacked forced to stacked", stacked, - NRTXN(force_current_segment)); - - /* - * Discard a stacked segment. - */ - nr_php_stacked_segment_deinit(stacked); - - tlib_pass_if_ptr_equal("current stacked segment forced to root", - NRTXN(segment_root), NRTXN(force_current_segment)); - tlib_pass_if_size_t_equal( - "no segment created", 0, - nr_segment_children_size(&NRTXN(segment_root)->children)); - - /* - * Add another stacked segment. - */ - stacked = nr_php_stacked_segment_init(stacked TSRMLS_CC); - - tlib_pass_if_ptr_equal("current stacked segment has txn", stacked->txn, - NRPRG(txn)); - tlib_pass_if_ptr_equal("current stacked forced to stacked", stacked, - NRTXN(force_current_segment)); - - /* - * End a stacked segment. - */ - segment = nr_php_stacked_segment_move_to_heap(stacked TSRMLS_CC); - nr_segment_end(&segment); - - tlib_pass_if_true("moved segment is different from stacked segment", - segment != stacked, "%p!=%p", segment, stacked); - tlib_pass_if_ptr_equal("current stacked segment forced to root", - NRTXN(segment_root), NRTXN(force_current_segment)); - tlib_pass_if_size_t_equal( - "no segment created", 1, - nr_segment_children_size(&NRTXN(segment_root)->children)); - - tlib_php_request_end(); -} - -static void test_unwind(TSRMLS_D) { - nr_segment_t* stacked_1 = NULL; - nr_segment_t* stacked_2 = NULL; - nr_segment_t* stacked_3 = NULL; - nr_segment_t* segment; - - tlib_php_request_start(); - - /* - * Add stacked segments. - */ - stacked_1 = nr_php_stacked_segment_init(stacked_1 TSRMLS_CC); - stacked_2 = nr_php_stacked_segment_init(stacked_2 TSRMLS_CC); - stacked_3 = nr_php_stacked_segment_init(stacked_3 TSRMLS_CC); - - /* - * Add a regular segment. - */ - segment = nr_segment_start(NRPRG(txn), NULL, NULL); - nr_segment_end(&segment); - - /* - * Sleep; otherwise, unwind will dump the short segments and fail. - */ - nr_msleep(500); - - /* - * Unwind the stacked segment stack. - */ - nr_php_stacked_segment_unwind(TSRMLS_C); - - tlib_pass_if_size_t_equal( - "one child segment of root", 1, - nr_segment_children_size(&NRTXN(segment_root)->children)); - - tlib_pass_if_size_t_equal("4 segments in total ", 4, NRTXN(segment_count)); - - tlib_php_request_end(); -} -#else +#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ + || defined OVERWRITE_ZEND_EXECUTE_DATA /* not OAPI */ static void test_start_end_discard(TSRMLS_D) { nr_segment_t stacked = {0}; nr_segment_t* segment; @@ -218,8 +116,6 @@ static void test_unwind(TSRMLS_D) { tlib_php_request_end(); } -#endif - void test_main(void* p NRUNUSED) { #if defined(ZTS) && !defined(PHP7) void*** tsrm_ls = NULL; @@ -229,3 +125,7 @@ void test_main(void* p NRUNUSED) { test_unwind(TSRMLS_C); tlib_php_engine_destroy(TSRMLS_C); } +#else +void test_main(void* p NRUNUSED) { +} +#endif /* not OAPI */ From 5d7ee9bc3c4cead51db219e946054eade3d24f54 Mon Sep 17 00:00:00 2001 From: ZNeumann Date: Tue, 6 Feb 2024 10:12:24 -0700 Subject: [PATCH 52/56] fix: correctly instrument WebdisConnection executeCommand in OAPI (#779) Co-authored-by: bduranleau-nr <106178551+bduranleau-nr@users.noreply.github.com> --- agent/lib_predis.c | 53 ++++++++++++++++++++++++++++++++++++++++++--- agent/php_wrapper.h | 12 +++++----- 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/agent/lib_predis.c b/agent/lib_predis.c index cbbc76104..04ed2a85c 100644 --- a/agent/lib_predis.c +++ b/agent/lib_predis.c @@ -747,7 +747,44 @@ NR_PHP_WRAPPER(nr_predis_pipeline_executePipeline_clean) { predis_executePipeline_handle_stack(); } NR_PHP_WRAPPER_END -#endif /* OAPI */ + +NR_PHP_WRAPPER(nr_predis_webdisconnection_executeCommand_before) { + (void)wraprec; + + nr_segment_t* segment = NULL; + segment = nr_segment_start(NRPRG(txn), NULL, NULL); + segment->wraprec = auto_segment->wraprec; +} +NR_PHP_WRAPPER_END + +NR_PHP_WRAPPER(nr_predis_webdisconnection_executeCommand_after) { + (void)wraprec; + char* operation = NULL; + zval* command_obj = NULL; + zval* conn = NULL; + nr_segment_datastore_params_t params = { + .datastore = { + .type = NR_DATASTORE_REDIS, + }, + }; + + command_obj = nr_php_arg_get(1, NR_EXECUTE_ORIG_ARGS); + conn = nr_php_scope_get(NR_EXECUTE_ORIG_ARGS); + + operation = nr_predis_get_operation_name_from_object(command_obj); + params.operation = operation; + + params.instance = nr_predis_retrieve_datastore_instance(conn); + + nr_segment_datastore_end(&auto_segment, ¶ms); + + nr_free(operation); + nr_php_arg_release(&command_obj); + nr_php_scope_release(&conn); +} +NR_PHP_WRAPPER_END + +#else NR_PHP_WRAPPER(nr_predis_webdisconnection_executeCommand) { char* operation = NULL; @@ -781,6 +818,7 @@ NR_PHP_WRAPPER(nr_predis_webdisconnection_executeCommand) { nr_php_scope_release(&conn); } NR_PHP_WRAPPER_END +#endif /* OAPI */ void nr_predis_enable(TSRMLS_D) { /* @@ -815,6 +853,15 @@ void nr_predis_enable(TSRMLS_D) { nr_predis_pipeline_executePipeline, nr_predis_pipeline_executePipeline_after, nr_predis_pipeline_executePipeline_clean); + /* + * Instrument Webdis connections, since they don't use the same + * writeRequest()/readResponse() pair as the other connection types. + */ + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("Predis\\Connection\\WebdisConnection::executeCommand"), + nr_predis_webdisconnection_executeCommand_before, + nr_predis_webdisconnection_executeCommand_after, + nr_predis_webdisconnection_executeCommand_after); #else nr_php_wrap_user_function( NR_PSTR("Predis\\Pipeline\\Pipeline::executePipeline"), @@ -828,8 +875,6 @@ void nr_predis_enable(TSRMLS_D) { nr_php_wrap_user_function( NR_PSTR("Predis\\Pipeline\\FireAndForget::executePipeline"), nr_predis_pipeline_executePipeline TSRMLS_CC); -#endif /* OAPI */ - /* * Instrument Webdis connections, since they don't use the same * writeRequest()/readResponse() pair as the other connection types. @@ -837,4 +882,6 @@ void nr_predis_enable(TSRMLS_D) { nr_php_wrap_user_function( NR_PSTR("Predis\\Connection\\WebdisConnection::executeCommand"), nr_predis_webdisconnection_executeCommand TSRMLS_CC); +#endif /* OAPI */ + } diff --git a/agent/php_wrapper.h b/agent/php_wrapper.h index d89a1ab5a..27e0dba91 100644 --- a/agent/php_wrapper.h +++ b/agent/php_wrapper.h @@ -10,7 +10,7 @@ #include "util_logging.h" /* - * Wrapper writing example: + * Wrapper writing example *pre-OAPI*: * * Using the functions below, you can register a wrapper for either a named * user function or directly on a zend_function pointer. This wrapper needs to @@ -83,14 +83,16 @@ * 3. Delegation: you can delegate from any wrapper to another wrapper with * NR_PHP_WRAPPER_DELEGATE (foo), provided the original function hasn't * already been called. - * + */ + + /* * OAPI updates: * There are now before, after, and clean callbacks. * 1) before_callback gets called when OAPI triggers the begin function hook. * 2) after_callback gets called when OAPI triggers the end function hook. - * 3) clean_callback gets called in the case of dangling segments that occur - * because an exception causes the end function hook to NOT be called and thus - * the clean function resets any variables. + * 3) clean_callback gets called in the case of an exception, because the + * return value will be null, so the after_callback might not function + * correctly. Use clean_callback to reset any variables or states. * 4) unless explicitly setting any of the above callbacks, the default * callback is set to after_callback. * From 958d85958d67436d120726c12b6d7b5628bc90d7 Mon Sep 17 00:00:00 2001 From: ZNeumann Date: Thu, 8 Feb 2024 13:04:10 -0700 Subject: [PATCH 53/56] feat: faster segment de-parenting via index tracking (#801) Segment order does not matter, as the collector reconstructs the tree with timing and parenting info. As such, we can remove segments with O(1) speed instead of O(N). This is an alternative approach than https://github.com/newrelic/newrelic-php-agent/pull/780 and only 1 of the 2 should be merged. --------- Co-authored-by: Michal Nowacki --- axiom/nr_segment.c | 17 +++++- axiom/nr_segment.h | 19 ++++++- axiom/nr_segment_children.c | 5 +- axiom/nr_segment_children.h | 55 +++++++++++-------- axiom/tests/test_segment_children.c | 11 ++++ axiom/tests/test_segment_private.c | 15 ++--- ...ion_tracer_max_segments_with_datastore.php | 50 +++++++++++------ ...racer_max_segments_with_datastore.php5.php | 46 +++++++++++----- 8 files changed, 151 insertions(+), 67 deletions(-) diff --git a/axiom/nr_segment.c b/axiom/nr_segment.c index e8635b6b6..fa91cf1b7 100644 --- a/axiom/nr_segment.c +++ b/axiom/nr_segment.c @@ -883,7 +883,9 @@ bool nr_segment_discard(nr_segment_t** segment_ptr) { } /* Unhook the segment from its parent. */ - nr_segment_children_remove(&segment->parent->children, segment); + if (!nr_segment_children_remove(&segment->parent->children, segment)) { + return false; + } /* Reparent all children. */ nr_segment_children_reparent(&segment->children, segment->parent); @@ -1237,3 +1239,16 @@ bool nr_segment_attributes_user_txn_event_add(nr_segment_t* segment, return (NR_SUCCESS == status); } + +ssize_t nr_segment_get_child_ix(const nr_segment_t* segment) { + if (NULL == segment) { + return -1; + } + return segment->child_ix; +} + +void nr_segment_set_child_ix(nr_segment_t* segment, size_t ix) { + if (NULL != segment) { + segment->child_ix = ix; + } +} diff --git a/axiom/nr_segment.h b/axiom/nr_segment.h index 591612c1f..5d68e9226 100644 --- a/axiom/nr_segment.h +++ b/axiom/nr_segment.h @@ -141,6 +141,7 @@ typedef struct _nr_segment_t { /* Tree related stuff. */ nr_segment_t* parent; nr_segment_children_t children; + size_t child_ix; /* index of this segment in its parent->children vector */ nr_segment_color_t color; /* Generic segment fields. */ @@ -169,7 +170,6 @@ typedef struct _nr_segment_t { */ nr_vector_t* metrics; /* Metrics to be created by this segment. */ nr_exclusive_time_t* exclusive_time; /* Exclusive time. - This is only calculated after the transaction has ended; before then, this will be NULL. */ @@ -647,4 +647,21 @@ extern void nr_segment_record_exception(nr_segment_t* segment, const char* error_message, const char* error_class); +/* + * Purpose : Gets the child_ix of a segment. + * + * Paramas : 1. The pointer to the segment + * + * Returns : the child_ix, or -1 if passed NULL + */ +extern ssize_t nr_segment_get_child_ix(const nr_segment_t*); + +/* + * Purpose : Sets the child_ix of a segment. + * + * Paramas : 1. The pointer to the segment + * 2. the child_ix to set + */ +extern void nr_segment_set_child_ix(nr_segment_t*, size_t); + #endif /* NR_SEGMENT_HDR */ diff --git a/axiom/nr_segment_children.c b/axiom/nr_segment_children.c index a1aa5bb96..8767319da 100644 --- a/axiom/nr_segment_children.c +++ b/axiom/nr_segment_children.c @@ -88,14 +88,15 @@ bool nr_segment_children_reparent(nr_segment_children_t* children, return true; } + parent_size = nr_segment_children_size(&new_parent->children); + req_parent_size = size + parent_size; for (i = 0; i < size; i++) { nr_segment_t* child = nr_segment_children_get(children, i); child->parent = new_parent; + child->child_ix = parent_size+i; } - parent_size = nr_segment_children_size(&new_parent->children); - req_parent_size = size + parent_size; if (req_parent_size > NR_SEGMENT_CHILDREN_PACKED_LIMIT) { nr_segment_children_migrate_to_vector(&new_parent->children); } diff --git a/axiom/nr_segment_children.h b/axiom/nr_segment_children.h index aff112dbf..a632d65df 100644 --- a/axiom/nr_segment_children.h +++ b/axiom/nr_segment_children.h @@ -13,6 +13,7 @@ #define NR_SEGMENT_CHILDREN_HDR #include +#include #define NR_SEGMENT_CHILDREN_PACKED_LIMIT 8 @@ -20,10 +21,12 @@ #include "util_vector.h" /* - * Forward declaration of nr_segment_t, since we have a circular dependency with - * nr_segment.h. + * Forward declaration of nr_segment_t, and some getters/setters, + * since we have a circular dependency with nr_segment.h. */ typedef struct _nr_segment_t nr_segment_t; +extern ssize_t nr_segment_get_child_ix(const nr_segment_t*); +extern void nr_segment_set_child_ix(nr_segment_t*, size_t); /* * The data structure for packed children, holding an array of children and the @@ -137,12 +140,15 @@ static inline bool nr_segment_children_add(nr_segment_children_t* children, if (new_count > NR_SEGMENT_CHILDREN_PACKED_LIMIT) { // We're about to overflow the packed array; migrate to a vector. nr_segment_children_migrate_to_vector(children); + nr_segment_set_child_ix(child, nr_vector_size(&children->vector)); nr_segment_children_add_vector(children, child); } else { children->packed.elements[children->packed.count] = child; children->packed.count = new_count; + nr_segment_set_child_ix(child, new_count - 1); } } else { + nr_segment_set_child_ix(child, nr_vector_size(&children->vector)); nr_segment_children_add_vector(children, child); } @@ -165,36 +171,41 @@ static inline bool nr_segment_children_remove(nr_segment_children_t* children, } if (children->is_packed) { - size_t i; + size_t ix = (size_t)nr_segment_get_child_ix(child); // safe to cast, child != NULL asserted earlier const size_t end = children->packed.count - 1; + nr_segment_t* temp; - if (child == children->packed.elements[end]) { - // The simple case: the child is the last element, so we can just - // decrement the count and we're done. - children->packed.count -= 1; - return true; + if (ix > end) { + return false; } - for (i = 0; i < end; i++) { - if (child == children->packed.elements[i]) { - nr_memmove(&children->packed.elements[i], - &children->packed.elements[i + 1], - sizeof(nr_segment_t*) * (children->packed.count - i - 1)); - children->packed.count -= 1; - return true; - } - } + // Swap'n'Pop + temp = children->packed.elements[end]; + nr_segment_set_child_ix(temp, ix); + children->packed.elements[ix] = temp; + children->packed.count -= 1; + } else { - size_t index; + size_t index = (size_t)nr_segment_get_child_ix(child); // safe to cast, child != NULL asserted earlier + nr_segment_t* temp; + if (index >= nr_vector_size(&children->vector)) { + return false; + } - if (nr_vector_find_first(&children->vector, child, NULL, NULL, &index)) { - void* element; + if (!nr_vector_get_element(&children->vector, nr_vector_size(&children->vector)-1, (void**)&temp)) { + return false; + } + nr_segment_set_child_ix(temp, index); - return nr_vector_remove(&children->vector, index, &element); + if (!nr_vector_replace(&children->vector, index, temp)) { + return false; + } + if (!nr_vector_pop_back(&children->vector, (void**)&temp)) { + return false; } } - return false; + return true; } /* diff --git a/axiom/tests/test_segment_children.c b/axiom/tests/test_segment_children.c index 69d6ada2d..a13e166cb 100644 --- a/axiom/tests/test_segment_children.c +++ b/axiom/tests/test_segment_children.c @@ -179,6 +179,17 @@ static void test_segment_children_remove(nr_segment_children_t* children, tlib_pass_if_bool_equal("adding a child should succeed", true, nr_segment_children_add(children, &segments[i])); } + /* + * initialize the child_ix value of this segment so that the attempted + * removal does not check an uninitialized value. In the real operation + * of the agent, external constructs should prevent the attempted + * removal of uninitialized segments. + */ + segments[count].child_ix=count; + + tlib_pass_if_size_t_equal("adding elements should increment size", + count, + nr_segment_children_size(children)); tlib_pass_if_bool_equal( "removing a non-existent element should fail", false, diff --git a/axiom/tests/test_segment_private.c b/axiom/tests/test_segment_private.c index ce186c0ff..e586f1f4f 100644 --- a/axiom/tests/test_segment_private.c +++ b/axiom/tests/test_segment_private.c @@ -143,13 +143,9 @@ static void test_remove(void) { "Removing an existing segment from an array of children must " "reduce the number of used locations", nr_segment_children_size(&children), total_children - 1); - tlib_pass_if_false( - "Removing a non-existent segment from an array of children must not be " - "successful", - nr_segment_children_remove(&children, &first_born), "Expected false"); - tlib_pass_if_null( - "Removing the first born means the second born must not have a prev", - nr_segment_children_get_prev(&children, &second_born)); + tlib_pass_if_ptr_equal( + "Removing the first born means the second born must have a new prev", + nr_segment_children_get_prev(&children, &second_born), &fifth_born); tlib_pass_if_ptr_equal( "Removing the first born means the second born must still have a next", nr_segment_children_get_next(&children, &second_born), &third_born); @@ -176,8 +172,9 @@ static void test_remove(void) { "Removing an existing segment from an array of children must " "reduce the number of used locations", nr_segment_children_size(&children), total_children - 3); - tlib_pass_if_null("Removing the fifth born means the fourth has no next", - nr_segment_children_get_next(&children, &fourth_born)); + tlib_pass_if_ptr_equal( + "Removing the fifth born means the previous last element has a new next", + nr_segment_children_get_next(&children, &fourth_born), &second_born); /* Clean up the mocked array of children */ nr_segment_children_deinit(&children); diff --git a/tests/integration/ini/test_transaction_tracer_max_segments_with_datastore.php b/tests/integration/ini/test_transaction_tracer_max_segments_with_datastore.php index f6e2b0171..cd233aa81 100644 --- a/tests/integration/ini/test_transaction_tracer_max_segments_with_datastore.php +++ b/tests/integration/ini/test_transaction_tracer_max_segments_with_datastore.php @@ -43,9 +43,9 @@ [ "?? start time", "?? end time", "`1", { - "code.lineno": 125, + "code.lineno": 139, "code.filepath": "__FILE__", - "code.function": "my_function" + "code.function": "my_function_3" }, [] ], [ @@ -73,7 +73,7 @@ ], [ "OtherTransaction\/php__FILE__", - "Custom\/my_function", + "Custom\/my_function_3", "Datastore\/statement\/MySQL\/table\/select" ] ], @@ -94,7 +94,9 @@ "?? timeframe start", "?? timeframe stop", [ - [{"name":"Custom/my_function"}, [3, "??", "??", "??", "??", "??"]], + [{"name":"Custom/my_function_1"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Custom/my_function_2"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Custom/my_function_3"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/MySQL/all"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/MySQL/allOther"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/all"}, [1, "??", "??", "??", "??", "??"]], @@ -106,10 +108,14 @@ [{"name":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], [{"name":"OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], [{"name":"OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Supportability/api/add_custom_tracer"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/api/add_custom_tracer"}, [3, "??", "??", "??", "??", "??"]], [{"name":"Supportability/api/record_datastore_segment"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Custom/my_function", - "scope":"OtherTransaction/php__FILE__"}, [3, "??", "??", "??", "??", "??"]], + [{"name":"Custom/my_function_1", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Custom/my_function_2", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Custom/my_function_3", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/statement/MySQL/table/select", "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], @@ -119,21 +125,31 @@ ] */ - - - -function my_function(){ - time_nanosleep(0, 50000); // force non-zero duration for the segment not to be dropped; duration needs to be shorter than datastore segment +/* + * We add multiple functions of differing durations to ensure deterministic dropping of certain segments. + * Because multiple segments of the same parent are dropped by a swap-n-pop removal from their parent's + * children list, we need to be able to determistically know which of these 3 functions is shortest + */ +function my_function_1(){ + usleep(10); // force non-zero duration for the segment not to be dropped; duration needs to be shorter than datastore segment +} +function my_function_2(){ + usleep(2000); // force non-zero duration for the segment not to be dropped; duration needs to be shorter than datastore segment +} +function my_function_3(){ + usleep(4000); // force non-zero duration for the segment not to be dropped; duration needs to be shorter than datastore segment } -newrelic_add_custom_tracer("my_function"); +newrelic_add_custom_tracer("my_function_1"); +newrelic_add_custom_tracer("my_function_2"); +newrelic_add_custom_tracer("my_function_3"); -my_function(); -my_function(); -my_function(); +my_function_3(); +my_function_2(); +my_function_1(); newrelic_record_datastore_segment(function () { - time_nanosleep(0, 70000); return 42; // force non-zero duration for the segment not to be dropped; duration needs to be longer than user func segment + usleep(6000); return 42; // force non-zero duration for the segment not to be dropped; duration needs to be longer than user func segment }, array( 'product' => 'mysql', 'collection' => 'table', diff --git a/tests/integration/ini/test_transaction_tracer_max_segments_with_datastore.php5.php b/tests/integration/ini/test_transaction_tracer_max_segments_with_datastore.php5.php index c27e5d3be..496b522e8 100644 --- a/tests/integration/ini/test_transaction_tracer_max_segments_with_datastore.php5.php +++ b/tests/integration/ini/test_transaction_tracer_max_segments_with_datastore.php5.php @@ -62,7 +62,7 @@ ], [ "OtherTransaction\/php__FILE__", - "Custom\/my_function", + "Custom\/my_function_3", "Datastore\/statement\/MySQL\/table\/select" ] ], @@ -83,7 +83,9 @@ "?? timeframe start", "?? timeframe stop", [ - [{"name":"Custom/my_function"}, [3, "??", "??", "??", "??", "??"]], + [{"name":"Custom/my_function_1"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Custom/my_function_2"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Custom/my_function_3"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/MySQL/all"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/MySQL/allOther"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/all"}, [1, "??", "??", "??", "??", "??"]], @@ -95,10 +97,14 @@ [{"name":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], [{"name":"OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], [{"name":"OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Supportability/api/add_custom_tracer"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/api/add_custom_tracer"}, [3, "??", "??", "??", "??", "??"]], [{"name":"Supportability/api/record_datastore_segment"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Custom/my_function", - "scope":"OtherTransaction/php__FILE__"}, [3, "??", "??", "??", "??", "??"]], + [{"name":"Custom/my_function_1", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Custom/my_function_2", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Custom/my_function_3", + "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/statement/MySQL/table/select", "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], @@ -108,21 +114,31 @@ ] */ - - - -function my_function(){ - time_nanosleep(0, 50000); // force non-zero duration for the segment not to be dropped; duration needs to be shorter than datastore segment +/* + * We add multiple functions of differing durations to ensure deterministic dropping of certain segments. + * Because multiple segments of the same parent are dropped by a swap-n-pop removal from their parent's + * children list, we need to be able to determistically know which of these 3 functions is shortest + */ +function my_function_1(){ + usleep(10); // force non-zero duration for the segment not to be dropped; duration needs to be shorter than datastore segment +} +function my_function_2(){ + usleep(2000); // force non-zero duration for the segment not to be dropped; duration needs to be shorter than datastore segment +} +function my_function_3(){ + usleep(4000); // force non-zero duration for the segment not to be dropped; duration needs to be shorter than datastore segment } -newrelic_add_custom_tracer("my_function"); +newrelic_add_custom_tracer("my_function_1"); +newrelic_add_custom_tracer("my_function_2"); +newrelic_add_custom_tracer("my_function_3"); -my_function(); -my_function(); -my_function(); +my_function_3(); +my_function_2(); +my_function_1(); newrelic_record_datastore_segment(function () { - time_nanosleep(0, 70000); return 42; // force non-zero duration for the segment not to be dropped; duration needs to be longer than user func segment + usleep(6000); return 42; // force non-zero duration for the segment not to be dropped; duration needs to be longer than user func segment }, array( 'product' => 'mysql', 'collection' => 'table', From 55d3904da4e8fa3fe87d67873510c80f1c12035f Mon Sep 17 00:00:00 2001 From: Amber Sistla Date: Thu, 22 Feb 2024 14:00:22 -0700 Subject: [PATCH 54/56] feat(tests): Enable opcache by default for the integration runner. (#752) * Enable the opcache.so extension by default in the integration_runner * Ensure the opcache INI settings in the integration_runner environment are enabled (these can be overwritten in the test case INI section if needing to test without opcache) * Added few tests to ensure compatibility with opcache disabled and to demonstrate how to overwrite the value. --------- Co-authored-by: ZNeumann Co-authored-by: Zach Neumann Co-authored-by: Michal Nowacki --- src/integration_runner/main.go | 11 + .../opcache/disabled/opcache_test.inc | 12 + tests/integration/opcache/disabled/skipif.inc | 10 + .../opcache/disabled/test_computations.php | 75 +++++ .../opcache/disabled/test_even_odd_count.php | 81 +++++ .../disabled/test_recursion_no_segfault.php | 52 ++++ .../disabled/test_span_class_function.php | 279 ++++++++++++++++++ ..._span_events_are_created_from_segments.php | 115 ++++++++ ...n_events_are_created_upon_caught_error.php | 192 ++++++++++++ ...test_span_events_are_created_upon_exit.php | 120 ++++++++ ...events_are_created_upon_uncaught_error.php | 130 ++++++++ ...reated_upon_uncaught_handled_exception.php | 196 ++++++++++++ ...d_upon_uncaught_handled_exception.php7.php | 167 +++++++++++ ...ated_upon_uncaught_unhandled_exception.php | 167 +++++++++++ ...t_span_events_error_collector_disabled.php | 128 ++++++++ ...st_span_events_exception_caught_nested.php | 214 ++++++++++++++ ...vents_exception_caught_nested_rethrown.php | 193 ++++++++++++ ...n_events_exception_caught_notice_error.php | 166 +++++++++++ ...s_exception_caught_notice_error_nested.php | 242 +++++++++++++++ ...span_events_exception_caught_same_span.php | 136 +++++++++ ..._span_events_exception_uncaught_nested.php | 221 ++++++++++++++ ...est_span_events_exist_when_no_segments.php | 62 ++++ .../disabled/test_span_events_hsm_error.php | 134 +++++++++ .../test_span_events_notice_error.php | 154 ++++++++++ .../test_span_events_on_dt_off_cat_off.php | 47 +++ .../test_span_events_on_dt_off_cat_on.php | 47 +++ .../disabled/test_span_events_root_parent.php | 137 +++++++++ .../opcache/enabled/test_opcache.php | 87 ++++++ 28 files changed, 3575 insertions(+) create mode 100644 tests/integration/opcache/disabled/opcache_test.inc create mode 100644 tests/integration/opcache/disabled/skipif.inc create mode 100644 tests/integration/opcache/disabled/test_computations.php create mode 100644 tests/integration/opcache/disabled/test_even_odd_count.php create mode 100644 tests/integration/opcache/disabled/test_recursion_no_segfault.php create mode 100644 tests/integration/opcache/disabled/test_span_class_function.php create mode 100644 tests/integration/opcache/disabled/test_span_events_are_created_from_segments.php create mode 100644 tests/integration/opcache/disabled/test_span_events_are_created_upon_caught_error.php create mode 100644 tests/integration/opcache/disabled/test_span_events_are_created_upon_exit.php create mode 100644 tests/integration/opcache/disabled/test_span_events_are_created_upon_uncaught_error.php create mode 100644 tests/integration/opcache/disabled/test_span_events_are_created_upon_uncaught_handled_exception.php create mode 100644 tests/integration/opcache/disabled/test_span_events_are_created_upon_uncaught_handled_exception.php7.php create mode 100644 tests/integration/opcache/disabled/test_span_events_are_created_upon_uncaught_unhandled_exception.php create mode 100644 tests/integration/opcache/disabled/test_span_events_error_collector_disabled.php create mode 100644 tests/integration/opcache/disabled/test_span_events_exception_caught_nested.php create mode 100644 tests/integration/opcache/disabled/test_span_events_exception_caught_nested_rethrown.php create mode 100644 tests/integration/opcache/disabled/test_span_events_exception_caught_notice_error.php create mode 100644 tests/integration/opcache/disabled/test_span_events_exception_caught_notice_error_nested.php create mode 100644 tests/integration/opcache/disabled/test_span_events_exception_caught_same_span.php create mode 100644 tests/integration/opcache/disabled/test_span_events_exception_uncaught_nested.php create mode 100644 tests/integration/opcache/disabled/test_span_events_exist_when_no_segments.php create mode 100644 tests/integration/opcache/disabled/test_span_events_hsm_error.php create mode 100644 tests/integration/opcache/disabled/test_span_events_notice_error.php create mode 100644 tests/integration/opcache/disabled/test_span_events_on_dt_off_cat_off.php create mode 100644 tests/integration/opcache/disabled/test_span_events_on_dt_off_cat_on.php create mode 100644 tests/integration/opcache/disabled/test_span_events_root_parent.php create mode 100644 tests/integration/opcache/enabled/test_opcache.php diff --git a/src/integration_runner/main.go b/src/integration_runner/main.go index ce66b4316..1fb4d3fa3 100644 --- a/src/integration_runner/main.go +++ b/src/integration_runner/main.go @@ -50,6 +50,7 @@ var ( flagTime = flag.Bool("time", false, "time each test") flagMaxCustomEvents = flag.Int("max_custom_events", 30000, "value for newrelic.custom_events.max_samples_stored") flagWarnIsFail = flag.Bool("warnisfail", false, "warn result is treated as a fail") + flagOpcacheOff = flag.Bool("opcacheoff", false, "run without opcache. Some tests are intended to fail when run this way") // externalPort is the port on which we start a server to handle // external calls. @@ -346,6 +347,16 @@ func main() { ctx.Settings["newrelic.loglevel"] = *flagLoglevel } + if false == *flagOpcacheOff { + // PHP Modules common to all tests + ctx.Settings["zend_extension"] = "opcache.so" + + // PHP INI values common to all tests + // These settings can be overwritten by adding new values to the INI block + ctx.Settings["opcache.enable"] = "1" + ctx.Settings["opcache.enable_cli"] = "1" + } + // If the user provided a custom agent extension, use it. if len(*flagAgent) > 0 { ctx.Settings["extension"], _ = filepath.Abs(*flagAgent) diff --git a/tests/integration/opcache/disabled/opcache_test.inc b/tests/integration/opcache/disabled/opcache_test.inc new file mode 100644 index 000000000..a9cbeea51 --- /dev/null +++ b/tests/integration/opcache/disabled/opcache_test.inc @@ -0,0 +1,12 @@ +functionName(); + } +} + +for($i = 0; $i < 5; ++$i){ + new LittleClass; +} +echo "OK\n"; diff --git a/tests/integration/opcache/disabled/test_span_events_are_created_from_segments.php b/tests/integration/opcache/disabled/test_span_events_are_created_from_segments.php new file mode 100644 index 000000000..232c68835 --- /dev/null +++ b/tests/integration/opcache/disabled/test_span_events_are_created_from_segments.php @@ -0,0 +1,115 @@ + 'FakeDB', +)); diff --git a/tests/integration/opcache/disabled/test_span_events_are_created_upon_caught_error.php b/tests/integration/opcache/disabled/test_span_events_are_created_upon_caught_error.php new file mode 100644 index 000000000..d61b7fea0 --- /dev/null +++ b/tests/integration/opcache/disabled/test_span_events_are_created_upon_caught_error.php @@ -0,0 +1,192 @@ + 'FakeDB', + ) +); +a(); + +echo 'this should never be printed'; diff --git a/tests/integration/opcache/disabled/test_span_events_are_created_upon_exit.php b/tests/integration/opcache/disabled/test_span_events_are_created_upon_exit.php new file mode 100644 index 000000000..633ce880d --- /dev/null +++ b/tests/integration/opcache/disabled/test_span_events_are_created_upon_exit.php @@ -0,0 +1,120 @@ + 'FakeDB', + ) +); +a(); diff --git a/tests/integration/opcache/disabled/test_span_events_are_created_upon_uncaught_error.php b/tests/integration/opcache/disabled/test_span_events_are_created_upon_uncaught_error.php new file mode 100644 index 000000000..bc42257cc --- /dev/null +++ b/tests/integration/opcache/disabled/test_span_events_are_created_upon_uncaught_error.php @@ -0,0 +1,130 @@ + 'FakeDB', + ) +); +a(); + +echo 'this should never be printed'; diff --git a/tests/integration/opcache/disabled/test_span_events_are_created_upon_uncaught_handled_exception.php b/tests/integration/opcache/disabled/test_span_events_are_created_upon_uncaught_handled_exception.php new file mode 100644 index 000000000..d45a094b8 --- /dev/null +++ b/tests/integration/opcache/disabled/test_span_events_are_created_upon_uncaught_handled_exception.php @@ -0,0 +1,196 @@ + 'FakeDB', + ) +); +a(); + +echo 'this should never be printed'; diff --git a/tests/integration/opcache/disabled/test_span_events_are_created_upon_uncaught_handled_exception.php7.php b/tests/integration/opcache/disabled/test_span_events_are_created_upon_uncaught_handled_exception.php7.php new file mode 100644 index 000000000..67bc1b428 --- /dev/null +++ b/tests/integration/opcache/disabled/test_span_events_are_created_upon_uncaught_handled_exception.php7.php @@ -0,0 +1,167 @@ +=")) { + die("skip: PHP > 8.0 not supported\n"); +} + +require('skipif.inc'); + +*/ + +/*INI +newrelic.distributed_tracing_enabled=1 +newrelic.transaction_tracer.threshold = 0 +newrelic.span_events_enabled=1 +newrelic.cross_application_tracer.enabled = false +display_errors=1 +log_errors=0 +error_reporting = E_ALL +opcache.enable=0 +opcache.enable_cli=0 +opcache.file_update_protection=0 +opcache.jit_buffer_size=32M +opcache.jit=function +*/ + +/*EXPECT_ERROR_EVENTS +null +*/ + + +/*EXPECT_SPAN_EVENTS +[ + "?? agent run id", + { + "reservoir_size": 10000, + "events_seen": 4 + }, + [ + [ + { + "traceId": "??", + "duration": "??", + "transactionId": "??", + "name": "OtherTransaction\/php__FILE__", + "guid": "??", + "type": "Span", + "category": "generic", + "priority": "??", + "sampled": true, + "nr.entryPoint": true, + "timestamp": "??", + "transaction.name": "OtherTransaction\/php__FILE__" + }, + {}, + {} + ], + [ + { + "type": "Span", + "traceId": "??", + "transactionId": "??", + "sampled": true, + "priority": "??", + "name": "Datastore\/statement\/FakeDB\/other\/other", + "guid": "??", + "timestamp": "??", + "duration": "??", + "category": "datastore", + "parentId": "??", + "span.kind": "client", + "component": "FakeDB" + }, + {}, + { + "db.instance": "unknown", + "peer.hostname": "unknown", + "peer.address": "unknown:unknown" + } + ], + [ + { + "type": "Span", + "traceId": "??", + "transactionId": "??", + "sampled": true, + "priority": "??", + "name": "Custom\/a", + "guid": "??", + "timestamp": "??", + "duration": "??", + "category": "generic", + "parentId": "??" + }, + {}, + { + "error.message": "Uncaught exception 'RuntimeException' with message 'oops' in __FILE__:??", + "error.class": "RuntimeException", + "code.lineno": "??", + "code.filepath": "__FILE__", + "code.function": "a" + } + ], + [ + { + "type": "Span", + "traceId": "??", + "transactionId": "??", + "sampled": true, + "priority": "??", + "name": "Custom\/{closure}", + "guid": "??", + "timestamp": "??", + "duration": "??", + "category": "generic", + "parentId": "??" + }, + {}, + { + "code.lineno": "??", + "code.filepath": "__FILE__", + "code.function": "{closure}" + } + ] + ] +] +*/ + +/*EXPECT +*/ +require('opcache_test.inc'); + +set_exception_handler( + function (Throwable $exception) { + time_nanosleep(0, 100000000); + exit(0); + } +); + +function a() +{ + time_nanosleep(0, 100000000); + throw new RuntimeException('oops'); +} + +newrelic_record_datastore_segment( + function () { + time_nanosleep(0, 100000000); + }, array( + 'product' => 'FakeDB', + ) +); +a(); + +echo 'this should never be printed'; diff --git a/tests/integration/opcache/disabled/test_span_events_are_created_upon_uncaught_unhandled_exception.php b/tests/integration/opcache/disabled/test_span_events_are_created_upon_uncaught_unhandled_exception.php new file mode 100644 index 000000000..94b69f7a8 --- /dev/null +++ b/tests/integration/opcache/disabled/test_span_events_are_created_upon_uncaught_unhandled_exception.php @@ -0,0 +1,167 @@ + 'FakeDB', + ) +); +a(); + +echo 'this should never be printed'; diff --git a/tests/integration/opcache/disabled/test_span_events_error_collector_disabled.php b/tests/integration/opcache/disabled/test_span_events_error_collector_disabled.php new file mode 100644 index 000000000..b8d5584dc --- /dev/null +++ b/tests/integration/opcache/disabled/test_span_events_error_collector_disabled.php @@ -0,0 +1,128 @@ + 'FakeDB', + ) +); +a(); + +echo 'this should never be printed'; diff --git a/tests/integration/opcache/disabled/test_span_events_exception_caught_nested.php b/tests/integration/opcache/disabled/test_span_events_exception_caught_nested.php new file mode 100644 index 000000000..41d3db2b6 --- /dev/null +++ b/tests/integration/opcache/disabled/test_span_events_exception_caught_nested.php @@ -0,0 +1,214 @@ +getMessage() . "\n"); + } +} + +function fraction($x) { + time_nanosleep(0, 100000000); + if (!$x) { + throw new RuntimeException('Division by zero'); + } + return 1/$x; +} + +function a() +{ + time_nanosleep(0, 100000000); + echo "Hello\n"; +}; + +a(); +b(); diff --git a/tests/integration/opcache/disabled/test_span_events_exception_caught_nested_rethrown.php b/tests/integration/opcache/disabled/test_span_events_exception_caught_nested_rethrown.php new file mode 100644 index 000000000..5b5729498 --- /dev/null +++ b/tests/integration/opcache/disabled/test_span_events_exception_caught_nested_rethrown.php @@ -0,0 +1,193 @@ +getMessage()); + } +} + +function fraction($x) { + time_nanosleep(0, 100000000); + if (!$x) { + throw new RuntimeException('Division by zero'); + } + return 1/$x; +} + +b(); diff --git a/tests/integration/opcache/disabled/test_span_events_exception_caught_notice_error.php b/tests/integration/opcache/disabled/test_span_events_exception_caught_notice_error.php new file mode 100644 index 000000000..d51e9e210 --- /dev/null +++ b/tests/integration/opcache/disabled/test_span_events_exception_caught_notice_error.php @@ -0,0 +1,166 @@ +getMessage() . "\n"); + } + newrelic_notice_error(new Exception('Notice me')); +} + +function a() +{ + time_nanosleep(0, 100000000); + echo "Hello\n"; +}; + +a(); +fraction(); diff --git a/tests/integration/opcache/disabled/test_span_events_exception_caught_notice_error_nested.php b/tests/integration/opcache/disabled/test_span_events_exception_caught_notice_error_nested.php new file mode 100644 index 000000000..7a3586e22 --- /dev/null +++ b/tests/integration/opcache/disabled/test_span_events_exception_caught_notice_error_nested.php @@ -0,0 +1,242 @@ +getMessage() . "\n"); + } + newrelic_notice_error(new Exception('Notice me')); +} + +function fraction($x) { + time_nanosleep(0, 100000000); + if (!$x) { + throw new RuntimeException('Division by zero'); + } + return 1/$x; +} + +function a() +{ + time_nanosleep(0, 100000000); + echo "Hello\n"; +}; + +a(); +b(); diff --git a/tests/integration/opcache/disabled/test_span_events_exception_caught_same_span.php b/tests/integration/opcache/disabled/test_span_events_exception_caught_same_span.php new file mode 100644 index 000000000..6de5a0aaf --- /dev/null +++ b/tests/integration/opcache/disabled/test_span_events_exception_caught_same_span.php @@ -0,0 +1,136 @@ +getMessage() . "\n"); + } +} + +function a() +{ + time_nanosleep(0, 100000000); + echo "Hello\n"; +}; + +a(); +fraction(); diff --git a/tests/integration/opcache/disabled/test_span_events_exception_uncaught_nested.php b/tests/integration/opcache/disabled/test_span_events_exception_uncaught_nested.php new file mode 100644 index 000000000..658bcec9e --- /dev/null +++ b/tests/integration/opcache/disabled/test_span_events_exception_uncaught_nested.php @@ -0,0 +1,221 @@ + 'FakeDB', + ) +); +a(); + +echo 'this should never be printed'; diff --git a/tests/integration/opcache/disabled/test_span_events_exist_when_no_segments.php b/tests/integration/opcache/disabled/test_span_events_exist_when_no_segments.php new file mode 100644 index 000000000..4a6036781 --- /dev/null +++ b/tests/integration/opcache/disabled/test_span_events_exist_when_no_segments.php @@ -0,0 +1,62 @@ + Date: Fri, 23 Feb 2024 02:01:52 -0500 Subject: [PATCH 55/56] fixup! e11b992cda2bad691bc651e28b0184ea720cdc04 fix Drupal package detection Use file that is loaded also on PHP 8.2 with include/require optimizations that don't execute files without executable statements. --- agent/php_execute.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/php_execute.c b/agent/php_execute.c index 01f8cba8b..9e93154ed 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -603,7 +603,7 @@ typedef struct _nr_vuln_mgmt_table_t { /* Note that all paths should be in lowercase. */ static const nr_vuln_mgmt_table_t vuln_mgmt_packages[] = { - {"Drupal", "core/lib/drupal.php", nr_drupal_version}, + {"Drupal", "drupal/component/dependencyinjection/container.php", nr_drupal_version}, {"Wordpress", "wp-includes/version.php", nr_wordpress_version}, }; From 4086b6b713cd2dfe1e369dfe8e7b960d47f10d1e Mon Sep 17 00:00:00 2001 From: Michal Nowacki Date: Tue, 27 Feb 2024 15:49:12 -0500 Subject: [PATCH 56/56] fix(agent): fix Lumen detection (#838) Remove 'bootstrap/app.php' from the list of signature files used to detect Laravel - Laravel is detected with 'illuminate/foundation/application.php' for all supported Laravel versions: 6, 7, 8, 9 and 10. Using 'bootstrap/app.php' causes Laravel to be detected before Lumen can be detected because 'bootstrap/app.php' is the second file loaded when Lumen based app is handling the request and this results in Lumen app being detected as Laravel. --- agent/php_execute.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/agent/php_execute.c b/agent/php_execute.c index 9e93154ed..2908671c8 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -381,8 +381,6 @@ static const nr_framework_table_t all_frameworks[] = { NR_FW_LARAVEL}, /* 5.0.15-5.0.x */ {"Laravel", "laravel", NR_PSTR("bootstrap/cache/compiled.php"), 0, nr_laravel_enable, NR_FW_LARAVEL}, /* 5.1.0-x */ - {"Laravel", "laravel", NR_PSTR("bootstrap/app.php"), 0, nr_laravel_enable, - NR_FW_LARAVEL}, /* 8+ */ {"Lumen", "lumen", NR_PSTR("lumen-framework/src/helpers.php"), 0, nr_lumen_enable, NR_FW_LUMEN}, @@ -857,6 +855,9 @@ static nrframework_t nr_try_detect_framework( } nr_framework_log("detected framework", frameworks[i].framework_name); + nrl_verbosedebug( + NRL_FRAMEWORK, "framework '%s' detected with %s, which ends with %s", + frameworks[i].framework_name, filename, frameworks[i].file_to_check); frameworks[i].enable(TSRMLS_C); detected = frameworks[i].detected;