mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-21 03:46:37 +00:00
35af688cd9
test(obs): add metrics dimension smoke harness Co-authored-by: heihutu <heihutu@gmail.com>
226 lines
10 KiB
Python
Executable File
226 lines
10 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Read-only Prometheus smoke checks for backlog #1649 metric dimensions.
|
|
|
|
The harness queries Prometheus' instant-query API. It never writes to RustFS,
|
|
Prometheus, or the scrape targets. A check is ``metric|label=value,...``;
|
|
``--require-labels`` accepts ``metric|label1,label2``. ``--retired`` checks
|
|
that an exact label set is absent after the scheduler's retirement window.
|
|
"""
|
|
|
|
import argparse
|
|
import base64
|
|
import json
|
|
import sys
|
|
from dataclasses import dataclass
|
|
from typing import Iterable
|
|
from urllib.error import HTTPError, URLError
|
|
from urllib.parse import urlencode, urlparse
|
|
from urllib.request import Request, urlopen
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Series:
|
|
labels: dict[str, str]
|
|
|
|
|
|
def query_url(value: str) -> str:
|
|
parsed = urlparse(value)
|
|
if parsed.path.rstrip("/").endswith("/api/v1/query"):
|
|
return value
|
|
return value.rstrip("/") + "/api/v1/query"
|
|
|
|
|
|
def parse_spec(spec: str, separator: str = "|") -> tuple[str, dict[str, str]]:
|
|
metric, _, labels = spec.partition(separator)
|
|
if not metric or any(c in metric for c in "{} \t"):
|
|
raise ValueError(f"invalid metric check: {spec!r}")
|
|
expected: dict[str, str] = {}
|
|
if labels:
|
|
for pair in labels.split(","):
|
|
key, sep, value = pair.partition("=")
|
|
if not sep or not key or not value:
|
|
raise ValueError(f"invalid label selector in {spec!r}")
|
|
if key in expected:
|
|
raise ValueError(f"duplicate label {key!r} in {spec!r}")
|
|
expected[key] = value
|
|
return metric, expected
|
|
|
|
|
|
def parse_label_names(spec: str) -> tuple[str, list[str]]:
|
|
metric, separator, labels = spec.partition("|")
|
|
names = [item for item in labels.split(",") if item] if separator else []
|
|
if not metric or any(c in metric for c in "{} \t") or not names or any(
|
|
"=" in item or not item.replace("_", "a").isalnum() for item in names
|
|
):
|
|
raise ValueError(f"invalid label-name check: {spec!r}")
|
|
return metric, names
|
|
|
|
|
|
def fetch_series(endpoint: str, metric: str, headers: dict[str, str], timeout: float) -> list[Series]:
|
|
query = f"{metric}{{}}" if "{" not in metric else metric
|
|
request = Request(f"{endpoint}?{urlencode({'query': query})}", headers=headers)
|
|
try:
|
|
with urlopen(request, timeout=timeout) as response:
|
|
payload = json.load(response)
|
|
except (HTTPError, URLError, TimeoutError) as error:
|
|
raise RuntimeError(f"Prometheus query failed for {query!r}: {error}") from error
|
|
if payload.get("status") != "success":
|
|
raise RuntimeError(f"Prometheus returned non-success for {query!r}: {payload}")
|
|
data = payload.get("data", {})
|
|
if data.get("resultType") != "vector":
|
|
raise RuntimeError(f"Prometheus query did not return an instant vector: {query!r}")
|
|
return [Series(dict(item.get("metric", {}))) for item in data.get("result", [])]
|
|
|
|
|
|
def has_labels(series: Iterable[Series], expected: dict[str, str]) -> bool:
|
|
return any(all(item.labels.get(key) == value for key, value in expected.items()) for item in series)
|
|
|
|
|
|
def run(args: argparse.Namespace) -> int:
|
|
headers = {"Accept": "application/json"}
|
|
if args.bearer:
|
|
headers["Authorization"] = f"Bearer {args.bearer}"
|
|
if args.basic:
|
|
headers["Authorization"] = "Basic " + base64.b64encode(args.basic.encode()).decode()
|
|
endpoint = query_url(args.query_url)
|
|
required = list(args.require)
|
|
labels = list(args.require_labels)
|
|
retired = list(args.retired)
|
|
if args.profile == "backlog-1649":
|
|
required += [
|
|
"rustfs_system_drive_total_bytes",
|
|
"rustfs_system_drive_writes_total",
|
|
"rustfs_system_drive_deletes_total",
|
|
"rustfs_scanner_source_work_total",
|
|
"rustfs_scanner_active_bucket_drive_scans",
|
|
"rustfs_scanner_bucket_drive_result_total",
|
|
"rustfs_ilm_action_tasks",
|
|
"rustfs_ilm_tasks",
|
|
"rustfs_ilm_task_events_total",
|
|
"rustfs_ilm_queue_backpressure_total",
|
|
"rustfs_ilm_versions_scanned_by_server",
|
|
"rustfs_notification_current_send_in_progress_by_server",
|
|
"rustfs_notification_events_errors_total_by_server",
|
|
"rustfs_notification_events_sent_total_by_server",
|
|
"rustfs_notification_events_skipped_total_by_server",
|
|
"rustfs_audit_failed_messages_by_server",
|
|
"rustfs_audit_target_queue_length_by_server",
|
|
"rustfs_audit_total_messages_by_server",
|
|
"rustfs_notification_events_errors_total",
|
|
"rustfs_notification_events_sent_total",
|
|
"rustfs_notification_events_skipped_total",
|
|
"rustfs_audit_failed_messages",
|
|
"rustfs_audit_target_queue_length",
|
|
"rustfs_audit_total_messages",
|
|
]
|
|
labels += [
|
|
"rustfs_system_drive_total_bytes|server,drive",
|
|
"rustfs_system_drive_writes_total|server,drive",
|
|
"rustfs_system_drive_deletes_total|server,drive",
|
|
"rustfs_scanner_source_work_total|server,source,state",
|
|
"rustfs_scanner_active_bucket_drive_scans|server,source,bucket,drive",
|
|
"rustfs_scanner_bucket_drive_result_total|server,bucket,drive,result",
|
|
"rustfs_ilm_action_tasks|server,action,state",
|
|
"rustfs_ilm_tasks|server,action,queue_state",
|
|
"rustfs_ilm_task_events_total|server,action,result",
|
|
"rustfs_ilm_queue_backpressure_total|server,action,reason",
|
|
"rustfs_ilm_versions_scanned_by_server|server,source",
|
|
"rustfs_notification_current_send_in_progress_by_server|server",
|
|
"rustfs_notification_events_errors_total_by_server|server",
|
|
"rustfs_notification_events_sent_total_by_server|server",
|
|
"rustfs_notification_events_skipped_total_by_server|server",
|
|
"rustfs_audit_failed_messages_by_server|server,target_id",
|
|
"rustfs_audit_target_queue_length_by_server|server,target_id",
|
|
"rustfs_audit_total_messages_by_server|server,target_id",
|
|
"rustfs_audit_failed_messages|target_id",
|
|
"rustfs_audit_target_queue_length|target_id",
|
|
"rustfs_audit_total_messages|target_id",
|
|
]
|
|
if not required and not labels and not retired:
|
|
raise ValueError("provide --profile backlog-1649 or at least one check")
|
|
failures: list[str] = []
|
|
cache: dict[str, list[Series]] = {}
|
|
|
|
def get(metric: str) -> list[Series]:
|
|
if metric not in cache:
|
|
cache[metric] = fetch_series(endpoint, metric, headers, args.timeout)
|
|
return cache[metric]
|
|
|
|
def missing_servers(series: list[Series]) -> list[str]:
|
|
if not args.server or not any("server" in item.labels for item in series):
|
|
return []
|
|
observed = {item.labels["server"] for item in series if "server" in item.labels}
|
|
return sorted(set(args.server) - observed)
|
|
|
|
for spec in required:
|
|
metric, expected = parse_spec(spec)
|
|
series = get(metric)
|
|
if not series:
|
|
failures.append(f"{metric}: no series returned")
|
|
elif expected and not has_labels(series, expected):
|
|
failures.append(f"{metric}: no series has labels {expected}; observed {len(series)} series")
|
|
elif missing_servers(series):
|
|
failures.append(f"{metric}: missing requested server series {missing_servers(series)}")
|
|
for spec in labels:
|
|
metric, required_labels = parse_label_names(spec)
|
|
series = get(metric)
|
|
if not series:
|
|
failures.append(f"{metric}: aggregate series absent")
|
|
elif any(not all(label in item.labels for label in required_labels) for item in series):
|
|
failures.append(f"{metric}: at least one series is missing labels {required_labels}")
|
|
elif missing_servers(series):
|
|
failures.append(f"{metric}: missing requested server series {missing_servers(series)}")
|
|
for spec in retired:
|
|
metric, expected = parse_spec(spec)
|
|
if has_labels(get(metric), expected):
|
|
failures.append(f"{metric}: retired series still present with labels {expected}")
|
|
if failures:
|
|
for failure in failures:
|
|
print(f"FAIL: {failure}", file=sys.stderr)
|
|
return 1
|
|
print(f"PASS: {len(required)} required, {len(labels)} label, {len(retired)} retirement checks")
|
|
return 0
|
|
|
|
|
|
def self_test() -> None:
|
|
assert parse_spec("metric|server=node1,drive=d1") == ("metric", {"server": "node1", "drive": "d1"})
|
|
assert parse_label_names("metric|server,drive") == ("metric", ["server", "drive"])
|
|
assert has_labels([Series({"server": "node1", "drive": "d1"})], {"server": "node1"})
|
|
assert not has_labels([Series({"server": "node1"})], {"server": "node2"})
|
|
try:
|
|
parse_spec("metric|server")
|
|
except ValueError:
|
|
pass
|
|
else:
|
|
raise AssertionError("malformed value selector accepted")
|
|
print("PASS: self-test")
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--query-url", help="Prometheus base URL or /api/v1/query endpoint")
|
|
parser.add_argument("--profile", choices=["backlog-1649"])
|
|
parser.add_argument("--server", action="append", default=[], help="server label value required in every server-scoped check")
|
|
parser.add_argument("--require", action="append", default=[], metavar="METRIC|k=v,...")
|
|
parser.add_argument("--require-labels", action="append", default=[], metavar="METRIC|k1,k2")
|
|
parser.add_argument("--retired", action="append", default=[], metavar="METRIC|k=v,...")
|
|
parser.add_argument("--bearer")
|
|
parser.add_argument("--basic", help="username:password; prefer --bearer in shared shells")
|
|
parser.add_argument("--timeout", type=float, default=10.0)
|
|
parser.add_argument("--self-test", action="store_true")
|
|
args = parser.parse_args()
|
|
if args.self_test:
|
|
self_test()
|
|
return 0
|
|
if not args.query_url:
|
|
parser.error("--query-url is required unless --self-test is used")
|
|
try:
|
|
return run(args)
|
|
except (RuntimeError, ValueError) as error:
|
|
print(f"ERROR: {error}", file=sys.stderr)
|
|
return 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|