From 4535ce0f43097aa48e44a65747d82064f2aadaf5 Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 8 Mar 2024 21:12:24 -0500 Subject: [PATCH 1/8] Add sanity check for label value (#1012) Signed-off-by: Pengfei Zhang --- prometheus_client/metrics.py | 2 ++ tests/test_core.py | 1 + 2 files changed, 3 insertions(+) diff --git a/prometheus_client/metrics.py b/prometheus_client/metrics.py index 91cd9ecf..af512115 100644 --- a/prometheus_client/metrics.py +++ b/prometheus_client/metrics.py @@ -705,6 +705,8 @@ def info(self, val: Dict[str, str]) -> None: if self._labelname_set.intersection(val.keys()): raise ValueError('Overlapping labels for Info metric, metric: {} child: {}'.format( self._labelnames, val)) + if any(i is None for i in val.values()): + raise ValueError('Label value cannot be None') with self._lock: self._value = dict(val) diff --git a/tests/test_core.py b/tests/test_core.py index 30f9e0ad..8a54a02d 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -534,6 +534,7 @@ def test_info(self): def test_labels(self): self.assertRaises(ValueError, self.labels.labels('a').info, {'l': ''}) + self.assertRaises(ValueError, self.labels.labels('a').info, {'il': None}) self.labels.labels('a').info({'foo': 'bar'}) self.assertEqual(1, self.registry.get_sample_value('il_info', {'l': 'a', 'foo': 'bar'})) From 7bc8cddfbbc9b72c98725a879d9b94a675a6c7da Mon Sep 17 00:00:00 2001 From: Jason Mobarak Date: Mon, 15 Apr 2024 14:57:30 -0700 Subject: [PATCH 2/8] docs: correct link to multiprocessing docs (#1023) * docs: correct link to multiprocessing docs Signed-off-by: Jason Mobarak * Update docs/content/exporting/http/fastapi-gunicorn.md Co-authored-by: Chris Marchbanks Signed-off-by: Jason Mobarak --------- Signed-off-by: Jason Mobarak Co-authored-by: Chris Marchbanks --- docs/content/exporting/http/fastapi-gunicorn.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/exporting/http/fastapi-gunicorn.md b/docs/content/exporting/http/fastapi-gunicorn.md index 9ce12381..148a36d7 100644 --- a/docs/content/exporting/http/fastapi-gunicorn.md +++ b/docs/content/exporting/http/fastapi-gunicorn.md @@ -19,7 +19,7 @@ metrics_app = make_asgi_app() app.mount("/metrics", metrics_app) ``` -For Multiprocessing support, use this modified code snippet. Full multiprocessing instructions are provided [here](https://github.com/prometheus/client_python#multiprocess-mode-eg-gunicorn). +For Multiprocessing support, use this modified code snippet. Full multiprocessing instructions are provided [here]({{< ref "/multiprocess" >}}). ```python from fastapi import FastAPI @@ -47,4 +47,4 @@ pip install gunicorn gunicorn -b 127.0.0.1:8000 myapp:app -k uvicorn.workers.UvicornWorker ``` -Visit http://localhost:8000/metrics to see the metrics \ No newline at end of file +Visit http://localhost:8000/metrics to see the metrics From eeec421b2f489d2c465bb8ca419b772829b7b16c Mon Sep 17 00:00:00 2001 From: Chris Marchbanks Date: Tue, 16 Apr 2024 08:17:42 -0600 Subject: [PATCH 3/8] Pin python 3.8 and 3.9 at patch level (#1024) Signed-off-by: Chris Marchbanks --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 624e4eae..2605a505 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -75,8 +75,8 @@ workflows: matrix: parameters: python: - - "3.8" - - "3.9" + - "3.8.18" + - "3.9.18" - "3.10" - "3.11" - "3.12" From e364a96f506bbb70ae744e0b3307e4b693e28258 Mon Sep 17 00:00:00 2001 From: Eden Yemini Date: Tue, 28 May 2024 17:55:39 +0300 Subject: [PATCH 4/8] Fix a typo in ASGI docs (#1036) Signed-off-by: Eden Yemini --- docs/content/exporting/http/asgi.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/exporting/http/asgi.md b/docs/content/exporting/http/asgi.md index 4ff115ea..5b9d5430 100644 --- a/docs/content/exporting/http/asgi.md +++ b/docs/content/exporting/http/asgi.md @@ -14,10 +14,10 @@ app = make_asgi_app() Such an application can be useful when integrating Prometheus metrics with ASGI apps. -By default, the WSGI application will respect `Accept-Encoding:gzip` headers used by Prometheus +By default, the ASGI application will respect `Accept-Encoding:gzip` headers used by Prometheus and compress the response if such a header is present. This behaviour can be disabled by passing `disable_compression=True` when creating the app, like this: ```python app = make_asgi_app(disable_compression=True) -``` \ No newline at end of file +``` From 09a5ae30602a7a81f6174dae4ba08b93ee7feed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel=20Garc=C3=ADa?= Date: Tue, 28 May 2024 22:11:34 +0200 Subject: [PATCH 5/8] Fix timestamp comparison (#1038) Signed-off-by: Miguel Angel Garcia --- prometheus_client/samples.py | 4 ++-- tests/test_samples.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/prometheus_client/samples.py b/prometheus_client/samples.py index 8735fed9..53c47264 100644 --- a/prometheus_client/samples.py +++ b/prometheus_client/samples.py @@ -28,10 +28,10 @@ def __ne__(self, other: object) -> bool: return not self == other def __gt__(self, other: "Timestamp") -> bool: - return self.sec > other.sec or self.nsec > other.nsec + return self.nsec > other.nsec if self.sec == other.sec else self.sec > other.sec def __lt__(self, other: "Timestamp") -> bool: - return self.sec < other.sec or self.nsec < other.nsec + return self.nsec < other.nsec if self.sec == other.sec else self.sec < other.sec # Timestamp and exemplar are optional. diff --git a/tests/test_samples.py b/tests/test_samples.py index 796afe7e..7b59218b 100644 --- a/tests/test_samples.py +++ b/tests/test_samples.py @@ -12,6 +12,8 @@ def test_gt(self): self.assertEqual(samples.Timestamp(1, 2) > samples.Timestamp(1, 1), True) self.assertEqual(samples.Timestamp(2, 1) > samples.Timestamp(1, 1), True) self.assertEqual(samples.Timestamp(2, 2) > samples.Timestamp(1, 1), True) + self.assertEqual(samples.Timestamp(0, 2) > samples.Timestamp(1, 1), False) + self.assertEqual(samples.Timestamp(2, 0) > samples.Timestamp(1, 1), True) def test_lt(self): self.assertEqual(samples.Timestamp(1, 1) < samples.Timestamp(1, 1), False) @@ -21,6 +23,8 @@ def test_lt(self): self.assertEqual(samples.Timestamp(1, 2) < samples.Timestamp(1, 1), False) self.assertEqual(samples.Timestamp(2, 1) < samples.Timestamp(1, 1), False) self.assertEqual(samples.Timestamp(2, 2) < samples.Timestamp(1, 1), False) + self.assertEqual(samples.Timestamp(0, 2) < samples.Timestamp(1, 1), True) + self.assertEqual(samples.Timestamp(2, 0) < samples.Timestamp(1, 1), False) if __name__ == '__main__': From 7c45f84e5e3d2e0a75b3946408fec1a4d5c72841 Mon Sep 17 00:00:00 2001 From: Andreas Maier Date: Fri, 2 Aug 2024 19:28:35 +0200 Subject: [PATCH 6/8] Reject invalid HTTP methods and resources (#1019) This change addresses the issue that currently, any HTTP method is handled by returning success and metrics data, which causes network scanners to report issues. Details: * This change rejects any HTTP methods and resources other than the following: OPTIONS (any) - returns 200 and an 'Allow' header indicating allowed methods GET (any) - returns 200 and metrics GET /favicon.ico - returns 200 and no body (this is no change) Other HTTP methods than these are rejected with 405 "Method Not Allowed" and an 'Allow' header indicating the allowed HTTP methods. Any returned HTTP errors are also displayed in the response body after a hash sign and with a brief hint, e.g. "# HTTP 405 Method Not Allowed: XXX; use OPTIONS or GET". Signed-off-by: Andreas Maier --- docs/content/exporting/http/_index.md | 21 ++++++++++++++++++++- prometheus_client/exposition.py | 14 +++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/docs/content/exporting/http/_index.md b/docs/content/exporting/http/_index.md index 71edc7e3..dc1b8f2c 100644 --- a/docs/content/exporting/http/_index.md +++ b/docs/content/exporting/http/_index.md @@ -52,4 +52,23 @@ chain is used (see Python [ssl.SSLContext.load_default_certs()](https://docs.pyt from prometheus_client import start_http_server start_http_server(8000, certfile="server.crt", keyfile="server.key") -``` \ No newline at end of file +``` + +# Supported HTTP methods + +The prometheus client will handle the following HTTP methods and resources: + +* `OPTIONS (any)` - returns HTTP status 200 and an 'Allow' header indicating the + allowed methods (OPTIONS, GET) +* `GET (any)` - returns HTTP status 200 and the metrics data +* `GET /favicon.ico` - returns HTTP status 200 and an empty response body. Some + browsers support this to display the returned icon in the browser tab. + +Other HTTP methods than these are rejected with HTTP status 405 "Method Not Allowed" +and an 'Allow' header indicating the allowed methods (OPTIONS, GET). + +Any returned HTTP errors are also displayed in the response body after a hash +sign and with a brief hint. Example: +``` +# HTTP 405 Method Not Allowed: XXX; use OPTIONS or GET +``` diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index 3a47917c..fab139df 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -118,12 +118,24 @@ def prometheus_app(environ, start_response): accept_header = environ.get('HTTP_ACCEPT') accept_encoding_header = environ.get('HTTP_ACCEPT_ENCODING') params = parse_qs(environ.get('QUERY_STRING', '')) - if environ['PATH_INFO'] == '/favicon.ico': + method = environ['REQUEST_METHOD'] + + if method == 'OPTIONS': + status = '200 OK' + headers = [('Allow', 'OPTIONS,GET')] + output = b'' + elif method != 'GET': + status = '405 Method Not Allowed' + headers = [('Allow', 'OPTIONS,GET')] + output = '# HTTP {}: {}; use OPTIONS or GET\n'.format(status, method).encode() + elif environ['PATH_INFO'] == '/favicon.ico': # Serve empty response for browsers status = '200 OK' headers = [('', '')] output = b'' else: + # Note: For backwards compatibility, the URI path for GET is not + # constrained to the documented /metrics, but any path is allowed. # Bake output status, headers, output = _bake_output(registry, accept_header, accept_encoding_header, params, disable_compression) # Return output From 0014e9776350a252930671ed170edee464f9b428 Mon Sep 17 00:00:00 2001 From: Ben Timby Date: Tue, 17 Sep 2024 16:07:17 -0400 Subject: [PATCH 7/8] Use re-entrant lock. (#1014) * Use re-entrant lock. --------- Signed-off-by: Ben Timby --- prometheus_client/metrics.py | 8 ++++---- prometheus_client/registry.py | 4 ++-- prometheus_client/values.py | 6 +++--- tests/test_core.py | 10 +++++++++- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/prometheus_client/metrics.py b/prometheus_client/metrics.py index af512115..3bda92c4 100644 --- a/prometheus_client/metrics.py +++ b/prometheus_client/metrics.py @@ -1,5 +1,5 @@ import os -from threading import Lock +from threading import RLock import time import types from typing import ( @@ -144,7 +144,7 @@ def __init__(self: T, if self._is_parent(): # Prepare the fields needed for child metrics. - self._lock = Lock() + self._lock = RLock() self._metrics: Dict[Sequence[str], T] = {} if self._is_observable(): @@ -697,7 +697,7 @@ class Info(MetricWrapperBase): def _metric_init(self): self._labelname_set = set(self._labelnames) - self._lock = Lock() + self._lock = RLock() self._value = {} def info(self, val: Dict[str, str]) -> None: @@ -759,7 +759,7 @@ def __init__(self, def _metric_init(self) -> None: self._value = 0 - self._lock = Lock() + self._lock = RLock() def state(self, state: str) -> None: """Set enum metric state.""" diff --git a/prometheus_client/registry.py b/prometheus_client/registry.py index 694e4bd8..4326b39a 100644 --- a/prometheus_client/registry.py +++ b/prometheus_client/registry.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod import copy -from threading import Lock +from threading import RLock from typing import Dict, Iterable, List, Optional from .metrics_core import Metric @@ -30,7 +30,7 @@ def __init__(self, auto_describe: bool = False, target_info: Optional[Dict[str, self._collector_to_names: Dict[Collector, List[str]] = {} self._names_to_collectors: Dict[str, Collector] = {} self._auto_describe = auto_describe - self._lock = Lock() + self._lock = RLock() self._target_info: Optional[Dict[str, str]] = {} self.set_target_info(target_info) diff --git a/prometheus_client/values.py b/prometheus_client/values.py index 6ff85e3b..05331f82 100644 --- a/prometheus_client/values.py +++ b/prometheus_client/values.py @@ -1,5 +1,5 @@ import os -from threading import Lock +from threading import RLock import warnings from .mmap_dict import mmap_key, MmapedDict @@ -13,7 +13,7 @@ class MutexValue: def __init__(self, typ, metric_name, name, labelnames, labelvalues, help_text, **kwargs): self._value = 0.0 self._exemplar = None - self._lock = Lock() + self._lock = RLock() def inc(self, amount): with self._lock: @@ -50,7 +50,7 @@ def MultiProcessValue(process_identifier=os.getpid): # Use a single global lock when in multi-processing mode # as we presume this means there is no threading going on. # This avoids the need to also have mutexes in __MmapDict. - lock = Lock() + lock = RLock() class MmapedValue: """A float protected by a mutex backed by a per-process mmaped file.""" diff --git a/tests/test_core.py b/tests/test_core.py index 8a54a02d..f80fb882 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -16,6 +16,14 @@ from prometheus_client.metrics import _get_use_created +def is_locked(lock): + "Tries to obtain a lock, returns True on success, False on failure." + locked = lock.acquire(blocking=False) + if locked: + lock.release() + return not locked + + def assert_not_observable(fn, *args, **kwargs): """ Assert that a function call falls with a ValueError exception containing @@ -963,7 +971,7 @@ def test_restricted_registry_does_not_yield_while_locked(self): m = Metric('target', 'Target metadata', 'info') m.samples = [Sample('target_info', {'foo': 'bar'}, 1)] for _ in registry.restricted_registry(['target_info', 's_sum']).collect(): - self.assertFalse(registry._lock.locked()) + self.assertFalse(is_locked(registry._lock)) if __name__ == '__main__': From 3b183b44994454be226c208037e1fe4b9a89dfc5 Mon Sep 17 00:00:00 2001 From: Chris Marchbanks Date: Fri, 20 Sep 2024 09:12:21 -0600 Subject: [PATCH 8/8] Release 0.21.0 Signed-off-by: Chris Marchbanks --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 595e5954..438f643a 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name="prometheus_client", - version="0.20.0", + version="0.21.0", author="Brian Brazil", author_email="brian.brazil@robustperception.io", description="Python client for the Prometheus monitoring system.",