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
|
from modules.routes.routeAdminAutomationEvents import router as adminAutomationEventsRouter
|
||||||
app.include_router(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
|
from modules.routes.routeAdminRbacRules import router as rbacAdminRulesRouter
|
||||||
app.include_router(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,
|
"adminOnly": True,
|
||||||
"sysAdminOnly": 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