Implement user-specific question numbering and update database schema. Added triggers for automatic question numbering and adjustments upon deletion. Enhanced CRUD operations to manage user_question_number effectively.
This commit is contained in:
31
services/infrastructure/__init__.py
Normal file
31
services/infrastructure/__init__.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
Инфраструктурные сервисы
|
||||
"""
|
||||
|
||||
from .database import DatabaseService
|
||||
from .logger import get_logger, setup_logging
|
||||
from .metrics import MetricsService, get_metrics_service
|
||||
from .pid_manager import PIDManager, get_pid_manager, cleanup_pid_file
|
||||
from .logging_decorators import (
|
||||
log_function_call, log_business_event, log_fsm_transition,
|
||||
log_handler, log_service, log_business, log_fsm,
|
||||
log_quiet, log_middleware, log_utility
|
||||
)
|
||||
from .logging_utils import (
|
||||
LoggingContext, get_logging_context,
|
||||
log_user_action, log_business_operation, log_fsm_event, log_performance,
|
||||
log_question_created, log_question_answered, log_user_created, log_user_blocked
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'DatabaseService',
|
||||
'get_logger', 'setup_logging',
|
||||
'MetricsService', 'get_metrics_service',
|
||||
'PIDManager', 'get_pid_manager', 'cleanup_pid_file',
|
||||
'log_function_call', 'log_business_event', 'log_fsm_transition',
|
||||
'log_handler', 'log_service', 'log_business', 'log_fsm',
|
||||
'log_quiet', 'log_middleware', 'log_utility',
|
||||
'LoggingContext', 'get_logging_context',
|
||||
'log_user_action', 'log_business_operation', 'log_fsm_event', 'log_performance',
|
||||
'log_question_created', 'log_question_answered', 'log_user_created', 'log_user_blocked'
|
||||
]
|
||||
255
services/infrastructure/database.py
Normal file
255
services/infrastructure/database.py
Normal file
@@ -0,0 +1,255 @@
|
||||
"""
|
||||
Сервис для работы с базой данных SQLite
|
||||
"""
|
||||
import aiosqlite
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Dict, Any, Tuple
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
|
||||
from models.user import User
|
||||
from models.question import Question, QuestionStatus
|
||||
from models.user_block import UserBlock
|
||||
from models.user_settings import UserSettings
|
||||
from database.crud import UserCRUD, QuestionCRUD, UserBlockCRUD, UserSettingsCRUD
|
||||
from .logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class DatabaseService:
|
||||
"""Сервис для работы с базой данных"""
|
||||
|
||||
def __init__(self, db_path: str):
|
||||
self.db_path = db_path
|
||||
# Инициализируем CRUD операции
|
||||
self.users = UserCRUD(db_path)
|
||||
self.questions = QuestionCRUD(db_path)
|
||||
self.user_blocks = UserBlockCRUD(db_path)
|
||||
self.user_settings = UserSettingsCRUD(db_path)
|
||||
|
||||
async def init(self):
|
||||
"""Инициализация базы данных и создание таблиц"""
|
||||
logger.info(f"💾 Инициализация базы данных: {self.db_path}")
|
||||
# Создаем директорию для базы данных если её нет
|
||||
db_path = Path(self.db_path)
|
||||
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
async with self.get_connection() as conn:
|
||||
await self._create_tables(conn)
|
||||
logger.info("✅ База данных инициализирована")
|
||||
|
||||
@asynccontextmanager
|
||||
async def get_connection(self):
|
||||
"""Контекстный менеджер для подключения к БД с использованием пула"""
|
||||
from database.crud import get_connection_pool
|
||||
pool = get_connection_pool(self.db_path)
|
||||
conn = await pool.get_connection()
|
||||
try:
|
||||
yield conn
|
||||
finally:
|
||||
await pool.return_connection(conn)
|
||||
|
||||
async def _create_tables(self, conn: aiosqlite.Connection):
|
||||
"""Создание таблиц в базе данных"""
|
||||
# Проверяем, существуют ли уже таблицы
|
||||
cursor = await conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='users';")
|
||||
if await cursor.fetchone():
|
||||
logger.info("📋 Таблицы уже существуют, пропускаем создание")
|
||||
return
|
||||
|
||||
# Читаем схему из файла
|
||||
schema_path = Path(__file__).parent.parent / "database" / "schema.sql"
|
||||
|
||||
if schema_path.exists():
|
||||
logger.info("📄 Создание таблиц из схемы")
|
||||
with open(schema_path, 'r', encoding='utf-8') as f:
|
||||
schema_sql = f.read()
|
||||
|
||||
# Выполняем SQL схему
|
||||
await conn.executescript(schema_sql)
|
||||
await conn.commit()
|
||||
logger.info("✅ Таблицы созданы из схемы")
|
||||
else:
|
||||
logger.warning("⚠️ Файл схемы не найден, создаем таблицы вручную")
|
||||
await self._create_tables_manual(conn)
|
||||
|
||||
async def _create_tables_manual(self, conn: aiosqlite.Connection):
|
||||
"""Создание таблиц вручную если схема не найдена"""
|
||||
# Простая схема для совместимости
|
||||
await conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
telegram_id INTEGER UNIQUE NOT NULL,
|
||||
username TEXT,
|
||||
first_name TEXT NOT NULL,
|
||||
last_name TEXT,
|
||||
chat_id INTEGER NOT NULL,
|
||||
profile_link TEXT UNIQUE NOT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
banned_until DATETIME,
|
||||
ban_reason TEXT
|
||||
)
|
||||
""")
|
||||
|
||||
await conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS questions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
from_user_id INTEGER,
|
||||
to_user_id INTEGER NOT NULL,
|
||||
message_text TEXT NOT NULL,
|
||||
answer_text TEXT,
|
||||
is_anonymous BOOLEAN DEFAULT TRUE,
|
||||
message_id INTEGER,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
answered_at DATETIME,
|
||||
is_read BOOLEAN DEFAULT FALSE,
|
||||
status TEXT DEFAULT 'pending'
|
||||
)
|
||||
""")
|
||||
|
||||
await conn.commit()
|
||||
|
||||
# Обертки для CRUD операций (для совместимости)
|
||||
|
||||
# Пользователи
|
||||
async def create_user(self, user: User) -> User:
|
||||
"""Создание нового пользователя"""
|
||||
return await self.users.create(user)
|
||||
|
||||
async def create_users_batch(self, users: List[User]) -> List[User]:
|
||||
"""Создание нескольких пользователей за одну транзакцию (batch операция)"""
|
||||
return await self.users.create_batch(users)
|
||||
|
||||
async def get_user(self, telegram_id: int) -> Optional[User]:
|
||||
"""Получение пользователя по Telegram ID"""
|
||||
return await self.users.get_by_telegram_id(telegram_id)
|
||||
|
||||
async def get_user_by_profile_link(self, profile_link: str) -> Optional[User]:
|
||||
"""Получение пользователя по ссылке профиля"""
|
||||
return await self.users.get_by_profile_link(profile_link)
|
||||
|
||||
async def update_user(self, user: User) -> User:
|
||||
"""Обновление пользователя"""
|
||||
return await self.users.update(user)
|
||||
|
||||
async def get_all_users(self, limit: int = 100, offset: int = 0) -> List[User]:
|
||||
"""Получение всех пользователей"""
|
||||
return await self.users.get_all(limit, offset)
|
||||
|
||||
async def get_all_users_cursor(self, last_id: int, last_created_at: str,
|
||||
limit: int, direction: str = "desc") -> List[User]:
|
||||
"""Получение пользователей с cursor-based пагинацией"""
|
||||
return await self.users.get_all_users_cursor(last_id, last_created_at, limit, direction)
|
||||
|
||||
async def get_all_users_asc(self, limit: int = 100, offset: int = 0) -> List[User]:
|
||||
"""Получение всех пользователей в порядке возрастания"""
|
||||
return await self.users.get_all_users_asc(limit, offset)
|
||||
|
||||
async def get_users_stats(self) -> Dict[str, Any]:
|
||||
"""Получение статистики пользователей"""
|
||||
return await self.users.get_stats()
|
||||
|
||||
# Вопросы
|
||||
async def create_question(self, question: Question) -> Question:
|
||||
"""Создание нового вопроса"""
|
||||
return await self.questions.create(question)
|
||||
|
||||
async def create_questions_batch(self, questions: List[Question]) -> List[Question]:
|
||||
"""Создание нескольких вопросов за одну транзакцию (batch операция)"""
|
||||
return await self.questions.create_batch(questions)
|
||||
|
||||
async def get_question(self, question_id: int) -> Optional[Question]:
|
||||
"""Получение вопроса по ID"""
|
||||
return await self.questions.get_by_id(question_id)
|
||||
|
||||
async def get_user_questions(self, user_id: int, status: Optional[QuestionStatus] = None,
|
||||
limit: int = 50, offset: int = 0) -> List[Question]:
|
||||
"""Получение вопросов пользователя"""
|
||||
return await self.questions.get_by_to_user(user_id, status, limit, offset)
|
||||
|
||||
async def get_user_questions_with_authors(self, user_id: int, status: Optional[QuestionStatus] = None,
|
||||
limit: int = 50, offset: int = 0) -> List[Tuple[Question, Optional[User]]]:
|
||||
"""Получение вопросов пользователя с информацией об авторах (оптимизированный запрос)"""
|
||||
return await self.questions.get_by_to_user_with_authors(user_id, status, limit, offset)
|
||||
|
||||
async def get_user_questions_cursor(self, user_id: int, last_id: int, last_created_at: str,
|
||||
limit: int, direction: str = "desc") -> List[Question]:
|
||||
"""Получение вопросов пользователя с cursor-based пагинацией"""
|
||||
return await self.questions.get_by_to_user_cursor(user_id, last_id, last_created_at, limit, direction)
|
||||
|
||||
async def get_user_questions_asc(self, user_id: int, status: Optional[QuestionStatus] = None,
|
||||
limit: int = 50, offset: int = 0) -> List[Question]:
|
||||
"""Получение вопросов пользователя в порядке возрастания"""
|
||||
return await self.questions.get_by_to_user_asc(user_id, status, limit, offset)
|
||||
|
||||
async def update_question(self, question: Question) -> Question:
|
||||
"""Обновление вопроса"""
|
||||
return await self.questions.update(question)
|
||||
|
||||
async def get_questions_stats(self) -> Dict[str, Any]:
|
||||
"""Получение статистики вопросов"""
|
||||
return await self.questions.get_stats()
|
||||
|
||||
async def get_unread_questions_count(self, user_id: int) -> int:
|
||||
"""Получение количества непрочитанных вопросов"""
|
||||
return await self.questions.get_unread_count(user_id)
|
||||
|
||||
async def get_user_questions_count(self, user_id: int, status: Optional[QuestionStatus] = None) -> int:
|
||||
"""Получение общего количества вопросов пользователя"""
|
||||
return await self.questions.get_count_by_to_user(user_id, status)
|
||||
|
||||
# Блокировки
|
||||
async def block_user(self, blocker_id: int, blocked_id: int) -> UserBlock:
|
||||
"""Блокировка пользователя"""
|
||||
user_block = UserBlock(
|
||||
blocker_id=blocker_id,
|
||||
blocked_id=blocked_id,
|
||||
created_at=datetime.now()
|
||||
)
|
||||
return await self.user_blocks.create(user_block)
|
||||
|
||||
async def unblock_user(self, blocker_id: int, blocked_id: int) -> bool:
|
||||
"""Разблокировка пользователя"""
|
||||
return await self.user_blocks.delete(blocker_id, blocked_id)
|
||||
|
||||
async def is_user_blocked(self, blocker_id: int, blocked_id: int) -> bool:
|
||||
"""Проверка, заблокирован ли пользователь"""
|
||||
return await self.user_blocks.is_blocked(blocker_id, blocked_id)
|
||||
|
||||
# Настройки
|
||||
async def get_user_settings(self, user_id: int) -> Optional[UserSettings]:
|
||||
"""Получение настроек пользователя"""
|
||||
return await self.user_settings.get_by_user_id(user_id)
|
||||
|
||||
async def get_user_by_id(self, user_id: int) -> Optional[User]:
|
||||
"""Получение пользователя по ID (для получения информации об авторах вопросов)"""
|
||||
return await self.users.get_by_telegram_id(user_id)
|
||||
|
||||
async def update_user_settings(self, settings: UserSettings) -> UserSettings:
|
||||
"""Обновление настроек пользователя"""
|
||||
return await self.user_settings.update(settings)
|
||||
|
||||
async def create_user_settings(self, settings: UserSettings) -> UserSettings:
|
||||
"""Создание настроек пользователя"""
|
||||
return await self.user_settings.create(settings)
|
||||
|
||||
async def check_connection(self):
|
||||
"""Проверка соединения с базой данных"""
|
||||
try:
|
||||
async with self.get_connection() as conn:
|
||||
# Выполняем простой запрос для проверки соединения
|
||||
cursor = await conn.execute("SELECT 1")
|
||||
await cursor.fetchone()
|
||||
logger.debug("Database connection check successful")
|
||||
except Exception as e:
|
||||
logger.error(f"Database connection check failed: {e}")
|
||||
raise
|
||||
|
||||
async def close(self):
|
||||
"""Закрытие соединения с БД"""
|
||||
from database.crud import get_connection_pool
|
||||
pool = get_connection_pool(self.db_path)
|
||||
await pool.close_all()
|
||||
350
services/infrastructure/http_server.py
Normal file
350
services/infrastructure/http_server.py
Normal file
@@ -0,0 +1,350 @@
|
||||
"""
|
||||
HTTP сервер для эндпоинтов метрик и health check
|
||||
"""
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from aiohttp import ClientSession, web
|
||||
from aiohttp.web import Request, Response
|
||||
from loguru import logger
|
||||
|
||||
from config.constants import DEFAULT_HTTP_HOST, DEFAULT_HTTP_PORT, APP_VERSION, HTTP_STATUS_OK, HTTP_STATUS_SERVICE_UNAVAILABLE, HTTP_STATUS_INTERNAL_SERVER_ERROR
|
||||
from dependencies import get_database_service
|
||||
from .metrics import get_metrics_service
|
||||
|
||||
|
||||
class HTTPServer:
|
||||
"""HTTP сервер для метрик и health check"""
|
||||
|
||||
def __init__(self, host: str = DEFAULT_HTTP_HOST, port: int = DEFAULT_HTTP_PORT):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.app = web.Application()
|
||||
self.metrics_service = get_metrics_service()
|
||||
self.database_service = get_database_service()
|
||||
self.start_time = time.time()
|
||||
self._setup_routes()
|
||||
|
||||
def _setup_routes(self):
|
||||
"""Настройка маршрутов"""
|
||||
self.app.router.add_get('/metrics', self.metrics_handler)
|
||||
self.app.router.add_get('/health', self.health_handler)
|
||||
self.app.router.add_get('/ready', self.ready_handler)
|
||||
self.app.router.add_get('/status', self.status_handler)
|
||||
self.app.router.add_get('/', self.root_handler)
|
||||
|
||||
async def metrics_handler(self, request: Request) -> Response:
|
||||
"""Обработчик эндпоинта /metrics"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# Получаем метрики
|
||||
metrics_data = self.metrics_service.get_metrics()
|
||||
content_type = self.metrics_service.get_content_type()
|
||||
|
||||
# Записываем метрику HTTP запроса
|
||||
duration = time.time() - start_time
|
||||
self.metrics_service.record_http_request_duration("GET", "/metrics", duration)
|
||||
self.metrics_service.increment_http_requests("GET", "/metrics", HTTP_STATUS_OK)
|
||||
|
||||
return Response(
|
||||
text=metrics_data,
|
||||
content_type=content_type,
|
||||
status=HTTP_STATUS_OK
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in metrics handler: {e}")
|
||||
duration = time.time() - start_time
|
||||
self.metrics_service.record_http_request_duration("GET", "/metrics", duration)
|
||||
self.metrics_service.increment_http_requests("GET", "/metrics", HTTP_STATUS_INTERNAL_SERVER_ERROR)
|
||||
self.metrics_service.increment_errors(type(e).__name__, "metrics_handler")
|
||||
|
||||
return Response(
|
||||
text="Internal Server Error",
|
||||
status=HTTP_STATUS_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
async def health_handler(self, request: Request) -> Response:
|
||||
"""Обработчик эндпоинта /health"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# Проверяем состояние сервисов
|
||||
health_status = {
|
||||
"status": "healthy",
|
||||
"timestamp": time.time(),
|
||||
"uptime": time.time() - self.start_time,
|
||||
"version": APP_VERSION,
|
||||
"services": {}
|
||||
}
|
||||
|
||||
# Проверяем базу данных
|
||||
try:
|
||||
await self.database_service.check_connection()
|
||||
health_status["services"]["database"] = "healthy"
|
||||
except Exception as e:
|
||||
health_status["services"]["database"] = f"unhealthy: {str(e)}"
|
||||
health_status["status"] = "unhealthy"
|
||||
|
||||
# Проверяем метрики
|
||||
try:
|
||||
self.metrics_service.get_metrics()
|
||||
health_status["services"]["metrics"] = "healthy"
|
||||
except Exception as e:
|
||||
health_status["services"]["metrics"] = f"unhealthy: {str(e)}"
|
||||
health_status["status"] = "unhealthy"
|
||||
|
||||
# Определяем HTTP статус
|
||||
http_status = HTTP_STATUS_OK if health_status["status"] == "healthy" else HTTP_STATUS_SERVICE_UNAVAILABLE
|
||||
|
||||
# Записываем метрику HTTP запроса
|
||||
duration = time.time() - start_time
|
||||
self.metrics_service.record_http_request_duration("GET", "/health", duration)
|
||||
self.metrics_service.increment_http_requests("GET", "/health", http_status)
|
||||
|
||||
return Response(
|
||||
json=health_status,
|
||||
status=http_status
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in health handler: {e}")
|
||||
duration = time.time() - start_time
|
||||
self.metrics_service.record_http_request_duration("GET", "/health", duration)
|
||||
self.metrics_service.increment_http_requests("GET", "/health", 500)
|
||||
self.metrics_service.increment_errors(type(e).__name__, "health_handler")
|
||||
|
||||
return Response(
|
||||
json={"status": "error", "message": str(e)},
|
||||
status=HTTP_STATUS_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
async def ready_handler(self, request: Request) -> Response:
|
||||
"""Обработчик эндпоинта /ready (readiness probe)"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# Проверяем готовность сервисов
|
||||
ready_status = {
|
||||
"status": "ready",
|
||||
"timestamp": time.time(),
|
||||
"services": {}
|
||||
}
|
||||
|
||||
# Проверяем базу данных
|
||||
try:
|
||||
await self.database_service.check_connection()
|
||||
ready_status["services"]["database"] = "ready"
|
||||
except Exception as e:
|
||||
ready_status["services"]["database"] = f"not_ready: {str(e)}"
|
||||
ready_status["status"] = "not_ready"
|
||||
|
||||
# Определяем HTTP статус
|
||||
http_status = HTTP_STATUS_OK if ready_status["status"] == "ready" else HTTP_STATUS_SERVICE_UNAVAILABLE
|
||||
|
||||
# Записываем метрику HTTP запроса
|
||||
duration = time.time() - start_time
|
||||
self.metrics_service.record_http_request_duration("GET", "/ready", duration)
|
||||
self.metrics_service.increment_http_requests("GET", "/ready", http_status)
|
||||
|
||||
return Response(
|
||||
json=ready_status,
|
||||
status=http_status
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in ready handler: {e}")
|
||||
duration = time.time() - start_time
|
||||
self.metrics_service.record_http_request_duration("GET", "/ready", duration)
|
||||
self.metrics_service.increment_http_requests("GET", "/ready", 500)
|
||||
self.metrics_service.increment_errors(type(e).__name__, "ready_handler")
|
||||
|
||||
return Response(
|
||||
json={"status": "error", "message": str(e)},
|
||||
status=HTTP_STATUS_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
async def status_handler(self, request: Request) -> Response:
|
||||
"""Handle /status endpoint for process status information."""
|
||||
try:
|
||||
import os
|
||||
import time
|
||||
import psutil
|
||||
|
||||
# Получаем PID текущего процесса
|
||||
current_pid = os.getpid()
|
||||
|
||||
try:
|
||||
# Получаем информацию о процессе
|
||||
process = psutil.Process(current_pid)
|
||||
create_time = process.create_time()
|
||||
uptime_seconds = time.time() - create_time
|
||||
|
||||
# Логируем для диагностики
|
||||
import datetime
|
||||
create_time_str = datetime.datetime.fromtimestamp(create_time).strftime('%Y-%m-%d %H:%M:%S')
|
||||
current_time_str = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
logger.info(f"Process PID {current_pid}: created at {create_time_str}, current time {current_time_str}, uptime {uptime_seconds:.1f}s")
|
||||
|
||||
# Форматируем uptime
|
||||
if uptime_seconds < 60:
|
||||
uptime_str = f"{int(uptime_seconds)}с"
|
||||
elif uptime_seconds < 3600:
|
||||
minutes = int(uptime_seconds // 60)
|
||||
uptime_str = f"{minutes}м"
|
||||
elif uptime_seconds < 86400:
|
||||
hours = int(uptime_seconds // 3600)
|
||||
minutes = int((uptime_seconds % 3600) // 60)
|
||||
uptime_str = f"{hours}ч {minutes}м"
|
||||
else:
|
||||
days = int(uptime_seconds // 86400)
|
||||
hours = int((uptime_seconds % 86400) // 3600)
|
||||
uptime_str = f"{days}д {hours}ч"
|
||||
|
||||
# Проверяем, что процесс активен
|
||||
if process.is_running():
|
||||
status = "running"
|
||||
else:
|
||||
status = "stopped"
|
||||
|
||||
# Формируем ответ
|
||||
response_data = {
|
||||
"status": status,
|
||||
"pid": current_pid,
|
||||
"uptime": uptime_str,
|
||||
"memory_usage_mb": round(process.memory_info().rss / 1024 / 1024, 2),
|
||||
"cpu_percent": process.cpu_percent(),
|
||||
"timestamp": time.time()
|
||||
}
|
||||
|
||||
import json
|
||||
return Response(
|
||||
text=json.dumps(response_data, ensure_ascii=False),
|
||||
content_type='application/json',
|
||||
status=200
|
||||
)
|
||||
|
||||
except psutil.NoSuchProcess:
|
||||
# Процесс не найден
|
||||
response_data = {
|
||||
"status": "not_found",
|
||||
"error": "Process not found",
|
||||
"timestamp": time.time()
|
||||
}
|
||||
|
||||
import json
|
||||
return Response(
|
||||
text=json.dumps(response_data, ensure_ascii=False),
|
||||
content_type='application/json',
|
||||
status=404
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Status check failed: {e}")
|
||||
import json
|
||||
response_data = {
|
||||
"status": "error",
|
||||
"error": str(e),
|
||||
"timestamp": time.time()
|
||||
}
|
||||
|
||||
return Response(
|
||||
text=json.dumps(response_data, ensure_ascii=False),
|
||||
content_type='application/json',
|
||||
status=500
|
||||
)
|
||||
|
||||
async def root_handler(self, request: Request) -> Response:
|
||||
"""Обработчик корневого эндпоинта"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
info = {
|
||||
"service": "AnonBot",
|
||||
"version": APP_VERSION,
|
||||
"endpoints": {
|
||||
"metrics": "/metrics",
|
||||
"health": "/health",
|
||||
"ready": "/ready",
|
||||
"status": "/status"
|
||||
},
|
||||
"uptime": time.time() - self.start_time
|
||||
}
|
||||
|
||||
# Записываем метрику HTTP запроса
|
||||
duration = time.time() - start_time
|
||||
self.metrics_service.record_http_request_duration("GET", "/", duration)
|
||||
self.metrics_service.increment_http_requests("GET", "/", 200)
|
||||
|
||||
return Response(
|
||||
json=info,
|
||||
status=HTTP_STATUS_OK
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in root handler: {e}")
|
||||
duration = time.time() - start_time
|
||||
self.metrics_service.record_http_request_duration("GET", "/", duration)
|
||||
self.metrics_service.increment_http_requests("GET", "/", 500)
|
||||
self.metrics_service.increment_errors(type(e).__name__, "root_handler")
|
||||
|
||||
return Response(
|
||||
json={"error": str(e)},
|
||||
status=HTTP_STATUS_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
async def start(self):
|
||||
"""Запуск HTTP сервера"""
|
||||
try:
|
||||
runner = web.AppRunner(self.app)
|
||||
await runner.setup()
|
||||
site = web.TCPSite(runner, self.host, self.port)
|
||||
await site.start()
|
||||
|
||||
logger.info(f"HTTP server started on {self.host}:{self.port}")
|
||||
logger.info(f"Metrics endpoint: http://{self.host}:{self.port}/metrics")
|
||||
logger.info(f"Health endpoint: http://{self.host}:{self.port}/health")
|
||||
logger.info(f"Ready endpoint: http://{self.host}:{self.port}/ready")
|
||||
logger.info(f"Status endpoint: http://{self.host}:{self.port}/status")
|
||||
|
||||
return runner
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start HTTP server: {e}")
|
||||
self.metrics_service.increment_errors(type(e).__name__, "http_server")
|
||||
raise
|
||||
|
||||
async def stop(self, runner: web.AppRunner):
|
||||
"""Остановка HTTP сервера"""
|
||||
try:
|
||||
await runner.cleanup()
|
||||
logger.info("HTTP server stopped")
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping HTTP server: {e}")
|
||||
self.metrics_service.increment_errors(type(e).__name__, "http_server")
|
||||
|
||||
|
||||
# Глобальный экземпляр HTTP сервера
|
||||
_http_server: Optional[HTTPServer] = None
|
||||
|
||||
|
||||
def get_http_server(host: str = DEFAULT_HTTP_HOST, port: int = DEFAULT_HTTP_PORT) -> HTTPServer:
|
||||
"""Получить экземпляр HTTP сервера"""
|
||||
global _http_server
|
||||
if _http_server is None:
|
||||
_http_server = HTTPServer(host, port)
|
||||
return _http_server
|
||||
|
||||
|
||||
async def start_http_server(host: str = DEFAULT_HTTP_HOST, port: int = DEFAULT_HTTP_PORT) -> web.AppRunner:
|
||||
"""Запустить HTTP сервер"""
|
||||
server = get_http_server(host, port)
|
||||
return await server.start()
|
||||
|
||||
|
||||
async def stop_http_server(runner: web.AppRunner):
|
||||
"""Остановить HTTP сервер"""
|
||||
server = get_http_server()
|
||||
await server.stop(runner)
|
||||
83
services/infrastructure/logger.py
Normal file
83
services/infrastructure/logger.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
Настройка системы логирования с использованием loguru
|
||||
"""
|
||||
import sys
|
||||
from loguru import logger
|
||||
from config import config
|
||||
|
||||
|
||||
def setup_logging():
|
||||
"""Настройка системы логирования"""
|
||||
# Удаляем стандартный обработчик loguru
|
||||
logger.remove()
|
||||
|
||||
# Настраиваем логирование в stderr для Docker
|
||||
log_level = "DEBUG" if config.DEBUG else "INFO"
|
||||
|
||||
# Основной обработчик для stderr (для Docker)
|
||||
logger.add(
|
||||
sys.stderr,
|
||||
level=log_level,
|
||||
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
|
||||
colorize=True,
|
||||
backtrace=True,
|
||||
diagnose=True
|
||||
)
|
||||
|
||||
# Дополнительный обработчик для файла (опционально)
|
||||
if config.DEBUG:
|
||||
logger.add(
|
||||
"logs/bot.log",
|
||||
level="DEBUG",
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
|
||||
rotation="10 MB",
|
||||
retention="7 days",
|
||||
compression="zip",
|
||||
backtrace=True,
|
||||
diagnose=True
|
||||
)
|
||||
|
||||
# Настраиваем логирование для внешних библиотек
|
||||
import logging
|
||||
|
||||
# Отключаем логирование aiogram по умолчанию
|
||||
logging.getLogger("aiogram").setLevel(logging.WARNING)
|
||||
logging.getLogger("aiohttp").setLevel(logging.WARNING)
|
||||
logging.getLogger("aiosqlite").setLevel(logging.WARNING)
|
||||
|
||||
# Перенаправляем стандартное логирование в loguru
|
||||
class InterceptHandler(logging.Handler):
|
||||
def emit(self, record):
|
||||
# Получаем соответствующий уровень loguru
|
||||
try:
|
||||
level = logger.level(record.levelname).name
|
||||
except ValueError:
|
||||
level = record.levelno
|
||||
|
||||
# Находим caller из логов
|
||||
frame, depth = logging.currentframe(), 2
|
||||
while frame.f_code.co_filename == logging.__file__:
|
||||
frame = frame.f_back
|
||||
depth += 1
|
||||
|
||||
logger.opt(depth=depth, exception=record.exc_info).log(
|
||||
level, record.getMessage()
|
||||
)
|
||||
|
||||
# Подключаем перехватчик к корневому логгеру
|
||||
logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)
|
||||
|
||||
logger.info("🔧 Система логирования loguru настроена")
|
||||
logger.info(f"📊 Уровень логирования: {log_level}")
|
||||
logger.info(f"🐳 Логи выводятся в stderr для Docker")
|
||||
|
||||
|
||||
def get_logger(name: str = None):
|
||||
"""Получить логгер для модуля"""
|
||||
if name:
|
||||
return logger.bind(name=name)
|
||||
return logger
|
||||
|
||||
|
||||
# Инициализируем логирование при импорте
|
||||
setup_logging()
|
||||
274
services/infrastructure/logging_decorators.py
Normal file
274
services/infrastructure/logging_decorators.py
Normal file
@@ -0,0 +1,274 @@
|
||||
"""
|
||||
Декораторы для автоматического логирования функций
|
||||
"""
|
||||
import asyncio
|
||||
import inspect
|
||||
from functools import wraps
|
||||
from typing import Callable, Any, Optional, Dict, Union
|
||||
from aiogram.types import Message, CallbackQuery
|
||||
|
||||
from services.infrastructure.logger import get_logger
|
||||
|
||||
|
||||
def log_function_call(
|
||||
function_name: Optional[str] = None,
|
||||
log_params: bool = True,
|
||||
log_result: bool = False,
|
||||
log_level: str = "info",
|
||||
quiet: bool = False
|
||||
):
|
||||
"""
|
||||
Декоратор для автоматического логирования входа/выхода из функций
|
||||
|
||||
Args:
|
||||
function_name: Кастомное имя функции для логов (по умолчанию берется из func.__name__)
|
||||
log_params: Логировать ли параметры вызова
|
||||
log_result: Логировать ли результат выполнения
|
||||
log_level: Уровень логирования ('info', 'debug', 'warning')
|
||||
quiet: Тихое логирование (только ошибки)
|
||||
"""
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@wraps(func)
|
||||
async def async_wrapper(*args, **kwargs):
|
||||
logger = get_logger(func.__module__)
|
||||
name = function_name or func.__name__
|
||||
|
||||
# Формируем контекстную информацию
|
||||
context_info = _build_context_info(args, kwargs, log_params)
|
||||
|
||||
# Логируем вход в функцию (только если не тихий режим)
|
||||
if not quiet:
|
||||
log_method = getattr(logger, log_level)
|
||||
log_method(f"🚀 Начало выполнения {name}{context_info}")
|
||||
|
||||
try:
|
||||
result = await func(*args, **kwargs)
|
||||
|
||||
# Логируем успешное завершение (только если не тихий режим)
|
||||
if not quiet:
|
||||
result_info = ""
|
||||
if log_result and result is not None:
|
||||
result_info = f" | Результат: {_format_result(result)}"
|
||||
|
||||
log_method(f"✅ Успешное завершение {name}{result_info}")
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
# Логируем ошибку (всегда, даже в тихом режиме)
|
||||
logger.error(f"❌ Ошибка в {name}: {e}")
|
||||
raise
|
||||
|
||||
@wraps(func)
|
||||
def sync_wrapper(*args, **kwargs):
|
||||
logger = get_logger(func.__module__)
|
||||
name = function_name or func.__name__
|
||||
|
||||
# Формируем контекстную информацию
|
||||
context_info = _build_context_info(args, kwargs, log_params)
|
||||
|
||||
# Логируем вход в функцию (только если не тихий режим)
|
||||
if not quiet:
|
||||
log_method = getattr(logger, log_level)
|
||||
log_method(f"🚀 Начало выполнения {name}{context_info}")
|
||||
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
|
||||
# Логируем успешное завершение (только если не тихий режим)
|
||||
if not quiet:
|
||||
result_info = ""
|
||||
if log_result and result is not None:
|
||||
result_info = f" | Результат: {_format_result(result)}"
|
||||
|
||||
log_method(f"✅ Успешное завершение {name}{result_info}")
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
# Логируем ошибку (всегда, даже в тихом режиме)
|
||||
logger.error(f"❌ Ошибка в {name}: {e}")
|
||||
raise
|
||||
|
||||
# Возвращаем правильный wrapper в зависимости от типа функции
|
||||
return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def log_business_event(
|
||||
event_name: str,
|
||||
log_params: bool = True,
|
||||
log_result: bool = True
|
||||
):
|
||||
"""
|
||||
Декоратор для логирования бизнес-событий
|
||||
|
||||
Args:
|
||||
event_name: Название бизнес-события
|
||||
log_params: Логировать ли параметры
|
||||
log_result: Логировать ли результат
|
||||
"""
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@wraps(func)
|
||||
async def async_wrapper(*args, **kwargs):
|
||||
logger = get_logger(func.__module__)
|
||||
|
||||
# Формируем контекстную информацию
|
||||
context_info = _build_context_info(args, kwargs, log_params)
|
||||
|
||||
# Логируем бизнес-событие
|
||||
logger.info(f"📊 Бизнес-событие: {event_name}{context_info}")
|
||||
|
||||
try:
|
||||
result = await func(*args, **kwargs)
|
||||
|
||||
# Логируем результат бизнес-события
|
||||
if log_result and result is not None:
|
||||
result_info = _format_result(result)
|
||||
logger.info(f"📈 Результат {event_name}: {result_info}")
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"💥 Ошибка в бизнес-событии {event_name}: {e}")
|
||||
raise
|
||||
|
||||
@wraps(func)
|
||||
def sync_wrapper(*args, **kwargs):
|
||||
logger = get_logger(func.__module__)
|
||||
|
||||
# Формируем контекстную информацию
|
||||
context_info = _build_context_info(args, kwargs, log_params)
|
||||
|
||||
# Логируем бизнес-событие
|
||||
logger.info(f"📊 Бизнес-событие: {event_name}{context_info}")
|
||||
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
|
||||
# Логируем результат бизнес-события
|
||||
if log_result and result is not None:
|
||||
result_info = _format_result(result)
|
||||
logger.info(f"📈 Результат {event_name}: {result_info}")
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"💥 Ошибка в бизнес-событии {event_name}: {e}")
|
||||
raise
|
||||
|
||||
return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def log_fsm_transition(
|
||||
from_state: Optional[str] = None,
|
||||
to_state: Optional[str] = None
|
||||
):
|
||||
"""
|
||||
Декоратор для логирования переходов FSM состояний
|
||||
|
||||
Args:
|
||||
from_state: Исходное состояние (если None, будет определено автоматически)
|
||||
to_state: Целевое состояние (если None, будет определено автоматически)
|
||||
"""
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@wraps(func)
|
||||
async def async_wrapper(*args, **kwargs):
|
||||
logger = get_logger(func.__module__)
|
||||
|
||||
# Извлекаем FSM context из аргументов
|
||||
fsm_context = None
|
||||
for arg in args:
|
||||
if hasattr(arg, 'get_state') and hasattr(arg, 'set_state'):
|
||||
fsm_context = arg
|
||||
break
|
||||
|
||||
# Логируем переход состояния
|
||||
if fsm_context:
|
||||
current_state = await fsm_context.get_state()
|
||||
logger.info(f"🔄 FSM переход: {current_state} -> {to_state or 'новое состояние'}")
|
||||
|
||||
try:
|
||||
result = await func(*args, **kwargs)
|
||||
|
||||
# Логируем успешный переход
|
||||
if fsm_context:
|
||||
new_state = await fsm_context.get_state()
|
||||
logger.info(f"✅ FSM переход завершен: {from_state or 'предыдущее состояние'} -> {new_state}")
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка в FSM переходе: {e}")
|
||||
raise
|
||||
|
||||
return async_wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def _build_context_info(args: tuple, kwargs: dict, log_params: bool) -> str:
|
||||
"""Построение контекстной информации для логов"""
|
||||
if not log_params:
|
||||
return ""
|
||||
|
||||
context_parts = []
|
||||
|
||||
# Извлекаем информацию о пользователе из аргументов
|
||||
user_id = None
|
||||
for arg in args:
|
||||
if isinstance(arg, (Message, CallbackQuery)):
|
||||
user_id = arg.from_user.id
|
||||
context_parts.append(f"user_id={user_id}")
|
||||
break
|
||||
elif hasattr(arg, 'from_user_id'):
|
||||
user_id = arg.from_user_id
|
||||
context_parts.append(f"from_user_id={user_id}")
|
||||
elif hasattr(arg, 'to_user_id'):
|
||||
context_parts.append(f"to_user_id={arg.to_user_id}")
|
||||
elif hasattr(arg, 'id') and isinstance(arg.id, int):
|
||||
context_parts.append(f"id={arg.id}")
|
||||
|
||||
# Добавляем важные параметры из kwargs
|
||||
important_params = ['question_id', 'user_id', 'page', 'limit', 'status']
|
||||
for param in important_params:
|
||||
if param in kwargs and kwargs[param] is not None:
|
||||
context_parts.append(f"{param}={kwargs[param]}")
|
||||
|
||||
return f" | {', '.join(context_parts)}" if context_parts else ""
|
||||
|
||||
|
||||
def _format_result(result: Any) -> str:
|
||||
"""Форматирование результата для логов"""
|
||||
if result is None:
|
||||
return "None"
|
||||
|
||||
if isinstance(result, (str, int, float, bool)):
|
||||
return str(result)
|
||||
|
||||
if hasattr(result, 'id'):
|
||||
return f"id={result.id}"
|
||||
|
||||
if isinstance(result, (list, tuple)):
|
||||
return f"count={len(result)}"
|
||||
|
||||
if isinstance(result, dict):
|
||||
return f"keys={list(result.keys())}"
|
||||
|
||||
return str(type(result).__name__)
|
||||
|
||||
|
||||
# Удобные алиасы для часто используемых декораторов
|
||||
log_handler = log_function_call
|
||||
log_service = log_function_call
|
||||
log_business = log_business_event
|
||||
log_fsm = log_fsm_transition
|
||||
|
||||
# Тихие декораторы для middleware и служебных функций
|
||||
log_quiet = lambda **kwargs: log_function_call(quiet=True, **kwargs)
|
||||
log_middleware = lambda **kwargs: log_function_call(quiet=True, log_level="debug", **kwargs)
|
||||
|
||||
# Декоратор для служебных функций (только ошибки)
|
||||
def log_utility(func: Callable) -> Callable:
|
||||
"""Декоратор для служебных функций - логирует только ошибки"""
|
||||
return log_function_call(quiet=True, log_params=False, log_result=False)(func)
|
||||
227
services/infrastructure/logging_utils.py
Normal file
227
services/infrastructure/logging_utils.py
Normal file
@@ -0,0 +1,227 @@
|
||||
"""
|
||||
Утилиты для контекстного логирования
|
||||
"""
|
||||
from typing import Any, Optional, Dict, Union
|
||||
from aiogram.types import Message, CallbackQuery, User
|
||||
|
||||
from services.infrastructure.logger import get_logger
|
||||
|
||||
|
||||
class LoggingContext:
|
||||
"""Контекст для логирования с дополнительной информацией"""
|
||||
|
||||
def __init__(self, module_name: str):
|
||||
self.logger = get_logger(module_name)
|
||||
self.context_data = {}
|
||||
|
||||
def add_context(self, key: str, value: Any) -> 'LoggingContext':
|
||||
"""Добавить данные в контекст"""
|
||||
self.context_data[key] = value
|
||||
return self
|
||||
|
||||
def log_info(self, message: str, **kwargs):
|
||||
"""Логирование с контекстом"""
|
||||
context_str = self._format_context()
|
||||
full_message = f"{message}{context_str}"
|
||||
self.logger.info(full_message, **kwargs)
|
||||
|
||||
def log_warning(self, message: str, **kwargs):
|
||||
"""Логирование предупреждения с контекстом"""
|
||||
context_str = self._format_context()
|
||||
full_message = f"{message}{context_str}"
|
||||
self.logger.warning(full_message, **kwargs)
|
||||
|
||||
def log_error(self, message: str, **kwargs):
|
||||
"""Логирование ошибки с контекстом"""
|
||||
context_str = self._format_context()
|
||||
full_message = f"{message}{context_str}"
|
||||
self.logger.error(full_message, **kwargs)
|
||||
|
||||
def _format_context(self) -> str:
|
||||
"""Форматирование контекстных данных"""
|
||||
if not self.context_data:
|
||||
return ""
|
||||
|
||||
context_parts = [f"{k}={v}" for k, v in self.context_data.items()]
|
||||
return f" | {', '.join(context_parts)}"
|
||||
|
||||
|
||||
def get_logging_context(module_name: str) -> LoggingContext:
|
||||
"""Получить контекст логирования для модуля"""
|
||||
return LoggingContext(module_name)
|
||||
|
||||
|
||||
def log_user_action(
|
||||
logger,
|
||||
action: str,
|
||||
user: Union[User, Message, CallbackQuery, int],
|
||||
additional_info: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
"""
|
||||
Логирование действий пользователя
|
||||
|
||||
Args:
|
||||
logger: Логгер
|
||||
action: Действие пользователя
|
||||
user: Объект пользователя, сообщение, callback или user_id
|
||||
additional_info: Дополнительная информация
|
||||
"""
|
||||
user_id = _extract_user_id(user)
|
||||
user_info = _extract_user_info(user)
|
||||
|
||||
context_parts = [f"user_id={user_id}"]
|
||||
if user_info:
|
||||
context_parts.append(f"user_info={user_info}")
|
||||
|
||||
if additional_info:
|
||||
for key, value in additional_info.items():
|
||||
context_parts.append(f"{key}={value}")
|
||||
|
||||
context_str = f" | {', '.join(context_parts)}" if context_parts else ""
|
||||
logger.info(f"👤 {action}{context_str}")
|
||||
|
||||
|
||||
def log_business_operation(
|
||||
logger,
|
||||
operation: str,
|
||||
entity_type: str,
|
||||
entity_id: Optional[Union[int, str]] = None,
|
||||
additional_info: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
"""
|
||||
Логирование бизнес-операций
|
||||
|
||||
Args:
|
||||
logger: Логгер
|
||||
operation: Операция (create, update, delete, etc.)
|
||||
entity_type: Тип сущности (question, user, etc.)
|
||||
entity_id: ID сущности
|
||||
additional_info: Дополнительная информация
|
||||
"""
|
||||
context_parts = [f"operation={operation}", f"entity_type={entity_type}"]
|
||||
|
||||
if entity_id is not None:
|
||||
context_parts.append(f"entity_id={entity_id}")
|
||||
|
||||
if additional_info:
|
||||
for key, value in additional_info.items():
|
||||
context_parts.append(f"{key}={value}")
|
||||
|
||||
context_str = f" | {', '.join(context_parts)}"
|
||||
logger.info(f"📊 Бизнес-операция: {operation} {entity_type}{context_str}")
|
||||
|
||||
|
||||
def log_fsm_event(
|
||||
logger,
|
||||
event: str,
|
||||
state: Optional[str] = None,
|
||||
user_id: Optional[int] = None,
|
||||
additional_info: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
"""
|
||||
Логирование FSM событий
|
||||
|
||||
Args:
|
||||
logger: Логгер
|
||||
event: Событие FSM
|
||||
state: Текущее состояние
|
||||
user_id: ID пользователя
|
||||
additional_info: Дополнительная информация
|
||||
"""
|
||||
context_parts = [f"event={event}"]
|
||||
|
||||
if state:
|
||||
context_parts.append(f"state={state}")
|
||||
|
||||
if user_id:
|
||||
context_parts.append(f"user_id={user_id}")
|
||||
|
||||
if additional_info:
|
||||
for key, value in additional_info.items():
|
||||
context_parts.append(f"{key}={value}")
|
||||
|
||||
context_str = f" | {', '.join(context_parts)}"
|
||||
logger.info(f"🔄 FSM: {event}{context_str}")
|
||||
|
||||
|
||||
def log_performance(
|
||||
logger,
|
||||
operation: str,
|
||||
duration: float,
|
||||
additional_info: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
"""
|
||||
Логирование производительности
|
||||
|
||||
Args:
|
||||
logger: Логгер
|
||||
operation: Операция
|
||||
duration: Время выполнения в секундах
|
||||
additional_info: Дополнительная информация
|
||||
"""
|
||||
context_parts = [f"duration={duration:.3f}s"]
|
||||
|
||||
if additional_info:
|
||||
for key, value in additional_info.items():
|
||||
context_parts.append(f"{key}={value}")
|
||||
|
||||
context_str = f" | {', '.join(context_parts)}"
|
||||
logger.info(f"⏱️ Производительность: {operation}{context_str}")
|
||||
|
||||
|
||||
def _extract_user_id(user: Union[User, Message, CallbackQuery, int]) -> int:
|
||||
"""Извлечение user_id из различных объектов"""
|
||||
if isinstance(user, int):
|
||||
return user
|
||||
elif isinstance(user, User):
|
||||
return user.id
|
||||
elif isinstance(user, (Message, CallbackQuery)):
|
||||
return user.from_user.id
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def _extract_user_info(user: Union[User, Message, CallbackQuery, int]) -> Optional[str]:
|
||||
"""Извлечение информации о пользователе"""
|
||||
if isinstance(user, int):
|
||||
return None
|
||||
elif isinstance(user, User):
|
||||
return f"{user.first_name or ''} {user.last_name or ''}".strip() or user.username or "Unknown"
|
||||
elif isinstance(user, (Message, CallbackQuery)):
|
||||
user_obj = user.from_user
|
||||
return f"{user_obj.first_name or ''} {user_obj.last_name or ''}".strip() or user_obj.username or "Unknown"
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
# Удобные функции для быстрого логирования
|
||||
def log_question_created(logger, question_id: int, from_user_id: int, to_user_id: int):
|
||||
"""Логирование создания вопроса"""
|
||||
log_business_operation(
|
||||
logger, "create", "question", question_id,
|
||||
{"from_user_id": from_user_id, "to_user_id": to_user_id}
|
||||
)
|
||||
|
||||
|
||||
def log_question_answered(logger, question_id: int, user_id: int):
|
||||
"""Логирование ответа на вопрос"""
|
||||
log_business_operation(
|
||||
logger, "answer", "question", question_id,
|
||||
{"user_id": user_id}
|
||||
)
|
||||
|
||||
|
||||
def log_user_created(logger, user_id: int, username: Optional[str] = None):
|
||||
"""Логирование создания пользователя"""
|
||||
additional_info = {"username": username} if username else None
|
||||
log_business_operation(
|
||||
logger, "create", "user", user_id, additional_info
|
||||
)
|
||||
|
||||
|
||||
def log_user_blocked(logger, user_id: int, reason: Optional[str] = None):
|
||||
"""Логирование блокировки пользователя"""
|
||||
additional_info = {"reason": reason} if reason else None
|
||||
log_business_operation(
|
||||
logger, "block", "user", user_id, additional_info
|
||||
)
|
||||
351
services/infrastructure/metrics.py
Normal file
351
services/infrastructure/metrics.py
Normal file
@@ -0,0 +1,351 @@
|
||||
"""
|
||||
Сервис для работы с Prometheus метриками
|
||||
"""
|
||||
import time
|
||||
import inspect
|
||||
from typing import Optional, Callable
|
||||
from prometheus_client import Counter, Histogram, Gauge, Info, generate_latest, CONTENT_TYPE_LATEST
|
||||
from loguru import logger
|
||||
|
||||
|
||||
|
||||
|
||||
class MetricsService:
|
||||
"""Сервис для управления Prometheus метриками"""
|
||||
|
||||
def __init__(self):
|
||||
self._init_metrics()
|
||||
|
||||
def _init_metrics(self):
|
||||
"""Инициализация метрик"""
|
||||
|
||||
# Информация о боте
|
||||
self.bot_info = Info('anon_bot_info', 'Information about the AnonBot')
|
||||
self.bot_info.info({
|
||||
'version': '1.0.0',
|
||||
'service': 'anon-bot'
|
||||
})
|
||||
|
||||
# Счетчики сообщений
|
||||
self.messages_total = Counter(
|
||||
'anon_bot_messages_total',
|
||||
'Total number of messages processed',
|
||||
['message_type', 'status']
|
||||
)
|
||||
|
||||
# Счетчики вопросов
|
||||
self.questions_total = Counter(
|
||||
'anon_bot_questions_total',
|
||||
'Total number of questions received',
|
||||
['status']
|
||||
)
|
||||
|
||||
# Счетчики ответов
|
||||
self.answers_total = Counter(
|
||||
'anon_bot_answers_total',
|
||||
'Total number of answers sent',
|
||||
['status']
|
||||
)
|
||||
|
||||
# Счетчики пользователей
|
||||
self.users_total = Counter(
|
||||
'anon_bot_users_total',
|
||||
'Total number of users',
|
||||
['action']
|
||||
)
|
||||
|
||||
# Время обработки сообщений
|
||||
self.message_processing_time = Histogram(
|
||||
'anon_bot_message_processing_seconds',
|
||||
'Time spent processing messages',
|
||||
['message_type'],
|
||||
buckets=[0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]
|
||||
)
|
||||
|
||||
# Время обработки вопросов
|
||||
self.question_processing_time = Histogram(
|
||||
'anon_bot_question_processing_seconds',
|
||||
'Time spent processing questions',
|
||||
buckets=[0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]
|
||||
)
|
||||
|
||||
# Время обработки ответов
|
||||
self.answer_processing_time = Histogram(
|
||||
'anon_bot_answer_processing_seconds',
|
||||
'Time spent processing answers',
|
||||
buckets=[0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]
|
||||
)
|
||||
|
||||
# Активные пользователи
|
||||
self.active_users = Gauge(
|
||||
'anon_bot_active_users',
|
||||
'Number of active users'
|
||||
)
|
||||
|
||||
# Активные вопросы
|
||||
self.active_questions = Gauge(
|
||||
'anon_bot_active_questions',
|
||||
'Number of active questions'
|
||||
)
|
||||
|
||||
# Ошибки
|
||||
self.errors_total = Counter(
|
||||
'anon_bot_errors_total',
|
||||
'Total number of errors',
|
||||
['error_type', 'component']
|
||||
)
|
||||
|
||||
# HTTP запросы к эндпоинтам
|
||||
self.http_requests_total = Counter(
|
||||
'anon_bot_http_requests_total',
|
||||
'Total number of HTTP requests',
|
||||
['method', 'endpoint', 'status_code']
|
||||
)
|
||||
|
||||
# Метрики производительности БД
|
||||
self.db_queries_total = Counter(
|
||||
'anon_bot_db_queries_total',
|
||||
'Total number of database queries',
|
||||
['operation', 'table', 'status']
|
||||
)
|
||||
|
||||
self.db_query_duration = Histogram(
|
||||
'anon_bot_db_query_duration_seconds',
|
||||
'Database query duration',
|
||||
['operation', 'table'],
|
||||
buckets=[0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0]
|
||||
)
|
||||
|
||||
self.db_connections_active = Gauge(
|
||||
'anon_bot_db_connections_active',
|
||||
'Number of active database connections'
|
||||
)
|
||||
|
||||
self.db_connections_total = Counter(
|
||||
'anon_bot_db_connections_total',
|
||||
'Total number of database connections',
|
||||
['status']
|
||||
)
|
||||
|
||||
# Метрики пагинации
|
||||
self.pagination_requests_total = Counter(
|
||||
'anon_bot_pagination_requests_total',
|
||||
'Total number of pagination requests',
|
||||
['entity_type', 'method']
|
||||
)
|
||||
|
||||
self.pagination_duration = Histogram(
|
||||
'anon_bot_pagination_duration_seconds',
|
||||
'Pagination operation duration',
|
||||
['entity_type', 'method'],
|
||||
buckets=[0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0]
|
||||
)
|
||||
|
||||
self.pagination_errors_total = Counter(
|
||||
'anon_bot_pagination_errors_total',
|
||||
'Total number of pagination errors',
|
||||
['entity_type', 'error_type']
|
||||
)
|
||||
|
||||
# Метрики batch операций
|
||||
self.batch_operations_total = Counter(
|
||||
'anon_bot_batch_operations_total',
|
||||
'Total number of batch operations',
|
||||
['operation', 'table', 'status']
|
||||
)
|
||||
|
||||
self.batch_operation_duration = Histogram(
|
||||
'anon_bot_batch_operation_duration_seconds',
|
||||
'Batch operation duration',
|
||||
['operation', 'table'],
|
||||
buckets=[0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]
|
||||
)
|
||||
|
||||
self.batch_operation_size = Histogram(
|
||||
'anon_bot_batch_operation_size',
|
||||
'Batch operation size (number of items)',
|
||||
['operation', 'table'],
|
||||
buckets=[1, 5, 10, 25, 50, 100, 250, 500, 1000]
|
||||
)
|
||||
|
||||
# Время ответа HTTP эндпоинтов
|
||||
self.http_request_duration = Histogram(
|
||||
'anon_bot_http_request_duration_seconds',
|
||||
'HTTP request duration',
|
||||
['method', 'endpoint'],
|
||||
buckets=[0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]
|
||||
)
|
||||
|
||||
logger.info("Prometheus metrics initialized")
|
||||
|
||||
def increment_messages(self, message_type: str, status: str = "success"):
|
||||
"""Увеличить счетчик сообщений"""
|
||||
self.messages_total.labels(message_type=message_type, status=status).inc()
|
||||
|
||||
def increment_questions(self, status: str = "received"):
|
||||
"""Увеличить счетчик вопросов"""
|
||||
self.questions_total.labels(status=status).inc()
|
||||
|
||||
def increment_answers(self, status: str = "sent"):
|
||||
"""Увеличить счетчик ответов"""
|
||||
self.answers_total.labels(status=status).inc()
|
||||
|
||||
def increment_users(self, action: str):
|
||||
"""Увеличить счетчик пользователей"""
|
||||
self.users_total.labels(action=action).inc()
|
||||
|
||||
def increment_errors(self, error_type: str, component: str):
|
||||
"""Увеличить счетчик ошибок"""
|
||||
self.errors_total.labels(error_type=error_type, component=component).inc()
|
||||
|
||||
def increment_http_requests(self, method: str, endpoint: str, status_code: int):
|
||||
"""Увеличить счетчик HTTP запросов"""
|
||||
self.http_requests_total.labels(
|
||||
method=method,
|
||||
endpoint=endpoint,
|
||||
status_code=status_code
|
||||
).inc()
|
||||
|
||||
def set_active_users(self, count: int):
|
||||
"""Установить количество активных пользователей"""
|
||||
self.active_users.set(count)
|
||||
|
||||
def set_active_questions(self, count: int):
|
||||
"""Установить количество активных вопросов"""
|
||||
self.active_questions.set(count)
|
||||
|
||||
def record_message_processing_time(self, message_type: str, duration: float):
|
||||
"""Записать время обработки сообщения"""
|
||||
self.message_processing_time.labels(message_type=message_type).observe(duration)
|
||||
|
||||
def record_question_processing_time(self, duration: float):
|
||||
"""Записать время обработки вопроса"""
|
||||
self.question_processing_time.observe(duration)
|
||||
|
||||
def record_answer_processing_time(self, duration: float):
|
||||
"""Записать время обработки ответа"""
|
||||
self.answer_processing_time.observe(duration)
|
||||
|
||||
def record_http_request_duration(self, method: str, endpoint: str, duration: float):
|
||||
"""Записать время обработки HTTP запроса"""
|
||||
self.http_request_duration.labels(method=method, endpoint=endpoint).observe(duration)
|
||||
|
||||
# Методы для метрик БД
|
||||
def record_db_query(self, operation: str, table: str, status: str, duration: float):
|
||||
"""Записать метрики запроса к БД"""
|
||||
self.db_queries_total.labels(operation=operation, table=table, status=status).inc()
|
||||
self.db_query_duration.labels(operation=operation, table=table).observe(duration)
|
||||
|
||||
def record_db_connection(self, status: str):
|
||||
"""Записать метрики подключения к БД"""
|
||||
self.db_connections_total.labels(status=status).inc()
|
||||
if status == "opened":
|
||||
self.db_connections_active.inc()
|
||||
elif status == "closed":
|
||||
self.db_connections_active.dec()
|
||||
|
||||
def record_pagination_time(self, entity_type: str, duration: float, method: str = "cursor"):
|
||||
"""Записать время пагинации"""
|
||||
self.pagination_requests_total.labels(entity_type=entity_type, method=method).inc()
|
||||
self.pagination_duration.labels(entity_type=entity_type, method=method).observe(duration)
|
||||
|
||||
def increment_pagination_requests(self, entity_type: str, method: str = "cursor"):
|
||||
"""Увеличить счетчик запросов пагинации"""
|
||||
self.pagination_requests_total.labels(entity_type=entity_type, method=method).inc()
|
||||
|
||||
def increment_pagination_errors(self, entity_type: str, error_type: str = "unknown"):
|
||||
"""Увеличить счетчик ошибок пагинации"""
|
||||
self.pagination_errors_total.labels(entity_type=entity_type, error_type=error_type).inc()
|
||||
|
||||
def record_batch_operation(self, operation: str, table: str, status: str, duration: float, size: int):
|
||||
"""Записать метрики batch операции"""
|
||||
self.batch_operations_total.labels(operation=operation, table=table, status=status).inc()
|
||||
self.batch_operation_duration.labels(operation=operation, table=table).observe(duration)
|
||||
self.batch_operation_size.labels(operation=operation, table=table).observe(size)
|
||||
|
||||
def get_metrics(self) -> str:
|
||||
"""Получить метрики в формате Prometheus"""
|
||||
return generate_latest()
|
||||
|
||||
def get_content_type(self) -> str:
|
||||
"""Получить Content-Type для метрик"""
|
||||
return CONTENT_TYPE_LATEST
|
||||
|
||||
|
||||
# Глобальный экземпляр сервиса метрик
|
||||
metrics_service = MetricsService()
|
||||
|
||||
|
||||
def get_metrics_service() -> MetricsService:
|
||||
"""Получить экземпляр сервиса метрик"""
|
||||
return metrics_service
|
||||
|
||||
|
||||
# Декораторы для автоматического сбора метрик
|
||||
def track_message_processing(message_type: str):
|
||||
"""Декоратор для отслеживания обработки сообщений"""
|
||||
def decorator(func):
|
||||
async def wrapper(*args, **kwargs):
|
||||
# Убираем dispatcher, если он есть, так как он не нужен
|
||||
kwargs.pop('dispatcher', None)
|
||||
|
||||
start_time = time.time()
|
||||
try:
|
||||
result = await func(*args, **kwargs)
|
||||
metrics_service.increment_messages(message_type, "success")
|
||||
return result
|
||||
except Exception as e:
|
||||
metrics_service.increment_messages(message_type, "error")
|
||||
metrics_service.increment_errors(type(e).__name__, "message_processing")
|
||||
raise
|
||||
finally:
|
||||
duration = time.time() - start_time
|
||||
metrics_service.record_message_processing_time(message_type, duration)
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def track_question_processing():
|
||||
"""Декоратор для отслеживания обработки вопросов"""
|
||||
def decorator(func):
|
||||
async def wrapper(*args, **kwargs):
|
||||
# Убираем dispatcher, если он есть, так как он не нужен
|
||||
kwargs.pop('dispatcher', None)
|
||||
|
||||
start_time = time.time()
|
||||
try:
|
||||
result = await func(*args, **kwargs)
|
||||
metrics_service.increment_questions("processed")
|
||||
return result
|
||||
except Exception as e:
|
||||
metrics_service.increment_questions("error")
|
||||
metrics_service.increment_errors(type(e).__name__, "question_processing")
|
||||
raise
|
||||
finally:
|
||||
duration = time.time() - start_time
|
||||
metrics_service.record_question_processing_time(duration)
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def track_answer_processing():
|
||||
"""Декоратор для отслеживания обработки ответов"""
|
||||
def decorator(func):
|
||||
async def wrapper(*args, **kwargs):
|
||||
# Убираем dispatcher, если он есть, так как он не нужен
|
||||
kwargs.pop('dispatcher', None)
|
||||
|
||||
start_time = time.time()
|
||||
try:
|
||||
result = await func(*args, **kwargs)
|
||||
metrics_service.increment_answers("sent")
|
||||
return result
|
||||
except Exception as e:
|
||||
metrics_service.increment_answers("error")
|
||||
metrics_service.increment_errors(type(e).__name__, "answer_processing")
|
||||
raise
|
||||
finally:
|
||||
duration = time.time() - start_time
|
||||
metrics_service.record_answer_processing_time(duration)
|
||||
return wrapper
|
||||
return decorator
|
||||
117
services/infrastructure/pid_manager.py
Normal file
117
services/infrastructure/pid_manager.py
Normal file
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
PID менеджер для управления PID файлом процесса
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class PIDManager:
|
||||
"""Менеджер для управления PID файлом процесса"""
|
||||
|
||||
def __init__(self, service_name: str = "anon_bot", pid_dir: str = "/tmp"):
|
||||
self.service_name = service_name
|
||||
self.pid_dir = Path(pid_dir)
|
||||
self.pid_file_path = self.pid_dir / f"{service_name}.pid"
|
||||
self.pid: Optional[int] = None
|
||||
|
||||
def create_pid_file(self) -> bool:
|
||||
"""Создать PID файл"""
|
||||
try:
|
||||
# Создаем директорию для PID файлов, если она не существует
|
||||
self.pid_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Проверяем, не запущен ли уже процесс
|
||||
if self.pid_file_path.exists():
|
||||
try:
|
||||
with open(self.pid_file_path, 'r') as f:
|
||||
existing_pid = int(f.read().strip())
|
||||
|
||||
# Проверяем, жив ли процесс с этим PID
|
||||
if self._is_process_running(existing_pid):
|
||||
logger.error(f"Процесс {self.service_name} уже запущен с PID {existing_pid}")
|
||||
return False
|
||||
else:
|
||||
logger.warning(f"Найден устаревший PID файл для {existing_pid}, удаляем его")
|
||||
self.pid_file_path.unlink()
|
||||
|
||||
except (ValueError, OSError) as e:
|
||||
logger.warning(f"Не удалось прочитать существующий PID файл: {e}, удаляем его")
|
||||
self.pid_file_path.unlink()
|
||||
|
||||
# Получаем PID текущего процесса
|
||||
self.pid = os.getpid()
|
||||
|
||||
# Создаем PID файл
|
||||
with open(self.pid_file_path, 'w') as f:
|
||||
f.write(str(self.pid))
|
||||
|
||||
logger.info(f"PID файл создан: {self.pid_file_path} (PID: {self.pid})")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Не удалось создать PID файл: {e}")
|
||||
return False
|
||||
|
||||
def cleanup_pid_file(self) -> None:
|
||||
"""Очистить PID файл"""
|
||||
try:
|
||||
if self.pid_file_path.exists():
|
||||
# Проверяем, что PID файл принадлежит нашему процессу
|
||||
with open(self.pid_file_path, 'r') as f:
|
||||
file_pid = int(f.read().strip())
|
||||
|
||||
if file_pid == self.pid:
|
||||
self.pid_file_path.unlink()
|
||||
logger.info(f"PID файл удален: {self.pid_file_path}")
|
||||
else:
|
||||
logger.warning(f"PID файл содержит другой PID ({file_pid}), не удаляем")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении PID файла: {e}")
|
||||
|
||||
def get_pid(self) -> Optional[int]:
|
||||
"""Получить PID процесса"""
|
||||
return self.pid
|
||||
|
||||
def get_pid_file_path(self) -> Path:
|
||||
"""Получить путь к PID файлу"""
|
||||
return self.pid_file_path
|
||||
|
||||
|
||||
def _is_process_running(self, pid: int) -> bool:
|
||||
"""Проверить, запущен ли процесс с указанным PID"""
|
||||
try:
|
||||
# В Unix-системах отправляем сигнал 0 для проверки существования процесса
|
||||
os.kill(pid, 0)
|
||||
return True
|
||||
except (OSError, ProcessLookupError):
|
||||
return False
|
||||
|
||||
|
||||
|
||||
# Глобальный экземпляр PID менеджера
|
||||
_pid_manager: Optional[PIDManager] = None
|
||||
|
||||
|
||||
def get_pid_manager(service_name: str = "anon_bot", pid_dir: str = "/tmp") -> PIDManager:
|
||||
"""Получить экземпляр PID менеджера"""
|
||||
global _pid_manager
|
||||
if _pid_manager is None:
|
||||
_pid_manager = PIDManager(service_name, pid_dir)
|
||||
return _pid_manager
|
||||
|
||||
|
||||
def create_pid_file(service_name: str = "anon_bot", pid_dir: str = "/tmp") -> bool:
|
||||
"""Создать PID файл"""
|
||||
pid_manager = get_pid_manager(service_name, pid_dir)
|
||||
return pid_manager.create_pid_file()
|
||||
|
||||
|
||||
def cleanup_pid_file() -> None:
|
||||
"""Очистить PID файл"""
|
||||
if _pid_manager:
|
||||
_pid_manager.cleanup_pid_file()
|
||||
Reference in New Issue
Block a user