Complete DDS training workflow and delivery package
This commit is contained in:
parent
68dd83c7c2
commit
4c4b91064f
229 changed files with 11969 additions and 1024 deletions
167
backend/app/monitoring.py
Normal file
167
backend/app/monitoring.py
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
"""Small, dependency-free runtime diagnostics for the local stand.
|
||||
|
||||
The product has to run on Windows as well as Linux. Keep the collector in
|
||||
the standard library: unavailable host counters are returned as ``None``
|
||||
instead of making the administrator screen fail.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from collections import deque
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
STARTED_MONOTONIC = time.monotonic()
|
||||
_events: deque[dict] = deque(maxlen=500)
|
||||
_installed = False
|
||||
_sample_lock = threading.Lock()
|
||||
_last_wall = time.monotonic()
|
||||
_last_cpu = time.process_time()
|
||||
|
||||
_CREDENTIALS = re.compile(r"(://[^:/\s]+:)([^@/\s]+)(@)")
|
||||
_TOKENS = re.compile(
|
||||
r"(?i)\b(session_secret|llm_api_key|authorization|password)\b\s*[:=]\s*([^\s,;]+)"
|
||||
)
|
||||
|
||||
|
||||
def _safe(message: str) -> str:
|
||||
message = _CREDENTIALS.sub(r"\1[скрыто]\3", message)
|
||||
message = _TOKENS.sub(r"\1=[скрыто]", message)
|
||||
return message.replace("\x00", "")[:2000]
|
||||
|
||||
|
||||
class DiagnosticHandler(logging.Handler):
|
||||
"""Bounded warning/error journal safe enough to expose to an admin."""
|
||||
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
try:
|
||||
_events.append({
|
||||
"at": datetime.fromtimestamp(record.created, timezone.utc),
|
||||
"level": record.levelname,
|
||||
"source": record.name[:120],
|
||||
"message": _safe(record.getMessage()),
|
||||
})
|
||||
except Exception: # logging must never take the service down
|
||||
pass
|
||||
|
||||
|
||||
def install_diagnostics() -> None:
|
||||
global _installed
|
||||
if _installed:
|
||||
return
|
||||
handler = DiagnosticHandler(level=logging.WARNING)
|
||||
logging.getLogger().addHandler(handler)
|
||||
_installed = True
|
||||
|
||||
|
||||
def recent_events(*, limit: int = 100) -> list[dict]:
|
||||
return list(_events)[-max(1, min(limit, 500)):][::-1]
|
||||
|
||||
|
||||
def _rss_bytes() -> int | None:
|
||||
try:
|
||||
if sys.platform.startswith("linux"):
|
||||
resident_pages = int(Path("/proc/self/statm").read_text().split()[1])
|
||||
return resident_pages * int(os.sysconf("SC_PAGE_SIZE"))
|
||||
if os.name == "nt":
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
|
||||
class Counters(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("cb", wintypes.DWORD), ("PageFaultCount", wintypes.DWORD),
|
||||
("PeakWorkingSetSize", ctypes.c_size_t),
|
||||
("WorkingSetSize", ctypes.c_size_t),
|
||||
("QuotaPeakPagedPoolUsage", ctypes.c_size_t),
|
||||
("QuotaPagedPoolUsage", ctypes.c_size_t),
|
||||
("QuotaPeakNonPagedPoolUsage", ctypes.c_size_t),
|
||||
("QuotaNonPagedPoolUsage", ctypes.c_size_t),
|
||||
("PagefileUsage", ctypes.c_size_t),
|
||||
("PeakPagefileUsage", ctypes.c_size_t),
|
||||
]
|
||||
|
||||
counters = Counters()
|
||||
counters.cb = ctypes.sizeof(counters)
|
||||
ctypes.windll.psapi.GetProcessMemoryInfo( # type: ignore[attr-defined]
|
||||
ctypes.windll.kernel32.GetCurrentProcess(), ctypes.byref(counters), counters.cb
|
||||
)
|
||||
return int(counters.WorkingSetSize)
|
||||
import resource
|
||||
|
||||
value = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
|
||||
return int(value if sys.platform == "darwin" else value * 1024)
|
||||
except (OSError, ValueError, AttributeError, ImportError, IndexError):
|
||||
return None
|
||||
|
||||
|
||||
def _memory() -> tuple[int | None, int | None]:
|
||||
try:
|
||||
if sys.platform.startswith("linux"):
|
||||
fields: dict[str, int] = {}
|
||||
for line in Path("/proc/meminfo").read_text().splitlines():
|
||||
name, value = line.split(":", 1)
|
||||
fields[name] = int(value.strip().split()[0]) * 1024
|
||||
return fields.get("MemTotal"), fields.get("MemAvailable")
|
||||
if os.name == "nt":
|
||||
import ctypes
|
||||
|
||||
class MemoryStatus(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("length", ctypes.c_ulong), ("memory_load", ctypes.c_ulong),
|
||||
("total_physical", ctypes.c_ulonglong),
|
||||
("available_physical", ctypes.c_ulonglong),
|
||||
("total_page_file", ctypes.c_ulonglong),
|
||||
("available_page_file", ctypes.c_ulonglong),
|
||||
("total_virtual", ctypes.c_ulonglong),
|
||||
("available_virtual", ctypes.c_ulonglong),
|
||||
("available_extended_virtual", ctypes.c_ulonglong),
|
||||
]
|
||||
|
||||
status = MemoryStatus()
|
||||
status.length = ctypes.sizeof(status)
|
||||
ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(status)) # type: ignore[attr-defined]
|
||||
return int(status.total_physical), int(status.available_physical)
|
||||
page = int(os.sysconf("SC_PAGE_SIZE"))
|
||||
return int(os.sysconf("SC_PHYS_PAGES")) * page, None
|
||||
except (OSError, ValueError, AttributeError, ImportError):
|
||||
return None, None
|
||||
|
||||
|
||||
def sample_metrics() -> dict:
|
||||
"""Return a cheap point-in-time sample; the first CPU sample is zero."""
|
||||
global _last_wall, _last_cpu
|
||||
now = time.monotonic()
|
||||
cpu_now = time.process_time()
|
||||
with _sample_lock:
|
||||
wall_delta = max(now - _last_wall, 0.001)
|
||||
cpu_percent = max(0.0, (cpu_now - _last_cpu) / wall_delta * 100.0)
|
||||
_last_wall, _last_cpu = now, cpu_now
|
||||
|
||||
cores = os.cpu_count() or 1
|
||||
try:
|
||||
load_1m_percent = os.getloadavg()[0] / cores * 100.0
|
||||
except (AttributeError, OSError):
|
||||
load_1m_percent = None
|
||||
memory_total, memory_available = _memory()
|
||||
disk = shutil.disk_usage(Path.cwd())
|
||||
return {
|
||||
"at": datetime.now(timezone.utc),
|
||||
"uptime_seconds": max(0.0, now - STARTED_MONOTONIC),
|
||||
"cpu_percent": round(cpu_percent, 2),
|
||||
"load_1m_percent": round(load_1m_percent, 2) if load_1m_percent is not None else None,
|
||||
"cpu_cores": cores,
|
||||
"rss_bytes": _rss_bytes(),
|
||||
"memory_total_bytes": memory_total,
|
||||
"memory_available_bytes": memory_available,
|
||||
"disk_total_bytes": disk.total,
|
||||
"disk_free_bytes": disk.free,
|
||||
"threads": threading.active_count(),
|
||||
}
|
||||
Loading…
Reference in a new issue