mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[APM] Correlations UI POC (#82256)
This commit is contained in:
parent
54ee94d8e8
commit
68b5625e5a
37 changed files with 1251 additions and 517 deletions
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* 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 {
|
||||
ScaleType,
|
||||
Chart,
|
||||
LineSeries,
|
||||
Axis,
|
||||
CurveType,
|
||||
Position,
|
||||
timeFormatter,
|
||||
Settings,
|
||||
} from '@elastic/charts';
|
||||
import React, { useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { useUrlParams } from '../../../hooks/useUrlParams';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher';
|
||||
import {
|
||||
APIReturnType,
|
||||
callApmApi,
|
||||
} from '../../../services/rest/createCallApmApi';
|
||||
import { px } from '../../../style/variables';
|
||||
import { SignificantTermsTable } from './SignificantTermsTable';
|
||||
import { ChartContainer } from '../../shared/charts/chart_container';
|
||||
|
||||
type CorrelationsApiResponse = NonNullable<
|
||||
APIReturnType<'GET /api/apm/correlations/failed_transactions'>
|
||||
>;
|
||||
|
||||
type SignificantTerm = NonNullable<
|
||||
CorrelationsApiResponse['significantTerms']
|
||||
>[0];
|
||||
|
||||
export function ErrorCorrelations() {
|
||||
const [
|
||||
selectedSignificantTerm,
|
||||
setSelectedSignificantTerm,
|
||||
] = useState<SignificantTerm | null>(null);
|
||||
|
||||
const { serviceName } = useParams<{ serviceName?: string }>();
|
||||
const { urlParams, uiFilters } = useUrlParams();
|
||||
const { transactionName, transactionType, start, end } = urlParams;
|
||||
|
||||
const { data, status } = useFetcher(() => {
|
||||
if (start && end) {
|
||||
return callApmApi({
|
||||
endpoint: 'GET /api/apm/correlations/failed_transactions',
|
||||
params: {
|
||||
query: {
|
||||
serviceName,
|
||||
transactionName,
|
||||
transactionType,
|
||||
start,
|
||||
end,
|
||||
uiFilters: JSON.stringify(uiFilters),
|
||||
fieldNames:
|
||||
'transaction.name,user.username,user.id,host.ip,user_agent.name,kubernetes.pod.uuid,kubernetes.pod.name,url.domain,container.id,service.node.name',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [serviceName, start, end, transactionName, transactionType, uiFilters]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="s">
|
||||
<h4>Error rate over time</h4>
|
||||
</EuiTitle>
|
||||
<ErrorTimeseriesChart
|
||||
data={data}
|
||||
status={status}
|
||||
selectedSignificantTerm={selectedSignificantTerm}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<SignificantTermsTable
|
||||
significantTerms={data?.significantTerms}
|
||||
status={status}
|
||||
setSelectedSignificantTerm={setSelectedSignificantTerm}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ErrorTimeseriesChart({
|
||||
data,
|
||||
selectedSignificantTerm,
|
||||
status,
|
||||
}: {
|
||||
data?: CorrelationsApiResponse;
|
||||
selectedSignificantTerm: SignificantTerm | null;
|
||||
status: FETCH_STATUS;
|
||||
}) {
|
||||
const dateFormatter = timeFormatter('HH:mm:ss');
|
||||
|
||||
return (
|
||||
<ChartContainer height={200} hasData={!!data} status={status}>
|
||||
<Chart size={{ height: px(200), width: px(600) }}>
|
||||
<Settings showLegend legendPosition={Position.Bottom} />
|
||||
|
||||
<Axis
|
||||
id="bottom"
|
||||
position={Position.Bottom}
|
||||
showOverlappingTicks
|
||||
tickFormat={dateFormatter}
|
||||
/>
|
||||
<Axis
|
||||
id="left"
|
||||
position={Position.Left}
|
||||
domain={{ min: 0, max: 1 }}
|
||||
tickFormat={(d) => `${roundFloat(d * 100)}%`}
|
||||
/>
|
||||
|
||||
<LineSeries
|
||||
id="Overall error rate"
|
||||
xScaleType={ScaleType.Time}
|
||||
yScaleType={ScaleType.Linear}
|
||||
xAccessor={'x'}
|
||||
yAccessors={['y']}
|
||||
data={data?.overall?.timeseries ?? []}
|
||||
curve={CurveType.CURVE_MONOTONE_X}
|
||||
/>
|
||||
|
||||
{selectedSignificantTerm !== null ? (
|
||||
<LineSeries
|
||||
id="Error rate for selected term"
|
||||
xScaleType={ScaleType.Time}
|
||||
yScaleType={ScaleType.Linear}
|
||||
xAccessor={'x'}
|
||||
yAccessors={['y']}
|
||||
color="red"
|
||||
data={selectedSignificantTerm.timeseries}
|
||||
curve={CurveType.CURVE_MONOTONE_X}
|
||||
/>
|
||||
) : null}
|
||||
</Chart>
|
||||
</ChartContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function roundFloat(n: number, digits = 2) {
|
||||
const factor = Math.pow(10, digits);
|
||||
return Math.round(n * factor) / factor;
|
||||
}
|
|
@ -0,0 +1,273 @@
|
|||
/*
|
||||
* 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 {
|
||||
ScaleType,
|
||||
Chart,
|
||||
LineSeries,
|
||||
Axis,
|
||||
CurveType,
|
||||
BarSeries,
|
||||
Position,
|
||||
timeFormatter,
|
||||
Settings,
|
||||
} from '@elastic/charts';
|
||||
import React, { useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { getDurationFormatter } from '../../../../common/utils/formatters';
|
||||
import { useUrlParams } from '../../../hooks/useUrlParams';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher';
|
||||
import {
|
||||
APIReturnType,
|
||||
callApmApi,
|
||||
} from '../../../services/rest/createCallApmApi';
|
||||
import { SignificantTermsTable } from './SignificantTermsTable';
|
||||
import { ChartContainer } from '../../shared/charts/chart_container';
|
||||
|
||||
type CorrelationsApiResponse = NonNullable<
|
||||
APIReturnType<'GET /api/apm/correlations/slow_transactions'>
|
||||
>;
|
||||
|
||||
type SignificantTerm = NonNullable<
|
||||
CorrelationsApiResponse['significantTerms']
|
||||
>[0];
|
||||
|
||||
export function LatencyCorrelations() {
|
||||
const [
|
||||
selectedSignificantTerm,
|
||||
setSelectedSignificantTerm,
|
||||
] = useState<SignificantTerm | null>(null);
|
||||
|
||||
const { serviceName } = useParams<{ serviceName?: string }>();
|
||||
const { urlParams, uiFilters } = useUrlParams();
|
||||
const { transactionName, transactionType, start, end } = urlParams;
|
||||
|
||||
const { data, status } = useFetcher(() => {
|
||||
if (start && end) {
|
||||
return callApmApi({
|
||||
endpoint: 'GET /api/apm/correlations/slow_transactions',
|
||||
params: {
|
||||
query: {
|
||||
serviceName,
|
||||
transactionName,
|
||||
transactionType,
|
||||
start,
|
||||
end,
|
||||
uiFilters: JSON.stringify(uiFilters),
|
||||
durationPercentile: '50',
|
||||
fieldNames:
|
||||
'user.username,user.id,host.ip,user_agent.name,kubernetes.pod.uuid,kubernetes.pod.name,url.domain,container.id,service.node.name',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [serviceName, start, end, transactionName, transactionType, uiFilters]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="row">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="s">
|
||||
<h4>Average latency over time</h4>
|
||||
</EuiTitle>
|
||||
<LatencyTimeseriesChart
|
||||
data={data}
|
||||
status={status}
|
||||
selectedSignificantTerm={selectedSignificantTerm}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="s">
|
||||
<h4>Latency distribution</h4>
|
||||
</EuiTitle>
|
||||
<LatencyDistributionChart
|
||||
data={data}
|
||||
status={status}
|
||||
selectedSignificantTerm={selectedSignificantTerm}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<SignificantTermsTable
|
||||
significantTerms={data?.significantTerms}
|
||||
status={status}
|
||||
setSelectedSignificantTerm={setSelectedSignificantTerm}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function getTimeseriesYMax(data?: CorrelationsApiResponse) {
|
||||
if (!data?.overall) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const yValues = [
|
||||
...data.overall.timeseries.map((p) => p.y ?? 0),
|
||||
...data.significantTerms.flatMap((term) =>
|
||||
term.timeseries.map((p) => p.y ?? 0)
|
||||
),
|
||||
];
|
||||
return Math.max(...yValues);
|
||||
}
|
||||
|
||||
function getDistributionYMax(data?: CorrelationsApiResponse) {
|
||||
if (!data?.overall) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const yValues = [
|
||||
...data.overall.distribution.map((p) => p.y ?? 0),
|
||||
...data.significantTerms.flatMap((term) =>
|
||||
term.distribution.map((p) => p.y ?? 0)
|
||||
),
|
||||
];
|
||||
return Math.max(...yValues);
|
||||
}
|
||||
|
||||
function LatencyTimeseriesChart({
|
||||
data,
|
||||
selectedSignificantTerm,
|
||||
status,
|
||||
}: {
|
||||
data?: CorrelationsApiResponse;
|
||||
selectedSignificantTerm: SignificantTerm | null;
|
||||
status: FETCH_STATUS;
|
||||
}) {
|
||||
const dateFormatter = timeFormatter('HH:mm:ss');
|
||||
|
||||
const yMax = getTimeseriesYMax(data);
|
||||
const durationFormatter = getDurationFormatter(yMax);
|
||||
|
||||
return (
|
||||
<ChartContainer height={200} hasData={!!data} status={status}>
|
||||
<Chart>
|
||||
<Settings showLegend legendPosition={Position.Bottom} />
|
||||
|
||||
<Axis
|
||||
id="bottom"
|
||||
position={Position.Bottom}
|
||||
showOverlappingTicks
|
||||
tickFormat={dateFormatter}
|
||||
/>
|
||||
<Axis
|
||||
id="left"
|
||||
position={Position.Left}
|
||||
domain={{ min: 0, max: yMax }}
|
||||
tickFormat={(d) => durationFormatter(d).formatted}
|
||||
/>
|
||||
|
||||
<LineSeries
|
||||
id="Overall latency"
|
||||
xScaleType={ScaleType.Time}
|
||||
yScaleType={ScaleType.Linear}
|
||||
xAccessor={'x'}
|
||||
yAccessors={['y']}
|
||||
data={data?.overall?.timeseries || []}
|
||||
curve={CurveType.CURVE_MONOTONE_X}
|
||||
/>
|
||||
|
||||
{selectedSignificantTerm !== null ? (
|
||||
<LineSeries
|
||||
id="Latency for selected term"
|
||||
xScaleType={ScaleType.Time}
|
||||
yScaleType={ScaleType.Linear}
|
||||
xAccessor={'x'}
|
||||
yAccessors={['y']}
|
||||
color="red"
|
||||
data={selectedSignificantTerm.timeseries}
|
||||
curve={CurveType.CURVE_MONOTONE_X}
|
||||
/>
|
||||
) : null}
|
||||
</Chart>
|
||||
</ChartContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function LatencyDistributionChart({
|
||||
data,
|
||||
selectedSignificantTerm,
|
||||
status,
|
||||
}: {
|
||||
data?: CorrelationsApiResponse;
|
||||
selectedSignificantTerm: SignificantTerm | null;
|
||||
status: FETCH_STATUS;
|
||||
}) {
|
||||
const xMax = Math.max(
|
||||
...(data?.overall?.distribution.map((p) => p.x ?? 0) ?? [])
|
||||
);
|
||||
const durationFormatter = getDurationFormatter(xMax);
|
||||
const yMax = getDistributionYMax(data);
|
||||
|
||||
return (
|
||||
<ChartContainer height={200} hasData={!!data} status={status}>
|
||||
<Chart>
|
||||
<Settings
|
||||
showLegend
|
||||
legendPosition={Position.Bottom}
|
||||
tooltip={{
|
||||
headerFormatter: (obj) => {
|
||||
const start = durationFormatter(obj.value);
|
||||
const end = durationFormatter(
|
||||
obj.value + data?.distributionInterval
|
||||
);
|
||||
|
||||
return `${start.value} - ${end.formatted}`;
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Axis
|
||||
id="x-axis"
|
||||
position={Position.Bottom}
|
||||
showOverlappingTicks
|
||||
tickFormat={(d) => durationFormatter(d).formatted}
|
||||
/>
|
||||
<Axis
|
||||
id="y-axis"
|
||||
position={Position.Left}
|
||||
tickFormat={(d) => `${d}%`}
|
||||
domain={{ min: 0, max: yMax }}
|
||||
/>
|
||||
|
||||
<BarSeries
|
||||
id="Overall latency distribution"
|
||||
xScaleType={ScaleType.Linear}
|
||||
yScaleType={ScaleType.Linear}
|
||||
xAccessor={'x'}
|
||||
yAccessors={['y']}
|
||||
data={data?.overall?.distribution || []}
|
||||
minBarHeight={5}
|
||||
tickFormat={(d) => `${roundFloat(d)}%`}
|
||||
/>
|
||||
|
||||
{selectedSignificantTerm !== null ? (
|
||||
<BarSeries
|
||||
id="Latency distribution for selected term"
|
||||
xScaleType={ScaleType.Linear}
|
||||
yScaleType={ScaleType.Linear}
|
||||
xAccessor={'x'}
|
||||
yAccessors={['y']}
|
||||
color="red"
|
||||
data={selectedSignificantTerm.distribution}
|
||||
minBarHeight={5}
|
||||
tickFormat={(d) => `${roundFloat(d)}%`}
|
||||
/>
|
||||
) : null}
|
||||
</Chart>
|
||||
</ChartContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function roundFloat(n: number, digits = 2) {
|
||||
const factor = Math.pow(10, digits);
|
||||
return Math.round(n * factor) / factor;
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { EuiBadge, EuiIcon, EuiToolTip, EuiLink } from '@elastic/eui';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { EuiBasicTable } from '@elastic/eui';
|
||||
import { asPercent, asInteger } from '../../../../common/utils/formatters';
|
||||
import { APIReturnType } from '../../../services/rest/createCallApmApi';
|
||||
import { FETCH_STATUS } from '../../../hooks/useFetcher';
|
||||
import { createHref } from '../../shared/Links/url_helpers';
|
||||
|
||||
type CorrelationsApiResponse =
|
||||
| APIReturnType<'GET /api/apm/correlations/failed_transactions'>
|
||||
| APIReturnType<'GET /api/apm/correlations/slow_transactions'>;
|
||||
|
||||
type SignificantTerm = NonNullable<
|
||||
NonNullable<CorrelationsApiResponse>['significantTerms']
|
||||
>[0];
|
||||
|
||||
interface Props<T> {
|
||||
significantTerms?: T[];
|
||||
status: FETCH_STATUS;
|
||||
setSelectedSignificantTerm: (term: T | null) => void;
|
||||
}
|
||||
|
||||
export function SignificantTermsTable<T extends SignificantTerm>({
|
||||
significantTerms,
|
||||
status,
|
||||
setSelectedSignificantTerm,
|
||||
}: Props<T>) {
|
||||
const history = useHistory();
|
||||
const columns = [
|
||||
{
|
||||
field: 'matches',
|
||||
name: 'Matches',
|
||||
render: (_: any, term: T) => {
|
||||
return (
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={`(${asInteger(term.fgCount)} of ${asInteger(
|
||||
term.bgCount
|
||||
)} requests)`}
|
||||
>
|
||||
<>
|
||||
<EuiBadge
|
||||
color={
|
||||
term.fgCount / term.bgCount > 0.03 ? 'primary' : 'secondary'
|
||||
}
|
||||
>
|
||||
{asPercent(term.fgCount, term.bgCount)}
|
||||
</EuiBadge>
|
||||
({Math.round(term.score)})
|
||||
</>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'fieldName',
|
||||
name: 'Field name',
|
||||
},
|
||||
{
|
||||
field: 'filedValue',
|
||||
name: 'Field value',
|
||||
render: (_: any, term: T) => String(term.fieldValue).slice(0, 50),
|
||||
},
|
||||
{
|
||||
field: 'filedValue',
|
||||
name: '',
|
||||
render: (_: any, term: T) => {
|
||||
return (
|
||||
<>
|
||||
<EuiLink
|
||||
href={createHref(history, {
|
||||
query: {
|
||||
kuery: `${term.fieldName}:"${encodeURIComponent(
|
||||
term.fieldValue
|
||||
)}"`,
|
||||
},
|
||||
})}
|
||||
>
|
||||
<EuiIcon type="magnifyWithPlus" />
|
||||
</EuiLink>
|
||||
<EuiLink
|
||||
href={createHref(history, {
|
||||
query: {
|
||||
kuery: `not ${term.fieldName}:"${encodeURIComponent(
|
||||
term.fieldValue
|
||||
)}"`,
|
||||
},
|
||||
})}
|
||||
>
|
||||
<EuiIcon type="magnifyWithMinus" />
|
||||
</EuiLink>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiBasicTable
|
||||
items={significantTerms ?? []}
|
||||
noItemsMessage={status === FETCH_STATUS.LOADING ? 'Loading' : 'No data'}
|
||||
loading={status === FETCH_STATUS.LOADING}
|
||||
columns={columns}
|
||||
rowProps={(term) => {
|
||||
return {
|
||||
onMouseEnter: () => setSelectedSignificantTerm(term),
|
||||
onMouseLeave: () => setSelectedSignificantTerm(null),
|
||||
};
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -4,82 +4,75 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import url from 'url';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { EuiTitle, EuiListGroup } from '@elastic/eui';
|
||||
import { useUrlParams } from '../../../hooks/useUrlParams';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutHeader,
|
||||
EuiTitle,
|
||||
EuiPortal,
|
||||
EuiCode,
|
||||
EuiLink,
|
||||
EuiCallOut,
|
||||
EuiButton,
|
||||
} from '@elastic/eui';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { enableCorrelations } from '../../../../common/ui_settings_keys';
|
||||
import { useApmPluginContext } from '../../../hooks/useApmPluginContext';
|
||||
|
||||
const SESSION_STORAGE_KEY = 'apm.debug.show_correlations';
|
||||
import { LatencyCorrelations } from './LatencyCorrelations';
|
||||
import { ErrorCorrelations } from './ErrorCorrelations';
|
||||
import { useUrlParams } from '../../../hooks/useUrlParams';
|
||||
import { createHref } from '../../shared/Links/url_helpers';
|
||||
|
||||
export function Correlations() {
|
||||
const location = useLocation();
|
||||
const { serviceName } = useParams<{ serviceName?: string }>();
|
||||
const { urlParams, uiFilters } = useUrlParams();
|
||||
const { core } = useApmPluginContext();
|
||||
const { transactionName, transactionType, start, end } = urlParams;
|
||||
|
||||
if (
|
||||
!location.search.includes('&_show_correlations') &&
|
||||
sessionStorage.getItem(SESSION_STORAGE_KEY) !== 'true'
|
||||
) {
|
||||
const { uiSettings } = useApmPluginContext().core;
|
||||
const { urlParams } = useUrlParams();
|
||||
const history = useHistory();
|
||||
const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
|
||||
if (!uiSettings.get(enableCorrelations)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
sessionStorage.setItem(SESSION_STORAGE_KEY, 'true');
|
||||
|
||||
const query = {
|
||||
serviceName,
|
||||
transactionName,
|
||||
transactionType,
|
||||
start,
|
||||
end,
|
||||
uiFilters: JSON.stringify(uiFilters),
|
||||
fieldNames:
|
||||
'user.username,user.id,host.ip,user_agent.name,kubernetes.pod.uuid,url.domain,container.id,service.node.name',
|
||||
};
|
||||
|
||||
const listItems = [
|
||||
{
|
||||
label: 'Show correlations between two ranges',
|
||||
href: url.format({
|
||||
query: {
|
||||
...query,
|
||||
gap: 24,
|
||||
},
|
||||
pathname: core.http.basePath.prepend(`/api/apm/correlations/ranges`),
|
||||
}),
|
||||
isDisabled: false,
|
||||
iconType: 'tokenRange',
|
||||
size: 's' as const,
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Show correlations for slow transactions',
|
||||
href: url.format({
|
||||
query: {
|
||||
...query,
|
||||
durationPercentile: 95,
|
||||
},
|
||||
pathname: core.http.basePath.prepend(
|
||||
`/api/apm/correlations/slow_durations`
|
||||
),
|
||||
}),
|
||||
isDisabled: false,
|
||||
iconType: 'clock',
|
||||
size: 's' as const,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTitle>
|
||||
<h2>Correlations</h2>
|
||||
</EuiTitle>
|
||||
<EuiButton
|
||||
onClick={() => {
|
||||
setIsFlyoutVisible(true);
|
||||
}}
|
||||
>
|
||||
View correlations
|
||||
</EuiButton>
|
||||
|
||||
<EuiListGroup listItems={listItems} />
|
||||
{isFlyoutVisible && (
|
||||
<EuiPortal>
|
||||
<EuiFlyout
|
||||
size="l"
|
||||
ownFocus
|
||||
onClose={() => setIsFlyoutVisible(false)}
|
||||
>
|
||||
<EuiFlyoutHeader hasBorder aria-labelledby="correlations-flyout">
|
||||
<EuiTitle>
|
||||
<h2 id="correlations-flyout">Correlations</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
{urlParams.kuery ? (
|
||||
<EuiCallOut size="m">
|
||||
<span>Filtering by</span>
|
||||
<EuiCode>{urlParams.kuery}</EuiCode>
|
||||
<EuiLink href={createHref(history, { query: { kuery: '' } })}>
|
||||
<EuiButtonEmpty iconType="cross">Clear</EuiButtonEmpty>
|
||||
</EuiLink>
|
||||
</EuiCallOut>
|
||||
) : null}
|
||||
|
||||
<LatencyCorrelations />
|
||||
<ErrorCorrelations />
|
||||
</EuiFlyoutBody>
|
||||
</EuiFlyout>
|
||||
</EuiPortal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -119,9 +119,9 @@ export function TransactionDetails({
|
|||
</ApmHeader>
|
||||
<SearchBar />
|
||||
<EuiPage>
|
||||
<Correlations />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={1}>
|
||||
<Correlations />
|
||||
<LocalUIFilters {...localUIFiltersConfig} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={7}>
|
||||
|
|
|
@ -123,10 +123,11 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) {
|
|||
return (
|
||||
<>
|
||||
<SearchBar />
|
||||
<Correlations />
|
||||
|
||||
<EuiPage>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={1}>
|
||||
<Correlations />
|
||||
<LocalUIFilters {...localFiltersConfig}>
|
||||
<TransactionTypeFilter
|
||||
transactionTypes={serviceTransactionTypes}
|
||||
|
|
|
@ -129,9 +129,9 @@ export function ServiceInventory() {
|
|||
<>
|
||||
<SearchBar />
|
||||
<EuiPage>
|
||||
<Correlations />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={1}>
|
||||
<Correlations />
|
||||
<LocalUIFilters {...localFiltersConfig} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={7}>
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { History } from 'history';
|
||||
import { parse, stringify } from 'query-string';
|
||||
import { url } from '../../../../../../../src/plugins/kibana_utils/public';
|
||||
import { LocalUIFilterName } from '../../../../common/ui_filter';
|
||||
|
@ -20,6 +21,48 @@ export function fromQuery(query: Record<string, any>) {
|
|||
return stringify(encodedQuery, { sort: false, encode: false });
|
||||
}
|
||||
|
||||
type LocationWithQuery = Partial<
|
||||
History['location'] & {
|
||||
query: Record<string, string>;
|
||||
}
|
||||
>;
|
||||
|
||||
function getNextLocation(
|
||||
history: History,
|
||||
locationWithQuery: LocationWithQuery
|
||||
) {
|
||||
const { query, ...rest } = locationWithQuery;
|
||||
return {
|
||||
...history.location,
|
||||
...rest,
|
||||
search: fromQuery({
|
||||
...toQuery(history.location.search),
|
||||
...query,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function replace(
|
||||
history: History,
|
||||
locationWithQuery: LocationWithQuery
|
||||
) {
|
||||
const location = getNextLocation(history, locationWithQuery);
|
||||
return history.replace(location);
|
||||
}
|
||||
|
||||
export function push(history: History, locationWithQuery: LocationWithQuery) {
|
||||
const location = getNextLocation(history, locationWithQuery);
|
||||
return history.push(location);
|
||||
}
|
||||
|
||||
export function createHref(
|
||||
history: History,
|
||||
locationWithQuery: LocationWithQuery
|
||||
) {
|
||||
const location = getNextLocation(history, locationWithQuery);
|
||||
return history.createHref(location);
|
||||
}
|
||||
|
||||
export type APMQueryParams = {
|
||||
transactionId?: string;
|
||||
transactionName?: string;
|
||||
|
|
|
@ -115,6 +115,12 @@ For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme)
|
|||
|
||||
_Note: Run the following commands from `kibana/`._
|
||||
|
||||
### Typescript
|
||||
|
||||
```
|
||||
yarn tsc --noEmit --project x-pack/tsconfig.json --skipLibCheck
|
||||
```
|
||||
|
||||
### Prettier
|
||||
|
||||
```
|
||||
|
|
|
@ -10,7 +10,6 @@ import { snakeCase } from 'lodash';
|
|||
import Boom from '@hapi/boom';
|
||||
import { ProcessorEvent } from '../../../common/processor_event';
|
||||
import { ML_ERRORS } from '../../../common/anomaly_detection';
|
||||
import { PromiseReturnType } from '../../../../observability/typings/common';
|
||||
import { Setup } from '../helpers/setup_request';
|
||||
import {
|
||||
TRANSACTION_DURATION,
|
||||
|
@ -19,9 +18,6 @@ import {
|
|||
import { APM_ML_JOB_GROUP, ML_MODULE_ID_APM_TRANSACTION } from './constants';
|
||||
import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es';
|
||||
|
||||
export type CreateAnomalyDetectionJobsAPIResponse = PromiseReturnType<
|
||||
typeof createAnomalyDetectionJobs
|
||||
>;
|
||||
export async function createAnomalyDetectionJobs(
|
||||
setup: Setup,
|
||||
environments: string[],
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
* 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 { isEmpty } from 'lodash';
|
||||
import { EventOutcome } from '../../../../common/event_outcome';
|
||||
import {
|
||||
formatTopSignificantTerms,
|
||||
TopSigTerm,
|
||||
} from '../get_correlations_for_slow_transactions/format_top_significant_terms';
|
||||
import { AggregationOptionsByType } from '../../../../../../typings/elasticsearch/aggregations';
|
||||
import { ESFilter } from '../../../../../../typings/elasticsearch';
|
||||
import { rangeFilter } from '../../../../common/utils/range_filter';
|
||||
import {
|
||||
EVENT_OUTCOME,
|
||||
SERVICE_NAME,
|
||||
TRANSACTION_NAME,
|
||||
TRANSACTION_TYPE,
|
||||
} from '../../../../common/elasticsearch_fieldnames';
|
||||
import { ProcessorEvent } from '../../../../common/processor_event';
|
||||
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
|
||||
import { getBucketSize } from '../../helpers/get_bucket_size';
|
||||
import {
|
||||
getOutcomeAggregation,
|
||||
getTransactionErrorRateTimeSeries,
|
||||
} from '../../helpers/transaction_error_rate';
|
||||
|
||||
export async function getCorrelationsForFailedTransactions({
|
||||
serviceName,
|
||||
transactionType,
|
||||
transactionName,
|
||||
fieldNames,
|
||||
setup,
|
||||
}: {
|
||||
serviceName: string | undefined;
|
||||
transactionType: string | undefined;
|
||||
transactionName: string | undefined;
|
||||
fieldNames: string[];
|
||||
setup: Setup & SetupTimeRange;
|
||||
}) {
|
||||
const { start, end, esFilter, apmEventClient } = setup;
|
||||
|
||||
const backgroundFilters: ESFilter[] = [
|
||||
...esFilter,
|
||||
{ range: rangeFilter(start, end) },
|
||||
];
|
||||
|
||||
if (serviceName) {
|
||||
backgroundFilters.push({ term: { [SERVICE_NAME]: serviceName } });
|
||||
}
|
||||
|
||||
if (transactionType) {
|
||||
backgroundFilters.push({ term: { [TRANSACTION_TYPE]: transactionType } });
|
||||
}
|
||||
|
||||
if (transactionName) {
|
||||
backgroundFilters.push({ term: { [TRANSACTION_NAME]: transactionName } });
|
||||
}
|
||||
|
||||
const params = {
|
||||
apm: { events: [ProcessorEvent.transaction] },
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
// foreground filters
|
||||
filter: [
|
||||
...backgroundFilters,
|
||||
{ term: { [EVENT_OUTCOME]: EventOutcome.failure } },
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: fieldNames.reduce((acc, fieldName) => {
|
||||
return {
|
||||
...acc,
|
||||
[fieldName]: {
|
||||
significant_terms: {
|
||||
size: 10,
|
||||
field: fieldName,
|
||||
background_filter: { bool: { filter: backgroundFilters } },
|
||||
},
|
||||
},
|
||||
};
|
||||
}, {} as Record<string, { significant_terms: AggregationOptionsByType['significant_terms'] }>),
|
||||
},
|
||||
};
|
||||
|
||||
const response = await apmEventClient.search(params);
|
||||
const topSigTerms = formatTopSignificantTerms(response.aggregations);
|
||||
return getChartsForTopSigTerms({ setup, backgroundFilters, topSigTerms });
|
||||
}
|
||||
|
||||
export async function getChartsForTopSigTerms({
|
||||
setup,
|
||||
backgroundFilters,
|
||||
topSigTerms,
|
||||
}: {
|
||||
setup: Setup & SetupTimeRange;
|
||||
backgroundFilters: ESFilter[];
|
||||
topSigTerms: TopSigTerm[];
|
||||
}) {
|
||||
const { start, end, apmEventClient } = setup;
|
||||
const { intervalString } = getBucketSize({ start, end, numBuckets: 30 });
|
||||
|
||||
if (isEmpty(topSigTerms)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const timeseriesAgg = {
|
||||
date_histogram: {
|
||||
field: '@timestamp',
|
||||
fixed_interval: intervalString,
|
||||
min_doc_count: 0,
|
||||
extended_bounds: { min: start, max: end },
|
||||
},
|
||||
aggs: {
|
||||
// TODO: add support for metrics
|
||||
outcomes: getOutcomeAggregation({ searchAggregatedTransactions: false }),
|
||||
},
|
||||
};
|
||||
|
||||
const perTermAggs = topSigTerms.reduce(
|
||||
(acc, term, index) => {
|
||||
acc[`term_${index}`] = {
|
||||
filter: { term: { [term.fieldName]: term.fieldValue } },
|
||||
aggs: { timeseries: timeseriesAgg },
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
{} as Record<
|
||||
string,
|
||||
{
|
||||
filter: AggregationOptionsByType['filter'];
|
||||
aggs: { timeseries: typeof timeseriesAgg };
|
||||
}
|
||||
>
|
||||
);
|
||||
|
||||
const params = {
|
||||
// TODO: add support for metrics
|
||||
apm: { events: [ProcessorEvent.transaction] },
|
||||
body: {
|
||||
size: 0,
|
||||
query: { bool: { filter: backgroundFilters } },
|
||||
aggs: {
|
||||
// overall aggs
|
||||
timeseries: timeseriesAgg,
|
||||
|
||||
// per term aggs
|
||||
...perTermAggs,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await apmEventClient.search(params);
|
||||
type Agg = NonNullable<typeof response.aggregations>;
|
||||
|
||||
if (!response.aggregations) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
overall: {
|
||||
timeseries: getTransactionErrorRateTimeSeries(
|
||||
response.aggregations.timeseries.buckets
|
||||
),
|
||||
},
|
||||
significantTerms: topSigTerms.map((topSig, index) => {
|
||||
// @ts-expect-error
|
||||
const agg = response.aggregations[`term_${index}`] as Agg;
|
||||
|
||||
return {
|
||||
...topSig,
|
||||
timeseries: getTransactionErrorRateTimeSeries(agg.timeseries.buckets),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 { orderBy } from 'lodash';
|
||||
import {
|
||||
AggregationOptionsByType,
|
||||
AggregationResultOf,
|
||||
} from '../../../../../../typings/elasticsearch/aggregations';
|
||||
|
||||
export interface TopSigTerm {
|
||||
bgCount: number;
|
||||
fgCount: number;
|
||||
fieldName: string;
|
||||
fieldValue: string | number;
|
||||
score: number;
|
||||
}
|
||||
|
||||
type SigTermAggs = AggregationResultOf<
|
||||
{ significant_terms: AggregationOptionsByType['significant_terms'] },
|
||||
{}
|
||||
>;
|
||||
|
||||
export function formatTopSignificantTerms(
|
||||
aggregations?: Record<string, SigTermAggs>
|
||||
) {
|
||||
const significantTerms = Object.entries(aggregations ?? []).flatMap(
|
||||
([fieldName, agg]) => {
|
||||
return agg.buckets.map((bucket) => ({
|
||||
fieldName,
|
||||
fieldValue: bucket.key,
|
||||
bgCount: bucket.bg_count,
|
||||
fgCount: bucket.doc_count,
|
||||
score: bucket.score,
|
||||
}));
|
||||
}
|
||||
);
|
||||
|
||||
// get top 10 terms ordered by score
|
||||
const topSigTerms = orderBy(significantTerms, 'score', 'desc').slice(0, 10);
|
||||
return topSigTerms;
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
* 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 { isEmpty } from 'lodash';
|
||||
import { AggregationOptionsByType } from '../../../../../../typings/elasticsearch/aggregations';
|
||||
import { ESFilter } from '../../../../../../typings/elasticsearch';
|
||||
import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames';
|
||||
import { ProcessorEvent } from '../../../../common/processor_event';
|
||||
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
|
||||
import { getBucketSize } from '../../helpers/get_bucket_size';
|
||||
import { TopSigTerm } from './format_top_significant_terms';
|
||||
import { getMaxLatency } from './get_max_latency';
|
||||
|
||||
export async function getChartsForTopSigTerms({
|
||||
setup,
|
||||
backgroundFilters,
|
||||
topSigTerms,
|
||||
}: {
|
||||
setup: Setup & SetupTimeRange;
|
||||
backgroundFilters: ESFilter[];
|
||||
topSigTerms: TopSigTerm[];
|
||||
}) {
|
||||
const { start, end, apmEventClient } = setup;
|
||||
const { intervalString } = getBucketSize({ start, end, numBuckets: 30 });
|
||||
|
||||
if (isEmpty(topSigTerms)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const maxLatency = await getMaxLatency({
|
||||
setup,
|
||||
backgroundFilters,
|
||||
topSigTerms,
|
||||
});
|
||||
|
||||
if (!maxLatency) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const intervalBuckets = 20;
|
||||
const distributionInterval = roundtoTenth(maxLatency / intervalBuckets);
|
||||
|
||||
const distributionAgg = {
|
||||
// filter out outliers not included in the significant term docs
|
||||
filter: { range: { [TRANSACTION_DURATION]: { lte: maxLatency } } },
|
||||
aggs: {
|
||||
dist_filtered_by_latency: {
|
||||
histogram: {
|
||||
// TODO: add support for metrics
|
||||
field: TRANSACTION_DURATION,
|
||||
interval: distributionInterval,
|
||||
min_doc_count: 0,
|
||||
extended_bounds: {
|
||||
min: 0,
|
||||
max: maxLatency,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const timeseriesAgg = {
|
||||
date_histogram: {
|
||||
field: '@timestamp',
|
||||
fixed_interval: intervalString,
|
||||
min_doc_count: 0,
|
||||
extended_bounds: { min: start, max: end },
|
||||
},
|
||||
aggs: {
|
||||
average: {
|
||||
avg: {
|
||||
// TODO: add support for metrics
|
||||
field: TRANSACTION_DURATION,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const perTermAggs = topSigTerms.reduce(
|
||||
(acc, term, index) => {
|
||||
acc[`term_${index}`] = {
|
||||
filter: { term: { [term.fieldName]: term.fieldValue } },
|
||||
aggs: {
|
||||
distribution: distributionAgg,
|
||||
timeseries: timeseriesAgg,
|
||||
},
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
{} as Record<
|
||||
string,
|
||||
{
|
||||
filter: AggregationOptionsByType['filter'];
|
||||
aggs: {
|
||||
distribution: typeof distributionAgg;
|
||||
timeseries: typeof timeseriesAgg;
|
||||
};
|
||||
}
|
||||
>
|
||||
);
|
||||
|
||||
const params = {
|
||||
// TODO: add support for metrics
|
||||
apm: { events: [ProcessorEvent.transaction] },
|
||||
body: {
|
||||
size: 0,
|
||||
query: { bool: { filter: backgroundFilters } },
|
||||
aggs: {
|
||||
// overall aggs
|
||||
distribution: distributionAgg,
|
||||
timeseries: timeseriesAgg,
|
||||
|
||||
// per term aggs
|
||||
...perTermAggs,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await apmEventClient.search(params);
|
||||
type Agg = NonNullable<typeof response.aggregations>;
|
||||
|
||||
if (!response.aggregations) {
|
||||
return;
|
||||
}
|
||||
|
||||
function formatTimeseries(timeseries: Agg['timeseries']) {
|
||||
return timeseries.buckets.map((bucket) => ({
|
||||
x: bucket.key,
|
||||
y: bucket.average.value,
|
||||
}));
|
||||
}
|
||||
|
||||
function formatDistribution(distribution: Agg['distribution']) {
|
||||
const total = distribution.doc_count;
|
||||
return distribution.dist_filtered_by_latency.buckets.map((bucket) => ({
|
||||
x: bucket.key,
|
||||
y: (bucket.doc_count / total) * 100,
|
||||
}));
|
||||
}
|
||||
|
||||
return {
|
||||
distributionInterval,
|
||||
overall: {
|
||||
timeseries: formatTimeseries(response.aggregations.timeseries),
|
||||
distribution: formatDistribution(response.aggregations.distribution),
|
||||
},
|
||||
significantTerms: topSigTerms.map((topSig, index) => {
|
||||
// @ts-expect-error
|
||||
const agg = response.aggregations[`term_${index}`] as Agg;
|
||||
|
||||
return {
|
||||
...topSig,
|
||||
timeseries: formatTimeseries(agg.timeseries),
|
||||
distribution: formatDistribution(agg.distribution),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function roundtoTenth(v: number) {
|
||||
return Math.pow(10, Math.round(Math.log10(v)));
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { ESFilter } from '../../../../../../typings/elasticsearch';
|
||||
import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames';
|
||||
import { ProcessorEvent } from '../../../../common/processor_event';
|
||||
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
|
||||
import { TopSigTerm } from './format_top_significant_terms';
|
||||
|
||||
export async function getMaxLatency({
|
||||
setup,
|
||||
backgroundFilters,
|
||||
topSigTerms,
|
||||
}: {
|
||||
setup: Setup & SetupTimeRange;
|
||||
backgroundFilters: ESFilter[];
|
||||
topSigTerms: TopSigTerm[];
|
||||
}) {
|
||||
const { apmEventClient } = setup;
|
||||
|
||||
const params = {
|
||||
// TODO: add support for metrics
|
||||
apm: { events: [ProcessorEvent.transaction] },
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: backgroundFilters,
|
||||
|
||||
// only include docs containing the significant terms
|
||||
should: topSigTerms.map((term) => ({
|
||||
term: { [term.fieldName]: term.fieldValue },
|
||||
})),
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
// TODO: add support for metrics
|
||||
// max_latency: { max: { field: TRANSACTION_DURATION } },
|
||||
max_latency: {
|
||||
percentiles: { field: TRANSACTION_DURATION, percents: [99] },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await apmEventClient.search(params);
|
||||
// return response.aggregations?.max_latency.value;
|
||||
return Object.values(response.aggregations?.max_latency.values ?? {})[0];
|
||||
}
|
|
@ -4,7 +4,9 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AggregationOptionsByType } from '../../../../../../typings/elasticsearch/aggregations';
|
||||
import { ESFilter } from '../../../../../../typings/elasticsearch';
|
||||
import { rangeFilter } from '../../../../common/utils/range_filter';
|
||||
import {
|
||||
SERVICE_NAME,
|
||||
TRANSACTION_DURATION,
|
||||
|
@ -12,15 +14,10 @@ import {
|
|||
TRANSACTION_TYPE,
|
||||
} from '../../../../common/elasticsearch_fieldnames';
|
||||
import { ProcessorEvent } from '../../../../common/processor_event';
|
||||
import { asDuration } from '../../../../common/utils/formatters';
|
||||
import { rangeFilter } from '../../../../common/utils/range_filter';
|
||||
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
|
||||
import { getDurationForPercentile } from './get_duration_for_percentile';
|
||||
import {
|
||||
formatAggregationResponse,
|
||||
getSignificantTermsAgg,
|
||||
} from './get_significant_terms_agg';
|
||||
import { SignificantTermsScoring } from './scoring_rt';
|
||||
import { formatTopSignificantTerms } from './format_top_significant_terms';
|
||||
import { getChartsForTopSigTerms } from './get_charts_for_top_sig_terms';
|
||||
|
||||
export async function getCorrelationsForSlowTransactions({
|
||||
serviceName,
|
||||
|
@ -28,13 +25,11 @@ export async function getCorrelationsForSlowTransactions({
|
|||
transactionName,
|
||||
durationPercentile,
|
||||
fieldNames,
|
||||
scoring,
|
||||
setup,
|
||||
}: {
|
||||
serviceName: string | undefined;
|
||||
transactionType: string | undefined;
|
||||
transactionName: string | undefined;
|
||||
scoring: SignificantTermsScoring;
|
||||
durationPercentile: number;
|
||||
fieldNames: string[];
|
||||
setup: Setup & SetupTimeRange;
|
||||
|
@ -79,16 +74,22 @@ export async function getCorrelationsForSlowTransactions({
|
|||
],
|
||||
},
|
||||
},
|
||||
aggs: getSignificantTermsAgg({ fieldNames, backgroundFilters, scoring }),
|
||||
aggs: fieldNames.reduce((acc, fieldName) => {
|
||||
return {
|
||||
...acc,
|
||||
[fieldName]: {
|
||||
significant_terms: {
|
||||
size: 10,
|
||||
field: fieldName,
|
||||
background_filter: { bool: { filter: backgroundFilters } },
|
||||
},
|
||||
},
|
||||
};
|
||||
}, {} as Record<string, { significant_terms: AggregationOptionsByType['significant_terms'] }>),
|
||||
},
|
||||
};
|
||||
|
||||
const response = await apmEventClient.search(params);
|
||||
|
||||
return {
|
||||
message: `Showing significant fields for transactions slower than ${durationPercentile}th percentile (${asDuration(
|
||||
durationForPercentile
|
||||
)})`,
|
||||
response: formatAggregationResponse(response.aggregations),
|
||||
};
|
||||
const topSigTerms = formatTopSignificantTerms(response.aggregations);
|
||||
return getChartsForTopSigTerms({ setup, backgroundFilters, topSigTerms });
|
||||
}
|
|
@ -8,11 +8,15 @@ import moment from 'moment';
|
|||
// @ts-expect-error
|
||||
import { calculateAuto } from './calculate_auto';
|
||||
|
||||
export function getBucketSize(
|
||||
start: number,
|
||||
end: number,
|
||||
numBuckets: number = 100
|
||||
) {
|
||||
export function getBucketSize({
|
||||
start,
|
||||
end,
|
||||
numBuckets = 100,
|
||||
}: {
|
||||
start: number;
|
||||
end: number;
|
||||
numBuckets?: number;
|
||||
}) {
|
||||
const duration = moment.duration(end - start, 'ms');
|
||||
const bucketSize = Math.max(
|
||||
calculateAuto.near(numBuckets, duration).asSeconds(),
|
||||
|
|
|
@ -11,7 +11,7 @@ export function getMetricsDateHistogramParams(
|
|||
end: number,
|
||||
metricsInterval: number
|
||||
) {
|
||||
const { bucketSize } = getBucketSize(start, end);
|
||||
const { bucketSize } = getBucketSize({ start, end });
|
||||
return {
|
||||
field: '@timestamp',
|
||||
|
||||
|
|
|
@ -20,6 +20,8 @@ export function getOutcomeAggregation({
|
|||
return {
|
||||
terms: { field: EVENT_OUTCOME },
|
||||
aggs: {
|
||||
// simply using the doc count to get the number of requests is not possible for transaction metrics (histograms)
|
||||
// to work around this we get the number of transactions by counting the number of latency values
|
||||
count: {
|
||||
value_count: {
|
||||
field: getTransactionDurationFieldForAggregatedTransactions(
|
||||
|
|
|
@ -40,7 +40,7 @@ export async function fetchAndTransformGcMetrics({
|
|||
}) {
|
||||
const { start, end, apmEventClient, config } = setup;
|
||||
|
||||
const { bucketSize } = getBucketSize(start, end);
|
||||
const { bucketSize } = getBucketSize({ start, end });
|
||||
|
||||
const projection = getMetricsProjection({
|
||||
setup,
|
||||
|
|
|
@ -43,7 +43,7 @@ export async function getServiceErrorGroups({
|
|||
}) {
|
||||
const { apmEventClient, start, end, esFilter } = setup;
|
||||
|
||||
const { intervalString } = getBucketSize(start, end, numBuckets);
|
||||
const { intervalString } = getBucketSize({ start, end, numBuckets });
|
||||
|
||||
const response = await apmEventClient.search({
|
||||
apm: {
|
||||
|
|
|
@ -37,7 +37,8 @@ import {
|
|||
function getDateHistogramOpts(start: number, end: number) {
|
||||
return {
|
||||
field: '@timestamp',
|
||||
fixed_interval: getBucketSize(start, end, 20).intervalString,
|
||||
fixed_interval: getBucketSize({ start, end, numBuckets: 20 })
|
||||
.intervalString,
|
||||
min_doc_count: 0,
|
||||
extended_bounds: { min: start, max: end },
|
||||
};
|
||||
|
|
|
@ -1,90 +0,0 @@
|
|||
/*
|
||||
* 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 { rangeFilter } from '../../../../common/utils/range_filter';
|
||||
import {
|
||||
SERVICE_NAME,
|
||||
TRANSACTION_NAME,
|
||||
TRANSACTION_TYPE,
|
||||
} from '../../../../common/elasticsearch_fieldnames';
|
||||
import { ProcessorEvent } from '../../../../common/processor_event';
|
||||
import { Setup, SetupTimeRange } from '../../helpers/setup_request';
|
||||
import {
|
||||
getSignificantTermsAgg,
|
||||
formatAggregationResponse,
|
||||
} from './get_significant_terms_agg';
|
||||
import { SignificantTermsScoring } from './scoring_rt';
|
||||
|
||||
export async function getCorrelationsForRanges({
|
||||
serviceName,
|
||||
transactionType,
|
||||
transactionName,
|
||||
scoring,
|
||||
gapBetweenRanges,
|
||||
fieldNames,
|
||||
setup,
|
||||
}: {
|
||||
serviceName: string | undefined;
|
||||
transactionType: string | undefined;
|
||||
transactionName: string | undefined;
|
||||
scoring: SignificantTermsScoring;
|
||||
gapBetweenRanges: number;
|
||||
fieldNames: string[];
|
||||
setup: Setup & SetupTimeRange;
|
||||
}) {
|
||||
const { start, end, esFilter, apmEventClient } = setup;
|
||||
|
||||
const baseFilters = [...esFilter];
|
||||
|
||||
if (serviceName) {
|
||||
baseFilters.push({ term: { [SERVICE_NAME]: serviceName } });
|
||||
}
|
||||
|
||||
if (transactionType) {
|
||||
baseFilters.push({ term: { [TRANSACTION_TYPE]: transactionType } });
|
||||
}
|
||||
|
||||
if (transactionName) {
|
||||
baseFilters.push({ term: { [TRANSACTION_NAME]: transactionName } });
|
||||
}
|
||||
|
||||
const diff = end - start + gapBetweenRanges;
|
||||
const baseRangeStart = start - diff;
|
||||
const baseRangeEnd = end - diff;
|
||||
const backgroundFilters = [
|
||||
...baseFilters,
|
||||
{ range: rangeFilter(baseRangeStart, baseRangeEnd) },
|
||||
];
|
||||
|
||||
const params = {
|
||||
apm: { events: [ProcessorEvent.transaction] },
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: { filter: [...baseFilters, { range: rangeFilter(start, end) }] },
|
||||
},
|
||||
aggs: getSignificantTermsAgg({
|
||||
fieldNames,
|
||||
backgroundFilters,
|
||||
backgroundIsSuperset: false,
|
||||
scoring,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const response = await apmEventClient.search(params);
|
||||
|
||||
return {
|
||||
message: `Showing significant fields between the ranges`,
|
||||
firstRange: `${new Date(baseRangeStart).toISOString()} - ${new Date(
|
||||
baseRangeEnd
|
||||
).toISOString()}`,
|
||||
lastRange: `${new Date(start).toISOString()} - ${new Date(
|
||||
end
|
||||
).toISOString()}`,
|
||||
response: formatAggregationResponse(response.aggregations),
|
||||
};
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
/*
|
||||
* 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 { ESFilter } from '../../../../../../typings/elasticsearch';
|
||||
import { SignificantTermsScoring } from './scoring_rt';
|
||||
|
||||
export function getSignificantTermsAgg({
|
||||
fieldNames,
|
||||
backgroundFilters,
|
||||
backgroundIsSuperset = true,
|
||||
scoring = 'percentage',
|
||||
}: {
|
||||
fieldNames: string[];
|
||||
backgroundFilters: ESFilter[];
|
||||
backgroundIsSuperset?: boolean;
|
||||
scoring: SignificantTermsScoring;
|
||||
}) {
|
||||
return fieldNames.reduce((acc, fieldName) => {
|
||||
return {
|
||||
...acc,
|
||||
[fieldName]: {
|
||||
significant_terms: {
|
||||
size: 10,
|
||||
field: fieldName,
|
||||
background_filter: { bool: { filter: backgroundFilters } },
|
||||
|
||||
// indicate whether background is a superset of the foreground
|
||||
mutual_information: { background_is_superset: backgroundIsSuperset },
|
||||
|
||||
// different scorings https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-significantterms-aggregation.html#significantterms-aggregation-parameters
|
||||
[scoring]: {},
|
||||
min_doc_count: 5,
|
||||
shard_min_doc_count: 5,
|
||||
},
|
||||
},
|
||||
[`cardinality-${fieldName}`]: {
|
||||
cardinality: { field: fieldName },
|
||||
},
|
||||
};
|
||||
}, {} as Record<string, any>);
|
||||
}
|
||||
|
||||
export function formatAggregationResponse(aggs?: Record<string, any>) {
|
||||
if (!aggs) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Object.entries(aggs).reduce((acc, [key, value]) => {
|
||||
if (key.startsWith('cardinality-')) {
|
||||
if (value.value > 0) {
|
||||
const fieldName = key.slice(12);
|
||||
acc[fieldName] = {
|
||||
...acc[fieldName],
|
||||
cardinality: value.value,
|
||||
};
|
||||
}
|
||||
} else if (value.buckets.length > 0) {
|
||||
acc[key] = {
|
||||
...acc[key],
|
||||
value,
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, { cardinality: number; value: any }>);
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
/*
|
||||
* 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 t from 'io-ts';
|
||||
|
||||
export const scoringRt = t.union([
|
||||
t.literal('jlh'),
|
||||
t.literal('chi_square'),
|
||||
t.literal('gnd'),
|
||||
t.literal('percentage'),
|
||||
]);
|
||||
|
||||
export type SignificantTermsScoring = t.TypeOf<typeof scoringRt>;
|
|
@ -78,7 +78,7 @@ export async function getErrorRate({
|
|||
timeseries: {
|
||||
date_histogram: {
|
||||
field: '@timestamp',
|
||||
fixed_interval: getBucketSize(start, end).intervalString,
|
||||
fixed_interval: getBucketSize({ start, end }).intervalString,
|
||||
min_doc_count: 0,
|
||||
extended_bounds: { min: start, max: end },
|
||||
},
|
||||
|
|
|
@ -77,7 +77,7 @@ export async function getAnomalySeries({
|
|||
return;
|
||||
}
|
||||
|
||||
const { intervalString, bucketSize } = getBucketSize(start, end);
|
||||
const { intervalString, bucketSize } = getBucketSize({ start, end });
|
||||
|
||||
const esResponse = await anomalySeriesFetcher({
|
||||
serviceName,
|
||||
|
|
|
@ -36,7 +36,7 @@ export function timeseriesFetcher({
|
|||
searchAggregatedTransactions: boolean;
|
||||
}) {
|
||||
const { start, end, apmEventClient } = setup;
|
||||
const { intervalString } = getBucketSize(start, end);
|
||||
const { intervalString } = getBucketSize({ start, end });
|
||||
|
||||
const filter: ESFilter[] = [
|
||||
{ term: { [SERVICE_NAME]: serviceName } },
|
||||
|
|
|
@ -17,7 +17,7 @@ export async function getApmTimeseriesData(options: {
|
|||
searchAggregatedTransactions: boolean;
|
||||
}) {
|
||||
const { start, end } = options.setup;
|
||||
const { bucketSize } = getBucketSize(start, end);
|
||||
const { bucketSize } = getBucketSize({ start, end });
|
||||
const durationAsMinutes = (end - start) / 1000 / 60;
|
||||
|
||||
const timeseriesResponse = await timeseriesFetcher(options);
|
||||
|
|
|
@ -6,21 +6,19 @@
|
|||
|
||||
import * as t from 'io-ts';
|
||||
import { rangeRt } from './default_api_types';
|
||||
import { getCorrelationsForSlowTransactions } from '../lib/transaction_groups/correlations/get_correlations_for_slow_transactions';
|
||||
import { getCorrelationsForRanges } from '../lib/transaction_groups/correlations/get_correlations_for_ranges';
|
||||
import { scoringRt } from '../lib/transaction_groups/correlations/scoring_rt';
|
||||
import { getCorrelationsForSlowTransactions } from '../lib/correlations/get_correlations_for_slow_transactions';
|
||||
import { getCorrelationsForFailedTransactions } from '../lib/correlations/get_correlations_for_failed_transactions';
|
||||
import { createRoute } from './create_route';
|
||||
import { setupRequest } from '../lib/helpers/setup_request';
|
||||
|
||||
export const correlationsForSlowTransactionsRoute = createRoute({
|
||||
endpoint: 'GET /api/apm/correlations/slow_durations',
|
||||
endpoint: 'GET /api/apm/correlations/slow_transactions',
|
||||
params: t.type({
|
||||
query: t.intersection([
|
||||
t.partial({
|
||||
serviceName: t.string,
|
||||
transactionName: t.string,
|
||||
transactionType: t.string,
|
||||
scoring: scoringRt,
|
||||
}),
|
||||
t.type({
|
||||
durationPercentile: t.string,
|
||||
|
@ -39,7 +37,6 @@ export const correlationsForSlowTransactionsRoute = createRoute({
|
|||
transactionName,
|
||||
durationPercentile,
|
||||
fieldNames,
|
||||
scoring = 'percentage',
|
||||
} = context.params.query;
|
||||
|
||||
return getCorrelationsForSlowTransactions({
|
||||
|
@ -48,22 +45,19 @@ export const correlationsForSlowTransactionsRoute = createRoute({
|
|||
transactionName,
|
||||
durationPercentile: parseInt(durationPercentile, 10),
|
||||
fieldNames: fieldNames.split(','),
|
||||
scoring,
|
||||
setup,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const correlationsForRangesRoute = createRoute({
|
||||
endpoint: 'GET /api/apm/correlations/ranges',
|
||||
export const correlationsForFailedTransactionsRoute = createRoute({
|
||||
endpoint: 'GET /api/apm/correlations/failed_transactions',
|
||||
params: t.type({
|
||||
query: t.intersection([
|
||||
t.partial({
|
||||
serviceName: t.string,
|
||||
transactionName: t.string,
|
||||
transactionType: t.string,
|
||||
scoring: scoringRt,
|
||||
gap: t.string,
|
||||
}),
|
||||
t.type({
|
||||
fieldNames: t.string,
|
||||
|
@ -75,27 +69,18 @@ export const correlationsForRangesRoute = createRoute({
|
|||
options: { tags: ['access:apm'] },
|
||||
handler: async ({ context, request }) => {
|
||||
const setup = await setupRequest(context, request);
|
||||
|
||||
const {
|
||||
serviceName,
|
||||
transactionType,
|
||||
transactionName,
|
||||
scoring = 'percentage',
|
||||
gap,
|
||||
|
||||
fieldNames,
|
||||
} = context.params.query;
|
||||
|
||||
const gapBetweenRanges = parseInt(gap || '0', 10) * 3600 * 1000;
|
||||
if (gapBetweenRanges < 0) {
|
||||
throw new Error('gap must be 0 or positive');
|
||||
}
|
||||
|
||||
return getCorrelationsForRanges({
|
||||
return getCorrelationsForFailedTransactions({
|
||||
serviceName,
|
||||
transactionType,
|
||||
transactionName,
|
||||
scoring,
|
||||
gapBetweenRanges,
|
||||
fieldNames: fieldNames.split(','),
|
||||
setup,
|
||||
});
|
||||
|
|
|
@ -43,8 +43,8 @@ import { serviceNodesRoute } from './service_nodes';
|
|||
import { tracesRoute, tracesByIdRoute } from './traces';
|
||||
import { transactionByTraceIdRoute } from './transaction';
|
||||
import {
|
||||
correlationsForRangesRoute,
|
||||
correlationsForSlowTransactionsRoute,
|
||||
correlationsForFailedTransactionsRoute,
|
||||
} from './correlations';
|
||||
import {
|
||||
transactionGroupsBreakdownRoute,
|
||||
|
@ -129,7 +129,7 @@ const createApmApi = () => {
|
|||
|
||||
// Correlations
|
||||
.add(correlationsForSlowTransactionsRoute)
|
||||
.add(correlationsForRangesRoute)
|
||||
.add(correlationsForFailedTransactionsRoute)
|
||||
|
||||
// APM indices
|
||||
.add(apmIndexSettingsRoute)
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { format } from 'url';
|
||||
import { PromiseReturnType } from '../../../../../plugins/observability/typings/common';
|
||||
import archives_metadata from '../../../common/archives_metadata';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const archiveName = 'apm_8.0.0';
|
||||
const range = archives_metadata[archiveName];
|
||||
|
||||
// url parameters
|
||||
const start = '2020-09-29T14:45:00.000Z';
|
||||
const end = range.end;
|
||||
const fieldNames =
|
||||
'user.username,user.id,host.ip,user_agent.name,kubernetes.pod.uuid,url.domain,container.id,service.node.name';
|
||||
|
||||
describe('Ranges', () => {
|
||||
const url = format({
|
||||
pathname: `/api/apm/correlations/ranges`,
|
||||
query: { start, end, fieldNames },
|
||||
});
|
||||
|
||||
describe('when data is not loaded ', () => {
|
||||
it('handles the empty state', async () => {
|
||||
const response = await supertest.get(url);
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.response).to.be(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when data is loaded', () => {
|
||||
let response: PromiseReturnType<typeof supertest.get>;
|
||||
before(async () => {
|
||||
await esArchiver.load(archiveName);
|
||||
response = await supertest.get(url);
|
||||
});
|
||||
|
||||
after(() => esArchiver.unload(archiveName));
|
||||
|
||||
it('returns successfully', () => {
|
||||
expect(response.status).to.eql(200);
|
||||
});
|
||||
|
||||
it('returns fields in response', () => {
|
||||
expectSnapshot(Object.keys(response.body.response)).toMatchInline(`
|
||||
Array [
|
||||
"service.node.name",
|
||||
"host.ip",
|
||||
"user.id",
|
||||
"user_agent.name",
|
||||
"container.id",
|
||||
"url.domain",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns cardinality for each field', () => {
|
||||
const cardinalitys = Object.values(response.body.response).map(
|
||||
(field: any) => field.cardinality
|
||||
);
|
||||
|
||||
expectSnapshot(cardinalitys).toMatchInline(`
|
||||
Array [
|
||||
5,
|
||||
6,
|
||||
20,
|
||||
6,
|
||||
5,
|
||||
4,
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns buckets', () => {
|
||||
const { buckets } = response.body.response['user.id'].value;
|
||||
expectSnapshot(buckets[0]).toMatchInline(`
|
||||
Object {
|
||||
"bg_count": 2,
|
||||
"doc_count": 7,
|
||||
"key": "20",
|
||||
"score": 3.5,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,115 +0,0 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { format } from 'url';
|
||||
import { PromiseReturnType } from '../../../../../plugins/observability/typings/common';
|
||||
import archives_metadata from '../../../common/archives_metadata';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const archiveName = 'apm_8.0.0';
|
||||
const range = archives_metadata[archiveName];
|
||||
|
||||
// url parameters
|
||||
const start = range.start;
|
||||
const end = range.end;
|
||||
const durationPercentile = 95;
|
||||
const fieldNames =
|
||||
'user.username,user.id,host.ip,user_agent.name,kubernetes.pod.uuid,url.domain,container.id,service.node.name';
|
||||
|
||||
// Failing: See https://github.com/elastic/kibana/issues/81264
|
||||
describe('Slow durations', () => {
|
||||
const url = format({
|
||||
pathname: `/api/apm/correlations/slow_durations`,
|
||||
query: { start, end, durationPercentile, fieldNames },
|
||||
});
|
||||
|
||||
describe('when data is not loaded ', () => {
|
||||
it('handles the empty state', async () => {
|
||||
const response = await supertest.get(url);
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.response).to.be(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when data is loaded', () => {
|
||||
before(() => esArchiver.load(archiveName));
|
||||
after(() => esArchiver.unload(archiveName));
|
||||
|
||||
describe('making request with default args', () => {
|
||||
let response: PromiseReturnType<typeof supertest.get>;
|
||||
before(async () => {
|
||||
response = await supertest.get(url);
|
||||
|
||||
it('returns successfully', () => {
|
||||
expect(response.status).to.eql(200);
|
||||
});
|
||||
|
||||
it('returns fields in response', () => {
|
||||
expectSnapshot(Object.keys(response.body.response)).toMatchInline(`
|
||||
Array [
|
||||
"service.node.name",
|
||||
"host.ip",
|
||||
"user.id",
|
||||
"user_agent.name",
|
||||
"container.id",
|
||||
"url.domain",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns cardinality for each field', () => {
|
||||
const cardinalitys = Object.values(response.body.response).map(
|
||||
(field: any) => field.cardinality
|
||||
);
|
||||
|
||||
expectSnapshot(cardinalitys).toMatchInline(`
|
||||
Array [
|
||||
5,
|
||||
6,
|
||||
3,
|
||||
5,
|
||||
5,
|
||||
4,
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns buckets', () => {
|
||||
const { buckets } = response.body.response['user.id'].value;
|
||||
expectSnapshot(buckets[0]).toMatchInline(`
|
||||
Object {
|
||||
"bg_count": 32,
|
||||
"doc_count": 6,
|
||||
"key": "2",
|
||||
"score": 0.1875,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('making a request for each "scoring"', () => {
|
||||
['percentage', 'jlh', 'chi_square', 'gnd'].map(async (scoring) => {
|
||||
it(`returns response for scoring "${scoring}"`, async () => {
|
||||
const response = await supertest.get(
|
||||
format({
|
||||
pathname: `/api/apm/correlations/slow_durations`,
|
||||
query: { start, end, durationPercentile, fieldNames, scoring },
|
||||
})
|
||||
);
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { format } from 'url';
|
||||
import { APIReturnType } from '../../../../../plugins/apm/public/services/rest/createCallApmApi';
|
||||
import archives_metadata from '../../../common/archives_metadata';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const archiveName = 'apm_8.0.0';
|
||||
const range = archives_metadata[archiveName];
|
||||
|
||||
describe('Slow durations', () => {
|
||||
const url = format({
|
||||
pathname: `/api/apm/correlations/slow_transactions`,
|
||||
query: {
|
||||
start: range.start,
|
||||
end: range.end,
|
||||
durationPercentile: 95,
|
||||
fieldNames:
|
||||
'user.username,user.id,host.ip,user_agent.name,kubernetes.pod.uuid,url.domain,container.id,service.node.name',
|
||||
},
|
||||
});
|
||||
|
||||
describe('when data is not loaded', () => {
|
||||
it('handles the empty state', async () => {
|
||||
const response = await supertest.get(url);
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body.response).to.be(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when data is loaded', () => {
|
||||
before(() => esArchiver.load(archiveName));
|
||||
after(() => esArchiver.unload(archiveName));
|
||||
|
||||
describe('making request with default args', () => {
|
||||
type ResponseBody = APIReturnType<'GET /api/apm/correlations/slow_transactions'>;
|
||||
let response: {
|
||||
status: number;
|
||||
body: NonNullable<ResponseBody>;
|
||||
};
|
||||
|
||||
before(async () => {
|
||||
response = await supertest.get(url);
|
||||
});
|
||||
|
||||
it('returns successfully', () => {
|
||||
expect(response.status).to.eql(200);
|
||||
});
|
||||
|
||||
it('returns significant terms', () => {
|
||||
expectSnapshot(response.body?.significantTerms?.map((term) => term.fieldName))
|
||||
.toMatchInline(`
|
||||
Array [
|
||||
"host.ip",
|
||||
"service.node.name",
|
||||
"container.id",
|
||||
"url.domain",
|
||||
"user_agent.name",
|
||||
"user.id",
|
||||
"host.ip",
|
||||
"service.node.name",
|
||||
"container.id",
|
||||
"user.id",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns a timeseries per term', () => {
|
||||
// @ts-ignore
|
||||
expectSnapshot(response.body?.significantTerms[0].timeseries.length).toMatchInline(`31`);
|
||||
});
|
||||
|
||||
it('returns a distribution per term', () => {
|
||||
// @ts-ignore
|
||||
expectSnapshot(response.body?.significantTerms[0].distribution.length).toMatchInline(
|
||||
`11`
|
||||
);
|
||||
});
|
||||
|
||||
it('returns overall timeseries', () => {
|
||||
// @ts-ignore
|
||||
expectSnapshot(response.body?.overall.timeseries.length).toMatchInline(`31`);
|
||||
});
|
||||
|
||||
it('returns overall distribution', () => {
|
||||
// @ts-ignore
|
||||
expectSnapshot(response.body?.overall.distribution.length).toMatchInline(`11`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -59,8 +59,7 @@ export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderCont
|
|||
});
|
||||
|
||||
describe('Correlations', function () {
|
||||
loadTestFile(require.resolve('./correlations/slow_durations'));
|
||||
loadTestFile(require.resolve('./correlations/ranges'));
|
||||
loadTestFile(require.resolve('./correlations/slow_transactions'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -99,9 +99,8 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
}
|
||||
`);
|
||||
|
||||
expectSnapshot(
|
||||
firstItem.occurrences.timeseries.filter(({ y }: any) => y > 0).length
|
||||
).toMatchInline(`7`);
|
||||
const visibleDataPoints = firstItem.occurrences.timeseries.filter(({ y }: any) => y > 0);
|
||||
expectSnapshot(visibleDataPoints.length).toMatchInline(`7`);
|
||||
});
|
||||
|
||||
it('sorts items in the correct order', async () => {
|
||||
|
|
|
@ -354,6 +354,7 @@ interface AggregationResponsePart<TAggregationOptionsMap extends AggregationOpti
|
|||
bg_count: number;
|
||||
buckets: Array<
|
||||
{
|
||||
score: number;
|
||||
bg_count: number;
|
||||
doc_count: number;
|
||||
key: string | number;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue