|
15 | 15 | #include "fw_support.h"
|
16 | 16 | #include "util_logging.h"
|
17 | 17 | #include "nr_segment_message.h"
|
| 18 | +#include "nr_segment_external.h" |
18 | 19 | #include "lib_aws_sdk_php.h"
|
19 | 20 |
|
20 | 21 | #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-]+))?" |
21 | 28 |
|
22 | 29 | #if ZEND_MODULE_API_NO >= ZEND_8_1_X_API_NO /* PHP8.1+ */
|
23 | 30 | /* Service instrumentation only supported above PHP 8.1+*/
|
@@ -295,6 +302,194 @@ void nr_lib_aws_sdk_php_sqs_parse_queueurl(
|
295 | 302 | cloud_attrs->cloud_region = region;
|
296 | 303 | }
|
297 | 304 |
|
| 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 | + |
298 | 493 | char* nr_lib_aws_sdk_php_get_command_arg_value(char* command_arg_name,
|
299 | 494 | NR_EXECUTE_PROTO) {
|
300 | 495 | zval* param_array = NULL;
|
@@ -383,6 +578,10 @@ NR_PHP_WRAPPER(nr_aws_client_call) {
|
383 | 578 | nr_lib_aws_sdk_php_sqs_handle(auto_segment, command_name_string,
|
384 | 579 | Z_STRLEN_P(command_name),
|
385 | 580 | 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); |
386 | 585 | }
|
387 | 586 |
|
388 | 587 | #undef AWS_CLASS_IS
|
@@ -566,5 +765,6 @@ void nr_aws_sdk_php_enable() {
|
566 | 765 | nr_php_wrap_user_function_before_after_clean(
|
567 | 766 | NR_PSTR("Aws\\AwsClient::__call"), NULL, nr_aws_client_call,
|
568 | 767 | nr_aws_client_call);
|
| 768 | + |
569 | 769 | #endif
|
570 | 770 | }
|
0 commit comments