Skip to content

Commit 583b144

Browse files
authored
Merge pull request #1227 from newrelic/support-lambda-entities
Add attrs for aws lambda entity
2 parents eecfb48 + 2c4f0c3 commit 583b144

File tree

4 files changed

+182
-2
lines changed

4 files changed

+182
-2
lines changed

newrelic/core/attribute.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"aws.operation",
4949
"aws.requestId",
5050
"cloud.account.id",
51+
"cloud.platform",
5152
"cloud.region",
5253
"cloud.resource_id",
5354
"code.filepath",

newrelic/hooks/external_botocore.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
wrap_function_wrapper,
3636
)
3737
from newrelic.common.package_version_utils import get_package_version
38+
from newrelic.common.signature import bind_args
3839
from newrelic.core.config import global_settings
3940

4041
QUEUE_URL_PATTERN = re.compile(r"https://sqs.([\w\d-]+).amazonaws.com/(\d+)/([^/]+)")
@@ -1001,6 +1002,53 @@ def _nr_sqs_message_trace_wrapper_(wrapped, instance, args, kwargs):
10011002
return _nr_sqs_message_trace_wrapper_
10021003

10031004

1005+
def wrap_emit_api_params(wrapped, instance, args, kwargs):
1006+
transaction = current_transaction()
1007+
if not transaction:
1008+
return wrapped(*args, **kwargs)
1009+
1010+
bound_args = bind_args(wrapped, args, kwargs)
1011+
1012+
api_params = wrapped(*args, **kwargs)
1013+
1014+
arn = bound_args.get("api_params").get("FunctionName")
1015+
if arn:
1016+
try:
1017+
if arn.startswith("arn:"):
1018+
api_params["_nr_arn"] = arn
1019+
except Exception:
1020+
pass # Unable to determine ARN from FunctionName.
1021+
1022+
# Wrap instance._serializer.serialize_to_request if not already wrapped.
1023+
if (
1024+
hasattr(instance, "_serializer")
1025+
and hasattr(instance._serializer, "serialize_to_request")
1026+
and not hasattr(instance._serializer, "_nr_wrapped")
1027+
):
1028+
1029+
@function_wrapper
1030+
def wrap_serialize_to_request(wrapped, instance, args, kwargs):
1031+
transaction = current_transaction()
1032+
if not transaction:
1033+
return wrapped(*args, **kwargs)
1034+
1035+
bound_args = bind_args(wrapped, args, kwargs)
1036+
1037+
arn = bound_args.get("parameters", {}).pop("_nr_arn", None)
1038+
1039+
request_dict = wrapped(*args, **kwargs)
1040+
1041+
if arn:
1042+
request_dict["_nr_arn"] = arn
1043+
1044+
return request_dict
1045+
1046+
instance._serializer.serialize_to_request = wrap_serialize_to_request(instance._serializer.serialize_to_request)
1047+
instance._serializer._nr_wrapped = True
1048+
1049+
return api_params
1050+
1051+
10041052
CUSTOM_TRACE_POINTS = {
10051053
("sns", "publish"): message_trace("SNS", "Produce", "Topic", extract(("TopicArn", "TargetArn"), "PhoneNumber")),
10061054
("dynamodb", "put_item"): dynamodb_datastore_trace("DynamoDB", extract("TableName"), "put_item"),
@@ -1063,6 +1111,11 @@ def _nr_endpoint_make_request_(wrapped, instance, args, kwargs):
10631111
with ExternalTrace(library="botocore", url=url, method=method, source=wrapped) as trace:
10641112
try:
10651113
trace._add_agent_attribute("aws.operation", operation_model.name)
1114+
bound_args = bind_args(wrapped, args, kwargs)
1115+
lambda_arn = bound_args.get("request_dict").pop("_nr_arn", None)
1116+
if lambda_arn:
1117+
trace._add_agent_attribute("cloud.platform", "aws_lambda")
1118+
trace._add_agent_attribute("cloud.resource_id", lambda_arn)
10661119
except:
10671120
pass
10681121

@@ -1080,5 +1133,8 @@ def instrument_botocore_endpoint(module):
10801133

10811134

10821135
def instrument_botocore_client(module):
1083-
wrap_function_wrapper(module, "ClientCreator._create_api_method", _nr_clientcreator__create_api_method_)
1084-
wrap_function_wrapper(module, "ClientCreator._create_methods", _nr_clientcreator__create_methods)
1136+
if hasattr(module, "ClientCreator"):
1137+
wrap_function_wrapper(module, "ClientCreator._create_api_method", _nr_clientcreator__create_api_method_)
1138+
wrap_function_wrapper(module, "ClientCreator._create_methods", _nr_clientcreator__create_methods)
1139+
if hasattr(module, "BaseClient"):
1140+
wrap_function_wrapper(module, "BaseClient._emit_api_params", wrap_emit_api_params)
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Copyright 2010 New Relic, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import io
16+
import json
17+
import zipfile
18+
19+
import boto3
20+
import pytest
21+
from moto import mock_aws
22+
from testing_support.fixtures import dt_enabled
23+
from testing_support.validators.validate_span_events import validate_span_events
24+
from testing_support.validators.validate_transaction_metrics import (
25+
validate_transaction_metrics,
26+
)
27+
28+
from newrelic.api.background_task import background_task
29+
from newrelic.common.package_version_utils import get_package_version_tuple
30+
31+
MOTO_VERSION = get_package_version_tuple("moto")
32+
BOTOCORE_VERSION = get_package_version_tuple("botocore")
33+
34+
AWS_ACCESS_KEY_ID = "AAAAAAAAAAAACCESSKEY"
35+
AWS_SECRET_ACCESS_KEY = "AAAAAASECRETKEY" # nosec
36+
AWS_REGION_NAME = "us-west-2"
37+
38+
LAMBDA_URL = "lambda.us-west-2.amazonaws.com"
39+
EXPECTED_LAMBDA_URL = f"https://{LAMBDA_URL}/2015-03-31/functions"
40+
LAMBDA_ARN = f"arn:aws:lambda:{AWS_REGION_NAME}:383735328703:function:lambdaFunction"
41+
42+
43+
_lambda_scoped_metrics = [
44+
(f"External/{LAMBDA_URL}/botocore/POST", 2),
45+
]
46+
47+
_lambda_rollup_metrics = [
48+
("External/all", 3),
49+
("External/allOther", 3),
50+
(f"External/{LAMBDA_URL}/all", 2),
51+
(f"External/{LAMBDA_URL}/botocore/POST", 2),
52+
]
53+
54+
55+
@dt_enabled
56+
@validate_span_events(exact_agents={"aws.operation": "CreateFunction"}, count=1)
57+
@validate_span_events(
58+
exact_agents={"aws.operation": "Invoke", "cloud.platform": "aws_lambda", "cloud.resource_id": LAMBDA_ARN}, count=1
59+
)
60+
@validate_span_events(exact_agents={"aws.operation": "Invoke"}, count=1)
61+
@validate_span_events(exact_agents={"http.url": EXPECTED_LAMBDA_URL}, count=1)
62+
@validate_transaction_metrics(
63+
"test_boto3_lambda:test_lambda",
64+
scoped_metrics=_lambda_scoped_metrics,
65+
rollup_metrics=_lambda_rollup_metrics,
66+
background_task=True,
67+
)
68+
@background_task()
69+
@mock_aws
70+
def test_lambda(iam_role_arn, lambda_zip):
71+
role_arn = iam_role_arn()
72+
73+
client = boto3.client(
74+
"lambda",
75+
aws_access_key_id=AWS_ACCESS_KEY_ID,
76+
aws_secret_access_key=AWS_SECRET_ACCESS_KEY,
77+
region_name=AWS_REGION_NAME,
78+
)
79+
80+
# Create lambda
81+
resp = client.create_function(
82+
FunctionName="lambdaFunction", Runtime="python3.9", Role=role_arn, Code={"ZipFile": lambda_zip}
83+
)
84+
assert resp["ResponseMetadata"]["HTTPStatusCode"] == 201
85+
86+
# Invoke lambda
87+
client.invoke(FunctionName=LAMBDA_ARN, InvocationType="RequestResponse", Payload=json.dumps({}))
88+
assert resp["ResponseMetadata"]["HTTPStatusCode"] == 201
89+
90+
91+
@pytest.fixture
92+
def lambda_zip():
93+
code = """
94+
def lambda_handler(event, context):
95+
return event
96+
"""
97+
zip_output = io.BytesIO()
98+
zip_file = zipfile.ZipFile(zip_output, "w", zipfile.ZIP_DEFLATED)
99+
zip_file.writestr("lambda_function.py", code)
100+
zip_file.close()
101+
zip_output.seek(0)
102+
return zip_output.read()
103+
104+
105+
@pytest.fixture
106+
def iam_role_arn():
107+
def create_role():
108+
iam = boto3.client(
109+
"iam",
110+
aws_access_key_id=AWS_ACCESS_KEY_ID,
111+
aws_secret_access_key=AWS_SECRET_ACCESS_KEY,
112+
region_name=AWS_REGION_NAME,
113+
)
114+
# Create IAM role
115+
role = iam.create_role(
116+
RoleName="my-role",
117+
AssumeRolePolicyDocument="some policy",
118+
Path="/my-path/",
119+
)
120+
return role["Role"]["Arn"]
121+
122+
return create_role

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ deps =
293293
external_botocore-botocore128: botocore<1.29
294294
external_botocore-botocore0125: botocore<1.26
295295
external_botocore: moto
296+
external_botocore: docker
296297
external_feedparser-feedparser06: feedparser<7
297298
external_httplib2: httplib2<1.0
298299
external_httpx: httpx[http2]

0 commit comments

Comments
 (0)