Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[APM] Correlations polish #85116

Merged
merged 8 commits into from
Dec 15, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ import {
} from '@elastic/charts';
import React, { useState } from 'react';
import { useParams } from 'react-router-dom';
import { EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import {
EuiTitle,
EuiFlexGroup,
EuiFlexItem,
EuiComboBox,
EuiAccordion,
} from '@elastic/eui';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
import {
Expand All @@ -35,12 +41,26 @@ type SignificantTerm = NonNullable<
CorrelationsApiResponse['significantTerms']
>[0];

const initialFieldNames = [
'transaction.name',
'user.username',
'user.id',
'host.ip',
'user_agent.name',
'kubernetes.pod.uuid',
'kubernetes.pod.name',
'url.domain',
'container.id',
'service.node.name',
].map((label) => ({ label }));

export function ErrorCorrelations() {
const [
selectedSignificantTerm,
setSelectedSignificantTerm,
] = useState<SignificantTerm | null>(null);

const [fieldNames, setFieldNames] = useState(initialFieldNames);
const { serviceName } = useParams<{ serviceName?: string }>();
const { urlParams, uiFilters } = useUrlParams();
const { transactionName, transactionType, start, end } = urlParams;
Expand All @@ -57,13 +77,20 @@ export function ErrorCorrelations() {
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',
fieldNames: fieldNames.map((field) => field.label).join(','),
},
},
});
}
}, [serviceName, start, end, transactionName, transactionType, uiFilters]);
}, [
serviceName,
start,
end,
transactionName,
transactionType,
uiFilters,
fieldNames,
]);

return (
<>
Expand All @@ -72,14 +99,30 @@ export function ErrorCorrelations() {
<EuiTitle size="s">
<h4>Error rate over time</h4>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem>
<ErrorTimeseriesChart
data={data}
status={status}
selectedSignificantTerm={selectedSignificantTerm}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiAccordion id="accordion" buttonContent="Customize">
<EuiComboBox
fullWidth={true}
placeholder="Select or create options"
selectedOptions={fieldNames}
onChange={setFieldNames}
onCreateOption={(term) =>
setFieldNames((names) => [...names, { label: term }])
}
/>
</EuiAccordion>
</EuiFlexItem>
<EuiFlexItem>
<SignificantTermsTable
cardinalityColumnName="# of failed transactions"
significantTerms={data?.significantTerms}
status={status}
setSelectedSignificantTerm={setSelectedSignificantTerm}
Expand All @@ -103,7 +146,7 @@ function ErrorTimeseriesChart({

return (
<ChartContainer height={200} hasData={!!data} status={status}>
<Chart size={{ height: px(200), width: px(600) }}>
<Chart size={{ height: px(200), width: '100%' }}>
<Settings showLegend legendPosition={Position.Bottom} />

<Axis
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,22 @@
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 {
EuiTitle,
EuiFlexGroup,
EuiFlexItem,
EuiComboBox,
EuiAccordion,
EuiFormRow,
EuiFieldNumber,
} from '@elastic/eui';
import { getDurationFormatter } from '../../../../common/utils/formatters';
import { useUrlParams } from '../../../context/url_params_context/use_url_params';
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
Expand All @@ -36,12 +41,26 @@ type SignificantTerm = NonNullable<
CorrelationsApiResponse['significantTerms']
>[0];

const initialFieldNames = [
'user.username',
'user.id',
'host.ip',
'user_agent.name',
'kubernetes.pod.uuid',
'kubernetes.pod.name',
'url.domain',
'container.id',
'service.node.name',
].map((label) => ({ label }));

export function LatencyCorrelations() {
const [
selectedSignificantTerm,
setSelectedSignificantTerm,
] = useState<SignificantTerm | null>(null);

const [fieldNames, setFieldNames] = useState(initialFieldNames);
const [durationPercentile, setDurationPercentile] = useState('50');
const { serviceName } = useParams<{ serviceName?: string }>();
const { urlParams, uiFilters } = useUrlParams();
const { transactionName, transactionType, start, end } = urlParams;
Expand All @@ -58,30 +77,28 @@ export function LatencyCorrelations() {
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',
durationPercentile,
fieldNames: fieldNames.map((field) => field.label).join(','),
},
},
});
}
}, [serviceName, start, end, transactionName, transactionType, uiFilters]);
}, [
serviceName,
start,
end,
transactionName,
transactionType,
uiFilters,
durationPercentile,
fieldNames,
]);

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>
Expand All @@ -94,8 +111,42 @@ export function LatencyCorrelations() {
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiAccordion id="accordion" buttonContent="Customize">
<EuiFlexGroup>
<EuiFlexItem grow={1}>
<EuiFormRow label="Threshold">
<EuiFieldNumber
value={durationPercentile}
onChange={(e) =>
setDurationPercentile(e.currentTarget.value)
}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={4}>
<EuiFormRow
fullWidth={true}
label="Field"
helpText="Fields to analyse for significant terms"
>
<EuiComboBox
fullWidth={true}
placeholder="Select or create options"
selectedOptions={fieldNames}
onChange={setFieldNames}
onCreateOption={(term) => {
setFieldNames((names) => [...names, { label: term }]);
}}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
</EuiAccordion>
</EuiFlexItem>
<EuiFlexItem>
<SignificantTermsTable
cardinalityColumnName="# of slow transactions"
Copy link
Contributor

Choose a reason for hiding this comment

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

Are we adding i18n at this stage?

Copy link
Member Author

Choose a reason for hiding this comment

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

I thought about it and opted not to. There's a good chance that all of the copy will be changed anyway (I made all of it up) - so translators will have to do this twice. Not sure what the official guidelines around experimental features are.

significantTerms={data?.significantTerms}
status={status}
setSelectedSignificantTerm={setSelectedSignificantTerm}
Expand All @@ -106,20 +157,6 @@ export function LatencyCorrelations() {
);
}

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;
Expand All @@ -134,65 +171,6 @@ function getDistributionYMax(data?: CorrelationsApiResponse) {
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,
Expand Down
Loading