Commit 9798f8be by Rashid Khan

Merge pull request #463 from rashidkpc/templated_json

Templated and scriptable dashboard options
parents dfb9388f 859c3996
...@@ -15,7 +15,7 @@ module.exports = function (grunt) { ...@@ -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'
} }
......
/*
* 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 @@ ...@@ -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 @@ ...@@ -22,8 +19,7 @@
}, },
"filter": { "filter": {
"idQueue": [ "idQueue": [
1, 1
2
], ],
"list": { "list": {
"0": { "0": {
...@@ -70,7 +66,7 @@ ...@@ -70,7 +66,7 @@
"7d", "7d",
"30d" "30d"
], ],
"timespan": "1h", "timespan": "{{ARGS.from || '1h'}}",
"timefield": "@timestamp", "timefield": "@timestamp",
"timeformat": "", "timeformat": "",
"refresh": { "refresh": {
...@@ -246,4 +242,4 @@ ...@@ -246,4 +242,4 @@
"pattern": "[logstash-]YYYY.MM.DD", "pattern": "[logstash-]YYYY.MM.DD",
"default": "NO_TIME_FILTER_OR_INDEX_PATTERN_NOT_MATCHED" "default": "NO_TIME_FILTER_OR_INDEX_PATTERN_NOT_MATCHED"
} }
} }
\ No newline at end of file
...@@ -48,10 +48,10 @@ labjs.wait(function(){ ...@@ -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({
......
...@@ -252,6 +252,13 @@ angular.module('kibana.services', []) ...@@ -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', []) ...@@ -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', []) ...@@ -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', []) ...@@ -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', []) ...@@ -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', []) ...@@ -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', []) ...@@ -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', []) ...@@ -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', []) ...@@ -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', []) ...@@ -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', []) ...@@ -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', []) ...@@ -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', []) ...@@ -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', []) ...@@ -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', []) ...@@ -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;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment