Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(agent): Add AWS Lambda Relationship #1023

Open
wants to merge 18 commits into
base: dev
Choose a base branch
from
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 = getThis();//NR_PHP_USER_FN_THIS();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't the NR_PHP_USER_FN_THIS macro used here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was leftover from testing about #1023 (comment). Using NR_PHP_USER_FN_THIS() in fbf408f

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");
Comment on lines +451 to +456
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this construct correct? Take a look at this example. The first argument to nr_php_get_zval_object_property_with_class is the object to extract the property from, and the second argument is the class entry for the context from which the property should be extracted. What is this code trying to do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LambdaClient extends AwsClient. region exists on the AwsClient, not the LambdaClient. So if we wish to extract region from this (which is a LambdaClient), we must indicate the base class.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the intent is to look for the "region" property either in this (which is Aws\Lambda\LambdaClient) and then fall back to looking for it in this's base class (which is Aws\AwsClient), then wouldn't it be safer to get the base_class like this: base_class = nr_php_find_class("aws\\awsclient");?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The best strategy to ensure that the agent will be able to retrieve the region is to call getRegion method on this like this: region = nr_php_call(this, "getRegion"); It doesn't rely on any assumptions but rather on publicly available API of AWS SDK for PHP.

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