update usage-dashboard theo flow moi cua cliproxyapi
This commit is contained in:
+88
-88
File diff suppressed because one or more lines are too long
@@ -0,0 +1,179 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
probe_cliproxy.py — Thăm dò CLIProxy để tìm command/channel hợp lệ
|
||||||
|
"""
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
|
||||||
|
HOST = "127.0.0.1"
|
||||||
|
PORT = 8317
|
||||||
|
PASSWORD = "123456"
|
||||||
|
|
||||||
|
|
||||||
|
# ── RESP helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def resp_command(*parts):
|
||||||
|
data = [f"*{len(parts)}\r\n".encode()]
|
||||||
|
for part in parts:
|
||||||
|
b = str(part).encode()
|
||||||
|
data.append(f"${len(b)}\r\n".encode())
|
||||||
|
data.append(b + b"\r\n")
|
||||||
|
return b"".join(data)
|
||||||
|
|
||||||
|
|
||||||
|
class Client:
|
||||||
|
def __init__(self, host, port, password, timeout=5):
|
||||||
|
self.sock = socket.create_connection((host, port), timeout=timeout)
|
||||||
|
self.file = self.sock.makefile("rb")
|
||||||
|
self._send("AUTH", password)
|
||||||
|
reply = self._read()
|
||||||
|
if not (isinstance(reply, str) and reply.upper() == "OK"):
|
||||||
|
raise RuntimeError(f"AUTH failed: {reply!r}")
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
try: self.file.close()
|
||||||
|
finally: self.sock.close()
|
||||||
|
|
||||||
|
def _send(self, *parts):
|
||||||
|
self.sock.sendall(resp_command(*parts))
|
||||||
|
|
||||||
|
def _read_line(self):
|
||||||
|
line = self.file.readline()
|
||||||
|
if not line:
|
||||||
|
raise EOFError("Connection closed")
|
||||||
|
return line.rstrip(b"\r\n")
|
||||||
|
|
||||||
|
def _read(self):
|
||||||
|
line = self._read_line()
|
||||||
|
prefix, payload = line[:1], line[1:]
|
||||||
|
if prefix == b"+": return payload.decode()
|
||||||
|
if prefix == b"-": raise RuntimeError(payload.decode())
|
||||||
|
if prefix == b":": return int(payload)
|
||||||
|
if prefix == b"$":
|
||||||
|
length = int(payload)
|
||||||
|
if length == -1: return None
|
||||||
|
data = self.file.read(length)
|
||||||
|
self.file.read(2)
|
||||||
|
return data.decode("utf-8", "replace")
|
||||||
|
if prefix == b"*":
|
||||||
|
count = int(payload)
|
||||||
|
if count == -1: return None
|
||||||
|
return [self._read() for _ in range(count)]
|
||||||
|
raise RuntimeError(f"Unknown RESP prefix: {line!r}")
|
||||||
|
|
||||||
|
def cmd(self, *parts):
|
||||||
|
"""Gửi command, trả về (ok, result). Không raise exception."""
|
||||||
|
self._send(*parts)
|
||||||
|
try:
|
||||||
|
return True, self._read()
|
||||||
|
except RuntimeError as e:
|
||||||
|
return False, str(e)
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"[{type(e).__name__}] {e}"
|
||||||
|
|
||||||
|
|
||||||
|
# ── Probe logic ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def section(title):
|
||||||
|
print(f"\n{'─'*55}")
|
||||||
|
print(f" {title}")
|
||||||
|
print(f"{'─'*55}")
|
||||||
|
|
||||||
|
|
||||||
|
def probe(c):
|
||||||
|
# 1. Các meta-command thường gặp
|
||||||
|
section("1. Meta commands (HELP / INFO / PING / COMMAND)")
|
||||||
|
for cmd in ["PING", "HELP", "INFO", "COMMAND", "COMMAND COUNT", "COMMAND DOCS"]:
|
||||||
|
ok, val = c.cmd(*cmd.split())
|
||||||
|
status = "✅ OK" if ok else "❌"
|
||||||
|
preview = str(val)[:120].replace("\r\n", "\\r\\n") if val else "nil"
|
||||||
|
print(f" {cmd:<20} {status} → {preview}")
|
||||||
|
|
||||||
|
# 2. Thử RPOP với các channel name phổ biến
|
||||||
|
section("2. RPOP — thử các channel name")
|
||||||
|
channels = [
|
||||||
|
"events", "queue", "logs", "usage", "requests",
|
||||||
|
"stream", "data", "codex", "proxy", "api",
|
||||||
|
"messages", "output", "items", "records",
|
||||||
|
]
|
||||||
|
valid_channels = []
|
||||||
|
for ch in channels:
|
||||||
|
ok, val = c.cmd("RPOP", ch)
|
||||||
|
if ok:
|
||||||
|
preview = str(val)[:80] if val else "nil (rỗng)"
|
||||||
|
print(f" RPOP {ch:<16} ✅ OK → {preview}")
|
||||||
|
valid_channels.append(ch)
|
||||||
|
else:
|
||||||
|
print(f" RPOP {ch:<16} ❌ {val}")
|
||||||
|
|
||||||
|
# 3. Thử LRANGE (nếu server dùng list-based)
|
||||||
|
section("3. LRANGE — xem nội dung list (không xoá)")
|
||||||
|
for ch in channels:
|
||||||
|
ok, val = c.cmd("LRANGE", ch, "0", "2")
|
||||||
|
if ok:
|
||||||
|
preview = str(val)[:100] if val else "[]"
|
||||||
|
print(f" LRANGE {ch:<14} ✅ OK → {preview}")
|
||||||
|
|
||||||
|
# 4. Thử LLEN (độ dài list)
|
||||||
|
section("4. LLEN — độ dài list")
|
||||||
|
for ch in channels:
|
||||||
|
ok, val = c.cmd("LLEN", ch)
|
||||||
|
if ok:
|
||||||
|
print(f" LLEN {ch:<16} ✅ OK → {val}")
|
||||||
|
|
||||||
|
# 5. Thử KEYS nếu server hỗ trợ
|
||||||
|
section("5. KEYS * — liệt kê tất cả key")
|
||||||
|
ok, val = c.cmd("KEYS", "*")
|
||||||
|
if ok:
|
||||||
|
print(f" ✅ KEYS * → {val}")
|
||||||
|
else:
|
||||||
|
print(f" ❌ KEYS * → {val}")
|
||||||
|
|
||||||
|
# 6. Thử SUBSCRIBE/PSUBSCRIBE (pub/sub mode)
|
||||||
|
section("6. SUBSCRIBE — thử pub/sub channels")
|
||||||
|
for ch in ["events", "logs", "usage", "requests"]:
|
||||||
|
ok, val = c.cmd("SUBSCRIBE", ch)
|
||||||
|
if ok:
|
||||||
|
print(f" SUBSCRIBE {ch:<12} ✅ OK → {val}")
|
||||||
|
else:
|
||||||
|
print(f" SUBSCRIBE {ch:<12} ❌ {val}")
|
||||||
|
|
||||||
|
# 7. Thử XREAD / XLEN (Redis Streams)
|
||||||
|
section("7. XREAD / XLEN — thử Redis Streams")
|
||||||
|
for ch in ["events", "logs", "usage"]:
|
||||||
|
ok, val = c.cmd("XLEN", ch)
|
||||||
|
if ok:
|
||||||
|
print(f" XLEN {ch:<16} ✅ OK → {val}")
|
||||||
|
else:
|
||||||
|
print(f" XLEN {ch:<16} ❌ {val}")
|
||||||
|
|
||||||
|
# 8. Tổng kết
|
||||||
|
section("TỔNG KẾT")
|
||||||
|
if valid_channels:
|
||||||
|
print(f" ✅ Channel RPOP hợp lệ: {valid_channels}")
|
||||||
|
print(f"\n → Thêm vào config.json:")
|
||||||
|
print(f' "queue_channel": "{valid_channels[0]}"')
|
||||||
|
else:
|
||||||
|
print(" ⚠️ Không tìm thấy channel RPOP hợp lệ nào.")
|
||||||
|
print(" → CLIProxy có thể dùng giao thức khác (pub/sub, stream, HTTP).")
|
||||||
|
print(" → Kiểm tra tài liệu hoặc source code của CLIProxy.")
|
||||||
|
|
||||||
|
|
||||||
|
# ── Main ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(f"Kết nối tới CLIProxy {HOST}:{PORT} ...")
|
||||||
|
try:
|
||||||
|
c = Client(HOST, PORT, PASSWORD)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Kết nối thất bại: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
print(f"✅ AUTH OK\n")
|
||||||
|
try:
|
||||||
|
probe(c)
|
||||||
|
finally:
|
||||||
|
c.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -36,6 +36,8 @@ DEFAULT_CONFIG = {
|
|||||||
"quota_refresh_seconds": 300,
|
"quota_refresh_seconds": 300,
|
||||||
"dashboard_host": "0.0.0.0",
|
"dashboard_host": "0.0.0.0",
|
||||||
"dashboard_port": 8320,
|
"dashboard_port": 8320,
|
||||||
|
"queue_channel": "usage", # CLIProxy dùng pub/sub trên channel này
|
||||||
|
"collector_mode": "subscribe", # "subscribe" (pub/sub) hoặc "rpop" (queue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -193,10 +195,39 @@ class RespClient:
|
|||||||
return [self.read() for _ in range(count)]
|
return [self.read() for _ in range(count)]
|
||||||
raise RuntimeError(f"Unknown RESP prefix: {line!r}")
|
raise RuntimeError(f"Unknown RESP prefix: {line!r}")
|
||||||
|
|
||||||
def rpop(self, count=100):
|
def cmd(self, *parts):
|
||||||
|
"""Gửi bất kỳ command nào và trả về response (không raise lỗi)."""
|
||||||
|
self.send(*parts)
|
||||||
|
try:
|
||||||
|
return self.read()
|
||||||
|
except RuntimeError as e:
|
||||||
|
return f"ERR: {e}"
|
||||||
|
|
||||||
|
def subscribe(self, channel="usage"):
|
||||||
|
"""Gửi SUBSCRIBE và đọc reply xác nhận."""
|
||||||
|
self.send("SUBSCRIBE", channel)
|
||||||
|
reply = self.read() # ['subscribe', channel, 1]
|
||||||
|
return reply
|
||||||
|
|
||||||
|
def read_message(self):
|
||||||
|
"""
|
||||||
|
Đọc 1 pub/sub message. Trả về data string nếu là message thật,
|
||||||
|
None nếu là subscribe confirmation hay message hệ thống.
|
||||||
|
Format RESP: ['message', channel, data]
|
||||||
|
"""
|
||||||
|
msg = self.read()
|
||||||
|
if not isinstance(msg, list) or len(msg) < 3:
|
||||||
|
return None
|
||||||
|
kind = msg[0] if isinstance(msg[0], str) else ""
|
||||||
|
if kind == "message":
|
||||||
|
return msg[2] # data payload
|
||||||
|
return None # 'subscribe'/'unsubscribe'/ping
|
||||||
|
|
||||||
|
def rpop(self, count=100, channel="usage"):
|
||||||
|
"""Fallback RPOP — dùng khi server hỗ trợ queue mode."""
|
||||||
result = []
|
result = []
|
||||||
for _ in range(count):
|
for _ in range(count):
|
||||||
self.send("RPOP", "queue")
|
self.send("RPOP", channel)
|
||||||
item = self.read()
|
item = self.read()
|
||||||
if item is None:
|
if item is None:
|
||||||
break
|
break
|
||||||
@@ -374,8 +405,10 @@ def epoch_to_local(value):
|
|||||||
|
|
||||||
|
|
||||||
def debug_collect():
|
def debug_collect():
|
||||||
"""Test one poll cycle and print raw queue items — does NOT write to DB."""
|
"""Test pub/sub subscribe, nhận tối đa 5 event rồi thoát — không ghi DB."""
|
||||||
|
import select as _select
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
|
channel = cfg.get("queue_channel", "usage")
|
||||||
print(f"[debug] Config: {cfg}", flush=True)
|
print(f"[debug] Config: {cfg}", flush=True)
|
||||||
print(f"[debug] Connecting to {cfg['cliproxy_host']}:{cfg['cliproxy_port']} ...", flush=True)
|
print(f"[debug] Connecting to {cfg['cliproxy_host']}:{cfg['cliproxy_port']} ...", flush=True)
|
||||||
try:
|
try:
|
||||||
@@ -385,45 +418,68 @@ def debug_collect():
|
|||||||
return
|
return
|
||||||
print("[debug] AUTH OK", flush=True)
|
print("[debug] AUTH OK", flush=True)
|
||||||
try:
|
try:
|
||||||
raw_items = client.rpop(10)
|
reply = client.subscribe(channel)
|
||||||
print(f"[debug] rpop returned {len(raw_items)} item(s)", flush=True)
|
print(f"[debug] SUBSCRIBE {channel!r} → {reply}", flush=True)
|
||||||
for i, raw in enumerate(raw_items):
|
print(f"[debug] Chờ event từ CLIProxy (timeout 15s, tối đa 5 event)...", flush=True)
|
||||||
print(f"[debug] item[{i}]: {raw[:300]}", flush=True)
|
client.sock.settimeout(None) # blocking, dùng select
|
||||||
|
|
||||||
|
received = 0
|
||||||
|
deadline = time.time() + 15
|
||||||
|
while received < 5:
|
||||||
|
remaining = deadline - time.time()
|
||||||
|
if remaining <= 0:
|
||||||
|
print("[debug] Timeout — không có event nào trong 15s.", flush=True)
|
||||||
|
print("[debug] Hãy thực hiện 1 request qua CLIProxy rồi chạy lại.", flush=True)
|
||||||
|
break
|
||||||
|
readable, _, _ = _select.select([client.sock], [], [], remaining)
|
||||||
|
if not readable:
|
||||||
|
print("[debug] Timeout — không có event nào trong 15s.", flush=True)
|
||||||
|
print("[debug] Hãy thực hiện 1 request qua CLIProxy rồi chạy lại.", flush=True)
|
||||||
|
break
|
||||||
|
data = client.read_message()
|
||||||
|
if data is None:
|
||||||
|
continue # confirmation frame
|
||||||
|
# Bỏ qua heartbeat
|
||||||
try:
|
try:
|
||||||
payload = json.loads(raw)
|
obj = json.loads(data)
|
||||||
print(f"[debug] parsed OK — keys: {list(payload.keys())}", flush=True)
|
if isinstance(obj, dict) and set(obj.keys()) == {"support_refresh"}:
|
||||||
|
print(f"[debug] (heartbeat nhận được, đang chờ event thật...)", flush=True)
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
received += 1
|
||||||
|
print(f"[debug] event[{received}] raw: {data[:300]}", flush=True)
|
||||||
|
try:
|
||||||
|
payload = json.loads(data)
|
||||||
|
print(f"[debug] keys: {list(payload.keys())}", flush=True)
|
||||||
tokens = payload.get("tokens") or {}
|
tokens = payload.get("tokens") or {}
|
||||||
print(f"[debug] tokens: {tokens}", flush=True)
|
print(f"[debug] model={payload.get('model')} source={payload.get('source')} tokens={tokens}", flush=True)
|
||||||
print(f"[debug] model: {payload.get('model')}, source: {payload.get('source')}", flush=True)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[debug] parse error: {e}", file=sys.stderr, flush=True)
|
print(f"[debug] parse error: {e}", file=sys.stderr, flush=True)
|
||||||
|
if received == 0:
|
||||||
|
print("[debug] Không nhận được event nào.", flush=True)
|
||||||
|
else:
|
||||||
|
print(f"[debug] Nhận được {received} event(s) thành công ✅", flush=True)
|
||||||
finally:
|
finally:
|
||||||
client.close()
|
client.close()
|
||||||
if not raw_items:
|
|
||||||
print("[debug] Queue is EMPTY — no events to collect. Make sure CLIProxy is running and receiving requests.", flush=True)
|
|
||||||
|
|
||||||
|
|
||||||
def collect_forever():
|
def collect_forever():
|
||||||
init_db()
|
init_db()
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
last_quota = 0
|
channel = cfg.get("queue_channel", "usage")
|
||||||
print(f"collector: connecting to {cfg['cliproxy_host']}:{cfg['cliproxy_port']}", flush=True)
|
mode = cfg.get("collector_mode", "subscribe")
|
||||||
|
print(f"collector: mode={mode!r} channel={channel!r} → {cfg['cliproxy_host']}:{cfg['cliproxy_port']}", flush=True)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
client = RespClient(cfg["cliproxy_host"], cfg["cliproxy_port"], cfg["management_key"])
|
client = RespClient(cfg["cliproxy_host"], cfg["cliproxy_port"], cfg["management_key"])
|
||||||
print("collector: connected and authenticated OK", flush=True)
|
print("collector: connected and authenticated OK", flush=True)
|
||||||
try:
|
try:
|
||||||
while True:
|
if mode == "subscribe":
|
||||||
raw_items = client.rpop(100)
|
_collect_subscribe(client, cfg, channel)
|
||||||
if raw_items:
|
else:
|
||||||
print(f"collector: got {len(raw_items)} raw item(s) from queue", flush=True)
|
_collect_rpop(client, cfg, channel)
|
||||||
inserted = insert_usage(raw_items)
|
|
||||||
print(f"collector: inserted {inserted}/{len(raw_items)} events into DB", flush=True)
|
|
||||||
now = time.time()
|
|
||||||
# if now - last_quota >= cfg["quota_refresh_seconds"]:
|
|
||||||
# refresh_quota(force=True)
|
|
||||||
# last_quota = now
|
|
||||||
time.sleep(cfg["poll_interval_seconds"])
|
|
||||||
finally:
|
finally:
|
||||||
client.close()
|
client.close()
|
||||||
print("collector: connection closed", flush=True)
|
print("collector: connection closed", flush=True)
|
||||||
@@ -435,6 +491,73 @@ def collect_forever():
|
|||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_subscribe(client, cfg, channel):
|
||||||
|
"""Collector dùng pub/sub SUBSCRIBE — nhận event theo thời gian thực.
|
||||||
|
Dùng select() thay vì socket.settimeout() để tránh OSError trên makefile.
|
||||||
|
"""
|
||||||
|
import select as _select
|
||||||
|
|
||||||
|
reply = client.subscribe(channel)
|
||||||
|
print(f"collector: SUBSCRIBE {channel!r} → {reply}", flush=True)
|
||||||
|
|
||||||
|
# Để socket ở blocking mode (không settimeout) — dùng select để poll
|
||||||
|
client.sock.settimeout(None)
|
||||||
|
|
||||||
|
batch = []
|
||||||
|
last_flush = time.time()
|
||||||
|
SELECT_TIMEOUT = 2.0 # giây
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# Chờ có dữ liệu đến trong SELECT_TIMEOUT giây
|
||||||
|
readable, _, exceptional = _select.select(
|
||||||
|
[client.sock], [], [client.sock], SELECT_TIMEOUT
|
||||||
|
)
|
||||||
|
|
||||||
|
if exceptional:
|
||||||
|
raise EOFError("socket error detected via select")
|
||||||
|
|
||||||
|
if readable:
|
||||||
|
try:
|
||||||
|
data = client.read_message()
|
||||||
|
except EOFError:
|
||||||
|
raise # kết nối đứt → vòng ngoài reconnect
|
||||||
|
except Exception as e:
|
||||||
|
print(f"collector: read_message error: {e}", file=sys.stderr, flush=True)
|
||||||
|
data = None
|
||||||
|
|
||||||
|
if data is not None:
|
||||||
|
# Lọc heartbeat {"support_refresh":true}
|
||||||
|
stripped = data.strip()
|
||||||
|
if stripped.startswith("{"):
|
||||||
|
try:
|
||||||
|
obj = json.loads(stripped)
|
||||||
|
if isinstance(obj, dict) and set(obj.keys()) == {"support_refresh"}:
|
||||||
|
continue # bỏ qua heartbeat
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
batch.append(data)
|
||||||
|
|
||||||
|
# Flush batch sau mỗi 50 item hoặc mỗi poll_interval giây
|
||||||
|
now = time.time()
|
||||||
|
if batch and (len(batch) >= 50 or now - last_flush >= cfg["poll_interval_seconds"]):
|
||||||
|
print(f"collector: flushing {len(batch)} event(s) from pub/sub", flush=True)
|
||||||
|
inserted = insert_usage(batch)
|
||||||
|
print(f"collector: inserted {inserted}/{len(batch)} events into DB", flush=True)
|
||||||
|
batch.clear()
|
||||||
|
last_flush = now
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_rpop(client, cfg, channel):
|
||||||
|
"""Collector dùng RPOP (fallback cho queue mode)."""
|
||||||
|
while True:
|
||||||
|
raw_items = client.rpop(100, channel=channel)
|
||||||
|
if raw_items:
|
||||||
|
print(f"collector: got {len(raw_items)} item(s) via RPOP {channel!r}", flush=True)
|
||||||
|
inserted = insert_usage(raw_items)
|
||||||
|
print(f"collector: inserted {inserted}/{len(raw_items)} events into DB", flush=True)
|
||||||
|
time.sleep(cfg["poll_interval_seconds"])
|
||||||
|
|
||||||
|
|
||||||
def range_bounds(name):
|
def range_bounds(name):
|
||||||
now = dt.datetime.now(LOCAL_TZ)
|
now = dt.datetime.now(LOCAL_TZ)
|
||||||
if name == "5h":
|
if name == "5h":
|
||||||
|
|||||||
Reference in New Issue
Block a user