[App Search] API logs: Server route + ApiLogsLogic + useEffects (#95732)

* Set up API route

* Set up API types

* Set up date util needed by filters dates

* Add ApiLogsLogic

* Update ApiLogs and EngineOverview views with polling behavior

* Add API type notes - maybe serves as a TODO to clean up our API data some day
This commit is contained in:
Constance 2021-03-30 11:35:54 -07:00 committed by GitHub
parent 738425932f
commit 53d4fa7052
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 715 additions and 5 deletions

View file

@ -5,28 +5,67 @@
* 2.0.
*/
import { setMockValues, setMockActions, rerender } from '../../../__mocks__';
import '../../../__mocks__/shallow_useeffect.mock';
import React from 'react';
import { shallow } from 'enzyme';
import { shallow, ShallowWrapper } from 'enzyme';
import { EuiPageHeader } from '@elastic/eui';
import { Loading } from '../../../shared/loading';
import { LogRetentionCallout, LogRetentionTooltip } from '../log_retention';
import { ApiLogs } from './';
describe('ApiLogs', () => {
const values = {
dataLoading: false,
apiLogs: [],
meta: { page: { current: 1 } },
};
const actions = {
fetchApiLogs: jest.fn(),
pollForApiLogs: jest.fn(),
};
let wrapper: ShallowWrapper;
beforeEach(() => {
jest.clearAllMocks();
setMockValues(values);
setMockActions(actions);
wrapper = shallow(<ApiLogs engineBreadcrumb={['some engine']} />);
});
it('renders', () => {
const wrapper = shallow(<ApiLogs engineBreadcrumb={['some engine']} />);
expect(wrapper.find(EuiPageHeader).prop('pageTitle')).toEqual('API Logs');
// TODO: Check for ApiLogsTable + NewApiEventsPrompt when those get added
expect(wrapper.find(LogRetentionCallout).prop('type')).toEqual('api');
expect(wrapper.find(LogRetentionTooltip).prop('type')).toEqual('api');
});
it('renders a loading screen', () => {
setMockValues({ ...values, dataLoading: true, apiLogs: [] });
rerender(wrapper);
expect(wrapper.find(Loading)).toHaveLength(1);
});
describe('effects', () => {
it('calls a manual fetchApiLogs on page load and pagination', () => {
expect(actions.fetchApiLogs).toHaveBeenCalledTimes(1);
setMockValues({ ...values, meta: { page: { current: 2 } } });
rerender(wrapper);
expect(actions.fetchApiLogs).toHaveBeenCalledTimes(2);
});
it('starts pollForApiLogs on page load', () => {
expect(actions.pollForApiLogs).toHaveBeenCalledTimes(1);
});
});
});

View file

@ -5,22 +5,40 @@
* 2.0.
*/
import React from 'react';
import React, { useEffect } from 'react';
import { useValues, useActions } from 'kea';
import { EuiPageHeader, EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { FlashMessages } from '../../../shared/flash_messages';
import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
import { BreadcrumbTrail } from '../../../shared/kibana_chrome/generate_breadcrumbs';
import { Loading } from '../../../shared/loading';
import { LogRetentionCallout, LogRetentionTooltip, LogRetentionOptions } from '../log_retention';
import { API_LOGS_TITLE, RECENT_API_EVENTS } from './constants';
import { ApiLogsLogic } from './';
interface Props {
engineBreadcrumb: BreadcrumbTrail;
}
export const ApiLogs: React.FC<Props> = ({ engineBreadcrumb }) => {
const { dataLoading, apiLogs, meta } = useValues(ApiLogsLogic);
const { fetchApiLogs, pollForApiLogs } = useActions(ApiLogsLogic);
useEffect(() => {
fetchApiLogs();
}, [meta.page.current]);
useEffect(() => {
pollForApiLogs();
}, []);
if (dataLoading && !apiLogs.length) return <Loading />;
return (
<>
<SetPageChrome trail={[...engineBreadcrumb, API_LOGS_TITLE]} />

View file

@ -0,0 +1,308 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { LogicMounter, mockHttpValues, mockFlashMessageHelpers } from '../../../__mocks__';
import '../../__mocks__/engine_logic.mock';
import { nextTick } from '@kbn/test/jest';
import { DEFAULT_META } from '../../../shared/constants';
import { POLLING_ERROR_MESSAGE } from './constants';
import { ApiLogsLogic } from './';
describe('ApiLogsLogic', () => {
const { mount, unmount } = new LogicMounter(ApiLogsLogic);
const { http } = mockHttpValues;
const { flashAPIErrors, setErrorMessage } = mockFlashMessageHelpers;
const DEFAULT_VALUES = {
dataLoading: true,
apiLogs: [],
meta: DEFAULT_META,
hasNewData: false,
polledData: {},
intervalId: null,
};
const MOCK_API_RESPONSE = {
results: [
{
timestamp: '1970-01-01T12:00:00.000Z',
http_method: 'POST',
status: 200,
user_agent: 'some browser agent string',
full_request_path: '/api/as/v1/engines/national-parks-demo/search.json',
request_body: '{"someMockRequest":"hello"}',
response_body: '{"someMockResponse":"world"}',
},
],
meta: {
page: {
current: 1,
total_pages: 10,
total_results: 100,
size: 10,
},
},
};
beforeEach(() => {
jest.clearAllMocks();
});
it('has expected default values', () => {
mount();
expect(ApiLogsLogic.values).toEqual(DEFAULT_VALUES);
});
describe('actions', () => {
describe('onPollStart', () => {
it('sets intervalId state', () => {
mount();
ApiLogsLogic.actions.onPollStart(123);
expect(ApiLogsLogic.values).toEqual({
...DEFAULT_VALUES,
intervalId: 123,
});
});
});
describe('storePolledData', () => {
it('sets hasNewData to true & polledData state', () => {
mount({ hasNewData: false });
ApiLogsLogic.actions.storePolledData(MOCK_API_RESPONSE);
expect(ApiLogsLogic.values).toEqual({
...DEFAULT_VALUES,
hasNewData: true,
polledData: MOCK_API_RESPONSE,
});
});
});
describe('updateView', () => {
it('sets dataLoading & hasNewData to false, sets apiLogs & meta state', () => {
mount({ dataLoading: true, hasNewData: true });
ApiLogsLogic.actions.updateView(MOCK_API_RESPONSE);
expect(ApiLogsLogic.values).toEqual({
...DEFAULT_VALUES,
dataLoading: false,
hasNewData: false,
apiLogs: MOCK_API_RESPONSE.results,
meta: MOCK_API_RESPONSE.meta,
});
});
});
describe('onPaginate', () => {
it('sets dataLoading to true & sets meta state', () => {
mount({ dataLoading: false });
ApiLogsLogic.actions.onPaginate(5);
expect(ApiLogsLogic.values).toEqual({
...DEFAULT_VALUES,
dataLoading: true,
meta: {
...DEFAULT_META,
page: {
...DEFAULT_META.page,
current: 5,
},
},
});
});
});
});
describe('listeners', () => {
describe('pollForApiLogs', () => {
jest.useFakeTimers();
const setIntervalSpy = jest.spyOn(global, 'setInterval');
it('starts a poll that calls fetchApiLogs at set intervals', () => {
mount();
jest.spyOn(ApiLogsLogic.actions, 'onPollStart');
jest.spyOn(ApiLogsLogic.actions, 'fetchApiLogs');
ApiLogsLogic.actions.pollForApiLogs();
expect(setIntervalSpy).toHaveBeenCalled();
expect(ApiLogsLogic.actions.onPollStart).toHaveBeenCalled();
jest.advanceTimersByTime(5000);
expect(ApiLogsLogic.actions.fetchApiLogs).toHaveBeenCalledWith({ isPoll: true });
});
it('does not create new polls if one already exists', () => {
mount({ intervalId: 123 });
ApiLogsLogic.actions.pollForApiLogs();
expect(setIntervalSpy).not.toHaveBeenCalled();
});
afterAll(() => jest.useRealTimers);
});
describe('fetchApiLogs', () => {
const mockDate = jest
.spyOn(global.Date, 'now')
.mockImplementation(() => new Date('1970-01-02').valueOf());
afterAll(() => mockDate.mockRestore());
it('should make an API call', () => {
mount();
ApiLogsLogic.actions.fetchApiLogs();
expect(http.get).toHaveBeenCalledWith('/api/app_search/engines/some-engine/api_logs', {
query: {
'page[current]': 1,
'filters[date][from]': '1970-01-01T00:00:00.000Z',
'filters[date][to]': '1970-01-02T00:00:00.000Z',
sort_direction: 'desc',
},
});
});
describe('manual fetch (page load & pagination)', () => {
it('updates the view immediately with the returned data', async () => {
http.get.mockReturnValueOnce(Promise.resolve(MOCK_API_RESPONSE));
mount();
jest.spyOn(ApiLogsLogic.actions, 'updateView');
ApiLogsLogic.actions.fetchApiLogs();
await nextTick();
expect(ApiLogsLogic.actions.updateView).toHaveBeenCalledWith(MOCK_API_RESPONSE);
});
it('handles API errors', async () => {
http.get.mockReturnValueOnce(Promise.reject('error'));
mount();
ApiLogsLogic.actions.fetchApiLogs();
await nextTick();
expect(flashAPIErrors).toHaveBeenCalledWith('error');
});
});
describe('poll fetch (interval)', () => {
it('does not automatically update the view', async () => {
http.get.mockReturnValueOnce(Promise.resolve(MOCK_API_RESPONSE));
mount({ dataLoading: false });
jest.spyOn(ApiLogsLogic.actions, 'onPollInterval');
ApiLogsLogic.actions.fetchApiLogs({ isPoll: true });
await nextTick();
expect(ApiLogsLogic.actions.onPollInterval).toHaveBeenCalledWith(MOCK_API_RESPONSE);
});
it('sets a custom error message on poll error', async () => {
http.get.mockReturnValueOnce(Promise.reject('error'));
mount({ dataLoading: false });
ApiLogsLogic.actions.fetchApiLogs({ isPoll: true });
await nextTick();
expect(setErrorMessage).toHaveBeenCalledWith(POLLING_ERROR_MESSAGE);
});
});
describe('when a manual fetch and a poll fetch occur at the same time', () => {
it('should short-circuit polls in favor of manual fetches', async () => {
// dataLoading is the signal we're using to check for a manual fetch
mount({ dataLoading: true });
jest.spyOn(ApiLogsLogic.actions, 'onPollInterval');
ApiLogsLogic.actions.fetchApiLogs({ isPoll: true });
await nextTick();
expect(http.get).not.toHaveBeenCalled();
expect(ApiLogsLogic.actions.onPollInterval).not.toHaveBeenCalled();
});
});
});
describe('onPollInterval', () => {
describe('when API logs are empty and new polled data comes in', () => {
it('updates the view immediately with the returned data (no manual action required)', () => {
mount({ meta: { page: { total_results: 0 } } });
jest.spyOn(ApiLogsLogic.actions, 'updateView');
ApiLogsLogic.actions.onPollInterval(MOCK_API_RESPONSE);
expect(ApiLogsLogic.actions.updateView).toHaveBeenCalledWith(MOCK_API_RESPONSE);
});
});
describe('when previous API logs already exist on the page', () => {
describe('when new data is returned', () => {
it('stores the new polled data', () => {
mount({ meta: { page: { total_results: 1 } } });
jest.spyOn(ApiLogsLogic.actions, 'storePolledData');
ApiLogsLogic.actions.onPollInterval(MOCK_API_RESPONSE);
expect(ApiLogsLogic.actions.storePolledData).toHaveBeenCalledWith(MOCK_API_RESPONSE);
});
});
describe('when the same data is returned', () => {
it('does nothing', () => {
mount({ meta: { page: { total_results: 100 } } });
jest.spyOn(ApiLogsLogic.actions, 'updateView');
jest.spyOn(ApiLogsLogic.actions, 'storePolledData');
ApiLogsLogic.actions.onPollInterval(MOCK_API_RESPONSE);
expect(ApiLogsLogic.actions.updateView).not.toHaveBeenCalled();
expect(ApiLogsLogic.actions.storePolledData).not.toHaveBeenCalled();
});
});
});
});
describe('onUserRefresh', () => {
it('updates the apiLogs data with the stored polled data', () => {
mount({ apiLogs: [], polledData: MOCK_API_RESPONSE });
ApiLogsLogic.actions.onUserRefresh();
expect(ApiLogsLogic.values).toEqual({
...DEFAULT_VALUES,
apiLogs: MOCK_API_RESPONSE.results,
meta: MOCK_API_RESPONSE.meta,
polledData: MOCK_API_RESPONSE,
dataLoading: false,
});
});
});
});
describe('events', () => {
describe('unmount', () => {
const clearIntervalSpy = jest.spyOn(global, 'clearInterval');
it('clears the poll interval', () => {
mount({ intervalId: 123 });
unmount();
expect(clearIntervalSpy).toHaveBeenCalledWith(123);
});
it('does not clearInterval if a poll has not been started', () => {
mount({ intervalId: null });
unmount();
expect(clearIntervalSpy).not.toHaveBeenCalled();
});
});
});
});

View file

@ -0,0 +1,156 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { kea, MakeLogicType } from 'kea';
import { DEFAULT_META } from '../../../shared/constants';
import { flashAPIErrors, setErrorMessage } from '../../../shared/flash_messages';
import { HttpLogic } from '../../../shared/http';
import { updateMetaPageIndex } from '../../../shared/table_pagination';
import { EngineLogic } from '../engine';
import { POLLING_DURATION, POLLING_ERROR_MESSAGE } from './constants';
import { ApiLogsData, ApiLog } from './types';
import { getDateString } from './utils';
interface ApiLogsValues {
dataLoading: boolean;
apiLogs: ApiLog[];
meta: ApiLogsData['meta'];
hasNewData: boolean;
polledData: ApiLogsData;
intervalId: number | null;
}
interface ApiLogsActions {
fetchApiLogs(options?: { isPoll: boolean }): { isPoll: boolean };
pollForApiLogs(): void;
onPollStart(intervalId: number): { intervalId: number };
onPollInterval(data: ApiLogsData): ApiLogsData;
storePolledData(data: ApiLogsData): ApiLogsData;
updateView(data: ApiLogsData): ApiLogsData;
onUserRefresh(): void;
onPaginate(newPageIndex: number): { newPageIndex: number };
}
export const ApiLogsLogic = kea<MakeLogicType<ApiLogsValues, ApiLogsActions>>({
path: ['enterprise_search', 'app_search', 'api_logs_logic'],
actions: () => ({
fetchApiLogs: ({ isPoll } = { isPoll: false }) => ({ isPoll }),
pollForApiLogs: true,
onPollStart: (intervalId) => ({ intervalId }),
onPollInterval: ({ results, meta }) => ({ results, meta }),
storePolledData: ({ results, meta }) => ({ results, meta }),
updateView: ({ results, meta }) => ({ results, meta }),
onUserRefresh: true,
onPaginate: (newPageIndex) => ({ newPageIndex }),
}),
reducers: () => ({
dataLoading: [
true,
{
updateView: () => false,
onPaginate: () => true,
},
],
apiLogs: [
[],
{
updateView: (_, { results }) => results,
},
],
meta: [
DEFAULT_META,
{
updateView: (_, { meta }) => meta,
onPaginate: (state, { newPageIndex }) => updateMetaPageIndex(state, newPageIndex),
},
],
hasNewData: [
false,
{
storePolledData: () => true,
updateView: () => false,
},
],
polledData: [
{} as ApiLogsData,
{
storePolledData: (_, data) => data,
},
],
intervalId: [
null,
{
onPollStart: (_, { intervalId }) => intervalId,
},
],
}),
listeners: ({ actions, values }) => ({
pollForApiLogs: () => {
if (values.intervalId) return; // Ensure we only have one poll at a time
const id = window.setInterval(() => actions.fetchApiLogs({ isPoll: true }), POLLING_DURATION);
actions.onPollStart(id);
},
fetchApiLogs: async ({ isPoll }) => {
if (isPoll && values.dataLoading) return; // Manual fetches (i.e. user pagination) should override polling
const { http } = HttpLogic.values;
const { engineName } = EngineLogic.values;
try {
const response = await http.get(`/api/app_search/engines/${engineName}/api_logs`, {
query: {
'page[current]': values.meta.page.current,
'filters[date][from]': getDateString(-1),
'filters[date][to]': getDateString(),
sort_direction: 'desc',
},
});
// Manual fetches (e.g. page load, user pagination) should update the view immediately,
// while polls are stored in-state until the user manually triggers the 'Refresh' action
if (isPoll) {
actions.onPollInterval(response);
} else {
actions.updateView(response);
}
} catch (e) {
if (isPoll) {
// If polling fails, it will typically be due due to http connection -
// we should send a more human-readable message if so
setErrorMessage(POLLING_ERROR_MESSAGE);
} else {
flashAPIErrors(e);
}
}
},
onPollInterval: (data, breakpoint) => {
breakpoint(); // Prevents errors if logic unmounts while fetching
const previousResults = values.meta.page.total_results;
const newResults = data.meta.page.total_results;
const isEmpty = previousResults === 0;
const hasNewData = previousResults !== newResults;
if (isEmpty && hasNewData) {
actions.updateView(data); // Empty logs should automatically update with new data without a manual action
} else if (hasNewData) {
actions.storePolledData(data); // Otherwise, store any new data until the user manually refreshes the table
}
},
onUserRefresh: () => {
actions.updateView(values.polledData);
},
}),
events: ({ values }) => ({
beforeUnmount() {
if (values.intervalId !== null) clearInterval(values.intervalId);
},
}),
});

View file

@ -16,3 +16,13 @@ export const RECENT_API_EVENTS = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.apiLogs.recent',
{ defaultMessage: 'Recent API events' }
);
export const POLLING_DURATION = 5000;
export const POLLING_ERROR_MESSAGE = i18n.translate(
'xpack.enterpriseSearch.appSearch.engine.apiLogs.pollingErrorMessage',
{
defaultMessage:
'Could not automatically refresh API logs data. Please check your connection or manually refresh the page.',
}
);

View file

@ -7,3 +7,4 @@
export { API_LOGS_TITLE } from './constants';
export { ApiLogs } from './api_logs';
export { ApiLogsLogic } from './api_logs_logic';

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { Meta } from '../../../../../common/types';
export interface ApiLog {
timestamp: string; // Date ISO string
status: number;
http_method: string;
full_request_path: string;
user_agent: string;
request_body: string; // JSON string
response_body: string; // JSON string
// NOTE: The API also sends us back `path: null`, but we don't appear to be
// using it anywhere, so I've opted not to list it in our types
}
export interface ApiLogsData {
results: ApiLog[];
meta: Meta;
// NOTE: The API sends us back even more `meta` data than the normal (sort_direction, filters, query),
// but we currently don't use that data in our front-end code, so I'm opting not to list them in our types
}

View file

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { getDateString } from './utils';
describe('getDateString', () => {
const mockDate = jest
.spyOn(global.Date, 'now')
.mockImplementation(() => new Date('1970-01-02').valueOf());
it('gets the current date in ISO format', () => {
expect(getDateString()).toEqual('1970-01-02T00:00:00.000Z');
});
it('allows passing a number of days to offset the timestamp by', () => {
expect(getDateString(-1)).toEqual('1970-01-01T00:00:00.000Z');
expect(getDateString(10)).toEqual('1970-01-12T00:00:00.000Z');
});
afterAll(() => mockDate.mockRestore());
});

View file

@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export const getDateString = (offSetDays?: number) => {
const date = new Date(Date.now());
if (offSetDays) date.setDate(date.getDate() + offSetDays);
return date.toISOString();
};

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import { setMockActions } from '../../../../__mocks__';
import '../../../../__mocks__/shallow_useeffect.mock';
import '../../../__mocks__/engine_logic.mock';
import React from 'react';
@ -14,10 +16,16 @@ import { shallow, ShallowWrapper } from 'enzyme';
import { RecentApiLogs } from './recent_api_logs';
describe('RecentApiLogs', () => {
const actions = {
fetchApiLogs: jest.fn(),
pollForApiLogs: jest.fn(),
};
let wrapper: ShallowWrapper;
beforeAll(() => {
jest.clearAllMocks();
setMockActions(actions);
wrapper = shallow(<RecentApiLogs />);
});
@ -25,4 +33,9 @@ describe('RecentApiLogs', () => {
expect(wrapper.prop('title')).toEqual(<h2>Recent API events</h2>);
// TODO: expect(wrapper.find(ApiLogsTable)).toHaveLength(1)
});
it('calls fetchApiLogs on page load and starts pollForApiLogs', () => {
expect(actions.fetchApiLogs).toHaveBeenCalledTimes(1);
expect(actions.pollForApiLogs).toHaveBeenCalledTimes(1);
});
});

View file

@ -5,10 +5,13 @@
* 2.0.
*/
import React from 'react';
import React, { useEffect } from 'react';
import { useActions } from 'kea';
import { EuiButtonEmptyTo } from '../../../../shared/react_router_helpers';
import { ENGINE_API_LOGS_PATH } from '../../../routes';
import { ApiLogsLogic } from '../../api_logs';
import { RECENT_API_EVENTS } from '../../api_logs/constants';
import { DataPanel } from '../../data_panel';
import { generateEnginePath } from '../../engine';
@ -16,6 +19,13 @@ import { generateEnginePath } from '../../engine';
import { VIEW_API_LOGS } from '../constants';
export const RecentApiLogs: React.FC = () => {
const { fetchApiLogs, pollForApiLogs } = useActions(ApiLogsLogic);
useEffect(() => {
fetchApiLogs();
pollForApiLogs();
}, []);
return (
<DataPanel
title={<h2>{RECENT_API_EVENTS}</h2>}

View file

@ -0,0 +1,54 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__';
import { registerApiLogsRoutes } from './api_logs';
describe('API logs routes', () => {
describe('GET /api/app_search/engines/{engineName}/api_logs', () => {
let mockRouter: MockRouter;
beforeEach(() => {
jest.clearAllMocks();
mockRouter = new MockRouter({
method: 'get',
path: '/api/app_search/engines/{engineName}/api_logs',
});
registerApiLogsRoutes({
...mockDependencies,
router: mockRouter.router,
});
});
it('creates a request handler', () => {
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
path: '/as/engines/:engineName/api_logs/collection',
});
});
describe('validates', () => {
it('with required query params', () => {
const request = {
query: {
'filters[date][from]': '1970-01-01T12:00:00.000Z',
'filters[date][to]': '1970-01-02T12:00:00.000Z',
'page[current]': 1,
sort_direction: 'desc',
},
};
mockRouter.shouldValidate(request);
});
it('missing params', () => {
const request = { query: {} };
mockRouter.shouldThrow(request);
});
});
});
});

View file

@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { schema } from '@kbn/config-schema';
import { RouteDependencies } from '../../plugin';
export function registerApiLogsRoutes({
router,
enterpriseSearchRequestHandler,
}: RouteDependencies) {
router.get(
{
path: '/api/app_search/engines/{engineName}/api_logs',
validate: {
params: schema.object({
engineName: schema.string(),
}),
query: schema.object({
'filters[date][from]': schema.string(), // Date string, expected format: ISO string
'filters[date][to]': schema.string(), // Date string, expected format: ISO string
'page[current]': schema.number(),
sort_direction: schema.string(),
}),
},
},
enterpriseSearchRequestHandler.createRequest({
path: '/as/engines/:engineName/api_logs/collection',
})
);
}

View file

@ -8,6 +8,7 @@
import { RouteDependencies } from '../../plugin';
import { registerAnalyticsRoutes } from './analytics';
import { registerApiLogsRoutes } from './api_logs';
import { registerCredentialsRoutes } from './credentials';
import { registerCurationsRoutes } from './curations';
import { registerDocumentsRoutes, registerDocumentRoutes } from './documents';
@ -29,5 +30,6 @@ export const registerAppSearchRoutes = (dependencies: RouteDependencies) => {
registerSearchSettingsRoutes(dependencies);
registerRoleMappingsRoutes(dependencies);
registerResultSettingsRoutes(dependencies);
registerApiLogsRoutes(dependencies);
registerOnboardingRoutes(dependencies);
};