Files
kis_bot/kis_trader/utils/logger.py
Hwang f61c471aac 브랜치 분리 방식: A / B / C
A 선택 시 커밋 메시지: 위 초안 OK / 수정 / 직접 작성
작업 시점: 지금 / 운영 데이터 1~2일 쌓고 / 주말
2026-05-05 21:04:17 +09:00

175 lines
6.1 KiB
Python

"""
kis_trader/utils/logger.py — 공통 로거 + JSON Atomic Save + 텔레그램/매터모스트 알림
=============================================================================
- get_logger(name): 표준 포맷(시:분:초 + 메시지)과 ANSI 컬러 로거 반환.
- atomic_save_json / atomic_load_json: 프로세스 동시 실행에도 파일 파손 없이 쓰기.
- msg_mm / msg_tg: 메시지 채널 분리. 서버 부하 방지를 위해 호출 전 짧은 jitter.
"""
from __future__ import annotations
import json
import logging
import os
import random
import tempfile
import threading
import time
from pathlib import Path
from typing import Any, Optional
import requests
from .env import get_env_from_db
# ── 로깅 ─────────────────────────────────────────────────────────────
# 루트 로거 한 번만 설정 (여러 모듈에서 import되어도 중복 핸들러 안 생기게)
_ROOT_CONFIGURED = False
def _configure_root() -> None:
global _ROOT_CONFIGURED
if _ROOT_CONFIGURED:
return
logging.basicConfig(
format="[%(asctime)s] [%(name)s] %(message)s",
datefmt="%H:%M:%S",
level=logging.INFO,
)
_ROOT_CONFIGURED = True
def get_logger(name: str) -> logging.Logger:
"""프로젝트 표준 로거."""
_configure_root()
return logging.getLogger(name)
# ── ANSI 컬러 (기존 봇 관례 유지) ──────────────────────────────────
LOG_RED = "\033[91m"
LOG_YELLOW = "\033[93m"
LOG_GREEN = "\033[92m"
LOG_CYAN = "\033[96m"
LOG_RESET = "\033[0m"
# ── Atomic JSON I/O ─────────────────────────────────────────────────
# json 파일 손상 방지: 임시파일에 쓰고 os.replace 로 원자적 교체.
# 이벤트 발생 시 즉시 저장(재시작해도 최신 상태 유지).
_IO_LOCK = threading.Lock()
def atomic_save_json(path: str | os.PathLike, data: Any) -> bool:
"""임시파일 → rename 으로 원자적 저장. 실패해도 기존 파일 보존."""
p = Path(path)
try:
p.parent.mkdir(parents=True, exist_ok=True)
with _IO_LOCK:
with tempfile.NamedTemporaryFile(
"w", delete=False, dir=str(p.parent),
prefix=p.name + ".", suffix=".tmp", encoding="utf-8",
) as tf:
json.dump(data, tf, ensure_ascii=False, indent=2)
tf.flush()
os.fsync(tf.fileno())
tmp_name = tf.name
os.replace(tmp_name, p)
return True
except Exception as e:
logging.getLogger("kis_trader.logger").error(
"atomic_save_json 실패 (%s): %s", p, e
)
return False
def atomic_load_json(path: str | os.PathLike, default: Any = None) -> Any:
p = Path(path)
if not p.exists():
return default
try:
with _IO_LOCK:
with open(p, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return default
# ── Mattermost / Telegram 알림 ─────────────────────────────────────
_MM_CONFIG_PATH_CACHE: Optional[Path] = None
def _mm_config_path() -> Path:
"""mm_config.json 위치 탐색 (프로젝트 루트 기준)."""
global _MM_CONFIG_PATH_CACHE
if _MM_CONFIG_PATH_CACHE is not None:
return _MM_CONFIG_PATH_CACHE
here = Path(__file__).resolve()
# kis_trader/utils/logger.py → kis_bot/
project_root = here.parent.parent.parent
_MM_CONFIG_PATH_CACHE = project_root / "mm_config.json"
return _MM_CONFIG_PATH_CACHE
def _load_mm_channel_id(channel_alias: str) -> Optional[str]:
cfg = atomic_load_json(_mm_config_path(), default={})
try:
return (cfg.get("channels") or {}).get(channel_alias)
except Exception:
return None
def msg_mm(text: str, channel_alias: Optional[str] = None, jitter: bool = True) -> bool:
"""
Mattermost 메시지 전송.
Args:
channel_alias: mm_config.json 의 channels 키. None 이면 MATTERMOST_CHANNEL 사용.
jitter: True 면 0.3~1.2s 랜덤 슬립 (버스트 차단 방지). 실시간 매매 로직에서는 False 권장.
"""
alias = channel_alias or get_env_from_db("MATTERMOST_CHANNEL", "stock")
server_url = get_env_from_db("MM_SERVER_URL", "https://mattermost.hoonfam.org")
bot_token = get_env_from_db("MM_BOT_TOKEN_", "").strip()
cid = _load_mm_channel_id(alias)
if not cid or not bot_token:
logging.getLogger("kis_trader.logger").debug(
"[MM 스킵] alias=%s cid=%s", alias, cid
)
return False
if jitter:
time.sleep(random.uniform(0.3, 1.2))
try:
r = requests.post(
f"{server_url}/api/v4/posts",
headers={
"Authorization": f"Bearer {bot_token}",
"Content-Type": "application/json",
},
json={"channel_id": cid, "message": text},
timeout=5,
)
return r.status_code < 400
except Exception as e:
logging.getLogger("kis_trader.logger").debug("MM 발송 실패: %s", e)
return False
def msg_tg(text: str, jitter: bool = True) -> bool:
"""
Telegram Bot 알림. TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID 둘 다 설정돼 있을 때만 전송.
실시간 매매 루프에서는 jitter=False 로 호출.
"""
token = get_env_from_db("TELEGRAM_BOT_TOKEN", "").strip()
chat_id = get_env_from_db("TELEGRAM_CHAT_ID", "").strip()
if not token or not chat_id:
return False
if jitter:
time.sleep(random.uniform(0.3, 1.2))
try:
r = requests.post(
f"https://api.telegram.org/bot{token}/sendMessage",
json={"chat_id": chat_id, "text": text, "parse_mode": "Markdown"},
timeout=5,
)
return r.status_code < 400
except Exception as e:
logging.getLogger("kis_trader.logger").debug("TG 발송 실패: %s", e)
return False