- Создан LogBoard клиент для отправки логов с удаленных серверов - Добавлен API эндпоинт /api/logs/remote с аутентификацией - Реализована структурированная система сохранения логов - Исправлена совместимость Docker client библиотеки - Добавлена полная документация и тестирование
215 lines
8.0 KiB
Python
215 lines
8.0 KiB
Python
#!/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)
|