feat(logs): 分页显示总页数(新增 /logs/stats,走 index/stats 只读索引)
用户反馈:底部只有「上一页 第 1 页 下一页」,看不到末尾/总页数。
原因:日志结果是 Loki 游标分页(cursor = 纳秒时间戳),本身不返回总数,
前端只能知道"有没有下一页",算不出总页数。
解法:新增 POST /api/v1/logs/stats
→ 调 Loki /loki/api/v1/index/stats(只读 TSDB 索引里的 chunk 元数据)
→ 返回 {streams, chunks, bytes, entries}
→ 前端用 entries / limit 得到总页数
实测成本极低:1h 51ms、24h 30ms(3.28 亿条 / 44.2 GB)。
改动:
1. backend: services.LokiStatsFor + controllers.LogsController.Stats + 路由 /logs/stats
2. frontend: logsApi.stats;查询成功后拉取总数(复用同一 AbortController,
失败静默不影响日志展示);计算 totalPages 传给结果面板
3. LogResultsPanel: 有总数时显示「第 N / M 页」,否则维持「第 N 页」
4. i18n: 新增 results.pageOfTotal(中英)
镜像:lghost/lokistack:latest + v0.1 = sha256:41d6576ec232
This commit is contained in:
1 parent
de376a1284
commit
5d530036e6
8 files changed
+88
-5
No files matched your search
@@ -78,6 +78,8 @@ export default function LogsPage() {
|
||||
// 从 boolean 改为字符串:原来只显示"日志量统计失败",
|
||||
// 真实原因(429 并发超限 / 400 范围过大 / Loki 不可用)全被吞掉。
|
||||
const [histogramError, setHistogramError] = useState<string>('')
|
||||
/** 当前查询的总条数(来自 Loki index/stats,只读索引),用于展示总页数 */
|
||||
const [totalEntries, setTotalEntries] = useState<number>(0)
|
||||
/** 当前在跑的查询/统计控制器:发起新请求前 abort 掉,避免重查询叠加 */
|
||||
const histAbortRef = useRef<AbortController | null>(null)
|
||||
const [cursor, setCursor] = useState('')
|
||||
@@ -99,7 +101,7 @@ export default function LogsPage() {
|
||||
} catch (e) { setError(e instanceof Error ? e.message : t('source.loadFailed')) }
|
||||
}, [t])
|
||||
useEffect(() => { void reloadSources() }, [reloadSources])
|
||||
useEffect(() => { requestVersion.current++; setEntries([]); setBuckets([]); setHistogramLoading(false); setHistogramError(''); setCursor(''); setPageIndex(0); setPageCursors(['']); setSearched(false); setLoading(false) }, [sourceId])
|
||||
useEffect(() => { requestVersion.current++; setEntries([]); setBuckets([]); setHistogramLoading(false); setHistogramError(''); setTotalEntries(0); setCursor(''); setPageIndex(0); setPageCursors(['']); setSearched(false); setLoading(false) }, [sourceId])
|
||||
|
||||
// 从日志分析页等入口跳转:带 ?q= 预填输入框并尝试执行一次;执行后清掉参数防刷新重复执行。
|
||||
// 预填不等数据源就绪(无数据源时也要让用户看到带来的查询),是否真正执行由 search 内部判断
|
||||
@@ -148,6 +150,12 @@ export default function LogsPage() {
|
||||
setPageIndex(targetPage ?? 0)
|
||||
setPageCursors(previous => paging ? [...previous.slice(0, targetPage), nextCursor] : [''])
|
||||
if (!paging) {
|
||||
// 总条数:走 index/stats(只读索引,24h 约 1.3s),失败静默不影响日志展示
|
||||
setTotalEntries(0)
|
||||
void logsApi.stats(data, { signal: histController.signal })
|
||||
.then(st => { if (version === requestVersion.current) setTotalEntries(Number(st?.entries) || 0) })
|
||||
.catch(() => undefined)
|
||||
|
||||
setHistogramLoading(true)
|
||||
setHistogramError('')
|
||||
void logsApi.histogram(data, { signal: histController.signal }).then(histogram => {
|
||||
@@ -259,7 +267,7 @@ export default function LogsPage() {
|
||||
{tab === 'query' && histogramLoading && <div role="status" className="flex items-center gap-2 rounded-xl border bg-card p-4 text-sm text-muted-foreground"><Loader2 className="size-4 animate-spin text-info" />{t('volume.statsLoading')}</div>}
|
||||
{tab === 'query' && histogramError && <p className="rounded-xl border bg-card p-4 text-sm text-muted-foreground">{histogramError}</p>}
|
||||
{tab === 'query' && buckets.length > 0 && <LogVolumeTimeline key={`${buckets[0].timestamp}-${buckets.length}`} buckets={buckets} onSelect={selectTimeRange} />}
|
||||
{tab === 'query' && <LogResultsPanel key={`${sourceId}:${executedQuery}:${executedRange?.start}:${executedRange?.end}`} entries={entries} searched={searched} hasSource={sources.length > 0} pageIndex={pageIndex} hasNextPage={Boolean(cursor)} loading={loading} loadingStartedAt={loadingStartedAt} query={query} terms={highlightTerms} onQueryChange={setQuery} onApplyDomain={applyDomainFilter} onShowContext={row => void showContext(row)} onSearch={() => void search()} onNextPage={() => void search(cursor, false, undefined, undefined, pageIndex + 1)} onPreviousPage={() => void search(pageCursors[pageIndex - 1] ?? '', false, undefined, undefined, pageIndex - 1)} />}
|
||||
{tab === 'query' && <LogResultsPanel key={`${sourceId}:${executedQuery}:${executedRange?.start}:${executedRange?.end}`} entries={entries} searched={searched} hasSource={sources.length > 0} pageIndex={pageIndex} hasNextPage={Boolean(cursor)} totalPages={totalEntries > 0 ? Math.max(1, Math.ceil(totalEntries / Math.max(1, limit))) : undefined} loading={loading} loadingStartedAt={loadingStartedAt} query={query} terms={highlightTerms} onQueryChange={setQuery} onApplyDomain={applyDomainFilter} onShowContext={row => void showContext(row)} onSearch={() => void search()} onNextPage={() => void search(cursor, false, undefined, undefined, pageIndex + 1)} onPreviousPage={() => void search(pageCursors[pageIndex - 1] ?? '', false, undefined, undefined, pageIndex - 1)} />}
|
||||
<LogContextDialog context={context} terms={highlightTerms} onClose={() => setContext(null)} />
|
||||
</div>
|
||||
</PageContainer>
|
||||
|
||||
Reference in new issue
Block a user