aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/prometheus_client
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/prometheus_client')
-rw-r--r--.venv/lib/python3.12/site-packages/prometheus_client/__init__.py72
-rw-r--r--.venv/lib/python3.12/site-packages/prometheus_client/asgi.py40
-rw-r--r--.venv/lib/python3.12/site-packages/prometheus_client/bridge/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/prometheus_client/bridge/graphite.py94
-rw-r--r--.venv/lib/python3.12/site-packages/prometheus_client/context_managers.py82
-rw-r--r--.venv/lib/python3.12/site-packages/prometheus_client/core.py32
-rw-r--r--.venv/lib/python3.12/site-packages/prometheus_client/decorator.py427
-rw-r--r--.venv/lib/python3.12/site-packages/prometheus_client/exposition.py666
-rw-r--r--.venv/lib/python3.12/site-packages/prometheus_client/gc_collector.py45
-rw-r--r--.venv/lib/python3.12/site-packages/prometheus_client/metrics.py776
-rw-r--r--.venv/lib/python3.12/site-packages/prometheus_client/metrics_core.py418
-rw-r--r--.venv/lib/python3.12/site-packages/prometheus_client/mmap_dict.py145
-rw-r--r--.venv/lib/python3.12/site-packages/prometheus_client/multiprocess.py170
-rw-r--r--.venv/lib/python3.12/site-packages/prometheus_client/openmetrics/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/prometheus_client/openmetrics/exposition.py72
-rw-r--r--.venv/lib/python3.12/site-packages/prometheus_client/openmetrics/parser.py614
-rw-r--r--.venv/lib/python3.12/site-packages/prometheus_client/parser.py225
-rw-r--r--.venv/lib/python3.12/site-packages/prometheus_client/platform_collector.py59
-rw-r--r--.venv/lib/python3.12/site-packages/prometheus_client/process_collector.py101
-rw-r--r--.venv/lib/python3.12/site-packages/prometheus_client/py.typed0
-rw-r--r--.venv/lib/python3.12/site-packages/prometheus_client/registry.py168
-rw-r--r--.venv/lib/python3.12/site-packages/prometheus_client/samples.py53
-rw-r--r--.venv/lib/python3.12/site-packages/prometheus_client/twisted/__init__.py3
-rw-r--r--.venv/lib/python3.12/site-packages/prometheus_client/twisted/_exposition.py8
-rw-r--r--.venv/lib/python3.12/site-packages/prometheus_client/utils.py24
-rw-r--r--.venv/lib/python3.12/site-packages/prometheus_client/values.py139
26 files changed, 4433 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/prometheus_client/__init__.py b/.venv/lib/python3.12/site-packages/prometheus_client/__init__.py
new file mode 100644
index 00000000..84a7ba82
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/prometheus_client/__init__.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python
+
+from . import (
+ exposition, gc_collector, metrics, metrics_core, platform_collector,
+ process_collector, registry,
+)
+from .exposition import (
+ CONTENT_TYPE_LATEST, delete_from_gateway, generate_latest,
+ instance_ip_grouping_key, make_asgi_app, make_wsgi_app, MetricsHandler,
+ push_to_gateway, pushadd_to_gateway, start_http_server, start_wsgi_server,
+ write_to_textfile,
+)
+from .gc_collector import GC_COLLECTOR, GCCollector
+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
+from .registry import CollectorRegistry, REGISTRY
+
+__all__ = (
+ 'CollectorRegistry',
+ 'REGISTRY',
+ 'Metric',
+ 'Counter',
+ 'Gauge',
+ 'Summary',
+ 'Histogram',
+ 'Info',
+ 'Enum',
+ 'enable_created_metrics',
+ 'disable_created_metrics',
+ 'CONTENT_TYPE_LATEST',
+ 'generate_latest',
+ 'MetricsHandler',
+ 'make_wsgi_app',
+ 'make_asgi_app',
+ 'start_http_server',
+ 'start_wsgi_server',
+ 'write_to_textfile',
+ 'push_to_gateway',
+ 'pushadd_to_gateway',
+ 'delete_from_gateway',
+ 'instance_ip_grouping_key',
+ 'ProcessCollector',
+ 'PROCESS_COLLECTOR',
+ 'PlatformCollector',
+ 'PLATFORM_COLLECTOR',
+ 'GCCollector',
+ 'GC_COLLECTOR',
+)
+
+if __name__ == '__main__':
+ c = Counter('cc', 'A counter')
+ c.inc()
+
+ g = Gauge('gg', 'A gauge')
+ g.set(17)
+
+ s = Summary('ss', 'A summary', ['a', 'b'])
+ s.labels('c', 'd').observe(17)
+
+ h = Histogram('hh', 'A histogram')
+ h.observe(.6)
+
+ start_http_server(8000)
+ import time
+
+ while True:
+ time.sleep(1)
diff --git a/.venv/lib/python3.12/site-packages/prometheus_client/asgi.py b/.venv/lib/python3.12/site-packages/prometheus_client/asgi.py
new file mode 100644
index 00000000..e1864b8b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/prometheus_client/asgi.py
@@ -0,0 +1,40 @@
+from typing import Callable
+from urllib.parse import parse_qs
+
+from .exposition import _bake_output
+from .registry import CollectorRegistry, REGISTRY
+
+
+def make_asgi_app(registry: CollectorRegistry = REGISTRY, disable_compression: bool = False) -> Callable:
+ """Create a ASGI app which serves the metrics from a registry."""
+
+ async def prometheus_app(scope, receive, send):
+ assert scope.get("type") == "http"
+ # Prepare parameters
+ params = parse_qs(scope.get('query_string', b''))
+ accept_header = ",".join([
+ value.decode("utf8") for (name, value) in scope.get('headers')
+ if name.decode("utf8").lower() == 'accept'
+ ])
+ accept_encoding_header = ",".join([
+ value.decode("utf8") for (name, value) in scope.get('headers')
+ if name.decode("utf8").lower() == 'accept-encoding'
+ ])
+ # Bake output
+ status, headers, output = _bake_output(registry, accept_header, accept_encoding_header, params, disable_compression)
+ formatted_headers = []
+ for header in headers:
+ formatted_headers.append(tuple(x.encode('utf8') for x in header))
+ # Return output
+ payload = await receive()
+ if payload.get("type") == "http.request":
+ await send(
+ {
+ "type": "http.response.start",
+ "status": int(status.split(' ')[0]),
+ "headers": formatted_headers,
+ }
+ )
+ await send({"type": "http.response.body", "body": output})
+
+ return prometheus_app
diff --git a/.venv/lib/python3.12/site-packages/prometheus_client/bridge/__init__.py b/.venv/lib/python3.12/site-packages/prometheus_client/bridge/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/prometheus_client/bridge/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/prometheus_client/bridge/graphite.py b/.venv/lib/python3.12/site-packages/prometheus_client/bridge/graphite.py
new file mode 100644
index 00000000..8cadbedc
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/prometheus_client/bridge/graphite.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python
+
+import logging
+import re
+import socket
+import threading
+import time
+from timeit import default_timer
+from typing import Callable, Tuple
+
+from ..registry import CollectorRegistry, REGISTRY
+
+# Roughly, have to keep to what works as a file name.
+# We also remove periods, so labels can be distinguished.
+
+_INVALID_GRAPHITE_CHARS = re.compile(r"[^a-zA-Z0-9_-]")
+
+
+def _sanitize(s):
+ return _INVALID_GRAPHITE_CHARS.sub('_', s)
+
+
+class _RegularPush(threading.Thread):
+ def __init__(self, pusher, interval, prefix):
+ super().__init__()
+ self._pusher = pusher
+ self._interval = interval
+ self._prefix = prefix
+
+ def run(self):
+ wait_until = default_timer()
+ while True:
+ while True:
+ now = default_timer()
+ if now >= wait_until:
+ # May need to skip some pushes.
+ while wait_until < now:
+ wait_until += self._interval
+ break
+ # time.sleep can return early.
+ time.sleep(wait_until - now)
+ try:
+ self._pusher.push(prefix=self._prefix)
+ except OSError:
+ logging.exception("Push failed")
+
+
+class GraphiteBridge:
+ def __init__(self,
+ address: Tuple[str, int],
+ registry: CollectorRegistry = REGISTRY,
+ timeout_seconds: float = 30,
+ _timer: Callable[[], float] = time.time,
+ tags: bool = False,
+ ):
+ self._address = address
+ self._registry = registry
+ self._tags = tags
+ self._timeout = timeout_seconds
+ self._timer = _timer
+
+ def push(self, prefix: str = '') -> None:
+ now = int(self._timer())
+ output = []
+
+ prefixstr = ''
+ if prefix:
+ prefixstr = prefix + '.'
+
+ for metric in self._registry.collect():
+ for s in metric.samples:
+ if s.labels:
+ if self._tags:
+ sep = ';'
+ fmt = '{0}={1}'
+ else:
+ sep = '.'
+ fmt = '{0}.{1}'
+ labelstr = sep + sep.join(
+ [fmt.format(
+ _sanitize(k), _sanitize(v))
+ for k, v in sorted(s.labels.items())])
+ else:
+ labelstr = ''
+ output.append(f'{prefixstr}{_sanitize(s.name)}{labelstr} {float(s.value)} {now}\n')
+
+ conn = socket.create_connection(self._address, self._timeout)
+ conn.sendall(''.join(output).encode('ascii'))
+ conn.close()
+
+ def start(self, interval: float = 60.0, prefix: str = '') -> None:
+ t = _RegularPush(self, interval, prefix)
+ t.daemon = True
+ t.start()
diff --git a/.venv/lib/python3.12/site-packages/prometheus_client/context_managers.py b/.venv/lib/python3.12/site-packages/prometheus_client/context_managers.py
new file mode 100644
index 00000000..3988ec22
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/prometheus_client/context_managers.py
@@ -0,0 +1,82 @@
+from timeit import default_timer
+from types import TracebackType
+from typing import (
+ Any, Callable, Literal, Optional, Tuple, Type, TYPE_CHECKING, TypeVar,
+ Union,
+)
+
+from .decorator import decorate
+
+if TYPE_CHECKING:
+ from . import Counter
+ F = TypeVar("F", bound=Callable[..., Any])
+
+
+class ExceptionCounter:
+ def __init__(self, counter: "Counter", exception: Union[Type[BaseException], Tuple[Type[BaseException], ...]]) -> None:
+ self._counter = counter
+ self._exception = exception
+
+ def __enter__(self) -> None:
+ pass
+
+ 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
+
+ def __call__(self, f: "F") -> "F":
+ def wrapped(func, *args, **kwargs):
+ with self:
+ return func(*args, **kwargs)
+
+ return decorate(f, wrapped)
+
+
+class InprogressTracker:
+ def __init__(self, gauge):
+ self._gauge = gauge
+
+ def __enter__(self):
+ self._gauge.inc()
+
+ def __exit__(self, typ, value, traceback):
+ self._gauge.dec()
+
+ def __call__(self, f: "F") -> "F":
+ def wrapped(func, *args, **kwargs):
+ with self:
+ return func(*args, **kwargs)
+
+ return decorate(f, wrapped)
+
+
+class Timer:
+ def __init__(self, metric, callback_name):
+ self._metric = metric
+ self._callback_name = callback_name
+
+ def _new_timer(self):
+ return self.__class__(self._metric, self._callback_name)
+
+ def __enter__(self):
+ self._start = default_timer()
+ return self
+
+ def __exit__(self, typ, value, traceback):
+ # Time can go backwards.
+ duration = max(default_timer() - self._start, 0)
+ callback = getattr(self._metric, self._callback_name)
+ callback(duration)
+
+ def labels(self, *args, **kw):
+ self._metric = self._metric.labels(*args, **kw)
+
+ def __call__(self, f: "F") -> "F":
+ def wrapped(func, *args, **kwargs):
+ # Obtaining new instance of timer every time
+ # ensures thread safety and reentrancy.
+ with self._new_timer():
+ return func(*args, **kwargs)
+
+ return decorate(f, wrapped)
diff --git a/.venv/lib/python3.12/site-packages/prometheus_client/core.py b/.venv/lib/python3.12/site-packages/prometheus_client/core.py
new file mode 100644
index 00000000..ad3a4542
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/prometheus_client/core.py
@@ -0,0 +1,32 @@
+from .metrics import Counter, Enum, Gauge, Histogram, Info, Summary
+from .metrics_core import (
+ CounterMetricFamily, GaugeHistogramMetricFamily, GaugeMetricFamily,
+ HistogramMetricFamily, InfoMetricFamily, Metric, StateSetMetricFamily,
+ SummaryMetricFamily, UnknownMetricFamily, UntypedMetricFamily,
+)
+from .registry import CollectorRegistry, REGISTRY
+from .samples import Exemplar, Sample, Timestamp
+
+__all__ = (
+ 'CollectorRegistry',
+ 'Counter',
+ 'CounterMetricFamily',
+ 'Enum',
+ 'Exemplar',
+ 'Gauge',
+ 'GaugeHistogramMetricFamily',
+ 'GaugeMetricFamily',
+ 'Histogram',
+ 'HistogramMetricFamily',
+ 'Info',
+ 'InfoMetricFamily',
+ 'Metric',
+ 'REGISTRY',
+ 'Sample',
+ 'StateSetMetricFamily',
+ 'Summary',
+ 'SummaryMetricFamily',
+ 'Timestamp',
+ 'UnknownMetricFamily',
+ 'UntypedMetricFamily',
+)
diff --git a/.venv/lib/python3.12/site-packages/prometheus_client/decorator.py b/.venv/lib/python3.12/site-packages/prometheus_client/decorator.py
new file mode 100644
index 00000000..1ad2c977
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/prometheus_client/decorator.py
@@ -0,0 +1,427 @@
+# ######################### LICENSE ############################ #
+
+# Copyright (c) 2005-2016, Michele Simionato
+# All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+
+# Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# Redistributions in bytecode form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in
+# the documentation and/or other materials provided with the
+# distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+# DAMAGE.
+
+"""
+Decorator module, see http://pypi.python.org/pypi/decorator
+for the documentation.
+"""
+from __future__ import print_function
+
+import collections
+import inspect
+import itertools
+import operator
+import re
+import sys
+
+__version__ = '4.0.10'
+
+if sys.version_info >= (3,):
+ from inspect import getfullargspec
+
+
+ def get_init(cls):
+ return cls.__init__
+else:
+ class getfullargspec(object):
+ "A quick and dirty replacement for getfullargspec for Python 2.X"
+
+ def __init__(self, f):
+ self.args, self.varargs, self.varkw, self.defaults = \
+ inspect.getargspec(f)
+ self.kwonlyargs = []
+ self.kwonlydefaults = None
+
+ def __iter__(self):
+ yield self.args
+ yield self.varargs
+ yield self.varkw
+ yield self.defaults
+
+ getargspec = inspect.getargspec
+
+
+ def get_init(cls):
+ return cls.__init__.__func__
+
+# getargspec has been deprecated in Python 3.5
+ArgSpec = collections.namedtuple(
+ 'ArgSpec', 'args varargs varkw defaults')
+
+
+def getargspec(f):
+ """A replacement for inspect.getargspec"""
+ spec = getfullargspec(f)
+ return ArgSpec(spec.args, spec.varargs, spec.varkw, spec.defaults)
+
+
+DEF = re.compile(r'\s*def\s*([_\w][_\w\d]*)\s*\(')
+
+
+# basic functionality
+class FunctionMaker(object):
+ """
+ An object with the ability to create functions with a given signature.
+ It has attributes name, doc, module, signature, defaults, dict and
+ methods update and make.
+ """
+
+ # Atomic get-and-increment provided by the GIL
+ _compile_count = itertools.count()
+
+ def __init__(self, func=None, name=None, signature=None,
+ defaults=None, doc=None, module=None, funcdict=None):
+ self.shortsignature = signature
+ if func:
+ # func can be a class or a callable, but not an instance method
+ self.name = func.__name__
+ if self.name == '<lambda>': # small hack for lambda functions
+ self.name = '_lambda_'
+ self.doc = func.__doc__
+ self.module = func.__module__
+ if inspect.isfunction(func):
+ argspec = getfullargspec(func)
+ self.annotations = getattr(func, '__annotations__', {})
+ for a in ('args', 'varargs', 'varkw', 'defaults', 'kwonlyargs',
+ 'kwonlydefaults'):
+ setattr(self, a, getattr(argspec, a))
+ for i, arg in enumerate(self.args):
+ setattr(self, 'arg%d' % i, arg)
+ if sys.version_info < (3,): # easy way
+ self.shortsignature = self.signature = (
+ inspect.formatargspec(
+ formatvalue=lambda val: "", *argspec)[1:-1])
+ else: # Python 3 way
+ allargs = list(self.args)
+ allshortargs = list(self.args)
+ if self.varargs:
+ allargs.append('*' + self.varargs)
+ allshortargs.append('*' + self.varargs)
+ elif self.kwonlyargs:
+ allargs.append('*') # single star syntax
+ for a in self.kwonlyargs:
+ allargs.append('%s=None' % a)
+ allshortargs.append('%s=%s' % (a, a))
+ if self.varkw:
+ allargs.append('**' + self.varkw)
+ allshortargs.append('**' + self.varkw)
+ self.signature = ', '.join(allargs)
+ self.shortsignature = ', '.join(allshortargs)
+ self.dict = func.__dict__.copy()
+ # func=None happens when decorating a caller
+ if name:
+ self.name = name
+ if signature is not None:
+ self.signature = signature
+ if defaults:
+ self.defaults = defaults
+ if doc:
+ self.doc = doc
+ if module:
+ self.module = module
+ if funcdict:
+ self.dict = funcdict
+ # check existence required attributes
+ assert hasattr(self, 'name')
+ if not hasattr(self, 'signature'):
+ raise TypeError('You are decorating a non function: %s' % func)
+
+ def update(self, func, **kw):
+ "Update the signature of func with the data in self"
+ func.__name__ = self.name
+ func.__doc__ = getattr(self, 'doc', None)
+ func.__dict__ = getattr(self, 'dict', {})
+ func.__defaults__ = getattr(self, 'defaults', ())
+ func.__kwdefaults__ = getattr(self, 'kwonlydefaults', None)
+ func.__annotations__ = getattr(self, 'annotations', None)
+ try:
+ frame = sys._getframe(3)
+ except AttributeError: # for IronPython and similar implementations
+ callermodule = '?'
+ else:
+ callermodule = frame.f_globals.get('__name__', '?')
+ func.__module__ = getattr(self, 'module', callermodule)
+ func.__dict__.update(kw)
+
+ def make(self, src_templ, evaldict=None, addsource=False, **attrs):
+ "Make a new function from a given template and update the signature"
+ src = src_templ % vars(self) # expand name and signature
+ evaldict = evaldict or {}
+ mo = DEF.match(src)
+ if mo is None:
+ raise SyntaxError('not a valid function template\n%s' % src)
+ name = mo.group(1) # extract the function name
+ names = set([name] + [arg.strip(' *') for arg in
+ self.shortsignature.split(',')])
+ for n in names:
+ if n in ('_func_', '_call_'):
+ raise NameError('%s is overridden in\n%s' % (n, src))
+
+ if not src.endswith('\n'): # add a newline for old Pythons
+ src += '\n'
+
+ # Ensure each generated function has a unique filename for profilers
+ # (such as cProfile) that depend on the tuple of (<filename>,
+ # <definition line>, <function name>) being unique.
+ filename = '<decorator-gen-%d>' % (next(self._compile_count),)
+ try:
+ code = compile(src, filename, 'single')
+ exec(code, evaldict)
+ except:
+ print('Error in generated code:', file=sys.stderr)
+ print(src, file=sys.stderr)
+ raise
+ func = evaldict[name]
+ if addsource:
+ attrs['__source__'] = src
+ self.update(func, **attrs)
+ return func
+
+ @classmethod
+ def create(cls, obj, body, evaldict, defaults=None,
+ doc=None, module=None, addsource=True, **attrs):
+ """
+ Create a function from the strings name, signature and body.
+ evaldict is the evaluation dictionary. If addsource is true an
+ attribute __source__ is added to the result. The attributes attrs
+ are added, if any.
+ """
+ if isinstance(obj, str): # "name(signature)"
+ name, rest = obj.strip().split('(', 1)
+ signature = rest[:-1] # strip a right parens
+ func = None
+ else: # a function
+ name = None
+ signature = None
+ func = obj
+ self = cls(func, name, signature, defaults, doc, module)
+ ibody = '\n'.join(' ' + line for line in body.splitlines())
+ return self.make('def %(name)s(%(signature)s):\n' + ibody,
+ evaldict, addsource, **attrs)
+
+
+def decorate(func, caller):
+ """
+ decorate(func, caller) decorates a function using a caller.
+ """
+ evaldict = dict(_call_=caller, _func_=func)
+ fun = FunctionMaker.create(
+ func, "return _call_(_func_, %(shortsignature)s)",
+ evaldict, __wrapped__=func)
+ if hasattr(func, '__qualname__'):
+ fun.__qualname__ = func.__qualname__
+ return fun
+
+
+def decorator(caller, _func=None):
+ """decorator(caller) converts a caller function into a decorator"""
+ if _func is not None: # return a decorated function
+ # this is obsolete behavior; you should use decorate instead
+ return decorate(_func, caller)
+ # else return a decorator function
+ if inspect.isclass(caller):
+ name = caller.__name__.lower()
+ doc = 'decorator(%s) converts functions/generators into ' \
+ 'factories of %s objects' % (caller.__name__, caller.__name__)
+ elif inspect.isfunction(caller):
+ if caller.__name__ == '<lambda>':
+ name = '_lambda_'
+ else:
+ name = caller.__name__
+ doc = caller.__doc__
+ else: # assume caller is an object with a __call__ method
+ name = caller.__class__.__name__.lower()
+ doc = caller.__call__.__doc__
+ evaldict = dict(_call_=caller, _decorate_=decorate)
+ return FunctionMaker.create(
+ '%s(func)' % name, 'return _decorate_(func, _call_)',
+ evaldict, doc=doc, module=caller.__module__,
+ __wrapped__=caller)
+
+
+# ####################### contextmanager ####################### #
+
+try: # Python >= 3.2
+ from contextlib import _GeneratorContextManager
+except ImportError: # Python >= 2.5
+ from contextlib import GeneratorContextManager as _GeneratorContextManager
+
+
+class ContextManager(_GeneratorContextManager):
+ def __call__(self, func):
+ """Context manager decorator"""
+ return FunctionMaker.create(
+ func, "with _self_: return _func_(%(shortsignature)s)",
+ dict(_self_=self, _func_=func), __wrapped__=func)
+
+
+init = getfullargspec(_GeneratorContextManager.__init__)
+n_args = len(init.args)
+if n_args == 2 and not init.varargs: # (self, genobj) Python 2.7
+ def __init__(self, g, *a, **k):
+ return _GeneratorContextManager.__init__(self, g(*a, **k))
+
+
+ ContextManager.__init__ = __init__
+elif n_args == 2 and init.varargs: # (self, gen, *a, **k) Python 3.4
+ pass
+elif n_args == 4: # (self, gen, args, kwds) Python 3.5
+ def __init__(self, g, *a, **k):
+ return _GeneratorContextManager.__init__(self, g, a, k)
+
+
+ ContextManager.__init__ = __init__
+
+contextmanager = decorator(ContextManager)
+
+
+# ############################ dispatch_on ############################ #
+
+def append(a, vancestors):
+ """
+ Append ``a`` to the list of the virtual ancestors, unless it is already
+ included.
+ """
+ add = True
+ for j, va in enumerate(vancestors):
+ if issubclass(va, a):
+ add = False
+ break
+ if issubclass(a, va):
+ vancestors[j] = a
+ add = False
+ if add:
+ vancestors.append(a)
+
+
+# inspired from simplegeneric by P.J. Eby and functools.singledispatch
+def dispatch_on(*dispatch_args):
+ """
+ Factory of decorators turning a function into a generic function
+ dispatching on the given arguments.
+ """
+ assert dispatch_args, 'No dispatch args passed'
+ dispatch_str = '(%s,)' % ', '.join(dispatch_args)
+
+ def check(arguments, wrong=operator.ne, msg=''):
+ """Make sure one passes the expected number of arguments"""
+ if wrong(len(arguments), len(dispatch_args)):
+ raise TypeError('Expected %d arguments, got %d%s' %
+ (len(dispatch_args), len(arguments), msg))
+
+ def gen_func_dec(func):
+ """Decorator turning a function into a generic function"""
+
+ # first check the dispatch arguments
+ argset = set(getfullargspec(func).args)
+ if not set(dispatch_args) <= argset:
+ raise NameError('Unknown dispatch arguments %s' % dispatch_str)
+
+ typemap = {}
+
+ def vancestors(*types):
+ """
+ Get a list of sets of virtual ancestors for the given types
+ """
+ check(types)
+ ras = [[] for _ in range(len(dispatch_args))]
+ for types_ in typemap:
+ for t, type_, ra in zip(types, types_, ras):
+ if issubclass(t, type_) and type_ not in t.__mro__:
+ append(type_, ra)
+ return [set(ra) for ra in ras]
+
+ def ancestors(*types):
+ """
+ Get a list of virtual MROs, one for each type
+ """
+ check(types)
+ lists = []
+ for t, vas in zip(types, vancestors(*types)):
+ n_vas = len(vas)
+ if n_vas > 1:
+ raise RuntimeError(
+ 'Ambiguous dispatch for %s: %s' % (t, vas))
+ elif n_vas == 1:
+ va, = vas
+ mro = type('t', (t, va), {}).__mro__[1:]
+ else:
+ mro = t.__mro__
+ lists.append(mro[:-1]) # discard t and object
+ return lists
+
+ def register(*types):
+ """
+ Decorator to register an implementation for the given types
+ """
+ check(types)
+
+ def dec(f):
+ check(getfullargspec(f).args, operator.lt, ' in ' + f.__name__)
+ typemap[types] = f
+ return f
+
+ return dec
+
+ def dispatch_info(*types):
+ """
+ An utility to introspect the dispatch algorithm
+ """
+ check(types)
+ lst = []
+ for anc in itertools.product(*ancestors(*types)):
+ lst.append(tuple(a.__name__ for a in anc))
+ return lst
+
+ def _dispatch(dispatch_args, *args, **kw):
+ types = tuple(type(arg) for arg in dispatch_args)
+ try: # fast path
+ f = typemap[types]
+ except KeyError:
+ pass
+ else:
+ return f(*args, **kw)
+ combinations = itertools.product(*ancestors(*types))
+ next(combinations) # the first one has been already tried
+ for types_ in combinations:
+ f = typemap.get(types_)
+ if f is not None:
+ return f(*args, **kw)
+
+ # else call the default implementation
+ return func(*args, **kw)
+
+ return FunctionMaker.create(
+ func, 'return _f_(%s, %%(shortsignature)s)' % dispatch_str,
+ dict(_f_=_dispatch), register=register, default=func,
+ typemap=typemap, vancestors=vancestors, ancestors=ancestors,
+ dispatch_info=dispatch_info, __wrapped__=func)
+
+ gen_func_dec.__name__ = 'dispatch_on' + dispatch_str
+ return gen_func_dec
diff --git a/.venv/lib/python3.12/site-packages/prometheus_client/exposition.py b/.venv/lib/python3.12/site-packages/prometheus_client/exposition.py
new file mode 100644
index 00000000..fab139df
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/prometheus_client/exposition.py
@@ -0,0 +1,666 @@
+import base64
+from contextlib import closing
+import gzip
+from http.server import BaseHTTPRequestHandler
+import os
+import socket
+from socketserver import ThreadingMixIn
+import ssl
+import sys
+import threading
+from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union
+from urllib.error import HTTPError
+from urllib.parse import parse_qs, quote_plus, urlparse
+from urllib.request import (
+ BaseHandler, build_opener, HTTPHandler, HTTPRedirectHandler, HTTPSHandler,
+ Request,
+)
+from wsgiref.simple_server import make_server, WSGIRequestHandler, WSGIServer
+
+from .openmetrics import exposition as openmetrics
+from .registry import CollectorRegistry, REGISTRY
+from .utils import floatToGoString
+
+__all__ = (
+ 'CONTENT_TYPE_LATEST',
+ 'delete_from_gateway',
+ 'generate_latest',
+ 'instance_ip_grouping_key',
+ 'make_asgi_app',
+ 'make_wsgi_app',
+ 'MetricsHandler',
+ 'push_to_gateway',
+ 'pushadd_to_gateway',
+ 'start_http_server',
+ 'start_wsgi_server',
+ 'write_to_textfile',
+)
+
+CONTENT_TYPE_LATEST = 'text/plain; version=0.0.4; charset=utf-8'
+"""Content type of the latest text format"""
+
+
+class _PrometheusRedirectHandler(HTTPRedirectHandler):
+ """
+ Allow additional methods (e.g. PUT) and data forwarding in redirects.
+
+ Use of this class constitute a user's explicit agreement to the
+ redirect responses the Prometheus client will receive when using it.
+ You should only use this class if you control or otherwise trust the
+ redirect behavior involved and are certain it is safe to full transfer
+ the original request (method and data) to the redirected URL. For
+ example, if you know there is a cosmetic URL redirect in front of a
+ local deployment of a Prometheus server, and all redirects are safe,
+ this is the class to use to handle redirects in that case.
+
+ The standard HTTPRedirectHandler does not forward request data nor
+ does it allow redirected PUT requests (which Prometheus uses for some
+ operations, for example `push_to_gateway`) because these cannot
+ generically guarantee no violations of HTTP RFC 2616 requirements for
+ the user to explicitly confirm redirects that could have unexpected
+ side effects (such as rendering a PUT request non-idempotent or
+ creating multiple resources not named in the original request).
+ """
+
+ def redirect_request(self, req, fp, code, msg, headers, newurl):
+ """
+ Apply redirect logic to a request.
+
+ See parent HTTPRedirectHandler.redirect_request for parameter info.
+
+ If the redirect is disallowed, this raises the corresponding HTTP error.
+ If the redirect can't be determined, return None to allow other handlers
+ to try. If the redirect is allowed, return the new request.
+
+ This method specialized for the case when (a) the user knows that the
+ redirect will not cause unacceptable side effects for any request method,
+ and (b) the user knows that any request data should be passed through to
+ the redirect. If either condition is not met, this should not be used.
+ """
+ # note that requests being provided by a handler will use get_method to
+ # indicate the method, by monkeypatching this, instead of setting the
+ # Request object's method attribute.
+ m = getattr(req, "method", req.get_method())
+ if not (code in (301, 302, 303, 307) and m in ("GET", "HEAD")
+ or code in (301, 302, 303) and m in ("POST", "PUT")):
+ raise HTTPError(req.full_url, code, msg, headers, fp)
+ new_request = Request(
+ newurl.replace(' ', '%20'), # space escaping in new url if needed.
+ headers=req.headers,
+ origin_req_host=req.origin_req_host,
+ unverifiable=True,
+ data=req.data,
+ )
+ new_request.method = m
+ return new_request
+
+
+def _bake_output(registry, accept_header, accept_encoding_header, params, disable_compression):
+ """Bake output for metrics output."""
+ # Choose the correct plain text format of the output.
+ encoder, content_type = choose_encoder(accept_header)
+ if 'name[]' in params:
+ registry = registry.restricted_registry(params['name[]'])
+ output = encoder(registry)
+ headers = [('Content-Type', content_type)]
+ # If gzip encoding required, gzip the output.
+ if not disable_compression and gzip_accepted(accept_encoding_header):
+ output = gzip.compress(output)
+ headers.append(('Content-Encoding', 'gzip'))
+ return '200 OK', headers, output
+
+
+def make_wsgi_app(registry: CollectorRegistry = REGISTRY, disable_compression: bool = False) -> Callable:
+ """Create a WSGI app which serves the metrics from a registry."""
+
+ def prometheus_app(environ, start_response):
+ # Prepare parameters
+ accept_header = environ.get('HTTP_ACCEPT')
+ accept_encoding_header = environ.get('HTTP_ACCEPT_ENCODING')
+ params = parse_qs(environ.get('QUERY_STRING', ''))
+ method = environ['REQUEST_METHOD']
+
+ if method == 'OPTIONS':
+ status = '200 OK'
+ headers = [('Allow', 'OPTIONS,GET')]
+ output = b''
+ elif method != 'GET':
+ status = '405 Method Not Allowed'
+ headers = [('Allow', 'OPTIONS,GET')]
+ output = '# HTTP {}: {}; use OPTIONS or GET\n'.format(status, method).encode()
+ elif environ['PATH_INFO'] == '/favicon.ico':
+ # Serve empty response for browsers
+ status = '200 OK'
+ headers = [('', '')]
+ output = b''
+ else:
+ # Note: For backwards compatibility, the URI path for GET is not
+ # constrained to the documented /metrics, but any path is allowed.
+ # Bake output
+ status, headers, output = _bake_output(registry, accept_header, accept_encoding_header, params, disable_compression)
+ # Return output
+ start_response(status, headers)
+ return [output]
+
+ return prometheus_app
+
+
+class _SilentHandler(WSGIRequestHandler):
+ """WSGI handler that does not log requests."""
+
+ def log_message(self, format, *args):
+ """Log nothing."""
+
+
+class ThreadingWSGIServer(ThreadingMixIn, WSGIServer):
+ """Thread per request HTTP server."""
+ # Make worker threads "fire and forget". Beginning with Python 3.7 this
+ # prevents a memory leak because ``ThreadingMixIn`` starts to gather all
+ # non-daemon threads in a list in order to join on them at server close.
+ daemon_threads = True
+
+
+def _get_best_family(address, port):
+ """Automatically select address family depending on address"""
+ # HTTPServer defaults to AF_INET, which will not start properly if
+ # binding an ipv6 address is requested.
+ # This function is based on what upstream python did for http.server
+ # in https://github.com/python/cpython/pull/11767
+ infos = socket.getaddrinfo(address, port, type=socket.SOCK_STREAM, flags=socket.AI_PASSIVE)
+ family, _, _, _, sockaddr = next(iter(infos))
+ return family, sockaddr[0]
+
+
+def _get_ssl_ctx(
+ certfile: str,
+ keyfile: str,
+ protocol: int,
+ cafile: Optional[str] = None,
+ capath: Optional[str] = None,
+ client_auth_required: bool = False,
+) -> ssl.SSLContext:
+ """Load context supports SSL."""
+ ssl_cxt = ssl.SSLContext(protocol=protocol)
+
+ if cafile is not None or capath is not None:
+ try:
+ ssl_cxt.load_verify_locations(cafile, capath)
+ except IOError as exc:
+ exc_type = type(exc)
+ msg = str(exc)
+ raise exc_type(f"Cannot load CA certificate chain from file "
+ f"{cafile!r} or directory {capath!r}: {msg}")
+ else:
+ try:
+ ssl_cxt.load_default_certs(purpose=ssl.Purpose.CLIENT_AUTH)
+ except IOError as exc:
+ exc_type = type(exc)
+ msg = str(exc)
+ raise exc_type(f"Cannot load default CA certificate chain: {msg}")
+
+ if client_auth_required:
+ ssl_cxt.verify_mode = ssl.CERT_REQUIRED
+
+ try:
+ ssl_cxt.load_cert_chain(certfile=certfile, keyfile=keyfile)
+ except IOError as exc:
+ exc_type = type(exc)
+ msg = str(exc)
+ raise exc_type(f"Cannot load server certificate file {certfile!r} or "
+ f"its private key file {keyfile!r}: {msg}")
+
+ return ssl_cxt
+
+
+def start_wsgi_server(
+ port: int,
+ addr: str = '0.0.0.0',
+ registry: CollectorRegistry = REGISTRY,
+ certfile: Optional[str] = None,
+ keyfile: Optional[str] = None,
+ client_cafile: Optional[str] = None,
+ client_capath: Optional[str] = None,
+ protocol: int = ssl.PROTOCOL_TLS_SERVER,
+ client_auth_required: bool = False,
+) -> Tuple[WSGIServer, threading.Thread]:
+ """Starts a WSGI server for prometheus metrics as a daemon thread."""
+
+ class TmpServer(ThreadingWSGIServer):
+ """Copy of ThreadingWSGIServer to update address_family locally"""
+
+ TmpServer.address_family, addr = _get_best_family(addr, port)
+ app = make_wsgi_app(registry)
+ httpd = make_server(addr, port, app, TmpServer, handler_class=_SilentHandler)
+ if certfile and keyfile:
+ context = _get_ssl_ctx(certfile, keyfile, protocol, client_cafile, client_capath, client_auth_required)
+ httpd.socket = context.wrap_socket(httpd.socket, server_side=True)
+ t = threading.Thread(target=httpd.serve_forever)
+ t.daemon = True
+ t.start()
+
+ return httpd, t
+
+
+start_http_server = start_wsgi_server
+
+
+def generate_latest(registry: CollectorRegistry = REGISTRY) -> bytes:
+ """Returns the metrics from the registry in latest text format as a string."""
+
+ def sample_line(line):
+ if line.labels:
+ labelstr = '{{{0}}}'.format(','.join(
+ ['{}="{}"'.format(
+ k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"'))
+ for k, v in sorted(line.labels.items())]))
+ else:
+ labelstr = ''
+ timestamp = ''
+ if line.timestamp is not None:
+ # Convert to milliseconds.
+ timestamp = f' {int(float(line.timestamp) * 1000):d}'
+ return f'{line.name}{labelstr} {floatToGoString(line.value)}{timestamp}\n'
+
+ output = []
+ for metric in registry.collect():
+ try:
+ mname = metric.name
+ mtype = metric.type
+ # Munging from OpenMetrics into Prometheus format.
+ if mtype == 'counter':
+ mname = mname + '_total'
+ elif mtype == 'info':
+ mname = mname + '_info'
+ mtype = 'gauge'
+ elif mtype == 'stateset':
+ mtype = 'gauge'
+ elif mtype == 'gaugehistogram':
+ # A gauge histogram is really a gauge,
+ # but this captures the structure better.
+ mtype = 'histogram'
+ elif mtype == 'unknown':
+ mtype = 'untyped'
+
+ output.append('# HELP {} {}\n'.format(
+ mname, metric.documentation.replace('\\', r'\\').replace('\n', r'\n')))
+ output.append(f'# TYPE {mname} {mtype}\n')
+
+ om_samples: Dict[str, List[str]] = {}
+ for s in metric.samples:
+ for suffix in ['_created', '_gsum', '_gcount']:
+ if s.name == metric.name + suffix:
+ # OpenMetrics specific sample, put in a gauge at the end.
+ om_samples.setdefault(suffix, []).append(sample_line(s))
+ break
+ else:
+ output.append(sample_line(s))
+ except Exception as exception:
+ exception.args = (exception.args or ('',)) + (metric,)
+ raise
+
+ for suffix, lines in sorted(om_samples.items()):
+ output.append('# HELP {}{} {}\n'.format(metric.name, suffix,
+ metric.documentation.replace('\\', r'\\').replace('\n', r'\n')))
+ output.append(f'# TYPE {metric.name}{suffix} gauge\n')
+ output.extend(lines)
+ return ''.join(output).encode('utf-8')
+
+
+def choose_encoder(accept_header: str) -> Tuple[Callable[[CollectorRegistry], bytes], str]:
+ accept_header = accept_header or ''
+ for accepted in accept_header.split(','):
+ if accepted.split(';')[0].strip() == 'application/openmetrics-text':
+ return (openmetrics.generate_latest,
+ openmetrics.CONTENT_TYPE_LATEST)
+ return generate_latest, CONTENT_TYPE_LATEST
+
+
+def gzip_accepted(accept_encoding_header: str) -> bool:
+ accept_encoding_header = accept_encoding_header or ''
+ for accepted in accept_encoding_header.split(','):
+ if accepted.split(';')[0].strip().lower() == 'gzip':
+ return True
+ return False
+
+
+class MetricsHandler(BaseHTTPRequestHandler):
+ """HTTP handler that gives metrics from ``REGISTRY``."""
+ registry: CollectorRegistry = REGISTRY
+
+ def do_GET(self) -> None:
+ # Prepare parameters
+ registry = self.registry
+ accept_header = self.headers.get('Accept')
+ accept_encoding_header = self.headers.get('Accept-Encoding')
+ params = parse_qs(urlparse(self.path).query)
+ # Bake output
+ status, headers, output = _bake_output(registry, accept_header, accept_encoding_header, params, False)
+ # Return output
+ self.send_response(int(status.split(' ')[0]))
+ for header in headers:
+ self.send_header(*header)
+ self.end_headers()
+ self.wfile.write(output)
+
+ def log_message(self, format: str, *args: Any) -> None:
+ """Log nothing."""
+
+ @classmethod
+ def factory(cls, registry: CollectorRegistry) -> type:
+ """Returns a dynamic MetricsHandler class tied
+ to the passed registry.
+ """
+ # This implementation relies on MetricsHandler.registry
+ # (defined above and defaulted to REGISTRY).
+
+ # As we have unicode_literals, we need to create a str()
+ # object for type().
+ cls_name = str(cls.__name__)
+ MyMetricsHandler = type(cls_name, (cls, object),
+ {"registry": registry})
+ return MyMetricsHandler
+
+
+def write_to_textfile(path: str, registry: CollectorRegistry) -> None:
+ """Write metrics to the given path.
+
+ This is intended for use with the Node exporter textfile collector.
+ The path must end in .prom for the textfile collector to process it."""
+ tmppath = f'{path}.{os.getpid()}.{threading.current_thread().ident}'
+ with open(tmppath, 'wb') as f:
+ f.write(generate_latest(registry))
+
+ # rename(2) is atomic but fails on Windows if the destination file exists
+ if os.name == 'nt':
+ os.replace(tmppath, path)
+ else:
+ os.rename(tmppath, path)
+
+
+def _make_handler(
+ url: str,
+ method: str,
+ timeout: Optional[float],
+ headers: Sequence[Tuple[str, str]],
+ data: bytes,
+ base_handler: Union[BaseHandler, type],
+) -> Callable[[], None]:
+ def handle() -> None:
+ request = Request(url, data=data)
+ request.get_method = lambda: method # type: ignore
+ for k, v in headers:
+ request.add_header(k, v)
+ resp = build_opener(base_handler).open(request, timeout=timeout)
+ if resp.code >= 400:
+ raise OSError(f"error talking to pushgateway: {resp.code} {resp.msg}")
+
+ return handle
+
+
+def default_handler(
+ url: str,
+ method: str,
+ timeout: Optional[float],
+ headers: List[Tuple[str, str]],
+ data: bytes,
+) -> Callable[[], None]:
+ """Default handler that implements HTTP/HTTPS connections.
+
+ Used by the push_to_gateway functions. Can be re-used by other handlers."""
+
+ return _make_handler(url, method, timeout, headers, data, HTTPHandler)
+
+
+def passthrough_redirect_handler(
+ url: str,
+ method: str,
+ timeout: Optional[float],
+ headers: List[Tuple[str, str]],
+ data: bytes,
+) -> Callable[[], None]:
+ """
+ Handler that automatically trusts redirect responses for all HTTP methods.
+
+ Augments standard HTTPRedirectHandler capability by permitting PUT requests,
+ preserving the method upon redirect, and passing through all headers and
+ data from the original request. Only use this handler if you control or
+ trust the source of redirect responses you encounter when making requests
+ via the Prometheus client. This handler will simply repeat the identical
+ request, including same method and data, to the new redirect URL."""
+
+ return _make_handler(url, method, timeout, headers, data, _PrometheusRedirectHandler)
+
+
+def basic_auth_handler(
+ url: str,
+ method: str,
+ timeout: Optional[float],
+ headers: List[Tuple[str, str]],
+ data: bytes,
+ username: Optional[str] = None,
+ password: Optional[str] = None,
+) -> Callable[[], None]:
+ """Handler that implements HTTP/HTTPS connections with Basic Auth.
+
+ Sets auth headers using supplied 'username' and 'password', if set.
+ Used by the push_to_gateway functions. Can be re-used by other handlers."""
+
+ def handle():
+ """Handler that implements HTTP Basic Auth.
+ """
+ if username is not None and password is not None:
+ auth_value = f'{username}:{password}'.encode()
+ auth_token = base64.b64encode(auth_value)
+ auth_header = b'Basic ' + auth_token
+ headers.append(('Authorization', auth_header))
+ default_handler(url, method, timeout, headers, data)()
+
+ return handle
+
+
+def tls_auth_handler(
+ url: str,
+ method: str,
+ timeout: Optional[float],
+ headers: List[Tuple[str, str]],
+ data: bytes,
+ certfile: str,
+ keyfile: str,
+ cafile: Optional[str] = None,
+ protocol: int = ssl.PROTOCOL_TLS_CLIENT,
+ insecure_skip_verify: bool = False,
+) -> Callable[[], None]:
+ """Handler that implements an HTTPS connection with TLS Auth.
+
+ The default protocol (ssl.PROTOCOL_TLS_CLIENT) will also enable
+ ssl.CERT_REQUIRED and SSLContext.check_hostname by default. This can be
+ disabled by setting insecure_skip_verify to True.
+
+ Both this handler and the TLS feature on pushgateay are experimental."""
+ context = ssl.SSLContext(protocol=protocol)
+ if cafile is not None:
+ context.load_verify_locations(cafile)
+ else:
+ context.load_default_certs()
+
+ if insecure_skip_verify:
+ context.check_hostname = False
+ context.verify_mode = ssl.CERT_NONE
+
+ context.load_cert_chain(certfile=certfile, keyfile=keyfile)
+ handler = HTTPSHandler(context=context)
+ return _make_handler(url, method, timeout, headers, data, handler)
+
+
+def push_to_gateway(
+ gateway: str,
+ job: str,
+ registry: CollectorRegistry,
+ grouping_key: Optional[Dict[str, Any]] = None,
+ timeout: Optional[float] = 30,
+ handler: Callable = default_handler,
+) -> None:
+ """Push metrics to the given pushgateway.
+
+ `gateway` the url for your push gateway. Either of the form
+ 'http://pushgateway.local', or 'pushgateway.local'.
+ Scheme defaults to 'http' if none is provided
+ `job` is the job label to be attached to all pushed metrics
+ `registry` is an instance of CollectorRegistry
+ `grouping_key` please see the pushgateway documentation for details.
+ Defaults to None
+ `timeout` is how long push will attempt to connect before giving up.
+ Defaults to 30s, can be set to None for no timeout.
+ `handler` is an optional function which can be provided to perform
+ requests to the 'gateway'.
+ Defaults to None, in which case an http or https request
+ will be carried out by a default handler.
+ If not None, the argument must be a function which accepts
+ the following arguments:
+ url, method, timeout, headers, and content
+ May be used to implement additional functionality not
+ supported by the built-in default handler (such as SSL
+ client certicates, and HTTP authentication mechanisms).
+ 'url' is the URL for the request, the 'gateway' argument
+ described earlier will form the basis of this URL.
+ 'method' is the HTTP method which should be used when
+ carrying out the request.
+ 'timeout' requests not successfully completed after this
+ many seconds should be aborted. If timeout is None, then
+ the handler should not set a timeout.
+ 'headers' is a list of ("header-name","header-value") tuples
+ which must be passed to the pushgateway in the form of HTTP
+ request headers.
+ The function should raise an exception (e.g. IOError) on
+ failure.
+ 'content' is the data which should be used to form the HTTP
+ Message Body.
+
+ This overwrites all metrics with the same job and grouping_key.
+ This uses the PUT HTTP method."""
+ _use_gateway('PUT', gateway, job, registry, grouping_key, timeout, handler)
+
+
+def pushadd_to_gateway(
+ gateway: str,
+ job: str,
+ registry: Optional[CollectorRegistry],
+ grouping_key: Optional[Dict[str, Any]] = None,
+ timeout: Optional[float] = 30,
+ handler: Callable = default_handler,
+) -> None:
+ """PushAdd metrics to the given pushgateway.
+
+ `gateway` the url for your push gateway. Either of the form
+ 'http://pushgateway.local', or 'pushgateway.local'.
+ Scheme defaults to 'http' if none is provided
+ `job` is the job label to be attached to all pushed metrics
+ `registry` is an instance of CollectorRegistry
+ `grouping_key` please see the pushgateway documentation for details.
+ Defaults to None
+ `timeout` is how long push will attempt to connect before giving up.
+ Defaults to 30s, can be set to None for no timeout.
+ `handler` is an optional function which can be provided to perform
+ requests to the 'gateway'.
+ Defaults to None, in which case an http or https request
+ will be carried out by a default handler.
+ See the 'prometheus_client.push_to_gateway' documentation
+ for implementation requirements.
+
+ This replaces metrics with the same name, job and grouping_key.
+ This uses the POST HTTP method."""
+ _use_gateway('POST', gateway, job, registry, grouping_key, timeout, handler)
+
+
+def delete_from_gateway(
+ gateway: str,
+ job: str,
+ grouping_key: Optional[Dict[str, Any]] = None,
+ timeout: Optional[float] = 30,
+ handler: Callable = default_handler,
+) -> None:
+ """Delete metrics from the given pushgateway.
+
+ `gateway` the url for your push gateway. Either of the form
+ 'http://pushgateway.local', or 'pushgateway.local'.
+ Scheme defaults to 'http' if none is provided
+ `job` is the job label to be attached to all pushed metrics
+ `grouping_key` please see the pushgateway documentation for details.
+ Defaults to None
+ `timeout` is how long delete will attempt to connect before giving up.
+ Defaults to 30s, can be set to None for no timeout.
+ `handler` is an optional function which can be provided to perform
+ requests to the 'gateway'.
+ Defaults to None, in which case an http or https request
+ will be carried out by a default handler.
+ See the 'prometheus_client.push_to_gateway' documentation
+ for implementation requirements.
+
+ This deletes metrics with the given job and grouping_key.
+ This uses the DELETE HTTP method."""
+ _use_gateway('DELETE', gateway, job, None, grouping_key, timeout, handler)
+
+
+def _use_gateway(
+ method: str,
+ gateway: str,
+ job: str,
+ registry: Optional[CollectorRegistry],
+ grouping_key: Optional[Dict[str, Any]],
+ timeout: Optional[float],
+ handler: Callable,
+) -> 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 gateway_url.scheme not in ['http', 'https']:
+ gateway = f'http://{gateway}'
+
+ gateway = gateway.rstrip('/')
+ url = '{}/metrics/{}/{}'.format(gateway, *_escape_grouping_key("job", job))
+
+ data = b''
+ if method != 'DELETE':
+ if registry is None:
+ registry = REGISTRY
+ data = generate_latest(registry)
+
+ if grouping_key is None:
+ grouping_key = {}
+ url += ''.join(
+ '/{}/{}'.format(*_escape_grouping_key(str(k), str(v)))
+ for k, v in sorted(grouping_key.items()))
+
+ handler(
+ url=url, method=method, timeout=timeout,
+ headers=[('Content-Type', CONTENT_TYPE_LATEST)], data=data,
+ )()
+
+
+def _escape_grouping_key(k, v):
+ if v == "":
+ # Per https://github.com/prometheus/pushgateway/pull/346.
+ return k + "@base64", "="
+ elif '/' in v:
+ # Added in Pushgateway 0.9.0.
+ return k + "@base64", base64.urlsafe_b64encode(v.encode("utf-8")).decode("utf-8")
+ else:
+ return k, quote_plus(v)
+
+
+def instance_ip_grouping_key() -> Dict[str, Any]:
+ """Grouping key with instance set to the IP Address of this host."""
+ with closing(socket.socket(socket.AF_INET, socket.SOCK_DGRAM)) as s:
+ if sys.platform == 'darwin':
+ # This check is done this way only on MacOS devices
+ # it is done this way because the localhost method does
+ # not work.
+ # This method was adapted from this StackOverflow answer:
+ # https://stackoverflow.com/a/28950776
+ s.connect(('10.255.255.255', 1))
+ else:
+ s.connect(('localhost', 0))
+
+ return {'instance': s.getsockname()[0]}
+
+
+from .asgi import make_asgi_app # noqa
diff --git a/.venv/lib/python3.12/site-packages/prometheus_client/gc_collector.py b/.venv/lib/python3.12/site-packages/prometheus_client/gc_collector.py
new file mode 100644
index 00000000..06e52dfc
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/prometheus_client/gc_collector.py
@@ -0,0 +1,45 @@
+import gc
+import platform
+from typing import Iterable
+
+from .metrics_core import CounterMetricFamily, Metric
+from .registry import Collector, CollectorRegistry, REGISTRY
+
+
+class GCCollector(Collector):
+ """Collector for Garbage collection statistics."""
+
+ def __init__(self, registry: CollectorRegistry = REGISTRY):
+ if not hasattr(gc, 'get_stats') or platform.python_implementation() != 'CPython':
+ return
+ registry.register(self)
+
+ def collect(self) -> Iterable[Metric]:
+ collected = CounterMetricFamily(
+ 'python_gc_objects_collected',
+ 'Objects collected during gc',
+ labels=['generation'],
+ )
+ uncollectable = CounterMetricFamily(
+ 'python_gc_objects_uncollectable',
+ 'Uncollectable objects found during GC',
+ labels=['generation'],
+ )
+
+ collections = CounterMetricFamily(
+ 'python_gc_collections',
+ 'Number of times this generation was collected',
+ labels=['generation'],
+ )
+
+ for gen, stat in enumerate(gc.get_stats()):
+ generation = str(gen)
+ collected.add_metric([generation], value=stat['collected'])
+ uncollectable.add_metric([generation], value=stat['uncollectable'])
+ collections.add_metric([generation], value=stat['collections'])
+
+ return [collected, uncollectable, collections]
+
+
+GC_COLLECTOR = GCCollector()
+"""Default GCCollector in default Registry REGISTRY."""
diff --git a/.venv/lib/python3.12/site-packages/prometheus_client/metrics.py b/.venv/lib/python3.12/site-packages/prometheus_client/metrics.py
new file mode 100644
index 00000000..af512115
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/prometheus_client/metrics.py
@@ -0,0 +1,776 @@
+import os
+from threading import Lock
+import time
+import types
+from typing import (
+ Any, Callable, Dict, Iterable, List, Literal, Optional, Sequence, Tuple,
+ Type, TypeVar, Union,
+)
+import warnings
+
+from . import values # retain this import style for testability
+from .context_managers import ExceptionCounter, InprogressTracker, Timer
+from .metrics_core import (
+ Metric, METRIC_LABEL_NAME_RE, METRIC_NAME_RE,
+ RESERVED_METRIC_LABEL_NAME_RE,
+)
+from .registry import Collector, CollectorRegistry, REGISTRY
+from .samples import Exemplar, Sample
+from .utils import floatToGoString, INF
+
+T = TypeVar('T', bound='MetricWrapperBase')
+F = TypeVar("F", bound=Callable[..., Any])
+
+
+def _build_full_name(metric_type, name, namespace, subsystem, unit):
+ full_name = ''
+ if namespace:
+ full_name += namespace + '_'
+ if subsystem:
+ full_name += subsystem + '_'
+ full_name += name
+ if metric_type == 'counter' and full_name.endswith('_total'):
+ full_name = full_name[:-6] # Munge to OpenMetrics.
+ if unit and not full_name.endswith("_" + unit):
+ full_name += "_" + unit
+ if unit and metric_type in ('info', 'stateset'):
+ raise ValueError('Metric name is of a type that cannot have a unit: ' + full_name)
+ return full_name
+
+
+def _validate_labelname(l):
+ if not METRIC_LABEL_NAME_RE.match(l):
+ raise ValueError('Invalid label metric name: ' + l)
+ if RESERVED_METRIC_LABEL_NAME_RE.match(l):
+ raise ValueError('Reserved label metric name: ' + l)
+
+
+def _validate_labelnames(cls, labelnames):
+ labelnames = tuple(labelnames)
+ for l in labelnames:
+ _validate_labelname(l)
+ if l in cls._reserved_labelnames:
+ raise ValueError('Reserved label metric name: ' + l)
+ return labelnames
+
+
+def _validate_exemplar(exemplar):
+ runes = 0
+ for k, v in exemplar.items():
+ _validate_labelname(k)
+ runes += len(k)
+ runes += len(v)
+ if runes > 128:
+ raise ValueError('Exemplar labels have %d UTF-8 characters, exceeding the limit of 128')
+
+
+def _get_use_created() -> bool:
+ return os.environ.get("PROMETHEUS_DISABLE_CREATED_SERIES", 'False').lower() not in ('true', '1', 't')
+
+
+_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] = ()
+
+ def _is_observable(self):
+ # Whether this metric is observable, i.e.
+ # * a metric without label names and values, or
+ # * the child of a labelled metric.
+ return not self._labelnames or (self._labelnames and self._labelvalues)
+
+ def _raise_if_not_observable(self):
+ # Functions that mutate the state of the metric, for example incrementing
+ # a counter, will fail if the metric is not observable, because only if a
+ # metric is observable will the value be initialized.
+ if not self._is_observable():
+ raise ValueError('%s metric is missing label values' % str(self._type))
+
+ def _is_parent(self):
+ return self._labelnames and not self._labelvalues
+
+ def _get_metric(self):
+ return Metric(self._name, self._documentation, self._type, self._unit)
+
+ def describe(self) -> Iterable[Metric]:
+ return [self._get_metric()]
+
+ def collect(self) -> Iterable[Metric]:
+ metric = self._get_metric()
+ for suffix, labels, value, timestamp, exemplar in self._samples():
+ metric.add_sample(self._name + suffix, labels, value, timestamp, exemplar)
+ return [metric]
+
+ def __str__(self) -> str:
+ return f"{self._type}:{self._name}"
+
+ def __repr__(self) -> str:
+ metric_type = type(self)
+ return f"{metric_type.__module__}.{metric_type.__name__}({self._name})"
+
+ def __init__(self: T,
+ name: str,
+ documentation: str,
+ labelnames: Iterable[str] = (),
+ namespace: str = '',
+ subsystem: str = '',
+ unit: str = '',
+ registry: Optional[CollectorRegistry] = REGISTRY,
+ _labelvalues: Optional[Sequence[str]] = None,
+ ) -> None:
+ self._name = _build_full_name(self._type, name, namespace, subsystem, unit)
+ self._labelnames = _validate_labelnames(self, labelnames)
+ self._labelvalues = tuple(_labelvalues or ())
+ self._kwargs: Dict[str, Any] = {}
+ self._documentation = documentation
+ self._unit = unit
+
+ if not METRIC_NAME_RE.match(self._name):
+ raise ValueError('Invalid metric name: ' + self._name)
+
+ if self._is_parent():
+ # Prepare the fields needed for child metrics.
+ self._lock = Lock()
+ self._metrics: Dict[Sequence[str], T] = {}
+
+ if self._is_observable():
+ self._metric_init()
+
+ if not self._labelvalues:
+ # Register the multi-wrapper parent metric, or if a label-less metric, the whole shebang.
+ if registry:
+ registry.register(self)
+
+ def labels(self: T, *labelvalues: Any, **labelkwargs: Any) -> T:
+ """Return the child for the given labelset.
+
+ All metrics can have labels, allowing grouping of related time series.
+ Taking a counter as an example:
+
+ from prometheus_client import Counter
+
+ c = Counter('my_requests_total', 'HTTP Failures', ['method', 'endpoint'])
+ c.labels('get', '/').inc()
+ c.labels('post', '/submit').inc()
+
+ Labels can also be provided as keyword arguments:
+
+ from prometheus_client import Counter
+
+ c = Counter('my_requests_total', 'HTTP Failures', ['method', 'endpoint'])
+ c.labels(method='get', endpoint='/').inc()
+ c.labels(method='post', endpoint='/submit').inc()
+
+ See the best practices on [naming](http://prometheus.io/docs/practices/naming/)
+ and [labels](http://prometheus.io/docs/practices/instrumentation/#use-labels).
+ """
+ if not self._labelnames:
+ raise ValueError('No label names were set when constructing %s' % self)
+
+ if self._labelvalues:
+ raise ValueError('{} already has labels set ({}); can not chain calls to .labels()'.format(
+ self,
+ dict(zip(self._labelnames, self._labelvalues))
+ ))
+
+ if labelvalues and labelkwargs:
+ raise ValueError("Can't pass both *args and **kwargs")
+
+ if labelkwargs:
+ if sorted(labelkwargs) != sorted(self._labelnames):
+ raise ValueError('Incorrect label names')
+ labelvalues = tuple(str(labelkwargs[l]) for l in self._labelnames)
+ else:
+ if len(labelvalues) != len(self._labelnames):
+ raise ValueError('Incorrect label count')
+ labelvalues = tuple(str(l) for l in labelvalues)
+ with self._lock:
+ if labelvalues not in self._metrics:
+ self._metrics[labelvalues] = self.__class__(
+ self._name,
+ documentation=self._documentation,
+ labelnames=self._labelnames,
+ unit=self._unit,
+ _labelvalues=labelvalues,
+ **self._kwargs
+ )
+ return self._metrics[labelvalues]
+
+ def remove(self, *labelvalues: Any) -> None:
+ if 'prometheus_multiproc_dir' in os.environ or 'PROMETHEUS_MULTIPROC_DIR' in os.environ:
+ warnings.warn(
+ "Removal of labels has not been implemented in multi-process mode yet.",
+ UserWarning)
+
+ if not self._labelnames:
+ raise ValueError('No label names were set when constructing %s' % self)
+
+ """Remove the given labelset from the metric."""
+ if len(labelvalues) != len(self._labelnames):
+ raise ValueError('Incorrect label count (expected %d, got %s)' % (len(self._labelnames), labelvalues))
+ labelvalues = tuple(str(l) for l in labelvalues)
+ with self._lock:
+ del self._metrics[labelvalues]
+
+ def clear(self) -> None:
+ """Remove all labelsets from the metric"""
+ if 'prometheus_multiproc_dir' in os.environ or 'PROMETHEUS_MULTIPROC_DIR' in os.environ:
+ warnings.warn(
+ "Clearing labels has not been implemented in multi-process mode yet",
+ UserWarning)
+ with self._lock:
+ self._metrics = {}
+
+ def _samples(self) -> Iterable[Sample]:
+ if self._is_parent():
+ return self._multi_samples()
+ else:
+ return self._child_samples()
+
+ def _multi_samples(self) -> Iterable[Sample]:
+ with self._lock:
+ metrics = self._metrics.copy()
+ for labels, metric in metrics.items():
+ series_labels = list(zip(self._labelnames, labels))
+ for suffix, sample_labels, value, timestamp, exemplar in metric._samples():
+ yield Sample(suffix, dict(series_labels + list(sample_labels.items())), value, timestamp, exemplar)
+
+ def _child_samples(self) -> Iterable[Sample]: # pragma: no cover
+ raise NotImplementedError('_child_samples() must be implemented by %r' % self)
+
+ def _metric_init(self): # pragma: no cover
+ """
+ Initialize the metric object as a child, i.e. when it has labels (if any) set.
+
+ This is factored as a separate function to allow for deferred initialization.
+ """
+ raise NotImplementedError('_metric_init() must be implemented by %r' % self)
+
+
+class Counter(MetricWrapperBase):
+ """A Counter tracks counts of events or running totals.
+
+ Example use cases for Counters:
+ - Number of requests processed
+ - Number of items that were inserted into a queue
+ - Total amount of data that a system has processed
+
+ Counters can only go up (and be reset when the process restarts). If your use case can go down,
+ you should use a Gauge instead.
+
+ An example for a Counter:
+
+ from prometheus_client import Counter
+
+ c = Counter('my_failures_total', 'Description of counter')
+ c.inc() # Increment by 1
+ c.inc(1.6) # Increment by given value
+
+ There are utilities to count exceptions raised:
+
+ @c.count_exceptions()
+ def f():
+ pass
+
+ with c.count_exceptions():
+ pass
+
+ # Count only one type of exception
+ with c.count_exceptions(ValueError):
+ pass
+
+ You can also reset the counter to zero in case your logical "process" restarts
+ without restarting the actual python process.
+
+ c.reset()
+
+ """
+ _type = 'counter'
+
+ def _metric_init(self) -> None:
+ self._value = values.ValueClass(self._type, self._name, self._name + '_total', self._labelnames,
+ self._labelvalues, self._documentation)
+ self._created = time.time()
+
+ def inc(self, amount: float = 1, exemplar: Optional[Dict[str, str]] = None) -> None:
+ """Increment counter by the given amount."""
+ self._raise_if_not_observable()
+ if amount < 0:
+ raise ValueError('Counters can only be incremented by non-negative amounts.')
+ self._value.inc(amount)
+ if exemplar:
+ _validate_exemplar(exemplar)
+ self._value.set_exemplar(Exemplar(exemplar, amount, time.time()))
+
+ def reset(self) -> None:
+ """Reset the counter to zero. Use this when a logical process restarts without restarting the actual python process."""
+ self._value.set(0)
+ self._created = time.time()
+
+ def count_exceptions(self, exception: Union[Type[BaseException], Tuple[Type[BaseException], ...]] = Exception) -> ExceptionCounter:
+ """Count exceptions in a block of code or function.
+
+ Can be used as a function decorator or context manager.
+ Increments the counter when an exception of the given
+ type is raised up out of the code.
+ """
+ self._raise_if_not_observable()
+ return ExceptionCounter(self, exception)
+
+ def _child_samples(self) -> Iterable[Sample]:
+ sample = Sample('_total', {}, self._value.get(), None, self._value.get_exemplar())
+ if _use_created:
+ return (
+ sample,
+ Sample('_created', {}, self._created, None, None)
+ )
+ return (sample,)
+
+
+class Gauge(MetricWrapperBase):
+ """Gauge metric, to report instantaneous values.
+
+ Examples of Gauges include:
+ - Inprogress requests
+ - Number of items in a queue
+ - Free memory
+ - Total memory
+ - Temperature
+
+ Gauges can go both up and down.
+
+ from prometheus_client import Gauge
+
+ g = Gauge('my_inprogress_requests', 'Description of gauge')
+ g.inc() # Increment by 1
+ g.dec(10) # Decrement by given value
+ g.set(4.2) # Set to a given value
+
+ There are utilities for common use cases:
+
+ g.set_to_current_time() # Set to current unixtime
+
+ # Increment when entered, decrement when exited.
+ @g.track_inprogress()
+ def f():
+ pass
+
+ with g.track_inprogress():
+ pass
+
+ A Gauge can also take its value from a callback:
+
+ d = Gauge('data_objects', 'Number of objects')
+ my_dict = {}
+ d.set_function(lambda: len(my_dict))
+ """
+ _type = 'gauge'
+ _MULTIPROC_MODES = frozenset(('all', 'liveall', 'min', 'livemin', 'max', 'livemax', 'sum', 'livesum', 'mostrecent', 'livemostrecent'))
+ _MOST_RECENT_MODES = frozenset(('mostrecent', 'livemostrecent'))
+
+ def __init__(self,
+ name: str,
+ documentation: str,
+ labelnames: Iterable[str] = (),
+ namespace: str = '',
+ subsystem: str = '',
+ unit: str = '',
+ registry: Optional[CollectorRegistry] = REGISTRY,
+ _labelvalues: Optional[Sequence[str]] = None,
+ 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:
+ raise ValueError('Invalid multiprocess mode: ' + multiprocess_mode)
+ super().__init__(
+ name=name,
+ documentation=documentation,
+ labelnames=labelnames,
+ namespace=namespace,
+ subsystem=subsystem,
+ unit=unit,
+ registry=registry,
+ _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(
+ self._type, self._name, self._name, self._labelnames, self._labelvalues,
+ self._documentation, multiprocess_mode=self._multiprocess_mode
+ )
+
+ 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()
+ 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."""
+ self.set(time.time())
+
+ def track_inprogress(self) -> InprogressTracker:
+ """Track inprogress blocks of code or functions.
+
+ Can be used as a function decorator or context manager.
+ Increments the gauge when the code is entered,
+ and decrements when it is exited.
+ """
+ self._raise_if_not_observable()
+ return InprogressTracker(self)
+
+ def time(self) -> Timer:
+ """Time a block of code or function, and set the duration in seconds.
+
+ Can be used as a function decorator or context manager.
+ """
+ return Timer(self, 'set')
+
+ def set_function(self, f: Callable[[], float]) -> None:
+ """Call the provided function to return the Gauge value.
+
+ The function must return a float, and may be called from
+ multiple threads. All other methods of the Gauge become NOOPs.
+ """
+
+ self._raise_if_not_observable()
+
+ def samples(_: Gauge) -> Iterable[Sample]:
+ return (Sample('', {}, float(f()), None, None),)
+
+ self._child_samples = types.MethodType(samples, self) # type: ignore
+
+ def _child_samples(self) -> Iterable[Sample]:
+ return (Sample('', {}, self._value.get(), None, None),)
+
+
+class Summary(MetricWrapperBase):
+ """A Summary tracks the size and number of events.
+
+ Example use cases for Summaries:
+ - Response latency
+ - Request size
+
+ Example for a Summary:
+
+ from prometheus_client import Summary
+
+ s = Summary('request_size_bytes', 'Request size (bytes)')
+ s.observe(512) # Observe 512 (bytes)
+
+ Example for a Summary using time:
+
+ from prometheus_client import Summary
+
+ REQUEST_TIME = Summary('response_latency_seconds', 'Response latency (seconds)')
+
+ @REQUEST_TIME.time()
+ def create_response(request):
+ '''A dummy function'''
+ time.sleep(1)
+
+ Example for using the same Summary object as a context manager:
+
+ with REQUEST_TIME.time():
+ pass # Logic to be timed
+ """
+ _type = 'summary'
+ _reserved_labelnames = ['quantile']
+
+ def _metric_init(self) -> None:
+ self._count = values.ValueClass(self._type, self._name, self._name + '_count', self._labelnames,
+ self._labelvalues, self._documentation)
+ self._sum = values.ValueClass(self._type, self._name, self._name + '_sum', self._labelnames, self._labelvalues, self._documentation)
+ self._created = time.time()
+
+ def observe(self, amount: float) -> None:
+ """Observe the given amount.
+
+ The amount is usually positive or zero. Negative values are
+ accepted but prevent current versions of Prometheus from
+ properly detecting counter resets in the sum of
+ observations. See
+ https://prometheus.io/docs/practices/histograms/#count-and-sum-of-observations
+ for details.
+ """
+ self._raise_if_not_observable()
+ self._count.inc(1)
+ self._sum.inc(amount)
+
+ def time(self) -> Timer:
+ """Time a block of code or function, and observe the duration in seconds.
+
+ Can be used as a function decorator or context manager.
+ """
+ return Timer(self, 'observe')
+
+ def _child_samples(self) -> Iterable[Sample]:
+ samples = [
+ Sample('_count', {}, self._count.get(), None, None),
+ Sample('_sum', {}, self._sum.get(), None, None),
+ ]
+ if _use_created:
+ samples.append(Sample('_created', {}, self._created, None, None))
+ return tuple(samples)
+
+
+class Histogram(MetricWrapperBase):
+ """A Histogram tracks the size and number of events in buckets.
+
+ You can use Histograms for aggregatable calculation of quantiles.
+
+ Example use cases:
+ - Response latency
+ - Request size
+
+ Example for a Histogram:
+
+ from prometheus_client import Histogram
+
+ h = Histogram('request_size_bytes', 'Request size (bytes)')
+ h.observe(512) # Observe 512 (bytes)
+
+ Example for a Histogram using time:
+
+ from prometheus_client import Histogram
+
+ REQUEST_TIME = Histogram('response_latency_seconds', 'Response latency (seconds)')
+
+ @REQUEST_TIME.time()
+ def create_response(request):
+ '''A dummy function'''
+ time.sleep(1)
+
+ Example of using the same Histogram object as a context manager:
+
+ with REQUEST_TIME.time():
+ pass # Logic to be timed
+
+ 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`.
+ """
+ _type = 'histogram'
+ _reserved_labelnames = ['le']
+ DEFAULT_BUCKETS = (.005, .01, .025, .05, .075, .1, .25, .5, .75, 1.0, 2.5, 5.0, 7.5, 10.0, INF)
+
+ def __init__(self,
+ name: str,
+ documentation: str,
+ labelnames: Iterable[str] = (),
+ namespace: str = '',
+ subsystem: str = '',
+ unit: str = '',
+ registry: Optional[CollectorRegistry] = REGISTRY,
+ _labelvalues: Optional[Sequence[str]] = None,
+ buckets: Sequence[Union[float, str]] = DEFAULT_BUCKETS,
+ ):
+ self._prepare_buckets(buckets)
+ super().__init__(
+ name=name,
+ documentation=documentation,
+ labelnames=labelnames,
+ namespace=namespace,
+ subsystem=subsystem,
+ unit=unit,
+ registry=registry,
+ _labelvalues=_labelvalues,
+ )
+ self._kwargs['buckets'] = buckets
+
+ def _prepare_buckets(self, source_buckets: Sequence[Union[float, str]]) -> None:
+ buckets = [float(b) for b in source_buckets]
+ if buckets != sorted(buckets):
+ # This is probably an error on the part of the user,
+ # so raise rather than sorting for them.
+ raise ValueError('Buckets not in sorted order')
+ if buckets and buckets[-1] != INF:
+ buckets.append(INF)
+ if len(buckets) < 2:
+ raise ValueError('Must have at least two buckets')
+ self._upper_bounds = buckets
+
+ def _metric_init(self) -> None:
+ self._buckets: List[values.ValueClass] = []
+ self._created = time.time()
+ bucket_labelnames = self._labelnames + ('le',)
+ self._sum = values.ValueClass(self._type, self._name, self._name + '_sum', self._labelnames, self._labelvalues, self._documentation)
+ for b in self._upper_bounds:
+ self._buckets.append(values.ValueClass(
+ self._type,
+ self._name,
+ self._name + '_bucket',
+ bucket_labelnames,
+ self._labelvalues + (floatToGoString(b),),
+ self._documentation)
+ )
+
+ def observe(self, amount: float, exemplar: Optional[Dict[str, str]] = None) -> None:
+ """Observe the given amount.
+
+ The amount is usually positive or zero. Negative values are
+ accepted but prevent current versions of Prometheus from
+ properly detecting counter resets in the sum of
+ observations. See
+ https://prometheus.io/docs/practices/histograms/#count-and-sum-of-observations
+ for details.
+ """
+ self._raise_if_not_observable()
+ self._sum.inc(amount)
+ for i, bound in enumerate(self._upper_bounds):
+ if amount <= bound:
+ self._buckets[i].inc(1)
+ if exemplar:
+ _validate_exemplar(exemplar)
+ self._buckets[i].set_exemplar(Exemplar(exemplar, amount, time.time()))
+ break
+
+ def time(self) -> Timer:
+ """Time a block of code or function, and observe the duration in seconds.
+
+ Can be used as a function decorator or context manager.
+ """
+ return Timer(self, 'observe')
+
+ def _child_samples(self) -> Iterable[Sample]:
+ samples = []
+ acc = 0.0
+ for i, bound in enumerate(self._upper_bounds):
+ acc += self._buckets[i].get()
+ samples.append(Sample('_bucket', {'le': floatToGoString(bound)}, acc, None, self._buckets[i].get_exemplar()))
+ samples.append(Sample('_count', {}, acc, None, None))
+ if self._upper_bounds[0] >= 0:
+ samples.append(Sample('_sum', {}, self._sum.get(), None, None))
+ if _use_created:
+ samples.append(Sample('_created', {}, self._created, None, None))
+ return tuple(samples)
+
+
+class Info(MetricWrapperBase):
+ """Info metric, key-value pairs.
+
+ Examples of Info include:
+ - Build information
+ - Version information
+ - Potential target metadata
+
+ Example usage:
+ from prometheus_client import Info
+
+ i = Info('my_build', 'Description of info')
+ i.info({'version': '1.2.3', 'buildhost': 'foo@bar'})
+
+ Info metrics do not work in multiprocess mode.
+ """
+ _type = 'info'
+
+ def _metric_init(self):
+ self._labelname_set = set(self._labelnames)
+ self._lock = Lock()
+ self._value = {}
+
+ def info(self, val: Dict[str, str]) -> None:
+ """Set info metric."""
+ if self._labelname_set.intersection(val.keys()):
+ raise ValueError('Overlapping labels for Info metric, metric: {} child: {}'.format(
+ self._labelnames, val))
+ if any(i is None for i in val.values()):
+ raise ValueError('Label value cannot be None')
+ with self._lock:
+ self._value = dict(val)
+
+ def _child_samples(self) -> Iterable[Sample]:
+ with self._lock:
+ return (Sample('_info', self._value, 1.0, None, None),)
+
+
+class Enum(MetricWrapperBase):
+ """Enum metric, which of a set of states is true.
+
+ Example usage:
+ from prometheus_client import Enum
+
+ e = Enum('task_state', 'Description of enum',
+ states=['starting', 'running', 'stopped'])
+ e.state('running')
+
+ The first listed state will be the default.
+ Enum metrics do not work in multiprocess mode.
+ """
+ _type = 'stateset'
+
+ def __init__(self,
+ name: str,
+ documentation: str,
+ labelnames: Sequence[str] = (),
+ namespace: str = '',
+ subsystem: str = '',
+ unit: str = '',
+ registry: Optional[CollectorRegistry] = REGISTRY,
+ _labelvalues: Optional[Sequence[str]] = None,
+ states: Optional[Sequence[str]] = None,
+ ):
+ super().__init__(
+ name=name,
+ documentation=documentation,
+ labelnames=labelnames,
+ namespace=namespace,
+ subsystem=subsystem,
+ unit=unit,
+ registry=registry,
+ _labelvalues=_labelvalues,
+ )
+ if name in labelnames:
+ raise ValueError(f'Overlapping labels for Enum metric: {name}')
+ if not states:
+ raise ValueError(f'No states provided for Enum metric: {name}')
+ self._kwargs['states'] = self._states = states
+
+ def _metric_init(self) -> None:
+ self._value = 0
+ self._lock = Lock()
+
+ def state(self, state: str) -> None:
+ """Set enum metric state."""
+ self._raise_if_not_observable()
+ with self._lock:
+ self._value = self._states.index(state)
+
+ def _child_samples(self) -> Iterable[Sample]:
+ with self._lock:
+ return [
+ Sample('', {self._name: s}, 1 if i == self._value else 0, None, None)
+ for i, s
+ in enumerate(self._states)
+ ]
diff --git a/.venv/lib/python3.12/site-packages/prometheus_client/metrics_core.py b/.venv/lib/python3.12/site-packages/prometheus_client/metrics_core.py
new file mode 100644
index 00000000..7226d920
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/prometheus_client/metrics_core.py
@@ -0,0 +1,418 @@
+import re
+from typing import Dict, List, Optional, Sequence, Tuple, Union
+
+from .samples import Exemplar, Sample, Timestamp
+
+METRIC_TYPES = (
+ 'counter', 'gauge', 'summary', 'histogram',
+ 'gaugehistogram', 'unknown', 'info', 'stateset',
+)
+METRIC_NAME_RE = re.compile(r'^[a-zA-Z_:][a-zA-Z0-9_:]*$')
+METRIC_LABEL_NAME_RE = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]*$')
+RESERVED_METRIC_LABEL_NAME_RE = re.compile(r'^__.*$')
+
+
+class Metric:
+ """A single metric family and its samples.
+
+ This is intended only for internal use by the instrumentation client.
+
+ Custom collectors should use GaugeMetricFamily, CounterMetricFamily
+ and SummaryMetricFamily instead.
+ """
+
+ def __init__(self, name: str, documentation: str, typ: str, unit: str = ''):
+ if unit and not name.endswith("_" + unit):
+ name += "_" + unit
+ if not METRIC_NAME_RE.match(name):
+ raise ValueError('Invalid metric name: ' + name)
+ self.name: str = name
+ self.documentation: str = documentation
+ self.unit: str = unit
+ if typ == 'untyped':
+ typ = 'unknown'
+ if typ not in METRIC_TYPES:
+ raise ValueError('Invalid metric type: ' + typ)
+ self.type: str = typ
+ self.samples: List[Sample] = []
+
+ def add_sample(self, name: str, labels: Dict[str, str], value: float, timestamp: Optional[Union[Timestamp, float]] = None, exemplar: Optional[Exemplar] = None) -> None:
+ """Add a sample to the metric.
+
+ Internal-only, do not use."""
+ self.samples.append(Sample(name, labels, value, timestamp, exemplar))
+
+ def __eq__(self, other: object) -> bool:
+ return (isinstance(other, Metric)
+ and self.name == other.name
+ and self.documentation == other.documentation
+ and self.type == other.type
+ and self.unit == other.unit
+ and self.samples == other.samples)
+
+ def __repr__(self) -> str:
+ return "Metric({}, {}, {}, {}, {})".format(
+ self.name,
+ self.documentation,
+ self.type,
+ self.unit,
+ self.samples,
+ )
+
+ def _restricted_metric(self, names):
+ """Build a snapshot of a metric with samples restricted to a given set of names."""
+ samples = [s for s in self.samples if s[0] in names]
+ if samples:
+ m = Metric(self.name, self.documentation, self.type)
+ m.samples = samples
+ return m
+ return None
+
+
+class UnknownMetricFamily(Metric):
+ """A single unknown metric and its samples.
+ For use by custom collectors.
+ """
+
+ def __init__(self,
+ name: str,
+ documentation: str,
+ value: Optional[float] = None,
+ labels: Optional[Sequence[str]] = None,
+ unit: str = '',
+ ):
+ Metric.__init__(self, name, documentation, 'unknown', unit)
+ if labels is not None and value is not None:
+ raise ValueError('Can only specify at most one of value and labels.')
+ if labels is None:
+ labels = []
+ self._labelnames = tuple(labels)
+ if value is not None:
+ self.add_metric([], value)
+
+ def add_metric(self, labels: Sequence[str], value: float, timestamp: Optional[Union[Timestamp, float]] = None) -> None:
+ """Add a metric to the metric family.
+ Args:
+ labels: A list of label values
+ value: The value of the metric.
+ """
+ self.samples.append(Sample(self.name, dict(zip(self._labelnames, labels)), value, timestamp))
+
+
+# For backward compatibility.
+UntypedMetricFamily = UnknownMetricFamily
+
+
+class CounterMetricFamily(Metric):
+ """A single counter and its samples.
+
+ For use by custom collectors.
+ """
+
+ def __init__(self,
+ name: str,
+ documentation: str,
+ value: Optional[float] = None,
+ labels: Optional[Sequence[str]] = None,
+ created: Optional[float] = None,
+ unit: str = '',
+ ):
+ # Glue code for pre-OpenMetrics metrics.
+ if name.endswith('_total'):
+ name = name[:-6]
+ Metric.__init__(self, name, documentation, 'counter', unit)
+ if labels is not None and value is not None:
+ raise ValueError('Can only specify at most one of value and labels.')
+ if labels is None:
+ labels = []
+ self._labelnames = tuple(labels)
+ if value is not None:
+ self.add_metric([], value, created)
+
+ def add_metric(self,
+ labels: Sequence[str],
+ value: float,
+ created: Optional[float] = None,
+ timestamp: Optional[Union[Timestamp, float]] = None,
+ ) -> None:
+ """Add a metric to the metric family.
+
+ Args:
+ labels: A list of label values
+ value: The value of the metric
+ created: Optional unix timestamp the child was created at.
+ """
+ self.samples.append(Sample(self.name + '_total', dict(zip(self._labelnames, labels)), value, timestamp))
+ if created is not None:
+ self.samples.append(Sample(self.name + '_created', dict(zip(self._labelnames, labels)), created, timestamp))
+
+
+class GaugeMetricFamily(Metric):
+ """A single gauge and its samples.
+
+ For use by custom collectors.
+ """
+
+ def __init__(self,
+ name: str,
+ documentation: str,
+ value: Optional[float] = None,
+ labels: Optional[Sequence[str]] = None,
+ unit: str = '',
+ ):
+ Metric.__init__(self, name, documentation, 'gauge', unit)
+ if labels is not None and value is not None:
+ raise ValueError('Can only specify at most one of value and labels.')
+ if labels is None:
+ labels = []
+ self._labelnames = tuple(labels)
+ if value is not None:
+ self.add_metric([], value)
+
+ def add_metric(self, labels: Sequence[str], value: float, timestamp: Optional[Union[Timestamp, float]] = None) -> None:
+ """Add a metric to the metric family.
+
+ Args:
+ labels: A list of label values
+ value: A float
+ """
+ self.samples.append(Sample(self.name, dict(zip(self._labelnames, labels)), value, timestamp))
+
+
+class SummaryMetricFamily(Metric):
+ """A single summary and its samples.
+
+ For use by custom collectors.
+ """
+
+ def __init__(self,
+ name: str,
+ documentation: str,
+ count_value: Optional[int] = None,
+ sum_value: Optional[float] = None,
+ labels: Optional[Sequence[str]] = None,
+ unit: str = '',
+ ):
+ Metric.__init__(self, name, documentation, 'summary', unit)
+ if (sum_value is None) != (count_value is None):
+ raise ValueError('count_value and sum_value must be provided together.')
+ if labels is not None and count_value is not None:
+ raise ValueError('Can only specify at most one of value and labels.')
+ if labels is None:
+ labels = []
+ self._labelnames = tuple(labels)
+ # The and clause is necessary only for typing, the above ValueError will raise if only one is set.
+ if count_value is not None and sum_value is not None:
+ self.add_metric([], count_value, sum_value)
+
+ def add_metric(self,
+ labels: Sequence[str],
+ count_value: int,
+ sum_value: float,
+ timestamp:
+ Optional[Union[float, Timestamp]] = None
+ ) -> None:
+ """Add a metric to the metric family.
+
+ Args:
+ labels: A list of label values
+ count_value: The count value of the metric.
+ sum_value: The sum value of the metric.
+ """
+ self.samples.append(Sample(self.name + '_count', dict(zip(self._labelnames, labels)), count_value, timestamp))
+ self.samples.append(Sample(self.name + '_sum', dict(zip(self._labelnames, labels)), sum_value, timestamp))
+
+
+class HistogramMetricFamily(Metric):
+ """A single histogram and its samples.
+
+ For use by custom collectors.
+ """
+
+ def __init__(self,
+ name: str,
+ documentation: str,
+ buckets: Optional[Sequence[Union[Tuple[str, float], Tuple[str, float, Exemplar]]]] = None,
+ sum_value: Optional[float] = None,
+ labels: Optional[Sequence[str]] = None,
+ unit: str = '',
+ ):
+ Metric.__init__(self, name, documentation, 'histogram', unit)
+ if sum_value is not None and buckets is None:
+ raise ValueError('sum value cannot be provided without buckets.')
+ if labels is not None and buckets is not None:
+ raise ValueError('Can only specify at most one of buckets and labels.')
+ if labels is None:
+ labels = []
+ self._labelnames = tuple(labels)
+ if buckets is not None:
+ self.add_metric([], buckets, sum_value)
+
+ def add_metric(self,
+ labels: Sequence[str],
+ buckets: Sequence[Union[Tuple[str, float], Tuple[str, float, Exemplar]]],
+ sum_value: Optional[float],
+ timestamp: Optional[Union[Timestamp, float]] = None) -> None:
+ """Add a metric to the metric family.
+
+ Args:
+ labels: A list of label values
+ buckets: A list of lists.
+ Each inner list can be a pair of bucket name and value,
+ or a triple of bucket name, value, and exemplar.
+ The buckets must be sorted, and +Inf present.
+ sum_value: The sum value of the metric.
+ """
+ for b in buckets:
+ bucket, value = b[:2]
+ exemplar = None
+ if len(b) == 3:
+ exemplar = b[2] # type: ignore
+ self.samples.append(Sample(
+ self.name + '_bucket',
+ dict(list(zip(self._labelnames, labels)) + [('le', bucket)]),
+ value,
+ timestamp,
+ exemplar,
+ ))
+ # Don't include sum and thus count if there's negative buckets.
+ if float(buckets[0][0]) >= 0 and sum_value is not None:
+ # +Inf is last and provides the count value.
+ self.samples.append(
+ Sample(self.name + '_count', dict(zip(self._labelnames, labels)), buckets[-1][1], timestamp))
+ self.samples.append(
+ Sample(self.name + '_sum', dict(zip(self._labelnames, labels)), sum_value, timestamp))
+
+
+
+class GaugeHistogramMetricFamily(Metric):
+ """A single gauge histogram and its samples.
+
+ For use by custom collectors.
+ """
+
+ def __init__(self,
+ name: str,
+ documentation: str,
+ buckets: Optional[Sequence[Tuple[str, float]]] = None,
+ gsum_value: Optional[float] = None,
+ labels: Optional[Sequence[str]] = None,
+ unit: str = '',
+ ):
+ Metric.__init__(self, name, documentation, 'gaugehistogram', unit)
+ if labels is not None and buckets is not None:
+ raise ValueError('Can only specify at most one of buckets and labels.')
+ if labels is None:
+ labels = []
+ self._labelnames = tuple(labels)
+ if buckets is not None:
+ self.add_metric([], buckets, gsum_value)
+
+ def add_metric(self,
+ labels: Sequence[str],
+ buckets: Sequence[Tuple[str, float]],
+ gsum_value: Optional[float],
+ timestamp: Optional[Union[float, Timestamp]] = None,
+ ) -> None:
+ """Add a metric to the metric family.
+
+ Args:
+ labels: A list of label values
+ buckets: A list of pairs of bucket names and values.
+ The buckets must be sorted, and +Inf present.
+ gsum_value: The sum value of the metric.
+ """
+ for bucket, value in buckets:
+ self.samples.append(Sample(
+ self.name + '_bucket',
+ dict(list(zip(self._labelnames, labels)) + [('le', bucket)]),
+ value, timestamp))
+ # +Inf is last and provides the count value.
+ self.samples.extend([
+ Sample(self.name + '_gcount', dict(zip(self._labelnames, labels)), buckets[-1][1], timestamp),
+ # TODO: Handle None gsum_value correctly. Currently a None will fail exposition but is allowed here.
+ Sample(self.name + '_gsum', dict(zip(self._labelnames, labels)), gsum_value, timestamp), # type: ignore
+ ])
+
+
+class InfoMetricFamily(Metric):
+ """A single info and its samples.
+
+ For use by custom collectors.
+ """
+
+ def __init__(self,
+ name: str,
+ documentation: str,
+ value: Optional[Dict[str, str]] = None,
+ labels: Optional[Sequence[str]] = None,
+ ):
+ Metric.__init__(self, name, documentation, 'info')
+ if labels is not None and value is not None:
+ raise ValueError('Can only specify at most one of value and labels.')
+ if labels is None:
+ labels = []
+ self._labelnames = tuple(labels)
+ if value is not None:
+ self.add_metric([], value)
+
+ def add_metric(self,
+ labels: Sequence[str],
+ value: Dict[str, str],
+ timestamp: Optional[Union[Timestamp, float]] = None,
+ ) -> None:
+ """Add a metric to the metric family.
+
+ Args:
+ labels: A list of label values
+ value: A dict of labels
+ """
+ self.samples.append(Sample(
+ self.name + '_info',
+ dict(dict(zip(self._labelnames, labels)), **value),
+ 1,
+ timestamp,
+ ))
+
+
+class StateSetMetricFamily(Metric):
+ """A single stateset and its samples.
+
+ For use by custom collectors.
+ """
+
+ def __init__(self,
+ name: str,
+ documentation: str,
+ value: Optional[Dict[str, bool]] = None,
+ labels: Optional[Sequence[str]] = None,
+ ):
+ Metric.__init__(self, name, documentation, 'stateset')
+ if labels is not None and value is not None:
+ raise ValueError('Can only specify at most one of value and labels.')
+ if labels is None:
+ labels = []
+ self._labelnames = tuple(labels)
+ if value is not None:
+ self.add_metric([], value)
+
+ def add_metric(self,
+ labels: Sequence[str],
+ value: Dict[str, bool],
+ timestamp: Optional[Union[Timestamp, float]] = None,
+ ) -> None:
+ """Add a metric to the metric family.
+
+ Args:
+ labels: A list of label values
+ value: A dict of string state names to booleans
+ """
+ labels = tuple(labels)
+ for state, enabled in sorted(value.items()):
+ v = (1 if enabled else 0)
+ self.samples.append(Sample(
+ self.name,
+ dict(zip(self._labelnames + (self.name,), labels + (state,))),
+ v,
+ timestamp,
+ ))
diff --git a/.venv/lib/python3.12/site-packages/prometheus_client/mmap_dict.py b/.venv/lib/python3.12/site-packages/prometheus_client/mmap_dict.py
new file mode 100644
index 00000000..edd895cd
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/prometheus_client/mmap_dict.py
@@ -0,0 +1,145 @@
+import json
+import mmap
+import os
+import struct
+from typing import List
+
+_INITIAL_MMAP_SIZE = 1 << 16
+_pack_integer_func = struct.Struct(b'i').pack
+_pack_two_doubles_func = struct.Struct(b'dd').pack
+_unpack_integer = struct.Struct(b'i').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_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, timestamp, pos). No locking is performed."""
+
+ if used <= 0:
+ # If not valid `used` value is passed in, read it from the file.
+ used = _unpack_integer(data, 0)[0]
+
+ pos = 8
+
+ while pos < used:
+ encoded_len = _unpack_integer(data, pos)[0]
+ # check we are not reading beyond bounds
+ if encoded_len + pos > used:
+ raise RuntimeError('Read beyond file size detected, file is corrupted.')
+ pos += 4
+ encoded_key = data[pos:pos + encoded_len]
+ padded_len = encoded_len + (8 - (encoded_len + 4) % 8)
+ pos += padded_len
+ value, timestamp = _unpack_two_doubles(data, pos)
+ yield encoded_key.decode('utf-8'), value, timestamp, pos
+ pos += 16
+
+
+class MmapedDict:
+ """A dict of doubles, backed by an mmapped file.
+
+ The file starts with a 4 byte int, indicating how much of it is used.
+ 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 and a 8 byte float
+ which is a UNIX timestamp in seconds.
+
+ Not thread safe.
+ """
+
+ def __init__(self, filename, read_mode=False):
+ self._f = open(filename, 'rb' if read_mode else 'a+b')
+ self._fname = filename
+ capacity = os.fstat(self._f.fileno()).st_size
+ if capacity == 0:
+ self._f.truncate(_INITIAL_MMAP_SIZE)
+ capacity = _INITIAL_MMAP_SIZE
+ self._capacity = capacity
+ self._m = mmap.mmap(self._f.fileno(), self._capacity,
+ access=mmap.ACCESS_READ if read_mode else mmap.ACCESS_WRITE)
+
+ self._positions = {}
+ self._used = _unpack_integer(self._m, 0)[0]
+ if self._used == 0:
+ self._used = 8
+ _pack_integer(self._m, 0, self._used)
+ else:
+ if not read_mode:
+ for key, _, _, pos in self._read_all_values():
+ self._positions[key] = pos
+
+ @staticmethod
+ def read_all_values_from_file(filename):
+ with open(filename, 'rb') as infp:
+ # Read the first block of data, including the first 4 bytes which tell us
+ # how much of the file (which is preallocated to _INITIAL_MMAP_SIZE bytes) is occupied.
+ data = infp.read(mmap.PAGESIZE)
+ used = _unpack_integer(data, 0)[0]
+ if used > len(data): # Then read in the rest, if needed.
+ data += infp.read(used - len(data))
+ return _read_all_values(data, used)
+
+ def _init_value(self, key):
+ """Initialize a value. Lock must be held by caller."""
+ 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)}sdd'.encode(), len(encoded), padded, 0.0, 0.0)
+ while self._used + len(value) > self._capacity:
+ self._capacity *= 2
+ self._f.truncate(self._capacity)
+ self._m = mmap.mmap(self._f.fileno(), self._capacity)
+ self._m[self._used:self._used + len(value)] = value
+
+ # Update how much space we've used.
+ self._used += len(value)
+ _pack_integer(self._m, 0, self._used)
+ 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, 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]
+ return _unpack_two_doubles(self._m, pos)
+
+ def write_value(self, key, value, timestamp):
+ if key not in self._positions:
+ self._init_value(key)
+ pos = self._positions[key]
+ _pack_two_doubles(self._m, pos, value, timestamp)
+
+ def close(self):
+ if self._f:
+ self._m.close()
+ self._m = None
+ self._f.close()
+ self._f = None
+
+
+def mmap_key(metric_name: str, name: str, labelnames: List[str], labelvalues: List[str], help_text: str) -> str:
+ """Format a key for use in the mmap file."""
+ # ensure labels are in consistent order for identity
+ labels = dict(zip(labelnames, labelvalues))
+ return json.dumps([metric_name, name, labels, help_text], sort_keys=True)
diff --git a/.venv/lib/python3.12/site-packages/prometheus_client/multiprocess.py b/.venv/lib/python3.12/site-packages/prometheus_client/multiprocess.py
new file mode 100644
index 00000000..7021b49a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/prometheus_client/multiprocess.py
@@ -0,0 +1,170 @@
+from collections import defaultdict
+import glob
+import json
+import os
+import warnings
+
+from .metrics import Gauge
+from .metrics_core import Metric
+from .mmap_dict import MmapedDict
+from .samples import Sample
+from .utils import floatToGoString
+
+try: # Python3
+ FileNotFoundError
+except NameError: # Python >= 2.5
+ FileNotFoundError = IOError
+
+
+class MultiProcessCollector:
+ """Collector for files for multi-process mode."""
+
+ def __init__(self, registry, path=None):
+ if path is None:
+ # This deprecation warning can go away in a few releases when removing the compatibility
+ if 'prometheus_multiproc_dir' in os.environ and 'PROMETHEUS_MULTIPROC_DIR' not in os.environ:
+ os.environ['PROMETHEUS_MULTIPROC_DIR'] = os.environ['prometheus_multiproc_dir']
+ warnings.warn("prometheus_multiproc_dir variable has been deprecated in favor of the upper case naming PROMETHEUS_MULTIPROC_DIR", DeprecationWarning)
+ path = os.environ.get('PROMETHEUS_MULTIPROC_DIR')
+ if not path or not os.path.isdir(path):
+ raise ValueError('env PROMETHEUS_MULTIPROC_DIR is not set or not a directory')
+ self._path = path
+ if registry:
+ registry.register(self)
+
+ @staticmethod
+ def merge(files, accumulate=True):
+ """Merge metrics from given mmap files.
+
+ By default, histograms are accumulated, as per prometheus wire format.
+ But if writing the merged data back to mmap files, use
+ accumulate=False to avoid compound accumulation.
+ """
+ metrics = MultiProcessCollector._read_metrics(files)
+ return MultiProcessCollector._accumulate_metrics(metrics, accumulate)
+
+ @staticmethod
+ def _read_metrics(files):
+ metrics = {}
+ key_cache = {}
+
+ def _parse_key(key):
+ val = key_cache.get(key)
+ if not val:
+ metric_name, name, labels, help_text = json.loads(key)
+ labels_key = tuple(sorted(labels.items()))
+ val = key_cache[key] = (metric_name, name, labels, labels_key, help_text)
+ return val
+
+ for f in files:
+ parts = os.path.basename(f).split('_')
+ typ = parts[0]
+ try:
+ file_values = MmapedDict.read_all_values_from_file(f)
+ except FileNotFoundError:
+ if typ == 'gauge' and parts[1].startswith('live'):
+ # Files for 'live*' gauges can be deleted between the glob of collect
+ # and now (via a mark_process_dead call) so don't fail if
+ # the file is missing
+ continue
+ raise
+ for key, value, timestamp, _ in file_values:
+ metric_name, name, labels, labels_key, help_text = _parse_key(key)
+
+ metric = metrics.get(metric_name)
+ if metric is None:
+ metric = Metric(metric_name, help_text, typ)
+ metrics[metric_name] = metric
+
+ if typ == 'gauge':
+ pid = parts[2][:-3]
+ metric._multiprocess_mode = parts[1]
+ metric.add_sample(name, labels_key + (('pid', pid),), value, timestamp)
+ else:
+ # The duplicates and labels are fixed in the next for.
+ metric.add_sample(name, labels_key, value)
+ return metrics
+
+ @staticmethod
+ def _accumulate_metrics(metrics, accumulate):
+ for metric in metrics.values():
+ samples = defaultdict(float)
+ sample_timestamps = defaultdict(float)
+ buckets = defaultdict(lambda: defaultdict(float))
+ samples_setdefault = samples.setdefault
+ for s in metric.samples:
+ name, labels, value, timestamp, exemplar = s
+ if metric.type == 'gauge':
+ without_pid_key = (name, tuple(l for l in labels if l[0] != 'pid'))
+ if metric._multiprocess_mode in ('min', 'livemin'):
+ current = samples_setdefault(without_pid_key, value)
+ if value < current:
+ samples[without_pid_key] = value
+ elif metric._multiprocess_mode in ('max', 'livemax'):
+ current = samples_setdefault(without_pid_key, value)
+ if value > current:
+ samples[without_pid_key] = value
+ elif metric._multiprocess_mode in ('sum', 'livesum'):
+ samples[without_pid_key] += value
+ elif metric._multiprocess_mode in ('mostrecent', 'livemostrecent'):
+ current_timestamp = sample_timestamps[without_pid_key]
+ timestamp = float(timestamp or 0)
+ if current_timestamp < timestamp:
+ samples[without_pid_key] = value
+ sample_timestamps[without_pid_key] = timestamp
+ else: # all/liveall
+ samples[(name, labels)] = value
+
+ elif metric.type == 'histogram':
+ # A for loop with early exit is faster than a genexpr
+ # or a listcomp that ends up building unnecessary things
+ for l in labels:
+ if l[0] == 'le':
+ bucket_value = float(l[1])
+ # _bucket
+ without_le = tuple(l for l in labels if l[0] != 'le')
+ buckets[without_le][bucket_value] += value
+ break
+ else: # did not find the `le` key
+ # _sum/_count
+ samples[(name, labels)] += value
+ else:
+ # Counter and Summary.
+ samples[(name, labels)] += value
+
+ # Accumulate bucket values.
+ if metric.type == 'histogram':
+ for labels, values in buckets.items():
+ acc = 0.0
+ for bucket, value in sorted(values.items()):
+ sample_key = (
+ metric.name + '_bucket',
+ labels + (('le', floatToGoString(bucket)),),
+ )
+ if accumulate:
+ acc += value
+ samples[sample_key] = acc
+ else:
+ samples[sample_key] = value
+ if accumulate:
+ samples[(metric.name + '_count', labels)] = acc
+
+ # Convert to correct sample format.
+ metric.samples = [Sample(name_, dict(labels), value) for (name_, labels), value in samples.items()]
+ return metrics.values()
+
+ def collect(self):
+ files = glob.glob(os.path.join(self._path, '*.db'))
+ return self.merge(files, accumulate=True)
+
+
+_LIVE_GAUGE_MULTIPROCESS_MODES = {m for m in Gauge._MULTIPROC_MODES if m.startswith('live')}
+
+
+def mark_process_dead(pid, path=None):
+ """Do bookkeeping for when one process dies in a multi-process setup."""
+ if path is None:
+ path = os.environ.get('PROMETHEUS_MULTIPROC_DIR', os.environ.get('prometheus_multiproc_dir'))
+ for mode in _LIVE_GAUGE_MULTIPROCESS_MODES:
+ for f in glob.glob(os.path.join(path, f'gauge_{mode}_{pid}.db')):
+ os.remove(f)
diff --git a/.venv/lib/python3.12/site-packages/prometheus_client/openmetrics/__init__.py b/.venv/lib/python3.12/site-packages/prometheus_client/openmetrics/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/prometheus_client/openmetrics/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/prometheus_client/openmetrics/exposition.py b/.venv/lib/python3.12/site-packages/prometheus_client/openmetrics/exposition.py
new file mode 100644
index 00000000..26f3109f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/prometheus_client/openmetrics/exposition.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python
+
+
+from ..utils import floatToGoString
+
+CONTENT_TYPE_LATEST = 'application/openmetrics-text; version=1.0.0; charset=utf-8'
+"""Content type of the latest OpenMetrics text format"""
+
+
+def _is_valid_exemplar_metric(metric, sample):
+ if metric.type == 'counter' and sample.name.endswith('_total'):
+ return True
+ if metric.type in ('histogram', 'gaugehistogram') and sample.name.endswith('_bucket'):
+ return True
+ return False
+
+
+def generate_latest(registry):
+ '''Returns the metrics from the registry in latest text format as a string.'''
+ output = []
+ for metric in registry.collect():
+ try:
+ mname = metric.name
+ output.append('# HELP {} {}\n'.format(
+ mname, metric.documentation.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"')))
+ output.append(f'# TYPE {mname} {metric.type}\n')
+ if metric.unit:
+ output.append(f'# UNIT {mname} {metric.unit}\n')
+ for s in metric.samples:
+ if s.labels:
+ labelstr = '{{{0}}}'.format(','.join(
+ ['{}="{}"'.format(
+ k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"'))
+ for k, v in sorted(s.labels.items())]))
+ else:
+ labelstr = ''
+ if s.exemplar:
+ if not _is_valid_exemplar_metric(metric, s):
+ raise ValueError(f"Metric {metric.name} has exemplars, but is not a histogram bucket or counter")
+ labels = '{{{0}}}'.format(','.join(
+ ['{}="{}"'.format(
+ k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"'))
+ for k, v in sorted(s.exemplar.labels.items())]))
+ if s.exemplar.timestamp is not None:
+ exemplarstr = ' # {} {} {}'.format(
+ labels,
+ floatToGoString(s.exemplar.value),
+ s.exemplar.timestamp,
+ )
+ else:
+ exemplarstr = ' # {} {}'.format(
+ labels,
+ floatToGoString(s.exemplar.value),
+ )
+ else:
+ exemplarstr = ''
+ timestamp = ''
+ if s.timestamp is not None:
+ timestamp = f' {s.timestamp}'
+ output.append('{}{} {}{}{}\n'.format(
+ s.name,
+ labelstr,
+ floatToGoString(s.value),
+ timestamp,
+ exemplarstr,
+ ))
+ except Exception as exception:
+ exception.args = (exception.args or ('',)) + (metric,)
+ raise
+
+ output.append('# EOF\n')
+ return ''.join(output).encode('utf-8')
diff --git a/.venv/lib/python3.12/site-packages/prometheus_client/openmetrics/parser.py b/.venv/lib/python3.12/site-packages/prometheus_client/openmetrics/parser.py
new file mode 100644
index 00000000..6128a0d3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/prometheus_client/openmetrics/parser.py
@@ -0,0 +1,614 @@
+#!/usr/bin/env python
+
+
+import io as StringIO
+import math
+import re
+
+from ..metrics_core import Metric, METRIC_LABEL_NAME_RE
+from ..samples import Exemplar, Sample, Timestamp
+from ..utils import floatToGoString
+
+
+def text_string_to_metric_families(text):
+ """Parse Openmetrics text format from a unicode string.
+
+ See text_fd_to_metric_families.
+ """
+ yield from text_fd_to_metric_families(StringIO.StringIO(text))
+
+
+_CANONICAL_NUMBERS = {float("inf")}
+
+
+def _isUncanonicalNumber(s):
+ f = float(s)
+ if f not in _CANONICAL_NUMBERS:
+ return False # Only the canonical numbers are required to be canonical.
+ return s != floatToGoString(f)
+
+
+ESCAPE_SEQUENCES = {
+ '\\\\': '\\',
+ '\\n': '\n',
+ '\\"': '"',
+}
+
+
+def _replace_escape_sequence(match):
+ return ESCAPE_SEQUENCES[match.group(0)]
+
+
+ESCAPING_RE = re.compile(r'\\[\\n"]')
+
+
+def _replace_escaping(s):
+ return ESCAPING_RE.sub(_replace_escape_sequence, s)
+
+
+def _unescape_help(text):
+ result = []
+ slash = False
+
+ for char in text:
+ if slash:
+ if char == '\\':
+ result.append('\\')
+ elif char == '"':
+ result.append('"')
+ elif char == 'n':
+ result.append('\n')
+ else:
+ result.append('\\' + char)
+ slash = False
+ else:
+ if char == '\\':
+ slash = True
+ else:
+ result.append(char)
+
+ if slash:
+ result.append('\\')
+
+ return ''.join(result)
+
+
+def _parse_value(value):
+ value = ''.join(value)
+ if value != value.strip() or '_' in value:
+ raise ValueError(f"Invalid value: {value!r}")
+ try:
+ return int(value)
+ except ValueError:
+ return float(value)
+
+
+def _parse_timestamp(timestamp):
+ timestamp = ''.join(timestamp)
+ if not timestamp:
+ return None
+ if timestamp != timestamp.strip() or '_' in timestamp:
+ raise ValueError(f"Invalid timestamp: {timestamp!r}")
+ try:
+ # Simple int.
+ return Timestamp(int(timestamp), 0)
+ except ValueError:
+ try:
+ # aaaa.bbbb. Nanosecond resolution supported.
+ parts = timestamp.split('.', 1)
+ return Timestamp(int(parts[0]), int(parts[1][:9].ljust(9, "0")))
+ except ValueError:
+ # Float.
+ ts = float(timestamp)
+ if math.isnan(ts) or math.isinf(ts):
+ raise ValueError(f"Invalid timestamp: {timestamp!r}")
+ return ts
+
+
+def _is_character_escaped(s, charpos):
+ num_bslashes = 0
+ while (charpos > num_bslashes
+ and s[charpos - 1 - num_bslashes] == '\\'):
+ num_bslashes += 1
+ return num_bslashes % 2 == 1
+
+
+def _parse_labels_with_state_machine(text):
+ # The { has already been parsed.
+ state = 'startoflabelname'
+ labelname = []
+ labelvalue = []
+ labels = {}
+ labels_len = 0
+
+ for char in text:
+ if state == 'startoflabelname':
+ if char == '}':
+ state = 'endoflabels'
+ else:
+ state = 'labelname'
+ labelname.append(char)
+ elif state == 'labelname':
+ if char == '=':
+ state = 'labelvaluequote'
+ else:
+ labelname.append(char)
+ elif state == 'labelvaluequote':
+ if char == '"':
+ state = 'labelvalue'
+ else:
+ raise ValueError("Invalid line: " + text)
+ elif state == 'labelvalue':
+ if char == '\\':
+ state = 'labelvalueslash'
+ elif char == '"':
+ ln = ''.join(labelname)
+ if not METRIC_LABEL_NAME_RE.match(ln):
+ raise ValueError("Invalid line, bad label name: " + text)
+ if ln in labels:
+ raise ValueError("Invalid line, duplicate label name: " + text)
+ labels[ln] = ''.join(labelvalue)
+ labelname = []
+ labelvalue = []
+ state = 'endoflabelvalue'
+ else:
+ labelvalue.append(char)
+ elif state == 'endoflabelvalue':
+ if char == ',':
+ state = 'labelname'
+ elif char == '}':
+ state = 'endoflabels'
+ else:
+ raise ValueError("Invalid line: " + text)
+ elif state == 'labelvalueslash':
+ state = 'labelvalue'
+ if char == '\\':
+ labelvalue.append('\\')
+ elif char == 'n':
+ labelvalue.append('\n')
+ elif char == '"':
+ labelvalue.append('"')
+ else:
+ labelvalue.append('\\' + char)
+ elif state == 'endoflabels':
+ if char == ' ':
+ break
+ else:
+ raise ValueError("Invalid line: " + text)
+ labels_len += 1
+ return labels, labels_len
+
+
+def _parse_labels(text):
+ labels = {}
+
+ # Raise error if we don't have valid labels
+ if text and "=" not in text:
+ raise ValueError
+
+ # Copy original labels
+ sub_labels = text
+ try:
+ # Process one label at a time
+ while sub_labels:
+ # The label name is before the equal
+ value_start = sub_labels.index("=")
+ label_name = sub_labels[:value_start]
+ sub_labels = sub_labels[value_start + 1:]
+
+ # Check for missing quotes
+ if not sub_labels or sub_labels[0] != '"':
+ raise ValueError
+
+ # The first quote is guaranteed to be after the equal
+ value_substr = sub_labels[1:]
+
+ # Check for extra commas
+ if not label_name or label_name[0] == ',':
+ raise ValueError
+ if not value_substr or value_substr[-1] == ',':
+ raise ValueError
+
+ # Find the last unescaped quote
+ i = 0
+ while i < len(value_substr):
+ i = value_substr.index('"', i)
+ if not _is_character_escaped(value_substr[:i], i):
+ break
+ i += 1
+
+ # The label value is between the first and last quote
+ quote_end = i + 1
+ label_value = sub_labels[1:quote_end]
+ # Replace escaping if needed
+ if "\\" in label_value:
+ label_value = _replace_escaping(label_value)
+ if not METRIC_LABEL_NAME_RE.match(label_name):
+ raise ValueError("invalid line, bad label name: " + text)
+ if label_name in labels:
+ raise ValueError("invalid line, duplicate label name: " + text)
+ labels[label_name] = label_value
+
+ # Remove the processed label from the sub-slice for next iteration
+ sub_labels = sub_labels[quote_end + 1:]
+ if sub_labels.startswith(","):
+ next_comma = 1
+ else:
+ next_comma = 0
+ sub_labels = sub_labels[next_comma:]
+
+ # Check for missing commas
+ if sub_labels and next_comma == 0:
+ raise ValueError
+
+ return labels
+
+ except ValueError:
+ raise ValueError("Invalid labels: " + text)
+
+
+def _parse_sample(text):
+ separator = " # "
+ # Detect the labels in the text
+ label_start = text.find("{")
+ if label_start == -1 or separator in text[:label_start]:
+ # We don't have labels, but there could be an exemplar.
+ name_end = text.index(" ")
+ name = text[:name_end]
+ # Parse the remaining text after the name
+ remaining_text = text[name_end + 1:]
+ value, timestamp, exemplar = _parse_remaining_text(remaining_text)
+ return Sample(name, {}, value, timestamp, exemplar)
+ # The name is before the labels
+ name = text[:label_start]
+ if separator not in text:
+ # Line doesn't contain an exemplar
+ # We can use `rindex` to find `label_end`
+ label_end = text.rindex("}")
+ label = text[label_start + 1:label_end]
+ labels = _parse_labels(label)
+ else:
+ # Line potentially contains an exemplar
+ # Fallback to parsing labels with a state machine
+ labels, labels_len = _parse_labels_with_state_machine(text[label_start + 1:])
+ label_end = labels_len + len(name)
+ # Parsing labels succeeded, continue parsing the remaining text
+ remaining_text = text[label_end + 2:]
+ value, timestamp, exemplar = _parse_remaining_text(remaining_text)
+ return Sample(name, labels, value, timestamp, exemplar)
+
+
+def _parse_remaining_text(text):
+ split_text = text.split(" ", 1)
+ val = _parse_value(split_text[0])
+ if len(split_text) == 1:
+ # We don't have timestamp or exemplar
+ return val, None, None
+
+ timestamp = []
+ exemplar_value = []
+ exemplar_timestamp = []
+ exemplar_labels = None
+
+ state = 'timestamp'
+ text = split_text[1]
+
+ it = iter(text)
+ for char in it:
+ if state == 'timestamp':
+ if char == '#' and not timestamp:
+ state = 'exemplarspace'
+ elif char == ' ':
+ state = 'exemplarhash'
+ else:
+ timestamp.append(char)
+ elif state == 'exemplarhash':
+ if char == '#':
+ state = 'exemplarspace'
+ else:
+ raise ValueError("Invalid line: " + text)
+ elif state == 'exemplarspace':
+ if char == ' ':
+ state = 'exemplarstartoflabels'
+ else:
+ raise ValueError("Invalid line: " + text)
+ elif state == 'exemplarstartoflabels':
+ if char == '{':
+ label_start, label_end = text.index("{"), text.rindex("}")
+ exemplar_labels = _parse_labels(text[label_start + 1:label_end])
+ state = 'exemplarparsedlabels'
+ else:
+ raise ValueError("Invalid line: " + text)
+ elif state == 'exemplarparsedlabels':
+ if char == '}':
+ state = 'exemplarvaluespace'
+ elif state == 'exemplarvaluespace':
+ if char == ' ':
+ state = 'exemplarvalue'
+ else:
+ raise ValueError("Invalid line: " + text)
+ elif state == 'exemplarvalue':
+ if char == ' ' and not exemplar_value:
+ raise ValueError("Invalid line: " + text)
+ elif char == ' ':
+ state = 'exemplartimestamp'
+ else:
+ exemplar_value.append(char)
+ elif state == 'exemplartimestamp':
+ exemplar_timestamp.append(char)
+
+ # Trailing space after value.
+ if state == 'timestamp' and not timestamp:
+ raise ValueError("Invalid line: " + text)
+
+ # Trailing space after value.
+ if state == 'exemplartimestamp' and not exemplar_timestamp:
+ raise ValueError("Invalid line: " + text)
+
+ # Incomplete exemplar.
+ if state in ['exemplarhash', 'exemplarspace', 'exemplarstartoflabels', 'exemplarparsedlabels']:
+ raise ValueError("Invalid line: " + text)
+
+ ts = _parse_timestamp(timestamp)
+ exemplar = None
+ if exemplar_labels is not None:
+ exemplar_length = sum(len(k) + len(v) for k, v in exemplar_labels.items())
+ if exemplar_length > 128:
+ raise ValueError("Exemplar labels are too long: " + text)
+ exemplar = Exemplar(
+ exemplar_labels,
+ _parse_value(exemplar_value),
+ _parse_timestamp(exemplar_timestamp),
+ )
+
+ return val, ts, exemplar
+
+
+def _group_for_sample(sample, name, typ):
+ if typ == 'info':
+ # We can't distinguish between groups for info metrics.
+ return {}
+ if typ == 'summary' and sample.name == name:
+ d = sample.labels.copy()
+ del d['quantile']
+ return d
+ if typ == 'stateset':
+ d = sample.labels.copy()
+ del d[name]
+ return d
+ if typ in ['histogram', 'gaugehistogram'] and sample.name == name + '_bucket':
+ d = sample.labels.copy()
+ del d['le']
+ return d
+ return sample.labels
+
+
+def _check_histogram(samples, name):
+ group = None
+ timestamp = None
+
+ def do_checks():
+ if bucket != float('+Inf'):
+ raise ValueError("+Inf bucket missing: " + name)
+ if count is not None and value != count:
+ raise ValueError("Count does not match +Inf value: " + name)
+ if has_sum and count is None:
+ raise ValueError("_count must be present if _sum is present: " + name)
+ if has_gsum and count is None:
+ raise ValueError("_gcount must be present if _gsum is present: " + name)
+ if not (has_sum or has_gsum) and count is not None:
+ raise ValueError("_sum/_gsum must be present if _count is present: " + name)
+ if has_negative_buckets and has_sum:
+ raise ValueError("Cannot have _sum with negative buckets: " + name)
+ if not has_negative_buckets and has_negative_gsum:
+ raise ValueError("Cannot have negative _gsum with non-negative buckets: " + name)
+
+ for s in samples:
+ suffix = s.name[len(name):]
+ g = _group_for_sample(s, name, 'histogram')
+ if g != group or s.timestamp != timestamp:
+ if group is not None:
+ do_checks()
+ count = None
+ bucket = None
+ has_negative_buckets = False
+ has_sum = False
+ has_gsum = False
+ has_negative_gsum = False
+ value = 0
+ group = g
+ timestamp = s.timestamp
+
+ if suffix == '_bucket':
+ b = float(s.labels['le'])
+ if b < 0:
+ has_negative_buckets = True
+ if bucket is not None and b <= bucket:
+ raise ValueError("Buckets out of order: " + name)
+ if s.value < value:
+ raise ValueError("Bucket values out of order: " + name)
+ bucket = b
+ value = s.value
+ elif suffix in ['_count', '_gcount']:
+ count = s.value
+ elif suffix in ['_sum']:
+ has_sum = True
+ elif suffix in ['_gsum']:
+ has_gsum = True
+ if s.value < 0:
+ has_negative_gsum = True
+
+ if group is not None:
+ do_checks()
+
+
+def text_fd_to_metric_families(fd):
+ """Parse Prometheus text format from a file descriptor.
+
+ This is a laxer parser than the main Go parser,
+ so successful parsing does not imply that the parsed
+ text meets the specification.
+
+ Yields Metric's.
+ """
+ name = None
+ allowed_names = []
+ eof = False
+
+ seen_names = set()
+ type_suffixes = {
+ 'counter': ['_total', '_created'],
+ 'summary': ['', '_count', '_sum', '_created'],
+ 'histogram': ['_count', '_sum', '_bucket', '_created'],
+ 'gaugehistogram': ['_gcount', '_gsum', '_bucket'],
+ 'info': ['_info'],
+ }
+
+ def build_metric(name, documentation, typ, unit, samples):
+ if typ is None:
+ typ = 'unknown'
+ for suffix in set(type_suffixes.get(typ, []) + [""]):
+ if name + suffix in seen_names:
+ raise ValueError("Clashing name: " + name + suffix)
+ seen_names.add(name + suffix)
+ if documentation is None:
+ documentation = ''
+ if unit is None:
+ unit = ''
+ if unit and not name.endswith("_" + unit):
+ raise ValueError("Unit does not match metric name: " + name)
+ if unit and typ in ['info', 'stateset']:
+ raise ValueError("Units not allowed for this metric type: " + name)
+ if typ in ['histogram', 'gaugehistogram']:
+ _check_histogram(samples, name)
+ metric = Metric(name, documentation, typ, unit)
+ # TODO: check labelvalues are valid utf8
+ metric.samples = samples
+ return metric
+
+ for line in fd:
+ if line[-1] == '\n':
+ line = line[:-1]
+
+ if eof:
+ raise ValueError("Received line after # EOF: " + line)
+
+ if not line:
+ raise ValueError("Received blank line")
+
+ if line == '# EOF':
+ eof = True
+ elif line.startswith('#'):
+ parts = line.split(' ', 3)
+ if len(parts) < 4:
+ raise ValueError("Invalid line: " + line)
+ if parts[2] == name and samples:
+ raise ValueError("Received metadata after samples: " + line)
+ if parts[2] != name:
+ if name is not None:
+ yield build_metric(name, documentation, typ, unit, samples)
+ # New metric
+ name = parts[2]
+ unit = None
+ typ = None
+ documentation = None
+ group = None
+ seen_groups = set()
+ group_timestamp = None
+ group_timestamp_samples = set()
+ samples = []
+ allowed_names = [parts[2]]
+
+ if parts[1] == 'HELP':
+ if documentation is not None:
+ raise ValueError("More than one HELP for metric: " + line)
+ documentation = _unescape_help(parts[3])
+ elif parts[1] == 'TYPE':
+ if typ is not None:
+ raise ValueError("More than one TYPE for metric: " + line)
+ typ = parts[3]
+ if typ == 'untyped':
+ raise ValueError("Invalid TYPE for metric: " + line)
+ allowed_names = [name + n for n in type_suffixes.get(typ, [''])]
+ elif parts[1] == 'UNIT':
+ if unit is not None:
+ raise ValueError("More than one UNIT for metric: " + line)
+ unit = parts[3]
+ else:
+ raise ValueError("Invalid line: " + line)
+ else:
+ sample = _parse_sample(line)
+ if sample.name not in allowed_names:
+ if name is not None:
+ yield build_metric(name, documentation, typ, unit, samples)
+ # Start an unknown metric.
+ name = sample.name
+ documentation = None
+ unit = None
+ typ = 'unknown'
+ samples = []
+ group = None
+ group_timestamp = None
+ group_timestamp_samples = set()
+ seen_groups = set()
+ allowed_names = [sample.name]
+
+ if typ == 'stateset' and name not in sample.labels:
+ raise ValueError("Stateset missing label: " + line)
+ if (name + '_bucket' == sample.name
+ and (sample.labels.get('le', "NaN") == "NaN"
+ or _isUncanonicalNumber(sample.labels['le']))):
+ raise ValueError("Invalid le label: " + line)
+ if (name + '_bucket' == sample.name
+ and (not isinstance(sample.value, int) and not sample.value.is_integer())):
+ raise ValueError("Bucket value must be an integer: " + line)
+ if ((name + '_count' == sample.name or name + '_gcount' == sample.name)
+ and (not isinstance(sample.value, int) and not sample.value.is_integer())):
+ raise ValueError("Count value must be an integer: " + line)
+ if (typ == 'summary' and name == sample.name
+ and (not (0 <= float(sample.labels.get('quantile', -1)) <= 1)
+ or _isUncanonicalNumber(sample.labels['quantile']))):
+ raise ValueError("Invalid quantile label: " + line)
+
+ g = tuple(sorted(_group_for_sample(sample, name, typ).items()))
+ if group is not None and g != group and g in seen_groups:
+ raise ValueError("Invalid metric grouping: " + line)
+ if group is not None and g == group:
+ if (sample.timestamp is None) != (group_timestamp is None):
+ raise ValueError("Mix of timestamp presence within a group: " + line)
+ if group_timestamp is not None and group_timestamp > sample.timestamp and typ != 'info':
+ raise ValueError("Timestamps went backwards within a group: " + line)
+ else:
+ group_timestamp_samples = set()
+
+ series_id = (sample.name, tuple(sorted(sample.labels.items())))
+ if sample.timestamp != group_timestamp or series_id not in group_timestamp_samples:
+ # Not a duplicate due to timestamp truncation.
+ samples.append(sample)
+ group_timestamp_samples.add(series_id)
+
+ group = g
+ group_timestamp = sample.timestamp
+ seen_groups.add(g)
+
+ if typ == 'stateset' and sample.value not in [0, 1]:
+ raise ValueError("Stateset samples can only have values zero and one: " + line)
+ if typ == 'info' and sample.value != 1:
+ raise ValueError("Info samples can only have value one: " + line)
+ if typ == 'summary' and name == sample.name and sample.value < 0:
+ raise ValueError("Quantile values cannot be negative: " + line)
+ if sample.name[len(name):] in ['_total', '_sum', '_count', '_bucket', '_gcount', '_gsum'] and math.isnan(
+ sample.value):
+ raise ValueError("Counter-like samples cannot be NaN: " + line)
+ if sample.name[len(name):] in ['_total', '_sum', '_count', '_bucket', '_gcount'] and sample.value < 0:
+ raise ValueError("Counter-like samples cannot be negative: " + line)
+ if sample.exemplar and not (
+ (typ in ['histogram', 'gaugehistogram'] and sample.name.endswith('_bucket'))
+ or (typ in ['counter'] and sample.name.endswith('_total'))):
+ raise ValueError("Invalid line only histogram/gaugehistogram buckets and counters can have exemplars: " + line)
+
+ if name is not None:
+ yield build_metric(name, documentation, typ, unit, samples)
+
+ if not eof:
+ raise ValueError("Missing # EOF at end")
diff --git a/.venv/lib/python3.12/site-packages/prometheus_client/parser.py b/.venv/lib/python3.12/site-packages/prometheus_client/parser.py
new file mode 100644
index 00000000..dc8e30df
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/prometheus_client/parser.py
@@ -0,0 +1,225 @@
+import io as StringIO
+import re
+from typing import Dict, Iterable, List, Match, Optional, TextIO, Tuple
+
+from .metrics_core import Metric
+from .samples import Sample
+
+
+def text_string_to_metric_families(text: str) -> Iterable[Metric]:
+ """Parse Prometheus text format from a unicode string.
+
+ See text_fd_to_metric_families.
+ """
+ yield from text_fd_to_metric_families(StringIO.StringIO(text))
+
+
+ESCAPE_SEQUENCES = {
+ '\\\\': '\\',
+ '\\n': '\n',
+ '\\"': '"',
+}
+
+
+def replace_escape_sequence(match: Match[str]) -> str:
+ return ESCAPE_SEQUENCES[match.group(0)]
+
+
+HELP_ESCAPING_RE = re.compile(r'\\[\\n]')
+ESCAPING_RE = re.compile(r'\\[\\n"]')
+
+
+def _replace_help_escaping(s: str) -> str:
+ return HELP_ESCAPING_RE.sub(replace_escape_sequence, s)
+
+
+def _replace_escaping(s: str) -> str:
+ return ESCAPING_RE.sub(replace_escape_sequence, s)
+
+
+def _is_character_escaped(s: str, charpos: int) -> bool:
+ num_bslashes = 0
+ while (charpos > num_bslashes
+ and s[charpos - 1 - num_bslashes] == '\\'):
+ num_bslashes += 1
+ return num_bslashes % 2 == 1
+
+
+def _parse_labels(labels_string: str) -> Dict[str, str]:
+ labels: Dict[str, str] = {}
+ # Return if we don't have valid labels
+ if "=" not in labels_string:
+ return labels
+
+ escaping = False
+ if "\\" in labels_string:
+ escaping = True
+
+ # Copy original labels
+ sub_labels = labels_string
+ try:
+ # Process one label at a time
+ while sub_labels:
+ # The label name is before the equal
+ value_start = sub_labels.index("=")
+ label_name = sub_labels[:value_start]
+ sub_labels = sub_labels[value_start + 1:].lstrip()
+ # Find the first quote after the equal
+ quote_start = sub_labels.index('"') + 1
+ value_substr = sub_labels[quote_start:]
+
+ # Find the last unescaped quote
+ i = 0
+ while i < len(value_substr):
+ i = value_substr.index('"', i)
+ if not _is_character_escaped(value_substr, i):
+ break
+ i += 1
+
+ # The label value is between the first and last quote
+ quote_end = i + 1
+ label_value = sub_labels[quote_start:quote_end]
+ # Replace escaping if needed
+ if escaping:
+ label_value = _replace_escaping(label_value)
+ labels[label_name.strip()] = label_value
+
+ # Remove the processed label from the sub-slice for next iteration
+ sub_labels = sub_labels[quote_end + 1:]
+ next_comma = sub_labels.find(",") + 1
+ sub_labels = sub_labels[next_comma:].lstrip()
+
+ return labels
+
+ except ValueError:
+ raise ValueError("Invalid labels: %s" % labels_string)
+
+
+# If we have multiple values only consider the first
+def _parse_value_and_timestamp(s: str) -> Tuple[float, Optional[float]]:
+ s = s.lstrip()
+ separator = " "
+ if separator not in s:
+ separator = "\t"
+ values = [value.strip() for value in s.split(separator) if value.strip()]
+ if not values:
+ return float(s), None
+ value = float(values[0])
+ timestamp = (float(values[-1]) / 1000) if len(values) > 1 else None
+ return value, timestamp
+
+
+def _parse_sample(text: str) -> Sample:
+ # Detect the labels in the text
+ try:
+ label_start, label_end = text.index("{"), text.rindex("}")
+ # The name is before the labels
+ name = text[:label_start].strip()
+ # We ignore the starting curly brace
+ label = text[label_start + 1:label_end]
+ # The value is after the label end (ignoring curly brace)
+ value, timestamp = _parse_value_and_timestamp(text[label_end + 1:])
+ return Sample(name, _parse_labels(label), value, timestamp)
+
+ # We don't have labels
+ except ValueError:
+ # Detect what separator is used
+ separator = " "
+ if separator not in text:
+ separator = "\t"
+ name_end = text.index(separator)
+ name = text[:name_end]
+ # The value is after the name
+ value, timestamp = _parse_value_and_timestamp(text[name_end:])
+ return Sample(name, {}, value, timestamp)
+
+
+def text_fd_to_metric_families(fd: TextIO) -> Iterable[Metric]:
+ """Parse Prometheus text format from a file descriptor.
+
+ This is a laxer parser than the main Go parser,
+ so successful parsing does not imply that the parsed
+ text meets the specification.
+
+ Yields Metric's.
+ """
+ name = ''
+ documentation = ''
+ typ = 'untyped'
+ samples: List[Sample] = []
+ allowed_names = []
+
+ def build_metric(name: str, documentation: str, typ: str, samples: List[Sample]) -> Metric:
+ # Munge counters into OpenMetrics representation
+ # used internally.
+ if typ == 'counter':
+ if name.endswith('_total'):
+ name = name[:-6]
+ else:
+ new_samples = []
+ for s in samples:
+ new_samples.append(Sample(s[0] + '_total', *s[1:]))
+ samples = new_samples
+ metric = Metric(name, documentation, typ)
+ metric.samples = samples
+ return metric
+
+ for line in fd:
+ line = line.strip()
+
+ if line.startswith('#'):
+ parts = line.split(None, 3)
+ if len(parts) < 2:
+ continue
+ if parts[1] == 'HELP':
+ if parts[2] != name:
+ if name != '':
+ yield build_metric(name, documentation, typ, samples)
+ # New metric
+ name = parts[2]
+ typ = 'untyped'
+ samples = []
+ allowed_names = [parts[2]]
+ if len(parts) == 4:
+ documentation = _replace_help_escaping(parts[3])
+ else:
+ documentation = ''
+ elif parts[1] == 'TYPE':
+ if parts[2] != name:
+ if name != '':
+ yield build_metric(name, documentation, typ, samples)
+ # New metric
+ name = parts[2]
+ documentation = ''
+ samples = []
+ typ = parts[3]
+ allowed_names = {
+ 'counter': [''],
+ 'gauge': [''],
+ 'summary': ['_count', '_sum', ''],
+ 'histogram': ['_count', '_sum', '_bucket'],
+ }.get(typ, [''])
+ allowed_names = [name + n for n in allowed_names]
+ else:
+ # Ignore other comment tokens
+ pass
+ elif line == '':
+ # Ignore blank lines
+ pass
+ else:
+ sample = _parse_sample(line)
+ if sample.name not in allowed_names:
+ if name != '':
+ yield build_metric(name, documentation, typ, samples)
+ # New metric, yield immediately as untyped singleton
+ name = ''
+ documentation = ''
+ typ = 'untyped'
+ samples = []
+ allowed_names = []
+ yield build_metric(sample[0], documentation, typ, [sample])
+ else:
+ samples.append(sample)
+
+ if name != '':
+ yield build_metric(name, documentation, typ, samples)
diff --git a/.venv/lib/python3.12/site-packages/prometheus_client/platform_collector.py b/.venv/lib/python3.12/site-packages/prometheus_client/platform_collector.py
new file mode 100644
index 00000000..6040fcce
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/prometheus_client/platform_collector.py
@@ -0,0 +1,59 @@
+import platform as pf
+from typing import Any, Iterable, Optional
+
+from .metrics_core import GaugeMetricFamily, Metric
+from .registry import Collector, CollectorRegistry, REGISTRY
+
+
+class PlatformCollector(Collector):
+ """Collector for python platform information"""
+
+ def __init__(self,
+ registry: Optional[CollectorRegistry] = REGISTRY,
+ platform: Optional[Any] = None,
+ ):
+ self._platform = pf if platform is None else platform
+ info = self._info()
+ system = self._platform.system()
+ if system == "Java":
+ info.update(self._java())
+ self._metrics = [
+ self._add_metric("python_info", "Python platform information", info)
+ ]
+ if registry:
+ registry.register(self)
+
+ def collect(self) -> Iterable[Metric]:
+ return self._metrics
+
+ @staticmethod
+ def _add_metric(name, documentation, data):
+ labels = data.keys()
+ values = [data[k] for k in labels]
+ g = GaugeMetricFamily(name, documentation, labels=labels)
+ g.add_metric(values, 1)
+ return g
+
+ def _info(self):
+ major, minor, patchlevel = self._platform.python_version_tuple()
+ return {
+ "version": self._platform.python_version(),
+ "implementation": self._platform.python_implementation(),
+ "major": major,
+ "minor": minor,
+ "patchlevel": patchlevel
+ }
+
+ def _java(self):
+ java_version, _, vminfo, osinfo = self._platform.java_ver()
+ vm_name, vm_release, vm_vendor = vminfo
+ return {
+ "jvm_version": java_version,
+ "jvm_release": vm_release,
+ "jvm_vendor": vm_vendor,
+ "jvm_name": vm_name
+ }
+
+
+PLATFORM_COLLECTOR = PlatformCollector()
+"""PlatformCollector in default Registry REGISTRY"""
diff --git a/.venv/lib/python3.12/site-packages/prometheus_client/process_collector.py b/.venv/lib/python3.12/site-packages/prometheus_client/process_collector.py
new file mode 100644
index 00000000..2894e874
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/prometheus_client/process_collector.py
@@ -0,0 +1,101 @@
+import os
+from typing import Callable, Iterable, Optional, Union
+
+from .metrics_core import CounterMetricFamily, GaugeMetricFamily, Metric
+from .registry import Collector, CollectorRegistry, REGISTRY
+
+try:
+ import resource
+
+ _PAGESIZE = resource.getpagesize()
+except ImportError:
+ # Not Unix
+ _PAGESIZE = 4096
+
+
+class ProcessCollector(Collector):
+ """Collector for Standard Exports such as cpu and memory."""
+
+ def __init__(self,
+ namespace: str = '',
+ pid: Callable[[], Union[int, str]] = lambda: 'self',
+ proc: str = '/proc',
+ registry: Optional[CollectorRegistry] = REGISTRY):
+ self._namespace = namespace
+ self._pid = pid
+ self._proc = proc
+ if namespace:
+ self._prefix = namespace + '_process_'
+ else:
+ self._prefix = 'process_'
+ self._ticks = 100.0
+ try:
+ self._ticks = os.sysconf('SC_CLK_TCK')
+ except (ValueError, TypeError, AttributeError, OSError):
+ pass
+
+ self._pagesize = _PAGESIZE
+
+ # This is used to test if we can access /proc.
+ self._btime = 0
+ try:
+ self._btime = self._boot_time()
+ except OSError:
+ pass
+ if registry:
+ registry.register(self)
+
+ def _boot_time(self):
+ with open(os.path.join(self._proc, 'stat'), 'rb') as stat:
+ for line in stat:
+ if line.startswith(b'btime '):
+ return float(line.split()[1])
+
+ def collect(self) -> Iterable[Metric]:
+ if not self._btime:
+ return []
+
+ pid = os.path.join(self._proc, str(self._pid()).strip())
+
+ result = []
+ try:
+ with open(os.path.join(pid, 'stat'), 'rb') as stat:
+ parts = (stat.read().split(b')')[-1].split())
+
+ vmem = GaugeMetricFamily(self._prefix + 'virtual_memory_bytes',
+ 'Virtual memory size in bytes.', value=float(parts[20]))
+ rss = GaugeMetricFamily(self._prefix + 'resident_memory_bytes', 'Resident memory size in bytes.',
+ value=float(parts[21]) * self._pagesize)
+ start_time_secs = float(parts[19]) / self._ticks
+ start_time = GaugeMetricFamily(self._prefix + 'start_time_seconds',
+ 'Start time of the process since unix epoch in seconds.',
+ value=start_time_secs + self._btime)
+ utime = float(parts[11]) / self._ticks
+ stime = float(parts[12]) / self._ticks
+ cpu = CounterMetricFamily(self._prefix + 'cpu_seconds_total',
+ 'Total user and system CPU time spent in seconds.',
+ value=utime + stime)
+ result.extend([vmem, rss, start_time, cpu])
+ except OSError:
+ pass
+
+ try:
+ with open(os.path.join(pid, 'limits'), 'rb') as limits:
+ for line in limits:
+ if line.startswith(b'Max open file'):
+ max_fds = GaugeMetricFamily(self._prefix + 'max_fds',
+ 'Maximum number of open file descriptors.',
+ value=float(line.split()[3]))
+ break
+ open_fds = GaugeMetricFamily(self._prefix + 'open_fds',
+ 'Number of open file descriptors.',
+ len(os.listdir(os.path.join(pid, 'fd'))))
+ result.extend([open_fds, max_fds])
+ except OSError:
+ pass
+
+ return result
+
+
+PROCESS_COLLECTOR = ProcessCollector()
+"""Default ProcessCollector in default Registry REGISTRY."""
diff --git a/.venv/lib/python3.12/site-packages/prometheus_client/py.typed b/.venv/lib/python3.12/site-packages/prometheus_client/py.typed
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/prometheus_client/py.typed
diff --git a/.venv/lib/python3.12/site-packages/prometheus_client/registry.py b/.venv/lib/python3.12/site-packages/prometheus_client/registry.py
new file mode 100644
index 00000000..694e4bd8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/prometheus_client/registry.py
@@ -0,0 +1,168 @@
+from abc import ABC, abstractmethod
+import copy
+from threading import Lock
+from typing import Dict, Iterable, List, Optional
+
+from .metrics_core import Metric
+
+
+# Ideally this would be a Protocol, but Protocols are only available in Python >= 3.8.
+class Collector(ABC):
+ @abstractmethod
+ def collect(self) -> Iterable[Metric]:
+ pass
+
+
+class _EmptyCollector(Collector):
+ def collect(self) -> Iterable[Metric]:
+ return []
+
+
+class CollectorRegistry(Collector):
+ """Metric collector registry.
+
+ Collectors must have a no-argument method 'collect' that returns a list of
+ Metric objects. The returned metrics should be consistent with the Prometheus
+ exposition formats.
+ """
+
+ def __init__(self, auto_describe: bool = False, target_info: Optional[Dict[str, str]] = None):
+ 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.set_target_info(target_info)
+
+ def register(self, collector: Collector) -> None:
+ """Add a collector to the registry."""
+ with self._lock:
+ names = self._get_names(collector)
+ duplicates = set(self._names_to_collectors).intersection(names)
+ if duplicates:
+ raise ValueError(
+ 'Duplicated timeseries in CollectorRegistry: {}'.format(
+ duplicates))
+ for name in names:
+ self._names_to_collectors[name] = collector
+ self._collector_to_names[collector] = names
+
+ def unregister(self, collector: Collector) -> None:
+ """Remove a collector from the registry."""
+ with self._lock:
+ for name in self._collector_to_names[collector]:
+ del self._names_to_collectors[name]
+ del self._collector_to_names[collector]
+
+ def _get_names(self, collector):
+ """Get names of timeseries the collector produces and clashes with."""
+ desc_func = None
+ # If there's a describe function, use it.
+ try:
+ desc_func = collector.describe
+ except AttributeError:
+ pass
+ # Otherwise, if auto describe is enabled use the collect function.
+ if not desc_func and self._auto_describe:
+ desc_func = collector.collect
+
+ if not desc_func:
+ return []
+
+ result = []
+ type_suffixes = {
+ 'counter': ['_total', '_created'],
+ 'summary': ['_sum', '_count', '_created'],
+ 'histogram': ['_bucket', '_sum', '_count', '_created'],
+ 'gaugehistogram': ['_bucket', '_gsum', '_gcount'],
+ 'info': ['_info'],
+ }
+ for metric in desc_func():
+ result.append(metric.name)
+ for suffix in type_suffixes.get(metric.type, []):
+ result.append(metric.name + suffix)
+ return result
+
+ def collect(self) -> Iterable[Metric]:
+ """Yields metrics from the collectors in the registry."""
+ collectors = None
+ ti = None
+ with self._lock:
+ collectors = copy.copy(self._collector_to_names)
+ if self._target_info:
+ ti = self._target_info_metric()
+ if ti:
+ yield ti
+ for collector in collectors:
+ yield from collector.collect()
+
+ def restricted_registry(self, names: Iterable[str]) -> "RestrictedRegistry":
+ """Returns object that only collects some metrics.
+
+ Returns an object which upon collect() will return
+ only samples with the given names.
+
+ Intended usage is:
+ generate_latest(REGISTRY.restricted_registry(['a_timeseries']))
+
+ Experimental."""
+ names = set(names)
+ return RestrictedRegistry(names, self)
+
+ def set_target_info(self, labels: Optional[Dict[str, str]]) -> None:
+ with self._lock:
+ if labels:
+ if not self._target_info and 'target_info' in self._names_to_collectors:
+ raise ValueError('CollectorRegistry already contains a target_info metric')
+ self._names_to_collectors['target_info'] = _EmptyCollector()
+ elif self._target_info:
+ self._names_to_collectors.pop('target_info', None)
+ self._target_info = labels
+
+ def get_target_info(self) -> Optional[Dict[str, str]]:
+ with self._lock:
+ return self._target_info
+
+ def _target_info_metric(self):
+ m = Metric('target', 'Target metadata', 'info')
+ m.add_sample('target_info', self._target_info, 1)
+ return m
+
+ def get_sample_value(self, name: str, labels: Optional[Dict[str, str]] = None) -> Optional[float]:
+ """Returns the sample value, or None if not found.
+
+ This is inefficient, and intended only for use in unittests.
+ """
+ if labels is None:
+ labels = {}
+ for metric in self.collect():
+ for s in metric.samples:
+ if s.name == name and s.labels == labels:
+ return s.value
+ return None
+
+
+class RestrictedRegistry:
+ def __init__(self, names: Iterable[str], registry: CollectorRegistry):
+ self._name_set = set(names)
+ self._registry = registry
+
+ def collect(self) -> Iterable[Metric]:
+ collectors = set()
+ target_info_metric = None
+ with self._registry._lock:
+ if 'target_info' in self._name_set and self._registry._target_info:
+ target_info_metric = self._registry._target_info_metric()
+ for name in self._name_set:
+ if name != 'target_info' and name in self._registry._names_to_collectors:
+ collectors.add(self._registry._names_to_collectors[name])
+ if target_info_metric:
+ yield target_info_metric
+ for collector in collectors:
+ for metric in collector.collect():
+ m = metric._restricted_metric(self._name_set)
+ if m:
+ yield m
+
+
+REGISTRY = CollectorRegistry(auto_describe=True)
diff --git a/.venv/lib/python3.12/site-packages/prometheus_client/samples.py b/.venv/lib/python3.12/site-packages/prometheus_client/samples.py
new file mode 100644
index 00000000..53c47264
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/prometheus_client/samples.py
@@ -0,0 +1,53 @@
+from typing import Dict, NamedTuple, Optional, Union
+
+
+class Timestamp:
+ """A nanosecond-resolution timestamp."""
+
+ def __init__(self, sec: float, nsec: float) -> None:
+ if nsec < 0 or nsec >= 1e9:
+ raise ValueError(f"Invalid value for nanoseconds in Timestamp: {nsec}")
+ if sec < 0:
+ nsec = -nsec
+ self.sec: int = int(sec)
+ self.nsec: int = int(nsec)
+
+ def __str__(self) -> str:
+ return f"{self.sec}.{self.nsec:09d}"
+
+ def __repr__(self) -> str:
+ return f"Timestamp({self.sec}, {self.nsec})"
+
+ def __float__(self) -> float:
+ return float(self.sec) + float(self.nsec) / 1e9
+
+ def __eq__(self, other: object) -> bool:
+ return isinstance(other, Timestamp) and self.sec == other.sec and self.nsec == other.nsec
+
+ def __ne__(self, other: object) -> bool:
+ return not self == other
+
+ def __gt__(self, other: "Timestamp") -> bool:
+ return self.nsec > other.nsec if self.sec == other.sec else self.sec > other.sec
+
+ def __lt__(self, other: "Timestamp") -> bool:
+ return self.nsec < other.nsec if self.sec == other.sec else self.sec < other.sec
+
+
+# Timestamp and exemplar are optional.
+# Value can be an int or a float.
+# Timestamp can be a float containing a unixtime in seconds,
+# a Timestamp object, or None.
+# Exemplar can be an Exemplar object, or None.
+class Exemplar(NamedTuple):
+ labels: Dict[str, str]
+ value: float
+ timestamp: Optional[Union[float, Timestamp]] = None
+
+
+class Sample(NamedTuple):
+ name: str
+ labels: Dict[str, str]
+ value: float
+ timestamp: Optional[Union[float, Timestamp]] = None
+ exemplar: Optional[Exemplar] = None
diff --git a/.venv/lib/python3.12/site-packages/prometheus_client/twisted/__init__.py b/.venv/lib/python3.12/site-packages/prometheus_client/twisted/__init__.py
new file mode 100644
index 00000000..87e0b8a6
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/prometheus_client/twisted/__init__.py
@@ -0,0 +1,3 @@
+from ._exposition import MetricsResource
+
+__all__ = ['MetricsResource']
diff --git a/.venv/lib/python3.12/site-packages/prometheus_client/twisted/_exposition.py b/.venv/lib/python3.12/site-packages/prometheus_client/twisted/_exposition.py
new file mode 100644
index 00000000..202a7d3b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/prometheus_client/twisted/_exposition.py
@@ -0,0 +1,8 @@
+from twisted.internet import reactor
+from twisted.web.wsgi import WSGIResource
+
+from .. import exposition, REGISTRY
+
+MetricsResource = lambda registry=REGISTRY: WSGIResource(
+ reactor, reactor.getThreadPool(), exposition.make_wsgi_app(registry)
+)
diff --git a/.venv/lib/python3.12/site-packages/prometheus_client/utils.py b/.venv/lib/python3.12/site-packages/prometheus_client/utils.py
new file mode 100644
index 00000000..0d2b0948
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/prometheus_client/utils.py
@@ -0,0 +1,24 @@
+import math
+
+INF = float("inf")
+MINUS_INF = float("-inf")
+NaN = float("NaN")
+
+
+def floatToGoString(d):
+ d = float(d)
+ if d == INF:
+ return '+Inf'
+ elif d == MINUS_INF:
+ return '-Inf'
+ elif math.isnan(d):
+ return 'NaN'
+ else:
+ s = repr(d)
+ dot = s.find('.')
+ # Go switches to exponents sooner than Python.
+ # We only need to care about positive values for le/quantile.
+ if d > 0 and dot > 6:
+ mantissa = f'{s[0]}.{s[1:dot]}{s[dot + 1:]}'.rstrip('0.')
+ return f'{mantissa}e+0{dot - 1}'
+ return s
diff --git a/.venv/lib/python3.12/site-packages/prometheus_client/values.py b/.venv/lib/python3.12/site-packages/prometheus_client/values.py
new file mode 100644
index 00000000..6ff85e3b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/prometheus_client/values.py
@@ -0,0 +1,139 @@
+import os
+from threading import Lock
+import warnings
+
+from .mmap_dict import mmap_key, MmapedDict
+
+
+class MutexValue:
+ """A float protected by a mutex."""
+
+ _multiprocess = False
+
+ def __init__(self, typ, metric_name, name, labelnames, labelvalues, help_text, **kwargs):
+ self._value = 0.0
+ self._exemplar = None
+ self._lock = Lock()
+
+ def inc(self, amount):
+ with self._lock:
+ self._value += amount
+
+ def set(self, value, timestamp=None):
+ with self._lock:
+ self._value = value
+
+ def set_exemplar(self, exemplar):
+ with self._lock:
+ self._exemplar = exemplar
+
+ def get(self):
+ with self._lock:
+ return self._value
+
+ def get_exemplar(self):
+ with self._lock:
+ return self._exemplar
+
+
+def MultiProcessValue(process_identifier=os.getpid):
+ """Returns a MmapedValue class based on a process_identifier function.
+
+ The 'process_identifier' function MUST comply with this simple rule:
+ when called in simultaneously running processes it MUST return distinct values.
+
+ Using a different function than the default 'os.getpid' is at your own risk.
+ """
+ files = {}
+ values = []
+ pid = {'value': process_identifier()}
+ # Use a single global lock when in multi-processing mode
+ # as we presume this means there is no threading going on.
+ # This avoids the need to also have mutexes in __MmapDict.
+ lock = Lock()
+
+ class MmapedValue:
+ """A float protected by a mutex backed by a per-process mmaped file."""
+
+ _multiprocess = True
+
+ def __init__(self, typ, metric_name, name, labelnames, labelvalues, help_text, multiprocess_mode='', **kwargs):
+ self._params = typ, metric_name, name, labelnames, labelvalues, help_text, multiprocess_mode
+ # This deprecation warning can go away in a few releases when removing the compatibility
+ if 'prometheus_multiproc_dir' in os.environ and 'PROMETHEUS_MULTIPROC_DIR' not in os.environ:
+ os.environ['PROMETHEUS_MULTIPROC_DIR'] = os.environ['prometheus_multiproc_dir']
+ warnings.warn("prometheus_multiproc_dir variable has been deprecated in favor of the upper case naming PROMETHEUS_MULTIPROC_DIR", DeprecationWarning)
+ with lock:
+ self.__check_for_pid_change()
+ self.__reset()
+ values.append(self)
+
+ def __reset(self):
+ typ, metric_name, name, labelnames, labelvalues, help_text, multiprocess_mode = self._params
+ if typ == 'gauge':
+ file_prefix = typ + '_' + multiprocess_mode
+ else:
+ file_prefix = typ
+ if file_prefix not in files:
+ filename = os.path.join(
+ os.environ.get('PROMETHEUS_MULTIPROC_DIR'),
+ '{}_{}.db'.format(file_prefix, pid['value']))
+
+ files[file_prefix] = MmapedDict(filename)
+ self._file = files[file_prefix]
+ self._key = mmap_key(metric_name, name, labelnames, labelvalues, help_text)
+ self._value, self._timestamp = self._file.read_value(self._key)
+
+ def __check_for_pid_change(self):
+ actual_pid = process_identifier()
+ if pid['value'] != actual_pid:
+ pid['value'] = actual_pid
+ # There has been a fork(), reset all the values.
+ for f in files.values():
+ f.close()
+ files.clear()
+ for value in values:
+ value.__reset()
+
+ def inc(self, amount):
+ with lock:
+ self.__check_for_pid_change()
+ self._value += amount
+ self._timestamp = 0.0
+ self._file.write_value(self._key, self._value, self._timestamp)
+
+ def set(self, value, timestamp=None):
+ with lock:
+ self.__check_for_pid_change()
+ self._value = value
+ self._timestamp = timestamp or 0.0
+ self._file.write_value(self._key, self._value, self._timestamp)
+
+ def set_exemplar(self, exemplar):
+ # TODO: Implement exemplars for multiprocess mode.
+ return
+
+ def get(self):
+ with lock:
+ self.__check_for_pid_change()
+ return self._value
+
+ def get_exemplar(self):
+ # TODO: Implement exemplars for multiprocess mode.
+ return None
+
+ return MmapedValue
+
+
+def get_value_class():
+ # Should we enable multi-process mode?
+ # This needs to be chosen before the first metric is constructed,
+ # and as that may be in some arbitrary library the user/admin has
+ # no control over we use an environment variable.
+ if 'prometheus_multiproc_dir' in os.environ or 'PROMETHEUS_MULTIPROC_DIR' in os.environ:
+ return MultiProcessValue()
+ else:
+ return MutexValue
+
+
+ValueClass = get_value_class()