Added switch between nodes and indices mode, added rate parameter to metrics

This commit is contained in:
Rashid Khan 2013-11-13 12:04:22 -07:00
parent 06e35fd439
commit 9eba0055b2
5 changed files with 220 additions and 145 deletions

View file

@ -0,0 +1 @@
// STUB

View file

@ -2,12 +2,8 @@
<div class="editor-row"> <div class="editor-row">
<div class="section"> <div class="section">
<div class="editor-option"> <div class="editor-option">
<label class="small">Persistent id field <tip>choose a field that does not change on node restart</tip></label> <label class="small">Mode</label>
<input type="text" bs-typeahead="fields.list" class="input-large" ng-model="panel.node_persistent_field"/> <select class="input-small" ng-model="panel.mode" ng-options="f for f in ['nodes','indices']"></select>
</div>
<div class="editor-option" ng-show="panel.mode != 'count'">
<label class="small">Display field <tip>will be used as the name of each row</tip></label>
<input type="text" bs-typeahead="fields.list" class="input-large" ng-model="panel.node_display_field"/>
</div> </div>
</div> </div>
</div> </div>
@ -68,7 +64,7 @@
</table> </table>
<form class="form-inline"> <form class="form-inline">
<select ng-model="metricEditor.add"> <select ng-model="metricEditor.add">
<option ng-repeat="metric in addMetricOptions()" <option ng-repeat="metric in addMetricOptions(panel.mode)"
value="{{metric.field}}"> value="{{metric.field}}">
{{metric.name}} {{metric.name}}
</option> </option>

View file

@ -21,7 +21,7 @@
.marvel-header { .marvel-header {
margin-bottom: 10px; margin-bottom: 10px;
} }
.marvel-header .nodes{ .marvel-header .count{
font-size: 20pt; font-size: 20pt;
font-weight: bold; font-weight: bold;
margin-left: 10px; margin-left: 10px;
@ -34,8 +34,8 @@
} }
</style> </style>
<div class="pull-left marvel-header marvel-table" ng-show="nodes.length > 0"> <div class="pull-left marvel-header marvel-table" ng-show="rows.length > 0">
<span class="nodes">{{nodes.length}} nodes</span> / Last 10m </span> <span class="count">{{rows.length}} {{panel.mode}}</span> / Last 10m </span>
</div> </div>
<div class="pull-right"> <div class="pull-right">
<a href="" ng-class="{strong:!panel.compact}" ng-click="panel.compact=false">Full</a> / <a href="" ng-class="{strong:!panel.compact}" ng-click="panel.compact=false">Full</a> /
@ -45,30 +45,33 @@
<table class="table table-bordered" ng-if="!panel.compact"> <table class="table table-bordered" ng-if="!panel.compact">
<thead> <thead>
<th ng-if="hasSelected(nodes)">node <a id="detail_view_link" ng-href="{{detailViewLink()}}" class="btn btn-mini btn-info" bs-tooltip="detailViewTip()" data-placement="right">nodes dashboard</a></th> <th ng-if="hasSelected(rows)">{{panel.mode}} <a id="detail_view_link" ng-href="{{detailViewLink()}}" class="btn btn-mini btn-info" bs-tooltip="detailViewTip()" data-placement="right">Dashboard</a></th>
<th ng-if="!hasSelected(nodes)">node <a id="detail_view_link" disabled class="btn btn-mini btn-info" bs-tooltip="detailViewTip()" data-placement="right">nodes dashboard</a></th> <th ng-if="!hasSelected(rows)">{{panel.mode}} <a id="detail_view_link" disabled class="btn btn-mini btn-info" bs-tooltip="detailViewTip()" data-placement="right">Dashboard</a></th>
<th ng-repeat="metric in panel.metrics" ng-class="alertClass(warnLevels['_global_'][metric.field])">{{metric.name}}</th> <th ng-repeat="metric in panel.metrics" ng-class="alertClass(warnLevels['_global_'][metric.field])">{{metric.name}}</th>
</thead> </thead>
<tr ng-repeat="node in nodes"> <tr ng-repeat="row in rows">
<td> <td>
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" ng-model="node.selected" ng-checked="node.selected"> <input type="checkbox" ng-model="row.selected" ng-checked="row.selected">
{{node.display_name}} {{row.display_name}}
<div class="marvel-persistent-name">{{node.id}}</div> <div class="marvel-persistent-name">{{row.id}}</div>
</label> </label>
</div> </div>
</td> </td>
<td ng-repeat="metric in panel.metrics" ng-class="alertClass(warnLevels[node.id][metric.field])"> <td ng-repeat="metric in panel.metrics" ng-class="alertClass(warnLevels[row.id][metric.field])">
<div class="marvel-mean pointer" ng-click="metricClick(node,metric)"> <div class="marvel-mean pointer" ng-click="metricClick(row,metric)">
{{data[node.id+"_"+metric.field].mean / metric.scale | number:metric.decimals}}<br> <span ng-hide="metric.rate">{{data[row.id+"_"+metric.field].mean / metric.scale | number:metric.decimals}}</span>
<span ng-show="metric.rate">{{(data[row.id+"_"+metric.field].max-data[row.id+"_"+metric.field].min) / 600 / metric.scale | number:metric.decimals}}</span>
<div class="marvel-nodes-health-chart" series="data[node.id+'_'+metric.field+'_history']"></div> <br>
<div class="marvel-nodes-health-chart" panel='panel' field="metric.field" series="data[row.id+'_'+metric.field+'_history']"></div>
</div> </div>
<div class="marvel-extended pointer" ng-click="metricClick(node,metric)"> <div class="marvel-extended pointer" ng-click="metricClick(row,metric)" ng-hide="metric.rate">
<span>min: {{data[node.id+"_"+metric.field].min / metric.scale | number:metric.decimals}}</span><br> <span>min: {{data[row.id+"_"+metric.field].min / metric.scale | number:metric.decimals}}</span><br>
<span>max: {{data[node.id+"_"+metric.field].max / metric.scale | number:metric.decimals}}</span> <span>max: {{data[row.id+"_"+metric.field].max / metric.scale | number:metric.decimals}}</span>
</div> </div>
</td> </td>
</tr> </tr>
@ -76,21 +79,24 @@
<table class="table table-bordered table-condensed marvel-table" ng-if="panel.compact"> <table class="table table-bordered table-condensed marvel-table" ng-if="panel.compact">
<thead> <thead>
<th>node <a ng-href="{{detailViewLink()}}" class="btn btn-mini btn-info" ng-disabled="!hasSelected(nodes)" bs-tooltip="detailViewTip()" data-placement="right">nodes dashboard</a></th> <th>{{panel.mode}} <a ng-href="{{detailViewLink()}}" class="btn btn-mini btn-info" ng-disabled="!hasSelected(rows)" bs-tooltip="detailViewTip()" data-placement="right">Dashboard</a></th>
<th ng-repeat="metric in panel.metrics" ng-class="alertClass(warnLevels['_global_'][metric.field])">{{metric.name}}</th> <th ng-repeat="metric in panel.metrics" ng-class="alertClass(warnLevels['_global_'][metric.field])">{{metric.name}}</th>
</thead> </thead>
<tr ng-repeat="node in nodes"> <tr ng-repeat="row in rows">
<td> <td>
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" ng-model="node.selected" ng-checked="node.selected"> <input type="checkbox" ng-model="row.selected" ng-checked="row.selected">
</label> </label>
{{ node.display_name }} {{ row.display_name }}
</div> </div>
</td> </td>
<td ng-repeat="metric in panel.metrics" ng-class="alertClass(warnLevels[node.id][metric.field])"> <td ng-repeat="metric in panel.metrics" ng-class="alertClass(warnLevels[row.id][metric.field])">
<div class="pointer" ng-click="metricClick(node,metric)">{{data[node.id+"_"+metric.field].mean / metric.scale | number:metric.decimals}} <div class="pointer" ng-click="metricClick(row,metric)">
<div class="marvel-nodes-health-chart pointer" ng-click="metricClick(node,metric)" series="data[node.id+'_'+metric.field+'_history']"></div> <span ng-hide="metric.rate">{{data[row.id+"_"+metric.field].mean / metric.scale | number:metric.decimals}}</span>
<span ng-show="metric.rate">{{(data[row.id+"_"+metric.field].max-data[row.id+"_"+metric.field].min) / 600 / metric.scale | number:metric.decimals}}</span>
<div class="marvel-nodes-health-chart pointer" ng-click="metricClick(row,metric)" panel='panel' field="metric.field" series="data[row.id+'_'+metric.field+'_history']"></div>
</div> </div>
</td> </td>
</tr> </tr>

View file

@ -24,8 +24,9 @@ define([
// Set and populate defaults // Set and populate defaults
var _d = { var _d = {
compact: false, compact: false,
node_display_field: "node.name", // used as primary display string for a node. mode: 'nodes',
node_persistent_field: "node.transport_address", // used as node identity - i.e., search queries, facets etc. display_field: "node.name", // used as primary display string for a node.
persistent_field: "node.transport_address", // used as node identity - i.e., search queries, facets etc.
metrics: [ 'process.cpu.percent', 'os.load_average.1m', 'os.mem.used_percent', 'fs.data.available_in_bytes' ] metrics: [ 'process.cpu.percent', 'os.load_average.1m', 'os.mem.used_percent', 'fs.data.available_in_bytes' ]
}; };
_.defaults($scope.panel, _d); _.defaults($scope.panel, _d);
@ -37,71 +38,137 @@ define([
add: undefined add: undefined
}; };
// The allowed metrics and their defaults, from which we can create a select list $scope.modeInfo = {
$scope.availableMetrics = [ nodes: {
{ defaults: {
name: 'CPU (%)', persistent_name: "node.name",
field: 'process.cpu.percent', persistent_field: "node.transport_address",
warning: 60, metrics: [ 'process.cpu.percent', 'os.load_average.1m', 'os.mem.used_percent', 'fs.data.available_in_bytes' ]
error: 90
},
{
name: 'Load (1m)',
field: 'os.load_average.1m',
warning: 8,
error: 10
},
{
name: 'Jvm Mem (%)',
field: 'os.mem.used_percent',
warning: 95,
error: 98
}
,
{
name: 'Free disk space (GB)',
field: 'fs.data.available_in_bytes',
warning: {
threshold: 5,
type: "lower_bound"
}, },
error: { availableMetrics: [
threshold: 2, {
type: "lower_bound" name: 'CPU (%)',
field: 'process.cpu.percent',
warning: 60,
error: 90
},
{
name: 'Load (1m)',
field: 'os.load_average.1m',
warning: 8,
error: 10
},
{
name: 'Jvm Mem (%)',
field: 'os.mem.used_percent',
warning: 95,
error: 98
},
{
name: 'Free disk space (GB)',
field: 'fs.data.available_in_bytes',
warning: {
threshold: 5,
type: "lower_bound"
},
error: {
threshold: 2,
type: "lower_bound"
},
scale: 1024 * 1024 * 1024
},
/* Dropping this until we have error handling for fields that don't exist
{
// allow people to add a new, not-predefined metric.
name: 'Custom',
field: ''
}
*/
]
},
indices: {
defaults: {
persistent_name: 'index',
persistent_field: 'index',
metrics: [ 'primaries.docs.count', 'primaries.indexing.index_total', 'total.search.query_total', 'total.merges.current' ]
}, },
scale: 1024 * 1024 * 1024 availableMetrics: [
}, {
{ name: 'Documents',
name: 'Field data size (MB)', field: 'primaries.docs.count',
field: 'indices.fielddata.memory_size_in_bytes', },
scale: 1024 * 1024 {
}, name: 'Index Rate',
{ field: 'primaries.indexing.index_total',
// allow people to add a new, not-predefined metric. rate: true
name: 'Custom', },
field: '' {
name: 'Search Rate',
field: 'total.search.query_total',
rate: true,
},
{
name: 'Merges',
field: 'total.merges.current',
},
/* Dropping this until we have error handling for fields that don't exist
{
// allow people to add a new, not-predefined metric.
name: 'Custom',
field: ''
}
*/
]
} }
]; };
var metricDefaults = function (m) {
if(_.isUndefined($scope.modeInfo[$scope.panel.mode])) {
return [];
}
if (_.isString(m)) {
m = { "field": m };
}
m = _.defaults(m, _.findWhere($scope.modeInfo[$scope.panel.mode].availableMetrics, { "field": m.field }));
var _metric_defaults = {field: "", decimals: 2, scale: 1};
m = _.defaults(m, _metric_defaults);
if (_.isNumber(m.error)) {
m.error = { threshold: m.error, type: "upper_bound"};
}
if (_.isNumber(m.warning)) {
m.warning = { threshold: m.warning, type: "upper_bound"};
}
return m;
};
$scope.panel.metrics = _.map($scope.panel.metrics, function (m) {
return metricDefaults(m);
});
$scope.$watch('panel.mode',function(m) {
if(_.isUndefined(m)) {
return;
}
$scope.panel.display_field = $scope.modeInfo[m].defaults.display_field;
$scope.panel.persistent_field = $scope.modeInfo[m].defaults.persistent_field;
$scope.panel.metrics = _.map($scope.modeInfo[m].defaults.metrics, function (m) {
return metricDefaults(m);
});
_.throttle($scope.get_rows(),500);
});
$scope.init = function () { $scope.init = function () {
$scope.warnLevels = {}; $scope.warnLevels = {};
$scope.nodes = []; $scope.rows = [];
$scope.panel.metrics = _.map($scope.panel.metrics, function (m) {
return metricDefaults(m);
});
$scope.$on('refresh', function () { $scope.$on('refresh', function () {
$scope.get_nodes(); $scope.get_rows();
}); });
$scope.get_nodes();
}; };
$scope.get_rows = function () {
$scope.get_nodes = function () {
if (dashboard.indices.length === 0) { if (dashboard.indices.length === 0) {
return; return;
} }
@ -116,7 +183,7 @@ define([
request = $scope.ejs.Request().indices(dashboard.indices).size(0).searchType("count"); request = $scope.ejs.Request().indices(dashboard.indices).size(0).searchType("count");
request.facet( request.facet(
$scope.ejs.TermsFacet('terms') $scope.ejs.TermsFacet('terms')
.field($scope.panel.node_persistent_field) .field($scope.panel.persistent_field)
.size(9999999) .size(9999999)
.order('term') .order('term')
.facetFilter(filter) .facetFilter(filter)
@ -125,59 +192,67 @@ define([
results = request.doSearch(); results = request.doSearch();
results.then(function (r) { results.then(function (r) {
var newPersistentIds = _.pluck(r.facets.terms.terms, 'term'); var newPersistentIds = _.pluck(r.facets.terms.terms, 'term'),
mrequest;
if (newPersistentIds.length === 0) { if (newPersistentIds.length === 0) {
$scope.get_data([]); // This seems more obvious if this is the point
$scope.rows = [];
// $scope.get_data([]);
return; return;
} }
var mrequest = $scope.ejs.MultiSearchRequest().indices(dashboard.indices); mrequest = $scope.ejs.MultiSearchRequest().indices(dashboard.indices);
_.each(newPersistentIds, function (persistentId) { _.each(newPersistentIds, function (persistentId) {
var nodeReqeust = $scope.ejs.Request().filter(filter); var rowRequest = $scope.ejs.Request().filter(filter);
nodeReqeust.query( rowRequest.query(
$scope.ejs.ConstantScoreQuery().query( $scope.ejs.ConstantScoreQuery().query(
$scope.ejs.TermQuery($scope.panel.node_persistent_field, persistentId) $scope.ejs.TermQuery($scope.panel.persistent_field, persistentId)
) )
); );
nodeReqeust.size(1).fields([ $scope.panel.node_display_field, $scope.panel.node_persistent_field]); rowRequest.size(1).fields([ $scope.panel.display_field, $scope.panel.persistent_field]);
nodeReqeust.sort("@timestamp", "desc"); rowRequest.sort("@timestamp", "desc");
mrequest.requests(nodeReqeust); mrequest.requests(rowRequest);
}); });
mrequest.doSearch(function (r) { mrequest.doSearch(function (r) {
var newNodes = []; var newRows = [],
_.each(r.responses, function (nodeResponse) { hit,
if (nodeResponse.hits.hits.length === 0) { display_name,
persistent_name;
_.each(r.responses, function (response) {
if (response.hits.hits.length === 0) {
return; return;
} }
var hit = nodeResponse.hits.hits[0]; hit = response.hits.hits[0];
var display_name = hit.fields[$scope.panel.node_display_field]; display_name = hit.fields[$scope.panel.display_field];
var persistent_name = hit.fields[$scope.panel.node_persistent_field]; persistent_name = hit.fields[$scope.panel.persistent_field];
newNodes.push({
newRows.push({
display_name: display_name || persistent_name, display_name: display_name || persistent_name,
id: persistent_name, id: persistent_name,
selected: ($scope.nodes[persistent_name] || {}).selected selected: ($scope.rows[persistent_name] || {}).selected
}); });
}); });
$scope.get_data(newNodes); $scope.get_data(newRows);
}); });
}); });
}; };
$scope.get_data = function (newNodes) { $scope.get_data = function (newRows) {
// Make sure we have everything for the request to complete // Make sure we have everything for the request to complete
if (newNodes === undefined) { if (_.isUndefined(newRows)) {
newNodes = $scope.nodes; newRows = $scope.rows;
} }
if (dashboard.indices.length === 0 || newNodes.length === 0) { if (dashboard.indices.length === 0 || newRows.length === 0) {
$scope.nodes = newNodes; $scope.rows = newRows;
return; return;
} }
@ -190,10 +265,10 @@ define([
var time = filterSrv.timeRange('last').to; var time = filterSrv.timeRange('last').to;
time = kbn.parseDate(time).valueOf(); time = kbn.parseDate(time).valueOf();
_.each(_.pluck(newNodes, 'id'), function (id) { _.each(_.pluck(newRows, 'id'), function (id) {
var filter = $scope.ejs.BoolFilter() var filter = $scope.ejs.BoolFilter()
.must($scope.ejs.RangeFilter('@timestamp').from(time + '||-10m/m')) .must($scope.ejs.RangeFilter('@timestamp').from(time + '||-10m'))
.must($scope.ejs.TermsFilter($scope.panel.node_persistent_field, id)); .must($scope.ejs.TermsFilter($scope.panel.persistent_field, id));
_.each($scope.panel.metrics, function (m) { _.each($scope.panel.metrics, function (m) {
request = request request = request
@ -210,7 +285,7 @@ define([
// Populate scope when we have results // Populate scope when we have results
results.then(function (results) { results.then(function (results) {
$scope.nodes = newNodes; $scope.rows = newRows;
$scope.data = results.facets; $scope.data = results.facets;
$scope.panelMeta.loading = false; $scope.panelMeta.loading = false;
$scope.warnLevels = {}; $scope.warnLevels = {};
@ -241,10 +316,10 @@ define([
$scope.detailViewLink = function (nodes, fields) { $scope.detailViewLink = function (nodes, fields) {
if (_.isUndefined(nodes)) { if (_.isUndefined(nodes)) {
nodes = _.where($scope.nodes, {selected: true}); nodes = _.where($scope.rows, {selected: true});
} }
nodes = _.map(nodes, function (node) { nodes = _.map(nodes, function (node) {
var query = $scope.panel.node_persistent_field + ':"' + node.id + '"'; var query = $scope.panel.persistent_field + ':"' + node.id + '"';
return { return {
q: query, q: query,
a: node.display_name a: node.display_name
@ -258,12 +333,12 @@ define([
} else { } else {
show = ""; show = "";
} }
return "#/dashboard/script/marvel.node_stats.js?nodes=" + encodeURI(nodes) + "&from=" + return "#/dashboard/script/marvel."+$scope.panel.mode+"_stats.js?ids=" + encodeURI(nodes) + "&from=" +
time.from + "&to=" + time.to + show; time.from + "&to=" + time.to + show;
}; };
$scope.detailViewTip = function () { $scope.detailViewTip = function () {
return $scope.hasSelected($scope.nodes) ? 'Open nodes dashboard for selected nodes' : return $scope.hasSelected($scope.rows) ? 'Open nodes dashboard for selected nodes' :
'Select nodes and click top open the nodes dashboard'; 'Select nodes and click top open the nodes dashboard';
}; };
@ -271,10 +346,16 @@ define([
$scope.warnLevels = {_global_: {}}; $scope.warnLevels = {_global_: {}};
_.each($scope.panel.metrics, function (metric) { _.each($scope.panel.metrics, function (metric) {
$scope.warnLevels._global_[metric.field] = 0; $scope.warnLevels._global_[metric.field] = 0;
_.each(_.pluck($scope.nodes, 'id'), function (nodeID) { _.each(_.pluck($scope.rows, 'id'), function (id) {
var level = $scope.alertLevel(metric, ($scope.data[nodeID + '_' + metric.field] || {}).mean); var num, level;
$scope.warnLevels[nodeID] = $scope.warnLevels[nodeID] || {}; if(metric.rate) {
$scope.warnLevels[nodeID][metric.field] = level; num = ($scope.data[id + '_' + metric.field].max - $scope.data[id + '_' + metric.field].max)/600;
} else {
num = $scope.data[id + '_' + metric.field].mean;
}
level = $scope.alertLevel(metric, num);
$scope.warnLevels[id] = $scope.warnLevels[id] || {};
$scope.warnLevels[id][metric.field] = level;
if (level > $scope.warnLevels._global_[metric.field]) { if (level > $scope.warnLevels._global_[metric.field]) {
$scope.warnLevels._global_[metric.field] = level; $scope.warnLevels._global_[metric.field] = level;
} }
@ -342,25 +423,6 @@ define([
}; };
var metricDefaults = function (m) {
if (typeof m === "string") {
m = { "field": m };
}
m = _.defaults(m, _.findWhere($scope.availableMetrics, { "field": m.field }));
var _metric_defaults = {field: "", decimals: 2, scale: 1};
m = _.defaults(m, _metric_defaults);
if (_.isNumber(m.error)) {
m.error = { threshold: m.error, type: "upper_bound"};
}
if (_.isNumber(m.warning)) {
m.warning = { threshold: m.warning, type: "upper_bound"};
}
return m;
};
$scope.addMetric = function (metric) { $scope.addMetric = function (metric) {
metric = metricDefaults(metric); metric = metricDefaults(metric);
$scope.panel.metrics.push(metric); $scope.panel.metrics.push(metric);
@ -370,9 +432,13 @@ define([
} }
}; };
$scope.addMetricOptions = function () { // This is expensive, it would be better to populate a scope object
$scope.addMetricOptions = function (m) {
if(_.isUndefined($scope.modeInfo[m])) {
return [];
}
var fields = _.pluck($scope.panel.metrics, 'field'); var fields = _.pluck($scope.panel.metrics, 'field');
return _.filter($scope.availableMetrics, function (value) { return _.filter($scope.modeInfo[m].availableMetrics, function (value) {
return !_.contains(fields, value.field); return !_.contains(fields, value.field);
}); });
}; };
@ -417,7 +483,8 @@ define([
restrict: 'C', restrict: 'C',
scope: { scope: {
series: '=', series: '=',
panel: '=' panel: '=',
field: '='
}, },
template: '<div></div>', template: '<div></div>',
link: function (scope, elem) { link: function (scope, elem) {
@ -450,9 +517,14 @@ define([
}; };
if (!_.isUndefined(scope.series)) { if (!_.isUndefined(scope.series)) {
var metric = _.findWhere(scope.panel.metrics, { "field": scope.field });
var _d = { var _d = {
data: _.map(scope.series.entries, function (p) { data: _.map(scope.series.entries, function (p) {
return [p.time, p.mean]; if(metric.rate) {
return [p.time, (p.max-p.min)];
} else {
return [p.time, p.mean];
}
}), }),
color: elem.css('color') color: elem.css('color')
}; };