"""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}}