diff --git a/Dockerfile b/Dockerfile index 0dfdd48..7a5c6ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,32 +1,49 @@ -# Используем официальный Python образ -FROM python:3.9-slim +########################################### +# Этап 1: Сборщик (Builder) +########################################### +FROM python:3.9-slim as builder -# Устанавливаем рабочую директорию -WORKDIR /app - -# Устанавливаем системные зависимости +# Устанавливаем ВСЁ для сборки RUN apt-get update && apt-get install -y \ gcc \ g++ \ - curl \ + python3-dev \ && rm -rf /var/lib/apt/lists/* -# Копируем файл зависимостей +WORKDIR /app COPY requirements.txt . -# Устанавливаем Python зависимости -RUN pip install --no-cache-dir -r requirements.txt +# Устанавливаем зависимости в отдельную папку +RUN pip install --no-cache-dir --target /install -r requirements.txt -# Копируем исходный код приложения -COPY . . + +########################################### +# Этап 2: Финальный образ (Runtime) +########################################### +FROM python:3.9-alpine as runtime + +# Устанавливаем ТОЛЬКО НЕОБХОДИМЫЕ рантайм-зависимости +# curl НЕ НУЖЕН - используем встроенный Python для healthcheck +RUN apk add --no-cache \ + # Минимальные библиотеки для работы Python-пакетов + libstdc++ + +# Создаем пользователя (в Alpine Linux другие команды) +RUN addgroup -g 1001 app && \ + adduser -D -u 1001 -G app app + +WORKDIR /app + +# Копируем зависимости из сборщика +COPY --from=builder --chown=1001:1001 /install /usr/local/lib/python3.9/site-packages # Создаем директории для данных -RUN mkdir -p database logs - -# Создаем пользователя для безопасности -RUN groupadd --gid 1001 app && \ - useradd --create-home --shell /bin/bash --uid 1001 --gid 1001 app && \ +RUN mkdir -p database logs && \ chown -R 1001:1001 /app + +# Копируем исходный код +COPY --chown=1001:1001 . . + USER 1001:1001 # Открываем порты @@ -36,9 +53,9 @@ EXPOSE 8081 ENV PYTHONPATH=/app ENV PYTHONUNBUFFERED=1 -# Добавляем healthcheck +# Healthcheck БЕЗ curl - используем встроенный Python HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ - CMD curl -f http://localhost:8081/health || exit 1 + CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8081/health', timeout=5)" || exit 1 # Команда по умолчанию -CMD ["python", "main.py"] +CMD ["python", "main.py"] \ No newline at end of file diff --git a/Dockerfile.optimized b/Dockerfile.optimized deleted file mode 100644 index 7a5c6ef..0000000 --- a/Dockerfile.optimized +++ /dev/null @@ -1,61 +0,0 @@ -########################################### -# Этап 1: Сборщик (Builder) -########################################### -FROM python:3.9-slim as builder - -# Устанавливаем ВСЁ для сборки -RUN apt-get update && apt-get install -y \ - gcc \ - g++ \ - python3-dev \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /app -COPY requirements.txt . - -# Устанавливаем зависимости в отдельную папку -RUN pip install --no-cache-dir --target /install -r requirements.txt - - -########################################### -# Этап 2: Финальный образ (Runtime) -########################################### -FROM python:3.9-alpine as runtime - -# Устанавливаем ТОЛЬКО НЕОБХОДИМЫЕ рантайм-зависимости -# curl НЕ НУЖЕН - используем встроенный Python для healthcheck -RUN apk add --no-cache \ - # Минимальные библиотеки для работы Python-пакетов - libstdc++ - -# Создаем пользователя (в Alpine Linux другие команды) -RUN addgroup -g 1001 app && \ - adduser -D -u 1001 -G app app - -WORKDIR /app - -# Копируем зависимости из сборщика -COPY --from=builder --chown=1001:1001 /install /usr/local/lib/python3.9/site-packages - -# Создаем директории для данных -RUN mkdir -p database logs && \ - chown -R 1001:1001 /app - -# Копируем исходный код -COPY --chown=1001:1001 . . - -USER 1001:1001 - -# Открываем порты -EXPOSE 8081 - -# Устанавливаем переменные окружения -ENV PYTHONPATH=/app -ENV PYTHONUNBUFFERED=1 - -# Healthcheck БЕЗ curl - используем встроенный Python -HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ - CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8081/health', timeout=5)" || exit 1 - -# Команда по умолчанию -CMD ["python", "main.py"] \ No newline at end of file diff --git a/README.md b/README.md index 61ecf85..2144027 100644 --- a/README.md +++ b/README.md @@ -98,56 +98,6 @@ docker logs anon-bot curl http://localhost:8081/health ``` -## 📄 PID файл и мониторинг процесса - -AnonBot автоматически создает PID файл для отслеживания процесса и предоставляет детальную информацию о состоянии через HTTP эндпоинты. - -### PID файл - -- **Расположение**: `/tmp/anon_bot.pid` -- **Содержимое**: PID процесса бота -- **Автоматическое управление**: создается при запуске, удаляется при остановке -- **Проверка дублирования**: предотвращает запуск нескольких экземпляров - -### Эндпоинт /status - -Предоставляет детальную информацию о процессе: - -```bash -curl http://localhost:8081/status -``` - -**Пример ответа:** -```json -{ - "status": "running", - "pid": 12345, - "uptime": "2ч 15м", - "memory_usage_mb": 45.2, - "cpu_percent": 0.1, - "timestamp": 1705312200.5 -} -``` - -**Поля ответа:** -- `status` - статус процесса (running/stopped/not_found/error) -- `pid` - идентификатор процесса -- `uptime` - время работы в человекочитаемом формате -- `memory_usage_mb` - использование памяти в МБ -- `cpu_percent` - загрузка CPU в процентах -- `timestamp` - время ответа - -### Тестирование - -Для тестирования PID функционала можно использовать curl: - -```bash -# Проверка статуса процесса -curl http://localhost:8081/status - -# Проверка всех эндпоинтов -curl http://localhost:8081/ -``` ## 📁 Структура проекта @@ -208,7 +158,6 @@ AnonBot/ │ │ ├── logging_utils.py # Утилиты для контекстного логирования │ │ ├── metrics.py # Prometheus метрики │ │ ├── http_server.py # HTTP сервер для метрик -│ │ └── pid_manager.py # Менеджер PID файлов │ ├── rate_limiting/ # Rate limiting │ │ ├── __init__.py │ │ ├── rate_limit_config.py # Конфигурация rate limiting @@ -1186,7 +1135,6 @@ AnonBot поддерживает экспорт метрик в формате P - **http://localhost:8081/metrics** - экспорт метрик Prometheus - **http://localhost:8081/health** - проверка здоровья бота - **http://localhost:8081/ready** - готовность к работе (readiness probe) -- **http://localhost:8081/status** - информация о процессе (PID, uptime, использование ресурсов) - **http://localhost:8081/** - информация о сервисе ### Доступные метрики diff --git a/bot.py b/bot.py index 79f2e1a..9788d26 100644 --- a/bot.py +++ b/bot.py @@ -12,7 +12,6 @@ from config import config from loader import loader from services.infrastructure.http_server import start_http_server, stop_http_server from services.infrastructure.logger import get_logger -from services.infrastructure.pid_manager import get_pid_manager, cleanup_pid_file from services.infrastructure.metrics_updater import start_metrics_updater, stop_metrics_updater from config.constants import DEFAULT_HTTP_HOST, DEFAULT_HTTP_PORT @@ -23,7 +22,6 @@ logger = get_logger(__name__) async def main(): """Главная функция для запуска бота""" http_runner = None - pid_manager = None try: logger.info("🤖 Запуск бота анонимных вопросов...") @@ -31,13 +29,6 @@ async def main(): logger.info(f"💾 База данных: {config.DATABASE_PATH}") logger.info(f"👑 Администраторы: {config.ADMINS}") - # Создаем PID файл для отслеживания процесса - logger.info("📄 Создание PID файла...") - pid_manager = get_pid_manager("anon_bot") - if not pid_manager.create_pid_file(): - logger.error("❌ Не удалось создать PID файл, завершаем работу") - return - logger.info(f"✅ PID файл создан: {pid_manager.get_pid_file_path()}") # Запускаем HTTP сервер для метрик и health check logger.info("🌐 Запуск HTTP сервера для метрик...") @@ -65,10 +56,6 @@ async def main(): logger.info("🛑 Остановка HTTP сервера...") await stop_http_server(http_runner) - # Очищаем PID файл - if pid_manager: - logger.info("📄 Очистка PID файла...") - pid_manager.cleanup_pid_file() logger.info("🛑 Бот остановлен") diff --git a/services/infrastructure/__init__.py b/services/infrastructure/__init__.py index eeefe09..877c965 100644 --- a/services/infrastructure/__init__.py +++ b/services/infrastructure/__init__.py @@ -7,7 +7,6 @@ from .logger import get_logger, setup_logging from .metrics import MetricsService, get_metrics_service from .metrics_updater import MetricsUpdater, get_metrics_updater, start_metrics_updater, stop_metrics_updater from .db_metrics_decorator import track_db_operation, track_db_connection -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, @@ -25,7 +24,6 @@ __all__ = [ 'MetricsService', 'get_metrics_service', 'MetricsUpdater', 'get_metrics_updater', 'start_metrics_updater', 'stop_metrics_updater', 'track_db_operation', 'track_db_connection', - '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', diff --git a/services/infrastructure/http_server.py b/services/infrastructure/http_server.py index 9c61c61..fe501c7 100644 --- a/services/infrastructure/http_server.py +++ b/services/infrastructure/http_server.py @@ -31,7 +31,6 @@ class HTTPServer: 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: @@ -167,95 +166,6 @@ class HTTPServer: 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: """Обработчик корневого эндпоинта""" @@ -268,8 +178,7 @@ class HTTPServer: "endpoints": { "metrics": "/metrics", "health": "/health", - "ready": "/ready", - "status": "/status" + "ready": "/ready" }, "uptime": time.time() - self.start_time } diff --git a/services/infrastructure/pid_manager.py b/services/infrastructure/pid_manager.py deleted file mode 100644 index 32b227a..0000000 --- a/services/infrastructure/pid_manager.py +++ /dev/null @@ -1,117 +0,0 @@ -""" -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()