Skip to content

Commit

Permalink
[SLOs] synthetics availability - add cardinality count for group by (#…
Browse files Browse the repository at this point in the history
…178454)

## Summary

Adds group by cardinality count to the synthetics availability SLO
indicator

Resolves #178409
Resolves #178140

Also, it's come to my attention that
#178341 was not fixed by a
previous PR. This PR now also resolves
#178341

### Testing

1. Create an cluster with oblt-cli and add the config to your
`kibana.dev.yml`
2. Navigate to the Synthetics app. Create at least two synthetic
monitors
3. Navigate to SLO create. Select the synthetic availability indicator
4. Check the group by cardinality callout. The cardinality should
reflect the number of monitor/location combinations
<img width="730" alt="Screenshot 2024-04-12 at 1 04 57 PM"
src="https://github.com/elastic/kibana/assets/11356435/a05ffaff-c01b-4107-8f8d-2ea8362fe72e">
5. Now filter by monitor name or tag. The group by cardinality should
reflect the number of monitors that match the filters
<img width="733" alt="Screenshot 2024-04-12 at 1 05 11 PM"
src="https://github.com/elastic/kibana/assets/11356435/079c74ea-dd1c-45f2-bf0e-2dbefea30f96">

### Testing #178341
To test the fix for #178341,
create a simple custom kql SLO with a group by. Add a overall filter
that would impact the overall group by count. Verify that the group by
count accurately reflects the overall filter.
  • Loading branch information
dominiqueclarke authored Apr 13, 2024
1 parent b67ab78 commit 672bb5a
Show file tree
Hide file tree
Showing 7 changed files with 618 additions and 38 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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 { ALL_VALUE, QuerySchema } from '@kbn/slo-schema';
import { i18n } from '@kbn/i18n';
import { EuiCallOut, EuiLoadingSpinner } from '@elastic/eui';
import React from 'react';
import { useFormContext } from 'react-hook-form';
import { useFetchGroupByCardinality } from '../../../../hooks/use_fetch_group_by_cardinality';
import { CreateSLOForm } from '../../types';
import { getGroupKeysProse } from '../../../../utils/slo/groupings';

export function GroupByCardinality({
titleAppend,
customFilters,
}: {
titleAppend?: React.ReactNode;
customFilters?: QuerySchema;
}) {
const { watch } = useFormContext<CreateSLOForm>();

const index = watch('indicator.params.index');
const filters = watch('indicator.params.filter');
const timestampField = watch('indicator.params.timestampField');
const groupByField = watch('groupBy');

const { isLoading: isGroupByCardinalityLoading, data: groupByCardinality } =
useFetchGroupByCardinality(index, timestampField, groupByField, customFilters || filters);
const groupBy = [groupByField].flat().filter((value) => !!value);
const hasGroupBy = !groupBy.includes(ALL_VALUE) && groupBy.length;

if (!hasGroupBy) {
return null;
}

if (isGroupByCardinalityLoading && !groupByCardinality) {
return <EuiCallOut size="s" title={<EuiLoadingSpinner />} />;
}

if (!groupByCardinality) {
return null;
}

const cardinalityMessage = i18n.translate('xpack.slo.sloEdit.groupBy.cardinalityInfo', {
defaultMessage:
'Selected group by field {groupBy} will generate at least {card} SLO instances based on the last 24h sample data.',
values: {
card: groupByCardinality.cardinality,
groupBy: getGroupKeysProse(groupByField),
},
});

return (
<EuiCallOut
size="s"
iconType={groupByCardinality.isHighCardinality ? 'warning' : ''}
color={groupByCardinality.isHighCardinality ? 'warning' : 'primary'}
title={
titleAppend ? (
<>
{titleAppend} {cardinalityMessage}
</>
) : (
cardinalityMessage
)
}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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 { canGroupBy } from './group_by_field';

describe('canGroupBy', () => {
it('handles multi fields where there are multi es types', () => {
const field = {
name: 'event.action.keyword',
type: 'string',
esTypes: ['keyword'],
searchable: true,
aggregatable: true,
readFromDocValues: true,
metadata_field: false,
subType: {
multi: {
parent: 'event.action',
},
},
isMapped: true,
shortDotsEnable: false,
};
expect(canGroupBy(field)).toBe(true);
const field2 = {
name: 'event.action',
type: 'string',
esTypes: ['keyword', 'text'],
searchable: true,
aggregatable: true,
readFromDocValues: true,
metadata_field: false,
isMapped: true,
shortDotsEnable: false,
};
expect(canGroupBy(field2)).toBe(false);
});

it('handles date fields', () => {
const field = {
name: '@timestamp',
type: 'date',
esTypes: ['date'],
searchable: true,
aggregatable: true,
readFromDocValues: true,
metadata_field: false,
isMapped: true,
shortDotsEnable: false,
};
expect(canGroupBy(field)).toBe(false);
});

it('handles non aggregatable fields', () => {
const field = {
name: 'event.action',
type: 'string',
esTypes: ['text'],
searchable: true,
aggregatable: false,
readFromDocValues: true,
metadata_field: false,
isMapped: true,
shortDotsEnable: false,
};

expect(canGroupBy(field)).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,20 @@

import { ALL_VALUE } from '@kbn/slo-schema';
import { i18n } from '@kbn/i18n';
import { EuiCallOut, EuiIconTip } from '@elastic/eui';
import { EuiIconTip } from '@elastic/eui';
import React from 'react';
import { DataView } from '@kbn/data-views-plugin/common';
import { DataView, FieldSpec } from '@kbn/data-views-plugin/common';
import { useFormContext } from 'react-hook-form';
import { OptionalText } from './optional_text';
import { useFetchGroupByCardinality } from '../../../../hooks/use_fetch_group_by_cardinality';
import { CreateSLOForm } from '../../types';
import { IndexFieldSelector } from './index_field_selector';
import { getGroupKeysProse } from '../../../../utils/slo/groupings';
import { GroupByCardinality } from './group_by_cardinality';

export function GroupByField({ dataView, isLoading }: { dataView?: DataView; isLoading: boolean }) {
const { watch } = useFormContext<CreateSLOForm>();

const groupByFields =
dataView?.fields?.filter((field) => field.aggregatable && field.type !== 'date') ?? [];
const groupByFields = dataView?.fields?.filter((field) => canGroupBy(field)) ?? [];
const index = watch('indicator.params.index');
const timestampField = watch('indicator.params.timestampField');
const groupByField = watch('groupBy');

const { isLoading: isGroupByCardinalityLoading, data: groupByCardinality } =
useFetchGroupByCardinality(index, timestampField, groupByField);
const groupBy = [groupByField].flat().filter((value) => !!value);
const hasGroupBy = !groupBy.includes(ALL_VALUE) && groupBy.length;

return (
<>
Expand Down Expand Up @@ -57,21 +48,17 @@ export function GroupByField({ dataView, isLoading }: { dataView?: DataView; isL
isLoading={!!index && isLoading}
isDisabled={!index}
/>
{!isGroupByCardinalityLoading && !!groupByCardinality && hasGroupBy && (
<EuiCallOut
size="s"
iconType={groupByCardinality.isHighCardinality ? 'warning' : ''}
color={groupByCardinality.isHighCardinality ? 'warning' : 'primary'}
title={i18n.translate('xpack.slo.sloEdit.groupBy.cardinalityInfo', {
defaultMessage:
'Selected group by field {groupBy} will generate at least {card} SLO instances based on the last 24h sample data.',
values: {
card: groupByCardinality.cardinality,
groupBy: getGroupKeysProse(groupByField),
},
})}
/>
)}
<GroupByCardinality />
</>
);
}

export const canGroupBy = (field: FieldSpec) => {
const isAggregatable = field.aggregatable;
const isNotDate = field.type !== 'date';
// handles multi fields where there are multi es types, which could include 'text'
// text fields break the transforms so we must ensure that the field is only a keyword
const isOnlyKeyword = field.esTypes?.length === 1 && field.esTypes[0] === 'keyword';

return isAggregatable && isNotDate && isOnlyKeyword;
};
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export function SloEditFormObjectiveSection() {
<p>
<FormattedMessage
id="xpack.slo.sloEdit.sliType.syntheticAvailability.objectiveMessage"
defaultMessage="The Synthetics availability indicator requires the budgeting method to be set to 'Occurances'."
defaultMessage="The Synthetics availability indicator requires the budgeting method to be set to 'Occurrences'."
/>
</p>
</EuiCallOut>
Expand Down
Loading

0 comments on commit 672bb5a

Please sign in to comment.