mirror of https://github.com/linkerd/linkerd2.git
Update web component to use new stat api (#753)
* Update web component to use new stat api * Address review feedback * Add external link icon Signed-off-by: Kevin Lingerfelt <kl@buoyant.io>
This commit is contained in:
parent
e9b209829d
commit
37434d048a
|
@ -1,25 +0,0 @@
|
|||
@import 'styles.css';
|
||||
|
||||
.bar-chart {
|
||||
& .bar {
|
||||
fill: #2F80ED;
|
||||
}
|
||||
|
||||
& .x-axis path {
|
||||
stroke: var(--coldgrey);
|
||||
stroke-width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.pod-distribution-chart {
|
||||
font-size: 12px;
|
||||
|
||||
& .bar-chart-title {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
& .bar-chart-tooltip {
|
||||
min-height: 32px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
@import 'styles.css';
|
||||
|
||||
.deployment-detail {
|
||||
& .upstream-downstream-list {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
& .deployment-title {
|
||||
margin: 6px 0;
|
||||
|
||||
& h1 {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
& .unadded-message {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
& .status-badge {
|
||||
display: inline-block;
|
||||
border-radius: 50px;
|
||||
padding: 4px 10px;
|
||||
margin: 6px 12px;
|
||||
font-size: small;
|
||||
|
||||
& p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.unadded {
|
||||
background: var(--silver);
|
||||
color: white;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
& .deployment-details {
|
||||
&.border-container {
|
||||
height: 276px; /* override height */
|
||||
}
|
||||
& .border-container-content {
|
||||
height: 260px; /* override height */
|
||||
}
|
||||
|
||||
& .metric {
|
||||
padding: 15px 0;
|
||||
}
|
||||
& .metric:not(:last-child) {
|
||||
border-bottom: 1px solid grey;
|
||||
}
|
||||
}
|
||||
|
||||
& .border-container {
|
||||
height: 151px; /* override height */
|
||||
}
|
||||
|
||||
& .border-container-content {
|
||||
height: 135px; /* override height */
|
||||
|
||||
& .summary-container .metric-info {
|
||||
float: left;
|
||||
}
|
||||
|
||||
& .summary-container .metric-value {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
@import 'styles.css';
|
||||
|
||||
.entity-health {
|
||||
margin-top: 32px;
|
||||
margin-bottom: 32px;
|
||||
|
||||
& .metric-title {
|
||||
text-transform: uppercase;
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
& .float-right .metric-value {
|
||||
float: right;
|
||||
}
|
||||
|
||||
& .metric-value {
|
||||
font-weight: var(--font-weight-extra-bold);
|
||||
}
|
||||
|
||||
& .entity-title {
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
padding: 0 var(--base-width);
|
||||
|
||||
&.health-good {
|
||||
border: 2px solid var(--green);
|
||||
}
|
||||
&.health-bad {
|
||||
border: 2px solid var(--siennared);
|
||||
}
|
||||
&.health-neutral {
|
||||
border: 2px solid #108ee9;
|
||||
}
|
||||
&.health-unknown {
|
||||
border: 2px solid var(--silver);
|
||||
}
|
||||
}
|
||||
|
||||
& .adjacent-health {
|
||||
/* override ant progress bar styles */
|
||||
&.health-good {
|
||||
& .ant-progress-bg {
|
||||
background: var(--green);
|
||||
}
|
||||
}
|
||||
&.health-bad {
|
||||
& .ant-progress-bg {
|
||||
background: var(--siennared);
|
||||
}
|
||||
}
|
||||
&.health-neutral {
|
||||
& .ant-progress-bg {
|
||||
background: #108ee9;
|
||||
}
|
||||
}
|
||||
&.health-unknown {
|
||||
& .ant-progress-bg {
|
||||
background: var(--silver);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .entity-count {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
& .float-left {
|
||||
float: left;
|
||||
}
|
||||
& .float-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* override ant for our custom component */
|
||||
& .ant-progress.ant-progress-line {
|
||||
margin-top: 4px;
|
||||
}
|
||||
& .ant-progress-show-info .ant-progress-outer {
|
||||
margin-right: 0;
|
||||
padding-right: 0;
|
||||
|
||||
& .ant-progress-bg {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
@import 'styles.css';
|
||||
|
||||
.current-latency {
|
||||
& .latency-metric {
|
||||
float: left;
|
||||
width: 197px;
|
||||
height: 28px;
|
||||
padding-left: 8px;
|
||||
height: 100%;
|
||||
|
||||
& .latency-title {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
& .latency-value {
|
||||
font-size: 16px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
}
|
||||
|
||||
& .current-latency-P50 {
|
||||
border-left: 4px solid var(--latency-p50);;
|
||||
}
|
||||
& .current-latency-P95 {
|
||||
border-left: 4px solid var(--latency-p95);
|
||||
}
|
||||
& .current-latency-P99 {
|
||||
border-left: 4px solid var(--latency-p99);
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
@import 'styles.css';
|
||||
|
||||
.metric-summary {
|
||||
& .metric {
|
||||
padding-left: var(--base-width);
|
||||
border-left: 4px solid var(--royalblue);
|
||||
|
||||
&.metric-large {
|
||||
& .metric-title {
|
||||
font-size: 12px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
& .metric-value {
|
||||
font-size: 18px;
|
||||
font-weight: var(--font-weight-extra-bold);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="351px" height="365px" viewBox="0 0 351 365" style="enable-background:new 0 0 351 365;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:url(#SVGID_1_);}
|
||||
</style>
|
||||
<g id="Layer_1_1_">
|
||||
</g>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="175.5" y1="445.4948" x2="175.5" y2="114.0346">
|
||||
<stop offset="0" style="stop-color:#FFF100"/>
|
||||
<stop offset="1" style="stop-color:#F05A28"/>
|
||||
</linearGradient>
|
||||
<path class="st0" d="M342,161.2c-0.6-6.1-1.6-13.1-3.6-20.9c-2-7.7-5-16.2-9.4-25c-4.4-8.8-10.1-17.9-17.5-26.8
|
||||
c-2.9-3.5-6.1-6.9-9.5-10.2c5.1-20.3-6.2-37.9-6.2-37.9c-19.5-1.2-31.9,6.1-36.5,9.4c-0.8-0.3-1.5-0.7-2.3-1
|
||||
c-3.3-1.3-6.7-2.6-10.3-3.7c-3.5-1.1-7.1-2.1-10.8-3c-3.7-0.9-7.4-1.6-11.2-2.2c-0.7-0.1-1.3-0.2-2-0.3
|
||||
c-8.5-27.2-32.9-38.6-32.9-38.6c-27.3,17.3-32.4,41.5-32.4,41.5s-0.1,0.5-0.3,1.4c-1.5,0.4-3,0.9-4.5,1.3c-2.1,0.6-4.2,1.4-6.2,2.2
|
||||
c-2.1,0.8-4.1,1.6-6.2,2.5c-4.1,1.8-8.2,3.8-12.2,6c-3.9,2.2-7.7,4.6-11.4,7.1c-0.5-0.2-1-0.4-1-0.4c-37.8-14.4-71.3,2.9-71.3,2.9
|
||||
c-3.1,40.2,15.1,65.5,18.7,70.1c-0.9,2.5-1.7,5-2.5,7.5c-2.8,9.1-4.9,18.4-6.2,28.1c-0.2,1.4-0.4,2.8-0.5,4.2
|
||||
C18.8,192.7,8.5,228,8.5,228c29.1,33.5,63.1,35.6,63.1,35.6c0,0,0.1-0.1,0.1-0.1c4.3,7.7,9.3,15,14.9,21.9c2.4,2.9,4.8,5.6,7.4,8.3
|
||||
c-10.6,30.4,1.5,55.6,1.5,55.6c32.4,1.2,53.7-14.2,58.2-17.7c3.2,1.1,6.5,2.1,9.8,2.9c10,2.6,20.2,4.1,30.4,4.5
|
||||
c2.5,0.1,5.1,0.2,7.6,0.1l1.2,0l0.8,0l1.6,0l1.6-0.1l0,0.1c15.3,21.8,42.1,24.9,42.1,24.9c19.1-20.1,20.2-40.1,20.2-44.4l0,0
|
||||
c0,0,0-0.1,0-0.3c0-0.4,0-0.6,0-0.6l0,0c0-0.3,0-0.6,0-0.9c4-2.8,7.8-5.8,11.4-9.1c7.6-6.9,14.3-14.8,19.9-23.3
|
||||
c0.5-0.8,1-1.6,1.5-2.4c21.6,1.2,36.9-13.4,36.9-13.4c-3.6-22.5-16.4-33.5-19.1-35.6l0,0c0,0-0.1-0.1-0.3-0.2
|
||||
c-0.2-0.1-0.2-0.2-0.2-0.2c0,0,0,0,0,0c-0.1-0.1-0.3-0.2-0.5-0.3c0.1-1.4,0.2-2.7,0.3-4.1c0.2-2.4,0.2-4.9,0.2-7.3l0-1.8l0-0.9
|
||||
l0-0.5c0-0.6,0-0.4,0-0.6l-0.1-1.5l-0.1-2c0-0.7-0.1-1.3-0.2-1.9c-0.1-0.6-0.1-1.3-0.2-1.9l-0.2-1.9l-0.3-1.9
|
||||
c-0.4-2.5-0.8-4.9-1.4-7.4c-2.3-9.7-6.1-18.9-11-27.2c-5-8.3-11.2-15.6-18.3-21.8c-7-6.2-14.9-11.2-23.1-14.9
|
||||
c-8.3-3.7-16.9-6.1-25.5-7.2c-4.3-0.6-8.6-0.8-12.9-0.7l-1.6,0l-0.4,0c-0.1,0-0.6,0-0.5,0l-0.7,0l-1.6,0.1c-0.6,0-1.2,0.1-1.7,0.1
|
||||
c-2.2,0.2-4.4,0.5-6.5,0.9c-8.6,1.6-16.7,4.7-23.8,9c-7.1,4.3-13.3,9.6-18.3,15.6c-5,6-8.9,12.7-11.6,19.6c-2.7,6.9-4.2,14.1-4.6,21
|
||||
c-0.1,1.7-0.1,3.5-0.1,5.2c0,0.4,0,0.9,0,1.3l0.1,1.4c0.1,0.8,0.1,1.7,0.2,2.5c0.3,3.5,1,6.9,1.9,10.1c1.9,6.5,4.9,12.4,8.6,17.4
|
||||
c3.7,5,8.2,9.1,12.9,12.4c4.7,3.2,9.8,5.5,14.8,7c5,1.5,10,2.1,14.7,2.1c0.6,0,1.2,0,1.7,0c0.3,0,0.6,0,0.9,0c0.3,0,0.6,0,0.9-0.1
|
||||
c0.5,0,1-0.1,1.5-0.1c0.1,0,0.3,0,0.4-0.1l0.5-0.1c0.3,0,0.6-0.1,0.9-0.1c0.6-0.1,1.1-0.2,1.7-0.3c0.6-0.1,1.1-0.2,1.6-0.4
|
||||
c1.1-0.2,2.1-0.6,3.1-0.9c2-0.7,4-1.5,5.7-2.4c1.8-0.9,3.4-2,5-3c0.4-0.3,0.9-0.6,1.3-1c1.6-1.3,1.9-3.7,0.6-5.3
|
||||
c-1.1-1.4-3.1-1.8-4.7-0.9c-0.4,0.2-0.8,0.4-1.2,0.6c-1.4,0.7-2.8,1.3-4.3,1.8c-1.5,0.5-3.1,0.9-4.7,1.2c-0.8,0.1-1.6,0.2-2.5,0.3
|
||||
c-0.4,0-0.8,0.1-1.3,0.1c-0.4,0-0.9,0-1.2,0c-0.4,0-0.8,0-1.2,0c-0.5,0-1,0-1.5-0.1c0,0-0.3,0-0.1,0l-0.2,0l-0.3,0
|
||||
c-0.2,0-0.5,0-0.7-0.1c-0.5-0.1-0.9-0.1-1.4-0.2c-3.7-0.5-7.4-1.6-10.9-3.2c-3.6-1.6-7-3.8-10.1-6.6c-3.1-2.8-5.8-6.1-7.9-9.9
|
||||
c-2.1-3.8-3.6-8-4.3-12.4c-0.3-2.2-0.5-4.5-0.4-6.7c0-0.6,0.1-1.2,0.1-1.8c0,0.2,0-0.1,0-0.1l0-0.2l0-0.5c0-0.3,0.1-0.6,0.1-0.9
|
||||
c0.1-1.2,0.3-2.4,0.5-3.6c1.7-9.6,6.5-19,13.9-26.1c1.9-1.8,3.9-3.4,6-4.9c2.1-1.5,4.4-2.8,6.8-3.9c2.4-1.1,4.8-2,7.4-2.7
|
||||
c2.5-0.7,5.1-1.1,7.8-1.4c1.3-0.1,2.6-0.2,4-0.2c0.4,0,0.6,0,0.9,0l1.1,0l0.7,0c0.3,0,0,0,0.1,0l0.3,0l1.1,0.1
|
||||
c2.9,0.2,5.7,0.6,8.5,1.3c5.6,1.2,11.1,3.3,16.2,6.1c10.2,5.7,18.9,14.5,24.2,25.1c2.7,5.3,4.6,11,5.5,16.9c0.2,1.5,0.4,3,0.5,4.5
|
||||
l0.1,1.1l0.1,1.1c0,0.4,0,0.8,0,1.1c0,0.4,0,0.8,0,1.1l0,1l0,1.1c0,0.7-0.1,1.9-0.1,2.6c-0.1,1.6-0.3,3.3-0.5,4.9
|
||||
c-0.2,1.6-0.5,3.2-0.8,4.8c-0.3,1.6-0.7,3.2-1.1,4.7c-0.8,3.1-1.8,6.2-3,9.3c-2.4,6-5.6,11.8-9.4,17.1
|
||||
c-7.7,10.6-18.2,19.2-30.2,24.7c-6,2.7-12.3,4.7-18.8,5.7c-3.2,0.6-6.5,0.9-9.8,1l-0.6,0l-0.5,0l-1.1,0l-1.6,0l-0.8,0
|
||||
c0.4,0-0.1,0-0.1,0l-0.3,0c-1.8,0-3.5-0.1-5.3-0.3c-7-0.5-13.9-1.8-20.7-3.7c-6.7-1.9-13.2-4.6-19.4-7.8
|
||||
c-12.3-6.6-23.4-15.6-32-26.5c-4.3-5.4-8.1-11.3-11.2-17.4c-3.1-6.1-5.6-12.6-7.4-19.1c-1.8-6.6-2.9-13.3-3.4-20.1l-0.1-1.3l0-0.3
|
||||
l0-0.3l0-0.6l0-1.1l0-0.3l0-0.4l0-0.8l0-1.6l0-0.3c0,0,0,0.1,0-0.1l0-0.6c0-0.8,0-1.7,0-2.5c0.1-3.3,0.4-6.8,0.8-10.2
|
||||
c0.4-3.4,1-6.9,1.7-10.3c0.7-3.4,1.5-6.8,2.5-10.2c1.9-6.7,4.3-13.2,7.1-19.3c5.7-12.2,13.1-23.1,22-31.8c2.2-2.2,4.5-4.2,6.9-6.2
|
||||
c2.4-1.9,4.9-3.7,7.5-5.4c2.5-1.7,5.2-3.2,7.9-4.6c1.3-0.7,2.7-1.4,4.1-2c0.7-0.3,1.4-0.6,2.1-0.9c0.7-0.3,1.4-0.6,2.1-0.9
|
||||
c2.8-1.2,5.7-2.2,8.7-3.1c0.7-0.2,1.5-0.4,2.2-0.7c0.7-0.2,1.5-0.4,2.2-0.6c1.5-0.4,3-0.8,4.5-1.1c0.7-0.2,1.5-0.3,2.3-0.5
|
||||
c0.8-0.2,1.5-0.3,2.3-0.5c0.8-0.1,1.5-0.3,2.3-0.4l1.1-0.2l1.2-0.2c0.8-0.1,1.5-0.2,2.3-0.3c0.9-0.1,1.7-0.2,2.6-0.3
|
||||
c0.7-0.1,1.9-0.2,2.6-0.3c0.5-0.1,1.1-0.1,1.6-0.2l1.1-0.1l0.5-0.1l0.6,0c0.9-0.1,1.7-0.1,2.6-0.2l1.3-0.1c0,0,0.5,0,0.1,0l0.3,0
|
||||
l0.6,0c0.7,0,1.5-0.1,2.2-0.1c2.9-0.1,5.9-0.1,8.8,0c5.8,0.2,11.5,0.9,17,1.9c11.1,2.1,21.5,5.6,31,10.3
|
||||
c9.5,4.6,17.9,10.3,25.3,16.5c0.5,0.4,0.9,0.8,1.4,1.2c0.4,0.4,0.9,0.8,1.3,1.2c0.9,0.8,1.7,1.6,2.6,2.4c0.9,0.8,1.7,1.6,2.5,2.4
|
||||
c0.8,0.8,1.6,1.6,2.4,2.5c3.1,3.3,6,6.6,8.6,10c5.2,6.7,9.4,13.5,12.7,19.9c0.2,0.4,0.4,0.8,0.6,1.2c0.2,0.4,0.4,0.8,0.6,1.2
|
||||
c0.4,0.8,0.8,1.6,1.1,2.4c0.4,0.8,0.7,1.5,1.1,2.3c0.3,0.8,0.7,1.5,1,2.3c1.2,3,2.4,5.9,3.3,8.6c1.5,4.4,2.6,8.3,3.5,11.7
|
||||
c0.3,1.4,1.6,2.3,3,2.1c1.5-0.1,2.6-1.3,2.6-2.8C342.6,170.4,342.5,166.1,342,161.2z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 5.6 KiB |
|
@ -1,163 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import { metricToFormatter } from './util/Utils.js';
|
||||
import Percentage from './util/Percentage.js';
|
||||
import React from 'react';
|
||||
import * as d3 from 'd3';
|
||||
import './../../css/bar-chart.css';
|
||||
|
||||
const defaultSvgWidth = 595;
|
||||
const defaultSvgHeight = 150;
|
||||
const margin = { top: 0, right: 0, bottom: 20, left: 0 };
|
||||
const horizontalLabelLimit = 4; // number of bars beyond which to tilt axis labels
|
||||
const labelLimit = 40; // beyond this, stop labelling bars entirely
|
||||
|
||||
export default class LineGraph extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = this.getChartDimensions();
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.initializeScales();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.svg = d3.select("." + this.props.containerClassName)
|
||||
.append("svg")
|
||||
.attr("class", "bar-chart")
|
||||
.attr("width", this.state.svgWidth)
|
||||
.attr("height", this.state.svgHeight)
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + this.state.margin.left + "," + this.state.margin.top + ")");
|
||||
|
||||
this.tooltip = d3.select("." + this.props.containerClassName + " .bar-chart-tooltip")
|
||||
.append("div").attr("class", "tooltip");
|
||||
|
||||
this.xAxis = this.svg.append("g")
|
||||
.attr("class", "x-axis")
|
||||
.attr("transform", "translate(0," + this.state.height + ")");
|
||||
|
||||
this.updateScales();
|
||||
this.renderGraph();
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (nextProps.lastUpdated === this.props.lastUpdated) {
|
||||
// control whether react re-renders the component
|
||||
// only rerender if the input data has changed
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.updateScales();
|
||||
this.renderGraph();
|
||||
}
|
||||
|
||||
getChartDimensions() {
|
||||
let svgWidth = this.props.width || defaultSvgWidth;
|
||||
let svgHeight = this.props.height || defaultSvgHeight;
|
||||
let tiltLabels = false;
|
||||
let hideLabels = false;
|
||||
|
||||
if (_.size(this.props.data) > horizontalLabelLimit) {
|
||||
if (_.size(this.props.data) > labelLimit) {
|
||||
// if there are way too many bars, don't label at all
|
||||
hideLabels = true;
|
||||
} else {
|
||||
// if there are many bars, tilt x axis labels
|
||||
margin.bottom += 100;
|
||||
tiltLabels = true;
|
||||
}
|
||||
}
|
||||
|
||||
let width = svgWidth - margin.left - margin.right;
|
||||
let height = svgHeight - margin.top - margin.bottom;
|
||||
|
||||
return {
|
||||
svgWidth: svgWidth,
|
||||
svgHeight: svgHeight,
|
||||
width: width,
|
||||
height: height,
|
||||
margin: margin,
|
||||
tiltLabels: tiltLabels,
|
||||
hideLabels: hideLabels
|
||||
};
|
||||
}
|
||||
|
||||
chartData() {
|
||||
let data = this.props.data;
|
||||
_.each(data, d => {
|
||||
let p = new Percentage(d.requestRate, d.totalRequests);
|
||||
d.shareOfRequests = p.get();
|
||||
d.pretty = p.prettyRate();
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
updateScales() {
|
||||
let data = this.chartData();
|
||||
this.xScale.domain(_.map(data, d => d.name));
|
||||
this.yScale.domain([0, d3.max(data, d => d.requestRate)]);
|
||||
}
|
||||
|
||||
initializeScales() {
|
||||
this.xScale = d3.scaleBand()
|
||||
.range([0, this.state.width])
|
||||
.padding(0.1);
|
||||
this.yScale = d3.scaleLinear()
|
||||
.range([this.state.height, 0]);
|
||||
}
|
||||
|
||||
renderGraph() {
|
||||
let data = this.chartData();
|
||||
let barChart = this.svg.selectAll(".bar")
|
||||
.remove()
|
||||
.exit()
|
||||
.data(data);
|
||||
|
||||
barChart.enter().append("rect")
|
||||
.attr("class", "bar")
|
||||
.attr("x", d => this.xScale(d.name))
|
||||
.attr("width", () => this.xScale.bandwidth())
|
||||
.attr("y", d => this.yScale(d.requestRate))
|
||||
.attr("height", d => this.state.height - this.yScale(d.requestRate))
|
||||
.on("mousemove", d => {
|
||||
this.tooltip
|
||||
.style("left", d3.event.pageX - 50 + "px")
|
||||
.style("top", d3.event.pageY - 70 + "px")
|
||||
.style("display", "inline-block") // show tooltip
|
||||
.html(`${d.name}:<br /> ${metricToFormatter["REQUEST_RATE"](d.requestRate)} (${d.pretty} of total)`);
|
||||
})
|
||||
.on("mouseout", () => this.tooltip.style("display", "none"));
|
||||
|
||||
this.updateAxes();
|
||||
}
|
||||
|
||||
updateAxes() {
|
||||
this.xAxis
|
||||
.call(d3.axisBottom(this.xScale)) // add x axis labels
|
||||
.selectAll("text")
|
||||
.attr("class", "tick-labels")
|
||||
.attr("transform", () => this.state.tiltLabels ? "rotate(-65)" : "")
|
||||
.style("text-anchor", () => this.state.tiltLabels ? "end" : "")
|
||||
.text(d => {
|
||||
if (this.state.hideLabels) {
|
||||
return;
|
||||
}
|
||||
|
||||
let displayText = d;
|
||||
// truncate long label names
|
||||
if (_.size(displayText) > 20) {
|
||||
displayText = "..." + displayText.substring(displayText.length - 20);
|
||||
}
|
||||
return displayText;
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -1,180 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import ConduitSpinner from "./ConduitSpinner.jsx";
|
||||
import ErrorBanner from './ErrorBanner.jsx';
|
||||
import GrafanaLink from './GrafanaLink.jsx';
|
||||
import { incompleteMeshMessage } from './util/CopyUtils.jsx';
|
||||
import MetricsSummary from './MetricsSummary.jsx';
|
||||
import PageHeader from './PageHeader.jsx';
|
||||
import React from 'react';
|
||||
import ResourceHealthOverview from './ResourceHealthOverview.jsx';
|
||||
import UpstreamDownstream from './UpstreamDownstream.jsx';
|
||||
import { getPodsByDeployment, processRollupMetrics } from './util/MetricUtils.js';
|
||||
import './../../css/deployment.css';
|
||||
import 'whatwg-fetch';
|
||||
|
||||
export default class DeploymentDetail extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.api = this.props.api;
|
||||
this.handleApiError = this.handleApiError.bind(this);
|
||||
this.loadFromServer = this.loadFromServer.bind(this);
|
||||
this.state = this.initialState(this.props.location);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadFromServer();
|
||||
this.timerId = window.setInterval(this.loadFromServer, this.state.pollingInterval);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
window.scrollTo(0, 0);
|
||||
this.setState(this.initialState(nextProps.location), () => {
|
||||
this.loadFromServer();
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.clearInterval(this.timerId);
|
||||
this.api.cancelCurrentRequests();
|
||||
}
|
||||
|
||||
initialState(location) {
|
||||
let urlParams = new URLSearchParams(location.search);
|
||||
let deployment = urlParams.get("deploy");
|
||||
return {
|
||||
lastUpdated: 0,
|
||||
pollingInterval: 2000,
|
||||
deploy: deployment,
|
||||
pods: [],
|
||||
upstreamMetrics: [],
|
||||
downstreamMetrics: [],
|
||||
pendingRequests: false,
|
||||
loaded: false,
|
||||
error: ''
|
||||
};
|
||||
}
|
||||
|
||||
loadFromServer() {
|
||||
if (this.state.pendingRequests) {
|
||||
return; // don't make more requests if the ones we sent haven't completed
|
||||
}
|
||||
this.setState({ pendingRequests: true });
|
||||
|
||||
let urls = this.api.urlsForResource;
|
||||
|
||||
let deployMetricsUrl = urls["deployment"].url(this.state.deploy).rollup;
|
||||
let upstreamRollupUrl = urls["upstream_deployment"].url(this.state.deploy).rollup;
|
||||
let downstreamRollupUrl = urls["downstream_deployment"].url(this.state.deploy).rollup;
|
||||
|
||||
this.api.setCurrentRequests([
|
||||
this.api.fetchMetrics(deployMetricsUrl),
|
||||
this.api.fetchMetrics(upstreamRollupUrl),
|
||||
this.api.fetchMetrics(downstreamRollupUrl),
|
||||
this.api.fetchPods()
|
||||
]);
|
||||
|
||||
// expose serverPromise for testing
|
||||
this.serverPromise = Promise.all(this.api.getCurrentPromises())
|
||||
.then(([deployMetrics, upstreamRollup, downstreamRollup, podList]) => {
|
||||
let deployRollup = processRollupMetrics(deployMetrics.metrics, "targetDeploy");
|
||||
let upstreamMetrics = processRollupMetrics(upstreamRollup.metrics, "sourceDeploy");
|
||||
let downstreamMetrics = processRollupMetrics(downstreamRollup.metrics, "targetDeploy");
|
||||
|
||||
let deploy = _.find(getPodsByDeployment(podList.pods), ["name", this.state.deploy]);
|
||||
|
||||
this.setState({
|
||||
added: deploy.added,
|
||||
pods: deploy.pods,
|
||||
deployMetrics: _.get(deployRollup, 0, {}),
|
||||
deployTs: {},
|
||||
upstreamMetrics: upstreamMetrics,
|
||||
downstreamMetrics: downstreamMetrics,
|
||||
lastUpdated: Date.now(),
|
||||
pendingRequests: false,
|
||||
loaded: true,
|
||||
error: ''
|
||||
});
|
||||
})
|
||||
.catch(this.handleApiError);
|
||||
}
|
||||
|
||||
handleApiError(e) {
|
||||
if (e.isCanceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
pendingRequests: false,
|
||||
error: `Error getting data from server: ${e.message}`
|
||||
});
|
||||
}
|
||||
|
||||
numUpstreams() {
|
||||
return _.size(this.state.upstreamMetrics);
|
||||
}
|
||||
|
||||
numDownstreams() {
|
||||
return _.size(this.state.downstreamMetrics);
|
||||
}
|
||||
|
||||
renderSections() {
|
||||
let srTs = _.get(this.state.deployTs, "SUCCESS_RATE", []);
|
||||
let currentSuccessRate = _.get(_.last(srTs), "value");
|
||||
|
||||
return [
|
||||
<MetricsSummary
|
||||
key="metrics-summary"
|
||||
metrics={this.state.deployMetrics} />,
|
||||
<ResourceHealthOverview
|
||||
key="deploy-health-pane"
|
||||
resourceName={this.state.deploy}
|
||||
resourceType="deployment"
|
||||
currentSr={currentSuccessRate}
|
||||
upstreamMetrics={this.state.upstreamMetrics}
|
||||
downstreamMetrics={this.state.downstreamMetrics}
|
||||
deploymentAdded={this.state.added} />,
|
||||
<UpstreamDownstream
|
||||
key="deploy-upstream-downstream"
|
||||
resourceType="deployment"
|
||||
resourceName={this.state.deploy}
|
||||
lastUpdated={this.state.lastUpdated}
|
||||
upstreamMetrics={this.state.upstreamMetrics}
|
||||
downstreamMetrics={this.state.downstreamMetrics}
|
||||
api={this.api} />,
|
||||
];
|
||||
}
|
||||
|
||||
renderDeploymentTitle() {
|
||||
return (
|
||||
<div className="deployment-title">
|
||||
<h1>{this.state.deploy}</h1>
|
||||
{
|
||||
!this.state.added ? (
|
||||
<p className="status-badge unadded">UNADDED</p>
|
||||
) : (
|
||||
<span> <GrafanaLink name={this.state.deploy} size={32} conduitLink={this.api.ConduitLink} /></span>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="page-content deployment-detail">
|
||||
{ !this.state.error ? null : <ErrorBanner message={this.state.error} /> }
|
||||
{ !this.state.loaded ? <ConduitSpinner /> :
|
||||
<div>
|
||||
<PageHeader
|
||||
subHeaderTitle="Deployment detail"
|
||||
subHeader={this.renderDeploymentTitle()}
|
||||
subMessage={!this.state.added ? incompleteMeshMessage(this.state.deploy) : null}
|
||||
api={this.api} />
|
||||
|
||||
{this.renderSections()}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@ import ErrorBanner from './ErrorBanner.jsx';
|
|||
import MetricsTable from './MetricsTable.jsx';
|
||||
import PageHeader from './PageHeader.jsx';
|
||||
import React from 'react';
|
||||
import { emptyMetric, getPodsByDeployment, processRollupMetrics } from './util/MetricUtils.js';
|
||||
import { getPodsByDeployment, processRollupMetrics } from './util/MetricUtils.js';
|
||||
import './../../css/deployments.css';
|
||||
import 'whatwg-fetch';
|
||||
|
||||
|
@ -35,15 +35,14 @@ export default class DeploymentsList extends React.Component {
|
|||
this.api.cancelCurrentRequests();
|
||||
}
|
||||
|
||||
addDeploysWithNoMetrics(deploys, metrics) {
|
||||
// also display deployments which have not been added to the service mesh
|
||||
// (and therefore have no associated metrics)
|
||||
let newMetrics = [];
|
||||
let metricsByName = _.groupBy(metrics, 'name');
|
||||
_.each(deploys, data => {
|
||||
newMetrics.push(_.get(metricsByName, [data.name, 0], emptyMetric(data.name, data.added)));
|
||||
});
|
||||
return newMetrics;
|
||||
filterDeploys(deploys, metrics) {
|
||||
let deploysByName = _.keyBy(deploys, 'name');
|
||||
return _.compact(_.map(metrics, metric => {
|
||||
if (_.has(deploysByName, metric.name)) {
|
||||
metric.added = deploysByName[metric.name].added;
|
||||
return metric;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
loadFromServer() {
|
||||
|
@ -61,8 +60,8 @@ export default class DeploymentsList extends React.Component {
|
|||
this.serverPromise = Promise.all(this.api.getCurrentPromises())
|
||||
.then(([rollup, p]) => {
|
||||
let poByDeploy = getPodsByDeployment(p.pods);
|
||||
let meshDeploys = processRollupMetrics(rollup.metrics, "targetDeploy");
|
||||
let combinedMetrics = this.addDeploysWithNoMetrics(poByDeploy, meshDeploys);
|
||||
let meshDeploys = processRollupMetrics(rollup);
|
||||
let combinedMetrics = this.filterDeploys(poByDeploy, meshDeploys);
|
||||
|
||||
this.setState({
|
||||
metrics: combinedMetrics,
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import grafanaicon from './../../img/grafana_icon.svg';
|
||||
import React from 'react';
|
||||
|
||||
export default class GrafanaLink extends React.Component {
|
||||
|
@ -11,12 +10,7 @@ export default class GrafanaLink extends React.Component {
|
|||
to={`/dashboard/db/conduit-deployment?var-namespace=${namespace}&var-deployment=${deployment}`}
|
||||
deployment={"grafana"}
|
||||
targetBlank={true}>
|
||||
<img
|
||||
src={grafanaicon}
|
||||
width={this.props.size}
|
||||
height={this.props.size}
|
||||
title={`${namespace}/${deployment} grafana dashboard`}
|
||||
alt={`link to ${namespace}/${deployment} grafana dashboard`} />
|
||||
{this.props.name} <i className="fa fa-external-link" />
|
||||
</this.props.conduitLink>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,186 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import { metricToFormatter } from './util/Utils.js';
|
||||
import React from 'react';
|
||||
import * as d3 from 'd3';
|
||||
import './../../css/latency-overview.css';
|
||||
import './../../css/line-graph.css';
|
||||
|
||||
const defaultSvgWidth = 874;
|
||||
const defaultSvgHeight = 350;
|
||||
const margin = { top: 20, right: 0, bottom: 30, left: 0 };
|
||||
const dataDefaults = { P50: [], P95: [], P99: [] };
|
||||
export default class MultiLineGraph extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = this.getChartDimensions();
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.initializeScales();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.svg = d3.select("." + this.props.containerClassName)
|
||||
.append("svg")
|
||||
.attr("width", this.state.svgWidth)
|
||||
.attr("height", this.state.svgHeight)
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + this.state.margin.left + "," + this.state.margin.top + ")");
|
||||
|
||||
this.xAxis = this.svg.append("g")
|
||||
.attr("class", "x-axis")
|
||||
.attr("transform", "translate(0," + this.state.height + ")");
|
||||
|
||||
this.yAxis = this.svg.append("g")
|
||||
.attr("class", "y-axis")
|
||||
.attr("transform", "translate(" + this.state.width + ",0)");
|
||||
|
||||
this.updateScales();
|
||||
this.initializeGraph();
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (nextProps.lastUpdated === this.props.lastUpdated) {
|
||||
// control whether react re-renders the component
|
||||
// only rerender if the input data has changed
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.updateScales();
|
||||
this.updateGraph();
|
||||
}
|
||||
|
||||
getChartDimensions() {
|
||||
let svgWidth = this.props.width || defaultSvgWidth;
|
||||
let svgHeight = this.props.height || defaultSvgHeight;
|
||||
|
||||
let width = svgWidth - margin.left - margin.right;
|
||||
let height = svgHeight - margin.top - margin.bottom;
|
||||
|
||||
return {
|
||||
svgWidth: svgWidth,
|
||||
svgHeight: svgHeight,
|
||||
width: width,
|
||||
height: height,
|
||||
margin: margin
|
||||
};
|
||||
}
|
||||
|
||||
updateScales() {
|
||||
// update scales over all the latency values
|
||||
let data = _.flatten(_.values(this.props.data));
|
||||
|
||||
this.xScale.domain(d3.extent(data, d => parseInt(d.timestamp)));
|
||||
this.yScale.domain([0, d3.max(data, d => parseFloat(d.value))]);
|
||||
}
|
||||
|
||||
initializeScales() {
|
||||
this.xScale = d3.scaleTime().range([0, this.state.width]);
|
||||
this.yScale = d3.scaleLinear().range([this.state.height, 0]);
|
||||
|
||||
let x = this.xScale;
|
||||
let y = this.yScale;
|
||||
|
||||
// define the line
|
||||
this.line = d3.line()
|
||||
.x(d => x(d.timestamp))
|
||||
.y(d => y(d.value));
|
||||
}
|
||||
|
||||
initializeGraph() {
|
||||
let d = _.isEmpty(this.props.data) ? dataDefaults : this.props.data;
|
||||
|
||||
this.svg.append("path")
|
||||
.attr("class", "chart-line line-p50")
|
||||
.attr("d", this.line(d.P50));
|
||||
|
||||
this.svg.append("path")
|
||||
.attr("class", "chart-line line-p95")
|
||||
.attr("d", this.line(d.P95));
|
||||
|
||||
this.svg.append("path")
|
||||
.attr("class", "chart-line line-p99")
|
||||
.attr("d", this.line(d.P99));
|
||||
|
||||
this.updateAxes();
|
||||
}
|
||||
|
||||
updateGraph() {
|
||||
let d = _.isEmpty(this.props.data) ? dataDefaults : this.props.data;
|
||||
|
||||
this.svg.select(".line-p50")
|
||||
.transition()
|
||||
.duration(450)
|
||||
.attr("d", this.line(d.P50));
|
||||
|
||||
this.svg.select(".line-p95")
|
||||
.transition()
|
||||
.duration(450)
|
||||
.attr("d", this.line(d.P95));
|
||||
|
||||
this.svg.select(".line-p99")
|
||||
.transition()
|
||||
.duration(450)
|
||||
.attr("d", this.line(d.P99));
|
||||
|
||||
this.updateAxes();
|
||||
}
|
||||
|
||||
updateAxes() {
|
||||
// Same as ScatterPlot.jsx
|
||||
if (this.props.showAxes) {
|
||||
let xAxis = d3.axisBottom(this.xScale)
|
||||
.ticks(5)
|
||||
.tickSize(5);
|
||||
this.xAxis.call(xAxis);
|
||||
|
||||
let yAxis = d3.axisLeft(this.yScale)
|
||||
.ticks(4)
|
||||
.tickSize(this.state.width)
|
||||
.tickFormat(metricToFormatter["LATENCY"]);
|
||||
|
||||
// custom axis styling: https://bl.ocks.org/mbostock/3371592
|
||||
let customYAxis = g => {
|
||||
g.call(yAxis);
|
||||
g.select(".domain").remove();
|
||||
g.selectAll(".tick text")
|
||||
.attr("x", 4)
|
||||
.attr("dx", -10)
|
||||
.attr("dy", -4);
|
||||
};
|
||||
this.yAxis.call(customYAxis);
|
||||
}
|
||||
}
|
||||
|
||||
renderCurrentLatencies() {
|
||||
return (
|
||||
<div className="current-latency">
|
||||
{
|
||||
_.map(["P50", "P95", "P99"], latency => {
|
||||
let ts = this.props.data[latency];
|
||||
let lat = metricToFormatter["LATENCY"](_.get(_.last(ts), 'value', []));
|
||||
return (
|
||||
<div key={latency} className={`latency-metric current-latency-${latency}`}>
|
||||
<div className="latency-title">Current latency ({latency})</div>
|
||||
<div className="latency-value">{lat}</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={`line-graph ${this.props.containerClassName}`}>
|
||||
<div className="subsection-header">Latency Overview</div>
|
||||
{this.renderCurrentLatencies()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import { metricToFormatter } from './util/Utils.js';
|
||||
import React from 'react';
|
||||
import { Col, Row } from 'antd';
|
||||
import './../../css/metric-summary.css';
|
||||
|
||||
export default class MetricsSummary extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Row className="metric-summary">
|
||||
<Col span={4} className="metric metric-large">
|
||||
<div className="metric-title">Request rate</div>
|
||||
<div className="metric-value">
|
||||
{metricToFormatter["REQUEST_RATE"](this.props.metrics.requestRate)}
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={4} className="metric metric-large">
|
||||
<div className="metric-title">Success rate</div>
|
||||
<div className="metric-value">
|
||||
{metricToFormatter["SUCCESS_RATE"](this.props.metrics.successRate)}
|
||||
</div>
|
||||
</Col>
|
||||
{
|
||||
_.map(['P50', 'P95', 'P99'], label => {
|
||||
let latency = _.get(this.props.metrics, ['latency', label]);
|
||||
|
||||
return (
|
||||
<Col span={4} className="metric metric-large" key={`latency${label}`}>
|
||||
<div className="metric-title">{label} latency</div>
|
||||
<div className="metric-value">{metricToFormatter["LATENCY"](latency)}</div>
|
||||
</Col>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -10,13 +10,6 @@ import { Tooltip } from 'antd';
|
|||
Expects rollup and timeseries data.
|
||||
*/
|
||||
|
||||
const resourceInfo = {
|
||||
"upstream_deployment": { title: "deployment", url: "/deployment?deploy=" },
|
||||
"downstream_deployment": { title: "deployment", url: "/deployment?deploy=" },
|
||||
"deployment": { title: "deployment", url: "/deployment?deploy=" },
|
||||
"path": { title: "path", url: null }
|
||||
};
|
||||
|
||||
const withTooltip = (d, metricName) => {
|
||||
return (
|
||||
<Tooltip
|
||||
|
@ -27,26 +20,16 @@ const withTooltip = (d, metricName) => {
|
|||
);
|
||||
};
|
||||
|
||||
const columnDefinitions = (sortable = true, resource, ConduitLink) => {
|
||||
const columnDefinitions = (sortable = true, title, ConduitLink) => {
|
||||
return [
|
||||
{
|
||||
title: resource.title,
|
||||
title: title,
|
||||
key: "name",
|
||||
defaultSortOrder: 'ascend',
|
||||
width: 150,
|
||||
sorter: sortable ? (a, b) => (a.name || "").localeCompare(b.name) : false,
|
||||
render: row => (<React.Fragment>
|
||||
{!resource.url ? row.name : <ConduitLink to={`${resource.url}${row.name}`}>{row.name}</ConduitLink>}
|
||||
{row.added ? <span> <GrafanaLink name={row.name} size={16} conduitLink={ConduitLink} /></span> : null}
|
||||
</React.Fragment>)
|
||||
},
|
||||
{
|
||||
title: "Request Rate",
|
||||
dataIndex: "requestRate",
|
||||
key: "requestRateRollup",
|
||||
className: "numeric",
|
||||
sorter: sortable ? (a, b) => numericSort(a.requestRate, b.requestRate) : false,
|
||||
render: d => withTooltip(d, "REQUEST_RATE")
|
||||
render: row => row.added ?
|
||||
<GrafanaLink name={row.name} conduitLink={ConduitLink} /> : row.name
|
||||
},
|
||||
{
|
||||
title: "Success Rate",
|
||||
|
@ -56,6 +39,14 @@ const columnDefinitions = (sortable = true, resource, ConduitLink) => {
|
|||
sorter: sortable ? (a, b) => numericSort(a.successRate, b.successRate) : false,
|
||||
render: d => metricToFormatter["SUCCESS_RATE"](d)
|
||||
},
|
||||
{
|
||||
title: "Request Rate",
|
||||
dataIndex: "requestRate",
|
||||
key: "requestRateRollup",
|
||||
className: "numeric",
|
||||
sorter: sortable ? (a, b) => numericSort(a.requestRate, b.requestRate) : false,
|
||||
render: d => withTooltip(d, "REQUEST_RATE")
|
||||
},
|
||||
{
|
||||
title: "P50 Latency",
|
||||
dataIndex: "P50",
|
||||
|
@ -104,9 +95,8 @@ export default class MetricsTable extends BaseTable {
|
|||
}
|
||||
|
||||
render() {
|
||||
let resource = resourceInfo[this.props.resource];
|
||||
let tableData = this.preprocessMetrics();
|
||||
let columns = _.compact(columnDefinitions(this.props.sortable, resource, this.api.ConduitLink));
|
||||
let columns = _.compact(columnDefinitions(this.props.sortable, this.props.resource, this.api.ConduitLink));
|
||||
|
||||
return (<BaseTable
|
||||
dataSource={tableData}
|
||||
|
|
|
@ -1,103 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import Metric from './Metric.jsx';
|
||||
import { metricToFormatter } from './util/Utils.js';
|
||||
import React from 'react';
|
||||
import { Col, Progress, Row } from 'antd';
|
||||
import './../../css/health-pane.css';
|
||||
|
||||
const TrafficIndicator = ({healthStat}) => {
|
||||
return (
|
||||
<Progress
|
||||
percent={100}
|
||||
status={healthStat === "health-unknown" ? "success" : "active"}
|
||||
strokeWidth={8}
|
||||
format={() => null} />
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default class ResourceHealthOverview extends React.Component {
|
||||
getRequestRate(metrics) {
|
||||
return _.sumBy(metrics, 'requestRate');
|
||||
}
|
||||
|
||||
getAvgSuccessRate(metrics) {
|
||||
return _.meanBy(metrics, 'successRate');
|
||||
}
|
||||
|
||||
getHealthClassName(successRate) {
|
||||
if (_.isNil(successRate) || _.isNaN(successRate)) {
|
||||
return "health-unknown";
|
||||
}
|
||||
if (successRate < 0.4) {
|
||||
return "health-bad";
|
||||
}
|
||||
if (successRate > 0.95) {
|
||||
return "health-good";
|
||||
}
|
||||
return "health-neutral";
|
||||
}
|
||||
|
||||
getHealthStats() {
|
||||
let inboundSr = this.getAvgSuccessRate(this.props.upstreamMetrics);
|
||||
let outboundSr = this.getAvgSuccessRate(this.props.downstreamMetrics);
|
||||
let sr = this.props.currentSr;
|
||||
|
||||
return {
|
||||
inbound: {
|
||||
requests: metricToFormatter["REQUEST_RATE"](this.getRequestRate(this.props.upstreamMetrics)),
|
||||
numDeploys: _.size(this.props.upstreamMetrics),
|
||||
health: this.getHealthClassName(inboundSr)
|
||||
},
|
||||
outbound: {
|
||||
requests: metricToFormatter["REQUEST_RATE"](this.getRequestRate(this.props.downstreamMetrics)),
|
||||
numDeploys: _.size(this.props.downstreamMetrics),
|
||||
health: this.getHealthClassName(outboundSr)
|
||||
},
|
||||
current: {
|
||||
health: this.getHealthClassName(sr)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
let stats = this.getHealthStats();
|
||||
|
||||
return (
|
||||
<div key="entity-heath" className="entity-health">
|
||||
<Row>
|
||||
<Col span={8}>
|
||||
{ stats.inbound.numDeploys === 0 ? null :
|
||||
<Metric title="Inbound request rate" value={stats.inbound.requests} className="float-right" />
|
||||
}
|
||||
</Col>
|
||||
<Col span={8} />
|
||||
<Col span={8}>
|
||||
{ stats.outbound.numDeploys === 0 ? null :
|
||||
<Metric title="Outbound request rate" value={stats.outbound.requests} className="float-left" />
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<Col span={8}>
|
||||
<div className="entity-count">« {stats.inbound.numDeploys} {this.props.resourceType}s</div>
|
||||
<div className={`adjacent-health ${stats.inbound.health}`}>
|
||||
<TrafficIndicator healthStat={stats.inbound.health} />
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<div className="entity-count"> </div>
|
||||
<div className={`entity-title ${stats.current.health}`}>{this.props.resourceName}</div>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<div className="entity-count float-right">{stats.outbound.numDeploys} {this.props.resourceType}s »</div>
|
||||
<div className={`adjacent-health ${stats.outbound.health}`}>
|
||||
<TrafficIndicator healthStat={stats.outbound.health} />
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import LatencyOverview from './LatencyOverview.jsx';
|
||||
import React from 'react';
|
||||
import ResourceOverviewMetric from './ResourceOverviewMetric.jsx';
|
||||
import { rowGutter } from './util/Utils.js';
|
||||
import { Col, Row } from 'antd';
|
||||
|
||||
export default class ResourceMetricsOverview extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div className="subsection-header">{this.props.resourceType} Health</div>
|
||||
<Row gutter={rowGutter}>
|
||||
<Col span={8}>
|
||||
<ResourceOverviewMetric
|
||||
name="Request rate"
|
||||
metric="REQUEST_RATE"
|
||||
lastUpdated={this.props.lastUpdated}
|
||||
window={this.props.window}
|
||||
timeseries={_.get(this.props.timeseries, "REQUEST_RATE", [])} />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<ResourceOverviewMetric
|
||||
name="Success rate"
|
||||
metric="SUCCESS_RATE"
|
||||
window={this.props.window}
|
||||
lastUpdated={this.props.lastUpdated}
|
||||
timeseries={_.get(this.props.timeseries, "SUCCESS_RATE", [])} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<div className="latency-chart-container">
|
||||
<LatencyOverview
|
||||
data={_.get(this.props.timeseries, "LATENCY", {})}
|
||||
lastUpdated={this.props.lastUpdated}
|
||||
showAxes={true}
|
||||
containerClassName="latency-chart-container" />
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import LineGraph from './LineGraph.jsx';
|
||||
import React from 'react';
|
||||
import { metricToFormatter, toClassName } from './util/Utils.js';
|
||||
|
||||
export default class ResourceOverviewMetric extends React.Component {
|
||||
render() {
|
||||
let lastDatapoint = _.last(this.props.timeseries) || {};
|
||||
let metric = _.get(lastDatapoint, "value");
|
||||
let displayMetric = metricToFormatter[this.props.metric](metric);
|
||||
|
||||
return (
|
||||
<div className={`border-container border-neutral`}>
|
||||
<div className="border-container-content">
|
||||
<div className="summary-container clearfix">
|
||||
<div className="metric-info">
|
||||
<div className="summary-title">{this.props.name}</div>
|
||||
<div className="summary-info">last {this.props.window}</div>
|
||||
</div>
|
||||
<div className="metric-value">{displayMetric}</div>
|
||||
</div>
|
||||
|
||||
<LineGraph
|
||||
data={this.props.timeseries}
|
||||
lastUpdated={this.props.lastUpdated}
|
||||
containerClassName={`stat-pane-stat-${toClassName(this.props.name)}`}
|
||||
flashLastDatapoint={true} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,20 +1,16 @@
|
|||
import _ from 'lodash';
|
||||
import CallToAction from './CallToAction.jsx';
|
||||
import ConduitSpinner from "./ConduitSpinner.jsx";
|
||||
import DeploymentSummary from './DeploymentSummary.jsx';
|
||||
import ErrorBanner from './ErrorBanner.jsx';
|
||||
import { incompleteMeshMessage } from './util/CopyUtils.jsx';
|
||||
import Metric from './Metric.jsx';
|
||||
import PageHeader from './PageHeader.jsx';
|
||||
import React from 'react';
|
||||
import { rowGutter } from './util/Utils.js';
|
||||
import StatusTable from './StatusTable.jsx';
|
||||
import { Col, Row, Table } from 'antd';
|
||||
import {
|
||||
getComponentPods,
|
||||
getPodsByDeployment,
|
||||
processRollupMetrics,
|
||||
processTimeseriesMetrics
|
||||
} from './util/MetricUtils.js';
|
||||
import './../../css/service-mesh.css';
|
||||
|
||||
|
@ -41,10 +37,6 @@ const componentNames = {
|
|||
"web": "Web UI"
|
||||
};
|
||||
|
||||
const componentGraphTitles = {
|
||||
"telemetry": "Telemetry requests"
|
||||
};
|
||||
|
||||
const componentDeploys = {
|
||||
"prometheus": "prometheus",
|
||||
"destination": "controller",
|
||||
|
@ -54,10 +46,6 @@ const componentDeploys = {
|
|||
"telemetry": "controller",
|
||||
"web": "web"
|
||||
};
|
||||
const componentsToGraph = ["proxy-api", "telemetry", "public-api"];
|
||||
const noData = {
|
||||
timeseries: { requestRate: [], successRate: [] }
|
||||
};
|
||||
|
||||
export default class ServiceMesh extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -94,25 +82,16 @@ export default class ServiceMesh extends React.Component {
|
|||
}
|
||||
this.setState({ pendingRequests: true });
|
||||
|
||||
let rollupPath = `/api/metrics?aggregation=mesh`;
|
||||
let timeseriesPath = `${rollupPath}×eries=true`;
|
||||
|
||||
this.api.setCurrentRequests([
|
||||
this.api.fetchMetrics(rollupPath),
|
||||
this.api.fetchMetrics(timeseriesPath),
|
||||
this.api.fetchPods()
|
||||
]);
|
||||
|
||||
this.serverPromise = Promise.all(this.api.getCurrentPromises())
|
||||
.then(([metrics, ts, pods]) => {
|
||||
let m = processRollupMetrics(metrics.metrics, "component");
|
||||
let tsByComponent = processTimeseriesMetrics(ts.metrics, "component");
|
||||
.then(([pods]) => {
|
||||
let podsByDeploy = getPodsByDeployment(pods.pods);
|
||||
let controlPlanePods = this.processComponents(pods.pods);
|
||||
|
||||
this.setState({
|
||||
metrics: m,
|
||||
timeseriesByComponent: tsByComponent,
|
||||
deploys: podsByDeploy,
|
||||
components: controlPlanePods,
|
||||
lastUpdated: Date.now(),
|
||||
|
@ -183,32 +162,6 @@ export default class ServiceMesh extends React.Component {
|
|||
.value();
|
||||
}
|
||||
|
||||
renderControllerHealth() {
|
||||
return (
|
||||
<div className="mesh-section">
|
||||
<div className="subsection-header">Control plane status</div>
|
||||
<Row gutter={rowGutter}>
|
||||
{
|
||||
_.map(componentsToGraph, meshComponent => {
|
||||
let data = _.cloneDeep(_.find(this.state.metrics, ["name", meshComponent]) || noData);
|
||||
data.id = meshComponent;
|
||||
data.name = componentGraphTitles[meshComponent] || componentNames[meshComponent];
|
||||
return (<Col span={8} key={`col-${data.id}`}>
|
||||
<DeploymentSummary
|
||||
api={this.api}
|
||||
key={data.id}
|
||||
lastUpdated={this.state.lastUpdated}
|
||||
data={data}
|
||||
requestTs={_.get(this.state.timeseriesByComponent,[meshComponent, "REQUEST_RATE"], [])}
|
||||
noLink={true} />
|
||||
</Col>);
|
||||
})
|
||||
}
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderControlPlaneDetails() {
|
||||
return (
|
||||
<div className="mesh-section">
|
||||
|
@ -315,8 +268,6 @@ export default class ServiceMesh extends React.Component {
|
|||
renderOverview() {
|
||||
if (this.proxyCount() === 0) {
|
||||
return <CallToAction numDeployments={this.deployCount()} />;
|
||||
} else {
|
||||
return this.renderControllerHealth();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -44,10 +44,8 @@ const columns = {
|
|||
return {
|
||||
title: "Deployment",
|
||||
key: "name",
|
||||
render: row => (<React.Fragment>
|
||||
{shouldLink ? <ConduitLink to={`/deployment?deploy=${row.name}`}>{row.name}</ConduitLink> : row.name}
|
||||
{row.added ? <span> <GrafanaLink name={row.name} size={16} conduitLink={ConduitLink} /></span> : null}
|
||||
</React.Fragment>)
|
||||
render: row => shouldLink && row.added ?
|
||||
<GrafanaLink name={row.name} conduitLink={ConduitLink} /> : row.name
|
||||
};
|
||||
},
|
||||
pods: {
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import MetricsTable from './MetricsTable.jsx';
|
||||
import React from 'react';
|
||||
import { rowGutter } from './util/Utils.js';
|
||||
import { Col, Row } from 'antd';
|
||||
|
||||
export default class UpstreamDownstreamTables extends React.Component {
|
||||
render() {
|
||||
let numUpstreams = _.size(this.props.upstreamMetrics);
|
||||
let numDownstreams = _.size(this.props.downstreamMetrics);
|
||||
return (
|
||||
<Row gutter={rowGutter}>
|
||||
<Col span={24}>
|
||||
{
|
||||
numUpstreams === 0 ? null :
|
||||
<div className="upstream-downstream-list">
|
||||
<div className="border-container border-neutral subsection-header">
|
||||
<div className="border-container-content subsection-header">Inbound</div>
|
||||
</div>
|
||||
<MetricsTable
|
||||
resource={`upstream_${this.props.resourceType}`}
|
||||
resourceName={this.props.resourceName}
|
||||
metrics={this.props.upstreamMetrics}
|
||||
api={this.props.api} />
|
||||
</div>
|
||||
}
|
||||
{
|
||||
numDownstreams === 0 ? null :
|
||||
<div className="upstream-downstream-list">
|
||||
<div className="border-container border-neutral subsection-header">
|
||||
<div className="border-container-content subsection-header">Outbound</div>
|
||||
</div>
|
||||
<MetricsTable
|
||||
resource={`downstream_${this.props.resourceType}`}
|
||||
resourceName={this.props.resourceName}
|
||||
metrics={this.props.downstreamMetrics}
|
||||
api={this.props.api} />
|
||||
</div>
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -71,8 +71,6 @@ export const ApiHelpers = (pathPrefix, defaultMetricsWindow = '10m') => {
|
|||
return apiFetch(podsPath);
|
||||
};
|
||||
|
||||
|
||||
|
||||
const getMetricsWindow = () => metricsWindow;
|
||||
const getMetricsWindowDisplayText = () => validMetricsWindows[metricsWindow];
|
||||
|
||||
|
@ -81,44 +79,24 @@ export const ApiHelpers = (pathPrefix, defaultMetricsWindow = '10m') => {
|
|||
metricsWindow = window;
|
||||
};
|
||||
|
||||
const metricsUrl = `/api/metrics?`;
|
||||
const deploymentUrl = `/api/stat?resource_type=deployment`;
|
||||
const urlsForResource = {
|
||||
// all deploys (default), or a given deploy if specified
|
||||
// all deploys (default), or all deploys in a given namespace, or a given
|
||||
// deploy if specified
|
||||
"deployment": {
|
||||
groupBy: "targetDeploy",
|
||||
url: (deploy = null) => {
|
||||
let rollupUrl = !deploy ? metricsUrl : `${metricsUrl}&target_deploy=${deploy}`;
|
||||
let timeseriesUrl = !deploy ? `${metricsUrl}×eries=true` :
|
||||
`${metricsUrl}×eries=true&target_deploy=${deploy}`;
|
||||
url: (namespace = null, name = null) => {
|
||||
let rollupUrl = deploymentUrl;
|
||||
if (!_.isNull(namespace)) {
|
||||
rollupUrl += `&namespace=${namespace}`;
|
||||
}
|
||||
if (!_.isNull(name)) {
|
||||
rollupUrl += `&resource_name=${name}`;
|
||||
}
|
||||
return {
|
||||
ts: timeseriesUrl,
|
||||
rollup: rollupUrl
|
||||
};
|
||||
}
|
||||
},
|
||||
"upstream_deployment": {
|
||||
// all upstreams of a given deploy
|
||||
groupBy: "sourceDeploy",
|
||||
url: deploy => {
|
||||
let upstreamRollupUrl = `${metricsUrl}&aggregation=source_deploy&target_deploy=${deploy}`;
|
||||
let upstreamTimeseriesUrl = `${upstreamRollupUrl}×eries=true`;
|
||||
return {
|
||||
ts: upstreamTimeseriesUrl,
|
||||
rollup: upstreamRollupUrl
|
||||
};
|
||||
}
|
||||
},
|
||||
"downstream_deployment": {
|
||||
// all downstreams of a given deploy
|
||||
groupBy: "targetDeploy",
|
||||
url: deploy => {
|
||||
let downstreamRollupUrl = `${metricsUrl}&aggregation=target_deploy&source_deploy=${deploy}`;
|
||||
let downstreamTimeseriesUrl = `${downstreamRollupUrl}×eries=true`;
|
||||
return {
|
||||
ts: downstreamTimeseriesUrl,
|
||||
rollup: downstreamRollupUrl
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,31 +1,5 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
const gaugeAccessor = ["datapoints", 0, "value", "gauge"];
|
||||
const latencyAccessor = ["datapoints", 0, "value", "histogram", "values"];
|
||||
|
||||
const convertTs = rawTs => {
|
||||
return _.map(rawTs, metric => {
|
||||
return {
|
||||
timestamp: metric.timestampMs,
|
||||
value: _.get(metric, "value.gauge")
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const convertLatencyTs = rawTs => {
|
||||
let latencies = _.flatMap(rawTs, metric => {
|
||||
return _.map(_.get(metric, ["value", "histogram", "values"]), hist => {
|
||||
return {
|
||||
timestamp: metric.timestampMs,
|
||||
value: hist.value,
|
||||
label: hist.label
|
||||
};
|
||||
});
|
||||
});
|
||||
// this could be made more efficient by combining this with the map
|
||||
return _.groupBy(latencies, 'label');
|
||||
};
|
||||
|
||||
const getPodCategorization = pod => {
|
||||
if (pod.added && pod.status === "Running") {
|
||||
return "good";
|
||||
|
@ -37,6 +11,54 @@ const getPodCategorization = pod => {
|
|||
return ""; // Terminating | Succeeded | Unknown
|
||||
};
|
||||
|
||||
const getRequestRate = row => {
|
||||
if (_.isEmpty(row.stats)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let success = parseInt(row.stats.successCount, 10);
|
||||
let failure = parseInt(row.stats.failureCount, 10);
|
||||
let seconds = 0;
|
||||
|
||||
if (row.timeWindow === "TEN_SEC") { seconds = 10; }
|
||||
if (row.timeWindow === "ONE_MIN") { seconds = 60; }
|
||||
if (row.timeWindow === "TEN_MIN") { seconds = 600; }
|
||||
if (row.timeWindow === "ONE_HOUR") { seconds = 3600; }
|
||||
|
||||
if (seconds === 0) {
|
||||
return null;
|
||||
} else {
|
||||
return (success + failure) / seconds;
|
||||
}
|
||||
};
|
||||
|
||||
const getSuccessRate = row => {
|
||||
if (_.isEmpty(row.stats)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let success = parseInt(row.stats.successCount, 10);
|
||||
let failure = parseInt(row.stats.failureCount, 10);
|
||||
|
||||
if (success + failure === 0) {
|
||||
return null;
|
||||
} else {
|
||||
return success / (success + failure);
|
||||
}
|
||||
};
|
||||
|
||||
const getLatency = row => {
|
||||
if (_.isEmpty(row.stats)) {
|
||||
return {};
|
||||
} else {
|
||||
return {
|
||||
P50: parseInt(row.stats.latencyMsP50, 10),
|
||||
P95: parseInt(row.stats.latencyMsP95, 10),
|
||||
P99: parseInt(row.stats.latencyMsP99, 10),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const getPodsByDeployment = pods => {
|
||||
return _(pods)
|
||||
.reject(p => _.isEmpty(p.deployment) || p.controlPlane)
|
||||
|
@ -70,62 +92,21 @@ export const getComponentPods = componentPods => {
|
|||
.value();
|
||||
};
|
||||
|
||||
export const processTimeseriesMetrics = (rawTs, targetEntity) => {
|
||||
let tsbyEntity = _.groupBy(rawTs, "metadata." + targetEntity);
|
||||
return _.reduce(tsbyEntity, (mem, metrics, entity) => {
|
||||
mem[entity] = mem[entity] || {};
|
||||
_.each(metrics, metric => {
|
||||
if (metric.name !== "LATENCY") {
|
||||
mem[entity][metric.name] = convertTs(metric.datapoints);
|
||||
} else {
|
||||
mem[entity][metric.name] = convertLatencyTs(metric.datapoints);
|
||||
}
|
||||
export const processRollupMetrics = rawMetrics => {
|
||||
if (_.isEmpty(rawMetrics.ok) || _.isEmpty(rawMetrics.ok.statTables)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let metrics = _.flatMap(rawMetrics.ok.statTables, table => {
|
||||
return _.map(table.podGroup.rows, row => {
|
||||
return {
|
||||
name: row.resource.namespace + "/" + row.resource.name,
|
||||
requestRate: getRequestRate(row),
|
||||
successRate: getSuccessRate(row),
|
||||
latency: getLatency(row)
|
||||
};
|
||||
});
|
||||
return mem;
|
||||
}, {});
|
||||
};
|
||||
|
||||
export const processRollupMetrics = (rawMetrics, targetEntity) => {
|
||||
let byEntity = _.groupBy(rawMetrics, "metadata." + targetEntity);
|
||||
let metrics = _.map(byEntity, (data, entity) => {
|
||||
if (!entity) return;
|
||||
|
||||
let requestRate = 0;
|
||||
let successRate = 0;
|
||||
let latency = {};
|
||||
|
||||
_.each(data, datum => {
|
||||
if (datum.name === "REQUEST_RATE") {
|
||||
requestRate = _.round(_.get(datum, gaugeAccessor, 0), 2);
|
||||
} else if (datum.name === "SUCCESS_RATE") {
|
||||
successRate = _.get(datum, gaugeAccessor);
|
||||
} else if (datum.name === "LATENCY") {
|
||||
let latencies = _.get(datum, latencyAccessor);
|
||||
latency = _.reduce(latencies, (mem, ea) => {
|
||||
mem[ea.label] = _.isNil(ea.value) ? null : parseInt(ea.value, 10);
|
||||
return mem;
|
||||
}, {});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
name: entity,
|
||||
requestRate: requestRate,
|
||||
successRate: successRate,
|
||||
latency: latency,
|
||||
added: true
|
||||
};
|
||||
});
|
||||
|
||||
return _.compact(_.sortBy(metrics, "name"));
|
||||
};
|
||||
|
||||
export const emptyMetric = (name, added) => {
|
||||
return {
|
||||
name: name,
|
||||
requestRate: null,
|
||||
successRate: null,
|
||||
latency: null,
|
||||
added: added
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { ApiHelpers } from './components/util/ApiHelpers.jsx';
|
||||
import DeploymentDetail from './components/DeploymentDetail.jsx';
|
||||
import DeploymentsList from './components/DeploymentsList.jsx';
|
||||
import { Layout } from 'antd';
|
||||
import NoMatch from './components/NoMatch.jsx';
|
||||
|
@ -47,7 +46,6 @@ let applicationHtml = hideSidebar => (
|
|||
<Redirect exact from={`${pathPrefix}/`} to={`${pathPrefix}/servicemesh`} />
|
||||
<Route path={`${pathPrefix}/servicemesh`} render={() => <ServiceMesh api={api} releaseVersion={appData.releaseVersion} controllerNamespace={appData.controllerNamespace} />} />
|
||||
<Route path={`${pathPrefix}/deployments`} render={() => <DeploymentsList api={api} />} />
|
||||
<Route path={`${pathPrefix}/deployment`} render={props => <DeploymentDetail api={api} location={props.location} />} />
|
||||
<Route component={NoMatch} />
|
||||
</Switch>
|
||||
</div>
|
||||
|
|
|
@ -21,7 +21,7 @@ describe('ApiHelpers', () => {
|
|||
fetchStub = sinon.stub(window, 'fetch');
|
||||
fetchStub.returnsPromise().resolves({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ metrics: [] })
|
||||
json: () => Promise.resolve({})
|
||||
});
|
||||
api = ApiHelpers('');
|
||||
});
|
||||
|
@ -247,24 +247,24 @@ describe('ApiHelpers', () => {
|
|||
});
|
||||
|
||||
it('adds a ?window= if metricsWindow is the only param', () => {
|
||||
api.fetchMetrics('/metrics');
|
||||
api.fetchMetrics('/api/stat');
|
||||
|
||||
expect(fetchStub.calledOnce).to.be.true;
|
||||
expect(fetchStub.args[0][0]).to.equal('/metrics?window=10m');
|
||||
expect(fetchStub.args[0][0]).to.equal('/api/stat?window=10m');
|
||||
});
|
||||
|
||||
it('adds &window= if metricsWindow is not the only param', () => {
|
||||
api.fetchMetrics('/metrics?foo=3&bar="me"');
|
||||
api.fetchMetrics('/api/stat?foo=3&bar="me"');
|
||||
|
||||
expect(fetchStub.calledOnce).to.be.true;
|
||||
expect(fetchStub.args[0][0]).to.equal('/metrics?foo=3&bar="me"&window=10m');
|
||||
expect(fetchStub.args[0][0]).to.equal('/api/stat?foo=3&bar="me"&window=10m');
|
||||
});
|
||||
|
||||
it('does not add another &window= if there is already a window param', () => {
|
||||
api.fetchMetrics('/metrics?foo=3&window=24h&bar="me"');
|
||||
api.fetchMetrics('/api/stat?foo=3&window=24h&bar="me"');
|
||||
|
||||
expect(fetchStub.calledOnce).to.be.true;
|
||||
expect(fetchStub.args[0][0]).to.equal('/metrics?foo=3&window=24h&bar="me"');
|
||||
expect(fetchStub.args[0][0]).to.equal('/api/stat?foo=3&window=24h&bar="me"');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -279,19 +279,10 @@ describe('ApiHelpers', () => {
|
|||
});
|
||||
|
||||
describe('urlsForResource', () => {
|
||||
it('returns the correct timeseries and metric rollup urls for deployment overviews', () => {
|
||||
it('returns the correct rollup url for deployment overviews', () => {
|
||||
api = ApiHelpers('/go/my/own/way');
|
||||
let deploymentUrls = api.urlsForResource["deployment"].url("myDeploy");
|
||||
|
||||
expect(deploymentUrls.ts).to.equal('/api/metrics?×eries=true&target_deploy=myDeploy');
|
||||
expect(deploymentUrls.rollup).to.equal('/api/metrics?&target_deploy=myDeploy');
|
||||
});
|
||||
|
||||
it('returns the correct timeseries and metric rollup urls for upstream deployments', () => {
|
||||
let deploymentUrls = api.urlsForResource["upstream_deployment"].url("farUp");
|
||||
|
||||
expect(deploymentUrls.ts).to.equal('/api/metrics?&aggregation=source_deploy&target_deploy=farUp×eries=true');
|
||||
expect(deploymentUrls.rollup).to.equal('/api/metrics?&aggregation=source_deploy&target_deploy=farUp');
|
||||
let deploymentUrls = api.urlsForResource["deployment"].url("myNamespace", "myDeploy");
|
||||
expect(deploymentUrls.rollup).to.equal('/api/stat?resource_type=deployment&namespace=myNamespace&resource_name=myDeploy');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
/* eslint-disable */
|
||||
import 'raf/polyfill'; // the polyfill import must be first
|
||||
import { ApiHelpers } from '../js/components/util/ApiHelpers.jsx';
|
||||
import Adapter from 'enzyme-adapter-react-16';
|
||||
import DeploymentDetail from '../js/components/DeploymentDetail.jsx';
|
||||
import Enzyme from 'enzyme';
|
||||
import { expect } from 'chai';
|
||||
import { mount } from 'enzyme';
|
||||
import { routerWrap } from "./testHelpers.jsx";
|
||||
import sinon from 'sinon';
|
||||
import sinonStubPromise from 'sinon-stub-promise';
|
||||
/* eslint-enable */
|
||||
|
||||
Enzyme.configure({ adapter: new Adapter() });
|
||||
sinonStubPromise(sinon);
|
||||
|
||||
describe('DeploymentDetail', () => {
|
||||
let component, fetchStub;
|
||||
|
||||
function withPromise(fn) {
|
||||
return component.find("DeploymentDetail").instance().serverPromise.then(fn);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
fetchStub = sinon.stub(window, 'fetch');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
component = null;
|
||||
window.fetch.restore();
|
||||
});
|
||||
|
||||
it('renders the spinner before metrics are loaded', () => {
|
||||
fetchStub.returnsPromise().resolves({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ metrics: [] })
|
||||
});
|
||||
component = mount(routerWrap(DeploymentDetail));
|
||||
|
||||
return withPromise(() => {
|
||||
expect(component.find("ConduitSpinner")).to.have.length(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,71 +0,0 @@
|
|||
import Adapter from 'enzyme-adapter-react-16';
|
||||
import DeploymentsList from '../js/components/DeploymentsList.jsx';
|
||||
import Enzyme from 'enzyme';
|
||||
import { expect } from 'chai';
|
||||
import { mount } from 'enzyme';
|
||||
import podFixtures from './fixtures/pods.json';
|
||||
import { routerWrap } from './testHelpers.jsx';
|
||||
import sinon from 'sinon';
|
||||
import sinonStubPromise from 'sinon-stub-promise';
|
||||
|
||||
Enzyme.configure({ adapter: new Adapter() });
|
||||
sinonStubPromise(sinon);
|
||||
|
||||
describe('DeploymentsList', () => {
|
||||
let component, fetchStub;
|
||||
|
||||
function withPromise(fn) {
|
||||
return component.find("DeploymentsList").instance().serverPromise.then(fn);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
fetchStub = sinon.stub(window, 'fetch');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
component = null;
|
||||
window.fetch.restore();
|
||||
});
|
||||
|
||||
it('renders the spinner before metrics are loaded', () => {
|
||||
fetchStub.returnsPromise().resolves({ ok: true });
|
||||
component = mount(routerWrap(DeploymentsList));
|
||||
|
||||
return withPromise(() => {
|
||||
expect(component.find("DeploymentsList")).to.have.length(1);
|
||||
expect(component.find("ConduitSpinner")).to.have.length(1);
|
||||
expect(component.find("CallToAction")).to.have.length(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders a call to action if no metrics are received', () => {
|
||||
fetchStub.returnsPromise().resolves({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ metrics: [] })
|
||||
});
|
||||
component = mount(routerWrap(DeploymentsList));
|
||||
|
||||
return withPromise(() => {
|
||||
component.update();
|
||||
expect(component.find("DeploymentsList").length).to.equal(1);
|
||||
expect(component.find("ConduitSpinner").length).to.equal(0);
|
||||
expect(component.find("CallToAction").length).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the deployments page if pod data is received', () => {
|
||||
fetchStub.returnsPromise().resolves({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ metrics: [], pods: podFixtures.pods })
|
||||
});
|
||||
component = mount(routerWrap(DeploymentsList));
|
||||
|
||||
return withPromise(() => {
|
||||
component.update();
|
||||
expect(component.find("DeploymentsList").length).to.equal(1);
|
||||
expect(component.find("ConduitSpinner").length).to.equal(0);
|
||||
expect(component.find("CallToAction").length).to.equal(0);
|
||||
expect(component.find("MetricsTable").length).to.equal(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,71 +1,34 @@
|
|||
import _ from 'lodash';
|
||||
import deployRollupFixtures from './fixtures/deployRollup.json';
|
||||
import { expect } from 'chai';
|
||||
import multiDeployRollupFixtures from './fixtures/multiDeployRollup.json';
|
||||
import timeseriesFixtures from './fixtures/singleDeployTs.json';
|
||||
import { processRollupMetrics, processTimeseriesMetrics } from '../js/components/util/MetricUtils.js';
|
||||
import { processRollupMetrics } from '../js/components/util/MetricUtils.js';
|
||||
|
||||
describe('MetricUtils', () => {
|
||||
describe('processTsWithLatencyBreakdown', () => {
|
||||
it('Converts raw metrics to plottable timeseries data', () => {
|
||||
let deployName = 'test/potato3';
|
||||
let histograms = ['P50', 'P95', 'P99'];
|
||||
let result = processTimeseriesMetrics(timeseriesFixtures.metrics, "targetDeploy")[deployName];
|
||||
|
||||
_.each(histograms, quantile => {
|
||||
_.each(result["LATENCY"][quantile], datum => {
|
||||
expect(datum.timestamp).not.to.be.empty;
|
||||
expect(datum.value).not.to.be.empty;
|
||||
expect(datum.label).to.equal(quantile);
|
||||
});
|
||||
});
|
||||
|
||||
_.each(result["REQUEST_RATE"], datum => {
|
||||
expect(datum.timestamp).not.to.be.empty;
|
||||
expect(datum.value).to.exist;
|
||||
});
|
||||
|
||||
_.each(result["SUCCESS_RATE"], datum => {
|
||||
expect(datum.timestamp).not.to.be.empty;
|
||||
expect(datum.value).to.exist;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('processMetrics', () => {
|
||||
it('Extracts the values from the nested raw rollup response', () => {
|
||||
let result = processRollupMetrics(deployRollupFixtures.metrics, "targetDeploy");
|
||||
describe('processRollupMetrics', () => {
|
||||
it('Extracts deploy metrics from a single response', () => {
|
||||
let result = processRollupMetrics(deployRollupFixtures);
|
||||
let expectedResult = [
|
||||
{
|
||||
name: 'test/potato3',
|
||||
requestRate: 6.1,
|
||||
successRate: 0.3770491803278688,
|
||||
name: 'emojivoto/voting',
|
||||
requestRate: 2.5,
|
||||
successRate: 0.9,
|
||||
latency: {
|
||||
P95: 953,
|
||||
P99: 990,
|
||||
P50: 537
|
||||
},
|
||||
added: true
|
||||
P50: 1,
|
||||
P95: 2,
|
||||
P99: 7
|
||||
}
|
||||
}
|
||||
];
|
||||
expect(result).to.deep.equal(expectedResult);
|
||||
});
|
||||
|
||||
it('Extracts the specified entity metrics in the rollup', () => {
|
||||
let helloResult = processRollupMetrics(multiDeployRollupFixtures.metrics, "targetDeploy");
|
||||
let helloPodResult = processRollupMetrics(multiDeployRollupFixtures.metrics, "targetPod");
|
||||
let meshResult = processRollupMetrics(multiDeployRollupFixtures.metrics, "component");
|
||||
let pathResult = processRollupMetrics(multiDeployRollupFixtures.metrics, "path");
|
||||
|
||||
expect(helloResult).to.have.length(1);
|
||||
expect(helloPodResult).to.have.length(1);
|
||||
expect(meshResult).to.have.length(1);
|
||||
expect(pathResult).to.have.length(1);
|
||||
|
||||
expect(helloResult[0].name).to.equal("default/hello");
|
||||
expect(helloPodResult[0].name).to.equal("default/hello-12f3f-1e2aa");
|
||||
expect(meshResult[0].name).to.equal("mesh");
|
||||
expect(pathResult[0].name).to.equal("/Get/Hello");
|
||||
it('Extracts and sorts multiple deploys from a single response', () => {
|
||||
let result = processRollupMetrics(multiDeployRollupFixtures);
|
||||
expect(result).to.have.length(4);
|
||||
expect(result[0].name).to.equal("emojivoto/emoji");
|
||||
expect(result[1].name).to.equal("emojivoto/vote-bot");
|
||||
expect(result[2].name).to.equal("emojivoto/voting");
|
||||
expect(result[3].name).to.equal("emojivoto/web");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
import Adapter from 'enzyme-adapter-react-16';
|
||||
import Enzyme from 'enzyme';
|
||||
import { expect } from 'chai';
|
||||
import { mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
import ResourceMetricsOverview from '../js/components/ResourceMetricsOverview.jsx';
|
||||
|
||||
Enzyme.configure({ adapter: new Adapter() });
|
||||
|
||||
describe('ResourceMetricsOverview', () => {
|
||||
it('renders the request, success rate and latency components', () => {
|
||||
let component = mount(
|
||||
<ResourceMetricsOverview
|
||||
lastUpdated={Date.now()}
|
||||
timeseries={[]} />
|
||||
);
|
||||
|
||||
expect(component.find(".border-container").length).to.equal(2);
|
||||
expect(component.find(".line-graph").length).to.equal(3);
|
||||
expect(component.find(".current-latency").length).to.equal(1);
|
||||
});
|
||||
});
|
|
@ -79,7 +79,6 @@ describe('ServiceMesh', () => {
|
|||
component.update();
|
||||
expect(component.find("ServiceMesh")).to.have.length(1);
|
||||
expect(component.find("ConduitSpinner")).to.have.length(0);
|
||||
expect(component.find("DeploymentSummary")).to.have.length(3);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,76 +1,29 @@
|
|||
{
|
||||
"metrics": [
|
||||
{
|
||||
"name": "REQUEST_RATE",
|
||||
"metadata": {
|
||||
"targetPod": "",
|
||||
"targetDeploy": "test/potato3",
|
||||
"sourcePod": "",
|
||||
"sourceDeploy": "",
|
||||
"component": "",
|
||||
"path": ""
|
||||
},
|
||||
"datapoints": [
|
||||
{
|
||||
"value": {
|
||||
"gauge": 6.10122024404881
|
||||
},
|
||||
"timestampMs": "1513211893354"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SUCCESS_RATE",
|
||||
"metadata": {
|
||||
"targetPod": "",
|
||||
"targetDeploy": "test/potato3",
|
||||
"sourcePod": "",
|
||||
"sourceDeploy": "",
|
||||
"component": "",
|
||||
"path": ""
|
||||
},
|
||||
"datapoints": [
|
||||
{
|
||||
"value": {
|
||||
"gauge": 0.3770491803278688
|
||||
},
|
||||
"timestampMs": "1513211893530"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "LATENCY",
|
||||
"metadata": {
|
||||
"targetPod": "",
|
||||
"targetDeploy": "test/potato3",
|
||||
"sourcePod": "",
|
||||
"sourceDeploy": "",
|
||||
"component": "",
|
||||
"path": ""
|
||||
},
|
||||
"datapoints": [
|
||||
{
|
||||
"value": {
|
||||
"histogram": {
|
||||
"values": [
|
||||
{
|
||||
"label": "P95",
|
||||
"value": "953"
|
||||
},
|
||||
{
|
||||
"label": "P99",
|
||||
"value": "990"
|
||||
},
|
||||
{
|
||||
"label": "P50",
|
||||
"value": "537"
|
||||
}
|
||||
]
|
||||
"ok": {
|
||||
"statTables": [
|
||||
{
|
||||
"podGroup": {
|
||||
"rows": [
|
||||
{
|
||||
"meshedPodCount": "1",
|
||||
"resource": {
|
||||
"name": "voting",
|
||||
"namespace": "emojivoto",
|
||||
"type": "deployments"
|
||||
},
|
||||
"stats": {
|
||||
"failureCount": "15",
|
||||
"latencyMsP50": "1",
|
||||
"latencyMsP95": "2",
|
||||
"latencyMsP99": "7",
|
||||
"successCount": "135"
|
||||
},
|
||||
"timeWindow": "ONE_MIN",
|
||||
"totalPodCount": "1"
|
||||
}
|
||||
},
|
||||
"timestampMs": "1513211893983"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,263 +1,80 @@
|
|||
{
|
||||
"metrics": [
|
||||
{
|
||||
"name": "REQUEST_RATE",
|
||||
"metadata": {
|
||||
"targetPod": "",
|
||||
"targetDeploy": "default/hello",
|
||||
"sourcePod": "",
|
||||
"sourceDeploy": "",
|
||||
"component": "",
|
||||
"path": ""
|
||||
},
|
||||
"datapoints": [
|
||||
{
|
||||
"value": {
|
||||
"gauge": 225.615160934523212
|
||||
},
|
||||
"timestampMs": "1513279247987"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "REQUEST_RATE",
|
||||
"metadata": {
|
||||
"targetPod": "default/hello-12f3f-1e2aa",
|
||||
"targetDeploy": "",
|
||||
"sourcePod": "",
|
||||
"sourceDeploy": "",
|
||||
"component": "",
|
||||
"path": ""
|
||||
},
|
||||
"datapoints": [
|
||||
{
|
||||
"value": {
|
||||
"gauge": 102024.5121828938132955
|
||||
},
|
||||
"timestampMs": "1513279247987"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "REQUEST_RATE",
|
||||
"metadata": {
|
||||
"targetPod": "",
|
||||
"targetDeploy": "",
|
||||
"sourcePod": "",
|
||||
"sourceDeploy": "",
|
||||
"component": "mesh",
|
||||
"path": ""
|
||||
},
|
||||
"datapoints": [
|
||||
{
|
||||
"value": {
|
||||
"gauge": 104.913265817707811
|
||||
},
|
||||
"timestampMs": "1513279247987"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "REQUEST_RATE",
|
||||
"metadata": {
|
||||
"targetPod": "",
|
||||
"targetDeploy": "",
|
||||
"sourcePod": "",
|
||||
"sourceDeploy": "",
|
||||
"component": "",
|
||||
"path": "/Get/Hello"
|
||||
},
|
||||
"datapoints": [
|
||||
{
|
||||
"value": {
|
||||
"gauge": 7.821117015943046
|
||||
},
|
||||
"timestampMs": "1513279247987"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SUCCESS_RATE",
|
||||
"metadata": {
|
||||
"targetPod": "",
|
||||
"targetDeploy": "default/hello",
|
||||
"sourcePod": "",
|
||||
"sourceDeploy": "",
|
||||
"component": "",
|
||||
"path": ""
|
||||
},
|
||||
"datapoints": [
|
||||
{
|
||||
"value": {
|
||||
"gauge": 0.3035714285714286
|
||||
},
|
||||
"timestampMs": "1513279248020"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SUCCESS_RATE",
|
||||
"metadata": {
|
||||
"targetPod": "default/hello-12f3f-1e2aa",
|
||||
"targetDeploy": "",
|
||||
"sourcePod": "",
|
||||
"sourceDeploy": "",
|
||||
"component": "",
|
||||
"path": ""
|
||||
},
|
||||
"datapoints": [
|
||||
{
|
||||
"value": {
|
||||
"gauge": 0.9597142857142857
|
||||
},
|
||||
"timestampMs": "1513279248020"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SUCCESS_RATE",
|
||||
"metadata": {
|
||||
"targetPod": "",
|
||||
"targetDeploy": "",
|
||||
"sourcePod": "",
|
||||
"sourceDeploy": "",
|
||||
"component": "mesh",
|
||||
"path": ""
|
||||
},
|
||||
"datapoints": [
|
||||
{
|
||||
"value": {
|
||||
"gauge": 0.99999333333333333
|
||||
},
|
||||
"timestampMs": "1513279248020"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SUCCESS_RATE",
|
||||
"metadata": {
|
||||
"targetPod": "",
|
||||
"targetDeploy": "",
|
||||
"sourcePod": "",
|
||||
"sourceDeploy": "",
|
||||
"component": "",
|
||||
"path": "/Get/Hello"
|
||||
},
|
||||
"datapoints": []
|
||||
},
|
||||
{
|
||||
"name": "LATENCY",
|
||||
"metadata": {
|
||||
"targetPod": "",
|
||||
"targetDeploy": "default/hello",
|
||||
"sourcePod": "",
|
||||
"sourceDeploy": "",
|
||||
"component": "",
|
||||
"path": ""
|
||||
},
|
||||
"datapoints": [
|
||||
{
|
||||
"value": {
|
||||
"histogram": {
|
||||
"values": [
|
||||
{
|
||||
"label": "P50",
|
||||
"value": "510"
|
||||
},
|
||||
{
|
||||
"label": "P95",
|
||||
"value": "951"
|
||||
},
|
||||
{
|
||||
"label": "P99",
|
||||
"value": "990"
|
||||
}
|
||||
]
|
||||
"ok": {
|
||||
"statTables": [
|
||||
{
|
||||
"podGroup": {
|
||||
"rows": [
|
||||
{
|
||||
"meshedPodCount": "1",
|
||||
"resource": {
|
||||
"name": "voting",
|
||||
"namespace": "emojivoto",
|
||||
"type": "deployments"
|
||||
},
|
||||
"stats": {
|
||||
"failureCount": "10",
|
||||
"latencyMsP50": "1",
|
||||
"latencyMsP95": "1",
|
||||
"latencyMsP99": "2",
|
||||
"successCount": "56"
|
||||
},
|
||||
"timeWindow": "ONE_MIN",
|
||||
"totalPodCount": "1"
|
||||
},
|
||||
{
|
||||
"meshedPodCount": "1",
|
||||
"resource": {
|
||||
"name": "emoji",
|
||||
"namespace": "emojivoto",
|
||||
"type": "deployments"
|
||||
},
|
||||
"stats": {
|
||||
"failureCount": "0",
|
||||
"latencyMsP50": "1",
|
||||
"latencyMsP95": "1",
|
||||
"latencyMsP99": "2",
|
||||
"successCount": "124"
|
||||
},
|
||||
"timeWindow": "ONE_MIN",
|
||||
"totalPodCount": "1"
|
||||
},
|
||||
{
|
||||
"meshedPodCount": "1",
|
||||
"resource": {
|
||||
"name": "vote-bot",
|
||||
"namespace": "emojivoto",
|
||||
"type": "deployments"
|
||||
},
|
||||
"stats": {
|
||||
"failureCount": "0",
|
||||
"latencyMsP50": "1",
|
||||
"latencyMsP95": "1",
|
||||
"latencyMsP99": "1",
|
||||
"successCount": "6"
|
||||
},
|
||||
"timeWindow": "ONE_MIN",
|
||||
"totalPodCount": "1"
|
||||
},
|
||||
{
|
||||
"meshedPodCount": "1",
|
||||
"resource": {
|
||||
"name": "web",
|
||||
"namespace": "emojivoto",
|
||||
"type": "deployments"
|
||||
},
|
||||
"stats": {
|
||||
"failureCount": "11",
|
||||
"latencyMsP50": "3",
|
||||
"latencyMsP95": "10",
|
||||
"latencyMsP99": "18",
|
||||
"successCount": "113"
|
||||
},
|
||||
"timeWindow": "ONE_MIN",
|
||||
"totalPodCount": "1"
|
||||
}
|
||||
},
|
||||
"timestampMs": "1513279248043"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "LATENCY",
|
||||
"metadata": {
|
||||
"targetPod": "default/hello-12f3f-1e2aa",
|
||||
"targetDeploy": "",
|
||||
"sourcePod": "",
|
||||
"sourceDeploy": "",
|
||||
"component": "",
|
||||
"path": ""
|
||||
},
|
||||
"datapoints": []
|
||||
},
|
||||
{
|
||||
"name": "LATENCY",
|
||||
"metadata": {
|
||||
"targetPod": "",
|
||||
"targetDeploy": "",
|
||||
"sourcePod": "",
|
||||
"sourceDeploy": "",
|
||||
"component": "mesh",
|
||||
"path": ""
|
||||
},
|
||||
"datapoints": [
|
||||
{
|
||||
"value": {
|
||||
"histogram": {
|
||||
"values": [
|
||||
{
|
||||
"label": "P50",
|
||||
"value": "481"
|
||||
},
|
||||
{
|
||||
"label": "P95",
|
||||
"value": "946"
|
||||
},
|
||||
{
|
||||
"label": "P99",
|
||||
"value": "989"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"timestampMs": "1513279248043"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "LATENCY",
|
||||
"metadata": {
|
||||
"targetPod": "",
|
||||
"targetDeploy": "",
|
||||
"sourcePod": "",
|
||||
"sourceDeploy": "",
|
||||
"component": "",
|
||||
"path": "/Get/Hello"
|
||||
},
|
||||
"datapoints": [
|
||||
{
|
||||
"value": {
|
||||
"histogram": {
|
||||
"values": [
|
||||
{
|
||||
"label": "P50",
|
||||
"value": "555"
|
||||
},
|
||||
{
|
||||
"label": "P95",
|
||||
"value": "666"
|
||||
},
|
||||
{
|
||||
"label": "P99",
|
||||
"value": "777"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"timestampMs": "1513279248043"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -19,21 +19,8 @@ type (
|
|||
)
|
||||
|
||||
var (
|
||||
defaultMetricTimeWindow = pb.TimeWindow_ONE_MIN
|
||||
defaultMetricAggregationType = pb.AggregationType_TARGET_DEPLOY
|
||||
|
||||
allMetrics = []pb.MetricName{
|
||||
pb.MetricName_REQUEST_RATE,
|
||||
pb.MetricName_SUCCESS_RATE,
|
||||
pb.MetricName_LATENCY,
|
||||
}
|
||||
|
||||
meshMetrics = []pb.MetricName{
|
||||
pb.MetricName_REQUEST_RATE,
|
||||
pb.MetricName_SUCCESS_RATE,
|
||||
}
|
||||
|
||||
pbMarshaler = jsonpb.Marshaler{EmitDefaults: true}
|
||||
defaultResourceType = "deployments"
|
||||
pbMarshaler = jsonpb.Marshaler{EmitDefaults: true}
|
||||
)
|
||||
|
||||
func renderJsonError(w http.ResponseWriter, err error, status int) {
|
||||
|
@ -72,80 +59,6 @@ func (h *handler) handleApiVersion(w http.ResponseWriter, req *http.Request, p h
|
|||
renderJson(w, resp)
|
||||
}
|
||||
|
||||
func validateMetricParams(metricNameParam, aggParam, timeWindowParam string) (
|
||||
metrics []pb.MetricName,
|
||||
groupBy pb.AggregationType,
|
||||
window pb.TimeWindow,
|
||||
err error,
|
||||
) {
|
||||
groupBy = defaultMetricAggregationType
|
||||
if aggParam != "" {
|
||||
groupBy, err = util.GetAggregationType(aggParam)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
metrics = allMetrics
|
||||
if metricNameParam != "" {
|
||||
var requestedMetricName pb.MetricName
|
||||
requestedMetricName, err = util.GetMetricName(metricNameParam)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
metrics = []pb.MetricName{requestedMetricName}
|
||||
} else if groupBy == pb.AggregationType_MESH {
|
||||
metrics = meshMetrics
|
||||
}
|
||||
|
||||
window = defaultMetricTimeWindow
|
||||
if timeWindowParam != "" {
|
||||
var requestedWindow pb.TimeWindow
|
||||
requestedWindow, err = util.GetWindow(timeWindowParam)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
window = requestedWindow
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (h *handler) handleApiMetrics(w http.ResponseWriter, req *http.Request, p httprouter.Params) {
|
||||
metricNameParam := req.FormValue("metric")
|
||||
timeWindowParam := req.FormValue("window")
|
||||
aggParam := req.FormValue("aggregation")
|
||||
timeseries := req.FormValue("timeseries") == "true"
|
||||
|
||||
filterBy := pb.MetricMetadata{
|
||||
TargetDeploy: req.FormValue("target_deploy"),
|
||||
SourceDeploy: req.FormValue("source_deploy"),
|
||||
Component: req.FormValue("component"),
|
||||
}
|
||||
|
||||
metrics, groupBy, window, err := validateMetricParams(metricNameParam, aggParam, timeWindowParam)
|
||||
if err != nil {
|
||||
renderJsonError(w, err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
metricsRequest := &pb.MetricRequest{
|
||||
Metrics: metrics,
|
||||
Window: window,
|
||||
FilterBy: &filterBy,
|
||||
GroupBy: groupBy,
|
||||
Summarize: !timeseries,
|
||||
}
|
||||
|
||||
metricsResponse, err := h.apiClient.Stat(req.Context(), metricsRequest)
|
||||
if err != nil {
|
||||
renderJsonError(w, err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
renderJsonPb(w, metricsResponse)
|
||||
}
|
||||
|
||||
func (h *handler) handleApiPods(w http.ResponseWriter, req *http.Request, p httprouter.Params) {
|
||||
pods, err := h.apiClient.ListPods(req.Context(), &pb.Empty{})
|
||||
if err != nil {
|
||||
|
@ -170,6 +83,11 @@ func (h *handler) handleApiStat(w http.ResponseWriter, req *http.Request, p http
|
|||
OutFromNamespace: req.FormValue("out_from_namespace"),
|
||||
}
|
||||
|
||||
// default to returning deployment stats
|
||||
if requestParams.ResourceType == "" {
|
||||
requestParams.ResourceType = defaultResourceType
|
||||
}
|
||||
|
||||
statRequest, err := util.BuildStatSummaryRequest(requestParams)
|
||||
if err != nil {
|
||||
renderJsonError(w, err, http.StatusInternalServerError)
|
||||
|
|
|
@ -3,7 +3,6 @@ package srv
|
|||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
@ -54,45 +53,3 @@ func TestHandleApiVersion(t *testing.T) {
|
|||
t.Errorf("Expected to find: %+v", expectedVersionJson)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleApiMetrics(t *testing.T) {
|
||||
mockApiClient := &public.MockConduitApiClient{
|
||||
MetricResponseToReturn: &pb.MetricResponse{},
|
||||
}
|
||||
server := FakeServer()
|
||||
|
||||
handler := &handler{
|
||||
render: server.RenderTemplate,
|
||||
apiClient: mockApiClient,
|
||||
}
|
||||
|
||||
// test that it returns an empty metrics response
|
||||
recorder := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/api/metrics", nil)
|
||||
req.Form = url.Values{
|
||||
"target": []string{"hello"},
|
||||
"metric": []string{"requests"},
|
||||
}
|
||||
handler.handleApiMetrics(recorder, req, httprouter.Params{})
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Errorf("Incorrect StatusCode: %+v", recorder.Code)
|
||||
t.Errorf("Expected %+v", http.StatusOK)
|
||||
}
|
||||
|
||||
header := http.Header{
|
||||
"Content-Type": []string{"application/json"},
|
||||
}
|
||||
if !reflect.DeepEqual(recorder.Header(), header) {
|
||||
t.Errorf("Incorrect headers: %+v", recorder.Header())
|
||||
t.Errorf("Expected: %+v", header)
|
||||
}
|
||||
|
||||
jsonResult := recorder.Body.String()
|
||||
expectedJson := "{\"metrics\":[]}"
|
||||
|
||||
if !strings.Contains(jsonResult, expectedJson) {
|
||||
t.Errorf("incorrect api result")
|
||||
t.Errorf("Got: %+v", jsonResult)
|
||||
t.Errorf("Expected to find: %+v", expectedJson)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,7 +82,6 @@ func NewServer(addr, templateDir, staticDir, uuid, controllerNamespace, webpackD
|
|||
|
||||
// webapp routes
|
||||
server.router.GET("/", handler.handleIndex)
|
||||
server.router.GET("/deployment", handler.handleIndex)
|
||||
server.router.GET("/deployments", handler.handleIndex)
|
||||
server.router.GET("/servicemesh", handler.handleIndex)
|
||||
server.router.ServeFiles(
|
||||
|
@ -91,7 +90,6 @@ func NewServer(addr, templateDir, staticDir, uuid, controllerNamespace, webpackD
|
|||
|
||||
// webapp api routes
|
||||
server.router.GET("/api/version", handler.handleApiVersion)
|
||||
server.router.GET("/api/metrics", handler.handleApiMetrics)
|
||||
server.router.GET("/api/stat", handler.handleApiStat)
|
||||
server.router.GET("/api/pods", handler.handleApiPods)
|
||||
|
||||
|
|
Loading…
Reference in New Issue