diff --git a/.github/workflows/cypress-tests.yml b/.github/workflows/cypress-tests.yml
index 733c572..245cdf8 100644
--- a/.github/workflows/cypress-tests.yml
+++ b/.github/workflows/cypress-tests.yml
@@ -1,154 +1,154 @@
-name: Cypress e2e integration tests workflow
-on:
- pull_request:
- branches:
- - "*"
- push:
- branches:
- - "*"
-env:
- OPENSEARCH_DASHBOARDS_VERSION: '2.x'
- QUERY_INSIGHTS_BRANCH: '2.x'
- GRADLE_VERSION: '7.6.1'
-jobs:
- tests:
- name: Run Cypress E2E tests
- strategy:
- matrix:
- os: [ubuntu-latest, windows-latest]
- include:
- - os: windows-latest
- cypress_cache_folder: ~/AppData/Local/Cypress/Cache
- - os: ubuntu-latest
- cypress_cache_folder: ~/.cache/Cypress
- runs-on: ${{ matrix.os }}
- env:
- # prevents extra Cypress installation progress messages
- CI: 1
- # avoid warnings like "tput: No value for $TERM and no -T specified"
- TERM: xterm
- steps:
- - name: Set up JDK
- uses: actions/setup-java@v1
- with:
- java-version: 21
-
- - name: Enable longer filenames
- if: ${{ matrix.os == 'windows-latest' }}
- run: git config --system core.longpaths true
-
- - name: Checkout Query Insights
- uses: actions/checkout@v2
- with:
- path: query-insights
- repository: opensearch-project/query-insights
- ref: ${{ env.QUERY_INSIGHTS_BRANCH }}
-
- - name: Set up Gradle
- uses: gradle/gradle-build-action@v2
- with:
- gradle-version: ${{ env.GRADLE_VERSION }}
-
- - name: Run OpenSearch with Query Insights plugin
- run: |
- cd query-insights
- ./gradlew run &
- timeout 300 bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' localhost:9200)" != "200" ]]; do sleep 5; done'
- shell: bash
-
- - name: Checkout OpenSearch-Dashboards
- uses: actions/checkout@v2
- with:
- repository: opensearch-project/OpenSearch-Dashboards
- path: OpenSearch-Dashboards
- ref: ${{ env.OPENSEARCH_DASHBOARDS_VERSION }}
-
- - name: Checkout Query Insights Dashboards plugin
- uses: actions/checkout@v2
- with:
- path: OpenSearch-Dashboards/plugins/query-insights-dashboards
-
- - name: Setup Node
- uses: actions/setup-node@v3
- with:
- node-version-file: './OpenSearch-Dashboards/.nvmrc'
- registry-url: 'https://registry.npmjs.org'
-
- - name: Install Yarn
- # Need to use bash to avoid having a windows/linux specific step
- shell: bash
- run: |
- YARN_VERSION=$(node -p "require('./OpenSearch-Dashboards/package.json').engines.yarn")
- echo "Installing yarn@$YARN_VERSION"
- npm i -g yarn@$YARN_VERSION
- - run: node -v
- - run: yarn -v
-
- - name: Bootstrap plugin/OpenSearch-Dashboards
- run: |
- cd OpenSearch-Dashboards/plugins/query-insights-dashboards
- yarn osd bootstrap --single-version=loose
-
- - name: Run OpenSearch-Dashboards server
- run: |
- cd OpenSearch-Dashboards
- yarn start --no-base-path --no-watch --server.host="0.0.0.0" &
- shell: bash
-
- # Window is slow so wait longer
- - name: Sleep until OSD server starts - windows
- if: ${{ matrix.os == 'windows-latest' }}
- run: Start-Sleep -s 600
- shell: powershell
-
- - name: Sleep until OSD server starts - non-windows
- if: ${{ matrix.os != 'windows-latest' }}
- run: sleep 500
- shell: bash
-
- - name: Install Cypress
- run: |
- cd OpenSearch-Dashboards/plugins/query-insights-dashboards
- # This will install Cypress in case the binary is missing which can happen on Windows and Mac
- # If the binary exists, this will exit quickly so it should not be an expensive operation
- npx cypress install
- shell: bash
-
- - name: Get Cypress version
- id: cypress_version
- run: |
- cd OpenSearch-Dashboards/plugins/query-insights-dashboards
- echo "::set-output name=cypress_version::$(cat ./package.json | jq '.dependencies.cypress' | tr -d '"')"
-
- - name: Cache Cypress
- id: cache-cypress
- uses: actions/cache@v2
- with:
- path: ${{ matrix.cypress_cache_folder }}
- key: cypress-cache-v2-${{ matrix.os }}-${{ hashFiles('OpenSearch-Dashboards/plugins/query-insights-dashboards/package.json') }}
-
- # for now just chrome, use matrix to do all browsers later
- - name: Cypress tests
- uses: cypress-io/github-action@v5
- with:
- working-directory: OpenSearch-Dashboards/plugins/query-insights-dashboards
- command: yarn run cypress run
- wait-on: 'http://localhost:5601'
- wait-on-timeout: 600
- browser: chrome
- env:
- CYPRESS_CACHE_FOLDER: ${{ matrix.cypress_cache_folder }}
-
- # Screenshots are only captured on failure, will change this once we do visual regression tests
- - uses: actions/upload-artifact@v4
- if: failure()
- with:
- name: cypress-screenshots-${{ matrix.os }}
- path: OpenSearch-Dashboards/plugins/query-insights-dashboards/cypress/screenshots
-
- # Test run video was always captured, so this action uses "always()" condition
- - uses: actions/upload-artifact@v4
- if: always()
- with:
- name: cypress-videos-${{ matrix.os }}
- path: OpenSearch-Dashboards/plugins/query-insights-dashboards/cypress/videos
+name: Cypress e2e integration tests workflow
+on:
+ pull_request:
+ branches:
+ - "*"
+ push:
+ branches:
+ - "*"
+env:
+ OPENSEARCH_DASHBOARDS_VERSION: '2.x'
+ QUERY_INSIGHTS_BRANCH: '2.x'
+ GRADLE_VERSION: '7.6.1'
+jobs:
+ tests:
+ name: Run Cypress E2E tests
+ strategy:
+ matrix:
+ os: [ubuntu-latest, windows-latest]
+ include:
+ - os: windows-latest
+ cypress_cache_folder: ~/AppData/Local/Cypress/Cache
+ - os: ubuntu-latest
+ cypress_cache_folder: ~/.cache/Cypress
+ runs-on: ${{ matrix.os }}
+ env:
+ # prevents extra Cypress installation progress messages
+ CI: 1
+ # avoid warnings like "tput: No value for $TERM and no -T specified"
+ TERM: xterm
+ steps:
+ - name: Set up JDK
+ uses: actions/setup-java@v1
+ with:
+ java-version: 21
+
+ - name: Enable longer filenames
+ if: ${{ matrix.os == 'windows-latest' }}
+ run: git config --system core.longpaths true
+
+ - name: Checkout Query Insights
+ uses: actions/checkout@v2
+ with:
+ path: query-insights
+ repository: opensearch-project/query-insights
+ ref: ${{ env.QUERY_INSIGHTS_BRANCH }}
+
+ - name: Set up Gradle
+ uses: gradle/gradle-build-action@v2
+ with:
+ gradle-version: ${{ env.GRADLE_VERSION }}
+
+ - name: Run OpenSearch with Query Insights plugin
+ run: |
+ cd query-insights
+ ./gradlew run &
+ timeout 300 bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' localhost:9200)" != "200" ]]; do sleep 5; done'
+ shell: bash
+
+ - name: Checkout OpenSearch-Dashboards
+ uses: actions/checkout@v2
+ with:
+ repository: opensearch-project/OpenSearch-Dashboards
+ path: OpenSearch-Dashboards
+ ref: ${{ env.OPENSEARCH_DASHBOARDS_VERSION }}
+
+ - name: Checkout Query Insights Dashboards plugin
+ uses: actions/checkout@v2
+ with:
+ path: OpenSearch-Dashboards/plugins/query-insights-dashboards
+
+ - name: Setup Node
+ uses: actions/setup-node@v3
+ with:
+ node-version-file: './OpenSearch-Dashboards/.nvmrc'
+ registry-url: 'https://registry.npmjs.org'
+
+ - name: Install Yarn
+ # Need to use bash to avoid having a windows/linux specific step
+ shell: bash
+ run: |
+ YARN_VERSION=$(node -p "require('./OpenSearch-Dashboards/package.json').engines.yarn")
+ echo "Installing yarn@$YARN_VERSION"
+ npm i -g yarn@$YARN_VERSION
+ - run: node -v
+ - run: yarn -v
+
+ - name: Bootstrap plugin/OpenSearch-Dashboards
+ run: |
+ cd OpenSearch-Dashboards/plugins/query-insights-dashboards
+ yarn osd bootstrap --single-version=loose
+
+ - name: Run OpenSearch-Dashboards server
+ run: |
+ cd OpenSearch-Dashboards
+ yarn start --no-base-path --no-watch --server.host="0.0.0.0" &
+ shell: bash
+
+ # Window is slow so wait longer
+ - name: Sleep until OSD server starts - windows
+ if: ${{ matrix.os == 'windows-latest' }}
+ run: Start-Sleep -s 600
+ shell: powershell
+
+ - name: Sleep until OSD server starts - non-windows
+ if: ${{ matrix.os != 'windows-latest' }}
+ run: sleep 500
+ shell: bash
+
+ - name: Install Cypress
+ run: |
+ cd OpenSearch-Dashboards/plugins/query-insights-dashboards
+ # This will install Cypress in case the binary is missing which can happen on Windows and Mac
+ # If the binary exists, this will exit quickly so it should not be an expensive operation
+ npx cypress install
+ shell: bash
+
+ - name: Get Cypress version
+ id: cypress_version
+ run: |
+ cd OpenSearch-Dashboards/plugins/query-insights-dashboards
+ echo "::set-output name=cypress_version::$(cat ./package.json | jq '.dependencies.cypress' | tr -d '"')"
+
+ - name: Cache Cypress
+ id: cache-cypress
+ uses: actions/cache@v2
+ with:
+ path: ${{ matrix.cypress_cache_folder }}
+ key: cypress-cache-v2-${{ matrix.os }}-${{ hashFiles('OpenSearch-Dashboards/plugins/query-insights-dashboards/package.json') }}
+
+ # for now just chrome, use matrix to do all browsers later
+ - name: Cypress tests
+ uses: cypress-io/github-action@v5
+ with:
+ working-directory: OpenSearch-Dashboards/plugins/query-insights-dashboards
+ command: yarn run cypress run
+ wait-on: 'http://localhost:5601'
+ wait-on-timeout: 600
+ browser: chrome
+ env:
+ CYPRESS_CACHE_FOLDER: ${{ matrix.cypress_cache_folder }}
+
+ # Screenshots are only captured on failure, will change this once we do visual regression tests
+ - uses: actions/upload-artifact@v4
+ if: failure()
+ with:
+ name: cypress-screenshots-${{ matrix.os }}
+ path: OpenSearch-Dashboards/plugins/query-insights-dashboards/cypress/screenshots
+
+ # Test run video was always captured, so this action uses "always()" condition
+ - uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: cypress-videos-${{ matrix.os }}
+ path: OpenSearch-Dashboards/plugins/query-insights-dashboards/cypress/videos
diff --git a/common/constants.ts b/common/constants.ts
index c685599..3ceae86 100644
--- a/common/constants.ts
+++ b/common/constants.ts
@@ -4,6 +4,9 @@
*/
export const TIMESTAMP = 'Timestamp';
+export const TYPE = 'Type';
+export const ID = 'Id';
+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,7 @@ 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';
+export const AVERAGE_LATENCY = 'Average Latency';
+export const AVERAGE_CPU_TIME = 'Average CPU Time';
+export const AVERAGE_MEMORY_USAGE = 'Average Memory Usage';
diff --git a/cypress/e2e/2_query_details.cy.js b/cypress/e2e/2_query_details.cy.js
index f61c5c3..b70e5e0 100644
--- a/cypress/e2e/2_query_details.cy.js
+++ b/cypress/e2e/2_query_details.cy.js
@@ -28,7 +28,7 @@ describe('Top Queries Details Page', () => {
// waiting for the query insights queue to drain
cy.wait(10000);
cy.navigateToOverview();
- cy.get('.euiTableRow').first().find('button').click(); // Navigate to details
+ cy.get('.euiTableRow').first().find('button').first().click(); // Navigate to details
});
it('should display correct details on the query details page', () => {
@@ -80,7 +80,7 @@ describe('Top Queries Details Page', () => {
.parent()
.next()
.invoke('text')
- .should('match', /^\d+ ms$/);
+ .should('match', /^\d+(\.\d{1,2})? ms$/);
// Validate CPU Time
cy.contains('h4', 'CPU Time')
.parent()
@@ -92,7 +92,7 @@ describe('Top Queries Details Page', () => {
.parent()
.next()
.invoke('text')
- .should('match', /^\d+ B$/);
+ .should('match', /^\d+(\.\d+)? B$/);
// Validate Indices
cy.contains('h4', 'Indices').parent().next().invoke('text').should('not.be.empty');
// Validate Search Type
diff --git a/cypress/e2e/3_configurations.cy.js b/cypress/e2e/3_configurations.cy.js
index 44c2e34..6c9429b 100644
--- a/cypress/e2e/3_configurations.cy.js
+++ b/cypress/e2e/3_configurations.cy.js
@@ -29,7 +29,7 @@ describe('Query Insights Configurations Page', () => {
cy.contains('button', 'Top N queries').should('be.visible');
cy.contains('button', 'Configuration').should('have.class', 'euiTab-isSelected');
// Validate the panels
- cy.get('.euiPanel').should('have.length', 2); // Two panels: configuration settings and statuses
+ cy.get('.euiPanel').should('have.length', 4); // Two panels: configuration settings and statuses
});
/**
@@ -125,7 +125,7 @@ describe('Query Insights Configurations Page', () => {
it('should display statuses for configuration metrics', () => {
// Validate the status panel header
cy.get('.euiPanel')
- .last()
+ .eq(1) // Selects the second panel (index 1)
.within(() => {
cy.get('h2').contains('Statuses for configuration metrics').should('be.visible');
});
diff --git a/public/components/__snapshots__/app.test.tsx.snap b/public/components/__snapshots__/app.test.tsx.snap
index 36c6673..6197159 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`] = `
aria-live="polite"
aria-sort="none"
class="euiTableHeaderCell"
- data-test-subj="tableHeaderCell_Timestamp_0"
+ data-test-subj="tableHeaderCell_Id_0"
+ role="columnheader"
+ scope="col"
+ >
+
+
+
+
+
render(
@@ -42,6 +46,7 @@ const renderConfiguration = (overrides = {}) =>
latencySettings={{ ...defaultLatencySettings, ...overrides }}
cpuSettings={defaultCpuSettings}
memorySettings={defaultMemorySettings}
+ groupBySettings={groupBySettings}
configInfo={mockConfigInfo}
core={mockCoreStart}
/>
@@ -96,7 +101,15 @@ describe('Configuration Component', () => {
fireEvent.change(getTopNSizeConfiguration(), { target: { value: '7' } });
fireEvent.click(screen.getByText('Save'));
await waitFor(() => {
- expect(mockConfigInfo).toHaveBeenCalledWith(false, true, 'latency', '7', '10', 'MINUTES');
+ expect(mockConfigInfo).toHaveBeenCalledWith(
+ false,
+ true,
+ 'latency',
+ '7',
+ '10',
+ 'MINUTES',
+ 'SIMILARITY'
+ );
});
});
diff --git a/public/pages/Configuration/Configuration.tsx b/public/pages/Configuration/Configuration.tsx
index 8882f8f..fec3761 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,39 +24,24 @@ 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';
+import { METRIC_TYPES, TIME_UNITS, MINUTES_OPTIONS, GROUP_BY_OPTIONS } from '../Utils/Constants';
const Configuration = ({
latencySettings,
cpuSettings,
memorySettings,
+ groupBySettings,
configInfo,
core,
}: {
latencySettings: MetricSettings;
cpuSettings: MetricSettings;
memorySettings: MetricSettings;
+ groupBySettings: GroupBySettings;
configInfo: any;
core: CoreStart;
}) => {
- const metricTypes = [
- { value: 'latency', text: 'Latency' },
- { value: 'cpu', text: 'CPU' },
- { value: 'memory', text: 'Memory' },
- ];
-
- const timeUnits = [
- { value: 'MINUTES', text: 'Minute(s)' },
- { value: 'HOURS', text: 'Hour(s)' },
- ];
-
- const minutesOptions = [
- { value: '1', text: '1' },
- { value: '5', text: '5' },
- { value: '10', text: '10' },
- { value: '30', text: '30' },
- ];
-
const history = useHistory();
const location = useLocation();
@@ -65,15 +50,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, setMetricSettingsMap] = useState({
+ latency: latencySettings,
+ cpu: cpuSettings,
+ memory: memorySettings,
+ groupBy: groupBySettings,
+ });
- const metricSettingsMap = useMemo(
- () => ({
+ 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 +80,7 @@ const Configuration = ({
useEffect(() => {
newOrReset();
- }, [newOrReset]);
+ }, [newOrReset, metricSettingsMap]);
useEffect(() => {
core.chrome.setBreadcrumbs([
@@ -122,11 +117,15 @@ const Configuration = ({
setTime(e.target.value);
};
+ const onGroupByChange = (e: any) => {
+ setGroupBy(e.target.value);
+ };
+
const MinutesBox = () => (
@@ -142,18 +141,19 @@ const Configuration = ({
/>
);
- const WindowChoice = time === timeUnits[0].value ? MinutesBox : HoursBox;
+ const WindowChoice = time === TIME_UNITS[0].value ? MinutesBox : HoursBox;
const isChanged =
isEnabled !== metricSettingsMap[metric].isEnabled ||
topNSize !== metricSettingsMap[metric].currTopN ||
windowSize !== metricSettingsMap[metric].currWindowSize ||
- time !== metricSettingsMap[metric].currTimeUnit;
+ time !== metricSettingsMap[metric].currTimeUnit ||
+ groupBy !== metricSettingsMap.groupBy.groupBy;
const isValid = (() => {
const nVal = parseInt(topNSize, 10);
if (nVal < 1 || nVal > 100) return false;
- if (time === timeUnits[0].value) return true;
+ if (time === TIME_UNITS[0].value) return true;
const windowVal = parseInt(windowSize, 10);
return windowVal >= 1 && windowVal <= 24;
})();
@@ -191,7 +191,7 @@ const Configuration = ({
@@ -261,7 +261,7 @@ const Configuration = ({
@@ -317,6 +317,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 ? (
@@ -333,7 +393,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/Configuration/__snapshots__/Configuration.test.tsx.snap b/public/pages/Configuration/__snapshots__/Configuration.test.tsx.snap
index 4034199..091a526 100644
--- a/public/pages/Configuration/__snapshots__/Configuration.test.tsx.snap
+++ b/public/pages/Configuration/__snapshots__/Configuration.test.tsx.snap
@@ -621,6 +621,193 @@ exports[`Configuration Component renders with default settings: should match def
+
+
+
+
+
+
+
+ Statuses for group by
+
+
+
+
+
+
+
`;
@@ -1215,6 +1402,196 @@ exports[`Configuration Component updates state when toggling metrics and enables
+
+
+
+
+
+
+
+ Statuses for group by
+
+
+
+
+
+
+
`;
diff --git a/public/pages/QueryDetails/Components/QuerySummary.tsx b/public/pages/QueryDetails/Components/QuerySummary.tsx
index ad12127..1e4fd62 100644
--- a/public/pages/QueryDetails/Components/QuerySummary.tsx
+++ b/public/pages/QueryDetails/Components/QuerySummary.tsx
@@ -16,6 +16,7 @@ import {
TIMESTAMP,
TOTAL_SHARDS,
} from '../../../../common/constants';
+import { calculateMetric } from '../../Utils/MetricUtils';
// Panel component for displaying query detail values
const PanelItem = ({ label, value }: { label: string; value: string | number }) => (
@@ -43,16 +44,30 @@ const QuerySummary = ({ query }: { query: SearchQueryRecord }) => {
-
+
+
-
diff --git a/public/pages/QueryDetails/__snapshots__/QueryDetails.test.tsx.snap b/public/pages/QueryDetails/__snapshots__/QueryDetails.test.tsx.snap
index fcd5254..b823bcb 100644
--- a/public/pages/QueryDetails/__snapshots__/QueryDetails.test.tsx.snap
+++ b/public/pages/QueryDetails/__snapshots__/QueryDetails.test.tsx.snap
@@ -60,7 +60,7 @@ exports[`QueryDetails component renders the QueryDetails page 1`] = `
{
+ const expectedHash = '8c1e50c035663459d567fa11d8eb494d';
+
+ it('renders aggregate summary correctly', () => {
+ render(
+
+
+
+
+
+ );
+
+ expect(screen.getByText('Aggregate summary for 8 queries')).toBeInTheDocument();
+ expect(screen.getByText('Id')).toBeInTheDocument();
+ expect(screen.getByText('Average Latency')).toBeInTheDocument();
+ expect(screen.getByText('Average CPU Time')).toBeInTheDocument();
+ expect(screen.getByText('Average 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 = '16528.00 B';
+ expect(screen.getByText(memoryUsage)).toBeInTheDocument();
+ });
+
+ it('displays correct query id', () => {
+ render(
+
+
+
+
+
+ );
+
+ const id = mockQueries[0].id;
+ expect(screen.getByText(id)).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..7047782
--- /dev/null
+++ b/public/pages/QueryGroupDetails/Components/QueryGroupAggregateSummary.tsx
@@ -0,0 +1,73 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { EuiFlexGrid, EuiFlexItem, EuiHorizontalRule, EuiPanel, EuiText } from '@elastic/eui';
+import {
+ AVERAGE_CPU_TIME,
+ AVERAGE_LATENCY,
+ AVERAGE_MEMORY_USAGE,
+ GROUP_BY,
+ ID,
+} from '../../../../common/constants';
+import { calculateMetric } from '../../Utils/MetricUtils';
+
+// Panel component for displaying query group detail values
+const PanelItem = ({ label, value }: { label: string; value: string | number }) => (
+
+
+ {label}
+
+ {value}
+
+);
+
+export const QueryGroupAggregateSummary = ({ query }: { query: any }) => {
+ const { measurements, id: id, group_by: groupBy } = query;
+ const queryCount =
+ measurements.latency?.count || measurements.cpu?.count || measurements.memory?.count || 1;
+ return (
+
+
+
+ Aggregate summary for {queryCount} {queryCount === 1 ? 'query' : 'queries'}
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/public/pages/QueryGroupDetails/Components/QueryGroupSampleQuerySummary.test.tsx b/public/pages/QueryGroupDetails/Components/QueryGroupSampleQuerySummary.test.tsx
new file mode 100644
index 0000000..b9583dc
--- /dev/null
+++ b/public/pages/QueryGroupDetails/Components/QueryGroupSampleQuerySummary.test.tsx
@@ -0,0 +1,81 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+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();
+ });
+});
diff --git a/public/pages/QueryGroupDetails/Components/QueryGroupSampleQuerySummary.tsx b/public/pages/QueryGroupDetails/Components/QueryGroupSampleQuerySummary.tsx
new file mode 100644
index 0000000..c1388ab
--- /dev/null
+++ b/public/pages/QueryGroupDetails/Components/QueryGroupSampleQuerySummary.tsx
@@ -0,0 +1,54 @@
+/*
+ * 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}
+
+);
+
+export 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: searchType,
+ node_id: nodeId,
+ total_shards: totalShards,
+ } = query;
+ return (
+
+
+ Sample query summary
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/public/pages/QueryGroupDetails/QueryGroupDetails.test.tsx b/public/pages/QueryGroupDetails/QueryGroupDetails.test.tsx
new file mode 100644
index 0000000..9659c48
--- /dev/null
+++ b/public/pages/QueryGroupDetails/QueryGroupDetails.test.tsx
@@ -0,0 +1,117 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+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: expect.stringMatching(/^Query group details: .+ @ .+$/), // Matches dynamic date/time format
+ },
+ ]);
+ });
+ });
+
+ 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);
+
+ if (!expectedQuery) {
+ throw new Error(`Query with hash ${expectedHash} was not found in mockQueries`);
+ }
+ expect(expectedQuery.id).toBe(expectedHash);
+ });
+
+ 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) },
+ 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..8a08d31
--- /dev/null
+++ b/public/pages/QueryGroupDetails/QueryGroupDetails.tsx
@@ -0,0 +1,178 @@
+/*
+ * 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 {
+ EuiButton,
+ EuiCodeBlock,
+ EuiFlexGrid,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiHorizontalRule,
+ EuiPanel,
+ EuiSpacer,
+ EuiText,
+ EuiTitle,
+ EuiIconTip,
+} from '@elastic/eui';
+import { QUERY_INSIGHTS } from '../TopNQueries/TopNQueries';
+import { QueryGroupAggregateSummary } from './Components/QueryGroupAggregateSummary';
+import { QueryGroupSampleQuerySummary } from './Components/QueryGroupSampleQuerySummary';
+
+export 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
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/public/pages/QueryInsights/QueryInsights.tsx b/public/pages/QueryInsights/QueryInsights.tsx
index e5b7e93..230ae71 100644
--- a/public/pages/QueryInsights/QueryInsights.tsx
+++ b/public/pages/QueryInsights/QueryInsights.tsx
@@ -16,10 +16,14 @@ import {
LATENCY,
MEMORY_USAGE,
NODE_ID,
+ QUERY_COUNT,
+ ID,
SEARCH_TYPE,
TIMESTAMP,
TOTAL_SHARDS,
+ TYPE,
} from '../../../common/constants';
+import { calculateMetric } from '../Utils/MetricUtils';
const TIMESTAMP_FIELD = 'timestamp';
const MEASUREMENTS_FIELD = 'measurements';
@@ -28,6 +32,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,15 +75,81 @@ const QueryInsights = ({
};
const cols: Array
> = [
+ {
+ name: ID,
+ render: (query: SearchQueryRecord) => {
+ return (
+
+ {
+ const route =
+ query.group_by === 'SIMILARITY'
+ ? `/query-group-details/${hash(query)}`
+ : `/query-details/${hash(query)}`;
+ history.push(route);
+ }}
+ >
+ {query.id || '-'} {/* TODO: Remove fallback '-' once query_id is available - #159 */}
+
+
+ );
+ },
+ sortable: (query: SearchQueryRecord) => query.id || '-',
+ truncateText: true,
+ },
+ {
+ name: TYPE,
+ render: (query: SearchQueryRecord) => {
+ 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: SearchQueryRecord['measurements']) =>
+ `${
+ measurements?.latency?.count ||
+ measurements?.cpu?.count ||
+ measurements?.memory?.count ||
+ 1
+ }`,
+ sortable: (measurements: SearchQueryRecord['measurements']) => {
+ return Number(
+ measurements?.latency?.count ||
+ measurements?.cpu?.count ||
+ measurements?.memory?.count ||
+ 1
+ );
+ },
+ truncateText: true,
+ },
{
// Make into flyout instead?
name: TIMESTAMP,
- render: (query: any) => {
+ render: (query: SearchQueryRecord) => {
+ 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}
);
},
@@ -88,9 +159,14 @@ const QueryInsights = ({
{
field: MEASUREMENTS_FIELD,
name: LATENCY,
- render: (measurements: any) => {
- const latencyValue = measurements?.latency?.number;
- return latencyValue !== undefined ? `${latencyValue} ms` : METRIC_DEFAULT_MSG;
+ render: (measurements: SearchQueryRecord['measurements']) => {
+ const result = calculateMetric(
+ measurements?.latency?.number,
+ measurements?.latency?.count,
+ 1,
+ METRIC_DEFAULT_MSG
+ );
+ return `${result} ms`;
},
sortable: true,
truncateText: true,
@@ -98,9 +174,14 @@ const QueryInsights = ({
{
field: MEASUREMENTS_FIELD,
name: CPU_TIME,
- render: (measurements: { cpu?: { number?: number } }) => {
- const cpuValue = measurements?.cpu?.number;
- return cpuValue !== undefined ? `${cpuValue / 1000000} ms` : METRIC_DEFAULT_MSG;
+ render: (measurements: SearchQueryRecord['measurements']) => {
+ const result = calculateMetric(
+ measurements?.cpu?.number,
+ measurements?.cpu?.count,
+ 1000000, // Divide by 1,000,000 for CPU time
+ METRIC_DEFAULT_MSG
+ );
+ return `${result} ms`;
},
sortable: true,
truncateText: true,
@@ -108,9 +189,14 @@ const QueryInsights = ({
{
field: MEASUREMENTS_FIELD,
name: MEMORY_USAGE,
- render: (measurements: { memory?: { number?: number } }) => {
- const memoryValue = measurements?.memory?.number;
- return memoryValue !== undefined ? `${memoryValue} B` : METRIC_DEFAULT_MSG;
+ render: (measurements: SearchQueryRecord['measurements']) => {
+ const result = calculateMetric(
+ measurements?.memory?.number,
+ measurements?.memory?.count,
+ 1,
+ METRIC_DEFAULT_MSG
+ );
+ return `${result} B`;
},
sortable: true,
truncateText: true,
@@ -118,26 +204,40 @@ const QueryInsights = ({
{
field: INDICES_FIELD,
name: INDICES,
- render: (indices: string[]) => Array.from(new Set(indices.flat())).join(', '),
+ render: (indices: string[], query: SearchQueryRecord) => {
+ 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: SearchQueryRecord) => {
+ 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: SearchQueryRecord) => {
+ const isSimilarity = query.group_by === 'SIMILARITY';
+ return isSimilarity ? '-' : nodeId;
+ },
sortable: true,
truncateText: true,
},
{
field: TOTAL_SHARDS_FIELD,
name: TOTAL_SHARDS,
+ render: (totalShards: number, query: SearchQueryRecord) => {
+ const isSimilarity = query.group_by === 'SIMILARITY';
+ return isSimilarity ? '-' : totalShards;
+ },
sortable: true,
truncateText: true,
},
@@ -171,6 +271,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 +355,7 @@ const QueryInsights = ({
],
}}
allowNeutralSort={false}
+ itemId={(query) => `${query.id}-${query.timestamp}`}
/>
);
};
diff --git a/public/pages/QueryInsights/__snapshots__/QueryInsights.test.tsx.snap b/public/pages/QueryInsights/__snapshots__/QueryInsights.test.tsx.snap
index 3054c55..74fde68 100644
--- a/public/pages/QueryInsights/__snapshots__/QueryInsights.test.tsx.snap
+++ b/public/pages/QueryInsights/__snapshots__/QueryInsights.test.tsx.snap
@@ -56,6 +56,49 @@ exports[`QueryInsights Component renders the table with the correct columns and
+
+
+
+
+
+
+
+
+ Type
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 80a17984b847133b8bf5e7d5dfbfa96c
+
+
+
+
+
+
+
+
+
+
+ query
+
+
+
+
+
+
+
+ 1
+
+
@@ -604,7 +783,7 @@ exports[`QueryInsights Component renders the table with the correct columns and
class="euiLink euiLink--primary"
type="button"
>
- Jan 13, 2025 @ 12:00:00 AM
+ -
@@ -620,7 +799,7 @@ exports[`QueryInsights Component renders the table with the correct columns and
- 8 ms
+ 8.00 ms
- Not enabled
+ Not enabled B
-
- Q36D2z_NRGKim6EZZMgi6A
-
+ Q36D2z_NRGKim6EZZMgi6A
-
- 1
-
+ 1
diff --git a/public/pages/TopNQueries/TopNQueries.tsx b/public/pages/TopNQueries/TopNQueries.tsx
index cc6a4cf..61edaf6 100644
--- a/public/pages/TopNQueries/TopNQueries.tsx
+++ b/public/pages/TopNQueries/TopNQueries.tsx
@@ -12,6 +12,7 @@ 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 +24,10 @@ export interface MetricSettings {
currTimeUnit: string;
}
+export interface GroupBySettings {
+ groupBy: string;
+}
+
const TopNQueries = ({
core,
initialStart = 'now-1d',
@@ -61,6 +66,8 @@ const TopNQueries = ({
currTimeUnit: 'HOURS',
});
+ const [groupBySettings, setGroupBySettings] = useState
({ groupBy: 'none' });
+
const setMetricSettings = (metricType: string, updates: Partial) => {
switch (metricType) {
case 'latency':
@@ -175,12 +182,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: groupBy } =
resp?.response?.persistent?.search?.insights?.top_queries || {};
if (latency !== undefined && latency.enabled === 'true') {
const [time, timeUnits] = latency.window_size
@@ -215,6 +223,9 @@ const TopNQueries = ({
currTimeUnit: timeUnits === 'm' ? 'MINUTES' : 'HOURS',
});
}
+ if (groupBy) {
+ setGroupBySettings({ groupBy });
+ }
} catch (error) {
console.error('Failed to retrieve settings:', error);
}
@@ -226,12 +237,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 +282,9 @@ const TopNQueries = ({
+
+
+
Query insights - Top N queries
@@ -297,6 +313,7 @@ const TopNQueries = ({
latencySettings={latencySettings}
cpuSettings={cpuSettings}
memorySettings={memorySettings}
+ groupBySettings={groupBySettings}
configInfo={retrieveConfigInfo}
core={core}
/>
diff --git a/public/pages/Utils/Constants.ts b/public/pages/Utils/Constants.ts
new file mode 100644
index 0000000..e52605c
--- /dev/null
+++ b/public/pages/Utils/Constants.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export const METRIC_TYPES = [
+ { value: 'latency', text: 'Latency' },
+ { value: 'cpu', text: 'CPU' },
+ { value: 'memory', text: 'Memory' },
+];
+
+export const TIME_UNITS = [
+ { value: 'MINUTES', text: 'Minute(s)' },
+ { value: 'HOURS', text: 'Hour(s)' },
+];
+
+export const MINUTES_OPTIONS = [
+ { value: '1', text: '1' },
+ { value: '5', text: '5' },
+ { value: '10', text: '10' },
+ { value: '30', text: '30' },
+];
+
+export const GROUP_BY_OPTIONS = [
+ { value: 'none', text: 'None' },
+ { value: 'similarity', text: 'Similarity' },
+];
diff --git a/public/pages/Utils/MetricUtils.ts b/public/pages/Utils/MetricUtils.ts
new file mode 100644
index 0000000..5b70ff0
--- /dev/null
+++ b/public/pages/Utils/MetricUtils.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export function calculateMetric(
+ value?: number,
+ count?: number,
+ factor: number = 1,
+ defaultMsg: string = 'N/A'
+): string {
+ if (value !== undefined && count !== undefined) {
+ return (value / count / factor).toFixed(2);
+ }
+ return defaultMsg;
+}
diff --git a/server/routes/index.ts b/server/routes/index.ts
index 6e06207..88525fd 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..74ec140
--- /dev/null
+++ b/test/mocks/mockQueries.ts
@@ -0,0 +1,71 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+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,
+ },
+ },
+ ],
+ id: '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/test/testUtils.ts b/test/testUtils.ts
index 55ad71d..8e94d06 100644
--- a/test/testUtils.ts
+++ b/test/testUtils.ts
@@ -69,7 +69,7 @@ export const MockQueries = (): SearchQueryRecord[] => {
},
},
},
- query_hashcode: '80a17984b847133b8bf5e7d5dfbfa96c',
+ id: '80a17984b847133b8bf5e7d5dfbfa96c',
phase_latency_map: {
expand: 0,
query: 5,
diff --git a/types/types.ts b/types/types.ts
index ca4ec71..a23478a 100644
--- a/types/types.ts
+++ b/types/types.ts
@@ -3,6 +3,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
+import { ISearchSource } from 'src/plugins/data/public';
+
export interface SearchQueryRecord {
timestamp: number;
measurements: {
@@ -12,13 +14,14 @@ export interface SearchQueryRecord {
};
total_shards: number;
node_id: string;
- source: Record;
+ source: ISearchSource;
labels: Record;
search_type: string;
indices: string[];
phase_latency_map: PhaseLatencyMap;
task_resource_usages: Task[];
- query_hashcode: string;
+ id: string;
+ group_by: string;
}
export interface Measurement {
diff --git a/yarn.lock b/yarn.lock
index a3d79cd..6fad380 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2397,6 +2397,7 @@ debug@^3.1.0, debug@^3.2.7:
dependencies:
ms "^2.1.1"
+
debug@^4.3.4:
version "4.4.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"