diff --git a/common/constants.ts b/common/constants.ts index 2186516..e2bb8de 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -4,6 +4,9 @@ */ export const TIMESTAMP = 'Timestamp'; +export const TYPE = 'Type'; +export const QUERY_HASHCODE = 'Query Hashcode'; +export const QUERY_COUNT = 'Query Count'; export const LATENCY = 'Latency'; export const CPU_TIME = 'CPU Time'; export const MEMORY_USAGE = 'Memory Usage'; @@ -11,3 +14,4 @@ export const INDICES = 'Indices'; export const SEARCH_TYPE = 'Search type'; export const NODE_ID = 'Coordinator node ID'; export const TOTAL_SHARDS = 'Total shards'; +export const GROUP_BY = 'Group by'; diff --git a/public/components/__snapshots__/app.test.tsx.snap b/public/components/__snapshots__/app.test.tsx.snap index 3571697..8cd7612 100644 --- a/public/components/__snapshots__/app.test.tsx.snap +++ b/public/components/__snapshots__/app.test.tsx.snap @@ -99,6 +99,49 @@ exports[` spec renders the component 1`] = `
+
+ +
+
+
spec renders the component 1`] = `
spec renders the component 1`] = `
spec renders the component 1`] = ` aria-live="polite" aria-sort="none" class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_Timestamp_0" + data-test-subj="tableHeaderCell_Query Hashcode_0" + role="columnheader" + scope="col" + > + + + + + + + + + @@ -452,7 +570,7 @@ exports[` spec renders the component 1`] = ` aria-live="polite" aria-sort="none" class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_measurements_1" + data-test-subj="tableHeaderCell_measurements_4" role="columnheader" scope="col" > @@ -477,7 +595,7 @@ exports[` spec renders the component 1`] = ` aria-live="polite" aria-sort="none" class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_measurements_2" + data-test-subj="tableHeaderCell_measurements_5" role="columnheader" scope="col" > @@ -502,7 +620,7 @@ exports[` spec renders the component 1`] = ` aria-live="polite" aria-sort="none" class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_measurements_3" + data-test-subj="tableHeaderCell_measurements_6" role="columnheader" scope="col" > @@ -527,7 +645,7 @@ exports[` spec renders the component 1`] = ` aria-live="polite" aria-sort="none" class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_indices_4" + data-test-subj="tableHeaderCell_indices_7" role="columnheader" scope="col" > @@ -552,7 +670,7 @@ exports[` spec renders the component 1`] = ` aria-live="polite" aria-sort="none" class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_search_type_5" + data-test-subj="tableHeaderCell_search_type_8" role="columnheader" scope="col" > @@ -577,7 +695,7 @@ exports[` spec renders the component 1`] = ` aria-live="polite" aria-sort="none" class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_node_id_6" + data-test-subj="tableHeaderCell_node_id_9" role="columnheader" scope="col" > @@ -602,7 +720,7 @@ exports[` spec renders the component 1`] = ` aria-live="polite" aria-sort="none" class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_total_shards_7" + data-test-subj="tableHeaderCell_total_shards_10" role="columnheader" scope="col" > @@ -631,7 +749,7 @@ exports[` spec renders the component 1`] = ` >
spec', () => { it('renders the component', () => { diff --git a/public/pages/Configuration/Configuration.tsx b/public/pages/Configuration/Configuration.tsx index 735fb6a..1d590a7 100644 --- a/public/pages/Configuration/Configuration.tsx +++ b/public/pages/Configuration/Configuration.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useMemo, useCallback, useState, useEffect } from 'react'; +import React, { useCallback, useState, useEffect } from 'react'; import { EuiBottomBar, EuiButton, @@ -24,18 +24,20 @@ import { } from '@elastic/eui'; import { useHistory, useLocation } from 'react-router-dom'; import { CoreStart } from 'opensearch-dashboards/public'; -import { QUERY_INSIGHTS, MetricSettings } from '../TopNQueries/TopNQueries'; +import {QUERY_INSIGHTS, MetricSettings, GroupBySettings} from '../TopNQueries/TopNQueries'; const Configuration = ({ latencySettings, cpuSettings, memorySettings, + groupBySettings, configInfo, core, }: { latencySettings: MetricSettings; cpuSettings: MetricSettings; memorySettings: MetricSettings; + groupBySettings: GroupBySettings; configInfo: any; core: CoreStart; }) => { @@ -57,6 +59,11 @@ const Configuration = ({ { value: '30', text: '30' }, ]; + const groupByOptions = [ + { value: 'none', text: 'None' }, + { value: 'similarity', text: 'Similarity' } + ]; + const history = useHistory(); const location = useLocation(); @@ -65,15 +72,25 @@ const Configuration = ({ const [topNSize, setTopNSize] = useState(latencySettings.currTopN); const [windowSize, setWindowSize] = useState(latencySettings.currWindowSize); const [time, setTime] = useState(latencySettings.currTimeUnit); + const [groupBy, setGroupBy] = useState(groupBySettings.groupBy); - const metricSettingsMap = useMemo( - () => ({ + const [metricSettingsMap, setMetricSettingsMap] = useState({ + latency: latencySettings, + cpu: cpuSettings, + memory: memorySettings, + groupBy: groupBySettings + }); + + useEffect(() => { + setMetricSettingsMap({ latency: latencySettings, cpu: cpuSettings, memory: memorySettings, - }), - [latencySettings, cpuSettings, memorySettings] - ); + groupBy: groupBySettings + }); + + setGroupBy(groupBySettings.groupBy); + }, [latencySettings, cpuSettings, memorySettings, groupBySettings]); const newOrReset = useCallback(() => { const currMetric = metricSettingsMap[metric]; @@ -85,7 +102,7 @@ const Configuration = ({ useEffect(() => { newOrReset(); - }, [newOrReset]); + }, [newOrReset, metricSettingsMap]); useEffect(() => { core.chrome.setBreadcrumbs([ @@ -122,6 +139,10 @@ const Configuration = ({ setTime(e.target.value); }; + const onGroupByChange = (e: any) => { + setGroupBy(e.target.value); + }; + const MinutesBox = () => ( { const nVal = parseInt(topNSize, 10); @@ -317,6 +339,66 @@ const Configuration = ({ + + + + + + + +

Top n queries grouping configuration settings

+
+
+
+ + + + +

Group By

+
+ + Specify the group by type. + +
+ + + + + +
+
+
+
+
+ + + + + +

Statuses for group by

+
+
+
+ + + + Group By + + + + {groupBySettings.groupBy == 'similarity' ? enabledSymb : disabledSymb} + + + +
+
+
{isChanged && isValid ? ( @@ -332,7 +414,7 @@ const Configuration = ({ size="s" iconType="check" onClick={() => { - configInfo(false, isEnabled, metric, topNSize, windowSize, time); + configInfo(false, isEnabled, metric, topNSize, windowSize, time, groupBy); return history.push(QUERY_INSIGHTS); }} > diff --git a/public/pages/QueryDetails/Components/QuerySummary.tsx b/public/pages/QueryDetails/Components/QuerySummary.tsx index cd10517..1463d75 100644 --- a/public/pages/QueryDetails/Components/QuerySummary.tsx +++ b/public/pages/QueryDetails/Components/QuerySummary.tsx @@ -43,16 +43,30 @@ const QuerySummary = ({ query }: { query: SearchQueryRecord }) => { - + + - diff --git a/public/pages/QueryGroupDetails/Components/QueryGroupAggregateSummary.test.tsx b/public/pages/QueryGroupDetails/Components/QueryGroupAggregateSummary.test.tsx new file mode 100644 index 0000000..05fb406 --- /dev/null +++ b/public/pages/QueryGroupDetails/Components/QueryGroupAggregateSummary.test.tsx @@ -0,0 +1,97 @@ +import { render, screen } from '@testing-library/react'; +import QueryGroupAggregateSummary from './QueryGroupAggregateSummary'; +import React from "react"; +import {mockQueries} from "../../../../test/mocks/mockQueries"; +import '@testing-library/jest-dom/extend-expect'; +import {MemoryRouter, Route} from "react-router-dom"; + +describe('QueryGroupAggregateSummary', () => { + const expectedHash = '8c1e50c035663459d567fa11d8eb494d'; + + it('renders aggregate summary correctly', () => { + render( + + + + + + ); + + expect(screen.getByText('Aggregate summary for 8 queries')).toBeInTheDocument(); + expect(screen.getByText('Query Hashcode')).toBeInTheDocument(); + expect(screen.getByText('Latency')).toBeInTheDocument(); + expect(screen.getByText('CPU Time')).toBeInTheDocument(); + expect(screen.getByText('Memory Usage')).toBeInTheDocument(); + expect(screen.getByText('Group by')).toBeInTheDocument(); + }); + + it('calculates and displays correct latency', () => { + render( + + + + + + ); + + const latency = '2.50 ms'; + expect(screen.getByText(latency)).toBeInTheDocument(); + }); + + it('calculates and displays correct CPU time', () => { + render( + + + + + + ); + + const cpuTime = '1.42 ms'; + expect(screen.getByText(cpuTime)).toBeInTheDocument(); + }); + + it('calculates and displays correct memory usage', () => { + render( + + + + + + ); + + const memoryUsage = '132224 B'; + expect(screen.getByText(memoryUsage)).toBeInTheDocument(); + }); + + it('displays correct query hashcode', () => { + render( + + + + + + ); + + const hashcode = mockQueries[0].query_hashcode; + expect(screen.getByText(hashcode)).toBeInTheDocument(); + }); + + it('displays correct group_by value when SIMILARITY', () => { + const queryWithSimilarity = { + ...mockQueries[0], + group_by: 'SIMILARITY', + }; + + render( + + + + + + ); + + expect(screen.getByText('Group by')).toBeInTheDocument(); + expect(screen.getByText('SIMILARITY')).toBeInTheDocument(); // Verifies the "group_by" value is rendered + }); +}); diff --git a/public/pages/QueryGroupDetails/Components/QueryGroupAggregateSummary.tsx b/public/pages/QueryGroupDetails/Components/QueryGroupAggregateSummary.tsx new file mode 100644 index 0000000..1f65798 --- /dev/null +++ b/public/pages/QueryGroupDetails/Components/QueryGroupAggregateSummary.tsx @@ -0,0 +1,75 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiFlexGrid, EuiFlexItem, EuiHorizontalRule, EuiPanel, EuiText } from '@elastic/eui'; +import { + CPU_TIME, GROUP_BY, + LATENCY, + MEMORY_USAGE, QUERY_HASHCODE +} from '../../../../common/constants'; + +// Panel component for displaying query group detail values +const PanelItem = ({ label, value }: { label: string; value: string | number }) => ( + + +

{label}

+
+ {value} +
+); + +const QueryGroupAggregateSummary = ({ query }: { query: any }) => { + + const { measurements, query_hashcode, group_by } = query; + const queryCount = measurements.latency?.count || measurements.cpu?.count || measurements.memory?.count || 1; + return ( + + +

+ Aggregate summary for {queryCount} {queryCount === 1 ? 'query' : 'queries'} +

+
+ + + + + + + + +
+ ); +}; + +export default QueryGroupAggregateSummary; diff --git a/public/pages/QueryGroupDetails/Components/QueryGroupSampleQuerySummary.test.tsx b/public/pages/QueryGroupDetails/Components/QueryGroupSampleQuerySummary.test.tsx new file mode 100644 index 0000000..620cafd --- /dev/null +++ b/public/pages/QueryGroupDetails/Components/QueryGroupSampleQuerySummary.test.tsx @@ -0,0 +1,89 @@ +import { render, screen } from '@testing-library/react'; +import React from "react"; +import {mockQueries} from "../../../../test/mocks/mockQueries"; +import {MemoryRouter, Route} from "react-router-dom"; +import QueryGroupSampleQuerySummary from "./QueryGroupSampleQuerySummary"; +import '@testing-library/jest-dom/extend-expect'; + +describe('QueryGroupSampleQuerySummary', () => { + const expectedHash = '8c1e50c035663459d567fa11d8eb494d'; + + it('renders sample query summary correctly', () => { + render( + + + + + + ); + + expect(screen.getByText('Sample query summary')).toBeInTheDocument(); + expect(screen.getByText('Timestamp')).toBeInTheDocument(); + expect(screen.getByText('Indices')).toBeInTheDocument(); + expect(screen.getByText('Search type')).toBeInTheDocument(); + expect(screen.getByText('Coordinator node ID')).toBeInTheDocument(); + expect(screen.getByText('Total shards')).toBeInTheDocument(); + }); + + it('displays correct indices value', () => { + render( + + + + + + ); + + const indices = mockQueries[0].indices.join(', '); + expect(screen.getByText(indices)).toBeInTheDocument(); + }); + + it('displays correct search type', () => { + render( + + + + + + ); + + expect(screen.getByText('query then fetch')).toBeInTheDocument(); + }); + + it('displays correct node ID', () => { + render( + + + + + + ); + + expect(screen.getByText(mockQueries[0].node_id)).toBeInTheDocument(); + }); + + it('displays correct total shards', () => { + render( + + + + + + ); + + expect(screen.getByText(mockQueries[0].total_shards)).toBeInTheDocument(); + }); + + it('formats timestamp correctly', () => { + render( + + + + + + ); + + const formattedTimestamp = 'Nov 15, 2024 @ 12:36:12 PM'; + expect(screen.getByText(formattedTimestamp)).toBeInTheDocument(); + }); +}); diff --git a/public/pages/QueryGroupDetails/Components/QueryGroupSampleQuerySummary.tsx b/public/pages/QueryGroupDetails/Components/QueryGroupSampleQuerySummary.tsx new file mode 100644 index 0000000..ae52f60 --- /dev/null +++ b/public/pages/QueryGroupDetails/Components/QueryGroupSampleQuerySummary.tsx @@ -0,0 +1,51 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiFlexGrid, EuiFlexItem, EuiHorizontalRule, EuiPanel, EuiText } from '@elastic/eui'; +import { + INDICES, + NODE_ID, + SEARCH_TYPE, + TIMESTAMP, + TOTAL_SHARDS, +} from '../../../../common/constants'; + +const PanelItem = ({ label, value }: { label: string; value: string | number }) => ( + + +

{label}

+
+ {value} +
+); + +const QueryGroupSampleQuerySummary = ({ query }: { query: any }) => { + const convertTime = (unixTime: number) => { + const date = new Date(unixTime); + const loc = date.toDateString().split(' '); + return `${loc[1]} ${loc[2]}, ${loc[3]} @ ${date.toLocaleTimeString('en-US')}`; + }; + + + const { timestamp, indices, search_type, node_id, total_shards } = query; + return ( + + +

Sample query summary

+
+ + + + + + + + +
+ ); +}; + +export default QueryGroupSampleQuerySummary; diff --git a/public/pages/QueryGroupDetails/QueryGroupDetails.test.tsx b/public/pages/QueryGroupDetails/QueryGroupDetails.test.tsx new file mode 100644 index 0000000..145daad --- /dev/null +++ b/public/pages/QueryGroupDetails/QueryGroupDetails.test.tsx @@ -0,0 +1,107 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import {MemoryRouter, Route} from 'react-router-dom'; +import QueryGroupDetails from './QueryGroupDetails'; +import { CoreStart } from 'opensearch-dashboards/public'; +import React from 'react'; +import { mockQueries } from '../../../test/mocks/mockQueries'; +import '@testing-library/jest-dom/extend-expect'; +import hash from 'object-hash'; + +jest.mock('object-hash', () => jest.fn(() => '8c1e50c035663459d567fa11d8eb494d')); + +jest.mock('plotly.js-dist', () => ({ + newPlot: jest.fn(), + react: jest.fn(), + relayout: jest.fn(), +})); + +jest.mock('react-ace', () => ({ + __esModule: true, + default: () =>
Mocked Ace Editor
, +})); + +describe('QueryGroupDetails', () => { + const coreMock = { + chrome: { + setBreadcrumbs: jest.fn(), + }, + } as unknown as CoreStart; + + const expectedHash = '8c1e50c035663459d567fa11d8eb494d'; + + it('renders the QueryGroupDetails component', async () => { + render( + + + + + + ); + + expect(screen.getByText('Query group details')).toBeInTheDocument(); + expect(screen.getByText('Sample query details')).toBeInTheDocument(); + + await waitFor(() => { + expect(coreMock.chrome.setBreadcrumbs).toHaveBeenCalledWith([ + { text: 'Query insights', href: '/queryInsights' , onClick: expect.any(Function)}, + { text: 'Query group details: Nov 15, 2024 @ 12:36:12 PM' }, + ]); + }); + }); + + it('renders latency bar chart', async () => { + render( + + + + + + ); + const latencyElements = await screen.findAllByText(/Latency/i); + + expect(latencyElements.length).toBe(2); + }); + + it('displays query details', () => { + render( + + + + + + ); + + expect(screen.getByText('Open in search comparision')).toBeInTheDocument(); + }); + + it('should find the query based on hash', () => { + const expectedQuery = mockQueries.find((q: any) => hash(q) === expectedHash); + + expect(expectedQuery).not.toBeUndefined(); + if (expectedQuery) { + expect(expectedQuery.query_hashcode).toBe(expectedHash); + } else { + throw new Error(`Query with hash ${expectedHash} was not found in mockQueries`); + } + }); + + it('renders correct breadcrumb based on query hash', async () => { + render( + + + + + + ); + + await waitFor(() => { + expect(coreMock.chrome.setBreadcrumbs).toHaveBeenCalledWith(expect.arrayContaining([ + { text: 'Query insights', href: '/queryInsights' , onClick: expect.any(Function)}, + { text: 'Query group details: Nov 15, 2024 @ 12:36:12 PM' }, + expect.objectContaining({ + text: expect.stringMatching(/^Query group details: .+/) + }) + ])); + }); + }); +}); diff --git a/public/pages/QueryGroupDetails/QueryGroupDetails.tsx b/public/pages/QueryGroupDetails/QueryGroupDetails.tsx new file mode 100644 index 0000000..58c4125 --- /dev/null +++ b/public/pages/QueryGroupDetails/QueryGroupDetails.tsx @@ -0,0 +1,180 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CoreStart } from 'opensearch-dashboards/public'; +// @ts-ignore +import Plotly from 'plotly.js-dist'; +import hash from 'object-hash'; +import { useParams, useHistory, useLocation } from 'react-router-dom'; +import React, {useEffect} from "react"; +import {QUERY_INSIGHTS} from "../TopNQueries/TopNQueries"; +import { + EuiButton, + EuiCodeBlock, + EuiFlexGrid, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiPanel, + EuiSpacer, + EuiText, + EuiTitle, + EuiIconTip +} from '@elastic/eui'; +import QueryGroupAggregateSummary from "./Components/QueryGroupAggregateSummary"; +import QueryGroupSampleQuerySummary from "./Components/QueryGroupSampleQuerySummary"; + +const QueryGroupDetails = ({ queries, core }: { queries: any; core: CoreStart }) => { + const { hashedQuery } = useParams<{ hashedQuery: string }>(); + const query = queries.find((q: any) => hash(q) === hashedQuery); + + const convertTime = (unixTime: number) => { + const date = new Date(unixTime); + const loc = date.toDateString().split(' '); + return loc[1] + ' ' + loc[2] + ', ' + loc[3] + ' @ ' + date.toLocaleTimeString('en-US'); + }; + + const history = useHistory(); + const location = useLocation(); + + useEffect(() => { + core.chrome.setBreadcrumbs([ + { + text: 'Query insights', + href: QUERY_INSIGHTS, + onClick: (e) => { + e.preventDefault(); + history.push(QUERY_INSIGHTS); + }, + }, + { text: `Query group details: ${convertTime(query.timestamp)}` }, + ]); + }, [core.chrome, history, location, query.timestamp]); + + useEffect(() => { + let x: number[] = Object.values(query.phase_latency_map); + if (x.length < 3) { + x = [0, 0, 0]; + } + const data = [ + { + x: x.reverse(), + y: ['Fetch ', 'Query ', 'Expand '], + type: 'bar', + orientation: 'h', + width: 0.5, + marker: { color: ['#F990C0', '#1BA9F5', '#7DE2D1'] }, + base: [x[2] + x[1], x[2], 0], + text: x.map((value) => `${value}ms`), + textposition: 'outside', + cliponaxis: false, + }, + ]; + const layout = { + autosize: true, + margin: { l: 80, r: 80, t: 25, b: 15, pad: 0 }, + autorange: true, + height: 120, + xaxis: { + side: 'top', + zeroline: false, + ticksuffix: 'ms', + autorangeoptions: { clipmin: 0 }, + tickfont: { color: '#535966' }, + linecolor: '#D4DAE5', + gridcolor: '#D4DAE5', + }, + yaxis: { linecolor: '#D4DAE5' }, + }; + const config = { responsive: true }; + Plotly.newPlot('latency', data, layout, config); + }, [query]); + + const queryString = JSON.stringify(JSON.parse(JSON.stringify(query.source)), null, 2); + const queryDisplay = `{\n "query": ${queryString ? queryString.replace(/\n/g, '\n ') : ''}\n}`; + + return ( +
+ + +

Query group details

+
+ +
+ + + + + + + + +

Sample query details

+
+ +
+ + + + + + + + + + +

Query

+
+
+ + + Open in search comparision + + +
+ + + + {queryDisplay} + +
+
+ + + +

Latency

+
+ +
+ + + + +
+ ); + +}; +export default QueryGroupDetails; diff --git a/public/pages/QueryInsights/QueryInsights.tsx b/public/pages/QueryInsights/QueryInsights.tsx index e5b7e93..40fe2f3 100644 --- a/public/pages/QueryInsights/QueryInsights.tsx +++ b/public/pages/QueryInsights/QueryInsights.tsx @@ -15,10 +15,11 @@ import { INDICES, LATENCY, MEMORY_USAGE, - NODE_ID, + NODE_ID, QUERY_COUNT, QUERY_HASHCODE, SEARCH_TYPE, TIMESTAMP, TOTAL_SHARDS, + TYPE, } from '../../../common/constants'; const TIMESTAMP_FIELD = 'timestamp'; @@ -28,6 +29,7 @@ const SEARCH_TYPE_FIELD = 'search_type'; const NODE_ID_FIELD = 'node_id'; const TOTAL_SHARDS_FIELD = 'total_shards'; const METRIC_DEFAULT_MSG = 'Not enabled'; +const GROUP_BY_FIELD = 'group_by' const QueryInsights = ({ queries, @@ -70,16 +72,63 @@ const QueryInsights = ({ }; const cols: Array> = [ + { + name: QUERY_HASHCODE, + render: (query: any) => { + return ( + + { + const route = query.group_by === 'SIMILARITY' ? `/query-group-details/${hash(query)}` : `/query-details/${hash(query)}`; + history.push(route); + }}> + {query.query_hashcode || '-'} + + + ); + }, + sortable: (query) => query.query_hashcode || '-', + truncateText: true, + }, + { + name: TYPE, + render: (query: any) => { + return ( + + { + const route = query.group_by === 'SIMILARITY' ? `/query-group-details/${hash(query)}` : `/query-details/${hash(query)}`; + history.push(route); + }}> + {query.group_by === 'SIMILARITY' ? 'group' : 'query'} + + + ); + }, + sortable: (query) => query.group_by || 'query', + truncateText: true, + }, + { + field: MEASUREMENTS_FIELD, + name: QUERY_COUNT, + render: (measurements: any) => `${measurements?.latency?.count || measurements?.cpu?.count || measurements?.memory?.count || 1}`, + sortable: (measurements: any) => { + return Number(measurements?.latency?.count || measurements?.cpu?.count || measurements?.memory?.count || 1); + }, + truncateText: true, + }, { // Make into flyout instead? name: TIMESTAMP, render: (query: any) => { + const isQuery = query.group_by === 'NONE'; + const linkContent = isQuery ? convertTime(query.timestamp): '-'; + const onClickHandler = () => history.push(`/query-details/${hash(query)}`); + return ( - history.push(`/query-details/${hash(query)}`)}> - {convertTime(query.timestamp)} - - + + {linkContent} + + ); }, sortable: (query) => query.timestamp, @@ -90,7 +139,11 @@ const QueryInsights = ({ name: LATENCY, render: (measurements: any) => { const latencyValue = measurements?.latency?.number; - return latencyValue !== undefined ? `${latencyValue} ms` : METRIC_DEFAULT_MSG; + const latencyCount = measurements?.latency?.count; + const result = latencyValue !== undefined && latencyCount !== undefined + ? (latencyValue / latencyCount).toFixed(2) + : METRIC_DEFAULT_MSG; + return `${result} ms`; }, sortable: true, truncateText: true, @@ -98,9 +151,13 @@ const QueryInsights = ({ { field: MEASUREMENTS_FIELD, name: CPU_TIME, - render: (measurements: { cpu?: { number?: number } }) => { + render: (measurements: any) => { const cpuValue = measurements?.cpu?.number; - return cpuValue !== undefined ? `${cpuValue / 1000000} ms` : METRIC_DEFAULT_MSG; + const cpuCount = measurements?.cpu?.count; + const result = cpuValue !== undefined && cpuCount !== undefined + ? (cpuValue / cpuCount / 1000000).toFixed(2) + : METRIC_DEFAULT_MSG; + return `${result} ms`; }, sortable: true, truncateText: true, @@ -108,9 +165,13 @@ const QueryInsights = ({ { field: MEASUREMENTS_FIELD, name: MEMORY_USAGE, - render: (measurements: { memory?: { number?: number } }) => { + render: (measurements: any) => { const memoryValue = measurements?.memory?.number; - return memoryValue !== undefined ? `${memoryValue} B` : METRIC_DEFAULT_MSG; + const memoryCount = measurements?.memory?.count; + const result = memoryValue !== undefined && memoryCount !== undefined + ? (memoryValue / memoryCount).toFixed(2) + : METRIC_DEFAULT_MSG; + return `${result} B`; }, sortable: true, truncateText: true, @@ -118,26 +179,40 @@ const QueryInsights = ({ { field: INDICES_FIELD, name: INDICES, - render: (indices: string[]) => Array.from(new Set(indices.flat())).join(', '), + render: (indices: string[], query: any) => { + const isSimilarity = query.group_by === 'SIMILARITY'; + return isSimilarity ? '-' : Array.from(new Set(indices.flat())).join(', '); + }, sortable: true, truncateText: true, }, { field: SEARCH_TYPE_FIELD, name: SEARCH_TYPE, - render: (searchType: string) => searchType.replaceAll('_', ' '), + render: (searchType: string, query: any) => { + const isSimilarity = query.group_by === 'SIMILARITY'; + return isSimilarity ? '-' : searchType.replaceAll('_', ' '); + }, sortable: true, truncateText: true, }, { field: NODE_ID_FIELD, name: NODE_ID, + render: (nodeId: string, query: any) => { + const isSimilarity = query.group_by === 'SIMILARITY'; + return isSimilarity ? '-' : nodeId; + }, sortable: true, truncateText: true, }, { field: TOTAL_SHARDS_FIELD, name: TOTAL_SHARDS, + render: (totalShards: number, query: any) => { + const isSimilarity = query.group_by === 'SIMILARITY'; + return isSimilarity ? '-' : totalShards; + }, sortable: true, truncateText: true, }, @@ -171,6 +246,25 @@ const QueryInsights = ({ schema: false, }, filters: [ + { + type: 'field_value_selection', + field: GROUP_BY_FIELD, + name: TYPE, + multiSelect: true, + options: [ + { + value: 'NONE', + name: 'query', + view: 'query' + }, + { + value: 'SIMILARITY', + name: 'group', + view: 'group' + }, + ], + noOptionsMessage: 'No data available for the selected type', // Fallback message when no queries match + }, { type: 'field_value_selection', field: INDICES_FIELD, @@ -236,6 +330,7 @@ const QueryInsights = ({ ], }} allowNeutralSort={false} + itemId={(query) => `${query.query_hashcode}-${query.timestamp}`} /> ); }; diff --git a/public/pages/TopNQueries/TopNQueries.tsx b/public/pages/TopNQueries/TopNQueries.tsx index cc6a4cf..43e2ced 100644 --- a/public/pages/TopNQueries/TopNQueries.tsx +++ b/public/pages/TopNQueries/TopNQueries.tsx @@ -12,6 +12,8 @@ import QueryInsights from '../QueryInsights/QueryInsights'; import Configuration from '../Configuration/Configuration'; import QueryDetails from '../QueryDetails/QueryDetails'; import { SearchQueryRecord } from '../../../types/types'; +import QueryGroupDetails from "../QueryGroupDetails/QueryGroupDetails"; + export const QUERY_INSIGHTS = '/queryInsights'; export const CONFIGURATION = '/configuration'; @@ -23,6 +25,10 @@ export interface MetricSettings { currTimeUnit: string; } +export interface GroupBySettings { + groupBy: string +} + const TopNQueries = ({ core, initialStart = 'now-1d', @@ -44,23 +50,25 @@ const TopNQueries = ({ isEnabled: false, currTopN: '', currWindowSize: '', - currTimeUnit: 'HOURS', + currTimeUnit: 'HOURS' }); const [cpuSettings, setCpuSettings] = useState({ isEnabled: false, currTopN: '', currWindowSize: '', - currTimeUnit: 'HOURS', + currTimeUnit: 'HOURS' }); const [memorySettings, setMemorySettings] = useState({ isEnabled: false, currTopN: '', currWindowSize: '', - currTimeUnit: 'HOURS', + currTimeUnit: 'HOURS' }); + const [groupBySettings, setGroupBySettings] = useState({ groupBy: 'none' }); + const setMetricSettings = (metricType: string, updates: Partial) => { switch (metricType) { case 'latency': @@ -175,12 +183,13 @@ const TopNQueries = ({ metric: string = '', newTopN: string = '', newWindowSize: string = '', - newTimeUnit: string = '' + newTimeUnit: string = '', + newGroupBy: string = '' ) => { if (get) { try { const resp = await core.http.get('/api/settings'); - const { latency, cpu, memory } = + const { latency, cpu, memory, group_by } = resp?.response?.persistent?.search?.insights?.top_queries || {}; if (latency !== undefined && latency.enabled === 'true') { const [time, timeUnits] = latency.window_size @@ -215,6 +224,9 @@ const TopNQueries = ({ currTimeUnit: timeUnits === 'm' ? 'MINUTES' : 'HOURS', }); } + if (group_by) { + setGroupBySettings({ groupBy: group_by }); + } } catch (error) { console.error('Failed to retrieve settings:', error); } @@ -226,12 +238,14 @@ const TopNQueries = ({ currWindowSize: newWindowSize, currTimeUnit: newTimeUnit, }); + setGroupBySettings({ groupBy: newGroupBy }) await core.http.put('/api/update_settings', { query: { metric, enabled, top_n_size: newTopN, window_size: `${newWindowSize}${newTimeUnit === 'MINUTES' ? 'm' : 'h'}`, + group_by: newGroupBy }, }); } catch (error) { @@ -269,6 +283,9 @@ const TopNQueries = ({ + + +

Query insights - Top N queries

@@ -297,6 +314,7 @@ const TopNQueries = ({ latencySettings={latencySettings} cpuSettings={cpuSettings} memorySettings={memorySettings} + groupBySettings={groupBySettings} configInfo={retrieveConfigInfo} core={core} /> diff --git a/server/routes/index.ts b/server/routes/index.ts index 6e06207..93496b7 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -181,6 +181,7 @@ export function defineRoutes(router: IRouter) { enabled: schema.maybe(schema.boolean({ defaultValue: false })), top_n_size: schema.maybe(schema.string({ defaultValue: '' })), window_size: schema.maybe(schema.string({ defaultValue: '' })), + group_by: schema.maybe(schema.string({ defaultValue: '' })), }), }, }, @@ -195,6 +196,7 @@ export function defineRoutes(router: IRouter) { [`search.insights.top_queries.${query.metric}.enabled`]: query.enabled, [`search.insights.top_queries.${query.metric}.top_n_size`]: query.top_n_size, [`search.insights.top_queries.${query.metric}.window_size`]: query.window_size, + [`search.insights.top_queries.group_by`]: query.group_by }, }, }; diff --git a/test/mocks/mockQueries.ts b/test/mocks/mockQueries.ts new file mode 100644 index 0000000..7e8a9cb --- /dev/null +++ b/test/mocks/mockQueries.ts @@ -0,0 +1,66 @@ +export const mockQueries = [ + { + timestamp: 1731702972708, // Example timestamp in milliseconds + search_type: 'query_then_fetch', + indices: ['my-index'], + group_by: 'SIMILARITY', + phase_latency_map: { + expand: 0, + query: 5, + fetch: 0, + }, + labels: {}, + source: { + size: 0, + aggregations: { + average_age: { + avg: { + field: 'age', + }, + }, + }, + }, + node_id: 'HjvgxQ4AQTiddd43OV7pJA', + task_resource_usages: [ + { + action: 'indices:data/read/search[phase/query]', + taskId: 82340, + parentTaskId: 82339, + nodeId: 'HjvgxQ4AQTiddd43OV7pJA', + taskResourceUsage: { + cpuTimeInNanos: 3335000, + memoryInBytes: 10504, + }, + }, + { + action: 'indices:data/read/search', + taskId: 82339, + parentTaskId: -1, + nodeId: 'HjvgxQ4AQTiddd43OV7pJA', + taskResourceUsage: { + cpuTimeInNanos: 690000, + memoryInBytes: 6080, + }, + }, + ], + query_hashcode: '8c1e50c035663459d567fa11d8eb494d', + total_shards: 1, + measurements: { + latency: { + number: 20, + count: 8, + aggregationType: 'AVERAGE', + }, + memory: { + number: 132224, + count: 8, + aggregationType: 'AVERAGE', + }, + cpu: { + number: 11397000, + count: 8, + aggregationType: 'AVERAGE', + }, + }, + }, +]; diff --git a/types/types.ts b/types/types.ts index 01f2f59..8efc554 100644 --- a/types/types.ts +++ b/types/types.ts @@ -20,6 +20,9 @@ export interface SearchQueryRecord { indices: string[]; phase_latency_map: PhaseLatencyMap; task_resource_usages: Task[]; + query_hashcode: string; + group_by: string; + } export interface Measurement {