diff --git a/.github/workflows/code-coverage-baseline.yml b/.github/workflows/code-coverage-baseline.yml index d5a700c28..fa4052e35 100644 --- a/.github/workflows/code-coverage-baseline.yml +++ b/.github/workflows/code-coverage-baseline.yml @@ -32,13 +32,13 @@ jobs: arch: [amd64] steps: - name: Checkout newrelic-php-agent code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: php-agent repository: ${{ inputs.origin }}/newrelic-php-agent ref: ${{ inputs.ref }} - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -77,13 +77,13 @@ jobs: codecov: 1 steps: - name: Checkout Repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: php-agent repository: ${{ inputs.origin }}/newrelic-php-agent ref: ${{ inputs.ref }} - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -155,7 +155,7 @@ jobs: codecov: 1 steps: - name: Checkout integration tests - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: php-agent repository: ${{ inputs.origin }}/newrelic-php-agent @@ -187,7 +187,7 @@ jobs: chmod 755 php-agent/bin/integration_runner chmod 755 php-agent/agent/modules/newrelic.so - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -250,7 +250,7 @@ jobs: echo "COMMIT_HASH=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT - name: Upload coverage reports to Codecov if: ${{ matrix.codecov == 1 }} - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v3.1.5 with: token: ${{ secrets.CODECOV_TOKEN }} working-directory: ./php-agent diff --git a/.github/workflows/make-agent.yml b/.github/workflows/make-agent.yml index 9d2d76dd5..87a473159 100644 --- a/.github/workflows/make-agent.yml +++ b/.github/workflows/make-agent.yml @@ -35,19 +35,19 @@ jobs: php: ['8.0', '8.1', '8.2', '8.3'] steps: - name: Checkout Repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: php-agent repository: ${{ inputs.origin }}/newrelic-php-agent ref: ${{ inputs.ref }} - name: Enable arm64 emulation if: ${{ inputs.arch == 'arm64' }} - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 with: image: tonistiigi/binfmt:${{vars.BINFMT_IMAGE_VERSION}} platforms: arm64 - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/make-for-platform-on-arch.yml b/.github/workflows/make-for-platform-on-arch.yml index 5f3933496..053d833ff 100644 --- a/.github/workflows/make-for-platform-on-arch.yml +++ b/.github/workflows/make-for-platform-on-arch.yml @@ -54,19 +54,19 @@ jobs: platform: [gnu, musl] steps: - name: Checkout newrelic-php-agent code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: php-agent repository: ${{ inputs.origin }}/newrelic-php-agent ref: ${{ inputs.ref }} - name: Enable arm64 emulation if: ${{ inputs.arch == 'arm64' }} - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 with: image: tonistiigi/binfmt:${{vars.BINFMT_IMAGE_VERSION}} platforms: arm64 - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/make-integration-tests.yml b/.github/workflows/make-integration-tests.yml index ab90a136b..9e08e0b5e 100644 --- a/.github/workflows/make-integration-tests.yml +++ b/.github/workflows/make-integration-tests.yml @@ -37,7 +37,7 @@ jobs: php: ['8.0', '8.1', '8.2', '8.3'] steps: - name: Checkout integration tests - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: php-agent repository: ${{ inputs.origin }}/newrelic-php-agent @@ -58,12 +58,12 @@ jobs: chmod 755 php-agent/agent/modules/newrelic.so - name: Enable arm64 emulation if: ${{ inputs.arch == 'arm64' }} - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 with: image: tonistiigi/binfmt:${{vars.BINFMT_IMAGE_VERSION}} platforms: arm64 - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 77f26bd92..b90fc891a 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -35,12 +35,12 @@ jobs: arch: [amd64] steps: - name: Checkout newrelic-php-agent code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.event.client_payload.ref }} path: newrelic-php-agent - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -69,12 +69,12 @@ jobs: arch: [amd64] steps: - name: Checkout Repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.event.client_payload.ref }} path: newrelic-php-agent - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/repolinter.yml b/.github/workflows/repolinter.yml index e1e3c1cc1..378cb4428 100644 --- a/.github/workflows/repolinter.yml +++ b/.github/workflows/repolinter.yml @@ -18,14 +18,14 @@ jobs: steps: - name: Test Default Branch id: default-branch - uses: actions/github-script@v2 + uses: actions/github-script@v7 with: script: | - const data = await github.repos.get(context.repo) + const data = await github.rest.repos.get(context.repo) return data.data && data.data.default_branch === context.ref.split('/').slice(-1)[0] - name: Checkout Self if: ${{ steps.default-branch.outputs.result == 'true' }} - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Run Repolinter if: ${{ steps.default-branch.outputs.result == 'true' }} uses: newrelic/repolinter-action@v1 diff --git a/.github/workflows/test-agent.yml b/.github/workflows/test-agent.yml index 2736d2cc1..0365b4ddb 100644 --- a/.github/workflows/test-agent.yml +++ b/.github/workflows/test-agent.yml @@ -31,17 +31,17 @@ jobs: arch: [amd64, arm64] steps: - name: Checkout newrelic-php-agent code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: php-agent - name: Enable arm64 emulation if: ${{ matrix.arch == 'arm64' }} - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 with: image: tonistiigi/binfmt:${{vars.BINFMT_IMAGE_VERSION}} platforms: arm64 - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -91,17 +91,17 @@ jobs: codecov: 1 steps: - name: Checkout Repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: php-agent - name: Enable arm64 emulation if: ${{ matrix.arch == 'arm64' }} - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 with: image: tonistiigi/binfmt:${{vars.BINFMT_IMAGE_VERSION}} platforms: arm64 - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -202,7 +202,7 @@ jobs: codecov: 1 steps: - name: Checkout integration tests - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: php-agent - name: Get integration_runner @@ -233,12 +233,12 @@ jobs: chmod 755 php-agent/agent/modules/newrelic.so - name: Enable arm64 emulation if: ${{ matrix.arch == 'arm64' }} - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 with: image: tonistiigi/binfmt:${{vars.BINFMT_IMAGE_VERSION}} platforms: arm64 - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -296,7 +296,7 @@ jobs: nr-php make gcov - name: Upload coverage reports to Codecov if: ${{ matrix.codecov == 1 }} - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v3.1.5 with: token: ${{ secrets.CODECOV_TOKEN }} working-directory: ./php-agent diff --git a/VERSION b/VERSION index 73bffb039..06c9b9d30 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -10.17.0 +10.18.0 diff --git a/agent/config.m4 b/agent/config.m4 index d85e50636..8d9e42b24 100644 --- a/agent/config.m4 +++ b/agent/config.m4 @@ -216,7 +216,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/fw_cakephp.c b/agent/fw_cakephp.c index 84a01deb1..1dfe88231 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 5305ab480..67efc1dab 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; @@ -67,6 +80,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 +226,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 +240,130 @@ 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)) { + /* + * 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 - 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 + +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 + +/* + * 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 @@ -216,7 +391,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 @@ -230,30 +405,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 +440,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; @@ -325,25 +478,39 @@ 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_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_invoke_all_hook); + size_t hook_len = NRPRG(drupal_invoke_all_hook_len); +#endif + if (NULL == hook_name) { nrl_verbosedebug(NRL_FRAMEWORK, "%s: cannot extract module name without knowing the hook", __func__); return; } - 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); + 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, NRPRG(drupal_module_invoke_all_hook), - NRPRG(drupal_module_invoke_all_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); } @@ -588,6 +755,34 @@ 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); + nr_drupal_invoke_all_hook_stacks_push(hook_copy); +} +NR_PHP_WRAPPER_END + +NR_PHP_WRAPPER(nr_drupal_wrap_module_invoke_all_after) { + (void)wraprec; + nr_drupal_invoke_all_hook_stacks_pop(); +} +NR_PHP_WRAPPER_END + +NR_PHP_WRAPPER(nr_drupal_wrap_module_invoke_all_clean) { + NR_UNUSED_SPECIALFN; + NR_UNUSED_FUNC_RETURN_VALUE; + (void)wraprec; + nr_drupal_invoke_all_hook_stacks_pop(); +} +NR_PHP_WRAPPER_END + +#else NR_PHP_WRAPPER(nr_drupal_wrap_module_invoke_all) { zval* hook = NULL; char* prev_hook = NULL; @@ -603,19 +798,19 @@ 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); NRPRG(check_cufa) = true; 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; - if (NULL == NRPRG(drupal_module_invoke_all_hook)) { + nr_free(NRPRG(drupal_invoke_all_hook)); + NRPRG(drupal_invoke_all_hook) = prev_hook; + NRPRG(drupal_invoke_all_hook_len) = prev_hook_len; + if (NULL == NRPRG(drupal_invoke_all_hook)) { NRPRG(check_cufa) = false; } @@ -623,6 +818,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. @@ -630,14 +826,26 @@ 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("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_cron_run"), - nr_drupal_cron_run TSRMLS_CC); 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, @@ -646,8 +854,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_drupal8.c b/agent/fw_drupal8.c index 5456cf390..fbd04f315 100644 --- a/agent/fw_drupal8.c +++ b/agent/fw_drupal8.c @@ -45,9 +45,10 @@ static void nr_drupal8_add_method_callback(const zend_class_entry* ce, 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); + "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; } @@ -71,6 +72,47 @@ 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( + class_method, nr_strlen(class_method), + before_callback, after_callback, clean_callback); + + nr_free(class_method); + } +} +#endif // OAPI + /* * Purpose : Check if the given function or method is in the current call * stack. @@ -155,9 +197,17 @@ 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_php_get_return_value_ptr(TSRMLS_C); + zval** retval_ptr = NR_GET_RETURN_VALUE_PTR; NR_UNUSED_SPECIALFN; (void)wraprec; @@ -204,9 +254,17 @@ 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_php_get_return_value_ptr(TSRMLS_C); + zval** retval_ptr = NR_GET_RETURN_VALUE_PTR; (void)wraprec; @@ -305,7 +363,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; @@ -339,7 +397,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; @@ -377,15 +435,28 @@ NR_PHP_WRAPPER(nr_drupal94_invoke_all_with_callback) { (void)wraprec; NR_PHP_WRAPPER_REQUIRE_FRAMEWORK(NR_FW_DRUPAL8); - module = nr_php_arg_get(2, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); if (!nr_php_is_zval_non_empty_string(module)) { - goto leave; + 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; @@ -394,16 +465,19 @@ NR_PHP_WRAPPER(nr_drupal94_invoke_all_with_callback) { NR_PHP_WRAPPER_END /* - * Purpose : Handles ModuleHandlerInterface::invokeAllWith() call and ensure that the - * relevant hook function is instrumented. At this point in the call + * Purpose : Handles ModuleHandlerInterface::invokeAllWith() call and ensure + * that the relevant hook function is instrumented. At this point in the call * stack, we do not know which module to instrument, so we * must first wrap the callback passed into this function */ 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; @@ -411,43 +485,75 @@ 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)) { - goto leave; +#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); NRPRG(check_cufa) = true; +#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 */ - nr_php_wrap_generic_callable(callback, nr_drupal94_invoke_all_with_callback TSRMLS_CC); - + nr_php_wrap_generic_callable(callback, + nr_drupal94_invoke_all_with_callback TSRMLS_CC); 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; - if (NULL == NRPRG(drupal_module_invoke_all_hook)) { +#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; + if (NULL == NRPRG(drupal_invoke_all_hook)) { NRPRG(check_cufa) = false; } +#endif // not OAPI -leave: +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 // OAPI + /* * Purpose : Wrap the invoke() method of the module handler instance in use. */ 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; @@ -474,11 +580,28 @@ 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 */ +#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 } 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; @@ -580,9 +703,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_drupal_common.c b/agent/fw_drupal_common.c index 27d55ea42..894ff2270 100644 --- a/agent/fw_drupal_common.c +++ b/agent/fw_drupal_common.c @@ -294,3 +294,26 @@ 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); + NRPRG(check_cufa) = true; + } 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); + } + if (nr_stack_is_empty(&NRPRG(drupal_invoke_all_hooks))) { + NRPRG(check_cufa) = false; + } +} +#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/fw_laminas3.c b/agent/fw_laminas3.c index 1bb61dabb..604976bf0 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 c2b55a96e..8d35715d2 100644 --- a/agent/fw_laravel.c +++ b/agent/fw_laravel.c @@ -485,6 +485,7 @@ static void nr_laravel_name_transaction(zval* router, zval* request 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 @@ -574,6 +575,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); @@ -598,6 +600,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; @@ -624,6 +629,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", @@ -654,7 +660,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; @@ -684,6 +692,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; @@ -753,8 +768,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); } } @@ -775,6 +796,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, @@ -803,9 +829,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); } @@ -861,6 +892,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; @@ -977,6 +1015,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; @@ -1024,6 +1070,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; @@ -1038,7 +1092,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. */ @@ -1170,9 +1224,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_laravel_queue.c b/agent/fw_laravel_queue.c index d038936a6..b4d5576e7 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; @@ -587,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); } @@ -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 @@ -695,7 +761,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; @@ -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/fw_lumen.c b/agent/fw_lumen.c index a4e4bc75c..239578ae1 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; @@ -181,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); @@ -204,10 +220,16 @@ 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 if (NRINI(vulnerability_management_package_detection_enabled)) { nr_txn_add_php_package(NRPRG(txn), "laravel/lumen-framework", diff --git a/agent/fw_magento2.c b/agent/fw_magento2.c index 9b944938f..f96994f25 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; @@ -129,7 +145,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; @@ -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; @@ -167,7 +191,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)) { @@ -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; @@ -251,7 +291,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)) { @@ -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; @@ -268,7 +316,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)) { @@ -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; @@ -322,7 +385,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: @@ -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 fd31aa88b..22aa927e6 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; @@ -142,7 +166,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; @@ -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 3da3854dd..e810dd7f4 100644 --- a/agent/fw_slim.c +++ b/agent/fw_slim.c @@ -42,6 +42,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; @@ -55,7 +61,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; @@ -76,6 +82,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 e7e0413d3..6e1c9982d 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, true, 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,9 +265,16 @@ 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 if (NRINI(vulnerability_management_package_detection_enabled)) { nr_txn_add_php_package(NRPRG(txn), "symfony/http-kernel", diff --git a/agent/fw_wordpress.c b/agent/fw_wordpress.c index 32ce1fdc1..874ab6cc8 100644 --- a/agent/fw_wordpress.c +++ b/agent/fw_wordpress.c @@ -245,7 +245,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_php_function_filename_len(func); @@ -290,13 +296,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); @@ -326,7 +344,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; } #if ZEND_MODULE_API_NO < ZEND_7_4_X_API_NO @@ -338,14 +362,21 @@ NR_PHP_WRAPPER(nr_wordpress_wrap_hook) { NR_PHP_WRAPPER_CALL; if (NULL != plugin || NRPRG(wordpress_core)) { +#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 nr_wordpress_create_metric(auto_segment, NR_WORDPRESS_PLUGIN_PREFIX, plugin); } } NR_PHP_WRAPPER_END +#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ + || defined OVERWRITE_ZEND_EXECUTE_DATA /* * PHP 7.3 and below uses old-style wraprec, and will use old style cufa calls. */ @@ -382,6 +413,7 @@ static void nr_wordpress_call_user_func_array(zend_function* func, nr_php_wrap_callable(func, nr_wordpress_wrap_hook TSRMLS_CC); } #endif /* PHP < 7.4 */ +#endif /* not OAPI */ static void free_tag(void* tag) { nr_free(tag); @@ -452,22 +484,38 @@ NR_PHP_WRAPPER(nr_wordpress_exec_handle_tag) { NR_UNUSED_SPECIALFN; (void)wraprec; + /* We will ignore the global wordpress_tag stack if wodpress_hooks is + * off */ if (nrlikely(0 != NRINI(wordpress_hooks))) { NR_PHP_WRAPPER_REQUIRE_FRAMEWORK(NR_FW_WORDPRESS); 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 + 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. + */ + 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); + } else { + nr_stack_push(&NRPRG(wordpress_tag_states), NULL); + } +#else 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. */ - char* old_tag = NRPRG(wordpress_tag); - NRPRG(check_cufa) = true; - - NRPRG(wordpress_tag) = nr_wordpress_clean_tag(tag); + char* old_tag = NRPRG(wordpress_tag); + NRPRG(wordpress_tag) = nr_wordpress_clean_tag(tag TSRMLS_CC); NR_PHP_WRAPPER_CALL; if (0 == NRPRG(wordpress_plugins)) { nr_wordpress_hooks_create_metric(auto_segment, NRPRG(wordpress_tag)); @@ -480,6 +528,7 @@ NR_PHP_WRAPPER(nr_wordpress_exec_handle_tag) { NRPRG(check_cufa) = false; NR_PHP_WRAPPER_CALL; } +#endif /* OAPI */ nr_php_arg_release(&tag); } @@ -535,10 +584,15 @@ 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; - zval** retval_ptr = nr_php_get_return_value_ptr(TSRMLS_C); NR_UNUSED_SPECIALFN; (void)wraprec; @@ -547,6 +601,28 @@ NR_PHP_WRAPPER(nr_wordpress_apply_filters) { 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. + */ + 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); + } else { + // Keep track of whether we pushed to NRPRG(wordpress_tags) + 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)) { /* @@ -577,11 +653,58 @@ NR_PHP_WRAPPER(nr_wordpress_apply_filters) { } 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(nr_segment_t* segment) { + if ((bool)nr_stack_pop(&NRPRG(wordpress_tag_states))) { + 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; + } +} + +NR_PHP_WRAPPER(nr_wordpress_handle_tag_stack_after) { + (void)wraprec; + if (0 != NRINI(wordpress_hooks)) { + clean_wordpress_tag_stack(auto_segment); + } +} +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(auto_segment); + } +} +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 */ + #if ZEND_MODULE_API_NO >= ZEND_7_4_X_API_NO /* * Wrap the wordpress function add_filter @@ -637,7 +760,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 @@ -687,6 +816,30 @@ void nr_wordpress_version() { } 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); + + if (0 != NRINI(wordpress_hooks)) { + 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); + if (0 != NRPRG(wordpress_plugins)) { + 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); @@ -709,6 +862,7 @@ void nr_wordpress_enable(TSRMLS_D) { #endif } } +#endif /* OAPI */ if (NRINI(vulnerability_management_package_detection_enabled)) { nr_txn_add_php_package(NRPRG(txn), "wordpress", 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/lib_doctrine2.c b/agent/lib_doctrine2.c index 8ffc141c9..e767d253f 100644 --- a/agent/lib_doctrine2.c +++ b/agent/lib_doctrine2.c @@ -49,9 +49,30 @@ 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; @@ -74,8 +95,15 @@ 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 */ if (NRINI(vulnerability_management_package_detection_enabled)) { nr_txn_add_php_package(NRPRG(txn), "doctrine/orm", diff --git a/agent/lib_phpunit.c b/agent/lib_phpunit.c index 436666dc9..e41c55b29 100644 --- a/agent/lib_phpunit.c +++ b/agent/lib_phpunit.c @@ -473,8 +473,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. @@ -491,8 +491,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; @@ -690,9 +695,6 @@ 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); if (NRINI(vulnerability_management_package_detection_enabled)) { nr_txn_add_php_package(NRPRG(txn), "phpunit/phpunit", diff --git a/agent/lib_predis.c b/agent/lib_predis.c index e0131c9f0..d4a287a03 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); @@ -596,7 +602,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; @@ -690,8 +696,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; /* @@ -704,19 +708,90 @@ 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 + +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; zval* command_obj = NULL; @@ -749,6 +824,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) { /* @@ -761,6 +837,38 @@ 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); + /* + * 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"), nr_predis_pipeline_executePipeline TSRMLS_CC); @@ -773,7 +881,6 @@ void nr_predis_enable(TSRMLS_D) { nr_php_wrap_user_function( NR_PSTR("Predis\\Pipeline\\FireAndForget::executePipeline"), nr_predis_pipeline_executePipeline TSRMLS_CC); - /* * Instrument Webdis connections, since they don't use the same * writeRequest()/readResponse() pair as the other connection types. @@ -781,4 +888,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/lib_zend_http.c b/agent/lib_zend_http.c index 16ef42560..136244c1a 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); @@ -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); @@ -456,4 +463,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 9ee9ef143..4d2fabf70 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, @@ -275,17 +300,29 @@ 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; + 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 +456,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 +480,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 a72c820ff..fbdc34620 100644 --- a/agent/php_agent.h +++ b/agent/php_agent.h @@ -39,8 +39,10 @@ #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" /* * The default connection mechanism to the daemon is: @@ -131,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 @@ -331,7 +349,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 @@ -379,6 +412,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) { @@ -405,7 +439,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 (ZEND_USER_CODE(execute_data->func->type) + || 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; @@ -737,7 +787,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) @@ -815,6 +868,9 @@ 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+ */ +#define NR_NOT_ZEND_USER_FUNC(x) \ + (x && (!x->func || !ZEND_USER_CODE(x->func->type))) + /* * Purpose : Return a uint32_t (zend_uint) line number value of zend_function. * diff --git a/agent/php_api.c b/agent/php_api.c index e1e328305..f9dc5c903 100644 --- a/agent/php_api.c +++ b/agent/php_api.c @@ -160,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"); @@ -172,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); 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_api_internal.c b/agent/php_api_internal.c index aaa2b4832..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); - 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 4a0f7c093..119271f66 100644 --- a/agent/php_call.c +++ b/agent/php_call.c @@ -53,11 +53,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 { @@ -70,17 +65,25 @@ zval* nr_php_call_user_func(zval* object_ptr, return NULL; } - zend_result = zend_call_method_if_exists(object, method_name, retval, - param_count, 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 + */ + 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 - /* + /* * 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); + retval, param_count, param_values); #else zend_result = call_user_function_ex(EG(function_table), object_ptr, fname, @@ -90,11 +93,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 */ @@ -284,7 +285,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_error.c b/agent/php_error.c index 50e047e3f..3702870a3 100644 --- a/agent/php_error.c +++ b/agent/php_error.c @@ -270,10 +270,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); - + 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 @@ -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; } @@ -547,6 +543,10 @@ 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. + */ void nr_php_error_cb(int type, zend_string* error_filename, uint error_lineno, @@ -563,6 +563,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; @@ -594,8 +599,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 @@ -617,24 +622,26 @@ 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 (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, zval* exception, int priority, + bool add_to_current_segment, const char* prefix, zend_llist* filters TSRMLS_DC) { zend_class_entry* ce; @@ -705,14 +712,16 @@ 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); nr_free(klass); nr_free(message); nr_free(stack_json); - nr_php_zval_free(&stack_trace); + Z_DELREF_P(stack_trace); return NR_SUCCESS; } @@ -727,10 +736,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_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 81d2f3d8b..2908671c8 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. @@ -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}, @@ -494,11 +492,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", NR_PSTR("phpunit/src/runner/basetestrunner.php"), nr_phpunit_enable}, - {"PHPUnit", NR_PSTR("phpunit/runner/basetestrunner.php"), nr_phpunit_enable}, + {"PHPUnit", NR_PSTR("phpunit/src/framework/test.php"), nr_phpunit_enable}, + {"PHPUnit", NR_PSTR("phpunit/framework/test.php"), nr_phpunit_enable}, {"Predis", NR_PSTR("predis/src/client.php"), nr_predis_enable}, {"Predis", NR_PSTR("predis/client.php"), nr_predis_enable}, @@ -604,7 +601,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}, }; @@ -858,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; @@ -1008,6 +1008,8 @@ static void nr_php_execute_file(const zend_op_array* op_array, const char* filename = nr_php_op_array_file_name(op_array); size_t filename_len = nr_php_op_array_file_name_len(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)); } @@ -1019,7 +1021,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; @@ -1028,44 +1031,6 @@ static void nr_php_execute_file(const zend_op_array* op_array, nr_php_add_user_instrumentation(TSRMLS_C); } -/* - * 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; - zend_string* filepath; - uint32_t function_lineno; -#else - zend_op_array* op_array; -#endif /* PHP7 */ -} nr_php_execute_metadata_t; - /* * Purpose : Initialise a metadata structure from an op array. * @@ -1085,7 +1050,6 @@ static void nr_php_execute_metadata_init(nr_php_execute_metadata_t* metadata, } else { metadata->scope = NULL; } - if (op_array->function_name && op_array->function_name->len) { metadata->function = op_array->function_name; zend_string_addref(metadata->function); @@ -1282,7 +1246,7 @@ 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. */ @@ -1330,6 +1294,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. */ @@ -1343,28 +1309,46 @@ 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); - if (create_metric || (duration >= NR_PHP_PROCESS_GLOBALS(expensive_min)) || 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); /* * Check if code level metrics are enabled in the ini. - * If they aren't, exit and don't create any metrics. + * If they aren't, exit and 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); } #endif + 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 } } @@ -1375,6 +1359,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; @@ -1436,7 +1422,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); } @@ -1532,7 +1518,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); } } @@ -1547,6 +1534,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) { /* @@ -1585,7 +1573,9 @@ 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) { +#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 * involves a setjmp, and so may be too expensive along this oft-used @@ -1611,7 +1601,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 @@ -1627,13 +1618,19 @@ void nr_php_execute(NR_EXECUTE_PROTO TSRMLS_DC) { return; } +#endif // not OAPI -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 "}", @@ -1701,7 +1698,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 @@ -1713,7 +1710,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; @@ -1796,3 +1792,409 @@ 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 \ + && !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; + if (NULL == execute_data->prev_execute_data) { + nrl_verbosedebug(NRL_AGENT, "%s: cannot get previous execute data", + __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. + */ + + /* + * 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; + } + + 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; + + /* + * 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 -= 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__); + 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; + nrtime_t txn_start_time = 0; + int zcaught = 0; + NR_UNUSED_FUNC_RETURN_VALUE; + + if (NULL == NRPRG(txn)) { + return; + } + + 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. + */ + 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, filename_len TSRMLS_CC); + return; + } + 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 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); + + segment = nr_segment_start(NRPRG(txn), NULL, NULL); + + if (nrunlikely(NULL == segment)) { + nrl_verbosedebug(NRL_AGENT, "Error starting segment."); + return; + } + + 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(); + } + + /* + * 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. + */ + 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_segment_t* segment = NULL; + nruserfn_t* wraprec = NULL; + bool create_metric = false; + nr_php_execute_metadata_t metadata = {0}; + 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. + */ + if (nrunlikely(OP_ARRAY_IS_A_FILE(NR_OP_ARRAY))) { + nr_php_execute_file(NR_OP_ARRAY, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); + return; + } + + /* + * Get the current segment and return if null. + */ + 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 + * can only exit since the segments were already closed. + */ + 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; + } + + wraprec = segment->wraprec; + + if (wraprec && wraprec->is_exception_handler) { + /* + * 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 + * 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__); + } + } + + /* + * 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. + */ + + if (NULL != wraprec) { + /* + * This is the case for specifically requested custom instrumentation. + */ + create_metric = wraprec->create_metric; + + /* + * 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_segment_discard(&segment); + return; + } + /* + * 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__); + + 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); + return; +} + +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 + */ + 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(); + } + + if (nrunlikely(0 == nr_php_recording())) { + return; + } + + 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); + + return; +} + +void nr_php_observer_fcall_end(zend_execute_data* execute_data, + zval* func_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 (nrunlikely(NULL == execute_data)) { + return; + } + + if (nrlikely(1 == nr_php_recording())) { + int show_executes_return + = 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); + } + + nr_php_instrument_func_end(NR_EXECUTE_ORIG_ARGS); + } + + NRPRG(php_cur_stack_depth) -= 1; + + return; +} + +#endif diff --git a/agent/php_execute.h b/agent/php_execute.h index dfefdc8c8..dd446a594 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) \ @@ -38,6 +41,44 @@ typedef enum { 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 + * 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; + zend_string* filepath; + uint32_t function_lineno; +#else + zend_op_array* op_array; +#endif /* PHP7 */ +} nr_php_execute_metadata_t; + extern nrframework_t nr_php_framework_from_config(const char* config_name); /* @@ -64,6 +105,7 @@ 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); #endif /* PHP_EXECUTE_HDR */ diff --git a/agent/php_hooks.h b/agent/php_hooks.h index 5f21f6f98..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 @@ -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 8952fab0b..72c25a26a 100644 --- a/agent/php_includes.h +++ b/agent/php_includes.h @@ -55,6 +55,10 @@ #define ZEND_8_2_X_API_NO 20220829 #define ZEND_8_3_X_API_NO 20230831 +#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_internal_instrument.c b/agent/php_internal_instrument.c index a314de027..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); @@ -1225,7 +1225,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 */ @@ -1621,9 +1625,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_minit.c b/agent/php_minit.c index 0c6bc2dc6..a0e7c321d 100644 --- a/agent/php_minit.c +++ b/agent/php_minit.c @@ -36,6 +36,8 @@ #include "util_syscalls.h" #include "util_threads.h" +#include "php_observer.h" + static void php_newrelic_init_globals(zend_newrelic_globals* nrg) { if (nrunlikely(NULL == nrg)) { return; @@ -660,8 +662,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( @@ -760,6 +768,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 \ + || 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; @@ -768,6 +779,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 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 bef5c06ac..4b55ae3b3 100644 --- a/agent/php_newrelic.h +++ b/agent/php_newrelic.h @@ -39,7 +39,37 @@ 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 @@ -47,9 +77,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, \ @@ -58,9 +92,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, \ @@ -69,11 +107,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 @@ -209,7 +257,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, @@ -376,21 +424,47 @@ nrframework_t current_framework; /* Current request framework (forced or detected) */ int framework_version; /* Current framework version */ -char* drupal_module_invoke_all_hook; /* The current Drupal hook */ -size_t drupal_module_invoke_all_hook_len; /* The length of the current Drupal +#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_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_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() 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 sfError404Exception::printStackTrace() frame */ +#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 */ +nr_stack_t wordpress_tags; +nr_stack_t wordpress_tag_states; /* stack of bools indicating + whether the current tag + needs to be released */ +#else bool check_cufa; /* Whether we need to check cufa because we are instrumenting hooks, or whether we can skip cufa */ - char* wordpress_tag; /* The current WordPress tag */ +#endif //OAPI + nr_matcher_t* wordpress_plugin_matcher; /* Matcher for plugin filenames */ nr_matcher_t* wordpress_theme_matcher; /* Matcher for theme filenames */ nr_matcher_t* wordpress_core_matcher; /* Matcher for plugin filenames */ @@ -406,7 +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 */ - /* * We instrument database connection constructors and store the instance * information in a hash keyed by a string containing the connection resource @@ -533,7 +606,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; nrcallbackfn_t error_group_user_callback; /* The user defined callback for diff --git a/agent/php_observer.c b/agent/php_observer.c new file mode 100644 index 000000000..e0d651fc0 --- /dev/null +++ b/agent/php_observer.c @@ -0,0 +1,107 @@ +/* + * 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 + +#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" +#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" + +/* + * 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_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 new file mode 100644 index 000000000..6a80873cb --- /dev/null +++ b/agent/php_observer.h @@ -0,0 +1,81 @@ +/* + * 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: 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. + * + * 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* func_return_value); + + +#endif /* PHP8+ */ + +#endif // NEWRELIC_PHP_AGENT_PHP_OBSERVER_H diff --git a/agent/php_rinit.c b/agent/php_rinit.c index c8045f2e1..d704f935e 100644 --- a/agent/php_rinit.c +++ b/agent/php_rinit.c @@ -24,6 +24,21 @@ 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); +} +static void zval_stack_dtor(void* e, NRUNUSED void* d) { + zval* zv = (zval*)e; + nr_php_zval_free(&zv); +} +#endif + #ifdef TAGS void zm_activate_newrelic(void); /* ctags landing pad only */ #endif @@ -39,6 +54,11 @@ PHP_RINIT_FUNCTION(newrelic) { NRPRG(error_group_user_callback).is_set = false; #if ZEND_MODULE_API_NO >= ZEND_7_4_X_API_NO nr_php_init_user_instrumentation(); +#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 #else NRPRG(pid) = getpid(); NRPRG(user_function_wrappers) = nr_vector_create(64, NULL, NULL); @@ -91,6 +111,22 @@ PHP_RINIT_FUNCTION(newrelic) { NRPRG(check_cufa) = false; + /* + * 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 + 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); + 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(drupal_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 1e68f4648..24b1311e4 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)); @@ -106,8 +113,24 @@ 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_invoke_all_hooks)); + nr_stack_destroy_fields(&NRPRG(drupal_invoke_all_states)); +#endif +#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 @@ -118,12 +141,12 @@ 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 \ + && !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/php_stacked_segment.c b/agent/php_stacked_segment.c index 8e5cc3d67..d285296ad 100644 --- a/agent/php_stacked_segment.c +++ b/agent/php_stacked_segment.c @@ -4,6 +4,11 @@ */ #include "php_stacked_segment.h" +#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 @@ -36,10 +41,9 @@ 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); @@ -48,17 +52,15 @@ void nr_php_stacked_segment_deinit(nr_segment_t* stacked TSRMLS_DC) { } 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); + 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); } } @@ -90,3 +92,4 @@ nr_segment_t* nr_php_stacked_segment_move_to_heap( return s; } +#endif /* not OAPI */ diff --git a/agent/php_stacked_segment.h b/agent/php_stacked_segment.h index e46659c81..b32f8afe9 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,7 +165,15 @@ * 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. @@ -215,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 6cbe28bc3..7e445c762 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); @@ -947,11 +950,10 @@ nr_status_t nr_php_txn_begin(const char* appnames, * returns 0 and INI_STR returns NULL. */ if (NR_PHP_PROCESS_GLOBALS(preload_framework_library_detection)) { - bool opcache_enabled = is_cli - ? INI_BOOL("opcache.enable_cli") - : INI_BOOL("opcache.enable"); - if ((opcache_enabled) - && (nr_php_ini_setting_is_set_by_user("opcache.preload"))) { + bool opcache_enabled + = is_cli ? INI_BOOL("opcache.enable_cli") : INI_BOOL("opcache.enable"); + if ((nr_php_ini_setting_is_set_by_user("opcache.preload")) + && (opcache_enabled)) { nr_php_user_instrumentation_from_opcache(TSRMLS_C); } } @@ -1065,15 +1067,27 @@ 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) * 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)); + + /* Stop all recording although we shouldn't be getting anything */ + NRTXN(status.recording) = 0; ignoretxn = nr_php_txn_should_ignore(ignoretxn TSRMLS_CC); diff --git a/agent/php_user_instrument.c b/agent/php_user_instrument.c index 7fd97db69..65b078b71 100644 --- a/agent/php_user_instrument.c +++ b/agent/php_user_instrument.c @@ -52,24 +52,61 @@ */ 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(); 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; + + if (wraprec && wraprec->special_instrumentation_before) { + zend_try { + wraprec->special_instrumentation_before(wraprec, segment, + NR_EXECUTE_ORIG_ARGS); + } + zend_catch { zcaught = 1; } + zend_end_try(); + } + + 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(); + } + return zcaught; +} +#endif 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; } @@ -218,10 +255,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 { diff --git a/agent/php_user_instrument.h b/agent/php_user_instrument.h index 50e2a54d3..f71889751 100644 --- a/agent/php_user_instrument.h +++ b/agent/php_user_instrument.h @@ -64,7 +64,21 @@ 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. 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; @@ -99,6 +113,7 @@ 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 : Init user instrumentation. This must only be called on request * init! This creates wraprec lookup hashmap and registers wraprec destructor @@ -194,7 +209,14 @@ 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); +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 * associated memory. diff --git a/agent/php_vm.c b/agent/php_vm.c index 4f8d7c023..982d0aff1 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. @@ -236,4 +241,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/agent/php_wrapper.c b/agent/php_wrapper.c index a7c4d9bf7..daa01bb4f 100644 --- a/agent/php_wrapper.c +++ b/agent/php_wrapper.c @@ -8,6 +8,98 @@ #include "php_wrapper.h" #include "util_logging.h" +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO +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) { + if (NULL == wraprec) { + return; + } + + /* 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; + } + + 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; + } + + 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->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( + const char* name, + size_t namelen, + nrspecialfn_t before_callback, + nrspecialfn_t after_callback, + nrspecialfn_t clean_callback) { + + nruserfn_t* wraprec = nr_php_add_custom_tracer_named(name, namelen); + + 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; +} +#endif nruserfn_t* nr_php_wrap_user_function(const char* name, size_t namelen, nrspecialfn_t callback TSRMLS_DC) { @@ -42,6 +134,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) { @@ -59,6 +152,17 @@ 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. + * + * This creates a transient wraprec that does NOT produce an + * "InstrumentedFunction" metric. + */ #define NR_WRAPPER_DEBUG_STRBUFSZ (1024) nruserfn_t* nr_php_wrap_generic_callable(zval* callable, @@ -66,7 +170,12 @@ nruserfn_t* nr_php_wrap_generic_callable(zval* callable, zend_function* zf = nr_php_zval_to_function(callable); if (NULL != zf) { +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + return nr_php_wrap_callable_before_after_clean(zf, callback, NULL, NULL); +#else return nr_php_wrap_callable(zf, callback); +#endif } if (nrl_should_print(NRL_VERBOSEDEBUG, NRL_INSTRUMENT)) { char strbuf[NR_WRAPPER_DEBUG_STRBUFSZ]; @@ -96,7 +205,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; @@ -222,6 +331,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) { diff --git a/agent/php_wrapper.h b/agent/php_wrapper.h index ed419bdcc..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 @@ -85,6 +85,72 @@ * 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 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. + * + * 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( + const char* name, + size_t namelen, + nrspecialfn_t before_callback, + nrspecialfn_t after_callback, + nrspecialfn_t clean_callback); + +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, nrspecialfn_t callback TSRMLS_DC); @@ -98,7 +164,8 @@ extern nruserfn_t* nr_php_wrap_callable(zend_function* callable, nrspecialfn_t callback TSRMLS_DC); extern nruserfn_t* nr_php_wrap_generic_callable(zval* callable, - nrspecialfn_t callback TSRMLS_DC); + nrspecialfn_t callback + TSRMLS_DC); /* * Purpose : Retrieve an argument from the current execute data. @@ -261,4 +328,14 @@ extern zval** nr_php_get_return_value_ptr(TSRMLS_D); was_executed = 1; \ } +static inline bool is_instrumentation_set_and_not_equal( + 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_agent.c b/agent/tests/test_agent.c index 509b4878b..12d3437e4 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" @@ -609,6 +610,7 @@ 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_function_lineno(); #endif /* PHP 7+ */ 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..f2f383110 100644 --- a/agent/tests/test_fw_drupal.c +++ b/agent/tests/test_fw_drupal.c @@ -370,6 +370,7 @@ static void test_drupal_http_request_drupal_6(TSRMLS_D) { } void test_main(void* p NRUNUSED) { + #if defined(ZTS) && !defined(PHP7) void*** tsrm_ls = NULL; #endif /* ZTS && !PHP7 */ diff --git a/agent/tests/test_internal_instrument.c b/agent/tests/test_internal_instrument.c index 0f4f2b3f8..4215c80cc 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 NRPRG(check_cufa) = true; define_cufa_function_f(TSRMLS_C); @@ -74,6 +78,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 NRPRG(check_cufa) = true; define_cufa_function_f(TSRMLS_C); diff --git a/agent/tests/test_php_execute.c b/agent/tests/test_php_execute.c index b3ad27f05..de770bc7d 100644 --- a/agent/tests/test_php_execute.c +++ b/agent/tests/test_php_execute.c @@ -95,6 +95,30 @@ 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 +126,7 @@ 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(TSRMLS_C); + 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..7d71f5ecd 100644 --- a/agent/tests/test_php_stacked_segment.c +++ b/agent/tests/test_php_stacked_segment.c @@ -10,10 +10,13 @@ #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}; +#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; @@ -95,6 +98,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. */ @@ -108,7 +116,6 @@ static void test_unwind(TSRMLS_D) { tlib_php_request_end(); } - void test_main(void* p NRUNUSED) { #if defined(ZTS) && !defined(PHP7) void*** tsrm_ls = NULL; @@ -118,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 */ diff --git a/agent/tests/test_php_wrapper.c b/agent/tests/test_php_wrapper.c index 88490e3db..975a46904 100644 --- a/agent/tests/test_php_wrapper.c +++ b/agent/tests/test_php_wrapper.c @@ -13,6 +13,13 @@ tlib_parallel_info_t parallel_info = {.suggested_nthreads = -1, .state_size = 0}; #if ZEND_MODULE_API_NO >= ZEND_7_3_X_API_NO + +/* + * 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(); @@ -44,12 +51,555 @@ 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; 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_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( + 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( + 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( + 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( + 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( + 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); @@ -71,7 +621,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 +643,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 +714,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 */ @@ -206,7 +756,17 @@ void test_main(void* p NRUNUSED) { #endif /* ZTS && !PHP7 */ tlib_php_engine_create("" PTSRMLS_CC); test_add_arg(); + 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) {} 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 + ... +} 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 a9ed249b4..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. */ @@ -182,6 +182,18 @@ 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. + */ + void* wraprec; /* wraprec, if one is associated with this segment, to reduce + wraprec lookups */ +#endif + } nr_segment_t; /* @@ -635,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/nr_txn.c b/axiom/nr_txn.c index 607ddfcba..353535777 100644 --- a/axiom/nr_txn.c +++ b/axiom/nr_txn.c @@ -1554,6 +1554,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) { @@ -1597,15 +1598,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 74a762bab..01f085624 100644 --- a/axiom/nr_txn.h +++ b/axiom/nr_txn.h @@ -455,9 +455,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. * @@ -472,6 +473,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/nr_version.c b/axiom/nr_version.c index 049aef173..8753d2bb6 100644 --- a/axiom/nr_version.c +++ b/axiom/nr_version.c @@ -44,8 +44,9 @@ * quince 13Nov2023 (10.14) * rose 20Dec2023 (10.15) * snapdragon 23Jan2024 (10.16) + * tulip 21Feb2024 (10.17) */ -#define NR_CODENAME "tulip" +#define NR_CODENAME "ulmus" const char* nr_version(void) { return NR_STR2(NR_VERSION); 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/axiom/tests/test_txn.c b/axiom/tests/test_txn.c index c100982b4..b8abea269 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/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/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/src/newrelic/integration/parse.go b/src/newrelic/integration/parse.go index 5619d0712..5e0f57fc8 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, @@ -199,6 +200,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 89a7326af..31aa7b8eb 100644 --- a/src/newrelic/integration/test.go +++ b/src/newrelic/integration/test.go @@ -58,10 +58,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 @@ -210,6 +211,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/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..f66452c15 --- /dev/null +++ b/tests/integration/api/add_custom_parameter/test_add_custom_parameter_nested_caught_exception.php @@ -0,0 +1,223 @@ +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..683a162e7 --- /dev/null +++ b/tests/integration/api/add_custom_parameter/test_add_custom_parameter_nested_happy.php @@ -0,0 +1,219 @@ +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..a8540345c --- /dev/null +++ b/tests/integration/api/add_custom_parameter/test_add_custom_parameter_nested_uncaught_exception.php @@ -0,0 +1,249 @@ +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..3c6323715 --- /dev/null +++ b/tests/integration/api/add_custom_span_parameter/test_span_event_parameter_nested_happy.php @@ -0,0 +1,219 @@ +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..53f82018f --- /dev/null +++ b/tests/integration/api/add_custom_span_parameter/test_span_event_parameter_nested_uncaught_exception.php @@ -0,0 +1,245 @@ +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..d532a053f --- /dev/null +++ b/tests/integration/api/distributed_trace/newrelic/test_create_payload_nested_happy.php @@ -0,0 +1,145 @@ +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..433e0f8ab --- /dev/null +++ b/tests/integration/api/distributed_trace/newrelic/test_create_payload_nested_uncaught_exception.php @@ -0,0 +1,236 @@ +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..6960e5237 --- /dev/null +++ b/tests/integration/api/notice_error/test_notice_error_nested_happy.php @@ -0,0 +1,233 @@ +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..d4f4e1e8d --- /dev/null +++ b/tests/integration/api/notice_error/test_notice_error_nested_uncaught_exception.php @@ -0,0 +1,235 @@ +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..231f82dac --- /dev/null +++ b/tests/integration/api/other/test_set_user_attributes_nested_happy.php @@ -0,0 +1,211 @@ +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..abc0c19ca --- /dev/null +++ b/tests/integration/api/other/test_set_user_attributes_nested_uncaught_exception.php @@ -0,0 +1,241 @@ +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/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/external/drupal7/test_bad_params_integer_headers.php b/tests/integration/external/drupal7/test_bad_params_integer_headers.php index cbc0935d9..1f0aec4c8 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..36cf6b0f1 --- /dev/null +++ b/tests/integration/external/drupal7/test_bad_params_integer_headers.php8.php @@ -0,0 +1,49 @@ + 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 12875d57b..fe9463431 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..1189821b1 --- /dev/null +++ b/tests/integration/external/drupal7/test_bad_params_null_headers.php8.php @@ -0,0 +1,49 @@ + NULL)); 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 ""; } diff --git a/tests/integration/frameworks/drupal/test_module_invoke_all.php b/tests/integration/frameworks/drupal/test_module_invoke_all.php index 46400cb81..c9b70e1cf 100644 --- a/tests/integration/frameworks/drupal/test_module_invoke_all.php +++ b/tests/integration/frameworks/drupal/test_module_invoke_all.php @@ -13,7 +13,11 @@ */ /*SKIPIF +=")) { + die("skip: PHP >= 8.0 uses other test\n"); +} */ /*EXPECT 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..fc694e039 --- /dev/null +++ b/tests/integration/frameworks/drupal/test_module_invoke_all.php8.php @@ -0,0 +1,91 @@ + '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', 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 @@ +')) { 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..62139de98 --- /dev/null +++ b/tests/integration/lang/test_generator_8.0.php @@ -0,0 +1,88 @@ +> 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"; 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 @@ 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/span_events/test_span_events_are_created_upon_caught_exception.php5.php b/tests/integration/opcache/disabled/test_span_events_are_created_upon_uncaught_handled_exception.php7.php similarity index 75% rename from tests/integration/span_events/test_span_events_are_created_upon_caught_exception.php5.php rename to tests/integration/opcache/disabled/test_span_events_are_created_upon_uncaught_handled_exception.php7.php index ad7e3a94d..67bc1b428 100644 --- a/tests/integration/span_events/test_span_events_are_created_upon_caught_exception.php5.php +++ b/tests/integration/opcache/disabled/test_span_events_are_created_upon_uncaught_handled_exception.php7.php @@ -3,10 +3,22 @@ * Copyright 2020 New Relic Corporation. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ - + /*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 not be created for PHP 7.x (non-OAPI). +*/ + +/*SKIPIF +=")) { + die("skip: PHP > 8.0 not supported\n"); +} + +require('skipif.inc'); + */ /*INI @@ -14,13 +26,21 @@ newrelic.transaction_tracer.threshold = 0 newrelic.span_events_enabled=1 newrelic.cross_application_tracer.enabled = false -newrelic.code_level_metrics.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", @@ -87,7 +107,10 @@ {}, { "error.message": "Uncaught exception 'RuntimeException' with message 'oops' in __FILE__:??", - "error.class": "RuntimeException" + "error.class": "RuntimeException", + "code.lineno": "??", + "code.filepath": "__FILE__", + "code.function": "a" } ], [ @@ -105,7 +128,11 @@ "parentId": "??" }, {}, - {} + { + "code.lineno": "??", + "code.filepath": "__FILE__", + "code.function": "{closure}" + } ] ] ] @@ -113,9 +140,10 @@ /*EXPECT */ +require('opcache_test.inc'); set_exception_handler( - function () { + function (Throwable $exception) { time_nanosleep(0, 100000000); exit(0); } 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 @@ +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(); } 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_uncaught_handled_exception.php b/tests/integration/span_events/test_span_events_are_created_upon_uncaught_handled_exception.php new file mode 100644 index 000000000..7670f8a27 --- /dev/null +++ b/tests/integration/span_events/test_span_events_are_created_upon_uncaught_handled_exception.php @@ -0,0 +1,182 @@ + 'FakeDB', + ) +); +a(); + +echo 'this should never be printed'; diff --git a/tests/integration/span_events/test_span_events_are_created_upon_caught_exception.php b/tests/integration/span_events/test_span_events_are_created_upon_uncaught_handled_exception.php7.php similarity index 94% rename from tests/integration/span_events/test_span_events_are_created_upon_caught_exception.php rename to tests/integration/span_events/test_span_events_are_created_upon_uncaught_handled_exception.php7.php index 628f5f66e..24c64e436 100644 --- a/tests/integration/span_events/test_span_events_are_created_upon_caught_exception.php +++ b/tests/integration/span_events/test_span_events_are_created_upon_uncaught_handled_exception.php7.php @@ -14,6 +14,9 @@ if (version_compare(PHP_VERSION, "7.0", "<")) { die("skip: CLM for PHP 5 not supported\n"); } +if (version_compare(PHP_VERSION, "8.0", ">=")) { + 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