From 1cd84ed977eebbbcd48eb4f30b02335a255e38f3 Mon Sep 17 00:00:00 2001 From: Shroominic Date: Fri, 21 Nov 2025 21:50:31 -0800 Subject: [PATCH] fix typing+linting --- routstr/core/admin.py | 6 +- routstr/core/log_manager.py | 154 ++++++++++++++++++++---------------- 2 files changed, 87 insertions(+), 73 deletions(-) diff --git a/routstr/core/admin.py b/routstr/core/admin.py index a2986a5a..c9357b60 100644 --- a/routstr/core/admin.py +++ b/routstr/core/admin.py @@ -1,7 +1,6 @@ import json import secrets -from collections import defaultdict -from datetime import datetime, timedelta, timezone +from datetime import datetime, timezone from pathlib import Path from fastapi import APIRouter, Depends, HTTPException, Query, Request @@ -19,8 +18,8 @@ from ..wallet import ( slow_filter_spend_proofs, ) from .db import ApiKey, ModelRow, UpstreamProviderRow, create_session -from .logging import get_logger from .log_manager import log_manager +from .logging import get_logger from .settings import SettingsService, settings logger = get_logger(__name__) @@ -2869,7 +2868,6 @@ h1 { color: #333; } """ - @admin_router.get("/api/usage/metrics", dependencies=[Depends(require_admin_api)]) async def get_usage_metrics( request: Request, diff --git a/routstr/core/log_manager.py b/routstr/core/log_manager.py index 557a349a..0916a33f 100644 --- a/routstr/core/log_manager.py +++ b/routstr/core/log_manager.py @@ -2,7 +2,7 @@ import json from collections import defaultdict from datetime import datetime, timedelta, timezone from pathlib import Path -from typing import Any, Generator, Iterator +from typing import Any, Iterator from .logging import get_logger @@ -14,14 +14,14 @@ class LogManager: self.logs_dir = logs_dir def _yield_log_entries( - self, + self, hours_back: int | None = None, specific_date: str | None = None, - reverse_files: bool = False + reverse_files: bool = False, ) -> Iterator[dict[str, Any]]: """ Yields log entries from files. - + Args: hours_back: specific number of hours to look back. specific_date: specific date string (YYYY-MM-DD) to look at. @@ -41,20 +41,22 @@ class LogManager: log_files = sorted(self.logs_dir.glob("app_*.log")) if reverse_files: log_files.reverse() - + # If we only care about hours back, we can optimize file selection if hours_back is not None: cutoff_date = datetime.now(timezone.utc) - timedelta(hours=hours_back) filtered_files = [] - for f in log_files: + for log_path in log_files: try: - file_date_str = f.stem.split("_")[1] - file_date = datetime.strptime(file_date_str, "%Y-%m-%d").replace( - tzinfo=timezone.utc - ) + file_date_str = log_path.stem.split("_")[1] + file_date = datetime.strptime( + file_date_str, "%Y-%m-%d" + ).replace(tzinfo=timezone.utc) # Include file if it's from the same day or after the cutoff day - if file_date >= cutoff_date.replace(hour=0, minute=0, second=0, microsecond=0): - filtered_files.append(f) + if file_date >= cutoff_date.replace( + hour=0, minute=0, second=0, microsecond=0 + ): + filtered_files.append(log_path) except Exception: continue log_files = filtered_files @@ -73,16 +75,18 @@ class LogManager: for line in lines: try: entry = json.loads(line.strip()) - + if cutoff_date: timestamp_str = entry.get("asctime", "") if not timestamp_str: continue - log_time = datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S") + log_time = datetime.strptime( + timestamp_str, "%Y-%m-%d %H:%M:%S" + ) log_time = log_time.replace(tzinfo=timezone.utc) if log_time < cutoff_date: continue - + yield entry except json.JSONDecodeError: continue @@ -102,33 +106,36 @@ class LogManager: Search through log files and return matching entries. """ log_entries: list[dict[str, Any]] = [] - + # Use reverse=True to get newest logs first by default # If date is specified, we only look at that file - + search_text_lower = search_text.lower() if search_text else None - + # We iterate efficiently - iterator = self._yield_log_entries(specific_date=date, reverse_files=True if not date else False) - + iterator = self._yield_log_entries( + specific_date=date, reverse_files=True if not date else False + ) + # If we are searching globally (no date), we might want to limit how far back we go? # PR 228 did: "glob("app_*.log") sorted by mtime reverse [:7]" (last 7 files) # My _yield_log_entries with reverse_files=True does all files. # Let's rely on limit to stop us. - - files_processed = 0 + # Optimization: if we are not searching by date, maybe limit to last 7 files inside _yield? # For now, let's just iterate. - + for log_data in iterator: - if not self._matches_filters(log_data, level, request_id, search_text_lower): + if not self._matches_filters( + log_data, level, request_id, search_text_lower + ): continue log_entries.append(log_data) if len(log_entries) >= limit: break - + # Sort by time descending (newest first) log_entries.sort(key=lambda x: x.get("asctime", ""), reverse=True) return log_entries @@ -152,7 +159,7 @@ class LogManager: pathname = str(log_data.get("pathname", "")).lower() if ( - search_text_lower not in message + search_text_lower not in message and search_text_lower not in name and search_text_lower not in pathname ): @@ -171,28 +178,30 @@ class LogManager: def get_error_details(self, hours: int = 24, limit: int = 100) -> dict: errors: list[dict] = [] # Iterate newest to oldest for errors? - # yield_log_entries sorts files by name (date) ascending by default. + # yield_log_entries sorts files by name (date) ascending by default. # usage stats logic usually expects ascending time for aggregation (though dictionaries don't care). # For error details "last N errors", we probably want newest first. - - # Using list() loads everything into memory, which is what PR 229 did. + + # Using list() loads everything into memory, which is what PR 229 did. # For optimization, we could use reverse iterator. - + # Let's just stick to PR 229 logic which filters 'ERROR' level. - - entries = self._yield_log_entries(hours_back=hours) # oldest to newest - + + entries = self._yield_log_entries(hours_back=hours) # oldest to newest + for entry in entries: - if entry.get("levelname", "").upper() == "ERROR": + if entry.get("levelname", "").upper() == "ERROR": timestamp_str = entry.get("asctime", "") - errors.append({ - "timestamp": timestamp_str, - "message": entry.get("message", ""), - "error_type": entry.get("error_type", "unknown"), - "pathname": entry.get("pathname", ""), - "lineno": entry.get("lineno", 0), - "request_id": entry.get("request_id", ""), - }) + errors.append( + { + "timestamp": timestamp_str, + "message": entry.get("message", ""), + "error_type": entry.get("error_type", "unknown"), + "pathname": entry.get("pathname", ""), + "lineno": entry.get("lineno", 0), + "request_id": entry.get("request_id", ""), + } + ) # Sort reverse time errors.sort(key=lambda x: x["timestamp"], reverse=True) @@ -200,7 +209,7 @@ class LogManager: def get_revenue_by_model(self, hours: int = 24, limit: int = 20) -> dict: entries = list(self._yield_log_entries(hours_back=hours)) - + model_stats: dict[str, dict[str, int | float]] = defaultdict( lambda: { "revenue_msats": 0, @@ -240,34 +249,36 @@ class LogManager: except Exception: continue - models = [] + models: list[dict[str, Any]] = [] total_revenue = 0.0 for model, stats in model_stats.items(): revenue_msats = float(stats["revenue_msats"]) refunds_msats = float(stats["refunds_msats"]) - + revenue_sats = revenue_msats / 1000 refunds_sats = refunds_msats / 1000 net_revenue_sats = revenue_sats - refunds_sats total_revenue += net_revenue_sats - + requests = int(stats["requests"]) successful = int(stats["successful"]) - - models.append({ - "model": model, - "revenue_sats": revenue_sats, - "refunds_sats": refunds_sats, - "net_revenue_sats": net_revenue_sats, - "requests": requests, - "successful": successful, - "failed": int(stats["failed"]), - "avg_revenue_per_request": ( - revenue_sats / successful if successful > 0 else 0 - ), - }) + + models.append( + { + "model": model, + "revenue_sats": revenue_sats, + "refunds_sats": refunds_sats, + "net_revenue_sats": net_revenue_sats, + "requests": requests, + "successful": successful, + "failed": int(stats["failed"]), + "avg_revenue_per_request": ( + revenue_sats / successful if successful > 0 else 0 + ), + } + ) models.sort(key=lambda x: float(x["net_revenue_sats"]), reverse=True) @@ -345,7 +356,7 @@ class LogManager: revenue_sats = stats["revenue_msats"] / 1000 refunds_sats = stats["refunds_msats"] / 1000 net_revenue_sats = revenue_sats - refunds_sats - + total_requests = stats["total_requests"] successful = stats["successful_chat_completions"] @@ -361,7 +372,9 @@ class LogManager: "unique_models_count": len(stats["unique_models"]), "unique_models": sorted(list(stats["unique_models"])), "error_types": dict(stats["error_types"]), - "success_rate": (successful / total_requests * 100) if total_requests > 0 else 0, + "success_rate": (successful / total_requests * 100) + if total_requests > 0 + else 0, "revenue_msats": stats["revenue_msats"], "refunds_msats": stats["refunds_msats"], "revenue_sats": revenue_sats, @@ -372,18 +385,18 @@ class LogManager: stats["revenue_msats"] / successful if successful > 0 else 0 ), "refund_rate": ( - (stats["failed_requests"] / total_requests * 100) if total_requests > 0 else 0 + (stats["failed_requests"] / total_requests * 100) + if total_requests > 0 + else 0 ), } def _aggregate_metrics_by_time( self, entries: list[dict], interval_minutes: int, hours_back: int ) -> dict: - time_buckets = defaultdict(lambda: { - "requests": 0, - "errors": 0, - "revenue_msats": 0.0 - }) + time_buckets: dict[str, dict[str, Any]] = defaultdict( + lambda: {"requests": 0, "errors": 0, "revenue_msats": 0.0} + ) for entry in entries: try: @@ -397,11 +410,13 @@ class LogManager: # Round down to nearest interval minutes = log_time.minute rounded_minutes = (minutes // interval_minutes) * interval_minutes - bucket_time = log_time.replace(minute=rounded_minutes, second=0, microsecond=0) + bucket_time = log_time.replace( + minute=rounded_minutes, second=0, microsecond=0 + ) bucket_key = bucket_time.strftime("%Y-%m-%d %H:%M:%S") bucket = time_buckets[bucket_key] - + message = entry.get("message", "").lower() level = entry.get("levelname", "").upper() @@ -431,4 +446,5 @@ class LogManager: "total_buckets": len(result), } + log_manager = LogManager()