#!/usr/bin/env python3 # -*- coding: utf-8 -*- """作业 8 · Prometheus 文本格式指标导出(:8781/metrics)。""" from __future__ import annotations import json import threading import time from http.server import BaseHTTPRequestHandler, HTTPServer from typing import Any import _bootstrap # noqa: F401 import _config as C from 作业8_sla_metrics import SLA_TARGETS, collect_sla, health_checks, load_state # Grafana 大盘展示名(label name),component 仍用英文 key 方便 PromQL COMPONENT_DISPLAY = { "dify": "Dify", "langfuse": "Langfuse", "vllm": "vLLM", "xinference": "Xinference", "vllm_health_api": "vLLM健康API", } _lock = threading.Lock() _state: dict[str, Any] = {} def refresh_state() -> dict[str, Any]: global _state with _lock: try: _state = collect_sla() except Exception as exc: _state = load_state() _state["collect_error"] = str(exc) return _state def _background_loop(interval: int) -> None: while True: try: refresh_state() except Exception: pass time.sleep(interval) def format_metrics(state: dict[str, Any]) -> str: lines: list[str] = [] health = state.get("health") or health_checks() def g(name: str, val: float, labels: str = "") -> None: lines.append("# TYPE %s gauge" % name) lines.append("%s%s %s" % (name, labels, val)) for comp, ok in health.items(): display = COMPONENT_DISPLAY.get(comp, comp) g( "ai_platform_component_up", 1.0 if ok else 0.0, '{component="%s",name="%s"}' % (comp, display), ) g("ai_platform_availability_ratio", float(state.get("availability_ratio", 0))) g("ai_sla_first_token_latency_p99_seconds", float(state.get("first_token_p99_seconds", 0))) g("ai_sla_full_response_latency_p99_seconds", float(state.get("full_response_p99_seconds", 0))) g("ai_sla_kb_accuracy_ratio", float(state.get("kb_accuracy_ratio", 0))) g("ai_sla_mttr_minutes", float(state.get("mttr_minutes", 0))) for key, val in SLA_TARGETS.items(): metric = { "availability_ratio": "ai_sla_target_availability_ratio", "first_token_p99_seconds": "ai_sla_target_first_token_seconds", "full_response_p99_seconds": "ai_sla_target_full_response_seconds", "kb_accuracy_ratio": "ai_sla_target_kb_accuracy_ratio", "mttr_minutes": "ai_sla_target_mttr_minutes", }.get(key) if metric: g(metric, float(val)) pass_map = state.get("sla_pass") or {} for k, ok in pass_map.items(): g("ai_sla_compliance", 1.0 if ok else 0.0, '{metric="%s"}' % k) lines.append("# TYPE ai_sla_exporter_info gauge") lines.append( 'ai_sla_exporter_info{version="1.0",latency_source="%s"} 1' % ((state.get("latency_meta") or {}).get("source", "unknown")) ) return "\n".join(lines) + "\n" class Handler(BaseHTTPRequestHandler): def log_message(self, fmt: str, *args: Any) -> None: pass def do_GET(self) -> None: if self.path in ("/metrics", "/metrics/"): body = format_metrics(_state or load_state()) self.send_response(200) self.send_header("Content-Type", "text/plain; version=0.0.4; charset=utf-8") self.end_headers() self.wfile.write(body.encode("utf-8")) elif self.path in ("/health", "/"): self.send_response(200) self.send_header("Content-Type", "application/json") self.end_headers() self.wfile.write(json.dumps({"status": "ok"}).encode("utf-8")) else: self.send_response(404) self.end_headers() def main() -> None: import argparse ap = argparse.ArgumentParser() ap.add_argument("--port", type=int, default=int(getattr(C, "SLA_EXPORTER_PORT", 8781))) ap.add_argument("--interval", type=int, default=60) args = ap.parse_args() refresh_state() t = threading.Thread(target=_background_loop, args=(args.interval,), daemon=True) t.start() srv = HTTPServer(("0.0.0.0", args.port), Handler) print("[OK] SLA exporter :%d/metrics interval=%ds" % (args.port, args.interval)) srv.serve_forever() if __name__ == "__main__": main()