diff --git a/.github/workflows/approve-workflows.yml b/.github/workflows/approve-workflows.yml
new file mode 100644
index 00000000..f372012f
--- /dev/null
+++ b/.github/workflows/approve-workflows.yml
@@ -0,0 +1,27 @@
+---
+###
+# This action is synced from https://github.com/prometheus/prometheus
+###
+name: Approve pending workflows
+
+on:
+ issue_comment:
+ types: [created]
+
+permissions: read-all
+
+jobs:
+ approve:
+ if: >-
+ github.event.issue.pull_request &&
+ github.event.comment.body == '/workflow-approve' &&
+ (github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community')
+ runs-on: ubuntu-latest
+ permissions:
+ actions: write
+ contents: read
+ pull-requests: write
+ steps:
+ - uses: prometheus/promci/approve_workflows@370e8c15dcec50043cbe66f2f34633d9efc0a190 # v0.9.0
+ with:
+ github_token: ${{ github.token }}
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index a7e4e094..f925b88c 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -108,3 +108,17 @@ jobs:
run: tox
env:
TOXENV: pypy${{ env.PYTHON_VERSION }}
+
+ test_windows:
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ - name: Set up Python
+ uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
+ with:
+ python-version: '3.12'
+ - name: Install dependencies
+ run: pip install tox "virtualenv<20.22.0"
+ - name: Run tests
+ run: python -m tox -e py3.12
+
diff --git a/SECURITY.md b/SECURITY.md
index fed02d85..5e6f976d 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -3,4 +3,4 @@
The Prometheus security policy, including how to report vulnerabilities, can be
found here:
-
+[https://prometheus.io/docs/operating/security/](https://prometheus.io/docs/operating/security/)
diff --git a/docs/content/collector/_index.md b/docs/content/collector/_index.md
index 957c8ba9..85c6f12f 100644
--- a/docs/content/collector/_index.md
+++ b/docs/content/collector/_index.md
@@ -18,8 +18,8 @@ ProcessCollector(namespace='mydaemon', pid=lambda: open('/var/run/daemon.pid').r
# Platform Collector
The client also automatically exports some metadata about Python. If using Jython,
-metadata about the JVM in use is also included. This information is available as
-labels on the `python_info` metric. The value of the metric is 1, since it is the
+metadata about the JVM in use is also included. This information is available as
+labels on the `python_info` metric. The value of the metric is 1, since it is the
labels that carry information.
# Disabling Default Collector metrics
@@ -33,4 +33,75 @@ import prometheus_client
prometheus_client.REGISTRY.unregister(prometheus_client.GC_COLLECTOR)
prometheus_client.REGISTRY.unregister(prometheus_client.PLATFORM_COLLECTOR)
prometheus_client.REGISTRY.unregister(prometheus_client.PROCESS_COLLECTOR)
-```
\ No newline at end of file
+```
+
+## API Reference
+
+### ProcessCollector
+
+```python
+ProcessCollector(namespace='', pid=lambda: 'self', proc='/proc', registry=REGISTRY)
+```
+
+Collects process metrics from `/proc`. Only available on Linux.
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `namespace` | `str` | `''` | Prefix added to all metric names, e.g. `'mydaemon'` produces `mydaemon_process_cpu_seconds_total`. |
+| `pid` | `Callable[[], int or str]` | `lambda: 'self'` | Callable that returns the PID to monitor. `'self'` monitors the current process. |
+| `proc` | `str` | `'/proc'` | Path to the proc filesystem. Useful for testing or containerised environments with a non-standard mount point. |
+| `registry` | `CollectorRegistry` | `REGISTRY` | Registry to register with. Pass `None` to skip registration. |
+
+Metrics exported:
+
+| Metric | Description |
+|--------|-------------|
+| `process_cpu_seconds_total` | Total user and system CPU time in seconds. |
+| `process_virtual_memory_bytes` | Virtual memory size in bytes. |
+| `process_resident_memory_bytes` | Resident memory size in bytes. |
+| `process_start_time_seconds` | Start time since Unix epoch in seconds. |
+| `process_open_fds` | Number of open file descriptors. |
+| `process_max_fds` | Maximum number of open file descriptors. |
+
+The module-level `PROCESS_COLLECTOR` is the default instance registered with `REGISTRY`.
+
+### PlatformCollector
+
+```python
+PlatformCollector(registry=REGISTRY, platform=None)
+```
+
+Exports Python runtime metadata as a `python_info` gauge metric with labels.
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `registry` | `CollectorRegistry` | `REGISTRY` | Registry to register with. Pass `None` to skip registration. |
+| `platform` | module | `None` | Override the `platform` module. Intended for testing. |
+
+Labels on `python_info`: `version`, `implementation`, `major`, `minor`, `patchlevel`.
+On Jython, additional labels are added: `jvm_version`, `jvm_release`, `jvm_vendor`, `jvm_name`.
+
+The module-level `PLATFORM_COLLECTOR` is the default instance registered with `REGISTRY`.
+
+### GCCollector
+
+```python
+GCCollector(registry=REGISTRY)
+```
+
+Exports Python garbage collector statistics. Only active on CPython (skipped silently on
+other implementations).
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `registry` | `CollectorRegistry` | `REGISTRY` | Registry to register with. |
+
+Metrics exported:
+
+| Metric | Description |
+|--------|-------------|
+| `python_gc_objects_collected_total` | Objects collected during GC, by generation. |
+| `python_gc_objects_uncollectable_total` | Uncollectable objects found during GC, by generation. |
+| `python_gc_collections_total` | Number of times each generation was collected. |
+
+The module-level `GC_COLLECTOR` is the default instance registered with `REGISTRY`.
diff --git a/docs/content/collector/custom.md b/docs/content/collector/custom.md
index bc6a021c..c1979109 100644
--- a/docs/content/collector/custom.md
+++ b/docs/content/collector/custom.md
@@ -35,4 +35,265 @@ not implemented and the CollectorRegistry was created with `auto_describe=True`
(which is the case for the default registry) then `collect` will be called at
registration time instead of `describe`. If this could cause problems, either
implement a proper `describe`, or if that's not practical have `describe`
-return an empty list.
\ No newline at end of file
+return an empty list.
+
+## Collector protocol
+
+A collector is any object that implements a `collect` method. Optionally it
+can also implement `describe`.
+
+### `collect()`
+
+Returns an iterable of metric family objects (`GaugeMetricFamily`,
+`CounterMetricFamily`, etc.). Called every time the registry is scraped.
+
+Using `yield` is the idiomatic way to implement `collect()` — it turns the method
+into a generator, which the registry iterates lazily without building an intermediate
+list first. Each scrape calls `collect()` fresh, so no state carries over between
+scrapes.
+
+### `describe()`
+
+Returns an iterable of metric family objects used only to determine the metric
+names the collector produces. Samples on the returned objects are ignored. If
+not implemented and the registry has `auto_describe=True`, `collect` is called
+at registration time instead.
+
+## value vs labels
+
+Every metric family constructor accepts either inline data or `labels`, but not
+both. The inline data parameter name varies by type: `value` for Gauge, Counter,
+and Info; `count_value`/`sum_value` for Summary; `buckets` for Histogram.
+
+- Pass inline data to emit a single unlabelled metric directly from the constructor.
+- Pass `labels` (a sequence of label names) and then call `add_metric` one or
+ more times to emit labelled metrics.
+
+```python
+# single unlabelled value
+GaugeMetricFamily('my_gauge', 'Help text', value=7)
+
+# labelled metrics via add_metric
+g = GaugeMetricFamily('my_gauge', 'Help text', labels=['region'])
+g.add_metric(['us-east-1'], 3)
+g.add_metric(['eu-west-1'], 5)
+```
+
+## API Reference
+
+The examples below show usage inside a `collect()` method body. Each snippet is
+meant to be placed within a custom collector class as shown in the example at the
+top of this page.
+
+### GaugeMetricFamily
+
+```python
+GaugeMetricFamily(name, documentation, value=None, labels=None, unit='')
+```
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `name` | `str` | required | Metric name. |
+| `documentation` | `str` | required | Help text shown in the `/metrics` output. |
+| `value` | `float` | `None` | Emit a single unlabelled sample with this value. Mutually exclusive with `labels`. |
+| `labels` | `Sequence[str]` | `None` | Label names. Use with `add_metric`. Mutually exclusive with `value`. |
+| `unit` | `str` | `''` | Optional unit suffix appended to the metric name. |
+
+#### `add_metric(labels, value, timestamp=None)`
+
+Add a labelled sample to the metric family.
+
+| Parameter | Type | Description |
+|-----------|------|-------------|
+| `labels` | `Sequence[str]` | Label values in the same order as the `labels` constructor argument. |
+| `value` | `float` | The gauge value. |
+| `timestamp` | `float` or `Timestamp` | Optional Unix timestamp for the sample. |
+
+```python
+g = GaugeMetricFamily('temperature_celsius', 'Temperature by location', labels=['location'])
+g.add_metric(['living_room'], 21.5)
+g.add_metric(['basement'], 18.0)
+yield g
+```
+
+### CounterMetricFamily
+
+```python
+CounterMetricFamily(name, documentation, value=None, labels=None, created=None, unit='', exemplar=None)
+```
+
+If `name` ends with `_total`, the suffix is stripped automatically so the
+metric is stored without it and the `_total` suffix is added on exposition.
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `name` | `str` | required | Metric name. A trailing `_total` is stripped and re-added on exposition. |
+| `documentation` | `str` | required | Help text. |
+| `value` | `float` | `None` | Emit a single unlabelled sample. Mutually exclusive with `labels`. |
+| `labels` | `Sequence[str]` | `None` | Label names. Use with `add_metric`. Mutually exclusive with `value`. |
+| `created` | `float` | `None` | Unix timestamp the counter was created at. Only used when `value` is set. |
+| `unit` | `str` | `''` | Optional unit suffix. |
+| `exemplar` | `Exemplar` | `None` | Exemplar for the single-value form. Only used when `value` is set. |
+
+#### `add_metric(labels, value, created=None, timestamp=None, exemplar=None)`
+
+| Parameter | Type | Description |
+|-----------|------|-------------|
+| `labels` | `Sequence[str]` | Label values. |
+| `value` | `float` | The counter value. |
+| `created` | `float` | Optional Unix timestamp the counter was created at. |
+| `timestamp` | `float` or `Timestamp` | Optional Unix timestamp for the sample. |
+| `exemplar` | `Exemplar` | Optional exemplar. See [Exemplars](../../instrumenting/exemplars/). |
+
+```python
+c = CounterMetricFamily('http_requests_total', 'HTTP requests by status', labels=['status'])
+c.add_metric(['200'], 1200)
+c.add_metric(['404'], 43)
+c.add_metric(['500'], 7)
+yield c
+```
+
+### SummaryMetricFamily
+
+```python
+SummaryMetricFamily(name, documentation, count_value=None, sum_value=None, labels=None, unit='')
+```
+
+`count_value` and `sum_value` must always be provided together or not at all.
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `name` | `str` | required | Metric name. |
+| `documentation` | `str` | required | Help text. |
+| `count_value` | `int` | `None` | Observation count for a single unlabelled metric. Must be paired with `sum_value`. |
+| `sum_value` | `float` | `None` | Observation sum for a single unlabelled metric. Must be paired with `count_value`. |
+| `labels` | `Sequence[str]` | `None` | Label names. Use with `add_metric`. Mutually exclusive with `count_value`/`sum_value`. |
+| `unit` | `str` | `''` | Optional unit suffix. |
+
+#### `add_metric(labels, count_value, sum_value, timestamp=None)`
+
+| Parameter | Type | Description |
+|-----------|------|-------------|
+| `labels` | `Sequence[str]` | Label values. |
+| `count_value` | `int` | The number of observations. |
+| `sum_value` | `float` | The sum of all observed values. |
+| `timestamp` | `float` or `Timestamp` | Optional Unix timestamp for the sample. |
+
+```python
+s = SummaryMetricFamily('rpc_duration_seconds', 'RPC duration', labels=['method'])
+s.add_metric(['get'], count_value=1000, sum_value=53.2)
+s.add_metric(['put'], count_value=400, sum_value=28.7)
+yield s
+```
+
+### HistogramMetricFamily
+
+```python
+HistogramMetricFamily(name, documentation, buckets=None, sum_value=None, labels=None, unit='')
+```
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `name` | `str` | required | Metric name. |
+| `documentation` | `str` | required | Help text. |
+| `buckets` | `Sequence` | `None` | Bucket data for a single unlabelled metric. Each entry is a `(le, value)` pair or `(le, value, exemplar)` triple. Must include a `+Inf` bucket. Mutually exclusive with `labels`. |
+| `sum_value` | `float` | `None` | Observation sum. Cannot be set without `buckets`. Omitted for histograms with negative buckets. |
+| `labels` | `Sequence[str]` | `None` | Label names. Use with `add_metric`. Mutually exclusive with `buckets`. |
+| `unit` | `str` | `''` | Optional unit suffix. |
+
+#### `add_metric(labels, buckets, sum_value, timestamp=None)`
+
+| Parameter | Type | Description |
+|-----------|------|-------------|
+| `labels` | `Sequence[str]` | Label values. |
+| `buckets` | `Sequence` | Bucket data. Each entry is a `(le, value)` pair or `(le, value, exemplar)` triple. Must be sorted and include `+Inf`. |
+| `sum_value` | `float` or `None` | The sum of all observed values. Pass `None` for histograms with negative buckets. |
+| `timestamp` | `float` or `Timestamp` | Optional Unix timestamp. |
+
+```python
+h = HistogramMetricFamily('request_size_bytes', 'Request sizes', labels=['handler'])
+h.add_metric(
+ ['api'],
+ buckets=[('100', 5), ('1000', 42), ('+Inf', 50)],
+ sum_value=18350.0,
+)
+yield h
+```
+
+### InfoMetricFamily
+
+```python
+InfoMetricFamily(name, documentation, value=None, labels=None)
+```
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `name` | `str` | required | Metric name. The `_info` suffix is added automatically on exposition. |
+| `documentation` | `str` | required | Help text. |
+| `value` | `Dict[str, str]` | `None` | Key-value label pairs for a single unlabelled info metric. Mutually exclusive with `labels`. |
+| `labels` | `Sequence[str]` | `None` | Label names for the outer grouping. Use with `add_metric`. Mutually exclusive with `value`. |
+
+#### `add_metric(labels, value, timestamp=None)`
+
+| Parameter | Type | Description |
+|-----------|------|-------------|
+| `labels` | `Sequence[str]` | Outer label values (from the `labels` constructor argument). |
+| `value` | `Dict[str, str]` | Key-value label pairs that form the info payload. |
+| `timestamp` | `float` or `Timestamp` | Optional Unix timestamp. |
+
+Single unlabelled info metric:
+
+```python
+yield InfoMetricFamily('build', 'Build metadata', value={'version': '1.2.3', 'commit': 'abc123'})
+```
+
+Labelled — one info metric per service:
+
+```python
+i = InfoMetricFamily('service_build', 'Per-service build info', labels=['service'])
+i.add_metric(['auth'], {'version': '2.0.1', 'commit': 'def456'})
+i.add_metric(['api'], {'version': '1.9.0', 'commit': 'ghi789'})
+yield i
+```
+
+## Real-world example
+
+Proxying metrics from an external source:
+
+```python
+from prometheus_client.core import CounterMetricFamily, GaugeMetricFamily, REGISTRY
+from prometheus_client.registry import Collector
+from prometheus_client import start_http_server
+
+# Simulated external data source
+_QUEUE_STATS = {
+ 'orders': {'depth': 14, 'processed': 9821},
+ 'notifications': {'depth': 3, 'processed': 45210},
+}
+
+class QueueCollector(Collector):
+ def collect(self):
+ depth = GaugeMetricFamily(
+ 'queue_depth',
+ 'Current number of messages waiting in the queue',
+ labels=['queue'],
+ )
+ processed = CounterMetricFamily(
+ 'queue_messages_processed_total',
+ 'Total messages processed from the queue',
+ labels=['queue'],
+ )
+ for name, stats in _QUEUE_STATS.items():
+ depth.add_metric([name], stats['depth'])
+ processed.add_metric([name], stats['processed'])
+ yield depth
+ yield processed
+
+REGISTRY.register(QueueCollector())
+
+if __name__ == '__main__':
+ start_http_server(8000)
+ import time
+ while True:
+ time.sleep(1)
+```
diff --git a/docs/content/exporting/http/fastapi-gunicorn.md b/docs/content/exporting/http/fastapi-gunicorn.md
index 148a36d7..6d0ec329 100644
--- a/docs/content/exporting/http/fastapi-gunicorn.md
+++ b/docs/content/exporting/http/fastapi-gunicorn.md
@@ -23,7 +23,7 @@ For Multiprocessing support, use this modified code snippet. Full multiprocessin
```python
from fastapi import FastAPI
-from prometheus_client import make_asgi_app
+from prometheus_client import make_asgi_app, CollectorRegistry, multiprocess
app = FastAPI(debug=False)
diff --git a/docs/content/exporting/pushgateway.md b/docs/content/exporting/pushgateway.md
index d9f9a945..6060c0bf 100644
--- a/docs/content/exporting/pushgateway.md
+++ b/docs/content/exporting/pushgateway.md
@@ -85,3 +85,109 @@ g = Gauge('job_last_success_unixtime', 'Last time a batch job successfully finis
g.set_to_current_time()
push_to_gateway('localhost:9091', job='batchA', registry=registry, handler=my_auth_handler)
```
+
+## API Reference
+
+### `push_to_gateway(gateway, job, registry, grouping_key=None, timeout=30, handler=default_handler, compression=None)`
+
+Pushes metrics to the pushgateway, replacing all metrics with the same job and grouping key.
+Uses the HTTP `PUT` method.
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `gateway` | `str` | required | URL of the pushgateway. If no scheme is provided, `http://` is assumed. |
+| `job` | `str` | required | Value for the `job` label attached to all pushed metrics. |
+| `registry` | `Collector` | required | Registry whose metrics are pushed. Typically a `CollectorRegistry` instance. |
+| `grouping_key` | `Optional[Dict[str, Any]]` | `None` | Additional labels to identify the group. See the [Pushgateway documentation](https://github.com/prometheus/pushgateway/blob/master/README.md) for details. |
+| `timeout` | `Optional[float]` | `30` | Seconds before the request is aborted. Pass `None` for no timeout. |
+| `handler` | `Callable` | `default_handler` | Function that performs the HTTP request. See [Handlers](#handlers) below. |
+| `compression` | `Optional[str]` | `None` | Compress the payload before sending. Accepts `'gzip'` or `'snappy'`. Snappy requires the [`python-snappy`](https://github.com/andrix/python-snappy) package. |
+
+### `pushadd_to_gateway(gateway, job, registry, grouping_key=None, timeout=30, handler=default_handler, compression=None)`
+
+Pushes metrics to the pushgateway, replacing only metrics with the same name, job, and grouping key.
+Uses the HTTP `POST` method.
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `gateway` | `str` | required | URL of the pushgateway. |
+| `job` | `str` | required | Value for the `job` label attached to all pushed metrics. |
+| `registry` | `Optional[Collector]` | required | Registry whose metrics are pushed. Pass `None` to use the default `REGISTRY`. |
+| `grouping_key` | `Optional[Dict[str, Any]]` | `None` | Additional labels to identify the group. |
+| `timeout` | `Optional[float]` | `30` | Seconds before the request is aborted. Pass `None` for no timeout. |
+| `handler` | `Callable` | `default_handler` | Function that performs the HTTP request. |
+| `compression` | `Optional[str]` | `None` | Compress the payload. Accepts `'gzip'` or `'snappy'`. |
+
+### `delete_from_gateway(gateway, job, grouping_key=None, timeout=30, handler=default_handler)`
+
+Deletes metrics from the pushgateway for the given job and grouping key.
+Uses the HTTP `DELETE` method. Has no `registry` or `compression` parameters.
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `gateway` | `str` | required | URL of the pushgateway. |
+| `job` | `str` | required | Value for the `job` label identifying the group to delete. |
+| `grouping_key` | `Optional[Dict[str, Any]]` | `None` | Additional labels to identify the group. |
+| `timeout` | `Optional[float]` | `30` | Seconds before the request is aborted. Pass `None` for no timeout. |
+| `handler` | `Callable` | `default_handler` | Function that performs the HTTP request. |
+
+### `instance_ip_grouping_key()`
+
+Returns a grouping key dict with the `instance` label set to the IP address of the current host.
+Takes no parameters.
+
+```python
+from prometheus_client.exposition import instance_ip_grouping_key
+
+push_to_gateway('localhost:9091', job='batchA', registry=registry,
+ grouping_key=instance_ip_grouping_key())
+```
+
+## Handlers
+
+A handler is a callable with the signature:
+
+```python
+def my_handler(url, method, timeout, headers, data):
+ # url: str — full request URL
+ # method: str — HTTP method (PUT, POST, DELETE)
+ # timeout: Optional[float] — seconds before aborting, or None
+ # headers: List[Tuple[str, str]] — HTTP headers to include
+ # data: bytes — request body
+ ...
+ return callable_that_performs_the_request
+```
+
+The handler must return a no-argument callable that performs the actual HTTP request and raises
+an exception (e.g. `IOError`) on failure. Three built-in handlers are available in
+`prometheus_client.exposition`:
+
+### `default_handler`
+
+Standard HTTP/HTTPS handler. Used by default in all push functions.
+
+### `basic_auth_handler(url, method, timeout, headers, data, username=None, password=None)`
+
+Wraps `default_handler` and adds an HTTP Basic Auth header.
+
+| Extra parameter | Type | Default | Description |
+|----------------|------|---------|-------------|
+| `username` | `Optional[str]` | `None` | HTTP Basic Auth username. |
+| `password` | `Optional[str]` | `None` | HTTP Basic Auth password. |
+
+### `tls_auth_handler(url, method, timeout, headers, data, certfile, keyfile, cafile=None, protocol=ssl.PROTOCOL_TLS_CLIENT, insecure_skip_verify=False)`
+
+Performs the request over HTTPS using TLS client certificate authentication.
+
+| Extra parameter | Type | Default | Description |
+|----------------|------|---------|-------------|
+| `certfile` | `str` | required | Path to the client certificate PEM file. |
+| `keyfile` | `str` | required | Path to the client private key PEM file. |
+| `cafile` | `Optional[str]` | `None` | Path to a CA certificate file for server verification. Uses system defaults if not set. |
+| `protocol` | `int` | `ssl.PROTOCOL_TLS_CLIENT` | SSL/TLS protocol version. |
+| `insecure_skip_verify` | `bool` | `False` | Skip server certificate verification. Use only in controlled environments. |
+
+### `passthrough_redirect_handler`
+
+Like `default_handler` but automatically follows redirects for all HTTP methods, including `PUT`
+and `POST`. Use only when you control or trust the source of redirect responses.
diff --git a/docs/content/exporting/textfile.md b/docs/content/exporting/textfile.md
index 80360e46..cb2571af 100644
--- a/docs/content/exporting/textfile.md
+++ b/docs/content/exporting/textfile.md
@@ -20,4 +20,24 @@ write_to_textfile('/configured/textfile/path/raid.prom', registry)
```
A separate registry is used, as the default registry may contain other metrics
-such as those from the Process Collector.
\ No newline at end of file
+such as those from the Process Collector.
+
+## API Reference
+
+### `write_to_textfile(path, registry, escaping='allow-utf-8', tmpdir=None)`
+
+Writes metrics from the registry to a file in Prometheus text format.
+
+The file is written atomically: metrics are first written to a temporary file in the same
+directory as `path` (or in `tmpdir` if provided), then renamed into place. This prevents the
+Node exporter from reading a partially written file.
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `path` | `str` | required | Destination file path. Must end in `.prom` for the Node exporter textfile collector to process it. |
+| `registry` | `Collector` | required | Registry whose metrics are written. |
+| `escaping` | `str` | `'allow-utf-8'` | Escaping scheme for metric and label names. Accepted values: `'allow-utf-8'`, `'underscores'`, `'dots'`, `'values'`. |
+| `tmpdir` | `Optional[str]` | `None` | Directory for the temporary file used during the atomic write. Defaults to the same directory as `path`. If provided, must be on the same filesystem as `path`. |
+
+Returns `None`. Raises an exception if the file cannot be written; the temporary file is cleaned
+up automatically on failure.
\ No newline at end of file
diff --git a/docs/content/instrumenting/_index.md b/docs/content/instrumenting/_index.md
index 13bbc6b6..1b013d58 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 94618025..4876b612 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 102091a1..b1e6169a 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 0b1529e9..62294944 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,149 @@ 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)`
-# Increment when entered, decrement when exited.
+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.
+
+```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
+
+with g.time() as t:
+ pass
+print(t.duration) # observed time in seconds.
+```
+
+### `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 cb85f183..fa0ffe1a 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,117 @@ 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
+
+with h.time() as t:
+ pass
+print(t.duration) # observed time in seconds.
+```
+
+## 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 6334d92b..6e369de7 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 ebf80b56..39ad29c8 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 fa407496..714dfd2f 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,99 @@ 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
+
+with s.time() as t:
+ pass
+print(t.duration) # observed time in seconds.
```
-The Python client doesn't store or expose quantile information at this time.
\ No newline at end of file
+## 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
+```
+
+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 42ea6a67..cd129930 100644
--- a/docs/content/multiprocess/_index.md
+++ b/docs/content/multiprocess/_index.md
@@ -35,6 +35,12 @@ between process/Gunicorn runs (before startup is recommended).
This environment variable should be set from a start-up shell script,
and not directly from Python (otherwise it may not propagate to child processes).
+Note: on Windows Subsystem for Linux (WSL), set `PROMETHEUS_MULTIPROC_DIR` to a
+Linux-native filesystem path (e.g. `/tmp` or `/home/`) rather than a
+Windows-mounted path (e.g. `/mnt/c/...`). On Windows-mounted filesystems the
+per-process metric files can be written with an incorrect internal offset,
+causing the collector to silently read no data.
+
**2. Metrics collector**:
The application must initialize a new `CollectorRegistry`, and store the
@@ -96,3 +102,53 @@ from prometheus_client import Gauge
# Example gauge
IN_PROGRESS = Gauge("inprogress_requests", "help", multiprocess_mode='livesum')
```
+
+## API Reference
+
+### `MultiProcessCollector(registry, path=None)`
+
+Collector that aggregates metrics written by all processes in the multiprocess directory.
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `registry` | `CollectorRegistry` | required | Registry to register with. Pass a registry created inside the request context to avoid duplicate metrics. |
+| `path` | `Optional[str]` | `None` | Path to the directory containing the per-process metric files. Defaults to the `PROMETHEUS_MULTIPROC_DIR` environment variable. |
+
+Raises `ValueError` if `path` is not set or does not point to an existing directory.
+
+```python
+from prometheus_client import multiprocess, CollectorRegistry
+
+def app(environ, start_response):
+ registry = CollectorRegistry(support_collectors_without_names=True)
+ multiprocess.MultiProcessCollector(registry)
+ ...
+```
+
+To use a custom path instead of the environment variable:
+
+```python
+collector = multiprocess.MultiProcessCollector(registry, path='/var/run/prom')
+```
+
+### `mark_process_dead(pid, path=None)`
+
+Removes the per-process metric files for a dead process. Call this from your process manager
+when a worker exits to prevent stale `live*` gauge values from accumulating.
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `pid` | `int` | required | PID of the process that has exited. |
+| `path` | `Optional[str]` | `None` | Path to the multiprocess directory. Defaults to the `PROMETHEUS_MULTIPROC_DIR` environment variable. |
+
+Returns `None`. Only removes files for `live*` gauge modes (e.g. `livesum`, `liveall`); files
+for non-live modes are left in place so their last values remain visible until the directory is
+wiped on restart.
+
+```python
+# Gunicorn config
+from prometheus_client import multiprocess
+
+def child_exit(server, worker):
+ multiprocess.mark_process_dead(worker.pid)
+```
diff --git a/docs/content/registry/_index.md b/docs/content/registry/_index.md
new file mode 100644
index 00000000..0d554535
--- /dev/null
+++ b/docs/content/registry/_index.md
@@ -0,0 +1,141 @@
+---
+title: Registry
+weight: 8
+---
+
+A `CollectorRegistry` holds all the collectors whose metrics are exposed when
+the registry is scraped. The global default registry is `REGISTRY`, which all
+metric constructors register with automatically unless told otherwise.
+
+```python
+from prometheus_client import REGISTRY, CollectorRegistry
+
+# Use the default global registry
+from prometheus_client import Counter
+c = Counter('my_counter', 'A counter') # registered with REGISTRY automatically
+
+# Create an isolated registry, e.g. for testing
+r = CollectorRegistry()
+c2 = Counter('my_counter', 'A counter', registry=r)
+```
+
+## Constructor
+
+```python
+CollectorRegistry(auto_describe=False, target_info=None, support_collectors_without_names=False)
+```
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `auto_describe` | `bool` | `False` | If `True`, calls `collect()` on a collector at registration time if the collector does not implement `describe()`. Used to detect duplicate metric names. The default `REGISTRY` is created with `auto_describe=True`. |
+| `target_info` | `Dict[str, str]` | `None` | Key-value labels to attach as a `target_info` metric. Equivalent to calling `set_target_info` after construction. |
+| `support_collectors_without_names` | `bool` | `False` | If `True`, allows registering collectors that produce no named metrics (i.e. whose `describe()` returns an empty list). |
+
+## Methods
+
+### `register(collector)`
+
+Register a collector with this registry. Raises `ValueError` if any of the
+metric names the collector produces are already registered.
+
+```python
+from prometheus_client.registry import Collector
+
+class MyCollector(Collector):
+ def collect(self):
+ ...
+
+REGISTRY.register(MyCollector())
+```
+
+### `unregister(collector)`
+
+Remove a previously registered collector.
+
+```python
+from prometheus_client import GC_COLLECTOR
+REGISTRY.unregister(GC_COLLECTOR)
+```
+
+### `collect()`
+
+Yield all metrics from every registered collector. Also yields the
+`target_info` metric if one has been set.
+
+```python
+for metric in REGISTRY.collect():
+ print(metric.name, metric.type)
+```
+
+### `restricted_registry(names)`
+
+Return a view of this registry that only exposes the named metrics. Useful
+for partial scrapes. See [Restricted registry](../restricted-registry/) for
+usage with `generate_latest` and the built-in HTTP server.
+
+```python
+from prometheus_client import generate_latest
+
+subset = REGISTRY.restricted_registry(['python_info', 'process_cpu_seconds_total'])
+output = generate_latest(subset)
+```
+
+### `get_sample_value(name, labels=None)`
+
+Return the current value of a single sample, or `None` if not found. Intended
+for use in unit tests; not efficient for production use.
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `name` | `str` | required | Full sample name including any suffix (e.g. `'my_counter_total'`). |
+| `labels` | `Dict[str, str]` | `{}` | Label key-value pairs to match. An empty dict matches an unlabelled sample. |
+
+```python
+from prometheus_client import Counter, CollectorRegistry
+
+r = CollectorRegistry()
+c = Counter('requests_total', 'Total requests', registry=r)
+c.inc(3)
+
+assert r.get_sample_value('requests_total') == 3.0
+```
+
+### `set_target_info(labels)`
+
+Set or replace the target metadata labels exposed as a `target_info` metric.
+Pass `None` to remove the target info metric.
+
+```python
+REGISTRY.set_target_info({'env': 'production', 'region': 'us-east-1'})
+```
+
+### `get_target_info()`
+
+Return the current target info labels as a `Dict[str, str]`, or `None` if not set.
+
+```python
+info = REGISTRY.get_target_info()
+```
+
+## The global REGISTRY
+
+`REGISTRY` is the module-level default instance, created as:
+
+```python
+REGISTRY = CollectorRegistry(auto_describe=True)
+```
+
+All metric constructors (`Counter`, `Gauge`, etc.) register with `REGISTRY`
+by default. Pass `registry=None` to skip registration, or pass a different
+`CollectorRegistry` instance to use a custom registry.
+
+```python
+from prometheus_client import Counter, CollectorRegistry
+
+# skip global registration — useful in tests
+c = Counter('my_counter', 'A counter', registry=None)
+
+# register with a custom registry
+r = CollectorRegistry()
+c2 = Counter('my_counter', 'A counter', registry=r)
+```
diff --git a/prometheus_client/context_managers.py b/prometheus_client/context_managers.py
index 3988ec22..3e8d7ced 100644
--- a/prometheus_client/context_managers.py
+++ b/prometheus_client/context_managers.py
@@ -55,6 +55,7 @@ class Timer:
def __init__(self, metric, callback_name):
self._metric = metric
self._callback_name = callback_name
+ self.duration = None
def _new_timer(self):
return self.__class__(self._metric, self._callback_name)
@@ -65,9 +66,9 @@ def __enter__(self):
def __exit__(self, typ, value, traceback):
# Time can go backwards.
- duration = max(default_timer() - self._start, 0)
+ self.duration = max(default_timer() - self._start, 0)
callback = getattr(self._metric, self._callback_name)
- callback(duration)
+ callback(self.duration)
def labels(self, *args, **kw):
self._metric = self._metric.labels(*args, **kw)
diff --git a/prometheus_client/core.py b/prometheus_client/core.py
index 60f93ce1..045e90ab 100644
--- a/prometheus_client/core.py
+++ b/prometheus_client/core.py
@@ -4,7 +4,7 @@
HistogramMetricFamily, InfoMetricFamily, Metric, StateSetMetricFamily,
SummaryMetricFamily, UnknownMetricFamily, UntypedMetricFamily,
)
-from .registry import CollectorRegistry, REGISTRY
+from .registry import CollectorRegistry, DuplicateTimeseries, REGISTRY
from .samples import BucketSpan, Exemplar, NativeHistogram, Sample, Timestamp
__all__ = (
@@ -12,6 +12,7 @@
'CollectorRegistry',
'Counter',
'CounterMetricFamily',
+ 'DuplicateTimeseries',
'Enum',
'Exemplar',
'Gauge',
diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py
index 2d402a0f..0b63f6f6 100644
--- a/prometheus_client/exposition.py
+++ b/prometheus_client/exposition.py
@@ -196,6 +196,8 @@ def _get_ssl_ctx(
cafile: Optional[str] = None,
capath: Optional[str] = None,
client_auth_required: bool = False,
+ tls_min_version: Optional[ssl.TLSVersion] = None,
+ tls_max_version: Optional[ssl.TLSVersion] = None
) -> ssl.SSLContext:
"""Load context supports SSL."""
ssl_cxt = ssl.SSLContext(protocol=protocol)
@@ -227,6 +229,11 @@ def _get_ssl_ctx(
raise exc_type(f"Cannot load server certificate file {certfile!r} or "
f"its private key file {keyfile!r}: {msg}")
+ if tls_min_version is not None:
+ ssl_cxt.minimum_version = tls_min_version
+ if tls_max_version is not None:
+ ssl_cxt.maximum_version = tls_max_version
+
return ssl_cxt
@@ -240,6 +247,8 @@ def start_wsgi_server(
client_capath: Optional[str] = None,
protocol: int = ssl.PROTOCOL_TLS_SERVER,
client_auth_required: bool = False,
+ tls_min_version: Optional[ssl.TLSVersion] = None,
+ tls_max_version: Optional[ssl.TLSVersion] = None
) -> Tuple[WSGIServer, threading.Thread]:
"""Starts a WSGI server for prometheus metrics as a daemon thread."""
@@ -250,7 +259,16 @@ class TmpServer(ThreadingWSGIServer):
app = make_wsgi_app(registry)
httpd = make_server(addr, port, app, TmpServer, handler_class=_SilentHandler)
if certfile and keyfile:
- context = _get_ssl_ctx(certfile, keyfile, protocol, client_cafile, client_capath, client_auth_required)
+ context = _get_ssl_ctx(
+ certfile,
+ keyfile,
+ protocol,
+ client_cafile,
+ client_capath,
+ client_auth_required,
+ tls_min_version,
+ tls_max_version
+ )
httpd.socket = context.wrap_socket(httpd.socket, server_side=True)
t = threading.Thread(target=httpd.serve_forever)
t.daemon = True
diff --git a/prometheus_client/metrics.py b/prometheus_client/metrics.py
index 4c53b26b..e3fe5323 100644
--- a/prometheus_client/metrics.py
+++ b/prometheus_client/metrics.py
@@ -135,7 +135,7 @@ def __init__(self: T,
if registry:
registry.register(self)
- def labels(self: T, *labelvalues: Any, **labelkwargs: Any) -> T:
+ def labels(self: T, *labelvalues: object, **labelkwargs: object) -> T:
"""Return the child for the given labelset.
All metrics can have labels, allowing grouping of related time series.
@@ -173,13 +173,13 @@ def labels(self: T, *labelvalues: Any, **labelkwargs: Any) -> T:
if labelkwargs:
if sorted(labelkwargs) != sorted(self._labelnames):
raise ValueError('Incorrect label names')
- labelvalues = tuple(str(labelkwargs[l]) for l in self._labelnames)
+ str_labelvalues = tuple(str(labelkwargs[l]) for l in self._labelnames)
else:
if len(labelvalues) != len(self._labelnames):
raise ValueError('Incorrect label count')
- labelvalues = tuple(str(l) for l in labelvalues)
+ str_labelvalues = tuple(str(l) for l in labelvalues)
with self._lock:
- if labelvalues not in self._metrics:
+ if str_labelvalues not in self._metrics:
original_name = getattr(self, '_original_name', self._name)
namespace = getattr(self, '_namespace', '')
@@ -190,17 +190,17 @@ def labels(self: T, *labelvalues: Any, **labelkwargs: Any) -> T:
for k in ('namespace', 'subsystem', 'unit'):
child_kwargs.pop(k, None)
- self._metrics[labelvalues] = self.__class__(
+ self._metrics[str_labelvalues] = self.__class__(
original_name,
documentation=self._documentation,
labelnames=self._labelnames,
namespace=namespace,
subsystem=subsystem,
unit=unit,
- _labelvalues=labelvalues,
+ _labelvalues=str_labelvalues,
**child_kwargs
)
- return self._metrics[labelvalues]
+ return self._metrics[str_labelvalues]
def remove(self, *labelvalues: Any) -> None:
if 'prometheus_multiproc_dir' in os.environ or 'PROMETHEUS_MULTIPROC_DIR' in os.environ:
@@ -254,6 +254,8 @@ def remove_by_labels(self, labels: dict[str, str]) -> None:
def clear(self) -> None:
"""Remove all labelsets from the metric"""
+ if not self._labelnames:
+ return
if 'prometheus_multiproc_dir' in os.environ or 'PROMETHEUS_MULTIPROC_DIR' in os.environ:
warnings.warn(
"Clearing labels has not been implemented in multi-process mode yet",
@@ -767,6 +769,10 @@ def __init__(self,
_labelvalues: Optional[Sequence[str]] = None,
states: Optional[Sequence[str]] = None,
):
+ if name in labelnames:
+ raise ValueError(f'Overlapping labels for Enum metric: {name}')
+ if not states:
+ raise ValueError(f'No states provided for Enum metric: {name}')
super().__init__(
name=name,
documentation=documentation,
@@ -777,10 +783,6 @@ def __init__(self,
registry=registry,
_labelvalues=_labelvalues,
)
- if name in labelnames:
- raise ValueError(f'Overlapping labels for Enum metric: {name}')
- if not states:
- raise ValueError(f'No states provided for Enum metric: {name}')
self._kwargs['states'] = self._states = states
def _metric_init(self) -> None:
diff --git a/prometheus_client/openmetrics/exposition.py b/prometheus_client/openmetrics/exposition.py
index 5e69e463..5a7711b0 100644
--- a/prometheus_client/openmetrics/exposition.py
+++ b/prometheus_client/openmetrics/exposition.py
@@ -25,9 +25,9 @@
def _is_valid_exemplar_metric(metric, sample):
if metric.type == 'counter' and sample.name.endswith('_total'):
return True
- if metric.type in ('gaugehistogram') and sample.name.endswith('_bucket'):
+ if metric.type == 'gaugehistogram' and sample.name.endswith('_bucket'):
return True
- if metric.type in ('histogram') and sample.name.endswith('_bucket') or sample.name == metric.name:
+ if metric.type == 'histogram' and (sample.name.endswith('_bucket') or sample.name == metric.name):
return True
return False
diff --git a/prometheus_client/openmetrics/parser.py b/prometheus_client/openmetrics/parser.py
index d967e83b..0c5c9c41 100644
--- a/prometheus_client/openmetrics/parser.py
+++ b/prometheus_client/openmetrics/parser.py
@@ -315,7 +315,7 @@ def _parse_nh_struct(text):
deltas = dict(re_deltas.findall(text))
count_value = int(items['count'])
- sum_value = int(items['sum'])
+ sum_value = float(items['sum'])
schema = int(items['schema'])
zero_threshold = float(items['zero_threshold'])
zero_count = int(items['zero_count'])
diff --git a/prometheus_client/registry.py b/prometheus_client/registry.py
index c2b55d15..63f8ab03 100644
--- a/prometheus_client/registry.py
+++ b/prometheus_client/registry.py
@@ -1,6 +1,6 @@
import copy
from threading import Lock
-from typing import Dict, Iterable, List, Optional, Protocol
+from typing import Dict, Iterable, List, Optional, Protocol, Set
from .metrics_core import Metric
@@ -15,6 +15,14 @@ def collect(self) -> Iterable[Metric]:
return []
+class DuplicateTimeseries(ValueError):
+ def __init__(self, duplicates: Set[str]):
+ msg = 'Duplicated timeseries in CollectorRegistry: {}'.format(
+ duplicates)
+ super().__init__(msg)
+ self.duplicates: Set[str] = duplicates
+
+
class CollectorRegistry:
"""Metric collector registry.
@@ -40,9 +48,7 @@ def register(self, collector: Collector) -> None:
names = self._get_names(collector)
duplicates = set(self._names_to_collectors).intersection(names)
if duplicates:
- raise ValueError(
- 'Duplicated timeseries in CollectorRegistry: {}'.format(
- duplicates))
+ raise DuplicateTimeseries(duplicates)
for name in names:
self._names_to_collectors[name] = collector
self._collector_to_names[collector] = names
@@ -55,6 +61,8 @@ def unregister(self, collector: Collector) -> None:
for name in self._collector_to_names[collector]:
del self._names_to_collectors[name]
del self._collector_to_names[collector]
+ if collector in self._collectors_without_names:
+ self._collectors_without_names.remove(collector)
def _get_names(self, collector):
"""Get names of timeseries the collector produces and clashes with."""
diff --git a/prometheus_client/utils.py b/prometheus_client/utils.py
index 87b75ca8..52c852ab 100644
--- a/prometheus_client/utils.py
+++ b/prometheus_client/utils.py
@@ -21,7 +21,7 @@ def floatToGoString(d):
# We only need to care about positive values for le/quantile.
if d > 0 and dot > 6:
mantissa = f'{s[0]}.{s[1:dot]}{s[dot + 1:]}'.rstrip('0.')
- return f'{mantissa}e+0{dot - 1}'
+ return f'{mantissa}e+{dot - 1:02d}'
return s
diff --git a/prometheus_client/values.py b/prometheus_client/values.py
index 6ff85e3b..16c745ed 100644
--- a/prometheus_client/values.py
+++ b/prometheus_client/values.py
@@ -1,9 +1,18 @@
import os
from threading import Lock
+from typing import Callable, List
import warnings
from .mmap_dict import mmap_key, MmapedDict
+_multi_process_cleanups: List[Callable[[], None]] = []
+
+
+def close_all_multiprocess_files():
+ for cleanup in _multi_process_cleanups:
+ cleanup()
+ _multi_process_cleanups.clear()
+
class MutexValue:
"""A float protected by a mutex."""
@@ -52,6 +61,13 @@ def MultiProcessValue(process_identifier=os.getpid):
# This avoids the need to also have mutexes in __MmapDict.
lock = Lock()
+ def cleanup():
+ for f in files.values():
+ f.close()
+ files.clear()
+ values.clear()
+ _multi_process_cleanups.append(cleanup)
+
class MmapedValue:
"""A float protected by a mutex backed by a per-process mmaped file."""
@@ -122,6 +138,14 @@ def get_exemplar(self):
# TODO: Implement exemplars for multiprocess mode.
return None
+ @classmethod
+ def close_all_files(cls):
+ with lock:
+ for f in files.values():
+ f.close()
+ files.clear()
+ values.clear()
+
return MmapedValue
diff --git a/pyproject.toml b/pyproject.toml
index ed3ef389..8b39c12d 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.26.0"
description = "Python client for the Prometheus monitoring system."
readme = "README.md"
license = "Apache-2.0 AND BSD-2-Clause"
diff --git a/tests/certs/client-cert.pem b/tests/certs/client-cert.pem
new file mode 100644
index 00000000..5a054199
--- /dev/null
+++ b/tests/certs/client-cert.pem
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE-----
+MIICrzCCAZcCFCVu7nbOAxRNKBYa2cl22rdRCtvfMA0GCSqGSIb3DQEBCwUAMBIx
+EDAOBgNVBAMMB1Rlc3QgQ0EwHhcNMjYwNTI2MTQyMTU0WhcNMzYwNTIzMTQyMTU0
+WjAWMRQwEgYDVQQDDAt0ZXN0LWNsaWVudDCCASIwDQYJKoZIhvcNAQEBBQADggEP
+ADCCAQoCggEBAJM2/f+8BBKjAlSF/9eiuB2444A2g6V007U5shZhBuPC9cNDxGKM
+W1WT3QsgvxOdagdaANkpufqHcYixgFhx/v3lSEzlzd3uXyFMOiK7BdiPsctkqlWZ
+VGIuUPpWwvJHWS4R5V1nYNCVsgyZB9XGThl7IknQzBK+tkY2GepqPQXyx1/AP7aB
+AlTVBx3r7jTWvkrzvAdrcevrjhOOJUbPmgoiiEGSQeZSMvkdLERujvu5Y3wno2Mg
+vcHJxCJwZ5y0RakmTzyAZLHke9lMavgt9F5yEA8G/8SnnXy6HrUp6B6I8Z1eLnof
+b3mjUwiGxqDwEVBQHfMtOH6uC7ZE6zbNB1cCAwEAATANBgkqhkiG9w0BAQsFAAOC
+AQEAJBchyhT2iyg42qi3uUE1NeCcEb/gM82LeihZbDd38ItUdU7TFqk7wEwsUNJk
+k1uwNFVlyMGbHD1IvCAS4L8l/9uPaDG4DmLZ42shFRCaABNEFlKtGPa+YNuhFJ5z
+DZKaLaJp8BKpvmoH+iPmsoCDlADwWmLgbdeFBGnHRuOnJBSmEEjQFrnz3jKrX6Lk
++IxVX5Rdp9xOKHBJkj99mgseEYZQk2YFFBCzHX7NNl6wBk/usKJoJeaOPhl9eOGK
+VaUOfEdO5NuTRf9nPOORzqFtW3ErNjNjPjKN8VppHtXhRO6dWsmzGnmjVChxoZWC
+H0rRJtGcab5HWf94laJilCj7Cw==
+-----END CERTIFICATE-----
diff --git a/tests/certs/client-key.pem b/tests/certs/client-key.pem
new file mode 100644
index 00000000..e218a006
--- /dev/null
+++ b/tests/certs/client-key.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCTNv3/vAQSowJU
+hf/XorgduOOANoOldNO1ObIWYQbjwvXDQ8RijFtVk90LIL8TnWoHWgDZKbn6h3GI
+sYBYcf795UhM5c3d7l8hTDoiuwXYj7HLZKpVmVRiLlD6VsLyR1kuEeVdZ2DQlbIM
+mQfVxk4ZeyJJ0MwSvrZGNhnqaj0F8sdfwD+2gQJU1Qcd6+401r5K87wHa3Hr644T
+jiVGz5oKIohBkkHmUjL5HSxEbo77uWN8J6NjIL3BycQicGectEWpJk88gGSx5HvZ
+TGr4LfRechAPBv/Ep518uh61KegeiPGdXi56H295o1MIhsag8BFQUB3zLTh+rgu2
+ROs2zQdXAgMBAAECggEAATafUlJzkCRtelKJFiG+YGmr37HTVPOeY8HVe29noKH0
+kkbxNpoPOKiEK7l53wiu8oo7M+RZpucjOEFfnEWtmIchbkIoomR6vpSubVHa+FAl
+jYEcvEw2u1ZuuW7Uotg+s8KsVXWVgTKdVJLq/cfpezaeGjtRK0hiH+MF71OFLD2I
+UoszlVbTI9FAP+xwuFSJO4xyOirz2VmqgYvQd+qTuuPU2ZjPHFbBUXm6JDpchGJk
+WdPp/7qEWKFwDufvgkA5rCFxwsiReQ9HfOS2f4l+7eg2uyjXAClYTt/lYq9PK1Ut
+sk/R1Gq5C4S8G0f04Jk8J2bQKS57oRALfaJEps5LUQKBgQDPCPID4w9TnAhWoHtR
+L5ps02KLi52sw9F3EVedVX2BjMM/jvRwtzg8I8iaWAE1iL0t8lDQRxbUcgNyWRvi
+0/WG/2IESVlciqd4XuITLthj1PDIpIM2iCjQZpZKDqe9bVRPx/AY+UNV1aLSCEbF
+xGS+uYoQRGpmiSYnRaQzIzgn0QKBgQC2CDOD1/1sEVFbfsWJTaAM3YjsQ1I5mXFI
+HhoWpMKBUogWBXp9dzO4Ae/iRo0QviVUUY2bHlJjCoaQ0FzuiieZIhbOwHG2Qtf3
+JzmUaOSMecwsTeM05XHciwY+sWU/Udw7EzDhVpOHPZR31LKeapchUJGnofnOdkcY
+zaHEwiuupwKBgQC6I0bD698Zws1UZRC6G1xxv1N4NtxaOewXawYktHoUgaQBftuS
+g4gRufJfogPkR74ekx/JQkDqXF9w7WC+/OZgqzdKt0+afia3eEc2DAYNK6QYIKC/
+5IcdZz5z8t0o2CTXXeEl8uVxRJQQ1dQbdslFGLdijMBE08XzxQ8t0tpoIQKBgH09
+U0QovME3gQQ0SnBXKgDwAp6bCt16RshZfZWKshAL2nlcN5RPCRRWsNa7t56HVGOY
+4JaS3BgsS70ivm2YO/pNy+df3FyLzM7M+/6x1F0aB3GL/QCNxDL6q8dCgeh4x88V
+OxIuYL4xjg6MFoCL0YMoTa5J8PctxWi5Qc1/0lINAoGASyTZT8emfSDW0+kpqiYw
+y+4ftFxqYPAVCf2IWGeQL8TrfkxUiJ7r4Pu5VK9nuYvMR/u4mvnJSG6F1NuJhxzY
+4kUnoOnPhITLZjUvNE/xQEuhiJndiehZgSj0JAU6MqGa4pZOxZfqPAfhEF62b5wx
+6Wlh7JxQAM+6agEfM3/OS3Y=
+-----END PRIVATE KEY-----
diff --git a/tests/certs/server-ca.pem b/tests/certs/server-ca.pem
new file mode 100644
index 00000000..bbcada29
--- /dev/null
+++ b/tests/certs/server-ca.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDBTCCAe2gAwIBAgIUMrjGc/qUt+rpFb14OvBFePSMQRIwDQYJKoZIhvcNAQEL
+BQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAeFw0yNjA1MjYxNDIxNTNaFw0zNjA1MjMx
+NDIxNTNaMBIxEDAOBgNVBAMMB1Rlc3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB
+DwAwggEKAoIBAQDFC2wPWOqoFHIYXASjL06yUnG0SlqLqw4oZphdj/q4pbyYPcva
+QKI8m4u7Tq/l8JQmbd0sHqGMVYLmACX5ygkppzepz/bVgDeij7RztUgDjJwvUxAC
+SEAss0dcE19P57j5ad24xmyV2iP0RK7oXnjapDrH1fhqvIyfybqRxt+50NODRh1t
+z471240lDBPOG3ReRZ06dYEpzYaq3PQPatPJnaLGOmsf2NQ8sETTK35vcTMZrXsr
+vzrftUCKn4DRyyZ58GE1VpevbVi8z/vHzWBYpRcHTZvfnOz12ijCd2wvnEtTu8TO
++GZS5j84KSF4AI7FlhDMPAS3/dhSLzXgnd4lAgMBAAGjUzBRMB0GA1UdDgQWBBRi
+IztvE2ErRLmziv1XxxHCbism8DAfBgNVHSMEGDAWgBRiIztvE2ErRLmziv1XxxHC
+bism8DAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAjBA50Oadg
+Fx6d/cxzHCEd29BluqxjfzYc4TAeTP4NSQPWKXM7BgIqZbHDS/IAK/Xfd1raVCm5
+yv2v4Pe+0fTkezUk5uZjtgZoH5+o4aQL9GdbLO93F4rxzZhpoY92iaXAsoDEntRO
+YyDnxLna6csiH4hyvr6Q8Yih/lDysw7DB1jozkFeZtX4ZsVFpsDnYLa5OQjJErpw
+9GCM0NEzEW6HlqblsAuBv3DHavUAzfR4obD+Md60BRMxwC5Otl63sS99y81ycs2S
+ffW0rLDtgB9hShCXBNeZkGsPrLwBr00nK7bvGaZwOU0Ysuxg+elP3cOXD2UcW7lc
+Q+ZBinkQEFpB
+-----END CERTIFICATE-----
diff --git a/tests/certs/server-cert.pem b/tests/certs/server-cert.pem
new file mode 100644
index 00000000..e1f11670
--- /dev/null
+++ b/tests/certs/server-cert.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDEjCCAfqgAwIBAgIUJW7uds4DFE0oFhrZyXbat1EK294wDQYJKoZIhvcNAQEL
+BQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAeFw0yNjA1MjYxNDIxNTNaFw0zNjA1MjMx
+NDIxNTNaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBAOl3Gnz9y+OHr3MIetuX7a/Bvo/lYkwTnmQpT4YC1ycalM0+
+M4wCewrbpn5RoNbH4j+oB6URuiLWlhk3SP9hOwbVbt4rOyCaulAVa5G0B46eqkG6
+ZDi9EWkt+L7Zvfwe+MbdG25xmRumkvc6iv8b+/0mM3t5cm3E8BHpwPi6kfoFmFHn
+qREDfI4406tBkbSYUkRQL8qZvSWMo5HgIYmgrkKiWuZNV4bNYKHUOyaOqWvgn8En
+ZxrlGt2ezHif/SFz+EaYJZkjBDJ5rMbwxl1BAVZKpSubKt5U59zXLkuachZF5sAY
+sEd/LoZxF23/qP5y16C4mVzBYO/0z2udNOW9YAcCAwEAAaNeMFwwGgYDVR0RBBMw
+EYIJbG9jYWxob3N0hwR/AAABMB0GA1UdDgQWBBSUEOHINPcDxtBsfNF5xSTUSqfF
+vDAfBgNVHSMEGDAWgBRiIztvE2ErRLmziv1XxxHCbism8DANBgkqhkiG9w0BAQsF
+AAOCAQEALxRf0TSusmJXO9pj5t3Njxc6VS+Ts/MnmE1NTloCCkVMEfYYzqROWHME
+LOCg2YSnqX6S6Gwk3zjBSuT7aA4SNQ3lD9HndRYa5k+6/6qunnz5Q/g205GJ97us
+HqkvdDjLE7lGmM5pIVjoyeMOWiQ6+EOtMt0CmL0nfqJ0DsDUZHVB7NB+MW20EVmC
+XiXr52SuvKHDIms3QFkZWOi+scOKleQnvEVU7VqrQamKNtf8fxxGa3/AvjLLJ1ra
+q9eB590eajBDdg50FttYLwyA/yb6cqrfIMfrHRj3R//yE2avtUkrKN6FgCtqgpoa
+ZsIk6qEmFQWUTglyLwhk0f6m21FKAA==
+-----END CERTIFICATE-----
diff --git a/tests/certs/server-key.pem b/tests/certs/server-key.pem
new file mode 100644
index 00000000..68f2b805
--- /dev/null
+++ b/tests/certs/server-key.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDpdxp8/cvjh69z
+CHrbl+2vwb6P5WJME55kKU+GAtcnGpTNPjOMAnsK26Z+UaDWx+I/qAelEboi1pYZ
+N0j/YTsG1W7eKzsgmrpQFWuRtAeOnqpBumQ4vRFpLfi+2b38HvjG3RtucZkbppL3
+Oor/G/v9JjN7eXJtxPAR6cD4upH6BZhR56kRA3yOONOrQZG0mFJEUC/Kmb0ljKOR
+4CGJoK5ColrmTVeGzWCh1Dsmjqlr4J/BJ2ca5Rrdnsx4n/0hc/hGmCWZIwQyeazG
+8MZdQQFWSqUrmyreVOfc1y5LmnIWRebAGLBHfy6GcRdt/6j+cteguJlcwWDv9M9r
+nTTlvWAHAgMBAAECggEAJ2/ZlxyOGPC+J+naSwbWfTZ2mLsQSDaWLmg2CTaonm/k
+i+kCbxeqLjLdZIAoca+RHdyl8fHVJfZmo3rNx2nmvShHkprt4XuRll6P7axiDGrr
+6q9wJ490hfZgiuigKZsXvgvyis0ApoWUVNPcT+yru978mlJxDG7UeMoqMTne18NQ
+jKw8zTPrcDnSqxdzNs9hGcM/RYDAnNM1jFqnmJpJF9nTMf9gDOYQMJCb7yUZVOj/
+ccyc2AjTdp6corXZpSqiYHz4UwfZ0Wvf7BXwBAOxdYnM6qlZXc+RKZJYnlUbSNHZ
+IRkIZXsRILAIgpL1fAhMAQodyFMjNU9wQAkva6j49QKBgQD8ohxWR3nKg9KXhSTU
+7sv1E5WzUCEAX5gJTSRx9S8lAAotGEByaTO/pWcYtpo+jXukukERhDdnsxR7SJAf
+7jOYLUQgqFgZXD/U7vaCNuYoRG0E1MwSqZnVIXOpZW2z6j0PwcbXLJVeLDpDctn1
+Ga7VvYiv7/KuvRSQfkSOrH8USwKBgQDsk5lLSOQb7Ke53gfHjeGMU4646JcNDFnD
+hWtXQujABwQZmSDWudCvsLWwDr0O0kUDqDcPCEMhbNYo388DwowzHnE35Tzmzo5D
+R/YZ+Mh+UuW+e5gLxdmn7Z1xENKft/4ceOkeBBExQumZYYsaNVA7znzotaPSjRfH
+J36QHsG1tQKBgQC9NMBaUf/CD4ZaWqpiG1J/gyJ8AEgnGnEojjD8dC/R2zzD10T1
+KxtJrhwPozrUHGx8y83Ny6MfNDzjtE3UzDayAzzh5JLOs4tO84WFso4fnFe15ZXN
+aF5BBGO2e7N0qrr+oRdFsitQM3mTaGIashiCFghYFDJCcnQDX74CyOgIDwKBgCht
+JHHf99LpwtOZJF0uWo9/K9FfNYiuRpyJrQkRTvKZgFLbfugSgp2zJaj7K8Vfmxl/
+4kC4WbhZf9MmQ5rR4OFPX2t8ycZrH5ZRsrVHdQNZKRc+yYGhgosWqKPMiyFt8Idv
+Be7yJPn1BDQInhuRZq+BnoipmV/+akTG8/Kuvs1NAoGALLN5lPRdZdTvjoiougt9
+MxqGfBR9H8PfAo/Eu8Et5Otln3P1Vl3SgeiwDGVb59avfBQ5N4UecTHMp/2jbxOw
+w/AzvF9LMLtXKdyqnOeBfP2xgbEZ9chLeoePEYkATpQfgjs7qmzK+mwZin5EyjFa
+tqn7AnX5AnDRtPIC10Z05rA=
+-----END PRIVATE KEY-----
diff --git a/tests/openmetrics/test_exposition.py b/tests/openmetrics/test_exposition.py
index a3ed0d6e..a849f5fa 100644
--- a/tests/openmetrics/test_exposition.py
+++ b/tests/openmetrics/test_exposition.py
@@ -347,6 +347,21 @@ def collect(self):
with self.assertRaises(ValueError):
generate_latest(self.registry)
+ def test_gauge_exemplar(self) -> None:
+ class MyCollector:
+ def collect(self):
+ metric = Metric("gg", "A gauge", 'gauge')
+ # A sample whose name equals the metric name must not be
+ # treated as exemplar-eligible just because it matches;
+ # only histogram/gaugehistogram buckets, counter _total, and
+ # native histograms may carry exemplars.
+ metric.add_sample("gg", {}, 1, None, Exemplar({'a': 'b'}, 0.5))
+ yield metric
+
+ self.registry.register(MyCollector())
+ with self.assertRaises(ValueError):
+ generate_latest(self.registry)
+
def test_gaugehistogram(self) -> None:
self.custom_collector(
GaugeHistogramMetricFamily('gh', 'help', buckets=[('1.0', 4), ('+Inf', (5))], gsum_value=7))
diff --git a/tests/openmetrics/test_parser.py b/tests/openmetrics/test_parser.py
index aeaa6ed6..79a2158c 100644
--- a/tests/openmetrics/test_parser.py
+++ b/tests/openmetrics/test_parser.py
@@ -218,6 +218,18 @@ def test_native_histogram(self):
hfm.add_sample("nativehistogram", None, None, None, None, NativeHistogram(24, 100, 0, 0.001, 4, (BucketSpan(0, 2), BucketSpan(1, 2)), (BucketSpan(0, 2), BucketSpan(1, 2)), (2, 1, -3, 3), (2, 1, -2, 3)))
self.assertEqual([hfm], families)
+ def test_native_histogram_float_sum(self):
+ families = text_string_to_metric_families("""# TYPE nativehistogram histogram
+# HELP nativehistogram Is a basic example of a native histogram
+nativehistogram {count:24,sum:100.5,schema:0,zero_threshold:0.001,zero_count:4,positive_spans:[0:2,1:2],negative_spans:[0:2,1:2],positive_deltas:[2,1,-3,3],negative_deltas:[2,1,-2,3]}
+# EOF
+""")
+ families = list(families)
+
+ hfm = HistogramMetricFamily("nativehistogram", "Is a basic example of a native histogram")
+ hfm.add_sample("nativehistogram", None, None, None, None, NativeHistogram(24, 100.5, 0, 0.001, 4, (BucketSpan(0, 2), BucketSpan(1, 2)), (BucketSpan(0, 2), BucketSpan(1, 2)), (2, 1, -3, 3), (2, 1, -2, 3)))
+ self.assertEqual([hfm], families)
+
def test_native_histogram_utf8(self):
families = text_string_to_metric_families("""# TYPE "native{histogram" histogram
# HELP "native{histogram" Is a basic example of a native histogram
diff --git a/tests/test_asgi.py b/tests/test_asgi.py
index 6e795e21..028dac2b 100644
--- a/tests/test_asgi.py
+++ b/tests/test_asgi.py
@@ -32,19 +32,24 @@ def setUp(self):
# Setup ASGI scope
self.scope = {}
setup_testing_defaults(self.scope)
+ self.loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(self.loop)
self.communicator = None
def tearDown(self):
if self.communicator:
- asyncio.new_event_loop().run_until_complete(
+ self.loop.run_until_complete(
self.communicator.wait()
)
+ self.loop.close()
def seed_app(self, app):
- self.communicator = ApplicationCommunicator(app, self.scope)
+ async def _init():
+ self.communicator = ApplicationCommunicator(app, self.scope)
+ self.loop.run_until_complete(_init())
def send_input(self, payload):
- asyncio.new_event_loop().run_until_complete(
+ self.loop.run_until_complete(
self.communicator.send_input(payload)
)
@@ -52,7 +57,7 @@ def send_default_request(self):
self.send_input({"type": "http.request", "body": b""})
def get_output(self):
- output = asyncio.new_event_loop().run_until_complete(
+ output = self.loop.run_until_complete(
self.communicator.receive_output(0)
)
return output
@@ -148,9 +153,9 @@ def test_gzip(self):
increments = 2
self.increment_metrics(metric_name, help_text, increments)
app = make_asgi_app(self.registry)
- self.seed_app(app)
# Send input with gzip header.
self.scope["headers"] = [(b"accept-encoding", b"gzip")]
+ self.seed_app(app)
self.send_input({"type": "http.request", "body": b""})
# Assert outputs are compressed.
outputs = self.get_all_output()
@@ -164,9 +169,9 @@ def test_gzip_disabled(self):
self.increment_metrics(metric_name, help_text, increments)
# Disable compression explicitly.
app = make_asgi_app(self.registry, disable_compression=True)
- self.seed_app(app)
# Send input with gzip header.
self.scope["headers"] = [(b"accept-encoding", b"gzip")]
+ self.seed_app(app)
self.send_input({"type": "http.request", "body": b""})
# Assert outputs are not compressed.
outputs = self.get_all_output()
@@ -175,8 +180,8 @@ def test_gzip_disabled(self):
def test_openmetrics_encoding(self):
"""Response content type is application/openmetrics-text when appropriate Accept header is in request"""
app = make_asgi_app(self.registry)
- self.seed_app(app)
self.scope["headers"] = [(b"Accept", b"application/openmetrics-text; version=1.0.0")]
+ self.seed_app(app)
self.send_input({"type": "http.request", "body": b""})
content_type = self.get_response_header_value('Content-Type').split(";")[0]
@@ -204,8 +209,8 @@ def test_qs_parsing(self):
self.increment_metrics(*m)
for i_1 in range(len(metrics)):
- self.seed_app(app)
self.scope['query_string'] = f"name[]={metrics[i_1][0]}_total".encode("utf-8")
+ self.seed_app(app)
self.send_default_request()
outputs = self.get_all_output()
@@ -220,7 +225,7 @@ def test_qs_parsing(self):
self.assert_not_metrics(output, *metrics[i_2])
- asyncio.new_event_loop().run_until_complete(
+ self.loop.run_until_complete(
self.communicator.wait()
)
@@ -237,8 +242,8 @@ def test_qs_parsing_multi(self):
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.seed_app(app)
self.send_default_request()
outputs = self.get_all_output()
@@ -249,6 +254,6 @@ def test_qs_parsing_multi(self):
self.assert_metrics(output, *metrics[1])
self.assert_not_metrics(output, *metrics[2])
- asyncio.new_event_loop().run_until_complete(
+ self.loop.run_until_complete(
self.communicator.wait()
)
diff --git a/tests/test_core.py b/tests/test_core.py
index 66492c6f..3aa19c24 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -7,8 +7,8 @@
from prometheus_client import metrics
from prometheus_client.core import (
- CollectorRegistry, Counter, CounterMetricFamily, Enum, Gauge,
- GaugeHistogramMetricFamily, GaugeMetricFamily, Histogram,
+ CollectorRegistry, Counter, CounterMetricFamily, DuplicateTimeseries, Enum,
+ Gauge, GaugeHistogramMetricFamily, GaugeMetricFamily, Histogram,
HistogramMetricFamily, Info, InfoMetricFamily, Metric, Sample,
StateSetMetricFamily, Summary, SummaryMetricFamily, UntypedMetricFamily,
)
@@ -59,6 +59,12 @@ def test_reset(self):
def test_repr(self):
self.assertEqual(repr(self.counter), "prometheus_client.metrics.Counter(c)")
+ def test_clear_without_labels_is_noop(self):
+ self.counter.inc()
+ self.assertEqual(1, self.registry.get_sample_value('c_total'))
+ self.counter.clear() # should not raise
+ self.assertEqual(1, self.registry.get_sample_value('c_total'))
+
def test_negative_increment_raises(self):
self.assertRaises(ValueError, self.counter.inc, -1)
@@ -378,6 +384,14 @@ def test_block_decorator_with_label(self):
metric.labels('foo')
self.assertEqual(1, value('s_with_labels_count', {'label1': 'foo'}))
+ def test_timer_duration_exposed(self):
+ with self.summary.time() as t:
+ time.sleep(0.01)
+ self.assertIsNotNone(t.duration)
+ self.assertGreater(t.duration, 0)
+ recorded_sum = self.registry.get_sample_value('s_sum')
+ self.assertEqual(t.duration, recorded_sum)
+
def test_timer_not_observable(self):
s = Summary('test', 'help', labelnames=('label',), registry=self.registry)
@@ -581,6 +595,19 @@ def test_overlapping_labels(self):
with pytest.raises(ValueError):
Enum('e', 'help', registry=None, labelnames=['e'])
+ def test_failed_init_does_not_pollute_registry(self):
+ registry = CollectorRegistry()
+ # A validation failure in __init__ must not leave a half-built collector
+ # registered: otherwise the name stays permanently taken and any later
+ # scrape of the registry crashes on the missing _states attribute.
+ with pytest.raises(ValueError):
+ Enum('task_state', 'help', states=None, registry=registry)
+ with pytest.raises(ValueError):
+ Enum('task_state', 'help', states=['a'], labelnames=['task_state'], registry=registry)
+ # The name is still free, so a correct definition registers and scrapes.
+ Enum('task_state', 'help', states=['a', 'b'], registry=registry)
+ self.assertEqual(1, registry.get_sample_value('task_state', {'task_state': 'a'}))
+
class TestMetricWrapper(unittest.TestCase):
def setUp(self):
@@ -908,44 +935,59 @@ class TestCollectorRegistry(unittest.TestCase):
def test_duplicate_metrics_raises(self):
registry = CollectorRegistry()
Counter('c_total', 'help', registry=registry)
- self.assertRaises(ValueError, Counter, 'c_total', 'help', registry=registry)
- self.assertRaises(ValueError, Gauge, 'c_total', 'help', registry=registry)
- self.assertRaises(ValueError, Gauge, 'c_created', 'help', registry=registry)
+ self.assertRaises(DuplicateTimeseries, Counter, 'c_total', 'help', registry=registry)
+ self.assertRaises(DuplicateTimeseries, Gauge, 'c_total', 'help', registry=registry)
+ self.assertRaises(DuplicateTimeseries, Gauge, 'c_created', 'help', registry=registry)
Gauge('g_created', 'help', registry=registry)
- self.assertRaises(ValueError, Gauge, 'g_created', 'help', registry=registry)
- self.assertRaises(ValueError, Counter, 'g', 'help', registry=registry)
+ self.assertRaises(DuplicateTimeseries, Gauge, 'g_created', 'help', registry=registry)
+ self.assertRaises(DuplicateTimeseries, Counter, 'g', 'help', registry=registry)
Summary('s', 'help', registry=registry)
- self.assertRaises(ValueError, Summary, 's', 'help', registry=registry)
- self.assertRaises(ValueError, Gauge, 's_created', 'help', registry=registry)
- self.assertRaises(ValueError, Gauge, 's_sum', 'help', registry=registry)
- self.assertRaises(ValueError, Gauge, 's_count', 'help', registry=registry)
+ self.assertRaises(DuplicateTimeseries, Summary, 's', 'help', registry=registry)
+ self.assertRaises(DuplicateTimeseries, Gauge, 's_created', 'help', registry=registry)
+ self.assertRaises(DuplicateTimeseries, Gauge, 's_sum', 'help', registry=registry)
+ self.assertRaises(DuplicateTimeseries, Gauge, 's_count', 'help', registry=registry)
# We don't currently expose quantiles, but let's prevent future
# clashes anyway.
- self.assertRaises(ValueError, Gauge, 's', 'help', registry=registry)
+ self.assertRaises(DuplicateTimeseries, Gauge, 's', 'help', registry=registry)
Histogram('h', 'help', registry=registry)
- self.assertRaises(ValueError, Histogram, 'h', 'help', registry=registry)
+ self.assertRaises(DuplicateTimeseries, Histogram, 'h', 'help', registry=registry)
# Clashes aggaint various suffixes.
- self.assertRaises(ValueError, Summary, 'h', 'help', registry=registry)
- self.assertRaises(ValueError, Gauge, 'h_count', 'help', registry=registry)
- self.assertRaises(ValueError, Gauge, 'h_sum', 'help', registry=registry)
- self.assertRaises(ValueError, Gauge, 'h_bucket', 'help', registry=registry)
- self.assertRaises(ValueError, Gauge, 'h_created', 'help', registry=registry)
+ self.assertRaises(DuplicateTimeseries, Summary, 'h', 'help', registry=registry)
+ self.assertRaises(DuplicateTimeseries, Gauge, 'h_count', 'help', registry=registry)
+ self.assertRaises(DuplicateTimeseries, Gauge, 'h_sum', 'help', registry=registry)
+ self.assertRaises(DuplicateTimeseries, Gauge, 'h_bucket', 'help', registry=registry)
+ self.assertRaises(DuplicateTimeseries, Gauge, 'h_created', 'help', registry=registry)
# The name of the histogram itself is also taken.
- self.assertRaises(ValueError, Gauge, 'h', 'help', registry=registry)
+ self.assertRaises(DuplicateTimeseries, Gauge, 'h', 'help', registry=registry)
Info('i', 'help', registry=registry)
- self.assertRaises(ValueError, Gauge, 'i_info', 'help', registry=registry)
+ self.assertRaises(DuplicateTimeseries, Gauge, 'i_info', 'help', registry=registry)
def test_unregister_works(self):
registry = CollectorRegistry()
s = Summary('s', 'help', registry=registry)
- self.assertRaises(ValueError, Gauge, 's_count', 'help', registry=registry)
+ self.assertRaises(DuplicateTimeseries, Gauge, 's_count', 'help', registry=registry)
registry.unregister(s)
Gauge('s_count', 'help', registry=registry)
+ def test_unregister_removes_no_names_collector(self):
+ registry = CollectorRegistry(support_collectors_without_names=True)
+
+ class NamelessCollector:
+ def collect(self):
+ return [GaugeMetricFamily('foo', 'help', value=42)]
+
+ collector = NamelessCollector()
+ registry.register(collector)
+ registry.unregister(collector)
+ # A nameless collector must be removed from the collectors-without-names
+ # list too, otherwise a restricted registry keeps collecting it after it
+ # was unregistered.
+ self.assertEqual([], list(registry.restricted_registry(['foo']).collect()))
+
def custom_collector(self, metric_family, registry):
class CustomCollector:
def collect(self):
diff --git a/tests/test_exposition.py b/tests/test_exposition.py
index a3c97820..1885480f 100644
--- a/tests/test_exposition.py
+++ b/tests/test_exposition.py
@@ -1,9 +1,11 @@
import gzip
from http.server import BaseHTTPRequestHandler, HTTPServer
import os
+import ssl
import threading
import time
import unittest
+import urllib
import pytest
@@ -16,7 +18,7 @@
from prometheus_client.core import GaugeHistogramMetricFamily, Timestamp
from prometheus_client.exposition import (
basic_auth_handler, choose_encoder, default_handler, MetricsHandler,
- passthrough_redirect_handler, tls_auth_handler,
+ passthrough_redirect_handler, start_wsgi_server, tls_auth_handler,
)
import prometheus_client.openmetrics.exposition as openmetrics
@@ -633,6 +635,148 @@ def test_prom_no_version(self):
self.assert_is_prom(exp)
+class TestWsgiTLS(unittest.TestCase):
+ def setUp(self):
+ self.certs_dir = os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), 'certs'
+ )
+ self.httpd = None
+ self.t = None
+
+ def tearDown(self):
+ if self.httpd:
+ self.httpd.shutdown()
+ self.httpd.server_close()
+ self.t.join()
+
+ def _assert_tls_connection(
+ self,
+ server_kwargs,
+ use_server_tls=True,
+ client_tls_kwargs=None,
+ request_tls_version=ssl.TLSVersion.TLSv1_3,
+ expect_exception=None
+ ):
+ self.httpd, self.t = start_wsgi_server(port=0, **server_kwargs)
+ port = self.httpd.server_address[1]
+
+ if use_server_tls:
+ ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+ ctx.minimum_version = request_tls_version
+ ctx.maximum_version = request_tls_version
+ ctx.load_verify_locations(
+ os.path.join(self.certs_dir, "server-ca.pem")
+ )
+
+ if client_tls_kwargs is not None:
+ ctx.load_cert_chain(**client_tls_kwargs)
+
+ url = f"https://localhost:{port}/metrics"
+ else:
+ ctx = None
+ url = f"http://localhost:{port}/metrics"
+
+ if expect_exception is not None:
+ self.assertRaises(
+ expect_exception,
+ urllib.request.urlopen,
+ url,
+ context=ctx
+ )
+ else:
+ response = urllib.request.urlopen(url, context=ctx)
+ self.assertEqual(response.status, 200)
+
+ def test_tls_disabled(self):
+ self._assert_tls_connection(server_kwargs={}, use_server_tls=False)
+
+ def test_tls_enabled(self):
+ server_kwargs = {
+ "certfile": os.path.join(self.certs_dir, "server-cert.pem"),
+ "keyfile": os.path.join(self.certs_dir, "server-key.pem"),
+ }
+ self._assert_tls_connection(server_kwargs)
+
+ def test_tls_untrusted_server_cert_raises(self):
+ server_kwargs = {
+ "certfile": os.path.join(self.certs_dir, "cert.pem"),
+ "keyfile": os.path.join(self.certs_dir, "key.pem"),
+ }
+ self._assert_tls_connection(
+ server_kwargs,
+ expect_exception=urllib.error.URLError
+ )
+
+ def test_tls_versions_configured_correctly(self):
+ server_kwargs = {
+ "certfile": os.path.join(self.certs_dir, "server-cert.pem"),
+ "keyfile": os.path.join(self.certs_dir, "server-key.pem"),
+ "tls_min_version": ssl.TLSVersion.TLSv1_2,
+ "tls_max_version": ssl.TLSVersion.TLSv1_3,
+ }
+ self._assert_tls_connection(
+ server_kwargs,
+ request_tls_version=ssl.TLSVersion.TLSv1_2
+ )
+
+ def test_tls_using_lower_version_than_min_raises(self):
+ server_kwargs = {
+ "certfile": os.path.join(self.certs_dir, "server-cert.pem"),
+ "keyfile": os.path.join(self.certs_dir, "server-key.pem"),
+ "tls_min_version": ssl.TLSVersion.TLSv1_3,
+ }
+ self._assert_tls_connection(
+ server_kwargs,
+ request_tls_version=ssl.TLSVersion.TLSv1_2,
+ expect_exception=urllib.error.URLError
+ )
+
+ def test_tls_using_higher_version_than_max_raises(self):
+ server_kwargs = {
+ "certfile": os.path.join(self.certs_dir, "server-cert.pem"),
+ "keyfile": os.path.join(self.certs_dir, "server-key.pem"),
+ "tls_max_version": ssl.TLSVersion.TLSv1_2,
+ }
+ self._assert_tls_connection(
+ server_kwargs,
+ request_tls_version=ssl.TLSVersion.TLSv1_3,
+ expect_exception=urllib.error.URLError
+ )
+
+ def test_mtls_enabled(self):
+ server_kwargs = {
+ "certfile": os.path.join(self.certs_dir, "server-cert.pem"),
+ "keyfile": os.path.join(self.certs_dir, "server-key.pem"),
+ "client_auth_required": True,
+ "client_cafile": os.path.join(self.certs_dir, "server-ca.pem"),
+ }
+ client_tls_kwargs = {
+ "certfile": os.path.join(self.certs_dir, "client-cert.pem"),
+ "keyfile": os.path.join(self.certs_dir, "client-key.pem")
+ }
+ self._assert_tls_connection(
+ server_kwargs,
+ client_tls_kwargs=client_tls_kwargs
+ )
+
+ def test_mtls_untrusted_client_cert_raises(self):
+ server_kwargs = {
+ "certfile": os.path.join(self.certs_dir, "server-cert.pem"),
+ "keyfile": os.path.join(self.certs_dir, "server-key.pem"),
+ "client_auth_required": True,
+ "client_cafile": os.path.join(self.certs_dir, "server-cert.pem"),
+ }
+ client_tls_kwargs = {
+ "certfile": os.path.join(self.certs_dir, "cert.pem"),
+ "keyfile": os.path.join(self.certs_dir, "key.pem")
+ }
+ self._assert_tls_connection(
+ server_kwargs,
+ client_tls_kwargs=client_tls_kwargs,
+ expect_exception=ssl.SSLError
+ )
+
+
@pytest.mark.parametrize("scenario", [
{
"name": "empty string",
diff --git a/tests/test_multiprocess.py b/tests/test_multiprocess.py
index ee0c7423..eab93f50 100644
--- a/tests/test_multiprocess.py
+++ b/tests/test_multiprocess.py
@@ -24,21 +24,26 @@ def setUp(self):
def tearDown(self):
os.environ.pop('prometheus_multiproc_dir', None)
os.environ.pop('PROMETHEUS_MULTIPROC_DIR', None)
+ values.close_all_multiprocess_files()
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:
+ warnings.simplefilter("always")
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)
+ if os.name != 'nt':
+ assert len(w) == 1
+ assert issubclass(w[-1].category, DeprecationWarning)
+ assert "PROMETHEUS_MULTIPROC_DIR" in str(w[-1].message)
+ else:
+ assert len(w) == 0
def test_mark_process_dead_respects_lowercase(self):
os.environ['prometheus_multiproc_dir'] = self.tempdir
@@ -61,8 +66,9 @@ def _value_class(self):
def tearDown(self):
del os.environ['PROMETHEUS_MULTIPROC_DIR']
- shutil.rmtree(self.tempdir)
+ values.close_all_multiprocess_files()
values.ValueClass = MutexValue
+ shutil.rmtree(self.tempdir)
def test_counter_adds(self):
c1 = Counter('c', 'help', registry=None)
@@ -119,6 +125,7 @@ def test_gauge_liveall(self):
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'}))
+ values.close_all_multiprocess_files()
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'}))
@@ -140,6 +147,7 @@ def test_gauge_livemin(self):
g1.set(1)
g2.set(2)
self.assertEqual(1, self.registry.get_sample_value('g'))
+ values.close_all_multiprocess_files()
mark_process_dead(123, os.environ['PROMETHEUS_MULTIPROC_DIR'])
self.assertEqual(2, self.registry.get_sample_value('g'))
@@ -160,6 +168,7 @@ def test_gauge_livemax(self):
g1.set(2)
g2.set(1)
self.assertEqual(2, self.registry.get_sample_value('g'))
+ values.close_all_multiprocess_files()
mark_process_dead(123, os.environ['PROMETHEUS_MULTIPROC_DIR'])
self.assertEqual(1, self.registry.get_sample_value('g'))
@@ -171,6 +180,7 @@ def test_gauge_sum(self):
g1.set(1)
g2.set(2)
self.assertEqual(3, self.registry.get_sample_value('g'))
+ values.close_all_multiprocess_files()
mark_process_dead(123, os.environ['PROMETHEUS_MULTIPROC_DIR'])
self.assertEqual(3, self.registry.get_sample_value('g'))
@@ -182,6 +192,7 @@ def test_gauge_livesum(self):
g1.set(1)
g2.set(2)
self.assertEqual(3, self.registry.get_sample_value('g'))
+ values.close_all_multiprocess_files()
mark_process_dead(123, os.environ['PROMETHEUS_MULTIPROC_DIR'])
self.assertEqual(2, self.registry.get_sample_value('g'))
@@ -192,6 +203,7 @@ def test_gauge_mostrecent(self):
g2.set(2)
g1.set(1)
self.assertEqual(1, self.registry.get_sample_value('g'))
+ values.close_all_multiprocess_files()
mark_process_dead(123, os.environ['PROMETHEUS_MULTIPROC_DIR'])
self.assertEqual(1, self.registry.get_sample_value('g'))
@@ -202,6 +214,7 @@ def test_gauge_livemostrecent(self):
g2.set(2)
g1.set(1)
self.assertEqual(1, self.registry.get_sample_value('g'))
+ values.close_all_multiprocess_files()
mark_process_dead(123, os.environ['PROMETHEUS_MULTIPROC_DIR'])
self.assertEqual(2, self.registry.get_sample_value('g'))
@@ -626,6 +639,7 @@ def test_corruption_detected(self):
list(self.d.read_all_values())
def tearDown(self):
+ self.d.close()
os.unlink(self.tempfile)
diff --git a/tests/test_parser.py b/tests/test_parser.py
index 49c4dc8c..8436dd0c 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -1,3 +1,4 @@
+import importlib.util
import math
import unittest
@@ -375,8 +376,11 @@ def collect(self):
self.assertEqual(text.encode('utf-8'), generate_latest(registry, ALLOWUTF8))
-def test_benchmark_text_string_to_metric_families(benchmark):
- text = """# HELP go_gc_duration_seconds A summary of the GC invocation durations.
+HAS_BENCHMARK = importlib.util.find_spec("pytest_benchmark") is not None
+
+if HAS_BENCHMARK:
+ def test_benchmark_text_string_to_metric_families(benchmark):
+ text = """# HELP go_gc_duration_seconds A summary of the GC invocation durations.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 0.013300656000000001
go_gc_duration_seconds{quantile="0.25"} 0.013638736
@@ -422,11 +426,11 @@ def test_benchmark_text_string_to_metric_families(benchmark):
hist_sum 2
"""
- @benchmark
- def _():
- # We need to convert the generator to a full list in order to
- # accurately measure the time to yield everything.
- return list(text_string_to_metric_families(text))
+ @benchmark
+ def _():
+ # We need to convert the generator to a full list in order to
+ # accurately measure the time to yield everything.
+ return list(text_string_to_metric_families(text))
if __name__ == '__main__':
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 00000000..50eac33c
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,18 @@
+import unittest
+
+from prometheus_client.utils import floatToGoString
+
+
+class TestFloatToGoString(unittest.TestCase):
+ def test_exponent_two_digits_has_no_leading_zero(self):
+ # floatToGoString mirrors Go's strconv.FormatFloat(f, 'g', -1, 64),
+ # which pads the exponent to a minimum of two digits. A two-digit
+ # exponent must not gain a spurious leading zero.
+ self.assertEqual('1e+10', floatToGoString(1e10))
+ self.assertEqual('1e+15', floatToGoString(1e15))
+ self.assertEqual('1.234567890123e+12', floatToGoString(1234567890123.0))
+
+ def test_exponent_one_digit_is_zero_padded(self):
+ # Single-digit exponents keep the two-digit zero padding.
+ self.assertEqual('1e+06', floatToGoString(1e6))
+ self.assertEqual('1.234567e+06', floatToGoString(1234567.0))