Extended node discovery to include a display value. Improved state transfer from overview dashboard to node dashboard

This commit is contained in:
Boaz Leskes 2013-11-07 21:20:02 +01:00
parent 741c0d8ded
commit ac7ef26211
4 changed files with 399 additions and 319 deletions

View file

@ -29,24 +29,24 @@ dashboard = {
};
// Set a title
dashboard.title = 'Node Statistics';
dashboard.title = 'Marvel - Node Statistics';
// And the index options
dashboard.failover = false;
dashboard.index = {
default: 'ADD_A_TIME_FILTER',
pattern: '[marvel-]YYYY.MM.DD',
interval: 'day'
'default': 'ADD_A_TIME_FILTER',
'pattern': '[marvel-]YYYY.MM.DD',
'interval': 'day'
};
// In this dashboard we let users pass nodes as comma seperated list to the query parameter.
// If nodes are defined, split into a list of query objects, otherwise, show all
// NOTE: ids must be integers, hence the parseInt()s
if (!_.isUndefined(ARGS.nodes)) {
queries = _.object(_.map(ARGS.nodes.split(','), function (v, k) {
queries = _.object(_.map(JSON.parse(ARGS.nodes), function (v, k) {
return [k, {
query: 'node.transport_address:"' + v + '"',
alias: v,
query: v.q,
alias: v.a || v.q,
pin: true,
id: parseInt(k, 10)
}];
@ -61,22 +61,22 @@ if (!_.isUndefined(ARGS.nodes)) {
};
}
var show = ARGS.show.split(',') || [];
var show = (ARGS.show || "").split(',');
// Now populate the query service with our objects
dashboard.services.query = {
list: queries,
ids: _.map(_.keys(queries), function (v) {
return parseInt(v, 10);
}),
})
};
// Lets also add a default time filter, the value of which can be specified by the user
dashboard.services.filter = {
list: {
0: {
from: (ARGS.from || "now-" + _d_timespan),
to: "now",
from: ARGS.from || "now-" + _d_timespan,
to: ARGS.to || "now",
field: "@timestamp",
type: "time",
active: true,
@ -128,16 +128,6 @@ var rows = [
"grid": {
"max": 100,
"min": 0
},
"annotate": {
"enable": false,
"query": "*",
"size": 20,
"field": "_type",
"sort": [
"_score",
"desc"
]
}
},
@ -171,7 +161,17 @@ var rows = [
{
"time_field": "@timestamp",
"value_field": "jvm.mem.heap_used_in_bytes",
"title": "Heap"
"title": "Heap",
"annotate": {
"enable": true,
"query": "_type:shard_event",
"size": 100,
"field": "message",
"sort": [
"_score",
"desc"
]
}
},
{
"value_field": "jvm.gc.collectors.ParNew.collection_time_in_millis",
@ -231,7 +231,7 @@ var rows = [
]
},
{
"title": "Disk IO",
"title": "Disk",
"panels": [
{
"value_field": "fs.data.disk_read_size_in_bytes",
@ -244,6 +244,10 @@ var rows = [
"title": "Disk writes (bytes)",
"derivative": true,
"scaleSeconds": true
},
{
"value_field": "fs.data.available_in_bytes",
"title": "Disk Free space (bytes)"
}
],
"notice": false
@ -370,7 +374,7 @@ dashboard.pulldowns = [
"type": "query",
"collapse": false,
"notice": false,
"enable": true,
"enable": true
},
{
"type": "filtering",

View file

@ -1,5 +1,5 @@
{
"title": "Overview",
"title": "Marvel - Overview",
"services": {
"query": {
"idQueue": [
@ -98,7 +98,7 @@
"linewidth": 2,
"timezone": "browser",
"spyable": true,
"zoomlinks": true,
"zoomlinks": false,
"bars": false,
"stack": false,
"points": false,
@ -111,7 +111,7 @@
"percentage": false,
"zerofill": true,
"interactive": true,
"options": true,
"options": false,
"derivative": false,
"scale": 1,
"tooltip": {
@ -167,10 +167,10 @@
"1y"
],
"fill": 0,
"linewidth": 3,
"linewidth": 2,
"timezone": "browser",
"spyable": true,
"zoomlinks": true,
"zoomlinks": false,
"bars": false,
"stack": false,
"points": false,
@ -183,7 +183,7 @@
"percentage": false,
"zerofill": true,
"interactive": true,
"options": true,
"options": false,
"derivative": true,
"scaleSeconds": true,
"scale": 1,
@ -240,10 +240,10 @@
"1y"
],
"fill": 0,
"linewidth": 3,
"linewidth": 2,
"timezone": "browser",
"spyable": true,
"zoomlinks": true,
"zoomlinks": false,
"bars": false,
"stack": false,
"points": false,
@ -256,7 +256,7 @@
"percentage": false,
"zerofill": true,
"interactive": true,
"options": true,
"options": false,
"derivative": true,
"scaleSeconds": true,
"scale": 1,
@ -316,7 +316,7 @@
"pulldowns": [
{
"type": "query",
"collapse": false,
"collapse": true,
"notice": false,
"enable": true,
"query": "*",

View file

@ -9,6 +9,9 @@
display: inline-block;
vertical-align: middle;
}
.marvel-persistent-name {
font-size: 9pt;
}
.marvel-extended {
display: inline-block;
font-size:9pt;
@ -41,27 +44,28 @@
<table class="table table-bordered" ng-if="!panel.compact">
<thead>
<th>node <a ng-href="{{detailViewLink()}}" target="_blank" class="btn btn-mini btn-info" ng-disabled="!hasSelected(nodes)" bs-tooltip="detailViewTip()" data-placement="right">nodes dashboard</a></th>
<th>node <a id="detail_view_link" ng-href="{{detailViewLink()}}" target="_top" class="btn btn-mini btn-info" ng-disabled="!hasSelected(nodes)" bs-tooltip="detailViewTip()" data-placement="right">nodes dashboard</a></th>
<th ng-repeat="metric in metrics" ng-class="alertClass(warnLevels['_global_'][metric.name])">{{metric.name}}</th>
</thead>
<tr ng-repeat="node in nodes">
<td>
<div class="checkbox">
<label>
<input type="checkbox" ng-model="nodes[$index].selected" ng-checked="nodes[$index].selected">
{{node.name}}
<input type="checkbox" ng-model="node.selected" ng-checked="node.selected">
{{node.display_name}}
<div class="marvel-persistent-name">{{node.id}}</div>
</label>
</div>
</td>
<td ng-repeat="metric in metrics" ng-class="alertClass(warnLevels[node][metric.name])">
<div class="marvel-mean">
{{data[node.name+"_"+metric.name].mean / metric.scale | number:metric.decimals}}<br>
<td ng-repeat="metric in metrics" ng-class="alertClass(warnLevels[node.id][metric.name])">
<div class="marvel-mean pointer" ng-click="metricClick(node,metric)">
{{data[node.id+"_"+metric.name].mean / metric.scale | number:metric.decimals}}<br>
<div class="marvel-nodes-health-chart" series="data[node.name+'_'+metric.name+'_history']"></div>
<div class="marvel-nodes-health-chart" series="data[node.id+'_'+metric.name+'_history']"></div>
</div>
<div class="marvel-extended">
<span>min: {{data[node.name+"_"+metric.name].min / metric.scale | number:metric.decimals}}</span><br>
<span>max: {{data[node.name+"_"+metric.name].max / metric.scale | number:metric.decimals}}</span>
<div class="marvel-extended pointer" ng-click="metricClick(node,metric)">
<span>min: {{data[node.id+"_"+metric.name].min / metric.scale | number:metric.decimals}}</span><br>
<span>max: {{data[node.id+"_"+metric.name].max / metric.scale | number:metric.decimals}}</span>
</div>
</td>
@ -77,14 +81,14 @@
<td>
<div class="checkbox">
<label>
<input type="checkbox" ng-model="nodes[$index].selected" ng-checked="nodes[$index].selected">
{{node.name}}
<input type="checkbox" ng-model="node.selected" ng-checked="node.selected">
</label>
{{ node.display_name }}
</div>
</td>
<td ng-repeat="metric in metrics" ng-class="alertClass(warnLevels[node.name][metric.name])">
<div>{{data[node.name+"_"+metric.name].mean / metric.scale | number:metric.decimals}}
<div class="marvel-nodes-health-chart" series="data[node.name+'_'+metric.name+'_history']"></div>
<td ng-repeat="metric in metrics" ng-class="alertClass(warnLevels[node.id][metric.name])">
<div class="pointer" ng-click="metricClick(node,metric)">{{data[node.id+"_"+metric.name].mean / metric.scale | number:metric.decimals}}
<div class="marvel-nodes-health-chart pointer" ng-click="metricClick(node,metric)" series="data[node.id+'_'+metric.name+'_history']"></div>
</div>
</td>
</tr>

View file

@ -5,300 +5,372 @@ define([
'underscore',
'jquery',
'jquery.flot',
'jquery.flot.time',
'jquery.flot.time'
],
function (angular, app, kbn, _, $) {
'use strict';
function (angular, app, kbn, _, $) {
'use strict';
var module = angular.module('kibana.panels.marvel.nodes_health', []);
app.useModule(module);
var module = angular.module('kibana.panels.marvel.nodes_health', []);
app.useModule(module);
module.controller('marvel.nodes_health', function($scope, dashboard, filterSrv) {
$scope.panelMeta = {
modals : [],
editorTabs : [],
status : "Experimental",
description : "An overview of cluster health, by node."
};
module.controller('marvel.nodes_health', function ($scope, dashboard, filterSrv) {
$scope.panelMeta = {
modals: [],
editorTabs: [],
status: "Experimental",
description: "An overview of cluster health, by node."
};
// Set and populate defaults
var _d = {
compact : false
};
_.defaults($scope.panel,_d);
// Set and populate defaults
var _d = {
compact: false,
node_display_field: "node.name", // used as primary display string for a node.
node_persistent_field: "node.transport_address" // used as node identity - i.e., search queries, facets etc.
$scope.init = function () {
$scope.warnLevels = {};
$scope.nodes = [];
};
_.defaults($scope.panel, _d);
$scope.$on('refresh',function(){
$scope.get_nodes();
});
$scope.get_nodes();
};
$scope.get_nodes = function () {
if(dashboard.indices.length === 0) {
return;
}
var
request,
results;
request = $scope.ejs.Request().indices(dashboard.indices);
request = request
.facet($scope.ejs.TermsFacet('terms')
.field("node.transport_address")
.size(9999999)
.order('term')
.facetFilter(filterSrv.getBoolFilter(filterSrv.ids))).size(0);
results = request.doSearch();
results.then(function(r) {
var newNodes = _.pluck(r.facets.terms.terms,'term');
newNodes = _.map(newNodes, function(n) {
return {
name: n,
selected: false
};
});
$scope.get_data(newNodes);
});
};
$scope.get_data = function(newNodes) {
// Make sure we have everything for the request to complete
if (typeof newNodes === "undefined") {
newNodes = $scope.nodes;
}
if(dashboard.indices.length === 0 || newNodes.length === 0) {
$scope.nodes = newNodes;
return;
}
$scope.panelMeta.loading = true;
var
request,
results;
$scope.metrics = [{
name:'CPU (%)',
field:'process.cpu.percent',
warning:60,
error: 90,
decimals: 2
},{
name:'Load (1m)',
field:'os.load_average.1m',
warning:8,
error: 10,
decimals: 2
},{
name: 'System Mem (%)',
field: 'os.mem.used_percent',
warning: 90,
error: 97,
decimals: 2
},{
name: 'Jvm Mem (%)',
field: 'os.mem.used_percent',
warning: 95,
error: 98,
decimals: 2
},{
name: 'Free disk space (GB)',
field: 'fs.data.available_in_bytes',
scale: 1024 * 1024 * 1024,
warning: { threshold: 5, type: "lower_bound" },
error: { threshold: 2, type: "lower_bound" },
decimals: 2
}];
_.each($scope.metrics, function (m) {
_.defaults(m, {scale : 1});
if (_.isNumber(m.error)) {
m.error = { threshold: m.error, type: "upper_bound"};
}
if (_.isNumber(m.warning)) {
m.warning = { threshold: m.warning, type: "upper_bound"};
}
});
request = $scope.ejs.Request().indices(dashboard.indices);
var time = filterSrv.timeRange('last').to;
time = kbn.parseDate(time).valueOf();
// Terms mode
_.each(_.pluck(newNodes,'name'),function(n) {
var filter = $scope.ejs.BoolFilter()
.must($scope.ejs.RangeFilter('@timestamp').from(time + '||-10m/m'))
.must($scope.ejs.TermsFilter('node.transport_address',n));
_.each($scope.metrics, function(m) {
request = request
.facet($scope.ejs.StatisticalFacet(n+"_"+m.name)
.field(m.field || m.name)
.facetFilter(filter));
request = request.facet($scope.ejs.DateHistogramFacet(n+"_"+m.name+"_history")
.keyField('@timestamp').valueField(m.field || m.name).interval('1m')
.facetFilter(filter)).size(0);
});
});
results = request.doSearch();
// Populate scope when we have results
results.then(function(results) {
$scope.nodes = newNodes;
$scope.data = results.facets;
$scope.panelMeta.loading = false;
$scope.init = function () {
$scope.warnLevels = {};
$scope.calculateWarnings();
});
};
$scope.nodes = [];
$scope.hasSelected = function(nodes) {
return _.some(nodes, function(n){
return n.selected;
});
};
$scope.$on('refresh', function () {
$scope.get_nodes();
});
$scope.detailViewLink = function() {
var nodes = _.pluck(_.where($scope.nodes,{selected:true}),'name');
return "#/dashboard/script/marvel.node_stats.js?show=OS&nodes="+nodes.join(',');
};
$scope.get_nodes();
$scope.detailViewTip = function() {
return $scope.hasSelected($scope.nodes) ? 'Open nodes dashboard for selected nodes' :
'Select nodes and click top open the nodes dashboard';
};
};
$scope.calculateWarnings = function () {
$scope.warnLevels = {_global_: {}};
_.each($scope.metrics, function (metric) {
$scope.warnLevels['_global_'][metric.name] = 0;
_.each(_.pluck($scope.nodes,'name'), function (node) {
var level = $scope.alertLevel(metric, $scope.data[node + '_' + metric.name].mean);
$scope.warnLevels[node] = $scope.warnLevels[node] || {};
$scope.warnLevels[node][metric.name] = level;
if (level > $scope.warnLevels['_global_'][metric.name]) {
$scope.warnLevels['_global_'][metric.name] = level;
$scope.get_nodes = function () {
if (dashboard.indices.length === 0) {
return;
}
var
request,
filter,
results;
filter = filterSrv.getBoolFilter(filterSrv.ids);
request = $scope.ejs.Request().indices(dashboard.indices).size(0).searchType("count");
request.facet(
$scope.ejs.TermsFacet('terms')
.field($scope.panel.node_persistent_field)
.size(9999999)
.order('term')
.facetFilter(filter)
);
results = request.doSearch();
results.then(function (r) {
var newPersistentIds = _.pluck(r.facets.terms.terms, 'term');
if (newPersistentIds.length === 0) {
$scope.get_data([]);
return;
}
var mrequest = $scope.ejs.MultiSearchRequest().indices(dashboard.indices);
_.each(newPersistentIds, function (persistentId) {
var nodeReqeust = $scope.ejs.Request().filter(filter);
nodeReqeust.query(
$scope.ejs.ConstantScoreQuery().query(
$scope.ejs.TermQuery($scope.panel.node_persistent_field, persistentId)
)
);
nodeReqeust.size(1).fields([ $scope.panel.node_display_field, $scope.panel.node_persistent_field]);
nodeReqeust.sort("@timestamp", "desc");
mrequest.requests(nodeReqeust);
});
mrequest.doSearch(function (r) {
var newNodes = [];
_.each(r.responses, function (nodeResponse) {
if (nodeResponse.hits.hits.length === 0) {
return;
}
var hit = nodeResponse.hits.hits[0];
var display_name = hit.fields[$scope.panel.node_display_field];
var persistent_name = hit.fields[$scope.panel.node_persistent_field];
newNodes.push({
display_name: display_name || persistent_name,
id: persistent_name,
selected: ($scope.nodes[persistent_name] || {}).selected
});
});
$scope.get_data(newNodes);
});
});
};
$scope.get_data = function (newNodes) {
// Make sure we have everything for the request to complete
if (newNodes === undefined) {
newNodes = $scope.nodes;
}
if (dashboard.indices.length === 0 || newNodes.length === 0) {
$scope.nodes = newNodes;
return;
}
$scope.panelMeta.loading = true;
var
request,
results;
$scope.metrics = [
{
name: 'CPU (%)',
field: 'process.cpu.percent',
warning: 60,
error: 90,
decimals: 2
},
{
name: 'Load (1m)',
field: 'os.load_average.1m',
warning: 8,
error: 10,
decimals: 2
},
{
name: 'System Mem (%)',
field: 'os.mem.used_percent',
warning: 90,
error: 97,
decimals: 2
},
{
name: 'Jvm Mem (%)',
field: 'os.mem.used_percent',
warning: 95,
error: 98,
decimals: 2
},
{
name: 'Free disk space (GB)',
field: 'fs.data.available_in_bytes',
scale: 1024 * 1024 * 1024,
warning: { threshold: 5, type: "lower_bound" },
error: { threshold: 2, type: "lower_bound" },
decimals: 2
}
];
_.each($scope.metrics, function (m) {
_.defaults(m, {scale: 1});
if (_.isNumber(m.error)) {
m.error = { threshold: m.error, type: "upper_bound"};
}
if (_.isNumber(m.warning)) {
m.warning = { threshold: m.warning, type: "upper_bound"};
}
});
});
};
$scope.alertLevel = function(metric,num) {
var level = 0;
request = $scope.ejs.Request().indices(dashboard.indices);
function testAlert(alert,num) {
if (!alert) {
return false;
var time = filterSrv.timeRange('last').to;
time = kbn.parseDate(time).valueOf();
// Terms mode
_.each(_.pluck(newNodes, 'id'), function (id) {
var filter = $scope.ejs.BoolFilter()
.must($scope.ejs.RangeFilter('@timestamp').from(time + '||-10m/m'))
.must($scope.ejs.TermsFilter($scope.panel.node_persistent_field, id));
_.each($scope.metrics, function (m) {
request = request
.facet($scope.ejs.StatisticalFacet(id + "_" + m.name)
.field(m.field || m.name)
.facetFilter(filter));
request = request.facet($scope.ejs.DateHistogramFacet(id + "_" + m.name + "_history")
.keyField('@timestamp').valueField(m.field || m.name).interval('1m')
.facetFilter(filter)).size(0);
});
});
results = request.doSearch();
// Populate scope when we have results
results.then(function (results) {
$scope.nodes = newNodes;
$scope.data = results.facets;
$scope.panelMeta.loading = false;
$scope.warnLevels = {};
$scope.calculateWarnings();
});
};
$scope.hasSelected = function (nodes) {
return _.some(nodes, function (n) {
return n.selected;
});
};
$scope.metricClick = function (node, metric) {
var current = window.location.href;
var i = current.indexOf('#');
if (i > 0) {
current = current.substr(0, i);
}
return alert.type === "upper_bound" ? num>alert.threshold : num<alert.threshold;
}
current += $scope.detailViewLink([node], [metric.field]);
window.location = current;
};
num /= metric.scale;
if (testAlert(metric.error, num)) {
level = 2;
} else if (testAlert(metric.warning, num)) {
level = 1;
}
$scope.detailViewLink = function (nodes, fields) {
if (nodes === undefined) {
nodes = _.where($scope.nodes, {selected: true});
}
nodes = _.map(nodes, function (node) {
var query = $scope.panel.node_persistent_field + ':"' + node.id + '"';
return {
q: query,
a: node.display_name
};
});
nodes = JSON.stringify(nodes);
var time = filterSrv.timeRange(false);
var show;
if (fields !== undefined) {
show = "&show=" + fields.join(",");
} else {
show = "";
}
return "#/dashboard/script/marvel.node_stats.js?nodes=" + encodeURI(nodes) + "&from=" + time.from + "&to=" + time.to + show;
};
if (document.location.search.match(/panic_demo/)) {
var r = Math.random();
if (r>0.9) {
$scope.detailViewTip = function () {
return $scope.hasSelected($scope.nodes) ? 'Open nodes dashboard for selected nodes' :
'Select nodes and click top open the nodes dashboard';
};
$scope.calculateWarnings = function () {
$scope.warnLevels = {_global_: {}};
_.each($scope.metrics, function (metric) {
$scope.warnLevels._global_[metric.name] = 0;
_.each(_.pluck($scope.nodes, 'id'), function (nodeID) {
var level = $scope.alertLevel(metric, $scope.data[nodeID + '_' + metric.name].mean);
$scope.warnLevels[nodeID] = $scope.warnLevels[nodeID] || {};
$scope.warnLevels[nodeID][metric.name] = level;
if (level > $scope.warnLevels._global_[metric.name]) {
$scope.warnLevels._global_[metric.name] = level;
}
});
});
};
$scope.alertLevel = function (metric, num) {
var level = 0;
function testAlert(alert, num) {
if (!alert) {
return false;
}
return alert.type === "upper_bound" ? num > alert.threshold : num < alert.threshold;
}
num /= metric.scale;
if (testAlert(metric.error, num)) {
level = 2;
} else if (r>0.8) {
} else if (testAlert(metric.warning, num)) {
level = 1;
}
}
return level;
};
$scope.alertClass = function(level) {
if (level >= 2) {
return ['text-error'];
}
if (level >= 1) {
return ['text-warning'];
}
return [];
};
});
module.directive('marvelNodesHealthChart', function() {
return {
restrict: 'C',
scope: {
series: '=',
panel: '='
},
template: '<div></div>',
link: function(scope, elem) {
// Receive render events
scope.$watch('series',function(){
render_panel();
});
// Re-render if the window is resized
angular.element(window).bind('resize', function(){
render_panel();
});
// Function for rendering panel
function render_panel() {
// Populate element
var options = {
legend: { show: false },
series: {
lines: {
show: true,
fill: 0,
lineWidth: 2,
steps: false
},
shadowSize: 1
},
yaxis: {
show: false
},
xaxis: {
show: false,
mode: "time"
},
grid: {
hoverable: false,
show: false
}
};
if(!_.isUndefined(scope.series)) {
var _d = {
data : _.map(scope.series.entries, function(p) {
return [p.time,p.mean];
}),
color : elem.css('color'),
};
$.plot(elem, [_d], options);
if (document.location.search.match(/panic_demo/)) {
var r = Math.random();
if (r > 0.9) {
level = 2;
} else if (r > 0.8) {
level = 1;
}
}
}
};
});
return level;
};
$scope.alertClass = function (level) {
if (level >= 2) {
return ['text-error'];
}
if (level >= 1) {
return ['text-warning'];
}
return [];
};
});
module.directive('marvelNodesHealthChart', function () {
return {
restrict: 'C',
scope: {
series: '=',
panel: '='
},
template: '<div></div>',
link: function (scope, elem) {
// Function for rendering panel
function render_panel() {
// Populate element
var options = {
legend: { show: false },
series: {
lines: {
show: true,
fill: 0,
lineWidth: 2,
steps: false
},
shadowSize: 1
},
yaxis: {
show: false
},
xaxis: {
show: false,
mode: "time"
},
grid: {
hoverable: false,
show: false
}
};
if (!_.isUndefined(scope.series)) {
var _d = {
data: _.map(scope.series.entries, function (p) {
return [p.time, p.mean];
}),
color: elem.css('color')
};
$.plot(elem, [_d], options);
}
}
// Receive render events
scope.$watch('series', function () {
render_panel();
});
// Re-render if the window is resized
angular.element(window).bind('resize', function () {
render_panel();
});
}
};
});
});
});