diff --git a/.gitignore b/.gitignore index cfa3a68c057..e6551334010 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ public/css/*.min.css .idea/ *.iml *.tmp +.DS_Store .vscode/ /data/* diff --git a/CHANGELOG.md b/CHANGELOG.md index a3ed3824ec1..aa8d7425b3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,17 @@ * **GCS**: Adds support for Google Cloud Storage [#8370](https://github.com/grafana/grafana/issues/8370) thx [@chuhlomin](https://github.com/chuhlomin) * **Prometheus**: Adds /metrics endpoint for exposing Grafana metrics. [#9187](https://github.com/grafana/grafana/pull/9187) * **Graph**: Add support for local formating in axis. [#1395](https://github.com/grafana/grafana/issues/1395), thx [@m0nhawk](https://github.com/m0nhawk) +* **Jaeger**: Add support for open tracing using jaeger in Grafana. [#9213](https://github.com/grafana/grafana/pull/9213) +* **Unit types**: New date & time unit types added, useful in singlestat to show dates & times. [#3678](https://github.com/grafana/grafana/issues/3678), [#6710](https://github.com/grafana/grafana/issues/6710), [#2764](https://github.com/grafana/grafana/issues/6710) ## Breaking changes * **Metrics**: The metric structure for internal metrics about Grafana published to graphite has changed. This might break dashboards for internal metrics. +# 4.5.2 (unreleased) + +## Fixes +* **Metrics**: dont write NaN values to graphite [#9279](https://github.com/grafana/grafana/issues/9279) + # 4.5.1 (2017-09-15) ## Fixes @@ -49,6 +56,7 @@ ### Breaking change * **InfluxDB/Elasticsearch**: The panel & data source option named "Group by time interval" is now named "Min time interval" and does now always define a lower limit for the auto group by time. Without having to use `>` prefix (that prefix still works). This should in theory have close to zero actual impact on existing dashboards. It does mean that if you used this setting to define a hard group by time interval of, say "1d", if you zoomed to a time range wide enough the time range could increase above the "1d" range as the setting is now always considered a lower limit. +* **Elasticsearch**: Elasticsearch metric queries without date histogram now return table formated data making table panel much easier to use for this use case. Should not break/change existing dashboards with stock panels but external panel plugins can be affected. ## Changes diff --git a/conf/defaults.ini b/conf/defaults.ini index 715ddb7513c..5abd27e5c51 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -452,6 +452,23 @@ url = https://grafana.com [grafana_com] url = https://grafana.com +#################################### Distributed tracing ############ +[tracing.jaeger] +# jaeger destination (ex localhost:6831) +address = +# tag that will always be included in when creating new spans. ex (tag1:value1,tag2:value2) +always_included_tag = +# Type specifies the type of the sampler: const, probabilistic, rateLimiting, or remote +sampler_type = const +# jaeger samplerconfig param +# for "const" sampler, 0 or 1 for always false/true respectively +# for "probabilistic" sampler, a probability between 0 and 1 +# for "rateLimiting" sampler, the number of spans per second +# for "remote" sampler, param is the same as for "probabilistic" +# and indicates the initial sampling rate before the actual one +# is received from the mothership +sampler_param = 1 + #################################### External Image Storage ############## [external_image_storage] # You can choose between (s3, webdav, gcs) diff --git a/conf/sample.ini b/conf/sample.ini index 8947a05b0a4..076b9306d84 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -391,6 +391,23 @@ ;address = ;prefix = prod.grafana.%(instance_name)s. +#################################### Distributed tracing ############ +[tracing.jaeger] +# Enable by setting the address sending traces to jaeger (ex localhost:6831) +;address = localhost:6831 +# Tag that will always be included in when creating new spans. ex (tag1:value1,tag2:value2) +;always_included_tag = tag1:value1 +# Type specifies the type of the sampler: const, probabilistic, rateLimiting, or remote +;sampler_type = const +# jaeger samplerconfig param +# for "const" sampler, 0 or 1 for always false/true respectively +# for "probabilistic" sampler, a probability between 0 and 1 +# for "rateLimiting" sampler, the number of spans per second +# for "remote" sampler, param is the same as for "probabilistic" +# and indicates the initial sampling rate before the actual one +# is received from the mothership +;sampler_param = 1 + #################################### Grafana.com integration ########################## # Url used to to import dashboards directly from Grafana.com [grafana_com] diff --git a/docker/blocks/jaeger/fig b/docker/blocks/jaeger/fig new file mode 100644 index 00000000000..ab9e2ec599b --- /dev/null +++ b/docker/blocks/jaeger/fig @@ -0,0 +1,6 @@ +jaeger: + image: jaegertracing/all-in-one:latest + ports: + - "localhost:6831:6831/udp" + - "16686:16686" + diff --git a/docker/blocks/prometheus/Dockerfile b/docker/blocks/prometheus/Dockerfile index 0e07679ddd5..1ad28f524ff 100644 --- a/docker/blocks/prometheus/Dockerfile +++ b/docker/blocks/prometheus/Dockerfile @@ -1,2 +1,3 @@ FROM prom/prometheus ADD prometheus.yml /etc/prometheus/ +ADD alert.rules /etc/prometheus/ diff --git a/docker/blocks/prometheus/alert.rules b/docker/blocks/prometheus/alert.rules new file mode 100644 index 00000000000..563d1e89994 --- /dev/null +++ b/docker/blocks/prometheus/alert.rules @@ -0,0 +1,10 @@ +# Alert Rules + +ALERT AppCrash + IF process_open_fds > 0 + FOR 15s + LABELS { severity="critical" } + ANNOTATIONS { + summary = "Number of open fds > 0", + description = "Just testing" + } diff --git a/docker/blocks/prometheus/fig b/docker/blocks/prometheus/fig index 1c70c14fc37..7d9bea68046 100644 --- a/docker/blocks/prometheus/fig +++ b/docker/blocks/prometheus/fig @@ -18,3 +18,8 @@ fake-prometheus-data: environment: FD_DATASOURCE: prom +alertmanager: + image: quay.io/prometheus/alertmanager + net: host + ports: + - "9093:9093" diff --git a/docker/blocks/prometheus/prometheus.yml b/docker/blocks/prometheus/prometheus.yml index 35be33b80a7..0afb7968ce5 100644 --- a/docker/blocks/prometheus/prometheus.yml +++ b/docker/blocks/prometheus/prometheus.yml @@ -6,9 +6,18 @@ global: # Load and evaluate rules in this file every 'evaluation_interval' seconds. rule_files: + - "alert.rules" # - "first.rules" # - "second.rules" +alerting: + alertmanagers: + - scheme: http + static_configs: + - targets: + - "127.0.0.1:9093" + + # A scrape configuration containing exactly one endpoint to scrape: # Here it's Prometheus itself. scrape_configs: diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index e931224cac3..b8593b681df 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -308,15 +308,15 @@ options are `Editor` and `Admin`. ## [auth.github] -You need to create a GitHub application (you find this under the GitHub -profile page). When you create the application you will need to specify +You need to create a GitHub OAuth application (you find this under the GitHub +settings page). When you create the application you will need to specify a callback URL. Specify this as callback: http://:/login/github This callback URL must match the full HTTP address that you use in your browser to access Grafana, but with the prefix path of `/login/github`. -When the GitHub application is created you will get a Client ID and a +When the GitHub OAuth application is created you will get a Client ID and a Client Secret. Specify these in the Grafana configuration file. For example: diff --git a/package.json b/package.json index 2b0173a7e67..a6ee71e8169 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "url": "http://github.com/grafana/grafana.git" }, "devDependencies": { + "@types/react": "^16.0.5", + "@types/react-dom": "^15.5.4", "autoprefixer": "^6.4.0", "es6-promise": "^3.0.2", "es6-shim": "^0.35.1", @@ -48,7 +50,7 @@ "mocha": "3.2.0", "phantomjs-prebuilt": "^2.1.14", "reflect-metadata": "0.1.8", - "rxjs": "^5.0.0-rc.5", + "rxjs": "^5.4.3", "sass-lint": "^1.10.2", "systemjs": "0.19.41", "zone.js": "^0.7.2" @@ -60,6 +62,7 @@ }, "license": "Apache-2.0", "dependencies": { + "@types/enzyme": "^2.8.8", "ace-builds": "^1.2.8", "eventemitter3": "^2.0.2", "gaze": "^1.1.2", @@ -73,13 +76,17 @@ "karma-sinon": "^1.0.5", "lodash": "^4.17.4", "mousetrap": "^1.6.0", + "ngreact": "^0.4.1", + "react": "^15.6.1", + "react-dom": "^15.6.1", + "react-test-renderer": "^15.6.1", "remarkable": "^1.7.1", "sinon": "1.17.6", "systemjs-builder": "^0.15.34", "tether": "^1.4.0", "tether-drop": "https://github.com/torkelo/drop", - "tslint": "^5.1.0", - "typescript": "^2.2.2", + "tslint": "^5.7.0", + "typescript": "^2.5.2", "virtual-scroll": "^1.1.1" } } diff --git a/pkg/api/api.go b/pkg/api/api.go index e63a851f405..843e883788a 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -21,7 +21,7 @@ func (hs *HttpServer) registerRoutes() { // automatically set HEAD for every GET macaronR.SetAutoHead(true) - r := newRouteRegister(middleware.RequestMetrics) + r := newRouteRegister(middleware.RequestMetrics, middleware.RequestTracing) // not logged in views r.Get("/", reqSignedIn, Index) diff --git a/pkg/api/cloudwatch/metrics.go b/pkg/api/cloudwatch/metrics.go index 2f1305f428a..16b496d6be6 100644 --- a/pkg/api/cloudwatch/metrics.go +++ b/pkg/api/cloudwatch/metrics.go @@ -126,7 +126,7 @@ func init() { "AWS/NATGateway": {"NatGatewayId"}, "AWS/OpsWorks": {"StackId", "LayerId", "InstanceId"}, "AWS/Redshift": {"NodeID", "ClusterIdentifier"}, - "AWS/RDS": {"DBInstanceIdentifier", "DBClusterIdentifier", "DatabaseClass", "EngineName", "Role"}, + "AWS/RDS": {"DBInstanceIdentifier", "DBClusterIdentifier", "DbClusterIdentifier", "DatabaseClass", "EngineName", "Role"}, "AWS/Route53": {"HealthCheckId", "Region"}, "AWS/S3": {"BucketName", "StorageType", "FilterId"}, "AWS/SES": {}, diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index 9d41366ae4c..2b3e3c09cd4 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -31,7 +31,7 @@ func QueryMetrics(c *middleware.Context, reqDto dtos.MetricRequest) Response { return ApiError(500, "failed to fetch data source", err) } - request := &tsdb.Request{TimeRange: timeRange} + request := &tsdb.TsdbQuery{TimeRange: timeRange} for _, query := range reqDto.Queries { request.Queries = append(request.Queries, &tsdb.Query{ @@ -98,7 +98,7 @@ func GetTestDataRandomWalk(c *middleware.Context) Response { intervalMs := c.QueryInt64("intervalMs") timeRange := tsdb.NewTimeRange(from, to) - request := &tsdb.Request{TimeRange: timeRange} + request := &tsdb.TsdbQuery{TimeRange: timeRange} request.Queries = append(request.Queries, &tsdb.Query{ RefId: "A", diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index fff8d099d4b..10fafe21fef 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -15,6 +15,8 @@ import ( "text/template" "time" + "github.com/opentracing/opentracing-go" + "github.com/grafana/grafana/pkg/api/cloudwatch" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/middleware" @@ -85,6 +87,20 @@ func (proxy *DataSourceProxy) HandleRequest() { proxy.logRequest() + span, ctx := opentracing.StartSpanFromContext(proxy.ctx.Req.Context(), "datasource reverse proxy") + proxy.ctx.Req.Request = proxy.ctx.Req.WithContext(ctx) + + defer span.Finish() + span.SetTag("datasource_id", proxy.ds.Id) + span.SetTag("datasource_type", proxy.ds.Type) + span.SetTag("user_id", proxy.ctx.SignedInUser.UserId) + span.SetTag("org_id", proxy.ctx.SignedInUser.OrgId) + + opentracing.GlobalTracer().Inject( + span.Context(), + opentracing.HTTPHeaders, + opentracing.HTTPHeadersCarrier(proxy.ctx.Req.Request.Header)) + reverseProxy.ServeHTTP(proxy.ctx.Resp, proxy.ctx.Req.Request) proxy.ctx.Resp.Header().Del("Set-Cookie") } diff --git a/pkg/api/route_register.go b/pkg/api/route_register.go index b23c92ed1c0..daa6f35e52c 100644 --- a/pkg/api/route_register.go +++ b/pkg/api/route_register.go @@ -8,6 +8,7 @@ import ( type Router interface { Handle(method, pattern string, handlers []macaron.Handler) *macaron.Route + Get(pattern string, handlers ...macaron.Handler) *macaron.Route } type RouteRegister interface { @@ -62,7 +63,14 @@ func (rr *routeRegister) Group(pattern string, fn func(rr RouteRegister), handle func (rr *routeRegister) Register(router Router) *macaron.Router { for _, r := range rr.routes { - router.Handle(r.method, r.pattern, r.handlers) + // GET requests have to be added to macaron routing using Get() + // Otherwise HEAD requests will not be allowed. + // https://github.com/go-macaron/macaron/blob/a325110f8b392bce3e5cdeb8c44bf98078ada3be/router.go#L198 + if r.method == http.MethodGet { + router.Get(r.pattern, r.handlers...) + } else { + router.Handle(r.method, r.pattern, r.handlers) + } } for _, g := range rr.groups { diff --git a/pkg/api/route_register_test.go b/pkg/api/route_register_test.go index c430c84abc1..f8a043c48df 100644 --- a/pkg/api/route_register_test.go +++ b/pkg/api/route_register_test.go @@ -1,6 +1,7 @@ package api import ( + "net/http" "strconv" "testing" @@ -21,6 +22,16 @@ func (fr *fakeRouter) Handle(method, pattern string, handlers []macaron.Handler) return &macaron.Route{} } +func (fr *fakeRouter) Get(pattern string, handlers ...macaron.Handler) *macaron.Route { + fr.route = append(fr.route, route{ + pattern: pattern, + method: http.MethodGet, + handlers: handlers, + }) + + return &macaron.Route{} +} + func emptyHandlers(n int) []macaron.Handler { res := []macaron.Handler{} for i := 1; n >= i; i++ { diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index 4551860a680..96fb3f2e9a1 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/pkg/cmd/grafana-server/main.go @@ -22,9 +22,9 @@ import ( _ "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/mqe" _ "github.com/grafana/grafana/pkg/tsdb/mysql" _ "github.com/grafana/grafana/pkg/tsdb/opentsdb" + _ "github.com/grafana/grafana/pkg/tsdb/prometheus" _ "github.com/grafana/grafana/pkg/tsdb/testdata" ) diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 3119574f7da..f6a94ac6ef0 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -24,6 +24,7 @@ import ( "github.com/grafana/grafana/pkg/services/search" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/social" + "github.com/grafana/grafana/pkg/tracing" ) func NewGrafanaServer() models.GrafanaServer { @@ -61,6 +62,14 @@ func (g *GrafanaServerImpl) Start() { eventpublisher.Init() plugins.Init() + closer, err := tracing.Init(setting.Cfg) + if err != nil { + g.log.Error("Tracing settings is not valid", "error", err) + g.Shutdown(1, "Startup failed") + return + } + defer closer.Close() + // init alerting if setting.AlertingEnabled && setting.ExecuteAlerts { engine := alerting.NewEngine() @@ -71,8 +80,8 @@ func (g *GrafanaServerImpl) Start() { cleanUpService := cleanup.NewCleanUpService() g.childRoutines.Go(func() error { return cleanUpService.Run(g.context) }) - if err := notifications.Init(); err != nil { - g.log.Error("Notification service failed to initialize", "erro", err) + if err = notifications.Init(); err != nil { + g.log.Error("Notification service failed to initialize", "error", err) g.Shutdown(1, "Startup failed") return } diff --git a/pkg/metrics/graphitebridge/graphite.go b/pkg/metrics/graphitebridge/graphite.go index 70f75d697b5..8504a908d9a 100644 --- a/pkg/metrics/graphitebridge/graphite.go +++ b/pkg/metrics/graphitebridge/graphite.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" "io" + "math" "net" "sort" "strings" @@ -53,7 +54,17 @@ const ( AbortOnError ) -var metricCategoryPrefix []string = []string{"proxy_", "api_", "page_", "alerting_", "aws_", "db_", "stat_", "go_", "process_"} +var metricCategoryPrefix []string = []string{ + "proxy_", + "api_", + "page_", + "alerting_", + "aws_", + "db_", + "stat_", + "go_", + "process_"} + var trimMetricPrefix []string = []string{"grafana_"} // Config defines the Graphite bridge config. @@ -208,6 +219,10 @@ func (b *Bridge) writeMetrics(w io.Writer, mfs []*dto.MetricFamily, prefix strin buf := bufio.NewWriter(w) for _, s := range vec { + if math.IsNaN(float64(s.Value)) { + continue + } + if err := writePrefix(buf, prefix); err != nil { return err } @@ -235,12 +250,6 @@ func writeMetric(buf *bufio.Writer, m model.Metric, mf *dto.MetricFamily) error if !hasName { numLabels = len(m) } - for _, v := range metricCategoryPrefix { - if strings.HasPrefix(string(metricName), v) { - group := strings.Replace(v, "_", " ", 1) - metricName = model.LabelValue(strings.Replace(string(metricName), v, group, 1)) - } - } for _, v := range trimMetricPrefix { if strings.HasPrefix(string(metricName), v) { @@ -248,6 +257,13 @@ func writeMetric(buf *bufio.Writer, m model.Metric, mf *dto.MetricFamily) error } } + for _, v := range metricCategoryPrefix { + if strings.HasPrefix(string(metricName), v) { + group := strings.Replace(v, "_", " ", 1) + metricName = model.LabelValue(strings.Replace(string(metricName), v, group, 1)) + } + } + labelStrings := make([]string, 0, numLabels) for label, value := range m { if label != model.MetricNameLabel { @@ -357,7 +373,7 @@ func replaceInvalidRune(c rune) rune { if c == ' ' { return '.' } - if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == ':' || (c >= '0' && c <= '9')) { + if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '-' || c == '_' || c == ':' || (c >= '0' && c <= '9')) { return '_' } return c diff --git a/pkg/metrics/graphitebridge/graphite_test.go b/pkg/metrics/graphitebridge/graphite_test.go index 254b813698a..4776a5ee5d9 100644 --- a/pkg/metrics/graphitebridge/graphite_test.go +++ b/pkg/metrics/graphitebridge/graphite_test.go @@ -128,6 +128,7 @@ func TestWriteSummary(t *testing.T) { prometheus.SummaryOpts{ Name: "name", Help: "docstring", + Namespace: "grafana", ConstLabels: prometheus.Labels{"constname": "constvalue"}, Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, }, @@ -187,6 +188,7 @@ func TestWriteHistogram(t *testing.T) { prometheus.HistogramOpts{ Name: "name", Help: "docstring", + Namespace: "grafana", ConstLabels: prometheus.Labels{"constname": "constvalue"}, Buckets: []float64{0.01, 0.02, 0.05, 0.1}, }, @@ -248,6 +250,17 @@ func TestCounterVec(t *testing.T) { cntVec := prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "page_response", + Namespace: "grafana", + Help: "docstring", + ConstLabels: prometheus.Labels{"constname": "constvalue"}, + }, + []string{"labelname"}, + ) + + apicntVec := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "api_response", + Namespace: "grafana", Help: "docstring", ConstLabels: prometheus.Labels{"constname": "constvalue"}, }, @@ -256,9 +269,12 @@ func TestCounterVec(t *testing.T) { reg := prometheus.NewRegistry() reg.MustRegister(cntVec) + reg.MustRegister(apicntVec) cntVec.WithLabelValues("val1").Inc() cntVec.WithLabelValues("val2").Inc() + apicntVec.WithLabelValues("val1").Inc() + apicntVec.WithLabelValues("val2").Inc() b, err := NewBridge(&Config{ URL: "localhost:8080", @@ -281,7 +297,9 @@ func TestCounterVec(t *testing.T) { t.Fatalf("error: %v", err) } - want := `prefix.page.response.constname.constvalue.labelname.val1.count 1 1477043 + want := `prefix.api.response.constname.constvalue.labelname.val1.count 1 1477043 +prefix.api.response.constname.constvalue.labelname.val2.count 1 1477043 +prefix.page.response.constname.constvalue.labelname.val1.count 1 1477043 prefix.page.response.constname.constvalue.labelname.val2.count 1 1477043 ` if got := buf.String(); want != got { @@ -291,6 +309,8 @@ prefix.page.response.constname.constvalue.labelname.val2.count 1 1477043 //next collect cntVec.WithLabelValues("val1").Inc() cntVec.WithLabelValues("val2").Inc() + apicntVec.WithLabelValues("val1").Inc() + apicntVec.WithLabelValues("val2").Inc() mfs, err = reg.Gather() if err != nil { @@ -303,7 +323,9 @@ prefix.page.response.constname.constvalue.labelname.val2.count 1 1477043 t.Fatalf("error: %v", err) } - want2 := `prefix.page.response.constname.constvalue.labelname.val1.count 1 1477053 + want2 := `prefix.api.response.constname.constvalue.labelname.val1.count 1 1477053 +prefix.api.response.constname.constvalue.labelname.val2.count 1 1477053 +prefix.page.response.constname.constvalue.labelname.val1.count 1 1477053 prefix.page.response.constname.constvalue.labelname.val2.count 1 1477053 ` if got := buf.String(); want2 != got { @@ -316,6 +338,7 @@ func TestCounter(t *testing.T) { prometheus.CounterOpts{ Name: "page_response", Help: "docstring", + Namespace: "grafana", ConstLabels: prometheus.Labels{"constname": "constvalue"}, }) @@ -373,7 +396,7 @@ func TestCounter(t *testing.T) { func TestTrimGrafanaNamespace(t *testing.T) { cntVec := prometheus.NewCounter( prometheus.CounterOpts{ - Name: "grafana_http_request_total", + Name: "http_request_total", Help: "docstring", ConstLabels: prometheus.Labels{"constname": "constvalue"}, }) @@ -410,12 +433,54 @@ func TestTrimGrafanaNamespace(t *testing.T) { } } +func TestSkipNanValues(t *testing.T) { + cntVec := prometheus.NewSummary( + prometheus.SummaryOpts{ + Name: "http_request_total", + Help: "docstring", + ConstLabels: prometheus.Labels{"constname": "constvalue"}, + }) + + reg := prometheus.NewRegistry() + reg.MustRegister(cntVec) + + b, err := NewBridge(&Config{ + URL: "localhost:8080", + Gatherer: reg, + CountersAsDelta: true, + }) + if err != nil { + t.Fatalf("error creating bridge: %v", err) + } + + // first collect + mfs, err := reg.Gather() + if err != nil { + t.Fatalf("error: %v", err) + } + + var buf bytes.Buffer + err = b.writeMetrics(&buf, mfs, "prefix.", model.Time(1477043083)) + if err != nil { + t.Fatalf("error: %v", err) + } + + want := `prefix.http_request_total_sum.constname.constvalue 0 1477043 +prefix.http_request_total_count.constname.constvalue.count 0 1477043 +` + + if got := buf.String(); want != got { + t.Fatalf("wanted \n%s\n, got \n%s\n", want, got) + } +} + func TestPush(t *testing.T) { reg := prometheus.NewRegistry() cntVec := prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "name", Help: "docstring", + Namespace: "grafana", ConstLabels: prometheus.Labels{"constname": "constvalue"}, }, []string{"labelname"}, diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index e3cca119cbb..fef1fe26f6c 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -102,7 +102,7 @@ func init() { M_Http_Request_Summary = prometheus.NewSummaryVec( prometheus.SummaryOpts{ - Name: "http_request_duration_milleseconds", + Name: "http_request_duration_milliseconds", Help: "http request summary", }, []string{"handler", "statuscode", "method"}, @@ -127,19 +127,19 @@ func init() { }) M_Api_Dashboard_Save = prometheus.NewSummary(prometheus.SummaryOpts{ - Name: "api_dashboard_save_milleseconds", + Name: "api_dashboard_save_milliseconds", Help: "summary for dashboard save duration", Namespace: exporterName, }) M_Api_Dashboard_Get = prometheus.NewSummary(prometheus.SummaryOpts{ - Name: "api_dashboard_get_milleseconds", + Name: "api_dashboard_get_milliseconds", Help: "summary for dashboard get duration", Namespace: exporterName, }) M_Api_Dashboard_Search = prometheus.NewSummary(prometheus.SummaryOpts{ - Name: "api_dashboard_search_milleseconds", + Name: "api_dashboard_search_milliseconds", Help: "summary for dashboard search duration", Namespace: exporterName, }) @@ -223,7 +223,7 @@ func init() { }) M_DataSource_ProxyReq_Timer = prometheus.NewSummary(prometheus.SummaryOpts{ - Name: "api_dataproxy_request_all_milleseconds", + Name: "api_dataproxy_request_all_milliseconds", Help: "summary for dashboard search duration", Namespace: exporterName, }) diff --git a/pkg/middleware/request_tracing.go b/pkg/middleware/request_tracing.go new file mode 100644 index 00000000000..c35c2a00734 --- /dev/null +++ b/pkg/middleware/request_tracing.go @@ -0,0 +1,36 @@ +package middleware + +import ( + "fmt" + "net/http" + + opentracing "github.com/opentracing/opentracing-go" + "github.com/opentracing/opentracing-go/ext" + + "gopkg.in/macaron.v1" +) + +func RequestTracing(handler string) macaron.Handler { + return func(res http.ResponseWriter, req *http.Request, c *macaron.Context) { + rw := res.(macaron.ResponseWriter) + + tracer := opentracing.GlobalTracer() + wireContext, _ := tracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(req.Header)) + span := tracer.StartSpan(fmt.Sprintf("HTTP %s", handler), ext.RPCServerOption(wireContext)) + defer span.Finish() + + ctx := opentracing.ContextWithSpan(req.Context(), span) + c.Req.Request = req.WithContext(ctx) + + c.Next() + + status := rw.Status() + + ext.HTTPStatusCode.Set(span, uint16(status)) + ext.HTTPUrl.Set(span, req.RequestURI) + ext.HTTPMethod.Set(span, req.Method) + if status >= 400 { + ext.Error.Set(span, true) + } + } +} diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index 433eb1b597f..417b3c96f10 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/pkg/services/alerting/conditions/query.go @@ -139,8 +139,8 @@ func (c *QueryCondition) executeQuery(context *alerting.EvalContext, timeRange * return result, nil } -func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource, timeRange *tsdb.TimeRange) *tsdb.Request { - req := &tsdb.Request{ +func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource, timeRange *tsdb.TimeRange) *tsdb.TsdbQuery { + req := &tsdb.TsdbQuery{ TimeRange: timeRange, Queries: []*tsdb.Query{ { diff --git a/pkg/services/alerting/conditions/query_test.go b/pkg/services/alerting/conditions/query_test.go index 17b47b2832b..c5a15adf45b 100644 --- a/pkg/services/alerting/conditions/query_test.go +++ b/pkg/services/alerting/conditions/query_test.go @@ -168,7 +168,7 @@ func (ctx *queryConditionTestContext) exec() (*alerting.ConditionResult, error) ctx.condition = condition - condition.HandleRequest = func(context context.Context, req *tsdb.Request) (*tsdb.Response, error) { + condition.HandleRequest = func(context context.Context, req *tsdb.TsdbQuery) (*tsdb.Response, error) { return &tsdb.Response{ Results: map[string]*tsdb.QueryResult{ "A": {Series: ctx.series}, diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go index 51d48287c14..4448a5cb978 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -2,8 +2,13 @@ package alerting import ( "context" + "fmt" "time" + "github.com/opentracing/opentracing-go" + "github.com/opentracing/opentracing-go/ext" + tlog "github.com/opentracing/opentracing-go/log" + "github.com/benbjohnson/clock" "github.com/grafana/grafana/pkg/log" "golang.org/x/sync/errgroup" @@ -99,22 +104,44 @@ func (e *Engine) processJob(grafanaCtx context.Context, job *Job) error { }() alertCtx, cancelFn := context.WithTimeout(context.Background(), alertTimeout) + span := opentracing.StartSpan("alert execution") + alertCtx = opentracing.ContextWithSpan(alertCtx, span) job.Running = true evalContext := NewEvalContext(alertCtx, job.Rule) + evalContext.Ctx = alertCtx done := make(chan struct{}) - go func() { defer func() { if err := recover(); err != nil { e.log.Error("Alert Panic", "error", err, "stack", log.Stack(1)) + ext.Error.Set(span, true) + span.LogFields( + tlog.Error(fmt.Errorf("%v", err)), + tlog.String("message", "failed to execute alert rule. panic was recovered."), + ) + span.Finish() close(done) } }() e.evalHandler.Eval(evalContext) e.resultHandler.Handle(evalContext) + + span.SetTag("alertId", evalContext.Rule.Id) + span.SetTag("dashboardId", evalContext.Rule.DashboardId) + span.SetTag("firing", evalContext.Firing) + span.SetTag("nodatapoints", evalContext.NoDataFound) + if evalContext.Error != nil { + ext.Error.Set(span, true) + span.LogFields( + tlog.Error(evalContext.Error), + tlog.String("message", "alerting execution failed"), + ) + } + + span.Finish() close(done) }() diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index 7f14e195799..b8579ffdc6a 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -89,6 +89,11 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) { continue } + panelId, err := panel.Get("id").Int64() + if err != nil { + return nil, fmt.Errorf("panel id is required. err %v", err) + } + // backward compatibility check, can be removed later enabled, hasEnabled := jsonAlert.CheckGet("enabled") if hasEnabled && enabled.MustBool() == false { @@ -103,7 +108,7 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) { alert := &m.Alert{ DashboardId: e.Dash.Id, OrgId: e.OrgId, - PanelId: panel.Get("id").MustInt64(), + PanelId: panelId, Id: jsonAlert.Get("id").MustInt64(), Name: jsonAlert.Get("name").MustString(), Handler: jsonAlert.Get("handler").MustInt64(), diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go index cb174783478..b7f83404452 100644 --- a/pkg/services/alerting/extractor_test.go +++ b/pkg/services/alerting/extractor_test.go @@ -200,6 +200,83 @@ func TestAlertRuleExtraction(t *testing.T) { }) }) + Convey("Panels missing id should return error", func() { + panelWithoutId := ` + { + "id": 57, + "title": "Graphite 4", + "originalTitle": "Graphite 4", + "tags": ["graphite"], + "rows": [ + { + "panels": [ + { + "title": "Active desktop users", + "editable": true, + "type": "graph", + "targets": [ + { + "refId": "A", + "target": "aliasByNode(statsd.fakesite.counters.session_start.desktop.count, 4)" + } + ], + "datasource": null, + "alert": { + "name": "name1", + "message": "desc1", + "handler": 1, + "frequency": "60s", + "conditions": [ + { + "type": "query", + "query": {"params": ["A", "5m", "now"]}, + "reducer": {"type": "avg", "params": []}, + "evaluator": {"type": ">", "params": [100]} + } + ] + } + }, + { + "title": "Active mobile users", + "id": 4, + "targets": [ + {"refId": "A", "target": ""}, + {"refId": "B", "target": "aliasByNode(statsd.fakesite.counters.session_start.mobile.count, 4)"} + ], + "datasource": "graphite2", + "alert": { + "name": "name2", + "message": "desc2", + "handler": 0, + "frequency": "60s", + "severity": "warning", + "conditions": [ + { + "type": "query", + "query": {"params": ["B", "5m", "now"]}, + "reducer": {"type": "avg", "params": []}, + "evaluator": {"type": ">", "params": [100]} + } + ] + } + } + ] + } + ] + }` + + dashJson, err := simplejson.NewJson([]byte(panelWithoutId)) + So(err, ShouldBeNil) + dash := m.NewDashboardFromJson(dashJson) + extractor := NewDashAlertExtractor(dash, 1) + + _, err = extractor.GetAlerts() + + Convey("panels without Id should return error", func() { + So(err, ShouldNotBeNil) + }) + }) + Convey("Parse and validate dashboard containing influxdb alert", func() { json2 := `{ diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 00eb0c4bc6b..cdf4be4ec6e 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -28,7 +28,6 @@ func init() { bus.AddHandler("sql", SearchUsers) bus.AddHandler("sql", GetUserOrgList) bus.AddHandler("sql", DeleteUser) - bus.AddHandler("sql", SetUsingOrg) bus.AddHandler("sql", UpdateUserPermissions) bus.AddHandler("sql", SetUserHelpFlag) } diff --git a/pkg/tracing/tracing.go b/pkg/tracing/tracing.go new file mode 100644 index 00000000000..be81cd7e794 --- /dev/null +++ b/pkg/tracing/tracing.go @@ -0,0 +1,116 @@ +package tracing + +import ( + "io" + "strings" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/setting" + + opentracing "github.com/opentracing/opentracing-go" + jaegercfg "github.com/uber/jaeger-client-go/config" + ini "gopkg.in/ini.v1" +) + +var ( + logger log.Logger = log.New("tracing") +) + +type TracingSettings struct { + Enabled bool + Address string + CustomTags map[string]string + SamplerType string + SamplerParam float64 +} + +func Init(file *ini.File) (io.Closer, error) { + settings := parseSettings(file) + return internalInit(settings) +} + +func parseSettings(file *ini.File) *TracingSettings { + settings := &TracingSettings{} + + var section, err = setting.Cfg.GetSection("tracing.jaeger") + if err != nil { + return settings + } + + settings.Address = section.Key("address").MustString("") + if settings.Address != "" { + settings.Enabled = true + } + + settings.CustomTags = splitTagSettings(section.Key("always_included_tag").MustString("")) + settings.SamplerType = section.Key("sampler_type").MustString("") + settings.SamplerParam = section.Key("sampler_param").MustFloat64(1) + + return settings +} + +func internalInit(settings *TracingSettings) (io.Closer, error) { + if !settings.Enabled { + return &nullCloser{}, nil + } + + cfg := jaegercfg.Configuration{ + Disabled: !settings.Enabled, + Sampler: &jaegercfg.SamplerConfig{ + Type: settings.SamplerType, + Param: settings.SamplerParam, + }, + Reporter: &jaegercfg.ReporterConfig{ + LogSpans: false, + LocalAgentHostPort: settings.Address, + }, + } + + jLogger := &jaegerLogWrapper{logger: log.New("jaeger")} + + options := []jaegercfg.Option{} + options = append(options, jaegercfg.Logger(jLogger)) + + for tag, value := range settings.CustomTags { + options = append(options, jaegercfg.Tag(tag, value)) + } + + tracer, closer, err := cfg.New("grafana", options...) + if err != nil { + return nil, err + } + + logger.Info("Initialized jaeger tracer", "address", settings.Address) + opentracing.InitGlobalTracer(tracer) + return closer, nil +} + +func splitTagSettings(input string) map[string]string { + res := map[string]string{} + + tags := strings.Split(input, ",") + for _, v := range tags { + kv := strings.Split(v, ":") + if len(kv) > 1 { + res[kv[0]] = kv[1] + } + } + + return res +} + +type jaegerLogWrapper struct { + logger log.Logger +} + +func (jlw *jaegerLogWrapper) Error(msg string) { + jlw.logger.Error(msg) +} + +func (jlw *jaegerLogWrapper) Infof(msg string, args ...interface{}) { + jlw.logger.Info(msg, args) +} + +type nullCloser struct{} + +func (*nullCloser) Close() error { return nil } diff --git a/pkg/tracing/tracing_test.go b/pkg/tracing/tracing_test.go new file mode 100644 index 00000000000..27e4de777a3 --- /dev/null +++ b/pkg/tracing/tracing_test.go @@ -0,0 +1,36 @@ +package tracing + +import "testing" + +func TestGroupSplit(t *testing.T) { + tests := []struct { + input string + expected map[string]string + }{ + { + input: "tag1:value1,tag2:value2", + expected: map[string]string{ + "tag1": "value1", + "tag2": "value2", + }, + }, + { + input: "", + expected: map[string]string{}, + }, + { + input: "tag1", + expected: map[string]string{}, + }, + } + + for _, test := range tests { + tags := splitTagSettings(test.input) + for k, v := range test.expected { + value, exists := tags[k] + if !exists || value != v { + t.Errorf("tags does not match %v ", test) + } + } + } +} diff --git a/pkg/tsdb/batch.go b/pkg/tsdb/batch.go deleted file mode 100644 index 8130ac94b0d..00000000000 --- a/pkg/tsdb/batch.go +++ /dev/null @@ -1,90 +0,0 @@ -package tsdb - -import "context" - -type Batch struct { - DataSourceId int64 - Queries QuerySlice - Depends map[string]bool - Done bool - Started bool -} - -type BatchSlice []*Batch - -func newBatch(dsId int64, queries QuerySlice) *Batch { - return &Batch{ - DataSourceId: dsId, - Queries: queries, - Depends: make(map[string]bool), - } -} - -func (bg *Batch) process(ctx context.Context, queryContext *QueryContext) { - executor, err := getExecutorFor(bg.Queries[0].DataSource) - - if err != nil { - bg.Done = true - result := &BatchResult{ - Error: err, - QueryResults: make(map[string]*QueryResult), - } - for _, query := range bg.Queries { - result.QueryResults[query.RefId] = &QueryResult{Error: result.Error} - } - queryContext.ResultsChan <- result - return - } - - res := executor.Execute(ctx, bg.Queries, queryContext) - bg.Done = true - queryContext.ResultsChan <- res -} - -func (bg *Batch) addQuery(query *Query) { - bg.Queries = append(bg.Queries, query) -} - -func (bg *Batch) allDependenciesAreIn(context *QueryContext) bool { - for key := range bg.Depends { - if _, exists := context.Results[key]; !exists { - return false - } - } - - return true -} - -func getBatches(req *Request) (BatchSlice, error) { - batches := make(BatchSlice, 0) - - for _, query := range req.Queries { - if foundBatch := findMatchingBatchGroup(query, batches); foundBatch != nil { - foundBatch.addQuery(query) - } else { - newBatch := newBatch(query.DataSource.Id, QuerySlice{query}) - batches = append(batches, newBatch) - - for _, refId := range query.Depends { - for _, batch := range batches { - for _, batchQuery := range batch.Queries { - if batchQuery.RefId == refId { - newBatch.Depends[refId] = true - } - } - } - } - } - } - - return batches, nil -} - -func findMatchingBatchGroup(query *Query, batches BatchSlice) *Batch { - for _, batch := range batches { - if batch.DataSourceId == query.DataSource.Id { - return batch - } - } - return nil -} diff --git a/pkg/tsdb/executor.go b/pkg/tsdb/executor.go deleted file mode 100644 index 251b3dc947a..00000000000 --- a/pkg/tsdb/executor.go +++ /dev/null @@ -1,36 +0,0 @@ -package tsdb - -import ( - "context" - "fmt" - - "github.com/grafana/grafana/pkg/models" -) - -type Executor interface { - Execute(ctx context.Context, queries QuerySlice, query *QueryContext) *BatchResult -} - -var registry map[string]GetExecutorFn - -type GetExecutorFn func(dsInfo *models.DataSource) (Executor, error) - -func init() { - registry = make(map[string]GetExecutorFn) -} - -func getExecutorFor(dsInfo *models.DataSource) (Executor, error) { - if fn, exists := registry[dsInfo.Type]; exists { - executor, err := fn(dsInfo) - if err != nil { - return nil, err - } - - return executor, nil - } - return nil, fmt.Errorf("Could not find executor for data source type: %s", dsInfo.Type) -} - -func RegisterExecutor(pluginId string, fn GetExecutorFn) { - registry[pluginId] = fn -} diff --git a/pkg/tsdb/fake_test.go b/pkg/tsdb/fake_test.go index 3c773971240..ab1bc944eb8 100644 --- a/pkg/tsdb/fake_test.go +++ b/pkg/tsdb/fake_test.go @@ -11,7 +11,7 @@ type FakeExecutor struct { resultsFn map[string]ResultsFn } -type ResultsFn func(context *QueryContext) *QueryResult +type ResultsFn func(context *TsdbQuery) *QueryResult func NewFakeExecutor(dsInfo *models.DataSource) (*FakeExecutor, error) { return &FakeExecutor{ @@ -20,9 +20,9 @@ func NewFakeExecutor(dsInfo *models.DataSource) (*FakeExecutor, error) { }, nil } -func (e *FakeExecutor) Execute(ctx context.Context, queries QuerySlice, context *QueryContext) *BatchResult { +func (e *FakeExecutor) Query(ctx context.Context, dsInfo *models.DataSource, context *TsdbQuery) *BatchResult { result := &BatchResult{QueryResults: make(map[string]*QueryResult)} - for _, query := range queries { + for _, query := range context.Queries { if results, has := e.results[query.RefId]; has { result.QueryResults[query.RefId] = results } diff --git a/pkg/tsdb/graphite/graphite.go b/pkg/tsdb/graphite/graphite.go index a467e839b82..273bf826de5 100644 --- a/pkg/tsdb/graphite/graphite.go +++ b/pkg/tsdb/graphite/graphite.go @@ -17,24 +17,15 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb" + opentracing "github.com/opentracing/opentracing-go" ) type GraphiteExecutor struct { - *models.DataSource HttpClient *http.Client } -func NewGraphiteExecutor(datasource *models.DataSource) (tsdb.Executor, error) { - httpClient, err := datasource.GetHttpClient() - - if err != nil { - return nil, err - } - - return &GraphiteExecutor{ - DataSource: datasource, - HttpClient: httpClient, - }, nil +func NewGraphiteExecutor(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) { + return &GraphiteExecutor{}, nil } var ( @@ -43,38 +34,61 @@ var ( func init() { glog = log.New("tsdb.graphite") - tsdb.RegisterExecutor("graphite", NewGraphiteExecutor) + tsdb.RegisterTsdbQueryEndpoint("graphite", NewGraphiteExecutor) } -func (e *GraphiteExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult { +func (e *GraphiteExecutor) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) *tsdb.BatchResult { result := &tsdb.BatchResult{} + from := "-" + formatTimeRange(tsdbQuery.TimeRange.From) + until := formatTimeRange(tsdbQuery.TimeRange.To) + var target string + formData := url.Values{ - "from": []string{"-" + formatTimeRange(context.TimeRange.From)}, - "until": []string{formatTimeRange(context.TimeRange.To)}, + "from": []string{from}, + "until": []string{until}, "format": []string{"json"}, "maxDataPoints": []string{"500"}, } - for _, query := range queries { + for _, query := range tsdbQuery.Queries { if fullTarget, err := query.Model.Get("targetFull").String(); err == nil { - formData["target"] = []string{fixIntervalFormat(fullTarget)} + target = fixIntervalFormat(fullTarget) } else { - formData["target"] = []string{fixIntervalFormat(query.Model.Get("target").MustString())} + target = fixIntervalFormat(query.Model.Get("target").MustString()) } } + formData["target"] = []string{target} + if setting.Env == setting.DEV { glog.Debug("Graphite request", "params", formData) } - req, err := e.createRequest(formData) + req, err := e.createRequest(dsInfo, formData) if err != nil { result.Error = err return result } - res, err := ctxhttp.Do(ctx, e.HttpClient, req) + httpClient, err := dsInfo.GetHttpClient() + if err != nil { + result.Error = err + return result + } + + span, ctx := opentracing.StartSpanFromContext(ctx, "graphite query") + span.SetTag("target", target) + span.SetTag("from", from) + span.SetTag("until", until) + defer span.Finish() + + opentracing.GlobalTracer().Inject( + span.Context(), + opentracing.HTTPHeaders, + opentracing.HTTPHeadersCarrier(req.Header)) + + res, err := ctxhttp.Do(ctx, httpClient, req) if err != nil { result.Error = err return result @@ -126,8 +140,8 @@ func (e *GraphiteExecutor) parseResponse(res *http.Response) ([]TargetResponseDT return data, nil } -func (e *GraphiteExecutor) createRequest(data url.Values) (*http.Request, error) { - u, _ := url.Parse(e.Url) +func (e *GraphiteExecutor) createRequest(dsInfo *models.DataSource, data url.Values) (*http.Request, error) { + u, _ := url.Parse(dsInfo.Url) u.Path = path.Join(u.Path, "render") req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(data.Encode())) @@ -137,8 +151,8 @@ func (e *GraphiteExecutor) createRequest(data url.Values) (*http.Request, error) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - if e.BasicAuth { - req.SetBasicAuth(e.BasicAuthUser, e.BasicAuthPassword) + if dsInfo.BasicAuth { + req.SetBasicAuth(dsInfo.BasicAuthUser, dsInfo.BasicAuthPassword) } return req, err diff --git a/pkg/tsdb/influxdb/influxdb.go b/pkg/tsdb/influxdb/influxdb.go index 21d359d24bf..e79e7b7790c 100644 --- a/pkg/tsdb/influxdb/influxdb.go +++ b/pkg/tsdb/influxdb/influxdb.go @@ -17,24 +17,16 @@ import ( ) type InfluxDBExecutor struct { - *models.DataSource + //*models.DataSource QueryParser *InfluxdbQueryParser ResponseParser *ResponseParser - HttpClient *http.Client + //HttpClient *http.Client } -func NewInfluxDBExecutor(datasource *models.DataSource) (tsdb.Executor, error) { - httpClient, err := datasource.GetHttpClient() - - if err != nil { - return nil, err - } - +func NewInfluxDBExecutor(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) { return &InfluxDBExecutor{ - DataSource: datasource, QueryParser: &InfluxdbQueryParser{}, ResponseParser: &ResponseParser{}, - HttpClient: httpClient, }, nil } @@ -44,18 +36,18 @@ var ( func init() { glog = log.New("tsdb.influxdb") - tsdb.RegisterExecutor("influxdb", NewInfluxDBExecutor) + tsdb.RegisterTsdbQueryEndpoint("influxdb", NewInfluxDBExecutor) } -func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult { +func (e *InfluxDBExecutor) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) *tsdb.BatchResult { result := &tsdb.BatchResult{} - query, err := e.getQuery(queries, context) + query, err := e.getQuery(dsInfo, tsdbQuery.Queries, tsdbQuery) if err != nil { return result.WithError(err) } - rawQuery, err := query.Build(context) + rawQuery, err := query.Build(tsdbQuery) if err != nil { return result.WithError(err) } @@ -64,12 +56,17 @@ func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, glog.Debug("Influxdb query", "raw query", rawQuery) } - req, err := e.createRequest(rawQuery) + req, err := e.createRequest(dsInfo, rawQuery) if err != nil { return result.WithError(err) } - resp, err := ctxhttp.Do(ctx, e.HttpClient, req) + httpClient, err := dsInfo.GetHttpClient() + if err != nil { + return result.WithError(err) + } + + resp, err := ctxhttp.Do(ctx, httpClient, req) if err != nil { return result.WithError(err) } @@ -98,10 +95,10 @@ func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, return result } -func (e *InfluxDBExecutor) getQuery(queries tsdb.QuerySlice, context *tsdb.QueryContext) (*Query, error) { +func (e *InfluxDBExecutor) getQuery(dsInfo *models.DataSource, queries []*tsdb.Query, context *tsdb.TsdbQuery) (*Query, error) { for _, v := range queries { - query, err := e.QueryParser.Parse(v.Model, e.DataSource) + query, err := e.QueryParser.Parse(v.Model, dsInfo) if err != nil { return nil, err } @@ -112,8 +109,8 @@ func (e *InfluxDBExecutor) getQuery(queries tsdb.QuerySlice, context *tsdb.Query return nil, fmt.Errorf("query request contains no queries") } -func (e *InfluxDBExecutor) createRequest(query string) (*http.Request, error) { - u, _ := url.Parse(e.Url) +func (e *InfluxDBExecutor) createRequest(dsInfo *models.DataSource, query string) (*http.Request, error) { + u, _ := url.Parse(dsInfo.Url) u.Path = path.Join(u.Path, "query") req, err := http.NewRequest(http.MethodGet, u.String(), nil) @@ -123,18 +120,18 @@ func (e *InfluxDBExecutor) createRequest(query string) (*http.Request, error) { params := req.URL.Query() params.Set("q", query) - params.Set("db", e.Database) + params.Set("db", dsInfo.Database) params.Set("epoch", "s") req.URL.RawQuery = params.Encode() req.Header.Set("User-Agent", "Grafana") - if e.BasicAuth { - req.SetBasicAuth(e.BasicAuthUser, e.BasicAuthPassword) + if dsInfo.BasicAuth { + req.SetBasicAuth(dsInfo.BasicAuthUser, dsInfo.BasicAuthPassword) } - if !e.BasicAuth && e.User != "" { - req.SetBasicAuth(e.User, e.Password) + if !dsInfo.BasicAuth && dsInfo.User != "" { + req.SetBasicAuth(dsInfo.User, dsInfo.Password) } glog.Debug("Influxdb request", "url", req.URL.String()) diff --git a/pkg/tsdb/influxdb/query.go b/pkg/tsdb/influxdb/query.go index 0bcdf1d5291..3a796e6db63 100644 --- a/pkg/tsdb/influxdb/query.go +++ b/pkg/tsdb/influxdb/query.go @@ -16,7 +16,7 @@ var ( regexpMeasurementPattern *regexp.Regexp = regexp.MustCompile(`^\/.*\/$`) ) -func (query *Query) Build(queryContext *tsdb.QueryContext) (string, error) { +func (query *Query) Build(queryContext *tsdb.TsdbQuery) (string, error) { var res string if query.UseRawQuery && query.RawQuery != "" { @@ -41,7 +41,7 @@ func (query *Query) Build(queryContext *tsdb.QueryContext) (string, error) { return res, nil } -func getDefinedInterval(query *Query, queryContext *tsdb.QueryContext) (*tsdb.Interval, error) { +func getDefinedInterval(query *Query, queryContext *tsdb.TsdbQuery) (*tsdb.Interval, error) { defaultInterval := tsdb.CalculateInterval(queryContext.TimeRange) if query.Interval == "" { @@ -104,7 +104,7 @@ func (query *Query) renderTags() []string { return res } -func (query *Query) renderTimeFilter(queryContext *tsdb.QueryContext) string { +func (query *Query) renderTimeFilter(queryContext *tsdb.TsdbQuery) string { from := "now() - " + queryContext.TimeRange.From to := "" @@ -115,7 +115,7 @@ func (query *Query) renderTimeFilter(queryContext *tsdb.QueryContext) string { return fmt.Sprintf("time > %s%s", from, to) } -func (query *Query) renderSelectors(queryContext *tsdb.QueryContext) string { +func (query *Query) renderSelectors(queryContext *tsdb.TsdbQuery) string { res := "SELECT " var selectors []string @@ -163,7 +163,7 @@ func (query *Query) renderWhereClause() string { return res } -func (query *Query) renderGroupBy(queryContext *tsdb.QueryContext) string { +func (query *Query) renderGroupBy(queryContext *tsdb.TsdbQuery) string { groupBy := "" for i, group := range query.GroupBy { if i == 0 { diff --git a/pkg/tsdb/influxdb/query_part.go b/pkg/tsdb/influxdb/query_part.go index 9c41fbbedba..981aea40526 100644 --- a/pkg/tsdb/influxdb/query_part.go +++ b/pkg/tsdb/influxdb/query_part.go @@ -15,7 +15,7 @@ type DefinitionParameters struct { } type QueryDefinition struct { - Renderer func(query *Query, queryContext *tsdb.QueryContext, part *QueryPart, innerExpr string) string + Renderer func(query *Query, queryContext *tsdb.TsdbQuery, part *QueryPart, innerExpr string) string Params []DefinitionParameters } @@ -94,14 +94,14 @@ func init() { renders["alias"] = QueryDefinition{Renderer: aliasRenderer} } -func fieldRenderer(query *Query, queryContext *tsdb.QueryContext, part *QueryPart, innerExpr string) string { +func fieldRenderer(query *Query, queryContext *tsdb.TsdbQuery, part *QueryPart, innerExpr string) string { if part.Params[0] == "*" { return "*" } return fmt.Sprintf(`"%s"`, part.Params[0]) } -func functionRenderer(query *Query, queryContext *tsdb.QueryContext, part *QueryPart, innerExpr string) string { +func functionRenderer(query *Query, queryContext *tsdb.TsdbQuery, part *QueryPart, innerExpr string) string { for i, param := range part.Params { if part.Type == "time" && param == "auto" { part.Params[i] = "$__interval" @@ -117,15 +117,15 @@ func functionRenderer(query *Query, queryContext *tsdb.QueryContext, part *Query return fmt.Sprintf("%s(%s)", part.Type, params) } -func suffixRenderer(query *Query, queryContext *tsdb.QueryContext, part *QueryPart, innerExpr string) string { +func suffixRenderer(query *Query, queryContext *tsdb.TsdbQuery, part *QueryPart, innerExpr string) string { return fmt.Sprintf("%s %s", innerExpr, part.Params[0]) } -func aliasRenderer(query *Query, queryContext *tsdb.QueryContext, part *QueryPart, innerExpr string) string { +func aliasRenderer(query *Query, queryContext *tsdb.TsdbQuery, part *QueryPart, innerExpr string) string { return fmt.Sprintf(`%s AS "%s"`, innerExpr, part.Params[0]) } -func (r QueryDefinition) Render(query *Query, queryContext *tsdb.QueryContext, part *QueryPart, innerExpr string) string { +func (r QueryDefinition) Render(query *Query, queryContext *tsdb.TsdbQuery, part *QueryPart, innerExpr string) string { return r.Renderer(query, queryContext, part, innerExpr) } @@ -149,6 +149,6 @@ type QueryPart struct { Params []string } -func (qp *QueryPart) Render(query *Query, queryContext *tsdb.QueryContext, expr string) string { +func (qp *QueryPart) Render(query *Query, queryContext *tsdb.TsdbQuery, expr string) string { return qp.Def.Renderer(query, queryContext, qp, expr) } diff --git a/pkg/tsdb/influxdb/query_part_test.go b/pkg/tsdb/influxdb/query_part_test.go index b5bae2fdf37..d23865174c8 100644 --- a/pkg/tsdb/influxdb/query_part_test.go +++ b/pkg/tsdb/influxdb/query_part_test.go @@ -10,7 +10,7 @@ import ( func TestInfluxdbQueryPart(t *testing.T) { Convey("Influxdb query parts", t, func() { - queryContext := &tsdb.QueryContext{TimeRange: tsdb.NewTimeRange("5m", "now")} + queryContext := &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("5m", "now")} query := &Query{} Convey("render field ", func() { diff --git a/pkg/tsdb/influxdb/query_test.go b/pkg/tsdb/influxdb/query_test.go index 0977ef8c1ce..5c8dc1eaf69 100644 --- a/pkg/tsdb/influxdb/query_test.go +++ b/pkg/tsdb/influxdb/query_test.go @@ -28,7 +28,7 @@ func TestInfluxdbQueryBuilder(t *testing.T) { tag1 := &Tag{Key: "hostname", Value: "server1", Operator: "="} tag2 := &Tag{Key: "hostname", Value: "server2", Operator: "=", Condition: "OR"} - queryContext := &tsdb.QueryContext{ + queryContext := &tsdb.TsdbQuery{ TimeRange: tsdb.NewTimeRange("5m", "now"), } @@ -101,12 +101,12 @@ func TestInfluxdbQueryBuilder(t *testing.T) { query := Query{} Convey("render from: 2h to now-1h", func() { query := Query{} - queryContext := &tsdb.QueryContext{TimeRange: tsdb.NewTimeRange("2h", "now-1h")} + queryContext := &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("2h", "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")} + queryContext := &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("10m", "now")} So(query.renderTimeFilter(queryContext), ShouldEqual, "time > now() - 10m") }) }) diff --git a/pkg/tsdb/models.go b/pkg/tsdb/models.go index 5ae27867c44..2fa2ed06c95 100644 --- a/pkg/tsdb/models.go +++ b/pkg/tsdb/models.go @@ -6,24 +6,21 @@ import ( "github.com/grafana/grafana/pkg/models" ) +type TsdbQuery struct { + TimeRange *TimeRange + Queries []*Query +} + type Query struct { RefId string Model *simplejson.Json Depends []string DataSource *models.DataSource Results []*TimeSeries - Exclude bool MaxDataPoints int64 IntervalMs int64 } -type QuerySlice []*Query - -type Request struct { - TimeRange *TimeRange - Queries QuerySlice -} - type Response struct { BatchTimings []*BatchTiming `json:"timings"` Results map[string]*QueryResult `json:"results"` diff --git a/pkg/tsdb/mqe/httpClient.go b/pkg/tsdb/mqe/httpClient.go deleted file mode 100644 index d8bf0888a35..00000000000 --- a/pkg/tsdb/mqe/httpClient.go +++ /dev/null @@ -1,129 +0,0 @@ -package mqe - -import ( - "context" - "net/http" - "net/url" - "path" - "strings" - - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/log" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/tsdb" - - "golang.org/x/net/context/ctxhttp" -) - -var ( - MaxWorker int = 4 -) - -type apiClient struct { - *models.DataSource - log log.Logger - httpClient *http.Client - responseParser *ResponseParser -} - -func NewApiClient(httpClient *http.Client, datasource *models.DataSource) *apiClient { - return &apiClient{ - DataSource: datasource, - log: log.New("tsdb.mqe"), - httpClient: httpClient, - responseParser: NewResponseParser(), - } -} - -func (e *apiClient) PerformRequests(ctx context.Context, queries []QueryToSend) (*tsdb.QueryResult, error) { - queryResult := &tsdb.QueryResult{} - - queryCount := len(queries) - jobsChan := make(chan QueryToSend, queryCount) - resultChan := make(chan []*tsdb.TimeSeries, queryCount) - errorsChan := make(chan error, 1) - for w := 1; w <= MaxWorker; w++ { - go e.spawnWorker(ctx, w, jobsChan, resultChan, errorsChan) - } - - for _, v := range queries { - jobsChan <- v - } - close(jobsChan) - - resultCounter := 0 - for { - select { - case timeseries := <-resultChan: - queryResult.Series = append(queryResult.Series, timeseries...) - resultCounter++ - - if resultCounter == queryCount { - close(resultChan) - return queryResult, nil - } - case err := <-errorsChan: - return nil, err - case <-ctx.Done(): - return nil, ctx.Err() - } - } -} - -func (e *apiClient) spawnWorker(ctx context.Context, id int, jobs chan QueryToSend, results chan []*tsdb.TimeSeries, errors chan error) { - e.log.Debug("Spawning worker", "id", id) - for query := range jobs { - if setting.Env == setting.DEV { - e.log.Debug("Sending request", "query", query.RawQuery) - } - - req, err := e.createRequest(query.RawQuery) - - resp, err := ctxhttp.Do(ctx, e.httpClient, req) - if err != nil { - errors <- err - return - } - - series, err := e.responseParser.Parse(resp, query) - if err != nil { - errors <- err - return - } - - results <- series - } - e.log.Debug("Worker is complete", "id", id) -} - -func (e *apiClient) createRequest(query string) (*http.Request, error) { - u, err := url.Parse(e.Url) - if err != nil { - return nil, err - } - - u.Path = path.Join(u.Path, "query") - - payload := simplejson.New() - payload.Set("query", query) - - jsonPayload, err := payload.MarshalJSON() - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(string(jsonPayload))) - if err != nil { - return nil, err - } - - req.Header.Set("User-Agent", "Grafana") - req.Header.Set("Content-Type", "application/json") - - if e.BasicAuth { - req.SetBasicAuth(e.BasicAuthUser, e.BasicAuthPassword) - } - - return req, nil -} diff --git a/pkg/tsdb/mqe/model_parser.go b/pkg/tsdb/mqe/model_parser.go deleted file mode 100644 index d139bea9c15..00000000000 --- a/pkg/tsdb/mqe/model_parser.go +++ /dev/null @@ -1,60 +0,0 @@ -package mqe - -import ( - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/tsdb" -) - -func NewQueryParser() *QueryParser { - return &QueryParser{} -} - -type QueryParser struct{} - -func (qp *QueryParser) Parse(model *simplejson.Json, dsInfo *models.DataSource, queryContext *tsdb.QueryContext) (*Query, error) { - query := &Query{TimeRange: queryContext.TimeRange} - query.AddClusterToAlias = model.Get("addClusterToAlias").MustBool(false) - query.AddHostToAlias = model.Get("addHostToAlias").MustBool(false) - query.UseRawQuery = model.Get("rawQuery").MustBool(false) - query.RawQuery = model.Get("query").MustString("") - - query.Cluster = model.Get("cluster").MustStringArray([]string{}) - query.Hosts = model.Get("hosts").MustStringArray([]string{}) - - var metrics []Metric - var err error - for _, metricsObj := range model.Get("metrics").MustArray() { - metricJson := simplejson.NewFromAny(metricsObj) - var m Metric - - m.Alias = metricJson.Get("alias").MustString("") - m.Metric, err = metricJson.Get("metric").String() - if err != nil { - return nil, err - } - - metrics = append(metrics, m) - } - - query.Metrics = metrics - - var functions []Function - for _, functionListObj := range model.Get("functionList").MustArray() { - functionListJson := simplejson.NewFromAny(functionListObj) - var f Function - - f.Func = functionListJson.Get("func").MustString("") - if err != nil { - return nil, err - } - - if f.Func != "" { - functions = append(functions, f) - } - } - - query.FunctionList = functions - - return query, nil -} diff --git a/pkg/tsdb/mqe/model_parser_test.go b/pkg/tsdb/mqe/model_parser_test.go deleted file mode 100644 index 58e7b7eb28d..00000000000 --- a/pkg/tsdb/mqe/model_parser_test.go +++ /dev/null @@ -1,127 +0,0 @@ -package mqe - -import ( - "testing" - - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/tsdb" - . "github.com/smartystreets/goconvey/convey" -) - -func TestMQEQueryParser(t *testing.T) { - Convey("MQE query parser", t, func() { - parser := &QueryParser{} - - dsInfo := &models.DataSource{JsonData: simplejson.New()} - queryContext := &tsdb.QueryContext{} - - Convey("can parse simple mqe model", func() { - json := ` - { - "cluster": [], - "hosts": [ - "staples-lab-1" - ], - "metrics": [ - { - "metric": "os.cpu.all*" - } - ], - "rawQuery": "", - "refId": "A" - } - ` - modelJson, err := simplejson.NewJson([]byte(json)) - So(err, ShouldBeNil) - - query, err := parser.Parse(modelJson, dsInfo, queryContext) - So(err, ShouldBeNil) - So(query.UseRawQuery, ShouldBeFalse) - - So(len(query.Cluster), ShouldEqual, 0) - So(query.Hosts[0], ShouldEqual, "staples-lab-1") - So(query.Metrics[0].Metric, ShouldEqual, "os.cpu.all*") - }) - - Convey("can parse multi serie mqe model", func() { - json := ` - { - "cluster": [ - "demoapp" - ], - "hosts": [ - "staples-lab-1" - ], - "metrics": [ - { - "metric": "os.cpu.all.active_percentage" - }, - { - "metric": "os.disk.sda.io_time" - } - ], - "functionList": [ - { - "func": "aggregate.min" - }, - { - "func": "aggregate.max" - } - ], - "rawQuery": "", - "refId": "A", - "addClusterToAlias": true, - "addHostToAlias": true - } - ` - modelJson, err := simplejson.NewJson([]byte(json)) - So(err, ShouldBeNil) - - query, err := parser.Parse(modelJson, dsInfo, queryContext) - So(err, ShouldBeNil) - So(query.UseRawQuery, ShouldBeFalse) - So(query.Cluster[0], ShouldEqual, "demoapp") - So(query.Metrics[0].Metric, ShouldEqual, "os.cpu.all.active_percentage") - So(query.Metrics[1].Metric, ShouldEqual, "os.disk.sda.io_time") - So(query.FunctionList[0].Func, ShouldEqual, "aggregate.min") - So(query.FunctionList[1].Func, ShouldEqual, "aggregate.max") - }) - - Convey("can parse raw query", func() { - json := ` - { - "addClusterToAlias": true, - "addHostToAlias": true, - "cluster": [], - "hosts": [ - "staples-lab-1" - ], - "metrics": [ - { - "alias": "cpu active", - "metric": "os.cpu.all.active_percentage" - }, - { - "alias": "disk sda time", - "metric": "os.disk.sda.io_time" - } - ], - "rawQuery": true, - "query": "raw-query", - "refId": "A" - } - ` - modelJson, err := simplejson.NewJson([]byte(json)) - So(err, ShouldBeNil) - - query, err := parser.Parse(modelJson, dsInfo, queryContext) - So(err, ShouldBeNil) - - So(query.UseRawQuery, ShouldBeTrue) - So(query.RawQuery, ShouldEqual, "raw-query") - So(query.AddClusterToAlias, ShouldBeTrue) - So(query.AddHostToAlias, ShouldBeTrue) - }) - }) -} diff --git a/pkg/tsdb/mqe/mqe.go b/pkg/tsdb/mqe/mqe.go deleted file mode 100644 index ae828e95c20..00000000000 --- a/pkg/tsdb/mqe/mqe.go +++ /dev/null @@ -1,85 +0,0 @@ -package mqe - -import ( - "context" - "net/http" - - "github.com/grafana/grafana/pkg/log" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/tsdb" -) - -type MQEExecutor struct { - *models.DataSource - queryParser *QueryParser - apiClient *apiClient - httpClient *http.Client - log log.Logger - tokenClient *TokenClient -} - -func NewMQEExecutor(dsInfo *models.DataSource) (tsdb.Executor, error) { - httpclient, err := dsInfo.GetHttpClient() - if err != nil { - return nil, err - } - - return &MQEExecutor{ - DataSource: dsInfo, - httpClient: httpclient, - log: log.New("tsdb.mqe"), - queryParser: NewQueryParser(), - apiClient: NewApiClient(httpclient, dsInfo), - tokenClient: NewTokenClient(dsInfo), - }, nil -} - -func init() { - tsdb.RegisterExecutor("mqe-datasource", NewMQEExecutor) -} - -type QueryToSend struct { - RawQuery string - Metric Metric - QueryRef *Query -} - -func (e *MQEExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) *tsdb.BatchResult { - result := &tsdb.BatchResult{} - - availableSeries, err := e.tokenClient.GetTokenData(ctx) - if err != nil { - return result.WithError(err) - } - - var mqeQueries []*Query - for _, v := range queries { - q, err := e.queryParser.Parse(v.Model, e.DataSource, queryContext) - if err != nil { - return result.WithError(err) - } - mqeQueries = append(mqeQueries, q) - } - - var rawQueries []QueryToSend - for _, v := range mqeQueries { - queries, err := v.Build(availableSeries.Metrics) - if err != nil { - return result.WithError(err) - } - - rawQueries = append(rawQueries, queries...) - } - - e.log.Debug("Sending request", "url", e.DataSource.Url) - - queryResult, err := e.apiClient.PerformRequests(ctx, rawQueries) - if err != nil { - return result.WithError(err) - } - - result.QueryResults = make(map[string]*tsdb.QueryResult) - result.QueryResults["A"] = queryResult - - return result -} diff --git a/pkg/tsdb/mqe/response_parser.go b/pkg/tsdb/mqe/response_parser.go deleted file mode 100644 index f3fdb00f0aa..00000000000 --- a/pkg/tsdb/mqe/response_parser.go +++ /dev/null @@ -1,177 +0,0 @@ -package mqe - -import ( - "encoding/json" - "io/ioutil" - "net/http" - "strconv" - "strings" - - "fmt" - - "regexp" - - "github.com/grafana/grafana/pkg/components/null" - "github.com/grafana/grafana/pkg/log" - "github.com/grafana/grafana/pkg/tsdb" -) - -func NewResponseParser() *ResponseParser { - return &ResponseParser{ - log: log.New("tsdb.mqe"), - } -} - -var ( - indexAliasPattern *regexp.Regexp - wildcardAliasPattern *regexp.Regexp -) - -func init() { - indexAliasPattern = regexp.MustCompile(`\$(\d)`) - wildcardAliasPattern = regexp.MustCompile(`[*!]`) -} - -type MQEResponse struct { - Success bool `json:"success"` - Name string `json:"name"` - Body []MQEResponseSerie `json:"body"` -} - -type ResponseTimeRange struct { - Start int64 `json:"start"` - End int64 `json:"end"` - Resolution int64 `json:"Resolution"` -} - -type MQEResponseSerie struct { - Query string `json:"query"` - Name string `json:"name"` - Type string `json:"type"` - Series []MQESerie `json:"series"` - TimeRange ResponseTimeRange `json:"timerange"` -} - -type MQESerie struct { - Values []null.Float `json:"values"` - Tagset map[string]string `json:"tagset"` -} - -type ResponseParser struct { - log log.Logger -} - -func (parser *ResponseParser) Parse(res *http.Response, queryRef QueryToSend) ([]*tsdb.TimeSeries, error) { - body, err := ioutil.ReadAll(res.Body) - defer res.Body.Close() - if err != nil { - return nil, err - } - - if res.StatusCode/100 != 2 { - parser.log.Error("Request failed", "status code", res.StatusCode, "body", string(body)) - return nil, fmt.Errorf("Returned invalid statuscode") - } - - var data *MQEResponse = &MQEResponse{} - err = json.Unmarshal(body, data) - if err != nil { - parser.log.Info("Failed to unmarshal response", "error", err, "status", res.Status, "body", string(body)) - return nil, err - } - - if !data.Success { - return nil, fmt.Errorf("Request failed.") - } - - var series []*tsdb.TimeSeries - for _, body := range data.Body { - for _, mqeSerie := range body.Series { - serie := &tsdb.TimeSeries{ - Tags: map[string]string{}, - Name: parser.formatLegend(body, mqeSerie, queryRef), - } - for key, value := range mqeSerie.Tagset { - serie.Tags[key] = value - } - - for i, value := range mqeSerie.Values { - timestamp := body.TimeRange.Start + int64(i)*body.TimeRange.Resolution - serie.Points = append(serie.Points, tsdb.NewTimePoint(value, float64(timestamp))) - } - - series = append(series, serie) - } - } - - return series, nil -} - -func (parser *ResponseParser) formatLegend(body MQEResponseSerie, mqeSerie MQESerie, queryToSend QueryToSend) string { - namePrefix := "" - - //append predefined tags to seriename - for key, value := range mqeSerie.Tagset { - if key == "cluster" && queryToSend.QueryRef.AddClusterToAlias { - namePrefix += value + " " - } - } - for key, value := range mqeSerie.Tagset { - if key == "host" && queryToSend.QueryRef.AddHostToAlias { - namePrefix += value + " " - } - } - - return namePrefix + parser.formatName(body, queryToSend) -} - -func (parser *ResponseParser) formatName(body MQEResponseSerie, queryToSend QueryToSend) string { - if indexAliasPattern.MatchString(queryToSend.Metric.Alias) { - return parser.indexAlias(body, queryToSend) - } - - if wildcardAliasPattern.MatchString(queryToSend.Metric.Metric) && wildcardAliasPattern.MatchString(queryToSend.Metric.Alias) { - return parser.wildcardAlias(body, queryToSend) - } - - return body.Name -} - -func (parser *ResponseParser) wildcardAlias(body MQEResponseSerie, queryToSend QueryToSend) string { - regString := strings.Replace(queryToSend.Metric.Metric, `*`, `(.*)`, 1) - reg, err := regexp.Compile(regString) - if err != nil { - return queryToSend.Metric.Alias - } - - matches := reg.FindAllStringSubmatch(queryToSend.RawQuery, -1) - - if len(matches) == 0 || len(matches[0]) < 2 { - return queryToSend.Metric.Alias - } - - return matches[0][1] -} - -func (parser *ResponseParser) indexAlias(body MQEResponseSerie, queryToSend QueryToSend) string { - queryNameParts := strings.Split(queryToSend.Metric.Metric, `.`) - - name := indexAliasPattern.ReplaceAllStringFunc(queryToSend.Metric.Alias, func(in string) string { - positionName := strings.TrimSpace(strings.Replace(in, "$", "", 1)) - - pos, err := strconv.Atoi(positionName) - if err != nil { - return "" - } - - for i, part := range queryNameParts { - if i == pos-1 { - return strings.TrimSpace(part) - } - } - - return "" - }) - - return strings.Replace(name, " ", ".", -1) -} diff --git a/pkg/tsdb/mqe/response_parser_test.go b/pkg/tsdb/mqe/response_parser_test.go deleted file mode 100644 index 34259aaea48..00000000000 --- a/pkg/tsdb/mqe/response_parser_test.go +++ /dev/null @@ -1,187 +0,0 @@ -package mqe - -import ( - "testing" - - "net/http" - "strings" - - "io/ioutil" - - "github.com/grafana/grafana/pkg/components/null" - . "github.com/smartystreets/goconvey/convey" -) - -var ( - testJson string -) - -func TestMQEResponseParser(t *testing.T) { - Convey("MQE response parser", t, func() { - parser := NewResponseParser() - - Convey("Can parse response", func() { - queryRef := QueryToSend{ - QueryRef: &Query{ - AddClusterToAlias: true, - AddHostToAlias: true, - }, - Metric: Metric{Alias: ""}, - } - - response := &http.Response{ - StatusCode: 200, - Body: ioutil.NopCloser(strings.NewReader(testJson)), - } - res, err := parser.Parse(response, queryRef) - So(err, ShouldBeNil) - So(len(res), ShouldEqual, 2) - So(len(res[0].Points), ShouldEqual, 14) - So(res[0].Name, ShouldEqual, "demoapp staples-lab-1 os.disk.sda3.weighted_io_time") - startTime := 1479287280000 - for i := 0; i < 11; i++ { - So(res[0].Points[i][0].Float64, ShouldEqual, i+1) - So(res[0].Points[i][1].Float64, ShouldEqual, startTime+(i*30000)) - } - - }) - - Convey("Can format legend", func() { - mqeSerie := MQESerie{ - Tagset: map[string]string{ - "cluster": "demoapp", - "host": "staples-lab-1", - }, - Values: []null.Float{null.NewFloat(3, true)}, - } - - Convey("with empty alias", func() { - serie := MQEResponseSerie{Name: "os.disk.sda3.weighted_io_time"} - queryRef := QueryToSend{ - QueryRef: &Query{ - AddClusterToAlias: true, - AddHostToAlias: true, - }, - Metric: Metric{Alias: ""}, - } - legend := parser.formatLegend(serie, mqeSerie, queryRef) - So(legend, ShouldEqual, "demoapp staples-lab-1 os.disk.sda3.weighted_io_time") - }) - - Convey("with index alias (ex $2 $3)", func() { - serie := MQEResponseSerie{Name: "os.disk.sda3.weighted_io_time"} - queryRef := QueryToSend{ - QueryRef: &Query{ - AddClusterToAlias: true, - AddHostToAlias: true, - }, - Metric: Metric{Alias: "$2 $3", Metric: "os.disk.sda3.weighted_io_time"}, - } - legend := parser.formatLegend(serie, mqeSerie, queryRef) - So(legend, ShouldEqual, "demoapp staples-lab-1 disk.sda3") - }) - - Convey("with wildcard alias", func() { - serie := MQEResponseSerie{Name: "os.disk.sda3.weighted_io_time", Query: "os.disk.*"} - - queryRef := QueryToSend{ - QueryRef: &Query{ - AddClusterToAlias: true, - AddHostToAlias: true, - }, - RawQuery: "os.disk.sda3.weighted_io_time", - Metric: Metric{Alias: "*", Metric: "os.disk.*.weighted_io_time"}, - } - legend := parser.formatLegend(serie, mqeSerie, queryRef) - So(legend, ShouldEqual, "demoapp staples-lab-1 sda3") - }) - }) - }) -} - -func init() { - testJson = `{ - "success": true, - "name": "select", - "body": [ - { - "query": "os.disk.sda3.weighted_io_time", - "name": "os.disk.sda3.weighted_io_time", - "type": "series", - "series": [ - { - "tagset": { - "cluster": "demoapp", - "host": "staples-lab-1" - }, - "values": [1,2,3,4,5,6,7,8,9,10,11, null, null, null] - }, - { - "tagset": { - "cluster": "demoapp", - "host": "staples-lab-2" - }, - "values": [11,10,9,8,7,6,5,4,3,2,1] - } - ], - "timerange": { - "start": 1479287280000, - "end": 1479287580000, - "resolution": 30000 - } - } - ], - "metadata": { - "description": { - "cluster": [ - "demoapp" - ], - "host": [ - "staples-lab-1", - "staples-lab-2" - ] - }, - "notes": null, - "profile": [ - { - "name": "Parsing Query", - "start": "2016-11-16T04:16:21.874354721-05:00", - "finish": "2016-11-16T04:16:21.874762291-05:00" - }, - { - "name": "Cassandra GetAllTags", - "start": "2016-11-16T04:16:21.874907171-05:00", - "finish": "2016-11-16T04:16:21.876401922-05:00" - }, - { - "name": "CachedMetricMetadataAPI_GetAllTags_Expired", - "start": "2016-11-16T04:16:21.874904751-05:00", - "finish": "2016-11-16T04:16:21.876407852-05:00" - }, - { - "name": "CachedMetricMetadataAPI_GetAllTags", - "start": "2016-11-16T04:16:21.874899491-05:00", - "finish": "2016-11-16T04:16:21.876410382-05:00" - }, - { - "name": "Blueflood FetchSingleTimeseries Resolution", - "description": "os.disk.sda3.weighted_io_time [app=demoapp,host=staples-lab-1]\n at 30s", - "start": "2016-11-16T04:16:21.876623312-05:00", - "finish": "2016-11-16T04:16:21.881763444-05:00" - }, - { - "name": "Blueflood FetchSingleTimeseries Resolution", - "description": "os.disk.sda3.weighted_io_time [app=demoapp,host=staples-lab-2]\n at 30s", - "start": "2016-11-16T04:16:21.876642682-05:00", - "finish": "2016-11-16T04:16:21.881895914-05:00" - }, - { - "name": "Blueflood FetchMultipleTimeseries", - "start": "2016-11-16T04:16:21.876418022-05:00", - "finish": "2016-11-16T04:16:21.881921474-05:00" - } - ] - } - } - ` -} diff --git a/pkg/tsdb/mqe/token_client.go b/pkg/tsdb/mqe/token_client.go deleted file mode 100644 index df136738ab6..00000000000 --- a/pkg/tsdb/mqe/token_client.go +++ /dev/null @@ -1,101 +0,0 @@ -package mqe - -import ( - "context" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/url" - "path" - "time" - - "golang.org/x/net/context/ctxhttp" - - "strconv" - - "github.com/grafana/grafana/pkg/log" - "github.com/grafana/grafana/pkg/models" - "github.com/patrickmn/go-cache" -) - -var tokenCache *cache.Cache - -func init() { - tokenCache = cache.New(5*time.Minute, 30*time.Second) -} - -type TokenClient struct { - log log.Logger - Datasource *models.DataSource - HttpClient *http.Client -} - -func NewTokenClient(datasource *models.DataSource) *TokenClient { - httpClient, _ := datasource.GetHttpClient() - - return &TokenClient{ - log: log.New("tsdb.mqe.tokenclient"), - Datasource: datasource, - HttpClient: httpClient, - } -} - -func (client *TokenClient) GetTokenData(ctx context.Context) (*TokenBody, error) { - key := strconv.FormatInt(client.Datasource.Id, 10) - - item, found := tokenCache.Get(key) - if found { - if result, ok := item.(*TokenBody); ok { - return result, nil - } - } - - b, err := client.RequestTokenData(ctx) - if err != nil { - return nil, err - } - - tokenCache.Set(key, b, cache.DefaultExpiration) - - return b, nil -} - -func (client *TokenClient) RequestTokenData(ctx context.Context) (*TokenBody, error) { - u, _ := url.Parse(client.Datasource.Url) - u.Path = path.Join(u.Path, "token") - - req, err := http.NewRequest(http.MethodGet, u.String(), nil) - if err != nil { - client.log.Info("Failed to create request", "error", err) - } - - res, err := ctxhttp.Do(ctx, client.HttpClient, req) - if err != nil { - return nil, err - } - - body, err := ioutil.ReadAll(res.Body) - defer res.Body.Close() - if err != nil { - return nil, err - } - - if res.StatusCode/100 != 2 { - client.log.Info("Request failed", "status", res.Status, "body", string(body)) - return nil, fmt.Errorf("Request failed status: %v", res.Status) - } - - var result *TokenResponse - err = json.Unmarshal(body, &result) - if err != nil { - client.log.Info("Failed to unmarshal response", "error", err, "status", res.Status, "body", string(body)) - return nil, err - } - - if !result.Success { - return nil, fmt.Errorf("Request failed for unknown reason.") - } - - return &result.Body, nil -} diff --git a/pkg/tsdb/mqe/token_client_test.go b/pkg/tsdb/mqe/token_client_test.go deleted file mode 100644 index f940f798b36..00000000000 --- a/pkg/tsdb/mqe/token_client_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package mqe - -import ( - "context" - "testing" - - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/models" - . "github.com/smartystreets/goconvey/convey" -) - -func TestTokenClient(t *testing.T) { - SkipConvey("Token client", t, func() { - dsInfo := &models.DataSource{ - JsonData: simplejson.New(), - Url: "", - } - - client := NewTokenClient(dsInfo) - - body, err := client.RequestTokenData(context.TODO()) - - So(err, ShouldBeNil) - //So(len(body.Functions), ShouldBeGreaterThan, 1) - So(len(body.Metrics), ShouldBeGreaterThan, 1) - }) -} diff --git a/pkg/tsdb/mqe/types.go b/pkg/tsdb/mqe/types.go deleted file mode 100644 index 0bd436ee9bd..00000000000 --- a/pkg/tsdb/mqe/types.go +++ /dev/null @@ -1,137 +0,0 @@ -package mqe - -import ( - "fmt" - - "strings" - - "regexp" - - "github.com/grafana/grafana/pkg/log" - "github.com/grafana/grafana/pkg/tsdb" -) - -type Metric struct { - Metric string - Alias string -} - -type Function struct { - Func string -} - -type Query struct { - Metrics []Metric - Hosts []string - Cluster []string - FunctionList []Function - AddClusterToAlias bool - AddHostToAlias bool - - TimeRange *tsdb.TimeRange - UseRawQuery bool - RawQuery string -} - -var ( - containsWildcardPattern *regexp.Regexp = regexp.MustCompile(`\*`) -) - -func (q *Query) Build(availableSeries []string) ([]QueryToSend, error) { - var queriesToSend []QueryToSend - where := q.buildWhereClause() - functions := q.buildFunctionList() - - for _, metric := range q.Metrics { - alias := "" - if metric.Alias != "" { - alias = fmt.Sprintf(" {%s}", metric.Alias) - } - - if !containsWildcardPattern.Match([]byte(metric.Metric)) { - rawQuery := q.renderQuerystring(metric.Metric, functions, alias, where, q.TimeRange) - queriesToSend = append(queriesToSend, QueryToSend{ - RawQuery: rawQuery, - QueryRef: q, - Metric: metric, - }) - } else { - m := strings.Replace(metric.Metric, "*", ".*", -1) - mp, err := regexp.Compile(m) - - if err != nil { - log.Error2("failed to compile regex for ", "metric", m) - continue - } - - //TODO: this lookup should be cached - for _, wildcardMatch := range availableSeries { - if mp.Match([]byte(wildcardMatch)) { - rawQuery := q.renderQuerystring(wildcardMatch, functions, alias, where, q.TimeRange) - queriesToSend = append(queriesToSend, QueryToSend{ - RawQuery: rawQuery, - QueryRef: q, - Metric: metric, - }) - } - } - } - } - - return queriesToSend, nil -} - -func (q *Query) renderQuerystring(path, functions, alias, where string, timerange *tsdb.TimeRange) string { - return fmt.Sprintf( - "`%s`%s%s %s from %v to %v", - path, - functions, - alias, - where, - q.TimeRange.GetFromAsMsEpoch(), - q.TimeRange.GetToAsMsEpoch()) -} - -func (q *Query) buildFunctionList() string { - functions := "" - for _, v := range q.FunctionList { - functions = fmt.Sprintf("%s|%s", functions, v.Func) - } - - return functions -} - -func (q *Query) buildWhereClause() string { - hasApps := len(q.Cluster) > 0 - hasHosts := len(q.Hosts) > 0 - - where := "" - if hasHosts || hasApps { - where += "where " - } - - if hasApps { - apps := strings.Join(q.Cluster, "', '") - where += fmt.Sprintf("cluster in ('%s')", apps) - } - - if hasHosts && hasApps { - where += " and " - } - - if hasHosts { - hosts := strings.Join(q.Hosts, "', '") - where += fmt.Sprintf("host in ('%s')", hosts) - } - - return where -} - -type TokenBody struct { - Metrics []string -} - -type TokenResponse struct { - Success bool - Body TokenBody -} diff --git a/pkg/tsdb/mqe/types_test.go b/pkg/tsdb/mqe/types_test.go deleted file mode 100644 index 6f716937f1e..00000000000 --- a/pkg/tsdb/mqe/types_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package mqe - -import ( - "testing" - - "time" - - "fmt" - - "github.com/grafana/grafana/pkg/tsdb" - . "github.com/smartystreets/goconvey/convey" -) - -func TestWildcardExpansion(t *testing.T) { - availableMetrics := []string{ - "os.cpu.all.idle", - "os.cpu.1.idle", - "os.cpu.2.idle", - "os.cpu.3.idle", - } - - now := time.Now() - from := now.Add((time.Minute*5)*-1).UnixNano() / int64(time.Millisecond) - to := now.UnixNano() / int64(time.Millisecond) - - Convey("Can expanding query", t, func() { - Convey("Without wildcard series", func() { - query := &Query{ - Metrics: []Metric{ - {Metric: "os.cpu.3.idle", Alias: ""}, - {Metric: "os.cpu.2.idle", Alias: ""}, - {Metric: "os.cpu.1.idle", Alias: "cpu"}, - }, - Hosts: []string{"staples-lab-1", "staples-lab-2"}, - Cluster: []string{"demoapp-1", "demoapp-2"}, - AddClusterToAlias: false, - AddHostToAlias: false, - FunctionList: []Function{ - {Func: "aggregate.min"}, - }, - TimeRange: &tsdb.TimeRange{Now: now, From: "5m", To: "now"}, - } - - expandeQueries, err := query.Build(availableMetrics) - So(err, ShouldBeNil) - So(len(expandeQueries), ShouldEqual, 3) - So(expandeQueries[0].RawQuery, ShouldEqual, fmt.Sprintf("`os.cpu.3.idle`|aggregate.min where cluster in ('demoapp-1', 'demoapp-2') and host in ('staples-lab-1', 'staples-lab-2') from %v to %v", from, to)) - So(expandeQueries[1].RawQuery, ShouldEqual, fmt.Sprintf("`os.cpu.2.idle`|aggregate.min where cluster in ('demoapp-1', 'demoapp-2') and host in ('staples-lab-1', 'staples-lab-2') from %v to %v", from, to)) - So(expandeQueries[2].RawQuery, ShouldEqual, fmt.Sprintf("`os.cpu.1.idle`|aggregate.min {cpu} where cluster in ('demoapp-1', 'demoapp-2') and host in ('staples-lab-1', 'staples-lab-2') from %v to %v", from, to)) - }) - - Convey("With two aggregate functions", func() { - query := &Query{ - Metrics: []Metric{ - {Metric: "os.cpu.3.idle", Alias: ""}, - }, - Hosts: []string{"staples-lab-1", "staples-lab-2"}, - Cluster: []string{"demoapp-1", "demoapp-2"}, - AddClusterToAlias: false, - AddHostToAlias: false, - FunctionList: []Function{ - {Func: "aggregate.min"}, - {Func: "aggregate.max"}, - }, - TimeRange: &tsdb.TimeRange{Now: now, From: "5m", To: "now"}, - } - - expandeQueries, err := query.Build(availableMetrics) - So(err, ShouldBeNil) - So(len(expandeQueries), ShouldEqual, 1) - So(expandeQueries[0].RawQuery, ShouldEqual, fmt.Sprintf("`os.cpu.3.idle`|aggregate.min|aggregate.max where cluster in ('demoapp-1', 'demoapp-2') and host in ('staples-lab-1', 'staples-lab-2') from %v to %v", from, to)) - }) - - Convey("Containing wildcard series", func() { - query := &Query{ - Metrics: []Metric{ - {Metric: "os.cpu*", Alias: ""}, - }, - Hosts: []string{"staples-lab-1"}, - AddClusterToAlias: false, - AddHostToAlias: false, - TimeRange: &tsdb.TimeRange{Now: now, From: "5m", To: "now"}, - } - - expandeQueries, err := query.Build(availableMetrics) - So(err, ShouldBeNil) - So(len(expandeQueries), ShouldEqual, 4) - - So(expandeQueries[0].RawQuery, ShouldEqual, fmt.Sprintf("`os.cpu.all.idle` where host in ('staples-lab-1') from %v to %v", from, to)) - So(expandeQueries[1].RawQuery, ShouldEqual, fmt.Sprintf("`os.cpu.1.idle` where host in ('staples-lab-1') from %v to %v", from, to)) - So(expandeQueries[2].RawQuery, ShouldEqual, fmt.Sprintf("`os.cpu.2.idle` where host in ('staples-lab-1') from %v to %v", from, to)) - So(expandeQueries[3].RawQuery, ShouldEqual, fmt.Sprintf("`os.cpu.3.idle` where host in ('staples-lab-1') from %v to %v", from, to)) - }) - }) -} diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go index e75dab8c04f..b1e7e916787 100644 --- a/pkg/tsdb/mysql/mysql.go +++ b/pkg/tsdb/mysql/mysql.go @@ -21,9 +21,8 @@ import ( ) type MysqlExecutor struct { - datasource *models.DataSource - engine *xorm.Engine - log log.Logger + engine *xorm.Engine + log log.Logger } type engineCacheType struct { @@ -38,16 +37,15 @@ var engineCache = engineCacheType{ } func init() { - tsdb.RegisterExecutor("mysql", NewMysqlExecutor) + tsdb.RegisterTsdbQueryEndpoint("mysql", NewMysqlExecutor) } -func NewMysqlExecutor(datasource *models.DataSource) (tsdb.Executor, error) { +func NewMysqlExecutor(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) { executor := &MysqlExecutor{ - datasource: datasource, - log: log.New("tsdb.mysql"), + log: log.New("tsdb.mysql"), } - err := executor.initEngine() + err := executor.initEngine(datasource) if err != nil { return nil, err } @@ -55,18 +53,24 @@ func NewMysqlExecutor(datasource *models.DataSource) (tsdb.Executor, error) { return executor, nil } -func (e *MysqlExecutor) initEngine() error { +func (e *MysqlExecutor) initEngine(dsInfo *models.DataSource) error { engineCache.Lock() defer engineCache.Unlock() - if engine, present := engineCache.cache[e.datasource.Id]; present { - if version, _ := engineCache.versions[e.datasource.Id]; version == e.datasource.Version { + if engine, present := engineCache.cache[dsInfo.Id]; present { + if version, _ := engineCache.versions[dsInfo.Id]; version == dsInfo.Version { e.engine = engine return nil } } - cnnstr := fmt.Sprintf("%s:%s@%s(%s)/%s?collation=utf8mb4_unicode_ci&parseTime=true&loc=UTC", e.datasource.User, e.datasource.Password, "tcp", e.datasource.Url, e.datasource.Database) + cnnstr := fmt.Sprintf("%s:%s@%s(%s)/%s?collation=utf8mb4_unicode_ci&parseTime=true&loc=UTC", + dsInfo.User, + dsInfo.Password, + "tcp", + dsInfo.Url, + dsInfo.Database) + e.log.Debug("getEngine", "connection", cnnstr) engine, err := xorm.NewEngine("mysql", cnnstr) @@ -76,22 +80,22 @@ func (e *MysqlExecutor) initEngine() error { return err } - engineCache.cache[e.datasource.Id] = engine + engineCache.cache[dsInfo.Id] = engine e.engine = engine return nil } -func (e *MysqlExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult { +func (e *MysqlExecutor) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) *tsdb.BatchResult { result := &tsdb.BatchResult{ QueryResults: make(map[string]*tsdb.QueryResult), } - macroEngine := NewMysqlMacroEngine(context.TimeRange) + macroEngine := NewMysqlMacroEngine(tsdbQuery.TimeRange) session := e.engine.NewSession() defer session.Close() db := session.DB() - for _, query := range queries { + for _, query := range tsdbQuery.Queries { rawSql := query.Model.Get("rawSql").MustString() if rawSql == "" { continue @@ -272,8 +276,6 @@ func (e MysqlExecutor) TransformToTimeSeries(query *tsdb.Query, rows *core.Rows, rowData.metric = "Unknown" } - //e.log.Debug("Rows", "metric", rowData.metric, "time", rowData.time, "value", rowData.value) - if !rowData.time.Valid { return fmt.Errorf("Found row with no time value") } diff --git a/pkg/tsdb/opentsdb/opentsdb.go b/pkg/tsdb/opentsdb/opentsdb.go index 987f341c7dc..f965392de01 100644 --- a/pkg/tsdb/opentsdb/opentsdb.go +++ b/pkg/tsdb/opentsdb/opentsdb.go @@ -22,20 +22,22 @@ import ( ) type OpenTsdbExecutor struct { - *models.DataSource - httpClient *http.Client + //*models.DataSource + //httpClient *http.Client } -func NewOpenTsdbExecutor(datasource *models.DataSource) (tsdb.Executor, error) { - httpClient, err := datasource.GetHttpClient() +func NewOpenTsdbExecutor(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) { + /* + httpClient, err := datasource.GetHttpClient() - if err != nil { - return nil, err - } + if err != nil { + return nil, err + } + */ return &OpenTsdbExecutor{ - DataSource: datasource, - httpClient: httpClient, + //DataSource: datasource, + //httpClient: httpClient, }, nil } @@ -45,10 +47,10 @@ var ( func init() { plog = log.New("tsdb.opentsdb") - tsdb.RegisterExecutor("opentsdb", NewOpenTsdbExecutor) + tsdb.RegisterTsdbQueryEndpoint("opentsdb", NewOpenTsdbExecutor) } -func (e *OpenTsdbExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) *tsdb.BatchResult { +func (e *OpenTsdbExecutor) Query(ctx context.Context, dsInfo *models.DataSource, queryContext *tsdb.TsdbQuery) *tsdb.BatchResult { result := &tsdb.BatchResult{} var tsdbQuery OpenTsdbQuery @@ -56,7 +58,7 @@ func (e *OpenTsdbExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, tsdbQuery.Start = queryContext.TimeRange.GetFromAsMsEpoch() tsdbQuery.End = queryContext.TimeRange.GetToAsMsEpoch() - for _, query := range queries { + for _, query := range queryContext.Queries { metric := e.buildMetric(query) tsdbQuery.Queries = append(tsdbQuery.Queries, metric) } @@ -65,13 +67,19 @@ func (e *OpenTsdbExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, plog.Debug("OpenTsdb request", "params", tsdbQuery) } - req, err := e.createRequest(tsdbQuery) + req, err := e.createRequest(dsInfo, tsdbQuery) if err != nil { result.Error = err return result } - res, err := ctxhttp.Do(ctx, e.httpClient, req) + httpClient, err := dsInfo.GetHttpClient() + if err != nil { + result.Error = err + return result + } + + res, err := ctxhttp.Do(ctx, httpClient, req) if err != nil { result.Error = err return result @@ -86,8 +94,8 @@ func (e *OpenTsdbExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, return result } -func (e *OpenTsdbExecutor) createRequest(data OpenTsdbQuery) (*http.Request, error) { - u, _ := url.Parse(e.Url) +func (e *OpenTsdbExecutor) createRequest(dsInfo *models.DataSource, data OpenTsdbQuery) (*http.Request, error) { + u, _ := url.Parse(dsInfo.Url) u.Path = path.Join(u.Path, "api/query") postData, err := json.Marshal(data) @@ -99,8 +107,8 @@ func (e *OpenTsdbExecutor) createRequest(data OpenTsdbQuery) (*http.Request, err } req.Header.Set("Content-Type", "application/json") - if e.BasicAuth { - req.SetBasicAuth(e.BasicAuthUser, e.BasicAuthPassword) + if dsInfo.BasicAuth { + req.SetBasicAuth(dsInfo.BasicAuthUser, dsInfo.BasicAuthPassword) } return req, err diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index 6540661efe6..9ca45109835 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -7,6 +7,8 @@ import ( "strings" "time" + "github.com/opentracing/opentracing-go" + "net/http" "github.com/grafana/grafana/pkg/components/null" @@ -16,12 +18,9 @@ import ( api "github.com/prometheus/client_golang/api" apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" "github.com/prometheus/common/model" - //api "github.com/prometheus/client_golang/api" - //apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" ) type PrometheusExecutor struct { - *models.DataSource Transport *http.Transport } @@ -37,15 +36,14 @@ func (bat basicAuthTransport) RoundTrip(req *http.Request) (*http.Response, erro return bat.Transport.RoundTrip(req) } -func NewPrometheusExecutor(dsInfo *models.DataSource) (tsdb.Executor, error) { +func NewPrometheusExecutor(dsInfo *models.DataSource) (tsdb.TsdbQueryEndpoint, error) { transport, err := dsInfo.GetHttpTransport() if err != nil { return nil, err } return &PrometheusExecutor{ - DataSource: dsInfo, - Transport: transport, + Transport: transport, }, nil } @@ -56,21 +54,21 @@ var ( func init() { plog = log.New("tsdb.prometheus") - tsdb.RegisterExecutor("prometheus", NewPrometheusExecutor) + tsdb.RegisterTsdbQueryEndpoint("prometheus", NewPrometheusExecutor) legendFormat = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`) } -func (e *PrometheusExecutor) getClient() (apiv1.API, error) { +func (e *PrometheusExecutor) getClient(dsInfo *models.DataSource) (apiv1.API, error) { cfg := api.Config{ - Address: e.DataSource.Url, + Address: dsInfo.Url, RoundTripper: e.Transport, } - if e.BasicAuth { + if dsInfo.BasicAuth { cfg.RoundTripper = basicAuthTransport{ Transport: e.Transport, - username: e.BasicAuthUser, - password: e.BasicAuthPassword, + username: dsInfo.BasicAuthUser, + password: dsInfo.BasicAuthPassword, } } @@ -82,15 +80,15 @@ func (e *PrometheusExecutor) getClient() (apiv1.API, error) { return apiv1.NewAPI(client), nil } -func (e *PrometheusExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) *tsdb.BatchResult { +func (e *PrometheusExecutor) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) *tsdb.BatchResult { result := &tsdb.BatchResult{} - client, err := e.getClient() + client, err := e.getClient(dsInfo) if err != nil { return result.WithError(err) } - query, err := parseQuery(queries, queryContext) + query, err := parseQuery(tsdbQuery.Queries, tsdbQuery) if err != nil { return result.WithError(err) } @@ -101,6 +99,12 @@ func (e *PrometheusExecutor) Execute(ctx context.Context, queries tsdb.QuerySlic Step: query.Step, } + span, ctx := opentracing.StartSpanFromContext(ctx, "alerting.prometheus") + span.SetTag("expr", query.Expr) + span.SetTag("start_unixnano", int64(query.Start.UnixNano())) + span.SetTag("stop_unixnano", int64(query.End.UnixNano())) + defer span.Finish() + value, err := client.QueryRange(ctx, query.Expr, timeRange) if err != nil { @@ -134,7 +138,7 @@ func formatLegend(metric model.Metric, query *PrometheusQuery) string { return string(result) } -func parseQuery(queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) (*PrometheusQuery, error) { +func parseQuery(queries []*tsdb.Query, queryContext *tsdb.TsdbQuery) (*PrometheusQuery, error) { queryModel := queries[0] expr, err := queryModel.Model.Get("expr").String() diff --git a/pkg/tsdb/query_context.go b/pkg/tsdb/query_context.go deleted file mode 100644 index db40ba6253c..00000000000 --- a/pkg/tsdb/query_context.go +++ /dev/null @@ -1,21 +0,0 @@ -package tsdb - -import "sync" - -type QueryContext struct { - TimeRange *TimeRange - Queries QuerySlice - Results map[string]*QueryResult - ResultsChan chan *BatchResult - Lock sync.RWMutex - BatchWaits sync.WaitGroup -} - -func NewQueryContext(queries QuerySlice, timeRange *TimeRange) *QueryContext { - return &QueryContext{ - TimeRange: timeRange, - Queries: queries, - ResultsChan: make(chan *BatchResult), - Results: make(map[string]*QueryResult), - } -} diff --git a/pkg/tsdb/query_endpoint.go b/pkg/tsdb/query_endpoint.go new file mode 100644 index 00000000000..74759959b08 --- /dev/null +++ b/pkg/tsdb/query_endpoint.go @@ -0,0 +1,36 @@ +package tsdb + +import ( + "context" + "fmt" + + "github.com/grafana/grafana/pkg/models" +) + +type TsdbQueryEndpoint interface { + Query(ctx context.Context, ds *models.DataSource, query *TsdbQuery) *BatchResult +} + +var registry map[string]GetTsdbQueryEndpointFn + +type GetTsdbQueryEndpointFn func(dsInfo *models.DataSource) (TsdbQueryEndpoint, error) + +func init() { + registry = make(map[string]GetTsdbQueryEndpointFn) +} + +func getTsdbQueryEndpointFor(dsInfo *models.DataSource) (TsdbQueryEndpoint, error) { + if fn, exists := registry[dsInfo.Type]; exists { + executor, err := fn(dsInfo) + if err != nil { + return nil, err + } + + return executor, nil + } + return nil, fmt.Errorf("Could not find executor for data source type: %s", dsInfo.Type) +} + +func RegisterTsdbQueryEndpoint(pluginId string, fn GetTsdbQueryEndpointFn) { + registry[pluginId] = fn +} diff --git a/pkg/tsdb/request.go b/pkg/tsdb/request.go index ed10a023e9c..6aa5f0efd38 100644 --- a/pkg/tsdb/request.go +++ b/pkg/tsdb/request.go @@ -4,60 +4,23 @@ import ( "context" ) -type HandleRequestFunc func(ctx context.Context, req *Request) (*Response, error) +type HandleRequestFunc func(ctx context.Context, req *TsdbQuery) (*Response, error) -func HandleRequest(ctx context.Context, req *Request) (*Response, error) { - context := NewQueryContext(req.Queries, req.TimeRange) - - batches, err := getBatches(req) +func HandleRequest(ctx context.Context, req *TsdbQuery) (*Response, error) { + //TODO niceify + ds := req.Queries[0].DataSource + endpoint, err := getTsdbQueryEndpointFor(ds) if err != nil { return nil, err } - currentlyExecuting := 0 - - for _, batch := range batches { - if len(batch.Depends) == 0 { - currentlyExecuting += 1 - batch.Started = true - go batch.process(ctx, context) - } + res := endpoint.Query(ctx, ds, req) + if res.Error != nil { + return nil, res.Error } - response := &Response{} - - for currentlyExecuting != 0 { - select { - case batchResult := <-context.ResultsChan: - currentlyExecuting -= 1 - - response.BatchTimings = append(response.BatchTimings, batchResult.Timings) - - if batchResult.Error != nil { - return nil, batchResult.Error - } - - for refId, result := range batchResult.QueryResults { - context.Results[refId] = result - } - - for _, batch := range batches { - // not interested in started batches - if batch.Started { - continue - } - - if batch.allDependenciesAreIn(context) { - currentlyExecuting += 1 - batch.Started = true - go batch.process(ctx, context) - } - } - case <-ctx.Done(): - return nil, ctx.Err() - } - } - - response.Results = context.Results - return response, nil + return &Response{ + Results: res.QueryResults, + BatchTimings: []*BatchTiming{res.Timings}, + }, nil } diff --git a/pkg/tsdb/testdata/scenarios.go b/pkg/tsdb/testdata/scenarios.go index 0c59973cd7b..073c220e838 100644 --- a/pkg/tsdb/testdata/scenarios.go +++ b/pkg/tsdb/testdata/scenarios.go @@ -11,7 +11,7 @@ import ( "github.com/grafana/grafana/pkg/tsdb" ) -type ScenarioHandler func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult +type ScenarioHandler func(query *tsdb.Query, context *tsdb.TsdbQuery) *tsdb.QueryResult type Scenario struct { Id string `json:"id"` @@ -33,9 +33,9 @@ func init() { Id: "random_walk", Name: "Random Walk", - Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult { - timeWalkerMs := context.TimeRange.GetFromAsMsEpoch() - to := context.TimeRange.GetToAsMsEpoch() + Handler: func(query *tsdb.Query, tsdbQuery *tsdb.TsdbQuery) *tsdb.QueryResult { + timeWalkerMs := tsdbQuery.TimeRange.GetFromAsMsEpoch() + to := tsdbQuery.TimeRange.GetToAsMsEpoch() series := newSeriesForQuery(query) @@ -60,7 +60,7 @@ func init() { registerScenario(&Scenario{ Id: "no_data_points", Name: "No Data Points", - Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult { + Handler: func(query *tsdb.Query, context *tsdb.TsdbQuery) *tsdb.QueryResult { return tsdb.NewQueryResult() }, }) @@ -68,7 +68,7 @@ func init() { registerScenario(&Scenario{ Id: "datapoints_outside_range", Name: "Datapoints Outside Range", - Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult { + Handler: func(query *tsdb.Query, context *tsdb.TsdbQuery) *tsdb.QueryResult { queryRes := tsdb.NewQueryResult() series := newSeriesForQuery(query) @@ -85,7 +85,7 @@ func init() { Id: "csv_metric_values", Name: "CSV Metric Values", StringInput: "1,20,90,30,5,0", - Handler: func(query *tsdb.Query, context *tsdb.QueryContext) *tsdb.QueryResult { + Handler: func(query *tsdb.Query, context *tsdb.TsdbQuery) *tsdb.QueryResult { queryRes := tsdb.NewQueryResult() stringInput := query.Model.Get("stringInput").MustString() diff --git a/pkg/tsdb/testdata/testdata.go b/pkg/tsdb/testdata/testdata.go index 6aefd8686d8..2ed19651130 100644 --- a/pkg/tsdb/testdata/testdata.go +++ b/pkg/tsdb/testdata/testdata.go @@ -13,7 +13,7 @@ type TestDataExecutor struct { log log.Logger } -func NewTestDataExecutor(dsInfo *models.DataSource) (tsdb.Executor, error) { +func NewTestDataExecutor(dsInfo *models.DataSource) (tsdb.TsdbQueryEndpoint, error) { return &TestDataExecutor{ DataSource: dsInfo, log: log.New("tsdb.testdata"), @@ -21,17 +21,17 @@ func NewTestDataExecutor(dsInfo *models.DataSource) (tsdb.Executor, error) { } func init() { - tsdb.RegisterExecutor("grafana-testdata-datasource", NewTestDataExecutor) + tsdb.RegisterTsdbQueryEndpoint("grafana-testdata-datasource", NewTestDataExecutor) } -func (e *TestDataExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult { +func (e *TestDataExecutor) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) *tsdb.BatchResult { result := &tsdb.BatchResult{} result.QueryResults = make(map[string]*tsdb.QueryResult) - for _, query := range queries { + for _, query := range tsdbQuery.Queries { scenarioId := query.Model.Get("scenarioId").MustString("random_walk") if scenario, exist := ScenarioRegistry[scenarioId]; exist { - result.QueryResults[query.RefId] = scenario.Handler(query, context) + result.QueryResults[query.RefId] = scenario.Handler(query, tsdbQuery) result.QueryResults[query.RefId].RefId = query.RefId } else { e.log.Error("Scenario not found", "scenarioId", scenarioId) diff --git a/pkg/tsdb/tsdb_test.go b/pkg/tsdb/tsdb_test.go index 2b1a2372cd6..5e84a646a31 100644 --- a/pkg/tsdb/tsdb_test.go +++ b/pkg/tsdb/tsdb_test.go @@ -3,60 +3,15 @@ package tsdb import ( "context" "testing" - "time" "github.com/grafana/grafana/pkg/models" . "github.com/smartystreets/goconvey/convey" ) func TestMetricQuery(t *testing.T) { - - Convey("When batches groups for query", t, func() { - - Convey("Given 3 queries for 2 data sources", func() { - request := &Request{ - Queries: QuerySlice{ - {RefId: "A", DataSource: &models.DataSource{Id: 1}}, - {RefId: "B", DataSource: &models.DataSource{Id: 1}}, - {RefId: "C", DataSource: &models.DataSource{Id: 2}}, - }, - } - - batches, err := getBatches(request) - So(err, ShouldBeNil) - - Convey("Should group into two batches", func() { - So(len(batches), ShouldEqual, 2) - }) - }) - - Convey("Given query 2 depends on query 1", func() { - request := &Request{ - Queries: QuerySlice{ - {RefId: "A", DataSource: &models.DataSource{Id: 1}}, - {RefId: "B", DataSource: &models.DataSource{Id: 2}}, - {RefId: "C", DataSource: &models.DataSource{Id: 3}, Depends: []string{"A", "B"}}, - }, - } - - batches, err := getBatches(request) - So(err, ShouldBeNil) - - Convey("Should return three batch groups", func() { - So(len(batches), ShouldEqual, 3) - }) - - Convey("Group 3 should have group 1 and 2 as dependencies", func() { - So(batches[2].Depends["A"], ShouldEqual, true) - So(batches[2].Depends["B"], ShouldEqual, true) - }) - - }) - }) - Convey("When executing request with one query", t, func() { - req := &Request{ - Queries: QuerySlice{ + req := &TsdbQuery{ + Queries: []*Query{ {RefId: "A", DataSource: &models.DataSource{Id: 1, Type: "test"}}, }, } @@ -74,8 +29,8 @@ func TestMetricQuery(t *testing.T) { }) Convey("When executing one request with two queries from same data source", t, func() { - req := &Request{ - Queries: QuerySlice{ + req := &TsdbQuery{ + Queries: []*Query{ {RefId: "A", DataSource: &models.DataSource{Id: 1, Type: "test"}}, {RefId: "B", DataSource: &models.DataSource{Id: 1, Type: "test"}}, }, @@ -99,26 +54,9 @@ func TestMetricQuery(t *testing.T) { }) - Convey("When executing one request with three queries from different datasources", t, func() { - req := &Request{ - Queries: QuerySlice{ - {RefId: "A", DataSource: &models.DataSource{Id: 1, Type: "test"}}, - {RefId: "B", DataSource: &models.DataSource{Id: 1, Type: "test"}}, - {RefId: "C", DataSource: &models.DataSource{Id: 2, Type: "test"}}, - }, - } - - res, err := HandleRequest(context.TODO(), req) - So(err, ShouldBeNil) - - Convey("Should have been batched in two requests", func() { - So(len(res.BatchTimings), ShouldEqual, 2) - }) - }) - Convey("When query uses data source of unknown type", t, func() { - req := &Request{ - Queries: QuerySlice{ + req := &TsdbQuery{ + Queries: []*Query{ {RefId: "A", DataSource: &models.DataSource{Id: 1, Type: "asdasdas"}}, }, } @@ -126,50 +64,11 @@ func TestMetricQuery(t *testing.T) { _, err := HandleRequest(context.TODO(), req) So(err, ShouldNotBeNil) }) - - Convey("When executing request that depend on other query", t, func() { - req := &Request{ - Queries: QuerySlice{ - { - RefId: "A", DataSource: &models.DataSource{Id: 1, Type: "test"}, - }, - { - RefId: "B", DataSource: &models.DataSource{Id: 2, Type: "test"}, Depends: []string{"A"}, - }, - }, - } - - fakeExecutor := registerFakeExecutor() - fakeExecutor.HandleQuery("A", func(c *QueryContext) *QueryResult { - time.Sleep(10 * time.Millisecond) - return &QueryResult{ - Series: TimeSeriesSlice{ - &TimeSeries{Name: "Ares"}, - }} - }) - fakeExecutor.HandleQuery("B", func(c *QueryContext) *QueryResult { - return &QueryResult{ - Series: TimeSeriesSlice{ - &TimeSeries{Name: "Bres+" + c.Results["A"].Series[0].Name}, - }} - }) - - res, err := HandleRequest(context.TODO(), req) - So(err, ShouldBeNil) - - Convey("Should have been batched in two requests", func() { - So(len(res.BatchTimings), ShouldEqual, 2) - }) - - Convey("Query B should have access to Query A results", func() { - So(res.Results["B"].Series[0].Name, ShouldEqual, "Bres+Ares") - }) - }) } func registerFakeExecutor() *FakeExecutor { executor, _ := NewFakeExecutor(nil) - RegisterExecutor("test", func(dsInfo *models.DataSource) (Executor, error) { + RegisterTsdbQueryEndpoint("test", func(dsInfo *models.DataSource) (TsdbQueryEndpoint, error) { return executor, nil }) diff --git a/public/app/app.ts b/public/app/app.ts index 3603e793d5c..27039c01350 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -9,6 +9,7 @@ import 'angular-sanitize'; import 'angular-dragdrop'; import 'angular-bindonce'; import 'angular-ui'; +import 'ngreact'; import $ from 'jquery'; import angular from 'angular'; @@ -84,6 +85,7 @@ export class GrafanaApp { 'pasvaz.bindonce', 'ui.bootstrap', 'ui.bootstrap.tpls', + 'react' ]; var module_types = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes']; diff --git a/public/app/core/components/PasswordStrength.tsx b/public/app/core/components/PasswordStrength.tsx new file mode 100644 index 00000000000..52e8a44165d --- /dev/null +++ b/public/app/core/components/PasswordStrength.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; +import 'react-dom'; +import coreModule from '../core_module'; + +export interface IProps { + password: string; +} + +export class PasswordStrength extends React.Component { + + constructor(props) { + super(props); + } + + render() { + let strengthText = "strength: strong like a bull."; + let strengthClass = "password-strength-good"; + + if (this.props.password.length < 4) { + strengthText = "strength: weak sauce."; + strengthClass = "password-strength-bad"; + } + + if (this.props.password.length <= 8) { + strengthText = "strength: you can do better."; + strengthClass = "password-strength-ok"; + } + + return ( +
+ {strengthText} +
+ ); + } +} + +coreModule.directive('passwordStrength', function(reactDirective) { + return reactDirective(PasswordStrength, ['password']); +}); diff --git a/public/app/core/components/collapse_box.ts b/public/app/core/components/collapse_box.ts deleted file mode 100644 index 7fc234cb583..00000000000 --- a/public/app/core/components/collapse_box.ts +++ /dev/null @@ -1,58 +0,0 @@ -/// - -import coreModule from 'app/core/core_module'; - -const template = ` - -`; - -export class CollapseBoxCtrl { - isOpen: boolean; - stateChanged: () => void; - - /** @ngInject **/ - constructor(private $timeout) { - this.isOpen = false; - } - - toggle() { - this.isOpen = !this.isOpen; - this.$timeout(() => { - this.stateChanged(); - }); - } -} - -export function collapseBox() { - return { - restrict: 'E', - template: template, - controller: CollapseBoxCtrl, - bindToController: true, - controllerAs: 'ctrl', - scope: { - "title": "@", - "isOpen": "=?", - "stateChanged": "&" - }, - transclude: { - 'actions': '?collapseBoxActions', - 'body': 'collapseBoxBody', - }, - link: function(scope, elem, attrs) { - } - }; -} - -coreModule.directive('collapseBox', collapseBox); diff --git a/public/app/core/components/colorpicker.ts b/public/app/core/components/colorpicker.ts index 1834a1eb8a1..f64903ad0b6 100644 --- a/public/app/core/components/colorpicker.ts +++ b/public/app/core/components/colorpicker.ts @@ -1,8 +1,5 @@ /// -import config from 'app/core/config'; -import _ from 'lodash'; -import $ from 'jquery'; import coreModule from 'app/core/core_module'; var template = ` @@ -37,7 +34,7 @@ export class ColorPickerCtrl { showAxisControls: boolean; /** @ngInject */ - constructor(private $scope, private $rootScope) { + constructor(private $scope, $rootScope) { this.colors = $rootScope.colors; this.autoClose = $scope.autoClose; this.series = $scope.series; diff --git a/public/app/core/components/dashboard_selector.ts b/public/app/core/components/dashboard_selector.ts index 4209ff3f8dc..f68e70a17c0 100644 --- a/public/app/core/components/dashboard_selector.ts +++ b/public/app/core/components/dashboard_selector.ts @@ -1,8 +1,5 @@ /// -import config from 'app/core/config'; -import _ from 'lodash'; -import $ from 'jquery'; import coreModule from 'app/core/core_module'; var template = ` diff --git a/public/app/core/components/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts index 6e3156df1fc..b1cbd9b6ced 100644 --- a/public/app/core/components/form_dropdown/form_dropdown.ts +++ b/public/app/core/components/form_dropdown/form_dropdown.ts @@ -1,6 +1,5 @@ /// -import config from 'app/core/config'; import _ from 'lodash'; import $ from 'jquery'; import coreModule from '../../core_module'; diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index ff8f0d1aadb..fe83442b301 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -1,9 +1,7 @@ /// import config from 'app/core/config'; -import store from 'app/core/store'; import _ from 'lodash'; -import angular from 'angular'; import $ from 'jquery'; import coreModule from 'app/core/core_module'; diff --git a/public/app/core/components/help/help.ts b/public/app/core/components/help/help.ts index 943a21f3c5f..bf84b43b5f9 100644 --- a/public/app/core/components/help/help.ts +++ b/public/app/core/components/help/help.ts @@ -8,7 +8,7 @@ export class HelpCtrl { shortcuts: any; /** @ngInject */ - constructor(private $scope, $sce) { + constructor() { this.tabIndex = 0; this.shortcuts = { 'Global': [ diff --git a/public/app/core/components/info_popover.ts b/public/app/core/components/info_popover.ts index df90728157d..a6ea853b7bb 100644 --- a/public/app/core/components/info_popover.ts +++ b/public/app/core/components/info_popover.ts @@ -1,7 +1,6 @@ /// import _ from 'lodash'; -import $ from 'jquery'; import coreModule from 'app/core/core_module'; import Drop from 'tether-drop'; diff --git a/public/app/core/components/json_explorer/helpers.ts b/public/app/core/components/json_explorer/helpers.ts index 2f2b80e73ea..7c4429d7c76 100644 --- a/public/app/core/components/json_explorer/helpers.ts +++ b/public/app/core/components/json_explorer/helpers.ts @@ -60,7 +60,7 @@ export function getValuePreview (object: Object, value: string): string { if (type === 'string') { value = '"' + escapeString(value) + '"'; } - if (type === 'function'){ + if (type === 'function') { // Remove content of the function return object.toString() diff --git a/public/app/core/components/json_explorer/json_explorer.ts b/public/app/core/components/json_explorer/json_explorer.ts index c2810f23b54..a7079477abc 100644 --- a/public/app/core/components/json_explorer/json_explorer.ts +++ b/public/app/core/components/json_explorer/json_explorer.ts @@ -6,7 +6,6 @@ import { getObjectName, getType, getValuePreview, - getPreview, cssClass, createElement } from './helpers'; @@ -191,7 +190,7 @@ export class JsonExplorer { if (this.element) { if (this.isOpen) { this.appendChildren(this.config.animateOpen); - } else{ + } else { this.removeChildren(this.config.animateClose); } this.element.classList.toggle(cssClass('open')); diff --git a/public/app/core/components/layout_selector/layout_selector.ts b/public/app/core/components/layout_selector/layout_selector.ts index e9d90aa1f48..b2071723677 100644 --- a/public/app/core/components/layout_selector/layout_selector.ts +++ b/public/app/core/components/layout_selector/layout_selector.ts @@ -1,9 +1,6 @@ /// -import config from 'app/core/config'; import store from 'app/core/store'; -import _ from 'lodash'; -import $ from 'jquery'; import coreModule from 'app/core/core_module'; var template = ` diff --git a/public/app/core/components/navbar/navbar.ts b/public/app/core/components/navbar/navbar.ts index 8388fe7c665..4bf55438e73 100644 --- a/public/app/core/components/navbar/navbar.ts +++ b/public/app/core/components/navbar/navbar.ts @@ -1,16 +1,13 @@ /// -import config from 'app/core/config'; -import _ from 'lodash'; -import $ from 'jquery'; import coreModule from '../../core_module'; -import {NavModel, NavModelItem} from '../../nav_model_srv'; +import {NavModel} from '../../nav_model_srv'; export class NavbarCtrl { model: NavModel; /** @ngInject */ - constructor(private $scope, private $rootScope, private contextSrv) { + constructor(private $rootScope) { } showSearch() { diff --git a/public/app/core/components/org_switcher.ts b/public/app/core/components/org_switcher.ts index fe8a7a3412c..41992f0b94f 100644 --- a/public/app/core/components/org_switcher.ts +++ b/public/app/core/components/org_switcher.ts @@ -1,7 +1,6 @@ /// import coreModule from 'app/core/core_module'; -import config from 'app/core/config'; import {contextSrv} from 'app/core/services/context_srv'; const template = ` diff --git a/public/app/core/components/row_ctrl.ts b/public/app/core/components/row_ctrl.ts index 8f2e0d81f46..6c4ebc0fb3a 100644 --- a/public/app/core/components/row_ctrl.ts +++ b/public/app/core/components/row_ctrl.ts @@ -30,7 +30,7 @@ export class DashboardRowCtrl { dashboard: any; panel: any; - constructor(private $rootScope) { + constructor() { this.panel.hiddenPanels = this.panel.hiddenPanels || []; } diff --git a/public/app/core/components/scroll/scroll.ts b/public/app/core/components/scroll/scroll.ts index c2e75df84c0..02e43bc76d9 100644 --- a/public/app/core/components/scroll/scroll.ts +++ b/public/app/core/components/scroll/scroll.ts @@ -2,7 +2,6 @@ import GeminiScrollbar from 'gemini-scrollbar'; import coreModule from 'app/core/core_module'; -import _ from 'lodash'; export function geminiScrollbar() { return { diff --git a/public/app/core/components/search/search.ts b/public/app/core/components/search/search.ts index ab3961858ba..32ea6800106 100644 --- a/public/app/core/components/search/search.ts +++ b/public/app/core/components/search/search.ts @@ -1,11 +1,7 @@ /// -import angular from 'angular'; -import config from 'app/core/config'; import _ from 'lodash'; -import $ from 'jquery'; import coreModule from '../../core_module'; -import appEvents from 'app/core/app_events'; export class SearchCtrl { isOpen: boolean; @@ -22,7 +18,7 @@ export class SearchCtrl { openCompleted: boolean; /** @ngInject */ - constructor(private $scope, private $location, private $timeout, private backendSrv, private contextSrv, private $rootScope) { + constructor($scope, private $location, private $timeout, private backendSrv, public contextSrv, $rootScope) { $rootScope.onAppEvent('show-dash-search', this.openSearch.bind(this), $scope); $rootScope.onAppEvent('hide-dash-search', this.closeSearch.bind(this), $scope); } diff --git a/public/app/core/components/sidemenu/sidemenu.ts b/public/app/core/components/sidemenu/sidemenu.ts index 2e01cadf6f9..b51f90d98f9 100644 --- a/public/app/core/components/sidemenu/sidemenu.ts +++ b/public/app/core/components/sidemenu/sidemenu.ts @@ -1,7 +1,7 @@ /// -import config from 'app/core/config'; import _ from 'lodash'; +import config from 'app/core/config'; import $ from 'jquery'; import coreModule from '../../core_module'; diff --git a/public/app/core/components/switch.ts b/public/app/core/components/switch.ts index 371b1ffe112..f7a2658a0ff 100644 --- a/public/app/core/components/switch.ts +++ b/public/app/core/components/switch.ts @@ -1,10 +1,6 @@ /// -import config from 'app/core/config'; -import _ from 'lodash'; -import $ from 'jquery'; import coreModule from 'app/core/core_module'; -import Drop from 'tether-drop'; var template = `