Spaces:
Running
Running
Upload 2 files
Browse files- hub_dashboard_service.py +183 -74
hub_dashboard_service.py
CHANGED
|
@@ -594,95 +594,204 @@ def api_trades_closed():
|
|
| 594 |
|
| 595 |
@app.route("/api/ranker/logs/recent")
|
| 596 |
def api_logs_recent():
|
| 597 |
-
"""Get recent ranker logs — reads directly from
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 619 |
|
| 620 |
-
|
| 621 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 622 |
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
_ASSET_RE = re.compile(r'\|\s*([A-Z][A-Z0-9_]+)\s*\|', re.IGNORECASE)
|
| 626 |
-
_TRAINING_RE = re.compile(
|
| 627 |
-
r'step=(\d+)\s*\|\s*loss=([\d.]+)\s*\|\s*lr=([\d.eE+\-]+)\s*\|\s*assets=(\d+)'
|
| 628 |
-
)
|
| 629 |
-
_JSON_RE = re.compile(r'(\{.*\})\s*$')
|
| 630 |
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
|
|
|
| 637 |
|
| 638 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 639 |
|
| 640 |
-
|
| 641 |
-
# "[ts] | LEVEL | CATEGORY | ..."
|
| 642 |
-
cat_m = _CAT_RE.search(line)
|
| 643 |
-
level = cat_m.group(1) if cat_m else "INFO"
|
| 644 |
-
category_field = cat_m.group(2).strip() if cat_m else ""
|
| 645 |
|
| 646 |
-
|
| 647 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 648 |
continue
|
| 649 |
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 658 |
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 677 |
|
| 678 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 679 |
|
| 680 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 681 |
|
| 682 |
|
| 683 |
@app.route("/api/health")
|
| 684 |
def health():
|
| 685 |
-
return jsonify({"status": "ok", "version": "v2.
|
| 686 |
|
| 687 |
|
| 688 |
if __name__ == "__main__":
|
|
|
|
| 594 |
|
| 595 |
@app.route("/api/ranker/logs/recent")
|
| 596 |
def api_logs_recent():
|
| 597 |
+
"""Get recent ranker logs — reads directly from log files.
|
| 598 |
+
v2.3 FIX: wrapped in try/except so internal errors return JSON (not HTML 500).
|
| 599 |
+
Also scans multiple candidate directories so relative vs absolute paths both work.
|
| 600 |
+
"""
|
| 601 |
+
try:
|
| 602 |
+
limit = int(request.args.get("limit", 50))
|
| 603 |
+
category = request.args.get("category") # optional filter
|
| 604 |
+
|
| 605 |
+
logs = []
|
| 606 |
+
|
| 607 |
+
# ── Find log files — try several candidate directories ────────────────
|
| 608 |
+
# The ranker may write to ./ranker_logs/ (relative to its cwd) which
|
| 609 |
+
# resolves differently from the service's _LOG_DIR. We try all candidates
|
| 610 |
+
# and use the first one that contains *.log* files.
|
| 611 |
+
candidate_dirs = [
|
| 612 |
+
_LOG_DIR, # /app/ranker_logs
|
| 613 |
+
str(Path(__file__).parent / "ranker_logs"), # next to this file
|
| 614 |
+
"./ranker_logs", # cwd-relative
|
| 615 |
+
str(Path.home() / "ranker_logs"), # home dir
|
| 616 |
+
]
|
| 617 |
+
files = []
|
| 618 |
+
found_dir = None
|
| 619 |
+
for cdir in candidate_dirs:
|
| 620 |
+
pattern = str(Path(cdir) / "*.log*")
|
| 621 |
+
found = sorted(glob.glob(pattern))
|
| 622 |
+
if found:
|
| 623 |
+
files = found
|
| 624 |
+
found_dir = cdir
|
| 625 |
+
logger.debug(f"[api_logs_recent] found {len(files)} log file(s) in {cdir}")
|
| 626 |
+
break
|
| 627 |
|
| 628 |
+
if not files:
|
| 629 |
+
searched = ", ".join(candidate_dirs)
|
| 630 |
+
return jsonify({
|
| 631 |
+
"logs": [],
|
| 632 |
+
"count": 0,
|
| 633 |
+
"error": f"No *.log* files found. Searched: {searched}",
|
| 634 |
+
})
|
| 635 |
+
|
| 636 |
+
# Read enough lines from the tail of the 3 newest files
|
| 637 |
+
raw_lines = []
|
| 638 |
+
for fpath in files[-3:]:
|
| 639 |
+
try:
|
| 640 |
+
with open(fpath, "r", encoding="utf-8", errors="replace") as f:
|
| 641 |
+
lines = f.readlines()
|
| 642 |
+
raw_lines.extend(lines[-200:])
|
| 643 |
+
except OSError:
|
| 644 |
+
pass
|
| 645 |
|
| 646 |
+
# Newest first
|
| 647 |
+
raw_lines.reverse()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 648 |
|
| 649 |
+
# Pre-compiled patterns
|
| 650 |
+
_CAT_RE = re.compile(r'\|\s*(INFO|DEBUG|WARNING|ERROR|CRITICAL)\s*\|\s*([A-Z_]+)\s*\|')
|
| 651 |
+
_TRAINING_RE = re.compile(
|
| 652 |
+
r'step=(\d+)\s*\|\s*loss=([\d.]+)\s*\|\s*lr=([\d.eE+\-]+)\s*\|\s*assets=(\d+)'
|
| 653 |
+
)
|
| 654 |
+
_JSON_RE = re.compile(r'(\{.*\})\s*$')
|
| 655 |
+
_ASSET_RE = re.compile(r'\|\s*TRADE\s*\|\s*(\w+)\s*\|')
|
| 656 |
|
| 657 |
+
for line in raw_lines[:limit]:
|
| 658 |
+
if not line.strip():
|
| 659 |
+
continue
|
| 660 |
+
ts_match = re.search(r'\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]', line)
|
| 661 |
+
if not ts_match:
|
| 662 |
+
continue
|
| 663 |
|
| 664 |
+
ts = ts_match.group(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 665 |
|
| 666 |
+
# Extract level + category from "[ts] | LEVEL | CATEGORY | ..."
|
| 667 |
+
cat_m = _CAT_RE.search(line)
|
| 668 |
+
level = cat_m.group(1) if cat_m else "INFO"
|
| 669 |
+
category_field = cat_m.group(2).strip() if cat_m else ""
|
| 670 |
+
|
| 671 |
+
if category and category.upper() not in line.upper():
|
| 672 |
continue
|
| 673 |
|
| 674 |
+
# Try to extract asset name (present for SIGNAL / TRADE lines)
|
| 675 |
+
asset_m = _ASSET_RE.search(line)
|
| 676 |
+
asset = asset_m.group(1) if asset_m else None
|
| 677 |
+
|
| 678 |
+
entry = {
|
| 679 |
+
"timestamp": ts,
|
| 680 |
+
"level": level,
|
| 681 |
+
"category": category_field,
|
| 682 |
+
"message": line.strip(),
|
| 683 |
+
"asset": asset,
|
| 684 |
+
"data": None,
|
| 685 |
+
}
|
| 686 |
|
| 687 |
+
if category_field == "TRAINING":
|
| 688 |
+
tm = _TRAINING_RE.search(line)
|
| 689 |
+
if tm:
|
| 690 |
+
entry["data"] = {
|
| 691 |
+
"step": int(tm.group(1)),
|
| 692 |
+
"loss": float(tm.group(2)),
|
| 693 |
+
"lr": float(tm.group(3)),
|
| 694 |
+
"asset_count": int(tm.group(4)),
|
| 695 |
+
}
|
| 696 |
+
else:
|
| 697 |
+
# Fallback: JSON blob at end of line
|
| 698 |
+
jm = _JSON_RE.search(line)
|
| 699 |
+
if jm:
|
| 700 |
+
try:
|
| 701 |
+
blob = json.loads(jm.group(1))
|
| 702 |
+
# Normalise key names so JS always sees step/loss/lr/asset_count
|
| 703 |
+
if "step" in blob:
|
| 704 |
+
entry["data"] = {
|
| 705 |
+
"step": blob.get("step", 0),
|
| 706 |
+
"loss": blob.get("loss", 0.0),
|
| 707 |
+
"lr": blob.get("lr", 0.0),
|
| 708 |
+
"asset_count": blob.get("asset_count", blob.get("assets", 0)),
|
| 709 |
+
}
|
| 710 |
+
except (ValueError, KeyError):
|
| 711 |
+
pass
|
| 712 |
+
|
| 713 |
+
logs.append(entry)
|
| 714 |
+
|
| 715 |
+
return jsonify({
|
| 716 |
+
"logs": logs,
|
| 717 |
+
"count": len(logs),
|
| 718 |
+
"log_dir": found_dir,
|
| 719 |
+
})
|
| 720 |
+
|
| 721 |
+
except Exception as exc:
|
| 722 |
+
logger.exception(f"[api_logs_recent] unhandled error: {exc}")
|
| 723 |
+
return jsonify({"logs": [], "count": 0, "error": str(exc)}), 200
|
| 724 |
+
|
| 725 |
+
|
| 726 |
+
@app.route("/api/ranker/logs/stats")
|
| 727 |
+
def api_logs_stats():
|
| 728 |
+
"""Return aggregate stats parsed from log files.
|
| 729 |
+
Previously missing endpoint — dashboard Logs tab called this and got 404.
|
| 730 |
+
"""
|
| 731 |
+
try:
|
| 732 |
+
by_category: dict = {}
|
| 733 |
+
by_level: dict = {}
|
| 734 |
+
by_asset: dict = {}
|
| 735 |
+
errors: dict = {}
|
| 736 |
+
total = 0
|
| 737 |
+
|
| 738 |
+
candidate_dirs = [
|
| 739 |
+
_LOG_DIR,
|
| 740 |
+
str(Path(__file__).parent / "ranker_logs"),
|
| 741 |
+
"./ranker_logs",
|
| 742 |
+
str(Path.home() / "ranker_logs"),
|
| 743 |
+
]
|
| 744 |
+
files = []
|
| 745 |
+
for cdir in candidate_dirs:
|
| 746 |
+
found = sorted(glob.glob(str(Path(cdir) / "*.log*")))
|
| 747 |
+
if found:
|
| 748 |
+
files = found
|
| 749 |
+
break
|
| 750 |
+
|
| 751 |
+
_CAT_RE = re.compile(r'\|\s*(INFO|DEBUG|WARNING|ERROR|CRITICAL)\s*\|\s*([A-Z_]+)\s*\|')
|
| 752 |
+
_ASSET_RE = re.compile(r'\|\s*TRADE\s*\|\s*(\w+)\s*\|')
|
| 753 |
|
| 754 |
+
for fpath in files[-3:]:
|
| 755 |
+
try:
|
| 756 |
+
with open(fpath, "r", encoding="utf-8", errors="replace") as f:
|
| 757 |
+
for line in f:
|
| 758 |
+
if not line.strip():
|
| 759 |
+
continue
|
| 760 |
+
m = _CAT_RE.search(line)
|
| 761 |
+
if not m:
|
| 762 |
+
continue
|
| 763 |
+
level = m.group(1)
|
| 764 |
+
cat = m.group(2).strip()
|
| 765 |
+
by_level[level] = by_level.get(level, 0) + 1
|
| 766 |
+
by_category[cat] = by_category.get(cat, 0) + 1
|
| 767 |
+
total += 1
|
| 768 |
+
if level in ("ERROR", "CRITICAL"):
|
| 769 |
+
errors[cat] = errors.get(cat, 0) + 1
|
| 770 |
+
am = _ASSET_RE.search(line)
|
| 771 |
+
if am:
|
| 772 |
+
a = am.group(1)
|
| 773 |
+
by_asset[a] = by_asset.get(a, 0) + 1
|
| 774 |
+
except OSError:
|
| 775 |
+
pass
|
| 776 |
|
| 777 |
+
return jsonify({
|
| 778 |
+
"total_events": total,
|
| 779 |
+
"by_level": by_level,
|
| 780 |
+
"by_category": by_category,
|
| 781 |
+
"by_asset": by_asset,
|
| 782 |
+
"errors": errors,
|
| 783 |
+
"buffer_size": total,
|
| 784 |
+
"buffer_capacity": total,
|
| 785 |
+
})
|
| 786 |
+
except Exception as exc:
|
| 787 |
+
logger.exception(f"[api_logs_stats] error: {exc}")
|
| 788 |
+
return jsonify({"total_events": 0, "by_level": {}, "by_category": {},
|
| 789 |
+
"by_asset": {}, "errors": {}, "error": str(exc)}), 200
|
| 790 |
|
| 791 |
|
| 792 |
@app.route("/api/health")
|
| 793 |
def health():
|
| 794 |
+
return jsonify({"status": "ok", "version": "v2.3-fixed"})
|
| 795 |
|
| 796 |
|
| 797 |
if __name__ == "__main__":
|