Skip to content
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

[Logs UI] [Alerting] "Group by" functionality #68250

Merged
merged 35 commits into from
Jun 30, 2020
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
a02b17d
Server side (executor) support for group by
Kerry350 Jun 2, 2020
dd92f5f
Add UI functionality
Kerry350 Jun 4, 2020
2f9f6cd
Ensure array entries
Kerry350 Jun 4, 2020
4246222
Merge remote-tracking branch 'upstream/master' into 67465-logs-alert-…
Kerry350 Jun 4, 2020
7563fac
Merge remote-tracking branch 'upstream/master' into 67465-logs-alert-…
Kerry350 Jun 5, 2020
9be25e6
Merge remote-tracking branch 'upstream/master' into 67465-logs-alert-…
Kerry350 Jun 5, 2020
b773abe
Amend current executor tests
Kerry350 Jun 5, 2020
8726524
Merge branch 'master' into 67465-logs-alert-group-by
elasticmachine Jun 8, 2020
7461571
Merge remote-tracking branch 'upstream/master' into 67465-logs-alert-…
Kerry350 Jun 10, 2020
54d0144
Merge remote-tracking branch 'upstream/master' into 67465-logs-alert-…
Kerry350 Jun 11, 2020
c9ef022
Server side amendments
Kerry350 Jun 12, 2020
27296a9
Client side changes
Kerry350 Jun 12, 2020
b29ee1a
Merge remote-tracking branch 'upstream/master' into 67465-logs-alert-…
Kerry350 Jun 12, 2020
3901a07
Ensure "more than" is handled and ensure correct component is exporte…
Kerry350 Jun 12, 2020
0ceb3fe
Remove hit total capping due to document count total dependency
Kerry350 Jun 12, 2020
f0e11f6
Spread aggs properly
Kerry350 Jun 12, 2020
7806cf3
Alter handling of composite aggs
Kerry350 Jun 12, 2020
971e4bd
Merge remote-tracking branch 'upstream/master' into 67465-logs-alert-…
Kerry350 Jun 12, 2020
c9e56a2
Add response runtime type check
Kerry350 Jun 12, 2020
16608ee
Fix type to account for undefined group_key
Kerry350 Jun 12, 2020
b4bcde8
Merge remote-tracking branch 'upstream/master' into 67465-logs-alert-…
Kerry350 Jun 15, 2020
8bc54ff
Use separate functions for grouped and ungrouped ES queries
Kerry350 Jun 15, 2020
8093dca
Cast a wider net for group results and add "must" filters to a sub ag…
Kerry350 Jun 15, 2020
0ae6fe3
Merge remote-tracking branch 'upstream/master' into 67465-logs-alert-…
Kerry350 Jun 15, 2020
c40d459
Merge remote-tracking branch 'upstream/master' into 67465-logs-alert-…
Kerry350 Jun 15, 2020
aed4063
Merge remote-tracking branch 'upstream/master' into 67465-logs-alert-…
Kerry350 Jun 16, 2020
902bd37
Amend executor tests
Kerry350 Jun 16, 2020
8cae6b6
Ensure inner filtering scopes back to unpadded range
Kerry350 Jun 16, 2020
b67ad73
Merge remote-tracking branch 'upstream/master' into 67465-logs-alert-…
Kerry350 Jun 29, 2020
b50de34
Update x-pack/plugins/infra/server/lib/alerting/log_threshold/log_thr…
Kerry350 Jun 29, 2020
34bbece
Update x-pack/plugins/infra/server/lib/alerting/log_threshold/log_thr…
Kerry350 Jun 29, 2020
827071d
Review changes
Kerry350 Jun 29, 2020
1b4b99f
Merge branch '67465-logs-alert-group-by' of github.com:Kerry350/kiban…
Kerry350 Jun 29, 2020
a6dfeb6
Tweak filtering of fields (remove searchable requirement)
Kerry350 Jun 29, 2020
17b49f8
Update x-pack/plugins/infra/public/components/alerting/logs/expressio…
Kerry350 Jun 29, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 87 additions & 16 deletions x-pack/plugins/infra/common/alerting/logs/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import * as rt from 'io-ts';
import { commonSearchSuccessResponseFieldsRT } from '../../utils/elasticsearch_runtime_types';

export const LOG_DOCUMENT_COUNT_ALERT_TYPE_ID = 'logs.alert.document.count';

Expand All @@ -20,6 +22,19 @@ export enum Comparator {
NOT_MATCH_PHRASE = 'does not match phrase',
}

const ComparatorRT = rt.keyof({
[Comparator.GT]: null,
[Comparator.GT_OR_EQ]: null,
[Comparator.LT]: null,
[Comparator.LT_OR_EQ]: null,
[Comparator.EQ]: null,
[Comparator.NOT_EQ]: null,
[Comparator.MATCH]: null,
[Comparator.NOT_MATCH]: null,
[Comparator.MATCH_PHRASE]: null,
[Comparator.NOT_MATCH_PHRASE]: null,
});

// Maps our comparators to i18n strings, some comparators have more specific wording
// depending on the field type the comparator is being used with.
export const ComparatorToi18nMap = {
Expand Down Expand Up @@ -74,22 +89,78 @@ export enum AlertStates {
ERROR,
}

export interface DocumentCount {
comparator: Comparator;
value: number;
}
const DocumentCountRT = rt.type({
comparator: ComparatorRT,
value: rt.number,
});

export interface Criterion {
field: string;
comparator: Comparator;
value: string | number;
}
export type DocumentCount = rt.TypeOf<typeof DocumentCountRT>;

export interface LogDocumentCountAlertParams {
count: DocumentCount;
criteria: Criterion[];
timeUnit: 's' | 'm' | 'h' | 'd';
timeSize: number;
}
const CriterionRT = rt.type({
field: rt.string,
comparator: ComparatorRT,
value: rt.union([rt.string, rt.number]),
});

export type Criterion = rt.TypeOf<typeof CriterionRT>;

const TimeUnitRT = rt.union([rt.literal('s'), rt.literal('m'), rt.literal('h'), rt.literal('d')]);
export type TimeUnit = rt.TypeOf<typeof TimeUnitRT>;

export const LogDocumentCountAlertParamsRT = rt.intersection([
rt.type({
count: DocumentCountRT,
criteria: rt.array(CriterionRT),
timeUnit: TimeUnitRT,
timeSize: rt.number,
}),
rt.partial({
groupBy: rt.array(rt.string),
}),
]);

export type LogDocumentCountAlertParams = rt.TypeOf<typeof LogDocumentCountAlertParamsRT>;

export const UngroupedSearchQueryResponseRT = rt.intersection([
commonSearchSuccessResponseFieldsRT,
rt.type({
hits: rt.type({
total: rt.type({
value: rt.number,
}),
}),
}),
]);

export type UngroupedSearchQueryResponse = rt.TypeOf<typeof UngroupedSearchQueryResponseRT>;

export const GroupedSearchQueryResponseRT = rt.intersection([
commonSearchSuccessResponseFieldsRT,
rt.type({
aggregations: rt.type({
groups: rt.intersection([
rt.type({
buckets: rt.array(
rt.type({
key: rt.record(rt.string, rt.string),
doc_count: rt.number,
filtered_results: rt.type({
doc_count: rt.number,
}),
})
),
}),
rt.partial({
after_key: rt.record(rt.string, rt.string),
}),
]),
}),
hits: rt.type({
total: rt.type({
value: rt.number,
}),
}),
}),
]);

export type TimeUnit = 's' | 'm' | 'h' | 'd';
export type GroupedSearchQueryResponse = rt.TypeOf<typeof GroupedSearchQueryResponseRT>;
18 changes: 18 additions & 0 deletions x-pack/plugins/infra/common/utils/elasticsearch_runtime_types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* 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 commonSearchSuccessResponseFieldsRT = rt.type({
_shards: rt.type({
total: rt.number,
successful: rt.number,
skipped: rt.number,
failed: rt.number,
}),
timed_out: rt.boolean,
took: rt.number,
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { DocumentCount } from './document_count';
import { Criteria } from './criteria';
import { useSourceId } from '../../../../containers/source_id';
import { LogSourceProvider, useLogSourceContext } from '../../../../containers/logs/log_source';
import { GroupByExpression } from '../../shared/group_by_expression/group_by_expression';

export interface ExpressionCriteria {
field?: string;
Expand Down Expand Up @@ -121,7 +122,6 @@ export const Editor: React.FC<Props> = (props) => {
const { setAlertParams, alertParams, errors } = props;
const [hasSetDefaults, setHasSetDefaults] = useState<boolean>(false);
const { sourceStatus } = useLogSourceContext();

useMount(() => {
for (const [key, value] of Object.entries({ ...DEFAULT_EXPRESSION, ...alertParams })) {
setAlertParams(key, value);
Expand All @@ -140,6 +140,17 @@ export const Editor: React.FC<Props> = (props) => {
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [sourceStatus]);

const groupByFields = useMemo(() => {
if (sourceStatus?.logIndexFields) {
return sourceStatus.logIndexFields.filter((field) => {
return field.type === 'string' && field.searchable;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that this is handled outside of the selector, do we want to allow other types than just string in here?

And could you help me understand why the fields have to be searchable?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any idea about this? 😇

});
} else {
return [];
}
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [sourceStatus]);

const updateCount = useCallback(
(countParams) => {
const nextCountParams = { ...alertParams.count, ...countParams };
Expand Down Expand Up @@ -172,6 +183,13 @@ export const Editor: React.FC<Props> = (props) => {
[setAlertParams]
);

const updateGroupBy = useCallback(
(groups: string[]) => {
setAlertParams('groupBy', groups);
},
[setAlertParams]
);

const addCriterion = useCallback(() => {
const nextCriteria = alertParams?.criteria
? [...alertParams.criteria, DEFAULT_CRITERIA]
Expand Down Expand Up @@ -219,6 +237,12 @@ export const Editor: React.FC<Props> = (props) => {
errors={errors as { [key: string]: string[] }}
/>

<GroupByExpression
selectedGroups={alertParams.groupBy}
onChange={updateGroupBy}
fields={groupByFields}
/>

<div>
<EuiButtonEmpty
color={'primary'}
Expand All @@ -239,4 +263,4 @@ export const Editor: React.FC<Props> = (props) => {

// required for dynamic import
// eslint-disable-next-line import/no-default-export
export default Editor;
export default ExpressionEditor;
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function getAlertType(): AlertTypeModel {
defaultActionMessage: i18n.translate(
'xpack.infra.logs.alerting.threshold.defaultActionMessage',
{
defaultMessage: `\\{\\{context.matchingDocuments\\}\\} log entries have matched the following conditions: \\{\\{context.conditions\\}\\}`,
defaultMessage: `\\{\\{#context.group\\}\\}\\{\\{context.group\\}\\} - \\{\\{/context.group\\}\\}\\{\\{context.matchingDocuments\\}\\} log entries have matched the following conditions: \\{\\{context.conditions\\}\\}`,
}
),
requiresAppContext: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* 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 React, { useState, useMemo } from 'react';
import { IFieldType } from 'src/plugins/data/public';
import { i18n } from '@kbn/i18n';
import {
EuiPopoverTitle,
EuiFlexItem,
EuiFlexGroup,
EuiPopover,
EuiExpression,
} from '@elastic/eui';
import { GroupBySelector } from './selector';

interface Props {
selectedGroups?: string[];
fields: IFieldType[];
onChange: (groupBy: string[]) => void;
label?: string;
}

const DEFAULT_GROUP_BY_LABEL = i18n.translate('xpack.infra.alerting.alertFlyout.groupByLabel', {
defaultMessage: 'Group By',
});

const EVERYTHING_PLACEHOLDER = i18n.translate(
'xpack.infra.alerting.alertFlyout.groupBy.placeholder',
{
defaultMessage: 'Nothing (ungrouped)',
}
);

export const GroupByExpression: React.FC<Props> = ({
selectedGroups = [],
fields,
label,
onChange,
}) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);

const expressionValue = useMemo(() => {
return selectedGroups.length > 0 ? selectedGroups.join(', ') : EVERYTHING_PLACEHOLDER;
}, [selectedGroups]);

const labelProp = label ?? DEFAULT_GROUP_BY_LABEL;

return (
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<EuiPopover
id="groupByExpression"
button={
<EuiExpression
description={labelProp}
uppercase={true}
value={expressionValue}
isActive={isPopoverOpen}
onClick={() => setIsPopoverOpen(true)}
/>
}
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(false)}
ownFocus
panelPaddingSize="s"
anchorPosition="downLeft"
>
<div style={{ zIndex: 11000 }}>
<EuiPopoverTitle>{labelProp}</EuiPopoverTitle>
<GroupBySelector
selectedGroups={selectedGroups}
onChange={onChange}
fields={fields}
label={labelProp}
placeholder={EVERYTHING_PLACEHOLDER}
/>
</div>
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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 { EuiComboBox } from '@elastic/eui';
import React, { useCallback, useMemo } from 'react';
import { IFieldType } from 'src/plugins/data/public';

interface Props {
selectedGroups?: string[];
onChange: (groupBy: string[]) => void;
fields: IFieldType[];
label: string;
placeholder: string;
}

export const GroupBySelector = ({
onChange,
fields,
selectedGroups = [],
label,
placeholder,
}: Props) => {
const handleChange = useCallback(
(selectedOptions: Array<{ label: string }>) => {
const groupBy = selectedOptions.map((option) => option.label);
onChange(groupBy);
},
[onChange]
);

const formattedSelectedGroups = useMemo(() => {
return selectedGroups.map((group) => ({ label: group }));
}, [selectedGroups]);

const options = useMemo(() => {
return fields.filter((field) => field.aggregatable).map((field) => ({ label: field.name }));
}, [fields]);

return (
<div style={{ minWidth: '300px' }}>
<EuiComboBox
placeholder={placeholder}
aria-label={label}
fullWidth
singleSelection={false}
selectedOptions={formattedSelectedGroups}
options={options}
onChange={handleChange}
isClearable={true}
/>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export interface InfraDatabaseSearchResponse<Hit = {}, Aggregations = undefined>
skipped: number;
failed: number;
};
timed_out: boolean;
aggregations?: Aggregations;
hits: {
total: {
Expand Down
Loading