tune(logs): 每页条数默认 1000→200,上下文默认 5 条(依据实测数据)

=== 一、每页条数:1000 太多,实测依据 ===
1. 每次查询响应体 671 B/条 → 1000 条 = **655 KB**
2. 前端结果表**没有虚拟滚动**(无 react-window / tanstack virtual),
   1000 行会全部渲染成 DOM 节点 → 滚动卡顿
3. 后端 maxLogQueryLimit 本来就是 1000,我上一版 UI 给了 2000/5000 选项
   是"假选项"(会被静默收敛成 1000),本次去掉
4. 密度参考:cex-ipublic-gateway 每天 3.28 亿条 ≈ **3800 条/秒**,
   1000 条只覆盖 0.26 秒 —— 靠翻页浏览无意义,应靠时间范围/LogQL 收窄

调整:默认 1000 → **200**(后端自身默认值,约 131KB);选项 [100/200/500/1000]
提示文案改为"日志量大时先收窄时间范围或加过滤,比调大每页条数更有效"

=== 二、上下文档数:1 条不够,默认用 5 ===
实测一个 SpringBoot 异常被拆成 **4 条独立日志**:
  16:15:35.259 [reactor-http-epoll-3] ERROR ...AbstractErrorWebExceptionHandler
  org.springframework.web.reactive.resource.NoResourceFoundException: 404 NOT_FOUND
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
    *__checkpoint ⇢ HTTP HEAD "/" [ExceptionHandlingWebHandler]
→ 前后各 1 条(共 2 条)连一个完整异常都拼不出来
→ 前后各 5 条(共 10 条)能看到"异常 + 前后业务日志",是排障的最小可用集
→ 完整 Java 堆栈(20+ 帧)需要 20-50 条

调整:默认 2 → **5**;选项增加 1 → [1/2/5/10/20/50]

镜像:lghost/lokistack:latest + v0.1 = sha256:2bcc86b8e30d
This commit is contained in:
opsx committed 2026-10-28 16:18:48 +00:00
1 parent 3191af8b6a
commit 6364a70215
5 files changed
+12 -7

No files matched your search

+3
View File
@@ -23,6 +23,9 @@ import (
"lokistack/repository" "lokistack/repository"
) )
// maxLogQueryLimit 单页硬上限。依据:实测每条日志约 671B——
// 1000 条 ≈ 655KB 响应;而前端结果表**没有虚拟滚动**,1000 行会全部渲染成
// DOM 节点导致卡顿。前端下拉只给到 1000,超过会被这里静默收敛。
const maxLogQueryLimit = 1000 const maxLogQueryLimit = 1000
const logQueryTimeout = 120 * time.Second const logQueryTimeout = 120 * time.Second
+1 -1
View File
@@ -20,7 +20,7 @@ export default {
sourceAria: 'Data source', sourceAria: 'Data source',
sourceEmptyOption: 'No data sources available', sourceEmptyOption: 'No data sources available',
limitLabel: 'Per page', limitLabel: 'Per page',
limitHint: 'Up to 1000 entries per page; paging keeps only the current page.', limitHint: 'Default 200 entries (~0.7KB each). For very dense logs, narrow the time range or add filters instead of raising the page size.',
rangeInvalid: 'Select a valid start and end time', rangeInvalid: 'Select a valid start and end time',
queryFailed: 'Query failed', queryFailed: 'Query failed',
}, },
+1 -1
View File
@@ -19,7 +19,7 @@ export default {
sourceAria: '数据源', sourceAria: '数据源',
sourceEmptyOption: '暂无可用数据源', sourceEmptyOption: '暂无可用数据源',
limitLabel: '每页条数', limitLabel: '每页条数',
limitHint: '每页最多 1000 条,翻页仅保留当前页。', limitHint: '默认 200 条(每条约 0.7KB)。日志量很大时先收窄时间范围或加过滤条件,比调大每页条数更有效。',
rangeInvalid: '请选择有效的起止时间', rangeInvalid: '请选择有效的起止时间',
queryFailed: '查询失败', queryFailed: '查询失败',
}, },
@@ -41,7 +41,7 @@ export function LogContextDialog({ context, terms, lines, onLinesChange, onClose
<Select value={String(lines)} onValueChange={value => onLinesChange(Number(value))}> <Select value={String(lines)} onValueChange={value => onLinesChange(Number(value))}>
<SelectTrigger className="h-6 w-16 px-2 text-xs"><SelectValue /></SelectTrigger> <SelectTrigger className="h-6 w-16 px-2 text-xs"><SelectValue /></SelectTrigger>
<SelectContent> <SelectContent>
{[2, 5, 10, 20, 50].map(value => <SelectItem key={value} value={String(value)}>{value}</SelectItem>)} {[1, 2, 5, 10, 20, 50].map(value => <SelectItem key={value} value={String(value)}>{value}</SelectItem>)}
</SelectContent> </SelectContent>
</Select> </Select>
</span> </span>
+6 -4
View File
@@ -68,7 +68,9 @@ export default function LogsPage() {
const [executedRange, setExecutedRange] = useState<{ start: number; end: number } | null>(null) const [executedRange, setExecutedRange] = useState<{ start: number; end: number } | null>(null)
const [start, setStart] = useState(() => toLocal(new Date(Date.now() - 300000))) const [start, setStart] = useState(() => toLocal(new Date(Date.now() - 300000)))
const [end, setEnd] = useState(() => toLocal(new Date())) const [end, setEnd] = useState(() => toLocal(new Date()))
const [limit, setLimit] = useState(1000) // 默认 200:实测 671B/条,200 条约 131KB、DOM 200 行渲染顺畅。
// 后端 maxLogQueryLimit=1000,所以下拉不给 2000/5000(否则静默截断成 1000,是假选项)
const [limit, setLimit] = useState(200)
const [queryHistory, setQueryHistory] = useState(readQueryHistory) const [queryHistory, setQueryHistory] = useState(readQueryHistory)
const [entries, setEntries] = useState<LogEntry[]>([]) const [entries, setEntries] = useState<LogEntry[]>([])
const [searched, setSearched] = useState(false) const [searched, setSearched] = useState(false)
@@ -198,10 +200,10 @@ export default function LogsPage() {
return () => window.clearInterval(timer) return () => window.clearInterval(timer)
}, [tail, sourceId, query, limit]) }, [tail, sourceId, query, limit])
/** 上下文前后条数:默认 2 条(原来写死 5),选择后记住,弹窗内可随时改 */ /** 上下文前后条数:默认 5 条(Java 一个异常常拆成 4 条日志,2 条拼不完整),选择后记住 */
const [contextLines, setContextLines] = useState<number>(() => { const [contextLines, setContextLines] = useState<number>(() => {
const saved = Number(localStorage.getItem('logContextLines')) const saved = Number(localStorage.getItem('logContextLines'))
return saved > 0 && saved <= 50 ? saved : 2 return saved > 0 && saved <= 50 ? saved : 5
}) })
/** 记住当前查看上下文的那一行,切换条数时原地重新拉取 */ /** 记住当前查看上下文的那一行,切换条数时原地重新拉取 */
const [contextRow, setContextRow] = useState<LogEntry | null>(null) const [contextRow, setContextRow] = useState<LogEntry | null>(null)
@@ -273,7 +275,7 @@ export default function LogsPage() {
<Select value={String(limit)} onValueChange={value => setLimit(Number(value))}> <Select value={String(limit)} onValueChange={value => setLimit(Number(value))}>
<SelectTrigger className="h-9 w-24" aria-label={t('toolbar.limitLabel')}><SelectValue /></SelectTrigger> <SelectTrigger className="h-9 w-24" aria-label={t('toolbar.limitLabel')}><SelectValue /></SelectTrigger>
<SelectContent> <SelectContent>
{[100, 500, 1000, 2000, 5000].map(value => <SelectItem key={value} value={String(value)}>{value}</SelectItem>)} {[100, 200, 500, 1000].map(value => <SelectItem key={value} value={String(value)}>{value}</SelectItem>)}
</SelectContent> </SelectContent>
</Select> </Select>
</label> </label>