| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197 |
- import os
- import glob
- import time
- import asyncio
- from typing import List, Optional
- from datetime import datetime, date
- from fastapi import APIRouter, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect
- from fastapi.responses import FileResponse, StreamingResponse
- from sqlalchemy.orm import Session
- from app.api.v1 import deps
- from app.models.user import User, UserRole
- from app.core.config import settings
- router = APIRouter()
- LOG_DIR = "/app/logs"
- CURRENT_LOG_FILE = "backend.log"
- def get_log_filename(target_date: Optional[date] = None) -> str:
- """Get filename based on date."""
- if not target_date or target_date == date.today():
- return os.path.join(LOG_DIR, CURRENT_LOG_FILE)
-
- # Format: backend.log.YYYY-MM-DD
- filename = f"{CURRENT_LOG_FILE}.{target_date.strftime('%Y-%m-%d')}"
- return os.path.join(LOG_DIR, filename)
- @router.get("/files", summary="获取日志文件列表")
- def list_log_files(
- current_user: User = Depends(deps.get_current_active_user),
- ):
- """
- 列出所有可用的日志文件。
- """
- if current_user.role != UserRole.SUPER_ADMIN:
- raise HTTPException(status_code=403, detail="权限不足")
- if not os.path.exists(LOG_DIR):
- return []
- # List all files matching backend.log*
- files = []
- # Current log
- if os.path.exists(os.path.join(LOG_DIR, CURRENT_LOG_FILE)):
- stat = os.stat(os.path.join(LOG_DIR, CURRENT_LOG_FILE))
- files.append({
- "name": CURRENT_LOG_FILE,
- "date": date.today().isoformat(),
- "size": stat.st_size,
- "mtime": datetime.fromtimestamp(stat.st_mtime)
- })
- # Rotated logs
- pattern = os.path.join(LOG_DIR, f"{CURRENT_LOG_FILE}.*")
- for file_path in glob.glob(pattern):
- name = os.path.basename(file_path)
- # Parse date from name backend.log.2023-10-10
- try:
- date_str = name.split(".")[-1]
- # Verify date format
- datetime.strptime(date_str, "%Y-%m-%d")
-
- stat = os.stat(file_path)
- files.append({
- "name": name,
- "date": date_str,
- "size": stat.st_size,
- "mtime": datetime.fromtimestamp(stat.st_mtime)
- })
- except ValueError:
- continue
-
- # Sort by date desc
- files.sort(key=lambda x: x["date"], reverse=True)
- return files
- @router.get("/download/{filename}", summary="下载日志文件")
- def download_log(
- filename: str,
- current_user: User = Depends(deps.get_current_active_user),
- ):
- if current_user.role != UserRole.SUPER_ADMIN:
- raise HTTPException(status_code=403, detail="权限不足")
-
- file_path = os.path.join(LOG_DIR, filename)
- # Security check: prevent directory traversal
- if os.path.dirname(os.path.abspath(file_path)) != os.path.abspath(LOG_DIR):
- raise HTTPException(status_code=400, detail="Invalid filename")
-
- if not os.path.exists(file_path):
- raise HTTPException(status_code=404, detail="File not found")
-
- def iterfile():
- with open(file_path, mode="rb") as file_like:
- while chunk := file_like.read(1024 * 64):
- yield chunk
- return StreamingResponse(
- iterfile(),
- media_type="text/plain",
- headers={"Content-Disposition": f'attachment; filename="{filename}"'}
- )
- @router.get("/search", summary="搜索日志内容")
- def search_logs(
- date_str: Optional[str] = Query(None, alias="date", description="YYYY-MM-DD"),
- keyword: Optional[str] = None,
- lines: int = 1000,
- current_user: User = Depends(deps.get_current_active_user),
- ):
- if current_user.role != UserRole.SUPER_ADMIN:
- raise HTTPException(status_code=403, detail="权限不足")
- target_date = None
- if date_str:
- try:
- target_date = datetime.strptime(date_str, "%Y-%m-%d").date()
- except ValueError:
- raise HTTPException(status_code=400, detail="Invalid date format")
-
- file_path = get_log_filename(target_date)
- if not os.path.exists(file_path):
- return {"lines": [], "error": "Log file not found for this date"}
- result_lines = []
-
- try:
- # Read file. For large files, this is not optimal, but logs are rotated daily.
- # We prefer reading from end.
- with open(file_path, "r", encoding="utf-8", errors="replace") as f:
- # Simple approach: read all lines, filter, take last N
- # For better performance on huge files, we should use a reverse line reader
- all_lines = f.readlines()
-
- # Filter
- if keyword:
- filtered = [l for l in all_lines if keyword.lower() in l.lower()]
- else:
- filtered = all_lines
-
- # Take last 'lines'
- result_lines = filtered[-lines:]
-
- # We might want them in reverse order (newest first) for UI
- result_lines.reverse()
-
- except Exception as e:
- return {"lines": [], "error": str(e)}
- return {"lines": [l.strip() for l in result_lines]}
- @router.websocket("/stream")
- async def websocket_log_stream(
- websocket: WebSocket,
- token: str = Query(...)
- ):
- # Validate token manually since WebSocket doesn't support Depends easily in all cases,
- # but FastAPI does support Depends in WebSocket. Let's try explicit verify.
- try:
- payload = deps.jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
- user_id = payload.get("sub")
- if user_id is None:
- await websocket.close(code=1008)
- return
- # We could check role here but need DB access.
- # For simplicity, assuming valid token is enough for connection,
- # but strictly should check role.
- except Exception:
- await websocket.close(code=1008)
- return
- await websocket.accept()
-
- file_path = os.path.join(LOG_DIR, CURRENT_LOG_FILE)
- if not os.path.exists(file_path):
- # Create if not exists to avoid error
- open(file_path, 'a').close()
- try:
- with open(file_path, "r", encoding="utf-8", errors="replace") as f:
- # Go to end
- f.seek(0, 2)
-
- while True:
- line = f.readline()
- if line:
- await websocket.send_text(line)
- else:
- await asyncio.sleep(0.5)
- except WebSocketDisconnect:
- print("Client disconnected")
- except Exception as e:
- print(f"WS Error: {e}")
- await websocket.close()
|