feat(logs): enhance log fetching with incremental support using cursor and after parameters

This commit is contained in:
LTbinglingfeng
2026-06-15 22:51:08 +08:00
Unverified
parent 1279cc1299
commit b0db1dfd5d
2 changed files with 69 additions and 25 deletions
+56 -20
View File
@@ -52,9 +52,27 @@ const MAX_BUFFER_LINES = 10000;
const LONG_PRESS_MS = 650; const LONG_PRESS_MS = 650;
const LONG_PRESS_MOVE_THRESHOLD = 10; const LONG_PRESS_MOVE_THRESHOLD = 10;
const getIncrementalAfter = (cursor: LogsQuery['after']): LogsQuery['after'] => { type LogPosition = Pick<LogsQuery, 'after' | 'cursor'>;
if (typeof cursor !== 'number') return cursor;
return cursor > 1 ? cursor - 1 : undefined; const getIncrementalAfter = (after: LogsQuery['after']): LogsQuery['after'] => {
if (typeof after !== 'number') return after;
return after > 1 ? after - 1 : undefined;
};
const buildLogsQuery = (incremental: boolean, position: LogPosition): LogsQuery => {
const params: LogsQuery = { limit: MAX_BUFFER_LINES };
if (!incremental) return params;
if (position.cursor) {
params.cursor = position.cursor;
}
const after = getIncrementalAfter(position.after);
if (after !== undefined) {
params.after = after;
}
return params;
}; };
const findLineOverlap = (currentLines: string[], incomingLines: string[]): number => { const findLineOverlap = (currentLines: string[], incomingLines: string[]): number => {
@@ -174,8 +192,29 @@ export function LogsPage() {
const logRequestInFlightRef = useRef(false); const logRequestInFlightRef = useRef(false);
const pendingFullReloadRef = useRef(false); const pendingFullReloadRef = useRef(false);
// 保存最新游标用于增量获取 // 保存最新游标用于增量获取;新 CPA 后端优先使用 cursor,旧接口和 Home 继续使用 after。
const latestCursorRef = useRef<LogsQuery['after']>(undefined); const logPositionRef = useRef<LogPosition>({});
const resetLogPosition = () => {
logPositionRef.current = {};
};
const updateLogPosition = (
data: Awaited<ReturnType<typeof logsApi.fetchLogs>>,
incremental: boolean
) => {
const currentPosition = logPositionRef.current;
const nextPosition: LogPosition = {};
if (data.nextCursor) {
nextPosition.cursor = data.nextCursor;
}
if (data.latestAfter !== undefined) {
nextPosition.after = data.latestAfter;
} else if (incremental && currentPosition.after !== undefined) {
nextPosition.after = currentPosition.after;
}
logPositionRef.current = nextPosition;
};
const disableControls = connectionStatus !== 'connected'; const disableControls = connectionStatus !== 'connected';
const refreshDisabled = disableControls || loading || cpaNeedsFileLogging; const refreshDisabled = disableControls || loading || cpaNeedsFileLogging;
@@ -190,7 +229,7 @@ export function LogsPage() {
if (cpaNeedsFileLogging) { if (cpaNeedsFileLogging) {
if (!incremental) { if (!incremental) {
latestCursorRef.current = undefined; resetLogPosition();
requestLogHomeIpByIdRef.current = {}; requestLogHomeIpByIdRef.current = {};
setFileLoggingRequired(false); setFileLoggingRequired(false);
setLogState({ buffer: [], visibleFrom: 0 }); setLogState({ buffer: [], visibleFrom: 0 });
@@ -222,19 +261,12 @@ export function LogsPage() {
scrollerInstance?.requestScrollToBottom(); scrollerInstance?.requestScrollToBottom();
} }
const params: LogsQuery = const params = buildLogsQuery(incremental, logPositionRef.current);
incremental && latestCursorRef.current
? { after: getIncrementalAfter(latestCursorRef.current), limit: MAX_BUFFER_LINES }
: { limit: MAX_BUFFER_LINES };
const data = await logsApi.fetchLogs(params); const data = await logsApi.fetchLogs(params);
setFileLoggingRequired(false); setFileLoggingRequired(false);
// 更新游标 updateLogPosition(data, incremental);
if (data.latestCursor) {
latestCursorRef.current = data.latestCursor;
} else if (!incremental) {
latestCursorRef.current = undefined;
}
if (data.requestLogHomeIpById) { if (data.requestLogHomeIpById) {
requestLogHomeIpByIdRef.current = incremental requestLogHomeIpByIdRef.current = incremental
? { ...requestLogHomeIpByIdRef.current, ...data.requestLogHomeIpById } ? { ...requestLogHomeIpByIdRef.current, ...data.requestLogHomeIpById }
@@ -245,7 +277,11 @@ export function LogsPage() {
const newLines = Array.isArray(data.lines) ? data.lines : []; const newLines = Array.isArray(data.lines) ? data.lines : [];
if (incremental && newLines.length > 0) { if (incremental && data.cursorReset) {
const buffer = newLines.slice(-MAX_BUFFER_LINES);
const visibleFrom = Math.max(buffer.length - INITIAL_DISPLAY_LINES, 0);
setLogState({ buffer, visibleFrom });
} else if (incremental && newLines.length > 0) {
// 增量更新:追加新日志并限制缓冲区大小(避免内存与渲染膨胀) // 增量更新:追加新日志并限制缓冲区大小(避免内存与渲染膨胀)
setLogState((prev) => { setLogState((prev) => {
const prevRenderedCount = prev.buffer.length - prev.visibleFrom; const prevRenderedCount = prev.buffer.length - prev.visibleFrom;
@@ -271,7 +307,7 @@ export function LogsPage() {
console.error('Failed to load logs:', err); console.error('Failed to load logs:', err);
if (isLoggingToFileDisabledError(err)) { if (isLoggingToFileDisabledError(err)) {
if (!incremental) { if (!incremental) {
latestCursorRef.current = undefined; resetLogPosition();
requestLogHomeIpByIdRef.current = {}; requestLogHomeIpByIdRef.current = {};
setFileLoggingRequired(true); setFileLoggingRequired(true);
setLogState({ buffer: [], visibleFrom: 0 }); setLogState({ buffer: [], visibleFrom: 0 });
@@ -318,7 +354,7 @@ export function LogsPage() {
try { try {
await logsApi.clearLogs(); await logsApi.clearLogs();
setLogState({ buffer: [], visibleFrom: 0 }); setLogState({ buffer: [], visibleFrom: 0 });
latestCursorRef.current = undefined; resetLogPosition();
requestLogHomeIpByIdRef.current = {}; requestLogHomeIpByIdRef.current = {};
setFileLoggingRequired(false); setFileLoggingRequired(false);
showNotification(t('logs.clear_success'), 'success'); showNotification(t('logs.clear_success'), 'success');
@@ -429,7 +465,7 @@ export function LogsPage() {
useEffect(() => { useEffect(() => {
if (connectionStatus === 'connected') { if (connectionStatus === 'connected') {
latestCursorRef.current = undefined; resetLogPosition();
requestLogHomeIpByIdRef.current = {}; requestLogHomeIpByIdRef.current = {};
setFileLoggingRequired(false); setFileLoggingRequired(false);
loadLogs(false); loadLogs(false);
+13 -5
View File
@@ -11,6 +11,7 @@ export type LogBackendKind = 'unknown' | 'file' | 'home-db';
export interface LogsQuery { export interface LogsQuery {
after?: LogCursor; after?: LogCursor;
cursor?: string;
limit?: number; limit?: number;
offset?: number; offset?: number;
} }
@@ -42,7 +43,9 @@ export interface HomeLogsResponse {
export interface LogsResponse { export interface LogsResponse {
lines: string[]; lines: string[];
lineCount: number; lineCount: number;
latestCursor?: LogCursor; latestAfter?: LogCursor;
nextCursor?: string;
cursorReset?: boolean;
logBackendKind: LogBackendKind; logBackendKind: LogBackendKind;
requestLogHomeIpById?: Record<string, string>; requestLogHomeIpById?: Record<string, string>;
total?: number; total?: number;
@@ -67,6 +70,9 @@ const numberValue = (value: unknown): number | undefined => {
return Number.isFinite(parsed) ? parsed : undefined; return Number.isFinite(parsed) ? parsed : undefined;
}; };
const booleanValue = (value: unknown): boolean =>
value === true || (typeof value === 'string' && value.trim().toLowerCase() === 'true');
const positiveNumberValue = (value: unknown): number | undefined => { const positiveNumberValue = (value: unknown): number | undefined => {
const parsed = numberValue(value); const parsed = numberValue(value);
return parsed !== undefined && parsed > 0 ? parsed : undefined; return parsed !== undefined && parsed > 0 ? parsed : undefined;
@@ -104,8 +110,10 @@ const normalizeCPALogs = (data: Record<string, unknown>): LogsResponse => {
return { return {
lines, lines,
lineCount: Number.isFinite(lineCount) ? lineCount : lines.length, lineCount: Number.isFinite(lineCount) ? lineCount : lines.length,
latestCursor: latestTimestamp > 0 ? latestTimestamp : undefined, latestAfter: latestTimestamp > 0 ? latestTimestamp : undefined,
logBackendKind: 'file' nextCursor: stringValue(data['next-cursor']) || undefined,
cursorReset: booleanValue(data['cursor-reset']),
logBackendKind: 'file',
}; };
}; };
@@ -140,12 +148,12 @@ const normalizeHomeLogs = (data: Record<string, unknown>): LogsResponse => {
return { return {
lines, lines,
lineCount: Number.isFinite(total) ? total : lines.length, lineCount: Number.isFinite(total) ? total : lines.length,
latestCursor, latestAfter: latestCursor,
logBackendKind: 'home-db', logBackendKind: 'home-db',
requestLogHomeIpById, requestLogHomeIpById,
total: Number.isFinite(total) ? total : undefined, total: Number.isFinite(total) ? total : undefined,
limit: Number.isFinite(limit) ? limit : undefined, limit: Number.isFinite(limit) ? limit : undefined,
offset: Number.isFinite(offset) ? offset : undefined offset: Number.isFinite(offset) ? offset : undefined,
}; };
}; };