Skip to content

Commit

Permalink
Upgrade python3-prometheus-client to v0.18.0.
Browse files Browse the repository at this point in the history
  • Loading branch information
gizmoguy committed Nov 15, 2023
1 parent 96c247d commit a890cea
Show file tree
Hide file tree
Showing 17 changed files with 169 additions and 73 deletions.
5 changes: 2 additions & 3 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,11 @@ workflows:
matrix:
parameters:
python:
- "3.6"
- "3.7"
- "3.8"
- "3.9"
- "3.10"
- "3.11"
- "3.12"
- test_nooptionals:
matrix:
parameters:
Expand All @@ -90,4 +89,4 @@ workflows:
matrix:
parameters:
python:
- "3.7"
- "3.8"
48 changes: 46 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,20 @@ h = Histogram('request_latency_seconds', 'Description of histogram')
h.observe(4.7, {'trace_id': 'abc123'})
```

Exemplars are only rendered in the OpenMetrics exposition format. If using the
HTTP server or apps in this library, content negotiation can be used to specify
OpenMetrics (which is done by default in Prometheus). Otherwise it will be
necessary to use `generate_latest` from
`prometheus_client.openmetrics.exposition` to view exemplars.

To view exemplars in Prometheus it is also necessary to enable the the
exemplar-storage feature flag:
```
--enable-feature=exemplar-storage
```
Additional information is available in [the Prometheus
documentation](https://prometheus.io/docs/prometheus/latest/feature_flags/#exemplars-storage).

### Disabling `_created` metrics

By default counters, histograms, and summaries export an additional series
Expand Down Expand Up @@ -590,8 +604,9 @@ To do so you need to create a custom collector, for example:

```python
from prometheus_client.core import GaugeMetricFamily, CounterMetricFamily, REGISTRY
from prometheus_client.registry import Collector

class CustomCollector(object):
class CustomCollector(Collector):
def collect(self):
yield GaugeMetricFamily('my_gauge', 'Help text', value=7)
c = CounterMetricFamily('my_counter_total', 'Help text', labels=['foo'])
Expand Down Expand Up @@ -696,9 +711,10 @@ Gauges have several modes they can run in, which can be selected with the `multi
- 'min': Return a single timeseries that is the minimum of the values of all processes (alive or dead).
- 'max': Return a single timeseries that is the maximum of the values of all processes (alive or dead).
- 'sum': Return a single timeseries that is the sum of the values of all processes (alive or dead).
- 'mostrecent': Return a single timeseries that is the most recent value among all processes (alive or dead).

Prepend 'live' to the beginning of the mode to return the same result but only considering living processes
(e.g., 'liveall, 'livesum', 'livemax', 'livemin').
(e.g., 'liveall, 'livesum', 'livemax', 'livemin', 'livemostrecent').

```python
from prometheus_client import Gauge
Expand All @@ -722,6 +738,34 @@ for family in text_string_to_metric_families(u"my_gauge 1.0\n"):
print("Name: {0} Labels: {1} Value: {2}".format(*sample))
```

## Restricted registry

Registries support restriction to only return specific metrics.
If you’re using the built-in HTTP server, you can use the GET parameter "name[]", since it’s an array it can be used multiple times.
If you’re directly using `generate_latest`, you can use the function `restricted_registry()`.

```python
curl --get --data-urlencode "name[]=python_gc_objects_collected_total" --data-urlencode "name[]=python_info" http://127.0.0.1:9200/metrics
```

```python
from prometheus_client import generate_latest

generate_latest(REGISTRY.restricted_registry(['python_gc_objects_collected_total', 'python_info']))
```

```python
# HELP python_info Python platform information
# TYPE python_info gauge
python_info{implementation="CPython",major="3",minor="9",patchlevel="3",version="3.9.3"} 1.0
# HELP python_gc_objects_collected_total Objects collected during gc
# TYPE python_gc_objects_collected_total counter
python_gc_objects_collected_total{generation="0"} 73129.0
python_gc_objects_collected_total{generation="1"} 8594.0
python_gc_objects_collected_total{generation="2"} 296.0
```


## Links

* [Releases](https://github.com/prometheus/client_python/releases): The releases page shows the history of the project and acts as a changelog.
Expand Down
9 changes: 7 additions & 2 deletions prometheus_client/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/env python

from . import (
exposition, gc_collector, metrics, metrics_core, platform_collector,
Expand All @@ -11,7 +11,10 @@
write_to_textfile,
)
from .gc_collector import GC_COLLECTOR, GCCollector
from .metrics import Counter, Enum, Gauge, Histogram, Info, Summary
from .metrics import (
Counter, disable_created_metrics, enable_created_metrics, Enum, Gauge,
Histogram, Info, Summary,
)
from .metrics_core import Metric
from .platform_collector import PLATFORM_COLLECTOR, PlatformCollector
from .process_collector import PROCESS_COLLECTOR, ProcessCollector
Expand All @@ -27,6 +30,8 @@
'Histogram',
'Info',
'Enum',
'enable_created_metrics',
'disable_created_metrics',
'CONTENT_TYPE_LATEST',
'generate_latest',
'MetricsHandler',
Expand Down
9 changes: 3 additions & 6 deletions prometheus_client/context_managers.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import sys
from timeit import default_timer
from types import TracebackType
from typing import (
Any, Callable, Optional, Tuple, Type, TYPE_CHECKING, TypeVar, Union,
Any, Callable, Literal, Optional, Tuple, Type, TYPE_CHECKING, TypeVar,
Union,
)

if sys.version_info >= (3, 8, 0):
from typing import Literal

from .decorator import decorate

if TYPE_CHECKING:
Expand All @@ -23,7 +20,7 @@ def __init__(self, counter: "Counter", exception: Union[Type[BaseException], Tup
def __enter__(self) -> None:
pass

def __exit__(self, typ: Optional[Type[BaseException]], value: Optional[BaseException], traceback: Optional[TracebackType]) -> "Literal[False]":
def __exit__(self, typ: Optional[Type[BaseException]], value: Optional[BaseException], traceback: Optional[TracebackType]) -> Literal[False]:
if isinstance(value, self._exception):
self._counter.inc()
return False
Expand Down
6 changes: 1 addition & 5 deletions prometheus_client/exposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@

CONTENT_TYPE_LATEST = 'text/plain; version=0.0.4; charset=utf-8'
"""Content type of the latest text format"""
PYTHON376_OR_NEWER = sys.version_info > (3, 7, 5)


class _PrometheusRedirectHandler(HTTPRedirectHandler):
Expand Down Expand Up @@ -545,10 +544,7 @@ def _use_gateway(
) -> None:
gateway_url = urlparse(gateway)
# See https://bugs.python.org/issue27657 for details on urlparse in py>=3.7.6.
if not gateway_url.scheme or (
PYTHON376_OR_NEWER
and gateway_url.scheme not in ['http', 'https']
):
if not gateway_url.scheme or gateway_url.scheme not in ['http', 'https']:
gateway = f'http://{gateway}'

gateway = gateway.rstrip('/')
Expand Down
31 changes: 26 additions & 5 deletions prometheus_client/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import time
import types
from typing import (
Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple, Type,
TypeVar, Union,
Any, Callable, Dict, Iterable, List, Literal, Optional, Sequence, Tuple,
Type, TypeVar, Union,
)

from . import values # retain this import style for testability
Expand Down Expand Up @@ -70,6 +70,18 @@ def _get_use_created() -> bool:
_use_created = _get_use_created()


def disable_created_metrics():
"""Disable exporting _created metrics on counters, histograms, and summaries."""
global _use_created
_use_created = False


def enable_created_metrics():
"""Enable exporting _created metrics on counters, histograms, and summaries."""
global _use_created
_use_created = True


class MetricWrapperBase(Collector):
_type: Optional[str] = None
_reserved_labelnames: Sequence[str] = ()
Expand Down Expand Up @@ -346,7 +358,8 @@ def f():
d.set_function(lambda: len(my_dict))
"""
_type = 'gauge'
_MULTIPROC_MODES = frozenset(('all', 'liveall', 'min', 'livemin', 'max', 'livemax', 'sum', 'livesum'))
_MULTIPROC_MODES = frozenset(('all', 'liveall', 'min', 'livemin', 'max', 'livemax', 'sum', 'livesum', 'mostrecent', 'livemostrecent'))
_MOST_RECENT_MODES = frozenset(('mostrecent', 'livemostrecent'))

def __init__(self,
name: str,
Expand All @@ -357,7 +370,7 @@ def __init__(self,
unit: str = '',
registry: Optional[CollectorRegistry] = REGISTRY,
_labelvalues: Optional[Sequence[str]] = None,
multiprocess_mode: str = 'all',
multiprocess_mode: Literal['all', 'liveall', 'min', 'livemin', 'max', 'livemax', 'sum', 'livesum', 'mostrecent', 'livemostrecent'] = 'all',
):
self._multiprocess_mode = multiprocess_mode
if multiprocess_mode not in self._MULTIPROC_MODES:
Expand All @@ -373,6 +386,7 @@ def __init__(self,
_labelvalues=_labelvalues,
)
self._kwargs['multiprocess_mode'] = self._multiprocess_mode
self._is_most_recent = self._multiprocess_mode in self._MOST_RECENT_MODES

def _metric_init(self) -> None:
self._value = values.ValueClass(
Expand All @@ -382,18 +396,25 @@ def _metric_init(self) -> None:

def inc(self, amount: float = 1) -> None:
"""Increment gauge by the given amount."""
if self._is_most_recent:
raise RuntimeError("inc must not be used with the mostrecent mode")
self._raise_if_not_observable()
self._value.inc(amount)

def dec(self, amount: float = 1) -> None:
"""Decrement gauge by the given amount."""
if self._is_most_recent:
raise RuntimeError("dec must not be used with the mostrecent mode")
self._raise_if_not_observable()
self._value.inc(-amount)

def set(self, value: float) -> None:
"""Set gauge to the given value."""
self._raise_if_not_observable()
self._value.set(float(value))
if self._is_most_recent:
self._value.set(float(value), timestamp=time.time())
else:
self._value.set(float(value))

def set_to_current_time(self) -> None:
"""Set gauge to the current unixtime."""
Expand Down
40 changes: 20 additions & 20 deletions prometheus_client/mmap_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,26 @@

_INITIAL_MMAP_SIZE = 1 << 16
_pack_integer_func = struct.Struct(b'i').pack
_pack_double_func = struct.Struct(b'd').pack
_pack_two_doubles_func = struct.Struct(b'dd').pack
_unpack_integer = struct.Struct(b'i').unpack_from
_unpack_double = struct.Struct(b'd').unpack_from
_unpack_two_doubles = struct.Struct(b'dd').unpack_from


# struct.pack_into has atomicity issues because it will temporarily write 0 into
# the mmap, resulting in false reads to 0 when experiencing a lot of writes.
# Using direct assignment solves this issue.

def _pack_double(data, pos, value):
data[pos:pos + 8] = _pack_double_func(value)

def _pack_two_doubles(data, pos, value, timestamp):
data[pos:pos + 16] = _pack_two_doubles_func(value, timestamp)


def _pack_integer(data, pos, value):
data[pos:pos + 4] = _pack_integer_func(value)


def _read_all_values(data, used=0):
"""Yield (key, value, pos). No locking is performed."""
"""Yield (key, value, timestamp, pos). No locking is performed."""

if used <= 0:
# If not valid `used` value is passed in, read it from the file.
Expand All @@ -41,9 +42,9 @@ def _read_all_values(data, used=0):
encoded_key = data[pos:pos + encoded_len]
padded_len = encoded_len + (8 - (encoded_len + 4) % 8)
pos += padded_len
value = _unpack_double(data, pos)[0]
yield encoded_key.decode('utf-8'), value, pos
pos += 8
value, timestamp = _unpack_two_doubles(data, pos)
yield encoded_key.decode('utf-8'), value, timestamp, pos
pos += 16


class MmapedDict:
Expand All @@ -53,7 +54,8 @@ class MmapedDict:
Then 4 bytes of padding.
There's then a number of entries, consisting of a 4 byte int which is the
size of the next field, a utf-8 encoded string key, padding to a 8 byte
alignment, and then a 8 byte float which is the value.
alignment, and then a 8 byte float which is the value and a 8 byte float
which is a UNIX timestamp in seconds.
Not thread safe.
"""
Expand All @@ -76,7 +78,7 @@ def __init__(self, filename, read_mode=False):
_pack_integer(self._m, 0, self._used)
else:
if not read_mode:
for key, _, pos in self._read_all_values():
for key, _, _, pos in self._read_all_values():
self._positions[key] = pos

@staticmethod
Expand All @@ -95,7 +97,7 @@ def _init_value(self, key):
encoded = key.encode('utf-8')
# Pad to be 8-byte aligned.
padded = encoded + (b' ' * (8 - (len(encoded) + 4) % 8))
value = struct.pack(f'i{len(padded)}sd'.encode(), len(encoded), padded, 0.0)
value = struct.pack(f'i{len(padded)}sdd'.encode(), len(encoded), padded, 0.0, 0.0)
while self._used + len(value) > self._capacity:
self._capacity *= 2
self._f.truncate(self._capacity)
Expand All @@ -105,30 +107,28 @@ def _init_value(self, key):
# Update how much space we've used.
self._used += len(value)
_pack_integer(self._m, 0, self._used)
self._positions[key] = self._used - 8
self._positions[key] = self._used - 16

def _read_all_values(self):
"""Yield (key, value, pos). No locking is performed."""
return _read_all_values(data=self._m, used=self._used)

def read_all_values(self):
"""Yield (key, value). No locking is performed."""
for k, v, _ in self._read_all_values():
yield k, v
"""Yield (key, value, timestamp). No locking is performed."""
for k, v, ts, _ in self._read_all_values():
yield k, v, ts

def read_value(self, key):
if key not in self._positions:
self._init_value(key)
pos = self._positions[key]
# We assume that reading from an 8 byte aligned value is atomic
return _unpack_double(self._m, pos)[0]
return _unpack_two_doubles(self._m, pos)

def write_value(self, key, value):
def write_value(self, key, value, timestamp):
if key not in self._positions:
self._init_value(key)
pos = self._positions[key]
# We assume that writing to an 8 byte aligned value is atomic
_pack_double(self._m, pos, value)
_pack_two_doubles(self._m, pos, value, timestamp)

def close(self):
if self._f:
Expand Down
Loading

0 comments on commit a890cea

Please sign in to comment.