Skip to content

Commit 9abd5a1

Browse files
Kinesis ResourceARN and ConsumerARN Parsing (#1302)
* Add parsing for kinesis ResourceARN and ConsumerARN * Add updated testing for kinesis * Fixup: lint errors --------- Co-authored-by: Hannah Stepanek <[email protected]>
1 parent de08634 commit 9abd5a1

File tree

2 files changed

+112
-41
lines changed

2 files changed

+112
-41
lines changed

newrelic/hooks/external_botocore.py

+35-16
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,14 @@ def extract_sqs(*args, **kwargs):
5959

6060

6161
def extract_kinesis(*args, **kwargs):
62-
# The stream name can be passed as the StreamName or as part of the StreamARN.
63-
stream_value = kwargs.get("StreamName", None)
64-
if stream_value is None:
65-
arn = kwargs.get("StreamARN", None)
66-
if arn is not None:
67-
stream_value = arn.split("/", 1)[-1]
68-
return stream_value
62+
# The stream name can be passed as the StreamName or as part of the StreamARN, ResourceARN, or ConsumerARN.
63+
stream_name = kwargs.get("StreamName", None)
64+
if stream_name is not None:
65+
return stream_name
66+
67+
arn = kwargs.get("StreamARN", None) or kwargs.get("ResourceARN", None) or kwargs.get("ConsumerARN", None)
68+
if arn is not None:
69+
return arn.split("/")[1]
6970

7071

7172
def extract_firehose(*args, **kwargs):
@@ -90,16 +91,18 @@ def extract_sqs_agent_attrs(instance, *args, **kwargs):
9091

9192

9293
def extract_kinesis_agent_attrs(instance, *args, **kwargs):
93-
# Try to capture AWS Kinesis ARN from the StreamARN parameter or by generating the ARN from various discoverable
94-
# info. Log any exception to debug.
94+
# Try to capture AWS Kinesis ARN from the StreamARN, ConsumerARN, or ResourceARN parameters, or by generating the
95+
# ARN from various discoverable info. Log any exception to debug.
9596
agent_attrs = {}
9697
try:
9798
stream_arn = kwargs.get("StreamARN", None)
98-
if stream_arn:
99+
if stream_arn is not None:
99100
agent_attrs["cloud.platform"] = "aws_kinesis_data_streams"
100101
agent_attrs["cloud.resource_id"] = stream_arn
101-
else:
102-
stream_name = kwargs.get("StreamName", None)
102+
return agent_attrs
103+
104+
stream_name = kwargs.get("StreamName", None)
105+
if stream_name is not None:
103106
transaction = current_transaction()
104107
settings = transaction.settings if transaction.settings else global_settings()
105108
account_id = settings.cloud.aws.account_id if settings and settings.cloud.aws.account_id else None
@@ -109,6 +112,14 @@ def extract_kinesis_agent_attrs(instance, *args, **kwargs):
109112
if stream_name and account_id and region:
110113
agent_attrs["cloud.platform"] = "aws_kinesis_data_streams"
111114
agent_attrs["cloud.resource_id"] = f"arn:aws:kinesis:{region}:{account_id}:stream/{stream_name}"
115+
116+
resource_arn = kwargs.get("ResourceARN", None) or kwargs.get("ConsumerARN", None)
117+
if resource_arn is not None:
118+
# Extract just the StreamARN out of ConsumerARNs.
119+
agent_attrs["cloud.resource_id"] = "/".join(resource_arn.split("/")[0:2])
120+
agent_attrs["cloud.platform"] = "aws_kinesis_data_streams"
121+
return agent_attrs
122+
112123
except Exception as e:
113124
_logger.debug("Failed to capture AWS Kinesis info.", exc_info=True)
114125
return agent_attrs
@@ -1178,7 +1189,9 @@ def wrap_serialize_to_request(wrapped, instance, args, kwargs):
11781189
extract_agent_attrs=extract_kinesis_agent_attrs,
11791190
library="Kinesis",
11801191
),
1181-
("kinesis", "delete_resource_policy"): aws_function_trace("delete_resource_policy", library="Kinesis"),
1192+
("kinesis", "delete_resource_policy"): aws_function_trace(
1193+
"delete_resource_policy", extract_kinesis, extract_agent_attrs=extract_kinesis_agent_attrs, library="Kinesis"
1194+
),
11821195
("kinesis", "delete_stream"): aws_function_trace(
11831196
"delete_stream", extract_kinesis, extract_agent_attrs=extract_kinesis_agent_attrs, library="Kinesis"
11841197
),
@@ -1210,7 +1223,9 @@ def wrap_serialize_to_request(wrapped, instance, args, kwargs):
12101223
extract_agent_attrs=extract_kinesis_agent_attrs,
12111224
library="Kinesis",
12121225
),
1213-
("kinesis", "get_resource_policy"): aws_function_trace("get_resource_policy", library="Kinesis"),
1226+
("kinesis", "get_resource_policy"): aws_function_trace(
1227+
"get_resource_policy", extract_kinesis, extract_agent_attrs=extract_kinesis_agent_attrs, library="Kinesis"
1228+
),
12141229
("kinesis", "get_shard_iterator"): aws_function_trace(
12151230
"get_shard_iterator", extract_kinesis, extract_agent_attrs=extract_kinesis_agent_attrs, library="Kinesis"
12161231
),
@@ -1233,7 +1248,9 @@ def wrap_serialize_to_request(wrapped, instance, args, kwargs):
12331248
("kinesis", "merge_shards"): aws_function_trace(
12341249
"merge_shards", extract_kinesis, extract_agent_attrs=extract_kinesis_agent_attrs, library="Kinesis"
12351250
),
1236-
("kinesis", "put_resource_policy"): aws_function_trace("put_resource_policy", library="Kinesis"),
1251+
("kinesis", "put_resource_policy"): aws_function_trace(
1252+
"put_resource_policy", extract_kinesis, extract_agent_attrs=extract_kinesis_agent_attrs, library="Kinesis"
1253+
),
12371254
("kinesis", "register_stream_consumer"): aws_function_trace(
12381255
"register_stream_consumer", extract_kinesis, extract_agent_attrs=extract_kinesis_agent_attrs, library="Kinesis"
12391256
),
@@ -1249,7 +1266,9 @@ def wrap_serialize_to_request(wrapped, instance, args, kwargs):
12491266
("kinesis", "stop_stream_encryption"): aws_function_trace(
12501267
"stop_stream_encryption", extract_kinesis, extract_agent_attrs=extract_kinesis_agent_attrs, library="Kinesis"
12511268
),
1252-
("kinesis", "subscribe_to_shard"): aws_function_trace("subscribe_to_shard", library="Kinesis"),
1269+
("kinesis", "subscribe_to_shard"): aws_function_trace(
1270+
"subscribe_to_shard", extract_kinesis, extract_agent_attrs=extract_kinesis_agent_attrs, library="Kinesis"
1271+
),
12531272
("kinesis", "update_shard_count"): aws_function_trace(
12541273
"update_shard_count", extract_kinesis, extract_agent_attrs=extract_kinesis_agent_attrs, library="Kinesis"
12551274
),

tests/external_botocore/test_boto3_kinesis.py

+77-25
Original file line numberDiff line numberDiff line change
@@ -32,65 +32,76 @@
3232
MOTO_VERSION = get_package_version_tuple("moto")
3333
BOTOCORE_VERSION = get_package_version_tuple("boto3")
3434

35-
URL = "kinesis.us-east-1.amazonaws.com"
35+
AWS_ACCESS_KEY_ID = "AAAAAAAAAAAACCESSKEY"
36+
AWS_SECRET_ACCESS_KEY = "AAAAAASECRETKEY" # nosec
37+
AWS_REGION = "us-east-1"
38+
AWS_ACCOUNT_ID = 123456789012
39+
40+
KINESIS_URL = "kinesis.us-east-1.amazonaws.com"
41+
KINESIS_CONTROL_URL = f"{AWS_ACCOUNT_ID}.control-kinesis.{AWS_REGION}.amazonaws.com"
42+
KINESIS_DATA_URL = f"{AWS_ACCOUNT_ID}.data-kinesis.{AWS_REGION}.amazonaws.com"
3643
TEST_STREAM = f"python-agent-test-{uuid.uuid4()}"
3744
EXPECTED_AGENT_ATTRS = {
3845
"exact_agents": {
3946
"cloud.platform": "aws_kinesis_data_streams",
40-
"cloud.resource_id": f"arn:aws:kinesis:us-east-1:123456789012:stream/{TEST_STREAM}",
47+
"cloud.resource_id": f"arn:aws:kinesis:us-east-1:{AWS_ACCOUNT_ID}:stream/{TEST_STREAM}",
4148
},
4249
}
4350

44-
AWS_ACCESS_KEY_ID = "AAAAAAAAAAAACCESSKEY"
45-
AWS_SECRET_ACCESS_KEY = "AAAAAASECRETKEY" # nosec
46-
AWS_REGION = "us-east-1"
47-
4851
_kinesis_scoped_metrics = [
4952
(f"MessageBroker/Kinesis/Stream/Produce/Named/{TEST_STREAM}", 2),
5053
(f"MessageBroker/Kinesis/Stream/Consume/Named/{TEST_STREAM}", 1),
5154
(f"Kinesis/create_stream/{TEST_STREAM}", 1),
52-
(f"Kinesis/list_streams", 1),
55+
("Kinesis/list_streams", 1),
5356
(f"Kinesis/describe_stream/{TEST_STREAM}", 1),
57+
(f"Kinesis/put_resource_policy/{TEST_STREAM}", 2),
5458
(f"Kinesis/get_shard_iterator/{TEST_STREAM}", 1),
5559
(f"Kinesis/delete_stream/{TEST_STREAM}", 1),
56-
(f"External/{URL}/botocore/POST", 3),
60+
(f"External/{KINESIS_URL}/botocore/POST", 3),
61+
(f"External/{KINESIS_CONTROL_URL}/botocore/POST", 3),
62+
(f"External/{KINESIS_DATA_URL}/botocore/POST", 1),
5763
]
5864
if BOTOCORE_VERSION < (1, 29, 0):
5965
_kinesis_scoped_metrics = [
6066
(f"MessageBroker/Kinesis/Stream/Produce/Named/{TEST_STREAM}", 2),
6167
(f"Kinesis/create_stream/{TEST_STREAM}", 1),
62-
(f"Kinesis/list_streams", 1),
68+
("Kinesis/list_streams", 1),
6369
(f"Kinesis/describe_stream/{TEST_STREAM}", 1),
6470
(f"Kinesis/get_shard_iterator/{TEST_STREAM}", 1),
6571
(f"Kinesis/delete_stream/{TEST_STREAM}", 1),
66-
(f"External/{URL}/botocore/POST", 5),
72+
(f"External/{KINESIS_URL}/botocore/POST", 5),
6773
]
6874

6975
_kinesis_rollup_metrics = [
7076
(f"MessageBroker/Kinesis/Stream/Produce/Named/{TEST_STREAM}", 2),
7177
(f"MessageBroker/Kinesis/Stream/Consume/Named/{TEST_STREAM}", 1),
7278
(f"Kinesis/create_stream/{TEST_STREAM}", 1),
73-
(f"Kinesis/list_streams", 1),
79+
("Kinesis/list_streams", 1),
7480
(f"Kinesis/describe_stream/{TEST_STREAM}", 1),
81+
(f"Kinesis/put_resource_policy/{TEST_STREAM}", 2),
7582
(f"Kinesis/get_shard_iterator/{TEST_STREAM}", 1),
7683
(f"Kinesis/delete_stream/{TEST_STREAM}", 1),
77-
("External/all", 5),
78-
("External/allOther", 5),
79-
(f"External/{URL}/all", 3),
80-
(f"External/{URL}/botocore/POST", 3),
84+
("External/all", 7),
85+
("External/allOther", 7),
86+
(f"External/{KINESIS_URL}/all", 3),
87+
(f"External/{KINESIS_URL}/botocore/POST", 3),
88+
(f"External/{KINESIS_CONTROL_URL}/all", 3),
89+
(f"External/{KINESIS_CONTROL_URL}/botocore/POST", 3),
90+
(f"External/{KINESIS_DATA_URL}/all", 1),
91+
(f"External/{KINESIS_DATA_URL}/botocore/POST", 1),
8192
]
8293
if BOTOCORE_VERSION < (1, 29, 0):
8394
_kinesis_rollup_metrics = [
8495
(f"MessageBroker/Kinesis/Stream/Produce/Named/{TEST_STREAM}", 2),
8596
(f"Kinesis/create_stream/{TEST_STREAM}", 1),
86-
(f"Kinesis/list_streams", 1),
97+
("Kinesis/list_streams", 1),
8798
(f"Kinesis/describe_stream/{TEST_STREAM}", 1),
8899
(f"Kinesis/get_shard_iterator/{TEST_STREAM}", 1),
89100
(f"Kinesis/delete_stream/{TEST_STREAM}", 1),
90101
("External/all", 5),
91102
("External/allOther", 5),
92-
(f"External/{URL}/all", 5),
93-
(f"External/{URL}/botocore/POST", 5),
103+
(f"External/{KINESIS_URL}/all", 5),
104+
(f"External/{KINESIS_URL}/botocore/POST", 5),
94105
]
95106

96107
_kinesis_scoped_metrics_error = [
@@ -114,7 +125,13 @@ def test_instrumented_kinesis_methods():
114125

115126
ignored_methods = set(
116127
("kinesis", method)
117-
for method in ("generate_presigned_url", "close", "get_waiter", "can_paginate", "get_paginator")
128+
for method in (
129+
"generate_presigned_url",
130+
"close",
131+
"get_waiter",
132+
"can_paginate",
133+
"get_paginator",
134+
)
118135
)
119136
client_methods = inspect.getmembers(client, predicate=inspect.ismethod)
120137
methods = {("kinesis", name) for (name, method) in client_methods if not name.startswith("_")}
@@ -128,7 +145,7 @@ def test_instrumented_kinesis_methods():
128145
@validate_span_events(exact_agents={"aws.operation": "CreateStream"}, count=1)
129146
@validate_span_events(
130147
**EXPECTED_AGENT_ATTRS,
131-
count=6 if BOTOCORE_VERSION < (1, 29, 0) else 7,
148+
count=6 if BOTOCORE_VERSION < (1, 29, 0) else 9,
132149
)
133150
@validate_span_events(exact_agents={"aws.operation": "DeleteStream"}, count=1)
134151
@validate_transaction_metrics(
@@ -147,7 +164,11 @@ def test_kinesis():
147164
region_name=AWS_REGION,
148165
)
149166
# Create stream
150-
resp = client.create_stream(StreamName=TEST_STREAM, ShardCount=123, StreamModeDetails={"StreamMode": "on-demand"})
167+
resp = client.create_stream(
168+
StreamName=TEST_STREAM,
169+
ShardCount=123,
170+
StreamModeDetails={"StreamMode": "on-demand"},
171+
)
151172
assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
152173

153174
# List streams
@@ -160,28 +181,55 @@ def test_kinesis():
160181
Limit=123,
161182
)
162183
assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
163-
ARN = resp["StreamDescription"]["StreamARN"]
184+
STREAM_ARN = resp["StreamDescription"]["StreamARN"]
185+
CONSUMER_ARN = f"{STREAM_ARN}/consumer/my_consumer:123" # Mock ConsumerARN
164186

165187
# StreamARN is not supported in older versions of botocore.
166-
stream_kwargs = {"StreamName": TEST_STREAM} if BOTOCORE_VERSION < (1, 29, 0) else {"StreamARN": ARN}
188+
stream_kwargs = {"StreamName": TEST_STREAM} if BOTOCORE_VERSION < (1, 29, 0) else {"StreamARN": STREAM_ARN}
167189

168190
# Send message
169191
resp = client.put_record(Data=b"foo1", PartitionKey="bar", **stream_kwargs)
170192
assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
171193

172194
# Send messages
173195
resp = client.put_records(
174-
Records=[{"Data": b"foo2", "PartitionKey": "bar"}, {"Data": b"foo3", "PartitionKey": "bar"}], **stream_kwargs
196+
Records=[
197+
{"Data": b"foo2", "PartitionKey": "bar"},
198+
{"Data": b"foo3", "PartitionKey": "bar"},
199+
],
200+
**stream_kwargs,
175201
)
176202
assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
177203

204+
# Shards
178205
shard_iter = client.get_shard_iterator(
179206
ShardId="shardId-000000000000",
180207
ShardIteratorType="AT_SEQUENCE_NUMBER",
181208
StartingSequenceNumber="0",
182209
**stream_kwargs,
183210
)["ShardIterator"]
184211

212+
# TODO: Unfortunately we can't test client.subscribe_to_shard() yet as moto has not implemented it.
213+
# It's the only method that uses ConsumerARN as a parameter name, so extracting that parameter can't be tested.
214+
# ResourceARN, however, can be tested and can be either a StreamARN or ConsumerARN format. We can therefore
215+
# at least cover the parsing of ConsumerARNs for the underlying stream by exercising that.
216+
217+
if BOTOCORE_VERSION >= (1, 29, 0):
218+
# This was only made available in Botocore 1.29.0, no way to test ResourceARN before that
219+
# Use ResourceARN as StreamARN
220+
resp = client.put_resource_policy(
221+
ResourceARN=STREAM_ARN,
222+
Policy="some policy",
223+
)
224+
assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
225+
226+
# Use ResourceARN as ConsumerARN
227+
resp = client.put_resource_policy(
228+
ResourceARN=CONSUMER_ARN,
229+
Policy="some policy",
230+
)
231+
assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
232+
185233
# Receive message
186234
if BOTOCORE_VERSION < (1, 29, 0):
187235
resp = client.get_records(ShardIterator=shard_iter)
@@ -214,7 +262,11 @@ def test_kinesis_error():
214262
region_name=AWS_REGION,
215263
)
216264
# Create stream
217-
resp = client.create_stream(StreamName=TEST_STREAM, ShardCount=123, StreamModeDetails={"StreamMode": "on-demand"})
265+
resp = client.create_stream(
266+
StreamName=TEST_STREAM,
267+
ShardCount=123,
268+
StreamModeDetails={"StreamMode": "on-demand"},
269+
)
218270
assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
219271

220272
# Stream ARN is needed for rest of methods.

0 commit comments

Comments
 (0)