mirror of
https://github.com/FerraSoft/bottohelp.git
synced 2026-08-06 21:55:03 +00:00
339 lines
14 KiB
Python
339 lines
14 KiB
Python
"""
|
|
Система мониторинга для телеграм-бота.
|
|
Включает метрики 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 |