bigwolfe commited on
Commit
8339370
·
1 Parent(s): 018ee5d

Add system logs endpoint and UI components

Browse files
backend/src/api/main.py CHANGED
@@ -21,7 +21,7 @@ from starlette.responses import Response
21
  from fastmcp.server.http import StreamableHTTPSessionManager, set_http_request
22
  from fastapi.responses import FileResponse
23
 
24
- from .routes import auth, index, notes, search, graph, demo
25
  from ..mcp.server import mcp
26
  from ..services.seed import init_and_seed
27
  from ..services.config import get_config
@@ -111,6 +111,7 @@ app.include_router(search.router, tags=["search"])
111
  app.include_router(index.router, tags=["index"])
112
  app.include_router(graph.router, tags=["graph"])
113
  app.include_router(demo.router, tags=["demo"])
 
114
 
115
 
116
  @app.api_route("/mcp", methods=["GET", "POST", "DELETE"])
 
21
  from fastmcp.server.http import StreamableHTTPSessionManager, set_http_request
22
  from fastapi.responses import FileResponse
23
 
24
+ from .routes import auth, index, notes, search, graph, demo, system
25
  from ..mcp.server import mcp
26
  from ..services.seed import init_and_seed
27
  from ..services.config import get_config
 
111
  app.include_router(index.router, tags=["index"])
112
  app.include_router(graph.router, tags=["graph"])
113
  app.include_router(demo.router, tags=["demo"])
114
+ app.include_router(system.router, tags=["system"])
115
 
116
 
117
  @app.api_route("/mcp", methods=["GET", "POST", "DELETE"])
backend/src/api/routes/system.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """System routes for logs and diagnostics."""
2
+
3
+ import logging
4
+ from collections import deque
5
+ from typing import List, Dict, Any
6
+ from datetime import datetime
7
+
8
+ from fastapi import APIRouter, Depends
9
+ from pydantic import BaseModel
10
+
11
+ from ..middleware import AuthContext, get_auth_context
12
+
13
+ router = APIRouter()
14
+
15
+ # Global in-memory log buffer
16
+ LOG_BUFFER: deque = deque(maxlen=100)
17
+
18
+ class LogEntry(BaseModel):
19
+ timestamp: str
20
+ level: str
21
+ message: str
22
+ extra: Dict[str, Any]
23
+
24
+ class MemoryLogHandler(logging.Handler):
25
+ """Custom handler to capture logs into memory."""
26
+ def emit(self, record):
27
+ try:
28
+ msg = self.format(record)
29
+ extra = {k: v for k, v in record.__dict__.items()
30
+ if k not in {'args', 'asctime', 'created', 'exc_info', 'exc_text', 'filename',
31
+ 'funcName', 'levelname', 'levelno', 'lineno', 'module',
32
+ 'msecs', 'message', 'msg', 'name', 'pathname', 'process',
33
+ 'processName', 'relativeCreated', 'stack_info', 'thread', 'threadName'}}
34
+
35
+ entry = {
36
+ "timestamp": datetime.fromtimestamp(record.created).isoformat(),
37
+ "level": record.levelname,
38
+ "message": msg,
39
+ "extra": extra
40
+ }
41
+ LOG_BUFFER.append(entry)
42
+ except Exception:
43
+ self.handleError(record)
44
+
45
+ # Attach handler to root logger or specific loggers
46
+ memory_handler = MemoryLogHandler()
47
+ formatter = logging.Formatter('%(message)s')
48
+ memory_handler.setFormatter(formatter)
49
+ logging.getLogger("backend.src.mcp.server").addHandler(memory_handler)
50
+ logging.getLogger("backend.src.services").addHandler(memory_handler)
51
+ # Catch uvicorn/fastapi logs too if desired
52
+ # logging.getLogger("uvicorn.access").addHandler(memory_handler)
53
+
54
+ @router.get("/api/system/logs", response_model=List[LogEntry])
55
+ async def get_logs(auth: AuthContext = Depends(get_auth_context)):
56
+ """Retrieve recent system logs."""
57
+ return list(LOG_BUFFER)
frontend/src/components/SystemLogs.tsx ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
3
+ import { ScrollArea } from '@/components/ui/scroll-area';
4
+ import { Badge } from '@/components/ui/badge';
5
+ import { getLogs, type LogEntry } from '@/services/api';
6
+ import { Loader2, RefreshCw } from 'lucide-react';
7
+ import { Button } from '@/components/ui/button';
8
+
9
+ export function SystemLogs() {
10
+ const [logs, setLogs] = useState<LogEntry[]>([]);
11
+ const [isLoading, setIsLoading] = useState(false);
12
+ const [error, setError] = useState<string | null>(null);
13
+
14
+ const fetchLogs = async () => {
15
+ setIsLoading(true);
16
+ setError(null);
17
+ try {
18
+ const data = await getLogs();
19
+ // Sort logs newest first if backend returns oldest first
20
+ setLogs(data.reverse());
21
+ } catch (err) {
22
+ console.error('Failed to fetch logs:', err);
23
+ setError('Failed to load logs.');
24
+ } finally {
25
+ setIsLoading(false);
26
+ }
27
+ };
28
+
29
+ useEffect(() => {
30
+ fetchLogs();
31
+ // Optional: Auto-refresh every 5 seconds?
32
+ // const interval = setInterval(fetchLogs, 5000);
33
+ // return () => clearInterval(interval);
34
+ }, []);
35
+
36
+ const getLevelVariant = (level: string) => {
37
+ switch (level.toUpperCase()) {
38
+ case 'ERROR': return 'destructive';
39
+ case 'WARNING': return 'secondary'; // yellow-ish usually
40
+ case 'INFO': return 'default';
41
+ case 'DEBUG': return 'outline';
42
+ default: return 'secondary';
43
+ }
44
+ };
45
+
46
+ return (
47
+ <Card className="h-full flex flex-col">
48
+ <CardHeader className="pb-3">
49
+ <div className="flex items-center justify-between">
50
+ <div>
51
+ <CardTitle>System Logs</CardTitle>
52
+ <CardDescription>Backend operational logs (last 100 entries)</CardDescription>
53
+ </div>
54
+ <Button variant="ghost" size="sm" onClick={fetchLogs} disabled={isLoading}>
55
+ <RefreshCw className={`h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
56
+ </Button>
57
+ </div>
58
+ </CardHeader>
59
+ <CardContent className="flex-1 overflow-hidden p-0">
60
+ <ScrollArea className="h-[400px] w-full p-4 pt-0">
61
+ {error && <div className="text-destructive text-sm p-2">{error}</div>}
62
+ {logs.length === 0 && !isLoading && !error && (
63
+ <div className="text-muted-foreground text-sm p-2">No logs available.</div>
64
+ )}
65
+ <div className="space-y-2">
66
+ {logs.map((log, i) => (
67
+ <div key={i} className="text-xs border-b border-border pb-2 last:border-0">
68
+ <div className="flex items-center gap-2 mb-1">
69
+ <span className="text-muted-foreground font-mono">{new Date(log.timestamp).toLocaleTimeString()}</span>
70
+ <Badge variant={getLevelVariant(log.level)} className="h-5 px-1.5 text-[10px]">
71
+ {log.level}
72
+ </Badge>
73
+ </div>
74
+ <div className="font-mono break-all whitespace-pre-wrap text-foreground/90">
75
+ {log.message}
76
+ </div>
77
+ {log.extra && Object.keys(log.extra).length > 0 && (
78
+ <div className="mt-1 pl-2 border-l-2 border-muted">
79
+ <pre className="text-[10px] text-muted-foreground">
80
+ {JSON.stringify(log.extra, null, 2)}
81
+ </pre>
82
+ </div>
83
+ )}
84
+ </div>
85
+ ))}
86
+ </div>
87
+ </ScrollArea>
88
+ </CardContent>
89
+ </Card>
90
+ );
91
+ }
frontend/src/pages/Settings.tsx CHANGED
@@ -15,6 +15,7 @@ import { getCurrentUser, getToken, logout, getStoredToken, isDemoSession, AUTH_T
15
  import { getIndexHealth, rebuildIndex, type RebuildResponse } from '@/services/api';
16
  import type { User } from '@/types/user';
17
  import type { IndexHealth } from '@/types/search';
 
18
 
19
  export function Settings() {
20
  const navigate = useNavigate();
@@ -326,6 +327,9 @@ export function Settings() {
326
  description="Full-text search index status and maintenance"
327
  />
328
  )}
 
 
 
329
  </div>
330
  </div>
331
  );
 
15
  import { getIndexHealth, rebuildIndex, type RebuildResponse } from '@/services/api';
16
  import type { User } from '@/types/user';
17
  import type { IndexHealth } from '@/types/search';
18
+ import { SystemLogs } from '@/components/SystemLogs';
19
 
20
  export function Settings() {
21
  const navigate = useNavigate();
 
327
  description="Full-text search index status and maintenance"
328
  />
329
  )}
330
+
331
+ {/* System Logs */}
332
+ <SystemLogs />
333
  </div>
334
  </div>
335
  );
frontend/src/services/api.ts CHANGED
@@ -207,6 +207,20 @@ export async function rebuildIndex(): Promise<RebuildResponse> {
207
  });
208
  }
209
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  /**
211
  * Move or rename a note to a new path
212
  */
 
207
  });
208
  }
209
 
210
+ /**
211
+ * Retrieve system logs
212
+ */
213
+ export interface LogEntry {
214
+ timestamp: string;
215
+ level: string;
216
+ message: string;
217
+ extra: Record<string, any>;
218
+ }
219
+
220
+ export async function getLogs(): Promise<LogEntry[]> {
221
+ return apiFetch<LogEntry[]>('/api/system/logs');
222
+ }
223
+
224
  /**
225
  * Move or rename a note to a new path
226
  */