Dev 3 #3
53
Dockerfile
53
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 \
|
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||||
procps \
|
gcc \
|
||||||
|
python3-dev \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Установка рабочей директории
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Копирование файлов зависимостей
|
|
||||||
COPY requirements.txt .
|
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 && \
|
# Этап 2: Финальный образ (Runtime)
|
||||||
useradd -m -u 1000 -g monitor monitor && \
|
###########################################
|
||||||
chown -R 1000:1000 /app
|
# Используем ОЧЕНЬ легковесный базовый образ
|
||||||
|
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
|
USER 1000
|
||||||
|
|
||||||
# Команда по умолчанию для запуска мониторинга
|
# Важно: явно указываем Python искать зависимости в скопированной директории
|
||||||
CMD ["python", "infra/monitoring/main.py"]
|
ENV PYTHONPATH="/usr/local/lib/python3.9/site-packages:${PYTHONPATH}"
|
||||||
|
|
||||||
|
# Оставляем базовую команду для совместимости
|
||||||
|
CMD ["python", "-c", "print('Dockerfile готов для использования')"]
|
||||||
@@ -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"]
|
|
||||||
12
Makefile
12
Makefile
@@ -25,9 +25,6 @@ down: ## Остановить все сервисы
|
|||||||
logs: ## Показать логи всех сервисов
|
logs: ## Показать логи всех сервисов
|
||||||
docker-compose logs -f
|
docker-compose logs -f
|
||||||
|
|
||||||
logs-monitor: ## Показать логи мониторинга
|
|
||||||
docker-compose logs -f server_monitor
|
|
||||||
|
|
||||||
logs-prometheus: ## Показать логи Prometheus
|
logs-prometheus: ## Показать логи Prometheus
|
||||||
docker-compose logs -f prometheus
|
docker-compose logs -f prometheus
|
||||||
|
|
||||||
@@ -45,9 +42,6 @@ restart: ## Перезапустить все сервисы
|
|||||||
docker-compose build --no-cache
|
docker-compose build --no-cache
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
|
|
||||||
restart-monitor: ## Перезапустить только мониторинг
|
|
||||||
docker-compose restart server_monitor
|
|
||||||
|
|
||||||
restart-prometheus: ## Перезапустить только Prometheus
|
restart-prometheus: ## Перезапустить только Prometheus
|
||||||
docker-compose restart prometheus
|
docker-compose restart prometheus
|
||||||
|
|
||||||
@@ -82,7 +76,6 @@ backup: ## Создать backup данных
|
|||||||
@tar -czf "backups/backup-$(date +%Y%m%d-%H%M%S).tar.gz" \
|
@tar -czf "backups/backup-$(date +%Y%m%d-%H%M%S).tar.gz" \
|
||||||
infra/grafana/provisioning/ \
|
infra/grafana/provisioning/ \
|
||||||
infra/prometheus/ \
|
infra/prometheus/ \
|
||||||
infra/monitoring/ \
|
|
||||||
.env \
|
.env \
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
@echo "✅ Backup created in backups/"
|
@echo "✅ Backup created in backups/"
|
||||||
@@ -212,13 +205,10 @@ check-ports: ## Проверить занятые порты
|
|||||||
@echo "Port 8081 (AnonBot):"
|
@echo "Port 8081 (AnonBot):"
|
||||||
@lsof -i :8081 2>/dev/null || echo " Free"
|
@lsof -i :8081 2>/dev/null || echo " Free"
|
||||||
|
|
||||||
check-grafana: ## Проверить состояние Grafana
|
|
||||||
@echo "📊 Checking Grafana status..."
|
|
||||||
@cd infra/monitoring && python3 check_grafana.py
|
|
||||||
|
|
||||||
check-deps: ## Проверить зависимости инфраструктуры
|
check-deps: ## Проверить зависимости инфраструктуры
|
||||||
@echo "🔍 Проверяю зависимости инфраструктуры..."
|
@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 "✅ Зависимости инфраструктуры установлены"
|
@echo "✅ Зависимости инфраструктуры установлены"
|
||||||
|
|
||||||
check-bot-deps: ## Проверить зависимости Telegram бота
|
check-bot-deps: ## Проверить зависимости Telegram бота
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -100,19 +100,11 @@ docker-compose ps
|
|||||||
- **Назначение**: Сбор и хранение метрик, API для запросов
|
- **Назначение**: Сбор и хранение метрик, API для запросов
|
||||||
- **Доступ**: Публичный (проброс из контейнера)
|
- **Доступ**: Публичный (проброс из контейнера)
|
||||||
- **Функции**:
|
- **Функции**:
|
||||||
- Сбор метрик с server_monitor (порт 9091)
|
|
||||||
- Сбор метрик с telegram-bot (порт 8080)
|
- Сбор метрик с telegram-bot (порт 8080)
|
||||||
|
- Сбор метрик с anon-bot (порт 8081)
|
||||||
|
- Сбор метрик с node_exporter (порт 9100)
|
||||||
- Хранение исторических данных
|
- Хранение исторических данных
|
||||||
|
|
||||||
#### **Порт 9091 - Server Monitor**
|
|
||||||
- **Контейнер**: `bots_server_monitor`
|
|
||||||
- **Назначение**: Мониторинг системных ресурсов сервера
|
|
||||||
- **Доступ**: Внутренний (только внутри Docker сети)
|
|
||||||
- **Функции**:
|
|
||||||
- Сбор CPU, RAM, Disk метрик
|
|
||||||
- Отправка алертов в Telegram
|
|
||||||
- Предоставление метрик для Prometheus
|
|
||||||
|
|
||||||
#### **Порт 8080 - Telegram Bot**
|
#### **Порт 8080 - Telegram Bot**
|
||||||
- **Контейнер**: `bots_telegram_bot`
|
- **Контейнер**: `bots_telegram_bot`
|
||||||
- **Назначение**: Основной функционал Telegram бота
|
- **Назначение**: Основной функционал Telegram бота
|
||||||
@@ -152,7 +144,6 @@ docker-compose ps
|
|||||||
docker-compose logs
|
docker-compose logs
|
||||||
|
|
||||||
# Только мониторинг
|
# Только мониторинг
|
||||||
docker-compose logs -f server_monitor
|
|
||||||
|
|
||||||
# Prometheus
|
# Prometheus
|
||||||
docker logs bots_prometheus
|
docker logs bots_prometheus
|
||||||
@@ -165,7 +156,6 @@ docker logs bots_grafana
|
|||||||
|
|
||||||
### Автоматическая проверка
|
### Автоматическая проверка
|
||||||
```bash
|
```bash
|
||||||
cd infra/monitoring
|
|
||||||
python3 check_grafana.py
|
python3 check_grafana.py
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -204,7 +194,6 @@ make health # Проверить здоровье всех сервисо
|
|||||||
### 📊 Мониторинг и логи
|
### 📊 Мониторинг и логи
|
||||||
```bash
|
```bash
|
||||||
make logs # Логи всех сервисов
|
make logs # Логи всех сервисов
|
||||||
make logs-monitor # Логи только мониторинга
|
|
||||||
make logs-bot # Логи Telegram бота
|
make logs-bot # Логи Telegram бота
|
||||||
make logs-errors # Только ошибки из логов
|
make logs-errors # Только ошибки из логов
|
||||||
make monitoring # Открыть Grafana в браузере
|
make monitoring # Открыть Grafana в браузере
|
||||||
@@ -213,7 +202,6 @@ make prometheus # Открыть Prometheus в браузере
|
|||||||
|
|
||||||
### 🔧 Управление отдельными сервисами
|
### 🔧 Управление отдельными сервисами
|
||||||
```bash
|
```bash
|
||||||
make restart-monitor # Перезапустить только мониторинг
|
|
||||||
make restart-grafana # Перезапустить только Grafana
|
make restart-grafana # Перезапустить только Grafana
|
||||||
make restart-prometheus # Перезапустить только Prometheus
|
make restart-prometheus # Перезапустить только Prometheus
|
||||||
make restart-bot # Перезапустить только Telegram бота
|
make restart-bot # Перезапустить только Telegram бота
|
||||||
|
|||||||
@@ -45,43 +45,17 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- prometheus
|
- prometheus
|
||||||
healthcheck:
|
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
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|
||||||
# Server Monitoring Service
|
|
||||||
server_monitor:
|
|
||||||
build: .
|
|
||||||
container_name: bots_server_monitor
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "9091:9091"
|
|
||||||
environment:
|
|
||||||
- TELEGRAM_BOT_TOKEN=${TELEGRAM_MONITORING_BOT_TOKEN}
|
|
||||||
- GROUP_FOR_LOGS=${GROUP_MONITORING_FOR_LOGS}
|
|
||||||
- IMPORTANT_LOGS=${IMPORTANT_MONITORING_LOGS}
|
|
||||||
- THRESHOLD=${THRESHOLD:-80.0}
|
|
||||||
- RECOVERY_THRESHOLD=${RECOVERY_THRESHOLD:-75.0}
|
|
||||||
volumes:
|
|
||||||
- /proc:/host/proc:ro
|
|
||||||
- /sys:/host/sys:ro
|
|
||||||
- /var/run:/host/var/run:ro
|
|
||||||
networks:
|
|
||||||
- bots_network
|
|
||||||
depends_on:
|
|
||||||
- prometheus
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "ps aux | grep python | grep server_monitor || exit 1"]
|
|
||||||
interval: 60s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
# Telegram Helper Bot
|
# Telegram Helper Bot
|
||||||
telegram-bot:
|
telegram-bot:
|
||||||
build:
|
build:
|
||||||
context: ./bots/telegram-helper-bot
|
context: ./bots/telegram-helper-bot
|
||||||
dockerfile: Dockerfile.bot
|
dockerfile: Dockerfile
|
||||||
container_name: bots_telegram_bot
|
container_name: bots_telegram_bot
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file:
|
env_file:
|
||||||
@@ -122,7 +96,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- prometheus
|
- prometheus
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
@@ -175,7 +149,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- prometheus
|
- prometheus
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8081/health"]
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8081/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -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']
|
|
||||||
@@ -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()
|
|
||||||
@@ -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())
|
|
||||||
@@ -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}** | <code>{system_info['current_time']}</code>
|
|
||||||
---------------------------------
|
|
||||||
**📊 Общая нагрузка:**
|
|
||||||
CPU: <b>{system_info['cpu_percent']}%</b> {cpu_emoji} | LA: <b>{system_info['load_avg_1m']} / {system_info['cpu_count']}</b> {la_emoji} | IO Wait: <b>{system_info['io_wait_percent']}%</b> {io_wait_emoji}
|
|
||||||
|
|
||||||
**💾 Память:**
|
|
||||||
RAM: <b>{system_info['ram_used']}/{system_info['ram_total']} GB</b> ({system_info['ram_percent']}%) {ram_emoji}
|
|
||||||
Swap: <b>{system_info['swap_used']}/{system_info['swap_total']} GB</b> ({system_info['swap_percent']}%) {swap_emoji}
|
|
||||||
|
|
||||||
**🗂️ Дисковое пространство:**
|
|
||||||
Диск (/): <b>{system_info['disk_used']}/{system_info['disk_total']} GB</b> ({system_info['disk_percent']}%) {disk_emoji}
|
|
||||||
|
|
||||||
**💿 Диск I/O:**
|
|
||||||
Read: <b>{system_info['disk_read_speed']}</b> | Write: <b>{system_info['disk_write_speed']}</b>
|
|
||||||
Диск загружен: <b>{system_info['disk_io_percent']}%</b>
|
|
||||||
|
|
||||||
**🤖 Процессы:**
|
|
||||||
{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}
|
|
||||||
**Текущее значение:** <b>{current_value}%</b> ⚠️
|
|
||||||
**Пороговое значение:** 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}
|
|
||||||
**Текущее значение:** <b>{current_value}%</b> ✔️
|
|
||||||
**Было превышение:** До {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}")
|
|
||||||
@@ -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: ошибка"
|
|
||||||
@@ -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)
|
|
||||||
@@ -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 сервер остановлен")
|
|
||||||
@@ -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()
|
|
||||||
@@ -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())
|
|
||||||
@@ -11,15 +11,6 @@ scrape_configs:
|
|||||||
static_configs:
|
static_configs:
|
||||||
- targets: ['localhost:9090']
|
- 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 для мониторинга Node Exporter
|
||||||
- job_name: 'node'
|
- job_name: 'node'
|
||||||
static_configs:
|
static_configs:
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
psutil>=5.9.0
|
|
||||||
asyncio
|
|
||||||
aiohttp>=3.8.0
|
|
||||||
python-dotenv>=1.0.0
|
|
||||||
requests>=2.28.0
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -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__])
|
|
||||||
|
|
||||||
@@ -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"])
|
|
||||||
@@ -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) == "🚨"
|
|
||||||
@@ -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"])
|
|
||||||
@@ -9,8 +9,6 @@ import sys
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Добавляем путь к модулям мониторинга
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../infra/monitoring'))
|
|
||||||
|
|
||||||
|
|
||||||
class TestPrometheusConfig:
|
class TestPrometheusConfig:
|
||||||
@@ -80,38 +78,6 @@ class TestPrometheusConfig:
|
|||||||
targets = static_configs[0].get('targets', [])
|
targets = static_configs[0].get('targets', [])
|
||||||
assert 'localhost:9090' in targets, "Prometheus should scrape localhost:9090"
|
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):
|
def test_telegram_bot_job(self, prometheus_config):
|
||||||
"""Тест job для telegram-helper-bot"""
|
"""Тест job для telegram-helper-bot"""
|
||||||
|
|||||||
@@ -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"])
|
|
||||||
@@ -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"])
|
|
||||||
@@ -17,22 +17,6 @@ def test_pytest_config_loaded():
|
|||||||
assert os.path.exists('tests/infra'), "Директория tests/infra должна существовать"
|
assert os.path.exists('tests/infra'), "Директория tests/infra должна существовать"
|
||||||
assert os.path.exists('tests/bot'), "Директория tests/bot должна существовать"
|
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():
|
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/infra/__init__.py'), "tests/infra/__init__.py должен существовать"
|
||||||
assert os.path.exists('tests/bot/__init__.py'), "tests/bot/__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__":
|
if __name__ == "__main__":
|
||||||
pytest.main([__file__, "-v"])
|
pytest.main([__file__, "-v"])
|
||||||
|
|||||||
Reference in New Issue
Block a user