Dev 9 #11
116
.dockerignore
116
.dockerignore
@@ -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
54
Dockerfile
Normal 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"]
|
||||||
@@ -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"]
|
|
||||||
@@ -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}")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user