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:
Kevin Lingerfelt 2018-04-12 17:35:03 -07:00 committed by GitHub
parent e9b209829d
commit 37434d048a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 236 additions and 3362 deletions

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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>&nbsp;<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>
);
}
}

View File

@ -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,

View File

@ -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}&nbsp;&nbsp;<i className="fa fa-external-link" />
</this.props.conduitLink>
);
}

View File

@ -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>
);
}
}

View File

@ -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>
);
}
}

View File

@ -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>&nbsp;<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}

View File

@ -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">&laquo; {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">&nbsp;</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 &raquo;</div>
<div className={`adjacent-health ${stats.outbound.health}`}>
<TrafficIndicator healthStat={stats.outbound.health} />
</div>
</Col>
</Row>
</div>
);
}
}

View File

@ -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>
);
}
}

View File

@ -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>
);
}
}

View File

@ -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}&timeseries=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();
}
}

View File

@ -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>&nbsp;<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: {

View File

@ -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>
);
}
}

View File

@ -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}&timeseries=true` :
`${metricsUrl}&timeseries=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}&timeseries=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}&timeseries=true`;
return {
ts: downstreamTimeseriesUrl,
rollup: downstreamRollupUrl
};
}
}
};

View File

@ -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
};
};

View File

@ -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>

View File

@ -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?&timeseries=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&timeseries=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');
});
});
});

View File

@ -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);
});
});
});

View File

@ -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);
});
});
});

View File

@ -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");
});
});
});

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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"
]
}
]
}
]
}
]
}
}

View File

@ -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

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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)