diff --git a/.github/workflows/test-agent.yml b/.github/workflows/test-agent.yml index fff60dae5..efb68d872 100644 --- a/.github/workflows/test-agent.yml +++ b/.github/workflows/test-agent.yml @@ -44,6 +44,39 @@ jobs: 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: diff --git a/VERSION b/VERSION index 7ef698131..146d5de79 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -11.5.0 +11.6.0 diff --git a/agent/Makefile.frag b/agent/Makefile.frag index fbff46d69..2bfc562a8 100644 --- a/agent/Makefile.frag +++ b/agent/Makefile.frag @@ -93,6 +93,7 @@ TEST_BINARIES = \ 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 \ diff --git a/agent/config.m4 b/agent/config.m4 index 6671bcd54..19785b2a3 100644 --- a/agent/config.m4 +++ b/agent/config.m4 @@ -231,7 +231,7 @@ if test "$PHP_NEWRELIC" = "yes"; then 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 \ - lib_composer.c" + lib_composer.c lib_php_amqplib.c" PHP_NEW_EXTENSION(newrelic, $FRAMEWORKS $LIBRARIES $NEWRELIC_AGENT, $ext_shared,, $(NEWRELIC_CFLAGS)) PHP_SUBST(NEWRELIC_CFLAGS) diff --git a/agent/fw_hooks.h b/agent/fw_hooks.h index 711c3b618..93665862c 100644 --- a/agent/fw_hooks.h +++ b/agent/fw_hooks.h @@ -46,6 +46,7 @@ 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); 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/lib_aws_sdk_php.c b/agent/lib_aws_sdk_php.c index 51ef17fda..46a71ce3c 100644 --- a/agent/lib_aws_sdk_php.c +++ b/agent/lib_aws_sdk_php.c @@ -14,10 +14,401 @@ #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") @@ -38,6 +429,12 @@ void nr_lib_aws_sdk_php_handle_version() { 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 = '';" @@ -162,4 +559,12 @@ void nr_aws_sdk_php_enable() { /* 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 index 11ffcd7aa..6eb0c1e54 100644 --- a/agent/lib_aws_sdk_php.h +++ b/agent/lib_aws_sdk_php.h @@ -7,6 +7,20 @@ #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 @@ -20,4 +34,63 @@ 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_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/php_execute.c b/agent/php_execute.c index 058fea30c..41c430d11 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -491,6 +491,10 @@ static nr_library_table_t libraries[] = { {"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. @@ -1255,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_newrelic.h b/agent/php_newrelic.h index 1860b5167..6afaf531a 100644 --- a/agent/php_newrelic.h +++ b/agent/php_newrelic.h @@ -596,6 +596,12 @@ nrinibool_t 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 /* * pid and user_function_wrappers are used to store user function wrappers. diff --git a/agent/php_nrini.c b/agent/php_nrini.c index 6c21d1bdf..4a2f7c471 100644 --- a/agent/php_nrini.c +++ b/agent/php_nrini.c @@ -3100,6 +3100,17 @@ STD_PHP_INI_ENTRY_EX("newrelic.vulnerability_management.composer_api.enabled", 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 b5b57975c..bc0ea6b7e 100644 --- a/agent/php_txn.c +++ b/agent/php_txn.c @@ -854,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 @@ -1165,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); } diff --git a/agent/scripts/newrelic.ini.template b/agent/scripts/newrelic.ini.template index 4ed06a091..b406628a9 100644 --- a/agent/scripts/newrelic.ini.template +++ b/agent/scripts/newrelic.ini.template @@ -1341,3 +1341,14 @@ newrelic.daemon.logfile = "/var/log/newrelic/newrelic-daemon.log" ; 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_lib_aws_sdk_php.c b/agent/tests/test_lib_aws_sdk_php.c index 91fa88b26..0ac040424 100644 --- a/agent/tests/test_lib_aws_sdk_php.c +++ b/agent/tests/test_lib_aws_sdk_php.c @@ -6,12 +6,324 @@ #include "tlib_php.h" #include "php_agent.h" -#include "lib_aws_sdk_php.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, @@ -151,6 +463,10 @@ void test_main(void* p NRUNUSED) { 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) {} 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/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_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_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 4e5c4c41e..06c0214ef 100644 --- a/axiom/nr_txn.c +++ b/axiom/nr_txn.c @@ -1221,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( @@ -2492,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)); @@ -2588,6 +2596,10 @@ nrobj_t* nr_txn_event_intrinsics(const nrtxn_t* txn) { "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); diff --git a/axiom/nr_txn.h b/axiom/nr_txn.h index 8874260ec..55871f1b1 100644 --- a/axiom/nr_txn.h +++ b/axiom/nr_txn.h @@ -132,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 { diff --git a/axiom/nr_version.c b/axiom/nr_version.c index 6125e272e..e6e54a250 100644 --- a/axiom/nr_version.c +++ b/axiom/nr_version.c @@ -47,8 +47,9 @@ * bowenite 30Sep2024 (11.2) * corundum 21Oct2024 (11.3) * diamond 09Dec2024 (11.4) + * emerald 13Jan2025 (11.5) */ -#define NR_CODENAME "emerald" +#define NR_CODENAME "fluorite" const char* nr_version(void) { return NR_STR2(NR_VERSION); 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 1107d9a79..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,10 +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_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_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 279bbbbee..5dc68deae 100644 --- a/axiom/tests/test_txn.c +++ b/axiom/tests/test_txn.c @@ -2006,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); } @@ -2962,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", @@ -2977,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}," @@ -2989,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}," @@ -3910,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); @@ -3926,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\"" "}," @@ -3951,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\"," @@ -4041,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); @@ -4059,6 +4072,8 @@ static void test_create_event(void) { "\"externalCallCount\":1," "\"databaseDuration\":1.00000," "\"databaseCallCount\":1," + "\"messageDuration\":2.00000," + "\"messageCallCount\":1," "\"error\":false" "}," "{\"user_long\":1}," @@ -4082,6 +4097,8 @@ static void test_create_event(void) { "\"externalCallCount\":1," "\"databaseDuration\":1.00000," "\"databaseCallCount\":1," + "\"messageDuration\":2.00000," + "\"messageCallCount\":1," "\"error\":false" "}," "{\"user_long\":1}," @@ -4108,6 +4125,8 @@ static void test_create_event(void) { "\"externalCallCount\":1," "\"databaseDuration\":1.00000," "\"databaseCallCount\":1," + "\"messageDuration\":2.00000," + "\"messageCallCount\":1," "\"error\":false" "}," "{\"user_long\":1}," @@ -4132,6 +4151,8 @@ static void test_create_event(void) { "\"externalCallCount\":1," "\"databaseDuration\":1.00000," "\"databaseCallCount\":1," + "\"messageDuration\":2.00000," + "\"messageCallCount\":1," "\"error\":false" "}," "{\"user_long\":1}," @@ -4155,6 +4176,8 @@ static void test_create_event(void) { "\"externalCallCount\":1," "\"databaseDuration\":1.00000," "\"databaseCallCount\":1," + "\"messageDuration\":2.00000," + "\"messageCallCount\":1," "\"error\":false" "}," "{\"user_long\":1}," @@ -8059,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", @@ -8068,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); 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/go.mod b/daemon/go.mod index 5685974d5..b76b8360f 100644 --- a/daemon/go.mod +++ b/daemon/go.mod @@ -2,7 +2,7 @@ module github.com/newrelic/newrelic-php-agent/daemon go 1.21 -toolchain go1.23.1 +toolchain go1.23.6 require ( github.com/golang/protobuf v1.5.3 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/tests/integration/synthetics/test_happy_path_with_dt.php b/tests/integration/synthetics/test_happy_path_with_dt.php index bd415e684..14c3b3b88 100644 --- a/tests/integration/synthetics/test_happy_path_with_dt.php +++ b/tests/integration/synthetics/test_happy_path_with_dt.php @@ -62,7 +62,7 @@ "nr.syntheticsJobId": "jjjjjjj-jjjj-1234-jjjj-jjjjjjjjjjjj", "nr.syntheticsMonitorId": "mmmmmmm-mmmm-1234-mmmm-mmmmmmmmmmmm", "externalDuration": "??", - "externalCallCount": 1, + "externalCallCount": 1, "guid": "??", "sampled": true, "priority": "??",