116 lines
4.3 KiB
Python
116 lines
4.3 KiB
Python
"""시스템 리소스 계측 및 로그 관리.
|
|
|
|
- 2분(config 설정) 간격으로 CPU/메모리/디스크 사용률을 계측한다.
|
|
- 계측값을 `system_resources` DB 테이블과 루트 `log/system_resources.log`에 함께 기록한다.
|
|
- 로그 파일과 DB 이력은 항상 최근 1개월치만 유지한다(오래된 줄 삭제 + 새 줄 추가).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
import shutil
|
|
from datetime import datetime, timedelta, timezone
|
|
from pathlib import Path
|
|
|
|
import psutil
|
|
|
|
from config.config_db import get_db_pool
|
|
from config.config_system import (
|
|
RESOURCE_LOG_PATH,
|
|
RESOURCE_LOG_RETENTION_DAYS,
|
|
RESOURCE_SAMPLE_INTERVAL_SEC,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _sample() -> dict[str, float | int]:
|
|
"""현재 시점 리소스 사용률을 계측한다."""
|
|
total, used, _free = shutil.disk_usage(".")
|
|
return {
|
|
"cpu_usage_percent": round(psutil.cpu_percent(interval=0.1), 2),
|
|
"memory_usage_percent": round(psutil.virtual_memory().percent, 2),
|
|
"disk_usage_percent": round(used / total * 100 if total else 0, 2),
|
|
"total_storage_mb": round(used / 1024 / 1024, 2),
|
|
}
|
|
|
|
|
|
def _trim_log_file(retention_days: int) -> list[str]:
|
|
"""로그 파일에서 보관 기간이 지난 줄을 제거하고 남은 줄을 반환한다."""
|
|
path = Path(RESOURCE_LOG_PATH)
|
|
if not path.exists():
|
|
return []
|
|
cutoff = datetime.now(timezone.utc) - timedelta(days=retention_days)
|
|
kept: list[str] = []
|
|
for line in path.read_text(encoding="utf-8").splitlines():
|
|
if not line.strip():
|
|
continue
|
|
try:
|
|
ts = datetime.fromisoformat(line.split("\t", 1)[0])
|
|
except ValueError:
|
|
continue
|
|
if ts >= cutoff:
|
|
kept.append(line)
|
|
return kept
|
|
|
|
|
|
def _write_log(sample: dict[str, float | int], now: datetime) -> None:
|
|
"""최근 1개월치만 유지하며 새 계측 줄을 로그 파일에 기록한다."""
|
|
path = Path(RESOURCE_LOG_PATH)
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
lines = _trim_log_file(RESOURCE_LOG_RETENTION_DAYS)
|
|
new_line = (
|
|
f"{now.isoformat()}\t"
|
|
f"cpu={sample['cpu_usage_percent']}\t"
|
|
f"mem={sample['memory_usage_percent']}\t"
|
|
f"disk={sample['disk_usage_percent']}\t"
|
|
f"storage_mb={sample['total_storage_mb']}"
|
|
)
|
|
lines.append(new_line)
|
|
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
|
|
|
|
async def _write_db(sample: dict[str, float | int]) -> None:
|
|
"""DB에 계측값을 저장하고 보관 기간이 지난 행을 삭제한다."""
|
|
pool = get_db_pool()
|
|
async with pool.acquire() as connection, connection.cursor() as cursor:
|
|
await cursor.execute(
|
|
"""SELECT
|
|
(SELECT COUNT(*) FROM sessions
|
|
WHERE expires_at > CURRENT_TIMESTAMP),
|
|
(SELECT COUNT(*) FROM projects WHERE deleted_at IS NULL)"""
|
|
)
|
|
active_users, active_projects = await cursor.fetchone()
|
|
await cursor.execute(
|
|
"""INSERT INTO system_resources
|
|
(cpu_usage_percent, memory_usage_percent, disk_usage_percent,
|
|
active_user_count, active_project_count, total_storage_mb)
|
|
VALUES (%s, %s, %s, %s, %s, %s)""",
|
|
(
|
|
sample["cpu_usage_percent"],
|
|
sample["memory_usage_percent"],
|
|
sample["disk_usage_percent"],
|
|
active_users,
|
|
active_projects,
|
|
sample["total_storage_mb"],
|
|
),
|
|
)
|
|
await cursor.execute(
|
|
"DELETE FROM system_resources WHERE timestamp < %s",
|
|
(datetime.utcnow() - timedelta(days=RESOURCE_LOG_RETENTION_DAYS),),
|
|
)
|
|
await connection.commit()
|
|
|
|
|
|
async def sample_resources_loop() -> None:
|
|
"""앱 라이프사이클 동안 주기적으로 리소스를 계측·기록하는 백그라운드 태스크."""
|
|
while True:
|
|
try:
|
|
sample = _sample()
|
|
_write_log(sample, datetime.now(timezone.utc))
|
|
await _write_db(sample)
|
|
except Exception as exc: # noqa: BLE001 — 계측 실패로 앱이 죽지 않게 방어
|
|
logger.warning("리소스 계측 실패: %s", exc)
|
|
await asyncio.sleep(RESOURCE_SAMPLE_INTERVAL_SEC)
|