diff --git a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts index 5a1412fd8f3d4..dfc3d2aabd11a 100644 --- a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts +++ b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts @@ -29,7 +29,7 @@ export type GetLogEntryRateRequestPayload = rt.TypeOf; + +export const logEntryRateHistogramBucketRT = rt.type({ partitions: rt.array(logEntryRatePartitionRT), startTime: rt.number, }); +export type LogEntryRateHistogramBucket = rt.TypeOf; + export const getLogEntryRateSuccessReponsePayloadRT = rt.type({ data: rt.type({ bucketDuration: rt.number, - histogramBuckets: rt.array(logEntryRateHistogramBucket), + histogramBuckets: rt.array(logEntryRateHistogramBucketRT), totalNumberOfLogEntries: rt.number, }), }); diff --git a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/index.ts b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/index.ts index 727faca69298e..f23ef7ee7c302 100644 --- a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/index.ts +++ b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './indices'; +export * from './log_entry_rate_indices'; diff --git a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/indices.ts b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/log_entry_rate_indices.ts similarity index 74% rename from x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/indices.ts rename to x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/log_entry_rate_indices.ts index 62d81dc136853..5b2509074f6ed 100644 --- a/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/indices.ts +++ b/x-pack/legacy/plugins/infra/common/http_api/log_analysis/validation/log_entry_rate_indices.ts @@ -6,14 +6,24 @@ import * as rt from 'io-ts'; -export const LOG_ANALYSIS_VALIDATION_INDICES_PATH = '/api/infra/log_analysis/validation/indices'; +export const LOG_ANALYSIS_VALIDATE_INDICES_PATH = + '/api/infra/log_analysis/validation/log_entry_rate_indices'; /** * Request types */ +export const validationIndicesFieldSpecificationRT = rt.type({ + name: rt.string, + validTypes: rt.array(rt.string), +}); + +export type ValidationIndicesFieldSpecification = rt.TypeOf< + typeof validationIndicesFieldSpecificationRT +>; + export const validationIndicesRequestPayloadRT = rt.type({ data: rt.type({ - timestampField: rt.string, + fields: rt.array(validationIndicesFieldSpecificationRT), indices: rt.array(rt.string), }), }); diff --git a/x-pack/legacy/plugins/infra/common/log_analysis/job_parameters.ts b/x-pack/legacy/plugins/infra/common/log_analysis/job_parameters.ts index 5cfe38394a2ce..626e90b65a7d8 100644 --- a/x-pack/legacy/plugins/infra/common/log_analysis/job_parameters.ts +++ b/x-pack/legacy/plugins/infra/common/log_analysis/job_parameters.ts @@ -4,19 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import { JobType } from './log_analysis'; +import * as rt from 'io-ts'; export const bucketSpan = 900000; +export const partitionField = 'event.dataset'; + export const getJobIdPrefix = (spaceId: string, sourceId: string) => `kibana-logs-ui-${spaceId}-${sourceId}-`; -export const getJobId = (spaceId: string, sourceId: string, jobType: JobType) => +export const getJobId = (spaceId: string, sourceId: string, jobType: string) => `${getJobIdPrefix(spaceId, sourceId)}${jobType}`; -export const getDatafeedId = (spaceId: string, sourceId: string, jobType: JobType) => +export const getDatafeedId = (spaceId: string, sourceId: string, jobType: string) => `datafeed-${getJobId(spaceId, sourceId, jobType)}`; -export const getAllModuleJobIds = (spaceId: string, sourceId: string) => [ - getJobId(spaceId, sourceId, 'log-entry-rate'), -]; +export const jobSourceConfigurationRT = rt.type({ + indexPattern: rt.string, + timestampField: rt.string, + bucketSpan: rt.number, +}); + +export type JobSourceConfiguration = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/analyze_in_ml_button.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_results/analyze_in_ml_button.tsx similarity index 96% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/analyze_in_ml_button.tsx rename to x-pack/legacy/plugins/infra/public/components/logging/log_analysis_results/analyze_in_ml_button.tsx index ef81f229034bd..c5d83e1c205cc 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/analyze_in_ml_button.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_results/analyze_in_ml_button.tsx @@ -4,14 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import url from 'url'; import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; +import { encode } from 'rison-node'; import chrome from 'ui/chrome'; import { QueryString } from 'ui/utils/query_string'; -import { encode } from 'rison-node'; -import { TimeRange } from '../../../../../common/http_api/shared/time_range'; +import url from 'url'; + +import { TimeRange } from '../../../../common/http_api/shared/time_range'; export const AnalyzeInMlButton: React.FunctionComponent<{ jobId: string; diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_results/index.ts b/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_results/index.ts new file mode 100644 index 0000000000000..8a4ceb70252a3 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_analysis_results/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './analyze_in_ml_button'; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_api_types.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_api_types.ts index deb3d528e42c2..9d4d419ceebe3 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_api_types.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_api_types.ts @@ -6,11 +6,30 @@ import * as rt from 'io-ts'; +import { jobSourceConfigurationRT } from '../../../../../common/log_analysis'; + export const jobCustomSettingsRT = rt.partial({ job_revision: rt.number, - logs_source_config: rt.partial({ - indexPattern: rt.string, - timestampField: rt.string, - bucketSpan: rt.number, + logs_source_config: rt.partial(jobSourceConfigurationRT.props), +}); + +export const getMlCapabilitiesResponsePayloadRT = rt.type({ + capabilities: rt.type({ + canGetJobs: rt.boolean, + canCreateJob: rt.boolean, + canDeleteJob: rt.boolean, + canOpenJob: rt.boolean, + canCloseJob: rt.boolean, + canForecastJob: rt.boolean, + canGetDatafeeds: rt.boolean, + canStartStopDatafeed: rt.boolean, + canUpdateJob: rt.boolean, + canUpdateDatafeed: rt.boolean, + canPreviewDatafeed: rt.boolean, }), + isPlatinumOrTrialLicense: rt.boolean, + mlFeatureEnabledInSpace: rt.boolean, + upgradeInProgress: rt.boolean, }); + +export type GetMlCapabilitiesResponsePayload = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_cleanup.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_cleanup.ts index 209da920c4c8b..5054f607fa5dc 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_cleanup.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_cleanup.ts @@ -9,17 +9,22 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { kfetch } from 'ui/kfetch'; -import { getAllModuleJobIds, getDatafeedId } from '../../../../../common/log_analysis'; + +import { getDatafeedId, getJobId } from '../../../../../common/log_analysis'; import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; -export const callDeleteJobs = async (spaceId: string, sourceId: string) => { +export const callDeleteJobs = async ( + spaceId: string, + sourceId: string, + jobTypes: JobType[] +) => { // NOTE: Deleting the jobs via this API will delete the datafeeds at the same time const deleteJobsResponse = await kfetch({ method: 'POST', pathname: '/api/ml/jobs/delete_jobs', body: JSON.stringify( deleteJobsRequestPayloadRT.encode({ - jobIds: getAllModuleJobIds(spaceId, sourceId), + jobIds: jobTypes.map(jobType => getJobId(spaceId, sourceId, jobType)), }) ), }); @@ -42,15 +47,24 @@ export const callGetJobDeletionTasks = async () => { ); }; -export const callStopDatafeed = async (spaceId: string, sourceId: string) => { +export const callStopDatafeeds = async ( + spaceId: string, + sourceId: string, + jobTypes: JobType[] +) => { // Stop datafeed due to https://github.com/elastic/kibana/issues/44652 const stopDatafeedResponse = await kfetch({ method: 'POST', - pathname: `/api/ml/datafeeds/${getDatafeedId(spaceId, sourceId, 'log-entry-rate')}/_stop`, + pathname: '/api/ml/jobs/stop_datafeeds', + body: JSON.stringify( + stopDatafeedsRequestPayloadRT.encode({ + datafeedIds: jobTypes.map(jobType => getDatafeedId(spaceId, sourceId, jobType)), + }) + ), }); return pipe( - stopDatafeedResponsePayloadRT.decode(stopDatafeedResponse), + stopDatafeedsResponsePayloadRT.decode(stopDatafeedResponse), fold(throwErrors(createPlainError), identity) ); }; @@ -68,10 +82,19 @@ export const deleteJobsResponsePayloadRT = rt.record( }) ); +export type DeleteJobsResponsePayload = rt.TypeOf; + export const getJobDeletionTasksResponsePayloadRT = rt.type({ jobIds: rt.array(rt.string), }); -export const stopDatafeedResponsePayloadRT = rt.type({ - stopped: rt.boolean, +export const stopDatafeedsRequestPayloadRT = rt.type({ + datafeedIds: rt.array(rt.string), }); + +export const stopDatafeedsResponsePayloadRT = rt.record( + rt.string, + rt.type({ + stopped: rt.boolean, + }) +); diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts index 6171d10b5f1aa..91e517b0db008 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_get_jobs_summary_api.ts @@ -12,15 +12,19 @@ import { kfetch } from 'ui/kfetch'; import { jobCustomSettingsRT } from './ml_api_types'; import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; -import { getAllModuleJobIds } from '../../../../../common/log_analysis'; +import { getJobId } from '../../../../../common/log_analysis'; -export const callJobsSummaryAPI = async (spaceId: string, sourceId: string) => { +export const callJobsSummaryAPI = async ( + spaceId: string, + sourceId: string, + jobTypes: JobType[] +) => { const response = await kfetch({ method: 'POST', pathname: '/api/ml/jobs/jobs_summary', body: JSON.stringify( fetchJobStatusRequestPayloadRT.encode({ - jobIds: getAllModuleJobIds(spaceId, sourceId), + jobIds: jobTypes.map(jobType => getJobId(spaceId, sourceId, jobType)), }) ), }); diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts index 1c937513c7950..80a4f975cdd57 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts @@ -12,7 +12,6 @@ import { kfetch } from 'ui/kfetch'; import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; import { getJobIdPrefix } from '../../../../../common/log_analysis'; -import { jobCustomSettingsRT } from './ml_api_types'; export const callSetupMlModuleAPI = async ( moduleId: string, @@ -21,8 +20,8 @@ export const callSetupMlModuleAPI = async ( spaceId: string, sourceId: string, indexPattern: string, - timeField: string, - bucketSpan: number + jobOverrides: SetupMlModuleJobOverrides[] = [], + datafeedOverrides: SetupMlModuleDatafeedOverrides[] = [] ) => { const response = await kfetch({ method: 'POST', @@ -34,25 +33,8 @@ export const callSetupMlModuleAPI = async ( indexPatternName: indexPattern, prefix: getJobIdPrefix(spaceId, sourceId), startDatafeed: true, - jobOverrides: [ - { - job_id: 'log-entry-rate' as const, - analysis_config: { - bucket_span: `${bucketSpan}ms`, - }, - data_description: { - time_field: timeField, - }, - custom_settings: { - logs_source_config: { - indexPattern, - timestampField: timeField, - bucketSpan, - }, - }, - }, - ], - datafeedOverrides: [], + jobOverrides, + datafeedOverrides, }) ), }); @@ -68,23 +50,20 @@ const setupMlModuleTimeParamsRT = rt.partial({ end: rt.number, }); -const setupMlModuleLogEntryRateJobOverridesRT = rt.type({ - job_id: rt.literal('log-entry-rate'), - analysis_config: rt.type({ - bucket_span: rt.string, - }), - data_description: rt.type({ - time_field: rt.string, - }), - custom_settings: jobCustomSettingsRT, -}); +const setupMlModuleJobOverridesRT = rt.object; + +export type SetupMlModuleJobOverrides = rt.TypeOf; + +const setupMlModuleDatafeedOverridesRT = rt.object; + +export type SetupMlModuleDatafeedOverrides = rt.TypeOf; const setupMlModuleRequestParamsRT = rt.type({ indexPatternName: rt.string, prefix: rt.string, startDatafeed: rt.boolean, - jobOverrides: rt.array(setupMlModuleLogEntryRateJobOverridesRT), - datafeedOverrides: rt.array(rt.object), + jobOverrides: rt.array(setupMlModuleJobOverridesRT), + datafeedOverrides: rt.array(setupMlModuleDatafeedOverridesRT), }); const setupMlModuleRequestPayloadRT = rt.intersection([ diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/index_patterns_validate.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/validate_indices.ts similarity index 70% rename from x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/index_patterns_validate.ts rename to x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/validate_indices.ts index 440ee10e4223d..0d2e9b673488e 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/index_patterns_validate.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/validate_indices.ts @@ -10,20 +10,22 @@ import { identity } from 'fp-ts/lib/function'; import { kfetch } from 'ui/kfetch'; import { - LOG_ANALYSIS_VALIDATION_INDICES_PATH, + LOG_ANALYSIS_VALIDATE_INDICES_PATH, + ValidationIndicesFieldSpecification, validationIndicesRequestPayloadRT, validationIndicesResponsePayloadRT, } from '../../../../../common/http_api'; import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; -export const callIndexPatternsValidate = async (timestampField: string, indices: string[]) => { +export const callValidateIndicesAPI = async ( + indices: string[], + fields: ValidationIndicesFieldSpecification[] +) => { const response = await kfetch({ method: 'POST', - pathname: LOG_ANALYSIS_VALIDATION_INDICES_PATH, - body: JSON.stringify( - validationIndicesRequestPayloadRT.encode({ data: { timestampField, indices } }) - ), + pathname: LOG_ANALYSIS_VALIDATE_INDICES_PATH, + body: JSON.stringify(validationIndicesRequestPayloadRT.encode({ data: { indices, fields } })), }); return pipe( diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/index.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/index.ts index cbe3b2ef1e9b8..eb044c86e50fe 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/index.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/index.ts @@ -6,7 +6,6 @@ export * from './log_analysis_capabilities'; export * from './log_analysis_cleanup'; -export * from './log_analysis_jobs'; -export * from './log_analysis_results'; -export * from './log_analysis_results_url_state'; -export * from './log_analysis_status_state'; +export * from './log_analysis_module'; +export * from './log_analysis_module_status'; +export * from './log_analysis_module_types'; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_capabilities.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_capabilities.tsx index 7ac7d051e6783..35a3ac737ada3 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_capabilities.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_capabilities.tsx @@ -15,7 +15,7 @@ import { useTrackedPromise } from '../../../utils/use_tracked_promise'; import { getMlCapabilitiesResponsePayloadRT, GetMlCapabilitiesResponsePayload, -} from './ml_api_types'; +} from './api/ml_api_types'; import { throwErrors, createPlainError } from '../../../../common/runtime_types'; export const useLogAnalysisCapabilities = () => { diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_cleanup.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_cleanup.tsx index 1b79d3c1ef786..a37d18cc33cfd 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_cleanup.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_cleanup.tsx @@ -4,64 +4,46 @@ * you may not use this file except in compliance with the Elastic License. */ -import createContainer from 'constate'; -import { useMemo } from 'react'; -import { useTrackedPromise } from '../../../utils/use_tracked_promise'; -import { callDeleteJobs, callStopDatafeed, callGetJobDeletionTasks } from './api/ml_cleanup'; -import { getAllModuleJobIds } from '../../../../common/log_analysis'; - -export const useLogAnalysisCleanup = ({ - sourceId, - spaceId, -}: { - sourceId: string; - spaceId: string; -}) => { - const [cleanupMLResourcesRequest, cleanupMLResources] = useTrackedPromise( - { - cancelPreviousOn: 'resolution', - createPromise: async () => { - try { - await callStopDatafeed(spaceId, sourceId); - } catch (err) { - // Datefeed has been deleted / doesn't exist, proceed with deleting jobs anyway - if (err && err.res && err.res.status === 404) { - return await deleteJobs(spaceId, sourceId); - } else { - throw err; - } - } - - return await deleteJobs(spaceId, sourceId); - }, - }, - [spaceId, sourceId] - ); - - const isCleaningUp = useMemo(() => cleanupMLResourcesRequest.state === 'pending', [ - cleanupMLResourcesRequest.state, - ]); +import { getJobId } from '../../../../common/log_analysis'; +import { callDeleteJobs, callGetJobDeletionTasks, callStopDatafeeds } from './api/ml_cleanup'; + +export const cleanUpJobsAndDatafeeds = async ( + spaceId: string, + sourceId: string, + jobTypes: JobType[] +) => { + try { + await callStopDatafeeds(spaceId, sourceId, jobTypes); + } catch (err) { + // Proceed only if datafeed has been deleted or didn't exist in the first place + if (err?.res?.status !== 404) { + throw err; + } + } - return { - cleanupMLResources, - isCleaningUp, - }; + return await deleteJobs(spaceId, sourceId, jobTypes); }; -export const LogAnalysisCleanup = createContainer(useLogAnalysisCleanup); - -const deleteJobs = async (spaceId: string, sourceId: string) => { - const deleteJobsResponse = await callDeleteJobs(spaceId, sourceId); - await waitUntilJobsAreDeleted(spaceId, sourceId); +const deleteJobs = async ( + spaceId: string, + sourceId: string, + jobTypes: JobType[] +) => { + const deleteJobsResponse = await callDeleteJobs(spaceId, sourceId, jobTypes); + await waitUntilJobsAreDeleted(spaceId, sourceId, jobTypes); return deleteJobsResponse; }; -const waitUntilJobsAreDeleted = async (spaceId: string, sourceId: string) => { +const waitUntilJobsAreDeleted = async ( + spaceId: string, + sourceId: string, + jobTypes: JobType[] +) => { + const moduleJobIds = jobTypes.map(jobType => getJobId(spaceId, sourceId, jobType)); while (true) { - const response = await callGetJobDeletionTasks(); - const jobIdsBeingDeleted = response.jobIds; - const moduleJobIds = getAllModuleJobIds(spaceId, sourceId); + const { jobIds: jobIdsBeingDeleted } = await callGetJobDeletionTasks(); const needToWait = jobIdsBeingDeleted.some(jobId => moduleJobIds.includes(jobId)); + if (needToWait) { await timeout(1000); } else { diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx deleted file mode 100644 index 0f386f416b866..0000000000000 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_jobs.tsx +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import createContainer from 'constate'; -import { useMemo, useCallback, useEffect } from 'react'; - -import { callGetMlModuleAPI } from './api/ml_get_module'; -import { bucketSpan, getJobId } from '../../../../common/log_analysis'; -import { useTrackedPromise } from '../../../utils/use_tracked_promise'; -import { callJobsSummaryAPI } from './api/ml_get_jobs_summary_api'; -import { callSetupMlModuleAPI, SetupMlModuleResponsePayload } from './api/ml_setup_module_api'; -import { useLogAnalysisCleanup } from './log_analysis_cleanup'; -import { useStatusState } from './log_analysis_status_state'; - -const MODULE_ID = 'logs_ui_analysis'; - -export const useLogAnalysisJobs = ({ - indexPattern, - sourceId, - spaceId, - timeField, -}: { - indexPattern: string; - sourceId: string; - spaceId: string; - timeField: string; -}) => { - const { cleanupMLResources } = useLogAnalysisCleanup({ sourceId, spaceId }); - const [statusState, dispatch] = useStatusState({ - bucketSpan, - indexPattern, - timestampField: timeField, - }); - - const [fetchModuleDefinitionRequest, fetchModuleDefinition] = useTrackedPromise( - { - cancelPreviousOn: 'resolution', - createPromise: async () => { - dispatch({ type: 'fetchingModuleDefinition' }); - return await callGetMlModuleAPI(MODULE_ID); - }, - onResolve: response => { - dispatch({ - type: 'fetchedModuleDefinition', - spaceId, - sourceId, - moduleDefinition: response, - }); - }, - onReject: () => { - dispatch({ type: 'failedFetchingModuleDefinition' }); - }, - }, - [] - ); - - const [setupMlModuleRequest, setupMlModule] = useTrackedPromise( - { - cancelPreviousOn: 'resolution', - createPromise: async ( - indices: string[], - start: number | undefined, - end: number | undefined - ) => { - dispatch({ type: 'startedSetup' }); - return await callSetupMlModuleAPI( - MODULE_ID, - start, - end, - spaceId, - sourceId, - indices.join(','), - timeField, - bucketSpan - ); - }, - onResolve: ({ datafeeds, jobs }: SetupMlModuleResponsePayload) => { - dispatch({ type: 'finishedSetup', datafeeds, jobs, spaceId, sourceId }); - }, - onReject: () => { - dispatch({ type: 'failedSetup' }); - }, - }, - [spaceId, sourceId, timeField, bucketSpan] - ); - - const [fetchJobStatusRequest, fetchJobStatus] = useTrackedPromise( - { - cancelPreviousOn: 'resolution', - createPromise: async () => { - dispatch({ type: 'fetchingJobStatuses' }); - return await callJobsSummaryAPI(spaceId, sourceId); - }, - onResolve: jobResponse => { - dispatch({ type: 'fetchedJobStatuses', payload: jobResponse, spaceId, sourceId }); - }, - onReject: err => { - dispatch({ type: 'failedFetchingJobStatuses' }); - }, - }, - [spaceId, sourceId] - ); - - const isLoadingSetupStatus = useMemo( - () => - fetchJobStatusRequest.state === 'pending' || fetchModuleDefinitionRequest.state === 'pending', - [fetchJobStatusRequest.state, fetchModuleDefinitionRequest.state] - ); - - const availableIndices = useMemo(() => indexPattern.split(','), [indexPattern]); - - const viewResults = useCallback(() => { - dispatch({ type: 'viewedResults' }); - }, []); - - const cleanupAndSetup = useCallback( - (indices: string[], start: number | undefined, end: number | undefined) => { - dispatch({ type: 'startedSetup' }); - cleanupMLResources() - .then(() => { - setupMlModule(indices, start, end); - }) - .catch(() => { - dispatch({ type: 'failedSetup' }); - }); - }, - [cleanupMLResources, setupMlModule] - ); - - const viewSetupForReconfiguration = useCallback(() => { - dispatch({ type: 'requestedJobConfigurationUpdate' }); - }, []); - - const viewSetupForUpdate = useCallback(() => { - dispatch({ type: 'requestedJobDefinitionUpdate' }); - }, []); - - useEffect(() => { - fetchModuleDefinition(); - }, [fetchModuleDefinition]); - - const jobIds = useMemo(() => { - return { - 'log-entry-rate': getJobId(spaceId, sourceId, 'log-entry-rate'), - }; - }, [sourceId, spaceId]); - - return { - availableIndices, - fetchJobStatus, - isLoadingSetupStatus, - jobStatus: statusState.jobStatus, - lastSetupErrorMessages: statusState.lastSetupErrorMessages, - cleanupAndSetup, - setup: setupMlModule, - setupMlModuleRequest, - setupStatus: statusState.setupStatus, - timestampField: timeField, - viewSetupForReconfiguration, - viewSetupForUpdate, - viewResults, - jobIds, - }; -}; - -export const LogAnalysisJobs = createContainer(useLogAnalysisJobs); diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx new file mode 100644 index 0000000000000..189b58d7923f8 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx @@ -0,0 +1,167 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useCallback, useMemo } from 'react'; + +import { useTrackedPromise } from '../../../utils/use_tracked_promise'; +import { useModuleStatus } from './log_analysis_module_status'; +import { ModuleDescriptor, ModuleSourceConfiguration } from './log_analysis_module_types'; + +export const useLogAnalysisModule = ({ + sourceConfiguration, + moduleDescriptor, +}: { + sourceConfiguration: ModuleSourceConfiguration; + moduleDescriptor: ModuleDescriptor; +}) => { + const { spaceId, sourceId, timestampField, indices } = sourceConfiguration; + const [moduleStatus, dispatchModuleStatus] = useModuleStatus(moduleDescriptor.jobTypes, { + bucketSpan: moduleDescriptor.bucketSpan, + indexPattern: indices.join(','), + timestampField, + }); + + const [fetchModuleDefinitionRequest, fetchModuleDefinition] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async () => { + dispatchModuleStatus({ type: 'fetchingModuleDefinition' }); + return await moduleDescriptor.getModuleDefinition(); + }, + onResolve: response => { + dispatchModuleStatus({ + type: 'fetchedModuleDefinition', + spaceId, + sourceId, + moduleDefinition: response, + }); + }, + onReject: () => { + dispatchModuleStatus({ type: 'failedFetchingModuleDefinition' }); + }, + }, + [moduleDescriptor.getModuleDefinition, spaceId, sourceId] + ); + + const [fetchJobStatusRequest, fetchJobStatus] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async () => { + dispatchModuleStatus({ type: 'fetchingJobStatuses' }); + return await moduleDescriptor.getJobSummary(spaceId, sourceId); + }, + onResolve: jobResponse => { + dispatchModuleStatus({ + type: 'fetchedJobStatuses', + payload: jobResponse, + spaceId, + sourceId, + }); + }, + onReject: () => { + dispatchModuleStatus({ type: 'failedFetchingJobStatuses' }); + }, + }, + [spaceId, sourceId] + ); + + const isLoadingModuleStatus = useMemo( + () => + fetchJobStatusRequest.state === 'pending' || fetchModuleDefinitionRequest.state === 'pending', + [fetchJobStatusRequest.state, fetchModuleDefinitionRequest.state] + ); + + const [, setUpModule] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async ( + selectedIndices: string[], + start: number | undefined, + end: number | undefined + ) => { + dispatchModuleStatus({ type: 'startedSetup' }); + return await moduleDescriptor.setUpModule(start, end, { + indices: selectedIndices, + sourceId, + spaceId, + timestampField, + }); + }, + onResolve: ({ datafeeds, jobs }) => { + dispatchModuleStatus({ type: 'finishedSetup', datafeeds, jobs, spaceId, sourceId }); + }, + onReject: () => { + dispatchModuleStatus({ type: 'failedSetup' }); + }, + }, + [moduleDescriptor.setUpModule, spaceId, sourceId, timestampField] + ); + + const [cleanUpModuleRequest, cleanUpModule] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async () => { + return await moduleDescriptor.cleanUpModule(spaceId, sourceId); + }, + }, + [spaceId, sourceId] + ); + + const isCleaningUp = useMemo(() => cleanUpModuleRequest.state === 'pending', [ + cleanUpModuleRequest.state, + ]); + + const cleanUpAndSetUpModule = useCallback( + (selectedIndices: string[], start: number | undefined, end: number | undefined) => { + dispatchModuleStatus({ type: 'startedSetup' }); + cleanUpModule() + .then(() => { + setUpModule(selectedIndices, start, end); + }) + .catch(() => { + dispatchModuleStatus({ type: 'failedSetup' }); + }); + }, + [cleanUpModule, setUpModule] + ); + + const viewSetupForReconfiguration = useCallback(() => { + dispatchModuleStatus({ type: 'requestedJobConfigurationUpdate' }); + }, []); + + const viewSetupForUpdate = useCallback(() => { + dispatchModuleStatus({ type: 'requestedJobDefinitionUpdate' }); + }, []); + + const viewResults = useCallback(() => { + dispatchModuleStatus({ type: 'viewedResults' }); + }, []); + + const jobIds = useMemo(() => moduleDescriptor.getJobIds(spaceId, sourceId), [ + moduleDescriptor.getJobIds, + spaceId, + sourceId, + ]); + + return { + cleanUpAndSetUpModule, + cleanUpModule, + fetchJobStatus, + fetchModuleDefinition, + isCleaningUp, + isLoadingModuleStatus, + jobIds, + jobStatus: moduleStatus.jobStatus, + lastSetupErrorMessages: moduleStatus.lastSetupErrorMessages, + moduleDescriptor, + setUpModule, + setupStatus: moduleStatus.setupStatus, + sourceConfiguration, + viewResults, + viewSetupForReconfiguration, + viewSetupForUpdate, + }; +}; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_status_state.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx similarity index 70% rename from x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_status_state.tsx rename to x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx index 1f4c924ea3da5..6d634538cd7fe 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_status_state.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx @@ -7,20 +7,19 @@ import { useReducer } from 'react'; import { + JobSourceConfiguration, + JobStatus, + SetupStatus, getDatafeedId, getJobId, isJobStatusWithResults, - JobStatus, - JobType, - jobTypeRT, - SetupStatus, } from '../../../../common/log_analysis'; import { FetchJobStatusResponsePayload, JobSummary } from './api/ml_get_jobs_summary_api'; import { GetMlModuleResponsePayload, JobDefinition } from './api/ml_get_module'; import { SetupMlModuleResponsePayload } from './api/ml_setup_module_api'; import { MandatoryProperty } from '../../../../common/utility_types'; -interface StatusReducerState { +interface StatusReducerState { jobDefinitions: JobDefinition[]; jobStatus: Record; jobSummaries: JobSummary[]; @@ -65,42 +64,60 @@ type StatusReducerAction = | { type: 'requestedJobDefinitionUpdate' } | { type: 'viewedResults' }; -const createInitialState = (sourceConfiguration: JobSourceConfiguration): StatusReducerState => ({ +const createInitialState = ({ + jobTypes, + sourceConfiguration, +}: { + jobTypes: JobType[]; + sourceConfiguration: JobSourceConfiguration; +}): StatusReducerState => ({ jobDefinitions: [], - jobStatus: { - 'log-entry-rate': 'unknown', - }, + jobStatus: jobTypes.reduce( + (accumulatedJobStatus, jobType) => ({ + ...accumulatedJobStatus, + [jobType]: 'unknown', + }), + {} as Record + ), jobSummaries: [], lastSetupErrorMessages: [], setupStatus: 'initializing', sourceConfiguration, }); -function statusReducer(state: StatusReducerState, action: StatusReducerAction): StatusReducerState { +const createStatusReducer = (jobTypes: JobType[]) => ( + state: StatusReducerState, + action: StatusReducerAction +): StatusReducerState => { switch (action.type) { case 'startedSetup': { return { ...state, - jobStatus: { - 'log-entry-rate': 'initializing', - }, + jobStatus: jobTypes.reduce( + (accumulatedJobStatus, jobType) => ({ + ...accumulatedJobStatus, + [jobType]: 'initializing', + }), + {} as Record + ), setupStatus: 'pending', }; } case 'finishedSetup': { const { jobs, datafeeds, spaceId, sourceId } = action; - const nextJobStatus = { - ...state.jobStatus, - 'log-entry-rate': - hasSuccessfullyCreatedJob(getJobId(spaceId, sourceId, 'log-entry-rate'))(jobs) && - hasSuccessfullyStartedDatafeed(getDatafeedId(spaceId, sourceId, 'log-entry-rate'))( - datafeeds - ) - ? ('started' as JobStatus) - : ('failed' as JobStatus), - }; - const nextSetupStatus = Object.values(nextJobStatus).every(jobState => - ['started'].includes(jobState) + const nextJobStatus = jobTypes.reduce( + (accumulatedJobStatus, jobType) => ({ + ...accumulatedJobStatus, + [jobType]: + hasSuccessfullyCreatedJob(getJobId(spaceId, sourceId, jobType))(jobs) && + hasSuccessfullyStartedDatafeed(getDatafeedId(spaceId, sourceId, jobType))(datafeeds) + ? 'started' + : 'failed', + }), + {} as Record + ); + const nextSetupStatus = Object.values(nextJobStatus).every( + jobState => jobState === 'started' ) ? 'succeeded' : 'failed'; @@ -122,10 +139,13 @@ function statusReducer(state: StatusReducerState, action: StatusReducerAction): case 'failedSetup': { return { ...state, - jobStatus: { - ...state.jobStatus, - 'log-entry-rate': 'failed', - }, + jobStatus: jobTypes.reduce( + (accumulatedJobStatus, jobType) => ({ + ...accumulatedJobStatus, + [jobType]: 'failed', + }), + {} as Record + ), setupStatus: 'failed', }; } @@ -140,10 +160,13 @@ function statusReducer(state: StatusReducerState, action: StatusReducerAction): const { payload: jobSummaries, spaceId, sourceId } = action; const { jobDefinitions, setupStatus, sourceConfiguration } = state; - const nextJobStatus = { - ...state.jobStatus, - 'log-entry-rate': getJobStatus(getJobId(spaceId, sourceId, 'log-entry-rate'))(jobSummaries), - }; + const nextJobStatus = jobTypes.reduce( + (accumulatedJobStatus, jobType) => ({ + ...accumulatedJobStatus, + [jobType]: getJobStatus(getJobId(spaceId, sourceId, jobType))(jobSummaries), + }), + {} as Record + ); const nextSetupStatus = getSetupStatus( spaceId, sourceId, @@ -164,10 +187,13 @@ function statusReducer(state: StatusReducerState, action: StatusReducerAction): return { ...state, setupStatus: 'unknown', - jobStatus: { - ...state.jobStatus, - 'log-entry-rate': 'unknown', - }, + jobStatus: jobTypes.reduce( + (accumulatedJobStatus, jobType) => ({ + ...accumulatedJobStatus, + [jobType]: 'unknown', + }), + {} as Record + ), }; } case 'fetchedModuleDefinition': { @@ -230,7 +256,7 @@ function statusReducer(state: StatusReducerState, action: StatusReducerAction): return state; } } -} +}; const hasSuccessfullyCreatedJob = (jobId: string) => ( jobSetupResponses: SetupMlModuleResponsePayload['jobs'] @@ -281,7 +307,7 @@ const getJobStatus = (jobId: string) => (jobSummaries: FetchJobStatusResponsePay } )[0] || 'missing'; -const getSetupStatus = ( +const getSetupStatus = ( spaceId: string, sourceId: string, sourceConfiguration: JobSourceConfiguration, @@ -289,44 +315,43 @@ const getSetupStatus = ( jobDefinitions: JobDefinition[], jobSummaries: JobSummary[] ) => (previousSetupStatus: SetupStatus) => - Object.entries(everyJobStatus).reduce((setupStatus, [jobType, jobStatus]) => { - if (!jobTypeRT.is(jobType)) { - return setupStatus; - } + Object.entries(everyJobStatus).reduce( + (setupStatus, [jobType, jobStatus]) => { + const jobId = getJobId(spaceId, sourceId, jobType); + const jobDefinition = jobDefinitions.find(({ id }) => id === jobType); - const jobId = getJobId(spaceId, sourceId, jobType); - const jobDefinition = jobDefinitions.find(({ id }) => id === jobType); + if (jobStatus === 'missing') { + return 'required'; + } else if ( + setupStatus === 'required' || + setupStatus === 'requiredForUpdate' || + setupStatus === 'requiredForReconfiguration' + ) { + return setupStatus; + } else if ( + setupStatus === 'skippedButUpdatable' || + (jobDefinition && + !isJobRevisionCurrent( + jobId, + jobDefinition.config.custom_settings.job_revision || 0 + )(jobSummaries)) + ) { + return 'skippedButUpdatable'; + } else if ( + setupStatus === 'skippedButReconfigurable' || + !isJobConfigurationConsistent(jobId, sourceConfiguration)(jobSummaries) + ) { + return 'skippedButReconfigurable'; + } else if (setupStatus === 'hiddenAfterSuccess') { + return setupStatus; + } else if (setupStatus === 'skipped' || isJobStatusWithResults(jobStatus)) { + return 'skipped'; + } - if (jobStatus === 'missing') { - return 'required'; - } else if ( - setupStatus === 'required' || - setupStatus === 'requiredForUpdate' || - setupStatus === 'requiredForReconfiguration' - ) { return setupStatus; - } else if ( - setupStatus === 'skippedButUpdatable' || - (jobDefinition && - !isJobRevisionCurrent( - jobId, - jobDefinition.config.custom_settings.job_revision || 0 - )(jobSummaries)) - ) { - return 'skippedButUpdatable'; - } else if ( - setupStatus === 'skippedButReconfigurable' || - !isJobConfigurationConsistent(jobId, sourceConfiguration)(jobSummaries) - ) { - return 'skippedButReconfigurable'; - } else if (setupStatus === 'hiddenAfterSuccess') { - return setupStatus; - } else if (setupStatus === 'skipped' || isJobStatusWithResults(jobStatus)) { - return 'skipped'; - } - - return setupStatus; - }, previousSetupStatus); + }, + previousSetupStatus + ); const isJobRevisionCurrent = (jobId: string, currentRevision: number) => ( jobSummaries: FetchJobStatusResponsePayload @@ -377,12 +402,13 @@ const isIndexPatternSubset = (indexPatternSubset: string, indexPatternSuperset: const hasError = (value: Value): value is MandatoryProperty => value.error != null; -export const useStatusState = (sourceConfiguration: JobSourceConfiguration) => { - return useReducer(statusReducer, sourceConfiguration, createInitialState); +export const useModuleStatus = ( + jobTypes: JobType[], + sourceConfiguration: JobSourceConfiguration +) => { + return useReducer( + createStatusReducer(jobTypes), + { jobTypes, sourceConfiguration }, + createInitialState + ); }; - -interface JobSourceConfiguration { - bucketSpan: number; - indexPattern: string; - timestampField: string; -} diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_types.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_types.ts new file mode 100644 index 0000000000000..dc9f25b492635 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_types.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DeleteJobsResponsePayload } from './api/ml_cleanup'; +import { FetchJobStatusResponsePayload } from './api/ml_get_jobs_summary_api'; +import { GetMlModuleResponsePayload } from './api/ml_get_module'; +import { SetupMlModuleResponsePayload } from './api/ml_setup_module_api'; +import { ValidationIndicesResponsePayload } from '../../../../common/http_api/log_analysis'; + +export interface ModuleDescriptor { + moduleId: string; + jobTypes: JobType[]; + bucketSpan: number; + getJobIds: (spaceId: string, sourceId: string) => Record; + getJobSummary: (spaceId: string, sourceId: string) => Promise; + getModuleDefinition: () => Promise; + setUpModule: ( + start: number | undefined, + end: number | undefined, + sourceConfiguration: ModuleSourceConfiguration + ) => Promise; + cleanUpModule: (spaceId: string, sourceId: string) => Promise; + validateSetupIndices: ( + sourceConfiguration: ModuleSourceConfiguration + ) => Promise; +} + +export interface ModuleSourceConfiguration { + indices: string[]; + sourceId: string; + spaceId: string; + timestampField: string; +} diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx index c965c50bedccc..275c0194be3b2 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx @@ -4,15 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useState, useCallback, useMemo, useEffect } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { ValidationIndicesError } from '../../../../common/http_api'; import { isExampleDataIndex } from '../../../../common/log_analysis'; -import { - ValidationIndicesError, - ValidationIndicesResponsePayload, -} from '../../../../common/http_api'; import { useTrackedPromise } from '../../../utils/use_tracked_promise'; -import { callIndexPatternsValidate } from './api/index_patterns_validate'; +import { ModuleDescriptor, ModuleSourceConfiguration } from './log_analysis_module_types'; type SetupHandler = ( indices: string[], @@ -25,53 +22,69 @@ export type ValidationIndicesUIError = | { error: 'NETWORK_ERROR' } | { error: 'TOO_FEW_SELECTED_INDICES' }; -export interface ValidatedIndex { - index: string; - errors: ValidationIndicesError[]; +interface ValidIndex { + validity: 'valid'; + name: string; isSelected: boolean; } -interface AnalysisSetupStateArguments { - availableIndices: string[]; +interface InvalidIndex { + validity: 'invalid'; + name: string; + errors: ValidationIndicesError[]; +} + +export type ValidatedIndex = ValidIndex | InvalidIndex; + +interface AnalysisSetupStateArguments { cleanupAndSetupModule: SetupHandler; + moduleDescriptor: ModuleDescriptor; setupModule: SetupHandler; - timestampField: string; + sourceConfiguration: ModuleSourceConfiguration; } const fourWeeksInMs = 86400000 * 7 * 4; -export const useAnalysisSetupState = ({ - availableIndices, +export const useAnalysisSetupState = ({ cleanupAndSetupModule, + moduleDescriptor: { validateSetupIndices }, setupModule, - timestampField, -}: AnalysisSetupStateArguments) => { + sourceConfiguration, +}: AnalysisSetupStateArguments) => { const [startTime, setStartTime] = useState(Date.now() - fourWeeksInMs); const [endTime, setEndTime] = useState(undefined); - // Prepare the validation - const [validatedIndices, setValidatedIndices] = useState( - availableIndices.map(index => ({ - index, - errors: [], - isSelected: false, - })) - ); + const [validatedIndices, setValidatedIndices] = useState([]); + const [validateIndicesRequest, validateIndices] = useTrackedPromise( { cancelPreviousOn: 'resolution', createPromise: async () => { - return await callIndexPatternsValidate(timestampField, availableIndices); + return await validateSetupIndices(sourceConfiguration); }, - onResolve: ({ data }: ValidationIndicesResponsePayload) => { - setValidatedIndices( - availableIndices.map(index => { - const errors = data.errors.filter(error => error.index === index); - return { - index, - errors, - isSelected: errors.length === 0 && !isExampleDataIndex(index), - }; + onResolve: ({ data: { errors } }) => { + setValidatedIndices(previousValidatedIndices => + sourceConfiguration.indices.map(indexName => { + const previousValidatedIndex = previousValidatedIndices.filter( + ({ name }) => name === indexName + )[0]; + const indexValiationErrors = errors.filter(({ index }) => index === indexName); + if (indexValiationErrors.length > 0) { + return { + validity: 'invalid', + name: indexName, + errors: indexValiationErrors, + }; + } else { + return { + validity: 'valid', + name: indexName, + isSelected: + previousValidatedIndex?.validity === 'valid' + ? previousValidatedIndex?.isSelected + : !isExampleDataIndex(indexName), + }; + } }) ); }, @@ -79,7 +92,7 @@ export const useAnalysisSetupState = ({ setValidatedIndices([]); }, }, - [availableIndices, timestampField] + [sourceConfiguration.indices] ); useEffect(() => { @@ -87,7 +100,10 @@ export const useAnalysisSetupState = ({ }, [validateIndices]); const selectedIndexNames = useMemo( - () => validatedIndices.filter(i => i.isSelected).map(i => i.index), + () => + validatedIndices + .filter(index => index.validity === 'valid' && index.isSelected) + .map(i => i.name), [validatedIndices] ); @@ -120,7 +136,9 @@ export const useAnalysisSetupState = ({ } return validatedIndices.reduce((errors, index) => { - return selectedIndexNames.includes(index.index) ? errors.concat(index.errors) : errors; + return index.validity === 'invalid' && selectedIndexNames.includes(index.name) + ? [...errors, ...index.errors] + : errors; }, []); }, [selectedIndexNames, validatedIndices, validateIndicesRequest.state]); diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_entry_rate.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_entry_rate.tsx deleted file mode 100644 index 8b21a7e829894..0000000000000 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_entry_rate.tsx +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { useMemo, useState } from 'react'; - -import { GetLogEntryRateSuccessResponsePayload } from '../../../../common/http_api/log_analysis'; -import { useTrackedPromise } from '../../../utils/use_tracked_promise'; -import { callGetLogEntryRateAPI } from './api/get_log_entry_rate'; - -type LogEntryRateResults = GetLogEntryRateSuccessResponsePayload['data']; - -export const useLogEntryRate = ({ - sourceId, - startTime, - endTime, - bucketDuration, -}: { - sourceId: string; - startTime: number; - endTime: number; - bucketDuration: number; -}) => { - const [logEntryRate, setLogEntryRate] = useState(null); - - const [getLogEntryRateRequest, getLogEntryRate] = useTrackedPromise( - { - cancelPreviousOn: 'resolution', - createPromise: async () => { - return await callGetLogEntryRateAPI(sourceId, startTime, endTime, bucketDuration); - }, - onResolve: response => { - setLogEntryRate(response.data); - }, - }, - [sourceId, startTime, endTime, bucketDuration] - ); - - const isLoading = useMemo(() => getLogEntryRateRequest.state === 'pending', [ - getLogEntryRateRequest.state, - ]); - - return { - getLogEntryRate, - isLoading, - logEntryRate, - }; -}; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/ml_api_types.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/ml_api_types.ts deleted file mode 100644 index ee70edc31d49b..0000000000000 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/ml_api_types.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as rt from 'io-ts'; - -export const getMlCapabilitiesResponsePayloadRT = rt.type({ - capabilities: rt.type({ - canGetJobs: rt.boolean, - canCreateJob: rt.boolean, - canDeleteJob: rt.boolean, - canOpenJob: rt.boolean, - canCloseJob: rt.boolean, - canForecastJob: rt.boolean, - canGetDatafeeds: rt.boolean, - canStartStopDatafeed: rt.boolean, - canUpdateJob: rt.boolean, - canUpdateDatafeed: rt.boolean, - canPreviewDatafeed: rt.boolean, - }), - isPlatinumOrTrialLicense: rt.boolean, - mlFeatureEnabledInSpace: rt.boolean, - upgradeInProgress: rt.boolean, -}); - -export type GetMlCapabilitiesResponsePayload = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/public/containers/source/index.ts b/x-pack/legacy/plugins/infra/public/containers/source/index.ts index 9442836f2a6c6..5911decf21774 100644 --- a/x-pack/legacy/plugins/infra/public/containers/source/index.ts +++ b/x-pack/legacy/plugins/infra/public/containers/source/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { Source, useSource } from './source'; +export * from './source'; diff --git a/x-pack/legacy/plugins/infra/public/containers/source/source.tsx b/x-pack/legacy/plugins/infra/public/containers/source/source.tsx index 955529c9759c4..4729f7aa31f0b 100644 --- a/x-pack/legacy/plugins/infra/public/containers/source/source.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/source/source.tsx @@ -176,3 +176,4 @@ export const useSource = ({ sourceId }: { sourceId: string }) => { }; export const Source = createContainer(useSource); +export const [SourceProvider, useSourceContext] = Source; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx index 1630de11bbdff..4eddecf732f75 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/index.tsx @@ -21,7 +21,7 @@ import { Source, useSource } from '../../containers/source'; import { StreamPage } from './stream'; import { SettingsPage } from '../shared/settings'; import { AppNavigation } from '../../components/navigation/app_navigation'; -import { AnalysisPage } from './analysis'; +import { LogEntryRatePage } from './log_entry_rate'; import { useLogAnalysisCapabilities, LogAnalysisCapabilities, @@ -98,7 +98,7 @@ export const LogsPage = injectUICapabilities(({ match, uiCapabilities }: LogsPag - + + jobTypes.reduce( + (accumulatedJobIds, jobType) => ({ + ...accumulatedJobIds, + [jobType]: getJobId(spaceId, sourceId, jobType), + }), + {} as Record + ); + +const getJobSummary = async (spaceId: string, sourceId: string) => { + const response = await callJobsSummaryAPI(spaceId, sourceId, jobTypes); + const jobIds = Object.values(getJobIds(spaceId, sourceId)); + + return response.filter(jobSummary => jobIds.includes(jobSummary.id)); +}; + +const getModuleDefinition = async () => { + return await callGetMlModuleAPI(moduleId); +}; + +const setUpModule = async ( + start: number | undefined, + end: number | undefined, + { spaceId, sourceId, indices, timestampField }: ModuleSourceConfiguration +) => { + const indexNamePattern = indices.join(','); + const jobOverrides = [ + { + job_id: 'log-entry-rate' as const, + analysis_config: { + bucket_span: `${bucketSpan}ms`, + }, + data_description: { + time_field: timestampField, + }, + custom_settings: { + logs_source_config: { + indexPattern: indexNamePattern, + timestampField, + bucketSpan, + }, + }, + }, + ]; + + return callSetupMlModuleAPI( + moduleId, + start, + end, + spaceId, + sourceId, + indexNamePattern, + jobOverrides + ); +}; + +const cleanUpModule = async (spaceId: string, sourceId: string) => { + return await cleanUpJobsAndDatafeeds(spaceId, sourceId, jobTypes); +}; + +const validateSetupIndices = async ({ indices, timestampField }: ModuleSourceConfiguration) => { + return await callValidateIndicesAPI(indices, [ + { + name: timestampField, + validTypes: ['date'], + }, + { + name: partitionField, + validTypes: ['keyword'], + }, + ]); +}; + +export const logEntryRateModule: ModuleDescriptor = { + moduleId, + jobTypes, + bucketSpan, + getJobIds, + getJobSummary, + getModuleDefinition, + setUpModule, + cleanUpModule, + validateSetupIndices, +}; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page.tsx similarity index 53% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/page.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page.tsx index d82da895f9a5a..5ff5cd4db7168 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page.tsx @@ -7,15 +7,15 @@ import React from 'react'; import { ColumnarPage } from '../../../components/page'; -import { AnalysisPageContent } from './page_content'; -import { AnalysisPageProviders } from './page_providers'; +import { LogEntryRatePageContent } from './page_content'; +import { LogEntryRatePageProviders } from './page_providers'; -export const AnalysisPage = () => { +export const LogEntryRatePage = () => { return ( - - - + + + - + ); }; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx similarity index 59% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_content.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx index f0a26eae25ecb..e62164cb17b2c 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx @@ -9,34 +9,37 @@ import React, { useContext, useEffect } from 'react'; import { isSetupStatusWithResults } from '../../../../common/log_analysis'; import { LoadingPage } from '../../../components/loading_page'; -import { LogAnalysisCapabilities, LogAnalysisJobs } from '../../../containers/logs/log_analysis'; +import { LogAnalysisCapabilities } from '../../../containers/logs/log_analysis'; import { Source } from '../../../containers/source'; -import { AnalysisResultsContent } from './page_results_content'; -import { AnalysisSetupContent } from './page_setup_content'; -import { AnalysisUnavailableContent } from './page_unavailable_content'; -import { AnalysisSetupStatusUnknownContent } from './page_setup_status_unknown'; +import { LogEntryRateResultsContent } from './page_results_content'; +import { LogEntryRateSetupContent } from './page_setup_content'; +import { LogEntryRateUnavailableContent } from './page_unavailable_content'; +import { LogEntryRateSetupStatusUnknownContent } from './page_setup_status_unknown'; +import { useLogEntryRateModuleContext } from './use_log_entry_rate_module'; -export const AnalysisPageContent = () => { +export const LogEntryRatePageContent = () => { const { sourceId } = useContext(Source.Context); const { hasLogAnalysisCapabilites } = useContext(LogAnalysisCapabilities.Context); const { - availableIndices, - cleanupAndSetup, + cleanUpAndSetUpModule: cleanupAndSetup, fetchJobStatus, + fetchModuleDefinition, lastSetupErrorMessages, - setup, + moduleDescriptor, + setUpModule, setupStatus, - timestampField, + sourceConfiguration, viewResults, - } = useContext(LogAnalysisJobs.Context); + } = useLogEntryRateModuleContext(); useEffect(() => { + fetchModuleDefinition(); fetchJobStatus(); }, []); if (!hasLogAnalysisCapabilites) { - return ; + return ; } else if (setupStatus === 'initializing') { return ( { /> ); } else if (setupStatus === 'unknown') { - return ; + return ; } else if (isSetupStatusWithResults(setupStatus)) { return ( - ); } else { return ( - ); diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_providers.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx similarity index 53% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_providers.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx index fba32f6cbd6d0..67c8ea7660a26 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_providers.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_providers.tsx @@ -4,24 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; +import React from 'react'; -import { LogAnalysisJobs } from '../../../containers/logs/log_analysis'; -import { Source } from '../../../containers/source'; +import { useSourceContext } from '../../../containers/source'; import { useKibanaSpaceId } from '../../../utils/use_kibana_space_id'; +import { LogEntryRateModuleProvider } from './use_log_entry_rate_module'; -export const AnalysisPageProviders: React.FunctionComponent = ({ children }) => { - const { sourceId, source } = useContext(Source.Context); +export const LogEntryRatePageProviders: React.FunctionComponent = ({ children }) => { + const { sourceId, source } = useSourceContext(); const spaceId = useKibanaSpaceId(); return ( - {children} - + ); }; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_results_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx similarity index 78% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_results_content.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx index 7fa9ff3c93db7..be637bc29a0db 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_results_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx @@ -17,36 +17,36 @@ import { import numeral from '@elastic/numeral'; import { FormattedMessage } from '@kbn/i18n/react'; import moment from 'moment'; -import React, { useCallback, useContext, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState, useEffect } from 'react'; import euiStyled from '../../../../../../common/eui_styled_components'; import { TimeRange } from '../../../../common/http_api/shared/time_range'; import { bucketSpan } from '../../../../common/log_analysis'; import { LoadingOverlayWrapper } from '../../../components/loading_overlay_wrapper'; -import { - LogAnalysisJobs, - StringTimeRange, - useLogAnalysisResults, - useLogAnalysisResultsUrlState, -} from '../../../containers/logs/log_analysis'; import { useInterval } from '../../../hooks/use_interval'; import { useTrackPageview } from '../../../hooks/use_track_metric'; import { useKibanaUiSetting } from '../../../utils/use_kibana_ui_setting'; import { FirstUseCallout } from './first_use'; import { AnomaliesResults } from './sections/anomalies'; import { LogRateResults } from './sections/log_rate'; +import { useLogEntryRateModuleContext } from './use_log_entry_rate_module'; +import { useLogEntryRateResults } from './use_log_entry_rate_results'; +import { + StringTimeRange, + useLogAnalysisResultsUrlState, +} from './use_log_entry_rate_results_url_state'; const JOB_STATUS_POLLING_INTERVAL = 30000; -export const AnalysisResultsContent = ({ +export const LogEntryRateResultsContent = ({ sourceId, isFirstUse, }: { sourceId: string; isFirstUse: boolean; }) => { - useTrackPageview({ app: 'infra_logs', path: 'analysis_results' }); - useTrackPageview({ app: 'infra_logs', path: 'analysis_results', delay: 15000 }); + useTrackPageview({ app: 'infra_logs', path: 'log_entry_rate_results' }); + useTrackPageview({ app: 'infra_logs', path: 'log_entry_rate_results', delay: 15000 }); const [dateFormat] = useKibanaUiSetting('dateFormat', 'MMMM D, YYYY h:mm A'); @@ -65,32 +65,20 @@ export const AnalysisResultsContent = ({ lastChangedTime: Date.now(), })); - const bucketDuration = useMemo(() => { - // This function takes the current time range in ms, - // works out the bucket interval we'd need to always - // display 100 data points, and then takes that new - // value and works out the nearest multiple of - // 900000 (15 minutes) to it, so that we don't end up with - // jaggy bucket boundaries between the ML buckets and our - // aggregation buckets. - const msRange = moment(queryTimeRange.value.endTime).diff( - moment(queryTimeRange.value.startTime) - ); - const bucketIntervalInMs = msRange / 100; - const result = bucketSpan * Math.round(bucketIntervalInMs / bucketSpan); - const roundedResult = parseInt(Number(result).toFixed(0), 10); - return roundedResult < bucketSpan ? bucketSpan : roundedResult; - }, [queryTimeRange.value.startTime, queryTimeRange.value.endTime]); + const bucketDuration = useMemo( + () => getBucketDuration(queryTimeRange.value.startTime, queryTimeRange.value.endTime), + [queryTimeRange.value.endTime, queryTimeRange.value.startTime] + ); - const { isLoading, logRateResults } = useLogAnalysisResults({ + const { getLogEntryRate, isLoading, logEntryRate } = useLogEntryRateResults({ sourceId, startTime: queryTimeRange.value.startTime, endTime: queryTimeRange.value.endTime, bucketDuration, - lastRequestTime: queryTimeRange.lastChangedTime, }); - const hasResults = useMemo(() => logRateResults && logRateResults.histogramBuckets.length > 0, [ - logRateResults, + + const hasResults = useMemo(() => (logEntryRate?.histogramBuckets?.length ?? 0) > 0, [ + logEntryRate, ]); const handleQueryTimeRangeChange = useCallback( @@ -145,7 +133,11 @@ export const AnalysisResultsContent = ({ viewSetupForReconfiguration, viewSetupForUpdate, jobIds, - } = useContext(LogAnalysisJobs.Context); + } = useLogEntryRateModuleContext(); + + useEffect(() => { + getLogEntryRate(); + }, [getLogEntryRate, queryTimeRange.lastChangedTime]); useInterval(() => { fetchJobStatus(); @@ -168,7 +160,7 @@ export const AnalysisResultsContent = ({ - {logRateResults ? ( + {logEntryRate ? ( - {numeral(logRateResults.totalNumberOfLogEntries).format('0.00a')} + {numeral(logEntryRate.totalNumberOfLogEntries).format('0.00a')} ), @@ -210,7 +202,7 @@ export const AnalysisResultsContent = ({ {isFirstUse && !hasResults ? : null} @@ -223,7 +215,7 @@ export const AnalysisResultsContent = ({ jobStatus={jobStatus['log-entry-rate']} viewSetupForReconfiguration={viewSetupForReconfiguration} viewSetupForUpdate={viewSetupForUpdate} - results={logRateResults} + results={logEntryRate} setTimeRange={handleChartTimeRangeChange} setupStatus={setupStatus} timeRange={queryTimeRange.value} @@ -250,6 +242,23 @@ const stringToNumericTimeRange = (timeRange: StringTimeRange): TimeRange => ({ ).valueOf(), }); +/** + * This function takes the current time range in ms, + * works out the bucket interval we'd need to always + * display 100 data points, and then takes that new + * value and works out the nearest multiple of + * 900000 (15 minutes) to it, so that we don't end up with + * jaggy bucket boundaries between the ML buckets and our + * aggregation buckets. + */ +const getBucketDuration = (startTime: number, endTime: number) => { + const msRange = moment(endTime).diff(moment(startTime)); + const bucketIntervalInMs = msRange / 100; + const result = bucketSpan * Math.round(bucketIntervalInMs / bucketSpan); + const roundedResult = parseInt(Number(result).toFixed(0), 10); + return roundedResult < bucketSpan ? bucketSpan : roundedResult; +}; + // This is needed due to the flex-basis: 100% !important; rule that // kicks in on small screens via media queries breaking when using direction="column" export const ResultsContentPage = euiStyled(EuiPage)` diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_setup_content.tsx similarity index 70% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_setup_content.tsx index 7ae174c4a7899..6c04404b91231 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_setup_content.tsx @@ -4,23 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; import { EuiPage, EuiPageBody, EuiPageContent, + EuiPageContentBody, EuiPageContentHeader, EuiPageContentHeaderSection, - EuiPageContentBody, + EuiSpacer, EuiText, EuiTitle, - EuiSpacer, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; + import euiStyled from '../../../../../../common/eui_styled_components'; import { SetupStatus } from '../../../../common/log_analysis'; +import { ModuleDescriptor, ModuleSourceConfiguration } from '../../../containers/logs/log_analysis'; import { useTrackPageview } from '../../../hooks/use_track_metric'; -import { AnalysisSetupSteps } from './setup'; +import { LogEntryRateSetupSteps } from './setup'; type SetupHandler = ( indices: string[], @@ -28,32 +30,32 @@ type SetupHandler = ( endTime: number | undefined ) => void; -interface AnalysisSetupContentProps { - availableIndices: string[]; +interface LogEntryRateSetupContentProps { cleanupAndSetup: SetupHandler; errorMessages: string[]; + moduleDescriptor: ModuleDescriptor; setup: SetupHandler; setupStatus: SetupStatus; - timestampField: string; + sourceConfiguration: ModuleSourceConfiguration; viewResults: () => void; } -export const AnalysisSetupContent: React.FunctionComponent = ({ - availableIndices, +export const LogEntryRateSetupContent = ({ cleanupAndSetup, errorMessages, setup, setupStatus, - timestampField, viewResults, -}) => { - useTrackPageview({ app: 'infra_logs', path: 'analysis_setup' }); - useTrackPageview({ app: 'infra_logs', path: 'analysis_setup', delay: 15000 }); + moduleDescriptor, + sourceConfiguration, +}: LogEntryRateSetupContentProps) => { + useTrackPageview({ app: 'infra_logs', path: 'log_entry_rate_setup' }); + useTrackPageview({ app: 'infra_logs', path: 'log_entry_rate_setup', delay: 15000 }); return ( - + - - - + - + ); }; // !important due to https://github.com/elastic/eui/issues/2232 -const AnalysisPageContent = euiStyled(EuiPageContent)` +const LogEntryRateSetupPageContent = euiStyled(EuiPageContent)` max-width: 768px !important; `; -const AnalysisSetupPage = euiStyled(EuiPage)` +const LogEntryRateSetupPage = euiStyled(EuiPage)` height: 100%; `; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_status_unknown.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_setup_status_unknown.tsx similarity index 92% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_status_unknown.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_setup_status_unknown.tsx index 953b0841ffe92..4c685bd42b937 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/page_setup_status_unknown.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_setup_status_unknown.tsx @@ -14,7 +14,7 @@ interface Props { retry: () => void; } -export const AnalysisSetupStatusUnknownContent: React.FunctionComponent = ({ +export const LogEntryRateSetupStatusUnknownContent: React.FunctionComponent = ({ retry, }: Props) => ( = () => ( +export const LogEntryRateUnavailableContent: React.FunctionComponent<{}> = () => ( diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/chart.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/chart.tsx similarity index 100% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/chart.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/chart.tsx diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/expanded_row.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx similarity index 84% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/expanded_row.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx index 0586f5282ddf7..f8a7f12364cf9 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/expanded_row.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/expanded_row.tsx @@ -4,38 +4,37 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; -import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiStat } from '@elastic/eui'; import numeral from '@elastic/numeral'; -import { EuiFlexGroup, EuiFlexItem, EuiStat, EuiSpacer } from '@elastic/eui'; -import { AnomaliesChart } from './chart'; -import { LogRateResults } from '../../../../../containers/logs/log_analysis/log_analysis_results'; +import { i18n } from '@kbn/i18n'; +import React, { useMemo } from 'react'; + import { TimeRange } from '../../../../../../common/http_api/shared/time_range'; +import { AnalyzeInMlButton } from '../../../../../components/logging/log_analysis_results'; +import { LogEntryRateResults } from '../../use_log_entry_rate_results'; import { - getLogEntryRateSeriesForPartition, getAnnotationsForPartition, + getLogEntryRateSeriesForPartition, getTotalNumberOfLogEntriesForPartition, } from '../helpers/data_formatters'; -import { AnalyzeInMlButton } from '../analyze_in_ml_button'; +import { AnomaliesChart } from './chart'; export const AnomaliesTableExpandedRow: React.FunctionComponent<{ partitionId: string; topAnomalyScore: number; - results: LogRateResults; + results: LogEntryRateResults; setTimeRange: (timeRange: TimeRange) => void; timeRange: TimeRange; jobId: string; }> = ({ results, timeRange, setTimeRange, partitionId, jobId }) => { const logEntryRateSeries = useMemo( () => - results && results.histogramBuckets - ? getLogEntryRateSeriesForPartition(results, partitionId) - : [], + results?.histogramBuckets ? getLogEntryRateSeriesForPartition(results, partitionId) : [], [results, partitionId] ); const anomalyAnnotations = useMemo( () => - results && results.histogramBuckets + results?.histogramBuckets ? getAnnotationsForPartition(results, partitionId) : { warning: [], @@ -47,7 +46,7 @@ export const AnomaliesTableExpandedRow: React.FunctionComponent<{ ); const totalNumberOfLogEntries = useMemo( () => - results && results.histogramBuckets + results?.histogramBuckets ? getTotalNumberOfLogEntriesForPartition(results, partitionId) : undefined, [results, partitionId] diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/index.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx similarity index 97% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/index.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx index e870c2d442719..38aa4b068c9e9 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx @@ -18,7 +18,7 @@ import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import euiStyled from '../../../../../../../../common/eui_styled_components'; -import { LogRateResults } from '../../../../../containers/logs/log_analysis/log_analysis_results'; +import { LogEntryRateResults } from '../../use_log_entry_rate_results'; import { TimeRange } from '../../../../../../common/http_api/shared/time_range'; import { JobStatus, SetupStatus } from '../../../../../../common/log_analysis'; import { @@ -30,13 +30,13 @@ import { import { AnomaliesChart } from './chart'; import { AnomaliesTable } from './table'; import { LogAnalysisJobProblemIndicator } from '../../../../../components/logging/log_analysis_job_status'; -import { AnalyzeInMlButton } from '../analyze_in_ml_button'; +import { AnalyzeInMlButton } from '../../../../../components/logging/log_analysis_results'; import { LoadingOverlayWrapper } from '../../../../../components/loading_overlay_wrapper'; export const AnomaliesResults: React.FunctionComponent<{ isLoading: boolean; jobStatus: JobStatus; - results: LogRateResults | null; + results: LogEntryRateResults | null; setTimeRange: (timeRange: TimeRange) => void; setupStatus: SetupStatus; timeRange: TimeRange; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/table.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx similarity index 97% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/table.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx index c0016d07c290b..2057d75f72354 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/anomalies/table.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx @@ -9,7 +9,7 @@ import { EuiBasicTable, EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; import { TimeRange } from '../../../../../../common/http_api/shared/time_range'; -import { LogRateResults } from '../../../../../containers/logs/log_analysis/log_analysis_results'; +import { LogEntryRateResults } from '../../use_log_entry_rate_results'; import { AnomaliesTableExpandedRow } from './expanded_row'; import { formatAnomalyScore, getFriendlyNameForPartitionId } from '../helpers/data_formatters'; import euiStyled from '../../../../../../../../common/eui_styled_components'; @@ -50,7 +50,7 @@ const maxAnomalyScoreColumnName = i18n.translate( ); export const AnomaliesTable: React.FunctionComponent<{ - results: LogRateResults; + results: LogEntryRateResults; setTimeRange: (timeRange: TimeRange) => void; timeRange: TimeRange; jobId: string; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/helpers/data_formatters.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/helpers/data_formatters.tsx similarity index 88% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/helpers/data_formatters.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/helpers/data_formatters.tsx index 74a3b5f80a577..f9b85fc4e20c2 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/helpers/data_formatters.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/helpers/data_formatters.tsx @@ -6,18 +6,19 @@ import { RectAnnotationDatum } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; -import { LogRateResults } from '../../../../../containers/logs/log_analysis/log_analysis_results'; -export type MLSeverityScoreCategories = 'warning' | 'minor' | 'major' | 'critical'; -type MLSeverityScores = Record; -const ML_SEVERITY_SCORES: MLSeverityScores = { +import { LogEntryRateResults } from '../../use_log_entry_rate_results'; + +const ML_SEVERITY_SCORES = { warning: 3, minor: 25, major: 50, critical: 75, }; -export const getLogEntryRatePartitionedSeries = (results: LogRateResults) => { +export type MLSeverityScoreCategories = keyof typeof ML_SEVERITY_SCORES; + +export const getLogEntryRatePartitionedSeries = (results: LogEntryRateResults) => { return results.histogramBuckets.reduce>( (buckets, bucket) => { return [ @@ -33,7 +34,7 @@ export const getLogEntryRatePartitionedSeries = (results: LogRateResults) => { ); }; -export const getLogEntryRateCombinedSeries = (results: LogRateResults) => { +export const getLogEntryRateCombinedSeries = (results: LogEntryRateResults) => { return results.histogramBuckets.reduce>( (buckets, bucket) => { return [ @@ -50,7 +51,10 @@ export const getLogEntryRateCombinedSeries = (results: LogRateResults) => { ); }; -export const getLogEntryRateSeriesForPartition = (results: LogRateResults, partitionId: string) => { +export const getLogEntryRateSeriesForPartition = ( + results: LogEntryRateResults, + partitionId: string +) => { return results.partitionBuckets[partitionId].buckets.reduce< Array<{ time: number; value: number }> >((buckets, bucket) => { @@ -64,7 +68,7 @@ export const getLogEntryRateSeriesForPartition = (results: LogRateResults, parti }, []); }; -export const getAnnotationsForPartition = (results: LogRateResults, partitionId: string) => { +export const getAnnotationsForPartition = (results: LogEntryRateResults, partitionId: string) => { return results.partitionBuckets[partitionId].buckets.reduce< Record >( @@ -106,13 +110,13 @@ export const getAnnotationsForPartition = (results: LogRateResults, partitionId: }; export const getTotalNumberOfLogEntriesForPartition = ( - results: LogRateResults, + results: LogEntryRateResults, partitionId: string ) => { return results.partitionBuckets[partitionId].totalNumberOfLogEntries; }; -export const getAnnotationsForAll = (results: LogRateResults) => { +export const getAnnotationsForAll = (results: LogEntryRateResults) => { return results.histogramBuckets.reduce>( (annotatedBucketsBySeverity, bucket) => { const maxAnomalyScoresByPartition = bucket.partitions.reduce< @@ -169,7 +173,7 @@ export const getAnnotationsForAll = (results: LogRateResults) => { ); }; -export const getTopAnomalyScoreAcrossAllPartitions = (results: LogRateResults) => { +export const getTopAnomalyScoreAcrossAllPartitions = (results: LogEntryRateResults) => { const allTopScores = Object.values(results.partitionBuckets).reduce( (scores: number[], partition) => { return [...scores, partition.topAnomalyScore]; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/log_rate/bar_chart.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/log_rate/bar_chart.tsx similarity index 100% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/log_rate/bar_chart.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/log_rate/bar_chart.tsx diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/log_rate/index.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/log_rate/index.tsx similarity index 96% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/log_rate/index.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/log_rate/index.tsx index 44805520f3b9e..a11dc9d4d607a 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/sections/log_rate/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/log_rate/index.tsx @@ -8,7 +8,7 @@ import { EuiEmptyPrompt, EuiLoadingSpinner, EuiSpacer, EuiTitle, EuiText } from import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; -import { LogRateResults as Results } from '../../../../../containers/logs/log_analysis/log_analysis_results'; +import { LogEntryRateResults as Results } from '../../use_log_entry_rate_results'; import { TimeRange } from '../../../../../../common/http_api/shared/time_range'; import { LogEntryRateBarChart } from './bar_chart'; import { getLogEntryRatePartitionedSeries } from '../helpers/data_formatters'; diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/get_log_entry_rate.ts b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_rate.ts similarity index 100% rename from x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/api/get_log_entry_rate.ts rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_rate.ts diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/index.ts b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/index.ts similarity index 100% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/index.ts rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/index.ts diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/analysis_setup_indices_form.tsx similarity index 91% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/analysis_setup_indices_form.tsx index 585a65b9ad1c8..91662c49adace 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_indices_form.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/analysis_setup_indices_form.tsx @@ -25,7 +25,7 @@ export const AnalysisSetupIndicesForm: React.FunctionComponent<{ onChangeSelectedIndices( indices.map(index => { const checkbox = event.currentTarget; - return index.index === checkbox.id ? { ...index, isSelected: checkbox.checked } : index; + return index.name === checkbox.id ? { ...index, isSelected: checkbox.checked } : index; }) ); }, @@ -35,22 +35,21 @@ export const AnalysisSetupIndicesForm: React.FunctionComponent<{ const choices = useMemo( () => indices.map(index => { - const validIndex = index.errors.length === 0; const checkbox = ( {index.index}} + key={index.name} + id={index.name} + label={{index.name}} onChange={handleCheckboxChange} - checked={index.isSelected} - disabled={!validIndex} + checked={index.validity === 'valid' && index.isSelected} + disabled={index.validity === 'invalid'} /> ); - return validIndex ? ( + return index.validity === 'valid' ? ( checkbox ) : ( -
+
{checkbox}
); diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_timerange_form.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/analysis_setup_timerange_form.tsx similarity index 100% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/analysis_setup_timerange_form.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/analysis_setup_timerange_form.tsx diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/index.ts b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/index.ts similarity index 100% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/index.ts rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/index.ts diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/initial_configuration_step.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/initial_configuration_step.tsx similarity index 100% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/initial_configuration_step/initial_configuration_step.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/initial_configuration_step.tsx diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/create_ml_jobs_button.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/process_step/create_ml_jobs_button.tsx similarity index 100% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/create_ml_jobs_button.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/process_step/create_ml_jobs_button.tsx diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/index.ts b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/process_step/index.ts similarity index 100% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/index.ts rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/process_step/index.ts diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/process_step.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/process_step/process_step.tsx similarity index 100% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/process_step.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/process_step/process_step.tsx diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/recreate_ml_jobs_button.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/process_step/recreate_ml_jobs_button.tsx similarity index 100% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/process_step/recreate_ml_jobs_button.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/process_step/recreate_ml_jobs_button.tsx diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/setup_steps.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/setup_steps.tsx similarity index 84% rename from x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/setup_steps.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/setup_steps.tsx index 4643516e73fac..967c69dfae950 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/analysis/setup/setup_steps.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/setup_steps.tsx @@ -12,6 +12,10 @@ import { SetupStatus } from '../../../../../common/log_analysis'; import { useAnalysisSetupState } from '../../../../containers/logs/log_analysis/log_analysis_setup_state'; import { InitialConfigurationStep } from './initial_configuration_step'; import { ProcessStep } from './process_step'; +import { + ModuleDescriptor, + ModuleSourceConfiguration, +} from '../../../../containers/logs/log_analysis'; type SetupHandler = ( indices: string[], @@ -19,25 +23,25 @@ type SetupHandler = ( endTime: number | undefined ) => void; -interface AnalysisSetupStepsProps { - availableIndices: string[]; +interface LogEntryRateSetupStepsProps { cleanupAndSetup: SetupHandler; errorMessages: string[]; setup: SetupHandler; setupStatus: SetupStatus; - timestampField: string; viewResults: () => void; + moduleDescriptor: ModuleDescriptor; + sourceConfiguration: ModuleSourceConfiguration; } -export const AnalysisSetupSteps: React.FunctionComponent = ({ - availableIndices, +export const LogEntryRateSetupSteps = ({ cleanupAndSetup: cleanupAndSetupModule, errorMessages, setup: setupModule, setupStatus, - timestampField, viewResults, -}: AnalysisSetupStepsProps) => { + moduleDescriptor, + sourceConfiguration, +}: LogEntryRateSetupStepsProps) => { const { setup, cleanupAndSetup, @@ -50,10 +54,10 @@ export const AnalysisSetupSteps: React.FunctionComponent { + const sourceConfiguration: ModuleSourceConfiguration = useMemo( + () => ({ + indices: indexPattern.split(','), + sourceId, + spaceId, + timestampField, + }), + [indexPattern] + ); + + return useLogAnalysisModule({ + moduleDescriptor: logEntryRateModule, + sourceConfiguration, + }); +}; + +export const [LogEntryRateModuleProvider, useLogEntryRateModuleContext] = createContainer( + useLogEntryRateModule +); diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results.ts similarity index 58% rename from x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results.ts index 81a80fb565a4b..de2b873001cce 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results.ts @@ -4,75 +4,77 @@ * you may not use this file except in compliance with the Elastic License. */ -import createContainer from 'constate'; -import { useMemo, useEffect } from 'react'; +import { useMemo, useState } from 'react'; -import { useLogEntryRate } from './log_entry_rate'; -import { GetLogEntryRateSuccessResponsePayload } from '../../../../common/http_api/log_analysis'; +import { + GetLogEntryRateSuccessResponsePayload, + LogEntryRateHistogramBucket, + LogEntryRatePartition, +} from '../../../../common/http_api/log_analysis'; +import { useTrackedPromise } from '../../../utils/use_tracked_promise'; +import { callGetLogEntryRateAPI } from './service_calls/get_log_entry_rate'; -type PartitionBucket = { +type PartitionBucket = LogEntryRatePartition & { startTime: number; -} & GetLogEntryRateSuccessResponsePayload['data']['histogramBuckets'][0]['partitions'][0]; +}; type PartitionRecord = Record< string, { buckets: PartitionBucket[]; topAnomalyScore: number; totalNumberOfLogEntries: number } >; -export interface LogRateResults { +export interface LogEntryRateResults { bucketDuration: number; totalNumberOfLogEntries: number; - histogramBuckets: GetLogEntryRateSuccessResponsePayload['data']['histogramBuckets']; + histogramBuckets: LogEntryRateHistogramBucket[]; partitionBuckets: PartitionRecord; } -export const useLogAnalysisResults = ({ +export const useLogEntryRateResults = ({ sourceId, startTime, endTime, bucketDuration = 15 * 60 * 1000, - lastRequestTime, }: { sourceId: string; startTime: number; endTime: number; - bucketDuration?: number; - lastRequestTime: number; + bucketDuration: number; }) => { - const { isLoading: isLoadingLogEntryRate, logEntryRate, getLogEntryRate } = useLogEntryRate({ - sourceId, - startTime, - endTime, - bucketDuration, - }); - - const isLoading = useMemo(() => isLoadingLogEntryRate, [isLoadingLogEntryRate]); + const [logEntryRate, setLogEntryRate] = useState(null); - useEffect(() => { - getLogEntryRate(); - }, [sourceId, startTime, endTime, bucketDuration, lastRequestTime]); + const [getLogEntryRateRequest, getLogEntryRate] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async () => { + return await callGetLogEntryRateAPI(sourceId, startTime, endTime, bucketDuration); + }, + onResolve: ({ data }) => { + setLogEntryRate({ + bucketDuration: data.bucketDuration, + totalNumberOfLogEntries: data.totalNumberOfLogEntries, + histogramBuckets: data.histogramBuckets, + partitionBuckets: formatLogEntryRateResultsByPartition(data), + }); + }, + onReject: () => { + setLogEntryRate(null); + }, + }, + [sourceId, startTime, endTime, bucketDuration] + ); - const logRateResults: LogRateResults | null = useMemo(() => { - if (logEntryRate) { - return { - bucketDuration: logEntryRate.bucketDuration, - totalNumberOfLogEntries: logEntryRate.totalNumberOfLogEntries, - histogramBuckets: logEntryRate.histogramBuckets, - partitionBuckets: formatLogEntryRateResultsByPartition(logEntryRate), - }; - } else { - return null; - } - }, [logEntryRate]); + const isLoading = useMemo(() => getLogEntryRateRequest.state === 'pending', [ + getLogEntryRateRequest.state, + ]); return { + getLogEntryRate, isLoading, - logRateResults, + logEntryRate, }; }; -export const LogAnalysisResults = createContainer(useLogAnalysisResults); - const formatLogEntryRateResultsByPartition = ( results: GetLogEntryRateSuccessResponsePayload['data'] ): PartitionRecord => { diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results_url_state.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx similarity index 96% rename from x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results_url_state.tsx rename to x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx index 19fb7f238fc04..017be6be49e16 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_results_url_state.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect } from 'react'; -import * as rt from 'io-ts'; -import { identity, constant } from 'fp-ts/lib/function'; import { fold } from 'fp-ts/lib/Either'; +import { constant, identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; +import * as rt from 'io-ts'; +import { useEffect } from 'react'; + import { useUrlState } from '../../../utils/use_url_state'; const autoRefreshRT = rt.union([ diff --git a/x-pack/legacy/plugins/infra/server/infra_server.ts b/x-pack/legacy/plugins/infra/server/infra_server.ts index 845e54e18c7c5..e0c8f607daa93 100644 --- a/x-pack/legacy/plugins/infra/server/infra_server.ts +++ b/x-pack/legacy/plugins/infra/server/infra_server.ts @@ -12,8 +12,8 @@ import { createSourceStatusResolvers } from './graphql/source_status'; import { createSourcesResolvers } from './graphql/sources'; import { InfraBackendLibs } from './lib/infra_types'; import { - initLogAnalysisGetLogEntryRateRoute, - initIndexPatternsValidateRoute, + initGetLogEntryRateRoute, + initValidateLogAnalysisIndicesRoute, } from './routes/log_analysis'; import { initMetricExplorerRoute } from './routes/metrics_explorer'; import { initMetadataRoute } from './routes/metadata'; @@ -33,10 +33,10 @@ export const initInfraServer = (libs: InfraBackendLibs) => { libs.framework.registerGraphQLEndpoint('/graphql', schema); initIpToHostName(libs); - initLogAnalysisGetLogEntryRateRoute(libs); + initGetLogEntryRateRoute(libs); initSnapshotRoute(libs); initNodeDetailsRoute(libs); - initIndexPatternsValidateRoute(libs); + initValidateLogAnalysisIndicesRoute(libs); initMetricExplorerRoute(libs); initMetadataRoute(libs); }; diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts index 625607c098028..e88736b08b95b 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -29,12 +29,12 @@ export interface InfraServerPluginDeps { export interface CallWithRequestParams extends GenericParams { max_concurrent_shard_requests?: number; name?: string; - index?: string; + index?: string | string[]; ignore_unavailable?: boolean; allow_no_indices?: boolean; size?: number; terminate_after?: number; - fields?: string; + fields?: string | string[]; } export type InfraResponse = Lifecycle.ReturnValue; diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index.ts index 7364d167efe47..378e32cb3582c 100644 --- a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index.ts +++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index.ts @@ -5,4 +5,4 @@ */ export * from './results'; -export * from './index_patterns'; +export * from './validation'; diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts index 973080c880e6d..02866e797e305 100644 --- a/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts +++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts @@ -22,10 +22,7 @@ import { NoLogRateResultsIndexError } from '../../../lib/log_analysis'; const anyObject = schema.object({}, { allowUnknowns: true }); -export const initLogAnalysisGetLogEntryRateRoute = ({ - framework, - logAnalysis, -}: InfraBackendLibs) => { +export const initGetLogEntryRateRoute = ({ framework, logAnalysis }: InfraBackendLibs) => { framework.registerRoute( { method: 'post', diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/index.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/validation/index.ts similarity index 89% rename from x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/index.ts rename to x-pack/legacy/plugins/infra/server/routes/log_analysis/validation/index.ts index a85e119e7318a..727faca69298e 100644 --- a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/index.ts +++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/validation/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './validate'; +export * from './indices'; diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/validation/indices.ts similarity index 77% rename from x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts rename to x-pack/legacy/plugins/infra/server/routes/log_analysis/validation/indices.ts index 1f64da1859b5f..ba143a597b66d 100644 --- a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts +++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/validation/indices.ts @@ -11,7 +11,7 @@ import { identity } from 'fp-ts/lib/function'; import { schema } from '@kbn/config-schema'; import { InfraBackendLibs } from '../../../lib/infra_types'; import { - LOG_ANALYSIS_VALIDATION_INDICES_PATH, + LOG_ANALYSIS_VALIDATE_INDICES_PATH, validationIndicesRequestPayloadRT, validationIndicesResponsePayloadRT, ValidationIndicesError, @@ -19,14 +19,13 @@ import { import { throwErrors } from '../../../../common/runtime_types'; -const partitionField = 'event.dataset'; const escapeHatch = schema.object({}, { allowUnknowns: true }); -export const initIndexPatternsValidateRoute = ({ framework }: InfraBackendLibs) => { +export const initValidateLogAnalysisIndicesRoute = ({ framework }: InfraBackendLibs) => { framework.registerRoute( { method: 'post', - path: LOG_ANALYSIS_VALIDATION_INDICES_PATH, + path: LOG_ANALYSIS_VALIDATE_INDICES_PATH, validate: { body: escapeHatch }, }, async (requestContext, request, response) => { @@ -36,7 +35,7 @@ export const initIndexPatternsValidateRoute = ({ framework }: InfraBackendLibs) fold(throwErrors(Boom.badRequest), identity) ); - const { timestampField, indices } = payload.data; + const { fields, indices } = payload.data; const errors: ValidationIndicesError[] = []; // Query each pattern individually, to map correctly the errors @@ -44,7 +43,7 @@ export const initIndexPatternsValidateRoute = ({ framework }: InfraBackendLibs) indices.map(async index => { const fieldCaps = await framework.callWithRequest(requestContext, 'fieldCaps', { index, - fields: `${timestampField},${partitionField}`, + fields: fields.map(field => field.name), }); if (fieldCaps.indices.length === 0) { @@ -55,32 +54,30 @@ export const initIndexPatternsValidateRoute = ({ framework }: InfraBackendLibs) return; } - ([ - [timestampField, 'date'], - [partitionField, 'keyword'], - ] as const).forEach(([field, fieldType]) => { - const fieldMetadata = fieldCaps.fields[field]; + fields.forEach(({ name: fieldName, validTypes }) => { + const fieldMetadata = fieldCaps.fields[fieldName]; if (fieldMetadata === undefined) { errors.push({ error: 'FIELD_NOT_FOUND', index, - field, + field: fieldName, }); } else { const fieldTypes = Object.keys(fieldMetadata); - if (fieldTypes.length > 1 || fieldTypes[0] !== fieldType) { + if (!fieldTypes.every(fieldType => validTypes.includes(fieldType))) { errors.push({ error: `FIELD_NOT_VALID`, index, - field, + field: fieldName, }); } } }); }) ); + return response.ok({ body: validationIndicesResponsePayloadRT.encode({ data: { errors } }), });