From 130a4e8a3f80e75122ea9f646b063a6f21db9b42 Mon Sep 17 00:00:00 2001 From: Krishna <107162115+k1chik@users.noreply.github.com> Date: Fri, 17 Apr 2026 11:11:33 -0400 Subject: [PATCH 1/6] docs: add API reference for pushgateway, textfile, and multiprocess (#1162) Closes #1161 Adds parameter tables and formal API reference sections to three pages that previously had examples but no parameter documentation. pushgateway.md: documents push_to_gateway, pushadd_to_gateway, delete_from_gateway, instance_ip_grouping_key, and all four built-in handlers (default, basic_auth, tls_auth, passthrough_redirect). textfile.md: documents write_to_textfile with all four parameters including the previously undocumented escaping and tmpdir, plus atomic write semantics and error behavior. multiprocess/_index.md: documents MultiProcessCollector constructor and mark_process_dead with parameter tables including the previously undocumented path parameter on both. Signed-off-by: k1chik <107162115+k1chik@users.noreply.github.com> --- docs/content/exporting/pushgateway.md | 106 ++++++++++++++++++++++++++ docs/content/exporting/textfile.md | 22 +++++- docs/content/multiprocess/_index.md | 50 ++++++++++++ 3 files changed, 177 insertions(+), 1 deletion(-) 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/multiprocess/_index.md b/docs/content/multiprocess/_index.md index 42ea6a67..f7befef8 100644 --- a/docs/content/multiprocess/_index.md +++ b/docs/content/multiprocess/_index.md @@ -96,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) +``` From e75a74f7a9000f414ddacefd6797d560d4bb6731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Vokr=C3=A1=C4=8Dko?= Date: Fri, 24 Apr 2026 23:07:17 +0200 Subject: [PATCH 2/6] Expose measured duration on Timer context manager (#1166) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assigning the .time() context manager (with ... as t) now yields a Timer whose .duration attribute holds the observed value in seconds after the block exits. This lets callers reuse the measurement (logging, further metrics) without calling default_timer() a second time. Signed-off-by: Lukáš Vokráčko --- docs/content/instrumenting/gauge.md | 4 ++++ docs/content/instrumenting/histogram.md | 4 ++++ docs/content/instrumenting/summary.md | 4 ++++ prometheus_client/context_managers.py | 5 +++-- tests/test_core.py | 8 ++++++++ 5 files changed, 23 insertions(+), 2 deletions(-) diff --git a/docs/content/instrumenting/gauge.md b/docs/content/instrumenting/gauge.md index 43168a68..62294944 100644 --- a/docs/content/instrumenting/gauge.md +++ b/docs/content/instrumenting/gauge.md @@ -108,6 +108,10 @@ def process(): with g.time(): pass + +with g.time() as t: + pass +print(t.duration) # observed time in seconds. ``` ### `set_function(f)` diff --git a/docs/content/instrumenting/histogram.md b/docs/content/instrumenting/histogram.md index 8975d859..fa0ffe1a 100644 --- a/docs/content/instrumenting/histogram.md +++ b/docs/content/instrumenting/histogram.md @@ -86,6 +86,10 @@ def process(): with h.time(): pass + +with h.time() as t: + pass +print(t.duration) # observed time in seconds. ``` ## Labels diff --git a/docs/content/instrumenting/summary.md b/docs/content/instrumenting/summary.md index 55428ecb..714dfd2f 100644 --- a/docs/content/instrumenting/summary.md +++ b/docs/content/instrumenting/summary.md @@ -68,6 +68,10 @@ def process(): with s.time(): pass + +with s.time() as t: + pass +print(t.duration) # observed time in seconds. ``` ## Labels 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/tests/test_core.py b/tests/test_core.py index 66492c6f..cdf32bfa 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -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) From 482656c8c07b78668adb5f3171dfe71ccc2396b2 Mon Sep 17 00:00:00 2001 From: Krishna <107162115+k1chik@users.noreply.github.com> Date: Fri, 24 Apr 2026 17:10:21 -0400 Subject: [PATCH 3/6] docs: add API reference for CollectorRegistry and custom collector classes (#1169) Closes #1163 collector/custom.md: Collector protocol section (collect/describe), value vs labels mutual exclusivity note, full constructor and add_metric tables for GaugeMetricFamily, CounterMetricFamily, SummaryMetricFamily, HistogramMetricFamily, and InfoMetricFamily, plus a runnable real-world example. collector/_index.md: constructor parameter tables for ProcessCollector, PlatformCollector, and GCCollector, with exported metrics listed for each. registry/_index.md (new): CollectorRegistry constructor and all public methods (register, unregister, collect, restricted_registry, get_sample_value, set_target_info, get_target_info), the global REGISTRY instance, and examples for isolated registry usage and registry=None. All code examples verified by running them in Python. Signed-off-by: k1chik <107162115+k1chik@users.noreply.github.com> --- docs/content/collector/_index.md | 77 +++++++++- docs/content/collector/custom.md | 250 ++++++++++++++++++++++++++++++- docs/content/registry/_index.md | 141 +++++++++++++++++ 3 files changed, 464 insertions(+), 4 deletions(-) create mode 100644 docs/content/registry/_index.md 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..62c0180a 100644 --- a/docs/content/collector/custom.md +++ b/docs/content/collector/custom.md @@ -35,4 +35,252 @@ 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. + +### `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 + +### 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. | + +```python +# single unlabelled info metric +yield InfoMetricFamily('build', 'Build metadata', value={'version': '1.2.3', 'commit': 'abc123'}) + +# labelled: one info metric per service +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/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) +``` From 2c84c2a545e8a48f1c9af4f57ac13472591e4216 Mon Sep 17 00:00:00 2001 From: Krishna <107162115+k1chik@users.noreply.github.com> Date: Mon, 4 May 2026 11:42:38 -0400 Subject: [PATCH 4/6] docs: follow-up fixes for collect() generator examples (#1169) (#1172) * docs: clarify collect() generator usage and API Reference snippet context Add a note to the collect() protocol section explaining that yield is idiomatic (generator iterates lazily, no state between scrapes) and a preamble to the API Reference section clarifying that code snippets belong inside a collect() method. Follows up on review feedback in #1169. Signed-off-by: k1chik * docs: split InfoMetricFamily example into two separate blocks The single block with two yield statements looked like one collect() yielding both patterns. Split into labelled prose + code pairs to make clear they are alternatives, not sequential yields. Signed-off-by: k1chik --------- Signed-off-by: k1chik --- docs/content/collector/custom.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/content/collector/custom.md b/docs/content/collector/custom.md index 62c0180a..c1979109 100644 --- a/docs/content/collector/custom.md +++ b/docs/content/collector/custom.md @@ -47,6 +47,11 @@ can also implement `describe`. 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 @@ -76,6 +81,10 @@ 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 @@ -232,11 +241,15 @@ InfoMetricFamily(name, documentation, value=None, labels=None) | `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 -# single unlabelled info metric yield InfoMetricFamily('build', 'Build metadata', value={'version': '1.2.3', 'commit': 'abc123'}) +``` + +Labelled — one info metric per service: -# 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'}) From 6133347b651fd83a82f0109ff5abadb2de8c3a7c Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Thu, 7 May 2026 02:35:55 +0900 Subject: [PATCH 5/6] Use specific exception for duplicate timeseries (#1074) Use sub-class of ValueError instead of ValueError, so that we can distinguish issues caused by wrong input (like invalid name format) from duplicate metrics being registered into the same registry. Signed-off-by: Takashi Kajinami --- prometheus_client/core.py | 3 ++- prometheus_client/registry.py | 14 ++++++++---- tests/test_core.py | 42 +++++++++++++++++------------------ 3 files changed, 33 insertions(+), 26 deletions(-) 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/registry.py b/prometheus_client/registry.py index c2b55d15..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. @@ -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 diff --git a/tests/test_core.py b/tests/test_core.py index cdf32bfa..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, ) @@ -916,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) From c0e416da2c959dfc5d9dd67b356efb663dcbd0ec Mon Sep 17 00:00:00 2001 From: PrometheusBot Date: Wed, 3 Jun 2026 01:18:37 +0200 Subject: [PATCH 6/6] Update common Prometheus files (#1179) Signed-off-by: prombot --- SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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/)