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-04-10 15:27:06 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-prometheus_client (Old) and /work/SRC/openSUSE:Factory/.python-prometheus_client.new.2401 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-prometheus_client" Sat Apr 10 15:27:06 2021 rev:12 rq:883902 version:0.10.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-prometheus_client/python-prometheus_client.changes 2020-11-23 10:45:25.374298532 +0100 +++ /work/SRC/openSUSE:Factory/.python-prometheus_client.new.2401/python-prometheus_client.changes 2021-04-10 15:28:08.426432318 +0200 @@ -1,0 +2,19 @@ +Thu Apr 8 17:16:38 UTC 2021 - Michael Str??der <mich...@stroeder.com> + +- Update to upstream 0.10.1 release + * [BUGFIX] Support lowercase prometheus_multiproc_dir environment + variable in mark_process_dead. + +------------------------------------------------------------------- +Mon Apr 5 23:44:20 UTC 2021 - Michael Str??der <mich...@stroeder.com> + +- Update to upstream 0.10.0 release + * [CHANGE] Python 2.6 is no longer supported. #592 + * [CHANGE] The prometheus_multiproc_dir environment variable is + deprecated in favor of PROMETHEUS_MULTIPROC_DIR. #624 + * [FEATURE] Follow redirects when pushing to Pushgateway using + passthrough_redirect_handler. #622 + * [FEATURE] Metrics support a clear() method to remove all children. #642 + * [ENHANCEMENT] Tag support in GraphiteBridge. #618 + +------------------------------------------------------------------- Old: ---- v0.9.0.tar.gz New: ---- v0.10.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-prometheus_client.spec ++++++ --- /var/tmp/diff_new_pack.6LgICQ/_old 2021-04-10 15:28:08.782432736 +0200 +++ /var/tmp/diff_new_pack.6LgICQ/_new 2021-04-10 15:28:08.786432741 +0200 @@ -1,7 +1,7 @@ # # spec file for package python-prometheus_client # -# Copyright (c) 2020 SUSE LLC +# Copyright (c) 2021 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -19,7 +19,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} %bcond_without python2 Name: python-prometheus_client -Version: 0.9.0 +Version: 0.10.1 Release: 0 Summary: Python client for the Prometheus monitoring system License: Apache-2.0 ++++++ v0.9.0.tar.gz -> v0.10.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/client_python-0.9.0/.circleci/config.yml new/client_python-0.10.1/.circleci/config.yml --- old/client_python-0.9.0/.circleci/config.yml 1970-01-01 01:00:00.000000000 +0100 +++ new/client_python-0.10.1/.circleci/config.yml 2021-04-08 18:30:13.000000000 +0200 @@ -0,0 +1,89 @@ +--- +version: 2.1 + +executors: + python: + docker: + - image: cimg/python:3.9 + +jobs: + flake8_lint: + executor: python + steps: + - checkout + - run: pip install tox + - run: tox -e flake8 + isort_lint: + executor: python + steps: + - checkout + - run: pip install tox + - run: tox -e isort + test: + parameters: + python: + type: string + docker: + - image: circleci/python:<< parameters.python >> + environment: + TOXENV: "py<< parameters.python >>" + steps: + - checkout + - run: echo 'export PATH=$HOME/.local/bin:$PATH' >> $BASH_ENV + - run: pip install --user tox + - run: tox + test_nooptionals: + parameters: + python: + type: string + docker: + - image: cimg/python:<< parameters.python >> + environment: + TOXENV: "py<< parameters.python >>-nooptionals" + steps: + - checkout + - run: pip install tox + - run: tox + test_pypy: + parameters: + python: + type: string + docker: + - image: pypy:<< parameters.python >> + environment: + TOXENV: "pypy<< parameters.python >>" + steps: + - checkout + - run: pip install tox + - run: tox + + +workflows: + version: 2 + client_python: + jobs: + - flake8_lint + - isort_lint + - test: + matrix: + parameters: + python: + - "2.7" + - "3.4" + - "3.5" + - "3.6" + - "3.7" + - "3.8" + - "3.9" + - test_nooptionals: + matrix: + parameters: + python: + - "2.7" + - "3.9" + - test_pypy: + matrix: + parameters: + python: + - "2.7" + - "3.7" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/client_python-0.9.0/.travis.yml new/client_python-0.10.1/.travis.yml --- old/client_python-0.9.0/.travis.yml 2020-11-16 14:26:28.000000000 +0100 +++ new/client_python-0.10.1/.travis.yml 1970-01-01 01:00:00.000000000 +0100 @@ -1,64 +0,0 @@ -os: linux -dist: xenial -cache: - directories: - - $HOME/.cache/pip - -language: python - -jobs: - include: - - stage: lint - name: flake8_lint - script: - - tox -e flake8 - - stage: lint - name: isort_lint - script: - - tox -e isort - - stage: test - python: "2.6" - env: TOXENV=py26 - dist: trusty - - stage: test - python: "2.7" - env: TOXENV=py27 - - stage: test - python: "2.7" - env: TOXENV=py27-nooptionals - - stage: test - python: "3.4" - env: TOXENV=py34 - - stage: test - python: "3.5" - env: TOXENV=py35 - - stage: test - python: "3.6" - env: TOXENV=py36 - - stage: test - python: "3.7" - env: TOXENV=py37 - - stage: test - python: "3.8" - env: TOXENV=py38 - - stage: test - python: "3.9" - env: TOXENV=py39 - - stage: test - python: "3.9" - env: TOXENV=py39-nooptionals - - stage: test - python: "pypy" - env: TOXENV=pypy - - stage: test - python: "pypy3" - env: TOXENV=pypy3 - -install: - - pip install tox - -script: - - tox - -notifications: - email: false diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/client_python-0.9.0/MAINTAINERS.md new/client_python-0.10.1/MAINTAINERS.md --- old/client_python-0.9.0/MAINTAINERS.md 2020-11-16 14:26:28.000000000 +0100 +++ new/client_python-0.10.1/MAINTAINERS.md 2021-04-08 18:30:13.000000000 +0200 @@ -1 +1 @@ -* Brian Brazil <brian.bra...@robustperception.io> +* Chris Marchbanks <csmarchba...@gmail.com> @csmarchbanks diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/client_python-0.9.0/README.md new/client_python-0.10.1/README.md --- old/client_python-0.9.0/README.md 2020-11-16 14:26:28.000000000 +0100 +++ new/client_python-0.10.1/README.md 2021-04-08 18:30:13.000000000 +0200 @@ -6,7 +6,7 @@ **One**: Install the client: ``` -pip install prometheus_client +pip install prometheus-client ``` **Two**: Paste the following into a Python interpreter: @@ -447,6 +447,17 @@ gb.start(10.0) ``` +Graphite [tags](https://grafana.com/blog/2018/01/11/graphite-1.1-teaching-an-old-dog-new-tricks/) are also supported. + +```python +from prometheus_client.bridge.graphite import GraphiteBridge + +gb = GraphiteBridge(('graphite.your.org', 2003), tags=True) +c = Counter('my_requests_total', 'HTTP Failures', ['method', 'endpoint']) +c.labels('get', '/').inc() +gb.push() +``` + ## Custom Collectors Sometimes it is not possible to directly instrument code, as it is not @@ -468,7 +479,7 @@ REGISTRY.register(CustomCollector()) ``` -`SummaryMetricFamily` and `HistogramMetricFamily` work similarly. +`SummaryMetricFamily`, `HistogramMetricFamily` and `InfoMetricFamily` work similarly. A collector may implement a `describe` method which returns metrics in the same format as `collect` (though you don't have to include the samples). This is diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/client_python-0.9.0/SECURITY.md new/client_python-0.10.1/SECURITY.md --- old/client_python-0.9.0/SECURITY.md 1970-01-01 01:00:00.000000000 +0100 +++ new/client_python-0.10.1/SECURITY.md 2021-04-08 18:30:13.000000000 +0200 @@ -0,0 +1,6 @@ +# Reporting a security issue + +The Prometheus security policy, including how to report vulnerabilities, can be +found here: + +https://prometheus.io/docs/operating/security/ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/client_python-0.9.0/prometheus_client/bridge/graphite.py new/client_python-0.10.1/prometheus_client/bridge/graphite.py --- old/client_python-0.9.0/prometheus_client/bridge/graphite.py 2020-11-16 14:26:28.000000000 +0100 +++ new/client_python-0.10.1/prometheus_client/bridge/graphite.py 2021-04-08 18:30:13.000000000 +0200 @@ -46,9 +46,10 @@ class GraphiteBridge(object): - def __init__(self, address, registry=REGISTRY, timeout_seconds=30, _timer=time.time): + def __init__(self, address, registry=REGISTRY, timeout_seconds=30, _timer=time.time, tags=False): self._address = address self._registry = registry + self._tags = tags self._timeout = timeout_seconds self._timer = _timer @@ -63,8 +64,14 @@ for metric in self._registry.collect(): for s in metric.samples: if s.labels: - labelstr = '.' + '.'.join( - ['{0}.{1}'.format( + if self._tags: + sep = ';' + fmt = '{0}={1}' + else: + sep = '.' + fmt = '{0}.{1}' + labelstr = sep + sep.join( + [fmt.format( _sanitize(k), _sanitize(v)) for k, v in sorted(s.labels.items())]) else: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/client_python-0.9.0/prometheus_client/exposition.py new/client_python-0.10.1/prometheus_client/exposition.py --- old/client_python-0.9.0/prometheus_client/exposition.py 2020-11-16 14:26:28.000000000 +0100 +++ new/client_python-0.10.1/prometheus_client/exposition.py 2021-04-08 18:30:13.000000000 +0200 @@ -17,22 +17,86 @@ from BaseHTTPServer import BaseHTTPRequestHandler from SocketServer import ThreadingMixIn - from urllib2 import build_opener, HTTPHandler, Request + from urllib2 import ( + build_opener, HTTPError, HTTPHandler, HTTPRedirectHandler, Request, + ) from urlparse import parse_qs, urlparse except ImportError: # Python 3 from http.server import BaseHTTPRequestHandler from socketserver import ThreadingMixIn + from urllib.error import HTTPError from urllib.parse import parse_qs, quote_plus, urlparse - from urllib.request import build_opener, HTTPHandler, Request + from urllib.request import ( + build_opener, HTTPHandler, HTTPRedirectHandler, Request, + ) 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) +class _PrometheusRedirectHandler(HTTPRedirectHandler): + """ + Allow additional methods (e.g. PUT) and data forwarding in redirects. + + Use of this class constitute a user's explicit agreement to the + redirect responses the Prometheus client will receive when using it. + You should only use this class if you control or otherwise trust the + redirect behavior involved and are certain it is safe to full transfer + the original request (method and data) to the redirected URL. For + example, if you know there is a cosmetic URL redirect in front of a + local deployment of a Prometheus server, and all redirects are safe, + this is the class to use to handle redirects in that case. + + The standard HTTPRedirectHandler does not forward request data nor + does it allow redirected PUT requests (which Prometheus uses for some + operations, for example `push_to_gateway`) because these cannot + generically guarantee no violations of HTTP RFC 2616 requirements for + the user to explicitly confirm redirects that could have unexpected + side effects (such as rendering a PUT request non-idempotent or + creating multiple resources not named in the original request). + """ + + def redirect_request(self, req, fp, code, msg, headers, newurl): + """ + Apply redirect logic to a request. + + See parent HTTPRedirectHandler.redirect_request for parameter info. + + If the redirect is disallowed, this raises the corresponding HTTP error. + If the redirect can't be determined, return None to allow other handlers + to try. If the redirect is allowed, return the new request. + + This method specialized for the case when (a) the user knows that the + redirect will not cause unacceptable side effects for any request method, + and (b) the user knows that any request data should be passed through to + the redirect. If either condition is not met, this should not be used. + """ + # note that requests being provided by a handler will use get_method to + # indicate the method, by monkeypatching this, instead of setting the + # Request object's method attribute. + m = getattr(req, "method", req.get_method()) + if not (code in (301, 302, 303, 307) and m in ("GET", "HEAD") + or code in (301, 302, 303) and m in ("POST", "PUT")): + raise HTTPError(req.full_url, code, msg, headers, fp) + new_request = Request( + newurl.replace(' ', '%20'), # space escaping in new url if needed. + headers=req.headers, + origin_req_host=req.origin_req_host, + unverifiable=True, + data=req.data, + ) + if PYTHON27_OR_OLDER: + # the `method` attribute did not exist for Request in Python 2.7. + new_request.get_method = lambda: m + else: + new_request.method = m + return new_request + + def _bake_output(registry, accept_header, params): """Bake output for metrics output.""" encoder, content_type = choose_encoder(accept_header) @@ -49,8 +113,14 @@ # Prepare parameters accept_header = environ.get('HTTP_ACCEPT') params = parse_qs(environ.get('QUERY_STRING', '')) - # Bake output - status, header, output = _bake_output(registry, accept_header, params) + if environ['PATH_INFO'] == '/favicon.ico': + # Serve empty response for browsers + status = '200 OK' + header = ('', '') + output = b'' + else: + # Bake output + status, header, output = _bake_output(registry, accept_header, params) # Return output start_response(status, [header]) return [output] @@ -141,7 +211,7 @@ raise for suffix, lines in sorted(om_samples.items()): - output.append('# HELP {0}{1} {2}\n'.format(metric.name, suffix, + output.append('# HELP {0}{1} {2}\n'.format(metric.name, suffix, metric.documentation.replace('\\', r'\\').replace('\n', r'\n'))) output.append('# TYPE {0}{1} gauge\n'.format(metric.name, suffix)) output.extend(lines) @@ -205,17 +275,14 @@ os.rename(tmppath, path) -def default_handler(url, method, timeout, headers, data): - """Default handler that implements HTTP/HTTPS connections. - - Used by the push_to_gateway functions. Can be re-used by other handlers.""" +def _make_handler(url, method, timeout, headers, data, base_handler): def handle(): request = Request(url, data=data) request.get_method = lambda: method for k, v in headers: request.add_header(k, v) - resp = build_opener(HTTPHandler).open(request, timeout=timeout) + resp = build_opener(base_handler).open(request, timeout=timeout) if resp.code >= 400: raise IOError("error talking to pushgateway: {0} {1}".format( resp.code, resp.msg)) @@ -223,6 +290,28 @@ return handle +def default_handler(url, method, timeout, headers, data): + """Default handler that implements HTTP/HTTPS connections. + + Used by the push_to_gateway functions. Can be re-used by other handlers.""" + + return _make_handler(url, method, timeout, headers, data, HTTPHandler) + + +def passthrough_redirect_handler(url, method, timeout, headers, data): + """ + Handler that automatically trusts redirect responses for all HTTP methods. + + Augments standard HTTPRedirectHandler capability by permitting PUT requests, + preserving the method upon redirect, and passing through all headers and + data from the original request. Only use this handler if you control or + trust the source of redirect responses you encounter when making requests + via the Prometheus client. This handler will simply repeat the identical + request, including same method and data, to the new redirect URL.""" + + return _make_handler(url, method, timeout, headers, data, _PrometheusRedirectHandler) + + def basic_auth_handler(url, method, timeout, headers, data, username=None, password=None): """Handler that implements HTTP/HTTPS connections with Basic Auth. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/client_python-0.9.0/prometheus_client/metrics.py new/client_python-0.10.1/prometheus_client/metrics.py --- old/client_python-0.9.0/prometheus_client/metrics.py 2020-11-16 14:26:28.000000000 +0100 +++ new/client_python-0.10.1/prometheus_client/metrics.py 2021-04-08 18:30:13.000000000 +0200 @@ -186,6 +186,11 @@ with self._lock: del self._metrics[labelvalues] + def clear(self): + """Remove all labelsets from the metric""" + with self._lock: + self._metrics = {} + def _samples(self): if self._is_parent(): return self._multi_samples() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/client_python-0.9.0/prometheus_client/multiprocess.py new/client_python-0.10.1/prometheus_client/multiprocess.py --- old/client_python-0.9.0/prometheus_client/multiprocess.py 2020-11-16 14:26:28.000000000 +0100 +++ new/client_python-0.10.1/prometheus_client/multiprocess.py 2021-04-08 18:30:13.000000000 +0200 @@ -4,6 +4,7 @@ import glob import json import os +import warnings from .metrics_core import Metric from .mmap_dict import MmapedDict @@ -23,9 +24,13 @@ def __init__(self, registry, path=None): if path is None: - path = os.environ.get('prometheus_multiproc_dir') + # This deprecation warning can go away in a few releases when removing the compatibility + if 'prometheus_multiproc_dir' in os.environ and 'PROMETHEUS_MULTIPROC_DIR' not in os.environ: + os.environ['PROMETHEUS_MULTIPROC_DIR'] = os.environ['prometheus_multiproc_dir'] + warnings.warn("prometheus_multiproc_dir variable has been deprecated in favor of the upper case naming PROMETHEUS_MULTIPROC_DIR", DeprecationWarning) + path = os.environ.get('PROMETHEUS_MULTIPROC_DIR') if not path or not os.path.isdir(path): - raise ValueError('env prometheus_multiproc_dir is not set or not a directory') + raise ValueError('env PROMETHEUS_MULTIPROC_DIR is not set or not a directory') self._path = path if registry: registry.register(self) @@ -66,7 +71,7 @@ # the file is missing continue raise - for key, value, pos in file_values: + for key, value, _ in file_values: metric_name, name, labels, labels_key = _parse_key(key) metric = metrics.get(metric_name) @@ -152,7 +157,7 @@ def mark_process_dead(pid, path=None): """Do bookkeeping for when one process dies in a multi-process setup.""" if path is None: - path = os.environ.get('prometheus_multiproc_dir') + path = os.environ.get('PROMETHEUS_MULTIPROC_DIR', os.environ.get('prometheus_multiproc_dir')) for f in glob.glob(os.path.join(path, 'gauge_livesum_{0}.db'.format(pid))): os.remove(f) for f in glob.glob(os.path.join(path, 'gauge_liveall_{0}.db'.format(pid))): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/client_python-0.9.0/prometheus_client/openmetrics/parser.py new/client_python-0.10.1/prometheus_client/openmetrics/parser.py --- old/client_python-0.9.0/prometheus_client/openmetrics/parser.py 2020-11-16 14:26:28.000000000 +0100 +++ new/client_python-0.10.1/prometheus_client/openmetrics/parser.py 2021-04-08 18:30:13.000000000 +0200 @@ -529,10 +529,7 @@ if parts[1] == 'HELP': if documentation is not None: raise ValueError("More than one HELP for metric: " + line) - if len(parts) == 4: - documentation = _unescape_help(parts[3]) - elif len(parts) == 3: - raise ValueError("Invalid line: " + line) + documentation = _unescape_help(parts[3]) elif parts[1] == 'TYPE': if typ is not None: raise ValueError("More than one TYPE for metric: " + line) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/client_python-0.9.0/prometheus_client/values.py new/client_python-0.10.1/prometheus_client/values.py --- old/client_python-0.9.0/prometheus_client/values.py 2020-11-16 14:26:28.000000000 +0100 +++ new/client_python-0.10.1/prometheus_client/values.py 2021-04-08 18:30:13.000000000 +0200 @@ -2,6 +2,7 @@ import os from threading import Lock +import warnings from .mmap_dict import mmap_key, MmapedDict @@ -51,6 +52,10 @@ def __init__(self, typ, metric_name, name, labelnames, labelvalues, multiprocess_mode='', **kwargs): self._params = typ, metric_name, name, labelnames, labelvalues, multiprocess_mode + # This deprecation warning can go away in a few releases when removing the compatibility + if 'prometheus_multiproc_dir' in os.environ and 'PROMETHEUS_MULTIPROC_DIR' not in os.environ: + os.environ['PROMETHEUS_MULTIPROC_DIR'] = os.environ['prometheus_multiproc_dir'] + warnings.warn("prometheus_multiproc_dir variable has been deprecated in favor of the upper case naming PROMETHEUS_MULTIPROC_DIR", DeprecationWarning) with lock: self.__check_for_pid_change() self.__reset() @@ -64,7 +69,7 @@ file_prefix = typ if file_prefix not in files: filename = os.path.join( - os.environ['prometheus_multiproc_dir'], + os.environ.get('PROMETHEUS_MULTIPROC_DIR'), '{0}_{1}.db'.format(file_prefix, pid['value'])) files[file_prefix] = MmapedDict(filename) @@ -108,7 +113,7 @@ # This needs to be chosen before the first metric is constructed, # and as that may be in some arbitrary library the user/admin has # no control over we use an environment variable. - if 'prometheus_multiproc_dir' in os.environ: + if 'prometheus_multiproc_dir' in os.environ or 'PROMETHEUS_MULTIPROC_DIR' in os.environ: return MultiProcessValue() else: return MutexValue diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/client_python-0.9.0/setup.py new/client_python-0.10.1/setup.py --- old/client_python-0.9.0/setup.py 2020-11-16 14:26:28.000000000 +0100 +++ new/client_python-0.10.1/setup.py 2021-04-08 18:30:13.000000000 +0200 @@ -12,7 +12,7 @@ setup( name="prometheus_client", - version="0.9.0", + version="0.10.1", author="Brian Brazil", author_email="brian.bra...@robustperception.io", description="Python client for the Prometheus monitoring system.", @@ -31,6 +31,7 @@ 'twisted': ['twisted'], }, test_suite="tests", + python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", classifiers=[ "Development Status :: 4 - Beta", "Intended Audience :: Developers", @@ -38,7 +39,6 @@ "Intended Audience :: System Administrators", "Programming Language :: Python", "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/client_python-0.9.0/tests/openmetrics/test_exposition.py new/client_python-0.10.1/tests/openmetrics/test_exposition.py --- old/client_python-0.9.0/tests/openmetrics/test_exposition.py 2020-11-16 14:26:28.000000000 +0100 +++ new/client_python-0.10.1/tests/openmetrics/test_exposition.py 2021-04-08 18:30:13.000000000 +0200 @@ -48,6 +48,12 @@ self.assertEqual(b'# HELP cc A counter\n# TYPE cc counter\ncc_total 1.0\ncc_created 123.456\n# EOF\n', generate_latest(self.registry)) + def test_counter_unit(self): + c = Counter('cc_seconds', 'A counter', registry=self.registry, unit="seconds") + c.inc() + self.assertEqual(b'# HELP cc_seconds A counter\n# TYPE cc_seconds counter\n# UNIT cc_seconds seconds\ncc_seconds_total 1.0\ncc_seconds_created 123.456\n# EOF\n', + generate_latest(self.registry)) + def test_gauge(self): g = Gauge('gg', 'A gauge', registry=self.registry) g.set(17) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/client_python-0.9.0/tests/openmetrics/test_parser.py new/client_python-0.10.1/tests/openmetrics/test_parser.py --- old/client_python-0.9.0/tests/openmetrics/test_parser.py 2020-11-16 14:26:28.000000000 +0100 +++ new/client_python-0.10.1/tests/openmetrics/test_parser.py 2021-04-08 18:30:13.000000000 +0200 @@ -221,6 +221,16 @@ cfm.add_sample("a_total", {}, 0.0, Timestamp(123, 0), Exemplar({"a": "b"}, 0.5)) self.assertEqual([cfm], list(families)) + def test_counter_exemplars_empty_brackets(self): + families = text_string_to_metric_families("""# TYPE a counter +# HELP a help +a_total{} 0 123 # {a="b"} 0.5 +# EOF +""") + cfm = CounterMetricFamily("a", "help") + cfm.add_sample("a_total", {}, 0.0, Timestamp(123, 0), Exemplar({"a": "b"}, 0.5)) + self.assertEqual([cfm], list(families)) + def test_simple_info(self): families = text_string_to_metric_families("""# TYPE a info # HELP a help @@ -409,11 +419,15 @@ # HELP a he\\n\\\\l\\tp a_total{foo="b\\"a\\nr"} 1 a_total{foo="b\\\\a\\z"} 2 +a_total{foo="b\\"a\\nr # "} 3 +a_total{foo="b\\\\a\\z # "} 4 # EOF """) metric_family = CounterMetricFamily("a", "he\n\\l\\tp", labels=["foo"]) metric_family.add_metric(["b\"a\nr"], 1) metric_family.add_metric(["b\\a\\z"], 2) + metric_family.add_metric(["b\"a\nr # "], 3) + metric_family.add_metric(["b\\a\\z # "], 4) self.assertEqual([metric_family], list(families)) def test_null_byte(self): @@ -591,6 +605,10 @@ foo_count 17.0 foo_sum 324789.3 foo_created 1.520430000123e+09 +# HELP bar histogram Testing with labels +# TYPE bar histogram +bar_bucket{a="b",le="+Inf"} 0.0 +bar_bucket{a="c",le="+Inf"} 0.0 # EOF """ families = list(text_string_to_metric_families(text)) @@ -628,9 +646,14 @@ ('a{a="1",b="2",} 1\n# EOF\n'), # Invalid labels. ('a{1="1"} 1\n# EOF\n'), + ('a{1="1"}1\n# EOF\n'), ('a{a="1",a="1"} 1\n# EOF\n'), + ('a{a="1"b} 1\n# EOF\n'), ('a{1=" # "} 1\n# EOF\n'), ('a{a=" # ",a=" # "} 1\n# EOF\n'), + ('a{a=" # "}1\n# EOF\n'), + ('a{a=" # ",b=}1\n# EOF\n'), + ('a{a=" # "b}1\n# EOF\n'), # Missing value. ('a\n# EOF\n'), ('a \n# EOF\n'), @@ -667,6 +690,8 @@ ('# HELP a x\n# HELP a x\n# EOF\n'), ('# TYPE a untyped\n# TYPE a untyped\n# EOF\n'), ('# UNIT a_s s\n# UNIT a_s s\n# EOF\n'), + # Bad metadata. + ('# FOO a x\n# EOF\n'), # Bad metric names. ('0a 1\n# EOF\n'), ('a.b 1\n# EOF\n'), @@ -706,6 +731,10 @@ '{a="23456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"} 1 1\n# EOF\n'), ('# TYPE a histogram\na_bucket{le="+Inf"} 1 # {} 0x1p-3\n# EOF\n'), ('# TYPE a histogram\na_bucket{le="+Inf"} 1 # {} 1 0x1p-3\n# EOF\n'), + ('# TYPE a counter\na_total 1 1 # {id="a"} \n# EOF\n'), + ('# TYPE a counter\na_total 1 1 # id="a"} 1\n# EOF\n'), + ('# TYPE a counter\na_total 1 1 #id=" # "} 1\n# EOF\n'), + ('# TYPE a counter\na_total 1 1 id=" # "} 1\n# EOF\n'), # Exemplars on unallowed samples. ('# TYPE a histogram\na_sum 1 # {a="b"} 0.5\n# EOF\n'), ('# TYPE a gaugehistogram\na_sum 1 # {a="b"} 0.5\n# EOF\n'), @@ -750,13 +779,18 @@ ('# TYPE a summary\na_sum -1\n# EOF\n'), ('# TYPE a summary\na_count -1\n# EOF\n'), ('# TYPE a summary\na{quantile="0.5"} -1\n# EOF\n'), + # Bad info and stateset values. + ('# TYPE a info\na_info{foo="bar"} 2\n# EOF\n'), + ('# TYPE a stateset\na{a="bar"} 2\n# EOF\n'), # Bad histograms. ('# TYPE a histogram\na_sum 1\n# EOF\n'), - ('# TYPE a histogram\na_bucket{le="+Inf"} 0\n#a_sum 0\n# EOF\n'), - ('# TYPE a histogram\na_bucket{le="+Inf"} 0\n#a_count 0\n# EOF\n'), + ('# TYPE a histogram\na_bucket{le="+Inf"} 0\na_sum 0\n# EOF\n'), + ('# TYPE a histogram\na_bucket{le="+Inf"} 0\na_count 0\n# EOF\n'), + ('# TYPE a histogram\na_bucket{le="-1"} 0\na_bucket{le="+Inf"} 0\na_sum 0\na_count 0\n# EOF\n'), ('# TYPE a gaugehistogram\na_gsum 1\n# EOF\n'), ('# TYPE a gaugehistogram\na_bucket{le="+Inf"} 0\na_gsum 0\n# EOF\n'), ('# TYPE a gaugehistogram\na_bucket{le="+Inf"} 0\na_gcount 0\n# EOF\n'), + ('# TYPE a gaugehistogram\na_bucket{le="+Inf"} 1\na_gsum -1\na_gcount 1\n# EOF\n'), ('# TYPE a histogram\na_count 1\na_bucket{le="+Inf"} 0\n# EOF\n'), ('# TYPE a histogram\na_bucket{le="+Inf"} 0\na_count 1\n# EOF\n'), ('# TYPE a histogram\na_bucket{le="+INF"} 0\n# EOF\n'), diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/client_python-0.9.0/tests/test_core.py new/client_python-0.10.1/tests/test_core.py --- old/client_python-0.9.0/tests/test_core.py 2020-11-16 14:26:28.000000000 +0100 +++ new/client_python-0.10.1/tests/test_core.py 2021-04-08 18:30:13.000000000 +0200 @@ -456,6 +456,15 @@ self.assertEqual(None, self.registry.get_sample_value('c_total', {'l': 'x'})) self.assertEqual(2, self.registry.get_sample_value('c_total', {'l': 'y'})) + def test_clear(self): + self.counter.labels('x').inc() + self.counter.labels('y').inc(2) + self.assertEqual(1, self.registry.get_sample_value('c_total', {'l': 'x'})) + self.assertEqual(2, self.registry.get_sample_value('c_total', {'l': 'y'})) + self.counter.clear() + self.assertEqual(None, self.registry.get_sample_value('c_total', {'l': 'x'})) + self.assertEqual(None, self.registry.get_sample_value('c_total', {'l': 'y'})) + def test_incorrect_label_count_raises(self): self.assertRaises(ValueError, self.counter.labels) self.assertRaises(ValueError, self.counter.labels, 'a', 'b') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/client_python-0.9.0/tests/test_exposition.py new/client_python-0.10.1/tests/test_exposition.py --- old/client_python-0.9.0/tests/test_exposition.py 2020-11-16 14:26:28.000000000 +0100 +++ new/client_python-0.10.1/tests/test_exposition.py 2021-04-08 18:30:13.000000000 +0200 @@ -14,6 +14,7 @@ from prometheus_client.core import GaugeHistogramMetricFamily, Timestamp from prometheus_client.exposition import ( basic_auth_handler, default_handler, MetricsHandler, + passthrough_redirect_handler, ) if sys.version_info < (2, 7): @@ -208,6 +209,8 @@ class TestPushGateway(unittest.TestCase): def setUp(self): + redirect_flag = 'testFlag' + self.redirect_flag = redirect_flag # preserve a copy for downstream test assertions self.registry = CollectorRegistry() self.counter = Gauge('g', 'help', registry=self.registry) self.requests = requests = [] @@ -216,6 +219,11 @@ def do_PUT(self): if 'with_basic_auth' in self.requestline and self.headers['authorization'] != 'Basic Zm9vOmJhcg==': self.send_response(401) + elif 'redirect' in self.requestline and redirect_flag not in self.requestline: + # checks for an initial test request with 'redirect' but without the redirect_flag, + # and simulates a redirect to a url with the redirect_flag (which will produce a 201) + self.send_response(301) + self.send_header('Location', getattr(self, 'redirect_address', None)) else: self.send_response(201) length = int(self.headers['content-length']) @@ -225,6 +233,22 @@ do_POST = do_PUT do_DELETE = do_PUT + # set up a separate server to serve a fake redirected request. + # the redirected URL will have `redirect_flag` added to it, + # which will cause the request handler to return 201. + httpd_redirect = HTTPServer(('localhost', 0), TestHandler) + self.redirect_address = TestHandler.redirect_address = \ + 'http://localhost:{0}/{1}'.format(httpd_redirect.server_address[1], redirect_flag) + + class TestRedirectServer(threading.Thread): + def run(self): + httpd_redirect.handle_request() + + self.redirect_server = TestRedirectServer() + self.redirect_server.daemon = True + self.redirect_server.start() + + # set up the normal server to serve the example requests across test cases. httpd = HTTPServer(('localhost', 0), TestHandler) self.address = 'http://localhost:{0}'.format(httpd.server_address[1]) @@ -236,6 +260,7 @@ self.server.daemon = True self.server.start() + def test_push(self): push_to_gateway(self.address, "my_job", self.registry) self.assertEqual(self.requests[0][0].command, 'PUT') @@ -330,6 +355,27 @@ self.assertEqual(self.requests[0][0].headers.get('content-type'), CONTENT_TYPE_LATEST) self.assertEqual(self.requests[0][1], b'# HELP g help\n# TYPE g gauge\ng 0.0\n') + def test_push_with_redirect_handler(self): + def my_redirect_handler(url, method, timeout, headers, data): + return passthrough_redirect_handler(url, method, timeout, headers, data) + + push_to_gateway(self.address, "my_job_with_redirect", self.registry, handler=my_redirect_handler) + self.assertEqual(self.requests[0][0].command, 'PUT') + self.assertEqual(self.requests[0][0].path, '/metrics/job/my_job_with_redirect') + self.assertEqual(self.requests[0][0].headers.get('content-type'), CONTENT_TYPE_LATEST) + self.assertEqual(self.requests[0][1], b'# HELP g help\n# TYPE g gauge\ng 0.0\n') + + # ensure the redirect preserved request settings from the initial request. + self.assertEqual(self.requests[0][0].command, self.requests[1][0].command) + self.assertEqual( + self.requests[0][0].headers.get('content-type'), + self.requests[1][0].headers.get('content-type') + ) + self.assertEqual(self.requests[0][1], self.requests[1][1]) + + # 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." diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/client_python-0.9.0/tests/test_graphite_bridge.py new/client_python-0.10.1/tests/test_graphite_bridge.py --- old/client_python-0.9.0/tests/test_graphite_bridge.py 2020-11-16 14:26:28.000000000 +0100 +++ new/client_python-0.10.1/tests/test_graphite_bridge.py 2021-04-08 18:30:13.000000000 +0200 @@ -35,8 +35,11 @@ self.t.start() # Explicitly use localhost as the target host, since connecting to 0.0.0.0 fails on Windows - address = ('localhost', server.server_address[1]) - self.gb = GraphiteBridge(address, self.registry, _timer=fake_timer) + self.address = ('localhost', server.server_address[1]) + self.gb = GraphiteBridge(self.address, self.registry, _timer=fake_timer) + + def _use_tags(self): + self.gb = GraphiteBridge(self.address, self.registry, tags=True, _timer=fake_timer) def test_nolabels(self): gauge = Gauge('g', 'help', registry=self.registry) @@ -56,6 +59,16 @@ self.assertEqual(b'labels.a.c.b.d 1.0 1434898897\n', self.data) + def test_labels_tags(self): + self._use_tags() + labels = Gauge('labels', 'help', ['a', 'b'], registry=self.registry) + labels.labels('c', 'd').inc() + + self.gb.push() + self.t.join() + + self.assertEqual(b'labels;a=c;b=d 1.0 1434898897\n', self.data) + def test_prefix(self): labels = Gauge('labels', 'help', ['a', 'b'], registry=self.registry) labels.labels('c', 'd').inc() @@ -65,6 +78,16 @@ self.assertEqual(b'pre.fix.labels.a.c.b.d 1.0 1434898897\n', self.data) + def test_prefix_tags(self): + self._use_tags() + labels = Gauge('labels', 'help', ['a', 'b'], registry=self.registry) + labels.labels('c', 'd').inc() + + self.gb.push(prefix='pre.fix') + self.t.join() + + self.assertEqual(b'pre.fix.labels;a=c;b=d 1.0 1434898897\n', self.data) + def test_sanitizing(self): labels = Gauge('labels', 'help', ['a'], registry=self.registry) labels.labels('c.:8').inc() @@ -73,3 +96,13 @@ self.t.join() self.assertEqual(b'labels.a.c__8 1.0 1434898897\n', self.data) + + def test_sanitizing_tags(self): + self._use_tags() + labels = Gauge('labels', 'help', ['a'], registry=self.registry) + labels.labels('c.:8').inc() + + self.gb.push() + self.t.join() + + self.assertEqual(b'labels;a=c__8 1.0 1434898897\n', self.data) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/client_python-0.9.0/tests/test_multiprocess.py new/client_python-0.10.1/tests/test_multiprocess.py --- old/client_python-0.9.0/tests/test_multiprocess.py 2020-11-16 14:26:28.000000000 +0100 +++ new/client_python-0.10.1/tests/test_multiprocess.py 2021-04-08 18:30:13.000000000 +0200 @@ -5,6 +5,7 @@ import shutil import sys import tempfile +import warnings from prometheus_client import mmap_dict, values from prometheus_client.core import ( @@ -13,7 +14,9 @@ from prometheus_client.multiprocess import ( mark_process_dead, MultiProcessCollector, ) -from prometheus_client.values import MultiProcessValue, MutexValue +from prometheus_client.values import ( + get_value_class, MultiProcessValue, MutexValue, +) if sys.version_info < (2, 7): # We need the skip decorators from unittest2 on Python 2.6. @@ -22,20 +25,50 @@ import unittest -class TestMultiProcess(unittest.TestCase): +class TestMultiProcessDeprecation(unittest.TestCase): def setUp(self): self.tempdir = tempfile.mkdtemp() + + def tearDown(self): + os.environ.pop('prometheus_multiproc_dir', None) + os.environ.pop('PROMETHEUS_MULTIPROC_DIR', None) + values.ValueClass = MutexValue + shutil.rmtree(self.tempdir) + + def test_deprecation_warning(self): os.environ['prometheus_multiproc_dir'] = self.tempdir + with warnings.catch_warnings(record=True) as w: + values.ValueClass = get_value_class() + registry = CollectorRegistry() + collector = MultiProcessCollector(registry) + Counter('c', 'help', registry=None) + + assert os.environ['PROMETHEUS_MULTIPROC_DIR'] == self.tempdir + assert len(w) == 1 + assert issubclass(w[-1].category, DeprecationWarning) + assert "PROMETHEUS_MULTIPROC_DIR" in str(w[-1].message) + + def test_mark_process_dead_respects_lowercase(self): + os.environ['prometheus_multiproc_dir'] = self.tempdir + # Just test that this does not raise with a lowercase env var. The + # logic is tested elsewhere. + mark_process_dead(123) + + +class TestMultiProcess(unittest.TestCase): + def setUp(self): + self.tempdir = tempfile.mkdtemp() + os.environ['PROMETHEUS_MULTIPROC_DIR'] = self.tempdir values.ValueClass = MultiProcessValue(lambda: 123) self.registry = CollectorRegistry() - self.collector = MultiProcessCollector(self.registry, self.tempdir) + self.collector = MultiProcessCollector(self.registry) @property def _value_class(self): return def tearDown(self): - del os.environ['prometheus_multiproc_dir'] + del os.environ['PROMETHEUS_MULTIPROC_DIR'] shutil.rmtree(self.tempdir) values.ValueClass = MutexValue @@ -80,7 +113,7 @@ self.assertEqual(0, self.registry.get_sample_value('g', {'pid': '456'})) g1.set(1) g2.set(2) - mark_process_dead(123, os.environ['prometheus_multiproc_dir']) + mark_process_dead(123) self.assertEqual(1, self.registry.get_sample_value('g', {'pid': '123'})) self.assertEqual(2, self.registry.get_sample_value('g', {'pid': '456'})) @@ -94,7 +127,7 @@ g2.set(2) self.assertEqual(1, self.registry.get_sample_value('g', {'pid': '123'})) self.assertEqual(2, self.registry.get_sample_value('g', {'pid': '456'})) - mark_process_dead(123, os.environ['prometheus_multiproc_dir']) + mark_process_dead(123, os.environ['PROMETHEUS_MULTIPROC_DIR']) self.assertEqual(None, self.registry.get_sample_value('g', {'pid': '123'})) self.assertEqual(2, self.registry.get_sample_value('g', {'pid': '456'})) @@ -124,7 +157,7 @@ g1.set(1) g2.set(2) self.assertEqual(3, self.registry.get_sample_value('g')) - mark_process_dead(123, os.environ['prometheus_multiproc_dir']) + mark_process_dead(123, os.environ['PROMETHEUS_MULTIPROC_DIR']) self.assertEqual(2, self.registry.get_sample_value('g')) def test_namespace_subsystem(self): @@ -151,7 +184,7 @@ # can not inspect the files cache directly, as it's a closure, so we # check for the actual files themselves def files(): - fs = os.listdir(os.environ['prometheus_multiproc_dir']) + fs = os.listdir(os.environ['PROMETHEUS_MULTIPROC_DIR']) fs.sort() return fs @@ -240,7 +273,7 @@ pid = 1 h.labels(**labels).observe(5) - path = os.path.join(os.environ['prometheus_multiproc_dir'], '*.db') + path = os.path.join(os.environ['PROMETHEUS_MULTIPROC_DIR'], '*.db') files = glob.glob(path) metrics = dict( (m.name, m) for m in self.collector.merge(files, accumulate=False) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/client_python-0.9.0/tests/test_wsgi.py new/client_python-0.10.1/tests/test_wsgi.py --- old/client_python-0.9.0/tests/test_wsgi.py 2020-11-16 14:26:28.000000000 +0100 +++ new/client_python-0.10.1/tests/test_wsgi.py 2021-04-08 18:30:13.000000000 +0200 @@ -1,10 +1,12 @@ from __future__ import absolute_import, unicode_literals +import sys +import unittest from unittest import TestCase from wsgiref.util import setup_testing_defaults from prometheus_client import CollectorRegistry, Counter, make_wsgi_app -from prometheus_client.exposition import CONTENT_TYPE_LATEST +from prometheus_client.exposition import _bake_output, CONTENT_TYPE_LATEST class WSGITest(TestCase): @@ -65,3 +67,22 @@ def test_report_metrics_4(self): self.validate_metrics("failed_requests", "Number of failed requests", 7) + + @unittest.skipIf(sys.version_info < (3, 3), "Test requires Python 3.3+.") + def test_favicon_path(self): + from unittest.mock import patch + + # Create mock to enable counting access of _bake_output + with patch("prometheus_client.exposition._bake_output", side_effect=_bake_output) as mock: + # Create and run WSGI app + app = make_wsgi_app(self.registry) + # Try accessing the favicon path + favicon_environ = dict(self.environ) + favicon_environ['PATH_INFO'] = '/favicon.ico' + outputs = app(favicon_environ, self.capture) + # Test empty response + self.assertEqual(outputs, [b'']) + self.assertEqual(mock.call_count, 0) + # Try accessing normal paths + app(self.environ, self.capture) + self.assertEqual(mock.call_count, 1) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/client_python-0.9.0/tox.ini new/client_python-0.10.1/tox.ini --- old/client_python-0.9.0/tox.ini 2020-11-16 14:26:28.000000000 +0100 +++ new/client_python-0.10.1/tox.ini 2021-04-08 18:30:13.000000000 +0200 @@ -1,5 +1,5 @@ [tox] -envlist = coverage-clean,py26,py27,py34,py35,py36,py37,py38,py39,pypy,pypy3,{py27,py39}-nooptionals,coverage-report,flake8,isort +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 [base] @@ -7,21 +7,12 @@ coverage pytest -[testenv:py26] -; Last pytest and py version supported on py26 . -deps = - unittest2 - py==1.4.31 - pytest==2.9.2 - coverage - futures - -[testenv:py27] +[testenv:py2.7] deps = {[base]deps} futures -[testenv:pypy] +[testenv:pypy2.7] deps = {[base]deps} futures @@ -29,18 +20,18 @@ [testenv] deps = {[base]deps} - {py27,py37,pypy,pypy3}: twisted - {py37,pypy3}: asgiref + {py2.7,py3.7,pypy2.7,pypy3.7}: twisted + {py3.7,pypy3.7}: asgiref commands = coverage run --parallel -m pytest {posargs} ; Ensure test suite passes if no optional dependencies are present. -[testenv:py27-nooptionals] +[testenv:py2.7-nooptionals] deps = {[base]deps} futures commands = coverage run --parallel -m pytest {posargs} -[testenv:py39-nooptionals] +[testenv:py3.9-nooptionals] commands = coverage run --parallel -m pytest {posargs} [testenv:coverage-clean]