mirror of https://github.com/status-im/consul.git
Add metrics rendering to the new topology view. (#8858)
* Remove unused StatsCard component * Create Card and Stats contextual components with styling * Send endpoint, item, and protocol to Stats as props * WIP basic plumbing for metrics in Ember * WIP metrics data source now works for different protocols and produces reasonable mock responses * WIP sparkline component * Mostly working metrics and graphs in topology * Fix date in tooltip to actually be correct * Clean up console.log * Add loading frame and create a style sheet for Stats * Various polish fixes: - Loading state for graph - Added fake latency cookie value to test loading - If metrics provider has no series/stats for the service show something that doesn't look broken - Graph hover works right to the edge now - Stats boxes now wrap so they are either shown or not as will fit not cut off - Graph resizes when browser window size changes - Some tweaks to number formats and stat metrics to make them more compact/useful * Thread Protocol through topology model correctly * Rebuild assetfs * Fix failing tests and remove stats-card now it's changed and become different * Fix merge conflict * Update api doublt * more merge fixes * Add data-permission and id attr to Card * Run JS linter * Move things around so the tests run with everything available * Get tests passing: 1. Remove fakeLatency setTimeout (will be replaced with CONSUL_LATENCY in mocks) 2. Make sure any event handlers are removed * Make sure the Consul/scripts are available before the app * Make sure interval gets set if there is no cookie value * Upgrade mocks so we can use CONSUL_LATENCY * Fix handling of no series values from Prometheus * Update assetfs and fix a comment * Rebase and rebuild assetfs; fix tcp metric series units to be bits not bytes * Rebuild assetfs * Hide stats when provider is not configured Co-authored-by: kenia <keniavalladarez@gmail.com> Co-authored-by: John Cowen <jcowen@hashicorp.com>
This commit is contained in:
parent
52fd707f3d
commit
27048a0612
File diff suppressed because one or more lines are too long
|
@ -1,9 +0,0 @@
|
||||||
{{yield}}
|
|
||||||
<div class="stats-card">
|
|
||||||
<header>
|
|
||||||
<YieldSlot @name="mini-stat">{{yield}}</YieldSlot>
|
|
||||||
<YieldSlot @name="header">{{yield}}</YieldSlot>
|
|
||||||
<YieldSlot @name="icon">{{yield}}</YieldSlot>
|
|
||||||
</header>
|
|
||||||
<YieldSlot @name="body">{{yield}}</YieldSlot>
|
|
||||||
</div>
|
|
|
@ -1,4 +0,0 @@
|
||||||
import Component from '@ember/component';
|
|
||||||
import Slotted from 'block-slots';
|
|
||||||
|
|
||||||
export default Component.extend(Slotted, {});
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
{{#each @items as |item|}}
|
||||||
|
<div
|
||||||
|
class="card"
|
||||||
|
data-permission={{service/intention-permissions item}}
|
||||||
|
id="{{item.Namespace}}{{item.Name}}"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{{item.Name}}
|
||||||
|
</p>
|
||||||
|
<div class="details">
|
||||||
|
{{#if (and (and nspace (env 'CONSUL_NSPACES_ENABLED')) @type)}}
|
||||||
|
<dl class="nspace">
|
||||||
|
<dt>
|
||||||
|
<Tooltip>
|
||||||
|
Namespace
|
||||||
|
</Tooltip>
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{item.Namespace}}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
{{/if}}
|
||||||
|
{{#if (eq item.Datacenter @dc)}}
|
||||||
|
{{#let (service/health-percentage item) as |percentage|}}
|
||||||
|
{{#if (not-eq percentage.passing 0)}}
|
||||||
|
<span class="passing">{{percentage.passing}}%</span>
|
||||||
|
{{/if}}
|
||||||
|
{{#if (not-eq percentage.warning 0)}}
|
||||||
|
<span class="warning">{{percentage.warning}}%</span>
|
||||||
|
{{/if}}
|
||||||
|
{{#if (not-eq percentage.critical 0)}}
|
||||||
|
<span class="critical">{{percentage.critical}}%</span>
|
||||||
|
{{/if}}
|
||||||
|
{{/let}}
|
||||||
|
{{else}}
|
||||||
|
<dl class="health">
|
||||||
|
<dt>
|
||||||
|
<Tooltip>
|
||||||
|
We are unable to determine the health status of services in remote datacenters.
|
||||||
|
</Tooltip>
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
Health
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{#if @hasMetricsProvider }}
|
||||||
|
{{#if (eq @type 'upstream')}}
|
||||||
|
<TopologyMetrics::Stats @endpoint='upstream-summary-for-service' @service={{@service}} @item={{item.Name}} />
|
||||||
|
{{else}}
|
||||||
|
<TopologyMetrics::Stats @endpoint='downstream-summary-for-service' @service={{@service}} @item={{item.Name}} />
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
|
@ -11,48 +11,23 @@
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{{#each @downstreams as |downstream|}}
|
<TopologyMetrics::Card
|
||||||
<div class="card"
|
@items={{@downstreams}}
|
||||||
data-permission={{service/intention-permissions downstream}}
|
@service={{@service.Service.Service}}
|
||||||
id="{{downstream.Namespace}}{{downstream.Name}}"
|
@dc={{@dc}}
|
||||||
>
|
@hasMetricsProvider={{this.hasMetricsProvider}}
|
||||||
<p>
|
/>
|
||||||
{{downstream.Name}}
|
|
||||||
</p>
|
|
||||||
<div class="detail">
|
|
||||||
{{#if (env 'CONSUL_NSPACES_ENABLED')}}
|
|
||||||
<dl class="nspace">
|
|
||||||
<dt>
|
|
||||||
<Tooltip>
|
|
||||||
Namespace
|
|
||||||
</Tooltip>
|
|
||||||
</dt>
|
|
||||||
<dd>
|
|
||||||
{{downstream.Namespace}}
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
{{/if}}
|
|
||||||
{{#let (service/health-percentage downstream) as |percentage|}}
|
|
||||||
{{#if (not-eq percentage.passing 0)}}
|
|
||||||
<span class="passing">{{percentage.passing}}%</span>
|
|
||||||
{{/if}}
|
|
||||||
{{#if (not-eq percentage.warning 0)}}
|
|
||||||
<span class="warning">{{percentage.warning}}%</span>
|
|
||||||
{{/if}}
|
|
||||||
{{#if (not-eq percentage.critical 0)}}
|
|
||||||
<span class="critical">{{percentage.critical}}%</span>
|
|
||||||
{{/if}}
|
|
||||||
{{/let}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<div id="metrics-container">
|
<div id="metrics-container">
|
||||||
<div>
|
<div>
|
||||||
{{@service.Service.Service}}
|
{{@service.Service.Service}}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{{#if this.hasMetricsProvider }}
|
||||||
|
<TopologyMetrics::Series @service={{@service.Service.Service}} @protocol={{@protocol}} />
|
||||||
|
<TopologyMetrics::Stats @endpoint='summary-for-service' @service={{@service.Service.Service}} @protocol={{@protocol}} />
|
||||||
|
{{/if}}
|
||||||
|
<div class="link">
|
||||||
{{#if @metricsHref}}
|
{{#if @metricsHref}}
|
||||||
<a class="metrics-link" href={{@metricsHref}} target="_blank" rel="noopener noreferrer">Open metrics Dashboard</a>
|
<a class="metrics-link" href={{@metricsHref}} target="_blank" rel="noopener noreferrer">Open metrics Dashboard</a>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
@ -74,54 +49,13 @@
|
||||||
{{#each-in (group-by "Datacenter" @upstreams) as |dc upstreams|}}
|
{{#each-in (group-by "Datacenter" @upstreams) as |dc upstreams|}}
|
||||||
<div id="upstream-container">
|
<div id="upstream-container">
|
||||||
<p>{{dc}}</p>
|
<p>{{dc}}</p>
|
||||||
{{#each upstreams as |upstream|}}
|
<TopologyMetrics::Card
|
||||||
<div class="card"
|
@items={{upstreams}}
|
||||||
data-permission={{service/intention-permissions upstream}}
|
@service={{@service.Service.Service}}
|
||||||
id="{{upstream.Namespace}}{{upstream.Name}}"
|
@dc={{@dc}}
|
||||||
>
|
@type='upstream'
|
||||||
<p>
|
@hasMetricsProvider={{this.hasMetricsProvider}}
|
||||||
{{upstream.Name}}
|
/>
|
||||||
</p>
|
|
||||||
<div class="detail">
|
|
||||||
{{#if (env 'CONSUL_NSPACES_ENABLED')}}
|
|
||||||
<dl class="nspace">
|
|
||||||
<dt>
|
|
||||||
<Tooltip>
|
|
||||||
Namespace
|
|
||||||
</Tooltip>
|
|
||||||
</dt>
|
|
||||||
<dd>
|
|
||||||
{{upstream.Namespace}}
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
{{/if}}
|
|
||||||
{{#if (eq upstream.Datacenter @dc)}}
|
|
||||||
{{#let (service/health-percentage upstream) as |percentage|}}
|
|
||||||
{{#if (not-eq percentage.passing 0)}}
|
|
||||||
<span class="passing">{{percentage.passing}}%</span>
|
|
||||||
{{/if}}
|
|
||||||
{{#if (not-eq percentage.warning 0)}}
|
|
||||||
<span class="warning">{{percentage.warning}}%</span>
|
|
||||||
{{/if}}
|
|
||||||
{{#if (not-eq percentage.critical 0)}}
|
|
||||||
<span class="critical">{{percentage.critical}}%</span>
|
|
||||||
{{/if}}
|
|
||||||
{{/let}}
|
|
||||||
{{else}}
|
|
||||||
<dl class="health">
|
|
||||||
<dt>
|
|
||||||
<Tooltip>
|
|
||||||
We are unable to determine the health status of services in remote datacenters.
|
|
||||||
</Tooltip>
|
|
||||||
</dt>
|
|
||||||
<dd>
|
|
||||||
Health
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
</div>
|
||||||
{{/each-in}}
|
{{/each-in}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,14 +1,23 @@
|
||||||
import Component from '@glimmer/component';
|
import Component from '@glimmer/component';
|
||||||
import { tracked } from '@glimmer/tracking';
|
import { tracked } from '@glimmer/tracking';
|
||||||
import { action } from '@ember/object';
|
import { action } from '@ember/object';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
export default class TopologyMetrics extends Component {
|
export default class TopologyMetrics extends Component {
|
||||||
|
@service('ui-config') cfg;
|
||||||
|
|
||||||
// =attributes
|
// =attributes
|
||||||
@tracked centerDimensions;
|
@tracked centerDimensions;
|
||||||
@tracked downView;
|
@tracked downView;
|
||||||
@tracked downLines = [];
|
@tracked downLines = [];
|
||||||
@tracked upView;
|
@tracked upView;
|
||||||
@tracked upLines = [];
|
@tracked upLines = [];
|
||||||
|
@tracked hasMetricsProvider = false;
|
||||||
|
|
||||||
|
constructor(owner, args) {
|
||||||
|
super(owner, args);
|
||||||
|
this.hasMetricsProvider = !!this.cfg.get().metrics_provider
|
||||||
|
}
|
||||||
|
|
||||||
// =methods
|
// =methods
|
||||||
drawDownLines(items) {
|
drawDownLines(items) {
|
||||||
|
|
|
@ -14,10 +14,12 @@
|
||||||
#downstream-lines {
|
#downstream-lines {
|
||||||
grid-row: 1 / 3;
|
grid-row: 1 / 3;
|
||||||
grid-column: 2 / 5;
|
grid-column: 2 / 5;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
#upstream-lines {
|
#upstream-lines {
|
||||||
grid-row: 1 / 3;
|
grid-row: 1 / 3;
|
||||||
grid-column: 6 / 9;
|
grid-column: 6 / 9;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
#upstream-column {
|
#upstream-column {
|
||||||
grid-row: 1 / 3;
|
grid-row: 1 / 3;
|
||||||
|
@ -51,8 +53,10 @@
|
||||||
}
|
}
|
||||||
#upstream-container .card,
|
#upstream-container .card,
|
||||||
#downstream-container .card {
|
#downstream-container .card {
|
||||||
padding: 12px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
p {
|
p {
|
||||||
|
padding: 12px 12px 0 12px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 0 !important;
|
margin-bottom: 0 !important;
|
||||||
|
@ -75,6 +79,10 @@
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.details {
|
||||||
|
padding: 0 12px 12px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metrics Container
|
// Metrics Container
|
||||||
|
@ -88,7 +96,7 @@
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
div:nth-child(2) {
|
.link {
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
a::before {
|
a::before {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
<DataSource
|
||||||
|
@src={{uri nspace dc 'metrics' 'summary-for-service' @service @protocol}}
|
||||||
|
@onchange={{action 'change'}} />
|
||||||
|
|
||||||
|
{{on-window 'resize' (action 'redraw')}}
|
||||||
|
|
||||||
|
<div class="sparkline-wrapper">
|
||||||
|
<div class="tooltip">
|
||||||
|
<div class="sparkline-time">Timestamp</div>
|
||||||
|
</div>
|
||||||
|
<div class="sparkline-loader"><span>Loading Metrics</span></div>
|
||||||
|
<svg class="sparkline"></svg>
|
||||||
|
</div>
|
||||||
|
|
|
@ -0,0 +1,234 @@
|
||||||
|
import Component from '@ember/component';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import Calendar from 'dayjs/plugin/calendar';
|
||||||
|
|
||||||
|
import { select, event, mouse } from 'd3-selection';
|
||||||
|
import { scaleLinear, scaleTime, scaleOrdinal } from 'd3-scale';
|
||||||
|
import { schemeTableau10 } from 'd3-scale-chromatic';
|
||||||
|
import { area, stack, stackOrderReverse } from 'd3-shape';
|
||||||
|
import { max, extent, bisector } from 'd3-array';
|
||||||
|
|
||||||
|
dayjs.extend(Calendar);
|
||||||
|
|
||||||
|
function niceTimeWithSeconds(d) {
|
||||||
|
return dayjs(d).calendar(null, {
|
||||||
|
sameDay: '[Today at] h:mm:ss A',
|
||||||
|
lastDay: '[Yesterday at] h:mm:ss A',
|
||||||
|
lastWeek: '[Last] dddd at h:mm:ss A',
|
||||||
|
sameElse: 'MMM DD at h:mm:ss A',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Component.extend({
|
||||||
|
data: null,
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
redraw: function(evt) {
|
||||||
|
this.drawGraphs();
|
||||||
|
},
|
||||||
|
change: function(evt) {
|
||||||
|
this.data = evt.data;
|
||||||
|
this.element.querySelector('.sparkline-loader').style.display = 'none';
|
||||||
|
this.drawGraphs();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
drawGraphs: function() {
|
||||||
|
if (!this.data.series) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let svg = (this.svg = select(this.element.querySelector('svg.sparkline')));
|
||||||
|
svg.on('mouseover mousemove mouseout', null);
|
||||||
|
svg.selectAll('path').remove();
|
||||||
|
svg.selectAll('rect').remove();
|
||||||
|
|
||||||
|
let bb = svg.node().getBoundingClientRect();
|
||||||
|
let w = bb.width;
|
||||||
|
let h = bb.height;
|
||||||
|
|
||||||
|
// To be safe, filter any series that actually have no data points. This can
|
||||||
|
// happen thanks to our current provider contract allowing empty arrays for
|
||||||
|
// series data if there is no value.
|
||||||
|
//
|
||||||
|
// TODO(banks): switch series provider data to be a single array with series
|
||||||
|
// values as properties as we need below to enforce sensible alignment of
|
||||||
|
// timestamps and explicit summing expectations.
|
||||||
|
let series = ((this.data || {}).series || []).filter(s => s.data.length > 0);
|
||||||
|
|
||||||
|
if (series.length == 0) {
|
||||||
|
// Put the graph in an error state that might get fixed if metrics show up
|
||||||
|
// on next poll.
|
||||||
|
let loader = this.element.querySelector('.sparkline-loader');
|
||||||
|
loader.innerHTML = 'No Metrics Available';
|
||||||
|
loader.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill the timestamps for x axis.
|
||||||
|
let data = series[0].data.map(d => {
|
||||||
|
return { time: d[0] };
|
||||||
|
});
|
||||||
|
let keys = [];
|
||||||
|
// Initialize zeros
|
||||||
|
let summed = this.data.series[0].data.map(d => 0);
|
||||||
|
|
||||||
|
for (var i = 0; i < series.length; i++) {
|
||||||
|
let s = series[i];
|
||||||
|
// Attach the value as a new field to the data grid.
|
||||||
|
s.data.map((d, idx) => {
|
||||||
|
data[idx][s.label] = d[1];
|
||||||
|
summed[idx] += d[1];
|
||||||
|
});
|
||||||
|
keys.push(s.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
let st = stack()
|
||||||
|
.keys(keys)
|
||||||
|
.order(stackOrderReverse);
|
||||||
|
|
||||||
|
let stackData = st(data);
|
||||||
|
|
||||||
|
let x = scaleTime()
|
||||||
|
.domain(extent(data, d => d.time))
|
||||||
|
.range([0, w]);
|
||||||
|
|
||||||
|
let y = scaleLinear()
|
||||||
|
.domain([0, max(summed)])
|
||||||
|
.range([h, 0]);
|
||||||
|
|
||||||
|
let a = area()
|
||||||
|
.x(d => x(d.data.time))
|
||||||
|
.y1(d => y(d[0]))
|
||||||
|
.y0(d => y(d[1]));
|
||||||
|
|
||||||
|
// Use the grey/red we prefer by default but have more colors available in
|
||||||
|
// case user adds extra series with a custom provider.
|
||||||
|
let colorScheme = ['#DCE0E6', '#C73445'].concat(schemeTableau10);
|
||||||
|
let color = scaleOrdinal(colorScheme).domain(keys);
|
||||||
|
|
||||||
|
svg
|
||||||
|
.selectAll('path')
|
||||||
|
.data(stackData)
|
||||||
|
.join('path')
|
||||||
|
.attr('fill', ({ key }) => color(key))
|
||||||
|
.attr('stroke', ({ key }) => color(key))
|
||||||
|
.attr('d', a);
|
||||||
|
|
||||||
|
let cursor = svg
|
||||||
|
.append('rect')
|
||||||
|
.attr('class', 'cursor')
|
||||||
|
.style('visibility', 'hidden')
|
||||||
|
.attr('width', 1)
|
||||||
|
.attr('height', h)
|
||||||
|
.attr('x', 0)
|
||||||
|
.attr('y', 0);
|
||||||
|
|
||||||
|
let tooltip = select(this.element.querySelector('.tooltip'));
|
||||||
|
tooltip.selectAll('.sparkline-tt-legend').remove();
|
||||||
|
|
||||||
|
for (var k of keys) {
|
||||||
|
let legend = tooltip.append('div').attr('class', 'sparkline-tt-legend');
|
||||||
|
|
||||||
|
legend
|
||||||
|
.append('div')
|
||||||
|
.attr('class', 'sparkline-tt-legend-color')
|
||||||
|
.style('background-color', color(k));
|
||||||
|
|
||||||
|
legend
|
||||||
|
.append('span')
|
||||||
|
.text(k + ': ')
|
||||||
|
.append('span')
|
||||||
|
.attr('class', 'sparkline-tt-legend-value');
|
||||||
|
}
|
||||||
|
|
||||||
|
let tipVals = tooltip.selectAll('.sparkline-tt-legend-value');
|
||||||
|
|
||||||
|
let self = this;
|
||||||
|
svg
|
||||||
|
.on('mouseover', function() {
|
||||||
|
tooltip.style('visibility', 'visible');
|
||||||
|
cursor.style('visibility', 'visible');
|
||||||
|
// We update here since we might redraw the graph with user's cursor
|
||||||
|
// stationary over it. If that happens mouseover fires but not
|
||||||
|
// mousemove but the tooltip and cursor are wrong (based on old data).
|
||||||
|
self.updateTooltip(event, data, stackData, keys, x, tooltip, tipVals, cursor);
|
||||||
|
})
|
||||||
|
.on('mousemove', function(d, i) {
|
||||||
|
self.updateTooltip(event, data, stackData, keys, x, tooltip, tipVals, cursor);
|
||||||
|
})
|
||||||
|
.on('mouseout', function() {
|
||||||
|
tooltip.style('visibility', 'hidden');
|
||||||
|
cursor.style('visibility', 'hidden');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
willDestroyElement: function() {
|
||||||
|
this._super(...arguments);
|
||||||
|
if (typeof this.svg !== 'undefined') {
|
||||||
|
this.svg.on('mouseover mousemove mouseout', null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateTooltip: function(event, data, stackData, keys, x, tooltip, tipVals, cursor) {
|
||||||
|
let [mouseX] = mouse(event.currentTarget);
|
||||||
|
cursor.attr('x', mouseX);
|
||||||
|
|
||||||
|
let mouseTime = x.invert(mouseX);
|
||||||
|
var bisectTime = bisector(function(d) {
|
||||||
|
return d.time;
|
||||||
|
}).left;
|
||||||
|
let tipIdx = bisectTime(data, mouseTime);
|
||||||
|
|
||||||
|
tooltip
|
||||||
|
// 22 px is the correction to align the arrow on the tool tip with
|
||||||
|
// cursor.
|
||||||
|
.style('left', mouseX - 22 + 'px')
|
||||||
|
.select('.sparkline-time')
|
||||||
|
.text(niceTimeWithSeconds(mouseTime));
|
||||||
|
|
||||||
|
tipVals.nodes().forEach((n, i) => {
|
||||||
|
let val = stackData[i][tipIdx][1] - stackData[i][tipIdx][0];
|
||||||
|
select(n).text(this.formatTooltip(keys[i], val));
|
||||||
|
});
|
||||||
|
cursor.attr('x', mouseX);
|
||||||
|
},
|
||||||
|
|
||||||
|
formatTooltip: function(label, val) {
|
||||||
|
switch (label) {
|
||||||
|
case 'Data rate received':
|
||||||
|
// fallthrough
|
||||||
|
case 'Data rate transmitted':
|
||||||
|
return dataRateStr(val);
|
||||||
|
default:
|
||||||
|
return shortNumStr(val);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Duplicated in vendor/metrics-providers/prometheus.js since we want that to
|
||||||
|
// remain a standalone example of a provider that could be loaded externally.
|
||||||
|
function shortNumStr(n) {
|
||||||
|
if (n < 1e3) {
|
||||||
|
if (Number.isInteger(n)) return '' + n;
|
||||||
|
if (n >= 100) {
|
||||||
|
// Go to 3 significant figures but wrap it in Number to avoid scientific
|
||||||
|
// notation lie 2.3e+2 for 230.
|
||||||
|
return Number(n.toPrecision(3));
|
||||||
|
}
|
||||||
|
if (n < 1) {
|
||||||
|
// Very small numbers show with limited precision to prevent long string
|
||||||
|
// of 0.000000.
|
||||||
|
return Number(n.toFixed(2));
|
||||||
|
} else {
|
||||||
|
// Two sig figs is enough below this
|
||||||
|
return Number(n.toPrecision(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (n >= 1e3 && n < 1e6) return +(n / 1e3).toPrecision(3) + 'k';
|
||||||
|
if (n >= 1e6 && n < 1e9) return +(n / 1e6).toPrecision(3) + 'm';
|
||||||
|
if (n >= 1e9 && n < 1e12) return +(n / 1e9).toPrecision(3) + 'g';
|
||||||
|
if (n >= 1e12) return +(n / 1e12).toFixed(0) + 't';
|
||||||
|
}
|
||||||
|
|
||||||
|
function dataRateStr(n) {
|
||||||
|
return shortNumStr(n) + 'bps';
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
@import './skin';
|
||||||
|
@import './layout';
|
|
@ -0,0 +1,39 @@
|
||||||
|
#metrics-container div .sparkline-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
height: 70px;
|
||||||
|
|
||||||
|
svg.sparkline {
|
||||||
|
width: 100%;
|
||||||
|
height: 70px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
visibility: hidden;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
bottom: 78px;
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sparkline-tt-legend-color {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sparkline-loader {
|
||||||
|
font-weight: normal;
|
||||||
|
padding-top: 15px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: $gray-500;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
span::after {
|
||||||
|
@extend %with-loading-icon, %as-pseudo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
#metrics-container div .sparkline-wrapper {
|
||||||
|
svg path {
|
||||||
|
stroke-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
padding: 5px 10px 10px 10px;
|
||||||
|
font-size: 0.875em;
|
||||||
|
line-height: 1.5em;
|
||||||
|
font-weight: normal;
|
||||||
|
border: 1px solid #BAC1CC;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.05), 0px 4px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.sparkline-time {
|
||||||
|
padding: 0;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #000;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sparkline-tt-legend {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sparkline-tt-legend-color {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin: 0 5px 0 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.tooltip:before{
|
||||||
|
content:'';
|
||||||
|
display:block;
|
||||||
|
position: absolute;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
left: 15px;
|
||||||
|
bottom: -7px;
|
||||||
|
border: 1px solid #BAC1CC;
|
||||||
|
border-top: 0;
|
||||||
|
border-left: 0;
|
||||||
|
background: #fff;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -58,6 +58,9 @@
|
||||||
background-color: $red-500;
|
background-color: $red-500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
div:nth-child(3) {
|
||||||
|
border-top: 1px solid $gray-200;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metrics Container
|
// Metrics Container
|
||||||
|
@ -65,7 +68,7 @@
|
||||||
div:first-child {
|
div:first-child {
|
||||||
background-color: $white;
|
background-color: $white;
|
||||||
}
|
}
|
||||||
div:nth-child(2) {
|
.link {
|
||||||
background-color: $gray-100;
|
background-color: $gray-100;
|
||||||
a {
|
a {
|
||||||
color: $gray-700;
|
color: $gray-700;
|
||||||
|
@ -83,6 +86,9 @@
|
||||||
@extend %with-docs-mask, %as-pseudo;
|
@extend %with-docs-mask, %as-pseudo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
div:nth-child(3) {
|
||||||
|
border-top: 1px solid $gray-200;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SVG Line styling
|
// SVG Line styling
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
<DataSource
|
||||||
|
@src={{uri nspace dc 'metrics' @endpoint @service @protocol}}
|
||||||
|
@onchange={{action 'statsUpdate'}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="stats">
|
||||||
|
{{#if hasLoaded }}
|
||||||
|
{{#each stats as |stat|}}
|
||||||
|
<dl>
|
||||||
|
<dt>
|
||||||
|
{{stat.value}}
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{stat.label}}
|
||||||
|
</dd>
|
||||||
|
<Tooltip>{{{stat.desc}}}</Tooltip>
|
||||||
|
</dl>
|
||||||
|
{{else}}
|
||||||
|
<span>No Metrics Available</span>
|
||||||
|
{{/each}}
|
||||||
|
{{else}}
|
||||||
|
<span class="loader">Loading Metrics</span>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
|
@ -0,0 +1,23 @@
|
||||||
|
import Component from '@glimmer/component';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
|
||||||
|
export default class TopologyMetricsStats extends Component {
|
||||||
|
@tracked stats = null;
|
||||||
|
@tracked hasLoaded = false;
|
||||||
|
|
||||||
|
@action
|
||||||
|
statsUpdate(event) {
|
||||||
|
if (this.args.endpoint == 'summary-for-service') {
|
||||||
|
// For the main service there is just one set of stats.
|
||||||
|
this.stats = event.data.stats;
|
||||||
|
} else {
|
||||||
|
// For up/downstreams we need to pull out the stats for the service we
|
||||||
|
// represent.
|
||||||
|
this.stats = event.data.stats[this.args.item];
|
||||||
|
}
|
||||||
|
// Limit to 4 metrics for now.
|
||||||
|
this.stats = (this.stats || []).slice(0, 4);
|
||||||
|
this.hasLoaded = true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
.stats {
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: stretch;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 46px;
|
||||||
|
dl {
|
||||||
|
display:flex;
|
||||||
|
margin-bottom: 50px; // pushes wrapped metrics well out of the bounding box to hide them.
|
||||||
|
}
|
||||||
|
dt {
|
||||||
|
margin-right: 5px;
|
||||||
|
line-height: 1.5em !important;
|
||||||
|
}
|
||||||
|
dd {
|
||||||
|
color: $gray-400 !important;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
margin: 0 auto !important;
|
||||||
|
color: $gray-500;
|
||||||
|
}
|
||||||
|
span.loader::after {
|
||||||
|
@extend %with-loading-icon, %as-pseudo;
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ export default Model.extend({
|
||||||
Namespace: attr('string'),
|
Namespace: attr('string'),
|
||||||
Upstreams: attr(),
|
Upstreams: attr(),
|
||||||
Downstreams: attr(),
|
Downstreams: attr(),
|
||||||
|
Protocol: attr(),
|
||||||
meta: attr(),
|
meta: attr(),
|
||||||
Exists: computed(function() {
|
Exists: computed(function() {
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -26,6 +26,7 @@ export default Service.extend({
|
||||||
policy: service('repository/policy'),
|
policy: service('repository/policy'),
|
||||||
roles: service('repository/role'),
|
roles: service('repository/role'),
|
||||||
oidc: service('repository/oidc-provider'),
|
oidc: service('repository/oidc-provider'),
|
||||||
|
metrics: service('repository/metrics'),
|
||||||
|
|
||||||
type: service('data-source/protocols/http/blocking'),
|
type: service('data-source/protocols/http/blocking'),
|
||||||
|
|
||||||
|
@ -47,8 +48,24 @@ export default Service.extend({
|
||||||
}
|
}
|
||||||
return event;
|
return event;
|
||||||
};
|
};
|
||||||
let method, slug;
|
let method, slug, more, protocol;
|
||||||
switch (model) {
|
switch (model) {
|
||||||
|
case 'metrics':
|
||||||
|
[method, slug, ...more] = rest;
|
||||||
|
switch (method) {
|
||||||
|
case 'summary-for-service':
|
||||||
|
[protocol, ...more] = more;
|
||||||
|
find = configuration =>
|
||||||
|
repo.findServiceSummary(protocol, slug, dc, nspace, configuration);
|
||||||
|
break;
|
||||||
|
case 'upstream-summary-for-service':
|
||||||
|
find = configuration => repo.findUpstreamSummary(slug, dc, nspace, configuration);
|
||||||
|
break;
|
||||||
|
case 'downstream-summary-for-service':
|
||||||
|
find = configuration => repo.findDownstreamSummary(slug, dc, nspace, configuration);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'datacenters':
|
case 'datacenters':
|
||||||
case 'namespaces':
|
case 'namespaces':
|
||||||
find = configuration => repo.findAll(configuration);
|
find = configuration => repo.findAll(configuration);
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
import RepositoryService from 'consul-ui/services/repository';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { env } from 'consul-ui/env';
|
||||||
|
|
||||||
|
// meta is used by DataSource to configure polling. The interval controls how
|
||||||
|
// long between each poll to the metrics provider. TODO - make this configurable
|
||||||
|
// in the UI settings.
|
||||||
|
const meta = {
|
||||||
|
interval: env('CONSUL_METRICS_POLL_INTERVAL') || 10000,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RepositoryService.extend({
|
||||||
|
cfg: service('ui-config'),
|
||||||
|
|
||||||
|
init: function() {
|
||||||
|
this._super(...arguments);
|
||||||
|
const uiCfg = this.cfg.get();
|
||||||
|
// Inject whether or not the proxy is enabled as an option into the opaque
|
||||||
|
// JSON options the user provided.
|
||||||
|
const opts = uiCfg.metrics_provider_options || {};
|
||||||
|
opts.metrics_proxy_enabled = uiCfg.metrics_proxy_enabled;
|
||||||
|
// Inject the base app URL
|
||||||
|
const provider = uiCfg.metrics_provider || 'prometheus';
|
||||||
|
this.provider = window.consul.getMetricsProvider(provider, opts);
|
||||||
|
},
|
||||||
|
|
||||||
|
findServiceSummary: function(protocol, slug, dc, nspace, configuration = {}) {
|
||||||
|
const promises = [
|
||||||
|
// TODO: support namespaces in providers
|
||||||
|
this.provider.serviceRecentSummarySeries(slug, protocol, {}),
|
||||||
|
this.provider.serviceRecentSummaryStats(slug, protocol, {}),
|
||||||
|
];
|
||||||
|
return Promise.all(promises).then(function(results) {
|
||||||
|
return {
|
||||||
|
meta: meta,
|
||||||
|
series: results[0].series,
|
||||||
|
stats: results[1].stats,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
findUpstreamSummary: function(slug, dc, nspace, configuration = {}) {
|
||||||
|
return this.provider.upstreamRecentSummaryStats(slug, {}).then(function(result) {
|
||||||
|
result.meta = meta;
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
findDownstreamSummary: function(slug, dc, nspace, configuration = {}) {
|
||||||
|
return this.provider.downstreamRecentSummaryStats(slug, {}).then(function(result) {
|
||||||
|
result.meta = meta;
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,15 @@
|
||||||
|
import Service from '@ember/service';
|
||||||
|
|
||||||
|
export default Service.extend({
|
||||||
|
config: undefined,
|
||||||
|
|
||||||
|
get: function() {
|
||||||
|
if (this.config === undefined) {
|
||||||
|
// Load config from our special meta tag for now. Later it might come from
|
||||||
|
// an API instead/as well.
|
||||||
|
var meta = unescape(document.getElementsByName('consul-ui/ui_config')[0].content);
|
||||||
|
this.config = JSON.parse(meta);
|
||||||
|
}
|
||||||
|
return this.config;
|
||||||
|
},
|
||||||
|
});
|
|
@ -68,3 +68,5 @@
|
||||||
@import 'consul-ui/components/consul-intention-permission-header-list';
|
@import 'consul-ui/components/consul-intention-permission-header-list';
|
||||||
@import 'consul-ui/components/role-selector';
|
@import 'consul-ui/components/role-selector';
|
||||||
@import 'consul-ui/components/topology-metrics';
|
@import 'consul-ui/components/topology-metrics';
|
||||||
|
@import 'consul-ui/components/topology-metrics/series';
|
||||||
|
@import 'consul-ui/components/topology-metrics/stats';
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
{{#if topology}}
|
{{#if topology}}
|
||||||
<TopologyMetrics
|
<TopologyMetrics
|
||||||
@service={{items.firstObject}}
|
@service={{items.firstObject}}
|
||||||
|
@protocol={{topology.Protocol}}
|
||||||
@upstreams={{topology.Upstreams}}
|
@upstreams={{topology.Upstreams}}
|
||||||
@downstreams={{filter-by 'Datacenter' topology.Datacenter topology.Downstreams}}
|
@downstreams={{filter-by 'Datacenter' topology.Datacenter topology.Downstreams}}
|
||||||
@dc={{topology.Datacenter}}
|
@dc={{topology.Datacenter}}
|
||||||
|
|
|
@ -119,6 +119,13 @@ module.exports = function(defaults) {
|
||||||
app.import('node_modules/codemirror/mode/yaml/yaml.js', {
|
app.import('node_modules/codemirror/mode/yaml/yaml.js', {
|
||||||
outputFile: 'assets/codemirror/mode/yaml/yaml.js',
|
outputFile: 'assets/codemirror/mode/yaml/yaml.js',
|
||||||
});
|
});
|
||||||
|
// metrics-providers
|
||||||
|
app.import('vendor/metrics-providers/consul.js', {
|
||||||
|
outputFile: 'assets/metrics-providers/consul.js',
|
||||||
|
});
|
||||||
|
app.import('vendor/metrics-providers/prometheus.js', {
|
||||||
|
outputFile: 'assets/metrics-providers/prometheus.js',
|
||||||
|
});
|
||||||
let tree = app.toTree();
|
let tree = app.toTree();
|
||||||
return tree;
|
return tree;
|
||||||
};
|
};
|
||||||
|
|
|
@ -26,6 +26,13 @@ module.exports = ({ appName, environment, rootURL, config }) => `
|
||||||
appendScript('${rootURL}assets/css.escape.js');
|
appendScript('${rootURL}assets/css.escape.js');
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<script src="${rootURL}assets/metrics-providers/consul.js"></script>
|
||||||
|
<script src="${rootURL}assets/metrics-providers/prometheus.js"></script>
|
||||||
|
${
|
||||||
|
environment === 'production'
|
||||||
|
? `{{ range .ExtraScripts }} <script src="{{.}}"></script> {{ end }}`
|
||||||
|
: ``
|
||||||
|
}
|
||||||
<script src="${rootURL}assets/${appName}.js"></script>
|
<script src="${rootURL}assets/${appName}.js"></script>
|
||||||
<script>
|
<script>
|
||||||
CodeMirror.modeURL = {
|
CodeMirror.modeURL = {
|
||||||
|
@ -41,6 +48,5 @@ module.exports = ({ appName, environment, rootURL, config }) => `
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
${environment === 'production' ? `{{ range .ExtraScripts }} <script src="{{.}}"></script> {{ end }}` : ``}
|
|
||||||
${environment === 'test' ? `<script src="${rootURL}assets/tests.js"></script>` : ``}
|
${environment === 'test' ? `<script src="${rootURL}assets/tests.js"></script>` : ``}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
module.exports = ({ appName, environment, rootURL, config }) => `
|
module.exports = ({ appName, environment, rootURL, config }) => `
|
||||||
<!-- CONSUL_VERSION: ${config.CONSUL_VERSION} -->
|
<!-- CONSUL_VERSION: ${config.CONSUL_VERSION} -->
|
||||||
<meta name="consul-ui/ui_config" content="{{ jsonEncodeAndEscape .UIConfig }}" />
|
<meta name="consul-ui/ui_config" content="${
|
||||||
|
environment === 'production'
|
||||||
|
? `{{ jsonEncodeAndEscape .UIConfig }}`
|
||||||
|
: escape(`{"metrics_provider":"prometheus","metrics_proxy_enabled":true}`)
|
||||||
|
}" />
|
||||||
|
|
||||||
<link rel="icon" type="image/png" href="${rootURL}assets/favicon-32x32.png" sizes="32x32">
|
<link rel="icon" type="image/png" href="${rootURL}assets/favicon-32x32.png" sizes="32x32">
|
||||||
<link rel="icon" type="image/png" href="${rootURL}assets/favicon-16x16.png" sizes="16x16">
|
<link rel="icon" type="image/png" href="${rootURL}assets/favicon-16x16.png" sizes="16x16">
|
||||||
|
|
|
@ -56,7 +56,7 @@
|
||||||
"@ember/render-modifiers": "^1.0.2",
|
"@ember/render-modifiers": "^1.0.2",
|
||||||
"@glimmer/component": "^1.0.0",
|
"@glimmer/component": "^1.0.0",
|
||||||
"@glimmer/tracking": "^1.0.0",
|
"@glimmer/tracking": "^1.0.0",
|
||||||
"@hashicorp/consul-api-double": "^5.3.5",
|
"@hashicorp/consul-api-double": "^5.3.7",
|
||||||
"@hashicorp/ember-cli-api-double": "^3.1.0",
|
"@hashicorp/ember-cli-api-double": "^3.1.0",
|
||||||
"@xstate/fsm": "^1.4.0",
|
"@xstate/fsm": "^1.4.0",
|
||||||
"babel-eslint": "^10.0.3",
|
"babel-eslint": "^10.0.3",
|
||||||
|
@ -91,6 +91,7 @@
|
||||||
"ember-collection": "^1.0.0-alpha.9",
|
"ember-collection": "^1.0.0-alpha.9",
|
||||||
"ember-composable-helpers": "~4.0.0",
|
"ember-composable-helpers": "~4.0.0",
|
||||||
"ember-computed-style": "^0.3.0",
|
"ember-computed-style": "^0.3.0",
|
||||||
|
"ember-d3": "^0.5.1",
|
||||||
"ember-data": "~3.20.4",
|
"ember-data": "~3.20.4",
|
||||||
"ember-data-model-fragments": "5.0.0-beta.0",
|
"ember-data-model-fragments": "5.0.0-beta.0",
|
||||||
"ember-exam": "^4.0.0",
|
"ember-exam": "^4.0.0",
|
||||||
|
@ -149,5 +150,8 @@
|
||||||
"lib/commands",
|
"lib/commands",
|
||||||
"lib/startup"
|
"lib/startup"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dayjs": "^1.9.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
import { module, test } from 'qunit';
|
|
||||||
import { setupRenderingTest } from 'ember-qunit';
|
|
||||||
import { render, find } from '@ember/test-helpers';
|
|
||||||
import hbs from 'htmlbars-inline-precompile';
|
|
||||||
|
|
||||||
module('Integration | Component | stats card', function(hooks) {
|
|
||||||
setupRenderingTest(hooks);
|
|
||||||
|
|
||||||
test('it renders', async function(assert) {
|
|
||||||
// Set any properties with this.set('myProperty', 'value');
|
|
||||||
// Handle any actions with this.on('myAction', function(val) { ... });
|
|
||||||
|
|
||||||
// Template block usage:
|
|
||||||
await render(hbs`
|
|
||||||
{{#stats-card}}
|
|
||||||
{{#block-slot name='icon'}}icon{{/block-slot}}
|
|
||||||
{{#block-slot name='mini-stat'}}mini-stat{{/block-slot}}
|
|
||||||
{{#block-slot name='header'}}header{{/block-slot}}
|
|
||||||
{{#block-slot name='body'}}body{{/block-slot}}
|
|
||||||
{{/stats-card}}
|
|
||||||
`);
|
|
||||||
['icon', 'mini-stat', 'header'].forEach(item => {
|
|
||||||
assert.ok(
|
|
||||||
find('header')
|
|
||||||
.textContent.trim()
|
|
||||||
.indexOf(item) !== -1
|
|
||||||
);
|
|
||||||
});
|
|
||||||
assert.ok(
|
|
||||||
find('*')
|
|
||||||
.textContent.trim()
|
|
||||||
.indexOf('body') !== -1
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
(
|
||||||
|
function(global) {
|
||||||
|
// Current interface is these three methods.
|
||||||
|
const requiredMethods = [
|
||||||
|
'init',
|
||||||
|
'serviceRecentSummarySeries',
|
||||||
|
'serviceRecentSummaryStats',
|
||||||
|
'upstreamRecentSummaryStats',
|
||||||
|
'downstreamRecentSummaryStats',
|
||||||
|
];
|
||||||
|
|
||||||
|
// This is a bit gross but we want to support simple extensibility by
|
||||||
|
// including JS in the browser without forcing operators to setup a whole
|
||||||
|
// transpiling stack. So for now we use a window global as a thin registry for
|
||||||
|
// these providers.
|
||||||
|
class Consul {
|
||||||
|
constructor() {
|
||||||
|
this.registry = {};
|
||||||
|
this.providers = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
registerMetricsProvider(name, provider) {
|
||||||
|
// Basic check that it matches the type we expect
|
||||||
|
for (var m of requiredMethods) {
|
||||||
|
if (typeof provider[m] !== 'function') {
|
||||||
|
throw new Error(`Can't register metrics provider '${name}': missing ${m} method.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.registry[name] = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMetricsProvider(name, options) {
|
||||||
|
if (!(name in this.registry)) {
|
||||||
|
throw new Error(`Metrics Provider '${name}' is not registered.`);
|
||||||
|
}
|
||||||
|
if (name in this.providers) {
|
||||||
|
return this.providers[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.providers[name] = Object.create(this.registry[name]);
|
||||||
|
this.providers[name].init(options);
|
||||||
|
|
||||||
|
return this.providers[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
global.consul = new Consul();
|
||||||
|
|
||||||
|
}
|
||||||
|
)(window);
|
||||||
|
|
|
@ -0,0 +1,684 @@
|
||||||
|
/*eslint no-console: "off"*/
|
||||||
|
(function () {
|
||||||
|
var prometheusProvider = {
|
||||||
|
options: {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* init is called when the provide is first loaded.
|
||||||
|
*
|
||||||
|
* options.providerOptions contains any operator configured parameters
|
||||||
|
* specified in the Consul agent config that is serving the UI.
|
||||||
|
*
|
||||||
|
* options.proxy.baseURL contains the base URL if the agent has a metrics
|
||||||
|
* proxy configured. If it doesn't options.proxy will be null. The provider
|
||||||
|
* should throw an Exception (TODO: specific type?) if it requires a metrics
|
||||||
|
* proxy and one is not configured.
|
||||||
|
*/
|
||||||
|
init: function(options) {
|
||||||
|
this.options = options;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* serviceRecentSummarySeries should return time series for a recent time
|
||||||
|
* period summarizing the usage of the named service.
|
||||||
|
*
|
||||||
|
* If these metrics aren't available then empty series may be returned.
|
||||||
|
*
|
||||||
|
* The period may (later) be specified in options.startTime and
|
||||||
|
* options.endTime.
|
||||||
|
*
|
||||||
|
* The service's protocol must be given as one of Consul's supported
|
||||||
|
* protocols e.g. "tcp", "http", "http2", "grpc". If it is empty or the
|
||||||
|
* provider doesn't recognize it it should treat it as "tcp" and provide
|
||||||
|
* just basic connection stats.
|
||||||
|
*
|
||||||
|
* The expected return value is a promise which resolves to an object that
|
||||||
|
* should look like the following:
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* series: [
|
||||||
|
* {
|
||||||
|
* label: "Requests per second",
|
||||||
|
* data: [...]
|
||||||
|
* },
|
||||||
|
* ...
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Each time series' data array is simple an array of tuples with the first
|
||||||
|
* being a Date object and the second a floating point value:
|
||||||
|
*
|
||||||
|
* [[Date(1600944516286), 1234.9], [Date(1600944526286), 1234.9], ...]
|
||||||
|
*/
|
||||||
|
serviceRecentSummarySeries: function(serviceName, protocol, options) {
|
||||||
|
// Fetch time-series
|
||||||
|
var series = []
|
||||||
|
var labels = []
|
||||||
|
|
||||||
|
// Set the start and end range here so that all queries end up with
|
||||||
|
// identical time axes. Later we might accept these as options.
|
||||||
|
var now = (new Date()).getTime()/1000;
|
||||||
|
options.start = now - (15*60);
|
||||||
|
options.end = now;
|
||||||
|
|
||||||
|
if (this.hasL7Metrics(protocol)) {
|
||||||
|
series.push(this.fetchRequestRateSeries(serviceName, options))
|
||||||
|
labels.push("Requests per second")
|
||||||
|
series.push(this.fetchErrorRateSeries(serviceName, options))
|
||||||
|
labels.push("Errors per second")
|
||||||
|
} else {
|
||||||
|
// Fallback to just L4 metrics.
|
||||||
|
series.push(this.fetchServiceRxSeries(serviceName, options))
|
||||||
|
labels.push("Data rate received")
|
||||||
|
series.push(this.fetchServiceTxSeries(serviceName, options))
|
||||||
|
labels.push("Data rate transmitted")
|
||||||
|
}
|
||||||
|
|
||||||
|
var all = Promise.allSettled(series).
|
||||||
|
then(function(results){
|
||||||
|
var data = { series: [] }
|
||||||
|
for (var i = 0; i < series.length; i++) {
|
||||||
|
if (results[i].value) {
|
||||||
|
data.series.push({
|
||||||
|
label: labels[i],
|
||||||
|
data: results[i].value
|
||||||
|
});
|
||||||
|
} else if (results[i].reason) {
|
||||||
|
console.log("ERROR processing series", labels[i], results[i].reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fetch the metrics async, and return a promise to the result.
|
||||||
|
return all
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* serviceRecentSummaryStats should return four summary statistics for a
|
||||||
|
* recent time period for the named service.
|
||||||
|
*
|
||||||
|
* If these metrics aren't available then an empty array may be returned.
|
||||||
|
*
|
||||||
|
* The period may (later) be specified in options.startTime and
|
||||||
|
* options.endTime.
|
||||||
|
*
|
||||||
|
* The service's protocol must be given as one of Consul's supported
|
||||||
|
* protocols e.g. "tcp", "http", "http2", "grpc". If it is empty or the
|
||||||
|
* provider doesn't recognize it it should treat it as "tcp" and provide
|
||||||
|
* just basic connection stats.
|
||||||
|
*
|
||||||
|
* The expected return value is a promise which resolves to an object that
|
||||||
|
* should look like the following:
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* stats: [ // We expect four of these for now.
|
||||||
|
* {
|
||||||
|
* // label should be 3 chars or fewer as an abbreviation
|
||||||
|
* label: "SR",
|
||||||
|
* // desc describes the stat in a tooltip
|
||||||
|
* desc: "Success Rate - the percentage of all requests that were not 5xx status",
|
||||||
|
* // value is a string allowing the provider to format it and add
|
||||||
|
* // units as appropriate. It should be as compact as possible.
|
||||||
|
* value: "98%",
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
serviceRecentSummaryStats: function(serviceName, protocol, options) {
|
||||||
|
// Fetch stats
|
||||||
|
var stats = [];
|
||||||
|
if (this.hasL7Metrics(protocol)) {
|
||||||
|
stats.push(this.fetchRPS(serviceName, "service", options))
|
||||||
|
stats.push(this.fetchER(serviceName, "service", options))
|
||||||
|
stats.push(this.fetchPercentile(50, serviceName, "service", options))
|
||||||
|
stats.push(this.fetchPercentile(99, serviceName, "service", options))
|
||||||
|
} else {
|
||||||
|
// Fallback to just L4 metrics.
|
||||||
|
stats.push(this.fetchConnRate(serviceName, "service", options))
|
||||||
|
stats.push(this.fetchServiceRx(serviceName, "service", options))
|
||||||
|
stats.push(this.fetchServiceTx(serviceName, "service", options))
|
||||||
|
stats.push(this.fetchServiceNoRoute(serviceName, "service", options))
|
||||||
|
}
|
||||||
|
return this.fetchStats(stats)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* upstreamRecentSummaryStats should return four summary statistics for each
|
||||||
|
* upstream service over a recent time period.
|
||||||
|
*
|
||||||
|
* If these metrics aren't available then an empty array may be returned.
|
||||||
|
*
|
||||||
|
* The period may (later) be specified in options.startTime and
|
||||||
|
* options.endTime.
|
||||||
|
*
|
||||||
|
* The expected return value format is shown below:
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* stats: {
|
||||||
|
* // Each upstream will appear as an entry keyed by the upstream
|
||||||
|
* // service name. The value is an array of stats with the same
|
||||||
|
* // format as serviceRecentSummaryStats response.stats. Note that
|
||||||
|
* // different upstreams might show different stats depending on
|
||||||
|
* // their protocol.
|
||||||
|
* "upstream_name": [
|
||||||
|
* {label: "SR", desc: "...", value: "99%"},
|
||||||
|
* ...
|
||||||
|
* ],
|
||||||
|
* ...
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
upstreamRecentSummaryStats: function(serviceName, upstreamName, options) {
|
||||||
|
return this.fetchRecentSummaryStats(serviceName, "upstream", options)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* downstreamRecentSummaryStats should return four summary statistics for each
|
||||||
|
* downstream service over a recent time period.
|
||||||
|
*
|
||||||
|
* If these metrics aren't available then an empty array may be returned.
|
||||||
|
*
|
||||||
|
* The period may (later) be specified in options.startTime and
|
||||||
|
* options.endTime.
|
||||||
|
*
|
||||||
|
* The expected return value format is shown below:
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* stats: {
|
||||||
|
* // Each downstream will appear as an entry keyed by the downstream
|
||||||
|
* // service name. The value is an array of stats with the same
|
||||||
|
* // format as serviceRecentSummaryStats response.stats. Note that
|
||||||
|
* // different downstreams might show different stats depending on
|
||||||
|
* // their protocol.
|
||||||
|
* "downstream_name": [
|
||||||
|
* {label: "SR", desc: "...", value: "99%"},
|
||||||
|
* ...
|
||||||
|
* ],
|
||||||
|
* ...
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
downstreamRecentSummaryStats: function(serviceName, options) {
|
||||||
|
return this.fetchRecentSummaryStats(serviceName, "downstream", options)
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchRecentSummaryStats: function(serviceName, type, options) {
|
||||||
|
// Fetch stats
|
||||||
|
var stats = [];
|
||||||
|
|
||||||
|
// We don't know which upstreams are HTTP/TCP so just fetch all of them.
|
||||||
|
|
||||||
|
// HTTP
|
||||||
|
stats.push(this.fetchRPS(serviceName, type, options))
|
||||||
|
stats.push(this.fetchER(serviceName, type, options))
|
||||||
|
stats.push(this.fetchPercentile(50, serviceName, type, options))
|
||||||
|
stats.push(this.fetchPercentile(99, serviceName, type, options))
|
||||||
|
|
||||||
|
// L4
|
||||||
|
stats.push(this.fetchConnRate(serviceName, type, options))
|
||||||
|
stats.push(this.fetchServiceRx(serviceName, type, options))
|
||||||
|
stats.push(this.fetchServiceTx(serviceName, type, options))
|
||||||
|
stats.push(this.fetchServiceNoRoute(serviceName, type, options))
|
||||||
|
|
||||||
|
return this.fetchStatsGrouped(stats)
|
||||||
|
},
|
||||||
|
|
||||||
|
hasL7Metrics: function(protocol) {
|
||||||
|
return protocol === "http" || protocol === "http2" || protocol === "grpc"
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchStats: function(statsPromises) {
|
||||||
|
var all = Promise.allSettled(statsPromises).
|
||||||
|
then(function(results){
|
||||||
|
var data = {
|
||||||
|
stats: []
|
||||||
|
}
|
||||||
|
// Add all non-empty stats
|
||||||
|
for (var i = 0; i < statsPromises.length; i++) {
|
||||||
|
if (results[i].value) {
|
||||||
|
data.stats.push(results[i].value);
|
||||||
|
} else if (results[i].reason) {
|
||||||
|
console.log("ERROR processing stat", results[i].reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fetch the metrics async, and return a promise to the result.
|
||||||
|
return all
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchStatsGrouped: function(statsPromises) {
|
||||||
|
var all = Promise.allSettled(statsPromises).
|
||||||
|
then(function(results){
|
||||||
|
var data = {
|
||||||
|
stats: {}
|
||||||
|
}
|
||||||
|
// Add all non-empty stats
|
||||||
|
for (var i = 0; i < statsPromises.length; i++) {
|
||||||
|
if (results[i].value) {
|
||||||
|
for (var group in results[i].value) {
|
||||||
|
if (!results[i].value.hasOwnProperty(group)) continue;
|
||||||
|
if (!data.stats[group]) {
|
||||||
|
data.stats[group] = []
|
||||||
|
}
|
||||||
|
data.stats[group].push(results[i].value[group])
|
||||||
|
}
|
||||||
|
} else if (results[i].reason) {
|
||||||
|
console.log("ERROR processing stat", results[i].reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fetch the metrics async, and return a promise to the result.
|
||||||
|
return all
|
||||||
|
},
|
||||||
|
|
||||||
|
reformatSeries: function(response) {
|
||||||
|
// Handle empty results from prometheus.
|
||||||
|
if (!response || !response.data || !response.data.result
|
||||||
|
|| response.data.result.length < 1) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
// Reformat the prometheus data to be the format we want which is
|
||||||
|
// essentially the same but with Date objects instead of unix timestamps.
|
||||||
|
return response.data.result[0].values.map(function(val){
|
||||||
|
return [new Date(val[0]*1000), parseFloat(val[1])]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchRequestRateSeries: function(serviceName, options){
|
||||||
|
var q = `sum(irate(envoy_listener_http_downstream_rq_xx{local_cluster="${serviceName}",envoy_http_conn_manager_prefix="public_listener_http"}[10m]))`
|
||||||
|
return this.fetchSeries(q, options).then(this.reformatSeries, function(xhr){
|
||||||
|
// Failure. log to console and return an blank result for now.
|
||||||
|
console.log("ERROR: failed to fetch requestRate", xhr.responseText)
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchErrorRateSeries: function(serviceName, options){
|
||||||
|
// 100 * to get a result in percent
|
||||||
|
var q = `sum(`+
|
||||||
|
`irate(envoy_listener_http_downstream_rq_xx{`+
|
||||||
|
`local_cluster="${serviceName}",`+
|
||||||
|
`envoy_http_conn_manager_prefix="public_listener_http",`+
|
||||||
|
`envoy_response_code_class="5"}[10m]`+
|
||||||
|
`)`+
|
||||||
|
`)`;
|
||||||
|
return this.fetchSeries(q, options).then(this.reformatSeries, function(xhr){
|
||||||
|
// Failure. log to console and return an blank result for now.
|
||||||
|
console.log("ERROR: failed to fetch errorRate", xhr.responseText)
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchServiceRxSeries: function(serviceName, options){
|
||||||
|
var q = `8 * sum(irate(envoy_tcp_downstream_cx_rx_bytes_total{local_cluster="${serviceName}", envoy_tcp_prefix="public_listener_tcp"}[10m]))`
|
||||||
|
return this.fetchSeries(q, options).then(this.reformatSeries, function(xhr){
|
||||||
|
// Failure. log to console and return an blank result for now.
|
||||||
|
console.log("ERROR: failed to fetch rx data rate", xhr.responseText)
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchServiceTxSeries: function(serviceName, options){
|
||||||
|
var q = `8 * sum(irate(envoy_tcp_downstream_cx_tx_bytes_total{local_cluster="${serviceName}", envoy_tcp_prefix="public_listener_tcp"}[10m]))`
|
||||||
|
return this.fetchSeries(q, options).then(this.reformatSeries, function(xhr){
|
||||||
|
// Failure. log to console and return an blank result for now.
|
||||||
|
console.log("ERROR: failed to fetch tx data rate", xhr.responseText)
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
makeSubject: function(serviceName, type) {
|
||||||
|
if (type == "upstream") {
|
||||||
|
// {{GROUP}} is a placeholder that is replaced by the upstream name
|
||||||
|
return `${serviceName} → {{GROUP}}`;
|
||||||
|
}
|
||||||
|
if (type == "downstream") {
|
||||||
|
// {{GROUP}} is a placeholder that is replaced by the downstream name
|
||||||
|
return `{{GROUP}} → ${serviceName}`;
|
||||||
|
}
|
||||||
|
return serviceName
|
||||||
|
},
|
||||||
|
|
||||||
|
makeHTTPSelector: function(serviceName, type) {
|
||||||
|
// Downstreams are totally different
|
||||||
|
if (type == "downstream") {
|
||||||
|
return `consul_service="${serviceName}"`
|
||||||
|
}
|
||||||
|
var lc = `local_cluster="${serviceName}"`
|
||||||
|
if (type == "upstream") {
|
||||||
|
lc += `,envoy_http_conn_manager_prefix=~"upstream_.*"`;
|
||||||
|
} else {
|
||||||
|
// Only care about inbound public listener
|
||||||
|
lc += `,envoy_http_conn_manager_prefix="public_listener_http"`
|
||||||
|
}
|
||||||
|
return lc
|
||||||
|
},
|
||||||
|
|
||||||
|
makeTCPSelector: function(serviceName, type) {
|
||||||
|
// Downstreams are totally different
|
||||||
|
if (type == "downstream") {
|
||||||
|
return `consul_service="${serviceName}"`
|
||||||
|
}
|
||||||
|
var lc = `local_cluster="${serviceName}"`
|
||||||
|
if (type == "upstream") {
|
||||||
|
lc += `,envoy_tcp_prefix=~"upstream_.*"`;
|
||||||
|
} else {
|
||||||
|
// Only care about inbound public listener
|
||||||
|
lc += `,envoy_tcp_prefix="public_listener_tcp"`
|
||||||
|
}
|
||||||
|
return lc
|
||||||
|
},
|
||||||
|
|
||||||
|
groupQueryHTTP: function(type, q) {
|
||||||
|
if (type == "upstream") {
|
||||||
|
q += " by (envoy_http_conn_manager_prefix)"
|
||||||
|
// Extract the raw upstream service name to group results by
|
||||||
|
q = this.upstreamRelabelQueryHTTP(q)
|
||||||
|
} else if (type == "downstream") {
|
||||||
|
q += " by (local_cluster)"
|
||||||
|
q = this.downstreamRelabelQuery(q)
|
||||||
|
}
|
||||||
|
return q
|
||||||
|
},
|
||||||
|
|
||||||
|
groupQueryTCP: function(type, q) {
|
||||||
|
if (type == "upstream") {
|
||||||
|
q += " by (envoy_tcp_prefix)"
|
||||||
|
// Extract the raw upstream service name to group results by
|
||||||
|
q = this.upstreamRelabelQueryTCP(q)
|
||||||
|
} else if (type == "downstream") {
|
||||||
|
q += " by (local_cluster)"
|
||||||
|
q = this.downstreamRelabelQuery(q)
|
||||||
|
}
|
||||||
|
return q
|
||||||
|
},
|
||||||
|
|
||||||
|
upstreamRelabelQueryHTTP: function(q) {
|
||||||
|
return `label_replace(${q}, "upstream", "$1", "envoy_http_conn_manager_prefix", "upstream_(.*)_http")`
|
||||||
|
},
|
||||||
|
|
||||||
|
upstreamRelabelQueryTCP: function(q) {
|
||||||
|
return `label_replace(${q}, "upstream", "$1", "envoy_tcp_prefix", "upstream_(.*)_tcp")`
|
||||||
|
},
|
||||||
|
|
||||||
|
downstreamRelabelQuery: function(q) {
|
||||||
|
return `label_replace(${q}, "downstream", "$1", "local_cluster", "(.*)")`
|
||||||
|
},
|
||||||
|
|
||||||
|
groupBy: function(type) {
|
||||||
|
if (type == "service") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return type;
|
||||||
|
},
|
||||||
|
|
||||||
|
metricPrefixHTTP: function(type) {
|
||||||
|
if (type == "downstream") {
|
||||||
|
return "envoy_cluster_upstream_rq"
|
||||||
|
}
|
||||||
|
return "envoy_http_downstream_rq";
|
||||||
|
},
|
||||||
|
|
||||||
|
metricPrefixTCP: function(type) {
|
||||||
|
if (type == "downstream") {
|
||||||
|
return "envoy_cluster_upstream_cx"
|
||||||
|
}
|
||||||
|
return "envoy_tcp_downstream_cx";
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchRPS: function(serviceName, type, options){
|
||||||
|
var sel = this.makeHTTPSelector(serviceName, type)
|
||||||
|
var subject = this.makeSubject(serviceName, type)
|
||||||
|
var metricPfx = this.metricPrefixHTTP(type)
|
||||||
|
var q = `sum(rate(${metricPfx}_completed{${sel}}[15m]))`
|
||||||
|
return this.fetchStat(this.groupQueryHTTP(type, q),
|
||||||
|
"RPS",
|
||||||
|
`<b>${subject}</b> request rate averaged over the last 15 minutes`,
|
||||||
|
shortNumStr,
|
||||||
|
this.groupBy(type)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchER: function(serviceName, type, options){
|
||||||
|
var sel = this.makeHTTPSelector(serviceName, type)
|
||||||
|
var subject = this.makeSubject(serviceName, type)
|
||||||
|
var groupBy = ""
|
||||||
|
if (type == "upstream") {
|
||||||
|
groupBy += " by (envoy_http_conn_manager_prefix)"
|
||||||
|
} else if (type == "downstream") {
|
||||||
|
groupBy += " by (local_cluster)"
|
||||||
|
}
|
||||||
|
var metricPfx = this.metricPrefixHTTP(type)
|
||||||
|
var q = `sum(rate(${metricPfx}_xx{${sel},envoy_response_code_class="5"}[15m]))${groupBy}/sum(rate(${metricPfx}_xx{${sel}}[15m]))${groupBy}`
|
||||||
|
if (type == "upstream") {
|
||||||
|
q = this.upstreamRelabelQueryHTTP(q)
|
||||||
|
} else if (type == "downstream") {
|
||||||
|
q = this.downstreamRelabelQuery(q)
|
||||||
|
}
|
||||||
|
return this.fetchStat(q,
|
||||||
|
"ER",
|
||||||
|
`Percentage of <b>${subject}</b> requests which were 5xx status over the last 15 minutes`,
|
||||||
|
function(val){
|
||||||
|
return shortNumStr(val)+"%"
|
||||||
|
},
|
||||||
|
this.groupBy(type)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchPercentile: function(percentile, serviceName, type, options){
|
||||||
|
var sel = this.makeHTTPSelector(serviceName, type)
|
||||||
|
var subject = this.makeSubject(serviceName, type)
|
||||||
|
var groupBy = "le"
|
||||||
|
if (type == "upstream") {
|
||||||
|
groupBy += ",envoy_http_conn_manager_prefix"
|
||||||
|
} else if (type == "downstream") {
|
||||||
|
groupBy += ",local_cluster"
|
||||||
|
}
|
||||||
|
var metricPfx = this.metricPrefixHTTP(type)
|
||||||
|
var q = `histogram_quantile(${percentile/100}, sum by(${groupBy}) (rate(${metricPfx}_time_bucket{${sel}}[15m])))`
|
||||||
|
if (type == "upstream") {
|
||||||
|
q = this.upstreamRelabelQueryHTTP(q)
|
||||||
|
} else if (type == "downstream") {
|
||||||
|
q = this.downstreamRelabelQuery(q)
|
||||||
|
}
|
||||||
|
return this.fetchStat(q,
|
||||||
|
`P${percentile}`,
|
||||||
|
`<b>${subject}</b> ${percentile}th percentile request service time over the last 15 minutes`,
|
||||||
|
shortTimeStr,
|
||||||
|
this.groupBy(type)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchConnRate: function(serviceName, type, options) {
|
||||||
|
var sel = this.makeTCPSelector(serviceName, type)
|
||||||
|
var subject = this.makeSubject(serviceName, type)
|
||||||
|
var metricPfx = this.metricPrefixTCP(type)
|
||||||
|
var q = `sum(rate(${metricPfx}_total{${sel}}[15m]))`
|
||||||
|
return this.fetchStat(this.groupQueryTCP(type, q),
|
||||||
|
"CR",
|
||||||
|
`<b>${subject}</b> inbound TCP connections per second averaged over the last 15 minutes`,
|
||||||
|
shortNumStr,
|
||||||
|
this.groupBy(type)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchServiceRx: function(serviceName, type, options) {
|
||||||
|
var sel = this.makeTCPSelector(serviceName, type)
|
||||||
|
var subject = this.makeSubject(serviceName, type)
|
||||||
|
var metricPfx = this.metricPrefixTCP(type)
|
||||||
|
var q = `8 * sum(rate(${metricPfx}_rx_bytes_total{${sel}}[15m]))`
|
||||||
|
return this.fetchStat(this.groupQueryTCP(type, q),
|
||||||
|
"RX",
|
||||||
|
`<b>${subject}</b> received bits per second averaged over the last 15 minutes`,
|
||||||
|
shortNumStr,
|
||||||
|
this.groupBy(type)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchServiceTx: function(serviceName, type, options) {
|
||||||
|
var sel = this.makeTCPSelector(serviceName, type)
|
||||||
|
var subject = this.makeSubject(serviceName, type)
|
||||||
|
var metricPfx = this.metricPrefixTCP(type)
|
||||||
|
var q = `8 * sum(rate(${metricPfx}_tx_bytes_total{${sel}}[15m]))`
|
||||||
|
var self = this
|
||||||
|
return this.fetchStat(this.groupQueryTCP(type, q),
|
||||||
|
"TX",
|
||||||
|
`<b>${subject}</b> transmitted bits per second averaged over the last 15 minutes`,
|
||||||
|
shortNumStr,
|
||||||
|
this.groupBy(type)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchServiceNoRoute: function(serviceName, type, options) {
|
||||||
|
var sel = this.makeTCPSelector(serviceName, type)
|
||||||
|
var subject = this.makeSubject(serviceName, type)
|
||||||
|
var metricPfx = this.metricPrefixTCP(type)
|
||||||
|
var metric = "_no_route"
|
||||||
|
if (type == "downstream") {
|
||||||
|
metric = "_connect_fail"
|
||||||
|
}
|
||||||
|
var q = `sum(rate(${metricPfx}${metric}{${sel}}[15m]))`
|
||||||
|
return this.fetchStat(this.groupQueryTCP(type, q),
|
||||||
|
"NR",
|
||||||
|
`<b>${subject}</b> unroutable (failed) connections per second averaged over the last 15 minutes`,
|
||||||
|
shortNumStr,
|
||||||
|
this.groupBy(type)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchStat: function(promql, label, desc, formatter, groupBy) {
|
||||||
|
if (!groupBy) {
|
||||||
|
// If we don't have a grouped result and its just a single stat, return
|
||||||
|
// no result as a zero not a missing stat.
|
||||||
|
promql += " OR on() vector(0)";
|
||||||
|
}
|
||||||
|
//console.log(promql)
|
||||||
|
var params = {
|
||||||
|
query: promql,
|
||||||
|
time: (new Date).getTime()/1000
|
||||||
|
}
|
||||||
|
return this.httpGet("/api/v1/query", params).then(function(response){
|
||||||
|
if (!groupBy) {
|
||||||
|
// Not grouped, expect just one stat value return that
|
||||||
|
var v = parseFloat(response.data.result[0].value[1])
|
||||||
|
return {
|
||||||
|
label: label,
|
||||||
|
desc: desc,
|
||||||
|
value: formatter(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = {};
|
||||||
|
for (var i = 0; i < response.data.result.length; i++) {
|
||||||
|
var res = response.data.result[i];
|
||||||
|
var v = parseFloat(res.value[1]);
|
||||||
|
var groupName = res.metric[groupBy];
|
||||||
|
data[groupName] = {
|
||||||
|
label: label,
|
||||||
|
desc: desc.replace('{{GROUP}}', groupName),
|
||||||
|
value: formatter(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}, function(xhr){
|
||||||
|
// Failure. log to console and return an blank result for now.
|
||||||
|
console.log("ERROR: failed to fetch stat", label, xhr.responseText)
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchSeries: function(promql, options) {
|
||||||
|
var params = {
|
||||||
|
query: promql,
|
||||||
|
start: options.start,
|
||||||
|
end: options.end,
|
||||||
|
step: "10s",
|
||||||
|
timeout: "8s"
|
||||||
|
}
|
||||||
|
return this.httpGet("/api/v1/query_range", params)
|
||||||
|
},
|
||||||
|
|
||||||
|
httpGet: function(path, params) {
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
var self = this
|
||||||
|
return new Promise(function(resolve, reject){
|
||||||
|
xhr.onreadystatechange = function(){
|
||||||
|
if (xhr.readyState !== 4) return;
|
||||||
|
|
||||||
|
if (xhr.status == 200) {
|
||||||
|
// Attempt to parse response as JSON and return the object
|
||||||
|
var o = JSON.parse(xhr.responseText)
|
||||||
|
resolve(o)
|
||||||
|
}
|
||||||
|
reject(xhr)
|
||||||
|
}
|
||||||
|
|
||||||
|
var url = self.baseURL()+path;
|
||||||
|
if (params) {
|
||||||
|
var qs = Object.keys(params).
|
||||||
|
map(function(key){
|
||||||
|
return encodeURIComponent(key)+"="+encodeURIComponent(params[key])
|
||||||
|
}).
|
||||||
|
join("&")
|
||||||
|
url = url+"?"+qs
|
||||||
|
}
|
||||||
|
xhr.open("GET", url, true);
|
||||||
|
xhr.send();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
baseURL: function() {
|
||||||
|
// TODO support configuring a direct Prometheus via
|
||||||
|
// metrics_provider_options_json.
|
||||||
|
return "/v1/internal/ui/metrics-proxy"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
function shortNumStr(n) {
|
||||||
|
if (n < 1e3) {
|
||||||
|
if (Number.isInteger(n)) return ""+n
|
||||||
|
if (n >= 100) {
|
||||||
|
// Go to 3 significant figures but wrap it in Number to avoid scientific
|
||||||
|
// notation lie 2.3e+2 for 230.
|
||||||
|
return Number(n.toPrecision(3))
|
||||||
|
} if (n < 1) {
|
||||||
|
// Very small numbers show with limited precision to prevent long string
|
||||||
|
// of 0.000000.
|
||||||
|
return Number(n.toFixed(2));
|
||||||
|
} else {
|
||||||
|
// Two sig figs is enough below this
|
||||||
|
return Number(n.toPrecision(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (n >= 1e3 && n < 1e6) return +(n / 1e3).toPrecision(3) + "k";
|
||||||
|
if (n >= 1e6 && n < 1e9) return +(n / 1e6).toPrecision(3) + "m";
|
||||||
|
if (n >= 1e9 && n < 1e12) return +(n / 1e9).toPrecision(3) + "g";
|
||||||
|
if (n >= 1e12) return +(n / 1e12).toFixed(0) + "t";
|
||||||
|
}
|
||||||
|
|
||||||
|
function shortTimeStr(n) {
|
||||||
|
if (n < 1e3) return Math.round(n) + "ms";
|
||||||
|
|
||||||
|
var secs = n / 1e3
|
||||||
|
if (secs < 60) return secs.toFixed(1) + "s"
|
||||||
|
|
||||||
|
var mins = secs/60
|
||||||
|
if (mins < 60) return mins.toFixed(1) + "m"
|
||||||
|
|
||||||
|
var hours = mins/60
|
||||||
|
if (hours < 24) return hours.toFixed(1) + "h"
|
||||||
|
|
||||||
|
var days = hours/24
|
||||||
|
return days.toFixed(1) + "d"
|
||||||
|
}
|
||||||
|
|
||||||
|
/* global consul:writable */
|
||||||
|
window.consul.registerMetricsProvider("prometheus", prometheusProvider)
|
||||||
|
|
||||||
|
}());
|
297
ui-v2/yarn.lock
297
ui-v2/yarn.lock
|
@ -1527,10 +1527,10 @@
|
||||||
faker "^4.1.0"
|
faker "^4.1.0"
|
||||||
js-yaml "^3.13.1"
|
js-yaml "^3.13.1"
|
||||||
|
|
||||||
"@hashicorp/consul-api-double@^5.3.5":
|
"@hashicorp/consul-api-double@^5.3.7":
|
||||||
version "5.3.5"
|
version "5.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-5.3.5.tgz#8e39d6af4ab6d32c7d8c469bb4aab23e16971bd3"
|
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-5.4.0.tgz#fc75e064c3e50385f4fb8c5dd9068875806d8901"
|
||||||
integrity sha512-SiT2lLk0J8CwsxtuAobrweC5VdOT6b66M1gSLcT/Lcx62fOLH1X/DfMt6F2VKwC4BN8WBFZGTmn0rwdFOjKpmw==
|
integrity sha512-vAi580MyPoFhjDl8WhSviMzFJ1/PZesLqYCuGy8vuxqFaKCQET4AR8gRuungWSdRf5432aJXUNtXLhMHdJeNPg==
|
||||||
|
|
||||||
"@hashicorp/ember-cli-api-double@^3.1.0":
|
"@hashicorp/ember-cli-api-double@^3.1.0":
|
||||||
version "3.1.2"
|
version "3.1.2"
|
||||||
|
@ -4513,6 +4513,11 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
delayed-stream "~1.0.0"
|
delayed-stream "~1.0.0"
|
||||||
|
|
||||||
|
commander@2, commander@^2.20.0, commander@^2.6.0:
|
||||||
|
version "2.20.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
||||||
|
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
|
||||||
|
|
||||||
commander@2.12.2:
|
commander@2.12.2:
|
||||||
version "2.12.2"
|
version "2.12.2"
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.2.tgz#0f5946c427ed9ec0d91a46bb9def53e54650e555"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.2.tgz#0f5946c427ed9ec0d91a46bb9def53e54650e555"
|
||||||
|
@ -4525,11 +4530,6 @@ commander@2.8.x:
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-readlink ">= 1.0.0"
|
graceful-readlink ">= 1.0.0"
|
||||||
|
|
||||||
commander@^2.20.0, commander@^2.6.0:
|
|
||||||
version "2.20.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
|
||||||
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
|
|
||||||
|
|
||||||
commander@^4.1.1:
|
commander@^4.1.1:
|
||||||
version "4.1.1"
|
version "4.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
|
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
|
||||||
|
@ -4890,6 +4890,262 @@ cyclist@^1.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
|
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
|
||||||
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
|
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
|
||||||
|
|
||||||
|
d3-array@1, d3-array@^1.1.1, d3-array@^1.2.0:
|
||||||
|
version "1.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f"
|
||||||
|
integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==
|
||||||
|
|
||||||
|
d3-axis@1:
|
||||||
|
version "1.0.12"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.12.tgz#cdf20ba210cfbb43795af33756886fb3638daac9"
|
||||||
|
integrity sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ==
|
||||||
|
|
||||||
|
d3-brush@1:
|
||||||
|
version "1.1.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-1.1.6.tgz#b0a22c7372cabec128bdddf9bddc058592f89e9b"
|
||||||
|
integrity sha512-7RW+w7HfMCPyZLifTz/UnJmI5kdkXtpCbombUSs8xniAyo0vIbrDzDwUJB6eJOgl9u5DQOt2TQlYumxzD1SvYA==
|
||||||
|
dependencies:
|
||||||
|
d3-dispatch "1"
|
||||||
|
d3-drag "1"
|
||||||
|
d3-interpolate "1"
|
||||||
|
d3-selection "1"
|
||||||
|
d3-transition "1"
|
||||||
|
|
||||||
|
d3-chord@1:
|
||||||
|
version "1.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-1.0.6.tgz#309157e3f2db2c752f0280fedd35f2067ccbb15f"
|
||||||
|
integrity sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==
|
||||||
|
dependencies:
|
||||||
|
d3-array "1"
|
||||||
|
d3-path "1"
|
||||||
|
|
||||||
|
d3-collection@1:
|
||||||
|
version "1.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e"
|
||||||
|
integrity sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==
|
||||||
|
|
||||||
|
d3-color@1:
|
||||||
|
version "1.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.4.1.tgz#c52002bf8846ada4424d55d97982fef26eb3bc8a"
|
||||||
|
integrity sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==
|
||||||
|
|
||||||
|
d3-contour@1:
|
||||||
|
version "1.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-1.3.2.tgz#652aacd500d2264cb3423cee10db69f6f59bead3"
|
||||||
|
integrity sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==
|
||||||
|
dependencies:
|
||||||
|
d3-array "^1.1.1"
|
||||||
|
|
||||||
|
d3-dispatch@1:
|
||||||
|
version "1.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.6.tgz#00d37bcee4dd8cd97729dd893a0ac29caaba5d58"
|
||||||
|
integrity sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==
|
||||||
|
|
||||||
|
d3-drag@1:
|
||||||
|
version "1.2.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-1.2.5.tgz#2537f451acd39d31406677b7dc77c82f7d988f70"
|
||||||
|
integrity sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==
|
||||||
|
dependencies:
|
||||||
|
d3-dispatch "1"
|
||||||
|
d3-selection "1"
|
||||||
|
|
||||||
|
d3-dsv@1:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-1.2.0.tgz#9d5f75c3a5f8abd611f74d3f5847b0d4338b885c"
|
||||||
|
integrity sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g==
|
||||||
|
dependencies:
|
||||||
|
commander "2"
|
||||||
|
iconv-lite "0.4"
|
||||||
|
rw "1"
|
||||||
|
|
||||||
|
d3-ease@1:
|
||||||
|
version "1.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.7.tgz#9a834890ef8b8ae8c558b2fe55bd57f5993b85e2"
|
||||||
|
integrity sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==
|
||||||
|
|
||||||
|
d3-fetch@1:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-1.2.0.tgz#15ce2ecfc41b092b1db50abd2c552c2316cf7fc7"
|
||||||
|
integrity sha512-yC78NBVcd2zFAyR/HnUiBS7Lf6inSCoWcSxFfw8FYL7ydiqe80SazNwoffcqOfs95XaLo7yebsmQqDKSsXUtvA==
|
||||||
|
dependencies:
|
||||||
|
d3-dsv "1"
|
||||||
|
|
||||||
|
d3-force@1:
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-1.2.1.tgz#fd29a5d1ff181c9e7f0669e4bd72bdb0e914ec0b"
|
||||||
|
integrity sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==
|
||||||
|
dependencies:
|
||||||
|
d3-collection "1"
|
||||||
|
d3-dispatch "1"
|
||||||
|
d3-quadtree "1"
|
||||||
|
d3-timer "1"
|
||||||
|
|
||||||
|
d3-format@1:
|
||||||
|
version "1.4.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.5.tgz#374f2ba1320e3717eb74a9356c67daee17a7edb4"
|
||||||
|
integrity sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==
|
||||||
|
|
||||||
|
d3-geo@1:
|
||||||
|
version "1.12.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-1.12.1.tgz#7fc2ab7414b72e59fbcbd603e80d9adc029b035f"
|
||||||
|
integrity sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==
|
||||||
|
dependencies:
|
||||||
|
d3-array "1"
|
||||||
|
|
||||||
|
d3-hierarchy@1:
|
||||||
|
version "1.1.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz#2f6bee24caaea43f8dc37545fa01628559647a83"
|
||||||
|
integrity sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==
|
||||||
|
|
||||||
|
d3-interpolate@1:
|
||||||
|
version "1.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987"
|
||||||
|
integrity sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==
|
||||||
|
dependencies:
|
||||||
|
d3-color "1"
|
||||||
|
|
||||||
|
d3-path@1:
|
||||||
|
version "1.0.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf"
|
||||||
|
integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==
|
||||||
|
|
||||||
|
d3-polygon@1:
|
||||||
|
version "1.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-1.0.6.tgz#0bf8cb8180a6dc107f518ddf7975e12abbfbd38e"
|
||||||
|
integrity sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==
|
||||||
|
|
||||||
|
d3-quadtree@1:
|
||||||
|
version "1.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-1.0.7.tgz#ca8b84df7bb53763fe3c2f24bd435137f4e53135"
|
||||||
|
integrity sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==
|
||||||
|
|
||||||
|
d3-random@1:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-1.1.2.tgz#2833be7c124360bf9e2d3fd4f33847cfe6cab291"
|
||||||
|
integrity sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ==
|
||||||
|
|
||||||
|
d3-scale-chromatic@1:
|
||||||
|
version "1.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz#54e333fc78212f439b14641fb55801dd81135a98"
|
||||||
|
integrity sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==
|
||||||
|
dependencies:
|
||||||
|
d3-color "1"
|
||||||
|
d3-interpolate "1"
|
||||||
|
|
||||||
|
d3-scale@2:
|
||||||
|
version "2.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-2.2.2.tgz#4e880e0b2745acaaddd3ede26a9e908a9e17b81f"
|
||||||
|
integrity sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==
|
||||||
|
dependencies:
|
||||||
|
d3-array "^1.2.0"
|
||||||
|
d3-collection "1"
|
||||||
|
d3-format "1"
|
||||||
|
d3-interpolate "1"
|
||||||
|
d3-time "1"
|
||||||
|
d3-time-format "2"
|
||||||
|
|
||||||
|
d3-selection-multi@^1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-selection-multi/-/d3-selection-multi-1.0.1.tgz#cd6c25413d04a2cb97470e786f2cd877f3e34f58"
|
||||||
|
integrity sha1-zWwlQT0EosuXRw54byzYd/PjT1g=
|
||||||
|
dependencies:
|
||||||
|
d3-selection "1"
|
||||||
|
d3-transition "1"
|
||||||
|
|
||||||
|
d3-selection@1, d3-selection@^1.1.0:
|
||||||
|
version "1.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.4.2.tgz#dcaa49522c0dbf32d6c1858afc26b6094555bc5c"
|
||||||
|
integrity sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==
|
||||||
|
|
||||||
|
d3-shape@1:
|
||||||
|
version "1.3.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7"
|
||||||
|
integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==
|
||||||
|
dependencies:
|
||||||
|
d3-path "1"
|
||||||
|
|
||||||
|
d3-time-format@2:
|
||||||
|
version "2.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.3.0.tgz#107bdc028667788a8924ba040faf1fbccd5a7850"
|
||||||
|
integrity sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==
|
||||||
|
dependencies:
|
||||||
|
d3-time "1"
|
||||||
|
|
||||||
|
d3-time@1:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1"
|
||||||
|
integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==
|
||||||
|
|
||||||
|
d3-timer@1:
|
||||||
|
version "1.0.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.10.tgz#dfe76b8a91748831b13b6d9c793ffbd508dd9de5"
|
||||||
|
integrity sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==
|
||||||
|
|
||||||
|
d3-transition@1:
|
||||||
|
version "1.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.3.2.tgz#a98ef2151be8d8600543434c1ca80140ae23b398"
|
||||||
|
integrity sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==
|
||||||
|
dependencies:
|
||||||
|
d3-color "1"
|
||||||
|
d3-dispatch "1"
|
||||||
|
d3-ease "1"
|
||||||
|
d3-interpolate "1"
|
||||||
|
d3-selection "^1.1.0"
|
||||||
|
d3-timer "1"
|
||||||
|
|
||||||
|
d3-voronoi@1:
|
||||||
|
version "1.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-voronoi/-/d3-voronoi-1.1.4.tgz#dd3c78d7653d2bb359284ae478645d95944c8297"
|
||||||
|
integrity sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==
|
||||||
|
|
||||||
|
d3-zoom@1:
|
||||||
|
version "1.8.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-1.8.3.tgz#b6a3dbe738c7763121cd05b8a7795ffe17f4fc0a"
|
||||||
|
integrity sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==
|
||||||
|
dependencies:
|
||||||
|
d3-dispatch "1"
|
||||||
|
d3-drag "1"
|
||||||
|
d3-interpolate "1"
|
||||||
|
d3-selection "1"
|
||||||
|
d3-transition "1"
|
||||||
|
|
||||||
|
d3@^5.0.0:
|
||||||
|
version "5.16.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3/-/d3-5.16.0.tgz#9c5e8d3b56403c79d4ed42fbd62f6113f199c877"
|
||||||
|
integrity sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw==
|
||||||
|
dependencies:
|
||||||
|
d3-array "1"
|
||||||
|
d3-axis "1"
|
||||||
|
d3-brush "1"
|
||||||
|
d3-chord "1"
|
||||||
|
d3-collection "1"
|
||||||
|
d3-color "1"
|
||||||
|
d3-contour "1"
|
||||||
|
d3-dispatch "1"
|
||||||
|
d3-drag "1"
|
||||||
|
d3-dsv "1"
|
||||||
|
d3-ease "1"
|
||||||
|
d3-fetch "1"
|
||||||
|
d3-force "1"
|
||||||
|
d3-format "1"
|
||||||
|
d3-geo "1"
|
||||||
|
d3-hierarchy "1"
|
||||||
|
d3-interpolate "1"
|
||||||
|
d3-path "1"
|
||||||
|
d3-polygon "1"
|
||||||
|
d3-quadtree "1"
|
||||||
|
d3-random "1"
|
||||||
|
d3-scale "2"
|
||||||
|
d3-scale-chromatic "1"
|
||||||
|
d3-selection "1"
|
||||||
|
d3-shape "1"
|
||||||
|
d3-time "1"
|
||||||
|
d3-time-format "2"
|
||||||
|
d3-timer "1"
|
||||||
|
d3-transition "1"
|
||||||
|
d3-voronoi "1"
|
||||||
|
d3-zoom "1"
|
||||||
|
|
||||||
dag-map@^2.0.2:
|
dag-map@^2.0.2:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/dag-map/-/dag-map-2.0.2.tgz#9714b472de82a1843de2fba9b6876938cab44c68"
|
resolved "https://registry.yarnpkg.com/dag-map/-/dag-map-2.0.2.tgz#9714b472de82a1843de2fba9b6876938cab44c68"
|
||||||
|
@ -4918,6 +5174,11 @@ data-urls@^1.0.1:
|
||||||
whatwg-mimetype "^2.2.0"
|
whatwg-mimetype "^2.2.0"
|
||||||
whatwg-url "^7.0.0"
|
whatwg-url "^7.0.0"
|
||||||
|
|
||||||
|
dayjs@^1.9.1:
|
||||||
|
version "1.9.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.9.1.tgz#201a755f7db5103ed6de63ba93a984141c754541"
|
||||||
|
integrity sha512-01NCTBg8cuMJG1OQc6PR7T66+AFYiPwgDvdJmvJBn29NGzIG+DIFxPLNjHzwz3cpFIvG+NcwIjP9hSaPVoOaDg==
|
||||||
|
|
||||||
debug@2.6.9, debug@^2.1.0, debug@^2.1.1, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
|
debug@2.6.9, debug@^2.1.0, debug@^2.1.1, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
|
||||||
version "2.6.9"
|
version "2.6.9"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||||
|
@ -5977,6 +6238,17 @@ ember-copy@1.0.0, ember-copy@^1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ember-cli-babel "^6.6.0"
|
ember-cli-babel "^6.6.0"
|
||||||
|
|
||||||
|
ember-d3@^0.5.1:
|
||||||
|
version "0.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ember-d3/-/ember-d3-0.5.1.tgz#b23ce145863f082b5e73d25d9a43a0f1d9e9f412"
|
||||||
|
integrity sha512-NyjTUuIOxGxZdyrxLasNwwjqyFgay1pVHGRAWFj7mriwTI44muKsM9ZMl6YeepqixceuFig2fDxHmLLrkQV+QQ==
|
||||||
|
dependencies:
|
||||||
|
broccoli-funnel "^2.0.0"
|
||||||
|
broccoli-merge-trees "^3.0.0"
|
||||||
|
d3 "^5.0.0"
|
||||||
|
d3-selection-multi "^1.0.1"
|
||||||
|
ember-cli-babel "^7.1.2"
|
||||||
|
|
||||||
ember-data-model-fragments@5.0.0-beta.0:
|
ember-data-model-fragments@5.0.0-beta.0:
|
||||||
version "5.0.0-beta.0"
|
version "5.0.0-beta.0"
|
||||||
resolved "https://registry.yarnpkg.com/ember-data-model-fragments/-/ember-data-model-fragments-5.0.0-beta.0.tgz#da90799970317ca852f96b2ea1548ca70094a5bb"
|
resolved "https://registry.yarnpkg.com/ember-data-model-fragments/-/ember-data-model-fragments-5.0.0-beta.0.tgz#da90799970317ca852f96b2ea1548ca70094a5bb"
|
||||||
|
@ -8109,7 +8381,7 @@ husky@^4.2.5:
|
||||||
slash "^3.0.0"
|
slash "^3.0.0"
|
||||||
which-pm-runs "^1.0.0"
|
which-pm-runs "^1.0.0"
|
||||||
|
|
||||||
iconv-lite@0.4.24, iconv-lite@^0.4.24:
|
iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.24:
|
||||||
version "0.4.24"
|
version "0.4.24"
|
||||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||||
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
|
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
|
||||||
|
@ -11472,6 +11744,11 @@ run-queue@^1.0.0, run-queue@^1.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
aproba "^1.1.1"
|
aproba "^1.1.1"
|
||||||
|
|
||||||
|
rw@1:
|
||||||
|
version "1.3.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4"
|
||||||
|
integrity sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=
|
||||||
|
|
||||||
rxjs@^6.4.0, rxjs@^6.5.5, rxjs@^6.6.0:
|
rxjs@^6.4.0, rxjs@^6.5.5, rxjs@^6.6.0:
|
||||||
version "6.6.0"
|
version "6.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.0.tgz#af2901eedf02e3a83ffa7f886240ff9018bbec84"
|
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.0.tgz#af2901eedf02e3a83ffa7f886240ff9018bbec84"
|
||||||
|
|
Loading…
Reference in New Issue