a
This commit is contained in:
parent
596b2ec788
commit
79526151f3
|
|
@ -187,6 +187,7 @@ def _get_redis_client_for_logs():
|
||||||
async def get_logs(
|
async def get_logs(
|
||||||
limit: int = 200,
|
limit: int = 200,
|
||||||
group: str = "error",
|
group: str = "error",
|
||||||
|
start: int = 0,
|
||||||
service: Optional[str] = None,
|
service: Optional[str] = None,
|
||||||
level: Optional[str] = None,
|
level: Optional[str] = None,
|
||||||
x_admin_token: Optional[str] = Header(default=None, alias="X-Admin-Token"),
|
x_admin_token: Optional[str] = Header(default=None, alias="X-Admin-Token"),
|
||||||
|
|
@ -204,8 +205,11 @@ async def get_logs(
|
||||||
|
|
||||||
if limit <= 0:
|
if limit <= 0:
|
||||||
limit = 200
|
limit = 200
|
||||||
if limit > 2000:
|
if limit > 20000:
|
||||||
limit = 2000
|
limit = 20000
|
||||||
|
|
||||||
|
if start < 0:
|
||||||
|
start = 0
|
||||||
|
|
||||||
group = (group or "error").strip().lower()
|
group = (group or "error").strip().lower()
|
||||||
if group not in LOG_GROUPS:
|
if group not in LOG_GROUPS:
|
||||||
|
|
@ -218,33 +222,71 @@ async def get_logs(
|
||||||
raise HTTPException(status_code=503, detail="Redis 不可用,无法读取日志")
|
raise HTTPException(status_code=503, detail="Redis 不可用,无法读取日志")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
raw_items = client.lrange(list_key, 0, limit - 1)
|
llen_total = int(client.llen(list_key) or 0)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"读取 Redis 日志失败: {e}")
|
raise HTTPException(status_code=500, detail=f"读取 Redis 日志失败: {e}")
|
||||||
|
|
||||||
|
if llen_total <= 0:
|
||||||
|
return {
|
||||||
|
"group": group,
|
||||||
|
"key": list_key,
|
||||||
|
"start": start,
|
||||||
|
"limit": limit,
|
||||||
|
"llen_total": 0,
|
||||||
|
"next_start": start,
|
||||||
|
"has_more": False,
|
||||||
|
"count": 0,
|
||||||
|
"items": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
# 分页扫描:为了支持 service/level 过滤,这里会向后多取一些直到凑够 limit 或到末尾
|
||||||
|
# 保护:最多扫描 limit*10 条,避免过滤太严格导致无限扫描
|
||||||
|
max_scan = min(llen_total, start + limit * 10)
|
||||||
|
pos = start
|
||||||
|
scanned = 0
|
||||||
items: list[Dict[str, Any]] = []
|
items: list[Dict[str, Any]] = []
|
||||||
for raw in raw_items or []:
|
|
||||||
try:
|
try:
|
||||||
obj = raw
|
while len(items) < limit and pos < llen_total and pos < max_scan:
|
||||||
if isinstance(raw, bytes):
|
chunk_size = min(500, limit, max_scan - pos)
|
||||||
obj = raw.decode("utf-8", errors="ignore")
|
end = pos + chunk_size - 1
|
||||||
if isinstance(obj, str):
|
raw_batch = client.lrange(list_key, pos, end)
|
||||||
parsed = json.loads(obj)
|
scanned += len(raw_batch or [])
|
||||||
else:
|
|
||||||
continue
|
for raw in raw_batch or []:
|
||||||
if not isinstance(parsed, dict):
|
try:
|
||||||
continue
|
obj = raw
|
||||||
if service and str(parsed.get("service")) != service:
|
if isinstance(raw, bytes):
|
||||||
continue
|
obj = raw.decode("utf-8", errors="ignore")
|
||||||
if level and str(parsed.get("level")) != level:
|
if not isinstance(obj, str):
|
||||||
continue
|
continue
|
||||||
items.append(parsed)
|
parsed = json.loads(obj)
|
||||||
except Exception:
|
if not isinstance(parsed, dict):
|
||||||
continue
|
continue
|
||||||
|
if service and str(parsed.get("service")) != service:
|
||||||
|
continue
|
||||||
|
if level and str(parsed.get("level")) != level:
|
||||||
|
continue
|
||||||
|
items.append(parsed)
|
||||||
|
if len(items) >= limit:
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
pos = end + 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"读取 Redis 日志失败: {e}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"group": group,
|
"group": group,
|
||||||
"key": list_key,
|
"key": list_key,
|
||||||
|
"start": start,
|
||||||
|
"limit": limit,
|
||||||
|
"llen_total": llen_total,
|
||||||
|
"scanned": scanned,
|
||||||
|
"next_start": pos,
|
||||||
|
"has_more": pos < llen_total,
|
||||||
"count": len(items),
|
"count": len(items),
|
||||||
"items": items,
|
"items": items,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,44 @@
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.log-paging {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paging-left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paging-meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paging-meta code {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paging-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.log-row {
|
.log-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 170px 140px 110px 1fr;
|
grid-template-columns: 170px 140px 110px 1fr;
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ export default function LogMonitor() {
|
||||||
const [group, setGroup] = useState('error')
|
const [group, setGroup] = useState('error')
|
||||||
const [overview, setOverview] = useState(null)
|
const [overview, setOverview] = useState(null)
|
||||||
const [saving, setSaving] = useState(false)
|
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 [limit, setLimit] = useState(200)
|
||||||
const [service, setService] = useState('')
|
const [service, setService] = useState('')
|
||||||
|
|
@ -32,11 +34,11 @@ export default function LogMonitor() {
|
||||||
const [refreshSec, setRefreshSec] = useState(5)
|
const [refreshSec, setRefreshSec] = useState(5)
|
||||||
|
|
||||||
const params = useMemo(() => {
|
const params = useMemo(() => {
|
||||||
const p = { limit: String(limit), group }
|
const p = { limit: String(limit), group, start: String(pageStart) }
|
||||||
if (service) p.service = service
|
if (service) p.service = service
|
||||||
if (level) p.level = level
|
if (level) p.level = level
|
||||||
return p
|
return p
|
||||||
}, [limit, service, level, group])
|
}, [limit, service, level, group, pageStart])
|
||||||
|
|
||||||
const loadOverview = async () => {
|
const loadOverview = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -53,6 +55,12 @@ export default function LogMonitor() {
|
||||||
try {
|
try {
|
||||||
const res = await api.getSystemLogs(params)
|
const res = await api.getSystemLogs(params)
|
||||||
setItems(res?.items || [])
|
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()
|
await loadOverview()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e?.message || '获取日志失败')
|
setError(e?.message || '获取日志失败')
|
||||||
|
|
@ -105,6 +113,10 @@ export default function LogMonitor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const goFirstPage = () => setPageStart(0)
|
||||||
|
const goNextPage = () => setPageStart(pageMeta?.next_start || 0)
|
||||||
|
const goPrevPage = () => setPageStart(Math.max(0, pageStart - Number(limit || 200)))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="log-monitor">
|
<div className="log-monitor">
|
||||||
<div className="log-header">
|
<div className="log-header">
|
||||||
|
|
@ -174,7 +186,13 @@ export default function LogMonitor() {
|
||||||
<div className="log-controls">
|
<div className="log-controls">
|
||||||
<div className="control">
|
<div className="control">
|
||||||
<label>分组</label>
|
<label>分组</label>
|
||||||
<select value={group} onChange={(e) => setGroup(e.target.value)}>
|
<select
|
||||||
|
value={group}
|
||||||
|
onChange={(e) => {
|
||||||
|
setGroup(e.target.value)
|
||||||
|
setPageStart(0)
|
||||||
|
}}
|
||||||
|
>
|
||||||
{GROUPS.map((g) => (
|
{GROUPS.map((g) => (
|
||||||
<option key={g.key} value={g.key}>
|
<option key={g.key} value={g.key}>
|
||||||
{g.label}
|
{g.label}
|
||||||
|
|
@ -188,9 +206,12 @@ export default function LogMonitor() {
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max="2000"
|
max="20000"
|
||||||
value={limit}
|
value={limit}
|
||||||
onChange={(e) => setLimit(Number(e.target.value || 200))}
|
onChange={(e) => {
|
||||||
|
setLimit(Number(e.target.value || 200))
|
||||||
|
setPageStart(0)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -242,6 +263,31 @@ export default function LogMonitor() {
|
||||||
|
|
||||||
{error ? <div className="log-error">{error}</div> : null}
|
{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-table">
|
||||||
<div className="log-row log-head">
|
<div className="log-row log-head">
|
||||||
<div className="c-time">时间</div>
|
<div className="c-time">时间</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user