diff --git a/public/models/interfaces.ts b/public/models/interfaces.ts index c742dd4b..ca31246c 100644 --- a/public/models/interfaces.ts +++ b/public/models/interfaces.ts @@ -98,6 +98,9 @@ export type Detector = { windowDelay: { period: Schedule }; detectionInterval: { period: Schedule }; uiMetadata: UiMetaData; + enabled?: boolean; + enabledTime?: Date; + disabledTime?: Date; }; export type DetectorListItem = { diff --git a/public/redux/reducers/ad.ts b/public/redux/reducers/ad.ts index 7d4d9458..814ca0b0 100644 --- a/public/redux/reducers/ad.ts +++ b/public/redux/reducers/ad.ts @@ -31,6 +31,8 @@ const GET_DETECTOR_LIST = 'ad/GET_DETECTOR_LIST'; const UPDATE_DETECTOR = 'ad/UPDATE_DETECTOR'; const SEARCH_DETECTOR = 'ad/SEARCH_DETECTOR'; const DELETE_DETECTOR = 'ad/DELETE_DETECTOR'; +const START_DETECTOR = 'ad/START_DETECTOR'; +const STOP_DETECTOR = 'ad/STOP_DETECTOR'; export interface Detectors { requesting: boolean; @@ -92,6 +94,51 @@ const reducer = handleActions<Detectors>( errorMessage: action.error.data.error, }), }, + [START_DETECTOR]: { + REQUEST: (state: Detectors): Detectors => { + const newState = { ...state, requesting: true, errorMessage: '' }; + return newState; + }, + SUCCESS: (state: Detectors, action: APIResponseAction): Detectors => ({ + ...state, + requesting: false, + detectors: { + ...state.detectors, + [action.detectorId]: { + ...[action.detectorId], + enabled: true, + }, + }, + }), + FAILURE: (state: Detectors, action: APIErrorAction): Detectors => ({ + ...state, + requesting: false, + errorMessage: action.error, + }), + }, + + [STOP_DETECTOR]: { + REQUEST: (state: Detectors): Detectors => { + const newState = { ...state, requesting: true, errorMessage: '' }; + return newState; + }, + SUCCESS: (state: Detectors, action: APIResponseAction): Detectors => ({ + ...state, + requesting: false, + detectors: { + ...state.detectors, + [action.detectorId]: { + ...[action.detectorId], + enabled: false, + }, + }, + }), + FAILURE: (state: Detectors, action: APIErrorAction): Detectors => ({ + ...state, + requesting: false, + errorMessage: action.error, + }), + }, [SEARCH_DETECTOR]: { REQUEST: (state: Detectors): Detectors => ({ ...state, @@ -233,4 +280,22 @@ export const deleteDetector = (detectorId: string): APIAction => ({ detectorId, }); +export const startDetector = (detectorId: string): APIAction => ({ + type: START_DETECTOR, + request: (client: IHttpService) => + client.post(`..${AD_NODE_API.DETECTOR}/${detectorId}/start`, { + detectorId: detectorId, + }), + detectorId, +}); + +export const stopDetector = (detectorId: string): APIAction => ({ + type: STOP_DETECTOR, + request: (client: IHttpService) => + client.post(`..${AD_NODE_API.DETECTOR}/${detectorId}/stop`, { + detectorId: detectorId, + }), + detectorId, +}); + export default reducer; diff --git a/server/cluster/ad/adPlugin.ts b/server/cluster/ad/adPlugin.ts index ce8c5b3c..91cdf582 100644 --- a/server/cluster/ad/adPlugin.ts +++ b/server/cluster/ad/adPlugin.ts @@ -83,7 +83,7 @@ export default function adPlugin(Client: any, config: any, components: any) { }); ad.getDetector = ca({ url: { - fmt: `${API.DETECTOR_BASE}/<%=detectorId%>`, + fmt: `${API.DETECTOR_BASE}/<%=detectorId%>?job=true`, req: { detectorId: { type: 'string', @@ -100,4 +100,30 @@ export default function adPlugin(Client: any, config: any, components: any) { needBody: true, method: 'POST', }); + + ad.startDetector = ca({ + url: { + fmt: `${API.DETECTOR_BASE}/<%=detectorId%>/_start`, + req: { + detectorId: { + type: 'string', + required: true, + }, + }, + }, + method: 'POST', + }); + + ad.stopDetector = ca({ + url: { + fmt: `${API.DETECTOR_BASE}/<%=detectorId%>/_stop`, + req: { + detectorId: { + type: 'string', + required: true, + }, + }, + }, + method: 'POST', + }); } diff --git a/server/models/types.ts b/server/models/types.ts index 718611fe..452e2a2f 100644 --- a/server/models/types.ts +++ b/server/models/types.ts @@ -62,6 +62,9 @@ export type Detector = { windowDelay?: { period: Schedule }; detectionInterval?: { period: Schedule }; uiMetadata?: { [key: string]: any }; + enabled: boolean; + enabledTime?: Date; + disabledTime?: Date; }; export type GetDetectorsQueryParams = { diff --git a/server/routes/ad.ts b/server/routes/ad.ts index 9eaef539..3b851de8 100644 --- a/server/routes/ad.ts +++ b/server/routes/ad.ts @@ -55,6 +55,8 @@ export default function (apiRouter: Router) { apiRouter.post('/detectors/{detectorId}/preview', previewDetector); apiRouter.get('/detectors/{detectorId}/results', getAnomalyResults); apiRouter.delete('/detectors/{detectorId}', deleteDetector); + apiRouter.post('/detectors/{detectorId}/start', startDetector); + apiRouter.post('/detectors/{detectorId}/stop', stopDetector); } const deleteDetector = async ( @@ -165,6 +167,7 @@ const getDetector = async ( id: response._id, primaryTerm: response._primary_term, seqNo: response._seq_no, + adJob: { ...response.anomaly_detector_job }, }; return { ok: true, @@ -176,6 +179,46 @@ const getDetector = async ( } }; +const startDetector = async ( + req: Request, + h: ResponseToolkit, + callWithRequest: CallClusterWithRequest +): Promise<ServerResponse<AnomalyResults>> => { + try { + const { detectorId } = req.params; + const response = await callWithRequest(req, 'ad.startDetector', { + detectorId, + }); + return { + ok: true, + response: response, + }; + } catch (err) { + console.log('Anomaly detector - strartDetector', err); + return { ok: false, error: err.body || err.message }; + } +}; + +const stopDetector = async ( + req: Request, + h: ResponseToolkit, + callWithRequest: CallClusterWithRequest +): Promise<ServerResponse<AnomalyResults>> => { + try { + const { detectorId } = req.params; + const response = await callWithRequest(req, 'ad.stopDetector', { + detectorId, + }); + return { + ok: true, + response: response, + }; + } catch (err) { + console.log('Anomaly detector - stopDetector', err); + return { ok: false, error: err.body || err.message }; + } +}; + const searchDetector = async ( req: Request, h: ResponseToolkit, @@ -366,8 +409,8 @@ const getAnomalyResults = async ( const sortQueryMap = { anomalyGrade: { anomaly_grade: sortDirection }, confidence: { confidence: sortDirection }, - startTime: { start_time: sortDirection }, - endTime: { end_time: sortDirection }, + startTime: { data_start_time: sortDirection }, + endTime: { data_end_time: sortDirection }, } as { [key: string]: object }; let sort = {}; const sortQuery = sortQueryMap[sortField]; @@ -396,8 +439,8 @@ const getAnomalyResults = async ( // Get all detectors from search detector API const detectorResults: AnomalyResult[] = get(response, 'hits.hits', []).map( (result: any) => ({ - startTime: result._source.start_time, - endTime: result._source.end_time, + startTime: result._source.data_start_time, + endTime: result._source.data_end_time, confidence: result._source.confidence != null && result._source.confidence > 0 ? Number.parseFloat(result._source.confidence).toFixed(3) : 0, anomalyGrade: result._source.anomaly_grade != null && result._source.anomaly_grade > 0 ? Number.parseFloat(result._source.anomaly_grade).toFixed(3) : 0 }) diff --git a/server/routes/utils/__tests__/adHelpers.test.ts b/server/routes/utils/__tests__/adHelpers.test.ts index 3fa98f1a..be5e2b89 100644 --- a/server/routes/utils/__tests__/adHelpers.test.ts +++ b/server/routes/utils/__tests__/adHelpers.test.ts @@ -218,10 +218,10 @@ describe('adHelpers', () => { aggs: { total_anomalies_in_24hr: { filter: { - range: { start_time: { gte: 'now-24h', lte: 'now' } }, + range: { data_start_time: { gte: 'now-24h', lte: 'now' } }, }, }, - latest_anomaly_time: { max: { field: 'start_time' } }, + latest_anomaly_time: { max: { field: 'data_start_time' } }, }, }, }, @@ -261,10 +261,10 @@ describe('adHelpers', () => { aggs: { total_anomalies_in_24hr: { filter: { - range: { start_time: { gte: 'now-24h', lte: 'now' } }, + range: { data_start_time: { gte: 'now-24h', lte: 'now' } }, }, }, - latest_anomaly_time: { max: { field: 'start_time' } }, + latest_anomaly_time: { max: { field: 'data_start_time' } }, }, }, }, @@ -301,10 +301,10 @@ describe('adHelpers', () => { aggs: { total_anomalies_in_24hr: { filter: { - range: { start_time: { gte: 'now-24h', lte: 'now' } }, + range: { data_start_time: { gte: 'now-24h', lte: 'now' } }, }, }, - latest_anomaly_time: { max: { field: 'start_time' } }, + latest_anomaly_time: { max: { field: 'data_start_time' } }, }, }, }, @@ -341,10 +341,10 @@ describe('adHelpers', () => { aggs: { total_anomalies_in_24hr: { filter: { - range: { start_time: { gte: 'now-24h', lte: 'now' } }, + range: { data_start_time: { gte: 'now-24h', lte: 'now' } }, }, }, - latest_anomaly_time: { max: { field: 'start_time' } }, + latest_anomaly_time: { max: { field: 'data_start_time' } }, }, }, }, diff --git a/server/routes/utils/adHelpers.ts b/server/routes/utils/adHelpers.ts index a998289c..c1ed3b17 100644 --- a/server/routes/utils/adHelpers.ts +++ b/server/routes/utils/adHelpers.ts @@ -45,6 +45,7 @@ export const convertDetectorKeysToCamelCase = (response: object) => { 'ui_metadata', 'feature_query', 'feature_attributes', + 'adJob', ]), toCamel ), @@ -56,6 +57,13 @@ export const convertDetectorKeysToCamelCase = (response: object) => { }) ), uiMetadata: get(response, 'ui_metadata', {}), + enabled: get(response, 'adJob.enabled', false), + enabledTime: get(response, 'adJob.enabled_time') + ? new Date(get(response, 'adJob.enabled_time')) + : undefined, + disabledTime: get(response, 'adJob.disabled_time') + ? new Date(get(response, 'adJob.disabled_time')) + : undefined, }; }; @@ -97,9 +105,9 @@ export const getResultAggregationQuery = ( }, aggs: { total_anomalies_in_24hr: { - filter: { range: { start_time: { gte: 'now-24h', lte: 'now' } } }, + filter: { range: { data_start_time: { gte: 'now-24h', lte: 'now' } } }, }, - latest_anomaly_time: { max: { field: 'start_time' } }, + latest_anomaly_time: { max: { field: 'data_start_time' } }, }, }, }, @@ -118,8 +126,9 @@ export const anomalyResultMapper = (anomalyResults: any[]): AnomalyResults => { resultData.featureData[feature.featureId] = []; }); anomalyResults.forEach(({ featureData, ...rest }) => { + const { dataStartTime, dataEndTime, ...others } = rest; resultData.anomalies.push({ - ...rest, + ...others, anomalyGrade: rest.anomalyGrade != null && rest.anomalyGrade > 0 ? Number.parseFloat(rest.anomalyGrade).toFixed(3) @@ -128,15 +137,17 @@ export const anomalyResultMapper = (anomalyResults: any[]): AnomalyResults => { rest.anomalyGrade != null && rest.anomalyGrade > 0 ? Number.parseFloat(rest.confidence).toFixed(3) : 0, + startTime: rest.dataStartTime, + endTime: rest.dataEndTime, plotTime: - rest.startTime + Math.floor((rest.endTime - rest.startTime) / 2), + rest.dataStartTime + Math.floor((rest.dataEndTime - rest.dataStartTime) / 2), }); featureData.forEach((feature: any) => { resultData.featureData[feature.featureId].push({ - startTime: rest.startTime, - endTime: rest.endTime, + startTime: rest.dataStartTime, + endTime: rest.dataEndTime, plotTime: - rest.startTime + Math.floor((rest.endTime - rest.startTime) / 2), + rest.dataStartTime + Math.floor((rest.dataEndTime - rest.dataStartTime) / 2), data: feature.data, }); });