Skip to content

Commit

Permalink
[Infra UI] Add APM Section to Node Detail Page
Browse files Browse the repository at this point in the history
- Closes elastic#42170
- Adds useApmMetrics hook
- Adds APM layout
- Adds APM section to node detail page
  • Loading branch information
simianhacker committed Aug 7, 2019
1 parent b57bee7 commit c7c54e4
Show file tree
Hide file tree
Showing 35 changed files with 834 additions and 157 deletions.
1 change: 1 addition & 0 deletions x-pack/legacy/plugins/infra/common/graphql/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,7 @@ export enum InfraMetric {
nginxRequestRate = 'nginxRequestRate',
nginxActiveConnections = 'nginxActiveConnections',
nginxRequestsPerConnection = 'nginxRequestsPerConnection',
apmMetrics = 'apmMetrics',
custom = 'custom',
}

Expand Down
33 changes: 26 additions & 7 deletions x-pack/legacy/plugins/infra/common/http_api/apm_metrics_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,38 @@ export const InfraApmMetricsRequestRT = rt.type({
sourceId: rt.string,
});

export const InfraApmMetricsTransactionTypeRT = rt.keyof({
request: null,
job: null,
});

export const InfraApmMetricsDataPointRT = rt.type({
timestamp: rt.number,
value: rt.union([rt.number, rt.null]),
});

export const InfraApmMetricsDataBucketRT = rt.type({
name: rt.string,
export const InfraApmMetricsSeriesRT = rt.type({
id: rt.string,
data: rt.array(InfraApmMetricsDataPointRT),
});

export const InfraApmMetricsDataSetRT = rt.type({
id: rt.string,
type: InfraApmMetricsTransactionTypeRT,
series: rt.array(InfraApmMetricsSeriesRT),
});

export const InfraApmMetricsServiceRT = rt.type({
name: rt.string,
transactionsPerMinute: rt.array(InfraApmMetricsDataBucketRT),
responseTimes: rt.array(InfraApmMetricsDataBucketRT),
id: rt.string,
dataSets: rt.array(InfraApmMetricsDataSetRT),
agentName: rt.string,
avgResponseTime: rt.number,
errorsPerMinute: rt.number,
transactionsPerMinute: rt.number,
});

export const InfraApmMetricsRT = rt.type({
id: rt.literal('apmMetrics'),
services: rt.array(InfraApmMetricsServiceRT),
});

Expand All @@ -58,7 +73,7 @@ export const APMChartResponseRT = rt.type({
tpmBuckets: rt.array(APMTpmBucketsRT),
}),
rt.partial({
overallAvgDuration: rt.number,
overallAvgDuration: rt.union([rt.number, rt.null]),
}),
]),
});
Expand Down Expand Up @@ -86,10 +101,14 @@ export type InfraApmMetrics = rt.TypeOf<typeof InfraApmMetricsRT>;

export type InfraApmMetricsService = rt.TypeOf<typeof InfraApmMetricsServiceRT>;

export type InfraApmMetricsDataBucket = rt.TypeOf<typeof InfraApmMetricsDataBucketRT>;
export type InfraApmMetricsSeries = rt.TypeOf<typeof InfraApmMetricsSeriesRT>;

export type InfraApmMetricsDataPoint = rt.TypeOf<typeof InfraApmMetricsDataPointRT>;

export type InfraApmMetricsTransactionType = rt.TypeOf<typeof InfraApmMetricsTransactionTypeRT>;

export type InfraApmMetricsDataSet = rt.TypeOf<typeof InfraApmMetricsDataSetRT>;

export type APMDataPoint = rt.TypeOf<typeof APMDataPointRT>;

export type APMTpmBuckets = rt.TypeOf<typeof APMTpmBucketsRT>;
Expand Down
1 change: 0 additions & 1 deletion x-pack/legacy/plugins/infra/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
"build-graphql-types": "node scripts/generate_types_from_graphql.js"
},
"devDependencies": {
"@types/boom": "7.2.1",
"@types/lodash": "^4.14.110"
},
"dependencies": {
Expand Down
29 changes: 15 additions & 14 deletions x-pack/legacy/plugins/infra/public/components/metrics/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,31 @@
*/

import { EuiPageContentBody, EuiTitle } from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';

import { InfraMetricData, InfraTimerangeInput } from '../../graphql/types';
import { InfraTimerangeInput, InfraNodeType } from '../../graphql/types';
import { InfraMetricLayout, InfraMetricLayoutSection } from '../../pages/metrics/layouts/types';
import { NoData } from '../empty_states';
import { InfraLoadingPanel } from '../loading';
import { Section } from './section';
import { InfraMetricCombinedData } from '../../containers/metrics/with_metrics';
import { SourceConfiguration } from '../../utils/source_configuration';

interface Props {
metrics: InfraMetricData[];
metrics: InfraMetricCombinedData[];
layouts: InfraMetricLayout[];
loading: boolean;
refetch: () => void;
nodeId: string;
label: string;
onChangeRangeTime?: (time: InfraTimerangeInput) => void;
isLiveStreaming?: boolean;
stopLiveStreaming?: () => void;
intl: InjectedIntl;
nodeId: string;
nodeType: InfraNodeType;
sourceConfiguration: SourceConfiguration;
timeRange: InfraTimerangeInput;
}

interface State {
Expand Down Expand Up @@ -85,15 +90,7 @@ export const Metrics = injectI18n(
<React.Fragment key={layout.id}>
<EuiPageContentBody>
<EuiTitle size="m">
<h2 id={layout.id}>
<FormattedMessage
id="xpack.infra.metrics.layoutLabelOverviewTitle"
defaultMessage="{layoutLabel} Overview"
values={{
layoutLabel: layout.label,
}}
/>
</h2>
<h2 id={layout.id}>{layout.label}</h2>
</EuiTitle>
</EuiPageContentBody>
{layout.sections.map(this.renderSection(layout))}
Expand All @@ -103,7 +100,7 @@ export const Metrics = injectI18n(

private renderSection = (layout: InfraMetricLayout) => (section: InfraMetricLayoutSection) => {
let sectionProps = {};
if (section.type === 'chart') {
if (['apm', 'chart'].includes(section.type)) {
const { onChangeRangeTime, isLiveStreaming, stopLiveStreaming } = this.props;
sectionProps = {
onChangeRangeTime,
Expand All @@ -118,6 +115,10 @@ export const Metrics = injectI18n(
section={section}
metrics={this.props.metrics}
key={`${layout.id}-${section.id}`}
nodeId={this.props.nodeId}
nodeType={this.props.nodeType}
sourceConfiguration={this.props.sourceConfiguration}
timeRange={this.props.timeRange}
{...sectionProps}
/>
);
Expand Down
24 changes: 20 additions & 4 deletions x-pack/legacy/plugins/infra/public/components/metrics/section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,24 @@
*/

import React from 'react';
import { InfraMetricData, InfraTimerangeInput } from '../../graphql/types';
import { InfraTimerangeInput, InfraNodeType } from '../../graphql/types';
import { InfraMetricLayoutSection } from '../../pages/metrics/layouts/types';
import { sections } from './sections';
import { InfraMetricCombinedData } from '../../containers/metrics/with_metrics';
import { SourceConfiguration } from '../../utils/source_configuration';

interface Props {
section: InfraMetricLayoutSection;
metrics: InfraMetricData[];
metrics: InfraMetricCombinedData[];
onChangeRangeTime?: (time: InfraTimerangeInput) => void;
crosshairValue?: number;
onCrosshairUpdate?: (crosshairValue: number) => void;
isLiveStreaming?: boolean;
stopLiveStreaming?: () => void;
nodeId: string;
nodeType: InfraNodeType;
sourceConfiguration: SourceConfiguration;
timeRange: InfraTimerangeInput;
}

export class Section extends React.PureComponent<Props> {
Expand All @@ -26,7 +32,7 @@ export class Section extends React.PureComponent<Props> {
return null;
}
let sectionProps = {};
if (this.props.section.type === 'chart') {
if (['apm', 'chart'].includes(this.props.section.type)) {
sectionProps = {
onChangeRangeTime: this.props.onChangeRangeTime,
crosshairValue: this.props.crosshairValue,
Expand All @@ -36,6 +42,16 @@ export class Section extends React.PureComponent<Props> {
};
}
const Component = sections[this.props.section.type];
return <Component section={this.props.section} metric={metric} {...sectionProps} />;
return (
<Component
nodeId={this.props.nodeId}
nodeType={this.props.nodeType}
sourceConfiguration={this.props.sourceConfiguration}
timeRange={this.props.timeRange}
section={this.props.section}
metric={metric}
{...sectionProps}
/>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* 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 {
Axis,
Chart,
getAxisId,
niceTimeFormatter,
Position,
Settings,
TooltipValue,
} from '@elastic/charts';
import moment from 'moment';
import React, { useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { useKibanaUiSetting } from '../../../utils/use_kibana_ui_setting';
import { getChartTheme } from '../../metrics_explorer/helpers/get_chart_theme';
import { SeriesChart } from './series_chart';
import { getFormatter, getMaxMinTimestamp, seriesHasLessThen2DataPoints } from './helpers';
import { InfraApmMetricsDataSet } from '../../../../common/http_api';
import { InfraFormatterType } from '../../../lib/lib';
import {
InfraMetricLayoutSection,
InfraMetricLayoutVisualizationType,
} from '../../../pages/metrics/layouts/types';
import { ErrorMessage } from './error_message';
import { InfraTimerangeInput } from '../../../graphql/types';

interface Props {
section: InfraMetricLayoutSection;
dataSet?: InfraApmMetricsDataSet | undefined;
formatterTemplate?: string;
transformDataPoint?: (value: number) => number;
onChangeRangeTime?: (time: InfraTimerangeInput) => void;
isLiveStreaming?: boolean;
stopLiveStreaming?: () => void;
}

export const ApmChart = ({
dataSet,
section,
formatterTemplate,
transformDataPoint = noopTransformer,
onChangeRangeTime,
isLiveStreaming,
stopLiveStreaming,
}: Props) => {
if (!dataSet) {
return (
<ErrorMessage
title={i18n.translate('xpack.infra.chartSection.missingMetricDataText', {
defaultMessage: 'Missing Data',
})}
body={i18n.translate('xpack.infra.chartSection.missingMetricDataBody', {
defaultMessage: 'The data for this chart is missing.',
})}
/>
);
}
if (dataSet.series.some(seriesHasLessThen2DataPoints)) {
return (
<ErrorMessage
title={i18n.translate('xpack.infra.chartSection.notEnoughDataPointsToRenderTitle', {
defaultMessage: 'Not Enough Data',
})}
body={i18n.translate('xpack.infra.chartSection.notEnoughDataPointsToRenderText', {
defaultMessage: 'Not enough data points to render chart, try increasing the time range.',
})}
/>
);
}

const [dateFormat] = useKibanaUiSetting('dateFormat');
const tooltipProps = {
headerFormatter: useCallback(
(data: TooltipValue) => moment(data.value).format(dateFormat || 'Y-MM-DD HH:mm:ss.SSS'),
[dateFormat]
),
};
const valueFormatter = getFormatter(InfraFormatterType.number, formatterTemplate || '{{value}}');
const valueFormatterWithTransorm = useCallback(
(value: number) => valueFormatter(transformDataPoint(value)),
[formatterTemplate, transformDataPoint]
);
const dateFormatter = useCallback(niceTimeFormatter(getMaxMinTimestamp(dataSet)), [dataSet]);
const handleTimeChange = useCallback(
(from: number, to: number) => {
if (onChangeRangeTime) {
if (isLiveStreaming && stopLiveStreaming) {
stopLiveStreaming();
}
onChangeRangeTime({
from,
to,
interval: '>=1m',
});
}
},
[onChangeRangeTime, isLiveStreaming, stopLiveStreaming]
);

return (
<Chart>
<Axis
id={getAxisId('timestamp')}
position={Position.Bottom}
showOverlappingTicks={true}
tickFormat={dateFormatter}
/>
<Axis
id={getAxisId('values')}
position={Position.Left}
tickFormat={valueFormatterWithTransorm}
/>
{dataSet &&
dataSet.series.map(series => (
<SeriesChart
key={`series-${section.id}-${series.id}`}
id={`series-${section.id}-${series.id}`}
series={series}
name={series.id}
type={InfraMetricLayoutVisualizationType.line}
ignoreGaps={true}
/>
))}
<Settings tooltip={tooltipProps} theme={getChartTheme()} onBrushEnd={handleTimeChange} />
</Chart>
);
};

const noopTransformer = (value: number) => value;
Loading

0 comments on commit c7c54e4

Please sign in to comment.