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:
55
Dockerfile
55
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"]
|
||||
@@ -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"]
|
||||
52
README.md
52
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/** - информация о сервисе
|
||||
|
||||
### Доступные метрики
|
||||
|
||||
13
bot.py
13
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("🛑 Бот остановлен")
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user