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,
    }