feat: Add real-time log streaming with a new backend SSE endpoint and a corresponding frontend component.

This commit is contained in:
2026-01-31 03:34:55 +08:00
parent a3491ab279
commit 05580bd025
2 changed files with 35 additions and 9 deletions

View File

@@ -273,7 +273,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) {
} }
func handleGetLogs(w http.ResponseWriter, r *http.Request) { func handleGetLogs(w http.ResponseWriter, r *http.Request) {
logs := logger.GetLogs(100) logs := logger.GetLogs(200)
api.Success(w, logs) api.Success(w, logs)
} }

View File

@@ -1,13 +1,15 @@
import { useState, useEffect, useRef } from 'react' import { useState, useEffect, useRef, useCallback } from 'react'
import { Terminal, Trash2, Play, Pause } from 'lucide-react' import { Terminal, Trash2, Play, Pause } from 'lucide-react'
import { Card, Button } from '../common' import { Card, Button } from '../common'
interface LogEntry { interface LogEntry {
type?: string
timestamp: string timestamp: string
level: string level: string
message: string message: string
email?: string email?: string
step?: string step?: string
module?: string
} }
const levelColors: Record<string, string> = { const levelColors: Record<string, string> = {
@@ -44,9 +46,33 @@ export default function LogStream({ hideHeader = false, className = '' }: LogStr
const [paused, setPaused] = useState(false) const [paused, setPaused] = useState(false)
const logContainerRef = useRef<HTMLDivElement>(null) const logContainerRef = useRef<HTMLDivElement>(null)
const eventSourceRef = useRef<EventSource | null>(null) const eventSourceRef = useRef<EventSource | null>(null)
const isPausedRef = useRef(paused)
isPausedRef.current = paused
// 加载历史日志最新200条
const loadHistoryLogs = useCallback(async () => {
try {
const res = await fetch('/api/logs')
const data = await res.json()
if (data.code === 0 && Array.isArray(data.data)) {
const historyLogs: LogEntry[] = data.data.map((log: LogEntry) => ({
type: 'log',
level: log.level || 'info',
message: log.message || '',
timestamp: log.timestamp || '',
email: log.email,
module: log.module || 'system',
}))
setLogs(historyLogs)
}
} catch (e) {
console.error('Failed to load history logs:', e)
}
}, [])
// 组件挂载时先加载历史日志,再连接 SSE
useEffect(() => { useEffect(() => {
if (paused) return loadHistoryLogs()
const eventSource = new EventSource('/api/logs/stream') const eventSource = new EventSource('/api/logs/stream')
eventSourceRef.current = eventSource eventSourceRef.current = eventSource
@@ -58,7 +84,11 @@ export default function LogStream({ hideHeader = false, className = '' }: LogStr
eventSource.onmessage = (event) => { eventSource.onmessage = (event) => {
try { try {
const log = JSON.parse(event.data) as LogEntry const log = JSON.parse(event.data) as LogEntry
setLogs((prev) => [...prev.slice(-499), log]) // 增加缓存行数 if (log.type === 'connected') return
if (!isPausedRef.current) {
setLogs((prev) => [...prev.slice(-499), log])
}
} catch (e) { } catch (e) {
console.error('Failed to parse log:', e) console.error('Failed to parse log:', e)
} }
@@ -66,13 +96,12 @@ export default function LogStream({ hideHeader = false, className = '' }: LogStr
eventSource.onerror = () => { eventSource.onerror = () => {
setConnected(false) setConnected(false)
eventSource.close()
} }
return () => { return () => {
eventSource.close() eventSource.close()
} }
}, [paused]) }, [loadHistoryLogs])
useEffect(() => { useEffect(() => {
if (logContainerRef.current) { if (logContainerRef.current) {
@@ -90,9 +119,6 @@ export default function LogStream({ hideHeader = false, className = '' }: LogStr
} }
const togglePause = () => { const togglePause = () => {
if (!paused && eventSourceRef.current) {
eventSourceRef.current.close()
}
setPaused(!paused) setPaused(!paused)
} }