[SLO] Allow users to easily view good/bad events in Discover for event panel (#178008)

## Summary

This PR adds a link to Discover for the "Good vs Bad" event chart for
non-APM indicators for the "Last 24 hours" with all the appropriate
filters applied. If the indicator is a "Custom KQL", the link will
include disabled filters for "Good events" and "Bad events". If the user
clicks on a "Good" or "Bad" bar on the chart for the "Custom KQL"
indicator, this will open Discover with the appropriate filter activated
along with the appropriate time range for the bar.


<img width="1775" alt="image"
src="50fe7ce5-f648-43ad-988d-1b54098461cc">


<img width="2045" alt="image"
src="c19cf93e-1d1c-4e30-9f51-b82af4986783">

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Chris Cowan 2024-03-06 17:05:41 -07:00 committed by GitHub
parent 171acb4633
commit 77141f7da2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 179 additions and 18 deletions

View file

@ -10,6 +10,7 @@ import {
Axis,
BarSeries,
Chart,
ElementClickListener,
LineAnnotation,
Position,
RectAnnotation,
@ -17,11 +18,13 @@ import {
Settings,
Tooltip,
TooltipType,
XYChartElementEvent,
} from '@elastic/charts';
import {
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiLink,
EuiLoadingChart,
EuiPanel,
EuiText,
@ -35,9 +38,11 @@ import { SLOWithSummaryResponse } from '@kbn/slo-schema';
import { max, min } from 'lodash';
import moment from 'moment';
import React, { useRef } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { useGetPreviewData } from '../../../hooks/slo/use_get_preview_data';
import { useKibana } from '../../../utils/kibana_react';
import { COMPARATOR_MAPPING } from '../../slo_edit/constants';
import { openInDiscover, getDiscoverLink } from '../../../utils/slo/get_discover_link';
export interface Props {
slo: SLOWithSummaryResponse;
@ -48,7 +53,7 @@ export interface Props {
}
export function EventsChartPanel({ slo, range }: Props) {
const { charts, uiSettings } = useKibana().services;
const { charts, uiSettings, discover } = useKibana().services;
const { euiTheme } = useEuiTheme();
const baseTheme = charts.theme.useChartsBaseTheme();
const chartRef = useRef(null);
@ -107,6 +112,11 @@ export function EventsChartPanel({ slo, range }: Props) {
threshold != null && maxValue != null && threshold > maxValue ? threshold : maxValue || NaN,
};
const intervalInMilliseconds =
data && data.length > 2
? moment(data[1].date).valueOf() - moment(data[0].date).valueOf()
: 10 * 60000;
const annotation =
slo.indicator.type === 'sli.metric.timeslice' && threshold ? (
<>
@ -142,18 +152,67 @@ export function EventsChartPanel({ slo, range }: Props) {
</>
) : null;
const goodEventId = i18n.translate(
'xpack.observability.slo.sloDetails.eventsChartPanel.goodEventsLabel',
{ defaultMessage: 'Good events' }
);
const badEventId = i18n.translate(
'xpack.observability.slo.sloDetails.eventsChartPanel.badEventsLabel',
{ defaultMessage: 'Bad events' }
);
const barClickHandler = (params: XYChartElementEvent[]) => {
if (slo.indicator.type === 'sli.kql.custom') {
const [datanum, eventDetail] = params[0];
const isBad = eventDetail.specId === badEventId;
const timeRange = {
from: moment(datanum.x).toISOString(),
to: moment(datanum.x).add(intervalInMilliseconds, 'ms').toISOString(),
mode: 'absolute' as const,
};
openInDiscover(discover, slo, isBad, !isBad, timeRange);
}
};
const showViewEventsLink = ![
'sli.apm.transactionErrorRate',
'sli.apm.transactionDuration',
].includes(slo.indicator.type);
return (
<EuiPanel paddingSize="m" color="transparent" hasBorder data-test-subj="eventsChartPanel">
<EuiFlexGroup direction="column" gutterSize="l">
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem>{title}</EuiFlexItem>
<EuiFlexItem>
<EuiText color="subdued" size="s">
{i18n.translate('xpack.observability.slo.sloDetails.eventsChartPanel.duration', {
defaultMessage: 'Last 24h',
})}
</EuiText>
</EuiFlexItem>
<EuiFlexGroup>
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem grow={1}> {title}</EuiFlexItem>
<EuiFlexItem>
<EuiText color="subdued" size="s">
{i18n.translate('xpack.observability.slo.sloDetails.eventsChartPanel.duration', {
defaultMessage: 'Last 24h',
})}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
{showViewEventsLink && (
<EuiFlexItem grow={0}>
<EuiLink
color="text"
href={getDiscoverLink(discover, slo, {
from: 'now-24h',
to: 'now',
mode: 'relative',
})}
data-test-subj="sloDetailDiscoverLink"
>
<EuiIcon type="sortRight" style={{ marginRight: '4px' }} />
<FormattedMessage
id="xpack.observability.slo.sloDetails.viewEventsLink"
defaultMessage="View events"
/>
</EuiLink>
</EuiFlexItem>
)}
</EuiFlexGroup>
<EuiFlexItem>
@ -177,6 +236,7 @@ export function EventsChartPanel({ slo, range }: Props) {
pointerUpdateDebounce={0}
pointerUpdateTrigger={'x'}
locale={i18n.getLocale()}
onElementClick={barClickHandler as ElementClickListener}
/>
{annotation}
@ -196,10 +256,7 @@ export function EventsChartPanel({ slo, range }: Props) {
{slo.indicator.type !== 'sli.metric.timeslice' ? (
<>
<BarSeries
id={i18n.translate(
'xpack.observability.slo.sloDetails.eventsChartPanel.goodEventsLabel',
{ defaultMessage: 'Good events' }
)}
id={goodEventId}
color={euiTheme.colors.success}
barSeriesStyle={{
rect: { fill: euiTheme.colors.success },
@ -219,10 +276,7 @@ export function EventsChartPanel({ slo, range }: Props) {
/>
<BarSeries
id={i18n.translate(
'xpack.observability.slo.sloDetails.eventsChartPanel.badEventsLabel',
{ defaultMessage: 'Bad events' }
)}
id={badEventId}
color={euiTheme.colors.danger}
barSeriesStyle={{
rect: { fill: euiTheme.colors.danger },

View file

@ -0,0 +1,107 @@
/*
* 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 { DiscoverStart } from '@kbn/discover-plugin/public';
import { kqlWithFiltersSchema, SLOWithSummaryResponse } from '@kbn/slo-schema';
import { Filter, FilterStateStore, TimeRange } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { buildEsQuery } from '../build_es_query';
function createDiscoverLocator(
slo: SLOWithSummaryResponse,
showBad = false,
showGood = false,
timeRange?: TimeRange
) {
const filters: Filter[] = [];
if (kqlWithFiltersSchema.is(slo.indicator.params.filter)) {
slo.indicator.params.filter.filters.forEach((i) => filters.push(i));
}
const filterKuery = kqlWithFiltersSchema.is(slo.indicator.params.filter)
? slo.indicator.params.filter.kqlQuery
: slo.indicator.params.filter;
if (slo.indicator.type === 'sli.kql.custom') {
const goodKuery = kqlWithFiltersSchema.is(slo.indicator.params.good)
? slo.indicator.params.good.kqlQuery
: slo.indicator.params.good;
const goodFilters = kqlWithFiltersSchema.is(slo.indicator.params.good)
? slo.indicator.params.good.filters
: [];
const customGoodFilter = buildEsQuery({ kuery: goodKuery, filters: goodFilters });
const customBadFilter = { bool: { must_not: customGoodFilter } };
filters.push({
$state: { store: FilterStateStore.APP_STATE },
meta: {
type: 'custom',
alias: i18n.translate('xpack.observability.slo.sloDetails.goodFilterLabel', {
defaultMessage: 'Good events',
}),
disabled: !showGood,
index: `${slo.indicator.params.index}-id`,
value: JSON.stringify(customGoodFilter),
},
query: customGoodFilter as Record<string, any>,
});
filters.push({
$state: { store: FilterStateStore.APP_STATE },
meta: {
type: 'custom',
alias: i18n.translate('xpack.observability.slo.sloDetails.badFilterLabel', {
defaultMessage: 'Bad events',
}),
disabled: !showBad,
index: `${slo.indicator.params.index}-id`,
value: JSON.stringify(customBadFilter),
},
query: customBadFilter as Record<string, any>,
});
}
const timeFieldName =
slo.indicator.type !== 'sli.apm.transactionDuration' &&
slo.indicator.type !== 'sli.apm.transactionErrorRate'
? slo.indicator.params.timestampField
: '@timestamp';
return {
timeRange,
query: {
query: filterKuery || '',
language: 'kuery',
},
filters,
dataViewSpec: {
id: `${slo.indicator.params.index}-id`,
title: slo.indicator.params.index,
timeFieldName,
},
};
}
export function getDiscoverLink(
discover: DiscoverStart,
slo: SLOWithSummaryResponse,
timeRange: TimeRange
) {
const config = createDiscoverLocator(slo, false, false, timeRange);
return discover?.locator?.getRedirectUrl(config);
}
export function openInDiscover(
discover: DiscoverStart,
slo: SLOWithSummaryResponse,
showBad = false,
showGood = false,
timeRange?: TimeRange
) {
const config = createDiscoverLocator(slo, showBad, showGood, timeRange);
discover?.locator?.navigate(config);
}