Remove PID management functionality from the bot, including related endpoints and references in the codebase. Update Dockerfile to optimize the build process by separating build and runtime stages. Enhance healthcheck implementation in Dockerfile to use Python instead of curl. Update README to reflect the removal of PID file management and related endpoints.

This commit is contained in:
2025-09-16 17:49:49 +03:00
parent dc4300c6f2
commit e8fa682926
7 changed files with 38 additions and 357 deletions

View File

@@ -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 \ RUN apt-get update && apt-get install -y \
gcc \ gcc \
g++ \ g++ \
curl \ python3-dev \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Копируем файл зависимостей WORKDIR /app
COPY requirements.txt . 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 mkdir -p database logs && \
# Создаем пользователя для безопасности
RUN groupadd --gid 1001 app && \
useradd --create-home --shell /bin/bash --uid 1001 --gid 1001 app && \
chown -R 1001:1001 /app chown -R 1001:1001 /app
# Копируем исходный код
COPY --chown=1001:1001 . .
USER 1001:1001 USER 1001:1001
# Открываем порты # Открываем порты
@@ -36,9 +53,9 @@ EXPOSE 8081
ENV PYTHONPATH=/app ENV PYTHONPATH=/app
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
# Добавляем healthcheck # Healthcheck БЕЗ curl - используем встроенный Python
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ 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"]

View File

@@ -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"]

View File

@@ -98,56 +98,6 @@ docker logs anon-bot
curl http://localhost:8081/health 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 # Утилиты для контекстного логирования │ │ ├── logging_utils.py # Утилиты для контекстного логирования
│ │ ├── metrics.py # Prometheus метрики │ │ ├── metrics.py # Prometheus метрики
│ │ ├── http_server.py # HTTP сервер для метрик │ │ ├── http_server.py # HTTP сервер для метрик
│ │ └── pid_manager.py # Менеджер PID файлов
│ ├── rate_limiting/ # Rate limiting │ ├── rate_limiting/ # Rate limiting
│ │ ├── __init__.py │ │ ├── __init__.py
│ │ ├── rate_limit_config.py # Конфигурация rate limiting │ │ ├── rate_limit_config.py # Конфигурация rate limiting
@@ -1186,7 +1135,6 @@ AnonBot поддерживает экспорт метрик в формате P
- **http://localhost:8081/metrics** - экспорт метрик Prometheus - **http://localhost:8081/metrics** - экспорт метрик Prometheus
- **http://localhost:8081/health** - проверка здоровья бота - **http://localhost:8081/health** - проверка здоровья бота
- **http://localhost:8081/ready** - готовность к работе (readiness probe) - **http://localhost:8081/ready** - готовность к работе (readiness probe)
- **http://localhost:8081/status** - информация о процессе (PID, uptime, использование ресурсов)
- **http://localhost:8081/** - информация о сервисе - **http://localhost:8081/** - информация о сервисе
### Доступные метрики ### Доступные метрики

13
bot.py
View File

@@ -12,7 +12,6 @@ from config import config
from loader import loader from loader import loader
from services.infrastructure.http_server import start_http_server, stop_http_server from services.infrastructure.http_server import start_http_server, stop_http_server
from services.infrastructure.logger import get_logger 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 services.infrastructure.metrics_updater import start_metrics_updater, stop_metrics_updater
from config.constants import DEFAULT_HTTP_HOST, DEFAULT_HTTP_PORT from config.constants import DEFAULT_HTTP_HOST, DEFAULT_HTTP_PORT
@@ -23,7 +22,6 @@ logger = get_logger(__name__)
async def main(): async def main():
"""Главная функция для запуска бота""" """Главная функция для запуска бота"""
http_runner = None http_runner = None
pid_manager = None
try: try:
logger.info("🤖 Запуск бота анонимных вопросов...") logger.info("🤖 Запуск бота анонимных вопросов...")
@@ -31,13 +29,6 @@ async def main():
logger.info(f"💾 База данных: {config.DATABASE_PATH}") logger.info(f"💾 База данных: {config.DATABASE_PATH}")
logger.info(f"👑 Администраторы: {config.ADMINS}") 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 # Запускаем HTTP сервер для метрик и health check
logger.info("🌐 Запуск HTTP сервера для метрик...") logger.info("🌐 Запуск HTTP сервера для метрик...")
@@ -65,10 +56,6 @@ async def main():
logger.info("🛑 Остановка HTTP сервера...") logger.info("🛑 Остановка HTTP сервера...")
await stop_http_server(http_runner) await stop_http_server(http_runner)
# Очищаем PID файл
if pid_manager:
logger.info("📄 Очистка PID файла...")
pid_manager.cleanup_pid_file()
logger.info("🛑 Бот остановлен") logger.info("🛑 Бот остановлен")

View File

@@ -7,7 +7,6 @@ from .logger import get_logger, setup_logging
from .metrics import MetricsService, get_metrics_service from .metrics import MetricsService, get_metrics_service
from .metrics_updater import MetricsUpdater, get_metrics_updater, start_metrics_updater, stop_metrics_updater 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 .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 ( from .logging_decorators import (
log_function_call, log_business_event, log_fsm_transition, log_function_call, log_business_event, log_fsm_transition,
log_handler, log_service, log_business, log_fsm, log_handler, log_service, log_business, log_fsm,
@@ -25,7 +24,6 @@ __all__ = [
'MetricsService', 'get_metrics_service', 'MetricsService', 'get_metrics_service',
'MetricsUpdater', 'get_metrics_updater', 'start_metrics_updater', 'stop_metrics_updater', 'MetricsUpdater', 'get_metrics_updater', 'start_metrics_updater', 'stop_metrics_updater',
'track_db_operation', 'track_db_connection', 'track_db_operation', 'track_db_connection',
'PIDManager', 'get_pid_manager', 'cleanup_pid_file',
'log_function_call', 'log_business_event', 'log_fsm_transition', 'log_function_call', 'log_business_event', 'log_fsm_transition',
'log_handler', 'log_service', 'log_business', 'log_fsm', 'log_handler', 'log_service', 'log_business', 'log_fsm',
'log_quiet', 'log_middleware', 'log_utility', 'log_quiet', 'log_middleware', 'log_utility',

View File

@@ -31,7 +31,6 @@ class HTTPServer:
self.app.router.add_get('/metrics', self.metrics_handler) self.app.router.add_get('/metrics', self.metrics_handler)
self.app.router.add_get('/health', self.health_handler) self.app.router.add_get('/health', self.health_handler)
self.app.router.add_get('/ready', self.ready_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) self.app.router.add_get('/', self.root_handler)
async def metrics_handler(self, request: Request) -> Response: async def metrics_handler(self, request: Request) -> Response:
@@ -167,95 +166,6 @@ class HTTPServer:
status=HTTP_STATUS_INTERNAL_SERVER_ERROR 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: async def root_handler(self, request: Request) -> Response:
"""Обработчик корневого эндпоинта""" """Обработчик корневого эндпоинта"""
@@ -268,8 +178,7 @@ class HTTPServer:
"endpoints": { "endpoints": {
"metrics": "/metrics", "metrics": "/metrics",
"health": "/health", "health": "/health",
"ready": "/ready", "ready": "/ready"
"status": "/status"
}, },
"uptime": time.time() - self.start_time "uptime": time.time() - self.start_time
} }

View File

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