Merge remote-tracking branch 'origin/master' into getting-started-panel-css2
This commit is contained in:
+2
-1
@@ -119,7 +119,8 @@ func AlertTest(c *middleware.Context, dto dtos.AlertTestCommand) Response {
|
||||
res := backendCmd.Result
|
||||
|
||||
dtoRes := &dtos.AlertTestResult{
|
||||
Firing: res.Firing,
|
||||
Firing: res.Firing,
|
||||
ConditionEvals: res.ConditionEvals,
|
||||
}
|
||||
|
||||
if res.Error != nil {
|
||||
|
||||
@@ -310,4 +310,5 @@ func Register(r *macaron.Macaron) {
|
||||
|
||||
InitAppPluginRoutes(r)
|
||||
|
||||
r.NotFound(NotFoundHandler)
|
||||
}
|
||||
|
||||
@@ -35,11 +35,12 @@ type AlertTestCommand struct {
|
||||
}
|
||||
|
||||
type AlertTestResult struct {
|
||||
Firing bool `json:"firing"`
|
||||
TimeMs string `json:"timeMs"`
|
||||
Error string `json:"error,omitempty"`
|
||||
EvalMatches []*EvalMatch `json:"matches,omitempty"`
|
||||
Logs []*AlertTestResultLog `json:"logs,omitempty"`
|
||||
Firing bool `json:"firing"`
|
||||
ConditionEvals string `json:"conditionEvals"`
|
||||
TimeMs string `json:"timeMs"`
|
||||
Error string `json:"error,omitempty"`
|
||||
EvalMatches []*EvalMatch `json:"matches,omitempty"`
|
||||
Logs []*AlertTestResultLog `json:"logs,omitempty"`
|
||||
}
|
||||
|
||||
type AlertTestResultLog struct {
|
||||
|
||||
@@ -96,7 +96,7 @@ func OAuthLogin(ctx *middleware.Context) {
|
||||
}
|
||||
sslcli := &http.Client{Transport: tr}
|
||||
|
||||
oauthCtx = context.TODO()
|
||||
oauthCtx = context.Background()
|
||||
oauthCtx = context.WithValue(oauthCtx, oauth2.HTTPClient, sslcli)
|
||||
}
|
||||
|
||||
@@ -106,6 +106,8 @@ func OAuthLogin(ctx *middleware.Context) {
|
||||
ctx.Handle(500, "login.OAuthLogin(NewTransportWithCode)", err)
|
||||
return
|
||||
}
|
||||
// token.TokenType was defaulting to "bearer", which is out of spec, so we explicitly set to "Bearer"
|
||||
token.TokenType = "Bearer"
|
||||
|
||||
ctx.Logger.Debug("OAuthLogin Got token")
|
||||
|
||||
|
||||
@@ -101,6 +101,7 @@ func sendUsageStats() {
|
||||
metrics["stats.plugins.apps.count"] = len(plugins.Apps)
|
||||
metrics["stats.plugins.panels.count"] = len(plugins.Panels)
|
||||
metrics["stats.plugins.datasources.count"] = len(plugins.DataSources)
|
||||
metrics["stats.alerts.count"] = statsQuery.Result.AlertCount
|
||||
|
||||
dsStats := m.GetDataSourceStatsQuery{}
|
||||
if err := bus.Dispatch(&dsStats); err != nil {
|
||||
|
||||
@@ -187,6 +187,7 @@ func (ctx *Context) Handle(status int, title string, err error) {
|
||||
}
|
||||
|
||||
ctx.Data["Title"] = title
|
||||
ctx.Data["AppSubUrl"] = setting.AppSubUrl
|
||||
ctx.HTML(status, strconv.Itoa(status))
|
||||
}
|
||||
|
||||
|
||||
+24
-50
@@ -19,53 +19,14 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"runtime"
|
||||
|
||||
"gopkg.in/macaron.v1"
|
||||
|
||||
"github.com/go-macaron/inject"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
const (
|
||||
panicHtml = `<html>
|
||||
<head><title>PANIC: %s</title>
|
||||
<meta charset="utf-8" />
|
||||
<style type="text/css">
|
||||
html, body {
|
||||
font-family: "Roboto", sans-serif;
|
||||
color: #333333;
|
||||
background-color: #ea5343;
|
||||
margin: 0px;
|
||||
}
|
||||
h1 {
|
||||
color: #d04526;
|
||||
background-color: #ffffff;
|
||||
padding: 20px;
|
||||
border-bottom: 1px dashed #2b3848;
|
||||
}
|
||||
pre {
|
||||
margin: 20px;
|
||||
padding: 20px;
|
||||
border: 2px solid #2b3848;
|
||||
background-color: #ffffff;
|
||||
white-space: pre-wrap; /* css-3 */
|
||||
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
|
||||
white-space: -pre-wrap; /* Opera 4-6 */
|
||||
white-space: -o-pre-wrap; /* Opera 7 */
|
||||
word-wrap: break-word; /* Internet Explorer 5.5+ */
|
||||
}
|
||||
</style>
|
||||
</head><body>
|
||||
<h1>PANIC</h1>
|
||||
<pre style="font-weight: bold;">%s</pre>
|
||||
<pre>%s</pre>
|
||||
</body>
|
||||
</html>`
|
||||
)
|
||||
|
||||
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)
|
||||
// }
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ type Notifier interface {
|
||||
type ConditionResult struct {
|
||||
Firing bool
|
||||
NoDataFound bool
|
||||
Operator string
|
||||
EvalMatches []*EvalMatch
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ type Query struct {
|
||||
GroupBy []*QueryPart
|
||||
Selects []*Select
|
||||
RawQuery string
|
||||
UseRawQuery bool
|
||||
Alias string
|
||||
|
||||
Interval string
|
||||
|
||||
@@ -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 {
|
||||
@@ -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/`)
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user