feat: Add real-time log streaming with a new backend SSE endpoint and a corresponding frontend component.
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user