logboard/client/app/main.py
Сергей Антропов 04dfe30d58 feat: Добавлена поддержка удаленных клиентов для LogBoard+
- Создан LogBoard клиент для отправки логов с удаленных серверов
- Добавлен API эндпоинт /api/logs/remote с аутентификацией
- Реализована структурированная система сохранения логов
- Исправлена совместимость Docker client библиотеки
- Добавлена полная документация и тестирование
2025-08-20 19:25:29 +03:00

215 lines
8.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
Клиент для отправки логов на удаленный сервер LogBoard
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
import asyncio
import json
import logging
import os
import sys
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional
import aiofiles
import aiohttp
import docker
from docker.errors import DockerException
# Настройка логирования
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler('/var/log/logboard-client.log')
]
)
logger = logging.getLogger(__name__)
class LogBoardClient:
"""Клиент для отправки логов в LogBoard сервер"""
def __init__(self, server_url: str, api_key: str, hostname: str):
"""
Инициализация клиента
Args:
server_url: URL сервера LogBoard
api_key: API ключ для аутентификации
hostname: Имя хоста для идентификации
"""
self.server_url = server_url.rstrip('/')
self.api_key = api_key
self.hostname = hostname
self.session: Optional[aiohttp.ClientSession] = None
try:
# Используем тот же способ, что и в основном сервисе
self.docker_client = docker.from_env()
# Проверяем подключение
self.docker_client.ping()
logger.info("Docker клиент успешно инициализирован")
except Exception as e:
logger.error(f"Критическая ошибка Docker клиента: {e}")
raise
async def __aenter__(self):
"""Асинхронный контекстный менеджер - вход"""
self.session = aiohttp.ClientSession(
headers={
'Authorization': f'Bearer {self.api_key}',
'Content-Type': 'application/json',
'User-Agent': 'LogBoard-Client/1.0'
}
)
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""Асинхронный контекстный менеджер - выход"""
if self.session:
await self.session.close()
async def send_logs(self, container_name: str, logs: List[str]) -> bool:
"""
Отправка логов на сервер
Args:
container_name: Имя контейнера
logs: Список строк логов
Returns:
bool: True если отправка успешна, False в противном случае
"""
if not self.session:
logger.error("Сессия не инициализирована")
return False
payload = {
"hostname": self.hostname,
"container_name": container_name,
"logs": logs,
"timestamp": datetime.utcnow().isoformat()
}
try:
async with self.session.post(
f"{self.server_url}/api/logs/remote",
json=payload,
timeout=aiohttp.ClientTimeout(total=30)
) as response:
if response.status == 200:
logger.info(f"Логи контейнера {container_name} успешно отправлены")
return True
else:
logger.error(f"Ошибка отправки логов: {response.status} - {await response.text()}")
return False
except Exception as e:
logger.error(f"Ошибка при отправке логов: {e}")
return False
def get_containers(self) -> List[Dict]:
"""
Получение списка контейнеров Docker
Returns:
List[Dict]: Список контейнеров с информацией
"""
try:
containers = []
for container in self.docker_client.containers.list():
containers.append({
"id": container.id,
"name": container.name,
"status": container.status,
"image": container.image.tags[0] if container.image.tags else container.image.id,
"created": container.attrs['Created']
})
return containers
except DockerException as e:
logger.error(f"Ошибка при получении списка контейнеров: {e}")
return []
async def collect_container_logs(self, container_name: str, lines: int = 100) -> List[str]:
"""
Сбор логов контейнера
Args:
container_name: Имя контейнера
lines: Количество строк логов для сбора
Returns:
List[str]: Список строк логов
"""
try:
container = self.docker_client.containers.get(container_name)
logs = container.logs(
stdout=True,
stderr=True,
tail=lines,
timestamps=True
).decode('utf-8')
return logs.splitlines() if logs else []
except DockerException as e:
logger.error(f"Ошибка при получении логов контейнера {container_name}: {e}")
return []
async def monitor_containers(self, interval: int = 60):
"""
Мониторинг контейнеров и отправка логов
Args:
interval: Интервал мониторинга в секундах
"""
logger.info(f"Запуск мониторинга контейнеров с интервалом {interval} секунд")
while True:
try:
containers = self.get_containers()
logger.info(f"Найдено {len(containers)} контейнеров")
for container in containers:
container_name = container['name']
if container['status'] == 'running':
logs = await self.collect_container_logs(container_name)
if logs:
await self.send_logs(container_name, logs)
except Exception as e:
logger.error(f"Ошибка в мониторинге: {e}")
await asyncio.sleep(interval)
async def main():
"""Основная функция"""
# Получение переменных окружения
server_url = os.getenv('LOGBOARD_SERVER_URL', 'http://localhost:8000')
api_key = os.getenv('LOGBOARD_API_KEY')
hostname = os.getenv('HOSTNAME', os.uname().nodename)
interval = int(os.getenv('LOGBOARD_INTERVAL', '60'))
if not api_key:
logger.error("LOGBOARD_API_KEY не установлен")
sys.exit(1)
logger.info(f"Запуск LogBoard клиента для хоста: {hostname}")
logger.info(f"Подключение к серверу: {server_url}")
async with LogBoardClient(server_url, api_key, hostname) as client:
await client.monitor_containers(interval)
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
logger.info("Получен сигнал прерывания, завершение работы")
except Exception as e:
logger.error(f"Критическая ошибка: {e}")
sys.exit(1)