переобут logger

This commit is contained in:
2025-10-24 10:06:38 +00:00
parent c30bc088c7
commit 5083423660

View File

@@ -27,12 +27,13 @@ import os
import sys
import time
import json
import getpass
import argparse
import logging
import logging.config
import threading
import concurrent.futures
import queue
import platform
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from datetime import datetime, timedelta, timezone
@@ -85,39 +86,55 @@ STATUS_CHECK_INTERVAL_SEC = 60 # проверять "онлайн?" раз
ONLINE_FETCH_INTERVAL_SEC = 1 # когда матч онлайн, дергать три запроса каждую секунду
POLL_INTERVAL_OFFLINE_SEC = 300 # резервный интервал сна при ошибках/до старта
USERNAME = getpass.getuser()
TELEGRAM_BOT_TOKEN = "7639240596:AAH0YtdQoWZSC-_R_EW4wKAHHNLIA0F_ARY"
TELEGRAM_CHAT_ID = 228977654
myhost = platform.node()
# Настройка логгера
def setup_logger(level: str = "INFO") -> logging.Logger:
user = getpass.getuser()
log_dir = "LOGS"
os.makedirs(log_dir, exist_ok=True)
LOG_CONFIG = {
"version": 1,
"handlers": {
"telegram": {
"class": "telegram_handler.TelegramHandler",
"level": "INFO",
"token": TELEGRAM_BOT_TOKEN,
"chat_id": TELEGRAM_CHAT_ID,
"formatter": "telegram",
},
"console": {
"class": "logging.StreamHandler",
"level": "DEBUG",
"formatter": "simple",
"stream": "ext://sys.stdout",
},
"file": {
"class": "logging.FileHandler",
"level": "DEBUG",
"formatter": "simple",
"filename": f"logs/GFX_{myhost}.log",
"encoding": "utf-8",
},
},
"loggers": {
__name__: {"handlers": ["console", "file", "telegram"], "level": "DEBUG"},
},
"formatters": {
"telegram": {
"class": "telegram_handler.HtmlFormatter",
"format": "%(levelname)s %(message)s",
"use_emoji": "True",
},
"simple": {
"class": "logging.Formatter",
"format": "%(asctime)s %(levelname)-8s %(funcName)s() - %(message)s",
"datefmt": "%d.%m.%Y %H:%M:%S",
},
},
}
log_path = os.path.join(log_dir, f"{user}.log")
logger = logging.getLogger("game_watcher")
logger.setLevel(getattr(logging, level.upper(), logging.INFO))
logger.propagate = False
if not logger.handlers:
fmt = logging.Formatter("%(asctime)s | %(levelname)s | %(message)s")
# потоковый вывод (консоль)
sh = logging.StreamHandler(sys.stdout)
sh.setFormatter(fmt)
logger.addHandler(sh)
# запись в файл
fh = logging.FileHandler(log_path, encoding="utf-8")
fh.setFormatter(fmt)
logger.addHandler(fh)
logger.info("Логи будут писаться в: %s", log_path)
return logger
logging.config.dictConfig(LOG_CONFIG)
logger = logging.getLogger(__name__)
logger.handlers[2].formatter.use_emoji = True
# ==========================
@@ -125,53 +142,6 @@ def setup_logger(level: str = "INFO") -> logging.Logger:
# ==========================
def send_telegram(message: str) -> None:
"""
Отправка уведомления в Telegram.
Можно использовать как переменные окружения,
так и значения, указанные в TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID.
"""
import requests
logger = logging.getLogger("game_watcher")
# 1⃣ Сначала пытаемся взять значения из переменных окружения
token = os.getenv("TELEGRAM_BOT_TOKEN", TELEGRAM_BOT_TOKEN)
chat_id = os.getenv("TELEGRAM_CHAT_ID", str(TELEGRAM_CHAT_ID))
if not token or not chat_id:
logger.warning(
"TELEGRAM_BOT_TOKEN/CHAT_ID не заданы — сообщение не отправлено: %s",
message,
)
return
url = f"https://api.telegram.org/bot{token}/sendMessage"
try:
resp = requests.post(
url, json={"chat_id": chat_id, "text": message}, timeout=10
)
resp.raise_for_status()
logger.info("Сообщение успешно отправлено в Telegram: %s", message)
except requests.HTTPError as e:
logger.error("Ошибка HTTP при отправке в Telegram: %s | Ответ: %s", e, resp.text)
except Exception as e:
logger.error("Ошибка отправки в Telegram: %s", e)
def notify_error(msg: str) -> None:
user = getpass.getuser()
full_msg = f"[{user}] ❗ Ошибка: {msg}"
logging.getLogger("game_watcher").error(full_msg)
# send_telegram(full_msg)
def notify_info(msg: str) -> None:
user = getpass.getuser()
full_msg = f"[{user}] {msg}"
logging.getLogger("game_watcher").info(full_msg)
# send_telegram(full_msg)
def fetch_json(url: str, params: dict | None = None, session: requests.Session | None = None) -> dict:
"""
GET JSON с таймаутом и внятными ошибками.
@@ -1075,8 +1045,7 @@ def Json_Team_Generation(merged: dict, *, out_dir: str = "static", who: str | No
def validate_league_or_die(league: str) -> str:
league = (league or DEFAULT_LEAGUE).lower().strip()
if league not in ALLOWED_LEAGUES:
msg = f"Неверный тег лиги: '{league}'. Допустимо: {sorted(ALLOWED_LEAGUES)}"
notify_error(msg)
logger.warning(f"Неверный тег лиги: '{league}'. Допустимо: {sorted(ALLOWED_LEAGUES)}")
sys.exit(2)
return league
@@ -1086,12 +1055,12 @@ def get_last_season_or_die(league: str, lang: str) -> str:
try:
data = fetch_json(url)
season = extract_last_season(data)
logging.getLogger("game_watcher").info(
"Последний сезон для %s: %s", league, season
logging.info(
f"Последний сезон для {league}: {season}"
)
return season
except Exception as e:
notify_error(f"Не получилось получить последний сезон для {league}: {e}")
logger.warning(f"Не получилось получить последний сезон для {league}: {e}")
sys.exit(3)
@@ -1101,10 +1070,10 @@ def get_team_schedule_or_die(league: str, season: str, team: str, lang: str) ->
data = fetch_json(url)
team_games = extract_team_schedule_for_season(data, team)
if not team_games:
notify_error(f"Для команды {team} не найдено игр в сезоне {season}.")
logger.warning(f"Для команды {team} не найдено игр в сезоне {season}.")
return team_games
except Exception as e:
notify_error(f"Не получилось получить расписание {league}/{season}: {e}")
logger.warning(f"Не получилось получить расписание {league}/{season}: {e}")
return []
@@ -1166,7 +1135,7 @@ class PostProcessor:
Json_Team_Generation(merged, out_dir="static", who="team1")
Json_Team_Generation(merged, out_dir="static", who="team2")
except Exception as e:
logging.getLogger("game_watcher").exception("Postproc failed: %s", e)
logging.exception(f"Postproc failed: {e}")
def stop(self):
self._stop.set()
@@ -1179,7 +1148,7 @@ class OnlinePoller:
self.lang = lang
self._stop_event = threading.Event()
self._thread: threading.Thread | None = None
self._log = logging.getLogger("game_watcher")
self._log = logging.info("start")
self._on_update = on_update
self._post = PostProcessor()
@@ -1204,7 +1173,7 @@ class OnlinePoller:
if self._thread and self._thread.is_alive():
self._stop_event.set()
self._thread.join(timeout=2)
self._log.info("Онлайн-поллер для игры %s остановлен.", self.game_id)
self._log.info(f"Онлайн-поллер для игры {self.game_id} остановлен.")
self._thread = None
try:
self._session.close()
@@ -1249,7 +1218,7 @@ class OnlinePoller:
len(ls) if isinstance(ls, dict) else "",
)
except Exception as e:
notify_error(f"Сбой online-поллера для игры {self.game_id}: {e}")
logger.warning(f"Сбой online-поллера для игры {self.game_id}: {e}")
# лёгкая задержка после ошибки, но не «наказание» на целую секунду
time.sleep(0.2)
@@ -1273,8 +1242,7 @@ class OnlinePoller:
self._log.info("Онлайн-поллер для игры %s запущен.", self.game_id)
def monitor_game_loop(league: str, game_id: str, lang:str, stop_event: threading.Event) -> None:
log = logging.getLogger("game_watcher")
notify_info(f"Старт мониторинга игры {game_id} ({league}).")
logger.info(f"Старт мониторинга игры {game_id} ({league}).")
poller = OnlinePoller(league, game_id, lang)
was_online = False
@@ -1285,15 +1253,14 @@ def monitor_game_loop(league: str, game_id: str, lang:str, stop_event: threading
is_finished = status in {"resultconfirmed", "result"}
if is_finished:
log.info("Матч %s завершён. Останавливаем мониторинг.", game_id)
notify_info(f"Матч {game_id} завершён.")
logger.info(f"Матч {game_id} завершён. Останавливаем мониторинг.")
break
if is_online and not was_online:
log.info("Матч %s перешёл в онлайн. Запускаем быстрый опрос (1 сек).", game_id)
logger.info(f"Матч {game_id} перешёл в онлайн. Запускаем быстрый опрос (1 сек).")
poller.start()
elif not is_online and was_online:
log.info("Матч %s вышел из онлайна (или ещё не стартовал). Останавливаем быстрый опрос.", game_id)
logger.info(f"Матч {game_id} вышел из онлайна (или ещё не стартовал). Останавливаем быстрый опрос.")
poller.stop()
was_online = is_online
@@ -1302,13 +1269,13 @@ def monitor_game_loop(league: str, game_id: str, lang:str, stop_event: threading
stop_event.wait(STATUS_CHECK_INTERVAL_SEC)
except Exception as e:
notify_error(f"Сбой проверки статуса матча {game_id}: {e}")
logger.warning(f"Сбой проверки статуса матча {game_id}: {e}")
# При ошибке — не дергаем быстро, подождём немного и повторим
stop_event.wait(POLL_INTERVAL_OFFLINE_SEC)
# Гарантированно остановим быстрый опрос при завершении
poller.stop()
log.info("Мониторинг матча %s остановлен.", game_id)
logger.info(f"Мониторинг матча {game_id} остановлен.")
def next_midnight_local(now: datetime) -> datetime:
@@ -1333,15 +1300,12 @@ def daily_rollover_loop(
- выбираем сегодняшнюю игру или последний сыгранный
- при наличии сегодняшней игры — перезапускаем монитор на неё
"""
log = logging.getLogger("game_watcher")
while not stop_event.is_set():
now = datetime.now(APP_TZ)
wakeup_at = next_midnight_local(now)
seconds = (wakeup_at - now).total_seconds()
log.info(
"Ежедневная перекладка: проснусь %s (через ~%d сек).",
wakeup_at.isoformat(),
int(seconds),
logger.info(
f"Ежедневная перекладка: проснусь {wakeup_at.isoformat()} (через {int(seconds)} сек)."
)
if stop_event.wait(seconds):
break
@@ -1351,7 +1315,7 @@ def daily_rollover_loop(
season = season_getter(league, lang)
games = schedule_getter(league, season, team, lang)
if not games:
notify_info(
logger.info(
f"Ежедневная проверка: у {team} нет игр в расписании сезона {season}."
)
continue
@@ -1361,19 +1325,19 @@ def daily_rollover_loop(
)
if today_game:
gid = today_game["game"]["id"]
notify_info(
logger.info(
f"Сегодня у {team} есть игра: gameID={gid}. Перезапуск мониторинга."
)
monitor_mgr.restart(gid, lang)
elif last_played:
gid = last_played["game"]["id"]
notify_info(
logger.info(
f"Сегодня у {team} нет игры. Последняя сыгранная: gameID={gid}. Мониторинг НЕ запускаем."
)
else:
notify_info(f"Сегодня у {team} нет игры и нет предыдущих сыгранных.")
logger.info(f"Сегодня у {team} нет игры и нет предыдущих сыгранных.")
except Exception as e:
notify_error(f"Ошибка ежедневной проверки: {e}")
logger.warning(f"Ошибка ежедневной проверки: {e}")
class MonitorManager:
@@ -1426,8 +1390,7 @@ def main():
)
args = parser.parse_args()
logger = setup_logger(args.log_level)
logger.info("Запуск программы пользователем: %s", USERNAME)
logger.info("Запуск программы пользователем: %s", myhost)
logger.info("Запуск с параметрами: league=%s, team=%s, lang=%s", args.league, args.team, args.lang)
league = validate_league_or_die(args.league)
@@ -1439,7 +1402,7 @@ def main():
# 2) Получить расписание для команды
team_games = get_team_schedule_or_die(league, season, team, args.lang)
if not team_games:
notify_error("Расписание пустое — работа завершена.")
logger.warning("Расписание пустое — работа завершена.")
sys.exit(4)
# 3) Найти сегодняшнюю или последнюю сыгранную игру
@@ -1451,7 +1414,7 @@ def main():
if today_game:
# В исходном расписании предполагалось наличие game.id
game_id = today_game["game"]["id"]
notify_info(
logger.info(
f"Сегодня у {team} есть игра: gameID={game_id}. Запускаю мониторинг."
)
monitor_mgr.restart(game_id, args.lang)
@@ -1470,15 +1433,16 @@ def main():
)
Json_Team_Generation(merged, out_dir="static", who="team1")
Json_Team_Generation(merged, out_dir="static", who="team2")
notify_info(
# print(merged)
logger.info(
f"Сегодня у {team} нет игры. Последняя сыгранная: gameID={game_id}. Мониторинг не запускаю."
)
except Exception as e:
logging.getLogger("game_watcher").exception(
logging.exception(
"Оффлайн-сохранение для gameID=%s упало: %s", game_id, e
)
else:
notify_info(f"Сегодня у {team} нет игры и нет предыдущих сыгранных.")
logger.info(f"Сегодня у {team} нет игры и нет предыдущих сыгранных.")
# 4) Ежедневная перекладка расписания
stop_event = threading.Event()