diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4dd0adba4c5..b30865b3e60 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,13 @@
* **Graph Panel**: Log base scale on right Y-axis had no effect, max value calc was not applied, [#6534](https://github.com/grafana/grafana/issues/6534)
* **Graph Panel**: Bar width if bars was only used in series override, [#6528](https://github.com/grafana/grafana/issues/6528)
* **UI/Browser**: Fixed issue with page/view header gradient border not showing in Safari, [#6530](https://github.com/grafana/grafana/issues/6530)
+* **UX**: Panel Drop zone visible after duplicating panel, and when entering fullscreen/edit view, [#6598](https://github.com/grafana/grafana/issues/6598)
+* **Templating**: Newly added variable was not visible directly only after dashboard reload, [#6622](https://github.com/grafana/grafana/issues/6622)
+
+### Enhancements
+* **Singlestat**: Support repeated template variables in prefix/postfix [#6595](https://github.com/grafana/grafana/issues/6595)
+* **Templating**: Don't persist variable options with refresh option [#6586](https://github.com/grafana/grafana/issues/6586)
+* **Alerting**: Add ability to have OR conditions (and mixing AND & OR) [#6579](https://github.com/grafana/grafana/issues/6579)
# 4.0-beta1 (2016-11-09)
diff --git a/conf/defaults.ini b/conf/defaults.ini
index b25f3ab6f28..7ead103d027 100644
--- a/conf/defaults.ini
+++ b/conf/defaults.ini
@@ -229,7 +229,7 @@ auth_url = https://accounts.google.com/o/oauth2/auth
token_url = https://accounts.google.com/o/oauth2/token
api_url = https://www.googleapis.com/oauth2/v1/userinfo
allowed_domains =
-hosted_domain =
+hosted_domain =
#################################### Grafana.net Auth ####################
[auth.grafananet]
@@ -390,21 +390,6 @@ global_api_key = -1
global_session = -1
#################################### Alerting ############################
-# docs about alerting can be found in /docs/sources/alerting/
-# __.-/|
-# \`o_O'
-# =( )= +----------------------------+
-# U| | Alerting is still in alpha |
-# /\ /\ / | +----------------------------+
-# ) /^\) ^\/ _)\ |
-# ) /^\/ _) \ |
-# ) _ / / _) \___|_
-# /\ )/\/ || | )_)\___,|))
-# < > |(,,) )__) |
-# || / \)___)\
-# | \____( )___) )____
-# \______(_______;;;)__;;;)
-
[alerting]
# Makes it possible to turn off alert rule execution.
execute_alerts = true
diff --git a/conf/sample.ini b/conf/sample.ini
index e1ed408210a..e8c9c990a63 100644
--- a/conf/sample.ini
+++ b/conf/sample.ini
@@ -339,21 +339,6 @@
;path = /var/lib/grafana/dashboards
#################################### Alerting ######################################
-# docs about alerting can be found in /docs/sources/alerting/
-# __.-/|
-# \`o_O'
-# =( )= +----------------------------+
-# U| | Alerting is still in alpha |
-# /\ /\ / | +----------------------------+
-# ) /^\) ^\/ _)\ |
-# ) /^\/ _) \ |
-# ) _ / / _) \___|_
-# /\ )/\/ || | )_)\___,|))
-# < > |(,,) )__) |
-# || / \)___)\
-# | \____( )___) )____
-# \______(_______;;;)__;;;)
-
[alerting]
# Makes it possible to turn off alert rule execution.
;execute_alerts = true
diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md
index ce9dcdf1b93..dfe84f142ec 100644
--- a/docs/sources/alerting/notifications.md
+++ b/docs/sources/alerting/notifications.md
@@ -98,6 +98,6 @@ Amazon S3 for this and Webdav. So to set that up you need to configure the
[external image uploader](/installation/configuration/#external-image-storage) in your grafana-server ini
config file.
-This is not an optional requirement, you can get slack and email notifications without setting this up.
+This is an optional requirement, you can get slack and email notifications without setting this up.
diff --git a/docs/sources/alerting/rules.md b/docs/sources/alerting/rules.md
index ecab88f48d2..af3cfecfdb8 100644
--- a/docs/sources/alerting/rules.md
+++ b/docs/sources/alerting/rules.md
@@ -55,7 +55,10 @@ Currently the only condition type that exists is a `Query` condition that allows
specify a query letter, time range and an aggregation function. The letter refers to
a query you already have added in the **Metrics** tab. The result from the query and the aggregation function is
a single value that is then used in the threshold check. The query used in an alert rule cannot
-contain any template variables. Currently we only support `AND` operator between conditions.
+contain any template variables. Currently we only support `AND` and `OR` operators between conditions and they are executed serially.
+For example, we have 3 conditions in the following order:
+`condition:A(evaluates to: TRUE) OR condition:B(evaluates to: FALSE) AND condition:C(evaluates to: TRUE)`
+so the result will be calculated as ((TRUE OR FALSE) AND TRUE) = TRUE.
We plan to add other condition types in the future, like `Other Alert`, where you can include the state
of another alert in your conditions, and `Time Of Day`.
diff --git a/docs/sources/datasources/influxdb.md b/docs/sources/datasources/influxdb.md
index a5eab80af06..01b68d5b7dc 100644
--- a/docs/sources/datasources/influxdb.md
+++ b/docs/sources/datasources/influxdb.md
@@ -118,7 +118,7 @@ SHOW TAG VALUES WITH KEY = "hostname" WHERE region =~ /$region/
> Always you `regex values` or `regex wildcard` for All format or multi select format.
-
+
## Annotations
Annotations allows you to overlay rich event information on top of graphs.
diff --git a/docs/sources/datasources/plugin_api.md b/docs/sources/datasources/plugin_api.md
index cdcaca29460..2e2121ed21a 100644
--- a/docs/sources/datasources/plugin_api.md
+++ b/docs/sources/datasources/plugin_api.md
@@ -30,11 +30,5 @@ Even though the data source type name is with lowercase `g`, the directive uses
that is how angular directives needs to be named in order to match an element with name `
%s-
%s- -` -) - var ( dunno = []byte("???") centerDot = []byte("ยท") @@ -151,21 +112,34 @@ func Recovery() macaron.Handler { panicLogger.Error("Request error", "error", err, "stack", string(stack)) - // Lookup the current responsewriter - val := c.GetVal(inject.InterfaceOf((*http.ResponseWriter)(nil))) - res := val.Interface().(http.ResponseWriter) + c.Data["Title"] = "Server Error" + c.Data["AppSubUrl"] = setting.AppSubUrl + + if theErr, ok := err.(error); ok { + c.Data["Title"] = theErr.Error() + } - // respond with panic message while in development mode - var body []byte if setting.Env == setting.DEV { - res.Header().Set("Content-Type", "text/html") - body = []byte(fmt.Sprintf(panicHtml, err, err, stack)) + c.Data["ErrorMsg"] = string(stack) } - res.WriteHeader(http.StatusInternalServerError) - if nil != body { - res.Write(body) - } + c.HTML(500, "500") + + // // Lookup the current responsewriter + // val := c.GetVal(inject.InterfaceOf((*http.ResponseWriter)(nil))) + // res := val.Interface().(http.ResponseWriter) + // + // // respond with panic message while in development mode + // var body []byte + // if setting.Env == setting.DEV { + // res.Header().Set("Content-Type", "text/html") + // body = []byte(fmt.Sprintf(panicHtml, err, err, stack)) + // } + // + // res.WriteHeader(http.StatusInternalServerError) + // if nil != body { + // res.Write(body) + // } } }() diff --git a/pkg/models/stats.go b/pkg/models/stats.go index 067dec763e5..09c251b6cd7 100644 --- a/pkg/models/stats.go +++ b/pkg/models/stats.go @@ -5,6 +5,7 @@ type SystemStats struct { UserCount int64 OrgCount int64 PlaylistCount int64 + AlertCount int64 } type DataSourceStats struct { @@ -29,6 +30,7 @@ type AdminStats struct { DataSourceCount int `json:"data_source_count"` PlaylistCount int `json:"playlist_count"` StarredDbCount int `json:"starred_db_count"` + AlertCount int `json:"alert_count"` } type GetAdminStatsQuery struct { diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index b73db9d590e..e58dbf1d583 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/pkg/services/alerting/conditions/query.go @@ -23,6 +23,7 @@ type QueryCondition struct { Query AlertQuery Reducer QueryReducer Evaluator AlertEvaluator + Operator string HandleRequest tsdb.HandleRequestFunc } @@ -72,6 +73,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio return &alerting.ConditionResult{ Firing: evalMatchCount > 0, NoDataFound: emptySerieCount == len(seriesList), + Operator: c.Operator, EvalMatches: matches, }, nil } @@ -168,8 +170,12 @@ func NewQueryCondition(model *simplejson.Json, index int) (*QueryCondition, erro if err != nil { return nil, err } - condition.Evaluator = evaluator + + operatorJson := model.Get("operator") + operator := operatorJson.Get("type").MustString("and") + condition.Operator = operator + return &condition, nil } diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index 711416970c5..2b252da8c4b 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -17,7 +17,7 @@ type EvalContext struct { EvalMatches []*EvalMatch Logs []*ResultLogEntry Error error - Description string + ConditionEvals string StartTime time.Time EndTime time.Time Rule *Rule diff --git a/pkg/services/alerting/eval_handler.go b/pkg/services/alerting/eval_handler.go index 538c639abb8..075d0833fdf 100644 --- a/pkg/services/alerting/eval_handler.go +++ b/pkg/services/alerting/eval_handler.go @@ -1,6 +1,8 @@ package alerting import ( + "strconv" + "strings" "time" "github.com/grafana/grafana/pkg/log" @@ -21,7 +23,10 @@ func NewEvalHandler() *DefaultEvalHandler { func (e *DefaultEvalHandler) Eval(context *EvalContext) { firing := true - for _, condition := range context.Rule.Conditions { + conditionEvals := "" + + for i := 0; i < len(context.Rule.Conditions); i++ { + condition := context.Rule.Conditions[i] cr, err := condition.Eval(context) if err != nil { context.Error = err @@ -32,15 +37,23 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) { break } - // break if result has not triggered yet - if cr.Firing == false { - firing = false - break + // calculating Firing based on operator + if cr.Operator == "or" { + firing = firing || cr.Firing + } else { + firing = firing && cr.Firing + } + + if i > 0 { + conditionEvals = "[" + conditionEvals + " " + strings.ToUpper(cr.Operator) + " " + strconv.FormatBool(cr.Firing) + "]" + } else { + conditionEvals = strconv.FormatBool(firing) } context.EvalMatches = append(context.EvalMatches, cr.EvalMatches...) } + context.ConditionEvals = conditionEvals + " = " + strconv.FormatBool(firing) context.Firing = firing context.EndTime = time.Now() elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond diff --git a/pkg/services/alerting/eval_handler_test.go b/pkg/services/alerting/eval_handler_test.go index 4c2ec24b506..84905870f52 100644 --- a/pkg/services/alerting/eval_handler_test.go +++ b/pkg/services/alerting/eval_handler_test.go @@ -8,12 +8,13 @@ import ( ) type conditionStub struct { - firing bool - matches []*EvalMatch + firing bool + operator string + matches []*EvalMatch } func (c *conditionStub) Eval(context *EvalContext) (*ConditionResult, error) { - return &ConditionResult{Firing: c.firing, EvalMatches: c.matches}, nil + return &ConditionResult{Firing: c.firing, EvalMatches: c.matches, Operator: c.operator}, nil } func TestAlertingExecutor(t *testing.T) { @@ -29,18 +30,102 @@ func TestAlertingExecutor(t *testing.T) { handler.Eval(context) So(context.Firing, ShouldEqual, true) + So(context.ConditionEvals, ShouldEqual, "true = true") }) Convey("Show return false with not passing asdf", func() { context := NewEvalContext(context.TODO(), &Rule{ Conditions: []Condition{ - &conditionStub{firing: true, matches: []*EvalMatch{&EvalMatch{}, &EvalMatch{}}}, - &conditionStub{firing: false}, + &conditionStub{firing: true, operator: "and", matches: []*EvalMatch{&EvalMatch{}, &EvalMatch{}}}, + &conditionStub{firing: false, operator: "and"}, }, }) handler.Eval(context) So(context.Firing, ShouldEqual, false) + So(context.ConditionEvals, ShouldEqual, "[true AND false] = false") + }) + + Convey("Show return true if any of the condition is passing with OR operator", func() { + context := NewEvalContext(context.TODO(), &Rule{ + Conditions: []Condition{ + &conditionStub{firing: true, operator: "and"}, + &conditionStub{firing: false, operator: "or"}, + }, + }) + + handler.Eval(context) + So(context.Firing, ShouldEqual, true) + So(context.ConditionEvals, ShouldEqual, "[true OR false] = true") + }) + + Convey("Show return false if any of the condition is failing with AND operator", func() { + context := NewEvalContext(context.TODO(), &Rule{ + Conditions: []Condition{ + &conditionStub{firing: true, operator: "and"}, + &conditionStub{firing: false, operator: "and"}, + }, + }) + + handler.Eval(context) + So(context.Firing, ShouldEqual, false) + So(context.ConditionEvals, ShouldEqual, "[true AND false] = false") + }) + + Convey("Show return true if one condition is failing with nested OR operator", func() { + context := NewEvalContext(context.TODO(), &Rule{ + Conditions: []Condition{ + &conditionStub{firing: true, operator: "and"}, + &conditionStub{firing: true, operator: "and"}, + &conditionStub{firing: false, operator: "or"}, + }, + }) + + handler.Eval(context) + So(context.Firing, ShouldEqual, true) + So(context.ConditionEvals, ShouldEqual, "[[true AND true] OR false] = true") + }) + + Convey("Show return false if one condition is passing with nested OR operator", func() { + context := NewEvalContext(context.TODO(), &Rule{ + Conditions: []Condition{ + &conditionStub{firing: true, operator: "and"}, + &conditionStub{firing: false, operator: "and"}, + &conditionStub{firing: false, operator: "or"}, + }, + }) + + handler.Eval(context) + So(context.Firing, ShouldEqual, false) + So(context.ConditionEvals, ShouldEqual, "[[true AND false] OR false] = false") + }) + + Convey("Show return false if a condition is failing with nested AND operator", func() { + context := NewEvalContext(context.TODO(), &Rule{ + Conditions: []Condition{ + &conditionStub{firing: true, operator: "and"}, + &conditionStub{firing: false, operator: "and"}, + &conditionStub{firing: true, operator: "and"}, + }, + }) + + handler.Eval(context) + So(context.Firing, ShouldEqual, false) + So(context.ConditionEvals, ShouldEqual, "[[true AND false] AND true] = false") + }) + + Convey("Show return true if a condition is passing with nested OR operator", func() { + context := NewEvalContext(context.TODO(), &Rule{ + Conditions: []Condition{ + &conditionStub{firing: true, operator: "and"}, + &conditionStub{firing: false, operator: "or"}, + &conditionStub{firing: true, operator: "or"}, + }, + }) + + handler.Eval(context) + So(context.Firing, ShouldEqual, true) + So(context.ConditionEvals, ShouldEqual, "[[true OR false] OR true] = true") }) }) } diff --git a/pkg/services/alerting/interfaces.go b/pkg/services/alerting/interfaces.go index cc2561473e3..566fbdb2898 100644 --- a/pkg/services/alerting/interfaces.go +++ b/pkg/services/alerting/interfaces.go @@ -24,6 +24,7 @@ type Notifier interface { type ConditionResult struct { Firing bool NoDataFound bool + Operator string EvalMatches []*EvalMatch } diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go index 809640ed4a7..bdf53798e34 100644 --- a/pkg/services/alerting/rule.go +++ b/pkg/services/alerting/rule.go @@ -26,11 +26,32 @@ type Rule struct { } type ValidationError struct { - Reason string + Reason string + Err error + Alertid int64 + DashboardId int64 + PanelId int64 } func (e ValidationError) Error() string { - return e.Reason + extraInfo := "" + if e.Alertid != 0 { + extraInfo = fmt.Sprintf("%s AlertId: %v", extraInfo, e.Alertid) + } + + if e.PanelId != 0 { + extraInfo = fmt.Sprintf("%s PanelId: %v ", extraInfo, e.PanelId) + } + + if e.DashboardId != 0 { + extraInfo = fmt.Sprintf("%s DashboardId: %v", extraInfo, e.DashboardId) + } + + if e.Err != nil { + return fmt.Sprintf("%s %s%s", e.Err.Error(), e.Reason, extraInfo) + } + + return fmt.Sprintf("Failed to extract alert.Reason: %s %s", e.Reason, extraInfo) } var ( @@ -83,7 +104,7 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { for _, v := range ruleDef.Settings.Get("notifications").MustArray() { jsonModel := simplejson.NewFromAny(v) if id, err := jsonModel.Get("id").Int64(); err != nil { - return nil, ValidationError{Reason: "Invalid notification schema"} + return nil, ValidationError{Reason: "Invalid notification schema", DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId} } else { model.Notifications = append(model.Notifications, id) } @@ -93,10 +114,10 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { conditionModel := simplejson.NewFromAny(condition) conditionType := conditionModel.Get("type").MustString() if factory, exist := conditionFactories[conditionType]; !exist { - return nil, ValidationError{Reason: "Unknown alert condition: " + conditionType} + return nil, ValidationError{Reason: "Unknown alert condition: " + conditionType, DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId} } else { if queryCondition, err := factory(conditionModel, index); err != nil { - return nil, err + return nil, ValidationError{Err: err, DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId} } else { model.Conditions = append(model.Conditions, queryCondition) } diff --git a/pkg/services/alerting/scheduler.go b/pkg/services/alerting/scheduler.go index b6ef1a63ff8..151f802ec15 100644 --- a/pkg/services/alerting/scheduler.go +++ b/pkg/services/alerting/scheduler.go @@ -39,6 +39,9 @@ func (s *SchedulerImpl) Update(rules []*Rule) { offset := ((rule.Frequency * 1000) / int64(len(rules))) * int64(i) job.Offset = int64(math.Floor(float64(offset) / 1000)) + if job.Offset == 0 { //zero offset causes division with 0 panics. + job.Offset = 1 + } jobs[rule.Id] = job } diff --git a/pkg/services/sqlstore/stats.go b/pkg/services/sqlstore/stats.go index 7580996ad57..dd1a8111332 100644 --- a/pkg/services/sqlstore/stats.go +++ b/pkg/services/sqlstore/stats.go @@ -39,7 +39,11 @@ func GetSystemStats(query *m.GetSystemStatsQuery) error { ( SELECT COUNT(*) FROM ` + dialect.Quote("playlist") + ` - ) AS playlist_count + ) AS playlist_count, + ( + SELECT COUNT(*) + FROM ` + dialect.Quote("alert") + ` + ) AS alert_count ` var stats m.SystemStats @@ -85,7 +89,11 @@ func GetAdminStats(query *m.GetAdminStatsQuery) error { ( SELECT COUNT(DISTINCT ` + dialect.Quote("dashboard_id") + ` ) FROM ` + dialect.Quote("star") + ` - ) AS starred_db_count + ) AS starred_db_count, + ( + SELECT COUNT(*) + FROM ` + dialect.Quote("alert") + ` + ) AS alert_count ` var stats m.AdminStats diff --git a/pkg/tsdb/influxdb/influxdb.go b/pkg/tsdb/influxdb/influxdb.go index a64a6af2ee9..5043a2deffc 100644 --- a/pkg/tsdb/influxdb/influxdb.go +++ b/pkg/tsdb/influxdb/influxdb.go @@ -18,7 +18,6 @@ import ( type InfluxDBExecutor struct { *tsdb.DataSourceInfo QueryParser *InfluxdbQueryParser - QueryBuilder *QueryBuilder ResponseParser *ResponseParser } @@ -26,7 +25,6 @@ func NewInfluxDBExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor { return &InfluxDBExecutor{ DataSourceInfo: dsInfo, QueryParser: &InfluxdbQueryParser{}, - QueryBuilder: &QueryBuilder{}, ResponseParser: &ResponseParser{}, } } @@ -51,7 +49,7 @@ func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, return result.WithError(err) } - rawQuery, err := e.QueryBuilder.Build(query, context) + rawQuery, err := query.Build(context) if err != nil { return result.WithError(err) } @@ -84,6 +82,10 @@ func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, return result.WithError(err) } + if response.Err != nil { + return result.WithError(response.Err) + } + result.QueryResults = make(map[string]*tsdb.QueryResult) result.QueryResults["A"] = e.ResponseParser.Parse(&response, query) diff --git a/pkg/tsdb/influxdb/model_parser.go b/pkg/tsdb/influxdb/model_parser.go index 2db7a78ed75..410bf8f6e82 100644 --- a/pkg/tsdb/influxdb/model_parser.go +++ b/pkg/tsdb/influxdb/model_parser.go @@ -12,6 +12,7 @@ type InfluxdbQueryParser struct{} func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *tsdb.DataSourceInfo) (*Query, error) { policy := model.Get("policy").MustString("default") rawQuery := model.Get("query").MustString("") + useRawQuery := model.Get("rawQuery").MustBool(false) alias := model.Get("alias").MustString("") measurement := model.Get("measurement").MustString("") @@ -54,6 +55,7 @@ func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *tsdb.DataSo RawQuery: rawQuery, Interval: interval, Alias: alias, + UseRawQuery: useRawQuery, }, nil } diff --git a/pkg/tsdb/influxdb/models.go b/pkg/tsdb/influxdb/models.go index 0dcecd20773..44e05608290 100644 --- a/pkg/tsdb/influxdb/models.go +++ b/pkg/tsdb/influxdb/models.go @@ -8,6 +8,7 @@ type Query struct { GroupBy []*QueryPart Selects []*Select RawQuery string + UseRawQuery bool Alias string Interval string diff --git a/pkg/tsdb/influxdb/query_builder.go b/pkg/tsdb/influxdb/query.go similarity index 62% rename from pkg/tsdb/influxdb/query_builder.go rename to pkg/tsdb/influxdb/query.go index b783ff5c603..094f98cbe53 100644 --- a/pkg/tsdb/influxdb/query_builder.go +++ b/pkg/tsdb/influxdb/query.go @@ -2,7 +2,6 @@ package influxdb import ( "fmt" - "strconv" "strings" "regexp" @@ -11,31 +10,30 @@ import ( ) var ( - regexpOperatorPattern *regexp.Regexp = regexp.MustCompile(`^\/.*\/$`) + regexpOperatorPattern *regexp.Regexp = regexp.MustCompile(`^\/.*\/$`) + regexpMeasurementPattern *regexp.Regexp = regexp.MustCompile(`^\/.*\/$`) ) -type QueryBuilder struct{} - -func (qb *QueryBuilder) Build(query *Query, queryContext *tsdb.QueryContext) (string, error) { - if query.RawQuery != "" { +func (query *Query) Build(queryContext *tsdb.QueryContext) (string, error) { + if query.UseRawQuery && query.RawQuery != "" { q := query.RawQuery - q = strings.Replace(q, "$timeFilter", qb.renderTimeFilter(query, queryContext), 1) + q = strings.Replace(q, "$timeFilter", query.renderTimeFilter(queryContext), 1) q = strings.Replace(q, "$interval", tsdb.CalculateInterval(queryContext.TimeRange), 1) return q, nil } - res := qb.renderSelectors(query, queryContext) - res += qb.renderMeasurement(query) - res += qb.renderWhereClause(query) - res += qb.renderTimeFilter(query, queryContext) - res += qb.renderGroupBy(query, queryContext) + res := query.renderSelectors(queryContext) + res += query.renderMeasurement() + res += query.renderWhereClause() + res += query.renderTimeFilter(queryContext) + res += query.renderGroupBy(queryContext) return res, nil } -func (qb *QueryBuilder) renderTags(query *Query) []string { +func (query *Query) renderTags() []string { var res []string for i, tag := range query.Tags { str := "" @@ -59,13 +57,12 @@ func (qb *QueryBuilder) renderTags(query *Query) []string { } textValue := "" - numericValue, err := strconv.ParseFloat(tag.Value, 64) // quote value unless regex or number if tag.Operator == "=~" || tag.Operator == "!~" { textValue = tag.Value - } else if err == nil { - textValue = fmt.Sprintf("%v", numericValue) + } else if tag.Operator == "<" || tag.Operator == ">" { + textValue = tag.Value } else { textValue = fmt.Sprintf("'%s'", tag.Value) } @@ -76,7 +73,7 @@ func (qb *QueryBuilder) renderTags(query *Query) []string { return res } -func (qb *QueryBuilder) renderTimeFilter(query *Query, queryContext *tsdb.QueryContext) string { +func (query *Query) renderTimeFilter(queryContext *tsdb.QueryContext) string { from := "now() - " + queryContext.TimeRange.From to := "" @@ -87,7 +84,7 @@ func (qb *QueryBuilder) renderTimeFilter(query *Query, queryContext *tsdb.QueryC return fmt.Sprintf("time > %s%s", from, to) } -func (qb *QueryBuilder) renderSelectors(query *Query, queryContext *tsdb.QueryContext) string { +func (query *Query) renderSelectors(queryContext *tsdb.QueryContext) string { res := "SELECT " var selectors []string @@ -103,19 +100,26 @@ func (qb *QueryBuilder) renderSelectors(query *Query, queryContext *tsdb.QueryCo return res + strings.Join(selectors, ", ") } -func (qb *QueryBuilder) renderMeasurement(query *Query) string { +func (query *Query) renderMeasurement() string { policy := "" if query.Policy == "" || query.Policy == "default" { policy = "" } else { policy = `"` + query.Policy + `".` } - return fmt.Sprintf(` FROM %s"%s"`, policy, query.Measurement) + + measurement := query.Measurement + + if !regexpMeasurementPattern.Match([]byte(measurement)) { + measurement = fmt.Sprintf(`"%s"`, measurement) + } + + return fmt.Sprintf(` FROM %s%s`, policy, measurement) } -func (qb *QueryBuilder) renderWhereClause(query *Query) string { +func (query *Query) renderWhereClause() string { res := " WHERE " - conditions := qb.renderTags(query) + conditions := query.renderTags() res += strings.Join(conditions, " ") if len(conditions) > 0 { res += " AND " @@ -124,7 +128,7 @@ func (qb *QueryBuilder) renderWhereClause(query *Query) string { return res } -func (qb *QueryBuilder) renderGroupBy(query *Query, queryContext *tsdb.QueryContext) string { +func (query *Query) renderGroupBy(queryContext *tsdb.QueryContext) string { groupBy := "" for i, group := range query.GroupBy { if i == 0 { diff --git a/pkg/tsdb/influxdb/query_builder_test.go b/pkg/tsdb/influxdb/query_test.go similarity index 66% rename from pkg/tsdb/influxdb/query_builder_test.go rename to pkg/tsdb/influxdb/query_test.go index 408db18e549..b6af67e5e42 100644 --- a/pkg/tsdb/influxdb/query_builder_test.go +++ b/pkg/tsdb/influxdb/query_test.go @@ -12,7 +12,6 @@ import ( func TestInfluxdbQueryBuilder(t *testing.T) { Convey("Influxdb query builder", t, func() { - builder := QueryBuilder{} qp1, _ := NewQueryPart("field", []string{"value"}) qp2, _ := NewQueryPart("mean", []string{}) @@ -37,7 +36,7 @@ func TestInfluxdbQueryBuilder(t *testing.T) { Interval: "10s", } - rawQuery, err := builder.Build(query, queryContext) + rawQuery, err := query.Build(queryContext) So(err, ShouldBeNil) So(rawQuery, ShouldEqual, `SELECT mean("value") FROM "policy"."cpu" WHERE time > now() - 5m GROUP BY time(10s) fill(null)`) }) @@ -51,23 +50,22 @@ func TestInfluxdbQueryBuilder(t *testing.T) { Interval: "5s", } - rawQuery, err := builder.Build(query, queryContext) + rawQuery, err := query.Build(queryContext) So(err, ShouldBeNil) So(rawQuery, ShouldEqual, `SELECT mean("value") FROM "cpu" WHERE "hostname" = 'server1' OR "hostname" = 'server2' AND time > now() - 5m GROUP BY time(5s), "datacenter" fill(null)`) }) Convey("can render time range", func() { query := Query{} - builder := &QueryBuilder{} Convey("render from: 2h to now-1h", func() { query := Query{} queryContext := &tsdb.QueryContext{TimeRange: tsdb.NewTimeRange("2h", "now-1h")} - So(builder.renderTimeFilter(&query, queryContext), ShouldEqual, "time > now() - 2h and time < now() - 1h") + So(query.renderTimeFilter(queryContext), ShouldEqual, "time > now() - 2h and time < now() - 1h") }) Convey("render from: 10m", func() { queryContext := &tsdb.QueryContext{TimeRange: tsdb.NewTimeRange("10m", "now")} - So(builder.renderTimeFilter(&query, queryContext), ShouldEqual, "time > now() - 10m") + So(query.renderTimeFilter(queryContext), ShouldEqual, "time > now() - 10m") }) }) @@ -79,9 +77,10 @@ func TestInfluxdbQueryBuilder(t *testing.T) { GroupBy: []*QueryPart{groupBy1, groupBy3}, Interval: "10s", RawQuery: "Raw query", + UseRawQuery: true, } - rawQuery, err := builder.Build(query, queryContext) + rawQuery, err := query.Build(queryContext) So(err, ShouldBeNil) So(rawQuery, ShouldEqual, `Raw query`) }) @@ -89,37 +88,55 @@ func TestInfluxdbQueryBuilder(t *testing.T) { Convey("can render normal tags without operator", func() { query := &Query{Tags: []*Tag{&Tag{Operator: "", Value: `value`, Key: "key"}}} - So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" = 'value'`) + So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" = 'value'`) }) Convey("can render regex tags without operator", func() { query := &Query{Tags: []*Tag{&Tag{Operator: "", Value: `/value/`, Key: "key"}}} - So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" =~ /value/`) + So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" =~ /value/`) }) Convey("can render regex tags", func() { query := &Query{Tags: []*Tag{&Tag{Operator: "=~", Value: `/value/`, Key: "key"}}} - So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" =~ /value/`) + So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" =~ /value/`) }) Convey("can render number tags", func() { query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "10001", Key: "key"}}} - So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" = 10001`) + So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" = '10001'`) }) - Convey("can render number tags with decimals", func() { - query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "10001.1", Key: "key"}}} + Convey("can render numbers less then condition tags", func() { + query := &Query{Tags: []*Tag{&Tag{Operator: "<", Value: "10001", Key: "key"}}} - So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" = 10001.1`) + So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" < 10001`) + }) + + Convey("can render number greather then condition tags", func() { + query := &Query{Tags: []*Tag{&Tag{Operator: ">", Value: "10001", Key: "key"}}} + + So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" > 10001`) }) Convey("can render string tags", func() { query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "value", Key: "key"}}} - So(strings.Join(builder.renderTags(query), ""), ShouldEqual, `"key" = 'value'`) + So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" = 'value'`) + }) + + Convey("can render regular measurement", func() { + query := &Query{Measurement: `apa`, Policy: "policy"} + + So(query.renderMeasurement(), ShouldEqual, ` FROM "policy"."apa"`) + }) + + Convey("can render regexp measurement", func() { + query := &Query{Measurement: `/apa/`, Policy: "policy"} + + So(query.renderMeasurement(), ShouldEqual, ` FROM "policy"./apa/`) }) }) } diff --git a/public/app/app.ts b/public/app/app.ts index c004bac4177..22431a5110c 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -40,7 +40,6 @@ export class GrafanaApp { init() { var app = angular.module('grafana', []); - app.constant('grafanaVersion', "@grafanaVersion@"); moment.locale(config.bootData.user.locale); diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index ed3425f670f..83b55ac476f 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -147,9 +147,14 @@ export function grafanaAppDirective(playlistSrv, contextSrv) { } } + // mouse and keyboard is user activity body.mousemove(userActivityDetected); body.keydown(userActivityDetected); - setInterval(checkForInActiveUser, 1000); + // treat tab change as activity + document.addEventListener('visibilitychange', userActivityDetected); + + // check every 2 seconds + setInterval(checkForInActiveUser, 2000); appEvents.on('toggle-view-mode', () => { lastActivity = 0; diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 567df736a4e..4aa2e7eb64a 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -6,11 +6,10 @@ import "./directives/dash_class"; import "./directives/confirm_click"; import "./directives/dash_edit_link"; import "./directives/dropdown_typeahead"; -import "./directives/grafana_version_check"; import "./directives/metric_segment"; import "./directives/misc"; import "./directives/ng_model_on_blur"; -import "./directives/password_strenght"; +import "./directives/password_strength"; import "./directives/spectrum_picker"; import "./directives/tags"; import "./directives/value_select_dropdown"; diff --git a/public/app/core/directives/grafana_version_check.js b/public/app/core/directives/grafana_version_check.js deleted file mode 100644 index bee437b8183..00000000000 --- a/public/app/core/directives/grafana_version_check.js +++ /dev/null @@ -1,31 +0,0 @@ -define([ - '../core_module', -], -function (coreModule) { - 'use strict'; - - coreModule.default.directive('grafanaVersionCheck', function($http, contextSrv) { - return { - restrict: 'A', - link: function(scope, elem) { - if (contextSrv.version === 'master') { - return; - } - - $http({ method: 'GET', url: 'https://grafanarel.s3.amazonaws.com/latest.json' }) - .then(function(response) { - if (!response.data || !response.data.version) { - return; - } - - if (contextSrv.version !== response.data.version) { - elem.append(' ' + - ' ' + - 'New version available: ' + response.data.version + - ''); - } - }); - } - }; - }); -}); diff --git a/public/app/core/directives/ng_model_on_blur.js b/public/app/core/directives/ng_model_on_blur.js index 6f4a55b53f0..09c87f5fabe 100644 --- a/public/app/core/directives/ng_model_on_blur.js +++ b/public/app/core/directives/ng_model_on_blur.js @@ -47,7 +47,7 @@ function (coreModule, kbn, rangeUtil) { if (ctrl.$isEmpty(modelValue)) { return true; } - if (viewValue.indexOf('$') === 0) { + if (viewValue.indexOf('$') === 0 || viewValue.indexOf('+$') === 0) { return true; // allow template variable } var info = rangeUtil.describeTextRange(viewValue); diff --git a/public/app/core/directives/password_strenght.js b/public/app/core/directives/password_strength.js similarity index 100% rename from public/app/core/directives/password_strenght.js rename to public/app/core/directives/password_strength.js diff --git a/public/app/core/utils/kbn.js b/public/app/core/utils/kbn.js index a807a249235..78489dcae58 100644 --- a/public/app/core/utils/kbn.js +++ b/public/app/core/utils/kbn.js @@ -420,11 +420,11 @@ function($, _, moment) { kbn.valueFormats.bps = kbn.formatBuilders.decimalSIPrefix('bps'); kbn.valueFormats.Bps = kbn.formatBuilders.decimalSIPrefix('Bps'); kbn.valueFormats.KBs = kbn.formatBuilders.decimalSIPrefix('Bs', 1); - kbn.valueFormats.Kbits = kbn.formatBuilders.decimalSIPrefix('bits', 1); + kbn.valueFormats.Kbits = kbn.formatBuilders.decimalSIPrefix('bps', 1); kbn.valueFormats.MBs = kbn.formatBuilders.decimalSIPrefix('Bs', 2); - kbn.valueFormats.Mbits = kbn.formatBuilders.decimalSIPrefix('bits', 2); + kbn.valueFormats.Mbits = kbn.formatBuilders.decimalSIPrefix('bps', 2); kbn.valueFormats.GBs = kbn.formatBuilders.decimalSIPrefix('Bs', 3); - kbn.valueFormats.Gbits = kbn.formatBuilders.decimalSIPrefix('bits', 3); + kbn.valueFormats.Gbits = kbn.formatBuilders.decimalSIPrefix('bps', 3); // Throughput kbn.valueFormats.ops = kbn.formatBuilders.simpleCountUnit('ops'); diff --git a/public/app/features/admin/partials/stats.html b/public/app/features/admin/partials/stats.html index bcc246b9e82..92e32f4f947 100644 --- a/public/app/features/admin/partials/stats.html +++ b/public/app/features/admin/partials/stats.html @@ -46,6 +46,10 @@
[[.ErrorMsg]]+