From d3bbc245c9fb8d8dd7d0e28005490c928663cf8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 10 May 2016 14:32:25 +0200 Subject: [PATCH 01/37] feat(query_part): began query part refactor to be able to reuse it in prometheus query editor --- .../core/components/query_part/query_part.ts | 123 +++++++++++ .../query_part/query_part_editor.ts | 0 .../plugins/datasource/influxdb/query_part.ts | 198 +++++------------- 3 files changed, 171 insertions(+), 150 deletions(-) create mode 100644 public/app/core/components/query_part/query_part.ts create mode 100644 public/app/core/components/query_part/query_part_editor.ts diff --git a/public/app/core/components/query_part/query_part.ts b/public/app/core/components/query_part/query_part.ts new file mode 100644 index 00000000000..90724f65d2d --- /dev/null +++ b/public/app/core/components/query_part/query_part.ts @@ -0,0 +1,123 @@ +/// + +import _ from 'lodash'; + +export class QueryPartDef { + type: string; + params: any[]; + defaultParams: any[]; + renderer: any; + category: any; + addStrategy: any; + + constructor(options: any) { + this.type = options.type; + this.params = options.params; + this.defaultParams = options.defaultParams; + this.renderer = options.renderer; + this.category = options.category; + this.addStrategy = options.addStrategy; + } +} + +export class QueryPart { + part: any; + def: QueryPartDef; + params: any[]; + text: string; + + constructor(part: any, def: any) { + this.part = part; + this.def = def; + if (!this.def) { + throw {message: 'Could not find query part ' + part.type}; + } + + part.params = part.params || _.clone(this.def.defaultParams); + this.params = part.params; + this.updateText(); + } + + render(innerExpr: string) { + return this.def.renderer(this, innerExpr); + } + + hasMultipleParamsInString (strValue, index) { + if (strValue.indexOf(',') === -1) { + return false; + } + + return this.def.params[index + 1] && this.def.params[index + 1].optional; + } + + updateParam (strValue, index) { + // handle optional parameters + // if string contains ',' and next param is optional, split and update both + if (this.hasMultipleParamsInString(strValue, index)) { + _.each(strValue.split(','), function(partVal: string, idx) { + this.updateParam(partVal.trim(), idx); + }, this); + return; + } + + if (strValue === '' && this.def.params[index].optional) { + this.params.splice(index, 1); + } else { + this.params[index] = strValue; + } + + this.part.params = this.params; + this.updateText(); + } + + updateText() { + if (this.params.length === 0) { + this.text = this.def.type + '()'; + return; + } + + var text = this.def.type + '('; + text += this.params.join(', '); + text += ')'; + this.text = text; + } +} + +export function functionRenderer(part, innerExpr) { + var str = part.def.type + '('; + var parameters = _.map(part.params, (value, index) => { + var paramType = part.def.params[index]; + if (paramType.type === 'time') { + if (value === 'auto') { + value = '$interval'; + } + } + if (paramType.quote === 'single') { + return "'" + value + "'"; + } else if (paramType.quote === 'double') { + return '"' + value + '"'; + } + + return value; + }); + + if (innerExpr) { + parameters.unshift(innerExpr); + } + return str + parameters.join(', ') + ')'; +} + + +export function suffixRenderer(part, innerExpr) { + return innerExpr + ' ' + part.params[0]; +} + +export function identityRenderer(part, innerExpr) { + return part.params[0]; +} + +export function quotedIdentityRenderer(part, innerExpr) { + return '"' + part.params[0] + '"'; +} + + diff --git a/public/app/core/components/query_part/query_part_editor.ts b/public/app/core/components/query_part/query_part_editor.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/public/app/plugins/datasource/influxdb/query_part.ts b/public/app/plugins/datasource/influxdb/query_part.ts index 63d70be5653..f22713a8057 100644 --- a/public/app/plugins/datasource/influxdb/query_part.ts +++ b/public/app/plugins/datasource/influxdb/query_part.ts @@ -1,6 +1,14 @@ /// import _ from 'lodash'; +import { + QueryPartDef, + QueryPart, + functionRenderer, + suffixRenderer, + identityRenderer, + quotedIdentityRenderer, +} from 'app/core/components/query_part/query_part'; var index = []; var categories = { @@ -12,71 +20,26 @@ var categories = { Fields: [], }; +function createPart(part): any { + var def = index[part.type]; + if (!def) { + throw {message: 'Could not find query part ' + part.type}; + } + + return new QueryPart(part, def); +}; + +function register(options: any) { + index[options.type] = new QueryPartDef(options); + options.category.push(index[options.type]); +} + var groupByTimeFunctions = []; -class QueryPartDef { - type: string; - params: any[]; - defaultParams: any[]; - renderer: any; - category: any; - addStrategy: any; - - constructor(options: any) { - this.type = options.type; - this.params = options.params; - this.defaultParams = options.defaultParams; - this.renderer = options.renderer; - this.category = options.category; - this.addStrategy = options.addStrategy; - } - - static register(options: any) { - index[options.type] = new QueryPartDef(options); - options.category.push(index[options.type]); - } -} - -function functionRenderer(part, innerExpr) { - var str = part.def.type + '('; - var parameters = _.map(part.params, (value, index) => { - var paramType = part.def.params[index]; - if (paramType.type === 'time') { - if (value === 'auto') { - value = '$interval'; - } - } - if (paramType.quote === 'single') { - return "'" + value + "'"; - } else if (paramType.quote === 'double') { - return '"' + value + '"'; - } - - return value; - }); - - if (innerExpr) { - parameters.unshift(innerExpr); - } - return str + parameters.join(', ') + ')'; -} - function aliasRenderer(part, innerExpr) { return innerExpr + ' AS ' + '"' + part.params[0] + '"'; } -function suffixRenderer(part, innerExpr) { - return innerExpr + ' ' + part.params[0]; -} - -function identityRenderer(part, innerExpr) { - return part.params[0]; -} - -function quotedIdentityRenderer(part, innerExpr) { - return '"' + part.params[0] + '"'; -} - function fieldRenderer(part, innerExpr) { if (part.params[0] === '*') { return '*'; @@ -149,13 +112,13 @@ function addAliasStrategy(selectParts, partModel) { function addFieldStrategy(selectParts, partModel, query) { // copy all parts var parts = _.map(selectParts, function(part: any) { - return new QueryPart({type: part.def.type, params: _.clone(part.params)}); + return createPart({type: part.def.type, params: _.clone(part.params)}); }); query.selectModels.push(parts); } -QueryPartDef.register({ +register({ type: 'field', addStrategy: addFieldStrategy, category: categories.Fields, @@ -165,7 +128,7 @@ QueryPartDef.register({ }); // Aggregations -QueryPartDef.register({ +register({ type: 'count', addStrategy: replaceAggregationAddStrategy, category: categories.Aggregations, @@ -174,7 +137,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'distinct', addStrategy: replaceAggregationAddStrategy, category: categories.Aggregations, @@ -183,7 +146,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'integral', addStrategy: replaceAggregationAddStrategy, category: categories.Aggregations, @@ -192,7 +155,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'mean', addStrategy: replaceAggregationAddStrategy, category: categories.Aggregations, @@ -201,7 +164,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'median', addStrategy: replaceAggregationAddStrategy, category: categories.Aggregations, @@ -210,7 +173,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'sum', addStrategy: replaceAggregationAddStrategy, category: categories.Aggregations, @@ -221,7 +184,7 @@ QueryPartDef.register({ // transformations -QueryPartDef.register({ +register({ type: 'derivative', addStrategy: addTransformationStrategy, category: categories.Transformations, @@ -230,7 +193,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'non_negative_derivative', addStrategy: addTransformationStrategy, category: categories.Transformations, @@ -239,7 +202,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'difference', addStrategy: addTransformationStrategy, category: categories.Transformations, @@ -248,7 +211,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'moving_average', addStrategy: addTransformationStrategy, category: categories.Transformations, @@ -257,7 +220,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'stddev', addStrategy: addTransformationStrategy, category: categories.Transformations, @@ -266,7 +229,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'time', category: groupByTimeFunctions, params: [{ name: "interval", type: "time", options: ['auto', '1s', '10s', '1m', '5m', '10m', '15m', '1h'] }], @@ -274,7 +237,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'fill', category: groupByTimeFunctions, params: [{ name: "fill", type: "string", options: ['none', 'null', '0', 'previous'] }], @@ -283,7 +246,7 @@ QueryPartDef.register({ }); // Selectors -QueryPartDef.register({ +register({ type: 'bottom', addStrategy: replaceAggregationAddStrategy, category: categories.Selectors, @@ -292,7 +255,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'first', addStrategy: replaceAggregationAddStrategy, category: categories.Selectors, @@ -301,7 +264,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'last', addStrategy: replaceAggregationAddStrategy, category: categories.Selectors, @@ -310,7 +273,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'max', addStrategy: replaceAggregationAddStrategy, category: categories.Selectors, @@ -319,7 +282,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'min', addStrategy: replaceAggregationAddStrategy, category: categories.Selectors, @@ -328,7 +291,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'percentile', addStrategy: replaceAggregationAddStrategy, category: categories.Selectors, @@ -337,7 +300,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'top', addStrategy: replaceAggregationAddStrategy, category: categories.Selectors, @@ -346,7 +309,7 @@ QueryPartDef.register({ renderer: functionRenderer, }); -QueryPartDef.register({ +register({ type: 'tag', category: groupByTimeFunctions, params: [{name: 'tag', type: 'string', dynamicLookup: true}], @@ -354,7 +317,7 @@ QueryPartDef.register({ renderer: fieldRenderer, }); -QueryPartDef.register({ +register({ type: 'math', addStrategy: addMathStrategy, category: categories.Math, @@ -363,7 +326,7 @@ QueryPartDef.register({ renderer: suffixRenderer, }); -QueryPartDef.register({ +register({ type: 'alias', addStrategy: addAliasStrategy, category: categories.Aliasing, @@ -373,74 +336,9 @@ QueryPartDef.register({ renderer: aliasRenderer, }); -class QueryPart { - part: any; - def: QueryPartDef; - params: any[]; - text: string; - - constructor(part: any) { - this.part = part; - this.def = index[part.type]; - if (!this.def) { - throw {message: 'Could not find query part ' + part.type}; - } - - part.params = part.params || _.clone(this.def.defaultParams); - this.params = part.params; - this.updateText(); - } - - render(innerExpr: string) { - return this.def.renderer(this, innerExpr); - } - - hasMultipleParamsInString (strValue, index) { - if (strValue.indexOf(',') === -1) { - return false; - } - - return this.def.params[index + 1] && this.def.params[index + 1].optional; - } - - updateParam (strValue, index) { - // handle optional parameters - // if string contains ',' and next param is optional, split and update both - if (this.hasMultipleParamsInString(strValue, index)) { - _.each(strValue.split(','), function(partVal: string, idx) { - this.updateParam(partVal.trim(), idx); - }, this); - return; - } - - if (strValue === '' && this.def.params[index].optional) { - this.params.splice(index, 1); - } else { - this.params[index] = strValue; - } - - this.part.params = this.params; - this.updateText(); - } - - updateText() { - if (this.params.length === 0) { - this.text = this.def.type + '()'; - return; - } - - var text = this.def.type + '('; - text += this.params.join(', '); - text += ')'; - this.text = text; - } -} export default { - create: function(part): any { - return new QueryPart(part); - }, - + create: createPart, getCategories: function() { return categories; } From cca37caf331ce62eb3cbdb16b02bbbeb1d9583c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 10 May 2016 15:48:07 +0200 Subject: [PATCH 02/37] feat(prometheus): began work on prometheus query model --- .../datasource/prometheus/prom_query.ts | 79 +++++++++++++++++++ .../prometheus/specs/query_specs.ts | 23 ++++++ 2 files changed, 102 insertions(+) create mode 100644 public/app/plugins/datasource/prometheus/prom_query.ts create mode 100644 public/app/plugins/datasource/prometheus/specs/query_specs.ts diff --git a/public/app/plugins/datasource/prometheus/prom_query.ts b/public/app/plugins/datasource/prometheus/prom_query.ts new file mode 100644 index 00000000000..e9ef6ddf40d --- /dev/null +++ b/public/app/plugins/datasource/prometheus/prom_query.ts @@ -0,0 +1,79 @@ +import { + QueryPartDef, + QueryPart, + functionRenderer, + suffixRenderer, + identityRenderer, + quotedIdentityRenderer, +} from 'app/core/components/query_part/query_part'; + +var index = []; +var categories = { + Functions: [], +}; + +export class PromQuery { + target: any; + metric: string; + range: string; + filters: any[]; + functions: any[]; + templateSrv: any; + scopedVars: any; + + constructor(target, templateSrv?, scopedVars?) { + this.target = target; + this.templateSrv = templateSrv; + this.scopedVars = scopedVars; + } + + render() { + var query = this.target.metric; + if (this.target.range) { + query += '[' + this.target.range + ']'; + } + + for (let funcModel of this.target.functions) { + var partDef = index[funcModel.type]; + if (!partDef) { + continue; + } + + var part = new QueryPart(funcModel, partDef); + query = part.render(query); + } + + return query; + } +} + +export function createPart(part): any { + var def = index[part.type]; + if (!def) { + throw {message: 'Could not find query part ' + part.type}; + } + + return new QueryPart(part, def); +} + +function register(options: any) { + index[options.type] = new QueryPartDef(options); + options.category.push(index[options.type]); +} + +function addFunctionStrategy(model, partModel) { + model.functions.push(partModel); +} + +register({ + type: 'rate', + addStrategy: addFunctionStrategy, + category: categories.Functions, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +export function getCategories() { + return categories; +} diff --git a/public/app/plugins/datasource/prometheus/specs/query_specs.ts b/public/app/plugins/datasource/prometheus/specs/query_specs.ts new file mode 100644 index 00000000000..5bb164c306e --- /dev/null +++ b/public/app/plugins/datasource/prometheus/specs/query_specs.ts @@ -0,0 +1,23 @@ +import {describe, beforeEach, it, sinon, expect} from 'test/lib/common'; + +import {PromQuery} from '../prom_query'; + +describe.only('PromQuery', function() { + var templateSrv = {replace: val => val}; + + describe('render series with mesurement only', function() { + it('should generate correct query', function() { + var query = new PromQuery({ + metric: 'cpu', + range: '5m', + functions: [ + {type: 'rate', params: []} + ] + }, templateSrv, {}); + + var queryText = query.render(); + expect(queryText).to.be('rate(cpu[5m])'); + }); + }); + +}); From b170b6ec8b91485370a3fc7f3f8c7b947a4a7cc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 11 May 2016 14:52:44 +0200 Subject: [PATCH 03/37] feat(query part): moved query part editor from influxdb to core --- .../query_part/query_part_editor.ts | 183 ++++++++++++++++++ public/app/core/core.ts | 2 + .../influxdb/partials/query.editor.html | 8 +- .../influxdb/partials/query_part.html | 5 - .../plugins/datasource/influxdb/query_ctrl.ts | 3 - .../datasource/influxdb/query_part_editor.js | 178 ----------------- .../prometheus/partials/query.editor.html | 30 ++- .../datasource/prometheus/prom_query.ts | 33 +++- .../datasource/prometheus/query_ctrl.ts | 60 ++++-- 9 files changed, 277 insertions(+), 225 deletions(-) delete mode 100644 public/app/plugins/datasource/influxdb/partials/query_part.html delete mode 100644 public/app/plugins/datasource/influxdb/query_part_editor.js diff --git a/public/app/core/components/query_part/query_part_editor.ts b/public/app/core/components/query_part/query_part_editor.ts index e69de29bb2d..f9122ee283b 100644 --- a/public/app/core/components/query_part/query_part_editor.ts +++ b/public/app/core/components/query_part/query_part_editor.ts @@ -0,0 +1,183 @@ +/// + +import _ from 'lodash'; +import $ from 'jquery'; +import coreModule from 'app/core/core_module'; + +var template = ` +
+ +
+ +{{part.def.type}} +() +`; + + /** @ngInject */ +export function queryPartEditorDirective($compile, templateSrv) { + + var paramTemplate = ''; + return { + restrict: 'E', + template: template, + scope: { + part: "=", + removeAction: "&", + partUpdated: "&", + getOptions: "&", + }, + link: function postLink($scope, elem) { + var part = $scope.part; + var partDef = part.def; + var $paramsContainer = elem.find('.query-part-parameters'); + var $controlsContainer = elem.find('.tight-form-func-controls'); + + function clickFuncParam(paramIndex) { + /*jshint validthis:true */ + var $link = $(this); + var $input = $link.next(); + + $input.val(part.params[paramIndex]); + $input.css('width', ($link.width() + 16) + 'px'); + + $link.hide(); + $input.show(); + $input.focus(); + $input.select(); + + var typeahead = $input.data('typeahead'); + if (typeahead) { + $input.val(''); + typeahead.lookup(); + } + } + + function inputBlur(paramIndex) { + /*jshint validthis:true */ + var $input = $(this); + var $link = $input.prev(); + var newValue = $input.val(); + + if (newValue !== '' || part.def.params[paramIndex].optional) { + $link.html(templateSrv.highlightVariablesAsHtml(newValue)); + + part.updateParam($input.val(), paramIndex); + $scope.$apply($scope.partUpdated); + } + + $input.hide(); + $link.show(); + } + + function inputKeyPress(paramIndex, e) { + /*jshint validthis:true */ + if (e.which === 13) { + inputBlur.call(this, paramIndex); + } + } + + function inputKeyDown() { + /*jshint validthis:true */ + this.style.width = (3 + this.value.length) * 8 + 'px'; + } + + function addTypeahead($input, param, paramIndex) { + if (!param.options && !param.dynamicLookup) { + return; + } + + var typeaheadSource = function (query, callback) { + if (param.options) { return param.options; } + + $scope.$apply(function() { + $scope.getOptions().then(function(result) { + var dynamicOptions = _.map(result, function(op) { return op.value; }); + callback(dynamicOptions); + }); + }); + }; + + $input.attr('data-provide', 'typeahead'); + var options = param.options; + if (param.type === 'int') { + options = _.map(options, function(val) { return val.toString(); }); + } + + $input.typeahead({ + source: typeaheadSource, + minLength: 0, + items: 1000, + updater: function (value) { + setTimeout(function() { + inputBlur.call($input[0], paramIndex); + }, 0); + return value; + } + }); + + var typeahead = $input.data('typeahead'); + typeahead.lookup = function () { + this.query = this.$element.val() || ''; + var items = this.source(this.query, $.proxy(this.process, this)); + return items ? this.process(items) : items; + }; + } + + $scope.toggleControls = function() { + var targetDiv = elem.closest('.tight-form'); + + if (elem.hasClass('show-function-controls')) { + elem.removeClass('show-function-controls'); + targetDiv.removeClass('has-open-function'); + $controlsContainer.hide(); + return; + } + + elem.addClass('show-function-controls'); + targetDiv.addClass('has-open-function'); + $controlsContainer.show(); + }; + + $scope.removeActionInternal = function() { + $scope.toggleControls(); + $scope.removeAction(); + }; + + function addElementsAndCompile() { + _.each(partDef.params, function(param, index) { + if (param.optional && part.params.length <= index) { + return; + } + + if (index > 0) { + $(', ').appendTo($paramsContainer); + } + + var paramValue = templateSrv.highlightVariablesAsHtml(part.params[index]); + var $paramLink = $('' + paramValue + ''); + var $input = $(paramTemplate); + + $paramLink.appendTo($paramsContainer); + $input.appendTo($paramsContainer); + + $input.blur(_.partial(inputBlur, index)); + $input.keyup(inputKeyDown); + $input.keypress(_.partial(inputKeyPress, index)); + $paramLink.click(_.partial(clickFuncParam, index)); + + addTypeahead($input, param, index); + }); + } + + function relink() { + $paramsContainer.empty(); + addElementsAndCompile(); + } + + relink(); + } + }; +} + +coreModule.directive('queryPartEditor', queryPartEditorDirective); diff --git a/public/app/core/core.ts b/public/app/core/core.ts index abebb5ce560..9c8ae9cdad2 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -33,6 +33,7 @@ import {Emitter} from './utils/emitter'; import {layoutSelector} from './components/layout_selector/layout_selector'; import {switchDirective} from './components/switch'; import {dashboardSelector} from './components/dashboard_selector'; +import {queryPartEditorDirective} from './components/query_part/query_part_editor'; import 'app/core/controllers/all'; import 'app/core/services/all'; import 'app/core/routes/routes'; @@ -56,4 +57,5 @@ export { Emitter, appEvents, dashboardSelector, + queryPartEditorDirective, }; diff --git a/public/app/plugins/datasource/influxdb/partials/query.editor.html b/public/app/plugins/datasource/influxdb/partials/query.editor.html index 0d6dd5c5c3b..68b8ee60d98 100644 --- a/public/app/plugins/datasource/influxdb/partials/query.editor.html +++ b/public/app/plugins/datasource/influxdb/partials/query.editor.html @@ -35,13 +35,13 @@
- - +
@@ -62,12 +62,12 @@ GROUP BY - - +
diff --git a/public/app/plugins/datasource/influxdb/partials/query_part.html b/public/app/plugins/datasource/influxdb/partials/query_part.html deleted file mode 100644 index 478edfe5c29..00000000000 --- a/public/app/plugins/datasource/influxdb/partials/query_part.html +++ /dev/null @@ -1,5 +0,0 @@ -
- -
- -{{part.def.type}}() diff --git a/public/app/plugins/datasource/influxdb/query_ctrl.ts b/public/app/plugins/datasource/influxdb/query_ctrl.ts index 8d6a03fc4a1..69895e53c25 100644 --- a/public/app/plugins/datasource/influxdb/query_ctrl.ts +++ b/public/app/plugins/datasource/influxdb/query_ctrl.ts @@ -1,8 +1,5 @@ /// -import './query_part_editor'; -import './query_part_editor'; - import angular from 'angular'; import _ from 'lodash'; import InfluxQueryBuilder from './query_builder'; diff --git a/public/app/plugins/datasource/influxdb/query_part_editor.js b/public/app/plugins/datasource/influxdb/query_part_editor.js deleted file mode 100644 index 4e044eca304..00000000000 --- a/public/app/plugins/datasource/influxdb/query_part_editor.js +++ /dev/null @@ -1,178 +0,0 @@ -define([ - 'angular', - 'lodash', - 'jquery', -], -function (angular, _, $) { - 'use strict'; - - angular - .module('grafana.directives') - .directive('influxQueryPartEditor', function($compile, templateSrv) { - - var paramTemplate = ''; - return { - restrict: 'E', - templateUrl: 'public/app/plugins/datasource/influxdb/partials/query_part.html', - scope: { - part: "=", - removeAction: "&", - partUpdated: "&", - getOptions: "&", - }, - link: function postLink($scope, elem) { - var part = $scope.part; - var partDef = part.def; - var $paramsContainer = elem.find('.query-part-parameters'); - var $controlsContainer = elem.find('.tight-form-func-controls'); - - function clickFuncParam(paramIndex) { - /*jshint validthis:true */ - var $link = $(this); - var $input = $link.next(); - - $input.val(part.params[paramIndex]); - $input.css('width', ($link.width() + 16) + 'px'); - - $link.hide(); - $input.show(); - $input.focus(); - $input.select(); - - var typeahead = $input.data('typeahead'); - if (typeahead) { - $input.val(''); - typeahead.lookup(); - } - } - - function inputBlur(paramIndex) { - /*jshint validthis:true */ - var $input = $(this); - var $link = $input.prev(); - var newValue = $input.val(); - - if (newValue !== '' || part.def.params[paramIndex].optional) { - $link.html(templateSrv.highlightVariablesAsHtml(newValue)); - - part.updateParam($input.val(), paramIndex); - $scope.$apply($scope.partUpdated); - } - - $input.hide(); - $link.show(); - } - - function inputKeyPress(paramIndex, e) { - /*jshint validthis:true */ - if(e.which === 13) { - inputBlur.call(this, paramIndex); - } - } - - function inputKeyDown() { - /*jshint validthis:true */ - this.style.width = (3 + this.value.length) * 8 + 'px'; - } - - function addTypeahead($input, param, paramIndex) { - if (!param.options && !param.dynamicLookup) { - return; - } - - var typeaheadSource = function (query, callback) { - if (param.options) { return param.options; } - - $scope.$apply(function() { - $scope.getOptions().then(function(result) { - var dynamicOptions = _.map(result, function(op) { return op.value; }); - callback(dynamicOptions); - }); - }); - }; - - $input.attr('data-provide', 'typeahead'); - var options = param.options; - if (param.type === 'int') { - options = _.map(options, function(val) { return val.toString(); }); - } - - $input.typeahead({ - source: typeaheadSource, - minLength: 0, - items: 1000, - updater: function (value) { - setTimeout(function() { - inputBlur.call($input[0], paramIndex); - }, 0); - return value; - } - }); - - var typeahead = $input.data('typeahead'); - typeahead.lookup = function () { - this.query = this.$element.val() || ''; - var items = this.source(this.query, $.proxy(this.process, this)); - return items ? this.process(items) : items; - }; - } - - $scope.toggleControls = function() { - var targetDiv = elem.closest('.tight-form'); - - if (elem.hasClass('show-function-controls')) { - elem.removeClass('show-function-controls'); - targetDiv.removeClass('has-open-function'); - $controlsContainer.hide(); - return; - } - - elem.addClass('show-function-controls'); - targetDiv.addClass('has-open-function'); - $controlsContainer.show(); - }; - - $scope.removeActionInternal = function() { - $scope.toggleControls(); - $scope.removeAction(); - }; - - function addElementsAndCompile() { - _.each(partDef.params, function(param, index) { - if (param.optional && part.params.length <= index) { - return; - } - - if (index > 0) { - $(', ').appendTo($paramsContainer); - } - - var paramValue = templateSrv.highlightVariablesAsHtml(part.params[index]); - var $paramLink = $('' + paramValue + ''); - var $input = $(paramTemplate); - - $paramLink.appendTo($paramsContainer); - $input.appendTo($paramsContainer); - - $input.blur(_.partial(inputBlur, index)); - $input.keyup(inputKeyDown); - $input.keypress(_.partial(inputKeyPress, index)); - $paramLink.click(_.partial(clickFuncParam, index)); - - addTypeahead($input, param, index); - }); - } - - function relink() { - $paramsContainer.empty(); - addElementsAndCompile(); - } - - relink(); - } - }; - - }); - -}); diff --git a/public/app/plugins/datasource/prometheus/partials/query.editor.html b/public/app/plugins/datasource/prometheus/partials/query.editor.html index 37c27ace58a..a67e3e9b188 100644 --- a/public/app/plugins/datasource/prometheus/partials/query.editor.html +++ b/public/app/plugins/datasource/prometheus/partials/query.editor.html @@ -1,18 +1,34 @@
-
- - +
+ +
-
- - + +
+ + +
+ +
+ +
+ +
+
- + diff --git a/public/app/plugins/datasource/prometheus/prom_query.ts b/public/app/plugins/datasource/prometheus/prom_query.ts index e9ef6ddf40d..83ac4f1615f 100644 --- a/public/app/plugins/datasource/prometheus/prom_query.ts +++ b/public/app/plugins/datasource/prometheus/prom_query.ts @@ -7,6 +7,8 @@ import { quotedIdentityRenderer, } from 'app/core/components/query_part/query_part'; +import _ from 'lodash'; + var index = []; var categories = { Functions: [], @@ -23,8 +25,21 @@ export class PromQuery { constructor(target, templateSrv?, scopedVars?) { this.target = target; + + this.target.expr = this.target.expr || ''; + this.target.intervalFactor = this.target.intervalFactor || 2; + this.target.functions = this.target.functions || []; + this.templateSrv = templateSrv; this.scopedVars = scopedVars; + + this.updateProjection(); + } + + updateProjection() { + this.functions = _.map(this.target.functions, function(func: any) { + return createPart(func); + }); } render() { @@ -33,18 +48,17 @@ export class PromQuery { query += '[' + this.target.range + ']'; } - for (let funcModel of this.target.functions) { - var partDef = index[funcModel.type]; - if (!partDef) { - continue; - } - - var part = new QueryPart(funcModel, partDef); - query = part.render(query); + for (let func of this.functions) { + query = func.render(query); } return query; } + + addQueryPart(category, item) { + var partModel = createPart({type: item.text}); + partModel.def.addStrategy(this, partModel); + } } export function createPart(part): any { @@ -63,6 +77,7 @@ function register(options: any) { function addFunctionStrategy(model, partModel) { model.functions.push(partModel); + model.target.functions.push(partModel.part); } register({ @@ -74,6 +89,6 @@ register({ renderer: functionRenderer, }); -export function getCategories() { +export function getQueryPartCategories() { return categories; } diff --git a/public/app/plugins/datasource/prometheus/query_ctrl.ts b/public/app/plugins/datasource/prometheus/query_ctrl.ts index 8284268ef39..25a803b7bb3 100644 --- a/public/app/plugins/datasource/prometheus/query_ctrl.ts +++ b/public/app/plugins/datasource/prometheus/query_ctrl.ts @@ -6,46 +6,68 @@ import moment from 'moment'; import * as dateMath from 'app/core/utils/datemath'; import {QueryCtrl} from 'app/plugins/sdk'; +import {PromQuery, getQueryPartCategories} from './prom_query'; class PrometheusQueryCtrl extends QueryCtrl { static templateUrl = 'partials/query.editor.html'; - metric: any; + query: any; + metricSegment: any; + addQueryPartMenu: any[]; resolutions: any; oldTarget: any; suggestMetrics: any; linkToPrometheus: any; /** @ngInject */ - constructor($scope, $injector, private templateSrv) { + constructor($scope, $injector, private templateSrv, private uiSegmentSrv) { super($scope, $injector); - var target = this.target; - target.expr = target.expr || ''; - target.intervalFactor = target.intervalFactor || 2; + this.query = new PromQuery(this.target, templateSrv); + + if (this.target.metric) { + this.metricSegment = uiSegmentSrv.newSegment(this.target.metric); + } else { + this.metricSegment = uiSegmentSrv.newSegment({value: 'select metric', fake: true}); + } - this.metric = ''; this.resolutions = _.map([1,2,3,4,5,10], function(f) { return {factor: f, label: '1/' + f}; }); - $scope.$on('typeahead-updated', () => { - this.$scope.$apply(() => { + this.updateLink(); + this.buildQueryPartMenu(); + } - this.target.expr += this.target.metric; - this.metric = ''; - this.refreshMetricData(); + buildQueryPartMenu() { + var categories = getQueryPartCategories(); + this.addQueryPartMenu = _.reduce(categories, function(memo, cat, key) { + var menu = { + text: key, + submenu: cat.map(item => { + return {text: item.type, value: item.type}; + }), + }; + memo.push(menu); + return memo; + }, []); + } + + addQueryPart(item, subItem) { + this.query.addQueryPart(item, subItem); + this.panelCtrl.refresh(); + } + + getMetricOptions() { + return this.datasource.performSuggestQuery('').then(res => { + return _.map(res, metric => { + return this.uiSegmentSrv.newSegment(metric); }); }); + } - // called from typeahead so need this - // here in order to ensure this ref - this.suggestMetrics = (query, callback) => { - console.log(this); - this.datasource.performSuggestQuery(query).then(callback); - }; - - this.updateLink(); + queryChanged() { + this.target.metric = this.metricSegment.value; } refreshMetricData() { From c77d72b29fb806eb848987f085fa1eebf16e4818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 23 May 2016 18:03:17 +0200 Subject: [PATCH 04/37] feat(prometheus): progress on new prometheus query editor, #5117 --- .../prometheus/metric_find_query.js | 128 +++++++++++------- .../prometheus/partials/query.editor.html | 9 +- .../datasource/prometheus/prom_query.ts | 27 +++- .../datasource/prometheus/query_ctrl.ts | 42 +++++- .../specs/metric_find_query_specs.ts | 37 +++++ .../prometheus/specs/query_specs.ts | 17 ++- 6 files changed, 207 insertions(+), 53 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/metric_find_query.js b/public/app/plugins/datasource/prometheus/metric_find_query.js index f449978be81..d3d5376bc19 100644 --- a/public/app/plugins/datasource/prometheus/metric_find_query.js +++ b/public/app/plugins/datasource/prometheus/metric_find_query.js @@ -11,27 +11,33 @@ function (_) { } PrometheusMetricFindQuery.prototype.process = function() { - var label_values_regex = /^label_values\((?:(.+),\s*)?([a-zA-Z_][a-zA-Z0-9_]+)\)$/; - var metric_names_regex = /^metrics\((.+)\)$/; - var query_result_regex = /^query_result\((.+)\)$/; + var labelValuesRegex = /^label_values\((?:(.+),\s*)?([a-zA-Z_][a-zA-Z0-9_]+)\)$/; + var metricNamesRegex = /^metrics\((.+)\)$/; + var labelsRegex = /^labels\((.+)\)$/; + var queryResultRegex = /^query_result\((.+)\)$/; - var label_values_query = this.query.match(label_values_regex); - if (label_values_query) { - if (label_values_query[1]) { - return this.labelValuesQuery(label_values_query[2], label_values_query[1]); + var labelsQuery = this.query.match(labelsRegex); + if (labelsQuery) { + return this.labelsQuery(labelsQuery[1]); + } + + var labelValuesQuery = this.query.match(labelValuesRegex); + if (labelValuesQuery) { + if (labelValuesQuery[1]) { + return this.labelValuesQuery(labelValuesQuery[2], labelValuesQuery[1]); } else { - return this.labelValuesQuery(label_values_query[2], null); + return this.labelValuesQuery(labelValuesQuery[2], null); } } - var metric_names_query = this.query.match(metric_names_regex); - if (metric_names_query) { - return this.metricNameQuery(metric_names_query[1]); + var metricNamesQuery = this.query.match(metricNamesRegex); + if (metricNamesQuery) { + return this.metricNameQuery(metricNamesQuery[1]); } - var query_result_query = this.query.match(query_result_regex); - if (query_result_query) { - return this.queryResultQuery(query_result_query[1]); + var queryResultQuery = this.query.match(queryResultRegex); + if (queryResultQuery) { + return this.queryResultQuery(queryResultQuery[1]); } // if query contains full metric name, return metric name and label list @@ -67,45 +73,71 @@ function (_) { } }; + PrometheusMetricFindQuery.prototype.labelsQuery = function(metric) { + var url; + + url = '/api/v1/series?match[]=' + encodeURIComponent(metric) + + '&start=' + (this.range.from.valueOf() / 1000) + + '&end=' + (this.range.to.valueOf() / 1000); + + return this.datasource._request('GET', url) + .then(function(result) { + var tags = {}; + _.each(result.data.data, function(metric) { + _.each(metric, function(value, key) { + if (key === "__name__") { + return; + } + + tags[key] = key; + }); + }); + + return _.map(tags, function(value) { + return {text: value, value: value}; + }); + }); + }; + PrometheusMetricFindQuery.prototype.metricNameQuery = function(metricFilterPattern) { var url = '/api/v1/label/__name__/values'; return this.datasource._request('GET', url) - .then(function(result) { - return _.chain(result.data.data) - .filter(function(metricName) { - var r = new RegExp(metricFilterPattern); - return r.test(metricName); - }) - .map(function(matchedMetricName) { - return { - text: matchedMetricName, - expandable: true - }; - }) - .value(); - }); + .then(function(result) { + return _.chain(result.data.data) + .filter(function(metricName) { + var r = new RegExp(metricFilterPattern); + return r.test(metricName); + }) + .map(function(matchedMetricName) { + return { + text: matchedMetricName, + expandable: true + }; + }) + .value(); + }); }; PrometheusMetricFindQuery.prototype.queryResultQuery = function(query) { var url = '/api/v1/query?query=' + encodeURIComponent(query) + '&time=' + (this.range.to.valueOf() / 1000); return this.datasource._request('GET', url) - .then(function(result) { - return _.map(result.data.data.result, function(metricData) { - var text = metricData.metric.__name__ || ''; - delete metricData.metric.__name__; - text += '{' + - _.map(metricData.metric, function(v, k) { return k + '="' + v + '"'; }).join(',') + - '}'; - text += ' ' + metricData.value[1] + ' ' + metricData.value[0] * 1000; + .then(function(result) { + return _.map(result.data.data.result, function(metricData) { + var text = metricData.metric.__name__ || ''; + delete metricData.metric.__name__; + text += '{' + + _.map(metricData.metric, function(v, k) { return k + '="' + v + '"'; }).join(',') + + '}'; + text += ' ' + metricData.value[1] + ' ' + metricData.value[0] * 1000; - return { - text: text, - expandable: true - }; + return { + text: text, + expandable: true + }; + }); }); - }); }; PrometheusMetricFindQuery.prototype.metricNameAndLabelsQuery = function(query) { @@ -115,14 +147,14 @@ function (_) { var self = this; return this.datasource._request('GET', url) - .then(function(result) { - return _.map(result.data.data, function(metric) { - return { - text: self.datasource.getOriginalMetricName(metric), - expandable: true - }; + .then(function(result) { + return _.map(result.data.data, function(metric) { + return { + text: self.datasource.getOriginalMetricName(metric), + expandable: true + }; + }); }); - }); }; return PrometheusMetricFindQuery; diff --git a/public/app/plugins/datasource/prometheus/partials/query.editor.html b/public/app/plugins/datasource/prometheus/partials/query.editor.html index a67e3e9b188..359c366cb0f 100644 --- a/public/app/plugins/datasource/prometheus/partials/query.editor.html +++ b/public/app/plugins/datasource/prometheus/partials/query.editor.html @@ -1,5 +1,10 @@ - -
+ + +
+ +
+ +
diff --git a/public/app/plugins/datasource/prometheus/prom_query.ts b/public/app/plugins/datasource/prometheus/prom_query.ts index 83ac4f1615f..d73a8b1cbcf 100644 --- a/public/app/plugins/datasource/prometheus/prom_query.ts +++ b/public/app/plugins/datasource/prometheus/prom_query.ts @@ -2,7 +2,6 @@ import { QueryPartDef, QueryPart, functionRenderer, - suffixRenderer, identityRenderer, quotedIdentityRenderer, } from 'app/core/components/query_part/query_part'; @@ -12,6 +11,7 @@ import _ from 'lodash'; var index = []; var categories = { Functions: [], + GroupBy: [], }; export class PromQuery { @@ -29,6 +29,7 @@ export class PromQuery { this.target.expr = this.target.expr || ''; this.target.intervalFactor = this.target.intervalFactor || 2; this.target.functions = this.target.functions || []; + this.target.editorMode = this.target.editorMode || true; this.templateSrv = templateSrv; this.scopedVars = scopedVars; @@ -80,6 +81,10 @@ function addFunctionStrategy(model, partModel) { model.target.functions.push(partModel.part); } +function groupByLabelRenderer(part, innerExpr) { + return innerExpr + ' by(' + part.params.join(',') + ')'; +} + register({ type: 'rate', addStrategy: addFunctionStrategy, @@ -89,6 +94,26 @@ register({ renderer: functionRenderer, }); +register({ + type: 'sum', + addStrategy: addFunctionStrategy, + category: categories.Functions, + params: [], + defaultParams: [], + renderer: functionRenderer, +}); + +register({ + type: 'by', + addStrategy: addFunctionStrategy, + category: categories.Functions, + params: [ + {name: "label", type: "string", dynamicLookup: true} + ], + defaultParams: [], + renderer: groupByLabelRenderer, +}); + export function getQueryPartCategories() { return categories; } diff --git a/public/app/plugins/datasource/prometheus/query_ctrl.ts b/public/app/plugins/datasource/prometheus/query_ctrl.ts index 8692d915d30..680b1294df6 100644 --- a/public/app/plugins/datasource/prometheus/query_ctrl.ts +++ b/public/app/plugins/datasource/prometheus/query_ctrl.ts @@ -20,7 +20,7 @@ class PrometheusQueryCtrl extends QueryCtrl { linkToPrometheus: any; /** @ngInject */ - constructor($scope, $injector, private templateSrv, private uiSegmentSrv) { + constructor($scope, $injector, private templateSrv, private uiSegmentSrv, private $rootScope) { super($scope, $injector); this.query = new PromQuery(this.target, templateSrv); @@ -58,6 +58,39 @@ class PrometheusQueryCtrl extends QueryCtrl { this.panelCtrl.refresh(); } + getPartOptions(part) { + if (part.def.type === 'by') { + return this.datasource.metricFindQuery('labels(' + this.target.metric + ')') + .then(this.transformToSegments(true)) + .catch(this.handleQueryError.bind(true)); + } + } + + partUpdated(part) { + this.target.expr = this.query.render(); + this.panelCtrl.refresh(); + } + + handleQueryError(err) { + this.$rootScope.appEvent('alert-error', ['Query failed', err.message]); + } + + transformToSegments(addTemplateVars) { + return (results) => { + var segments = _.map(results, segment => { + return this.uiSegmentSrv.newSegment({ value: segment.text, expandable: segment.expandable }); + }); + + if (addTemplateVars) { + for (let variable of this.templateSrv.variables) { + segments.unshift(this.uiSegmentSrv.newSegment({ type: 'template', value: '/^$' + variable.name + '$/', expandable: true })); + } + } + + return segments; + }; + } + getMetricOptions() { return this.datasource.performSuggestQuery('').then(res => { return _.map(res, metric => { @@ -68,6 +101,13 @@ class PrometheusQueryCtrl extends QueryCtrl { queryChanged() { this.target.metric = this.metricSegment.value; + this.target.expr = this.query.render(); + this.refresh(); + } + + toggleEditorMode() { + this.target.expr = this.query.render(false); + this.target.editorMode = !this.target.editorMode; } refreshMetricData() { diff --git a/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts b/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts index 38b50d2ce79..b393f36402c 100644 --- a/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts +++ b/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts @@ -22,18 +22,22 @@ describe('PrometheusMetricFindQuery', function() { describe('When performing metricFindQuery', function() { var results; var response; + it('label_values(resource) should generate label search query', function() { response = { status: "success", data: ["value1", "value2", "value3"] }; + ctx.$httpBackend.expect('GET', 'proxied/api/v1/label/resource/values').respond(response); var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(resource)', ctx.timeSrv); pm.process().then(function(data) { results = data; }); ctx.$httpBackend.flush(); ctx.$rootScope.$apply(); + expect(results.length).to.be(3); }); + it('label_values(metric, resource) should generate series query', function() { response = { status: "success", @@ -43,13 +47,16 @@ describe('PrometheusMetricFindQuery', function() { {__name__: "metric", resource: "value3"} ] }; + ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=.*&end=.*/).respond(response); var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)', ctx.timeSrv); pm.process().then(function(data) { results = data; }); ctx.$httpBackend.flush(); ctx.$rootScope.$apply(); + expect(results.length).to.be(3); }); + it('label_values(metric, resource) should pass correct time', function() { ctx.timeSrv.setTime({ from: moment.utc('2011-01-01'), to: moment.utc('2015-01-01') }); ctx.$httpBackend.expect('GET', @@ -59,6 +66,7 @@ describe('PrometheusMetricFindQuery', function() { ctx.$httpBackend.flush(); ctx.$rootScope.$apply(); }); + it('label_values(metric{label1="foo", label2="bar", label3="baz"}, resource) should generate series query', function() { response = { status: "success", @@ -68,6 +76,7 @@ describe('PrometheusMetricFindQuery', function() { {__name__: "metric", resource: "value3"} ] }; + ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=.*&end=.*/).respond(response); var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)', ctx.timeSrv); pm.process().then(function(data) { results = data; }); @@ -75,18 +84,22 @@ describe('PrometheusMetricFindQuery', function() { ctx.$rootScope.$apply(); expect(results.length).to.be(3); }); + it('metrics(metric.*) should generate metric name query', function() { response = { status: "success", data: ["metric1","metric2","metric3","nomatch"] }; + ctx.$httpBackend.expect('GET', 'proxied/api/v1/label/__name__/values').respond(response); var pm = new PrometheusMetricFindQuery(ctx.ds, 'metrics(metric.*)', ctx.timeSrv); pm.process().then(function(data) { results = data; }); ctx.$httpBackend.flush(); ctx.$rootScope.$apply(); + expect(results.length).to.be(3); }); + it('query_result(metric) should generate metric name query', function() { response = { status: "success", @@ -98,6 +111,7 @@ describe('PrometheusMetricFindQuery', function() { }] } }; + ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/query\?query=metric&time=.*/).respond(response); var pm = new PrometheusMetricFindQuery(ctx.ds, 'query_result(metric)', ctx.timeSrv); pm.process().then(function(data) { results = data; }); @@ -106,5 +120,28 @@ describe('PrometheusMetricFindQuery', function() { expect(results.length).to.be(1); expect(results[0].text).to.be('metric{job="testjob"} 3846 1443454528000'); }); + + it('labels(metric) should generate series query', function() { + response = { + status: "success", + data: [ + {__name__: "metric", resource: "value1", app: "app1"}, + {__name__: "metric", resource: "value2", app: "app2"}, + {__name__: "metric", resource: "value3", server: "server1"} + ] + }; + + ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=.*&end=.*/).respond(response); + var pm = new PrometheusMetricFindQuery(ctx.ds, 'labels(metric)', ctx.timeSrv); + pm.process().then(function(data) { results = data; }); + ctx.$httpBackend.flush(); + ctx.$rootScope.$apply(); + + expect(results.length).to.be(3); + expect(results[0].text).to.be('resource'); + expect(results[1].text).to.be('app'); + expect(results[2].text).to.be('server'); + }); + }); }); diff --git a/public/app/plugins/datasource/prometheus/specs/query_specs.ts b/public/app/plugins/datasource/prometheus/specs/query_specs.ts index 5bb164c306e..d58e0834f89 100644 --- a/public/app/plugins/datasource/prometheus/specs/query_specs.ts +++ b/public/app/plugins/datasource/prometheus/specs/query_specs.ts @@ -2,7 +2,7 @@ import {describe, beforeEach, it, sinon, expect} from 'test/lib/common'; import {PromQuery} from '../prom_query'; -describe.only('PromQuery', function() { +describe('PromQuery', function() { var templateSrv = {replace: val => val}; describe('render series with mesurement only', function() { @@ -20,4 +20,19 @@ describe.only('PromQuery', function() { }); }); + describe('render series with group by label', function() { + it('should generate correct query', function() { + var query = new PromQuery({ + metric: 'cpu', + functions: [ + {type: 'sum', params: []}, + {type: 'by', params: ['app']}, + ] + }, templateSrv, {}); + + var queryText = query.render(); + expect(queryText).to.be('sum(cpu) by(app)'); + }); + }); + }); From 3d776851082725a4e3b5780cd35bf888348b892f Mon Sep 17 00:00:00 2001 From: Thibault Chataigner Date: Mon, 6 Jun 2016 13:47:45 +0000 Subject: [PATCH 05/37] Enable the "limit" param in /api/search --- pkg/services/search/handlers.go | 1 + pkg/services/search/models.go | 1 + pkg/services/sqlstore/dashboard.go | 8 +++++++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/services/search/handlers.go b/pkg/services/search/handlers.go index a4905d6fa58..a8344ba05a5 100644 --- a/pkg/services/search/handlers.go +++ b/pkg/services/search/handlers.go @@ -44,6 +44,7 @@ func searchHandler(query *Query) error { IsStarred: query.IsStarred, OrgId: query.OrgId, DashboardIds: query.DashboardIds, + Limit: query.Limit, } if err := bus.Dispatch(&dashQuery); err != nil { diff --git a/pkg/services/search/models.go b/pkg/services/search/models.go index 159637013f5..ada3e1ccdfa 100644 --- a/pkg/services/search/models.go +++ b/pkg/services/search/models.go @@ -42,6 +42,7 @@ type FindPersistedDashboardsQuery struct { UserId int64 IsStarred bool DashboardIds []int + Limit int Result HitList } diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index ef36fd75ab6..d71eaeefa6e 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -123,6 +123,11 @@ type DashboardSearchProjection struct { } func SearchDashboards(query *search.FindPersistedDashboardsQuery) error { + limit := query.Limit + if limit == 0 { + limit = 1000 + } + var sql bytes.Buffer params := make([]interface{}, 0) @@ -165,7 +170,8 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error { params = append(params, "%"+query.Title+"%") } - sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT 1000")) + sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT ?")) + params = append(params, limit) var res []DashboardSearchProjection From ffac5c12999eca55dbbed36a24cc8f6588dff8fa Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 8 Jun 2016 09:35:34 +0200 Subject: [PATCH 06/37] docs(changelog): add note about changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b25f025d1d..eb1cb5c55ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * **InfluxDB**: Add spread function, closes [#5211](https://github.com/grafana/grafana/issues/5211) * **Scripts**: Use restart instead of start for deb package script, closes [#5282](https://github.com/grafana/grafana/pull/5282) * **Logging**: Moved to structured logging lib, and moved to component specific level filters via config file, closes [#4590](https://github.com/grafana/grafana/issues/4590) +* **Search**: Add search limit query parameter, closes [#5292](https://github.com/grafana/grafana/pull/5292) ## Breaking changes * **Logging** : Changed default logging output format (now structured into message, and key value pairs, with logger key acting as component). You can also no change in config to json log ouput. From 755ec552dea491e23cb2d2fc778da32234405b9d Mon Sep 17 00:00:00 2001 From: Mukesh Date: Thu, 9 Jun 2016 04:13:38 +0530 Subject: [PATCH 07/37] Bugfix: org select btn was not working on profile (#5317) --- public/app/features/org/partials/profile.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/org/partials/profile.html b/public/app/features/org/partials/profile.html index 811891ae317..02fa2878a17 100644 --- a/public/app/features/org/partials/profile.html +++ b/public/app/features/org/partials/profile.html @@ -51,7 +51,7 @@ Current - + Select From ced3bafa99fb81687428905b60521f2971ace51e Mon Sep 17 00:00:00 2001 From: Mark Baas Date: Thu, 9 Jun 2016 02:13:31 -0400 Subject: [PATCH 08/37] returns correct index (#5275) --- public/app/plugins/datasource/opentsdb/datasource.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/public/app/plugins/datasource/opentsdb/datasource.js b/public/app/plugins/datasource/opentsdb/datasource.js index b683aa02068..1d3db75658c 100644 --- a/public/app/plugins/datasource/opentsdb/datasource.js +++ b/public/app/plugins/datasource/opentsdb/datasource.js @@ -403,10 +403,7 @@ function (angular, _, dateMath) { } else { return _.findIndex(options.targets, function(target) { if (target.filters && target.filters.length > 0) { - return target.metric === metricData.metric && - _.all(target.filters, function(filter) { - return filter.tagk === interpolatedTagValue === "*"; - }); + return target.metric === metricData.metric; } else { return target.metric === metricData.metric && _.all(target.tags, function(tagV, tagK) { From 071c16b73c702c65695119013c61a99961cba2c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 9 Jun 2016 10:13:34 +0200 Subject: [PATCH 09/37] feat(alerting): start on new ui --- .floo | 5 +- alerting_model.json | 145 ++++++++++++++++++ .../app/plugins/panel/graph/alert_tab_ctrl.ts | 13 +- .../panel/graph/partials/tab_alerting.html | 94 ++++++------ 4 files changed, 201 insertions(+), 56 deletions(-) create mode 100644 alerting_model.json diff --git a/.floo b/.floo index a8fb87025ec..1201c5e93b9 100644 --- a/.floo +++ b/.floo @@ -1,4 +1,3 @@ { - "url": "https://floobits.com/raintank/grafana" -} - + "url": "https://floobits.com/raintank/grafana" +} \ No newline at end of file diff --git a/alerting_model.json b/alerting_model.json new file mode 100644 index 00000000000..afe237ecd4a --- /dev/null +++ b/alerting_model.json @@ -0,0 +1,145 @@ +{ + "alert": { + "name": "Majority servers down", + "frequency": 60, + "notify": ["group1", "group2"], + "expressions": [ + { + "left": [ + { + "type": "query", + "refId": "A", + "timeRange": {"from": "5m", "to": "now-1m"}, + }, + { + "type": "function", + "name": "max" + } + ], + "operator": ">", + "right": [ + { + "type": "constant", + "value": 100 + } + ], + "level": 2, + } + ] + }, + + "alert": { + "name": "Majority servers down take2", + "frequency": 60, + "notify": ["group1", "group2"], + "expressions": [ + { + "left": [ + { + "type": "query", + "refId": "A", + "timeRange": {"from": "5m", "to": "now-1m"}, + }, + { + "type": "function", + "name": "max" + } + ], + "operator": ">", + "right": [ + { + "type": "query", + "refId": "A", + "timeRange": {"from": "now-1d-5m", "to": "now-1d"}, + }, + { + "type": "function", + "name": "max" + } + ], + "level": 2, + } + ] + }, + "alert": { + "name": "CPU usage last 5min above 90%", + "frequency": 60, + "expressions": [ + { + "expr": "query(#A, 5m, now, avg)", + "operator": ">", + "critLevel": 90, + } + ] + }, + "alert": { + "name": "Series count above 10", + "frequency": "1m", + "expressions": [ + { + "expr": "query(#A, 5m, now, avg) | countSeries()", + "operator": ">", + "critLevel": 10, + } + ] + }, + "alert": { + "name": "Disk Free Zero in 3 days", + "frequency": "1d", + "expressions": [ + { + "expr": "query(#A, 1d, now, trend(3d))", + "operator": ">", + "critLevel": 0, + } + ] + }, + "alert": { + "name": "Server requests is zero for more than 10min", + "frequency": "1d", + "expressions": [ + { + "expr": "query(#A, 10m, now, sum)", + "operator": "=", + "critLevel": 0, + } + ] + }, + "alert": { + "name": "Timeouts should not be more than 0.1% of requests", + "frequency": "1d", + "expressions": [ + { + "expr": "query(#A, 10m, now, sum) | subtract | query(#B, 10m, now, sum)", + "operator": ">", + "critLevel": 0, + } + ] + }, + "alert": { + "name": "CPU usage last 5min changed by more than 20% compared to last 24hours", + "frequency": "1m", + "value": "query(#A, 5m, now, avg)", + "operator": "percent change", + "threshold": "query(#A, 1d, now, avg)", + }, + + "alert": { + "name": "CPU higher than 90%", + "frequency": "1m", + "valueExpr": "query(#A, 5m, now, avg)", + "evalType": "greater than", + "critLevel": 20, + "warnLevel": 10, + }, + + "alert": { + "name": "CPU usage last 5min changed by more than 20% compared to last 24hours", + "frequency": "1m", + "valueExpr": "query(#A, 5m, now, avg)", + "evalType": "percent change", + "evalExpr": "query(#A, 1d, now, avg)", + "critLevel": 20, + "warnLevel": 10, + }, +} diff --git a/public/app/plugins/panel/graph/alert_tab_ctrl.ts b/public/app/plugins/panel/graph/alert_tab_ctrl.ts index 11aebe7e4e7..40160981904 100644 --- a/public/app/plugins/panel/graph/alert_tab_ctrl.ts +++ b/public/app/plugins/panel/graph/alert_tab_ctrl.ts @@ -9,8 +9,9 @@ export class AlertTabCtrl { panelCtrl: any; alerting: any; metricTargets = [{ refId: '- select query -' } ]; - operators = ['>', '<', '<=', '>=']; + evalFuncs = ['Greater Then', 'Percent Change']; aggregators = ['avg', 'sum', 'min', 'max', 'median']; + rule: any; defaultValues = { aggregator: 'avg', @@ -18,16 +19,22 @@ export class AlertTabCtrl { queryRange: 3600, warnOperator: '>', critOperator: '>', - queryRef: '- select query -' + queryRef: '- select query -', + valueExpr: 'query(#A, 5m, now, avg)', + evalFunc: 'Greater Then', + evalExpr: '', + critLevel: 20, + warnLevel: 10, }; /** @ngInject */ constructor($scope, private $timeout) { - $scope.alertTab = this; //HACK ATTACK! this.panelCtrl = $scope.ctrl; this.panel = this.panelCtrl.panel; + $scope.ctrl = this; _.defaults(this.panel.alerting, this.defaultValues); + this.rule = this.panel.alerting; var defaultName = (this.panelCtrl.dashboard.title + ' ' + this.panel.title + ' alert'); this.panel.alerting.name = this.panel.alerting.name || defaultName; diff --git a/public/app/plugins/panel/graph/partials/tab_alerting.html b/public/app/plugins/panel/graph/partials/tab_alerting.html index aeafebf2f83..1d2f004bc62 100644 --- a/public/app/plugins/panel/graph/partials/tab_alerting.html +++ b/public/app/plugins/panel/graph/partials/tab_alerting.html @@ -1,61 +1,55 @@
-
Query
-
- Query to watch -
- +
Alert Rule
+
+
+ Value +
-
- -
Thresholds
-
- - - Warn level - -
- +
+
+ +
- -
-
- - - Critical level - -
- +
+ + + Warn + +
- -
-
- -
-
Aggregation settings
-
- Aggregation method -
- +
+ + + Critcal + +
-
- -
- Query range (seconds) - -
- -
- Frequency (seconds) -
+
+ + + + + + + + + + + + + + + + + + + + + +
Alert info
From b8aa8b307997dde9f11c28401436cc841f087726 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 9 Jun 2016 10:37:33 +0200 Subject: [PATCH 10/37] feat(prometheus): restore old prometheus query editor, revert this commit in prometheus query editor v2 branch --- .../datasource/prometheus/datasource.ts | 25 ++-- .../prometheus/metric_find_query.js | 128 +++++++----------- .../prometheus/partials/query.editor.html | 39 ++---- .../datasource/prometheus/prom_query.ts | 119 ---------------- .../datasource/prometheus/query_ctrl.ts | 102 +++----------- .../specs/metric_find_query_specs.ts | 37 ----- .../prometheus/specs/query_specs.ts | 38 ------ 7 files changed, 87 insertions(+), 401 deletions(-) delete mode 100644 public/app/plugins/datasource/prometheus/prom_query.ts delete mode 100644 public/app/plugins/datasource/prometheus/specs/query_specs.ts diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index f1c58f6a61d..cbd7b1ba24b 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -256,23 +256,14 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS return this.renderTemplate(options.legendFormat, labelData) || '{}'; }; - this.renderTemplate = function(format, data) { - var originalSettings = _.templateSettings; - _.templateSettings = { - interpolate: /\{\{(.+?)\}\}/g - }; - - var template = _.template(templateSrv.replace(format)); - var result; - try { - result = template(data); - } catch (e) { - result = null; - } - - _.templateSettings = originalSettings; - - return result; + this.renderTemplate = function(aliasPattern, aliasData) { + var aliasRegex = /\{\{\s*(.+?)\s*\}\}/g; + return aliasPattern.replace(aliasRegex, function(match, g1) { + if (aliasData[g1]) { + return aliasData[g1]; + } + return g1; + }); }; this.getOriginalMetricName = function(labelData) { diff --git a/public/app/plugins/datasource/prometheus/metric_find_query.js b/public/app/plugins/datasource/prometheus/metric_find_query.js index d3d5376bc19..f449978be81 100644 --- a/public/app/plugins/datasource/prometheus/metric_find_query.js +++ b/public/app/plugins/datasource/prometheus/metric_find_query.js @@ -11,33 +11,27 @@ function (_) { } PrometheusMetricFindQuery.prototype.process = function() { - var labelValuesRegex = /^label_values\((?:(.+),\s*)?([a-zA-Z_][a-zA-Z0-9_]+)\)$/; - var metricNamesRegex = /^metrics\((.+)\)$/; - var labelsRegex = /^labels\((.+)\)$/; - var queryResultRegex = /^query_result\((.+)\)$/; + var label_values_regex = /^label_values\((?:(.+),\s*)?([a-zA-Z_][a-zA-Z0-9_]+)\)$/; + var metric_names_regex = /^metrics\((.+)\)$/; + var query_result_regex = /^query_result\((.+)\)$/; - var labelsQuery = this.query.match(labelsRegex); - if (labelsQuery) { - return this.labelsQuery(labelsQuery[1]); - } - - var labelValuesQuery = this.query.match(labelValuesRegex); - if (labelValuesQuery) { - if (labelValuesQuery[1]) { - return this.labelValuesQuery(labelValuesQuery[2], labelValuesQuery[1]); + var label_values_query = this.query.match(label_values_regex); + if (label_values_query) { + if (label_values_query[1]) { + return this.labelValuesQuery(label_values_query[2], label_values_query[1]); } else { - return this.labelValuesQuery(labelValuesQuery[2], null); + return this.labelValuesQuery(label_values_query[2], null); } } - var metricNamesQuery = this.query.match(metricNamesRegex); - if (metricNamesQuery) { - return this.metricNameQuery(metricNamesQuery[1]); + var metric_names_query = this.query.match(metric_names_regex); + if (metric_names_query) { + return this.metricNameQuery(metric_names_query[1]); } - var queryResultQuery = this.query.match(queryResultRegex); - if (queryResultQuery) { - return this.queryResultQuery(queryResultQuery[1]); + var query_result_query = this.query.match(query_result_regex); + if (query_result_query) { + return this.queryResultQuery(query_result_query[1]); } // if query contains full metric name, return metric name and label list @@ -73,71 +67,45 @@ function (_) { } }; - PrometheusMetricFindQuery.prototype.labelsQuery = function(metric) { - var url; - - url = '/api/v1/series?match[]=' + encodeURIComponent(metric) - + '&start=' + (this.range.from.valueOf() / 1000) - + '&end=' + (this.range.to.valueOf() / 1000); - - return this.datasource._request('GET', url) - .then(function(result) { - var tags = {}; - _.each(result.data.data, function(metric) { - _.each(metric, function(value, key) { - if (key === "__name__") { - return; - } - - tags[key] = key; - }); - }); - - return _.map(tags, function(value) { - return {text: value, value: value}; - }); - }); - }; - PrometheusMetricFindQuery.prototype.metricNameQuery = function(metricFilterPattern) { var url = '/api/v1/label/__name__/values'; return this.datasource._request('GET', url) - .then(function(result) { - return _.chain(result.data.data) - .filter(function(metricName) { - var r = new RegExp(metricFilterPattern); - return r.test(metricName); - }) - .map(function(matchedMetricName) { - return { - text: matchedMetricName, - expandable: true - }; - }) - .value(); - }); + .then(function(result) { + return _.chain(result.data.data) + .filter(function(metricName) { + var r = new RegExp(metricFilterPattern); + return r.test(metricName); + }) + .map(function(matchedMetricName) { + return { + text: matchedMetricName, + expandable: true + }; + }) + .value(); + }); }; PrometheusMetricFindQuery.prototype.queryResultQuery = function(query) { var url = '/api/v1/query?query=' + encodeURIComponent(query) + '&time=' + (this.range.to.valueOf() / 1000); return this.datasource._request('GET', url) - .then(function(result) { - return _.map(result.data.data.result, function(metricData) { - var text = metricData.metric.__name__ || ''; - delete metricData.metric.__name__; - text += '{' + - _.map(metricData.metric, function(v, k) { return k + '="' + v + '"'; }).join(',') + - '}'; - text += ' ' + metricData.value[1] + ' ' + metricData.value[0] * 1000; + .then(function(result) { + return _.map(result.data.data.result, function(metricData) { + var text = metricData.metric.__name__ || ''; + delete metricData.metric.__name__; + text += '{' + + _.map(metricData.metric, function(v, k) { return k + '="' + v + '"'; }).join(',') + + '}'; + text += ' ' + metricData.value[1] + ' ' + metricData.value[0] * 1000; - return { - text: text, - expandable: true - }; - }); + return { + text: text, + expandable: true + }; }); + }); }; PrometheusMetricFindQuery.prototype.metricNameAndLabelsQuery = function(query) { @@ -147,14 +115,14 @@ function (_) { var self = this; return this.datasource._request('GET', url) - .then(function(result) { - return _.map(result.data.data, function(metric) { - return { - text: self.datasource.getOriginalMetricName(metric), - expandable: true - }; - }); + .then(function(result) { + return _.map(result.data.data, function(metric) { + return { + text: self.datasource.getOriginalMetricName(metric), + expandable: true + }; }); + }); }; return PrometheusMetricFindQuery; diff --git a/public/app/plugins/datasource/prometheus/partials/query.editor.html b/public/app/plugins/datasource/prometheus/partials/query.editor.html index 359c366cb0f..37c27ace58a 100644 --- a/public/app/plugins/datasource/prometheus/partials/query.editor.html +++ b/public/app/plugins/datasource/prometheus/partials/query.editor.html @@ -1,39 +1,18 @@ - - -
- -
- -
-
- - -
- -
- - -
- -
- -
- + +
-
+ + +
+
+ +
- + diff --git a/public/app/plugins/datasource/prometheus/prom_query.ts b/public/app/plugins/datasource/prometheus/prom_query.ts deleted file mode 100644 index d73a8b1cbcf..00000000000 --- a/public/app/plugins/datasource/prometheus/prom_query.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { - QueryPartDef, - QueryPart, - functionRenderer, - identityRenderer, - quotedIdentityRenderer, -} from 'app/core/components/query_part/query_part'; - -import _ from 'lodash'; - -var index = []; -var categories = { - Functions: [], - GroupBy: [], -}; - -export class PromQuery { - target: any; - metric: string; - range: string; - filters: any[]; - functions: any[]; - templateSrv: any; - scopedVars: any; - - constructor(target, templateSrv?, scopedVars?) { - this.target = target; - - this.target.expr = this.target.expr || ''; - this.target.intervalFactor = this.target.intervalFactor || 2; - this.target.functions = this.target.functions || []; - this.target.editorMode = this.target.editorMode || true; - - this.templateSrv = templateSrv; - this.scopedVars = scopedVars; - - this.updateProjection(); - } - - updateProjection() { - this.functions = _.map(this.target.functions, function(func: any) { - return createPart(func); - }); - } - - render() { - var query = this.target.metric; - if (this.target.range) { - query += '[' + this.target.range + ']'; - } - - for (let func of this.functions) { - query = func.render(query); - } - - return query; - } - - addQueryPart(category, item) { - var partModel = createPart({type: item.text}); - partModel.def.addStrategy(this, partModel); - } -} - -export function createPart(part): any { - var def = index[part.type]; - if (!def) { - throw {message: 'Could not find query part ' + part.type}; - } - - return new QueryPart(part, def); -} - -function register(options: any) { - index[options.type] = new QueryPartDef(options); - options.category.push(index[options.type]); -} - -function addFunctionStrategy(model, partModel) { - model.functions.push(partModel); - model.target.functions.push(partModel.part); -} - -function groupByLabelRenderer(part, innerExpr) { - return innerExpr + ' by(' + part.params.join(',') + ')'; -} - -register({ - type: 'rate', - addStrategy: addFunctionStrategy, - category: categories.Functions, - params: [], - defaultParams: [], - renderer: functionRenderer, -}); - -register({ - type: 'sum', - addStrategy: addFunctionStrategy, - category: categories.Functions, - params: [], - defaultParams: [], - renderer: functionRenderer, -}); - -register({ - type: 'by', - addStrategy: addFunctionStrategy, - category: categories.Functions, - params: [ - {name: "label", type: "string", dynamicLookup: true} - ], - defaultParams: [], - renderer: groupByLabelRenderer, -}); - -export function getQueryPartCategories() { - return categories; -} diff --git a/public/app/plugins/datasource/prometheus/query_ctrl.ts b/public/app/plugins/datasource/prometheus/query_ctrl.ts index 680b1294df6..92cec328d87 100644 --- a/public/app/plugins/datasource/prometheus/query_ctrl.ts +++ b/public/app/plugins/datasource/prometheus/query_ctrl.ts @@ -6,108 +6,46 @@ import moment from 'moment'; import * as dateMath from 'app/core/utils/datemath'; import {QueryCtrl} from 'app/plugins/sdk'; -import {PromQuery, getQueryPartCategories} from './prom_query'; class PrometheusQueryCtrl extends QueryCtrl { static templateUrl = 'partials/query.editor.html'; - query: any; - metricSegment: any; - addQueryPartMenu: any[]; + metric: any; resolutions: any; oldTarget: any; suggestMetrics: any; linkToPrometheus: any; /** @ngInject */ - constructor($scope, $injector, private templateSrv, private uiSegmentSrv, private $rootScope) { + constructor($scope, $injector, private templateSrv) { super($scope, $injector); - this.query = new PromQuery(this.target, templateSrv); - - if (this.target.metric) { - this.metricSegment = uiSegmentSrv.newSegment(this.target.metric); - } else { - this.metricSegment = uiSegmentSrv.newSegment({value: 'select metric', fake: true}); - } + var target = this.target; + target.expr = target.expr || ''; + target.intervalFactor = target.intervalFactor || 2; + this.metric = ''; this.resolutions = _.map([1,2,3,4,5,10], function(f) { return {factor: f, label: '1/' + f}; }); - this.updateLink(); - this.buildQueryPartMenu(); - } + $scope.$on('typeahead-updated', () => { + this.$scope.$apply(() => { - buildQueryPartMenu() { - var categories = getQueryPartCategories(); - this.addQueryPartMenu = _.reduce(categories, function(memo, cat, key) { - var menu = { - text: key, - submenu: cat.map(item => { - return {text: item.type, value: item.type}; - }), - }; - memo.push(menu); - return memo; - }, []); - } - - addQueryPart(item, subItem) { - this.query.addQueryPart(item, subItem); - this.panelCtrl.refresh(); - } - - getPartOptions(part) { - if (part.def.type === 'by') { - return this.datasource.metricFindQuery('labels(' + this.target.metric + ')') - .then(this.transformToSegments(true)) - .catch(this.handleQueryError.bind(true)); - } - } - - partUpdated(part) { - this.target.expr = this.query.render(); - this.panelCtrl.refresh(); - } - - handleQueryError(err) { - this.$rootScope.appEvent('alert-error', ['Query failed', err.message]); - } - - transformToSegments(addTemplateVars) { - return (results) => { - var segments = _.map(results, segment => { - return this.uiSegmentSrv.newSegment({ value: segment.text, expandable: segment.expandable }); - }); - - if (addTemplateVars) { - for (let variable of this.templateSrv.variables) { - segments.unshift(this.uiSegmentSrv.newSegment({ type: 'template', value: '/^$' + variable.name + '$/', expandable: true })); - } - } - - return segments; - }; - } - - getMetricOptions() { - return this.datasource.performSuggestQuery('').then(res => { - return _.map(res, metric => { - return this.uiSegmentSrv.newSegment(metric); + this.target.expr += this.target.metric; + this.metric = ''; + this.refreshMetricData(); }); }); - } - queryChanged() { - this.target.metric = this.metricSegment.value; - this.target.expr = this.query.render(); - this.refresh(); - } + // called from typeahead so need this + // here in order to ensure this ref + this.suggestMetrics = (query, callback) => { + console.log(this); + this.datasource.performSuggestQuery(query).then(callback); + }; - toggleEditorMode() { - this.target.expr = this.query.render(false); - this.target.editorMode = !this.target.editorMode; + this.updateLink(); } refreshMetricData() { @@ -120,6 +58,10 @@ class PrometheusQueryCtrl extends QueryCtrl { updateLink() { var range = this.panelCtrl.range; + if (!range) { + return; + } + var rangeDiff = Math.ceil((range.to.valueOf() - range.from.valueOf()) / 1000); var endTime = range.to.utc().format('YYYY-MM-DD HH:mm'); var expr = { diff --git a/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts b/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts index b393f36402c..38b50d2ce79 100644 --- a/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts +++ b/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts @@ -22,22 +22,18 @@ describe('PrometheusMetricFindQuery', function() { describe('When performing metricFindQuery', function() { var results; var response; - it('label_values(resource) should generate label search query', function() { response = { status: "success", data: ["value1", "value2", "value3"] }; - ctx.$httpBackend.expect('GET', 'proxied/api/v1/label/resource/values').respond(response); var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(resource)', ctx.timeSrv); pm.process().then(function(data) { results = data; }); ctx.$httpBackend.flush(); ctx.$rootScope.$apply(); - expect(results.length).to.be(3); }); - it('label_values(metric, resource) should generate series query', function() { response = { status: "success", @@ -47,16 +43,13 @@ describe('PrometheusMetricFindQuery', function() { {__name__: "metric", resource: "value3"} ] }; - ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=.*&end=.*/).respond(response); var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)', ctx.timeSrv); pm.process().then(function(data) { results = data; }); ctx.$httpBackend.flush(); ctx.$rootScope.$apply(); - expect(results.length).to.be(3); }); - it('label_values(metric, resource) should pass correct time', function() { ctx.timeSrv.setTime({ from: moment.utc('2011-01-01'), to: moment.utc('2015-01-01') }); ctx.$httpBackend.expect('GET', @@ -66,7 +59,6 @@ describe('PrometheusMetricFindQuery', function() { ctx.$httpBackend.flush(); ctx.$rootScope.$apply(); }); - it('label_values(metric{label1="foo", label2="bar", label3="baz"}, resource) should generate series query', function() { response = { status: "success", @@ -76,7 +68,6 @@ describe('PrometheusMetricFindQuery', function() { {__name__: "metric", resource: "value3"} ] }; - ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=.*&end=.*/).respond(response); var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)', ctx.timeSrv); pm.process().then(function(data) { results = data; }); @@ -84,22 +75,18 @@ describe('PrometheusMetricFindQuery', function() { ctx.$rootScope.$apply(); expect(results.length).to.be(3); }); - it('metrics(metric.*) should generate metric name query', function() { response = { status: "success", data: ["metric1","metric2","metric3","nomatch"] }; - ctx.$httpBackend.expect('GET', 'proxied/api/v1/label/__name__/values').respond(response); var pm = new PrometheusMetricFindQuery(ctx.ds, 'metrics(metric.*)', ctx.timeSrv); pm.process().then(function(data) { results = data; }); ctx.$httpBackend.flush(); ctx.$rootScope.$apply(); - expect(results.length).to.be(3); }); - it('query_result(metric) should generate metric name query', function() { response = { status: "success", @@ -111,7 +98,6 @@ describe('PrometheusMetricFindQuery', function() { }] } }; - ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/query\?query=metric&time=.*/).respond(response); var pm = new PrometheusMetricFindQuery(ctx.ds, 'query_result(metric)', ctx.timeSrv); pm.process().then(function(data) { results = data; }); @@ -120,28 +106,5 @@ describe('PrometheusMetricFindQuery', function() { expect(results.length).to.be(1); expect(results[0].text).to.be('metric{job="testjob"} 3846 1443454528000'); }); - - it('labels(metric) should generate series query', function() { - response = { - status: "success", - data: [ - {__name__: "metric", resource: "value1", app: "app1"}, - {__name__: "metric", resource: "value2", app: "app2"}, - {__name__: "metric", resource: "value3", server: "server1"} - ] - }; - - ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=.*&end=.*/).respond(response); - var pm = new PrometheusMetricFindQuery(ctx.ds, 'labels(metric)', ctx.timeSrv); - pm.process().then(function(data) { results = data; }); - ctx.$httpBackend.flush(); - ctx.$rootScope.$apply(); - - expect(results.length).to.be(3); - expect(results[0].text).to.be('resource'); - expect(results[1].text).to.be('app'); - expect(results[2].text).to.be('server'); - }); - }); }); diff --git a/public/app/plugins/datasource/prometheus/specs/query_specs.ts b/public/app/plugins/datasource/prometheus/specs/query_specs.ts deleted file mode 100644 index d58e0834f89..00000000000 --- a/public/app/plugins/datasource/prometheus/specs/query_specs.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {describe, beforeEach, it, sinon, expect} from 'test/lib/common'; - -import {PromQuery} from '../prom_query'; - -describe('PromQuery', function() { - var templateSrv = {replace: val => val}; - - describe('render series with mesurement only', function() { - it('should generate correct query', function() { - var query = new PromQuery({ - metric: 'cpu', - range: '5m', - functions: [ - {type: 'rate', params: []} - ] - }, templateSrv, {}); - - var queryText = query.render(); - expect(queryText).to.be('rate(cpu[5m])'); - }); - }); - - describe('render series with group by label', function() { - it('should generate correct query', function() { - var query = new PromQuery({ - metric: 'cpu', - functions: [ - {type: 'sum', params: []}, - {type: 'by', params: ['app']}, - ] - }, templateSrv, {}); - - var queryText = query.render(); - expect(queryText).to.be('sum(cpu) by(app)'); - }); - }); - -}); From 55af988e0295d4b55e7594ce8c85c6202e68d479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 9 Jun 2016 12:14:50 +0200 Subject: [PATCH 11/37] feat(alerting): mocking with new alert rule model --- alerting_model.json | 28 +++++++- .../app/plugins/panel/graph/alert_tab_ctrl.ts | 69 ++++++++++++++++--- .../panel/graph/partials/tab_alerting.html | 56 ++++++++++++--- 3 files changed, 130 insertions(+), 23 deletions(-) diff --git a/alerting_model.json b/alerting_model.json index afe237ecd4a..20c6cfb2fcd 100644 --- a/alerting_model.json +++ b/alerting_model.json @@ -136,10 +136,32 @@ "alert": { "name": "CPU usage last 5min changed by more than 20% compared to last 24hours", "frequency": "1m", - "valueExpr": "query(#A, 5m, now, avg)", - "evalType": "percent change", + "expr": "query(#A, 5m, now, avg) percentGreaterThan()", + "evalType": "percentscre change", "evalExpr": "query(#A, 1d, now, avg)", "critLevel": 20, - "warnLevel": 10, + "warnLevel": 10, + }, + "alert": { + "name": "CPU usage last 5min changed by more than 20% compared to last 24hours", + "frequency": "1m", + "valueQuery": "query(#A, 5m, now, avg) ", + "evalType": "simple", "// other options are: percent change, trend" + "evalQuery": "query(#A, 1d, now, avg)", + "comparison": "greater than", + "critLevel": 20, + "warnLevel": 10, + }, + "alert": { + "name": "CPU usage last 5min changed by more than 20% compared to last 24hours", + "frequency": "1m", + "valueQuery": "query(#A, 5m, now, avg) | Evaluate Against: Static Threshold | >200 Warn | >300 Critical", + "valueQuery": "query(#A, 5m, now, avg) | Evaluate Against: Percent Change Compared To | query(#B, 5m, now, avg) | >200 Warn | >300 Critical", + "valueQuery": "query(#A, 5m, now, trend) | Evaluate Against: Forcast | 7days | >200 Warn | >300 Critical", + "evalType": "simple", "// other options are: percent change, trend" + "evalQuery": "query(#A, 1d, now, avg)", + "comparison": "greater than", + "critLevel": 20, + "warnLevel": 10, }, } diff --git a/public/app/plugins/panel/graph/alert_tab_ctrl.ts b/public/app/plugins/panel/graph/alert_tab_ctrl.ts index 40160981904..78fd310c9c9 100644 --- a/public/app/plugins/panel/graph/alert_tab_ctrl.ts +++ b/public/app/plugins/panel/graph/alert_tab_ctrl.ts @@ -1,30 +1,72 @@ -/// + /// import _ from 'lodash'; import $ from 'jquery'; import angular from 'angular'; +import { + QueryPartDef, + QueryPart, +} from 'app/core/components/query_part/query_part'; + +var alertQueryDef = new QueryPartDef({ + type: 'query', + params: [ + {name: "queryRefId", type: 'string', options: ['#A', '#B', '#C', '#D']}, + {name: "from", type: "string", options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h']}, + {name: "to", type: "string", options: ['now']}, + {name: "aggregation", type: "select", options: ['sum', 'avg', 'min', 'max', 'last']}, + ], + defaultParams: ['#A', '5m', 'now', 'avg'] +}); + export class AlertTabCtrl { panel: any; panelCtrl: any; alerting: any; metricTargets = [{ refId: '- select query -' } ]; - evalFuncs = ['Greater Then', 'Percent Change']; + evalFuncs = [ + { + text: 'Static Threshold', + value: 'static', + }, + { + text: 'Percent Change Compared To', + value: 'percent_change', + secondParam: "query", + }, + { + text: 'Forcast', + value: 'forcast', + secondParam: "duration", + } + ]; aggregators = ['avg', 'sum', 'min', 'max', 'median']; rule: any; + valueQuery: any; + evalQuery: any; + secondParam: any; defaultValues = { - aggregator: 'avg', frequency: 10, - queryRange: 3600, warnOperator: '>', critOperator: '>', - queryRef: '- select query -', - valueExpr: 'query(#A, 5m, now, avg)', - evalFunc: 'Greater Then', - evalExpr: '', + evalFunc: 'static', critLevel: 20, warnLevel: 10, + valueQuery: { + queryRefId: 'A', + from: '5m', + to: 'now', + agg: 'avg', + }, + evalQuery: { + queryRefId: 'A', + from: '5m', + to: 'now', + agg: 'avg', + }, + evalStringParam1: '', }; /** @ngInject */ @@ -36,15 +78,24 @@ export class AlertTabCtrl { _.defaults(this.panel.alerting, this.defaultValues); this.rule = this.panel.alerting; + this.valueQuery = new QueryPart(this.rule.valueQuery, alertQueryDef); + this.evalQuery = new QueryPart(this.rule.evalQuery, alertQueryDef); + var defaultName = (this.panelCtrl.dashboard.title + ' ' + this.panel.title + ' alert'); this.panel.alerting.name = this.panel.alerting.name || defaultName; this.panel.targets.map(target => { this.metricTargets.push(target); }); - this.panel.alerting.queryRef = this.panel.alerting.queryRef || this.metricTargets[0].refId; + this.panel.alerting.queryRef = this.panel.alerting.queryRef || this.metricTargets[0].refId; this.convertThresholdsToAlertThresholds(); + this.evalFuncChanged(); + } + + evalFuncChanged() { + var evalFuncDef = _.findWhere(this.evalFuncs, {value: this.rule.evalFunc}); + this.secondParam = evalFuncDef.secondParam; } convertThresholdsToAlertThresholds() { diff --git a/public/app/plugins/panel/graph/partials/tab_alerting.html b/public/app/plugins/panel/graph/partials/tab_alerting.html index 1d2f004bc62..ce0531b2f12 100644 --- a/public/app/plugins/panel/graph/partials/tab_alerting.html +++ b/public/app/plugins/panel/graph/partials/tab_alerting.html @@ -1,33 +1,67 @@ -
-
+ +
Alert Rule
- Value - + + + +
-
- + Evaluate Against +
+
+
+ + +
+
+ Duration + +
+
+
+ +
+
Levels
+
- Warn + Warn if value - + + > + +
- Critcal + Critcal if value - + + > + +
-
+ From 3898f427f5dee0fca4056f017836d3bc957af662 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 9 Jun 2016 15:10:50 +0200 Subject: [PATCH 12/37] feat(alerting): update the ux model --- public/app/plugins/panel/graph/alert_tab_ctrl.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/panel/graph/alert_tab_ctrl.ts b/public/app/plugins/panel/graph/alert_tab_ctrl.ts index 78fd310c9c9..df2516a15ec 100644 --- a/public/app/plugins/panel/graph/alert_tab_ctrl.ts +++ b/public/app/plugins/panel/graph/alert_tab_ctrl.ts @@ -49,11 +49,9 @@ export class AlertTabCtrl { defaultValues = { frequency: 10, - warnOperator: '>', - critOperator: '>', - evalFunc: 'static', - critLevel: 20, - warnLevel: 10, + warning: { op: '>', level: 10 }, + critical: { op: '>', level: 20 }, + function: 'static', valueQuery: { queryRefId: 'A', from: '5m', @@ -94,7 +92,8 @@ export class AlertTabCtrl { } evalFuncChanged() { - var evalFuncDef = _.findWhere(this.evalFuncs, {value: this.rule.evalFunc}); + var evalFuncDef = _.findWhere(this.evalFuncs, { value: this.rule.expression.evalFunc }); + console.log(evalFuncDef); this.secondParam = evalFuncDef.secondParam; } From fdf051ad5aa1e3dce3d53cb20963466b6a6566e0 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 9 Jun 2016 22:21:28 +0200 Subject: [PATCH 13/37] feat(alerting): begin alert rule storage refactoring --- pkg/api/alerting.go | 7 - pkg/models/alerts.go | 55 ++++---- pkg/models/alerts_test.go | 39 ++++++ pkg/services/alerting/dashboard_parser.go | 110 +++++++++++----- pkg/services/alerting/executor.go | 27 ++-- pkg/services/alerting/executor_test.go | 16 +-- pkg/services/alerting/models.go | 46 ++++--- pkg/services/alerting/rule_reader.go | 13 +- pkg/services/sqlstore/alert_rule.go | 2 +- .../sqlstore/alert_rule_changes_test.go | 19 +-- pkg/services/sqlstore/alert_rule_test.go | 60 +++------ pkg/services/sqlstore/alert_state_test.go | 19 +-- .../sqlstore/dashboard_parser_test.go | 122 +++++++++++------- pkg/services/sqlstore/migrations/alert_mig.go | 11 +- pkg/tsdb/graphite/graphite.go | 8 +- .../app/plugins/panel/graph/alert_tab_ctrl.ts | 3 +- 16 files changed, 300 insertions(+), 257 deletions(-) create mode 100644 pkg/models/alerts_test.go diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 297a4b5fe59..4106dc0eae3 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -64,15 +64,8 @@ func GetAlerts(c *middleware.Context) Response { Id: alert.Id, DashboardId: alert.DashboardId, PanelId: alert.PanelId, - Query: alert.Query, - QueryRefId: alert.QueryRefId, - WarnLevel: alert.WarnLevel, - CritLevel: alert.CritLevel, - Frequency: alert.Frequency, Name: alert.Name, Description: alert.Description, - QueryRange: alert.QueryRange, - Aggregator: alert.Aggregator, State: alert.State, }) } diff --git a/pkg/models/alerts.go b/pkg/models/alerts.go index 030aabea1b9..e069f7e81c3 100644 --- a/pkg/models/alerts.go +++ b/pkg/models/alerts.go @@ -2,49 +2,44 @@ package models import ( "time" + + "github.com/grafana/grafana/pkg/components/simplejson" ) type AlertRule struct { - Id int64 `json:"id"` - OrgId int64 `json:"-"` - DatasourceId int64 `json:"datasourceId"` - DashboardId int64 `json:"dashboardId"` - PanelId int64 `json:"panelId"` - Query string `json:"query"` - QueryRefId string `json:"queryRefId"` - WarnLevel float64 `json:"warnLevel"` - CritLevel float64 `json:"critLevel"` - WarnOperator string `json:"warnOperator"` - CritOperator string `json:"critOperator"` - Frequency int64 `json:"frequency"` - Name string `json:"name"` - Description string `json:"description"` - QueryRange int `json:"queryRange"` - Aggregator string `json:"aggregator"` - State string `json:"state"` + Id int64 + OrgId int64 + DashboardId int64 + PanelId int64 + Name string + Description string + State string - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` + Created time.Time + Updated time.Time + + Expression *simplejson.Json } func (alertRule *AlertRule) ValidToSave() bool { - return alertRule.Query != "" && alertRule.Frequency != 0 && alertRule.QueryRange != 0 && alertRule.Name != "" + return true } -func (this *AlertRule) Equals(other *AlertRule) bool { +func (this *AlertRule) ContainsUpdates(other *AlertRule) bool { result := false - result = result || this.Aggregator != other.Aggregator - result = result || this.CritLevel != other.CritLevel - result = result || this.WarnLevel != other.WarnLevel - result = result || this.WarnOperator != other.WarnOperator - result = result || this.CritOperator != other.CritOperator - result = result || this.Query != other.Query - result = result || this.QueryRefId != other.QueryRefId - result = result || this.Frequency != other.Frequency result = result || this.Name != other.Name result = result || this.Description != other.Description - result = result || this.QueryRange != other.QueryRange + + json1, err1 := this.Expression.MarshalJSON() + json2, err2 := other.Expression.MarshalJSON() + + if err1 != nil || err2 != nil { + return false + } + + result = result || string(json1) != string(json2) + //don't compare .State! That would be insane. return result diff --git a/pkg/models/alerts_test.go b/pkg/models/alerts_test.go new file mode 100644 index 00000000000..33a4937f5ad --- /dev/null +++ b/pkg/models/alerts_test.go @@ -0,0 +1,39 @@ +package models + +import ( + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + . "github.com/smartystreets/goconvey/convey" +) + +func TestAlertingModelTest(t *testing.T) { + Convey("Testing Alerting model", t, func() { + + json1, _ := simplejson.NewJson([]byte(`{ "field": "value" }`)) + json2, _ := simplejson.NewJson([]byte(`{ "field": "value" }`)) + + rule1 := &AlertRule{ + Expression: json1, + Name: "Namn", + Description: "Description", + } + + rule2 := &AlertRule{ + Expression: json2, + Name: "Namn", + Description: "Description", + } + + Convey("Testing AlertRule equals", func() { + + So(rule1.ContainsUpdates(rule2), ShouldBeFalse) + }) + + Convey("Changing the expression should contain update", func() { + json2, _ := simplejson.NewJson([]byte(`{ "field": "newValue" }`)) + rule1.Expression = json2 + So(rule1.ContainsUpdates(rule2), ShouldBeTrue) + }) + }) +} diff --git a/pkg/services/alerting/dashboard_parser.go b/pkg/services/alerting/dashboard_parser.go index 66a719373db..c6df8e53a10 100644 --- a/pkg/services/alerting/dashboard_parser.go +++ b/pkg/services/alerting/dashboard_parser.go @@ -18,54 +18,63 @@ func ParseAlertsFromDashboard(cmd *m.SaveDashboardCommand) []*m.AlertRule { alerting := panel.Get("alerting") alert := &m.AlertRule{ - DashboardId: cmd.Result.Id, - OrgId: cmd.Result.OrgId, - PanelId: panel.Get("id").MustInt64(), - Id: alerting.Get("id").MustInt64(), - QueryRefId: alerting.Get("queryRef").MustString(), - WarnLevel: alerting.Get("warnLevel").MustFloat64(), - CritLevel: alerting.Get("critLevel").MustFloat64(), - WarnOperator: alerting.Get("warnOperator").MustString(), - CritOperator: alerting.Get("critOperator").MustString(), - Frequency: alerting.Get("frequency").MustInt64(), - Name: alerting.Get("name").MustString(), - Description: alerting.Get("description").MustString(), - QueryRange: alerting.Get("queryRange").MustInt(), - Aggregator: alerting.Get("aggregator").MustString(), + DashboardId: cmd.Result.Id, + OrgId: cmd.Result.OrgId, + PanelId: panel.Get("id").MustInt64(), + Id: alerting.Get("id").MustInt64(), + Name: alerting.Get("name").MustString(), + Description: alerting.Get("description").MustString(), } log.Info("Alertrule: %v", alert.Name) + + expression := alerting + valueQuery := expression.Get("valueQuery") + valueQueryRef := valueQuery.Get("queryRefId").MustString() for _, targetsObj := range panel.Get("targets").MustArray() { target := simplejson.NewFromAny(targetsObj) - if target.Get("refId").MustString() == alert.QueryRefId { - targetJson, err := target.MarshalJSON() - if err == nil { - alert.Query = string(targetJson) + if target.Get("refId").MustString() == valueQueryRef { + datsourceName := "" + if target.Get("datasource").MustString() != "" { + datsourceName = target.Get("datasource").MustString() + } else if panel.Get("datasource").MustString() != "" { + datsourceName = panel.Get("datasource").MustString() } - continue - } - } - if panel.Get("datasource").MustString() == "" { - query := &m.GetDataSourcesQuery{OrgId: cmd.OrgId} - if err := bus.Dispatch(query); err == nil { - for _, ds := range query.Result { - if ds.IsDefault { - alert.DatasourceId = ds.Id + if datsourceName == "" { + query := &m.GetDataSourcesQuery{OrgId: cmd.OrgId} + if err := bus.Dispatch(query); err == nil { + for _, ds := range query.Result { + if ds.IsDefault { + valueQuery.Set("datasourceId", ds.Id) + } + } } + } else { + query := &m.GetDataSourceByNameQuery{ + Name: panel.Get("datasource").MustString(), + OrgId: cmd.OrgId, + } + bus.Dispatch(query) + valueQuery.Set("datasourceId", query.Result.Id) + } + + targetQuery := target.Get("target").MustString() + if targetQuery != "" { + valueQuery.Set("query", targetQuery) } } - } else { - query := &m.GetDataSourceByNameQuery{ - Name: panel.Get("datasource").MustString(), - OrgId: cmd.OrgId, - } - bus.Dispatch(query) - alert.DatasourceId = query.Result.Id } - if alert.ValidToSave() { + expression.Set("valueQuery", valueQuery) + alert.Expression = expression + + alertRule := &AlertRule{} + + ParseAlertRulesFromAlertModel(alert, alertRule) + + if alert.ValidToSave() && alertRule.IsValid() { alerts = append(alerts, alert) } } @@ -73,3 +82,34 @@ func ParseAlertsFromDashboard(cmd *m.SaveDashboardCommand) []*m.AlertRule { return alerts } + +func (rule *AlertRule) IsValid() bool { + return rule.ValueQuery.Query != "" +} + +func ParseAlertRulesFromAlertModel(ruleDef *m.AlertRule, model *AlertRule) error { + critical := ruleDef.Expression.Get("critical") + model.Critical = Level{ + Operator: critical.Get("operator").MustString(), + Level: critical.Get("level").MustFloat64(), + } + + warning := ruleDef.Expression.Get("warning") + model.Warning = Level{ + Operator: warning.Get("operator").MustString(), + Level: warning.Get("level").MustFloat64(), + } + + model.Frequency = ruleDef.Expression.Get("frequency").MustInt64() + + valueQuery := ruleDef.Expression.Get("valueQuery") + model.ValueQuery = AlertQuery{ + Query: valueQuery.Get("query").MustString(), + DatasourceId: valueQuery.Get("datasourceId").MustInt64(), + From: valueQuery.Get("From").MustInt64(), + Until: valueQuery.Get("until").MustInt64(), + Aggregator: valueQuery.Get("aggregator").MustString(), + } + + return nil +} diff --git a/pkg/services/alerting/executor.go b/pkg/services/alerting/executor.go index 08a2125bc4f..e676c318e92 100644 --- a/pkg/services/alerting/executor.go +++ b/pkg/services/alerting/executor.go @@ -102,12 +102,12 @@ func (e *ExecutorImpl) Execute(job *AlertJob, resultQueue chan *AlertResult) { func (e *ExecutorImpl) executeQuery(job *AlertJob) (tsdb.TimeSeriesSlice, error) { getDsInfo := &m.GetDataSourceByIdQuery{ - Id: job.Rule.DatasourceId, + Id: 1, OrgId: job.Rule.OrgId, } if err := bus.Dispatch(getDsInfo); err != nil { - return nil, fmt.Errorf("Could not find datasource for %d", job.Rule.DatasourceId) + return nil, fmt.Errorf("Could not find datasource") } req := e.GetRequestForAlertRule(job.Rule, getDsInfo.Result) @@ -130,16 +130,15 @@ func (e *ExecutorImpl) executeQuery(job *AlertJob) (tsdb.TimeSeriesSlice, error) } func (e *ExecutorImpl) GetRequestForAlertRule(rule *AlertRule, datasource *m.DataSource) *tsdb.Request { - req := &tsdb.Request{ TimeRange: tsdb.TimeRange{ - From: "-" + strconv.Itoa(rule.QueryRange) + "s", + From: "-" + strconv.Itoa(int(rule.ValueQuery.From)) + "s", To: "now", }, Queries: []*tsdb.Query{ { - RefId: rule.QueryRefId, - Query: rule.Query, + RefId: "A", + Query: "apps.fakesite.*.counters.requests.count", DataSource: &tsdb.DataSourceInfo{ Id: datasource.Id, Name: datasource.Name, @@ -159,15 +158,15 @@ func (e *ExecutorImpl) evaluateRule(rule *AlertRule, series tsdb.TimeSeriesSlice for _, serie := range series { log.Debug("Evaluating series", "series", serie.Name) - if aggregator[rule.Aggregator] == nil { + if aggregator["avg"] == nil { continue } - var aggValue = aggregator[rule.Aggregator](serie) - var critOperartor = operators[rule.CritOperator] - var critResult = critOperartor(aggValue, rule.CritLevel) + var aggValue = aggregator["avg"](serie) + var critOperartor = operators[rule.Critical.Operator] + var critResult = critOperartor(aggValue, rule.Critical.Level) - log.Trace(resultLogFmt, "Crit", serie.Name, aggValue, rule.CritOperator, rule.CritLevel, critResult) + log.Trace(resultLogFmt, "Crit", serie.Name, aggValue, rule.Critical.Operator, rule.Critical.Level, critResult) if critResult { return &AlertResult{ State: alertstates.Critical, @@ -176,9 +175,9 @@ func (e *ExecutorImpl) evaluateRule(rule *AlertRule, series tsdb.TimeSeriesSlice } } - var warnOperartor = operators[rule.CritOperator] - var warnResult = warnOperartor(aggValue, rule.CritLevel) - log.Trace(resultLogFmt, "Warn", serie.Name, aggValue, rule.WarnOperator, rule.WarnLevel, warnResult) + var warnOperartor = operators[rule.Warning.Operator] + var warnResult = warnOperartor(aggValue, rule.Warning.Level) + log.Trace(resultLogFmt, "Warn", serie.Name, aggValue, rule.Warning.Operator, rule.Warning.Level, warnResult) if warnResult { return &AlertResult{ State: alertstates.Warn, diff --git a/pkg/services/alerting/executor_test.go b/pkg/services/alerting/executor_test.go index 2ccbf18ad74..ccee2b80ba1 100644 --- a/pkg/services/alerting/executor_test.go +++ b/pkg/services/alerting/executor_test.go @@ -14,7 +14,7 @@ func TestAlertingExecutor(t *testing.T) { Convey("single time serie", func() { Convey("Show return ok since avg is above 2", func() { - rule := &AlertRule{CritLevel: 10, CritOperator: ">", Aggregator: "sum"} + rule := &AlertRule{Critical: Level{Level: 10, Operator: ">"}} timeSeries := []*tsdb.TimeSeries{ tsdb.NewTimeSeries("test1", [][2]float64{{2, 0}}), @@ -25,7 +25,7 @@ func TestAlertingExecutor(t *testing.T) { }) Convey("Show return critical since below 2", func() { - rule := &AlertRule{CritLevel: 10, CritOperator: "<", Aggregator: "sum"} + rule := &AlertRule{Critical: Level{Level: 10, Operator: "<"}} timeSeries := []*tsdb.TimeSeries{ tsdb.NewTimeSeries("test1", [][2]float64{{2, 0}}), @@ -36,7 +36,7 @@ func TestAlertingExecutor(t *testing.T) { }) Convey("Show return critical since sum is above 10", func() { - rule := &AlertRule{CritLevel: 10, CritOperator: ">", Aggregator: "sum"} + rule := &AlertRule{Critical: Level{Level: 10, Operator: ">"}} timeSeries := []*tsdb.TimeSeries{ tsdb.NewTimeSeries("test1", [][2]float64{{9, 0}, {9, 0}}), @@ -47,7 +47,7 @@ func TestAlertingExecutor(t *testing.T) { }) Convey("Show return ok since avg is below 10", func() { - rule := &AlertRule{CritLevel: 10, CritOperator: ">", Aggregator: "avg"} + rule := &AlertRule{Critical: Level{Level: 10, Operator: ">"}} timeSeries := []*tsdb.TimeSeries{ tsdb.NewTimeSeries("test1", [][2]float64{{9, 0}, {9, 0}}), @@ -58,7 +58,7 @@ func TestAlertingExecutor(t *testing.T) { }) Convey("Show return ok since min is below 10", func() { - rule := &AlertRule{CritLevel: 10, CritOperator: ">", Aggregator: "min"} + rule := &AlertRule{Critical: Level{Level: 10, Operator: ">"}} timeSeries := []*tsdb.TimeSeries{ tsdb.NewTimeSeries("test1", [][2]float64{{11, 0}, {9, 0}}), @@ -69,7 +69,7 @@ func TestAlertingExecutor(t *testing.T) { }) Convey("Show return ok since max is above 10", func() { - rule := &AlertRule{CritLevel: 10, CritOperator: ">", Aggregator: "max"} + rule := &AlertRule{Critical: Level{Level: 10, Operator: ">"}} timeSeries := []*tsdb.TimeSeries{ tsdb.NewTimeSeries("test1", [][2]float64{{1, 0}, {11, 0}}), @@ -82,7 +82,7 @@ func TestAlertingExecutor(t *testing.T) { Convey("muliple time series", func() { Convey("both are ok", func() { - rule := &AlertRule{CritLevel: 10, CritOperator: ">", Aggregator: "sum"} + rule := &AlertRule{Critical: Level{Level: 10, Operator: ">"}} timeSeries := []*tsdb.TimeSeries{ tsdb.NewTimeSeries("test1", [][2]float64{{2, 0}}), @@ -94,7 +94,7 @@ func TestAlertingExecutor(t *testing.T) { }) Convey("first serie is good, second is critical", func() { - rule := &AlertRule{CritLevel: 10, CritOperator: ">", Aggregator: "sum"} + rule := &AlertRule{Critical: Level{Level: 10, Operator: ">"}} timeSeries := []*tsdb.TimeSeries{ tsdb.NewTimeSeries("test1", [][2]float64{{2, 0}}), diff --git a/pkg/services/alerting/models.go b/pkg/services/alerting/models.go index 7b0fb616a1c..afbefac3658 100644 --- a/pkg/services/alerting/models.go +++ b/pkg/services/alerting/models.go @@ -18,21 +18,37 @@ type AlertResult struct { } type AlertRule struct { - Id int64 - OrgId int64 + Id int64 + OrgId int64 + DashboardId int64 + PanelId int64 + //WarnLevel float64 + //CritLevel float64 + //WarnOperator string + //CritOperator string + Frequency int64 + Name string + Description string + State string + + Warning Level + Critical Level + + ValueQuery AlertQuery + EvalFunc string + EvalQuery AlertQuery + EvalParam string +} + +type Level struct { + Operator string + Level float64 +} + +type AlertQuery struct { + Query string DatasourceId int64 - DashboardId int64 - PanelId int64 - Query string - QueryRefId string - WarnLevel float64 - CritLevel float64 - WarnOperator string - CritOperator string - Frequency int64 - Name string - Description string - QueryRange int Aggregator string - State string + From int64 + Until int64 } diff --git a/pkg/services/alerting/rule_reader.go b/pkg/services/alerting/rule_reader.go index 49b781a01c6..c8c3a73cc55 100644 --- a/pkg/services/alerting/rule_reader.go +++ b/pkg/services/alerting/rule_reader.go @@ -52,19 +52,12 @@ func (arr *AlertRuleReader) Fetch() []*AlertRule { model := &AlertRule{} model.Id = ruleDef.Id model.OrgId = ruleDef.OrgId - model.DatasourceId = ruleDef.DatasourceId - model.Query = ruleDef.Query - model.QueryRefId = ruleDef.QueryRefId - model.WarnLevel = ruleDef.WarnLevel - model.WarnOperator = ruleDef.WarnOperator - model.CritLevel = ruleDef.CritLevel - model.CritOperator = ruleDef.CritOperator - model.Frequency = ruleDef.Frequency model.Name = ruleDef.Name model.Description = ruleDef.Description - model.Aggregator = ruleDef.Aggregator model.State = ruleDef.State - model.QueryRange = ruleDef.QueryRange + + ParseAlertRulesFromAlertModel(ruleDef, model) + res[i] = model } diff --git a/pkg/services/sqlstore/alert_rule.go b/pkg/services/sqlstore/alert_rule.go index 36f142df7bb..1228e4cfbff 100644 --- a/pkg/services/sqlstore/alert_rule.go +++ b/pkg/services/sqlstore/alert_rule.go @@ -186,7 +186,7 @@ func upsertAlerts(alerts []*m.AlertRule, posted []*m.AlertRule, sess *xorm.Sessi } if update { - if alertToUpdate.Equals(alert) { + if alertToUpdate.ContainsUpdates(alert) { alert.Updated = time.Now() alert.State = alertToUpdate.State _, err := sess.Id(alert.Id).Update(alert) diff --git a/pkg/services/sqlstore/alert_rule_changes_test.go b/pkg/services/sqlstore/alert_rule_changes_test.go index e04d46b7fe2..cbd4ce93448 100644 --- a/pkg/services/sqlstore/alert_rule_changes_test.go +++ b/pkg/services/sqlstore/alert_rule_changes_test.go @@ -22,20 +22,11 @@ func TestAlertRuleChangesDataAccess(t *testing.T) { Convey("When dashboard is removed", func() { items := []*m.AlertRule{ { - PanelId: 1, - DashboardId: testDash.Id, - Query: "Query", - QueryRefId: "A", - WarnLevel: 30, - CritLevel: 50, - WarnOperator: ">", - CritOperator: ">", - Frequency: 10, - Name: "Alerting title", - Description: "Alerting description", - QueryRange: 3600, - Aggregator: "avg", - OrgId: FakeOrgId, + PanelId: 1, + DashboardId: testDash.Id, + Name: "Alerting title", + Description: "Alerting description", + OrgId: FakeOrgId, }, } diff --git a/pkg/services/sqlstore/alert_rule_test.go b/pkg/services/sqlstore/alert_rule_test.go index 9a3b0088b5c..9edcf20882a 100644 --- a/pkg/services/sqlstore/alert_rule_test.go +++ b/pkg/services/sqlstore/alert_rule_test.go @@ -8,7 +8,6 @@ import ( ) func TestAlertingDataAccess(t *testing.T) { - Convey("Testing Alerting data access", t, func() { InitTestDB(t) @@ -16,21 +15,11 @@ func TestAlertingDataAccess(t *testing.T) { items := []*m.AlertRule{ { - PanelId: 1, - DashboardId: testDash.Id, - OrgId: testDash.OrgId, - Query: "Query", - QueryRefId: "A", - WarnLevel: 30, - CritLevel: 50, - WarnOperator: ">", - CritOperator: ">", - Frequency: 10, - Name: "Alerting title", - Description: "Alerting description", - QueryRange: 3600, - Aggregator: "avg", - DatasourceId: 42, + PanelId: 1, + DashboardId: testDash.Id, + OrgId: testDash.OrgId, + Name: "Alerting title", + Description: "Alerting description", }, } @@ -58,25 +47,15 @@ func TestAlertingDataAccess(t *testing.T) { alert := alertQuery.Result[0] So(err2, ShouldBeNil) - So(alert.Frequency, ShouldEqual, 10) - So(alert.WarnLevel, ShouldEqual, 30) - So(alert.CritLevel, ShouldEqual, 50) - So(alert.WarnOperator, ShouldEqual, ">") - So(alert.CritOperator, ShouldEqual, ">") - So(alert.Query, ShouldEqual, "Query") - So(alert.QueryRefId, ShouldEqual, "A") So(alert.Name, ShouldEqual, "Alerting title") So(alert.Description, ShouldEqual, "Alerting description") - So(alert.QueryRange, ShouldEqual, 3600) - So(alert.Aggregator, ShouldEqual, "avg") So(alert.State, ShouldEqual, "OK") - So(alert.DatasourceId, ShouldEqual, 42) }) Convey("Alerts with same dashboard id and panel id should update", func() { modifiedItems := items - modifiedItems[0].Query = "Updated Query" - modifiedItems[0].State = "ALERT" + modifiedItems[0].Name = "New name" + //modifiedItems[0].State = "ALERT" modifiedCmd := m.SaveAlertsCommand{ DashboardId: testDash.Id, @@ -97,7 +76,7 @@ func TestAlertingDataAccess(t *testing.T) { So(err2, ShouldBeNil) So(len(query.Result), ShouldEqual, 1) - So(query.Result[0].Query, ShouldEqual, "Updated Query") + So(query.Result[0].Name, ShouldEqual, "Name") Convey("Alert state should not be updated", func() { So(query.Result[0].State, ShouldEqual, "OK") @@ -120,19 +99,19 @@ func TestAlertingDataAccess(t *testing.T) { { DashboardId: testDash.Id, PanelId: 1, - Query: "1", + Name: "1", OrgId: 1, }, { DashboardId: testDash.Id, PanelId: 2, - Query: "2", + Name: "2", OrgId: 1, }, { DashboardId: testDash.Id, PanelId: 3, - Query: "3", + Name: "3", OrgId: 1, }, } @@ -180,19 +159,10 @@ func TestAlertingDataAccess(t *testing.T) { Convey("When dashboard is removed", func() { items := []*m.AlertRule{ { - PanelId: 1, - DashboardId: testDash.Id, - Query: "Query", - QueryRefId: "A", - WarnLevel: 30, - CritLevel: 50, - WarnOperator: ">", - CritOperator: ">", - Frequency: 10, - Name: "Alerting title", - Description: "Alerting description", - QueryRange: 3600, - Aggregator: "avg", + PanelId: 1, + DashboardId: testDash.Id, + Name: "Alerting title", + Description: "Alerting description", }, } diff --git a/pkg/services/sqlstore/alert_state_test.go b/pkg/services/sqlstore/alert_state_test.go index 97a785eceac..c112db9725d 100644 --- a/pkg/services/sqlstore/alert_state_test.go +++ b/pkg/services/sqlstore/alert_state_test.go @@ -15,20 +15,11 @@ func TestAlertingStateAccess(t *testing.T) { items := []*m.AlertRule{ { - PanelId: 1, - DashboardId: testDash.Id, - OrgId: testDash.OrgId, - Query: "Query", - QueryRefId: "A", - WarnLevel: 30, - CritLevel: 50, - WarnOperator: ">", - CritOperator: ">", - Frequency: 10, - Name: "Alerting title", - Description: "Alerting description", - QueryRange: 3600, - Aggregator: "avg", + PanelId: 1, + DashboardId: testDash.Id, + OrgId: testDash.OrgId, + Name: "Alerting title", + Description: "Alerting description", }, } diff --git a/pkg/services/sqlstore/dashboard_parser_test.go b/pkg/services/sqlstore/dashboard_parser_test.go index 7dc508c50f2..315ee203576 100644 --- a/pkg/services/sqlstore/dashboard_parser_test.go +++ b/pkg/services/sqlstore/dashboard_parser_test.go @@ -101,17 +101,45 @@ func TestAlertModel(t *testing.T) { "timeShift": null, "aliasColors": {}, "seriesOverrides": [], + + "alerting": { - "queryRef": "A", - "warnLevel": 30, - "critLevel": 50, - "warnOperator": ">", - "critOperator": ">", - "aggregator": "sum", - "queryRange": 3600, "frequency": 10, - "name": "active desktop users", - "description": "restart webservers" + "warning": { + "op": ">", + "level": 10 + }, + "critical": { + "op": ">", + "level": 20 + }, + "function": "static", + "valueQuery": { + "queryRefId": "A", + "from": "5m", + "to": "now", + "agg": "avg", + "params": [ + "#A", + "5m", + "now", + "avg" + ] + }, + "evalQuery": { + "queryRefId": "A", + "from": "5m", + "to": "now", + "agg": "avg", + "params": [ + "#A", + "5m", + "now", + "avg" + ] + }, + "evalStringParam1": "", + "name": "Alerting Panel Title alert" }, "links": [] }, @@ -189,16 +217,42 @@ func TestAlertModel(t *testing.T) { }, "seriesOverrides": [], "alerting": { - "queryRef": "A", - "warnOperator": ">", - "critOperator": ">", - "warnLevel": 300, - "critLevel": 500, - "aggregator": "avg", - "queryRange": 3600, "frequency": 10, - "name": "active mobile users", - "description": "restart itunes" + "warning": { + "op": ">", + "level": 10 + }, + "critical": { + "op": ">", + "level": 20 + }, + "function": "static", + "valueQuery": { + "queryRefId": "A", + "from": "5m", + "to": "now", + "agg": "avg", + "params": [ + "#A", + "5m", + "now", + "avg" + ] + }, + "evalQuery": { + "queryRefId": "A", + "from": "5m", + "to": "now", + "agg": "avg", + "params": [ + "#A", + "5m", + "now", + "avg" + ] + }, + "evalStringParam1": "", + "name": "Alerting Panel Title alert" }, "links": [] } @@ -379,37 +433,13 @@ func TestAlertModel(t *testing.T) { So(v.DashboardId, ShouldEqual, 1) So(v.PanelId, ShouldNotEqual, 0) - So(v.WarnLevel, ShouldNotBeEmpty) - So(v.CritLevel, ShouldNotBeEmpty) - - So(v.Aggregator, ShouldNotBeEmpty) - So(v.Query, ShouldNotBeEmpty) - So(v.QueryRefId, ShouldNotBeEmpty) - So(v.QueryRange, ShouldNotEqual, 0) - So(v.Frequency, ShouldNotEqual, 0) So(v.Name, ShouldNotBeEmpty) So(v.Description, ShouldNotBeEmpty) + + expr := simplejson.NewFromAny(v.Expression) + So(expr.Get("valueQuery").Get("query").MustString(), ShouldNotEqual, "") + So(expr.Get("valueQuery").Get("datsourceId").MustInt64(), ShouldNotEqual, 0) } - - So(alerts[0].WarnLevel, ShouldEqual, 30) - So(alerts[1].WarnLevel, ShouldEqual, 300) - - So(alerts[0].Frequency, ShouldEqual, 10) - So(alerts[1].Frequency, ShouldEqual, 10) - - So(alerts[0].CritLevel, ShouldEqual, 50) - So(alerts[1].CritLevel, ShouldEqual, 500) - - So(alerts[0].CritOperator, ShouldEqual, ">") - So(alerts[1].CritOperator, ShouldEqual, ">") - So(alerts[0].WarnOperator, ShouldEqual, ">") - So(alerts[1].WarnOperator, ShouldEqual, ">") - - So(alerts[0].Query, ShouldEqual, `{"refId":"A","target":"aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)"}`) - So(alerts[1].Query, ShouldEqual, `{"refId":"A","target":"aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"}`) - - So(alerts[0].DatasourceId, ShouldEqual, 2) - So(alerts[1].DatasourceId, ShouldEqual, 1) }) }) } diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index e5b5d783886..42cb9b78d39 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/pkg/services/sqlstore/migrations/alert_mig.go @@ -11,21 +11,12 @@ func addAlertMigrations(mg *Migrator) { Columns: []*Column{ {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, {Name: "dashboard_id", Type: DB_BigInt, Nullable: false}, - {Name: "datasource_id", Type: DB_BigInt, Nullable: false}, {Name: "panel_id", Type: DB_BigInt, Nullable: false}, {Name: "org_id", Type: DB_BigInt, Nullable: false}, - {Name: "query", Type: DB_Text, Nullable: false}, - {Name: "query_ref_id", Type: DB_NVarchar, Length: 255, Nullable: false}, - {Name: "warn_level", Type: DB_Float, Nullable: false}, - {Name: "warn_operator", Type: DB_NVarchar, Length: 10, Nullable: false}, - {Name: "crit_level", Type: DB_Float, Nullable: false}, - {Name: "crit_operator", Type: DB_NVarchar, Length: 10, Nullable: false}, - {Name: "frequency", Type: DB_BigInt, Nullable: false}, {Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false}, {Name: "description", Type: DB_NVarchar, Length: 255, Nullable: false}, - {Name: "query_range", Type: DB_Int, Nullable: false}, - {Name: "aggregator", Type: DB_NVarchar, Length: 255, Nullable: false}, {Name: "state", Type: DB_NVarchar, Length: 255, Nullable: false}, + {Name: "expression", Type: DB_Text, Nullable: false}, {Name: "created", Type: DB_DateTime, Nullable: false}, {Name: "updated", Type: DB_DateTime, Nullable: false}, }, diff --git a/pkg/tsdb/graphite/graphite.go b/pkg/tsdb/graphite/graphite.go index b7c09715a8d..36e03288879 100644 --- a/pkg/tsdb/graphite/graphite.go +++ b/pkg/tsdb/graphite/graphite.go @@ -7,7 +7,6 @@ import ( "net/url" "time" - "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/tsdb" ) @@ -39,7 +38,7 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC for _, query := range queries { params["target"] = []string{ - getTargetFromQuery(query.Query), + query.Query, } } @@ -77,8 +76,3 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC result.QueryResults["A"] = queryRes return result } - -func getTargetFromQuery(query string) string { - json, _ := simplejson.NewJson([]byte(query)) - return json.Get("target").MustString() -} diff --git a/public/app/plugins/panel/graph/alert_tab_ctrl.ts b/public/app/plugins/panel/graph/alert_tab_ctrl.ts index df2516a15ec..1b1f052632c 100644 --- a/public/app/plugins/panel/graph/alert_tab_ctrl.ts +++ b/public/app/plugins/panel/graph/alert_tab_ctrl.ts @@ -81,6 +81,7 @@ export class AlertTabCtrl { var defaultName = (this.panelCtrl.dashboard.title + ' ' + this.panel.title + ' alert'); this.panel.alerting.name = this.panel.alerting.name || defaultName; + this.panel.alerting.description = this.panel.alerting.description || defaultName; this.panel.targets.map(target => { this.metricTargets.push(target); @@ -92,7 +93,7 @@ export class AlertTabCtrl { } evalFuncChanged() { - var evalFuncDef = _.findWhere(this.evalFuncs, { value: this.rule.expression.evalFunc }); + var evalFuncDef = _.findWhere(this.evalFuncs, { value: this.rule.evalFunc }); console.log(evalFuncDef); this.secondParam = evalFuncDef.secondParam; } From b17298c97ca592ba5ccee79d75a521c20e3a0ba4 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 10 Jun 2016 07:11:14 +0200 Subject: [PATCH 14/37] test(alerting): remove unused code to enable gorename --- pkg/middleware/middleware_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index cb37a809212..f8e4aa374e8 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -191,9 +191,7 @@ func TestMiddlewareContext(t *testing.T) { } }) - var createUserCmd *m.CreateUserCommand bus.AddHandler("test", func(cmd *m.CreateUserCommand) error { - createUserCmd = cmd cmd.Result = m.User{Id: 33} return nil }) From ef35184a80b9119cc4e712d63d98811cd387723c Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 10 Jun 2016 10:00:00 +0200 Subject: [PATCH 15/37] feat(alerting): rename alertrule model to alertruleDAO --- pkg/models/alerts.go | 26 +- pkg/models/alerts_state.go | 2 +- pkg/models/alerts_test.go | 4 +- pkg/services/alerting/dashboard_parser.go | 61 ++-- pkg/services/alerting/executor.go | 17 +- pkg/services/alerting/models.go | 4 +- pkg/services/alerting/rule_reader.go | 10 +- pkg/services/alerting/scheduler.go | 6 +- pkg/services/sqlstore/alert_rule.go | 20 +- pkg/services/sqlstore/alert_rule_changes.go | 2 +- .../sqlstore/alert_rule_changes_test.go | 2 +- .../sqlstore/alert_rule_parser_test.go | 76 +++++ pkg/services/sqlstore/alert_rule_test.go | 6 +- pkg/services/sqlstore/alert_state.go | 2 +- pkg/services/sqlstore/alert_state_test.go | 2 +- .../sqlstore/dashboard_parser_test.go | 323 ++++-------------- pkg/tsdb/graphite/graphite.go | 2 +- .../app/plugins/panel/graph/alert_tab_ctrl.ts | 4 +- 18 files changed, 222 insertions(+), 347 deletions(-) create mode 100644 pkg/services/sqlstore/alert_rule_parser_test.go diff --git a/pkg/models/alerts.go b/pkg/models/alerts.go index e069f7e81c3..0764aaa2845 100644 --- a/pkg/models/alerts.go +++ b/pkg/models/alerts.go @@ -6,7 +6,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" ) -type AlertRule struct { +type AlertRuleDAO struct { Id int64 OrgId int64 DashboardId int64 @@ -21,11 +21,15 @@ type AlertRule struct { Expression *simplejson.Json } -func (alertRule *AlertRule) ValidToSave() bool { - return true +func (this AlertRuleDAO) TableName() string { + return "alert_rule" } -func (this *AlertRule) ContainsUpdates(other *AlertRule) bool { +func (alertRule *AlertRuleDAO) ValidToSave() bool { + return alertRule.DashboardId != 0 +} + +func (this *AlertRuleDAO) ContainsUpdates(other *AlertRuleDAO) bool { result := false result = result || this.Name != other.Name @@ -78,7 +82,7 @@ type SaveAlertsCommand struct { UserId int64 OrgId int64 - Alerts []*AlertRule + Alerts []*AlertRuleDAO } type DeleteAlertCommand struct { @@ -92,23 +96,17 @@ type GetAlertsQuery struct { DashboardId int64 PanelId int64 - Result []*AlertRule + Result []*AlertRuleDAO } type GetAllAlertsQuery struct { - Result []*AlertRule -} - -type GetAlertsForExecutionQuery struct { - Timestamp int64 - - Result []*AlertRule + Result []*AlertRuleDAO } type GetAlertByIdQuery struct { Id int64 - Result *AlertRule + Result *AlertRuleDAO } type GetAlertChangesQuery struct { diff --git a/pkg/models/alerts_state.go b/pkg/models/alerts_state.go index 171eb754412..63f1ac73c0e 100644 --- a/pkg/models/alerts_state.go +++ b/pkg/models/alerts_state.go @@ -31,7 +31,7 @@ type UpdateAlertStateCommand struct { NewState string `json:"newState" binding:"Required"` Info string `json:"info"` - Result *AlertRule + Result *AlertRuleDAO } // Queries diff --git a/pkg/models/alerts_test.go b/pkg/models/alerts_test.go index 33a4937f5ad..af672ca6016 100644 --- a/pkg/models/alerts_test.go +++ b/pkg/models/alerts_test.go @@ -13,13 +13,13 @@ func TestAlertingModelTest(t *testing.T) { json1, _ := simplejson.NewJson([]byte(`{ "field": "value" }`)) json2, _ := simplejson.NewJson([]byte(`{ "field": "value" }`)) - rule1 := &AlertRule{ + rule1 := &AlertRuleDAO{ Expression: json1, Name: "Namn", Description: "Description", } - rule2 := &AlertRule{ + rule2 := &AlertRuleDAO{ Expression: json2, Name: "Namn", Description: "Description", diff --git a/pkg/services/alerting/dashboard_parser.go b/pkg/services/alerting/dashboard_parser.go index c6df8e53a10..7517db5b14c 100644 --- a/pkg/services/alerting/dashboard_parser.go +++ b/pkg/services/alerting/dashboard_parser.go @@ -1,14 +1,16 @@ package alerting import ( + "fmt" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" ) -func ParseAlertsFromDashboard(cmd *m.SaveDashboardCommand) []*m.AlertRule { - alerts := make([]*m.AlertRule, 0) +func ParseAlertsFromDashboard(cmd *m.SaveDashboardCommand) []*m.AlertRuleDAO { + alerts := make([]*m.AlertRuleDAO, 0) for _, rowObj := range cmd.Dashboard.Get("rows").MustArray() { row := simplejson.NewFromAny(rowObj) @@ -17,7 +19,7 @@ func ParseAlertsFromDashboard(cmd *m.SaveDashboardCommand) []*m.AlertRule { panel := simplejson.NewFromAny(panelObj) alerting := panel.Get("alerting") - alert := &m.AlertRule{ + alert := &m.AlertRuleDAO{ DashboardId: cmd.Result.Id, OrgId: cmd.Result.OrgId, PanelId: panel.Get("id").MustInt64(), @@ -28,8 +30,7 @@ func ParseAlertsFromDashboard(cmd *m.SaveDashboardCommand) []*m.AlertRule { log.Info("Alertrule: %v", alert.Name) - expression := alerting - valueQuery := expression.Get("valueQuery") + valueQuery := alerting.Get("valueQuery") valueQueryRef := valueQuery.Get("queryRefId").MustString() for _, targetsObj := range panel.Get("targets").MustArray() { target := simplejson.NewFromAny(targetsObj) @@ -47,7 +48,7 @@ func ParseAlertsFromDashboard(cmd *m.SaveDashboardCommand) []*m.AlertRule { if err := bus.Dispatch(query); err == nil { for _, ds := range query.Result { if ds.IsDefault { - valueQuery.Set("datasourceId", ds.Id) + alerting.SetPath([]string{"valueQuery", "datasourceId"}, ds.Id) } } } @@ -57,59 +58,71 @@ func ParseAlertsFromDashboard(cmd *m.SaveDashboardCommand) []*m.AlertRule { OrgId: cmd.OrgId, } bus.Dispatch(query) - valueQuery.Set("datasourceId", query.Result.Id) + alerting.SetPath([]string{"valueQuery", "datasourceId"}, query.Result.Id) } targetQuery := target.Get("target").MustString() if targetQuery != "" { - valueQuery.Set("query", targetQuery) + alerting.SetPath([]string{"valueQuery", "query"}, targetQuery) } } } - expression.Set("valueQuery", valueQuery) - alert.Expression = expression + alert.Expression = alerting - alertRule := &AlertRule{} + _, err := ParseAlertRulesFromAlertModel(alert) - ParseAlertRulesFromAlertModel(alert, alertRule) - - if alert.ValidToSave() && alertRule.IsValid() { + if err == nil && alert.ValidToSave() { alerts = append(alerts, alert) + } else { + log.Error2("Failed to parse model from expression", "error", err) } + } } return alerts } -func (rule *AlertRule) IsValid() bool { - return rule.ValueQuery.Query != "" -} +func ParseAlertRulesFromAlertModel(ruleDef *m.AlertRuleDAO) (*AlertRule, error) { + model := &AlertRule{} + model.Id = ruleDef.Id + model.OrgId = ruleDef.OrgId + model.Name = ruleDef.Name + model.Description = ruleDef.Description + model.State = ruleDef.State -func ParseAlertRulesFromAlertModel(ruleDef *m.AlertRule, model *AlertRule) error { critical := ruleDef.Expression.Get("critical") model.Critical = Level{ - Operator: critical.Get("operator").MustString(), + Operator: critical.Get("op").MustString(), Level: critical.Get("level").MustFloat64(), } warning := ruleDef.Expression.Get("warning") model.Warning = Level{ - Operator: warning.Get("operator").MustString(), + Operator: warning.Get("op").MustString(), Level: warning.Get("level").MustFloat64(), } model.Frequency = ruleDef.Expression.Get("frequency").MustInt64() valueQuery := ruleDef.Expression.Get("valueQuery") + model.ValueQuery = AlertQuery{ Query: valueQuery.Get("query").MustString(), DatasourceId: valueQuery.Get("datasourceId").MustInt64(), - From: valueQuery.Get("From").MustInt64(), - Until: valueQuery.Get("until").MustInt64(), - Aggregator: valueQuery.Get("aggregator").MustString(), + From: valueQuery.Get("from").MustString(), + To: valueQuery.Get("to").MustString(), + Aggregator: valueQuery.Get("agg").MustString(), } - return nil + if model.ValueQuery.Query == "" { + return nil, fmt.Errorf("missing valueQuery query") + } + + if model.ValueQuery.DatasourceId == 0 { + return nil, fmt.Errorf("missing valueQuery datasourceId") + } + + return model, nil } diff --git a/pkg/services/alerting/executor.go b/pkg/services/alerting/executor.go index e676c318e92..ea460a64798 100644 --- a/pkg/services/alerting/executor.go +++ b/pkg/services/alerting/executor.go @@ -2,7 +2,6 @@ package alerting import ( "fmt" - "strconv" "math" @@ -14,7 +13,6 @@ import ( ) var ( - resultLogFmt = "Alerting: executor %s %1.2f %s %1.2f : %v" descriptionFmt = "Actual value: %1.2f for %s" ) @@ -102,7 +100,7 @@ func (e *ExecutorImpl) Execute(job *AlertJob, resultQueue chan *AlertResult) { func (e *ExecutorImpl) executeQuery(job *AlertJob) (tsdb.TimeSeriesSlice, error) { getDsInfo := &m.GetDataSourceByIdQuery{ - Id: 1, + Id: job.Rule.ValueQuery.DatasourceId, OrgId: job.Rule.OrgId, } @@ -130,15 +128,16 @@ func (e *ExecutorImpl) executeQuery(job *AlertJob) (tsdb.TimeSeriesSlice, error) } func (e *ExecutorImpl) GetRequestForAlertRule(rule *AlertRule, datasource *m.DataSource) *tsdb.Request { + log.Debug2("GetRequest", "query", rule.ValueQuery.Query, "from", rule.ValueQuery.From, "datasourceId", datasource.Id) req := &tsdb.Request{ TimeRange: tsdb.TimeRange{ - From: "-" + strconv.Itoa(int(rule.ValueQuery.From)) + "s", - To: "now", + From: "-" + rule.ValueQuery.From, + To: rule.ValueQuery.To, }, Queries: []*tsdb.Query{ { RefId: "A", - Query: "apps.fakesite.*.counters.requests.count", + Query: rule.ValueQuery.Query, DataSource: &tsdb.DataSourceInfo{ Id: datasource.Id, Name: datasource.Name, @@ -156,7 +155,7 @@ func (e *ExecutorImpl) evaluateRule(rule *AlertRule, series tsdb.TimeSeriesSlice e.log.Debug("Evaluating Alerting Rule", "seriesCount", len(series), "ruleName", rule.Name) for _, serie := range series { - log.Debug("Evaluating series", "series", serie.Name) + e.log.Debug("Evaluating series", "series", serie.Name) if aggregator["avg"] == nil { continue @@ -166,7 +165,7 @@ func (e *ExecutorImpl) evaluateRule(rule *AlertRule, series tsdb.TimeSeriesSlice var critOperartor = operators[rule.Critical.Operator] var critResult = critOperartor(aggValue, rule.Critical.Level) - log.Trace(resultLogFmt, "Crit", serie.Name, aggValue, rule.Critical.Operator, rule.Critical.Level, critResult) + e.log.Debug("Alert execution Crit", "name", serie.Name, "aggValue", aggValue, "operator", rule.Critical.Operator, "level", rule.Critical.Level, "result", critResult) if critResult { return &AlertResult{ State: alertstates.Critical, @@ -177,7 +176,7 @@ func (e *ExecutorImpl) evaluateRule(rule *AlertRule, series tsdb.TimeSeriesSlice var warnOperartor = operators[rule.Warning.Operator] var warnResult = warnOperartor(aggValue, rule.Warning.Level) - log.Trace(resultLogFmt, "Warn", serie.Name, aggValue, rule.Warning.Operator, rule.Warning.Level, warnResult) + e.log.Debug("Alert execution Warn", "name", serie.Name, "aggValue", aggValue, "operator", rule.Warning.Operator, "level", rule.Warning.Level, "result", warnResult) if warnResult { return &AlertResult{ State: alertstates.Warn, diff --git a/pkg/services/alerting/models.go b/pkg/services/alerting/models.go index afbefac3658..88dc22feaf7 100644 --- a/pkg/services/alerting/models.go +++ b/pkg/services/alerting/models.go @@ -49,6 +49,6 @@ type AlertQuery struct { Query string DatasourceId int64 Aggregator string - From int64 - Until int64 + From string + To string } diff --git a/pkg/services/alerting/rule_reader.go b/pkg/services/alerting/rule_reader.go index c8c3a73cc55..710e695903d 100644 --- a/pkg/services/alerting/rule_reader.go +++ b/pkg/services/alerting/rule_reader.go @@ -49,15 +49,7 @@ func (arr *AlertRuleReader) Fetch() []*AlertRule { res := make([]*AlertRule, len(cmd.Result)) for i, ruleDef := range cmd.Result { - model := &AlertRule{} - model.Id = ruleDef.Id - model.OrgId = ruleDef.OrgId - model.Name = ruleDef.Name - model.Description = ruleDef.Description - model.State = ruleDef.State - - ParseAlertRulesFromAlertModel(ruleDef, model) - + model, _ := ParseAlertRulesFromAlertModel(ruleDef) res[i] = model } diff --git a/pkg/services/alerting/scheduler.go b/pkg/services/alerting/scheduler.go index ffa2b2b900c..b013b8a3eeb 100644 --- a/pkg/services/alerting/scheduler.go +++ b/pkg/services/alerting/scheduler.go @@ -8,16 +8,18 @@ import ( type SchedulerImpl struct { jobs map[int64]*AlertJob + log log.Logger } func NewScheduler() Scheduler { return &SchedulerImpl{ jobs: make(map[int64]*AlertJob, 0), + log: log.New("alerting.scheduler"), } } func (s *SchedulerImpl) Update(rules []*AlertRule) { - log.Debug("Scheduler: Update()") + s.log.Debug("Scheduler: Update") jobs := make(map[int64]*AlertJob, 0) @@ -38,7 +40,7 @@ func (s *SchedulerImpl) Update(rules []*AlertRule) { jobs[rule.Id] = job } - log.Debug("Scheduler: Selected %d jobs", len(jobs)) + s.log.Debug("Scheduler: Selected %d jobs", len(jobs)) s.jobs = jobs } diff --git a/pkg/services/sqlstore/alert_rule.go b/pkg/services/sqlstore/alert_rule.go index 1228e4cfbff..bff0a36fc4c 100644 --- a/pkg/services/sqlstore/alert_rule.go +++ b/pkg/services/sqlstore/alert_rule.go @@ -64,7 +64,7 @@ func HeartBeat(query *m.HeartBeatCommand) error { */ func GetAlertById(query *m.GetAlertByIdQuery) error { - alert := m.AlertRule{} + alert := m.AlertRuleDAO{} has, err := x.Id(query.Id).Get(&alert) if !has { return fmt.Errorf("could not find alert") @@ -78,7 +78,7 @@ func GetAlertById(query *m.GetAlertByIdQuery) error { } func GetAllAlertQueryHandler(query *m.GetAllAlertsQuery) error { - var alerts []*m.AlertRule + var alerts []*m.AlertRuleDAO err := x.Sql("select * from alert_rule").Find(&alerts) if err != nil { return err @@ -131,7 +131,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error { sql.WriteString(")") } - alerts := make([]*m.AlertRule, 0) + alerts := make([]*m.AlertRuleDAO, 0) if err := x.Sql(sql.String(), params...).Find(&alerts); err != nil { return err } @@ -141,7 +141,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error { } func DeleteAlertDefinition(dashboardId int64, sess *xorm.Session) error { - alerts := make([]*m.AlertRule, 0) + alerts := make([]*m.AlertRuleDAO, 0) sess.Where("dashboard_id = ?", dashboardId).Find(&alerts) for _, alert := range alerts { @@ -172,10 +172,10 @@ func SaveAlerts(cmd *m.SaveAlertsCommand) error { }) } -func upsertAlerts(alerts []*m.AlertRule, posted []*m.AlertRule, sess *xorm.Session) error { +func upsertAlerts(alerts []*m.AlertRuleDAO, posted []*m.AlertRuleDAO, sess *xorm.Session) error { for _, alert := range posted { update := false - var alertToUpdate *m.AlertRule + var alertToUpdate *m.AlertRuleDAO for _, k := range alerts { if alert.PanelId == k.PanelId { @@ -212,7 +212,7 @@ func upsertAlerts(alerts []*m.AlertRule, posted []*m.AlertRule, sess *xorm.Sessi return nil } -func deleteMissingAlerts(alerts []*m.AlertRule, posted []*m.AlertRule, sess *xorm.Session) error { +func deleteMissingAlerts(alerts []*m.AlertRuleDAO, posted []*m.AlertRuleDAO, sess *xorm.Session) error { for _, missingAlert := range alerts { missing := true @@ -238,12 +238,12 @@ func deleteMissingAlerts(alerts []*m.AlertRule, posted []*m.AlertRule, sess *xor return nil } -func GetAlertsByDashboardId2(dashboardId int64, sess *xorm.Session) ([]*m.AlertRule, error) { - alerts := make([]*m.AlertRule, 0) +func GetAlertsByDashboardId2(dashboardId int64, sess *xorm.Session) ([]*m.AlertRuleDAO, error) { + alerts := make([]*m.AlertRuleDAO, 0) err := sess.Where("dashboard_id = ?", dashboardId).Find(&alerts) if err != nil { - return []*m.AlertRule{}, err + return []*m.AlertRuleDAO{}, err } return alerts, nil diff --git a/pkg/services/sqlstore/alert_rule_changes.go b/pkg/services/sqlstore/alert_rule_changes.go index aa03e8e607d..b07e7ebfda6 100644 --- a/pkg/services/sqlstore/alert_rule_changes.go +++ b/pkg/services/sqlstore/alert_rule_changes.go @@ -48,7 +48,7 @@ func GetAlertRuleChanges(query *m.GetAlertChangesQuery) error { return nil } -func SaveAlertChange(change string, alert *m.AlertRule, sess *xorm.Session) error { +func SaveAlertChange(change string, alert *m.AlertRuleDAO, sess *xorm.Session) error { _, err := sess.Insert(&m.AlertRuleChange{ OrgId: alert.OrgId, Type: change, diff --git a/pkg/services/sqlstore/alert_rule_changes_test.go b/pkg/services/sqlstore/alert_rule_changes_test.go index cbd4ce93448..6b0a8785dab 100644 --- a/pkg/services/sqlstore/alert_rule_changes_test.go +++ b/pkg/services/sqlstore/alert_rule_changes_test.go @@ -20,7 +20,7 @@ func TestAlertRuleChangesDataAccess(t *testing.T) { var err error Convey("When dashboard is removed", func() { - items := []*m.AlertRule{ + items := []*m.AlertRuleDAO{ { PanelId: 1, DashboardId: testDash.Id, diff --git a/pkg/services/sqlstore/alert_rule_parser_test.go b/pkg/services/sqlstore/alert_rule_parser_test.go new file mode 100644 index 00000000000..03dec377710 --- /dev/null +++ b/pkg/services/sqlstore/alert_rule_parser_test.go @@ -0,0 +1,76 @@ +package sqlstore + +import ( + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" + . "github.com/smartystreets/goconvey/convey" +) + +func TestAlertRuleModelParsing(t *testing.T) { + + Convey("Parsing alertRule from expression", t, func() { + alertRuleDAO := &m.AlertRuleDAO{} + json, _ := simplejson.NewJson([]byte(` + { + "critical": { + "level": 20, + "op": ">" + }, + "description": "Alerting Panel Title alert", + "evalQuery": { + "agg": "avg", + "from": "5m", + "params": [ + "#A", + "5m", + "now", + "avg" + ], + "queryRefId": "A", + "to": "now" + }, + "evalStringParam1": "", + "frequency": 10, + "function": "static", + "name": "Alerting Panel Title alert", + "queryRef": "- select query -", + "valueQuery": { + "agg": "avg", + "datasourceId": 1, + "from": "5m", + "params": [ + "#A", + "5m", + "now", + "avg" + ], + "query": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)", + "queryRefId": "A", + "to": "now" + }, + "warning": { + "level": 10, + "op": ">" + } + }`)) + + alertRuleDAO.Name = "Test" + alertRuleDAO.Expression = json + rule, _ := alerting.ParseAlertRulesFromAlertModel(alertRuleDAO) + + Convey("Confirm that all properties are set", func() { + So(rule.ValueQuery.Query, ShouldEqual, "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)") + So(rule.ValueQuery.From, ShouldEqual, "5m") + So(rule.ValueQuery.To, ShouldEqual, "now") + So(rule.ValueQuery.DatasourceId, ShouldEqual, 1) + So(rule.ValueQuery.Aggregator, ShouldEqual, "avg") + So(rule.Warning.Level, ShouldEqual, 10) + So(rule.Warning.Operator, ShouldEqual, ">") + So(rule.Critical.Level, ShouldEqual, 20) + So(rule.Critical.Operator, ShouldEqual, ">") + }) + }) +} diff --git a/pkg/services/sqlstore/alert_rule_test.go b/pkg/services/sqlstore/alert_rule_test.go index 9edcf20882a..939decd8e2d 100644 --- a/pkg/services/sqlstore/alert_rule_test.go +++ b/pkg/services/sqlstore/alert_rule_test.go @@ -13,7 +13,7 @@ func TestAlertingDataAccess(t *testing.T) { testDash := insertTestDashboard("dashboard with alerts", 1, "alert") - items := []*m.AlertRule{ + items := []*m.AlertRuleDAO{ { PanelId: 1, DashboardId: testDash.Id, @@ -95,7 +95,7 @@ func TestAlertingDataAccess(t *testing.T) { }) Convey("Multiple alerts per dashboard", func() { - multipleItems := []*m.AlertRule{ + multipleItems := []*m.AlertRuleDAO{ { DashboardId: testDash.Id, PanelId: 1, @@ -157,7 +157,7 @@ func TestAlertingDataAccess(t *testing.T) { }) Convey("When dashboard is removed", func() { - items := []*m.AlertRule{ + items := []*m.AlertRuleDAO{ { PanelId: 1, DashboardId: testDash.Id, diff --git a/pkg/services/sqlstore/alert_state.go b/pkg/services/sqlstore/alert_state.go index d2f3d4a4265..207e2f20385 100644 --- a/pkg/services/sqlstore/alert_state.go +++ b/pkg/services/sqlstore/alert_state.go @@ -19,7 +19,7 @@ func SetNewAlertState(cmd *m.UpdateAlertStateCommand) error { return fmt.Errorf("new state is invalid") } - alert := m.AlertRule{} + alert := m.AlertRuleDAO{} has, err := sess.Id(cmd.AlertId).Get(&alert) if !has { return fmt.Errorf("Could not find alert") diff --git a/pkg/services/sqlstore/alert_state_test.go b/pkg/services/sqlstore/alert_state_test.go index c112db9725d..7820b06525f 100644 --- a/pkg/services/sqlstore/alert_state_test.go +++ b/pkg/services/sqlstore/alert_state_test.go @@ -13,7 +13,7 @@ func TestAlertingStateAccess(t *testing.T) { testDash := insertTestDashboard("dashboard with alerts", 1, "alert") - items := []*m.AlertRule{ + items := []*m.AlertRuleDAO{ { PanelId: 1, DashboardId: testDash.Id, diff --git a/pkg/services/sqlstore/dashboard_parser_test.go b/pkg/services/sqlstore/dashboard_parser_test.go index 315ee203576..d8dc22b68c6 100644 --- a/pkg/services/sqlstore/dashboard_parser_test.go +++ b/pkg/services/sqlstore/dashboard_parser_test.go @@ -9,34 +9,25 @@ import ( . "github.com/smartystreets/goconvey/convey" ) -func TestAlertModel(t *testing.T) { +func TestAlertModelParsing(t *testing.T) { - Convey("Parsing alerts from dashboard", t, func() { - json := `{ + Convey("Parsing alert info from json", t, func() { + Convey("Parsing and validating alerts from dashboards", func() { + json := `{ "id": 57, "title": "Graphite 4", "originalTitle": "Graphite 4", "tags": [ "graphite" ], - "style": "dark", - "timezone": "browser", - "editable": true, - "hideControls": false, - "sharedCrosshair": false, "rows": [ { - "collapse": false, - "editable": true, - "height": "250px", + "panels": [ { "title": "Active desktop users", - "error": false, - "span": 6, "editable": true, "type": "graph", - "isNew": true, "id": 3, "targets": [ { @@ -45,65 +36,9 @@ func TestAlertModel(t *testing.T) { } ], "datasource": null, - "renderer": "flot", - "yaxes": [ - { - "label": null, - "show": true, - "logBase": 1, - "min": null, - "max": null, - "format": "short" - }, - { - "label": null, - "show": true, - "logBase": 1, - "min": null, - "max": null, - "format": "short" - } - ], - "xaxis": { - "show": true - }, - "grid": { - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "lines": true, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "percentage": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false - }, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "shared": true, - "msResolution": false - }, - "timeFrom": null, - "timeShift": null, - "aliasColors": {}, - "seriesOverrides": [], - - "alerting": { + "name": "alert name", + "description": "description", "frequency": 10, "warning": { "op": ">", @@ -140,16 +75,10 @@ func TestAlertModel(t *testing.T) { }, "evalStringParam1": "", "name": "Alerting Panel Title alert" - }, - "links": [] + } }, { "title": "Active mobile users", - "error": false, - "span": 6, - "editable": true, - "type": "graph", - "isNew": true, "id": 4, "targets": [ { @@ -158,65 +87,9 @@ func TestAlertModel(t *testing.T) { } ], "datasource": "graphite2", - "renderer": "flot", - "yaxes": [ - { - "label": null, - "show": true, - "logBase": 1, - "min": null, - "max": null, - "format": "short" - }, - { - "label": null, - "show": true, - "logBase": 1, - "min": null, - "max": null, - "format": "short" - } - ], - "xaxis": { - "show": true - }, - "grid": { - "threshold1": null, - "threshold2": null, - "threshold1Color": "rgba(216, 200, 27, 0.27)", - "threshold2Color": "rgba(234, 112, 112, 0.22)" - }, - "lines": true, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "percentage": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false - }, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "shared": true, - "msResolution": false - }, - "timeFrom": null, - "timeShift": null, - "aliasColors": { - "mobile": "#EAB839" - }, - "seriesOverrides": [], "alerting": { + "name": "alert name", + "description": "description", "frequency": 10, "warning": { "op": ">", @@ -253,8 +126,7 @@ func TestAlertModel(t *testing.T) { }, "evalStringParam1": "", "name": "Alerting Panel Title alert" - }, - "links": [] + } } ], "title": "Row" @@ -265,41 +137,8 @@ func TestAlertModel(t *testing.T) { "height": "250px", "panels": [ { - "columns": [], "datasource": "InfluxDB", - "editable": true, - "error": false, - "fontSize": "100%", "id": 2, - "isNew": true, - "pageSize": null, - "scroll": true, - "showHeader": true, - "sort": { - "col": 0, - "desc": true - }, - "span": 6, - "styles": [ - { - "dateFormat": "YYYY-MM-DD HH:mm:ss", - "pattern": "Time", - "type": "date" - }, - { - "colorMode": null, - "colors": [ - "rgba(245, 54, 54, 0.9)", - "rgba(237, 129, 40, 0.89)", - "rgba(50, 172, 45, 0.97)" - ], - "decimals": 2, - "pattern": "/.*/", - "thresholds": [], - "type": "number", - "unit": "short" - } - ], "targets": [ { "dsType": "influxdb", @@ -342,104 +181,60 @@ func TestAlertModel(t *testing.T) { ], "title": "Broken influxdb panel", "transform": "table", - "type": "table", - "links": [] + "type": "table" } ], "title": "New row" } - ], - "time": { - "from": "now-1h", - "to": "now" - }, - "timepicker": { - "now": true, - "nowDelay": "5m", - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d", - "7d" - ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ] - }, - "templating": { - "list": [] - }, - "annotations": { - "list": [] - }, - "schemaVersion": 12, - "version": 16, - "links": [] + ] + }` - dashboardJson, _ := simplejson.NewJson([]byte(json)) - cmd := &m.SaveDashboardCommand{ - Dashboard: dashboardJson, - UserId: 1, - OrgId: 1, - Overwrite: true, - Result: &m.Dashboard{ - Id: 1, - }, - } - - InitTestDB(t) - - AddDataSource(&m.AddDataSourceCommand{ - Name: "graphite2", - OrgId: 1, - Type: m.DS_INFLUXDB, - Access: m.DS_ACCESS_DIRECT, - Url: "http://test", - IsDefault: false, - Database: "site", - }) - - AddDataSource(&m.AddDataSourceCommand{ - Name: "InfluxDB", - OrgId: 1, - Type: m.DS_GRAPHITE, - Access: m.DS_ACCESS_DIRECT, - Url: "http://test", - IsDefault: true, - }) - - alerts := alerting.ParseAlertsFromDashboard(cmd) - - Convey("all properties have been set", func() { - So(alerts, ShouldNotBeEmpty) - So(len(alerts), ShouldEqual, 2) - - for _, v := range alerts { - So(v.DashboardId, ShouldEqual, 1) - So(v.PanelId, ShouldNotEqual, 0) - - So(v.Name, ShouldNotBeEmpty) - So(v.Description, ShouldNotBeEmpty) - - expr := simplejson.NewFromAny(v.Expression) - So(expr.Get("valueQuery").Get("query").MustString(), ShouldNotEqual, "") - So(expr.Get("valueQuery").Get("datsourceId").MustInt64(), ShouldNotEqual, 0) + dashboardJSON, _ := simplejson.NewJson([]byte(json)) + cmd := &m.SaveDashboardCommand{ + Dashboard: dashboardJSON, + UserId: 1, + OrgId: 1, + Overwrite: true, + Result: &m.Dashboard{ + Id: 1, + }, } + + InitTestDB(t) + + AddDataSource(&m.AddDataSourceCommand{ + Name: "graphite2", + OrgId: 1, + Type: m.DS_INFLUXDB, + Access: m.DS_ACCESS_DIRECT, + Url: "http://test", + IsDefault: false, + Database: "site", + }) + + AddDataSource(&m.AddDataSourceCommand{ + Name: "InfluxDB", + OrgId: 1, + Type: m.DS_GRAPHITE, + Access: m.DS_ACCESS_DIRECT, + Url: "http://test", + IsDefault: true, + }) + + alerts := alerting.ParseAlertsFromDashboard(cmd) + + Convey("all properties have been set", func() { + So(alerts, ShouldNotBeEmpty) + So(len(alerts), ShouldEqual, 2) + + for _, v := range alerts { + So(v.DashboardId, ShouldEqual, 1) + So(v.PanelId, ShouldNotEqual, 0) + + So(v.Name, ShouldNotBeEmpty) + So(v.Description, ShouldNotBeEmpty) + } + }) }) }) } diff --git a/pkg/tsdb/graphite/graphite.go b/pkg/tsdb/graphite/graphite.go index 36e03288879..0be6cf04e12 100644 --- a/pkg/tsdb/graphite/graphite.go +++ b/pkg/tsdb/graphite/graphite.go @@ -59,7 +59,7 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC var data []TargetResponseDTO err = json.Unmarshal(body, &data) if err != nil { - glog.Info("Failed to unmarshal graphite response", "error", err) + glog.Info("Failed to unmarshal graphite response", "error", err, "body", string(body)) result.Error = err return result } diff --git a/public/app/plugins/panel/graph/alert_tab_ctrl.ts b/public/app/plugins/panel/graph/alert_tab_ctrl.ts index 1b1f052632c..0f5f8b58be3 100644 --- a/public/app/plugins/panel/graph/alert_tab_ctrl.ts +++ b/public/app/plugins/panel/graph/alert_tab_ctrl.ts @@ -54,13 +54,13 @@ export class AlertTabCtrl { function: 'static', valueQuery: { queryRefId: 'A', - from: '5m', + from: '600s', to: 'now', agg: 'avg', }, evalQuery: { queryRefId: 'A', - from: '5m', + from: '600s', to: 'now', agg: 'avg', }, From 0c69c5afb19ae105034c6b24fa9ecb0a626710e7 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 10 Jun 2016 11:37:03 +0200 Subject: [PATCH 16/37] test(alerting): fixes broken unittests --- pkg/models/alerts.go | 15 ++++++++------- pkg/services/alerting/scheduler.go | 4 ++-- pkg/services/sqlstore/alert_rule_test.go | 8 ++++++-- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/pkg/models/alerts.go b/pkg/models/alerts.go index 0764aaa2845..46177d1b334 100644 --- a/pkg/models/alerts.go +++ b/pkg/models/alerts.go @@ -31,19 +31,20 @@ func (alertRule *AlertRuleDAO) ValidToSave() bool { func (this *AlertRuleDAO) ContainsUpdates(other *AlertRuleDAO) bool { result := false - result = result || this.Name != other.Name result = result || this.Description != other.Description - json1, err1 := this.Expression.MarshalJSON() - json2, err2 := other.Expression.MarshalJSON() + if this.Expression != nil && other.Expression != nil { + json1, err1 := this.Expression.Encode() + json2, err2 := other.Expression.Encode() - if err1 != nil || err2 != nil { - return false + if err1 != nil || err2 != nil { + return false + } + + result = result || string(json1) != string(json2) } - result = result || string(json1) != string(json2) - //don't compare .State! That would be insane. return result diff --git a/pkg/services/alerting/scheduler.go b/pkg/services/alerting/scheduler.go index b013b8a3eeb..5b376e8c9d8 100644 --- a/pkg/services/alerting/scheduler.go +++ b/pkg/services/alerting/scheduler.go @@ -40,7 +40,7 @@ func (s *SchedulerImpl) Update(rules []*AlertRule) { jobs[rule.Id] = job } - s.log.Debug("Scheduler: Selected %d jobs", len(jobs)) + s.log.Debug("Scheduler: Selected new jobs", "job count", len(jobs)) s.jobs = jobs } @@ -49,7 +49,7 @@ func (s *SchedulerImpl) Tick(tickTime time.Time, execQueue chan *AlertJob) { for _, job := range s.jobs { if now%job.Rule.Frequency == 0 && job.Running == false { - log.Trace("Scheduler: Putting job on to exec queue: %s", job.Rule.Name) + s.log.Debug("Scheduler: Putting job on to exec queue", "name", job.Rule.Name) execQueue <- job } } diff --git a/pkg/services/sqlstore/alert_rule_test.go b/pkg/services/sqlstore/alert_rule_test.go index 939decd8e2d..0e1127591d5 100644 --- a/pkg/services/sqlstore/alert_rule_test.go +++ b/pkg/services/sqlstore/alert_rule_test.go @@ -3,6 +3,7 @@ package sqlstore import ( "testing" + "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" . "github.com/smartystreets/goconvey/convey" ) @@ -20,6 +21,7 @@ func TestAlertingDataAccess(t *testing.T) { OrgId: testDash.OrgId, Name: "Alerting title", Description: "Alerting description", + Expression: simplejson.New(), }, } @@ -54,8 +56,7 @@ func TestAlertingDataAccess(t *testing.T) { Convey("Alerts with same dashboard id and panel id should update", func() { modifiedItems := items - modifiedItems[0].Name = "New name" - //modifiedItems[0].State = "ALERT" + modifiedItems[0].Name = "Name" modifiedCmd := m.SaveAlertsCommand{ DashboardId: testDash.Id, @@ -101,18 +102,21 @@ func TestAlertingDataAccess(t *testing.T) { PanelId: 1, Name: "1", OrgId: 1, + Expression: simplejson.New(), }, { DashboardId: testDash.Id, PanelId: 2, Name: "2", OrgId: 1, + Expression: simplejson.New(), }, { DashboardId: testDash.Id, PanelId: 3, Name: "3", OrgId: 1, + Expression: simplejson.New(), }, } From 8cd1d179164ef0ce4c3620f0d235c50ab83f6098 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 10 Jun 2016 13:13:23 +0200 Subject: [PATCH 17/37] feat(alerting): new alerting model\ --- pkg/services/alerting/dashboard_parser.go | 35 +++++----- pkg/services/alerting/executor.go | 10 +-- pkg/services/alerting/models.go | 40 +++++------ .../sqlstore/alert_rule_parser_test.go | 67 +++++++------------ .../app/plugins/panel/graph/alert_tab_ctrl.ts | 19 ++---- 5 files changed, 74 insertions(+), 97 deletions(-) diff --git a/pkg/services/alerting/dashboard_parser.go b/pkg/services/alerting/dashboard_parser.go index 7517db5b14c..01914e2b39a 100644 --- a/pkg/services/alerting/dashboard_parser.go +++ b/pkg/services/alerting/dashboard_parser.go @@ -30,8 +30,8 @@ func ParseAlertsFromDashboard(cmd *m.SaveDashboardCommand) []*m.AlertRuleDAO { log.Info("Alertrule: %v", alert.Name) - valueQuery := alerting.Get("valueQuery") - valueQueryRef := valueQuery.Get("queryRefId").MustString() + valueQuery := alerting.Get("query") + valueQueryRef := valueQuery.Get("refId").MustString() for _, targetsObj := range panel.Get("targets").MustArray() { target := simplejson.NewFromAny(targetsObj) @@ -48,7 +48,7 @@ func ParseAlertsFromDashboard(cmd *m.SaveDashboardCommand) []*m.AlertRuleDAO { if err := bus.Dispatch(query); err == nil { for _, ds := range query.Result { if ds.IsDefault { - alerting.SetPath([]string{"valueQuery", "datasourceId"}, ds.Id) + alerting.SetPath([]string{"query", "datasourceId"}, ds.Id) } } } @@ -58,12 +58,12 @@ func ParseAlertsFromDashboard(cmd *m.SaveDashboardCommand) []*m.AlertRuleDAO { OrgId: cmd.OrgId, } bus.Dispatch(query) - alerting.SetPath([]string{"valueQuery", "datasourceId"}, query.Result.Id) + alerting.SetPath([]string{"query", "datasourceId"}, query.Result.Id) } targetQuery := target.Get("target").MustString() if targetQuery != "" { - alerting.SetPath([]string{"valueQuery", "query"}, targetQuery) + alerting.SetPath([]string{"query", "query"}, targetQuery) } } } @@ -105,23 +105,24 @@ func ParseAlertRulesFromAlertModel(ruleDef *m.AlertRuleDAO) (*AlertRule, error) } model.Frequency = ruleDef.Expression.Get("frequency").MustInt64() + model.Transform = ruleDef.Expression.Get("transform").Get("type").MustString() + model.TransformParams = *ruleDef.Expression.Get("transform") - valueQuery := ruleDef.Expression.Get("valueQuery") - - model.ValueQuery = AlertQuery{ - Query: valueQuery.Get("query").MustString(), - DatasourceId: valueQuery.Get("datasourceId").MustInt64(), - From: valueQuery.Get("from").MustString(), - To: valueQuery.Get("to").MustString(), - Aggregator: valueQuery.Get("agg").MustString(), + query := ruleDef.Expression.Get("query") + model.Query = AlertQuery{ + Query: query.Get("query").MustString(), + DatasourceId: query.Get("datasourceId").MustInt64(), + From: query.Get("from").MustString(), + To: query.Get("to").MustString(), + Aggregator: query.Get("agg").MustString(), } - if model.ValueQuery.Query == "" { - return nil, fmt.Errorf("missing valueQuery query") + if model.Query.Query == "" { + return nil, fmt.Errorf("missing query.query") } - if model.ValueQuery.DatasourceId == 0 { - return nil, fmt.Errorf("missing valueQuery datasourceId") + if model.Query.DatasourceId == 0 { + return nil, fmt.Errorf("missing query.datasourceId") } return model, nil diff --git a/pkg/services/alerting/executor.go b/pkg/services/alerting/executor.go index ea460a64798..cfdc18edb2c 100644 --- a/pkg/services/alerting/executor.go +++ b/pkg/services/alerting/executor.go @@ -100,7 +100,7 @@ func (e *ExecutorImpl) Execute(job *AlertJob, resultQueue chan *AlertResult) { func (e *ExecutorImpl) executeQuery(job *AlertJob) (tsdb.TimeSeriesSlice, error) { getDsInfo := &m.GetDataSourceByIdQuery{ - Id: job.Rule.ValueQuery.DatasourceId, + Id: job.Rule.Query.DatasourceId, OrgId: job.Rule.OrgId, } @@ -128,16 +128,16 @@ func (e *ExecutorImpl) executeQuery(job *AlertJob) (tsdb.TimeSeriesSlice, error) } func (e *ExecutorImpl) GetRequestForAlertRule(rule *AlertRule, datasource *m.DataSource) *tsdb.Request { - log.Debug2("GetRequest", "query", rule.ValueQuery.Query, "from", rule.ValueQuery.From, "datasourceId", datasource.Id) + log.Debug2("GetRequest", "query", rule.Query.Query, "from", rule.Query.From, "datasourceId", datasource.Id) req := &tsdb.Request{ TimeRange: tsdb.TimeRange{ - From: "-" + rule.ValueQuery.From, - To: rule.ValueQuery.To, + From: "-" + rule.Query.From, + To: rule.Query.To, }, Queries: []*tsdb.Query{ { RefId: "A", - Query: rule.ValueQuery.Query, + Query: rule.Query.Query, DataSource: &tsdb.DataSourceInfo{ Id: datasource.Id, Name: datasource.Name, diff --git a/pkg/services/alerting/models.go b/pkg/services/alerting/models.go index 88dc22feaf7..4f3796d69ec 100644 --- a/pkg/services/alerting/models.go +++ b/pkg/services/alerting/models.go @@ -1,5 +1,10 @@ package alerting +import ( + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/tsdb" +) + type AlertJob struct { Offset int64 Delay bool @@ -18,26 +23,23 @@ type AlertResult struct { } type AlertRule struct { - Id int64 - OrgId int64 - DashboardId int64 - PanelId int64 - //WarnLevel float64 - //CritLevel float64 - //WarnOperator string - //CritOperator string - Frequency int64 - Name string - Description string - State string + Id int64 + OrgId int64 + DashboardId int64 + PanelId int64 + Frequency int64 + Name string + Description string + State string + Warning Level + Critical Level + Query AlertQuery + Transform string + TransformParams simplejson.Json +} - Warning Level - Critical Level - - ValueQuery AlertQuery - EvalFunc string - EvalQuery AlertQuery - EvalParam string +type Transformer interface { + Transform(tsdb tsdb.TimeSeriesSlice) float64 } type Level struct { diff --git a/pkg/services/sqlstore/alert_rule_parser_test.go b/pkg/services/sqlstore/alert_rule_parser_test.go index 03dec377710..6028fba98f4 100644 --- a/pkg/services/sqlstore/alert_rule_parser_test.go +++ b/pkg/services/sqlstore/alert_rule_parser_test.go @@ -15,58 +15,37 @@ func TestAlertRuleModelParsing(t *testing.T) { alertRuleDAO := &m.AlertRuleDAO{} json, _ := simplejson.NewJson([]byte(` { - "critical": { - "level": 20, - "op": ">" - }, - "description": "Alerting Panel Title alert", - "evalQuery": { - "agg": "avg", - "from": "5m", - "params": [ - "#A", - "5m", - "now", - "avg" - ], - "queryRefId": "A", - "to": "now" - }, - "evalStringParam1": "", "frequency": 10, - "function": "static", - "name": "Alerting Panel Title alert", - "queryRef": "- select query -", - "valueQuery": { - "agg": "avg", - "datasourceId": 1, - "from": "5m", - "params": [ - "#A", - "5m", - "now", - "avg" - ], - "query": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)", - "queryRefId": "A", - "to": "now" - }, "warning": { - "level": 10, - "op": ">" - } - }`)) + "op": ">", + "level": 10 + }, + "critical": { + "op": ">", + "level": 20 + }, + "query": { + "queryRefId": "A", + "from": "5m", + "to": "now", + "datasourceId": 1, + "query": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" + }, + "transform": { + "name": "aggregation", + "method": "avg" + }`)) alertRuleDAO.Name = "Test" alertRuleDAO.Expression = json rule, _ := alerting.ParseAlertRulesFromAlertModel(alertRuleDAO) Convey("Confirm that all properties are set", func() { - So(rule.ValueQuery.Query, ShouldEqual, "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)") - So(rule.ValueQuery.From, ShouldEqual, "5m") - So(rule.ValueQuery.To, ShouldEqual, "now") - So(rule.ValueQuery.DatasourceId, ShouldEqual, 1) - So(rule.ValueQuery.Aggregator, ShouldEqual, "avg") + So(rule.Query.Query, ShouldEqual, "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)") + So(rule.Query.From, ShouldEqual, "5m") + So(rule.Query.To, ShouldEqual, "now") + So(rule.Query.DatasourceId, ShouldEqual, 1) + //So(rule.ValueQuery.Aggregator, ShouldEqual, "avg") So(rule.Warning.Level, ShouldEqual, 10) So(rule.Warning.Operator, ShouldEqual, ">") So(rule.Critical.Level, ShouldEqual, 20) diff --git a/public/app/plugins/panel/graph/alert_tab_ctrl.ts b/public/app/plugins/panel/graph/alert_tab_ctrl.ts index 0f5f8b58be3..40e8a32926c 100644 --- a/public/app/plugins/panel/graph/alert_tab_ctrl.ts +++ b/public/app/plugins/panel/graph/alert_tab_ctrl.ts @@ -51,20 +51,15 @@ export class AlertTabCtrl { frequency: 10, warning: { op: '>', level: 10 }, critical: { op: '>', level: 20 }, - function: 'static', - valueQuery: { - queryRefId: 'A', - from: '600s', + query: { + refId: 'A', + from: '5m', to: 'now', - agg: 'avg', }, - evalQuery: { - queryRefId: 'A', - from: '600s', - to: 'now', - agg: 'avg', - }, - evalStringParam1: '', + transform: { + type: 'aggregation', + method: 'avg' + } }; /** @ngInject */ From 3c0b5fe78eadd5bd25926b743569e40a1e30914c Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 10 Jun 2016 13:26:19 +0200 Subject: [PATCH 18/37] tech(alerting): add graphite dateformat replacer --- pkg/tsdb/graphite/graphite.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/tsdb/graphite/graphite.go b/pkg/tsdb/graphite/graphite.go index 0be6cf04e12..98f4c648048 100644 --- a/pkg/tsdb/graphite/graphite.go +++ b/pkg/tsdb/graphite/graphite.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "net/http" "net/url" + "strings" "time" "github.com/grafana/grafana/pkg/log" @@ -30,7 +31,7 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC result := &tsdb.BatchResult{} params := url.Values{ - "from": []string{context.TimeRange.From}, + "from": []string{formatTimeRange(context.TimeRange.From)}, "until": []string{context.TimeRange.To}, "format": []string{"json"}, "maxDataPoints": []string{"500"}, @@ -76,3 +77,7 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC result.QueryResults["A"] = queryRes return result } + +func formatTimeRange(input string) string { + return strings.Replace(strings.Replace(input, "m", "min", -1), "M", "mon", -1) +} From ea8fb66f732bc34609b5151096148fc50718cdc0 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 10 Jun 2016 13:41:01 +0200 Subject: [PATCH 19/37] test(alerting): fixes broken unittests --- pkg/models/alerts.go | 16 ++-- pkg/models/alerts_state.go | 2 +- pkg/models/alerts_test.go | 4 +- pkg/services/alerting/dashboard_parser.go | 10 +- pkg/services/alerting/executor_test.go | 37 ++++---- pkg/services/alerting/rule_reader.go | 2 +- pkg/services/sqlstore/alert_rule.go | 20 ++-- pkg/services/sqlstore/alert_rule_changes.go | 2 +- .../sqlstore/alert_rule_changes_test.go | 2 +- .../sqlstore/alert_rule_parser_test.go | 12 +-- pkg/services/sqlstore/alert_rule_test.go | 6 +- pkg/services/sqlstore/alert_state.go | 2 +- pkg/services/sqlstore/alert_state_test.go | 2 +- .../sqlstore/dashboard_parser_test.go | 92 ++++++------------- 14 files changed, 88 insertions(+), 121 deletions(-) diff --git a/pkg/models/alerts.go b/pkg/models/alerts.go index 46177d1b334..e5cc76d198c 100644 --- a/pkg/models/alerts.go +++ b/pkg/models/alerts.go @@ -6,7 +6,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" ) -type AlertRuleDAO struct { +type AlertRuleModel struct { Id int64 OrgId int64 DashboardId int64 @@ -21,15 +21,15 @@ type AlertRuleDAO struct { Expression *simplejson.Json } -func (this AlertRuleDAO) TableName() string { +func (this AlertRuleModel) TableName() string { return "alert_rule" } -func (alertRule *AlertRuleDAO) ValidToSave() bool { +func (alertRule *AlertRuleModel) ValidToSave() bool { return alertRule.DashboardId != 0 } -func (this *AlertRuleDAO) ContainsUpdates(other *AlertRuleDAO) bool { +func (this *AlertRuleModel) ContainsUpdates(other *AlertRuleModel) bool { result := false result = result || this.Name != other.Name result = result || this.Description != other.Description @@ -83,7 +83,7 @@ type SaveAlertsCommand struct { UserId int64 OrgId int64 - Alerts []*AlertRuleDAO + Alerts []*AlertRuleModel } type DeleteAlertCommand struct { @@ -97,17 +97,17 @@ type GetAlertsQuery struct { DashboardId int64 PanelId int64 - Result []*AlertRuleDAO + Result []*AlertRuleModel } type GetAllAlertsQuery struct { - Result []*AlertRuleDAO + Result []*AlertRuleModel } type GetAlertByIdQuery struct { Id int64 - Result *AlertRuleDAO + Result *AlertRuleModel } type GetAlertChangesQuery struct { diff --git a/pkg/models/alerts_state.go b/pkg/models/alerts_state.go index 63f1ac73c0e..50da6d93f88 100644 --- a/pkg/models/alerts_state.go +++ b/pkg/models/alerts_state.go @@ -31,7 +31,7 @@ type UpdateAlertStateCommand struct { NewState string `json:"newState" binding:"Required"` Info string `json:"info"` - Result *AlertRuleDAO + Result *AlertRuleModel } // Queries diff --git a/pkg/models/alerts_test.go b/pkg/models/alerts_test.go index af672ca6016..84d5c989b26 100644 --- a/pkg/models/alerts_test.go +++ b/pkg/models/alerts_test.go @@ -13,13 +13,13 @@ func TestAlertingModelTest(t *testing.T) { json1, _ := simplejson.NewJson([]byte(`{ "field": "value" }`)) json2, _ := simplejson.NewJson([]byte(`{ "field": "value" }`)) - rule1 := &AlertRuleDAO{ + rule1 := &AlertRuleModel{ Expression: json1, Name: "Namn", Description: "Description", } - rule2 := &AlertRuleDAO{ + rule2 := &AlertRuleModel{ Expression: json2, Name: "Namn", Description: "Description", diff --git a/pkg/services/alerting/dashboard_parser.go b/pkg/services/alerting/dashboard_parser.go index 01914e2b39a..88001435581 100644 --- a/pkg/services/alerting/dashboard_parser.go +++ b/pkg/services/alerting/dashboard_parser.go @@ -9,8 +9,8 @@ import ( m "github.com/grafana/grafana/pkg/models" ) -func ParseAlertsFromDashboard(cmd *m.SaveDashboardCommand) []*m.AlertRuleDAO { - alerts := make([]*m.AlertRuleDAO, 0) +func ParseAlertsFromDashboard(cmd *m.SaveDashboardCommand) []*m.AlertRuleModel { + alerts := make([]*m.AlertRuleModel, 0) for _, rowObj := range cmd.Dashboard.Get("rows").MustArray() { row := simplejson.NewFromAny(rowObj) @@ -19,7 +19,7 @@ func ParseAlertsFromDashboard(cmd *m.SaveDashboardCommand) []*m.AlertRuleDAO { panel := simplejson.NewFromAny(panelObj) alerting := panel.Get("alerting") - alert := &m.AlertRuleDAO{ + alert := &m.AlertRuleModel{ DashboardId: cmd.Result.Id, OrgId: cmd.Result.OrgId, PanelId: panel.Get("id").MustInt64(), @@ -70,7 +70,7 @@ func ParseAlertsFromDashboard(cmd *m.SaveDashboardCommand) []*m.AlertRuleDAO { alert.Expression = alerting - _, err := ParseAlertRulesFromAlertModel(alert) + _, err := ConvetAlertModelToAlertRule(alert) if err == nil && alert.ValidToSave() { alerts = append(alerts, alert) @@ -84,7 +84,7 @@ func ParseAlertsFromDashboard(cmd *m.SaveDashboardCommand) []*m.AlertRuleDAO { return alerts } -func ParseAlertRulesFromAlertModel(ruleDef *m.AlertRuleDAO) (*AlertRule, error) { +func ConvetAlertModelToAlertRule(ruleDef *m.AlertRuleModel) (*AlertRule, error) { model := &AlertRule{} model.Id = ruleDef.Id model.OrgId = ruleDef.OrgId diff --git a/pkg/services/alerting/executor_test.go b/pkg/services/alerting/executor_test.go index ccee2b80ba1..4da75bdcc16 100644 --- a/pkg/services/alerting/executor_test.go +++ b/pkg/services/alerting/executor_test.go @@ -35,16 +35,18 @@ func TestAlertingExecutor(t *testing.T) { So(result.State, ShouldEqual, alertstates.Critical) }) - Convey("Show return critical since sum is above 10", func() { - rule := &AlertRule{Critical: Level{Level: 10, Operator: ">"}} + /* + Convey("Show return critical since sum is above 10", func() { + rule := &AlertRule{Critical: Level{Level: 10, Operator: ">"}} - timeSeries := []*tsdb.TimeSeries{ - tsdb.NewTimeSeries("test1", [][2]float64{{9, 0}, {9, 0}}), - } + timeSeries := []*tsdb.TimeSeries{ + tsdb.NewTimeSeries("test1", [][2]float64{{9, 0}, {9, 0}}), + } - result := executor.evaluateRule(rule, timeSeries) - So(result.State, ShouldEqual, alertstates.Critical) - }) + result := executor.evaluateRule(rule, timeSeries) + So(result.State, ShouldEqual, alertstates.Critical) + }) + */ Convey("Show return ok since avg is below 10", func() { rule := &AlertRule{Critical: Level{Level: 10, Operator: ">"}} @@ -67,17 +69,18 @@ func TestAlertingExecutor(t *testing.T) { result := executor.evaluateRule(rule, timeSeries) So(result.State, ShouldEqual, alertstates.Ok) }) + /* + Convey("Show return ok since max is above 10", func() { + rule := &AlertRule{Critical: Level{Level: 10, Operator: ">"}} - Convey("Show return ok since max is above 10", func() { - rule := &AlertRule{Critical: Level{Level: 10, Operator: ">"}} + timeSeries := []*tsdb.TimeSeries{ + tsdb.NewTimeSeries("test1", [][2]float64{{1, 0}, {11, 0}}), + } - timeSeries := []*tsdb.TimeSeries{ - tsdb.NewTimeSeries("test1", [][2]float64{{1, 0}, {11, 0}}), - } - - result := executor.evaluateRule(rule, timeSeries) - So(result.State, ShouldEqual, alertstates.Critical) - }) + result := executor.evaluateRule(rule, timeSeries) + So(result.State, ShouldEqual, alertstates.Critical) + }) + */ }) Convey("muliple time series", func() { diff --git a/pkg/services/alerting/rule_reader.go b/pkg/services/alerting/rule_reader.go index 710e695903d..7f8f6b2c5de 100644 --- a/pkg/services/alerting/rule_reader.go +++ b/pkg/services/alerting/rule_reader.go @@ -49,7 +49,7 @@ func (arr *AlertRuleReader) Fetch() []*AlertRule { res := make([]*AlertRule, len(cmd.Result)) for i, ruleDef := range cmd.Result { - model, _ := ParseAlertRulesFromAlertModel(ruleDef) + model, _ := ConvetAlertModelToAlertRule(ruleDef) res[i] = model } diff --git a/pkg/services/sqlstore/alert_rule.go b/pkg/services/sqlstore/alert_rule.go index bff0a36fc4c..d4d4531b9d3 100644 --- a/pkg/services/sqlstore/alert_rule.go +++ b/pkg/services/sqlstore/alert_rule.go @@ -64,7 +64,7 @@ func HeartBeat(query *m.HeartBeatCommand) error { */ func GetAlertById(query *m.GetAlertByIdQuery) error { - alert := m.AlertRuleDAO{} + alert := m.AlertRuleModel{} has, err := x.Id(query.Id).Get(&alert) if !has { return fmt.Errorf("could not find alert") @@ -78,7 +78,7 @@ func GetAlertById(query *m.GetAlertByIdQuery) error { } func GetAllAlertQueryHandler(query *m.GetAllAlertsQuery) error { - var alerts []*m.AlertRuleDAO + var alerts []*m.AlertRuleModel err := x.Sql("select * from alert_rule").Find(&alerts) if err != nil { return err @@ -131,7 +131,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error { sql.WriteString(")") } - alerts := make([]*m.AlertRuleDAO, 0) + alerts := make([]*m.AlertRuleModel, 0) if err := x.Sql(sql.String(), params...).Find(&alerts); err != nil { return err } @@ -141,7 +141,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error { } func DeleteAlertDefinition(dashboardId int64, sess *xorm.Session) error { - alerts := make([]*m.AlertRuleDAO, 0) + alerts := make([]*m.AlertRuleModel, 0) sess.Where("dashboard_id = ?", dashboardId).Find(&alerts) for _, alert := range alerts { @@ -172,10 +172,10 @@ func SaveAlerts(cmd *m.SaveAlertsCommand) error { }) } -func upsertAlerts(alerts []*m.AlertRuleDAO, posted []*m.AlertRuleDAO, sess *xorm.Session) error { +func upsertAlerts(alerts []*m.AlertRuleModel, posted []*m.AlertRuleModel, sess *xorm.Session) error { for _, alert := range posted { update := false - var alertToUpdate *m.AlertRuleDAO + var alertToUpdate *m.AlertRuleModel for _, k := range alerts { if alert.PanelId == k.PanelId { @@ -212,7 +212,7 @@ func upsertAlerts(alerts []*m.AlertRuleDAO, posted []*m.AlertRuleDAO, sess *xorm return nil } -func deleteMissingAlerts(alerts []*m.AlertRuleDAO, posted []*m.AlertRuleDAO, sess *xorm.Session) error { +func deleteMissingAlerts(alerts []*m.AlertRuleModel, posted []*m.AlertRuleModel, sess *xorm.Session) error { for _, missingAlert := range alerts { missing := true @@ -238,12 +238,12 @@ func deleteMissingAlerts(alerts []*m.AlertRuleDAO, posted []*m.AlertRuleDAO, ses return nil } -func GetAlertsByDashboardId2(dashboardId int64, sess *xorm.Session) ([]*m.AlertRuleDAO, error) { - alerts := make([]*m.AlertRuleDAO, 0) +func GetAlertsByDashboardId2(dashboardId int64, sess *xorm.Session) ([]*m.AlertRuleModel, error) { + alerts := make([]*m.AlertRuleModel, 0) err := sess.Where("dashboard_id = ?", dashboardId).Find(&alerts) if err != nil { - return []*m.AlertRuleDAO{}, err + return []*m.AlertRuleModel{}, err } return alerts, nil diff --git a/pkg/services/sqlstore/alert_rule_changes.go b/pkg/services/sqlstore/alert_rule_changes.go index b07e7ebfda6..a78ff7ef723 100644 --- a/pkg/services/sqlstore/alert_rule_changes.go +++ b/pkg/services/sqlstore/alert_rule_changes.go @@ -48,7 +48,7 @@ func GetAlertRuleChanges(query *m.GetAlertChangesQuery) error { return nil } -func SaveAlertChange(change string, alert *m.AlertRuleDAO, sess *xorm.Session) error { +func SaveAlertChange(change string, alert *m.AlertRuleModel, sess *xorm.Session) error { _, err := sess.Insert(&m.AlertRuleChange{ OrgId: alert.OrgId, Type: change, diff --git a/pkg/services/sqlstore/alert_rule_changes_test.go b/pkg/services/sqlstore/alert_rule_changes_test.go index 6b0a8785dab..a7a4254964d 100644 --- a/pkg/services/sqlstore/alert_rule_changes_test.go +++ b/pkg/services/sqlstore/alert_rule_changes_test.go @@ -20,7 +20,7 @@ func TestAlertRuleChangesDataAccess(t *testing.T) { var err error Convey("When dashboard is removed", func() { - items := []*m.AlertRuleDAO{ + items := []*m.AlertRuleModel{ { PanelId: 1, DashboardId: testDash.Id, diff --git a/pkg/services/sqlstore/alert_rule_parser_test.go b/pkg/services/sqlstore/alert_rule_parser_test.go index 6028fba98f4..94da627fa25 100644 --- a/pkg/services/sqlstore/alert_rule_parser_test.go +++ b/pkg/services/sqlstore/alert_rule_parser_test.go @@ -12,7 +12,7 @@ import ( func TestAlertRuleModelParsing(t *testing.T) { Convey("Parsing alertRule from expression", t, func() { - alertRuleDAO := &m.AlertRuleDAO{} + alertRuleDAO := &m.AlertRuleModel{} json, _ := simplejson.NewJson([]byte(` { "frequency": 10, @@ -25,27 +25,27 @@ func TestAlertRuleModelParsing(t *testing.T) { "level": 20 }, "query": { - "queryRefId": "A", + "refId": "A", "from": "5m", "to": "now", "datasourceId": 1, "query": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" }, "transform": { - "name": "aggregation", + "type": "aggregation", "method": "avg" - }`)) + } + }`)) alertRuleDAO.Name = "Test" alertRuleDAO.Expression = json - rule, _ := alerting.ParseAlertRulesFromAlertModel(alertRuleDAO) + rule, _ := alerting.ConvetAlertModelToAlertRule(alertRuleDAO) Convey("Confirm that all properties are set", func() { So(rule.Query.Query, ShouldEqual, "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)") So(rule.Query.From, ShouldEqual, "5m") So(rule.Query.To, ShouldEqual, "now") So(rule.Query.DatasourceId, ShouldEqual, 1) - //So(rule.ValueQuery.Aggregator, ShouldEqual, "avg") So(rule.Warning.Level, ShouldEqual, 10) So(rule.Warning.Operator, ShouldEqual, ">") So(rule.Critical.Level, ShouldEqual, 20) diff --git a/pkg/services/sqlstore/alert_rule_test.go b/pkg/services/sqlstore/alert_rule_test.go index 0e1127591d5..8baf0428025 100644 --- a/pkg/services/sqlstore/alert_rule_test.go +++ b/pkg/services/sqlstore/alert_rule_test.go @@ -14,7 +14,7 @@ func TestAlertingDataAccess(t *testing.T) { testDash := insertTestDashboard("dashboard with alerts", 1, "alert") - items := []*m.AlertRuleDAO{ + items := []*m.AlertRuleModel{ { PanelId: 1, DashboardId: testDash.Id, @@ -96,7 +96,7 @@ func TestAlertingDataAccess(t *testing.T) { }) Convey("Multiple alerts per dashboard", func() { - multipleItems := []*m.AlertRuleDAO{ + multipleItems := []*m.AlertRuleModel{ { DashboardId: testDash.Id, PanelId: 1, @@ -161,7 +161,7 @@ func TestAlertingDataAccess(t *testing.T) { }) Convey("When dashboard is removed", func() { - items := []*m.AlertRuleDAO{ + items := []*m.AlertRuleModel{ { PanelId: 1, DashboardId: testDash.Id, diff --git a/pkg/services/sqlstore/alert_state.go b/pkg/services/sqlstore/alert_state.go index 207e2f20385..6c72adfb3b2 100644 --- a/pkg/services/sqlstore/alert_state.go +++ b/pkg/services/sqlstore/alert_state.go @@ -19,7 +19,7 @@ func SetNewAlertState(cmd *m.UpdateAlertStateCommand) error { return fmt.Errorf("new state is invalid") } - alert := m.AlertRuleDAO{} + alert := m.AlertRuleModel{} has, err := sess.Id(cmd.AlertId).Get(&alert) if !has { return fmt.Errorf("Could not find alert") diff --git a/pkg/services/sqlstore/alert_state_test.go b/pkg/services/sqlstore/alert_state_test.go index 7820b06525f..a07b68e0447 100644 --- a/pkg/services/sqlstore/alert_state_test.go +++ b/pkg/services/sqlstore/alert_state_test.go @@ -13,7 +13,7 @@ func TestAlertingStateAccess(t *testing.T) { testDash := insertTestDashboard("dashboard with alerts", 1, "alert") - items := []*m.AlertRuleDAO{ + items := []*m.AlertRuleModel{ { PanelId: 1, DashboardId: testDash.Id, diff --git a/pkg/services/sqlstore/dashboard_parser_test.go b/pkg/services/sqlstore/dashboard_parser_test.go index d8dc22b68c6..b7266a926c8 100644 --- a/pkg/services/sqlstore/dashboard_parser_test.go +++ b/pkg/services/sqlstore/dashboard_parser_test.go @@ -37,44 +37,26 @@ func TestAlertModelParsing(t *testing.T) { ], "datasource": null, "alerting": { - "name": "alert name", + "name": "Alerting Panel Title alert", "description": "description", - "frequency": 10, - "warning": { - "op": ">", - "level": 10 - }, "critical": { - "op": ">", - "level": 20 + "level": 20, + "op": ">" }, - "function": "static", - "valueQuery": { - "queryRefId": "A", + "frequency": 10, + "query": { "from": "5m", - "to": "now", - "agg": "avg", - "params": [ - "#A", - "5m", - "now", - "avg" - ] + "refId": "A", + "to": "now" }, - "evalQuery": { - "queryRefId": "A", - "from": "5m", - "to": "now", - "agg": "avg", - "params": [ - "#A", - "5m", - "now", - "avg" - ] + "transform": { + "method": "avg", + "name": "aggregation" }, - "evalStringParam1": "", - "name": "Alerting Panel Title alert" + "warning": { + "level": 10, + "op": ">" + } } }, { @@ -88,44 +70,26 @@ func TestAlertModelParsing(t *testing.T) { ], "datasource": "graphite2", "alerting": { - "name": "alert name", + "name": "Alerting Panel Title alert", "description": "description", - "frequency": 10, - "warning": { - "op": ">", - "level": 10 - }, "critical": { - "op": ">", - "level": 20 + "level": 20, + "op": ">" }, - "function": "static", - "valueQuery": { - "queryRefId": "A", + "frequency": 10, + "query": { "from": "5m", - "to": "now", - "agg": "avg", - "params": [ - "#A", - "5m", - "now", - "avg" - ] + "refId": "A", + "to": "now" }, - "evalQuery": { - "queryRefId": "A", - "from": "5m", - "to": "now", - "agg": "avg", - "params": [ - "#A", - "5m", - "now", - "avg" - ] + "transform": { + "method": "avg", + "name": "aggregation" }, - "evalStringParam1": "", - "name": "Alerting Panel Title alert" + "warning": { + "level": 10, + "op": ">" + } } } ], From 48cbeb96bf0a65d506a6a14637de79ee53b6ad47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 10 Jun 2016 14:49:54 +0200 Subject: [PATCH 20/37] feat(alerting): updated rule model and UI --- .../app/plugins/panel/graph/alert_tab_ctrl.ts | 123 ++++++++++-------- .../panel/graph/partials/tab_alerting.html | 32 ++--- 2 files changed, 89 insertions(+), 66 deletions(-) diff --git a/public/app/plugins/panel/graph/alert_tab_ctrl.ts b/public/app/plugins/panel/graph/alert_tab_ctrl.ts index 1b1f052632c..65bfa159d9d 100644 --- a/public/app/plugins/panel/graph/alert_tab_ctrl.ts +++ b/public/app/plugins/panel/graph/alert_tab_ctrl.ts @@ -15,7 +15,6 @@ var alertQueryDef = new QueryPartDef({ {name: "queryRefId", type: 'string', options: ['#A', '#B', '#C', '#D']}, {name: "from", type: "string", options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h']}, {name: "to", type: "string", options: ['now']}, - {name: "aggregation", type: "select", options: ['sum', 'avg', 'min', 'max', 'last']}, ], defaultParams: ['#A', '5m', 'now', 'avg'] }); @@ -25,46 +24,44 @@ export class AlertTabCtrl { panelCtrl: any; alerting: any; metricTargets = [{ refId: '- select query -' } ]; - evalFuncs = [ + transforms = [ { - text: 'Static Threshold', - value: 'static', + text: 'Aggregation', + type: 'aggregation', }, { - text: 'Percent Change Compared To', - value: 'percent_change', - secondParam: "query", + text: 'Linear Forecast', + type: 'forecast', }, { - text: 'Forcast', - value: 'forcast', - secondParam: "duration", - } + text: 'Percent Change', + type: 'percent_change', + }, + { + text: 'Query diff', + type: 'query_diff', + }, ]; - aggregators = ['avg', 'sum', 'min', 'max', 'median']; + aggregators = ['avg', 'sum', 'min', 'max', 'last']; rule: any; - valueQuery: any; - evalQuery: any; - secondParam: any; + query: any; + queryParams: any; + transformDef: any; + trasnformQuery: any; defaultValues = { frequency: 10, - warning: { op: '>', level: 10 }, - critical: { op: '>', level: 20 }, - function: 'static', - valueQuery: { - queryRefId: 'A', + warning: { op: '>', level: undefined }, + critical: { op: '>', level: undefined }, + query: { + refId: 'A', from: '5m', to: 'now', - agg: 'avg', }, - evalQuery: { - queryRefId: 'A', - from: '5m', - to: 'now', - agg: 'avg', + transform: { + type: 'aggregation', + method: 'avg', }, - evalStringParam1: '', }; /** @ngInject */ @@ -73,53 +70,77 @@ export class AlertTabCtrl { this.panel = this.panelCtrl.panel; $scope.ctrl = this; - _.defaults(this.panel.alerting, this.defaultValues); - this.rule = this.panel.alerting; + this.metricTargets = this.panel.targets.map(val => val); + this.rule = this.panel.alerting = this.panel.alerting || {}; - this.valueQuery = new QueryPart(this.rule.valueQuery, alertQueryDef); - this.evalQuery = new QueryPart(this.rule.evalQuery, alertQueryDef); + // set defaults + _.defaults(this.rule, this.defaultValues); var defaultName = (this.panelCtrl.dashboard.title + ' ' + this.panel.title + ' alert'); - this.panel.alerting.name = this.panel.alerting.name || defaultName; - this.panel.alerting.description = this.panel.alerting.description || defaultName; + this.rule.name = this.rule.name || defaultName; + this.rule.description = this.rule.description || defaultName; + this.rule.queryRef = this.panel.alerting.queryRef || this.metricTargets[0].refId; - this.panel.targets.map(target => { - this.metricTargets.push(target); - }); + // great temp working model + this.queryParams = { + params: [ + this.rule.query.refId, + this.rule.query.from, + this.rule.query.to + ] + }; - this.panel.alerting.queryRef = this.panel.alerting.queryRef || this.metricTargets[0].refId; + // init the query part components model + this.query = new QueryPart(this.queryParams, alertQueryDef); this.convertThresholdsToAlertThresholds(); - this.evalFuncChanged(); + this.transformDef = _.findWhere(this.transforms, {type: this.rule.transform.type}); } - evalFuncChanged() { - var evalFuncDef = _.findWhere(this.evalFuncs, { value: this.rule.evalFunc }); - console.log(evalFuncDef); - this.secondParam = evalFuncDef.secondParam; + queryUpdated() { + this.rule.query = { + refId: this.query.params[0], + from: this.query.params[1], + to: this.query.params[2], + }; + } + + transformChanged() { + // clear model + this.rule.transform = {type: this.rule.transform.type}; + this.transformDef = _.findWhere(this.transforms, {type: this.rule.transform.type}); + + switch (this.rule.transform.type) { + case 'aggregation': { + this.rule.transform.method = 'avg'; + break; + } + case "forecast": { + this.rule.transform.timespan = '7d'; + break; + } + } } convertThresholdsToAlertThresholds() { if (this.panel.grid && this.panel.grid.threshold1 - && this.panel.alerting.warnLevel === undefined + && this.rule.warnLevel === undefined ) { - this.panel.alerting.warnOperator = '>'; - this.panel.alerting.warnLevel = this.panel.grid.threshold1; + this.rule.warning.op = '>'; + this.rule.warning.level = this.panel.grid.threshold1; } if (this.panel.grid && this.panel.grid.threshold2 - && this.panel.alerting.critLevel === undefined + && this.rule.critical.level === undefined ) { - this.panel.alerting.critOperator = '>'; - this.panel.alerting.critLevel = this.panel.grid.threshold2; + this.rule.critical.op = '>'; + this.rule.critical.level = this.panel.grid.threshold2; } } markAsDeleted() { - if (this.panel.alerting) { - this.panel.alerting = this.defaultValues; - } + this.panel.alerting = this.defaultValues; } thresholdsUpdated() { diff --git a/public/app/plugins/panel/graph/partials/tab_alerting.html b/public/app/plugins/panel/graph/partials/tab_alerting.html index ce0531b2f12..ab688bb003e 100644 --- a/public/app/plugins/panel/graph/partials/tab_alerting.html +++ b/public/app/plugins/panel/graph/partials/tab_alerting.html @@ -6,32 +6,34 @@ + part="ctrl.query" + part-updated="ctrl.queryUpdated()">
- Evaluate Against + Transform using
-
- - +
+ Method +
+ +
-
- Duration - +
+ Timespan +
From 83c422e6ef034c71ec7a9565f8c30949e881c42e Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 10 Jun 2016 15:30:38 +0200 Subject: [PATCH 21/37] feat(alerting): implement transform objects --- pkg/services/alerting/dashboard_parser.go | 6 ++ pkg/services/alerting/evaluator.go | 15 ++++ pkg/services/alerting/executor.go | 84 +++-------------------- pkg/services/alerting/executor_test.go | 30 ++++++-- pkg/services/alerting/models.go | 6 +- pkg/services/alerting/transformer.go | 73 ++++++++++++++++++++ 6 files changed, 128 insertions(+), 86 deletions(-) create mode 100644 pkg/services/alerting/evaluator.go create mode 100644 pkg/services/alerting/transformer.go diff --git a/pkg/services/alerting/dashboard_parser.go b/pkg/services/alerting/dashboard_parser.go index 88001435581..c3c9b9b643b 100644 --- a/pkg/services/alerting/dashboard_parser.go +++ b/pkg/services/alerting/dashboard_parser.go @@ -108,6 +108,12 @@ func ConvetAlertModelToAlertRule(ruleDef *m.AlertRuleModel) (*AlertRule, error) model.Transform = ruleDef.Expression.Get("transform").Get("type").MustString() model.TransformParams = *ruleDef.Expression.Get("transform") + if model.Transform == "aggregation" { + model.Transformer = &AggregationTransformer{ + Method: ruleDef.Expression.Get("transform").Get("method").MustString(), + } + } + query := ruleDef.Expression.Get("query") model.Query = AlertQuery{ Query: query.Get("query").MustString(), diff --git a/pkg/services/alerting/evaluator.go b/pkg/services/alerting/evaluator.go new file mode 100644 index 00000000000..efa7231b435 --- /dev/null +++ b/pkg/services/alerting/evaluator.go @@ -0,0 +1,15 @@ +package alerting + +type compareFn func(float64, float64) bool + +func evalCondition(level Level, result float64) bool { + return operators[level.Operator](result, level.Level) +} + +var operators = map[string]compareFn{ + ">": func(num1, num2 float64) bool { return num1 > num2 }, + ">=": func(num1, num2 float64) bool { return num1 >= num2 }, + "<": func(num1, num2 float64) bool { return num1 < num2 }, + "<=": func(num1, num2 float64) bool { return num1 <= num2 }, + "": func(num1, num2 float64) bool { return false }, +} diff --git a/pkg/services/alerting/executor.go b/pkg/services/alerting/executor.go index cfdc18edb2c..1520bc494c0 100644 --- a/pkg/services/alerting/executor.go +++ b/pkg/services/alerting/executor.go @@ -3,8 +3,6 @@ package alerting import ( "fmt" - "math" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" @@ -26,63 +24,6 @@ func NewExecutor() *ExecutorImpl { } } -type compareFn func(float64, float64) bool -type aggregationFn func(*tsdb.TimeSeries) float64 - -var operators = map[string]compareFn{ - ">": func(num1, num2 float64) bool { return num1 > num2 }, - ">=": func(num1, num2 float64) bool { return num1 >= num2 }, - "<": func(num1, num2 float64) bool { return num1 < num2 }, - "<=": func(num1, num2 float64) bool { return num1 <= num2 }, - "": func(num1, num2 float64) bool { return false }, -} -var aggregator = map[string]aggregationFn{ - "avg": func(series *tsdb.TimeSeries) float64 { - sum := float64(0) - - for _, v := range series.Points { - sum += v[0] - } - - return sum / float64(len(series.Points)) - }, - "sum": func(series *tsdb.TimeSeries) float64 { - sum := float64(0) - - for _, v := range series.Points { - sum += v[0] - } - - return sum - }, - "min": func(series *tsdb.TimeSeries) float64 { - min := series.Points[0][0] - - for _, v := range series.Points { - if v[0] < min { - min = v[0] - } - } - - return min - }, - "max": func(series *tsdb.TimeSeries) float64 { - max := series.Points[0][0] - - for _, v := range series.Points { - if v[0] > max { - max = v[0] - } - } - - return max - }, - "mean": func(series *tsdb.TimeSeries) float64 { - midPosition := int64(math.Floor(float64(len(series.Points)) / float64(2))) - return series.Points[midPosition][0] - }, -} - func (e *ExecutorImpl) Execute(job *AlertJob, resultQueue chan *AlertResult) { timeSeries, err := e.executeQuery(job) if err != nil { @@ -156,32 +97,25 @@ func (e *ExecutorImpl) evaluateRule(rule *AlertRule, series tsdb.TimeSeriesSlice for _, serie := range series { e.log.Debug("Evaluating series", "series", serie.Name) + transformedValue, _ := rule.Transformer.Transform(serie) - if aggregator["avg"] == nil { - continue - } - - var aggValue = aggregator["avg"](serie) - var critOperartor = operators[rule.Critical.Operator] - var critResult = critOperartor(aggValue, rule.Critical.Level) - - e.log.Debug("Alert execution Crit", "name", serie.Name, "aggValue", aggValue, "operator", rule.Critical.Operator, "level", rule.Critical.Level, "result", critResult) + critResult := evalCondition(rule.Critical, transformedValue) + e.log.Debug("Alert execution Crit", "name", serie.Name, "transformedValue", transformedValue, "operator", rule.Critical.Operator, "level", rule.Critical.Level, "result", critResult) if critResult { return &AlertResult{ State: alertstates.Critical, - ActualValue: aggValue, - Description: fmt.Sprintf(descriptionFmt, aggValue, serie.Name), + ActualValue: transformedValue, + Description: fmt.Sprintf(descriptionFmt, transformedValue, serie.Name), } } - var warnOperartor = operators[rule.Warning.Operator] - var warnResult = warnOperartor(aggValue, rule.Warning.Level) - e.log.Debug("Alert execution Warn", "name", serie.Name, "aggValue", aggValue, "operator", rule.Warning.Operator, "level", rule.Warning.Level, "result", warnResult) + warnResult := evalCondition(rule.Warning, transformedValue) + e.log.Debug("Alert execution Warn", "name", serie.Name, "transformedValue", transformedValue, "operator", rule.Warning.Operator, "level", rule.Warning.Level, "result", warnResult) if warnResult { return &AlertResult{ State: alertstates.Warn, - Description: fmt.Sprintf(descriptionFmt, aggValue, serie.Name), - ActualValue: aggValue, + Description: fmt.Sprintf(descriptionFmt, transformedValue, serie.Name), + ActualValue: transformedValue, } } } diff --git a/pkg/services/alerting/executor_test.go b/pkg/services/alerting/executor_test.go index 4da75bdcc16..70074bbcf60 100644 --- a/pkg/services/alerting/executor_test.go +++ b/pkg/services/alerting/executor_test.go @@ -14,7 +14,10 @@ func TestAlertingExecutor(t *testing.T) { Convey("single time serie", func() { Convey("Show return ok since avg is above 2", func() { - rule := &AlertRule{Critical: Level{Level: 10, Operator: ">"}} + rule := &AlertRule{ + Critical: Level{Level: 10, Operator: ">"}, + Transformer: &AggregationTransformer{Method: "avg"}, + } timeSeries := []*tsdb.TimeSeries{ tsdb.NewTimeSeries("test1", [][2]float64{{2, 0}}), @@ -25,7 +28,10 @@ func TestAlertingExecutor(t *testing.T) { }) Convey("Show return critical since below 2", func() { - rule := &AlertRule{Critical: Level{Level: 10, Operator: "<"}} + rule := &AlertRule{ + Critical: Level{Level: 10, Operator: "<"}, + Transformer: &AggregationTransformer{Method: "avg"}, + } timeSeries := []*tsdb.TimeSeries{ tsdb.NewTimeSeries("test1", [][2]float64{{2, 0}}), @@ -49,7 +55,10 @@ func TestAlertingExecutor(t *testing.T) { */ Convey("Show return ok since avg is below 10", func() { - rule := &AlertRule{Critical: Level{Level: 10, Operator: ">"}} + rule := &AlertRule{ + Critical: Level{Level: 10, Operator: ">"}, + Transformer: &AggregationTransformer{Method: "avg"}, + } timeSeries := []*tsdb.TimeSeries{ tsdb.NewTimeSeries("test1", [][2]float64{{9, 0}, {9, 0}}), @@ -60,7 +69,10 @@ func TestAlertingExecutor(t *testing.T) { }) Convey("Show return ok since min is below 10", func() { - rule := &AlertRule{Critical: Level{Level: 10, Operator: ">"}} + rule := &AlertRule{ + Critical: Level{Level: 10, Operator: ">"}, + Transformer: &AggregationTransformer{Method: "avg"}, + } timeSeries := []*tsdb.TimeSeries{ tsdb.NewTimeSeries("test1", [][2]float64{{11, 0}, {9, 0}}), @@ -85,7 +97,10 @@ func TestAlertingExecutor(t *testing.T) { Convey("muliple time series", func() { Convey("both are ok", func() { - rule := &AlertRule{Critical: Level{Level: 10, Operator: ">"}} + rule := &AlertRule{ + Critical: Level{Level: 10, Operator: ">"}, + Transformer: &AggregationTransformer{Method: "avg"}, + } timeSeries := []*tsdb.TimeSeries{ tsdb.NewTimeSeries("test1", [][2]float64{{2, 0}}), @@ -97,7 +112,10 @@ func TestAlertingExecutor(t *testing.T) { }) Convey("first serie is good, second is critical", func() { - rule := &AlertRule{Critical: Level{Level: 10, Operator: ">"}} + rule := &AlertRule{ + Critical: Level{Level: 10, Operator: ">"}, + Transformer: &AggregationTransformer{Method: "avg"}, + } timeSeries := []*tsdb.TimeSeries{ tsdb.NewTimeSeries("test1", [][2]float64{{2, 0}}), diff --git a/pkg/services/alerting/models.go b/pkg/services/alerting/models.go index 4f3796d69ec..c13669ea3d7 100644 --- a/pkg/services/alerting/models.go +++ b/pkg/services/alerting/models.go @@ -2,7 +2,6 @@ package alerting import ( "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/tsdb" ) type AlertJob struct { @@ -36,10 +35,7 @@ type AlertRule struct { Query AlertQuery Transform string TransformParams simplejson.Json -} - -type Transformer interface { - Transform(tsdb tsdb.TimeSeriesSlice) float64 + Transformer Transformer } type Level struct { diff --git a/pkg/services/alerting/transformer.go b/pkg/services/alerting/transformer.go new file mode 100644 index 00000000000..1f574e6fce6 --- /dev/null +++ b/pkg/services/alerting/transformer.go @@ -0,0 +1,73 @@ +package alerting + +import ( + "fmt" + "math" + + "github.com/grafana/grafana/pkg/tsdb" +) + +type Transformer interface { + Transform(timeserie *tsdb.TimeSeries) (float64, error) +} + +type AggregationTransformer struct { + Method string +} + +func (at *AggregationTransformer) Transform(timeserie *tsdb.TimeSeries) (float64, error) { + + if at.Method == "avg" { + sum := float64(0) + for _, point := range timeserie.Points { + sum += point[0] + } + + return sum / float64(len(timeserie.Points)), nil + } + + //"sum": func(series *tsdb.TimeSeries) float64 { + if at.Method == "sum" { + sum := float64(0) + + for _, v := range timeserie.Points { + sum += v[0] + } + + return sum, nil + } + + //"min": func(series *tsdb.TimeSeries) float64 { + if at.Method == "min" { + min := timeserie.Points[0][0] + + for _, v := range timeserie.Points { + if v[0] < min { + min = v[0] + } + } + + return min, nil + } + + //"max": func(series *tsdb.TimeSeries) float64 { + if at.Method == "max" { + max := timeserie.Points[0][0] + + for _, v := range timeserie.Points { + if v[0] > max { + max = v[0] + } + } + + return max, nil + } + + //"mean": func(series *tsdb.TimeSeries) float64 { + if at.Method == "mean" { + midPosition := int64(math.Floor(float64(len(timeserie.Points)) / float64(2))) + return timeserie.Points[midPosition][0], nil + } + + return float64(0), fmt.Errorf("Missing method") +} From 1fa9ae810b4c79286454a02e1f599234c775902f Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 10 Jun 2016 15:49:23 +0200 Subject: [PATCH 22/37] test(alerting): enable disabled tests --- pkg/services/alerting/executor_test.go | 44 ++++++++++++++------------ 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/pkg/services/alerting/executor_test.go b/pkg/services/alerting/executor_test.go index 70074bbcf60..753f5dd244c 100644 --- a/pkg/services/alerting/executor_test.go +++ b/pkg/services/alerting/executor_test.go @@ -41,18 +41,19 @@ func TestAlertingExecutor(t *testing.T) { So(result.State, ShouldEqual, alertstates.Critical) }) - /* - Convey("Show return critical since sum is above 10", func() { - rule := &AlertRule{Critical: Level{Level: 10, Operator: ">"}} + Convey("Show return critical since sum is above 10", func() { + rule := &AlertRule{ + Critical: Level{Level: 10, Operator: ">"}, + Transformer: &AggregationTransformer{Method: "sum"}, + } - timeSeries := []*tsdb.TimeSeries{ - tsdb.NewTimeSeries("test1", [][2]float64{{9, 0}, {9, 0}}), - } + timeSeries := []*tsdb.TimeSeries{ + tsdb.NewTimeSeries("test1", [][2]float64{{9, 0}, {9, 0}}), + } - result := executor.evaluateRule(rule, timeSeries) - So(result.State, ShouldEqual, alertstates.Critical) - }) - */ + result := executor.evaluateRule(rule, timeSeries) + So(result.State, ShouldEqual, alertstates.Critical) + }) Convey("Show return ok since avg is below 10", func() { rule := &AlertRule{ @@ -81,18 +82,21 @@ func TestAlertingExecutor(t *testing.T) { result := executor.evaluateRule(rule, timeSeries) So(result.State, ShouldEqual, alertstates.Ok) }) - /* - Convey("Show return ok since max is above 10", func() { - rule := &AlertRule{Critical: Level{Level: 10, Operator: ">"}} - timeSeries := []*tsdb.TimeSeries{ - tsdb.NewTimeSeries("test1", [][2]float64{{1, 0}, {11, 0}}), - } + Convey("Show return ok since max is above 10", func() { + rule := &AlertRule{ + Critical: Level{Level: 10, Operator: ">"}, + Transformer: &AggregationTransformer{Method: "max"}, + } + + timeSeries := []*tsdb.TimeSeries{ + tsdb.NewTimeSeries("test1", [][2]float64{{1, 0}, {11, 0}}), + } + + result := executor.evaluateRule(rule, timeSeries) + So(result.State, ShouldEqual, alertstates.Critical) + }) - result := executor.evaluateRule(rule, timeSeries) - So(result.State, ShouldEqual, alertstates.Critical) - }) - */ }) Convey("muliple time series", func() { From 2b4a9954b14ecbd37e511f4abe9431fd49a5d124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 11 Jun 2016 10:13:33 +0200 Subject: [PATCH 23/37] feat(alerting): progress on alerting UI and model, refactoring of dashboard parser and tests into extractor component, moved tests from sqlstore to alerting package --- pkg/api/dashboard.go | 12 +- pkg/models/alerts.go | 7 +- pkg/services/alerting/alert_rule.go | 76 ++++++++++ pkg/services/alerting/commands.go | 89 ++++++++++++ pkg/services/alerting/dashboard_parser.go | 135 ------------------ pkg/services/alerting/extractor.go | 120 ++++++++++++++++ .../extractor_test.go} | 106 ++++++++------ pkg/services/alerting/models.go | 21 --- pkg/services/sqlstore/migrations/alert_mig.go | 3 + .../app/plugins/panel/graph/alert_tab_ctrl.ts | 34 +++-- .../panel/graph/partials/tab_alerting.html | 101 +++++++------ public/sass/components/_tagsinput.scss | 1 + .../vendor/tagsinput/bootstrap-tagsinput.js | 2 +- 13 files changed, 439 insertions(+), 268 deletions(-) create mode 100644 pkg/services/alerting/alert_rule.go create mode 100644 pkg/services/alerting/commands.go delete mode 100644 pkg/services/alerting/dashboard_parser.go create mode 100644 pkg/services/alerting/extractor.go rename pkg/services/{sqlstore/dashboard_parser_test.go => alerting/extractor_test.go} (63%) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 7b5c1f9f764..3ca7a406832 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -151,15 +151,13 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) { } if setting.AlertingEnabled { - saveAlertCommand := m.SaveAlertsCommand{ - DashboardId: cmd.Result.Id, - OrgId: c.OrgId, - UserId: c.UserId, - Alerts: alerting.ParseAlertsFromDashboard(&cmd), + alertCmd := alerting.UpdateDashboardAlertsCommand{ + OrgId: c.OrgId, + UserId: c.UserId, + Dashboard: cmd.Result, } - err = bus.Dispatch(&saveAlertCommand) - if err != nil { + if err := bus.Dispatch(&alertCmd); err != nil { c.JsonApiErr(500, "Failed to save alerts", err) return } diff --git a/pkg/models/alerts.go b/pkg/models/alerts.go index e5cc76d198c..2ba21fd17cf 100644 --- a/pkg/models/alerts.go +++ b/pkg/models/alerts.go @@ -14,6 +14,9 @@ type AlertRuleModel struct { Name string Description string State string + Scheduler int64 + Enabled bool + Frequency int Created time.Time Updated time.Time @@ -21,6 +24,8 @@ type AlertRuleModel struct { Expression *simplejson.Json } +type AlertRules []*AlertRuleModel + func (this AlertRuleModel) TableName() string { return "alert_rule" } @@ -83,7 +88,7 @@ type SaveAlertsCommand struct { UserId int64 OrgId int64 - Alerts []*AlertRuleModel + Alerts AlertRules } type DeleteAlertCommand struct { diff --git a/pkg/services/alerting/alert_rule.go b/pkg/services/alerting/alert_rule.go new file mode 100644 index 00000000000..867f88861e9 --- /dev/null +++ b/pkg/services/alerting/alert_rule.go @@ -0,0 +1,76 @@ +package alerting + +import ( + "fmt" + + "github.com/grafana/grafana/pkg/components/simplejson" + + m "github.com/grafana/grafana/pkg/models" +) + +type AlertRule struct { + Id int64 + OrgId int64 + DashboardId int64 + PanelId int64 + Frequency int64 + Name string + Description string + State string + Warning Level + Critical Level + Query AlertQuery + Transform string + TransformParams simplejson.Json + Transformer Transformer +} + +func NewAlertRuleFromDBModel(ruleDef *m.AlertRuleModel) (*AlertRule, error) { + model := &AlertRule{} + model.Id = ruleDef.Id + model.OrgId = ruleDef.OrgId + model.Name = ruleDef.Name + model.Description = ruleDef.Description + model.State = ruleDef.State + + critical := ruleDef.Expression.Get("critical") + model.Critical = Level{ + Operator: critical.Get("op").MustString(), + Level: critical.Get("level").MustFloat64(), + } + + warning := ruleDef.Expression.Get("warning") + model.Warning = Level{ + Operator: warning.Get("op").MustString(), + Level: warning.Get("level").MustFloat64(), + } + + model.Frequency = ruleDef.Expression.Get("frequency").MustInt64() + model.Transform = ruleDef.Expression.Get("transform").Get("type").MustString() + model.TransformParams = *ruleDef.Expression.Get("transform") + + if model.Transform == "aggregation" { + model.Transformer = &AggregationTransformer{ + Method: ruleDef.Expression.Get("transform").Get("method").MustString(), + } + } + + query := ruleDef.Expression.Get("query") + model.Query = AlertQuery{ + Query: query.Get("query").MustString(), + DatasourceId: query.Get("datasourceId").MustInt64(), + From: query.Get("from").MustString(), + To: query.Get("to").MustString(), + Aggregator: query.Get("agg").MustString(), + } + + if model.Query.Query == "" { + return nil, fmt.Errorf("missing query.query") + } + + if model.Query.DatasourceId == 0 { + return nil, fmt.Errorf("missing query.datasourceId") + } + + return model, nil +} diff --git a/pkg/services/alerting/commands.go b/pkg/services/alerting/commands.go new file mode 100644 index 00000000000..83d906f41fb --- /dev/null +++ b/pkg/services/alerting/commands.go @@ -0,0 +1,89 @@ +package alerting + +import ( + "fmt" + + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" +) + +type UpdateDashboardAlertsCommand struct { + UserId int64 + OrgId int64 + Dashboard *m.Dashboard +} + +func init() { + bus.AddHandler("alerting", updateDashboardAlerts) +} + +func updateDashboardAlerts(cmd *UpdateDashboardAlertsCommand) error { + saveRulesCmd := m.SaveAlertsCommand{ + OrgId: cmd.OrgId, + UserId: cmd.UserId, + } + + extractor := NewAlertRuleExtractor(cmd.Dashboard, cmd.OrgId) + + rules, err := extractor.GetRuleModels() + if err != nil { + return err + } + + saveRulesCmd.Alerts = rules + if bus.Dispatch(&saveRulesCmd); err != nil { + return err + } + + return nil +} + +func ConvetAlertModelToAlertRule(ruleDef *m.AlertRuleModel) (*AlertRule, error) { + model := &AlertRule{} + model.Id = ruleDef.Id + model.OrgId = ruleDef.OrgId + model.Name = ruleDef.Name + model.Description = ruleDef.Description + model.State = ruleDef.State + + critical := ruleDef.Expression.Get("critical") + model.Critical = Level{ + Operator: critical.Get("op").MustString(), + Level: critical.Get("level").MustFloat64(), + } + + warning := ruleDef.Expression.Get("warning") + model.Warning = Level{ + Operator: warning.Get("op").MustString(), + Level: warning.Get("level").MustFloat64(), + } + + model.Frequency = ruleDef.Expression.Get("frequency").MustInt64() + model.Transform = ruleDef.Expression.Get("transform").Get("type").MustString() + model.TransformParams = *ruleDef.Expression.Get("transform") + + if model.Transform == "aggregation" { + model.Transformer = &AggregationTransformer{ + Method: ruleDef.Expression.Get("transform").Get("method").MustString(), + } + } + + query := ruleDef.Expression.Get("query") + model.Query = AlertQuery{ + Query: query.Get("query").MustString(), + DatasourceId: query.Get("datasourceId").MustInt64(), + From: query.Get("from").MustString(), + To: query.Get("to").MustString(), + Aggregator: query.Get("agg").MustString(), + } + + if model.Query.Query == "" { + return nil, fmt.Errorf("missing query.query") + } + + if model.Query.DatasourceId == 0 { + return nil, fmt.Errorf("missing query.datasourceId") + } + + return model, nil +} diff --git a/pkg/services/alerting/dashboard_parser.go b/pkg/services/alerting/dashboard_parser.go deleted file mode 100644 index c3c9b9b643b..00000000000 --- a/pkg/services/alerting/dashboard_parser.go +++ /dev/null @@ -1,135 +0,0 @@ -package alerting - -import ( - "fmt" - - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/log" - m "github.com/grafana/grafana/pkg/models" -) - -func ParseAlertsFromDashboard(cmd *m.SaveDashboardCommand) []*m.AlertRuleModel { - alerts := make([]*m.AlertRuleModel, 0) - - for _, rowObj := range cmd.Dashboard.Get("rows").MustArray() { - row := simplejson.NewFromAny(rowObj) - - for _, panelObj := range row.Get("panels").MustArray() { - panel := simplejson.NewFromAny(panelObj) - - alerting := panel.Get("alerting") - alert := &m.AlertRuleModel{ - DashboardId: cmd.Result.Id, - OrgId: cmd.Result.OrgId, - PanelId: panel.Get("id").MustInt64(), - Id: alerting.Get("id").MustInt64(), - Name: alerting.Get("name").MustString(), - Description: alerting.Get("description").MustString(), - } - - log.Info("Alertrule: %v", alert.Name) - - valueQuery := alerting.Get("query") - valueQueryRef := valueQuery.Get("refId").MustString() - for _, targetsObj := range panel.Get("targets").MustArray() { - target := simplejson.NewFromAny(targetsObj) - - if target.Get("refId").MustString() == valueQueryRef { - datsourceName := "" - if target.Get("datasource").MustString() != "" { - datsourceName = target.Get("datasource").MustString() - } else if panel.Get("datasource").MustString() != "" { - datsourceName = panel.Get("datasource").MustString() - } - - if datsourceName == "" { - query := &m.GetDataSourcesQuery{OrgId: cmd.OrgId} - if err := bus.Dispatch(query); err == nil { - for _, ds := range query.Result { - if ds.IsDefault { - alerting.SetPath([]string{"query", "datasourceId"}, ds.Id) - } - } - } - } else { - query := &m.GetDataSourceByNameQuery{ - Name: panel.Get("datasource").MustString(), - OrgId: cmd.OrgId, - } - bus.Dispatch(query) - alerting.SetPath([]string{"query", "datasourceId"}, query.Result.Id) - } - - targetQuery := target.Get("target").MustString() - if targetQuery != "" { - alerting.SetPath([]string{"query", "query"}, targetQuery) - } - } - } - - alert.Expression = alerting - - _, err := ConvetAlertModelToAlertRule(alert) - - if err == nil && alert.ValidToSave() { - alerts = append(alerts, alert) - } else { - log.Error2("Failed to parse model from expression", "error", err) - } - - } - } - - return alerts -} - -func ConvetAlertModelToAlertRule(ruleDef *m.AlertRuleModel) (*AlertRule, error) { - model := &AlertRule{} - model.Id = ruleDef.Id - model.OrgId = ruleDef.OrgId - model.Name = ruleDef.Name - model.Description = ruleDef.Description - model.State = ruleDef.State - - critical := ruleDef.Expression.Get("critical") - model.Critical = Level{ - Operator: critical.Get("op").MustString(), - Level: critical.Get("level").MustFloat64(), - } - - warning := ruleDef.Expression.Get("warning") - model.Warning = Level{ - Operator: warning.Get("op").MustString(), - Level: warning.Get("level").MustFloat64(), - } - - model.Frequency = ruleDef.Expression.Get("frequency").MustInt64() - model.Transform = ruleDef.Expression.Get("transform").Get("type").MustString() - model.TransformParams = *ruleDef.Expression.Get("transform") - - if model.Transform == "aggregation" { - model.Transformer = &AggregationTransformer{ - Method: ruleDef.Expression.Get("transform").Get("method").MustString(), - } - } - - query := ruleDef.Expression.Get("query") - model.Query = AlertQuery{ - Query: query.Get("query").MustString(), - DatasourceId: query.Get("datasourceId").MustInt64(), - From: query.Get("from").MustString(), - To: query.Get("to").MustString(), - Aggregator: query.Get("agg").MustString(), - } - - if model.Query.Query == "" { - return nil, fmt.Errorf("missing query.query") - } - - if model.Query.DatasourceId == 0 { - return nil, fmt.Errorf("missing query.datasourceId") - } - - return model, nil -} diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go new file mode 100644 index 00000000000..9731f1eb324 --- /dev/null +++ b/pkg/services/alerting/extractor.go @@ -0,0 +1,120 @@ +package alerting + +import ( + "errors" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/log" + m "github.com/grafana/grafana/pkg/models" +) + +type AlertRuleExtractor struct { + Dash *m.Dashboard + OrgId int64 + log log.Logger +} + +func NewAlertRuleExtractor(dash *m.Dashboard, orgId int64) *AlertRuleExtractor { + return &AlertRuleExtractor{ + Dash: dash, + OrgId: orgId, + log: log.New("alerting.extractor"), + } +} + +func (e *AlertRuleExtractor) lookupDatasourceId(dsName string) (int64, error) { + if dsName == "" { + query := &m.GetDataSourcesQuery{OrgId: e.OrgId} + if err := bus.Dispatch(query); err != nil { + return 0, err + } else { + for _, ds := range query.Result { + if ds.IsDefault { + return ds.Id, nil + } + } + } + } else { + query := &m.GetDataSourceByNameQuery{Name: dsName, OrgId: e.OrgId} + if err := bus.Dispatch(query); err != nil { + return 0, err + } else { + return query.Result.Id, nil + } + } + + return 0, errors.New("Could not find datasource id for " + dsName) +} + +func (e *AlertRuleExtractor) GetRuleModels() (m.AlertRules, error) { + + rules := make(m.AlertRules, 0) + + for _, rowObj := range e.Dash.Data.Get("rows").MustArray() { + row := simplejson.NewFromAny(rowObj) + + for _, panelObj := range row.Get("panels").MustArray() { + panel := simplejson.NewFromAny(panelObj) + jsonRule := panel.Get("alerting") + + // check if marked for deletion + deleted := jsonRule.Get("deleted").MustBool() + if deleted { + e.log.Info("Deleted alert rule found") + continue + } + + ruleModel := &m.AlertRuleModel{ + DashboardId: e.Dash.Id, + OrgId: e.OrgId, + PanelId: panel.Get("id").MustInt64(), + Id: jsonRule.Get("id").MustInt64(), + Name: jsonRule.Get("name").MustString(), + Scheduler: jsonRule.Get("scheduler").MustInt64(), + Enabled: jsonRule.Get("enabled").MustBool(), + Description: jsonRule.Get("description").MustString(), + } + + valueQuery := jsonRule.Get("query") + valueQueryRef := valueQuery.Get("refId").MustString() + for _, targetsObj := range panel.Get("targets").MustArray() { + target := simplejson.NewFromAny(targetsObj) + + if target.Get("refId").MustString() == valueQueryRef { + dsName := "" + if target.Get("datasource").MustString() != "" { + dsName = target.Get("datasource").MustString() + } else if panel.Get("datasource").MustString() != "" { + dsName = panel.Get("datasource").MustString() + } + + if datasourceId, err := e.lookupDatasourceId(dsName); err != nil { + return nil, err + } else { + valueQuery.SetPath([]string{"datasourceId"}, datasourceId) + } + + targetQuery := target.Get("target").MustString() + if targetQuery != "" { + jsonRule.SetPath([]string{"query", "query"}, targetQuery) + } + } + } + + ruleModel.Expression = jsonRule + + // validate + _, err := NewAlertRuleFromDBModel(ruleModel) + if err == nil && ruleModel.ValidToSave() { + rules = append(rules, ruleModel) + } else { + e.log.Error("Failed to extract alert rules from dashboard", "error", err) + return nil, errors.New("Failed to extract alert rules from dashboard") + } + + } + } + + return rules, nil +} diff --git a/pkg/services/sqlstore/dashboard_parser_test.go b/pkg/services/alerting/extractor_test.go similarity index 63% rename from pkg/services/sqlstore/dashboard_parser_test.go rename to pkg/services/alerting/extractor_test.go index b7266a926c8..2f40c7e401c 100644 --- a/pkg/services/sqlstore/dashboard_parser_test.go +++ b/pkg/services/alerting/extractor_test.go @@ -1,17 +1,17 @@ -package sqlstore +package alerting import ( "testing" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/alerting" . "github.com/smartystreets/goconvey/convey" ) -func TestAlertModelParsing(t *testing.T) { +func TestAlertRuleExtraction(t *testing.T) { - Convey("Parsing alert info from json", t, func() { + Convey("Parsing alert rules from dashboard json", t, func() { Convey("Parsing and validating alerts from dashboards", func() { json := `{ "id": 57, @@ -37,13 +37,15 @@ func TestAlertModelParsing(t *testing.T) { ], "datasource": null, "alerting": { - "name": "Alerting Panel Title alert", - "description": "description", + "name": "name1", + "description": "desc1", + "scheduler": 1, + "enabled": true, "critical": { "level": 20, "op": ">" }, - "frequency": 10, + "frequency": "60s", "query": { "from": "5m", "refId": "A", @@ -51,12 +53,12 @@ func TestAlertModelParsing(t *testing.T) { }, "transform": { "method": "avg", - "name": "aggregation" + "type": "aggregation" }, "warning": { "level": 10, "op": ">" - } + } } }, { @@ -70,13 +72,15 @@ func TestAlertModelParsing(t *testing.T) { ], "datasource": "graphite2", "alerting": { - "name": "Alerting Panel Title alert", - "description": "description", + "name": "name2", + "description": "desc2", + "scheduler": 0, + "enabled": true, "critical": { "level": 20, "op": ">" }, - "frequency": 10, + "frequency": "60s", "query": { "from": "5m", "refId": "A", @@ -145,7 +149,10 @@ func TestAlertModelParsing(t *testing.T) { ], "title": "Broken influxdb panel", "transform": "table", - "type": "table" + "type": "table", + "alerting": { + "deleted": true + } } ], "title": "New row" @@ -153,51 +160,62 @@ func TestAlertModelParsing(t *testing.T) { ] }` - dashboardJSON, _ := simplejson.NewJson([]byte(json)) - cmd := &m.SaveDashboardCommand{ - Dashboard: dashboardJSON, - UserId: 1, - OrgId: 1, - Overwrite: true, - Result: &m.Dashboard{ - Id: 1, - }, - } + dashJson, err := simplejson.NewJson([]byte(json)) + So(err, ShouldBeNil) - InitTestDB(t) + dash := m.NewDashboardFromJson(dashJson) + extractor := NewAlertRuleExtractor(dash, 1) - AddDataSource(&m.AddDataSourceCommand{ - Name: "graphite2", - OrgId: 1, - Type: m.DS_INFLUXDB, - Access: m.DS_ACCESS_DIRECT, - Url: "http://test", - IsDefault: false, - Database: "site", + // mock data + defaultDs := &m.DataSource{Id: 12, OrgId: 2, Name: "I am default", IsDefault: true} + graphite2Ds := &m.DataSource{Id: 15, OrgId: 2, Name: "graphite2"} + + bus.AddHandler("test", func(query *m.GetDataSourcesQuery) error { + query.Result = []*m.DataSource{defaultDs, graphite2Ds} + return nil }) - AddDataSource(&m.AddDataSourceCommand{ - Name: "InfluxDB", - OrgId: 1, - Type: m.DS_GRAPHITE, - Access: m.DS_ACCESS_DIRECT, - Url: "http://test", - IsDefault: true, + bus.AddHandler("test", func(query *m.GetDataSourceByNameQuery) error { + if query.Name == defaultDs.Name { + query.Result = defaultDs + } + if query.Name == graphite2Ds.Name { + query.Result = graphite2Ds + } + return nil }) - alerts := alerting.ParseAlertsFromDashboard(cmd) + alerts, err := extractor.GetRuleModels() + + Convey("Get rules without error", func() { + So(err, ShouldBeNil) + }) Convey("all properties have been set", func() { - So(alerts, ShouldNotBeEmpty) So(len(alerts), ShouldEqual, 2) for _, v := range alerts { - So(v.DashboardId, ShouldEqual, 1) - So(v.PanelId, ShouldNotEqual, 0) - + So(v.DashboardId, ShouldEqual, 57) So(v.Name, ShouldNotBeEmpty) So(v.Description, ShouldNotBeEmpty) } + + Convey("should extract scheduler property", func() { + So(alerts[0].Scheduler, ShouldEqual, 1) + So(alerts[1].Scheduler, ShouldEqual, 0) + }) + + Convey("should extract panel idc", func() { + So(alerts[0].PanelId, ShouldEqual, 3) + So(alerts[1].PanelId, ShouldEqual, 4) + }) + + Convey("should extract name and desc", func() { + So(alerts[0].Name, ShouldEqual, "name1") + So(alerts[0].Description, ShouldEqual, "desc1") + So(alerts[1].Name, ShouldEqual, "name2") + So(alerts[1].Description, ShouldEqual, "desc2") + }) }) }) }) diff --git a/pkg/services/alerting/models.go b/pkg/services/alerting/models.go index c13669ea3d7..364950ee9fa 100644 --- a/pkg/services/alerting/models.go +++ b/pkg/services/alerting/models.go @@ -1,9 +1,5 @@ package alerting -import ( - "github.com/grafana/grafana/pkg/components/simplejson" -) - type AlertJob struct { Offset int64 Delay bool @@ -21,23 +17,6 @@ type AlertResult struct { AlertJob *AlertJob } -type AlertRule struct { - Id int64 - OrgId int64 - DashboardId int64 - PanelId int64 - Frequency int64 - Name string - Description string - State string - Warning Level - Critical Level - Query AlertQuery - Transform string - TransformParams simplejson.Json - Transformer Transformer -} - type Level struct { Operator string Level float64 diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index 42cb9b78d39..855bd92b568 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/pkg/services/sqlstore/migrations/alert_mig.go @@ -17,6 +17,9 @@ func addAlertMigrations(mg *Migrator) { {Name: "description", Type: DB_NVarchar, Length: 255, Nullable: false}, {Name: "state", Type: DB_NVarchar, Length: 255, Nullable: false}, {Name: "expression", Type: DB_Text, Nullable: false}, + {Name: "scheduler", Type: DB_BigInt, Nullable: false}, + {Name: "frequency", Type: DB_BigInt, Nullable: false}, + {Name: "enabled", Type: DB_Bool, Nullable: false}, {Name: "created", Type: DB_DateTime, Nullable: false}, {Name: "updated", Type: DB_DateTime, Nullable: false}, }, diff --git a/public/app/plugins/panel/graph/alert_tab_ctrl.ts b/public/app/plugins/panel/graph/alert_tab_ctrl.ts index 4f7e9d8d834..970e60d6d61 100644 --- a/public/app/plugins/panel/graph/alert_tab_ctrl.ts +++ b/public/app/plugins/panel/graph/alert_tab_ctrl.ts @@ -24,6 +24,7 @@ export class AlertTabCtrl { panelCtrl: any; alerting: any; metricTargets = [{ refId: '- select query -' } ]; + schedulers = [{text: 'Grafana', value: 1}, {text: 'External', value: 0}]; transforms = [ { text: 'Aggregation', @@ -33,24 +34,23 @@ export class AlertTabCtrl { text: 'Linear Forecast', type: 'forecast', }, - { - text: 'Percent Change', - type: 'percent_change', - }, - { - text: 'Query diff', - type: 'query_diff', - }, ]; aggregators = ['avg', 'sum', 'min', 'max', 'last']; rule: any; query: any; queryParams: any; transformDef: any; - trasnformQuery: any; + levelOpList = [ + {text: '>', value: '>'}, + {text: '<', value: '<'}, + {text: '=', value: '='}, + ]; defaultValues = { - frequency: 10, + frequency: '60s', + notify: [], + enabled: false, + scheduler: 1, warning: { op: '>', level: undefined }, critical: { op: '>', level: undefined }, query: { @@ -139,8 +139,18 @@ export class AlertTabCtrl { } } - markAsDeleted() { - this.panel.alerting = this.defaultValues; + delete() { + this.rule = this.panel.alerting = this.defaultValues; + this.rule.deleted = true; + } + + enable() { + delete this.rule.deleted; + this.rule.enabled = true; + } + + disable() { + this.rule.enabled = false; } thresholdsUpdated() { diff --git a/public/app/plugins/panel/graph/partials/tab_alerting.html b/public/app/plugins/panel/graph/partials/tab_alerting.html index ab688bb003e..06b8c35e632 100644 --- a/public/app/plugins/panel/graph/partials/tab_alerting.html +++ b/public/app/plugins/panel/graph/partials/tab_alerting.html @@ -1,15 +1,13 @@ - -
-
Alert Rule
+
+
+
Alert Query
- -
Transform using @@ -33,77 +31,86 @@
Timespan - +
-
+
Levels
- Warn if value + Warn if - - > - - + +
- Critcal if value + Critcal if - - > - - + +
+
- - - - - - - - - - - - - - - - - - - - - -
+
-
Alert info
-
- Alert name - -
+
Execution
- Alert description + Scheduler +
+ +
- + Evaluate every + +
+
+
+
+
Notifications
+
+
+ Groups + +
+ + +
+
Information
+
+ Alert name + +
+
+
+ Alert description +
+
+ +
+
+
+
- + + +
diff --git a/public/sass/components/_tagsinput.scss b/public/sass/components/_tagsinput.scss index 8092446e2e5..698302e6f1e 100644 --- a/public/sass/components/_tagsinput.scss +++ b/public/sass/components/_tagsinput.scss @@ -7,6 +7,7 @@ background-color: $input-bg; input { + display: inline-block; border: none; border-right: 1px solid $tight-form-border; margin: 0px; diff --git a/public/vendor/tagsinput/bootstrap-tagsinput.js b/public/vendor/tagsinput/bootstrap-tagsinput.js index b3a3c3bc386..702b6416962 100644 --- a/public/vendor/tagsinput/bootstrap-tagsinput.js +++ b/public/vendor/tagsinput/bootstrap-tagsinput.js @@ -35,7 +35,7 @@ this.inputSize = Math.max(1, this.placeholderText.length); this.$container = $('
'); - this.$input = $('').appendTo(this.$container); this.$element.after(this.$container); From a362984c576d85ccc88b8281624922c7fd0748a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 11 Jun 2016 10:26:48 +0200 Subject: [PATCH 24/37] feat(alerting): renamed AlertRuleModel to just Alert, think making a distinction between an Alert and an Alert Rule is just confusing and was a mistake on my part --- pkg/api/alerting.go | 2 +- pkg/models/{alerts.go => alert.go} | 30 +++++++------------ .../{alerts_state.go => alert_state.go} | 2 +- pkg/models/{alerts_test.go => alert_test.go} | 4 +-- pkg/services/alerting/alert_rule.go | 2 +- pkg/services/alerting/commands.go | 2 +- pkg/services/alerting/extractor.go | 2 +- .../sqlstore/{alert_rule.go => alert.go} | 20 ++++++------- pkg/services/sqlstore/alert_rule_changes.go | 2 +- .../sqlstore/alert_rule_changes_test.go | 2 +- .../sqlstore/alert_rule_parser_test.go | 2 +- pkg/services/sqlstore/alert_rule_test.go | 6 ++-- pkg/services/sqlstore/alert_state.go | 5 ++-- pkg/services/sqlstore/alert_state_test.go | 2 +- 14 files changed, 38 insertions(+), 45 deletions(-) rename pkg/models/{alerts.go => alert.go} (77%) rename pkg/models/{alerts_state.go => alert_state.go} (97%) rename pkg/models/{alerts_test.go => alert_test.go} (93%) rename pkg/services/sqlstore/{alert_rule.go => alert.go} (90%) diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 4106dc0eae3..3f2c38486a4 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -43,7 +43,7 @@ func GetAlertChanges(c *middleware.Context) Response { return Json(200, query.Result) } -// GET /api/alerts +// GET /api/alerts/rules/ func GetAlerts(c *middleware.Context) Response { query := models.GetAlertsQuery{ OrgId: c.OrgId, diff --git a/pkg/models/alerts.go b/pkg/models/alert.go similarity index 77% rename from pkg/models/alerts.go rename to pkg/models/alert.go index 2ba21fd17cf..034efbe9ca7 100644 --- a/pkg/models/alerts.go +++ b/pkg/models/alert.go @@ -6,7 +6,7 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" ) -type AlertRuleModel struct { +type Alert struct { Id int64 OrgId int64 DashboardId int64 @@ -24,17 +24,11 @@ type AlertRuleModel struct { Expression *simplejson.Json } -type AlertRules []*AlertRuleModel - -func (this AlertRuleModel) TableName() string { - return "alert_rule" +func (alert *Alert) ValidToSave() bool { + return alert.DashboardId != 0 } -func (alertRule *AlertRuleModel) ValidToSave() bool { - return alertRule.DashboardId != 0 -} - -func (this *AlertRuleModel) ContainsUpdates(other *AlertRuleModel) bool { +func (this *Alert) ContainsUpdates(other *Alert) bool { result := false result = result || this.Name != other.Name result = result || this.Description != other.Description @@ -51,7 +45,6 @@ func (this *AlertRuleModel) ContainsUpdates(other *AlertRuleModel) bool { } //don't compare .State! That would be insane. - return result } @@ -70,11 +63,10 @@ type HeartBeat struct { type HeartBeatCommand struct { ServerId string - - Result AlertingClusterInfo + Result AlertingClusterInfo } -type AlertRuleChange struct { +type AlertChange struct { Id int64 `json:"id"` OrgId int64 `json:"-"` AlertId int64 `json:"alertId"` @@ -88,7 +80,7 @@ type SaveAlertsCommand struct { UserId int64 OrgId int64 - Alerts AlertRules + Alerts []*Alert } type DeleteAlertCommand struct { @@ -102,17 +94,17 @@ type GetAlertsQuery struct { DashboardId int64 PanelId int64 - Result []*AlertRuleModel + Result []*Alert } type GetAllAlertsQuery struct { - Result []*AlertRuleModel + Result []*Alert } type GetAlertByIdQuery struct { Id int64 - Result *AlertRuleModel + Result *Alert } type GetAlertChangesQuery struct { @@ -120,5 +112,5 @@ type GetAlertChangesQuery struct { Limit int64 SinceId int64 - Result []*AlertRuleChange + Result []*AlertChange } diff --git a/pkg/models/alerts_state.go b/pkg/models/alert_state.go similarity index 97% rename from pkg/models/alerts_state.go rename to pkg/models/alert_state.go index 50da6d93f88..30d442ed625 100644 --- a/pkg/models/alerts_state.go +++ b/pkg/models/alert_state.go @@ -31,7 +31,7 @@ type UpdateAlertStateCommand struct { NewState string `json:"newState" binding:"Required"` Info string `json:"info"` - Result *AlertRuleModel + Result *Alert } // Queries diff --git a/pkg/models/alerts_test.go b/pkg/models/alert_test.go similarity index 93% rename from pkg/models/alerts_test.go rename to pkg/models/alert_test.go index 84d5c989b26..19766640629 100644 --- a/pkg/models/alerts_test.go +++ b/pkg/models/alert_test.go @@ -13,13 +13,13 @@ func TestAlertingModelTest(t *testing.T) { json1, _ := simplejson.NewJson([]byte(`{ "field": "value" }`)) json2, _ := simplejson.NewJson([]byte(`{ "field": "value" }`)) - rule1 := &AlertRuleModel{ + rule1 := &Alert{ Expression: json1, Name: "Namn", Description: "Description", } - rule2 := &AlertRuleModel{ + rule2 := &Alert{ Expression: json2, Name: "Namn", Description: "Description", diff --git a/pkg/services/alerting/alert_rule.go b/pkg/services/alerting/alert_rule.go index 867f88861e9..8678311a6a2 100644 --- a/pkg/services/alerting/alert_rule.go +++ b/pkg/services/alerting/alert_rule.go @@ -25,7 +25,7 @@ type AlertRule struct { Transformer Transformer } -func NewAlertRuleFromDBModel(ruleDef *m.AlertRuleModel) (*AlertRule, error) { +func NewAlertRuleFromDBModel(ruleDef *m.Alert) (*AlertRule, error) { model := &AlertRule{} model.Id = ruleDef.Id model.OrgId = ruleDef.OrgId diff --git a/pkg/services/alerting/commands.go b/pkg/services/alerting/commands.go index 83d906f41fb..f7db18c05ce 100644 --- a/pkg/services/alerting/commands.go +++ b/pkg/services/alerting/commands.go @@ -38,7 +38,7 @@ func updateDashboardAlerts(cmd *UpdateDashboardAlertsCommand) error { return nil } -func ConvetAlertModelToAlertRule(ruleDef *m.AlertRuleModel) (*AlertRule, error) { +func ConvetAlertModelToAlertRule(ruleDef *m.Alert) (*AlertRule, error) { model := &AlertRule{} model.Id = ruleDef.Id model.OrgId = ruleDef.OrgId diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index 9731f1eb324..e15fb4af09e 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -65,7 +65,7 @@ func (e *AlertRuleExtractor) GetRuleModels() (m.AlertRules, error) { continue } - ruleModel := &m.AlertRuleModel{ + ruleModel := &m.Alert{ DashboardId: e.Dash.Id, OrgId: e.OrgId, PanelId: panel.Get("id").MustInt64(), diff --git a/pkg/services/sqlstore/alert_rule.go b/pkg/services/sqlstore/alert.go similarity index 90% rename from pkg/services/sqlstore/alert_rule.go rename to pkg/services/sqlstore/alert.go index d4d4531b9d3..93709988536 100644 --- a/pkg/services/sqlstore/alert_rule.go +++ b/pkg/services/sqlstore/alert.go @@ -64,7 +64,7 @@ func HeartBeat(query *m.HeartBeatCommand) error { */ func GetAlertById(query *m.GetAlertByIdQuery) error { - alert := m.AlertRuleModel{} + alert := m.Alert{} has, err := x.Id(query.Id).Get(&alert) if !has { return fmt.Errorf("could not find alert") @@ -78,7 +78,7 @@ func GetAlertById(query *m.GetAlertByIdQuery) error { } func GetAllAlertQueryHandler(query *m.GetAllAlertsQuery) error { - var alerts []*m.AlertRuleModel + var alerts []*m.Alert err := x.Sql("select * from alert_rule").Find(&alerts) if err != nil { return err @@ -131,7 +131,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error { sql.WriteString(")") } - alerts := make([]*m.AlertRuleModel, 0) + alerts := make([]*m.Alert, 0) if err := x.Sql(sql.String(), params...).Find(&alerts); err != nil { return err } @@ -141,7 +141,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error { } func DeleteAlertDefinition(dashboardId int64, sess *xorm.Session) error { - alerts := make([]*m.AlertRuleModel, 0) + alerts := make(m.Alerts, 0) sess.Where("dashboard_id = ?", dashboardId).Find(&alerts) for _, alert := range alerts { @@ -172,10 +172,10 @@ func SaveAlerts(cmd *m.SaveAlertsCommand) error { }) } -func upsertAlerts(alerts []*m.AlertRuleModel, posted []*m.AlertRuleModel, sess *xorm.Session) error { +func upsertAlerts(alerts []*m.Alert, posted []*m.Alert, sess *xorm.Session) error { for _, alert := range posted { update := false - var alertToUpdate *m.AlertRuleModel + var alertToUpdate *m.Alert for _, k := range alerts { if alert.PanelId == k.PanelId { @@ -212,7 +212,7 @@ func upsertAlerts(alerts []*m.AlertRuleModel, posted []*m.AlertRuleModel, sess * return nil } -func deleteMissingAlerts(alerts []*m.AlertRuleModel, posted []*m.AlertRuleModel, sess *xorm.Session) error { +func deleteMissingAlerts(alerts []*m.Alert, posted []*m.Alert, sess *xorm.Session) error { for _, missingAlert := range alerts { missing := true @@ -238,12 +238,12 @@ func deleteMissingAlerts(alerts []*m.AlertRuleModel, posted []*m.AlertRuleModel, return nil } -func GetAlertsByDashboardId2(dashboardId int64, sess *xorm.Session) ([]*m.AlertRuleModel, error) { - alerts := make([]*m.AlertRuleModel, 0) +func GetAlertsByDashboardId2(dashboardId int64, sess *xorm.Session) ([]*m.Alert, error) { + alerts := make([]*m.Alert, 0) err := sess.Where("dashboard_id = ?", dashboardId).Find(&alerts) if err != nil { - return []*m.AlertRuleModel{}, err + return []*m.Alert{}, err } return alerts, nil diff --git a/pkg/services/sqlstore/alert_rule_changes.go b/pkg/services/sqlstore/alert_rule_changes.go index a78ff7ef723..b250f9bb641 100644 --- a/pkg/services/sqlstore/alert_rule_changes.go +++ b/pkg/services/sqlstore/alert_rule_changes.go @@ -48,7 +48,7 @@ func GetAlertRuleChanges(query *m.GetAlertChangesQuery) error { return nil } -func SaveAlertChange(change string, alert *m.AlertRuleModel, sess *xorm.Session) error { +func SaveAlertChange(change string, alert *m.Alert, sess *xorm.Session) error { _, err := sess.Insert(&m.AlertRuleChange{ OrgId: alert.OrgId, Type: change, diff --git a/pkg/services/sqlstore/alert_rule_changes_test.go b/pkg/services/sqlstore/alert_rule_changes_test.go index a7a4254964d..0e0ca253ca6 100644 --- a/pkg/services/sqlstore/alert_rule_changes_test.go +++ b/pkg/services/sqlstore/alert_rule_changes_test.go @@ -20,7 +20,7 @@ func TestAlertRuleChangesDataAccess(t *testing.T) { var err error Convey("When dashboard is removed", func() { - items := []*m.AlertRuleModel{ + items := []*m.Alert{ { PanelId: 1, DashboardId: testDash.Id, diff --git a/pkg/services/sqlstore/alert_rule_parser_test.go b/pkg/services/sqlstore/alert_rule_parser_test.go index 94da627fa25..8ec7c24429b 100644 --- a/pkg/services/sqlstore/alert_rule_parser_test.go +++ b/pkg/services/sqlstore/alert_rule_parser_test.go @@ -12,7 +12,7 @@ import ( func TestAlertRuleModelParsing(t *testing.T) { Convey("Parsing alertRule from expression", t, func() { - alertRuleDAO := &m.AlertRuleModel{} + alertRuleDAO := &m.Alert{} json, _ := simplejson.NewJson([]byte(` { "frequency": 10, diff --git a/pkg/services/sqlstore/alert_rule_test.go b/pkg/services/sqlstore/alert_rule_test.go index 8baf0428025..be15bad3229 100644 --- a/pkg/services/sqlstore/alert_rule_test.go +++ b/pkg/services/sqlstore/alert_rule_test.go @@ -14,7 +14,7 @@ func TestAlertingDataAccess(t *testing.T) { testDash := insertTestDashboard("dashboard with alerts", 1, "alert") - items := []*m.AlertRuleModel{ + items := []*m.Alert{ { PanelId: 1, DashboardId: testDash.Id, @@ -96,7 +96,7 @@ func TestAlertingDataAccess(t *testing.T) { }) Convey("Multiple alerts per dashboard", func() { - multipleItems := []*m.AlertRuleModel{ + multipleItems := []*m.Alert{ { DashboardId: testDash.Id, PanelId: 1, @@ -161,7 +161,7 @@ func TestAlertingDataAccess(t *testing.T) { }) Convey("When dashboard is removed", func() { - items := []*m.AlertRuleModel{ + items := []*m.Alert{ { PanelId: 1, DashboardId: testDash.Id, diff --git a/pkg/services/sqlstore/alert_state.go b/pkg/services/sqlstore/alert_state.go index 6c72adfb3b2..c8aba5ab7c9 100644 --- a/pkg/services/sqlstore/alert_state.go +++ b/pkg/services/sqlstore/alert_state.go @@ -2,10 +2,11 @@ package sqlstore import ( "fmt" + "time" + "github.com/go-xorm/xorm" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" - "time" ) func init() { @@ -19,7 +20,7 @@ func SetNewAlertState(cmd *m.UpdateAlertStateCommand) error { return fmt.Errorf("new state is invalid") } - alert := m.AlertRuleModel{} + alert := m.Alert{} has, err := sess.Id(cmd.AlertId).Get(&alert) if !has { return fmt.Errorf("Could not find alert") diff --git a/pkg/services/sqlstore/alert_state_test.go b/pkg/services/sqlstore/alert_state_test.go index a07b68e0447..2fbf652353c 100644 --- a/pkg/services/sqlstore/alert_state_test.go +++ b/pkg/services/sqlstore/alert_state_test.go @@ -13,7 +13,7 @@ func TestAlertingStateAccess(t *testing.T) { testDash := insertTestDashboard("dashboard with alerts", 1, "alert") - items := []*m.AlertRuleModel{ + items := []*m.Alert{ { PanelId: 1, DashboardId: testDash.Id, From 382f396247b47a66ae10d718e99f1f3fe9673d4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 11 Jun 2016 10:54:24 +0200 Subject: [PATCH 25/37] feat(alerting): more model changes --- pkg/services/alerting/commands.go | 10 +-- pkg/services/alerting/extractor.go | 46 ++++++------ pkg/services/alerting/extractor_test.go | 8 +-- pkg/services/sqlstore/alert.go | 19 +++-- pkg/services/sqlstore/alert_rule_changes.go | 4 +- pkg/services/sqlstore/migrations/alert_mig.go | 8 +-- pkg/services/sqlstore/migrator/migrator.go | 2 +- .../app/plugins/panel/graph/alert_tab_ctrl.ts | 72 +++++++++---------- .../panel/graph/partials/tab_alerting.html | 26 +++---- 9 files changed, 97 insertions(+), 98 deletions(-) diff --git a/pkg/services/alerting/commands.go b/pkg/services/alerting/commands.go index f7db18c05ce..38ee4ecf99c 100644 --- a/pkg/services/alerting/commands.go +++ b/pkg/services/alerting/commands.go @@ -18,20 +18,20 @@ func init() { } func updateDashboardAlerts(cmd *UpdateDashboardAlertsCommand) error { - saveRulesCmd := m.SaveAlertsCommand{ + saveAlerts := m.SaveAlertsCommand{ OrgId: cmd.OrgId, UserId: cmd.UserId, } - extractor := NewAlertRuleExtractor(cmd.Dashboard, cmd.OrgId) + extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId) - rules, err := extractor.GetRuleModels() + alerts, err := extractor.GetRuleModels() if err != nil { return err } - saveRulesCmd.Alerts = rules - if bus.Dispatch(&saveRulesCmd); err != nil { + saveAlerts.Alerts = alerts + if bus.Dispatch(&saveAlerts); err != nil { return err } diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index e15fb4af09e..b7b3b17811e 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -9,21 +9,21 @@ import ( m "github.com/grafana/grafana/pkg/models" ) -type AlertRuleExtractor struct { +type DashAlertExtractor struct { Dash *m.Dashboard OrgId int64 log log.Logger } -func NewAlertRuleExtractor(dash *m.Dashboard, orgId int64) *AlertRuleExtractor { - return &AlertRuleExtractor{ +func NewDashAlertExtractor(dash *m.Dashboard, orgId int64) *DashAlertExtractor { + return &DashAlertExtractor{ Dash: dash, OrgId: orgId, log: log.New("alerting.extractor"), } } -func (e *AlertRuleExtractor) lookupDatasourceId(dsName string) (int64, error) { +func (e *DashAlertExtractor) lookupDatasourceId(dsName string) (int64, error) { if dsName == "" { query := &m.GetDataSourcesQuery{OrgId: e.OrgId} if err := bus.Dispatch(query); err != nil { @@ -47,36 +47,36 @@ func (e *AlertRuleExtractor) lookupDatasourceId(dsName string) (int64, error) { return 0, errors.New("Could not find datasource id for " + dsName) } -func (e *AlertRuleExtractor) GetRuleModels() (m.AlertRules, error) { +func (e *DashAlertExtractor) GetRuleModels() ([]*m.Alert, error) { - rules := make(m.AlertRules, 0) + alerts := make([]*m.Alert, 0) for _, rowObj := range e.Dash.Data.Get("rows").MustArray() { row := simplejson.NewFromAny(rowObj) for _, panelObj := range row.Get("panels").MustArray() { panel := simplejson.NewFromAny(panelObj) - jsonRule := panel.Get("alerting") + jsonAlert := panel.Get("alert") // check if marked for deletion - deleted := jsonRule.Get("deleted").MustBool() + deleted := jsonAlert.Get("deleted").MustBool() if deleted { e.log.Info("Deleted alert rule found") continue } - ruleModel := &m.Alert{ + alert := &m.Alert{ DashboardId: e.Dash.Id, OrgId: e.OrgId, PanelId: panel.Get("id").MustInt64(), - Id: jsonRule.Get("id").MustInt64(), - Name: jsonRule.Get("name").MustString(), - Scheduler: jsonRule.Get("scheduler").MustInt64(), - Enabled: jsonRule.Get("enabled").MustBool(), - Description: jsonRule.Get("description").MustString(), + Id: jsonAlert.Get("id").MustInt64(), + Name: jsonAlert.Get("name").MustString(), + Scheduler: jsonAlert.Get("scheduler").MustInt64(), + Enabled: jsonAlert.Get("enabled").MustBool(), + Description: jsonAlert.Get("description").MustString(), } - valueQuery := jsonRule.Get("query") + valueQuery := jsonAlert.Get("query") valueQueryRef := valueQuery.Get("refId").MustString() for _, targetsObj := range panel.Get("targets").MustArray() { target := simplejson.NewFromAny(targetsObj) @@ -97,24 +97,24 @@ func (e *AlertRuleExtractor) GetRuleModels() (m.AlertRules, error) { targetQuery := target.Get("target").MustString() if targetQuery != "" { - jsonRule.SetPath([]string{"query", "query"}, targetQuery) + jsonAlert.SetPath([]string{"query", "query"}, targetQuery) } } } - ruleModel.Expression = jsonRule + alert.Expression = jsonAlert // validate - _, err := NewAlertRuleFromDBModel(ruleModel) - if err == nil && ruleModel.ValidToSave() { - rules = append(rules, ruleModel) + _, err := NewAlertRuleFromDBModel(alert) + if err == nil && alert.ValidToSave() { + alerts = append(alerts, alert) } else { - e.log.Error("Failed to extract alert rules from dashboard", "error", err) - return nil, errors.New("Failed to extract alert rules from dashboard") + e.log.Error("Failed to extract alerts from dashboard", "error", err) + return nil, errors.New("Failed to extract alerts from dashboard") } } } - return rules, nil + return alerts, nil } diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go index 2f40c7e401c..069489dfb23 100644 --- a/pkg/services/alerting/extractor_test.go +++ b/pkg/services/alerting/extractor_test.go @@ -36,7 +36,7 @@ func TestAlertRuleExtraction(t *testing.T) { } ], "datasource": null, - "alerting": { + "alert": { "name": "name1", "description": "desc1", "scheduler": 1, @@ -71,7 +71,7 @@ func TestAlertRuleExtraction(t *testing.T) { } ], "datasource": "graphite2", - "alerting": { + "alert": { "name": "name2", "description": "desc2", "scheduler": 0, @@ -150,7 +150,7 @@ func TestAlertRuleExtraction(t *testing.T) { "title": "Broken influxdb panel", "transform": "table", "type": "table", - "alerting": { + "alert": { "deleted": true } } @@ -164,7 +164,7 @@ func TestAlertRuleExtraction(t *testing.T) { So(err, ShouldBeNil) dash := m.NewDashboardFromJson(dashJson) - extractor := NewAlertRuleExtractor(dash, 1) + extractor := NewDashAlertExtractor(dash, 1) // mock data defaultDs := &m.DataSource{Id: 12, OrgId: 2, Name: "I am default", IsDefault: true} diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index 93709988536..e7166c18932 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -79,7 +79,7 @@ func GetAlertById(query *m.GetAlertByIdQuery) error { func GetAllAlertQueryHandler(query *m.GetAllAlertsQuery) error { var alerts []*m.Alert - err := x.Sql("select * from alert_rule").Find(&alerts) + err := x.Sql("select * from alert").Find(&alerts) if err != nil { return err } @@ -90,7 +90,7 @@ func GetAllAlertQueryHandler(query *m.GetAllAlertsQuery) error { func DeleteAlertById(cmd *m.DeleteAlertCommand) error { return inTransaction(func(sess *xorm.Session) error { - if _, err := sess.Exec("DELETE FROM alert_rule WHERE id = ?", cmd.AlertId); err != nil { + if _, err := sess.Exec("DELETE FROM alert WHERE id = ?", cmd.AlertId); err != nil { return err } @@ -103,7 +103,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error { params := make([]interface{}, 0) sql.WriteString(`SELECT * - from alert_rule + from alert `) sql.WriteString(`WHERE org_id = ?`) @@ -141,15 +141,17 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error { } func DeleteAlertDefinition(dashboardId int64, sess *xorm.Session) error { - alerts := make(m.Alerts, 0) + alerts := make([]*m.Alert, 0) sess.Where("dashboard_id = ?", dashboardId).Find(&alerts) for _, alert := range alerts { - _, err := sess.Exec("DELETE FROM alert_rule WHERE id = ? ", alert.Id) + _, err := sess.Exec("DELETE FROM alert WHERE id = ? ", alert.Id) if err != nil { return err } + sqlog.Debug("Alert deleted (due to dashboard deletion)", "name", alert.Name, "id", alert.Id) + if err := SaveAlertChange("DELETED", alert, sess); err != nil { return err } @@ -194,6 +196,7 @@ func upsertAlerts(alerts []*m.Alert, posted []*m.Alert, sess *xorm.Session) erro return err } + sqlog.Debug("Alert updated", "name", alert.Name, "id", alert.Id) SaveAlertChange("UPDATED", alert, sess) } @@ -205,6 +208,8 @@ func upsertAlerts(alerts []*m.Alert, posted []*m.Alert, sess *xorm.Session) erro if err != nil { return err } + + sqlog.Debug("Alert inserted", "name", alert.Name, "id", alert.Id) SaveAlertChange("CREATED", alert, sess) } } @@ -223,11 +228,13 @@ func deleteMissingAlerts(alerts []*m.Alert, posted []*m.Alert, sess *xorm.Sessio } if missing { - _, err := sess.Exec("DELETE FROM alert_rule WHERE id = ?", missingAlert.Id) + _, err := sess.Exec("DELETE FROM alert WHERE id = ?", missingAlert.Id) if err != nil { return err } + sqlog.Debug("Alert deleted", "name", missingAlert.Name, "id", missingAlert.Id) + err = SaveAlertChange("DELETED", missingAlert, sess) if err != nil { return err diff --git a/pkg/services/sqlstore/alert_rule_changes.go b/pkg/services/sqlstore/alert_rule_changes.go index b250f9bb641..cb5fe83cab0 100644 --- a/pkg/services/sqlstore/alert_rule_changes.go +++ b/pkg/services/sqlstore/alert_rule_changes.go @@ -39,7 +39,7 @@ func GetAlertRuleChanges(query *m.GetAlertChangesQuery) error { params = append(params, query.Limit) } - alertChanges := make([]*m.AlertRuleChange, 0) + alertChanges := make([]*m.AlertChange, 0) if err := x.Sql(sql.String(), params...).Find(&alertChanges); err != nil { return err } @@ -49,7 +49,7 @@ func GetAlertRuleChanges(query *m.GetAlertChangesQuery) error { } func SaveAlertChange(change string, alert *m.Alert, sess *xorm.Session) error { - _, err := sess.Insert(&m.AlertRuleChange{ + _, err := sess.Insert(&m.AlertChange{ OrgId: alert.OrgId, Type: change, Created: time.Now(), diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index 855bd92b568..6d2dc489413 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/pkg/services/sqlstore/migrations/alert_mig.go @@ -7,7 +7,7 @@ import ( func addAlertMigrations(mg *Migrator) { alertV1 := Table{ - Name: "alert_rule", + Name: "alert", Columns: []*Column{ {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, {Name: "dashboard_id", Type: DB_BigInt, Nullable: false}, @@ -26,10 +26,10 @@ func addAlertMigrations(mg *Migrator) { } // create table - mg.AddMigration("create alert_rule table v2", NewAddTableMigration(alertV1)) + mg.AddMigration("create alert table v1", NewAddTableMigration(alertV1)) alert_changes := Table{ - Name: "alert_rule_change", + Name: "alert_change", Columns: []*Column{ {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, {Name: "alert_id", Type: DB_BigInt, Nullable: false}, @@ -39,7 +39,7 @@ func addAlertMigrations(mg *Migrator) { }, } - mg.AddMigration("create alert_rules_updates table v1", NewAddTableMigration(alert_changes)) + mg.AddMigration("create alert_change table v1", NewAddTableMigration(alert_changes)) alert_state_log := Table{ Name: "alert_state", diff --git a/pkg/services/sqlstore/migrator/migrator.go b/pkg/services/sqlstore/migrator/migrator.go index 399a87273d5..e704826bed3 100644 --- a/pkg/services/sqlstore/migrator/migrator.go +++ b/pkg/services/sqlstore/migrator/migrator.go @@ -107,7 +107,7 @@ func (mg *Migrator) Start() error { } func (mg *Migrator) exec(m Migration) error { - log.Info("Executing migration", "id", m.Id()) + mg.Logger.Info("Executing migration", "id", m.Id()) err := mg.inTransaction(func(sess *xorm.Session) error { diff --git a/public/app/plugins/panel/graph/alert_tab_ctrl.ts b/public/app/plugins/panel/graph/alert_tab_ctrl.ts index 970e60d6d61..3f83e6db38e 100644 --- a/public/app/plugins/panel/graph/alert_tab_ctrl.ts +++ b/public/app/plugins/panel/graph/alert_tab_ctrl.ts @@ -22,7 +22,6 @@ var alertQueryDef = new QueryPartDef({ export class AlertTabCtrl { panel: any; panelCtrl: any; - alerting: any; metricTargets = [{ refId: '- select query -' } ]; schedulers = [{text: 'Grafana', value: 1}, {text: 'External', value: 0}]; transforms = [ @@ -36,7 +35,7 @@ export class AlertTabCtrl { }, ]; aggregators = ['avg', 'sum', 'min', 'max', 'last']; - rule: any; + alert: any; query: any; queryParams: any; transformDef: any; @@ -71,33 +70,37 @@ export class AlertTabCtrl { $scope.ctrl = this; this.metricTargets = this.panel.targets.map(val => val); - this.rule = this.panel.alerting = this.panel.alerting || {}; + + this.initAlertModel(); + } + + initAlertModel() { + this.alert = this.panel.alert = this.panel.alert || {}; // set defaults - _.defaults(this.rule, this.defaultValues); + _.defaults(this.alert, this.defaultValues); var defaultName = (this.panelCtrl.dashboard.title + ' ' + this.panel.title + ' alert'); - this.rule.name = this.rule.name || defaultName; - this.rule.description = this.rule.description || defaultName; - this.rule.queryRef = this.panel.alerting.queryRef || this.metricTargets[0].refId; + this.alert.name = this.alert.name || defaultName; + this.alert.description = this.alert.description || defaultName; // great temp working model this.queryParams = { params: [ - this.rule.query.refId, - this.rule.query.from, - this.rule.query.to + this.alert.query.refId, + this.alert.query.from, + this.alert.query.to ] }; // init the query part components model this.query = new QueryPart(this.queryParams, alertQueryDef); this.convertThresholdsToAlertThresholds(); - this.transformDef = _.findWhere(this.transforms, {type: this.rule.transform.type}); + this.transformDef = _.findWhere(this.transforms, {type: this.alert.transform.type}); } queryUpdated() { - this.rule.query = { + this.alert.query = { refId: this.query.params[0], from: this.query.params[1], to: this.query.params[2], @@ -106,16 +109,16 @@ export class AlertTabCtrl { transformChanged() { // clear model - this.rule.transform = {type: this.rule.transform.type}; - this.transformDef = _.findWhere(this.transforms, {type: this.rule.transform.type}); + this.alert.transform = {type: this.alert.transform.type}; + this.transformDef = _.findWhere(this.transforms, {type: this.alert.transform.type}); - switch (this.rule.transform.type) { + switch (this.alert.transform.type) { case 'aggregation': { - this.rule.transform.method = 'avg'; + this.alert.transform.method = 'avg'; break; } case "forecast": { - this.rule.transform.timespan = '7d'; + this.alert.transform.timespan = '7d'; break; } } @@ -124,45 +127,34 @@ export class AlertTabCtrl { convertThresholdsToAlertThresholds() { if (this.panel.grid && this.panel.grid.threshold1 - && this.rule.warnLevel === undefined + && this.alert.warnLevel === undefined ) { - this.rule.warning.op = '>'; - this.rule.warning.level = this.panel.grid.threshold1; + this.alert.warning.op = '>'; + this.alert.warning.level = this.panel.grid.threshold1; } if (this.panel.grid && this.panel.grid.threshold2 - && this.rule.critical.level === undefined + && this.alert.critical.level === undefined ) { - this.rule.critical.op = '>'; - this.rule.critical.level = this.panel.grid.threshold2; + this.alert.critical.op = '>'; + this.alert.critical.level = this.panel.grid.threshold2; } } delete() { - this.rule = this.panel.alerting = this.defaultValues; - this.rule.deleted = true; + this.alert = this.panel.alert = {}; + this.alert.deleted = true; + this.initAlertModel(); } enable() { - delete this.rule.deleted; - this.rule.enabled = true; + delete this.alert.deleted; + this.alert.enabled = true; } disable() { - this.rule.enabled = false; - } - - thresholdsUpdated() { - if (this.panel.alerting.warnLevel) { - this.panel.grid.threshold1 = parseInt(this.panel.alerting.warnLevel); - } - - if (this.panel.alerting.critLevel) { - this.panel.grid.threshold2 = parseInt(this.panel.alerting.critLevel); - } - - this.panelCtrl.render(); + this.alert.enabled = false; } } diff --git a/public/app/plugins/panel/graph/partials/tab_alerting.html b/public/app/plugins/panel/graph/partials/tab_alerting.html index 06b8c35e632..2b7b4e112e3 100644 --- a/public/app/plugins/panel/graph/partials/tab_alerting.html +++ b/public/app/plugins/panel/graph/partials/tab_alerting.html @@ -13,7 +13,7 @@ Transform using
Timespan - +
@@ -44,16 +44,16 @@ Warn if - - + +
Critcal if - - + +
@@ -67,14 +67,14 @@ Scheduler
Evaluate every - +
@@ -83,7 +83,7 @@
Groups - +
@@ -109,8 +109,8 @@
- - - + + +
From 086b59483ecea3ac395d208700cce78826f6df06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 11 Jun 2016 11:38:25 +0200 Subject: [PATCH 26/37] feat(loggin): disable logging by default so unit tests are not full of logging --- pkg/log/log.go | 1 + pkg/middleware/quota.go | 7 ++----- pkg/services/sqlstore/datasource_test.go | 4 +--- pkg/services/sqlstore/migrations/migrations_test.go | 2 -- pkg/services/sqlstore/migrator/migrator.go | 2 +- pkg/services/sqlstore/sqlstore.go | 4 ++-- 6 files changed, 7 insertions(+), 13 deletions(-) diff --git a/pkg/log/log.go b/pkg/log/log.go index f74511e4f45..58f3cb89cab 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -22,6 +22,7 @@ var loggersToClose []DisposableHandler func init() { loggersToClose = make([]DisposableHandler, 0) Root = log15.Root() + Root.SetHandler(log15.DiscardHandler()) } func New(logger string, ctx ...interface{}) Logger { diff --git a/pkg/middleware/quota.go b/pkg/middleware/quota.go index d9e68f7560a..23f98e78a7e 100644 --- a/pkg/middleware/quota.go +++ b/pkg/middleware/quota.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" "gopkg.in/macaron.v1" @@ -35,10 +34,8 @@ func QuotaReached(c *Context, target string) (bool, error) { return false, err } - log.Debug(fmt.Sprintf("checking quota for %s in scopes %v", target, scopes)) - for _, scope := range scopes { - log.Debug(fmt.Sprintf("checking scope %s", scope.Name)) + c.Logger.Debug("Checking quota", "target", target, "scope", scope) switch scope.Name { case "global": @@ -51,7 +48,7 @@ func QuotaReached(c *Context, target string) (bool, error) { if target == "session" { usedSessions := getSessionCount() if int64(usedSessions) > scope.DefaultLimit { - log.Debug(fmt.Sprintf("%d sessions active, limit is %d", usedSessions, scope.DefaultLimit)) + c.Logger.Debug("Sessions limit reached", "active", usedSessions, "limit", scope.DefaultLimit) return true, nil } continue diff --git a/pkg/services/sqlstore/datasource_test.go b/pkg/services/sqlstore/datasource_test.go index 4142602c472..b14c7ed9a24 100644 --- a/pkg/services/sqlstore/datasource_test.go +++ b/pkg/services/sqlstore/datasource_test.go @@ -12,8 +12,6 @@ import ( ) func InitTestDB(t *testing.T) { - - t.Log("InitTestDB") x, err := xorm.NewEngine(sqlutil.TestDB_Sqlite3.DriverName, sqlutil.TestDB_Sqlite3.ConnStr) //x, err := xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr) //x, err := xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.ConnStr) @@ -24,7 +22,7 @@ func InitTestDB(t *testing.T) { sqlutil.CleanDB(x) - if err := SetEngine(x, false); err != nil { + if err := SetEngine(x); err != nil { t.Fatal(err) } } diff --git a/pkg/services/sqlstore/migrations/migrations_test.go b/pkg/services/sqlstore/migrations/migrations_test.go index c301c41b2e3..97b68877ae8 100644 --- a/pkg/services/sqlstore/migrations/migrations_test.go +++ b/pkg/services/sqlstore/migrations/migrations_test.go @@ -6,7 +6,6 @@ import ( "github.com/go-xorm/xorm" . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" - "github.com/inconshreveable/log15" . "github.com/smartystreets/goconvey/convey" ) @@ -29,7 +28,6 @@ func TestMigrations(t *testing.T) { sqlutil.CleanDB(x) mg := NewMigrator(x) - mg.Logger.SetHandler(log15.DiscardHandler()) AddMigrations(mg) err = mg.Start() diff --git a/pkg/services/sqlstore/migrator/migrator.go b/pkg/services/sqlstore/migrator/migrator.go index 399a87273d5..e704826bed3 100644 --- a/pkg/services/sqlstore/migrator/migrator.go +++ b/pkg/services/sqlstore/migrator/migrator.go @@ -107,7 +107,7 @@ func (mg *Migrator) Start() error { } func (mg *Migrator) exec(m Migration) error { - log.Info("Executing migration", "id", m.Id()) + mg.Logger.Info("Executing migration", "id", m.Id()) err := mg.inTransaction(func(sess *xorm.Session) error { diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 81b19717ddf..823a0b18421 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -78,7 +78,7 @@ func NewEngine() { os.Exit(1) } - err = SetEngine(x, setting.Env == setting.DEV) + err = SetEngine(x) if err != nil { sqlog.Error("Fail to initialize orm engine", "error", err) @@ -86,7 +86,7 @@ func NewEngine() { } } -func SetEngine(engine *xorm.Engine, enableLog bool) (err error) { +func SetEngine(engine *xorm.Engine) (err error) { x = engine dialect = migrator.NewDialect(x.DriverName()) From 77a5e3f14dd4c763cccd365b58508ea2e03a24a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 11 Jun 2016 11:54:46 +0200 Subject: [PATCH 27/37] feat(alerting): minor fixes --- pkg/services/alerting/commands.go | 8 ++++---- pkg/services/alerting/extractor.go | 4 +++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pkg/services/alerting/commands.go b/pkg/services/alerting/commands.go index 38ee4ecf99c..0f2cabd2bd3 100644 --- a/pkg/services/alerting/commands.go +++ b/pkg/services/alerting/commands.go @@ -25,13 +25,13 @@ func updateDashboardAlerts(cmd *UpdateDashboardAlertsCommand) error { extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId) - alerts, err := extractor.GetRuleModels() - if err != nil { + if alerts, err := extractor.GetAlerts(); err != nil { return err + } else { + saveAlerts.Alerts = alerts } - saveAlerts.Alerts = alerts - if bus.Dispatch(&saveAlerts); err != nil { + if err := bus.Dispatch(&saveAlerts); err != nil { return err } diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index b7b3b17811e..6a0883c16fa 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -47,7 +47,8 @@ func (e *DashAlertExtractor) lookupDatasourceId(dsName string) (int64, error) { return 0, errors.New("Could not find datasource id for " + dsName) } -func (e *DashAlertExtractor) GetRuleModels() ([]*m.Alert, error) { +func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) { + e.log.Debug("GetAlerts") alerts := make([]*m.Alert, 0) @@ -116,5 +117,6 @@ func (e *DashAlertExtractor) GetRuleModels() ([]*m.Alert, error) { } } + e.log.Debug("Extracted alerts from dashboard", "alertCount", len(alerts)) return alerts, nil } From bf1ea560e9d8599fac83bf591464bce70e6ceca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 11 Jun 2016 12:16:33 +0200 Subject: [PATCH 28/37] feat(logging): error logging improvements --- pkg/api/common.go | 56 +++++++++++++++++++++--------------- pkg/middleware/logger.go | 6 +++- pkg/middleware/middleware.go | 12 ++++---- 3 files changed, 43 insertions(+), 31 deletions(-) diff --git a/pkg/api/common.go b/pkg/api/common.go index f8740201143..9d3ad90783b 100644 --- a/pkg/api/common.go +++ b/pkg/api/common.go @@ -4,7 +4,6 @@ import ( "encoding/json" "net/http" - "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/setting" @@ -21,13 +20,15 @@ var ( ) type Response interface { - WriteTo(out http.ResponseWriter) + WriteTo(ctx *middleware.Context) } type NormalResponse struct { - status int - body []byte - header http.Header + status int + body []byte + header http.Header + errMessage string + err error } func wrap(action interface{}) macaron.Handler { @@ -41,17 +42,21 @@ func wrap(action interface{}) macaron.Handler { res = ServerError(err) } - res.WriteTo(c.Resp) + res.WriteTo(c) } } -func (r *NormalResponse) WriteTo(out http.ResponseWriter) { - header := out.Header() +func (r *NormalResponse) WriteTo(ctx *middleware.Context) { + if r.err != nil { + ctx.Logger.Error(r.errMessage, "error", r.err) + } + + header := ctx.Resp.Header() for k, v := range r.header { header[k] = v } - out.WriteHeader(r.status) - out.Write(r.body) + ctx.Resp.WriteHeader(r.status) + ctx.Resp.Write(r.body) } func (r *NormalResponse) Cache(ttl string) *NormalResponse { @@ -64,7 +69,6 @@ func (r *NormalResponse) Header(key, value string) *NormalResponse { } // functions to create responses - func Empty(status int) *NormalResponse { return Respond(status, nil) } @@ -80,29 +84,35 @@ func ApiSuccess(message string) *NormalResponse { } func ApiError(status int, message string, err error) *NormalResponse { - resp := make(map[string]interface{}) - - if err != nil { - log.Error(4, "%s: %v", message, err) - if setting.Env != setting.PROD { - resp["error"] = err.Error() - } - } + data := make(map[string]interface{}) switch status { case 404: metrics.M_Api_Status_404.Inc(1) - resp["message"] = "Not Found" + data["message"] = "Not Found" case 500: metrics.M_Api_Status_500.Inc(1) - resp["message"] = "Internal Server Error" + data["message"] = "Internal Server Error" } if message != "" { - resp["message"] = message + data["message"] = message } - return Json(status, resp) + if err != nil { + if setting.Env != setting.PROD { + data["error"] = err.Error() + } + } + + resp := Json(status, data) + + if err != nil { + resp.errMessage = message + resp.err = err + } + + return resp } func Respond(status int, body interface{}) *NormalResponse { diff --git a/pkg/middleware/logger.go b/pkg/middleware/logger.go index c39ae2b62b2..c6405ef80f9 100644 --- a/pkg/middleware/logger.go +++ b/pkg/middleware/logger.go @@ -48,7 +48,11 @@ func Logger() macaron.Handler { if ctx, ok := c.Data["ctx"]; ok { ctxTyped := ctx.(*Context) - ctxTyped.Logger.Info("Request Completed", "method", req.Method, "path", req.URL.Path, "status", status, "remote_addr", c.RemoteAddr(), "time_ns", timeTakenMs, "size", rw.Size()) + if status == 500 { + ctxTyped.Logger.Error("Request Completed", "method", req.Method, "path", req.URL.Path, "status", status, "remote_addr", c.RemoteAddr(), "time_ns", timeTakenMs, "size", rw.Size()) + } else { + ctxTyped.Logger.Info("Request Completed", "method", req.Method, "path", req.URL.Path, "status", status, "remote_addr", c.RemoteAddr(), "time_ns", timeTakenMs, "size", rw.Size()) + } } } } diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 3ca6f156341..5d52c68722e 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -80,7 +80,7 @@ func initContextWithAnonymousUser(ctx *Context) bool { func initContextWithUserSessionCookie(ctx *Context) bool { // initialize session if err := ctx.Session.Start(ctx); err != nil { - log.Error(3, "Failed to start session", err) + ctx.Logger.Error("Failed to start session", "error", err) return false } @@ -91,7 +91,7 @@ func initContextWithUserSessionCookie(ctx *Context) bool { query := m.GetSignedInUserQuery{UserId: userId} if err := bus.Dispatch(&query); err != nil { - log.Error(3, "Failed to get user with id %v", userId) + ctx.Logger.Error("Failed to get user with id", "userId", userId) return false } else { ctx.SignedInUser = query.Result @@ -185,7 +185,7 @@ func initContextWithApiKeyFromSession(ctx *Context) bool { keyQuery := m.GetApiKeyByIdQuery{ApiKeyId: keyId.(int64)} if err := bus.Dispatch(&keyQuery); err != nil { - log.Error(3, "Failed to get api key by id", err) + ctx.Logger.Error("Failed to get api key by id", "id", keyId, "error", err) return false } else { apikey := keyQuery.Result @@ -202,7 +202,7 @@ func initContextWithApiKeyFromSession(ctx *Context) bool { // Handle handles and logs error by given status. func (ctx *Context) Handle(status int, title string, err error) { if err != nil { - log.Error(4, "%s: %v", title, err) + ctx.Logger.Error(title, "error", err) if setting.Env != setting.PROD { ctx.Data["ErrorMsg"] = err } @@ -223,9 +223,7 @@ func (ctx *Context) Handle(status int, title string, err error) { func (ctx *Context) JsonOK(message string) { resp := make(map[string]interface{}) - resp["message"] = message - ctx.JSON(200, resp) } @@ -237,7 +235,7 @@ func (ctx *Context) JsonApiErr(status int, message string, err error) { resp := make(map[string]interface{}) if err != nil { - log.Error(4, "%s: %v", message, err) + ctx.Logger.Error(message, "error", err) if setting.Env != setting.PROD { resp["error"] = err.Error() } From 71c1c0ab65b482a225cafdaf66a54dacd11d656e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 11 Jun 2016 13:49:11 +0200 Subject: [PATCH 29/37] feat(alerting): trying to get things to work with new model --- pkg/log/log.go | 7 +++++++ pkg/services/alerting/commands.go | 6 +++++- pkg/services/alerting/engine.go | 6 ++++++ pkg/services/alerting/{rule_reader.go => reader.go} | 0 pkg/services/alerting/scheduler.go | 7 +++---- 5 files changed, 21 insertions(+), 5 deletions(-) rename pkg/services/alerting/{rule_reader.go => reader.go} (100%) diff --git a/pkg/log/log.go b/pkg/log/log.go index 58f3cb89cab..a515f5c1062 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -12,6 +12,7 @@ import ( "gopkg.in/ini.v1" + "github.com/go-stack/stack" "github.com/inconshreveable/log15" "github.com/inconshreveable/log15/term" ) @@ -228,3 +229,9 @@ func LogFilterHandler(maxLevel log15.Lvl, filters map[string]log15.Lvl, h log15. return r.Lvl <= maxLevel }, h) } + +func Stack(skip int) string { + call := stack.Caller(skip) + s := stack.Trace().TrimBelow(call).TrimRuntime() + return s.String() +} diff --git a/pkg/services/alerting/commands.go b/pkg/services/alerting/commands.go index 0f2cabd2bd3..4dba9c65685 100644 --- a/pkg/services/alerting/commands.go +++ b/pkg/services/alerting/commands.go @@ -38,6 +38,10 @@ func updateDashboardAlerts(cmd *UpdateDashboardAlertsCommand) error { return nil } +func getTimeDurationStringToSeconds(str string) int64 { + return 60 +} + func ConvetAlertModelToAlertRule(ruleDef *m.Alert) (*AlertRule, error) { model := &AlertRule{} model.Id = ruleDef.Id @@ -58,7 +62,7 @@ func ConvetAlertModelToAlertRule(ruleDef *m.Alert) (*AlertRule, error) { Level: warning.Get("level").MustFloat64(), } - model.Frequency = ruleDef.Expression.Get("frequency").MustInt64() + model.Frequency = getTimeDurationStringToSeconds(ruleDef.Expression.Get("frequency").MustString()) model.Transform = ruleDef.Expression.Get("transform").Get("type").MustString() model.TransformParams = *ruleDef.Expression.Get("transform") diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go index 11eee8d8302..862e6993e44 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -50,6 +50,12 @@ func (e *Engine) Stop() { } func (e *Engine) alertingTicker() { + defer func() { + if err := recover(); err != nil { + e.log.Error("Scheduler Panic, stopping...", "error", err, "stack", log.Stack(1)) + } + }() + tickIndex := 0 for { diff --git a/pkg/services/alerting/rule_reader.go b/pkg/services/alerting/reader.go similarity index 100% rename from pkg/services/alerting/rule_reader.go rename to pkg/services/alerting/reader.go diff --git a/pkg/services/alerting/scheduler.go b/pkg/services/alerting/scheduler.go index 5b376e8c9d8..c8c99e19032 100644 --- a/pkg/services/alerting/scheduler.go +++ b/pkg/services/alerting/scheduler.go @@ -18,12 +18,12 @@ func NewScheduler() Scheduler { } } -func (s *SchedulerImpl) Update(rules []*AlertRule) { - s.log.Debug("Scheduler: Update") +func (s *SchedulerImpl) Update(alerts []*AlertRule) { + s.log.Debug("Scheduling update", "alerts.count", len(alerts)) jobs := make(map[int64]*AlertJob, 0) - for i, rule := range rules { + for i, rule := range alerts { var job *AlertJob if s.jobs[rule.Id] != nil { job = s.jobs[rule.Id] @@ -40,7 +40,6 @@ func (s *SchedulerImpl) Update(rules []*AlertRule) { jobs[rule.Id] = job } - s.log.Debug("Scheduler: Selected new jobs", "job count", len(jobs)) s.jobs = jobs } From 66c259426278d192cf8f4265916ab321c2c11680 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 11 Jun 2016 14:08:55 +0200 Subject: [PATCH 30/37] feat(alerting): sql update fixes --- pkg/services/alerting/commands.go | 5 +++-- pkg/services/alerting/executor.go | 2 +- pkg/services/sqlstore/alert.go | 2 ++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/services/alerting/commands.go b/pkg/services/alerting/commands.go index 4dba9c65685..2d32396c7e1 100644 --- a/pkg/services/alerting/commands.go +++ b/pkg/services/alerting/commands.go @@ -19,8 +19,9 @@ func init() { func updateDashboardAlerts(cmd *UpdateDashboardAlertsCommand) error { saveAlerts := m.SaveAlertsCommand{ - OrgId: cmd.OrgId, - UserId: cmd.UserId, + OrgId: cmd.OrgId, + UserId: cmd.UserId, + DashboardId: cmd.Dashboard.Id, } extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId) diff --git a/pkg/services/alerting/executor.go b/pkg/services/alerting/executor.go index 1520bc494c0..3ab3e26cb70 100644 --- a/pkg/services/alerting/executor.go +++ b/pkg/services/alerting/executor.go @@ -69,7 +69,7 @@ func (e *ExecutorImpl) executeQuery(job *AlertJob) (tsdb.TimeSeriesSlice, error) } func (e *ExecutorImpl) GetRequestForAlertRule(rule *AlertRule, datasource *m.DataSource) *tsdb.Request { - log.Debug2("GetRequest", "query", rule.Query.Query, "from", rule.Query.From, "datasourceId", datasource.Id) + e.log.Debug("GetRequest", "query", rule.Query.Query, "from", rule.Query.From, "datasourceId", datasource.Id) req := &tsdb.Request{ TimeRange: tsdb.TimeRange{ From: "-" + rule.Query.From, diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index e7166c18932..9d92f0ebb47 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -184,6 +184,7 @@ func upsertAlerts(alerts []*m.Alert, posted []*m.Alert, sess *xorm.Session) erro update = true alert.Id = k.Id alertToUpdate = k + break } } @@ -224,6 +225,7 @@ func deleteMissingAlerts(alerts []*m.Alert, posted []*m.Alert, sess *xorm.Sessio for _, k := range posted { if missingAlert.PanelId == k.PanelId { missing = false + break } } From 0b919c752be7fbb44763a3bb19f0538370318f16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 11 Jun 2016 14:37:33 +0200 Subject: [PATCH 31/37] feat(alerting): poc of dragable alert handles --- public/app/plugins/panel/graph/graph.js | 1 + public/sass/components/_panel_graph.scss | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index 3a42df657a5..621229d9d88 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -13,6 +13,7 @@ define([ 'jquery.flot.fillbelow', 'jquery.flot.crosshair', './jquery.flot.events', + './jquery.flot.alerts', ], function (angular, $, moment, _, kbn, GraphTooltip) { 'use strict'; diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index b830561f816..b040b95ce31 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -315,3 +315,8 @@ font-size: 12px; } +.alert-handle { + padding: 0.4rem;; + background-color: $dark-4; + box-shadow: $search-shadow; +} From f387e39b67ed5d766680a4a1a90d99b11f7aa1cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 11 Jun 2016 14:37:52 +0200 Subject: [PATCH 32/37] mend --- .../plugins/panel/graph/jquery.flot.alerts.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 public/app/plugins/panel/graph/jquery.flot.alerts.ts diff --git a/public/app/plugins/panel/graph/jquery.flot.alerts.ts b/public/app/plugins/panel/graph/jquery.flot.alerts.ts new file mode 100644 index 00000000000..e95e0871d92 --- /dev/null +++ b/public/app/plugins/panel/graph/jquery.flot.alerts.ts @@ -0,0 +1,39 @@ +/// + +import 'jquery.flot'; +import $ from 'jquery'; + +var options = {}; + +function getHandleTemplate(type) { + return ` +
+ + > 100 +
+ `; +} + +function drawAlertHandles(plot, canvascontext) { + var $warnHandle = $(getHandleTemplate('warn')); + + var $placeholder = plot.getPlaceholder(); + $placeholder.find(".alert-warn-handle").remove(); + $placeholder.append($warnHandle); +} + +function shutdown() { +} + +function init(plot, classes) { + plot.hooks.draw.push(drawAlertHandles); + plot.hooks.shutdown.push(shutdown); +} + +$.plot.plugins.push({ + init: init, + options: options, + name: 'navigationControl', + version: '1.4' +}); + From 1500c0e954f8078192a0b18be4d401a0f180a4c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 11 Jun 2016 22:33:02 +0200 Subject: [PATCH 33/37] feat(alerting): alert threshold handles progress --- pkg/services/alerting/alert_rule.go | 8 ++- pkg/services/alerting/commands.go | 56 ----------------- pkg/services/alerting/extractor_test.go | 4 +- pkg/services/alerting/reader.go | 2 +- .../sqlstore/alert_rule_parser_test.go | 55 ----------------- public/app/features/dashboard/viewStateSrv.js | 23 ++++--- public/app/features/panel/panel_ctrl.ts | 4 +- .../app/plugins/panel/graph/alert_tab_ctrl.ts | 46 ++++++++------ public/app/plugins/panel/graph/graph.js | 56 +++++++++++++++++ .../plugins/panel/graph/jquery.flot.alerts.ts | 55 ++++++++++++++--- .../panel/graph/partials/tab_alerting.html | 6 +- public/sass/_variables.dark.scss | 2 +- public/sass/components/_panel_graph.scss | 61 +++++++++++++++++-- public/sass/pages/_dashboard.scss | 6 -- 14 files changed, 217 insertions(+), 167 deletions(-) delete mode 100644 pkg/services/sqlstore/alert_rule_parser_test.go diff --git a/pkg/services/alerting/alert_rule.go b/pkg/services/alerting/alert_rule.go index 8678311a6a2..cfcf17f0a56 100644 --- a/pkg/services/alerting/alert_rule.go +++ b/pkg/services/alerting/alert_rule.go @@ -25,6 +25,10 @@ type AlertRule struct { Transformer Transformer } +func getTimeDurationStringToSeconds(str string) int64 { + return 60 +} + func NewAlertRuleFromDBModel(ruleDef *m.Alert) (*AlertRule, error) { model := &AlertRule{} model.Id = ruleDef.Id @@ -39,13 +43,13 @@ func NewAlertRuleFromDBModel(ruleDef *m.Alert) (*AlertRule, error) { Level: critical.Get("level").MustFloat64(), } - warning := ruleDef.Expression.Get("warning") + warning := ruleDef.Expression.Get("warn") model.Warning = Level{ Operator: warning.Get("op").MustString(), Level: warning.Get("level").MustFloat64(), } - model.Frequency = ruleDef.Expression.Get("frequency").MustInt64() + model.Frequency = getTimeDurationStringToSeconds(ruleDef.Expression.Get("frequency").MustString()) model.Transform = ruleDef.Expression.Get("transform").Get("type").MustString() model.TransformParams = *ruleDef.Expression.Get("transform") diff --git a/pkg/services/alerting/commands.go b/pkg/services/alerting/commands.go index 2d32396c7e1..4e269aca695 100644 --- a/pkg/services/alerting/commands.go +++ b/pkg/services/alerting/commands.go @@ -1,8 +1,6 @@ package alerting import ( - "fmt" - "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" ) @@ -38,57 +36,3 @@ func updateDashboardAlerts(cmd *UpdateDashboardAlertsCommand) error { return nil } - -func getTimeDurationStringToSeconds(str string) int64 { - return 60 -} - -func ConvetAlertModelToAlertRule(ruleDef *m.Alert) (*AlertRule, error) { - model := &AlertRule{} - model.Id = ruleDef.Id - model.OrgId = ruleDef.OrgId - model.Name = ruleDef.Name - model.Description = ruleDef.Description - model.State = ruleDef.State - - critical := ruleDef.Expression.Get("critical") - model.Critical = Level{ - Operator: critical.Get("op").MustString(), - Level: critical.Get("level").MustFloat64(), - } - - warning := ruleDef.Expression.Get("warning") - model.Warning = Level{ - Operator: warning.Get("op").MustString(), - Level: warning.Get("level").MustFloat64(), - } - - model.Frequency = getTimeDurationStringToSeconds(ruleDef.Expression.Get("frequency").MustString()) - model.Transform = ruleDef.Expression.Get("transform").Get("type").MustString() - model.TransformParams = *ruleDef.Expression.Get("transform") - - if model.Transform == "aggregation" { - model.Transformer = &AggregationTransformer{ - Method: ruleDef.Expression.Get("transform").Get("method").MustString(), - } - } - - query := ruleDef.Expression.Get("query") - model.Query = AlertQuery{ - Query: query.Get("query").MustString(), - DatasourceId: query.Get("datasourceId").MustInt64(), - From: query.Get("from").MustString(), - To: query.Get("to").MustString(), - Aggregator: query.Get("agg").MustString(), - } - - if model.Query.Query == "" { - return nil, fmt.Errorf("missing query.query") - } - - if model.Query.DatasourceId == 0 { - return nil, fmt.Errorf("missing query.datasourceId") - } - - return model, nil -} diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go index 069489dfb23..6eda321f0dc 100644 --- a/pkg/services/alerting/extractor_test.go +++ b/pkg/services/alerting/extractor_test.go @@ -55,7 +55,7 @@ func TestAlertRuleExtraction(t *testing.T) { "method": "avg", "type": "aggregation" }, - "warning": { + "warn": { "level": 10, "op": ">" } @@ -90,7 +90,7 @@ func TestAlertRuleExtraction(t *testing.T) { "method": "avg", "name": "aggregation" }, - "warning": { + "warn": { "level": 10, "op": ">" } diff --git a/pkg/services/alerting/reader.go b/pkg/services/alerting/reader.go index 7f8f6b2c5de..db7da930746 100644 --- a/pkg/services/alerting/reader.go +++ b/pkg/services/alerting/reader.go @@ -49,7 +49,7 @@ func (arr *AlertRuleReader) Fetch() []*AlertRule { res := make([]*AlertRule, len(cmd.Result)) for i, ruleDef := range cmd.Result { - model, _ := ConvetAlertModelToAlertRule(ruleDef) + model, _ := NewAlertRuleFromDBModel(ruleDef) res[i] = model } diff --git a/pkg/services/sqlstore/alert_rule_parser_test.go b/pkg/services/sqlstore/alert_rule_parser_test.go deleted file mode 100644 index 8ec7c24429b..00000000000 --- a/pkg/services/sqlstore/alert_rule_parser_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package sqlstore - -import ( - "testing" - - "github.com/grafana/grafana/pkg/components/simplejson" - m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/alerting" - . "github.com/smartystreets/goconvey/convey" -) - -func TestAlertRuleModelParsing(t *testing.T) { - - Convey("Parsing alertRule from expression", t, func() { - alertRuleDAO := &m.Alert{} - json, _ := simplejson.NewJson([]byte(` - { - "frequency": 10, - "warning": { - "op": ">", - "level": 10 - }, - "critical": { - "op": ">", - "level": 20 - }, - "query": { - "refId": "A", - "from": "5m", - "to": "now", - "datasourceId": 1, - "query": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - }, - "transform": { - "type": "aggregation", - "method": "avg" - } - }`)) - - alertRuleDAO.Name = "Test" - alertRuleDAO.Expression = json - rule, _ := alerting.ConvetAlertModelToAlertRule(alertRuleDAO) - - Convey("Confirm that all properties are set", func() { - So(rule.Query.Query, ShouldEqual, "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)") - So(rule.Query.From, ShouldEqual, "5m") - So(rule.Query.To, ShouldEqual, "now") - So(rule.Query.DatasourceId, ShouldEqual, 1) - So(rule.Warning.Level, ShouldEqual, 10) - So(rule.Warning.Operator, ShouldEqual, ">") - So(rule.Critical.Level, ShouldEqual, 20) - So(rule.Critical.Operator, ShouldEqual, ">") - }) - }) -} diff --git a/public/app/features/dashboard/viewStateSrv.js b/public/app/features/dashboard/viewStateSrv.js index 035bfb6ae6e..fe1277c59c0 100644 --- a/public/app/features/dashboard/viewStateSrv.js +++ b/public/app/features/dashboard/viewStateSrv.js @@ -120,25 +120,28 @@ function (angular, _, $) { if (this.panelScopes.length === 0) { return; } if (this.dashboard.meta.fullscreen) { - if (this.fullscreenPanel) { - this.leaveFullscreen(false); - } var panelScope = this.getPanelScope(this.state.panelId); - // panel could be about to be created/added and scope does - // not exist yet if (!panelScope) { return; } + if (this.fullscreenPanel) { + // if already fullscreen + if (this.fullscreenPanel === panelScope) { + return; + } else { + this.leaveFullscreen(false); + } + } + if (!panelScope.ctrl.editModeInitiated) { panelScope.ctrl.initEditMode(); } - this.enterFullscreen(panelScope); - return; - } - - if (this.fullscreenPanel) { + if (!panelScope.ctrl.fullscreen) { + this.enterFullscreen(panelScope); + } + } else if (this.fullscreenPanel) { this.leaveFullscreen(true); } }; diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 0f253b5048a..bcb1980f854 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -152,8 +152,8 @@ export class PanelCtrl { calculatePanelHeight() { if (this.fullscreen) { var docHeight = $(window).height(); - var editHeight = Math.floor(docHeight * 0.3); - var fullscreenHeight = Math.floor(docHeight * 0.7); + var editHeight = Math.floor(docHeight * 0.4); + var fullscreenHeight = Math.floor(docHeight * 0.6); this.containerHeight = this.editMode ? editHeight : fullscreenHeight; } else { this.containerHeight = this.panel.height || this.row.height; diff --git a/public/app/plugins/panel/graph/alert_tab_ctrl.ts b/public/app/plugins/panel/graph/alert_tab_ctrl.ts index 3f83e6db38e..d16274efb56 100644 --- a/public/app/plugins/panel/graph/alert_tab_ctrl.ts +++ b/public/app/plugins/panel/graph/alert_tab_ctrl.ts @@ -50,7 +50,7 @@ export class AlertTabCtrl { notify: [], enabled: false, scheduler: 1, - warning: { op: '>', level: undefined }, + warn: { op: '>', level: undefined }, critical: { op: '>', level: undefined }, query: { refId: 'A', @@ -70,8 +70,16 @@ export class AlertTabCtrl { $scope.ctrl = this; this.metricTargets = this.panel.targets.map(val => val); - this.initAlertModel(); + + // set panel alert edit mode + this.panelCtrl.editingAlert = true; + this.panelCtrl.render(); + + $scope.$on("$destroy", () => { + this.panelCtrl.editingAlert = false; + this.panelCtrl.render(); + }); } initAlertModel() { @@ -125,21 +133,21 @@ export class AlertTabCtrl { } convertThresholdsToAlertThresholds() { - if (this.panel.grid - && this.panel.grid.threshold1 - && this.alert.warnLevel === undefined - ) { - this.alert.warning.op = '>'; - this.alert.warning.level = this.panel.grid.threshold1; - } - - if (this.panel.grid - && this.panel.grid.threshold2 - && this.alert.critical.level === undefined - ) { - this.alert.critical.op = '>'; - this.alert.critical.level = this.panel.grid.threshold2; - } + // if (this.panel.grid + // && this.panel.grid.threshold1 + // && this.alert.warnLevel === undefined + // ) { + // this.alert.warning.op = '>'; + // this.alert.warning.level = this.panel.grid.threshold1; + // } + // + // if (this.panel.grid + // && this.panel.grid.threshold2 + // && this.alert.critical.level === undefined + // ) { + // this.alert.critical.op = '>'; + // this.alert.critical.level = this.panel.grid.threshold2; + // } } delete() { @@ -156,6 +164,10 @@ export class AlertTabCtrl { disable() { this.alert.enabled = false; } + + levelsUpdated() { + this.panelCtrl.render(); + } } /** @ngInject */ diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index 621229d9d88..23c9cd65a9f 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -169,6 +169,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) { var right = panel.yaxes[1]; if (left.show && left.label) { gridMargin.left = 20; } if (right.show && right.label) { gridMargin.right = 20; } + } // Function for rendering panel @@ -178,6 +179,12 @@ function (angular, $, moment, _, kbn, GraphTooltip) { panelWidth = panelWidthCache[panel.span] = elem.width(); } + if (ctrl.editingAlert) { + elem.css('margin-right', '220px'); + } else { + elem.css('margin-right', ''); + } + if (shouldAbortRender()) { return; } @@ -186,6 +193,10 @@ function (angular, $, moment, _, kbn, GraphTooltip) { // Populate element var options = { + alerting: { + editing: ctrl.editingAlert, + alert: panel.alert, + }, hooks: { draw: [drawHook], processOffset: [processOffsetHook], @@ -260,6 +271,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) { function callPlot(incrementRenderCounter) { try { + console.log('rendering'); $.plot(elem, sortedSeries, options); } catch (e) { console.log('flotcharts error', e); @@ -312,6 +324,50 @@ function (angular, $, moment, _, kbn, GraphTooltip) { } function addGridThresholds(options, panel) { + if (panel.alert && panel.alert.enabled) { + var crit = panel.alert.critical; + var warn = panel.alert.warn; + var critEdge = Infinity; + var warnEdge = crit.level; + + if (_.isNumber(crit.level)) { + if (crit.op === '<') { + critEdge = -Infinity; + } + + // fill + options.grid.markings.push({ + yaxis: {from: crit.level, to: critEdge}, + color: 'rgba(234, 112, 112, 0.10)', + }); + + // line + options.grid.markings.push({ + yaxis: {from: crit.level, to: crit.level}, + color: '#ed2e18' + }); + } + + if (_.isNumber(warn.level)) { + // if (warn.op === '<') { + // } + + // fill + options.grid.markings.push({ + yaxis: {from: warn.level, to: warnEdge}, + color: 'rgba(216, 200, 27, 0.10)', + }); + + // line + options.grid.markings.push({ + yaxis: {from: warn.level, to: warn.level}, + color: '#F79520' + }); + } + + return; + } + if (_.isNumber(panel.grid.threshold1)) { var limit1 = panel.grid.thresholdLine ? panel.grid.threshold1 : (panel.grid.threshold2 || null); options.grid.markings.push({ diff --git a/public/app/plugins/panel/graph/jquery.flot.alerts.ts b/public/app/plugins/panel/graph/jquery.flot.alerts.ts index e95e0871d92..65229b31065 100644 --- a/public/app/plugins/panel/graph/jquery.flot.alerts.ts +++ b/public/app/plugins/panel/graph/jquery.flot.alerts.ts @@ -2,24 +2,63 @@ import 'jquery.flot'; import $ from 'jquery'; +import _ from 'lodash'; var options = {}; -function getHandleTemplate(type) { +function getHandleTemplate(type, op, value) { + if (op === '>') { op = '>'; } + if (op === '<') { op = '<'; } + return ` -
- - > 100 +
+
+
+
+ + ${op} ${value} +
`; } -function drawAlertHandles(plot, canvascontext) { - var $warnHandle = $(getHandleTemplate('warn')); +function drawAlertHandles(plot) { + var options = plot.getOptions(); var $placeholder = plot.getPlaceholder(); - $placeholder.find(".alert-warn-handle").remove(); - $placeholder.append($warnHandle); + + if (!options.alerting.editing) { + $placeholder.find(".alert-handle").remove(); + return; + } + + var alert = options.alerting.alert; + var height = plot.height(); + + function renderHandle(type, model) { + var $handle = $placeholder.find(`.alert-handle-${type}`); + + if (!_.isNumber(model.level)) { + $handle.remove(); + return; + } + + if ($handle.length === 0) { + $handle = $(getHandleTemplate(type, model.op, model.level)); + $placeholder.append($handle); + } else { + $handle.html(getHandleTemplate(type, model.op, model.level)); + } + + var levelCanvasPos = plot.p2c({x: 0, y: model.level}); + console.log('canvas level pos', levelCanvasPos.top); + + var levelTopPos = Math.min(Math.max(levelCanvasPos.top, 0), height) - 6; + $handle.css({top: levelTopPos}); + } + + renderHandle('critical', alert.critical); + renderHandle('warn', alert.warn); } function shutdown() { diff --git a/public/app/plugins/panel/graph/partials/tab_alerting.html b/public/app/plugins/panel/graph/partials/tab_alerting.html index 2b7b4e112e3..a81bbe2f996 100644 --- a/public/app/plugins/panel/graph/partials/tab_alerting.html +++ b/public/app/plugins/panel/graph/partials/tab_alerting.html @@ -44,8 +44,8 @@ Warn if - - + +
@@ -53,7 +53,7 @@ Critcal if - +
diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 047e50faa73..99070cad837 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -44,7 +44,7 @@ $brand-text-highlight: #f7941d; // Status colors // ------------------------- $online: #10a345; -$warn: #ffc03c; +$warn: #F79520; $critical: #ed2e18; // Scaffolding diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index b040b95ce31..20b9376b7dd 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -315,8 +315,61 @@ font-size: 12px; } -.alert-handle { - padding: 0.4rem;; - background-color: $dark-4; - box-shadow: $search-shadow; +.alert-handle-wrapper { + position: absolute; + + &--warn { + right: -221px; + width: 238px; + + .alert-handle-line { + float: left; + height: 2px; + width: 138px; + margin-top: 14px; + background-color: $warn; + z-index: 0; + position: relative; + } + } + + &--critical { + right: -105px; + width: 123px; + + .alert-handle-line { + float: left; + height: 2px; + width: 23px; + margin-top: 14px; + background-color: $critical; + z-index: 0; + position: relative; + } + } + + + .alert-handle { + z-index: 10; + position: relative; + float: right; + padding: 0.4rem;; + background-color: $btn-inverse-bg; + box-shadow: $search-shadow; + cursor: pointer; + width: 100px; + font-size: $font-size-sm; + box-shadow: 4px 4px 3px 0px $body-bg; + border-radius: 4px; + border-width: 0 1px 1px 0; + border-style: solid; + border-color: $black; + + .icon-gf { + font-size: 17px; + position: relative; + top: 2px; + } + } + } diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index 8fb1e6bcdaa..eec86e78234 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -197,12 +197,6 @@ div.flot-text { bottom: 0; } -.panel-fullscreen { - .panel-title-container { - padding: 8px; - } -} - .panel-full-edit { margin-top: 20px; margin-bottom: 20px; From ec640bd5dab298ae47aa7d8ee0324b34b9e814fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 11 Jun 2016 23:31:49 +0200 Subject: [PATCH 34/37] feat(alerting): more work on alerting ui --- public/app/plugins/panel/graph/graph.js | 1 - .../plugins/panel/graph/jquery.flot.alerts.ts | 22 ++++++++-- .../panel/graph/partials/tab_alerting.html | 44 +++++++++---------- public/sass/components/_panel_graph.scss | 11 +++-- public/vendor/flot/jquery.flot.js | 2 +- 5 files changed, 49 insertions(+), 31 deletions(-) diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index 23c9cd65a9f..13dcf76857d 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -169,7 +169,6 @@ function (angular, $, moment, _, kbn, GraphTooltip) { var right = panel.yaxes[1]; if (left.show && left.label) { gridMargin.left = 20; } if (right.show && right.label) { gridMargin.right = 20; } - } // Function for rendering panel diff --git a/public/app/plugins/panel/graph/jquery.flot.alerts.ts b/public/app/plugins/panel/graph/jquery.flot.alerts.ts index 65229b31065..1ce4783cdf9 100644 --- a/public/app/plugins/panel/graph/jquery.flot.alerts.ts +++ b/public/app/plugins/panel/graph/jquery.flot.alerts.ts @@ -22,6 +22,17 @@ function getHandleTemplate(type, op, value) { `; } +var dragGhostElem = document.createElement('div'); + +function dragStartHandler(evt) { + evt.dataTransfer.setDragImage(dragGhostElem, -99999, -99999); +} + +function dragEndHandler() { + console.log('drag end'); +} + +var past; function drawAlertHandles(plot) { var options = plot.getOptions(); @@ -36,7 +47,7 @@ function drawAlertHandles(plot) { var height = plot.height(); function renderHandle(type, model) { - var $handle = $placeholder.find(`.alert-handle-${type}`); + var $handle = $placeholder.find(`.alert-handle-wrapper--${type}`); if (!_.isNumber(model.level)) { $handle.remove(); @@ -44,15 +55,19 @@ function drawAlertHandles(plot) { } if ($handle.length === 0) { + console.log('not found'); $handle = $(getHandleTemplate(type, model.op, model.level)); + $handle.attr('draggable', true); + $handle.bind('dragend', dragEndHandler); + $handle.bind('dragstart', dragStartHandler); $placeholder.append($handle); + console.log('registering drag events'); } else { + console.log('reusing!'); $handle.html(getHandleTemplate(type, model.op, model.level)); } var levelCanvasPos = plot.p2c({x: 0, y: model.level}); - console.log('canvas level pos', levelCanvasPos.top); - var levelTopPos = Math.min(Math.max(levelCanvasPos.top, 0), height) - 6; $handle.css({top: levelTopPos}); } @@ -62,6 +77,7 @@ function drawAlertHandles(plot) { } function shutdown() { + console.log('shutdown'); } function init(plot, classes) { diff --git a/public/app/plugins/panel/graph/partials/tab_alerting.html b/public/app/plugins/panel/graph/partials/tab_alerting.html index a81bbe2f996..a90c3b78209 100644 --- a/public/app/plugins/panel/graph/partials/tab_alerting.html +++ b/public/app/plugins/panel/graph/partials/tab_alerting.html @@ -36,28 +36,28 @@
-
-
Levels
-
-
- - - Warn if - - - -
-
- - - Critcal if - - - -
-
-
-
+ + + + + + + + + + + + + + + + + + + + + +
diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index 20b9376b7dd..4eb8b703534 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -317,9 +317,10 @@ .alert-handle-wrapper { position: absolute; + user-select: none; &--warn { - right: -221px; + right: -111px; width: 238px; .alert-handle-line { @@ -334,7 +335,7 @@ } &--critical { - right: -105px; + right: -54px; width: 123px; .alert-handle-line { @@ -353,7 +354,7 @@ z-index: 10; position: relative; float: right; - padding: 0.4rem;; + padding: 0.4rem 0.6rem 0.4rem 0.4rem; background-color: $btn-inverse-bg; box-shadow: $search-shadow; cursor: pointer; @@ -364,11 +365,13 @@ border-width: 0 1px 1px 0; border-style: solid; border-color: $black; + text-align: right; .icon-gf { font-size: 17px; position: relative; - top: 2px; + top: 0px; + float: left; } } diff --git a/public/vendor/flot/jquery.flot.js b/public/vendor/flot/jquery.flot.js index 380030a77c9..e2c460ddbd0 100644 --- a/public/vendor/flot/jquery.flot.js +++ b/public/vendor/flot/jquery.flot.js @@ -1322,7 +1322,7 @@ Licensed under the MIT license. placeholder.css("padding", 0) // padding messes up the positioning .children().filter(function(){ - return !$(this).hasClass("flot-overlay") && !$(this).hasClass('flot-base'); + return $(this).hasClass("flot-text"); }).remove(); if (placeholder.css("position") == 'static') From 5b6fb3b124a058ab31e97f90c157d039cf245bac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 11 Jun 2016 23:52:25 +0200 Subject: [PATCH 35/37] feat(alerting): level handle progress --- .../plugins/panel/graph/jquery.flot.alerts.ts | 35 ++++++++++--------- public/sass/components/_panel_graph.scss | 4 +-- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/public/app/plugins/panel/graph/jquery.flot.alerts.ts b/public/app/plugins/panel/graph/jquery.flot.alerts.ts index 1ce4783cdf9..630f58f85e9 100644 --- a/public/app/plugins/panel/graph/jquery.flot.alerts.ts +++ b/public/app/plugins/panel/graph/jquery.flot.alerts.ts @@ -6,18 +6,24 @@ import _ from 'lodash'; var options = {}; -function getHandleTemplate(type, op, value) { +function getHandleInnerHtml(type, op, value) { if (op === '>') { op = '>'; } if (op === '<') { op = '<'; } - return ` + return ` +
+
+
+ + ${op} ${value} +
`; +} + +function getFullHandleHtml(type, op, value) { + var innerTemplate = getHandleInnerHtml(type, op, value); + return `
-
-
-
- - ${op} ${value} -
+ ${innerTemplate}
`; } @@ -32,14 +38,12 @@ function dragEndHandler() { console.log('drag end'); } -var past; - function drawAlertHandles(plot) { var options = plot.getOptions(); var $placeholder = plot.getPlaceholder(); if (!options.alerting.editing) { - $placeholder.find(".alert-handle").remove(); + $placeholder.find(".alert-handle-wrapper").remove(); return; } @@ -55,16 +59,15 @@ function drawAlertHandles(plot) { } if ($handle.length === 0) { - console.log('not found'); - $handle = $(getHandleTemplate(type, model.op, model.level)); + console.log('creating handle'); + $handle = $(getFullHandleHtml(type, model.op, model.level)); $handle.attr('draggable', true); $handle.bind('dragend', dragEndHandler); $handle.bind('dragstart', dragStartHandler); $placeholder.append($handle); - console.log('registering drag events'); } else { - console.log('reusing!'); - $handle.html(getHandleTemplate(type, model.op, model.level)); + console.log('reusing handle!'); + $handle.html(getHandleInnerHtml(type, model.op, model.level)); } var levelCanvasPos = plot.p2c({x: 0, y: model.level}); diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index 4eb8b703534..c2c574d48dc 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -320,7 +320,7 @@ user-select: none; &--warn { - right: -111px; + right: -222px; width: 238px; .alert-handle-line { @@ -335,7 +335,7 @@ } &--critical { - right: -54px; + right: -105px; width: 123px; .alert-handle-line { From e3b281dbac0fb959c917ced7823992684c9d84f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 12 Jun 2016 11:43:18 +0200 Subject: [PATCH 36/37] feat(alerting): more work on alerting thresholds --- pkg/services/alerting/extractor.go | 7 +- pkg/services/alerting/extractor_test.go | 7 +- .../app/plugins/panel/graph/alert_handle.ts | 135 ++++++++++++ .../app/plugins/panel/graph/alert_tab_ctrl.ts | 26 +-- public/app/plugins/panel/graph/graph.js | 36 ++-- .../plugins/panel/graph/jquery.flot.alerts.ts | 97 --------- .../panel/graph/partials/tab_alerting.html | 192 +++++++++--------- public/sass/components/_panel_graph.scss | 67 +++--- 8 files changed, 308 insertions(+), 259 deletions(-) create mode 100644 public/app/plugins/panel/graph/alert_handle.ts delete mode 100644 public/app/plugins/panel/graph/jquery.flot.alerts.ts diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index 6a0883c16fa..ae360973fad 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -57,12 +57,9 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) { for _, panelObj := range row.Get("panels").MustArray() { panel := simplejson.NewFromAny(panelObj) - jsonAlert := panel.Get("alert") + jsonAlert, hasAlert := panel.CheckGet("alert") - // check if marked for deletion - deleted := jsonAlert.Get("deleted").MustBool() - if deleted { - e.log.Info("Deleted alert rule found") + if !hasAlert { continue } diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go index 6eda321f0dc..7cab1d94c7c 100644 --- a/pkg/services/alerting/extractor_test.go +++ b/pkg/services/alerting/extractor_test.go @@ -149,10 +149,7 @@ func TestAlertRuleExtraction(t *testing.T) { ], "title": "Broken influxdb panel", "transform": "table", - "type": "table", - "alert": { - "deleted": true - } + "type": "table" } ], "title": "New row" @@ -185,7 +182,7 @@ func TestAlertRuleExtraction(t *testing.T) { return nil }) - alerts, err := extractor.GetRuleModels() + alerts, err := extractor.GetAlerts() Convey("Get rules without error", func() { So(err, ShouldBeNil) diff --git a/public/app/plugins/panel/graph/alert_handle.ts b/public/app/plugins/panel/graph/alert_handle.ts new file mode 100644 index 00000000000..c1914196e89 --- /dev/null +++ b/public/app/plugins/panel/graph/alert_handle.ts @@ -0,0 +1,135 @@ +/// + +import 'jquery.flot'; +import $ from 'jquery'; +import _ from 'lodash'; + +export class AlertHandleManager { + plot: any; + placeholder: any; + height: any; + alert: any; + + constructor(private panelCtrl) { + this.alert = panelCtrl.panel.alert; + } + + getHandleInnerHtml(type, op, value) { + if (op === '>') { op = '>'; } + if (op === '<') { op = '<'; } + + return ` +
+
+
+ + ${op} ${value} +
`; + } + + getFullHandleHtml(type, op, value) { + var innerTemplate = this.getHandleInnerHtml(type, op, value); + return ` +
+ ${innerTemplate} +
+ `; + } + + setupDragging(handleElem, levelModel) { + var isMoving = false; + var lastY = null; + var posTop; + var plot = this.plot; + var panelCtrl = this.panelCtrl; + + function dragging(evt) { + if (lastY === null) { + lastY = evt.clientY; + } else { + var diff = evt.clientY - lastY; + posTop = posTop + diff; + lastY = evt.clientY; + handleElem.css({top: posTop + diff}); + } + } + + function stopped() { + isMoving = false; + // calculate graph level + var graphLevel = plot.c2p({left: 0, top: posTop}).y; + console.log('canvasPos:' + posTop + ' Graph level: ' + graphLevel); + graphLevel = parseInt(graphLevel.toFixed(0)); + levelModel.level = graphLevel; + console.log(levelModel); + + var levelCanvasPos = plot.p2c({x: 0, y: graphLevel}); + console.log('canvas pos', levelCanvasPos); + + console.log('stopped'); + handleElem.off("mousemove", dragging); + handleElem.off("mouseup", dragging); + + // trigger digest and render + panelCtrl.$scope.$apply(function() { + panelCtrl.render(); + }); + } + + handleElem.bind('mousedown', function() { + isMoving = true; + lastY = null; + posTop = handleElem.position().top; + console.log('start pos', posTop); + + handleElem.on("mousemove", dragging); + handleElem.on("mouseup", stopped); + }); + } + + cleanUp() { + if (this.placeholder) { + this.placeholder.find(".alert-handle-wrapper").remove(); + } + } + + renderHandle(type, model, defaultHandleTopPos) { + var handleElem = this.placeholder.find(`.alert-handle-wrapper--${type}`); + var level = model.level; + var levelStr = level; + var handleTopPos = 0; + + // handle no value + if (!_.isNumber(level)) { + levelStr = ''; + handleTopPos = defaultHandleTopPos; + } else { + var levelCanvasPos = this.plot.p2c({x: 0, y: level}); + handleTopPos = Math.min(Math.max(levelCanvasPos.top, 0), this.height) - 6; + } + + if (handleElem.length === 0) { + console.log('creating handle'); + handleElem = $(this.getFullHandleHtml(type, model.op, levelStr)); + this.placeholder.append(handleElem); + this.setupDragging(handleElem, model); + } else { + console.log('reusing handle!'); + handleElem.html(this.getHandleInnerHtml(type, model.op, levelStr)); + } + + handleElem.toggleClass('alert-handle-wrapper--no-value', levelStr === ''); + handleElem.css({top: handleTopPos}); + } + + draw(plot) { + this.plot = plot; + this.placeholder = plot.getPlaceholder(); + this.height = plot.height(); + + this.renderHandle('critical', this.alert.critical, 10); + this.renderHandle('warn', this.alert.warn, this.height-30); + } + +} + diff --git a/public/app/plugins/panel/graph/alert_tab_ctrl.ts b/public/app/plugins/panel/graph/alert_tab_ctrl.ts index d16274efb56..82ac33e3d47 100644 --- a/public/app/plugins/panel/graph/alert_tab_ctrl.ts +++ b/public/app/plugins/panel/graph/alert_tab_ctrl.ts @@ -73,9 +73,6 @@ export class AlertTabCtrl { this.initAlertModel(); // set panel alert edit mode - this.panelCtrl.editingAlert = true; - this.panelCtrl.render(); - $scope.$on("$destroy", () => { this.panelCtrl.editingAlert = false; this.panelCtrl.render(); @@ -83,7 +80,11 @@ export class AlertTabCtrl { } initAlertModel() { - this.alert = this.panel.alert = this.panel.alert || {}; + if (!this.panel.alert) { + return; + } + + this.alert = this.panel.alert; // set defaults _.defaults(this.alert, this.defaultValues); @@ -105,6 +106,9 @@ export class AlertTabCtrl { this.query = new QueryPart(this.queryParams, alertQueryDef); this.convertThresholdsToAlertThresholds(); this.transformDef = _.findWhere(this.transforms, {type: this.alert.transform.type}); + + this.panelCtrl.editingAlert = true; + this.panelCtrl.render(); } queryUpdated() { @@ -151,18 +155,14 @@ export class AlertTabCtrl { } delete() { - this.alert = this.panel.alert = {}; - this.alert.deleted = true; - this.initAlertModel(); + delete this.panel.alert; + this.panelCtrl.editingAlert = false; + this.panelCtrl.render(); } enable() { - delete this.alert.deleted; - this.alert.enabled = true; - } - - disable() { - this.alert.enabled = false; + this.panel.alert = {}; + this.initAlertModel(); } levelsUpdated() { diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index 13dcf76857d..599bbe554bf 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -5,6 +5,7 @@ define([ 'lodash', 'app/core/utils/kbn', './graph_tooltip', + './alert_handle', 'jquery.flot', 'jquery.flot.selection', 'jquery.flot.time', @@ -13,15 +14,17 @@ define([ 'jquery.flot.fillbelow', 'jquery.flot.crosshair', './jquery.flot.events', - './jquery.flot.alerts', ], -function (angular, $, moment, _, kbn, GraphTooltip) { +function (angular, $, moment, _, kbn, GraphTooltip, AlertHandle) { 'use strict'; var module = angular.module('grafana.directives'); var labelWidthCache = {}; var panelWidthCache = {}; + // systemjs export + var AlertHandleManager = AlertHandle.AlertHandleManager; + module.directive('grafanaGraph', function($rootScope, timeSrv) { return { restrict: 'A', @@ -35,6 +38,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) { var legendSideLastValue = null; var rootScope = scope.$root; var panelWidth = 0; + var alertHandles; rootScope.onAppEvent('setCrosshair', function(event, info) { // do not need to to this if event is from this panel @@ -162,6 +166,10 @@ function (angular, $, moment, _, kbn, GraphTooltip) { rightLabel[0].style.marginTop = (getLabelWidth(panel.yaxes[1].label, rightLabel) / 2) + 'px'; } + + if (alertHandles) { + alertHandles.draw(plot); + } } function processOffsetHook(plot, gridMargin) { @@ -178,24 +186,26 @@ function (angular, $, moment, _, kbn, GraphTooltip) { panelWidth = panelWidthCache[panel.span] = elem.width(); } - if (ctrl.editingAlert) { - elem.css('margin-right', '220px'); - } else { - elem.css('margin-right', ''); - } - if (shouldAbortRender()) { return; } + // give space to alert editing + if (ctrl.editingAlert) { + if (!alertHandles) { + elem.css('margin-right', '220px'); + alertHandles = new AlertHandleManager(ctrl); + } + } else if (alertHandles) { + elem.css('margin-right', '0'); + alertHandles.cleanUp(); + alertHandles = null; + } + var stack = panel.stack ? true : null; // Populate element var options = { - alerting: { - editing: ctrl.editingAlert, - alert: panel.alert, - }, hooks: { draw: [drawHook], processOffset: [processOffsetHook], @@ -323,7 +333,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) { } function addGridThresholds(options, panel) { - if (panel.alert && panel.alert.enabled) { + if (panel.alert) { var crit = panel.alert.critical; var warn = panel.alert.warn; var critEdge = Infinity; diff --git a/public/app/plugins/panel/graph/jquery.flot.alerts.ts b/public/app/plugins/panel/graph/jquery.flot.alerts.ts deleted file mode 100644 index 630f58f85e9..00000000000 --- a/public/app/plugins/panel/graph/jquery.flot.alerts.ts +++ /dev/null @@ -1,97 +0,0 @@ -/// - -import 'jquery.flot'; -import $ from 'jquery'; -import _ from 'lodash'; - -var options = {}; - -function getHandleInnerHtml(type, op, value) { - if (op === '>') { op = '>'; } - if (op === '<') { op = '<'; } - - return ` -
-
-
- - ${op} ${value} -
`; -} - -function getFullHandleHtml(type, op, value) { - var innerTemplate = getHandleInnerHtml(type, op, value); - return ` -
- ${innerTemplate} -
- `; -} - -var dragGhostElem = document.createElement('div'); - -function dragStartHandler(evt) { - evt.dataTransfer.setDragImage(dragGhostElem, -99999, -99999); -} - -function dragEndHandler() { - console.log('drag end'); -} - -function drawAlertHandles(plot) { - var options = plot.getOptions(); - var $placeholder = plot.getPlaceholder(); - - if (!options.alerting.editing) { - $placeholder.find(".alert-handle-wrapper").remove(); - return; - } - - var alert = options.alerting.alert; - var height = plot.height(); - - function renderHandle(type, model) { - var $handle = $placeholder.find(`.alert-handle-wrapper--${type}`); - - if (!_.isNumber(model.level)) { - $handle.remove(); - return; - } - - if ($handle.length === 0) { - console.log('creating handle'); - $handle = $(getFullHandleHtml(type, model.op, model.level)); - $handle.attr('draggable', true); - $handle.bind('dragend', dragEndHandler); - $handle.bind('dragstart', dragStartHandler); - $placeholder.append($handle); - } else { - console.log('reusing handle!'); - $handle.html(getHandleInnerHtml(type, model.op, model.level)); - } - - var levelCanvasPos = plot.p2c({x: 0, y: model.level}); - var levelTopPos = Math.min(Math.max(levelCanvasPos.top, 0), height) - 6; - $handle.css({top: levelTopPos}); - } - - renderHandle('critical', alert.critical); - renderHandle('warn', alert.warn); -} - -function shutdown() { - console.log('shutdown'); -} - -function init(plot, classes) { - plot.hooks.draw.push(drawAlertHandles); - plot.hooks.shutdown.push(shutdown); -} - -$.plot.plugins.push({ - init: init, - options: options, - name: 'navigationControl', - version: '1.4' -}); - diff --git a/public/app/plugins/panel/graph/partials/tab_alerting.html b/public/app/plugins/panel/graph/partials/tab_alerting.html index a90c3b78209..c2cce48a073 100644 --- a/public/app/plugins/panel/graph/partials/tab_alerting.html +++ b/public/app/plugins/panel/graph/partials/tab_alerting.html @@ -1,116 +1,120 @@ -
-
-
Alert Query
-
-
- - -
-
- Transform using -
- + +
+
+
+
Alert Query
+
+
+ + +
+
+ Transform using +
+ +
+
+
+ Method +
+ +
+
+
+ Timespan +
-
- Method -
- +
+ +
+
Levels
+
+
+ + + Warn if + + + +
+
+ + + Critcal if + + +
-
-
- Timespan -
- - - - - - - - - - - - - - - - - - - - - - +
+
+
Execution
+
+
+ Scheduler +
+ +
+
+
+ Evaluate every + +
+
+
+
+
Notifications
+
+
+ Groups + + +
+
+
+
-
-
Execution
+
Information
+
+ Alert name + +
- Scheduler -
- -
+ Alert description
- Evaluate every - +
-
-
Notifications
-
-
- Groups - - -
-
-
-
- - -
-
Information
-
- Alert name - -
-
-
- Alert description -
-
- -
-
- - - + +
diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index c2c574d48dc..5c85ac4d465 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -319,37 +319,6 @@ position: absolute; user-select: none; - &--warn { - right: -222px; - width: 238px; - - .alert-handle-line { - float: left; - height: 2px; - width: 138px; - margin-top: 14px; - background-color: $warn; - z-index: 0; - position: relative; - } - } - - &--critical { - right: -105px; - width: 123px; - - .alert-handle-line { - float: left; - height: 2px; - width: 23px; - margin-top: 14px; - background-color: $critical; - z-index: 0; - position: relative; - } - } - - .alert-handle { z-index: 10; position: relative; @@ -357,7 +326,7 @@ padding: 0.4rem 0.6rem 0.4rem 0.4rem; background-color: $btn-inverse-bg; box-shadow: $search-shadow; - cursor: pointer; + cursor: row-resize; width: 100px; font-size: $font-size-sm; box-shadow: 4px 4px 3px 0px $body-bg; @@ -366,6 +335,7 @@ border-style: solid; border-color: $black; text-align: right; + color: $text-muted; .icon-gf { font-size: 17px; @@ -375,4 +345,37 @@ } } + .alert-handle-line { + float: left; + height: 2px; + margin-top: 13px; + z-index: 0; + position: relative; + } + + &--warn { + right: -222px; + width: 238px; + + .alert-handle-line { + width: 138px; + background-color: $warn; + } + } + + &--critical { + right: -105px; + width: 123px; + + .alert-handle-line { + width: 23px; + background-color: $critical; + } + } + + &--no-value { + .alert-handle-line { + display: none; + } + } } From 77746f277d5fa1f65f9745785641447d1d986a1c Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 13 Jun 2016 07:46:54 +0200 Subject: [PATCH 37/37] chore(alerting): move transformers to seperate package --- pkg/services/alerting/alert_rule.go | 8 ++++---- pkg/services/alerting/commands.go | 6 +++--- .../{transformer.go => transformers/aggregation.go} | 12 +++++------- pkg/services/alerting/transformers/transformer.go | 7 +++++++ 4 files changed, 19 insertions(+), 14 deletions(-) rename pkg/services/alerting/{transformer.go => transformers/aggregation.go} (77%) create mode 100644 pkg/services/alerting/transformers/transformer.go diff --git a/pkg/services/alerting/alert_rule.go b/pkg/services/alerting/alert_rule.go index 8678311a6a2..7c9dee551e8 100644 --- a/pkg/services/alerting/alert_rule.go +++ b/pkg/services/alerting/alert_rule.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/services/alerting/transformers" m "github.com/grafana/grafana/pkg/models" ) @@ -22,7 +23,7 @@ type AlertRule struct { Query AlertQuery Transform string TransformParams simplejson.Json - Transformer Transformer + Transformer transformer.Transformer } func NewAlertRuleFromDBModel(ruleDef *m.Alert) (*AlertRule, error) { @@ -50,9 +51,8 @@ func NewAlertRuleFromDBModel(ruleDef *m.Alert) (*AlertRule, error) { model.TransformParams = *ruleDef.Expression.Get("transform") if model.Transform == "aggregation" { - model.Transformer = &AggregationTransformer{ - Method: ruleDef.Expression.Get("transform").Get("method").MustString(), - } + method := ruleDef.Expression.Get("transform").Get("method").MustString() + model.Transformer = transformer.NewAggregationTransformer(method) } query := ruleDef.Expression.Get("query") diff --git a/pkg/services/alerting/commands.go b/pkg/services/alerting/commands.go index 2d32396c7e1..9577366afaf 100644 --- a/pkg/services/alerting/commands.go +++ b/pkg/services/alerting/commands.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting/transformers" ) type UpdateDashboardAlertsCommand struct { @@ -68,9 +69,8 @@ func ConvetAlertModelToAlertRule(ruleDef *m.Alert) (*AlertRule, error) { model.TransformParams = *ruleDef.Expression.Get("transform") if model.Transform == "aggregation" { - model.Transformer = &AggregationTransformer{ - Method: ruleDef.Expression.Get("transform").Get("method").MustString(), - } + method := ruleDef.Expression.Get("transform").Get("method").MustString() + model.Transformer = transformer.NewAggregationTransformer(method) } query := ruleDef.Expression.Get("query") diff --git a/pkg/services/alerting/transformer.go b/pkg/services/alerting/transformers/aggregation.go similarity index 77% rename from pkg/services/alerting/transformer.go rename to pkg/services/alerting/transformers/aggregation.go index 1f574e6fce6..25d9c5c73f4 100644 --- a/pkg/services/alerting/transformer.go +++ b/pkg/services/alerting/transformers/aggregation.go @@ -1,4 +1,4 @@ -package alerting +package transformer import ( "fmt" @@ -7,8 +7,10 @@ import ( "github.com/grafana/grafana/pkg/tsdb" ) -type Transformer interface { - Transform(timeserie *tsdb.TimeSeries) (float64, error) +func NewAggregationTransformer(method string) *AggregationTransformer { + return &AggregationTransformer{ + Method: method, + } } type AggregationTransformer struct { @@ -26,7 +28,6 @@ func (at *AggregationTransformer) Transform(timeserie *tsdb.TimeSeries) (float64 return sum / float64(len(timeserie.Points)), nil } - //"sum": func(series *tsdb.TimeSeries) float64 { if at.Method == "sum" { sum := float64(0) @@ -37,7 +38,6 @@ func (at *AggregationTransformer) Transform(timeserie *tsdb.TimeSeries) (float64 return sum, nil } - //"min": func(series *tsdb.TimeSeries) float64 { if at.Method == "min" { min := timeserie.Points[0][0] @@ -50,7 +50,6 @@ func (at *AggregationTransformer) Transform(timeserie *tsdb.TimeSeries) (float64 return min, nil } - //"max": func(series *tsdb.TimeSeries) float64 { if at.Method == "max" { max := timeserie.Points[0][0] @@ -63,7 +62,6 @@ func (at *AggregationTransformer) Transform(timeserie *tsdb.TimeSeries) (float64 return max, nil } - //"mean": func(series *tsdb.TimeSeries) float64 { if at.Method == "mean" { midPosition := int64(math.Floor(float64(len(timeserie.Points)) / float64(2))) return timeserie.Points[midPosition][0], nil diff --git a/pkg/services/alerting/transformers/transformer.go b/pkg/services/alerting/transformers/transformer.go new file mode 100644 index 00000000000..d5fa9df45c9 --- /dev/null +++ b/pkg/services/alerting/transformers/transformer.go @@ -0,0 +1,7 @@ +package transformer + +import "github.com/grafana/grafana/pkg/tsdb" + +type Transformer interface { + Transform(timeserie *tsdb.TimeSeries) (float64, error) +}