Skip to content

Commit 36b217b

Browse files
author
John Tompkins
authored
Make certain request fields optional to unblock contract testing. (#120)
1 parent 79b67a2 commit 36b217b

File tree

8 files changed

+146
-94
lines changed

8 files changed

+146
-94
lines changed

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def find_version(*file_paths):
3838
include_package_data=True,
3939
zip_safe=True,
4040
python_requires=">=3.6",
41-
install_requires=["cloudformation-cli>=0.1,<0.2", "docker>=3.7,<5"],
41+
install_requires=["cloudformation-cli>=0.1.10,<0.2", "docker>=3.7,<5"],
4242
entry_points={
4343
"rpdk.v1.languages": [
4444
"python37 = rpdk.python.codegen:Python37LanguagePlugin",

src/cloudformation_cli_python_lib/log_delivery.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def setup(
4343
stream_name = f"{request.awsAccountId}-{request.region}"
4444

4545
log_handler = cls._get_existing_logger()
46-
if provider_sess and log_group:
46+
if provider_sess and log_group and request.resourceType:
4747
if log_handler:
4848
# This is a re-used lambda container, log handler is already setup, so
4949
# we just refresh the client with new creds

src/cloudformation_cli_python_lib/metrics.py

+117-57
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,29 @@ def format_dimensions(dimensions: Mapping[str, str]) -> List[Mapping[str, str]]:
1616
return [{"Name": key, "Value": value} for key, value in dimensions.items()]
1717

1818

19-
class MetricPublisher:
20-
def __init__(self, session: SessionProxy, namespace: str) -> None:
21-
self.client = session.client("cloudwatch")
22-
self.namespace = namespace
19+
class MetricsPublisher:
20+
"""A cloudwatch based metric publisher.\
21+
Given a resource type and session, \
22+
this publisher will publish metrics to CloudWatch.\
23+
Can be used with the MetricsPublisherProxy.
24+
25+
Functions:
26+
----------
27+
__init__: Initializes metric publisher with given session and resource type
28+
29+
publish_exception_metric: Publishes an exception based metric
30+
31+
publish_invocation_metric: Publishes a metric related to invocations
32+
33+
publish_duration_metric: Publishes an duration metric
34+
35+
publish_log_delivery_exception_metric: Publishes an log delivery exception metric
36+
"""
37+
38+
def __init__(self, session: SessionProxy, resource_type: str) -> None:
39+
self._client = session.client("cloudwatch")
40+
self._resource_type = resource_type
41+
self._namespace = self._make_namespace(self._resource_type)
2342

2443
def publish_metric( # pylint: disable-msg=too-many-arguments
2544
self,
@@ -30,8 +49,8 @@ def publish_metric( # pylint: disable-msg=too-many-arguments
3049
timestamp: datetime.datetime,
3150
) -> None:
3251
try:
33-
self.client.put_metric_data(
34-
Namespace=self.namespace,
52+
self._client.put_metric_data(
53+
Namespace=self._namespace,
3554
MetricData=[
3655
{
3756
"MetricName": metric_name.name,
@@ -46,84 +65,125 @@ def publish_metric( # pylint: disable-msg=too-many-arguments
4665
except ClientError as e:
4766
LOG.error("An error occurred while publishing metrics: %s", str(e))
4867

49-
50-
class MetricsPublisherProxy:
51-
@staticmethod
52-
def _make_namespace(resource_type: str) -> str:
53-
suffix = resource_type.replace("::", "/")
54-
return f"{METRIC_NAMESPACE_ROOT}/{suffix}"
55-
56-
def __init__(self, resource_type: str) -> None:
57-
self.namespace = self._make_namespace(resource_type)
58-
self.resource_type = resource_type
59-
self._publishers: List[MetricPublisher] = []
60-
61-
def add_metrics_publisher(self, session: Optional[SessionProxy]) -> None:
62-
if session:
63-
self._publishers.append(MetricPublisher(session, self.namespace))
64-
6568
def publish_exception_metric(
6669
self, timestamp: datetime.datetime, action: Action, error: Any
6770
) -> None:
6871
dimensions: Mapping[str, str] = {
6972
"DimensionKeyActionType": action.name,
7073
"DimensionKeyExceptionType": str(type(error)),
71-
"DimensionKeyResourceType": self.resource_type,
74+
"DimensionKeyResourceType": self._resource_type,
7275
}
73-
for publisher in self._publishers:
74-
publisher.publish_metric(
75-
metric_name=MetricTypes.HandlerException,
76-
dimensions=dimensions,
77-
unit=StandardUnit.Count,
78-
value=1.0,
79-
timestamp=timestamp,
80-
)
76+
self.publish_metric(
77+
metric_name=MetricTypes.HandlerException,
78+
dimensions=dimensions,
79+
unit=StandardUnit.Count,
80+
value=1.0,
81+
timestamp=timestamp,
82+
)
8183

8284
def publish_invocation_metric(
8385
self, timestamp: datetime.datetime, action: Action
8486
) -> None:
8587
dimensions = {
8688
"DimensionKeyActionType": action.name,
87-
"DimensionKeyResourceType": self.resource_type,
89+
"DimensionKeyResourceType": self._resource_type,
8890
}
89-
for publisher in self._publishers:
90-
publisher.publish_metric(
91-
metric_name=MetricTypes.HandlerInvocationCount,
92-
dimensions=dimensions,
93-
unit=StandardUnit.Count,
94-
value=1.0,
95-
timestamp=timestamp,
96-
)
91+
self.publish_metric(
92+
metric_name=MetricTypes.HandlerInvocationCount,
93+
dimensions=dimensions,
94+
unit=StandardUnit.Count,
95+
value=1.0,
96+
timestamp=timestamp,
97+
)
9798

9899
def publish_duration_metric(
99100
self, timestamp: datetime.datetime, action: Action, milliseconds: float
100101
) -> None:
101102
dimensions = {
102103
"DimensionKeyActionType": action.name,
103-
"DimensionKeyResourceType": self.resource_type,
104+
"DimensionKeyResourceType": self._resource_type,
104105
}
105-
for publisher in self._publishers:
106-
publisher.publish_metric(
107-
metric_name=MetricTypes.HandlerInvocationDuration,
108-
dimensions=dimensions,
109-
unit=StandardUnit.Milliseconds,
110-
value=milliseconds,
111-
timestamp=timestamp,
112-
)
106+
107+
self.publish_metric(
108+
metric_name=MetricTypes.HandlerInvocationDuration,
109+
dimensions=dimensions,
110+
unit=StandardUnit.Milliseconds,
111+
value=milliseconds,
112+
timestamp=timestamp,
113+
)
113114

114115
def publish_log_delivery_exception_metric(
115116
self, timestamp: datetime.datetime, error: Any
116117
) -> None:
117118
dimensions = {
118119
"DimensionKeyActionType": "ProviderLogDelivery",
119120
"DimensionKeyExceptionType": str(type(error)),
120-
"DimensionKeyResourceType": self.resource_type,
121+
"DimensionKeyResourceType": self._resource_type,
121122
}
123+
self.publish_metric(
124+
metric_name=MetricTypes.HandlerException,
125+
dimensions=dimensions,
126+
unit=StandardUnit.Count,
127+
value=1.0,
128+
timestamp=timestamp,
129+
)
130+
131+
@staticmethod
132+
def _make_namespace(resource_type: str) -> str:
133+
suffix = resource_type.replace("::", "/")
134+
return f"{METRIC_NAMESPACE_ROOT}/{suffix}"
135+
136+
137+
class MetricsPublisherProxy:
138+
"""A proxy for publishing metrics to multiple publishers. \
139+
Iterates over available publishers and publishes.
140+
141+
Functions:
142+
----------
143+
add_metrics_publisher: Adds a metrics publisher to the list of publishers
144+
145+
publish_exception_metric: \
146+
Publishes an exception based metric to the list of publishers
147+
148+
publish_invocation_metric: \
149+
Publishes a metric related to invocations to the list of publishers
150+
151+
publish_duration_metric: Publishes a duration metric to the list of publishers
152+
153+
publish_log_delivery_exception_metric: \
154+
Publishes a log delivery exception metric to the list of publishers
155+
"""
156+
157+
def __init__(self) -> None:
158+
self._publishers: List[MetricsPublisher] = []
159+
160+
def add_metrics_publisher(
161+
self, session: Optional[SessionProxy], type_name: Optional[str]
162+
) -> None:
163+
if session and type_name:
164+
publisher = MetricsPublisher(session, type_name)
165+
self._publishers.append(publisher)
166+
167+
def publish_exception_metric(
168+
self, timestamp: datetime.datetime, action: Action, error: Any
169+
) -> None:
122170
for publisher in self._publishers:
123-
publisher.publish_metric(
124-
metric_name=MetricTypes.HandlerException,
125-
dimensions=dimensions,
126-
unit=StandardUnit.Count,
127-
value=1.0,
128-
timestamp=timestamp,
129-
)
171+
publisher.publish_exception_metric(timestamp, action, error)
172+
173+
def publish_invocation_metric(
174+
self, timestamp: datetime.datetime, action: Action
175+
) -> None:
176+
for publisher in self._publishers:
177+
publisher.publish_invocation_metric(timestamp, action)
178+
179+
def publish_duration_metric(
180+
self, timestamp: datetime.datetime, action: Action, milliseconds: float
181+
) -> None:
182+
for publisher in self._publishers:
183+
publisher.publish_duration_metric(timestamp, action, milliseconds)
184+
185+
def publish_log_delivery_exception_metric(
186+
self, timestamp: datetime.datetime, error: Any
187+
) -> None:
188+
for publisher in self._publishers:
189+
publisher.publish_log_delivery_exception_metric(timestamp, error)

src/cloudformation_cli_python_lib/resource.py

+6-10
Original file line numberDiff line numberDiff line change
@@ -148,12 +148,7 @@ def _parse_request(
148148
except Exception as e: # pylint: disable=broad-except
149149
LOG.exception("Invalid request")
150150
raise InvalidRequest(f"{e} ({type(e).__name__})") from e
151-
return (
152-
(caller_sess, provider_sess),
153-
action,
154-
callback_context,
155-
event,
156-
)
151+
return ((caller_sess, provider_sess), action, callback_context, event)
157152

158153
def _cast_resource_request(
159154
self, request: HandlerRequest
@@ -191,13 +186,14 @@ def print_or_log(message: str) -> None:
191186
try:
192187
sessions, action, callback, event = self._parse_request(event_data)
193188
caller_sess, provider_sess = sessions
194-
ProviderLogHandler.setup(event, provider_sess)
195-
logs_setup = True
196189

197190
request = self._cast_resource_request(event)
198191

199-
metrics = MetricsPublisherProxy(event.resourceType)
200-
metrics.add_metrics_publisher(provider_sess)
192+
metrics = MetricsPublisherProxy()
193+
if event.requestData.providerLogGroupName and provider_sess:
194+
ProviderLogHandler.setup(event, provider_sess)
195+
logs_setup = True
196+
metrics.add_metrics_publisher(provider_sess, event.resourceType)
201197

202198
metrics.publish_invocation_metric(datetime.utcnow(), action)
203199
start_time = datetime.utcnow()

src/cloudformation_cli_python_lib/utils.py

+5-6
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ class Credentials:
4747
# pylint: disable=too-many-instance-attributes
4848
@dataclass
4949
class RequestData:
50-
providerLogGroupName: str
51-
logicalResourceId: str
5250
resourceProperties: Mapping[str, Any]
51+
providerLogGroupName: Optional[str] = None
52+
logicalResourceId: Optional[str] = None
5353
systemTags: Optional[Mapping[str, Any]] = None
5454
stackTags: Optional[Mapping[str, Any]] = None
5555
# platform credentials aren't really optional, but this is used to
@@ -86,13 +86,12 @@ class HandlerRequest:
8686
bearerToken: str
8787
region: str
8888
responseEndpoint: str
89-
resourceType: str
90-
resourceTypeVersion: str
9189
requestData: RequestData
92-
stackId: str
90+
stackId: Optional[str] = None
91+
resourceType: Optional[str] = None
92+
resourceTypeVersion: Optional[str] = None
9393
callbackContext: Optional[MutableMapping[str, Any]] = None
9494
nextToken: Optional[str] = None
95-
requestContext: MutableMapping[str, Any] = field(default_factory=dict)
9695

9796
@classmethod
9897
def deserialize(cls, json_data: MutableMapping[str, Any]) -> "HandlerRequest":

tests/lib/metrics_test.py

+13-13
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@
77
import pytest
88
from cloudformation_cli_python_lib.interface import Action, MetricTypes, StandardUnit
99
from cloudformation_cli_python_lib.metrics import (
10-
MetricPublisher,
10+
MetricsPublisher,
1111
MetricsPublisherProxy,
1212
format_dimensions,
1313
)
1414

1515
from botocore.stub import Stubber # pylint: disable=C0411
1616

1717
RESOURCE_TYPE = "Aa::Bb::Cc"
18-
NAMESPACE = MetricsPublisherProxy._make_namespace( # pylint: disable=protected-access
18+
NAMESPACE = MetricsPublisher._make_namespace( # pylint: disable=protected-access
1919
RESOURCE_TYPE
2020
)
2121

@@ -43,7 +43,7 @@ def test_put_metric_catches_error(mock_session):
4343

4444
mock_session.client.return_value = client
4545

46-
publisher = MetricPublisher(mock_session, NAMESPACE)
46+
publisher = MetricsPublisher(mock_session, NAMESPACE)
4747
dimensions = {
4848
"DimensionKeyActionType": Action.CREATE.name,
4949
"DimensionKeyResourceType": RESOURCE_TYPE,
@@ -73,8 +73,8 @@ def test_put_metric_catches_error(mock_session):
7373

7474
def test_publish_exception_metric(mock_session):
7575
fake_datetime = datetime(2019, 1, 1)
76-
proxy = MetricsPublisherProxy(RESOURCE_TYPE)
77-
proxy.add_metrics_publisher(mock_session)
76+
proxy = MetricsPublisherProxy()
77+
proxy.add_metrics_publisher(mock_session, RESOURCE_TYPE)
7878
proxy.publish_exception_metric(fake_datetime, Action.CREATE, Exception("fake-err"))
7979
expected_calls = [
8080
call.client("cloudwatch"),
@@ -103,8 +103,8 @@ def test_publish_exception_metric(mock_session):
103103

104104
def test_publish_invocation_metric(mock_session):
105105
fake_datetime = datetime(2019, 1, 1)
106-
proxy = MetricsPublisherProxy(RESOURCE_TYPE)
107-
proxy.add_metrics_publisher(mock_session)
106+
proxy = MetricsPublisherProxy()
107+
proxy.add_metrics_publisher(mock_session, RESOURCE_TYPE)
108108
proxy.publish_invocation_metric(fake_datetime, Action.CREATE)
109109

110110
expected_calls = [
@@ -130,8 +130,8 @@ def test_publish_invocation_metric(mock_session):
130130

131131
def test_publish_duration_metric(mock_session):
132132
fake_datetime = datetime(2019, 1, 1)
133-
proxy = MetricsPublisherProxy(RESOURCE_TYPE)
134-
proxy.add_metrics_publisher(mock_session)
133+
proxy = MetricsPublisherProxy()
134+
proxy.add_metrics_publisher(mock_session, RESOURCE_TYPE)
135135
proxy.publish_duration_metric(fake_datetime, Action.CREATE, 100)
136136

137137
expected_calls = [
@@ -157,8 +157,8 @@ def test_publish_duration_metric(mock_session):
157157

158158
def test_publish_log_delivery_exception_metric(mock_session):
159159
fake_datetime = datetime(2019, 1, 1)
160-
proxy = MetricsPublisherProxy(RESOURCE_TYPE)
161-
proxy.add_metrics_publisher(mock_session)
160+
proxy = MetricsPublisherProxy()
161+
proxy.add_metrics_publisher(mock_session, RESOURCE_TYPE)
162162
proxy.publish_log_delivery_exception_metric(fake_datetime, TypeError("test"))
163163

164164
expected_calls = [
@@ -190,6 +190,6 @@ def test_publish_log_delivery_exception_metric(mock_session):
190190

191191

192192
def test_metrics_publisher_proxy_add_metrics_publisher_none_safe():
193-
proxy = MetricsPublisherProxy(RESOURCE_TYPE)
194-
proxy.add_metrics_publisher(None)
193+
proxy = MetricsPublisherProxy()
194+
proxy.add_metrics_publisher(None, None)
195195
assert proxy._publishers == [] # pylint: disable=protected-access

tests/lib/resource_test.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"responseEndpoint": None,
2424
"resourceType": "AWS::Test::TestModel",
2525
"resourceTypeVersion": "1.0",
26-
"requestContext": {},
26+
"callbackContext": {},
2727
"requestData": {
2828
"callerCredentials": {
2929
"accessKeyId": "IASAYK835GAIFHAHEI23",
@@ -145,7 +145,7 @@ def test_entrypoint_non_mutating_action():
145145

146146
def test_entrypoint_with_context():
147147
payload = ENTRYPOINT_PAYLOAD.copy()
148-
payload["requestContext"] = {"a": "b"}
148+
payload["callbackContext"] = {"a": "b"}
149149
resource = Resource(TYPE_NAME, Mock())
150150
event = ProgressEvent(
151151
status=OperationStatus.SUCCESS, message="", callbackContext={"c": "d"}

0 commit comments

Comments
 (0)