auto_trade_sys/frontend/src/components/LogMonitor.jsx
薇薇安 59b8e7b44f a
2026-01-18 21:37:11 +08:00

343 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useMemo, useState } from 'react'
import { api } from '../services/api'
import './LogMonitor.css'
const GROUPS = [
{ key: 'error', label: '错误' },
{ key: 'warning', label: '警告' },
{ key: 'info', label: '信息' },
]
const LEVELS = ['', 'ERROR', 'CRITICAL', 'WARNING', 'INFO']
const SERVICES = ['', 'backend', 'trading_system']
function formatCount(item) {
const c = Number(item?.count || 1)
return c > 1 ? `×${c}` : ''
}
export default function LogMonitor() {
const [items, setItems] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [group, setGroup] = useState('error')
const [overview, setOverview] = useState(null)
const [saving, setSaving] = useState(false)
const [pageStart, setPageStart] = useState(0)
const [pageMeta, setPageMeta] = useState({ key: '', llen_total: 0, has_more: false, next_start: 0 })
const [limit, setLimit] = useState(200)
const [service, setService] = useState('')
const [level, setLevel] = useState('')
const [autoRefresh, setAutoRefresh] = useState(true)
const [refreshSec, setRefreshSec] = useState(5)
const params = useMemo(() => {
const p = { limit: String(limit), group, start: String(pageStart) }
if (service) p.service = service
if (level) p.level = level
return p
}, [limit, service, level, group, pageStart])
const loadOverview = async () => {
try {
const res = await api.getLogsOverview()
setOverview(res)
} catch (e) {
// 概览失败不阻塞日志列表
}
}
const load = async () => {
setLoading(true)
setError('')
try {
const res = await api.getSystemLogs(params)
setItems(res?.items || [])
setPageMeta({
key: res?.key || '',
llen_total: Number(res?.llen_total || 0),
has_more: !!res?.has_more,
next_start: Number(res?.next_start || 0),
})
await loadOverview()
} catch (e) {
setError(e?.message || '获取日志失败')
} finally {
setLoading(false)
}
}
useEffect(() => {
load()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [params])
useEffect(() => {
if (!autoRefresh) return
const sec = Number(refreshSec)
if (!sec || sec <= 0) return
const t = setInterval(() => load(), sec * 1000)
return () => clearInterval(t)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [autoRefresh, refreshSec, params])
const maxLen = overview?.config?.max_len || {}
const enabled = overview?.config?.enabled || {}
const llen = overview?.stats?.llen || {}
const addedToday = overview?.stats?.added_today || {}
const day = overview?.stats?.day || ''
const [maxLenDraft, setMaxLenDraft] = useState({ error: 2000, warning: 2000, info: 2000 })
useEffect(() => {
if (maxLen?.error || maxLen?.warning || maxLen?.info) {
setMaxLenDraft({
error: Number(maxLen.error || 2000),
warning: Number(maxLen.warning || 2000),
info: Number(maxLen.info || 2000),
})
}
}, [maxLen?.error, maxLen?.warning, maxLen?.info])
const saveConfig = async () => {
setSaving(true)
setError('')
try {
await api.updateLogsConfig({ max_len: maxLenDraft })
await loadOverview()
} catch (e) {
setError(e?.message || '更新日志配置失败')
} finally {
setSaving(false)
}
}
const writeTest = async () => {
setError('')
try {
await api.writeLogsTest()
await load()
} catch (e) {
setError(e?.message || '写入测试日志失败')
}
}
const goFirstPage = () => setPageStart(0)
const goNextPage = () => setPageStart(pageMeta?.next_start || 0)
const goPrevPage = () => setPageStart(Math.max(0, pageStart - Number(limit || 200)))
return (
<div className="log-monitor">
<div className="log-header">
<div>
<h2>日志监控</h2>
<div className="log-subtitle">
来源Redis List分组存储 + 只保留最近 N + 连续同类合并计数
</div>
</div>
<div className="log-actions">
<button className="btn" onClick={load} disabled={loading}>
刷新
</button>
<button className="btn" onClick={writeTest} disabled={loading}>
写入测试日志
</button>
</div>
</div>
<div className="log-overview">
<div className="overview-row">
<div className="overview-title">今日统计 {day ? `(${day})` : ''}</div>
<div className="overview-items">
<span>error: {addedToday.error || 0} / {llen.error || 0}{enabled.error === false ? '(已停用)' : ''}</span>
<span>warning: {addedToday.warning || 0} / {llen.warning || 0}{enabled.warning === false ? '(已停用)' : ''}</span>
<span>info: {addedToday.info || 0} / {llen.info || 0}{enabled.info === false ? '(已停用)' : ''}</span>
</div>
</div>
<div className="overview-row">
<div className="overview-title">最大条数每类</div>
<div className="overview-config">
<div className="mini">
<label>error</label>
<input
type="number"
min="100"
max="20000"
value={maxLenDraft.error}
onChange={(e) => setMaxLenDraft((s) => ({ ...s, error: Number(e.target.value || 2000) }))}
/>
</div>
<div className="mini">
<label>warning</label>
<input
type="number"
min="100"
max="20000"
value={maxLenDraft.warning}
onChange={(e) => setMaxLenDraft((s) => ({ ...s, warning: Number(e.target.value || 2000) }))}
/>
</div>
<div className="mini">
<label>info</label>
<input
type="number"
min="100"
max="20000"
value={maxLenDraft.info}
onChange={(e) => setMaxLenDraft((s) => ({ ...s, info: Number(e.target.value || 2000) }))}
/>
</div>
<button className="btn" onClick={saveConfig} disabled={saving}>
{saving ? '保存中...' : '保存配置'}
</button>
</div>
</div>
</div>
<div className="log-controls">
<div className="control">
<label>分组</label>
<select
value={group}
onChange={(e) => {
setGroup(e.target.value)
setPageStart(0)
}}
>
{GROUPS.map((g) => (
<option key={g.key} value={g.key}>
{g.label}
</option>
))}
</select>
</div>
<div className="control">
<label>条数</label>
<input
type="number"
min="1"
max="20000"
value={limit}
onChange={(e) => {
setLimit(Number(e.target.value || 200))
setPageStart(0)
}}
/>
</div>
<div className="control">
<label>服务</label>
<select value={service} onChange={(e) => setService(e.target.value)}>
{SERVICES.map((s) => (
<option key={s} value={s}>
{s || '全部'}
</option>
))}
</select>
</div>
<div className="control">
<label>级别</label>
<select value={level} onChange={(e) => setLevel(e.target.value)}>
{LEVELS.map((l) => (
<option key={l} value={l}>
{l || '全部'}
</option>
))}
</select>
</div>
<div className="control inline">
<label>
<input
type="checkbox"
checked={autoRefresh}
onChange={(e) => setAutoRefresh(e.target.checked)}
/>
自动刷新
</label>
</div>
<div className="control">
<label>间隔()</label>
<input
type="number"
min="1"
max="60"
value={refreshSec}
onChange={(e) => setRefreshSec(Number(e.target.value || 5))}
disabled={!autoRefresh}
/>
</div>
</div>
{error ? <div className="log-error">{error}</div> : null}
<div className="log-paging">
<div className="paging-left">
<div className="paging-meta">
当前 key<code>{pageMeta.key || '-'}</code>
</div>
<div className="paging-meta">
显示范围
{pageMeta.llen_total
? `${pageStart + 1}-${pageStart + (items?.length || 0)} / ${pageMeta.llen_total}`
: '0 / 0'}
</div>
</div>
<div className="paging-actions">
<button className="btn" onClick={goFirstPage} disabled={loading || pageStart === 0}>
首页
</button>
<button className="btn" onClick={goPrevPage} disabled={loading || pageStart === 0}>
上一页
</button>
<button className="btn" onClick={goNextPage} disabled={loading || !pageMeta.has_more}>
下一页
</button>
</div>
</div>
<div className="log-table">
<div className="log-row log-head">
<div className="c-time">时间</div>
<div className="c-svc">服务</div>
<div className="c-level">级别</div>
<div className="c-msg">内容</div>
</div>
{items.length === 0 ? (
<div className="log-empty">{loading ? '加载中...' : '暂无日志'}</div>
) : (
items.map((it, idx) => (
<div className="log-row" key={`${it.ts || ''}-${idx}`}>
<div className="c-time">{it.time || ''}</div>
<div className="c-svc">{it.service || ''}</div>
<div className="c-level">
<span className={`pill pill-${String(it.level || '').toLowerCase()}`}>{it.level}</span>
</div>
<div className="c-msg">
<div className="msg-line">
<span className="msg-text">{it.message || ''}</span>
<span className="msg-count">{formatCount(it)}</span>
</div>
{it.logger ? <div className="msg-meta">{it.logger}</div> : null}
{it.exc_text ? (
<details className="msg-details">
<summary>堆栈</summary>
<pre className="stack">{it.exc_text}</pre>
</details>
) : null}
</div>
</div>
))
)}
</div>
</div>
)
}