diff --git a/prometheus_client/metrics_core.py b/prometheus_client/metrics_core.py index 503d7fc5..ad6b6033 100644 --- a/prometheus_client/metrics_core.py +++ b/prometheus_client/metrics_core.py @@ -257,10 +257,13 @@ def add_metric(self, labels, buckets, gsum_value, timestamp=None): dict(list(zip(self._labelnames, labels)) + [('le', bucket)]), value, timestamp)) # +Inf is last and provides the count value. - self.samples.extend([ + self.samples.append( Sample(self.name + '_gcount', dict(zip(self._labelnames, labels)), buckets[-1][1], timestamp), - Sample(self.name + '_gsum', dict(zip(self._labelnames, labels)), gsum_value, timestamp), - ]) + ) + if gsum_value is not None: + self.samples.append( + Sample(self.name + '_gsum', dict(zip(self._labelnames, labels)), gsum_value, timestamp), + ) class InfoMetricFamily(Metric): diff --git a/prometheus_client/openmetrics/exposition.py b/prometheus_client/openmetrics/exposition.py index 6d5925ed..a269c5c0 100644 --- a/prometheus_client/openmetrics/exposition.py +++ b/prometheus_client/openmetrics/exposition.py @@ -8,6 +8,13 @@ """Content type of the latest OpenMetrics text format""" +def _to_openmetrics_value(value): + # Openmetrics distinguishes integers and floats with different text representations. + if type(value) == int: + return str(value) + return floatToGoString(value) + + def generate_latest(registry): '''Returns the metrics from the registry in latest text format as a string.''' output = [] @@ -37,13 +44,13 @@ def generate_latest(registry): if s.exemplar.timestamp is not None: exemplarstr = ' # {0} {1} {2}'.format( labels, - floatToGoString(s.exemplar.value), + _to_openmetrics_value(s.exemplar.value), s.exemplar.timestamp, ) else: exemplarstr = ' # {0} {1}'.format( labels, - floatToGoString(s.exemplar.value), + _to_openmetrics_value(s.exemplar.value), ) else: exemplarstr = '' @@ -53,7 +60,7 @@ def generate_latest(registry): output.append('{0}{1} {2}{3}{4}\n'.format( s.name, labelstr, - floatToGoString(s.value), + _to_openmetrics_value(s.value), timestamp, exemplarstr, )) diff --git a/prometheus_client/samples.py b/prometheus_client/samples.py index 9ff8ead8..255ddbb5 100644 --- a/prometheus_client/samples.py +++ b/prometheus_client/samples.py @@ -36,8 +36,17 @@ def __gt__(self, other): # Timestamp can be a float containing a unixtime in seconds, # a Timestamp object, or None. # Exemplar can be an Exemplar object, or None. -Sample = namedtuple('Sample', ['name', 'labels', 'value', 'timestamp', 'exemplar']) -Sample.__new__.__defaults__ = (None, None) +sample = namedtuple('Sample', ['name', 'labels', 'value', 'timestamp', 'exemplar']) + + +# Wrap the namedtuple to provide eager type-checking that value is a float. +def Sample(name, labels, value, timestamp=None, exemplar=None): + # Preserve ints, convert anything else to float. + if type(value) != int: + value = float(value) + + return sample(name, labels, value, timestamp, exemplar) + Exemplar = namedtuple('Exemplar', ['labels', 'value', 'timestamp']) Exemplar.__new__.__defaults__ = (None,) diff --git a/tests/openmetrics/test_exposition.py b/tests/openmetrics/test_exposition.py index 05791de1..4c9358de 100644 --- a/tests/openmetrics/test_exposition.py +++ b/tests/openmetrics/test_exposition.py @@ -122,11 +122,11 @@ def collect(self): self.registry.register(MyCollector()) self.assertEqual(b"""# HELP hh help # TYPE hh histogram -hh_bucket{le="1"} 0.0 # {a="b"} 0.5 -hh_bucket{le="2"} 0.0 # {le="7"} 0.5 12 -hh_bucket{le="3"} 0.0 123 # {a="b"} 2.5 12 -hh_bucket{le="4"} 0.0 # {a="\\n\\"\\\\"} 3.5 -hh_bucket{le="+Inf"} 0.0 +hh_bucket{le="1"} 0 # {a="b"} 0.5 +hh_bucket{le="2"} 0 # {le="7"} 0.5 12 +hh_bucket{le="3"} 0 123 # {a="b"} 2.5 12 +hh_bucket{le="4"} 0 # {a="\\n\\"\\\\"} 3.5 +hh_bucket{le="+Inf"} 0 # EOF """, generate_latest(self.registry)) @@ -159,10 +159,10 @@ def test_gaugehistogram(self): GaugeHistogramMetricFamily('gh', 'help', buckets=[('1.0', 4), ('+Inf', (5))], gsum_value=7)) self.assertEqual(b"""# HELP gh help # TYPE gh gaugehistogram -gh_bucket{le="1.0"} 4.0 -gh_bucket{le="+Inf"} 5.0 -gh_gcount 5.0 -gh_gsum 7.0 +gh_bucket{le="1.0"} 4 +gh_bucket{le="+Inf"} 5 +gh_gcount 5 +gh_gsum 7 # EOF """, generate_latest(self.registry)) @@ -171,10 +171,10 @@ def test_gaugehistogram_negative_buckets(self): GaugeHistogramMetricFamily('gh', 'help', buckets=[('-1.0', 4), ('+Inf', (5))], gsum_value=-7)) self.assertEqual(b"""# HELP gh help # TYPE gh gaugehistogram -gh_bucket{le="-1.0"} 4.0 -gh_bucket{le="+Inf"} 5.0 -gh_gcount 5.0 -gh_gsum -7.0 +gh_bucket{le="-1.0"} 4 +gh_bucket{le="+Inf"} 5 +gh_gcount 5 +gh_gsum -7 # EOF """, generate_latest(self.registry)) @@ -192,8 +192,8 @@ def test_enum(self): i.labels('c', 'd').state('bar') self.assertEqual(b"""# HELP ee An enum # TYPE ee stateset -ee{a="c",b="d",ee="foo"} 0.0 -ee{a="c",b="d",ee="bar"} 1.0 +ee{a="c",b="d",ee="foo"} 0 +ee{a="c",b="d",ee="bar"} 1 # EOF """, generate_latest(self.registry)) @@ -250,12 +250,12 @@ def collect(self): self.registry.register(MyCollector()) self.assertEqual(b"""# HELP ts help # TYPE ts unknown -ts{foo="a"} 0.0 123.456 -ts{foo="b"} 0.0 -123.456 -ts{foo="c"} 0.0 123 -ts{foo="d"} 0.0 123.456000000 -ts{foo="e"} 0.0 123.000456000 -ts{foo="f"} 0.0 123.000000456 +ts{foo="a"} 0 123.456 +ts{foo="b"} 0 -123.456 +ts{foo="c"} 0 123 +ts{foo="d"} 0 123.456000000 +ts{foo="e"} 0 123.000456000 +ts{foo="f"} 0 123.000000456 # EOF """, generate_latest(self.registry)) diff --git a/tests/test_exposition.py b/tests/test_exposition.py index 47c200f3..716b86c6 100644 --- a/tests/test_exposition.py +++ b/tests/test_exposition.py @@ -348,17 +348,6 @@ def collect(self): 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, @@ -373,7 +362,12 @@ def _expect_metric_exception(registry, expected_error): 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) + try: + generate_latest(registry) + except error as exception: + # Got a valid error as expected, return quietly + return + raise RuntimeError('Expected exception not raised') @pytest.mark.parametrize('count_value,sum_value,error', [ @@ -389,14 +383,18 @@ def test_basic_metric_families(registry, MetricFamily, value, error): 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) + try: + generate_latest(registry) + except error as exception: + # Got a valid error as expected, return quietly + return + raise RuntimeError('Expected exception not raised') @pytest.mark.parametrize('MetricFamily', [ 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), @@ -408,7 +406,12 @@ def test_summary_metric_family(registry, count_value, sum_value, error): 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) + try: + generate_latest(registry) + except error as exception: + # Got a valid error as expected, return quietly + return + raise RuntimeError('Expected exception not raised') if __name__ == '__main__':