diff --git a/agent/lib_aws_sdk_php.c b/agent/lib_aws_sdk_php.c index 51ef17fda..b706df518 100644 --- a/agent/lib_aws_sdk_php.c +++ b/agent/lib_aws_sdk_php.c @@ -14,10 +14,413 @@ #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* segment, + char* command_name_string, + size_t command_name_len, + NR_EXECUTE_PROTO) { + char* command_arg_value = 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 == 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 + + 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(segment, &cloud_attrs); + + /* Now end the instrumented segment as a message segment. */ + nr_segment_message_end(&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_before) { + (void)wraprec; + nr_segment_t* segment = NULL; + /* + * Start a segment in case it needs to become an external, message, or + * datastore segment. + */ + segment = nr_segment_start(NRPRG(txn), NULL, NULL); + if (NULL != segment) { + segment->wraprec = auto_segment->wraprec; + } +} +NR_PHP_WRAPPER_END + +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 + + if (NR_SEGMENT_CUSTOM == auto_segment->type) { + /* + * We need to end the segment that we started in the 'before' wrapper if + * it wasn't handled and ended by the handling function. Handling + * function would have changed the segment type from from default + * (`NR_SEGMENT_CUSTOM`) if it ended it. + */ + nr_segment_discard(&auto_segment); + } + + /* + * 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 + */ + + segment = nr_txn_get_current_segment(NRPRG(txn), NULL); + if (NULL != segment) { + real_class_and_command + = nr_formatf("Custom/%s::%s", klass, command_name_string); + nr_segment_set_name(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") @@ -162,4 +565,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"), nr_aws_client_call_before, + 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/php_execute.c b/agent/php_execute.c index 058fea30c..4a8765f41 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -1255,12 +1255,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/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/axiom/nr_segment_message.c b/axiom/nr_segment_message.c index 56e9ed2b9..92f8babd1 100644 --- a/axiom/nr_segment_message.c +++ b/axiom/nr_segment_message.c @@ -189,6 +189,7 @@ bool nr_segment_message_end(nr_segment_t** segment_ptr, nr_segment_t* segment; nrtime_t duration = 0; char* scoped_metric = NULL; + nr_segment_t* child = NULL; if (NULL == segment_ptr) { return false; @@ -206,12 +207,15 @@ bool nr_segment_message_end(nr_segment_t** segment_ptr, * 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++) { - nr_segment_t* child = nr_segment_children_get(&segment->children, i); - nr_segment_discard(&child); + 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);