diff --git a/.github/actions/ubuntu16-build-action/Dockerfile b/.github/actions/ubuntu16-build-action/Dockerfile deleted file mode 100644 index c43e2c564..000000000 --- a/.github/actions/ubuntu16-build-action/Dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -FROM ubuntu:16.04 - -# Uncomment deb-src lines for all enabled repos. First part of single-quoted -# string (up the the !) is the pattern of the lines that will be ignored. -# Needed for apt-get build-dep call later in script -RUN sed -Ei '/.*partner/! s/^# (deb-src .*)/\1/g' /etc/apt/sources.list - -ARG DEBIAN_FRONTEND=noninteractive -RUN apt-get update -RUN apt-get install -y build-essential - -# PHP dependencies -RUN apt-get build-dep -y php7.0 -RUN apt-get install -y libmysqlclient-dev php-dev libmcrypt-dev libphp7.0-embed - -# Other tools -RUN apt-get install -y curl gdb valgrind libcurl4-openssl-dev pkg-config postgresql python-psycopg2 libpq-dev libedit-dev libreadline-dev git - -COPY build.sh /build.sh - -ENTRYPOINT ["/build.sh"] diff --git a/.github/actions/ubuntu16-build-action/action.yml b/.github/actions/ubuntu16-build-action/action.yml deleted file mode 100644 index 2a01f96da..000000000 --- a/.github/actions/ubuntu16-build-action/action.yml +++ /dev/null @@ -1,5 +0,0 @@ -name: ubuntu16-build-action -description: 'Build the agent on ubuntu 16' -runs: - using: 'docker' - image: 'Dockerfile' diff --git a/.github/actions/ubuntu16-build-action/build.sh b/.github/actions/ubuntu16-build-action/build.sh deleted file mode 100755 index 079de1ba1..000000000 --- a/.github/actions/ubuntu16-build-action/build.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -make -j $(nproc) all -make -j $(nproc) run_tests - diff --git a/.github/actions/ubuntu18-build-action/Dockerfile b/.github/actions/ubuntu18-build-action/Dockerfile deleted file mode 100644 index b9bb190db..000000000 --- a/.github/actions/ubuntu18-build-action/Dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -FROM ubuntu:18.04 - -# Uncomment deb-src lines for all enabled repos. First part of single-quoted -# string (up the the !) is the pattern of the lines that will be ignored. -# Needed for apt-get build-dep call later in script -RUN sed -Ei '/.*partner/! s/^# (deb-src .*)/\1/g' /etc/apt/sources.list - -ARG DEBIAN_FRONTEND=noninteractive -RUN apt-get update -RUN apt-get install -y build-essential - -# PHP dependencies -RUN apt-get build-dep -y php7.2 -RUN apt-get install -y libmysqlclient-dev php-dev libmcrypt-dev libphp7.2-embed - -# Other tools -RUN apt-get install -y curl gdb valgrind libcurl4-openssl-dev pkg-config postgresql python-psycopg2 libpq-dev libedit-dev libreadline-dev git - -COPY build.sh /build.sh - -ENTRYPOINT ["/build.sh"] diff --git a/.github/actions/ubuntu18-build-action/action.yml b/.github/actions/ubuntu18-build-action/action.yml deleted file mode 100644 index 5b8fc28d6..000000000 --- a/.github/actions/ubuntu18-build-action/action.yml +++ /dev/null @@ -1,5 +0,0 @@ -name: ubuntu18-build-action -description: 'Build the agent on ubuntu 18' -runs: - using: 'docker' - image: 'Dockerfile' diff --git a/.github/actions/ubuntu18-build-action/build.sh b/.github/actions/ubuntu18-build-action/build.sh deleted file mode 100755 index 079de1ba1..000000000 --- a/.github/actions/ubuntu18-build-action/build.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -make -j $(nproc) all -make -j $(nproc) run_tests - diff --git a/.github/actions/ubuntu20-build-action/Dockerfile b/.github/actions/ubuntu20-build-action/Dockerfile deleted file mode 100644 index bbd6fe406..000000000 --- a/.github/actions/ubuntu20-build-action/Dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -FROM ubuntu:20.04 - -# Uncomment deb-src lines for all enabled repos. First part of single-quoted -# string (up the the !) is the pattern of the lines that will be ignored. -# Needed for apt-get build-dep call later in script -RUN sed -Ei '/.*partner/! s/^# (deb-src .*)/\1/g' /etc/apt/sources.list - -ARG DEBIAN_FRONTEND=noninteractive -RUN apt-get update -RUN apt-get install -y build-essential - -# PHP dependencies -RUN apt-get build-dep -y php7.4 -RUN apt-get install -y libmysqlclient-dev php-dev libmcrypt-dev libphp7.4-embed - -# Other tools -RUN apt-get install -y curl gdb valgrind libcurl4-openssl-dev pkg-config postgresql libpq-dev libedit-dev libreadline-dev git - -COPY build.sh /build.sh - -ENTRYPOINT ["/build.sh"] diff --git a/.github/actions/ubuntu20-build-action/action.yml b/.github/actions/ubuntu20-build-action/action.yml deleted file mode 100644 index d42d71cf9..000000000 --- a/.github/actions/ubuntu20-build-action/action.yml +++ /dev/null @@ -1,5 +0,0 @@ -name: ubuntu20-build-action -description: 'Build the agent on ubuntu 20' -runs: - using: 'docker' - image: 'Dockerfile' diff --git a/.github/actions/ubuntu20-build-action/build.sh b/.github/actions/ubuntu20-build-action/build.sh deleted file mode 100755 index 079de1ba1..000000000 --- a/.github/actions/ubuntu20-build-action/build.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -make -j $(nproc) all -make -j $(nproc) run_tests - diff --git a/.github/workflows/code-coverage-baseline.yml b/.github/workflows/code-coverage-baseline.yml index dcef64963..199bdccac 100644 --- a/.github/workflows/code-coverage-baseline.yml +++ b/.github/workflows/code-coverage-baseline.yml @@ -69,7 +69,7 @@ jobs: matrix: platform: [gnu, musl] arch: [amd64] - php: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3'] + php: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] include: - codecov: 0 - platform: gnu @@ -147,7 +147,7 @@ jobs: matrix: platform: [gnu, musl] arch: [amd64] - php: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3'] + php: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] include: - codecov: 0 - platform: gnu diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c715fe73b..48d046adf 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -16,7 +16,7 @@ jobs: # - https://gh.io/supported-runners-and-hardware-resources # - https://gh.io/using-larger-runners # Consider using larger runners for possible analysis time improvements. - runs-on: 'ubuntu-latest' + runs-on: 'ubuntu-22.04' timeout-minutes: 360 permissions: # required for all workflows diff --git a/.github/workflows/issue-comment.yml b/.github/workflows/issue-comment.yml index cd72f7874..ba345e98a 100644 --- a/.github/workflows/issue-comment.yml +++ b/.github/workflows/issue-comment.yml @@ -25,4 +25,4 @@ jobs: steps: - name: respond to issue run: | - gh issue comment ${{ inputs.issue-number }} --body "${{ inputs.message }}" + gh issue -R ${{ github.repository }} comment ${{ inputs.issue-number }} --body "${{ inputs.message }}" diff --git a/.github/workflows/make-agent.yml b/.github/workflows/make-agent.yml index b29881676..6508fcaf3 100644 --- a/.github/workflows/make-agent.yml +++ b/.github/workflows/make-agent.yml @@ -32,7 +32,7 @@ jobs: strategy: matrix: platform: [gnu, musl] - php: ['8.0', '8.1', '8.2', '8.3'] + php: ['8.0', '8.1', '8.2', '8.3', '8.4'] steps: - name: Checkout Repo uses: actions/checkout@v4 diff --git a/.github/workflows/make-integration-tests.yml b/.github/workflows/make-integration-tests.yml index 800146f41..0b6660575 100644 --- a/.github/workflows/make-integration-tests.yml +++ b/.github/workflows/make-integration-tests.yml @@ -34,7 +34,7 @@ jobs: fail-fast: true matrix: platform: [gnu, musl] - php: ['8.0', '8.1', '8.2', '8.3'] + php: ['8.0', '8.1', '8.2', '8.3', '8.4'] steps: - name: Checkout integration tests uses: actions/checkout@v4 diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml new file mode 100644 index 000000000..a5bbd9cd9 --- /dev/null +++ b/.github/workflows/security-scan.yml @@ -0,0 +1,49 @@ +name: Security scan +on: + push: + branches: + - main + - dev + pull_request: + schedule: + - cron: '0 0 * * 0' # Every Sunday at 12:00 AM + +jobs: + trivy-scan: + runs-on: ubuntu-latest + steps: + - name: Checkout newrelic-php-agent code + uses: actions/checkout@v4 + with: + path: php-agent + - name: Run Trivy in table mode + # Table output is only useful when running on a pull request or push. + if: contains(fromJSON('["push", "pull_request"]'), github.event_name) + uses: aquasecurity/trivy-action@0.28.0 + with: + scan-type: fs + scan-ref: ./php-agent + trivy-config: ./php-agent/trivy.yaml + trivyignores: ./php-agent/.trivyignore + format: table + exit-code: 1 + + - name: Run Trivy in report mode + # Only generate sarif when running nightly on the dev branch. + if: ${{ github.event_name == 'schedule' }} + uses: aquasecurity/trivy-action@0.28.0 + with: + scan-type: fs + scan-ref: ./php-agent + trivy-config: ./php-agent/trivy.yaml + trivyignores: ./php-agent/.trivyignore + format: sarif + output: trivy-results.sarif + + - name: Upload Trivy scan results to GitHub Security tab + # Only upload sarif when running nightly on the dev branch. + if: ${{ github.event_name == 'schedule' }} + uses: github/codeql-action/upload-sarif@v3 + with: + checkout_path: ./php-agent + sarif_file: trivy-results.sarif diff --git a/.github/workflows/test-agent.yml b/.github/workflows/test-agent.yml index d40384e13..efb68d872 100644 --- a/.github/workflows/test-agent.yml +++ b/.github/workflows/test-agent.yml @@ -19,6 +19,64 @@ on: pull_request: jobs: + gofmt-check: + runs-on: ubuntu-latest + continue-on-error: true + steps: + - name: Checkout newrelic-php-agent code + uses: actions/checkout@v4 + with: + path: php-agent + - name: Setup go + uses: actions/setup-go@v5 + with: + go-version-file: ./php-agent/daemon/go.mod + cache: false + - name: Display go version + run: | + go version + - name: Run gofmt + run: | + GOFMT_REPORTED_FILES="$(gofmt -l -e ./php-agent/daemon)" + if [ ! -z "$GOFMT_REPORTED_FILES" ]; then + gofmt -d -e ./php-agent/daemon + echo "### gofmt violations found in $(echo "$GOFMT_REPORTED_FILES" | wc -l) files" >> $GITHUB_STEP_SUMMARY + echo "$GOFMT_REPORTED_FILES" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + govet: + runs-on: ubuntu-latest + continue-on-error: true + steps: + - name: Checkout newrelic-php-agent code + uses: actions/checkout@v4 + with: + path: php-agent + - name: Get go version + id: get-go-version + run: | + echo "go toolchain version required to build the daemon:" + toolchain="$(awk '/^toolchain */ {gsub(/^go/, "", $2); print $2}' ./php-agent/daemon/go.mod)" + echo "[${toolchain}]" + echo "go-toolchain-version=${toolchain}" >> $GITHUB_OUTPUT + - name: Setup go + uses: actions/setup-go@v5 + with: + go-version: ${{ steps.get-go-version.outputs.go-toolchain-version }} + cache-dependency-path: "**/*.sum" + - name: Verify go version + run: | + echo "Verify correct go toolchain version is used" + actual="$(go version)" + echo "Actual: [$actual]" + expected="go version go${{ steps.get-go-version.outputs.go-toolchain-version }} linux/amd64" + echo "Expected: [$expected]" + if [ "$actual" != "$expected" ]; then + exit 1 + fi + - name: Run go vet + run: go vet -C ./php-agent/daemon ./... + shell: bash daemon-unit-tests: runs-on: ubuntu-latest env: @@ -72,12 +130,8 @@ jobs: matrix: platform: [gnu, musl] arch: [amd64, arm64] - php: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3'] + php: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] exclude: - - arch: arm64 - php: '7.0' - - arch: arm64 - php: '7.1' - arch: arm64 php: '7.2' - arch: arm64 @@ -117,9 +171,7 @@ jobs: echo "AGENT_CHECK_VARIANT=check" >> $GITHUB_OUTPUT elif [[ ${{ matrix.platform }} = 'gnu' ]]; then echo "AGENT_CHECK_VARIANT=valgrind" >> $GITHUB_OUTPUT - elif [[ ${{matrix.php}} = '7.0' || ${{matrix.php}} = '7.1' ]]; then - echo "AGENT_CHECK_VARIANT=check" >> $GITHUB_OUTPUT - else + else echo "AGENT_CHECK_VARIANT=valgrind" >> $GITHUB_OUTPUT fi - name: Build axiom @@ -174,6 +226,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: agent.gcov-${{matrix.platform}}-${{matrix.arch}}-${{matrix.php}} + include-hidden-files: true path: php-agent/agent/.libs/*.gc* integration-tests: needs: [daemon-unit-tests, agent-unit-test] @@ -183,12 +236,8 @@ jobs: matrix: platform: [gnu, musl] arch: [amd64, arm64] - php: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3'] + php: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] exclude: - - arch: arm64 - php: '7.0' - - arch: arm64 - php: '7.1' - arch: arm64 php: '7.2' - arch: arm64 diff --git a/.github/workflows/trigger-test-suite.yml b/.github/workflows/trigger-test-suite.yml index f141241b7..1671ae46d 100644 --- a/.github/workflows/trigger-test-suite.yml +++ b/.github/workflows/trigger-test-suite.yml @@ -8,11 +8,11 @@ on: pull_request: jobs: - trigger-multiverse-tests: + trigger-test-suite: runs-on: ubuntu-latest env: GH_TOKEN: ${{ secrets.TEST_SUITE_REPO_GH_TOKEN }} steps: - - name: Trigger Multiverse Test Suite + - name: Trigger Test Suite run: | gh workflow run -R ${{ secrets.TEST_SUITE_REPO }} ${{ secrets.TEST_SUITE_WORKFLOW }} -f agent_git_ref=${{ github.head_ref }} -f pr-number=${{ github.event.number }} diff --git a/.trivyignore b/.trivyignore new file mode 100644 index 000000000..01a5782ee --- /dev/null +++ b/.trivyignore @@ -0,0 +1,2 @@ +# Ignore missing HEALTHCHECK in Dockerfile - devenv service from files/Dockerfile doesn't need it: +AVD-DS-0026 diff --git a/Makefile b/Makefile index 0e95237d5..11a871312 100644 --- a/Makefile +++ b/Makefile @@ -475,25 +475,28 @@ test-services-stop: # Docker Development Environment # -dev-shell: - docker compose --profile dev up --build --remove-orphans -d - docker exec -it agent-devenv bash -c "sh files/set_path.sh ; bash" +devenv-image: + @docker compose --profile dev build devenv -dev-build: - docker compose --profile dev up --build --remove-orphans -d - docker exec -it agent-devenv bash -c "sh files/set_path.sh ; make -j4 all" +dev-shell: devenv-image + docker compose --profile dev up --pull missing --remove-orphans -d + docker compose exec -it devenv bash -c "sh files/set_path.sh ; bash" -dev-unit-tests: - docker compose --profile dev up --build --remove-orphans -d - docker exec -it agent-devenv bash -c "sh files/set_path.sh ; make -j4 valgrind" +dev-build: devenv-image + docker compose --profile dev up --pull missing --remove-orphans -d + docker compose exec -it devenv bash -c "sh files/set_path.sh ; make -j4 all" -dev-integration-tests: - docker compose --profile dev up --build --remove-orphans -d - docker exec -it agent-devenv bash -c "sh files/set_path.sh ; ./bin/integration_runner -agent ./agent/.libs/newrelic.so" +dev-unit-tests: devenv-image + docker compose --profile dev up --pull missing --remove-orphans -d + docker compose exec -it devenv bash -c "sh files/set_path.sh ; make -j4 valgrind" -dev-all: - docker compose --profile dev up --build --remove-orphans -d - docker exec -it agent-devenv bash -c "sh files/set_path.sh ; make -j4 all valgrind; ./bin/integration_runner -agent ./agent/.libs/newrelic.so" +dev-integration-tests: devenv-image + docker compose --profile dev up --pull missing --remove-orphans -d + docker compose exec -it devenv bash -c "sh files/set_path.sh ; ./bin/integration_runner -agent ./agent/.libs/newrelic.so" + +dev-all: devenv-image + docker compose --profile dev up --pull missing --remove-orphans -d + docker compose exec -it devenv bash -c "sh files/set_path.sh ; make -j4 all valgrind; ./bin/integration_runner -agent ./agent/.libs/newrelic.so" dev-stop: docker compose --profile dev stop diff --git a/VERSION b/VERSION index b61c07ffd..146d5de79 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -10.21.0 +11.6.0 diff --git a/agent/Makefile.frag b/agent/Makefile.frag index f11dfbf75..2bfc562a8 100644 --- a/agent/Makefile.frag +++ b/agent/Makefile.frag @@ -49,13 +49,17 @@ $(PHP_MODULES): .libs/deps.mk newrelic.la: $(PHP_AXIOM)/libaxiom.a # -# The version number is needed by php_newrelic.c as a static string literal, +# The version number is needed by several source files as a static string literal, # so it can be placed in the module entry. # include ../make/version.mk + php_newrelic.lo: CPPFLAGS += -DNR_VERSION="\"$(AGENT_VERSION)\"" php_newrelic.lo: ../VERSION +php_txn.lo: CPPFLAGS += -DNR_VERSION="\"$(AGENT_VERSION)\"" +php_txn.lo: ../VERSION + # # Unit tests! # @@ -88,6 +92,9 @@ TEST_BINARIES = \ tests/test_globals \ tests/test_internal_instrument \ tests/test_hash \ + tests/test_lib_aws_sdk_php \ + tests/test_lib_php_amqplib \ + tests/test_memcached \ tests/test_mongodb \ tests/test_monolog \ tests/test_mysql \ @@ -102,6 +109,7 @@ TEST_BINARIES = \ tests/test_php_minit \ tests/test_php_stack \ tests/test_php_stacked_segment \ + tests/test_php_txn \ tests/test_php_wrapper \ tests/test_predis \ tests/test_redis \ @@ -260,6 +268,12 @@ ifeq (/opt/nr/lamp/lib,$(findstring /opt/nr/lamp/lib,$(PHP_EMBED_LIBRARY))) endif endif +# +# Need agent version for test_txn +# +tests/test_txn.o: EXTRA_CFLAGS += -DNR_VERSION="\"$(AGENT_VERSION)\"" +tests/test_txn.o: ../VERSION + # # Used when linking test binaries. # diff --git a/agent/config.m4 b/agent/config.m4 index 8beef6a4f..33a43eda2 100644 --- a/agent/config.m4 +++ b/agent/config.m4 @@ -215,7 +215,7 @@ if test "$PHP_NEWRELIC" = "yes"; then php_error.c php_execute.c php_explain.c php_explain_mysqli.c \ 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_internal_instrument.c php_memcached.c php_minit.c php_mshutdown.c php_mysql.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 \ @@ -228,16 +228,20 @@ if test "$PHP_NEWRELIC" = "yes"; then fw_silex.c fw_slim.c fw_support.c fw_symfony4.c fw_symfony2.c \ fw_symfony.c fw_symfony_common.c fw_wordpress.c fw_yii.c \ fw_zend2.c fw_zend.c" - LIBRARIES="lib_monolog.c lib_doctrine2.c lib_guzzle3.c \ + LIBRARIES="lib_aws_sdk_php.c lib_monolog.c lib_doctrine2.c lib_guzzle3.c \ lib_guzzle4.c lib_guzzle6.c lib_guzzle_common.c \ - lib_mongodb.c lib_phpunit.c lib_predis.c lib_zend_http.c" - PHP_NEW_EXTENSION(newrelic, $FRAMEWORKS $LIBRARIES $NEWRELIC_AGENT, $ext_shared,, \\$(NEWRELIC_CFLAGS)) + lib_mongodb.c lib_phpunit.c lib_predis.c lib_zend_http.c \ + lib_composer.c lib_php_amqplib.c" + PHP_NEW_EXTENSION(newrelic, $FRAMEWORKS $LIBRARIES $NEWRELIC_AGENT, $ext_shared,, $(NEWRELIC_CFLAGS)) PHP_SUBST(NEWRELIC_CFLAGS) dnl Define $(PHP_CONFIG) so we can call it when building tests. PHP_SUBST(PHP_CONFIG) + dnl Make sure we include the source directory in the include search path. + PHP_ADD_INCLUDE([$abs_srcdir], [1]) + dnl Include the Makefile.frag, which we use to handle build time dnl dependencies. PHP_ADD_MAKEFILE_FRAGMENT diff --git a/agent/csec_metadata.h b/agent/csec_metadata.h index a98b79339..155926447 100644 --- a/agent/csec_metadata.h +++ b/agent/csec_metadata.h @@ -36,4 +36,4 @@ typedef enum { extern int nr_php_csec_get_metadata(const nr_php_csec_metadata_key_t k, char** value); typedef int (*nr_php_csec_get_metadata_t)(const nr_php_csec_metadata_key_t k, char** value); #define NR_PHP_CSEC_GET_METADATA "nr_php_csec_get_metadata" -#endif +#endif \ No newline at end of file diff --git a/agent/fw_cakephp.c b/agent/fw_cakephp.c index 1dfe88231..572f6f23f 100644 --- a/agent/fw_cakephp.c +++ b/agent/fw_cakephp.c @@ -4,6 +4,7 @@ */ #include "php_agent.h" +#include "php_error.h" #include "php_execute.h" #include "php_user_instrument.h" #include "php_wrapper.h" @@ -12,132 +13,14 @@ #include "util_logging.h" #include "util_memory.h" -nr_framework_classification_t nr_cakephp_special_1( - const char* filename TSRMLS_DC) { - NR_UNUSED_TSRMLS; - - if (nr_strcaseidx(filename, "cake/libs/object.php") >= 0) { - return FRAMEWORK_IS_SPECIAL; - } - - return FRAMEWORK_IS_NORMAL; -} - -nr_framework_classification_t nr_cakephp_special_2( - const char* filename TSRMLS_DC) { - NR_UNUSED_TSRMLS; - - if (nr_strcaseidx(filename, "cake/core/app.php") >= 0) { - return FRAMEWORK_IS_SPECIAL; - } - - return FRAMEWORK_IS_NORMAL; -} - -/* - * For CakePHP 1.2 and 1.3 (and possibly earlier versions too) we hook into - * 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; - zval* params; - zval* czval; - zval* azval; - char* controller = 0; - char* action = 0; - int clen = 0; - int alen = 0; - char* name; - - (void)wraprec; - NR_UNUSED_SPECIALFN; - - NR_PHP_WRAPPER_REQUIRE_FRAMEWORK(NR_FW_CAKEPHP); - - arg1 = nr_php_arg_get(1, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); - if (!nr_php_is_zval_valid_object(arg1)) { - NR_PHP_WRAPPER_CALL; - goto end; - } - - NR_PHP_WRAPPER_CALL; - - params = nr_php_get_zval_object_property(arg1, "params" TSRMLS_CC); - if (0 == params) { - nrl_verbosedebug(NRL_FRAMEWORK, "CakePHP: no params found in component"); - goto end; - } - - if (IS_ARRAY != Z_TYPE_P(params)) { - nrl_verbosedebug(NRL_FRAMEWORK, - "CakePHP: component params is not an array"); - goto end; - } - - czval = nr_php_get_zval_object_property(params, "controller" TSRMLS_CC); - if (0 == czval) { - nrl_verbosedebug(NRL_FRAMEWORK, - "CakePHP: no params['controller'] in component"); - } else { - clen = Z_STRLEN_P(czval); - controller = (char*)nr_alloca(clen + 1); - nr_strxcpy(controller, Z_STRVAL_P(czval), clen); - } - - azval = nr_php_get_zval_object_property(params, "action" TSRMLS_CC); - if (0 == azval) { - nrl_verbosedebug(NRL_FRAMEWORK, - "CakePHP: no params['action'] in component"); - } else { - alen = Z_STRLEN_P(azval); - action = (char*)nr_alloca(alen + 1); - nr_strxcpy(action, Z_STRVAL_P(azval), alen); - } - - if ((0 == clen) && (0 == alen)) { - nrl_verbosedebug(NRL_FRAMEWORK, - "CakePHP: nothing to call the transaction (yet?)"); - goto end; - } - - name = (char*)nr_alloca(alen + clen + 2); - if (clen) { - nr_strcpy(name, controller); - } - if (alen) { - if (clen) { - nr_strcat(name, "/"); - nr_strcat(name, action); - } else { - nr_strcpy(name, action); - } - } - - nr_txn_set_path("CakePHP", NRPRG(txn), name, NR_PATH_TYPE_ACTION, - NR_NOT_OK_TO_OVERWRITE); - -end: - nr_php_arg_release(&arg1); -} -NR_PHP_WRAPPER_END +#define PHP_PACKAGE_NAME "cakephp/cakephp" /* - * For CakePHP 2.0 and on, we do things a little differently as the params - * array doesn't exist in the component any more. Instead we hook the - * Controller's invokeAction method. This gets the request as a parameter - * 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. + * For CakePHP 4.0 and on, we retrieve the current controller object + * and are able to extract the controller name from that. We then + * retrieve the request object from the controller and are able to + * extract the action name from that. We then concatenate the two + * strings to form the transaction name. * * txn naming scheme: * In this case, `nr_txn_set_path` is called after `NR_PHP_WRAPPER_CALL` with @@ -147,8 +30,7 @@ NR_PHP_WRAPPER_END * default way of calling the wrapped function in func_end. * */ -NR_PHP_WRAPPER(nr_cakephp_name_the_wt_2) { - zval* arg1 = 0; +NR_PHP_WRAPPER(nr_cakephp_name_the_wt_4) { zval* this_var = 0; zval* czval = 0; char* controller = 0; @@ -156,8 +38,9 @@ NR_PHP_WRAPPER(nr_cakephp_name_the_wt_2) { int clen = 0; int alen = 0; char* name = 0; - zval* params; - zval* azval; + zval* action_zval = NULL; + zval* request = NULL; + zval action_param; (void)wraprec; NR_UNUSED_SPECIALFN; @@ -193,37 +76,24 @@ NR_PHP_WRAPPER(nr_cakephp_name_the_wt_2) { } } - arg1 = nr_php_arg_get(1, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); - if (!nr_php_is_zval_valid_object(arg1)) { - NR_PHP_WRAPPER_CALL; - goto end; - } - NR_PHP_WRAPPER_CALL; - params = nr_php_get_zval_object_property(arg1, "params" TSRMLS_CC); - if (0 == params) { - nrl_verbosedebug(NRL_FRAMEWORK, "CakePHP: no params found in request"); + request = nr_php_call(this_var, "getRequest"); + if (!nr_php_is_zval_valid_object(request)) { + nrl_verbosedebug(NRL_FRAMEWORK, "CakePHP: no request found in controller"); goto end; } - if (IS_ARRAY != Z_TYPE_P(params)) { - nrl_verbosedebug(NRL_FRAMEWORK, "CakePHP: request params is not an array"); + nr_php_zval_str(&action_param, "action"); + action_zval = nr_php_call(request, "getParam", &action_param); + zval_dtor(&action_param); + if (!nr_php_is_zval_non_empty_string(action_zval)) { + nrl_verbosedebug(NRL_FRAMEWORK, "CakePHP: no action param found in request"); goto end; - } - - azval = nr_php_get_zval_object_property(params, "action" TSRMLS_CC); - if (0 == azval) { - nrl_verbosedebug(NRL_FRAMEWORK, "CakePHP: no params['action'] in request"); } else { - if (!nr_php_is_zval_valid_string(azval)) { - nrl_verbosedebug(NRL_FRAMEWORK, - "CakePHP: no string-valued params['action'] in request"); - } else { - alen = Z_STRLEN_P(azval); - action = (char*)nr_alloca(alen + 1); - nr_strxcpy(action, Z_STRVAL_P(azval), alen); - } + alen = Z_STRLEN_P(action_zval); + action = (char*)nr_alloca(alen + 1); + nr_strxcpy(action, Z_STRVAL_P(action_zval), alen); } if ((0 == clen) && (0 == alen)) { @@ -249,92 +119,64 @@ NR_PHP_WRAPPER(nr_cakephp_name_the_wt_2) { NR_NOT_OK_TO_OVERWRITE); end: - nr_php_arg_release(&arg1); nr_php_scope_release(&this_var); + nr_php_zval_free(&request); + nr_php_zval_free(&action_zval); } NR_PHP_WRAPPER_END /* - * CakePHP 1.2, 1.3 - * - * 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. + * CakePHP 4.0+ * + * Report errors and exceptions caught by CakePHP's error handler. */ -NR_PHP_WRAPPER(nr_cakephp_problem_1) { - const char* name = "Dispatcher::cakeError"; +NR_PHP_WRAPPER(nr_cakephp_error_handler_wrapper) { + zval* exception = NULL; + char* request_uri = nr_php_get_server_global("REQUEST_URI"); - (void)wraprec; NR_UNUSED_SPECIALFN; + (void)wraprec; NR_PHP_WRAPPER_REQUIRE_FRAMEWORK(NR_FW_CAKEPHP); - nr_txn_set_path("CakePHP", NRPRG(txn), name, NR_PATH_TYPE_ACTION, - NR_NOT_OK_TO_OVERWRITE); - - NR_PHP_WRAPPER_CALL; -} -NR_PHP_WRAPPER_END - -/* - * CakePHP 2.0+ - * - * If the action or controller is not found during the dispatch process, the - * 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"; - - (void)wraprec; - NR_UNUSED_SPECIALFN; + exception = nr_php_arg_get(1, NR_EXECUTE_ORIG_ARGS); + if (!nr_php_is_zval_valid_object(exception)) { + nrl_verbosedebug(NRL_FRAMEWORK, "%s: exception is NULL or not an object", + __func__); + goto end; + } - NR_PHP_WRAPPER_REQUIRE_FRAMEWORK(NR_FW_CAKEPHP); + if (NR_SUCCESS + != nr_php_error_record_exception( + NRPRG(txn), exception, nr_php_error_get_priority(E_ERROR), true, + "Uncaught exception ", &NRPRG(exception_filters))) { + nrl_verbosedebug(NRL_FRAMEWORK, "%s: unable to record exception", __func__); + } - nr_txn_set_path("CakePHP", NRPRG(txn), name, NR_PATH_TYPE_ACTION, - NR_NOT_OK_TO_OVERWRITE); + if (NULL != request_uri) { + nr_txn_set_path("CakePHP Exception", NRPRG(txn), request_uri, NR_PATH_TYPE_URI, + NR_OK_TO_OVERWRITE); + } else { + nrl_verbosedebug(NRL_FRAMEWORK, "%s: request uri is NULL", __func__); + } - NR_PHP_WRAPPER_CALL; +end: + nr_php_arg_release(&exception); + nr_free(request_uri); } NR_PHP_WRAPPER_END /* - * Enable CakePHP 1.2, 1.3 - */ -void nr_cakephp_enable_1(TSRMLS_D) { - nr_php_wrap_user_function(NR_PSTR("Component::initialize"), - nr_cakephp_name_the_wt_pre20 TSRMLS_CC); - nr_php_wrap_user_function(NR_PSTR("Dispatcher::cakeError"), - nr_cakephp_problem_1 TSRMLS_CC); -} - -/* - * Enable CakePHP 2.0+ + * Enable CakePHP 4.0+ */ -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 +void nr_cakephp_enable(TSRMLS_D) { + nr_php_wrap_user_function( + NR_PSTR("Cake\\Controller\\Controller::invokeAction"), + nr_cakephp_name_the_wt_4); + nr_php_wrap_user_function( + NR_PSTR( + "Cake\\Error\\Middleware\\ErrorHandlerMiddleware::handleException"), + nr_cakephp_error_handler_wrapper); + nr_txn_suggest_package_supportability_metric(NRPRG(txn), PHP_PACKAGE_NAME, + PHP_PACKAGE_VERSION_UNKNOWN); } diff --git a/agent/fw_drupal.c b/agent/fw_drupal.c index 67efc1dab..3537f8b42 100644 --- a/agent/fw_drupal.c +++ b/agent/fw_drupal.c @@ -20,6 +20,8 @@ #include "util_memory.h" #include "util_strings.h" +#define PHP_PACKAGE_NAME "drupal/drupal" + /* * Set the Web Transaction (WT) name to "(cached page)" * @@ -265,7 +267,9 @@ NR_PHP_WRAPPER(nr_drupal_http_request_before) { * 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; + if (NULL != NRPRG(drupal_http_request_segment)) { + NRPRG(drupal_http_request_segment)->wraprec = auto_segment->wraprec; + } } } NR_PHP_WRAPPER_END @@ -877,4 +881,12 @@ void nr_drupal_enable(TSRMLS_D) { nr_php_user_function_add_declared_callback( NR_PSTR("drupal_http_request"), nr_drupal_replace_http_request TSRMLS_CC); #endif + + if (NRINI(vulnerability_management_package_detection_enabled)) { + nr_txn_add_php_package(NRPRG(txn), PHP_PACKAGE_NAME, + PHP_PACKAGE_VERSION_UNKNOWN); + } + + nr_txn_suggest_package_supportability_metric(NRPRG(txn), PHP_PACKAGE_NAME, + PHP_PACKAGE_VERSION_UNKNOWN); } diff --git a/agent/fw_drupal8.c b/agent/fw_drupal8.c index 6593e17a8..32976c64a 100644 --- a/agent/fw_drupal8.c +++ b/agent/fw_drupal8.c @@ -9,6 +9,7 @@ #include "php_user_instrument.h" #include "php_execute.h" #include "php_wrapper.h" +#include "php_error.h" #include "fw_drupal_common.h" #include "fw_hooks.h" #include "fw_support.h" @@ -20,6 +21,62 @@ #define PHP_PACKAGE_NAME "drupal/core" +NR_PHP_WRAPPER(nr_drupal_exception) { + int priority = nr_php_error_get_priority(E_ERROR); + zval* event = NULL; + zval* exception = NULL; + + /* Warning avoidance */ + (void)wraprec; + + NR_PHP_WRAPPER_REQUIRE_FRAMEWORK(NR_FW_DRUPAL8); + + if (NR_SUCCESS != nr_txn_record_error_worthy(NRPRG(txn), priority)) { + NR_PHP_WRAPPER_CALL; + goto end; + } + + /* Get the event that was given. */ + event = nr_php_arg_get(1, NR_EXECUTE_ORIG_ARGS); + + /* Call the original function. */ + NR_PHP_WRAPPER_CALL; + + if (0 == nr_php_is_zval_valid_object(event)) { + nrl_verbosedebug(NRL_TXN, + "Drupal: ExceptionSubscriber::onException() does not " + "have an `event` parameter"); + goto end; + } + + /* + * Get the exception from the event. + */ + exception = nr_php_call(event, "getThrowable"); + if (!nr_php_is_zval_valid_object(exception)) { + // be abundantly cautious: free exception before attempting to re-assign + nr_php_zval_free(&exception); + exception = nr_php_call(event, "getException"); + } + + if (!nr_php_is_zval_valid_object(exception)) { + nrl_verbosedebug(NRL_TXN, "Drupal: getException() returned a non-object"); + goto end; + } + + if (NR_SUCCESS + != nr_php_error_record_exception(NRPRG(txn), exception, priority, true, + NULL, + &NRPRG(exception_filters))) { + nrl_verbosedebug(NRL_TXN, "Drupal: unable to record exception"); + } + +end: + nr_php_arg_release(&event); + nr_php_zval_free(&exception); +} +NR_PHP_WRAPPER_END + /* * Purpose : Convenience function to handle adding a callback to a method, * given a class entry and a method name. This will check the @@ -689,8 +746,6 @@ void nr_drupal_version() { if (NRINI(vulnerability_management_package_detection_enabled)) { nr_txn_add_php_package(NRPRG(txn), PHP_PACKAGE_NAME, version); } - nr_fw_support_add_package_supportability_metric(NRPRG(txn), PHP_PACKAGE_NAME, - version); } nr_php_zval_free(&zval_version); @@ -732,6 +787,26 @@ void nr_drupal8_enable(TSRMLS_D) { "er::getControllerFromDefinition"), nr_drupal8_name_the_wt TSRMLS_CC); + /* + * ExceptionSubscribers handle Drupal errors and exceptions before + * the agent has the opportunity to capture them. Instrument several + * of these ExceptionSubscriber function `onException` methods in order + * to capture Exceptions and Errors in Drupal 9.x+ + */ + // clang-format off + /* + * Log exceptions without further handling. + */ + nr_php_wrap_user_function(NR_PSTR("Drupal\\Core\\EventSubscriber\\ExceptionLoggingSubscriber::onException"), + nr_drupal_exception); + + /* + * Last-chance handler for exceptions: the final exception subscriber. + */ + nr_php_wrap_user_function(NR_PSTR("Drupal\\Core\\EventSubscriber\\FinalExceptionSubscriber::onException"), + nr_drupal_exception); + // clang-format on + /* * The drupal_modules config setting controls instrumentation of modules, * hooks, and views. @@ -762,4 +837,7 @@ void nr_drupal8_enable(TSRMLS_D) { nr_txn_add_php_package(NRPRG(txn), PHP_PACKAGE_NAME, PHP_PACKAGE_VERSION_UNKNOWN); } + + nr_txn_suggest_package_supportability_metric(NRPRG(txn), PHP_PACKAGE_NAME, + PHP_PACKAGE_VERSION_UNKNOWN); } diff --git a/agent/fw_hooks.h b/agent/fw_hooks.h index baaf400c7..93665862c 100644 --- a/agent/fw_hooks.h +++ b/agent/fw_hooks.h @@ -12,13 +12,7 @@ */ #include "php_execute.h" -extern void nr_cakephp_enable_1(TSRMLS_D); -extern void nr_cakephp_enable_2(TSRMLS_D); -extern nr_framework_classification_t nr_cakephp_special_1( - const char* filename TSRMLS_DC); -extern nr_framework_classification_t nr_cakephp_special_2( - const char* filename TSRMLS_DC); - +extern void nr_cakephp_enable(TSRMLS_D); extern void nr_codeigniter_enable(TSRMLS_D); extern int nr_drupal_is_framework(nrframework_t fw); @@ -45,16 +39,19 @@ extern void nr_zend_enable(TSRMLS_D); extern void nr_fw_zend2_enable(TSRMLS_D); /* Libraries. */ +extern void nr_aws_sdk_php_enable(); extern void nr_doctrine2_enable(TSRMLS_D); extern void nr_guzzle3_enable(TSRMLS_D); extern void nr_guzzle4_enable(TSRMLS_D); extern void nr_guzzle6_enable(TSRMLS_D); extern void nr_laminas_http_enable(TSRMLS_D); extern void nr_mongodb_enable(TSRMLS_D); +extern void nr_php_amqplib_enable(); extern void nr_phpunit_enable(TSRMLS_D); extern void nr_predis_enable(TSRMLS_D); extern void nr_zend_http_enable(TSRMLS_D); extern void nr_monolog_enable(TSRMLS_D); +extern void nr_composer_handle_autoload(const char* filename); /* Vulnerability Management Packages */ extern void nr_drupal_version(void); diff --git a/agent/fw_laminas3.c b/agent/fw_laminas3.c index 604976bf0..a5d27de94 100644 --- a/agent/fw_laminas3.c +++ b/agent/fw_laminas3.c @@ -13,6 +13,8 @@ #include "util_logging.h" #include "util_memory.h" +#define PHP_PACKAGE_NAME "laminas/laminas-mvc" + /* * Laminas is a rebranding of Zend, but the logic remains the same, * it is simply a name change and corresponds directly to Zend 3.x. @@ -163,7 +165,10 @@ void nr_laminas3_enable(TSRMLS_D) { nr_laminas3_name_the_wt TSRMLS_CC); if (NRINI(vulnerability_management_package_detection_enabled)) { - nr_txn_add_php_package(NRPRG(txn), "laminas/laminas-mvc", + nr_txn_add_php_package(NRPRG(txn), PHP_PACKAGE_NAME, PHP_PACKAGE_VERSION_UNKNOWN); } + + nr_txn_suggest_package_supportability_metric(NRPRG(txn), PHP_PACKAGE_NAME, + PHP_PACKAGE_VERSION_UNKNOWN); } diff --git a/agent/fw_laravel.c b/agent/fw_laravel.c index 11718a7e6..bbac65b20 100644 --- a/agent/fw_laravel.c +++ b/agent/fw_laravel.c @@ -963,8 +963,9 @@ NR_PHP_WRAPPER(nr_laravel_application_construct) { // Add php package to transaction nr_txn_add_php_package(NRPRG(txn), PHP_PACKAGE_NAME, version); } - nr_fw_support_add_package_supportability_metric(NRPRG(txn), PHP_PACKAGE_NAME, - version); + + nr_txn_suggest_package_supportability_metric(NRPRG(txn), PHP_PACKAGE_NAME, + version); if (version) { nrl_debug(NRL_FRAMEWORK, "Laravel version is " NRP_FMT, NRP_PHP(version)); diff --git a/agent/fw_laravel_queue.c b/agent/fw_laravel_queue.c index b4d5576e7..0ac042800 100644 --- a/agent/fw_laravel_queue.c +++ b/agent/fw_laravel_queue.c @@ -136,7 +136,6 @@ static void nr_laravel_queue_set_cat_txn(zval* job TSRMLS_DC) { } if (headers.dt_payload || headers.traceparent) { - nr_hashmap_t* header_map = nr_header_create_distributed_trace_map( headers.dt_payload, headers.traceparent, headers.tracestate); @@ -225,6 +224,11 @@ static char* nr_laravel_queue_job_txn_name(zval* job TSRMLS_DC) { name = nr_formatf("%s (%s:%s)", resolve_name, connection_name, queue_name); + nr_free(connection_name); + nr_free(resolve_name); + nr_free(queue_name); + + /* Caller is responsible for freeing name. */ return name; } @@ -252,6 +256,7 @@ NR_PHP_WRAPPER(nr_laravel_queue_syncqueue_raiseBeforeJobEvent_before) { job = nr_php_arg_get(1, NR_EXECUTE_ORIG_ARGS); + /* txn_name needs to be freed by the caller. */ txn_name = nr_laravel_queue_job_txn_name(job); /* @@ -271,6 +276,7 @@ NR_PHP_WRAPPER(nr_laravel_queue_syncqueue_raiseBeforeJobEvent_before) { NR_OK_TO_OVERWRITE); } nr_php_arg_release(&job); + nr_free(txn_name); NR_PHP_WRAPPER_CALL; } NR_PHP_WRAPPER_END @@ -317,6 +323,7 @@ NR_PHP_WRAPPER(nr_laravel_queue_worker_raiseBeforeJobEvent_after) { nr_txn_set_path("Laravel", NRPRG(txn), txn_name, NR_PATH_TYPE_CUSTOM, NR_OK_TO_OVERWRITE); } + nr_free(txn_name); nr_php_arg_release(&job); NR_PHP_WRAPPER_CALL; } @@ -574,7 +581,7 @@ NR_PHP_WRAPPER(nr_laravel_queue_worker_process) { * as the first parameter. */ char* connection_name = NULL; - char* job_name; + char* job_name = NULL; char* txn_name = NULL; connection = nr_php_arg_get(1, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); diff --git a/agent/fw_lumen.c b/agent/fw_lumen.c index 239578ae1..2d34551f0 100644 --- a/agent/fw_lumen.c +++ b/agent/fw_lumen.c @@ -11,10 +11,13 @@ #include "php_wrapper.h" #include "php_hash.h" #include "fw_hooks.h" +#include "fw_support.h" #include "util_logging.h" #include "util_memory.h" #include "util_strings.h" +#define PHP_PACKAGE_NAME "laravel/lumen-framework" + /* * Sets the web transaction name. If strip_base == true, * leading class path components will be stripped. @@ -232,7 +235,10 @@ void nr_lumen_enable(TSRMLS_D) { #endif if (NRINI(vulnerability_management_package_detection_enabled)) { - nr_txn_add_php_package(NRPRG(txn), "laravel/lumen-framework", + nr_txn_add_php_package(NRPRG(txn), PHP_PACKAGE_NAME, PHP_PACKAGE_VERSION_UNKNOWN); } + + nr_txn_suggest_package_supportability_metric(NRPRG(txn), PHP_PACKAGE_NAME, + PHP_PACKAGE_VERSION_UNKNOWN); } diff --git a/agent/fw_slim.c b/agent/fw_slim.c index 493e325a8..facaeecc1 100644 --- a/agent/fw_slim.c +++ b/agent/fw_slim.c @@ -81,28 +81,65 @@ NR_PHP_WRAPPER(nr_slim2_route_dispatch) { NR_PHP_WRAPPER_END /* - * Wrap the \Slim\Route::run method, which is the happy path for Slim routing. + * Wrap the Slim 3\Slim\Route::run method + * and + * Slim 4 Slim\\Routing\\Route::run + * which are the happy paths for Slim 3/4 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. + * 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 last wrapped + * function call of this type gets to name the txn; therefore needs a before + * call for OAPI. */ NR_PHP_WRAPPER(nr_slim3_4_route_run) { zval* this_var = NULL; char* txn_name = NULL; (void)wraprec; + NR_PHP_WRAPPER_REQUIRE_FRAMEWORK(NR_FW_SLIM); this_var = nr_php_scope_get(NR_EXECUTE_ORIG_ARGS TSRMLS_CC); txn_name = nr_slim_path_from_route(this_var TSRMLS_CC); nr_php_scope_release(&this_var); + if (txn_name) { + nr_txn_set_path("Slim", NRPRG(txn), txn_name, NR_PATH_TYPE_ACTION, + NR_OK_TO_OVERWRITE); + nr_free(txn_name); + } + NR_PHP_WRAPPER_CALL; +} +NR_PHP_WRAPPER_END + +/* + * public function dispatch(string $method, string $uri): RoutingResults + * This is fallback naming mechanism for Slim 4 routing when the Slim 4 + * Slim\\Routing\\Route::run does not run due middlware intervening on + * certain errors. + * 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 first wrapped + * function in func_begin. + */ +NR_PHP_WRAPPER(nr_slim4_route_dispatch) { + char* txn_name = NULL; + zval* route_name = NULL; + + (void)wraprec; + + NR_PHP_WRAPPER_REQUIRE_FRAMEWORK(NR_FW_SLIM); + + /* Get the route name. The first arg is the method, 2nd arg is routename. */ + route_name = nr_php_arg_get(2, NR_EXECUTE_ORIG_ARGS); + + if (nr_php_is_zval_valid_string(route_name)) { + txn_name = nr_strndup(Z_STRVAL_P(route_name), Z_STRLEN_P(route_name)); + } + + nr_php_arg_release(&route_name); if (txn_name) { nr_txn_set_path("Slim", NRPRG(txn), txn_name, NR_PATH_TYPE_ACTION, @@ -120,14 +157,14 @@ NR_PHP_WRAPPER(nr_slim_application_construct) { (void)wraprec; version = nr_php_get_object_constant(this_var, "VERSION"); - + if (NRINI(vulnerability_management_package_detection_enabled)) { // Add php package to transaction nr_txn_add_php_package(NRPRG(txn), PHP_PACKAGE_NAME, version); } - nr_fw_support_add_package_supportability_metric(NRPRG(txn), PHP_PACKAGE_NAME, - version); + nr_txn_suggest_package_supportability_metric(NRPRG(txn), PHP_PACKAGE_NAME, + version); nr_free(version); nr_php_scope_release(&this_var); @@ -139,12 +176,34 @@ void nr_slim_enable(TSRMLS_D) { nr_php_wrap_user_function(NR_PSTR("Slim\\Route::dispatch"), nr_slim2_route_dispatch TSRMLS_CC); + +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + /* Slim 3 */ - nr_php_wrap_user_function(NR_PSTR("Slim\\Route::run"), - nr_slim3_4_route_run TSRMLS_CC); + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("Slim\\Route::run"), nr_slim3_4_route_run, NULL, NULL); + /* Slim 4 */ + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("Slim\\Routing\\Route::run"), nr_slim3_4_route_run, NULL, NULL); + + /* Slim 4 */ + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("Slim\\Routing\\Dispatcher::dispatch"), nr_slim4_route_dispatch, + NULL, NULL); +#else + /* Slim 4*/ nr_php_wrap_user_function(NR_PSTR("Slim\\Routing\\Route::run"), nr_slim3_4_route_run TSRMLS_CC); + /* Slim 4 */ + nr_php_wrap_user_function(NR_PSTR("Slim\\Routing\\Dispatcher::dispatch"), + nr_slim4_route_dispatch TSRMLS_CC); + + /* Slim 3 */ + nr_php_wrap_user_function(NR_PSTR("Slim\\Route::run"), + nr_slim3_4_route_run TSRMLS_CC); +#endif /* Slim 2 does not have the same path as Slim 3/4 which is why we need to separate these*/ diff --git a/agent/fw_support.c b/agent/fw_support.c index daff2692c..23c9c44c1 100644 --- a/agent/fw_support.c +++ b/agent/fw_support.c @@ -58,23 +58,39 @@ void nr_fw_support_add_logging_supportability_metric(nrtxn_t* txn, void nr_fw_support_add_package_supportability_metric( nrtxn_t* txn, const char* package_name, - const char* package_version) { - if (NULL == txn || NULL == package_name || NULL == package_version) { + const char* package_version, + nr_php_package_t* p) { + if (NULL == txn || NULL == package_name) { return; } char* metname = NULL; char major_version[MAJOR_VERSION_LENGTH] = {0}; + const char* version = package_version; + + // override provided package_version only if: + // - php_package is provided + // - its version is not NULL + // - its version is not PHP_PACKAGE_VERSION_UNKNOWN + if (NULL != p && NULL != p->package_version + && 0 != nr_strcmp(p->package_version, PHP_PACKAGE_VERSION_UNKNOWN)) { + version = p->package_version; + } + + // only generate metric if version is known + if (NULL == version || 0 == nr_strcmp(version, PHP_PACKAGE_VERSION_UNKNOWN)) { + return; + } /* The below for loop checks if the major version of the package is more than * one digit and keeps looping until a '.' is encountered or one of the * conditions is met. */ - for (int i = 0; package_version[i] && i < MAJOR_VERSION_LENGTH - 1; i++) { - if ('.' == package_version[i]) { + for (int i = 0; version[i] && i < MAJOR_VERSION_LENGTH - 1; i++) { + if ('.' == version[i]) { break; } - major_version[i] = package_version[i]; + major_version[i] = version[i]; } if (NR_FW_UNSET == NRINI(force_framework)) { diff --git a/agent/fw_support.h b/agent/fw_support.h index 5099a7d35..ad4b02722 100644 --- a/agent/fw_support.h +++ b/agent/fw_support.h @@ -8,6 +8,7 @@ #define FW_SUPPORT_HDR #include "php_user_instrument.h" +#include "nr_php_packages.h" extern void nr_php_framework_add_supportability_metric( const char* framework_name, @@ -44,11 +45,13 @@ extern void nr_fw_support_add_logging_supportability_metric( * Params : 1. Transaction object * 2. Package name * 3. Package version + * 4. PHP package reported for vulnerability management * */ extern void nr_fw_support_add_package_supportability_metric( nrtxn_t* txn, const char* package_name, - const char* package_version); + const char* package_version, + nr_php_package_t* p); #endif /* FW_SUPPORT_HDR */ diff --git a/agent/fw_symfony4.c b/agent/fw_symfony4.c index 6e1c9982d..8592186ac 100644 --- a/agent/fw_symfony4.c +++ b/agent/fw_symfony4.c @@ -10,6 +10,8 @@ #include "fw_support.h" #include "fw_symfony_common.h" +#define PHP_PACKAGE_NAME "symfony/http-kernel" + NR_PHP_WRAPPER(nr_symfony4_exception) { int priority = nr_php_error_get_priority(E_ERROR); zval* event = NULL; @@ -277,7 +279,10 @@ void nr_symfony4_enable(TSRMLS_D) { #endif if (NRINI(vulnerability_management_package_detection_enabled)) { - nr_txn_add_php_package(NRPRG(txn), "symfony/http-kernel", + nr_txn_add_php_package(NRPRG(txn), PHP_PACKAGE_NAME, PHP_PACKAGE_VERSION_UNKNOWN); } + + nr_txn_suggest_package_supportability_metric(NRPRG(txn), PHP_PACKAGE_NAME, + PHP_PACKAGE_VERSION_UNKNOWN); } diff --git a/agent/fw_wordpress.c b/agent/fw_wordpress.c index 55a0be5f0..050750f6a 100644 --- a/agent/fw_wordpress.c +++ b/agent/fw_wordpress.c @@ -804,8 +804,7 @@ void nr_wordpress_version() { "})();"; zval retval; - int result - = zend_eval_string(func_string, &retval, "Get Wordpress Version"); + int result = zend_eval_string(func_string, &retval, "Get Wordpress Version"); // Add php package to transaction if (SUCCESS == result) { if (nr_php_is_zval_valid_string(&retval)) { @@ -813,8 +812,9 @@ void nr_wordpress_version() { if (NRINI(vulnerability_management_package_detection_enabled)) { nr_txn_add_php_package(NRPRG(txn), PHP_PACKAGE_NAME, version); } - nr_fw_support_add_package_supportability_metric(NRPRG(txn), PHP_PACKAGE_NAME, - version); + + nr_txn_suggest_package_supportability_metric(NRPRG(txn), PHP_PACKAGE_NAME, + version); } zval_dtor(&retval); } diff --git a/agent/fw_yii.c b/agent/fw_yii.c index 74aa19335..0b1af7c96 100644 --- a/agent/fw_yii.c +++ b/agent/fw_yii.c @@ -14,6 +14,7 @@ #include "util_memory.h" #include "util_strings.h" +#define PHP_PACKAGE_NAME "yiisoft/yii2" /* * Yii1: Set the web transaction name from the controllerId + actionId combo. * @@ -221,4 +222,12 @@ void nr_yii2_enable(TSRMLS_D) { nr_php_wrap_user_function(NR_PSTR("yii\\base\\ErrorHandler::logException"), nr_yii2_error_handler_wrapper TSRMLS_CC); #endif + + if (NRINI(vulnerability_management_package_detection_enabled)) { + nr_txn_add_php_package(NRPRG(txn), PHP_PACKAGE_NAME, + PHP_PACKAGE_VERSION_UNKNOWN); + } + + nr_txn_suggest_package_supportability_metric(NRPRG(txn), PHP_PACKAGE_NAME, + PHP_PACKAGE_VERSION_UNKNOWN); } diff --git a/agent/lib_aws_sdk_php.c b/agent/lib_aws_sdk_php.c new file mode 100644 index 000000000..46a71ce3c --- /dev/null +++ b/agent/lib_aws_sdk_php.c @@ -0,0 +1,570 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* + * Functions relating to instrumenting the AWS-SDK-PHP. + * https://github.com/aws/aws-sdk-php + */ +#include "php_agent.h" +#include "php_call.h" +#include "php_hash.h" +#include "php_wrapper.h" +#include "fw_hooks.h" +#include "fw_support.h" +#include "util_logging.h" +#include "nr_segment_message.h" +#include "lib_aws_sdk_php.h" + +#define PHP_PACKAGE_NAME "aws/aws-sdk-php" + +#if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO /* PHP8.1+ */ +/* Service instrumentation only supported above PHP 8.1+*/ + +/* +* Note: For SQS, the command_arg_array will contain the following arrays seen +below: +//clang-format off +$result = $client->receiveMessage(array( + // QueueUrl is required + 'QueueUrl' => 'string', + 'AttributeNames' => array('string', ... ), + 'MessageAttributeNames' => array('string', ... ), + 'MaxNumberOfMessages' => integer, + 'VisibilityTimeout' => integer, + 'WaitTimeSeconds' => integer, +)); + +$result = $client->sendMessage(array( + // QueueUrl is required + 'QueueUrl' => 'string', + // MessageBody is required + 'MessageBody' => 'string', + 'DelaySeconds' => integer, + 'MessageAttributes' => array( + // Associative array of custom 'String' key names + 'String' => array( + 'StringValue' => 'string', + 'BinaryValue' => 'string', + 'StringListValues' => array('string', ... ), + 'BinaryListValues' => array('string', ... ), + // DataType is required + 'DataType' => 'string', + ), + // ... repeated + ), +)); + +$result = $client->sendMessageBatch(array( + // QueueUrl is required + 'QueueUrl' => 'string', + // Entries is required + 'Entries' => array( + array( + // Id is required + 'Id' => 'string', + // MessageBody is required + 'MessageBody' => 'string', + 'DelaySeconds' => integer, + 'MessageAttributes' => array( + // Associative array of custom 'String' key names + 'String' => array( + 'StringValue' => 'string', + 'BinaryValue' => 'string', + 'StringListValues' => array('string', ... ), + 'BinaryListValues' => array('string', ... ), + // DataType is required + 'DataType' => 'string', + ), + // ... repeated + ), + ), + // ... repeated + ), +)); + +//clang-format on +*/ +void nr_lib_aws_sdk_php_sqs_handle(nr_segment_t* auto_segment, + char* command_name_string, + size_t command_name_len, + NR_EXECUTE_PROTO) { + char* command_arg_value = NULL; + nr_segment_t* message_segment = NULL; + + nr_segment_message_params_t message_params = { + .library = SQS_LIBRARY_NAME, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_QUEUE, + .messaging_system = AWS_SQS_MESSAGING_SERVICE, + }; + nr_segment_cloud_attrs_t cloud_attrs = {0}; + + if (NULL == auto_segment) { + return; + } + + if (NULL == command_name_string || 0 == command_name_len) { + return; + } + +#define AWS_COMMAND_IS(CMD) \ + (command_name_len == (sizeof(CMD) - 1) && nr_streq(CMD, command_name_string)) + + /* Determine if we instrument this command. */ + if (AWS_COMMAND_IS("sendMessageBatch")) { + message_params.message_action = NR_SPANKIND_PRODUCER; + } else if (AWS_COMMAND_IS("sendMessage")) { + message_params.message_action = NR_SPANKIND_PRODUCER; + } else if (AWS_COMMAND_IS("receiveMessage")) { + message_params.message_action = NR_SPANKIND_CONSUMER; + } else { + /* Nothing to do here so exit. */ + return; + } +#undef AWS_COMMAND_IS + + /* + * By this point, it's been determined that this call will be instrumented so + * only create the segment now, grab the parent segment start time, add our + * special segment attributes/metrics then close the newly created segment. + */ + message_segment = nr_segment_start(NRPRG(txn), NULL, NULL); + if (NULL == message_segment) { + return; + } + /* re-use start time from auto_segment started in func_begin */ + message_segment->start_time = auto_segment->start_time; + cloud_attrs.aws_operation = command_name_string; + + command_arg_value = nr_lib_aws_sdk_php_get_command_arg_value( + AWS_SDK_PHP_SQSCLIENT_QUEUEURL_ARG, NR_EXECUTE_ORIG_ARGS); + + /* + * nr_lib_aws_sdk_php_sqs_parse_queueurl requires a modifiable string to + * populate message_params and cloud_attrs. + */ + nr_lib_aws_sdk_php_sqs_parse_queueurl(command_arg_value, &message_params, + &cloud_attrs); + + /* Add cloud attributes, if available. */ + + nr_segment_traces_add_cloud_attributes(message_segment, &cloud_attrs); + + /* Now end the instrumented segment as a message segment. */ + nr_segment_message_end(&message_segment, &message_params); + + nr_free(command_arg_value); +} + +void nr_lib_aws_sdk_php_sqs_parse_queueurl( + char* sqs_queueurl, + nr_segment_message_params_t* message_params, + nr_segment_cloud_attrs_t* cloud_attrs) { + char* region = NULL; + char* queue_name = NULL; + char* account_id = NULL; + char* queueurl_pointer = NULL; + + if (NULL == sqs_queueurl || NULL == message_params || NULL == cloud_attrs) { + return; + } + + /* + * AWS QueueUrl has a very specific format. + * The QueueUrl we are looking for will be of the following format: + * queueUrl = + * 'https://sqs.REGION_NAME.amazonaws.com/ACCOUNT_ID_NAME/SQS_QUEUE_NAME' + * where REGION_NAME, ACCOUNT_ID_NAME, and SQS_QUEUE_NAME are the acutal + * values such as: queueUrl = + * 'https://sqs.us-east-2.amazonaws.com/123456789012/my_amazing_queue' + * If we are unable to match any part of this, the whole decode is suspect and + * all values are discarded. + * + * Due to the overhead involved in escaping the original buffer, creating a + * regex, matching a regex, destroying a regex, this method was chosen as a + * more performant option because it's a very limited pattern. + */ + queueurl_pointer = sqs_queueurl; + + /* + * Find the pattern of the AWS queueurl that should immediately precede the + * region. + */ + if (0 + != strncmp(queueurl_pointer, AWS_QUEUEURL_PREFIX, + AWS_QUEUEURL_PREFIX_LEN)) { + /* Malformed queueurl, we can't decode this. */ + return; + } + + /* + * Find the start of the region. It follows the 12 chars of 'https://sqs.' + * and continues until the next '.' It is safe to move the pointer along at + * this point since we just verified the prefix exists. + */ + queueurl_pointer += AWS_QUEUEURL_PREFIX_LEN; + if (nr_strempty(queueurl_pointer)) { + /* Malformed queueurl, we can't decode this. */ + return; + } + + region = queueurl_pointer; + + /* Find the end of the region. */ + queueurl_pointer = nr_strchr(queueurl_pointer, '.'); + if (NULL == queueurl_pointer) { + /* Malformed queueurl, we can't decode this. */ + return; + } + *queueurl_pointer = '\0'; + + /* + * Move the pointer along. Again, we found a valid '.' so moving the pointer + * beyond that point should be safe and give us either more string or the end + * of the string. + */ + queueurl_pointer += 1; + if (nr_strempty(queueurl_pointer)) { + /* Malformed queueurl, we can't decode this. */ + return; + } + + /* Move past the next pattern to find the start of the account id. */ + if (0 + != strncmp(queueurl_pointer, AWS_QUEUEURL_AWS_POSTFIX, + AWS_QUEUEURL_AWS_POSTFIX_LEN)) { + /* Malformed queueurl, we can't decode this. */ + return; + } + + /* + * Move the pointer along. Since we found a valid pattern match moving the + * pointer beyond that point should be safe and give us either more string or + * the end of the string. + */ + queueurl_pointer += AWS_QUEUEURL_AWS_POSTFIX_LEN; + if (nr_strempty(queueurl_pointer)) { + /* Malformed queueurl, we can't decode this. */ + return; + } + + /* If it's not an empty string, we've found the start of the account_id*/ + account_id = queueurl_pointer; + + /* Find the end of account id which goes until the next forward slash. */ + queueurl_pointer = nr_strchr(queueurl_pointer, '/'); + if (NULL == queueurl_pointer) { + /* Malformed queueurl, we can't decode this. */ + return; + } + *queueurl_pointer = '\0'; + + /* Move the pointer along. */ + queueurl_pointer += 1; + if (nr_strempty(queueurl_pointer)) { + /* Malformed queueurl, we can't decode this. */ + return; + } + + /* This should be the start of the start of the queuename.*/ + queue_name = queueurl_pointer; + + /* + * Almost done. At this point, the string should only have queue name left. + * Let's check if there's another slash, if it isn't followed by empty string, + * the queueurl is malformed. + */ + queueurl_pointer = nr_strchr(queueurl_pointer, '/'); + if (NULL != queueurl_pointer) { + *queueurl_pointer = '\0'; + /* Let's check if it's followed by empty string */ + *queueurl_pointer += 1; + if (!nr_strempty(queueurl_pointer)) { + /* Malformed queueurl, we can't decode this. */ + return; + } + } + + /* + * SQS entity relationship requires: messaging.system, cloud.region, + * cloud.account.id, messaging.destination.name + */ + message_params->destination_name = queue_name; + cloud_attrs->cloud_account_id = account_id; + cloud_attrs->cloud_region = region; +} + +char* nr_lib_aws_sdk_php_get_command_arg_value(char* command_arg_name, + NR_EXECUTE_PROTO) { + zval* param_array = NULL; + zval* command_arg_array = NULL; + char* command_arg_value = NULL; + + if (NULL == command_arg_name) { + return NULL; + } + /* To extract the Aws/AwsClient::__call $argument, we get the second arg. */ + param_array = nr_php_arg_get(2, NR_EXECUTE_ORIG_ARGS); + + if (nr_php_is_zval_valid_array(param_array)) { + /* The first element in param_array is an array of parameters. */ + command_arg_array = nr_php_zend_hash_index_find(Z_ARRVAL_P(param_array), 0); + if (nr_php_is_zval_valid_array(command_arg_array)) { + zval* queueurl_arg = nr_php_zend_hash_find(Z_ARRVAL_P(command_arg_array), + command_arg_name); + + if (nr_php_is_zval_non_empty_string(queueurl_arg)) { + command_arg_value = nr_strdup(Z_STRVAL_P(queueurl_arg)); + } + } + } + + nr_php_arg_release(¶m_array); + return command_arg_value; +} + +/* + * For Aws/AwsClient::__call see + * https://github.com/aws/aws-sdk-php/blob/master/src/AwsClientInterface.php + * ALL + * client commands are handled by this function, so it is the start and end of + * any command. Creates and executes a command for an operation by name. + * When a class command isn't explicitly created as a function, the __call class + * handles the invocation. This means all AWS Client Service commands are + * handled by this call. Any invocation starts when this function starts, and + * ends when it ends. This function decodes the command name, determines the + * appropriate args, decodes the args, generates a guzzle request to send to the + * AWS Service, gets the guzzle response from the AWS Service, and bundles that + * response into an AswResult to return. + * + * @param string $name Name of the command to execute. + * @param array $arguments Arguments to pass to the getCommand method. + * + * @return ResultInterface + * @throws \Exception + */ + +NR_PHP_WRAPPER(nr_aws_client_call) { + (void)wraprec; + + zval* command_name = NULL; + const char* klass = NULL; + char* command_name_string = NULL; + char* real_class_and_command = NULL; + nr_segment_t* segment = NULL; + zend_class_entry* class_entry = NULL; + int klass_len = 0; + + class_entry = Z_OBJCE_P(nr_php_execute_scope(execute_data)); + if (NULL == class_entry) { + goto end; + } + + klass = nr_php_class_entry_name(class_entry); + + if (NULL == klass) { + goto end; + } + /* Get the arg command_name. */ + command_name = nr_php_arg_get(1, NR_EXECUTE_ORIG_ARGS); + + if (!nr_php_is_zval_non_empty_string(command_name)) { + goto end; + } + command_name_string = Z_STRVAL_P(command_name); + klass_len = nr_php_class_entry_name_length(class_entry); + +#define AWS_CLASS_IS(KLASS, SHORT_KLASS) \ + (klass_len == (sizeof(KLASS) - 1) \ + && nr_striendswith(klass, klass_len, SHORT_KLASS, sizeof(SHORT_KLASS) - 1)) + + if (AWS_CLASS_IS("Aws\\Sqs\\SqsClient", "SqsClient")) { + nr_lib_aws_sdk_php_sqs_handle(auto_segment, command_name_string, + Z_STRLEN_P(command_name), + NR_EXECUTE_ORIG_ARGS); + } + +#undef AWS_CLASS_IS + + /* + * Since we have klass and command_name, we can give the calling segment + * a more meaningful name than Aws/AwsClient::__call We can decode it to + * Aws/CALLING_CLASS_NAME/CALLING_CLASS_CLIENT::CALLING_CLASS_COMMAND + * + * EX: Aws\\Sqs\\SqsClient::sendMessage + */ + + if (NULL != auto_segment) { + real_class_and_command + = nr_formatf("Custom/%s::%s", klass, command_name_string); + nr_segment_set_name(auto_segment, real_class_and_command); + nr_free(real_class_and_command); + } + +end: + /* Release the command_name. */ + nr_php_arg_release(&command_name); +} +NR_PHP_WRAPPER_END + +#endif /* PHP >= 8.1*/ +/* + * In a normal course of events, the following line will always work + * zend_eval_string("Aws\\Sdk::VERSION;", &retval, "Get AWS Version") + * By the time we have detected the existence of the aws-sdk-php and with + * default composer project settings, it is callable even from + * nr_aws_sdk_php_enable which will automatically load the class if it isn't + * loaded yet and then evaluate the string. In the rare case that files + * are not loaded via autoloader and/or have non-default composer classload + * settings, if the class is not found, PHP 8.2+ will generate an + * error whenever it cannot find a class which must be caught. Calling this + * from nr_aws_sdk_php_enable would allow the sdk version value to be set only + * once. To avoid the VERY unlikely but not impossible fatal error, we need to + * wrap the call in a try/catch block and make it a lambda so that we avoid + * fatal errors. + */ +void nr_lib_aws_sdk_php_handle_version() { + char* version = NULL; + zval retval; + int result = FAILURE; + + /* + * The following block initializes nr_aws_sdk_version to the empty string. + * If it is able to extract the version, nr_aws_sdk_version is set to that. + * Nothing is needed in the catch block. + * The final return will either return a proper version or an empty string. + */ + result = zend_eval_string( + "(function() {" + " $nr_aws_sdk_version = '';" + " try {" + " $nr_aws_sdk_version = Aws\\Sdk::VERSION;" + " } catch (Throwable $e) {" + " }" + " return $nr_aws_sdk_version;" + "})();", + &retval, "Get nr_aws_sdk_version"); + + /* See if we got a non-empty/non-null string for version. */ + if (SUCCESS == result) { + if (nr_php_is_zval_non_empty_string(&retval)) { + version = Z_STRVAL(retval); + } + } + + if (NRINI(vulnerability_management_package_detection_enabled)) { + /* Add php package to transaction */ + nr_txn_add_php_package(NRPRG(txn), PHP_PACKAGE_NAME, version); + } + + nr_txn_suggest_package_supportability_metric(NRPRG(txn), PHP_PACKAGE_NAME, + version); + + zval_dtor(&retval); +} + +void nr_lib_aws_sdk_php_add_supportability_service_metric( + const char* service_name) { + /* total MAX metric name length per agent-specs */ + char buf[MAX_METRIC_NAME_LEN]; + char* cp = NULL; + + if (nr_strempty(service_name)) { + return; + } + if (NULL == NRPRG(txn)) { + return; + } + + cp = buf; + nr_strcpy(cp, PHP_AWS_SDK_SERVICE_NAME_METRIC_PREFIX); + cp += PHP_AWS_SDK_SERVICE_NAME_METRIC_PREFIX_LEN - 1; + nr_strlcpy(cp, service_name, MAX_AWS_SERVICE_NAME_LEN); + nrm_force_add(NRPRG(txn) ? NRTXN(unscoped_metrics) : 0, buf, 0); +} + +/* + * AwsClient::parseClass + * This is called from the base AwsClient class for every client associated + * with a service during client initialization. + * parseClass already computes the service name for internal use, so we don't + * need to store it, we just need to snag it from the return value as it goes + * through the client initialization process. + */ +NR_PHP_WRAPPER(nr_create_aws_service_metric) { + (void)wraprec; + + zval** ret_val_ptr = NULL; + ret_val_ptr = NR_GET_RETURN_VALUE_PTR; + + NR_PHP_WRAPPER_CALL; + + if (NULL != ret_val_ptr && nr_php_is_zval_valid_array(*ret_val_ptr)) { + /* obtain ret_val_ptr[0] which contains the service name */ + zval* service_name + = nr_php_zend_hash_index_find(Z_ARRVAL_P(*ret_val_ptr), 0); + if (nr_php_is_zval_non_empty_string(service_name)) { + nr_lib_aws_sdk_php_add_supportability_service_metric( + Z_STRVAL_P(service_name)); + } + } +} +NR_PHP_WRAPPER_END + +/* + * The ideal file to begin immediate detection of the aws-sdk is: + * aws-sdk-php/src/functions.php + * Unfortunately, Php8.2+ and composer autoload leads to the + * file being optimized directly and not loaded. + * + * Options considered: + * + * 1. for PHP8.2, and only optimizable libraries, when encountering autoload.php + * files, ask the file what includes it added and check against only the + * optimizable library. Small overhead incurred when encountering an autoload + * file, but detects aws-sdk-php immediately before any sdk code executes + * (changes needed for this are detailed in the original PR) + * 2. use a file that gets called later and only when AwsClient.php file is + * called. It's called later and we'll miss some instrumentation, but if we're + * only ever going to be interested in Client calls anyway, maybe that's ok? + * Doesn't detect Sdk.php (optimized out) so when customers only use that or + * when they use it first, we will not instrument it. This only detects when a + * Client is called to use a service so potentially misses out on other + * instrumentation and misses out when customers use the aws-sdk-php but use + * non-SDK way to interact with the service (possibly with redis/memcached). + * This way is definitely the least complex and lowest overhead and less + * complexity means lower risk as well. + * 3. Directly add the wrappers to the hash map. With potentially 50ish clients + * to wrap, this will add overhead to every hash map lookup. Currently + * implemented option is 2, use the AwsClient.php as this is our main focus. + * This means until a call to an Aws/AwsClient function, + * all calls including aws\sdk calls are ignored. + * + * Version detection will be called directly from Aws\Sdk.php + */ +void nr_aws_sdk_php_enable() { + /* + * Set the UNKNOWN package first, so it doesn't overwrite what we find with + * nr_lib_aws_sdk_php_handle_version. + */ + if (NRINI(vulnerability_management_package_detection_enabled)) { + nr_txn_add_php_package(NRPRG(txn), PHP_PACKAGE_NAME, + PHP_PACKAGE_VERSION_UNKNOWN); + } + + /* Extract the version for aws-sdk 3+ */ + nr_lib_aws_sdk_php_handle_version(); + + /* Called when initializing all Clients */ + nr_php_wrap_user_function(NR_PSTR("Aws\\AwsClient::parseClass"), + nr_create_aws_service_metric); + +#if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO /* PHP8.1+ */ + /* We only support instrumentation above PHP 8.1 */ + /* Called when a service command is issued from a Client */ + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("Aws\\AwsClient::__call"), NULL, nr_aws_client_call, + nr_aws_client_call); +#endif +} diff --git a/agent/lib_aws_sdk_php.h b/agent/lib_aws_sdk_php.h new file mode 100644 index 000000000..6eb0c1e54 --- /dev/null +++ b/agent/lib_aws_sdk_php.h @@ -0,0 +1,96 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Functions relating to instrumenting AWS-SDK-PHP. + */ +#ifndef LIB_AWS_SDK_PHP_HDR +#define LIB_AWS_SDK_PHP_HDR + +#if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO /* PHP8.1+ */ +/* Service instrumentation only supported above PHP 8.1+*/ + +/* SQS */ +#define SQS_LIBRARY_NAME "SQS" +#define AWS_SQS_MESSAGING_SERVICE "aws_sqs" +#define AWS_SDK_PHP_SQSCLIENT_QUEUEURL_ARG "QueueUrl" +#define AWS_QUEUEURL_PREFIX "https://sqs." +#define AWS_QUEUEURL_PREFIX_LEN sizeof(AWS_QUEUEURL_PREFIX) - 1 +#define AWS_QUEUEURL_AWS_POSTFIX "amazonaws.com/" +#define AWS_QUEUEURL_AWS_POSTFIX_LEN sizeof(AWS_QUEUEURL_AWS_POSTFIX) - 1 + +#endif /* PHP 8.1+ */ + +#define PHP_AWS_SDK_SERVICE_NAME_METRIC_PREFIX \ + "Supportability/PHP/AWS/Services/" +#define MAX_METRIC_NAME_LEN 256 +#define PHP_AWS_SDK_SERVICE_NAME_METRIC_PREFIX_LEN \ + sizeof(PHP_AWS_SDK_SERVICE_NAME_METRIC_PREFIX) +#define MAX_AWS_SERVICE_NAME_LEN \ + (MAX_METRIC_NAME_LEN - PHP_AWS_SDK_SERVICE_NAME_METRIC_PREFIX_LEN) + +extern void nr_aws_sdk_php_enable(); +extern void nr_lib_aws_sdk_php_handle_version(); +extern void nr_lib_aws_sdk_php_add_supportability_service_metric( + const char* service_name); + +#if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO /* PHP8.1+ */ +/* Aside from service class and version detection, instrumentation is only + * supported with PHP 8.1+ */ + +/* + * Purpose : Parses the QueueUrl to extract cloud_region, cloud_account_id, and + * destination_name. The extraction sets all or none since the values are from + * the same string and if it is malformed, it cannot be used. + * + * Params : 1. The QueueUrl, MUST be a modifiable string + * 2. message_params to set message_params.destination_name + * 3. cloud_attrs to set message_params.cloud_region, + * message_params.cloud_account_id + * + * Returns : applicable cloud_attrs and message params fields will point to null + * terminated strings within the original string. + * + */ +extern void nr_lib_aws_sdk_php_sqs_parse_queueurl( + char* sqs_queueurl, + nr_segment_message_params_t* message_params, + nr_segment_cloud_attrs_t* cloud_attrs); + +/* + * Purpose : Handle when an SqsClient initiates a command + * + * Params : 1. segment : if we instrument the commandName, we'll need to end + * the segment as a message segment + * 2. command_name_string : the string of the command being called + * 3. command_name_len : the length of the command being called + * 4. NR_EXECUTE_ORIG_ARGS (execute_data, func_return_value) + * + * Returns : + * + */ +extern void nr_lib_aws_sdk_php_sqs_handle(nr_segment_t* segment, + char* command_name_string, + size_t command_name_len, + NR_EXECUTE_PROTO); + +/* + * Purpose : The second argument to the Aws/AwsClient::__call function should be + * an array, the first element of which is itself an array of arguments that + * were passed to the called function and are in name:value pairs. Given an + * argument name, this will return the value of the argument. + * + * Params : 1. arg_name: name of argument to extract from command arg array + * 2. NR_EXECUTE_PROTO (execute_data, func_return_value) + * + * Returns : the strduped value of the arg_name; NULL if does not exist + * + * Note: The caller is responsible for freeing the returned string value + * + */ +extern char* nr_lib_aws_sdk_php_get_command_arg_value(char* command_arg_name, + NR_EXECUTE_PROTO); + +#endif /* PHP8.1+ */ + +#endif /* LIB_AWS_SDK_PHP_HDR */ diff --git a/agent/lib_composer.c b/agent/lib_composer.c new file mode 100644 index 000000000..f6596639a --- /dev/null +++ b/agent/lib_composer.c @@ -0,0 +1,272 @@ +/* + * Copyright 2022 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "php_agent.h" +#include "fw_hooks.h" +#include "fw_support.h" +#include "nr_txn.h" +#include "util_logging.h" +#include "util_memory.h" +#include "util_syscalls.h" + +static bool nr_execute_handle_autoload_composer_is_initialized() { + zend_class_entry* zce = NULL; + + if (NULL == (zce = nr_php_find_class("composer\\installedversions"))) { + nrl_verbosedebug(NRL_INSTRUMENT, + "Composer\\InstalledVersions class not found"); + return false; + }; + + // the class is found - there's hope! + if (NULL == nr_php_find_class_method(zce, "getallrawdata") + || NULL == nr_php_find_class_method(zce, "getrootpackage")) { + nrl_verbosedebug( + NRL_INSTRUMENT, + "Composer\\InstalledVersions class found, but methods not found"); + return false; + } + + return true; +} + +static int nr_execute_handle_autoload_composer_init(const char* vendor_path) { + char* code = NULL; + zval retval; + int result = FAILURE; + + if (nr_execute_handle_autoload_composer_is_initialized()) { + nrl_verbosedebug(NRL_INSTRUMENT, "%s: already initialized", __func__); + return NR_SUCCESS; + } + + code = nr_formatf("include_once '%s/composer/InstalledVersions.php';", + vendor_path); + + result = zend_eval_string(code, &retval, "newrelic\\init_composer_api"); + if (result != SUCCESS) { + nrl_verbosedebug(NRL_INSTRUMENT, + "%s: zend_eval_string(%s) failed, result=%d", __func__, + code, result); + nr_free(code); + return NR_FAILURE; + } + + zval_dtor(&retval); + nr_free(code); + + // Make sure runtime API is available after loading + // Composer\\InstalledVersions class: + if (!nr_execute_handle_autoload_composer_is_initialized()) { + nrl_verbosedebug(NRL_INSTRUMENT, + "%s: unable to initialize Composer runtime API", __func__); + return NR_FAILURE; + } + + return NR_SUCCESS; +} + +static void nr_execute_handle_autoload_composer_get_packages_information( + const char* vendor_path) { + zval retval; // This is used as a return value for zend_eval_string. + // It will only be set if the result of the eval is SUCCESS. + int result = FAILURE; + + // nrunlikely because this should alredy be ensured by the caller + if (nrunlikely(!NRINI(vulnerability_management_package_detection_enabled))) { + // do nothing when collecting package information for vulnerability + // management is disabled + return; + } + + // nrunlikely because this should alredy be ensured by the caller + if (nrunlikely(!NRINI(vulnerability_management_composer_api_enabled))) { + // do nothing when use of composer to collect package info is disabled + return; + } + + // clang-format off + char* getallrawdata + = "" + "(function() {" + " try {" + " $root_package = \\Composer\\InstalledVersions::getRootPackage();" + " $packages = array();" + " foreach (\\Composer\\InstalledVersions::getAllRawData() as $installed) { " + " foreach ($installed['versions'] as $packageName => $packageData) {" + " if (!is_string($packageName)) {" + " continue;" + " }" + " if (is_array($root_package) && array_key_exists('name', $root_package) && $packageName == $root_package['name']) {" + " continue;" + " }" + " if (!array_key_exists('pretty_version', $packageData)) {" + " continue;" + " }" + " $pretty_version = $packageData['pretty_version'];" + " if (is_string($pretty_version)) {" + " $packages[$packageName] = ltrim($pretty_version, 'v');" + " }" + " }" + " }" + " return $packages;" + " } catch (Throwable $e) {" + " return NULL;" + " }" + "})();"; + // clang-format on + + if (NR_SUCCESS != nr_execute_handle_autoload_composer_init(vendor_path)) { + nrl_debug(NRL_INSTRUMENT, + "%s - unable to initialize Composer runtime API - package info " + "unavailable", + __func__); + return; + } + + nrl_verbosedebug(NRL_INSTRUMENT, "%s - Composer runtime API available", + __func__); + + result + = zend_eval_string(getallrawdata, &retval, "composer_getallrawdata.php"); + if (SUCCESS != result) { + nrl_verbosedebug(NRL_INSTRUMENT, "%s - composer_getallrawdata.php failed", + __func__); + return; + } + if (IS_ARRAY == Z_TYPE(retval)) { + zend_string* package_name = NULL; + zval* package_version = NULL; + ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL(retval), package_name, + package_version) { + if (NULL == package_name || NULL == package_version) { + continue; + } + if (nr_php_is_zval_non_empty_string(package_version)) { + nrl_verbosedebug(NRL_INSTRUMENT, "package %s, version %s", + NRSAFESTR(ZSTR_VAL(package_name)), + NRSAFESTR(Z_STRVAL_P(package_version))); + nr_txn_add_php_package_from_source(NRPRG(txn), ZSTR_VAL(package_name), + Z_STRVAL_P(package_version), + NR_PHP_PACKAGE_SOURCE_COMPOSER); + } + } + ZEND_HASH_FOREACH_END(); + } else { + char strbuf[80]; + nr_format_zval_for_debug(&retval, strbuf, 0, sizeof(strbuf) - 1, 0); + nrl_verbosedebug(NRL_INSTRUMENT, + "%s - installed packages is: " NRP_FMT ", not an array", + __func__, NRP_ARGSTR(strbuf)); + } + zval_dtor(&retval); +} + +static char* nr_execute_handle_autoload_composer_get_vendor_path( + const char* filename) { + char* vendor_path = NULL; // result of dirname(filename) + char* cp = NULL; + + // nrunlikely because this should alredy be ensured by the caller + if (nrunlikely(NULL == filename)) { + nrl_verbosedebug(NRL_FRAMEWORK, "%s - filename is NULL", __func__); + return NULL; + } + + // vendor_path = dirname(filename): + // 1. copy filename to vendor_path + vendor_path = nr_strdup(filename); + // 2. // find last occurence of '/' in vendor_path + cp = nr_strrchr(vendor_path, '/'); + // 3. replace '/' with '\0' to get the directory path + if (NULL != cp) { + *cp = '\0'; + } else { + nrl_verbosedebug(NRL_FRAMEWORK, "%s - no '/' in filename '%s'", __func__, + filename); + } + + return vendor_path; +} + +static bool nr_execute_handle_autoload_composer_file_exists( + const char* vendor_path, + const char* filename) { + char* composer_magic_file = NULL; // vendor_path + filename + bool file_exists = false; + + // nrunlikely because this should alredy be ensured by the caller + if (nrunlikely(NULL == vendor_path)) { + nrl_verbosedebug(NRL_FRAMEWORK, "%s - vendor_path is NULL", __func__); + return false; + } + + // nrunlikely because this should alredy be ensured by the caller + if (nrunlikely(NULL == filename)) { + nrl_verbosedebug(NRL_FRAMEWORK, "%s - filename is NULL", __func__); + return false; + } + + composer_magic_file = nr_formatf("%s/%s", vendor_path, filename); + if (0 == nr_access(composer_magic_file, F_OK | R_OK)) { + file_exists = true; + } + nr_free(composer_magic_file); + return file_exists; +} + +void nr_composer_handle_autoload(const char* filename) { +// Composer signature file" +#define COMPOSER_MAGIC_FILE_1 "composer/autoload_real.php" +#define COMPOSER_MAGIC_FILE_1_LEN (sizeof(COMPOSER_MAGIC_FILE_1) - 1) +// Composer runtime API files: +#define COMPOSER_MAGIC_FILE_2 "composer/InstalledVersions.php" +#define COMPOSER_MAGIC_FILE_2_LEN (sizeof(COMPOSER_MAGIC_FILE_2) - 1) +#define COMPOSER_MAGIC_FILE_3 "composer/installed.php" +#define COMPOSER_MAGIC_FILE_3_LEN (sizeof(COMPOSER_MAGIC_FILE_3) - 1) + char* vendor_path = NULL; // result of dirname(filename) + + // nrunlikely because this should alredy be ensured by the caller + if (nrunlikely(NULL == filename)) { + nrl_verbosedebug(NRL_FRAMEWORK, "%s - filename is NULL", __func__); + return; + } + + vendor_path = nr_execute_handle_autoload_composer_get_vendor_path(filename); + if (NULL == vendor_path) { + nrl_verbosedebug(NRL_FRAMEWORK, "unable to get vendor path from '%s'", + filename); + return; + } + + if (!nr_execute_handle_autoload_composer_file_exists(vendor_path, + COMPOSER_MAGIC_FILE_1)) { + nrl_verbosedebug(NRL_FRAMEWORK, "'%s' not found in '%s'", + COMPOSER_MAGIC_FILE_1, vendor_path); + goto leave; + } + + if (!nr_execute_handle_autoload_composer_file_exists(vendor_path, + COMPOSER_MAGIC_FILE_2)) { + nrl_verbosedebug(NRL_FRAMEWORK, "'%s' not found in '%s'", + COMPOSER_MAGIC_FILE_2, vendor_path); + goto leave; + } + + if (!nr_execute_handle_autoload_composer_file_exists(vendor_path, + COMPOSER_MAGIC_FILE_3)) { + nrl_verbosedebug(NRL_FRAMEWORK, "'%s' not found in '%s'", + COMPOSER_MAGIC_FILE_3, vendor_path); + goto leave; + } + + nrl_verbosedebug(NRL_FRAMEWORK, "detected composer"); + NRPRG(txn)->composer_info.composer_detected = true; + nr_fw_support_add_library_supportability_metric(NRPRG(txn), "Composer"); + + nr_execute_handle_autoload_composer_get_packages_information(vendor_path); +leave: + nr_free(vendor_path); +} diff --git a/agent/lib_doctrine2.c b/agent/lib_doctrine2.c index e767d253f..8f8b8dfe3 100644 --- a/agent/lib_doctrine2.c +++ b/agent/lib_doctrine2.c @@ -16,6 +16,8 @@ #include "php_call.h" #include "lib_doctrine2.h" +#define PHP_PACKAGE_NAME "doctrine/orm" + /* * This answers the somewhat complicated question of whether we should * instrument DQL, which is dependent on the input query setting as well as SQL @@ -106,7 +108,10 @@ void nr_doctrine2_enable(TSRMLS_D) { #endif /* OAPI */ if (NRINI(vulnerability_management_package_detection_enabled)) { - nr_txn_add_php_package(NRPRG(txn), "doctrine/orm", + nr_txn_add_php_package(NRPRG(txn), PHP_PACKAGE_NAME, PHP_PACKAGE_VERSION_UNKNOWN); } + + nr_txn_suggest_package_supportability_metric(NRPRG(txn), PHP_PACKAGE_NAME, + PHP_PACKAGE_VERSION_UNKNOWN); } diff --git a/agent/lib_guzzle4.c b/agent/lib_guzzle4.c index f3dfc94e9..c52ddffff 100644 --- a/agent/lib_guzzle4.c +++ b/agent/lib_guzzle4.c @@ -33,6 +33,8 @@ #include "util_logging.h" #include "util_memory.h" +#define PHP_PACKAGE_NAME "guzzlehttp/guzzle" + /* * We rely on the const correctness of certain Zend functions that weren't * const correct before 5.3 and/or 5.4: since Guzzle 4 requires 5.4.0 anyway, @@ -520,9 +522,12 @@ void nr_guzzle4_enable(TSRMLS_D) { nr_guzzle_client_construct TSRMLS_CC); if (NRINI(vulnerability_management_package_detection_enabled)) { - nr_txn_add_php_package(NRPRG(txn), "guzzlehttp/guzzle", + nr_txn_add_php_package(NRPRG(txn), PHP_PACKAGE_NAME, PHP_PACKAGE_VERSION_UNKNOWN); } + + nr_txn_suggest_package_supportability_metric(NRPRG(txn), PHP_PACKAGE_NAME, + PHP_PACKAGE_VERSION_UNKNOWN); } void nr_guzzle4_minit(TSRMLS_D) { diff --git a/agent/lib_guzzle6.c b/agent/lib_guzzle6.c index a106eac71..e65a684b7 100644 --- a/agent/lib_guzzle6.c +++ b/agent/lib_guzzle6.c @@ -352,16 +352,32 @@ NR_PHP_WRAPPER_START(nr_guzzle6_client_construct) { zval* this_var = nr_php_scope_get(NR_EXECUTE_ORIG_ARGS); char* version = nr_php_get_object_constant(this_var, "VERSION"); - if (NULL == version) { - version = nr_php_get_object_constant(this_var, "MAJOR_VERSION"); - } if (NRINI(vulnerability_management_package_detection_enabled)) { // Add php package to transaction nr_txn_add_php_package(NRPRG(txn), PHP_PACKAGE_NAME, version); } - nr_fw_support_add_package_supportability_metric(NRPRG(txn), PHP_PACKAGE_NAME, - version); + + /* + * If we were unable to get the full version before, at least we can extract + * the major version to send to the supportability metric now, as + * this incomplete version will not be stored in a php package record + * and so the supportability metric cannot be created later like for + * most packages. + * + * This is relevant to guzzle7+ which no longer supplies full version. + */ + if (NULL == version) { + version = nr_php_get_object_constant(this_var, "MAJOR_VERSION"); + } + + /* if version is still NULL that is OK this next call will accept + * that value and when supportability metrics are made for + * packages if another method has determined the package version + * (composer api for example) then it will be filled in at that time + */ + nr_txn_suggest_package_supportability_metric(NRPRG(txn), PHP_PACKAGE_NAME, + version); nr_free(version); (void)wraprec; diff --git a/agent/lib_mongodb.c b/agent/lib_mongodb.c index 51e979abd..8d129ae9d 100644 --- a/agent/lib_mongodb.c +++ b/agent/lib_mongodb.c @@ -18,6 +18,8 @@ #include "lib_mongodb_private.h" +#define PHP_PACKAGE_NAME "mongodb/mongodb" + static int nr_mongodb_is_server(const zval* obj TSRMLS_DC) { return nr_php_object_instanceof_class(obj, "MongoDB\\Driver\\Server" TSRMLS_CC); @@ -108,6 +110,8 @@ void nr_mongodb_get_host_and_port_path_or_id(zval* server, } } +#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO \ + || defined OVERWRITE_ZEND_EXECUTE_DATA NR_PHP_WRAPPER(nr_mongodb_operation) { const char* this_klass = "MongoDB\\Operation\\Executable"; zval* collection = NULL; @@ -173,7 +177,174 @@ NR_PHP_WRAPPER(nr_mongodb_operation) { } NR_PHP_WRAPPER_END -void nr_mongodb_enable(TSRMLS_D) { +#else + +NR_PHP_WRAPPER(nr_mongodb_operation_before) { + (void)wraprec; + nr_segment_t* segment = NULL; + segment = nr_segment_start(NRPRG(txn), NULL, NULL); + if (NULL != segment) { + segment->wraprec = auto_segment->wraprec; + } +} +NR_PHP_WRAPPER_END + +NR_PHP_WRAPPER(nr_mongodb_operation_after) { + const char* this_klass = "MongoDB\\Operation\\Executable"; + zval* collection = NULL; + zval* database = NULL; + zval* server = NULL; + zval* this_var = NULL; + bool discard_segment = false; + nr_datastore_instance_t instance = { + .host = NULL, + .port_path_or_id = NULL, + .database_name = NULL, + }; + + // tell the compiler to ignore the cast from const char * to char * + // to save having to do a strdup operation +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wcast-qual" + nr_segment_datastore_params_t params = { + .collection = NULL, + .datastore = { + .type = NR_DATASTORE_MONGODB, + }, + .operation = (char *)wraprec->extra, + .instance = &instance, + .callbacks = { + .backtrace = nr_php_backtrace_callback, + }, + }; +#pragma GCC diagnostic pop + /* + * We check for the interface all Collection operations extend, rather than + * their specific class. Not all operations have the properties we need but + * the ones we hook do (as of mongo-php-library v.1.1). + */ + this_var = nr_php_scope_get(NR_EXECUTE_ORIG_ARGS); + if (!nr_php_object_instanceof_class(this_var, this_klass)) { + nrl_verbosedebug(NRL_FRAMEWORK, "%s: operation is not %s", __func__, + this_klass); + discard_segment = true; + goto leave; + } + + collection = nr_php_get_zval_object_property(this_var, "collectionName"); + if (nr_php_is_zval_valid_string(collection)) { + params.collection = Z_STRVAL_P(collection); + } + + database = nr_php_get_zval_object_property(this_var, "databaseName"); + if (nr_php_is_zval_valid_string(database)) { + instance.database_name = Z_STRVAL_P(database); + } + + server = nr_php_arg_get(1, NR_EXECUTE_ORIG_ARGS); + nr_mongodb_get_host_and_port_path_or_id(server, &instance.host, + &instance.port_path_or_id); + +leave: + if (discard_segment) { + nr_segment_discard(&auto_segment); + } else { + nr_segment_datastore_end(&auto_segment, ¶ms); + } + nr_php_arg_release(&server); + nr_php_scope_release(&this_var); + nr_free(instance.host); + nr_free(instance.port_path_or_id); +} +NR_PHP_WRAPPER_END + +#endif /* OAPI */ + +void nr_mongodb_enable() { +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO \ + && !defined OVERWRITE_ZEND_EXECUTE_DATA + + nr_php_wrap_user_function_before_after_clean_extra( + NR_PSTR("MongoDB\\Operation\\Aggregate::execute"), + nr_mongodb_operation_before, nr_mongodb_operation_after, + nr_mongodb_operation_after, "aggregate"); + + nr_php_wrap_user_function_before_after_clean_extra( + NR_PSTR("MongoDB\\Operation\\BulkWrite::execute"), + nr_mongodb_operation_before, nr_mongodb_operation_after, + nr_mongodb_operation_after, "bulkWrite"); + + nr_php_wrap_user_function_before_after_clean_extra( + NR_PSTR("MongoDB\\Operation\\Count::execute"), + nr_mongodb_operation_before, nr_mongodb_operation_after, + nr_mongodb_operation_after, "count"); + + nr_php_wrap_user_function_before_after_clean_extra( + NR_PSTR("MongoDB\\Operation\\CountDocuments::execute"), + nr_mongodb_operation_before, nr_mongodb_operation_after, + nr_mongodb_operation_after, "countDocuments"); + + nr_php_wrap_user_function_before_after_clean_extra( + NR_PSTR("MongoDB\\Operation\\CreateIndexes::execute"), + nr_mongodb_operation_before, nr_mongodb_operation_after, + nr_mongodb_operation_after, "createIndexes"); + + nr_php_wrap_user_function_before_after_clean_extra( + NR_PSTR("MongoDB\\Operation\\Delete::execute"), + nr_mongodb_operation_before, nr_mongodb_operation_after, + nr_mongodb_operation_after, "delete"); + + nr_php_wrap_user_function_before_after_clean_extra( + NR_PSTR("MongoDB\\Operation\\Distinct::execute"), + nr_mongodb_operation_before, nr_mongodb_operation_after, + nr_mongodb_operation_after, "distinct"); + + nr_php_wrap_user_function_before_after_clean_extra( + NR_PSTR("MongoDB\\Operation\\DropCollection::execute"), + nr_mongodb_operation_before, nr_mongodb_operation_after, + nr_mongodb_operation_after, "dropCollection"); + + nr_php_wrap_user_function_before_after_clean_extra( + NR_PSTR("MongoDB\\Operation\\DropIndexes::execute"), + nr_mongodb_operation_before, nr_mongodb_operation_after, + nr_mongodb_operation_after, "dropIndexes"); + + nr_php_wrap_user_function_before_after_clean_extra( + NR_PSTR("MongoDB\\Operation\\Find::execute"), nr_mongodb_operation_before, + nr_mongodb_operation_after, nr_mongodb_operation_after, "find"); + + nr_php_wrap_user_function_before_after_clean_extra( + NR_PSTR("MongoDB\\Operation\\FindAndModify::execute"), + nr_mongodb_operation_before, nr_mongodb_operation_after, + nr_mongodb_operation_after, "findAndModify"); + + nr_php_wrap_user_function_before_after_clean_extra( + NR_PSTR("MongoDB\\Operation\\InsertMany::execute"), + nr_mongodb_operation_before, nr_mongodb_operation_after, + nr_mongodb_operation_after, "insertMany"); + + nr_php_wrap_user_function_before_after_clean_extra( + NR_PSTR("MongoDB\\Operation\\InsertOne::execute"), + nr_mongodb_operation_before, nr_mongodb_operation_after, + nr_mongodb_operation_after, "insertOne"); + + nr_php_wrap_user_function_before_after_clean_extra( + NR_PSTR("MongoDB\\Operation\\ListIndexes::execute"), + nr_mongodb_operation_before, nr_mongodb_operation_after, + nr_mongodb_operation_after, "listIndexes"); + + nr_php_wrap_user_function_before_after_clean_extra( + NR_PSTR("MongoDB\\Operation\\Update::execute"), + nr_mongodb_operation_before, nr_mongodb_operation_after, + nr_mongodb_operation_after, "update"); + + nr_php_wrap_user_function_before_after_clean_extra( + NR_PSTR("MongoDB\\Operation\\DatabaseCommand::execute"), + nr_mongodb_operation_before, nr_mongodb_operation_after, + nr_mongodb_operation_after, "databaseCommand"); + +#else /* Non-OAPI */ + /* * We instrument interesting methods on the MongoDB\Collection class via their * associated MongoDB\Operation classes. @@ -187,6 +358,14 @@ void nr_mongodb_enable(TSRMLS_D) { nr_php_wrap_user_function_extra(NR_PSTR("MongoDB\\Operation\\Count::execute"), nr_mongodb_operation, "count" TSRMLS_CC); + /* + * `count` has been deprecated in later versions of mongodb and replaced by + * `countDocuments` + */ + nr_php_wrap_user_function_extra( + NR_PSTR("MongoDB\\Operation\\CountDocuments::execute"), + nr_mongodb_operation, "countDocuments"); + /* * This also catches MongoDB\Collection::createIndex */ @@ -265,8 +444,13 @@ void nr_mongodb_enable(TSRMLS_D) { NR_PSTR("MongoDB\\Operation\\DatabaseCommand::execute"), nr_mongodb_operation, "databaseCommand" TSRMLS_CC); +#endif /* OAPI */ + if (NRINI(vulnerability_management_package_detection_enabled)) { - nr_txn_add_php_package(NRPRG(txn), "mongodb/mongodb", + nr_txn_add_php_package(NRPRG(txn), PHP_PACKAGE_NAME, PHP_PACKAGE_VERSION_UNKNOWN); } + + nr_txn_suggest_package_supportability_metric(NRPRG(txn), PHP_PACKAGE_NAME, + PHP_PACKAGE_VERSION_UNKNOWN); } diff --git a/agent/lib_monolog.c b/agent/lib_monolog.c index 667e33583..fcdb1ac06 100644 --- a/agent/lib_monolog.c +++ b/agent/lib_monolog.c @@ -378,8 +378,8 @@ NR_PHP_WRAPPER(nr_monolog_logger_addrecord) { = nr_monolog_get_timestamp(api, argc, NR_EXECUTE_ORIG_ARGS TSRMLS_CC); char version[MAJOR_VERSION_LENGTH]; snprintf(version, sizeof(version), "%d", api); - nr_fw_support_add_package_supportability_metric(NRPRG(txn), - PHP_PACKAGE_NAME, version); + nr_txn_suggest_package_supportability_metric(NRPRG(txn), PHP_PACKAGE_NAME, + version); } /* Record the log event */ @@ -523,4 +523,7 @@ void nr_monolog_enable(TSRMLS_D) { nr_txn_add_php_package(NRPRG(txn), PHP_PACKAGE_NAME, PHP_PACKAGE_VERSION_UNKNOWN); } + + nr_txn_suggest_package_supportability_metric(NRPRG(txn), PHP_PACKAGE_NAME, + PHP_PACKAGE_VERSION_UNKNOWN); } diff --git a/agent/lib_php_amqplib.c b/agent/lib_php_amqplib.c new file mode 100644 index 000000000..94e15fe96 --- /dev/null +++ b/agent/lib_php_amqplib.c @@ -0,0 +1,833 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* + * Functions relating to instrumenting the php-ampqlib + * https://github.com/php-amqplib/php-amqplib + */ +#include "php_agent.h" +#include "php_api_distributed_trace.h" +#include "php_call.h" +#include "php_hash.h" +#include "php_wrapper.h" +#include "fw_hooks.h" +#include "fw_support.h" +#include "util_logging.h" +#include "lib_php_amqplib.h" +#include "nr_segment_message.h" +#include "nr_header.h" + +#define PHP_PACKAGE_NAME "php-amqplib/php-amqplib" + +/* + * With PHP 8+, we have access to all the zend_execute_data structures both + * before and after the function call so we can just maintain pointers into the + * struct. With PHP 7.x, without doing special handling, we don't have access + * to the values afterwards. Sometimes nr_php_arg_get is used as that DUPs the + * zval which then later needs to be freed with nr_php_arg_release. In this + * case, we don't need to go through the extra trouble of duplicating a ZVAL + * when we don't need to duplicate anything if there is no valid value. We + * check for a valid value, and if we want to store it, we'll strdup it. So + * instead of doing multiple zval dups all of the time, we do some strdups some + * of the time. + */ +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO /* PHP8.0+ */ +#define ENSURE_PERSISTENCE(x) x +#define UNDO_PERSISTENCE(x) +#else +#define ENSURE_PERSISTENCE(x) nr_strdup(x) +#define UNDO_PERSISTENCE(x) nr_free(x); +#endif + +/* + * See here for supported Amazon MQ for RabbitMQ engine versions + * https://docs.aws.amazon.com/amazon-mq/latest/developer-guide/rabbitmq-version-management.html + * For instance: + * As of Feb 2025, 3.13 (recommended) + * + * See here for latest RabbitMQ Server https://www.rabbitmq.com/docs/download + * For instance: + * As of Feb 2025, the latest release of RabbitMQ Server is 4.0.5. + * + * https://www.rabbitmq.com/tutorials/tutorial-one-php + * Installing RabbitMQ + * + * While the RabbitMQ tutorial for using with the dockerized RabbitMQ setup + * correctly and loads the PhpAmqpLib\\Channel\\AMQPChannel class in time for + * the agent to wrap the instrumented functions, with AWS MQ_BROKER + * specific but valid scenarios where the PhpAmqpLib\\Channel\\AMQPChannel class + * file does not explicitly load and the instrumented + * functions are NEVER wrapped regardless of how many times they are called in + * one txn. + * Specifically, this centered around the very slight but impactful + * differences when using managing the AWS MQ_BROKER connect vs using the + * official RabbitMq Server, and this function is needed ONLY to support AWS's + * MQ_BROKER. + * + * When connecting via SSL with rabbitmq's official server is explicitly loaded. + * Hoever, when connecting via SSL with an MQ_BROKER that uses RabbitMQ(using + * the exact same php file and with only changes in the server name for the + * connection), the AMQPChannel file (and therefore class), the AMQPChannel file + * (and therefore class) is NOT explicitly loaded. + * + * Because the very key `PhpAmqpLib/Channel/AMQPChannel.php` file never gets + * explicitly loaded when interacting with the AWS MQ_BROKER, the class is not + * automatically loaded even though it is available and can be resolved if + * called from within PHP. Because of this, the instrumented functions NEVER + * get wrapped when connecting to the MQ_BROKER and therefore the + * instrumentation is never triggered. The explicit loading of the class is + * needed to work with MQ_BROKER. + */ + +/* + * Purpose : Ensures the php-amqplib instrumentation gets wrapped. + * + * Params : None + * + * Returns : None + */ +static void nr_php_amqplib_ensure_class() { + int result = FAILURE; + zend_class_entry* class_entry = NULL; + + class_entry = nr_php_find_class("phpamqplib\\channel\\amqpchannel"); + if (NULL == class_entry) { + result = zend_eval_stringl( + NR_PSTR("class_exists('PhpAmqpLib\\Channel\\AMQPChannel');"), NULL, + "nr_php_amqplib_class_exists_channel_amqpchannel"); + } + /* + * We don't need to check anything else at this point. If this fails, there's + * nothing else we can do anyway. + */ +} + +/* + * Version information will be pulled from PhpAmqpLib\\Package::VERSION + * nr_php_amqplib_handle_version will automatically load the class if it isn't + * loaded yet and then evaluate the string. To avoid the VERY unlikely but not + * impossible fatal error if the file/class doesn't exist, we need to wrap + * the call in a try/catch block and make it a lambda so that we avoid errors. + * This won't load the file if it doesn't exist, but by the time this is called, + * the existence of the php-amqplib is a known quantity so calling the following + * lambda will result in the PhpAmqpLib\\Package class being loaded. + */ +void nr_php_amqplib_handle_version() { + char* version = NULL; + zval retval_zpd; + int result = FAILURE; + + result = zend_eval_stringl( + NR_PSTR( + "(function() {" + " $nr_php_amqplib_version = null;" + " try {" + " $nr_php_amqplib_version = PhpAmqpLib\\Package::VERSION;" + " } catch (Throwable $e) {" + " }" + " return $nr_php_amqplib_version;" + "})();"), + &retval_zpd, "nr_php_amqplib_get_phpamqplib_package_version"); + + /* See if we got a non-empty/non-null string for version. */ + if (SUCCESS == result) { + if (nr_php_is_zval_valid_string(&retval_zpd)) { + version = Z_STRVAL(retval_zpd); + } + } + + if (NRINI(vulnerability_management_package_detection_enabled)) { + /* Add php package to transaction */ + nr_txn_add_php_package(NRPRG(txn), PHP_PACKAGE_NAME, version); + } + + nr_txn_suggest_package_supportability_metric(NRPRG(txn), PHP_PACKAGE_NAME, + version); + + zval_dtor(&retval_zpd); +} + +/* + * Purpose : Retrieves host and port from an AMQP Connection and sets the + * host/port values in the message_params. + * + * Params : 1. PhpAmqpLib\Connection family of connections that inherit from + * AbstractConnection + * 2. nr_segment_message_params_t* message_params that will be + * modified with port and host info, if available + * + * Returns : None + * + * See here for more information about the AbstractConnection class that all + * Connection classes inherit from: + * https://github.com/php-amqplib/php-amqplib/blob/master/PhpAmqpLib/Connection/AbstractConnection.php + */ +static inline void nr_php_amqplib_get_host_and_port( + zval* amqp_connection, + nr_segment_message_params_t* message_params) { + zval* amqp_server = NULL; + zval* amqp_port = NULL; + zval* connect_constructor_params = NULL; + + if (NULL == amqp_connection || NULL == message_params) { + return; + } + + if (!nr_php_is_zval_valid_object(amqp_connection)) { + return; + } + + /* construct_params are always saved to use for cloning purposes. */ + connect_constructor_params + = nr_php_get_zval_object_property(amqp_connection, "construct_params"); + if (!nr_php_is_zval_valid_array(connect_constructor_params)) { + return; + } + + amqp_server + = nr_php_zend_hash_index_find(Z_ARRVAL_P(connect_constructor_params), + AMQP_CONSTRUCT_PARAMS_SERVER_INDEX); + if (nr_php_is_zval_non_empty_string(amqp_server)) { + message_params->server_address + = ENSURE_PERSISTENCE(Z_STRVAL_P(amqp_server)); + } + + amqp_port = nr_php_zend_hash_index_find( + Z_ARRVAL_P(connect_constructor_params), AMQP_CONSTRUCT_PARAMS_PORT_INDEX); + if (nr_php_is_zval_valid_integer(amqp_port)) { + message_params->server_port = Z_LVAL_P(amqp_port); + } +} + +/* + * Purpose : Applies DT headers to an outbound AMQPMessage. + * Note: + * The DT header 'newrelic' will only be added if both + * newrelic.distributed_tracing_enabled is enabled and + * newrelic.distributed_tracing_exclude_newrelic_header is set to false in the + * INI settings. The W3C headers 'traceparent' and 'tracestate' will will only + * be added if newrelic.distributed_tracing_enabled is enabled in the + * newrelic.ini settings. + * + * Params : PhpAmqpLib\Message\AMQPMessage + * + * Returns : None + * + * Refer here for AMQPMessage: + * https://github.com/php-amqplib/php-amqplib/blob/master/PhpAmqpLib/Message/AMQPMessage.php + * Refer here for AMQPTable: + * https://github.com/php-amqplib/php-amqplib/blob/master/PhpAmqpLib/Wire/AMQPTable.php + */ + +static inline void nr_php_amqplib_insert_dt_headers(zval* amqp_msg) { + zval* amqp_properties_array = NULL; + zval* dt_headers_zvf = NULL; + zval* amqp_headers_table = NULL; + zval* retval_set_property_zvf = NULL; + zval* retval_set_table_zvf = NULL; + zval application_headers_zpd; + zval key_zval_zpd; + zval amqp_table_retval_zpd; + zval* key_exists = NULL; + zval* amqp_table_data = NULL; + zend_ulong key_num = 0; + nr_php_string_hash_key_t* key_str = NULL; + zval* val = NULL; + int retval = FAILURE; + + /* + * Note: + * The DT header 'newrelic' will only be added if both + * newrelic.distributed_tracing_enabled is enabled and + * newrelic.distributed_tracing_exclude_newrelic_header is set to false in the + * INI settings. The W3C headers 'traceparent' and 'tracestate' will will only + * be added if newrelic.distributed_tracing_enabled is enabled in the + * newrelic.ini settings. + */ + + /* + * Refer here for AMQPMessage: + * https://github.com/php-amqplib/php-amqplib/blob/master/PhpAmqpLib/Message/AMQPMessage.php + * Refer here for AMQPTable: + * https://github.com/php-amqplib/php-amqplib/blob/master/PhpAmqpLib/Wire/AMQPTable.php + */ + if (!nr_php_is_zval_valid_object(amqp_msg)) { + return; + } + + if (!NRPRG(txn)->options.distributed_tracing_enabled) { + return; + } + + amqp_properties_array + = nr_php_get_zval_object_property(amqp_msg, "properties"); + if (!nr_php_is_zval_valid_array(amqp_properties_array)) { + nrl_verbosedebug( + NRL_INSTRUMENT, + "AMQPMessage properties are invalid. AMQPMessage always sets " + "this to empty arrray by default so something is seriously wrong with " + "the message object. Exit."); + return; + } + + /* + * newrelic_get_request_metadata is an internal API that will only return the + * DT header 'newrelic' will only be added if both + * newrelic.distributed_tracing_enabled is enabled and + * newrelic.distributed_tracing_exclude_newrelic_header is set to false in the + * INI settings. The W3C headers 'traceparent' and 'tracestate' will will only + * be returned if newrelic.distributed_tracing_enabled is enabled in the + * newrelic.ini settings. + */ + dt_headers_zvf = nr_php_call(NULL, "newrelic_get_request_metadata"); + if (!nr_php_is_zval_valid_array(dt_headers_zvf)) { + nr_php_zval_free(&dt_headers_zvf); + return; + } + + /* + * The application_headers are stored in an encoded PhpAmqpLib\Wire\AMQPTable + * object + */ + + amqp_headers_table = nr_php_zend_hash_find(Z_ARRVAL_P(amqp_properties_array), + "application_headers"); + /* + * If the application_headers AMQPTable object doesn't exist, we'll have to + * create it with an empty array. + */ + if (!nr_php_is_zval_valid_object(amqp_headers_table)) { + retval = zend_eval_stringl( + NR_PSTR("(function() {" + " try {" + " return new PhpAmqpLib\\Wire\\AMQPTable(array());" + " } catch (Throwable $e) {" + " return null;" + " }" + "})();"), + &amqp_table_retval_zpd, "nr_php_amqplib_create_empty_amqptable"); + + if (FAILURE == retval) { + nrl_verbosedebug(NRL_INSTRUMENT, + "No application headers in AMQPTable, but couldn't " + "create one. Exit."); + goto end; + } + if (!nr_php_is_zval_valid_object(&amqp_table_retval_zpd)) { + nrl_verbosedebug(NRL_INSTRUMENT, + "No application headers in AMQPTable, but couldn't " + "create one. Exit."); + zval_ptr_dtor(&amqp_table_retval_zpd); + goto end; + } + /* + * Get application+_headers string in zval form for use with nr_php_call + */ + ZVAL_STRING(&application_headers_zpd, "application_headers"); + /* + * Set the valid AMQPTable on the AMQPMessage. + */ + retval_set_property_zvf = nr_php_call( + amqp_msg, "set", &application_headers_zpd, &amqp_table_retval_zpd); + + zval_ptr_dtor(&application_headers_zpd); + zval_ptr_dtor(&amqp_table_retval_zpd); + + if (NULL == retval_set_property_zvf) { + nrl_verbosedebug(NRL_INSTRUMENT, + "AMQPMessage had no application_headers AMQPTable, but " + "set failed for the AMQPTable wthat was just created " + "for the application headers. Unable to proceed, exit."); + goto end; + } + /* Should have valid AMQPTable objec on the AMQPMessage at this point. */ + amqp_headers_table = nr_php_zend_hash_find( + Z_ARRVAL_P(amqp_properties_array), "application_headers"); + if (!nr_php_is_zval_valid_object(amqp_headers_table)) { + nrl_info( + NRL_INSTRUMENT, + "AMQPMessage had no application_headers AMQPTable, but unable to " + "retrieve even after creating and setting. Unable to proceed, exit."); + goto end; + } + } + + /* + * This contains the application_headers data. It is an array of + * key/encoded_array_val pairs. + */ + amqp_table_data = nr_php_get_zval_object_property(amqp_headers_table, "data"); + + /* + * First check if it's a reference to another zval, and if so, get point to + * the actual zval. + */ + + if (IS_REFERENCE == Z_TYPE_P(amqp_table_data)) { + amqp_table_data = Z_REFVAL_P(amqp_table_data); + } + if (!nr_php_is_zval_valid_array(amqp_table_data)) { + /* + * This is a basic part of the AMQPTable, if this doesn't exist, something + * is seriously wrong. Cannot proceed, exit. + */ + goto end; + } + + /* + * Loop through the DT Header array and set the headers in the + * application_header AMQPTable if they do not already exist. + */ + + ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(dt_headers_zvf), key_num, key_str, val) { + (void)key_num; + + if (NULL != key_str && nr_php_is_zval_valid_string(val)) { + key_exists + = nr_php_zend_hash_find(HASH_OF(amqp_table_data), ZSTR_VAL(key_str)); + if (NULL == key_exists) { + /* Key doesn't exist, so set the value in the AMQPTable. */ + + /* key_str is a zend_string. It needs to be a zval to pass to + * nr_php_call. */ + ZVAL_STR_COPY(&key_zval_zpd, key_str); + retval_set_table_zvf + = nr_php_call(amqp_headers_table, "set", &key_zval_zpd, val); + if (NULL == retval_set_table_zvf) { + nrl_verbosedebug(NRL_INSTRUMENT, + "%s didn't exist in the AMQPTable, but couldn't " + "set the key/val to the table.", + NRSAFESTR(ZSTR_VAL(key_str))); + } + zval_ptr_dtor(&key_zval_zpd); + nr_php_zval_free(&retval_set_table_zvf); + } + } + } + ZEND_HASH_FOREACH_END(); + +end: + nr_php_zval_free(&dt_headers_zvf); + nr_php_zval_free(&retval_set_property_zvf); +} + +/* + * Purpose : Retrieve any DT headers from an inbound AMQPMessage if + * newrelic.distributed_tracing_exclude_newrelic_header INI setting is false + * and apply to txn. + * + * Params : PhpAmqpLib\Message\AMQPMessage + * + * Returns : None + * + * Refer here for AMQPMessage: + * https://github.com/php-amqplib/php-amqplib/blob/master/PhpAmqpLib/Message/AMQPMessage.php + * Refer here for AMQPTable: + * https://github.com/php-amqplib/php-amqplib/blob/master/PhpAmqpLib/Wire/AMQPTable.php + */ +static inline void nr_php_amqplib_retrieve_dt_headers(zval* amqp_msg) { + zval* amqp_headers_native_data_zvf = NULL; + zval* amqp_properties_array = NULL; + zval* amqp_headers_table = NULL; + zval* amqp_table_data = NULL; + zval* dt_payload = NULL; + zval* traceparent = NULL; + zval* tracestate = NULL; + char* dt_payload_string = NULL; + char* traceparent_string = NULL; + char* tracestate_string = NULL; + zend_ulong key_num = 0; + nr_php_string_hash_key_t* key_str = NULL; + zval* val = NULL; + int retval = FAILURE; + + /* + * Refer here for AMQPMessage: + * https://github.com/php-amqplib/php-amqplib/blob/master/PhpAmqpLib/Message/AMQPMessage.php + * Refer here for AMQPTable: + * https://github.com/php-amqplib/php-amqplib/blob/master/PhpAmqpLib/Wire/AMQPTable.php + */ + if (!nr_php_is_zval_valid_object(amqp_msg)) { + return; + } + + if (!NRPRG(txn)->options.distributed_tracing_enabled) { + return; + } + + amqp_properties_array + = nr_php_get_zval_object_property(amqp_msg, "properties"); + if (!nr_php_is_zval_valid_array(amqp_properties_array)) { + nrl_verbosedebug( + NRL_INSTRUMENT, + "AMQPMessage properties not valid. AMQPMessage always sets " + "this to empty arrray by default. something seriously wrong with " + "the message object. Unable to proceed, Exit"); + return; + } + + /* PhpAmqpLib\Wire\AMQPTable object*/ + amqp_headers_table = nr_php_zend_hash_find(Z_ARRVAL_P(amqp_properties_array), + "application_headers"); + if (!nr_php_is_zval_valid_object(amqp_headers_table)) { + /* No headers here, exit. */ + return; + } + + /* + * We can't use amqp table "data" property here because while it has the + * correct keys, the vals are encoded arrays. We need to use getNativeData + * so it will decode the values for us since it formats the AMQPTable as an + * array of unencoded key/val pairs. */ + amqp_headers_native_data_zvf + = nr_php_call(amqp_headers_table, "getNativeData"); + + if (!nr_php_is_zval_valid_array(amqp_headers_native_data_zvf)) { + nr_php_zval_free(&amqp_headers_native_data_zvf); + return; + } + + dt_payload + = nr_php_zend_hash_find(HASH_OF(amqp_headers_native_data_zvf), NEWRELIC); + dt_payload_string + = nr_php_is_zval_valid_string(dt_payload) ? Z_STRVAL_P(dt_payload) : NULL; + + traceparent = nr_php_zend_hash_find(HASH_OF(amqp_headers_native_data_zvf), + W3C_TRACEPARENT); + traceparent_string = nr_php_is_zval_valid_string(traceparent) + ? Z_STRVAL_P(traceparent) + : NULL; + + tracestate = nr_php_zend_hash_find(HASH_OF(amqp_headers_native_data_zvf), + W3C_TRACESTATE); + tracestate_string + = nr_php_is_zval_valid_string(tracestate) ? Z_STRVAL_P(tracestate) : NULL; + + if (NULL != dt_payload || NULL != traceparent) { + nr_hashmap_t* header_map = nr_header_create_distributed_trace_map( + dt_payload_string, traceparent_string, tracestate_string); + + /* + * nr_php_api_accept_distributed_trace_payload_httpsafe will add the headers + * to the txn if there have been no other inbound/outbound headers added + * already. + */ + nr_php_api_accept_distributed_trace_payload_httpsafe(NRPRG(txn), header_map, + "Queue"); + + nr_hashmap_destroy(&header_map); + } + nr_php_zval_free(&amqp_headers_native_data_zvf); + + return; +} + +/* + * Purpose : A wrapper to instrument the php-amqplib basic_publish. This + * retrieves values to populate a message segment and insert the DT headers, if + * applicable. + * + * Note: The DT header 'newrelic' will only be added if both + * newrelic.distributed_tracing_enabled is enabled and + * newrelic.distributed_tracing_exclude_newrelic_header is set to false in the + * INI settings. The W3C headers 'traceparent' and 'tracestate' will will only + * be added if newrelic.distributed_tracing_enabled is enabled in the + * newrelic.ini settings. + * + * PhpAmqpLib\Channel\AMQPChannel::basic_publish + * Publishes a message + * + * @param AMQPMessage $msg + * @param string $exchange + * @param string $routing_key + * @param bool $mandatory + * @param bool $immediate + * @param int|null $ticket + * @throws AMQPChannelClosedException + * @throws AMQPConnectionClosedException + * @throws AMQPConnectionBlockedException + * + */ + +NR_PHP_WRAPPER(nr_rabbitmq_basic_publish_before) { + zval* amqp_msg = NULL; + (void)wraprec; + + amqp_msg = nr_php_get_user_func_arg(1, NR_EXECUTE_ORIG_ARGS); + /* + * nr_php_amqplib_insert_dt_headers will check the validity of the object. + */ + nr_php_amqplib_insert_dt_headers(amqp_msg); +} +NR_PHP_WRAPPER_END + +NR_PHP_WRAPPER(nr_rabbitmq_basic_publish) { + zval* amqp_exchange = NULL; + zval* amqp_routing_key = NULL; + zval* amqp_connection = NULL; + nr_segment_t* message_segment = NULL; + + nr_segment_message_params_t message_params = { + .library = RABBITMQ_LIBRARY_NAME, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_EXCHANGE, + .message_action = NR_SPANKIND_PRODUCER, + .messaging_system = RABBITMQ_MESSAGING_SYSTEM, + }; + + (void)wraprec; + +#if ZEND_MODULE_API_NO < ZEND_8_0_X_API_NO /* PHP8.0+ */ + zval* amqp_msg = NULL; + amqp_msg = nr_php_get_user_func_arg(1, NR_EXECUTE_ORIG_ARGS); + /* + * nr_php_amqplib_insert_dt_headers will check the validity of the object. + */ + nr_php_amqplib_insert_dt_headers(amqp_msg); +#endif + + amqp_exchange = nr_php_get_user_func_arg(2, NR_EXECUTE_ORIG_ARGS); + if (nr_php_is_zval_non_empty_string(amqp_exchange)) { + /* + * In PHP 7.x, the following will create a strdup in + * message_params.destination_name that needs to be freed. + */ + message_params.destination_name + = ENSURE_PERSISTENCE(Z_STRVAL_P(amqp_exchange)); + } else { + /* + * For producer, this is exchange name. Exchange name is Default in case + * of empty string. + */ + if (nr_php_is_zval_valid_string(amqp_exchange)) { + message_params.destination_name = ENSURE_PERSISTENCE("Default"); + } + } + + amqp_routing_key = nr_php_get_user_func_arg(3, NR_EXECUTE_ORIG_ARGS); + if (nr_php_is_zval_non_empty_string(amqp_routing_key)) { + /* + * In PHP 7.x, the following will create a strdup in + * message_params.messaging_destination_routing_key that needs to be + * freed. + */ + message_params.messaging_destination_routing_key + = ENSURE_PERSISTENCE(Z_STRVAL_P(amqp_routing_key)); + } + + amqp_connection = nr_php_get_zval_object_property( + nr_php_execute_scope(execute_data), "connection"); + /* + * In PHP 7.x, the following will create a strdup in + * message_params.server_address that needs to be freed. + */ + nr_php_amqplib_get_host_and_port(amqp_connection, &message_params); + + /* For PHP 7.x compatibility. */ + NR_PHP_WRAPPER_CALL + + /* + * Now create and end the instrumented segment as a message segment. + * + * By this point, it's been determined that this call will be instrumented + * so only create the message segment now, grab the parent segment start + * time, add our message segment attributes/metrics then close the newly + * created message segment. + */ + + if (NULL == auto_segment) { + /* + * Must be checked after PHP_WRAPPER_CALL to ensure txn didn't end during + * the call. + */ + goto end; + } + + message_segment = nr_segment_start(NRPRG(txn), NULL, NULL); + if (NULL != message_segment) { + /* re-use start time from auto_segment started in func_begin */ + message_segment->start_time = auto_segment->start_time; + nr_segment_message_end(&message_segment, &message_params); + } + +end: + /* + * Because we had to strdup values to persist them beyond + * NR_PHP_WRAPPER_CALL, now we destroy them. There isn't a separate function + * to destroy all since some of the params are string literals and we don't + * want to strdup everything if we don't have to. RabbitMQ basic_publish + * PHP 7.x will only strdup server_address, destination_name, and + * messaging_destination_routing_key. + */ + UNDO_PERSISTENCE(message_params.server_address); + UNDO_PERSISTENCE(message_params.destination_name); + UNDO_PERSISTENCE(message_params.messaging_destination_routing_key); +} +NR_PHP_WRAPPER_END + +/* + * Purpose : A wrapper to instrument the php-amqplib basic_get. This + * retrieves values to populate a message segment. + * Note: + * The DT header 'newrelic' will only be considered if both + * newrelic.distributed_tracing_enabled is enabled and + * newrelic.distributed_tracing_exclude_newrelic_header is set to false in the + * INI settings. The W3C headers 'traceparent' and 'tracestate' will will only + * be considered if newrelic.distributed_tracing_enabled is enabled in the + * newrelic.ini settings. If settings are correct, it will + * retrieve the DT headers and, if applicable, apply to the txn. + * + * PhpAmqpLib\Channel\AMQPChannel::basic_get + * Direct access to a queue if no message was available in the queue, return + * null + * + * @param string $queue + * @param bool $no_ack + * @param int|null $ticket + * @throws \PhpAmqpLib\Exception\AMQPTimeoutException if the specified + * operation timeout was exceeded + * @return AMQPMessage|null + */ +NR_PHP_WRAPPER(nr_rabbitmq_basic_get) { + zval* amqp_queue = NULL; + zval* amqp_exchange = NULL; + zval* amqp_routing_key = NULL; + zval* amqp_connection = NULL; + nr_segment_t* message_segment = NULL; + zval** retval_ptr = NR_GET_RETURN_VALUE_PTR; + + nr_segment_message_params_t message_params = { + .library = RABBITMQ_LIBRARY_NAME, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_EXCHANGE, + .message_action = NR_SPANKIND_CONSUMER, + .messaging_system = RABBITMQ_MESSAGING_SYSTEM, + }; + + (void)wraprec; + + amqp_queue = nr_php_get_user_func_arg(1, NR_EXECUTE_ORIG_ARGS); + if (nr_php_is_zval_non_empty_string(amqp_queue)) { + /* For consumer, this is queue name. */ + message_params.destination_name + = ENSURE_PERSISTENCE(Z_STRVAL_P(amqp_queue)); + } + + amqp_connection = nr_php_get_zval_object_property( + nr_php_execute_scope(execute_data), "connection"); + /* + * In PHP 7.x, the following will create a strdup in + * message_params.server_address that needs to be freed. + */ + nr_php_amqplib_get_host_and_port(amqp_connection, &message_params); + + /* Compatibility with PHP 7.x */ + NR_PHP_WRAPPER_CALL; + + if (NULL == auto_segment) { + /* + * Must be checked after PHP_WRAPPER_CALL to ensure txn didn't end during + * the call. + */ + goto end; + } + /* + *The retval should be an AMQPMessage. nr_php_is_zval_* ops do NULL checks + * as well. + */ + if (NULL != retval_ptr && nr_php_is_zval_valid_object(*retval_ptr)) { + /* + * Get the exchange and routing key from the AMQPMessage + */ + amqp_exchange = nr_php_get_zval_object_property(*retval_ptr, "exchange"); + if (nr_php_is_zval_non_empty_string(amqp_exchange)) { + /* Used with consumer only; this is exchange name. Exchange name is + * Default in case of empty string. */ + message_params.messaging_destination_publish_name + = Z_STRVAL_P(amqp_exchange); + } else { + /* + * For consumer, this is exchange name. Exchange name is Default in + * case of empty string. + */ + if (nr_php_is_zval_valid_string(amqp_exchange)) { + message_params.messaging_destination_publish_name = "Default"; + } + } + + amqp_routing_key + = nr_php_get_zval_object_property(*retval_ptr, "routingKey"); + if (nr_php_is_zval_non_empty_string(amqp_routing_key)) { + message_params.messaging_destination_routing_key + = Z_STRVAL_P(amqp_routing_key); + } + + nr_php_amqplib_retrieve_dt_headers(*retval_ptr); + } + + /* Now create and end the instrumented segment as a message segment. */ + /* + * By this point, it's been determined that this call will be instrumented + * so only create the message segment now, grab the parent segment start + * time, add our message segment attributes/metrics then close the newly + * created message segment. + */ + message_segment = nr_segment_start(NRPRG(txn), NULL, NULL); + + if (NULL == message_segment) { + goto end; + } + + /* re-use start time from auto_segment started in func_begin */ + message_segment->start_time = auto_segment->start_time; + + nr_segment_message_end(&message_segment, &message_params); + +end: + /* + * Because we had to strdup values to persist them beyond + * NR_PHP_WRAPPER_CALL, now we destroy them. There isn't a separate function + * to destroy all since some of the params are string literals and we don't + * want to strdup everything if we don't have to. RabbitMQ basic_get PHP 7.x + * will only strdup server_address and destination_name. + */ + // amber make these peristent for all since retval of null clears the values + // from the cxn + UNDO_PERSISTENCE(message_params.server_address); + UNDO_PERSISTENCE(message_params.destination_name); +} +NR_PHP_WRAPPER_END + +void nr_php_amqplib_enable() { + /* + * Set the UNKNOWN package first, so it doesn't overwrite what we find with + * nr_php_amqplib_handle_version. + */ + if (NRINI(vulnerability_management_package_detection_enabled)) { + nr_txn_add_php_package(NRPRG(txn), PHP_PACKAGE_NAME, + PHP_PACKAGE_VERSION_UNKNOWN); + } + + /* Extract the version */ + nr_php_amqplib_handle_version(); + nr_php_amqplib_ensure_class(); + +#if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO /* less than PHP8.0 */ + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("PhpAmqpLib\\Channel\\AMQPChannel::basic_publish"), + nr_rabbitmq_basic_publish_before, nr_rabbitmq_basic_publish, + nr_rabbitmq_basic_publish); + + nr_php_wrap_user_function_before_after_clean( + NR_PSTR("PhpAmqpLib\\Channel\\AMQPChannel::basic_get"), NULL, + nr_rabbitmq_basic_get, nr_rabbitmq_basic_get); +#else + nr_php_wrap_user_function( + NR_PSTR("PhpAmqpLib\\Channel\\AMQPChannel::basic_publish"), + nr_rabbitmq_basic_publish); + + nr_php_wrap_user_function( + NR_PSTR("PhpAmqpLib\\Channel\\AMQPChannel::basic_get"), + nr_rabbitmq_basic_get); +#endif +} diff --git a/agent/lib_php_amqplib.h b/agent/lib_php_amqplib.h new file mode 100644 index 000000000..b4e565b40 --- /dev/null +++ b/agent/lib_php_amqplib.h @@ -0,0 +1,34 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Functions relating to instrumenting AWS-SDK-PHP. + */ +#ifndef LIB_PHP_AMQPLIB +#define LIB_PHP_AMQPLIB + +#define RABBITMQ_LIBRARY_NAME "RabbitMQ" +#define RABBITMQ_MESSAGING_SYSTEM "rabbitmq" + +#define AMQP_CONSTRUCT_PARAMS_SERVER_INDEX 0 +#define AMQP_CONSTRUCT_PARAMS_PORT_INDEX 1 + +/* + * Purpose : Enable the library after detection. + * + * Params : None + * + * Returns : None + */ +extern void nr_aws_php_amqplib_enable(); + +/* + * Purpose : Detect the version and create package and metrics. + * + * Params : None + * + * Returns : None + */ +extern void nr_php_amqplib_handle_version(); + +#endif /* LIB_PHP_AMQPLIB */ diff --git a/agent/lib_phpunit.c b/agent/lib_phpunit.c index e41c55b29..4050bf969 100644 --- a/agent/lib_phpunit.c +++ b/agent/lib_phpunit.c @@ -14,6 +14,8 @@ #include "util_logging.h" #include "util_strings.h" +#define PHP_PACKAGE_NAME "phpunit/phpunit" + /* * PHPUnit instrumentation * ======================= @@ -697,7 +699,10 @@ void nr_phpunit_enable(TSRMLS_D) { nr_phpunit_instrument_testresult_adderror TSRMLS_CC); if (NRINI(vulnerability_management_package_detection_enabled)) { - nr_txn_add_php_package(NRPRG(txn), "phpunit/phpunit", + nr_txn_add_php_package(NRPRG(txn), PHP_PACKAGE_NAME, PHP_PACKAGE_VERSION_UNKNOWN); } + + nr_txn_suggest_package_supportability_metric(NRPRG(txn), PHP_PACKAGE_NAME, + PHP_PACKAGE_VERSION_UNKNOWN); } diff --git a/agent/lib_predis.c b/agent/lib_predis.c index 862649d3b..607e11ed2 100644 --- a/agent/lib_predis.c +++ b/agent/lib_predis.c @@ -654,8 +654,10 @@ NR_PHP_WRAPPER(nr_predis_client_construct) { // Add php package to transaction nr_txn_add_php_package(NRPRG(txn), PHP_PACKAGE_NAME, version); } - nr_fw_support_add_package_supportability_metric(NRPRG(txn), PHP_PACKAGE_NAME, - version); + + nr_txn_suggest_package_supportability_metric(NRPRG(txn), PHP_PACKAGE_NAME, + version); + nr_free(version); /* @@ -764,7 +766,9 @@ NR_PHP_WRAPPER(nr_predis_webdisconnection_executeCommand_before) { nr_segment_t* segment = NULL; segment = nr_segment_start(NRPRG(txn), NULL, NULL); - segment->wraprec = auto_segment->wraprec; + if (NULL != segment) { + segment->wraprec = auto_segment->wraprec; + } } NR_PHP_WRAPPER_END @@ -894,5 +898,4 @@ void nr_predis_enable(TSRMLS_D) { NR_PSTR("Predis\\Connection\\WebdisConnection::executeCommand"), nr_predis_webdisconnection_executeCommand TSRMLS_CC); #endif /* OAPI */ - } diff --git a/agent/newrelic-install.sh b/agent/newrelic-install.sh index 68c2d1c45..8ff9cd3ed 100755 --- a/agent/newrelic-install.sh +++ b/agent/newrelic-install.sh @@ -327,18 +327,18 @@ check_file "${ilibdir}/scripts/newrelic.ini.template" # MAKE SURE TO UPDATE THIS LIST WHENEVER SUPPORT IS ADDED OR REMOVED # FOR A PHP VERSION # Currently supported versions: -# (7.0, 7.1, 7.2, 7.3, 7.4) +# (7.2, 7.3, 7.4) # for x64 if [ ${arch} = x64 ]; then -for pmv in "20151012" "20160303" "20170718" "20180731" "20190902"; do +for pmv in "20170718" "20180731" "20190902"; do check_file "${ilibdir}/agent/${arch}/newrelic-${pmv}.so" done fi # Currently supported versions: -# (8.0, 8.1, 8.2, 8.3) +# (8.0, 8.1, 8.2, 8.3, 8.4) # for x64 and aarch64 if [ ${arch} = x64 ] || [ ${arch} = aarch64 ]; then - for pmv in "20200930" "20210902" "20220829" "20230831"; do + for pmv in "20200930" "20210902" "20220829" "20230831" "20240924"; do check_file "${ilibdir}/agent/${arch}/newrelic-${pmv}.so" done fi @@ -535,8 +535,6 @@ add_to_path /usr/local/php add_to_path /usr/local/php/bin add_to_path /usr/local/zend/bin -add_to_path /usr/local/php-7.0/bin -add_to_path /usr/local/php-7.1/bin add_to_path /usr/local/php-7.2/bin add_to_path /usr/local/php-7.3/bin add_to_path /usr/local/php-7.4/bin @@ -544,12 +542,11 @@ add_to_path /usr/local/php-8.0/bin add_to_path /usr/local/php-8.1/bin add_to_path /usr/local/php-8.2/bin add_to_path /usr/local/php-8.3/bin +add_to_path /usr/local/php-8.4/bin add_to_path /opt/local/bin add_to_path /usr/php/bin -add_to_path /usr/php-7.0/bin -add_to_path /usr/php-7.1/bin add_to_path /usr/php-7.2/bin add_to_path /usr/php-7.3/bin add_to_path /usr/php-7.4/bin @@ -557,9 +554,8 @@ add_to_path /usr/php-8.0/bin add_to_path /usr/php-8.1/bin add_to_path /usr/php-8.2/bin add_to_path /usr/php-8.3/bin +add_to_path /usr/php-8.4/bin -add_to_path /usr/php/7.0/bin -add_to_path /usr/php/7.1/bin add_to_path /usr/php/7.2/bin add_to_path /usr/php/7.3/bin add_to_path /usr/php/7.4/bin @@ -567,12 +563,11 @@ add_to_path /usr/php/8.0/bin add_to_path /usr/php/8.1/bin add_to_path /usr/php/8.2/bin add_to_path /usr/php/8.3/bin +add_to_path /usr/php/8.4/bin add_to_path /opt/php/bin add_to_path /opt/zend/bin -add_to_path /opt/php-7.0/bin -add_to_path /opt/php-7.1/bin add_to_path /opt/php-7.2/bin add_to_path /opt/php-7.3/bin add_to_path /opt/php-7.4/bin @@ -580,6 +575,7 @@ add_to_path /opt/php-8.0/bin add_to_path /opt/php-8.1/bin add_to_path /opt/php-8.2/bin add_to_path /opt/php-8.3/bin +add_to_path /opt/php-8.4/bin if [ -n "${NR_INSTALL_PATH}" ]; then oIFS="${IFS}" @@ -852,8 +848,8 @@ disp_get_php_list() { set_osdifile() { osdifile= - if [ -n "${NR_INSTALL_INITFILE}" ]; then - osdifile="${NR_INSTALL_INITFILE}" + if [ -n "${NR_INSTALL_INITSCRIPT}" ]; then + osdifile="${NR_INSTALL_INITSCRIPT}" fi if [ "${ostype}" = "darwin" ]; then : ${osdifile:=/usr/bin/newrelic-daemon-service} @@ -1037,12 +1033,6 @@ for this copy of PHP. We apologize for the inconvenience. fi case "${pi_ver}" in - 7.0.*) - ;; - - 7.1.*) - ;; - 7.2.*) ;; @@ -1066,7 +1056,11 @@ for this copy of PHP. We apologize for the inconvenience. 8.3.*) pi_php8="yes" - ;; + ;; + + 8.4.*) + pi_php8="yes" + ;; *) error "unsupported version '${pi_ver}' of PHP found at: @@ -1239,8 +1233,6 @@ does not exist. This particular instance of PHP will be skipped. # pi_modver= case "${pi_ver}" in - 7.0.*) pi_modver="20151012" ;; - 7.1.*) pi_modver="20160303" ;; 7.2.*) pi_modver="20170718" ;; 7.3.*) pi_modver="20180731" ;; 7.4.*) pi_modver="20190902" ;; @@ -1248,6 +1240,7 @@ does not exist. This particular instance of PHP will be skipped. 8.1.*) pi_modver="20210902" ;; 8.2.*) pi_modver="20220829" ;; 8.3.*) pi_modver="20230831" ;; + 8.4.*) pi_modver="20240924" ;; esac log "${pdir}: pi_modver=${pi_modver}" @@ -1387,6 +1380,25 @@ EOF if [ -d "${cfg_pfx}/fpm/conf.d" ]; then pi_inidir_dso="${cfg_pfx}/fpm/conf.d" fi + + # + # Debian can use a mods-available directory to store the ini files. + # It creates a symlink from the ini file in the conf.d directory that + # our installer can fail to find (because the symlink is prefixed with + # "20-" (notably the number can change based on configurations). + # While this install script will not install into the mods-available + # directory, our .deb installer can. Therefore, we want to detect if + # newrelic has previously been installed in the mods-available directory + # so that we do not create an additional ini file -- which would result in + # the conf.d directory having both newrelic.ini and 20-newrelic.ini. + # + + if [ -d "${cfg_pfx}/mods-available" -a -f "${cfg_pfx}/mods-available/newrelic.ini" ]; then + pi_inidir_cli="${cfg_pfx}/mods-available" + if [ -n "${pi_inidir_dso}" ]; then + pi_inidir_dso="${cfg_pfx}/mods-available" + fi + fi fi done @@ -1780,6 +1792,10 @@ EOF set_osdifile if [ -z "${ispkg}" ]; then + # ensure target directory exists + if [ ! -d "$(dirname ${osdifile})" ]; then + logcmd mkdir -p -m 0755 "$(dirname ${osdifile})" + fi if logcmd cp -f "${ilibdir}/scripts/init.${ostype}" "${osdifile}"; then logcmd chmod 755 "${osdifile}" || { fatal "failed to set permissions on ${osdifile}" @@ -1798,6 +1814,10 @@ EOF fi if [ -n "${sysconf}" -a ! -f "${sysconf}" ]; then + # ensure target directory exists + if [ ! -d "$(dirname ${sysconf})" ]; then + logcmd mkdir -p -m 0755 "$(dirname ${sysconf})" + fi if logcmd cp -f "${ilibdir}/scripts/newrelic.sysconfig" "${sysconf}"; then logcmd chmod 755 "${sysconf}" || { fatal "failed to set permissions on ${sysconf}" diff --git a/agent/php_api.c b/agent/php_api.c index f9dc5c903..865344442 100644 --- a/agent/php_api.c +++ b/agent/php_api.c @@ -58,10 +58,30 @@ void nr_php_api_add_supportability_metric(const char* name TSRMLS_DC) { /* * Purpose : (New Relic API) Pretend that there is an error at this exact spot. - * Useful for business logic errors. newrelic_notice_error($errstr) + * Useful for business logic errors. + * - newrelic_notice_error($errstr) + * - $errstr : string : The error message to record * - newrelic_notice_error($exception) + * - $exception : object : The exception to use to record the exception + * NOTE: This version is compatible with being a callback for set_exception_handler() * - newrelic_notice_error($errstr,$exception) + * - $errstr : string : The error message to record + * - $exception : object : The exception to use to record the exception + * NOTE: The $errstr value is ignored! Started with agent version 4.23 + * - newrelic_notice_error($errno,$errstr,$fname,$line_nr) + * - $errno : int : The error number + * - $errstr : string : The error message + * - $fname : string : The filename where the error occurred + * - $line_nr : int : The line number where the error occurred + * NOTE: This version is compatible with being a callback for set_error_handler() for PHP 8+ * - newrelic_notice_error($errno,$errstr,$fname,$line_nr,$ctx) + * - $errno : int : The error number + * - $errstr : string : The error message + * - $fname : string : The filename where the error occurred + * - $line_nr : int : The line number where the error occurred + * - $ctx : array : The context of the error + * NOTE: This version is compatible with being a callback for set_error_handler() for PHP < 8 + * The $ctx is ignored! */ #ifdef TAGS void zif_newrelic_notice_error(void); /* ctags landing pad only */ @@ -141,10 +161,15 @@ PHP_FUNCTION(newrelic_notice_error) { } break; + case 4: case 5: + /* PHP 8+ will only pass the first 4 parameters so the 5th parameter is + * declared to be optional. Also this parameter is completely ignored + * so it doesn't matter if it is passed or not. + */ if (FAILURE == zend_parse_parameters_ex(ZEND_PARSE_PARAMS_QUIET, - ZEND_NUM_ARGS() TSRMLS_CC, "lsslz!", + ZEND_NUM_ARGS() TSRMLS_CC, "lssl|z!", &ignore1, &errormsgstr, &errormsglen, &ignore2, &ignore3, &ignore4, &ignore5)) { nrl_debug(NRL_API, "newrelic_notice_error: invalid five arguments"); @@ -153,7 +178,7 @@ PHP_FUNCTION(newrelic_notice_error) { break; default: - nrl_debug(NRL_API, "newrelic_notice_error: invalid number of arguments"); + nrl_debug(NRL_API, "newrelic_notice_error: invalid number of arguments: %d", ZEND_NUM_ARGS()); RETURN_NULL(); } diff --git a/agent/php_api_internal.h b/agent/php_api_internal.h index 61db36648..f8ae9a653 100644 --- a/agent/php_api_internal.h +++ b/agent/php_api_internal.h @@ -16,6 +16,8 @@ */ extern PHP_FUNCTION(newrelic_get_request_metadata); +extern PHP_FUNCTION(newrelic_get_security_metadata); + #ifdef ENABLE_TESTING_API /* diff --git a/agent/php_execute.c b/agent/php_execute.c index e4300d902..41c430d11 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -331,15 +331,8 @@ typedef struct _nr_framework_table_t { */ // clang-format: off static const nr_framework_table_t all_frameworks[] = { - /* - * Watch out: - * cake1.2 and cake1.3 use a subdirectory named 'cake' (lower case) - * cake2.0 and on use a subdirectory named 'Cake' (upper case file name) - */ - {"CakePHP", "cakephp", NR_PSTR("cake/libs/object.php"), nr_cakephp_special_1, - nr_cakephp_enable_1, NR_FW_CAKEPHP}, - {"CakePHP", "cakephp", NR_PSTR("cake/core/app.php"), nr_cakephp_special_2, - nr_cakephp_enable_2, NR_FW_CAKEPHP}, + {"CakePHP", "cakephp", NR_PSTR("cakephp/src/core/functions.php"), 0, + nr_cakephp_enable, NR_FW_CAKEPHP}, /* * Watch out: frameworks or CMS' build on top of CodeIgniter might not get @@ -411,7 +404,7 @@ static const nr_framework_table_t all_frameworks[] = { NR_PSTR("symfony/bundle/frameworkbundle/frameworkbundle.php"), 0, nr_symfony2_enable, NR_FW_SYMFONY2}, /* also Symfony 3 */ {"Symfony4", "symfony4", NR_PSTR("http-kernel/httpkernel.php"), 0, - nr_symfony4_enable, NR_FW_SYMFONY4}, /* also Symfony 5 */ + nr_symfony4_enable, NR_FW_SYMFONY4}, /* also Symfony 5/6/7 */ {"WordPress", "wordpress", NR_PSTR("wp-config.php"), 0, nr_wordpress_enable, NR_FW_WORDPRESS}, @@ -484,13 +477,24 @@ typedef struct _nr_library_table_t { */ // clang-format: off static nr_library_table_t libraries[] = { + /* AWS-SDK-PHP 3 */ + {"AWS-SDK-PHP", NR_PSTR("aws-sdk-php/src/awsclient.php"), nr_aws_sdk_php_enable}, + + /* Doctrine < 2.18 */ {"Doctrine 2", NR_PSTR("doctrine/orm/query.php"), nr_doctrine2_enable}, + /* Doctrine 2.18 reworked the directory structure */ + {"Doctrine 2", NR_PSTR("doctrine/orm/src/query.php"), nr_doctrine2_enable}, + {"Guzzle 3", NR_PSTR("guzzle/http/client.php"), nr_guzzle3_enable}, {"Guzzle 4-5", NR_PSTR("hasemitterinterface.php"), nr_guzzle4_enable}, {"Guzzle 6", NR_PSTR("guzzle/src/functions_include.php"), nr_guzzle6_enable}, {"MongoDB", NR_PSTR("mongodb/src/client.php"), nr_mongodb_enable}, + /* php-amqplib RabbitMQ; PHP Agent supports php-amqplib >= 3.7 */ + {"php-amqplib", NR_PSTR("phpamqplib/connection/abstractconnection.php"), + nr_php_amqplib_enable}, + /* * The first path is for Composer installs, the second is for * /usr/local/bin. @@ -515,60 +519,6 @@ static nr_library_table_t libraries[] = { * with other frameworks or even without a framework at all. */ {"Laminas_Http", NR_PSTR("laminas-http/src/client.php"), nr_laminas_http_enable}, - - /* - * Other frameworks, detected only, but not specifically - * instrumented. We detect these as libraries so that we don't prevent - * detection of a supported framework or library later (since a transaction - * can only have one framework). - */ - {"Aura1", NR_PSTR("aura/framework/system.php"), NULL}, - {"Aura2", NR_PSTR("aura/di/src/containerinterface.php"), NULL}, - {"Aura3", NR_PSTR("aura/di/src/containerconfiginterface.php"), NULL}, - {"CakePHP3", NR_PSTR("cakephp/src/core/functions.php"), NULL}, - {"Fuel", NR_PSTR("fuel/core/classes/fuel.php"), NULL}, - {"Lithium", NR_PSTR("lithium/core/libraries.php"), NULL}, - {"Phpbb", NR_PSTR("phpbb/request/request.php"), NULL}, - {"Phpixie2", NR_PSTR("phpixie/core/classes/phpixie/pixie.php"), NULL}, - {"Phpixie3", NR_PSTR("phpixie/framework.php"), NULL}, - {"React", NR_PSTR("react/event-loop/src/loopinterface.php"), NULL}, - {"SilverStripe", NR_PSTR("injector/silverstripeinjectioncreator.php"), NULL}, - {"SilverStripe4", NR_PSTR("silverstripeserviceconfigurationlocator.php"), NULL}, - {"Typo3", NR_PSTR("classes/typo3/flow/core/bootstrap.php"), NULL}, - {"Typo3", NR_PSTR("typo3/sysext/core/classes/core/bootstrap.php"), NULL}, - - /* - * Other CMS (content management systems), detected only, but - * not specifically instrumented. - */ - {"Moodle", NR_PSTR("moodlelib.php"), NULL}, - /* - * It is likely that this will never be found, since the CodeIgniter.php - * will get loaded first, and as such mark this transaction as belonging to - * CodeIgniter, and not Expession Engine. - */ - {"ExpressionEngine", NR_PSTR("system/expressionengine/config/config.php"), NULL}, - /* - * ExpressionEngine 5, however, has a very obvious file we can look for. - */ - {"ExpressionEngine5", NR_PSTR("expressionengine/boot/boot.php"), NULL}, - /* - * DokuWiki uses doku.php as an entry point, but has other files that are - * loaded directly that this won't pick up. That's probably OK for - * supportability metrics, but we'll add the most common name for the - * configuration file as well just in case. - */ - {"DokuWiki", NR_PSTR("doku.php"), NULL}, - {"DokuWiki", NR_PSTR("conf/dokuwiki.php"), NULL}, - - /* - * SugarCRM no longer has a community edition, so this likely only works - * with older versions. - */ - {"SugarCRM", NR_PSTR("sugarobjects/sugarconfig.php"), NULL}, - - {"Xoops", NR_PSTR("class/xoopsload.php"), NULL}, - {"E107", NR_PSTR("e107_handlers/e107_class.php"), NULL}, }; // clang-format: on @@ -583,9 +533,6 @@ static nr_library_table_t logging_frameworks[] = { /* laminas-log - Logging for PHP */ {"laminas-log", NR_PSTR("laminas-log/src/logger.php"), NULL}, /* cakephp-log - Logging for PHP */ - {"cakephp-log", NR_PSTR("cakephp/log/log.php"), NULL}, - /* Analog - Logging for PHP */ - {"Analog", NR_PSTR("analog/analog.php"), NULL}, }; // clang-format: on @@ -596,14 +543,17 @@ static size_t num_logging_frameworks typedef struct _nr_vuln_mgmt_table_t { const char* package_name; const char* file_to_check; + size_t file_to_check_len; nr_vuln_mgmt_enable_fn_t enable; } nr_vuln_mgmt_table_t; /* Note that all paths should be in lowercase. */ +// clang-format: off static const nr_vuln_mgmt_table_t vuln_mgmt_packages[] = { - {"Drupal", "drupal/component/dependencyinjection/container.php", nr_drupal_version}, - {"Wordpress", "wp-includes/version.php", nr_wordpress_version}, + {"Drupal", NR_PSTR("drupal/component/dependencyinjection/container.php"), nr_drupal_version}, + {"Wordpress", NR_PSTR("wp-includes/version.php"), nr_wordpress_version}, }; +// clang-format: on static const size_t num_packages = sizeof(vuln_mgmt_packages) / sizeof(nr_vuln_mgmt_table_t); @@ -924,6 +874,40 @@ static void nr_execute_handle_library(const char* filename, } } +static void nr_execute_handle_autoload(const char* filename, + const size_t filename_len) { +#define AUTOLOAD_MAGIC_FILE "vendor/autoload.php" +#define AUTOLOAD_MAGIC_FILE_LEN (sizeof(AUTOLOAD_MAGIC_FILE) - 1) + + if (!NRINI(vulnerability_management_package_detection_enabled)) { + // do nothing when vulnerability management package detection is disabled + return; + } + + if (!NRINI(vulnerability_management_composer_api_enabled)) { + // do nothing when use of composer to collect package info is disabled + return; + } + + if (NRPRG(txn)->composer_info.autoload_detected) { + // autoload already handled + return; + } + + if (!nr_striendswith(STR_AND_LEN(filename), AUTOLOAD_MAGIC_FILE, + AUTOLOAD_MAGIC_FILE_LEN)) { + // not an autoload file + return; + } + + nrl_debug(NRL_FRAMEWORK, "detected autoload with %s, which ends with %s", + filename, AUTOLOAD_MAGIC_FILE); + NRPRG(txn)->composer_info.autoload_detected = true; + nr_fw_support_add_library_supportability_metric(NRPRG(txn), "Autoloader"); + + nr_composer_handle_autoload(filename); +} + static void nr_execute_handle_logging_framework(const char* filename, const size_t filename_len TSRMLS_DC) { @@ -949,28 +933,22 @@ static void nr_execute_handle_logging_framework(const char* filename, } } -#undef STR_AND_LEN - -static void nr_execute_handle_package(const char* filename) { - if (NULL == filename || 0 >= nr_strlen(filename)) { - nrl_verbosedebug(NRL_FRAMEWORK, "%s: The file name is NULL", - __func__); - return; - } - char* filename_lower = nr_string_to_lowercase(filename); +static void nr_execute_handle_package(const char* filename, + const size_t filename_len) { size_t i = 0; for (i = 0; i < num_packages; i++) { - if (nr_stridx(filename_lower, vuln_mgmt_packages[i].file_to_check) >= 0) { + if (nr_striendswith(STR_AND_LEN(filename), + STR_AND_LEN(vuln_mgmt_packages[i].file_to_check))) { if (NULL != vuln_mgmt_packages[i].enable) { vuln_mgmt_packages[i].enable(); } } } - - nr_free(filename_lower); } +#undef STR_AND_LEN + /* * Purpose : Detect library and framework usage from a PHP file. * @@ -992,9 +970,10 @@ static void nr_php_user_instrumentation_from_file(const char* filename, nr_execute_handle_framework(all_frameworks, num_all_frameworks, filename, filename_len TSRMLS_CC); nr_execute_handle_library(filename, filename_len TSRMLS_CC); + nr_execute_handle_autoload(filename, filename_len); nr_execute_handle_logging_framework(filename, filename_len TSRMLS_CC); if (NRINI(vulnerability_management_package_detection_enabled)) { - nr_execute_handle_package(filename); + nr_execute_handle_package(filename, filename_len); } } @@ -1280,12 +1259,27 @@ static inline void nr_php_execute_segment_add_metric( bool create_metric) { char buf[METRIC_NAME_MAX_LEN]; - nr_php_execute_metadata_metric(metadata, buf, sizeof(buf)); +/* + * If the name is not already set, use the metadata to get the class and + * function name to name the metric and the segment. + * + * If the segment name is already set, use that to name the metric. + */ + if (!segment->name) { + nr_php_execute_metadata_metric(metadata, buf, sizeof(buf)); - if (create_metric) { - nr_segment_add_metric(segment, buf, true); + if (create_metric) { + nr_segment_add_metric(segment, buf, true); + } + nr_segment_set_name(segment, buf); + } else { + /* Segment already named, so only create the metric with the name. */ + if (create_metric) { + nr_segment_add_metric( + segment, nr_string_get(segment->txn->trace_strings, segment->name), + true); + } } - nr_segment_set_name(segment, buf); } /* diff --git a/agent/php_includes.h b/agent/php_includes.h index 72c25a26a..da255fdbc 100644 --- a/agent/php_includes.h +++ b/agent/php_includes.h @@ -54,6 +54,7 @@ #define ZEND_8_1_X_API_NO 20210902 #define ZEND_8_2_X_API_NO 20220829 #define ZEND_8_3_X_API_NO 20230831 +#define ZEND_8_4_X_API_NO 20240924 #if ZEND_MODULE_API_NO >= ZEND_8_0_X_API_NO /* PHP8+ */ #include "Zend/zend_observer.h" diff --git a/agent/php_internal_instrument.c b/agent/php_internal_instrument.c index 821379edb..e37510e32 100644 --- a/agent/php_internal_instrument.c +++ b/agent/php_internal_instrument.c @@ -13,8 +13,10 @@ #include "php_explain_mysqli.h" #include "php_file_get_contents.h" #include "php_globals.h" +#include "php_hash.h" #include "php_httprequest_send.h" #include "php_internal_instrument.h" +#include "php_memcached.h" #include "php_mysql.h" #include "php_mysqli.h" #include "php_pdo.h" @@ -1531,6 +1533,57 @@ NR_INNER_WRAPPER(memcache_function) { INTERNAL_FUNCTION_PARAM_PASSTHRU); } +NR_INNER_WRAPPER(memcached_add_server) { + char* host = NULL; + nr_string_len_t host_len = 0; + zend_long port = 0; + zend_long weight = 0; + int zcaught = 0; + + if (SUCCESS + == zend_parse_parameters_ex( + ZEND_PARSE_PARAMS_QUIET, ZEND_NUM_ARGS(), "s|ll", &host, + &host_len, &port, &weight) && + NULL != host) { + nr_php_memcached_create_instance_metric(host, port); + } + zcaught = nr_zend_call_old_handler(nr_wrapper->oldhandler, + INTERNAL_FUNCTION_PARAM_PASSTHRU); + if (zcaught) { + zend_bailout(); + /* NOTREACHED */ + } +} + +NR_INNER_WRAPPER(memcached_add_servers) { + zval* servers = NULL; + zval* server = NULL; + int zcaught = 0; + + if (SUCCESS + == zend_parse_parameters_ex( + ZEND_PARSE_PARAMS_QUIET, ZEND_NUM_ARGS(), "a", &servers)) { + if (NULL != servers && Z_TYPE_P(servers) == IS_ARRAY) { + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(servers), server) { + zval* host = nr_php_zend_hash_index_find(Z_ARRVAL_P(server), 0); + zval* port = nr_php_zend_hash_index_find(Z_ARRVAL_P(server), 1); + if (nr_php_is_zval_valid_string(host) && + nr_php_is_zval_valid_integer(port)) { + nr_php_memcached_create_instance_metric(Z_STRVAL_P(host), Z_LVAL_P(port)); + } + } + ZEND_HASH_FOREACH_END(); + } + } + zcaught = nr_zend_call_old_handler(nr_wrapper->oldhandler, + INTERNAL_FUNCTION_PARAM_PASSTHRU); + + if (zcaught) { + zend_bailout(); + /* NOTREACHED */ + } +} + /* * Handle * bool redis::connect ( string $host[, int $port = 6379 ... ] ) @@ -3098,6 +3151,8 @@ NR_OUTER_WRAPPER(memcached_set) NR_OUTER_WRAPPER(memcached_setbykey) NR_OUTER_WRAPPER(memcached_setmulti) NR_OUTER_WRAPPER(memcached_setmultibykey) +NR_OUTER_WRAPPER(memcached_addserver) +NR_OUTER_WRAPPER(memcached_addservers) NR_OUTER_WRAPPER(redis_append) NR_OUTER_WRAPPER(redis_bitcount) @@ -3511,6 +3566,10 @@ void nr_php_generate_internal_wrap_records(void) { memcache_function, 0, "set") NR_INTERNAL_WRAPREC("memcached::setmultibykey", memcached_setmultibykey, memcache_function, 0, "set") + NR_INTERNAL_WRAPREC("memcached::addserver", memcached_addserver, + memcached_add_server, 0, 0); + NR_INTERNAL_WRAPREC("memcached::addservers", memcached_addservers, + memcached_add_servers, 0, 0); NR_INTERNAL_WRAPREC("redis::connect", redis_connect, redis_connect, 0, "connect") @@ -3781,9 +3840,37 @@ void nr_php_generate_internal_wrap_records(void) { NR_INTERNAL_WRAPREC("sqlite3::exec", sqlite3_exec, sqlite3, 0, 0) NR_INTERNAL_WRAPREC("pdo::__construct", pdo_construct, pdo_construct, 0, 0) +#if ZEND_MODULE_API_NO >= ZEND_8_4_X_API_NO + NR_INTERNAL_WRAPREC("pdo\\firebird::__construct", pdo_construct, pdo_construct, 0, 0) + NR_INTERNAL_WRAPREC("pdo\\mysql::__construct", pdo_construct, pdo_construct, 0, 0) + NR_INTERNAL_WRAPREC("pdo\\odbc::__construct", pdo_construct, pdo_construct, 0, 0) + NR_INTERNAL_WRAPREC("pdo\\pgsql::__construct", pdo_construct, pdo_construct, 0, 0) + NR_INTERNAL_WRAPREC("pdo\\sqlite::__construct", pdo_construct, pdo_construct, 0, 0) +#endif NR_INTERNAL_WRAPREC("pdo::query", pdo_query, pdo_query, 0, 0) +#if ZEND_MODULE_API_NO >= ZEND_8_4_X_API_NO + NR_INTERNAL_WRAPREC("pdo\\firebird::query", pdo_query, pdo_query, 0, 0) + NR_INTERNAL_WRAPREC("pdo\\mysql::query", pdo_query, pdo_query, 0, 0) + NR_INTERNAL_WRAPREC("pdo\\odbc::query", pdo_query, pdo_query, 0, 0) + NR_INTERNAL_WRAPREC("pdo\\pgsql::query", pdo_query, pdo_query, 0, 0) + NR_INTERNAL_WRAPREC("pdo\\sqlite::query", pdo_query, pdo_query, 0, 0) +#endif NR_INTERNAL_WRAPREC("pdo::exec", pdo_exec, pdo_exec, 0, 0) +#if ZEND_MODULE_API_NO >= ZEND_8_4_X_API_NO + NR_INTERNAL_WRAPREC("pdo\\firebird::exec", pdo_exec, pdo_exec, 0, 0) + NR_INTERNAL_WRAPREC("pdo\\mysql::exec", pdo_exec, pdo_exec, 0, 0) + NR_INTERNAL_WRAPREC("pdo\\odbc::exec", pdo_exec, pdo_exec, 0, 0) + NR_INTERNAL_WRAPREC("pdo\\pgsql::exec", pdo_exec, pdo_exec, 0, 0) + NR_INTERNAL_WRAPREC("pdo\\sqlite::exec", pdo_exec, pdo_exec, 0, 0) +#endif NR_INTERNAL_WRAPREC("pdo::prepare", pdo_prepare, pdo_prepare, 0, 0) +#if ZEND_MODULE_API_NO >= ZEND_8_4_X_API_NO + NR_INTERNAL_WRAPREC("pdo\\firebird::prepare", pdo_prepare, pdo_prepare, 0, 0) + NR_INTERNAL_WRAPREC("pdo\\mysql::prepare", pdo_prepare, pdo_prepare, 0, 0) + NR_INTERNAL_WRAPREC("pdo\\odbc::prepare", pdo_prepare, pdo_prepare, 0, 0) + NR_INTERNAL_WRAPREC("pdo\\pgsql::prepare", pdo_prepare, pdo_prepare, 0, 0) + NR_INTERNAL_WRAPREC("pdo\\sqlite::prepare", pdo_prepare, pdo_prepare, 0, 0) +#endif NR_INTERNAL_WRAPREC("pdostatement::execute", pdostmt_execute, pdostatement_execute, 0, 0) diff --git a/agent/php_memcached.c b/agent/php_memcached.c new file mode 100644 index 000000000..ee13e1e73 --- /dev/null +++ b/agent/php_memcached.c @@ -0,0 +1,34 @@ +/* + * Copyright 2020 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "php_memcached.h" +#include "nr_datastore_instance.h" +#include "php_agent.h" + +nr_datastore_instance_t* nr_php_memcached_create_datastore_instance( + const char* host_or_socket, + zend_long port) { + nr_datastore_instance_t* instance = NULL; + if (port == 0) { // local socket + instance = nr_datastore_instance_create("localhost", host_or_socket, NULL); + } else { + char* port_str = nr_formatf("%ld", (long)port); + instance = nr_datastore_instance_create(host_or_socket, port_str, NULL); + nr_free(port_str); + } + return instance; +} + +void nr_php_memcached_create_instance_metric( + const char* host_or_socket, + zend_long port) { + nr_datastore_instance_t* instance + = nr_php_memcached_create_datastore_instance(host_or_socket, port); + char* instance_metric = nr_formatf("Datastore/instance/Memcached/%s/%s", + instance->host, instance->port_path_or_id); + nrm_force_add(NRPRG(txn)->unscoped_metrics, instance_metric, 0); + nr_datastore_instance_destroy(&instance); + nr_free(instance_metric); +} diff --git a/agent/php_memcached.h b/agent/php_memcached.h new file mode 100644 index 000000000..f449030ea --- /dev/null +++ b/agent/php_memcached.h @@ -0,0 +1,35 @@ +/* + * Copyright 2020 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef PHP_MEMCACHED_HDR +#define PHP_MEMCACHED_HDR + +#include "nr_datastore_instance.h" +#include "php_includes.h" + +/* + * Purpose : Create a datastore instance metadata for a Memcached server. + * + * Params : 1. The memcached host or socket name as given to Memcached::addServer(). + * 2. The memcached port as given as given to Memcached::addServer(). + * + * Returns: nr_datastore_instance_t* that the caller is responsible for freeing + */ +nr_datastore_instance_t* nr_php_memcached_create_datastore_instance( + const char* host_or_socket, + zend_long port); + +/* + * Purpose : Create a memcached instance metric + * + * Params : 1. The memcached host or socket name as given to Memcached::addServer(). + * 2. The memcached port as given as given to Memcached::addServer(). + */ +extern void nr_php_memcached_create_instance_metric( + const char* host_or_socket, + zend_long port); + + +#endif diff --git a/agent/php_mysqli.c b/agent/php_mysqli.c index b17fd2491..24a321b02 100644 --- a/agent/php_mysqli.c +++ b/agent/php_mysqli.c @@ -398,42 +398,51 @@ static nr_status_t nr_php_mysqli_link_real_connect( zval* link, const nr_mysqli_metadata_link_t* metadata TSRMLS_DC) { zend_ulong argc = 0; + zend_ulong arg_required = 0; zval* argv[7] = {0}; zend_ulong i; zval* retval = NULL; -#define ADD_IF_INT_SET(args, argc, value) \ - if (value) { \ - args[argc] = nr_php_zval_alloc(); \ - ZVAL_LONG(args[argc], value); \ - argc++; \ - } - -#define ADD_IF_STR_SET(args, argc, value) \ - if (value) { \ - args[argc] = nr_php_zval_alloc(); \ - nr_php_zval_str(args[argc], value); \ - argc++; \ - } - - ADD_IF_STR_SET(argv, argc, +#define ADD_IF_INT_SET(null_ok, args, argc, value) \ + if (value) { \ + args[argc] = nr_php_zval_alloc(); \ + ZVAL_LONG(args[argc], value); \ + argc++; \ + } else if (true == null_ok) { \ + args[argc] = nr_php_zval_alloc(); \ + ZVAL_NULL(args[argc]); \ + argc++; \ + } + +#define ADD_IF_STR_SET(null_ok, args, argc, value) \ + if (value) { \ + args[argc] = nr_php_zval_alloc(); \ + nr_php_zval_str(args[argc], value); \ + argc++; \ + } else if (true == null_ok) { \ + args[argc] = nr_php_zval_alloc(); \ + ZVAL_NULL(args[argc]); \ + argc++; \ + } + + ADD_IF_STR_SET(false, argv, argc, nr_php_mysqli_strip_persistent_prefix(metadata->host)); - ADD_IF_STR_SET(argv, argc, metadata->user); - ADD_IF_STR_SET(argv, argc, metadata->password); + ADD_IF_STR_SET(false, argv, argc, metadata->user); + ADD_IF_STR_SET(false, argv, argc, metadata->password); /* * We can only add the remaining metadata fields if we already have three * arguments (host, user and password) above, lest we accidentally set the - * wrong positional argument to something it doesn't mean. + * wrong positional argument to something it doesn't mean. Note, prior + * to 7.4 not all args are nullable. */ + arg_required = argc; if (argc == 3) { - ADD_IF_STR_SET(argv, argc, metadata->database); - ADD_IF_INT_SET(argv, argc, metadata->port); - ADD_IF_STR_SET(argv, argc, metadata->socket); - ADD_IF_INT_SET(argv, argc, metadata->flags); - } - - retval = nr_php_call_user_func(link, "real_connect", argc, argv TSRMLS_CC); + ADD_IF_STR_SET(true, argv, argc, metadata->database); + ADD_IF_INT_SET(true, argv, argc, metadata->port); + ADD_IF_STR_SET(true, argv, argc, metadata->socket); + ADD_IF_INT_SET(false, argv, argc, metadata->flags); + } retval = nr_php_call_user_func(link, "real_connect", argc, argv TSRMLS_CC); for (i = 0; i < argc; i++) { nr_php_zval_free(&argv[i]); @@ -450,7 +459,7 @@ static nr_status_t nr_php_mysqli_link_real_connect( * If we didn't specify the database in the connection parameters, we need to * call mysqli::select_db here. */ - if (metadata->database && (argc < 4)) { + if (metadata->database && (arg_required < 3)) { zval* database = nr_php_zval_alloc(); nr_php_zval_str(database, metadata->database); diff --git a/agent/php_newrelic.c b/agent/php_newrelic.c index 210c04428..fb81d65b2 100644 --- a/agent/php_newrelic.c +++ b/agent/php_newrelic.c @@ -120,8 +120,8 @@ ZEND_BEGIN_ARG_INFO_EX(newrelic_arginfo_void, 0, 0, 0) ZEND_END_ARG_INFO() #endif /* PHP 8.0+ */ -ZEND_BEGIN_ARG_INFO_EX(newrelic_get_request_metadata_arginfo, 0, 0, 0) -ZEND_ARG_INFO(0, transport) +ZEND_BEGIN_ARG_INFO_EX(newrelic_get_request_metadata_arginfo, 0, 0, 0) +ZEND_ARG_INFO(0, transport) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_INFO_EX(newrelic_add_custom_parameter_arginfo, 0, 0, 2) @@ -223,7 +223,6 @@ ZEND_BEGIN_ARG_INFO_EX(newrelic_set_user_id_arginfo, 0, 0, 1) ZEND_ARG_INFO(0, uuid) ZEND_END_ARG_INFO() - ZEND_BEGIN_ARG_INFO_EX(newrelic_set_error_group_callback_arginfo, 0, 0, 1) ZEND_ARG_INFO(0, callback) ZEND_END_ARG_INFO() @@ -343,9 +342,11 @@ static zend_function_entry newrelic_functions[] = { #ifdef PHP8 PHP_FE(newrelic_get_linking_metadata, newrelic_arginfo_void) PHP_FE(newrelic_get_trace_metadata, newrelic_arginfo_void) + PHP_FE(newrelic_get_security_metadata, newrelic_arginfo_void) #else PHP_FE(newrelic_get_linking_metadata, 0) PHP_FE(newrelic_get_trace_metadata, 0) + PHP_FE(newrelic_get_security_metadata, 0) #endif /* PHP 8 */ /* * Integration test helpers diff --git a/agent/php_newrelic.h b/agent/php_newrelic.h index e114a6464..6afaf531a 100644 --- a/agent/php_newrelic.h +++ b/agent/php_newrelic.h @@ -542,6 +542,8 @@ nrinibool_t nrinibool_t distributed_tracing_exclude_newrelic_header; /* newrelic.distributed_tracing_exclude_newrelic_header */ +nrinibool_t + distributed_tracing_pad_trace_id; /* newrelic.distributed_tracing.pad_trace_id */ nrinibool_t span_events_enabled; /* newrelic.span_events_enabled */ nriniuint_t span_events_max_samples_stored; /* newrelic.span_events.max_samples_stored @@ -591,6 +593,14 @@ nrinibool_t nrinibool_t vulnerability_management_package_detection_enabled; /* newrelic.vulnerability_management.package_detection.enabled */ +nrinibool_t + vulnerability_management_composer_api_enabled; /* newrelic.vulnerability_management.composer_api.enabled */ + +/* + * Configuration options for recording Messaging APIs + */ +nrinibool_t + message_tracer_segment_parameters_enabled; /* newrelic.segment_tracer.segment_parameters.enabled */ #if ZEND_MODULE_API_NO < ZEND_7_4_X_API_NO /* diff --git a/agent/php_nrini.c b/agent/php_nrini.c index 3029392ad..ab91404da 100644 --- a/agent/php_nrini.c +++ b/agent/php_nrini.c @@ -2943,6 +2943,21 @@ STD_PHP_INI_ENTRY_EX("newrelic.distributed_tracing_exclude_newrelic_header", newrelic_globals, 0) +/* + * This setting is not documented and affects the length of the interally used + * trace id. This INI setting should not be modified unless requested by + * customer. + */ +STD_PHP_INI_ENTRY_EX("newrelic.distributed_tracing.pad_trace_id", + "", // default is false represented as an empty string - + // see notes about PHP INI parser + NR_PHP_REQUEST, + nr_boolean_mh, + distributed_tracing_pad_trace_id, + zend_newrelic_globals, + newrelic_globals, + nr_enabled_disabled_dh) + /** * Flag to turn the span events on/off * When on, the agent will create span events. Span events require that @@ -3140,6 +3155,26 @@ STD_PHP_INI_ENTRY_EX("newrelic.vulnerability_management.package_detection.enable newrelic_globals, nr_enabled_disabled_dh) +STD_PHP_INI_ENTRY_EX("newrelic.vulnerability_management.composer_api.enabled", + "0", + NR_PHP_REQUEST, + nr_boolean_mh, + vulnerability_management_composer_api_enabled, + zend_newrelic_globals, + newrelic_globals, + nr_enabled_disabled_dh) + +/* + * Messaging API + */ +STD_PHP_INI_ENTRY_EX("newrelic.message_tracer.segment_parameters.enabled", + "1", + NR_PHP_REQUEST, + nr_boolean_mh, + message_tracer_segment_parameters_enabled, + zend_newrelic_globals, + newrelic_globals, + nr_enabled_disabled_dh) PHP_INI_END() /* } */ void nr_php_register_ini_entries(int module_number TSRMLS_DC) { diff --git a/agent/php_txn.c b/agent/php_txn.c index 7e445c762..bc0ea6b7e 100644 --- a/agent/php_txn.c +++ b/agent/php_txn.c @@ -19,10 +19,12 @@ #include "nr_agent.h" #include "nr_commands.h" #include "nr_header.h" +#include "nr_php_packages.h" #include "nr_rum.h" #include "nr_segment_children.h" #include "nr_txn.h" #include "nr_version.h" +#include "fw_support.h" #include "util_labels.h" #include "util_logging.h" #include "util_memory.h" @@ -669,6 +671,92 @@ static void nr_php_txn_send_metrics_once(nrtxn_t* txn TSRMLS_DC) { #undef FMT_BOOL } +void nr_php_txn_create_agent_version_metric(nrtxn_t* txn) { + if (NULL == txn) { + return; + } + + nrm_force_add(NRTXN(unscoped_metrics), + "Supportability/PHP/AgentVersion/" NR_VERSION, 0); +} + +void nr_php_txn_create_php_version_metric(nrtxn_t* txn, const char* version) { + char* metric_name = NULL; + + if (NULL == txn) { + return; + } + + if (nr_strempty(version)) { + return; + } + + metric_name = nr_formatf("Supportability/PHP/Version/%s", version); + nrm_force_add(NRTXN(unscoped_metrics), metric_name, 0); + nr_free(metric_name); +} + +void nr_php_txn_create_agent_php_version_metrics(nrtxn_t* txn) { + char* version = NULL; + + if (NULL == txn) { + return; + } + nr_php_txn_create_agent_version_metric(txn); + + if (!nr_strempty(NR_PHP_PROCESS_GLOBALS(php_version))) { + version = NR_PHP_PROCESS_GLOBALS(php_version); + } else { + version = "unknown"; + } + + nr_php_txn_create_php_version_metric(txn, version); +} + +void nr_php_txn_php_package_create_major_metric(void* value, + const char* key, + size_t key_len, + void* user_data) { + nrtxn_t* txn = (nrtxn_t*)user_data; + nr_php_package_t* suggested = value; + nr_php_package_t* actual = NULL; + + (void)key; + (void)key_len; + + if (NULL == txn) { + return; + } + + if (NULL == suggested) { + return; + } + + /* see if the actual packages has a version we can use over the + * one provided with the suggested package + */ + actual + = nr_php_packages_get_package(txn->php_packages, suggested->package_name); + + nrl_verbosedebug( + NRL_INSTRUMENT, + "Creating PHP Package Supportability Metric for package " + "'%s', suggested version '%s', actual version '%s'", + NRSAFESTR(suggested->package_name), NRSAFESTR(suggested->package_version), + NRSAFESTR(NULL != actual ? actual->package_version : "NULL")); + nr_fw_support_add_package_supportability_metric( + txn, suggested->package_name, suggested->package_version, actual); +} + +void nr_php_txn_create_packages_major_metrics(nrtxn_t* txn) { + if (NULL == txn) { + return; + } + + nr_php_packages_iterate(txn->php_package_major_version_metrics_suggestions, + nr_php_txn_php_package_create_major_metric, txn); +} + nr_status_t nr_php_txn_begin(const char* appnames, const char* license TSRMLS_DC) { nrtxnopt_t opts; @@ -748,6 +836,8 @@ nr_status_t nr_php_txn_begin(const char* appnames, opts.allow_raw_exception_messages = NRINI(allow_raw_exception_messages); opts.custom_parameters_enabled = NRINI(custom_parameters_enabled); opts.distributed_tracing_enabled = NRINI(distributed_tracing_enabled); + opts.distributed_tracing_pad_trace_id + = NRINI(distributed_tracing_pad_trace_id); opts.distributed_tracing_exclude_newrelic_header = NRINI(distributed_tracing_exclude_newrelic_header); opts.span_events_enabled = NRINI(span_events_enabled); @@ -764,6 +854,8 @@ nr_status_t nr_php_txn_begin(const char* appnames, opts.log_forwarding_log_level = NRINI(log_forwarding_log_level); opts.log_events_max_samples_stored = NRINI(log_events_max_samples_stored); opts.log_metrics_enabled = NRINI(log_metrics_enabled); + opts.message_tracer_segment_parameters_enabled + = NRINI(message_tracer_segment_parameters_enabled); /* * Enable the behaviour whereby asynchronous time is discounted from the total @@ -1075,7 +1167,7 @@ nr_status_t nr_php_txn_end(int ignoretxn, int in_post_deactivate TSRMLS_DC) { #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)) { + while (NULL != segment && segment != NRTXN(segment_root)) { nr_segment_end(&segment); segment = nr_txn_get_current_segment(NRPRG(txn), NULL); } @@ -1118,6 +1210,12 @@ nr_status_t nr_php_txn_end(int ignoretxn, int in_post_deactivate TSRMLS_DC) { "Supportability/execute/allocated_segment_count", nr_txn_allocated_segment_count(txn)); + /* Agent and PHP version metrics*/ + nr_php_txn_create_agent_php_version_metrics(txn); + + /* PHP packages major version metrics */ + nr_php_txn_create_packages_major_metrics(txn); + /* Add CPU and memory metrics */ nr_php_resource_usage_sampler_end(TSRMLS_C); diff --git a/agent/php_txn_private.h b/agent/php_txn_private.h index 743adc06f..6788877ad 100644 --- a/agent/php_txn_private.h +++ b/agent/php_txn_private.h @@ -36,3 +36,53 @@ nrobj_t* nr_php_txn_get_supported_security_policy_settings(); * Params : 1. The current transaction. */ extern void nr_php_txn_handle_fpm_error(nrtxn_t* txn TSRMLS_DC); + +/* + * Purpose : Create and record metrics for the PHP and agent versions. + * + * Params : 1. The current transaction. + * + * Notes : This function relies on NR_VERSION and the value of + * NRPRG(php_version) to create the metrics. + */ +extern void nr_php_txn_create_agent_php_version_metrics(nrtxn_t* txn); + +/* + * Purpose : Create and record metric for a specific agent version. + * + * Params : 1. The current transaction. + * + * Notes : This function relies on the value of the macro NR_VERSION + * to create. + */ +extern void nr_php_txn_create_agent_version_metric(nrtxn_t* txn); + +/* + * Purpose : Create and record metric for a specific PHP version. + * + * Params : 1. The current transaction. + * 2. The PHP agent version. + */ +extern void nr_php_txn_create_php_version_metric(nrtxn_t* txn, + const char* version); + +/* + * Purpose : Callback for nr_php_packages_iterate to create major + * version metrics. + * + * Params : 1. PHP suggestion package version + * 2. PHP suggestion package name + * 3. PHP suggestion package name length + * 4. The current transaction (via userdata) + */ +extern void nr_php_txn_php_package_create_major_metric(void* value, + const char* key, + size_t key_len, + void* user_data); + +/* + * Purpose : Create and record metric for a package major versions. + * + * Params : 1. The current transaction. + */ +extern void nr_php_txn_create_packages_major_metrics(nrtxn_t* txn); diff --git a/agent/php_wrapper.c b/agent/php_wrapper.c index daa01bb4f..6631c06d2 100644 --- a/agent/php_wrapper.c +++ b/agent/php_wrapper.c @@ -73,6 +73,27 @@ nruserfn_t* nr_php_wrap_user_function_before_after_clean( return wraprec; } +nruserfn_t* nr_php_wrap_user_function_before_after_clean_extra( + const char* name, + size_t namelen, + nrspecialfn_t before_callback, + nrspecialfn_t after_callback, + nrspecialfn_t clean_callback, + const char* extra) { + nruserfn_t* wraprec = nr_php_wrap_user_function_before_after_clean( + name, namelen, before_callback, after_callback, clean_callback); + + if (nrunlikely(NULL == wraprec)) { + nrl_warning(NRL_INSTRUMENT, "%s: unable to wrap '%s'", __func__, + NRSAFESTR(name)); + return wraprec; + } + + wraprec->extra = extra; + + return wraprec; +} + nruserfn_t* nr_php_wrap_callable_before_after_clean( zend_function* callable, nrspecialfn_t before_callback, diff --git a/agent/php_wrapper.h b/agent/php_wrapper.h index 27e0dba91..058e06e08 100644 --- a/agent/php_wrapper.h +++ b/agent/php_wrapper.h @@ -150,6 +150,14 @@ extern nruserfn_t* nr_php_wrap_callable_before_after_clean( nrspecialfn_t before_callback, nrspecialfn_t after_callback, nrspecialfn_t clean_callback); + +extern nruserfn_t* nr_php_wrap_user_function_before_after_clean_extra( + const char* name, + size_t namelen, + nrspecialfn_t before_callback, + nrspecialfn_t after_callback, + nrspecialfn_t clean_callback, + const char* extra); #endif extern nruserfn_t* nr_php_wrap_user_function(const char* name, size_t namelen, diff --git a/agent/scripts/init.alpine b/agent/scripts/init.alpine index 63d143e66..0fc717dbc 100755 --- a/agent/scripts/init.alpine +++ b/agent/scripts/init.alpine @@ -1,10 +1,9 @@ +#!/sbin/openrc-run # # Copyright 2020 New Relic Corporation. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # -#!/sbin/openrc-run - description="New Relic Daemon" command="${nrdaemon:-/usr/bin/newrelic-daemon}" command_args= diff --git a/agent/scripts/init.rhel b/agent/scripts/init.rhel index 82f43608c..2e88a6307 100644 --- a/agent/scripts/init.rhel +++ b/agent/scripts/init.rhel @@ -10,6 +10,15 @@ # processname: newrelic-daemon # config: /etc/newrelic/newrelic.cfg # +### BEGIN INIT INFO +# Provides: newrelic-daemon +# Required-Start: $all +# Required-Stop: +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Description: The New Relic Daemon is used by the New Relic PHP Agent to communicate +# with the New Relic Backend +### END INIT INFO LANG=C NAME=newrelic-daemon @@ -18,7 +27,11 @@ DESC="New Relic Daemon" # # Source function library. # -. /etc/init.d/functions +has_initd_functions=0 +if [ -f /etc/init.d/functions ]; then + . /etc/init.d/functions + has_initd_functions=1 +fi ulimit -n 2048 > /dev/null 2>&1 @@ -223,14 +236,22 @@ EOF if running ; then if [ -z "${NR_SILENT}" -a -z "${SILENT}" ]; then - success - echo + if [ $has_initd_functions -eq 1 ]; then + success + echo + else + echo " [ OK ]" + fi fi return 0 else if [ -z "${NR_SILENT}" -a -z "${SILENT}" ]; then - failure - echo + if [ $has_initd_functions -eq 1 ]; then + failure + echo + else + echo " [FAILED]" + fi fi return 1 fi @@ -274,14 +295,22 @@ stop() { if running ; then if [ -z "${NR_SILENT}" -a -z "${SILENT}" ]; then - failure - echo + if [ $has_initd_functions -eq 1 ]; then + failure + echo + else + echo " [FAILED]" + fi fi return 1 else if [ -z "${NR_SILENT}" -a -z "${SILENT}" ]; then - success - echo + if [ $has_initd_functions -eq 1 ]; then + success + echo + else + echo " [ OK ]" + fi fi return 0 fi diff --git a/agent/scripts/newrelic.ini.private.template b/agent/scripts/newrelic.ini.private.template index 67175b6b8..92f4546f7 100644 --- a/agent/scripts/newrelic.ini.private.template +++ b/agent/scripts/newrelic.ini.private.template @@ -193,3 +193,14 @@ ; hooks instrumented. ; ;newrelic.framework.wordpress.hooks_skip_filename="" + +; Setting: newrelic.distributed_tracing.pad_trace_id +; Type : bool +; Scope : per-directory +; Default: false +; Info : If set to true, the distributed trace's id generated by the agent, +; which is only meant to be used internally by New Relic, will be left +; padded with '0's to have the length of 32 characters. This will increase +; the amount of data sent to New Relic. +; +;newrelic.distributed_tracing.pad_trace_id = false diff --git a/agent/scripts/newrelic.ini.template b/agent/scripts/newrelic.ini.template index d6770a217..eb8e2f202 100644 --- a/agent/scripts/newrelic.ini.template +++ b/agent/scripts/newrelic.ini.template @@ -1333,3 +1333,23 @@ newrelic.daemon.logfile = "/var/log/newrelic/newrelic-daemon.log" ; for vulnerability management. ; ;newrelic.vulnerability_management.package_detection.enabled = true + +; Setting: newrelic.vulnerability_management.composer_api.enabled +; Type : boolean +; Scope : per-directory +; Default: false +; Info : Toggles whether the agent should try using Composer's runtime API +; to gather package information for vulnerability management. +; +;newrelic.vulnerability_management.composer_api.enabled = false + +; Setting: newrelic.message_tracer.segment_parameters.enabled +; Type : boolean +; Scope : per-directory +; Default: true +; Info : If this setting is true, then message parameters will be captured and +; stored on their respective segments. While enabled, specific attributes +; can be filtered by using newrelic.attributes.include/exclude and +; newrelic.span_events.attributes.include/exclude +; +;newrelic.message_tracer.segment_parameters.enabled = true diff --git a/agent/tests/test_agent.c b/agent/tests/test_agent.c index 12d3437e4..53b3495ee 100644 --- a/agent/tests/test_agent.c +++ b/agent/tests/test_agent.c @@ -142,7 +142,11 @@ static void test_function_debug_name(TSRMLS_D) { func = nr_php_zval_to_function(closure TSRMLS_CC); name = nr_php_function_debug_name(func); +#if ZEND_MODULE_API_NO < ZEND_8_4_X_API_NO tlib_pass_if_str_equal("closure", "{closure} declared at -:1", name); +#else + tlib_pass_if_str_equal("closure", "{closure:-:1} declared at -:1", name); +#endif nr_php_zval_free(&closure); nr_free(name); diff --git a/agent/tests/test_fw_support.c b/agent/tests/test_fw_support.c index c8b8e021d..980dc02aa 100644 --- a/agent/tests/test_fw_support.c +++ b/agent/tests/test_fw_support.c @@ -11,7 +11,12 @@ tlib_parallel_info_t parallel_info = {.suggested_nthreads = -1, .state_size = 0}; -static void test_fw_supportability_metrics(void) { +// When package detection for vulnerability management is disabled, +// txn->php_packages is not populated and package version cannot +// be obtained from php_package. This test is to ensure that +// the package supportability metric is created in case php_package +// is not available and that fallback version is used. +static void test_fw_supportability_metrics_with_vm_disabled(void) { #define LIBRARY_NAME "php-package" #define LIBRARY_MAJOR_VERSION "7" #define LIBRARY_MAJOR_VERSION_2 "10" @@ -22,9 +27,11 @@ static void test_fw_supportability_metrics(void) { #define LIBRARY_MAJOR_VERSION_7 "0.4.5" #define LIBRARY_METRIC "Supportability/library/" LIBRARY_NAME "/detected" #define LOGGING_LIBRARY_METRIC "Supportability/Logging/PHP/" LIBRARY_NAME -#define PACKAGE_METRIC "Supportability/PHP/package/" LIBRARY_NAME +#define PACKAGE_METRIC_PREFIX "Supportability/PHP/package/" +#define PACKAGE_METRIC PACKAGE_METRIC_PREFIX LIBRARY_NAME nrtxn_t t; nrtxn_t* txn = &t; + nr_php_package_t* php_package = NULL; txn->unscoped_metrics = nrm_table_create(10); /* NULL tests - don't blow up */ @@ -44,15 +51,18 @@ static void test_fw_supportability_metrics(void) { tlib_pass_if_int_equal("NULL logging library metric not created", 0, nrm_table_size(txn->unscoped_metrics)); - nr_fw_support_add_package_supportability_metric(NULL, LIBRARY_NAME, LIBRARY_MAJOR_VERSION); + nr_fw_support_add_package_supportability_metric( + NULL, LIBRARY_NAME, LIBRARY_MAJOR_VERSION, php_package); tlib_pass_if_int_equal("package metric not created in NULL metrics", 0, nrm_table_size(txn->unscoped_metrics)); - nr_fw_support_add_package_supportability_metric(txn, NULL, LIBRARY_MAJOR_VERSION); + nr_fw_support_add_package_supportability_metric( + txn, NULL, LIBRARY_MAJOR_VERSION, php_package); tlib_pass_if_int_equal("NULL package name, metric not created", 0, nrm_table_size(txn->unscoped_metrics)); - nr_fw_support_add_package_supportability_metric(txn, LIBRARY_NAME, NULL); + nr_fw_support_add_package_supportability_metric(txn, LIBRARY_NAME, NULL, + php_package); tlib_pass_if_int_equal("NULL major version, metric not created", 0, nrm_table_size(txn->unscoped_metrics)); @@ -71,51 +81,51 @@ static void test_fw_supportability_metrics(void) { "happy path: logging library metric created", nrm_find(txn->unscoped_metrics, LOGGING_LIBRARY_METRIC "/disabled")); - nr_fw_support_add_package_supportability_metric(txn, LIBRARY_NAME, - LIBRARY_MAJOR_VERSION); + nr_fw_support_add_package_supportability_metric( + txn, LIBRARY_NAME, LIBRARY_MAJOR_VERSION, php_package); tlib_pass_if_not_null("happy path test 1: package metric created", nrm_find(txn->unscoped_metrics, PACKAGE_METRIC "/" LIBRARY_MAJOR_VERSION "/detected")); - nr_fw_support_add_package_supportability_metric(txn, LIBRARY_NAME, - LIBRARY_MAJOR_VERSION_2); + nr_fw_support_add_package_supportability_metric( + txn, LIBRARY_NAME, LIBRARY_MAJOR_VERSION_2, php_package); tlib_pass_if_not_null("happy path test 2: package metric created", nrm_find(txn->unscoped_metrics, PACKAGE_METRIC "/" LIBRARY_MAJOR_VERSION_2 "/detected")); - nr_fw_support_add_package_supportability_metric(txn, LIBRARY_NAME, - LIBRARY_MAJOR_VERSION_3); + nr_fw_support_add_package_supportability_metric( + txn, LIBRARY_NAME, LIBRARY_MAJOR_VERSION_3, php_package); tlib_pass_if_not_null("happy path test 3: package metric created", nrm_find(txn->unscoped_metrics, PACKAGE_METRIC "/" LIBRARY_MAJOR_VERSION_3 "/detected")); - nr_fw_support_add_package_supportability_metric(txn, LIBRARY_NAME, - LIBRARY_MAJOR_VERSION_4); + nr_fw_support_add_package_supportability_metric( + txn, LIBRARY_NAME, LIBRARY_MAJOR_VERSION_4, php_package); tlib_pass_if_not_null( "happy path test 4: package metric created", nrm_find(txn->unscoped_metrics, PACKAGE_METRIC "/1/detected")); - nr_fw_support_add_package_supportability_metric(txn, LIBRARY_NAME, - LIBRARY_MAJOR_VERSION_5); + nr_fw_support_add_package_supportability_metric( + txn, LIBRARY_NAME, LIBRARY_MAJOR_VERSION_5, php_package); tlib_pass_if_not_null( "happy path test 5: package metric created", nrm_find(txn->unscoped_metrics, PACKAGE_METRIC "/12/detected")); - nr_fw_support_add_package_supportability_metric(txn, LIBRARY_NAME, - LIBRARY_MAJOR_VERSION_6); + nr_fw_support_add_package_supportability_metric( + txn, LIBRARY_NAME, LIBRARY_MAJOR_VERSION_6, php_package); tlib_pass_if_not_null( "happy path test 6: package metric created", nrm_find(txn->unscoped_metrics, PACKAGE_METRIC "/123/detected")); - nr_fw_support_add_package_supportability_metric(txn, LIBRARY_NAME, - LIBRARY_MAJOR_VERSION_7); + nr_fw_support_add_package_supportability_metric( + txn, LIBRARY_NAME, LIBRARY_MAJOR_VERSION_7, php_package); tlib_pass_if_not_null( "happy path test 7: package metric created", nrm_find(txn->unscoped_metrics, PACKAGE_METRIC "/0/detected")); NRINI(force_framework) = true; - nr_fw_support_add_package_supportability_metric(txn, LIBRARY_NAME, - LIBRARY_MAJOR_VERSION); + nr_fw_support_add_package_supportability_metric( + txn, LIBRARY_NAME, LIBRARY_MAJOR_VERSION, php_package); tlib_pass_if_not_null( "happy path test 8: package metric created", nrm_find(txn->unscoped_metrics, PACKAGE_METRIC "/7/forced")); @@ -123,6 +133,88 @@ static void test_fw_supportability_metrics(void) { nrm_table_destroy(&txn->unscoped_metrics); } +// When package detection for vulnerability management is enabled, +// txn->php_packages is populated and package version can be obtained +// from php_package stored in txn->php_packages. This test is to ensure +// that the package supportability metric is created in case php_package +// is available and that package version from php_package is used. +static void test_fw_supportability_metrics_with_vm_enabled(void) { +#define PHP_PACKAGE_MAJOR_VERSION "8" +#define PHP_PACKAGE_VERSION PHP_PACKAGE_MAJOR_VERSION ".4.0" + nrtxn_t t; + nrtxn_t* txn = &t; + nr_php_package_t php_package + = {.package_name = LIBRARY_NAME, + .package_version = PHP_PACKAGE_VERSION, + .source_priority = NR_PHP_PACKAGE_SOURCE_COMPOSER}; + nr_php_package_t php_package_null_version + = {.package_name = LIBRARY_NAME, + .package_version = NULL, + .source_priority = NR_PHP_PACKAGE_SOURCE_COMPOSER}; + nr_php_package_t php_package_unknown_version + = {.package_name = LIBRARY_NAME, + .package_version = PHP_PACKAGE_VERSION_UNKNOWN, + .source_priority = NR_PHP_PACKAGE_SOURCE_COMPOSER}; + txn->unscoped_metrics = nrm_table_create(10); + + NRINI(force_framework) = false; + nr_fw_support_add_package_supportability_metric(txn, LIBRARY_NAME, NULL, + &php_package_null_version); + tlib_pass_if_null( + "library major version metric not created when version is unknown - " + "version is NULL and package version is NULL", + nrm_get_metric(txn->unscoped_metrics, 0)); + + nr_fw_support_add_package_supportability_metric(txn, LIBRARY_NAME, + PHP_PACKAGE_VERSION_UNKNOWN, + &php_package_null_version); + tlib_pass_if_null( + "library major version metric not created when version is unknown - " + "version is PHP_PACKAGE_VERSION_UNKNOWN and package version is NULL", + nrm_get_metric(txn->unscoped_metrics, 0)); + + nr_fw_support_add_package_supportability_metric(txn, LIBRARY_NAME, NULL, + &php_package_unknown_version); + tlib_pass_if_null( + "library major version metric not created when version is unknown - " + "version is NULL and package version is PHP_PACKAGE_VERSION_UNKNOWN", + nrm_get_metric(txn->unscoped_metrics, 0)); + + nr_fw_support_add_package_supportability_metric( + txn, LIBRARY_NAME, LIBRARY_MAJOR_VERSION, &php_package); + tlib_pass_if_not_null( + "php package major version is used for 'detected' metric", + nrm_find(txn->unscoped_metrics, + PACKAGE_METRIC "/" PHP_PACKAGE_MAJOR_VERSION "/detected")); + + nr_fw_support_add_package_supportability_metric( + txn, LIBRARY_NAME, LIBRARY_MAJOR_VERSION, &php_package_null_version); + tlib_pass_if_not_null( + "library major version is used for 'detected' metric when php package " + "version is NULL", + nrm_find(txn->unscoped_metrics, + PACKAGE_METRIC "/" LIBRARY_MAJOR_VERSION "/detected")); + + NRINI(force_framework) = true; + nr_fw_support_add_package_supportability_metric( + txn, LIBRARY_NAME, LIBRARY_MAJOR_VERSION, &php_package); + tlib_pass_if_not_null( + "php package major version is used for 'detected' metric", + nrm_find(txn->unscoped_metrics, + PACKAGE_METRIC "/" PHP_PACKAGE_MAJOR_VERSION "/forced")); + + nr_fw_support_add_package_supportability_metric( + txn, LIBRARY_NAME, LIBRARY_MAJOR_VERSION, &php_package_null_version); + tlib_pass_if_not_null( + "library major version is used for 'forced' metric when php package " + "version is NULL", + nrm_find(txn->unscoped_metrics, + PACKAGE_METRIC "/" LIBRARY_MAJOR_VERSION "/detected")); + + nrm_table_destroy(&txn->unscoped_metrics); +} + void test_main(void* p NRUNUSED) { - test_fw_supportability_metrics(); + test_fw_supportability_metrics_with_vm_disabled(); + test_fw_supportability_metrics_with_vm_enabled(); } diff --git a/agent/tests/test_lib_aws_sdk_php.c b/agent/tests/test_lib_aws_sdk_php.c new file mode 100644 index 000000000..0ac040424 --- /dev/null +++ b/agent/tests/test_lib_aws_sdk_php.c @@ -0,0 +1,473 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "tlib_php.h" + +#include "php_agent.h" +#include "php_call.h" +#include "php_wrapper.h" +#include "fw_support.h" +#include "nr_segment_message.h" +#include "lib_aws_sdk_php.h" + +tlib_parallel_info_t parallel_info + = {.suggested_nthreads = -1, .state_size = 0}; + +#if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO +/* + * Aside from service class and version detection, instrumentation is only + * supported with PHP 8.1+ + */ + +#define ARG_VALUE_FOR_TEST "curly_q" +#define COMMAND_NAME_FOR_TEST "uniquelyAwesome" +#define COMMAND_NAME_LEN_FOR_TEST sizeof(COMMAND_NAME_FOR_TEST) - 1 +#define ARG_TO_FIND_FOR_TEST AWS_SDK_PHP_SQSCLIENT_QUEUEURL_ARG +#define AWS_QUEUEURL_LEN_MAX 512 + +/* These wrappers are used so we don't have to mock up zend_execute_data. */ + +NR_PHP_WRAPPER(expect_arg_value_not_null) { + char* command_arg_value = NULL; + + (void)wraprec; + + command_arg_value = nr_lib_aws_sdk_php_get_command_arg_value( + ARG_TO_FIND_FOR_TEST, NR_EXECUTE_ORIG_ARGS); + tlib_pass_if_not_null( + "Expect a valid command_arg_value if a valid named arg exists.", + command_arg_value); + tlib_pass_if_str_equal("Arg name/value pair should match.", + ARG_VALUE_FOR_TEST, command_arg_value); + nr_free(command_arg_value); + NR_PHP_WRAPPER_CALL; +} +NR_PHP_WRAPPER_END + +NR_PHP_WRAPPER(expect_arg_value_null) { + char* command_arg_value = NULL; + + (void)wraprec; + + command_arg_value = nr_lib_aws_sdk_php_get_command_arg_value( + ARG_TO_FIND_FOR_TEST, NR_EXECUTE_ORIG_ARGS); + tlib_pass_if_null( + "Expect a null command_arg_value if no valid named arg exists.", + command_arg_value); + + NR_PHP_WRAPPER_CALL; +} +NR_PHP_WRAPPER_END + +static void test_nr_lib_aws_sdk_php_get_command_arg_value() { + zval* expr = NULL; + zval* first_arg = NULL; + zval* array_arg = NULL; + + /* + * nr_lib_aws_sdk_php_get_command_arg_value extracts an arg value from the 2nd + * argument in the argument list, so we need to have at least 2 args to + * extract properly. + */ + tlib_php_engine_create(""); + tlib_php_request_start(); + + tlib_php_request_eval("function one_param($a) { return; }"); + nr_php_wrap_user_function(NR_PSTR("one_param"), expect_arg_value_null); + tlib_php_request_eval("function two_param_valid($a, $b) { return; }"); + nr_php_wrap_user_function(NR_PSTR("two_param_valid"), + expect_arg_value_not_null); + tlib_php_request_eval("function two_param($a, $b) { return; }"); + nr_php_wrap_user_function(NR_PSTR("two_param"), expect_arg_value_null); + tlib_php_request_eval("function no_param() { return;}"); + nr_php_wrap_user_function(NR_PSTR("no_param"), expect_arg_value_null); + + /* + * The function isn't decoding this arg, so it doesn't matter what it is as + * long as it exists. + */ + first_arg = tlib_php_request_eval_expr("1"); + + /* Valid case. The wrapper should verify strings match. */ + + char* valid_array_args + = "array(" + " 0 => array(" + " 'QueueUrl' => 'curly_q'" + " )" + ")"; + array_arg = tlib_php_request_eval_expr(valid_array_args); + expr = nr_php_call(NULL, "two_param_valid", first_arg, array_arg); + tlib_pass_if_not_null("Expression should evaluate.", expr); + nr_php_zval_free(&expr); + nr_php_zval_free(&array_arg); + + /* Test Invalid Cases*/ + + /* + * Invalid case: QueueUrl found but value was not a string. The wrapper + * should see the null return value. + */ + char* queueurl_not_string_arg + = "array(" + " 0 => array(" + " 'QueueUrl' => array(" + " 'Nope' => 'curly_q'" + " )" + " )" + ")"; + array_arg = tlib_php_request_eval_expr(queueurl_not_string_arg); + expr = nr_php_call(NULL, "two_param", first_arg, array_arg); + tlib_pass_if_not_null("Expression should evaluate.", expr); + nr_php_zval_free(&expr); + nr_php_zval_free(&array_arg); + + /* Invalid case: only one parameter. The wrapper should see the null return + * value. */ + expr = nr_php_call(NULL, "one_param", first_arg); + tlib_pass_if_not_null("Expression should evaluate.", expr); + nr_php_zval_free(&expr); + + /* Invalid case: no parameter. The wrapper should see the null return value. + */ + expr = nr_php_call(NULL, "no_param"); + tlib_pass_if_not_null("Expression should evaluate.", expr); + nr_php_zval_free(&expr); + + /* + *Invalid case: QueueUrl not found in the argument array. The wrapper should + *see the null return value. + */ + char* no_queueurl_arg + = "array(" + " 0 => array(" + " 'Nope' => 'curly_q'" + " )" + ")"; + array_arg = tlib_php_request_eval_expr(no_queueurl_arg); + expr = nr_php_call(NULL, "two_param", first_arg, array_arg); + tlib_pass_if_not_null("Expression should evaluate.", expr); + nr_php_zval_free(&expr); + nr_php_zval_free(&array_arg); + + /* + *Invalid case: inner arg in the argument array is not an array. The wrapper + *should see the null return value. + */ + char* arg_in_array_not_array + = "array(" + " 0 => '1'" + ")"; + array_arg = tlib_php_request_eval_expr(arg_in_array_not_array); + expr = nr_php_call(NULL, "two_param", first_arg, array_arg); + tlib_pass_if_not_null("Expression should evaluate.", expr); + nr_php_zval_free(&expr); + nr_php_zval_free(&array_arg); + + /* + *Invalid case: empty argument array. The wrapper should see + * the null return value. + */ + char* no_arg_in_array + = "array(" + ")"; + array_arg = tlib_php_request_eval_expr(no_arg_in_array); + expr = nr_php_call(NULL, "two_param", first_arg, array_arg); + tlib_pass_if_not_null("Expression should evaluate.", expr); + nr_php_zval_free(&expr); + nr_php_zval_free(&array_arg); + + /* + *Invalid case: The argument array is not an array. The wrapper should see + * the null return value. + */ + char* array_arg_not_array = "1"; + array_arg = tlib_php_request_eval_expr(array_arg_not_array); + expr = nr_php_call(NULL, "two_param", first_arg, array_arg); + tlib_pass_if_not_null("Expression should evaluate.", expr); + nr_php_zval_free(&expr); + nr_php_zval_free(&array_arg); + + nr_php_zval_free(&first_arg); + tlib_php_request_end(); + tlib_php_engine_destroy(); +} + +static inline void test_message_param_queueurl_settings_expect_val( + nr_segment_message_params_t* message_params, + nr_segment_cloud_attrs_t* cloud_attrs, + char* cloud_region, + char* cloud_account_id, + char* destination_name) { + tlib_pass_if_str_equal("cloud_region should match.", cloud_attrs->cloud_region, + cloud_region); + tlib_pass_if_str_equal("cloud_account_id should match.", + cloud_attrs->cloud_account_id, cloud_account_id); + tlib_pass_if_str_equal("destination_name should match.", + message_params->destination_name, destination_name); +} + +static inline void test_message_param_queueurl_settings_expect_null( + nr_segment_message_params_t* message_params, + nr_segment_cloud_attrs_t* cloud_attrs) { + if (NULL != cloud_attrs) { + tlib_pass_if_null("cloud_region should be null.", cloud_attrs->cloud_region); + tlib_pass_if_null("cloud_account_id should be null.", + cloud_attrs->cloud_account_id); + } + if (NULL != message_params) { + tlib_pass_if_null("destination_name should be null.", + message_params->destination_name); + } +} + +static void test_nr_lib_aws_sdk_php_sqs_parse_queueurl() { + /* + * nr_lib_aws_sdk_php_sqs_parse_queueurl extracts either ALL cloud_region, + * cloud_account_id, and destination_name or none. + */ + nr_segment_message_params_t message_params = {0}; + nr_segment_cloud_attrs_t cloud_attrs = {0}; + char modifiable_string[AWS_QUEUEURL_LEN_MAX]; + + tlib_php_engine_create(""); + +// clang-format off +#define VALID_QUEUE_URL "https://sqs.us-east-2.amazonaws.com/123456789012/SQS_QUEUE_NAME" +#define INVALID_QUEUE_URL_1 "https://us-east-2.amazonaws.com/123456789012/SQS_QUEUE_NAME" +#define INVALID_QUEUE_URL_2 "https://sqs.us-east-2.amazonaws.com/123456789012/" +#define INVALID_QUEUE_URL_3 "https://sqs.us-east-2.amazonaws.com/SQS_QUEUE_NAME" +#define INVALID_QUEUE_URL_4 "https://random.com" +#define INVALID_QUEUE_URL_5 "https://sqs.us-east-2.amazonaws.com/123456789012" +#define INVALID_QUEUE_URL_6 "https://sqs.us-east-2.amazonaws.com/" +#define INVALID_QUEUE_URL_7 "https://sqs.us-east-2.amazonaws.com" +#define INVALID_QUEUE_URL_8 "https://sqs.us-east-2.random.com/123456789012/SQS_QUEUE_NAME" + // clang-format on + + /* Test null queueurl. Extracted message_param values should be null.*/ + nr_lib_aws_sdk_php_sqs_parse_queueurl(NULL, &message_params, &cloud_attrs); + test_message_param_queueurl_settings_expect_null(&message_params, &cloud_attrs); + + /* Test null message_params. No values extracted, all values should be + * null.*/ + nr_lib_aws_sdk_php_sqs_parse_queueurl(NULL, NULL, &cloud_attrs); + test_message_param_queueurl_settings_expect_null(&message_params, &cloud_attrs); + + /* Test null cloud_attrs. No values extracted, all values should be null.*/ + nr_lib_aws_sdk_php_sqs_parse_queueurl(NULL, &message_params, NULL); + test_message_param_queueurl_settings_expect_null(&message_params, &cloud_attrs); + + /* Test Invalid values. Extracted message_param values should be null.*/ + nr_strcpy(modifiable_string, INVALID_QUEUE_URL_1); + nr_lib_aws_sdk_php_sqs_parse_queueurl(modifiable_string, &message_params, + &cloud_attrs); + test_message_param_queueurl_settings_expect_null(&message_params, &cloud_attrs); + + /* Test Invalid values. Extracted message_param values should be null.*/ + nr_strcpy(modifiable_string, INVALID_QUEUE_URL_2); + nr_lib_aws_sdk_php_sqs_parse_queueurl(modifiable_string, &message_params, + &cloud_attrs); + test_message_param_queueurl_settings_expect_null(&message_params, &cloud_attrs); + + /* Test Invalid values. Extracted message_param values should be null.*/ + nr_strcpy(modifiable_string, INVALID_QUEUE_URL_3); + nr_lib_aws_sdk_php_sqs_parse_queueurl(modifiable_string, &message_params, + &cloud_attrs); + test_message_param_queueurl_settings_expect_null(&message_params, &cloud_attrs); + + /* Test Invalid values. Extracted message_param values should be null.*/ + nr_strcpy(modifiable_string, INVALID_QUEUE_URL_4); + nr_lib_aws_sdk_php_sqs_parse_queueurl(modifiable_string, &message_params, + &cloud_attrs); + test_message_param_queueurl_settings_expect_null(&message_params, &cloud_attrs); + + /* Test Invalid values. Extracted message_param values should be null.*/ + nr_strcpy(modifiable_string, INVALID_QUEUE_URL_5); + nr_lib_aws_sdk_php_sqs_parse_queueurl(modifiable_string, &message_params, + &cloud_attrs); + test_message_param_queueurl_settings_expect_null(&message_params, &cloud_attrs); + + /* Test Invalid values. Extracted message_param values should be null.*/ + nr_strcpy(modifiable_string, INVALID_QUEUE_URL_6); + nr_lib_aws_sdk_php_sqs_parse_queueurl(modifiable_string, &message_params, + &cloud_attrs); + test_message_param_queueurl_settings_expect_null(&message_params, &cloud_attrs); + + /* Test Invalid values. Extracted message_param values should be null.*/ + nr_strcpy(modifiable_string, INVALID_QUEUE_URL_7); + nr_lib_aws_sdk_php_sqs_parse_queueurl(modifiable_string, &message_params, + &cloud_attrs); + test_message_param_queueurl_settings_expect_null(&message_params, &cloud_attrs); + + /* Test Invalid values. Extracted message_param values should be null.*/ + + nr_strcpy(modifiable_string, INVALID_QUEUE_URL_8); + nr_lib_aws_sdk_php_sqs_parse_queueurl(modifiable_string, &message_params, + &cloud_attrs); + test_message_param_queueurl_settings_expect_null(&message_params, &cloud_attrs); + + /* + * Test 'https://sqs.us-east-2.amazonaws.com/123456789012/SQS_QUEUE_NAME'. + * Extracted message_param values should set. + */ + + nr_strcpy(modifiable_string, VALID_QUEUE_URL); + nr_lib_aws_sdk_php_sqs_parse_queueurl(modifiable_string, &message_params, + &cloud_attrs); + test_message_param_queueurl_settings_expect_val(&message_params, &cloud_attrs, + "us-east-2", "123456789012", + "SQS_QUEUE_NAME"); + + tlib_php_engine_destroy(); +} +#endif /* PHP 8.1+ */ + +#if ZEND_MODULE_API_NO > ZEND_7_1_X_API_NO + +static void declare_aws_sdk_class(const char* ns, + const char* klass, + const char* sdk_version) { + char* source = nr_formatf( + "namespace %s;" + "class %s{" + "const VERSION = '%s';" + "}", + ns, klass, sdk_version); + + tlib_php_request_eval(source); + + nr_free(source); +} + +static void test_nr_lib_aws_sdk_php_add_supportability_service_metric(void) { + /* + * Should return aws metric with classname + */ + tlib_php_request_start(); + + int num_metrics = nrm_table_size(NRPRG(txn)->unscoped_metrics); + nr_lib_aws_sdk_php_add_supportability_service_metric(NULL); + tlib_pass_if_int_equal( + "aws supportability metric 0: metric not created in NULL metrics", + num_metrics, nrm_table_size(NRPRG(txn)->unscoped_metrics)); + + nr_lib_aws_sdk_php_add_supportability_service_metric("one\\two"); + tlib_pass_if_not_null( + "aws supportability metric 1: service/client metric created", + nrm_find(NRPRG(txn)->unscoped_metrics, + PHP_AWS_SDK_SERVICE_NAME_METRIC_PREFIX "one\\two")); + + nr_lib_aws_sdk_php_add_supportability_service_metric("three\\four"); + tlib_pass_if_not_null( + "aws supportability metric 2: service/client metric created", + nrm_find(NRPRG(txn)->unscoped_metrics, + PHP_AWS_SDK_SERVICE_NAME_METRIC_PREFIX "three\\four")); + + nr_lib_aws_sdk_php_add_supportability_service_metric("three\\four\\five"); + tlib_pass_if_not_null( + "aws supportability metric 3: service/client metric created", + nrm_find(NRPRG(txn)->unscoped_metrics, + PHP_AWS_SDK_SERVICE_NAME_METRIC_PREFIX "three\\four\\five")); + + nr_lib_aws_sdk_php_add_supportability_service_metric("three\\"); + tlib_pass_if_not_null( + "aws supportability metric 4: service/client metric created", + nrm_find(NRPRG(txn)->unscoped_metrics, + PHP_AWS_SDK_SERVICE_NAME_METRIC_PREFIX "three\\")); + nr_lib_aws_sdk_php_add_supportability_service_metric("\\four"); + tlib_pass_if_not_null( + "aws supportability metric 5: service/client metric created", + nrm_find(NRPRG(txn)->unscoped_metrics, + PHP_AWS_SDK_SERVICE_NAME_METRIC_PREFIX "\\four")); + nr_lib_aws_sdk_php_add_supportability_service_metric("five"); + tlib_pass_if_not_null( + "aws supportability metric 6: service/client metric created", + nrm_find(NRPRG(txn)->unscoped_metrics, + PHP_AWS_SDK_SERVICE_NAME_METRIC_PREFIX "five")); + + tlib_php_request_end(); +} + +static void test_nr_lib_aws_sdk_php_handle_version(void) { +#define LIBRARY_NAME "aws/aws-sdk-php" + const char* library_versions[] + = {"7", "10", "100", "4.23", "55.34", "6123.45", "0.4.5"}; + nr_php_package_t* p = NULL; +#define TEST_DESCRIPTION_FMT \ + "nr_lib_aws_sdk_php_handle_version with library_versions[%ld]=%s: package " \ + "major version metric - %s" + char* test_description = NULL; + size_t i = 0; + + /* + * If lib_aws_sdk_php_handle_version function is ever called, we have already + * detected the aws-sdk-php library. + */ + + /* + * Aws/Sdk class exists. Should create aws package metric suggestion with + * version + */ + for (i = 0; i < sizeof(library_versions) / sizeof(library_versions[0]); i++) { + tlib_php_request_start(); + + declare_aws_sdk_class("Aws", "Sdk", library_versions[i]); + nr_lib_aws_sdk_php_handle_version(); + + p = nr_php_packages_get_package( + NRPRG(txn)->php_package_major_version_metrics_suggestions, + LIBRARY_NAME); + + test_description = nr_formatf(TEST_DESCRIPTION_FMT, i, library_versions[i], + "suggestion created"); + tlib_pass_if_not_null(test_description, p); + nr_free(test_description); + + test_description = nr_formatf(TEST_DESCRIPTION_FMT, i, library_versions[i], + "suggested version set"); + tlib_pass_if_str_equal(test_description, library_versions[i], + p->package_version); + nr_free(test_description); + + tlib_php_request_end(); + } + + /* + * Aws/Sdk class does not exist, should create package metric suggestion + * with PHP_PACKAGE_VERSION_UNKNOWN version. This case should never happen + * in real situations. + */ + tlib_php_request_start(); + + nr_lib_aws_sdk_php_handle_version(); + + p = nr_php_packages_get_package( + NRPRG(txn)->php_package_major_version_metrics_suggestions, LIBRARY_NAME); + + tlib_pass_if_not_null( + "nr_lib_aws_sdk_php_handle_version when Aws\\Sdk class is not defined - " + "suggestion created", + p); + tlib_pass_if_str_equal( + "nr_lib_aws_sdk_php_handle_version when Aws\\Sdk class is not defined - " + "suggested version set to PHP_PACKAGE_VERSION_UNKNOWN", + PHP_PACKAGE_VERSION_UNKNOWN, p->package_version); + + tlib_php_request_end(); +} + +void test_main(void* p NRUNUSED) { + tlib_php_engine_create(""); + test_nr_lib_aws_sdk_php_add_supportability_service_metric(); + test_nr_lib_aws_sdk_php_handle_version(); + tlib_php_engine_destroy(); +#if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO + test_nr_lib_aws_sdk_php_sqs_parse_queueurl(); + test_nr_lib_aws_sdk_php_get_command_arg_value(); +#endif /* PHP 8.1+ */ +} +#else +void test_main(void* p NRUNUSED) {} +#endif diff --git a/agent/tests/test_lib_php_amqplib.c b/agent/tests/test_lib_php_amqplib.c new file mode 100644 index 000000000..c0261cfc2 --- /dev/null +++ b/agent/tests/test_lib_php_amqplib.c @@ -0,0 +1,144 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "tlib_php.h" + +#include "php_agent.h" +#include "lib_php_amqplib.h" +#include "fw_support.h" + +tlib_parallel_info_t parallel_info + = {.suggested_nthreads = -1, .state_size = 0}; + +#if ZEND_MODULE_API_NO > ZEND_7_1_X_API_NO + +static void declare_php_amqplib_package_class(const char* ns, + const char* klass, + const char* package_version) { + char* source = nr_formatf( + "namespace %s;" + "class %s{" + "const VERSION = '%s';" + "}", + ns, klass, package_version); + + tlib_php_request_eval(source); + + nr_free(source); +} + +static void test_nr_lib_php_amqplib_handle_version(void) { +#define LIBRARY_NAME "php-amqplib/php-amqplib" + const char* library_versions[] + = {"7", "10", "100", "4.23", "55.34", "6123.45", "0.4.5"}; + nr_php_package_t* p = NULL; +#define TEST_DESCRIPTION_FMT \ + "nr_lib_php_amqplib_handle_version with library_versions[%ld]=%s: package " \ + "major version metric - %s" + char* test_description = NULL; + size_t i = 0; + + /* + * If lib_php_amqplib_handle_version function is ever called, we have already + * detected the php-amqplib library. + */ + + /* + * PhpAmqpLib/Package class exists. Should create php-amqplib package metric + * suggestion with version + */ + for (i = 0; i < sizeof(library_versions) / sizeof(library_versions[0]); i++) { + tlib_php_request_start(); + + declare_php_amqplib_package_class("PhpAmqpLib", "Package", + library_versions[i]); + nr_php_amqplib_handle_version(); + + p = nr_php_packages_get_package( + NRPRG(txn)->php_package_major_version_metrics_suggestions, + LIBRARY_NAME); + + test_description = nr_formatf(TEST_DESCRIPTION_FMT, i, library_versions[i], + "suggestion created"); + tlib_pass_if_not_null(test_description, p); + nr_free(test_description); + + test_description = nr_formatf(TEST_DESCRIPTION_FMT, i, library_versions[i], + "suggested version set"); + tlib_pass_if_str_equal(test_description, library_versions[i], + p->package_version); + nr_free(test_description); + + tlib_php_request_end(); + } + + /* + * PhpAmqpLib/Package class does not exist, should create package metric + * suggestion with PHP_PACKAGE_VERSION_UNKNOWN version. This case should never + * happen in real situations. + */ + tlib_php_request_start(); + + nr_php_amqplib_handle_version(); + + p = nr_php_packages_get_package( + NRPRG(txn)->php_package_major_version_metrics_suggestions, LIBRARY_NAME); + + tlib_pass_if_not_null( + "nr_lib_php_amqplib_handle_version when PhpAmqpLib\\Package class is not " + "defined - " + "suggestion created", + p); + tlib_pass_if_str_equal( + "nr_lib_php_amqplib_handle_version when PhpAmqpLib\\Package class is not " + "defined - " + "suggested version set to PHP_PACKAGE_VERSION_UNKNOWN", + PHP_PACKAGE_VERSION_UNKNOWN, p->package_version); + + tlib_php_request_end(); + + /* + * PhpAmqpLib\\Package class exists but VERSION does not. + * Should create package metric suggestion with PHP_PACKAGE_VERSION_UNKNOWN + * version. This case should never happen in real situations. + */ + tlib_php_request_start(); + + char* source + = "namespace PhpAmqpLib;" + "class Package{" + "const SADLY_DEPRECATED = 5.4;" + "}"; + + tlib_php_request_eval(source); + + nr_php_amqplib_handle_version(); + + p = nr_php_packages_get_package( + NRPRG(txn)->php_package_major_version_metrics_suggestions, LIBRARY_NAME); + + tlib_pass_if_not_null( + "nr_lib_php_amqplib_handle_version when PhpAmqpLib\\Package class is SET " + "but the const VERSION does not exist - " + "suggestion created", + p); + tlib_pass_if_str_equal( + "nr_lib_php_amqplib_handle_version when PhpAmqpLib\\Package class is SET " + "but the const VERSION does not exist - " + "defined - " + "suggested version set to PHP_PACKAGE_VERSION_UNKNOWN", + PHP_PACKAGE_VERSION_UNKNOWN, p->package_version); + + tlib_php_request_end(); +} + +void test_main(void* p NRUNUSED) { + tlib_php_engine_create(""); + test_nr_lib_php_amqplib_handle_version(); + tlib_php_engine_destroy(); +} +#else +void test_main(void* p NRUNUSED) {} +#endif diff --git a/agent/tests/test_memcached.c b/agent/tests/test_memcached.c new file mode 100644 index 000000000..1f05c2443 --- /dev/null +++ b/agent/tests/test_memcached.c @@ -0,0 +1,127 @@ +/* + * Copyright 2020 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +#include "tlib_php.h" +#include "tlib_datastore.h" + +#include "php_agent.h" +#include "php_memcached.h" +#include "util_system.h" + +tlib_parallel_info_t parallel_info + = {.suggested_nthreads = -1, .state_size = 0}; + +static char* system_host_name; + +static void test_create_datastore_instance(void) { + assert_datastore_instance_equals_destroy( + "named socket", + &((nr_datastore_instance_t){ + .host = system_host_name, + .database_name = "unknown", + .port_path_or_id = "/tmp/memcached.sock", + }), + nr_php_memcached_create_datastore_instance("/tmp/memcached.sock", 0)); + + assert_datastore_instance_equals_destroy( + "empty socket", + &((nr_datastore_instance_t){ + .host = system_host_name, + .database_name = "unknown", + .port_path_or_id = "unknown", + }), + nr_php_memcached_create_datastore_instance("", 0)); + + assert_datastore_instance_equals_destroy( + "empty host", + &((nr_datastore_instance_t){ + .host = system_host_name, + .database_name = "unknown", + .port_path_or_id = "unknown", + }), + nr_php_memcached_create_datastore_instance(NULL, 0)); + + assert_datastore_instance_equals_destroy( + "host.name socket", + &((nr_datastore_instance_t){ + .host = "host.name", + .database_name = "unknown", + .port_path_or_id = "11211", + }), + nr_php_memcached_create_datastore_instance("host.name", 11211)); + + assert_datastore_instance_equals_destroy( + "host and port", + &((nr_datastore_instance_t){ + .host = "unknown", + .database_name = "unknown", + .port_path_or_id = "6379", + }), + nr_php_memcached_create_datastore_instance("", 6379)); + + assert_datastore_instance_equals_destroy( + "NULL socket", + &((nr_datastore_instance_t){ + .host = "unknown", + .database_name = "unknown", + .port_path_or_id = "11211", + }), + nr_php_memcached_create_datastore_instance(NULL, 11211)); +} + +static void test_create_instance_metric(void) { + nrtxn_t* txn; + nrmetric_t* metric; + char* metric_str; + tlib_php_engine_create(""); + tlib_php_request_start(); + txn = NRPRG(txn); + + nr_php_memcached_create_instance_metric("host", 11211); + metric = nrm_find(txn->unscoped_metrics, "Datastore/instance/Memcached/host/11211"); + tlib_pass_if_not_null("metric found", metric); + + nr_php_memcached_create_instance_metric("", 11211); + metric = nrm_find(txn->unscoped_metrics, "Datastore/instance/Memcached/unknown/11211"); + tlib_pass_if_not_null("metric found", metric); + + nr_php_memcached_create_instance_metric(NULL, 7); + metric = nrm_find(txn->unscoped_metrics, "Datastore/instance/Memcached/unknown/7"); + tlib_pass_if_not_null("metric found", metric); + + nr_php_memcached_create_instance_metric("path/to/sock", 0); + metric_str = nr_formatf("Datastore/instance/Memcached/%s/path/to/sock", system_host_name); + metric = nrm_find(txn->unscoped_metrics, metric_str); + nr_free(metric_str); + tlib_pass_if_not_null("metric found", metric); + + nr_php_memcached_create_instance_metric("", 0); + metric_str = nr_formatf("Datastore/instance/Memcached/%s/unknown", system_host_name); + metric = nrm_find(txn->unscoped_metrics, metric_str); + nr_free(metric_str); + tlib_pass_if_not_null("metric found", metric); + + // restart the transaction because the next metric is the same as a previous metric + tlib_php_request_end(); + tlib_php_request_start(); + txn = NRPRG(txn); + + nr_php_memcached_create_instance_metric(NULL, 0); + metric_str = nr_formatf("Datastore/instance/Memcached/%s/unknown", system_host_name); + metric = nrm_find(txn->unscoped_metrics, metric_str); + nr_free(metric_str); + tlib_pass_if_not_null("metric found", metric); + + tlib_php_request_end(); + tlib_php_engine_destroy(); +} + +void test_main(void* p NRUNUSED) { + system_host_name = nr_system_get_hostname(); + + test_create_datastore_instance(); + test_create_instance_metric(); + + nr_free(system_host_name); +} diff --git a/agent/tests/test_php_txn.c b/agent/tests/test_php_txn.c new file mode 100644 index 000000000..d0665bb26 --- /dev/null +++ b/agent/tests/test_php_txn.c @@ -0,0 +1,268 @@ +/* + * Copyright 2020 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "tlib_php.h" + +#include "php_agent.h" +#include "php_txn_private.h" + +#define LIBRARY_NAME "vendor_name/package_name" +#define LIBRARY_VERSION "1.2.3" +#define DEVEL_LIBRARY_VERSION "1.2.x-dev" +#define LIBRARY_MAJOR_VERSION "1" +#define COMPOSER_PACKAGE_VERSION "2.1.3" +#define COMPOSER_MAJOR_VERSION "2" +#define PACKAGE_METRIC_PREFIX "Supportability/PHP/package/" +#define PACKAGE_METRIC PACKAGE_METRIC_PREFIX LIBRARY_NAME + +tlib_parallel_info_t parallel_info = {.suggested_nthreads = 1, .state_size = 0}; + +nr_php_package_t php_package + = {.package_name = LIBRARY_NAME, + .package_version = LIBRARY_VERSION, + .source_priority = NR_PHP_PACKAGE_SOURCE_COMPOSER}; + +static void test_nr_php_txn_php_package_create_major_metric() { + nrtxn_t t; + nrtxn_t* txn = &t; + + txn->unscoped_metrics = nrm_table_create(10); + txn->php_packages = nr_php_packages_create(); + txn->php_package_major_version_metrics_suggestions = nr_php_packages_create(); + + tlib_php_request_start(); + + /* need to call callback with invalid values to make sure it doesnt crash + * code depends on txn and txn->php_packages existing so these are created + * above with the package suggestions data structure included for good measure + */ + + /* this tests these params in callback: + * suggested = NULL + * actual = NULL + * key = NULL + * txn = NULL + */ + nr_php_txn_php_package_create_major_metric(NULL, NULL, 0, NULL); + tlib_pass_if_int_equal("NULL txn, metric not created", 0, + nrm_table_size(txn->unscoped_metrics)); + + /* this tests these params in callback: + * suggested = NULL + * actual = NULL + * key != NULL + * txn != NULL + */ + nr_php_txn_php_package_create_major_metric(NULL, LIBRARY_NAME, + strlen(LIBRARY_NAME), (void*)txn); + tlib_pass_if_int_equal("NULL value, metric not created", 0, + nrm_table_size(txn->unscoped_metrics)); + + /* the key is not actually used by the callback - just the package name + * in the suggested package so this casee will still create a metric + */ + /* this tests these params in callback: + * suggested != NULL + * actual = NULL + * key = NULL + * txn = != NULL + */ + nr_php_txn_php_package_create_major_metric(&php_package, NULL, 0, (void*)txn); + tlib_pass_if_int_equal("NULL key, metric created", 1, + nrm_table_size(txn->unscoped_metrics)); + + /* cleanup */ + nr_php_packages_destroy(&txn->php_packages); + nr_php_packages_destroy(&txn->php_package_major_version_metrics_suggestions); + nrm_table_destroy(&txn->unscoped_metrics); + + tlib_php_request_end(); +} + +static void test_nr_php_txn_create_packages_major_metrics() { + nrtxn_t t; + nrtxn_t* txn = &t; + + txn->unscoped_metrics = nrm_table_create(10); + txn->php_packages = nr_php_packages_create(); + txn->php_package_major_version_metrics_suggestions = nr_php_packages_create(); + + tlib_php_request_start(); + + /* invalid txn should not crash */ + nr_php_txn_create_packages_major_metrics(NULL); + tlib_pass_if_int_equal("NULL txn, metric not created", 0, + nrm_table_size(txn->unscoped_metrics)); + + /* test with valid txn no package suggestions */ + nr_php_txn_create_packages_major_metrics(txn); + tlib_pass_if_int_equal("valid txn with no suggestions, metric not created", 0, + nrm_table_size(txn->unscoped_metrics)); + + /* + * Tests: + * 1. suggestion with NULL version, no packages + * 2. suggestion with PHP_PACKAGE_VERSION_UNKNOWN version, no packages + * 3. suggestion with known version, no packages + * 4. package with known version and suggestion with known version + * 5. package with known version and suggestion with unknown version + * 6. package with unknown version and suggestion with known version + * 7. package with unknown version and suggestion with unknown version + * 8. test that causes "actual" to be NULL in callback + * 9. package with known "dev" version with and suggestion with unknown version + */ + + /* 1. suggestion with NULL version, no packages */ + nr_txn_suggest_package_supportability_metric(txn, LIBRARY_NAME, NULL); + nr_php_txn_create_packages_major_metrics(txn); + tlib_pass_if_int_equal("suggestion with NULL version, metric not created", 0, + nrm_table_size(txn->unscoped_metrics)); + + /* 2. suggestion with PHP_PACKAGE_VERSION_UNKNOWN version, no packages + * also + * 8. test that causes "actual" to be NULL in callback + */ + nr_txn_suggest_package_supportability_metric(txn, LIBRARY_NAME, + PHP_PACKAGE_VERSION_UNKNOWN); + nr_php_txn_create_packages_major_metrics(txn); + tlib_pass_if_int_equal( + "suggestion with PHP_PACKAGE_VERSION_UNKNOWN version, metric not created", + 0, nrm_table_size(txn->unscoped_metrics)); + + /* 3. suggestion with known version, no packages + * also + * 8. test that causes "actual" to be NULL in callback + */ + nr_txn_suggest_package_supportability_metric(txn, LIBRARY_NAME, + LIBRARY_VERSION); + nr_php_txn_create_packages_major_metrics(txn); + tlib_pass_if_int_equal("suggestion with valid version, metric created", 1, + nrm_table_size(txn->unscoped_metrics)); + tlib_pass_if_not_null( + "php package major version is used for 'detected' metric", + nrm_find(txn->unscoped_metrics, + PACKAGE_METRIC "/" LIBRARY_MAJOR_VERSION "/detected")); + + /* reset metrics */ + nrm_table_destroy(&txn->unscoped_metrics); + txn->unscoped_metrics = nrm_table_create(10); + + /* 4. package with known version and suggestion with known version + * + * add a package with a "better" version determined from composer api + * and use existing suggestion which has a different version + */ + nr_txn_add_php_package_from_source(txn, LIBRARY_NAME, + COMPOSER_PACKAGE_VERSION, + NR_PHP_PACKAGE_SOURCE_COMPOSER); + nr_php_txn_create_packages_major_metrics(txn); + tlib_pass_if_int_equal("suggestion with valid version, metric created", 1, + nrm_table_size(txn->unscoped_metrics)); + tlib_pass_if_not_null( + "php package major version is used for 'detected' metric", + nrm_find(txn->unscoped_metrics, + PACKAGE_METRIC "/" COMPOSER_MAJOR_VERSION "/detected")); + + /* reset suggestions, leave package with known version in place */ + nr_php_packages_destroy(&txn->php_package_major_version_metrics_suggestions); + txn->php_package_major_version_metrics_suggestions = nr_php_packages_create(); + + /* reset metrics */ + nrm_table_destroy(&txn->unscoped_metrics); + txn->unscoped_metrics = nrm_table_create(10); + + /* 5. package with known version and suggestion with unknown version + * + * add a suggestion with no version and test metric uses package version + */ + nr_txn_suggest_package_supportability_metric(txn, LIBRARY_NAME, + PHP_PACKAGE_VERSION_UNKNOWN); + nr_php_txn_create_packages_major_metrics(txn); + tlib_pass_if_int_equal("suggestion with valid version, metric created", 1, + nrm_table_size(txn->unscoped_metrics)); + tlib_pass_if_not_null( + "php package major version is used for 'detected' metric", + nrm_find(txn->unscoped_metrics, + PACKAGE_METRIC "/" COMPOSER_MAJOR_VERSION "/detected")); + + /* reset everything */ + nrm_table_destroy(&txn->unscoped_metrics); + txn->unscoped_metrics = nrm_table_create(10); + nr_php_packages_destroy(&txn->php_packages); + txn->php_packages = nr_php_packages_create(); + nr_php_packages_destroy(&txn->php_package_major_version_metrics_suggestions); + txn->php_package_major_version_metrics_suggestions = nr_php_packages_create(); + + /* 6. package with unknown version and suggestion with known version */ + nr_txn_suggest_package_supportability_metric(txn, LIBRARY_NAME, + LIBRARY_VERSION); + nr_txn_add_php_package_from_source(txn, LIBRARY_NAME, + PHP_PACKAGE_VERSION_UNKNOWN, + NR_PHP_PACKAGE_SOURCE_COMPOSER); + nr_php_txn_create_packages_major_metrics(txn); + tlib_pass_if_int_equal("suggestion with valid version, metric created", 1, + nrm_table_size(txn->unscoped_metrics)); + tlib_pass_if_not_null( + "php package suggestion major version is used for 'detected' metric", + nrm_find(txn->unscoped_metrics, + PACKAGE_METRIC "/" LIBRARY_MAJOR_VERSION "/detected")); + + /* reset everything */ + nrm_table_destroy(&txn->unscoped_metrics); + txn->unscoped_metrics = nrm_table_create(10); + nr_php_packages_destroy(&txn->php_packages); + txn->php_packages = nr_php_packages_create(); + nr_php_packages_destroy(&txn->php_package_major_version_metrics_suggestions); + txn->php_package_major_version_metrics_suggestions = nr_php_packages_create(); + + /* 7. package with unknown version and suggestion with unknown version */ + nr_txn_suggest_package_supportability_metric(txn, LIBRARY_NAME, + PHP_PACKAGE_VERSION_UNKNOWN); + nr_txn_add_php_package_from_source(txn, LIBRARY_NAME, + PHP_PACKAGE_VERSION_UNKNOWN, + NR_PHP_PACKAGE_SOURCE_COMPOSER); + nr_php_txn_create_packages_major_metrics(txn); + tlib_pass_if_int_equal( + "suggestion and package w/o version, metric not created", 0, + nrm_table_size(txn->unscoped_metrics)); + + /* reset everything */ + nrm_table_destroy(&txn->unscoped_metrics); + txn->unscoped_metrics = nrm_table_create(10); + nr_php_packages_destroy(&txn->php_packages); + txn->php_packages = nr_php_packages_create(); + nr_php_packages_destroy(&txn->php_package_major_version_metrics_suggestions); + txn->php_package_major_version_metrics_suggestions = nr_php_packages_create(); + + /* 9. package with known "dev" version with and suggestion with unknown version */ + nr_txn_suggest_package_supportability_metric(txn, LIBRARY_NAME, + PHP_PACKAGE_VERSION_UNKNOWN); + nr_txn_add_php_package_from_source(txn, LIBRARY_NAME, + DEVEL_LIBRARY_VERSION, + NR_PHP_PACKAGE_SOURCE_COMPOSER); + nr_php_txn_create_packages_major_metrics(txn); + tlib_pass_if_int_equal("suggestion with valid 'dev' version, metric created", 1, + nrm_table_size(txn->unscoped_metrics)); + tlib_pass_if_not_null( + "php package suggestion major version with 'dev' is used for 'detected' metric", + nrm_find(txn->unscoped_metrics, + PACKAGE_METRIC "/" LIBRARY_MAJOR_VERSION "/detected")); + + + /* cleanup */ + nr_php_packages_destroy(&txn->php_packages); + nr_php_packages_destroy(&txn->php_package_major_version_metrics_suggestions); + nrm_table_destroy(&txn->unscoped_metrics); + + tlib_php_request_end(); +} + + +void test_main(void* p NRUNUSED) { + tlib_php_engine_create(""); + test_nr_php_txn_php_package_create_major_metric(); + test_nr_php_txn_create_packages_major_metrics(); + tlib_php_engine_destroy(); +} diff --git a/agent/tests/test_txn.c b/agent/tests/test_txn.c index 3209e3c27..50c4ff8bc 100644 --- a/agent/tests/test_txn.c +++ b/agent/tests/test_txn.c @@ -161,6 +161,133 @@ static void test_max_segments_config_values(TSRMLS_D) { tlib_php_request_end(); } +#define PHP_VERSION_METRIC_BASE "Supportability/PHP/Version" +#define AGENT_VERSION_METRIC_BASE "Supportability/PHP/AgentVersion" + +static void test_create_php_version_metric() { + nrtxn_t* txn; + int count; + + tlib_php_request_start(); + txn = NRPRG(txn); + + count = nrm_table_size(txn->unscoped_metrics); + + /* Test invalid values are properly handled */ + nr_php_txn_create_php_version_metric(NULL, NULL); + tlib_pass_if_int_equal("PHP version metric shouldnt be created 1", count, + nrm_table_size(txn->unscoped_metrics)); + + nr_php_txn_create_php_version_metric(txn, NULL); + tlib_pass_if_int_equal("PHP version metric shouldnt be created 2", count, + nrm_table_size(txn->unscoped_metrics)); + + nr_php_txn_create_php_version_metric(NULL, "7.4.0"); + tlib_pass_if_int_equal("PHP version metric shouldnt be created 3", count, + nrm_table_size(txn->unscoped_metrics)); + + nr_php_txn_create_php_version_metric(txn, ""); + tlib_pass_if_int_equal("PHP version metric shouldnt be created 4", count, + nrm_table_size(txn->unscoped_metrics)); + + /* test valid values */ + nr_php_txn_create_php_version_metric(txn, "7.4.0"); + tlib_pass_if_int_equal("PHP version metric should be create", count + 1, + nrm_table_size(txn->unscoped_metrics)); + + const nrmetric_t* metric + = nrm_find(txn->unscoped_metrics, PHP_VERSION_METRIC_BASE "/7.4.0"); + const char* metric_name = nrm_get_name(txn->unscoped_metrics, metric); + + tlib_pass_if_not_null("PHP version metric found", metric); + tlib_pass_if_str_equal("PHP version metric name check", metric_name, + PHP_VERSION_METRIC_BASE "/7.4.0"); + + tlib_php_request_end(); +} + +static void test_create_agent_version_metric() { + nrtxn_t* txn; + int count; + + tlib_php_request_start(); + txn = NRPRG(txn); + + count = nrm_table_size(txn->unscoped_metrics); + + /* Test invalid values are properly handled */ + nr_php_txn_create_agent_version_metric(NULL); + tlib_pass_if_int_equal("Agent version metric shouldnt be created - txn is NULL", count, + nrm_table_size(txn->unscoped_metrics)); + + /* Test valid values */ + nr_php_txn_create_agent_version_metric(txn); + tlib_pass_if_int_equal("Agent version metric should be created - txn is not NULL", count + 1, + nrm_table_size(txn->unscoped_metrics)); + + const nrmetric_t* metric + = nrm_find(txn->unscoped_metrics, AGENT_VERSION_METRIC_BASE "/" NR_VERSION); + const char* metric_name = nrm_get_name(txn->unscoped_metrics, metric); + + tlib_pass_if_not_null("Agent version metric found", metric); + tlib_pass_if_str_equal("Agent version metric name check", metric_name, + AGENT_VERSION_METRIC_BASE "/" NR_VERSION); + + tlib_php_request_end(); +} + +static void test_create_agent_php_version_metrics() { + nrtxn_t* txn; + + /* + * Test : Create agent PHP version metrics. + */ + tlib_php_request_start(); + txn = NRPRG(txn); + + zval* expected_php_zval = tlib_php_request_eval_expr("phpversion();"); + + char* php_version_name = nr_formatf(PHP_VERSION_METRIC_BASE "/%s", + Z_STRVAL_P(expected_php_zval)); + + nr_php_zval_free(&expected_php_zval); + + char* agent_version_name + = nr_formatf(AGENT_VERSION_METRIC_BASE "/%s", NR_VERSION); + + nr_php_txn_create_agent_php_version_metrics(txn); + + /* Test the PHP version metric creation */ + const nrmetric_t* metric = nrm_find(txn->unscoped_metrics, php_version_name); + const char* metric_name = nrm_get_name(txn->unscoped_metrics, metric); + + tlib_pass_if_not_null("happy path: PHP version metric created", metric); + tlib_pass_if_not_null("happy path: PHP version metric name created", + metric_name); + + tlib_pass_if_str_equal("happy path: PHP version metric name check", + metric_name, php_version_name); + + /* Test the agent version metric creation*/ + metric = nrm_find(txn->unscoped_metrics, agent_version_name); + metric_name = nrm_get_name(txn->unscoped_metrics, metric); + + tlib_pass_if_not_null("happy path: Agent version metric created", metric); + tlib_pass_if_not_null("happy path: Agent version metric name created", + metric_name); + + tlib_pass_if_str_equal("happy path: Agent version metric name check", + metric_name, agent_version_name); + + nr_free(agent_version_name); + nr_free(php_version_name); + + tlib_php_request_end(); +} + +#undef PHP_VERSION_METRIC_BASE +#undef AGENT_VERSION_METRIC_BASE + tlib_parallel_info_t parallel_info = {.suggested_nthreads = 1, .state_size = 0}; void test_main(void* p NRUNUSED) { @@ -175,8 +302,11 @@ void test_main(void* p NRUNUSED) { tlib_php_engine_create( "newrelic.transaction_events.attributes.include=request.uri" PTSRMLS_CC); - test_handle_fpm_error(TSRMLS_C); - test_max_segments_config_values(TSRMLS_C); + test_handle_fpm_error(); + test_max_segments_config_values(); + test_create_php_version_metric(); + test_create_agent_version_metric(); + test_create_agent_php_version_metrics(); - tlib_php_engine_destroy(TSRMLS_C); + tlib_php_engine_destroy(); } diff --git a/axiom/Makefile b/axiom/Makefile index 34b45c229..4cce3a6f6 100644 --- a/axiom/Makefile +++ b/axiom/Makefile @@ -113,6 +113,7 @@ OBJS := \ nr_segment_children.o \ nr_segment_datastore.o \ nr_segment_external.o \ + nr_segment_message.o \ nr_segment_private.o \ nr_segment_terms.o \ nr_segment_traces.o \ diff --git a/axiom/nr_distributed_trace.c b/axiom/nr_distributed_trace.c index 151ae6406..25fcd27d5 100644 --- a/axiom/nr_distributed_trace.c +++ b/axiom/nr_distributed_trace.c @@ -481,14 +481,30 @@ void nr_distributed_trace_set_app_id(nr_distributed_trace_t* dt, } void nr_distributed_trace_set_trace_id(nr_distributed_trace_t* dt, - const char* trace_id) { + const char* trace_id, + bool do_padding) { if (NULL == dt) { return; } nr_free(dt->trace_id); if (trace_id) { - dt->trace_id = nr_strdup(trace_id); + if (nrunlikely(do_padding)) { + int len = nr_strlen(trace_id); + if (len < NR_TRACE_ID_MAX_SIZE) { + int padding = NR_TRACE_ID_MAX_SIZE - len; + char* dest = (char*)nr_malloc(NR_TRACE_ID_MAX_SIZE + 1); + for (int i = 0; i < padding; i++) { + dest[i] = '0'; + } + nr_strcpy(dest + padding, trace_id); + dt->trace_id = dest; + } else { + dt->trace_id = nr_strdup(trace_id); + } + } else { + dt->trace_id = nr_strdup(trace_id); + } } } diff --git a/axiom/nr_distributed_trace.h b/axiom/nr_distributed_trace.h index c84010dfa..f59d7ae24 100644 --- a/axiom/nr_distributed_trace.h +++ b/axiom/nr_distributed_trace.h @@ -12,6 +12,9 @@ #include "util_time.h" #include "util_object.h" +/* This is the maximum size of trace id that the backend will accept */ +#define NR_TRACE_ID_MAX_SIZE 32 + static const char NR_DISTRIBUTED_TRACE_ACCEPT_SUCCESS[] = "Supportability/DistributedTrace/AcceptPayload/Success"; static const char NR_DISTRIBUTED_TRACE_ACCEPT_EXCEPTION[] @@ -242,9 +245,12 @@ extern void nr_distributed_trace_set_app_id(nr_distributed_trace_t* dt, * * Params : 1. The distributed trace. * 2. The trace id. + * 3. Bool where true indicates to left pad the trace_id + * with '0's to make it NR_TRACE_ID_MAX_SIZE characters */ void nr_distributed_trace_set_trace_id(nr_distributed_trace_t* dt, - const char* trace_id); + const char* trace_id, + bool do_padding); /* * Purpose : Set the distributed trace priority. diff --git a/axiom/nr_header.c b/axiom/nr_header.c index f70398e4d..377096dd1 100644 --- a/axiom/nr_header.c +++ b/axiom/nr_header.c @@ -48,7 +48,8 @@ nr_hashmap_t* nr_header_create_distributed_trace_map(const char* nr_header, return NULL; } - header_map = nr_hashmap_create(NULL); + header_map = nr_hashmap_create((nr_hashmap_dtor_func_t)nr_hashmap_dtor_str); + if (nr_header) { nr_hashmap_set(header_map, NR_PSTR(NEWRELIC), nr_strdup(nr_header)); } diff --git a/axiom/nr_php_packages.c b/axiom/nr_php_packages.c index 9c1f97ef8..b8a034086 100644 --- a/axiom/nr_php_packages.c +++ b/axiom/nr_php_packages.c @@ -23,7 +23,23 @@ typedef struct { bool package_added; } nr_php_package_json_builder_t; -nr_php_package_t* nr_php_package_create(char* name, char* version) { +static inline const char* nr_php_package_source_priority_to_string(const nr_php_package_source_priority_t source_priority) { + switch (source_priority) { + case NR_PHP_PACKAGE_SOURCE_SUGGESTION: + return "suggestion"; + case NR_PHP_PACKAGE_SOURCE_LEGACY: + return "legacy"; + case NR_PHP_PACKAGE_SOURCE_COMPOSER: + return "composer"; + default: + return "unknown"; + } +} + +nr_php_package_t* nr_php_package_create_with_source( + const char* name, + const char* version, + const nr_php_package_source_priority_t source_priority) { nr_php_package_t* p = NULL; if (NULL == name) { @@ -43,12 +59,17 @@ nr_php_package_t* nr_php_package_create(char* name, char* version) { PHP_PACKAGE_VERSION_UNKNOWN); // if null, version is set to an empty // string with a space according to spec } + p->source_priority = source_priority; - nrl_verbosedebug(NRL_INSTRUMENT, "Creating PHP Package '%s', version '%s'", - p->package_name, p->package_version); + nrl_verbosedebug(NRL_INSTRUMENT, "Creating PHP Package '%s', version '%s', source %s", + p->package_name, p->package_version, nr_php_package_source_priority_to_string(source_priority)); return p; } +nr_php_package_t* nr_php_package_create(const char* name, const char* version) { + return nr_php_package_create_with_source(name, version, NR_PHP_PACKAGE_SOURCE_LEGACY); +} + void nr_php_package_destroy(nr_php_package_t* p) { if (NULL != p) { nr_free(p->package_name); @@ -72,14 +93,15 @@ nr_php_packages_t* nr_php_packages_create() { return h; } -void nr_php_packages_add_package(nr_php_packages_t* h, nr_php_package_t* p) { +nr_php_package_t* nr_php_packages_add_package(nr_php_packages_t* h, + nr_php_package_t* p) { nr_php_package_t* package; if (NULL == h) { - return; + return NULL; } if (NULL == p || NULL == p->package_name || NULL == p->package_version) { - return; + return NULL; } // If package with the same key already exists, we will check if the value is @@ -87,15 +109,26 @@ void nr_php_packages_add_package(nr_php_packages_t* h, nr_php_package_t* p) { package = (nr_php_package_t*)nr_hashmap_get(h->data, p->package_name, nr_strlen(p->package_name)); if (NULL != package) { - if (0 != nr_strcmp(package->package_version, p->package_version)) { + if (package->source_priority <= p->source_priority && 0 != nr_strcmp(package->package_version, p->package_version)) { nr_free(package->package_version); package->package_version = nr_strdup(p->package_version); } nr_php_package_destroy(p); - return; + return package; } nr_hashmap_set(h->data, p->package_name, nr_strlen(p->package_name), p); + return p; +} + +void nr_php_packages_iterate(nr_php_packages_t* packages, + nr_php_packages_iter_t callback, + void* userdata) { + if (NULL == packages || NULL == callback) { + return; + } + + nr_hashmap_apply(packages->data, (nr_hashmap_apply_func_t)callback, userdata); } char* nr_php_package_to_json(nr_php_package_t* package) { diff --git a/axiom/nr_php_packages.h b/axiom/nr_php_packages.h index 118a63f79..f4945c43e 100644 --- a/axiom/nr_php_packages.h +++ b/axiom/nr_php_packages.h @@ -10,32 +10,65 @@ #include "util_random.h" #include "util_vector.h" #include "util_hashmap.h" +#include "util_strings.h" #define PHP_PACKAGE_VERSION_UNKNOWN " " +typedef enum { + NR_PHP_PACKAGE_SOURCE_SUGGESTION, + NR_PHP_PACKAGE_SOURCE_LEGACY, + NR_PHP_PACKAGE_SOURCE_COMPOSER +} nr_php_package_source_priority_t; + typedef struct _nr_php_package_t { char* package_name; char* package_version; + nr_php_package_source_priority_t source_priority; } nr_php_package_t; typedef struct _nr_php_packages_t { nr_hashmap_t* data; } nr_php_packages_t; +typedef void(nr_php_packages_iter_t)(void* value, + const char* name, + size_t name_len, + void* user_data); + /* - * Purpose : Create a new php package. If the name is null, then no package will + * Purpose : Create a new php package with desired source priority. If the name is null, then no package will * be created. If the version is null (version = NULL), then * the package will still be created and the version will be set to an * empty string with a space. * * Params : 1. Package name * 2. Package version + * 3. Package source priority (legacy or composer) * * Returns : A php package that has a name and version. If * nr_php_packages_add_package() is not called, then it must be freed * by nr_php_package_destroy() */ -extern nr_php_package_t* nr_php_package_create(char* name, char* version); +extern nr_php_package_t* nr_php_package_create_with_source( + const char* name, + const char* version, + const nr_php_package_source_priority_t source_priority); + +/* + * Purpose : Create a new php package with legacy source priority. If the name is null, then no package will + * be created. If the version is null (version = NULL), then + * the package will still be created and the version will be set to an + * empty string with a space. + * + * Params : 1. Package name + * 2. Package version + * + * Returns : A php package that has a name and version. If + * nr_php_packages_add_package() is not called, then it must be freed + * by nr_php_package_destroy() + */ +extern nr_php_package_t* nr_php_package_create(const char* name, + const char* version); /* * Purpose : Destroy/free php package @@ -64,10 +97,10 @@ extern nr_php_packages_t* nr_php_packages_create(void); * 2. A pointer to the php package that needs to be added to the * collection * - * Returns : Nothing + * Returns : pointer to added package on success or NULL otherwise. */ -extern void nr_php_packages_add_package(nr_php_packages_t* h, - nr_php_package_t* p); +extern nr_php_package_t* nr_php_packages_add_package(nr_php_packages_t* h, + nr_php_package_t* p); /* * Purpose : Destroy/free the collection @@ -118,6 +151,40 @@ static inline int nr_php_packages_has_package(nr_php_packages_t* h, return 0; } +/* + * Purpose : Retrieve a pointer to php package from the collection + * + * Params : 1. A pointer to nr_php_packages_t + * 2. The name of the package to retrieve + * + * Returns : Returns pointer to php package if the package exists or NULL + */ +static inline nr_php_package_t* nr_php_packages_get_package( + nr_php_packages_t* php_packages, + const char* package_name) { + if (NULL == package_name) { + return NULL; + } + + if (nrlikely(NULL != php_packages && NULL != php_packages->data)) { + return (nr_php_package_t*)nr_hashmap_get(php_packages->data, package_name, nr_strlen(package_name)); + } + return NULL; +} + +/* + * Purpose : Iterate over packages calling callback function + * + * Params : 1. A pointer to nr_php_packages_t + * 2. Callback function (nr_php_packages_iter_t) + * 3. Pointer to user data (can be NULL) + * + * Returns : Nothing + */ +void nr_php_packages_iterate(nr_php_packages_t* packages, + nr_php_packages_iter_t callback, + void* userdata); + /* * Purpose : Converts a package to a json * diff --git a/axiom/nr_segment.c b/axiom/nr_segment.c index fa91cf1b7..ade63d454 100644 --- a/axiom/nr_segment.c +++ b/axiom/nr_segment.c @@ -313,6 +313,35 @@ static void nr_populate_http_spans(nr_span_event_t* span_event, segment->typed_attributes->external.status); } +static void nr_populate_message_spans(nr_span_event_t* span_event, + const nr_segment_t* segment) { + nr_span_event_set_category(span_event, NR_SPAN_MESSAGE); + + if (nrunlikely(NULL == segment || NULL == segment->typed_attributes)) { + return; + } + + nr_span_event_set_spankind(span_event, + segment->typed_attributes->message.message_action); + nr_span_event_set_message( + span_event, NR_SPAN_MESSAGE_DESTINATION_NAME, + segment->typed_attributes->message.destination_name); + nr_span_event_set_message( + span_event, NR_SPAN_MESSAGE_MESSAGING_SYSTEM, + segment->typed_attributes->message.messaging_system); + nr_span_event_set_message(span_event, NR_SPAN_MESSAGE_SERVER_ADDRESS, + segment->typed_attributes->message.server_address); + nr_span_event_set_message( + span_event, NR_SPAN_MESSAGE_MESSAGING_DESTINATION_ROUTING_KEY, + segment->typed_attributes->message.messaging_destination_routing_key); + nr_span_event_set_message( + span_event, NR_SPAN_MESSAGE_MESSAGING_DESTINATION_PUBLISH_NAME, + segment->typed_attributes->message.messaging_destination_publish_name); + nr_span_event_set_message_ulong( + span_event, NR_SPAN_MESSAGE_SERVER_PORT, + segment->typed_attributes->message.server_port); +} + static nr_status_t add_user_attribute_to_span_event(const char* key, const nrobj_t* val, void* ptr) { @@ -431,8 +460,8 @@ nr_span_event_t* nr_segment_to_span_event(nr_segment_t* segment) { nr_span_event_set_trusted_parent_id( event, nr_distributed_trace_inbound_get_trusted_parent_id( segment->txn->distributed_trace)); - nr_span_event_set_parent_id(event, - nr_distributed_trace_inbound_get_guid(segment->txn->distributed_trace)); + nr_span_event_set_parent_id(event, nr_distributed_trace_inbound_get_guid( + segment->txn->distributed_trace)); nr_span_event_set_transaction_name(event, segment->txn->name); @@ -482,6 +511,10 @@ nr_span_event_t* nr_segment_to_span_event(nr_segment_t* segment) { nr_populate_http_spans(event, segment); break; + case NR_SEGMENT_MESSAGE: + nr_populate_message_spans(event, segment); + break; + case NR_SEGMENT_CUSTOM: nr_span_event_set_category(event, NR_SPAN_GENERIC); break; @@ -599,6 +632,33 @@ bool nr_segment_set_external(nr_segment_t* segment, return true; } +bool nr_segment_set_message(nr_segment_t* segment, + const nr_segment_message_t* message) { + if (nrunlikely((NULL == segment) || (NULL == message))) { + return false; + } + + nr_segment_destroy_typed_attributes(segment->type, + &segment->typed_attributes); + segment->type = NR_SEGMENT_MESSAGE; + segment->typed_attributes = nr_zalloc(sizeof(nr_segment_typed_attributes_t)); + + // clang-format off + // Initialize the fields of the message attributes, one field per line. + segment->typed_attributes->message = (nr_segment_message_t){ + .message_action = message->message_action, + .destination_name = nr_strempty(message->destination_name) ? NULL: nr_strdup(message->destination_name), + .messaging_system = nr_strempty(message->messaging_system) ? NULL: nr_strdup(message->messaging_system), + .messaging_destination_routing_key = nr_strempty(message->messaging_destination_routing_key) ? NULL: nr_strdup(message->messaging_destination_routing_key), + .messaging_destination_publish_name = nr_strempty(message->messaging_destination_publish_name) ? NULL: nr_strdup(message->messaging_destination_publish_name), + .server_address = nr_strempty(message->server_address) ? NULL: nr_strdup(message->server_address), + .server_port = message->server_port, + }; + // clang-format on + + return true; +} + bool nr_segment_add_child(nr_segment_t* parent, nr_segment_t* child) { if (nrunlikely((NULL == parent) || (NULL == child))) { return false; diff --git a/axiom/nr_segment.h b/axiom/nr_segment.h index 56d972579..646f11e5f 100644 --- a/axiom/nr_segment.h +++ b/axiom/nr_segment.h @@ -34,7 +34,8 @@ typedef struct _nrtxn_t nrtxn_t; typedef enum _nr_segment_type_t { NR_SEGMENT_CUSTOM, NR_SEGMENT_DATASTORE, - NR_SEGMENT_EXTERNAL + NR_SEGMENT_EXTERNAL, + NR_SEGMENT_MESSAGE } nr_segment_type_t; /* @@ -109,6 +110,61 @@ typedef struct _nr_segment_external_t { uint64_t status; } nr_segment_external_t; +typedef struct _nr_segment_message_t { + /* + * Attributes needed for entity relationship building. + * Compare to OTEL attributes: + * https://opentelemetry.io/docs/specs/semconv/attributes-registry/cloud/ + * cloud.account.id, cloud.region, messaging.system and server.address are + * used to create relationships between APM and cloud services. It may not + * make sense to add these attributes unless they are used for creating one of + * the relationships in Entity Relationships. + */ + + nr_span_spankind_t + message_action; /*The action of the message, e.g.,Produce/Consume.*/ + char* destination_name; /*The name of the Queue, Topic, or Exchange; + otherwise, Temp. Needed for SQS relationship.*/ + char* messaging_system; /* for ex: aws_sqs. Needed for SQS relationship.*/ + char* server_address; /*The server domain name or IP address. Needed for + MQBROKER relationship.*/ + char* + messaging_destination_publish_name; /* Otel attribute for message + consumers. (In the agent, this + means Action is Consume in the span + name). This attribute is equal to + the corresponding attribute + messaging.destination.name from the + producer. This attribute is needed + for apps using RabbitMQ and it + represents the exchange name.*/ + char* messaging_destination_routing_key; /* The routing key for a RabbitMQ + operation.*/ + uint64_t server_port; /*The server port.*/ +} nr_segment_message_t; + +typedef struct _nr_segment_cloud_attrs_t { + /* + * Attributes needed for entity relationship building. + * Compare to OTEL attributes: + * https://opentelemetry.io/docs/specs/semconv/attributes-registry/cloud/ + * cloud.account.id, cloud.region, messaging.system and server.address are + * used to create relationships between APM and cloud services. It may not + * make sense to add these attributes unless they are used for creating one of + * the relationships in Entity Relationships. + * These attributes aren't specific to a segment category so don't belong as + * typed attributes and can be added whenever they are available. + */ + char* cloud_region; /*Targeted region; ex:us-east-1*. Needed for SQS + relationship.*/ + char* cloud_account_id; /*The cloud provider account ID. Needed for SQS + relationship.*/ + char* cloud_resource_id; /*Unique cloud provider identifier. For AWS, this is + the ARN of the AWS resource being accessed.*/ + char* aws_operation; /*AWS specific operation name.*/ + +} nr_segment_cloud_attrs_t; + typedef struct _nr_segment_metric_t { char* name; bool scoped; @@ -132,6 +188,7 @@ typedef struct _nr_segment_error_t { typedef union { nr_segment_datastore_t datastore; nr_segment_external_t external; + nr_segment_message_t message; } nr_segment_typed_attributes_t; typedef struct _nr_segment_t { @@ -179,8 +236,8 @@ typedef struct _nr_segment_t { int priority; /* Used to determine which segments are preferred for span event creation */ nr_segment_typed_attributes_t* typed_attributes; /* Attributes specific to - external or datastore - segments. */ + external, datastore, + or message 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 */ @@ -314,6 +371,17 @@ extern bool nr_segment_set_datastore(nr_segment_t* segment, */ extern bool nr_segment_set_external(nr_segment_t* segment, const nr_segment_external_t* external); + +/* + * Purpose : Mark the segment as being a message segment. + * + * Params : 1. The pointer to the segment. + * 2. The message attributes, which will be copied into the segment. + * + * Returns : true if successful, false otherwise. + */ +extern bool nr_segment_set_message(nr_segment_t* segment, + const nr_segment_message_t* message); /* * Purpose : Add a child to a segment. * diff --git a/axiom/nr_segment_external.c b/axiom/nr_segment_external.c index dbaf4c78a..738cec6f5 100644 --- a/axiom/nr_segment_external.c +++ b/axiom/nr_segment_external.c @@ -57,8 +57,8 @@ static void nr_segment_external_set_attrs( * External/{host}/all non-CAT * ExternalTransaction/{host}/{external_id}/{external_txnname} CAT * - * These metrics are dictated by the spec located here: - * https://source.datanerd.us/agents/agent-specs/blob/master/Cross-Application-Tracing-PORTED.md + * These metrics are dictated by the agent-spec in this file: + * Cross-Application-Tracing-PORTED.md */ static void nr_segment_external_create_metrics(nr_segment_t* segment, const char* uri, diff --git a/axiom/nr_segment_message.c b/axiom/nr_segment_message.c new file mode 100644 index 000000000..d07a3217e --- /dev/null +++ b/axiom/nr_segment_message.c @@ -0,0 +1,260 @@ +/* + * Copyright 2020 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "nr_axiom.h" + +#include + +#include "nr_header.h" +#include "nr_segment_message.h" +#include "nr_segment_private.h" +#include "util_strings.h" +#include "util_url.h" +#include "util_logging.h" + +/* + * Purpose : Set all the typed message attributes on the segment. + * + * Params : 1. nr_segment_t* ASSUMED TO BE NON-NULL - the segment to set the + * attributes on + * 2. nr_segment_message_params_t* ASSUMED TO BE NON-NULL - the + * parameters set the attributes to + * + * Returns: true on success. + * + * Note: This is a function private to this file and assumes the calling + * function has already checked the input parameters for NULL prior to calling + * this function. Calling function is assumed to check the following items for + * NULL: if (NULL == segment || NULL == message_params || NULL == segment->txn) + */ +static void nr_segment_message_set_attrs( + nr_segment_t* segment, + const nr_segment_message_params_t* params) { + nr_segment_message_t message_attributes = {0}; + + message_attributes.message_action = params->message_action; + + if (segment->txn->options.message_tracer_segment_parameters_enabled) { + message_attributes.destination_name = params->destination_name; + message_attributes.messaging_system = params->messaging_system; + message_attributes.server_address = params->server_address; + message_attributes.messaging_destination_routing_key + = params->messaging_destination_routing_key; + message_attributes.messaging_destination_publish_name + = params->messaging_destination_publish_name; + message_attributes.server_port = params->server_port; + } + + nr_segment_set_message(segment, &message_attributes); +} + +/* + * Purpose : Create metrics for a completed message call and set the segment + * name. + * + * Metrics created during this call + * ---------------------------------------------------------------------------------- + * MessageBroker/all Unscoped Always + * MessageBroker/{library}/all Scoped Always + * + * Metrics created based on MessageBroker/all (in nr_txn_create_rollup_metrics) + * ---------------------------------------------------------------------------------- + * MessageBroker/allWeb Unscoped Web + * MessageBroker/allOther Unscoped non-Web + * + * Segment name + * ----------------------------------------------------------------------------------- + * MessageBroker/{library}/all Always + * For non-temp: + * MessageBroker/{Library}/{DestinationType}/{Action}/Named/{DestinationName} + * For temp: + * MessageBroker/{Library}/{DestinationType}/{Action}/Temp + * + * + * These metrics are dictated by the agent-spec file here: + * APIs/messaging.md#metrics + * When the destination is temporary (such as a temporary queue, or a temporary + * topic), the destination name MUST be omitted. The metric segment 'Named' MUST + * be replaced with 'Temp'. The DestinationType segment SHOULD NOT contain + * "Temporary". Thus, "Temporary " should be removed from the destination type + * enum before metric use. Examples: MessageBroker/JMS/Queue/Produce/Temp, + * MessageBroker/JMS/Topic/Produce/Temp + * + * Further note that for pull-style messaging, the transaction segment name MUST + * be equal to the scoped metric name (e.g., + * MessageBroker/JMS/Queue/Produce/Named/SortQueue) + * + * + * Params : 1. The message segment. + * 2. Message parameters + * 3. Duration of the segment + * + * Returns : the scoped metric that was created. Caller is responsible for + * freeing this value. + */ + +static char* nr_segment_message_create_metrics( + nr_segment_t* segment, + const nr_segment_message_params_t* message_params, + nrtime_t duration) { + const char* action_string = NULL; + const char* destination_type_string = NULL; + const char* library_string = NULL; + const char* final_destination_string = NULL; + const char* destination_string = NULL; + char* rollup_metric = NULL; + char* scoped_metric = NULL; + + if (NULL == segment) { + return NULL; + } + + if (NULL == message_params) { + return NULL; + } + + /* Rollup metric. + * + * This has to be created on the transaction in order to create + * MessageBroker/allWeb and MessageBroker/allOther and to calculate + * messageDuration later on. + */ + + nrm_force_add(segment->txn->unscoped_metrics, "MessageBroker/all", duration); + + if (nr_strempty(message_params->library)) { + library_string = ""; + } else { + library_string = message_params->library; + } + rollup_metric = nr_formatf("MessageBroker/%s/all", library_string); + nrm_force_add(segment->txn->unscoped_metrics, rollup_metric, duration); + nr_free(rollup_metric); + + /* + * Note: although the concept of Temporary queues/topics is detailed in the + * spec, in practice, we are unlikely to encounter it as it is currently only + * meaningful with JMS (Java Message Service). It is added here for adherence + * with spec. + */ + + if (NR_SPANKIND_PRODUCER == message_params->message_action) { + action_string = "Produce"; + } else if (NR_SPANKIND_CONSUMER == message_params->message_action) { + action_string = "Consume"; + } else { + action_string = ""; + } + + switch (message_params->destination_type) { + case NR_MESSAGE_DESTINATION_TYPE_TEMP_QUEUE: + case NR_MESSAGE_DESTINATION_TYPE_QUEUE: + destination_type_string = "Queue"; + break; + case NR_MESSAGE_DESTINATION_TYPE_TEMP_TOPIC: + case NR_MESSAGE_DESTINATION_TYPE_TOPIC: + destination_type_string = "Topic"; + break; + case NR_MESSAGE_DESTINATION_TYPE_EXCHANGE: + destination_type_string = "Exchange"; + break; + default: + destination_type_string = ""; + break; + } + + destination_string = nr_strempty(message_params->destination_name) + ? "" + : message_params->destination_name; + /* + * messaging_destination_publish_name is only used if it exists; In all other + * cases, we use the value from destination_string. + */ + final_destination_string + = nr_strempty(message_params->messaging_destination_publish_name) + ? destination_string + : message_params->messaging_destination_publish_name; + + /* + * Create the scoped metric + * MessageBroker/{Library}/{DestinationType}/{Action}/Named/{DestinationName} + * non-temp MessageBroker/{Library}/{DestinationType}/{Action}/Temp + */ + if (NR_MESSAGE_DESTINATION_TYPE_TEMP_QUEUE == message_params->destination_type + || NR_MESSAGE_DESTINATION_TYPE_TEMP_TOPIC + == message_params->destination_type) { + scoped_metric = nr_formatf("MessageBroker/%s/%s/%s/Temp", library_string, + destination_type_string, action_string); + } else { + scoped_metric = nr_formatf("MessageBroker/%s/%s/%s/Named/%s", + library_string, destination_type_string, + action_string, final_destination_string); + } + + nr_segment_add_metric(segment, scoped_metric, true); + + /* + * The scoped metric will be used as the segment name. + */ + return scoped_metric; +} + +bool nr_segment_message_end(nr_segment_t** segment_ptr, + const nr_segment_message_params_t* message_params) { + bool rv = false; + nr_segment_t* segment; + nrtime_t duration = 0; + char* scoped_metric = NULL; + nr_segment_t* child = NULL; + + if (NULL == segment_ptr) { + return false; + } + + segment = *segment_ptr; + + if (NULL == segment || NULL == message_params || NULL == segment->txn) { + return false; + } + + /* + * We don't want message segments to have any children, as + * this would scramble the exclusive time calculation. + * Additionally, because it makes http calls under the hood, + * we don't want additional external calls created for this same txn. + * Therefore, we delete all children of the message segment. + * By destroying the tree we are able to destroy all descendants vs just + * destroying the child which then reparents all it's children to the segment. + */ + if (segment) { + for (size_t i = 0; i < nr_segment_children_size(&segment->children); i++) { + child = nr_segment_children_get(&segment->children, i); + nr_segment_destroy_tree(child); + } + nr_segment_children_deinit(&segment->children); + } + + nr_segment_message_set_attrs(segment, message_params); + + /* + * We set the end time here because we need the duration, (nr_segment_end will + * not overwrite this value if it's already set). + */ + if (!segment->stop_time) { + segment->stop_time + = nr_time_duration(nr_txn_start_time(segment->txn), nr_get_time()); + } + duration = nr_time_duration(segment->start_time, segment->stop_time); + + scoped_metric + = nr_segment_message_create_metrics(segment, message_params, duration); + nr_segment_set_name(segment, scoped_metric); + + rv = nr_segment_end(&segment); + + nr_free(scoped_metric); + + return rv; +} diff --git a/axiom/nr_segment_message.h b/axiom/nr_segment_message.h new file mode 100644 index 000000000..6917f78de --- /dev/null +++ b/axiom/nr_segment_message.h @@ -0,0 +1,68 @@ +/* + * Copyright 2020 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef NR_SEGMENT_MESSAGE_HDR +#define NR_SEGMENT_MESSAGE_HDR + +#include "nr_segment.h" +#include "nr_segment_traces.h" + +/* + * Note: + * CAT is EOLed and this feature is not compatible with CAT. + */ + +typedef enum _nr_segment_message_destination_type_t { + NR_MESSAGE_DESTINATION_TYPE_QUEUE, + NR_MESSAGE_DESTINATION_TYPE_TOPIC, + NR_MESSAGE_DESTINATION_TYPE_TEMP_QUEUE, + NR_MESSAGE_DESTINATION_TYPE_TEMP_TOPIC, + NR_MESSAGE_DESTINATION_TYPE_EXCHANGE +} nr_segment_message_destination_type_t; + +typedef struct { + /* All strings are null-terminated. When NULL/empty the values are ingored. */ + + /* Only used for creating metrics. */ + char* library; /* Library; Possible values are SQS, SNS, RabbitMQ, JMS */ + nr_segment_message_destination_type_t + destination_type; /* Named/temp queue/topic/exchange */ + + /* Used for creating message attributes. */ + nr_span_spankind_t + message_action; /*The action of the message, e.g.,Produce/Consume.*/ + char* destination_name; /*The name of the Queue, Topic, or Exchange; + otherwise, Temp. Needed for SQS relationship.*/ + char* messaging_system; /* for ex: aws_sqs. Needed for SQS relationship.*/ + char* server_address; /*The server domain name or IP address. Needed for + MQBROKER relationship.*/ + char* + messaging_destination_publish_name; /* Otel attribute for message + consumers. (In the agent, this + means Action is Consume in the span + name). This attribute is equal to + the corresponding attribute + messaging.destination.name from the + producer. This attribute is needed + for apps using RabbitMQ and it + represents the exchange name.*/ + char* messaging_destination_routing_key; /* The routing key for a RabbitMQ + operation.*/ + uint64_t server_port; /*The server port.*/ + +} nr_segment_message_params_t; + +/* + * Purpose : End a message segment and record metrics. + * + * Params : 1. nr_segment_t** segment: Segment to apply message params to and end + * 2. const nr_segment_message_params_t* params: params to apply to segment + * + * Returns: true on success. + */ +extern bool nr_segment_message_end(nr_segment_t** segment, + const nr_segment_message_params_t* params); + +#endif diff --git a/axiom/nr_segment_private.c b/axiom/nr_segment_private.c index a60865afd..ea5bbf8cc 100644 --- a/axiom/nr_segment_private.c +++ b/axiom/nr_segment_private.c @@ -39,6 +39,18 @@ void nr_segment_external_destroy_fields(nr_segment_external_t* external) { nr_free(external->procedure); } +void nr_segment_message_destroy_fields(nr_segment_message_t* message) { + if (nrunlikely(NULL == message)) { + return; + } + + nr_free(message->destination_name); + nr_free(message->messaging_system); + nr_free(message->server_address); + nr_free(message->messaging_destination_publish_name); + nr_free(message->messaging_destination_routing_key); +} + void nr_segment_destroy_typed_attributes( nr_segment_type_t type, nr_segment_typed_attributes_t** attributes) { @@ -54,6 +66,8 @@ void nr_segment_destroy_typed_attributes( nr_segment_datastore_destroy_fields(&attrs->datastore); } else if (NR_SEGMENT_EXTERNAL == type) { nr_segment_external_destroy_fields(&attrs->external); + } else if (NR_SEGMENT_MESSAGE == type) { + nr_segment_message_destroy_fields(&attrs->message); } nr_free(attrs); diff --git a/axiom/nr_segment_private.h b/axiom/nr_segment_private.h index 70b499795..b2d6fe2c4 100644 --- a/axiom/nr_segment_private.h +++ b/axiom/nr_segment_private.h @@ -33,6 +33,13 @@ void nr_segment_datastore_destroy_fields(nr_segment_datastore_t* datastore); */ void nr_segment_external_destroy_fields(nr_segment_external_t* external); +/* + * Purpose : Free all data related to a segment's message metadata. + * + * Params : 1. A pointer to a segment's nr_segment_message_t structure. + */ +void nr_segment_message_destroy_fields(nr_segment_message_t* message); + /* * Purpose : Free all data related to a segment metric. * diff --git a/axiom/nr_segment_traces.c b/axiom/nr_segment_traces.c index 846d89a55..d2f1fddac 100644 --- a/axiom/nr_segment_traces.c +++ b/axiom/nr_segment_traces.c @@ -162,6 +162,25 @@ static void add_typed_attributes_to_buffer(nrbuf_t* buf, ext->transaction_guid, false); add_hash_key_value_to_buffer_int(buf, "status", &ext->status); } break; + case NR_SEGMENT_MESSAGE: { + const nr_segment_message_t* message = &segment->typed_attributes->message; + add_hash_key_value_to_buffer(buf, "destination_name", + message->destination_name, false); + add_hash_key_value_to_buffer(buf, "messaging_system", + message->messaging_system, false); + add_hash_key_value_to_buffer(buf, "server_address", + message->server_address, false); + add_hash_key_value_to_buffer(buf, "messaging_destination_publish_name", + message->messaging_destination_publish_name, + false); + add_hash_key_value_to_buffer(buf, "messaging_destination_routing_key", + message->messaging_destination_routing_key, + false); + if (0 != message->server_port) { + add_hash_key_value_to_buffer_int(buf, "server_port", + &message->server_port); + } + } break; case NR_SEGMENT_CUSTOM: default: break; @@ -578,3 +597,65 @@ void nr_segment_traces_create_data( return; } + +/* + * Purpose : If available, add cloud attributes to segment. + * + * Params : 1. segment to create and add agent attributes to + * 2. nr_segment_cloud_attrs_t* that contains the attributes + * + * Returns : void + * + */ +extern void nr_segment_traces_add_cloud_attributes( + nr_segment_t* segment, + const nr_segment_cloud_attrs_t* cloud_attrs) { + if (NULL == cloud_attrs) { + return; + } + + if (NULL == segment) { + return; + } + + /* + * Ensure a spot for the attributes. + */ + + if (NULL == segment->attributes) { + segment->attributes = nr_attributes_create(segment->txn->attribute_config); + } + + if (nrunlikely(NULL == segment->attributes)) { + return; + } + +#define NR_CLOUD_AGENT_ATTRIBUTE_DESTINATION \ + (NR_ATTRIBUTE_DESTINATION_TXN_TRACE | NR_ATTRIBUTE_DESTINATION_ERROR \ + | NR_ATTRIBUTE_DESTINATION_TXN_EVENT | NR_ATTRIBUTE_DESTINATION_SPAN) + + /* + * If the value is empty or null, ignore it. + */ + + if (!nr_strempty(cloud_attrs->cloud_region)) { + nr_attributes_agent_add_string( + segment->attributes, NR_CLOUD_AGENT_ATTRIBUTE_DESTINATION, + NR_ATTR_CLOUD_REGION, cloud_attrs->cloud_region); + } + if (!nr_strempty(cloud_attrs->cloud_account_id)) { + nr_attributes_agent_add_string( + segment->attributes, NR_CLOUD_AGENT_ATTRIBUTE_DESTINATION, + NR_ATTR_CLOUD_ACCOUNT_ID, cloud_attrs->cloud_account_id); + } + if (!nr_strempty(cloud_attrs->cloud_resource_id)) { + nr_attributes_agent_add_string( + segment->attributes, NR_CLOUD_AGENT_ATTRIBUTE_DESTINATION, + NR_ATTR_CLOUD_RESOURCE_ID, cloud_attrs->cloud_resource_id); + } + if (!nr_strempty(cloud_attrs->aws_operation)) { + nr_attributes_agent_add_string( + segment->attributes, NR_CLOUD_AGENT_ATTRIBUTE_DESTINATION, + NR_ATTR_AWS_OPERATION, cloud_attrs->aws_operation); + } +} diff --git a/axiom/nr_segment_traces.h b/axiom/nr_segment_traces.h index a8ccbb10f..aa9ad4610 100644 --- a/axiom/nr_segment_traces.h +++ b/axiom/nr_segment_traces.h @@ -124,4 +124,8 @@ bool nr_segment_traces_json_print_segments(nrbuf_t* buf, extern nr_segment_iter_return_t nr_segment_traces_stot_iterator_callback( nr_segment_t* segment, void* userdata); + +extern void nr_segment_traces_add_cloud_attributes( + nr_segment_t* segment, + const nr_segment_cloud_attrs_t* cloud_attrs); #endif diff --git a/axiom/nr_span_event.c b/axiom/nr_span_event.c index 965b297d9..7c8c2dbe0 100644 --- a/axiom/nr_span_event.c +++ b/axiom/nr_span_event.c @@ -141,20 +141,49 @@ void nr_span_event_set_category(nr_span_event_t* event, switch (category) { case NR_SPAN_DATASTORE: nro_set_hash_string(event->intrinsics, "category", "datastore"); - nro_set_hash_string(event->intrinsics, "span.kind", "client"); + nr_span_event_set_spankind(event, NR_SPANKIND_CLIENT); break; case NR_SPAN_GENERIC: nro_set_hash_string(event->intrinsics, "category", "generic"); - if (nro_get_hash_value(event->intrinsics, "span.kind", NULL)) { - nro_set_hash_none(event->intrinsics, "span.kind"); - } + nr_span_event_set_spankind(event, NR_SPANKIND_NO_SPANKIND); break; case NR_SPAN_HTTP: nro_set_hash_string(event->intrinsics, "category", "http"); + nr_span_event_set_spankind(event, NR_SPANKIND_CLIENT); + break; + + case NR_SPAN_MESSAGE: + nro_set_hash_string(event->intrinsics, "category", "message"); + /* give it a default value in case we exit before spankind is set*/ + nr_span_event_set_spankind(event, NR_SPANKIND_NO_SPANKIND); + break; + } +} + +void nr_span_event_set_spankind(nr_span_event_t* event, + nr_span_spankind_t spankind) { + if (NULL == event) { + return; + } + + switch (spankind) { + case NR_SPANKIND_PRODUCER: + nro_set_hash_string(event->intrinsics, "span.kind", "producer"); + break; + case NR_SPANKIND_CLIENT: nro_set_hash_string(event->intrinsics, "span.kind", "client"); break; + case NR_SPANKIND_CONSUMER: + nro_set_hash_string(event->intrinsics, "span.kind", "consumer"); + break; + case NR_SPANKIND_NO_SPANKIND: + default: + if (nro_get_hash_value(event->intrinsics, "span.kind", NULL)) { + nro_set_hash_none(event->intrinsics, "span.kind"); + } + break; } } @@ -328,6 +357,65 @@ void nr_span_event_set_external_status(nr_span_event_t* event, nro_set_hash_ulong(event->agent_attributes, "http.statusCode", status); } +void nr_span_event_set_message(nr_span_event_t* event, + nr_span_event_message_member_t member, + const char* new_value) { + if (NULL == event || NULL == new_value) { + return; + } + + switch (member) { + case NR_SPAN_MESSAGE_DESTINATION_NAME: + nro_set_hash_string(event->agent_attributes, + NR_ATTR_MESSAGING_DESTINATION_NAME, new_value); + break; + case NR_SPAN_MESSAGE_MESSAGING_SYSTEM: + nro_set_hash_string(event->agent_attributes, NR_ATTR_MESSAGING_SYSTEM, + new_value); + break; + case NR_SPAN_MESSAGE_SERVER_ADDRESS: + nro_set_hash_string(event->agent_attributes, NR_ATTR_SERVER_ADDRESS, + new_value); + break; + case NR_SPAN_MESSAGE_MESSAGING_DESTINATION_ROUTING_KEY: + nro_set_hash_string(event->agent_attributes, + NR_ATTR_MESSAGING_DESTINATION_ROUTING_KEY, new_value); + break; + case NR_SPAN_MESSAGE_MESSAGING_DESTINATION_PUBLISH_NAME: + nro_set_hash_string(event->agent_attributes, + NR_ATTR_MESSAGING_DESTINATION_PUBLISH_NAME, + new_value); + break; + case NR_SPAN_MESSAGE_SERVER_PORT: + break; + } +} + +void nr_span_event_set_message_ulong(nr_span_event_t* event, + nr_span_event_message_member_t member, + const uint64_t new_value) { + if (NULL == event || 0 == new_value) { + return; + } + + switch (member) { + case NR_SPAN_MESSAGE_SERVER_PORT: + nro_set_hash_ulong(event->agent_attributes, NR_ATTR_SERVER_PORT, + new_value); + break; + case NR_SPAN_MESSAGE_DESTINATION_NAME: + break; + case NR_SPAN_MESSAGE_MESSAGING_SYSTEM: + break; + case NR_SPAN_MESSAGE_SERVER_ADDRESS: + break; + case NR_SPAN_MESSAGE_MESSAGING_DESTINATION_ROUTING_KEY: + break; + case NR_SPAN_MESSAGE_MESSAGING_DESTINATION_PUBLISH_NAME: + break; + } +} + /* * Getters. * @@ -378,6 +466,7 @@ SPAN_EVENT_GETTER_STRING(nr_span_event_get_transaction_name, intrinsics, "transaction.name") SPAN_EVENT_GETTER_STRING(nr_span_event_get_category, intrinsics, "category") +SPAN_EVENT_GETTER_STRING(nr_span_event_get_spankind, intrinsics, "span.kind") SPAN_EVENT_GETTER_TIME(nr_span_event_get_timestamp, intrinsics, "timestamp") SPAN_EVENT_GETTER_DOUBLE(nr_span_event_get_duration, intrinsics, "duration") SPAN_EVENT_GETTER_DOUBLE(nr_span_event_get_priority, intrinsics, "priority") @@ -466,6 +555,61 @@ const char* nr_span_event_get_external(const nr_span_event_t* event, return NULL; } +const char* nr_span_event_get_message(const nr_span_event_t* event, + nr_span_event_message_member_t member) { + if (NULL == event) { + return NULL; + } + + switch (member) { + case NR_SPAN_MESSAGE_DESTINATION_NAME: + return nro_get_hash_string(event->agent_attributes, + NR_ATTR_MESSAGING_DESTINATION_NAME, NULL); + case NR_SPAN_MESSAGE_MESSAGING_SYSTEM: + return nro_get_hash_string(event->agent_attributes, + NR_ATTR_MESSAGING_SYSTEM, NULL); + case NR_SPAN_MESSAGE_SERVER_ADDRESS: + return nro_get_hash_string(event->agent_attributes, + NR_ATTR_SERVER_ADDRESS, NULL); + case NR_SPAN_MESSAGE_MESSAGING_DESTINATION_ROUTING_KEY: + return nro_get_hash_string(event->agent_attributes, + NR_ATTR_MESSAGING_DESTINATION_ROUTING_KEY, + NULL); + case NR_SPAN_MESSAGE_MESSAGING_DESTINATION_PUBLISH_NAME: + return nro_get_hash_string(event->agent_attributes, + NR_ATTR_MESSAGING_DESTINATION_PUBLISH_NAME, + NULL); + case NR_SPAN_MESSAGE_SERVER_PORT: + break; + } + return NULL; +} + +uint64_t nr_span_event_get_message_ulong( + const nr_span_event_t* event, + nr_span_event_message_member_t member) { + if (NULL == event) { + return 0; + } + + switch (member) { + case NR_SPAN_MESSAGE_SERVER_PORT: + return nro_get_hash_ulong(event->agent_attributes, NR_ATTR_SERVER_PORT, + NULL); + case NR_SPAN_MESSAGE_DESTINATION_NAME: + break; + case NR_SPAN_MESSAGE_MESSAGING_SYSTEM: + break; + case NR_SPAN_MESSAGE_SERVER_ADDRESS: + break; + case NR_SPAN_MESSAGE_MESSAGING_DESTINATION_ROUTING_KEY: + break; + case NR_SPAN_MESSAGE_MESSAGING_DESTINATION_PUBLISH_NAME: + break; + } + return 0; +} + void nr_span_event_set_attribute_user(nr_span_event_t* event, const char* name, const nrobj_t* value) { diff --git a/axiom/nr_span_event.h b/axiom/nr_span_event.h index 74ee2119c..33dced5c2 100644 --- a/axiom/nr_span_event.h +++ b/axiom/nr_span_event.h @@ -13,6 +13,19 @@ #include #include +#define NR_ATTR_MESSAGING_DESTINATION_NAME "messaging.destination.name" +#define NR_ATTR_MESSAGING_SYSTEM "messaging.system" +#define NR_ATTR_MESSAGING_DESTINATION_ROUTING_KEY \ + "messaging.rabbitmq.destination.routing_key" +#define NR_ATTR_MESSAGING_DESTINATION_PUBLISH_NAME \ + "messaging.destination_publish.name" +#define NR_ATTR_SERVER_ADDRESS "server.address" +#define NR_ATTR_SERVER_PORT "server.port" +#define NR_ATTR_CLOUD_REGION "cloud.region" +#define NR_ATTR_CLOUD_ACCOUNT_ID "cloud.account.id" +#define NR_ATTR_CLOUD_RESOURCE_ID "cloud.resource_id" +#define NR_ATTR_AWS_OPERATION "aws.operation" + typedef struct _nr_span_event_t nr_span_event_t; /* @@ -21,9 +34,30 @@ typedef struct _nr_span_event_t nr_span_event_t; typedef enum { NR_SPAN_GENERIC, NR_SPAN_HTTP, - NR_SPAN_DATASTORE + NR_SPAN_DATASTORE, + NR_SPAN_MESSAGE } nr_span_category_t; +/* + * The spankinds a span may fall into. + * This is set according to: + * 1) guidelines in agent-specs which state datastore and http spans set + * span.kind to client and further states that generic span.kind is unset + * + * 2) for message spans follow guidance here: + * https://opentelemetry.io/docs/specs/semconv/messaging/messaging-spans/ + * which states that span.kind is + * a) producer when the operation type is create or send(if the context is + * create) b) client when the operation type is create or send(if the context is + * NOT create) c) consumer when the operation type is process + */ +typedef enum { + NR_SPANKIND_PRODUCER, + NR_SPANKIND_CLIENT, + NR_SPANKIND_CONSUMER, + NR_SPANKIND_NO_SPANKIND +} nr_span_spankind_t; + /* * Fields that can be set on datastore spans. */ @@ -44,6 +78,18 @@ typedef enum { NR_SPAN_EXTERNAL_METHOD } nr_span_event_external_member_t; +/* + * Fields that can be set on message spans. + */ +typedef enum { + NR_SPAN_MESSAGE_DESTINATION_NAME, + NR_SPAN_MESSAGE_MESSAGING_SYSTEM, + NR_SPAN_MESSAGE_SERVER_ADDRESS, + NR_SPAN_MESSAGE_SERVER_PORT, + NR_SPAN_MESSAGE_MESSAGING_DESTINATION_ROUTING_KEY, + NR_SPAN_MESSAGE_MESSAGING_DESTINATION_PUBLISH_NAME +} nr_span_event_message_member_t; + /* * The parent attributes that can be set on service entry spans. * parent.transportDuration is set in @@ -115,6 +161,8 @@ extern void nr_span_event_set_transaction_name(nr_span_event_t* event, const char* transaction_name); extern void nr_span_event_set_category(nr_span_event_t* event, nr_span_category_t category); +extern void nr_span_event_set_spankind(nr_span_event_t* event, + nr_span_spankind_t spankind); extern void nr_span_event_set_timestamp(nr_span_event_t* event, nrtime_t time); extern void nr_span_event_set_duration(nr_span_event_t* event, nrtime_t duration); @@ -170,6 +218,31 @@ extern void nr_span_event_set_external(nr_span_event_t* event, extern void nr_span_event_set_external_status(nr_span_event_t* event, const uint64_t status); +/* + * Purpose : Set a message attribute with a given string new_value. + * + * Params : 1. The target Span Event that should be changed. + * 2. The message attribute to be set. + * 3. The string value that the field will be after the function has + * executed. + */ +extern void nr_span_event_set_message(nr_span_event_t* event, + nr_span_event_message_member_t member, + const char* new_value); + +/* + * Purpose : Set a message attribute with a given ulong new_value. + * + * Params : 1. The target Span Event that should be changed. + * 2. The message attribute to be set. + * 3. The ulong value that the field will be after the function has + * executed. + */ +extern void nr_span_event_set_message_ulong( + nr_span_event_t* event, + nr_span_event_message_member_t member, + const uint64_t new_value); + /* * Purpose : Set a user attribute. * diff --git a/axiom/nr_span_event_private.h b/axiom/nr_span_event_private.h index 349c50538..01d544fc2 100644 --- a/axiom/nr_span_event_private.h +++ b/axiom/nr_span_event_private.h @@ -28,6 +28,7 @@ extern const char* nr_span_event_get_name(const nr_span_event_t* event); extern const char* nr_span_event_get_transaction_name( const nr_span_event_t* event); extern const char* nr_span_event_get_category(const nr_span_event_t* event); +extern const char* nr_span_event_get_spankind(const nr_span_event_t* event); extern nrtime_t nr_span_event_get_timestamp(const nr_span_event_t* event); extern double nr_span_event_get_duration(const nr_span_event_t* event); extern double nr_span_event_get_priority(const nr_span_event_t* event); @@ -44,6 +45,12 @@ extern const char* nr_span_event_get_external( const nr_span_event_t* event, nr_span_event_external_member_t member); extern uint64_t nr_span_event_get_external_status(const nr_span_event_t* event); +extern const char* nr_span_event_get_message( + const nr_span_event_t* event, + nr_span_event_message_member_t member); +extern uint64_t nr_span_event_get_message_ulong( + const nr_span_event_t* event, + nr_span_event_message_member_t member); extern const char* nr_span_event_get_error_message( const nr_span_event_t* event); extern const char* nr_span_event_get_error_class(const nr_span_event_t* event); diff --git a/axiom/nr_txn.c b/axiom/nr_txn.c index 2ea39bb26..06c0214ef 100644 --- a/axiom/nr_txn.c +++ b/axiom/nr_txn.c @@ -542,6 +542,7 @@ nrtxn_t* nr_txn_begin(nrapp_t* app, nt->custom_events = nr_analytics_events_create(app->limits.custom_events); nt->log_events = nr_log_events_create(app->limits.log_events); nt->php_packages = nr_php_packages_create(); + nt->php_package_major_version_metrics_suggestions = nr_php_packages_create(); /* * reset flag for creation of one-time logging metrics @@ -633,7 +634,8 @@ nrtxn_t* nr_txn_begin(nrapp_t* app, */ guid = nr_guid_create(app->rnd); nr_distributed_trace_set_txn_id(nt->distributed_trace, guid); - nr_distributed_trace_set_trace_id(nt->distributed_trace, guid); + nr_distributed_trace_set_trace_id(nt->distributed_trace, guid, + opts->distributed_tracing_pad_trace_id); nr_distributed_trace_set_trusted_key( nt->distributed_trace, @@ -1219,11 +1221,15 @@ void nr_txn_create_rollup_metrics(nrtxn_t* txn) { "Datastore/allOther"); nrm_duplicate_metric(txn->unscoped_metrics, "External/all", "External/allOther"); + nrm_duplicate_metric(txn->unscoped_metrics, "MessageBroker/all", + "MessageBroker/allOther"); } else { nrm_duplicate_metric(txn->unscoped_metrics, "Datastore/all", "Datastore/allWeb"); nrm_duplicate_metric(txn->unscoped_metrics, "External/all", "External/allWeb"); + nrm_duplicate_metric(txn->unscoped_metrics, "MessageBroker/all", + "MessageBroker/allWeb"); } nr_string_pool_apply( @@ -1245,6 +1251,7 @@ void nr_txn_destroy_fields(nrtxn_t* txn) { nr_segment_destroy_tree(txn->segment_root); nr_hashmap_destroy(&txn->parent_stacks); nr_php_packages_destroy(&txn->php_packages); + nr_php_packages_destroy(&txn->php_package_major_version_metrics_suggestions); nr_stack_destroy_fields(&txn->default_parent_stack); nr_slab_destroy(&txn->segment_slab); nr_minmax_heap_set_destructor(txn->segment_heap, NULL, NULL); @@ -2489,11 +2496,15 @@ nr_analytics_event_t* nr_error_to_event(const nrtxn_t* txn) { "External/all", "externalDuration"); nr_txn_add_metric_total_as_attribute(params, txn->unscoped_metrics, "Datastore/all", "databaseDuration"); + nr_txn_add_metric_total_as_attribute(params, txn->unscoped_metrics, + "MessageBroker/all", "messageDuration"); nr_txn_add_metric_count_as_attribute(params, txn->unscoped_metrics, "Datastore/all", "databaseCallCount"); nr_txn_add_metric_count_as_attribute(params, txn->unscoped_metrics, "External/all", "externalCallCount"); + nr_txn_add_metric_count_as_attribute(params, txn->unscoped_metrics, + "MessageBroker/all", "messageCallCount"); nro_set_hash_string(params, "nr.transactionGuid", nr_txn_get_guid(txn)); @@ -2579,10 +2590,16 @@ nrobj_t* nr_txn_event_intrinsics(const nrtxn_t* txn) { params, txn->unscoped_metrics, "WebFrontend/QueueTime", "queueDuration"); nr_txn_add_metric_total_as_attribute(params, txn->unscoped_metrics, "External/all", "externalDuration"); + nr_txn_add_metric_count_as_attribute(params, txn->unscoped_metrics, + "External/all", "externalCallCount"); nr_txn_add_metric_total_as_attribute(params, txn->unscoped_metrics, "Datastore/all", "databaseDuration"); nr_txn_add_metric_count_as_attribute(params, txn->unscoped_metrics, "Datastore/all", "databaseCallCount"); + nr_txn_add_metric_total_as_attribute(params, txn->unscoped_metrics, + "MessageBroker/all", "messageDuration"); + nr_txn_add_metric_count_as_attribute(params, txn->unscoped_metrics, + "MessageBroker/all", "messageCallCount"); if (txn->options.distributed_tracing_enabled) { nr_txn_add_distributed_tracing_intrinsics(txn, params); @@ -3493,9 +3510,35 @@ void nr_txn_record_log_event(nrtxn_t* txn, nr_txn_add_logging_metrics(txn, log_level_name); } -void nr_txn_add_php_package(nrtxn_t* txn, - char* package_name, - char* package_version) { +nr_php_package_t* nr_txn_add_php_package_from_source( + nrtxn_t* txn, + char* package_name, + char* package_version, + const nr_php_package_source_priority_t source) { + nr_php_package_t* p = NULL; + + if (nrunlikely(NULL == txn)) { + return NULL; + } + + if (nr_strempty(package_name)) { + return NULL; + } + + p = nr_php_package_create_with_source(package_name, package_version, source); + return nr_php_packages_add_package(txn->php_packages, p); +} + +nr_php_package_t* nr_txn_add_php_package(nrtxn_t* txn, + char* package_name, + char* package_version) { + return nr_txn_add_php_package_from_source(txn, package_name, package_version, + NR_PHP_PACKAGE_SOURCE_LEGACY); +} + +void nr_txn_suggest_package_supportability_metric(nrtxn_t* txn, + const char* package_name, + const char* package_version) { nr_php_package_t* p = NULL; if (nrunlikely(NULL == txn)) { @@ -3506,6 +3549,11 @@ void nr_txn_add_php_package(nrtxn_t* txn, return; } - p = nr_php_package_create(package_name, package_version); - nr_php_packages_add_package(txn->php_packages, p); + nrl_verbosedebug(NRL_TXN, "Suggesting package %s %s", NRSAFESTR(package_name), + NRSAFESTR(package_version)); + + p = nr_php_package_create_with_source(package_name, package_version, + NR_PHP_PACKAGE_SOURCE_SUGGESTION); + nr_php_packages_add_package( + txn->php_package_major_version_metrics_suggestions, p); } diff --git a/axiom/nr_txn.h b/axiom/nr_txn.h index dc2cf014b..55871f1b1 100644 --- a/axiom/nr_txn.h +++ b/axiom/nr_txn.h @@ -91,6 +91,9 @@ typedef struct _nrtxnopt_t { int distributed_tracing_enabled; /* Whether distributed tracing functionality is enabled */ + bool distributed_tracing_pad_trace_id; /* whether to pad internally generated + trace_id to NR_TRACE_ID_MAX_SIZE + characters */ bool distributed_tracing_exclude_newrelic_header; /* Whether distributed tracing outbound headers should omit newrelic @@ -129,6 +132,8 @@ typedef struct _nrtxnopt_t { size_t log_events_max_samples_stored; /* The maximum number of log events per transaction */ bool log_metrics_enabled; /* Whether log metrics are enabled */ + bool message_tracer_segment_parameters_enabled; /* Determines whether to add + message attr */ } nrtxnopt_t; typedef enum _nrtxnstatus_cross_process_t { @@ -200,6 +205,11 @@ typedef enum _nr_cpu_usage_t { NR_CPU_USAGE_COUNT = 2 } nr_cpu_usage_t; +typedef struct _nr_composer_info_t { + bool autoload_detected; + bool composer_detected; +} nr_composer_info_t; + /* * Possible transaction types, which go into the type bitfield in the nrtxn_t * struct. @@ -278,6 +288,9 @@ typedef struct _nrtxn_t { custom_events; /* Custom events created through the API. */ nr_log_events_t* log_events; /* Log events pool */ nr_php_packages_t* php_packages; /* Detected php packages */ + nr_php_packages_t* + php_package_major_version_metrics_suggestions; /* Suggested packages for + major metric creation */ nrtime_t user_cpu[NR_CPU_USAGE_COUNT]; /* User CPU usage */ nrtime_t sys_cpu[NR_CPU_USAGE_COUNT]; /* System CPU usage */ @@ -299,6 +312,7 @@ typedef struct _nrtxn_t { nr_distributed_trace_t* distributed_trace; /* distributed tracing metadata for the transaction */ nr_span_queue_t* span_queue; /* span queue when 8T is enabled */ + nr_composer_info_t composer_info; /* * flag to indicate if one time (per transaction) logging metrics @@ -1161,16 +1175,52 @@ static inline nr_segment_t* nr_txn_allocate_segment(nrtxn_t* txn) { } /* - * Purpose : Add php packages to transaction. This function should only be - * called when Vulnerability Management is enabled. + * Purpose : Add php package to transaction from desired source. This function + * should only be called when Vulnerability Management is enabled. + * + * Params : 1. The transaction + * 2. Package name + * 3. Package version + * 4. Source priority + * + * Returns : pointer to added package on success or NULL otherwise. + */ +nr_php_package_t* nr_txn_add_php_package_from_source( + nrtxn_t* txn, + char* package_name, + char* package_version, + const nr_php_package_source_priority_t source); + +/* + * Purpose : Add php package to transaction from legacy source. This function + * should only be called when Vulnerability Management is enabled. * * Params : 1. The transaction * 2. Package name * 3. Package version * + * Returns : pointer to added package on success or NULL otherwise. */ -void nr_txn_add_php_package(nrtxn_t* txn, - char* package_name, - char* package_version); +extern nr_php_package_t* nr_txn_add_php_package(nrtxn_t* txn, + char* package_name, + char* package_version); +/* + * Purpose : Add php package suggestion to transaction. This function + * can be used when Vulnerability Management is not enabled. It will + * add the package to the transaction's + * php_package_major_version_metrics_suggestions list. At the end of the + * transaction this list is traversed and any suggestions with a known version + * will have a package major version metric created. + * + * Params : 1. The transaction + * 2. Package name + * 3. Package version (can be NULL or PHP_PACKAGE_VERSION_UNKNOWN) + * + * Returns : Nothing. + */ +extern void nr_txn_suggest_package_supportability_metric( + nrtxn_t* txn, + const char* package_name, + const char* package_version); #endif /* NR_TXN_HDR */ diff --git a/axiom/nr_version.c b/axiom/nr_version.c index 18d948754..e6e54a250 100644 --- a/axiom/nr_version.c +++ b/axiom/nr_version.c @@ -21,13 +21,8 @@ #endif /* - * Current version naming scheme is flowers + * Current version naming scheme is gemstones * - * buttercup 26Apr2022 (9.21) - * cosmos 29Jun2022 (10.0) - * dahlia 19Sep2022 (10.1) - * echinacea 03Oct2022 (10.2) - * freesia 03Nov2022 (10.3) * goldenrod 12Dec2022 (10.4) * hydrangea 18Jan2023 (10.5) * impatiens 13Feb2023 (10.6) @@ -45,8 +40,16 @@ * ulmus 04Mar2024 (10.18) * viburnum 18Mar2024 (10.19) * wallflower 06May2024 (10.20) + * xerophyllum 20May2024 (10.21) + * yarrow 26Jun2024 (10.22) + * zinnia 30Jul2024 (11.0) + * amethyst 26Aug2024 (11.1) + * bowenite 30Sep2024 (11.2) + * corundum 21Oct2024 (11.3) + * diamond 09Dec2024 (11.4) + * emerald 13Jan2025 (11.5) */ -#define NR_CODENAME "xerophyllum" +#define NR_CODENAME "fluorite" const char* nr_version(void) { return NR_STR2(NR_VERSION); diff --git a/axiom/tests/.gitignore b/axiom/tests/.gitignore index e2e2db564..22ee3b042 100644 --- a/axiom/tests/.gitignore +++ b/axiom/tests/.gitignore @@ -82,6 +82,7 @@ test_number_converter test_obfuscate test_object test_offsets +test_php_packages test_postgres test_quarantine test_random diff --git a/axiom/tests/Makefile b/axiom/tests/Makefile index 0a37821bb..3d67984ca 100644 --- a/axiom/tests/Makefile +++ b/axiom/tests/Makefile @@ -173,6 +173,7 @@ TESTS := \ test_segment_children \ test_segment_datastore \ test_segment_external \ + test_segment_message \ test_segment_private \ test_segment_terms \ test_segment_traces \ diff --git a/axiom/tests/test_cmd_txndata.c b/axiom/tests/test_cmd_txndata.c index fc9272e1c..ffb53a6c9 100644 --- a/axiom/tests/test_cmd_txndata.c +++ b/axiom/tests/test_cmd_txndata.c @@ -134,7 +134,8 @@ static void test_encode_errors(void) { "[887788,\"txnname\",\"msg\",\"cls\",{\"stack_trace\":[" "\"stacktrace " "json\"],\"agentAttributes\":{\"agent_long\":2},\"userAttributes\":{" - "\"user_long\":1},\"intrinsics\":{\"a\":\"b\",\"guid\":\"abcdef\"}},\"abcdef\"]"), + "\"user_long\":1},\"intrinsics\":{\"a\":\"b\",\"guid\":\"abcdef\"}}," + "\"abcdef\"]"), nr_flatbuffers_table_read_bytes(&tbl, ERROR_FIELD_DATA), nr_flatbuffers_table_read_vector_len(&tbl, ERROR_FIELD_DATA), __FILE__, __LINE__); @@ -1042,6 +1043,7 @@ static void test_encode_txn_event(void) { nrm_add(txn.unscoped_metrics, "Datastore/all", 1 * NR_TIME_DIVISOR); nrm_add(txn.unscoped_metrics, "Datastore/all", 1 * NR_TIME_DIVISOR); nrm_add(txn.unscoped_metrics, "External/all", 2 * NR_TIME_DIVISOR); + nrm_add(txn.unscoped_metrics, "MessageBroker/all", 2 * NR_TIME_DIVISOR); nrm_add(txn.unscoped_metrics, "WebFrontend/QueueTime", 3 * NR_TIME_DIVISOR); txn.attributes = nr_attributes_create(0); @@ -1093,9 +1095,13 @@ static void test_encode_txn_event(void) { "\"timestamp\":123.00000," "\"duration\":0.98700,\"totalTime\":0.98700,\"nr.apdexPerfZone\":" "\"F\"," - "\"queueDuration\":3.00000,\"externalDuration\":2.00000," + "\"queueDuration\":3.00000," + "\"externalDuration\":2.00000," + "\"externalCallCount\":1," "\"databaseDuration\":2.00000," "\"databaseCallCount\":2," + "\"messageDuration\":2.00000," + "\"messageCallCount\":1," "\"error\":false}," "{\"user_long\":1},{\"agent_long\":2}]"), nr_flatbuffers_table_read_bytes(&tbl, EVENT_FIELD_DATA), diff --git a/axiom/tests/test_distributed_trace.c b/axiom/tests/test_distributed_trace.c index c74d549f5..008aea768 100644 --- a/axiom/tests/test_distributed_trace.c +++ b/axiom/tests/test_distributed_trace.c @@ -1670,6 +1670,46 @@ static void test_distributed_trace_create_trace_parent_header(void) { nr_free(actual); } +static void test_distributed_trace_set_trace_id(const bool pad_trace_id) { + nr_distributed_trace_t dt = {0}; + // clang-format off + const char* t_input[] = { + "", // empty string + "1234567890", // 10 characters + "1234567890abcdef", // 16 characters + "1234567890abcdef1234567890abcdef", // 32 characters + "1234567890123456789012345678901234567890123456789012345678901234567890" // 64 characters + }; + const char* t_padded[] = { + "00000000000000000000000000000000", // empty string lpadded to NR_TRACE_ID_MAX_SIZE with '0' + "00000000000000000000001234567890", // 10 characters lpadded to NR_TRACE_ID_MAX_SIZE with '0' + "00000000000000001234567890abcdef", // 16 characters lpadded to NR_TRACE_ID_MAX_SIZE with '0' + "1234567890abcdef1234567890abcdef", // 32 characters - no padding done + "1234567890123456789012345678901234567890123456789012345678901234567890" // 64 characters - no padding + }; + // clang-format on + + /* + * Test : NULL input => no trace id generated + */ + nr_distributed_trace_set_trace_id(&dt, NULL, pad_trace_id); + tlib_pass_if_null("NULL trace id", dt.trace_id); + + /* + * Test : valid input => trace id generated + */ + for (long unsigned int i = 0; i < sizeof(t_input) / sizeof(t_input[0]); i++) { + const char* expected = t_input[i]; + if (pad_trace_id) { + expected = t_padded[i]; + } + nr_distributed_trace_set_trace_id(&dt, t_input[i], pad_trace_id); + tlib_pass_if_not_null("trace id is set", dt.trace_id); + tlib_pass_if_str_equal("trace id has correct value", dt.trace_id, expected); + nr_free(dt.trace_id); + } +} + tlib_parallel_info_t parallel_info = {.suggested_nthreads = 2, .state_size = 0}; void test_main(void* p NRUNUSED) { @@ -1707,4 +1747,6 @@ void test_main(void* p NRUNUSED) { test_create_trace_state_header(); test_distributed_trace_create_trace_parent_header(); + test_distributed_trace_set_trace_id(false); + test_distributed_trace_set_trace_id(true); } diff --git a/axiom/tests/test_header.c b/axiom/tests/test_header.c index c34bd1d5b..3ff48e4f5 100644 --- a/axiom/tests/test_header.c +++ b/axiom/tests/test_header.c @@ -1702,6 +1702,75 @@ static void test_account_id_from_cross_process_id(void) { nr_header_account_id_from_cross_process_id("10#10")); } +static void test_nr_header_create_distributed_trace_map(void) { + nr_hashmap_t* header_map = NULL; + char* tracestate = "tracestate"; + char* traceparent = "traceparent"; + char* dt_payload = "newrelic"; + + header_map = nr_header_create_distributed_trace_map(NULL, NULL, NULL); + tlib_pass_if_null( + "NULL payload and NULL traceparent should return NULL header map", + header_map); + + header_map = nr_header_create_distributed_trace_map(NULL, NULL, tracestate); + tlib_pass_if_null( + "NULL payload and NULL traceparent should return NULL header map", + header_map); + + header_map = nr_header_create_distributed_trace_map(dt_payload, NULL, NULL); + tlib_pass_if_not_null("if valid dt_payload should return a header map", + header_map); + tlib_pass_if_size_t_equal( + "1 header passed in so should expect headers hashmap size of 1", 1, + nr_hashmap_count(header_map)); + nr_hashmap_destroy(&header_map); + + header_map + = nr_header_create_distributed_trace_map(dt_payload, traceparent, NULL); + tlib_pass_if_not_null("if valid dt_payload should return a header map", + header_map); + tlib_pass_if_size_t_equal( + "2 headers passed in so should expect headers hashmap size of 2", 2, + nr_hashmap_count(header_map)); + nr_hashmap_destroy(&header_map); + + header_map + = nr_header_create_distributed_trace_map(dt_payload, NULL, tracestate); + tlib_pass_if_not_null("if valid dt_payload should return a header map", + header_map); + tlib_pass_if_size_t_equal( + "2 headers passed in so should expect headers hashmap size of 2", 2, + nr_hashmap_count(header_map)); + nr_hashmap_destroy(&header_map); + + header_map = nr_header_create_distributed_trace_map(dt_payload, traceparent, + tracestate); + tlib_pass_if_not_null("if valid dt_payload should return a header map", + header_map); + tlib_pass_if_size_t_equal( + "3 headers passed in so should expect headers hashmap size of 3", 3, + nr_hashmap_count(header_map)); + nr_hashmap_destroy(&header_map); + + header_map + = nr_header_create_distributed_trace_map(NULL, traceparent, tracestate); + tlib_pass_if_not_null("if valid traceparent should return a header map", + header_map); + tlib_pass_if_size_t_equal( + "Two headers passed in so should expect headers hashmap size of 2", 2, + nr_hashmap_count(header_map)); + nr_hashmap_destroy(&header_map); + + header_map = nr_header_create_distributed_trace_map(NULL, traceparent, NULL); + tlib_pass_if_not_null("if valid traceparent should return a header map", + header_map); + tlib_pass_if_size_t_equal( + "1 header passed in so should expect headers hashmap size of 1", 1, + nr_hashmap_count(header_map)); + nr_hashmap_destroy(&header_map); +} + tlib_parallel_info_t parallel_info = {.suggested_nthreads = 2, .state_size = 0}; void test_main(void* p NRUNUSED) { @@ -1721,4 +1790,5 @@ void test_main(void* p NRUNUSED) { test_set_cat_txn(); test_set_synthetics_txn(); test_account_id_from_cross_process_id(); + test_nr_header_create_distributed_trace_map(); } diff --git a/axiom/tests/test_php_packages.c b/axiom/tests/test_php_packages.c index 427ece058..bd2323b2f 100644 --- a/axiom/tests/test_php_packages.c +++ b/axiom/tests/test_php_packages.c @@ -192,6 +192,183 @@ static void test_php_package_without_version(void) { nr_php_packages_destroy(&hm); } +static void test_php_package_priority(void) { +#define PACKAGE_NAME "vendor/package" +#define NO_VERSION NULL +#define PACKAGE_VERSION "1.0.0" +#define COMPOSER_VERSION "1.0.1" +#define COMPOSER_VERSION_2 "2.0.1" + nr_php_package_t* legacy_package; + nr_php_package_t* composer_package; + nr_php_package_t* composer_package_2; + nr_php_package_t* p; + nr_php_packages_t* hm = NULL; + int count; + char* legacy_versions[] = {NO_VERSION, PACKAGE_VERSION}; + + // Package added with legacy priority first - version from composer should win + for (size_t i = 0; i < sizeof(legacy_versions) / sizeof(legacy_versions[0]); + i++) { + legacy_package = nr_php_package_create( + PACKAGE_NAME, legacy_versions[i]); // legacy priority + tlib_pass_if_int_equal("create package by uses legacy priority", + NR_PHP_PACKAGE_SOURCE_LEGACY, + legacy_package->source_priority); + composer_package = nr_php_package_create_with_source( + PACKAGE_NAME, COMPOSER_VERSION, + NR_PHP_PACKAGE_SOURCE_COMPOSER); // composer priority + tlib_pass_if_int_equal("create package by uses composer priority", + NR_PHP_PACKAGE_SOURCE_COMPOSER, + composer_package->source_priority); + + hm = nr_php_packages_create(); + // order of adding packages: legacy first, composer second + nr_php_packages_add_package(hm, legacy_package); + nr_php_packages_add_package(hm, composer_package); + + count = nr_php_packages_count(hm); + tlib_pass_if_int_equal("add same package", 1, count); + + p = nr_php_packages_get_package(hm, PACKAGE_NAME); + tlib_pass_if_not_null("package exists", p); + tlib_pass_if_str_equal("package version from composer wins", + COMPOSER_VERSION, p->package_version); + + nr_php_packages_destroy(&hm); + } + + // Package added with composer priority first - version from composer should + // win + for (size_t i = 0; i < sizeof(legacy_versions) / sizeof(legacy_versions[0]); + i++) { + legacy_package = nr_php_package_create( + PACKAGE_NAME, legacy_versions[i]); // legacy priority + tlib_pass_if_int_equal("create package by uses legacy priority", + NR_PHP_PACKAGE_SOURCE_LEGACY, + legacy_package->source_priority); + composer_package = nr_php_package_create_with_source( + PACKAGE_NAME, COMPOSER_VERSION, + NR_PHP_PACKAGE_SOURCE_COMPOSER); // composer priority + tlib_pass_if_int_equal("create package by uses composer priority", + NR_PHP_PACKAGE_SOURCE_COMPOSER, + composer_package->source_priority); + + hm = nr_php_packages_create(); + // order of adding packages: legacy first, composer second + nr_php_packages_add_package(hm, composer_package); + nr_php_packages_add_package(hm, legacy_package); + + count = nr_php_packages_count(hm); + tlib_pass_if_int_equal("add same package", 1, count); + + p = nr_php_packages_get_package(hm, PACKAGE_NAME); + tlib_pass_if_not_null("package exists", p); + tlib_pass_if_str_equal("package version from composer wins", + COMPOSER_VERSION, p->package_version); + + nr_php_packages_destroy(&hm); + } + + // Package added with composer priority only - last version from composer + // should win + composer_package = nr_php_package_create_with_source( + PACKAGE_NAME, COMPOSER_VERSION, + NR_PHP_PACKAGE_SOURCE_COMPOSER); // composer priority + tlib_pass_if_int_equal("create package by uses composer priority", + NR_PHP_PACKAGE_SOURCE_COMPOSER, + composer_package->source_priority); + + composer_package_2 = nr_php_package_create_with_source( + PACKAGE_NAME, COMPOSER_VERSION_2, + NR_PHP_PACKAGE_SOURCE_COMPOSER); // composer priority + tlib_pass_if_int_equal("create package by uses composer priority", + NR_PHP_PACKAGE_SOURCE_COMPOSER, + composer_package_2->source_priority); + + hm = nr_php_packages_create(); + // order of adding packages: composer first, composer second + nr_php_packages_add_package(hm, composer_package); + nr_php_packages_add_package(hm, composer_package_2); + + count = nr_php_packages_count(hm); + tlib_pass_if_int_equal("add same package", 1, count); + + p = nr_php_packages_get_package(hm, PACKAGE_NAME); + tlib_pass_if_not_null("package exists", p); + tlib_pass_if_str_equal("package version from last composer wins", + COMPOSER_VERSION_2, p->package_version); + + nr_php_packages_destroy(&hm); +} + +static void nr_php_packages_itereate_callback(void* value, + const char* key, + size_t key_len, + void* user_data) { + nr_php_package_t* package = value; + const char* name = key; + nrbuf_t* buf = (nrbuf_t*)user_data; + + if (NULL == buf) { + return; + } + + /* append name, len, version to string */ + nr_buffer_add(buf, name, key_len); + nr_buffer_add(buf, NR_PSTR(",")); + nr_buffer_add(buf, package->package_version, + nr_strlen(package->package_version)); + nr_buffer_add(buf, NR_PSTR("\n")); +} + +static void test_php_package_iterate(void) { + nr_php_package_t* package1; + nr_php_package_t* package2; + nr_php_package_t* package3; + nr_php_packages_t* hm = nr_php_packages_create(); + nrbuf_t* buf; + int count; + + // Test: create multiple new packages and add to hashmap + package1 = nr_php_package_create("name1", "name1_version"); + package2 = nr_php_package_create("name2", "name2_version"); + package3 = nr_php_package_create("name3", "name3_version"); + + nr_php_packages_add_package(hm, package1); + nr_php_packages_add_package(hm, package2); + nr_php_packages_add_package(hm, package3); + + count = nr_php_packages_count(hm); + + tlib_pass_if_int_equal("package count", 3, count); + + /* tests with invalid values + * NOTE: nr_buffer_cptr(buf) will return NULL if no data is in the buffer + */ + buf = nr_buffer_create(0, 0); + nr_php_packages_iterate(NULL, nr_php_packages_itereate_callback, (void*)buf); + tlib_pass_if_null("iterate with NULL hashmap", nr_buffer_cptr(buf)); + nr_php_packages_iterate(hm, nr_php_packages_itereate_callback, NULL); + tlib_pass_if_null("iterate with NULL userdata", nr_buffer_cptr(buf)); + nr_php_packages_iterate(hm, NULL, (void*)buf); + tlib_pass_if_null("iterate with NULL callback", nr_buffer_cptr(buf)); + nr_php_packages_iterate(NULL, NULL, NULL); + tlib_pass_if_null("iterate with all NULL", nr_buffer_cptr(buf)); + nr_buffer_destroy(&buf); + + /* test with valid values */ + buf = nr_buffer_create(0, 0); + nr_php_packages_iterate(hm, nr_php_packages_itereate_callback, (void*)buf); + nr_buffer_add(buf, NR_PSTR("\0")); + tlib_pass_if_str_equal("iterate created proper string", + "name1,name1_version\nname2,name2_version\nname3,name3_version\n", nr_buffer_cptr(buf)); + nr_buffer_destroy(&buf); + + nr_php_packages_destroy(&hm); +} + + + tlib_parallel_info_t parallel_info = {.suggested_nthreads = -1, .state_size = 0}; @@ -203,4 +380,6 @@ void test_main(void* p NRUNUSED) { test_php_packages_to_json(); test_php_package_exists_in_hashmap(); test_php_package_without_version(); -} \ No newline at end of file + test_php_package_priority(); + test_php_package_iterate(); +} diff --git a/axiom/tests/test_segment_helpers.h b/axiom/tests/test_segment_helpers.h index 908432798..9ff7f54f7 100644 --- a/axiom/tests/test_segment_helpers.h +++ b/axiom/tests/test_segment_helpers.h @@ -12,6 +12,7 @@ #include "nr_segment.h" #include "nr_segment_datastore.h" #include "nr_segment_external.h" +#include "nr_segment_message.h" #include "tlib_main.h" #include "util_metrics_private.h" #include "nr_limits.h" @@ -287,7 +288,7 @@ static NRUNUSED bool test_segment_end_and_keep(nr_segment_t** segment_ptr) { } /* - * Purpose : Ends an external segment without nulling out the segment pointer. + * Purpose : Ends an external segment without nulling out the segment pointer. * * WARNING : This can only be used safely when the segment priority queue is * disabled. @@ -326,4 +327,24 @@ static NRUNUSED bool test_segment_datastore_end_and_keep( return nr_segment_datastore_end(&segment, params); } +/* + * Purpose : Ends a message segment without nulling out the segment pointer. + * + * WARNING : This can only be used safely when the segment priority queue is + * disabled. + */ +static NRUNUSED bool test_segment_message_end_and_keep( + nr_segment_t** segment_ptr, + nr_segment_message_params_t* params) { + nr_segment_t* segment; + + if (NULL == segment_ptr) { + return false; + } + + segment = *segment_ptr; + + return nr_segment_message_end(&segment, params); +} + #endif /* TEST_SEGMENT_HELPERS_HDR */ diff --git a/axiom/tests/test_segment_message.c b/axiom/tests/test_segment_message.c new file mode 100644 index 000000000..bdb944f39 --- /dev/null +++ b/axiom/tests/test_segment_message.c @@ -0,0 +1,1229 @@ +/* + * Copyright 2020 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "nr_axiom.h" +#include "nr_header.h" +#include "nr_segment_message.h" +#include "test_segment_helpers.h" +#include "nr_attributes.h" +#include "nr_attributes_private.h" +#include "util_hash.h" +#include "util_memory.h" +#include "util_object.h" +#include "util_reply.h" +#include "util_strings.h" +#include "util_text.h" + +#include "tlib_main.h" + +typedef struct { + const char* test_name; + const char* name; + const char* txn_rollup_metric; + const char* library_metric; + uint32_t num_metrics; + const char* destination_name; + const char* cloud_region; + const char* cloud_account_id; + const char* messaging_system; + const char* cloud_resource_id; + const char* server_address; + const char* aws_operation; + char* messaging_destination_publish_name; + char* messaging_destination_routing_key; + uint64_t server_port; +} segment_message_expecteds_t; + +static nr_segment_t* mock_txn_segment(void) { + nrtxn_t* txn = new_txn(0); + + return nr_segment_start(txn, NULL, NULL); +} + +static void test_message_segment(nr_segment_message_params_t* params, + nr_segment_cloud_attrs_t* cloud_attrs, + bool message_attributes_enabled, + segment_message_expecteds_t expecteds) { + uint32_t all = NR_ATTRIBUTE_DESTINATION_ALL; + nrobj_t* obj = NULL; + nr_segment_t* seg = mock_txn_segment(); + nrtxn_t* txn = seg->txn; + seg->txn->options.message_tracer_segment_parameters_enabled + = message_attributes_enabled; + + nr_segment_traces_add_cloud_attributes(seg, cloud_attrs); + /* Check the agent cloud attributes. */ + obj = nr_attributes_agent_to_obj(seg->attributes, all); + tlib_pass_if_str_equal(expecteds.test_name, expecteds.aws_operation, + nro_get_hash_string(obj, NR_ATTR_AWS_OPERATION, 0)); + tlib_pass_if_str_equal( + expecteds.test_name, expecteds.cloud_resource_id, + nro_get_hash_string(obj, NR_ATTR_CLOUD_RESOURCE_ID, 0)); + tlib_pass_if_str_equal(expecteds.test_name, expecteds.cloud_account_id, + nro_get_hash_string(obj, NR_ATTR_CLOUD_ACCOUNT_ID, 0)); + tlib_pass_if_str_equal(expecteds.test_name, expecteds.cloud_region, + nro_get_hash_string(obj, NR_ATTR_CLOUD_REGION, 0)); + nro_delete(obj); + + test_segment_message_end_and_keep(&seg, params); + /* Check the metrics and txn naming. */ + tlib_pass_if_str_equal(expecteds.test_name, expecteds.name, + nr_string_get(seg->txn->trace_strings, seg->name)); + test_txn_metric_created(expecteds.test_name, txn->unscoped_metrics, + expecteds.txn_rollup_metric); + test_txn_metric_created(expecteds.test_name, txn->unscoped_metrics, + expecteds.library_metric); + test_metric_vector_size(seg->metrics, expecteds.num_metrics); + + /* Check the segment settings and typed attributes. */ + tlib_pass_if_true(expecteds.test_name, NR_SEGMENT_MESSAGE == seg->type, + "NR_SEGMENT_MESSAGE"); + tlib_pass_if_str_equal(expecteds.test_name, + seg->typed_attributes->message.destination_name, + expecteds.destination_name); + tlib_pass_if_str_equal(expecteds.test_name, + seg->typed_attributes->message.messaging_system, + expecteds.messaging_system); + tlib_pass_if_str_equal(expecteds.test_name, + seg->typed_attributes->message.server_address, + expecteds.server_address); + tlib_pass_if_str_equal( + expecteds.test_name, + seg->typed_attributes->message.messaging_destination_publish_name, + expecteds.messaging_destination_publish_name); + tlib_pass_if_str_equal( + expecteds.test_name, + seg->typed_attributes->message.messaging_destination_routing_key, + expecteds.messaging_destination_routing_key); + tlib_pass_if_int_equal(expecteds.test_name, + seg->typed_attributes->message.server_port, + expecteds.server_port); + nr_txn_destroy(&txn); +} + +static void test_bad_parameters(void) { + nr_segment_t seg_null = {0}; + nr_segment_t* seg_null_ptr; + nr_segment_t* seg = mock_txn_segment(); + nrtxn_t* txn = seg->txn; + nr_segment_message_params_t params = {0}; + + tlib_pass_if_false("bad parameters", nr_segment_message_end(NULL, ¶ms), + "expected false"); + + seg_null_ptr = NULL; + + tlib_pass_if_false("bad parameters", + nr_segment_message_end(&seg_null_ptr, ¶ms), + "expected false"); + + seg_null_ptr = &seg_null; + tlib_pass_if_false("bad parameters", + nr_segment_message_end(&seg_null_ptr, ¶ms), + "expected false"); + + tlib_pass_if_false("bad parameters", nr_segment_message_end(&seg, NULL), + "expected false"); + test_metric_vector_size(seg->metrics, 0); + + nr_txn_destroy(&txn); +} + +static void test_segment_message_destination_type(void) { + /* + * The following values are used to create metrics: + * library + * destination_type + * message_action + * destination_name + */ + /* Test NR_MESSAGE_DESTINATION_TYPE_TEMP_TOPIC destination type */ + test_message_segment( + &(nr_segment_message_params_t){ + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TEMP_TOPIC, + .destination_name = "my_queue_or_topic"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name + = "Test NR_MESSAGE_DESTINATION_TYPE_TEMP_TOPIC destination type", + .name = "MessageBroker/SQS/Topic/Produce/Temp", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_queue_or_topic", + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = NULL}); + + /* Test NR_MESSAGE_DESTINATION_TYPE_TEMP_QUEUE destination type */ + test_message_segment( + &(nr_segment_message_params_t){ + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TEMP_QUEUE, + .destination_name = "my_queue_or_topic"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name + = "Test NR_MESSAGE_DESTINATION_TYPE_TEMP_QUEUE destination type", + .name = "MessageBroker/SQS/Queue/Produce/Temp", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_queue_or_topic", + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = NULL}); + + /* Test NR_MESSAGE_DESTINATION_TYPE_EXCHANGE destination type */ + test_message_segment( + &(nr_segment_message_params_t){ + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_EXCHANGE, + .destination_name = "my_queue_or_topic"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name + = "Test NR_MESSAGE_DESTINATION_TYPE_EXCHANGE destination type", + .name = "MessageBroker/SQS/Exchange/Produce/Named/my_queue_or_topic", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_queue_or_topic", + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = NULL}); + + /* Test NR_MESSAGE_DESTINATION_TYPE_TOPIC destination type */ + test_message_segment( + &(nr_segment_message_params_t){ + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_queue_or_topic"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name + = "Test NR_MESSAGE_DESTINATION_TYPE_EXCHANGE destination type", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_queue_or_topic", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_queue_or_topic", + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = NULL}); + + /* Test NR_MESSAGE_DESTINATION_TYPE_QUEUE destination type */ + test_message_segment( + &(nr_segment_message_params_t){ + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_QUEUE, + .destination_name = "my_queue_or_topic"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name + = "Test NR_MESSAGE_DESTINATION_TYPE_QUEUE destination type", + .name = "MessageBroker/SQS/Queue/Produce/Named/my_queue_or_topic", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_queue_or_topic", + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = NULL}); +} + +static void test_segment_message_message_action(void) { + /* + * The following values are used to create metrics: + * library + * destination_type + * message_action + * destination_name + */ + + /* Test NR_SPANKIND_PRODUCER message action */ + test_message_segment( + &(nr_segment_message_params_t){ + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_queue_or_topic"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test NR_SPANKIND_PRODUCER message action", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_queue_or_topic", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_queue_or_topic", + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = NULL}); + + /* Test NR_SPANKIND_CONSUMER message action */ + + test_message_segment( + &(nr_segment_message_params_t){ + .library = "SQS", + .message_action = NR_SPANKIND_CONSUMER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_queue_or_topic"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test NR_SPANKIND_CONSUMER message action", + .name = "MessageBroker/SQS/Topic/Consume/Named/my_queue_or_topic", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_queue_or_topic", + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = NULL}); + + /* + * Test NR_SPANKIND_CLIENT message action; this is not + * allowed for message segments, should show unknown. + */ + test_message_segment( + &(nr_segment_message_params_t){ + .library = "SQS", + .message_action = NR_SPANKIND_CLIENT, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_queue_or_topic"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test NR_SPANKIND_CLIENT message action", + .name = "MessageBroker/SQS/Topic//Named/my_queue_or_topic", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_queue_or_topic", + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = NULL}); +} + +static void test_segment_message_library(void) { + /* + * The following values are used to create metrics: + * library + * destination_type + * message_action + * destination_name + */ + /* Test null library */ + test_message_segment( + &(nr_segment_message_params_t){ + .library = NULL, + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_queue_or_topic"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test null library", + .name + = "MessageBroker//Topic/Produce/Named/my_queue_or_topic", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker//all", + .num_metrics = 1, + .destination_name = "my_queue_or_topic", + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = NULL}); + + /* Test empty library */ + + test_message_segment( + &(nr_segment_message_params_t){ + .library = "", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_queue_or_topic"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test empty library", + .name + = "MessageBroker//Topic/Produce/Named/my_queue_or_topic", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker//all", + .num_metrics = 1, + .destination_name = "my_queue_or_topic", + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = NULL}); + + /* Test valid library */ + + test_message_segment( + &(nr_segment_message_params_t){ + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_queue_or_topic"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test valid library", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_queue_or_topic", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_queue_or_topic", + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = NULL}); +} + +static void test_segment_message_destination_name(void) { + /* + * The following values are used to create metrics: + * library + * destination_type + * message_action + * destination_name + */ + /* Test null destination_name */ + test_message_segment( + &(nr_segment_message_params_t){ + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = NULL}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test null destination_name", + .name = "MessageBroker/SQS/Topic/Produce/Named/", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = NULL, + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = NULL}); + + /* Test empty destination_name */ + test_message_segment( + &(nr_segment_message_params_t){ + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = ""}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test empty destination_name", + .name = "MessageBroker/SQS/Topic/Produce/Named/", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = NULL, + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = NULL}); + + /* Test valid destination_name */ + test_message_segment( + &(nr_segment_message_params_t){ + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test valid destination_name", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_destination", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_destination", + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = NULL}); +} + +static void test_segment_message_cloud_region(void) { + /* + * cloud_region values should NOT impact the creation of + * metrics. + */ + + /* Test null cloud_region */ + test_message_segment( + &(nr_segment_message_params_t){ + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test null cloud_region", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_destination", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_destination", + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = NULL}); + + /* Test empty cloud_region */ + test_message_segment( + &(nr_segment_message_params_t){ + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){.cloud_region = ""}, + true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test empty cloud_region", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_destination", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_destination", + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = NULL}); + + /* Test valid cloud_region */ + test_message_segment( + &(nr_segment_message_params_t){ + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){.cloud_region = "wild-west-1"}, + true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test valid cloud_region", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_destination", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_destination", + .cloud_region = "wild-west-1", + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = NULL}); +} + +static void test_segment_message_cloud_account_id(void) { + /* + * cloud_account_id values should NOT impact the creation + * of metrics. + */ + + /* Test null cloud_account_id */ + test_message_segment( + &(nr_segment_message_params_t){ + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test null cloud_account_id", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_destination", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_destination", + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = NULL}); + + /* Test empty cloud_account_id */ + test_message_segment( + &(nr_segment_message_params_t){ + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){.cloud_account_id = ""}, + true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test empty cloud_account_id", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_destination", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_destination", + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = NULL}); + + /* Test valid cloud_account_id */ + test_message_segment( + &(nr_segment_message_params_t){ + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){.cloud_account_id = "12345678"}, + true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test valid cloud_account_id", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_destination", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_destination", + .cloud_region = NULL, + .cloud_account_id = "12345678", + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = NULL}); +} + +static void test_segment_message_messaging_system(void) { + /* + * messaging_system values should NOT impact the creation + * of metrics. + */ + + /* Test null messaging_system */ + test_message_segment( + &(nr_segment_message_params_t){ + .messaging_system = NULL, + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test null messaging_system", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_destination", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_destination", + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = NULL}); + + /* Test empty messaging_system */ + test_message_segment( + &(nr_segment_message_params_t){ + .messaging_system = "", + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test empty messaging_system", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_destination", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_destination", + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = NULL}); + + /* Test valid messaging_system */ + test_message_segment( + &(nr_segment_message_params_t){ + .messaging_system = "my_messaging_system", + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test valid messaging_system", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_destination", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_destination", + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = "my_messaging_system", + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = NULL}); +} + +static void test_segment_message_cloud_resource_id(void) { + /* + * cloud_resource_id values should NOT impact the creation + * of metrics. + */ + + /* Test null cloud_resource_id */ + test_message_segment( + &(nr_segment_message_params_t){ + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test null cloud_resource_id ", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_destination", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_destination", + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = NULL}); + + /* Test empty cloud_resource_id */ + test_message_segment( + &(nr_segment_message_params_t){ + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){.cloud_resource_id = ""}, + true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test empty cloud_resource_id ", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_destination", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_destination", + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = NULL}); + + /* Test valid cloud_resource_id */ + test_message_segment( + &(nr_segment_message_params_t){ + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){.cloud_resource_id = "my_resource_id"}, + true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test valid cloud_resource_id ", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_destination", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_destination", + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = "my_resource_id", + .server_address = NULL, + .aws_operation = NULL}); +} + +static void test_segment_message_server_address(void) { + /* + * server_address values should NOT impact the creation + * of metrics. + */ + + /* Test null server_address */ + test_message_segment( + &(nr_segment_message_params_t){ + .server_address = "localhost", + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test null server_address", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_destination", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_destination", + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = "localhost", + .aws_operation = NULL}); + + /* Test empty server_address */ + test_message_segment( + &(nr_segment_message_params_t){ + .server_address = "", + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test empty server_address", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_destination", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_destination", + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = NULL}); + + /* Test valid server_address */ + test_message_segment( + &(nr_segment_message_params_t){ + .server_address = "localhost", + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test valid server_address", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_destination", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_destination", + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = "localhost", + .aws_operation = NULL}); +} + +static void test_segment_message_aws_operation(void) { + /* + * aws_operation values should NOT impact the creation + * of metrics. + */ + + /* Test null aws_operation */ + test_message_segment( + &(nr_segment_message_params_t){ + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test null aws_operation", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_destination", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_destination", + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = NULL}); + + /* Test empty aws_operation */ + test_message_segment( + &(nr_segment_message_params_t){ + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){.aws_operation = ""}, + true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test empty aws_operation", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_destination", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_destination", + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = NULL}); + + /* Test valid aws_operation */ + test_message_segment( + &(nr_segment_message_params_t){ + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){.aws_operation = "sendMessage"}, + true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test valid aws_operation", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_destination", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_destination", + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = "sendMessage"}); +} + +static void test_segment_message_server_port(void) { + /* + * server port values should NOT impact the creation + * of metrics. + */ + + /* Test server port not set, implicitly unset */ + test_message_segment( + &(nr_segment_message_params_t){ + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "server port not set, implicitly unset", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_destination", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_destination", + .server_port = 0}); + + /* Test server port explicitly set to 0 (unset) */ + test_message_segment( + &(nr_segment_message_params_t){ + .server_port = 0, + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "server port explicitly set to 0 (unset)", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_destination", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_destination", + .server_port = 0}); + + /* Test valid server_port */ + test_message_segment( + &(nr_segment_message_params_t){ + .server_port = 1234, + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test valid aws_operation", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_destination", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_destination", + .server_port = 1234}); +} + +static void test_segment_messaging_destination_publishing_name(void) { + /* + * messaging_destination_publish_name values should NOT impact the creation + * of metrics. + */ + + /* Test messaging_destination_publish_name is NULL */ + test_message_segment( + &(nr_segment_message_params_t){ + .messaging_destination_publish_name = NULL, + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name + = "messaging_destination_publish_name is NULL, attribute " + "should be NULL, destination_name is used for metric/txn", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_destination", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_destination", + .messaging_destination_publish_name = NULL}); + + /* Test destination_publishing_name is empty string */ + test_message_segment( + &(nr_segment_message_params_t){ + .messaging_destination_publish_name = "", + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name + = "messaging_destination_publish_name is empty string, " + "attribute should be NULL, destination_name is used for metric/txn", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_destination", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_destination", + .messaging_destination_publish_name = NULL}); + + /* Test valid messaging_destination_publish_name */ + test_message_segment( + &(nr_segment_message_params_t){ + .messaging_destination_publish_name = "publish_name", + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test valid messaging_destination_publish_name is " + "non-empty string, attribute should be the string, " + "should be used for metric/txn", + .name = "MessageBroker/SQS/Topic/Produce/Named/publish_name", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_destination", + .messaging_destination_publish_name = "publish_name"}); +} + +static void test_segment_messaging_destination_routing_key(void) { + /* + * messaging_destination_routing_key values should NOT impact the creation + * of metrics. + */ + + /* Test messaging_destination_routing_key is NULL */ + test_message_segment( + &(nr_segment_message_params_t){ + .messaging_destination_routing_key = NULL, + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "messaging_destination_routing_key is NULL, attribute " + "should be NULL", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_destination", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_destination", + .messaging_destination_routing_key = NULL}); + + /* Test messaging_destination_routing_key is empty string */ + test_message_segment( + &(nr_segment_message_params_t){ + .messaging_destination_routing_key = "", + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "messaging_destination_routing_key is empty string, " + "attribute should be NULL", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_destination", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_destination", + .messaging_destination_routing_key = NULL}); + + /* Test valid messaging_destination_routing_key */ + test_message_segment( + &(nr_segment_message_params_t){ + .messaging_destination_routing_key = "key to the kingdom", + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test valid messaging_destination_routing_key is " + "non-empty string, attribute should be the string", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_destination", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_destination", + .messaging_destination_routing_key = "key to the kingdom"}); +} + +static void test_segment_message_parameters_enabled(void) { + /* + * Attributes should be set based on value of parameters_enabled. + */ + + /* Test true message_parameters_enabled */ + test_message_segment( + &(nr_segment_message_params_t){ + .messaging_destination_routing_key = "key to the kingdom", + .messaging_destination_publish_name = "publish_name", + .server_port = 1234, + .server_address = "localhost", + .messaging_system = "my_system", + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){.aws_operation = "sendMessage", + .cloud_region = "wild-west-1", + .cloud_account_id = "12345678", + .cloud_resource_id = "my_resource_id"}, + true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test true message_parameters_enabled", + .name = "MessageBroker/SQS/Topic/Produce/Named/publish_name", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = "my_destination", + .cloud_region = "wild-west-1", + .cloud_account_id = "12345678", + .messaging_system = "my_system", + .cloud_resource_id = "my_resource_id", + .server_address = "localhost", + .messaging_destination_routing_key = "key to the kingdom", + .server_port = 1234, + .messaging_destination_publish_name = "publish_name", + .aws_operation = "sendMessage"}); + + /* + * Test false message_parameters_enabled. Message attributes should not show, + * but cloud attributes should be unaffected. + */ + test_message_segment( + &(nr_segment_message_params_t){ + .messaging_destination_routing_key = "key to the kingdom", + .server_port = 1234, + .server_address = "localhost", + .messaging_system = "my_system", + .library = "SQS", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_TOPIC, + .destination_name = "my_destination"}, + &(nr_segment_cloud_attrs_t){.aws_operation = "sendMessage", + .cloud_region = "wild-west-1", + .cloud_account_id = "12345678", + .cloud_resource_id = "my_resource_id"}, + false /* enable attributes */, + (segment_message_expecteds_t){ + .test_name = "Test false message_parameters_enabled", + .name = "MessageBroker/SQS/Topic/Produce/Named/my_destination", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/SQS/all", + .num_metrics = 1, + .destination_name = NULL, + .cloud_region = "wild-west-1", + .cloud_account_id = "12345678", + .messaging_system = NULL, + .cloud_resource_id = "my_resource_id", + .server_address = NULL, + .messaging_destination_routing_key = NULL, + .server_port = 0, + .messaging_destination_publish_name = NULL, + .aws_operation = "sendMessage"}); +} + +tlib_parallel_info_t parallel_info = {.suggested_nthreads = 4, .state_size = 0}; + +void test_main(void* p NRUNUSED) { + test_bad_parameters(); + test_segment_message_destination_type(); + test_segment_message_message_action(); + test_segment_message_library(); + test_segment_message_destination_name(); + test_segment_message_cloud_region(); + test_segment_message_cloud_account_id(); + test_segment_message_messaging_system(); + test_segment_message_cloud_resource_id(); + test_segment_message_server_address(); + test_segment_message_server_port(); + test_segment_messaging_destination_publishing_name(); + test_segment_messaging_destination_routing_key(); + test_segment_message_aws_operation(); + test_segment_message_parameters_enabled(); +} diff --git a/axiom/tests/test_segment_private.c b/axiom/tests/test_segment_private.c index e586f1f4f..82e0dfd80 100644 --- a/axiom/tests/test_segment_private.c +++ b/axiom/tests/test_segment_private.c @@ -25,6 +25,7 @@ static void test_bad_parameters(void) { nr_segment_destroy_fields(NULL); nr_segment_datastore_destroy_fields(NULL); nr_segment_external_destroy_fields(NULL); + nr_segment_message_destroy_fields(NULL); nr_segment_metric_destroy_fields(NULL); nr_segment_error_destroy_fields(NULL); } @@ -205,7 +206,6 @@ static void test_set_custom(void) { tlib_pass_if_int_equal( "Setting an untyped segment to custom must set the type", (int)NR_SEGMENT_CUSTOM, (int)s.type); - nr_segment_set_datastore(&t, &d); tlib_pass_if_true("Setting a datastore segment to custom must be successful", nr_segment_set_custom(&t), "Expected true"); @@ -316,6 +316,48 @@ static void test_set_destroy_external_fields(void) { &s.typed_attributes); } +static void test_set_destroy_message_fields(void) { + nr_segment_t s = {.type = NR_SEGMENT_MESSAGE}; + + nr_segment_message_t m = {.message_action = NR_SPANKIND_CLIENT, + .messaging_system = "my_messaging_system", + .server_address = "localhost"}; + + nr_segment_external_t e = {.transaction_guid = "transaction_guid", + .uri = "uri", + .library = "library", + .procedure = "procedure", + .status = 200}; + /* + * Test : Bad parameters. + */ + tlib_pass_if_false( + "Setting a NULL segment's message attributes must not be successful", + nr_segment_set_message(NULL, &m), "Expected false"); + + tlib_pass_if_false( + "Setting a segment with NULL message attributes must not be successful", + nr_segment_set_message(&s, NULL), "Expected false"); + + /* + * Test : Normal operation. + */ + tlib_pass_if_true("Setting a segment's message attributes must be successful", + nr_segment_set_message(&s, &m), "Expected true"); + + tlib_pass_if_true( + "Setting a segment from message attributes to external attributes must " + "be successful", + nr_segment_set_external(&s, &e), "Expected true"); + + /* Valgrind shall affirm that the attributes for s were cleaned + * up when the segment type was changed from message to external. + */ + + /* Clean up */ + nr_segment_destroy_typed_attributes(NR_SEGMENT_EXTERNAL, &s.typed_attributes); +} + static void test_destroy_typed_attributes(void) { nr_segment_t s = {0}; char* test_string = "0123456789"; @@ -325,10 +367,41 @@ static void test_destroy_typed_attributes(void) { */ nr_segment_destroy_typed_attributes(NR_SEGMENT_EXTERNAL, NULL); nr_segment_destroy_typed_attributes(NR_SEGMENT_EXTERNAL, &s.typed_attributes); + tlib_pass_if_null( + "Even with bad parameters, nr_segment_destroy_typed_attributes should " + "not crash and s.typed_attributes should be NULL", + s.typed_attributes); nr_segment_destroy_typed_attributes(NR_SEGMENT_DATASTORE, NULL); nr_segment_destroy_typed_attributes(NR_SEGMENT_DATASTORE, &s.typed_attributes); + tlib_pass_if_null( + "Even with bad parameters, nr_segment_destroy_typed_attributes should " + "not crash and s.typed_attributes should be NULL", + s.typed_attributes); + nr_segment_destroy_typed_attributes(NR_SEGMENT_MESSAGE, NULL); + nr_segment_destroy_typed_attributes(NR_SEGMENT_MESSAGE, &s.typed_attributes); + tlib_pass_if_null( + "Even with bad parameters, nr_segment_destroy_typed_attributes should " + "not crash and s.typed_attributes should be NULL", + s.typed_attributes); + /* + * Test : Clean up typed attributes for a message segment + */ + s.type = NR_SEGMENT_MESSAGE; + s.typed_attributes = nr_zalloc(sizeof(nr_segment_typed_attributes_t)); + s.typed_attributes->message.destination_name = nr_strdup("queue_name"); + s.typed_attributes->message.messaging_system = nr_strdup("aws_sqs"); + s.typed_attributes->message.server_address = nr_strdup("localhost"); + + /* + * Valgrind shall affirm that the attributes were cleaned up. + */ + nr_segment_destroy_typed_attributes(NR_SEGMENT_MESSAGE, &s.typed_attributes); + tlib_pass_if_null( + "After nr_segment_destroy_typed_attributes, s.typed_attributes should be " + "NULL", + s.typed_attributes); /* * Test : Clean up typed attributes for an external segment */ @@ -340,7 +413,14 @@ static void test_destroy_typed_attributes(void) { s.typed_attributes->external.procedure = nr_strdup(test_string); s.typed_attributes->external.status = 200; + /* + * Valgrind shall affirm that the attributes were cleaned up. + */ nr_segment_destroy_typed_attributes(NR_SEGMENT_EXTERNAL, &s.typed_attributes); + tlib_pass_if_null( + "After nr_segment_destroy_typed_attributes, s.typed_attributes should be " + "NULL", + s.typed_attributes); /* * Test : Clean up typed attributes for a datastore segment @@ -358,8 +438,15 @@ static void test_destroy_typed_attributes(void) { = nr_strdup(test_string); s.typed_attributes->datastore.instance.database_name = nr_strdup(test_string); + /* + * Valgrind shall affirm that the attributes were cleaned up. + */ nr_segment_destroy_typed_attributes(NR_SEGMENT_DATASTORE, &s.typed_attributes); + tlib_pass_if_null( + "After nr_segment_destroy_typed_attributes, s.typed_attributes should be " + "NULL", + s.typed_attributes); } static void test_destroy_fields(void) { @@ -394,6 +481,7 @@ void test_main(void* p NRUNUSED) { test_set_custom(); test_set_destroy_datastore_fields(); test_set_destroy_external_fields(); + test_set_destroy_message_fields(); test_destroy_typed_attributes(); test_destroy_fields(); test_destroy_metric(); diff --git a/axiom/tests/test_segment_traces.c b/axiom/tests/test_segment_traces.c index 450ea8af6..b43e15b28 100644 --- a/axiom/tests/test_segment_traces.c +++ b/axiom/tests/test_segment_traces.c @@ -51,6 +51,10 @@ tlib_pass_if_str_equal("category", "http", \ nr_span_event_get_category(evt)); \ break; \ + case NR_SPAN_MESSAGE: \ + tlib_pass_if_str_equal("category", "message", \ + nr_span_event_get_category(evt)); \ + break; \ default: \ tlib_pass_if_true("invalid category", false, "category=%s", \ nr_span_event_get_category(evt)); \ @@ -86,6 +90,20 @@ tlib_pass_if_int_equal("status", expected_status, \ nr_span_event_get_external_status(span_event)); +#define SPAN_EVENT_COMPARE_MESSAGE(span_event, expected_destination_name, \ + expected_messaging_system, \ + expected_server_address) \ + tlib_pass_if_str_equal("messaging.destination.name", \ + expected_destination_name, \ + nr_span_event_get_message( \ + span_event, NR_SPAN_MESSAGE_DESTINATION_NAME)); \ + tlib_pass_if_str_equal("messaging.system", expected_messaging_system, \ + nr_span_event_get_message( \ + span_event, NR_SPAN_MESSAGE_MESSAGING_SYSTEM)); \ + tlib_pass_if_str_equal( \ + "server.address", expected_server_address, \ + nr_span_event_get_message(span_event, NR_SPAN_MESSAGE_SERVER_ADDRESS)); + static void nr_vector_span_event_dtor(void* element, void* userdata NRUNUSED) { nr_span_event_destroy((nr_span_event_t**)&element); } @@ -652,13 +670,15 @@ static void test_json_print_segments_invalid_typed_attributes(void) { nr_span_event_t* evt_root; nr_span_event_t* evt_a; nr_span_event_t* evt_b; + nr_span_event_t* evt_c; nrtxn_t txn = {0}; // clang-format off - nr_segment_t root = {.txn = &txn, .start_time = 0, .stop_time = 9000}; + nr_segment_t root = {.txn = &txn, .start_time = 0, .stop_time = 11000}; nr_segment_t A = {.txn = &txn, .start_time = 1000, .stop_time = 6000}; nr_segment_t B = {.txn = &txn, .start_time = 6000, .stop_time = 8000}; + nr_segment_t C = {.txn = &txn, .start_time = 9000, .stop_time = 10000}; // clang-format on buf = nr_buffer_create(4096, 4096); @@ -668,20 +688,23 @@ static void test_json_print_segments_invalid_typed_attributes(void) { /* Mock up the transaction */ mock_txn(&txn, &root); txn.abs_start_time = 1000; - txn.segment_count = 2; + txn.segment_count = 3; /* Create a collection of mock segments */ nr_segment_children_init(&root.children); nr_segment_add_child(&root, &A); nr_segment_add_child(&root, &B); + nr_segment_add_child(&root, &C); root.name = nr_string_add(txn.trace_strings, "WebTransaction/*"); A.name = nr_string_add(txn.trace_strings, "A"); B.name = nr_string_add(txn.trace_strings, "B"); + C.name = nr_string_add(txn.trace_strings, "C"); A.type = NR_SEGMENT_EXTERNAL; B.type = NR_SEGMENT_DATASTORE; + C.type = NR_SEGMENT_MESSAGE; /* * Test : Normal operation @@ -689,23 +712,27 @@ static void test_json_print_segments_invalid_typed_attributes(void) { rv = nr_segment_traces_json_print_segments(buf, span_events, NULL, NULL, &txn, &root, segment_names); tlib_pass_if_bool_equal("success", true, rv); - test_buffer_contents("datastore params", buf, - "[0,9,\"`0\",{},[[1,6," - "\"`1\",{},[]],[6,8," - "\"`2\",{},[]]]]"); + test_buffer_contents("segment attributes", buf, + "[0,11,\"`0\",{}," + "[[1,6,\"`1\",{},[]]," + "[6,8,\"`2\",{},[]]," + "[9,10,\"`3\",{},[]]]]"); - tlib_pass_if_uint_equal("span event size", nr_vector_size(span_events), 3); + tlib_pass_if_uint_equal("span event size", nr_vector_size(span_events), 4); evt_root = (nr_span_event_t*)nr_vector_get(span_events, 0); evt_a = (nr_span_event_t*)nr_vector_get(span_events, 1); evt_b = (nr_span_event_t*)nr_vector_get(span_events, 2); + evt_c = (nr_span_event_t*)nr_vector_get(span_events, 3); SPAN_EVENT_COMPARE(evt_root, "WebTransaction/*", NR_SPAN_GENERIC, NULL, 1000, - 9000); + 11000); SPAN_EVENT_COMPARE(evt_a, "A", NR_SPAN_HTTP, evt_root, 2000, 5000); SPAN_EVENT_COMPARE_EXTERNAL(evt_a, NULL, NULL, NULL, 0); SPAN_EVENT_COMPARE(evt_b, "B", NR_SPAN_DATASTORE, evt_root, 7000, 2000); SPAN_EVENT_COMPARE_DATASTORE(evt_b, NULL, NULL, NULL, NULL); + SPAN_EVENT_COMPARE(evt_c, "C", NR_SPAN_MESSAGE, evt_root, 10000, 1000); + SPAN_EVENT_COMPARE_MESSAGE(evt_c, NULL, NULL, NULL); /* Clean up */ nr_segment_children_deinit(&root.children); @@ -713,6 +740,7 @@ static void test_json_print_segments_invalid_typed_attributes(void) { nr_segment_destroy_fields(&A); nr_segment_destroy_fields(&B); + nr_segment_destroy_fields(&C); cleanup_mock_txn(&txn); nr_string_pool_destroy(&segment_names); @@ -904,7 +932,88 @@ static void test_json_print_segments_external_async_user_attrs(void) { nr_vector_destroy(&span_events); } -static void test_json_print_segments_datastore_external(void) { +static void test_json_print_segments_message_attributes(void) { + bool rv; + nrbuf_t* buf; + nr_vector_t* span_events; + nrpool_t* segment_names; + + nrtxn_t txn = {0}; + + nr_span_event_t* evt_root; + nr_span_event_t* evt_a; + + // clang-format off + nr_segment_t root = {.txn = &txn, .start_time = 0, .stop_time = 9000}; + nr_segment_t A = {.txn = &txn, .start_time = 1000, .stop_time = 6000}; + // clang-format on + + buf = nr_buffer_create(4096, 4096); + span_events = nr_vector_create(9, nr_vector_span_event_dtor, NULL); + segment_names = nr_string_pool_create(); + + /* Mock up the transaction */ + mock_txn(&txn, &root); + txn.abs_start_time = 1000; + txn.segment_count = 2; + + /* Create a collection of mock segments */ + + /* ------root------- + * ------A------ + */ + + nr_segment_children_init(&root.children); + + nr_segment_add_child(&root, &A); + + root.name = nr_string_add(txn.trace_strings, "WebTransaction/*"); + A.name = nr_string_add(txn.trace_strings, "A"); + + A.type = NR_SEGMENT_MESSAGE; + A.attributes = NULL; + A.typed_attributes = nr_zalloc(sizeof(nr_segment_typed_attributes_t)); + A.typed_attributes->message.destination_name = nr_strdup("queue_name"); + A.typed_attributes->message.messaging_system = nr_strdup("aws_sqs"); + A.typed_attributes->message.server_address = nr_strdup("localhost"); + + /* + * Test : Normal operation + */ + rv = nr_segment_traces_json_print_segments(buf, span_events, NULL, NULL, &txn, + &root, segment_names); + tlib_pass_if_bool_equal("success", true, rv); + test_buffer_contents("message attributes", buf, + "[0,9,\"`0\",{},[[1,6,\"`1\",{" + "\"destination_name\":\"queue_name\"," + "\"messaging_system\":\"aws_sqs\"," + "\"server_address\":\"localhost\"" + "},[]]]]"); + + tlib_pass_if_uint_equal("span event size", nr_vector_size(span_events), 2); + + evt_root = (nr_span_event_t*)nr_vector_get(span_events, 0); + evt_a = (nr_span_event_t*)nr_vector_get(span_events, 1); + + SPAN_EVENT_COMPARE(evt_root, "WebTransaction/*", NR_SPAN_GENERIC, NULL, 1000, + 9000); + SPAN_EVENT_COMPARE(evt_a, "A", NR_SPAN_MESSAGE, evt_root, 2000, 5000); + SPAN_EVENT_COMPARE_MESSAGE(evt_a, "queue_name", "aws_sqs", "localhost"); + + /* Clean up */ + nr_segment_children_deinit(&root.children); + nr_segment_destroy_fields(&root); + + nr_segment_destroy_fields(&A); + + cleanup_mock_txn(&txn); + nr_string_pool_destroy(&segment_names); + + nr_buffer_destroy(&buf); + nr_vector_destroy(&span_events); +} + +static void test_json_print_segments_datastore_external_message(void) { bool rv; nrbuf_t* buf; nr_vector_t* span_events; @@ -972,12 +1081,11 @@ static void test_json_print_segments_datastore_external(void) { C.typed_attributes->external.transaction_guid = nr_strdup("guid"); C.typed_attributes->external.status = 200; - D.type = NR_SEGMENT_DATASTORE; + D.type = NR_SEGMENT_MESSAGE; D.attributes = NULL; D.typed_attributes = nr_zalloc(sizeof(nr_segment_typed_attributes_t)); - D.typed_attributes->datastore.sql = nr_strdup("SELECT pass"); - D.typed_attributes->datastore.instance.host = nr_strdup("localhost"); - D.typed_attributes->datastore.instance.database_name = nr_strdup("db"); + D.typed_attributes->message.destination_name = nr_strdup("queue_name"); + D.typed_attributes->message.messaging_system = nr_strdup("aws_sqs"); /* * Test : Normal operation @@ -999,9 +1107,8 @@ static void test_json_print_segments_datastore_external(void) { "\"transaction_guid\":\"guid\"," "\"status\":200},[]]," "[5,6,\"`4\"," - "{\"host\":\"localhost\"," - "\"database_name\":\"db\"," - "\"sql\":\"SELECT pass\"},[]]]]]]"); + "{\"destination_name\":\"queue_name\"," + "\"messaging_system\":\"aws_sqs\"},[]]]]]]"); tlib_pass_if_uint_equal("span event size", nr_vector_size(span_events), 5); @@ -1019,9 +1126,8 @@ static void test_json_print_segments_datastore_external(void) { "localhost:3308"); SPAN_EVENT_COMPARE(evt_c, "C", NR_SPAN_HTTP, evt_a, 5000, 1000); SPAN_EVENT_COMPARE_EXTERNAL(evt_c, "example.com", "GET", "curl", 200); - SPAN_EVENT_COMPARE(evt_d, "D", NR_SPAN_DATASTORE, evt_a, 6000, 1000); - SPAN_EVENT_COMPARE_DATASTORE(evt_d, "localhost", "db", "SELECT pass", - "localhost:unknown"); + SPAN_EVENT_COMPARE(evt_d, "D", NR_SPAN_MESSAGE, evt_a, 6000, 1000); + SPAN_EVENT_COMPARE_MESSAGE(evt_d, "queue_name", "aws_sqs", NULL); /* Clean up */ nr_segment_children_deinit(&root.children); @@ -1146,8 +1252,8 @@ static void test_json_print_segments_async_basic(void) { * * These diagrams all follow the same pattern: time is shown in seconds on * the first row, followed by the ROOT node, and then individual contexts - * with their nodes. The "main" context indicates that no async_context will - * be attached to nodes in that context. + * with their nodes. The "main" context indicates that no async_context + * will be attached to nodes in that context. * * time (s) 0 1 2 3 4 5 6 7 8 9 10 * |------------------- ROOT -------------------| @@ -1245,8 +1351,8 @@ static void test_json_print_segments_async_multi_child(void) { /* * Multiple children test: main context lasts the same timespan as ROOT, and - * spawns one child context with three nodes for part of its run time, one of - * which has a duplicated name. + * spawns one child context with three nodes for part of its run time, one + * of which has a duplicated name. * * time (s) 0 1 2 3 4 5 6 7 8 9 10 * |------------------- ROOT -------------------| @@ -1955,7 +2061,8 @@ static void test_json_print_segments_with_sampling_cousin_parent(void) { rv = nr_segment_traces_json_print_segments(buf, span_events, set, set, &txn, &root, segment_names); tlib_pass_if_bool_equal( - "Printing JSON for a sampled cousin parent tree of segments must succeed", + "Printing JSON for a sampled cousin parent tree of segments must " + "succeed", true, rv); test_buffer_contents("Cousin Parent", buf, "[0,14,\"`0\",{},[[1,5,\"`1\",{},[[1,3,\"`2\",{},[]]]],[" @@ -2269,7 +2376,8 @@ static void test_json_print_segments_with_sampling_genghis_khan(void) { rv = nr_segment_traces_json_print_segments(buf, span_events, set, set, &txn, &root, segment_names); tlib_pass_if_bool_equal( - "Printing JSON for a genghis khan sampled tree of segments must succeed", + "Printing JSON for a genghis khan sampled tree of segments must " + "succeed", true, rv); test_buffer_contents("genghis khan", buf, "[0,9,\"`0\",{},[[1,6,\"`1\",{},[]],[3,4,\"`2\",{},[]],[" @@ -2470,7 +2578,8 @@ static void test_trace_create_data_bad_parameters(void) { agent_attributes, user_attributes, intrinsics, true, false); tlib_pass_if_null( - "A transaction with more than NR_MAX_SEGMENTS segments must not succeed " + "A transaction with more than NR_MAX_SEGMENTS segments must not " + "succeed " "in creating " "a trace", metadata.out->trace_json); @@ -2786,9 +2895,10 @@ void test_main(void* p NRUNUSED) { test_json_print_segments_hanoi(); test_json_print_segments_three_siblings(); test_json_print_segments_two_generations(); - test_json_print_segments_datastore_external(); + test_json_print_segments_datastore_external_message(); test_json_print_segments_datastore_params(); test_json_print_segments_external_async_user_attrs(); + test_json_print_segments_message_attributes(); test_json_print_segments_async_basic(); test_json_print_segments_async_multi_child(); diff --git a/axiom/tests/test_span_event.c b/axiom/tests/test_span_event.c index 85ca788b9..a00700a6b 100644 --- a/axiom/tests/test_span_event.c +++ b/axiom/tests/test_span_event.c @@ -256,6 +256,55 @@ static void test_span_event_category(void) { tlib_pass_if_str_equal("Category should be the one we set - http", "http", nr_span_event_get_category(event)); + nr_span_event_set_category(event, NR_SPAN_MESSAGE); + tlib_pass_if_str_equal("Category should be the one we set - message", + "message", nr_span_event_get_category(event)); + + nr_span_event_destroy(&event); +} + +static void test_span_event_spankind(void) { + nr_span_event_t* event = nr_span_event_create(); + + // Test : the default is NULL (spankind must be explicitly set) + tlib_pass_if_str_equal( + "When not explicitly set, The default spankind is NULL", NULL, + nr_span_event_get_spankind(event)); + + // Test : A null event returns NULL + tlib_pass_if_null("nr_span_event_get_spankind(NULL) returns NULL", + nr_span_event_get_spankind(NULL)); + + // Test : passing a NULL event should not crash + nr_span_event_set_spankind(NULL, NR_SPANKIND_PRODUCER); + + // Test : invalid spankind + nr_span_event_set_spankind(event, 255); + tlib_pass_if_str_equal( + "Invalid spankind value doesn't crash and sets spankind to none (NULL)", + NULL, nr_span_event_get_spankind(event)); + + // Test : setting the spankind back and forth + nr_span_event_set_spankind(event, NR_SPANKIND_NO_SPANKIND); + tlib_pass_if_str_equal( + "Spankind should be the one we set - no spankind (NULL)", NULL, + nr_span_event_get_spankind(event)); + + // Test : setting the spankind back and forth + nr_span_event_set_spankind(event, NR_SPANKIND_PRODUCER); + tlib_pass_if_str_equal("Spankind should be the one we set - producer", + "producer", nr_span_event_get_spankind(event)); + + // Test : setting the spankind back and forth + nr_span_event_set_spankind(event, NR_SPANKIND_CLIENT); + tlib_pass_if_str_equal("Spankind should be the one we set - client", "client", + nr_span_event_get_spankind(event)); + + // Test : setting the spankind back and forth + nr_span_event_set_spankind(event, NR_SPANKIND_CONSUMER); + tlib_pass_if_str_equal("Spankind should be the one we set - consumer", + "consumer", nr_span_event_get_spankind(event)); + nr_span_event_destroy(&event); } @@ -434,6 +483,119 @@ static void test_span_events_extern_get_and_set(void) { nr_span_event_destroy(&span); } +static void test_span_event_message_get_and_set(void) { + nr_span_event_t* event = nr_span_event_create(); + + // Test : that is does not crash when we give the setter a NULL pointer + nr_span_event_set_message(NULL, NR_SPAN_MESSAGE_DESTINATION_NAME, "wallaby"); + tlib_pass_if_null( + "the destination name should still be NULL", + nr_span_event_get_message(event, NR_SPAN_MESSAGE_DESTINATION_NAME)); + nr_span_event_set_message(event, NR_SPAN_MESSAGE_DESTINATION_NAME, NULL); + tlib_pass_if_null( + "given a NULL value we should get a NULL", + nr_span_event_get_message(event, NR_SPAN_MESSAGE_DESTINATION_NAME)); + + // Test : the getter should not crash when we send it an event with a NULL + // component + tlib_pass_if_null( + "NULL event -> NULL component", + nr_span_event_get_message(NULL, NR_SPAN_MESSAGE_DESTINATION_NAME)); + + // Test : send getter invalid range + tlib_pass_if_null("invalid range sent to nr_span_event_get_message", + nr_span_event_get_message(event, 54321)); + + // Test: the ulong getter should return 0 (unset) for any string values passed + // in + nr_span_event_set_message(event, NR_SPAN_MESSAGE_DESTINATION_NAME, "chicken"); + tlib_pass_if_uint_equal( + "nr_span_event_get_message_ulong should return 0(unset) if given the " + "enum for a string value", + 0, + nr_span_event_get_message_ulong(event, NR_SPAN_MESSAGE_DESTINATION_NAME)); + + // Test: the string getter should return NULL if given the enum for a + // non-string value + nr_span_event_set_message_ulong(event, NR_SPAN_MESSAGE_SERVER_PORT, 1234); + tlib_pass_if_null( + "nr_span_event_get_message should return NULL if given the enum for a " + "non-string value", + nr_span_event_get_message(event, NR_SPAN_MESSAGE_SERVER_PORT)); + + // Test : setting the destination name back and forth behaves as expected + nr_span_event_set_message(event, NR_SPAN_MESSAGE_DESTINATION_NAME, "chicken"); + tlib_pass_if_str_equal( + "should be the destination name we set first", "chicken", + nr_span_event_get_message(event, NR_SPAN_MESSAGE_DESTINATION_NAME)); + nr_span_event_set_message(event, NR_SPAN_MESSAGE_DESTINATION_NAME, "oracle"); + tlib_pass_if_str_equal( + "should be the destination name we set second", "oracle", + nr_span_event_get_message(event, NR_SPAN_MESSAGE_DESTINATION_NAME)); + + // Test : setting the messaging system back and forth behaves as expected + nr_span_event_set_message(event, NR_SPAN_MESSAGE_MESSAGING_SYSTEM, "chicken"); + tlib_pass_if_str_equal( + "should be the messaging system we set first", "chicken", + nr_span_event_get_message(event, NR_SPAN_MESSAGE_MESSAGING_SYSTEM)); + nr_span_event_set_message(event, NR_SPAN_MESSAGE_MESSAGING_SYSTEM, "oracle"); + tlib_pass_if_str_equal( + "should be the messaging system we set second", "oracle", + nr_span_event_get_message(event, NR_SPAN_MESSAGE_MESSAGING_SYSTEM)); + + // Test : setting the server address back and forth behaves as expected + nr_span_event_set_message(event, NR_SPAN_MESSAGE_SERVER_ADDRESS, "chicken"); + tlib_pass_if_str_equal( + "should be the server address we set first", "chicken", + nr_span_event_get_message(event, NR_SPAN_MESSAGE_SERVER_ADDRESS)); + nr_span_event_set_message(event, NR_SPAN_MESSAGE_SERVER_ADDRESS, "oracle"); + tlib_pass_if_str_equal( + "should be the server address we set second", "oracle", + nr_span_event_get_message(event, NR_SPAN_MESSAGE_SERVER_ADDRESS)); + + // Test : setting the destination pubishing name back and forth behaves as + // expected + nr_span_event_set_message( + event, NR_SPAN_MESSAGE_MESSAGING_DESTINATION_PUBLISH_NAME, "chicken"); + tlib_pass_if_str_equal( + "should be the destination publish name we set first", "chicken", + nr_span_event_get_message( + event, NR_SPAN_MESSAGE_MESSAGING_DESTINATION_PUBLISH_NAME)); + nr_span_event_set_message( + event, NR_SPAN_MESSAGE_MESSAGING_DESTINATION_PUBLISH_NAME, "oracle"); + tlib_pass_if_str_equal( + "should be the destination publish name we set second", "oracle", + nr_span_event_get_message( + event, NR_SPAN_MESSAGE_MESSAGING_DESTINATION_PUBLISH_NAME)); + + // Test : setting the destination routing key back and forth behaves as + // expected + nr_span_event_set_message( + event, NR_SPAN_MESSAGE_MESSAGING_DESTINATION_ROUTING_KEY, "chicken"); + tlib_pass_if_str_equal( + "should be the destination routing key we set first", "chicken", + nr_span_event_get_message( + event, NR_SPAN_MESSAGE_MESSAGING_DESTINATION_ROUTING_KEY)); + nr_span_event_set_message( + event, NR_SPAN_MESSAGE_MESSAGING_DESTINATION_ROUTING_KEY, "oracle"); + tlib_pass_if_str_equal( + "should be the destination routing key we set second", "oracle", + nr_span_event_get_message( + event, NR_SPAN_MESSAGE_MESSAGING_DESTINATION_ROUTING_KEY)); + + // Test : setting the server port back and forth behaves as expected + nr_span_event_set_message_ulong(event, NR_SPAN_MESSAGE_SERVER_PORT, 1234); + tlib_pass_if_ulong_equal( + "should be the server port we set first", 1234, + nr_span_event_get_message_ulong(event, NR_SPAN_MESSAGE_SERVER_PORT)); + nr_span_event_set_message_ulong(event, NR_SPAN_MESSAGE_SERVER_PORT, 4321); + tlib_pass_if_ulong_equal( + "should be the server port we set first", 4321, + nr_span_event_get_message_ulong(event, NR_SPAN_MESSAGE_SERVER_PORT)); + + nr_span_event_destroy(&event); +} + static void test_span_event_error(void) { nr_span_event_t* event = nr_span_event_create(); @@ -614,10 +776,12 @@ void test_main(void* p NRUNUSED) { test_span_event_name(); test_span_event_transaction_name(); test_span_event_category(); + test_span_event_spankind(); test_span_event_timestamp(); test_span_event_duration(); test_span_event_datastore_string_get_and_set(); test_span_events_extern_get_and_set(); + test_span_event_message_get_and_set(); test_span_event_error(); test_span_event_set_attribute_user(); test_span_event_txn_parent_attributes(); diff --git a/axiom/tests/test_txn.c b/axiom/tests/test_txn.c index 905189c2a..5dc68deae 100644 --- a/axiom/tests/test_txn.c +++ b/axiom/tests/test_txn.c @@ -1705,6 +1705,7 @@ static void test_begin(void) { opts->max_segments = 0; opts->span_queue_batch_size = 1000; opts->span_queue_batch_timeout = 1 * NR_TIME_DIVISOR; + opts->distributed_tracing_pad_trace_id = false; app->rnd = nr_random_create(); nr_random_seed(app->rnd, 345345); @@ -2005,7 +2006,7 @@ static nrtxn_t* create_full_txn_and_reset(nrapp_t* app) { nr_segment_t* seg = nr_segment_start(txn, NULL, NULL); seg->start_time = 5 * NR_TIME_DIVISOR; seg->stop_time = 6 * NR_TIME_DIVISOR; - seg->type = NR_SEGMENT_DATASTORE; + seg->type = NR_SEGMENT_MESSAGE; seg->typed_attributes = nr_zalloc(sizeof(nr_segment_typed_attributes_t)); nr_segment_end(&seg); } @@ -2961,6 +2962,7 @@ static void test_create_rollup_metrics(void) { txn.datastore_products = nr_string_pool_create(); nrm_force_add(txn.unscoped_metrics, "Datastore/all", 4 * NR_TIME_DIVISOR); nrm_force_add(txn.unscoped_metrics, "External/all", 1 * NR_TIME_DIVISOR); + nrm_force_add(txn.unscoped_metrics, "MessageBroker/all", 1 * NR_TIME_DIVISOR); nrm_force_add(txn.unscoped_metrics, "Datastore/MongoDB/all", 2 * NR_TIME_DIVISOR); nrm_force_add(txn.unscoped_metrics, "Datastore/SQLite/all", @@ -2976,6 +2978,9 @@ static void test_create_rollup_metrics(void) { "{\"name\":\"External\\/" "all\",\"data\":[1,1.00000,1.00000,1.00000,1.00000,1." "00000],\"forced\":true}," + "{\"name\":\"MessageBroker\\/" + "all\",\"data\":[1,1.00000,1.00000,1.00000,1.00000,1." + "00000],\"forced\":true}," "{\"name\":\"Datastore\\/MongoDB\\/" "all\",\"data\":[1,2.00000,2.00000,2.00000,2.00000,4." "00000],\"forced\":true}," @@ -2988,6 +2993,9 @@ static void test_create_rollup_metrics(void) { "{\"name\":\"External\\/" "allOther\",\"data\":[1,1.00000,1.00000,1.00000,1." "00000,1.00000],\"forced\":true}," + "{\"name\":\"MessageBroker\\/" + "allOther\",\"data\":[1,1.00000,1.00000,1.00000,1." + "00000,1.00000],\"forced\":true}," "{\"name\":\"Datastore\\/MongoDB\\/" "allOther\",\"data\":[1,2.00000,2.00000,2.00000,2." "00000,4.00000],\"forced\":true}," @@ -3909,6 +3917,7 @@ static void test_error_to_event(void) { nrm_add(txn.unscoped_metrics, "Datastore/all", 1 * NR_TIME_DIVISOR); nrm_add(txn.unscoped_metrics, "External/all", 2 * NR_TIME_DIVISOR); + nrm_add(txn.unscoped_metrics, "MessageBroker/all", 1 * NR_TIME_DIVISOR); nrm_add(txn.unscoped_metrics, "WebFrontend/QueueTime", 3 * NR_TIME_DIVISOR); event = nr_error_to_event(&txn); @@ -3925,8 +3934,10 @@ static void test_error_to_event(void) { "\"queueDuration\":3.00000," "\"externalDuration\":2.00000," "\"databaseDuration\":1.00000," + "\"messageDuration\":1.00000," "\"databaseCallCount\":1," "\"externalCallCount\":1," + "\"messageCallCount\":1," "\"nr.transactionGuid\":\"abcd\"," "\"guid\":\"abcd\"" "}," @@ -3950,8 +3961,10 @@ static void test_error_to_event(void) { "\"queueDuration\":3.00000," "\"externalDuration\":2.00000," "\"databaseDuration\":1.00000," + "\"messageDuration\":1.00000," "\"databaseCallCount\":1," "\"externalCallCount\":1," + "\"messageCallCount\":1," "\"nr.transactionGuid\":\"abcd\"," "\"guid\":\"abcd\"," "\"nr.referringTransactionGuid\":\"foo_guid\"," @@ -4040,6 +4053,7 @@ static void test_create_event(void) { nrm_add(txn.unscoped_metrics, "Datastore/all", 1 * NR_TIME_DIVISOR); nrm_add(txn.unscoped_metrics, "External/all", 2 * NR_TIME_DIVISOR); + nrm_add(txn.unscoped_metrics, "MessageBroker/all", 2 * NR_TIME_DIVISOR); nrm_add(txn.unscoped_metrics, "WebFrontend/QueueTime", 3 * NR_TIME_DIVISOR); event = nr_txn_to_event(&txn); @@ -4055,8 +4069,11 @@ static void test_create_event(void) { "\"nr.apdexPerfZone\":\"F\"," "\"queueDuration\":3.00000," "\"externalDuration\":2.00000," + "\"externalCallCount\":1," "\"databaseDuration\":1.00000," "\"databaseCallCount\":1," + "\"messageDuration\":2.00000," + "\"messageCallCount\":1," "\"error\":false" "}," "{\"user_long\":1}," @@ -4077,8 +4094,11 @@ static void test_create_event(void) { "\"totalTime\":0.98700," "\"queueDuration\":3.00000," "\"externalDuration\":2.00000," + "\"externalCallCount\":1," "\"databaseDuration\":1.00000," "\"databaseCallCount\":1," + "\"messageDuration\":2.00000," + "\"messageCallCount\":1," "\"error\":false" "}," "{\"user_long\":1}," @@ -4102,8 +4122,11 @@ static void test_create_event(void) { "\"nr.apdexPerfZone\":\"F\"," "\"queueDuration\":3.00000," "\"externalDuration\":2.00000," + "\"externalCallCount\":1," "\"databaseDuration\":1.00000," "\"databaseCallCount\":1," + "\"messageDuration\":2.00000," + "\"messageCallCount\":1," "\"error\":false" "}," "{\"user_long\":1}," @@ -4125,8 +4148,11 @@ static void test_create_event(void) { "\"nr.apdexPerfZone\":\"F\"," "\"queueDuration\":3.00000," "\"externalDuration\":2.00000," + "\"externalCallCount\":1," "\"databaseDuration\":1.00000," "\"databaseCallCount\":1," + "\"messageDuration\":2.00000," + "\"messageCallCount\":1," "\"error\":false" "}," "{\"user_long\":1}," @@ -4147,8 +4173,11 @@ static void test_create_event(void) { "\"nr.apdexPerfZone\":\"F\"," "\"queueDuration\":3.00000," "\"externalDuration\":2.00000," + "\"externalCallCount\":1," "\"databaseDuration\":1.00000," "\"databaseCallCount\":1," + "\"messageDuration\":2.00000," + "\"messageCallCount\":1," "\"error\":false" "}," "{\"user_long\":1}," @@ -5940,6 +5969,10 @@ static void test_create_w3c_traceparent_header(void) { nr_memset(&txn, 0, sizeof(nrtxn_t)); txn.options.span_events_enabled = true; + // default value for padding can be used for this test, because other + // test (test_distributed_trace_create_trace_parent_header) already + // covers using trace_id of different lengths when w3c header is created. + txn.options.distributed_tracing_pad_trace_id = false; txn.status.recording = true; txn.rnd = nr_random_create(); txn.status.recording = 1; @@ -5969,7 +6002,9 @@ static void test_create_w3c_traceparent_header(void) { /* * Test : valid string random guid */ - nr_distributed_trace_set_trace_id(txn.distributed_trace, "meatballs!"); + nr_distributed_trace_set_trace_id( + txn.distributed_trace, "meatballs!", + txn.options.distributed_tracing_pad_trace_id); actual = nr_txn_create_w3c_traceparent_header(&txn, segment); expected = "00-0000000000000000000000meatballs!-"; tlib_pass_if_not_null("random guid", nr_strstr(actual, expected)); @@ -6280,6 +6315,10 @@ static void test_txn_accept_distributed_trace_payload_w3c(void) { txn.app_connect_reply = nro_new_hash(); txn.unscoped_metrics = nrm_table_create(0); nro_set_hash_string(txn.app_connect_reply, "trusted_account_key", "123"); + // default value for padding can be used for this test, because other + // test (test_distributed_trace_create_trace_parent_header) already + // covers using trace_id of different lengths when w3c header is created. + txn.options.distributed_tracing_pad_trace_id = false; #define TEST_TXN_ACCEPT_DT_PAYLOAD_RESET \ txn.distributed_trace->inbound.set = 0; \ @@ -6725,7 +6764,9 @@ static void test_txn_accept_distributed_trace_payload_w3c(void) { TEST_TXN_ACCEPT_DT_PAYLOAD_RESET nr_distributed_trace_destroy(&txn.distributed_trace); txn.distributed_trace = nr_distributed_trace_create(); - nr_distributed_trace_set_trace_id(txn.distributed_trace, "35ff77"); + nr_distributed_trace_set_trace_id( + txn.distributed_trace, "35ff77", + txn.options.distributed_tracing_pad_trace_id); traceparent = nr_txn_create_w3c_traceparent_header(&txn, NULL); nr_free(traceparent); @@ -7525,6 +7566,7 @@ static void test_get_current_trace_id(void) { char* trace_id; nrtxn_t* txn; const char* txn_id; + const char* dt_trace_id; /* setup and start txn */ nr_memset(&app, 0, sizeof(app)); @@ -7540,7 +7582,7 @@ static void test_get_current_trace_id(void) { nr_txn_get_current_trace_id(NULL)); /* - * Test : Correct trace id + * Test : trace id == txn_id with default pad_trace_id setting - no padding */ txn_id = nr_txn_get_guid(txn); trace_id = nr_txn_get_current_trace_id(txn); @@ -7564,6 +7606,28 @@ static void test_get_current_trace_id(void) { nr_txn_get_current_trace_id(txn)); nr_txn_destroy(&txn); + + /* setup and start txn */ + nr_memset(&app, 0, sizeof(app)); + nr_memset(&opts, 0, sizeof(opts)); + app.state = NR_APP_OK; + opts.distributed_tracing_enabled = 1; + opts.distributed_tracing_pad_trace_id = true; + txn = nr_txn_begin(&app, &opts, NULL); + /* + * Test : trace id != txn_id with trace_id padding enabled + */ + txn_id = nr_txn_get_guid(txn); + trace_id = nr_txn_get_current_trace_id(txn); + dt_trace_id = nr_distributed_trace_get_trace_id(txn->distributed_trace); + tlib_fail_if_null("txn id", txn_id); + tlib_fail_if_str_equal("txn_id != trace_id", txn_id, trace_id); + tlib_pass_if_str_equal("txn_id = ltrim(trace_id)", txn_id, + trace_id + (nr_strlen(trace_id) - NR_GUID_SIZE)); + tlib_pass_if_str_equal("trace_id = dt_trace_id", trace_id, dt_trace_id); + nr_free(trace_id); + + nr_txn_destroy(&txn); } static void test_get_current_span_id(void) { @@ -8018,7 +8082,8 @@ static void test_segment_record_error(void) { /* 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\"]"); + "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", @@ -8027,7 +8092,8 @@ static void test_segment_record_error(void) { 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\"]"); + 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); @@ -8561,6 +8627,8 @@ static void test_nr_txn_add_php_package(void) { char* package_name4 = "Wordpress"; char* package_version4 = PHP_PACKAGE_VERSION_UNKNOWN; nrtxn_t* txn = new_txn(0); + nr_php_package_t* p1 = NULL; + nr_php_package_t* p2 = NULL; /* * NULL parameters: ensure it does not crash @@ -8585,6 +8653,130 @@ static void test_nr_txn_add_php_package(void) { nr_free(json); nr_txn_destroy(&txn); + + txn = new_txn(0); + p1 = nr_txn_add_php_package(txn, package_name1, package_version1); + p2 = nr_txn_add_php_package(txn, package_name1, package_version2); + tlib_pass_if_ptr_equal( + "same package name, different version, add returns same pointer", p1, p2); + nr_txn_destroy(&txn); +} + +static void test_nr_txn_add_php_package_from_source(void) { + char* json; + char* package_name1 = "Laravel"; + char* package_version1 = "8.83.27"; + char* package_name2 = "Slim"; + char* package_version2 = "4.12.0"; + char* package_name3 = "Drupal"; + char* package_version3 = NULL; + char* package_name4 = "Wordpress"; + char* package_version4 = PHP_PACKAGE_VERSION_UNKNOWN; + nrtxn_t* txn = new_txn(0); + nr_php_package_t* p1 = NULL; + nr_php_package_t* p2 = NULL; + + /* + * NULL parameters: ensure it does not crash + */ + nr_txn_add_php_package_from_source(NULL, NULL, NULL, 0); + nr_txn_add_php_package_from_source(NULL, package_name1, package_version1, 0); + nr_txn_add_php_package_from_source(txn, NULL, package_version1, 0); + nr_txn_add_php_package_from_source(txn, package_name1, NULL, 0); + + // Test: add php packages to transaction + nr_txn_add_php_package_from_source(txn, package_name1, package_version1, + NR_PHP_PACKAGE_SOURCE_COMPOSER); + nr_txn_add_php_package_from_source(txn, package_name2, package_version2, + NR_PHP_PACKAGE_SOURCE_LEGACY); + nr_txn_add_php_package_from_source(txn, package_name3, package_version3, + NR_PHP_PACKAGE_SOURCE_COMPOSER); + nr_txn_add_php_package_from_source(txn, package_name4, package_version4, + NR_PHP_PACKAGE_SOURCE_LEGACY); + json = nr_php_packages_to_json(txn->php_packages); + + tlib_pass_if_str_equal("correct json", + "[[\"Laravel\",\"8.83.27\",{}]," + "[\"Drupal\",\" \",{}],[\"Wordpress\",\" \",{}]," + "[\"Slim\",\"4.12.0\",{}]]", + json); + + nr_free(json); + nr_txn_destroy(&txn); + + txn = new_txn(0); + p1 = nr_txn_add_php_package_from_source(txn, package_name1, package_version1, + NR_PHP_PACKAGE_SOURCE_COMPOSER); + p2 = nr_txn_add_php_package_from_source(txn, package_name1, package_version2, + NR_PHP_PACKAGE_SOURCE_COMPOSER); + tlib_pass_if_ptr_equal( + "same package name, different version, add returns same pointer", p1, p2); + nr_txn_destroy(&txn); + + txn = new_txn(0); + p1 = nr_txn_add_php_package_from_source(txn, package_name1, package_version1, + NR_PHP_PACKAGE_SOURCE_LEGACY); + p2 = nr_txn_add_php_package_from_source(txn, package_name1, package_version2, + NR_PHP_PACKAGE_SOURCE_COMPOSER); + tlib_pass_if_ptr_equal( + "same package name, different version, add returns different pointer", p1, + p2); + tlib_pass_if_str_equal("composer version used", package_version2, + p2->package_version); + + nr_txn_destroy(&txn); +} + +static void test_nr_txn_suggest_package_supportability_metric(void) { + char* json; + char* package_name1 = "Laravel"; + char* package_version1 = "8.83.27"; + char* package_name2 = "Slim"; + char* package_version2 = "4.12.0"; + char* package_name3 = "Drupal"; + char* package_version3 = NULL; + char* package_name4 = "Wordpress"; + char* package_version4 = PHP_PACKAGE_VERSION_UNKNOWN; + nrtxn_t* txn = new_txn(0); + nr_php_package_t* p1 = NULL; + nr_php_package_t* p2 = NULL; + + /* + * NULL parameters: ensure it does not crash + */ + nr_txn_suggest_package_supportability_metric(NULL, NULL, NULL); + nr_txn_suggest_package_supportability_metric(NULL, package_name1, + package_version1); + nr_txn_suggest_package_supportability_metric(txn, NULL, package_version1); + nr_txn_suggest_package_supportability_metric(txn, package_name1, NULL); + + // Test: add php packages to transaction + nr_txn_suggest_package_supportability_metric(txn, package_name1, + package_version1); + nr_txn_suggest_package_supportability_metric(txn, package_name2, + package_version2); + nr_txn_suggest_package_supportability_metric(txn, package_name3, + package_version3); + nr_txn_suggest_package_supportability_metric(txn, package_name4, + package_version4); + json = nr_php_packages_to_json( + txn->php_package_major_version_metrics_suggestions); + + tlib_pass_if_str_equal("correct json", + "[[\"Laravel\",\"8.83.27\",{}]," + "[\"Drupal\",\" \",{}],[\"Wordpress\",\" \",{}]," + "[\"Slim\",\"4.12.0\",{}]]", + json); + + nr_free(json); + nr_txn_destroy(&txn); + + txn = new_txn(0); + p1 = nr_txn_add_php_package(txn, package_name1, package_version1); + p2 = nr_txn_add_php_package(txn, package_name1, package_version2); + tlib_pass_if_ptr_equal( + "same package name, different version, add returns same pointer", p1, p2); + nr_txn_destroy(&txn); } tlib_parallel_info_t parallel_info @@ -8689,4 +8881,6 @@ void test_main(void* p NRUNUSED) { test_record_log_event(); test_txn_log_configuration(); test_nr_txn_add_php_package(); + test_nr_txn_add_php_package_from_source(); + test_nr_txn_suggest_package_supportability_metric(); } diff --git a/daemon/cmd/daemon/main.go b/daemon/cmd/daemon/main.go index e48dd2f7d..d3d8f977b 100644 --- a/daemon/cmd/daemon/main.go +++ b/daemon/cmd/daemon/main.go @@ -371,7 +371,7 @@ func main() { proxy = false } else { log.Debugf("ARGV[%d]: %s", i, os.Args[i]) - if ("--proxy" == os.Args[i]) { + if "--proxy" == os.Args[i] { proxy = true } } @@ -570,10 +570,9 @@ func (env *Environment) Set(key, value string) { // initLog opens the daemon log based on the current configuration settings. // If no log has been specified, initLog will try the following standard -// locations. -// -// /var/log/newrelic/newrelic-daemon.log -// /var/log/newrelic-daemon.log +// locations: +// - /var/log/newrelic/newrelic-daemon.log +// - /var/log/newrelic-daemon.log // // If no suitable location can be found, a generic error is returned. func initLog(cfg *Config) error { diff --git a/daemon/cmd/daemon/worker.go b/daemon/cmd/daemon/worker.go index 25ec5a2ca..eade69f46 100644 --- a/daemon/cmd/daemon/worker.go +++ b/daemon/cmd/daemon/worker.go @@ -124,6 +124,7 @@ func runWorker(cfg *Config) { hasProgenitor := !(cfg.Foreground || cfg.WatchdogForeground) ctx, cancel := context.WithCancel(context.Background()) + defer cancel() // Ensure that the context is always cancelled when the worker exits, not only when signal is caught. select { case <-listenAndServe(ctx, cfg.BindAddr, errorChan, p, hasProgenitor): @@ -140,7 +141,8 @@ func runWorker(cfg *Config) { case caught := <-signalChan: // Close the listener before sending remaining data. This ensures that the socket // connection is closed as soon as possible and other processes can start listening - // the socket while remaining data is sent. + // the socket while remaining data is sent. Earlier defer cancel() will be a no-op + // because cancel() is called here explicitly. cancel() log.Infof("worker received signal %d - sending remaining data", caught) p.CleanExit() diff --git a/daemon/cmd/integration_runner/main.go b/daemon/cmd/integration_runner/main.go index 40f472af4..f1a65a402 100644 --- a/daemon/cmd/integration_runner/main.go +++ b/daemon/cmd/integration_runner/main.go @@ -376,6 +376,14 @@ func main() { // Env vars common to all tests. ctx.Env["EXTERNAL_HOST"] = externalHost + ctx.Env["PHP_VERSION"] = integration.GetPHPVersion() + + agent_extension, ok := ctx.Settings["extension"] + if !ok { + agent_extension = "newrelic.so" + } + ctx.Env["AGENT_VERSION"] = integration.GetAgentVersion(agent_extension) + handler, err := startDaemon("unix", *flagPort, flagSecurityToken.String(), flagSecuityPolicies.String()) if err != nil { fmt.Fprintln(os.Stderr, err) diff --git a/daemon/cmd/stressor/main.go b/daemon/cmd/stressor/main.go index 20616bb4c..0782dc9d0 100644 --- a/daemon/cmd/stressor/main.go +++ b/daemon/cmd/stressor/main.go @@ -122,8 +122,8 @@ const DaemonSampleRate = 3 * time.Second // Application Behavior Constants // In order to roughly mimic the PHP agent, these should settings match -// NR_APP_UNKNOWN_QUERY_BACKOFF_LIMIT_SECONDS -// NR_APP_REFRESH_QUERY_PERIOD_SECONDS +// - NR_APP_UNKNOWN_QUERY_BACKOFF_LIMIT_SECONDS +// - NR_APP_REFRESH_QUERY_PERIOD_SECONDS const ( connectedAppInfoPeriod = 20 * time.Second unconnectedAppInfoPeriod = 5 * time.Second diff --git a/daemon/go.mod b/daemon/go.mod index 41f8c79ed..b76b8360f 100644 --- a/daemon/go.mod +++ b/daemon/go.mod @@ -1,18 +1,19 @@ module github.com/newrelic/newrelic-php-agent/daemon go 1.21 -toolchain go1.22.3 + +toolchain go1.23.6 require ( github.com/golang/protobuf v1.5.3 github.com/google/flatbuffers v23.5.26+incompatible - golang.org/x/net v0.23.0 + golang.org/x/net v0.33.0 google.golang.org/grpc v1.61.0 google.golang.org/protobuf v1.33.0 ) require ( - golang.org/x/sys v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect ) diff --git a/daemon/go.sum b/daemon/go.sum index f7f322d87..8ede3e252 100644 --- a/daemon/go.sum +++ b/daemon/go.sum @@ -6,12 +6,12 @@ github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZat github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14= google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA= diff --git a/daemon/internal/flatbuffersdata/data_test.go b/daemon/internal/flatbuffersdata/data_test.go index b2778f1e4..a9a3a9354 100644 --- a/daemon/internal/flatbuffersdata/data_test.go +++ b/daemon/internal/flatbuffersdata/data_test.go @@ -150,15 +150,15 @@ func TestFlatbuffersTxnData(t *testing.T) { // compilation to handle this rather than the flakier runtime.Version() // string. expect := `[` + - `{"name":"Supportability/TxnData/CustomEvents","forced":true,"data":[1,3,0,3,3,9]},` + - `{"name":"Supportability/TxnData/Metrics","forced":true,"data":[1,2,0,2,2,4]},` + - `{"name":"Supportability/TxnData/Size","forced":true,"data":[1,1408,0,1408,1408,1982464]},` + - `{"name":"Supportability/TxnData/SlowSQL","forced":true,"data":[1,1,0,1,1,1]},` + - `{"name":"Supportability/TxnData/TraceSize","forced":true,"data":[1,2,0,2,2,4]},` + - `{"name":"forced","forced":true,"data":[6,5,4,3,2,1]},` + - `{"name":"scoped","forced":false,"data":[1,2,3,4,5,6]},` + - `{"name":"scoped","forced":false,"data":[1,2,3,4,5,6]}` + - `]` + `{"name":"Supportability/TxnData/CustomEvents","forced":true,"data":[1,3,0,3,3,9]},` + + `{"name":"Supportability/TxnData/Metrics","forced":true,"data":[1,2,0,2,2,4]},` + + `{"name":"Supportability/TxnData/Size","forced":true,"data":[1,1408,0,1408,1408,1982464]},` + + `{"name":"Supportability/TxnData/SlowSQL","forced":true,"data":[1,1,0,1,1,1]},` + + `{"name":"Supportability/TxnData/TraceSize","forced":true,"data":[1,2,0,2,2,4]},` + + `{"name":"forced","forced":true,"data":[6,5,4,3,2,1]},` + + `{"name":"scoped","forced":false,"data":[1,2,3,4,5,6]},` + + `{"name":"scoped","forced":false,"data":[1,2,3,4,5,6]}` + + `]` if s != expect { t.Fatal(s, expect) diff --git a/daemon/internal/newrelic/collector/client.go b/daemon/internal/newrelic/collector/client.go index 6693e962b..7fb10326c 100644 --- a/daemon/internal/newrelic/collector/client.go +++ b/daemon/internal/newrelic/collector/client.go @@ -58,15 +58,15 @@ type RpmControls struct { // Agent Behavior Summary: // // on connect/preconnect: -// 410 means shutdown -// 200, 202 mean success (start run) -// all other response codes and errors mean try after backoff +// - 410 means shutdown +// - 200, 202 mean success (start run) +// - all other response codes and errors mean try after backoff // // on harvest: -// 410 means shutdown -// 401, 409 mean restart run -// 408, 429, 500, 503 mean save data for next harvest -// all other response codes and errors discard the data and continue the current harvest +// - 410 means shutdown +// - 401, 409 mean restart run +// - 408, 429, 500, 503 mean save data for next harvest +// - all other response codes and errors discard the data and continue the current harvest type RPMResponse struct { StatusCode int Body []byte @@ -97,7 +97,6 @@ func removeURLFromError(err error) error { ue.URL = "**REDACTED-URL**" } - return err } @@ -116,7 +115,7 @@ func (resp RPMResponse) IsDisconnect() bool { // IsRestartException indicates that the agent should restart. // 401 (License Exception) is considered a restart exception according to the spec, -// and is included here as such, however the PHP agent will not restart on a 401 and instead stop +// and is included here as such, however the PHP agent will not restart on a 401 and instead stop func (resp RPMResponse) IsRestartException() bool { return resp.StatusCode == 401 || resp.StatusCode == 409 } diff --git a/daemon/internal/newrelic/config/config.go b/daemon/internal/newrelic/config/config.go index 739a78d25..b34bf41b1 100644 --- a/daemon/internal/newrelic/config/config.go +++ b/daemon/internal/newrelic/config/config.go @@ -65,13 +65,13 @@ func NewDecoder(r io.Reader) *Decoder { // // Decode implements the following PEG. // -// INI = ((KEYWORD WS* '=' WS* VALUE) / COMMENT / WS)* -// KEYWORD = ALPHA (ALPHA / NUMERIC / '_' / '.')* -// VALUE = QUOTED / DQUOTED / RAW -// QUOTED = '\'' .* '\'' -// DQUOTED = '"' .* '"' -// RAW = .* EOL -// COMMENT = ('#' / ';') .* EOL +// INI = ((KEYWORD WS* '=' WS* VALUE) / COMMENT / WS)* +// KEYWORD = ALPHA (ALPHA / NUMERIC / '_' / '.')* +// VALUE = QUOTED / DQUOTED / RAW +// QUOTED = '\'' .* '\'' +// DQUOTED = '"' .* '"' +// RAW = .* EOL +// COMMENT = ('#' / ';') .* EOL func (d *Decoder) Decode(v interface{}) (err error) { val := reflect.ValueOf(v) if val.Kind() != reflect.Ptr { diff --git a/daemon/internal/newrelic/harvest_trigger.go b/daemon/internal/newrelic/harvest_trigger.go index 88cd9d109..3da02051d 100644 --- a/daemon/internal/newrelic/harvest_trigger.go +++ b/daemon/internal/newrelic/harvest_trigger.go @@ -65,10 +65,10 @@ func triggerBuilder(t HarvestType, duration time.Duration) HarvestTriggerFunc { // To create a group of goroutines that may be cancelled by sending a single // message on a single channel, this function: -// - Creates a cancel channel for the goroutine function f. -// - Starts the goroutine. -// - Returns the newly-created cancel channel so that it may be added to a -// broadcast group. +// - Creates a cancel channel for the goroutine function f. +// - Starts the goroutine. +// - Returns the newly-created cancel channel so that it may be added to a +// broadcast group. func startGroupMember(f HarvestTriggerFunc, trigger chan HarvestType) chan bool { cancel := make(chan bool) go f(trigger, cancel) @@ -144,10 +144,10 @@ func customTriggerBuilder(reply *ConnectReply) HarvestTriggerFunc { // This function returns the harvest trigger function that should be used for // this agent. In priority order: -// 1. Either it uses the ConnectReply to build custom triggers as specified by -// the New Relic server-side collector. -// 2. Or it creates a default harvest trigger, harvesting all data at the -// default period. +// 1. Either it uses the ConnectReply to build custom triggers as specified by +// the New Relic server-side collector. +// 2. Or it creates a default harvest trigger, harvesting all data at the +// default period. func getHarvestTrigger(key collector.LicenseKey, reply *ConnectReply) HarvestTriggerFunc { // Build a trigger from the server-side collector configuration. if reply.isHarvestAll() { diff --git a/daemon/internal/newrelic/integration/parse.go b/daemon/internal/newrelic/integration/parse.go index 5e0f57fc8..a033a33c1 100644 --- a/daemon/internal/newrelic/integration/parse.go +++ b/daemon/internal/newrelic/integration/parse.go @@ -228,7 +228,6 @@ func parsePHPModules(t *Test, content []byte) error { return nil } - func parseAnalyticEvents(test *Test, content []byte) error { test.analyticEvents = content return nil diff --git a/daemon/internal/newrelic/integration/php_packages.go b/daemon/internal/newrelic/integration/php_packages.go index 5151359d9..fc9a10a59 100644 --- a/daemon/internal/newrelic/integration/php_packages.go +++ b/daemon/internal/newrelic/integration/php_packages.go @@ -31,12 +31,13 @@ type PhpPackagesCollection struct { // PHP packages config describes how to collect the JSON for the packages installed // for the current test case type PhpPackagesConfiguration struct { - path string - command string - supportedListFile string - overrideVersionsFile string - expectedPackages []string - packageNameOnly []string + path string // + command string // command to run to detect packages + supportedListFile string // JSON file containing list of packages we expect agent to detect + overrideVersionsFile string // JSON file containing overrides for expected package versions + expectedPackages []string // manual override of packages we expect to detect + packageNameOnly []string // list of packages which only have a name because agent cannot determine the version + expectAllDetected bool // flag to indicate we expect all packages detected by the command "command" } // composer package JSON @@ -51,13 +52,14 @@ type ComposerJSON struct { Installed []ComposerPackage } -// // Given a JSON harvest payload, extract the PHP packages // -// Params 1 : JSON byte string containing update_loaded_modules endpoint data +// Params 1: +// - JSON byte string containing update_loaded_modules endpoint data // -// Returns : []PhpPackage with extracted package info, sorted by package name -// nil upon error processing JSON +// Returns: +// - []PhpPackage with extracted package info, sorted by package name +// - nil upon error processing JSON func GetPhpPackagesFromData(data []byte) ([]PhpPackage, error) { var pkgs []PhpPackage var x []interface{} @@ -158,16 +160,22 @@ func NewPhpPackagesCollection(path string, config []byte) (*PhpPackagesCollectio } } - if supportedOK && expectedOK { - return nil, fmt.Errorf("Improper EXPECT_PHP_PACKAGES config - cannot specify 'supported_packages' and 'expected packages' - got %+v", params) + // or "expect_all" which means we expect the agent to detect all the packages that the "command" option would detect + _, expectAllOK := params["expect_all"] + + if (supportedOK && expectedOK) || (supportedOK && expectAllOK) || (expectedOK && expectAllOK) { + return nil, fmt.Errorf("Improper EXPECT_PHP_PACKAGES config - must specify one of 'supported_packages', "+ + "'expected packages' or 'expect_all' - got %+v", params) } - if !supportedOK && !expectedOK { - return nil, fmt.Errorf("Improper EXPECT_PHP_PACKAGES config - must specify 'supported_packages' or 'expected packages' - got %+v", params) + if !supportedOK && !expectedOK && !expectAllOK { + return nil, fmt.Errorf("Improper EXPECT_PHP_PACKAGES config - must specify 'supported_packages' or 'expected packages' "+ + "or 'expect_all' - got %+v", params) } - if supportedOK && !commandOK { - return nil, fmt.Errorf("Improper EXPECT_PHP_PACKAGES config - must specify 'command' option with `supported_packages` - got %+v", params) + if (supportedOK || expectAllOK) && !commandOK { + return nil, fmt.Errorf("Improper EXPECT_PHP_PACKAGES config - must specify 'command' option with `supported_packages` / "+ + "'expect_all' - got %+v", params) } // optional option to specify which packages will only have a name because agent cannot determine the version @@ -208,7 +216,8 @@ func NewPhpPackagesCollection(path string, config []byte) (*PhpPackagesCollectio supportedListFile: supportedListFile, overrideVersionsFile: overrideVersionsFile, expectedPackages: expectedPackagesArr, - packageNameOnly: packageNameOnlyArr}, + packageNameOnly: packageNameOnlyArr, + expectAllDetected: expectAllOK}, } return p, nil @@ -279,11 +288,11 @@ func (pkgs *PhpPackagesCollection) OverrideVersionsFile() string { // Detects installed PHP packages // -// Returns : []PhpPackage with extracted package info, sorted by package name -// nil upon error processing JSON -// -// Notes : Currently only supports an application created with composer +// Returns: +// - []PhpPackage with extracted package info, sorted by package name +// - nil upon error processing JSON // +// Notes: Currently only supports an application created with composer func (pkgs *PhpPackagesCollection) GatherInstalledPackages() ([]PhpPackage, error) { var err error @@ -295,18 +304,27 @@ func (pkgs *PhpPackagesCollection) GatherInstalledPackages() ([]PhpPackage, erro var supported []string // get list of packages we expected the agent to detect - // this can be one of 2 scenarios: + // this can be one of 3 scenarios: // 1) test case used the "supported_packages" option which gives a JSON file which // lists all the packages the agent can detect // 2) test case used the "expected_packages" options which provides a comma separated // list of packages we expect the agent to detect + // 3) test case used the "expect_all" option which means we expect the agent to + // detect all the packages that the "command" option would detect + // + // Options #1 and #2 are mutually exclusive, and are intended for testing the legacy VM detection + // mechanism where the agent looks for "magic" files of a package and examinew internals of the + // package to determine its version. // - // Option #1 is preferable as it provides the most comprehensive view of what the agent can do. + // Option #1 is preferable when it available as it provides the most comprehensive view of what the agent can do. // // Option #2 is needed because some test cases do not exercise all the packages which are // installed and so the agent will not detect everything for that test case run which it could // theorectically detect if the test case used all the available packages installed. // + // Option #3 is used when testing the agent's ability to detect packages using the Composer API. In + // this case we expect the agent to detect the exact same packages as composer would detect. + // // Once the list of packages the agent is expected to detect is created it is used to filter // down the package list returned by running the "command" (usually composer) option for the // test case provided. @@ -317,8 +335,9 @@ func (pkgs *PhpPackagesCollection) GatherInstalledPackages() ([]PhpPackage, erro } } else if 0 < len(pkgs.config.expectedPackages) { supported = pkgs.config.expectedPackages - } else { - return nil, fmt.Errorf("Error determining expected packages - supported_packages and expected_packages are both empty") + } else if !pkgs.config.expectAllDetected { + return nil, fmt.Errorf("Error determining expected packages - supported_packages and expected_packages are both empty " + + "and expect_all is false") } splitCmd := strings.Split(pkgs.config.command, " ") @@ -338,7 +357,7 @@ func (pkgs *PhpPackagesCollection) GatherInstalledPackages() ([]PhpPackage, erro json.Unmarshal([]byte(out), &detected) for _, v := range detected.Installed { //fmt.Printf("composer detected %s %s\n", v.Name, v.Version) - if StringSliceContains(supported, v.Name) { + if pkgs.config.expectAllDetected || StringSliceContains(supported, v.Name) { var version string // remove any 'v' from front of version string @@ -370,6 +389,18 @@ func (pkgs *PhpPackagesCollection) GatherInstalledPackages() ([]PhpPackage, erro if 0 < len(version) { pkgs.packages = append(pkgs.packages, PhpPackage{"wordpress", version}) } + } else if 1 < len(splitCmd) && "composer-show.php" == splitCmd[1] { + lines := strings.Split(string(out), "\n") + version := "" + for _, line := range lines { + //fmt.Printf("line is |%s|\n", line) + splitLine := strings.Split(line, "=>") + if 2 == len(splitLine) { + name := strings.TrimSpace(splitLine[0]) + version = strings.TrimSpace(splitLine[1]) + pkgs.packages = append(pkgs.packages, PhpPackage{name, version}) + } + } } else { return nil, fmt.Errorf("ERROR - unknown method '%s'\n", splitCmd[0]) } diff --git a/daemon/internal/newrelic/integration/test.go b/daemon/internal/newrelic/integration/test.go index 0f5a23aea..3e70e769a 100644 --- a/daemon/internal/newrelic/integration/test.go +++ b/daemon/internal/newrelic/integration/test.go @@ -10,10 +10,12 @@ import ( "encoding/json" "errors" "fmt" + "math" "net/http" "os" "path/filepath" "regexp" + "strconv" "strings" "time" @@ -456,6 +458,119 @@ func (t *Test) compareSpanEventsLike(harvest *newrelic.Harvest) { } } +// Handles EXPECT_METRICS_EXIST +func (t *Test) compareMetricsExist(harvest *newrelic.Harvest) { + for _, spec := range strings.Split(strings.TrimSpace(string(t.subEnvVars(t.metricsExist))), "\n") { + var err error + var count int64 + + fields := strings.Split(spec, ",") + expected := fields[0] + expected = strings.TrimSpace(expected) + + // -1 means just test for existence without forcing an exact count + count = -1 + if len(fields) == 2 { + countStr := strings.TrimSpace(fields[1]) + if len(countStr) > 0 { + // is it "??" - with or without quotes + match, _ := regexp.MatchString("^\"*\\?\\?\"*$", countStr) + if !match { + count, err = strconv.ParseInt(countStr, 10, 64) + if nil != err { + t.Fail(fmt.Errorf("EXPECT_METRICS_EXIST has unparsable count: %s", spec)) + } + } + } + } + + id := newrelic.AgentRunID("?? agent run id") + actualJSON, _ := newrelic.IntegrationData(harvest.Metrics, id, time.Now()) + + actualPretty := bytes.Buffer{} + json.Indent(&actualPretty, actualJSON, "", " ") + + // scrub actual spans + scrubjson := ScrubLineNumbers(actualJSON) + scrubjson = ScrubFilename(scrubjson, t.Path) + scrubjson = ScrubHost(scrubjson) + + // parse actual JSON to internal format since we can't get the actual count from + // the MetricTable type + var x1 interface{} + if err := json.Unmarshal(scrubjson, &x1); nil != err { + t.Fatal(fmt.Errorf("unable to parse actual metric like json for fuzzy matching: %v"+ + "actual metric table: %s", err, actualPretty.String())) + return + } + + // expect x1 to be of type "[]interface {}" which wraps the entire metric event data + // within this generic array there will be: + // - a string of the form "?? agent run id" + // - a timestamp + // - another timestamp + // - an array of all metrics + // - an array for a single metric + // - map containing key for the metric name - can include scope + // - array of length 6 containing metric data values + + // test initial type is as expected + switch x1.(type) { + case []interface{}: + default: + t.Fatal(errors.New("metric event data json doesnt match expected format")) + return + } + + // expect array of len 4 + v2, _ := x1.([]interface{}) + if len(v2) != 4 { + t.Fatal(errors.New("metric event data json doesnt match expected format - expected 4 elements")) + return + } + + // get array of actual metric from 4th element + actual := v2[3].([]interface{}) + found := false + for i := 0; i < len(actual); i++ { + actualName := actual[i].([]interface{})[0].(map[string]interface{}) + act1 := actualName["name"] + act2 := actualName["scope"] + + // only support an empty scope for now + if act1 == expected && act2 == nil { + // compare count + actualData := actual[i].([]interface{})[1] + actualCount := int64(math.Round(actualData.([]interface{})[0].(float64))) + + metricPasses := false + + // apdex metrics can have a count of 0 since the count field is + // actually the "satisfied" count, not a total count of metric + // as it is for other types of metrics + apdex_metric := strings.HasPrefix(expected, "Apdex/") + if (apdex_metric || (count == -1 && actualCount > 0)) || (actualCount == count) { + metricPasses = true + } + + if !metricPasses { + t.Fail(fmt.Errorf("metric count does not match for %s: expected = %d actual = %d\n"+ + "actual metric table: %s", expected, count, actualCount, actualPretty.String())) + return + } + + found = true + break + } + } + + if !found { + t.Fail(fmt.Errorf("metric %s not found, actual metric table: %s", expected, actualPretty.String())) + return + } + } +} + func (t *Test) comparePayload(expected json.RawMessage, pc newrelic.PayloadCreator, isMetrics bool) { if nil == expected { // No expected output has been specified: Anything passes. @@ -522,6 +637,7 @@ func (t *Test) comparePhpPackages(harvest *newrelic.Harvest) { var expectedPkgsCollection *PhpPackagesCollection var expectNullPkgs bool var version_overrides map[string]interface{} + var subsetMatch bool = false if nil != t.phpPackagesConfig { var err error @@ -541,6 +657,19 @@ func (t *Test) comparePhpPackages(harvest *newrelic.Harvest) { t.Fatal(err) return } + + // Determine if we expect an exact match between expected and actual packages + // when using the composer API in the agent to detect packages it is possible + // it will return packages which the "composer show" command does not. These + // are usually virtual or other types of packages which "composer show" + // decides to not show. + // + // Currently the test handles this by listing all the packages that the agent + // detected but "composer show" did not as "Notes" on the test results. + // + // Tests for the legacy package detection mechanism still expect an exact match. + // + subsetMatch = expectedPkgsCollection.config.expectAllDetected } } else { // no configuration given for package (no EXPECT_PHP_PACKAGES in test case) so don't run test @@ -575,22 +704,44 @@ func (t *Test) comparePhpPackages(harvest *newrelic.Harvest) { if nil == expectedPackages { t.Fail(fmt.Errorf("No expected PHP packages, harvest contains %+v\n", actualPackages)) } - // compare expected and actual lists of packages + // Compare expected and actual lists of packages // since package names should be identical, iterate over // expected list and compare element by element with same // position in actual list. Name and version should match. // this works because the functions which generate these // lists sort them by package name for us - if len(expectedPackages) != len(actualPackages) { + // + // As explained above - if testing results from the Composer API + // agent detection mechanism, we may not get an exact match so + // we relax the test that the lengths of the two lists are the same + if !subsetMatch && (len(expectedPackages) != len(actualPackages)) { t.Fail(fmt.Errorf("Expected and actual php packages differ in length %d vs %d: expected %+v actual %+v", len(expectedPackages), len(actualPackages), expectedPackages, actualPackages)) return } for i, _ := range expectedPackages { - if expectedPackages[i].Name == actualPackages[i].Name { + var matchingIdx int = -1 + for j, pkg := range actualPackages { + //fmt.Printf("Comparing %s to %s\n", pkg.Name, expectedPackages[i].Name) + if pkg.Name == expectedPackages[i].Name { + //fmt.Printf("Match - index = %d\n", j) + matchingIdx = j + break + } + } + + //fmt.Printf("MatchingIdx: %d\n", matchingIdx) + //fmt.Printf("expectedPatckages[%d]: %+v\n", i, expectedPackages[i]) + // if -1 != matchingIdx { + // fmt.Printf("actualPackages[%d]: %+v\n", matchingIdx, actualPackages[matchingIdx]) + // } else { + // fmt.Printf("no match in actualPackages!\n") + // } + + if -1 != matchingIdx { testPackageNameOnly := false if nil != expectedPkgsCollection.config.packageNameOnly { - testPackageNameOnly = StringSliceContains(expectedPkgsCollection.config.packageNameOnly, actualPackages[i].Name) + testPackageNameOnly = StringSliceContains(expectedPkgsCollection.config.packageNameOnly, actualPackages[matchingIdx].Name) if testPackageNameOnly { t.AddNote(fmt.Sprintf("Tested package name only for packages: %+v", expectedPkgsCollection.config.packageNameOnly)) } @@ -604,14 +755,42 @@ func (t *Test) comparePhpPackages(harvest *newrelic.Harvest) { expected_version = override_version.(string) } - if testPackageNameOnly || expected_version == actualPackages[i].Version { + if testPackageNameOnly { + if " " != actualPackages[matchingIdx].Version { + t.Fail(fmt.Errorf("Expected no package version and a package version was detected - expected \" \" actual %+v. ", + actualPackages[matchingIdx].Version)) + return + } else { + continue + } + } + + if expected_version == actualPackages[matchingIdx].Version { continue + } else { + t.Fail(fmt.Errorf("Expected version %s does not match actual version %s for package %s", + expected_version, actualPackages[matchingIdx].Version, expectedPackages[i].Name)) + return } } - t.Fail(fmt.Errorf("Expected and actual Php packages do not match: expected %+v actual %+v. Complete expected %v actual %v.", - expectedPackages[i], actualPackages[i], expectedPackages, actualPackages)) + t.Fail(fmt.Errorf("Expected Php packages do not match any actual packages: expected %+v\nComplete:\nexpected %v\nactual %v.\n", + expectedPackages[i], expectedPackages, actualPackages)) return } + + // create notes for all packages in the actual list not in the expected list + for ii, _ := range actualPackages { + var found bool = false + for _, pkg := range expectedPackages { + if pkg.Name == actualPackages[ii].Name { + found = true + break + } + } + if !found { + t.AddNote(fmt.Sprintf("Detected package not in expected: %+v", actualPackages[ii])) + } + } } else { if nil != expectedPackages { t.Fail(fmt.Errorf("Expected PHP packages %+v, harvest contains none\n", expectedPackages)) @@ -631,6 +810,8 @@ var ( regexp.MustCompile(`^Supportability\/InstrumentedFunction`), regexp.MustCompile(`^Supportability\/TxnData\/.*`), regexp.MustCompile(`^Supportability/C/NewrelicVersion/.*`), + regexp.MustCompile(`^Supportability/PHP/Version/.*`), + regexp.MustCompile(`^Supportability/PHP/AgentVersion/.*`), } ) @@ -684,15 +865,7 @@ func (t *Test) Compare(harvest *newrelic.Harvest) { } if nil != t.metricsExist { - for _, name := range strings.Split(strings.TrimSpace(string(t.subEnvVars(t.metricsExist))), "\n") { - name = strings.TrimSpace(name) - expected := strings.Replace(name, "__FILE__", t.Path, -1) - if !harvest.Metrics.Has(expected) { - actualPretty := bytes.Buffer{} - json.Indent(&actualPretty, []byte(harvest.Metrics.DebugJSON()), "", " ") - t.Fail(fmt.Errorf("metric does not exist: %s\n\nactual metric table: %s", expected, actualPretty.String())) - } - } + t.compareMetricsExist(harvest) } if nil != t.metricsDontExist { diff --git a/daemon/internal/newrelic/integration/util.go b/daemon/internal/newrelic/integration/util.go new file mode 100644 index 000000000..25ab9462f --- /dev/null +++ b/daemon/internal/newrelic/integration/util.go @@ -0,0 +1,33 @@ +// +// Copyright 2020 New Relic Corporation. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// + +package integration + +import ( + "fmt" + "os/exec" +) + +func GetPHPVersion() string { + cmd := exec.Command("php", "-r", "echo PHP_VERSION;") + + output, err := cmd.Output() + if err != nil { + fmt.Printf("Failed to get PHP version: %v\n", err) + return "failed" + } + + return string(output) +} + +func GetAgentVersion(agent_extension string) string { + cmd := exec.Command("php", "-d", "extension="+agent_extension, "-r", "echo phpversion('newrelic');") + + output, err := cmd.Output() + if err != nil { + return fmt.Errorf("Failed to get agent version: %v", err).Error() + } + return string(output) +} diff --git a/daemon/internal/newrelic/processor_test.go b/daemon/internal/newrelic/processor_test.go index 4910535ab..1c07ea331 100644 --- a/daemon/internal/newrelic/processor_test.go +++ b/daemon/internal/newrelic/processor_test.go @@ -1473,11 +1473,11 @@ func TestShouldConnect(t *testing.T) { } } -// runs a mocked test of resolution of agent harvest limit request and value returned by collector +// Runs a mocked test of resolution of agent harvest limit request and value returned by collector. // Notes: -// eventType: "log_event_data" or "custom_event_data" (no others supported currently) -// agentLimit: Harvest limit from agent (INI file) for a 60 second harvest period -// collectorLimit: Harvest limit sent from collector for a 5 second harvest period +// - eventType: "log_event_data" or "custom_event_data" (no others supported currently) +// - agentLimit: Harvest limit from agent (INI file) for a 60 second harvest period +// - collectorLimit: Harvest limit sent from collector for a 5 second harvest period // // Be aware the agentLimit will be scaled down by 12 (60/5) before being compared to the // collectorLimit. diff --git a/daemon/internal/newrelic/utilization/kubernetes.go b/daemon/internal/newrelic/utilization/kubernetes.go index 2bca922ca..e96a6e560 100644 --- a/daemon/internal/newrelic/utilization/kubernetes.go +++ b/daemon/internal/newrelic/utilization/kubernetes.go @@ -11,7 +11,7 @@ import ( ) type kubernetes struct { - KubernetesServiceHost string `json:"kubernetes_service_host",omitempty` + KubernetesServiceHost string `json:"kubernetes_service_host,omitempty"` // Having a custom getter allows the unit tests to mock os.Getenv(). environmentVariableGetter func(key string) string diff --git a/daemon/internal/newrelic/utilization/utilization_hash.go b/daemon/internal/newrelic/utilization/utilization_hash.go index 7791dfee8..1046f84ed 100644 --- a/daemon/internal/newrelic/utilization/utilization_hash.go +++ b/daemon/internal/newrelic/utilization/utilization_hash.go @@ -207,8 +207,8 @@ func OverrideDockerId(util *Data, id string) error { if nil == util.Vendors { util.Vendors = &vendors{} } - util.Vendors.Docker = &docker{ID: id} - return nil + util.Vendors.Docker = &docker{ID: id} + return nil } func OverrideVendors(util *Data) { @@ -222,20 +222,20 @@ func OverrideVendors(util *Data) { } func GetDockerId(util *Data) (string, error) { - id := "" - if nil == util { - return id, fmt.Errorf("Util is nil") - } - if util.Vendors.isEmpty() { - return id, fmt.Errorf("Vendors structure is empty") - } - if nil == util.Vendors.Docker { - return id, fmt.Errorf("Docker structure is empty") - } - - id = util.Vendors.Docker.ID - - return id, nil + id := "" + if nil == util { + return id, fmt.Errorf("Util is nil") + } + if util.Vendors.isEmpty() { + return id, fmt.Errorf("Vendors structure is empty") + } + if nil == util.Vendors.Docker { + return id, fmt.Errorf("Docker structure is empty") + } + + id = util.Vendors.Docker.ID + + return id, nil } func GatherMemory(util *Data) error { diff --git a/docker-compose.yaml b/docker-compose.yaml index 4626f2523..b99c89dc0 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -2,7 +2,6 @@ # Copyright 2021 New Relic Corporation. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # -version: '3.8' services: # The Database mysqldb: @@ -15,41 +14,26 @@ services: MYSQL_USER: admin MYSQL_PASSWORD: admin MYSQL_HOST: mysqldb - ports: - - "3306:3306" healthcheck: test: ["CMD", "mysql", "--user=admin", "--password=admin", "-e", "SELECT 1"] interval: 10s timeout: 10s retries: 3 start_period: 20s - container_name: mysqldb + volumes: + - var-run-mysqld:/var/run/mysqld redisdb: image: redis restart: always - ports: - - "6379:6379" - container_name: redisdb memcached: image: memcached:latest restart: always - ports: - - "11211:11211" - container_name: memcached -# mongodb: -# image: mongo:latest -# restart: always -# ports: -# - "27019:27019" postgres: image: postgres restart: always environment: POSTGRES_PASSWORD: root POSTGRES_USER: postgres - ports: - - "5432:5432" - container_name: postgres php: platform: ${PLATFORM:-} image: newrelic/nr-php-agent-builder:make-php-${PHP:-8.2}-${LIBC:-gnu}-${IMAGE_VERSION:-v1} @@ -57,12 +41,11 @@ services: environment: MEMCACHE_HOST: memcached -# MONGO_HOST: mongodb - MYSQL_DB: database MYSQL_USER: admin MYSQL_PASSWD: admin MYSQL_HOST: mysqldb + MYSQL_SOCKET: /var/run/mysqld/mysqld.sock PG_HOST: postgres PG_PORT: 5432 @@ -74,6 +57,7 @@ services: volumes: - ${AGENT_CODE:-$PWD}:/usr/local/src/newrelic-php-agent + - var-run-mysqld:/var/run/mysqld entrypoint: tail command: -f /dev/null container_name: nr-php @@ -82,16 +66,16 @@ services: build: context: . dockerfile: files/Dockerfile - user: ${UID}:${GID} + args: + PHP_VER: ${PHP:-8.3} environment: MEMCACHE_HOST: memcached -# MONGO_HOST: mongodb - MYSQL_DB: database MYSQL_USER: admin MYSQL_PASSWD: admin MYSQL_HOST: mysqldb + MYSQL_SOCKET: /var/run/mysqld/mysqld.sock PG_HOST: postgres PG_PORT: 5432 @@ -106,8 +90,11 @@ services: NEWRELIC_LICENSE_KEY: ${NEW_RELIC_LICENSE_KEY} volumes: - ${PWD}:/usr/src/myapp + - var-run-mysqld:/var/run/mysqld working_dir: /usr/src/myapp stdin_open: true tty: true - container_name: agent-devenv profiles: ["dev"] + +volumes: + var-run-mysqld: diff --git a/docs/dev_environment.md b/docs/dev_environment.md index 7e6940d05..15948fc9f 100644 --- a/docs/dev_environment.md +++ b/docs/dev_environment.md @@ -6,19 +6,38 @@ The dockerized development environment prototype allows contributors to both dev docker-compose spins up `mysql` and `redis` and other databases in separate containers. -Two environment variables to note: -`NEWRELIC_LICENSE_KEY` is required to run the integration tests and should be set to your NR license key. -If your collector isn’t the default (collector.newrelic.com), set the `NEWRELIC_COLLECTOR_HOST` to the appropriate value. +## Prerequisites -PHP_VER can also be set to vary the PHP version being used. +### 1. Docker Compose -Set all environment variables prior to running the development environment. +Dockerized development environment for the New Relic PHP Agent uses following Docker Compose services as a runtime platform. So you need `docker` with `docker compose` installed. + +### 2. Environment variables + +Dockerized development environment for the New Relic PHP Agent needs a valid license key available in `NEW_RELIC_LICENSE_KEY` environment variable. +This environment variable must be set prior to starting Dockerized development environment for the New Relic PHP Agent. The easiest way to set +`NEW_RELIC_LICENSE_KEY` environment variable is via `.env` file. Simply create `.env` file in the top level directory, and add definition +of `NEW_RELIC_LICENSE_KEY` environment variable there, e.g.: +``` +NEW_RELIC_LICENSE_KEY=... +``` + +The second, optional environment variable, that controls PHP version in Dockerized development environment for the New Relic PHP Agent is `PHP`. `PHP` defaults to latest PHP supported by the agent. +This environment variable can be provided at the time when Dockerized development environment for the New Relic PHP Agent is started, e.g.: +``` +make dev-shell PHP=8.2 +``` ## Options for using the environment -## With a shell environment +### With a shell environment + +To start the dev environment type `make dev-shell`. This will spin up `devenv` service in `agent-devenv` container, with: + - latest PHP supported by the agent (this can be overriden with `PHP` environment variable like this: `make dev-shell PHP=8.2`) + - all the tools needed to build the agent + - all the tools needed to run unit tests + - all the tools and supporting services to run integration tests -To start the dev environment type `make dev-shell`. This will create a set of docker containers. A prompt will open and you’ll be able to compile and run all `make` commands right away with no additional setup (for example: `make -j4 all` or `make -j4 valgrind` or `make -j4 run_tests`). After compiling the agent, the integration tests can be run using the `integration_runner`. @@ -31,27 +50,27 @@ To end the session type `exit`. You can run `make dev-stop` to stop the docker- In the shell, you can run all `make` commands as you normally would. -## Build only +### Build only `make dev-build` -## Unit Tests only +### Unit Tests only `make dev-unit tests` -## Integration Tests only +### Integration Tests only `make dev-integration-tests` -## Build and test all +### Build and test all `make dev-all` -## Stop all containers +### Stop all containers `make dev-stop` -# Next steps and issues +## Next steps and issues -## There is possibly some incompatibility with mysql in the main build container as one of the mysql unit tests fails. Unless this is resolved, It might make sense at a future point to have the integration tests run from a different container than the build container. +### There is possibly some incompatibility with mysql in the main build container as one of the mysql unit tests fails. Unless this is resolved, It might make sense at a future point to have the integration tests run from a different container than the build container. diff --git a/docs/development.md b/docs/development.md index c85755903..ec42de64d 100644 --- a/docs/development.md +++ b/docs/development.md @@ -58,7 +58,7 @@ _(most operating systems package these with `-dev` or `-devel` suffixes)_ ### PHP -The PHP agent supports PHP versions `7.0`, `7.1`, `7.2`, `7.3`, `7.4`,`8.0`, `8.1`, '8.2', and `8.3`. +The PHP agent supports PHP versions `7.2`, `7.3`, `7.4`,`8.0`, `8.1`, `8.2`, `8.3` and `8.4`. ## Build the PHP Agent diff --git a/files/Dockerfile b/files/Dockerfile index 4b50bebb8..2f99740e3 100644 --- a/files/Dockerfile +++ b/files/Dockerfile @@ -9,100 +9,70 @@ ARG PHP_VER -FROM php:${PHP_VER:-8.3} +FROM php:${PHP_VER:-8.4} RUN docker-php-source extract ARG DEBIAN_FRONTEND=noninteractive -RUN apt-get update -RUN apt-get install -y build-essential - -# -# PHP dependencies -# -RUN apt-get update \ - && apt-get -y install gcc git netcat-openbsd \ - libpcre3 libpcre3-dev psmisc automake libtool \ - insserv procps vim ${PHP_USER_SPECIFIED_PACKAGES} \ - zlib1g-dev libmcrypt-dev +RUN apt-get update && apt-get install -y --no-install-recommends \ +# Install build entrypoint - make: + make \ +# Makefile's default shell: + bash \ +# git; techincally not needed to build agent but is required for successfull +# processing of top level Makefile: +# - make/version.mk if GIT_COMMIT is not defined, git is used to compute it: + git \ +# The following are required to build PHP extension: + $PHPIZE_DEPS \ +# valgrind, awk; required for agent-valgrind and axiom-valgrind targets + valgrind gawk \ +# Other useful developer tools: + vim lcov gdb strace ccache procps psmisc curl wget bzip2 zip unzip perl sqlite3 openssl \ +# Other build dependencies: + argon2 \ + automake \ + autotools-dev \ + dnsutils \ + gyp \ + insserv \ + libc6 libc6-dev libc6-dbg \ + libcurl4-openssl-dev \ + libedit-dev \ + libghc-argon2-dev \ + libgtest-dev \ + libmcrypt-dev \ + libonig-dev \ + libpcre3 libpcre3-dev \ + libreadline-dev \ + libssl-dev \ + libsqlite3-dev \ + libtool \ + libxml2 libxml2-dev \ + locales \ + locales-all \ + netcat-openbsd \ + python3-yaml \ + ${PHP_USER_SPECIFIED_PACKAGES} \ + zlib1g-dev # pgsql extension -RUN apt-get install -y libpq-dev +RUN apt-get install -y --no-install-recommends libpq-dev RUN docker-php-ext-install pgsql -# -# Other tools -# -RUN apt-get install -y gdb valgrind libcurl4-openssl-dev pkg-config libpq-dev libedit-dev libreadline-dev git - -# -# Install other packages. -# -RUN apt-get update && apt-get install -y \ - autoconf \ - autotools-dev \ - build-essential \ - bzip2 \ - ccache \ - curl \ - dnsutils \ - git \ - gyp \ - lcov \ - libc6 \ - libc6-dbg \ - libc6-dev \ - libgtest-dev \ - libtool \ - locales \ - locales-all \ - make \ - perl \ - strace \ - python-dev-is-python3 \ - python3-yaml \ - sqlite3 \ - libsqlite3-dev \ - openssl \ - libxml2 \ - libxml2-dev \ - libonig-dev \ - libssl-dev \ - unzip \ - wget \ - zip && apt-get clean - -# -# Download and install Go -# -RUN arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) && \ - wget https://go.dev/dl/go1.21.1.linux-${arch}.tar.gz -O- | tar -C /usr/local -zxvf -; - -RUN ln -s /usr/local/go/bin/go /usr/bin/go +# install latest go to work with the daemon +COPY --from=golang /usr/local/go /usr/local/go +ENV PATH /usr/local/go/bin:$PATH # -# If the debian version is jessie, don't install argon2 +# If the debian version is buster, don't install python-dev-is-python3 # -RUN if [ -z "$(grep '^8\.' /etc/debian_version)" ]; then \ - apt-get install -y argon2 libghc-argon2-dev; \ +RUN if [ -z "$(grep '^10\.' /etc/debian_version)" ]; then \ + apt-get install -y --no-install-recommends python-dev-is-python3; \ fi # install composer -WORKDIR /usr/src - -# based on https://getcomposer.org/doc/faqs/how-to-install-composer-programmatically.md -RUN \ - EXPECTED_CHECKSUM="$(php -r 'copy("https://composer.github.io/installer.sig", "php://stdout");')" \ - && php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \ - && ACTUAL_CHECKSUM="$(php -r "echo hash_file('sha384', 'composer-setup.php');")" \ - && if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; \ - then \ - >&2 echo 'ERROR: Invalid installer checksum'; \ - rm composer-setup.php; \ - exit 1; \ - fi \ - && php composer-setup.php \ - && php -r "unlink('composer-setup.php');" +COPY --from=composer ["/usr/bin/composer", "/usr/bin/composer"] # # The explain plan in the sql tests contain partition/filtered properties @@ -119,10 +89,18 @@ RUN \ # and 8.0 has problems with how the explanation for informational_schema # work (refer to bug https://bugs.mysql.com/bug.php?id=102536) so to run # the mysql tests a separate machine running mysql server 5.6 is required. -RUN docker-php-ext-install pdo pdo_mysql +RUN docker-php-ext-install pdo pdo_mysql pdo_pgsql -# redis -RUN pecl install redis && docker-php-ext-enable redis +# install redis extension required by test_redis: +RUN \ + php_cmp=$(php -r "echo version_compare(PHP_VERSION, '7.4.0', '>=');"); \ + if [ "$php_cmp" = 1 ]; then \ + # install latest redis for PHPs >= 7.4 + echo 'no' | pecl install redis; \ + else \ + # install redis-4.3.0 - last one with support for php 7.2 + echo 'no' | pecl install redis-4.3.0; \ + fi && docker-php-ext-enable redis # memcache # Pre 8.0 requires 4.0.5.2 @@ -135,7 +113,7 @@ RUN \ fi # memcached -RUN apt-get install -y libmemcached-dev +RUN apt-get install -y --no-install-recommends libmemcached-dev RUN pecl install memcached && docker-php-ext-enable memcached # uopz @@ -152,13 +130,9 @@ RUN \ # install predis # installation will be in /usr/src/vendor/predis/predis # which is value which should be used for PREDIS_HOME -RUN php composer.phar require "predis/predis" -RUN php composer.phar update - -# -# install composer and make executable so it can be used in dev env -# -RUN cp composer.phar /usr/local/bin/composer && chmod +x /usr/local/bin/composer +WORKDIR /usr/src +RUN composer require "predis/predis" +RUN composer update # # These args need to be repeated so we can propagate the VARS within this build context. @@ -168,11 +142,18 @@ ENV PHP_VER=${PHP_VER} ARG PS1 ENV PS1="New Relic > " +ARG USER=developer +ARG UID=501 +ARG GID=20 +RUN useradd --uid ${UID} --gid ${GID} --shell /bin/bash --create-home ${USER} +USER ${USER} # QOL aliases # `rebuild` - make clean + make agent + make tests # `integ` - run all integration tests RUN echo 'alias integ="/usr/src/myapp/bin/integration_runner -agent /usr/src/myapp/agent/.libs/newrelic.so"' >> ~/.bashrc \ && echo 'alias rebuild="make -C agent clean && rm agent/Makefile && make && make tests"' >> ~/.bashrc - WORKDIR /usr/src/myapp +# Avoid "fatal: detected dubious ownership in repository at '/usr/src/myapp/'" error +# when running git commands inside container with host volume mounted: +RUN git config --global --add safe.directory /usr/src/myapp/ CMD ["bash"] diff --git a/files/set_path.sh b/files/set_path.sh index 3a5cc965c..0b9e88c48 100644 --- a/files/set_path.sh +++ b/files/set_path.sh @@ -17,11 +17,6 @@ case ":$PATH:" in *) PATH=/usr/local/bin:$PATH esac -case ":$PATH:" in - *:/usr/local/go/bin:*) ;; - *) PATH=/usr/local/go/bin:$PATH -esac - export PATH diff --git a/make/php_versions.mk b/make/php_versions.mk index 0629bfabd..f28157f09 100644 --- a/make/php_versions.mk +++ b/make/php_versions.mk @@ -3,4 +3,4 @@ # SPDX-License-Identifier: Apache-2.0 # -PHP_VERSION_LIST=$${PHPS:-8.3 8.2 8.1 8.0 7.4 7.3 7.2 7.1 7.0} +PHP_VERSION_LIST=$${PHPS:-8.4 8.3 8.2 8.1 8.0 7.4 7.3 7.2} diff --git a/make/release.mk b/make/release.mk index 6bb9eba61..bb166f66e 100644 --- a/make/release.mk +++ b/make/release.mk @@ -163,6 +163,7 @@ release-$1-zts: Makefile agent | releases/$$(RELEASE_OS)/agent/$$(RELEASE_ARCH)/ endef +$(eval $(call RELEASE_AGENT_TARGET,8.4,20240924)) $(eval $(call RELEASE_AGENT_TARGET,8.3,20230831)) $(eval $(call RELEASE_AGENT_TARGET,8.2,20220829)) $(eval $(call RELEASE_AGENT_TARGET,8.1,20210902)) @@ -170,8 +171,6 @@ $(eval $(call RELEASE_AGENT_TARGET,8.0,20200930)) $(eval $(call RELEASE_AGENT_TARGET,7.4,20190902)) $(eval $(call RELEASE_AGENT_TARGET,7.3,20180731)) $(eval $(call RELEASE_AGENT_TARGET,7.2,20170718)) -$(eval $(call RELEASE_AGENT_TARGET,7.1,20160303)) -$(eval $(call RELEASE_AGENT_TARGET,7.0,20151012)) # diff --git a/tests/include/config.php b/tests/include/config.php index 8f29f3faf..399d15552 100644 --- a/tests/include/config.php +++ b/tests/include/config.php @@ -13,7 +13,7 @@ function isset_or($check, $alternate = NULL) $MYSQL_USER = isset_or('MYSQL_USER', 'root'); $MYSQL_PASSWD = isset_or('MYSQL_PASSWD', 'root'); -$MYSQL_DB = 'information_schema'; // TODO: MSL comment here. +$MYSQL_DB = isset_or('MYSQL_DB', 'information_schema'); $MYSQL_HOST = isset_or('MYSQL_HOST', 'localhost'); $MYSQL_PORT = isset_or('MYSQL_PORT', 3306); $MYSQL_SOCKET = isset_or('MYSQL_SOCKET', ''); @@ -24,14 +24,6 @@ function isset_or($check, $alternate = NULL) $MYSQL_SERVER = $MYSQL_HOST . ":" . $MYSQL_PORT; } -if (class_exists('MongoClient')) { - $MONGO_HOST = isset_or('MONGO_HOST', MongoClient::DEFAULT_HOST); - $MONGO_PORT = isset_or('MONGO_PORT', MongoClient::DEFAULT_PORT); -} else { - $MONGO_HOST = null; - $MONGO_PORT = null; -} - $MEMCACHE_HOST = isset_or('MEMCACHE_HOST', '127.0.0.1'); $MEMCACHE_PORT = isset_or('MEMCACHE_PORT', '11211'); @@ -83,6 +75,10 @@ function make_tracing_url($file) $PG_PORT = isset_or('PG_PORT', '5433'); $PG_CONNECTION = "host=$PG_HOST port=$PG_PORT user=$PG_USER password=$PG_PW connect_timeout=1"; +$PDO_PGSQL_DSN = 'pgsql:'; +$PDO_PGSQL_DSN .= 'host=' . $PG_HOST . ';'; +$PDO_PGSQL_DSN .= 'port=' . $PG_PORT . ';'; + // Header used to track whether or not our CAT instrumentation interferes with // other existing headers. define('CUSTOMER_HEADER', 'Customer-Header'); diff --git a/tests/include/guzzle.tar.bz2 b/tests/include/guzzle.tar.bz2 index 49e7b4ef2..a7ecfeb72 100644 Binary files a/tests/include/guzzle.tar.bz2 and b/tests/include/guzzle.tar.bz2 differ diff --git a/tests/include/monolog2.tar.bz2 b/tests/include/monolog2.tar.bz2 index 2b97f8938..a1c0578e4 100644 Binary files a/tests/include/monolog2.tar.bz2 and b/tests/include/monolog2.tar.bz2 differ diff --git a/tests/include/monolog3.tar.bz2 b/tests/include/monolog3.tar.bz2 index 2cce31210..783c067d6 100644 Binary files a/tests/include/monolog3.tar.bz2 and b/tests/include/monolog3.tar.bz2 differ diff --git a/tests/integration/api/error_group_callback/test_error_group_callback_error_web.php b/tests/integration/api/error_group_callback/test_error_group_callback_error_web.php index c4c02c418..3b82e16f9 100644 --- a/tests/integration/api/error_group_callback/test_error_group_callback_error_web.php +++ b/tests/integration/api/error_group_callback/test_error_group_callback_error_web.php @@ -8,6 +8,14 @@ Tests newrelic_set_error_group_callback() API for Web errors. */ +/*SKIPIF +=")) { + die("skip: newer test for PHPs 8.4+\n"); +} +*/ + + /*ENVIRONMENT REQUEST_METHOD=GET QUERY_STRING=foo=1&bar=2 diff --git a/tests/integration/api/error_group_callback/test_error_group_callback_error_web.php84.php b/tests/integration/api/error_group_callback/test_error_group_callback_error_web.php84.php new file mode 100644 index 000000000..e740c7613 --- /dev/null +++ b/tests/integration/api/error_group_callback/test_error_group_callback_error_web.php84.php @@ -0,0 +1,179 @@ + \/test_error_group_callback_error_web.php84.php\?foo=1&bar=2 +path => .*test_error_group_callback_error_web.php84.php +method => GET +status_code => 200 + +klass => E_USER_WARNING +message => I'M COVERED IN BEES +file => .*test_error_group_callback_error_web.php84.php +stack => \[" in trigger_error called at .*test_error_group_callback_error_web.php84.php \(.*\)"," in alpha called at .*test_error_group_callback_error_web.php84.php \(.*\)"\] +*/ + +/*EXPECT_METRICS +[ + "?? agent run id", + "?? timeframe start", + "?? timeframe stop", + [ + [{"name":"Apdex"}, [0, "??", "??", "??", "??", "??"]], + [{"name":"Apdex/Uri__FILE__"}, [0, "??", "??", "??", "??", "??"]], + [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/allWeb"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Errors/WebTransaction/Uri__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Errors/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Errors/allWeb"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/all"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"ErrorsByCaller/Unknown/Unknown/Unknown/Unknown/allWeb"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"HttpDispatcher"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/api/set_error_group_callback"}, [1, 0, 0, 0, 0, 0]], + [{"name":"WebTransaction"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"WebTransaction/Uri__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"WebTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"WebTransactionTotalTime/Uri__FILE__"}, [1, "??", "??", "??", "??", "??"]], + [{"name":"Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]] + ] +] +*/ + + +/*EXPECT_ERROR_EVENTS +[ + "?? agent run id", + { + "reservoir_size": 100, + "events_seen": 1 + }, + [ + [ + { + "type": "TransactionError", + "timestamp": "??", + "error.class": "E_USER_WARNING", + "error.message": "I'M COVERED IN BEES", + "transactionName": "WebTransaction\/Uri__FILE__", + "duration": "??", + "nr.transactionGuid": "??", + "guid": "??", + "sampled": true, + "priority": "??", + "traceId": "??", + "spanId": "??" + }, + {}, + { + "response.headers.contentType": "application\/json", + "http.statusCode": 200, + "response.statusCode": 200, + "httpResponseCode": "200", + "request.uri": "__FILE__", + "error.group.name": "CUSTOM ERROR GROUP NAME", + "SERVER_NAME": "127.0.0.1", + "request.method": "GET", + "request.headers.host": "127.0.0.1" + } + ] + ] +] +*/ + +/*EXPECT_TRACED_ERRORS +[ + "?? agent run id", + [ + [ + "??", + "WebTransaction\/Uri__FILE__", + "I'M COVERED IN BEES", + "E_USER_WARNING", + { + "stack_trace": [ + " in trigger_error called at __FILE__ (??)", + " in alpha called at __FILE__ (??)" + ], + "agentAttributes": { + "response.headers.contentType": "application\/json", + "http.statusCode": 200, + "response.statusCode": 200, + "httpResponseCode": "200", + "request.uri": "__FILE__", + "error.group.name": "CUSTOM ERROR GROUP NAME", + "SERVER_NAME": "127.0.0.1", + "request.method": "GET", + "request.headers.host": "127.0.0.1" + }, + "intrinsics": { + "totalTime": "??", + "cpu_time": "??", + "cpu_user_time": "??", + "cpu_sys_time": "??", + "guid": "??", + "sampled": true, + "priority": "??", + "traceId": "??" + }, + "request_uri": "__FILE__" + }, + "?? transaction ID" + ] + ] +] +*/ + +require_once(realpath(dirname(__FILE__)) . '/../../../include/tap.php'); + +header('Content-Type: text/html'); +header('Content-Type: application/json'); + +function alpha() +{ + trigger_error("I'M COVERED IN BEES", E_USER_WARNING); +} + +$callback = function($txndata, $errdata) +{ + foreach($txndata as $tkey => $tdata) { + echo "$tkey => $tdata\n"; + }; + + echo "\n"; + + foreach($errdata as $ekey => $edata) { + echo "$ekey => $edata\n"; + }; + + $fingerprint = "CUSTOM ERROR GROUP NAME"; + return $fingerprint; +}; + +$result = newrelic_set_error_group_callback($callback); + +tap_assert($result, "callback registered"); + +alpha(); diff --git a/tests/integration/api/notice_error/test_bad_inputs.php b/tests/integration/api/notice_error/test_bad_inputs.php index 8f72e2d25..75d582884 100644 --- a/tests/integration/api/notice_error/test_bad_inputs.php +++ b/tests/integration/api/notice_error/test_bad_inputs.php @@ -15,6 +15,9 @@ ok - 2 args ok - 3 args ok - 4 args +ok - 4 args +ok - 4 args +ok - 4 args ok - 5 args ok - 6 args */ @@ -32,9 +35,15 @@ tap_equal(null, newrelic_notice_error("", 42), "2 args"); tap_equal(null, newrelic_notice_error("", array()), "2 args"); -// Three and four argument forms are not allowed. +// Three argument forms are not allowed. tap_equal(null, newrelic_notice_error(42, "message", "file"), "3 args"); -tap_equal(null, newrelic_notice_error(42, "message", "file", __LINE__), "4 args"); + +// Four argument form requires integer, string, string, integer +// This is like the five argument form but for PHP 8+ where the context is not supplied +tap_equal(null, newrelic_notice_error("", "message", "file", __LINE__), "4 args"); +tap_equal(null, newrelic_notice_error(42, array(), "file", __LINE__), "4 args"); +tap_equal(null, newrelic_notice_error("", "message", array(), __LINE__), "4 args"); +tap_equal(null, newrelic_notice_error("", "message", "file", ""), "4 args"); // Five argument form requires second arg to be convertible to a string. tap_equal(null, newrelic_notice_error("", curl_init()), "5 args"); diff --git a/tests/integration/api/notice_error/test_good_1_arg_exception.php b/tests/integration/api/notice_error/test_good_1_arg_exception.php new file mode 100644 index 000000000..c71750439 --- /dev/null +++ b/tests/integration/api/notice_error/test_good_1_arg_exception.php @@ -0,0 +1,133 @@ +=")) { + die("skip: newer test for PHP 8.4+\n"); +} */ /*INI @@ -94,7 +97,7 @@ class name, and lineno for closures. }, {}, { - "code.lineno": 151, + "code.lineno": 154, "code.filepath": "__FILE__", "code.function": "{closure}" } @@ -115,7 +118,7 @@ class name, and lineno for closures. }, {}, { - "code.lineno": 159, + "code.lineno": 162, "code.filepath": "__FILE__", "code.function": "{closure}" } @@ -136,7 +139,7 @@ class name, and lineno for closures. }, {}, { - "code.lineno": 159, + "code.lineno": 162, "code.filepath": "__FILE__", "code.function": "{closure}" } diff --git a/tests/integration/attributes/test_transaction_closure_clm.php84.php b/tests/integration/attributes/test_transaction_closure_clm.php84.php new file mode 100644 index 000000000..7a30585c4 --- /dev/null +++ b/tests/integration/attributes/test_transaction_closure_clm.php84.php @@ -0,0 +1,167 @@ +=")) { + die("skip: newer test for PHP 8.4+\n"); +} */ /*INI diff --git a/tests/integration/attributes/test_transaction_closure_clm_off.php84.php b/tests/integration/attributes/test_transaction_closure_clm_off.php84.php new file mode 100644 index 000000000..dda499cb6 --- /dev/null +++ b/tests/integration/attributes/test_transaction_closure_clm_off.php84.php @@ -0,0 +1,154 @@ + $info) { + $version = ltrim($info['pretty_version'], 'v'); + echo "$package => $version\n"; + } + } +} diff --git a/tests/integration/autoloader/autoload-with-broken-composer-00/vendor/composer/autoload_real.php b/tests/integration/autoloader/autoload-with-broken-composer-00/vendor/composer/autoload_real.php new file mode 100644 index 000000000..014c86e9a --- /dev/null +++ b/tests/integration/autoloader/autoload-with-broken-composer-00/vendor/composer/autoload_real.php @@ -0,0 +1,9 @@ + array( + 'pretty_version' => 'v1.0.0', + 'version' => '1.0.0.0', + 'type' => 'project' + ), + // Mocked data: installed packages and their versions + 'versions' => array( + 'vendor1/package1' => array( + 'pretty_version' => 'v1.1.3', + 'version' => '1.1.3.0', + 'type' => 'library' + ), + 'vendor2/package2' => array( + 'pretty_version' => '2.1.5', + 'version' => '2.1.5.0', + 'type' => 'library' + ) + ) + ); diff --git a/tests/integration/autoloader/autoload-with-broken-composer-01/vendor/autoload.php b/tests/integration/autoloader/autoload-with-broken-composer-01/vendor/autoload.php new file mode 100644 index 000000000..01d206c0f --- /dev/null +++ b/tests/integration/autoloader/autoload-with-broken-composer-01/vendor/autoload.php @@ -0,0 +1,12 @@ + $info) { + $version = ltrim($info['pretty_version'], 'v'); + echo "$package => $version\n"; + } + } +} diff --git a/tests/integration/autoloader/autoload-with-broken-composer-01/vendor/composer/autoload_real.php b/tests/integration/autoloader/autoload-with-broken-composer-01/vendor/composer/autoload_real.php new file mode 100644 index 000000000..014c86e9a --- /dev/null +++ b/tests/integration/autoloader/autoload-with-broken-composer-01/vendor/composer/autoload_real.php @@ -0,0 +1,9 @@ + array( + 'pretty_version' => 'v1.0.0', + 'version' => '1.0.0.0', + 'type' => 'project' + ), + // Mocked data: installed packages and their versions + 'versions' => array( + 'vendor1/package1' => array( + 'pretty_version' => 'v1.1.3', + 'version' => '1.1.3.0', + 'type' => 'library' + ), + 'vendor2/package2' => array( + 'pretty_version' => '2.1.5', + 'version' => '2.1.5.0', + 'type' => 'library' + ) + ) + ); diff --git a/tests/integration/autoloader/autoload-with-broken-composer-02/vendor/autoload.php b/tests/integration/autoloader/autoload-with-broken-composer-02/vendor/autoload.php new file mode 100644 index 000000000..01d206c0f --- /dev/null +++ b/tests/integration/autoloader/autoload-with-broken-composer-02/vendor/autoload.php @@ -0,0 +1,12 @@ + $info) { + $version = ltrim($info['pretty_version'], 'v'); + echo "$package => $version\n"; + } + } +} diff --git a/tests/integration/autoloader/autoload-with-broken-composer-02/vendor/composer/autoload_real.php b/tests/integration/autoloader/autoload-with-broken-composer-02/vendor/composer/autoload_real.php new file mode 100644 index 000000000..014c86e9a --- /dev/null +++ b/tests/integration/autoloader/autoload-with-broken-composer-02/vendor/composer/autoload_real.php @@ -0,0 +1,9 @@ + $info) { + $version = ltrim($info['pretty_version'], 'v'); + echo "$package => $version\n"; + } + } +} diff --git a/tests/integration/autoloader/autoload-with-composer-throwing-error/vendor/composer/autoload_real.php b/tests/integration/autoloader/autoload-with-composer-throwing-error/vendor/composer/autoload_real.php new file mode 100644 index 000000000..014c86e9a --- /dev/null +++ b/tests/integration/autoloader/autoload-with-composer-throwing-error/vendor/composer/autoload_real.php @@ -0,0 +1,9 @@ + array( + 'pretty_version' => 'v1.0.0', + 'version' => '1.0.0.0', + 'type' => 'project' + ), + // Mocked data: installed packages and their versions + 'versions' => array( + 'vendor1/package1' => array( + 'pretty_version' => 'v1.1.3', + 'version' => '1.1.3.0', + 'type' => 'library' + ), + 'vendor2/package2' => array( + 'pretty_version' => '2.1.5', + 'version' => '2.1.5.0', + 'type' => 'library' + ) + ) + ); diff --git a/tests/integration/autoloader/autoload-with-composer-throwing-exception/vendor/autoload.php b/tests/integration/autoloader/autoload-with-composer-throwing-exception/vendor/autoload.php new file mode 100644 index 000000000..01d206c0f --- /dev/null +++ b/tests/integration/autoloader/autoload-with-composer-throwing-exception/vendor/autoload.php @@ -0,0 +1,12 @@ + $info) { + $version = ltrim($info['pretty_version'], 'v'); + echo "$package => $version\n"; + } + } +} diff --git a/tests/integration/autoloader/autoload-with-composer-throwing-exception/vendor/composer/autoload_real.php b/tests/integration/autoloader/autoload-with-composer-throwing-exception/vendor/composer/autoload_real.php new file mode 100644 index 000000000..014c86e9a --- /dev/null +++ b/tests/integration/autoloader/autoload-with-composer-throwing-exception/vendor/composer/autoload_real.php @@ -0,0 +1,9 @@ + array( + 'pretty_version' => 'v1.0.0', + 'version' => '1.0.0.0', + 'type' => 'project' + ), + // Mocked data: installed packages and their versions + 'versions' => array( + 'vendor1/package1' => array( + 'pretty_version' => 'v1.1.3', + 'version' => '1.1.3.0', + 'type' => 'library' + ), + 'vendor2/package2' => array( + 'pretty_version' => '2.1.5', + 'version' => '2.1.5.0', + 'type' => 'library' + ) + ) + ); diff --git a/tests/integration/autoloader/autoload-with-composer/vendor/autoload.php b/tests/integration/autoloader/autoload-with-composer/vendor/autoload.php new file mode 100644 index 000000000..01d206c0f --- /dev/null +++ b/tests/integration/autoloader/autoload-with-composer/vendor/autoload.php @@ -0,0 +1,12 @@ + $info) { + $version = ltrim($info['pretty_version'], 'v'); + echo "$package => $version\n"; + } + } +} diff --git a/tests/integration/autoloader/autoload-with-composer/vendor/composer/autoload_real.php b/tests/integration/autoloader/autoload-with-composer/vendor/composer/autoload_real.php new file mode 100644 index 000000000..014c86e9a --- /dev/null +++ b/tests/integration/autoloader/autoload-with-composer/vendor/composer/autoload_real.php @@ -0,0 +1,9 @@ + array( + 'pretty_version' => 'v1.0.0', + 'version' => '1.0.0.0', + 'type' => 'project' + ), + // Mocked data: installed packages and their versions + 'versions' => array( + 'vendor1/package1' => array( + 'pretty_version' => 'v1.1.3', + 'version' => '1.1.3.0', + 'type' => 'library' + ), + 'vendor2/package2' => array( + 'pretty_version' => '2.1.5', + 'version' => '2.1.5.0', + 'type' => 'library' + ) + ) + ); diff --git a/tests/integration/autoloader/autoload-without-composer/vendor/autoload.php b/tests/integration/autoloader/autoload-without-composer/vendor/autoload.php new file mode 100644 index 000000000..0a6947fef --- /dev/null +++ b/tests/integration/autoloader/autoload-without-composer/vendor/autoload.php @@ -0,0 +1,12 @@ + 1) { + $installedVersions = $argv[1]; +} + +include $installedVersions; +if ($argc > 2) { + $installed = Composer\InstalledVersions::getAllRawData(); + $package = $argv[2]; + $version = ltrim($installed[0]['versions'][$package]['pretty_version'], 'v'); + echo "$package => $version\n"; +} else { + Composer\InstalledVersions::show(); +} diff --git a/tests/integration/autoloader/packages-with-broken-composer-00/vendor/autoload.php b/tests/integration/autoloader/packages-with-broken-composer-00/vendor/autoload.php new file mode 100644 index 000000000..01d206c0f --- /dev/null +++ b/tests/integration/autoloader/packages-with-broken-composer-00/vendor/autoload.php @@ -0,0 +1,12 @@ + $info) { + $version = ltrim($info['pretty_version'], 'v'); + echo "$package => $version\n"; + } + } +} diff --git a/tests/integration/autoloader/packages-with-broken-composer-00/vendor/composer/autoload_real.php b/tests/integration/autoloader/packages-with-broken-composer-00/vendor/composer/autoload_real.php new file mode 100644 index 000000000..014c86e9a --- /dev/null +++ b/tests/integration/autoloader/packages-with-broken-composer-00/vendor/composer/autoload_real.php @@ -0,0 +1,9 @@ + array( + 'pretty_version' => 'v1.0.0', + 'version' => '1.0.0.0', + 'type' => 'project' + ), + // Mocked data: installed packages and their versions + 'versions' => array( + 'vendor1/package1' => array( + 'pretty_version' => 'v1.1.3', + 'version' => '1.1.3.0', + 'type' => 'library' + ), + 'vendor2/package2' => array( + 'pretty_version' => '2.1.5', + 'version' => '2.1.5.0', + 'type' => 'library' + ), + 'symfony/http-kernel' => array( + 'pretty_version' => '5.4.5', + 'version' => '5.4.5.0', + 'type' => 'library' + ), + ) + ); diff --git a/tests/integration/autoloader/packages-with-broken-composer-00/vendor/symfony/http-kernel/HttpKernel.php b/tests/integration/autoloader/packages-with-broken-composer-00/vendor/symfony/http-kernel/HttpKernel.php new file mode 100644 index 000000000..2cceb6ac7 --- /dev/null +++ b/tests/integration/autoloader/packages-with-broken-composer-00/vendor/symfony/http-kernel/HttpKernel.php @@ -0,0 +1,12 @@ + $info) { + $version = ltrim($info['pretty_version'], 'v'); + echo "$package => $version\n"; + } + } +} diff --git a/tests/integration/autoloader/packages-with-broken-composer-01/vendor/composer/autoload_real.php b/tests/integration/autoloader/packages-with-broken-composer-01/vendor/composer/autoload_real.php new file mode 100644 index 000000000..014c86e9a --- /dev/null +++ b/tests/integration/autoloader/packages-with-broken-composer-01/vendor/composer/autoload_real.php @@ -0,0 +1,9 @@ + array( + 'pretty_version' => 'v1.0.0', + 'version' => '1.0.0.0', + 'type' => 'project' + ), + // Mocked data: installed packages and their versions + 'versions' => array( + 'vendor1/package1' => array( + 'pretty_version' => 'v1.1.3', + 'version' => '1.1.3.0', + 'type' => 'library' + ), + 'vendor2/package2' => array( + 'pretty_version' => '2.1.5', + 'version' => '2.1.5.0', + 'type' => 'library' + ), + 'laravel/framework' => array( + 'pretty_version' => '11.4.5', + 'version' => '11.4.5', + 'type' => 'library' + ), + ) + ); diff --git a/tests/integration/autoloader/packages-with-broken-composer-01/vendor/laravel/framework/src/Illuminate/Foundation/Application.php b/tests/integration/autoloader/packages-with-broken-composer-01/vendor/laravel/framework/src/Illuminate/Foundation/Application.php new file mode 100644 index 000000000..3c6718e89 --- /dev/null +++ b/tests/integration/autoloader/packages-with-broken-composer-01/vendor/laravel/framework/src/Illuminate/Foundation/Application.php @@ -0,0 +1,25 @@ + $info) { + if (!is_string($package)) { + continue; + } + if (!is_array($info)) { + continue; + } + if (!array_key_exists('pretty_version', $info)) { + continue; + } + if (!is_string($info['pretty_version'])) { + continue; + } + $version = ltrim($info['pretty_version'], 'v'); + echo "$package => $version\n"; + } + } +} diff --git a/tests/integration/autoloader/packages-with-broken-composer-02/vendor/composer/autoload_real.php b/tests/integration/autoloader/packages-with-broken-composer-02/vendor/composer/autoload_real.php new file mode 100644 index 000000000..014c86e9a --- /dev/null +++ b/tests/integration/autoloader/packages-with-broken-composer-02/vendor/composer/autoload_real.php @@ -0,0 +1,9 @@ + array( + 'pretty_version' => 'v1.0.0', + 'version' => '1.0.0.0', + 'type' => 'project' + ), + // Mocked invalid package data: + // - package without name and version + // - package without name but with version + // - package with name but without version + // Mocked valid package data: + // - package with name and version + 'versions' => array( + array( + 'version' => '1.1.3.0', + 'type' => 'library' + ), + array( + 'pretty_version' => 'v2.1.3', + 'version' => '2.1.3.0', + 'type' => 'library' + ), + 'vendor2/package2' => array( + 'version' => '3.1.5.0', + 'type' => 'library' + ), + 'laravel/framework' => array( + 'pretty_version' => '11.4.5', + 'version' => '11.4.5', + 'type' => 'library' + ), + ) + ); diff --git a/tests/integration/autoloader/packages-with-broken-composer-02/vendor/laravel/framework/src/Illuminate/Foundation/Application.php b/tests/integration/autoloader/packages-with-broken-composer-02/vendor/laravel/framework/src/Illuminate/Foundation/Application.php new file mode 100644 index 000000000..3c6718e89 --- /dev/null +++ b/tests/integration/autoloader/packages-with-broken-composer-02/vendor/laravel/framework/src/Illuminate/Foundation/Application.php @@ -0,0 +1,25 @@ +=")) { + die("skip: PHP >= 8.4.0 not supported\n"); +} */ /*INI diff --git a/tests/integration/errors/test_E_DEPRECATED_2.php84.php b/tests/integration/errors/test_E_DEPRECATED_2.php84.php new file mode 100644 index 000000000..afb0b9ca6 --- /dev/null +++ b/tests/integration/errors/test_E_DEPRECATED_2.php84.php @@ -0,0 +1,81 @@ +=")) { + die("skip: E_USER_ERROR deprecated in PHP 8.4\n"); +} +*/ + /*INI display_errors=1 log_errors=0 diff --git a/tests/integration/errors/test_uncaught_handled_exception_02.php b/tests/integration/errors/test_uncaught_handled_exception_02.php index 5fc475dca..059686adb 100644 --- a/tests/integration/errors/test_uncaught_handled_exception_02.php +++ b/tests/integration/errors/test_uncaught_handled_exception_02.php @@ -26,6 +26,9 @@ if (version_compare(PHP_VERSION, "8.0", "<")) { die("skip: PHP < 8.0.0 not supported\n"); } +if (version_compare(PHP_VERSION, "8.4", ">=")) { + die("skip: newer test for PHP 8.4+\n"); +} */ diff --git a/tests/integration/errors/test_uncaught_handled_exception_02.php84.php b/tests/integration/errors/test_uncaught_handled_exception_02.php84.php new file mode 100644 index 000000000..01a5df516 --- /dev/null +++ b/tests/integration/errors/test_uncaught_handled_exception_02.php84.php @@ -0,0 +1,169 @@ +query('select * from tables limit 1;'); + $result = $conn->query('select * from information_schema.tables limit 1;'); } test_slow_sql(); diff --git a/tests/integration/external/curl_exec/test_dt_simple.php b/tests/integration/external/curl_exec/test_dt_simple.php index 5574bcf38..b7009267a 100644 --- a/tests/integration/external/curl_exec/test_dt_simple.php +++ b/tests/integration/external/curl_exec/test_dt_simple.php @@ -28,6 +28,36 @@ /*EXPECT_RESPONSE_HEADERS */ +/*EXPECT_ANALYTICS_EVENTS +[ + "?? agent run id", + { + "reservoir_size": 50, + "events_seen": 1 + }, + [ + [ + { + "type": "Transaction", + "name": "OtherTransaction\/php__FILE__", + "timestamp": "??", + "duration": "??", + "totalTime": "??", + "externalDuration": "??", + "externalCallCount": 1, + "guid": "??", + "sampled": true, + "priority": "??", + "traceId": "??", + "error": false + }, + {}, + {} + ] + ] +] +*/ + /*EXPECT_TRACED_ERRORS null */ diff --git a/tests/integration/external/guzzle7/skipif.inc b/tests/integration/external/guzzle7/skipif.inc index 91ee13a0b..e8ede4887 100644 --- a/tests/integration/external/guzzle7/skipif.inc +++ b/tests/integration/external/guzzle7/skipif.inc @@ -6,8 +6,8 @@ require_once(realpath(dirname(__FILE__)) . '/../../../include/unpack_guzzle.php'); -if (version_compare(phpversion(), '7.2.5', '<')) { - die("skip: PHP >= 7.2.5 required\n"); +if (version_compare(phpversion(), '8.1', '<')) { + die("skip: PHP >= 8.1 required\n"); } /* diff --git a/tests/integration/external/guzzle7/test_spans_are_created_correctly.php b/tests/integration/external/guzzle7/test_spans_are_created_correctly.php index 87388da64..4ec83c617 100644 --- a/tests/integration/external/guzzle7/test_spans_are_created_correctly.php +++ b/tests/integration/external/guzzle7/test_spans_are_created_correctly.php @@ -19,6 +19,35 @@ newrelic.transaction_tracer.detail = 0 */ +/*EXPECT_ANALYTICS_EVENTS +[ + "?? agent run id", + { + "reservoir_size": 50, + "events_seen": 1 + }, + [ + [ + { + "type": "Transaction", + "name": "OtherTransaction\/php__FILE__", + "timestamp": "??", + "duration": "??", + "totalTime": "??", + "externalDuration": "??", + "externalCallCount": 1, + "guid": "??", + "sampled": true, + "priority": "??", + "traceId": "??", + "error": false + }, + {}, + {} + ] + ] +] +*/ /*EXPECT_SPAN_EVENTS_LIKE [ diff --git a/tests/integration/external/guzzle7/test_uncaught_bad_response_exception_sync.php b/tests/integration/external/guzzle7/test_uncaught_bad_response_exception_sync.php index 88e2acdaf..98172f8ef 100644 --- a/tests/integration/external/guzzle7/test_uncaught_bad_response_exception_sync.php +++ b/tests/integration/external/guzzle7/test_uncaught_bad_response_exception_sync.php @@ -81,6 +81,39 @@ ] */ +/*EXPECT_ANALYTICS_EVENTS +[ + "?? agent run id", + { + "reservoir_size": 50, + "events_seen": 1 + }, + [ + [ + { + "type": "Transaction", + "name": "OtherTransaction\/php__FILE__", + "timestamp": "??", + "duration": "??", + "totalTime": "??", + "externalDuration": "??", + "externalCallCount": 1, + "guid": "??", + "sampled": true, + "priority": "??", + "traceId": "??", + "error": true + }, + {}, + { + "errorType": "GuzzleHttp\\Exception\\BadResponseException", + "errorMessage": "Uncaught exception 'GuzzleHttp\\Exception\\BadResponseException' with message 'ClientException' in __FILE__:??" + } + ] + ] +] +*/ + /*EXPECT_TRACED_ERRORS [ "?? agent run id", 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 index bca70be7e..0c113528a 100644 --- 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 @@ -15,6 +15,9 @@ =")) { + die("skip: newer test for PHP 8.4+\n"); +} */ diff --git a/tests/integration/jit/function/test_span_events_are_created_upon_caught_error.php84.php b/tests/integration/jit/function/test_span_events_are_created_upon_caught_error.php84.php new file mode 100644 index 000000000..f5ef9e41e --- /dev/null +++ b/tests/integration/jit/function/test_span_events_are_created_upon_caught_error.php84.php @@ -0,0 +1,196 @@ + '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 index c3f064b6d..cc1fc14b3 100644 --- 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 @@ -13,6 +13,9 @@ =")) { + die("skip: newer test for PHPs 8.4+\n"); +} + +*/ + +/*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_SPAN_EVENTS +[ + "?? agent run id", + { + "reservoir_size": 10000, + "events_seen": 3 + }, + [ + [ + { + "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": "foo", + "error.class": "E_USER_ERROR", + "code.lineno": "??", + "code.filepath": "__FILE__", + "code.function": "??" + } + ] + ] +] +*/ + +/*EXPECT_REGEX +^\s*(PHP )?Fatal error:\s*foo in .*? on line [0-9]+\s*$ +*/ + +function a() +{ + time_nanosleep(0, 100000000); + trigger_error('foo', E_USER_ERROR); +} + +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_handled_exception.php b/tests/integration/jit/function/test_span_events_are_created_upon_uncaught_handled_exception.php index 2381e2045..7624a2a4b 100644 --- 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 @@ -15,6 +15,9 @@ =")) { + die("skip: newer test for PHP 8.4+\n"); +} */ diff --git a/tests/integration/jit/function/test_span_events_are_created_upon_uncaught_handled_exception.php84.php b/tests/integration/jit/function/test_span_events_are_created_upon_uncaught_handled_exception.php84.php new file mode 100644 index 000000000..28099b17c --- /dev/null +++ b/tests/integration/jit/function/test_span_events_are_created_upon_uncaught_handled_exception.php84.php @@ -0,0 +1,200 @@ + 'FakeDB', + ) +); +a(); + +echo 'this should never be printed'; 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 index ebde4a26b..74e03bf46 100644 --- 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 @@ -15,6 +15,9 @@ =")) { + die("skip: newer test for PHP 8.4+\n"); +} */ diff --git a/tests/integration/jit/tracing/test_span_events_are_created_upon_caught_error.php84.php b/tests/integration/jit/tracing/test_span_events_are_created_upon_caught_error.php84.php new file mode 100644 index 000000000..eeb18dc14 --- /dev/null +++ b/tests/integration/jit/tracing/test_span_events_are_created_upon_caught_error.php84.php @@ -0,0 +1,195 @@ + '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 index ee1a578ae..51f8b899a 100644 --- 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 @@ -13,6 +13,9 @@ =")) { + die("skip: newer test for PHPs 8.4+\n"); +} */ diff --git a/tests/integration/jit/tracing/test_span_events_are_created_upon_uncaught_error.php84.php b/tests/integration/jit/tracing/test_span_events_are_created_upon_uncaught_error.php84.php new file mode 100644 index 000000000..54660f8a3 --- /dev/null +++ b/tests/integration/jit/tracing/test_span_events_are_created_upon_uncaught_error.php84.php @@ -0,0 +1,135 @@ + 'FakeDB', + ) +); +a(); 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 index 395b1231f..7444c28e7 100644 --- 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 @@ -15,6 +15,9 @@ =")) { + die("skip: newer test for PHP 8.4+\n"); +} */ diff --git a/tests/integration/jit/tracing/test_span_events_are_created_upon_uncaught_handled_exception.php84.php b/tests/integration/jit/tracing/test_span_events_are_created_upon_uncaught_handled_exception.php84.php new file mode 100644 index 000000000..922a0c5a7 --- /dev/null +++ b/tests/integration/jit/tracing/test_span_events_are_created_upon_uncaught_handled_exception.php84.php @@ -0,0 +1,197 @@ + 'FakeDB', + ) +); +a(); + +echo 'this should never be printed'; diff --git a/tests/integration/logging/analog/test_supportability_metric.php b/tests/integration/logging/analog/test_supportability_metric.php deleted file mode 100644 index f3cfb4477..000000000 --- a/tests/integration/logging/analog/test_supportability_metric.php +++ /dev/null @@ -1,25 +0,0 @@ -emergency("emergency"); } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/logging/monolog2/test_monolog_cat.php b/tests/integration/logging/monolog2/test_monolog_cat.php index d96e38aec..b192b9ce9 100644 --- a/tests/integration/logging/monolog2/test_monolog_cat.php +++ b/tests/integration/logging/monolog2/test_monolog_cat.php @@ -56,7 +56,7 @@ [{"name": "OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], [{"name": "OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [8, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], @@ -183,4 +183,4 @@ function test_logging() { $logger->emergency("emergency"); } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/logging/monolog2/test_monolog_context_simple.php b/tests/integration/logging/monolog2/test_monolog_context_simple.php index 769694845..a0f0b7260 100644 --- a/tests/integration/logging/monolog2/test_monolog_context_simple.php +++ b/tests/integration/logging/monolog2/test_monolog_context_simple.php @@ -59,7 +59,7 @@ [{"name": "OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], [{"name": "OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [8, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], diff --git a/tests/integration/logging/monolog2/test_monolog_decoration_and_forwarding.php b/tests/integration/logging/monolog2/test_monolog_decoration_and_forwarding.php index 1737a919c..82ad4be61 100644 --- a/tests/integration/logging/monolog2/test_monolog_decoration_and_forwarding.php +++ b/tests/integration/logging/monolog2/test_monolog_decoration_and_forwarding.php @@ -113,7 +113,7 @@ [{"name": "OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/api/get_linking_metadata"}, [16, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [8, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/LocalDecorating/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], @@ -257,4 +257,4 @@ function test_logging() { $logger->emergency("emergency"); } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/logging/monolog2/test_monolog_disable_metrics.php b/tests/integration/logging/monolog2/test_monolog_disable_metrics.php index 95b84d636..eaed5d5d4 100644 --- a/tests/integration/logging/monolog2/test_monolog_disable_metrics.php +++ b/tests/integration/logging/monolog2/test_monolog_disable_metrics.php @@ -51,7 +51,7 @@ [{"name": "OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], [{"name": "OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [8, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], @@ -153,4 +153,4 @@ function test_logging() { $logger->emergency("emergency"); } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/logging/monolog2/test_monolog_drop_empty.php b/tests/integration/logging/monolog2/test_monolog_drop_empty.php index 8139e3606..5d48030ca 100644 --- a/tests/integration/logging/monolog2/test_monolog_drop_empty.php +++ b/tests/integration/logging/monolog2/test_monolog_drop_empty.php @@ -57,7 +57,7 @@ [{"name": "OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], [{"name": "OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [8, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], @@ -171,4 +171,4 @@ function test_logging() { $logger->emergency("emergency"); } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/logging/monolog2/test_monolog_large_message_limit.php b/tests/integration/logging/monolog2/test_monolog_large_message_limit.php index d7c9b677a..7a0e1eacd 100644 --- a/tests/integration/logging/monolog2/test_monolog_large_message_limit.php +++ b/tests/integration/logging/monolog2/test_monolog_large_message_limit.php @@ -40,7 +40,7 @@ [{"name": "OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], [{"name": "OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [833, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], @@ -78,4 +78,4 @@ function test_logging() { } } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/logging/monolog2/test_monolog_large_message_limit_drops.php b/tests/integration/logging/monolog2/test_monolog_large_message_limit_drops.php index 0dfe1dc2d..313e73edd 100644 --- a/tests/integration/logging/monolog2/test_monolog_large_message_limit_drops.php +++ b/tests/integration/logging/monolog2/test_monolog_large_message_limit_drops.php @@ -40,7 +40,7 @@ [{"name": "OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], [{"name": "OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [1666, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], @@ -78,4 +78,4 @@ function test_logging() { } } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/logging/monolog2/test_monolog_limit_log_events.php b/tests/integration/logging/monolog2/test_monolog_limit_log_events.php index c9106b2ea..ccc75de41 100644 --- a/tests/integration/logging/monolog2/test_monolog_limit_log_events.php +++ b/tests/integration/logging/monolog2/test_monolog_limit_log_events.php @@ -58,7 +58,7 @@ [{"name": "OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], [{"name": "OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [8, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], @@ -161,4 +161,4 @@ function test_logging() { $logger->emergency("emergency"); } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/logging/monolog2/test_monolog_limit_zero_events.php b/tests/integration/logging/monolog2/test_monolog_limit_zero_events.php index 6ff0fa7d1..573850d3a 100644 --- a/tests/integration/logging/monolog2/test_monolog_limit_zero_events.php +++ b/tests/integration/logging/monolog2/test_monolog_limit_zero_events.php @@ -58,7 +58,7 @@ [{"name": "OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], [{"name": "OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [8, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], @@ -101,4 +101,4 @@ function test_logging() { $logger->emergency("emergency"); } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/logging/monolog2/test_monolog_log_events_max_samples_stored_invalid1.php b/tests/integration/logging/monolog2/test_monolog_log_events_max_samples_stored_invalid1.php index f760b19ac..c346aa55e 100644 --- a/tests/integration/logging/monolog2/test_monolog_log_events_max_samples_stored_invalid1.php +++ b/tests/integration/logging/monolog2/test_monolog_log_events_max_samples_stored_invalid1.php @@ -45,7 +45,7 @@ [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [833, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]] ] ] @@ -81,4 +81,4 @@ function test_logging() { } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/logging/monolog2/test_monolog_log_events_max_samples_stored_invalid2.php b/tests/integration/logging/monolog2/test_monolog_log_events_max_samples_stored_invalid2.php index ada730ed7..ab55e0359 100644 --- a/tests/integration/logging/monolog2/test_monolog_log_events_max_samples_stored_invalid2.php +++ b/tests/integration/logging/monolog2/test_monolog_log_events_max_samples_stored_invalid2.php @@ -45,7 +45,7 @@ [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [833, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]] ] ] @@ -81,4 +81,4 @@ function test_logging() { } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/logging/monolog2/test_monolog_log_events_max_samples_stored_invalid3.php b/tests/integration/logging/monolog2/test_monolog_log_events_max_samples_stored_invalid3.php index cdd99bc5f..616eb81f8 100644 --- a/tests/integration/logging/monolog2/test_monolog_log_events_max_samples_stored_invalid3.php +++ b/tests/integration/logging/monolog2/test_monolog_log_events_max_samples_stored_invalid3.php @@ -45,7 +45,7 @@ [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [833, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]] ] ] @@ -81,4 +81,4 @@ function test_logging() { } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/logging/monolog2/test_monolog_log_events_max_samples_stored_invalid4.php b/tests/integration/logging/monolog2/test_monolog_log_events_max_samples_stored_invalid4.php index 354108a09..0f13144eb 100644 --- a/tests/integration/logging/monolog2/test_monolog_log_events_max_samples_stored_invalid4.php +++ b/tests/integration/logging/monolog2/test_monolog_log_events_max_samples_stored_invalid4.php @@ -45,7 +45,7 @@ [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [833, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]] ] ] @@ -81,4 +81,4 @@ function test_logging() { } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/logging/monolog2/test_monolog_log_level_filter.php b/tests/integration/logging/monolog2/test_monolog_log_level_filter.php index 939dd9cf5..fa7902395 100644 --- a/tests/integration/logging/monolog2/test_monolog_log_level_filter.php +++ b/tests/integration/logging/monolog2/test_monolog_log_level_filter.php @@ -60,7 +60,7 @@ [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [8, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]] ] ] @@ -171,4 +171,4 @@ function test_logging() { $logger->emergency("emergency"); } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/logging/monolog2/test_monolog_log_level_filter_invalid.php b/tests/integration/logging/monolog2/test_monolog_log_level_filter_invalid.php index d2eac9410..22107fd98 100644 --- a/tests/integration/logging/monolog2/test_monolog_log_level_filter_invalid.php +++ b/tests/integration/logging/monolog2/test_monolog_log_level_filter_invalid.php @@ -61,7 +61,7 @@ [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [8, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/2/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]] ] ] @@ -172,4 +172,4 @@ function test_logging() { $logger->emergency("emergency"); } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/logging/monolog3/test_monolog_basic.php b/tests/integration/logging/monolog3/test_monolog_basic.php index 5cbc3a1da..0e494b078 100644 --- a/tests/integration/logging/monolog3/test_monolog_basic.php +++ b/tests/integration/logging/monolog3/test_monolog_basic.php @@ -56,7 +56,7 @@ [{"name": "OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], [{"name": "OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [8, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], @@ -200,4 +200,4 @@ function test_logging() { $logger->emergency("emergency"); } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/logging/monolog3/test_monolog_cat.php b/tests/integration/logging/monolog3/test_monolog_cat.php index dcf557213..12b0c76ae 100644 --- a/tests/integration/logging/monolog3/test_monolog_cat.php +++ b/tests/integration/logging/monolog3/test_monolog_cat.php @@ -56,7 +56,7 @@ [{"name": "OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], [{"name": "OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [8, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], @@ -184,4 +184,4 @@ function test_logging() { $logger->emergency("emergency"); } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/logging/monolog3/test_monolog_context_simple.php b/tests/integration/logging/monolog3/test_monolog_context_simple.php index 1e77e6d25..ddcfd65f9 100644 --- a/tests/integration/logging/monolog3/test_monolog_context_simple.php +++ b/tests/integration/logging/monolog3/test_monolog_context_simple.php @@ -59,7 +59,7 @@ [{"name": "OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], [{"name": "OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [8, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], diff --git a/tests/integration/logging/monolog3/test_monolog_decoration_and_forwarding.php b/tests/integration/logging/monolog3/test_monolog_decoration_and_forwarding.php index fa0bbb6cb..3d8edcc7c 100644 --- a/tests/integration/logging/monolog3/test_monolog_decoration_and_forwarding.php +++ b/tests/integration/logging/monolog3/test_monolog_decoration_and_forwarding.php @@ -113,7 +113,7 @@ [{"name": "OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/api/get_linking_metadata"}, [16, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [8, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/LocalDecorating/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], @@ -257,4 +257,4 @@ function test_logging() { $logger->emergency("emergency"); } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/logging/monolog3/test_monolog_disable_metrics.php b/tests/integration/logging/monolog3/test_monolog_disable_metrics.php index cb5beb8e1..11bad2177 100644 --- a/tests/integration/logging/monolog3/test_monolog_disable_metrics.php +++ b/tests/integration/logging/monolog3/test_monolog_disable_metrics.php @@ -51,7 +51,7 @@ [{"name": "OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], [{"name": "OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [8, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], @@ -153,4 +153,4 @@ function test_logging() { $logger->emergency("emergency"); } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/logging/monolog3/test_monolog_drop_empty.php b/tests/integration/logging/monolog3/test_monolog_drop_empty.php index 61937fb9f..6a470653b 100644 --- a/tests/integration/logging/monolog3/test_monolog_drop_empty.php +++ b/tests/integration/logging/monolog3/test_monolog_drop_empty.php @@ -57,7 +57,7 @@ [{"name": "OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], [{"name": "OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [8, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], @@ -171,4 +171,4 @@ function test_logging() { $logger->emergency("emergency"); } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/logging/monolog3/test_monolog_large_message_limit.php b/tests/integration/logging/monolog3/test_monolog_large_message_limit.php index 191800f49..50b2a26a5 100644 --- a/tests/integration/logging/monolog3/test_monolog_large_message_limit.php +++ b/tests/integration/logging/monolog3/test_monolog_large_message_limit.php @@ -40,7 +40,7 @@ [{"name": "OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], [{"name": "OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [833, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], @@ -78,4 +78,4 @@ function test_logging() { } } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/logging/monolog3/test_monolog_large_message_limit_drops.php b/tests/integration/logging/monolog3/test_monolog_large_message_limit_drops.php index eed9a410a..c2913419d 100644 --- a/tests/integration/logging/monolog3/test_monolog_large_message_limit_drops.php +++ b/tests/integration/logging/monolog3/test_monolog_large_message_limit_drops.php @@ -40,7 +40,7 @@ [{"name": "OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], [{"name": "OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [1666, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], @@ -78,4 +78,4 @@ function test_logging() { } } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/logging/monolog3/test_monolog_limit_log_events.php b/tests/integration/logging/monolog3/test_monolog_limit_log_events.php index b1f72e6b6..b5ffaf258 100644 --- a/tests/integration/logging/monolog3/test_monolog_limit_log_events.php +++ b/tests/integration/logging/monolog3/test_monolog_limit_log_events.php @@ -58,7 +58,7 @@ [{"name": "OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], [{"name": "OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [8, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], @@ -161,4 +161,4 @@ function test_logging() { $logger->emergency("emergency"); } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/logging/monolog3/test_monolog_limit_zero_events.php b/tests/integration/logging/monolog3/test_monolog_limit_zero_events.php index 38abfe890..4b65d64f3 100644 --- a/tests/integration/logging/monolog3/test_monolog_limit_zero_events.php +++ b/tests/integration/logging/monolog3/test_monolog_limit_zero_events.php @@ -58,7 +58,7 @@ [{"name": "OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], [{"name": "OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [8, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], @@ -101,4 +101,4 @@ function test_logging() { $logger->emergency("emergency"); } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/logging/monolog3/test_monolog_log_events_max_samples_stored_invalid1.php b/tests/integration/logging/monolog3/test_monolog_log_events_max_samples_stored_invalid1.php index f33d832af..f94ea94af 100644 --- a/tests/integration/logging/monolog3/test_monolog_log_events_max_samples_stored_invalid1.php +++ b/tests/integration/logging/monolog3/test_monolog_log_events_max_samples_stored_invalid1.php @@ -45,7 +45,7 @@ [{"name": "Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [833, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]] ] ] @@ -81,4 +81,4 @@ function test_logging() { } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/logging/monolog3/test_monolog_log_events_max_samples_stored_invalid2.php b/tests/integration/logging/monolog3/test_monolog_log_events_max_samples_stored_invalid2.php index 73c70e58e..5d0363243 100644 --- a/tests/integration/logging/monolog3/test_monolog_log_events_max_samples_stored_invalid2.php +++ b/tests/integration/logging/monolog3/test_monolog_log_events_max_samples_stored_invalid2.php @@ -45,7 +45,7 @@ [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [833, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]] ] ] @@ -81,4 +81,4 @@ function test_logging() { } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/logging/monolog3/test_monolog_log_events_max_samples_stored_invalid3.php b/tests/integration/logging/monolog3/test_monolog_log_events_max_samples_stored_invalid3.php index 72ca4b06b..8b451bdea 100644 --- a/tests/integration/logging/monolog3/test_monolog_log_events_max_samples_stored_invalid3.php +++ b/tests/integration/logging/monolog3/test_monolog_log_events_max_samples_stored_invalid3.php @@ -45,7 +45,7 @@ [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [833, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]] ] ] @@ -81,4 +81,4 @@ function test_logging() { } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/logging/monolog3/test_monolog_log_events_max_samples_stored_invalid4.php b/tests/integration/logging/monolog3/test_monolog_log_events_max_samples_stored_invalid4.php index 2f60d6bc1..fe4185197 100644 --- a/tests/integration/logging/monolog3/test_monolog_log_events_max_samples_stored_invalid4.php +++ b/tests/integration/logging/monolog3/test_monolog_log_events_max_samples_stored_invalid4.php @@ -45,7 +45,7 @@ [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [833, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]] ] ] @@ -81,4 +81,4 @@ function test_logging() { } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/logging/monolog3/test_monolog_log_level_filter.php b/tests/integration/logging/monolog3/test_monolog_log_level_filter.php index 529ef1459..69d482e9b 100644 --- a/tests/integration/logging/monolog3/test_monolog_log_level_filter.php +++ b/tests/integration/logging/monolog3/test_monolog_log_level_filter.php @@ -60,7 +60,7 @@ [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [8, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]] ] ] @@ -171,4 +171,4 @@ function test_logging() { $logger->emergency("emergency"); } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/logging/monolog3/test_monolog_log_level_filter_invalid.php b/tests/integration/logging/monolog3/test_monolog_log_level_filter_invalid.php index e5387616f..649cd5acf 100644 --- a/tests/integration/logging/monolog3/test_monolog_log_level_filter_invalid.php +++ b/tests/integration/logging/monolog3/test_monolog_log_level_filter_invalid.php @@ -61,7 +61,7 @@ [{"name": "Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/Logging/PHP/Monolog/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [8, "??", "??", "??", "??", "??"]], + [{"name": "Supportability/PHP/package/monolog/monolog/3/detected"}, [1, "??", "??", "??", "??", "??"]], [{"name": "Supportability/library/Monolog/detected"}, [1, "??", "??", "??", "??", "??"]] ] ] @@ -172,4 +172,4 @@ function test_logging() { $logger->emergency("emergency"); } -test_logging(); \ No newline at end of file +test_logging(); diff --git a/tests/integration/memcached/test_add_servers.php b/tests/integration/memcached/test_add_servers.php new file mode 100644 index 000000000..41e9b5a63 --- /dev/null +++ b/tests/integration/memcached/test_add_servers.php @@ -0,0 +1,39 @@ + +*/ + +/*INI +*/ + +/*EXPECT_METRICS_EXIST +Datastore/instance/Memcached/host1/1, 1 +Datastore/instance/Memcached/host2/2, 1 +Datastore/instance/Memcached/host3/11211, 1 +Datastore/instance/Memcached/host4/1, 1 +*/ + +/*EXPECT_ERROR_EVENTS null */ + +require_once(realpath (dirname ( __FILE__ )) . '/../../include/helpers.php'); +require_once(realpath (dirname ( __FILE__ )) . '/../../include/tap.php'); +require_once(realpath (dirname ( __FILE__ )) . '/memcache.inc'); + +$memcached = new Memcached(); +$memcached->addServers(array( + array("host1", 1), + array("host2", 2), + array("host3", 11211))); +$memcached->addServers(array()); +$memcached->addServers(array(array("host4", 1, "test field"))); +$memcached->quit(); diff --git a/tests/integration/memcached/test_add_servers_bad.php b/tests/integration/memcached/test_add_servers_bad.php new file mode 100644 index 000000000..c4887584f --- /dev/null +++ b/tests/integration/memcached/test_add_servers_bad.php @@ -0,0 +1,68 @@ + +*/ + +/*INI +*/ + +/*EXPECT_REGEX + +.*(PHP )?Warning:.*could not add entry.* + +.*(PHP )?Warning:.*could not add entry.* + +*/ + +/*EXPECT_ERROR_EVENTS +[ + "?? agent run id", + { + "reservoir_size": "??", + "events_seen": 1 + }, + [ + [ + { + "type": "TransactionError", + "timestamp": "??", + "error.class": "E_WARNING", + "error.message": "Memcached::addServers(): could not add entry #2 to the server list", + "transactionName": "OtherTransaction\/php__FILE__", + "duration": "??", + "nr.transactionGuid": "??", + "guid": "??", + "sampled": true, + "priority": "??", + "traceId": "??", + "spanId": "??" + }, + {}, + {} + ] + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../include/helpers.php'); +require_once(realpath (dirname ( __FILE__ )) . '/../../include/tap.php'); +require_once(realpath (dirname ( __FILE__ )) . '/memcache.inc'); + +$memcached = new Memcached(); +$memcached->addServer(5, 5); +//$memcached->addServer("host", string); crashes PHP +$memcached->addServers(array(array(1))); +$memcached->addServers(array(array("host1"))); +$memcached->addServers(array(array(1, "host1"))); +//$memcahed->addServers("string"); crashes PHP +$memcached->quit(); diff --git a/tests/integration/memcached/test_basic.php b/tests/integration/memcached/test_basic.php index 6ccde57b7..1e24bfd7e 100644 --- a/tests/integration/memcached/test_basic.php +++ b/tests/integration/memcached/test_basic.php @@ -46,6 +46,7 @@ [{"name":"Datastore/allOther"}, [15, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/all"}, [15, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/allOther"}, [15, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/instance/Memcached/ENV[MEMCACHE_HOST]/11211"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/add"}, [2, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/add", "scope":"OtherTransaction/php__FILE__"}, [2, "??", "??", "??", "??", "??"]], diff --git a/tests/integration/memcached/test_basic_logging_off.php b/tests/integration/memcached/test_basic_logging_off.php index 521fc1d44..05e87a4c1 100644 --- a/tests/integration/memcached/test_basic_logging_off.php +++ b/tests/integration/memcached/test_basic_logging_off.php @@ -49,6 +49,7 @@ [{"name":"Datastore/allOther"}, [15, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/all"}, [15, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/allOther"}, [15, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/instance/Memcached/ENV[MEMCACHE_HOST]/11211"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/add"}, [2, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/add", "scope":"OtherTransaction/php__FILE__"}, [2, "??", "??", "??", "??", "??"]], diff --git a/tests/integration/memcached/test_by_key.php b/tests/integration/memcached/test_by_key.php index 006d6cd7d..ceb2758d8 100644 --- a/tests/integration/memcached/test_by_key.php +++ b/tests/integration/memcached/test_by_key.php @@ -48,6 +48,7 @@ [{"name":"Datastore/allOther"}, [11, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/all"}, [11, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/allOther"}, [11, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/instance/Memcached/ENV[MEMCACHE_HOST]/11211"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/add"}, [2, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/add", "scope":"OtherTransaction/php__FILE__"}, [2, "??", "??", "??", "??", "??"]], diff --git a/tests/integration/memcached/test_cas.php7.php b/tests/integration/memcached/test_cas.php7.php index b69010e59..650bb22ec 100644 --- a/tests/integration/memcached/test_cas.php7.php +++ b/tests/integration/memcached/test_cas.php7.php @@ -42,6 +42,7 @@ [{"name":"Datastore/allOther"}, [5, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/all"}, [5, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/allOther"}, [5, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/instance/Memcached/ENV[MEMCACHE_HOST]/11211"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/delete"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/delete", "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], diff --git a/tests/integration/memcached/test_cas_by_key.php7.php b/tests/integration/memcached/test_cas_by_key.php7.php index f43a6dc29..10b073016 100644 --- a/tests/integration/memcached/test_cas_by_key.php7.php +++ b/tests/integration/memcached/test_cas_by_key.php7.php @@ -42,6 +42,7 @@ [{"name":"Datastore/allOther"}, [5, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/all"}, [5, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/allOther"}, [5, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/instance/Memcached/ENV[MEMCACHE_HOST]/11211"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/delete"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/delete", "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], diff --git a/tests/integration/memcached/test_concat.php b/tests/integration/memcached/test_concat.php index 092bd5683..0a526adcd 100644 --- a/tests/integration/memcached/test_concat.php +++ b/tests/integration/memcached/test_concat.php @@ -35,6 +35,7 @@ [{"name":"Datastore/allOther"}, [5, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/all"}, [5, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/allOther"}, [5, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/instance/Memcached/ENV[MEMCACHE_HOST]/11211"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/delete"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/delete", "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], diff --git a/tests/integration/memcached/test_concat_by_key.php b/tests/integration/memcached/test_concat_by_key.php index 7339eed12..2cb041974 100644 --- a/tests/integration/memcached/test_concat_by_key.php +++ b/tests/integration/memcached/test_concat_by_key.php @@ -35,6 +35,7 @@ [{"name":"Datastore/allOther"}, [5, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/all"}, [5, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/allOther"}, [5, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/instance/Memcached/ENV[MEMCACHE_HOST]/11211"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/delete"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/delete", "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], diff --git a/tests/integration/memcached/test_concat_by_key_logging_off.php b/tests/integration/memcached/test_concat_by_key_logging_off.php index d4e3f2d60..4d644a74d 100644 --- a/tests/integration/memcached/test_concat_by_key_logging_off.php +++ b/tests/integration/memcached/test_concat_by_key_logging_off.php @@ -38,6 +38,7 @@ [{"name":"Datastore/allOther"}, [5, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/all"}, [5, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/allOther"}, [5, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/instance/Memcached/ENV[MEMCACHE_HOST]/11211"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/delete"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/delete", "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], diff --git a/tests/integration/memcached/test_multi.php b/tests/integration/memcached/test_multi.php index 82328762a..78dbd2fa3 100644 --- a/tests/integration/memcached/test_multi.php +++ b/tests/integration/memcached/test_multi.php @@ -42,6 +42,7 @@ [{"name":"Datastore/allOther"}, [5, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/all"}, [5, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/allOther"}, [5, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/instance/Memcached/ENV[MEMCACHE_HOST]/11211"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/get"}, [4, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/get", "scope":"OtherTransaction/php__FILE__"}, [4, "??", "??", "??", "??", "??"]], diff --git a/tests/integration/memcached/test_multi_by_key.php b/tests/integration/memcached/test_multi_by_key.php index d5867c36d..5d4c2e9d3 100644 --- a/tests/integration/memcached/test_multi_by_key.php +++ b/tests/integration/memcached/test_multi_by_key.php @@ -42,6 +42,7 @@ [{"name":"Datastore/allOther"}, [5, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/all"}, [5, "??", "??", "??", "??", "??"]], [{"name":"Datastore/Memcached/allOther"}, [5, "??", "??", "??", "??", "??"]], + [{"name":"Datastore/instance/Memcached/ENV[MEMCACHE_HOST]/11211"}, [1, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/get"}, [4, "??", "??", "??", "??", "??"]], [{"name":"Datastore/operation/Memcached/get", "scope":"OtherTransaction/php__FILE__"}, [4, "??", "??", "??", "??", "??"]], diff --git a/tests/integration/memcached/test_socket.php b/tests/integration/memcached/test_socket.php new file mode 100644 index 000000000..19a1787e9 --- /dev/null +++ b/tests/integration/memcached/test_socket.php @@ -0,0 +1,30 @@ + +*/ + +/*INI +*/ + +/*EXPECT_METRICS_EXIST +Datastore/instance/Memcached/__HOST__/my/socket, 1 +*/ + +/*EXPECT_ERROR_EVENTS null */ + +require_once(realpath (dirname ( __FILE__ )) . '/../../include/helpers.php'); +require_once(realpath (dirname ( __FILE__ )) . '/../../include/tap.php'); +require_once(realpath (dirname ( __FILE__ )) . '/memcache.inc'); + +$memcached = new Memcached(); +$memcached->addServer("my/socket", 0); +$memcached->quit(); diff --git a/tests/integration/mongo/mongo.inc b/tests/integration/mongo/mongo.inc deleted file mode 100644 index ddc83282f..000000000 --- a/tests/integration/mongo/mongo.inc +++ /dev/null @@ -1,14 +0,0 @@ -close(); -} catch (MongoConnectionException $e) { - die('skip: ' . $e->getMessage() . "\n"); -} diff --git a/tests/integration/mongo/test_execute.php b/tests/integration/mongo/test_execute.php deleted file mode 100644 index 245e92036..000000000 --- a/tests/integration/mongo/test_execute.php +++ /dev/null @@ -1,55 +0,0 @@ - -*/ - -/*INI -*/ - -/*EXPECT_METRICS -[ - "?? agent run id", - "?? start time", - "?? stop time", - [ - [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/all"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/all"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/allOther"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/MongoDB/all"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/MongoDB/allOther"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/operation/MongoDB/execute"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/operation/MongoDB/execute", - "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"OtherTransaction/all"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]] - ] -] -*/ - - - - -/*EXPECT_TRACED_ERRORS -null -*/ - -require_once(realpath(dirname(__FILE__ )) . '/mongo.inc'); - -$client = new MongoClient(mongo_server()); -$db = $client->selectDB('test'); -$db->execute("17.0;"); diff --git a/tests/integration/mongo/test_execute_logging_off.php b/tests/integration/mongo/test_execute_logging_off.php deleted file mode 100644 index 29661c7c7..000000000 --- a/tests/integration/mongo/test_execute_logging_off.php +++ /dev/null @@ -1,56 +0,0 @@ - -*/ - -/*INI -newrelic.application_logging.enabled = false -newrelic.application_logging.forwarding.enabled = false -newrelic.application_logging.metrics.enabled = false -*/ - -/*EXPECT_METRICS -[ - "?? agent run id", - "?? start time", - "?? stop time", - [ - [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/all"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/all"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/allOther"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/MongoDB/all"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/MongoDB/allOther"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/operation/MongoDB/execute"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/operation/MongoDB/execute", - "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"OtherTransaction/all"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Supportability/Logging/Forwarding/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Supportability/Logging/Metrics/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]] - ] -] -*/ - - -/*EXPECT_TRACED_ERRORS -null -*/ - -require_once(realpath(dirname(__FILE__ )) . '/mongo.inc'); - -$client = new MongoClient(mongo_server()); -$db = $client->selectDB('test'); -$db->execute("17.0;"); diff --git a/tests/integration/mongo/test_find.php b/tests/integration/mongo/test_find.php deleted file mode 100644 index 00d023364..000000000 --- a/tests/integration/mongo/test_find.php +++ /dev/null @@ -1,71 +0,0 @@ - -*/ - -/*INI -*/ - -/*EXPECT_METRICS -[ - "?? agent run id", - "?? start time", - "?? stop time", - [ - [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/all"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"DurationByCaller/Unknown/Unknown/Unknown/Unknown/allOther"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/all"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/allOther"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/MongoDB/all"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/MongoDB/allOther"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/operation/MongoDB/find"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/statement/MongoDB/test.produce/find"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Datastore/statement/MongoDB/test.produce/find", - "scope":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"OtherTransaction/all"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"OtherTransaction/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"OtherTransactionTotalTime"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"OtherTransactionTotalTime/php__FILE__"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Supportability/Logging/Forwarding/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Supportability/Logging/Metrics/PHP/enabled"}, [1, "??", "??", "??", "??", "??"]], - [{"name":"Supportability/Logging/LocalDecorating/PHP/disabled"}, [1, "??", "??", "??", "??", "??"]] - ] -] -*/ - - - - -/*EXPECT_TRACED_ERRORS -null -*/ - -require_once(realpath (dirname ( __FILE__ )) . '/mongo.inc'); - -/* See http://php.net/manual/en/mongocollection.find.php */ -function test_find($db) { - $produce = new MongoCollection($db, 'produce'); - $fruitQuery = array('Type' => 'Fruit'); - $cursor = $produce->find($fruitQuery); - foreach ($cursor as $doc) { - var_dump($doc); - } -} - -function main() { - $client = new MongoClient(mongo_server()); - $db = $client->selectDB('test'); - - test_find($db); -} - -main(); diff --git a/tests/integration/mysqli/test_explain_connect_socket.php b/tests/integration/mysqli/test_explain_connect_socket.php new file mode 100644 index 000000000..0c56e3bc9 --- /dev/null +++ b/tests/integration/mysqli/test_explain_connect_socket.php @@ -0,0 +1,148 @@ +", + "?? SQL ID", + "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name=?", + "Datastore/statement/MySQL/tables/select", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "explain_plan": [ + [ + "id", + "select_type", + "table", + "type", + "possible_keys", + "key", + "key_len", + "ref", + "rows", + "Extra" + ], + [ + [ + 1, + "SIMPLE", + "tables", + "ALL", + null, + "TABLE_NAME", + null, + null, + null, + "Using where; Skip_open_table; Scanned 1 database" + ] + ] + ], + "backtrace": [ + " in mysqli_stmt_execute called at __FILE__ (??)", + " in test_prepare called at __FILE__ (??)" + ] + } + ] + ] +] +*/ + +/*EXPECT_TRACED_ERRORS +null +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../include/config.php'); + +function test_prepare($link) +{ + $query = "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name='STATISTICS'"; + + $stmt = mysqli_prepare($link, $query); + if (FALSE === $stmt) { + echo mysqli_error($link) . "\n"; + return; + } + + if (FALSE === mysqli_stmt_execute($stmt)) { + echo mysqli_stmt_error($stmt) . "\n"; + return; + } + + if (FALSE === mysqli_stmt_bind_result($stmt, $value)) { + echo mysqli_stmt_error($stmt) . "\n"; + return; + } + + while (mysqli_stmt_fetch($stmt)) { + echo $value . "\n"; + } + + mysqli_stmt_close($stmt); +} + +$link = mysqli_connect('localhost', $MYSQL_USER, $MYSQL_PASSWD, $MYSQL_DB, null, $MYSQL_SOCKET); +if (mysqli_connect_errno()) { + echo mysqli_connect_error() . "\n"; + exit(1); +} + +test_prepare($link); +mysqli_close($link); diff --git a/tests/integration/mysqli/test_explain_construct_socket.php b/tests/integration/mysqli/test_explain_construct_socket.php new file mode 100644 index 000000000..20c6e1481 --- /dev/null +++ b/tests/integration/mysqli/test_explain_construct_socket.php @@ -0,0 +1,150 @@ +", + "?? SQL ID", + "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name=?", + "Datastore/statement/MySQL/tables/select", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "explain_plan": [ + [ + "id", + "select_type", + "table", + "type", + "possible_keys", + "key", + "key_len", + "ref", + "rows", + "Extra" + ], + [ + [ + 1, + "SIMPLE", + "tables", + "ALL", + null, + "TABLE_NAME", + null, + null, + null, + "Using where; Skip_open_table; Scanned 1 database" + ] + ] + ], + "backtrace": [ + " in mysqli_stmt_execute called at __FILE__ (??)", + " in test_prepare called at __FILE__ (??)" + ] + } + ] + ] +] +*/ + +/*EXPECT_TRACED_ERRORS +null +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../include/config.php'); + +function test_prepare($link) +{ + + $query = "SELECT TABLE_NAME FROM information_schema.tables WHERE table_name='STATISTICS'"; + + $stmt = mysqli_prepare($link, $query); + if (FALSE === $stmt) { + echo mysqli_error($link) . "\n"; + return; + } + + if (FALSE === mysqli_stmt_execute($stmt)) { + echo mysqli_stmt_error($stmt) . "\n"; + return; + } + + if (FALSE === mysqli_stmt_bind_result($stmt, $value)) { + echo mysqli_stmt_error($stmt) . "\n"; + return; + } + + while (mysqli_stmt_fetch($stmt)) { + echo $value . "\n"; + } + + mysqli_stmt_close($stmt); +} + +$link = new mysqli('localhost', $MYSQL_USER, $MYSQL_PASSWD, $MYSQL_DB, null, $MYSQL_SOCKET); +if (mysqli_connect_errno()) { + echo mysqli_connect_error() . "\n"; + exit(1); +} + +test_prepare($link); +mysqli_close($link); 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 index d61b7fea0..ce62bfc12 100644 --- 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 @@ -15,6 +15,9 @@ =")) { + die("skip: newer test for PHP 8.4+\n"); +} */ diff --git a/tests/integration/opcache/disabled/test_span_events_are_created_upon_caught_error.php84.php b/tests/integration/opcache/disabled/test_span_events_are_created_upon_caught_error.php84.php new file mode 100644 index 000000000..86c24a776 --- /dev/null +++ b/tests/integration/opcache/disabled/test_span_events_are_created_upon_caught_error.php84.php @@ -0,0 +1,193 @@ + '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 index bc42257cc..49320f19b 100644 --- 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 @@ -13,6 +13,9 @@ =")) { + die("skip: newer test for PHPs 8.4+\n"); +} */ diff --git a/tests/integration/opcache/disabled/test_span_events_are_created_upon_uncaught_error.php84.php b/tests/integration/opcache/disabled/test_span_events_are_created_upon_uncaught_error.php84.php new file mode 100644 index 000000000..35014d54f --- /dev/null +++ b/tests/integration/opcache/disabled/test_span_events_are_created_upon_uncaught_error.php84.php @@ -0,0 +1,132 @@ + 'FakeDB', + ) +); +a(); 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 index d45a094b8..76dd4fe41 100644 --- 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 @@ -17,6 +17,9 @@ if (version_compare(PHP_VERSION, "8.0", "<")) { die("skip: PHP < 7.0.0 not supported\n"); } +if (version_compare(PHP_VERSION, "8.4", ">=")) { + die("skip: newer test for PHP 8.4+\n"); +} require('skipif.inc'); diff --git a/tests/integration/opcache/disabled/test_span_events_are_created_upon_uncaught_handled_exception.php84.php b/tests/integration/opcache/disabled/test_span_events_are_created_upon_uncaught_handled_exception.php84.php new file mode 100644 index 000000000..e4be687b9 --- /dev/null +++ b/tests/integration/opcache/disabled/test_span_events_are_created_upon_uncaught_handled_exception.php84.php @@ -0,0 +1,197 @@ + 'FakeDB', + ) +); +a(); + +echo 'this should never be printed'; diff --git a/tests/integration/pdo/mysql/base-class/test_instance_reporting_port.php b/tests/integration/pdo/mysql/base-class/test_instance_reporting_port.php new file mode 100644 index 000000000..0742b5461 --- /dev/null +++ b/tests/integration/pdo/mysql/base-class/test_instance_reporting_port.php @@ -0,0 +1,157 @@ +", + "?? SQL id", + "DROP TABLE ENV[DATASTORE_COLLECTION];", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDO::exec called at .*\/", + " in test_instance_reporting called at __FILE__ (??)" + ], + "host": "ENV[MYSQL_HOST]", + "port_path_or_id": "ENV[MYSQL_PORT]", + "database_name": "ENV[MYSQL_DB]" + } + ], + [ + "OtherTransaction/php__FILE__", + "", + "?? SQL id", + "CREATE TABLE ENV[DATASTORE_COLLECTION] (id INT, description VARCHAR(?));", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDO::exec called at .*\/", + " in test_instance_reporting called at __FILE__ (??)" + ], + "host": "ENV[MYSQL_HOST]", + "port_path_or_id": "ENV[MYSQL_PORT]", + "database_name": "ENV[MYSQL_DB]" + } + ] + ] +] +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.hostname": "ENV[MYSQL_HOST]", + "peer.address": "ENV[MYSQL_HOST]:ENV[MYSQL_PORT]", + "db.instance": "ENV[MYSQL_DB]", + "db.statement": "CREATE TABLE ENV[DATASTORE_COLLECTION] (id INT, description VARCHAR(?));" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.hostname": "ENV[MYSQL_HOST]", + "peer.address": "ENV[MYSQL_HOST]:ENV[MYSQL_PORT]", + "db.instance": "ENV[MYSQL_DB]", + "db.statement": "DROP TABLE ENV[DATASTORE_COLLECTION];" + } + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../test_instance_reporting.inc'); +require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php'); + +test_instance_reporting(new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD), 0); diff --git a/tests/integration/pdo/mysql/base-class/test_instance_reporting_socket.php b/tests/integration/pdo/mysql/base-class/test_instance_reporting_socket.php new file mode 100644 index 000000000..95177d7c7 --- /dev/null +++ b/tests/integration/pdo/mysql/base-class/test_instance_reporting_socket.php @@ -0,0 +1,160 @@ +", + "?? SQL id", + "DROP TABLE ENV[DATASTORE_COLLECTION];", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDO::exec called at .*\/", + " in test_instance_reporting called at __FILE__ (??)" + ], + "host": "__HOST__", + "port_path_or_id": "ENV[MYSQL_SOCKET]", + "database_name": "ENV[MYSQL_DB]" + } + ], + [ + "OtherTransaction/php__FILE__", + "", + "?? SQL id", + "CREATE TABLE ENV[DATASTORE_COLLECTION] (id INT, description VARCHAR(?));", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDO::exec called at .*\/", + " in test_instance_reporting called at __FILE__ (??)" + ], + "host": "__HOST__", + "port_path_or_id": "ENV[MYSQL_SOCKET]", + "database_name": "ENV[MYSQL_DB]" + } + ] + ] +] +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.hostname": "__HOST__", + "peer.address": "__HOST__:ENV[MYSQL_SOCKET]", + "db.instance": "ENV[MYSQL_DB]", + "db.statement": "CREATE TABLE ENV[DATASTORE_COLLECTION] (id INT, description VARCHAR(?));" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.hostname": "__HOST__", + "peer.address": "__HOST__:ENV[MYSQL_SOCKET]", + "db.instance": "ENV[MYSQL_DB]", + "db.statement": "DROP TABLE ENV[DATASTORE_COLLECTION];" + } + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../test_instance_reporting.inc'); +require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php'); + +$DSN = 'mysql:'; +$DSN .= 'unix_socket=' . $MYSQL_SOCKET . ';'; +$DSN .= 'dbname=' . $MYSQL_DB . ';'; + +test_instance_reporting(new PDO($DSN, $MYSQL_USER, $MYSQL_PASSWD), 0); diff --git a/tests/integration/pdo/mysql/base-class/test_prepared_stmt_basic.php b/tests/integration/pdo/mysql/base-class/test_prepared_stmt_basic.php new file mode 100644 index 000000000..93e54a033 --- /dev/null +++ b/tests/integration/pdo/mysql/base-class/test_prepared_stmt_basic.php @@ -0,0 +1,110 @@ +", + "?? SQL id", + "select * from information_schema.tables limit ?;", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDOStatement::execute called at .*\/", + " in test_prepared_stmt called at __FILE__ (??)" + ], + "explain_plan": "??" + } + ] + ] +] +*/ + +// peer.address is unknown:unknown because instance_reporting is disabled +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "select * from information_schema.tables limit ?;" + } + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../test_prepared_stmt_basic.inc'); +require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php'); + +$query = 'select * from information_schema.tables limit 1;'; +test_prepared_stmt(new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD), $query); diff --git a/tests/integration/pdo/mysql/base-class/test_prepared_stmt_bind_value.php b/tests/integration/pdo/mysql/base-class/test_prepared_stmt_bind_value.php new file mode 100644 index 000000000..34868e9ec --- /dev/null +++ b/tests/integration/pdo/mysql/base-class/test_prepared_stmt_bind_value.php @@ -0,0 +1,111 @@ +", + "?? SQL id", + "select * from information_schema.tables where table_name = ? limit ?;", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDOStatement::execute called at .*\/", + " in test_prepared_stmt called at __FILE__ (??)" + ], + "explain_plan": "??" + } + ] + ] +] +*/ + +// peer.address is unknown:unknown because instance_reporting is disabled +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "select * from information_schema.tables where table_name = ? limit ?;" + } + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../test_prepared_stmt_bind_value.inc'); +require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php'); + +$query = 'select * from information_schema.tables where table_name = ? limit 1;'; +test_prepared_stmt(new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD), $query); diff --git a/tests/integration/pdo/mysql/base-class/test_query_1_arg.php b/tests/integration/pdo/mysql/base-class/test_query_1_arg.php new file mode 100644 index 000000000..65a1da4fa --- /dev/null +++ b/tests/integration/pdo/mysql/base-class/test_query_1_arg.php @@ -0,0 +1,52 @@ +", + "?? SQL id", + "DROP TABLE ENV[DATASTORE_COLLECTION];", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDO::exec called at .*\/", + " in test_instance_reporting called at __FILE__ (??)" + ], + "host": "ENV[MYSQL_HOST]", + "port_path_or_id": "ENV[MYSQL_PORT]", + "database_name": "ENV[MYSQL_DB]" + } + ], + [ + "OtherTransaction/php__FILE__", + "", + "?? SQL id", + "CREATE TABLE ENV[DATASTORE_COLLECTION] (id INT, description VARCHAR(?));", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDO::exec called at .*\/", + " in test_instance_reporting called at __FILE__ (??)" + ], + "host": "ENV[MYSQL_HOST]", + "port_path_or_id": "ENV[MYSQL_PORT]", + "database_name": "ENV[MYSQL_DB]" + } + ] + ] +] +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.hostname": "ENV[MYSQL_HOST]", + "peer.address": "ENV[MYSQL_HOST]:ENV[MYSQL_PORT]", + "db.instance": "ENV[MYSQL_DB]", + "db.statement": "CREATE TABLE ENV[DATASTORE_COLLECTION] (id INT, description VARCHAR(?));" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.hostname": "ENV[MYSQL_HOST]", + "peer.address": "ENV[MYSQL_HOST]:ENV[MYSQL_PORT]", + "db.instance": "ENV[MYSQL_DB]", + "db.statement": "DROP TABLE ENV[DATASTORE_COLLECTION];" + } + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../test_instance_reporting.inc'); +require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php'); + +test_instance_reporting(new Pdo\Mysql($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD), 0); diff --git a/tests/integration/pdo/mysql/constructor/test_instance_reporting_socket.php b/tests/integration/pdo/mysql/constructor/test_instance_reporting_socket.php new file mode 100644 index 000000000..073bceb80 --- /dev/null +++ b/tests/integration/pdo/mysql/constructor/test_instance_reporting_socket.php @@ -0,0 +1,161 @@ +", + "?? SQL id", + "DROP TABLE ENV[DATASTORE_COLLECTION];", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDO::exec called at .*\/", + " in test_instance_reporting called at __FILE__ (??)" + ], + "host": "__HOST__", + "port_path_or_id": "ENV[MYSQL_SOCKET]", + "database_name": "ENV[MYSQL_DB]" + } + ], + [ + "OtherTransaction/php__FILE__", + "", + "?? SQL id", + "CREATE TABLE ENV[DATASTORE_COLLECTION] (id INT, description VARCHAR(?));", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDO::exec called at .*\/", + " in test_instance_reporting called at __FILE__ (??)" + ], + "host": "__HOST__", + "port_path_or_id": "ENV[MYSQL_SOCKET]", + "database_name": "ENV[MYSQL_DB]" + } + ] + ] +] +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.hostname": "__HOST__", + "peer.address": "__HOST__:ENV[MYSQL_SOCKET]", + "db.instance": "ENV[MYSQL_DB]", + "db.statement": "CREATE TABLE ENV[DATASTORE_COLLECTION] (id INT, description VARCHAR(?));" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.hostname": "__HOST__", + "peer.address": "__HOST__:ENV[MYSQL_SOCKET]", + "db.instance": "ENV[MYSQL_DB]", + "db.statement": "DROP TABLE ENV[DATASTORE_COLLECTION];" + } + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../test_instance_reporting.inc'); +require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php'); + +$DSN = 'mysql:'; +$DSN .= 'unix_socket=' . $MYSQL_SOCKET . ';'; +$DSN .= 'dbname=' . $MYSQL_DB . ';'; + +test_instance_reporting(new Pdo\Mysql($DSN, $MYSQL_USER, $MYSQL_PASSWD), 0); diff --git a/tests/integration/pdo/mysql/constructor/test_prepared_stmt_basic.php b/tests/integration/pdo/mysql/constructor/test_prepared_stmt_basic.php new file mode 100644 index 000000000..695dab225 --- /dev/null +++ b/tests/integration/pdo/mysql/constructor/test_prepared_stmt_basic.php @@ -0,0 +1,111 @@ +", + "?? SQL id", + "select * from information_schema.tables limit ?;", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDOStatement::execute called at .*\/", + " in test_prepared_stmt called at __FILE__ (??)" + ], + "explain_plan": "??" + } + ] + ] +] +*/ + +// peer.address is unknown:unknown because instance_reporting is disabled +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "select * from information_schema.tables limit ?;" + } + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../test_prepared_stmt_basic.inc'); +require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php'); + +$query = 'select * from information_schema.tables limit 1;'; +test_prepared_stmt(new Pdo\Mysql($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD), $query); diff --git a/tests/integration/pdo/mysql/constructor/test_prepared_stmt_bind_value.php b/tests/integration/pdo/mysql/constructor/test_prepared_stmt_bind_value.php new file mode 100644 index 000000000..214533332 --- /dev/null +++ b/tests/integration/pdo/mysql/constructor/test_prepared_stmt_bind_value.php @@ -0,0 +1,112 @@ +", + "?? SQL id", + "select * from information_schema.tables where table_name = ? limit ?;", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDOStatement::execute called at .*\/", + " in test_prepared_stmt called at __FILE__ (??)" + ], + "explain_plan": "??" + } + ] + ] +] +*/ + +// peer.address is unknown:unknown because instance_reporting is disabled +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "select * from information_schema.tables where table_name = ? limit ?;" + } + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../test_prepared_stmt_bind_value.inc'); +require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php'); + +$query = 'select * from information_schema.tables where table_name = ? limit 1;'; +test_prepared_stmt(new Pdo\Mysql($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD), $query); diff --git a/tests/integration/pdo/mysql/constructor/test_query_1_arg.php b/tests/integration/pdo/mysql/constructor/test_query_1_arg.php new file mode 100644 index 000000000..63d641e70 --- /dev/null +++ b/tests/integration/pdo/mysql/constructor/test_query_1_arg.php @@ -0,0 +1,53 @@ +", + "?? SQL id", + "DROP TABLE ENV[DATASTORE_COLLECTION];", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDO::exec called at .*\/", + " in test_instance_reporting called at __FILE__ (??)" + ], + "host": "ENV[MYSQL_HOST]", + "port_path_or_id": "ENV[MYSQL_PORT]", + "database_name": "ENV[MYSQL_DB]" + } + ], + [ + "OtherTransaction/php__FILE__", + "", + "?? SQL id", + "CREATE TABLE ENV[DATASTORE_COLLECTION] (id INT, description VARCHAR(?));", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDO::exec called at .*\/", + " in test_instance_reporting called at __FILE__ (??)" + ], + "host": "ENV[MYSQL_HOST]", + "port_path_or_id": "ENV[MYSQL_PORT]", + "database_name": "ENV[MYSQL_DB]" + } + ] + ] +] +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.hostname": "ENV[MYSQL_HOST]", + "peer.address": "ENV[MYSQL_HOST]:ENV[MYSQL_PORT]", + "db.instance": "ENV[MYSQL_DB]", + "db.statement": "CREATE TABLE ENV[DATASTORE_COLLECTION] (id INT, description VARCHAR(?));" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.hostname": "ENV[MYSQL_HOST]", + "peer.address": "ENV[MYSQL_HOST]:ENV[MYSQL_PORT]", + "db.instance": "ENV[MYSQL_DB]", + "db.statement": "DROP TABLE ENV[DATASTORE_COLLECTION];" + } + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../test_instance_reporting.inc'); +require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php'); + +test_instance_reporting(PDO::connect($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD), 0); diff --git a/tests/integration/pdo/mysql/factory/test_instance_reporting_socket.php b/tests/integration/pdo/mysql/factory/test_instance_reporting_socket.php new file mode 100644 index 000000000..796d108db --- /dev/null +++ b/tests/integration/pdo/mysql/factory/test_instance_reporting_socket.php @@ -0,0 +1,161 @@ +", + "?? SQL id", + "DROP TABLE ENV[DATASTORE_COLLECTION];", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDO::exec called at .*\/", + " in test_instance_reporting called at __FILE__ (??)" + ], + "host": "__HOST__", + "port_path_or_id": "ENV[MYSQL_SOCKET]", + "database_name": "ENV[MYSQL_DB]" + } + ], + [ + "OtherTransaction/php__FILE__", + "", + "?? SQL id", + "CREATE TABLE ENV[DATASTORE_COLLECTION] (id INT, description VARCHAR(?));", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDO::exec called at .*\/", + " in test_instance_reporting called at __FILE__ (??)" + ], + "host": "__HOST__", + "port_path_or_id": "ENV[MYSQL_SOCKET]", + "database_name": "ENV[MYSQL_DB]" + } + ] + ] +] +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.hostname": "__HOST__", + "peer.address": "__HOST__:ENV[MYSQL_SOCKET]", + "db.instance": "ENV[MYSQL_DB]", + "db.statement": "CREATE TABLE ENV[DATASTORE_COLLECTION] (id INT, description VARCHAR(?));" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.hostname": "__HOST__", + "peer.address": "__HOST__:ENV[MYSQL_SOCKET]", + "db.instance": "ENV[MYSQL_DB]", + "db.statement": "DROP TABLE ENV[DATASTORE_COLLECTION];" + } + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../test_instance_reporting.inc'); +require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php'); + +$DSN = 'mysql:'; +$DSN .= 'unix_socket=' . $MYSQL_SOCKET . ';'; +$DSN .= 'dbname=' . $MYSQL_DB . ';'; + +test_instance_reporting(PDO::connect($DSN, $MYSQL_USER, $MYSQL_PASSWD), 0); diff --git a/tests/integration/pdo/mysql/factory/test_prepared_stmt_basic.php b/tests/integration/pdo/mysql/factory/test_prepared_stmt_basic.php new file mode 100644 index 000000000..6488f0ffe --- /dev/null +++ b/tests/integration/pdo/mysql/factory/test_prepared_stmt_basic.php @@ -0,0 +1,111 @@ +", + "?? SQL id", + "select * from information_schema.tables limit ?;", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDOStatement::execute called at .*\/", + " in test_prepared_stmt called at __FILE__ (??)" + ], + "explain_plan": "??" + } + ] + ] +] +*/ + +// peer.address is unknown:unknown because instance_reporting is disabled +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "select * from information_schema.tables limit ?;" + } + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../test_prepared_stmt_basic.inc'); +require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php'); + +$query = 'select * from information_schema.tables limit 1;'; +test_prepared_stmt(PDO::connect($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD), $query); diff --git a/tests/integration/pdo/mysql/factory/test_prepared_stmt_bind_value.php b/tests/integration/pdo/mysql/factory/test_prepared_stmt_bind_value.php new file mode 100644 index 000000000..e1cae652e --- /dev/null +++ b/tests/integration/pdo/mysql/factory/test_prepared_stmt_bind_value.php @@ -0,0 +1,112 @@ +", + "?? SQL id", + "select * from information_schema.tables where table_name = ? limit ?;", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDOStatement::execute called at .*\/", + " in test_prepared_stmt called at __FILE__ (??)" + ], + "explain_plan": "??" + } + ] + ] +] +*/ + +// peer.address is unknown:unknown because instance_reporting is disabled +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "select * from information_schema.tables where table_name = ? limit ?;" + } + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../test_prepared_stmt_bind_value.inc'); +require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php'); + +$query = 'select * from information_schema.tables where table_name = ? limit 1;'; +test_prepared_stmt(PDO::connect($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD), $query); diff --git a/tests/integration/pdo/mysql/factory/test_query_1_arg.php b/tests/integration/pdo/mysql/factory/test_query_1_arg.php new file mode 100644 index 000000000..b15999e0b --- /dev/null +++ b/tests/integration/pdo/mysql/factory/test_query_1_arg.php @@ -0,0 +1,53 @@ +", + "?? SQL id", + "DROP TABLE ENV[DATASTORE_COLLECTION];", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDO::exec called at .*\/", + " in test_instance_reporting called at __FILE__ (??)" + ], + "host": "ENV[PG_HOST]", + "port_path_or_id": "ENV[PG_PORT]", + "database_name": "postgres" + } + ], + [ + "OtherTransaction/php__FILE__", + "", + "?? SQL id", + "CREATE TABLE ENV[DATASTORE_COLLECTION] (id INT, description VARCHAR(?));", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDO::exec called at .*\/", + " in test_instance_reporting called at __FILE__ (??)" + ], + "host": "ENV[PG_HOST]", + "port_path_or_id": "ENV[PG_PORT]", + "database_name": "postgres" + } + ] + ] +] +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.hostname": "ENV[PG_HOST]", + "peer.address": "ENV[PG_HOST]:ENV[PG_PORT]", + "db.instance": "postgres", + "db.statement": "CREATE TABLE ENV[DATASTORE_COLLECTION] (id INT, description VARCHAR(?));" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.hostname": "ENV[PG_HOST]", + "peer.address": "ENV[PG_HOST]:ENV[PG_PORT]", + "db.instance": "postgres", + "db.statement": "DROP TABLE ENV[DATASTORE_COLLECTION];" + } + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../test_instance_reporting.inc'); +require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php'); + +test_instance_reporting(new PDO($PDO_PGSQL_DSN, $PG_USER, $PG_PW), 0); diff --git a/tests/integration/pdo/pgsql/base-class/test_prepared_stmt_basic.php b/tests/integration/pdo/pgsql/base-class/test_prepared_stmt_basic.php new file mode 100644 index 000000000..9c7960080 --- /dev/null +++ b/tests/integration/pdo/pgsql/base-class/test_prepared_stmt_basic.php @@ -0,0 +1,106 @@ +", + "?? SQL id", + "select * from information_schema.tables limit ?;", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDOStatement::execute called at .*\/", + " in test_prepared_stmt called at __FILE__ (??)" + ] + } + ] + ] +] +*/ + +// peer.address is unknown:unknown because instance_reporting is disabled +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "select * from information_schema.tables limit ?;" + } + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../test_prepared_stmt_basic.inc'); +require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php'); + +$query = 'select * from information_schema.tables limit 1;'; +test_prepared_stmt(new PDO($PDO_PGSQL_DSN, $PG_USER, $PG_PW), $query); diff --git a/tests/integration/pdo/pgsql/base-class/test_prepared_stmt_bind_value.php b/tests/integration/pdo/pgsql/base-class/test_prepared_stmt_bind_value.php new file mode 100644 index 000000000..56737cafb --- /dev/null +++ b/tests/integration/pdo/pgsql/base-class/test_prepared_stmt_bind_value.php @@ -0,0 +1,107 @@ +", + "?? SQL id", + "select * from information_schema.tables where table_name = ? limit ?;", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDOStatement::execute called at .*\/", + " in test_prepared_stmt called at __FILE__ (??)" + ] + } + ] + ] +] +*/ + +// peer.address is unknown:unknown because instance_reporting is disabled +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "select * from information_schema.tables where table_name = ? limit ?;" + } + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../test_prepared_stmt_bind_value.inc'); +require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php'); + +$query = 'select * from information_schema.tables where table_name = ? limit 1;'; +test_prepared_stmt(new PDO($PDO_PGSQL_DSN, $PG_USER, $PG_PW), $query); diff --git a/tests/integration/pdo/pgsql/base-class/test_query_1_arg.php b/tests/integration/pdo/pgsql/base-class/test_query_1_arg.php new file mode 100644 index 000000000..48e212208 --- /dev/null +++ b/tests/integration/pdo/pgsql/base-class/test_query_1_arg.php @@ -0,0 +1,55 @@ +", + "?? SQL id", + "DROP TABLE ENV[DATASTORE_COLLECTION];", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDO::exec called at .*\/", + " in test_instance_reporting called at __FILE__ (??)" + ], + "host": "ENV[PG_HOST]", + "port_path_or_id": "ENV[PG_PORT]", + "database_name": "postgres" + } + ], + [ + "OtherTransaction/php__FILE__", + "", + "?? SQL id", + "CREATE TABLE ENV[DATASTORE_COLLECTION] (id INT, description VARCHAR(?));", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDO::exec called at .*\/", + " in test_instance_reporting called at __FILE__ (??)" + ], + "host": "ENV[PG_HOST]", + "port_path_or_id": "ENV[PG_PORT]", + "database_name": "postgres" + } + ] + ] +] +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.hostname": "ENV[PG_HOST]", + "peer.address": "ENV[PG_HOST]:ENV[PG_PORT]", + "db.instance": "postgres", + "db.statement": "CREATE TABLE ENV[DATASTORE_COLLECTION] (id INT, description VARCHAR(?));" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.hostname": "ENV[PG_HOST]", + "peer.address": "ENV[PG_HOST]:ENV[PG_PORT]", + "db.instance": "postgres", + "db.statement": "DROP TABLE ENV[DATASTORE_COLLECTION];" + } + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../test_instance_reporting.inc'); +require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php'); + +test_instance_reporting(new Pdo\Pgsql($PDO_PGSQL_DSN, $PG_USER, $PG_PW), 0); diff --git a/tests/integration/pdo/pgsql/constructor/test_prepared_stmt_basic.php b/tests/integration/pdo/pgsql/constructor/test_prepared_stmt_basic.php new file mode 100644 index 000000000..131ab0935 --- /dev/null +++ b/tests/integration/pdo/pgsql/constructor/test_prepared_stmt_basic.php @@ -0,0 +1,107 @@ +", + "?? SQL id", + "select * from information_schema.tables limit ?;", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDOStatement::execute called at .*\/", + " in test_prepared_stmt called at __FILE__ (??)" + ] + } + ] + ] +] +*/ + +// peer.address is unknown:unknown because instance_reporting is disabled +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "select * from information_schema.tables limit ?;" + } + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../test_prepared_stmt_basic.inc'); +require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php'); + +$query = 'select * from information_schema.tables limit 1;'; +test_prepared_stmt(new Pdo\Pgsql($PDO_PGSQL_DSN, $PG_USER, $PG_PW), $query); diff --git a/tests/integration/pdo/pgsql/constructor/test_prepared_stmt_bind_value.php b/tests/integration/pdo/pgsql/constructor/test_prepared_stmt_bind_value.php new file mode 100644 index 000000000..c954060f8 --- /dev/null +++ b/tests/integration/pdo/pgsql/constructor/test_prepared_stmt_bind_value.php @@ -0,0 +1,108 @@ +", + "?? SQL id", + "select * from information_schema.tables where table_name = ? limit ?;", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDOStatement::execute called at .*\/", + " in test_prepared_stmt called at __FILE__ (??)" + ] + } + ] + ] +] +*/ + +// peer.address is unknown:unknown because instance_reporting is disabled +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "select * from information_schema.tables where table_name = ? limit ?;" + } + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../test_prepared_stmt_bind_value.inc'); +require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php'); + +$query = 'select * from information_schema.tables where table_name = ? limit 1;'; +test_prepared_stmt(new Pdo\Pgsql($PDO_PGSQL_DSN, $PG_USER, $PG_PW), $query); diff --git a/tests/integration/pdo/pgsql/constructor/test_query_1_arg.php b/tests/integration/pdo/pgsql/constructor/test_query_1_arg.php new file mode 100644 index 000000000..c3cc9381d --- /dev/null +++ b/tests/integration/pdo/pgsql/constructor/test_query_1_arg.php @@ -0,0 +1,52 @@ +", + "?? SQL id", + "DROP TABLE ENV[DATASTORE_COLLECTION];", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDO::exec called at .*\/", + " in test_instance_reporting called at __FILE__ (??)" + ], + "host": "ENV[PG_HOST]", + "port_path_or_id": "ENV[PG_PORT]", + "database_name": "postgres" + } + ], + [ + "OtherTransaction/php__FILE__", + "", + "?? SQL id", + "CREATE TABLE ENV[DATASTORE_COLLECTION] (id INT, description VARCHAR(?));", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDO::exec called at .*\/", + " in test_instance_reporting called at __FILE__ (??)" + ], + "host": "ENV[PG_HOST]", + "port_path_or_id": "ENV[PG_PORT]", + "database_name": "postgres" + } + ] + ] +] +*/ + +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/create", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.hostname": "ENV[PG_HOST]", + "peer.address": "ENV[PG_HOST]:ENV[PG_PORT]", + "db.instance": "postgres", + "db.statement": "CREATE TABLE ENV[DATASTORE_COLLECTION] (id INT, description VARCHAR(?));" + } + ], + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/drop", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.hostname": "ENV[PG_HOST]", + "peer.address": "ENV[PG_HOST]:ENV[PG_PORT]", + "db.instance": "postgres", + "db.statement": "DROP TABLE ENV[DATASTORE_COLLECTION];" + } + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../test_instance_reporting.inc'); +require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php'); + +test_instance_reporting(PDO::connect($PDO_PGSQL_DSN, $PG_USER, $PG_PW), 0); diff --git a/tests/integration/pdo/pgsql/factory/test_prepared_stmt_basic.php b/tests/integration/pdo/pgsql/factory/test_prepared_stmt_basic.php new file mode 100644 index 000000000..58841fbf7 --- /dev/null +++ b/tests/integration/pdo/pgsql/factory/test_prepared_stmt_basic.php @@ -0,0 +1,107 @@ +", + "?? SQL id", + "select * from information_schema.tables limit ?;", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDOStatement::execute called at .*\/", + " in test_prepared_stmt called at __FILE__ (??)" + ] + } + ] + ] +] +*/ + +// peer.address is unknown:unknown because instance_reporting is disabled +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "select * from information_schema.tables limit ?;" + } + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../test_prepared_stmt_basic.inc'); +require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php'); + +$query = 'select * from information_schema.tables limit 1;'; +test_prepared_stmt(PDO::connect($PDO_PGSQL_DSN, $PG_USER, $PG_PW), $query); diff --git a/tests/integration/pdo/pgsql/factory/test_prepared_stmt_bind_value.php b/tests/integration/pdo/pgsql/factory/test_prepared_stmt_bind_value.php new file mode 100644 index 000000000..7ccfa9e9f --- /dev/null +++ b/tests/integration/pdo/pgsql/factory/test_prepared_stmt_bind_value.php @@ -0,0 +1,108 @@ +", + "?? SQL id", + "select * from information_schema.tables where table_name = ? limit ?;", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDOStatement::execute called at .*\/", + " in test_prepared_stmt called at __FILE__ (??)" + ] + } + ] + ] +] +*/ + +// peer.address is unknown:unknown because instance_reporting is disabled +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "select * from information_schema.tables where table_name = ? limit ?;" + } + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../test_prepared_stmt_bind_value.inc'); +require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php'); + +$query = 'select * from information_schema.tables where table_name = ? limit 1;'; +test_prepared_stmt(PDO::connect($PDO_PGSQL_DSN, $PG_USER, $PG_PW), $query); diff --git a/tests/integration/pdo/pgsql/factory/test_query_1_arg.php b/tests/integration/pdo/pgsql/factory/test_query_1_arg.php new file mode 100644 index 000000000..1f47b55b7 --- /dev/null +++ b/tests/integration/pdo/pgsql/factory/test_query_1_arg.php @@ -0,0 +1,52 @@ += 8.4\n"); +} + diff --git a/tests/integration/pdo/skipif_pgsql.inc b/tests/integration/pdo/skipif_pgsql.inc new file mode 100644 index 000000000..92caf44be --- /dev/null +++ b/tests/integration/pdo/skipif_pgsql.inc @@ -0,0 +1,37 @@ +getMessage() . "\n"); +} diff --git a/tests/integration/pdo/sqlite/base-class/test_prepared_stmt_basic.php b/tests/integration/pdo/sqlite/base-class/test_prepared_stmt_basic.php new file mode 100644 index 000000000..ddf000ce8 --- /dev/null +++ b/tests/integration/pdo/sqlite/base-class/test_prepared_stmt_basic.php @@ -0,0 +1,106 @@ +", + "?? SQL id", + "select * from ENV[DATASTORE_COLLECTION] limit ?;", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDOStatement::execute called at .*\/", + " in test_prepared_stmt called at __FILE__ (??)" + ] + } + ] + ] +] +*/ + +// peer.address is unknown:unknown because instance_reporting is disabled +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "select * from ENV[DATASTORE_COLLECTION] limit ?;" + } + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../test_prepared_stmt_basic.inc'); +require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php'); + +$query = 'select * from sqlite_master limit 1;'; +test_prepared_stmt(new PDO('sqlite::memory:'), $query); diff --git a/tests/integration/pdo/sqlite/base-class/test_prepared_stmt_bind_value.php b/tests/integration/pdo/sqlite/base-class/test_prepared_stmt_bind_value.php new file mode 100644 index 000000000..a26e0802f --- /dev/null +++ b/tests/integration/pdo/sqlite/base-class/test_prepared_stmt_bind_value.php @@ -0,0 +1,107 @@ +", + "?? SQL id", + "select * from ENV[DATASTORE_COLLECTION] where tbl_name = ? limit ?;", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDOStatement::execute called at .*\/", + " in test_prepared_stmt called at __FILE__ (??)" + ] + } + ] + ] +] +*/ + +// peer.address is unknown:unknown because instance_reporting is disabled +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "select * from ENV[DATASTORE_COLLECTION] where tbl_name = ? limit ?;" + } + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../test_prepared_stmt_bind_value.inc'); +require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php'); + +$query = 'select * from sqlite_master where tbl_name = ? limit 1;'; +test_prepared_stmt(new PDO('sqlite::memory:'), $query); diff --git a/tests/integration/pdo/test_query_1.php b/tests/integration/pdo/sqlite/base-class/test_query_1_arg.php similarity index 76% rename from tests/integration/pdo/test_query_1.php rename to tests/integration/pdo/sqlite/base-class/test_query_1_arg.php index 21b932f49..df16dc8a3 100644 --- a/tests/integration/pdo/test_query_1.php +++ b/tests/integration/pdo/sqlite/base-class/test_query_1_arg.php @@ -6,14 +6,12 @@ /*DESCRIPTION The agent should record Datastore metrics for the one argument form of -PDO::query(). +PDO::query() when PDO base class constructor is used to create connection +object. */ /*SKIPIF -=")) { - die("skip: PHP >= 8.1.0 not supported\n"); -} +exec("CREATE TABLE test (id INT, desc VARCHAR(10));"), 'create table'); - - tap_equal(1, $conn->exec("INSERT INTO test VALUES (1, 'one');"), 'insert one'); - tap_equal(1, $conn->exec("INSERT INTO test VALUES (2, 'two');"), 'insert two'); - tap_equal(1, $conn->exec("INSERT INTO test VALUES (3, 'three');"), 'insert three'); - - $expected = array( - array('id' => '1', 'desc' => 'one'), - array('id' => '2', 'desc' => 'two'), - array('id' => '3', 'desc' => 'three') - ); - - $result = $conn->query('SELECT * FROM test;'); - $actual = $result->fetchAll(PDO::FETCH_ASSOC); - $result->closeCursor(); - tap_equal($expected, $actual, 'query (1-arg)'); - - tap_equal(1, $conn->exec("DROP TABLE test;"), 'drop table'); -} - -test_pdo_query(); +test_pdo_query(new PDO('sqlite::memory:')); diff --git a/tests/integration/pdo/test_query_3.php b/tests/integration/pdo/sqlite/base-class/test_query_fetch_class.php similarity index 82% rename from tests/integration/pdo/test_query_3.php rename to tests/integration/pdo/sqlite/base-class/test_query_fetch_class.php index e37919a72..e84fb4027 100644 --- a/tests/integration/pdo/test_query_3.php +++ b/tests/integration/pdo/sqlite/base-class/test_query_fetch_class.php @@ -6,11 +6,12 @@ /*DESCRIPTION The agent should record database metrics for the FETCH_CLASS variant of -PDO::query(). +PDO::query() when PDO base class constructor is used to create connection +object. */ /*SKIPIF -exec("CREATE TABLE test (id INT, desc VARCHAR(10));"), 'create table'); - tap_equal(1, $conn->exec("INSERT INTO test VALUES (1, 'one');"), 'insert row'); - - $expected = new Row(); - $expected->id = '1'; - $expected->desc = 'one'; - - $actual = $conn->query('SELECT * FROM test;', PDO::FETCH_CLASS, 'Row')->fetch(); - tap_assert($expected == $actual, 'fetch row as object'); - - tap_equal(1, $conn->exec("DROP TABLE test;"), 'drop table'); -} - -test_pdo_query(); +test_pdo_query(new PDO('sqlite::memory:')); diff --git a/tests/integration/pdo/test_query_2.php b/tests/integration/pdo/sqlite/base-class/test_query_fetch_column.php similarity index 81% rename from tests/integration/pdo/test_query_2.php rename to tests/integration/pdo/sqlite/base-class/test_query_fetch_column.php index e0269144c..37989527d 100644 --- a/tests/integration/pdo/test_query_2.php +++ b/tests/integration/pdo/sqlite/base-class/test_query_fetch_column.php @@ -6,11 +6,12 @@ /*DESCRIPTION The agent should record database metrics for the FETCH_COLUMN variant of -PDO::query(). +PDO::query() when PDO base class constructor is used to create connection +object. */ /*SKIPIF -exec("CREATE TABLE test (id INT, desc VARCHAR(10));"), 'create table'); - - tap_equal(1, $conn->exec("INSERT INTO test VALUES (1, 'one');"), 'insert one'); - tap_equal(1, $conn->exec("INSERT INTO test VALUES (2, 'two');"), 'insert two'); - tap_equal(1, $conn->exec("INSERT INTO test VALUES (3, 'three');"), 'insert three'); - - $result = $conn->query('SELECT * FROM test;', PDO::FETCH_COLUMN, 1); - $actual = $result->fetchAll(PDO::FETCH_ASSOC); - $result->closeCursor(); - tap_equal(3, count($actual), 'fetch column'); - - tap_equal(1, $conn->exec("DROP TABLE test;"), 'drop table'); -} - -test_pdo_query(); +test_pdo_query(new PDO('sqlite::memory:')); diff --git a/tests/integration/pdo/test_query_2_logging_off.php b/tests/integration/pdo/sqlite/base-class/test_query_fetch_column_logging_off.php similarity index 81% rename from tests/integration/pdo/test_query_2_logging_off.php rename to tests/integration/pdo/sqlite/base-class/test_query_fetch_column_logging_off.php index f3d2da948..76a35f1fc 100644 --- a/tests/integration/pdo/test_query_2_logging_off.php +++ b/tests/integration/pdo/sqlite/base-class/test_query_fetch_column_logging_off.php @@ -6,11 +6,12 @@ /*DESCRIPTION The agent should record database metrics for the FETCH_COLUMN variant of -PDO::query(). +PDO::query() when PDO base class constructor is used to create connection +object. */ /*SKIPIF -exec("CREATE TABLE test (id INT, desc VARCHAR(10));"), 'create table'); - - tap_equal(1, $conn->exec("INSERT INTO test VALUES (1, 'one');"), 'insert one'); - tap_equal(1, $conn->exec("INSERT INTO test VALUES (2, 'two');"), 'insert two'); - tap_equal(1, $conn->exec("INSERT INTO test VALUES (3, 'three');"), 'insert three'); - - $result = $conn->query('SELECT * FROM test;', PDO::FETCH_COLUMN, 1); - $actual = $result->fetchAll(PDO::FETCH_ASSOC); - $result->closeCursor(); - tap_equal(3, count($actual), 'fetch column'); - - tap_equal(1, $conn->exec("DROP TABLE test;"), 'drop table'); -} - -test_pdo_query(); +test_pdo_query(new PDO('sqlite::memory:')); diff --git a/tests/integration/pdo/test_query_4.php b/tests/integration/pdo/sqlite/base-class/test_query_fetch_into.php similarity index 82% rename from tests/integration/pdo/test_query_4.php rename to tests/integration/pdo/sqlite/base-class/test_query_fetch_into.php index 884bb1f8d..63c132317 100644 --- a/tests/integration/pdo/test_query_4.php +++ b/tests/integration/pdo/sqlite/base-class/test_query_fetch_into.php @@ -6,11 +6,12 @@ /*DESCRIPTION The agent should record database metrics for the FETCH_INTO variant of -PDO::query(). +PDO::query() when PDO base class constructor is used to create connection +object. */ /*SKIPIF -exec("CREATE TABLE test (id INT, desc VARCHAR(10));"), 'create table'); - tap_equal(1, $conn->exec("INSERT INTO test VALUES (1, 'one');"), 'insert row'); - - $expected = new Row(); - $expected->id = '1'; - $expected->desc = 'one'; - - $actual = new Row(); - $conn->query('SELECT * FROM test;', PDO::FETCH_INTO, $actual)->fetch(); - tap_assert($expected == $actual, 'fetch row into object'); - - tap_equal(1, $conn->exec("DROP TABLE test;"), 'drop table'); -} - -test_pdo_query(); +test_pdo_query(new PDO('sqlite::memory:')); diff --git a/tests/integration/pdo/sqlite/constructor/test_prepared_stmt_basic.php b/tests/integration/pdo/sqlite/constructor/test_prepared_stmt_basic.php new file mode 100644 index 000000000..bdb32dd76 --- /dev/null +++ b/tests/integration/pdo/sqlite/constructor/test_prepared_stmt_basic.php @@ -0,0 +1,107 @@ +", + "?? SQL id", + "select * from ENV[DATASTORE_COLLECTION] limit ?;", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDOStatement::execute called at .*\/", + " in test_prepared_stmt called at __FILE__ (??)" + ] + } + ] + ] +] +*/ + +// peer.address is unknown:unknown because instance_reporting is disabled +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "select * from ENV[DATASTORE_COLLECTION] limit ?;" + } + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../test_prepared_stmt_basic.inc'); +require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php'); + +$query = 'select * from sqlite_schema limit 1;'; +test_prepared_stmt(new Pdo\Sqlite('sqlite::memory:'), $query); diff --git a/tests/integration/pdo/sqlite/constructor/test_prepared_stmt_bind_value.php b/tests/integration/pdo/sqlite/constructor/test_prepared_stmt_bind_value.php new file mode 100644 index 000000000..36e02cb5e --- /dev/null +++ b/tests/integration/pdo/sqlite/constructor/test_prepared_stmt_bind_value.php @@ -0,0 +1,108 @@ +", + "?? SQL id", + "select * from ENV[DATASTORE_COLLECTION] where tbl_name = ? limit ?;", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDOStatement::execute called at .*\/", + " in test_prepared_stmt called at __FILE__ (??)" + ] + } + ] + ] +] +*/ + +// peer.address is unknown:unknown because instance_reporting is disabled +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "select * from ENV[DATASTORE_COLLECTION] where tbl_name = ? limit ?;" + } + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../test_prepared_stmt_bind_value.inc'); +require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php'); + +$query = 'select * from sqlite_schema where tbl_name = ? limit 1;'; +test_prepared_stmt(new Pdo\Sqlite('sqlite::memory:'), $query); diff --git a/tests/integration/pdo/sqlite/constructor/test_query_1_arg.php b/tests/integration/pdo/sqlite/constructor/test_query_1_arg.php new file mode 100644 index 000000000..d0dc104cf --- /dev/null +++ b/tests/integration/pdo/sqlite/constructor/test_query_1_arg.php @@ -0,0 +1,75 @@ +exec("CREATE TABLE test (id INT, desc VARCHAR(10));"), 'create table'); - - tap_equal(1, $conn->exec("INSERT INTO test VALUES (1, 'one');"), 'insert one'); - tap_equal(1, $conn->exec("INSERT INTO test VALUES (2, 'two');"), 'insert two'); - tap_equal(1, $conn->exec("INSERT INTO test VALUES (3, 'three');"), 'insert three'); - - $expected = array( - array('id' => 1, 'desc' => 'one'), - array('id' => 2, 'desc' => 'two'), - array('id' => 3, 'desc' => 'three') - ); - - $result = $conn->query('SELECT * FROM test;'); - $actual = $result->fetchAll(PDO::FETCH_ASSOC); - $result->closeCursor(); - tap_equal($expected, $actual, 'query (1-arg)'); - - tap_equal(1, $conn->exec("DROP TABLE test;"), 'drop table'); +class MyPDO extends \PDO +{ } -test_pdo_query(); +test_pdo_query(new MyPDO('sqlite::memory:')); diff --git a/tests/integration/pdo/sqlite/factory/test_prepared_stmt_basic.php b/tests/integration/pdo/sqlite/factory/test_prepared_stmt_basic.php new file mode 100644 index 000000000..652f65165 --- /dev/null +++ b/tests/integration/pdo/sqlite/factory/test_prepared_stmt_basic.php @@ -0,0 +1,107 @@ +", + "?? SQL id", + "select * from ENV[DATASTORE_COLLECTION] limit ?;", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDOStatement::execute called at .*\/", + " in test_prepared_stmt called at __FILE__ (??)" + ] + } + ] + ] +] +*/ + +// peer.address is unknown:unknown because instance_reporting is disabled +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "select * from ENV[DATASTORE_COLLECTION] limit ?;" + } + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../test_prepared_stmt_basic.inc'); +require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php'); + +$query = 'select * from sqlite_schema limit 1;'; +test_prepared_stmt(PDO::Connect('sqlite::memory:'), $query); diff --git a/tests/integration/pdo/sqlite/factory/test_prepared_stmt_bind_value.php b/tests/integration/pdo/sqlite/factory/test_prepared_stmt_bind_value.php new file mode 100644 index 000000000..c4c548970 --- /dev/null +++ b/tests/integration/pdo/sqlite/factory/test_prepared_stmt_bind_value.php @@ -0,0 +1,108 @@ +", + "?? SQL id", + "select * from ENV[DATASTORE_COLLECTION] where tbl_name = ? limit ?;", + "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + 1, + "?? total time", + "?? min time", + "?? max time", + { + "backtrace": [ + "/ in PDOStatement::execute called at .*\/", + " in test_prepared_stmt called at __FILE__ (??)" + ] + } + ] + ] +] +*/ + +// peer.address is unknown:unknown because instance_reporting is disabled +/*EXPECT_SPAN_EVENTS_LIKE +[ + [ + { + "category": "datastore", + "type": "Span", + "guid": "??", + "traceId": "??", + "transactionId": "??", + "name": "Datastore/statement/ENV[DATASTORE_PRODUCT]/ENV[DATASTORE_COLLECTION]/select", + "timestamp": "??", + "duration": "??", + "priority": "??", + "sampled": true, + "parentId": "??", + "span.kind": "client", + "component": "ENV[DATASTORE_PRODUCT]" + }, + {}, + { + "peer.address": "unknown:unknown", + "db.statement": "select * from ENV[DATASTORE_COLLECTION] where tbl_name = ? limit ?;" + } + ] +] +*/ + +require_once(realpath (dirname ( __FILE__ )) . '/../../test_prepared_stmt_bind_value.inc'); +require_once(realpath (dirname ( __FILE__ )) . '/../../../../include/config.php'); + +$query = 'select * from sqlite_schema where tbl_name = ? limit 1;'; +test_prepared_stmt(PDO::Connect('sqlite::memory:'), $query); diff --git a/tests/integration/pdo/sqlite/factory/test_query_1_arg.php b/tests/integration/pdo/sqlite/factory/test_query_1_arg.php new file mode 100644 index 000000000..edab45185 --- /dev/null +++ b/tests/integration/pdo/sqlite/factory/test_query_1_arg.php @@ -0,0 +1,75 @@ +", "?? SQL id", - "select * from tables limit ?;", + "select * from information_schema.tables limit ?;", "Datastore/statement/MySQL/tables/select", 1, "?? total time", @@ -85,7 +85,7 @@ function test_slow_sql() global $PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD; $conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD, array()); - $result = $conn->query('select * from tables limit 1;'); + $result = $conn->query('select * from information_schema.tables limit 1;'); } test_slow_sql(); diff --git a/tests/integration/pdo/test_empty_driver_options.php81.php b/tests/integration/pdo/test_empty_driver_options.php81.php index 60f3e3778..e29984f62 100644 --- a/tests/integration/pdo/test_empty_driver_options.php81.php +++ b/tests/integration/pdo/test_empty_driver_options.php81.php @@ -32,7 +32,7 @@ "OtherTransaction/php__FILE__", "", "?? SQL id", - "select * from tables limit ?;", + "select * from information_schema.tables limit ?;", "Datastore/statement/MySQL/tables/select", 1, "?? total time", @@ -85,7 +85,7 @@ function test_slow_sql() global $PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD; $conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD, array()); - $result = $conn->query('select * from tables limit 1;'); + $result = $conn->query('select * from information_schema.tables limit 1;'); } test_slow_sql(); diff --git a/tests/integration/pdo/test_instance_reporting.inc b/tests/integration/pdo/test_instance_reporting.inc new file mode 100644 index 000000000..0ae7fb6a8 --- /dev/null +++ b/tests/integration/pdo/test_instance_reporting.inc @@ -0,0 +1,33 @@ +exec("CREATE TABLE test (id INT, description VARCHAR(10));"), 'create table'); + tap_equal($want_drop_table_result, $conn->exec("DROP TABLE test;"), 'drop table'); +} diff --git a/tests/integration/pdo/test_instance_slow_sql_1.php b/tests/integration/pdo/test_instance_slow_sql_1.php index 465ab00a1..b767100ba 100644 --- a/tests/integration/pdo/test_instance_slow_sql_1.php +++ b/tests/integration/pdo/test_instance_slow_sql_1.php @@ -45,7 +45,7 @@ "OtherTransaction/php__FILE__", "", "?? SQL id", - "select * from tables limit ?;", + "select * from information_schema.tables limit ?;", "Datastore/statement/MySQL/tables/select", 1, "?? total time", @@ -103,7 +103,7 @@ function test_query() { global $PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD; $conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD); - $result = $conn->query('select * from tables limit 1;'); + $result = $conn->query('select * from information_schema.tables limit 1;'); } test_query(); diff --git a/tests/integration/pdo/test_instance_slow_sql_1.php81.php b/tests/integration/pdo/test_instance_slow_sql_1.php81.php index 80a890f2e..1d217e04f 100644 --- a/tests/integration/pdo/test_instance_slow_sql_1.php81.php +++ b/tests/integration/pdo/test_instance_slow_sql_1.php81.php @@ -41,7 +41,7 @@ "OtherTransaction/php__FILE__", "", "?? SQL id", - "select * from tables limit ?;", + "select * from information_schema.tables limit ?;", "Datastore/statement/MySQL/tables/select", 1, "?? total time", @@ -99,7 +99,7 @@ function test_query() { global $PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD; $conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD); - $result = $conn->query('select * from tables limit 1;'); + $result = $conn->query('select * from information_schema.tables limit 1;'); } test_query(); diff --git a/tests/integration/pdo/test_instance_slow_sql_2.php b/tests/integration/pdo/test_instance_slow_sql_2.php index afa7e5df6..47330ea78 100644 --- a/tests/integration/pdo/test_instance_slow_sql_2.php +++ b/tests/integration/pdo/test_instance_slow_sql_2.php @@ -40,7 +40,7 @@ "OtherTransaction/php__FILE__", "", "?? SQL id", - "select * from tables limit ?;", + "select * from information_schema.tables limit ?;", "Datastore/statement/MySQL/tables/select", 1, "?? total time", @@ -98,7 +98,7 @@ function test_slow_sql() { global $PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD; $conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD); - $stmt = $conn->prepare('select * from tables limit 1;'); + $stmt = $conn->prepare('select * from information_schema.tables limit 1;'); tap_assert($stmt->execute(), 'execute slow query'); } diff --git a/tests/integration/pdo/test_instance_slow_sql_2.php81.php b/tests/integration/pdo/test_instance_slow_sql_2.php81.php index 60e833f34..9a7767cf0 100644 --- a/tests/integration/pdo/test_instance_slow_sql_2.php81.php +++ b/tests/integration/pdo/test_instance_slow_sql_2.php81.php @@ -40,7 +40,7 @@ "OtherTransaction/php__FILE__", "", "?? SQL id", - "select * from tables limit ?;", + "select * from information_schema.tables limit ?;", "Datastore/statement/MySQL/tables/select", 1, "?? total time", @@ -98,7 +98,7 @@ function test_slow_sql() { global $PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD; $conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD); - $stmt = $conn->prepare('select * from tables limit 1;'); + $stmt = $conn->prepare('select * from information_schema.tables limit 1;'); tap_assert($stmt->execute(), 'execute slow query'); } diff --git a/tests/integration/pdo/test_instance_slow_sql_for_update.php b/tests/integration/pdo/test_instance_slow_sql_for_update.php index 3ba2a18bc..1aac88955 100644 --- a/tests/integration/pdo/test_instance_slow_sql_for_update.php +++ b/tests/integration/pdo/test_instance_slow_sql_for_update.php @@ -36,7 +36,7 @@ "OtherTransaction/php__FILE__", "", "?? SQL id", - "select * from tables limit ? for update;", + "select * from information_schema.tables limit ? for update;", "Datastore/statement/MySQL/tables/select", 1, "?? total time", @@ -66,7 +66,7 @@ function test_slow_sql() { global $PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD; $conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD); - $result = $conn->query('select * from tables limit 1 for update;'); + $result = $conn->query('select * from information_schema.tables limit 1 for update;'); } test_slow_sql(); diff --git a/tests/integration/pdo/test_instance_slow_sql_lock.php b/tests/integration/pdo/test_instance_slow_sql_lock.php index 798d4272d..ea3db1bc7 100644 --- a/tests/integration/pdo/test_instance_slow_sql_lock.php +++ b/tests/integration/pdo/test_instance_slow_sql_lock.php @@ -37,7 +37,7 @@ "OtherTransaction/php__FILE__", "", "?? SQL id", - "select * from tables limit ? lock in share mode;", + "select * from information_schema.tables limit ? lock in share mode;", "Datastore/statement/MySQL/tables/select", 1, "?? total time", @@ -67,7 +67,7 @@ function test_slow_sql() { global $PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD; $conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD); - $result = $conn->query('select * from tables limit 1 lock in share mode;'); + $result = $conn->query('select * from information_schema.tables limit 1 lock in share mode;'); } test_slow_sql(); diff --git a/tests/integration/pdo/test_instance_slow_sql_semicolon.php b/tests/integration/pdo/test_instance_slow_sql_semicolon.php index 2c07e0023..c26da78ed 100644 --- a/tests/integration/pdo/test_instance_slow_sql_semicolon.php +++ b/tests/integration/pdo/test_instance_slow_sql_semicolon.php @@ -36,7 +36,7 @@ "OtherTransaction/php__FILE__", "", "?? SQL id", - "select * from tables where engine = ?;", + "select * from information_schema.tables where engine = ?;", "Datastore/statement/MySQL/tables/select", 1, "?? total time", @@ -66,7 +66,7 @@ function test_slow_sql() { global $PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD; $conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD); - $result = $conn->query('select * from tables where engine = \';\';'); + $result = $conn->query('select * from information_schema.tables where engine = \';\';'); } test_slow_sql(); diff --git a/tests/integration/pdo/test_null_options.php b/tests/integration/pdo/test_null_options.php index 16e522a57..ea61e7343 100644 --- a/tests/integration/pdo/test_null_options.php +++ b/tests/integration/pdo/test_null_options.php @@ -32,7 +32,7 @@ "OtherTransaction/php__FILE__", "", "?? SQL id", - "select * from tables limit ?;", + "select * from information_schema.tables limit ?;", "Datastore/statement/MySQL/tables/select", 1, "?? total time", @@ -85,7 +85,7 @@ function test_slow_sql() { $options = null; $conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD, $options); - $result = $conn->query('select * from tables limit 1;'); + $result = $conn->query('select * from information_schema.tables limit 1;'); } test_slow_sql(); diff --git a/tests/integration/pdo/test_null_options.php81.php b/tests/integration/pdo/test_null_options.php81.php index 528855b81..6f61e3a00 100644 --- a/tests/integration/pdo/test_null_options.php81.php +++ b/tests/integration/pdo/test_null_options.php81.php @@ -32,7 +32,7 @@ "OtherTransaction/php__FILE__", "", "?? SQL id", - "select * from tables limit ?;", + "select * from information_schema.tables limit ?;", "Datastore/statement/MySQL/tables/select", 1, "?? total time", @@ -85,7 +85,7 @@ function test_slow_sql() { $options = null; $conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD, $options); - $result = $conn->query('select * from tables limit 1;'); + $result = $conn->query('select * from information_schema.tables limit 1;'); } test_slow_sql(); diff --git a/tests/integration/pdo/test_persistent_conn.php b/tests/integration/pdo/test_persistent_conn.php index b44785305..b5453d889 100644 --- a/tests/integration/pdo/test_persistent_conn.php +++ b/tests/integration/pdo/test_persistent_conn.php @@ -32,7 +32,7 @@ "OtherTransaction/php__FILE__", "", "?? SQL id", - "select * from tables limit ?;", + "select * from information_schema.tables limit ?;", "Datastore/statement/MySQL/tables/select", 1, "?? total time", @@ -86,7 +86,7 @@ function test_slow_sql() $options = array(PDO::ATTR_PERSISTENT => true); $conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD, $options); - $result = $conn->query('select * from tables limit 1;'); + $result = $conn->query('select * from information_schema.tables limit 1;'); } test_slow_sql(); diff --git a/tests/integration/pdo/test_persistent_conn.php81.php b/tests/integration/pdo/test_persistent_conn.php81.php index ad185d896..2c669bd7f 100644 --- a/tests/integration/pdo/test_persistent_conn.php81.php +++ b/tests/integration/pdo/test_persistent_conn.php81.php @@ -32,7 +32,7 @@ "OtherTransaction/php__FILE__", "", "?? SQL id", - "select * from tables limit ?;", + "select * from information_schema.tables limit ?;", "Datastore/statement/MySQL/tables/select", 1, "?? total time", @@ -86,7 +86,7 @@ function test_slow_sql() $options = array(PDO::ATTR_PERSISTENT => true); $conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD, $options); - $result = $conn->query('select * from tables limit 1;'); + $result = $conn->query('select * from information_schema.tables limit 1;'); } test_slow_sql(); diff --git a/tests/integration/pdo/test_prepared_stmt_basic.inc b/tests/integration/pdo/test_prepared_stmt_basic.inc new file mode 100644 index 000000000..4db6fd87b --- /dev/null +++ b/tests/integration/pdo/test_prepared_stmt_basic.inc @@ -0,0 +1,31 @@ +prepare($query); + tap_assert($stmt->execute(), 'execute prepared statement'); +} diff --git a/tests/integration/pdo/test_prepared_stmt_bind_value.inc b/tests/integration/pdo/test_prepared_stmt_bind_value.inc new file mode 100644 index 000000000..6ba2ffd82 --- /dev/null +++ b/tests/integration/pdo/test_prepared_stmt_bind_value.inc @@ -0,0 +1,32 @@ +prepare($query); + $stmt->bindValue(1, "missing"); + tap_assert($stmt->execute(), 'execute prepared statement'); +} diff --git a/tests/integration/pdo/test_prepared_stmt_params.php b/tests/integration/pdo/test_prepared_stmt_params.php index cd097e04f..7bb92e7c3 100644 --- a/tests/integration/pdo/test_prepared_stmt_params.php +++ b/tests/integration/pdo/test_prepared_stmt_params.php @@ -36,7 +36,7 @@ "OtherTransaction/php__FILE__", "", "?? SQL id", - "select * from tables where table_name = ? limit ?;", + "select * from information_schema.tables where table_name = ? limit ?;", "Datastore/statement/MySQL/tables/select", 1, "?? total time", @@ -88,7 +88,7 @@ function test_prepared_statement() { global $PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD; $conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD); - $stmt = $conn->prepare('select * from tables where table_name = ? limit 1;'); + $stmt = $conn->prepare('select * from information_schema.tables where table_name = ? limit 1;'); $stmt->bindValue(1, "missing"); tap_assert($stmt->execute(), 'execute prepared statement with a param'); } diff --git a/tests/integration/pdo/test_prepared_stmt_params.php81.php b/tests/integration/pdo/test_prepared_stmt_params.php81.php index 001b38dd3..5c1b40c99 100644 --- a/tests/integration/pdo/test_prepared_stmt_params.php81.php +++ b/tests/integration/pdo/test_prepared_stmt_params.php81.php @@ -36,7 +36,7 @@ "OtherTransaction/php__FILE__", "", "?? SQL id", - "select * from tables where table_name = ? limit ?;", + "select * from information_schema.tables where table_name = ? limit ?;", "Datastore/statement/MySQL/tables/select", 1, "?? total time", @@ -88,7 +88,7 @@ function test_prepared_statement() { global $PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD; $conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD); - $stmt = $conn->prepare('select * from tables where table_name = ? limit 1;'); + $stmt = $conn->prepare('select * from information_schema.tables where table_name = ? limit 1;'); $stmt->bindValue(1, "missing"); tap_assert($stmt->execute(), 'execute prepared statement with a param'); } diff --git a/tests/integration/pdo/test_query_1_arg.inc b/tests/integration/pdo/test_query_1_arg.inc new file mode 100644 index 000000000..104405786 --- /dev/null +++ b/tests/integration/pdo/test_query_1_arg.inc @@ -0,0 +1,51 @@ +exec("CREATE TABLE test (id INT, description VARCHAR(10));"), 'create table'); + + tap_equal(1, $conn->exec("INSERT INTO test VALUES (1, 'one');"), 'insert one'); + tap_equal(1, $conn->exec("INSERT INTO test VALUES (2, 'two');"), 'insert two'); + tap_equal(1, $conn->exec("INSERT INTO test VALUES (3, 'three');"), 'insert three'); + + $expected = array( + array('id' => 1, 'description' => 'one'), + array('id' => 2, 'description' => 'two'), + array('id' => 3, 'description' => 'three') + ); + + $result = $conn->query('SELECT * FROM test;'); + $actual = $result->fetchAll(PDO::FETCH_ASSOC); + $result->closeCursor(); + // normalize id to int type + array_walk($actual, function (&$row, $key) { + $row['id'] = intval($row['id']); + }); + tap_equal($expected, $actual, 'query (1-arg)'); + + tap_equal($want_drop_table_result, $conn->exec("DROP TABLE test;"), 'drop table'); +} diff --git a/tests/integration/pdo/test_query_fetch_class.inc b/tests/integration/pdo/test_query_fetch_class.inc new file mode 100644 index 000000000..4aef4c76f --- /dev/null +++ b/tests/integration/pdo/test_query_fetch_class.inc @@ -0,0 +1,44 @@ +exec("CREATE TABLE test (id INT, description VARCHAR(10));"), 'create table'); + tap_equal(1, $conn->exec("INSERT INTO test VALUES (1, 'one');"), 'insert row'); + + $expected = new Row(); + $expected->id = '1'; + $expected->description = 'one'; + + $actual = $conn->query('SELECT * FROM test;', PDO::FETCH_CLASS, 'Row')->fetch(); + tap_assert($expected == $actual, 'fetch row as object'); + + tap_equal($want_drop_table_result, $conn->exec("DROP TABLE test;"), 'drop table'); +} diff --git a/tests/integration/pdo/test_query_fetch_column.inc b/tests/integration/pdo/test_query_fetch_column.inc new file mode 100644 index 000000000..df1c590e5 --- /dev/null +++ b/tests/integration/pdo/test_query_fetch_column.inc @@ -0,0 +1,40 @@ +exec("CREATE TABLE test (id INT, description VARCHAR(10));"), 'create table'); + + tap_equal(1, $conn->exec("INSERT INTO test VALUES (1, 'one');"), 'insert one'); + tap_equal(1, $conn->exec("INSERT INTO test VALUES (2, 'two');"), 'insert two'); + tap_equal(1, $conn->exec("INSERT INTO test VALUES (3, 'three');"), 'insert three'); + + $result = $conn->query('SELECT * FROM test;', PDO::FETCH_COLUMN, 1); + $actual = $result->fetchAll(PDO::FETCH_ASSOC); + $result->closeCursor(); + tap_equal(3, count($actual), 'fetch column'); + + tap_equal($want_drop_table_result, $conn->exec("DROP TABLE test;"), 'drop table'); +} diff --git a/tests/integration/pdo/test_query_fetch_into.inc b/tests/integration/pdo/test_query_fetch_into.inc new file mode 100644 index 000000000..2cc144881 --- /dev/null +++ b/tests/integration/pdo/test_query_fetch_into.inc @@ -0,0 +1,45 @@ +exec("CREATE TABLE test (id INT, description VARCHAR(10));"), 'create table'); + tap_equal(1, $conn->exec("INSERT INTO test VALUES (1, 'one');"), 'insert row'); + + $expected = new Row(); + $expected->id = '1'; + $expected->description = 'one'; + + $actual = new Row(); + $conn->query('SELECT * FROM test;', PDO::FETCH_INTO, $actual)->fetch(); + tap_assert($expected == $actual, 'fetch row into object'); + + tap_equal($want_drop_table_result, $conn->exec("DROP TABLE test;"), 'drop table'); +} diff --git a/tests/integration/pdo/test_slow_sql_1.php b/tests/integration/pdo/test_slow_sql_1.php index 84e0aa06c..86b4aea7f 100644 --- a/tests/integration/pdo/test_slow_sql_1.php +++ b/tests/integration/pdo/test_slow_sql_1.php @@ -31,7 +31,7 @@ "OtherTransaction/php__FILE__", "", "?? SQL id", - "select * from tables limit ?;", + "select * from information_schema.tables limit ?;", "Datastore/statement/MySQL/tables/select", 1, "?? total time", @@ -83,7 +83,7 @@ function test_slow_sql() { global $PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD; $conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD); - $result = $conn->query('select * from tables limit 1;'); + $result = $conn->query('select * from information_schema.tables limit 1;'); } test_slow_sql(); diff --git a/tests/integration/pdo/test_slow_sql_1.php81.php b/tests/integration/pdo/test_slow_sql_1.php81.php index 6d5b4064e..e420b7b97 100644 --- a/tests/integration/pdo/test_slow_sql_1.php81.php +++ b/tests/integration/pdo/test_slow_sql_1.php81.php @@ -31,7 +31,7 @@ "OtherTransaction/php__FILE__", "", "?? SQL id", - "select * from tables limit ?;", + "select * from information_schema.tables limit ?;", "Datastore/statement/MySQL/tables/select", 1, "?? total time", @@ -83,7 +83,7 @@ function test_slow_sql() { global $PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD; $conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD); - $result = $conn->query('select * from tables limit 1;'); + $result = $conn->query('select * from information_schema.tables limit 1;'); } test_slow_sql(); diff --git a/tests/integration/pdo/test_slow_sql_2.php b/tests/integration/pdo/test_slow_sql_2.php index f2f928385..2c3bb54d2 100644 --- a/tests/integration/pdo/test_slow_sql_2.php +++ b/tests/integration/pdo/test_slow_sql_2.php @@ -35,7 +35,7 @@ "OtherTransaction/php__FILE__", "", "?? SQL id", - "select * from tables limit ?;", + "select * from information_schema.tables limit ?;", "Datastore/statement/MySQL/tables/select", 1, "?? total time", @@ -87,7 +87,7 @@ function test_slow_sql() { global $PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD; $conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD); - $stmt = $conn->prepare('select * from tables limit 1;'); + $stmt = $conn->prepare('select * from information_schema.tables limit 1;'); tap_assert($stmt->execute(), 'execute slow query'); } diff --git a/tests/integration/pdo/test_slow_sql_2.php81.php b/tests/integration/pdo/test_slow_sql_2.php81.php index a80a43510..f52008c2c 100644 --- a/tests/integration/pdo/test_slow_sql_2.php81.php +++ b/tests/integration/pdo/test_slow_sql_2.php81.php @@ -35,7 +35,7 @@ "OtherTransaction/php__FILE__", "", "?? SQL id", - "select * from tables limit ?;", + "select * from information_schema.tables limit ?;", "Datastore/statement/MySQL/tables/select", 1, "?? total time", @@ -87,7 +87,7 @@ function test_slow_sql() { global $PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD; $conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD); - $stmt = $conn->prepare('select * from tables limit 1;'); + $stmt = $conn->prepare('select * from information_schema.tables limit 1;'); tap_assert($stmt->execute(), 'execute slow query'); } diff --git a/tests/integration/pdo/test_slow_sql_for_update.php b/tests/integration/pdo/test_slow_sql_for_update.php index 2a074b689..63e75d373 100644 --- a/tests/integration/pdo/test_slow_sql_for_update.php +++ b/tests/integration/pdo/test_slow_sql_for_update.php @@ -28,7 +28,7 @@ "OtherTransaction/php__FILE__", "", "?? SQL id", - "select * from tables limit ? for update;", + "select * from information_schema.tables limit ? for update;", "Datastore/statement/MySQL/tables/select", 1, "?? total time", @@ -51,7 +51,7 @@ function test_slow_sql() { global $PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD; $conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD); - $result = $conn->query('select * from tables limit 1 for update;'); + $result = $conn->query('select * from information_schema.tables limit 1 for update;'); } test_slow_sql(); diff --git a/tests/integration/pdo/test_slow_sql_lock.php b/tests/integration/pdo/test_slow_sql_lock.php index c87464ad3..7f1d07722 100644 --- a/tests/integration/pdo/test_slow_sql_lock.php +++ b/tests/integration/pdo/test_slow_sql_lock.php @@ -28,7 +28,7 @@ "OtherTransaction/php__FILE__", "", "?? SQL id", - "select * from tables limit ? lock in share mode;", + "select * from information_schema.tables limit ? lock in share mode;", "Datastore/statement/MySQL/tables/select", 1, "?? total time", @@ -51,7 +51,7 @@ function test_slow_sql() { global $PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD; $conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD); - $result = $conn->query('select * from tables limit 1 lock in share mode;'); + $result = $conn->query('select * from information_schema.tables limit 1 lock in share mode;'); } test_slow_sql(); diff --git a/tests/integration/pdo/test_slow_sql_semicolon.php b/tests/integration/pdo/test_slow_sql_semicolon.php index 87901b7e5..4f88e76d5 100644 --- a/tests/integration/pdo/test_slow_sql_semicolon.php +++ b/tests/integration/pdo/test_slow_sql_semicolon.php @@ -28,7 +28,7 @@ "OtherTransaction/php__FILE__", "", "?? SQL id", - "select * from tables where engine = ?;", + "select * from information_schema.tables where engine = ?;", "Datastore/statement/MySQL/tables/select", 1, "?? total time", @@ -51,7 +51,7 @@ function test_slow_sql() { global $PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD; $conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD); - $result = $conn->query('select * from tables where engine = \';\';'); + $result = $conn->query('select * from information_schema.tables where engine = \';\';'); } test_slow_sql(); 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 91b1420be..56e161179 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 @@ -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.4", ">=")) { + die("skip: newer test for PHP 8.4+\n"); +} */ /*INI @@ -92,7 +95,7 @@ { "error.message": "foo", "error.class": "E_USER_ERROR", - "code.lineno": 136, + "code.lineno": 139, "code.filepath": "__FILE__", "code.function": "a" } @@ -113,7 +116,7 @@ }, {}, { - "code.lineno": 130, + "code.lineno": 133, "code.filepath": "__FILE__", "code.function": "{closure}" } diff --git a/tests/integration/span_events/test_span_events_are_created_upon_caught_error.php5.php b/tests/integration/span_events/test_span_events_are_created_upon_caught_error.php84.php similarity index 80% rename from tests/integration/span_events/test_span_events_are_created_upon_caught_error.php5.php rename to tests/integration/span_events/test_span_events_are_created_upon_caught_error.php84.php index 22a82caa9..fabe4bddd 100644 --- a/tests/integration/span_events/test_span_events_are_created_upon_caught_error.php5.php +++ b/tests/integration/span_events/test_span_events_are_created_upon_caught_error.php84.php @@ -7,6 +7,14 @@ /*DESCRIPTION Test that span events are correctly created from any eligible segment, even when an error is generated and handled. +PHP 8.4+ names closures differently. +*/ + +/*SKIPIF +=")) { + die("skip: newer test for PHP 8.4\n"); +} */ /*INI @@ -92,7 +95,7 @@ { "error.message": "foo", "error.class": "E_USER_ERROR", - "code.lineno": 108, + "code.lineno": 111, "code.filepath": "__FILE__", "code.function": "a" } diff --git a/tests/integration/span_events/test_span_events_are_created_upon_uncaught_error.php5.php b/tests/integration/span_events/test_span_events_are_created_upon_uncaught_error.php5.php index 5353e0da0..3e17f5371 100644 --- a/tests/integration/span_events/test_span_events_are_created_upon_uncaught_error.php5.php +++ b/tests/integration/span_events/test_span_events_are_created_upon_uncaught_error.php5.php @@ -19,6 +19,13 @@ newrelic.code_level_metrics.enabled=false */ +/*SKIPIF +=")) { + die("skip: newer test for PHP 8.4\n"); +} +*/ + /*EXPECT_SPAN_EVENTS [ "?? agent run id", diff --git a/tests/integration/span_events/test_span_events_are_created_upon_uncaught_error.php84.php b/tests/integration/span_events/test_span_events_are_created_upon_uncaught_error.php84.php new file mode 100644 index 000000000..b56ef56a2 --- /dev/null +++ b/tests/integration/span_events/test_span_events_are_created_upon_uncaught_error.php84.php @@ -0,0 +1,122 @@ + 'FakeDB', + ) +); +a(); 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 index 7670f8a27..24d3e6da2 100644 --- 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 @@ -16,6 +16,9 @@ if (version_compare(PHP_VERSION, "8.0", "<")) { die("skip: test for oapi agent only\n"); } +if (version_compare(PHP_VERSION, "8.4", ">=")) { + die("skip: newer test for PHP 8.4+\n"); +} */ /*INI diff --git a/tests/integration/span_events/test_span_events_are_created_upon_uncaught_handled_exception.php84.php b/tests/integration/span_events/test_span_events_are_created_upon_uncaught_handled_exception.php84.php new file mode 100644 index 000000000..c9e6a3455 --- /dev/null +++ b/tests/integration/span_events/test_span_events_are_created_upon_uncaught_handled_exception.php84.php @@ -0,0 +1,183 @@ + 'FakeDB', + ) +); +a(); + +echo 'this should never be printed'; diff --git a/tests/integration/span_events/test_span_events_max_samples_stored5.php b/tests/integration/span_events/test_span_events_max_samples_stored5.php index 0a7bd2b89..e69f9c400 100644 --- a/tests/integration/span_events/test_span_events_max_samples_stored5.php +++ b/tests/integration/span_events/test_span_events_max_samples_stored5.php @@ -5,7 +5,7 @@ */ /*DESCRIPTION -This is the max that can be sent with DT only. With span limit < 7000, +This is the max that can be sent with DT only. With span limit > 7000, infinite tracing NEEDS to be enabled; otherwise, the daemon will error out with the following type message: `Error: listener: closing connection: maximum message size exceeded, (2886388 > 2097152)` diff --git a/tests/integration/supportability/test_php_and_agent_version_metrics.php b/tests/integration/supportability/test_php_and_agent_version_metrics.php new file mode 100644 index 000000000..bc63110f8 --- /dev/null +++ b/tests/integration/supportability/test_php_and_agent_version_metrics.php @@ -0,0 +1,27 @@ + +query('select * from tables limit 1;'); + $result = $conn->query('select * from information_schema.tables limit 1;'); } test_slow_sql(); diff --git a/tests/lasp/suite-least-secure/test_slow_sql_on.php81.php b/tests/lasp/suite-least-secure/test_slow_sql_on.php81.php index b4e678a8b..a9696beb7 100644 --- a/tests/lasp/suite-least-secure/test_slow_sql_on.php81.php +++ b/tests/lasp/suite-least-secure/test_slow_sql_on.php81.php @@ -31,8 +31,8 @@ [ "OtherTransaction/php__FILE__", "\u003cunknown\u003e", - 2691358128, - "select * from tables limit ?;", + 2279837883, + "select * from information_schema.tables limit ?;", "Datastore/statement/MySQL/tables/select", "??", "??", @@ -85,7 +85,7 @@ function test_slow_sql() global $PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD; $conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD, array()); - $result = $conn->query('select * from tables limit 1;'); + $result = $conn->query('select * from information_schema.tables limit 1;'); } test_slow_sql(); diff --git a/tests/lasp/suite-most-secure/test_slow_sql_off.php b/tests/lasp/suite-most-secure/test_slow_sql_off.php index e5e252eea..e464fd0d0 100644 --- a/tests/lasp/suite-most-secure/test_slow_sql_off.php +++ b/tests/lasp/suite-most-secure/test_slow_sql_off.php @@ -35,7 +35,7 @@ function test_slow_sql() global $PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD; $conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD, array()); - $result = $conn->query('select * from tables limit 1;'); + $result = $conn->query('select * from information_schema.tables limit 1;'); } test_slow_sql(); diff --git a/tests/lasp/suite-random-2/test_slow_sql_on.php b/tests/lasp/suite-random-2/test_slow_sql_on.php index b68fb481f..c1e86a5fa 100644 --- a/tests/lasp/suite-random-2/test_slow_sql_on.php +++ b/tests/lasp/suite-random-2/test_slow_sql_on.php @@ -30,8 +30,8 @@ [ "OtherTransaction/php__FILE__", "\u003cunknown\u003e", - 2691358128, - "select * from tables limit ?;", + 2279837883, + "select * from information_schema.tables limit ?;", "Datastore/statement/MySQL/tables/select", "??", "??", @@ -84,7 +84,7 @@ function test_slow_sql() global $PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD; $conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD, array()); - $result = $conn->query('select * from tables limit 1;'); + $result = $conn->query('select * from information_schema.tables limit 1;'); } test_slow_sql(); diff --git a/tests/lasp/suite-random-2/test_slow_sql_on.php81.php b/tests/lasp/suite-random-2/test_slow_sql_on.php81.php index d229271ef..c25195200 100644 --- a/tests/lasp/suite-random-2/test_slow_sql_on.php81.php +++ b/tests/lasp/suite-random-2/test_slow_sql_on.php81.php @@ -30,8 +30,8 @@ [ "OtherTransaction/php__FILE__", "\u003cunknown\u003e", - 2691358128, - "select * from tables limit ?;", + 2279837883, + "select * from information_schema.tables limit ?;", "Datastore/statement/MySQL/tables/select", "??", "??", @@ -84,7 +84,7 @@ function test_slow_sql() global $PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD; $conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD, array()); - $result = $conn->query('select * from tables limit 1;'); + $result = $conn->query('select * from information_schema.tables limit 1;'); } test_slow_sql(); diff --git a/tests/lasp/suite-random-3/test_slow_sql_off.php b/tests/lasp/suite-random-3/test_slow_sql_off.php index 18c3c7f84..087f130bb 100644 --- a/tests/lasp/suite-random-3/test_slow_sql_off.php +++ b/tests/lasp/suite-random-3/test_slow_sql_off.php @@ -34,7 +34,7 @@ function test_slow_sql() global $PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD; $conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD, array()); - $result = $conn->query('select * from tables limit 1;'); + $result = $conn->query('select * from information_schema.tables limit 1;'); } test_slow_sql(); diff --git a/tests/lasp/suite-random-3/test_slow_sql_on.php b/tests/lasp/suite-random-3/test_slow_sql_on.php index d55b78edf..6ec54ea93 100644 --- a/tests/lasp/suite-random-3/test_slow_sql_on.php +++ b/tests/lasp/suite-random-3/test_slow_sql_on.php @@ -34,7 +34,7 @@ function test_slow_sql() global $PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD; $conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD, array()); - $result = $conn->query('select * from tables limit 1;'); + $result = $conn->query('select * from information_schema.tables limit 1;'); } test_slow_sql(); diff --git a/tests/regression/test_php_1179_pdo_options_crash.php b/tests/regression/test_php_1179_pdo_options_crash.php index e35ca7f0a..9b6419337 100644 --- a/tests/regression/test_php_1179_pdo_options_crash.php +++ b/tests/regression/test_php_1179_pdo_options_crash.php @@ -59,7 +59,7 @@ function test_slow_sql(array $options) global $PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD; $conn = new PDO($PDO_MYSQL_DSN, $MYSQL_USER, $MYSQL_PASSWD, $options); - $result = $conn->query('select * from tables limit 1;'); + $result = $conn->query('select * from information_schema.tables limit 1;'); } $shared_options = array(PDO::ATTR_PERSISTENT => true); diff --git a/trivy.yaml b/trivy.yaml new file mode 100644 index 000000000..e27bd8157 --- /dev/null +++ b/trivy.yaml @@ -0,0 +1,18 @@ +db: + repository: + - mirror.gcr.io/aquasec/trivy-db:2 + +scan: + scanners: + - vuln + - misconfig + skip-dirs: vendor + +severities: + - CRITICAL + - HIGH + - MEDIUM + - LOW + +vulnerability: + ignore-unfixed: true