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..d620e95 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -35,6 +35,9 @@ services:
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:-admin}
- GF_USERS_ALLOW_SIGN_UP=false
- GF_INSTALL_PLUGINS=grafana-clock-panel,grafana-simple-json-datasource
+ - GF_SERVER_ROOT_URL=https://${SERVER_IP:-localhost}/grafana/
+ - GF_SERVER_SERVE_FROM_SUB_PATH=true
+ - GF_SERVER_DOMAIN=${SERVER_IP:-localhost}
ports:
- "3000:3000"
volumes:
@@ -45,35 +48,32 @@ 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
+ # Nginx Reverse Proxy
+ nginx:
+ image: nginx:alpine
+ container_name: bots_nginx
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}
+ - "80:80"
+ - "443:443"
volumes:
- - /proc:/host/proc:ro
- - /sys:/host/sys:ro
- - /var/run:/host/var/run:ro
+ - ./infra/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
+ - ./infra/nginx/conf.d:/etc/nginx/conf.d:ro
+ - ./infra/nginx/ssl:/etc/nginx/ssl:ro
+ - ./infra/nginx/.htpasswd:/etc/nginx/.htpasswd:ro
networks:
- bots_network
depends_on:
+ - grafana
- prometheus
healthcheck:
- test: ["CMD-SHELL", "ps aux | grep python | grep server_monitor || exit 1"]
- interval: 60s
+ test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost/nginx-health"]
+ interval: 30s
timeout: 10s
retries: 3
@@ -81,7 +81,7 @@ services:
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 +122,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 +175,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/env.template b/env.template
index a8af820..63839d0 100644
--- a/env.template
+++ b/env.template
@@ -21,3 +21,9 @@ PROMETHEUS_RETENTION_DAYS=30
# Grafana Configuration
GRAFANA_ADMIN_USER=admin
GRAFANA_ADMIN_PASSWORD=admin
+
+# Server Configuration
+SERVER_IP=your_server_ip_here
+
+# Status Page Configuration
+STATUS_PAGE_PASSWORD=admin123
diff --git a/infra/ansible/playbook.yml b/infra/ansible/playbook.yml
index b8c8027..cb509d1 100644
--- a/infra/ansible/playbook.yml
+++ b/infra/ansible/playbook.yml
@@ -54,6 +54,9 @@
- prometheus-node-exporter
- fail2ban
- tzdata
+ - nginx
+ - openssl
+ - apache2-utils
state: present
- name: Установить часовой пояс Europe/Moscow
@@ -257,6 +260,112 @@
- "80" # HTTP
- "443" # HTTPS
+ # --- НАСТРОЙКА NGINX ---
+ - name: Остановить nginx (если запущен)
+ systemd:
+ name: nginx
+ state: stopped
+ ignore_errors: yes
+
+ - name: Создать директории для nginx конфигураций
+ file:
+ path: "{{ item }}"
+ state: directory
+ owner: root
+ group: root
+ mode: '0755'
+ loop:
+ - "{{ project_root }}/infra/nginx"
+ - "{{ project_root }}/infra/nginx/ssl"
+ - "{{ project_root }}/infra/nginx/conf.d"
+
+ - name: Сгенерировать самоподписанный SSL сертификат
+ command: >
+ openssl req -x509 -newkey rsa:4096 -keyout {{ project_root }}/infra/nginx/ssl/key.pem
+ -out {{ project_root }}/infra/nginx/ssl/cert.pem -days 365 -nodes
+ -subj "/CN={{ ansible_host }}/O=Monitoring/C=RU"
+ args:
+ creates: "{{ project_root }}/infra/nginx/ssl/cert.pem"
+
+ - name: Установить права на SSL сертификаты
+ file:
+ path: "{{ item }}"
+ owner: root
+ group: root
+ mode: '0600'
+ loop:
+ - "{{ project_root }}/infra/nginx/ssl/cert.pem"
+ - "{{ project_root }}/infra/nginx/ssl/key.pem"
+
+ - name: Создать htpasswd файл для status page
+ htpasswd:
+ path: "{{ project_root }}/infra/nginx/.htpasswd"
+ name: "admin"
+ password: "{{ lookup('env', 'STATUS_PAGE_PASSWORD') | default('admin123') }}"
+ owner: root
+ group: root
+ mode: '0644'
+
+ - name: Скопировать основную конфигурацию nginx
+ copy:
+ src: "{{ project_root }}/infra/nginx/nginx.conf"
+ dest: /etc/nginx/nginx.conf
+ owner: root
+ group: root
+ mode: '0644'
+ backup: yes
+
+ - name: Скопировать конфигурации nginx для сервисов
+ copy:
+ src: "{{ project_root }}/infra/nginx/conf.d/"
+ dest: /etc/nginx/conf.d/
+ owner: root
+ group: root
+ mode: '0644'
+ backup: yes
+
+ - name: Скопировать SSL сертификаты
+ copy:
+ src: "{{ project_root }}/infra/nginx/ssl/"
+ dest: /etc/nginx/ssl/
+ owner: root
+ group: root
+ mode: '0600'
+ backup: yes
+
+ - name: Скопировать htpasswd файл
+ copy:
+ src: "{{ project_root }}/infra/nginx/.htpasswd"
+ dest: /etc/nginx/.htpasswd
+ owner: root
+ group: root
+ mode: '0644'
+ backup: yes
+
+ - name: Проверить конфигурацию nginx
+ command: nginx -t
+ register: nginx_config_test
+ changed_when: false
+
+ - name: Показать результат проверки nginx
+ debug:
+ var: nginx_config_test.stdout_lines
+
+ - name: Включить и запустить nginx
+ systemd:
+ name: nginx
+ enabled: yes
+ state: started
+
+ - name: Проверить статус nginx
+ command: systemctl status nginx
+ register: nginx_status
+ changed_when: false
+
+ - name: Показать статус nginx
+ debug:
+ var: nginx_status.stdout_lines
+
- name: Проверить существование пользователя deploy
getent:
database: passwd
@@ -686,6 +795,49 @@
timeout: 30
state: started
+ - name: Проверить, что порт 80 (Nginx HTTP) открыт
+ wait_for:
+ port: 80
+ host: "{{ ansible_host }}"
+ timeout: 30
+ state: started
+
+ - name: Проверить, что порт 443 (Nginx HTTPS) открыт
+ wait_for:
+ port: 443
+ host: "{{ ansible_host }}"
+ timeout: 30
+ state: started
+
+ - name: Проверить доступность Nginx
+ uri:
+ url: "http://{{ ansible_host }}/nginx-health"
+ method: GET
+ status_code: 200
+ register: nginx_health
+ retries: 5
+ delay: 10
+
+ - name: Проверить доступность Grafana через Nginx
+ uri:
+ url: "https://{{ ansible_host }}/grafana/api/health"
+ method: GET
+ status_code: 200
+ validate_certs: no
+ register: grafana_nginx_health
+ retries: 5
+ delay: 10
+
+ - name: Проверить доступность Prometheus через Nginx
+ uri:
+ url: "https://{{ ansible_host }}/prometheus/-/healthy"
+ method: GET
+ status_code: 200
+ validate_certs: no
+ register: prometheus_nginx_health
+ retries: 5
+ delay: 10
+
- name: Проверить доступность Grafana API
uri:
url: "http://{{ ansible_host }}:3000/api/health"
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/nginx/README.md b/infra/nginx/README.md
new file mode 100644
index 0000000..e43831b
--- /dev/null
+++ b/infra/nginx/README.md
@@ -0,0 +1,106 @@
+# Nginx Reverse Proxy Configuration
+
+## Обзор
+
+Данная конфигурация nginx обеспечивает безопасный доступ к сервисам мониторинга через HTTPS с самоподписанными SSL сертификатами.
+
+## Архитектура
+
+```
+Интернет → Nginx (443) →
+ ├→ /grafana → Grafana (3000)
+ ├→ /prometheus → Prometheus (9090)
+ ├→ /status → Status page (с Basic Auth)
+ └→ / → Redirect to /grafana
+```
+
+## Структура файлов
+
+```
+infra/nginx/
+├── nginx.conf # Основная конфигурация nginx
+├── ssl/ # SSL сертификаты (создаются автоматически)
+│ ├── cert.pem # SSL сертификат
+│ └── key.pem # Приватный ключ
+├── conf.d/ # Конфигурации location'ов
+│ ├── grafana.conf # Конфиг для Grafana
+│ ├── prometheus.conf # Конфиг для Prometheus
+│ └── status.conf # Конфиг для status page
+└── .htpasswd # Basic Auth для status page
+```
+
+## Доступ к сервисам
+
+### Grafana
+- **URL**: `https://your-server-ip/grafana/`
+- **Аутентификация**: Grafana admin credentials
+- **Особенности**: Настроен для работы через sub-path
+
+### Prometheus
+- **URL**: `https://your-server-ip/prometheus/`
+- **Особенности**: Полный доступ к Prometheus UI
+
+### Status Page
+- **URL**: `https://your-server-ip/status`
+- **Аутентификация**: Basic Auth (admin/admin123 по умолчанию)
+- **Особенности**: Показывает статус nginx (заготовка для Uptime Kuma)
+
+## Переменные окружения
+
+Добавьте в ваш `.env` файл:
+
+```bash
+# Server Configuration
+SERVER_IP=your_server_ip_here
+
+# Status Page Configuration
+STATUS_PAGE_PASSWORD=admin123
+```
+
+## Безопасность
+
+- **SSL/TLS**: Самоподписанные сертификаты (365 дней)
+- **Rate Limiting**: 10 req/s для API, 1 req/s для status page
+- **Security Headers**: X-Frame-Options, X-Content-Type-Options, CSP
+- **Basic Auth**: Для status page
+- **Fail2ban**: Интеграция с nginx логами
+
+## Мониторинг
+
+- **Health Check**: `https://your-server-ip/nginx-health`
+- **Nginx Status**: `https://your-server-ip/nginx_status` (только локальные сети)
+- **Logs**: `/var/log/nginx/access.log`, `/var/log/nginx/error.log`
+
+## Развертывание
+
+Конфигурация автоматически развертывается через Ansible playbook:
+
+```bash
+ansible-playbook -i inventory.ini playbook.yml
+```
+
+## Устранение неполадок
+
+### Проверка конфигурации nginx
+```bash
+nginx -t
+```
+
+### Проверка SSL сертификатов
+```bash
+openssl x509 -in /etc/nginx/ssl/cert.pem -text -noout
+```
+
+### Проверка доступности сервисов
+```bash
+curl -k https://your-server-ip/grafana/api/health
+curl -k https://your-server-ip/prometheus/-/healthy
+curl -k https://your-server-ip/nginx-health
+```
+
+## Будущие улучшения
+
+- Интеграция с Uptime Kuma для status page
+- Let's Encrypt сертификаты вместо самоподписанных
+- Дополнительные security headers
+- Мониторинг nginx метрик в Prometheus
diff --git a/infra/nginx/conf.d/grafana.conf b/infra/nginx/conf.d/grafana.conf
new file mode 100644
index 0000000..5a4e2ed
--- /dev/null
+++ b/infra/nginx/conf.d/grafana.conf
@@ -0,0 +1,32 @@
+# Grafana reverse proxy configuration
+upstream grafana_backend {
+ server grafana:3000;
+ keepalive 32;
+}
+
+# Grafana proxy configuration
+location /grafana/ {
+ proxy_pass http://grafana_backend/;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_set_header X-Forwarded-Port $server_port;
+
+ # WebSocket support for Grafana
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+
+ # Timeouts
+ proxy_connect_timeout 60s;
+ proxy_send_timeout 60s;
+ proxy_read_timeout 60s;
+
+ # Buffer settings
+ proxy_buffering on;
+ proxy_buffer_size 4k;
+ proxy_buffers 8 4k;
+ proxy_busy_buffers_size 8k;
+}
diff --git a/infra/nginx/conf.d/prometheus.conf b/infra/nginx/conf.d/prometheus.conf
new file mode 100644
index 0000000..b3a3156
--- /dev/null
+++ b/infra/nginx/conf.d/prometheus.conf
@@ -0,0 +1,34 @@
+# Prometheus reverse proxy configuration
+upstream prometheus_backend {
+ server prometheus:9090;
+ keepalive 32;
+}
+
+# Prometheus proxy configuration
+location /prometheus/ {
+ proxy_pass http://prometheus_backend/;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_set_header X-Forwarded-Port $server_port;
+
+ # Timeouts
+ proxy_connect_timeout 30s;
+ proxy_send_timeout 30s;
+ proxy_read_timeout 30s;
+
+ # Buffer settings
+ proxy_buffering on;
+ proxy_buffer_size 4k;
+ proxy_buffers 8 4k;
+ proxy_busy_buffers_size 8k;
+}
+
+# Health check endpoint
+location /prometheus/-/healthy {
+ proxy_pass http://prometheus_backend/-/healthy;
+ proxy_set_header Host $host;
+ access_log off;
+}
diff --git a/infra/nginx/conf.d/status.conf b/infra/nginx/conf.d/status.conf
new file mode 100644
index 0000000..9b89b20
--- /dev/null
+++ b/infra/nginx/conf.d/status.conf
@@ -0,0 +1,24 @@
+# Status page configuration (for future uptime kuma integration)
+
+# Rate limiting for status page
+location /status {
+ # Basic authentication for status page
+ auth_basic "Status Page Access";
+ auth_basic_user_file /etc/nginx/.htpasswd;
+
+ # Placeholder for future uptime kuma integration
+ # For now, show nginx status
+ access_log off;
+ return 200 '{"status": "ok", "nginx": "running", "timestamp": "$time_iso8601"}';
+ add_header Content-Type application/json;
+}
+
+# Nginx status stub (for monitoring)
+location /nginx_status {
+ stub_status on;
+ access_log off;
+ allow 127.0.0.1;
+ allow 172.16.0.0/12; # Docker networks
+ allow 192.168.0.0/16; # Private networks
+ deny all;
+}
diff --git a/infra/nginx/nginx.conf b/infra/nginx/nginx.conf
new file mode 100644
index 0000000..645bd02
--- /dev/null
+++ b/infra/nginx/nginx.conf
@@ -0,0 +1,103 @@
+user nginx;
+worker_processes auto;
+error_log /var/log/nginx/error.log warn;
+pid /var/run/nginx.pid;
+
+events {
+ worker_connections 1024;
+ use epoll;
+ multi_accept on;
+}
+
+http {
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ # Logging format
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+ '$status $body_bytes_sent "$http_referer" '
+ '"$http_user_agent" "$http_x_forwarded_for"';
+
+ access_log /var/log/nginx/access.log main;
+
+ # Basic settings
+ sendfile on;
+ tcp_nopush on;
+ tcp_nodelay on;
+ keepalive_timeout 65;
+ types_hash_max_size 2048;
+ client_max_body_size 16M;
+
+ # Gzip compression
+ gzip on;
+ gzip_vary on;
+ gzip_min_length 1024;
+ gzip_proxied any;
+ gzip_comp_level 6;
+ gzip_types
+ text/plain
+ text/css
+ text/xml
+ text/javascript
+ application/json
+ application/javascript
+ application/xml+rss
+ application/atom+xml
+ image/svg+xml;
+
+ # Rate limiting
+ limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
+ limit_req_zone $binary_remote_addr zone=status:10m rate=1r/s;
+
+ # Security headers
+ add_header X-Frame-Options "SAMEORIGIN" always;
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header X-XSS-Protection "1; mode=block" always;
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
+ add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' wss: https:;" always;
+
+ # SSL configuration
+ ssl_protocols TLSv1.2 TLSv1.3;
+ ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384;
+ ssl_prefer_server_ciphers off;
+ ssl_session_cache shared:SSL:10m;
+ ssl_session_timeout 10m;
+
+ # Main server block
+ server {
+ listen 80;
+ server_name _;
+ return 301 https://$host$request_uri;
+ }
+
+ server {
+ listen 443 ssl http2;
+ server_name _;
+
+ # SSL configuration
+ ssl_certificate /etc/nginx/ssl/cert.pem;
+ ssl_certificate_key /etc/nginx/ssl/key.pem;
+
+ # Security headers
+ add_header X-Frame-Options "SAMEORIGIN" always;
+ add_header X-Content-Type-Options "nosniff" always;
+
+ # Rate limiting
+ limit_req zone=api burst=20 nodelay;
+
+ # Redirect root to Grafana
+ location = / {
+ return 301 /grafana/;
+ }
+
+ # Health check endpoint
+ location /nginx-health {
+ access_log off;
+ return 200 "healthy\n";
+ add_header Content-Type text/plain;
+ }
+
+ # Include location configurations
+ include /etc/nginx/conf.d/*.conf;
+ }
+}
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"])