переобут 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 sys
import time import time
import json import json
import getpass
import argparse import argparse
import logging import logging
import logging.config
import threading import threading
import concurrent.futures import concurrent.futures
import queue import queue
import platform
from requests.adapters import HTTPAdapter from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry from urllib3.util.retry import Retry
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
@@ -85,39 +86,55 @@ STATUS_CHECK_INTERVAL_SEC = 60 # проверять "онлайн?" раз
ONLINE_FETCH_INTERVAL_SEC = 1 # когда матч онлайн, дергать три запроса каждую секунду ONLINE_FETCH_INTERVAL_SEC = 1 # когда матч онлайн, дергать три запроса каждую секунду
POLL_INTERVAL_OFFLINE_SEC = 300 # резервный интервал сна при ошибках/до старта POLL_INTERVAL_OFFLINE_SEC = 300 # резервный интервал сна при ошибках/до старта
USERNAME = getpass.getuser()
TELEGRAM_BOT_TOKEN = "7639240596:AAH0YtdQoWZSC-_R_EW4wKAHHNLIA0F_ARY" TELEGRAM_BOT_TOKEN = "7639240596:AAH0YtdQoWZSC-_R_EW4wKAHHNLIA0F_ARY"
TELEGRAM_CHAT_ID = 228977654 TELEGRAM_CHAT_ID = 228977654
myhost = platform.node()
# Настройка логгера LOG_CONFIG = {
def setup_logger(level: str = "INFO") -> logging.Logger: "version": 1,
user = getpass.getuser() "handlers": {
log_dir = "LOGS" "telegram": {
os.makedirs(log_dir, exist_ok=True) "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") logging.config.dictConfig(LOG_CONFIG)
logger = logging.getLogger(__name__)
logger = logging.getLogger("game_watcher") logger.handlers[2].formatter.use_emoji = True
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
# ========================== # ==========================
@@ -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: def fetch_json(url: str, params: dict | None = None, session: requests.Session | None = None) -> dict:
""" """
GET JSON с таймаутом и внятными ошибками. 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: def validate_league_or_die(league: str) -> str:
league = (league or DEFAULT_LEAGUE).lower().strip() league = (league or DEFAULT_LEAGUE).lower().strip()
if league not in ALLOWED_LEAGUES: if league not in ALLOWED_LEAGUES:
msg = f"Неверный тег лиги: '{league}'. Допустимо: {sorted(ALLOWED_LEAGUES)}" logger.warning(f"Неверный тег лиги: '{league}'. Допустимо: {sorted(ALLOWED_LEAGUES)}")
notify_error(msg)
sys.exit(2) sys.exit(2)
return league return league
@@ -1086,12 +1055,12 @@ def get_last_season_or_die(league: str, lang: str) -> str:
try: try:
data = fetch_json(url) data = fetch_json(url)
season = extract_last_season(data) season = extract_last_season(data)
logging.getLogger("game_watcher").info( logging.info(
"Последний сезон для %s: %s", league, season f"Последний сезон для {league}: {season}"
) )
return season return season
except Exception as e: except Exception as e:
notify_error(f"Не получилось получить последний сезон для {league}: {e}") logger.warning(f"Не получилось получить последний сезон для {league}: {e}")
sys.exit(3) 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) data = fetch_json(url)
team_games = extract_team_schedule_for_season(data, team) team_games = extract_team_schedule_for_season(data, team)
if not team_games: if not team_games:
notify_error(f"Для команды {team} не найдено игр в сезоне {season}.") logger.warning(f"Для команды {team} не найдено игр в сезоне {season}.")
return team_games return team_games
except Exception as e: except Exception as e:
notify_error(f"Не получилось получить расписание {league}/{season}: {e}") logger.warning(f"Не получилось получить расписание {league}/{season}: {e}")
return [] return []
@@ -1166,7 +1135,7 @@ class PostProcessor:
Json_Team_Generation(merged, out_dir="static", who="team1") Json_Team_Generation(merged, out_dir="static", who="team1")
Json_Team_Generation(merged, out_dir="static", who="team2") Json_Team_Generation(merged, out_dir="static", who="team2")
except Exception as e: except Exception as e:
logging.getLogger("game_watcher").exception("Postproc failed: %s", e) logging.exception(f"Postproc failed: {e}")
def stop(self): def stop(self):
self._stop.set() self._stop.set()
@@ -1179,7 +1148,7 @@ class OnlinePoller:
self.lang = lang self.lang = lang
self._stop_event = threading.Event() self._stop_event = threading.Event()
self._thread: threading.Thread | None = None self._thread: threading.Thread | None = None
self._log = logging.getLogger("game_watcher") self._log = logging.info("start")
self._on_update = on_update self._on_update = on_update
self._post = PostProcessor() self._post = PostProcessor()
@@ -1204,7 +1173,7 @@ class OnlinePoller:
if self._thread and self._thread.is_alive(): if self._thread and self._thread.is_alive():
self._stop_event.set() self._stop_event.set()
self._thread.join(timeout=2) self._thread.join(timeout=2)
self._log.info("Онлайн-поллер для игры %s остановлен.", self.game_id) self._log.info(f"Онлайн-поллер для игры {self.game_id} остановлен.")
self._thread = None self._thread = None
try: try:
self._session.close() self._session.close()
@@ -1249,7 +1218,7 @@ class OnlinePoller:
len(ls) if isinstance(ls, dict) else "", len(ls) if isinstance(ls, dict) else "",
) )
except Exception as e: except Exception as e:
notify_error(f"Сбой online-поллера для игры {self.game_id}: {e}") logger.warning(f"Сбой online-поллера для игры {self.game_id}: {e}")
# лёгкая задержка после ошибки, но не «наказание» на целую секунду # лёгкая задержка после ошибки, но не «наказание» на целую секунду
time.sleep(0.2) time.sleep(0.2)
@@ -1273,8 +1242,7 @@ class OnlinePoller:
self._log.info("Онлайн-поллер для игры %s запущен.", self.game_id) self._log.info("Онлайн-поллер для игры %s запущен.", self.game_id)
def monitor_game_loop(league: str, game_id: str, lang:str, stop_event: threading.Event) -> None: def monitor_game_loop(league: str, game_id: str, lang:str, stop_event: threading.Event) -> None:
log = logging.getLogger("game_watcher") logger.info(f"Старт мониторинга игры {game_id} ({league}).")
notify_info(f"Старт мониторинга игры {game_id} ({league}).")
poller = OnlinePoller(league, game_id, lang) poller = OnlinePoller(league, game_id, lang)
was_online = False 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"} is_finished = status in {"resultconfirmed", "result"}
if is_finished: if is_finished:
log.info("Матч %s завершён. Останавливаем мониторинг.", game_id) logger.info(f"Матч {game_id} завершён. Останавливаем мониторинг.")
notify_info(f"Матч {game_id} завершён.")
break break
if is_online and not was_online: if is_online and not was_online:
log.info("Матч %s перешёл в онлайн. Запускаем быстрый опрос (1 сек).", game_id) logger.info(f"Матч {game_id} перешёл в онлайн. Запускаем быстрый опрос (1 сек).")
poller.start() poller.start()
elif not is_online and was_online: elif not is_online and was_online:
log.info("Матч %s вышел из онлайна (или ещё не стартовал). Останавливаем быстрый опрос.", game_id) logger.info(f"Матч {game_id} вышел из онлайна (или ещё не стартовал). Останавливаем быстрый опрос.")
poller.stop() poller.stop()
was_online = is_online 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) stop_event.wait(STATUS_CHECK_INTERVAL_SEC)
except Exception as e: except Exception as e:
notify_error(f"Сбой проверки статуса матча {game_id}: {e}") logger.warning(f"Сбой проверки статуса матча {game_id}: {e}")
# При ошибке — не дергаем быстро, подождём немного и повторим # При ошибке — не дергаем быстро, подождём немного и повторим
stop_event.wait(POLL_INTERVAL_OFFLINE_SEC) stop_event.wait(POLL_INTERVAL_OFFLINE_SEC)
# Гарантированно остановим быстрый опрос при завершении # Гарантированно остановим быстрый опрос при завершении
poller.stop() poller.stop()
log.info("Мониторинг матча %s остановлен.", game_id) logger.info(f"Мониторинг матча {game_id} остановлен.")
def next_midnight_local(now: datetime) -> datetime: 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(): while not stop_event.is_set():
now = datetime.now(APP_TZ) now = datetime.now(APP_TZ)
wakeup_at = next_midnight_local(now) wakeup_at = next_midnight_local(now)
seconds = (wakeup_at - now).total_seconds() seconds = (wakeup_at - now).total_seconds()
log.info( logger.info(
"Ежедневная перекладка: проснусь %s (через ~%d сек).", f"Ежедневная перекладка: проснусь {wakeup_at.isoformat()} (через {int(seconds)} сек)."
wakeup_at.isoformat(),
int(seconds),
) )
if stop_event.wait(seconds): if stop_event.wait(seconds):
break break
@@ -1351,7 +1315,7 @@ def daily_rollover_loop(
season = season_getter(league, lang) season = season_getter(league, lang)
games = schedule_getter(league, season, team, lang) games = schedule_getter(league, season, team, lang)
if not games: if not games:
notify_info( logger.info(
f"Ежедневная проверка: у {team} нет игр в расписании сезона {season}." f"Ежедневная проверка: у {team} нет игр в расписании сезона {season}."
) )
continue continue
@@ -1361,19 +1325,19 @@ def daily_rollover_loop(
) )
if today_game: if today_game:
gid = today_game["game"]["id"] gid = today_game["game"]["id"]
notify_info( logger.info(
f"Сегодня у {team} есть игра: gameID={gid}. Перезапуск мониторинга." f"Сегодня у {team} есть игра: gameID={gid}. Перезапуск мониторинга."
) )
monitor_mgr.restart(gid, lang) monitor_mgr.restart(gid, lang)
elif last_played: elif last_played:
gid = last_played["game"]["id"] gid = last_played["game"]["id"]
notify_info( logger.info(
f"Сегодня у {team} нет игры. Последняя сыгранная: gameID={gid}. Мониторинг НЕ запускаем." f"Сегодня у {team} нет игры. Последняя сыгранная: gameID={gid}. Мониторинг НЕ запускаем."
) )
else: else:
notify_info(f"Сегодня у {team} нет игры и нет предыдущих сыгранных.") logger.info(f"Сегодня у {team} нет игры и нет предыдущих сыгранных.")
except Exception as e: except Exception as e:
notify_error(f"Ошибка ежедневной проверки: {e}") logger.warning(f"Ошибка ежедневной проверки: {e}")
class MonitorManager: class MonitorManager:
@@ -1426,8 +1390,7 @@ def main():
) )
args = parser.parse_args() args = parser.parse_args()
logger = setup_logger(args.log_level) logger.info("Запуск программы пользователем: %s", myhost)
logger.info("Запуск программы пользователем: %s", USERNAME)
logger.info("Запуск с параметрами: league=%s, team=%s, lang=%s", args.league, args.team, args.lang) logger.info("Запуск с параметрами: league=%s, team=%s, lang=%s", args.league, args.team, args.lang)
league = validate_league_or_die(args.league) league = validate_league_or_die(args.league)
@@ -1439,7 +1402,7 @@ def main():
# 2) Получить расписание для команды # 2) Получить расписание для команды
team_games = get_team_schedule_or_die(league, season, team, args.lang) team_games = get_team_schedule_or_die(league, season, team, args.lang)
if not team_games: if not team_games:
notify_error("Расписание пустое — работа завершена.") logger.warning("Расписание пустое — работа завершена.")
sys.exit(4) sys.exit(4)
# 3) Найти сегодняшнюю или последнюю сыгранную игру # 3) Найти сегодняшнюю или последнюю сыгранную игру
@@ -1451,7 +1414,7 @@ def main():
if today_game: if today_game:
# В исходном расписании предполагалось наличие game.id # В исходном расписании предполагалось наличие game.id
game_id = today_game["game"]["id"] game_id = today_game["game"]["id"]
notify_info( logger.info(
f"Сегодня у {team} есть игра: gameID={game_id}. Запускаю мониторинг." f"Сегодня у {team} есть игра: gameID={game_id}. Запускаю мониторинг."
) )
monitor_mgr.restart(game_id, args.lang) 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="team1")
Json_Team_Generation(merged, out_dir="static", who="team2") Json_Team_Generation(merged, out_dir="static", who="team2")
notify_info( # print(merged)
logger.info(
f"Сегодня у {team} нет игры. Последняя сыгранная: gameID={game_id}. Мониторинг не запускаю." f"Сегодня у {team} нет игры. Последняя сыгранная: gameID={game_id}. Мониторинг не запускаю."
) )
except Exception as e: except Exception as e:
logging.getLogger("game_watcher").exception( logging.exception(
"Оффлайн-сохранение для gameID=%s упало: %s", game_id, e "Оффлайн-сохранение для gameID=%s упало: %s", game_id, e
) )
else: else:
notify_info(f"Сегодня у {team} нет игры и нет предыдущих сыгранных.") logger.info(f"Сегодня у {team} нет игры и нет предыдущих сыгранных.")
# 4) Ежедневная перекладка расписания # 4) Ежедневная перекладка расписания
stop_event = threading.Event() stop_event = threading.Event()