Dev 9 #11

Merged
KerradKerridi merged 3 commits from dev-9 into master 2025-09-19 10:02:40 +00:00
5 changed files with 93 additions and 292 deletions
Showing only changes of commit a0a7a47c8d - Show all commits

View File

@@ -1,95 +1,29 @@
# Python # .dockerignore
__pycache__/ .*
*.py[cod] !.gitignore
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Virtual environments # Исключаем тяжелые папки
voice_users/
logs/
.venv/
__pycache__
*.pyc
*.pyo
*.pyd
# Исключаем файлы БД (они создаются при запуске)
database/*.db
database/*.db-*
# Служебные файлы
Dockerfile
docker-compose*
README.md
.env .env
.venv *.log
env/
venv/
ENV/
env.bak/
venv.bak/
# IDE tests/
.vscode/ test/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Git
.git/
.gitignore
# Logs
logs/*.log
# Database
*.db
*.db-shm
*.db-wal
# Tests
test_*.py
.pytest_cache/
# Documentation
*.md
docs/ docs/
.idea/
# Docker .vscode/
Dockerfile*
docker-compose*.yml
.dockerignore
# Development files
*.sh
# Stickers and media
Stick/
# Temporary files
*.tmp
*.temp
.cache/
# Backup files
*.bak
*.backup
# Environment files
.env*
!.env.example
# Monitoring configs (will be mounted)
prometheus.yml

54
Dockerfile Normal file
View File

@@ -0,0 +1,54 @@
###########################################
# Этап 1: Сборщик (Builder)
###########################################
FROM python:3.9-alpine as builder
# Устанавливаем инструменты для компиляции + linux-headers для psutil
RUN apk add --no-cache \
gcc \
g++ \
musl-dev \
python3-dev \
linux-headers # ← ЭТО КРИТИЧЕСКИ ВАЖНО ДЛЯ psutil
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
# Минимальные рантайм-зависимости
RUN apk add --no-cache \
libstdc++ \
sqlite-libs
# Создаем пользователя
RUN addgroup -g 1001 deploy && adduser -D -u 1001 -G deploy deploy
WORKDIR /app
# Копируем зависимости
COPY --from=builder --chown=1001:1001 /install /usr/local/lib/python3.9/site-packages
# Создаем структуру папок
RUN mkdir -p database logs voice_users && \
chown -R 1001:1001 /app
# Копируем исходный код
COPY --chown=1001:1001 . .
USER 1001
# Healthcheck
HEALTHCHECK --interval=30s --timeout=15s --start-period=10s --retries=5 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health', timeout=5)" || exit 1
EXPOSE 8080
CMD ["python", "-u", "run_helper.py"]

View File

@@ -1,76 +0,0 @@
# Multi-stage build for production
FROM python:3.9-slim as builder
# Install build dependencies
RUN apt-get update && apt-get install -y \
gcc \
g++ \
&& rm -rf /var/lib/apt/lists/*
# Create virtual environment
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Copy and install requirements
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements.txt
# Production stage
FROM python:3.9-slim
# Set security options
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
# Install runtime dependencies only
RUN apt-get update && apt-get upgrade -y && apt-get install -y \
curl \
sqlite3 \
ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean
# Create non-root user with fixed UID
RUN groupadd -g 1001 deploy && useradd -u 1001 -g deploy deploy
# Copy virtual environment from builder
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
RUN chown -R 1001:1001 /opt/venv
# Create app directory and set permissions
WORKDIR /app
RUN mkdir -p /app/database /app/logs /app/voice_users && \
chown -R 1001:1001 /app
# Copy application code
COPY --chown=1001:1001 . .
# Initialize SQLite database with schema
RUN sqlite3 /app/database/tg-bot-database.db < /app/database/schema.sql && \
chown 1001:1001 /app/database/tg-bot-database.db && \
chmod 644 /app/database/tg-bot-database.db
# Switch to non-root user
USER deploy
# Health check with better timeout handling
HEALTHCHECK --interval=30s --timeout=15s --start-period=10s --retries=5 \
CMD curl -f --connect-timeout 5 --max-time 10 http://localhost:8080/health || exit 1
# Expose metrics port
EXPOSE 8080
# Graceful shutdown with longer timeout
STOPSIGNAL SIGTERM
# Set environment variables for better network stability
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONHASHSEED=random
# Run application with proper signal handling
CMD ["python", "-u", "run_helper.py"]

View File

@@ -31,7 +31,6 @@ class MetricsServer:
# Настраиваем роуты # Настраиваем роуты
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('/status', self.status_handler)
async def metrics_handler(self, request: web.Request) -> web.Response: async def metrics_handler(self, request: web.Request) -> web.Response:
"""Handle /metrics endpoint for Prometheus scraping.""" """Handle /metrics endpoint for Prometheus scraping."""
@@ -103,95 +102,6 @@ class MetricsServer:
status=500 status=500
) )
async def status_handler(self, request: web.Request) -> web.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 web.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 web.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 web.Response(
text=json.dumps(response_data, ensure_ascii=False),
content_type='application/json',
status=500
)
async def start(self) -> None: async def start(self) -> None:
"""Start the HTTP server.""" """Start the HTTP server."""
@@ -206,7 +116,6 @@ class MetricsServer:
logger.info("Available endpoints:") logger.info("Available endpoints:")
logger.info(f" - /metrics - Prometheus metrics") logger.info(f" - /metrics - Prometheus metrics")
logger.info(f" - /health - Health check") logger.info(f" - /health - Health check")
logger.info(f" - /status - Process status")
except Exception as e: except Exception as e:
logger.error(f"Failed to start metrics server: {e}") logger.error(f"Failed to start metrics server: {e}")

View File

@@ -2,6 +2,7 @@ import asyncio
import os import os
import sys import sys
import signal import signal
import sqlite3
# Ensure project root is on sys.path for module resolution # Ensure project root is on sys.path for module resolution
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -13,41 +14,10 @@ from helper_bot.utils.base_dependency_factory import get_global_instance
from helper_bot.utils.auto_unban_scheduler import get_auto_unban_scheduler from helper_bot.utils.auto_unban_scheduler import get_auto_unban_scheduler
from logs.custom_logger import logger from logs.custom_logger import logger
# Импортируем PID менеджер из инфраструктуры (если доступен)
import sys
import os
def get_pid_manager():
"""Получение PID менеджера из инфраструктуры проекта"""
try:
# Пытаемся импортировать из инфраструктуры проекта
infra_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'infra', 'monitoring')
if infra_path not in sys.path:
sys.path.insert(0, infra_path)
from pid_manager import get_bot_pid_manager
return get_bot_pid_manager
except ImportError:
# В изолированном запуске PID менеджер не нужен
logger.info("PID менеджер недоступен (изолированный запуск), PID файл не создается")
return None
# Получаем функцию создания PID менеджера
get_bot_pid_manager = get_pid_manager()
async def main(): async def main():
"""Основная функция запуска""" """Основная функция запуска"""
# Создаем PID менеджер для отслеживания процесса (если доступен)
pid_manager = None
if get_bot_pid_manager:
pid_manager = get_bot_pid_manager("helper_bot")
if not pid_manager.create_pid_file():
logger.error("Не удалось создать PID файл, завершаем работу")
return
else:
logger.info("PID менеджер недоступен, запуск без PID файла")
bdf = get_global_instance() bdf = get_global_instance()
@@ -111,9 +81,6 @@ async def main():
# Отменяем задачу бота # Отменяем задачу бота
bot_task.cancel() bot_task.cancel()
# Очищаем PID файл (если PID менеджер доступен)
if pid_manager:
pid_manager.cleanup_pid_file()
# Ждем завершения задачи бота и получаем результат main bot # Ждем завершения задачи бота и получаем результат main bot
try: try:
@@ -145,9 +112,22 @@ async def main():
logger.info("Бот корректно остановлен") logger.info("Бот корректно остановлен")
def init_db():
db_path = '/app/database/tg-bot-database.db'
schema_path = '/app/database/schema.sql'
if not os.path.exists(db_path):
print("Initializing database...")
with open(schema_path, 'r') as f:
schema = f.read()
with sqlite3.connect(db_path) as conn:
conn.executescript(schema)
print("Database initialized successfully")
if __name__ == '__main__': if __name__ == '__main__':
try: try:
init_db()
asyncio.run(main()) asyncio.run(main())
except AttributeError: except AttributeError:
# Fallback for Python 3.6-3.7 # Fallback for Python 3.6-3.7