180 lines
6.3 KiB
Python
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})
|