Сергей Антропов 9d4add2a7d fix: resolve static files and import issues
- Fix static files not loading due to volume mount conflict
- Remove problematic volume mount from docker-compose.yml
- Add __init__.py files to make Python packages
- Fix all import statements to use relative imports
- Update start.sh to use correct module name
- Update config.py with correct default paths and values
- Ensure all environment variables are properly loaded from .env file
2025-08-20 18:14:35 +03:00

222 lines
8.6 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
# -*- coding: utf-8 -*-
"""
LogBoard+ - Логи API
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
import re
from datetime import datetime
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query, Body
from fastapi.responses import JSONResponse
import docker
from core.auth import get_current_user
from core.docker import docker_client, DEFAULT_TAIL
from core.logger import api_logger
router = APIRouter()
@router.get("/stats/{container_id}")
def api_logs_stats(container_id: str, current_user: str = Depends(get_current_user)):
"""Получить статистику логов контейнера"""
try:
# Ищем контейнер
container = None
for c in docker_client.containers.list(all=True):
if c.id.startswith(container_id):
container = c
break
if container is None:
return JSONResponse({"error": "Container not found"}, status_code=404)
# Получаем логи
logs = container.logs(tail=1000).decode(errors="ignore")
# Подсчитываем статистику
stats = {"debug": 0, "info": 0, "warn": 0, "error": 0}
for line in logs.split('\n'):
if not line.strip():
continue
line_lower = line.lower()
if 'level=debug' in line_lower or 'debug' in line_lower:
stats["debug"] += 1
elif 'level=info' in line_lower or 'info' in line_lower:
stats["info"] += 1
elif 'level=warning' in line_lower or 'level=warn' in line_lower or 'warning' in line_lower or 'warn' in line_lower:
stats["warn"] += 1
elif 'level=error' in line_lower or 'error' in line_lower:
stats["error"] += 1
return JSONResponse(
content=stats,
headers={
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
"Expires": "0"
}
)
except Exception as e:
api_logger.error(f"Error getting log stats for {container_id}: {e}")
return JSONResponse({"error": str(e)}, status_code=500)
@router.get("/{container_id}")
def api_logs(
container_id: str,
tail: str = Query(str(DEFAULT_TAIL), description="Количество последних строк логов или 'all' для всех логов"),
since: Optional[str] = Query(None, description="Время начала в формате ISO или относительное время (например, '10m', '1h')"),
current_user: str = Depends(get_current_user)
):
"""
Получить логи контейнера через AJAX
Args:
container_id: ID контейнера
tail: Количество последних строк или 'all' для всех логов (по умолчанию 500)
since: Время начала для фильтрации логов
Returns:
JSON с логами и метаданными
"""
try:
# Ищем контейнер
container = None
for c in docker_client.containers.list(all=True):
if c.id.startswith(container_id):
container = c
break
if container is None:
return JSONResponse({"error": "Container not found"}, status_code=404)
# Формируем параметры для получения логов
log_params = {
'timestamps': True
}
# Обрабатываем параметр tail
if tail.lower() == 'all':
# Для всех логов не указываем параметр tail
pass
else:
try:
tail_lines = int(tail)
log_params['tail'] = tail_lines
except ValueError:
# Если не удалось преобразовать в число, используем значение по умолчанию
log_params['tail'] = DEFAULT_TAIL
# Добавляем фильтр по времени, если указан (используем Unix timestamp секундной точности)
if since:
def _parse_since(value: str) -> Optional[int]:
try:
# Числовое значение (unix timestamp)
if re.fullmatch(r"\d+", value or ""):
return int(value)
# ISO 8601 с Z
if value.endswith('Z'):
dt = datetime.fromisoformat(value.replace('Z', '+00:00'))
return int(dt.timestamp())
# Пытаемся распарсить как ISO без Z
try:
dt2 = datetime.fromisoformat(value)
if dt2.tzinfo is None:
# Считаем UTC, если таймзона не указана
from datetime import timezone
dt2 = dt2.replace(tzinfo=timezone.utc)
return int(dt2.timestamp())
except Exception:
pass
except Exception:
return None
return None
parsed_since = _parse_since(since)
if parsed_since is not None:
log_params['since'] = parsed_since
# Получаем логи
logs = container.logs(**log_params).decode(errors="ignore")
# Разбиваем на строки и обрабатываем
log_lines = []
for line in logs.split('\n'):
if line.strip():
# Извлекаем временную метку и сообщение
timestamp_match = re.match(r'^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z)\s+(.+)$', line)
if timestamp_match:
timestamp, message = timestamp_match.groups()
log_lines.append({
'timestamp': timestamp,
'message': message,
'raw': line
})
else:
# Если временная метка не найдена, используем всю строку как сообщение
log_lines.append({
'timestamp': None,
'message': line,
'raw': line
})
# Получаем информацию о контейнере
container_info = {
'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'],
'state': container.attrs['State']
}
return JSONResponse(
content={
'container': container_info,
'logs': log_lines,
'total_lines': len(log_lines),
'tail': tail,
'since': since,
'timestamp': datetime.now().isoformat()
},
headers={
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
"Expires": "0"
}
)
except Exception as e:
api_logger.error(f"Error getting logs for {container_id}: {e}")
return JSONResponse({"error": str(e)}, status_code=500)
@router.post("/snapshot")
def api_snapshot(
current_user: str = Depends(get_current_user),
container_id: str = Body(..., embed=True),
service: str = Body("", embed=True),
content: str = Body("", embed=True),
):
"""Сохранить снимок логов"""
import os
from core.config import SNAP_DIR
# Save posted content as a snapshot file
safe_service = re.sub(r"[^a-zA-Z0-9_.-]+", "_", service or container_id[:12])
ts = os.getenv("TZ_TS") or ""
from datetime import datetime
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
fname = f"{safe_service}-{stamp}.log"
fpath = os.path.join(SNAP_DIR, fname)
with open(fpath, "w", encoding="utf-8") as f:
f.write(content)
url = f"/snapshots/{fname}"
return {"file": fname, "url": url}