feat: добавлены страницы ошибок и кнопка OTHER в LogLevels

- Добавлена кнопка OTHER в LogLevels для неклассифицированных логов
- Созданы красивые страницы ошибок с поддержкой темной/светлой темы
- Добавлены обработчики для ошибок 401, 403, 404, 500
- Реализована безопасность: убраны детали ошибок из пользовательского интерфейса
- Кнопка 'Войти в систему' показывается только на странице ошибки 403
- На странице 403 убран error-message, оставлен только auth-notice
- Обновлены счетчики логов для поддержки уровня OTHER
- Добавлены тестовые маршруты для проверки страниц ошибок
- Улучшен UX: адаптивный дизайн, интерактивность, навигация

Автор: Сергей Антропов
Сайт: https://devops.org.ru
This commit is contained in:
Сергей Антропов
2025-08-17 18:49:54 +03:00
parent a979dd2838
commit d0a4b57233
5 changed files with 547 additions and 362 deletions

213
app.py
View File

@@ -18,6 +18,8 @@ from pathlib import Path
import docker
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Query, Depends, HTTPException, status, Body, Request, Response
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse, RedirectResponse
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi.staticfiles import StaticFiles
@@ -52,6 +54,182 @@ app = FastAPI(
version="1.0.0"
)
# Обработчики исключений
@app.exception_handler(404)
async def not_found_handler(request: Request, exc: HTTPException):
"""Обработчик ошибки 404 - Страница не найдена"""
return templates.TemplateResponse("error.html", {
"request": request,
"error_code": 404,
"error_title": "Страница не найдена",
"error_message": "Запрашиваемая страница не существует или была перемещена."
}, status_code=404)
@app.exception_handler(401)
async def unauthorized_handler(request: Request, exc: HTTPException):
"""Обработчик ошибки 401 - Не авторизован"""
return templates.TemplateResponse("error.html", {
"request": request,
"error_code": 401,
"error_title": "Требуется авторизация",
"error_message": "Для доступа к этой странице необходимо войти в систему."
}, status_code=401)
@app.exception_handler(403)
async def forbidden_handler(request: Request, exc: HTTPException):
"""Обработчик ошибки 403 - Доступ запрещен"""
return templates.TemplateResponse("error.html", {
"request": request,
"error_code": 403,
"error_title": "Доступ запрещен",
"error_message": "У вас нет прав для доступа к этой странице."
}, status_code=403)
@app.exception_handler(500)
async def internal_server_error_handler(request: Request, exc: HTTPException):
"""Обработчик ошибки 500 - Внутренняя ошибка сервера"""
return templates.TemplateResponse("error.html", {
"request": request,
"error_code": 500,
"error_title": "Внутренняя ошибка сервера",
"error_message": "Произошла непредвиденная ошибка. Попробуйте обновить страницу или обратитесь к администратору."
}, status_code=500)
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
"""Общий обработчик HTTP исключений"""
# Для API маршрутов возвращаем JSON ответ
if request.url.path.startswith("/api/"):
if exc.status_code == 401:
return JSONResponse(
status_code=401,
content={
"error": "unauthorized",
"message": "Требуется авторизация",
"details": "Для доступа к этому API необходимо войти в систему."
},
headers={"WWW-Authenticate": "Bearer"}
)
elif exc.status_code == 403:
return JSONResponse(
status_code=403,
content={
"error": "forbidden",
"message": "Доступ запрещен",
"details": "У вас нет прав для доступа к этому API."
}
)
else:
return JSONResponse(
status_code=exc.status_code,
content={
"error": f"http_{exc.status_code}",
"message": exc.detail or "Произошла ошибка при обработке запроса.",
"details": f"URL: {request.url.path}"
}
)
# Для обычных страниц возвращаем HTML
if exc.status_code == 401:
title = "Требуется авторизация"
message = "Для доступа к этой странице необходимо войти в систему."
elif exc.status_code == 403:
title = "Доступ запрещен"
message = "У вас нет прав для доступа к этой странице."
elif exc.status_code == 404:
title = "Страница не найдена"
message = "Запрашиваемая страница не существует или была перемещена."
elif exc.status_code == 500:
title = "Внутренняя ошибка сервера"
message = "Произошла непредвиденная ошибка. Попробуйте обновить страницу или обратитесь к администратору."
else:
title = f"Ошибка {exc.status_code}"
message = exc.detail or "Произошла ошибка при обработке запроса."
return templates.TemplateResponse("error.html", {
"request": request,
"error_code": exc.status_code,
"error_title": title,
"error_message": message
}, status_code=exc.status_code)
@app.exception_handler(StarletteHTTPException)
async def starlette_http_exception_handler(request: Request, exc: StarletteHTTPException):
"""Обработчик Starlette HTTP исключений (включая ошибки безопасности)"""
# Для API маршрутов возвращаем JSON ответ
if request.url.path.startswith("/api/"):
if exc.status_code == 401:
return JSONResponse(
status_code=401,
content={
"error": "unauthorized",
"message": "Требуется авторизация",
"details": "Для доступа к этому API необходимо войти в систему."
},
headers={"WWW-Authenticate": "Bearer"}
)
elif exc.status_code == 403:
return JSONResponse(
status_code=403,
content={
"error": "forbidden",
"message": "Доступ запрещен",
"details": "У вас нет прав для доступа к этому API."
}
)
else:
return JSONResponse(
status_code=exc.status_code,
content={
"error": f"http_{exc.status_code}",
"message": exc.detail or "Произошла ошибка при обработке запроса.",
"details": f"URL: {request.url.path}"
}
)
# Для обычных страниц возвращаем HTML
if exc.status_code == 401:
title = "Требуется авторизация"
message = "Для доступа к этой странице необходимо войти в систему."
elif exc.status_code == 403:
title = "Доступ запрещен"
message = "У вас нет прав для доступа к этой странице."
elif exc.status_code == 404:
title = "Страница не найдена"
message = "Запрашиваемая страница не существует или была перемещена."
elif exc.status_code == 500:
title = "Внутренняя ошибка сервера"
message = "Произошла непредвиденная ошибка. Попробуйте обновить страницу или обратитесь к администратору."
else:
title = f"Ошибка {exc.status_code}"
message = exc.detail or "Произошла ошибка при обработке запроса."
return templates.TemplateResponse("error.html", {
"request": request,
"error_code": exc.status_code,
"error_title": title,
"error_message": message
}, status_code=exc.status_code)
@app.exception_handler(Exception)
async def general_exception_handler(request: Request, exc: Exception):
"""Общий обработчик всех исключений"""
import traceback
# Логируем ошибку
print(f"❌ Необработанная ошибка: {exc}")
print(f"❌ URL: {request.url.path}")
print(f"❌ Traceback: {traceback.format_exc()}")
return templates.TemplateResponse("error.html", {
"request": request,
"error_code": 500,
"error_title": "Внутренняя ошибка сервера",
"error_message": "Произошла непредвиденная ошибка. Попробуйте обновить страницу или обратитесь к администратору."
}, status_code=500)
# Инициализация шаблонов
templates = Jinja2Templates(directory="templates")
@@ -119,7 +297,7 @@ async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(s
if username is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Недействительный токен",
detail="Недействительный токен. Требуется авторизация.",
headers={"WWW-Authenticate": "Bearer"},
)
return username
@@ -410,7 +588,7 @@ async def login(user_data: UserLogin, response: Response):
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Неверное имя пользователя или пароль",
detail="Неверное имя пользователя или пароль. Проверьте учетные данные и попробуйте снова.",
headers={"WWW-Authenticate": "Bearer"},
)
@@ -430,6 +608,32 @@ def healthz():
"""Health check endpoint"""
return "ok"
# Маршруты для тестирования страниц ошибок (только в режиме разработки)
@app.get("/test/error/404")
async def test_404_error():
"""Тест страницы ошибки 404"""
raise HTTPException(status_code=404, detail="Тестовая ошибка 404")
@app.get("/test/error/401")
async def test_401_error():
"""Тест страницы ошибки 401"""
raise HTTPException(status_code=401, detail="Тестовая ошибка 401")
@app.get("/test/error/403")
async def test_403_error():
"""Тест страницы ошибки 403"""
raise HTTPException(status_code=403, detail="Тестовая ошибка 403")
@app.get("/test/error/500")
async def test_500_error():
"""Тест страницы ошибки 500"""
raise HTTPException(status_code=500, detail="Тестовая ошибка 500")
@app.get("/test/error/general")
async def test_general_error():
"""Тест общей ошибки"""
raise Exception("Тестовая общая ошибка")
@app.get("/api/logs/stats/{container_id}")
def api_logs_stats(container_id: str, current_user: str = Depends(get_current_user)):
"""Получить статистику логов контейнера"""
@@ -506,7 +710,10 @@ def api_update_excluded_containers(
}
)
else:
raise HTTPException(status_code=500, detail="Ошибка сохранения списка")
raise HTTPException(
status_code=500,
detail="Ошибка сохранения списка исключенных контейнеров. Попробуйте еще раз или обратитесь к администратору."
)
@app.get("/api/projects")
def api_projects(current_user: str = Depends(get_current_user)):