Joseph Davis 5c6dddfc6e Add the metric name to the Exception message when validation fails
In the general validate_metric case, when possible add the metric
name to the exception message. This makes it much easier to
track down what metric failed.

Also, fixing code where an exception could be thrown but then
immediately ignored and another exception fo the same type
with a different message would be thrown.

Change-Id: I8b8f04bb98a65b6904894dc00af88ea367a350f8
Story: 2004185
Task: 27675
2019-02-28 10:37:10 +00:00

184 lines
6.1 KiB
Python

# (C) Copyright 2016-2017 Hewlett Packard Enterprise Development LP
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import math
import re
import six
import sys
import ujson
# This is used to ensure that metrics with a timestamp older than
# RECENT_POINT_THRESHOLD_DEFAULT seconds (or the value passed in to
# the MetricsAggregator constructor) get discarded rather than being
# input into the incorrect bucket. Currently, the MetricsAggregator
# does not support submitting values for the past, and all values get
# submitted for the timestamp passed into the flush() function.
RECENT_POINT_THRESHOLD_DEFAULT = 3600
VALUE_META_MAX_NUMBER = 16
VALUE_META_VALUE_MAX_LENGTH = 2048
VALUE_META_NAME_MAX_LENGTH = 255
INVALID_CHARS = "<>={},\"\\\\;&"
RESTRICTED_DIMENSION_CHARS = re.compile('[' + INVALID_CHARS + ']')
RESTRICTED_NAME_CHARS = re.compile('[' + INVALID_CHARS + '() ' + ']')
NUMERIC_VALUES = [int, float]
if six.PY2:
# according to PEP537 long was renamed to int in PY3
# need to add long, as possible value, for PY2
NUMERIC_VALUES += [long] # noqa
NUMERIC_VALUES = tuple(NUMERIC_VALUES) # convert to tuple for instance call
class InvalidMetricName(Exception):
pass
class InvalidDimensionKey(Exception):
pass
class InvalidDimensionValue(Exception):
pass
class InvalidValue(Exception):
pass
class InvalidValueMeta(Exception):
pass
class InvalidTimeStamp(Exception):
pass
def validate(metrics):
if isinstance(metrics, list):
for metric in metrics:
validate_metric(metric)
else:
validate_metric(metrics)
def validate_metric(metric):
validate_name(metric['name'])
try:
validate_value(metric['value'])
validate_timestamp(metric['timestamp'])
if "dimensions" in metric:
validate_dimensions(metric['dimensions'])
if "value_meta" in metric:
validate_value_meta(metric['value_meta'])
except Exception as ex:
six.reraise(type(ex),
type(ex)(str(ex) + ": for metric %s" % metric['name']),
sys.exc_info()[2])
def validate_value_meta(value_meta):
if value_meta is None:
return
if len(value_meta) > VALUE_META_MAX_NUMBER:
msg = "Too many valueMeta entries {0}, limit is {1}: valueMeta {2}".\
format(len(value_meta), VALUE_META_MAX_NUMBER, value_meta)
raise InvalidValueMeta(msg)
for key, value in six.iteritems(value_meta):
if not key:
raise InvalidValueMeta("valueMeta name cannot be empty: key={}, "
"value={}".format(key, value))
if len(key) > VALUE_META_NAME_MAX_LENGTH:
msg = "valueMeta name too long: {0} must be {1} characters or " \
"less".format(key, VALUE_META_NAME_MAX_LENGTH)
raise InvalidValueMeta(msg)
try:
value_meta_json = ujson.dumps(value_meta)
except Exception:
raise InvalidValueMeta("Unable to serialize valueMeta into JSON")
if len(value_meta_json) > VALUE_META_VALUE_MAX_LENGTH:
msg = "valueMeta name value combinations must be {0} characters " \
"or less: valueMeta {1}".format(VALUE_META_VALUE_MAX_LENGTH,
value_meta)
raise InvalidValueMeta(msg)
def validate_dimension_key(k):
if not isinstance(k, (str, six.text_type)):
msg = "invalid dimension key type: " \
"{0} is not a string type".format(k)
raise InvalidDimensionKey(msg)
if len(k) > 255 or len(k) < 1:
msg = "invalid length ({0}) for dimension key {1}". \
format(len(k), k)
raise InvalidDimensionKey(msg)
if RESTRICTED_DIMENSION_CHARS.search(k) or re.match('^_', k):
msg = "invalid characters in dimension key {0}". \
format(k)
raise InvalidDimensionKey(msg)
def validate_dimension_value(k, v):
if not isinstance(v, (str, six.text_type)):
msg = "invalid dimension value type: {0} must be a " \
"string (from key {1})".format(v, k)
raise InvalidDimensionValue(msg)
if len(v) > 255 or len(v) < 1:
msg = "invalid length ({0}) for dimension value {1} from key {2}". \
format(len(v), v, k)
raise InvalidDimensionValue(msg)
if RESTRICTED_DIMENSION_CHARS.search(v):
msg = "invalid characters in dimension value {0} from key {1}".format(v, k)
raise InvalidDimensionValue(msg)
def validate_dimensions(dimensions):
for k, v in six.iteritems(dimensions):
validate_dimension_key(k)
validate_dimension_value(k, v)
def validate_name(name):
if not isinstance(name, (str, six.text_type)):
msg = "invalid metric name type: {0} is not a string type ".format(
name)
raise InvalidMetricName(msg)
if len(name) > 255 or len(name) < 1:
msg = "invalid length for metric name: {0}".format(name)
raise InvalidMetricName(msg)
if RESTRICTED_NAME_CHARS.search(name):
msg = "invalid characters in metric name: {0}".format(name)
raise InvalidMetricName(msg)
def validate_value(value):
if not isinstance(value, NUMERIC_VALUES):
msg = "invalid value type: {0} is not a number type for metric".\
format(value)
raise InvalidValue(msg)
if math.isnan(value) or math.isinf(value):
msg = "invalid value: {0} is not a valid value for metric".format(value)
raise InvalidValue(msg)
def validate_timestamp(timestamp):
if not isinstance(timestamp, NUMERIC_VALUES):
msg = "invalid timestamp type: {0} is not a number type for " \
"metric".format(timestamp)
raise InvalidTimeStamp(msg)