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="section">
<div class="editor-option">
<label class="small">Persistent id field <tip>choose a field that does not change on node restart</tip></label>
<input type="text" bs-typeahead="fields.list" class="input-large" ng-model="panel.node_persistent_field"/>
</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"/>
<label class="small">Mode</label>
<select class="input-small" ng-model="panel.mode" ng-options="f for f in ['nodes','indices']"></select>
</div>
</div>
</div>
@ -68,7 +64,7 @@
</table>
<form class="form-inline">
<select ng-model="metricEditor.add">
<option ng-repeat="metric in addMetricOptions()"
<option ng-repeat="metric in addMetricOptions(panel.mode)"
value="{{metric.field}}">
{{metric.name}}
</option>

View file

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

View file

@ -24,8 +24,9 @@ define([
// 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.
mode: 'nodes',
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' ]
};
_.defaults($scope.panel, _d);
@ -37,71 +38,137 @@ define([
add: undefined
};
// The allowed metrics and their defaults, from which we can create a select list
$scope.availableMetrics = [
{
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"
$scope.modeInfo = {
nodes: {
defaults: {
persistent_name: "node.name",
persistent_field: "node.transport_address",
metrics: [ 'process.cpu.percent', 'os.load_average.1m', 'os.mem.used_percent', 'fs.data.available_in_bytes' ]
},
error: {
threshold: 2,
type: "lower_bound"
availableMetrics: [
{
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
},
{
name: 'Field data size (MB)',
field: 'indices.fielddata.memory_size_in_bytes',
scale: 1024 * 1024
},
{
// allow people to add a new, not-predefined metric.
name: 'Custom',
field: ''
availableMetrics: [
{
name: 'Documents',
field: 'primaries.docs.count',
},
{
name: 'Index Rate',
field: 'primaries.indexing.index_total',
rate: true
},
{
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.warnLevels = {};
$scope.nodes = [];
$scope.panel.metrics = _.map($scope.panel.metrics, function (m) {
return metricDefaults(m);
});
$scope.rows = [];
$scope.$on('refresh', function () {
$scope.get_nodes();
$scope.get_rows();
});
$scope.get_nodes();
};
$scope.get_nodes = function () {
$scope.get_rows = function () {
if (dashboard.indices.length === 0) {
return;
}
@ -116,7 +183,7 @@ define([
request = $scope.ejs.Request().indices(dashboard.indices).size(0).searchType("count");
request.facet(
$scope.ejs.TermsFacet('terms')
.field($scope.panel.node_persistent_field)
.field($scope.panel.persistent_field)
.size(9999999)
.order('term')
.facetFilter(filter)
@ -125,59 +192,67 @@ define([
results = request.doSearch();
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) {
$scope.get_data([]);
// This seems more obvious if this is the point
$scope.rows = [];
// $scope.get_data([]);
return;
}
var mrequest = $scope.ejs.MultiSearchRequest().indices(dashboard.indices);
mrequest = $scope.ejs.MultiSearchRequest().indices(dashboard.indices);
_.each(newPersistentIds, function (persistentId) {
var nodeReqeust = $scope.ejs.Request().filter(filter);
nodeReqeust.query(
var rowRequest = $scope.ejs.Request().filter(filter);
rowRequest.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]);
nodeReqeust.sort("@timestamp", "desc");
mrequest.requests(nodeReqeust);
rowRequest.size(1).fields([ $scope.panel.display_field, $scope.panel.persistent_field]);
rowRequest.sort("@timestamp", "desc");
mrequest.requests(rowRequest);
});
mrequest.doSearch(function (r) {
var newNodes = [];
_.each(r.responses, function (nodeResponse) {
if (nodeResponse.hits.hits.length === 0) {
var newRows = [],
hit,
display_name,
persistent_name;
_.each(r.responses, function (response) {
if (response.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({
hit = response.hits.hits[0];
display_name = hit.fields[$scope.panel.display_field];
persistent_name = hit.fields[$scope.panel.persistent_field];
newRows.push({
display_name: display_name || 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
if (newNodes === undefined) {
newNodes = $scope.nodes;
if (_.isUndefined(newRows)) {
newRows = $scope.rows;
}
if (dashboard.indices.length === 0 || newNodes.length === 0) {
$scope.nodes = newNodes;
if (dashboard.indices.length === 0 || newRows.length === 0) {
$scope.rows = newRows;
return;
}
@ -190,10 +265,10 @@ define([
var time = filterSrv.timeRange('last').to;
time = kbn.parseDate(time).valueOf();
_.each(_.pluck(newNodes, 'id'), function (id) {
_.each(_.pluck(newRows, '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));
.must($scope.ejs.RangeFilter('@timestamp').from(time + '||-10m'))
.must($scope.ejs.TermsFilter($scope.panel.persistent_field, id));
_.each($scope.panel.metrics, function (m) {
request = request
@ -210,7 +285,7 @@ define([
// Populate scope when we have results
results.then(function (results) {
$scope.nodes = newNodes;
$scope.rows = newRows;
$scope.data = results.facets;
$scope.panelMeta.loading = false;
$scope.warnLevels = {};
@ -241,10 +316,10 @@ define([
$scope.detailViewLink = function (nodes, fields) {
if (_.isUndefined(nodes)) {
nodes = _.where($scope.nodes, {selected: true});
nodes = _.where($scope.rows, {selected: true});
}
nodes = _.map(nodes, function (node) {
var query = $scope.panel.node_persistent_field + ':"' + node.id + '"';
var query = $scope.panel.persistent_field + ':"' + node.id + '"';
return {
q: query,
a: node.display_name
@ -258,12 +333,12 @@ define([
} else {
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;
};
$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';
};
@ -271,10 +346,16 @@ define([
$scope.warnLevels = {_global_: {}};
_.each($scope.panel.metrics, function (metric) {
$scope.warnLevels._global_[metric.field] = 0;
_.each(_.pluck($scope.nodes, 'id'), function (nodeID) {
var level = $scope.alertLevel(metric, ($scope.data[nodeID + '_' + metric.field] || {}).mean);
$scope.warnLevels[nodeID] = $scope.warnLevels[nodeID] || {};
$scope.warnLevels[nodeID][metric.field] = level;
_.each(_.pluck($scope.rows, 'id'), function (id) {
var num, level;
if(metric.rate) {
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]) {
$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) {
metric = metricDefaults(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');
return _.filter($scope.availableMetrics, function (value) {
return _.filter($scope.modeInfo[m].availableMetrics, function (value) {
return !_.contains(fields, value.field);
});
};
@ -417,7 +483,8 @@ define([
restrict: 'C',
scope: {
series: '=',
panel: '='
panel: '=',
field: '='
},
template: '<div></div>',
link: function (scope, elem) {
@ -450,9 +517,14 @@ define([
};
if (!_.isUndefined(scope.series)) {
var metric = _.findWhere(scope.panel.metrics, { "field": scope.field });
var _d = {
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')
};