a
This commit is contained in:
parent
596b2ec788
commit
79526151f3
|
|
@ -187,6 +187,7 @@ def _get_redis_client_for_logs():
|
|||
async def get_logs(
|
||||
limit: int = 200,
|
||||
group: str = "error",
|
||||
start: int = 0,
|
||||
service: Optional[str] = None,
|
||||
level: Optional[str] = None,
|
||||
x_admin_token: Optional[str] = Header(default=None, alias="X-Admin-Token"),
|
||||
|
|
@ -204,8 +205,11 @@ async def get_logs(
|
|||
|
||||
if limit <= 0:
|
||||
limit = 200
|
||||
if limit > 2000:
|
||||
limit = 2000
|
||||
if limit > 20000:
|
||||
limit = 20000
|
||||
|
||||
if start < 0:
|
||||
start = 0
|
||||
|
||||
group = (group or "error").strip().lower()
|
||||
if group not in LOG_GROUPS:
|
||||
|
|
@ -218,33 +222,71 @@ async def get_logs(
|
|||
raise HTTPException(status_code=503, detail="Redis 不可用,无法读取日志")
|
||||
|
||||
try:
|
||||
raw_items = client.lrange(list_key, 0, limit - 1)
|
||||
llen_total = int(client.llen(list_key) or 0)
|
||||
except Exception as 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]] = []
|
||||
for raw in raw_items or []:
|
||||
try:
|
||||
obj = raw
|
||||
if isinstance(raw, bytes):
|
||||
obj = raw.decode("utf-8", errors="ignore")
|
||||
if isinstance(obj, str):
|
||||
parsed = json.loads(obj)
|
||||
else:
|
||||
continue
|
||||
if not isinstance(parsed, dict):
|
||||
continue
|
||||
if service and str(parsed.get("service")) != service:
|
||||
continue
|
||||
if level and str(parsed.get("level")) != level:
|
||||
continue
|
||||
items.append(parsed)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
try:
|
||||
while len(items) < limit and pos < llen_total and pos < max_scan:
|
||||
chunk_size = min(500, limit, max_scan - pos)
|
||||
end = pos + chunk_size - 1
|
||||
raw_batch = client.lrange(list_key, pos, end)
|
||||
scanned += len(raw_batch or [])
|
||||
|
||||
for raw in raw_batch or []:
|
||||
try:
|
||||
obj = raw
|
||||
if isinstance(raw, bytes):
|
||||
obj = raw.decode("utf-8", errors="ignore")
|
||||
if not isinstance(obj, str):
|
||||
continue
|
||||
parsed = json.loads(obj)
|
||||
if not isinstance(parsed, dict):
|
||||
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 {
|
||||
"group": group,
|
||||
"key": list_key,
|
||||
"start": start,
|
||||
"limit": limit,
|
||||
"llen_total": llen_total,
|
||||
"scanned": scanned,
|
||||
"next_start": pos,
|
||||
"has_more": pos < llen_total,
|
||||
"count": len(items),
|
||||
"items": items,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,6 +141,44 @@
|
|||
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 {
|
||||
display: grid;
|
||||
grid-template-columns: 170px 140px 110px 1fr;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ export default function LogMonitor() {
|
|||
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('')
|
||||
|
|
@ -32,11 +34,11 @@ export default function LogMonitor() {
|
|||
const [refreshSec, setRefreshSec] = useState(5)
|
||||
|
||||
const params = useMemo(() => {
|
||||
const p = { limit: String(limit), group }
|
||||
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])
|
||||
}, [limit, service, level, group, pageStart])
|
||||
|
||||
const loadOverview = async () => {
|
||||
try {
|
||||
|
|
@ -53,6 +55,12 @@ export default function LogMonitor() {
|
|||
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 || '获取日志失败')
|
||||
|
|
@ -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 (
|
||||
<div className="log-monitor">
|
||||
<div className="log-header">
|
||||
|
|
@ -174,7 +186,13 @@ export default function LogMonitor() {
|
|||
<div className="log-controls">
|
||||
<div className="control">
|
||||
<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) => (
|
||||
<option key={g.key} value={g.key}>
|
||||
{g.label}
|
||||
|
|
@ -188,9 +206,12 @@ export default function LogMonitor() {
|
|||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="2000"
|
||||
max="20000"
|
||||
value={limit}
|
||||
onChange={(e) => setLimit(Number(e.target.value || 200))}
|
||||
onChange={(e) => {
|
||||
setLimit(Number(e.target.value || 200))
|
||||
setPageStart(0)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -242,6 +263,31 @@ export default function LogMonitor() {
|
|||
|
||||
{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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user