From 653a0502c453166f3f8cb37436dacd8babaeba3a Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 3 Oct 2016 14:31:33 +0200 Subject: [PATCH 01/84] tech(tsdb): add jsondata to tsdb datasource struct --- pkg/services/alerting/conditions/query.go | 1 + pkg/tsdb/models.go | 1 + 2 files changed, 2 insertions(+) diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index b37123df5c3..af550a1ad18 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/pkg/services/alerting/conditions/query.go @@ -123,6 +123,7 @@ func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource, timeRa BasicAuth: datasource.BasicAuth, BasicAuthUser: datasource.BasicAuthUser, BasicAuthPassword: datasource.BasicAuthPassword, + JsonData: datasource.JsonData, }, }, }, diff --git a/pkg/tsdb/models.go b/pkg/tsdb/models.go index bbf7bba7ac7..918f0ad65eb 100644 --- a/pkg/tsdb/models.go +++ b/pkg/tsdb/models.go @@ -39,6 +39,7 @@ type DataSourceInfo struct { BasicAuth bool BasicAuthUser string BasicAuthPassword string + JsonData *simplejson.Json } type BatchTiming struct { From d889415c4bb75045ce0927b6f6c41d7dfa50bd45 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 3 Oct 2016 14:32:04 +0200 Subject: [PATCH 02/84] tech(tsdb): rename context to query --- pkg/tsdb/executor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tsdb/executor.go b/pkg/tsdb/executor.go index 97736ef4678..cc1d592dcd5 100644 --- a/pkg/tsdb/executor.go +++ b/pkg/tsdb/executor.go @@ -3,7 +3,7 @@ package tsdb import "context" type Executor interface { - Execute(ctx context.Context, queries QuerySlice, context *QueryContext) *BatchResult + Execute(ctx context.Context, queries QuerySlice, query *QueryContext) *BatchResult } var registry map[string]GetExecutorFn From 5a201cfd35b735d1d50513fe680462b3ba6aeac0 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 3 Oct 2016 14:33:47 +0200 Subject: [PATCH 03/84] tech(tsdb): abort batch procces when context closes --- pkg/tsdb/request.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/tsdb/request.go b/pkg/tsdb/request.go index 88c6eb81eac..2934443bc53 100644 --- a/pkg/tsdb/request.go +++ b/pkg/tsdb/request.go @@ -51,6 +51,8 @@ func HandleRequest(ctx context.Context, req *Request) (*Response, error) { go batch.process(ctx, context) } } + case <-ctx.Done(): + return nil, ctx.Err() } } From d726f49a8c49a22cd1b595d65222debc22f46e19 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 3 Oct 2016 17:03:21 +0200 Subject: [PATCH 04/84] fix(notification): broken test notifications ref #6159 --- pkg/services/alerting/conditions/query.go | 2 +- pkg/services/alerting/eval_context.go | 24 ++++++---------------- pkg/services/alerting/notifier.go | 2 +- pkg/services/alerting/notifiers/email.go | 2 +- pkg/services/alerting/notifiers/slack.go | 2 +- pkg/services/alerting/notifiers/webhook.go | 2 +- 6 files changed, 11 insertions(+), 23 deletions(-) diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index af550a1ad18..a9a99ba919e 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/pkg/services/alerting/conditions/query.go @@ -82,7 +82,7 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange * req := c.getRequestForAlertRule(getDsInfo.Result, timeRange) result := make(tsdb.TimeSeriesSlice, 0) - resp, err := c.HandleRequest(context.Context, req) + resp, err := c.HandleRequest(context.Ctx, req) if err != nil { return nil, fmt.Errorf("tsdb.HandleRequest() error %v", err) } diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index 4903069b887..d9aaec28b0a 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -28,23 +28,7 @@ type EvalContext struct { NoDataFound bool RetryCount int - Context context.Context -} - -func (evalContext *EvalContext) Deadline() (deadline time.Time, ok bool) { - return evalContext.Deadline() -} - -func (evalContext *EvalContext) Done() <-chan struct{} { - return evalContext.Context.Done() -} - -func (evalContext *EvalContext) Err() error { - return evalContext.Context.Err() -} - -func (evalContext *EvalContext) Value(key interface{}) interface{} { - return evalContext.Context.Value(key) + Ctx context.Context } type StateDescription struct { @@ -103,6 +87,10 @@ func (c *EvalContext) GetDashboardSlug() (string, error) { } func (c *EvalContext) GetRuleUrl() (string, error) { + if c.IsTestRun { + return setting.AppUrl, nil + } + if slug, err := c.GetDashboardSlug(); err != nil { return "", err } else { @@ -113,7 +101,7 @@ func (c *EvalContext) GetRuleUrl() (string, error) { func NewEvalContext(alertCtx context.Context, rule *Rule) *EvalContext { return &EvalContext{ - Context: alertCtx, + Ctx: alertCtx, StartTime: time.Now(), Rule: rule, Logs: make([]*ResultLogEntry, 0), diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index c44ea777595..db84b20e041 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -57,7 +57,7 @@ func (n *RootNotifier) Notify(context *EvalContext) error { } func (n *RootNotifier) sendNotifications(context *EvalContext, notifiers []Notifier) error { - g, _ := errgroup.WithContext(context.Context) + g, _ := errgroup.WithContext(context.Ctx) for _, notifier := range notifiers { n.log.Info("Sending notification", "firing", context.Firing, "type", notifier.GetType()) diff --git a/pkg/services/alerting/notifiers/email.go b/pkg/services/alerting/notifiers/email.go index dfb12e9cb7b..d51383a7ddc 100644 --- a/pkg/services/alerting/notifiers/email.go +++ b/pkg/services/alerting/notifiers/email.go @@ -63,7 +63,7 @@ func (this *EmailNotifier) Notify(evalContext *alerting.EvalContext) error { }, } - err = bus.DispatchCtx(evalContext, cmd) + err = bus.DispatchCtx(evalContext.Ctx, cmd) if err != nil { this.log.Error("Failed to send alert notification email", "error", err) diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index ff71ae5d8c8..b0f36631626 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -90,7 +90,7 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error { data, _ := json.Marshal(&body) cmd := &m.SendWebhookSync{Url: this.Url, Body: string(data)} - if err := bus.DispatchCtx(evalContext, cmd); err != nil { + if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { this.log.Error("Failed to send slack notification", "error", err, "webhook", this.Name) } diff --git a/pkg/services/alerting/notifiers/webhook.go b/pkg/services/alerting/notifiers/webhook.go index 223ea2a5d54..09134702549 100644 --- a/pkg/services/alerting/notifiers/webhook.go +++ b/pkg/services/alerting/notifiers/webhook.go @@ -65,7 +65,7 @@ func (this *WebhookNotifier) Notify(evalContext *alerting.EvalContext) error { Body: string(body), } - if err := bus.DispatchCtx(evalContext, cmd); err != nil { + if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { this.log.Error("Failed to send webhook", "error", err, "webhook", this.Name) } From e556df5b499e9f3d2c66d8978b27f69e605baca0 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 4 Oct 2016 16:25:33 +0200 Subject: [PATCH 05/84] feat(tsdb): add draft implementation for influxdb --- pkg/cmd/grafana-server/main.go | 1 + pkg/tsdb/influxdb/influxdb.go | 42 +++++++++++++++++++ pkg/tsdb/influxdb/models.go | 1 + .../plugins/datasource/influxdb/plugin.json | 1 + 4 files changed, 45 insertions(+) create mode 100644 pkg/tsdb/influxdb/influxdb.go create mode 100644 pkg/tsdb/influxdb/models.go diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index 6cd063f798f..caf9d2cb56f 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/pkg/cmd/grafana-server/main.go @@ -20,6 +20,7 @@ import ( _ "github.com/grafana/grafana/pkg/services/alerting/conditions" _ "github.com/grafana/grafana/pkg/services/alerting/notifiers" _ "github.com/grafana/grafana/pkg/tsdb/graphite" + _ "github.com/grafana/grafana/pkg/tsdb/influxdb" _ "github.com/grafana/grafana/pkg/tsdb/prometheus" _ "github.com/grafana/grafana/pkg/tsdb/testdata" ) diff --git a/pkg/tsdb/influxdb/influxdb.go b/pkg/tsdb/influxdb/influxdb.go new file mode 100644 index 00000000000..010b18f4070 --- /dev/null +++ b/pkg/tsdb/influxdb/influxdb.go @@ -0,0 +1,42 @@ +package influxdb + +import ( + "context" + "crypto/tls" + "net/http" + "time" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/tsdb" +) + +type InfluxDBExecutor struct { + *tsdb.DataSourceInfo +} + +func NewInfluxDBExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor { + return &InfluxDBExecutor{dsInfo} +} + +var ( + glog log.Logger + HttpClient *http.Client +) + +func init() { + glog = log.New("tsdb.influxdb") + tsdb.RegisterExecutor("influxdb", NewInfluxDBExecutor) + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + + HttpClient = &http.Client{ + Timeout: time.Duration(15 * time.Second), + Transport: tr, + } +} + +func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult { + panic("missing implementation") +} diff --git a/pkg/tsdb/influxdb/models.go b/pkg/tsdb/influxdb/models.go new file mode 100644 index 00000000000..ab711e3b83a --- /dev/null +++ b/pkg/tsdb/influxdb/models.go @@ -0,0 +1 @@ +package influxdb diff --git a/public/app/plugins/datasource/influxdb/plugin.json b/public/app/plugins/datasource/influxdb/plugin.json index 605ce168831..635309d1610 100644 --- a/public/app/plugins/datasource/influxdb/plugin.json +++ b/public/app/plugins/datasource/influxdb/plugin.json @@ -6,6 +6,7 @@ "defaultMatchFormat": "regex values", "metrics": true, "annotations": true, + "alerting": true, "info": { "author": { From 946e0bf32e983972858fcf319038240e9e14b7b9 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 4 Oct 2016 21:28:05 +0200 Subject: [PATCH 06/84] feat(influxdb): parse query json --- pkg/tsdb/influxdb/influxdb.go | 14 ++++- pkg/tsdb/influxdb/models.go | 98 +++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/pkg/tsdb/influxdb/influxdb.go b/pkg/tsdb/influxdb/influxdb.go index 010b18f4070..5268a573ab0 100644 --- a/pkg/tsdb/influxdb/influxdb.go +++ b/pkg/tsdb/influxdb/influxdb.go @@ -38,5 +38,17 @@ func init() { } func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult { - panic("missing implementation") + result := &tsdb.BatchResult{} + for _, v := range queries { + + query, err := ParseQuery(v.Model) + + if err != nil { + result.Error = err + return result + } + glog.Info("Influxdb executor", "query", query) + } + + return result } diff --git a/pkg/tsdb/influxdb/models.go b/pkg/tsdb/influxdb/models.go index ab711e3b83a..78f9ce2bb3e 100644 --- a/pkg/tsdb/influxdb/models.go +++ b/pkg/tsdb/influxdb/models.go @@ -1 +1,99 @@ package influxdb + +import "github.com/grafana/grafana/pkg/components/simplejson" + +type InfluxDBQuery struct { + Measurement string + Policy string + ResultFormat string + Tags []Tag + GroupBy []GroupBy +} + +type Tag struct { + Key string + Operator string + Value string +} + +type GroupBy struct { + Type string + Params []string +} + +type InfluxDbSelect struct { + Type string +} + +func ParseQuery(model *simplejson.Json) (*InfluxDBQuery, error) { + measurement, err := model.Get("measurement").String() + if err != nil { + return nil, err + } + + policy := model.Get("policy").MustString("default") + + resultFormat, err := model.Get("resultFormat").String() + if err != nil { + return nil, err + } + + var tags []Tag + var groupBy []GroupBy + + for _, g := range model.Get("groupBy").MustArray() { + group := simplejson.NewFromAny(g) + + typ, err := group.Get("type").String() + if err != nil { + return nil, err + } + + var params []string + for _, p := range group.Get("params").MustArray() { + param := simplejson.NewFromAny(p) + + pv, err := param.String() + if err != nil { + return nil, err + } + params = append(params, pv) + } + + gap := GroupBy{ + Type: typ, + Params: params, + } + + groupBy = append(groupBy, gap) + } + + for _, t := range model.Get("tags").MustArray() { + tag := simplejson.NewFromAny(t) + + key, err := tag.Get("key").String() + if err != nil { + return nil, err + } + + operator, err := tag.Get("operator").String() + if err != nil { + return nil, err + } + + value, err := tag.Get("value").String() + if err != nil { + return nil, err + } + + tags = append(tags, Tag{Key: key, Operator: operator, Value: value}) + } + + return &InfluxDBQuery{ + Measurement: measurement, + Policy: policy, + ResultFormat: resultFormat, + GroupBy: groupBy, + Tags: tags, + }, nil +} From 39769ea65b7f816e9d73e41b03d0087fefc21cdf Mon Sep 17 00:00:00 2001 From: Benjamin Smith Date: Tue, 4 Oct 2016 12:40:34 -0700 Subject: [PATCH 07/84] fix(docs): duplicate word data in Graph Panel --- docs/sources/reference/graph.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/reference/graph.md b/docs/sources/reference/graph.md index 9c2ab63fbaa..2e26334347e 100644 --- a/docs/sources/reference/graph.md +++ b/docs/sources/reference/graph.md @@ -91,7 +91,7 @@ The legend values are calculated client side by Grafana and depend on what type aggregation or point consolidation you metric query is using. All the above legend values cannot be correct at the same time. For example if you plot a rate like requests/second, this is probably using average as aggregator, then the Total in the legend will not represent the total number of requests. -It is just the sum of all data data points received by Grafana. +It is just the sum of all data points received by Grafana. ## Display styles From 92121ea656dd6bad61c9aef1e438ff9ae9cf9b95 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 5 Oct 2016 07:34:48 +0200 Subject: [PATCH 08/84] feat(alerting): add nop uploader ref #6183 --- conf/defaults.ini | 2 +- conf/sample.ini | 4 ++-- pkg/components/imguploader/imguploader.go | 9 ++++++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 39e10280645..209f0e24350 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -421,7 +421,7 @@ url = https://grafana.net #################################### External Image Storage ############## [external_image_storage] # You can choose between (s3, webdav) -provider = s3 +provider = [external_image_storage.s3] bucket_url = diff --git a/conf/sample.ini b/conf/sample.ini index bb575a596b0..cc0e982a114 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -375,8 +375,8 @@ #################################### External image storage ########################## [external_image_storage] # Used for uploading images to public servers so they can be included in slack/email messages. -# you can choose between (s3, webdav or internal) -;provider = s3 +# you can choose between (s3, webdav) +;provider = [external_image_storage.s3] ;bucket_url = diff --git a/pkg/components/imguploader/imguploader.go b/pkg/components/imguploader/imguploader.go index 1de46e7fd1d..1cbe55c8572 100644 --- a/pkg/components/imguploader/imguploader.go +++ b/pkg/components/imguploader/imguploader.go @@ -10,6 +10,13 @@ type ImageUploader interface { Upload(path string) (string, error) } +type NopImageUploader struct { +} + +func (NopImageUploader) Upload(path string) (string, error) { + return "", nil +} + func NewImageUploader() (ImageUploader, error) { switch setting.ImageUploadProvider { @@ -53,5 +60,5 @@ func NewImageUploader() (ImageUploader, error) { return NewWebdavImageUploader(url, username, password) } - return nil, fmt.Errorf("could not find specified provider") + return NopImageUploader{}, nil } From d0e6a9559eef48d08a74881a9a4164b9e53d0274 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 5 Oct 2016 10:56:34 +0200 Subject: [PATCH 09/84] feat(influxdb): add query part model --- pkg/tsdb/influxdb/influxdb.go | 8 +- pkg/tsdb/influxdb/models.go | 86 ++-------------- pkg/tsdb/influxdb/parser.go | 128 ++++++++++++++++++++++++ pkg/tsdb/influxdb/parser_test.go | 93 +++++++++++++++++ pkg/tsdb/influxdb/query_builder.go | 10 ++ pkg/tsdb/influxdb/query_builder_test.go | 57 +++++++++++ pkg/tsdb/influxdb/query_part.go | 5 + pkg/tsdb/influxdb/query_part_test.go | 63 ++++++++++++ 8 files changed, 369 insertions(+), 81 deletions(-) create mode 100644 pkg/tsdb/influxdb/parser.go create mode 100644 pkg/tsdb/influxdb/parser_test.go create mode 100644 pkg/tsdb/influxdb/query_builder.go create mode 100644 pkg/tsdb/influxdb/query_builder_test.go create mode 100644 pkg/tsdb/influxdb/query_part.go create mode 100644 pkg/tsdb/influxdb/query_part_test.go diff --git a/pkg/tsdb/influxdb/influxdb.go b/pkg/tsdb/influxdb/influxdb.go index 5268a573ab0..b0775ebf9dd 100644 --- a/pkg/tsdb/influxdb/influxdb.go +++ b/pkg/tsdb/influxdb/influxdb.go @@ -12,10 +12,14 @@ import ( type InfluxDBExecutor struct { *tsdb.DataSourceInfo + QueryParser *InfluxdbQueryParser } func NewInfluxDBExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor { - return &InfluxDBExecutor{dsInfo} + return &InfluxDBExecutor{ + DataSourceInfo: dsInfo, + QueryParser: &InfluxdbQueryParser{}, + } } var ( @@ -41,7 +45,7 @@ func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, result := &tsdb.BatchResult{} for _, v := range queries { - query, err := ParseQuery(v.Model) + query, err := e.QueryParser.Parse(v.Model) if err != nil { result.Error = err diff --git a/pkg/tsdb/influxdb/models.go b/pkg/tsdb/influxdb/models.go index 78f9ce2bb3e..00a718e9b1e 100644 --- a/pkg/tsdb/influxdb/models.go +++ b/pkg/tsdb/influxdb/models.go @@ -1,13 +1,12 @@ package influxdb -import "github.com/grafana/grafana/pkg/components/simplejson" - -type InfluxDBQuery struct { +type Query struct { Measurement string Policy string ResultFormat string - Tags []Tag - GroupBy []GroupBy + Tags []*Tag + GroupBy []*QueryPart + Selects []*Select } type Tag struct { @@ -16,84 +15,13 @@ type Tag struct { Value string } -type GroupBy struct { +type QueryPart struct { Type string Params []string } +type Select []QueryPart + type InfluxDbSelect struct { Type string } - -func ParseQuery(model *simplejson.Json) (*InfluxDBQuery, error) { - measurement, err := model.Get("measurement").String() - if err != nil { - return nil, err - } - - policy := model.Get("policy").MustString("default") - - resultFormat, err := model.Get("resultFormat").String() - if err != nil { - return nil, err - } - - var tags []Tag - var groupBy []GroupBy - - for _, g := range model.Get("groupBy").MustArray() { - group := simplejson.NewFromAny(g) - - typ, err := group.Get("type").String() - if err != nil { - return nil, err - } - - var params []string - for _, p := range group.Get("params").MustArray() { - param := simplejson.NewFromAny(p) - - pv, err := param.String() - if err != nil { - return nil, err - } - params = append(params, pv) - } - - gap := GroupBy{ - Type: typ, - Params: params, - } - - groupBy = append(groupBy, gap) - } - - for _, t := range model.Get("tags").MustArray() { - tag := simplejson.NewFromAny(t) - - key, err := tag.Get("key").String() - if err != nil { - return nil, err - } - - operator, err := tag.Get("operator").String() - if err != nil { - return nil, err - } - - value, err := tag.Get("value").String() - if err != nil { - return nil, err - } - - tags = append(tags, Tag{Key: key, Operator: operator, Value: value}) - } - - return &InfluxDBQuery{ - Measurement: measurement, - Policy: policy, - ResultFormat: resultFormat, - GroupBy: groupBy, - Tags: tags, - }, nil -} diff --git a/pkg/tsdb/influxdb/parser.go b/pkg/tsdb/influxdb/parser.go new file mode 100644 index 00000000000..0e024ab4f27 --- /dev/null +++ b/pkg/tsdb/influxdb/parser.go @@ -0,0 +1,128 @@ +package influxdb + +import "github.com/grafana/grafana/pkg/components/simplejson" + +type InfluxdbQueryParser struct{} + +func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json) (*Query, error) { + policy := model.Get("policy").MustString("default") + + measurement, err := model.Get("measurement").String() + if err != nil { + return nil, err + } + + resultFormat, err := model.Get("resultFormat").String() + if err != nil { + return nil, err + } + + tags, err := qp.parseTags(model) + if err != nil { + return nil, err + } + + groupBys, err := qp.parseGroupBy(model) + if err != nil { + return nil, err + } + + selects, err := qp.parseSelects(model) + if err != nil { + return nil, err + } + + return &Query{ + Measurement: measurement, + Policy: policy, + ResultFormat: resultFormat, + GroupBy: groupBys, + Tags: tags, + Selects: selects, + }, nil +} + +func (qp *InfluxdbQueryParser) parseSelects(model *simplejson.Json) ([]*Select, error) { + var result []*Select + + for _, selectObj := range model.Get("select").MustArray() { + selectJson := simplejson.NewFromAny(selectObj) + var parts Select + + for _, partObj := range selectJson.MustArray() { + part := simplejson.NewFromAny(partObj) + queryPart, err := qp.parseQueryPart(part) + if err != nil { + return nil, err + } + + parts = append(parts, *queryPart) + } + + result = append(result, &parts) + } + + return result, nil +} + +func (*InfluxdbQueryParser) parseTags(model *simplejson.Json) ([]*Tag, error) { + var result []*Tag + for _, t := range model.Get("tags").MustArray() { + tagJson := simplejson.NewFromAny(t) + + key, err := tagJson.Get("key").String() + if err != nil { + return nil, err + } + + operator, err := tagJson.Get("operator").String() + if err != nil { + return nil, err + } + + value, err := tagJson.Get("value").String() + if err != nil { + return nil, err + } + + result = append(result, &Tag{Key: key, Operator: operator, Value: value}) + } + + return result, nil +} + +func (*InfluxdbQueryParser) parseQueryPart(model *simplejson.Json) (*QueryPart, error) { + typ, err := model.Get("type").String() + if err != nil { + return nil, err + } + + var params []string + for _, paramObj := range model.Get("params").MustArray() { + param := simplejson.NewFromAny(paramObj) + + pv, err := param.String() + if err != nil { + return nil, err + } + params = append(params, pv) + } + + return &QueryPart{Type: typ, Params: params}, nil +} + +func (qp *InfluxdbQueryParser) parseGroupBy(model *simplejson.Json) ([]*QueryPart, error) { + var result []*QueryPart + + for _, groupObj := range model.Get("groupBy").MustArray() { + groupJson := simplejson.NewFromAny(groupObj) + queryPart, err := qp.parseQueryPart(groupJson) + + if err != nil { + return nil, err + } + result = append(result, queryPart) + } + + return result, nil +} diff --git a/pkg/tsdb/influxdb/parser_test.go b/pkg/tsdb/influxdb/parser_test.go new file mode 100644 index 00000000000..cb7c27ec35f --- /dev/null +++ b/pkg/tsdb/influxdb/parser_test.go @@ -0,0 +1,93 @@ +package influxdb + +import ( + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + . "github.com/smartystreets/goconvey/convey" +) + +func TestInfluxdbQueryParser(t *testing.T) { + Convey("Influxdb query parser", t, func() { + + parser := &InfluxdbQueryParser{} + + Convey("converting metric name", func() { + json := ` + { + "dsType": "influxdb", + "groupBy": [ + { + "params": [ + "$interval" + ], + "type": "time" + }, + { + "type": "tag", + "params": [ + "datacenter" + ] + }, + { + "params": [ + "null" + ], + "type": "fill" + } + ], + "measurement": "logins.count", + "policy": "default", + "refId": "B", + "resultFormat": "time_series", + "select": [ + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [ + + ], + "type": "count" + } + ], + [ + { + "params": [ + "value" + ], + "type": "field" + }, + { + "params": [ + + ], + "type": "mean" + } + ] + ], + "tags": [ + { + "key": "datacenter", + "operator": "=", + "value": "America" + } + ] + } + ` + + modelJson, err := simplejson.NewJson([]byte(json)) + So(err, ShouldBeNil) + + res, err := parser.Parse(modelJson) + So(err, ShouldBeNil) + So(len(res.GroupBy), ShouldEqual, 3) + So(len(res.Selects), ShouldEqual, 2) + So(len(res.Tags), ShouldEqual, 1) + }) + }) +} diff --git a/pkg/tsdb/influxdb/query_builder.go b/pkg/tsdb/influxdb/query_builder.go new file mode 100644 index 00000000000..86d4981133e --- /dev/null +++ b/pkg/tsdb/influxdb/query_builder.go @@ -0,0 +1,10 @@ +package influxdb + +import "fmt" + +type QueryBuild struct{} + +func (*QueryBuild) Build(query *Query) (string, error) { + + return "", fmt.Errorf("query is not valid") +} diff --git a/pkg/tsdb/influxdb/query_builder_test.go b/pkg/tsdb/influxdb/query_builder_test.go new file mode 100644 index 00000000000..7ec80d389dd --- /dev/null +++ b/pkg/tsdb/influxdb/query_builder_test.go @@ -0,0 +1,57 @@ +package influxdb + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestInfluxdbQueryBuilder(t *testing.T) { + Convey("Influxdb query builder", t, func() { + + builder := &QueryBuild{} + + Convey("can build query", func() { + //query := &Query{} + //res, err := builder.Build(query) + //So(err, ShouldBeNil) + }) + + Convey("empty queries should return error", func() { + query := &Query{} + + res, err := builder.Build(query) + So(err, ShouldNotBeNil) + So(res, ShouldEqual, "") + }) + }) +} + +/* + describe('render series with mesurement only', function() { + it('should generate correct query', function() { + var query = new InfluxQuery({ + measurement: 'cpu', + }, templateSrv, {}); + + var queryText = query.render(); + expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE $timeFilter GROUP BY time($interval) fill(null)'); + }); + }); +*/ + +/* + describe('series with tags OR condition', function() { + it('should generate correct query', function() { + var query = new InfluxQuery({ + measurement: 'cpu', + groupBy: [{type: 'time', params: ['auto']}], + tags: [{key: 'hostname', value: 'server1'}, {key: 'hostname', value: 'server2', condition: "OR"}] + }, templateSrv, {}); + + var queryText = query.render(); + expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE "hostname" = \'server1\' OR "hostname" = \'server2\' AND ' + + '$timeFilter GROUP BY time($interval)'); + }); + }); +*/ diff --git a/pkg/tsdb/influxdb/query_part.go b/pkg/tsdb/influxdb/query_part.go new file mode 100644 index 00000000000..3d5a51d42c0 --- /dev/null +++ b/pkg/tsdb/influxdb/query_part.go @@ -0,0 +1,5 @@ +package influxdb + +type Selector interface { + Render(input string) string +} diff --git a/pkg/tsdb/influxdb/query_part_test.go b/pkg/tsdb/influxdb/query_part_test.go new file mode 100644 index 00000000000..20f12cc3359 --- /dev/null +++ b/pkg/tsdb/influxdb/query_part_test.go @@ -0,0 +1,63 @@ +package influxdb + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestInfluxdbQueryPart(t *testing.T) { + Convey("Influxdb query part builder", t, func() { + + Convey("can build query", func() { + }) + + Convey("empty queries should return error", func() { + + }) + }) +} + +/* + describe('series with mesurement only', () => { + it('should handle nested function parts', () => { + var part = queryPart.create({ + type: 'derivative', + params: ['10s'], + }); + + expect(part.text).to.be('derivative(10s)'); + expect(part.render('mean(value)')).to.be('derivative(mean(value), 10s)'); + }); + + it('should nest spread function', () => { + var part = queryPart.create({ + type: 'spread' + }); + + expect(part.text).to.be('spread()'); + expect(part.render('value')).to.be('spread(value)'); + }); + + it('should handle suffirx parts', () => { + var part = queryPart.create({ + type: 'math', + params: ['/ 100'], + }); + + expect(part.text).to.be('math(/ 100)'); + expect(part.render('mean(value)')).to.be('mean(value) / 100'); + }); + + it('should handle alias parts', () => { + var part = queryPart.create({ + type: 'alias', + params: ['test'], + }); + + expect(part.text).to.be('alias(test)'); + expect(part.render('mean(value)')).to.be('mean(value) AS "test"'); + }); + + }); +*/ From c7abd3ba4e296cc98a14c3302d4fa16c63872836 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 5 Oct 2016 14:42:06 +0200 Subject: [PATCH 10/84] feat(influxdb): add querypart renderers --- pkg/tsdb/influxdb/models.go | 5 - pkg/tsdb/influxdb/parser_test.go | 138 +++++++++++++++------------ pkg/tsdb/influxdb/query_part.go | 123 +++++++++++++++++++++++- pkg/tsdb/influxdb/query_part_test.go | 91 +++++++++--------- 4 files changed, 242 insertions(+), 115 deletions(-) diff --git a/pkg/tsdb/influxdb/models.go b/pkg/tsdb/influxdb/models.go index 00a718e9b1e..a70fe7f5d47 100644 --- a/pkg/tsdb/influxdb/models.go +++ b/pkg/tsdb/influxdb/models.go @@ -15,11 +15,6 @@ type Tag struct { Value string } -type QueryPart struct { - Type string - Params []string -} - type Select []QueryPart type InfluxDbSelect struct { diff --git a/pkg/tsdb/influxdb/parser_test.go b/pkg/tsdb/influxdb/parser_test.go index cb7c27ec35f..19e2c228f1a 100644 --- a/pkg/tsdb/influxdb/parser_test.go +++ b/pkg/tsdb/influxdb/parser_test.go @@ -14,70 +14,84 @@ func TestInfluxdbQueryParser(t *testing.T) { Convey("converting metric name", func() { json := ` - { - "dsType": "influxdb", - "groupBy": [ - { - "params": [ - "$interval" - ], - "type": "time" - }, - { - "type": "tag", - "params": [ - "datacenter" - ] - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "measurement": "logins.count", - "policy": "default", - "refId": "B", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" + { + "dsType": "influxdb", + "groupBy": [ + { + "params": [ + "$interval" + ], + "type": "time" + }, + { + "params": [ + "datacenter" + ], + "type": "tag" + }, + { + "params": [ + "null" + ], + "type": "fill" + } ], - "type": "field" - }, - { - "params": [ - + "measurement": "logins.count", + "policy": "default", + "refId": "B", + "resultFormat": "time_series", + "select": [ + [ + { + "type": "field", + "params": [ + "value" + ] + }, + { + "type": "count", + "params": [] + } + ], + [ + { + "type": "field", + "params": [ + "value" + ] + }, + { + "type": "mean", + "params": [] + } + ], + [ + { + "type": "field", + "params": [ + "value" + ] + }, + { + "type": "mean", + "params": [] + }, + { + "type": "math", + "params": [ + " / 100" + ] + } + ] ], - "type": "count" + "tags": [ + { + "key": "datacenter", + "operator": "=", + "value": "America" + } + ] } - ], - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [ - - ], - "type": "mean" - } - ] - ], - "tags": [ - { - "key": "datacenter", - "operator": "=", - "value": "America" - } - ] - } ` modelJson, err := simplejson.NewJson([]byte(json)) @@ -86,7 +100,7 @@ func TestInfluxdbQueryParser(t *testing.T) { res, err := parser.Parse(modelJson) So(err, ShouldBeNil) So(len(res.GroupBy), ShouldEqual, 3) - So(len(res.Selects), ShouldEqual, 2) + So(len(res.Selects), ShouldEqual, 3) So(len(res.Tags), ShouldEqual, 1) }) }) diff --git a/pkg/tsdb/influxdb/query_part.go b/pkg/tsdb/influxdb/query_part.go index 3d5a51d42c0..cba0c57b6b1 100644 --- a/pkg/tsdb/influxdb/query_part.go +++ b/pkg/tsdb/influxdb/query_part.go @@ -1,5 +1,124 @@ package influxdb -type Selector interface { - Render(input string) string +import ( + "fmt" + "strings" +) + +var renders map[string]QueryDefinition + +type QueryDefinition struct { + Renderer func(part *QueryPart, innerExpr string) string +} + +func init() { + renders = make(map[string]QueryDefinition) + + renders["field"] = QueryDefinition{Renderer: fieldRenderer} + + renders["spread"] = QueryDefinition{Renderer: functionRenderer} + renders["count"] = QueryDefinition{Renderer: functionRenderer} + renders["distinct"] = QueryDefinition{Renderer: functionRenderer} + renders["integral"] = QueryDefinition{Renderer: functionRenderer} + renders["mean"] = QueryDefinition{Renderer: functionRenderer} + renders["median"] = QueryDefinition{Renderer: functionRenderer} + renders["sum"] = QueryDefinition{Renderer: functionRenderer} + + renders["derivative"] = QueryDefinition{ + Renderer: functionRenderer, + //params: [{ name: "duration", type: "interval", options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h']}], + } + + renders["non_negative_derivative"] = QueryDefinition{ + Renderer: functionRenderer, + //params: [{ name: "duration", type: "interval", options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h']}], + } + renders["difference"] = QueryDefinition{Renderer: functionRenderer} + renders["moving_average"] = QueryDefinition{ + Renderer: functionRenderer, + //params: [{ name: "window", type: "number", options: [5, 10, 20, 30, 40]}] + } + renders["stddev"] = QueryDefinition{Renderer: functionRenderer} + renders["time"] = QueryDefinition{ + Renderer: functionRenderer, + //params: [{ name: "interval", type: "time", options: ['auto', '1s', '10s', '1m', '5m', '10m', '15m', '1h'] }], + } + renders["fill"] = QueryDefinition{ + Renderer: functionRenderer, + //params: [{ name: "fill", type: "string", options: ['none', 'null', '0', 'previous'] }], + } + renders["elapsed"] = QueryDefinition{ + Renderer: functionRenderer, + //params: [{ name: "duration", type: "interval", options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h']}], + } + renders["bottom"] = QueryDefinition{ + Renderer: functionRenderer, + //params: [{name: 'count', type: 'int'}], + } + + renders["first"] = QueryDefinition{Renderer: functionRenderer} + renders["last"] = QueryDefinition{Renderer: functionRenderer} + renders["max"] = QueryDefinition{Renderer: functionRenderer} + renders["min"] = QueryDefinition{Renderer: functionRenderer} + renders["percentile"] = QueryDefinition{ + Renderer: functionRenderer, + //params: [{name: 'nth', type: 'int'}], + } + renders["top"] = QueryDefinition{ + Renderer: functionRenderer, + //params: [{name: 'count', type: 'int'}], + } + renders["tag"] = QueryDefinition{ + Renderer: fieldRenderer, + //params: [{name: 'tag', type: 'string', dynamicLookup: true}], + } + + renders["math"] = QueryDefinition{Renderer: suffixRenderer} + renders["alias"] = QueryDefinition{Renderer: aliasRenderer} +} + +func fieldRenderer(part *QueryPart, innerExpr string) string { + if part.Params[0] == "*" { + return "*" + } + return fmt.Sprintf(`"%v"`, part.Params[0]) +} + +func functionRenderer(part *QueryPart, innerExpr string) string { + params := strings.Join(part.Params, ", ") + + if len(part.Params) > 0 { + return fmt.Sprintf("%s(%s, %s)", part.Type, innerExpr, params) + } + + return fmt.Sprintf("%s(%s)", part.Type, innerExpr) +} + +func suffixRenderer(part *QueryPart, innerExpr string) string { + return fmt.Sprintf("%s %s", innerExpr, part.Params[0]) +} + +func aliasRenderer(part *QueryPart, innerExpr string) string { + return fmt.Sprintf(`%s AS "%s"`, innerExpr, part.Params[0]) +} + +func (r QueryDefinition) Render(part *QueryPart, innerExpr string) string { + return r.Renderer(part, innerExpr) +} + +type QueryPartDefinition struct { +} + +type QueryPart struct { + Type string + Params []string +} + +func (qp *QueryPart) Render(expr string) (string, error) { + renderFn, exist := renders[qp.Type] + if !exist { + return "", fmt.Errorf("could not find render strategy %s", qp.Type) + } + + return renderFn.Renderer(qp, expr), nil } diff --git a/pkg/tsdb/influxdb/query_part_test.go b/pkg/tsdb/influxdb/query_part_test.go index 20f12cc3359..eb2583e7711 100644 --- a/pkg/tsdb/influxdb/query_part_test.go +++ b/pkg/tsdb/influxdb/query_part_test.go @@ -9,55 +9,54 @@ import ( func TestInfluxdbQueryPart(t *testing.T) { Convey("Influxdb query part builder", t, func() { - Convey("can build query", func() { + Convey("should handle field renderer parts", func() { + part := QueryPart{ + Type: "field", + Params: []string{"value"}, + } + + res, _ := part.Render("value") + So(res, ShouldEqual, `"value"`) }) - Convey("empty queries should return error", func() { + Convey("should handle nested function parts", func() { + part := QueryPart{ + Type: "derivative", + Params: []string{"10s"}, + } + res, _ := part.Render("mean(value)") + So(res, ShouldEqual, "derivative(mean(value), 10s)") + }) + + Convey("should nest spread function", func() { + part := QueryPart{ + Type: "spread", + } + + res, err := part.Render("value") + So(err, ShouldBeNil) + So(res, ShouldEqual, "spread(value)") + }) + + Convey("should handle suffix parts", func() { + part := QueryPart{ + Type: "math", + Params: []string{"/ 100"}, + } + + res, _ := part.Render("mean(value)") + So(res, ShouldEqual, "mean(value) / 100") + }) + + Convey("should handle alias parts", func() { + part := QueryPart{ + Type: "alias", + Params: []string{"test"}, + } + + res, _ := part.Render("mean(value)") + So(res, ShouldEqual, `mean(value) AS "test"`) }) }) } - -/* - describe('series with mesurement only', () => { - it('should handle nested function parts', () => { - var part = queryPart.create({ - type: 'derivative', - params: ['10s'], - }); - - expect(part.text).to.be('derivative(10s)'); - expect(part.render('mean(value)')).to.be('derivative(mean(value), 10s)'); - }); - - it('should nest spread function', () => { - var part = queryPart.create({ - type: 'spread' - }); - - expect(part.text).to.be('spread()'); - expect(part.render('value')).to.be('spread(value)'); - }); - - it('should handle suffirx parts', () => { - var part = queryPart.create({ - type: 'math', - params: ['/ 100'], - }); - - expect(part.text).to.be('math(/ 100)'); - expect(part.render('mean(value)')).to.be('mean(value) / 100'); - }); - - it('should handle alias parts', () => { - var part = queryPart.create({ - type: 'alias', - params: ['test'], - }); - - expect(part.text).to.be('alias(test)'); - expect(part.render('mean(value)')).to.be('mean(value) AS "test"'); - }); - - }); -*/ From bf4c3f3ae6f10219517973284c4d298e9e012c6d Mon Sep 17 00:00:00 2001 From: Peter Dave Hello Date: Wed, 5 Oct 2016 21:57:50 +0800 Subject: [PATCH 11/84] optimize png images using zopflipng (#6197) --- .../cloudwatch/img/amazon-web-services.png | Bin 9559 -> 7838 bytes .../elasticsearch/img/logo_large.png | Bin 38427 -> 22570 bytes .../datasource/graphite/img/graphite_logo.png | Bin 6285 -> 4452 bytes .../datasource/opentsdb/img/opentsdb_logo.png | Bin 11644 -> 9600 bytes public/img/CopyQuery.png | Bin 144 -> 139 bytes public/img/alert_howto_new.png | Bin 6149 -> 2670 bytes public/img/angle_gradient_light_rev.png | Bin 73730 -> 46141 bytes public/img/angle_gradient_rev.png | Bin 87003 -> 58200 bytes public/img/check_radio_sheet.png | Bin 4343 -> 3669 bytes public/img/checkbox.png | Bin 1461 -> 1071 bytes public/img/checkbox_white.png | Bin 960 -> 551 bytes public/img/cubes.png | Bin 22505 -> 21593 bytes public/img/fav16.png | Bin 3971 -> 3607 bytes public/img/fav32.png | Bin 3971 -> 3607 bytes public/img/fav_dark_16.png | Bin 4837 -> 4305 bytes public/img/fav_dark_32.png | Bin 3435 -> 3204 bytes public/img/glyphicons-halflings-white.png | Bin 8777 -> 8622 bytes public/img/glyphicons-halflings.png | Bin 12799 -> 12586 bytes public/img/kibana.png | Bin 7359 -> 5290 bytes public/img/light.png | Bin 14080 -> 13308 bytes public/img/logo_transparent_200x.png | Bin 8910 -> 8031 bytes public/img/logo_transparent_200x75.png | Bin 8910 -> 8031 bytes public/img/logo_transparent_400x.png | Bin 19803 -> 17506 bytes public/img/page_header_line.png | Bin 32993 -> 22190 bytes public/img/small.png | Bin 480 -> 335 bytes public/img/transparent.png | Bin 95 -> 70 bytes .../vendor/firebug-lite/skin/xp/buttonBg.png | Bin 167 -> 110 bytes .../firebug-lite/skin/xp/buttonBgHover.png | Bin 171 -> 113 bytes .../vendor/firebug-lite/skin/xp/detach.png | Bin 655 -> 447 bytes .../firebug-lite/skin/xp/detachHover.png | Bin 586 -> 392 bytes .../vendor/firebug-lite/skin/xp/disable.png | Bin 543 -> 294 bytes .../firebug-lite/skin/xp/disableHover.png | Bin 512 -> 407 bytes .../vendor/firebug-lite/skin/xp/down.png | Bin 637 -> 398 bytes .../firebug-lite/skin/xp/downActive.png | Bin 543 -> 354 bytes .../vendor/firebug-lite/skin/xp/downHover.png | Bin 526 -> 349 bytes .../firebug-lite/skin/xp/errorIcon-sm.png | Bin 447 -> 316 bytes .../vendor/firebug-lite/skin/xp/errorIcon.png | Bin 457 -> 338 bytes .../vendor/firebug-lite/skin/xp/firebug.png | Bin 1167 -> 903 bytes .../vendor/firebug-lite/skin/xp/infoIcon.png | Bin 524 -> 385 bytes .../vendor/firebug-lite/skin/xp/min.png | Bin 552 -> 359 bytes .../vendor/firebug-lite/skin/xp/minHover.png | Bin 485 -> 320 bytes .../vendor/firebug-lite/skin/xp/off.png | Bin 742 -> 609 bytes .../vendor/firebug-lite/skin/xp/offHover.png | Bin 680 -> 527 bytes .../vendor/firebug-lite/skin/xp/search.png | Bin 685 -> 491 bytes .../firebug-lite/skin/xp/shadowAlpha.png | Bin 3403 -> 1813 bytes .../vendor/firebug-lite/skin/xp/sprite.png | Bin 40027 -> 3452 bytes .../firebug-lite/skin/xp/tabHoverLeft.png | Bin 438 -> 282 bytes .../firebug-lite/skin/xp/tabHoverMid.png | Bin 261 -> 141 bytes .../firebug-lite/skin/xp/tabHoverRight.png | Bin 436 -> 285 bytes .../vendor/firebug-lite/skin/xp/tabLeft.png | Bin 449 -> 288 bytes .../firebug-lite/skin/xp/tabMenuCheckbox.png | Bin 220 -> 133 bytes .../firebug-lite/skin/xp/tabMenuPin.png | Bin 207 -> 123 bytes .../firebug-lite/skin/xp/tabMenuRadio.png | Bin 192 -> 110 bytes .../firebug-lite/skin/xp/tabMenuTarget.png | Bin 142 -> 110 bytes .../skin/xp/tabMenuTargetHover.png | Bin 148 -> 91 bytes .../vendor/firebug-lite/skin/xp/tabMid.png | Bin 262 -> 141 bytes .../vendor/firebug-lite/skin/xp/tabRight.png | Bin 448 -> 290 bytes .../skin/xp/textEditorBorders.png | Bin 3144 -> 3139 bytes .../skin/xp/textEditorCorners.png | Bin 3960 -> 3820 bytes .../firebug-lite/skin/xp/titlebarMid.png | Bin 273 -> 153 bytes .../firebug-lite/skin/xp/toolbarMid.png | Bin 242 -> 127 bytes .../firebug-lite/skin/xp/twistyClosed.png | Bin 334 -> 223 bytes .../firebug-lite/skin/xp/twistyOpen.png | Bin 309 -> 224 bytes .../lodash/vendor/firebug-lite/skin/xp/up.png | Bin 619 -> 392 bytes .../vendor/firebug-lite/skin/xp/upActive.png | Bin 551 -> 348 bytes .../vendor/firebug-lite/skin/xp/upHover.png | Bin 526 -> 341 bytes .../firebug-lite/skin/xp/warningIcon.png | Bin 516 -> 390 bytes .../go-xorm/xorm/docs/images/cache_design.png | Bin 243596 -> 120924 bytes vendor/gopkg.in/macaron.v1/macaronlogo.png | Bin 88924 -> 70275 bytes 69 files changed, 0 insertions(+), 0 deletions(-) diff --git a/public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png b/public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png index f11f40347510903c178610d44d261ef530db2dc5..7f9687e839c27e53b0ba34b8eaa994cf31787e60 100644 GIT binary patch delta 5252 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 diff --git a/public/app/plugins/datasource/elasticsearch/img/logo_large.png b/public/app/plugins/datasource/elasticsearch/img/logo_large.png index b76d01e5d74cf26415174edce6852366edf7c42e..5ded1d8f4386220953cbeca7c96bc385602722da 100644 GIT binary patch literal 22570 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