feat(core): one Prometheus text parser, with types; Traefik reads through it (#5667)
steward/core/prometheus.py parses the 0.0.4 exposition into families keyed by name, each with its declared TYPE, so a scraper can tell a counter from a gauge. A histogram or summary owns its _bucket/_sum/_count samples. Label values are unescaped, and commas or braces inside quotes stay part of the value. NaN and +/-Inf are read, timestamps are ignored, and a malformed line is skipped. Traefik's parse_prometheus is now a thin reshaping of it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,8 @@ from datetime import datetime, timezone
|
||||
|
||||
import httpx
|
||||
|
||||
from steward.core import prometheus
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Prometheus text format parser
|
||||
@@ -26,36 +28,14 @@ import httpx
|
||||
# Parsed sample: {metric_name: {frozenset(label_items): float}}
|
||||
ParsedMetrics = dict[str, dict[frozenset, float]]
|
||||
|
||||
_SAMPLE_RE = re.compile(
|
||||
r'^([a-zA-Z_:][a-zA-Z0-9_:]*)(?:\{([^}]*)\})?\s+'
|
||||
r'([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?|[+-]?Inf|NaN)'
|
||||
)
|
||||
_LABEL_RE = re.compile(r'([a-zA-Z_][a-zA-Z0-9_]*)="([^"\\]*(?:\\.[^"\\]*)*)"')
|
||||
|
||||
|
||||
def parse_prometheus(text: str) -> ParsedMetrics:
|
||||
"""Parse Prometheus text exposition format into nested dicts.
|
||||
|
||||
Returns: {metric_name: {frozenset({(label, value), ...}): float}}
|
||||
"""
|
||||
"""The exposition keyed by sample name, then by its label set — the shape
|
||||
the per-router arithmetic below reads. Parsing is steward.core.prometheus."""
|
||||
metrics: ParsedMetrics = {}
|
||||
for line in text.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
m = _SAMPLE_RE.match(line)
|
||||
if not m:
|
||||
continue
|
||||
name, labels_str, value_str = m.groups()
|
||||
try:
|
||||
value = float(value_str)
|
||||
except ValueError:
|
||||
continue
|
||||
labels = frozenset(
|
||||
(lm.group(1), lm.group(2))
|
||||
for lm in _LABEL_RE.finditer(labels_str or "")
|
||||
)
|
||||
metrics.setdefault(name, {})[labels] = value
|
||||
for family in prometheus.parse(text).values():
|
||||
for sample in family.samples:
|
||||
metrics.setdefault(sample.name, {})[frozenset(sample.labels)] = sample.value
|
||||
return metrics
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user