Skip to content

Commit

Permalink
Use heatmap in trace details (kiali#2016)
Browse files Browse the repository at this point in the history
* Use heatmap in trace details

Fixes kiali/kiali#3279

- Reduxify stats metrics for comparison with traces
Same data will be use in several places, so redux is a convenient
solution here

- Refactor heatmap to use css grid layout instead of flex (flex doesn't
  work so well for two-axis grids). Heatmap style & structure is well
simplified

- Rename SpanItemData -> RichSpanData and make it computed directly in
  transformTraceData (sooner as before)

- Create one heatmap from Metrics Stats as an aggregation of all
  span-based stats...

- ...and another heatmap to replace the textual information we had,
  comparing trace stats with other traces displayed on screen

* Create a heatmap of similar traces

- Replace old list of similar with a heatmap. Merge the small 2x2 within
  that new one
- Add a tooltip giving some explanations about the similarity algo

* Add a couple of tests on trace stats

Also move some code so that TraceStats (from utils) doesn't depend on
StatsComparison (from components)

* Responsive design for heatmaps

Width is now capped with previously used width, but cells are auto-shrinked
for smaller width
  • Loading branch information
jotak authored Dec 3, 2020
1 parent 4483879 commit b6d36ac
Show file tree
Hide file tree
Showing 24 changed files with 745 additions and 470 deletions.
2 changes: 2 additions & 0 deletions src/actions/ActionKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export enum ActionKeys {
MC_HIDE_NOTIFICATION = 'MC_HIDE_NOTIFICATION',
MC_EXPAND_GROUP = 'MC_EXPAND_GROUP',

METRICS_STATS_SET = 'METRICS_STATS_SET',

NAMESPACE_REQUEST_STARTED = 'NAMESPACE_REQUEST_STARTED',
NAMESPACE_SUCCESS = 'NAMESPACE_SUCCESS',
NAMESPACE_FAILED = 'NAMESPACE_FAILED',
Expand Down
4 changes: 3 additions & 1 deletion src/actions/KialiAppAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { JaegerAction } from './JaegerActions';
import { MeshTlsAction } from './MeshTlsActions';
import { TourAction } from './TourActions';
import { IstioStatusAction } from './IstioStatusActions';
import { MetricsStatsAction } from './MetricsStatsActions';

export type KialiAppAction =
| GlobalAction
Expand All @@ -23,4 +24,5 @@ export type KialiAppAction =
| JaegerAction
| MeshTlsAction
| IstioStatusAction
| TourAction;
| TourAction
| MetricsStatsAction;
11 changes: 11 additions & 0 deletions src/actions/MetricsStatsActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ActionType, createAction } from 'typesafe-actions';
import { ActionKeys } from './ActionKeys';
import { MetricsStats } from 'types/Metrics';

export const MetricsStatsActions = {
setStats: createAction(ActionKeys.METRICS_STATS_SET, resolve => (stats: Map<string, MetricsStats>) =>
resolve({ metricsStats: stats })
)
};

export type MetricsStatsAction = ActionType<typeof MetricsStatsActions>;
39 changes: 39 additions & 0 deletions src/actions/MetricsStatsThunkActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { ThunkDispatch } from 'redux-thunk';
import { KialiAppState } from '../store/Store';
import { KialiAppAction } from './KialiAppAction';
import * as API from '../services/Api';
import { MetricsStatsActions } from './MetricsStatsActions';
import { MetricsStatsQuery, statsQueryToKey } from 'types/MetricsOptions';
import { addError, addInfo } from 'utils/AlertUtils';
import { MetricsStats } from 'types/Metrics';

type ExpiringStats = MetricsStats & { timestamp: number };

const expiry = 2 * 60 * 1000;
const MetricsStatsThunkActions = {
load: (queries: MetricsStatsQuery[]) => {
return (dispatch: ThunkDispatch<KialiAppState, void, KialiAppAction>, getState: () => KialiAppState) => {
const oldStats = getState().metricsStats.data as Map<string, ExpiringStats>;
const now = Date.now();
// Keep only queries for stats we don't already have
const newStats = new Map(Array.from(oldStats).filter(([_, v]) => now - v.timestamp < expiry));
const filtered = queries.filter(q => !newStats.has(statsQueryToKey(q)));
if (filtered.length > 0) {
API.getMetricsStats(filtered)
.then(res => {
// Merge result
Object.entries(res.data.stats).forEach(e => newStats.set(e[0], { ...e[1], timestamp: now }));
dispatch(MetricsStatsActions.setStats(newStats));
if (res.data.warnings && res.data.warnings.length > 0) {
addInfo(res.data.warnings.join('; '), false);
}
})
.catch(err => {
addError('Could not fetch metrics stats.', err);
});
}
};
}
};

export default MetricsStatsThunkActions;
9 changes: 8 additions & 1 deletion src/components/Filters/StatefulFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,14 @@ export class StatefulFilters extends React.Component<StatefulFiltersProps, State
}
});

this.promises.registerAll('filterType', filterTypePromises).then(types => this.setState({ filterTypes: types }));
this.promises
.registerAll('filterType', filterTypePromises)
.then(types => this.setState({ filterTypes: types }))
.catch(err => {
if (!err.isCanceled) {
console.debug(err);
}
});
}

componentDidUpdate(prev: StatefulFiltersProps) {
Expand Down
153 changes: 53 additions & 100 deletions src/components/HeatMap/HeatMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,74 +10,24 @@ export type Color = { r: number; g: number; b: number };
export type ColorMap = Color[];

type Props = {
xLabels: string[];
yLabels: string[];
xLabels: (string | JSX.Element)[];
yLabels: (string | JSX.Element)[];
data: (number | undefined)[][];
colorMap: ColorMap;
dataRange: { from: number; to: number };
colorUndefined: string;
valueFormat: (v: number) => string;
tooltip: (x: number, y: number, v: number) => string;
compactMode?: boolean;
displayMode?: 'compact' | 'normal' | 'large';
};

const cellHeight = '2rem';
const compactCellHeight = '1rem';

const rowStyle = style({
display: 'flex',
flexDirection: 'row'
});

const columnStyle = style({
display: 'flex',
flexDirection: 'column'
});

const yLabelStyle = style({
boxSizing: 'border-box',
padding: '0 0.2rem',
lineHeight: cellHeight,
whiteSpace: 'nowrap'
});

const xLabelRowStyle = style({
display: 'flex',
textAlign: 'center'
});

const xLabelStyle = style({
padding: '0.2rem 0',
boxSizing: 'border-box',
overflow: 'hidden',
flexShrink: 1,
flexBasis: cellHeight,
width: cellHeight
});

const largeCellStyle = style({
textAlign: 'center',
const cellStyle = style({
overflow: 'hidden',
boxSizing: 'border-box',
flexBasis: cellHeight,
flexShrink: 0,
height: cellHeight,
lineHeight: cellHeight,
fontSize: '.7rem',
borderRadius: 3,
margin: 1
});

const compactCellStyle = style({
textAlign: 'center',
overflow: 'hidden',
boxSizing: 'border-box',
flexBasis: compactCellHeight,
flexShrink: 0,
height: compactCellHeight,
lineHeight: compactCellHeight,
borderRadius: 3,
margin: 1
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
});

export class HeatMap extends React.Component<Props> {
Expand All @@ -89,6 +39,18 @@ export class HeatMap extends React.Component<Props> {
{ r: 201, g: 25, b: 11 } // PF Danger 100 (#c9190b)
];

private getGridStyle = (): React.CSSProperties => {
const cellHeight = this.props.displayMode === 'compact' ? '1rem' : '2rem';
const cellWidth = this.props.displayMode === 'compact' ? 1 : this.props.displayMode === 'large' ? 3 : 2;
return {
display: 'grid',
gridTemplateColumns: `${cellWidth}rem repeat(${this.props.xLabels.length}, 1fr)`,
gridTemplateRows: new Array(this.props.yLabels.length + 1).fill(cellHeight).join(' '),
gridGap: 2,
maxWidth: `${cellWidth * (1 + this.props.xLabels.length)}rem`
};
};

private getCellColors = (value: number) => {
const { from, to } = this.props.dataRange;
const clamped = Math.max(from, Math.min(to, value));
Expand All @@ -109,58 +71,49 @@ export class HeatMap extends React.Component<Props> {
};

render() {
const cellStyle = this.props.compactMode ? compactCellStyle : largeCellStyle;
const isCompact = this.props.displayMode === 'compact';
return (
<div className={rowStyle}>
<div className={columnStyle} style={{ marginTop: cellHeight }}>
{!this.props.compactMode &&
this.props.yLabels.map(label => (
<div key={label} className={yLabelStyle}>
{label}
</div>
))}
</div>
<div className={columnStyle}>
<div className={xLabelRowStyle}>
{!this.props.compactMode &&
this.props.xLabels.map(label => (
<div key={label} className={xLabelStyle}>
{label}
</div>
))}
<div style={this.getGridStyle()}>
<div></div>
{this.props.xLabels.map((xLabel, x) => (
<div key={'xlabel_' + x} className={cellStyle}>
{isCompact ? '' : xLabel}
</div>
<div className={columnStyle}>
{this.props.yLabels.map((_, y) => (
<div key={`heatmap_${y}`} className={rowStyle}>
{this.props.xLabels.map((_, x) => {
const value = this.props.data[x][y];
if (value) {
const style = this.getCellColors(value);
return (
<div
key={`heatmap_${x}-${y}`}
className={cellStyle}
style={style}
title={this.props.tooltip(x, y, value)}
>
{!this.props.compactMode && this.props.valueFormat(value)}
</div>
);
}
))}
{this.props.yLabels.map((yLabel, y) => {
return (
<>
<div key={'ylabel_' + y} className={cellStyle}>
{isCompact ? '' : yLabel}
</div>
{this.props.xLabels.map((_, x) => {
const value = this.props.data[x][y];
if (value) {
const style = this.getCellColors(value);
return (
<div
key={`heatmap_${x}-${y}`}
className={cellStyle}
style={{ backgroundColor: this.props.colorUndefined }}
style={style}
title={this.props.tooltip(x, y, value)}
>
{!this.props.compactMode && 'n/a'}
{!isCompact && this.props.valueFormat(value)}
</div>
);
})}
</div>
))}
</div>
</div>
}
return (
<div
key={`heatmap_${x}-${y}`}
className={cellStyle}
style={{ backgroundColor: this.props.colorUndefined }}
>
{!isCompact && 'n/a'}
</div>
);
})}
</>
);
})}
</div>
);
}
Expand Down
38 changes: 12 additions & 26 deletions src/components/JaegerIntegration/JaegerHelper.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import logfmtParser from 'logfmt/lib/logfmt_parser';
import { KeyValuePair, Span } from '../../types/JaegerInfo';
import {
EnvoySpanInfo,
KeyValuePair,
OpenTracingBaseInfo,
OpenTracingHTTPInfo,
OpenTracingTCPInfo,
Span
} from '../../types/JaegerInfo';
import { retrieveTimeRange } from 'components/Time/TimeRangeHelper';
import { guardTimeRange, durationToBounds } from 'types/Common';
import { Target } from 'types/MetricsOptions';
import { defaultMetricsDuration } from '../Metrics/Helper';

export const buildTags = (showErrors: boolean, statusCode: string): string => {
Expand Down Expand Up @@ -150,18 +156,6 @@ export const getSpanType = (span: Span): 'envoy' | 'http' | 'tcp' | 'unknown' =>
return 'unknown';
};

export type OpenTracingBaseInfo = {
component?: string;
hasError: boolean;
};

export type OpenTracingHTTPInfo = OpenTracingBaseInfo & {
statusCode?: number;
url?: string;
method?: string;
direction?: 'inbound' | 'outbound';
};

export const extractOpenTracingBaseInfo = (span: Span): OpenTracingBaseInfo => {
const info: OpenTracingBaseInfo = { hasError: false };
span.tags.forEach(t => {
Expand Down Expand Up @@ -199,13 +193,6 @@ export const extractOpenTracingHTTPInfo = (span: Span): OpenTracingHTTPInfo => {
return info;
};

export type OpenTracingTCPInfo = OpenTracingBaseInfo & {
topic?: string;
peerAddress?: string;
peerHostname?: string;
direction?: 'inbound' | 'outbound';
};

export const extractOpenTracingTCPInfo = (span: Span): OpenTracingTCPInfo => {
// See https://github.com/opentracing/specification/blob/master/semantic_conventions.md
const info: OpenTracingTCPInfo = extractOpenTracingBaseInfo(span);
Expand All @@ -227,11 +214,6 @@ export const extractOpenTracingTCPInfo = (span: Span): OpenTracingTCPInfo => {
return info;
};

export type EnvoySpanInfo = OpenTracingHTTPInfo & {
responseFlags?: string;
peer?: Target;
};

export const extractEnvoySpanInfo = (span: Span): EnvoySpanInfo => {
const info: EnvoySpanInfo = extractOpenTracingHTTPInfo(span);
span.tags.forEach(t => {
Expand Down Expand Up @@ -281,3 +263,7 @@ export const extractSpanInfo = (span: Span) => {
: extractOpenTracingBaseInfo(span);
return { type: type, info: info };
};

export const sameSpans = (a: Span[], b: Span[]): boolean => {
return a.map(s => s.spanID).join() === b.map(s => s.spanID).join();
};
Loading

0 comments on commit b6d36ac

Please sign in to comment.