Логирование всех событий
This commit is contained in:
350
bot/middlewares/logging_mdw.py
Normal file
350
bot/middlewares/logging_mdw.py
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
"""
|
||||||
|
Middleware для логирования всех событий бота
|
||||||
|
"""
|
||||||
|
from typing import Callable, Awaitable, Any, Dict, Optional, Tuple
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from aiogram import BaseMiddleware
|
||||||
|
from aiogram.types import (
|
||||||
|
TelegramObject,
|
||||||
|
Update,
|
||||||
|
Message,
|
||||||
|
CallbackQuery,
|
||||||
|
InlineQuery,
|
||||||
|
ChatMemberUpdated
|
||||||
|
)
|
||||||
|
|
||||||
|
from middleware.loggers import logger
|
||||||
|
from ..utils import (
|
||||||
|
username,
|
||||||
|
get_content_type,
|
||||||
|
is_command,
|
||||||
|
parse_command,
|
||||||
|
is_group_chat
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = ('LoggingMiddleware',)
|
||||||
|
|
||||||
|
|
||||||
|
class LoggingMiddleware(BaseMiddleware):
|
||||||
|
"""
|
||||||
|
Middleware для детального логирования всех событий бота.
|
||||||
|
|
||||||
|
Типы логов:
|
||||||
|
- CMD: Команды бота
|
||||||
|
- MSG: Текстовые сообщения
|
||||||
|
- MEDIA: Медиа сообщения
|
||||||
|
- CBD: Callback queries
|
||||||
|
- INLINE: Inline queries
|
||||||
|
- MEMBER: Изменения участников чата
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, project_prefix: str = "PRIMO"):
|
||||||
|
super().__init__()
|
||||||
|
self.project_prefix = project_prefix
|
||||||
|
|
||||||
|
# Статистика
|
||||||
|
self.stats = {
|
||||||
|
'total': 0,
|
||||||
|
'commands': 0,
|
||||||
|
'messages': 0,
|
||||||
|
'callbacks': 0,
|
||||||
|
'errors': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
async def __call__(
|
||||||
|
self,
|
||||||
|
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||||
|
event: TelegramObject,
|
||||||
|
data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""Обрабатывает входящее событие"""
|
||||||
|
self.stats['total'] += 1
|
||||||
|
start_time = datetime.now()
|
||||||
|
|
||||||
|
# Анализируем событие
|
||||||
|
log_info = self._analyze_event(event)
|
||||||
|
|
||||||
|
if not log_info:
|
||||||
|
return await handler(event, data)
|
||||||
|
|
||||||
|
log_type, log_text, user_str = log_info
|
||||||
|
|
||||||
|
# Добавляем префикс проекта
|
||||||
|
prefixed_log_type = f"{self.project_prefix}-{log_type}"
|
||||||
|
|
||||||
|
# Логируем получение события
|
||||||
|
logger.info(text=log_text, log_type=prefixed_log_type, user=user_str)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Выполняем обработчик
|
||||||
|
result = await handler(event, data)
|
||||||
|
|
||||||
|
# Вычисляем время обработки
|
||||||
|
processing_time = (datetime.now() - start_time).total_seconds()
|
||||||
|
|
||||||
|
# Логируем успешное выполнение для команд
|
||||||
|
if log_type == "CMD":
|
||||||
|
self.stats['commands'] += 1
|
||||||
|
logger.debug(
|
||||||
|
text=f"✅ Команда обработана за {processing_time:.3f}s",
|
||||||
|
log_type=prefixed_log_type,
|
||||||
|
user=user_str
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.stats['errors'] += 1
|
||||||
|
logger.error(
|
||||||
|
text=f"❌ Ошибка обработки: {str(e)}",
|
||||||
|
log_type=prefixed_log_type,
|
||||||
|
user=user_str,
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _analyze_event(self, event: TelegramObject) -> Optional[Tuple[str, str, str]]:
|
||||||
|
"""
|
||||||
|
Анализирует событие и извлекает информацию для логирования.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple: (тип_лога, текст_лога, пользователь) или None
|
||||||
|
"""
|
||||||
|
if isinstance(event, Update):
|
||||||
|
return self._analyze_update(event)
|
||||||
|
elif isinstance(event, Message):
|
||||||
|
return self._analyze_message(event)
|
||||||
|
elif isinstance(event, CallbackQuery):
|
||||||
|
return self._analyze_callback(event)
|
||||||
|
elif isinstance(event, InlineQuery):
|
||||||
|
return self._analyze_inline_query(event)
|
||||||
|
elif isinstance(event, ChatMemberUpdated):
|
||||||
|
return self._analyze_member_update(event)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _analyze_update(self, update: Update) -> Optional[Tuple[str, str, str]]:
|
||||||
|
"""Анализирует Update объект"""
|
||||||
|
if update.message:
|
||||||
|
return self._analyze_message(update.message)
|
||||||
|
elif update.edited_message:
|
||||||
|
result = self._analyze_message(update.edited_message)
|
||||||
|
if result:
|
||||||
|
log_type, log_text, user_str = result
|
||||||
|
log_text = f"✏️ [РЕДАКТИРОВАНО] {log_text}"
|
||||||
|
return log_type, log_text, user_str
|
||||||
|
elif update.channel_post:
|
||||||
|
return self._analyze_message(update.channel_post, is_channel=True)
|
||||||
|
elif update.edited_channel_post:
|
||||||
|
result = self._analyze_message(update.edited_channel_post, is_channel=True)
|
||||||
|
if result:
|
||||||
|
log_type, log_text, user_str = result
|
||||||
|
log_text = f"✏️ [РЕДАКТИРОВАНО] {log_text}"
|
||||||
|
return log_type, log_text, user_str
|
||||||
|
elif update.callback_query:
|
||||||
|
return self._analyze_callback(update.callback_query)
|
||||||
|
elif update.inline_query:
|
||||||
|
return self._analyze_inline_query(update.inline_query)
|
||||||
|
elif update.my_chat_member:
|
||||||
|
return self._analyze_member_update(update.my_chat_member)
|
||||||
|
elif update.chat_member:
|
||||||
|
return self._analyze_member_update(update.chat_member)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _analyze_message(self, message: Message, is_channel: bool = False) -> Tuple[str, str, str]:
|
||||||
|
"""Анализирует сообщение"""
|
||||||
|
user_str = username(message)
|
||||||
|
|
||||||
|
# Формируем префикс с информацией о чате
|
||||||
|
chat_info = ""
|
||||||
|
if is_group_chat(message):
|
||||||
|
chat_info = f"[{message.chat.type.upper()} {message.chat.id}] "
|
||||||
|
elif is_channel:
|
||||||
|
chat_info = f"[CHANNEL {message.chat.id}] "
|
||||||
|
else:
|
||||||
|
chat_info = f"[PM {message.chat.id}] "
|
||||||
|
|
||||||
|
# Проверяем команду
|
||||||
|
if message.text and is_command(message.text):
|
||||||
|
self.stats['messages'] += 1
|
||||||
|
parsed = parse_command(message.text)
|
||||||
|
|
||||||
|
if parsed:
|
||||||
|
log_text = f"{chat_info}📝 Команда: /{parsed.command}"
|
||||||
|
|
||||||
|
if parsed.args:
|
||||||
|
args_str = ' '.join(parsed.args[:3])
|
||||||
|
if len(parsed.args) > 3:
|
||||||
|
args_str += f" ... (+{len(parsed.args) - 3})"
|
||||||
|
log_text += f" | Аргументы: {args_str}"
|
||||||
|
|
||||||
|
if parsed.flags:
|
||||||
|
flags_str = ', '.join(f"--{k}" for k in list(parsed.flags.keys())[:3])
|
||||||
|
if len(parsed.flags) > 3:
|
||||||
|
flags_str += f" ... (+{len(parsed.flags) - 3})"
|
||||||
|
log_text += f" | Флаги: {flags_str}"
|
||||||
|
|
||||||
|
return "CMD", log_text, user_str
|
||||||
|
|
||||||
|
# Обычное сообщение
|
||||||
|
self.stats['messages'] += 1
|
||||||
|
|
||||||
|
content_type = get_content_type(message, russian=True)
|
||||||
|
content_emoji = self._get_content_emoji(message)
|
||||||
|
|
||||||
|
# Текстовое сообщение
|
||||||
|
if message.text:
|
||||||
|
text_preview = message.text
|
||||||
|
if len(text_preview) > 100:
|
||||||
|
text_preview = text_preview[:100] + "..."
|
||||||
|
|
||||||
|
log_text = f"{chat_info}{content_emoji} Сообщение ({len(message.text)} симв.): {text_preview!r}"
|
||||||
|
|
||||||
|
# Медиа с caption
|
||||||
|
elif message.caption:
|
||||||
|
caption_preview = message.caption
|
||||||
|
if len(caption_preview) > 50:
|
||||||
|
caption_preview = caption_preview[:50] + "..."
|
||||||
|
|
||||||
|
log_text = f"{chat_info}{content_emoji} {content_type}"
|
||||||
|
|
||||||
|
# Добавляем детали медиа
|
||||||
|
media_details = self._get_media_details_str(message)
|
||||||
|
if media_details:
|
||||||
|
log_text += f" {media_details}"
|
||||||
|
|
||||||
|
log_text += f" | Описание: {caption_preview!r}"
|
||||||
|
|
||||||
|
# Медиа без caption
|
||||||
|
else:
|
||||||
|
log_text = f"{chat_info}{content_emoji} {content_type}"
|
||||||
|
|
||||||
|
media_details = self._get_media_details_str(message)
|
||||||
|
if media_details:
|
||||||
|
log_text += f" {media_details}"
|
||||||
|
|
||||||
|
# Определяем тип лога
|
||||||
|
log_type = "MEDIA" if message.content_type != "text" else "MSG"
|
||||||
|
|
||||||
|
# Добавляем префикс канала
|
||||||
|
if is_channel:
|
||||||
|
log_text = f"📢 {log_text}"
|
||||||
|
|
||||||
|
return log_type, log_text, user_str
|
||||||
|
|
||||||
|
def _analyze_callback(self, callback: CallbackQuery) -> Tuple[str, str, str]:
|
||||||
|
"""Анализирует callback query"""
|
||||||
|
self.stats['callbacks'] += 1
|
||||||
|
|
||||||
|
user_str = f"@{callback.from_user.username}" if callback.from_user.username else f"id{callback.from_user.id}"
|
||||||
|
|
||||||
|
callback_data = callback.data or "None"
|
||||||
|
if len(callback_data) > 50:
|
||||||
|
callback_data = callback_data[:50] + "..."
|
||||||
|
|
||||||
|
chat_info = f"[MSG {callback.message.message_id}] " if callback.message else ""
|
||||||
|
log_text = f"{chat_info}🔘 Callback: {callback_data!r}"
|
||||||
|
|
||||||
|
return "CBD", log_text, user_str
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _analyze_inline_query(inline_query: InlineQuery) -> Tuple[str, str, str]:
|
||||||
|
"""Анализирует inline query"""
|
||||||
|
user_str = f"@{inline_query.from_user.username}" if inline_query.from_user.username else f"id{inline_query.from_user.id}"
|
||||||
|
|
||||||
|
query = inline_query.query or ""
|
||||||
|
if len(query) > 50:
|
||||||
|
query = query[:50] + "..."
|
||||||
|
|
||||||
|
log_text = f"🔍 Inline запрос: {query!r}"
|
||||||
|
|
||||||
|
return "INLINE", log_text, user_str
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _analyze_member_update(update: ChatMemberUpdated) -> Tuple[str, str, str]:
|
||||||
|
"""Анализирует изменения участников"""
|
||||||
|
user_str = f"@{update.from_user.username}" if update.from_user.username else f"id{update.from_user.id}"
|
||||||
|
|
||||||
|
old_status = update.old_chat_member.status
|
||||||
|
new_status = update.new_chat_member.status
|
||||||
|
|
||||||
|
chat_info = f"[{update.chat.type.upper()} {update.chat.id}] "
|
||||||
|
log_text = f"{chat_info}👥 Изменение статуса: {old_status} → {new_status}"
|
||||||
|
|
||||||
|
return "MEMBER", log_text, user_str
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_content_emoji(message: Message) -> str:
|
||||||
|
"""Возвращает emoji для типа контента"""
|
||||||
|
emoji_map = {
|
||||||
|
'text': '💬',
|
||||||
|
'photo': '📷',
|
||||||
|
'video': '🎥',
|
||||||
|
'animation': '🎞️',
|
||||||
|
'audio': '🎵',
|
||||||
|
'voice': '🎤',
|
||||||
|
'video_note': '🎬',
|
||||||
|
'document': '📄',
|
||||||
|
'sticker': '🎨',
|
||||||
|
'location': '📍',
|
||||||
|
'contact': '👤',
|
||||||
|
'poll': '📊',
|
||||||
|
'dice': '🎲'
|
||||||
|
}
|
||||||
|
|
||||||
|
return emoji_map.get(message.content_type, '📎')
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_media_details_str(message: Message) -> Optional[str]:
|
||||||
|
"""Возвращает строку с деталями медиа файла"""
|
||||||
|
from ..utils import get_media_info
|
||||||
|
|
||||||
|
try:
|
||||||
|
media_info = get_media_info(message)
|
||||||
|
details = []
|
||||||
|
|
||||||
|
# Размер файла
|
||||||
|
if 'file_size_mb' in media_info:
|
||||||
|
details.append(f"{media_info['file_size_mb']} MB")
|
||||||
|
elif 'file_size_kb' in media_info:
|
||||||
|
details.append(f"{media_info['file_size_kb']} KB")
|
||||||
|
|
||||||
|
# Длительность
|
||||||
|
if 'duration_formatted' in media_info:
|
||||||
|
details.append(media_info['duration_formatted'])
|
||||||
|
|
||||||
|
# Разрешение
|
||||||
|
if 'width' in media_info and 'height' in media_info:
|
||||||
|
details.append(f"{media_info['width']}x{media_info['height']}")
|
||||||
|
|
||||||
|
return f"({', '.join(details)})" if details else None
|
||||||
|
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_stats(self) -> Dict[str, int]:
|
||||||
|
"""Возвращает статистику middleware"""
|
||||||
|
return self.stats.copy()
|
||||||
|
|
||||||
|
def reset_stats(self):
|
||||||
|
"""Сбрасывает статистику"""
|
||||||
|
self.stats = {
|
||||||
|
'total': 0,
|
||||||
|
'commands': 0,
|
||||||
|
'messages': 0,
|
||||||
|
'callbacks': 0,
|
||||||
|
'errors': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def format_log_stats(stats: Dict[str, int]) -> str:
|
||||||
|
"""Форматирует статистику для вывода"""
|
||||||
|
return (
|
||||||
|
f"📊 Статистика логирования:\n"
|
||||||
|
f"├─ 📨 Всего событий: {stats['total']}\n"
|
||||||
|
f"├─ 📝 Команд: {stats['commands']}\n"
|
||||||
|
f"├─ 💬 Сообщений: {stats['messages']}\n"
|
||||||
|
f"├─ 🔘 Callbacks: {stats['callbacks']}\n"
|
||||||
|
f"└─ ❌ Ошибок: {stats['errors']}"
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user