blob: b3640d03f172d1e63cacc14ddfd4a7b9f6a3a9b8 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
var dateformat = require('dateformat');
var hg = require('mercury');
var h = require('mercury').h;
var svg = require('virtual-dom/virtual-hyperscript/svg');
var Consts = require('../../constants');
var MouseMoveHandler = require('../../mousemove-handler.js');
var Util = require('../../util');
module.exports = create;
module.exports.render = render;
var tableRows = [
// Mounttable.
{
rowHeader: Consts.metricNames.MN_MOUNTTABLE,
columns: [
{
dataKey: Consts.dataKeys.DK_SERVICE_LATENCY,
label: 'LATENCY',
metricName: Consts.metricNames.MN_MOUNTTABLE,
threshold: 2000
},
{
dataKey: Consts.dataKeys.DK_SERVICE_QPS,
label: 'QPS',
metricName: Consts.metricNames.MN_MOUNTTABLE
},
{
dataKey: Consts.dataKeys.DK_SERVICE_METADATA,
label: 'BUILD AGE (h)',
metricName: Consts.metricNames.MN_MOUNTTABLE
},
{
dataKey: Consts.dataKeys.DK_SERVICE_COUNTERS,
label: 'MOUNTED SERVERS',
metricName: Consts.metricNames.MN_MT_MOUNTED_SERVERS
},
{
dataKey: Consts.dataKeys.DK_SERVICE_COUNTERS,
label: 'NODES',
metricName: Consts.metricNames.MN_MT_NODES
},
]
},
// Proxy.
{
rowHeader: Consts.metricNames.MN_PROXY,
columns: [
{
dataKey: Consts.dataKeys.DK_SERVICE_LATENCY,
label: 'LATENCY',
metricName: Consts.metricNames.MN_PROXY,
threshold: 2000
},
{
dataKey: Consts.dataKeys.DK_SERVICE_QPS,
label: 'QPS',
metricName: Consts.metricNames.MN_PROXY
},
{
dataKey: Consts.dataKeys.DK_SERVICE_METADATA,
label: 'BUILD AGE (h)',
metricName: Consts.metricNames.MN_PROXY
}
]
},
// Roled.
{
rowHeader: Consts.metricNames.MN_ROLE,
columns: [
{
dataKey: Consts.dataKeys.DK_SERVICE_LATENCY,
label: 'LATENCY',
metricName: Consts.metricNames.MN_ROLE,
threshold: 2000
},
{
dataKey: Consts.dataKeys.DK_SERVICE_QPS,
label: 'QPS',
metricName: Consts.metricNames.MN_ROLE
},
{
dataKey: Consts.dataKeys.DK_SERVICE_METADATA,
label: 'BUILD AGE (h)',
metricName: Consts.metricNames.MN_ROLE
}
]
},
// Identityd.
{
rowHeader: Consts.metricNames.MN_IDENTITY,
columns: [
{
dataKey: Consts.dataKeys.DK_SERVICE_LATENCY,
label: 'LATENCY (MACAROON)',
metricName: Consts.metricNames.MN_MACAROON,
threshold: 2000
},
{
dataKey: Consts.dataKeys.DK_SERVICE_LATENCY,
label: 'LATENCY (BINARY DISCHARGER)',
metricName: Consts.metricNames.MN_BINARY_DISCHARGER,
threshold: 2000
},
{
dataKey: Consts.dataKeys.DK_SERVICE_QPS,
label: 'QPS',
metricName: Consts.metricNames.MN_IDENTITY
},
{
dataKey: Consts.dataKeys.DK_SERVICE_METADATA,
label: 'BUILD AGE (h)',
metricName: Consts.metricNames.MN_IDENTITY
}
]
},
// Benchmarks.
{
rowHeader: Consts.metricNames.MN_BENCHMARKS,
columns: [
{
dataKey: Consts.dataKeys.DK_SERVICE_LATENCY,
label: 'LATENCY',
metricName: Consts.metricNames.MN_BENCHMARKS,
threshold: 2000
},
{
dataKey: Consts.dataKeys.DK_SERVICE_QPS,
label: 'QPS',
metricName: Consts.metricNames.MN_BENCHMARKS
},
{
dataKey: Consts.dataKeys.DK_SERVICE_METADATA,
label: 'BUILD AGE (h)',
metricName: Consts.metricNames.MN_BENCHMARKS
}
]
},
// Syncbase allocator.
{
rowHeader: Consts.metricNames.MN_SB_ALLOCATOR,
columns: [
{
dataKey: Consts.dataKeys.DK_SERVICE_LATENCY,
label: 'LATENCY',
metricName: Consts.metricNames.MN_SB_ALLOCATOR,
threshold: 2000
},
{
dataKey: Consts.dataKeys.DK_SERVICE_QPS,
label: 'QPS',
metricName: Consts.metricNames.MN_SB_ALLOCATOR
},
{
dataKey: Consts.dataKeys.DK_SERVICE_METADATA,
label: 'BUILD AGE (h)',
metricName: Consts.metricNames.MN_SB_ALLOCATOR
}
]
}
];
/** Constructor. */
function create(data) {
if (!data) {
return null;
}
return hg.state({
// Raw data.
data: hg.struct(data.data),
mouseOffsetFactor: hg.value(-1),
mouseOverDataKey: hg.value(''),
mouseOverMetricName: hg.value(''),
channels: {
mouseMoveOnSparkline: mouseMoveOnSparkline,
mouseOverSparkline: mouseOverSparkline,
mouseOutOfSparkline: mouseOutOfSparkline,
}
});
}
/** Callback for moving mouse on sparkline. */
function mouseMoveOnSparkline(state, data) {
state.mouseOffsetFactor.set(data.f);
}
/** Callback for moving mouse over a sparkline. */
function mouseOverSparkline(state, colData) {
state.mouseOverDataKey.set(colData.dataKey);
state.mouseOverMetricName.set(colData.metricName);
}
/** Callback for moving mouse out of sparkline. */
function mouseOutOfSparkline(state) {
state.mouseOffsetFactor.set(-1);
state.mouseOverDataKey.set('');
state.mouseOverMetricName.set('');
}
/** The main render function. */
function render(globalState, state) {
var data = state.data;
var rows = tableRows.map(function(rowData, rowIndex) {
var numWarnings = 0;
var numReplicas = 0;
var hasFatalErrors = false;
var cols = rowData.columns.map(function(colData, colIndex) {
// Create a column for each metric.
//
// Use the first column (usually latency) to determine number of replicas.
var metricsData = data[colData.dataKey][colData.metricName];
if (numReplicas === 0) {
numReplicas = metricsData.length;
}
// Calculate average from all replicas.
var avg = {}; // timestamps -> [values from replicas]
var numErrors = 0;
metricsData.forEach(function(metricData) {
if (metricData && metricData.HistoryTimestamps) {
for (var i = 0; i < metricData.HistoryTimestamps.length; i++) {
var t = metricData.HistoryTimestamps[i];
var v = metricData.HistoryValues[i];
if (!avg[t]) {
avg[t] = [];
}
avg[t].push(v);
}
}
var hasError = false;
// Handle error when getting time series.
if (metricData.ErrMsg !== '') {
hasError = true;
}
// Handle stale data.
var tsLen = metricData.HistoryTimestamps.length;
if (tsLen > 0) {
var lastTimestamp = metricData.HistoryTimestamps[tsLen - 1];
if (data.MaxTime - lastTimestamp >
Consts.stableDataThresholdInSeconds) {
hasError = true;
}
} else {
hasError = true;
}
// Value over threshold.
var overThreshold = (
colData.threshold && metricData.CurrentValue >= colData.threshold);
if (overThreshold) {
hasError = true;
}
if (hasError) {
numErrors++;
}
});
var extraColMetricClass = '';
if (numErrors > 0) {
extraColMetricClass = '.warning';
// Use the latency column to estimate how many replicas are in warning
// state.
if (colData.label === 'LATENCY') {
numWarnings = numErrors;
}
}
// Prepare data for average timeseries.
var avgTimestamps = [];
var avgValues = [];
var avgMinValue = Number.MAX_VALUE;
var avgMaxValue = 0;
Object.keys(avg).sort().forEach(function(t) {
avgTimestamps.push(parseInt(t));
var values = avg[t];
var sum = 0;
values.forEach(function(v) {
sum += v;
});
var avgValue = sum / values.length;
avgValues.push(avgValue);
avgMinValue = Math.min(avgMinValue, avgValue);
avgMaxValue = Math.max(avgMaxValue, avgValue);
});
// Render sparkline for average values.
//
// 100 is the default logical width of any svg graphs.
var points = '0,100 100,100';
if (avgTimestamps.length > 0 && avgValues.length > 0) {
points = Util.genPolylinePoints(
avgTimestamps, avgValues,
data.MinTime, data.MaxTime,
avgMinValue, avgMaxValue);
}
var avgCurValue = 0;
if (avgValues.length > 0) {
avgCurValue = avgValues[avgValues.length - 1];
}
var curValue = Util.formatValue(avgCurValue);
var extraCurValueClass = '';
var mouseOffset = 100 * state.mouseOffsetFactor;
if (mouseOffset >= 0) {
curValue = Util.formatValue(Util.interpolateValue(
avgCurValue, state.mouseOffsetFactor,
avgTimestamps, avgValues));
extraCurValueClass = '.history';
}
// Handle avg value over threshold.
var overThreshold = (
colData.threshold && avgCurValue >= colData.threshold);
var thresholdValue = -100;
if (overThreshold) {
hasFatalErrors = true;
extraColMetricClass = '.fatal';
thresholdValue = (colData.threshold-avgMinValue)/
(avgMaxValue - avgMinValue)*100.0;
}
var sparklineItems = [
h('div.highlight-overlay'),
Util.renderMouseLine(mouseOffset),
h('div.sparkline', {
'ev-mousemove': new MouseMoveHandler(
state.channels.mouseMoveOnSparkline),
'ev-mouseover': hg.send(state.channels.mouseOverSparkline, colData),
'ev-mouseout': hg.send(state.channels.mouseOutOfSparkline),
}, [
renderThreshold(thresholdValue),
Util.renderSparkline(points)
]),
h('div.cur-value' + extraCurValueClass, [curValue])
];
if (state.mouseOffsetFactor >= 0 &&
state.mouseOverDataKey === colData.dataKey &&
state.mouseOverMetricName === colData.metricName) {
var curTimestamp =
(data.MaxTime - data.MinTime) * state.mouseOffsetFactor +
data.MinTime;
sparklineItems.push(
h('div.mouseover-time', dateformat(new Date(curTimestamp * 1000))));
}
var sparkline = h('div.col-metric' + extraColMetricClass, {
'ev-click': hg.send(globalState.channels.mouseClickOnMetric, {
serviceName: rowData.rowHeader,
colData: colData
})
}, sparklineItems);
var items = [h('div.col-header', colData.label), sparkline];
return h('div.col', items);
});
var headerExtraClass = (numWarnings !== 0) ? '.warning' : '';
if (hasFatalErrors) {
headerExtraClass = '.fatal';
}
cols.unshift(h('div.row-header' + headerExtraClass, [
h('div.header-label', Consts.getDisplayName(rowData.rowHeader)),
h('div.header-numhealthy',
((numReplicas - numWarnings) + '/' + numReplicas))
]));
return h('div.row', cols);
});
return h('div.status-table', rows);
}
/**
* Renders threshold line.
*/
function renderThreshold(value) {
return svg('svg', {
'class': 'threshold',
'viewBox': '0 0 100 100',
'preserveAspectRatio': 'none'
}, [
svg('path', {
'd': 'M 0 ' + value + ' L 100 ' + value,
'stroke-dasharray': '2,2'
}),
]);
}