New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[ML] Adds validation of field selected for log pattern analysis #162319
Changes from 27 commits
9e290aa
bacf7d8
5963371
db972cf
f174123
6478436
93fecdc
f401d4c
b1ea5a0
c9e025a
3868966
2a8e2d2
f5801ae
a42b7bb
d801056
34066a5
efa072c
9a970ff
8000d37
ed00ad1
cc020ba
82bc81c
7944f53
431238b
9389219
383d92a
402d62a
ed77ab5
5864f72
d02a6cc
40fb370
914c43d
141b182
3f10c3f
04f6564
8903063
1de953b
092a206
8704072
922b8da
96a6c88
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,7 +8,6 @@ | |
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; | ||
|
||
import type { Filter, Query } from '@kbn/es-query'; | ||
import { isPopulatedObject } from '@kbn/ml-is-populated-object'; | ||
|
||
import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from './search_utils'; | ||
|
||
|
@@ -40,6 +39,20 @@ export const getDefaultAiOpsListState = ( | |
...overrides, | ||
}); | ||
|
||
export const isFullAiOpsListState = (arg: unknown): arg is AiOpsFullIndexBasedAppState => { | ||
return isPopulatedObject(arg, Object.keys(getDefaultAiOpsListState())); | ||
export interface LogCategorizationPageUrlState { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you manage to find out if it's possible to disable auto refresh on the page? It's good that we persist the field selection, but we lose the results when the page refreshes (inside ML). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I could not find a way to do it. you can disable some parts of the time picker, but not the refresh checkbox. |
||
pageKey: 'logCategorization'; | ||
pageUrlState: LogCategorizationAppState; | ||
} | ||
|
||
export interface LogCategorizationAppState extends AiOpsFullIndexBasedAppState { | ||
field: string | undefined; | ||
} | ||
|
||
export const getDefaultLogCategorizationAppState = ( | ||
overrides?: Partial<LogCategorizationAppState> | ||
): LogCategorizationAppState => { | ||
return { | ||
field: undefined, | ||
...getDefaultAiOpsListState(overrides), | ||
}; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
|
||
import React, { FC } from 'react'; | ||
import { | ||
FieldValidationResults, | ||
CATEGORY_EXAMPLES_VALIDATION_STATUS, | ||
} from '@kbn/ml-category-validator'; | ||
|
||
import { EuiCallOut } from '@elastic/eui'; | ||
import { i18n } from '@kbn/i18n'; | ||
|
||
interface Props { | ||
validationResults: FieldValidationResults | null; | ||
} | ||
|
||
export const FieldValidationCallout: FC<Props> = ({ validationResults }) => { | ||
if (validationResults === null) { | ||
return null; | ||
} | ||
|
||
if (validationResults.overallValidStatus === CATEGORY_EXAMPLES_VALIDATION_STATUS.VALID) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<> | ||
{validationResults !== null ? ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guess this ternary can be removed since the same check is done above and returns early. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated in 914c43d |
||
<EuiCallOut | ||
color="warning" | ||
title={i18n.translate('xpack.aiops.logCategorization.fieldValidationTitle', { | ||
defaultMessage: 'The selected field is possibly not suitable for pattern analysis', | ||
})} | ||
> | ||
{validationResults.validationChecks | ||
.filter((check) => check.valid !== CATEGORY_EXAMPLES_VALIDATION_STATUS.VALID) | ||
.map((check) => ( | ||
<div key={check.id}>{check.message}</div> | ||
))} | ||
</EuiCallOut> | ||
) : null} | ||
</> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,7 @@ import { | |
import { buildEmptyFilter, Filter } from '@kbn/es-query'; | ||
|
||
import { usePageUrlState } from '@kbn/ml-url-state'; | ||
import type { FieldValidationResults } from '@kbn/ml-category-validator'; | ||
import { useData } from '../../hooks/use_data'; | ||
import { useSearch } from '../../hooks/use_search'; | ||
import { useCategorizeRequest } from './use_categorize_request'; | ||
|
@@ -32,11 +33,12 @@ import { createMergedEsQuery } from '../../application/utils/search_utils'; | |
import { SamplingMenu } from './sampling_menu'; | ||
import { TechnicalPreviewBadge } from './technical_preview_badge'; | ||
import { LoadingCategorization } from './loading_categorization'; | ||
import { useValidateFieldRequest } from './use_validate_category_field'; | ||
import { | ||
type AiOpsPageUrlState, | ||
getDefaultAiOpsListState, | ||
isFullAiOpsListState, | ||
type LogCategorizationPageUrlState, | ||
getDefaultLogCategorizationAppState, | ||
} from '../../application/utils/url_state'; | ||
import { FieldValidationCallout } from './category_validation_callout'; | ||
|
||
export interface LogCategorizationPageProps { | ||
dataView: DataView; | ||
|
@@ -60,14 +62,21 @@ export const LogCategorizationFlyout: FC<LogCategorizationPageProps> = ({ | |
}, | ||
uiSettings, | ||
} = useAiopsAppContext(); | ||
|
||
const { runValidateFieldRequest, cancelRequest: cancelValidationRequest } = | ||
useValidateFieldRequest(); | ||
const { euiTheme } = useEuiTheme(); | ||
const { filters, query } = useMemo(() => getState(), [getState]); | ||
|
||
const mounted = useRef(false); | ||
const { runCategorizeRequest, cancelRequest, randomSampler } = useCategorizeRequest(); | ||
const [aiopsListState] = usePageUrlState<AiOpsPageUrlState>( | ||
'AIOPS_INDEX_VIEWER', | ||
getDefaultAiOpsListState({ | ||
const { | ||
runCategorizeRequest, | ||
cancelRequest: cancelCategorizationRequest, | ||
randomSampler, | ||
} = useCategorizeRequest(); | ||
const [stateFromUrl] = usePageUrlState<LogCategorizationPageUrlState>( | ||
'logCategorization', | ||
getDefaultLogCategorizationAppState({ | ||
searchQuery: createMergedEsQuery(query, filters, dataView, uiSettings), | ||
}) | ||
); | ||
|
@@ -80,6 +89,14 @@ export const LogCategorizationFlyout: FC<LogCategorizationPageProps> = ({ | |
categories: Category[]; | ||
sparkLines: SparkLinesPerCategory; | ||
} | null>(null); | ||
const [fieldValidationResult, setFieldValidationResult] = useState<FieldValidationResults | null>( | ||
null | ||
); | ||
|
||
const cancelRequest = useCallback(() => { | ||
cancelValidationRequest(); | ||
cancelCategorizationRequest(); | ||
}, [cancelCategorizationRequest, cancelValidationRequest]); | ||
|
||
useEffect( | ||
function cancelRequestOnLeave() { | ||
|
@@ -94,7 +111,7 @@ export const LogCategorizationFlyout: FC<LogCategorizationPageProps> = ({ | |
|
||
const { searchQueryLanguage, searchString, searchQuery } = useSearch( | ||
{ dataView, savedSearch: selectedSavedSearch }, | ||
aiopsListState, | ||
stateFromUrl, | ||
true | ||
); | ||
|
||
|
@@ -109,7 +126,8 @@ export const LogCategorizationFlyout: FC<LogCategorizationPageProps> = ({ | |
); | ||
|
||
const loadCategories = useCallback(async () => { | ||
const { title: index, timeFieldName: timeField } = dataView; | ||
const { getIndexPattern, timeFieldName: timeField } = dataView; | ||
const index = getIndexPattern(); | ||
|
||
if (selectedField === undefined || timeField === undefined) { | ||
return; | ||
|
@@ -119,20 +137,35 @@ export const LogCategorizationFlyout: FC<LogCategorizationPageProps> = ({ | |
|
||
setLoading(true); | ||
setData(null); | ||
setFieldValidationResult(null); | ||
|
||
try { | ||
const { categories, sparkLinesPerCategory: sparkLines } = await runCategorizeRequest( | ||
index, | ||
selectedField.name, | ||
timeField, | ||
earliest, | ||
latest, | ||
searchQuery, | ||
intervalMs | ||
); | ||
const [validationResult, categorizationResult] = await Promise.all([ | ||
runValidateFieldRequest( | ||
index, | ||
selectedField.name, | ||
timeField, | ||
earliest, | ||
latest, | ||
searchQuery | ||
), | ||
runCategorizeRequest( | ||
index, | ||
selectedField.name, | ||
timeField, | ||
earliest, | ||
latest, | ||
searchQuery, | ||
intervalMs | ||
), | ||
]); | ||
|
||
if (mounted.current === true) { | ||
setData({ categories, sparkLines }); | ||
setFieldValidationResult(validationResult); | ||
setData({ | ||
categories: categorizationResult.categories, | ||
sparkLines: categorizationResult.sparkLinesPerCategory, | ||
}); | ||
} | ||
} catch (error) { | ||
toasts.addError(error, { | ||
|
@@ -149,10 +182,11 @@ export const LogCategorizationFlyout: FC<LogCategorizationPageProps> = ({ | |
dataView, | ||
selectedField, | ||
cancelRequest, | ||
runCategorizeRequest, | ||
runValidateFieldRequest, | ||
earliest, | ||
latest, | ||
searchQuery, | ||
runCategorizeRequest, | ||
intervalMs, | ||
toasts, | ||
]); | ||
|
@@ -217,6 +251,8 @@ export const LogCategorizationFlyout: FC<LogCategorizationPageProps> = ({ | |
</EuiFlexGroup> | ||
</EuiFlyoutHeader> | ||
<EuiFlyoutBody data-test-subj="mlJobSelectorFlyoutBody"> | ||
<FieldValidationCallout validationResults={fieldValidationResult} /> | ||
peteharverson marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With this example, the field does not produce any warnings because the data is tokenized correctly but it also does not produce any categories. I suspect this is because every doc contains the same data. |
||
|
||
{loading === true ? <LoadingCategorization onClose={onClose} /> : null} | ||
|
||
<InformationText | ||
|
@@ -226,13 +262,10 @@ export const LogCategorizationFlyout: FC<LogCategorizationPageProps> = ({ | |
fieldSelected={selectedField !== null} | ||
/> | ||
|
||
{loading === false && | ||
data !== null && | ||
data.categories.length > 0 && | ||
isFullAiOpsListState(aiopsListState) ? ( | ||
{loading === false && data !== null && data.categories.length > 0 ? ( | ||
<CategoryTable | ||
categories={data.categories} | ||
aiopsListState={aiopsListState} | ||
aiopsListState={stateFromUrl} | ||
dataViewId={dataView.id!} | ||
eventRate={eventRate} | ||
sparkLines={data.sparkLines} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wonder if we should add runtimeMappingsSchema to
ml-runtime-field-utils
as we do use it in several plugins 🤔There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved in 40fb370
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've reverted this move. I caused a 200KB bundle increase in ML and Transforms.