- Добавлен параметр include_stopped в функцию get_remote_containers - Исправлен вызов функции в list_containers_with_remote - Добавлена документация для параметра include_stopped - Ошибка больше не появляется в логах сервера Теперь API и WebSocket работают корректно: ✅ API endpoints: 200 OK ✅ WebSocket соединения: устанавливаются ✅ Логи передаются через WebSocket ✅ Нет ошибок в логах сервера Автор: Сергей Антропов Сайт: https://devops.org.ru
391 lines
18 KiB
Python
391 lines
18 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
LogBoard+ - Docker функции
|
||
Автор: Сергей Антропов
|
||
Сайт: https://devops.org.ru
|
||
"""
|
||
|
||
import json
|
||
import os
|
||
from typing import List, Dict, Optional
|
||
|
||
import docker
|
||
|
||
from core.config import (
|
||
DEFAULT_TAIL,
|
||
DEFAULT_PROJECT,
|
||
DEFAULT_PROJECTS,
|
||
SKIP_UNHEALTHY
|
||
)
|
||
from core.logger import docker_logger
|
||
|
||
# Инициализация Docker клиента
|
||
docker_client = docker.from_env()
|
||
|
||
def load_excluded_containers() -> List[str]:
|
||
"""
|
||
Загружает список исключенных контейнеров из JSON файла
|
||
Автор: Сергей Антропов
|
||
Сайт: https://devops.org.ru
|
||
"""
|
||
try:
|
||
with open("app/excluded_containers.json", "r", encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
return data.get("excluded_containers", [])
|
||
except FileNotFoundError:
|
||
docker_logger.warning("Файл app/excluded_containers.json не найден, используем пустой список")
|
||
return []
|
||
except json.JSONDecodeError as e:
|
||
docker_logger.error(f"Ошибка парсинга app/excluded_containers.json: {e}")
|
||
return []
|
||
except Exception as e:
|
||
docker_logger.error(f"Ошибка загрузки app/excluded_containers.json: {e}")
|
||
return []
|
||
|
||
def save_excluded_containers(containers: List[str]) -> bool:
|
||
"""
|
||
Сохраняет список исключенных контейнеров в JSON файл
|
||
Автор: Сергей Антропов
|
||
Сайт: https://devops.org.ru
|
||
"""
|
||
try:
|
||
data = {
|
||
"excluded_containers": containers,
|
||
"description": "Список контейнеров, которые генерируют слишком много логов и исключаются из отображения"
|
||
}
|
||
with open("app/excluded_containers.json", "w", encoding="utf-8") as f:
|
||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||
return True
|
||
except Exception as e:
|
||
docker_logger.error(f"Ошибка сохранения app/excluded_containers.json: {e}")
|
||
return False
|
||
|
||
def get_all_projects() -> List[str]:
|
||
"""
|
||
Получает список всех проектов Docker Compose с учетом исключенных контейнеров
|
||
Автор: Сергей Антропов
|
||
Сайт: https://devops.org.ru
|
||
"""
|
||
projects = set()
|
||
excluded_containers = load_excluded_containers()
|
||
|
||
try:
|
||
containers = docker_client.containers.list(all=True)
|
||
|
||
# Словарь для подсчета контейнеров по проектам
|
||
project_containers = {}
|
||
standalone_containers = []
|
||
|
||
for c in containers:
|
||
try:
|
||
# Пропускаем исключенные контейнеры
|
||
if c.name in excluded_containers:
|
||
continue
|
||
|
||
labels = c.labels or {}
|
||
project = labels.get("com.docker.compose.project")
|
||
|
||
if project:
|
||
if project not in project_containers:
|
||
project_containers[project] = 0
|
||
project_containers[project] += 1
|
||
else:
|
||
standalone_containers.append(c.name)
|
||
|
||
except Exception:
|
||
continue
|
||
|
||
# Добавляем проекты, у которых есть хотя бы один неисключенный контейнер
|
||
for project, count in project_containers.items():
|
||
if count > 0:
|
||
projects.add(project)
|
||
|
||
# Добавляем standalone, если есть неисключенные контейнеры без проекта
|
||
if standalone_containers:
|
||
projects.add("standalone")
|
||
|
||
except Exception as e:
|
||
docker_logger.error(f"Ошибка получения списка проектов: {e}")
|
||
return []
|
||
|
||
result = sorted(list(projects))
|
||
docker_logger.info(f"Доступные проекты (с учетом исключенных контейнеров): {result}")
|
||
return result
|
||
|
||
def list_containers(projects: Optional[List[str]] = None, include_stopped: bool = False) -> List[Dict]:
|
||
"""
|
||
Получает список контейнеров с поддержкой множественных проектов
|
||
Автор: Сергей Антропов
|
||
Сайт: https://devops.org.ru
|
||
"""
|
||
# Загружаем список исключенных контейнеров из JSON файла
|
||
excluded_containers = load_excluded_containers()
|
||
|
||
docker_logger.info(f"Список исключенных контейнеров: {excluded_containers}")
|
||
|
||
items = []
|
||
excluded_count = 0
|
||
|
||
try:
|
||
# Получаем список контейнеров с базовой обработкой ошибок
|
||
containers = docker_client.containers.list(all=include_stopped)
|
||
|
||
for c in containers:
|
||
try:
|
||
# Базовая информация о контейнере (без health check)
|
||
basic_info = {
|
||
"id": c.id[:12],
|
||
"name": c.name,
|
||
"status": c.status,
|
||
"image": "unknown",
|
||
"service": c.name,
|
||
"project": None,
|
||
"health": None,
|
||
"ports": [],
|
||
"url": None,
|
||
}
|
||
|
||
# Безопасно получаем метки
|
||
try:
|
||
labels = c.labels or {}
|
||
basic_info["project"] = labels.get("com.docker.compose.project")
|
||
basic_info["service"] = labels.get("com.docker.compose.service") or c.name
|
||
except Exception:
|
||
pass # Используем значения по умолчанию
|
||
|
||
# Безопасно получаем информацию об образе
|
||
try:
|
||
if c.image and c.image.tags:
|
||
basic_info["image"] = c.image.tags[0]
|
||
elif c.image:
|
||
basic_info["image"] = c.image.short_id
|
||
except Exception:
|
||
pass # Оставляем "unknown"
|
||
|
||
# Безопасно получаем информацию о портах
|
||
try:
|
||
ports = c.ports or {}
|
||
if ports:
|
||
basic_info["ports"] = list(ports.keys())
|
||
|
||
# Пытаемся найти HTTP/HTTPS порт для создания URL
|
||
for port_mapping in ports.values():
|
||
if port_mapping:
|
||
for mapping in port_mapping:
|
||
if isinstance(mapping, dict) and mapping.get("HostPort"):
|
||
host_port = mapping["HostPort"]
|
||
# Проверяем, что это HTTP порт (80, 443, 8080, 3000, etc.)
|
||
host_port_int = int(host_port)
|
||
if (host_port_int in [80, 443] or
|
||
(1 <= host_port_int <= 7999) or
|
||
(8000 <= host_port_int <= 65535)):
|
||
protocol = "https" if host_port == "443" else "http"
|
||
basic_info["url"] = f"{protocol}://localhost:{host_port}"
|
||
basic_info["host_port"] = host_port
|
||
break
|
||
if basic_info["url"]:
|
||
break
|
||
except Exception:
|
||
pass # Оставляем пустые значения
|
||
|
||
# Фильтрация по проектам
|
||
if projects:
|
||
# Если проект не указан, считаем его standalone
|
||
container_project = basic_info["project"] or "standalone"
|
||
if container_project not in projects:
|
||
continue
|
||
|
||
# Фильтрация исключенных контейнеров
|
||
if basic_info["name"] in excluded_containers:
|
||
excluded_count += 1
|
||
docker_logger.warning(f"Пропускаем исключенный контейнер: {basic_info['name']}")
|
||
continue
|
||
|
||
# Добавляем контейнер в список
|
||
items.append(basic_info)
|
||
|
||
except Exception as e:
|
||
# Пропускаем контейнеры с критическими ошибками
|
||
docker_logger.warning(f"Пропускаем проблемный контейнер {c.name if hasattr(c, 'name') else 'unknown'} (ID: {c.id[:12]}): {e}")
|
||
continue
|
||
|
||
except Exception as e:
|
||
docker_logger.error(f"Ошибка получения списка контейнеров: {e}")
|
||
return []
|
||
|
||
# Сортируем по проекту, сервису и имени
|
||
items.sort(key=lambda x: (x.get("project") or "", x.get("service") or "", x.get("name") or ""))
|
||
|
||
# Подсчитываем статистику по проектам
|
||
project_stats = {}
|
||
for item in items:
|
||
project = item.get("project") or "standalone"
|
||
if project not in project_stats:
|
||
project_stats[project] = {"visible": 0, "excluded": 0}
|
||
project_stats[project]["visible"] += 1
|
||
|
||
# Подсчитываем исключенные контейнеры по проектам
|
||
for c in containers:
|
||
try:
|
||
if c.name in excluded_containers:
|
||
labels = c.labels or {}
|
||
project = labels.get("com.docker.compose.project") or "standalone"
|
||
if project not in project_stats:
|
||
project_stats[project] = {"visible": 0, "excluded": 0}
|
||
project_stats[project]["excluded"] += 1
|
||
except Exception:
|
||
continue
|
||
|
||
docker_logger.info(f"Статистика: найдено {len(items)} контейнеров, исключено {excluded_count} контейнеров")
|
||
for project, stats in project_stats.items():
|
||
docker_logger.info(f" 📦 {project}: {stats['visible']} видимых, {stats['excluded']} исключенных")
|
||
|
||
return items
|
||
|
||
def get_remote_hosts() -> List[str]:
|
||
"""
|
||
Получает список удаленных хостов из папки logs/remote
|
||
Автор: Сергей Антропов
|
||
Сайт: https://devops.org.ru
|
||
"""
|
||
remote_hosts = []
|
||
remote_logs_dir = os.path.join(os.getcwd(), 'logs', 'remote')
|
||
|
||
try:
|
||
if os.path.exists(remote_logs_dir):
|
||
for item in os.listdir(remote_logs_dir):
|
||
item_path = os.path.join(remote_logs_dir, item)
|
||
if os.path.isdir(item_path):
|
||
remote_hosts.append(item)
|
||
except Exception as e:
|
||
docker_logger.error(f"Ошибка получения списка удаленных хостов: {e}")
|
||
|
||
return sorted(remote_hosts)
|
||
|
||
def get_remote_containers(hostname: str, include_stopped: bool = False) -> List[Dict]:
|
||
"""
|
||
Получает список контейнеров для удаленного хоста
|
||
Автор: Сергей Антропов
|
||
Сайт: https://devops.org.ru
|
||
|
||
Args:
|
||
hostname: Имя удаленного хоста
|
||
include_stopped: Включать ли остановленные контейнеры
|
||
"""
|
||
containers = []
|
||
remote_logs_dir = os.path.join(os.getcwd(), 'logs', 'remote', hostname)
|
||
|
||
try:
|
||
if os.path.exists(remote_logs_dir):
|
||
for filename in os.listdir(remote_logs_dir):
|
||
if filename.endswith('.log'):
|
||
# Извлекаем имя контейнера из имени файла
|
||
# Формат: container-name-YYYYMMDD.log
|
||
container_name = filename.replace('.log', '')
|
||
# Убираем дату из конца
|
||
if '-' in container_name:
|
||
parts = container_name.split('-')
|
||
if len(parts) > 1 and parts[-1].isdigit() and len(parts[-1]) == 8:
|
||
container_name = '-'.join(parts[:-1])
|
||
|
||
# Получаем информацию о файле
|
||
file_path = os.path.join(remote_logs_dir, filename)
|
||
stat = os.stat(file_path)
|
||
|
||
# Проверяем, активен ли контейнер (логи обновлялись в последние 5 минут)
|
||
import time
|
||
current_time = time.time()
|
||
last_modified = stat.st_mtime
|
||
is_active = (current_time - last_modified) < 300 # 5 минут = 300 секунд
|
||
|
||
# Добавляем контейнер только если он активен или если include_stopped=True
|
||
if is_active or include_stopped:
|
||
containers.append({
|
||
"id": f"remote-{hostname}-{container_name}",
|
||
"name": container_name,
|
||
"status": "running" if is_active else "stopped",
|
||
"image": "remote",
|
||
"service": container_name,
|
||
"project": "remote",
|
||
"health": "healthy" if is_active else "unhealthy",
|
||
"ports": [],
|
||
"url": None,
|
||
"hostname": hostname,
|
||
"is_remote": True,
|
||
"last_modified": last_modified,
|
||
"size": stat.st_size
|
||
})
|
||
except Exception as e:
|
||
docker_logger.error(f"Ошибка получения контейнеров для хоста {hostname}: {e}")
|
||
|
||
return containers
|
||
|
||
def get_all_projects_with_remote() -> List[str]:
|
||
"""
|
||
Получает список всех проектов включая удаленные
|
||
Автор: Сергей Антропов
|
||
Сайт: https://devops.org.ru
|
||
"""
|
||
# Получаем локальные проекты
|
||
local_projects = get_all_projects()
|
||
|
||
# Добавляем удаленные хосты как проекты
|
||
remote_hosts = get_remote_hosts()
|
||
remote_projects = [f"remote-{host}" for host in remote_hosts]
|
||
|
||
# Объединяем и сортируем
|
||
all_projects = local_projects + remote_projects
|
||
return sorted(all_projects)
|
||
|
||
def list_containers_with_remote(projects: Optional[List[str]] = None, include_stopped: bool = False) -> List[Dict]:
|
||
"""
|
||
Получает список всех контейнеров включая удаленные
|
||
Автор: Сергей Антропов
|
||
Сайт: https://devops.org.ru
|
||
"""
|
||
# Получаем локальные контейнеры
|
||
local_containers = list_containers(projects, include_stopped)
|
||
|
||
# Добавляем информацию о том, что это локальные контейнеры
|
||
for container in local_containers:
|
||
container["hostname"] = "localhost"
|
||
container["is_remote"] = False
|
||
|
||
# Получаем удаленные контейнеры
|
||
remote_hosts = get_remote_hosts()
|
||
remote_containers = []
|
||
|
||
for hostname in remote_hosts:
|
||
# Проверяем, нужно ли включать этот хост
|
||
if projects is None or any(f"remote-{hostname}" in project for project in projects):
|
||
host_containers = get_remote_containers(hostname, include_stopped)
|
||
remote_containers.extend(host_containers)
|
||
|
||
# Объединяем локальные и удаленные контейнеры
|
||
all_containers = local_containers + remote_containers
|
||
|
||
# Фильтруем demo и test контейнеры
|
||
filtered_containers = []
|
||
for container in all_containers:
|
||
container_name = container.get("name", "").lower()
|
||
# Исключаем контейнеры с demo или test в названии
|
||
if "demo" not in container_name and "test" not in container_name:
|
||
filtered_containers.append(container)
|
||
|
||
# Фильтруем по проектам, если указаны
|
||
if projects:
|
||
project_filtered_containers = []
|
||
for container in filtered_containers:
|
||
if container["is_remote"]:
|
||
# Для удаленных контейнеров проверяем соответствие хоста
|
||
if any(f"remote-{container['hostname']}" in project for project in projects):
|
||
project_filtered_containers.append(container)
|
||
else:
|
||
# Для локальных контейнеров проверяем проект
|
||
if container["project"] in projects or "standalone" in projects:
|
||
project_filtered_containers.append(container)
|
||
return project_filtered_containers
|
||
|
||
return filtered_containers
|