Skip to content

Commit dd22e39

Browse files
committed
feat(agent): Add AWS DynamoDb Instrumentation
* Added instrumentation to handle the calls specified in the agent spec * Added unit tests
1 parent 9e20b93 commit dd22e39

File tree

3 files changed

+740
-61
lines changed

3 files changed

+740
-61
lines changed

agent/lib_aws_sdk_php.c

Lines changed: 201 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,16 @@
1414
#include "fw_hooks.h"
1515
#include "fw_support.h"
1616
#include "util_logging.h"
17-
#include "nr_segment_message.h"
18-
#include "nr_segment_external.h"
1917
#include "lib_aws_sdk_php.h"
2018

2119
#define PHP_PACKAGE_NAME "aws/aws-sdk-php"
22-
#define AWS_LAMBDA_ARN_REGEX "(arn:(aws[a-zA-Z-]*)?:lambda:)?" \
23-
"((?<region>[a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{1}):)?" \
24-
"((?<accountId>\\d{12}):)?" \
25-
"(function:)?" \
26-
"(?<functionName>[a-zA-Z0-9-\\.]+)" \
27-
"(:(?<qualifier>\\$LATEST|[a-zA-Z0-9-]+))?"
20+
#define AWS_LAMBDA_ARN_REGEX \
21+
"(arn:(aws[a-zA-Z-]*)?:lambda:)?" \
22+
"((?<region>[a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{1}):)?" \
23+
"((?<accountId>\\d{12}):)?" \
24+
"(function:)?" \
25+
"(?<functionName>[a-zA-Z0-9-\\.]+)" \
26+
"(:(?<qualifier>\\$LATEST|[a-zA-Z0-9-]+))?"
2827

2928
#if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO /* PHP8.1+ */
3029
/* Service instrumentation only supported above PHP 8.1+*/
@@ -309,9 +308,7 @@ void nr_lib_aws_sdk_php_lambda_handle(nr_segment_t* auto_segment,
309308
nr_segment_t* external_segment = NULL;
310309
zval** retval_ptr = NR_GET_RETURN_VALUE_PTR;
311310

312-
nr_segment_cloud_attrs_t cloud_attrs = {
313-
.cloud_platform = "aws_lambda"
314-
};
311+
nr_segment_cloud_attrs_t cloud_attrs = {.cloud_platform = "aws_lambda"};
315312

316313
if (NULL == auto_segment) {
317314
return;
@@ -332,7 +329,8 @@ void nr_lib_aws_sdk_php_lambda_handle(nr_segment_t* auto_segment,
332329
/* Determine if we instrument this command. */
333330
if (AWS_COMMAND_IS("invoke")) {
334331
/* reconstruct the ARN */
335-
nr_aws_sdk_lambda_client_invoke_parse_args(NR_EXECUTE_ORIG_ARGS, &cloud_attrs);
332+
nr_aws_sdk_lambda_client_invoke_parse_args(NR_EXECUTE_ORIG_ARGS,
333+
&cloud_attrs);
336334
} else {
337335
return;
338336
}
@@ -362,7 +360,7 @@ void nr_lib_aws_sdk_php_lambda_handle(nr_segment_t* auto_segment,
362360
external_params.status = Z_LVAL_P(status_code);
363361
}
364362
zval* metadata = nr_php_zend_hash_find(Z_ARRVAL_P(data), "@metadata");
365-
if (NULL != metadata && IS_REFERENCE == Z_TYPE_P(metadata)) {
363+
if (NULL != metadata && IS_REFERENCE == Z_TYPE_P(metadata)) {
366364
metadata = Z_REFVAL_P(metadata);
367365
}
368366
if (nr_php_is_zval_valid_array(metadata)) {
@@ -371,14 +369,13 @@ void nr_lib_aws_sdk_php_lambda_handle(nr_segment_t* auto_segment,
371369
external_params.uri = Z_STRVAL_P(uri);
372370
}
373371
}
374-
375372
}
376373
nr_segment_external_end(&external_segment, &external_params);
377374
nr_free(cloud_attrs.cloud_resource_id);
378375
}
379376

380-
/* This stores the compiled regex to parse AWS ARNs. The compilation happens when
381-
* it is first needed and is destroyed in mshutdown
377+
/* This stores the compiled regex to parse AWS ARNs. The compilation happens
378+
* when it is first needed and is destroyed in mshutdown
382379
*/
383380
static nr_regex_t* aws_arn_regex;
384381

@@ -390,7 +387,9 @@ void nr_aws_sdk_mshutdown(void) {
390387
nr_regex_destroy(&aws_arn_regex);
391388
}
392389

393-
void nr_aws_sdk_lambda_client_invoke_parse_args(NR_EXECUTE_PROTO, nr_segment_cloud_attrs_t* cloud_attrs) {
390+
void nr_aws_sdk_lambda_client_invoke_parse_args(
391+
NR_EXECUTE_PROTO,
392+
nr_segment_cloud_attrs_t* cloud_attrs) {
394393
zval* call_args = nr_php_get_user_func_arg(2, NR_EXECUTE_ORIG_ARGS);
395394
zval* this_obj = NR_PHP_USER_FN_THIS();
396395
char* arn = NULL;
@@ -409,7 +408,8 @@ void nr_aws_sdk_lambda_client_invoke_parse_args(NR_EXECUTE_PROTO, nr_segment_clo
409408
if (!nr_php_is_zval_valid_array(lambda_args)) {
410409
return;
411410
}
412-
zval* lambda_name = nr_php_zend_hash_find(Z_ARRVAL_P(lambda_args), "FunctionName");
411+
zval* lambda_name
412+
= nr_php_zend_hash_find(Z_ARRVAL_P(lambda_args), "FunctionName");
413413
if (!nr_php_is_zval_non_empty_string(lambda_name)) {
414414
return;
415415
}
@@ -420,10 +420,8 @@ void nr_aws_sdk_lambda_client_invoke_parse_args(NR_EXECUTE_PROTO, nr_segment_clo
420420
}
421421

422422
/* Extract all information possible from the passed lambda name via regex */
423-
nr_regex_substrings_t* matches =
424-
nr_regex_match_capture(aws_arn_regex,
425-
Z_STRVAL_P(lambda_name),
426-
Z_STRLEN_P(lambda_name));
423+
nr_regex_substrings_t* matches = nr_regex_match_capture(
424+
aws_arn_regex, Z_STRVAL_P(lambda_name), Z_STRLEN_P(lambda_name));
427425
function_name = nr_regex_substrings_get_named(matches, "functionName");
428426
accountID = nr_regex_substrings_get_named(matches, "accountId");
429427
region = nr_regex_substrings_get_named(matches, "region");
@@ -449,11 +447,12 @@ void nr_aws_sdk_lambda_client_invoke_parse_args(NR_EXECUTE_PROTO, nr_segment_clo
449447
}
450448
if (nr_strempty(region)) {
451449
zend_class_entry* base_class = NULL;
452-
if (NULL != execute_data->func && NULL!= execute_data->func->common.scope) {
453-
base_class = execute_data->func->common.scope;
450+
if (NULL != execute_data->func
451+
&& NULL != execute_data->func->common.scope) {
452+
base_class = execute_data->func->common.scope;
454453
}
455-
region_zval
456-
= nr_php_get_zval_object_property_with_class(this_obj, base_class, "region");
454+
region_zval = nr_php_get_zval_object_property_with_class(
455+
this_obj, base_class, "region");
457456
if (nr_php_is_zval_valid_string(region_zval)) {
458457
/*
459458
* In this case, region is likely to be NULL, but could be an empty
@@ -467,11 +466,11 @@ void nr_aws_sdk_lambda_client_invoke_parse_args(NR_EXECUTE_PROTO, nr_segment_clo
467466
if (!nr_strempty(accountID) && !nr_strempty(region)) {
468467
/* construct the ARN */
469468
if (!nr_strempty(qualifier)) {
470-
arn = nr_formatf("arn:aws:lambda:%s:%s:function:%s:%s",
471-
region, accountID, function_name, qualifier);
469+
arn = nr_formatf("arn:aws:lambda:%s:%s:function:%s:%s", region, accountID,
470+
function_name, qualifier);
472471
} else {
473-
arn = nr_formatf("arn:aws:lambda:%s:%s:function:%s",
474-
region, accountID, function_name);
472+
arn = nr_formatf("arn:aws:lambda:%s:%s:function:%s", region, accountID,
473+
function_name);
475474
}
476475

477476
/* Attach the ARN */
@@ -519,6 +518,172 @@ char* nr_lib_aws_sdk_php_get_command_arg_value(char* command_arg_name,
519518
return command_arg_value;
520519
}
521520

521+
void nr_lib_aws_sdk_php_dynamodb_set_params(
522+
nr_segment_datastore_params_t* datastore_params,
523+
nr_segment_cloud_attrs_t* cloud_attrs,
524+
NR_EXECUTE_PROTO) {
525+
zval* endpoint_zval = NULL;
526+
zval* region_zval = NULL;
527+
zval* host_zval = NULL;
528+
zval* port_zval = NULL;
529+
zval* this_obj = NULL;
530+
zend_function* func = NULL;
531+
zend_class_entry* base_class = NULL;
532+
char* table_name = NULL;
533+
char* account_id = NULL;
534+
535+
if (NULL == datastore_params || NULL == cloud_attrs) {
536+
return;
537+
}
538+
539+
this_obj = NR_PHP_USER_FN_THIS();
540+
func = nr_php_execute_function(NR_EXECUTE_ORIG_ARGS);
541+
542+
if (NULL == this_obj || NULL == func) {
543+
return;
544+
}
545+
546+
if (NULL != func->common.scope) {
547+
base_class = func->common.scope;
548+
}
549+
550+
region_zval = nr_php_get_zval_object_property_with_class(this_obj, base_class,
551+
"region");
552+
if (nr_php_is_zval_non_empty_string(region_zval)) {
553+
cloud_attrs->cloud_region = Z_STRVAL_P(region_zval);
554+
}
555+
556+
endpoint_zval = nr_php_get_zval_object_property_with_class(
557+
this_obj, base_class, "endpoint");
558+
if (nr_php_is_zval_valid_object(endpoint_zval)) {
559+
host_zval = nr_php_get_zval_object_property(endpoint_zval, "host");
560+
if (nr_php_is_zval_non_empty_string(host_zval)) {
561+
datastore_params->instance->host = Z_STRVAL_P(host_zval);
562+
563+
/* Only try to get a port if we have a valid host. */
564+
port_zval = nr_php_get_zval_object_property(endpoint_zval, "port");
565+
if (nr_php_is_zval_valid_integer(port_zval)) {
566+
/* Must be freed by caller */
567+
datastore_params->instance->port_path_or_id
568+
= nr_formatf(NR_INT64_FMT, Z_LVAL_P(port_zval));
569+
} else {
570+
/* In case where host was found but port was not, spec says return
571+
* unknown for port. */
572+
datastore_params->instance->port_path_or_id = nr_strdup("unknown");
573+
}
574+
}
575+
}
576+
577+
if (NULL == datastore_params->instance->host) {
578+
/* Unable to retrieve the endpoint, go with AWS defaults. */
579+
datastore_params->instance->host = AWS_SDK_PHP_DYNAMODBCLIENT_DEFAULT_HOST;
580+
/* Need to strdup because the calling function will free it. */
581+
datastore_params->instance->port_path_or_id
582+
= nr_strdup(AWS_SDK_PHP_DYNAMODBCLIENT_DEFAULT_PORT);
583+
}
584+
585+
table_name = nr_lib_aws_sdk_php_get_command_arg_value(
586+
AWS_SDK_PHP_DYNAMODBCLIENT_TABLENAME_ARG, NR_EXECUTE_ORIG_ARGS);
587+
if (!nr_strempty(table_name)) {
588+
/* Must be freed by caller */
589+
datastore_params->collection = table_name;
590+
}
591+
if (!nr_strempty(NRINI(aws_account_id))) {
592+
account_id = NRINI(aws_account_id);
593+
}
594+
595+
if (NULL != datastore_params->collection && NULL != account_id
596+
&& NULL != cloud_attrs->cloud_region) {
597+
/* Must be freed by caller */
598+
cloud_attrs->cloud_resource_id = nr_formatf(
599+
"arn:aws:dynamodb:%s:%s:table/%s", cloud_attrs->cloud_region,
600+
account_id, datastore_params->collection);
601+
}
602+
}
603+
604+
void nr_lib_aws_sdk_php_dynamodb_handle(nr_segment_t* auto_segment,
605+
char* command_name_string,
606+
size_t command_name_len,
607+
NR_EXECUTE_PROTO) {
608+
nr_segment_t* datastore_segment = NULL;
609+
nr_segment_cloud_attrs_t cloud_attrs = {0};
610+
nr_datastore_instance_t instance = {0};
611+
nr_segment_datastore_params_t datastore_params = {
612+
.db_system = AWS_SDK_PHP_DYNAMODBCLIENT_DATASTORE_SYSTEM,
613+
.datastore = {
614+
.type = NR_DATASTORE_DYNAMODB,
615+
},
616+
.instance = &instance,
617+
.callbacks = {
618+
.backtrace = nr_php_backtrace_callback,
619+
},
620+
};
621+
if (NULL == auto_segment) {
622+
return;
623+
}
624+
625+
if (NULL == command_name_string || 0 == command_name_len) {
626+
return;
627+
}
628+
629+
#define AWS_COMMAND_IS(CMD) \
630+
(command_name_len == (sizeof(CMD) - 1) && nr_streq(CMD, command_name_string))
631+
632+
/* Determine if we instrument this command. */
633+
if (AWS_COMMAND_IS("createTable")) {
634+
datastore_params.operation = AWS_SDK_PHP_DYNAMODBCLIENT_CREATE_TABLE;
635+
} else if (AWS_COMMAND_IS("deleteItem")) {
636+
datastore_params.operation = AWS_SDK_PHP_DYNAMODBCLIENT_DELETE_ITEM;
637+
} else if (AWS_COMMAND_IS("deleteTable")) {
638+
datastore_params.operation = AWS_SDK_PHP_DYNAMODBCLIENT_DELETE_TABLE;
639+
} else if (AWS_COMMAND_IS("getItem")) {
640+
datastore_params.operation = AWS_SDK_PHP_DYNAMODBCLIENT_GET_ITEM;
641+
} else if (AWS_COMMAND_IS("putItem")) {
642+
datastore_params.operation = AWS_SDK_PHP_DYNAMODBCLIENT_PUT_ITEM;
643+
} else if (AWS_COMMAND_IS("query")) {
644+
datastore_params.operation = AWS_SDK_PHP_DYNAMODBCLIENT_QUERY;
645+
} else if (AWS_COMMAND_IS("scan")) {
646+
datastore_params.operation = AWS_SDK_PHP_DYNAMODBCLIENT_SCAN;
647+
} else if (AWS_COMMAND_IS("updateItem")) {
648+
datastore_params.operation = AWS_SDK_PHP_DYNAMODBCLIENT_UPDATE_ITEM;
649+
} else {
650+
/* Nothing to do here so exit. */
651+
return;
652+
}
653+
#undef AWS_COMMAND_IS
654+
655+
/*
656+
* nr_lib_aws_sdk_php_dynamodb_set_params sets:
657+
* the cloud_attrs->region and cloud_resource_id(needs to be freed)
658+
* datastore->instance host and port_path_or_id(needs to be freed)
659+
* datastore->collection (needs to be freed)
660+
*/
661+
nr_lib_aws_sdk_php_dynamodb_set_params(&datastore_params, &cloud_attrs,
662+
NR_EXECUTE_ORIG_ARGS);
663+
664+
/*
665+
* By this point, the datastore params are decoded, grab the parent segment
666+
* start time, add the special segment attributes/metrics then close the newly
667+
* created segment.
668+
*/
669+
datastore_segment = nr_segment_start(NRPRG(txn), NULL, NULL);
670+
if (NULL == datastore_segment) {
671+
return;
672+
}
673+
/* re-use start time from auto_segment started in func_begin */
674+
datastore_segment->start_time = auto_segment->start_time;
675+
cloud_attrs.aws_operation = command_name_string;
676+
677+
/* Add cloud attributes, if available. */
678+
nr_segment_traces_add_cloud_attributes(datastore_segment, &cloud_attrs);
679+
680+
/* Now end the instrumented segment as a message segment. */
681+
nr_segment_datastore_end(&datastore_segment, &datastore_params);
682+
nr_free(datastore_params.collection);
683+
nr_free(cloud_attrs.cloud_resource_id);
684+
nr_free(instance.port_path_or_id);
685+
}
686+
522687
/*
523688
* For Aws/AwsClient::__call see
524689
* https://github.com/aws/aws-sdk-php/blob/master/src/AwsClientInterface.php
@@ -580,8 +745,12 @@ NR_PHP_WRAPPER(nr_aws_client_call) {
580745
NR_EXECUTE_ORIG_ARGS);
581746
} else if (AWS_CLASS_IS("Aws\\Lambda\\LambdaClient", "LambdaClient")) {
582747
nr_lib_aws_sdk_php_lambda_handle(auto_segment, command_name_string,
583-
Z_STRLEN_P(command_name),
584-
NR_EXECUTE_ORIG_ARGS);
748+
Z_STRLEN_P(command_name),
749+
NR_EXECUTE_ORIG_ARGS);
750+
} else if (AWS_CLASS_IS("Aws\\DynamoDb\\DynamoDbClient", "DynamoDbClient")) {
751+
nr_lib_aws_sdk_php_dynamodb_handle(auto_segment, command_name_string,
752+
Z_STRLEN_P(command_name),
753+
NR_EXECUTE_ORIG_ARGS);
585754
}
586755

587756
#undef AWS_CLASS_IS

0 commit comments

Comments
 (0)