From 539dd4f2163f5d26f475c58b1f7fc27e7a85c892 Mon Sep 17 00:00:00 2001 From: Michal Nowacki Date: Fri, 19 Apr 2024 12:41:08 -0400 Subject: [PATCH] feat(agent): add support for Yii v2 (#848) Implement Yii v2 instrumentation for auto transaction naming. Original work by @razvanphp in #823. Fixes #821. --------- Co-authored-by: Razvan Grigore --- agent/fw_hooks.h | 3 +- agent/fw_yii.c | 140 ++++++++++++++++-- agent/php_execute.c | 6 +- agent/php_newrelic.h | 3 +- agent/scripts/newrelic.ini.template | 2 +- .../frameworks/yii/test_basic_cli.php | 22 +++ .../frameworks/yii/test_basic_web.php | 22 +++ .../frameworks/yii/yii2/baseyii.php | 46 ++++++ 8 files changed, 227 insertions(+), 17 deletions(-) create mode 100644 tests/integration/frameworks/yii/test_basic_cli.php create mode 100644 tests/integration/frameworks/yii/test_basic_web.php create mode 100644 tests/integration/frameworks/yii/yii2/baseyii.php diff --git a/agent/fw_hooks.h b/agent/fw_hooks.h index f49bc591b..baaf400c7 100644 --- a/agent/fw_hooks.h +++ b/agent/fw_hooks.h @@ -39,7 +39,8 @@ extern void nr_symfony4_enable(TSRMLS_D); extern void nr_silex_enable(TSRMLS_D); extern void nr_slim_enable(TSRMLS_D); extern void nr_wordpress_enable(TSRMLS_D); -extern void nr_yii_enable(TSRMLS_D); +extern void nr_yii1_enable(TSRMLS_D); +extern void nr_yii2_enable(TSRMLS_D); extern void nr_zend_enable(TSRMLS_D); extern void nr_fw_zend2_enable(TSRMLS_D); diff --git a/agent/fw_yii.c b/agent/fw_yii.c index c6ff7cef8..74aa19335 100644 --- a/agent/fw_yii.c +++ b/agent/fw_yii.c @@ -5,6 +5,7 @@ #include "php_agent.h" #include "php_call.h" #include "php_user_instrument.h" +#include "php_error.h" #include "php_execute.h" #include "php_wrapper.h" #include "fw_hooks.h" @@ -14,7 +15,7 @@ #include "util_strings.h" /* - * Set the web transaction name from the action. + * Yii1: Set the web transaction name from the controllerId + actionId combo. * * * txn naming scheme: * In this case, `nr_txn_set_path` is called before `NR_PHP_WRAPPER_CALL` with @@ -23,8 +24,7 @@ * ensure OAPI compatibility. This entails that the first wrapped call gets to * name the txn. */ - -NR_PHP_WRAPPER(nr_yii_runWithParams_wrapper) { +NR_PHP_WRAPPER(nr_yii1_runWithParams_wrapper) { zval* classz = NULL; zval* idz = NULL; zval* this_var = NULL; @@ -37,7 +37,7 @@ NR_PHP_WRAPPER(nr_yii_runWithParams_wrapper) { (void)wraprec; NR_UNUSED_SPECIALFN; - NR_PHP_WRAPPER_REQUIRE_FRAMEWORK(NR_FW_YII); + NR_PHP_WRAPPER_REQUIRE_FRAMEWORK(NR_FW_YII1); this_var = nr_php_scope_get(NR_EXECUTE_ORIG_ARGS TSRMLS_CC); if (NULL == this_var) { @@ -76,8 +76,9 @@ NR_PHP_WRAPPER(nr_yii_runWithParams_wrapper) { NR_NOT_OK_TO_OVERWRITE); } } + nr_php_zval_free(&idz); } - + nr_php_zval_free(&classz); end: NR_PHP_WRAPPER_CALL; @@ -86,21 +87,138 @@ NR_PHP_WRAPPER(nr_yii_runWithParams_wrapper) { NR_PHP_WRAPPER_END /* - * Enable Yii instrumentation. + * Enable Yii1 instrumentation. */ -void nr_yii_enable(TSRMLS_D) { +void nr_yii1_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, + NR_PSTR("CAction::runWithParams"), nr_yii1_runWithParams_wrapper, NULL, NULL); nr_php_wrap_user_function_before_after_clean( - NR_PSTR("CInlineAction::runWithParams"), nr_yii_runWithParams_wrapper, + NR_PSTR("CInlineAction::runWithParams"), nr_yii1_runWithParams_wrapper, NULL, NULL); #else nr_php_wrap_user_function(NR_PSTR("CAction::runWithParams"), - nr_yii_runWithParams_wrapper TSRMLS_CC); + nr_yii1_runWithParams_wrapper TSRMLS_CC); nr_php_wrap_user_function(NR_PSTR("CInlineAction::runWithParams"), - nr_yii_runWithParams_wrapper TSRMLS_CC); + nr_yii1_runWithParams_wrapper TSRMLS_CC); +#endif +} + +/* + * Yii2: Set the web transaction name from the unique action ID. + */ +NR_PHP_WRAPPER(nr_yii2_runWithParams_wrapper) { + zval* this_var = NULL; + zval* unique_idz = NULL; + const char* unique_id = NULL; + nr_string_len_t unique_id_length; + char* transaction_name = NULL; + + (void)wraprec; + NR_UNUSED_SPECIALFN; + + NR_PHP_WRAPPER_REQUIRE_FRAMEWORK(NR_FW_YII2); + + this_var = nr_php_scope_get(NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + if (NULL == this_var) { + nrl_verbosedebug(NRL_FRAMEWORK, "Yii2: improper this"); + goto end; + } + + unique_idz = nr_php_call(this_var, "getUniqueId"); + if (nr_php_is_zval_non_empty_string(unique_idz)) { + unique_id = Z_STRVAL_P(unique_idz); + unique_id_length = Z_STRLEN_P(unique_idz); + + if (unique_id_length > 256) { + nrl_warning(NRL_FRAMEWORK, + "Yii2 unique ID is too long (> %d); Yii2 naming not used", + 256); + } else { + transaction_name = (char*)nr_alloca(unique_id_length + 1); + nr_strxcpy(transaction_name, unique_id, unique_id_length); + + nr_txn_set_path("Yii2", NRPRG(txn), transaction_name, NR_PATH_TYPE_ACTION, + NR_NOT_OK_TO_OVERWRITE); + } + } else { + nrl_verbosedebug(NRL_FRAMEWORK, + "getUniqueId does not return a non empty string (%d)", + Z_TYPE_P(unique_idz)); + } + nr_php_zval_free(&unique_idz); +end: + NR_PHP_WRAPPER_CALL; + + nr_php_scope_release(&this_var); +} +NR_PHP_WRAPPER_END + +#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ + || defined OVERWRITE_ZEND_EXECUTE_DATA +/* + * Yii2: Report errors and exceptions when built-in ErrorHandler is enabled. + */ +NR_PHP_WRAPPER(nr_yii2_error_handler_wrapper) { + zval* exception = NULL; + + NR_UNUSED_SPECIALFN; + (void)wraprec; + + NR_PHP_WRAPPER_REQUIRE_FRAMEWORK(NR_FW_YII2); + + exception = nr_php_arg_get(1, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + if (!nr_php_is_zval_valid_object(exception)) { + nrl_verbosedebug(NRL_FRAMEWORK, "%s: exception is NULL or not an object", + __func__); + goto end; + } + + if (NR_SUCCESS + != nr_php_error_record_exception( + NRPRG(txn), exception, nr_php_error_get_priority(E_ERROR), true, + "Uncaught exception ", &NRPRG(exception_filters) TSRMLS_CC)) { + nrl_verbosedebug(NRL_FRAMEWORK, "%s: unable to record exception", __func__); + } + +end: + nr_php_arg_release(&exception); +} +NR_PHP_WRAPPER_END +#endif + +/* + * Enable Yii2 instrumentation. + */ +void nr_yii2_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("yii\\base\\Action::runWithParams"), + nr_yii2_runWithParams_wrapper, NULL, NULL); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("yii\\base\\InlineAction::runWithParams"), + nr_yii2_runWithParams_wrapper, NULL, NULL); +#else + nr_php_wrap_user_function(NR_PSTR("yii\\base\\Action::runWithParams"), + nr_yii2_runWithParams_wrapper TSRMLS_CC); + nr_php_wrap_user_function(NR_PSTR("yii\\base\\InlineAction::runWithParams"), + nr_yii2_runWithParams_wrapper TSRMLS_CC); + /* + * Wrap Yii2 global error and exception handling methods. + * Given that: ErrorHandler::handleException(), ::handleError() and + * ::handleFatalError() all call ::logException($exception) at the right time, + * we will wrap this one to cover all cases. + * @see + * https://github.com/yiisoft/yii2/blob/master/framework/base/ErrorHandler.php + * + * Note: one can also set YII_ENABLE_ERROR_HANDLER constant to FALSE, this way + * allowing default PHP error handler to be intercepted by the NewRelic agent + * implementation. + */ + nr_php_wrap_user_function(NR_PSTR("yii\\base\\ErrorHandler::logException"), + nr_yii2_error_handler_wrapper TSRMLS_CC); #endif } diff --git a/agent/php_execute.c b/agent/php_execute.c index f7ea011e5..341502f6c 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -416,8 +416,9 @@ static const nr_framework_table_t all_frameworks[] = { {"WordPress", "wordpress", NR_PSTR("wp-config.php"), 0, nr_wordpress_enable, NR_FW_WORDPRESS}, - {"Yii", "yii", NR_PSTR("framework/yii.php"), 0, nr_yii_enable, NR_FW_YII}, - {"Yii", "yii", NR_PSTR("framework/yiilite.php"), 0, nr_yii_enable, NR_FW_YII}, + {"Yii", "yii", NR_PSTR("framework/yii.php"), 0, nr_yii1_enable, NR_FW_YII1}, + {"Yii", "yii", NR_PSTR("framework/yiilite.php"), 0, nr_yii1_enable, NR_FW_YII1}, + {"Yii2", "yii2", NR_PSTR("yii2/baseyii.php"), 0, nr_yii2_enable, NR_FW_YII2}, /* See above: Laminas, the successor to Zend, which shares much of the instrumentation implementation with Zend */ @@ -535,7 +536,6 @@ static nr_library_table_t libraries[] = { {"SilverStripe4", NR_PSTR("silverstripeserviceconfigurationlocator.php"), NULL}, {"Typo3", NR_PSTR("classes/typo3/flow/core/bootstrap.php"), NULL}, {"Typo3", NR_PSTR("typo3/sysext/core/classes/core/bootstrap.php"), NULL}, - {"Yii2", NR_PSTR("yii2/baseyii.php"), NULL}, /* * Other CMS (content management systems), detected only, but diff --git a/agent/php_newrelic.h b/agent/php_newrelic.h index 4b55ae3b3..e114a6464 100644 --- a/agent/php_newrelic.h +++ b/agent/php_newrelic.h @@ -167,7 +167,8 @@ typedef enum { NR_FW_SYMFONY2, NR_FW_SYMFONY4, NR_FW_WORDPRESS, - NR_FW_YII, + NR_FW_YII1, + NR_FW_YII2, NR_FW_ZEND, NR_FW_ZEND2, NR_FW_LAMINAS3, diff --git a/agent/scripts/newrelic.ini.template b/agent/scripts/newrelic.ini.template index d720e83c0..eb0d24b00 100644 --- a/agent/scripts/newrelic.ini.template +++ b/agent/scripts/newrelic.ini.template @@ -639,7 +639,7 @@ newrelic.daemon.logfile = "/var/log/newrelic/newrelic-daemon.log" ; Must be one of the following values: ; cakephp, codeigniter, drupal, drupal8, joomla, kohana, laravel, ; magento, magento2, mediawiki, slim, symfony2, symfony4, -; wordpress, yii, zend, zend2, no_framework +; wordpress, yii, yii2, zend, zend2, no_framework ; ; Note that "drupal" covers only Drupal 6 and 7 and "symfony2" ; now only supports Symfony 3.x. diff --git a/tests/integration/frameworks/yii/test_basic_cli.php b/tests/integration/frameworks/yii/test_basic_cli.php new file mode 100644 index 000000000..abae3bb06 --- /dev/null +++ b/tests/integration/frameworks/yii/test_basic_cli.php @@ -0,0 +1,22 @@ +runWithParams([]); diff --git a/tests/integration/frameworks/yii/test_basic_web.php b/tests/integration/frameworks/yii/test_basic_web.php new file mode 100644 index 000000000..b03af867c --- /dev/null +++ b/tests/integration/frameworks/yii/test_basic_web.php @@ -0,0 +1,22 @@ +runWithParams([]); diff --git a/tests/integration/frameworks/yii/yii2/baseyii.php b/tests/integration/frameworks/yii/yii2/baseyii.php new file mode 100644 index 000000000..f884c6853 --- /dev/null +++ b/tests/integration/frameworks/yii/yii2/baseyii.php @@ -0,0 +1,46 @@ +uniqid = $argument; + } + + public function getUniqueId ( ) { + return $this->uniqid; + } + + public function runWithParams($params) { + return; + } + } + + /* Console action */ + class InlineAction { + private $uniqid; + + public function __construct($id) { + $this->uniqid = $id; + } + + public function getUniqueId ( ) { + return $this->uniqid; + } + + public function runWithParams($params) { + return; + } + } + echo ""; +}