Merge pull request #172 from openqj/feat-metrics-extended
feat: /metrics 可观测性扩展
This commit is contained in:
2 files changed
+62
-2
No files matched your search
+55
-2
@@ -8224,16 +8224,33 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
|||||||
done = by_status.get("completed", 0) + by_status.get("merged", 0)
|
done = by_status.get("completed", 0) + by_status.get("merged", 0)
|
||||||
failed = by_status.get("failed", 0) + by_status.get("merge-failed", 0)
|
failed = by_status.get("failed", 0) + by_status.get("merge-failed", 0)
|
||||||
finished = done + failed
|
finished = done + failed
|
||||||
|
# 新增可观测字段(只加键,旧键语义不动):队列深度、
|
||||||
|
# 近 24h 失败数、跑超 300 秒的 running 任务数。
|
||||||
|
now = time.time()
|
||||||
|
failures_last_24h = 0
|
||||||
|
running_over_300s = 0
|
||||||
|
for row in rows:
|
||||||
|
if row["project_id"] not in member_ids:
|
||||||
|
continue
|
||||||
|
if row["status"] in ("failed", "merge-failed") \
|
||||||
|
and row["finished_at"] \
|
||||||
|
and now - row["finished_at"] <= 86400:
|
||||||
|
failures_last_24h += 1
|
||||||
|
if row["status"] == "running" and row["started_at"] \
|
||||||
|
and now - row["started_at"] > 300:
|
||||||
|
running_over_300s += 1
|
||||||
try:
|
try:
|
||||||
cost_rows = conn.execute(
|
cost_rows = conn.execute(
|
||||||
"SELECT project_id, source, total_tokens, usd_micros,"
|
"SELECT project_id, model, source, total_tokens,"
|
||||||
" fallback_count FROM cost_records"
|
" usd_micros, fallback_count FROM cost_records"
|
||||||
).fetchall()
|
).fetchall()
|
||||||
except (sqlite3.OperationalError, _PgOperationalError):
|
except (sqlite3.OperationalError, _PgOperationalError):
|
||||||
cost_rows = []
|
cost_rows = []
|
||||||
actual_tokens = estimated_tokens = 0
|
actual_tokens = estimated_tokens = 0
|
||||||
actual_usd = estimated_usd = 0
|
actual_usd = estimated_usd = 0
|
||||||
fallback_total = 0
|
fallback_total = 0
|
||||||
|
by_model = {}
|
||||||
|
unpriced_tokens = 0
|
||||||
for row in cost_rows:
|
for row in cost_rows:
|
||||||
if row["project_id"] not in member_ids:
|
if row["project_id"] not in member_ids:
|
||||||
continue
|
continue
|
||||||
@@ -8244,8 +8261,26 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
|||||||
estimated_tokens += row["total_tokens"] or 0
|
estimated_tokens += row["total_tokens"] or 0
|
||||||
estimated_usd += row["usd_micros"] or 0
|
estimated_usd += row["usd_micros"] or 0
|
||||||
fallback_total += row["fallback_count"] or 0
|
fallback_total += row["fallback_count"] or 0
|
||||||
|
model = row["model"] or "unknown"
|
||||||
|
entry = by_model.setdefault(
|
||||||
|
model, {"model": model, "total_tokens": 0,
|
||||||
|
"usd_micros": 0, "priced": False})
|
||||||
|
entry["total_tokens"] += row["total_tokens"] or 0
|
||||||
|
if row["usd_micros"] is not None:
|
||||||
|
entry["usd_micros"] += row["usd_micros"]
|
||||||
|
entry["priced"] = True
|
||||||
|
else:
|
||||||
|
unpriced_tokens += row["total_tokens"] or 0
|
||||||
|
all_tokens = actual_tokens + estimated_tokens
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
try:
|
||||||
|
workers = _registry_list(self.server.db_path)
|
||||||
|
except Exception: # noqa: BLE001 - 观测端点不因 worker 表失败
|
||||||
|
workers = []
|
||||||
|
workers_online = sum(1 for w in workers if w.get("alive"))
|
||||||
|
worker_ages = [now - w["last_seen"] for w in workers
|
||||||
|
if w.get("last_seen")]
|
||||||
self._send_json(200, {
|
self._send_json(200, {
|
||||||
"total_tasks": total,
|
"total_tasks": total,
|
||||||
"by_status": by_status,
|
"by_status": by_status,
|
||||||
@@ -8258,6 +8293,24 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
|
|||||||
"estimated_tokens": estimated_tokens,
|
"estimated_tokens": estimated_tokens,
|
||||||
"actual_usd_micros": actual_usd,
|
"actual_usd_micros": actual_usd,
|
||||||
"estimated_usd_micros": estimated_usd,
|
"estimated_usd_micros": estimated_usd,
|
||||||
|
"unpriced_tokens": unpriced_tokens,
|
||||||
|
"unpriced_share": (
|
||||||
|
unpriced_tokens / all_tokens) if all_tokens else None,
|
||||||
|
"by_model": sorted(by_model.values(),
|
||||||
|
key=lambda e: -e["total_tokens"]),
|
||||||
|
},
|
||||||
|
"queue_depth": {
|
||||||
|
"queued": by_status.get("queued", 0),
|
||||||
|
"running": by_status.get("running", 0),
|
||||||
|
},
|
||||||
|
"failures_last_24h": failures_last_24h,
|
||||||
|
"running_over_300s": running_over_300s,
|
||||||
|
"workers": {
|
||||||
|
"online": workers_online,
|
||||||
|
"offline": len(workers) - workers_online,
|
||||||
|
"total": len(workers),
|
||||||
|
"max_last_seen_age_seconds": (
|
||||||
|
max(worker_ages) if worker_ages else None),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -287,6 +287,13 @@ class MetricsAndBackupTestCase(unittest.TestCase):
|
|||||||
self.assertEqual(metrics["fallback_total"], 1)
|
self.assertEqual(metrics["fallback_total"], 1)
|
||||||
self.assertEqual(metrics["cost"]["actual_tokens"], 15)
|
self.assertEqual(metrics["cost"]["actual_tokens"], 15)
|
||||||
self.assertEqual(metrics["cost"]["estimated_tokens"], 20)
|
self.assertEqual(metrics["cost"]["estimated_tokens"], 20)
|
||||||
|
# 新增观测键:队列深度、worker 聚合、分模型、未定价占比。
|
||||||
|
self.assertEqual(metrics["queue_depth"], {"queued": 0, "running": 0})
|
||||||
|
self.assertEqual(metrics["workers"]["total"], 0)
|
||||||
|
self.assertEqual(metrics["failures_last_24h"], 0)
|
||||||
|
models = {m["model"]: m for m in metrics["cost"]["by_model"]}
|
||||||
|
self.assertEqual(models["gpt-6.1-sol"]["total_tokens"], 35)
|
||||||
|
self.assertIsNotNone(metrics["cost"]["unpriced_share"])
|
||||||
# 无 key 401。
|
# 无 key 401。
|
||||||
status, _ = self._req("GET", "/metrics")
|
status, _ = self._req("GET", "/metrics")
|
||||||
self.assertEqual(status, 401)
|
self.assertEqual(status, 401)
|
||||||
|
|||||||
Reference in new issue
Block a user