Spaces:
Running
Running
Update hub_dashboard_service.py
Browse files- hub_dashboard_service.py +113 -35
hub_dashboard_service.py
CHANGED
|
@@ -365,54 +365,132 @@ def api_trades_closed():
|
|
| 365 |
|
| 366 |
@app.route("/api/ranker/logs/recent")
|
| 367 |
def api_logs_recent():
|
| 368 |
-
"""Get recent ranker logs
|
| 369 |
-
limit
|
| 370 |
-
|
| 371 |
-
|
| 372 |
logs = []
|
|
|
|
|
|
|
| 373 |
pattern = str(Path(_LOG_DIR) / "*.log")
|
| 374 |
-
files
|
| 375 |
-
|
| 376 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
try:
|
| 378 |
-
with open(fpath, "r") as f:
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
# Parse log line into structured format
|
| 382 |
-
match = re.match(r'\[([^\]]+)\] \| (\w+) \| (\w+) \| (.*)', line.strip())
|
| 383 |
-
if match:
|
| 384 |
-
logs.append({
|
| 385 |
-
"ts": match.group(1),
|
| 386 |
-
"level": match.group(2),
|
| 387 |
-
"category": match.group(3),
|
| 388 |
-
"message": match.group(4),
|
| 389 |
-
})
|
| 390 |
-
except Exception:
|
| 391 |
pass
|
| 392 |
-
|
| 393 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
|
| 395 |
|
| 396 |
@app.route("/api/ranker/logs/stats")
|
| 397 |
def api_logs_stats():
|
| 398 |
-
"""Get log statistics."""
|
| 399 |
pattern = str(Path(_LOG_DIR) / "*.log")
|
| 400 |
-
files
|
| 401 |
-
|
| 402 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
for fpath in files:
|
| 404 |
try:
|
| 405 |
-
with open(fpath, "r") as f:
|
| 406 |
-
|
| 407 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 408 |
pass
|
| 409 |
-
|
| 410 |
return jsonify({
|
| 411 |
-
"total_events":
|
| 412 |
-
"by_level":
|
| 413 |
-
"by_category":
|
| 414 |
-
"by_asset":
|
| 415 |
-
"errors":
|
|
|
|
|
|
|
| 416 |
})
|
| 417 |
|
| 418 |
|
|
|
|
| 365 |
|
| 366 |
@app.route("/api/ranker/logs/recent")
|
| 367 |
def api_logs_recent():
|
| 368 |
+
"""Get recent ranker logs β reads directly from /app/ranker_logs/*.log"""
|
| 369 |
+
limit = int(request.args.get("limit", 50))
|
| 370 |
+
category = request.args.get("category") # optional filter
|
| 371 |
+
|
| 372 |
logs = []
|
| 373 |
+
|
| 374 |
+
# ββ Collect the 3 newest log files βββββββββββββββββββββββββββββββββββββββ
|
| 375 |
pattern = str(Path(_LOG_DIR) / "*.log")
|
| 376 |
+
files = sorted(glob.glob(pattern))
|
| 377 |
+
if not files:
|
| 378 |
+
return jsonify({"logs": [], "count": 0, "error": f"No *.log files in {_LOG_DIR}"})
|
| 379 |
+
|
| 380 |
+
# Read enough lines from the tail of each file
|
| 381 |
+
raw_lines = []
|
| 382 |
+
for fpath in files[-3:]:
|
| 383 |
try:
|
| 384 |
+
with open(fpath, "r", encoding="utf-8", errors="replace") as f:
|
| 385 |
+
raw_lines.extend(f.readlines())
|
| 386 |
+
except OSError:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
pass
|
| 388 |
+
|
| 389 |
+
# Keep only the last `limit` lines before parsing so we don't over-parse
|
| 390 |
+
raw_lines = raw_lines[-limit * 3:]
|
| 391 |
+
|
| 392 |
+
# ββ Log line format (from ranker_logging.py to_file_line) βββββββββββββββ
|
| 393 |
+
# WITH asset:
|
| 394 |
+
# [2026-03-30 17:14:35] | INFO | TRADE | V100_1s | TRADE OPENED | ID=β¦ | β¦
|
| 395 |
+
# WITHOUT asset:
|
| 396 |
+
# [2026-03-30 17:14:35] | DEBUG | RANKING | rankings | top=β¦ | β¦
|
| 397 |
+
#
|
| 398 |
+
# Padding: level=8 chars, category=15 chars β use \s+ / \s* to absorb them.
|
| 399 |
+
#
|
| 400 |
+
# Strategy: capture everything after the category into `rest`, then decide
|
| 401 |
+
# whether the first token of `rest` is a known short asset id or the start
|
| 402 |
+
# of the message text.
|
| 403 |
+
|
| 404 |
+
# Categories that always carry an asset field
|
| 405 |
+
ASSET_CATEGORIES = {"TRADE", "SIGNAL", "PROCESSING"}
|
| 406 |
+
|
| 407 |
+
LINE_RE = re.compile(
|
| 408 |
+
r'^\[([^\]]+)\] \| ' # [timestamp]
|
| 409 |
+
r'(\w+)\s*\| ' # LEVEL (padded) |
|
| 410 |
+
r'(\w+)\s*' # CATEGORY (padded, no trailing | yet)
|
| 411 |
+
r'(?:\| (\S+) )?\| ' # optional | asset |
|
| 412 |
+
r'(.*)' # rest of message
|
| 413 |
+
)
|
| 414 |
+
|
| 415 |
+
for line in raw_lines:
|
| 416 |
+
line = line.rstrip('\n')
|
| 417 |
+
m = LINE_RE.match(line)
|
| 418 |
+
if not m:
|
| 419 |
+
continue
|
| 420 |
+
|
| 421 |
+
ts_str, level, cat, asset, message = m.groups()
|
| 422 |
+
level = level.strip()
|
| 423 |
+
cat = cat.strip()
|
| 424 |
+
|
| 425 |
+
# When the optional asset group matched something that is NOT a real
|
| 426 |
+
# asset (e.g. the word "rankings" on a RANKING line), pull it back
|
| 427 |
+
# into the message.
|
| 428 |
+
if asset and cat not in ASSET_CATEGORIES:
|
| 429 |
+
message = f"{asset} | {message}" if message else asset
|
| 430 |
+
asset = None
|
| 431 |
+
|
| 432 |
+
entry = {
|
| 433 |
+
"ts": ts_str,
|
| 434 |
+
"level": level,
|
| 435 |
+
"category": cat,
|
| 436 |
+
"message": message or "",
|
| 437 |
+
}
|
| 438 |
+
if asset:
|
| 439 |
+
entry["asset"] = asset
|
| 440 |
+
|
| 441 |
+
if category and cat != category:
|
| 442 |
+
continue
|
| 443 |
+
|
| 444 |
+
logs.append(entry)
|
| 445 |
+
|
| 446 |
+
logs = logs[-limit:]
|
| 447 |
+
return jsonify({"logs": logs, "count": len(logs)})
|
| 448 |
|
| 449 |
|
| 450 |
@app.route("/api/ranker/logs/stats")
|
| 451 |
def api_logs_stats():
|
| 452 |
+
"""Get real log statistics β counts by level, category and asset."""
|
| 453 |
pattern = str(Path(_LOG_DIR) / "*.log")
|
| 454 |
+
files = glob.glob(pattern)
|
| 455 |
+
|
| 456 |
+
total = 0
|
| 457 |
+
by_level: Dict[str, int] = defaultdict(int)
|
| 458 |
+
by_cat: Dict[str, int] = defaultdict(int)
|
| 459 |
+
by_asset: Dict[str, int] = defaultdict(int)
|
| 460 |
+
errors: Dict[str, int] = defaultdict(int)
|
| 461 |
+
|
| 462 |
+
STAT_RE = re.compile(
|
| 463 |
+
r'^\[([^\]]+)\] \| (\w+)\s*\| (\w+)\s*(?:\| (\S+) )?\|'
|
| 464 |
+
)
|
| 465 |
+
|
| 466 |
for fpath in files:
|
| 467 |
try:
|
| 468 |
+
with open(fpath, "r", encoding="utf-8", errors="replace") as f:
|
| 469 |
+
for line in f:
|
| 470 |
+
m = STAT_RE.match(line)
|
| 471 |
+
if not m:
|
| 472 |
+
continue
|
| 473 |
+
_, level, cat, asset = m.groups()
|
| 474 |
+
level = level.strip()
|
| 475 |
+
cat = cat.strip()
|
| 476 |
+
total += 1
|
| 477 |
+
by_level[level] += 1
|
| 478 |
+
by_cat[cat] += 1
|
| 479 |
+
if asset:
|
| 480 |
+
by_asset[asset] += 1
|
| 481 |
+
if level in ("ERROR", "CRITICAL"):
|
| 482 |
+
errors[cat] += 1
|
| 483 |
+
except OSError:
|
| 484 |
pass
|
| 485 |
+
|
| 486 |
return jsonify({
|
| 487 |
+
"total_events": total,
|
| 488 |
+
"by_level": dict(by_level),
|
| 489 |
+
"by_category": dict(by_cat),
|
| 490 |
+
"by_asset": dict(by_asset),
|
| 491 |
+
"errors": dict(errors),
|
| 492 |
+
"log_files": len(files),
|
| 493 |
+
"log_dir": str(_LOG_DIR),
|
| 494 |
})
|
| 495 |
|
| 496 |
|