""" Система мониторинга для телеграм-бота. Включает метрики Prometheus, интеграцию с Sentry и улучшенное логирование. """ import logging import json import time import asyncio from functools import wraps from typing import Optional, Dict, Any, Callable from prometheus_client import Counter, Histogram, Gauge, start_http_server import sentry_sdk from sentry_sdk.integrations.logging import LoggingIntegration from .config import Config class MetricsCollector: """Сборщик метрик для мониторинга""" def __init__(self, config: Config): self.config = config self.logger = logging.getLogger(__name__) # Инициализируем метрики Prometheus self._init_prometheus_metrics() # Инициализируем Sentry если включено self._init_sentry() def _init_prometheus_metrics(self): """Инициализация метрик Prometheus""" # Счетчики ошибок self.error_counter = Counter( 'telegram_bot_errors_total', 'Total number of errors by type', ['error_type', 'handler'] ) # Время выполнения команд self.command_duration = Histogram( 'telegram_bot_command_duration_seconds', 'Time spent processing commands', ['command', 'handler'] ) # Активные пользователи self.active_users = Gauge( 'telegram_bot_active_users', 'Number of active users' ) # Общее количество сообщений self.total_messages = Counter( 'telegram_bot_messages_total', 'Total number of messages processed', ['message_type'] ) # Время отклика API self.api_response_time = Histogram( 'telegram_bot_api_response_time_seconds', 'Response time for external API calls', ['api_name'] ) # Статус бота self.bot_status = Gauge( 'telegram_bot_status', 'Bot operational status (1 = running, 0 = stopped)' ) def _init_sentry(self): """Инициализация Sentry""" if self.config.bot_config.enable_sentry and self.config.bot_config.sentry_dsn: # Настройка интеграции логирования logging_integration = LoggingIntegration( level=logging.INFO, # Захватывать логи уровня INFO и выше event_level=logging.ERROR # Отправлять в Sentry только ERROR и выше ) sentry_sdk.init( dsn=self.config.bot_config.sentry_dsn, integrations=[logging_integration], traces_sample_rate=1.0, # Захватывать все транзакции для мониторинга производительности environment="production" if self.config.is_production() else "development", release="telegram-bot-v1.5.0", # Дополнительные настройки send_default_pii=False, # Не отправлять личную информацию before_send=self._before_send_sentry, # Фильтр событий before_breadcrumb=self._before_breadcrumb_sentry # Фильтр breadcrumbs ) self.logger.info("Sentry initialized successfully") else: self.logger.info("Sentry integration disabled") def record_error(self, error_type: str, handler: str, error: Exception = None): """Запись ошибки в метрики""" self.error_counter.labels(error_type=error_type, handler=handler).inc() if error: self.logger.error( f"Error recorded: {error_type} in {handler}", extra={ 'error_type': error_type, 'handler': handler, 'error_message': str(error), 'error_class': error.__class__.__name__ } ) def record_command(self, command: str, handler: str = None, duration: float = None, user_role: str = None): """Запись выполнения команды""" # Поддержка старого API (command, handler, duration) if handler is not None and duration is not None and user_role is None: self.command_duration.labels(command=command, handler=handler).observe(duration) self.total_messages.labels(message_type='command').inc() # Поддержка нового API с user_role elif user_role is not None and duration is not None: # Можно расширить метрики для учета роли пользователя в будущем self.command_duration.labels(command=command, handler=user_role).observe(duration) self.total_messages.labels(message_type='command').inc() else: # Fallback для некорректных вызовов self.logger.warning(f"Invalid record_command call: command={command}, handler={handler}, duration={duration}, user_role={user_role}") self.total_messages.labels(message_type='command').inc() def record_message(self, message_type: str = 'text'): """Запись обработки сообщения""" self.total_messages.labels(message_type=message_type).inc() def record_api_call(self, api_name: str, duration: float): """Запись вызова внешнего API""" self.api_response_time.labels(api_name=api_name).observe(duration) def update_active_users(self, count: int): """Обновление количества активных пользователей""" self.active_users.set(count) def set_bot_status(self, status: int): """Установка статуса бота""" self.bot_status.set(status) def start_metrics_server(self): """Запуск HTTP сервера для Prometheus метрик""" try: port = self.config.bot_config.prometheus_port start_http_server(port) self.logger.info(f"Prometheus metrics server started on port {port}") except Exception as e: self.logger.error(f"Failed to start Prometheus server: {e}") def _before_send_sentry(self, event, hint): """Фильтр событий перед отправкой в Sentry""" # Не отправлять события с низким приоритетом if event.get('level') == 'info': return None # Не отправлять события от тестовых пользователей if self._is_test_user(event): return None # Добавляем дополнительный контекст if 'user' not in event.get('contexts', {}): event.setdefault('contexts', {})['bot'] = { 'config': { 'enable_ai_processing': self.config.bot_config.enable_ai_processing, 'prometheus_port': self.config.bot_config.prometheus_port } } return event def _before_breadcrumb_sentry(self, breadcrumb, hint): """Фильтр breadcrumbs перед отправкой в Sentry""" # Не отправлять breadcrumbs с логами уровня DEBUG if breadcrumb.get('level') == 'debug': return None # Не отправлять breadcrumbs с конфиденциальной информацией message = breadcrumb.get('message', '') if any(sensitive in message.lower() for sensitive in ['token', 'password', 'key', 'secret']): return None return breadcrumb def _is_test_user(self, event): """Проверка, является ли событие от тестового пользователя""" user = event.get('user', {}) user_id = user.get('id') if user_id: try: user_id = int(user_id) # Считаем тестовыми пользователей с ID > 999999999 (или другой критерий) return user_id > 999999999 except (ValueError, TypeError): pass return False def send_sentry_message(self, message: str, level: str = 'info', extra: Dict = None): """Отправка сообщения в Sentry""" if self.config.bot_config.enable_sentry: sentry_sdk.capture_message(message, level=level, extra=extra) else: self.logger.log(getattr(logging, level.upper(), logging.INFO), message) def set_sentry_context(self, key: str, value: Any): """Установка контекста для Sentry""" if self.config.bot_config.enable_sentry: sentry_sdk.set_context(key, value) def structured_logger(logger_name: str, config: Config) -> logging.Logger: """Создание логгера с структурированным выводом в JSON""" class JsonFormatter(logging.Formatter): """Форматтер для JSON логов""" def format(self, record: logging.LogRecord) -> str: log_entry = { 'timestamp': self.formatTime(record), 'level': record.levelname, 'logger': record.name, 'message': record.getMessage(), 'module': record.module, 'function': record.funcName, 'line': record.lineno } # Добавляем extra данные если есть if hasattr(record, 'extra'): log_entry.update(record.extra) return json.dumps(log_entry, ensure_ascii=False) logger = logging.getLogger(logger_name) # Устанавливаем уровень логирования log_level = getattr(logging, config.get_log_level().upper(), logging.INFO) logger.setLevel(log_level) # Убираем существующие обработчики чтобы избежать дублирования for handler in logger.handlers[:]: logger.removeHandler(handler) # Консольный обработчик console_handler = logging.StreamHandler() console_handler.setLevel(log_level) console_handler.setFormatter(JsonFormatter()) logger.addHandler(console_handler) # Файловый обработчик log_file = config.get('log_file', 'bot.log') file_handler = logging.FileHandler(log_file, encoding='utf-8') file_handler.setLevel(log_level) file_handler.setFormatter(JsonFormatter()) logger.addHandler(file_handler) return logger def measure_time(metric: MetricsCollector, api_name: Optional[str] = None): """Декоратор для измерения времени выполнения""" def decorator(func: Callable) -> Callable: @wraps(func) async def async_wrapper(*args, **kwargs): start_time = time.time() try: result = await func(*args, **kwargs) duration = time.time() - start_time # Записываем метрику если указан API if api_name: metric.record_api_call(api_name, duration) return result except Exception as e: duration = time.time() - start_time if api_name: metric.record_api_call(api_name, duration) raise @wraps(func) def sync_wrapper(*args, **kwargs): start_time = time.time() try: result = func(*args, **kwargs) duration = time.time() - start_time # Записываем метрику если указан API if api_name: metric.record_api_call(api_name, duration) return result except Exception as e: duration = time.time() - start_time if api_name: metric.record_api_call(api_name, duration) raise # Возвращаем соответствующий wrapper в зависимости от типа функции if asyncio.iscoroutinefunction(func): return async_wrapper else: return sync_wrapper return decorator def error_handler(metric: MetricsCollector, handler_name: str): """Декоратор для обработки ошибок в обработчиках""" def decorator(func: Callable) -> Callable: @wraps(func) async def async_wrapper(*args, **kwargs): try: return await func(*args, **kwargs) except Exception as e: metric.record_error(e.__class__.__name__, handler_name, e) raise @wraps(func) def sync_wrapper(*args, **kwargs): try: return func(*args, **kwargs) except Exception as e: metric.record_error(e.__class__.__name__, handler_name, e) raise # Возвращаем соответствующий wrapper if asyncio.iscoroutinefunction(func): return async_wrapper else: return sync_wrapper return decorator