Files
Aislo/common_util/common_util_resource_monitor.py

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)