diff --git a/Dockerfile b/Dockerfile index 4dd7590..5c71ec8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,27 +1,46 @@ -FROM python:3.9-slim +########################################### +# Этап 1: Сборщик (Builder) +########################################### +FROM python:3.9-slim as builder -# Установка системных зависимостей -RUN apt-get update && apt-get install -y \ - procps \ +# Устанавливаем ТОЧНО ТОЛЬКО то, что нужно для компиляции +RUN apt-get update && apt-get install --no-install-recommends -y \ + gcc \ + 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 . . -# Создание пользователя для безопасности -RUN groupadd -g 1000 monitor && \ - useradd -m -u 1000 -g monitor monitor && \ - chown -R 1000:1000 /app +########################################### +# Этап 2: Финальный образ (Runtime) +########################################### +# Используем ОЧЕНЬ легковесный базовый образ +FROM python:3.9-alpine as runtime + +# В Alpine Linux свои пакеты. apk вместо apt. +# Устанавливаем минимальные рантайм-зависимости +RUN apk add --no-cache libstdc++ + +# Создаем пользователя (в Alpine другие команды) +RUN addgroup -g 1000 app && \ + adduser -D -u 1000 -G app app + +WORKDIR /app + +# Копируем зависимости из сборщика (если есть) +COPY --from=builder --chown=1000:1000 /install /usr/local/lib/python3.9/site-packages +# Копируем исходный код +COPY --chown=1000:1000 . . + USER 1000 -# Команда по умолчанию для запуска мониторинга -CMD ["python", "infra/monitoring/main.py"] +# Важно: явно указываем Python искать зависимости в скопированной директории +ENV PYTHONPATH="/usr/local/lib/python3.9/site-packages:${PYTHONPATH}" + +# Оставляем базовую команду для совместимости +CMD ["python", "-c", "print('Dockerfile готов для использования')"] \ No newline at end of file diff --git a/Dockerfile.optimized b/Dockerfile.optimized deleted file mode 100644 index b591ace..0000000 --- a/Dockerfile.optimized +++ /dev/null @@ -1,45 +0,0 @@ -########################################### -# Этап 1: Сборщик (Builder) -########################################### -FROM python:3.9-slim as builder - -# Устанавливаем ТОЧНО ТОЛЬКО то, что нужно для компиляции -RUN apt-get update && apt-get install --no-install-recommends -y \ - gcc \ - 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 - -# В Alpine Linux свои пакеты. apk вместо apt. -# Устанавливаем минимальные рантайм-зависимости для psutil и подобных. -RUN apk add --no-cache libstdc++ - -# Создаем пользователя (в Alpine другие команды) -RUN addgroup -g 1000 monitor && \ - adduser -D -u 1000 -G monitor monitor - -WORKDIR /app - -# Копируем зависимости из сборщика -COPY --from=builder --chown=1000:1000 /install /usr/local/lib/python3.9/site-packages -# Копируем исходный код -COPY --chown=1000:1000 . . - -USER 1000 - -# Важно: явно указываем Python искать зависимости в скопированной директории -ENV PYTHONPATH="/usr/local/lib/python3.9/site-packages:${PYTHONPATH}" - -CMD ["python", "infra/monitoring/main.py"] \ No newline at end of file diff --git a/Makefile b/Makefile index a7c921a..aa72363 100644 --- a/Makefile +++ b/Makefile @@ -25,9 +25,6 @@ down: ## Остановить все сервисы logs: ## Показать логи всех сервисов docker-compose logs -f -logs-monitor: ## Показать логи мониторинга - docker-compose logs -f server_monitor - logs-prometheus: ## Показать логи Prometheus docker-compose logs -f prometheus @@ -45,9 +42,6 @@ restart: ## Перезапустить все сервисы docker-compose build --no-cache docker-compose up -d -restart-monitor: ## Перезапустить только мониторинг - docker-compose restart server_monitor - restart-prometheus: ## Перезапустить только Prometheus docker-compose restart prometheus @@ -82,7 +76,6 @@ backup: ## Создать backup данных @tar -czf "backups/backup-$(date +%Y%m%d-%H%M%S).tar.gz" \ infra/grafana/provisioning/ \ infra/prometheus/ \ - infra/monitoring/ \ .env \ docker-compose.yml @echo "✅ Backup created in backups/" @@ -212,13 +205,10 @@ check-ports: ## Проверить занятые порты @echo "Port 8081 (AnonBot):" @lsof -i :8081 2>/dev/null || echo " Free" -check-grafana: ## Проверить состояние Grafana - @echo "📊 Checking Grafana status..." - @cd infra/monitoring && python3 check_grafana.py check-deps: ## Проверить зависимости инфраструктуры @echo "🔍 Проверяю зависимости инфраструктуры..." - @python3 -c "import pytest, prometheus_client, psutil, aiohttp" 2>/dev/null || (echo "❌ Отсутствуют зависимости инфраструктуры. Установите: pip install pytest prometheus-client psutil aiohttp" && exit 1) + @python3 -c "import pytest" 2>/dev/null || (echo "❌ Отсутствуют зависимости инфраструктуры. Установите: pip install pytest" && exit 1) @echo "✅ Зависимости инфраструктуры установлены" check-bot-deps: ## Проверить зависимости Telegram бота diff --git a/README.md b/README.md index 9c8ecb3..1793388 100644 --- a/README.md +++ b/README.md @@ -100,19 +100,11 @@ docker-compose ps - **Назначение**: Сбор и хранение метрик, API для запросов - **Доступ**: Публичный (проброс из контейнера) - **Функции**: - - Сбор метрик с server_monitor (порт 9091) - Сбор метрик с telegram-bot (порт 8080) + - Сбор метрик с anon-bot (порт 8081) + - Сбор метрик с node_exporter (порт 9100) - Хранение исторических данных -#### **Порт 9091 - Server Monitor** -- **Контейнер**: `bots_server_monitor` -- **Назначение**: Мониторинг системных ресурсов сервера -- **Доступ**: Внутренний (только внутри Docker сети) -- **Функции**: - - Сбор CPU, RAM, Disk метрик - - Отправка алертов в Telegram - - Предоставление метрик для Prometheus - #### **Порт 8080 - Telegram Bot** - **Контейнер**: `bots_telegram_bot` - **Назначение**: Основной функционал Telegram бота @@ -152,7 +144,6 @@ docker-compose ps docker-compose logs # Только мониторинг -docker-compose logs -f server_monitor # Prometheus docker logs bots_prometheus @@ -165,7 +156,6 @@ docker logs bots_grafana ### Автоматическая проверка ```bash -cd infra/monitoring python3 check_grafana.py ``` @@ -204,7 +194,6 @@ make health # Проверить здоровье всех сервисо ### 📊 Мониторинг и логи ```bash make logs # Логи всех сервисов -make logs-monitor # Логи только мониторинга make logs-bot # Логи Telegram бота make logs-errors # Только ошибки из логов make monitoring # Открыть Grafana в браузере @@ -213,7 +202,6 @@ make prometheus # Открыть Prometheus в браузере ### 🔧 Управление отдельными сервисами ```bash -make restart-monitor # Перезапустить только мониторинг make restart-grafana # Перезапустить только Grafana make restart-prometheus # Перезапустить только Prometheus make restart-bot # Перезапустить только Telegram бота diff --git a/docker-compose.yml b/docker-compose.yml index fd7465e..fa59cb9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -45,43 +45,17 @@ services: depends_on: - prometheus healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"] + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"] interval: 30s timeout: 10s retries: 3 - # Server Monitoring Service - server_monitor: - build: . - container_name: bots_server_monitor - restart: unless-stopped - ports: - - "9091:9091" - environment: - - TELEGRAM_BOT_TOKEN=${TELEGRAM_MONITORING_BOT_TOKEN} - - GROUP_FOR_LOGS=${GROUP_MONITORING_FOR_LOGS} - - IMPORTANT_LOGS=${IMPORTANT_MONITORING_LOGS} - - THRESHOLD=${THRESHOLD:-80.0} - - RECOVERY_THRESHOLD=${RECOVERY_THRESHOLD:-75.0} - volumes: - - /proc:/host/proc:ro - - /sys:/host/sys:ro - - /var/run:/host/var/run:ro - networks: - - bots_network - depends_on: - - prometheus - healthcheck: - test: ["CMD-SHELL", "ps aux | grep python | grep server_monitor || exit 1"] - interval: 60s - timeout: 10s - retries: 3 # Telegram Helper Bot telegram-bot: build: context: ./bots/telegram-helper-bot - dockerfile: Dockerfile.bot + dockerfile: Dockerfile container_name: bots_telegram_bot restart: unless-stopped env_file: @@ -122,7 +96,7 @@ services: depends_on: - prometheus healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"] interval: 30s timeout: 10s retries: 3 @@ -175,7 +149,7 @@ services: depends_on: - prometheus healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8081/health"] + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8081/health"] interval: 30s timeout: 10s retries: 3 diff --git a/infra/grafana/provisioning/dashboards/server-dashboard.json b/infra/grafana/provisioning/dashboards/server-dashboard.json deleted file mode 100644 index 0ec0790..0000000 --- a/infra/grafana/provisioning/dashboards/server-dashboard.json +++ /dev/null @@ -1,649 +0,0 @@ -{ - "id": null, - "title": "Server Monitoring", - "tags": ["monitoring", "server"], - "style": "dark", - "timezone": "browser", - "panels": [ - { - "id": 1, - "title": "CPU Usage", - "type": "stat", - "targets": [ - { - "expr": "cpu_usage_percent", - "legendFormat": "CPU %" - } - ], - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "thresholds": { - "steps": [ - {"color": "green", "value": null}, - {"color": "yellow", "value": 70}, - {"color": "red", "value": 90} - ] - }, - "unit": "percent" - } - }, - "gridPos": {"h": 8, "w": 6, "x": 0, "y": 0} - }, - { - "id": 2, - "title": "RAM Usage", - "type": "stat", - "targets": [ - { - "expr": "ram_usage_percent", - "legendFormat": "RAM %" - } - ], - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "thresholds": { - "steps": [ - {"color": "green", "value": null}, - {"color": "yellow", "value": 70}, - {"color": "red", "value": 90} - ] - }, - "unit": "percent" - } - }, - "gridPos": {"h": 8, "w": 6, "x": 6, "y": 0} - }, - { - "id": 3, - "title": "Disk Usage", - "type": "stat", - "targets": [ - { - "expr": "disk_usage_percent", - "legendFormat": "Disk %" - } - ], - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "thresholds": { - "steps": [ - {"color": "green", "value": null}, - {"color": "yellow", "value": 80}, - {"color": "red", "value": 95} - ] - }, - "unit": "percent" - } - }, - "gridPos": {"h": 8, "w": 6, "x": 18, "y": 16} - }, - { - "id": 4, - "title": "Load Average", - "type": "timeseries", - "targets": [ - { - "expr": "load_average_1m", - "legendFormat": "1m" - }, - { - "expr": "load_average_5m", - "legendFormat": "5m" - }, - { - "expr": "load_average_15m", - "legendFormat": "15m" - } - ], - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "vis": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - } - } - }, - "gridPos": {"h": 8, "w": 12, "x": 0, "y": 16} - }, - { - "id": 5, - "title": "System Uptime", - "type": "stat", - "targets": [ - { - "expr": "system_uptime_seconds", - "legendFormat": "Uptime" - } - ], - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "unit": "s" - } - }, - "gridPos": {"h": 8, "w": 6, "x": 12, "y": 16} - }, - { - "id": 6, - "title": "Disk I/O Usage", - "type": "stat", - "targets": [ - { - "expr": "disk_io_percent", - "legendFormat": "Disk I/O %" - } - ], - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "thresholds": { - "steps": [ - {"color": "green", "value": null}, - {"color": "yellow", "value": 50}, - {"color": "red", "value": 80} - ] - }, - "unit": "percent" - } - }, - "gridPos": {"h": 8, "w": 6, "x": 12, "y": 16} - }, - { - "id": 7, - "title": "Swap Usage", - "type": "stat", - "targets": [ - { - "expr": "swap_usage_percent", - "legendFormat": "Swap %" - } - ], - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "thresholds": { - "steps": [ - {"color": "green", "value": null}, - {"color": "yellow", "value": 50}, - {"color": "red", "value": 80} - ] - }, - "unit": "percent" - } - }, - "gridPos": {"h": 8, "w": 6, "x": 18, "y": 16} - }, - { - "id": 8, - "title": "CPU Usage Gauge", - "type": "gauge", - "targets": [ - { - "expr": "cpu_usage_percent", - "legendFormat": "CPU %" - } - ], - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "thresholds": { - "steps": [ - {"color": "green", "value": null}, - {"color": "yellow", "value": 70}, - {"color": "red", "value": 90} - ] - }, - "unit": "percent", - "min": 0, - "max": 100 - } - }, - "options": { - "orientation": "auto", - "reduceOptions": { - "values": false, - "calcs": ["lastNotNull"], - "fields": "" - }, - "showThresholdLabels": false, - "showThresholdMarkers": true - }, - "gridPos": {"h": 8, "w": 6, "x": 12, "y": 0} - }, - { - "id": 9, - "title": "RAM Usage Gauge", - "type": "gauge", - "targets": [ - { - "expr": "ram_usage_percent", - "legendFormat": "RAM %" - } - ], - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "thresholds": { - "steps": [ - {"color": "green", "value": null}, - {"color": "yellow", "value": 70}, - {"color": "red", "value": 90} - ] - }, - "unit": "percent", - "min": 0, - "max": 100 - } - }, - "options": { - "orientation": "auto", - "reduceOptions": { - "values": false, - "calcs": ["lastNotNull"], - "fields": "" - }, - "showThresholdLabels": false, - "showThresholdMarkers": true - }, - "gridPos": {"h": 8, "w": 6, "x": 18, "y": 0} - }, - { - "id": 10, - "title": "System Resources Overview", - "type": "timeseries", - "targets": [ - { - "expr": "cpu_usage_percent", - "legendFormat": "CPU %" - }, - { - "expr": "ram_usage_percent", - "legendFormat": "RAM %" - }, - { - "expr": "disk_usage_percent", - "legendFormat": "Disk %" - } - ], - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "Usage %", - "axisPlacement": "left", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 20, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "vis": false - }, - "lineInterpolation": "linear", - "lineWidth": 2, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "unit": "percent", - "min": 0, - "max": 100 - } - }, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom" - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "gridPos": {"h": 8, "w": 12, "x": 0, "y": 8} - }, - { - "id": 11, - "title": "AnonBot Health Status", - "type": "timeseries", - "targets": [ - { - "expr": "rate(anon_bot_errors_total[5m])", - "legendFormat": "{{component}} - {{error_type}}" - } - ], - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "vis": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "unit": "short" - } - }, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom" - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "gridPos": {"h": 8, "w": 12, "x": 0, "y": 24} - }, - { - "id": 12, - "title": "AnonBot Database Connections", - "type": "timeseries", - "targets": [ - { - "expr": "anon_bot_db_connections_active", - "legendFormat": "Active Connections" - }, - { - "expr": "rate(anon_bot_db_connections_total[5m])", - "legendFormat": "Total Connections/min - {{status}}" - } - ], - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "vis": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "unit": "short" - } - }, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom" - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "gridPos": {"h": 8, "w": 12, "x": 12, "y": 24} - }, - { - "id": 13, - "title": "AnonBot System Health", - "type": "stat", - "targets": [ - { - "expr": "anon_bot_active_users", - "legendFormat": "Active Users" - } - ], - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "thresholds": { - "steps": [ - {"color": "green", "value": null}, - {"color": "yellow", "value": 10}, - {"color": "red", "value": 50} - ] - }, - "unit": "short" - } - }, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": ["lastNotNull"], - "fields": "", - "values": false - }, - "textMode": "auto" - }, - "gridPos": {"h": 8, "w": 6, "x": 0, "y": 32} - }, - { - "id": 14, - "title": "AnonBot Active Questions", - "type": "stat", - "targets": [ - { - "expr": "anon_bot_active_questions", - "legendFormat": "Active Questions" - } - ], - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "thresholds": { - "steps": [ - {"color": "green", "value": null}, - {"color": "yellow", "value": 20}, - {"color": "red", "value": 100} - ] - }, - "unit": "short" - } - }, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": ["lastNotNull"], - "fields": "", - "values": false - }, - "textMode": "auto" - }, - "gridPos": {"h": 8, "w": 6, "x": 6, "y": 32} - }, - { - "id": 15, - "title": "AnonBot Message Rate", - "type": "stat", - "targets": [ - { - "expr": "rate(anon_bot_messages_total[1m]) * 60", - "legendFormat": "Messages/min" - } - ], - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "thresholds": { - "steps": [ - {"color": "green", "value": null}, - {"color": "yellow", "value": 10}, - {"color": "red", "value": 50} - ] - }, - "unit": "short" - } - }, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": ["lastNotNull"], - "fields": "", - "values": false - }, - "textMode": "auto" - }, - "gridPos": {"h": 8, "w": 6, "x": 12, "y": 32} - }, - { - "id": 16, - "title": "AnonBot Error Rate", - "type": "stat", - "targets": [ - { - "expr": "rate(anon_bot_errors_total[5m])", - "legendFormat": "Errors/min" - } - ], - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "thresholds": { - "steps": [ - {"color": "green", "value": null}, - {"color": "yellow", "value": 1}, - {"color": "red", "value": 5} - ] - }, - "unit": "short" - } - }, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": ["lastNotNull"], - "fields": "", - "values": false - }, - "textMode": "auto" - }, - "gridPos": {"h": 8, "w": 6, "x": 18, "y": 32} - } - ], - "time": { - "from": "now-1h", - "to": "now" - }, - "refresh": "30s" -} diff --git a/infra/monitoring/__init__.py b/infra/monitoring/__init__.py deleted file mode 100644 index 62a9270..0000000 --- a/infra/monitoring/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Infrastructure Monitoring Module - -from .metrics_collector import MetricsCollector -from .message_sender import MessageSender -from .server_monitor import ServerMonitor - -__all__ = ['MetricsCollector', 'MessageSender', 'ServerMonitor'] diff --git a/infra/monitoring/check_grafana.py b/infra/monitoring/check_grafana.py deleted file mode 100644 index 452c248..0000000 --- a/infra/monitoring/check_grafana.py +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env python3 -""" -Скрипт для проверки статуса Grafana и дашбордов -""" - -import requests -import json -import sys -from datetime import datetime - -def check_grafana_status(): - """Проверка статуса Grafana""" - try: - response = requests.get("http://localhost:3000/api/health", timeout=5) - if response.status_code == 200: - data = response.json() - print(f"✅ Grafana работает (версия: {data.get('version', 'unknown')})") - return True - else: - print(f"❌ Grafana: HTTP {response.status_code}") - return False - except Exception as e: - print(f"❌ Grafana: ошибка подключения - {e}") - return False - -def check_prometheus_connection(): - """Проверка подключения Grafana к Prometheus""" - try: - # Проверяем, что Prometheus доступен - response = requests.get("http://localhost:9090/api/v1/targets", timeout=5) - if response.status_code == 200: - print("✅ Prometheus доступен для Grafana") - return True - else: - print(f"❌ Prometheus: HTTP {response.status_code}") - return False - except Exception as e: - print(f"❌ Prometheus: ошибка подключения - {e}") - return False - -def check_metrics_availability(): - """Проверка доступности метрик""" - try: - response = requests.get("http://localhost:9091/metrics", timeout=5) - if response.status_code == 200: - content = response.text - if "cpu_usage_percent" in content and "ram_usage_percent" in content: - print("✅ Метрики доступны и содержат данные") - return True - else: - print("⚠️ Метрики доступны, но данные неполные") - return False - else: - print(f"❌ Метрики: HTTP {response.status_code}") - return False - except Exception as e: - print(f"❌ Метрики: ошибка подключения - {e}") - return False - -def check_prometheus_targets(): - """Проверка статуса targets в Prometheus""" - try: - response = requests.get("http://localhost:9090/api/v1/targets", timeout=5) - if response.status_code == 200: - data = response.json() - targets = data.get('data', {}).get('activeTargets', []) - - print("\n📊 Статус targets в Prometheus:") - for target in targets: - job = target.get('labels', {}).get('job', 'unknown') - instance = target.get('labels', {}).get('instance', 'unknown') - health = target.get('health', 'unknown') - last_error = target.get('lastError', '') - - status_emoji = "✅" if health == "up" else "❌" - print(f" {status_emoji} {job} ({instance}): {health}") - - if last_error: - print(f" Ошибка: {last_error}") - - return True - else: - print(f"❌ Prometheus API: HTTP {response.status_code}") - return False - except Exception as e: - print(f"❌ Prometheus API: ошибка подключения - {e}") - return False - -def main(): - """Основная функция проверки""" - print(f"🔍 Проверка Grafana и системы мониторинга - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") - print("=" * 70) - - # Проверяем все компоненты - all_ok = True - - if not check_grafana_status(): - all_ok = False - - if not check_prometheus_connection(): - all_ok = False - - if not check_metrics_availability(): - all_ok = False - - if not check_prometheus_targets(): - all_ok = False - - print("\n" + "=" * 70) - if all_ok: - print("🎉 Все компоненты работают корректно!") - print("\n📋 Доступные адреса:") - print(" • Grafana: http://localhost:3000 (admin/admin)") - print(" • Prometheus: http://localhost:9090") - print(" • Метрики: http://localhost:9091/metrics") - print("\n📊 Дашборды должны быть доступны в Grafana:") - print(" • Server Monitoring") - print(" • Server Monitoring Dashboard") - print("\n💡 Если дашборды не видны, используйте ручную настройку:") - print(" • См. файл: GRAFANA_MANUAL_SETUP.md") - else: - print("⚠️ Обнаружены проблемы в системе мониторинга") - print(" Проверьте логи и настройки") - sys.exit(1) - -if __name__ == "__main__": - main() diff --git a/infra/monitoring/main.py b/infra/monitoring/main.py deleted file mode 100644 index afb7ebc..0000000 --- a/infra/monitoring/main.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env python3 -""" -Основной скрипт для запуска модуля мониторинга сервера -""" - -import asyncio -import logging -import os -import sys - -# Добавляем корневую папку проекта в путь -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) - -from dotenv import load_dotenv -from infra.monitoring.server_monitor import ServerMonitor - -# Загружаем переменные окружения из .env файла -load_dotenv() - -# Настройка логирования -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' -) - -logger = logging.getLogger(__name__) - - -async def main(): - """Основная функция запуска мониторинга""" - try: - # Создаем экземпляр мониторинга - monitor = ServerMonitor() - - # Отправляем статус при запуске - await monitor.send_startup_status() - - # Запускаем основной цикл мониторинга - await monitor.monitor_loop() - - except KeyboardInterrupt: - logger.info("Мониторинг остановлен пользователем") - except Exception as e: - logger.error(f"Критическая ошибка в мониторинге: {e}") - raise - - -if __name__ == "__main__": - # Запускаем асинхронную функцию - asyncio.run(main()) diff --git a/infra/monitoring/message_sender.py b/infra/monitoring/message_sender.py deleted file mode 100644 index 6669fc0..0000000 --- a/infra/monitoring/message_sender.py +++ /dev/null @@ -1,378 +0,0 @@ -import os -import aiohttp -import logging -from datetime import datetime -from typing import Dict, List, Tuple -try: - from .metrics_collector import MetricsCollector -except ImportError: - from metrics_collector import MetricsCollector - -logger = logging.getLogger(__name__) - - -class MessageSender: - def __init__(self): - # Получаем переменные окружения - self.telegram_bot_token = os.getenv('TELEGRAM_MONITORING_BOT_TOKEN') - self.group_for_logs = os.getenv('GROUP_MONITORING_FOR_LOGS') - self.important_logs = os.getenv('IMPORTANT_MONITORING_LOGS') - - # Интервал отправки статуса в минутах (по умолчанию 2 минуты) - self.status_update_interval_minutes = int(os.getenv('STATUS_UPDATE_INTERVAL_MINUTES', 2)) - - # Создаем экземпляр сборщика метрик - self.metrics_collector = MetricsCollector() - - # Время последней отправки статуса - self.last_status_time = None - - if not self.telegram_bot_token: - logger.warning("TELEGRAM_MONITORING_BOT_TOKEN не установлен в переменных окружения") - if not self.group_for_logs: - logger.warning("GROUP_MONITORING_FOR_LOGS не установлен в переменных окружения") - if not self.important_logs: - logger.warning("IMPORTANT_MONITORING_LOGS не установлен в переменных окружения") - - logger.info(f"Интервал отправки статуса установлен: {self.status_update_interval_minutes} минут") - - async def send_telegram_message(self, chat_id: str, message: str) -> bool: - """Отправка сообщения в Telegram через прямое обращение к API""" - if not self.telegram_bot_token: - logger.error("TELEGRAM_MONITORING_BOT_TOKEN не установлен") - return False - - try: - async with aiohttp.ClientSession() as session: - url = f"https://api.telegram.org/bot{self.telegram_bot_token}/sendMessage" - payload = { - "chat_id": chat_id, - "text": message, - "parse_mode": "HTML" - } - - async with session.post(url, json=payload) as response: - if response.status == 200: - logger.info(f"Сообщение успешно отправлено в чат {chat_id}") - return True - else: - response_text = await response.text() - logger.error(f"Ошибка отправки в Telegram: {response.status} - {response_text}") - return False - - except Exception as e: - logger.error(f"Ошибка при отправке сообщения в Telegram: {e}") - return False - - async def get_anonbot_status(self) -> Tuple[str, str]: - """Получение статуса AnonBot через HTTP API""" - try: - async with aiohttp.ClientSession() as session: - # AnonBot доступен через Docker network - url = "http://bots_anon_bot:8081/status" - - async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as response: - if response.status == 200: - data = await response.json() - status = data.get('status', 'unknown') - uptime = data.get('uptime', 'unknown') - - # Форматируем статус с эмодзи - if status == 'running': - status_emoji = "✅" - elif status == 'stopped': - status_emoji = "❌" - else: - status_emoji = "⚠️" - - return f"{status_emoji}", uptime - else: - logger.warning(f"AnonBot API вернул статус {response.status}") - return "⚠️ AnonBot", "API недоступен" - - except aiohttp.ClientError as e: - logger.warning(f"Ошибка подключения к AnonBot API: {e}") - return "❌", "Недоступен" - except Exception as e: - logger.error(f"Неожиданная ошибка при получении статуса AnonBot: {e}") - return "⚠️", "Ошибка" - - def should_send_status(self) -> bool: - """Проверка, нужно ли отправить статус (каждые N минут)""" - now = datetime.now() - - # Логируем для диагностики - import logging - logger = logging.getLogger(__name__) - - if self.last_status_time is None: - logger.info(f"should_send_status: last_status_time is None, отправляем статус") - return True - - # Вычисляем разницу в минутах - time_diff_minutes = (now - self.last_status_time).total_seconds() / 60 - logger.info(f"should_send_status: прошло {time_diff_minutes:.1f} минут с последней отправки, нужно {self.status_update_interval_minutes} минут") - - # Проверяем, что прошло N минут с последней отправки - if time_diff_minutes >= self.status_update_interval_minutes: - logger.info(f"should_send_status: отправляем статус (прошло {time_diff_minutes:.1f} минут)") - return True - - logger.info(f"should_send_status: статус не отправляем (прошло {time_diff_minutes:.1f} минут)") - return False - - def should_send_startup_status(self) -> bool: - """Проверка, нужно ли отправить статус при запуске""" - # Отправляем статус при запуске только если он еще не был отправлен - if self.last_status_time is None: - logger.info("should_send_startup_status: отправляем статус при запуске") - return True - logger.info("should_send_startup_status: статус уже был отправлен, пропускаем") - return False - - def _get_disk_space_emoji(self, disk_percent: float) -> str: - """Получение эмодзи для дискового пространства""" - if disk_percent < 60: - return "🟢" - elif disk_percent < 90: - return "⚠️" - else: - return "🚨" - - def _get_cpu_emoji(self, cpu_percent: float) -> str: - """Получение эмодзи для CPU""" - if cpu_percent < 50: - return "🟢" - elif cpu_percent < 80: - return "⚠️" - else: - return "🚨" - - def _get_memory_emoji(self, memory_percent: float) -> str: - """Получение эмодзи для памяти (RAM/Swap)""" - if memory_percent < 60: - return "🟢" - elif memory_percent < 85: - return "⚠️" - else: - return "🚨" - - def _get_load_average_emoji(self, load_avg: float, cpu_count: int) -> str: - """Получение эмодзи для Load Average""" - # Load Average считается нормальным если < 1.0 на ядро - # Критичным если > 2.0 на ядро - load_per_core = load_avg / cpu_count - if load_per_core < 1.0: - return "🟢" - elif load_per_core < 2.0: - return "⚠️" - else: - return "🚨" - - def _get_io_wait_emoji(self, io_wait_percent: float) -> str: - """Получение эмодзи для IO Wait""" - # IO Wait считается нормальным если < 5% - # Критичным если > 20% - if io_wait_percent < 5: - return "🟢" - elif io_wait_percent < 20: - return "⚠️" - else: - return "🚨" - - async def get_status_message(self, system_info: Dict) -> str: - """Формирование сообщения со статусом сервера""" - try: - helper_bot_status, helper_bot_uptime = self.metrics_collector.check_process_status('helper_bot') - - # Получаем статус AnonBot - anonbot_status, anonbot_uptime = await self.get_anonbot_status() - - # Получаем эмодзи для всех метрик - cpu_emoji = self._get_cpu_emoji(system_info['cpu_percent']) - ram_emoji = self._get_memory_emoji(system_info['ram_percent']) - swap_emoji = self._get_memory_emoji(system_info['swap_percent']) - la_emoji = self._get_load_average_emoji(system_info['load_avg_1m'], system_info['cpu_count']) - io_wait_emoji = self._get_io_wait_emoji(system_info['io_wait_percent']) - disk_emoji = self._get_disk_space_emoji(system_info['disk_percent']) - - # Определяем уровень мониторинга - monitoring_level = system_info.get('monitoring_level', 'unknown') - level_emoji = "🖥️" if monitoring_level == 'host' else "📦" - level_text = "Хост" if monitoring_level == 'host' else "Контейнер" - - message = f"""{level_emoji} **Статус {level_text}** | {system_info['current_time']} ---------------------------------- -**📊 Общая нагрузка:** -CPU: {system_info['cpu_percent']}% {cpu_emoji} | LA: {system_info['load_avg_1m']} / {system_info['cpu_count']} {la_emoji} | IO Wait: {system_info['io_wait_percent']}% {io_wait_emoji} - -**💾 Память:** -RAM: {system_info['ram_used']}/{system_info['ram_total']} GB ({system_info['ram_percent']}%) {ram_emoji} -Swap: {system_info['swap_used']}/{system_info['swap_total']} GB ({system_info['swap_percent']}%) {swap_emoji} - -**🗂️ Дисковое пространство:** -Диск (/): {system_info['disk_used']}/{system_info['disk_total']} GB ({system_info['disk_percent']}%) {disk_emoji} - -**💿 Диск I/O:** -Read: {system_info['disk_read_speed']} | Write: {system_info['disk_write_speed']} -Диск загружен: {system_info['disk_io_percent']}% - -**🤖 Процессы:** -{helper_bot_status} helper-bot - {helper_bot_uptime} -{anonbot_status} AnonBot - {anonbot_uptime} ---------------------------------- -⏰ Uptime сервера: {system_info['system_uptime']} -🔍 Уровень мониторинга: {level_text} ({monitoring_level})""" - - return message - - except Exception as e: - logger.error(f"Ошибка при формировании статуса сервера: {e}") - return f"Ошибка при получении статуса сервера: {e}" - - def get_alert_message(self, metric_name: str, current_value: float, details: str) -> str: - """Формирование сообщения об алерте""" - try: - # Получаем информацию о задержке для данного метрика - delay_info = "" - if hasattr(self.metrics_collector, 'alert_delays'): - metric_type = metric_name.lower().replace('использование ', '').replace('заполнение диска (/)', 'disk') - if 'cpu' in metric_type: - delay_info = f"⏱️ Задержка срабатывания: {self.metrics_collector.alert_delays['cpu']} сек" - elif 'память' in metric_type or 'ram' in metric_type: - delay_info = f"⏱️ Задержка срабатывания: {self.metrics_collector.alert_delays['ram']} сек" - elif 'диск' in metric_type or 'disk' in metric_type: - delay_info = f"⏱️ Задержка срабатывания: {self.metrics_collector.alert_delays['disk']} сек" - - message = f"""🚨 **ALERT: Высокая нагрузка на сервере!** ---------------------------------- -**Показатель:** {metric_name} -**Текущее значение:** {current_value}% ⚠️ -**Пороговое значение:** 80% - -**Детали:** -{details} - -{delay_info} - -**Сервер:** `{self.metrics_collector.os_type.upper()}` -**Время:** `{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}` ----------------------------------""" - - return message - - except Exception as e: - logger.error(f"Ошибка при формировании алерта: {e}") - return f"Ошибка при формировании алерта: {e}" - - def get_recovery_message(self, metric_name: str, current_value: float, peak_value: float) -> str: - """Формирование сообщения о восстановлении""" - try: - message = f"""✅ **RECOVERY: Нагрузка нормализовалась** ---------------------------------- -**Показатель:** {metric_name} -**Текущее значение:** {current_value}% ✔️ -**Было превышение:** До {peak_value}% - -**Сервер:** `{self.metrics_collector.os_type.upper()}` -**Время:** `{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}` ----------------------------------""" - - return message - - except Exception as e: - logger.error(f"Ошибка при формировании сообщения о восстановлении: {e}") - return f"Ошибка при формировании сообщения о восстановлении: {e}" - - async def send_status_message(self) -> bool: - """Отправка статуса сервера в группу логов""" - if not self.group_for_logs: - logger.warning("GROUP_MONITORING_FOR_LOGS не установлен, пропускаем отправку статуса") - return False - - try: - system_info = self.metrics_collector.get_system_info() - if not system_info: - logger.error("Не удалось получить информацию о системе") - return False - - status_message = await self.get_status_message(system_info) - success = await self.send_telegram_message(self.group_for_logs, status_message) - - # Обновляем время последней отправки только при успешной отправке - if success: - self.last_status_time = datetime.now() - logger.info("send_status_message: время последней отправки обновлено") - - return success - - except Exception as e: - logger.error(f"Ошибка при отправке статуса: {e}") - return False - - async def send_alert_message(self, metric_type: str, current_value: float, details: str) -> bool: - """Отправка сообщения об алерте в важные логи""" - if not self.important_logs: - logger.warning("IMPORTANT_MONITORING_LOGS не установлен, пропускаем отправку алерта") - return False - - try: - metric_names = { - 'cpu': 'Использование CPU', - 'ram': 'Использование оперативной памяти', - 'disk': 'Заполнение диска (/)' - } - - metric_name = metric_names.get(metric_type, metric_type) - alert_message = self.get_alert_message(metric_name, current_value, details) - return await self.send_telegram_message(self.important_logs, alert_message) - - except Exception as e: - logger.error(f"Ошибка при отправке алерта: {e}") - return False - - async def send_recovery_message(self, metric_type: str, current_value: float, peak_value: float) -> bool: - """Отправка сообщения о восстановлении в важные логи""" - if not self.important_logs: - logger.warning("IMPORTANT_MONITORING_LOGS не установлен, пропускаем отправку сообщения о восстановлении") - return False - - try: - metric_names = { - 'cpu': 'Использование CPU', - 'ram': 'Использование оперативной памяти', - 'disk': 'Заполнение диска (/)' - } - - metric_name = metric_names.get(metric_type, metric_type) - recovery_message = self.get_recovery_message(metric_name, current_value, peak_value) - return await self.send_telegram_message(self.important_logs, recovery_message) - - except Exception as e: - logger.error(f"Ошибка при отправке сообщения о восстановлении: {e}") - return False - - async def process_alerts_and_recoveries(self) -> None: - """Обработка алертов и восстановлений""" - try: - system_info = self.metrics_collector.get_system_info() - if not system_info: - return - - # Проверка алертов - alerts, recoveries = self.metrics_collector.check_alerts(system_info) - - # Отправка алертов - for metric_type, value, details in alerts: - await self.send_alert_message(metric_type, value, details) - logger.warning(f"ALERT отправлен: {metric_type} - {value}% - {details}") - - # Отправка сообщений о восстановлении - for metric_type, value in recoveries: - # Находим пиковое значение для сообщения о восстановлении - peak_value = self.metrics_collector.threshold - await self.send_recovery_message(metric_type, value, peak_value) - logger.info(f"RECOVERY отправлен: {metric_type} - {value}%") - - except Exception as e: - logger.error(f"Ошибка при обработке алертов и восстановлений: {e}") diff --git a/infra/monitoring/metrics_collector.py b/infra/monitoring/metrics_collector.py deleted file mode 100644 index 08e201d..0000000 --- a/infra/monitoring/metrics_collector.py +++ /dev/null @@ -1,858 +0,0 @@ -import os -import psutil -import time -import platform -from datetime import datetime -from typing import Dict, Optional, Tuple -import logging -from pid_manager import create_pid_manager - -logger = logging.getLogger(__name__) - - -class MetricsCollector: - def __init__(self): - # Определяем ОС - self.os_type = self._detect_os() - logger.info(f"Обнаружена ОС: {self.os_type}") - - # Проверяем, запущены ли мы в Docker с доступом к хосту - self.is_docker_host_monitoring = self._check_docker_host_access() - if self.is_docker_host_monitoring: - logger.info("Обнаружен доступ к хосту через Docker volumes - мониторинг будет вестись на уровне хоста") - else: - logger.warning("Мониторинг будет вестись на уровне контейнера (не рекомендуется для продакшена)") - - # Пороговые значения для алертов - self.threshold = float(os.getenv('THRESHOLD', '80.0')) - self.recovery_threshold = float(os.getenv('RECOVERY_THRESHOLD', '75.0')) - - # Задержки для алертов (в секундах) - предотвращают ложные срабатывания - self.alert_delays = { - 'cpu': int(os.getenv('CPU_ALERT_DELAY', '30')), # 30 сек для CPU - 'ram': int(os.getenv('RAM_ALERT_DELAY', '45')), # 45 сек для RAM - 'disk': int(os.getenv('DISK_ALERT_DELAY', '60')) # 60 сек для диска - } - - # Состояние алертов для предотвращения спама - self.alert_states = { - 'cpu': False, - 'ram': False, - 'disk': False - } - - # Время первого превышения порога для каждого метрика - self.alert_start_times = { - 'cpu': None, - 'ram': None, - 'disk': None - } - - # PID файлы для отслеживания процессов - # Определяем корень проекта для поиска PID файлов - current_file = os.path.abspath(__file__) - self.project_root = os.path.dirname(os.path.dirname(current_file)) - - self.pid_files = { - 'helper_bot': os.path.join(self.project_root, 'helper_bot.pid') - } - - # Для расчета скорости диска - self.last_disk_io = None - self.last_disk_io_time = None - - # Для расчета процента загрузки диска (отдельные переменные) - self.last_disk_io_for_percent = None - self.last_disk_io_time_for_percent = None - - # Инициализируем базовые значения для скорости диска при первом вызове - self._initialize_disk_io() - - - - # Время запуска мониторинга для расчета uptime - self.monitor_start_time = time.time() - - logger.info(f"Инициализированы задержки алертов: CPU={self.alert_delays['cpu']}s, RAM={self.alert_delays['ram']}s, Disk={self.alert_delays['disk']}s") - - def add_bot_to_monitoring(self, bot_name: str): - """ - Добавление нового бота в мониторинг - - Args: - bot_name: Имя бота (например, 'helper_bot', 'admin_bot', etc.) - """ - pid_file_path = os.path.join(self.project_root, f"{bot_name}.pid") - self.pid_files[bot_name] = pid_file_path - logger.info(f"Добавлен бот {bot_name} в мониторинг: {pid_file_path}") - - def _detect_os(self) -> str: - """Определение типа операционной системы""" - system = platform.system().lower() - if system == "darwin": - return "macos" - elif system == "linux": - return "ubuntu" - else: - return "unknown" - - def _check_docker_host_access(self) -> bool: - """Проверка доступности хоста через Docker volumes""" - try: - # Проверяем, доступны ли файлы хоста через /host/proc - # Это означает, что контейнер запущен с --privileged и volume mounts - if os.path.exists('/host/proc/stat') and os.path.exists('/host/proc/meminfo'): - return True - - # Альтернативная проверка - проверяем, запущены ли мы в Docker - # и есть ли доступ к системным файлам хоста - if os.path.exists('/.dockerenv'): - # Проверяем, можем ли мы читать системные файлы хоста - try: - with open('/proc/stat', 'r') as f: - f.read(100) # Читаем немного для проверки доступа - return True - except (OSError, PermissionError): - pass - - return False - except Exception as e: - logger.debug(f"Ошибка при проверке доступа к хосту: {e}") - return False - - def _initialize_disk_io(self): - """Инициализация базовых значений для расчета скорости диска""" - try: - disk_io = self._get_disk_io_counters() - if disk_io: - self.last_disk_io = disk_io - self.last_disk_io_time = time.time() - logger.debug("Инициализированы базовые значения для расчета скорости диска") - except Exception as e: - logger.error(f"Ошибка при инициализации диска I/O: {e}") - - def _get_disk_path(self) -> str: - """Получение пути к диску в зависимости от ОС""" - if self.os_type == "macos": - return "/" - elif self.os_type == "ubuntu": - return "/" - else: - return "/" - - def _get_disk_usage(self) -> Optional[object]: - """Получение информации о диске с учетом ОС""" - try: - if self.os_type == "macos": - # На macOS используем diskutil для получения реального использования диска - return self._get_macos_disk_usage() - else: - disk_path = self._get_disk_path() - return psutil.disk_usage(disk_path) - except Exception as e: - logger.error(f"Ошибка при получении информации о диске: {e}") - return None - - def _get_macos_disk_usage(self) -> Optional[object]: - """Получение информации о диске на macOS через diskutil""" - try: - import subprocess - import re - - # Получаем информацию о диске через diskutil - result = subprocess.run(['diskutil', 'info', '/'], capture_output=True, text=True) - if result.returncode != 0: - # Fallback к psutil - return psutil.disk_usage('/') - - output = result.stdout - - # Извлекаем размеры из вывода diskutil - total_match = re.search(r'Container Total Space:\s+(\d+\.\d+)\s+GB', output) - free_match = re.search(r'Container Free Space:\s+(\d+\.\d+)\s+GB', output) - - if total_match and free_match: - total_gb = float(total_match.group(1)) - free_gb = float(free_match.group(1)) - used_gb = total_gb - free_gb - - # Создаем объект, похожий на результат psutil.disk_usage - class DiskUsage: - def __init__(self, total, used, free): - self.total = total * (1024**3) # Конвертируем в байты - self.used = used * (1024**3) - self.free = free * (1024**3) - - return DiskUsage(total_gb, used_gb, free_gb) - else: - # Fallback к psutil - return psutil.disk_usage('/') - - except Exception as e: - logger.error(f"Ошибка при получении информации о диске macOS: {e}") - # Fallback к psutil - return psutil.disk_usage('/') - - def _get_disk_io_counters(self): - """Получение статистики диска с учетом ОС""" - try: - if self.os_type == "macos": - # На macOS может быть несколько дисков, берем основной - return psutil.disk_io_counters(perdisk=False) - elif self.os_type == "ubuntu": - # На Ubuntu обычно один диск - return psutil.disk_io_counters(perdisk=False) - else: - return psutil.disk_io_counters() - except Exception as e: - logger.error(f"Ошибка при получении статистики диска: {e}") - return None - - def _get_system_uptime(self) -> float: - """Получение uptime системы с учетом ОС""" - try: - if self.os_type == "macos": - # На macOS используем boot_time - boot_time = psutil.boot_time() - return time.time() - boot_time - elif self.os_type == "ubuntu": - # На Ubuntu также используем boot_time - boot_time = psutil.boot_time() - return time.time() - boot_time - else: - boot_time = psutil.boot_time() - return time.time() - boot_time - except Exception as e: - logger.error(f"Ошибка при получении uptime системы: {e}") - return 0.0 - - def get_monitor_uptime(self) -> str: - """Получение uptime мониторинга""" - uptime_seconds = time.time() - self.monitor_start_time - return self._format_uptime(uptime_seconds) - - def get_system_info(self) -> Dict: - """Получение информации о системе""" - try: - # Определяем, какой psutil использовать - current_psutil = psutil - if self.is_docker_host_monitoring: - # Для хоста используем специальные методы - host_cpu = self._get_host_cpu_info() - host_memory = self._get_host_memory_info() - host_disk = self._get_host_disk_info() - - if host_cpu and host_memory and host_disk: - # Используем данные хоста - cpu_count = host_cpu['cpu_count'] - load_avg = host_cpu['load_avg'] - - # Для CPU процента используем упрощенный расчет на основе load average - # Load average > 1.0 на ядро считается высокой нагрузкой - load_per_core = load_avg[0] / cpu_count if cpu_count > 0 else 0 - cpu_percent = min(100, load_per_core * 100) # Упрощенный расчет - - # Память хоста - ram_total = host_memory['ram_total'] - ram_used = host_memory['ram_used'] - ram_percent = host_memory['ram_percent'] - swap_total = host_memory['swap_total'] - swap_used = host_memory['swap_used'] - swap_percent = host_memory['swap_percent'] - - # Диск хоста - disk_total = host_disk['total'] - disk_used = host_disk['used'] - disk_free = host_disk['free'] - disk_percent = host_disk['percent'] - - # IO Wait и другие метрики недоступны через /proc, используем 0 - io_wait_percent = 0.0 - - logger.debug("Используются метрики хоста через Docker volumes") - else: - # Fallback к стандартному psutil - logger.warning("Не удалось получить метрики хоста, используем контейнер") - current_psutil = psutil - host_cpu = host_memory = host_disk = None - else: - # Стандартный psutil для контейнера - host_cpu = host_memory = host_disk = None - - # Если не используем хост, получаем стандартные метрики - if not host_cpu: - cpu_percent = current_psutil.cpu_percent(interval=1) - load_avg = current_psutil.getloadavg() - cpu_count = current_psutil.cpu_count() - - # CPU times для получения IO Wait - cpu_times = current_psutil.cpu_times_percent(interval=1) - io_wait_percent = getattr(cpu_times, 'iowait', 0.0) - - # Память - memory = current_psutil.virtual_memory() - swap = current_psutil.swap_memory() - - # Используем единый расчет для всех ОС: used / total для получения процента занятой памяти - ram_percent = (memory.used / memory.total) * 100 - ram_total = memory.total - ram_used = memory.used - swap_total = swap.total - swap_used = swap.used - swap_percent = swap.percent - - # Диск - disk = self._get_disk_usage() - disk_total = disk.total if disk else 0 - disk_used = disk.used if disk else 0 - disk_free = disk.free if disk else 0 - disk_percent = (disk_used / disk_total * 100) if disk_total > 0 else 0 - - # Диск I/O (может быть недоступен для хоста) - disk_io = self._get_disk_io_counters() - if disk_io: - disk_io_percent = self._calculate_disk_io_percent() - disk_read_speed, disk_write_speed = self._calculate_disk_speed(disk_io) - else: - disk_io_percent = 0 - disk_read_speed = "0 B/s" - disk_write_speed = "0 B/s" - - # Система - system_uptime = self._get_system_uptime() - - # Получаем имя хоста - if self.is_docker_host_monitoring: - try: - with open('/host/proc/sys/kernel/hostname', 'r') as f: - hostname = f.read().strip() - except: - hostname = "host" - else: - hostname = os.uname().nodename - - return { - 'cpu_percent': round(cpu_percent, 1), - 'load_avg_1m': round(load_avg[0], 2), - 'load_avg_5m': round(load_avg[1], 2), - 'load_avg_15m': round(load_avg[2], 2), - 'cpu_count': cpu_count, - 'io_wait_percent': round(io_wait_percent, 1), - 'ram_used': round(ram_used / (1024**3), 2), - 'ram_total': round(ram_total / (1024**3), 2), - 'ram_percent': round(ram_percent, 1), - 'swap_used': round(swap_used / (1024**3), 2), - 'swap_total': round(swap_total / (1024**3), 2), - 'swap_percent': round(swap_percent, 1), - 'disk_used': round(disk_used / (1024**3), 2), - 'disk_total': round(disk_total / (1024**3), 2), - 'disk_percent': round(disk_percent, 1), - 'disk_free': round(disk_free / (1024**3), 2), - 'disk_read_speed': disk_read_speed, - 'disk_write_speed': disk_write_speed, - 'disk_io_percent': disk_io_percent, - 'system_uptime': self._format_uptime(system_uptime), - 'monitor_uptime': self.get_monitor_uptime(), - 'server_hostname': hostname, - 'current_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), - 'monitoring_level': 'host' if self.is_docker_host_monitoring else 'container' - } - except Exception as e: - logger.error(f"Ошибка при получении информации о системе: {e}") - return {} - - def _format_bytes(self, bytes_value: int) -> str: - """Форматирование байтов в человекочитаемый вид""" - if bytes_value == 0: - return "0 B" - - size_names = ["B", "KB", "MB", "GB", "TB"] - i = 0 - while bytes_value >= 1024 and i < len(size_names) - 1: - bytes_value /= 1024.0 - i += 1 - - return f"{bytes_value:.1f} {size_names[i]}" - - def _format_uptime(self, seconds: float) -> str: - """Форматирование времени работы системы""" - days = int(seconds // 86400) - hours = int((seconds % 86400) // 3600) - minutes = int((seconds % 3600) // 60) - - if days > 0: - return f"{days}д {hours}ч {minutes}м" - elif hours > 0: - return f"{hours}ч {minutes}м" - else: - return f"{minutes}м" - - def check_process_status(self, process_name: str) -> Tuple[str, str]: - """Проверка статуса процесса и возврат статуса с uptime""" - try: - # Для helper_bot используем HTTP endpoint - if process_name == 'helper_bot': - return self._check_helper_bot_status() - - # Для других процессов используем стандартную проверку - return self._check_local_process_status(process_name) - - except Exception as e: - logger.error(f"Ошибка при проверке процесса {process_name}: {e}") - return "❌", "Выключен" - - def _check_local_process_status(self, process_name: str) -> Tuple[str, str]: - """Проверка локального процесса по PID файлу или имени""" - try: - # Проверяем по PID файлу - pid_file = self.pid_files.get(process_name) - if pid_file and os.path.exists(pid_file): - try: - with open(pid_file, 'r') as f: - content = f.read().strip() - if content and content != '# Этот файл будет автоматически обновляться при запуске бота': - pid = int(content) - if psutil.pid_exists(pid): - proc = psutil.Process(pid) - proc_uptime = time.time() - proc.create_time() - uptime_str = self._format_uptime(proc_uptime) - return "✅", f"Uptime {uptime_str}" - except (ValueError, FileNotFoundError): - pass - - # Проверяем по имени процесса - for proc in psutil.process_iter(['pid', 'name', 'cmdline']): - try: - proc_name = proc.info['name'].lower() - cmdline = ' '.join(proc.info['cmdline']).lower() if proc.info['cmdline'] else '' - - if (process_name in proc_name or - process_name in cmdline or - 'python' in proc_name and process_name in cmdline): - - proc_uptime = time.time() - proc.create_time() - uptime_str = self._format_uptime(proc_uptime) - return "✅", f"Uptime {uptime_str}" - - except (psutil.NoSuchProcess, psutil.AccessDenied): - continue - - return "❌", "Выключен" - - except Exception as e: - logger.error(f"Ошибка при проверке локального процесса {process_name}: {e}") - return "❌", "Выключен" - - def _calculate_disk_speed(self, current_disk_io) -> Tuple[str, str]: - """Расчет скорости чтения/записи диска""" - current_time = time.time() - - if self.last_disk_io is None or self.last_disk_io_time is None: - self.last_disk_io = current_disk_io - self.last_disk_io_time = current_time - return "0 B/s", "0 B/s" - - time_diff = current_time - self.last_disk_io_time - if time_diff < 1: # Минимальный интервал 1 секунда - return "0 B/s", "0 B/s" - - read_diff = current_disk_io.read_bytes - self.last_disk_io.read_bytes - write_diff = current_disk_io.write_bytes - self.last_disk_io.write_bytes - - read_speed = read_diff / time_diff - write_speed = write_diff / time_diff - - # Обновляем предыдущие значения - self.last_disk_io = current_disk_io - self.last_disk_io_time = current_time - - return self._format_bytes(read_speed) + "/s", self._format_bytes(write_speed) + "/s" - - def _calculate_disk_io_percent(self) -> int: - """Расчет процента загрузки диска на основе реальной скорости I/O""" - try: - # Получаем текущую статистику диска - current_disk_io = self._get_disk_io_counters() - if current_disk_io is None: - return 0 - - current_time = time.time() - - # Если это первое измерение, инициализируем - if self.last_disk_io_for_percent is None or self.last_disk_io_time_for_percent is None: - logger.debug("Первое измерение диска для процента, инициализируем базовые значения") - self.last_disk_io_for_percent = current_disk_io - self.last_disk_io_time_for_percent = current_time - return 0 - - # Рассчитываем время между измерениями - time_diff = current_time - self.last_disk_io_time_for_percent - if time_diff < 0.1: # Минимальный интервал 0.1 секунды для более точных измерений - logger.debug(f"Интервал между измерениями слишком мал: {time_diff:.3f}s, возвращаем 0%") - return 0 - - # Рассчитываем скорость операций в секунду - read_ops_diff = current_disk_io.read_count - self.last_disk_io_for_percent.read_count - write_ops_diff = current_disk_io.write_count - self.last_disk_io_for_percent.write_count - - read_ops_per_sec = read_ops_diff / time_diff - write_ops_per_sec = write_ops_diff / time_diff - total_ops_per_sec = read_ops_per_sec + write_ops_per_sec - - # Рассчитываем скорость передачи данных в байтах в секунду - read_bytes_diff = current_disk_io.read_bytes - self.last_disk_io_for_percent.read_bytes - write_bytes_diff = current_disk_io.write_bytes - self.last_disk_io_for_percent.write_bytes - - read_bytes_per_sec = read_bytes_diff / time_diff - write_bytes_per_sec = write_bytes_diff / time_diff - total_bytes_per_sec = read_bytes_per_sec + write_bytes_per_sec - - # Обновляем предыдущие значения для процента - self.last_disk_io_for_percent = current_disk_io - self.last_disk_io_time_for_percent = current_time - - # Определяем максимальную производительность диска в зависимости от ОС - if self.os_type == "macos": - # macOS обычно имеет SSD с высокой производительностью - max_ops_per_sec = 50000 # Операций в секунду - max_bytes_per_sec = 3 * (1024**3) # 3 GB/s - elif self.os_type == "ubuntu": - # Ubuntu может быть на разных типах дисков - max_ops_per_sec = 30000 # Операций в секунду - max_bytes_per_sec = 2 * (1024**3) # 2 GB/s - else: - max_ops_per_sec = 40000 - max_bytes_per_sec = 2.5 * (1024**3) - - # Рассчитываем процент загрузки на основе операций и байтов - # Защита от деления на ноль - if max_ops_per_sec > 0: - ops_percent = min(100, (total_ops_per_sec / max_ops_per_sec) * 100) - else: - ops_percent = 0 - - if max_bytes_per_sec > 0: - bytes_percent = min(100, (total_bytes_per_sec / max_bytes_per_sec) * 100) - else: - bytes_percent = 0 - - # Взвешенный средний процент (операции важнее для большинства случаев) - final_percent = (ops_percent * 0.7) + (bytes_percent * 0.3) - - # Логируем для отладки (только при высоких значениях) - if final_percent > 10: - logger.debug(f"Диск I/O: {total_ops_per_sec:.1f} ops/s, {total_bytes_per_sec/(1024**2):.1f} MB/s, " - f"Загрузка: {final_percent:.1f}% (ops: {ops_percent:.1f}%, bytes: {bytes_percent:.1f}%)") - - # Округляем до целого числа - return round(final_percent) - - except Exception as e: - logger.error(f"Ошибка при расчете процента загрузки диска: {e}") - return 0 - - def get_metrics_data(self) -> Dict: - """Получение данных для метрик Prometheus""" - system_info = self.get_system_info() - if not system_info: - return {} - - return { - 'cpu_usage_percent': system_info.get('cpu_percent', 0), - 'ram_usage_percent': system_info.get('ram_percent', 0), - 'disk_usage_percent': system_info.get('disk_percent', 0), - 'load_average_1m': system_info.get('load_avg_1m', 0), - 'load_average_5m': system_info.get('load_avg_5m', 0), - 'load_average_15m': system_info.get('load_avg_15m', 0), - 'swap_usage_percent': system_info.get('swap_percent', 0), - 'disk_io_percent': system_info.get('disk_io_percent', 0), - 'system_uptime_seconds': self._get_system_uptime(), - 'monitor_uptime_seconds': time.time() - self.monitor_start_time - } - - def check_alerts(self, system_info: Dict) -> Tuple[bool, Optional[str]]: - """Проверка необходимости отправки алертов с учетом задержек""" - current_time = time.time() - alerts = [] - recoveries = [] - - # Проверка CPU с задержкой - if system_info['cpu_percent'] > self.threshold: - if not self.alert_states['cpu']: - # Первое превышение порога - if self.alert_start_times['cpu'] is None: - self.alert_start_times['cpu'] = current_time - logger.debug(f"CPU превысил порог {self.threshold}%: {system_info['cpu_percent']:.1f}% - начинаем отсчет задержки {self.alert_delays['cpu']}s") - - # Проверяем, прошла ли задержка - if self.alert_delays['cpu'] == 0 or current_time - self.alert_start_times['cpu'] >= self.alert_delays['cpu']: - self.alert_states['cpu'] = True - alerts.append(('cpu', system_info['cpu_percent'], f"Нагрузка за 1 мин: {system_info['load_avg_1m']}")) - logger.warning(f"CPU ALERT: {system_info['cpu_percent']:.1f}% > {self.threshold}% (задержка {self.alert_delays['cpu']}s)") - else: - # CPU ниже порога - сбрасываем состояние только если был активный алерт - if self.alert_states['cpu']: - self.alert_states['cpu'] = False - recoveries.append(('cpu', system_info['cpu_percent'])) - logger.info(f"CPU RECOVERY: {system_info['cpu_percent']:.1f}% < {self.recovery_threshold}%") - # Сбрасываем время начала превышения только после отправки алерта - self.alert_start_times['cpu'] = None - elif system_info['cpu_percent'] < self.recovery_threshold and self.alert_start_times['cpu'] is not None: - # Если CPU опустился значительно ниже порога, сбрасываем время начала превышения - logger.debug(f"CPU значительно ниже порога {self.recovery_threshold}%: {system_info['cpu_percent']:.1f}% - сбрасываем время начала превышения") - self.alert_start_times['cpu'] = None - - # Проверка RAM с задержкой - if system_info['ram_percent'] > self.threshold: - if not self.alert_states['ram']: - # Первое превышение порога - if self.alert_start_times['ram'] is None: - self.alert_start_times['ram'] = current_time - logger.debug(f"RAM превысил порог {self.threshold}%: {system_info['ram_percent']:.1f}% - начинаем отсчет задержки {self.alert_delays['ram']}s") - - # Проверяем, прошла ли задержка - if self.alert_delays['ram'] == 0 or current_time - self.alert_start_times['ram'] >= self.alert_delays['ram']: - self.alert_states['ram'] = True - alerts.append(('ram', system_info['ram_percent'], f"Используется: {system_info['ram_used']} GB из {system_info['ram_total']} GB")) - logger.warning(f"RAM ALERT: {system_info['ram_percent']:.1f}% > {self.threshold}% (задержка {self.alert_delays['ram']}s)") - else: - # RAM ниже порога - сбрасываем состояние только если был активный алерт - if self.alert_states['ram']: - self.alert_states['ram'] = False - recoveries.append(('ram', system_info['ram_percent'])) - logger.info(f"RAM RECOVERY: {system_info['ram_percent']:.1f}% < {self.recovery_threshold}%") - # Сбрасываем время начала превышения только после отправки алерта - self.alert_start_times['ram'] = None - elif system_info['ram_percent'] < self.recovery_threshold and self.alert_start_times['ram'] is not None: - # Если RAM опустился значительно ниже порога, сбрасываем время начала превышения - logger.debug(f"RAM значительно ниже порога {self.recovery_threshold}%: {system_info['ram_percent']:.1f}% - сбрасываем время начала превышения") - self.alert_start_times['ram'] = None - - # Проверка диска с задержкой - if system_info['disk_percent'] > self.threshold: - if not self.alert_states['disk']: - # Первое превышение порога - if self.alert_start_times['disk'] is None: - self.alert_start_times['disk'] = current_time - logger.debug(f"Disk превысил порог {self.threshold}%: {system_info['disk_percent']:.1f}% - начинаем отсчет задержки {self.alert_delays['disk']}s") - - # Проверяем, прошла ли задержка - if self.alert_delays['disk'] == 0 or current_time - self.alert_start_times['disk'] >= self.alert_delays['disk']: - self.alert_states['disk'] = True - alerts.append(('disk', system_info['disk_percent'], f"Свободно: {system_info['disk_free']} GB на /")) - logger.warning(f"DISK ALERT: {system_info['disk_percent']:.1f}% > {self.threshold}% (задержка {self.alert_delays['disk']}s)") - else: - # Диск ниже порога - сбрасываем состояние только если был активный алерт - if self.alert_states['disk']: - self.alert_states['disk'] = False - recoveries.append(('disk', system_info['disk_percent'])) - logger.info(f"DISK RECOVERY: {system_info['disk_percent']:.1f}% < {self.recovery_threshold}%") - # Сбрасываем время начала превышения только после отправки алерта - self.alert_start_times['disk'] = None - elif system_info['disk_percent'] < self.recovery_threshold and self.alert_start_times['disk'] is not None: - # Если диск опустился значительно ниже порога, сбрасываем время начала превышения - logger.debug(f"Disk значительно ниже порога {self.recovery_threshold}%: {system_info['disk_percent']:.1f}% - сбрасываем время начала превышения") - self.alert_start_times['disk'] = None - - return alerts, recoveries - - def _get_host_psutil(self): - """Получение psutil с доступом к хосту""" - if self.is_docker_host_monitoring: - # Переключаемся на директории хоста - os.environ['PROC_ROOT'] = '/host/proc' - os.environ['SYS_ROOT'] = '/host/sys' - # Перезагружаем psutil для использования новых путей - import importlib - import psutil - importlib.reload(psutil) - return psutil - return psutil - - def _get_host_cpu_info(self): - """Получение информации о CPU хоста""" - try: - if self.is_docker_host_monitoring: - # Читаем информацию о CPU напрямую из /proc - with open('/host/proc/cpuinfo', 'r') as f: - cpu_info = f.read() - - # Подсчитываем количество ядер - cpu_count = cpu_info.count('processor') - - # Читаем load average - with open('/host/proc/loadavg', 'r') as f: - load_avg = f.read().strip().split()[:3] - load_avg = [float(x) for x in load_avg] - - # Читаем статистику CPU - with open('/host/proc/stat', 'r') as f: - cpu_stat = f.readline().strip().split()[1:] - cpu_stat = [int(x) for x in cpu_stat] - - # Рассчитываем процент CPU (упрощенный метод) - # В реальности нужно сравнивать с предыдущими значениями - cpu_percent = 0.0 # Будет рассчитано в get_system_info - - return { - 'cpu_count': cpu_count, - 'load_avg': load_avg, - 'cpu_stat': cpu_stat - } - else: - # Используем стандартный psutil - return { - 'cpu_count': psutil.cpu_count(), - 'load_avg': psutil.getloadavg(), - 'cpu_stat': None - } - except Exception as e: - logger.error(f"Ошибка при получении информации о CPU хоста: {e}") - return None - - def _get_host_memory_info(self): - """Получение информации о памяти хоста""" - try: - if self.is_docker_host_monitoring: - # Читаем информацию о памяти из /proc/meminfo - with open('/host/proc/meminfo', 'r') as f: - mem_info = f.read() - - # Парсим значения - mem_lines = mem_info.split('\n') - mem_data = {} - for line in mem_lines: - if ':' in line: - key, value = line.split(':', 1) - mem_data[key.strip()] = int(value.strip().split()[0]) * 1024 # Конвертируем в байты - - # Рассчитываем проценты - total = mem_data.get('MemTotal', 0) - available = mem_data.get('MemAvailable', 0) - used = total - available - ram_percent = (used / total * 100) if total > 0 else 0 - - # Swap - swap_total = mem_data.get('SwapTotal', 0) - swap_free = mem_data.get('SwapFree', 0) - swap_used = swap_total - swap_free - swap_percent = (swap_used / swap_total * 100) if swap_total > 0 else 0 - - return { - 'ram_total': total, - 'ram_used': used, - 'ram_percent': ram_percent, - 'swap_total': swap_total, - 'swap_used': swap_used, - 'swap_percent': swap_percent - } - else: - # Используем стандартный psutil - memory = psutil.virtual_memory() - swap = psutil.swap_memory() - return { - 'ram_total': memory.total, - 'ram_used': memory.used, - 'ram_percent': memory.percent, - 'swap_total': swap.total, - 'swap_used': swap.used, - 'swap_percent': swap.percent - } - except Exception as e: - logger.error(f"Ошибка при получении информации о памяти хоста: {e}") - return None - - def _get_host_disk_info(self): - """Получение информации о диске хоста""" - try: - if self.is_docker_host_monitoring: - # Используем df для получения информации о диске - import subprocess - result = subprocess.run(['df', '/'], capture_output=True, text=True) - if result.returncode == 0: - lines = result.stdout.strip().split('\n') - if len(lines) >= 2: - parts = lines[1].split() - if len(parts) >= 4: - total_kb = int(parts[1]) - used_kb = int(parts[2]) - available_kb = int(parts[3]) - - total = total_kb * 1024 - used = used_kb * 1024 - available = available_kb * 1024 - percent = (used / total * 100) if total > 0 else 0 - - return { - 'total': total, - 'used': used, - 'free': available, - 'percent': percent - } - - # Fallback к стандартному psutil - return None - else: - # Используем стандартный psutil - return None - except Exception as e: - logger.error(f"Ошибка при получении информации о диске хоста: {e}") - return None - - def _check_helper_bot_status(self) -> Tuple[str, str]: - """Проверка статуса helper_bot через HTTP endpoint""" - try: - import requests - - logger.info("Проверяем статус helper_bot через HTTP endpoint /status") - - # Обращаемся к endpoint /status в helper_bot - url = 'http://bots_telegram_bot:8080/status' - logger.info(f"Отправляем HTTP запрос к: {url}") - - response = requests.get(url, timeout=5) - logger.info(f"Получен HTTP ответ: статус {response.status_code}") - - if response.status_code == 200: - try: - data = response.json() - logger.info(f"Получены данные: {data}") - - status = data.get('status', 'unknown') - uptime = data.get('uptime', 'unknown') - - if status == 'running': - result = "✅", f"Uptime {uptime}" - logger.info(f"Helper_bot работает: {result}") - return result - elif status == 'starting': - result = "🔄", f"Запуск: {uptime}" - logger.info(f"Helper_bot запускается: {result}") - return result - else: - result = "⚠️", f"Статус: {status}" - logger.warning(f"Helper_bot необычный статус: {result}") - return result - - except (ValueError, KeyError) as e: - # Если не удалось распарсить JSON, но статус 200 - logger.warning(f"Не удалось распарсить JSON ответ: {e}, но статус 200") - result = "✅", "HTTP: доступен" - logger.info(f"Helper_bot доступен: {result}") - return result - else: - logger.warning(f"HTTP статус не 200: {response.status_code}") - return "⚠️", f"HTTP: {response.status_code}" - - except requests.exceptions.Timeout: - logger.error("HTTP запрос к helper_bot завершился таймаутом") - return "⚠️", "HTTP: таймаут" - except requests.exceptions.ConnectionError as e: - logger.error(f"HTTP ошибка соединения с helper_bot: {e}") - return "❌", "HTTP: нет соединения" - except ImportError: - logger.debug("requests не доступен для HTTP проверки") - return "❌", "HTTP: requests недоступен" - except Exception as e: - logger.error(f"Неожиданная ошибка при HTTP проверке helper_bot: {e}") - return "❌", f"HTTP: ошибка" diff --git a/infra/monitoring/pid_manager.py b/infra/monitoring/pid_manager.py deleted file mode 100644 index a7357db..0000000 --- a/infra/monitoring/pid_manager.py +++ /dev/null @@ -1,161 +0,0 @@ -""" -Модуль для управления PID файлами процессов -Общий модуль для всех ботов в проекте -""" -import os -import sys -import signal -import atexit -import logging -from typing import Optional - -logger = logging.getLogger(__name__) - - -class PIDManager: - """Класс для управления PID файлами""" - - def __init__(self, pid_file_path: str, process_name: str = "process"): - """ - Инициализация PID менеджера - - Args: - pid_file_path: Путь к PID файлу - process_name: Имя процесса для логирования - """ - self.pid_file_path = pid_file_path - self.process_name = process_name - self.pid = os.getpid() - - def create_pid_file(self) -> bool: - """ - Создание PID файла с текущим PID процесса - - Returns: - bool: True если файл создан успешно, False в противном случае - """ - try: - # Создаем директорию если не существует - pid_dir = os.path.dirname(self.pid_file_path) - if pid_dir and not os.path.exists(pid_dir): - os.makedirs(pid_dir, exist_ok=True) - - # Записываем PID в файл - with open(self.pid_file_path, 'w') as f: - f.write(str(self.pid)) - - logger.info(f"PID файл создан для {self.process_name}: {self.pid_file_path} (PID: {self.pid})") - - # Регистрируем функцию очистки при завершении - atexit.register(self.cleanup_pid_file) - - # Регистрируем обработчики сигналов для корректной очистки - signal.signal(signal.SIGTERM, self._signal_handler) - signal.signal(signal.SIGINT, self._signal_handler) - - return True - - except Exception as e: - logger.error(f"Ошибка при создании PID файла для {self.process_name}: {e}") - return False - - def cleanup_pid_file(self): - """Удаление PID файла при завершении процесса""" - try: - if os.path.exists(self.pid_file_path): - os.remove(self.pid_file_path) - logger.info(f"PID файл удален для {self.process_name}: {self.pid_file_path}") - except Exception as e: - logger.error(f"Ошибка при удалении PID файла для {self.process_name}: {e}") - - def _signal_handler(self, signum, frame): - """Обработчик сигналов для корректного завершения""" - logger.info(f"Получен сигнал {signum} для {self.process_name}, очищаем PID файл...") - self.cleanup_pid_file() - sys.exit(0) - - def is_running(self) -> bool: - """ - Проверка, запущен ли процесс с PID из файла - - Returns: - bool: True если процесс запущен, False в противном случае - """ - try: - if not os.path.exists(self.pid_file_path): - return False - - with open(self.pid_file_path, 'r') as f: - content = f.read().strip() - if not content: - return False - - try: - pid = int(content) - # Проверяем, существует ли процесс с таким PID - os.kill(pid, 0) # Отправляем сигнал 0 для проверки существования - return True - except (ValueError, OSError): - # PID не валидный или процесс не существует - return False - - except Exception as e: - logger.error(f"Ошибка при проверке PID файла для {self.process_name}: {e}") - return False - - def get_pid(self) -> Optional[int]: - """ - Получение PID из файла - - Returns: - int: PID процесса или None если файл не существует или невалидный - """ - try: - if not os.path.exists(self.pid_file_path): - return None - - with open(self.pid_file_path, 'r') as f: - content = f.read().strip() - if not content: - return None - - return int(content) - - except (ValueError, FileNotFoundError) as e: - logger.error(f"Ошибка при чтении PID файла для {self.process_name}: {e}") - return None - - -def create_pid_manager(process_name: str, project_root: str = None) -> PIDManager: - """ - Создание PID менеджера для указанного процесса - - Args: - process_name: Имя процесса (например, 'helper_bot', 'admin_bot', etc.) - project_root: Корневая директория проекта. Если None, определяется автоматически - - Returns: - PIDManager: Экземпляр PID менеджера - """ - if project_root is None: - # Определяем корень проекта автоматически - current_file = os.path.abspath(__file__) - # Поднимаемся на 2 уровня вверх от infra/monitoring/pid_manager.py - project_root = os.path.dirname(os.path.dirname(current_file)) - - pid_file_path = os.path.join(project_root, f"{process_name}.pid") - - return PIDManager(pid_file_path, process_name) - - -def get_bot_pid_manager(bot_name: str) -> PIDManager: - """ - Удобная функция для создания PID менеджера для ботов - - Args: - bot_name: Имя бота (например, 'helper_bot', 'admin_bot', etc.) - - Returns: - PIDManager: Экземпляр PID менеджера - """ - return create_pid_manager(bot_name) diff --git a/infra/monitoring/prometheus_server.py b/infra/monitoring/prometheus_server.py deleted file mode 100644 index cf00562..0000000 --- a/infra/monitoring/prometheus_server.py +++ /dev/null @@ -1,143 +0,0 @@ -import asyncio -import logging -from aiohttp import web -try: - from .metrics_collector import MetricsCollector -except ImportError: - from metrics_collector import MetricsCollector - -logger = logging.getLogger(__name__) - - -class PrometheusServer: - def __init__(self, host='0.0.0.0', port=9091): - self.host = host - self.port = port - self.metrics_collector = MetricsCollector() - self.app = web.Application() - self.setup_routes() - - def setup_routes(self): - """Настройка маршрутов для Prometheus""" - self.app.router.add_get('/', self.root_handler) - self.app.router.add_get('/metrics', self.metrics_handler) - self.app.router.add_get('/health', self.health_handler) - - async def root_handler(self, request): - """Главная страница""" - return web.Response( - text="Prometheus Metrics Server\n\n" - "Available endpoints:\n" - "- /metrics - Prometheus metrics\n" - "- /health - Health check", - content_type='text/plain' - ) - - async def health_handler(self, request): - """Health check endpoint""" - return web.Response( - text="OK", - content_type='text/plain' - ) - - async def metrics_handler(self, request): - """Endpoint для Prometheus метрик""" - try: - metrics_data = self.metrics_collector.get_metrics_data() - prometheus_metrics = self._format_prometheus_metrics(metrics_data) - - return web.Response( - text=prometheus_metrics, - content_type='text/plain' - ) - - except Exception as e: - logger.error(f"Ошибка при получении метрик: {e}") - return web.Response( - text=f"Error: {str(e)}", - status=500, - content_type='text/plain' - ) - - def _format_prometheus_metrics(self, metrics_data: dict) -> str: - """Форматирование метрик в Prometheus формат""" - lines = [] - - # Системная информация - lines.append("# HELP system_info System information") - lines.append("# TYPE system_info gauge") - lines.append(f"system_info{{os=\"{self.metrics_collector.os_type}\"}} 1") - - # CPU метрики - if 'cpu_usage_percent' in metrics_data: - lines.append("# HELP cpu_usage_percent CPU usage percentage") - lines.append("# TYPE cpu_usage_percent gauge") - lines.append(f"cpu_usage_percent {metrics_data['cpu_usage_percent']}") - - if 'load_average_1m' in metrics_data: - lines.append("# HELP load_average_1m 1 minute load average") - lines.append("# TYPE load_average_1m gauge") - lines.append(f"load_average_1m {metrics_data['load_average_1m']}") - - if 'load_average_5m' in metrics_data: - lines.append("# HELP load_average_5m 5 minute load average") - lines.append("# TYPE load_average_5m gauge") - lines.append(f"load_average_5m {metrics_data['load_average_5m']}") - - if 'load_average_15m' in metrics_data: - lines.append("# HELP load_average_15m 15 minute load average") - lines.append("# TYPE load_average_15m gauge") - lines.append(f"load_average_15m {metrics_data['load_average_15m']}") - - # RAM метрики - if 'ram_usage_percent' in metrics_data: - lines.append("# HELP ram_usage_percent RAM usage percentage") - lines.append("# TYPE ram_usage_percent gauge") - lines.append(f"ram_usage_percent {metrics_data['ram_usage_percent']}") - - # Disk метрики - if 'disk_usage_percent' in metrics_data: - lines.append("# HELP disk_usage_percent Disk usage percentage") - lines.append("# TYPE disk_usage_percent gauge") - lines.append(f"disk_usage_percent {metrics_data['disk_usage_percent']}") - - if 'disk_io_percent' in metrics_data: - lines.append("# HELP disk_io_percent Disk I/O usage percentage") - lines.append("# TYPE disk_io_percent gauge") - lines.append(f"disk_io_percent {metrics_data['disk_io_percent']}") - - # Swap метрики - if 'swap_usage_percent' in metrics_data: - lines.append("# HELP swap_usage_percent Swap usage percentage") - lines.append("# TYPE swap_usage_percent gauge") - lines.append(f"swap_usage_percent {metrics_data['swap_usage_percent']}") - - # Uptime метрики - if 'system_uptime_seconds' in metrics_data: - lines.append("# HELP system_uptime_seconds System uptime in seconds") - lines.append("# TYPE system_uptime_seconds gauge") - lines.append(f"system_uptime_seconds {metrics_data['system_uptime_seconds']}") - - if 'monitor_uptime_seconds' in metrics_data: - lines.append("# HELP monitor_uptime_seconds Monitor uptime in seconds") - lines.append("# TYPE monitor_uptime_seconds gauge") - lines.append(f"monitor_uptime_seconds {metrics_data['monitor_uptime_seconds']}") - - return '\n'.join(lines) - - async def start(self): - """Запуск HTTP сервера""" - runner = web.AppRunner(self.app) - await runner.setup() - - site = web.TCPSite(runner, self.host, self.port) - await site.start() - - logger.info(f"Prometheus сервер запущен на http://{self.host}:{self.port}") - - return runner - - async def stop(self, runner): - """Остановка HTTP сервера""" - await runner.cleanup() - logger.info("Prometheus сервер остановлен") diff --git a/infra/monitoring/server_monitor.py b/infra/monitoring/server_monitor.py deleted file mode 100644 index daca516..0000000 --- a/infra/monitoring/server_monitor.py +++ /dev/null @@ -1,62 +0,0 @@ -import asyncio -import logging -try: - from .metrics_collector import MetricsCollector - from .message_sender import MessageSender - from .prometheus_server import PrometheusServer -except ImportError: - from metrics_collector import MetricsCollector - from message_sender import MessageSender - from prometheus_server import PrometheusServer - -logger = logging.getLogger(__name__) - - -class ServerMonitor: - def __init__(self): - # Создаем экземпляры модулей - self.metrics_collector = MetricsCollector() - self.message_sender = MessageSender() - self.prometheus_server = PrometheusServer() - - logger.info(f"Модуль мониторинга сервера запущен на {self.metrics_collector.os_type.upper()}") - - async def monitor_loop(self): - """Основной цикл мониторинга""" - logger.info(f"Модуль мониторинга сервера запущен на {self.metrics_collector.os_type.upper()}") - - # Запускаем Prometheus сервер - prometheus_runner = await self.prometheus_server.start() - - try: - while True: - try: - # Проверка алертов и восстановлений - await self.message_sender.process_alerts_and_recoveries() - - # Проверка необходимости отправки статуса - if self.message_sender.should_send_status(): - await self.message_sender.send_status_message() - - # Пауза между проверками (30 секунд) - await asyncio.sleep(30) - - except Exception as e: - logger.error(f"Ошибка в цикле мониторинга: {e}") - await asyncio.sleep(30) - finally: - # Останавливаем Prometheus сервер при завершении - await self.prometheus_server.stop(prometheus_runner) - - async def send_startup_status(self): - """Отправка статуса при запуске""" - if self.message_sender.should_send_startup_status(): - await self.message_sender.send_status_message() - - def get_system_info(self): - """Получение информации о системе (для обратной совместимости)""" - return self.metrics_collector.get_system_info() - - def get_metrics_data(self): - """Получение данных для метрик Prometheus (для обратной совместимости)""" - return self.metrics_collector.get_metrics_data() diff --git a/infra/monitoring/test_monitor.py b/infra/monitoring/test_monitor.py deleted file mode 100644 index ede0e35..0000000 --- a/infra/monitoring/test_monitor.py +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env python3 -""" -Тестовый скрипт для проверки работы модуля мониторинга -""" - -import sys -import os -import logging - -# Добавляем текущую директорию в путь для импорта -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) - -from server_monitor import ServerMonitor - -# Настройка логирования -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' -) - -def main(): - """Основная функция тестирования""" - print("🚀 Тестирование модуля мониторинга сервера") - print("=" * 50) - - try: - # Создаем экземпляр мониторинга - monitor = ServerMonitor() - - # Получаем информацию о системе - print("📊 Получение информации о системе...") - system_info = monitor.get_system_info() - - if system_info: - print("✅ Информация о системе получена успешно") - print(f" CPU: {system_info.get('cpu_percent', 'N/A')}%") - print(f" RAM: {system_info.get('ram_percent', 'N/A')}%") - print(f" Диск: {system_info.get('disk_percent', 'N/A')}%") - print(f" Хост: {system_info.get('server_hostname', 'N/A')}") - print(f" ОС: {monitor.os_type}") - else: - print("❌ Не удалось получить информацию о системе") - return - - # Проверяем статус процессов - print("\n🤖 Проверка статуса процессов...") - helper_status, helper_uptime = monitor.check_process_status('helper_bot') - - print(f" Helper Bot: {helper_status} - {helper_uptime}") - - # Получаем метрики для Prometheus - print("\n📈 Получение метрик для Prometheus...") - metrics = monitor.get_metrics_data() - - if metrics: - print("✅ Метрики получены успешно") - for key, value in metrics.items(): - print(f" {key}: {value}") - else: - print("❌ Не удалось получить метрики") - - # Проверяем алерты - print("\n🚨 Проверка алертов...") - alerts, recoveries = monitor.check_alerts(system_info) - - if alerts: - print(f" Найдено алертов: {len(alerts)}") - for alert_type, value, details in alerts: - print(f" {alert_type}: {value}% - {details}") - else: - print(" Алертов не найдено") - - if recoveries: - print(f" Найдено восстановлений: {len(recoveries)}") - for recovery_type, value in recoveries: - print(f" {recovery_type}: {value}%") - - # Получаем сообщение о статусе - print("\n💬 Формирование сообщения о статусе...") - status_message = monitor.get_status_message(system_info) - if status_message: - print("✅ Сообщение о статусе сформировано") - print(" Первые 200 символов:") - print(f" {status_message[:200]}...") - else: - print("❌ Не удалось сформировать сообщение о статусе") - - print("\n🎉 Тестирование завершено успешно!") - - except Exception as e: - print(f"❌ Ошибка при тестировании: {e}") - logging.error(f"Ошибка при тестировании: {e}", exc_info=True) - return 1 - - return 0 - -if __name__ == "__main__": - exit(main()) diff --git a/infra/prometheus/prometheus.yml b/infra/prometheus/prometheus.yml index ec62596..0bafff2 100644 --- a/infra/prometheus/prometheus.yml +++ b/infra/prometheus/prometheus.yml @@ -11,15 +11,6 @@ scrape_configs: static_configs: - targets: ['localhost:9090'] - # Job для мониторинга инфраструктуры - - job_name: 'infrastructure' - static_configs: - - targets: ['bots_server_monitor:9091'] # Порт для метрик сервера мониторинга - metrics_path: '/metrics' - scrape_interval: 30s - scrape_timeout: 10s - honor_labels: true - # Job для мониторинга Node Exporter - job_name: 'node' static_configs: diff --git a/requirements.txt b/requirements.txt index 0a47b71..e69de29 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +0,0 @@ -psutil>=5.9.0 -asyncio -aiohttp>=3.8.0 -python-dotenv>=1.0.0 -requests>=2.28.0 diff --git a/tests/infra/conftest.py b/tests/infra/conftest.py deleted file mode 100644 index 214205e..0000000 --- a/tests/infra/conftest.py +++ /dev/null @@ -1,318 +0,0 @@ -#!/usr/bin/env python3 -""" -Общие фикстуры для тестов инфраструктуры -""" - -import pytest -import asyncio -import sys -import os -from unittest.mock import Mock, AsyncMock, patch -from pathlib import Path - -# Добавляем путь к модулям мониторинга -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../infra/monitoring')) - -# Настройка pytest-asyncio -pytest_plugins = ('pytest_asyncio',) - - -@pytest.fixture(scope="session") -def event_loop(): - """Создает event loop для асинхронных тестов""" - loop = asyncio.get_event_loop_policy().new_event_loop() - yield loop - loop.close() - - -@pytest.fixture -def mock_metrics_data(): - """Создает мок данных метрик для тестов""" - return { - 'cpu_usage_percent': 25.5, - 'ram_usage_percent': 60.2, - 'disk_usage_percent': 45.8, - 'load_average_1m': 1.2, - 'load_average_5m': 1.1, - 'load_average_15m': 1.0, - 'swap_usage_percent': 10.5, - 'disk_io_percent': 15.3, - 'system_uptime_seconds': 86400.0, - 'monitor_uptime_seconds': 3600.0 - } - - -@pytest.fixture -def mock_system_info(): - """Создает мок системной информации для тестов""" - return { - 'cpu_percent': 25.5, - 'load_avg_1m': 1.2, - 'load_avg_5m': 1.1, - 'load_avg_15m': 1.0, - 'cpu_count': 8, - 'io_wait_percent': 2.5, - 'ram_used': 8.0, - 'ram_total': 16.0, - 'ram_percent': 50.0, - 'swap_used': 1.0, - 'swap_total': 2.0, - 'swap_percent': 50.0, - 'disk_used': 100.0, - 'disk_total': 500.0, - 'disk_percent': 20.0, - 'disk_free': 400.0, - 'disk_read_speed': '1.0 MB/s', - 'disk_write_speed': '512.0 KB/s', - 'disk_io_percent': 15, - 'system_uptime': '1д 0ч 0м', - 'monitor_uptime': '1ч 0м', - 'server_hostname': 'test-host', - 'current_time': '2025-01-01 12:00:00' - } - - -@pytest.fixture -def mock_psutil(): - """Создает мок для psutil""" - mock_psutil = Mock() - - # Мокаем CPU - mock_psutil.cpu_percent.return_value = 25.5 - mock_psutil.getloadavg.return_value = (1.2, 1.1, 1.0) - mock_psutil.cpu_count.return_value = 8 - - # Мокаем память - mock_memory = Mock() - mock_memory.used = 8 * (1024**3) # 8 GB - mock_memory.total = 16 * (1024**3) # 16 GB - mock_psutil.virtual_memory.return_value = mock_memory - - mock_swap = Mock() - mock_swap.used = 1 * (1024**3) # 1 GB - mock_swap.total = 2 * (1024**3) # 2 GB - mock_swap.percent = 50.0 - mock_psutil.swap_memory.return_value = mock_swap - - # Мокаем диск - mock_disk = Mock() - mock_disk.used = 100 * (1024**3) # 100 GB - mock_disk.total = 500 * (1024**3) # 500 GB - mock_disk.free = 400 * (1024**3) # 400 GB - mock_psutil.disk_usage.return_value = mock_disk - - # Мокаем disk I/O - mock_disk_io = Mock() - mock_disk_io.read_count = 1000 - mock_disk_io.write_count = 500 - mock_disk_io.read_bytes = 1024 * (1024**2) # 1 GB - mock_disk_io.write_bytes = 512 * (1024**2) # 512 MB - mock_psutil.disk_io_counters.return_value = mock_disk_io - - # Мокаем boot time - import time - mock_psutil.boot_time.return_value = time.time() - 86400 # 1 день назад - - return mock_psutil - - -@pytest.fixture -def mock_platform(): - """Создает мок для platform""" - mock_platform = Mock() - mock_platform.system.return_value = 'Linux' - return mock_platform - - -@pytest.fixture -def mock_subprocess(): - """Создает мок для subprocess""" - mock_subprocess = Mock() - - # Мокаем успешный результат diskutil - mock_result = Mock() - mock_result.returncode = 0 - mock_result.stdout = """ - Container Total Space: 500.0 GB - Container Free Space: 400.0 GB - """ - mock_subprocess.run.return_value = mock_result - - return mock_subprocess - - -@pytest.fixture -def mock_os(): - """Создает мок для os""" - mock_os = Mock() - mock_os.getenv.side_effect = lambda key, default=None: { - 'THRESHOLD': '80.0', - 'RECOVERY_THRESHOLD': '75.0' - }.get(key, default) - - # Мокаем uname - mock_uname = Mock() - mock_uname.nodename = "test-host" - mock_os.uname.return_value = mock_uname - - return mock_os - - -@pytest.fixture -def prometheus_config_sample(): - """Создает пример конфигурации Prometheus для тестов""" - return { - 'global': { - 'scrape_interval': '15s', - 'evaluation_interval': '15s' - }, - 'rule_files': [ - '# - "first_rules.yml"', - '# - "second_rules.yml"' - ], - 'scrape_configs': [ - { - 'job_name': 'prometheus', - 'static_configs': [ - { - 'targets': ['localhost:9090'] - } - ] - }, - { - 'job_name': 'infrastructure', - 'static_configs': [ - { - 'targets': ['host.docker.internal:9091'] - } - ], - 'metrics_path': '/metrics', - 'scrape_interval': '30s', - 'scrape_timeout': '10s', - 'honor_labels': True - }, - { - 'job_name': 'telegram-helper-bot', - 'static_configs': [ - { - 'targets': ['bots_telegram_bot:8080'], - 'labels': { - 'bot_name': 'telegram-helper-bot', - 'environment': 'production', - 'service': 'telegram-bot' - } - } - ], - 'metrics_path': '/metrics', - 'scrape_interval': '15s', - 'scrape_timeout': '10s', - 'honor_labels': True - } - ], - 'alerting': { - 'alertmanagers': [ - { - 'static_configs': [ - { - 'targets': [ - '# - alertmanager:9093' - ] - } - ] - } - ] - } - } - - -@pytest.fixture -def mock_aiohttp(): - """Создает мок для aiohttp""" - mock_aiohttp = Mock() - - # Мокаем web.Application - mock_app = Mock() - mock_aiohttp.web.Application.return_value = mock_app - - # Мокаем web.Response - mock_response = Mock() - mock_response.status = 200 - mock_response.content_type = 'text/plain' - mock_response.text = 'Test response' - mock_aiohttp.web.Response.return_value = mock_response - - return mock_aiohttp - - -@pytest.fixture -def mock_request(): - """Создает мок для HTTP запроса""" - request = Mock() - request.method = 'GET' - request.path = '/metrics' - request.headers = {} - return request - - -@pytest.fixture -def test_environment(): - """Создает тестовое окружение""" - return { - 'os_type': 'ubuntu', - 'threshold': 80.0, - 'recovery_threshold': 75.0, - 'host': '127.0.0.1', - 'port': 9091 - } - - -# Маркеры для категоризации тестов -def pytest_configure(config): - """Настройка маркеров pytest""" - config.addinivalue_line( - "markers", "asyncio: mark test as async" - ) - config.addinivalue_line( - "markers", "slow: mark test as slow" - ) - config.addinivalue_line( - "markers", "integration: mark test as integration test" - ) - config.addinivalue_line( - "markers", "unit: mark test as unit test" - ) - config.addinivalue_line( - "markers", "prometheus: mark test as prometheus related" - ) - config.addinivalue_line( - "markers", "metrics: mark test as metrics related" - ) - - -# Автоматическая маркировка тестов -def pytest_collection_modifyitems(config, items): - """Автоматически маркирует тесты по их расположению""" - for item in items: - # Маркируем асинхронные тесты - if "async" in item.name or "Async" in item.name: - item.add_marker(pytest.mark.asyncio) - - # Маркируем интеграционные тесты - if "integration" in item.name.lower() or "Integration" in str(item.cls): - item.add_marker(pytest.mark.integration) - - # Маркируем unit тесты - if "unit" in item.name.lower() or "Unit" in str(item.cls): - item.add_marker(pytest.mark.unit) - - # Маркируем медленные тесты - if "slow" in item.name.lower() or "Slow" in str(item.cls): - item.add_marker(pytest.mark.slow) - - # Маркируем тесты Prometheus - if "prometheus" in item.name.lower() or "Prometheus" in str(item.cls): - item.add_marker(pytest.mark.prometheus) - - # Маркируем тесты метрик - if "metrics" in item.name.lower() or "Metrics" in str(item.cls): - item.add_marker(pytest.mark.metrics) diff --git a/tests/infra/test_alert_delays.py b/tests/infra/test_alert_delays.py deleted file mode 100644 index 2039542..0000000 --- a/tests/infra/test_alert_delays.py +++ /dev/null @@ -1,230 +0,0 @@ -import pytest -import time -from unittest.mock import Mock, patch -import sys -import os - -# Добавляем путь к модулю для импорта -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'infra', 'monitoring')) - -from metrics_collector import MetricsCollector - - -class TestAlertDelays: - """Тесты для механизма задержки алертов""" - - def setup_method(self): - """Настройка перед каждым тестом""" - # Мокаем переменные окружения - with patch.dict(os.environ, { - 'CPU_ALERT_DELAY': '5', # 5 секунд для быстрого тестирования - 'RAM_ALERT_DELAY': '7', # 7 секунд для быстрого тестирования - 'DISK_ALERT_DELAY': '10' # 10 секунд для быстрого тестирования - }): - self.collector = MetricsCollector() - - def test_alert_delays_initialization(self): - """Тест инициализации задержек алертов""" - assert self.collector.alert_delays['cpu'] == 5 - assert self.collector.alert_delays['ram'] == 7 - assert self.collector.alert_delays['disk'] == 10 - - # Проверяем, что время начала превышения инициализировано как None - assert self.collector.alert_start_times['cpu'] is None - assert self.collector.alert_start_times['ram'] is None - assert self.collector.alert_start_times['disk'] is None - - def test_cpu_alert_delay_logic(self): - """Тест логики задержки алерта CPU""" - # Симулируем превышение порога CPU - system_info = { - 'cpu_percent': 85.0, # Выше порога 80% - 'ram_percent': 70.0, # Нормально - 'disk_percent': 75.0, # Нормально - 'load_avg_1m': 2.5, - 'ram_used': 8.0, - 'ram_total': 16.0, - 'disk_free': 25.0 - } - - # Первая проверка - должно начать отсчет задержки - alerts, recoveries = self.collector.check_alerts(system_info) - assert len(alerts) == 0 # Алерт еще не отправлен - assert self.collector.alert_start_times['cpu'] is not None # Время начала установлено - - # Проверяем, что состояние алерта не изменилось - assert not self.collector.alert_states['cpu'] - - # Симулируем время, прошедшее с начала превышения - # Устанавливаем время начала в прошлое (больше задержки) - self.collector.alert_start_times['cpu'] = time.time() - 6 # 6 секунд назад - - # Теперь алерт должен сработать - alerts, recoveries = self.collector.check_alerts(system_info) - assert len(alerts) == 1 # Алерт отправлен - assert alerts[0][0] == 'cpu' # Тип алерта - assert alerts[0][1] == 85.0 # Значение CPU - assert self.collector.alert_states['cpu'] # Состояние алерта установлено - - def test_alert_reset_on_recovery(self): - """Тест сброса алерта при восстановлении""" - # Сначала превышаем порог и ждем задержку - system_info_high = { - 'cpu_percent': 85.0, - 'ram_percent': 70.0, - 'disk_percent': 75.0, - 'load_avg_1m': 2.5, - 'ram_used': 8.0, - 'ram_total': 16.0, - 'disk_free': 25.0 - } - - # Устанавливаем время начала превышения в прошлое - self.collector.alert_start_times['cpu'] = time.time() - 6 - - # Проверяем - алерт должен сработать - alerts, recoveries = self.collector.check_alerts(system_info_high) - assert len(alerts) == 1 # Алерт отправлен - assert self.collector.alert_states['cpu'] # Состояние установлено - - # Теперь симулируем восстановление - system_info_low = { - 'cpu_percent': 70.0, # Ниже порога восстановления 75% - 'ram_percent': 70.0, - 'disk_percent': 75.0, - 'load_avg_1m': 1.2, - 'ram_used': 8.0, - 'ram_total': 16.0, - 'disk_free': 25.0 - } - - alerts, recoveries = self.collector.check_alerts(system_info_low) - assert len(recoveries) == 1 # Сообщение о восстановлении - assert recoveries[0][0] == 'cpu' # Тип восстановления - assert not self.collector.alert_states['cpu'] # Состояние сброшено - assert self.collector.alert_start_times['cpu'] is None # Время сброшено - - def test_multiple_metrics_alert(self): - """Тест алертов по нескольким метрикам одновременно""" - system_info = { - 'cpu_percent': 85.0, # Выше порога - 'ram_percent': 85.0, # Выше порога - 'disk_percent': 75.0, # Нормально - 'load_avg_1m': 2.5, - 'ram_used': 13.0, - 'ram_total': 16.0, - 'disk_free': 25.0 - } - - # Устанавливаем время начала превышения для CPU и RAM в прошлое - self.collector.alert_start_times['cpu'] = time.time() - 6 # Больше CPU_ALERT_DELAY (5 сек) - self.collector.alert_start_times['ram'] = time.time() - 8 # Больше RAM_ALERT_DELAY (7 сек) - - alerts, recoveries = self.collector.check_alerts(system_info) - assert len(alerts) == 2 # Два алерта: CPU и RAM - - # Проверяем типы алертов - alert_types = [alert[0] for alert in alerts] - assert 'cpu' in alert_types - assert 'ram' in alert_types - - # Проверяем состояния - assert self.collector.alert_states['cpu'] - assert self.collector.alert_states['ram'] - assert not self.collector.alert_states['disk'] - - def test_alert_delay_customization(self): - """Тест настройки пользовательских задержек""" - # Тестируем с другими значениями задержек - with patch.dict(os.environ, { - 'CPU_ALERT_DELAY': '2', - 'RAM_ALERT_DELAY': '3', - 'DISK_ALERT_DELAY': '4' - }): - collector = MetricsCollector() - - assert collector.alert_delays['cpu'] == 2 - assert collector.alert_delays['ram'] == 3 - assert collector.alert_delays['disk'] == 4 - - def test_no_false_alerts(self): - """Тест отсутствия ложных алертов при кратковременных пиках""" - system_info = { - 'cpu_percent': 85.0, - 'ram_percent': 70.0, - 'disk_percent': 75.0, - 'load_avg_1m': 2.5, - 'ram_used': 8.0, - 'ram_total': 16.0, - 'disk_free': 25.0 - } - - # Проверяем сразу после превышения порога - alerts, recoveries = self.collector.check_alerts(system_info) - assert len(alerts) == 0 # Алерт не должен сработать сразу - - # Проверяем, что время начала установлено - assert self.collector.alert_start_times['cpu'] is not None - - # Проверяем через короткое время (до истечения задержки) - # Устанавливаем время начала в прошлое, но меньше задержки - self.collector.alert_start_times['cpu'] = time.time() - 2 # 2 секунды назад - - alerts, recoveries = self.collector.check_alerts(system_info) - assert len(alerts) == 0 # Алерт все еще не должен сработать - - def test_alert_state_persistence(self): - """Тест сохранения состояния алерта между проверками""" - system_info = { - 'cpu_percent': 85.0, - 'ram_percent': 70.0, - 'disk_percent': 75.0, - 'load_avg_1m': 2.5, - 'ram_used': 8.0, - 'ram_total': 16.0, - 'disk_free': 25.0 - } - - # Первая проверка - начинаем отсчет - alerts, recoveries = self.collector.check_alerts(system_info) - assert len(alerts) == 0 - initial_time = self.collector.alert_start_times['cpu'] - assert initial_time is not None - - # Проверяем еще раз - время начала должно сохраниться - alerts, recoveries = self.collector.check_alerts(system_info) - assert len(alerts) == 0 - assert self.collector.alert_start_times['cpu'] == initial_time # Время не изменилось - - def test_disk_alert_delay(self): - """Тест задержки алерта для диска""" - system_info = { - 'cpu_percent': 70.0, - 'ram_percent': 70.0, - 'disk_percent': 85.0, # Выше порога - 'load_avg_1m': 1.2, - 'ram_used': 8.0, - 'ram_total': 16.0, - 'disk_free': 15.0 - } - - # Первая проверка - alerts, recoveries = self.collector.check_alerts(system_info) - assert len(alerts) == 0 - assert self.collector.alert_start_times['disk'] is not None - - # Устанавливаем время начала превышения в прошлое, но меньше задержки - self.collector.alert_start_times['disk'] = time.time() - 5 # 5 секунд назад (меньше DISK_ALERT_DELAY) - alerts, recoveries = self.collector.check_alerts(system_info) - assert len(alerts) == 0 # Алерт не должен сработать - - # Устанавливаем время начала превышения в прошлое, больше задержки - self.collector.alert_start_times['disk'] = time.time() - 11 # 11 секунд назад (больше DISK_ALERT_DELAY) - alerts, recoveries = self.collector.check_alerts(system_info) - assert len(alerts) == 1 # Алерт должен сработать - assert alerts[0][0] == 'disk' - - -if __name__ == '__main__': - pytest.main([__file__]) - diff --git a/tests/infra/test_infra.py b/tests/infra/test_infra.py deleted file mode 100644 index 1c3f232..0000000 --- a/tests/infra/test_infra.py +++ /dev/null @@ -1,102 +0,0 @@ -#!/usr/bin/env python3 -""" -Тесты для инфраструктуры мониторинга -""" - -import pytest -import sys -import os - -# Добавляем путь к модулям мониторинга -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../infra/monitoring')) - -def test_imports(): - """Тест импорта основных модулей""" - try: - from metrics_collector import MetricsCollector - from message_sender import MessageSender - from prometheus_server import PrometheusServer - from server_monitor import ServerMonitor - assert True - except ImportError as e: - pytest.fail(f"Failed to import modules: {e}") - -def test_metrics_collector_creation(): - """Тест создания MetricsCollector""" - try: - from metrics_collector import MetricsCollector - collector = MetricsCollector() - assert collector is not None - assert hasattr(collector, 'get_system_info') - assert hasattr(collector, 'get_metrics_data') - except Exception as e: - pytest.fail(f"Failed to create MetricsCollector: {e}") - -def test_message_sender_creation(): - """Тест создания MessageSender""" - try: - from message_sender import MessageSender - sender = MessageSender() - assert sender is not None - except Exception as e: - pytest.fail(f"Failed to create MessageSender: {e}") - -def test_prometheus_server_creation(): - """Тест создания PrometheusServer""" - try: - from prometheus_server import PrometheusServer - server = PrometheusServer() - assert server is not None - assert hasattr(server, 'host') - assert hasattr(server, 'port') - except Exception as e: - pytest.fail(f"Failed to create PrometheusServer: {e}") - -def test_server_monitor_creation(): - """Тест создания ServerMonitor""" - try: - from server_monitor import ServerMonitor - monitor = ServerMonitor() - assert monitor is not None - assert hasattr(monitor, 'metrics_collector') - assert hasattr(monitor, 'message_sender') - assert hasattr(monitor, 'prometheus_server') - except Exception as e: - pytest.fail(f"Failed to create ServerMonitor: {e}") - -def test_system_info_structure(): - """Тест структуры системной информации""" - try: - from metrics_collector import MetricsCollector - collector = MetricsCollector() - system_info = collector.get_system_info() - - # Проверяем, что system_info это словарь - assert isinstance(system_info, dict) - - # Проверяем наличие основных ключей - expected_keys = ['cpu_percent', 'ram_percent', 'disk_percent', 'server_hostname'] - for key in expected_keys: - assert key in system_info, f"Missing key: {key}" - - except Exception as e: - pytest.fail(f"Failed to get system info: {e}") - -def test_metrics_data_structure(): - """Тест структуры метрик""" - try: - from metrics_collector import MetricsCollector - collector = MetricsCollector() - metrics = collector.get_metrics_data() - - # Проверяем, что metrics это словарь - assert isinstance(metrics, dict) - - # Проверяем, что есть хотя бы одна метрика - assert len(metrics) > 0, "Metrics should not be empty" - - except Exception as e: - pytest.fail(f"Failed to get metrics data: {e}") - -if __name__ == "__main__": - pytest.main([__file__, "-v"]) diff --git a/tests/infra/test_message_sender.py b/tests/infra/test_message_sender.py deleted file mode 100644 index 39bc3b8..0000000 --- a/tests/infra/test_message_sender.py +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env python3 -""" -Тесты для MessageSender -""" - -import pytest -import sys -import os - -# Добавляем путь к модулям мониторинга -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../infra/monitoring')) - -from infra.monitoring.message_sender import MessageSender - - -class TestMessageSender: - """Тесты для класса MessageSender""" - - @pytest.fixture - def message_sender(self): - """Создает экземпляр MessageSender для тестов""" - return MessageSender() - - def test_get_cpu_emoji(self, message_sender): - """Тест получения эмодзи для CPU""" - # Тест зеленого уровня (нормальная нагрузка) - assert message_sender._get_cpu_emoji(25.0) == "🟢" - assert message_sender._get_cpu_emoji(49.9) == "🟢" - - # Тест желтого уровня (средняя нагрузка) - assert message_sender._get_cpu_emoji(50.0) == "⚠️" - assert message_sender._get_cpu_emoji(79.9) == "⚠️" - - # Тест красного уровня (высокая нагрузка) - assert message_sender._get_cpu_emoji(80.0) == "🚨" - assert message_sender._get_cpu_emoji(95.0) == "🚨" - - def test_get_memory_emoji(self, message_sender): - """Тест получения эмодзи для памяти""" - # Тест зеленого уровня (нормальное использование) - assert message_sender._get_memory_emoji(30.0) == "🟢" - assert message_sender._get_memory_emoji(59.9) == "🟢" - - # Тест желтого уровня (среднее использование) - assert message_sender._get_memory_emoji(60.0) == "⚠️" - assert message_sender._get_memory_emoji(84.9) == "⚠️" - - # Тест красного уровня (высокое использование) - assert message_sender._get_memory_emoji(85.0) == "🚨" - assert message_sender._get_memory_emoji(95.0) == "🚨" - - def test_get_load_average_emoji(self, message_sender): - """Тест получения эмодзи для Load Average""" - # Тест зеленого уровня (нормальная нагрузка) - assert message_sender._get_load_average_emoji(4.0, 8) == "🟢" # 0.5 на ядро - assert message_sender._get_load_average_emoji(7.9, 8) == "🟢" # 0.9875 на ядро - - # Тест желтого уровня (средняя нагрузка) - assert message_sender._get_load_average_emoji(8.0, 8) == "⚠️" # 1.0 на ядро - assert message_sender._get_load_average_emoji(15.9, 8) == "⚠️" # 1.9875 на ядро - - # Тест красного уровня (высокая нагрузка) - assert message_sender._get_load_average_emoji(16.0, 8) == "🚨" # 2.0 на ядро - assert message_sender._get_load_average_emoji(24.0, 8) == "🚨" # 3.0 на ядро - - def test_get_io_wait_emoji(self, message_sender): - """Тест получения эмодзи для IO Wait""" - # Тест зеленого уровня (нормальный IO Wait) - assert message_sender._get_io_wait_emoji(2.0) == "🟢" - assert message_sender._get_io_wait_emoji(4.9) == "🟢" - - # Тест желтого уровня (средний IO Wait) - assert message_sender._get_io_wait_emoji(5.0) == "⚠️" - assert message_sender._get_io_wait_emoji(19.9) == "⚠️" - - # Тест красного уровня (высокий IO Wait) - assert message_sender._get_io_wait_emoji(20.0) == "🚨" - assert message_sender._get_io_wait_emoji(35.0) == "🚨" - - def test_get_disk_space_emoji(self, message_sender): - """Тест получения эмодзи для дискового пространства""" - # Тест зеленого уровня (нормальное использование) - assert message_sender._get_disk_space_emoji(30.0) == "🟢" - assert message_sender._get_disk_space_emoji(59.9) == "🟢" - - # Тест желтого уровня (среднее использование) - assert message_sender._get_disk_space_emoji(60.0) == "⚠️" - assert message_sender._get_disk_space_emoji(89.9) == "⚠️" - - # Тест красного уровня (высокое использование) - assert message_sender._get_disk_space_emoji(90.0) == "🚨" - assert message_sender._get_disk_space_emoji(95.0) == "🚨" diff --git a/tests/infra/test_metrics_collector.py b/tests/infra/test_metrics_collector.py deleted file mode 100644 index 77d8236..0000000 --- a/tests/infra/test_metrics_collector.py +++ /dev/null @@ -1,464 +0,0 @@ -#!/usr/bin/env python3 -""" -Тесты для MetricsCollector -""" - -import pytest -import sys -import os -import time -import platform -from unittest.mock import Mock, patch, MagicMock -from datetime import datetime - -# Добавляем путь к модулям мониторинга -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../infra/monitoring')) - -from infra.monitoring.metrics_collector import MetricsCollector - - -class TestMetricsCollector: - """Тесты для класса MetricsCollector""" - - @pytest.fixture - def metrics_collector(self): - """Создает экземпляр MetricsCollector для тестов""" - return MetricsCollector() - - @pytest.fixture - def mock_psutil(self): - """Мок для psutil""" - mock_psutil = Mock() - - # Мокаем CPU - mock_psutil.cpu_percent.return_value = 25.5 - mock_psutil.getloadavg.return_value = (1.2, 1.1, 1.0) - mock_psutil.cpu_count.return_value = 8 - - # Мокаем память - mock_memory = Mock() - mock_memory.used = 8 * (1024**3) # 8 GB - mock_memory.total = 16 * (1024**3) # 16 GB - mock_psutil.virtual_memory.return_value = mock_memory - - mock_swap = Mock() - mock_swap.used = 1 * (1024**3) # 1 GB - mock_swap.total = 2 * (1024**3) # 2 GB - mock_swap.percent = 50.0 - mock_psutil.swap_memory.return_value = mock_swap - - # Мокаем диск - mock_disk = Mock() - mock_disk.used = 100 * (1024**3) # 100 GB - mock_disk.total = 500 * (1024**3) # 500 GB - mock_disk.free = 400 * (1024**3) # 400 GB - mock_psutil.disk_usage.return_value = mock_disk - - # Мокаем disk I/O - mock_disk_io = Mock() - mock_disk_io.read_count = 1000 - mock_disk_io.write_count = 500 - mock_disk_io.read_bytes = 1024 * (1024**2) # 1 GB - mock_disk_io.write_bytes = 512 * (1024**2) # 512 MB - mock_psutil.disk_io_counters.return_value = mock_disk_io - - # Мокаем boot time - mock_psutil.boot_time.return_value = time.time() - 86400 # 1 день назад - - return mock_psutil - - def test_init(self, metrics_collector): - """Тест инициализации MetricsCollector""" - assert metrics_collector.threshold == 80.0 - assert metrics_collector.recovery_threshold == 75.0 - assert isinstance(metrics_collector.alert_states, dict) - assert 'cpu' in metrics_collector.alert_states - assert 'ram' in metrics_collector.alert_states - assert 'disk' in metrics_collector.alert_states - assert metrics_collector.monitor_start_time > 0 - - def test_detect_os_macos(self): - """Тест определения macOS""" - with patch('platform.system', return_value='Darwin'): - collector = MetricsCollector() - assert collector.os_type == "macos" - - def test_detect_os_linux(self): - """Тест определения Linux""" - with patch('platform.system', return_value='Linux'): - collector = MetricsCollector() - assert collector.os_type == "ubuntu" - - def test_detect_os_unknown(self): - """Тест определения неизвестной ОС""" - with patch('platform.system', return_value='Windows'): - collector = MetricsCollector() - assert collector.os_type == "unknown" - - def test_get_disk_path(self, metrics_collector): - """Тест получения пути к диску""" - # Для всех ОС должен возвращаться "/" - assert metrics_collector._get_disk_path() == "/" - - @patch('subprocess.run') - def test_get_macos_disk_usage_success(self, mock_subprocess, metrics_collector): - """Тест получения информации о диске macOS через diskutil""" - # Настраиваем мок для macOS - metrics_collector.os_type = "macos" - - # Мокаем успешный вывод diskutil - mock_result = Mock() - mock_result.returncode = 0 - mock_result.stdout = """ - Container Total Space: 500.0 GB - Container Free Space: 400.0 GB - """ - mock_subprocess.return_value = mock_result - - disk_info = metrics_collector._get_macos_disk_usage() - - assert disk_info is not None - assert disk_info.total == 500.0 * (1024**3) # В байтах - assert disk_info.free == 400.0 * (1024**3) - assert disk_info.used == 100.0 * (1024**3) - - @patch('subprocess.run') - def test_get_macos_disk_usage_fallback(self, mock_subprocess, metrics_collector): - """Тест fallback к psutil при ошибке diskutil""" - metrics_collector.os_type = "macos" - - # Мокаем неуспешный вывод diskutil - mock_result = Mock() - mock_result.returncode = 1 - mock_subprocess.return_value = mock_result - - with patch('metrics_collector.psutil.disk_usage') as mock_psutil_disk: - mock_disk = Mock() - mock_disk.used = 100 * (1024**3) - mock_disk.total = 500 * (1024**3) - mock_disk.free = 400 * (1024**3) - mock_psutil_disk.return_value = mock_disk - - disk_info = metrics_collector._get_macos_disk_usage() - assert disk_info == mock_disk - - def test_get_system_uptime(self, metrics_collector): - """Тест получения uptime системы""" - with patch('metrics_collector.psutil.boot_time') as mock_boot_time: - mock_boot_time.return_value = time.time() - 3600 # 1 час назад - - uptime = metrics_collector._get_system_uptime() - assert uptime > 0 - assert uptime <= 3600.1 # Не больше часа (с небольшим допуском) - - def test_get_monitor_uptime(self, metrics_collector): - """Тест получения uptime мониторинга""" - # Ждем немного, чтобы uptime изменился - time.sleep(0.1) - - uptime = metrics_collector.get_monitor_uptime() - assert isinstance(uptime, str) - assert 'м' in uptime or 'ч' in uptime or 'д' in uptime - - def test_get_system_info_success(self, metrics_collector): - """Тест получения системной информации""" - # Мокаем все необходимые функции psutil - with patch('metrics_collector.psutil.cpu_percent', return_value=25.5) as mock_cpu, \ - patch('metrics_collector.psutil.getloadavg', return_value=(1.2, 1.1, 1.0)) as mock_load, \ - patch('metrics_collector.psutil.cpu_count', return_value=8) as mock_cpu_count, \ - patch('metrics_collector.psutil.cpu_times_percent') as mock_cpu_times, \ - patch('metrics_collector.psutil.virtual_memory') as mock_virtual_memory, \ - patch('metrics_collector.psutil.swap_memory') as mock_swap_memory, \ - patch('metrics_collector.psutil.disk_usage') as mock_disk_usage, \ - patch('metrics_collector.psutil.disk_io_counters') as mock_disk_io, \ - patch('metrics_collector.psutil.boot_time', return_value=time.time() - 86400) as mock_boot_time, \ - patch('os.uname') as mock_uname: - - # Настраиваем моки для CPU - mock_cpu_times_obj = Mock() - mock_cpu_times_obj.iowait = 2.5 - mock_cpu_times.return_value = mock_cpu_times_obj - - # Настраиваем моки для памяти - mock_memory = Mock() - mock_memory.used = 8 * (1024**3) - mock_memory.total = 16 * (1024**3) - mock_virtual_memory.return_value = mock_memory - - # Настраиваем моки для swap - mock_swap = Mock() - mock_swap.used = 1 * (1024**3) - mock_swap.total = 2 * (1024**3) - mock_swap.percent = 50.0 - mock_swap_memory.return_value = mock_swap - - # Настраиваем моки для диска - mock_disk = Mock() - mock_disk.used = 100 * (1024**3) - mock_disk.total = 500 * (1024**3) - mock_disk.free = 400 * (1024**3) - mock_disk_usage.return_value = mock_disk - - # Настраиваем моки для disk I/O - mock_disk_io_obj = Mock() - mock_disk_io_obj.read_count = 1000 - mock_disk_io_obj.write_count = 500 - mock_disk_io_obj.read_bytes = 1024 * (1024**2) - mock_disk_io_obj.write_bytes = 512 * (1024**2) - mock_disk_io.return_value = mock_disk_io_obj - - # Настраиваем мок для hostname - mock_uname.return_value.nodename = "test-host" - - # Мокаем _get_disk_usage чтобы возвращал наш мок - with patch.object(metrics_collector, '_get_disk_usage', return_value=mock_disk): - system_info = metrics_collector.get_system_info() - - assert isinstance(system_info, dict) - assert 'cpu_percent' in system_info - assert 'ram_percent' in system_info - assert 'disk_percent' in system_info - assert 'io_wait_percent' in system_info - assert 'server_hostname' in system_info - - # Проверяем расчеты - assert system_info['cpu_percent'] == 25.5 - assert system_info['ram_percent'] == 50.0 # 8/16 * 100 - assert system_info['disk_percent'] == 20.0 # 100/500 * 100 - assert system_info['io_wait_percent'] == 2.5 - assert system_info['server_hostname'] == "test-host" - - def test_get_system_info_error(self, metrics_collector): - """Тест получения системной информации при ошибке""" - with patch('metrics_collector.psutil.cpu_percent', side_effect=Exception("Test error")): - system_info = metrics_collector.get_system_info() - assert system_info == {} - - def test_format_bytes(self, metrics_collector): - """Тест форматирования байтов""" - assert metrics_collector._format_bytes(0) == "0 B" - assert metrics_collector._format_bytes(1024) == "1.0 KB" - assert metrics_collector._format_bytes(1024**2) == "1.0 MB" - assert metrics_collector._format_bytes(1024**3) == "1.0 GB" - assert metrics_collector._format_bytes(1024**4) == "1.0 TB" - - def test_format_uptime(self, metrics_collector): - """Тест форматирования uptime""" - assert metrics_collector._format_uptime(60) == "1м" - assert metrics_collector._format_uptime(3600) == "1ч 0м" - assert metrics_collector._format_uptime(86400) == "1д 0ч 0м" - assert metrics_collector._format_uptime(90000) == "1д 1ч 0м" - - def test_check_process_status_pid_file(self, metrics_collector, tmp_path): - """Тест проверки статуса процесса по PID файлу""" - # Создаем временный PID файл - pid_file = tmp_path / "test_bot.pid" - pid_file.write_text("12345") - - # Временно заменяем путь к PID файлу - original_pid_files = metrics_collector.pid_files.copy() - metrics_collector.pid_files['test_bot'] = str(pid_file) - - with patch('infra.monitoring.metrics_collector.psutil.pid_exists', return_value=True), \ - patch('infra.monitoring.metrics_collector.psutil.Process') as mock_process: - - mock_proc = Mock() - mock_proc.create_time.return_value = time.time() - 3600 - mock_process.return_value = mock_proc - - status, uptime = metrics_collector.check_process_status('test_bot') - - assert status == "✅" - assert "Uptime" in uptime - - # Восстанавливаем оригинальные PID файлы - metrics_collector.pid_files = original_pid_files - - def test_check_process_status_not_running(self, metrics_collector): - """Тест проверки статуса неработающего процесса""" - with patch('metrics_collector.psutil.process_iter', return_value=[]): - status, message = metrics_collector.check_process_status('nonexistent_bot') - assert status == "❌" - assert message == "Выключен" - - def test_calculate_disk_speed(self, metrics_collector): - """Тест расчета скорости диска""" - # Инициализируем базовые значения - metrics_collector._initialize_disk_io() - - # Создаем текущую статистику диска - current_disk_io = Mock() - current_disk_io.read_bytes = 2048 * (1024**2) # 2 GB - current_disk_io.write_bytes = 1024 * (1024**2) # 1 GB - - # Ждем немного для расчета скорости - time.sleep(0.1) - - read_speed, write_speed = metrics_collector._calculate_disk_speed(current_disk_io) - - assert isinstance(read_speed, str) - assert isinstance(write_speed, str) - assert "/s" in read_speed - assert "/s" in write_speed - - def test_calculate_disk_io_percent(self, metrics_collector): - """Тест расчета процента загрузки диска""" - # Инициализируем базовые значения - metrics_collector._initialize_disk_io() - - # Создаем текущую статистику диска - current_disk_io = Mock() - current_disk_io.read_count = 2000 - current_disk_io.write_count = 1000 - current_disk_io.read_bytes = 2048 * (1024**2) - current_disk_io.write_bytes = 1024 * (1024**2) - - # Ждем немного для расчета - time.sleep(0.1) - - io_percent = metrics_collector._calculate_disk_io_percent() - - assert isinstance(io_percent, int) - assert 0 <= io_percent <= 100 - - def test_get_metrics_data(self, metrics_collector): - """Тест получения данных для метрик Prometheus""" - with patch.object(metrics_collector, 'get_system_info') as mock_get_system_info: - mock_get_system_info.return_value = { - 'cpu_percent': 25.5, - 'ram_percent': 60.2, - 'disk_percent': 45.8, - 'load_avg_1m': 1.2, - 'load_avg_5m': 1.1, - 'load_avg_15m': 1.0, - 'swap_percent': 10.5 - } - - with patch.object(metrics_collector, '_get_system_uptime', return_value=86400.0): - metrics_data = metrics_collector.get_metrics_data() - - assert isinstance(metrics_data, dict) - assert 'cpu_usage_percent' in metrics_data - assert 'ram_usage_percent' in metrics_data - assert 'disk_usage_percent' in metrics_data - assert 'load_average_1m' in metrics_data - assert 'system_uptime_seconds' in metrics_data - assert 'monitor_uptime_seconds' in metrics_data - - def test_check_alerts(self, metrics_collector): - """Тест проверки алертов""" - # Сбрасываем состояния алертов для чистого теста - metrics_collector.alert_states = {'cpu': False, 'ram': False, 'disk': False} - metrics_collector.alert_start_times = {'cpu': None, 'ram': None, 'disk': None} - - # Устанавливаем минимальные задержки для тестов - metrics_collector.alert_delays = {'cpu': 0, 'ram': 0, 'disk': 0} - - # Тестируем превышение порога CPU - system_info = { - 'cpu_percent': 85.0, # Выше порога 80.0 - 'ram_percent': 60.0, # Ниже порога - 'disk_percent': 70.0, # Ниже порога - 'load_avg_1m': 2.5, - 'ram_used': 8.0, - 'ram_total': 16.0, - 'disk_free': 300.0 - } - - alerts, recoveries = metrics_collector.check_alerts(system_info) - - assert len(alerts) == 1 - assert alerts[0][0] == 'cpu' # Тип алерта - assert alerts[0][1] == 85.0 # Значение - assert len(recoveries) == 0 - - # Проверяем, что состояние алерта изменилось - assert metrics_collector.alert_states['cpu'] is True - - # Тестируем восстановление - system_info['cpu_percent'] = 70.0 # Ниже recovery threshold 75.0 - - alerts, recoveries = metrics_collector.check_alerts(system_info) - - assert len(alerts) == 0 - assert len(recoveries) == 1 - assert recoveries[0][0] == 'cpu' - assert metrics_collector.alert_states['cpu'] is False - - def test_environment_variables(self): - """Тест работы с переменными окружения""" - with patch.dict(os.environ, {'THRESHOLD': '90.0', 'RECOVERY_THRESHOLD': '85.0'}): - collector = MetricsCollector() - assert collector.threshold == 90.0 - assert collector.recovery_threshold == 85.0 - - def test_metrics_collector_integration(self, metrics_collector): - """Интеграционный тест MetricsCollector""" - # Проверяем, что можем получить системную информацию - system_info = metrics_collector.get_system_info() - - # Даже если некоторые метрики недоступны, должны получить словарь - assert isinstance(system_info, dict) - - # Проверяем, что можем получить метрики для Prometheus - metrics_data = metrics_collector.get_metrics_data() - assert isinstance(metrics_data, dict) - - # Проверяем, что можем проверить алерты - alerts, recoveries = metrics_collector.check_alerts(system_info) - assert isinstance(alerts, list) - assert isinstance(recoveries, list) - - -class TestMetricsCollectorEdgeCases: - """Тесты граничных случаев для MetricsCollector""" - - def test_empty_system_info(self): - """Тест работы с пустой системной информацией""" - with patch('metrics_collector.psutil.cpu_percent', side_effect=Exception("Test error")): - collector = MetricsCollector() - system_info = collector.get_system_info() - assert system_info == {} - - def test_missing_disk_info(self): - """Тест работы при отсутствии информации о диске""" - collector = MetricsCollector() - - with patch.object(collector, '_get_disk_usage', return_value=None), \ - patch('metrics_collector.psutil.cpu_percent', side_effect=Exception("Test error")): - system_info = collector.get_system_info() - assert system_info == {} - - def test_disk_io_calculation_without_previous_data(self): - """Тест расчета I/O диска без предыдущих данных""" - collector = MetricsCollector() - - # Сбрасываем предыдущие данные - collector.last_disk_io = None - collector.last_disk_io_time = None - - current_disk_io = Mock() - current_disk_io.read_bytes = 1024 - current_disk_io.write_bytes = 512 - - read_speed, write_speed = collector._calculate_disk_speed(current_disk_io) - - assert read_speed == "0 B/s" - assert write_speed == "0 B/s" - - def test_uptime_calculation_edge_cases(self): - """Тест расчета uptime для граничных случаев""" - collector = MetricsCollector() - - # Тест для очень малого времени - assert collector._format_uptime(0) == "0м" - assert collector._format_uptime(30) == "0м" - - # Тест для очень большого времени - large_uptime = 365 * 24 * 3600 # 1 год - uptime_str = collector._format_uptime(large_uptime) - assert "д" in uptime_str - - -if __name__ == "__main__": - pytest.main([__file__, "-v"]) diff --git a/tests/infra/test_prometheus_config.py b/tests/infra/test_prometheus_config.py index 9d478a8..63321e6 100644 --- a/tests/infra/test_prometheus_config.py +++ b/tests/infra/test_prometheus_config.py @@ -9,8 +9,6 @@ import sys import os from pathlib import Path -# Добавляем путь к модулям мониторинга -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../infra/monitoring')) class TestPrometheusConfig: @@ -80,38 +78,6 @@ class TestPrometheusConfig: targets = static_configs[0].get('targets', []) assert 'localhost:9090' in targets, "Prometheus should scrape localhost:9090" - def test_infrastructure_job(self, prometheus_config): - """Тест job для инфраструктуры""" - scrape_configs = prometheus_config['scrape_configs'] - - # Ищем job для infrastructure - infra_job = None - for job in scrape_configs: - if job.get('job_name') == 'infrastructure': - infra_job = job - break - - assert infra_job is not None, "Should have infrastructure job" - - # Проверяем основные параметры - assert 'static_configs' in infra_job, "Infrastructure job should have static_configs" - assert 'metrics_path' in infra_job, "Infrastructure job should have metrics_path" - assert 'scrape_interval' in infra_job, "Infrastructure job should have scrape_interval" - assert 'scrape_timeout' in infra_job, "Infrastructure job should have scrape_timeout" - assert 'honor_labels' in infra_job, "Infrastructure job should have honor_labels" - - # Проверяем значения - assert infra_job['metrics_path'] == '/metrics', "Metrics path should be /metrics" - assert infra_job['scrape_interval'] == '30s', "Scrape interval should be 30s" - assert infra_job['scrape_timeout'] == '10s', "Scrape timeout should be 10s" - assert infra_job['honor_labels'] is True, "honor_labels should be True" - - # Проверяем targets - static_configs = infra_job['static_configs'] - assert len(static_configs) > 0, "Should have at least one static config" - - targets = static_configs[0].get('targets', []) - assert 'bots_server_monitor:9091' in targets, "Should scrape bots_server_monitor:9091" def test_telegram_bot_job(self, prometheus_config): """Тест job для telegram-helper-bot""" diff --git a/tests/infra/test_prometheus_integration.py b/tests/infra/test_prometheus_integration.py deleted file mode 100644 index 38ce201..0000000 --- a/tests/infra/test_prometheus_integration.py +++ /dev/null @@ -1,437 +0,0 @@ -#!/usr/bin/env python3 -""" -Интеграционные тесты для Prometheus и связанных компонентов -""" - -import pytest -import pytest_asyncio -import asyncio -import sys -import os -import tempfile -import yaml -from unittest.mock import Mock, AsyncMock, patch, MagicMock -from pathlib import Path - -# Добавляем путь к модулям мониторинга -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../infra/monitoring')) - -from prometheus_server import PrometheusServer -from metrics_collector import MetricsCollector - - -class TestPrometheusIntegration: - """Интеграционные тесты для Prometheus""" - - @pytest_asyncio.fixture - async def prometheus_server(self): - """Создает экземпляр PrometheusServer для интеграционных тестов""" - server = PrometheusServer(host='127.0.0.1', port=0) - return server - - @pytest.fixture - def metrics_collector(self): - """Создает экземпляр MetricsCollector для интеграционных тестов""" - return MetricsCollector() - - @pytest.fixture - def sample_prometheus_config(self): - """Создает пример конфигурации Prometheus для тестов""" - return { - 'global': { - 'scrape_interval': '15s', - 'evaluation_interval': '15s' - }, - 'scrape_configs': [ - { - 'job_name': 'test-infrastructure', - 'static_configs': [ - { - 'targets': ['127.0.0.1:9091'], - 'labels': { - 'environment': 'test', - 'service': 'test-monitoring' - } - } - ], - 'metrics_path': '/metrics', - 'scrape_interval': '30s', - 'scrape_timeout': '10s', - 'honor_labels': True - } - ] - } - - @pytest.mark.integration - @pytest.mark.asyncio - async def test_prometheus_server_with_real_metrics_collector(self, prometheus_server): - """Тест интеграции PrometheusServer с реальным MetricsCollector""" - # Получаем реальные метрики - metrics_data = prometheus_server.metrics_collector.get_metrics_data() - - # Проверяем, что можем получить метрики - assert isinstance(metrics_data, dict) - - # Форматируем метрики в Prometheus формат - prometheus_metrics = prometheus_server._format_prometheus_metrics(metrics_data) - - # Проверяем, что метрики содержат системную информацию - assert '# HELP system_info System information' in prometheus_metrics - assert '# TYPE system_info gauge' in prometheus_metrics - - # Проверяем, что есть хотя бы одна метрика - lines = prometheus_metrics.split('\n') - assert len(lines) >= 3 # system_info help, type, value - - @pytest.mark.integration - def test_metrics_collector_system_integration(self, metrics_collector): - """Тест интеграции MetricsCollector с системой""" - # Получаем системную информацию - system_info = metrics_collector.get_system_info() - - # Проверяем, что получили словарь - assert isinstance(system_info, dict) - - # Проверяем, что можем получить метрики для Prometheus - metrics_data = metrics_collector.get_metrics_data() - assert isinstance(metrics_data, dict) - - # Проверяем, что можем проверить алерты - alerts, recoveries = metrics_collector.check_alerts(system_info) - assert isinstance(alerts, list) - assert isinstance(recoveries, list) - - @pytest.mark.integration - def test_prometheus_metrics_format_integration(self, prometheus_server, metrics_collector): - """Тест интеграции форматирования метрик Prometheus""" - # Получаем реальные метрики - metrics_data = metrics_collector.get_metrics_data() - - # Форматируем в Prometheus формат - prometheus_metrics = prometheus_server._format_prometheus_metrics(metrics_data) - - # Проверяем структуру метрик - lines = prometheus_metrics.split('\n') - - # Должна быть системная информация - system_info_lines = [line for line in lines if 'system_info' in line] - assert len(system_info_lines) >= 3 # help, type, value - - # Проверяем, что метрики содержат правильные типы - type_lines = [line for line in lines if '# TYPE' in line] - assert len(type_lines) > 0 - - # Проверяем, что все метрики имеют правильный формат - metric_lines = [line for line in lines if line and not line.startswith('#')] - for line in metric_lines: - # Проверяем, что строка метрики содержит имя и значение - assert ' ' in line - parts = line.split(' ') - assert len(parts) >= 2 - - @pytest.mark.integration - def test_os_detection_integration(self): - """Тест интеграции определения ОС""" - # Создаем коллектор с реальным определением ОС - collector = MetricsCollector() - - # Проверяем, что ОС определена - assert collector.os_type in ["macos", "ubuntu", "unknown"] - - # Проверяем, что можем получить информацию о диске - disk_info = collector._get_disk_usage() - if disk_info is not None: - assert hasattr(disk_info, 'total') - assert hasattr(disk_info, 'used') - assert hasattr(disk_info, 'free') - - @pytest.mark.integration - def test_disk_io_calculation_integration(self, metrics_collector): - """Тест интеграции расчета I/O диска""" - # Инициализируем базовые значения - metrics_collector._initialize_disk_io() - - # Получаем текущую статистику диска - current_disk_io = metrics_collector._get_disk_io_counters() - - if current_disk_io is not None: - # Рассчитываем скорость - read_speed, write_speed = metrics_collector._calculate_disk_speed(current_disk_io) - - # Проверяем, что получили строки с единицами измерения - assert isinstance(read_speed, str) - assert isinstance(write_speed, str) - assert "/s" in read_speed - assert "/s" in write_speed - - # Рассчитываем процент загрузки - io_percent = metrics_collector._calculate_disk_io_percent() - assert isinstance(io_percent, int) - assert 0 <= io_percent <= 100 - - @pytest.mark.integration - def test_process_monitoring_integration(self, metrics_collector): - """Тест интеграции мониторинга процессов""" - # Проверяем статус процессов - for process_name in ['helper_bot']: - status, message = metrics_collector.check_process_status(process_name) - - # Статус должен быть либо ✅, либо ❌ - assert status in ["✅", "❌"] - - # Сообщение должно быть строкой - assert isinstance(message, str) - - @pytest.mark.integration - def test_alert_system_integration(self, metrics_collector): - """Тест интеграции системы алертов""" - # Сбрасываем состояния алертов для чистого теста - metrics_collector.alert_states = {'cpu': False, 'ram': False, 'disk': False} - metrics_collector.alert_start_times = {'cpu': None, 'ram': None, 'disk': None} - - # Устанавливаем минимальные задержки для тестов - metrics_collector.alert_delays = {'cpu': 0, 'ram': 0, 'disk': 0} - - # Создаем тестовые данные - test_system_info = { - 'cpu_percent': 85.0, # Выше порога - 'ram_percent': 60.0, # Ниже порога - 'disk_percent': 70.0, # Ниже порога - 'load_avg_1m': 2.5, - 'ram_used': 8.0, - 'ram_total': 16.0, - 'disk_free': 300.0 - } - - # Проверяем алерты - alerts, recoveries = metrics_collector.check_alerts(test_system_info) - - # Должен быть хотя бы один алерт для CPU - assert len(alerts) >= 1 - assert any(alert[0] == 'cpu' for alert in alerts) - - # Проверяем, что состояние алерта изменилось - assert metrics_collector.alert_states['cpu'] is True - - # Тестируем восстановление - test_system_info['cpu_percent'] = 70.0 # Ниже recovery threshold - - alerts, recoveries = metrics_collector.check_alerts(test_system_info) - - # Должно быть восстановление - assert len(recoveries) >= 1 - assert any(recovery[0] == 'cpu' for recovery in recoveries) - assert metrics_collector.alert_states['cpu'] is False - - @pytest.mark.integration - def test_uptime_calculation_integration(self, metrics_collector): - """Тест интеграции расчета uptime""" - # Получаем uptime системы - system_uptime = metrics_collector._get_system_uptime() - assert system_uptime > 0 - - # Получаем uptime мониторинга - monitor_uptime = metrics_collector.get_monitor_uptime() - assert isinstance(monitor_uptime, str) - assert len(monitor_uptime) > 0 - - # Форматируем uptime - formatted_uptime = metrics_collector._format_uptime(system_uptime) - assert isinstance(formatted_uptime, str) - assert len(formatted_uptime) > 0 - - @pytest.mark.integration - def test_environment_variables_integration(self): - """Тест интеграции с переменными окружения""" - # Тестируем с пользовательскими значениями - test_threshold = '90.0' - test_recovery_threshold = '85.0' - - with patch.dict(os.environ, { - 'THRESHOLD': test_threshold, - 'RECOVERY_THRESHOLD': test_recovery_threshold - }): - collector = MetricsCollector() - - # Проверяем, что значения установлены - assert collector.threshold == float(test_threshold) - assert collector.recovery_threshold == float(test_recovery_threshold) - - @pytest.mark.integration - def test_prometheus_config_validation_integration(self, sample_prometheus_config): - """Тест интеграции валидации конфигурации Prometheus""" - # Проверяем структуру конфигурации - assert 'global' in sample_prometheus_config - assert 'scrape_configs' in sample_prometheus_config - - global_config = sample_prometheus_config['global'] - assert 'scrape_interval' in global_config - assert 'evaluation_interval' in global_config - - scrape_configs = sample_prometheus_config['scrape_configs'] - assert len(scrape_configs) > 0 - - # Проверяем каждый job - for job in scrape_configs: - assert 'job_name' in job - assert 'static_configs' in job - - static_configs = job['static_configs'] - assert len(static_configs) > 0 - - for static_config in static_configs: - assert 'targets' in static_config - targets = static_config['targets'] - assert len(targets) > 0 - - @pytest.mark.integration - def test_metrics_data_consistency_integration(self, prometheus_server, metrics_collector): - """Тест интеграции консистентности данных метрик""" - # Получаем метрики разными способами - system_info = metrics_collector.get_system_info() - metrics_data = metrics_collector.get_metrics_data() - - # Проверяем консистентность между system_info и metrics_data - # Реальные метрики могут значительно отличаться из-за времени между вызовами - # и системной нагрузки, поэтому используем более широкие допуски - - if 'cpu_percent' in system_info and 'cpu_usage_percent' in metrics_data: - # CPU метрики могут сильно колебаться, используем допуск 50% - # Это связано с тем, что CPU измеряется в разные моменты времени - cpu_diff = abs(system_info['cpu_percent'] - metrics_data['cpu_usage_percent']) - assert cpu_diff < 50.0, f"CPU metrics difference too large: {cpu_diff}% (system: {system_info['cpu_percent']}%, metrics: {metrics_data['cpu_usage_percent']}%)" - - if 'ram_percent' in system_info and 'ram_usage_percent' in metrics_data: - # RAM метрики более стабильны, но все же используем допуск 15% - ram_diff = abs(system_info['ram_percent'] - metrics_data['ram_usage_percent']) - assert ram_diff < 15.0, f"RAM metrics difference too large: {ram_diff}% (system: {system_info['ram_percent']}%, metrics: {metrics_data['ram_usage_percent']}%)" - - if 'disk_percent' in system_info and 'disk_usage_percent' in metrics_data: - # Disk метрики должны быть очень стабильными, допуск 10% - disk_diff = abs(system_info['disk_percent'] - metrics_data['disk_usage_percent']) - assert disk_diff < 10.0, f"Disk metrics difference too large: {disk_diff}% (system: {system_info['disk_percent']}%, metrics: {metrics_data['disk_usage_percent']}%)" - - # Проверяем, что все метрики имеют разумные значения - for metric_name, value in system_info.items(): - if isinstance(value, (int, float)): - assert value >= 0, f"Metric {metric_name} should be non-negative: {value}" - - for metric_name, value in metrics_data.items(): - if isinstance(value, (int, float)): - assert value >= 0, f"Metric {metric_name} should be non-negative: {value}" - - @pytest.mark.integration - def test_error_handling_integration(self, prometheus_server, metrics_collector): - """Тест интеграции обработки ошибок""" - # Тестируем обработку ошибок в PrometheusServer - with patch.object(metrics_collector, 'get_metrics_data', side_effect=Exception("Test error")): - prometheus_server.metrics_collector = metrics_collector - - # Создаем мок запрос - request = Mock() - - # Обрабатываем запрос метрик - response = asyncio.run(prometheus_server.metrics_handler(request)) - - # Должен вернуться ответ с ошибкой - assert response.status == 500 - assert 'Error: Test error' in response.text - - @pytest.mark.integration - def test_performance_integration(self, prometheus_server, metrics_collector): - """Тест интеграции производительности""" - import time - - # Измеряем время получения системной информации - start_time = time.time() - system_info = metrics_collector.get_system_info() - system_info_time = time.time() - start_time - - # Измеряем время получения метрик - start_time = time.time() - metrics_data = metrics_collector.get_metrics_data() - metrics_time = time.time() - start_time - - # Измеряем время форматирования Prometheus метрик - start_time = time.time() - prometheus_metrics = prometheus_server._format_prometheus_metrics(metrics_data) - formatting_time = time.time() - start_time - - # Проверяем, что операции выполняются в разумное время - assert system_info_time < 5.0, f"System info collection took too long: {system_info_time}s" - assert metrics_time < 3.0, f"Metrics collection took too long: {metrics_time}s" - assert formatting_time < 0.1, f"Metrics formatting took too long: {formatting_time}s" - - # Проверяем, что получили данные - assert isinstance(system_info, dict) - assert isinstance(metrics_data, dict) - assert isinstance(prometheus_metrics, str) - assert len(prometheus_metrics) > 0 - - -class TestPrometheusEndToEnd: - """End-to-end тесты для Prometheus""" - - @pytest.mark.integration - @pytest.mark.slow - def test_full_metrics_pipeline(self): - """Тест полного пайплайна метрик""" - # Создаем все компоненты - metrics_collector = MetricsCollector() - prometheus_server = PrometheusServer() - - # 1. Собираем системную информацию - system_info = metrics_collector.get_system_info() - assert isinstance(system_info, dict) - - # 2. Получаем метрики для Prometheus - metrics_data = metrics_collector.get_metrics_data() - assert isinstance(metrics_data, dict) - - # 3. Форматируем метрики в Prometheus формат - prometheus_metrics = prometheus_server._format_prometheus_metrics(metrics_data) - assert isinstance(prometheus_metrics, str) - - # 4. Проверяем, что метрики содержат необходимую информацию - lines = prometheus_metrics.split('\n') - - # Должна быть системная информация - assert any('system_info' in line for line in lines) - - # Должны быть метрики системы - assert any('cpu_usage_percent' in line for line in lines) or any('ram_usage_percent' in line for line in lines) - - # 5. Проверяем алерты - alerts, recoveries = metrics_collector.check_alerts(system_info) - assert isinstance(alerts, list) - assert isinstance(recoveries, list) - - @pytest.mark.integration - @pytest.mark.slow - def test_metrics_stability(self): - """Тест стабильности метрик""" - import time - metrics_collector = MetricsCollector() - - # Получаем метрики несколько раз подряд - metrics_list = [] - for _ in range(3): - metrics = metrics_collector.get_metrics_data() - metrics_list.append(metrics) - time.sleep(0.1) # Небольшая пауза - - # Проверяем, что структура метрик не изменилась - for metrics in metrics_list: - assert isinstance(metrics, dict) - assert len(metrics) > 0 - - # Проверяем, что ключи метрик не изменились - first_keys = set(metrics_list[0].keys()) - for metrics in metrics_list[1:]: - current_keys = set(metrics.keys()) - # Некоторые метрики могут отсутствовать, но структура должна быть похожей - assert len(current_keys.intersection(first_keys)) > 0 - - -if __name__ == "__main__": - pytest.main([__file__, "-v", "-m", "integration"]) diff --git a/tests/infra/test_prometheus_server.py b/tests/infra/test_prometheus_server.py deleted file mode 100644 index 5193b91..0000000 --- a/tests/infra/test_prometheus_server.py +++ /dev/null @@ -1,309 +0,0 @@ -#!/usr/bin/env python3 -""" -Тесты для PrometheusServer -""" - -import pytest -import asyncio -import sys -import os -from unittest.mock import Mock, AsyncMock, patch, MagicMock -from aiohttp import web -from aiohttp.test_utils import TestClient - -# Добавляем путь к модулям мониторинга -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../infra/monitoring')) - -from prometheus_server import PrometheusServer - - -class TestPrometheusServer: - """Тесты для класса PrometheusServer""" - - @pytest.fixture - def prometheus_server(self): - """Создает экземпляр PrometheusServer для тестов""" - return PrometheusServer(host='127.0.0.1', port=9091) - - @pytest.fixture - def mock_metrics_collector(self): - """Создает мок MetricsCollector""" - mock_collector = Mock() - mock_collector.os_type = "ubuntu" - mock_collector.get_metrics_data.return_value = { - 'cpu_usage_percent': 25.5, - 'ram_usage_percent': 60.2, - 'disk_usage_percent': 45.8, - 'load_average_1m': 1.2, - 'load_average_5m': 1.1, - 'load_average_15m': 1.0, - 'swap_usage_percent': 10.5, - 'disk_io_percent': 15.3, - 'system_uptime_seconds': 86400.0, - 'monitor_uptime_seconds': 3600.0 - } - return mock_collector - - def test_init(self, prometheus_server): - """Тест инициализации PrometheusServer""" - assert prometheus_server.host == '127.0.0.1' - assert prometheus_server.port == 9091 - assert prometheus_server.metrics_collector is not None - assert isinstance(prometheus_server.app, web.Application) - - def test_setup_routes(self, prometheus_server): - """Тест настройки маршрутов""" - routes = list(prometheus_server.app.router.routes()) - # aiohttp создает по 2 маршрута для каждого эндпоинта (GET и HEAD) - assert len(routes) == 6 - - # Проверяем наличие всех маршрутов - route_paths = [route.resource.canonical for route in routes] - assert '/' in route_paths - assert '/metrics' in route_paths - assert '/health' in route_paths - - @pytest.mark.asyncio - async def test_root_handler(self, prometheus_server): - """Тест главного обработчика""" - request = Mock() - response = await prometheus_server.root_handler(request) - - assert isinstance(response, web.Response) - assert response.status == 200 - assert response.content_type == 'text/plain' - assert 'Prometheus Metrics Server' in response.text - assert '/metrics' in response.text - assert '/health' in response.text - - @pytest.mark.asyncio - async def test_health_handler(self, prometheus_server): - """Тест health check обработчика""" - request = Mock() - response = await prometheus_server.health_handler(request) - - assert isinstance(response, web.Response) - assert response.status == 200 - assert response.content_type == 'text/plain' - assert response.text == 'OK' - - @pytest.mark.asyncio - async def test_metrics_handler_success(self, prometheus_server, mock_metrics_collector): - """Тест обработчика метрик при успешном получении данных""" - # Заменяем metrics_collector на мок - prometheus_server.metrics_collector = mock_metrics_collector - - request = Mock() - response = await prometheus_server.metrics_handler(request) - - assert isinstance(response, web.Response) - assert response.status == 200 - assert response.content_type == 'text/plain' - - # Проверяем, что метрики содержат ожидаемые данные - metrics_text = response.text - assert '# HELP system_info System information' in metrics_text - assert '# TYPE system_info gauge' in metrics_text - assert 'system_info{os="ubuntu"}' in metrics_text - assert '# HELP cpu_usage_percent CPU usage percentage' in metrics_text - assert 'cpu_usage_percent 25.5' in metrics_text - - @pytest.mark.asyncio - async def test_metrics_handler_error(self, prometheus_server, mock_metrics_collector): - """Тест обработчика метрик при ошибке""" - # Настраиваем мок для вызова исключения - mock_metrics_collector.get_metrics_data.side_effect = Exception("Test error") - prometheus_server.metrics_collector = mock_metrics_collector - - request = Mock() - response = await prometheus_server.metrics_handler(request) - - assert isinstance(response, web.Response) - assert response.status == 500 - assert response.content_type == 'text/plain' - assert 'Error: Test error' in response.text - - def test_format_prometheus_metrics(self, prometheus_server, mock_metrics_collector): - """Тест форматирования метрик в Prometheus формат""" - prometheus_server.metrics_collector = mock_metrics_collector - - metrics_data = mock_metrics_collector.get_metrics_data() - formatted_metrics = prometheus_server._format_prometheus_metrics(metrics_data) - - # Проверяем структуру метрик - lines = formatted_metrics.split('\n') - - # Проверяем наличие системной информации - assert any('system_info' in line for line in lines) - assert any('os="ubuntu"' in line for line in lines) - - # Проверяем наличие CPU метрик - assert any('cpu_usage_percent' in line for line in lines) - assert any('25.5' in line for line in lines) - - # Проверяем наличие RAM метрик - assert any('ram_usage_percent' in line for line in lines) - assert any('60.2' in line for line in lines) - - # Проверяем наличие disk метрик - assert any('disk_usage_percent' in line for line in lines) - assert any('45.8' in line for line in lines) - - # Проверяем наличие load average метрик - assert any('load_average_1m' in line for line in lines) - assert any('1.2' in line for line in lines) - - def test_format_prometheus_metrics_empty_data(self, prometheus_server): - """Тест форматирования метрик с пустыми данными""" - empty_metrics = {} - formatted_metrics = prometheus_server._format_prometheus_metrics(empty_metrics) - - # Должна быть только системная информация - lines = formatted_metrics.split('\n') - assert len(lines) == 3 # system_info help, type, value - assert any('system_info' in line for line in lines) - - def test_format_prometheus_metrics_partial_data(self, prometheus_server, mock_metrics_collector): - """Тест форматирования метрик с частичными данными""" - prometheus_server.metrics_collector = mock_metrics_collector - - # Только CPU метрики - partial_metrics = { - 'cpu_usage_percent': 50.0, - 'load_average_1m': 2.5 - } - - formatted_metrics = prometheus_server._format_prometheus_metrics(partial_metrics) - lines = formatted_metrics.split('\n') - - # Проверяем, что есть системная информация + CPU + load average - assert any('system_info' in line for line in lines) - assert any('cpu_usage_percent' in line for line in lines) - assert any('load_average_1m' in line for line in lines) - assert any('50.0' in line for line in lines) - assert any('2.5' in line for line in lines) - - # Проверяем, что нет RAM метрик - assert not any('ram_usage_percent' in line for line in lines) - - @pytest.mark.asyncio - async def test_start_and_stop(self, prometheus_server): - """Тест запуска и остановки сервера""" - # Мокаем web.AppRunner и TCPSite - with patch('prometheus_server.web.AppRunner') as mock_runner_class, \ - patch('prometheus_server.web.TCPSite') as mock_site_class: - - mock_runner = Mock() - mock_runner.setup = AsyncMock() - mock_runner.cleanup = AsyncMock() - mock_runner_class.return_value = mock_runner - - mock_site = Mock() - mock_site.start = AsyncMock() - mock_site_class.return_value = mock_site - - # Запускаем сервер - runner = await prometheus_server.start() - - # Проверяем, что методы были вызваны - mock_runner.setup.assert_called_once() - mock_site.start.assert_called_once() - assert runner == mock_runner - - # Останавливаем сервер - await prometheus_server.stop(runner) - mock_runner.cleanup.assert_called_once() - - def test_different_os_types(self): - """Тест работы с разными типами ОС""" - # Тестируем macOS - with patch('platform.system', return_value='Darwin'): - server_macos = PrometheusServer() - assert server_macos.metrics_collector.os_type == "macos" - - # Тестируем Linux - with patch('platform.system', return_value='Linux'): - server_linux = PrometheusServer() - assert server_linux.metrics_collector.os_type == "ubuntu" - - # Тестируем неизвестную ОС - with patch('platform.system', return_value='Windows'): - server_unknown = PrometheusServer() - assert server_unknown.metrics_collector.os_type == "unknown" - - def test_custom_host_port(self): - """Тест создания сервера с пользовательскими параметрами""" - server = PrometheusServer(host='192.168.1.100', port=9092) - assert server.host == '192.168.1.100' - assert server.port == 9092 - - def test_metrics_collector_integration(self, prometheus_server): - """Тест интеграции с MetricsCollector""" - # Проверяем, что metrics_collector имеет необходимые методы - collector = prometheus_server.metrics_collector - assert hasattr(collector, 'get_metrics_data') - assert hasattr(collector, 'os_type') - - # Проверяем, что можем получить данные - metrics_data = collector.get_metrics_data() - assert isinstance(metrics_data, dict) - - -class TestPrometheusServerIntegration: - """Интеграционные тесты для PrometheusServer""" - - @pytest.mark.asyncio - async def test_server_creation_integration(self): - """Интеграционный тест создания сервера""" - server = PrometheusServer(host='127.0.0.1', port=0) - - # Проверяем, что сервер создался - assert server is not None - assert server.host == '127.0.0.1' - assert server.port == 0 - - # Проверяем, что приложение создалось - assert server.app is not None - - # Проверяем, что маршруты настроены - routes = list(server.app.router.routes()) - assert len(routes) > 0 - - @pytest.mark.asyncio - async def test_metrics_collector_integration(self): - """Интеграционный тест с MetricsCollector""" - server = PrometheusServer(host='127.0.0.1', port=0) - - # Проверяем, что можем получить метрики - metrics_data = server.metrics_collector.get_metrics_data() - assert isinstance(metrics_data, dict) - - # Проверяем, что можем отформатировать метрики - prometheus_metrics = server._format_prometheus_metrics(metrics_data) - assert isinstance(prometheus_metrics, str) - assert len(prometheus_metrics) > 0 - - @pytest.mark.asyncio - async def test_endpoint_handlers_integration(self): - """Интеграционный тест обработчиков эндпоинтов""" - server = PrometheusServer(host='127.0.0.1', port=0) - - # Тестируем корневой обработчик - request = Mock() - response = await server.root_handler(request) - assert response.status == 200 - assert 'Prometheus Metrics Server' in response.text - - # Тестируем health обработчик - response = await server.health_handler(request) - assert response.status == 200 - assert response.text == 'OK' - - # Тестируем metrics обработчик - response = await server.metrics_handler(request) - assert response.status == 200 - assert '# HELP system_info' in response.text - - -if __name__ == "__main__": - pytest.main([__file__, "-v"]) diff --git a/tests/test_pytest_config.py b/tests/test_pytest_config.py index 0ee8eb0..af60dc1 100644 --- a/tests/test_pytest_config.py +++ b/tests/test_pytest_config.py @@ -17,22 +17,6 @@ def test_pytest_config_loaded(): assert os.path.exists('tests/infra'), "Директория tests/infra должна существовать" assert os.path.exists('tests/bot'), "Директория tests/bot должна существовать" -def test_import_paths(): - """Проверяем, что пути импорта настроены правильно""" - # Проверяем, что можем импортировать модули мониторинга - sys.path.insert(0, 'infra/monitoring') - try: - import metrics_collector - import message_sender - import prometheus_server - import server_monitor - assert True - except ImportError as e: - pytest.fail(f"Failed to import monitoring modules: {e}") - finally: - # Убираем добавленный путь - if 'infra/monitoring' in sys.path: - sys.path.remove('infra/monitoring') def test_test_structure(): """Проверяем структуру тестов""" @@ -41,8 +25,6 @@ def test_test_structure(): assert os.path.exists('tests/infra/__init__.py'), "tests/infra/__init__.py должен существовать" assert os.path.exists('tests/bot/__init__.py'), "tests/bot/__init__.py должен существовать" - # Проверяем наличие тестов инфраструктуры - assert os.path.exists('tests/infra/test_infra.py'), "tests/infra/test_infra.py должен существовать" if __name__ == "__main__": pytest.main([__file__, "-v"])