From a3fa45060800f677b530ccd6a20e2f1ca3d07ce7 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Tue, 8 Jan 2019 04:32:25 +0100 Subject: [PATCH 1/4] Added metrics to MetricFamily exceptions to fix #362 Signed-off-by: Rick van Hattem --- prometheus_client/exceptions.py | 35 ++++++++++++++ prometheus_client/exposition.py | 21 +++++---- tests/test_metrics_core.py | 84 +++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 8 deletions(-) create mode 100644 prometheus_client/exceptions.py create mode 100644 tests/test_metrics_core.py diff --git a/prometheus_client/exceptions.py b/prometheus_client/exceptions.py new file mode 100644 index 00000000..857392d3 --- /dev/null +++ b/prometheus_client/exceptions.py @@ -0,0 +1,35 @@ + + +class MetricErrorBase(BaseException): + + def __init__(self, metric, *args): + self.metric = metric + super(MetricErrorBase, self).__init__(*args) + + +class MetricTypeError(MetricErrorBase, TypeError): + pass + + +class MetricValueError(MetricErrorBase, ValueError): + pass + + +class MetricAttributeError(MetricErrorBase, AttributeError): + pass + + +Exceptions = { + TypeError: MetricTypeError, + ValueError: MetricValueError, + AttributeError: MetricAttributeError, +} + + +def from_exception(metric, exception): + # Fetch the exception class from the dictionary + type_ = Exceptions[type(exception)] + # Init the exception and add the metric + instance = type_(*exception.args) + instance.metric = metric + return instance diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index 61996618..fe9f0147 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -13,6 +13,7 @@ from .openmetrics import exposition as openmetrics from .registry import REGISTRY from .utils import floatToGoString +from . import exceptions try: from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer @@ -109,14 +110,18 @@ def sample_line(s): output.append('# TYPE {0} {1}\n'.format(mname, mtype)) om_samples = {} - for s in metric.samples: - for suffix in ['_created', '_gsum', '_gcount']: - if s.name == metric.name + suffix: - # OpenMetrics specific sample, put in a gauge at the end. - om_samples.setdefault(suffix, []).append(sample_line(s)) - break - else: - output.append(sample_line(s)) + try: + for s in metric.samples: + for suffix in ['_created', '_gsum', '_gcount']: + if s.name == metric.name + suffix: + # OpenMetrics specific sample, put in a gauge at the end. + om_samples.setdefault(suffix, []).append(sample_line(s)) + break + else: + output.append(sample_line(s)) + except tuple(exceptions.Exceptions) as exception: + raise exceptions.from_exception(metric, exception) + for suffix, lines in sorted(om_samples.items()): output.append('# TYPE {0}{1} gauge\n'.format(metric.name, suffix)) output.extend(lines) diff --git a/tests/test_metrics_core.py b/tests/test_metrics_core.py new file mode 100644 index 00000000..f1c7882d --- /dev/null +++ b/tests/test_metrics_core.py @@ -0,0 +1,84 @@ +import pytest + +from prometheus_client import core +from prometheus_client import exceptions +from prometheus_client import exposition + + +@pytest.fixture +def registry(): + return core.CollectorRegistry() + + +class Collector: + def __init__(self, metric_family, *values): + self.metric_family = metric_family + self.values = values + + def collect(self): + self.metric_family.add_metric([], *self.values) + return [self.metric_family] + + +def _expect_metric_exception(registry, expected_error): + try: + exposition.generate_latest(registry) + except expected_error as exception: + assert hasattr(exception, 'metric') + # Got a valid error as expected, return quietly + return + + raise RuntimeError('Expected exception not raised') + + +@pytest.mark.parametrize('MetricFamily', [ + core.CounterMetricFamily, + core.GaugeMetricFamily, +]) +@pytest.mark.parametrize('value,error', [ + (None, exceptions.MetricTypeError), + ('', exceptions.MetricValueError), + ('x', exceptions.MetricValueError), + ([], exceptions.MetricTypeError), + ({}, exceptions.MetricTypeError), +]) +def test_basic_metric_families(registry, MetricFamily, value, error): + metric_family = MetricFamily(MetricFamily.__name__, 'help') + registry.register(Collector(metric_family, value)) + _expect_metric_exception(registry, error) + + +@pytest.mark.parametrize('count_value,sum_value,error', [ + (None, 0, exceptions.MetricTypeError), + (0, None, exceptions.MetricTypeError), + ('', 0, exceptions.MetricValueError), + (0, '', exceptions.MetricValueError), + ([], 0, exceptions.MetricTypeError), + (0, [], exceptions.MetricTypeError), + ({}, 0, exceptions.MetricTypeError), + (0, {}, exceptions.MetricTypeError), +]) +def test_summary_metric_family(registry, count_value, sum_value, error): + metric_family = core.SummaryMetricFamily('summary', 'help') + registry.register(Collector(metric_family, count_value, sum_value)) + _expect_metric_exception(registry, error) + + +@pytest.mark.parametrize('MetricFamily', [ + core.HistogramMetricFamily, + core.GaugeHistogramMetricFamily, +]) +@pytest.mark.parametrize('buckets,sum_value,error', [ + ([('spam', 0), ('eggs', 0)], None, exceptions.MetricTypeError), + ([('spam', 0), ('eggs', None)], 0, exceptions.MetricTypeError), + ([('spam', 0), (None, 0)], 0, exceptions.MetricAttributeError), + ([('spam', None), ('eggs', 0)], 0, exceptions.MetricTypeError), + ([(None, 0), ('eggs', 0)], 0, exceptions.MetricAttributeError), + ([('spam', 0), ('eggs', 0)], '', exceptions.MetricValueError), + ([('spam', 0), ('eggs', '')], 0, exceptions.MetricValueError), + ([('spam', ''), ('eggs', 0)], 0, exceptions.MetricValueError), +]) +def test_histogram_metric_families(MetricFamily, registry, buckets, sum_value, error): + metric_family = MetricFamily(MetricFamily.__name__, 'help') + registry.register(Collector(metric_family, buckets, sum_value)) + _expect_metric_exception(registry, error) From 6b4592e693ae23685be08e877eaa5dc1a292078d Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Tue, 8 Jan 2019 15:15:00 +0100 Subject: [PATCH 2/4] Simplified exception system Signed-off-by: Rick van Hattem --- prometheus_client/exceptions.py | 35 ------------------------- prometheus_client/exposition.py | 6 ++--- tests/test_metrics_core.py | 45 ++++++++++++++++----------------- 3 files changed, 25 insertions(+), 61 deletions(-) delete mode 100644 prometheus_client/exceptions.py diff --git a/prometheus_client/exceptions.py b/prometheus_client/exceptions.py deleted file mode 100644 index 857392d3..00000000 --- a/prometheus_client/exceptions.py +++ /dev/null @@ -1,35 +0,0 @@ - - -class MetricErrorBase(BaseException): - - def __init__(self, metric, *args): - self.metric = metric - super(MetricErrorBase, self).__init__(*args) - - -class MetricTypeError(MetricErrorBase, TypeError): - pass - - -class MetricValueError(MetricErrorBase, ValueError): - pass - - -class MetricAttributeError(MetricErrorBase, AttributeError): - pass - - -Exceptions = { - TypeError: MetricTypeError, - ValueError: MetricValueError, - AttributeError: MetricAttributeError, -} - - -def from_exception(metric, exception): - # Fetch the exception class from the dictionary - type_ = Exceptions[type(exception)] - # Init the exception and add the metric - instance = type_(*exception.args) - instance.metric = metric - return instance diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index fe9f0147..efa53a20 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -13,7 +13,6 @@ from .openmetrics import exposition as openmetrics from .registry import REGISTRY from .utils import floatToGoString -from . import exceptions try: from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer @@ -119,8 +118,9 @@ def sample_line(s): break else: output.append(sample_line(s)) - except tuple(exceptions.Exceptions) as exception: - raise exceptions.from_exception(metric, exception) + except Exception as exception: + exception.args = (exception.args or ('',)) + (metric,) + raise for suffix, lines in sorted(om_samples.items()): output.append('# TYPE {0}{1} gauge\n'.format(metric.name, suffix)) diff --git a/tests/test_metrics_core.py b/tests/test_metrics_core.py index f1c7882d..a3c2c9ba 100644 --- a/tests/test_metrics_core.py +++ b/tests/test_metrics_core.py @@ -1,7 +1,6 @@ import pytest from prometheus_client import core -from prometheus_client import exceptions from prometheus_client import exposition @@ -24,7 +23,7 @@ def _expect_metric_exception(registry, expected_error): try: exposition.generate_latest(registry) except expected_error as exception: - assert hasattr(exception, 'metric') + assert isinstance(exception.args[-1], core.Metric) # Got a valid error as expected, return quietly return @@ -36,11 +35,11 @@ def _expect_metric_exception(registry, expected_error): core.GaugeMetricFamily, ]) @pytest.mark.parametrize('value,error', [ - (None, exceptions.MetricTypeError), - ('', exceptions.MetricValueError), - ('x', exceptions.MetricValueError), - ([], exceptions.MetricTypeError), - ({}, exceptions.MetricTypeError), + (None, TypeError), + ('', ValueError), + ('x', ValueError), + ([], TypeError), + ({}, TypeError), ]) def test_basic_metric_families(registry, MetricFamily, value, error): metric_family = MetricFamily(MetricFamily.__name__, 'help') @@ -49,14 +48,14 @@ def test_basic_metric_families(registry, MetricFamily, value, error): @pytest.mark.parametrize('count_value,sum_value,error', [ - (None, 0, exceptions.MetricTypeError), - (0, None, exceptions.MetricTypeError), - ('', 0, exceptions.MetricValueError), - (0, '', exceptions.MetricValueError), - ([], 0, exceptions.MetricTypeError), - (0, [], exceptions.MetricTypeError), - ({}, 0, exceptions.MetricTypeError), - (0, {}, exceptions.MetricTypeError), + (None, 0, TypeError), + (0, None, TypeError), + ('', 0, ValueError), + (0, '', ValueError), + ([], 0, TypeError), + (0, [], TypeError), + ({}, 0, TypeError), + (0, {}, TypeError), ]) def test_summary_metric_family(registry, count_value, sum_value, error): metric_family = core.SummaryMetricFamily('summary', 'help') @@ -69,14 +68,14 @@ def test_summary_metric_family(registry, count_value, sum_value, error): core.GaugeHistogramMetricFamily, ]) @pytest.mark.parametrize('buckets,sum_value,error', [ - ([('spam', 0), ('eggs', 0)], None, exceptions.MetricTypeError), - ([('spam', 0), ('eggs', None)], 0, exceptions.MetricTypeError), - ([('spam', 0), (None, 0)], 0, exceptions.MetricAttributeError), - ([('spam', None), ('eggs', 0)], 0, exceptions.MetricTypeError), - ([(None, 0), ('eggs', 0)], 0, exceptions.MetricAttributeError), - ([('spam', 0), ('eggs', 0)], '', exceptions.MetricValueError), - ([('spam', 0), ('eggs', '')], 0, exceptions.MetricValueError), - ([('spam', ''), ('eggs', 0)], 0, exceptions.MetricValueError), + ([('spam', 0), ('eggs', 0)], None, TypeError), + ([('spam', 0), ('eggs', None)], 0, TypeError), + ([('spam', 0), (None, 0)], 0, AttributeError), + ([('spam', None), ('eggs', 0)], 0, TypeError), + ([(None, 0), ('eggs', 0)], 0, AttributeError), + ([('spam', 0), ('eggs', 0)], '', ValueError), + ([('spam', 0), ('eggs', '')], 0, ValueError), + ([('spam', ''), ('eggs', 0)], 0, ValueError), ]) def test_histogram_metric_families(MetricFamily, registry, buckets, sum_value, error): metric_family = MetricFamily(MetricFamily.__name__, 'help') From 810a7def7caa15461a3e78f7ed13826fd2606ceb Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Tue, 8 Jan 2019 15:51:43 +0100 Subject: [PATCH 3/4] Added extra exception info to openmetrics Signed-off-by: Rick van Hattem --- prometheus_client/exposition.py | 44 +++++----- prometheus_client/openmetrics/exposition.py | 91 +++++++++++---------- 2 files changed, 70 insertions(+), 65 deletions(-) diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index efa53a20..22dea5da 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -87,29 +87,29 @@ def sample_line(s): output = [] for metric in registry.collect(): - mname = metric.name - mtype = metric.type - # Munging from OpenMetrics into Prometheus format. - if mtype == 'counter': - mname = mname + '_total' - elif mtype == 'info': - mname = mname + '_info' - mtype = 'gauge' - elif mtype == 'stateset': - mtype = 'gauge' - elif mtype == 'gaugehistogram': - # A gauge histogram is really a gauge, - # but this captures the strucutre better. - mtype = 'histogram' - elif mtype == 'unknown': - mtype = 'untyped' - - output.append('# HELP {0} {1}\n'.format( - mname, metric.documentation.replace('\\', r'\\').replace('\n', r'\n'))) - output.append('# TYPE {0} {1}\n'.format(mname, mtype)) - - om_samples = {} try: + mname = metric.name + mtype = metric.type + # Munging from OpenMetrics into Prometheus format. + if mtype == 'counter': + mname = mname + '_total' + elif mtype == 'info': + mname = mname + '_info' + mtype = 'gauge' + elif mtype == 'stateset': + mtype = 'gauge' + elif mtype == 'gaugehistogram': + # A gauge histogram is really a gauge, + # but this captures the strucutre better. + mtype = 'histogram' + elif mtype == 'unknown': + mtype = 'untyped' + + output.append('# HELP {0} {1}\n'.format( + mname, metric.documentation.replace('\\', r'\\').replace('\n', r'\n'))) + output.append('# TYPE {0} {1}\n'.format(mname, mtype)) + + om_samples = {} for s in metric.samples: for suffix in ['_created', '_gsum', '_gcount']: if s.name == metric.name + suffix: diff --git a/prometheus_client/openmetrics/exposition.py b/prometheus_client/openmetrics/exposition.py index 74087341..2f39c140 100644 --- a/prometheus_client/openmetrics/exposition.py +++ b/prometheus_client/openmetrics/exposition.py @@ -12,49 +12,54 @@ def generate_latest(registry): '''Returns the metrics from the registry in latest text format as a string.''' output = [] for metric in registry.collect(): - mname = metric.name - output.append('# HELP {0} {1}\n'.format( - mname, metric.documentation.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"'))) - output.append('# TYPE {0} {1}\n'.format(mname, metric.type)) - if metric.unit: - output.append('# UNIT {0} {1}\n'.format(mname, metric.unit)) - for s in metric.samples: - if s.labels: - labelstr = '{{{0}}}'.format(','.join( - ['{0}="{1}"'.format( - k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"')) - for k, v in sorted(s.labels.items())])) - else: - labelstr = '' - if s.exemplar: - if metric.type not in ('histogram', 'gaugehistogram') or not s.name.endswith('_bucket'): - raise ValueError("Metric {0} has exemplars, but is not a histogram bucket".format(metric.name)) - labels = '{{{0}}}'.format(','.join( - ['{0}="{1}"'.format( - k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"')) - for k, v in sorted(s.exemplar.labels.items())])) - if s.exemplar.timestamp is not None: - exemplarstr = ' # {0} {1} {2}'.format( - labels, - floatToGoString(s.exemplar.value), - s.exemplar.timestamp, - ) + try: + mname = metric.name + output.append('# HELP {0} {1}\n'.format( + mname, metric.documentation.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"'))) + output.append('# TYPE {0} {1}\n'.format(mname, metric.type)) + if metric.unit: + output.append('# UNIT {0} {1}\n'.format(mname, metric.unit)) + for s in metric.samples: + if s.labels: + labelstr = '{{{0}}}'.format(','.join( + ['{0}="{1}"'.format( + k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"')) + for k, v in sorted(s.labels.items())])) else: - exemplarstr = ' # {0} {1}'.format( - labels, - floatToGoString(s.exemplar.value), - ) - else: - exemplarstr = '' - timestamp = '' - if s.timestamp is not None: - timestamp = ' {0}'.format(s.timestamp) - output.append('{0}{1} {2}{3}{4}\n'.format( - s.name, - labelstr, - floatToGoString(s.value), - timestamp, - exemplarstr, - )) + labelstr = '' + if s.exemplar: + if metric.type not in ('histogram', 'gaugehistogram') or not s.name.endswith('_bucket'): + raise ValueError("Metric {0} has exemplars, but is not a histogram bucket".format(metric.name)) + labels = '{{{0}}}'.format(','.join( + ['{0}="{1}"'.format( + k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"')) + for k, v in sorted(s.exemplar.labels.items())])) + if s.exemplar.timestamp is not None: + exemplarstr = ' # {0} {1} {2}'.format( + labels, + floatToGoString(s.exemplar.value), + s.exemplar.timestamp, + ) + else: + exemplarstr = ' # {0} {1}'.format( + labels, + floatToGoString(s.exemplar.value), + ) + else: + exemplarstr = '' + timestamp = '' + if s.timestamp is not None: + timestamp = ' {0}'.format(s.timestamp) + output.append('{0}{1} {2}{3}{4}\n'.format( + s.name, + labelstr, + floatToGoString(s.value), + timestamp, + exemplarstr, + )) + except Exception as exception: + exception.args = (exception.args or ('',)) + (metric,) + raise + output.append('# EOF\n') return ''.join(output).encode('utf-8') From 4e0135b66bb04c2743648474e0e782ecc3e4a75f Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Tue, 8 Jan 2019 15:55:06 +0100 Subject: [PATCH 4/4] Moved metric family tests to test_exposition.py Signed-off-by: Rick van Hattem --- tests/test_exposition.py | 83 +++++++++++++++++++++++++++++++++++++- tests/test_metrics_core.py | 83 -------------------------------------- 2 files changed, 82 insertions(+), 84 deletions(-) delete mode 100644 tests/test_metrics_core.py diff --git a/tests/test_exposition.py b/tests/test_exposition.py index 844bd112..10e5d242 100644 --- a/tests/test_exposition.py +++ b/tests/test_exposition.py @@ -3,6 +3,7 @@ import sys import threading import time +import pytest from prometheus_client import ( CollectorRegistry, CONTENT_TYPE_LATEST, Counter, delete_from_gateway, Enum, @@ -11,8 +12,9 @@ ) from prometheus_client.core import GaugeHistogramMetricFamily, Timestamp from prometheus_client.exposition import ( - basic_auth_handler, default_handler, MetricsHandler, + basic_auth_handler, default_handler, MetricsHandler, generate_latest, ) +from prometheus_client import core if sys.version_info < (2, 7): # We need the skip decorators from unittest2 on Python 2.6. @@ -303,5 +305,84 @@ def test_metrics_handler_subclassing(self): self.assertTrue(issubclass(handler, (MetricsHandler, subclass))) +@pytest.fixture +def registry(): + return core.CollectorRegistry() + + +class Collector: + def __init__(self, metric_family, *values): + self.metric_family = metric_family + self.values = values + + def collect(self): + self.metric_family.add_metric([], *self.values) + return [self.metric_family] + + +def _expect_metric_exception(registry, expected_error): + try: + generate_latest(registry) + except expected_error as exception: + assert isinstance(exception.args[-1], core.Metric) + # Got a valid error as expected, return quietly + return + + raise RuntimeError('Expected exception not raised') + + +@pytest.mark.parametrize('MetricFamily', [ + core.CounterMetricFamily, + core.GaugeMetricFamily, +]) +@pytest.mark.parametrize('value,error', [ + (None, TypeError), + ('', ValueError), + ('x', ValueError), + ([], TypeError), + ({}, TypeError), +]) +def test_basic_metric_families(registry, MetricFamily, value, error): + metric_family = MetricFamily(MetricFamily.__name__, 'help') + registry.register(Collector(metric_family, value)) + _expect_metric_exception(registry, error) + + +@pytest.mark.parametrize('count_value,sum_value,error', [ + (None, 0, TypeError), + (0, None, TypeError), + ('', 0, ValueError), + (0, '', ValueError), + ([], 0, TypeError), + (0, [], TypeError), + ({}, 0, TypeError), + (0, {}, TypeError), +]) +def test_summary_metric_family(registry, count_value, sum_value, error): + metric_family = core.SummaryMetricFamily('summary', 'help') + registry.register(Collector(metric_family, count_value, sum_value)) + _expect_metric_exception(registry, error) + + +@pytest.mark.parametrize('MetricFamily', [ + core.HistogramMetricFamily, + core.GaugeHistogramMetricFamily, +]) +@pytest.mark.parametrize('buckets,sum_value,error', [ + ([('spam', 0), ('eggs', 0)], None, TypeError), + ([('spam', 0), ('eggs', None)], 0, TypeError), + ([('spam', 0), (None, 0)], 0, AttributeError), + ([('spam', None), ('eggs', 0)], 0, TypeError), + ([(None, 0), ('eggs', 0)], 0, AttributeError), + ([('spam', 0), ('eggs', 0)], '', ValueError), + ([('spam', 0), ('eggs', '')], 0, ValueError), + ([('spam', ''), ('eggs', 0)], 0, ValueError), +]) +def test_histogram_metric_families(MetricFamily, registry, buckets, sum_value, error): + metric_family = MetricFamily(MetricFamily.__name__, 'help') + registry.register(Collector(metric_family, buckets, sum_value)) + _expect_metric_exception(registry, error) + + if __name__ == '__main__': unittest.main() diff --git a/tests/test_metrics_core.py b/tests/test_metrics_core.py deleted file mode 100644 index a3c2c9ba..00000000 --- a/tests/test_metrics_core.py +++ /dev/null @@ -1,83 +0,0 @@ -import pytest - -from prometheus_client import core -from prometheus_client import exposition - - -@pytest.fixture -def registry(): - return core.CollectorRegistry() - - -class Collector: - def __init__(self, metric_family, *values): - self.metric_family = metric_family - self.values = values - - def collect(self): - self.metric_family.add_metric([], *self.values) - return [self.metric_family] - - -def _expect_metric_exception(registry, expected_error): - try: - exposition.generate_latest(registry) - except expected_error as exception: - assert isinstance(exception.args[-1], core.Metric) - # Got a valid error as expected, return quietly - return - - raise RuntimeError('Expected exception not raised') - - -@pytest.mark.parametrize('MetricFamily', [ - core.CounterMetricFamily, - core.GaugeMetricFamily, -]) -@pytest.mark.parametrize('value,error', [ - (None, TypeError), - ('', ValueError), - ('x', ValueError), - ([], TypeError), - ({}, TypeError), -]) -def test_basic_metric_families(registry, MetricFamily, value, error): - metric_family = MetricFamily(MetricFamily.__name__, 'help') - registry.register(Collector(metric_family, value)) - _expect_metric_exception(registry, error) - - -@pytest.mark.parametrize('count_value,sum_value,error', [ - (None, 0, TypeError), - (0, None, TypeError), - ('', 0, ValueError), - (0, '', ValueError), - ([], 0, TypeError), - (0, [], TypeError), - ({}, 0, TypeError), - (0, {}, TypeError), -]) -def test_summary_metric_family(registry, count_value, sum_value, error): - metric_family = core.SummaryMetricFamily('summary', 'help') - registry.register(Collector(metric_family, count_value, sum_value)) - _expect_metric_exception(registry, error) - - -@pytest.mark.parametrize('MetricFamily', [ - core.HistogramMetricFamily, - core.GaugeHistogramMetricFamily, -]) -@pytest.mark.parametrize('buckets,sum_value,error', [ - ([('spam', 0), ('eggs', 0)], None, TypeError), - ([('spam', 0), ('eggs', None)], 0, TypeError), - ([('spam', 0), (None, 0)], 0, AttributeError), - ([('spam', None), ('eggs', 0)], 0, TypeError), - ([(None, 0), ('eggs', 0)], 0, AttributeError), - ([('spam', 0), ('eggs', 0)], '', ValueError), - ([('spam', 0), ('eggs', '')], 0, ValueError), - ([('spam', ''), ('eggs', 0)], 0, ValueError), -]) -def test_histogram_metric_families(MetricFamily, registry, buckets, sum_value, error): - metric_family = MetricFamily(MetricFamily.__name__, 'help') - registry.register(Collector(metric_family, buckets, sum_value)) - _expect_metric_exception(registry, error)