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:
2025-09-06 18:35:12 +03:00
parent 50be010026
commit 596a2fa813
111 changed files with 16847 additions and 65 deletions

View 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'
]

View 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()

View 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)

View 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()

View 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)

View 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
)

View 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

View 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()