From 5d7dc5d162b25500a6a903b8f7290b800dc5320f Mon Sep 17 00:00:00 2001 From: Amber Sistla Date: Sun, 30 Nov 2025 19:06:13 -0700 Subject: [PATCH 1/6] =?UTF-8?q?feat(agent):=20Add=20new=20NR=5FMESSAGE=5FD?= =?UTF-8?q?ESTINATION=5FTYPE=5FSTREAM=20=E2=80=9CStream=E2=80=9D=20to=20me?= =?UTF-8?q?ssage=5Fsegment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- axiom/nr_segment_message.c | 3 +++ axiom/nr_segment_message.h | 3 ++- axiom/tests/test_segment_message.c | 23 +++++++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/axiom/nr_segment_message.c b/axiom/nr_segment_message.c index d07a3217e..9febd7f93 100644 --- a/axiom/nr_segment_message.c +++ b/axiom/nr_segment_message.c @@ -160,6 +160,9 @@ static char* nr_segment_message_create_metrics( case NR_MESSAGE_DESTINATION_TYPE_EXCHANGE: destination_type_string = "Exchange"; break; + case NR_MESSAGE_DESTINATION_TYPE_STREAM: + destination_type_string = "Stream"; + break; default: destination_type_string = ""; break; diff --git a/axiom/nr_segment_message.h b/axiom/nr_segment_message.h index 6917f78de..54fe58e1d 100644 --- a/axiom/nr_segment_message.h +++ b/axiom/nr_segment_message.h @@ -19,7 +19,8 @@ typedef enum _nr_segment_message_destination_type_t { NR_MESSAGE_DESTINATION_TYPE_TOPIC, NR_MESSAGE_DESTINATION_TYPE_TEMP_QUEUE, NR_MESSAGE_DESTINATION_TYPE_TEMP_TOPIC, - NR_MESSAGE_DESTINATION_TYPE_EXCHANGE + NR_MESSAGE_DESTINATION_TYPE_EXCHANGE, + NR_MESSAGE_DESTINATION_TYPE_STREAM } nr_segment_message_destination_type_t; typedef struct { diff --git a/axiom/tests/test_segment_message.c b/axiom/tests/test_segment_message.c index bdb944f39..5af99c1ec 100644 --- a/axiom/tests/test_segment_message.c +++ b/axiom/tests/test_segment_message.c @@ -253,6 +253,29 @@ static void test_segment_message_destination_type(void) { .cloud_resource_id = NULL, .server_address = NULL, .aws_operation = NULL}); + + /* Test NR_MESSAGE_DESTINATION_TYPE_STREAM destination type */ + test_message_segment( + &(nr_segment_message_params_t){ + .library = "Kinesis", + .message_action = NR_SPANKIND_PRODUCER, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_STREAM, + .destination_name = "my_stream_name"}, + &(nr_segment_cloud_attrs_t){0}, true /* enable attributes */, + (segment_message_expecteds_t){ + .test_name + = "Test NR_MESSAGE_DESTINATION_TYPE_STREAM destination type", + .name = "MessageBroker/Kinesis/Stream/Produce/Named/my_stream_name", + .txn_rollup_metric = "MessageBroker/all", + .library_metric = "MessageBroker/Kinesis/all", + .num_metrics = 1, + .destination_name = "my_stream_name", + .cloud_region = NULL, + .cloud_account_id = NULL, + .messaging_system = NULL, + .cloud_resource_id = NULL, + .server_address = NULL, + .aws_operation = NULL}); } static void test_segment_message_message_action(void) { From 965896a25a853e7d0c36dc12ed44d5428d50d8a8 Mon Sep 17 00:00:00 2001 From: Amber Sistla Date: Sun, 30 Nov 2025 19:24:23 -0700 Subject: [PATCH 2/6] feat(agent): aws-sdk-php kinesis data stream instrumentation Instrument kinesis data stream putRecord(producer), putRecords(producer), getRecords(consumer) as new message segments. Required new library kinesis and new platform aws_kinesis_data_streams. Add unit tests --- agent/lib_aws_sdk_php.c | 264 ++++++++++++++++++++ agent/lib_aws_sdk_php.h | 57 ++++- agent/tests/test_lib_aws_sdk_php.c | 383 ++++++++++++++++++++++++++++- 3 files changed, 698 insertions(+), 6 deletions(-) diff --git a/agent/lib_aws_sdk_php.c b/agent/lib_aws_sdk_php.c index 596c3995e..6fbf756f2 100644 --- a/agent/lib_aws_sdk_php.c +++ b/agent/lib_aws_sdk_php.c @@ -28,6 +28,266 @@ #if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO /* PHP8.1+ */ /* Service instrumentation only supported above PHP 8.1+*/ +/* + * Note: For Kinesis data streams, see the following for information regarding + * inputs/outputs 1) underlying AWS API: + * https://docs.aws.amazon.com/kinesis/latest/APIReference/API_Operations.html + * 2) aws-sdk-php API: + * https://docs.aws.amazon.com/aws-sdk-php/v3/api/class-Aws.Kinesis.KinesisClient.html + * + * The Kinesis Data Stream ARN is of the format: + * arn:${Partition}:kinesis:${Region}:${Account}:stream/${StreamName} + * No partial ARNs are allowed. The aws-sdk-php will throw a fatal error if + * provided with an incomplete ARN. + * + * putRecord/putRecords: input MUST include the StreamName or the StreamARN and + * MAY include both. + * + * getRecords: input MAY include the StreamARN. + * If the ARN is not included for getRecords, there is no way to infer the ARN. + * + * Message params are determined as follows: + * | Call | Action | DestinationName | DestinationType | Library | + * | --- | --- | --- | --- | --- | + * | Put Record | `Produce` | Stream Name | `Stream` | `Kinesis` | + * | Put Records | `Produce` | Stream Name | `Stream` | `Kinesis` | + * | Get Records | `Consume` | Stream Name | `Stream` | `Kinesis`| + * + * Kinesis Data Streams entity relationship requires the following span + * attributes: + * - `cloud.platform`: the string `aws_kinesis_data_streams` + * - `cloud.resource_id`: the ARN for the Kinesis data stream instance. + */ +void nr_lib_aws_sdk_php_kinesis_handle(nr_segment_t* auto_segment, + char* command_name_string, + size_t command_name_len, + NR_EXECUTE_PROTO) { + char* stream_name_arg_value = NULL; + char* stream_arn_arg_value = NULL; + + nr_segment_t* message_segment = NULL; + + nr_segment_message_params_t message_params = { + .library = KINESIS_LIBRARY_NAME, + .destination_type = NR_MESSAGE_DESTINATION_TYPE_STREAM, + .messaging_system = AWS_KINESIS_PLATFORM, + }; + nr_segment_cloud_attrs_t cloud_attrs = {0}; + + if (NULL == auto_segment) { + return; + } + + if (NULL == command_name_string || 0 == command_name_len) { + return; + } + +#define AWS_COMMAND_IS(CMD) \ + (command_name_len == (sizeof(CMD) - 1) && nr_streq(CMD, command_name_string)) + + /* Determine if we instrument this command. */ + if (AWS_COMMAND_IS("putRecord")) { + message_params.message_action = NR_SPANKIND_PRODUCER; + } else if (AWS_COMMAND_IS("putRecords")) { + message_params.message_action = NR_SPANKIND_PRODUCER; + } else if (AWS_COMMAND_IS("getRecords")) { + message_params.message_action = NR_SPANKIND_CONSUMER; + } else { + /* Nothing to do here so exit. */ + return; + } +#undef AWS_COMMAND_IS + + /* + * Linking between APM and Kinesis Data Streams will be done using the + * following span attributes: + * - `cloud.platform`: the string `aws_kinesis_data_streams` + * - `cloud.resource_id`: the ARN for the Kinesis data stream instance. + */ + + /* + * By this point, it's been determined that this call will be instrumented so + * only create the segment now, grab the parent segment start time, add our + * special segment attributes/metrics then close the newly created segment. + */ + message_segment = nr_segment_start(NRPRG(txn), NULL, NULL); + if (NULL == message_segment) { + return; + } + /* re-use start time from auto_segment started in func_begin */ + message_segment->start_time = auto_segment->start_time; + cloud_attrs.aws_operation = command_name_string; + + /* + * Only try to add attributes from the command parameters if there valid + * return value. This means the sdk validates that the ARN is complete and + * valid. It also validates that the StreamName is valid. If the values are + * formatted incorrectly, the sdk will throw a fatal error. For instance, if + * a partial ARN is passed to the function, the sdk will throw a fatal error. + * If the values are formatted incorrectly, but unable to resolve to a known + * StreamName or ARN, the sdk will again throw an error. This means, if any + * error occurs, it's still okay to start/stop the segment, but any + * parameters passed to the call are suspect and will not be trusted and no + * attempts should be made to extract or infer any information. + */ + + if (NULL != func_return_value) { + /* + * If given both a StreamName and a StreamARN, in the aws-sdk-php, the + * StreamARN will take precedence if both are present and potentially + * conflicting. We will do likewise and prioritize information in the + * StreamARN. + */ + stream_arn_arg_value = nr_lib_aws_sdk_php_get_command_arg_value( + AWS_SDK_PHP_KINESISCLIENT_STREAMARN_ARG, NR_EXECUTE_ORIG_ARGS); + + nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn( + stream_arn_arg_value, &message_params, &cloud_attrs); + + if (NULL == cloud_attrs.cloud_resource_id) { + /* + * We weren't able to get the cloud_resource_id from the StreamARN, let's + * try to infer it from the StreamName. + */ + stream_name_arg_value = nr_lib_aws_sdk_php_get_command_arg_value( + AWS_SDK_PHP_KINESISCLIENT_STREAMNAME_ARG, NR_EXECUTE_ORIG_ARGS); + /* NOTE: The cloud_attrs.cloud_resource_id must be freed by the caller. */ + nr_lib_aws_sdk_php_kinesis_set_params_from_stream_name( + stream_name_arg_value, &message_params, &cloud_attrs, + NR_EXECUTE_ORIG_ARGS); + } + } + + /* Add cloud attributes, if available. */ + nr_segment_traces_add_cloud_attributes(message_segment, &cloud_attrs); + + /* Now end the instrumented segment as a message segment. */ + nr_segment_message_end(&message_segment, &message_params); + + /* + * NOTE: responsible for freeing cloud_attrs->cloud_resource_id ONLY if we + * inferred it using StreamName. If we got it from the StreamARN, we don't + * need to free it as it is pointing to the same memory as + * stream_arn_arg_value. + */ + if (cloud_attrs.cloud_resource_id != stream_arn_arg_value) { + nr_free(cloud_attrs.cloud_resource_id); + } + + nr_free(stream_arn_arg_value); + nr_free(stream_name_arg_value); +} + +void nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn( + char* stream_arn, + nr_segment_message_params_t* message_params, + nr_segment_cloud_attrs_t* cloud_attrs) { + char* destination_name = NULL; + + if (nr_strempty(stream_arn) || NULL == message_params + || NULL == cloud_attrs) { + return; + } + + /* + * AWS Kinesis Stream ARN has a very specific format. + * arn:${Partition}:kinesis:${Region}:${Account}:stream/${StreamName} + * We only need to extract the StreamName. + * + * No partial ARNs or invalid ARNS are allowed. The aws-sdk-php will throw a + * fatal error for anything other than a complete and valid ARN and this + * function isn't called if an exception occurs. We still do error checking, + * even though the sdk shouldn't allow it through, and if we are still unable + * to match what we are looking for, we will not attempt to set any values. + * + */ + + destination_name = stream_arn; + + /* + * Find the pattern ':stream/' of the AWS stream ARN that should immediately + * precede the StreamName. + */ + destination_name = nr_strstr(destination_name, AWS_STREAMARN_STREAM); + if (NULL == destination_name) { + /* Invalid Kinesis stream ARN */ + 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 the start of + * the StreamName or the end of the string. + */ + destination_name += AWS_STREAMARN_STREAM_LEN; + if (nr_strempty(destination_name)) { + /* Malformed stream ARN, we can't decode this. */ + return; + } + + /* + * If it's not an empty string, we've found the start of the StreamName and + * the end of the StreamName goes until the end of the string. + */ + + message_params->destination_name = destination_name; + cloud_attrs->cloud_resource_id = stream_arn; +} + +/* NOTE: caller is responsible for freeing cloud_attrs->cloud_resource_id */ +void nr_lib_aws_sdk_php_kinesis_set_params_from_stream_name( + char* stream_name, + nr_segment_message_params_t* message_params, + nr_segment_cloud_attrs_t* cloud_attrs, + NR_EXECUTE_PROTO) { + + char* region = NULL; + char* account_id = NULL; + char* cloud_resource_id = NULL; + zval* region_zval = NULL; + zval* this_obj = NR_PHP_USER_FN_THIS(); + zend_class_entry* base_class = NULL; + + if (NULL == message_params || NULL == cloud_attrs) { + return; + } + + /* + * If stream_name is empty or NULL, we can still attempt to get + * region/accountid + */ + if (!nr_strempty(stream_name)) { + message_params->destination_name = stream_name; + } + + if (!nr_strempty(NRINI(aws_account_id))) { + account_id = NRINI(aws_account_id); + } + + if (NULL != execute_data->func && NULL != execute_data->func->common.scope) { + base_class = execute_data->func->common.scope; + } + region_zval = nr_php_get_zval_object_property_with_class(this_obj, base_class, + "region"); + if (nr_php_is_zval_non_empty_string(region_zval)) { + region = Z_STRVAL_P(region_zval); + } + if (NULL != region && NULL != account_id + && NULL != message_params->destination_name) { + /* NOTE: caller is responsible for freeing cloud_resource_id */ + cloud_resource_id = nr_formatf("arn:aws:kinesis:%s:%s:stream/%s", region, + account_id, stream_name); + } + + if (NULL == cloud_resource_id) { + /* If we have a valid ARN, these values are redundant so don't set. */ + cloud_attrs->cloud_account_id = account_id; + cloud_attrs->cloud_region = region; + } + /* NOTE: caller is responsible for freeing cloud_attrs->cloud_resource_id */ + cloud_attrs->cloud_resource_id = cloud_resource_id; +} + /* * Note: For SQS, the command_arg_array will contain the following arrays seen below: @@ -749,6 +1009,10 @@ NR_PHP_WRAPPER(nr_aws_client_call) { nr_lib_aws_sdk_php_dynamodb_handle(auto_segment, command_name_string, Z_STRLEN_P(command_name), NR_EXECUTE_ORIG_ARGS); + } else if (AWS_CLASS_IS("Aws\\Kinesis\\KinesisClient", "KinesisClient")) { + nr_lib_aws_sdk_php_kinesis_handle(auto_segment, command_name_string, + Z_STRLEN_P(command_name), + NR_EXECUTE_ORIG_ARGS); } #undef AWS_CLASS_IS diff --git a/agent/lib_aws_sdk_php.h b/agent/lib_aws_sdk_php.h index 05a0dc30c..6ce3b597f 100644 --- a/agent/lib_aws_sdk_php.h +++ b/agent/lib_aws_sdk_php.h @@ -13,10 +13,17 @@ #include "nr_segment_datastore.h" #include "nr_segment_datastore_private.h" - #if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO /* PHP8.1+ */ /* Service instrumentation only supported above PHP 8.1+*/ +/* Kinesis Data Streams */ +#define KINESIS_LIBRARY_NAME "Kinesis" +#define AWS_KINESIS_PLATFORM "aws_kinesis_data_streams" +#define AWS_SDK_PHP_KINESISCLIENT_STREAMNAME_ARG "StreamName" +#define AWS_SDK_PHP_KINESISCLIENT_STREAMARN_ARG "StreamARN" +#define AWS_STREAMARN_STREAM ":stream/" +#define AWS_STREAMARN_STREAM_LEN sizeof(AWS_STREAMARN_STREAM) - 1 + /* SQS */ #define SQS_LIBRARY_NAME "SQS" #define AWS_SQS_MESSAGING_SERVICE "aws_sqs" @@ -63,6 +70,50 @@ extern void nr_lib_aws_sdk_php_add_supportability_service_metric( /* Aside from service class and version detection, instrumentation is only * supported with PHP 8.1+ */ +/* + * Purpose : Uses the StreamName to infer cloud_resource_id, if possible; + * otherwise, sets region and account_id, if found. Sets + * message_params.destination_name. + * + * Params : + * 1. stream_name + * 2. message_params to set destination_name + * 3. cloud_attrs to set either cloud_resource_id OR cloud_region and + * cloud_account_id + * 4. NR_EXECUTE_ORIG_ARGS (execute_data, func_return_value) + * + * Returns : + * + * Note: the following values are set for the caller: + * - cloud_attrs->cloud_resource_id MUST be freed by caller + * - cloud_attrs->cloud_region ONLY if cloud_resource_id is NULL + * - cloud_attrs->cloud_account_id ONLY if cloud_resource_id is NULL + * - message_params->destination_name + */ +extern void nr_lib_aws_sdk_php_kinesis_set_params_from_stream_name( + char* stream_name, + nr_segment_message_params_t* message_params, + nr_segment_cloud_attrs_t* cloud_attrs, + NR_EXECUTE_PROTO); + +/* + * Purpose : Uses the StreamARN to extract the StreamName. + * + * Params : 1. stream_arn + * 2. message_params to set destination_name + * 3. cloud_attrs to set cloud_resource_id + * + * Returns : + * + * Note: the following values are set for the caller: + * - cloud_attrs->cloud_resource_id + * - message_params->destination_name + */ +extern void nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn( + char* stream_arn, + nr_segment_message_params_t* message_params, + nr_segment_cloud_attrs_t* cloud_attrs); + /* * 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 @@ -70,8 +121,8 @@ extern void nr_lib_aws_sdk_php_add_supportability_service_metric( * * 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 + * 3. cloud_attrs to set + * cloud_attrs.cloud_region,cloud_attrs.cloud_account_id * * Returns : applicable cloud_attrs and message params fields will point to null * terminated strings within the original string. diff --git a/agent/tests/test_lib_aws_sdk_php.c b/agent/tests/test_lib_aws_sdk_php.c index ee0989168..2b96e7525 100644 --- a/agent/tests/test_lib_aws_sdk_php.c +++ b/agent/tests/test_lib_aws_sdk_php.c @@ -61,6 +61,70 @@ NR_PHP_WRAPPER(expect_arg_value_null) { } NR_PHP_WRAPPER_END +NR_PHP_WRAPPER(aws_kinesis_set_params_from_stream_name_wrapper) { + (void)wraprec; + nr_segment_cloud_attrs_t cloud_attrs = {0}; + nr_segment_message_params_t message_params = {0}; + char* expected_string = NULL; + char* test_name_string = NULL; + char* stream_name_string = NULL; + zval* test_name = NULL; + zval* expected = NULL; + zval* stream_name = NULL; + + /* + * argument 1 is is used to pass in the test name + */ + test_name = nr_php_get_user_func_arg(1, NR_EXECUTE_ORIG_ARGS); + if (nr_php_is_zval_valid_string(test_name)) { + test_name_string = Z_STRVAL_P(test_name); + } else { + test_name_string = "Params should match expected."; + } + + /* + * argument 2 is is used to pass in the stream name + */ + + stream_name = nr_php_get_user_func_arg(2, NR_EXECUTE_ORIG_ARGS); + if (nr_php_is_zval_valid_string(stream_name)) { + stream_name_string = Z_STRVAL_P(stream_name); + } + + /* + * argument 3 is is used to pass in the expected value + */ + + expected = nr_php_get_user_func_arg(3, NR_EXECUTE_ORIG_ARGS); + if (nr_php_is_zval_valid_string(expected)) { + expected_string = Z_STRVAL_P(expected); + } + + /* + * nr_lib_aws_sdk_php_kinesis_set_params_from_stream_name sets: + * - cloud_attrs->cloud_resource_id MUST be freed by caller + * - cloud_attrs->cloud_region ONLY if cloud_resource_id is NULL + * - cloud_attrs->cloud_account_id ONLY if cloud_resource_id is NULL + * - message_params->destination_name + */ + NR_PHP_WRAPPER_CALL; + nr_lib_aws_sdk_php_kinesis_set_params_from_stream_name( + stream_name_string, &message_params, &cloud_attrs, NR_EXECUTE_ORIG_ARGS); + + char* result_string = nr_formatf( + "cloud_resource_id:%s region:%s account_id:%s destination_name:%s", + NRSAFESTR(cloud_attrs.cloud_resource_id), + NRSAFESTR(cloud_attrs.cloud_region), + NRSAFESTR(cloud_attrs.cloud_account_id), + NRSAFESTR(message_params.destination_name)); + + tlib_pass_if_str_equal(test_name_string, expected_string, result_string); + + nr_free(result_string); + nr_free(cloud_attrs.cloud_resource_id); +} +NR_PHP_WRAPPER_END + NR_PHP_WRAPPER(aws_dynamodb_set_params_wrapper) { (void)wraprec; nr_segment_cloud_attrs_t cloud_attrs = {0}; @@ -72,7 +136,6 @@ NR_PHP_WRAPPER(aws_dynamodb_set_params_wrapper) { zval* expected = NULL; datastore_params.instance = &instance; - /* * argument 1 is is used to pass in the test name */ @@ -318,7 +381,7 @@ static void test_nr_lib_aws_sdk_php_sqs_parse_queueurl() { tlib_php_engine_create(""); -// clang-format off + // 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/" @@ -566,7 +629,7 @@ static void setup_inherited_classes() { "class base_class {" "private ?string $region;" "private ?endpoint_class $endpoint;" - "function base_func($command, $args, $expects) {return;}" + "function base_func($command1, $args, $expects) {return;}" "function __construct(?string $region = null, ?int $port = null, ?string $host = null) {" "$this->region = $region;" "$this->endpoint = new endpoint_class($port, $host);" @@ -576,6 +639,318 @@ static void setup_inherited_classes() { // clang-format on tlib_php_request_eval(classes); } + +static void test_nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn() { + /* + * nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn sets ONLY + * cloud_resource_id and destination_name, if possible. + */ + nr_segment_message_params_t message_params = {0}; + nr_segment_cloud_attrs_t cloud_attrs = {0}; + + tlib_php_engine_create(""); + + // clang-format off +#define VALID_STREAM_ARN "arn:aws:kinesis:us-east-2:123456789012:stream/MY_STREAM_NAME" +#define VALID_STREAM_ARN_DESTINATION_NAME "MY_STREAM_NAME" +#define INVALID_STREAM_ARN_1 "arn:aws:kinesis:us-east-2:123456789012:stream/" +#define INVALID_STREAM_ARN_2 "arn:aws:kinesis:us-east-2:123456789012:stream:MY_STREAM_NAME" +#define INVALID_STREAM_ARN_3 "arn:aws:kinesis:us-east-2:123456789012:MY_STREAM_NAME" +#define INVALID_STREAM_ARN_4 "arn:aws:kinesis:us-east-2:123456789012stream:MY_STREAM_NAME" + + // clang-format on + + /* Test null stream_arn. Output values should be null.*/ + nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn(NULL, &message_params, + &cloud_attrs); + tlib_pass_if_null("null stream_arn, cloud_resource_id should be null.", + cloud_attrs.cloud_resource_id); + tlib_pass_if_null("null stream_arn, destination_name should be null.", + message_params.destination_name); + + /* Test empty stream_arn. Output values should be null.*/ + nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn(NULL, &message_params, + &cloud_attrs); + tlib_pass_if_null("null stream_arn, cloud_resource_id should be null.", + cloud_attrs.cloud_resource_id); + tlib_pass_if_null("null stream_arn, destination_name should be null.", + message_params.destination_name); + + /* Test null message_params. No values extracted, shouldn't modify + * cloud_attrs. */ + nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn(NULL, NULL, + &cloud_attrs); + tlib_pass_if_null("null stream_arn, cloud_resource_id should be null.", + cloud_attrs.cloud_resource_id); + + /* Test null cloud_attrs. No values extracted, shouldn't modify + * message_params. */ + nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn(NULL, &message_params, + NULL); + tlib_pass_if_null("null stream_arn, destination_name should be null.", + message_params.destination_name); + + /* Test invalid stream_arns. Output values should be null.*/ + nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn( + INVALID_STREAM_ARN_1, &message_params, &cloud_attrs); + tlib_pass_if_null("null stream_arn, cloud_resource_id should be null.", + cloud_attrs.cloud_resource_id); + tlib_pass_if_null("null stream_arn, destination_name should be null.", + message_params.destination_name); + + nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn( + INVALID_STREAM_ARN_2, &message_params, &cloud_attrs); + tlib_pass_if_null("null stream_arn, cloud_resource_id should be null.", + cloud_attrs.cloud_resource_id); + tlib_pass_if_null("null stream_arn, destination_name should be null.", + message_params.destination_name); + + nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn( + INVALID_STREAM_ARN_3, &message_params, &cloud_attrs); + tlib_pass_if_null("null stream_arn, cloud_resource_id should be null.", + cloud_attrs.cloud_resource_id); + tlib_pass_if_null("null stream_arn, destination_name should be null.", + message_params.destination_name); + + nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn( + INVALID_STREAM_ARN_4, &message_params, &cloud_attrs); + tlib_pass_if_null("null stream_arn, cloud_resource_id should be null.", + cloud_attrs.cloud_resource_id); + tlib_pass_if_null("null stream_arn, destination_name should be null.", + message_params.destination_name); + + /* Test valid stream_arns. Output values should be match.*/ + nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn( + VALID_STREAM_ARN, &message_params, &cloud_attrs); + tlib_pass_if_str_equal("valid stream_arn, cloud_resource_id should be match.", + cloud_attrs.cloud_resource_id, VALID_STREAM_ARN); + tlib_pass_if_str_equal("valid stream_arn, destination_name should match.", + message_params.destination_name, + VALID_STREAM_ARN_DESTINATION_NAME); + + tlib_php_engine_destroy(); +} + +static void test_nr_lib_aws_sdk_php_kinesis_set_params_from_stream_name() { + zval* obj = NULL; + zval* expect_arg = NULL; + zval* stream_name_arg = NULL; + zval* test_name_arg = NULL; + zval* expr = NULL; + char* stream_name = NULL; + char* expect = NULL; + char* test_name = NULL; + + tlib_php_engine_create(""); + tlib_php_request_start(); + + setup_inherited_classes(); + + /* Get class with all values set to NULL*/ + obj = tlib_php_request_eval_expr("new top_class"); + tlib_pass_if_not_null("object shouldn't be NULL", obj); + + nr_php_wrap_user_function(NR_PSTR("base_class::base_func"), + aws_kinesis_set_params_from_stream_name_wrapper); + + test_name + = "'test1:stream_name is null, region is null, account id is null so " + "expect no values set in structs'"; + + stream_name = NULL; + + expect + = "'cloud_resource_id: region: account_id: " + "destination_name:'"; + + test_name_arg = tlib_php_request_eval_expr(test_name); + stream_name_arg = tlib_php_request_eval_expr(stream_name); + expect_arg = tlib_php_request_eval_expr(expect); + + expr = nr_php_call(obj, "base_func", test_name_arg, stream_name_arg, + expect_arg); + nr_php_zval_free(&expr); + nr_php_zval_free(&expect_arg); + nr_php_zval_free(&stream_name_arg); + nr_php_zval_free(&test_name_arg); + + test_name + = "'test2:stream_name is empty string, region is null, account id is " + "null so expect no values set in structs'"; + + stream_name = "''"; + + expect + = "'cloud_resource_id: region: account_id: " + "destination_name:'"; + + test_name_arg = tlib_php_request_eval_expr(test_name); + stream_name_arg = tlib_php_request_eval_expr(stream_name); + expect_arg = tlib_php_request_eval_expr(expect); + + expr = nr_php_call(obj, "base_func", test_name_arg, stream_name_arg, + expect_arg); + nr_php_zval_free(&expr); + nr_php_zval_free(&expect_arg); + nr_php_zval_free(&stream_name_arg); + nr_php_zval_free(&test_name_arg); + + test_name + = "'test3:stream_name is not null, region is null, account id is null so " + "expect destination_name to be set'"; + + stream_name = "'my_stream'"; + + expect + = "'cloud_resource_id: region: account_id: " + "destination_name:my_stream'"; + + test_name_arg = tlib_php_request_eval_expr(test_name); + stream_name_arg = tlib_php_request_eval_expr(stream_name); + expect_arg = tlib_php_request_eval_expr(expect); + + expr = nr_php_call(obj, "base_func", test_name_arg, stream_name_arg, + expect_arg); + nr_php_zval_free(&expr); + nr_php_zval_free(&expect_arg); + nr_php_zval_free(&stream_name_arg); + nr_php_zval_free(&test_name_arg); + + NRINI(aws_account_id) = "111122223333"; + test_name + = "'test4:stream_name is not null, region is null, account id not null " + "so expect only destination_name and account_id to be set'"; + + stream_name = "'my_stream'"; + + expect + = "'cloud_resource_id: region: account_id:111122223333 " + "destination_name:my_stream'"; + + test_name_arg = tlib_php_request_eval_expr(test_name); + stream_name_arg = tlib_php_request_eval_expr(stream_name); + expect_arg = tlib_php_request_eval_expr(expect); + + expr = nr_php_call(obj, "base_func", test_name_arg, stream_name_arg, + expect_arg); + nr_php_zval_free(&expr); + nr_php_zval_free(&expect_arg); + nr_php_zval_free(&stream_name_arg); + nr_php_zval_free(&test_name_arg); + + /* Done with obj where region is not set*/ + nr_php_zval_free(&obj); + + /* Get class with empty string region*/ + obj = tlib_php_request_eval_expr("new top_class(region:'')"); + tlib_pass_if_not_null("object shouldn't be NULL", obj); + + nr_php_wrap_user_function(NR_PSTR("base_class::base_func"), + aws_kinesis_set_params_from_stream_name_wrapper); + + NRINI(aws_account_id) = ""; + test_name + = "'test5:stream_name null, region is empty string, account id is empty" + "string so expect no values to be set'"; + + stream_name = NULL; + + expect + = "'cloud_resource_id: region: account_id: " + "destination_name:'"; + + test_name_arg = tlib_php_request_eval_expr(test_name); + stream_name_arg = tlib_php_request_eval_expr(stream_name); + expect_arg = tlib_php_request_eval_expr(expect); + + expr = nr_php_call(obj, "base_func", test_name_arg, stream_name_arg, + expect_arg); + nr_php_zval_free(&expr); + nr_php_zval_free(&expect_arg); + nr_php_zval_free(&stream_name_arg); + nr_php_zval_free(&test_name_arg); + + /* Done with obj where region is not set*/ + nr_php_zval_free(&obj); + + /* Get class with non-empty string region*/ + obj = tlib_php_request_eval_expr("new top_class(region:'my_region')"); + tlib_pass_if_not_null("object shouldn't be NULL", obj); + + nr_php_wrap_user_function(NR_PSTR("base_class::base_func"), + aws_kinesis_set_params_from_stream_name_wrapper); + + NRINI(aws_account_id) = NULL; + test_name + = "'test6:stream_name null, region is not null, account id null so " + "expect only region to be set'"; + + stream_name = NULL; + + expect + = "'cloud_resource_id: region:my_region account_id: " + "destination_name:'"; + + test_name_arg = tlib_php_request_eval_expr(test_name); + stream_name_arg = tlib_php_request_eval_expr(stream_name); + expect_arg = tlib_php_request_eval_expr(expect); + + expr = nr_php_call(obj, "base_func", test_name_arg, stream_name_arg, + expect_arg); + nr_php_zval_free(&expr); + nr_php_zval_free(&expect_arg); + nr_php_zval_free(&stream_name_arg); + nr_php_zval_free(&test_name_arg); + + test_name + = "'test7:stream_name not null, region is not null, account id null so " + "expect only region to be set'"; + + stream_name = "'my_stream'"; + + expect + = "'cloud_resource_id: region:my_region account_id: " + "destination_name:my_stream'"; + + test_name_arg = tlib_php_request_eval_expr(test_name); + stream_name_arg = tlib_php_request_eval_expr(stream_name); + expect_arg = tlib_php_request_eval_expr(expect); + + expr = nr_php_call(obj, "base_func", test_name_arg, stream_name_arg, + expect_arg); + nr_php_zval_free(&expr); + nr_php_zval_free(&expect_arg); + nr_php_zval_free(&stream_name_arg); + nr_php_zval_free(&test_name_arg); + + test_name + = "'test8:stream_name is not null, region is not null, account id not " + "null so expect only destination_name and resource_id to be set'"; + + stream_name = "'my_stream'"; + + expect + = "'cloud_resource_id:arn:aws:kinesis:my_region:111122223333:stream/" + "my_stream region: account_id: destination_name:my_stream'"; + NRINI(aws_account_id) = "111122223333"; + test_name_arg = tlib_php_request_eval_expr(test_name); + stream_name_arg = tlib_php_request_eval_expr(stream_name); + expect_arg = tlib_php_request_eval_expr(expect); + + expr = nr_php_call(obj, "base_func", test_name_arg, stream_name_arg, + expect_arg); + nr_php_zval_free(&expr); + nr_php_zval_free(&expect_arg); + nr_php_zval_free(&stream_name_arg); + nr_php_zval_free(&test_name_arg); + + /* Done with obj*/ + nr_php_zval_free(&obj); + + tlib_php_request_end(); + tlib_php_engine_destroy(); +} + static void test_nr_lib_aws_sdk_php_dynamodb_set_params() { zval* obj = NULL; zval* expect_arg = NULL; @@ -1115,5 +1490,7 @@ void test_main(void* p NRUNUSED) { test_nr_lib_aws_sdk_php_get_command_arg_value(); test_nr_lib_aws_sdk_php_lambda_invoke(); test_nr_lib_aws_sdk_php_dynamodb_set_params(); + test_nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn(); + test_nr_lib_aws_sdk_php_kinesis_set_params_from_stream_name(); #endif /* PHP 8.1+ */ } From 39071ea72f8f50df95ee98d4f7a42e3712fa547e Mon Sep 17 00:00:00 2001 From: Amber Sistla Date: Mon, 1 Dec 2025 11:34:13 -0700 Subject: [PATCH 3/6] fix: cloud.platform --- agent/lib_aws_sdk_php.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/agent/lib_aws_sdk_php.c b/agent/lib_aws_sdk_php.c index 6fbf756f2..b0bc66bdb 100644 --- a/agent/lib_aws_sdk_php.c +++ b/agent/lib_aws_sdk_php.c @@ -70,9 +70,10 @@ void nr_lib_aws_sdk_php_kinesis_handle(nr_segment_t* auto_segment, nr_segment_message_params_t message_params = { .library = KINESIS_LIBRARY_NAME, .destination_type = NR_MESSAGE_DESTINATION_TYPE_STREAM, - .messaging_system = AWS_KINESIS_PLATFORM, }; - nr_segment_cloud_attrs_t cloud_attrs = {0}; + nr_segment_cloud_attrs_t cloud_attrs = { + .cloud_platform = AWS_KINESIS_PLATFORM, + }; if (NULL == auto_segment) { return; @@ -240,7 +241,6 @@ void nr_lib_aws_sdk_php_kinesis_set_params_from_stream_name( nr_segment_message_params_t* message_params, nr_segment_cloud_attrs_t* cloud_attrs, NR_EXECUTE_PROTO) { - char* region = NULL; char* account_id = NULL; char* cloud_resource_id = NULL; @@ -274,7 +274,7 @@ void nr_lib_aws_sdk_php_kinesis_set_params_from_stream_name( } if (NULL != region && NULL != account_id && NULL != message_params->destination_name) { - /* NOTE: caller is responsible for freeing cloud_resource_id */ + /* NOTE: caller is responsible for freeing cloud_resource_id */ cloud_resource_id = nr_formatf("arn:aws:kinesis:%s:%s:stream/%s", region, account_id, stream_name); } From 47921b727a7e29a5275cdf1d9a75df99d4c7aaee Mon Sep 17 00:00:00 2001 From: Amber Sistla Date: Wed, 3 Dec 2025 12:06:30 -0700 Subject: [PATCH 4/6] fix: Add additional tests and fix a comment --- agent/tests/test_lib_aws_sdk_php.c | 32 +++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/agent/tests/test_lib_aws_sdk_php.c b/agent/tests/test_lib_aws_sdk_php.c index 2b96e7525..6479014e8 100644 --- a/agent/tests/test_lib_aws_sdk_php.c +++ b/agent/tests/test_lib_aws_sdk_php.c @@ -669,26 +669,44 @@ static void test_nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn() { message_params.destination_name); /* Test empty stream_arn. Output values should be null.*/ - nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn(NULL, &message_params, + nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn("", &message_params, &cloud_attrs); - tlib_pass_if_null("null stream_arn, cloud_resource_id should be null.", + tlib_pass_if_null("empty stream_arn, cloud_resource_id should be null.", cloud_attrs.cloud_resource_id); tlib_pass_if_null("null stream_arn, destination_name should be null.", message_params.destination_name); - /* Test null message_params. No values extracted, shouldn't modify + /* Test null message_params, null arn. No values extracted, shouldn't modify * cloud_attrs. */ nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn(NULL, NULL, &cloud_attrs); - tlib_pass_if_null("null stream_arn, cloud_resource_id should be null.", - cloud_attrs.cloud_resource_id); + tlib_pass_if_null( + "null stream_arn and null arn, cloud_resource_id should be null.", + cloud_attrs.cloud_resource_id); /* Test null cloud_attrs. No values extracted, shouldn't modify * message_params. */ nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn(NULL, &message_params, NULL); - tlib_pass_if_null("null stream_arn, destination_name should be null.", - message_params.destination_name); + tlib_pass_if_null( + "null cloud_attrsnull stream_arn, destination_name should be null.", + message_params.destination_name); + + /* Test null message_params but valid stream arn. No values extracted, + * shouldn't modify cloud_attrs. */ + nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn(VALID_STREAM_ARN, NULL, + &cloud_attrs); + tlib_pass_if_null( + "null stream_arn but valid stream arn, cloud_resource_id should be null.", + cloud_attrs.cloud_resource_id); + + /* Test null cloud_attrs but valid stream arn. No values extracted, shouldn't + * modify message_params. */ + nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn(VALID_STREAM_ARN, + &message_params, NULL); + tlib_pass_if_null( + "null stream_arn but valid stream arn, destination_name should be null.", + message_params.destination_name); /* Test invalid stream_arns. Output values should be null.*/ nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn( From 260dfb8b3f8fe8c07c3bdeb25a299b84fff19629 Mon Sep 17 00:00:00 2001 From: Amber Sistla Date: Wed, 3 Dec 2025 12:14:19 -0700 Subject: [PATCH 5/6] fix: update test descriptions --- agent/tests/test_lib_aws_sdk_php.c | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/agent/tests/test_lib_aws_sdk_php.c b/agent/tests/test_lib_aws_sdk_php.c index 6479014e8..d470e7be4 100644 --- a/agent/tests/test_lib_aws_sdk_php.c +++ b/agent/tests/test_lib_aws_sdk_php.c @@ -673,7 +673,7 @@ static void test_nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn() { &cloud_attrs); tlib_pass_if_null("empty stream_arn, cloud_resource_id should be null.", cloud_attrs.cloud_resource_id); - tlib_pass_if_null("null stream_arn, destination_name should be null.", + tlib_pass_if_null("empty stream_arn, destination_name should be null.", message_params.destination_name); /* Test null message_params, null arn. No values extracted, shouldn't modify @@ -681,7 +681,7 @@ static void test_nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn() { nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn(NULL, NULL, &cloud_attrs); tlib_pass_if_null( - "null stream_arn and null arn, cloud_resource_id should be null.", + "null message_params and null arn, cloud_resource_id should be null.", cloud_attrs.cloud_resource_id); /* Test null cloud_attrs. No values extracted, shouldn't modify @@ -689,7 +689,7 @@ static void test_nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn() { nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn(NULL, &message_params, NULL); tlib_pass_if_null( - "null cloud_attrsnull stream_arn, destination_name should be null.", + "null cloud_attrs and null stream_arn, destination_name should be null.", message_params.destination_name); /* Test null message_params but valid stream arn. No values extracted, @@ -697,7 +697,8 @@ static void test_nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn() { nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn(VALID_STREAM_ARN, NULL, &cloud_attrs); tlib_pass_if_null( - "null stream_arn but valid stream arn, cloud_resource_id should be null.", + "null message_params but valid stream arn, cloud_resource_id should be " + "null.", cloud_attrs.cloud_resource_id); /* Test null cloud_attrs but valid stream arn. No values extracted, shouldn't @@ -705,36 +706,36 @@ static void test_nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn() { nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn(VALID_STREAM_ARN, &message_params, NULL); tlib_pass_if_null( - "null stream_arn but valid stream arn, destination_name should be null.", + "null cloud_attrs but valid stream arn, destination_name should be null.", message_params.destination_name); /* Test invalid stream_arns. Output values should be null.*/ nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn( INVALID_STREAM_ARN_1, &message_params, &cloud_attrs); - tlib_pass_if_null("null stream_arn, cloud_resource_id should be null.", + tlib_pass_if_null("invalid stream_arn, cloud_resource_id should be null.", cloud_attrs.cloud_resource_id); - tlib_pass_if_null("null stream_arn, destination_name should be null.", + tlib_pass_if_null("invalid stream_arn, destination_name should be null.", message_params.destination_name); nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn( INVALID_STREAM_ARN_2, &message_params, &cloud_attrs); - tlib_pass_if_null("null stream_arn, cloud_resource_id should be null.", + tlib_pass_if_null("invalid stream_arn, cloud_resource_id should be null.", cloud_attrs.cloud_resource_id); - tlib_pass_if_null("null stream_arn, destination_name should be null.", + tlib_pass_if_null("invalid stream_arn, destination_name should be null.", message_params.destination_name); nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn( INVALID_STREAM_ARN_3, &message_params, &cloud_attrs); - tlib_pass_if_null("null stream_arn, cloud_resource_id should be null.", + tlib_pass_if_null("invalid stream_arn, cloud_resource_id should be null.", cloud_attrs.cloud_resource_id); - tlib_pass_if_null("null stream_arn, destination_name should be null.", + tlib_pass_if_null("invalid stream_arn, destination_name should be null.", message_params.destination_name); nr_lib_aws_sdk_php_kinesis_set_params_from_stream_arn( INVALID_STREAM_ARN_4, &message_params, &cloud_attrs); - tlib_pass_if_null("null stream_arn, cloud_resource_id should be null.", + tlib_pass_if_null("invalid stream_arn, cloud_resource_id should be null.", cloud_attrs.cloud_resource_id); - tlib_pass_if_null("null stream_arn, destination_name should be null.", + tlib_pass_if_null("invalid stream_arn, destination_name should be null.", message_params.destination_name); /* Test valid stream_arns. Output values should be match.*/ From 837fdc6f68a1847d381a38be6433b455d2e07c09 Mon Sep 17 00:00:00 2001 From: Amber Sistla Date: Wed, 3 Dec 2025 12:16:42 -0700 Subject: [PATCH 6/6] fix: revert change --- agent/tests/test_lib_aws_sdk_php.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/tests/test_lib_aws_sdk_php.c b/agent/tests/test_lib_aws_sdk_php.c index d470e7be4..775b0c99b 100644 --- a/agent/tests/test_lib_aws_sdk_php.c +++ b/agent/tests/test_lib_aws_sdk_php.c @@ -629,7 +629,7 @@ static void setup_inherited_classes() { "class base_class {" "private ?string $region;" "private ?endpoint_class $endpoint;" - "function base_func($command1, $args, $expects) {return;}" + "function base_func($command, $args, $expects) {return;}" "function __construct(?string $region = null, ?int $port = null, ?string $host = null) {" "$this->region = $region;" "$this->endpoint = new endpoint_class($port, $host);"