KarlQuant commited on
Commit
0ce8540
Β·
verified Β·
1 Parent(s): 8a3f292

Update hub_dashboard_service.py

Browse files
Files changed (1) hide show
  1. 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 (minimal implementation)."""
369
- limit = int(request.args.get("limit", 50))
370
-
371
- # Read directly from log file
372
  logs = []
 
 
373
  pattern = str(Path(_LOG_DIR) / "*.log")
374
- files = sorted(glob.glob(pattern))
375
-
376
- for fpath in files[-3:]: # Last 3 log files
 
 
 
 
377
  try:
378
- with open(fpath, "r") as f:
379
- lines = f.readlines()[-limit:]
380
- for line in lines:
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
- return jsonify({"logs": logs[-limit:], "count": len(logs)})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 = glob.glob(pattern)
401
-
402
- total_lines = 0
 
 
 
 
 
 
 
 
 
403
  for fpath in files:
404
  try:
405
- with open(fpath, "r") as f:
406
- total_lines += sum(1 for _ in f)
407
- except Exception:
 
 
 
 
 
 
 
 
 
 
 
 
 
408
  pass
409
-
410
  return jsonify({
411
- "total_events": total_lines,
412
- "by_level": {"INFO": total_lines},
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