118 lines
4.2 KiB
Python
118 lines
4.2 KiB
Python
"""The Prometheus text parser every scraper shares (steward/core/prometheus.py)."""
|
|
import math
|
|
|
|
from steward.core import prometheus
|
|
|
|
# Shaped like Fabled Archives' /metrics: HELP and TYPE lines, info and state
|
|
# gauges with several labels, counters, and an untyped stray.
|
|
ARCHIVES = """\
|
|
# HELP fabledarchives_build_info Build of the running server.
|
|
# TYPE fabledarchives_build_info gauge
|
|
fabledarchives_build_info{version="2026.10.11.1458",channel="dev",commit="c49b470"} 1
|
|
# TYPE fabledarchives_backup_target_state gauge
|
|
fabledarchives_backup_target_state{target="Steward",target_id="3",state="fresh"} 1
|
|
fabledarchives_backup_target_state{target="Steward",target_id="3",state="late"} 0
|
|
# TYPE fabledarchives_http_requests_total counter
|
|
fabledarchives_http_requests_total{route="GET /api/files",status="200"} 1027
|
|
fabledarchives_http_requests_total{route="GET /api/files",status="404"} 3
|
|
stray_metric 4.5
|
|
"""
|
|
|
|
|
|
def test_families_carry_their_declared_type():
|
|
fams = prometheus.parse(ARCHIVES)
|
|
assert fams["fabledarchives_build_info"].type == "gauge"
|
|
assert fams["fabledarchives_build_info"].help == "Build of the running server."
|
|
assert fams["fabledarchives_http_requests_total"].type == "counter"
|
|
assert fams["stray_metric"].type == "untyped"
|
|
assert list(fams) == [
|
|
"fabledarchives_build_info",
|
|
"fabledarchives_backup_target_state",
|
|
"fabledarchives_http_requests_total",
|
|
"stray_metric",
|
|
]
|
|
|
|
|
|
def test_labels_keep_their_order_and_values():
|
|
state = prometheus.parse(ARCHIVES)["fabledarchives_backup_target_state"]
|
|
assert [s.labels for s in state.samples] == [
|
|
(("target", "Steward"), ("target_id", "3"), ("state", "fresh")),
|
|
(("target", "Steward"), ("target_id", "3"), ("state", "late")),
|
|
]
|
|
assert [s.value for s in state.samples] == [1.0, 0.0]
|
|
reqs = prometheus.parse(ARCHIVES)["fabledarchives_http_requests_total"]
|
|
assert reqs.samples[0].label_dict() == {"route": "GET /api/files", "status": "200"}
|
|
|
|
|
|
def test_escaped_label_values_and_separators_inside_quotes():
|
|
text = 'm{a="say \\"hi\\"",b="back\\\\slash",c="two\\nlines",d="x,y}z"} 2\n'
|
|
(sample,) = prometheus.parse(text)["m"].samples
|
|
assert sample.label_dict() == {
|
|
"a": 'say "hi"', "b": "back\\slash", "c": "two\nlines", "d": "x,y}z",
|
|
}
|
|
|
|
|
|
def test_histogram_and_summary_samples_belong_to_their_family():
|
|
text = """\
|
|
# TYPE req_seconds histogram
|
|
req_seconds_bucket{le="0.1"} 3
|
|
req_seconds_bucket{le="+Inf"} 5
|
|
req_seconds_sum 0.9
|
|
req_seconds_count 5
|
|
# TYPE rpc summary
|
|
rpc{quantile="0.5"} 0.2
|
|
rpc_sum 7
|
|
rpc_count 30
|
|
"""
|
|
fams = prometheus.parse(text)
|
|
assert set(fams) == {"req_seconds", "rpc"}
|
|
assert [s.name for s in fams["req_seconds"].samples] == [
|
|
"req_seconds_bucket", "req_seconds_bucket", "req_seconds_sum", "req_seconds_count",
|
|
]
|
|
assert fams["req_seconds"].samples[1].label_dict() == {"le": "+Inf"}
|
|
assert [s.name for s in fams["rpc"].samples] == ["rpc", "rpc_sum", "rpc_count"]
|
|
|
|
|
|
def test_an_openmetrics_counter_owns_its_total_sample():
|
|
fams = prometheus.parse("# TYPE jobs counter\njobs_total 12\n")
|
|
assert list(fams) == ["jobs"]
|
|
assert fams["jobs"].type == "counter"
|
|
assert fams["jobs"].samples[0].name == "jobs_total"
|
|
|
|
|
|
def test_special_values_and_timestamps():
|
|
text = "a NaN\nb +Inf\nc -Inf\nd 1.5e3 1700000000000\n"
|
|
fams = prometheus.parse(text)
|
|
assert math.isnan(fams["a"].samples[0].value)
|
|
assert fams["b"].samples[0].value == math.inf
|
|
assert fams["c"].samples[0].value == -math.inf
|
|
assert fams["d"].samples[0].value == 1500.0
|
|
|
|
|
|
def test_malformed_lines_are_skipped_not_fatal():
|
|
text = """\
|
|
good 1
|
|
bad-name 2
|
|
unterminated{a="x 3
|
|
nolabelvalue{a=} 4
|
|
novalue
|
|
spaced{a="1"}5
|
|
# TYPE weird nonsense
|
|
weird 6
|
|
good_too{a="b"} 7
|
|
"""
|
|
fams = prometheus.parse(text)
|
|
assert set(fams) == {"good", "weird", "good_too"}
|
|
assert fams["weird"].type == "untyped"
|
|
assert fams["good_too"].samples[0].value == 7.0
|
|
|
|
|
|
def test_traefik_reads_through_the_shared_parser():
|
|
from plugins.traefik.scraper import parse_prometheus
|
|
|
|
out = parse_prometheus(
|
|
'traefik_service_requests_total{service="web@docker",code="200"} 10\n'
|
|
)
|
|
key = frozenset({("service", "web@docker"), ("code", "200")})
|
|
assert out == {"traefik_service_requests_total": {key: 10.0}}
|