[APM] Add warning to notify user about legacy ML jobs (#71030)
This commit is contained in:
parent
716d56e4d0
commit
58cdbf0fe6
|
@ -4,13 +4,9 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function rangeFilter(
|
export function rangeFilter(start: number, end: number) {
|
||||||
start: number,
|
|
||||||
end: number,
|
|
||||||
timestampField = '@timestamp'
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
[timestampField]: {
|
'@timestamp': {
|
||||||
gte: start,
|
gte: start,
|
||||||
lte: end,
|
lte: end,
|
||||||
format: 'epoch_millis',
|
format: 'epoch_millis',
|
||||||
|
|
|
@ -25,7 +25,7 @@ export function AgentConfigurations() {
|
||||||
(callApmApi) =>
|
(callApmApi) =>
|
||||||
callApmApi({ pathname: '/api/apm/settings/agent-configuration' }),
|
callApmApi({ pathname: '/api/apm/settings/agent-configuration' }),
|
||||||
[],
|
[],
|
||||||
{ preservePreviousData: false }
|
{ preservePreviousData: false, showToastOnError: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
useTrackPageview({ app: 'apm', path: 'agent_configuration' });
|
useTrackPageview({ app: 'apm', path: 'agent_configuration' });
|
||||||
|
|
|
@ -10,9 +10,15 @@ import { i18n } from '@kbn/i18n';
|
||||||
import { EuiPanel } from '@elastic/eui';
|
import { EuiPanel } from '@elastic/eui';
|
||||||
import { JobsList } from './jobs_list';
|
import { JobsList } from './jobs_list';
|
||||||
import { AddEnvironments } from './add_environments';
|
import { AddEnvironments } from './add_environments';
|
||||||
import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher';
|
import { useFetcher } from '../../../../hooks/useFetcher';
|
||||||
import { LicensePrompt } from '../../../shared/LicensePrompt';
|
import { LicensePrompt } from '../../../shared/LicensePrompt';
|
||||||
import { useLicense } from '../../../../hooks/useLicense';
|
import { useLicense } from '../../../../hooks/useLicense';
|
||||||
|
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
|
||||||
|
|
||||||
|
const DEFAULT_VALUE: APIReturnType<'/api/apm/settings/anomaly-detection'> = {
|
||||||
|
jobs: [],
|
||||||
|
hasLegacyJobs: false,
|
||||||
|
};
|
||||||
|
|
||||||
export const AnomalyDetection = () => {
|
export const AnomalyDetection = () => {
|
||||||
const license = useLicense();
|
const license = useLicense();
|
||||||
|
@ -20,17 +26,13 @@ export const AnomalyDetection = () => {
|
||||||
|
|
||||||
const [viewAddEnvironments, setViewAddEnvironments] = useState(false);
|
const [viewAddEnvironments, setViewAddEnvironments] = useState(false);
|
||||||
|
|
||||||
const { refetch, data = [], status } = useFetcher(
|
const { refetch, data = DEFAULT_VALUE, status } = useFetcher(
|
||||||
(callApmApi) =>
|
(callApmApi) =>
|
||||||
callApmApi({ pathname: `/api/apm/settings/anomaly-detection` }),
|
callApmApi({ pathname: `/api/apm/settings/anomaly-detection` }),
|
||||||
[],
|
[],
|
||||||
{ preservePreviousData: false }
|
{ preservePreviousData: false, showToastOnError: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
const isLoading =
|
|
||||||
status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING;
|
|
||||||
const hasFetchFailure = status === FETCH_STATUS.FAILURE;
|
|
||||||
|
|
||||||
if (!hasValidLicense) {
|
if (!hasValidLicense) {
|
||||||
return (
|
return (
|
||||||
<EuiPanel>
|
<EuiPanel>
|
||||||
|
@ -66,7 +68,7 @@ export const AnomalyDetection = () => {
|
||||||
<EuiSpacer size="l" />
|
<EuiSpacer size="l" />
|
||||||
{viewAddEnvironments ? (
|
{viewAddEnvironments ? (
|
||||||
<AddEnvironments
|
<AddEnvironments
|
||||||
currentEnvironments={data.map(({ environment }) => environment)}
|
currentEnvironments={data.jobs.map(({ environment }) => environment)}
|
||||||
onCreateJobSuccess={() => {
|
onCreateJobSuccess={() => {
|
||||||
refetch();
|
refetch();
|
||||||
setViewAddEnvironments(false);
|
setViewAddEnvironments(false);
|
||||||
|
@ -77,9 +79,9 @@ export const AnomalyDetection = () => {
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<JobsList
|
<JobsList
|
||||||
isLoading={isLoading}
|
status={status}
|
||||||
hasFetchFailure={hasFetchFailure}
|
anomalyDetectionJobsByEnv={data.jobs}
|
||||||
anomalyDetectionJobsByEnv={data}
|
hasLegacyJobs={data.hasLegacyJobs}
|
||||||
onAddEnvironments={() => {
|
onAddEnvironments={() => {
|
||||||
setViewAddEnvironments(true);
|
setViewAddEnvironments(true);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -16,12 +16,14 @@ import {
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
|
import { FETCH_STATUS } from '../../../../hooks/useFetcher';
|
||||||
import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable';
|
import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable';
|
||||||
import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt';
|
import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt';
|
||||||
import { AnomalyDetectionJobByEnv } from '../../../../../typings/anomaly_detection';
|
import { AnomalyDetectionJobByEnv } from '../../../../../typings/anomaly_detection';
|
||||||
import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink';
|
import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink';
|
||||||
import { MLLink } from '../../../shared/Links/MachineLearningLinks/MLLink';
|
import { MLLink } from '../../../shared/Links/MachineLearningLinks/MLLink';
|
||||||
import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values';
|
import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values';
|
||||||
|
import { LegacyJobsCallout } from './legacy_jobs_callout';
|
||||||
|
|
||||||
const columns: Array<ITableColumn<AnomalyDetectionJobByEnv>> = [
|
const columns: Array<ITableColumn<AnomalyDetectionJobByEnv>> = [
|
||||||
{
|
{
|
||||||
|
@ -60,17 +62,22 @@ const columns: Array<ITableColumn<AnomalyDetectionJobByEnv>> = [
|
||||||
];
|
];
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isLoading: boolean;
|
status: FETCH_STATUS;
|
||||||
hasFetchFailure: boolean;
|
|
||||||
onAddEnvironments: () => void;
|
onAddEnvironments: () => void;
|
||||||
anomalyDetectionJobsByEnv: AnomalyDetectionJobByEnv[];
|
anomalyDetectionJobsByEnv: AnomalyDetectionJobByEnv[];
|
||||||
|
hasLegacyJobs: boolean;
|
||||||
}
|
}
|
||||||
export const JobsList = ({
|
export const JobsList = ({
|
||||||
isLoading,
|
status,
|
||||||
hasFetchFailure,
|
|
||||||
onAddEnvironments,
|
onAddEnvironments,
|
||||||
anomalyDetectionJobsByEnv,
|
anomalyDetectionJobsByEnv,
|
||||||
|
hasLegacyJobs,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const isLoading =
|
||||||
|
status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING;
|
||||||
|
|
||||||
|
const hasFetchFailure = status === FETCH_STATUS.FAILURE;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiPanel>
|
<EuiPanel>
|
||||||
<EuiFlexGroup>
|
<EuiFlexGroup>
|
||||||
|
@ -131,6 +138,8 @@ export const JobsList = ({
|
||||||
items={isLoading || hasFetchFailure ? [] : anomalyDetectionJobsByEnv}
|
items={isLoading || hasFetchFailure ? [] : anomalyDetectionJobsByEnv}
|
||||||
/>
|
/>
|
||||||
<EuiSpacer size="l" />
|
<EuiSpacer size="l" />
|
||||||
|
|
||||||
|
{hasLegacyJobs && <LegacyJobsCallout />}
|
||||||
</EuiPanel>
|
</EuiPanel>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* 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 { EuiCallOut, EuiButton } from '@elastic/eui';
|
||||||
|
import React from 'react';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import { useApmPluginContext } from '../../../../hooks/useApmPluginContext';
|
||||||
|
|
||||||
|
export function LegacyJobsCallout() {
|
||||||
|
const { core } = useApmPluginContext();
|
||||||
|
return (
|
||||||
|
<EuiCallOut
|
||||||
|
title={i18n.translate(
|
||||||
|
'xpack.apm.settings.anomaly_detection.legacy_jobs.title',
|
||||||
|
{ defaultMessage: 'Legacy ML jobs are no longer used in APM app' }
|
||||||
|
)}
|
||||||
|
iconType="iInCircle"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{i18n.translate(
|
||||||
|
'xpack.apm.settings.anomaly_detection.legacy_jobs.body',
|
||||||
|
{
|
||||||
|
defaultMessage:
|
||||||
|
'We have discovered legacy Machine Learning jobs from our previous integration which are no longer being used in the APM app',
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<EuiButton
|
||||||
|
href={core.http.basePath.prepend(
|
||||||
|
'/app/ml#/jobs?mlManagement=(jobId:high_mean_response_time)'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{i18n.translate(
|
||||||
|
'xpack.apm.settings.anomaly_detection.legacy_jobs.button',
|
||||||
|
{ defaultMessage: 'Review jobs' }
|
||||||
|
)}
|
||||||
|
</EuiButton>
|
||||||
|
</EuiCallOut>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
* 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 const ML_MODULE_ID_APM_TRANSACTION = 'apm_transaction';
|
||||||
|
export const APM_ML_JOB_GROUP = 'apm';
|
|
@ -14,9 +14,7 @@ import {
|
||||||
PROCESSOR_EVENT,
|
PROCESSOR_EVENT,
|
||||||
} from '../../../common/elasticsearch_fieldnames';
|
} from '../../../common/elasticsearch_fieldnames';
|
||||||
import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values';
|
import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values';
|
||||||
|
import { APM_ML_JOB_GROUP, ML_MODULE_ID_APM_TRANSACTION } from './constants';
|
||||||
const ML_MODULE_ID_APM_TRANSACTION = 'apm_transaction';
|
|
||||||
export const ML_GROUP_NAME_APM = 'apm';
|
|
||||||
|
|
||||||
export type CreateAnomalyDetectionJobsAPIResponse = PromiseReturnType<
|
export type CreateAnomalyDetectionJobsAPIResponse = PromiseReturnType<
|
||||||
typeof createAnomalyDetectionJobs
|
typeof createAnomalyDetectionJobs
|
||||||
|
@ -83,8 +81,8 @@ async function createAnomalyDetectionJob({
|
||||||
|
|
||||||
return ml.modules.setup({
|
return ml.modules.setup({
|
||||||
moduleId: ML_MODULE_ID_APM_TRANSACTION,
|
moduleId: ML_MODULE_ID_APM_TRANSACTION,
|
||||||
prefix: `${ML_GROUP_NAME_APM}-${convertedEnvironmentName}-${randomToken}-`,
|
prefix: `${APM_ML_JOB_GROUP}-${convertedEnvironmentName}-${randomToken}-`,
|
||||||
groups: [ML_GROUP_NAME_APM, convertedEnvironmentName],
|
groups: [APM_ML_JOB_GROUP, convertedEnvironmentName],
|
||||||
indexPatternName,
|
indexPatternName,
|
||||||
query: {
|
query: {
|
||||||
bool: {
|
bool: {
|
||||||
|
|
|
@ -5,56 +5,34 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Logger } from 'kibana/server';
|
import { Logger } from 'kibana/server';
|
||||||
import { PromiseReturnType } from '../../../../observability/typings/common';
|
|
||||||
import { Setup } from '../helpers/setup_request';
|
import { Setup } from '../helpers/setup_request';
|
||||||
import { AnomalyDetectionJobByEnv } from '../../../typings/anomaly_detection';
|
import { getMlJobsWithAPMGroup } from './get_ml_jobs_by_group';
|
||||||
import { ML_GROUP_NAME_APM } from './create_anomaly_detection_jobs';
|
|
||||||
|
|
||||||
export type AnomalyDetectionJobsAPIResponse = PromiseReturnType<
|
export async function getAnomalyDetectionJobs(setup: Setup, logger: Logger) {
|
||||||
typeof getAnomalyDetectionJobs
|
|
||||||
>;
|
|
||||||
export async function getAnomalyDetectionJobs(
|
|
||||||
setup: Setup,
|
|
||||||
logger: Logger
|
|
||||||
): Promise<AnomalyDetectionJobByEnv[]> {
|
|
||||||
const { ml } = setup;
|
const { ml } = setup;
|
||||||
if (!ml) {
|
if (!ml) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
const mlCapabilities = await ml.mlSystem.mlCapabilities();
|
const mlCapabilities = await ml.mlSystem.mlCapabilities();
|
||||||
if (
|
if (
|
||||||
!(
|
!(
|
||||||
mlCapabilities.mlFeatureEnabledInSpace &&
|
mlCapabilities.mlFeatureEnabledInSpace &&
|
||||||
mlCapabilities.isPlatinumOrTrialLicense
|
mlCapabilities.isPlatinumOrTrialLicense
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
logger.warn(
|
logger.warn('Anomaly detection integration is not availble for this user.');
|
||||||
'Anomaly detection integration is not availble for this user.'
|
|
||||||
);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn('Unable to get ML capabilities.');
|
|
||||||
logger.error(error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const { jobs } = await ml.anomalyDetectors.jobs(ML_GROUP_NAME_APM);
|
|
||||||
return jobs
|
|
||||||
.map((job) => {
|
|
||||||
const environment = job.custom_settings?.job_tags?.environment ?? '';
|
|
||||||
return {
|
|
||||||
job_id: job.job_id,
|
|
||||||
environment,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter((job) => job.environment);
|
|
||||||
} catch (error) {
|
|
||||||
if (error.statusCode !== 404) {
|
|
||||||
logger.warn('Unable to get APM ML jobs.');
|
|
||||||
logger.error(error);
|
|
||||||
}
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const response = await getMlJobsWithAPMGroup(ml);
|
||||||
|
return response.jobs
|
||||||
|
.map((job) => {
|
||||||
|
const environment = job.custom_settings?.job_tags?.environment ?? '';
|
||||||
|
return {
|
||||||
|
job_id: job.job_id,
|
||||||
|
environment,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((job) => job.environment);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* 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 { Setup } from '../helpers/setup_request';
|
||||||
|
import { APM_ML_JOB_GROUP } from './constants';
|
||||||
|
|
||||||
|
// returns ml jobs containing "apm" group
|
||||||
|
// workaround: the ML api returns 404 when no jobs are found. This is handled so instead of throwing an empty response is returned
|
||||||
|
export async function getMlJobsWithAPMGroup(ml: NonNullable<Setup['ml']>) {
|
||||||
|
try {
|
||||||
|
return await ml.anomalyDetectors.jobs(APM_ML_JOB_GROUP);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.statusCode === 404) {
|
||||||
|
return { count: 0, jobs: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* 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 { Setup } from '../helpers/setup_request';
|
||||||
|
import { getMlJobsWithAPMGroup } from './get_ml_jobs_by_group';
|
||||||
|
|
||||||
|
// Determine whether there are any legacy ml jobs.
|
||||||
|
// A legacy ML job has a job id that ends with "high_mean_response_time" and created_by=ml-module-apm-transaction
|
||||||
|
export async function hasLegacyJobs(setup: Setup) {
|
||||||
|
const { ml } = setup;
|
||||||
|
|
||||||
|
if (!ml) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await getMlJobsWithAPMGroup(ml);
|
||||||
|
return response.jobs.some(
|
||||||
|
(job) =>
|
||||||
|
job.job_id.endsWith('high_mean_response_time') &&
|
||||||
|
job.custom_settings?.created_by === 'ml-module-apm-transaction'
|
||||||
|
);
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import { getAnomalyDetectionJobs } from '../../lib/anomaly_detection/get_anomaly
|
||||||
import { createAnomalyDetectionJobs } from '../../lib/anomaly_detection/create_anomaly_detection_jobs';
|
import { createAnomalyDetectionJobs } from '../../lib/anomaly_detection/create_anomaly_detection_jobs';
|
||||||
import { setupRequest } from '../../lib/helpers/setup_request';
|
import { setupRequest } from '../../lib/helpers/setup_request';
|
||||||
import { getAllEnvironments } from '../../lib/environments/get_all_environments';
|
import { getAllEnvironments } from '../../lib/environments/get_all_environments';
|
||||||
|
import { hasLegacyJobs } from '../../lib/anomaly_detection/has_legacy_jobs';
|
||||||
|
|
||||||
// get ML anomaly detection jobs for each environment
|
// get ML anomaly detection jobs for each environment
|
||||||
export const anomalyDetectionJobsRoute = createRoute(() => ({
|
export const anomalyDetectionJobsRoute = createRoute(() => ({
|
||||||
|
@ -17,7 +18,11 @@ export const anomalyDetectionJobsRoute = createRoute(() => ({
|
||||||
path: '/api/apm/settings/anomaly-detection',
|
path: '/api/apm/settings/anomaly-detection',
|
||||||
handler: async ({ context, request }) => {
|
handler: async ({ context, request }) => {
|
||||||
const setup = await setupRequest(context, request);
|
const setup = await setupRequest(context, request);
|
||||||
return await getAnomalyDetectionJobs(setup, context.logger);
|
const jobs = await getAnomalyDetectionJobs(setup, context.logger);
|
||||||
|
return {
|
||||||
|
jobs,
|
||||||
|
hasLegacyJobs: await hasLegacyJobs(setup),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ export enum CREATED_BY_LABEL {
|
||||||
MULTI_METRIC = 'multi-metric-wizard',
|
MULTI_METRIC = 'multi-metric-wizard',
|
||||||
POPULATION = 'population-wizard',
|
POPULATION = 'population-wizard',
|
||||||
CATEGORIZATION = 'categorization-wizard',
|
CATEGORIZATION = 'categorization-wizard',
|
||||||
|
APM_TRANSACTION = 'ml-module-apm-transaction',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_MODEL_MEMORY_LIMIT = '10MB';
|
export const DEFAULT_MODEL_MEMORY_LIMIT = '10MB';
|
||||||
|
|
Loading…
Reference in a new issue