Skip to content

Commit

Permalink
Instrument metrics on server-side (#162)
Browse files Browse the repository at this point in the history
* Server side implementation on metrics

Signed-off-by: Louis Chu <[email protected]>

* Refactor metrics to use sliding window

Signed-off-by: Louis Chu <[email protected]>

* Update unit test

Signed-off-by: Louis Chu <[email protected]>

---------

Signed-off-by: Louis Chu <[email protected]>
  • Loading branch information
noCharger authored Mar 10, 2023
1 parent 5d5a2b0 commit e02db64
Show file tree
Hide file tree
Showing 13 changed files with 526 additions and 93 deletions.
1 change: 1 addition & 0 deletions common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export const PLUGIN_NAME = 'Search Relevance';
export enum ServiceEndpoints {
GetIndexes = '/api/relevancy/search/indexes',
GetSearchResults = '/api/relevancy/search',
GetStats = '/api/relevancy/stats',
}
17 changes: 17 additions & 0 deletions config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { schema, TypeOf } from '@osd/config-schema';

import { METRIC_INTERVAL, DEFAULT_WINDOW_SIZE } from './server/metrics';

export const configSchema = schema.object({
metrics: schema.object({
metricInterval: schema.number({ defaultValue: METRIC_INTERVAL.ONE_MINUTE }),
windowSize: schema.number({ min: 2, max: 10, defaultValue: DEFAULT_WINDOW_SIZE }),
}),
});

export type SearchRelevancePluginConfigType = TypeOf<typeof configSchema>;
4 changes: 1 addition & 3 deletions opensearch_dashboards.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,5 @@
"opensearchDashboardsVersion": "3.0.0",
"server": true,
"ui": true,
"requiredPlugins": [
"navigation"
]
"requiredPlugins": ["navigation"]
}
113 changes: 60 additions & 53 deletions public/components/query_compare/search_result/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,85 +38,92 @@ export const SearchResult = ({ http }: SearchResultProps) => {
} = useSearchRelevanceContext();

const onClickSearch = () => {
const queryError1: QueryError = { ...initialQueryErrorState };
const queryError2: QueryError = { ...initialQueryErrorState };
const queryErrors = [{ ...initialQueryErrorState }, { ...initialQueryErrorState }];
const jsonQueries = [{}, {}];

validateQuery(selectedIndex1, queryString1, queryErrors[0]);
jsonQueries[0] = rewriteQuery(searchBarValue, queryString1, queryErrors[0]);

validateQuery(selectedIndex2, queryString2, queryErrors[1]);
jsonQueries[1] = rewriteQuery(searchBarValue, queryString2, queryErrors[1]);

handleQuery(jsonQueries, queryErrors);
};

const validateQuery = (selectedIndex: string, queryString: string, queryError: QueryError) => {
// Check if select an index
if (!selectedIndex1.length) {
queryError1.selectIndex = 'An index is required. Select an index.';
}
if (!selectedIndex2.length) {
queryError2.selectIndex = 'An index is required. Select an index.';
if (!selectedIndex.length) {
queryError.selectIndex = 'An index is required. Select an index.';
}

// Check if query string is empty
if (!queryString1.length) {
queryError1.queryString = QueryStringError.empty;
}
if (!queryString2.length) {
queryError2.queryString = QueryStringError.empty;
if (!queryString.length) {
queryError.queryString = QueryStringError.empty;
}
};

// Check if query string is valid
let jsonQuery1 = {};
let jsonQuery2 = {};
if (queryString1.trim().length > 0) {
const rewriteQuery = (searchBarValue: string, queryString: string, queryError: QueryError) => {
if (queryString.trim().length > 0) {
try {
jsonQuery1 = JSON.parse(queryString1.replace(/%SearchText%/g, searchBarValue));
return JSON.parse(queryString.replace(/%SearchText%/g, searchBarValue));
} catch {
queryError1.queryString = QueryStringError.invalid;
}
}
if (queryString2.trim().length > 0) {
try {
jsonQuery2 = JSON.parse(queryString2.replace(/%SearchText%/g, searchBarValue));
} catch {
queryError2.queryString = QueryStringError.invalid;
queryError.queryString = QueryStringError.invalid;
}
}
};

const handleQuery = (jsonQueries: any, queryErrors: QueryError[]) => {
let requestBody = {};

// Handle query1
if (queryError1.queryString.length || queryError1.selectIndex.length) {
setQueryError1(queryError1);
if (queryErrors[0].queryString.length || queryErrors[0].selectIndex.length) {
setQueryError1(queryErrors[0]);
setQueryResult1({} as any);
updateComparedResult1({} as any);
} else if (!queryError1.queryString.length && !queryError1.selectIndex.length) {
http
.post(ServiceEndpoints.GetSearchResults, {
body: JSON.stringify({ index: selectedIndex1, ...jsonQuery1 }),
})
.then((res) => {
setQueryResult1(res);
updateComparedResult1(res);
})
.catch((error: Error) => {
setQueryError1({
...queryError1,
queryString: error.body.message,
});
console.error(error);
});
} else if (!queryErrors[0].queryString.length && !queryErrors[0].selectIndex.length) {
requestBody = {
query1: { index: selectedIndex1, ...jsonQueries[0] },
};
}

// Handle query2
if (queryError2.queryString.length || queryError2.selectIndex.length) {
setQueryError2(queryError2);
if (queryErrors[1].queryString.length || queryErrors[1].selectIndex.length) {
setQueryError2(queryErrors[1]);
setQueryResult2({} as any);
updateComparedResult2({} as any);
} else if (!queryError2.queryString.length && !queryError2.selectIndex.length) {
} else if (!queryErrors[1].queryString.length && !queryErrors[1].selectIndex.length) {
requestBody = {
...requestBody,
query2: { index: selectedIndex2, ...jsonQueries[1] },
};
}

if (Object.keys(requestBody).length !== 0) {
http
.post(ServiceEndpoints.GetSearchResults, {
body: JSON.stringify({ index: selectedIndex2, ...jsonQuery2 }),
body: JSON.stringify(requestBody),
})
.then((res) => {
setQueryResult2(res);
updateComparedResult2(res);
setQueryResult1(res.result1);
updateComparedResult1(res.result1);
setQueryResult2(res.result2);
updateComparedResult2(res.result2);

if (res.errorMessage1) {
setQueryError1({
...queryErrors[0],
queryString: res.errorMessage1,
});
}

if (res.errorMessage2) {
setQueryError2({
...queryErrors[1],
queryString: res.errorMessage2,
});
}
})
.catch((error: Error) => {
setQueryError2({
...queryError2,
queryString: error.body.message,
});
console.error(error);
});
}
Expand Down
8 changes: 5 additions & 3 deletions server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { PluginInitializerContext } from '../../../src/core/server';
import { PluginConfigDescriptor, PluginInitializerContext } from '../../../src/core/server';
import { SearchRelevancePlugin } from './plugin';
import { configSchema, SearchRelevancePluginConfigType } from '../config';

// This exports static code and TypeScript types,
// as well as, OpenSearch Dashboards Platform `plugin()` initializer.
export const config: PluginConfigDescriptor<SearchRelevancePluginConfigType> = {
schema: configSchema,
};

export function plugin(initializerContext: PluginInitializerContext) {
return new SearchRelevancePlugin(initializerContext);
Expand Down
23 changes: 23 additions & 0 deletions server/metrics/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export { MetricsServiceSetup, MetricsService } from './metrics_service';

export enum METRIC_INTERVAL {
ONE_SECOND = 1000,
ONE_MINUTE = 60000,
}

export const DEFAULT_WINDOW_SIZE = 3;

export enum METRIC_NAME {
RELEVANT_SEARCH = 'search_relevance',
}

export enum METRIC_ACTION {
COMPARE_SEARCH = 'compare_search',
SINGLE_SEARCH = 'single_search',
FETCH_INDEX = 'fetch_index',
}
75 changes: 75 additions & 0 deletions server/metrics/metrics_service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { MetricsService, MetricsServiceSetup } from './';

describe('MetricsService', () => {
let metricsService: MetricsService;
let setup: MetricsServiceSetup;

beforeEach(() => {
metricsService = new MetricsService();
setup = metricsService.setup();
});

afterEach(() => {
metricsService.stop();
});

describe('test addMetric and getStats', () => {
it('should add metrics to the correct interval and ignore metrics in the future', () => {
jest.useFakeTimers('modern');
jest.setSystemTime(0);
setup.addMetric('component1', 'action1', 200, 100);
jest.advanceTimersByTime(30000);
setup.addMetric('component1', 'action1', 200, 200);
setup.addMetric('component1', 'action1', 200, 300);
jest.advanceTimersByTime(30000);
setup.addMetric('component1', 'action1', 200, 400);

const stats = setup.getStats();
expect(stats.data['component1']['action1'][200]).toEqual({
sum: 600,
count: 3,
});
expect(stats.overall.response_time_avg).toEqual(200);
expect(stats.overall.requests_per_second).toEqual(0.05);
expect(stats.component_counts['component1']).toEqual(3);
expect(stats.status_code_counts[200]).toEqual(3);
});
});

describe('resetMetrics', () => {
it('should clear all metrics data', () => {
setup.addMetric('component1', 'action1', 200, 100);
metricsService.resetMetrics();
const stats = setup.getStats();
expect(stats.data).toEqual({});
expect(stats.overall).toEqual({ response_time_avg: 0, requests_per_second: 0 });
expect(stats.component_counts).toEqual({});
expect(stats.status_code_counts).toEqual({});
});
});

describe('trim', () => {
it('should remove old metrics data', () => {
jest.useFakeTimers('modern');
jest.setSystemTime(0);
setup.addMetric('component1', 'action1', 200, 100);
jest.advanceTimersByTime(60000);
setup.addMetric('component1', 'action1', 200, 200);
jest.advanceTimersByTime(60000);
setup.addMetric('component1', 'action1', 200, 300);
jest.advanceTimersByTime(60000);
metricsService.trim();
jest.setSystemTime(0);
const stats = setup.getStats();
expect(stats.data).toEqual({});
expect(stats.overall).toEqual({ response_time_avg: 0, requests_per_second: 0 });
expect(stats.component_counts).toEqual({});
expect(stats.status_code_counts).toEqual({});
});
});
});
Loading

0 comments on commit e02db64

Please sign in to comment.