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/lib_aws_sdk_php.c b/agent/lib_aws_sdk_php.c index 1bd144cca..46a71ce3c 100644 --- a/agent/lib_aws_sdk_php.c +++ b/agent/lib_aws_sdk_php.c @@ -429,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 = '';" diff --git a/agent/lib_php_amqplib.c b/agent/lib_php_amqplib.c new file mode 100644 index 000000000..ec246ecc7 --- /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 detection will be called 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 4a8765f41..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. 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/nr_segment.c b/axiom/nr_segment.c index 4aaf7c120..ade63d454 100644 --- a/axiom/nr_segment.c +++ b/axiom/nr_segment.c @@ -331,6 +331,15 @@ static void nr_populate_message_spans(nr_span_event_t* span_event, 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, @@ -640,7 +649,10 @@ bool nr_segment_set_message(nr_segment_t* segment, .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 diff --git a/axiom/nr_segment.h b/axiom/nr_segment.h index f35ab6224..646f11e5f 100644 --- a/axiom/nr_segment.h +++ b/axiom/nr_segment.h @@ -128,6 +128,19 @@ typedef struct _nr_segment_message_t { 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 { diff --git a/axiom/nr_segment_message.c b/axiom/nr_segment_message.c index 92f8babd1..d07a3217e 100644 --- a/axiom/nr_segment_message.c +++ b/axiom/nr_segment_message.c @@ -12,6 +12,7 @@ #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. @@ -39,6 +40,11 @@ static void nr_segment_message_set_attrs( 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); @@ -96,6 +102,8 @@ static char* nr_segment_message_create_metrics( 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; @@ -156,6 +164,19 @@ static char* nr_segment_message_create_metrics( 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} @@ -167,12 +188,9 @@ static char* nr_segment_message_create_metrics( 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, - nr_strempty(message_params->destination_name) - ? "" - : message_params->destination_name); + 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); diff --git a/axiom/nr_segment_message.h b/axiom/nr_segment_message.h index 2104e3f35..6917f78de 100644 --- a/axiom/nr_segment_message.h +++ b/axiom/nr_segment_message.h @@ -38,12 +38,27 @@ typedef struct { 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_message_params_t + * 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. */ diff --git a/axiom/nr_segment_private.c b/axiom/nr_segment_private.c index f063862b7..ea5bbf8cc 100644 --- a/axiom/nr_segment_private.c +++ b/axiom/nr_segment_private.c @@ -47,6 +47,8 @@ void nr_segment_message_destroy_fields(nr_segment_message_t* message) { 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( diff --git a/axiom/nr_segment_traces.c b/axiom/nr_segment_traces.c index 153ccd5c2..d2f1fddac 100644 --- a/axiom/nr_segment_traces.c +++ b/axiom/nr_segment_traces.c @@ -170,6 +170,16 @@ static void add_typed_attributes_to_buffer(nrbuf_t* buf, 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: diff --git a/axiom/nr_span_event.c b/axiom/nr_span_event.c index 00987881a..7c8c2dbe0 100644 --- a/axiom/nr_span_event.c +++ b/axiom/nr_span_event.c @@ -377,6 +377,42 @@ void nr_span_event_set_message(nr_span_event_t* event, 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; } } @@ -535,10 +571,45 @@ const char* nr_span_event_get_message(const nr_span_event_t* event, 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 2a87b2306..33dced5c2 100644 --- a/axiom/nr_span_event.h +++ b/axiom/nr_span_event.h @@ -15,7 +15,12 @@ #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" @@ -80,6 +85,9 @@ 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; /* @@ -211,7 +219,7 @@ extern void nr_span_event_set_external_status(nr_span_event_t* event, const uint64_t status); /* - * Purpose : Set a message attribute. + * 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. @@ -222,6 +230,19 @@ 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 4f55c6f2f..01d544fc2 100644 --- a/axiom/nr_span_event_private.h +++ b/axiom/nr_span_event_private.h @@ -48,6 +48,9 @@ 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/tests/test_segment_message.c b/axiom/tests/test_segment_message.c index a68403caf..bdb944f39 100644 --- a/axiom/tests/test_segment_message.c +++ b/axiom/tests/test_segment_message.c @@ -31,6 +31,9 @@ typedef struct { 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) { @@ -86,6 +89,17 @@ static void test_message_segment(nr_segment_message_params_t* params, 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); } @@ -927,6 +941,197 @@ static void test_segment_message_aws_operation(void) { .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. @@ -935,6 +1140,9 @@ static void test_segment_message_parameters_enabled(void) { /* 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", @@ -948,7 +1156,7 @@ static void test_segment_message_parameters_enabled(void) { true /* enable attributes */, (segment_message_expecteds_t){ .test_name = "Test true message_parameters_enabled", - .name = "MessageBroker/SQS/Topic/Produce/Named/my_destination", + .name = "MessageBroker/SQS/Topic/Produce/Named/publish_name", .txn_rollup_metric = "MessageBroker/all", .library_metric = "MessageBroker/SQS/all", .num_metrics = 1, @@ -958,6 +1166,9 @@ static void test_segment_message_parameters_enabled(void) { .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"}); /* @@ -966,6 +1177,8 @@ static void test_segment_message_parameters_enabled(void) { */ 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", @@ -989,6 +1202,9 @@ static void test_segment_message_parameters_enabled(void) { .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"}); } @@ -1005,6 +1221,9 @@ void test_main(void* p NRUNUSED) { 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_span_event.c b/axiom/tests/test_span_event.c index ff44ab1cd..a00700a6b 100644 --- a/axiom/tests/test_span_event.c +++ b/axiom/tests/test_span_event.c @@ -483,7 +483,7 @@ static void test_span_events_extern_get_and_set(void) { nr_span_event_destroy(&span); } -static void test_span_event_message_string_get_and_set(void) { +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 @@ -506,36 +506,93 @@ static void test_span_event_message_string_get_and_set(void) { 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 component we set 1", "chicken", + "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 component we set 2", "oracle", + "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 component we set 1", "chicken", + "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 component we set 2", "oracle", + "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 component we set 1", "chicken", + "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 component we set 2", "oracle", + "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); } @@ -724,7 +781,7 @@ void test_main(void* p NRUNUSED) { test_span_event_duration(); test_span_event_datastore_string_get_and_set(); test_span_events_extern_get_and_set(); - test_span_event_message_string_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();