diff --git a/.circleci/config.yml b/.circleci/config.yml
deleted file mode 100644
index f29bd265..00000000
--- a/.circleci/config.yml
+++ /dev/null
@@ -1,93 +0,0 @@
----
-version: 2.1
-
-executors:
- python:
- docker:
- - image: cimg/python:3.9
-
-jobs:
- flake8_lint:
- executor: python
- steps:
- - checkout
- - run: pip install tox
- - run: tox -e flake8
- isort_lint:
- executor: python
- steps:
- - checkout
- - run: pip install tox
- - run: tox -e isort
- mypy_lint:
- executor: python
- steps:
- - checkout
- - run: pip install tox
- - run: tox -e mypy
- test:
- parameters:
- python:
- type: string
- docker:
- - image: cimg/python:<< parameters.python >>
- environment:
- TOXENV: "py<< parameters.python >>"
- steps:
- - checkout
- - run: echo 'export PATH=$HOME/.local/bin:$PATH' >> $BASH_ENV
- - run: pip install --user tox "virtualenv<20.22.0"
- - run: tox
- test_nooptionals:
- parameters:
- python:
- type: string
- docker:
- - image: cimg/python:<< parameters.python >>
- environment:
- TOXENV: "py<< parameters.python >>-nooptionals"
- steps:
- - checkout
- - run: pip install tox
- - run: tox
- test_pypy:
- parameters:
- python:
- type: string
- docker:
- - image: pypy:<< parameters.python >>
- environment:
- TOXENV: "pypy<< parameters.python >>"
- steps:
- - checkout
- - run: pip install tox
- - run: tox
-
-
-workflows:
- version: 2
- client_python:
- jobs:
- - flake8_lint
- - isort_lint
- - mypy_lint
- - test:
- matrix:
- parameters:
- python:
- - "3.9.18"
- - "3.10"
- - "3.11"
- - "3.12"
- - "3.13"
- - "3.14"
- - test_nooptionals:
- matrix:
- parameters:
- python:
- - "3.9"
- - test_pypy:
- matrix:
- parameters:
- python:
- - "3.9"
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
new file mode 100644
index 00000000..a7e4e094
--- /dev/null
+++ b/.github/workflows/ci.yaml
@@ -0,0 +1,110 @@
+name: CI
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+
+permissions:
+ contents: read
+
+jobs:
+ flake8_lint:
+ runs-on: ubuntu-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.9'
+ - name: Install tox
+ run: pip install tox
+ - name: Run flake8
+ run: tox -e flake8
+
+ isort_lint:
+ runs-on: ubuntu-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.9'
+ - name: Install tox
+ run: pip install tox
+ - name: Run isort
+ run: tox -e isort
+
+ mypy_lint:
+ runs-on: ubuntu-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.9'
+ - name: Install tox
+ run: pip install tox
+ - name: Run mypy
+ run: tox -e mypy
+
+ test:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
+ steps:
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies
+ run: |
+ pip install --user tox "virtualenv<20.22.0"
+ echo "$HOME/.local/bin" >> $GITHUB_PATH
+ - name: Set tox environment
+ id: toxenv
+ run: |
+ VERSION="${{ matrix.python-version }}"
+ # Extract major.minor version (strip patch if present)
+ TOX_VERSION=$(echo "$VERSION" | cut -d. -f1,2)
+ echo "toxenv=py${TOX_VERSION}" >> $GITHUB_OUTPUT
+ - name: Run tests
+ run: tox
+ env:
+ TOXENV: ${{ steps.toxenv.outputs.toxenv }}
+
+ test_nooptionals:
+ runs-on: ubuntu-latest
+ env:
+ PYTHON_VERSION: '3.9'
+ steps:
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ - name: Set up Python
+ uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
+ with:
+ python-version: ${{ env.PYTHON_VERSION }}
+ - name: Install tox
+ run: pip install tox
+ - name: Run tests without optional dependencies
+ run: tox
+ env:
+ TOXENV: py${{ env.PYTHON_VERSION }}-nooptionals
+
+ test_pypy:
+ runs-on: ubuntu-latest
+ env:
+ PYTHON_VERSION: '3.9'
+ steps:
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
+ - name: Set up PyPy
+ uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
+ with:
+ python-version: pypy-${{ env.PYTHON_VERSION }}
+ - name: Install tox
+ run: pip install tox
+ - name: Run tests with PyPy
+ run: tox
+ env:
+ TOXENV: pypy${{ env.PYTHON_VERSION }}
diff --git a/.github/workflows/github-pages.yaml b/.github/workflows/github-pages.yaml
index 621f2d73..d8db8cbc 100644
--- a/.github/workflows/github-pages.yaml
+++ b/.github/workflows/github-pages.yaml
@@ -11,9 +11,6 @@ on:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
- pages: write
- id-token: write
- actions: read
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
@@ -32,6 +29,9 @@ jobs:
runs-on: ubuntu-latest
env:
HUGO_VERSION: 0.145.0
+ permissions:
+ pages: write
+ id-token: write
steps:
- name: Install Hugo CLI
run: |
@@ -40,13 +40,13 @@ jobs:
#- name: Install Dart Sass
# run: sudo snap install dart-sass
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
submodules: recursive
fetch-depth: 0
- name: Setup Pages
id: pages
- uses: actions/configure-pages@v5
+ uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0
- name: Install Node.js dependencies
run: "[[ -f package-lock.json || -f npm-shrinkwrap.json ]] && npm ci || true"
working-directory: ./docs
@@ -62,7 +62,7 @@ jobs:
--baseURL "${{ steps.pages.outputs.base_url }}/"
working-directory: ./docs
- name: Upload artifact
- uses: actions/upload-pages-artifact@v3
+ uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0
with:
path: ./docs/public
@@ -73,7 +73,11 @@ jobs:
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
+ permissions:
+ pages: write
+ id-token: write
+ actions: read
steps:
- name: Deploy to GitHub Pages
id: deployment
- uses: actions/deploy-pages@v4
+ uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5
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/_index.md b/docs/content/exporting/http/_index.md
index dc1b8f2c..f7a6aac6 100644
--- a/docs/content/exporting/http/_index.md
+++ b/docs/content/exporting/http/_index.md
@@ -24,6 +24,7 @@ to shutdown the server gracefully:
```python
server, t = start_http_server(8000)
server.shutdown()
+server.server_close()
t.join()
```
diff --git a/docs/content/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 33507cd9..f7befef8 100644
--- a/docs/content/multiprocess/_index.md
+++ b/docs/content/multiprocess/_index.md
@@ -10,9 +10,12 @@ it's common to have processes rather than threads to handle large workloads.
To handle this the client library can be put in multiprocess mode.
This comes with a number of limitations:
-- Registries can not be used as normal, all instantiated metrics are exported
+- Registries can not be used as normal:
+ - all instantiated metrics are collected
- Registering metrics to a registry later used by a `MultiProcessCollector`
may cause duplicate metrics to be exported
+ - Filtering on metrics works if and only if the constructor was called with
+ `support_collectors_without_names=True` and it but might be inefficient.
- Custom collectors do not work (e.g. cpu and memory metrics)
- Gauges cannot use `set_function`
- Info and Enum metrics do not work
@@ -49,7 +52,7 @@ MY_COUNTER = Counter('my_counter', 'Description of my counter')
# Expose metrics.
def app(environ, start_response):
- registry = CollectorRegistry()
+ registry = CollectorRegistry(support_collectors_without_names=True)
multiprocess.MultiProcessCollector(registry)
data = generate_latest(registry)
status = '200 OK'
@@ -93,3 +96,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/django/exposition.py b/prometheus_client/django/exposition.py
index 085e8fcf..71fc8d8a 100644
--- a/prometheus_client/django/exposition.py
+++ b/prometheus_client/django/exposition.py
@@ -6,7 +6,6 @@
import prometheus_client
from prometheus_client import multiprocess
from prometheus_client.exposition import _bake_output
-from prometheus_client.registry import registry
class PrometheusDjangoView(View):
@@ -17,7 +16,7 @@ def get(self, request, *args, **kwargs):
if self.registry is None:
if self.multiprocess_mode:
self.registry = prometheus_client.CollectorRegistry()
- multiprocess.MultiProcessCollector(registry)
+ multiprocess.MultiProcessCollector(self.registry)
else:
self.registry = prometheus_client.REGISTRY
accept_header = request.headers.get("Accept")
diff --git a/prometheus_client/exposition.py b/prometheus_client/exposition.py
index ca06d916..2d402a0f 100644
--- a/prometheus_client/exposition.py
+++ b/prometheus_client/exposition.py
@@ -783,8 +783,9 @@ def _escape_grouping_key(k, v):
if v == "":
# Per https://github.com/prometheus/pushgateway/pull/346.
return k + "@base64", "="
- elif '/' in v:
+ elif '/' in v or ' ' in v:
# Added in Pushgateway 0.9.0.
+ # Use base64 encoding for values containing slashes or spaces
return k + "@base64", base64.urlsafe_b64encode(v.encode("utf-8")).decode("utf-8")
else:
return k, quote_plus(v)
diff --git a/prometheus_client/registry.py b/prometheus_client/registry.py
index 9934117d..d4cfc273 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.
@@ -23,12 +31,15 @@ class CollectorRegistry:
exposition formats.
"""
- def __init__(self, auto_describe: bool = False, target_info: Optional[Dict[str, str]] = None):
+ def __init__(self, auto_describe: bool = False, target_info: Optional[Dict[str, str]] = None,
+ support_collectors_without_names: bool = False):
self._collector_to_names: Dict[Collector, List[str]] = {}
self._names_to_collectors: Dict[str, Collector] = {}
self._auto_describe = auto_describe
self._lock = Lock()
self._target_info: Optional[Dict[str, str]] = {}
+ self._support_collectors_without_names = support_collectors_without_names
+ self._collectors_without_names: List[Collector] = []
self.set_target_info(target_info)
def register(self, collector: Collector) -> None:
@@ -37,12 +48,12 @@ 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
+ if self._support_collectors_without_names and not names:
+ self._collectors_without_names.append(collector)
def unregister(self, collector: Collector) -> None:
"""Remove a collector from the registry."""
@@ -145,7 +156,7 @@ def __init__(self, names: Iterable[str], registry: CollectorRegistry):
self._registry = registry
def collect(self) -> Iterable[Metric]:
- collectors = set()
+ collectors = set(self._registry._collectors_without_names)
target_info_metric = None
with self._registry._lock:
if 'target_info' in self._name_set and self._registry._target_info:
diff --git a/pyproject.toml b/pyproject.toml
index d45ef12d..336cfb4f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "prometheus_client"
-version = "0.24.0"
+version = "0.25.0"
description = "Python client for the Prometheus monitoring system."
readme = "README.md"
license = "Apache-2.0 AND BSD-2-Clause"
diff --git a/tests/test_asgi.py b/tests/test_asgi.py
index d4933cec..6e795e21 100644
--- a/tests/test_asgi.py
+++ b/tests/test_asgi.py
@@ -223,3 +223,32 @@ def test_qs_parsing(self):
asyncio.new_event_loop().run_until_complete(
self.communicator.wait()
)
+
+ def test_qs_parsing_multi(self):
+ """Only metrics that match the 'name[]' query string param appear"""
+
+ app = make_asgi_app(self.registry)
+ metrics = [
+ ("asdf", "first test metric", 1),
+ ("bsdf", "second test metric", 2),
+ ("csdf", "third test metric", 3)
+ ]
+
+ for m in metrics:
+ self.increment_metrics(*m)
+
+ self.seed_app(app)
+ self.scope['query_string'] = "&".join([f"name[]={m[0]}_total" for m in metrics[0:2]]).encode("utf-8")
+ self.send_default_request()
+
+ outputs = self.get_all_output()
+ response_body = outputs[1]
+ output = response_body['body'].decode('utf8')
+
+ self.assert_metrics(output, *metrics[0])
+ self.assert_metrics(output, *metrics[1])
+ self.assert_not_metrics(output, *metrics[2])
+
+ asyncio.new_event_loop().run_until_complete(
+ self.communicator.wait()
+ )
diff --git a/tests/test_core.py b/tests/test_core.py
index c7c9c14f..cee4bfb0 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,
)
@@ -378,6 +378,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)
@@ -908,41 +916,41 @@ 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)
@@ -1024,6 +1032,24 @@ def test_restricted_registry_does_not_call_extra(self):
self.assertEqual([m], list(registry.restricted_registry(['s_sum']).collect()))
mock_collector.collect.assert_not_called()
+ def test_restricted_registry_ignore_no_names_collectors(self):
+ from unittest.mock import MagicMock
+ registry = CollectorRegistry()
+ mock_collector = MagicMock()
+ mock_collector.describe.return_value = []
+ registry.register(mock_collector)
+ self.assertEqual(list(registry.restricted_registry(['metric']).collect()), [])
+ mock_collector.collect.assert_not_called()
+
+ def test_restricted_registry_collects_no_names_collectors(self):
+ from unittest.mock import MagicMock
+ registry = CollectorRegistry(support_collectors_without_names=True)
+ mock_collector = MagicMock()
+ mock_collector.describe.return_value = []
+ registry.register(mock_collector)
+ self.assertEqual(list(registry.restricted_registry(['metric']).collect()), [])
+ mock_collector.collect.assert_called()
+
def test_restricted_registry_does_not_yield_while_locked(self):
registry = CollectorRegistry(target_info={'foo': 'bar'})
Summary('s', 'help', registry=registry).observe(7)
diff --git a/tests/test_exposition.py b/tests/test_exposition.py
index aceff738..a3c97820 100644
--- a/tests/test_exposition.py
+++ b/tests/test_exposition.py
@@ -301,6 +301,13 @@ def test_push_with_groupingkey_empty_label(self):
self.assertEqual(self.requests[0][0].headers.get('content-type'), CONTENT_TYPE_PLAIN_0_0_4)
self.assertEqual(self.requests[0][1], b'# HELP g help\n# TYPE g gauge\ng 0.0\n')
+ def test_push_with_groupingkey_with_spaces(self):
+ push_to_gateway(self.address, "my_job", self.registry, {'label': 'value with spaces'})
+ self.assertEqual(self.requests[0][0].command, 'PUT')
+ self.assertEqual(self.requests[0][0].path, '/metrics/job/my_job/label@base64/dmFsdWUgd2l0aCBzcGFjZXM=')
+ self.assertEqual(self.requests[0][0].headers.get('content-type'), CONTENT_TYPE_PLAIN_0_0_4)
+ self.assertEqual(self.requests[0][1], b'# HELP g help\n# TYPE g gauge\ng 0.0\n')
+
def test_push_with_complex_groupingkey(self):
push_to_gateway(self.address, "my_job", self.registry, {'a': 9, 'b': 'a/ z'})
self.assertEqual(self.requests[0][0].command, 'PUT')
diff --git a/tests/test_multiprocess.py b/tests/test_multiprocess.py
index c2f71d26..ee0c7423 100644
--- a/tests/test_multiprocess.py
+++ b/tests/test_multiprocess.py
@@ -52,7 +52,7 @@ def setUp(self):
self.tempdir = tempfile.mkdtemp()
os.environ['PROMETHEUS_MULTIPROC_DIR'] = self.tempdir
values.ValueClass = MultiProcessValue(lambda: 123)
- self.registry = CollectorRegistry()
+ self.registry = CollectorRegistry(support_collectors_without_names=True)
self.collector = MultiProcessCollector(self.registry)
@property
@@ -358,6 +358,35 @@ def add_label(key, value):
self.assertEqual(metrics['h'].samples, expected_histogram)
+ def test_restrict(self):
+ pid = 0
+ values.ValueClass = MultiProcessValue(lambda: pid)
+ labels = {i: i for i in 'abcd'}
+
+ def add_label(key, value):
+ l = labels.copy()
+ l[key] = value
+ return l
+
+ c = Counter('c', 'help', labelnames=labels.keys(), registry=None)
+ g = Gauge('g', 'help', labelnames=labels.keys(), registry=None)
+
+ c.labels(**labels).inc(1)
+ g.labels(**labels).set(1)
+
+ pid = 1
+
+ c.labels(**labels).inc(1)
+ g.labels(**labels).set(1)
+
+ metrics = {m.name: m for m in self.registry.restricted_registry(['c_total']).collect()}
+
+ self.assertEqual(metrics.keys(), {'c'})
+
+ self.assertEqual(
+ metrics['c'].samples, [Sample('c_total', labels, 2.0)]
+ )
+
def test_collect_preserves_help(self):
pid = 0
values.ValueClass = MultiProcessValue(lambda: pid)