This commit is contained in:
2025-09-16 18:52:24 +03:00
33 changed files with 515 additions and 4657 deletions

View File

@@ -54,6 +54,9 @@
- prometheus-node-exporter
- fail2ban
- tzdata
- nginx
- openssl
- apache2-utils
state: present
- name: Установить часовой пояс Europe/Moscow
@@ -257,6 +260,112 @@
- "80" # HTTP
- "443" # HTTPS
# --- НАСТРОЙКА NGINX ---
- name: Остановить nginx (если запущен)
systemd:
name: nginx
state: stopped
ignore_errors: yes
- name: Создать директории для nginx конфигураций
file:
path: "{{ item }}"
state: directory
owner: root
group: root
mode: '0755'
loop:
- "{{ project_root }}/infra/nginx"
- "{{ project_root }}/infra/nginx/ssl"
- "{{ project_root }}/infra/nginx/conf.d"
- name: Сгенерировать самоподписанный SSL сертификат
command: >
openssl req -x509 -newkey rsa:4096 -keyout {{ project_root }}/infra/nginx/ssl/key.pem
-out {{ project_root }}/infra/nginx/ssl/cert.pem -days 365 -nodes
-subj "/CN={{ ansible_host }}/O=Monitoring/C=RU"
args:
creates: "{{ project_root }}/infra/nginx/ssl/cert.pem"
- name: Установить права на SSL сертификаты
file:
path: "{{ item }}"
owner: root
group: root
mode: '0600'
loop:
- "{{ project_root }}/infra/nginx/ssl/cert.pem"
- "{{ project_root }}/infra/nginx/ssl/key.pem"
- name: Создать htpasswd файл для status page
htpasswd:
path: "{{ project_root }}/infra/nginx/.htpasswd"
name: "admin"
password: "{{ lookup('env', 'STATUS_PAGE_PASSWORD') | default('admin123') }}"
owner: root
group: root
mode: '0644'
- name: Скопировать основную конфигурацию nginx
copy:
src: "{{ project_root }}/infra/nginx/nginx.conf"
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
backup: yes
- name: Скопировать конфигурации nginx для сервисов
copy:
src: "{{ project_root }}/infra/nginx/conf.d/"
dest: /etc/nginx/conf.d/
owner: root
group: root
mode: '0644'
backup: yes
- name: Скопировать SSL сертификаты
copy:
src: "{{ project_root }}/infra/nginx/ssl/"
dest: /etc/nginx/ssl/
owner: root
group: root
mode: '0600'
backup: yes
- name: Скопировать htpasswd файл
copy:
src: "{{ project_root }}/infra/nginx/.htpasswd"
dest: /etc/nginx/.htpasswd
owner: root
group: root
mode: '0644'
backup: yes
- name: Проверить конфигурацию nginx
command: nginx -t
register: nginx_config_test
changed_when: false
- name: Показать результат проверки nginx
debug:
var: nginx_config_test.stdout_lines
- name: Включить и запустить nginx
systemd:
name: nginx
enabled: yes
state: started
- name: Проверить статус nginx
command: systemctl status nginx
register: nginx_status
changed_when: false
- name: Показать статус nginx
debug:
var: nginx_status.stdout_lines
- name: Проверить существование пользователя deploy
getent:
database: passwd
@@ -688,6 +797,49 @@
timeout: 30
state: started
- name: Проверить, что порт 80 (Nginx HTTP) открыт
wait_for:
port: 80
host: "{{ ansible_host }}"
timeout: 30
state: started
- name: Проверить, что порт 443 (Nginx HTTPS) открыт
wait_for:
port: 443
host: "{{ ansible_host }}"
timeout: 30
state: started
- name: Проверить доступность Nginx
uri:
url: "http://{{ ansible_host }}/nginx-health"
method: GET
status_code: 200
register: nginx_health
retries: 5
delay: 10
- name: Проверить доступность Grafana через Nginx
uri:
url: "https://{{ ansible_host }}/grafana/api/health"
method: GET
status_code: 200
validate_certs: no
register: grafana_nginx_health
retries: 5
delay: 10
- name: Проверить доступность Prometheus через Nginx
uri:
url: "https://{{ ansible_host }}/prometheus/-/healthy"
method: GET
status_code: 200
validate_certs: no
register: prometheus_nginx_health
retries: 5
delay: 10
- name: Проверить доступность Grafana API
uri:
url: "http://{{ ansible_host }}:3000/api/health"

View File

@@ -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"
}

View File

@@ -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']

View File

@@ -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()

View File

@@ -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())

View File

@@ -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}")

View File

@@ -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: ошибка"

View File

@@ -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)

View File

@@ -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 сервер остановлен")

View File

@@ -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()

View File

@@ -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())

106
infra/nginx/README.md Normal file
View File

@@ -0,0 +1,106 @@
# Nginx Reverse Proxy Configuration
## Обзор
Данная конфигурация nginx обеспечивает безопасный доступ к сервисам мониторинга через HTTPS с самоподписанными SSL сертификатами.
## Архитектура
```
Интернет → Nginx (443) →
├→ /grafana → Grafana (3000)
├→ /prometheus → Prometheus (9090)
├→ /status → Status page (с Basic Auth)
└→ / → Redirect to /grafana
```
## Структура файлов
```
infra/nginx/
├── nginx.conf # Основная конфигурация nginx
├── ssl/ # SSL сертификаты (создаются автоматически)
│ ├── cert.pem # SSL сертификат
│ └── key.pem # Приватный ключ
├── conf.d/ # Конфигурации location'ов
│ ├── grafana.conf # Конфиг для Grafana
│ ├── prometheus.conf # Конфиг для Prometheus
│ └── status.conf # Конфиг для status page
└── .htpasswd # Basic Auth для status page
```
## Доступ к сервисам
### Grafana
- **URL**: `https://your-server-ip/grafana/`
- **Аутентификация**: Grafana admin credentials
- **Особенности**: Настроен для работы через sub-path
### Prometheus
- **URL**: `https://your-server-ip/prometheus/`
- **Особенности**: Полный доступ к Prometheus UI
### Status Page
- **URL**: `https://your-server-ip/status`
- **Аутентификация**: Basic Auth (admin/admin123 по умолчанию)
- **Особенности**: Показывает статус nginx (заготовка для Uptime Kuma)
## Переменные окружения
Добавьте в ваш `.env` файл:
```bash
# Server Configuration
SERVER_IP=your_server_ip_here
# Status Page Configuration
STATUS_PAGE_PASSWORD=admin123
```
## Безопасность
- **SSL/TLS**: Самоподписанные сертификаты (365 дней)
- **Rate Limiting**: 10 req/s для API, 1 req/s для status page
- **Security Headers**: X-Frame-Options, X-Content-Type-Options, CSP
- **Basic Auth**: Для status page
- **Fail2ban**: Интеграция с nginx логами
## Мониторинг
- **Health Check**: `https://your-server-ip/nginx-health`
- **Nginx Status**: `https://your-server-ip/nginx_status` (только локальные сети)
- **Logs**: `/var/log/nginx/access.log`, `/var/log/nginx/error.log`
## Развертывание
Конфигурация автоматически развертывается через Ansible playbook:
```bash
ansible-playbook -i inventory.ini playbook.yml
```
## Устранение неполадок
### Проверка конфигурации nginx
```bash
nginx -t
```
### Проверка SSL сертификатов
```bash
openssl x509 -in /etc/nginx/ssl/cert.pem -text -noout
```
### Проверка доступности сервисов
```bash
curl -k https://your-server-ip/grafana/api/health
curl -k https://your-server-ip/prometheus/-/healthy
curl -k https://your-server-ip/nginx-health
```
## Будущие улучшения
- Интеграция с Uptime Kuma для status page
- Let's Encrypt сертификаты вместо самоподписанных
- Дополнительные security headers
- Мониторинг nginx метрик в Prometheus

View File

@@ -0,0 +1,32 @@
# Grafana reverse proxy configuration
upstream grafana_backend {
server grafana:3000;
keepalive 32;
}
# Grafana proxy configuration
location /grafana/ {
proxy_pass http://grafana_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# WebSocket support for Grafana
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Buffer settings
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
proxy_busy_buffers_size 8k;
}

View File

@@ -0,0 +1,34 @@
# Prometheus reverse proxy configuration
upstream prometheus_backend {
server prometheus:9090;
keepalive 32;
}
# Prometheus proxy configuration
location /prometheus/ {
proxy_pass http://prometheus_backend/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# Timeouts
proxy_connect_timeout 30s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
# Buffer settings
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
proxy_busy_buffers_size 8k;
}
# Health check endpoint
location /prometheus/-/healthy {
proxy_pass http://prometheus_backend/-/healthy;
proxy_set_header Host $host;
access_log off;
}

View File

@@ -0,0 +1,24 @@
# Status page configuration (for future uptime kuma integration)
# Rate limiting for status page
location /status {
# Basic authentication for status page
auth_basic "Status Page Access";
auth_basic_user_file /etc/nginx/.htpasswd;
# Placeholder for future uptime kuma integration
# For now, show nginx status
access_log off;
return 200 '{"status": "ok", "nginx": "running", "timestamp": "$time_iso8601"}';
add_header Content-Type application/json;
}
# Nginx status stub (for monitoring)
location /nginx_status {
stub_status on;
access_log off;
allow 127.0.0.1;
allow 172.16.0.0/12; # Docker networks
allow 192.168.0.0/16; # Private networks
deny all;
}

103
infra/nginx/nginx.conf Normal file
View File

@@ -0,0 +1,103 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging format
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
# Basic settings
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 16M;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;
# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=status:10m rate=1r/s;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' wss: https:;" always;
# SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# Main server block
server {
listen 80;
server_name _;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name _;
# SSL configuration
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
# Rate limiting
limit_req zone=api burst=20 nodelay;
# Redirect root to Grafana
location = / {
return 301 /grafana/;
}
# Health check endpoint
location /nginx-health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# Include location configurations
include /etc/nginx/conf.d/*.conf;
}
}

View File

@@ -11,15 +11,6 @@ scrape_configs:
static_configs:
- targets: ['localhost:9090']
# Job для мониторинга инфраструктуры
- job_name: 'infrastructure'
static_configs:
- targets: ['bots_server_monitor:9091'] # Порт для метрик сервера мониторинга
metrics_path: '/metrics'
scrape_interval: 30s
scrape_timeout: 10s
honor_labels: true
# Job для мониторинга Node Exporter
- job_name: 'node'
static_configs: