diff --git a/public/components/trace_analytics/components/common/shared_components/component_helper_functions.tsx b/public/components/trace_analytics/components/common/shared_components/component_helper_functions.tsx index a9f083a29..ca113f78e 100644 --- a/public/components/trace_analytics/components/common/shared_components/component_helper_functions.tsx +++ b/public/components/trace_analytics/components/common/shared_components/component_helper_functions.tsx @@ -10,23 +10,34 @@ export const useInjectElementsIntoGrid = ( rowCount: number, maxDisplayRows: number, tracesTableMode: string, - loadMoreHandler?: () => void + loadMoreHandler?: () => void, + maxTraces?: number ) => { useEffect(() => { setTimeout(() => { const toolbar = document.querySelector('.euiDataGrid__controls'); - if (toolbar && rowCount > maxDisplayRows) { - toolbar.style.display = 'flex'; - toolbar.style.alignItems = 'center'; - toolbar.style.justifyContent = 'space-between'; - - let warningDiv = toolbar.querySelector('.trace-table-warning'); - if (!warningDiv) { - warningDiv = document.createElement('div'); + + const shouldShowWarning = + (tracesTableMode === 'traces' && maxTraces != null && maxTraces < rowCount) || + (tracesTableMode !== 'traces' && rowCount > maxDisplayRows); + + if (toolbar) { + const existingWarning = toolbar.querySelector('.trace-table-warning'); + if (existingWarning) { + existingWarning.remove(); + } + + if (shouldShowWarning) { + toolbar.style.display = 'flex'; + toolbar.style.alignItems = 'center'; + toolbar.style.justifyContent = 'space-between'; + + const warningDiv = document.createElement('div'); warningDiv.className = 'trace-table-warning'; const strongElement = document.createElement('strong'); - strongElement.textContent = `${maxDisplayRows}`; + strongElement.textContent = + tracesTableMode === 'traces' ? `${maxTraces ?? maxDisplayRows}` : `${maxDisplayRows}`; const textSpan = document.createElement('span'); textSpan.appendChild(strongElement); @@ -69,5 +80,5 @@ export const useInjectElementsIntoGrid = ( } } }, 100); - }, [rowCount, tracesTableMode, loadMoreHandler]); + }, [rowCount, tracesTableMode, loadMoreHandler, maxTraces, maxDisplayRows]); }; diff --git a/public/components/trace_analytics/components/common/shared_components/custom_datagrid.tsx b/public/components/trace_analytics/components/common/shared_components/custom_datagrid.tsx index 0a47279da..54a2a0e4c 100644 --- a/public/components/trace_analytics/components/common/shared_components/custom_datagrid.tsx +++ b/public/components/trace_analytics/components/common/shared_components/custom_datagrid.tsx @@ -110,6 +110,7 @@ export const RenderCustomDataGrid: React.FC = ({ isTableDataLoading, tracesTableMode, setTracesTableMode, + maxTraces, setMaxTraces, }) => { const defaultVisibleColumns = useMemo(() => { @@ -125,7 +126,8 @@ export const RenderCustomDataGrid: React.FC = ({ const [isFullScreen, setIsFullScreen] = useState(fullScreen); const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const displayedRowCount = rowCount > MAX_DISPLAY_ROWS ? MAX_DISPLAY_ROWS : rowCount; + const displayedRowCount = + tracesTableMode === 'traces' ? maxTraces : Math.min(rowCount, MAX_DISPLAY_ROWS); const isDarkMode = uiSettingsService.get('theme:darkMode'); @@ -135,9 +137,15 @@ export const RenderCustomDataGrid: React.FC = ({ ) : []; - useInjectElementsIntoGrid(rowCount, MAX_DISPLAY_ROWS, tracesTableMode ?? '', () => { - setMaxTraces((prevMax: number) => Math.min(prevMax + 500, MAX_DISPLAY_ROWS)); - }); + useInjectElementsIntoGrid( + rowCount, + MAX_DISPLAY_ROWS, + tracesTableMode ?? '', + () => { + setMaxTraces((prevMax: number) => Math.min(prevMax + 500, MAX_DISPLAY_ROWS)); + }, + maxTraces + ); const disableInteractions = useMemo(() => isFullScreen, [isFullScreen]); diff --git a/public/components/trace_analytics/components/traces/traces_content.tsx b/public/components/trace_analytics/components/traces/traces_content.tsx index 68dcb6a1d..1aaa443fd 100644 --- a/public/components/trace_analytics/components/traces/traces_content.tsx +++ b/public/components/trace_analytics/components/traces/traces_content.tsx @@ -324,7 +324,8 @@ export function TracesContent(props: TracesProps) { maxTraces, props.dataSourceMDSId[0].id, sort, - isUnderOneHour + isUnderOneHour, + setTotalHits ).finally(() => setIsTraceTableLoading(false)); }; @@ -390,7 +391,8 @@ export function TracesContent(props: TracesProps) { maxTraces, props.dataSourceMDSId[0].id, newSort, - isUnderOneHour + isUnderOneHour, + setTotalHits ); tracesRequest.finally(() => setIsTraceTableLoading(false)); diff --git a/public/components/trace_analytics/components/traces/traces_custom_indices_table.tsx b/public/components/trace_analytics/components/traces/traces_custom_indices_table.tsx index e60728926..5d6be0de2 100644 --- a/public/components/trace_analytics/components/traces/traces_custom_indices_table.tsx +++ b/public/components/trace_analytics/components/traces/traces_custom_indices_table.tsx @@ -123,7 +123,7 @@ export function TracesCustomIndicesTable(props: TracesLandingTableProps) { key={columns.map((col) => col.id).join('-')} // Force re-render for switching from spans to traces columns={columns} renderCellValue={renderCellValue} - rowCount={props.tracesTableMode === 'traces' ? items.length : totalHits} + rowCount={totalHits} sorting={{ columns: sortingColumns, onSort }} pagination={pagination} isTableDataLoading={loading} diff --git a/public/components/trace_analytics/requests/__tests__/helper_funtions.test.tsx b/public/components/trace_analytics/requests/__tests__/helper_funtions.test.tsx new file mode 100644 index 000000000..127142484 --- /dev/null +++ b/public/components/trace_analytics/requests/__tests__/helper_funtions.test.tsx @@ -0,0 +1,93 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { handleError } from '../helper_functions'; +import { coreRefs } from '../../../../../public/framework/core_refs'; + +describe('handleError in handleDslRequest', () => { + const addDangerMock = jest.fn(); + const addErrorMock = jest.fn(); + + beforeAll(() => { + coreRefs.core = { + notifications: { + toasts: { + addDanger: addDangerMock, + addError: addErrorMock, + }, + }, + } as any; + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('displays danger toast for too_many_buckets_exception', () => { + const error = { + response: JSON.stringify({ + error: { + caused_by: { + type: 'too_many_buckets_exception', + reason: 'Too many buckets', + }, + }, + }), + }; + + handleError(error); + + expect(addDangerMock).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'Too many buckets in aggregation', + text: 'Too many buckets', + }) + ); + expect(addErrorMock).not.toHaveBeenCalled(); + }); + + it('logs error for non-bucket exceptions', () => { + const error = { + body: { + error: { + type: 'search_phase_execution_exception', + reason: 'Some other error', + }, + }, + }; + + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + handleError(error); + + expect(consoleSpy).toHaveBeenCalledWith(error); + expect(addDangerMock).not.toHaveBeenCalled(); + expect(addErrorMock).not.toHaveBeenCalled(); + consoleSpy.mockRestore(); + }); + + it('parses nested message JSON string', () => { + const error = { + body: { + message: JSON.stringify({ + error: { + caused_by: { + type: 'too_many_buckets_exception', + reason: 'Buckets exceeded', + }, + }, + }), + }, + }; + + handleError(error); + + expect(addDangerMock).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'Too many buckets in aggregation', + text: 'Buckets exceeded', + }) + ); + }); +}); diff --git a/public/components/trace_analytics/requests/helper_functions.tsx b/public/components/trace_analytics/requests/helper_functions.tsx new file mode 100644 index 000000000..256cbc045 --- /dev/null +++ b/public/components/trace_analytics/requests/helper_functions.tsx @@ -0,0 +1,47 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { coreRefs } from '../../../../public/framework/core_refs'; + +export const handleError = (error: any) => { + let parsedError: any = {}; + + const safeJsonParse = (input: string) => { + try { + return JSON.parse(input); + } catch { + return null; + } + }; + + if (typeof error?.response === 'string') { + parsedError = safeJsonParse(error.response) || {}; + } else if (typeof error?.body === 'string') { + parsedError = safeJsonParse(error.body) || {}; + } else { + parsedError = error?.body || error; + if (typeof parsedError.message === 'string') { + const nested = safeJsonParse(parsedError.message); + if (nested?.error) { + parsedError = nested; + } + } + } + + const errorType = parsedError?.error?.caused_by?.type || parsedError?.error?.type || ''; + const errorReason = parsedError?.error?.caused_by?.reason || parsedError?.error?.reason || ''; + + if (errorType === 'too_many_buckets_exception') { + coreRefs.core?.notifications.toasts.addDanger({ + title: 'Too many buckets in aggregation', + text: + errorReason || + 'Try using a shorter time range or increase the "search.max_buckets" cluster setting.', + toastLifeTimeMs: 10000, + }); + } else { + console.error(error); + } +}; diff --git a/public/components/trace_analytics/requests/queries/traces_queries.ts b/public/components/trace_analytics/requests/queries/traces_queries.ts index 4ebc82fe3..f053bf421 100644 --- a/public/components/trace_analytics/requests/queries/traces_queries.ts +++ b/public/components/trace_analytics/requests/queries/traces_queries.ts @@ -187,7 +187,7 @@ export const getTracesQuery = ( }, }, }, - track_total_hits: false, + track_total_hits: true, }; if (traceId) { jaegerQuery.query.bool.filter.push({ diff --git a/public/components/trace_analytics/requests/request_handler.ts b/public/components/trace_analytics/requests/request_handler.ts index 421c489b6..61c0e61cb 100644 --- a/public/components/trace_analytics/requests/request_handler.ts +++ b/public/components/trace_analytics/requests/request_handler.ts @@ -11,6 +11,7 @@ import { } from '../../../../common/constants/trace_analytics'; import { TraceAnalyticsMode } from '../../../../common/types/trace_analytics'; import { getSpanIndices } from '../components/common/helper_functions'; +import { handleError } from './helper_functions'; export async function handleDslRequest( http: CoreStart['http'], @@ -35,6 +36,7 @@ export async function handleDslRequest( const query = { dataSourceMDSId, }; + if (setShowTimeoutToast) { const id = setTimeout(() => setShowTimeoutToast(), 25000); // 25 seconds @@ -44,7 +46,7 @@ export async function handleDslRequest( query, }); } catch (error) { - console.error(error); + handleError(error); } finally { clearTimeout(id); } @@ -54,10 +56,11 @@ export async function handleDslRequest( body: JSON.stringify(body), query, }); - } catch (error_1) { - console.error(error_1); + } catch (error) { + handleError(error); } } + return undefined; } export async function handleJaegerIndicesExistRequest( diff --git a/public/components/trace_analytics/requests/traces_request_handler.ts b/public/components/trace_analytics/requests/traces_request_handler.ts index 7a7f18c11..986520e23 100644 --- a/public/components/trace_analytics/requests/traces_request_handler.ts +++ b/public/components/trace_analytics/requests/traces_request_handler.ts @@ -113,7 +113,8 @@ export const handleTracesRequest = async ( maxTraces: number = 500, dataSourceMDSId?: string, sort?: PropertySort, - isUnderOneHour?: boolean + isUnderOneHour?: boolean, + setTotalHits?: (count: number) => void ) => { const binarySearch = (arr: number[], target: number) => { if (!arr) return Number.NaN; @@ -167,6 +168,11 @@ export const handleTracesRequest = async ( percentileRangesResult.status === 'fulfilled' ? percentileRangesResult.value : {}; const response = responseResult.value; + if (setTotalHits) { + const totalHits = response?.hits?.total?.value ?? 0; + setTotalHits(totalHits); + } + if ( !response?.aggregations?.traces?.buckets || response.aggregations.traces.buckets.length === 0 diff --git a/server/routes/trace_analytics_dsl_router.ts b/server/routes/trace_analytics_dsl_router.ts index 4637ee983..e2b1138fe 100644 --- a/server/routes/trace_analytics_dsl_router.ts +++ b/server/routes/trace_analytics_dsl_router.ts @@ -158,7 +158,7 @@ export function registerTraceAnalyticsDslRouter(router: IRouter, dataSourceEnabl if (error.statusCode !== 404) console.error(error); return response.custom({ statusCode: error.statusCode || 400, - body: error.message, + body: error.response, }); } }