changeset 98:32e2c91eeaf3

easy api over prometheus, for ease of porting. Might be slow
author drewp@bigasterisk.com
date Mon, 30 May 2022 20:39:59 -0700
parents 354ffd78c0fe
children 22f81cb04da4
files rdfdb/metrics.py
diffstat 1 files changed, 160 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rdfdb/metrics.py	Mon May 30 20:39:59 2022 -0700
@@ -0,0 +1,160 @@
+# copy from light9 version- lib me
+"""for easier porting, and less boilerplate, allow these styles using the
+form of the call to set up the right type of metric automatically:
+
+  from metrics import metrics
+  metrics.setProcess('pretty_name')
+
+  @metrics('loop').time()              # a common one to get the fps of each service. Gets us qty and time
+  def frame():
+      if err:
+         metrics('foo_errors').incr()  # if you incr it, it's a counter
+
+  @metrics('foo_calls').time()         # qty & time because it's a decorator
+  def foo(): 
+
+  metrics('goal_fps').set(f)           # a gauge because we called set()
+
+  with metrics('recompute'): ...       # ctxmgr also makes a timer
+     time_this_part()
+
+I don't see a need for labels yet, but maybe some code will want like
+metrics('foo', label1=one). Need histogram? Info?
+
+"""
+from typing import Dict, Tuple, Callable, Type, TypeVar, cast
+import cyclone.web
+from prometheus_client import Counter, Gauge, Metric, Summary
+from prometheus_client.exposition import generate_latest
+from prometheus_client.registry import REGISTRY
+
+_created: Dict[str, Metric] = {}
+
+# _process=sys.argv[0]
+# def setProcess(name: str):
+#   global _process
+#   _process = name
+
+MT = TypeVar("MT")
+
+
+class _MetricsRequest:
+
+    def __init__(self, name: str, **labels):
+        self.name = name
+        self.labels = labels
+
+    def _ensure(self, cls: Type[MT]) -> MT:
+        if self.name not in _created:
+            _created[self.name] = cls(name=self.name, documentation=self.name, labelnames=self.labels.keys())
+        m = _created[self.name]
+        if self.labels:
+            m = m.labels(**self.labels)
+        return m
+
+    def __call__(self, fn) -> Callable:
+        return timed_fn
+
+    def set(self, v: float):
+        self._ensure(Gauge).set(v)
+
+    def inc(self):
+        self._ensure(Counter).inc()
+
+    def offset(self, amount: float):
+        self._ensure(Gauge).inc(amount)
+
+    def time(self):
+        return self._ensure(Summary).time()
+
+    def observe(self, x: float):
+        return self._ensure(Summary).observe(x)
+
+    def __enter__(self):
+        return self._ensure(Summary).__enter__()
+
+
+def metrics(name: str, **labels):
+    return _MetricsRequest(name, **labels)
+
+
+class _CycloneMetrics(cyclone.web.RequestHandler):
+
+    def get(self):
+        self.add_header('content-type', 'text/plain')
+        self.write(generate_latest(REGISTRY))
+
+
+def metricsRoute() -> Tuple[str, Type[cyclone.web.RequestHandler]]:
+    return ('/metrics', _CycloneMetrics)
+
+
+"""
+stuff we used to have in greplin. Might be nice to get (client-side-computed) min/max/stddev back.
+
+class PmfStat(Stat):
+  A stat that stores min, max, mean, standard deviation, and some
+  percentiles for arbitrary floating-point data. This is potentially a
+  bit expensive, so its child values are only updated once every
+  twenty seconds.
+
+
+
+metrics consumer side can do this with the changing counts:
+
+class RecentFps(object):
+  def __init__(self, window=20):
+    self.window = window
+    self.recentTimes = []
+
+  def mark(self):
+    now = time.time()
+    self.recentTimes.append(now)
+    self.recentTimes = self.recentTimes[-self.window:]
+
+  def rate(self):
+    def dec(innerFunc):
+      def f(*a, **kw):
+        self.mark()
+        return innerFunc(*a, **kw)
+      return f
+    return dec
+
+  def __call__(self):
+    if len(self.recentTimes) < 2:
+      return {}
+    recents = sorted(round(1 / (b - a), 3)
+                      for a, b in zip(self.recentTimes[:-1],
+                                      self.recentTimes[1:]))
+    avg = (len(self.recentTimes) - 1) / (
+      self.recentTimes[-1] - self.recentTimes[0])
+    return {'average': round(avg, 5), 'recents': recents}
+
+
+i think prometheus covers this one:
+
+import psutil
+def gatherProcessStats():
+    procStats = scales.collection('/process',
+                                  scales.DoubleStat('time'),
+                                  scales.DoubleStat('cpuPercent'),
+                                  scales.DoubleStat('memMb'),
+    )
+    proc = psutil.Process()
+    lastCpu = [0.]
+    def updateTimeStat():
+        now = time.time()
+        procStats.time = round(now, 3)
+        if now - lastCpu[0] > 3:
+            procStats.cpuPercent = round(proc.cpu_percent(), 6) # (since last call)
+            lastCpu[0] = now
+        procStats.memMb = round(proc.memory_info().rss / 1024 / 1024, 6)
+    task.LoopingCall(updateTimeStat).start(.1)
+
+"""
+
+
+class M:
+
+    def __call__(self, name):
+        return