system_logs.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. import os
  2. import glob
  3. import time
  4. import asyncio
  5. from typing import List, Optional
  6. from datetime import datetime, date
  7. from fastapi import APIRouter, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect
  8. from fastapi.responses import FileResponse, StreamingResponse
  9. from sqlalchemy.orm import Session
  10. from app.api.v1 import deps
  11. from app.models.user import User, UserRole
  12. from app.core.config import settings
  13. router = APIRouter()
  14. LOG_DIR = "/app/logs"
  15. CURRENT_LOG_FILE = "backend.log"
  16. def get_log_filename(target_date: Optional[date] = None) -> str:
  17. """Get filename based on date."""
  18. if not target_date or target_date == date.today():
  19. return os.path.join(LOG_DIR, CURRENT_LOG_FILE)
  20. # Format: backend.log.YYYY-MM-DD
  21. filename = f"{CURRENT_LOG_FILE}.{target_date.strftime('%Y-%m-%d')}"
  22. return os.path.join(LOG_DIR, filename)
  23. @router.get("/files", summary="获取日志文件列表")
  24. def list_log_files(
  25. current_user: User = Depends(deps.get_current_active_user),
  26. ):
  27. """
  28. 列出所有可用的日志文件。
  29. """
  30. if current_user.role != UserRole.SUPER_ADMIN:
  31. raise HTTPException(status_code=403, detail="权限不足")
  32. if not os.path.exists(LOG_DIR):
  33. return []
  34. # List all files matching backend.log*
  35. files = []
  36. # Current log
  37. if os.path.exists(os.path.join(LOG_DIR, CURRENT_LOG_FILE)):
  38. stat = os.stat(os.path.join(LOG_DIR, CURRENT_LOG_FILE))
  39. files.append({
  40. "name": CURRENT_LOG_FILE,
  41. "date": date.today().isoformat(),
  42. "size": stat.st_size,
  43. "mtime": datetime.fromtimestamp(stat.st_mtime)
  44. })
  45. # Rotated logs
  46. pattern = os.path.join(LOG_DIR, f"{CURRENT_LOG_FILE}.*")
  47. for file_path in glob.glob(pattern):
  48. name = os.path.basename(file_path)
  49. # Parse date from name backend.log.2023-10-10
  50. try:
  51. date_str = name.split(".")[-1]
  52. # Verify date format
  53. datetime.strptime(date_str, "%Y-%m-%d")
  54. stat = os.stat(file_path)
  55. files.append({
  56. "name": name,
  57. "date": date_str,
  58. "size": stat.st_size,
  59. "mtime": datetime.fromtimestamp(stat.st_mtime)
  60. })
  61. except ValueError:
  62. continue
  63. # Sort by date desc
  64. files.sort(key=lambda x: x["date"], reverse=True)
  65. return files
  66. @router.get("/download/{filename}", summary="下载日志文件")
  67. def download_log(
  68. filename: str,
  69. current_user: User = Depends(deps.get_current_active_user),
  70. ):
  71. if current_user.role != UserRole.SUPER_ADMIN:
  72. raise HTTPException(status_code=403, detail="权限不足")
  73. file_path = os.path.join(LOG_DIR, filename)
  74. # Security check: prevent directory traversal
  75. if os.path.dirname(os.path.abspath(file_path)) != os.path.abspath(LOG_DIR):
  76. raise HTTPException(status_code=400, detail="Invalid filename")
  77. if not os.path.exists(file_path):
  78. raise HTTPException(status_code=404, detail="File not found")
  79. def iterfile():
  80. with open(file_path, mode="rb") as file_like:
  81. while chunk := file_like.read(1024 * 64):
  82. yield chunk
  83. return StreamingResponse(
  84. iterfile(),
  85. media_type="text/plain",
  86. headers={"Content-Disposition": f'attachment; filename="{filename}"'}
  87. )
  88. @router.get("/search", summary="搜索日志内容")
  89. def search_logs(
  90. date_str: Optional[str] = Query(None, alias="date", description="YYYY-MM-DD"),
  91. keyword: Optional[str] = None,
  92. lines: int = 1000,
  93. current_user: User = Depends(deps.get_current_active_user),
  94. ):
  95. if current_user.role != UserRole.SUPER_ADMIN:
  96. raise HTTPException(status_code=403, detail="权限不足")
  97. target_date = None
  98. if date_str:
  99. try:
  100. target_date = datetime.strptime(date_str, "%Y-%m-%d").date()
  101. except ValueError:
  102. raise HTTPException(status_code=400, detail="Invalid date format")
  103. file_path = get_log_filename(target_date)
  104. if not os.path.exists(file_path):
  105. return {"lines": [], "error": "Log file not found for this date"}
  106. result_lines = []
  107. try:
  108. # Read file. For large files, this is not optimal, but logs are rotated daily.
  109. # We prefer reading from end.
  110. with open(file_path, "r", encoding="utf-8", errors="replace") as f:
  111. # Simple approach: read all lines, filter, take last N
  112. # For better performance on huge files, we should use a reverse line reader
  113. all_lines = f.readlines()
  114. # Filter
  115. if keyword:
  116. filtered = [l for l in all_lines if keyword.lower() in l.lower()]
  117. else:
  118. filtered = all_lines
  119. # Take last 'lines'
  120. result_lines = filtered[-lines:]
  121. # We might want them in reverse order (newest first) for UI
  122. result_lines.reverse()
  123. except Exception as e:
  124. return {"lines": [], "error": str(e)}
  125. return {"lines": [l.strip() for l in result_lines]}
  126. @router.websocket("/stream")
  127. async def websocket_log_stream(
  128. websocket: WebSocket,
  129. token: str = Query(...)
  130. ):
  131. # Validate token manually since WebSocket doesn't support Depends easily in all cases,
  132. # but FastAPI does support Depends in WebSocket. Let's try explicit verify.
  133. try:
  134. payload = deps.jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
  135. user_id = payload.get("sub")
  136. if user_id is None:
  137. await websocket.close(code=1008)
  138. return
  139. # We could check role here but need DB access.
  140. # For simplicity, assuming valid token is enough for connection,
  141. # but strictly should check role.
  142. except Exception:
  143. await websocket.close(code=1008)
  144. return
  145. await websocket.accept()
  146. file_path = os.path.join(LOG_DIR, CURRENT_LOG_FILE)
  147. if not os.path.exists(file_path):
  148. # Create if not exists to avoid error
  149. open(file_path, 'a').close()
  150. try:
  151. with open(file_path, "r", encoding="utf-8", errors="replace") as f:
  152. # Go to end
  153. f.seek(0, 2)
  154. while True:
  155. line = f.readline()
  156. if line:
  157. await websocket.send_text(line)
  158. else:
  159. await asyncio.sleep(0.5)
  160. except WebSocketDisconnect:
  161. print("Client disconnected")
  162. except Exception as e:
  163. print(f"WS Error: {e}")
  164. await websocket.close()