File size: 7,453 Bytes
cb505ff | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 | import json
import os
import re
from datetime import datetime, timezone
from typing import Dict, Any, List, Optional
_CURRENT_RUN_NUMBER: Optional[int] = None
DEFAULT_MAX_RUNS = 10
def _get_default_logs_dir() -> str:
"""Return the absolute path to the backend/logs directory."""
utils_dir = os.path.dirname(os.path.abspath(__file__))
backend_dir = os.path.dirname(os.path.dirname(utils_dir))
return os.path.join(backend_dir, "logs")
def _read_counter(counter_file: str) -> Optional[int]:
"""Read the last run number from the counter file if it exists."""
if not os.path.exists(counter_file):
return None
try:
with open(counter_file, "r", encoding="utf-8") as f:
content = f.read().strip()
if content.isdigit():
return int(content)
except Exception:
pass
return None
def _find_max_existing_run(logs_dir: str) -> int:
"""Scan existing run_*.log files to find the highest run number."""
if not os.path.exists(logs_dir):
return 0
pattern = re.compile(r"^run_(\d+)\.log$")
max_num = 0
for name in os.listdir(logs_dir):
match = pattern.match(name)
if match:
max_num = max(max_num, int(match.group(1)))
return max_num
def prune_old_runs(logs_dir: str, max_runs: int = DEFAULT_MAX_RUNS) -> List[str]:
"""
Ensure only the latest `max_runs` run files are kept.
Deletes the oldest run_*.log files exceeding the limit.
Returns the list of deleted filenames.
"""
if not os.path.exists(logs_dir) or max_runs <= 0:
return []
pattern = re.compile(r"^run_(\d+)\.log$")
run_files = []
for name in os.listdir(logs_dir):
match = pattern.match(name)
if match:
run_files.append((int(match.group(1)), name))
# Sort ascending by run number
run_files.sort(key=lambda x: x[0])
deleted = []
if len(run_files) > max_runs:
excess_count = len(run_files) - max_runs
to_delete = run_files[:excess_count]
for _, name in to_delete:
file_path = os.path.join(logs_dir, name)
try:
os.remove(file_path)
deleted.append(name)
except Exception:
pass
_prune_history_file(logs_dir, max_runs)
return deleted
def _prune_history_file(logs_dir: str, max_runs: int):
"""Keep only the last `max_runs` entries in run_history.json."""
history_file = os.path.join(logs_dir, "run_history.json")
if not os.path.exists(history_file):
return
try:
with open(history_file, "r", encoding="utf-8") as f:
history = json.load(f)
if isinstance(history, list) and len(history) > max_runs:
history = history[-max_runs:]
with open(history_file, "w", encoding="utf-8") as f:
json.dump(history, f, indent=2)
except Exception:
pass
def _record_run_history(logs_dir: str, run_number: int, log_file: str, max_runs: int):
"""Append a record for this run in run_history.json and prune older entries."""
history_file = os.path.join(logs_dir, "run_history.json")
history = []
if os.path.exists(history_file):
try:
with open(history_file, "r", encoding="utf-8") as f:
loaded = json.load(f)
if isinstance(loaded, list):
history = loaded
except Exception:
history = []
history.append({
"run_number": run_number,
"timestamp": datetime.now(timezone.utc).isoformat(),
"log_file": os.path.basename(log_file),
})
if len(history) > max_runs:
history = history[-max_runs:]
try:
with open(history_file, "w", encoding="utf-8") as f:
json.dump(history, f, indent=2)
except Exception:
pass
def initialize_run(
logs_dir: Optional[str] = None,
max_runs: Optional[int] = None,
force_new: bool = False,
) -> int:
"""
Initialize the run for this backend execution:
- Increments the persistent run counter by 1 (or starts at 1).
- Prunes older runs so only the last `max_runs` (default 10) are kept.
- Creates/touches the log file for this run.
- Caches the current run number for the process.
"""
global _CURRENT_RUN_NUMBER
if _CURRENT_RUN_NUMBER is not None and not force_new:
return _CURRENT_RUN_NUMBER
if logs_dir is None:
logs_dir = _get_default_logs_dir()
if max_runs is None:
env_max = os.getenv("MAX_LOG_RUNS", "").strip()
max_runs = int(env_max) if env_max.isdigit() else DEFAULT_MAX_RUNS
os.makedirs(logs_dir, exist_ok=True)
counter_file = os.path.join(logs_dir, "run_counter.txt")
last_run = _read_counter(counter_file)
if last_run is None:
last_run = _find_max_existing_run(logs_dir)
new_run = last_run + 1
# Persist the new run number
with open(counter_file, "w", encoding="utf-8") as f:
f.write(str(new_run))
_CURRENT_RUN_NUMBER = new_run
# Create run log file
run_log_file = os.path.join(logs_dir, f"run_{new_run}.log")
if not os.path.exists(run_log_file):
with open(run_log_file, "a", encoding="utf-8"):
pass
# Prune older runs
prune_old_runs(logs_dir, max_runs=max_runs)
# Record in history
_record_run_history(logs_dir, new_run, run_log_file, max_runs)
return new_run
def get_current_run_number(logs_dir: Optional[str] = None) -> int:
"""Return the current cached run number, or initialize one if not yet done."""
global _CURRENT_RUN_NUMBER
if _CURRENT_RUN_NUMBER is None:
return initialize_run(logs_dir=logs_dir)
return _CURRENT_RUN_NUMBER
def get_current_run_log_file(logs_dir: Optional[str] = None) -> str:
"""Return the absolute path to the current run's log file."""
if logs_dir is None:
logs_dir = _get_default_logs_dir()
run_num = get_current_run_number(logs_dir=logs_dir)
return os.path.join(logs_dir, f"run_{run_num}.log")
def get_run_info(logs_dir: Optional[str] = None, max_runs: Optional[int] = None) -> Dict[str, Any]:
"""Return structured metadata about the current and recent runs."""
if logs_dir is None:
logs_dir = _get_default_logs_dir()
if max_runs is None:
env_max = os.getenv("MAX_LOG_RUNS", "").strip()
max_runs = int(env_max) if env_max.isdigit() else DEFAULT_MAX_RUNS
current_run = get_current_run_number(logs_dir=logs_dir)
# Inspect present run files
pattern = re.compile(r"^run_(\d+)\.log$")
present_runs = []
if os.path.exists(logs_dir):
for name in os.listdir(logs_dir):
match = pattern.match(name)
if match:
full_path = os.path.join(logs_dir, name)
stat = os.stat(full_path)
present_runs.append({
"run_number": int(match.group(1)),
"filename": name,
"size_bytes": stat.st_size,
"modified_at": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(),
})
present_runs.sort(key=lambda x: x["run_number"])
return {
"run_number": current_run,
"max_runs": max_runs,
"current_log_file": os.path.basename(get_current_run_log_file(logs_dir=logs_dir)),
"available_runs": present_runs,
}
|