-
Notifications
You must be signed in to change notification settings - Fork 22
temp #417
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
temp #417
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,7 +37,7 @@ export const cancellableFetch = <T>( | |
| const abort = () => abortController.abort(); | ||
|
|
||
| const fetchPromise = async (): Promise<T> => { | ||
| const requestTimeout = timeout ?? 30 * 1000; | ||
| const requestTimeout = timeout; | ||
|
|
||
| try { | ||
| const method = init?.method || 'GET'; | ||
|
|
@@ -48,24 +48,36 @@ export const cancellableFetch = <T>( | |
| }; | ||
|
|
||
| let result: T; | ||
| const timeoutPromise = new Promise<Response>((_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()); | ||
| }, | ||
|
Comment on lines
+74
to
+76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Rethrow the non-POST request failure. This rejection handler logs the failure and then resolves with 🤖 Prompt for AI Agents |
||
| ); | ||
| } | ||
|
|
||
| return result; | ||
| return await result; | ||
| } catch (error: unknown) { | ||
| if (error instanceof Error) { | ||
| if (error.name === 'AbortError') { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,10 @@ import { | |
| executeHistogramQuery, | ||
| executeQueryRange, | ||
| executeVolumeRange, | ||
| throwResponseError, | ||
| toQueryRangeResponse, | ||
| toRecord, | ||
| validateQueryRangeResponse, | ||
| } from '../loki-client'; | ||
| import { intervalFromTimeRange, numericTimeRange, timeRangeFromDuration } from '../time-range'; | ||
| import { msToNs } from '../value-utils'; | ||
|
|
@@ -36,6 +40,7 @@ type State = { | |
| isLoadingMoreLogsData: boolean; | ||
| logsData?: QueryRangeResponse; | ||
| logsError?: unknown; | ||
| moreLogsError?: unknown; | ||
| isLoadingVolumeData?: boolean; | ||
| volumeData?: VolumeRangeResponse; | ||
| volumeError?: unknown; | ||
|
|
@@ -80,6 +85,10 @@ type Action = | |
| type: 'logsError'; | ||
| payload: { error: unknown }; | ||
| } | ||
| | { | ||
| type: 'moreLogsError'; | ||
| payload: { error: unknown }; | ||
| } | ||
| | { | ||
| type: 'histogramError'; | ||
| payload: { error: unknown }; | ||
|
|
@@ -156,20 +165,25 @@ const reducer = (state: State, action: Action): State => { | |
| 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,28 +225,43 @@ 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, | ||
| logsData: appendData(state.logsData, action.payload.logsData), | ||
| 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, | ||
|
Comment on lines
+263
to
+264
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Store pagination failures in The 🤖 Prompt for AI Agents |
||
| }; | ||
|
|
||
| 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,19 +382,25 @@ 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', | ||
| payload: { logsData: queryResponse, config }, | ||
| }); | ||
| } 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, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<string, unknown> => | ||
| typeof response === 'object' && response !== null && !Array.isArray(response); | ||
|
|
||
| export const toRecord = (response: unknown): Record<string, unknown> => { | ||
| if (!isRecord(response)) { | ||
| throw new Error('Invalid Loki query response'); | ||
| } | ||
|
|
||
| return response; | ||
| }; | ||
|
|
||
| export const throwResponseError = (response: Record<string, unknown>): Record<string, unknown> => { | ||
| 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<string, unknown>, | ||
| ): response is QueryRangeResponse => { | ||
| const data = response.data; | ||
| return isRecord(data) && Array.isArray(data.result); | ||
| }; | ||
|
|
||
| export const toQueryRangeResponse = (response: Record<string, unknown>): 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, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Convert the timeout durations to milliseconds.
🤖 Prompt for AI Agents |
||
| }; | ||
| }; | ||
|
|
||
|
|
@@ -147,6 +196,7 @@ export const executeQueryRange = ({ | |
| namespace, | ||
| direction, | ||
| schema, | ||
| timeout, | ||
| }: QueryRangeParams): CancellableFetch<QueryRangeResponse> => { | ||
| 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<QueryRangeResponse>( | ||
| `${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, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Create the timeout race only when
timeout > 0.backend-client.tsandattribute-filters.tsxcallcancellableFetchwithouttimeout. The helper still schedulessetTimeoutwithundefined, which creates a zero-delay timer. That timer can win before the fetch completes. For non-POSTrequests, the rejection handler then converts the result toundefinedinstead of propagating the error. Guard timer creation and include the timeout promise inPromise.raceonly whenrequestTimeout > 0.🤖 Prompt for AI Agents