Skip to content
Merged
200 changes: 200 additions & 0 deletions agent/lib_aws_sdk_php.c
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,16 @@
#include "fw_support.h"
#include "util_logging.h"
#include "nr_segment_message.h"
#include "nr_segment_external.h"
#include "lib_aws_sdk_php.h"

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

#if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO /* PHP8.1+ */
/* Service instrumentation only supported above PHP 8.1+*/
Expand Down Expand Up @@ -295,6 +302,194 @@ void nr_lib_aws_sdk_php_sqs_parse_queueurl(
cloud_attrs->cloud_region = region;
}

void nr_lib_aws_sdk_php_lambda_handle(nr_segment_t* auto_segment,
char* command_name_string,
size_t command_name_len,
NR_EXECUTE_PROTO) {
nr_segment_t* external_segment = NULL;
zval** retval_ptr = NR_GET_RETURN_VALUE_PTR;

nr_segment_cloud_attrs_t cloud_attrs = {
.cloud_platform = "aws_lambda"
};

if (NULL == auto_segment) {
return;
}

if (NULL == command_name_string || 0 == command_name_len) {
return;
}

if (NULL == *retval_ptr) {
/* Do not instrument when an exception has happened */
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("invoke")) {
/* reconstruct the ARN */
nr_aws_sdk_lambda_client_invoke_parse_args(NR_EXECUTE_ORIG_ARGS, &cloud_attrs);
} else {
return;
}
#undef AWS_COMMAND_IS

/*
* 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.
*/
external_segment = nr_segment_start(NRPRG(txn), NULL, NULL);
if (NULL == external_segment) {
nr_free(cloud_attrs.cloud_resource_id);
return;
}
/* re-use start time from auto_segment started in func_begin */
external_segment->start_time = auto_segment->start_time;
cloud_attrs.aws_operation = command_name_string;

/* end the segment */
nr_segment_traces_add_cloud_attributes(external_segment, &cloud_attrs);
nr_segment_external_params_t external_params = {.library = "aws_sdk"};
zval* data = nr_php_get_zval_object_property(*retval_ptr, "data");
if (nr_php_is_zval_valid_array(data)) {
zval* status_code = nr_php_zend_hash_find(Z_ARRVAL_P(data), "StatusCode");
if (nr_php_is_zval_valid_integer(status_code)) {
external_params.status = Z_LVAL_P(status_code);
}
zval* metadata = nr_php_zend_hash_find(Z_ARRVAL_P(data), "@metadata");
if (NULL != metadata && IS_REFERENCE == Z_TYPE_P(metadata)) {
metadata = Z_REFVAL_P(metadata);
}
if (nr_php_is_zval_valid_array(metadata)) {
zval* uri = nr_php_zend_hash_find(Z_ARRVAL_P(metadata), "effectiveUri");
if (nr_php_is_zval_non_empty_string(uri)) {
external_params.uri = Z_STRVAL_P(uri);
}
}

}
nr_segment_external_end(&external_segment, &external_params);
nr_free(cloud_attrs.cloud_resource_id);
}

/* This stores the compiled regex to parse AWS ARNs. The compilation happens when
* it is first needed and is destroyed in mshutdown
*/
static nr_regex_t* aws_arn_regex;

static void nr_aws_sdk_compile_regex(void) {
aws_arn_regex = nr_regex_create(AWS_LAMBDA_ARN_REGEX, 0, 0);
}

void nr_aws_sdk_mshutdown(void) {
nr_regex_destroy(&aws_arn_regex);
}

void nr_aws_sdk_lambda_client_invoke_parse_args(NR_EXECUTE_PROTO, nr_segment_cloud_attrs_t* cloud_attrs) {
zval* call_args = nr_php_get_user_func_arg(2, NR_EXECUTE_ORIG_ARGS);
zval* this_obj = NR_PHP_USER_FN_THIS();
char* arn = NULL;
char* function_name = NULL;
char* region = NULL;
zval* region_zval = NULL;
char* qualifier = NULL;
char* accountID = NULL;
bool using_account_id_ini = false;

/* verify arguments */
if (!nr_php_is_zval_valid_array(call_args)) {
return;
}
zval* lambda_args = nr_php_zend_hash_index_find(Z_ARRVAL_P(call_args), 0);
if (!nr_php_is_zval_valid_array(lambda_args)) {
return;
}
zval* lambda_name = nr_php_zend_hash_find(Z_ARRVAL_P(lambda_args), "FunctionName");
if (!nr_php_is_zval_non_empty_string(lambda_name)) {
return;
}

/* Ensure regex exists */
if (NULL == aws_arn_regex) {
nr_aws_sdk_compile_regex();
}

/* Extract all information possible from the passed lambda name via regex */
nr_regex_substrings_t* matches =
nr_regex_match_capture(aws_arn_regex,
Z_STRVAL_P(lambda_name),
Z_STRLEN_P(lambda_name));
function_name = nr_regex_substrings_get_named(matches, "functionName");
accountID = nr_regex_substrings_get_named(matches, "accountId");
region = nr_regex_substrings_get_named(matches, "region");
qualifier = nr_regex_substrings_get_named(matches, "qualifier");

/* supplement missing information with API calls */
if (nr_strempty(function_name)) {
/*
* Cannot get the needed data. Function name is required in the
* argument, so this won't happen in normal operation
*/
nr_free(function_name);
nr_free(accountID);
nr_free(region);
nr_free(qualifier);
nr_regex_substrings_destroy(&matches);
return;
}
if (nr_strempty(accountID)) {
nr_free(accountID);
accountID = NRINI(aws_account_id);
using_account_id_ini = true;
}
if (nr_strempty(region)) {
zend_class_entry* base_class = NULL;
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_valid_string(region_zval)) {
/*
* In this case, region is likely to be NULL, but could be an empty
* string instead, so we must free
*/
nr_free(region);
region = Z_STRVAL_P(region_zval);
}
}

if (!nr_strempty(accountID) && !nr_strempty(region)) {
/* construct the ARN */
if (!nr_strempty(qualifier)) {
arn = nr_formatf("arn:aws:lambda:%s:%s:function:%s:%s",
region, accountID, function_name, qualifier);
} else {
arn = nr_formatf("arn:aws:lambda:%s:%s:function:%s",
region, accountID, function_name);
}

/* Attach the ARN */
cloud_attrs->cloud_resource_id = arn;
}

nr_regex_substrings_destroy(&matches);
nr_free(function_name);
if (!using_account_id_ini) {
nr_free(accountID);
}
/* if region_zval is a valid string, we have already freed region */
if (!nr_php_is_zval_valid_string(region_zval)) {
nr_free(region);
}
nr_free(qualifier);
}

char* nr_lib_aws_sdk_php_get_command_arg_value(char* command_arg_name,
NR_EXECUTE_PROTO) {
zval* param_array = NULL;
Expand Down Expand Up @@ -383,6 +578,10 @@ NR_PHP_WRAPPER(nr_aws_client_call) {
nr_lib_aws_sdk_php_sqs_handle(auto_segment, command_name_string,
Z_STRLEN_P(command_name),
NR_EXECUTE_ORIG_ARGS);
} else if (AWS_CLASS_IS("Aws\\Lambda\\LambdaClient", "LambdaClient")) {
nr_lib_aws_sdk_php_lambda_handle(auto_segment, command_name_string,
Z_STRLEN_P(command_name),
NR_EXECUTE_ORIG_ARGS);
}

#undef AWS_CLASS_IS
Expand Down Expand Up @@ -566,5 +765,6 @@ void nr_aws_sdk_php_enable() {
nr_php_wrap_user_function_before_after_clean(
NR_PSTR("Aws\\AwsClient::__call"), NULL, nr_aws_client_call,
nr_aws_client_call);

#endif
}
18 changes: 18 additions & 0 deletions agent/lib_aws_sdk_php.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,24 @@ extern void nr_lib_aws_sdk_php_sqs_handle(nr_segment_t* segment,
size_t command_name_len,
NR_EXECUTE_PROTO);

/*
* Purpose : Handle when a LambdaClient::invoke command happens
*
* Params : 1. NR_EXECUTE_ORIG_ARGS (execute_data, func_return_value)
* 2. cloud_attrs : the cloud attributes pointer to be
* populated with the ARN
*
* Returns :
*
* Note: The caller is responsible for freeing cloud_attrs->cloud_resource_id
*/
void nr_aws_sdk_lambda_client_invoke_parse_args(NR_EXECUTE_PROTO, nr_segment_cloud_attrs_t* cloud_attrs);

/*
* Purpose : Handles regex destruction during mshutdown
*/
void nr_aws_sdk_mshutdown(void);

/*
* 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
Expand Down
4 changes: 4 additions & 0 deletions agent/php_mshutdown.c
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ PHP_MSHUTDOWN_FUNCTION(newrelic) {

nr_wordpress_mshutdown();

#if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO /* PHP 8.1+ */
nr_aws_sdk_mshutdown();
#endif

/* restore header handler */
sapi_module.header_handler = NR_PHP_PROCESS_GLOBALS(orig_header_handler);
NR_PHP_PROCESS_GLOBALS(orig_header_handler) = NULL;
Expand Down
9 changes: 8 additions & 1 deletion agent/php_newrelic.h
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,6 @@ bool wordpress_core; /* set based on
nrinistr_t
wordpress_hooks_skip_filename; /* newrelic.framework.wordpress.hooks_skip_filename
*/

nrinibool_t
analytics_events_enabled; /* DEPRECATED newrelic.analytics_events.enabled */
nrinibool_t
Expand Down Expand Up @@ -383,6 +382,11 @@ nrinibool_t
nrinibool_t
database_name_reporting_enabled; /* newrelic.datastore_tracer.database_name_reporting.enabled
*/
/*
* Cloud relationship settings
*/
nrinistr_t
aws_account_id; /* newrelic.cloud.aws.account_id */

/*
* Deprecated settings that control request parameter capture.
Expand Down Expand Up @@ -464,6 +468,7 @@ nr_stack_t wordpress_tag_states; /* stack of bools indicating
bool check_cufa; /* Whether we need to check cufa because we are
instrumenting hooks, or whether we can skip cufa */
char* wordpress_tag; /* The current WordPress tag */

#endif //OAPI

nr_matcher_t* wordpress_plugin_matcher; /* Matcher for plugin filenames */
Expand All @@ -481,6 +486,8 @@ int php_cur_stack_depth; /* Total current depth of PHP stack, measured in PHP

nrphpcufafn_t
cufa_callback; /* The current call_user_func_array callback, if any */

nr_regex_t* aws_arn_regex; /* The compiled regex to search for ARNs */
/*
* We instrument database connection constructors and store the instance
* information in a hash keyed by a string containing the connection resource
Expand Down
39 changes: 39 additions & 0 deletions agent/php_nrini.c
Original file line number Diff line number Diff line change
Expand Up @@ -1212,6 +1212,33 @@ static PHP_INI_MH(nr_string_mh) {
return FAILURE;
}

static PHP_INI_MH(nr_aws_account_id_mh) {
nrinistr_t* p;
const int AWS_ACCOUNT_ID_SIZE = 12;

#ifndef ZTS
char* base = (char*)mh_arg2;
#else
char* base = (char*)ts_resource(*((int*)mh_arg2));
#endif

p = (nrinistr_t*)(base + (size_t)mh_arg1);

(void)entry;
(void)mh_arg3;
NR_UNUSED_TSRMLS;

p->where = 0;

if (NEW_VALUE_LEN == AWS_ACCOUNT_ID_SIZE) {
p->value = NEW_VALUE;
p->where = stage;
return SUCCESS;
}

return FAILURE;
}

static PHP_INI_MH(nr_boolean_mh) {
nrinibool_t* p;
int val = 0;
Expand Down Expand Up @@ -3100,6 +3127,18 @@ STD_PHP_INI_ENTRY_EX("newrelic.vulnerability_management.composer_api.enabled",
newrelic_globals,
nr_enabled_disabled_dh)

/*
* Cloud relationship settings
*/
STD_PHP_INI_ENTRY_EX("newrelic.cloud.aws.account_id",
"",
NR_PHP_REQUEST,
nr_aws_account_id_mh,
aws_account_id,
zend_newrelic_globals,
newrelic_globals,
0)

/*
* Messaging API
*/
Expand Down
14 changes: 14 additions & 0 deletions agent/scripts/newrelic.ini.template
Original file line number Diff line number Diff line change
Expand Up @@ -1352,3 +1352,17 @@ newrelic.daemon.logfile = "/var/log/newrelic/newrelic-daemon.log"
; newrelic.span_events.attributes.include/exclude
;
;newrelic.message_tracer.segment_parameters.enabled = true

; Setting: newrelic.cloud.aws.account_id
; Type : string
; Scope : per-directory
; Default: none
; Info : This setting is read by some cloud service instrumentation so the
; cloud.resource_id attribute can be set in the respective spans.
; Do not include any "-" characters; this should be 12 characters.
;
; AWS DynamoDB and Kinesis are services that require this value to be
; able to populate the cloud.resource_id attribute. Likewise, AWS Lambda
; requires that this value when the account ID is not part of the function name.
;
;newrelic.cloud.aws.account_id = ""
Loading