A bundled prometheus plugin, on by default like the other protocol plugins. Targets are rows edited on its page: address, bearer token (encrypted), interval, keep/leave-out name patterns and a series cap. Steward does the filtering; the endpoint serves everything (Archives' ruling). - A 5 s tick scrapes due targets at once, each within a 30 s deadline, and writes the outcome to its row and its series through record_metrics, so alert rules can watch any of them. - Gauges are stored as they are; counters (and a histogram's or summary's _sum/_count) as <name>_per_sec rates from the second scrape. Buckets and quantiles are skipped. A labelled series is stored under "<target>:k=v,…". Each target also records is_up, scrape_ms, series_kept and series_capped. - Pages: targets not answering first, with why; a target's series grouped by metric with their newest values; a chart per series; the editor with Test, which scrapes once and says what would be stored. - The counter-to-rate keeper moves to steward/core/counter_rates.py; SNMP's RateKeeper now scales its rate on top of it. - metric_value reads Prometheus's _seconds as a span and _per_sec as a rate. - The catalog guard treats an empty list as a dynamic source (snmp, prometheus), as the rule form does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+6
-22
@@ -6,6 +6,7 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
from steward.core.counter_rates import CounterRates
|
||||
from steward.core.crypto import decrypt_secret, encrypt_secret
|
||||
|
||||
from .models import SnmpDevice
|
||||
@@ -168,28 +169,11 @@ def device_state(device: SnmpDevice, poll_seconds: int, now: datetime) -> Device
|
||||
return DeviceState("up", "Answering.")
|
||||
|
||||
|
||||
class RateKeeper:
|
||||
"""A counter's previous sample per (device, key), to turn the next one
|
||||
into a per-second rate. In memory: after a restart the first poll of each
|
||||
counter stores nothing and the second stores its rate. A sample lower
|
||||
than the one before (the device restarted, or a 32-bit counter wrapped)
|
||||
is skipped rather than read as a huge negative or wrapped rate."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._last: dict[tuple[str, str], tuple[float, datetime]] = {}
|
||||
class RateKeeper(CounterRates):
|
||||
"""A counter reading's previous sample per (device, key), its rate scaled
|
||||
to the reading's unit (steward.core.counter_rates has the rules)."""
|
||||
|
||||
def rate(self, device_id: str, reading: Reading, value: float, at: datetime) -> float | None:
|
||||
from .readings import COUNTER_UNITS
|
||||
key = (device_id, reading.key)
|
||||
prev = self._last.get(key)
|
||||
self._last[key] = (value, at)
|
||||
if prev is None:
|
||||
return None
|
||||
seconds = (at - prev[1]).total_seconds()
|
||||
if seconds <= 0 or value < prev[0]:
|
||||
return None
|
||||
return (value - prev[0]) / seconds * COUNTER_UNITS[reading.unit][1]
|
||||
|
||||
def forget(self, device_id: str) -> None:
|
||||
for key in [k for k in self._last if k[0] == device_id]:
|
||||
del self._last[key]
|
||||
per_sec = super().rate((device_id, reading.key), value, at)
|
||||
return None if per_sec is None else per_sec * COUNTER_UNITS[reading.unit][1]
|
||||
|
||||
Reference in New Issue
Block a user