Files

180 lines
6.3 KiB
Python

"""Settings persistence and backend log export used by HTTP routes."""
import asyncio
import os
import time
from ..log_system import LogLevel
from ..log_system.config import LOG_LEVEL as BACKEND_DEFAULT_LOG_LEVEL
from ..log_system.logger import parse_rotated_log_filename
def _flush_backend_log_handlers(backend_log_controller):
for handler in getattr(backend_log_controller, "file_handlers", {}).values():
try:
handler.flush()
except Exception:
pass
def _backend_log_sort_key(path):
name = os.path.basename(path)
parsed_name = parse_rotated_log_filename(name)
if parsed_name is None:
return (name.lower(), 999)
base_name, rotation = parsed_name
return (base_name.lower(), rotation)
def _collect_backend_log_files(log_dir):
if not log_dir or not os.path.isdir(log_dir):
return []
files = []
for entry in os.listdir(log_dir):
name = str(entry or "")
if not name.startswith("azlogs_"):
continue
if parse_rotated_log_filename(name) is None:
continue
path = os.path.abspath(os.path.join(log_dir, name))
try:
if os.path.commonpath([log_dir, path]) != log_dir:
continue
except ValueError:
continue
if os.path.isfile(path):
files.append(path)
return sorted(files, key=_backend_log_sort_key)
def build_backend_log_export(backend_log_controller):
"""Build a text export from the configured backend log files."""
_flush_backend_log_handlers(backend_log_controller)
raw_log_dir = str(backend_log_controller.config.get("log_dir") or "")
log_dir = os.path.abspath(raw_log_dir) if raw_log_dir else ""
exported_at = time.strftime("%Y-%m-%d %H:%M:%S")
lines = [
"Model Resolver Backend Logs",
f"Exported: {exported_at}",
f"Log directory: {log_dir or 'not configured'}",
f"File logging: {bool(backend_log_controller.config.get('log_to_file'))}",
"",
]
log_files = _collect_backend_log_files(log_dir)
if not log_files:
lines.append("No backend log files found.")
lines.append("")
return "\n".join(lines)
for path in log_files:
name = os.path.basename(path)
try:
stat = os.stat(path)
modified_at = time.strftime(
"%Y-%m-%d %H:%M:%S",
time.localtime(stat.st_mtime),
)
lines.append(
f"===== {name} ({stat.st_size} bytes, modified {modified_at}) ====="
)
with open(path, encoding="utf-8", errors="replace") as log_file:
lines.append(log_file.read().rstrip())
except OSError as exc:
lines.append(f"===== {name} =====")
lines.append(f"Could not read log file: {exc}")
lines.append("")
return "\n".join(lines)
def _log_level_setting(value, default: str = BACKEND_DEFAULT_LOG_LEVEL) -> LogLevel:
normalized = str(value or default or "INFO").strip().upper()
if hasattr(LogLevel, normalized):
return getattr(LogLevel, normalized)
fallback = str(default or "INFO").strip().upper()
return getattr(LogLevel, fallback, LogLevel.INFO)
def apply_backend_logging_settings(backend_log_controller, settings: dict) -> None:
"""Apply persisted backend logging settings to the active logger."""
from ..settings import bool_setting as resolver_bool_setting
enabled = resolver_bool_setting(settings.get("backend_logs_enabled"), True)
level = _log_level_setting(settings.get("backend_log_level"))
backend_log_controller.set_enabled(enabled)
backend_log_controller.set_global_level(level)
class SettingsService:
"""Implement settings persistence and backend log export endpoints."""
def __init__(
self,
routes,
web,
json_api_endpoint,
load_settings,
save_settings,
get_settings_schema,
apply_logging_settings,
build_log_export,
):
self.asyncio = asyncio
self.apply_logging_settings = apply_logging_settings
self.build_log_export = build_log_export
self.get_settings_schema = get_settings_schema
self.json_api_endpoint = json_api_endpoint
self.load_settings = load_settings
self.routes = routes
self.save_settings = save_settings
self.web = web
def register(self):
"""Register settings and backend log export routes."""
self.apply_logging_settings(self.load_settings())
@self.routes.get("/model_resolver/logs/backend/export")
@self.json_api_endpoint("backend log export")
async def export_backend_logs_route(request):
"""Download Model Resolver backend logs as a text file."""
content = await self.asyncio.to_thread(self.build_log_export)
filename = (
"model_resolver_backend_logs_"
f"{time.strftime('%Y%m%d_%H%M%S')}.txt"
)
return self.web.Response(
text=content,
content_type="text/plain",
headers={
"Content-Disposition": (
f'attachment; filename="{filename}"'
),
"Cache-Control": "no-store",
},
)
@self.routes.get("/model_resolver/settings")
@self.json_api_endpoint("settings GET")
async def get_settings_route(request):
"""Return persisted settings (API keys, preferences)."""
data = await self.asyncio.to_thread(self.load_settings)
return self.web.json_response(
{"settings": data, "schema": self.get_settings_schema()}
)
@self.routes.post("/model_resolver/settings")
@self.json_api_endpoint("settings POST")
async def save_settings_route(request):
"""Persist settings (API keys, preferences) to disk."""
payload = await request.json()
if not isinstance(payload, dict):
return self.web.json_response(
{"error": "Expected JSON object"}, status=400
)
settings = await self.asyncio.to_thread(self.save_settings, payload)
self.apply_logging_settings(settings)
return self.web.json_response({"success": True})