feat(prometheus): fold state sets on the target page; forget series an endpoint drops (#5689)
#5689 "Prometheus target page: fold state sets, show epoch times, size bytes", step 1 of milestone 508 "Prometheus widget templates". - A state set (name ending in _state, a state label, every series 0 or 1; or the OpenMetrics form, the label named after the metric) reads as one row per thing it describes, naming the current state, the others on hover. fabledarchives_jobs{state} is a count and stays as it is. - label_pairs / state_label / fold_states in service.py, for the widget templates to reuse. - After a good scrape, series it no longer has lose their newest value (forget_gone): Archives' old build_info row read as current beside the new one. History stays; a failed scrape forgets nothing; alert rules keep their own last-sample time. - Epoch times and _bytes sizes were already read by shown_value / metric_value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -60,6 +60,67 @@ def rate_name(sample_name: str) -> str:
|
||||
return base + "_per_sec"
|
||||
|
||||
|
||||
def label_pairs(labels: str) -> list[tuple[str, str]]:
|
||||
"""resource_for's labels read back: "target=S,state=late" →
|
||||
[("target", "S"), ("state", "late")]. A piece without "=" was part of the
|
||||
value before it, a value that held a comma."""
|
||||
pairs: list[tuple[str, str]] = []
|
||||
for piece in labels.split(",") if labels else []:
|
||||
key, eq, value = piece.partition("=")
|
||||
if eq and key:
|
||||
pairs.append((key, value))
|
||||
elif pairs:
|
||||
pairs[-1] = (pairs[-1][0], pairs[-1][1] + "," + piece)
|
||||
return pairs
|
||||
|
||||
|
||||
def state_label(metric: str, pairs: list[tuple[str, str]]) -> str | None:
|
||||
"""Which label names a state set's state: `state`, or the metric's own
|
||||
name as OpenMetrics writes a stateset (foo{foo="a"} 1)."""
|
||||
keys = {k for k, _ in pairs}
|
||||
for key in ("state", metric):
|
||||
if key in keys:
|
||||
return key
|
||||
return None
|
||||
|
||||
|
||||
def fold_states(metric: str, rows: list[dict]) -> list[dict] | None:
|
||||
"""A state set's series folded to one row per thing it describes.
|
||||
|
||||
A state set is one series per possible state, the current one 1 and the
|
||||
rest 0: Archives' fabledarchives_backup_target_state{target, state}. Each
|
||||
row in `rows` has `labels` (resource_for's form), `value` and `resource`.
|
||||
A metric is folded when its name ends in _state (or its state label is
|
||||
its own name) and every series carries the state label and reads 0 or 1;
|
||||
anything else is None, and shown series by series. The name matters: a
|
||||
count split by a `state` label (fabledarchives_jobs{state="waiting"})
|
||||
often reads 0 or 1 too, and is not a state set.
|
||||
|
||||
Each folded row: `labels` without the state, `state` (the one set to 1;
|
||||
several are joined, none is ""), `states` (every state, in order) and
|
||||
`resource` (the current state's series, else the first)."""
|
||||
if not rows:
|
||||
return None
|
||||
sets: dict[str, list[tuple[str, dict]]] = {}
|
||||
for row in rows:
|
||||
pairs = label_pairs(row["labels"])
|
||||
key = state_label(metric, pairs)
|
||||
if key is None or row["value"] not in (0, 1):
|
||||
return None
|
||||
if key == "state" and not metric.endswith("_state"):
|
||||
return None
|
||||
rest = ",".join(f"{k}={v}" for k, v in pairs if k != key)
|
||||
sets.setdefault(rest, []).append((dict(pairs)[key], row))
|
||||
folded = []
|
||||
for rest, members in sorted(sets.items()):
|
||||
members.sort(key=lambda m: m[0])
|
||||
current = [(s, r) for s, r in members if r["value"] == 1]
|
||||
folded.append({"labels": rest, "state": ", ".join(s for s, _ in current),
|
||||
"states": [s for s, _ in members],
|
||||
"resource": (current or members)[0][1]["resource"]})
|
||||
return folded
|
||||
|
||||
|
||||
@dataclass
|
||||
class Series:
|
||||
resource: str
|
||||
|
||||
Reference in New Issue
Block a user