diff --git a/.github/workflows/code-coverage-baseline.yml b/.github/workflows/code-coverage-baseline.yml index dcef64963..ab5b213f8 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'] 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'] include: - codecov: 0 - platform: gnu diff --git a/.github/workflows/test-agent.yml b/.github/workflows/test-agent.yml index d40384e13..78a27ee41 100644 --- a/.github/workflows/test-agent.yml +++ b/.github/workflows/test-agent.yml @@ -19,6 +19,31 @@ 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 daemon-unit-tests: runs-on: ubuntu-latest env: @@ -72,12 +97,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'] exclude: - - arch: arm64 - php: '7.0' - - arch: arm64 - php: '7.1' - arch: arm64 php: '7.2' - arch: arm64 @@ -117,9 +138,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 @@ -183,12 +202,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'] 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/VERSION b/VERSION index b7604b0c8..4a68b557e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -10.22.0 +11.0.0 \ No newline at end of file diff --git a/agent/Makefile.frag b/agent/Makefile.frag index f11dfbf75..def094486 100644 --- a/agent/Makefile.frag +++ b/agent/Makefile.frag @@ -88,6 +88,7 @@ TEST_BINARIES = \ tests/test_globals \ tests/test_internal_instrument \ tests/test_hash \ + tests/test_lib_aws_sdk_php \ tests/test_mongodb \ tests/test_monolog \ tests/test_mysql \ diff --git a/agent/config.m4 b/agent/config.m4 index 8d9e42b24..a77122c37 100644 --- a/agent/config.m4 +++ b/agent/config.m4 @@ -228,7 +228,7 @@ 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)) diff --git a/agent/fw_hooks.h b/agent/fw_hooks.h index baaf400c7..e78f65cbd 100644 --- a/agent/fw_hooks.h +++ b/agent/fw_hooks.h @@ -45,6 +45,7 @@ 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); diff --git a/agent/lib_aws_sdk_php.c b/agent/lib_aws_sdk_php.c new file mode 100644 index 000000000..d9055690d --- /dev/null +++ b/agent/lib_aws_sdk_php.c @@ -0,0 +1,177 @@ +/* + * 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 "lib_aws_sdk_php.h" + +#define PHP_PACKAGE_NAME "aws/aws-sdk-php" + +/* + * 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 profject settings, it 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. However, 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 a fatal + * unrecoverable uncatchable error error whenever it cannot find a class. While + * calling this from nr_aws_sdk_php_enable would have been great and would allow + * the sdk version value to be set only once, to avoid the very unlikely but not + * impossible fatal error, this will be called from the + * "Aws\\ClientResolver::_apply_user_agent" wrapper which GUARANTEES that + * aws/sdk exists and is already loaded. + * + * + * Additionally given that aws-sdk-php is currently detected from the + * AwsClient.php file, this method will always be called when a client is + * created unlike Sdk::construct which doesn't show with PHP 8.2+. + * + * Using Aws/Sdk::__construct for version is currently nonviable as it is + * unreliable as a version determiner. + * Having separate functionality to extract from Aws/Sdk::__construct + * is both not required and is redundant and causes additional overhead and + * so only one function is needed to extract version. + * + * Aws\\ClientResolver::_apply_user_agent a reliable function as it is + * always called on client initialization since it is key to populating + * the request headers, and it loads Sdk by default. + * + * Concerns about future/past proofing to the checking prioritized the following + * implementation vs using the eval method. + */ +void nr_lib_aws_sdk_php_handle_version() { + zval* zval_version = NULL; + zend_class_entry* class_entry = NULL; + char* version = NULL; + + class_entry = nr_php_find_class("aws\\sdk"); + if (NULL != class_entry) { + zval_version = nr_php_get_class_constant(class_entry, "VERSION"); + + if (nr_php_is_zval_non_empty_string(zval_version)) { + version = Z_STRVAL_P(zval_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_php_zval_free(&zval_version); +} + +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; + strcpy(cp, PHP_AWS_SDK_SERVICE_NAME_METRIC_PREFIX); + cp += PHP_AWS_SDK_SERVICE_NAME_METRIC_PREFIX_LEN - 1; + strlcpy(cp, service_name, MAX_AWS_SERVICE_NAME_LEN); + nrm_force_add(NRPRG(txn) ? NRTXN(unscoped_metrics) : 0, buf, 0); +} + +NR_PHP_WRAPPER(nr_create_aws_sdk_version_metrics) { + (void)wraprec; + NR_PHP_WRAPPER_CALL; + nr_lib_aws_sdk_php_handle_version(); +} +NR_PHP_WRAPPER_END + +/* + * 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 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 + * tied to Aws/ClientResolver::_apply_user_agent which is ALWAYS called when + * dealing with aws clients. It will not be computed from + * Aws/Sdk::__constructor which would at best be duplicate info and worst would + * never be ignored until a client is called. + */ +void nr_aws_sdk_php_enable() { + /* This will be used to extract the version. */ + nr_php_wrap_user_function(NR_PSTR("Aws\\ClientResolver::_apply_user_agent"), + nr_create_aws_sdk_version_metrics); + /* Called when initializing all other Clients */ + nr_php_wrap_user_function(NR_PSTR("Aws\\AwsClient::parseClass"), + nr_create_aws_service_metric); + + if (NRINI(vulnerability_management_package_detection_enabled)) { + nr_txn_add_php_package(NRPRG(txn), PHP_PACKAGE_NAME, + PHP_PACKAGE_VERSION_UNKNOWN); + } +} diff --git a/agent/lib_aws_sdk_php.h b/agent/lib_aws_sdk_php.h new file mode 100644 index 000000000..11ffcd7aa --- /dev/null +++ b/agent/lib_aws_sdk_php.h @@ -0,0 +1,23 @@ +/* + * 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 + +#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); + +#endif /* LIB_AWS_SDK_PHP_HDR */ diff --git a/agent/lib_mongodb.c b/agent/lib_mongodb.c index ad1786373..c21e1e01b 100644 --- a/agent/lib_mongodb.c +++ b/agent/lib_mongodb.c @@ -356,6 +356,14 @@ void nr_mongodb_enable() { 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 */ diff --git a/agent/newrelic-install.sh b/agent/newrelic-install.sh index d61137721..d0959b252 100755 --- a/agent/newrelic-install.sh +++ b/agent/newrelic-install.sh @@ -327,10 +327,10 @@ 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 @@ -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 @@ -548,8 +546,6 @@ add_to_path /usr/local/php-8.3/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 @@ -558,8 +554,6 @@ 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/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 @@ -571,8 +565,6 @@ add_to_path /usr/php/8.3/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 @@ -1037,22 +1029,6 @@ for this copy of PHP. We apologize for the inconvenience. fi case "${pi_ver}" in - 7.0.*) - warning_message="${pdir}: Support for PHP '${pi_ver}' in the New Relic PHP agent is deprecated." - if [ -z "${NR_INSTALL_SILENT}" ]; then - echo $warning_message - fi - log $warning_message - ;; - - 7.1.*) - warning_message="${pdir}: Support for PHP '${pi_ver}' in the New Relic PHP agent is deprecated." - if [ -z "${NR_INSTALL_SILENT}" ]; then - echo $warning_message - fi - log $warning_message - ;; - 7.2.*) ;; @@ -1249,8 +1225,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" ;; diff --git a/agent/php_execute.c b/agent/php_execute.c index 80ad27bdf..3ecb33100 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -484,6 +484,9 @@ 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 */ diff --git a/agent/php_newrelic.h b/agent/php_newrelic.h index e114a6464..04ebd1ad2 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 diff --git a/agent/php_nrini.c b/agent/php_nrini.c index 2ed62eb10..6fdd898d3 100644 --- a/agent/php_nrini.c +++ b/agent/php_nrini.c @@ -2879,6 +2879,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 diff --git a/agent/php_txn.c b/agent/php_txn.c index 7e445c762..cdfa1269f 100644 --- a/agent/php_txn.c +++ b/agent/php_txn.c @@ -748,6 +748,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); 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/tests/test_lib_aws_sdk_php.c b/agent/tests/test_lib_aws_sdk_php.c new file mode 100644 index 000000000..8ddffce9d --- /dev/null +++ b/agent/tests/test_lib_aws_sdk_php.c @@ -0,0 +1,176 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "tlib_php.h" + +#include "php_agent.h" +#include "lib_aws_sdk_php.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_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" +#define LIBRARY_MAJOR_VERSION "7" +#define LIBRARY_MAJOR_VERSION_2 "10" +#define LIBRARY_MAJOR_VERSION_3 "100" +#define LIBRARY_MAJOR_VERSION_4 "4.23" +#define LIBRARY_MAJOR_VERSION_55 "55.34" +#define LIBRARY_MAJOR_VERSION_6123 "6123.45" +#define LIBRARY_MAJOR_VERSION_0 "0.4.5" +#define PACKAGE_METRIC "Supportability/PHP/package/" LIBRARY_NAME + + /* + * If lib_aws_sdk_php_handle_version function is ever called, we have already + * detected the aws-sdk-php library. + */ + + /* + * Aws/Sdk exists. Should return aws package metric with + * version + */ + tlib_php_request_start(); + declare_aws_sdk_class("Aws", "Sdk", LIBRARY_MAJOR_VERSION); + nr_lib_aws_sdk_php_handle_version(); + tlib_pass_if_not_null("version test 1: package metric created", + nrm_find(NRPRG(txn)->unscoped_metrics, PACKAGE_METRIC + "/" LIBRARY_MAJOR_VERSION "/detected")); + tlib_php_request_end(); + + tlib_php_request_start(); + declare_aws_sdk_class("Aws", "Sdk", LIBRARY_MAJOR_VERSION_2); + nr_lib_aws_sdk_php_handle_version(); + tlib_pass_if_not_null("version test 2: package metric created", + nrm_find(NRPRG(txn)->unscoped_metrics, PACKAGE_METRIC + "/" LIBRARY_MAJOR_VERSION_2 "/detected")); + tlib_php_request_end(); + + tlib_php_request_start(); + declare_aws_sdk_class("Aws", "Sdk", LIBRARY_MAJOR_VERSION_3); + nr_lib_aws_sdk_php_handle_version(); + tlib_pass_if_not_null("version test 3: package metric created", + nrm_find(NRPRG(txn)->unscoped_metrics, PACKAGE_METRIC + "/" LIBRARY_MAJOR_VERSION_3 "/detected")); + tlib_php_request_end(); + + tlib_php_request_start(); + declare_aws_sdk_class("Aws", "Sdk", LIBRARY_MAJOR_VERSION_4); + nr_lib_aws_sdk_php_handle_version(); + tlib_pass_if_not_null( + "version test 4: package metric created", + nrm_find(NRPRG(txn)->unscoped_metrics, PACKAGE_METRIC "/4/detected")); + tlib_php_request_end(); + + tlib_php_request_start(); + declare_aws_sdk_class("Aws", "Sdk", LIBRARY_MAJOR_VERSION_55); + nr_lib_aws_sdk_php_handle_version(); + tlib_pass_if_not_null( + "version test 5: package metric created", + nrm_find(NRPRG(txn)->unscoped_metrics, PACKAGE_METRIC "/55/detected")); + tlib_php_request_end(); + + tlib_php_request_start(); + declare_aws_sdk_class("Aws", "Sdk", LIBRARY_MAJOR_VERSION_6123); + nr_lib_aws_sdk_php_handle_version(); + tlib_pass_if_not_null( + "version test 6: package metric created", + nrm_find(NRPRG(txn)->unscoped_metrics, PACKAGE_METRIC "/6123/detected")); + tlib_php_request_end(); + + tlib_php_request_start(); + declare_aws_sdk_class("Aws", "Sdk", LIBRARY_MAJOR_VERSION_0); + nr_lib_aws_sdk_php_handle_version(); + tlib_pass_if_not_null( + "version test 7: package metric created", + nrm_find(NRPRG(txn)->unscoped_metrics, PACKAGE_METRIC "/0/detected")); + tlib_php_request_end(); + + /* + * Aws/Sdk does not exist, should not return package metric if no version + * This case should never happen in real situations. + */ + tlib_php_request_start(); + nr_lib_aws_sdk_php_handle_version(); + tlib_pass_if_null("aws library metric created", + nrm_find(NRPRG(txn)->unscoped_metrics, PACKAGE_METRIC)); + 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(); +} +#else +void test_main(void* p NRUNUSED) {} +#endif diff --git a/axiom/nr_distributed_trace.c b/axiom/nr_distributed_trace.c index 151ae6406..bdc587e24 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'; + } + 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_txn.c b/axiom/nr_txn.c index 2ea39bb26..c3667db81 100644 --- a/axiom/nr_txn.c +++ b/axiom/nr_txn.c @@ -633,7 +633,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, diff --git a/axiom/nr_txn.h b/axiom/nr_txn.h index dc2cf014b..a6f942f66 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 diff --git a/axiom/nr_version.c b/axiom/nr_version.c index 0000847fd..8f4a3646d 100644 --- a/axiom/nr_version.c +++ b/axiom/nr_version.c @@ -45,8 +45,9 @@ * viburnum 18Mar2024 (10.19) * wallflower 06May2024 (10.20) * xerophyllum 20May2024 (10.21) + * yarrow 26Jun2024 (10.22) */ -#define NR_CODENAME "yarrow" +#define NR_CODENAME "zinnia" const char* nr_version(void) { return NR_STR2(NR_VERSION); 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_txn.c b/axiom/tests/test_txn.c index 905189c2a..59b19f9b0 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); @@ -5940,6 +5941,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 +5974,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 +6287,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 +6736,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 +7538,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 +7554,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 +7578,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) { 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/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..a7354c622 100644 --- a/daemon/go.mod +++ b/daemon/go.mod @@ -1,7 +1,7 @@ module github.com/newrelic/newrelic-php-agent/daemon go 1.21 -toolchain go1.22.3 +toolchain go1.22.5 require ( github.com/golang/protobuf v1.5.3 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..375954b0d 100644 --- a/daemon/internal/newrelic/integration/php_packages.go +++ b/daemon/internal/newrelic/integration/php_packages.go @@ -51,13 +51,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{} @@ -279,11 +280,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 diff --git a/daemon/internal/newrelic/integration/test.go b/daemon/internal/newrelic/integration/test.go index 4a52f31a3..0f9342030 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,114 @@ 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 + if (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. @@ -694,15 +804,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/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/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/docs/development.md b/docs/development.md index c85755903..11700cdee 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', and `8.3`. ## Build the PHP Agent diff --git a/make/php_versions.mk b/make/php_versions.mk index 0629bfabd..e0a63e9b9 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.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..8eee97388 100644 --- a/make/release.mk +++ b/make/release.mk @@ -170,8 +170,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/integration/distributed_tracing/newrelic/test_default_trace_id.php b/tests/integration/distributed_tracing/newrelic/test_default_trace_id.php new file mode 100644 index 000000000..e0e027972 --- /dev/null +++ b/tests/integration/distributed_tracing/newrelic/test_default_trace_id.php @@ -0,0 +1,30 @@ + 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)`