From 809c313f65ba83156530d419040f5baf8472b262 Mon Sep 17 00:00:00 2001 From: Brad Cowie Date: Thu, 16 Apr 2026 10:09:02 +1200 Subject: [PATCH 1/2] Upgrade python3-prometheus-client to v0.25.0 --- .circleci/config.yml | 93 --------------- docs/content/exporting/http/_index.md | 1 + docs/content/instrumenting/_index.md | 16 ++- docs/content/instrumenting/counter.md | 109 ++++++++++++++++-- docs/content/instrumenting/enum.md | 88 +++++++++++++- docs/content/instrumenting/gauge.md | 146 ++++++++++++++++++++++-- docs/content/instrumenting/histogram.md | 116 +++++++++++++++++-- docs/content/instrumenting/info.md | 75 +++++++++++- docs/content/instrumenting/labels.md | 33 +++++- docs/content/instrumenting/summary.md | 97 +++++++++++++++- docs/content/multiprocess/_index.md | 7 +- prometheus_client/exposition.py | 3 +- prometheus_client/registry.py | 9 +- pyproject.toml | 2 +- tests/test_asgi.py | 29 +++++ tests/test_core.py | 18 +++ tests/test_exposition.py | 7 ++ tests/test_multiprocess.py | 31 ++++- 18 files changed, 738 insertions(+), 142 deletions(-) delete mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index f29bd26..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,93 +0,0 @@ ---- -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 - mypy_lint: - executor: python - steps: - - checkout - - run: pip install tox - - run: tox -e mypy - test: - parameters: - python: - type: string - docker: - - image: cimg/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 "virtualenv<20.22.0" - - 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 - - mypy_lint - - test: - matrix: - parameters: - python: - - "3.9.18" - - "3.10" - - "3.11" - - "3.12" - - "3.13" - - "3.14" - - test_nooptionals: - matrix: - parameters: - python: - - "3.9" - - test_pypy: - matrix: - parameters: - python: - - "3.9" diff --git a/docs/content/exporting/http/_index.md b/docs/content/exporting/http/_index.md index dc1b8f2..f7a6aac 100644 --- a/docs/content/exporting/http/_index.md +++ b/docs/content/exporting/http/_index.md @@ -24,6 +24,7 @@ to shutdown the server gracefully: ```python server, t = start_http_server(8000) server.shutdown() +server.server_close() t.join() ``` diff --git a/docs/content/instrumenting/_index.md b/docs/content/instrumenting/_index.md index 13bbc6b..1b013d5 100644 --- a/docs/content/instrumenting/_index.md +++ b/docs/content/instrumenting/_index.md @@ -3,10 +3,20 @@ title: Instrumenting weight: 2 --- -Four types of metric are offered: Counter, Gauge, Summary and Histogram. -See the documentation on [metric types](http://prometheus.io/docs/concepts/metric_types/) +Six metric types are available. Pick based on what your value does: + +| Type | Update model | Use for | +|------|-----------|---------| +| [Counter](counter/) | only up | requests served, errors, bytes sent | +| [Gauge](gauge/) | up and down | queue depth, active connections, memory usage | +| [Histogram](histogram/) | observations in buckets | request latency, request size — when you need quantiles in queries | +| [Summary](summary/) | observations (count + sum) | request latency, request size — when average is enough | +| [Info](info/) | static key-value pairs | build version, environment metadata | +| [Enum](enum/) | one of N states | task state, lifecycle phase | + +See the Prometheus documentation on [metric types](https://prometheus.io/docs/concepts/metric_types/) and [instrumentation best practices](https://prometheus.io/docs/practices/instrumentation/#counter-vs-gauge-summary-vs-histogram) -on how to use them. +for deeper guidance on choosing between Histogram and Summary. ## Disabling `_created` metrics diff --git a/docs/content/instrumenting/counter.md b/docs/content/instrumenting/counter.md index 9461802..4876b61 100644 --- a/docs/content/instrumenting/counter.md +++ b/docs/content/instrumenting/counter.md @@ -3,8 +3,10 @@ title: Counter weight: 1 --- -Counters go up, and reset when the process restarts. +A Counter tracks a value that only ever goes up. Use it for things you count — requests +served, errors raised, bytes sent. When the process restarts, the counter resets to zero. +If your value can go down, use a [Gauge](../gauge/) instead. ```python from prometheus_client import Counter @@ -18,17 +20,110 @@ exposing the time series for counter, a `_total` suffix will be added. This is for compatibility between OpenMetrics and the Prometheus text format, as OpenMetrics requires the `_total` suffix. -There are utilities to count exceptions raised: +## Constructor + +```python +Counter(name, documentation, labelnames=(), namespace='', subsystem='', unit='', registry=REGISTRY) +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `name` | `str` | required | Metric name. A `_total` suffix is appended automatically when exposing the time series. | +| `documentation` | `str` | required | Help text shown in the `/metrics` output and Prometheus UI. | +| `labelnames` | `Iterable[str]` | `()` | Names of labels for this metric. See [Labels](../labels/). | +| `namespace` | `str` | `''` | Optional prefix. | +| `subsystem` | `str` | `''` | Optional middle component. | +| `unit` | `str` | `''` | Optional unit suffix appended to the metric name. | +| `registry` | `CollectorRegistry` | `REGISTRY` | Registry to register with. Pass `None` to skip registration, which is useful in tests where you create metrics without wanting them in the global registry. | + +`namespace`, `subsystem`, and `name` are joined with underscores to form the full metric name: + +```python +# namespace='myapp', subsystem='http', name='requests_total' +# produces: myapp_http_requests_total +Counter('requests_total', 'Total requests', namespace='myapp', subsystem='http') +``` + +## Methods + +### `inc(amount=1, exemplar=None)` + +Increment the counter by the given amount. The amount must be non-negative. + +```python +c.inc() # increment by 1 +c.inc(5) # increment by 5 +c.inc(0.7) # fractional increments are allowed +``` + +To attach trace context to an observation, pass an `exemplar` dict. Exemplars are +only rendered in OpenMetrics format. See [Exemplars](../exemplars/) for details. + +```python +c.inc(exemplar={'trace_id': 'abc123'}) +``` + +### `reset()` + +Reset the counter to zero. Use this when a logical process restarts without +restarting the actual Python process. + +```python +c.reset() +``` + +### `count_exceptions(exception=Exception)` + +Count exceptions raised in a block of code or function. Can be used as a +decorator or context manager. Increments the counter each time an exception +of the given type is raised. ```python @c.count_exceptions() def f(): - pass + pass with c.count_exceptions(): - pass + pass -# Count only one type of exception +# Count only a specific exception type with c.count_exceptions(ValueError): - pass -``` \ No newline at end of file + pass +``` + +## Labels + +See [Labels](../labels/) for how to use `.labels()`, `.remove()`, `.remove_by_labels()`, and `.clear()`. + +## Real-world example + +Tracking HTTP requests by method and status code in a web application: + +```python +from prometheus_client import Counter, start_http_server + +REQUESTS = Counter( + 'requests_total', + 'Total HTTP requests received', + labelnames=['method', 'status'], + namespace='myapp', +) +EXCEPTIONS = Counter( + 'exceptions_total', + 'Total unhandled exceptions', + labelnames=['handler'], + namespace='myapp', +) + +def handle_request(method, handler): + with EXCEPTIONS.labels(handler=handler).count_exceptions(): + # ... process the request ... + status = '200' + REQUESTS.labels(method=method, status=status).inc() + +if __name__ == '__main__': + start_http_server(8000) # exposes metrics at http://localhost:8000/metrics + # ... start your application ... +``` + +This produces time series like `myapp_requests_total{method="GET",status="200"}`. diff --git a/docs/content/instrumenting/enum.md b/docs/content/instrumenting/enum.md index 102091a..b1e6169 100644 --- a/docs/content/instrumenting/enum.md +++ b/docs/content/instrumenting/enum.md @@ -3,11 +3,95 @@ title: Enum weight: 6 --- -Enum tracks which of a set of states something is currently in. +Enum tracks which of a fixed set of states something is currently in. Only one state is active at a time. Use it for things like task state machines or lifecycle phases. ```python from prometheus_client import Enum e = Enum('my_task_state', 'Description of enum', states=['starting', 'running', 'stopped']) e.state('running') -``` \ No newline at end of file +``` + +Enum exposes one time series per state: +- `{=""}` — 1 if this is the current state, 0 otherwise + +The first listed state is the default. + +Note: Enum metrics do not work in multiprocess mode. + +## Constructor + +```python +Enum(name, documentation, labelnames=(), namespace='', subsystem='', unit='', registry=REGISTRY, states=[]) +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `name` | `str` | required | Metric name. | +| `documentation` | `str` | required | Help text shown in the `/metrics` output and Prometheus UI. | +| `labelnames` | `Iterable[str]` | `()` | Names of labels for this metric. See [Labels](../labels/). The metric name itself cannot be used as a label name. | +| `namespace` | `str` | `''` | Optional prefix. | +| `subsystem` | `str` | `''` | Optional middle component. | +| `unit` | `str` | `''` | Not supported — raises `ValueError`. Enum metrics cannot have a unit. | +| `registry` | `CollectorRegistry` | `REGISTRY` | Registry to register with. Pass `None` to skip registration, which is useful in tests where you create metrics without wanting them in the global registry. | +| `states` | `List[str]` | required | The complete list of valid states. Must be non-empty. The first entry is the initial state. | + +`namespace`, `subsystem`, and `name` are joined with underscores to form the full metric name: + +```python +# namespace='myapp', subsystem='worker', name='state' +# produces: myapp_worker_state +Enum('state', 'Worker state', states=['idle', 'running', 'error'], namespace='myapp', subsystem='worker') +``` + +## Methods + +### `state(state)` + +Set the current state. The value must be one of the strings passed in the `states` list. Raises `ValueError` if the state is not recognized. + +```python +e.state('running') +e.state('stopped') +``` + +## Labels + +See [Labels](../labels/) for how to use `.labels()`, `.remove()`, `.remove_by_labels()`, and `.clear()`. + +## Real-world example + +Tracking the lifecycle state of a background worker: + +```python +from prometheus_client import Enum, start_http_server + +WORKER_STATE = Enum( + 'worker_state', + 'Current state of the background worker', + states=['idle', 'running', 'error'], + namespace='myapp', +) + +def process_job(): + WORKER_STATE.state('running') + try: + # ... do work ... + pass + except Exception: + WORKER_STATE.state('error') + raise + finally: + WORKER_STATE.state('idle') + +if __name__ == '__main__': + start_http_server(8000) # exposes metrics at http://localhost:8000/metrics + # ... start your application ... +``` + +This produces: +``` +myapp_worker_state{myapp_worker_state="idle"} 0.0 +myapp_worker_state{myapp_worker_state="running"} 1.0 +myapp_worker_state{myapp_worker_state="error"} 0.0 +``` diff --git a/docs/content/instrumenting/gauge.md b/docs/content/instrumenting/gauge.md index 0b1529e..43168a6 100644 --- a/docs/content/instrumenting/gauge.md +++ b/docs/content/instrumenting/gauge.md @@ -3,7 +3,8 @@ title: Gauge weight: 2 --- -Gauges can go up and down. +A Gauge tracks a value that can go up and down. Use it for things you sample at a +point in time — active connections, queue depth, memory usage, temperature. ```python from prometheus_client import Gauge @@ -13,24 +14,145 @@ g.dec(10) # Decrement by given value g.set(4.2) # Set to a given value ``` -There are utilities for common use cases: +## Constructor ```python -g.set_to_current_time() # Set to current unixtime +Gauge(name, documentation, labelnames=(), namespace='', subsystem='', unit='', registry=REGISTRY, multiprocess_mode='all') +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `name` | `str` | required | Metric name. | +| `documentation` | `str` | required | Help text shown in the `/metrics` output and Prometheus UI. | +| `labelnames` | `Iterable[str]` | `()` | Names of labels for this metric. See [Labels](../labels/). | +| `namespace` | `str` | `''` | Optional prefix. | +| `subsystem` | `str` | `''` | Optional middle component. | +| `unit` | `str` | `''` | Optional unit suffix appended to the metric name. | +| `registry` | `CollectorRegistry` | `REGISTRY` | Registry to register with. Pass `None` to skip registration, which is useful in tests where you create metrics without wanting them in the global registry. | +| `multiprocess_mode` | `str` | `'all'` | How to aggregate this gauge across multiple processes. See [Multiprocess mode](../../multiprocess/). Options: `all`, `liveall`, `min`, `livemin`, `max`, `livemax`, `sum`, `livesum`, `mostrecent`, `livemostrecent`. | + +`namespace`, `subsystem`, and `name` are joined with underscores to form the full metric name: + +```python +# namespace='myapp', subsystem='db', name='connections_active' +# produces: myapp_db_connections_active +Gauge('connections_active', 'Active DB connections', namespace='myapp', subsystem='db') +``` + +## Methods + +### `inc(amount=1)` + +Increment the gauge by the given amount. + +```python +g.inc() # increment by 1 +g.inc(3) # increment by 3 +``` + +Note: raises `RuntimeError` if `multiprocess_mode` is `mostrecent` or `livemostrecent`. + +### `dec(amount=1)` + +Decrement the gauge by the given amount. + +```python +g.dec() # decrement by 1 +g.dec(3) # decrement by 3 +``` + +Note: raises `RuntimeError` if `multiprocess_mode` is `mostrecent` or `livemostrecent`. + +### `set(value)` + +Set the gauge to the given value. + +```python +g.set(42.5) +``` + +### `set_to_current_time()` + +Set the gauge to the current Unix timestamp in seconds. Useful for tracking +when an event last occurred. -# Increment when entered, decrement when exited. +```python +g.set_to_current_time() +``` + +### `track_inprogress()` + +Increment the gauge when a block of code or function is entered, and decrement +it when exited. Can be used as a decorator or context manager. + +```python @g.track_inprogress() -def f(): - pass +def process_job(): + pass with g.track_inprogress(): - pass + pass +``` + +### `time()` + +Set the gauge to the duration in seconds of the most recent execution of a +block of code or function. Unlike `Histogram.time()` and `Summary.time()`, +which accumulate all observations, this overwrites the gauge with the latest +duration each time. Can be used as a decorator or context manager. + +```python +@g.time() +def process(): + pass + +with g.time(): + pass +``` + +### `set_function(f)` + +Bind a callback function that returns the gauge value. The function is called +each time the metric is scraped. All other methods become no-ops after calling +this. + +```python +queue = [] +g.set_function(lambda: len(queue)) ``` -A Gauge can also take its value from a callback: +## Labels + +See [Labels](../labels/) for how to use `.labels()`, `.remove()`, `.remove_by_labels()`, and `.clear()`. + +## Real-world example + +Tracking active database connections and queue depth: ```python -d = Gauge('data_objects', 'Number of objects') -my_dict = {} -d.set_function(lambda: len(my_dict)) -``` \ No newline at end of file +from prometheus_client import Gauge, start_http_server + +ACTIVE_CONNECTIONS = Gauge( + 'connections_active', + 'Number of active database connections', + namespace='myapp', +) +QUEUE_SIZE = Gauge( + 'job_queue_size', + 'Number of jobs waiting in the queue', + namespace='myapp', +) + +job_queue = [] +QUEUE_SIZE.set_function(lambda: len(job_queue)) + +def acquire_connection(): + ACTIVE_CONNECTIONS.inc() + +def release_connection(): + ACTIVE_CONNECTIONS.dec() + +if __name__ == '__main__': + start_http_server(8000) # exposes metrics at http://localhost:8000/metrics + # ... start your application ... +``` diff --git a/docs/content/instrumenting/histogram.md b/docs/content/instrumenting/histogram.md index cb85f18..8975d85 100644 --- a/docs/content/instrumenting/histogram.md +++ b/docs/content/instrumenting/histogram.md @@ -3,8 +3,9 @@ title: Histogram weight: 4 --- -Histograms track the size and number of events in buckets. -This allows for aggregatable calculation of quantiles. +A Histogram samples observations and counts them in configurable buckets. Use it +when you want to track distributions — request latency, response sizes — and need +to calculate quantiles (p50, p95, p99) in your queries. ```python from prometheus_client import Histogram @@ -12,16 +13,113 @@ h = Histogram('request_latency_seconds', 'Description of histogram') h.observe(4.7) # Observe 4.7 (seconds in this case) ``` -The default buckets are intended to cover a typical web/rpc request from milliseconds to seconds. -They can be overridden by passing `buckets` keyword argument to `Histogram`. +A Histogram exposes three time series per metric: +- `_bucket{le=""}` — count of observations with value ≤ le (cumulative) +- `_sum` — sum of all observed values +- `_count` — total number of observations -There are utilities for timing code: +## Constructor + +```python +Histogram(name, documentation, labelnames=(), namespace='', subsystem='', unit='', registry=REGISTRY, buckets=DEFAULT_BUCKETS) +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `name` | `str` | required | Metric name. | +| `documentation` | `str` | required | Help text shown in the `/metrics` output and Prometheus UI. | +| `labelnames` | `Iterable[str]` | `()` | Names of labels for this metric. See [Labels](../labels/). Note: `le` is reserved and cannot be used as a label name. | +| `namespace` | `str` | `''` | Optional prefix. | +| `subsystem` | `str` | `''` | Optional middle component. | +| `unit` | `str` | `''` | Optional unit suffix appended to the metric name. | +| `registry` | `CollectorRegistry` | `REGISTRY` | Registry to register with. Pass `None` to skip registration, which is useful in tests where you create metrics without wanting them in the global registry. | +| `buckets` | `Sequence[float]` | `DEFAULT_BUCKETS` | Upper bounds of the histogram buckets. Must be in ascending order. `+Inf` is always appended automatically. | + +`namespace`, `subsystem`, and `name` are joined with underscores to form the full metric name: + +```python +# namespace='myapp', subsystem='http', name='request_duration_seconds' +# produces: myapp_http_request_duration_seconds +Histogram('request_duration_seconds', 'Latency', namespace='myapp', subsystem='http') +``` + +Default buckets are intended to cover typical web/RPC request latency in seconds and are +accessible as `Histogram.DEFAULT_BUCKETS`: + +``` +.005, .01, .025, .05, .075, .1, .25, .5, .75, 1.0, 2.5, 5.0, 7.5, 10.0, +Inf +``` + +To override with buckets tuned to your workload: + +```python +h = Histogram('request_latency_seconds', 'Latency', buckets=[.1, .5, 1, 2, 5]) +``` + +## Methods + +### `observe(amount, exemplar=None)` + +Record a single observation. The amount is typically positive or zero. + +```python +h.observe(0.43) # observe 430ms +``` + +To attach trace context to an observation, pass an `exemplar` dict. Exemplars are +only rendered in OpenMetrics format. See [Exemplars](../exemplars/) for details. + +```python +h.observe(0.43, exemplar={'trace_id': 'abc123'}) +``` + +### `time()` + +Observe the duration in seconds of a block of code or function and add it to the +histogram. Every call accumulates — unlike `Gauge.time()`, which only keeps the +most recent duration. Can be used as a decorator or context manager. ```python @h.time() -def f(): - pass +def process(): + pass with h.time(): - pass -``` \ No newline at end of file + pass +``` + +## Labels + +See [Labels](../labels/) for how to use `.labels()`, `.remove()`, `.remove_by_labels()`, and `.clear()`. + +## Real-world example + +Tracking HTTP request latency with custom buckets tuned to the workload: + +```python +from prometheus_client import Histogram, start_http_server + +REQUEST_LATENCY = Histogram( + 'request_duration_seconds', + 'HTTP request latency', + labelnames=['method', 'endpoint'], + namespace='myapp', + buckets=[.01, .05, .1, .25, .5, 1, 2.5, 5], +) + +def handle_request(method, endpoint): + with REQUEST_LATENCY.labels(method=method, endpoint=endpoint).time(): + # ... handle the request ... + pass + +if __name__ == '__main__': + start_http_server(8000) # exposes metrics at http://localhost:8000/metrics + # ... start your application ... +``` + +This produces time series like: +``` +myapp_request_duration_seconds_bucket{method="GET",endpoint="/api/users",le="0.1"} 42 +myapp_request_duration_seconds_sum{method="GET",endpoint="/api/users"} 3.7 +myapp_request_duration_seconds_count{method="GET",endpoint="/api/users"} 50 +``` diff --git a/docs/content/instrumenting/info.md b/docs/content/instrumenting/info.md index 6334d92..6e369de 100644 --- a/docs/content/instrumenting/info.md +++ b/docs/content/instrumenting/info.md @@ -3,10 +3,83 @@ title: Info weight: 5 --- -Info tracks key-value information, usually about a whole target. +Info tracks key-value pairs that describe a target — build version, configuration, or environment metadata. The values are static: once set, the metric outputs a single time series with all key-value pairs as labels and a constant value of 1. ```python from prometheus_client import Info i = Info('my_build_version', 'Description of info') i.info({'version': '1.2.3', 'buildhost': 'foo@bar'}) ``` + +Info exposes one time series per metric: +- `_info{="", ...}` — always 1; the key-value pairs become labels + +Note: Info metrics do not work in multiprocess mode. + +## Constructor + +```python +Info(name, documentation, labelnames=(), namespace='', subsystem='', unit='', registry=REGISTRY) +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `name` | `str` | required | Metric name. A `_info` suffix is appended automatically when exposing the time series. | +| `documentation` | `str` | required | Help text shown in the `/metrics` output and Prometheus UI. | +| `labelnames` | `Iterable[str]` | `()` | Names of labels for this metric. See [Labels](../labels/). Keys passed to `.info()` must not overlap with these label names. | +| `namespace` | `str` | `''` | Optional prefix. | +| `subsystem` | `str` | `''` | Optional middle component. | +| `unit` | `str` | `''` | Not supported — raises `ValueError`. Info metrics cannot have a unit. | +| `registry` | `CollectorRegistry` | `REGISTRY` | Registry to register with. Pass `None` to skip registration, which is useful in tests where you create metrics without wanting them in the global registry. | + +`namespace`, `subsystem`, and `name` are joined with underscores to form the full metric name: + +```python +# namespace='myapp', subsystem='http', name='build' +# produces: myapp_http_build_info +Info('build', 'Build information', namespace='myapp', subsystem='http') +``` + +## Methods + +### `info(val)` + +Set the key-value pairs for this metric. `val` must be a `dict[str, str]` — both keys and values must be strings. Keys must not overlap with the metric's label names and values cannot be `None`. Calling `info()` again overwrites the previous value. + +```python +i.info({'version': '1.4.2', 'revision': 'abc123', 'branch': 'main'}) +``` + +## Labels + +See [Labels](../labels/) for how to use `.labels()`, `.remove()`, `.remove_by_labels()`, and `.clear()`. + +## Real-world example + +Exposing application build metadata so dashboards can join on version: + +```python +from prometheus_client import Info, start_http_server + +BUILD_INFO = Info( + 'build', + 'Application build information', + namespace='myapp', +) + +BUILD_INFO.info({ + 'version': '1.4.2', + 'revision': 'abc123def456', + 'branch': 'main', + 'build_date': '2024-01-15', +}) + +if __name__ == '__main__': + start_http_server(8000) # exposes metrics at http://localhost:8000/metrics + # ... start your application ... +``` + +This produces: +``` +myapp_build_info{branch="main",build_date="2024-01-15",revision="abc123def456",version="1.4.2"} 1.0 +``` diff --git a/docs/content/instrumenting/labels.md b/docs/content/instrumenting/labels.md index ebf80b5..39ad29c 100644 --- a/docs/content/instrumenting/labels.md +++ b/docs/content/instrumenting/labels.md @@ -5,8 +5,8 @@ weight: 7 All metrics can have labels, allowing grouping of related time series. -See the best practices on [naming](http://prometheus.io/docs/practices/naming/) -and [labels](http://prometheus.io/docs/practices/instrumentation/#use-labels). +See the best practices on [naming](https://prometheus.io/docs/practices/naming/) +and [labels](https://prometheus.io/docs/practices/instrumentation/#use-labels). Taking a counter as an example: @@ -35,4 +35,33 @@ from prometheus_client import Counter c = Counter('my_requests_total', 'HTTP Failures', ['method', 'endpoint']) c.labels('get', '/') c.labels('post', '/submit') +``` + +## Removing labelsets + +### `remove(*labelvalues)` + +Remove a specific labelset from the metric. Values must be passed in the same +order as `labelnames` were declared. + +```python +c = Counter('my_requests_total', 'HTTP Failures', ['method', 'endpoint']) +c.labels('get', '/').inc() +c.remove('get', '/') +``` + +### `remove_by_labels(labels)` + +Remove all labelsets that partially match the given dict of label names and values. + +```python +c.remove_by_labels({'method': 'get'}) # removes all labelsets where method='get' +``` + +### `clear()` + +Remove all labelsets from the metric at once. + +```python +c.clear() ``` \ No newline at end of file diff --git a/docs/content/instrumenting/summary.md b/docs/content/instrumenting/summary.md index fa40749..55428ec 100644 --- a/docs/content/instrumenting/summary.md +++ b/docs/content/instrumenting/summary.md @@ -3,7 +3,12 @@ title: Summary weight: 3 --- -Summaries track the size and number of events. +A Summary samples observations and tracks the total count and sum. Use it when +you want to track the size or duration of events and compute averages, but do not +need per-bucket breakdown or quantiles in your Prometheus queries. + +The Python client does not compute quantiles locally. If you need p50/p95/p99, +use a [Histogram](../histogram/) instead. ```python from prometheus_client import Summary @@ -11,15 +16,95 @@ s = Summary('request_latency_seconds', 'Description of summary') s.observe(4.7) # Observe 4.7 (seconds in this case) ``` -There are utilities for timing code: +A Summary exposes two time series per metric: +- `_count` — total number of observations +- `_sum` — sum of all observed values + +## Constructor + +```python +Summary(name, documentation, labelnames=(), namespace='', subsystem='', unit='', registry=REGISTRY) +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `name` | `str` | required | Metric name. | +| `documentation` | `str` | required | Help text shown in the `/metrics` output and Prometheus UI. | +| `labelnames` | `Iterable[str]` | `()` | Names of labels for this metric. See [Labels](../labels/). Note: `quantile` is reserved and cannot be used as a label name. | +| `namespace` | `str` | `''` | Optional prefix. | +| `subsystem` | `str` | `''` | Optional middle component. | +| `unit` | `str` | `''` | Optional unit suffix appended to the metric name. | +| `registry` | `CollectorRegistry` | `REGISTRY` | Registry to register with. Pass `None` to skip registration, which is useful in tests where you create metrics without wanting them in the global registry. | + +`namespace`, `subsystem`, and `name` are joined with underscores to form the full metric name: + +```python +# namespace='myapp', subsystem='worker', name='task_duration_seconds' +# produces: myapp_worker_task_duration_seconds +Summary('task_duration_seconds', 'Task duration', namespace='myapp', subsystem='worker') +``` + +## Methods + +### `observe(amount)` + +Record a single observation. The amount is typically positive or zero. + +```python +s.observe(0.43) # observe 430ms +s.observe(1024) # observe 1024 bytes +``` + +### `time()` + +Observe the duration in seconds of a block of code or function and add it to the +summary. Every call accumulates — unlike `Gauge.time()`, which only keeps the +most recent duration. Can be used as a decorator or context manager. ```python @s.time() -def f(): - pass +def process(): + pass with s.time(): - pass + pass +``` + +## Labels + +See [Labels](../labels/) for how to use `.labels()`, `.remove()`, `.remove_by_labels()`, and `.clear()`. + +## Real-world example + +Tracking the duration of background tasks: + +```python +from prometheus_client import Summary, start_http_server + +TASK_DURATION = Summary( + 'task_duration_seconds', + 'Time spent processing background tasks', + labelnames=['task_type'], + namespace='myapp', +) + +def run_task(task_type, task): + with TASK_DURATION.labels(task_type=task_type).time(): + # ... run the task ... + pass + +if __name__ == '__main__': + start_http_server(8000) # exposes metrics at http://localhost:8000/metrics + # ... start your application ... +``` + +This produces: +``` +myapp_task_duration_seconds_count{task_type="email"} 120 +myapp_task_duration_seconds_sum{task_type="email"} 48.3 ``` -The Python client doesn't store or expose quantile information at this time. \ No newline at end of file +You can compute the average duration in PromQL as: +``` +rate(myapp_task_duration_seconds_sum[5m]) / rate(myapp_task_duration_seconds_count[5m]) +``` diff --git a/docs/content/multiprocess/_index.md b/docs/content/multiprocess/_index.md index 33507cd..42ea6a6 100644 --- a/docs/content/multiprocess/_index.md +++ b/docs/content/multiprocess/_index.md @@ -10,9 +10,12 @@ it's common to have processes rather than threads to handle large workloads. To handle this the client library can be put in multiprocess mode. This comes with a number of limitations: -- Registries can not be used as normal, all instantiated metrics are exported +- Registries can not be used as normal: + - all instantiated metrics are collected - Registering metrics to a registry later used by a `MultiProcessCollector` may cause duplicate metrics to be exported + - Filtering on metrics works if and only if the constructor was called with + `support_collectors_without_names=True` and it but might be inefficient. - Custom collectors do not work (e.g. cpu and memory metrics) - Gauges cannot use `set_function` - Info and Enum metrics do not work @@ -49,7 +52,7 @@ MY_COUNTER = Counter('my_counter', 'Description of my counter') # Expose metrics. def app(environ, start_response): - registry = CollectorRegistry() + registry = CollectorRegistry(support_collectors_without_names=True) multiprocess.MultiProcessCollector(registry) data = generate_latest(registry) status = '200 OK' diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py index ca06d91..2d402a0 100644 --- a/prometheus_client/exposition.py +++ b/prometheus_client/exposition.py @@ -783,8 +783,9 @@ def _escape_grouping_key(k, v): if v == "": # Per https://github.com/prometheus/pushgateway/pull/346. return k + "@base64", "=" - elif '/' in v: + elif '/' in v or ' ' in v: # Added in Pushgateway 0.9.0. + # Use base64 encoding for values containing slashes or spaces return k + "@base64", base64.urlsafe_b64encode(v.encode("utf-8")).decode("utf-8") else: return k, quote_plus(v) diff --git a/prometheus_client/registry.py b/prometheus_client/registry.py index 9934117..c2b55d1 100644 --- a/prometheus_client/registry.py +++ b/prometheus_client/registry.py @@ -23,12 +23,15 @@ class CollectorRegistry: exposition formats. """ - def __init__(self, auto_describe: bool = False, target_info: Optional[Dict[str, str]] = None): + def __init__(self, auto_describe: bool = False, target_info: Optional[Dict[str, str]] = None, + support_collectors_without_names: bool = False): self._collector_to_names: Dict[Collector, List[str]] = {} self._names_to_collectors: Dict[str, Collector] = {} self._auto_describe = auto_describe self._lock = Lock() self._target_info: Optional[Dict[str, str]] = {} + self._support_collectors_without_names = support_collectors_without_names + self._collectors_without_names: List[Collector] = [] self.set_target_info(target_info) def register(self, collector: Collector) -> None: @@ -43,6 +46,8 @@ def register(self, collector: Collector) -> None: for name in names: self._names_to_collectors[name] = collector self._collector_to_names[collector] = names + if self._support_collectors_without_names and not names: + self._collectors_without_names.append(collector) def unregister(self, collector: Collector) -> None: """Remove a collector from the registry.""" @@ -145,7 +150,7 @@ def __init__(self, names: Iterable[str], registry: CollectorRegistry): self._registry = registry def collect(self) -> Iterable[Metric]: - collectors = set() + collectors = set(self._registry._collectors_without_names) target_info_metric = None with self._registry._lock: if 'target_info' in self._name_set and self._registry._target_info: diff --git a/pyproject.toml b/pyproject.toml index ed3ef38..336cfb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "prometheus_client" -version = "0.24.1" +version = "0.25.0" description = "Python client for the Prometheus monitoring system." readme = "README.md" license = "Apache-2.0 AND BSD-2-Clause" diff --git a/tests/test_asgi.py b/tests/test_asgi.py index d4933ce..6e795e2 100644 --- a/tests/test_asgi.py +++ b/tests/test_asgi.py @@ -223,3 +223,32 @@ def test_qs_parsing(self): asyncio.new_event_loop().run_until_complete( self.communicator.wait() ) + + def test_qs_parsing_multi(self): + """Only metrics that match the 'name[]' query string param appear""" + + app = make_asgi_app(self.registry) + metrics = [ + ("asdf", "first test metric", 1), + ("bsdf", "second test metric", 2), + ("csdf", "third test metric", 3) + ] + + for m in metrics: + self.increment_metrics(*m) + + self.seed_app(app) + self.scope['query_string'] = "&".join([f"name[]={m[0]}_total" for m in metrics[0:2]]).encode("utf-8") + self.send_default_request() + + outputs = self.get_all_output() + response_body = outputs[1] + output = response_body['body'].decode('utf8') + + self.assert_metrics(output, *metrics[0]) + self.assert_metrics(output, *metrics[1]) + self.assert_not_metrics(output, *metrics[2]) + + asyncio.new_event_loop().run_until_complete( + self.communicator.wait() + ) diff --git a/tests/test_core.py b/tests/test_core.py index c7c9c14..66492c6 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1024,6 +1024,24 @@ def test_restricted_registry_does_not_call_extra(self): self.assertEqual([m], list(registry.restricted_registry(['s_sum']).collect())) mock_collector.collect.assert_not_called() + def test_restricted_registry_ignore_no_names_collectors(self): + from unittest.mock import MagicMock + registry = CollectorRegistry() + mock_collector = MagicMock() + mock_collector.describe.return_value = [] + registry.register(mock_collector) + self.assertEqual(list(registry.restricted_registry(['metric']).collect()), []) + mock_collector.collect.assert_not_called() + + def test_restricted_registry_collects_no_names_collectors(self): + from unittest.mock import MagicMock + registry = CollectorRegistry(support_collectors_without_names=True) + mock_collector = MagicMock() + mock_collector.describe.return_value = [] + registry.register(mock_collector) + self.assertEqual(list(registry.restricted_registry(['metric']).collect()), []) + mock_collector.collect.assert_called() + def test_restricted_registry_does_not_yield_while_locked(self): registry = CollectorRegistry(target_info={'foo': 'bar'}) Summary('s', 'help', registry=registry).observe(7) diff --git a/tests/test_exposition.py b/tests/test_exposition.py index aceff73..a3c9782 100644 --- a/tests/test_exposition.py +++ b/tests/test_exposition.py @@ -301,6 +301,13 @@ def test_push_with_groupingkey_empty_label(self): self.assertEqual(self.requests[0][0].headers.get('content-type'), CONTENT_TYPE_PLAIN_0_0_4) self.assertEqual(self.requests[0][1], b'# HELP g help\n# TYPE g gauge\ng 0.0\n') + def test_push_with_groupingkey_with_spaces(self): + push_to_gateway(self.address, "my_job", self.registry, {'label': 'value with spaces'}) + self.assertEqual(self.requests[0][0].command, 'PUT') + self.assertEqual(self.requests[0][0].path, '/metrics/job/my_job/label@base64/dmFsdWUgd2l0aCBzcGFjZXM=') + self.assertEqual(self.requests[0][0].headers.get('content-type'), CONTENT_TYPE_PLAIN_0_0_4) + self.assertEqual(self.requests[0][1], b'# HELP g help\n# TYPE g gauge\ng 0.0\n') + def test_push_with_complex_groupingkey(self): push_to_gateway(self.address, "my_job", self.registry, {'a': 9, 'b': 'a/ z'}) self.assertEqual(self.requests[0][0].command, 'PUT') diff --git a/tests/test_multiprocess.py b/tests/test_multiprocess.py index c2f71d2..ee0c742 100644 --- a/tests/test_multiprocess.py +++ b/tests/test_multiprocess.py @@ -52,7 +52,7 @@ def setUp(self): self.tempdir = tempfile.mkdtemp() os.environ['PROMETHEUS_MULTIPROC_DIR'] = self.tempdir values.ValueClass = MultiProcessValue(lambda: 123) - self.registry = CollectorRegistry() + self.registry = CollectorRegistry(support_collectors_without_names=True) self.collector = MultiProcessCollector(self.registry) @property @@ -358,6 +358,35 @@ def add_label(key, value): self.assertEqual(metrics['h'].samples, expected_histogram) + def test_restrict(self): + pid = 0 + values.ValueClass = MultiProcessValue(lambda: pid) + labels = {i: i for i in 'abcd'} + + def add_label(key, value): + l = labels.copy() + l[key] = value + return l + + c = Counter('c', 'help', labelnames=labels.keys(), registry=None) + g = Gauge('g', 'help', labelnames=labels.keys(), registry=None) + + c.labels(**labels).inc(1) + g.labels(**labels).set(1) + + pid = 1 + + c.labels(**labels).inc(1) + g.labels(**labels).set(1) + + metrics = {m.name: m for m in self.registry.restricted_registry(['c_total']).collect()} + + self.assertEqual(metrics.keys(), {'c'}) + + self.assertEqual( + metrics['c'].samples, [Sample('c_total', labels, 2.0)] + ) + def test_collect_preserves_help(self): pid = 0 values.ValueClass = MultiProcessValue(lambda: pid) From 474f36c716ab44b8945f00e043171963578b3891 Mon Sep 17 00:00:00 2001 From: Brad Cowie Date: Thu, 16 Apr 2026 10:14:14 +1200 Subject: [PATCH 2/2] Update d/p/0002-Update-pyproject.toml.patch --- debian/patches/0002-Update-pyproject.toml.patch | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/patches/0002-Update-pyproject.toml.patch b/debian/patches/0002-Update-pyproject.toml.patch index df7b84b..fe631fd 100644 --- a/debian/patches/0002-Update-pyproject.toml.patch +++ b/debian/patches/0002-Update-pyproject.toml.patch @@ -3,7 +3,7 @@ Index: python3-prometheus-client/pyproject.toml --- python3-prometheus-client.orig/pyproject.toml +++ python3-prometheus-client/pyproject.toml @@ -7,11 +7,7 @@ name = "prometheus_client" - version = "0.24.1" + version = "0.25.0" description = "Python client for the Prometheus monitoring system." readme = "README.md" -license = "Apache-2.0 AND BSD-2-Clause"