mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #463 from rashidkpc/templated_json
Templated and scriptable dashboard options
This commit is contained in:
commit
9798f8be30
@ -15,7 +15,7 @@ module.exports = function (grunt) {
|
|||||||
' Licensed <%= pkg.license %> */\n\n'
|
' Licensed <%= pkg.license %> */\n\n'
|
||||||
},
|
},
|
||||||
jshint: {
|
jshint: {
|
||||||
files: ['Gruntfile.js', 'js/*.js', 'panels/*/*.js' ],
|
files: ['Gruntfile.js', 'js/*.js', 'panels/*/*.js', 'dashboards/*.js' ],
|
||||||
options: {
|
options: {
|
||||||
jshintrc: '.jshintrc'
|
jshintrc: '.jshintrc'
|
||||||
}
|
}
|
||||||
|
174
dashboards/logstash.js
Normal file
174
dashboards/logstash.js
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
/*
|
||||||
|
* Complex scripted Logstash dashboard
|
||||||
|
* This script generates a dashboard object that Kibana can load. It also takes a number of user
|
||||||
|
* supplied URL parameters, none are required:
|
||||||
|
*
|
||||||
|
* index :: Which index to search? If this is specified, interval is set to 'none'
|
||||||
|
* pattern :: Does nothing if index is specified. Set a timestamped index pattern. Default: [logstash-]YYYY.MM.DD
|
||||||
|
* interval :: Sets the index interval (eg: day,week,month,year), Default: day
|
||||||
|
*
|
||||||
|
* split :: The character to split the queries on Default: ','
|
||||||
|
* query :: By default, a comma seperated list of queries to run. Default: *
|
||||||
|
*
|
||||||
|
* from :: Search this amount of time back, eg 15m, 1h, 2d. Default: 15m
|
||||||
|
* timefield :: The field containing the time to filter on, Default: @timestamp
|
||||||
|
*
|
||||||
|
* fields :: comma seperated list of fields to show in the table
|
||||||
|
* sort :: comma seperated field to sort on, and direction, eg sort=@timestamp,desc
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Setup some variables
|
||||||
|
var dashboard, queries, _d_timespan;
|
||||||
|
|
||||||
|
// All url parameters are available via the ARGS object
|
||||||
|
var ARGS;
|
||||||
|
|
||||||
|
// Set a default timespan if one isn't specified
|
||||||
|
_d_timespan = '1h';
|
||||||
|
|
||||||
|
// Intialize a skeleton with nothing but a rows array and service object
|
||||||
|
dashboard = {
|
||||||
|
rows : [],
|
||||||
|
services : {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set a title
|
||||||
|
dashboard.title = 'Logstash Search';
|
||||||
|
|
||||||
|
// Allow the user to set the index, if they dont, fall back to logstash.
|
||||||
|
if(!_.isUndefined(ARGS.index)) {
|
||||||
|
dashboard.index = {
|
||||||
|
default: ARGS.index,
|
||||||
|
interval: 'none'
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Don't fail to default
|
||||||
|
dashboard.failover = false;
|
||||||
|
dashboard.index = {
|
||||||
|
default: ARGS.index||'ADD_A_TIME_FILTER',
|
||||||
|
pattern: ARGS.pattern||'[logstash-]YYYY.MM.DD',
|
||||||
|
interval: ARGS.interval||'day'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// In this dashboard we let users pass queries as comma seperated list to the query parameter.
|
||||||
|
// Or they can specify a split character using the split aparameter
|
||||||
|
// If query is defined, split it into a list of query objects
|
||||||
|
// NOTE: ids must be integers, hence the parseInt()s
|
||||||
|
if(!_.isUndefined(ARGS.query)) {
|
||||||
|
queries = _.object(_.map(ARGS.query.split(ARGS.split||','), function(v,k) {
|
||||||
|
return [k,{
|
||||||
|
query: v,
|
||||||
|
id: parseInt(k,10),
|
||||||
|
alias: v
|
||||||
|
}];
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
// No queries passed? Initialize a single query to match everything
|
||||||
|
queries = {
|
||||||
|
0: {
|
||||||
|
query: '*',
|
||||||
|
id: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// This isn't strictly needed, but it gets rid of the info alert about the missing time filter
|
||||||
|
dashboard.services.filter = {
|
||||||
|
list: {
|
||||||
|
0: {
|
||||||
|
from: kbn.time_ago(ARGS.from||_d_timespan),
|
||||||
|
to: new Date(),
|
||||||
|
field: ARGS.timefield||"@timestamp",
|
||||||
|
type: "time",
|
||||||
|
active: true,
|
||||||
|
id: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ids: [0]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ok, lets make some rows. The Filters row is collapsed by default
|
||||||
|
dashboard.rows = [
|
||||||
|
{
|
||||||
|
title: "Options",
|
||||||
|
height: "30px"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Query",
|
||||||
|
height: "30px"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Filters",
|
||||||
|
height: "100px",
|
||||||
|
collapse: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Chart",
|
||||||
|
height: "300px"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Events",
|
||||||
|
height: "400px"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Setup some panels. A query panel and a filter panel on the same row
|
||||||
|
dashboard.rows[0].panels = [
|
||||||
|
{
|
||||||
|
type: 'timepicker',
|
||||||
|
span: 6,
|
||||||
|
timespan: ARGS.from||_d_timespan
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'dashcontrol',
|
||||||
|
span: 3
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add a filtering panel to the 3rd row
|
||||||
|
dashboard.rows[1].panels = [
|
||||||
|
{
|
||||||
|
type: 'Query'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
// Add a filtering panel to the 3rd row
|
||||||
|
dashboard.rows[2].panels = [
|
||||||
|
{
|
||||||
|
type: 'filtering'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// And a histogram that allows the user to specify the interval and time field
|
||||||
|
dashboard.rows[3].panels = [
|
||||||
|
{
|
||||||
|
type: 'histogram',
|
||||||
|
time_field: ARGS.timefield||"@timestamp",
|
||||||
|
auto_int: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// And a table row where you can specify field and sort order
|
||||||
|
dashboard.rows[4].panels = [
|
||||||
|
{
|
||||||
|
type: 'table',
|
||||||
|
fields: !_.isUndefined(ARGS.fields) ? ARGS.fields.split(',') : ['@timestamp','@message'],
|
||||||
|
sort: !_.isUndefined(ARGS.sort) ? ARGS.sort.split(',') : [ARGS.timefield||'@timestamp','desc'],
|
||||||
|
overflow: 'expand'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Now return the object and we're good!
|
||||||
|
return dashboard;
|
@ -3,14 +3,11 @@
|
|||||||
"services": {
|
"services": {
|
||||||
"query": {
|
"query": {
|
||||||
"idQueue": [
|
"idQueue": [
|
||||||
1,
|
1
|
||||||
2,
|
|
||||||
3,
|
|
||||||
4
|
|
||||||
],
|
],
|
||||||
"list": {
|
"list": {
|
||||||
"0": {
|
"0": {
|
||||||
"query": "*",
|
"query": "{{ARGS.query || '*'}}",
|
||||||
"alias": "",
|
"alias": "",
|
||||||
"color": "#7EB26D",
|
"color": "#7EB26D",
|
||||||
"id": 0
|
"id": 0
|
||||||
@ -22,8 +19,7 @@
|
|||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"idQueue": [
|
"idQueue": [
|
||||||
1,
|
1
|
||||||
2
|
|
||||||
],
|
],
|
||||||
"list": {
|
"list": {
|
||||||
"0": {
|
"0": {
|
||||||
@ -70,7 +66,7 @@
|
|||||||
"7d",
|
"7d",
|
||||||
"30d"
|
"30d"
|
||||||
],
|
],
|
||||||
"timespan": "1h",
|
"timespan": "{{ARGS.from || '1h'}}",
|
||||||
"timefield": "@timestamp",
|
"timefield": "@timestamp",
|
||||||
"timeformat": "",
|
"timeformat": "",
|
||||||
"refresh": {
|
"refresh": {
|
||||||
|
@ -48,10 +48,10 @@ labjs.wait(function(){
|
|||||||
.when('/dashboard', {
|
.when('/dashboard', {
|
||||||
templateUrl: 'partials/dashboard.html',
|
templateUrl: 'partials/dashboard.html',
|
||||||
})
|
})
|
||||||
.when('/dashboard/:type/:id', {
|
.when('/dashboard/:kbnType/:kbnId', {
|
||||||
templateUrl: 'partials/dashboard.html',
|
templateUrl: 'partials/dashboard.html',
|
||||||
})
|
})
|
||||||
.when('/dashboard/:type/:id/:params', {
|
.when('/dashboard/:kbnType/:kbnId/:params', {
|
||||||
templateUrl: 'partials/dashboard.html'
|
templateUrl: 'partials/dashboard.html'
|
||||||
})
|
})
|
||||||
.otherwise({
|
.otherwise({
|
||||||
|
112
js/services.js
112
js/services.js
@ -252,6 +252,13 @@ angular.module('kibana.services', [])
|
|||||||
ids : [],
|
ids : [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Defaults for query objects
|
||||||
|
var _query = {
|
||||||
|
query: '*',
|
||||||
|
alias: '',
|
||||||
|
pin: false,
|
||||||
|
type: 'lucene'
|
||||||
|
};
|
||||||
// For convenience
|
// For convenience
|
||||||
var ejs = ejsResource(config.elasticsearch);
|
var ejs = ejsResource(config.elasticsearch);
|
||||||
var _q = dashboard.current.services.query;
|
var _q = dashboard.current.services.query;
|
||||||
@ -275,6 +282,12 @@ angular.module('kibana.services', [])
|
|||||||
self.list = dashboard.current.services.query.list;
|
self.list = dashboard.current.services.query.list;
|
||||||
self.ids = dashboard.current.services.query.ids;
|
self.ids = dashboard.current.services.query.ids;
|
||||||
|
|
||||||
|
// Check each query object, populate its defaults
|
||||||
|
_.each(self.list,function(query,id) {
|
||||||
|
_.defaults(query,_query);
|
||||||
|
query.color = colorAt(id);
|
||||||
|
});
|
||||||
|
|
||||||
if (self.ids.length === 0) {
|
if (self.ids.length === 0) {
|
||||||
self.set({});
|
self.set({});
|
||||||
}
|
}
|
||||||
@ -290,16 +303,12 @@ angular.module('kibana.services', [])
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var _id = nextId();
|
var _id = query.id || nextId();
|
||||||
var _query = {
|
query.id = _id;
|
||||||
query: '*',
|
query.color = query.color || colorAt(_id);
|
||||||
alias: '',
|
|
||||||
color: colorAt(_id),
|
|
||||||
pin: false,
|
|
||||||
id: _id,
|
|
||||||
type: 'lucene'
|
|
||||||
};
|
|
||||||
_.defaults(query,_query);
|
_.defaults(query,_query);
|
||||||
|
|
||||||
|
|
||||||
self.list[_id] = query;
|
self.list[_id] = query;
|
||||||
self.ids.push(_id);
|
self.ids.push(_id);
|
||||||
return _id;
|
return _id;
|
||||||
@ -373,11 +382,13 @@ angular.module('kibana.services', [])
|
|||||||
.service('filterSrv', function(dashboard, ejsResource) {
|
.service('filterSrv', function(dashboard, ejsResource) {
|
||||||
// Create an object to hold our service state on the dashboard
|
// Create an object to hold our service state on the dashboard
|
||||||
dashboard.current.services.filter = dashboard.current.services.filter || {};
|
dashboard.current.services.filter = dashboard.current.services.filter || {};
|
||||||
_.defaults(dashboard.current.services.filter,{
|
|
||||||
|
// Defaults for it
|
||||||
|
var _d = {
|
||||||
idQueue : [],
|
idQueue : [],
|
||||||
list : {},
|
list : {},
|
||||||
ids : []
|
ids : []
|
||||||
});
|
};
|
||||||
|
|
||||||
// For convenience
|
// For convenience
|
||||||
var ejs = ejsResource(config.elasticsearch);
|
var ejs = ejsResource(config.elasticsearch);
|
||||||
@ -388,6 +399,9 @@ angular.module('kibana.services', [])
|
|||||||
|
|
||||||
// Call this whenever we need to reload the important stuff
|
// Call this whenever we need to reload the important stuff
|
||||||
this.init = function() {
|
this.init = function() {
|
||||||
|
// Populate defaults
|
||||||
|
_.defaults(dashboard.current.services.filter,_d);
|
||||||
|
|
||||||
// Accessors
|
// Accessors
|
||||||
self.list = dashboard.current.services.filter.list;
|
self.list = dashboard.current.services.filter.list;
|
||||||
self.ids = dashboard.current.services.filter.ids;
|
self.ids = dashboard.current.services.filter.ids;
|
||||||
@ -592,9 +606,9 @@ angular.module('kibana.services', [])
|
|||||||
|
|
||||||
var route = function() {
|
var route = function() {
|
||||||
// Is there a dashboard type and id in the URL?
|
// Is there a dashboard type and id in the URL?
|
||||||
if(!(_.isUndefined($routeParams.type)) && !(_.isUndefined($routeParams.id))) {
|
if(!(_.isUndefined($routeParams.kbnType)) && !(_.isUndefined($routeParams.kbnId))) {
|
||||||
var _type = $routeParams.type;
|
var _type = $routeParams.kbnType;
|
||||||
var _id = $routeParams.id;
|
var _id = $routeParams.kbnId;
|
||||||
|
|
||||||
switch(_type) {
|
switch(_type) {
|
||||||
case ('elasticsearch'):
|
case ('elasticsearch'):
|
||||||
@ -606,6 +620,9 @@ angular.module('kibana.services', [])
|
|||||||
case ('file'):
|
case ('file'):
|
||||||
self.file_load(_id);
|
self.file_load(_id);
|
||||||
break;
|
break;
|
||||||
|
case('script'):
|
||||||
|
self.script_load(_id);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
self.file_load('default.json');
|
self.file_load('default.json');
|
||||||
}
|
}
|
||||||
@ -642,9 +659,7 @@ angular.module('kibana.services', [])
|
|||||||
if(self.current.failover) {
|
if(self.current.failover) {
|
||||||
self.indices = [self.current.index.default];
|
self.indices = [self.current.index.default];
|
||||||
} else {
|
} else {
|
||||||
alertSrv.set('No indices matched','The pattern <i>'+self.current.index.pattern+
|
|
||||||
'</i> did not match any indices in your selected'+
|
|
||||||
' time range.','info',5000);
|
|
||||||
// Do not issue refresh if no indices match. This should be removed when panels
|
// Do not issue refresh if no indices match. This should be removed when panels
|
||||||
// properly understand when no indices are present
|
// properly understand when no indices are present
|
||||||
return false;
|
return false;
|
||||||
@ -653,10 +668,14 @@ angular.module('kibana.services', [])
|
|||||||
$rootScope.$broadcast('refresh');
|
$rootScope.$broadcast('refresh');
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// This is not optimal, we should be getting the entire index list here, or at least every
|
if(self.current.failover) {
|
||||||
// index that possibly matches the pattern
|
|
||||||
self.indices = [self.current.index.default];
|
self.indices = [self.current.index.default];
|
||||||
$rootScope.$broadcast('refresh');
|
$rootScope.$broadcast('refresh');
|
||||||
|
} else {
|
||||||
|
alertSrv.set("No time filter",
|
||||||
|
'Timestamped indices are configured without a failover. Waiting for time filter.',
|
||||||
|
'info',5000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.indices = [self.current.index.default];
|
self.indices = [self.current.index.default];
|
||||||
@ -665,6 +684,7 @@ angular.module('kibana.services', [])
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.dash_load = function(dashboard) {
|
this.dash_load = function(dashboard) {
|
||||||
|
|
||||||
// Cancel all timers
|
// Cancel all timers
|
||||||
timer.cancel_all();
|
timer.cancel_all();
|
||||||
|
|
||||||
@ -744,11 +764,32 @@ angular.module('kibana.services', [])
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var renderTemplate = function(json,params) {
|
||||||
|
var _r;
|
||||||
|
_.templateSettings = {interpolate : /\{\{(.+?)\}\}/g};
|
||||||
|
var template = _.template(json);
|
||||||
|
var rendered = template({ARGS:params});
|
||||||
|
|
||||||
|
try {
|
||||||
|
_r = angular.fromJson(rendered);
|
||||||
|
} catch(e) {
|
||||||
|
_r = false;
|
||||||
|
}
|
||||||
|
return _r;
|
||||||
|
};
|
||||||
|
|
||||||
this.file_load = function(file) {
|
this.file_load = function(file) {
|
||||||
return $http({
|
return $http({
|
||||||
url: "dashboards/"+file,
|
url: "dashboards/"+file,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
transformResponse: function(response) {
|
||||||
|
return renderTemplate(response,$routeParams);
|
||||||
|
}
|
||||||
}).then(function(result) {
|
}).then(function(result) {
|
||||||
|
if(!result) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var _dashboard = result.data;
|
var _dashboard = result.data;
|
||||||
_.defaults(_dashboard,_dash);
|
_.defaults(_dashboard,_dash);
|
||||||
self.dash_load(_dashboard);
|
self.dash_load(_dashboard);
|
||||||
@ -759,11 +800,13 @@ angular.module('kibana.services', [])
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
this.elasticsearch_load = function(type,id) {
|
this.elasticsearch_load = function(type,id) {
|
||||||
return $http({
|
return $http({
|
||||||
url: config.elasticsearch + "/" + config.kibana_index + "/"+type+"/"+id,
|
url: config.elasticsearch + "/" + config.kibana_index + "/"+type+"/"+id,
|
||||||
method: "GET"
|
method: "GET",
|
||||||
|
transformResponse: function(response) {
|
||||||
|
return renderTemplate(angular.fromJson(response)['_source']['dashboard'],$routeParams);
|
||||||
|
}
|
||||||
}).error(function(data, status, headers, conf) {
|
}).error(function(data, status, headers, conf) {
|
||||||
if(status === 0) {
|
if(status === 0) {
|
||||||
alertSrv.set('Error',"Could not contact Elasticsearch at "+config.elasticsearch+
|
alertSrv.set('Error',"Could not contact Elasticsearch at "+config.elasticsearch+
|
||||||
@ -774,7 +817,32 @@ angular.module('kibana.services', [])
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}).success(function(data, status, headers) {
|
}).success(function(data, status, headers) {
|
||||||
self.dash_load(angular.fromJson(data['_source']['dashboard']));
|
self.dash_load(data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.script_load = function(file) {
|
||||||
|
return $http({
|
||||||
|
url: "dashboards/"+file,
|
||||||
|
method: "GET",
|
||||||
|
transformResponse: function(response) {
|
||||||
|
/*jshint -W054 */
|
||||||
|
var _f = new Function("ARGS",response);
|
||||||
|
return _f($routeParams);
|
||||||
|
}
|
||||||
|
}).then(function(result) {
|
||||||
|
if(!result) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var _dashboard = result.data;
|
||||||
|
_.defaults(_dashboard,_dash);
|
||||||
|
self.dash_load(_dashboard);
|
||||||
|
return true;
|
||||||
|
},function(result) {
|
||||||
|
alertSrv.set('Error',
|
||||||
|
"Could not load <i>scripts/"+file+"</i>. Please make sure it exists and returns a valid dashboard" ,
|
||||||
|
'error');
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -135,8 +135,6 @@ angular.module('kibana.histogram', [])
|
|||||||
if(dashboard.indices.length === 0) {
|
if(dashboard.indices.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var _range = $scope.get_time_range();
|
var _range = $scope.get_time_range();
|
||||||
var _interval = $scope.get_interval(_range);
|
var _interval = $scope.get_interval(_range);
|
||||||
|
|
||||||
@ -177,6 +175,7 @@ angular.module('kibana.histogram', [])
|
|||||||
// Then run it
|
// Then run it
|
||||||
var results = request.doSearch();
|
var results = request.doSearch();
|
||||||
|
|
||||||
|
|
||||||
// Populate scope when we have results
|
// Populate scope when we have results
|
||||||
results.then(function(results) {
|
results.then(function(results) {
|
||||||
$scope.panelMeta.loading = false;
|
$scope.panelMeta.loading = false;
|
||||||
|
Loading…
Reference in New Issue
Block a user