Procházet zdrojové kódy

Merge pull request #463 from rashidkpc/templated_json

Templated and scriptable dashboard options
Rashid Khan před 12 roky
rodič
revize
9798f8be30
6 změnil soubory, kde provedl 275 přidání a 38 odebrání
  1. 1 1
      Gruntfile.js
  2. 174 0
      dashboards/logstash.js
  3. 5 9
      dashboards/logstash.json
  4. 2 2
      js/app.js
  5. 92 24
      js/services.js
  6. 1 2
      panels/histogram/module.js

+ 1 - 1
Gruntfile.js

@@ -15,7 +15,7 @@ module.exports = function (grunt) {
         ' Licensed <%= pkg.license %> */\n\n'
     },
     jshint: {
-      files: ['Gruntfile.js', 'js/*.js', 'panels/*/*.js' ],
+      files: ['Gruntfile.js', 'js/*.js', 'panels/*/*.js', 'dashboards/*.js' ],
       options: {
         jshintrc: '.jshintrc'
       }

+ 174 - 0
dashboards/logstash.js

@@ -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;

+ 5 - 9
dashboards/logstash.json

@@ -3,14 +3,11 @@
   "services": {
     "query": {
       "idQueue": [
-        1,
-        2,
-        3,
-        4
+        1
       ],
       "list": {
         "0": {
-          "query": "*",
+          "query": "{{ARGS.query || '*'}}",
           "alias": "",
           "color": "#7EB26D",
           "id": 0
@@ -22,8 +19,7 @@
     },
     "filter": {
       "idQueue": [
-        1,
-        2
+        1
       ],
       "list": {
         "0": {
@@ -70,7 +66,7 @@
             "7d",
             "30d"
           ],
-          "timespan": "1h",
+          "timespan": "{{ARGS.from || '1h'}}",
           "timefield": "@timestamp",
           "timeformat": "",
           "refresh": {
@@ -246,4 +242,4 @@
     "pattern": "[logstash-]YYYY.MM.DD",
     "default": "NO_TIME_FILTER_OR_INDEX_PATTERN_NOT_MATCHED"
   }
-}
+}

+ 2 - 2
js/app.js

@@ -48,10 +48,10 @@ labjs.wait(function(){
         .when('/dashboard', {
           templateUrl: 'partials/dashboard.html',
         })
-        .when('/dashboard/:type/:id', {
+        .when('/dashboard/:kbnType/:kbnId', {
           templateUrl: 'partials/dashboard.html',
         })
-        .when('/dashboard/:type/:id/:params', {
+        .when('/dashboard/:kbnType/:kbnId/:params', {
           templateUrl: 'partials/dashboard.html'
         })
         .otherwise({

+ 92 - 24
js/services.js

@@ -252,6 +252,13 @@ angular.module('kibana.services', [])
     ids : [],
   });
 
+  // Defaults for query objects
+  var _query = {
+    query: '*',
+    alias: '',
+    pin: false,
+    type: 'lucene'
+  };
   // For convenience
   var ejs = ejsResource(config.elasticsearch);
   var _q = dashboard.current.services.query;
@@ -275,6 +282,12 @@ angular.module('kibana.services', [])
     self.list = dashboard.current.services.query.list;
     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) {
       self.set({});
     }
@@ -290,16 +303,12 @@ angular.module('kibana.services', [])
         return false;
       }
     } else {
-      var _id = nextId();
-      var _query = {
-        query: '*',
-        alias: '',
-        color: colorAt(_id),
-        pin: false,
-        id: _id,
-        type: 'lucene'
-      };
+      var _id = query.id || nextId();
+      query.id = _id;
+      query.color = query.color || colorAt(_id);
       _.defaults(query,_query);
+
+
       self.list[_id] = query;
       self.ids.push(_id);
       return _id;
@@ -373,11 +382,13 @@ angular.module('kibana.services', [])
 .service('filterSrv', function(dashboard, ejsResource) {
   // Create an object to hold our service state on the dashboard
   dashboard.current.services.filter = dashboard.current.services.filter || {};
-  _.defaults(dashboard.current.services.filter,{
+
+  // Defaults for it
+  var _d = {
     idQueue : [],
     list : {},
     ids : []
-  });
+  };
 
   // For convenience
   var ejs = ejsResource(config.elasticsearch);
@@ -388,6 +399,9 @@ angular.module('kibana.services', [])
 
   // Call this whenever we need to reload the important stuff
   this.init = function() {
+    // Populate defaults
+    _.defaults(dashboard.current.services.filter,_d);
+
     // Accessors
     self.list = dashboard.current.services.filter.list;
     self.ids = dashboard.current.services.filter.ids;
@@ -592,9 +606,9 @@ angular.module('kibana.services', [])
 
   var route = function() {
     // Is there a dashboard type and id in the URL?
-    if(!(_.isUndefined($routeParams.type)) && !(_.isUndefined($routeParams.id))) {
-      var _type = $routeParams.type;
-      var _id = $routeParams.id;
+    if(!(_.isUndefined($routeParams.kbnType)) && !(_.isUndefined($routeParams.kbnId))) {
+      var _type = $routeParams.kbnType;
+      var _id = $routeParams.kbnId;
 
       switch(_type) {
       case ('elasticsearch'):
@@ -606,6 +620,9 @@ angular.module('kibana.services', [])
       case ('file'):
         self.file_load(_id);
         break;
+      case('script'):
+        self.script_load(_id);
+        break;
       default:
         self.file_load('default.json');
       }
@@ -642,9 +659,7 @@ angular.module('kibana.services', [])
             if(self.current.failover) {
               self.indices = [self.current.index.default];
             } 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
               // properly understand when no indices are present
               return false;
@@ -653,10 +668,14 @@ angular.module('kibana.services', [])
           $rootScope.$broadcast('refresh');
         });
       } else {
-        // This is not optimal, we should be getting the entire index list here, or at least every
-        // index that possibly matches the pattern
-        self.indices = [self.current.index.default];
-        $rootScope.$broadcast('refresh');
+        if(self.current.failover) {
+          self.indices = [self.current.index.default];
+          $rootScope.$broadcast('refresh');
+        } else {
+          alertSrv.set("No time filter",
+            'Timestamped indices are configured without a failover. Waiting for time filter.',
+            'info',5000);
+        }
       }
     } else {
       self.indices = [self.current.index.default];
@@ -665,6 +684,7 @@ angular.module('kibana.services', [])
   };
 
   this.dash_load = function(dashboard) {
+
     // Cancel all timers
     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) {
     return $http({
       url: "dashboards/"+file,
       method: "GET",
+      transformResponse: function(response) {
+        return renderTemplate(response,$routeParams);
+      }
     }).then(function(result) {
+      if(!result) {
+        return false;
+      }
+
       var _dashboard = result.data;
       _.defaults(_dashboard,_dash);
       self.dash_load(_dashboard);
@@ -759,11 +800,13 @@ angular.module('kibana.services', [])
     });
   };
 
-
   this.elasticsearch_load = function(type,id) {
     return $http({
       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) {
       if(status === 0) {
         alertSrv.set('Error',"Could not contact Elasticsearch at "+config.elasticsearch+
@@ -774,7 +817,32 @@ angular.module('kibana.services', [])
       }
       return false;
     }).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;
     });
   };
 

+ 1 - 2
panels/histogram/module.js

@@ -135,8 +135,6 @@ angular.module('kibana.histogram', [])
     if(dashboard.indices.length === 0) {
       return;
     }
-
-
     var _range = $scope.get_time_range();
     var _interval = $scope.get_interval(_range);
 
@@ -177,6 +175,7 @@ angular.module('kibana.histogram', [])
     // Then run it
     var results = request.doSearch();
 
+
     // Populate scope when we have results
     results.then(function(results) {
       $scope.panelMeta.loading = false;