log admin
This commit is contained in:
parent
3fa5b98f47
commit
e313055de6
3 changed files with 156 additions and 0 deletions
3
app.py
3
app.py
|
|
@ -525,6 +525,9 @@ app.include_router(sharepointRouter)
|
|||
from modules.routes.routeAdminAutomationEvents import router as adminAutomationEventsRouter
|
||||
app.include_router(adminAutomationEventsRouter)
|
||||
|
||||
from modules.routes.routeAdminLogs import router as adminLogsRouter
|
||||
app.include_router(adminLogsRouter)
|
||||
|
||||
from modules.routes.routeAdminRbacRules import router as rbacAdminRulesRouter
|
||||
app.include_router(rbacAdminRulesRouter)
|
||||
|
||||
|
|
|
|||
143
modules/routes/routeAdminLogs.py
Normal file
143
modules/routes/routeAdminLogs.py
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
# Copyright (c) 2025 Patrick Motsch
|
||||
# All rights reserved.
|
||||
"""
|
||||
Admin log viewer routes for the backend API.
|
||||
Sysadmin-only endpoint for viewing gateway application logs.
|
||||
Reads from the daily rotating log files (log_app_YYYYMMDD.log).
|
||||
"""
|
||||
|
||||
import os
|
||||
import glob
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, HTTPException, Depends, Request, Query
|
||||
from fastapi.responses import PlainTextResponse
|
||||
from modules.auth import limiter, requireSysAdminRole
|
||||
from modules.shared.configuration import APP_CONFIG
|
||||
from modules.datamodels.datamodelUam import User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/admin/logs",
|
||||
tags=["Admin Logs"],
|
||||
responses={
|
||||
401: {"description": "Unauthorized"},
|
||||
403: {"description": "Forbidden - Sysadmin only"},
|
||||
500: {"description": "Internal server error"},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _getLogDir() -> str:
|
||||
"""Resolve the configured log directory (same logic as app.py initLogging)."""
|
||||
logDir = APP_CONFIG.get("APP_LOGGING_LOG_DIR", "./")
|
||||
if not os.path.isabs(logDir):
|
||||
gatewayDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
logDir = os.path.join(gatewayDir, logDir)
|
||||
return logDir
|
||||
|
||||
|
||||
def _getLogFiles() -> list[str]:
|
||||
"""Return all log_app_*.log files sorted by name descending (newest first)."""
|
||||
logDir = _getLogDir()
|
||||
pattern = os.path.join(logDir, "log_app_*.log")
|
||||
files = glob.glob(pattern)
|
||||
files.sort(reverse=True)
|
||||
return files
|
||||
|
||||
|
||||
def _readLastNLines(filePath: str, n: int) -> list[str]:
|
||||
"""Read last n lines from a file efficiently."""
|
||||
lines = []
|
||||
try:
|
||||
with open(filePath, "r", encoding="utf-8", errors="replace") as f:
|
||||
lines = f.readlines()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to read log file {filePath}: {e}")
|
||||
return lines[-n:] if len(lines) > n else lines
|
||||
|
||||
|
||||
@router.get("")
|
||||
@limiter.limit("30/minute")
|
||||
def getLogEntries(
|
||||
request: Request,
|
||||
count: int = Query(default=200, ge=1, le=50000, description="Number of log entries to return"),
|
||||
currentUser: User = Depends(requireSysAdminRole),
|
||||
) -> dict:
|
||||
"""
|
||||
Get the last N log entries from the gateway log files.
|
||||
Merges across multiple daily log files if needed.
|
||||
"""
|
||||
try:
|
||||
logFiles = _getLogFiles()
|
||||
|
||||
if not logFiles:
|
||||
return {"lines": [], "totalCollected": 0, "logDir": _getLogDir()}
|
||||
|
||||
collectedLines: list[str] = []
|
||||
remaining = count
|
||||
|
||||
for filePath in logFiles:
|
||||
if remaining <= 0:
|
||||
break
|
||||
|
||||
lines = _readLastNLines(filePath, remaining)
|
||||
collectedLines = lines + collectedLines
|
||||
remaining = count - len(collectedLines)
|
||||
|
||||
finalLines = collectedLines[-count:] if len(collectedLines) > count else collectedLines
|
||||
|
||||
return {
|
||||
"lines": [line.rstrip("\n").rstrip("\r") for line in finalLines],
|
||||
"totalCollected": len(finalLines),
|
||||
"logDir": _getLogDir(),
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to read log files: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to read log files: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/download")
|
||||
@limiter.limit("10/minute")
|
||||
def downloadLog(
|
||||
request: Request,
|
||||
count: int = Query(default=1000, ge=1, le=100000, description="Number of log entries to download"),
|
||||
currentUser: User = Depends(requireSysAdminRole),
|
||||
) -> PlainTextResponse:
|
||||
"""
|
||||
Download the last N log entries as a plain text file.
|
||||
"""
|
||||
try:
|
||||
logFiles = _getLogFiles()
|
||||
|
||||
if not logFiles:
|
||||
return PlainTextResponse("No log files found.", media_type="text/plain")
|
||||
|
||||
collectedLines: list[str] = []
|
||||
remaining = count
|
||||
|
||||
for filePath in logFiles:
|
||||
if remaining <= 0:
|
||||
break
|
||||
|
||||
lines = _readLastNLines(filePath, remaining)
|
||||
collectedLines = lines + collectedLines
|
||||
remaining = count - len(collectedLines)
|
||||
|
||||
finalLines = collectedLines[-count:] if len(collectedLines) > count else collectedLines
|
||||
content = "".join(finalLines)
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
return PlainTextResponse(
|
||||
content=content,
|
||||
media_type="text/plain",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="gateway_log_{timestamp}.log"',
|
||||
},
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to download log files: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to download log files: {str(e)}")
|
||||
|
|
@ -223,6 +223,16 @@ NAVIGATION_SECTIONS = [
|
|||
"adminOnly": True,
|
||||
"sysAdminOnly": True,
|
||||
},
|
||||
{
|
||||
"id": "admin-logs",
|
||||
"objectKey": "ui.admin.logs",
|
||||
"label": {"en": "Logs", "de": "Logs", "fr": "Logs"},
|
||||
"icon": "FaFileAlt",
|
||||
"path": "/admin/logs",
|
||||
"order": 70,
|
||||
"adminOnly": True,
|
||||
"sysAdminOnly": True,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in a new issue