Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-prometheus-client for
openSUSE:Factory checked in at 2021-11-17 01:14:16
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-prometheus-client (Old)
and /work/SRC/openSUSE:Factory/.python-prometheus-client.new.1890 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-prometheus-client"
Wed Nov 17 01:14:16 2021 rev:2 rq:931776 version:0.12.0
Changes:
--------
---
/work/SRC/openSUSE:Factory/python-prometheus-client/python-prometheus-client.changes
2021-08-28 22:29:11.461979249 +0200
+++
/work/SRC/openSUSE:Factory/.python-prometheus-client.new.1890/python-prometheus-client.changes
2021-11-17 01:15:26.338191206 +0100
@@ -1,0 +2,11 @@
+Sat Nov 13 22:50:42 UTC 2021 - Michael Str??der <[email protected]>
+
+- Update to upstream 0.12.0 release
+ * [FEATURE] Exemplar support (excludes multiprocess) #669
+ * [ENHANCEMENT] Add support for Python 3.10 #706
+ * [ENHANCEMENT] Restricted Registry will handle metrics added after
restricting #675, #680
+ * [ENHANCEMENT] Raise a more helpful error if a metric is not observable #666
+ * [BUGFIX] Fix instance_ip_grouping_key not working on MacOS #687
+ * [BUGFIX] Fix assertion error from favicion.ico with Python 2.7 #715
+
+-------------------------------------------------------------------
Old:
----
v0.11.0.tar.gz
New:
----
v0.12.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-prometheus-client.spec ++++++
--- /var/tmp/diff_new_pack.NsPkfV/_old 2021-11-17 01:15:26.982191237 +0100
+++ /var/tmp/diff_new_pack.NsPkfV/_new 2021-11-17 01:15:26.982191237 +0100
@@ -19,7 +19,7 @@
%{?!python_module:%define python_module() python-%{**} python3-%{**}}
%bcond_without python2
Name: python-prometheus-client
-Version: 0.11.0
+Version: 0.12.0
Release: 0
Summary: Python client for the Prometheus monitoring system
License: Apache-2.0
++++++ v0.11.0.tar.gz -> v0.12.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/client_python-0.11.0/.circleci/config.yml
new/client_python-0.12.0/.circleci/config.yml
--- old/client_python-0.11.0/.circleci/config.yml 2021-06-01
19:19:33.000000000 +0200
+++ new/client_python-0.12.0/.circleci/config.yml 2021-10-29
19:27:14.000000000 +0200
@@ -75,6 +75,7 @@
- "3.7"
- "3.8"
- "3.9"
+ - "3.10"
- test_nooptionals:
matrix:
parameters:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/client_python-0.11.0/README.md
new/client_python-0.12.0/README.md
--- old/client_python-0.11.0/README.md 2021-06-01 19:19:33.000000000 +0200
+++ new/client_python-0.12.0/README.md 2021-10-29 19:27:14.000000000 +0200
@@ -231,6 +231,27 @@
c.labels('post', '/submit')
```
+### Exemplars
+
+Exemplars can be added to counter and histogram metrics. Exemplars can be
+specified by passing a dict of label value pairs to be exposed as the exemplar.
+For example with a counter:
+
+```python
+from prometheus_client import Counter
+c = Counter('my_requests_total', 'HTTP Failures', ['method', 'endpoint'])
+c.labels('get', '/').inc(exemplar={'trace_id': 'abc123'})
+c.labels('post', '/submit').inc(1.0, {'trace_id': 'def456'})
+```
+
+And with a histogram:
+
+```python
+from prometheus_client import Histogram
+h = Histogram('request_latency_seconds', 'Description of histogram')
+h.observe(4.7, {'trace_id': 'abc123'})
+```
+
### Process Collector
The Python client automatically exports metrics about process CPU usage, RAM,
@@ -510,6 +531,7 @@
- Info and Enum metrics do not work
- The pushgateway cannot be used
- Gauges cannot use the `pid` label
+- Exemplars are not supported
There's several steps to getting this working:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/client_python-0.11.0/prometheus_client/exposition.py
new/client_python-0.12.0/prometheus_client/exposition.py
--- old/client_python-0.11.0/prometheus_client/exposition.py 2021-06-01
19:19:33.000000000 +0200
+++ new/client_python-0.12.0/prometheus_client/exposition.py 2021-10-29
19:27:14.000000000 +0200
@@ -34,7 +34,6 @@
CONTENT_TYPE_LATEST = str('text/plain; version=0.0.4; charset=utf-8')
"""Content type of the latest text format"""
PYTHON27_OR_OLDER = sys.version_info < (3, )
-PYTHON26_OR_OLDER = sys.version_info < (2, 7)
PYTHON376_OR_NEWER = sys.version_info > (3, 7, 5)
@@ -115,8 +114,8 @@
params = parse_qs(environ.get('QUERY_STRING', ''))
if environ['PATH_INFO'] == '/favicon.ico':
# Serve empty response for browsers
- status = '200 OK'
- header = ('', '')
+ status = str('200 OK')
+ header = (str(''), str(''))
output = b''
else:
# Bake output
@@ -445,7 +444,7 @@
gateway_url = urlparse(gateway)
# See https://bugs.python.org/issue27657 for details on urlparse in
py>=3.7.6.
if not gateway_url.scheme or (
- (PYTHON376_OR_NEWER or PYTHON26_OR_OLDER)
+ PYTHON376_OR_NEWER
and gateway_url.scheme not in ['http', 'https']
):
gateway = 'http://{0}'.format(gateway)
@@ -481,7 +480,16 @@
def instance_ip_grouping_key():
"""Grouping key with instance set to the IP Address of this host."""
with closing(socket.socket(socket.AF_INET, socket.SOCK_DGRAM)) as s:
- s.connect(('localhost', 0))
+ if sys.platform == 'darwin':
+ # This check is done this way only on MacOS devices
+ # it is done this way because the localhost method does
+ # not work.
+ # This method was adapted from this StackOverflow answer:
+ # https://stackoverflow.com/a/28950776
+ s.connect(('10.255.255.255', 1))
+ else:
+ s.connect(('localhost', 0))
+
return {'instance': s.getsockname()[0]}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/client_python-0.11.0/prometheus_client/metrics.py
new/client_python-0.12.0/prometheus_client/metrics.py
--- old/client_python-0.11.0/prometheus_client/metrics.py 2021-06-01
19:19:33.000000000 +0200
+++ new/client_python-0.12.0/prometheus_client/metrics.py 2021-10-29
19:27:14.000000000 +0200
@@ -10,6 +10,7 @@
RESERVED_METRIC_LABEL_NAME_RE,
)
from .registry import REGISTRY
+from .samples import Exemplar
from .utils import floatToGoString, INF
if sys.version_info > (3,):
@@ -36,18 +37,32 @@
return full_name
+def _validate_labelname(l):
+ if not METRIC_LABEL_NAME_RE.match(l):
+ raise ValueError('Invalid label metric name: ' + l)
+ if RESERVED_METRIC_LABEL_NAME_RE.match(l):
+ raise ValueError('Reserved label metric name: ' + l)
+
+
def _validate_labelnames(cls, labelnames):
labelnames = tuple(labelnames)
for l in labelnames:
- if not METRIC_LABEL_NAME_RE.match(l):
- raise ValueError('Invalid label metric name: ' + l)
- if RESERVED_METRIC_LABEL_NAME_RE.match(l):
- raise ValueError('Reserved label metric name: ' + l)
+ _validate_labelname(l)
if l in cls._reserved_labelnames:
raise ValueError('Reserved label metric name: ' + l)
return labelnames
+def _validate_exemplar(exemplar):
+ runes = 0
+ for k, v in exemplar.items():
+ _validate_labelname(k)
+ runes += len(k)
+ runes += len(v)
+ if runes > 128:
+ raise ValueError('Exemplar labels have %d UTF-8 characters, exceeding
the limit of 128')
+
+
class MetricWrapperBase(object):
_type = None
_reserved_labelnames = ()
@@ -76,8 +91,8 @@
def collect(self):
metric = self._get_metric()
- for suffix, labels, value in self._samples():
- metric.add_sample(self._name + suffix, labels, value)
+ for suffix, labels, value, timestamp, exemplar in self._samples():
+ metric.add_sample(self._name + suffix, labels, value, timestamp,
exemplar)
return [metric]
def __str__(self):
@@ -202,8 +217,8 @@
metrics = self._metrics.copy()
for labels, metric in metrics.items():
series_labels = list(zip(self._labelnames, labels))
- for suffix, sample_labels, value in metric._samples():
- yield (suffix, dict(series_labels +
list(sample_labels.items())), value)
+ for suffix, sample_labels, value, timestamp, exemplar in
metric._samples():
+ yield (suffix, dict(series_labels +
list(sample_labels.items())), value, timestamp, exemplar)
def _child_samples(self): # pragma: no cover
raise NotImplementedError('_child_samples() must be implemented by %r'
% self)
@@ -256,11 +271,15 @@
self._labelvalues)
self._created = time.time()
- def inc(self, amount=1):
+ def inc(self, amount=1, exemplar=None):
"""Increment counter by the given amount."""
+ self._raise_if_not_observable()
if amount < 0:
raise ValueError('Counters can only be incremented by non-negative
amounts.')
self._value.inc(amount)
+ if exemplar:
+ _validate_exemplar(exemplar)
+ self._value.set_exemplar(Exemplar(exemplar, amount, time.time()))
def count_exceptions(self, exception=Exception):
"""Count exceptions in a block of code or function.
@@ -274,8 +293,8 @@
def _child_samples(self):
return (
- ('_total', {}, self._value.get()),
- ('_created', {}, self._created),
+ ('_total', {}, self._value.get(), None,
self._value.get_exemplar()),
+ ('_created', {}, self._created, None, None),
)
@@ -353,14 +372,17 @@
def inc(self, amount=1):
"""Increment gauge by the given amount."""
+ self._raise_if_not_observable()
self._value.inc(amount)
def dec(self, amount=1):
"""Decrement gauge by the given amount."""
+ self._raise_if_not_observable()
self._value.inc(-amount)
def set(self, value):
"""Set gauge to the given value."""
+ self._raise_if_not_observable()
self._value.set(float(value))
def set_to_current_time(self):
@@ -392,13 +414,15 @@
multiple threads. All other methods of the Gauge become NOOPs.
"""
+ self._raise_if_not_observable()
+
def samples(self):
- return (('', {}, float(f())),)
+ return (('', {}, float(f()), None, None),)
self._child_samples = create_bound_method(samples, self)
def _child_samples(self):
- return (('', {}, self._value.get()),)
+ return (('', {}, self._value.get(), None, None),)
class Summary(MetricWrapperBase):
@@ -450,6 +474,7 @@
https://prometheus.io/docs/practices/histograms/#count-and-sum-of-observations
for details.
"""
+ self._raise_if_not_observable()
self._count.inc(1)
self._sum.inc(amount)
@@ -463,9 +488,10 @@
def _child_samples(self):
return (
- ('_count', {}, self._count.get()),
- ('_sum', {}, self._sum.get()),
- ('_created', {}, self._created))
+ ('_count', {}, self._count.get(), None, None),
+ ('_sum', {}, self._sum.get(), None, None),
+ ('_created', {}, self._created, None, None),
+ )
class Histogram(MetricWrapperBase):
@@ -557,7 +583,7 @@
self._labelvalues + (floatToGoString(b),))
)
- def observe(self, amount):
+ def observe(self, amount, exemplar=None):
"""Observe the given amount.
The amount is usually positive or zero. Negative values are
@@ -567,10 +593,14 @@
https://prometheus.io/docs/practices/histograms/#count-and-sum-of-observations
for details.
"""
+ self._raise_if_not_observable()
self._sum.inc(amount)
for i, bound in enumerate(self._upper_bounds):
if amount <= bound:
self._buckets[i].inc(1)
+ if exemplar:
+ _validate_exemplar(exemplar)
+ self._buckets[i].set_exemplar(Exemplar(exemplar, amount,
time.time()))
break
def time(self):
@@ -585,11 +615,11 @@
acc = 0
for i, bound in enumerate(self._upper_bounds):
acc += self._buckets[i].get()
- samples.append(('_bucket', {'le': floatToGoString(bound)}, acc))
- samples.append(('_count', {}, acc))
+ samples.append(('_bucket', {'le': floatToGoString(bound)}, acc,
None, self._buckets[i].get_exemplar()))
+ samples.append(('_count', {}, acc, None, None))
if self._upper_bounds[0] >= 0:
- samples.append(('_sum', {}, self._sum.get()))
- samples.append(('_created', {}, self._created))
+ samples.append(('_sum', {}, self._sum.get(), None, None))
+ samples.append(('_created', {}, self._created, None, None))
return tuple(samples)
@@ -626,7 +656,7 @@
def _child_samples(self):
with self._lock:
- return (('_info', self._value, 1.0,),)
+ return (('_info', self._value, 1.0, None, None),)
class Enum(MetricWrapperBase):
@@ -684,7 +714,7 @@
def _child_samples(self):
with self._lock:
return [
- ('', {self._name: s}, 1 if i == self._value else 0,)
+ ('', {self._name: s}, 1 if i == self._value else 0, None, None)
for i, s
in enumerate(self._states)
]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/client_python-0.11.0/prometheus_client/metrics_core.py
new/client_python-0.12.0/prometheus_client/metrics_core.py
--- old/client_python-0.11.0/prometheus_client/metrics_core.py 2021-06-01
19:19:33.000000000 +0200
+++ new/client_python-0.12.0/prometheus_client/metrics_core.py 2021-10-29
19:27:14.000000000 +0200
@@ -58,6 +58,15 @@
self.samples,
)
+ def _restricted_metric(self, names):
+ """Build a snapshot of a metric with samples restricted to a given set
of names."""
+ samples = [s for s in self.samples if s[0] in names]
+ if samples:
+ m = Metric(self.name, self.documentation, self.type)
+ m.samples = samples
+ return m
+ return None
+
class UnknownMetricFamily(Metric):
"""A single unknown metric and its samples.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/client_python-0.11.0/prometheus_client/openmetrics/exposition.py
new/client_python-0.12.0/prometheus_client/openmetrics/exposition.py
--- old/client_python-0.11.0/prometheus_client/openmetrics/exposition.py
2021-06-01 19:19:33.000000000 +0200
+++ new/client_python-0.12.0/prometheus_client/openmetrics/exposition.py
2021-10-29 19:27:14.000000000 +0200
@@ -8,6 +8,14 @@
"""Content type of the latest OpenMetrics text format"""
+def _is_valid_exemplar_metric(metric, sample):
+ if metric.type == 'counter' and sample.name.endswith('_total'):
+ return True
+ if metric.type in ('histogram', 'gaugehistogram') and
sample.name.endswith('_bucket'):
+ return True
+ return False
+
+
def generate_latest(registry):
'''Returns the metrics from the registry in latest text format as a
string.'''
output = []
@@ -28,8 +36,8 @@
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))
+ if not _is_valid_exemplar_metric(metric, s):
+ raise ValueError("Metric {0} has exemplars, but is not
a histogram bucket or counter".format(metric.name))
labels = '{{{0}}}'.format(','.join(
['{0}="{1}"'.format(
k, v.replace('\\', r'\\').replace('\n',
r'\n').replace('"', r'\"'))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/client_python-0.11.0/prometheus_client/registry.py
new/client_python-0.12.0/prometheus_client/registry.py
--- old/client_python-0.11.0/prometheus_client/registry.py 2021-06-01
19:19:33.000000000 +0200
+++ new/client_python-0.12.0/prometheus_client/registry.py 2021-10-29
19:27:14.000000000 +0200
@@ -94,28 +94,7 @@
Experimental."""
names = set(names)
- collectors = set()
- metrics = []
- with self._lock:
- if 'target_info' in names and self._target_info:
- metrics.append(self._target_info_metric())
- names.remove('target_info')
- for name in names:
- if name in self._names_to_collectors:
- collectors.add(self._names_to_collectors[name])
- for collector in collectors:
- for metric in collector.collect():
- samples = [s for s in metric.samples if s[0] in names]
- if samples:
- m = Metric(metric.name, metric.documentation, metric.type)
- m.samples = samples
- metrics.append(m)
-
- class RestrictedRegistry(object):
- def collect(self):
- return metrics
-
- return RestrictedRegistry()
+ return RestrictedRegistry(names, self)
def set_target_info(self, labels):
with self._lock:
@@ -150,4 +129,27 @@
return None
+class RestrictedRegistry(object):
+ def __init__(self, names, registry):
+ self._name_set = set(names)
+ self._registry = registry
+
+ def collect(self):
+ collectors = set()
+ target_info_metric = None
+ with self._registry._lock:
+ if 'target_info' in self._name_set and self._registry._target_info:
+ target_info_metric = self._registry._target_info_metric()
+ for name in self._name_set:
+ if name != 'target_info' and name in
self._registry._names_to_collectors:
+ collectors.add(self._registry._names_to_collectors[name])
+ if target_info_metric:
+ yield target_info_metric
+ for collector in collectors:
+ for metric in collector.collect():
+ m = metric._restricted_metric(self._name_set)
+ if m:
+ yield m
+
+
REGISTRY = CollectorRegistry(auto_describe=True)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/client_python-0.11.0/prometheus_client/values.py
new/client_python-0.12.0/prometheus_client/values.py
--- old/client_python-0.11.0/prometheus_client/values.py 2021-06-01
19:19:33.000000000 +0200
+++ new/client_python-0.12.0/prometheus_client/values.py 2021-10-29
19:27:14.000000000 +0200
@@ -14,6 +14,7 @@
def __init__(self, typ, metric_name, name, labelnames, labelvalues,
**kwargs):
self._value = 0.0
+ self._exemplar = None
self._lock = Lock()
def inc(self, amount):
@@ -24,10 +25,18 @@
with self._lock:
self._value = value
+ def set_exemplar(self, exemplar):
+ with self._lock:
+ self._exemplar = exemplar
+
def get(self):
with self._lock:
return self._value
+ def get_exemplar(self):
+ with self._lock:
+ return self._exemplar
+
def MultiProcessValue(process_identifier=os.getpid):
"""Returns a MmapedValue class based on a process_identifier function.
@@ -100,11 +109,19 @@
self._value = value
self._file.write_value(self._key, self._value)
+ def set_exemplar(self, exemplar):
+ # TODO: Implement exemplars for multiprocess mode.
+ return
+
def get(self):
with lock:
self.__check_for_pid_change()
return self._value
+ def get_exemplar(self):
+ # TODO: Implement exemplars for multiprocess mode.
+ return None
+
return MmapedValue
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/client_python-0.11.0/setup.py
new/client_python-0.12.0/setup.py
--- old/client_python-0.11.0/setup.py 2021-06-01 19:19:33.000000000 +0200
+++ new/client_python-0.12.0/setup.py 2021-10-29 19:27:14.000000000 +0200
@@ -1,18 +1,14 @@
from os import path
-import sys
from setuptools import setup
-if sys.version_info >= (2, 7):
- with open(path.join(path.abspath(path.dirname(__file__)), 'README.md')) as
f:
- long_description = f.read()
-else: # Assuming we don't run setup in order to publish under python 2.6
- long_description = "NA"
+with open(path.join(path.abspath(path.dirname(__file__)), 'README.md')) as f:
+ long_description = f.read()
setup(
name="prometheus_client",
- version="0.11.0",
+ version="0.12.0",
author="Brian Brazil",
author_email="[email protected]",
description="Python client for the Prometheus monitoring system.",
@@ -47,6 +43,7 @@
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Topic :: System :: Monitoring",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/client_python-0.11.0/tests/openmetrics/test_exposition.py
new/client_python-0.12.0/tests/openmetrics/test_exposition.py
--- old/client_python-0.11.0/tests/openmetrics/test_exposition.py
2021-06-01 19:19:33.000000000 +0200
+++ new/client_python-0.12.0/tests/openmetrics/test_exposition.py
2021-10-29 19:27:14.000000000 +0200
@@ -1,7 +1,7 @@
from __future__ import unicode_literals
-import sys
import time
+import unittest
from prometheus_client import (
CollectorRegistry, Counter, Enum, Gauge, Histogram, Info, Metric, Summary,
@@ -11,12 +11,6 @@
)
from prometheus_client.openmetrics.exposition import generate_latest
-if sys.version_info < (2, 7):
- # We need the skip decorators from unittest2 on Python 2.6.
- import unittest2 as unittest
-else:
- import unittest
-
class TestGenerateText(unittest.TestCase):
def setUp(self):
@@ -70,7 +64,6 @@
# EOF
""", generate_latest(self.registry))
- @unittest.skipIf(sys.version_info < (2, 7), "Test requires Python 2.7+.")
def test_histogram(self):
s = Histogram('hh', 'A histogram', registry=self.registry)
s.observe(0.05)
@@ -114,29 +107,36 @@
""", generate_latest(self.registry))
def test_histogram_exemplar(self):
- class MyCollector(object):
- def collect(self):
- metric = Metric("hh", "help", 'histogram')
- # This is not sane, but it covers all the cases.
- metric.add_sample("hh_bucket", {"le": "1"}, 0, None,
Exemplar({'a': 'b'}, 0.5))
- metric.add_sample("hh_bucket", {"le": "2"}, 0, None,
Exemplar({'le': '7'}, 0.5, 12))
- metric.add_sample("hh_bucket", {"le": "3"}, 0, 123,
Exemplar({'a': 'b'}, 2.5, 12))
- metric.add_sample("hh_bucket", {"le": "4"}, 0, None,
Exemplar({'a': '\n"\\'}, 3.5))
- metric.add_sample("hh_bucket", {"le": "+Inf"}, 0, None, None)
- yield metric
-
- self.registry.register(MyCollector())
- self.assertEqual(b"""# HELP hh help
+ s = Histogram('hh', 'A histogram', buckets=[1, 2, 3, 4],
registry=self.registry)
+ s.observe(0.5, {'a': 'b'})
+ s.observe(1.5, {'le': '7'})
+ s.observe(2.5, {'a': 'b'})
+ s.observe(3.5, {'a': '\n"\\'})
+ print(generate_latest(self.registry))
+ self.assertEqual(b"""# HELP hh A histogram
# 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"} 1.0 # {a="b"} 0.5 123.456
+hh_bucket{le="2.0"} 2.0 # {le="7"} 1.5 123.456
+hh_bucket{le="3.0"} 3.0 # {a="b"} 2.5 123.456
+hh_bucket{le="4.0"} 4.0 # {a="\\n\\"\\\\"} 3.5 123.456
+hh_bucket{le="+Inf"} 4.0
+hh_count 4.0
+hh_sum 8.0
+hh_created 123.456
+# EOF
+""", generate_latest(self.registry))
+
+ def test_counter_exemplar(self):
+ c = Counter('cc', 'A counter', registry=self.registry)
+ c.inc(exemplar={'a': 'b'})
+ self.assertEqual(b"""# HELP cc A counter
+# TYPE cc counter
+cc_total 1.0 # {a="b"} 1.0 123.456
+cc_created 123.456
# EOF
""", generate_latest(self.registry))
- def test_nonhistogram_exemplar(self):
+ def test_untyped_exemplar(self):
class MyCollector(object):
def collect(self):
metric = Metric("hh", "help", 'untyped')
@@ -148,7 +148,7 @@
with self.assertRaises(ValueError):
generate_latest(self.registry)
- def test_nonhistogram_bucket_exemplar(self):
+ def test_histogram_non_bucket_exemplar(self):
class MyCollector(object):
def collect(self):
metric = Metric("hh", "help", 'histogram')
@@ -157,6 +157,18 @@
yield metric
self.registry.register(MyCollector())
+ with self.assertRaises(ValueError):
+ generate_latest(self.registry)
+
+ def test_counter_non_total_exemplar(self):
+ class MyCollector(object):
+ def collect(self):
+ metric = Metric("cc", "A counter", 'counter')
+ metric.add_sample("cc_total", {}, 1, None, None)
+ metric.add_sample("cc_created", {}, 123.456, None,
Exemplar({'a': 'b'}, 1.0, 123.456))
+ yield metric
+
+ self.registry.register(MyCollector())
with self.assertRaises(ValueError):
generate_latest(self.registry)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/client_python-0.11.0/tests/openmetrics/test_parser.py
new/client_python-0.12.0/tests/openmetrics/test_parser.py
--- old/client_python-0.11.0/tests/openmetrics/test_parser.py 2021-06-01
19:19:33.000000000 +0200
+++ new/client_python-0.12.0/tests/openmetrics/test_parser.py 2021-10-29
19:27:14.000000000 +0200
@@ -2,6 +2,7 @@
import math
import sys
+import unittest
from prometheus_client.core import (
CollectorRegistry, CounterMetricFamily, Exemplar,
@@ -12,12 +13,6 @@
from prometheus_client.openmetrics.exposition import generate_latest
from prometheus_client.openmetrics.parser import text_string_to_metric_families
-if sys.version_info < (2, 7):
- # We need the skip decorators from unittest2 on Python 2.6.
- import unittest2 as unittest
-else:
- import unittest
-
class TestParse(unittest.TestCase):
@@ -549,7 +544,6 @@
mock2.assert_not_called()
mock3.assert_called_once_with('1')
- @unittest.skipIf(sys.version_info < (2, 7), "Test requires Python 2.7+.")
def test_roundtrip(self):
text = """# HELP go_gc_duration_seconds A summary of the GC invocation
durations.
# TYPE go_gc_duration_seconds summary
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/client_python-0.11.0/tests/test_asgi.py
new/client_python-0.12.0/tests/test_asgi.py
--- old/client_python-0.11.0/tests/test_asgi.py 2021-06-01 19:19:33.000000000
+0200
+++ new/client_python-0.12.0/tests/test_asgi.py 2021-10-29 19:27:14.000000000
+0200
@@ -1,16 +1,10 @@
from __future__ import absolute_import, unicode_literals
-import sys
-from unittest import TestCase
+from unittest import skipUnless, TestCase
from prometheus_client import CollectorRegistry, Counter
from prometheus_client.exposition import CONTENT_TYPE_LATEST
-if sys.version_info < (2, 7):
- from unittest2 import skipUnless
-else:
- from unittest import skipUnless
-
try:
# Python >3.5 only
import asyncio
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/client_python-0.11.0/tests/test_core.py
new/client_python-0.12.0/tests/test_core.py
--- old/client_python-0.11.0/tests/test_core.py 2021-06-01 19:19:33.000000000
+0200
+++ new/client_python-0.12.0/tests/test_core.py 2021-10-29 19:27:14.000000000
+0200
@@ -1,6 +1,8 @@
+# coding=utf-8
from __future__ import unicode_literals
from concurrent.futures import ThreadPoolExecutor
+import sys
import time
import pytest
@@ -19,6 +21,21 @@
import unittest
+def assert_not_observable(fn, *args, **kwargs):
+ """
+ Assert that a function call falls with a ValueError exception containing
+ 'missing label values'
+ """
+
+ try:
+ fn(*args, **kwargs)
+ except ValueError as e:
+ assert 'missing label values' in str(e)
+ return
+
+ assert False, "Did not raise a 'missing label values' exception"
+
+
class TestCounter(unittest.TestCase):
def setUp(self):
self.registry = CollectorRegistry()
@@ -30,13 +47,12 @@
self.assertEqual(1, self.registry.get_sample_value('c_total'))
self.counter.inc(7)
self.assertEqual(8, self.registry.get_sample_value('c_total'))
-
+
def test_repr(self):
self.assertEqual(repr(self.counter),
"prometheus_client.metrics.Counter(c)")
def test_negative_increment_raises(self):
self.assertRaises(ValueError, self.counter.inc, -1)
-
def test_function_decorator(self):
@self.counter.count_exceptions(ValueError)
@@ -76,18 +92,43 @@
def test_count_exceptions_not_observable(self):
counter = Counter('counter', 'help', labelnames=('label',),
registry=self.registry)
+ assert_not_observable(counter.count_exceptions)
- try:
- counter.count_exceptions()
- except ValueError as e:
- self.assertIn('missing label values', str(e))
+ def test_inc_not_observable(self):
+ """.inc() must fail if the counter is not observable."""
+
+ counter = Counter('counter', 'help', labelnames=('label',),
registry=self.registry)
+ assert_not_observable(counter.inc)
+
+ def test_exemplar_invalid_label_name(self):
+ self.assertRaises(ValueError, self.counter.inc, exemplar={':o)':
'smile'})
+ self.assertRaises(ValueError, self.counter.inc, exemplar={'1':
'number'})
+
+ def test_exemplar_unicode(self):
+ # 128 characters should not raise, even using characters larger than 1
byte.
+ self.counter.inc(exemplar={
+ 'abcdefghijklmnopqrstuvwxyz': '26+16 characters',
+ 'x123456': '7+15 characters',
+ 'zyxwvutsrqponmlkjihgfedcba': '26+16 characters',
+ 'unicode': '7+15 chars ???',
+ })
+
+ def test_exemplar_too_long(self):
+ # 129 characters should fail.
+ self.assertRaises(ValueError, self.counter.inc, exemplar={
+ 'abcdefghijklmnopqrstuvwxyz': '26+16 characters',
+ 'x1234567': '8+15 characters',
+ 'zyxwvutsrqponmlkjihgfedcba': '26+16 characters',
+ 'y123456': '7+15 characters',
+ })
class TestGauge(unittest.TestCase):
def setUp(self):
self.registry = CollectorRegistry()
self.gauge = Gauge('g', 'help', registry=self.registry)
-
+ self.gauge_with_label = Gauge('g2', 'help', labelnames=("label1",),
registry=self.registry)
+
def test_repr(self):
self.assertEqual(repr(self.gauge),
"prometheus_client.metrics.Gauge(g)")
@@ -100,6 +141,21 @@
self.gauge.set(9)
self.assertEqual(9, self.registry.get_sample_value('g'))
+ def test_inc_not_observable(self):
+ """.inc() must fail if the gauge is not observable."""
+
+ assert_not_observable(self.gauge_with_label.inc)
+
+ def test_dec_not_observable(self):
+ """.dec() must fail if the gauge is not observable."""
+
+ assert_not_observable(self.gauge_with_label.dec)
+
+ def test_set_not_observable(self):
+ """.set() must fail if the gauge is not observable."""
+
+ assert_not_observable(self.gauge_with_label.set, 1)
+
def test_inprogress_function_decorator(self):
self.assertEqual(0, self.registry.get_sample_value('g'))
@@ -127,6 +183,11 @@
x['a'] = None
self.assertEqual(1, self.registry.get_sample_value('g'))
+ def test_set_function_not_observable(self):
+ """.set_function() must fail if the gauge is not observable."""
+
+ assert_not_observable(self.gauge_with_label.set_function, lambda: 1)
+
def test_time_function_decorator(self):
self.assertEqual(0, self.registry.get_sample_value('g'))
@@ -167,25 +228,18 @@
def test_track_in_progress_not_observable(self):
g = Gauge('test', 'help', labelnames=('label',),
registry=self.registry)
-
- try:
- g.track_inprogress()
- except ValueError as e:
- self.assertIn('missing label values', str(e))
+ assert_not_observable(g.track_inprogress)
def test_timer_not_observable(self):
g = Gauge('test', 'help', labelnames=('label',),
registry=self.registry)
-
- try:
- g.time()
- except ValueError as e:
- self.assertIn('missing label values', str(e))
+ assert_not_observable(g.time)
class TestSummary(unittest.TestCase):
def setUp(self):
self.registry = CollectorRegistry()
self.summary = Summary('s', 'help', registry=self.registry)
+ self.summary_with_labels = Summary('s_with_labels', 'help',
labelnames=("label1",), registry=self.registry)
def test_repr(self):
self.assertEqual(repr(self.summary),
"prometheus_client.metrics.Summary(s)")
@@ -197,6 +251,10 @@
self.assertEqual(1, self.registry.get_sample_value('s_count'))
self.assertEqual(10, self.registry.get_sample_value('s_sum'))
+ def test_summary_not_observable(self):
+ """.observe() must fail if the Summary is not observable."""
+ assert_not_observable(self.summary_with_labels.observe, 1)
+
def test_function_decorator(self):
self.assertEqual(0, self.registry.get_sample_value('s_count'))
@@ -267,10 +325,7 @@
def test_timer_not_observable(self):
s = Summary('test', 'help', labelnames=('label',),
registry=self.registry)
- try:
- s.time()
- except ValueError as e:
- self.assertIn('missing label values', str(e))
+ assert_not_observable(s.time)
class TestHistogram(unittest.TestCase):
@@ -315,6 +370,10 @@
self.assertEqual(3, self.registry.get_sample_value('h_count'))
self.assertEqual(float("inf"), self.registry.get_sample_value('h_sum'))
+ def test_histogram_not_observable(self):
+ """.observe() must fail if the Summary is not observable."""
+ assert_not_observable(self.labels.observe, 1)
+
def test_setting_buckets(self):
h = Histogram('h', 'help', registry=None, buckets=[0, 1, 2])
self.assertEqual([0.0, 1.0, 2.0, float("inf")], h._upper_bounds)
@@ -380,6 +439,19 @@
self.assertEqual(1, self.registry.get_sample_value('h_count'))
self.assertEqual(1, self.registry.get_sample_value('h_bucket', {'le':
'+Inf'}))
+ def test_exemplar_invalid_label_name(self):
+ self.assertRaises(ValueError, self.histogram.observe, 3.0,
exemplar={':o)': 'smile'})
+ self.assertRaises(ValueError, self.histogram.observe, 3.0,
exemplar={'1': 'number'})
+
+ def test_exemplar_too_long(self):
+ # 129 characters in total should fail.
+ self.assertRaises(ValueError, self.histogram.observe, 1.0, exemplar={
+ 'abcdefghijklmnopqrstuvwxyz': '26+16 characters',
+ 'x1234567': '8+15 characters',
+ 'zyxwvutsrqponmlkjihgfedcba': '26+16 characters',
+ 'y123456': '7+15 characters',
+ })
+
class TestInfo(unittest.TestCase):
def setUp(self):
@@ -761,7 +833,19 @@
m = Metric('s', 'help', 'summary')
m.samples = [Sample('s_sum', {}, 7)]
- self.assertEqual([m],
registry.restricted_registry(['s_sum']).collect())
+ self.assertEqual([m],
list(registry.restricted_registry(['s_sum']).collect()))
+
+ def test_restricted_registry_adds_new_metrics(self):
+ registry = CollectorRegistry()
+ Counter('c_total', 'help', registry=registry)
+
+ restricted_registry = registry.restricted_registry(['s_sum'])
+
+ Summary('s', 'help', registry=registry).observe(7)
+ m = Metric('s', 'help', 'summary')
+ m.samples = [Sample('s_sum', {}, 7)]
+
+ self.assertEqual([m], list(restricted_registry.collect()))
def test_target_info_injected(self):
registry = CollectorRegistry(target_info={'foo': 'bar'})
@@ -785,11 +869,38 @@
m = Metric('s', 'help', 'summary')
m.samples = [Sample('s_sum', {}, 7)]
- self.assertEqual([m],
registry.restricted_registry(['s_sum']).collect())
+ self.assertEqual([m],
list(registry.restricted_registry(['s_sum']).collect()))
+
+ m = Metric('target', 'Target metadata', 'info')
+ m.samples = [Sample('target_info', {'foo': 'bar'}, 1)]
+ self.assertEqual([m],
list(registry.restricted_registry(['target_info']).collect()))
+
+ @unittest.skipIf(sys.version_info < (3, 3), "Test requires Python 3.3+.")
+ def test_restricted_registry_does_not_call_extra(self):
+ from unittest.mock import MagicMock
+ registry = CollectorRegistry()
+ mock_collector = MagicMock()
+ mock_collector.describe.return_value = [Metric('foo', 'help',
'summary')]
+ registry.register(mock_collector)
+ Summary('s', 'help', registry=registry).observe(7)
+
+ m = Metric('s', 'help', 'summary')
+ m.samples = [Sample('s_sum', {}, 7)]
+ self.assertEqual([m],
list(registry.restricted_registry(['s_sum']).collect()))
+ mock_collector.collect.assert_not_called()
+
+ def test_restricted_registry_does_not_yield_while_locked(self):
+ registry = CollectorRegistry(target_info={'foo': 'bar'})
+ Summary('s', 'help', registry=registry).observe(7)
+
+ m = Metric('s', 'help', 'summary')
+ m.samples = [Sample('s_sum', {}, 7)]
+ self.assertEqual([m],
list(registry.restricted_registry(['s_sum']).collect()))
m = Metric('target', 'Target metadata', 'info')
m.samples = [Sample('target_info', {'foo': 'bar'}, 1)]
- self.assertEqual([m],
registry.restricted_registry(['target_info']).collect())
+ for _ in registry.restricted_registry(['target_info',
's_sum']).collect():
+ self.assertFalse(registry._lock.locked())
if __name__ == '__main__':
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/client_python-0.11.0/tests/test_exposition.py
new/client_python-0.12.0/tests/test_exposition.py
--- old/client_python-0.11.0/tests/test_exposition.py 2021-06-01
19:19:33.000000000 +0200
+++ new/client_python-0.12.0/tests/test_exposition.py 2021-10-29
19:27:14.000000000 +0200
@@ -1,8 +1,8 @@
from __future__ import unicode_literals
-import sys
import threading
import time
+import unittest
import pytest
@@ -17,12 +17,6 @@
passthrough_redirect_handler,
)
-if sys.version_info < (2, 7):
- # We need the skip decorators from unittest2 on Python 2.6.
- import unittest2 as unittest
-else:
- import unittest
-
try:
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
except ImportError:
@@ -98,7 +92,6 @@
ss_created{a="c",b="d"} 123.456
""", generate_latest(self.registry))
- @unittest.skipIf(sys.version_info < (2, 7), "Test requires Python 2.7+.")
def test_histogram(self):
s = Histogram('hh', 'A histogram', registry=self.registry)
s.observe(0.05)
@@ -376,10 +369,6 @@
# ensure the redirect took place at the expected redirect location.
self.assertEqual(self.requests[1][0].path, "/" + self.redirect_flag)
- @unittest.skipIf(
- sys.platform == "darwin",
- "instance_ip_grouping_key() does not work on macOS."
- )
def test_instance_ip_grouping_key(self):
self.assertTrue('' != instance_ip_grouping_key()['instance'])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/client_python-0.11.0/tests/test_gc_collector.py
new/client_python-0.12.0/tests/test_gc_collector.py
--- old/client_python-0.11.0/tests/test_gc_collector.py 2021-06-01
19:19:33.000000000 +0200
+++ new/client_python-0.12.0/tests/test_gc_collector.py 2021-10-29
19:27:14.000000000 +0200
@@ -3,12 +3,7 @@
import gc
import platform
import sys
-
-if sys.version_info < (2, 7):
- # We need the skip decorators from unittest2 on Python 2.6.
- import unittest2 as unittest
-else:
- import unittest
+import unittest
from prometheus_client import CollectorRegistry, GCCollector
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/client_python-0.11.0/tests/test_multiprocess.py
new/client_python-0.12.0/tests/test_multiprocess.py
--- old/client_python-0.11.0/tests/test_multiprocess.py 2021-06-01
19:19:33.000000000 +0200
+++ new/client_python-0.12.0/tests/test_multiprocess.py 2021-10-29
19:27:14.000000000 +0200
@@ -3,8 +3,8 @@
import glob
import os
import shutil
-import sys
import tempfile
+import unittest
import warnings
from prometheus_client import mmap_dict, values
@@ -18,12 +18,6 @@
get_value_class, MultiProcessValue, MutexValue,
)
-if sys.version_info < (2, 7):
- # We need the skip decorators from unittest2 on Python 2.6.
- import unittest2 as unittest
-else:
- import unittest
-
class TestMultiProcessDeprecation(unittest.TestCase):
def setUp(self):
@@ -196,7 +190,6 @@
c3 = Counter('c3', 'c3', registry=None)
self.assertEqual(files(), ['counter_0.db', 'counter_1.db'])
- @unittest.skipIf(sys.version_info < (2, 7), "Test requires Python 2.7+.")
def test_collect(self):
pid = 0
values.ValueClass = MultiProcessValue(lambda: pid)
@@ -257,7 +250,6 @@
self.assertEqual(metrics['h'].samples, expected_histogram)
- @unittest.skipIf(sys.version_info < (2, 7), "Test requires Python 2.7+.")
def test_merge_no_accumulate(self):
pid = 0
values.ValueClass = MultiProcessValue(lambda: pid)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/client_python-0.11.0/tests/test_parser.py
new/client_python-0.12.0/tests/test_parser.py
--- old/client_python-0.11.0/tests/test_parser.py 2021-06-01
19:19:33.000000000 +0200
+++ new/client_python-0.12.0/tests/test_parser.py 2021-10-29
19:27:14.000000000 +0200
@@ -1,7 +1,7 @@
from __future__ import unicode_literals
import math
-import sys
+import unittest
from prometheus_client.core import (
CollectorRegistry, CounterMetricFamily, GaugeMetricFamily,
@@ -10,12 +10,6 @@
from prometheus_client.exposition import generate_latest
from prometheus_client.parser import text_string_to_metric_families
-if sys.version_info < (2, 7):
- # We need the skip decorators from unittest2 on Python 2.6.
- import unittest2 as unittest
-else:
- import unittest
-
class TestParse(unittest.TestCase):
def assertEqualMetrics(self, first, second, msg=None):
@@ -289,7 +283,6 @@
b.add_metric([], 88, timestamp=1234566)
self.assertEqualMetrics([a, b], list(families))
- @unittest.skipIf(sys.version_info < (2, 7), "Test requires Python 2.7+.")
def test_roundtrip(self):
text = """# HELP go_gc_duration_seconds A summary of the GC invocation
durations.
# TYPE go_gc_duration_seconds summary
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/client_python-0.11.0/tests/test_twisted.py
new/client_python-0.12.0/tests/test_twisted.py
--- old/client_python-0.11.0/tests/test_twisted.py 2021-06-01
19:19:33.000000000 +0200
+++ new/client_python-0.12.0/tests/test_twisted.py 2021-10-29
19:27:14.000000000 +0200
@@ -1,14 +1,9 @@
from __future__ import absolute_import, unicode_literals
-import sys
+from unittest import skipUnless
from prometheus_client import CollectorRegistry, Counter, generate_latest
-if sys.version_info < (2, 7):
- from unittest2 import skipUnless
-else:
- from unittest import skipUnless
-
try:
from twisted.internet import reactor
from twisted.trial.unittest import TestCase
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/client_python-0.11.0/tests/test_wsgi.py
new/client_python-0.12.0/tests/test_wsgi.py
--- old/client_python-0.11.0/tests/test_wsgi.py 2021-06-01 19:19:33.000000000
+0200
+++ new/client_python-0.12.0/tests/test_wsgi.py 2021-10-29 19:27:14.000000000
+0200
@@ -22,17 +22,6 @@
self.captured_status = status
self.captured_headers = header
- def assertIn(self, item, iterable):
- try:
- super().assertIn(item, iterable)
- except: # Python < 2.7
- self.assertTrue(
- item in iterable,
- msg="{item} not found in {iterable}".format(
- item=item, iterable=iterable
- )
- )
-
def validate_metrics(self, metric_name, help_text, increments):
"""
WSGI app serves the metrics from the provided registry.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/client_python-0.11.0/tox.ini
new/client_python-0.12.0/tox.ini
--- old/client_python-0.11.0/tox.ini 2021-06-01 19:19:33.000000000 +0200
+++ new/client_python-0.12.0/tox.ini 2021-10-29 19:27:14.000000000 +0200
@@ -1,5 +1,5 @@
[tox]
-envlist =
coverage-clean,py2.7,py3.4,py3.5,py3.6,py3.7,py3.8,py3.9,pypy2.7,pypy3.7,{py2.7,py3.9}-nooptionals,coverage-report,flake8,isort
+envlist =
coverage-clean,py2.7,py3.4,py3.5,py3.6,py3.7,py3.8,py3.9,py3.10,pypy2.7,pypy3.7,{py2.7,py3.9}-nooptionals,coverage-report,flake8,isort
[base]