From b83815b31f4aa38201e3b6b4d025a068ec57c4de Mon Sep 17 00:00:00 2001 From: PeterYurkovich Date: Fri, 11 Sep 2026 17:31:11 -0400 Subject: [PATCH] temp --- web/src/cancellable-fetch.ts | 38 +++++++----- web/src/components/logs-table.tsx | 2 +- web/src/components/virtualized-logs-table.tsx | 9 ++- web/src/hooks/useLogs.ts | 47 +++++++++++++-- web/src/loki-client.ts | 58 +++++++++++++++++-- 5 files changed, 129 insertions(+), 25 deletions(-) diff --git a/web/src/cancellable-fetch.ts b/web/src/cancellable-fetch.ts index 43ac5a808..49a357f3d 100644 --- a/web/src/cancellable-fetch.ts +++ b/web/src/cancellable-fetch.ts @@ -37,7 +37,7 @@ export const cancellableFetch = ( const abort = () => abortController.abort(); const fetchPromise = async (): Promise => { - const requestTimeout = timeout ?? 30 * 1000; + const requestTimeout = timeout; try { const method = init?.method || 'GET'; @@ -48,24 +48,36 @@ export const cancellableFetch = ( }; let result: T; + const timeoutPromise = new Promise((_resolve, reject) => { + setTimeout(() => reject(new TimeoutError(url, timeout)), timeout); + }); if (method.toUpperCase() === 'POST') { - result = await consoleFetchJSON.post( - url, - init?.body, - options, - requestTimeout > 0 ? requestTimeout : undefined, - ); + result = await Promise.race([ + consoleFetchJSON.post( + url, + init?.body, + options, + requestTimeout > 0 ? requestTimeout : undefined, + ), + timeoutPromise, + ]); } else { - result = await consoleFetchJSON( - url, - method, - options, - requestTimeout > 0 ? requestTimeout : undefined, + result = await Promise.race([ + consoleFetchJSON(url, method, options, requestTimeout > 0 ? requestTimeout : undefined), + timeoutPromise, + ]).then( + (res) => { + console.debug('success', Date.now().toLocaleString()); + return res; + }, + () => { + console.debug('failure', Date.now().toLocaleString()); + }, ); } - return result; + return await result; } catch (error: unknown) { if (error instanceof Error) { if (error.name === 'AbortError') { diff --git a/web/src/components/logs-table.tsx b/web/src/components/logs-table.tsx index 2c78169fc..59c5f2137 100644 --- a/web/src/components/logs-table.tsx +++ b/web/src/components/logs-table.tsx @@ -38,7 +38,7 @@ interface LogsTableProps { logsData?: QueryRangeResponse; isLoading?: boolean; hasMoreLogsData?: boolean; - isLoadingMore?: boolean; + isLoadingMore: boolean; onLoadMore?: (lastTimestampNs: string) => void; onSortByDate?: (direction?: Direction) => void; direction?: Direction; diff --git a/web/src/components/virtualized-logs-table.tsx b/web/src/components/virtualized-logs-table.tsx index 871b3addf..4c8c878dd 100644 --- a/web/src/components/virtualized-logs-table.tsx +++ b/web/src/components/virtualized-logs-table.tsx @@ -67,7 +67,6 @@ type RowMemoProps = RowProps & { }; const RowMemo = memo( - // eslint-disable-next-line ({ Row, isScrolling, style, ...props }: RowMemoProps) => , (_, nextProps) => { if (nextProps.isScrolling) { @@ -181,7 +180,6 @@ const VirtualizedTableBody = ({ } cellMeasurementCache.clearAll(); tableBodyRef.current?.forceUpdateVirtualGrid(); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [expandedItems, showResources]); const activeColumnIDs = useMemo(() => new Set(columns.map((c) => c.id)), [columns]); @@ -321,6 +319,11 @@ export const VirtualizedLogsTable = ({ scrollerRef.current?.updatePosition(); }, [shouldResize]); + console.debug('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~'); + console.debug('isLoading', isLoading); + console.debug('isLoadingMore', isLoadingMore); + console.debug('hasMoreLogsData', hasMoreLogsData); + return (
@@ -416,7 +419,7 @@ export const VirtualizedLogsTable = ({ )} - {!isLoading && hasMoreLogsData && ( + {!(isLoading || isLoadingMore) && hasMoreLogsData && ( { histogramError: action.payload.error, }; case 'logsRequest': + console.debug('logsRequest'); return { ...state, isLoadingLogsData: true, logsData: undefined, logsError: undefined, + moreLogsError: undefined, hasMoreLogsData: false, + isLoadingMoreLogsData: false, isStreaming: false, isLoadingVolumeData: false, }; case 'startStreaming': + console.debug('startStreaming'); return { ...state, logsData: undefined, logsError: undefined, + moreLogsError: undefined, hasMoreLogsData: false, isStreaming: true, }; @@ -180,6 +194,7 @@ const reducer = (state: State, action: Action): State => { logsError: undefined, }; case 'streamingResponse': + console.debug('streamingResponse'); return { ...state, logsData: appendData(state.logsData, action.payload.logsData, STREAMING_MAX_LOGS_LIMIT), @@ -210,16 +225,20 @@ const reducer = (state: State, action: Action): State => { ...state, isLoadingMoreLogsData: true, logsError: undefined, + moreLogsError: undefined, }; case 'logsResponse': + console.debug('logsResponse'); return { ...state, isLoadingLogsData: false, + isLoadingMoreLogsData: false, showVolumeGraph: false, logsData: action.payload.logsData, hasMoreLogsData: hasMoreLogs(action.payload.logsData, action.payload.config.logsLimit), }; case 'moreLogsResponse': + console.debug('moreLogsResponse'); return { ...state, isLoadingMoreLogsData: false, @@ -227,11 +246,22 @@ const reducer = (state: State, action: Action): State => { hasMoreLogsData: hasMoreLogs(action.payload.logsData, action.payload.config.logsLimit), }; case 'logsError': + console.debug('logsError: only sets isLoadingsFalse'); return { ...state, isLoadingLogsData: false, isLoadingMoreLogsData: false, logsError: action.payload.error, + moreLogsError: undefined, + }; + case 'moreLogsError': + console.debug('moreLogsError: only sets isLoadingsFalse'); + return { + ...state, + isLoadingLogsData: false, + isLoadingMoreLogsData: false, + logsError: action.payload.error, + moreLogsError: undefined, }; default: @@ -269,7 +299,7 @@ export const useLogs = ( } const configRef = useRef(logsContext.config); - // eslint-disable-next-line react-hooks/refs + configRef.current = logsContext.config; const [ @@ -283,6 +313,7 @@ export const useLogs = ( histogramError, volumeData, logsError, + moreLogsError, volumeError, showVolumeGraph, hasMoreLogsData, @@ -312,7 +343,7 @@ export const useLogs = ( schema: Schema; }) => { if (query.length === 0) { - dispatch({ type: 'logsError', payload: { error: new Error('Query is empty') } }); + dispatch({ type: 'moreLogsError', payload: { error: new Error('Query is empty') } }); return; } @@ -351,11 +382,17 @@ export const useLogs = ( namespace, direction: currentDirection.current, schema, + timeout: 2, }); logsAbort.current = abort; - const queryResponse = await request(); + const queryResponse = await request() + .then(toRecord) + .then(throwResponseError) + .then(toQueryRangeResponse) + .then(validateQueryRangeResponse); + console.debug('queryResponse', queryResponse); dispatch({ type: 'moreLogsResponse', @@ -363,7 +400,7 @@ export const useLogs = ( }); } catch (error) { if (!isAbortError(error)) { - dispatch({ type: 'logsError', payload: { error } }); + dispatch({ type: 'moreLogsError', payload: { error } }); } } }; @@ -419,6 +456,7 @@ export const useLogs = ( namespace, direction: currentDirection.current, schema, + timeout: 2000, }); logsAbort.current = abort; @@ -671,6 +709,7 @@ export const useLogs = ( getMoreLogs, hasMoreLogsData, logsError, + moreLogsError, getHistogram, histogramError, toggleStreaming, diff --git a/web/src/loki-client.ts b/web/src/loki-client.ts index 695b7e50b..d41150cf8 100644 --- a/web/src/loki-client.ts +++ b/web/src/loki-client.ts @@ -26,6 +26,7 @@ type QueryRangeParams = { tenant: string; direction?: Direction; schema: Schema; + timeout: number; }; type VolumeRangeParams = { @@ -61,6 +62,54 @@ type LokiTailQueryParams = { const MAX_RANGE_REQUEST_NS = 21_600_000_000_000n; // 6 hours in nanoseconds +export const isRecord = (response: unknown): response is Record => + typeof response === 'object' && response !== null && !Array.isArray(response); + +export const toRecord = (response: unknown): Record => { + if (!isRecord(response)) { + throw new Error('Invalid Loki query response'); + } + + return response; +}; + +export const throwResponseError = (response: Record): Record => { + if (response.status !== 'error') { + return response; + } + + const errorType = typeof response.errorType === 'string' ? response.errorType : undefined; + const error = typeof response.error === 'string' ? response.error : undefined; + throw new Error([errorType, error].filter(Boolean).join(': ') || 'Loki query failed'); +}; + +export const isQueryRangeResponse = ( + response: Record, +): response is QueryRangeResponse => { + const data = response.data; + return isRecord(data) && Array.isArray(data.result); +}; + +export const toQueryRangeResponse = (response: Record): QueryRangeResponse => { + if (!isQueryRangeResponse(response)) { + throw new Error('Invalid Loki query response: missing data.result'); + } + + return response; +}; + +export const validateQueryRangeResponse = (response: QueryRangeResponse): QueryRangeResponse => { + if (response.status !== 'success') { + throw new Error(`Invalid Loki query response status: ${String(response.status)}`); + } + + if (response.data.resultType !== 'streams' && response.data.resultType !== 'matrix') { + throw new Error('Invalid Loki query response: invalid data.resultType'); + } + + return response; +}; + export const getFetchConfig = ({ config, tenant, @@ -84,7 +133,7 @@ export const getFetchConfig = ({ return { requestInit: {}, endpoint: `${LOKI_ENDPOINT}/api/logs/v1/${tenant}`, - timeout, + timeout: 100, }; }; @@ -147,6 +196,7 @@ export const executeQueryRange = ({ namespace, direction, schema, + timeout, }: QueryRangeParams): CancellableFetch => { const extendedQuery = queryWithNamespace({ query, @@ -165,7 +215,7 @@ export const executeQueryRange = ({ params.direction = direction; } - const { endpoint, requestInit, timeout } = getFetchConfig({ config, tenant }); + const { endpoint, requestInit } = getFetchConfig({ config, tenant }); return cancellableFetch( `${endpoint}/loki/api/v1/query_range?${new URLSearchParams(params)}`, @@ -242,8 +292,8 @@ export const executeHistogramQuery = ({ schema, }); - // eslint-disable-next-line max-len - const histogramQuery = `sum by (${labelSeverity}) (count_over_time(${extendedQuery} [${intervalString}]))`; + const histogramQuery = + `sum by (${labelSeverity}) ` + ` (count_over_time(${extendedQuery} [${intervalString}]))`; const params = { query: histogramQuery,