107 lines
4.4 KiB
Python
107 lines
4.4 KiB
Python
from __future__ import annotations
|
|
from pathlib import Path
|
|
import logging
|
|
import os
|
|
from logging.handlers import TimedRotatingFileHandler
|
|
from typing import Optional
|
|
from config import Config
|
|
|
|
|
|
_DEF_LEVEL = os.getenv("APP_LOG_LEVEL", "INFO").upper()
|
|
_DEF_FMT = "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
|
|
|
|
|
|
def _ensure_log_dir() -> Path:
|
|
p = Path(Config.LOG_FOLDER)
|
|
p.mkdir(parents=True, exist_ok=True)
|
|
return p
|
|
|
|
|
|
def setup_logging(app: Optional[object] = None) -> logging.Logger:
|
|
"""앱 전역 로깅을 파일(일단위 회전) + 콘솔로 설정.
|
|
- 회전 파일명: YYYY-MM-DD.log
|
|
- 중복 핸들러 방지
|
|
- Windows/Linux 공통 동작
|
|
"""
|
|
log_dir = _ensure_log_dir()
|
|
log_path = log_dir / "app.log"
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# [Fix] 앱 시작 시점에 app.log가 있고 날짜가 지났다면 강제 로테이션 (백업)
|
|
# TimedRotatingFileHandler는 프로세스가 재시작되면 파일의 생성일을 기준으로
|
|
# 롤오버를 즉시 수행하지 않고 append 모드로 여는 경우가 많음.
|
|
# 이를 보완하기 위해, 직접 날짜를 확인하고 파일을 옮겨준다.
|
|
# ─────────────────────────────────────────────────────────────
|
|
if log_path.exists():
|
|
try:
|
|
from datetime import date
|
|
|
|
# 파일 마지막 수정 시간 확인
|
|
mtime = os.path.getmtime(log_path)
|
|
file_date = date.fromtimestamp(mtime)
|
|
today = date.today()
|
|
|
|
# "파일 날짜" < "오늘" 이면 백업
|
|
if file_date < today:
|
|
backup_name = file_date.strftime("%Y-%m-%d.log")
|
|
backup_path = log_dir / backup_name
|
|
|
|
# 이미 백업 파일이 있으면 굳이 덮어쓰거나 하지 않음(안전성)
|
|
if not backup_path.exists():
|
|
os.rename(log_path, backup_path)
|
|
print(f"[Logger] Rotated old log file: app.log -> {backup_name}")
|
|
|
|
except Exception as e:
|
|
# 권한 문제, 파일 잠금(Windows) 등으로 실패 시 무시하고 진행
|
|
print(f"[Logger] Failed to force rotate log file: {e}")
|
|
|
|
root = logging.getLogger()
|
|
root.setLevel(_DEF_LEVEL)
|
|
root.propagate = False
|
|
|
|
# 기존 핸들러 제거(중복 방지)
|
|
for h in root.handlers[:]:
|
|
root.removeHandler(h)
|
|
|
|
# 파일 로거
|
|
file_handler = TimedRotatingFileHandler(
|
|
filename=str(log_path), when="midnight", interval=1, backupCount=90, encoding="utf-8", utc=False
|
|
)
|
|
|
|
# 회전 파일명: 2025-09-30.log 형태로
|
|
def _namer(default_name: str) -> str:
|
|
# default_name: app.log.YYYY-MM-DD
|
|
base_dir = os.path.dirname(default_name)
|
|
date_str = default_name.rsplit(".", 1)[-1]
|
|
return os.path.join(base_dir, f"{date_str}.log")
|
|
|
|
file_handler.namer = _namer
|
|
file_handler.setFormatter(logging.Formatter(_DEF_FMT))
|
|
|
|
# 콘솔 로거
|
|
console = logging.StreamHandler()
|
|
console.setFormatter(logging.Formatter("[%(levelname)s] %(message)s"))
|
|
|
|
root.addHandler(file_handler)
|
|
root.addHandler(console)
|
|
|
|
if app is not None:
|
|
# Flask 앱 로거에도 동일 핸들러 바인딩
|
|
app.logger.handlers = root.handlers
|
|
app.logger.setLevel(root.level)
|
|
# 루트 로거로 전파되면 메시지가 두 번 출력되므로 방지
|
|
app.logger.propagate = False
|
|
|
|
# 제3자 라이브러리 로그 레벨 조정 (너무 시끄러운 경우)
|
|
# werkzeug: 기본적인 HTTP 요청 로그(GET/POST 등)를 숨김 (WARNING 이상만 표시)
|
|
logging.getLogger("werkzeug").setLevel(logging.WARNING)
|
|
logging.getLogger("socketio").setLevel(logging.WARNING)
|
|
logging.getLogger("engineio").setLevel(logging.WARNING)
|
|
|
|
# httpx, telegram 라이브러리의 HTTP 요청 로그 숨기기
|
|
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
|
logging.getLogger("telegram").setLevel(logging.WARNING)
|
|
|
|
root.debug("Logger initialized | level=%s | file=%s", _DEF_LEVEL, log_path)
|
|
return root |