Skip to content

Commit 5550f62

Browse files
authored
feat(agent): Add AWS DynamoDb Instrumentation (#1037)
Added functionality to instrument ``` Call collection(tablename) operation product Create Table TableName parameter create_table DynamoDB Delete Item TableName parameter delete_item DynamoDB Delete Table TableName parameter delete_table DynamoDB Get Item TableName parameter get_item DynamoDB Put Item TableName parameter put_item DynamoDB Query TableName parameter query DynamoDB Scan TableName parameter scan DynamoDB Update Item TableName parameter update_item DynamoDB ``` Unit tests added. See also relevant multiverse PR. See also relevant soak test PR. Needed a modification to axiom to: * add db.system * modify datastore to allow for datastore that don't have concept of database name
1 parent b00dcc3 commit 5550f62

13 files changed

+794
-68
lines changed

agent/lib_aws_sdk_php.c

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

587754
#undef AWS_CLASS_IS

0 commit comments

Comments
 (0)