Skip to content

Commit 8dfbef9

Browse files
committed
refactor: use native python validation over JSON Schema
1 parent 073254c commit 8dfbef9

File tree

6 files changed

+41
-144
lines changed

6 files changed

+41
-144
lines changed

aws_lambda_powertools/metrics/base.py

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,12 @@
99

1010
from ..shared import constants
1111
from ..shared.functions import resolve_env_var_choice
12-
from ..shared.lazy_import import LazyLoader
1312
from .exceptions import MetricUnitError, MetricValueError, SchemaValidationError
14-
from .schema import CLOUDWATCH_EMF_SCHEMA
1513

16-
fastjsonschema = LazyLoader("fastjsonschema", globals(), "fastjsonschema")
1714
logger = logging.getLogger(__name__)
1815

1916
MAX_METRICS = 100
17+
MAX_DIMENSIONS = 9
2018

2119

2220
class MetricUnit(Enum):
@@ -138,9 +136,7 @@ def add_metric(self, name: str, unit: Union[MetricUnit, str], value: float):
138136
# since we could have more than 100 metrics
139137
self.metric_set.clear()
140138

141-
def serialize_metric_set(
142-
self, metrics: Dict = None, dimensions: Dict = None, metadata: Dict = None, validate_metrics: bool = True
143-
) -> Dict:
139+
def serialize_metric_set(self, metrics: Dict = None, dimensions: Dict = None, metadata: Dict = None) -> Dict:
144140
"""Serializes metric and dimensions set
145141
146142
Parameters
@@ -151,8 +147,6 @@ def serialize_metric_set(
151147
Dictionary of dimensions to serialize, by default None
152148
metadata: Dict, optional
153149
Dictionary of metadata to serialize, by default None
154-
validate_metrics: bool, optional
155-
Whether to validate metrics against schema
156150
157151
Example
158152
-------
@@ -184,6 +178,12 @@ def serialize_metric_set(
184178
if self.service and not self.dimension_set.get("service"):
185179
self.dimension_set["service"] = self.service
186180

181+
if len(metrics) == 0:
182+
raise SchemaValidationError("Must contain at least one metric.")
183+
184+
if self.namespace is None:
185+
raise SchemaValidationError("Must contain a metric namespace.")
186+
187187
logger.debug({"details": "Serializing metrics", "metrics": metrics, "dimensions": dimensions})
188188

189189
metric_names_and_units: List[Dict[str, str]] = [] # [ { "Name": "metric_name", "Unit": "Count" } ]
@@ -213,20 +213,8 @@ def serialize_metric_set(
213213
**metric_names_and_values, # "single_metric": 1.0
214214
}
215215

216-
if validate_metrics:
217-
self._validate_metrics(metrics=embedded_metrics_object)
218-
219216
return embedded_metrics_object
220217

221-
@staticmethod
222-
def _validate_metrics(metrics: Dict, schema: Dict = CLOUDWATCH_EMF_SCHEMA):
223-
try:
224-
logger.debug("Validating serialized metrics against CloudWatch EMF schema")
225-
fastjsonschema.validate(definition=schema, data=metrics)
226-
except fastjsonschema.JsonSchemaException as e:
227-
message = f"Invalid format. Error: {e.message}, Invalid item: {e.name}" # noqa: B306, E501
228-
raise SchemaValidationError(message)
229-
230218
def add_dimension(self, name: str, value: str):
231219
"""Adds given dimension to all metrics
232220
@@ -244,7 +232,10 @@ def add_dimension(self, name: str, value: str):
244232
Dimension value
245233
"""
246234
logger.debug(f"Adding dimension: {name}:{value}")
247-
235+
if len(self.dimension_set) == 9:
236+
raise SchemaValidationError(
237+
f"Maximum number of dimensions exceeded ({MAX_DIMENSIONS}): Unable to add dimension {name}."
238+
)
248239
# Cast value to str according to EMF spec
249240
# Majority of values are expected to be string already, so
250241
# checking before casting improves performance in most cases
@@ -305,7 +296,7 @@ def __extract_metric_unit_value(self, unit: Union[str, MetricUnit]) -> str:
305296
if unit in self._metric_unit_options:
306297
unit = MetricUnit[unit].value
307298

308-
if unit not in self._metric_units: # str correta
299+
if unit not in self._metric_units:
309300
raise MetricUnitError(
310301
f"Invalid metric unit '{unit}', expected either option: {self._metric_unit_options}"
311302
)

aws_lambda_powertools/metrics/metric.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def add_metric(self, name: str, unit: MetricUnit, value: float):
6161

6262

6363
@contextmanager
64-
def single_metric(name: str, unit: MetricUnit, value: float, namespace: str = None, validate_metrics: bool = True):
64+
def single_metric(name: str, unit: MetricUnit, value: float, namespace: str = None):
6565
"""Context manager to simplify creation of a single metric
6666
6767
Example
@@ -94,8 +94,6 @@ def single_metric(name: str, unit: MetricUnit, value: float, namespace: str = No
9494
Metric value
9595
namespace: str
9696
Namespace for metrics
97-
validate_metrics: bool, optional
98-
Whether to validate metrics against schema, by default True
9997
10098
Yields
10199
-------
@@ -116,6 +114,6 @@ def single_metric(name: str, unit: MetricUnit, value: float, namespace: str = No
116114
metric: SingleMetric = SingleMetric(namespace=namespace)
117115
metric.add_metric(name=name, unit=unit, value=value)
118116
yield metric
119-
metric_set: Dict = metric.serialize_metric_set(validate_metrics=validate_metrics)
117+
metric_set: Dict = metric.serialize_metric_set()
120118
finally:
121-
print(json.dumps(metric_set))
119+
print(json.dumps(metric_set, separators=(",", ":")))

aws_lambda_powertools/metrics/metrics.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,6 @@ def do_something():
6161
service name to be used as metric dimension, by default "service_undefined"
6262
namespace : str
6363
Namespace for metrics
64-
validate_metrics: bool, optional
65-
Whether to validate metrics against schema, by default True
6664
6765
Raises
6866
------
@@ -78,13 +76,12 @@ def do_something():
7876
_dimensions = {}
7977
_metadata = {}
8078

81-
def __init__(self, service: str = None, namespace: str = None, validate_metrics: bool = True):
79+
def __init__(self, service: str = None, namespace: str = None):
8280
self.metric_set = self._metrics
8381
self.dimension_set = self._dimensions
8482
self.service = service
8583
self.namespace = namespace
8684
self.metadata_set = self._metadata
87-
self.validate_metrics = validate_metrics
8885

8986
super().__init__(
9087
metric_set=self.metric_set,
@@ -159,7 +156,7 @@ def decorate(event, context):
159156
else:
160157
metrics = self.serialize_metric_set()
161158
self.clear_metrics()
162-
print(json.dumps(metrics))
159+
print(json.dumps(metrics, separators=(",", ":")))
163160

164161
return response
165162

aws_lambda_powertools/metrics/schema.py

Lines changed: 0 additions & 94 deletions
This file was deleted.

tests/functional/test_metrics.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def metadata() -> Dict[str, str]:
7777

7878

7979
@pytest.fixture
80-
def a_hundred_metrics(namespace=namespace) -> List[Dict[str, str]]:
80+
def a_hundred_metrics() -> List[Dict[str, str]]:
8181
return [{"name": f"metric_{i}", "unit": "Count", "value": 1} for i in range(100)]
8282

8383

@@ -257,7 +257,7 @@ def test_schema_validation_no_namespace(metric, dimension):
257257
# GIVEN we don't add any namespace
258258
# WHEN we attempt to serialize a valid EMF object
259259
# THEN it should fail namespace validation
260-
with pytest.raises(SchemaValidationError, match=".*Namespace must be string"):
260+
with pytest.raises(SchemaValidationError, match="Must contain a metric namespace."):
261261
with single_metric(**metric) as my_metric:
262262
my_metric.add_dimension(**dimension)
263263

@@ -278,7 +278,7 @@ def test_schema_no_metrics(service, namespace):
278278
my_metrics = Metrics(service=service, namespace=namespace)
279279

280280
# THEN it should fail validation and raise SchemaValidationError
281-
with pytest.raises(SchemaValidationError, match=".*Metrics must contain at least 1 items"):
281+
with pytest.raises(SchemaValidationError, match="Must contain at least one metric."):
282282
my_metrics.serialize_metric_set()
283283

284284

@@ -288,7 +288,7 @@ def test_exceed_number_of_dimensions(metric, namespace):
288288

289289
# WHEN we attempt to serialize them into a valid EMF object
290290
# THEN it should fail validation and raise SchemaValidationError
291-
with pytest.raises(SchemaValidationError, match="must contain less than or equal to 9 items"):
291+
with pytest.raises(SchemaValidationError, match="Maximum number of dimensions exceeded.*"):
292292
with single_metric(**metric, namespace=namespace) as my_metric:
293293
for dimension in dimensions:
294294
my_metric.add_dimension(**dimension)
@@ -328,7 +328,7 @@ def lambda_handler(evt, context):
328328

329329
# THEN the raised exception should be SchemaValidationError
330330
# and specifically about the lack of Metrics
331-
with pytest.raises(SchemaValidationError, match=".*Metrics must contain at least 1 items"):
331+
with pytest.raises(SchemaValidationError, match="Must contain at least one metric."):
332332
lambda_handler({}, {})
333333

334334

tests/performance/test_metrics.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import time
23
from contextlib import contextmanager
34
from typing import Dict, Generator
@@ -8,8 +9,8 @@
89
from aws_lambda_powertools.metrics import MetricUnit
910
from aws_lambda_powertools.metrics import metrics as metrics_global
1011

11-
METRICS_VALIDATION_SLA: float = 0.01
12-
METRICS_SERIALIZATION_SLA: float = 0.01
12+
METRICS_VALIDATION_SLA: float = 0.001
13+
METRICS_SERIALIZATION_SLA: float = 0.001
1314

1415

1516
@contextmanager
@@ -45,37 +46,41 @@ def metric() -> Dict[str, str]:
4546
return {"name": "single_metric", "unit": MetricUnit.Count, "value": 1}
4647

4748

48-
def time_large_metric_set_operation(metrics_instance: Metrics, validate_metrics: bool = True) -> float:
49+
def add_max_metrics_before_serialization(metrics_instance: Metrics):
4950
metrics_instance.add_dimension(name="test_dimension", value="test")
5051

5152
for i in range(99):
5253
metrics_instance.add_metric(name=f"metric_{i}", unit="Count", value=1)
5354

54-
with timing() as t:
55-
metrics_instance.serialize_metric_set(validate_metrics=validate_metrics)
56-
57-
return t()
58-
5955

6056
@pytest.mark.perf
61-
def test_metrics_validation_sla(namespace):
57+
def test_metrics_large_operation_without_json_serialization_sla(namespace):
6258
# GIVEN Metrics is initialized
6359
my_metrics = Metrics(namespace=namespace)
60+
6461
# WHEN we add and serialize 99 metrics
65-
elapsed = time_large_metric_set_operation(metrics_instance=my_metrics)
62+
with timing() as t:
63+
add_max_metrics_before_serialization(metrics_instance=my_metrics)
64+
my_metrics.serialize_metric_set()
6665

6766
# THEN completion time should be below our validation SLA
67+
elapsed = t()
6868
if elapsed > METRICS_VALIDATION_SLA:
6969
pytest.fail(f"Metric validation should be below {METRICS_VALIDATION_SLA}s: {elapsed}")
7070

7171

7272
@pytest.mark.perf
73-
def test_metrics_serialization_sla(namespace):
73+
def test_metrics_large_operation_and_json_serialization_sla(namespace):
7474
# GIVEN Metrics is initialized with validation disabled
75-
my_metrics = Metrics(namespace=namespace, validate_metrics=False)
75+
my_metrics = Metrics(namespace=namespace)
76+
7677
# WHEN we add and serialize 99 metrics
77-
elapsed = time_large_metric_set_operation(metrics_instance=my_metrics, validate_metrics=False)
78+
with timing() as t:
79+
add_max_metrics_before_serialization(metrics_instance=my_metrics)
80+
metrics = my_metrics.serialize_metric_set()
81+
print(json.dumps(metrics, separators=(",", ":")))
7882

7983
# THEN completion time should be below our serialization SLA
84+
elapsed = t()
8085
if elapsed > METRICS_SERIALIZATION_SLA:
8186
pytest.fail(f"Metric serialization should be below {METRICS_SERIALIZATION_SLA}s: {elapsed}")

0 commit comments

Comments
 (0)