Skip to content

Commit 57f7f12

Browse files
ZNeumannzsistla
andauthored
feat(agent): Add AWS Lambda Relationship (#1023)
Instrument AWS Lambda `invoke` and reconstruct the ARN to facilitate a complete service map. --------- Co-authored-by: Amber Sistla <[email protected]>
1 parent f02c410 commit 57f7f12

10 files changed

+491
-1
lines changed

agent/lib_aws_sdk_php.c

+200
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,16 @@
1515
#include "fw_support.h"
1616
#include "util_logging.h"
1717
#include "nr_segment_message.h"
18+
#include "nr_segment_external.h"
1819
#include "lib_aws_sdk_php.h"
1920

2021
#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-]+))?"
2128

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

305+
void nr_lib_aws_sdk_php_lambda_handle(nr_segment_t* auto_segment,
306+
char* command_name_string,
307+
size_t command_name_len,
308+
NR_EXECUTE_PROTO) {
309+
nr_segment_t* external_segment = NULL;
310+
zval** retval_ptr = NR_GET_RETURN_VALUE_PTR;
311+
312+
nr_segment_cloud_attrs_t cloud_attrs = {
313+
.cloud_platform = "aws_lambda"
314+
};
315+
316+
if (NULL == auto_segment) {
317+
return;
318+
}
319+
320+
if (NULL == command_name_string || 0 == command_name_len) {
321+
return;
322+
}
323+
324+
if (NULL == *retval_ptr) {
325+
/* Do not instrument when an exception has happened */
326+
return;
327+
}
328+
329+
#define AWS_COMMAND_IS(CMD) \
330+
(command_name_len == (sizeof(CMD) - 1) && nr_streq(CMD, command_name_string))
331+
332+
/* Determine if we instrument this command. */
333+
if (AWS_COMMAND_IS("invoke")) {
334+
/* reconstruct the ARN */
335+
nr_aws_sdk_lambda_client_invoke_parse_args(NR_EXECUTE_ORIG_ARGS, &cloud_attrs);
336+
} else {
337+
return;
338+
}
339+
#undef AWS_COMMAND_IS
340+
341+
/*
342+
* By this point, it's been determined that this call will be instrumented so
343+
* only create the segment now, grab the parent segment start time, add our
344+
* special segment attributes/metrics then close the newly created segment.
345+
*/
346+
external_segment = nr_segment_start(NRPRG(txn), NULL, NULL);
347+
if (NULL == external_segment) {
348+
nr_free(cloud_attrs.cloud_resource_id);
349+
return;
350+
}
351+
/* re-use start time from auto_segment started in func_begin */
352+
external_segment->start_time = auto_segment->start_time;
353+
cloud_attrs.aws_operation = command_name_string;
354+
355+
/* end the segment */
356+
nr_segment_traces_add_cloud_attributes(external_segment, &cloud_attrs);
357+
nr_segment_external_params_t external_params = {.library = "aws_sdk"};
358+
zval* data = nr_php_get_zval_object_property(*retval_ptr, "data");
359+
if (nr_php_is_zval_valid_array(data)) {
360+
zval* status_code = nr_php_zend_hash_find(Z_ARRVAL_P(data), "StatusCode");
361+
if (nr_php_is_zval_valid_integer(status_code)) {
362+
external_params.status = Z_LVAL_P(status_code);
363+
}
364+
zval* metadata = nr_php_zend_hash_find(Z_ARRVAL_P(data), "@metadata");
365+
if (NULL != metadata && IS_REFERENCE == Z_TYPE_P(metadata)) {
366+
metadata = Z_REFVAL_P(metadata);
367+
}
368+
if (nr_php_is_zval_valid_array(metadata)) {
369+
zval* uri = nr_php_zend_hash_find(Z_ARRVAL_P(metadata), "effectiveUri");
370+
if (nr_php_is_zval_non_empty_string(uri)) {
371+
external_params.uri = Z_STRVAL_P(uri);
372+
}
373+
}
374+
375+
}
376+
nr_segment_external_end(&external_segment, &external_params);
377+
nr_free(cloud_attrs.cloud_resource_id);
378+
}
379+
380+
/* This stores the compiled regex to parse AWS ARNs. The compilation happens when
381+
* it is first needed and is destroyed in mshutdown
382+
*/
383+
static nr_regex_t* aws_arn_regex;
384+
385+
static void nr_aws_sdk_compile_regex(void) {
386+
aws_arn_regex = nr_regex_create(AWS_LAMBDA_ARN_REGEX, 0, 0);
387+
}
388+
389+
void nr_aws_sdk_mshutdown(void) {
390+
nr_regex_destroy(&aws_arn_regex);
391+
}
392+
393+
void nr_aws_sdk_lambda_client_invoke_parse_args(NR_EXECUTE_PROTO, nr_segment_cloud_attrs_t* cloud_attrs) {
394+
zval* call_args = nr_php_get_user_func_arg(2, NR_EXECUTE_ORIG_ARGS);
395+
zval* this_obj = NR_PHP_USER_FN_THIS();
396+
char* arn = NULL;
397+
char* function_name = NULL;
398+
char* region = NULL;
399+
zval* region_zval = NULL;
400+
char* qualifier = NULL;
401+
char* accountID = NULL;
402+
bool using_account_id_ini = false;
403+
404+
/* verify arguments */
405+
if (!nr_php_is_zval_valid_array(call_args)) {
406+
return;
407+
}
408+
zval* lambda_args = nr_php_zend_hash_index_find(Z_ARRVAL_P(call_args), 0);
409+
if (!nr_php_is_zval_valid_array(lambda_args)) {
410+
return;
411+
}
412+
zval* lambda_name = nr_php_zend_hash_find(Z_ARRVAL_P(lambda_args), "FunctionName");
413+
if (!nr_php_is_zval_non_empty_string(lambda_name)) {
414+
return;
415+
}
416+
417+
/* Ensure regex exists */
418+
if (NULL == aws_arn_regex) {
419+
nr_aws_sdk_compile_regex();
420+
}
421+
422+
/* 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));
427+
function_name = nr_regex_substrings_get_named(matches, "functionName");
428+
accountID = nr_regex_substrings_get_named(matches, "accountId");
429+
region = nr_regex_substrings_get_named(matches, "region");
430+
qualifier = nr_regex_substrings_get_named(matches, "qualifier");
431+
432+
/* supplement missing information with API calls */
433+
if (nr_strempty(function_name)) {
434+
/*
435+
* Cannot get the needed data. Function name is required in the
436+
* argument, so this won't happen in normal operation
437+
*/
438+
nr_free(function_name);
439+
nr_free(accountID);
440+
nr_free(region);
441+
nr_free(qualifier);
442+
nr_regex_substrings_destroy(&matches);
443+
return;
444+
}
445+
if (nr_strempty(accountID)) {
446+
nr_free(accountID);
447+
accountID = NRINI(aws_account_id);
448+
using_account_id_ini = true;
449+
}
450+
if (nr_strempty(region)) {
451+
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;
454+
}
455+
region_zval
456+
= nr_php_get_zval_object_property_with_class(this_obj, base_class, "region");
457+
if (nr_php_is_zval_valid_string(region_zval)) {
458+
/*
459+
* In this case, region is likely to be NULL, but could be an empty
460+
* string instead, so we must free
461+
*/
462+
nr_free(region);
463+
region = Z_STRVAL_P(region_zval);
464+
}
465+
}
466+
467+
if (!nr_strempty(accountID) && !nr_strempty(region)) {
468+
/* construct the ARN */
469+
if (!nr_strempty(qualifier)) {
470+
arn = nr_formatf("arn:aws:lambda:%s:%s:function:%s:%s",
471+
region, accountID, function_name, qualifier);
472+
} else {
473+
arn = nr_formatf("arn:aws:lambda:%s:%s:function:%s",
474+
region, accountID, function_name);
475+
}
476+
477+
/* Attach the ARN */
478+
cloud_attrs->cloud_resource_id = arn;
479+
}
480+
481+
nr_regex_substrings_destroy(&matches);
482+
nr_free(function_name);
483+
if (!using_account_id_ini) {
484+
nr_free(accountID);
485+
}
486+
/* if region_zval is a valid string, we have already freed region */
487+
if (!nr_php_is_zval_valid_string(region_zval)) {
488+
nr_free(region);
489+
}
490+
nr_free(qualifier);
491+
}
492+
298493
char* nr_lib_aws_sdk_php_get_command_arg_value(char* command_arg_name,
299494
NR_EXECUTE_PROTO) {
300495
zval* param_array = NULL;
@@ -383,6 +578,10 @@ NR_PHP_WRAPPER(nr_aws_client_call) {
383578
nr_lib_aws_sdk_php_sqs_handle(auto_segment, command_name_string,
384579
Z_STRLEN_P(command_name),
385580
NR_EXECUTE_ORIG_ARGS);
581+
} else if (AWS_CLASS_IS("Aws\\Lambda\\LambdaClient", "LambdaClient")) {
582+
nr_lib_aws_sdk_php_lambda_handle(auto_segment, command_name_string,
583+
Z_STRLEN_P(command_name),
584+
NR_EXECUTE_ORIG_ARGS);
386585
}
387586

388587
#undef AWS_CLASS_IS
@@ -566,5 +765,6 @@ void nr_aws_sdk_php_enable() {
566765
nr_php_wrap_user_function_before_after_clean(
567766
NR_PSTR("Aws\\AwsClient::__call"), NULL, nr_aws_client_call,
568767
nr_aws_client_call);
768+
569769
#endif
570770
}

agent/lib_aws_sdk_php.h

+18
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,24 @@ extern void nr_lib_aws_sdk_php_sqs_handle(nr_segment_t* segment,
7474
size_t command_name_len,
7575
NR_EXECUTE_PROTO);
7676

77+
/*
78+
* Purpose : Handle when a LambdaClient::invoke command happens
79+
*
80+
* Params : 1. NR_EXECUTE_ORIG_ARGS (execute_data, func_return_value)
81+
* 2. cloud_attrs : the cloud attributes pointer to be
82+
* populated with the ARN
83+
*
84+
* Returns :
85+
*
86+
* Note: The caller is responsible for freeing cloud_attrs->cloud_resource_id
87+
*/
88+
void nr_aws_sdk_lambda_client_invoke_parse_args(NR_EXECUTE_PROTO, nr_segment_cloud_attrs_t* cloud_attrs);
89+
90+
/*
91+
* Purpose : Handles regex destruction during mshutdown
92+
*/
93+
void nr_aws_sdk_mshutdown(void);
94+
7795
/*
7896
* Purpose : The second argument to the Aws/AwsClient::__call function should be
7997
* an array, the first element of which is itself an array of arguments that

agent/php_mshutdown.c

+4
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ PHP_MSHUTDOWN_FUNCTION(newrelic) {
4242

4343
nr_wordpress_mshutdown();
4444

45+
#if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO /* PHP 8.1+ */
46+
nr_aws_sdk_mshutdown();
47+
#endif
48+
4549
/* restore header handler */
4650
sapi_module.header_handler = NR_PHP_PROCESS_GLOBALS(orig_header_handler);
4751
NR_PHP_PROCESS_GLOBALS(orig_header_handler) = NULL;

agent/php_newrelic.h

+8-1
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,6 @@ bool wordpress_core; /* set based on
328328
nrinistr_t
329329
wordpress_hooks_skip_filename; /* newrelic.framework.wordpress.hooks_skip_filename
330330
*/
331-
332331
nrinibool_t
333332
analytics_events_enabled; /* DEPRECATED newrelic.analytics_events.enabled */
334333
nrinibool_t
@@ -383,6 +382,11 @@ nrinibool_t
383382
nrinibool_t
384383
database_name_reporting_enabled; /* newrelic.datastore_tracer.database_name_reporting.enabled
385384
*/
385+
/*
386+
* Cloud relationship settings
387+
*/
388+
nrinistr_t
389+
aws_account_id; /* newrelic.cloud.aws.account_id */
386390

387391
/*
388392
* Deprecated settings that control request parameter capture.
@@ -464,6 +468,7 @@ nr_stack_t wordpress_tag_states; /* stack of bools indicating
464468
bool check_cufa; /* Whether we need to check cufa because we are
465469
instrumenting hooks, or whether we can skip cufa */
466470
char* wordpress_tag; /* The current WordPress tag */
471+
467472
#endif //OAPI
468473

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

482487
nrphpcufafn_t
483488
cufa_callback; /* The current call_user_func_array callback, if any */
489+
490+
nr_regex_t* aws_arn_regex; /* The compiled regex to search for ARNs */
484491
/*
485492
* We instrument database connection constructors and store the instance
486493
* information in a hash keyed by a string containing the connection resource

agent/php_nrini.c

+39
Original file line numberDiff line numberDiff line change
@@ -1212,6 +1212,33 @@ static PHP_INI_MH(nr_string_mh) {
12121212
return FAILURE;
12131213
}
12141214

1215+
static PHP_INI_MH(nr_aws_account_id_mh) {
1216+
nrinistr_t* p;
1217+
const int AWS_ACCOUNT_ID_SIZE = 12;
1218+
1219+
#ifndef ZTS
1220+
char* base = (char*)mh_arg2;
1221+
#else
1222+
char* base = (char*)ts_resource(*((int*)mh_arg2));
1223+
#endif
1224+
1225+
p = (nrinistr_t*)(base + (size_t)mh_arg1);
1226+
1227+
(void)entry;
1228+
(void)mh_arg3;
1229+
NR_UNUSED_TSRMLS;
1230+
1231+
p->where = 0;
1232+
1233+
if (NEW_VALUE_LEN == AWS_ACCOUNT_ID_SIZE) {
1234+
p->value = NEW_VALUE;
1235+
p->where = stage;
1236+
return SUCCESS;
1237+
}
1238+
1239+
return FAILURE;
1240+
}
1241+
12151242
static PHP_INI_MH(nr_boolean_mh) {
12161243
nrinibool_t* p;
12171244
int val = 0;
@@ -3100,6 +3127,18 @@ STD_PHP_INI_ENTRY_EX("newrelic.vulnerability_management.composer_api.enabled",
31003127
newrelic_globals,
31013128
nr_enabled_disabled_dh)
31023129

3130+
/*
3131+
* Cloud relationship settings
3132+
*/
3133+
STD_PHP_INI_ENTRY_EX("newrelic.cloud.aws.account_id",
3134+
"",
3135+
NR_PHP_REQUEST,
3136+
nr_aws_account_id_mh,
3137+
aws_account_id,
3138+
zend_newrelic_globals,
3139+
newrelic_globals,
3140+
0)
3141+
31033142
/*
31043143
* Messaging API
31053144
*/

agent/scripts/newrelic.ini.template

+14
Original file line numberDiff line numberDiff line change
@@ -1352,3 +1352,17 @@ newrelic.daemon.logfile = "/var/log/newrelic/newrelic-daemon.log"
13521352
; newrelic.span_events.attributes.include/exclude
13531353
;
13541354
;newrelic.message_tracer.segment_parameters.enabled = true
1355+
1356+
; Setting: newrelic.cloud.aws.account_id
1357+
; Type : string
1358+
; Scope : per-directory
1359+
; Default: none
1360+
; Info : This setting is read by some cloud service instrumentation so the
1361+
; cloud.resource_id attribute can be set in the respective spans.
1362+
; Do not include any "-" characters; this should be 12 characters.
1363+
;
1364+
; AWS DynamoDB and Kinesis are services that require this value to be
1365+
; able to populate the cloud.resource_id attribute. Likewise, AWS Lambda
1366+
; requires that this value when the account ID is not part of the function name.
1367+
;
1368+
;newrelic.cloud.aws.account_id = ""

0 commit comments

Comments
 (0)