From c5902ab8764aaba47b2b3a278528f735b4712536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 6 Dec 2013 14:53:05 +0100 Subject: [PATCH] graphite works with modified histogram panel --- src/app/components/require.config.js | 13 +- src/app/dashboards/default.json | 129 +++- src/app/panels/filtering/editor.html | 7 + src/app/panels/filtering/meta.html | 15 + src/app/panels/filtering/module.html | 90 +++ src/app/panels/filtering/module.js | 80 ++ src/app/panels/graph/module.js | 67 +- src/app/panels/graphite/editor.html | 48 ++ src/app/panels/graphite/graphiteUtil.js | 151 ++++ src/app/panels/graphite/interval.js | 57 ++ src/app/panels/graphite/module.html | 97 +++ src/app/panels/graphite/module.js | 703 ++++++++++++++++++ src/app/panels/graphite/queriesEditor.html | 43 ++ src/app/panels/graphite/styleEditor.html | 88 +++ src/app/panels/graphite/timeSeries.js | 216 ++++++ src/app/panels/histogram/editor.html | 48 ++ src/app/panels/histogram/interval.js | 57 ++ src/app/panels/histogram/module.html | 99 +++ src/app/panels/histogram/module.js | 775 ++++++++++++++++++++ src/app/panels/histogram/queriesEditor.html | 43 ++ src/app/panels/histogram/styleEditor.html | 88 +++ src/app/panels/histogram/timeSeries.js | 216 ++++++ src/config.js | 5 +- 23 files changed, 3066 insertions(+), 69 deletions(-) create mode 100644 src/app/panels/filtering/editor.html create mode 100644 src/app/panels/filtering/meta.html create mode 100644 src/app/panels/filtering/module.html create mode 100644 src/app/panels/filtering/module.js create mode 100644 src/app/panels/graphite/editor.html create mode 100644 src/app/panels/graphite/graphiteUtil.js create mode 100644 src/app/panels/graphite/interval.js create mode 100644 src/app/panels/graphite/module.html create mode 100644 src/app/panels/graphite/module.js create mode 100644 src/app/panels/graphite/queriesEditor.html create mode 100644 src/app/panels/graphite/styleEditor.html create mode 100644 src/app/panels/graphite/timeSeries.js create mode 100644 src/app/panels/histogram/editor.html create mode 100644 src/app/panels/histogram/interval.js create mode 100644 src/app/panels/histogram/module.html create mode 100644 src/app/panels/histogram/module.js create mode 100644 src/app/panels/histogram/queriesEditor.html create mode 100644 src/app/panels/histogram/styleEditor.html create mode 100644 src/app/panels/histogram/timeSeries.js diff --git a/src/app/components/require.config.js b/src/app/components/require.config.js index e9c5ba59574..ba6cd679992 100644 --- a/src/app/components/require.config.js +++ b/src/app/components/require.config.js @@ -44,8 +44,8 @@ require.config({ modernizr: '../vendor/modernizr-2.6.1', elasticjs: '../vendor/elasticjs/elastic-angular-client', - 'ts-widget': '../vendor/timeserieswidget/jquery.tswidget', - 'ts-graphite-helpers': '../vendor/timeserieswidget/graphite_helpers' + 'ts-widget': '../vendor/timeserieswidget/jquery.tswidget', + 'ts-graphite-helpers': '../vendor/timeserieswidget/graphite_helpers' }, shim: { underscore: { @@ -96,14 +96,7 @@ require.config({ elasticjs: ['angular', '../vendor/elasticjs/elastic'], - 'ts-widget': [ - 'jquery', - 'jquery.flot', - 'jquery.flot.selection', - 'jquery.flot.stack', - 'jquery.flot.time', - 'ts-graphite-helpers' - ] + 'ts-widget': ['jquery', 'jquery.flot', 'jquery.flot.selection', 'jquery.flot.stack', 'jquery.flot.time', 'ts-graphite-helpers'] }, waitSeconds: 60, }); diff --git a/src/app/dashboards/default.json b/src/app/dashboards/default.json index 7733c47f030..bd3df035893 100644 --- a/src/app/dashboards/default.json +++ b/src/app/dashboards/default.json @@ -17,8 +17,21 @@ ] }, "filter": { - "list": {}, - "ids": [] + "list": { + "0": { + "type": "time", + "field": "@timestamp", + "from": "now-1h", + "to": "now", + "mandate": "must", + "active": true, + "alias": "", + "id": 0 + } + }, + "ids": [ + 0 + ] } }, "rows": [ @@ -30,28 +43,99 @@ "collapsable": false, "panels": [ { + "title": "graphite", "error": false, - "span": 4, - "editable": false, - "group": [ - "default" - ], - "type": "text", - "mode": "markdown", - "content": "hej!", - "style": {}, - "title": "", - "status": "Stable" - }, - { - "error": false, - "span": 8, + "span": 12, "editable": true, "group": [ "default" ], - "type": "graph", - "someprop": "hej from config" + "type": "graphite", + "spyable": true + } + ], + "notice": true + }, + { + "title": "Intro", + "height": "150px", + "editable": true, + "collapse": false, + "collapsable": false, + "panels": [ + { + "span": 12, + "editable": true, + "group": [ + "default" + ], + "type": "histogram", + "mode": "count", + "time_field": "@timestamp", + "value_field": null, + "x-axis": true, + "y-axis": true, + "scale": 1, + "y_format": "none", + "grid": { + "max": null, + "min": 0 + }, + "queries": { + "mode": "all", + "ids": [ + 0 + ] + }, + "annotate": { + "enable": false, + "query": "*", + "size": 20, + "field": "_type", + "sort": [ + "_score", + "desc" + ] + }, + "auto_int": true, + "resolution": 100, + "interval": "30s", + "intervals": [ + "auto", + "1s", + "1m", + "5m", + "10m", + "30m", + "1h", + "3h", + "12h", + "1d", + "1w", + "1y" + ], + "lines": false, + "fill": 0, + "linewidth": 3, + "points": false, + "pointradius": 5, + "bars": true, + "stack": true, + "spyable": true, + "zoomlinks": true, + "options": true, + "legend": true, + "show_query": true, + "interactive": true, + "legend_counts": true, + "timezone": "browser", + "percentage": false, + "zerofill": true, + "derivative": false, + "tooltip": { + "value_type": "cumulative", + "query_as_alias": true + } } ], "notice": true @@ -60,7 +144,7 @@ "editable": true, "index": { "interval": "none", - "pattern": "[logstash-]YYYY.MM.DD", + "pattern": "applogs-error", "default": "_all", "warm_fields": false }, @@ -97,7 +181,10 @@ "2h", "1d" ], - "timefield": "@timestamp" + "timefield": "@timestamp", + "enable": true, + "now": true, + "filter_id": 0 } ], "loader": { diff --git a/src/app/panels/filtering/editor.html b/src/app/panels/filtering/editor.html new file mode 100644 index 00000000000..3100415c7f2 --- /dev/null +++ b/src/app/panels/filtering/editor.html @@ -0,0 +1,7 @@ +
+
+
+ No options here +
+
+
\ No newline at end of file diff --git a/src/app/panels/filtering/meta.html b/src/app/panels/filtering/meta.html new file mode 100644 index 00000000000..8af862ff5bf --- /dev/null +++ b/src/app/panels/filtering/meta.html @@ -0,0 +1,15 @@ +
+ + × +
Query Alias
+
+ +
+ +
+
+
\ No newline at end of file diff --git a/src/app/panels/filtering/module.html b/src/app/panels/filtering/module.html new file mode 100644 index 00000000000..a0a0a677a94 --- /dev/null +++ b/src/app/panels/filtering/module.html @@ -0,0 +1,90 @@ +
+ + +
+ +
No filters available
+
+
+
+ {{filterSrv.list[id].type}} + + {{filterSrv.list[id].mandate}} + + + {{filterSrv.list[id].mandate}} + + + + + + + + + +
+ +
+
    +
  • + {{key}} : {{value}} +
  • +
+
+
+
    +
  • + {{key}} : +
  • +
+
+ + +
+
+
+ +
+
\ No newline at end of file diff --git a/src/app/panels/filtering/module.js b/src/app/panels/filtering/module.js new file mode 100644 index 00000000000..aaa2fc9c804 --- /dev/null +++ b/src/app/panels/filtering/module.js @@ -0,0 +1,80 @@ +/* + + ## filtering + +*/ +define([ + 'angular', + 'app', + 'underscore' +], +function (angular, app, _) { + 'use strict'; + + var module = angular.module('kibana.panels.filtering', []); + app.useModule(module); + + module.controller('filtering', function($scope, filterSrv, $rootScope, dashboard) { + + $scope.panelMeta = { + status : "Stable", + description : "A controllable list of all filters currently applied to the dashboard. You "+ + "almost certainly want one of these on your dashboard somewhere." + }; + + // Set and populate defaults + var _d = { + }; + _.defaults($scope.panel,_d); + + $scope.$on('filter', function() { + $scope.row.notice = true; + }); + + $scope.init = function() { + $scope.filterSrv = filterSrv; + }; + + $scope.remove = function(id) { + filterSrv.remove(id); + }; + + // This function should be moved to the service + $scope.toggle = function(id) { + filterSrv.list[id].active = !filterSrv.list[id].active; + dashboard.refresh(); + }; + + $scope.add = function(query) { + query = query || '*'; + filterSrv.set({ + editing : true, + type : 'querystring', + query : query, + mandate : 'must' + },undefined,true); + }; + + $scope.refresh = function() { + dashboard.refresh(); + }; + + $scope.render = function() { + $rootScope.$broadcast('render'); + }; + + $scope.show_key = function(key) { + return !_.contains(['type','id','alias','mandate','active','editing'],key); + }; + + $scope.isEditable = function(filter) { + var uneditable = ['time']; + if(_.contains(uneditable,filter.type)) { + return false; + } else { + return true; + } + }; + + }); +}); \ No newline at end of file diff --git a/src/app/panels/graph/module.js b/src/app/panels/graph/module.js index bd69ff01af9..bddc8c64ee8 100644 --- a/src/app/panels/graph/module.js +++ b/src/app/panels/graph/module.js @@ -5,7 +5,7 @@ define([ 'underscore', 'ts-widget' ], -function ($, angular, app, _, timeseriesWidget) { +function ($, angular, app, _) { 'use strict'; var module = angular.module('kibana.panels.graph', []); @@ -33,47 +33,42 @@ function ($, angular, app, _, timeseriesWidget) { angular .module('kibana.directives') .directive('mychart', function () { - return { + return { restrict: 'E', - link: function (scope, elem, attrs) { - var tsData = { - graphite_url: 'http://localhost:3030/data', - from: '-24hours', - until: 'now', - height: '300', - width: '740', - targets: [ - { - name: 'series 1', - color: '#CC6699', - target: 'random1', - } - ], - title: 'horizontal title', - vtitle: 'vertical title', - drawNullAsZero: false, - legend: { container: '#legend_flot_simple', noColumns: 1 }, + link: function (scope, elem) { + var tsData = { + from: '-30d', + until: 'now', + height: '300', + width: '740', + targets: [ + { + name: 'series 1', + color: '#CC6699', + target: 'randomWalk("random1")', + }, + { + name: 'series 2', + color: '#006699', + target: 'randomWalk("random2")', + } + ], + title: 'horizontal title', + vtitle: 'vertical title', + drawNullAsZero: false, + state: 'stacked', + hover_details: true, + legend: { container: '#legend_flot_simple', noColumns: 4 }, }; + $("#chart_flot").graphiteFlot(tsData, function(err) { console.log(err); }); - console.log('asd'); - $(elem).html('NJEEEJ!'); - /*// If the data changes somehow, update it in the chart - scope.$watch('data', function(v){ - if(!chart){ - chart = $.plot(elem, v , options); - elem.show(); - }else{ - chart.setData(v); - chart.setupGrid(); - chart.draw(); - } - });*/ + console.log('asd'); + $(elem).html('NJEEEJ!'); } - }; - - }); + }; + }); }); \ No newline at end of file diff --git a/src/app/panels/graphite/editor.html b/src/app/panels/graphite/editor.html new file mode 100644 index 00000000000..15490701bb4 --- /dev/null +++ b/src/app/panels/graphite/editor.html @@ -0,0 +1,48 @@ +
+
+
Values
+
+ + +
+
+ + +
+
+
+
Transform Series
+
+ + +
+
+ +
+
+ +
+
+
+
Time Options
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
\ No newline at end of file diff --git a/src/app/panels/graphite/graphiteUtil.js b/src/app/panels/graphite/graphiteUtil.js new file mode 100644 index 00000000000..7b58b383139 --- /dev/null +++ b/src/app/panels/graphite/graphiteUtil.js @@ -0,0 +1,151 @@ +define([ + 'jquery' +], +function ($) { + 'use strict'; + + String.prototype.graphiteGlob = function(glob) { + var regex = '^'; + for (var i = 0; i < glob.length; i++ ) { + var c = glob.charAt(i); + switch (c) { + case '*': + regex += '[^\.]+'; + break; + case '.': + regex += '\\.'; + break; + default: + regex += c; + } + } + regex += '$'; + return this.match(regex); + } + + function build_graphite_options(options, raw) { + raw = raw || false; + var clean_options = []; + //var internal_options = ['_t']; + var graphite_options = ['target', 'targets', 'from', 'until', 'rawData', 'format']; + var graphite_png_options = ['areaMode', 'width', 'height', 'template', 'margin', 'bgcolor', + 'fgcolor', 'fontName', 'fontSize', 'fontBold', 'fontItalic', + 'yMin', 'yMax', 'colorList', 'title', 'vtitle', 'lineMode', + 'lineWith', 'hideLegend', 'hideAxes', 'hideGrid', 'minXstep', + 'majorGridlineColor', 'minorGridLineColor', 'minorY', + 'thickness', 'min', 'max', 'tz']; + + if(raw) { + options['format'] = 'json'; + } else { + // use random parameter to force image refresh + options["_t"] = options["_t"] || Math.random(); + } + + $.each(options, function (key, value) { + if(raw) { + if ($.inArray(key, graphite_options) === -1) { + return; + } + } else { + if ($.inArray(key, graphite_options) === -1 && $.inArray(key, graphite_png_options) === -1) { + return; + } + } + if (key === "targets") { + $.each(value, function (index, value) { + if (raw) { + // it's normally pointless to use alias() in raw mode, because we apply an alias (name) ourself + // in the client rendering step. we just need graphite to return the target. + // but graphite sometimes alters the name of the target in the returned data + // (https://github.com/graphite-project/graphite-web/issues/248) + // so we need a good string identifier and set it using alias() (which graphite will honor) + // so that we recognize the returned output. simplest is just to include the target spec again + // though this duplicates a lot of info in the url. + clean_options.push("target=" + encodeURIComponent(value.target)); + } else { + clean_options.push("target=alias(color(" +encodeURIComponent(value.target + ",'" + value.color) +"'),'" + value.name +"')"); + } + }); + } else if (value !== null) { + clean_options.push(key + "=" + encodeURIComponent(value)); + } + }); + + return clean_options; + } + + function build_graphite_url(options) { + var limit = 2000; // http://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers + var url = options.graphite_url + "?"; + + options = build_graphite_options(options, false); + $.map(options, function(option) { + if (url.length + option.length < limit) { + url += '&' + option; + } + }); + return url.replace(/\?&/, "?"); + } + + function find_definition (target_graphite, options) { + var matching_i = undefined; + + for (var cfg_i = 0; cfg_i < options.targets.length && matching_i == undefined; cfg_i++) { + // string match (no globbing) + if(options.targets[cfg_i].target == target_graphite.target) { + matching_i = cfg_i; + } + // glob match? + else if(target_graphite.target.graphiteGlob(options.targets[cfg_i].target)) { + matching_i = cfg_i; + } + } + + if (matching_i == undefined) { + console.error ("internal error: could not figure out which target_option target_graphite '" + + target_graphite.target + "' comes from"); + return []; + } + + return options.targets[matching_i]; + } + + function add_targets(options, response_data) { + var all_targets = []; + for (var res_i = 0; res_i < response_data.length; res_i++) { + var target = find_definition(response_data[res_i], options); + target.label = target.name; // flot wants 'label' + target.data = []; + var nulls = 0; + var non_nulls = 0; + for (var i in response_data[res_i].datapoints) { + if(response_data[res_i].datapoints[i][0] == null) { + nulls++; + if('drawNullAsZero' in options && options['drawNullAsZero']) { + response_data[res_i].datapoints[i][0] = 0; + } else { + // don't tell flot about null values, it prevents adjacent non-null values from + // being rendered correctly + continue; + } + } else { + non_nulls++; + } + target.data.push([response_data[res_i].datapoints[i][1] * 1000, response_data[res_i].datapoints[i][0]]); + } + if (nulls/non_nulls > 0.3) { + console.log("warning: rendered target contains " + nulls + " null values, " + non_nulls + " non_nulls"); + } + all_targets.push(target); + } + + return all_targets; + } + + return { + build_graphite_options: build_graphite_options, + build_graphite_url: build_graphite_url, + add_targets: add_targets + }; +}); \ No newline at end of file diff --git a/src/app/panels/graphite/interval.js b/src/app/panels/graphite/interval.js new file mode 100644 index 00000000000..673371fcf4c --- /dev/null +++ b/src/app/panels/graphite/interval.js @@ -0,0 +1,57 @@ +define([ + 'kbn' +], +function (kbn) { + 'use strict'; + + /** + * manages the interval logic + * @param {[type]} interval_string An interval string in the format '1m', '1y', etc + */ + function Interval(interval_string) { + this.string = interval_string; + + var info = kbn.describe_interval(interval_string); + this.type = info.type; + this.ms = info.sec * 1000 * info.count; + + // does the length of the interval change based on the current time? + if (this.type === 'y' || this.type === 'M') { + // we will just modify this time object rather that create a new one constantly + this.get = this.get_complex; + this.date = new Date(0); + } else { + this.get = this.get_simple; + } + } + + Interval.prototype = { + toString: function () { + return this.string; + }, + after: function(current_ms) { + return this.get(current_ms, 1); + }, + before: function (current_ms) { + return this.get(current_ms, -1); + }, + get_complex: function (current, delta) { + this.date.setTime(current); + switch(this.type) { + case 'M': + this.date.setUTCMonth(this.date.getUTCMonth() + delta); + break; + case 'y': + this.date.setUTCFullYear(this.date.getUTCFullYear() + delta); + break; + } + return this.date.getTime(); + }, + get_simple: function (current, delta) { + return current + (delta * this.ms); + } + }; + + return Interval; + +}); \ No newline at end of file diff --git a/src/app/panels/graphite/module.html b/src/app/panels/graphite/module.html new file mode 100644 index 00000000000..c100c911598 --- /dev/null +++ b/src/app/panels/graphite/module.html @@ -0,0 +1,97 @@ +
+ +
+ + + View + |  + + + + Zoom Out + +
+
+ +
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+ +
+ +
+
+ + + +
+
+
+ + + + {{series.info.alias || series.info.query}} + {{series.info.alias}} + + +
\ No newline at end of file diff --git a/src/app/panels/graphite/module.js b/src/app/panels/graphite/module.js new file mode 100644 index 00000000000..6199e583875 --- /dev/null +++ b/src/app/panels/graphite/module.js @@ -0,0 +1,703 @@ +/** @scratch /panels/5 + * include::panels/histogram.asciidoc[] + */ + +/** @scratch /panels/histogram/0 + * == Histogram + * Status: *Stable* + * + * The histogram panel allow for the display of time charts. It includes several modes and tranformations + * to display event counts, mean, min, max and total of numeric fields, and derivatives of counter + * fields. + * + */ +define([ + 'angular', + 'app', + 'jquery', + 'underscore', + 'kbn', + 'moment', + './timeSeries', + './graphiteUtil', + 'jquery.flot', + 'jquery.flot.events', + 'jquery.flot.selection', + 'jquery.flot.time', + 'jquery.flot.byte', + 'jquery.flot.stack', + 'jquery.flot.stackpercent' +], +function (angular, app, $, _, kbn, moment, timeSeries, graphiteUtil) { + + 'use strict'; + + var module = angular.module('kibana.panels.histogram', []); + app.useModule(module); + + module.controller('histogram', function($scope, querySrv, dashboard, filterSrv) { + $scope.panelMeta = { + modals : [ + { + description: "Inspect", + icon: "icon-info-sign", + partial: "app/partials/inspector.html", + show: $scope.panel.spyable + } + ], + editorTabs : [ + { + title:'Style', + src:'app/panels/histogram/styleEditor.html' + }, + { + title:'Queries', + src:'app/panels/histogram/queriesEditor.html' + }, + ], + status : "Stable", + description : "A bucketed time series chart of the current query or queries. Uses the "+ + "Elasticsearch date_histogram facet. If using time stamped indices this panel will query"+ + " them sequentially to attempt to apply the lighest possible load to your Elasticsearch cluster" + }; + + // Set and populate defaults + var _d = { + /** @scratch /panels/histogram/3 + * === Parameters + * ==== Axis options + * mode:: Value to use for the y-axis. For all modes other than count, +value_field+ must be + * defined. Possible values: count, mean, max, min, total. + */ + mode : 'count', + /** @scratch /panels/histogram/3 + * time_field:: x-axis field. This must be defined as a date type in Elasticsearch. + */ + time_field : '@timestamp', + /** @scratch /panels/histogram/3 + * value_field:: y-axis field if +mode+ is set to mean, max, min or total. Must be numeric. + */ + value_field : null, + /** @scratch /panels/histogram/3 + * x-axis:: Show the x-axis + */ + 'x-axis' : true, + /** @scratch /panels/histogram/3 + * y-axis:: Show the y-axis + */ + 'y-axis' : true, + /** @scratch /panels/histogram/3 + * scale:: Scale the y-axis by this factor + */ + scale : 1, + /** @scratch /panels/histogram/3 + * y_format:: 'none','bytes','short ' + */ + y_format : 'none', + /** @scratch /panels/histogram/5 + * grid object:: Min and max y-axis values + * grid.min::: Minimum y-axis value + * grid.max::: Maximum y-axis value + */ + grid : { + max: null, + min: 0 + }, + /** @scratch /panels/histogram/5 + * ==== Queries + * queries object:: This object describes the queries to use on this panel. + * queries.mode::: Of the queries available, which to use. Options: +all, pinned, unpinned, selected+ + * queries.ids::: In +selected+ mode, which query ids are selected. + */ + queries : { + mode : 'all', + ids : [] + }, + /** @scratch /panels/histogram/3 + * ==== Annotations + * annotate object:: A query can be specified, the results of which will be displayed as markers on + * the chart. For example, for noting code deploys. + * annotate.enable::: Should annotations, aka markers, be shown? + * annotate.query::: Lucene query_string syntax query to use for markers. + * annotate.size::: Max number of markers to show + * annotate.field::: Field from documents to show + * annotate.sort::: Sort array in format [field,order], For example [`@timestamp',`desc'] + */ + annotate : { + enable : false, + query : "*", + size : 20, + field : '_type', + sort : ['_score','desc'] + }, + /** @scratch /panels/histogram/3 + * ==== Interval options + * auto_int:: Automatically scale intervals? + */ + auto_int : true, + /** @scratch /panels/histogram/3 + * resolution:: If auto_int is true, shoot for this many bars. + */ + resolution : 100, + /** @scratch /panels/histogram/3 + * interval:: If auto_int is set to false, use this as the interval. + */ + interval : '5m', + /** @scratch /panels/histogram/3 + * interval:: Array of possible intervals in the *View* selector. Example [`auto',`1s',`5m',`3h'] + */ + intervals : ['auto','1s','1m','5m','10m','30m','1h','3h','12h','1d','1w','1y'], + /** @scratch /panels/histogram/3 + * ==== Drawing options + * lines:: Show line chart + */ + lines : false, + /** @scratch /panels/histogram/3 + * fill:: Area fill factor for line charts, 1-10 + */ + fill : 0, + /** @scratch /panels/histogram/3 + * linewidth:: Weight of lines in pixels + */ + linewidth : 3, + /** @scratch /panels/histogram/3 + * points:: Show points on chart + */ + points : false, + /** @scratch /panels/histogram/3 + * pointradius:: Size of points in pixels + */ + pointradius : 5, + /** @scratch /panels/histogram/3 + * bars:: Show bars on chart + */ + bars : true, + /** @scratch /panels/histogram/3 + * stack:: Stack multiple series + */ + stack : true, + /** @scratch /panels/histogram/3 + * spyable:: Show inspect icon + */ + spyable : true, + /** @scratch /panels/histogram/3 + * zoomlinks:: Show `Zoom Out' link + */ + zoomlinks : false, + /** @scratch /panels/histogram/3 + * options:: Show quick view options section + */ + options : false, + /** @scratch /panels/histogram/3 + * legend:: Display the legond + */ + legend : true, + /** @scratch /panels/histogram/3 + * show_query:: If no alias is set, should the query be displayed? + */ + show_query : true, + /** @scratch /panels/histogram/3 + * interactive:: Enable click-and-drag to zoom functionality + */ + interactive : true, + /** @scratch /panels/histogram/3 + * legend_counts:: Show counts in legend + */ + legend_counts : true, + /** @scratch /panels/histogram/3 + * ==== Transformations + * timezone:: Correct for browser timezone?. Valid values: browser, utc + */ + timezone : 'browser', // browser or utc + /** @scratch /panels/histogram/3 + * percentage:: Show the y-axis as a percentage of the axis total. Only makes sense for multiple + * queries + */ + percentage : false, + /** @scratch /panels/histogram/3 + * zerofill:: Improves the accuracy of line charts at a small performance cost. + */ + zerofill : true, + /** @scratch /panels/histogram/3 + * derivative:: Show each point on the x-axis as the change from the previous point + */ + derivative : false, + /** @scratch /panels/histogram/3 + * tooltip object:: + * tooltip.value_type::: Individual or cumulative controls how tooltips are display on stacked charts + * tooltip.query_as_alias::: If no alias is set, should the query be displayed? + */ + tooltip : { + value_type: 'cumulative', + query_as_alias: true + } + }; + + _.defaults($scope.panel,_d); + _.defaults($scope.panel.tooltip,_d.tooltip); + _.defaults($scope.panel.annotate,_d.annotate); + _.defaults($scope.panel.grid,_d.grid); + + + + $scope.init = function() { + // Hide view options by default + $scope.options = false; + $scope.$on('refresh',function(){ + $scope.get_data(); + }); + + // Always show the query if an alias isn't set. Users can set an alias if the query is too + // long + $scope.panel.tooltip.query_as_alias = true; + + $scope.get_data(); + + }; + + $scope.set_interval = function(interval) { + if(interval !== 'auto') { + $scope.panel.auto_int = false; + $scope.panel.interval = interval; + } else { + $scope.panel.auto_int = true; + } + }; + + $scope.interval_label = function(interval) { + return $scope.panel.auto_int && interval === $scope.panel.interval ? interval+" (auto)" : interval; + }; + + /** + * The time range effecting the panel + * @return {[type]} [description] + */ + $scope.get_time_range = function () { + var range = $scope.range = filterSrv.timeRange('last'); + return range; + }; + + $scope.get_interval = function () { + var interval = $scope.panel.interval, + range; + if ($scope.panel.auto_int) { + range = $scope.get_time_range(); + if (range) { + interval = kbn.secondsToHms( + kbn.calculate_interval(range.from, range.to, $scope.panel.resolution, 0) / 1000 + ); + } + } + $scope.panel.interval = interval || '10m'; + return $scope.panel.interval; + }; + + var graphOptions = { + graphite_url: 'http://metrics.prod.tradera.com/render/', + from: '-1h', + until: 'now', + height: '300', + width: '740', + targets: [ + { + name: 'series 1', + color: '#CC6699', + target: "summarize(groupByNode(prod.apps.tradera_site.*.counters.global.request_status.code_{301,302,404}.count, 7, 'sum'), '1min')", + //target: "randomWalk('random1')", + } + ], + title: 'horizontal title', + vtitle: 'vertical title', + drawNullAsZero: false, + state: 'stacked', + hover_details: true, + }; + + $scope.getGraphiteData = function (options, parameters) { + return $.ajax({ + accepts: { text: 'application/json' }, + cache: false, + dataType: 'json', + url: options['graphite_url'], + type: "POST", + data: parameters.join('&'), + error: function(xhr, textStatus, errorThrown) { + $scope.panel.error = 'Failed to do graphite POST request: ' + textStatus + ' : ' + errorThrown; + } + }); + }; + + $scope.colors = [ + "#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0", //1 + "#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477", //2 + "#B7DBAB","#F4D598","#70DBED","#F9BA8F","#F29191","#82B5D8","#E5A8E2","#AEA2E0", //3 + "#629E51","#E5AC0E","#64B0C8","#E0752D","#BF1B00","#0A50A1","#962D82","#614D93", //4 + "#9AC48A","#F2C96D","#65C5DB","#F9934E","#EA6460","#5195CE","#D683CE","#806EB7", //5 + "#3F6833","#967302","#2F575E","#99440A","#58140C","#052B51","#511749","#3F2B5B", //6 + "#E0F9D7","#FCEACA","#CFFAFF","#F9E2D2","#FCE2DE","#BADFF4","#F9D9F9","#DEDAF7" //7 + ]; + + /** + * Fetch the data for a chunk of a queries results. Multiple segments occur when several indicies + * need to be consulted (like timestamped logstash indicies) + * + * The results of this function are stored on the scope's data property. This property will be an + * array of objects with the properties info, time_series, and hits. These objects are used in the + * render_panel function to create the historgram. + * + */ + $scope.get_data = function() { + delete $scope.panel.error; + + var range = $scope.get_time_range(); + var interval = $scope.get_interval(range); + console.log('range: ', range); + console.log('interval: ', interval); + + $scope.panelMeta.loading = true; + + var graphiteParameters = graphiteUtil.build_graphite_options(graphOptions, true); + var request = $scope.getGraphiteData(graphOptions, graphiteParameters); + $scope.populate_modal(graphiteParameters); + + request.done(function(results) { + $scope.data = []; + $scope.panelMeta.loading = false; + + if(results.length == 0 ) { + $scope.panel.error = 'no data in response from graphite'; + } + + console.log('Data from graphite:', results); + + var tsOpts = { + interval: "30s", + start_date: range && range.from, + end_date: range && range.to, + fill_style: 'null' + }; + + var hits = 0; + + _.each(results, function(targetData) { + var time_series = new timeSeries.ZeroFilled(tsOpts); + + _.each(targetData.datapoints, function(valueArray) { + time_series.addValue(valueArray[1] * 1000, valueArray[0]); + hits += +1; + }); + + $scope.data.push({ + info: { + alias: targetData.target, + color: $scope.colors[$scope.data.length], + enable: true, + id: 0, + parent: 0, + pin: false, + query: "*", + type: "lucene" + }, + time_series: time_series, + hits: hits + }); + }); + + $scope.hits = hits; + + // Tell the histogram directive to render. + $scope.$emit('render'); + }); + }; + + // function $scope.zoom + // factor :: Zoom factor, so 0.5 = cuts timespan in half, 2 doubles timespan + $scope.zoom = function(factor) { + var _range = filterSrv.timeRange('last'); + var _timespan = (_range.to.valueOf() - _range.from.valueOf()); + var _center = _range.to.valueOf() - _timespan/2; + + var _to = (_center + (_timespan*factor)/2); + var _from = (_center - (_timespan*factor)/2); + + // If we're not already looking into the future, don't. + if(_to > Date.now() && _range.to < Date.now()) { + var _offset = _to - Date.now(); + _from = _from - _offset; + _to = Date.now(); + } + + if(factor > 1) { + filterSrv.removeByType('time'); + } + filterSrv.set({ + type:'time', + from:moment.utc(_from).toDate(), + to:moment.utc(_to).toDate(), + field:$scope.panel.time_field + }); + }; + + // I really don't like this function, too much dom manip. Break out into directive? + $scope.populate_modal = function(request) { + $scope.inspector = angular.toJson(request,true); + }; + + $scope.set_refresh = function (state) { + $scope.refresh = state; + }; + + $scope.close_edit = function() { + if($scope.refresh) { + $scope.get_data(); + } + $scope.refresh = false; + $scope.$emit('render'); + }; + + $scope.render = function() { + $scope.$emit('render'); + }; + + }); + + module.directive('histogramChart', function(dashboard, filterSrv) { + return { + restrict: 'A', + template: '
', + link: function(scope, elem) { + + // Receive render events + scope.$on('render',function(){ + render_panel(); + }); + + // Re-render if the window is resized + angular.element(window).bind('resize', function(){ + render_panel(); + }); + + var scale = function(series,factor) { + return _.map(series,function(p) { + return [p[0],p[1]*factor]; + }); + }; + + var scaleSeconds = function(series,interval) { + return _.map(series,function(p) { + return [p[0],p[1]/kbn.interval_to_seconds(interval)]; + }); + }; + + var derivative = function(series) { + return _.map(series, function(p,i) { + var _v; + if(i === 0 || p[1] === null) { + _v = [p[0],null]; + } else { + _v = series[i-1][1] === null ? [p[0],null] : [p[0],p[1]-(series[i-1][1])]; + } + return _v; + }); + }; + + // Function for rendering panel + function render_panel() { + // IE doesn't work without this + elem.css({height:scope.panel.height || scope.row.height}); + + // Populate from the query service + try { + _.each(scope.data, function(series) { + series.label = series.info.alias; + series.color = series.info.color; + }); + } catch(e) {return;} + + // Set barwidth based on specified interval + var barwidth = kbn.interval_to_ms(scope.panel.interval); + + var stack = scope.panel.stack ? true : null; + + // Populate element + try { + var options = { + legend: { show: false }, + series: { + stackpercent: scope.panel.stack ? scope.panel.percentage : false, + stack: scope.panel.percentage ? null : stack, + lines: { + show: scope.panel.lines, + // Silly, but fixes bug in stacked percentages + fill: scope.panel.fill === 0 ? 0.001 : scope.panel.fill/10, + lineWidth: scope.panel.linewidth, + steps: false + }, + bars: { + show: scope.panel.bars, + fill: 1, + barWidth: barwidth/1.5, + zero: false, + lineWidth: 0 + }, + points: { + show: scope.panel.points, + fill: 1, + fillColor: false, + radius: scope.panel.pointradius + }, + shadowSize: 1 + }, + yaxis: { + show: scope.panel['y-axis'], + min: scope.panel.grid.min, + max: scope.panel.percentage && scope.panel.stack ? 100 : scope.panel.grid.max + }, + xaxis: { + timezone: scope.panel.timezone, + show: scope.panel['x-axis'], + mode: "time", + min: _.isUndefined(scope.range.from) ? null : scope.range.from.getTime(), + max: _.isUndefined(scope.range.to) ? null : scope.range.to.getTime(), + timeformat: time_format(scope.panel.interval), + label: "Datetime", + ticks: elem.width()/100 + }, + grid: { + backgroundColor: null, + borderWidth: 0, + hoverable: true, + color: '#c8c8c8' + } + }; + + if(scope.panel.y_format === 'bytes') { + options.yaxis.mode = "byte"; + } + + if(scope.panel.y_format === 'short') { + options.yaxis.tickFormatter = function(val) { + return kbn.shortFormat(val,0); + }; + } + + if(scope.panel.annotate.enable) { + options.events = { + levels: 1, + data: scope.annotations, + types: { + 'annotation': { + level: 1, + icon: { + icon: "icon-tag icon-flip-vertical", + size: 20, + color: "#222", + outline: "#bbb" + } + } + } + //xaxis: int // the x axis to attach events to + }; + } + + if(scope.panel.interactive) { + options.selection = { mode: "x", color: '#666' }; + } + + // when rendering stacked bars, we need to ensure each point that has data is zero-filled + // so that the stacking happens in the proper order + var required_times = []; + if (scope.data.length > 1) { + required_times = Array.prototype.concat.apply([], _.map(scope.data, function (query) { + return query.time_series.getOrderedTimes(); + })); + required_times = _.uniq(required_times.sort(function (a, b) { + // decending numeric sort + return a-b; + }), true); + } + + + for (var i = 0; i < scope.data.length; i++) { + var _d = scope.data[i].time_series.getFlotPairs(required_times); + if(scope.panel.derivative) { + _d = derivative(_d); + } + if(scope.panel.scale !== 1) { + _d = scale(_d,scope.panel.scale); + } + if(scope.panel.scaleSeconds) { + _d = scaleSeconds(_d,scope.panel.interval); + } + scope.data[i].data = _d; + } + + scope.plot = $.plot(elem, scope.data, options); + + } catch(e) { + // Nothing to do here + } + } + + function time_format(interval) { + var _int = kbn.interval_to_seconds(interval); + if(_int >= 2628000) { + return "%Y-%m"; + } + if(_int >= 86400) { + return "%Y-%m-%d"; + } + if(_int >= 60) { + return "%H:%M
%m-%d"; + } + + return "%H:%M:%S"; + } + + var $tooltip = $('
'); + elem.bind("plothover", function (event, pos, item) { + var group, value, timestamp; + if (item) { + if (item.series.info.alias || scope.panel.tooltip.query_as_alias) { + group = '' + + '' + ' ' + + (item.series.info.alias || item.series.info.query)+ + '
'; + } else { + group = kbn.query_color_dot(item.series.color, 15) + ' '; + } + value = (scope.panel.stack && scope.panel.tooltip.value_type === 'individual') ? + item.datapoint[1] - item.datapoint[2] : + item.datapoint[1]; + if(scope.panel.y_format === 'bytes') { + value = kbn.byteFormat(value,2); + } + if(scope.panel.y_format === 'short') { + value = kbn.shortFormat(value,2); + } + timestamp = scope.panel.timezone === 'browser' ? + moment(item.datapoint[0]).format('YYYY-MM-DD HH:mm:ss') : + moment.utc(item.datapoint[0]).format('YYYY-MM-DD HH:mm:ss'); + $tooltip + .html( + group + value + " @ " + timestamp + ) + .place_tt(pos.pageX, pos.pageY); + } else { + $tooltip.detach(); + } + }); + + elem.bind("plotselected", function (event, ranges) { + filterSrv.set({ + type : 'time', + from : moment.utc(ranges.xaxis.from).toDate(), + to : moment.utc(ranges.xaxis.to).toDate(), + field : scope.panel.time_field + }); + }); + } + }; + }); + +}); diff --git a/src/app/panels/graphite/queriesEditor.html b/src/app/panels/graphite/queriesEditor.html new file mode 100644 index 00000000000..414de272ef8 --- /dev/null +++ b/src/app/panels/graphite/queriesEditor.html @@ -0,0 +1,43 @@ +

Charted

+
+ +
+

Markers

+ +
+ Here you can specify a query to be plotted on your chart as a marker. Hovering over a marker will display the field you specify below. If more documents are found than the limit you set, they will be scored by Elasticsearch and events that best match your query will be displayed. +
+ +

+

+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
diff --git a/src/app/panels/graphite/styleEditor.html b/src/app/panels/graphite/styleEditor.html new file mode 100644 index 00000000000..874a22640b5 --- /dev/null +++ b/src/app/panels/graphite/styleEditor.html @@ -0,0 +1,88 @@ +
+
+
Chart Options
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
Multiple Series
+
+ +
+
+ + +
+
+ + +
+
+
+ +
+
+
Header
+
+ +
+
+ +
+
+
+
Legend
+
+ +
+
+ +
+
+ +
+
+ +
+
Grid
+
+ + +
+
+ + +
+
+ +
diff --git a/src/app/panels/graphite/timeSeries.js b/src/app/panels/graphite/timeSeries.js new file mode 100644 index 00000000000..8885e734371 --- /dev/null +++ b/src/app/panels/graphite/timeSeries.js @@ -0,0 +1,216 @@ +define([ + 'underscore', + './interval' +], +function (_, Interval) { + 'use strict'; + + var ts = {}; + + // map compatable parseInt + function base10Int(val) { + return parseInt(val, 10); + } + + // trim the ms off of a time, but return it with empty ms. + function getDatesTime(date) { + return Math.floor(date.getTime() / 1000)*1000; + } + + /** + * Certain graphs require 0 entries to be specified for them to render + * properly (like the line graph). So with this we will caluclate all of + * the expected time measurements, and fill the missing ones in with 0 + * @param {object} opts An object specifying some/all of the options + * + * OPTIONS: + * @opt {string} interval The interval notion describing the expected spacing between + * each data point. + * @opt {date} start_date (optional) The start point for the time series, setting this and the + * end_date will ensure that the series streches to resemble the entire + * expected result + * @opt {date} end_date (optional) The end point for the time series, see start_date + * @opt {string} fill_style Either "minimal", or "all" describing the strategy used to zero-fill + * the series. + */ + ts.ZeroFilled = function (opts) { + opts = _.defaults(opts, { + interval: '10m', + start_date: null, + end_date: null, + fill_style: 'minimal' + }); + + // the expected differenece between readings. + this.interval = new Interval(opts.interval); + + // will keep all values here, keyed by their time + this._data = {}; + this.start_time = opts.start_date && getDatesTime(opts.start_date); + this.end_time = opts.end_date && getDatesTime(opts.end_date); + this.opts = opts; + }; + + /** + * Add a row + * @param {int} time The time for the value, in + * @param {any} value The value at this time + */ + ts.ZeroFilled.prototype.addValue = function (time, value) { + if (time instanceof Date) { + time = getDatesTime(time); + } else { + time = base10Int(time); + } + if (!isNaN(time)) { + this._data[time] = (_.isUndefined(value) ? 0 : value); + } + this._cached_times = null; + }; + + /** + * Get an array of the times that have been explicitly set in the series + * @param {array} include (optional) list of timestamps to include in the response + * @return {array} An array of integer times. + */ + ts.ZeroFilled.prototype.getOrderedTimes = function (include) { + var times = _.map(_.keys(this._data), base10Int); + if (_.isArray(include)) { + times = times.concat(include); + } + return _.uniq(times.sort(function (a, b) { + // decending numeric sort + return a - b; + }), true); + }; + + /** + * return the rows in the format: + * [ [time, value], [time, value], ... ] + * + * Heavy lifting is done by _get(Min|Default|All)FlotPairs() + * @param {array} required_times An array of timestamps that must be in the resulting pairs + * @return {array} + */ + ts.ZeroFilled.prototype.getFlotPairs = function (required_times) { + var times = this.getOrderedTimes(required_times), + strategy, + pairs; + + if(this.opts.fill_style === 'all') { + strategy = this._getAllFlotPairs; + } else if(this.opts.fill_style === 'null') { + strategy = this._getNullFlotPairs; + } else { + strategy = this._getMinFlotPairs; + } + + pairs = _.reduce( + times, // what + strategy, // how + [], // where + this // context + ); + + // if the first or last pair is inside either the start or end time, + // add those times to the series with null values so the graph will stretch to contain them. + // Removing, flot 0.8.1's max/min params satisfy this + /* + if (this.start_time && (pairs.length === 0 || pairs[0][0] > this.start_time)) { + pairs.unshift([this.start_time, null]); + } + if (this.end_time && (pairs.length === 0 || pairs[pairs.length - 1][0] < this.end_time)) { + pairs.push([this.end_time, null]); + } + */ + + return pairs; + }; + + /** + * ** called as a reduce stragegy in getFlotPairs() ** + * Fill zero's on either side of the current time, unless there is already a measurement there or + * we are looking at an edge. + * @return {array} An array of points to plot with flot + */ + ts.ZeroFilled.prototype._getMinFlotPairs = function (result, time, i, times) { + var next, expected_next, prev, expected_prev; + + // check for previous measurement + if (i > 0) { + prev = times[i - 1]; + expected_prev = this.interval.before(time); + if (prev < expected_prev) { + result.push([expected_prev, 0]); + } + } + + // add the current time + result.push([ time, this._data[time] || 0]); + + // check for next measurement + if (times.length > i) { + next = times[i + 1]; + expected_next = this.interval.after(time); + if (next > expected_next) { + result.push([expected_next, 0]); + } + } + + return result; + }; + + /** + * ** called as a reduce stragegy in getFlotPairs() ** + * Fill zero's to the right of each time, until the next measurement is reached or we are at the + * last measurement + * @return {array} An array of points to plot with flot + */ + ts.ZeroFilled.prototype._getAllFlotPairs = function (result, time, i, times) { + var next, expected_next; + + result.push([ times[i], this._data[times[i]] || 0 ]); + next = times[i + 1]; + expected_next = this.interval.after(time); + for(; times.length > i && next > expected_next; expected_next = this.interval.after(expected_next)) { + result.push([expected_next, 0]); + } + + return result; + }; + + /** + * ** called as a reduce stragegy in getFlotPairs() ** + * Same as min, but fills with nulls + * @return {array} An array of points to plot with flot + */ + ts.ZeroFilled.prototype._getNullFlotPairs = function (result, time, i, times) { + var next, expected_next, prev, expected_prev; + + // check for previous measurement + if (i > 0) { + prev = times[i - 1]; + expected_prev = this.interval.before(time); + if (prev < expected_prev) { + result.push([expected_prev, null]); + } + } + + // add the current time + result.push([ time, this._data[time] || null]); + + // check for next measurement + if (times.length > i) { + next = times[i + 1]; + expected_next = this.interval.after(time); + if (next > expected_next) { + result.push([expected_next, null]); + } + } + + return result; + }; + + + return ts; +}); \ No newline at end of file diff --git a/src/app/panels/histogram/editor.html b/src/app/panels/histogram/editor.html new file mode 100644 index 00000000000..15490701bb4 --- /dev/null +++ b/src/app/panels/histogram/editor.html @@ -0,0 +1,48 @@ +
+
+
Values
+
+ + +
+
+ + +
+
+
+
Transform Series
+
+ + +
+
+ +
+
+ +
+
+
+
Time Options
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
\ No newline at end of file diff --git a/src/app/panels/histogram/interval.js b/src/app/panels/histogram/interval.js new file mode 100644 index 00000000000..673371fcf4c --- /dev/null +++ b/src/app/panels/histogram/interval.js @@ -0,0 +1,57 @@ +define([ + 'kbn' +], +function (kbn) { + 'use strict'; + + /** + * manages the interval logic + * @param {[type]} interval_string An interval string in the format '1m', '1y', etc + */ + function Interval(interval_string) { + this.string = interval_string; + + var info = kbn.describe_interval(interval_string); + this.type = info.type; + this.ms = info.sec * 1000 * info.count; + + // does the length of the interval change based on the current time? + if (this.type === 'y' || this.type === 'M') { + // we will just modify this time object rather that create a new one constantly + this.get = this.get_complex; + this.date = new Date(0); + } else { + this.get = this.get_simple; + } + } + + Interval.prototype = { + toString: function () { + return this.string; + }, + after: function(current_ms) { + return this.get(current_ms, 1); + }, + before: function (current_ms) { + return this.get(current_ms, -1); + }, + get_complex: function (current, delta) { + this.date.setTime(current); + switch(this.type) { + case 'M': + this.date.setUTCMonth(this.date.getUTCMonth() + delta); + break; + case 'y': + this.date.setUTCFullYear(this.date.getUTCFullYear() + delta); + break; + } + return this.date.getTime(); + }, + get_simple: function (current, delta) { + return current + (delta * this.ms); + } + }; + + return Interval; + +}); \ No newline at end of file diff --git a/src/app/panels/histogram/module.html b/src/app/panels/histogram/module.html new file mode 100644 index 00000000000..20ca4732d93 --- /dev/null +++ b/src/app/panels/histogram/module.html @@ -0,0 +1,99 @@ +
+ +
+ + + View + |  + + + + Zoom Out |  + + + + + {{series.info.alias || series.info.query}} + {{series.info.alias}} + ({{series.hits}}) + + + change in {{panel.value_field}} {{panel.mode}} per {{panel.interval}}1s | ({{hits}} hits) +
+
+ +
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+ +
+ +
+
+ + + +
+
+
+
\ No newline at end of file diff --git a/src/app/panels/histogram/module.js b/src/app/panels/histogram/module.js new file mode 100644 index 00000000000..15c8e46c044 --- /dev/null +++ b/src/app/panels/histogram/module.js @@ -0,0 +1,775 @@ +/** @scratch /panels/5 + * include::panels/histogram.asciidoc[] + */ + +/** @scratch /panels/histogram/0 + * == Histogram + * Status: *Stable* + * + * The histogram panel allow for the display of time charts. It includes several modes and tranformations + * to display event counts, mean, min, max and total of numeric fields, and derivatives of counter + * fields. + * + */ +define([ + 'angular', + 'app', + 'jquery', + 'underscore', + 'kbn', + 'moment', + './timeSeries', + 'jquery.flot', + 'jquery.flot.events', + 'jquery.flot.selection', + 'jquery.flot.time', + 'jquery.flot.byte', + 'jquery.flot.stack', + 'jquery.flot.stackpercent' +], +function (angular, app, $, _, kbn, moment, timeSeries) { + + 'use strict'; + + var module = angular.module('kibana.panels.histogram', []); + app.useModule(module); + + module.controller('histogram', function($scope, querySrv, dashboard, filterSrv) { + $scope.panelMeta = { + modals : [ + { + description: "Inspect", + icon: "icon-info-sign", + partial: "app/partials/inspector.html", + show: $scope.panel.spyable + } + ], + editorTabs : [ + { + title:'Style', + src:'app/panels/histogram/styleEditor.html' + }, + { + title:'Queries', + src:'app/panels/histogram/queriesEditor.html' + }, + ], + status : "Stable", + description : "A bucketed time series chart of the current query or queries. Uses the "+ + "Elasticsearch date_histogram facet. If using time stamped indices this panel will query"+ + " them sequentially to attempt to apply the lighest possible load to your Elasticsearch cluster" + }; + + // Set and populate defaults + var _d = { + /** @scratch /panels/histogram/3 + * === Parameters + * ==== Axis options + * mode:: Value to use for the y-axis. For all modes other than count, +value_field+ must be + * defined. Possible values: count, mean, max, min, total. + */ + mode : 'count', + /** @scratch /panels/histogram/3 + * time_field:: x-axis field. This must be defined as a date type in Elasticsearch. + */ + time_field : '@timestamp', + /** @scratch /panels/histogram/3 + * value_field:: y-axis field if +mode+ is set to mean, max, min or total. Must be numeric. + */ + value_field : null, + /** @scratch /panels/histogram/3 + * x-axis:: Show the x-axis + */ + 'x-axis' : true, + /** @scratch /panels/histogram/3 + * y-axis:: Show the y-axis + */ + 'y-axis' : true, + /** @scratch /panels/histogram/3 + * scale:: Scale the y-axis by this factor + */ + scale : 1, + /** @scratch /panels/histogram/3 + * y_format:: 'none','bytes','short ' + */ + y_format : 'none', + /** @scratch /panels/histogram/5 + * grid object:: Min and max y-axis values + * grid.min::: Minimum y-axis value + * grid.max::: Maximum y-axis value + */ + grid : { + max: null, + min: 0 + }, + /** @scratch /panels/histogram/5 + * ==== Queries + * queries object:: This object describes the queries to use on this panel. + * queries.mode::: Of the queries available, which to use. Options: +all, pinned, unpinned, selected+ + * queries.ids::: In +selected+ mode, which query ids are selected. + */ + queries : { + mode : 'all', + ids : [] + }, + /** @scratch /panels/histogram/3 + * ==== Annotations + * annotate object:: A query can be specified, the results of which will be displayed as markers on + * the chart. For example, for noting code deploys. + * annotate.enable::: Should annotations, aka markers, be shown? + * annotate.query::: Lucene query_string syntax query to use for markers. + * annotate.size::: Max number of markers to show + * annotate.field::: Field from documents to show + * annotate.sort::: Sort array in format [field,order], For example [`@timestamp',`desc'] + */ + annotate : { + enable : false, + query : "*", + size : 20, + field : '_type', + sort : ['_score','desc'] + }, + /** @scratch /panels/histogram/3 + * ==== Interval options + * auto_int:: Automatically scale intervals? + */ + auto_int : true, + /** @scratch /panels/histogram/3 + * resolution:: If auto_int is true, shoot for this many bars. + */ + resolution : 100, + /** @scratch /panels/histogram/3 + * interval:: If auto_int is set to false, use this as the interval. + */ + interval : '5m', + /** @scratch /panels/histogram/3 + * interval:: Array of possible intervals in the *View* selector. Example [`auto',`1s',`5m',`3h'] + */ + intervals : ['auto','1s','1m','5m','10m','30m','1h','3h','12h','1d','1w','1y'], + /** @scratch /panels/histogram/3 + * ==== Drawing options + * lines:: Show line chart + */ + lines : false, + /** @scratch /panels/histogram/3 + * fill:: Area fill factor for line charts, 1-10 + */ + fill : 0, + /** @scratch /panels/histogram/3 + * linewidth:: Weight of lines in pixels + */ + linewidth : 3, + /** @scratch /panels/histogram/3 + * points:: Show points on chart + */ + points : false, + /** @scratch /panels/histogram/3 + * pointradius:: Size of points in pixels + */ + pointradius : 5, + /** @scratch /panels/histogram/3 + * bars:: Show bars on chart + */ + bars : true, + /** @scratch /panels/histogram/3 + * stack:: Stack multiple series + */ + stack : true, + /** @scratch /panels/histogram/3 + * spyable:: Show inspect icon + */ + spyable : true, + /** @scratch /panels/histogram/3 + * zoomlinks:: Show `Zoom Out' link + */ + zoomlinks : true, + /** @scratch /panels/histogram/3 + * options:: Show quick view options section + */ + options : true, + /** @scratch /panels/histogram/3 + * legend:: Display the legond + */ + legend : true, + /** @scratch /panels/histogram/3 + * show_query:: If no alias is set, should the query be displayed? + */ + show_query : true, + /** @scratch /panels/histogram/3 + * interactive:: Enable click-and-drag to zoom functionality + */ + interactive : true, + /** @scratch /panels/histogram/3 + * legend_counts:: Show counts in legend + */ + legend_counts : true, + /** @scratch /panels/histogram/3 + * ==== Transformations + * timezone:: Correct for browser timezone?. Valid values: browser, utc + */ + timezone : 'browser', // browser or utc + /** @scratch /panels/histogram/3 + * percentage:: Show the y-axis as a percentage of the axis total. Only makes sense for multiple + * queries + */ + percentage : false, + /** @scratch /panels/histogram/3 + * zerofill:: Improves the accuracy of line charts at a small performance cost. + */ + zerofill : true, + /** @scratch /panels/histogram/3 + * derivative:: Show each point on the x-axis as the change from the previous point + */ + derivative : false, + /** @scratch /panels/histogram/3 + * tooltip object:: + * tooltip.value_type::: Individual or cumulative controls how tooltips are display on stacked charts + * tooltip.query_as_alias::: If no alias is set, should the query be displayed? + */ + tooltip : { + value_type: 'cumulative', + query_as_alias: true + } + }; + + _.defaults($scope.panel,_d); + _.defaults($scope.panel.tooltip,_d.tooltip); + _.defaults($scope.panel.annotate,_d.annotate); + _.defaults($scope.panel.grid,_d.grid); + + + + $scope.init = function() { + // Hide view options by default + $scope.options = false; + $scope.$on('refresh',function(){ + $scope.get_data(); + }); + + // Always show the query if an alias isn't set. Users can set an alias if the query is too + // long + $scope.panel.tooltip.query_as_alias = true; + + $scope.get_data(); + + }; + + $scope.set_interval = function(interval) { + if(interval !== 'auto') { + $scope.panel.auto_int = false; + $scope.panel.interval = interval; + } else { + $scope.panel.auto_int = true; + } + }; + + $scope.interval_label = function(interval) { + return $scope.panel.auto_int && interval === $scope.panel.interval ? interval+" (auto)" : interval; + }; + + /** + * The time range effecting the panel + * @return {[type]} [description] + */ + $scope.get_time_range = function () { + var range = $scope.range = filterSrv.timeRange('last'); + return range; + }; + + $scope.get_interval = function () { + var interval = $scope.panel.interval, + range; + if ($scope.panel.auto_int) { + range = $scope.get_time_range(); + if (range) { + interval = kbn.secondsToHms( + kbn.calculate_interval(range.from, range.to, $scope.panel.resolution, 0) / 1000 + ); + } + } + $scope.panel.interval = interval || '10m'; + return $scope.panel.interval; + }; + + $scope.colors = [ + "#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0", //1 + "#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477", //2 + "#B7DBAB","#F4D598","#70DBED","#F9BA8F","#F29191","#82B5D8","#E5A8E2","#AEA2E0", //3 + "#629E51","#E5AC0E","#64B0C8","#E0752D","#BF1B00","#0A50A1","#962D82","#614D93", //4 + "#9AC48A","#F2C96D","#65C5DB","#F9934E","#EA6460","#5195CE","#D683CE","#806EB7", //5 + "#3F6833","#967302","#2F575E","#99440A","#58140C","#052B51","#511749","#3F2B5B", //6 + "#E0F9D7","#FCEACA","#CFFAFF","#F9E2D2","#FCE2DE","#BADFF4","#F9D9F9","#DEDAF7" //7 + ]; + + /** + * Fetch the data for a chunk of a queries results. Multiple segments occur when several indicies + * need to be consulted (like timestamped logstash indicies) + * + * The results of this function are stored on the scope's data property. This property will be an + * array of objects with the properties info, time_series, and hits. These objects are used in the + * render_panel function to create the historgram. + * + * @param {number} segment The segment count, (0 based) + * @param {number} query_id The id of the query, generated on the first run and passed back when + * this call is made recursively for more segments + */ + $scope.get_data = function(segment, query_id) { + var + _range, + _interval, + request, + queries, + results; + + if (_.isUndefined(segment)) { + segment = 0; + } + delete $scope.panel.error; + + // Make sure we have everything for the request to complete + if(dashboard.indices.length === 0) { + return; + } + _range = $scope.get_time_range(); + _interval = $scope.get_interval(_range); + + if ($scope.panel.auto_int) { + $scope.panel.interval = kbn.secondsToHms( + kbn.calculate_interval(_range.from,_range.to,$scope.panel.resolution,0)/1000); + } + + $scope.panelMeta.loading = true; + request = $scope.ejs.Request().indices(dashboard.indices[segment]); + + $scope.panel.queries.ids = querySrv.idsByMode($scope.panel.queries); + + queries = querySrv.getQueryObjs($scope.panel.queries.ids); + + // Build the query + _.each(queries, function(q) { + var query = $scope.ejs.FilteredQuery( + querySrv.toEjsObj(q), + filterSrv.getBoolFilter(filterSrv.ids) + ); + + var facet = $scope.ejs.DateHistogramFacet(q.id); + + if($scope.panel.mode === 'count') { + facet = facet.field($scope.panel.time_field).global(true); + } else { + if(_.isNull($scope.panel.value_field)) { + $scope.panel.error = "In " + $scope.panel.mode + " mode a field must be specified"; + return; + } + facet = facet.keyField($scope.panel.time_field).valueField($scope.panel.value_field).global(true); + } + facet = facet.interval(_interval).facetFilter($scope.ejs.QueryFilter(query)); + request = request.facet(facet) + .size($scope.panel.annotate.enable ? $scope.panel.annotate.size : 0); + }); + + if($scope.panel.annotate.enable) { + var query = $scope.ejs.FilteredQuery( + $scope.ejs.QueryStringQuery($scope.panel.annotate.query || '*'), + filterSrv.getBoolFilter(filterSrv.idsByType('time')) + ); + request = request.query(query); + + // This is a hack proposed by @boaz to work around the fact that we can't get + // to field data values directly, and we need timestamps as normalized longs + request = request.sort([ + $scope.ejs.Sort($scope.panel.annotate.sort[0]).order($scope.panel.annotate.sort[1]), + $scope.ejs.Sort($scope.panel.time_field).desc() + ]); + } + + // Populate the inspector panel + $scope.populate_modal(request); + + // Then run it + results = request.doSearch(); + + // Populate scope when we have results + results.then(function(results) { + + $scope.panelMeta.loading = false; + if(segment === 0) { + $scope.hits = 0; + $scope.data = []; + $scope.annotations = []; + query_id = $scope.query_id = new Date().getTime(); + } + + // Check for error and abort if found + if(!(_.isUndefined(results.error))) { + $scope.panel.error = $scope.parse_error(results.error); + return; + } + + // Make sure we're still on the same query/queries + if($scope.query_id === query_id) { + + var i = 0, + time_series, + hits; + + _.each(queries, function(q) { + var query_results = results.facets[q.id]; + // we need to initialize the data variable on the first run, + // and when we are working on the first segment of the data. + if(_.isUndefined($scope.data[i]) || segment === 0) { + var tsOpts = { + interval: _interval, + start_date: _range && _range.from, + end_date: _range && _range.to, + fill_style: $scope.panel.derivative ? 'null' : 'minimal' + }; + time_series = new timeSeries.ZeroFilled(tsOpts); + hits = 0; + } else { + time_series = $scope.data[i].time_series; + hits = $scope.data[i].hits; + } + + // push each entry into the time series, while incrementing counters + _.each(query_results.entries, function(entry) { + time_series.addValue(entry.time, entry[$scope.panel.mode]); + hits += entry.count; // The series level hits counter + $scope.hits += entry.count; // Entire dataset level hits counter + }); + $scope.data[i] = { + info: q, + time_series: time_series, + hits: hits + }; + console.log("elastic time_series:", time_series); + + i++; + }); + + if($scope.panel.annotate.enable) { + $scope.annotations = $scope.annotations.concat(_.map(results.hits.hits, function(hit) { + var _p = _.omit(hit,'_source','sort','_score'); + var _h = _.extend(kbn.flatten_json(hit._source),_p); + return { + min: hit.sort[1], + max: hit.sort[1], + eventType: "annotation", + title: null, + description: " "+ + _h[$scope.panel.annotate.field]+"
"+ + moment(hit.sort[1]).format('YYYY-MM-DD HH:mm:ss'), + score: hit.sort[0] + }; + })); + // Sort the data + $scope.annotations = _.sortBy($scope.annotations, function(v){ + // Sort in reverse + return v.score*($scope.panel.annotate.sort[1] === 'desc' ? -1 : 1); + }); + // And slice to the right size + $scope.annotations = $scope.annotations.slice(0,$scope.panel.annotate.size); + } + + // Tell the histogram directive to render. + $scope.$emit('render'); + + // If we still have segments left, get them + if(segment < dashboard.indices.length-1) { + $scope.get_data(segment+1,query_id); + } + } + }); + }; + + // function $scope.zoom + // factor :: Zoom factor, so 0.5 = cuts timespan in half, 2 doubles timespan + $scope.zoom = function(factor) { + var _range = filterSrv.timeRange('last'); + var _timespan = (_range.to.valueOf() - _range.from.valueOf()); + var _center = _range.to.valueOf() - _timespan/2; + + var _to = (_center + (_timespan*factor)/2); + var _from = (_center - (_timespan*factor)/2); + + // If we're not already looking into the future, don't. + if(_to > Date.now() && _range.to < Date.now()) { + var _offset = _to - Date.now(); + _from = _from - _offset; + _to = Date.now(); + } + + if(factor > 1) { + filterSrv.removeByType('time'); + } + filterSrv.set({ + type:'time', + from:moment.utc(_from).toDate(), + to:moment.utc(_to).toDate(), + field:$scope.panel.time_field + }); + }; + + // I really don't like this function, too much dom manip. Break out into directive? + $scope.populate_modal = function(request) { + $scope.inspector = angular.toJson(JSON.parse(request.toString()),true); + }; + + $scope.set_refresh = function (state) { + $scope.refresh = state; + }; + + $scope.close_edit = function() { + if($scope.refresh) { + $scope.get_data(); + } + $scope.refresh = false; + $scope.$emit('render'); + }; + + $scope.render = function() { + $scope.$emit('render'); + }; + + }); + + module.directive('histogramChart', function(dashboard, filterSrv) { + return { + restrict: 'A', + template: '
', + link: function(scope, elem) { + + // Receive render events + scope.$on('render',function(){ + render_panel(); + }); + + // Re-render if the window is resized + angular.element(window).bind('resize', function(){ + render_panel(); + }); + + var scale = function(series,factor) { + return _.map(series,function(p) { + return [p[0],p[1]*factor]; + }); + }; + + var scaleSeconds = function(series,interval) { + return _.map(series,function(p) { + return [p[0],p[1]/kbn.interval_to_seconds(interval)]; + }); + }; + + var derivative = function(series) { + return _.map(series, function(p,i) { + var _v; + if(i === 0 || p[1] === null) { + _v = [p[0],null]; + } else { + _v = series[i-1][1] === null ? [p[0],null] : [p[0],p[1]-(series[i-1][1])]; + } + return _v; + }); + }; + + // Function for rendering panel + function render_panel() { + // IE doesn't work without this + elem.css({height:scope.panel.height || scope.row.height}); + + // Populate from the query service + try { + _.each(scope.data, function(series) { + series.label = series.info.alias; + series.color = series.info.color; + }); + } catch(e) {return;} + + // Set barwidth based on specified interval + var barwidth = kbn.interval_to_ms(scope.panel.interval); + + var stack = scope.panel.stack ? true : null; + + // Populate element + try { + var options = { + legend: { show: false }, + series: { + stackpercent: scope.panel.stack ? scope.panel.percentage : false, + stack: scope.panel.percentage ? null : stack, + lines: { + show: scope.panel.lines, + // Silly, but fixes bug in stacked percentages + fill: scope.panel.fill === 0 ? 0.001 : scope.panel.fill/10, + lineWidth: scope.panel.linewidth, + steps: false + }, + bars: { + show: scope.panel.bars, + fill: 1, + barWidth: barwidth/1.5, + zero: false, + lineWidth: 0 + }, + points: { + show: scope.panel.points, + fill: 1, + fillColor: false, + radius: scope.panel.pointradius + }, + shadowSize: 1 + }, + yaxis: { + show: scope.panel['y-axis'], + min: scope.panel.grid.min, + max: scope.panel.percentage && scope.panel.stack ? 100 : scope.panel.grid.max + }, + xaxis: { + timezone: scope.panel.timezone, + show: scope.panel['x-axis'], + mode: "time", + min: _.isUndefined(scope.range.from) ? null : scope.range.from.getTime(), + max: _.isUndefined(scope.range.to) ? null : scope.range.to.getTime(), + timeformat: time_format(scope.panel.interval), + label: "Datetime", + ticks: elem.width()/100 + }, + grid: { + backgroundColor: null, + borderWidth: 0, + hoverable: true, + color: '#c8c8c8' + } + }; + + if(scope.panel.y_format === 'bytes') { + options.yaxis.mode = "byte"; + } + + if(scope.panel.y_format === 'short') { + options.yaxis.tickFormatter = function(val) { + return kbn.shortFormat(val,0); + }; + } + + if(scope.panel.annotate.enable) { + options.events = { + levels: 1, + data: scope.annotations, + types: { + 'annotation': { + level: 1, + icon: { + icon: "icon-tag icon-flip-vertical", + size: 20, + color: "#222", + outline: "#bbb" + } + } + } + //xaxis: int // the x axis to attach events to + }; + } + + if(scope.panel.interactive) { + options.selection = { mode: "x", color: '#666' }; + } + + // when rendering stacked bars, we need to ensure each point that has data is zero-filled + // so that the stacking happens in the proper order + var required_times = []; + if (scope.data.length > 1) { + required_times = Array.prototype.concat.apply([], _.map(scope.data, function (query) { + return query.time_series.getOrderedTimes(); + })); + required_times = _.uniq(required_times.sort(function (a, b) { + // decending numeric sort + return a-b; + }), true); + } + + + for (var i = 0; i < scope.data.length; i++) { + var _d = scope.data[i].time_series.getFlotPairs(required_times); + if(scope.panel.derivative) { + _d = derivative(_d); + } + if(scope.panel.scale !== 1) { + _d = scale(_d,scope.panel.scale); + } + if(scope.panel.scaleSeconds) { + _d = scaleSeconds(_d,scope.panel.interval); + } + scope.data[i].data = _d; + } + + scope.plot = $.plot(elem, scope.data, options); + + } catch(e) { + // Nothing to do here + } + } + + function time_format(interval) { + var _int = kbn.interval_to_seconds(interval); + if(_int >= 2628000) { + return "%Y-%m"; + } + if(_int >= 86400) { + return "%Y-%m-%d"; + } + if(_int >= 60) { + return "%H:%M
%m-%d"; + } + + return "%H:%M:%S"; + } + + var $tooltip = $('
'); + elem.bind("plothover", function (event, pos, item) { + var group, value, timestamp; + if (item) { + if (item.series.info.alias || scope.panel.tooltip.query_as_alias) { + group = '' + + '' + ' ' + + (item.series.info.alias || item.series.info.query)+ + '
'; + } else { + group = kbn.query_color_dot(item.series.color, 15) + ' '; + } + value = (scope.panel.stack && scope.panel.tooltip.value_type === 'individual') ? + item.datapoint[1] - item.datapoint[2] : + item.datapoint[1]; + if(scope.panel.y_format === 'bytes') { + value = kbn.byteFormat(value,2); + } + if(scope.panel.y_format === 'short') { + value = kbn.shortFormat(value,2); + } + timestamp = scope.panel.timezone === 'browser' ? + moment(item.datapoint[0]).format('YYYY-MM-DD HH:mm:ss') : + moment.utc(item.datapoint[0]).format('YYYY-MM-DD HH:mm:ss'); + $tooltip + .html( + group + value + " @ " + timestamp + ) + .place_tt(pos.pageX, pos.pageY); + } else { + $tooltip.detach(); + } + }); + + elem.bind("plotselected", function (event, ranges) { + filterSrv.set({ + type : 'time', + from : moment.utc(ranges.xaxis.from).toDate(), + to : moment.utc(ranges.xaxis.to).toDate(), + field : scope.panel.time_field + }); + }); + } + }; + }); + +}); diff --git a/src/app/panels/histogram/queriesEditor.html b/src/app/panels/histogram/queriesEditor.html new file mode 100644 index 00000000000..414de272ef8 --- /dev/null +++ b/src/app/panels/histogram/queriesEditor.html @@ -0,0 +1,43 @@ +

Charted

+
+ +
+

Markers

+ +
+ Here you can specify a query to be plotted on your chart as a marker. Hovering over a marker will display the field you specify below. If more documents are found than the limit you set, they will be scored by Elasticsearch and events that best match your query will be displayed. +
+ +

+

+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
diff --git a/src/app/panels/histogram/styleEditor.html b/src/app/panels/histogram/styleEditor.html new file mode 100644 index 00000000000..874a22640b5 --- /dev/null +++ b/src/app/panels/histogram/styleEditor.html @@ -0,0 +1,88 @@ +
+
+
Chart Options
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
Multiple Series
+
+ +
+
+ + +
+
+ + +
+
+
+ +
+
+
Header
+
+ +
+
+ +
+
+
+
Legend
+
+ +
+
+ +
+
+ +
+
+ +
+
Grid
+
+ + +
+
+ + +
+
+ +
diff --git a/src/app/panels/histogram/timeSeries.js b/src/app/panels/histogram/timeSeries.js new file mode 100644 index 00000000000..8885e734371 --- /dev/null +++ b/src/app/panels/histogram/timeSeries.js @@ -0,0 +1,216 @@ +define([ + 'underscore', + './interval' +], +function (_, Interval) { + 'use strict'; + + var ts = {}; + + // map compatable parseInt + function base10Int(val) { + return parseInt(val, 10); + } + + // trim the ms off of a time, but return it with empty ms. + function getDatesTime(date) { + return Math.floor(date.getTime() / 1000)*1000; + } + + /** + * Certain graphs require 0 entries to be specified for them to render + * properly (like the line graph). So with this we will caluclate all of + * the expected time measurements, and fill the missing ones in with 0 + * @param {object} opts An object specifying some/all of the options + * + * OPTIONS: + * @opt {string} interval The interval notion describing the expected spacing between + * each data point. + * @opt {date} start_date (optional) The start point for the time series, setting this and the + * end_date will ensure that the series streches to resemble the entire + * expected result + * @opt {date} end_date (optional) The end point for the time series, see start_date + * @opt {string} fill_style Either "minimal", or "all" describing the strategy used to zero-fill + * the series. + */ + ts.ZeroFilled = function (opts) { + opts = _.defaults(opts, { + interval: '10m', + start_date: null, + end_date: null, + fill_style: 'minimal' + }); + + // the expected differenece between readings. + this.interval = new Interval(opts.interval); + + // will keep all values here, keyed by their time + this._data = {}; + this.start_time = opts.start_date && getDatesTime(opts.start_date); + this.end_time = opts.end_date && getDatesTime(opts.end_date); + this.opts = opts; + }; + + /** + * Add a row + * @param {int} time The time for the value, in + * @param {any} value The value at this time + */ + ts.ZeroFilled.prototype.addValue = function (time, value) { + if (time instanceof Date) { + time = getDatesTime(time); + } else { + time = base10Int(time); + } + if (!isNaN(time)) { + this._data[time] = (_.isUndefined(value) ? 0 : value); + } + this._cached_times = null; + }; + + /** + * Get an array of the times that have been explicitly set in the series + * @param {array} include (optional) list of timestamps to include in the response + * @return {array} An array of integer times. + */ + ts.ZeroFilled.prototype.getOrderedTimes = function (include) { + var times = _.map(_.keys(this._data), base10Int); + if (_.isArray(include)) { + times = times.concat(include); + } + return _.uniq(times.sort(function (a, b) { + // decending numeric sort + return a - b; + }), true); + }; + + /** + * return the rows in the format: + * [ [time, value], [time, value], ... ] + * + * Heavy lifting is done by _get(Min|Default|All)FlotPairs() + * @param {array} required_times An array of timestamps that must be in the resulting pairs + * @return {array} + */ + ts.ZeroFilled.prototype.getFlotPairs = function (required_times) { + var times = this.getOrderedTimes(required_times), + strategy, + pairs; + + if(this.opts.fill_style === 'all') { + strategy = this._getAllFlotPairs; + } else if(this.opts.fill_style === 'null') { + strategy = this._getNullFlotPairs; + } else { + strategy = this._getMinFlotPairs; + } + + pairs = _.reduce( + times, // what + strategy, // how + [], // where + this // context + ); + + // if the first or last pair is inside either the start or end time, + // add those times to the series with null values so the graph will stretch to contain them. + // Removing, flot 0.8.1's max/min params satisfy this + /* + if (this.start_time && (pairs.length === 0 || pairs[0][0] > this.start_time)) { + pairs.unshift([this.start_time, null]); + } + if (this.end_time && (pairs.length === 0 || pairs[pairs.length - 1][0] < this.end_time)) { + pairs.push([this.end_time, null]); + } + */ + + return pairs; + }; + + /** + * ** called as a reduce stragegy in getFlotPairs() ** + * Fill zero's on either side of the current time, unless there is already a measurement there or + * we are looking at an edge. + * @return {array} An array of points to plot with flot + */ + ts.ZeroFilled.prototype._getMinFlotPairs = function (result, time, i, times) { + var next, expected_next, prev, expected_prev; + + // check for previous measurement + if (i > 0) { + prev = times[i - 1]; + expected_prev = this.interval.before(time); + if (prev < expected_prev) { + result.push([expected_prev, 0]); + } + } + + // add the current time + result.push([ time, this._data[time] || 0]); + + // check for next measurement + if (times.length > i) { + next = times[i + 1]; + expected_next = this.interval.after(time); + if (next > expected_next) { + result.push([expected_next, 0]); + } + } + + return result; + }; + + /** + * ** called as a reduce stragegy in getFlotPairs() ** + * Fill zero's to the right of each time, until the next measurement is reached or we are at the + * last measurement + * @return {array} An array of points to plot with flot + */ + ts.ZeroFilled.prototype._getAllFlotPairs = function (result, time, i, times) { + var next, expected_next; + + result.push([ times[i], this._data[times[i]] || 0 ]); + next = times[i + 1]; + expected_next = this.interval.after(time); + for(; times.length > i && next > expected_next; expected_next = this.interval.after(expected_next)) { + result.push([expected_next, 0]); + } + + return result; + }; + + /** + * ** called as a reduce stragegy in getFlotPairs() ** + * Same as min, but fills with nulls + * @return {array} An array of points to plot with flot + */ + ts.ZeroFilled.prototype._getNullFlotPairs = function (result, time, i, times) { + var next, expected_next, prev, expected_prev; + + // check for previous measurement + if (i > 0) { + prev = times[i - 1]; + expected_prev = this.interval.before(time); + if (prev < expected_prev) { + result.push([expected_prev, null]); + } + } + + // add the current time + result.push([ time, this._data[time] || null]); + + // check for next measurement + if (times.length > i) { + next = times[i + 1]; + expected_next = this.interval.after(time); + if (next > expected_next) { + result.push([expected_next, null]); + } + } + + return result; + }; + + + return ts; +}); \ No newline at end of file diff --git a/src/config.js b/src/config.js index 3c3497d7596..77d370d693d 100644 --- a/src/config.js +++ b/src/config.js @@ -21,7 +21,7 @@ function (Settings) { * elasticsearch installed on. You probably want to set it to the FQDN of your * elasticsearch host */ - elasticsearch: "http://"+window.location.hostname+":9200", + elasticsearch: "http://se0-elasticstash-01:9200", /** @scratch /configuration/config.js/5 * ==== kibana-int ==== @@ -40,7 +40,8 @@ function (Settings) { panel_names: [ 'graph', 'text', - 'column' + 'column', + 'histogram' ] }); });