(thon): add structured logging and improve error handling

Add logging infrastructure across the codebase with a consistent logger named "thon" and replace print statements with appropriate log levels (debug, info, warning, error). Improve error handling in the session processing pipeline by:

- Adding proper error message tracking in Thon.__aiter__
- Using shutil.move instead of os.rename for better cross-platform compatibility
- Enhancing the move utility function with better error handling and retry logic
- Adding debug logging in Converter for session conversion tracking
- Centralizing logging configuration in the main module

This change improves debugging capabilities and provides better visibility into the application's operation while maintaining the same functionality.
This commit is contained in:
2026-04-02 23:06:21 +03:00
parent 174b74f0ab
commit 38078b36b3
4 changed files with 56 additions and 45 deletions

View File

@ -1,3 +1,4 @@
import logging
from pathlib import Path from pathlib import Path
from typing import AsyncGenerator, Callable from typing import AsyncGenerator, Callable
@ -7,6 +8,8 @@ from thon.models.options import ThonOptions
from thon.process import ProcessThon from thon.process import ProcessThon
from thon.utils import move from thon.utils import move
logger = logging.getLogger("thon")
class Thon: class Thon:
def __init__( def __init__(
@ -23,10 +26,16 @@ class Thon:
async def __aiter__(self) -> AsyncGenerator[ThonSession, None]: async def __aiter__(self) -> AsyncGenerator[ThonSession, None]:
async for session in self.__converter: async for session in self.__converter:
error_msg = None
async with ProcessThon(session, self.__options) as thon: async with ProcessThon(session, self.__options) as thon:
if isinstance(thon, str): if isinstance(thon, str):
if "Banned" in thon and self.__move_banned_folder: error_msg = thon
print( else:
yield ThonSession(session, thon)
if error_msg:
if "Banned" in error_msg and self.__move_banned_folder:
logger.info(
f"{session.item.name} | Аккаунт забанен, перемещение файлов..." f"{session.item.name} | Аккаунт забанен, перемещение файлов..."
) )
# Перемещаем файлы сессии с обработкой ошибок # Перемещаем файлы сессии с обработкой ошибок
@ -34,15 +43,14 @@ class Thon:
success_json = await move(session.json_file, "banned") success_json = await move(session.json_file, "banned")
if success_session and success_json: if success_session and success_json:
print( logger.info(
f"{session.item.name} | Файлы успешно перемещены / Files moved successfully" f"{session.item.name} | Файлы успешно перемещены / Files moved successfully"
) )
else: else:
print( logger.error(
f"{session.item.name} | Не удалось полностью переместить файлы / Failed to move all files" f"{session.item.name} | Не удалось полностью переместить файлы / Failed to move all files"
) )
if self.__callback_error is not None: if self.__callback_error is not None:
self.__callback_error() self.__callback_error()
continue continue
continue
yield ThonSession(session, thon)

View File

@ -1,5 +1,6 @@
import asyncio import asyncio
import contextlib import contextlib
import logging
from pathlib import Path from pathlib import Path
from typing import AsyncGenerator from typing import AsyncGenerator
@ -11,6 +12,8 @@ from thon.proxy import ProxyParser
from thon.session import ThonSession from thon.session import ThonSession
from thon.utils import json_write from thon.utils import json_write
logger = logging.getLogger("thon")
class Converter: class Converter:
def __init__(self, sessions_folder: Path = Path("sessions"), proxy: str = ""): def __init__(self, sessions_folder: Path = Path("sessions"), proxy: str = ""):
@ -36,6 +39,7 @@ class Converter:
Конвертация сессии в json (string_session) Конвертация сессии в json (string_session)
Возвращает объект Session Возвращает объект Session
""" """
logger.debug(f"Converting session: {session.item.name}")
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
client = TelegramClient(str(session.item), self.__api_id, self.__api_hash) client = TelegramClient(str(session.item), self.__api_id, self.__api_hash)

View File

@ -3,7 +3,7 @@ import contextlib
import logging import logging
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from random import randint from random import randint
from typing import Self from typing import Self # ty:ignore[unresolved-import]
from telethon import TelegramClient from telethon import TelegramClient
from telethon.tl.functions.account import UpdateStatusRequest from telethon.tl.functions.account import UpdateStatusRequest
@ -25,6 +25,9 @@ from thon.models.options import ThonOptions
from thon.models.session import Session from thon.models.session import Session
from thon.utils import extract_verification_code from thon.utils import extract_verification_code
logger = logging.getLogger("thon")
API_PACKS = { API_PACKS = {
4: "android", 4: "android",
5: "android", 5: "android",
@ -46,7 +49,6 @@ class ProcessThon(Data):
self.__item = session.item self.__item = session.item
self.__retries = options.retries self.__retries = options.retries
self.__timeout = options.timeout self.__timeout = options.timeout
self._logger = logging.getLogger("thon")
self._async_check_timeout = options.check_timeout self._async_check_timeout = options.check_timeout
super().__init__(session.json_data) super().__init__(session.json_data)
self.__client = self.__get_client() self.__client = self.__get_client()
@ -59,7 +61,7 @@ class ProcessThon(Data):
def __get_client(self) -> TelegramClient: def __get_client(self) -> TelegramClient:
__session = str(self.__item) if self.__item else self.string_session __session = str(self.__item) if self.__item else self.string_session
proxy = self.proxy proxy = self.proxy
self._logger.info(f"{self.session_file} | {proxy}") logger.debug(f"{self.session_file} | {proxy}")
client = TelegramClient( client = TelegramClient(
session=__session, session=__session,
api_id=self.app_id, api_id=self.app_id,
@ -125,7 +127,7 @@ class ProcessThon(Data):
return f"{self.session_file} | Забанен / Banned" return f"{self.session_file} | Забанен / Banned"
except Exception as e: except Exception as e:
await self.disconnect() await self.disconnect()
self._logger.exception(e) logger.exception(e)
return f"{self.session_file} | Ошибка авторизации / AuthorizationError: {e}" return f"{self.session_file} | Ошибка авторизации / AuthorizationError: {e}"
async def check(self) -> str: async def check(self) -> str:
@ -184,7 +186,7 @@ class ProcessThon(Data):
async def __aenter__(self) -> Self | str: async def __aenter__(self) -> Self | str:
r = await self.check() r = await self.check()
self._logger.info(f"{self.session_file} | {r}") logger.info(f"{self.session_file} | {r}")
if r != "OK": if r != "OK":
await self.disconnect() await self.disconnect()
return r return r

View File

@ -1,11 +1,14 @@
import asyncio import asyncio
import json import json
import os import logging
import re import re
import shutil
from pathlib import Path from pathlib import Path
import aiofiles import aiofiles
logger = logging.getLogger("thon")
async def json_read(path: Path) -> dict: async def json_read(path: Path) -> dict:
try: try:
@ -28,7 +31,9 @@ def extract_verification_code(text: str, regexp: str) -> str | None:
async def move( async def move(
path: Path, move_folder_name: str = "banned", max_retries: int = 5 path: Path,
move_folder_name: str = "banned",
max_retries: int = 5,
) -> bool: ) -> bool:
""" """
Перемещает файл с retry-логикой, если файл занят. Перемещает файл с retry-логикой, если файл занят.
@ -46,39 +51,31 @@ async def move(
try: try:
# Проверяем, что файл существует и доступен для чтения # Проверяем, что файл существует и доступен для чтения
if not path.exists(): if not path.exists():
print(f"{path.name} | Файл не найден / File not found") logger.error(f"{path.name} | Файл не найден / File not found")
return False return False
if not path.is_file(): if not path.is_file():
print(f"{path.name} | Это не файл / Not a file") logger.error(f"{path.name} | Это не файл / Not a file")
return False return False
# Попытка переместить файл # Попытка переместить файл
await loop.run_in_executor(None, os.rename, path, _folder / path.name) await loop.run_in_executor(
None, shutil.move, str(path), str(_folder / path.name)
)
return True return True
except OSError: except OSError:
if attempt < max_retries - 1: if attempt < max_retries - 1:
# Ждем перед следующей попыткой (с экспоненциальной задержкой) # Ждем перед следующей попыткой (с экспоненциальной задержкой)
wait_time = 0.5 * (2**attempt) # 0.5s, 1s, 2s, 4s... wait_time = 0.5 * (2**attempt) # 0.5s, 1s, 2s, 4s...
print( logger.warning(
f"{path.name} | Попытка {attempt + 1}/{max_retries}: файл занят, ждем {wait_time}s..." f"{path.name} | Попытка {attempt + 1}/{max_retries}: файл занят, ждем {wait_time}s..."
) )
await asyncio.sleep(wait_time) await asyncio.sleep(wait_time)
else: else:
# Последняя попытка неудачна - попробуем копию # Последняя попытка неудачна
print( logger.error(
f"{path.name} | Ошибка перемещения / Failed to move (final attempt), trying copy..." f"{path.name} | Ошибка перемещения / Failed to move (final attempt)"
)
try:
await loop.run_in_executor(
None, os.replace, path, _folder / path.name
)
print(f"{path.name} | Файл перемещен через копию / Moved via copy")
return True
except OSError as e:
print(
f"{path.name} | Не удалось ни переместить, ни скопировать / Failed to move or copy: {e}"
) )
return False return False