KarlQuant commited on
Commit
4d744eb
·
verified ·
1 Parent(s): 4e64a47

Upload 2 files

Browse files
Files changed (1) hide show
  1. 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 /app/ranker_logs/*.log"""
598
- limit = int(request.args.get("limit", 50))
599
- category = request.args.get("category") # optional filter
600
-
601
- logs = []
602
-
603
- # ── Collect the 3 newest log files ───────────────────────────────────────
604
- # FIXED: Now includes rotated files with *.log*
605
- pattern = str(Path(_LOG_DIR) / "*.log*")
606
- files = sorted(glob.glob(pattern))
607
- if not files:
608
- return jsonify({"logs": [], "count": 0, "error": f"No *.log* files in {_LOG_DIR}"})
609
-
610
- # Read enough lines from the tail of each file
611
- raw_lines = []
612
- for fpath in files[-3:]:
613
- try:
614
- with open(fpath, "r", encoding="utf-8", errors="replace") as f:
615
- lines = f.readlines()
616
- raw_lines.extend(lines[-200:])
617
- except OSError:
618
- pass
 
 
 
 
 
 
 
 
619
 
620
- # Reverse so newest is first
621
- raw_lines.reverse()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
622
 
623
- # Pre-compiled patterns for structured field extraction
624
- _CAT_RE = re.compile(r'\|\s*(INFO|DEBUG|WARNING|ERROR|CRITICAL)\s*\|\s*([A-Z_]+)\s*\|')
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
- for line in raw_lines[:limit]:
632
- if not line.strip():
633
- continue
634
- ts_match = re.search(r'\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]', line)
635
- if not ts_match:
636
- continue
 
637
 
638
- ts = ts_match.group(1)
 
 
 
 
 
639
 
640
- # Extract level AND category from the pipe-delimited format:
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
- if category:
647
- if category.upper() not in line.upper():
 
 
 
 
648
  continue
649
 
650
- # Build base entry
651
- entry = {
652
- "timestamp": ts,
653
- "level": level,
654
- "category": category_field,
655
- "message": line.strip(),
656
- "data": None,
657
- }
 
 
 
 
658
 
659
- # Parse structured data for TRAINING entries
660
- if category_field == "TRAINING":
661
- tm = _TRAINING_RE.search(line)
662
- if tm:
663
- entry["data"] = {
664
- "step": int(tm.group(1)),
665
- "loss": float(tm.group(2)),
666
- "lr": float(tm.group(3)),
667
- "asset_count": int(tm.group(4)),
668
- }
669
- else:
670
- # Fallback: try the trailing JSON blob
671
- jm = _JSON_RE.search(line)
672
- if jm:
673
- try:
674
- entry["data"] = json.loads(jm.group(1))
675
- except ValueError:
676
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
677
 
678
- logs.append(entry)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
679
 
680
- return jsonify({"logs": logs, "count": len(logs)})
 
 
 
 
 
 
 
 
 
 
 
 
681
 
682
 
683
  @app.route("/api/health")
684
  def health():
685
- return jsonify({"status": "ok", "version": "v2.2-fixed"})
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__":