From 405f5e7fb514e9a81c40b86388cd8ff8542dc4e4 Mon Sep 17 00:00:00 2001 From: Emily Guo <35637792+LilyCaroline17@users.noreply.github.com> Date: Wed, 4 Sep 2024 16:28:14 -0700 Subject: [PATCH] Top n queries connected to api (#10) * Add 2 tabs to home screen Signed-off-by: Emily Guo * two tabs for query insights and configuration Signed-off-by: Emily Guo * Breadcrumbs + restructuring Signed-off-by: Emily Guo * Top n queries table + search Signed-off-by: Emily Guo * search in general Signed-off-by: Emily Guo * Search includes indices and time selection fully works Signed-off-by: Emily Guo * Fix tabs display Signed-off-by: Emily Guo * Basic functionalities of configuration page Signed-off-by: Emily Guo * Save button + cancel button working Signed-off-by: Emily Guo * Fixing lint and including relevant files Signed-off-by: Emily Guo * Typo Signed-off-by: Emily Guo * ignore lintcache Signed-off-by: Emily Guo * Delete .eslintcache Signed-off-by: Emily Guo <35637792+LilyCaroline17@users.noreply.github.com> * remove commented out lines Signed-off-by: Emily Guo * Update based on comments on overview page Signed-off-by: Emily Guo * Rerun security check? Signed-off-by: Emily Guo * Added updates to all configurations Signed-off-by: Emily Guo * Updated unit test Signed-off-by: Emily Guo * Fix based on comments Signed-off-by: Emily Guo * Fixed lint issues Signed-off-by: Emily Guo * Update TopNQueries.tsx Signed-off-by: Emily Guo <35637792+LilyCaroline17@users.noreply.github.com> * Connected api to frontend Signed-off-by: Emily Guo --------- Signed-off-by: Emily Guo Signed-off-by: Emily Guo <35637792+LilyCaroline17@users.noreply.github.com> Signed-off-by: Emily Guo --- public/pages/TopNQueries/TopNQueries.tsx | 145 ++++++++++++++-- server/clusters/queryInsightsPlugin.ts | 83 +++++++++ server/plugin.ts | 15 ++ server/routes/index.ts | 212 ++++++++++++++++++++++- 4 files changed, 435 insertions(+), 20 deletions(-) create mode 100644 server/clusters/queryInsightsPlugin.ts diff --git a/public/pages/TopNQueries/TopNQueries.tsx b/public/pages/TopNQueries/TopNQueries.tsx index 4404369..1a09ca3 100644 --- a/public/pages/TopNQueries/TopNQueries.tsx +++ b/public/pages/TopNQueries/TopNQueries.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { Redirect, Route, Switch, useHistory, useLocation } from 'react-router-dom'; import { EuiTab, EuiTabs, EuiTitle, EuiSpacer } from '@elastic/eui'; +import dateMath from '@elastic/datemath'; import QueryInsights from '../QueryInsights/QueryInsights'; import Configuration from '../Configuration/Configuration'; import { CoreStart } from '../../../../../src/core/public'; @@ -91,18 +92,135 @@ const TopNQueries = ({ core }: { core: CoreStart }) => { ); - const retrieveQueries = useCallback(async (start: string, end: string) => { - try { - setLoading(true); - const noDuplicates: any[] = []; - setQueries(noDuplicates); - } catch (error) { - // eslint-disable-next-line no-console - console.error('Error retrieving queries:', error); - } finally { - setLoading(false); - } - }, []); + const parseDateString = (dateString: string) => { + const date = dateMath.parse(dateString); + return date ? date.toDate().getTime() : new Date().getTime(); + }; + + const retrieveQueries = useCallback( + async (start: string, end: string) => { + const nullResponse = { response: { top_queries: [] } }; + const params = { + query: { + from: new Date(parseDateString(start)).toISOString(), + to: new Date(parseDateString(end)).toISOString(), + }, + }; + const fetchMetric = async (endpoint: string) => { + try { + const response = await core.http.get(endpoint, params); + return { + response: { + top_queries: Array.isArray(response?.response?.top_queries) + ? response.response.top_queries + : [], + }, + }; + } catch { + return nullResponse; + } + }; + try { + setLoading(true); + const respLatency = latencySettings.isEnabled + ? await fetchMetric('/api/top_queries/latency') + : nullResponse; + const respCpu = cpuSettings.isEnabled + ? await fetchMetric('/api/top_queries/cpu') + : nullResponse; + const respMemory = memorySettings.isEnabled + ? await fetchMetric('/api/top_queries/memory') + : nullResponse; + const newQueries = [ + ...respLatency.response.top_queries, + ...respCpu.response.top_queries, + ...respMemory.response.top_queries, + ]; + const noDuplicates = Array.from( + new Set(newQueries.map((item) => JSON.stringify(item))) + ).map((item) => JSON.parse(item)); + setQueries(noDuplicates); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error retrieving queries:', error); + } finally { + setLoading(false); + } + }, + [latencySettings, cpuSettings, memorySettings, core] + ); + + const retrieveConfigInfo = useCallback( + async ( + get: boolean, + enabled: boolean = false, + metric: string = '', + newTopN: string = '', + newWindowSize: string = '', + newTimeUnit: string = '' + ) => { + if (get) { + try { + const resp = await core.http.get('/api/settings'); + const settings = resp.response.persistent.search.insights.top_queries; + const latency = settings.latency; + const cpu = settings.cpu; + const memory = settings.memory; + if (latency !== undefined && latency.enabled === 'true') { + const [time, timeUnits] = latency.window_size.match(/\D+|\d+/g); + setMetricSettings('latency', { + isEnabled: true, + currTopN: latency.top_n_size, + currWindowSize: time, + currTimeUnit: timeUnits === 'm' ? 'MINUTES' : 'HOURS', + }); + } + if (cpu !== undefined && cpu.enabled === 'true') { + const [time, timeUnits] = cpu.window_size.match(/\D+|\d+/g); + setMetricSettings('cpu', { + isEnabled: true, + currTopN: cpu.top_n_size, + currWindowSize: time, + currTimeUnit: timeUnits === 'm' ? 'MINUTES' : 'HOURS', + }); + } + if (memory !== undefined && memory.enabled === 'true') { + const [time, timeUnits] = memory.window_size.match(/\D+|\d+/g); + setMetricSettings('memory', { + isEnabled: true, + currTopN: memory.top_n_size, + currWindowSize: time, + currTimeUnit: timeUnits === 'm' ? 'MINUTES' : 'HOURS', + }); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error('Failed to retrieve settings:', error); + } + } else { + try { + setMetricSettings(metric, { + isEnabled: enabled, + currTopN: newTopN, + currWindowSize: newWindowSize, + currTimeUnit: newTimeUnit, + }); + await core.http.put('/api/update_settings', { + query: { + metric, + enabled, + top_n_size: newTopN, + window_size: `${newWindowSize}${newTimeUnit === 'MINUTES' ? 'm' : 'h'}`, + }, + }); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Failed to set settings:', error); + } + } + }, + [core] + ); const retrieveConfigInfo = useCallback( async ( @@ -136,6 +254,7 @@ const TopNQueries = ({ core }: { core: CoreStart }) => { setStart(start); setEnd(end); setRecentlyUsedRanges(usedRange.length > 10 ? usedRange.slice(0, 9) : usedRange); + retrieveConfigInfo(true); retrieveQueries(start, end); }; @@ -146,7 +265,7 @@ const TopNQueries = ({ core }: { core: CoreStart }) => { useEffect(() => { retrieveQueries(currStart, currEnd); - }, [currStart, currEnd, retrieveQueries]); + }, [latencySettings, cpuSettings, memorySettings, currStart, currEnd, retrieveQueries]); return (
diff --git a/server/clusters/queryInsightsPlugin.ts b/server/clusters/queryInsightsPlugin.ts new file mode 100644 index 0000000..24b0f75 --- /dev/null +++ b/server/clusters/queryInsightsPlugin.ts @@ -0,0 +1,83 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const QueryInsightsPlugin = function (Client, config, components) { + const ca = components.clientAction.factory; + Client.prototype.queryInsights = components.clientAction.namespaceFactory(); + const queryInsights = Client.prototype.queryInsights.prototype; + + queryInsights.getTopNQueries = ca({ + url: { + fmt: `/_insights/top_queries`, + }, + method: 'GET', + }); + + queryInsights.getTopNQueriesLatency = ca({ + url: { + fmt: `/_insights/top_queries?type=latency&from=<%=from%>&to=<%=to%>`, + req: { + from: { + type: 'string', + required: true, + }, + to: { + type: 'string', + required: true, + }, + }, + }, + method: 'GET', + }); + + queryInsights.getTopNQueriesCpu = ca({ + url: { + fmt: `/_insights/top_queries?type=cpu&from=<%=from%>&to=<%=to%>`, + req: { + from: { + type: 'string', + required: true, + }, + to: { + type: 'string', + required: true, + }, + }, + }, + method: 'GET', + }); + + queryInsights.getTopNQueriesMemory = ca({ + url: { + fmt: `/_insights/top_queries?type=memory&from=<%=from%>&to=<%=to%>`, + req: { + from: { + type: 'string', + required: true, + }, + to: { + type: 'string', + required: true, + }, + }, + }, + method: 'GET', + }); + + queryInsights.getSettings = ca({ + url: { + fmt: `_cluster/settings?include_defaults=true`, + }, + method: 'GET', + }); + + queryInsights.setSettings = ca({ + url: { + fmt: `_cluster/settings`, + }, + method: 'PUT', + needBody: true, + }); +}; diff --git a/server/plugin.ts b/server/plugin.ts index 3d16bbd..d48449f 100644 --- a/server/plugin.ts +++ b/server/plugin.ts @@ -4,7 +4,9 @@ import { CoreStart, Plugin, Logger, + ILegacyCustomClusterClient, } from '../../../src/core/server'; +import { QueryInsightsPlugin } from './clusters/queryInsightsPlugin'; import { QueryInsightsDashboardsPluginSetup, QueryInsightsDashboardsPluginStart } from './types'; import { defineRoutes } from './routes'; @@ -20,6 +22,19 @@ export class QueryInsightsDashboardsPlugin public setup(core: CoreSetup) { this.logger.debug('query-insights-dashboards: Setup'); const router = core.http.createRouter(); + const queryInsightsClient: ILegacyCustomClusterClient = core.opensearch.legacy.createClient( + 'opensearch_queryInsights', + { + plugins: [QueryInsightsPlugin], + } + ); + // @ts-ignore + core.http.registerRouteHandlerContext('queryInsights_plugin', (_context, _request) => { + return { + logger: this.logger, + queryInsightsClient, + }; + }); // Register server side APIs defineRoutes(router); diff --git a/server/routes/index.ts b/server/routes/index.ts index 64b5d59..cd59293 100644 --- a/server/routes/index.ts +++ b/server/routes/index.ts @@ -1,17 +1,215 @@ +import { schema } from '@osd/config-schema'; import { IRouter } from '../../../../src/core/server'; - export function defineRoutes(router: IRouter) { router.get( { - path: '/api/query_insights_dashboards/example', + path: '/api/top_queries', + validate: false, + }, + async (context, request, response) => { + try { + const client = context.queryInsights_plugin.queryInsightsClient.asScoped(request) + .callAsCurrentUser; + const res = await client('queryInsights.getTopNQueries'); + return response.custom({ + statusCode: 200, + body: { + ok: true, + response: res, + }, + }); + } catch (error) { + console.error('Unable to get top queries: ', error); + return response.ok({ + body: { + ok: false, + response: error.message, + }, + }); + } + } + ); + + router.get( + { + path: '/api/top_queries/latency', + validate: { + query: schema.object({ + from: schema.maybe(schema.string({ defaultValue: '' })), + to: schema.maybe(schema.string({ defaultValue: '' })), + }), + }, + }, + async (context, request, response) => { + try { + const { from, to } = request.query; + const params = { from, to }; + const client = context.queryInsights_plugin.queryInsightsClient.asScoped(request) + .callAsCurrentUser; + const res = await client('queryInsights.getTopNQueriesLatency', params); + return response.custom({ + statusCode: 200, + body: { + ok: true, + response: res, + }, + }); + } catch (error) { + console.error('Unable to get top queries (latency): ', error); + return response.ok({ + body: { + ok: false, + response: error.message, + }, + }); + } + } + ); + + router.get( + { + path: '/api/top_queries/cpu', + validate: { + query: schema.object({ + from: schema.maybe(schema.string({ defaultValue: '' })), + to: schema.maybe(schema.string({ defaultValue: '' })), + }), + }, + }, + async (context, request, response) => { + try { + const { from, to } = request.query; + const params = { from, to }; + const client = context.queryInsights_plugin.queryInsightsClient.asScoped(request) + .callAsCurrentUser; + const res = await client('queryInsights.getTopNQueriesCpu', params); + return response.custom({ + statusCode: 200, + body: { + ok: true, + response: res, + }, + }); + } catch (error) { + console.error('Unable to get top queries (cpu): ', error); + return response.ok({ + body: { + ok: false, + response: error.message, + }, + }); + } + } + ); + + router.get( + { + path: '/api/top_queries/memory', + validate: { + query: schema.object({ + from: schema.maybe(schema.string({ defaultValue: '' })), + to: schema.maybe(schema.string({ defaultValue: '' })), + }), + }, + }, + async (context, request, response) => { + try { + const { from, to } = request.query; + const params = { from, to }; + const client = context.queryInsights_plugin.queryInsightsClient.asScoped(request) + .callAsCurrentUser; + const res = await client('queryInsights.getTopNQueriesMemory', params); + return response.custom({ + statusCode: 200, + body: { + ok: true, + response: res, + }, + }); + } catch (error) { + console.error('Unable to get top queries (memory): ', error); + return response.ok({ + body: { + ok: false, + response: error.message, + }, + }); + } + } + ); + + router.get( + { + path: '/api/settings', validate: false, }, async (context, request, response) => { - return response.ok({ - body: { - time: new Date().toISOString(), - }, - }); + try { + const client = context.queryInsights_plugin.queryInsightsClient.asScoped(request) + .callAsCurrentUser; + const res = await client('queryInsights.getSettings'); + return response.custom({ + statusCode: 200, + body: { + ok: true, + response: res, + }, + }); + } catch (error) { + console.error('Unable to get top queries: ', error); + return response.ok({ + body: { + ok: false, + response: error.message, + }, + }); + } + } + ); + + router.put( + { + path: '/api/update_settings', + validate: { + query: schema.object({ + metric: schema.maybe(schema.string({ defaultValue: '' })), + enabled: schema.maybe(schema.boolean({ defaultValue: false })), + top_n_size: schema.maybe(schema.string({ defaultValue: '' })), + window_size: schema.maybe(schema.string({ defaultValue: '' })), + }), + }, + }, + async (context, request, response) => { + try { + const query = request.query; + const client = context.queryInsights_plugin.queryInsightsClient.asScoped(request) + .callAsCurrentUser; + const params = { + body: { + persistent: { + [`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, + }, + }, + }; + const res = await client('queryInsights.setSettings', params); + return response.custom({ + statusCode: 200, + body: { + ok: true, + response: res, + }, + }); + } catch (error) { + console.error('Unable to set settings: ', error); + return response.ok({ + body: { + ok: false, + response: error.message, + }, + }); + } } ); }