From 138c8f93780080fe2f858af80971be91d0141fce Mon Sep 17 00:00:00 2001 From: Chenyang Ji Date: Thu, 16 Jan 2025 15:20:44 -0800 Subject: [PATCH] Query grouping dashboard changes and extensive tests (#33) (#44) * Revert "remove records with grouping (#26)" This reverts commit 9efdbaf889650eee00502106210cbb294c447eb3. * Query grouping dashboard changes and extensive tests * Fix tests and linting * Address review comments * Address review comments and update tests * Update cypress tests, hashcode to id, refactoring * Lint --------- Signed-off-by: Siddhant Deshmukh Co-authored-by: Siddhant Deshmukh --- .github/workflows/cypress-tests.yml | 308 +++++++------- common/constants.ts | 7 + cypress/e2e/2_query_details.cy.js | 6 +- cypress/e2e/3_configurations.cy.js | 4 +- .../__snapshots__/app.test.tsx.snap | 140 ++++++- .../Configuration/Configuration.test.tsx | 15 +- public/pages/Configuration/Configuration.tsx | 126 ++++-- .../__snapshots__/Configuration.test.tsx.snap | 377 ++++++++++++++++++ .../QueryDetails/Components/QuerySummary.tsx | 25 +- .../__snapshots__/QueryDetails.test.tsx.snap | 4 +- .../QueryGroupAggregateSummary.test.tsx | 102 +++++ .../Components/QueryGroupAggregateSummary.tsx | 73 ++++ .../QueryGroupSampleQuerySummary.test.tsx | 81 ++++ .../QueryGroupSampleQuerySummary.tsx | 54 +++ .../QueryGroupDetails.test.tsx | 117 ++++++ .../QueryGroupDetails/QueryGroupDetails.tsx | 178 +++++++++ public/pages/QueryInsights/QueryInsights.tsx | 150 ++++++- .../__snapshots__/QueryInsights.test.tsx.snap | 221 ++++++++-- public/pages/TopNQueries/TopNQueries.tsx | 21 +- public/pages/Utils/Constants.ts | 27 ++ public/pages/Utils/MetricUtils.ts | 16 + server/routes/index.ts | 2 + test/mocks/mockQueries.ts | 71 ++++ test/testUtils.ts | 2 +- types/types.ts | 7 +- yarn.lock | 1 + 26 files changed, 1879 insertions(+), 256 deletions(-) create mode 100644 public/pages/QueryGroupDetails/Components/QueryGroupAggregateSummary.test.tsx create mode 100644 public/pages/QueryGroupDetails/Components/QueryGroupAggregateSummary.tsx create mode 100644 public/pages/QueryGroupDetails/Components/QueryGroupSampleQuerySummary.test.tsx create mode 100644 public/pages/QueryGroupDetails/Components/QueryGroupSampleQuerySummary.tsx create mode 100644 public/pages/QueryGroupDetails/QueryGroupDetails.test.tsx create mode 100644 public/pages/QueryGroupDetails/QueryGroupDetails.tsx create mode 100644 public/pages/Utils/Constants.ts create mode 100644 public/pages/Utils/MetricUtils.ts create mode 100644 test/mocks/mockQueries.ts 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`] = `
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" + > + + + + + + + + + @@ -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`] = ` >
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
+
+
+
+
+
+
+

+ Top n queries grouping configuration settings +

+
+
+
+
+
+
+

+ Group By +

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

+ Statuses for group by +

+
+
+
+
+
+
+ Group By +
+
+
+
+
+
+
+ +
+
+ Disabled +
+
+
+
+
+
+
+
+
`; @@ -1215,6 +1402,196 @@ exports[`Configuration Component updates state when toggling metrics and enables
+
+
+
+
+
+
+

+ Top n queries grouping configuration settings +

+
+
+
+
+
+
+

+ Group By +

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

+ Statuses for group by +

+
+
+
+
+
+
+ Group By +
+
+
+
+
+
+
+ +
+
+ Disabled +
+
+
+
+
+
+
+
+
`; 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`] = `
- 8 ms + 8.00 ms
- N/A B + N/A
{ + 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
+
+ +
+
+
+ + + + + + + + + @@ -409,7 +527,7 @@ exports[`QueryInsights Component renders the table with the correct columns and aria-live="polite" aria-sort="none" class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_measurements_1" + data-test-subj="tableHeaderCell_measurements_4" role="columnheader" scope="col" > @@ -434,7 +552,7 @@ exports[`QueryInsights Component renders the table with the correct columns and aria-live="polite" aria-sort="none" class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_measurements_2" + data-test-subj="tableHeaderCell_measurements_5" role="columnheader" scope="col" > @@ -459,7 +577,7 @@ exports[`QueryInsights Component renders the table with the correct columns and aria-live="polite" aria-sort="none" class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_measurements_3" + data-test-subj="tableHeaderCell_measurements_6" role="columnheader" scope="col" > @@ -484,7 +602,7 @@ exports[`QueryInsights Component renders the table with the correct columns and aria-live="polite" aria-sort="none" class="euiTableHeaderCell" - data-test-subj="tableHeaderCell_indices_4" + data-test-subj="tableHeaderCell_indices_7" role="columnheader" scope="col" > @@ -509,7 +627,7 @@ exports[`QueryInsights Component renders the table with the correct columns and 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" > @@ -534,7 +652,7 @@ exports[`QueryInsights Component renders the table with the correct columns and 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" > @@ -559,7 +677,7 @@ exports[`QueryInsights Component renders the table with the correct columns and 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" > @@ -586,6 +704,67 @@ exports[`QueryInsights Component renders the table with the correct columns and + +
+ Id +
+
+ + + +
+ + +
+ Type +
+
+ + + +
+ + +
+ Query Count +
+
+ 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"