mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
171acb4633
commit
77141f7da2
2 changed files with 179 additions and 18 deletions
|
@ -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 },
|
||||
|
|
|
@ -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);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue