mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into develop
This commit is contained in:
commit
18337f610d
1
.gitignore
vendored
1
.gitignore
vendored
@ -25,6 +25,7 @@ public/css/*.min.css
|
||||
.idea/
|
||||
*.iml
|
||||
*.tmp
|
||||
.DS_Store
|
||||
.vscode/
|
||||
|
||||
/data/*
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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]
|
||||
|
6
docker/blocks/jaeger/fig
Normal file
6
docker/blocks/jaeger/fig
Normal file
@ -0,0 +1,6 @@
|
||||
jaeger:
|
||||
image: jaegertracing/all-in-one:latest
|
||||
ports:
|
||||
- "localhost:6831:6831/udp"
|
||||
- "16686:16686"
|
||||
|
@ -1,2 +1,3 @@
|
||||
FROM prom/prometheus
|
||||
ADD prometheus.yml /etc/prometheus/
|
||||
ADD alert.rules /etc/prometheus/
|
||||
|
10
docker/blocks/prometheus/alert.rules
Normal file
10
docker/blocks/prometheus/alert.rules
Normal file
@ -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"
|
||||
}
|
@ -18,3 +18,8 @@ fake-prometheus-data:
|
||||
environment:
|
||||
FD_DATASOURCE: prom
|
||||
|
||||
alertmanager:
|
||||
image: quay.io/prometheus/alertmanager
|
||||
net: host
|
||||
ports:
|
||||
- "9093:9093"
|
||||
|
@ -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:
|
||||
|
@ -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://<my_grafana_server_name_or_ip>:<grafana_server_port>/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:
|
||||
|
||||
|
13
package.json
13
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"
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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": {},
|
||||
|
@ -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",
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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++ {
|
||||
|
@ -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"
|
||||
)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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"},
|
||||
|
@ -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,
|
||||
})
|
||||
|
36
pkg/middleware/request_tracing.go
Normal file
36
pkg/middleware/request_tracing.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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{
|
||||
{
|
||||
|
@ -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},
|
||||
|
@ -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)
|
||||
}()
|
||||
|
||||
|
@ -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(),
|
||||
|
@ -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 := `{
|
||||
|
@ -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)
|
||||
}
|
||||
|
116
pkg/tracing/tracing.go
Normal file
116
pkg/tracing/tracing.go
Normal file
@ -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 }
|
36
pkg/tracing/tracing_test.go
Normal file
36
pkg/tracing/tracing_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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")
|
||||
})
|
||||
})
|
||||
|
@ -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"`
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
@ -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
|
||||
}
|
@ -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))
|
||||
})
|
||||
})
|
||||
}
|
@ -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")
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
36
pkg/tsdb/query_endpoint.go
Normal file
36
pkg/tsdb/query_endpoint.go
Normal file
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
14
pkg/tsdb/testdata/scenarios.go
vendored
14
pkg/tsdb/testdata/scenarios.go
vendored
@ -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()
|
||||
|
10
pkg/tsdb/testdata/testdata.go
vendored
10
pkg/tsdb/testdata/testdata.go
vendored
@ -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)
|
||||
|
@ -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
|
||||
})
|
||||
|
||||
|
@ -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'];
|
||||
|
39
public/app/core/components/PasswordStrength.tsx
Normal file
39
public/app/core/components/PasswordStrength.tsx
Normal file
@ -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<IProps, any> {
|
||||
|
||||
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 (
|
||||
<div className={`password-strength small ${strengthClass}`}>
|
||||
<em>{strengthText}</em>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.directive('passwordStrength', function(reactDirective) {
|
||||
return reactDirective(PasswordStrength, ['password']);
|
||||
});
|
@ -1,58 +0,0 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
const template = `
|
||||
<div class="collapse-box">
|
||||
<div class="collapse-box__header">
|
||||
<a class="collapse-box__header-title pointer" ng-click="ctrl.toggle()">
|
||||
<span class="fa fa-fw fa-caret-right" ng-hide="ctrl.isOpen"></span>
|
||||
<span class="fa fa-fw fa-caret-down" ng-hide="!ctrl.isOpen"></span>
|
||||
{{ctrl.title}}
|
||||
</a>
|
||||
<div class="collapse-box__header-actions" ng-transclude="actions" ng-if="ctrl.isOpen"></div>
|
||||
</div>
|
||||
<div class="collapse-box__body" ng-transclude="body" ng-if="ctrl.isOpen">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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);
|
@ -1,8 +1,5 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
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;
|
||||
|
@ -1,8 +1,5 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import config from 'app/core/config';
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
var template = `
|
||||
|
@ -1,6 +1,5 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import config from 'app/core/config';
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import coreModule from '../../core_module';
|
||||
|
@ -1,9 +1,7 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
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';
|
||||
|
@ -8,7 +8,7 @@ export class HelpCtrl {
|
||||
shortcuts: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $scope, $sce) {
|
||||
constructor() {
|
||||
this.tabIndex = 0;
|
||||
this.shortcuts = {
|
||||
'Global': [
|
||||
|
@ -1,7 +1,6 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import Drop from 'tether-drop';
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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'));
|
||||
|
@ -1,9 +1,6 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
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 = `
|
||||
|
@ -1,16 +1,13 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
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() {
|
||||
|
@ -1,7 +1,6 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
import config from 'app/core/config';
|
||||
import {contextSrv} from 'app/core/services/context_srv';
|
||||
|
||||
const template = `
|
||||
|
@ -30,7 +30,7 @@ export class DashboardRowCtrl {
|
||||
dashboard: any;
|
||||
panel: any;
|
||||
|
||||
constructor(private $rootScope) {
|
||||
constructor() {
|
||||
this.panel.hiddenPanels = this.panel.hiddenPanels || [];
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
import GeminiScrollbar from 'gemini-scrollbar';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import _ from 'lodash';
|
||||
|
||||
export function geminiScrollbar() {
|
||||
return {
|
||||
|
@ -1,11 +1,7 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
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);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import config from 'app/core/config';
|
||||
import _ from 'lodash';
|
||||
import config from 'app/core/config';
|
||||
import $ from 'jquery';
|
||||
import coreModule from '../../core_module';
|
||||
|
||||
|
@ -1,10 +1,6 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
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 = `
|
||||
<label for="check-{{ctrl.id}}" class="gf-form-label {{ctrl.labelClass}} pointer" ng-show="ctrl.label">
|
||||
|
@ -1,5 +1,4 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import _ from 'lodash';
|
||||
|
||||
const template = `
|
||||
@ -17,7 +16,7 @@ export class UserGroupPickerCtrl {
|
||||
debouncedSearchGroups: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv, private $scope, $sce, private uiSegmentSrv) {
|
||||
constructor(private backendSrv) {
|
||||
this.debouncedSearchGroups = _.debounce(this.searchGroups, 500, {'leading': true, 'trailing': false});
|
||||
this.reset();
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import _ from 'lodash';
|
||||
|
||||
const template = `
|
||||
@ -17,7 +16,7 @@ export class UserPickerCtrl {
|
||||
userPicked: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv, private $scope, $sce) {
|
||||
constructor(private backendSrv) {
|
||||
this.reset();
|
||||
this.debouncedSearchUsers = _.debounce(this.searchUsers, 500, {'leading': true, 'trailing': false});
|
||||
}
|
||||
|
@ -1,32 +0,0 @@
|
||||
<div class="modal-body">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-header-title">
|
||||
<i class="fa fa-cog fa-spin"></i>
|
||||
<span class="p-l-1">{{model.name}}</span>
|
||||
</h2>
|
||||
|
||||
<a class="modal-header-close" ng-click="dismiss();">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="modal-content">
|
||||
<div ng-if="activeStep">
|
||||
|
||||
</div>
|
||||
|
||||
<!-- <table class="filter-table"> -->
|
||||
<!-- <tbody> -->
|
||||
<!-- <tr ng-repeat="step in model.steps"> -->
|
||||
<!-- <td>{{step.name}}</td> -->
|
||||
<!-- <td>{{step.status}}</td> -->
|
||||
<!-- <td width="1%"> -->
|
||||
<!-- <i class="fa fa-check" style="color: #39A039"></i> -->
|
||||
<!-- </td> -->
|
||||
<!-- </tr> -->
|
||||
<!-- </tbody> -->
|
||||
<!-- </table> -->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -1,73 +0,0 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import config from 'app/core/config';
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
export class WizardSrv {
|
||||
/** @ngInject */
|
||||
constructor() {
|
||||
}
|
||||
}
|
||||
|
||||
export interface WizardStep {
|
||||
name: string;
|
||||
type: string;
|
||||
process: any;
|
||||
}
|
||||
|
||||
export class SelectOptionStep {
|
||||
type: string;
|
||||
name: string;
|
||||
fulfill: any;
|
||||
|
||||
constructor() {
|
||||
this.type = 'select';
|
||||
}
|
||||
|
||||
process() {
|
||||
return new Promise((fulfill, reject) => {
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class WizardFlow {
|
||||
name: string;
|
||||
steps: WizardStep[];
|
||||
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
this.steps = [];
|
||||
}
|
||||
|
||||
addStep(step) {
|
||||
this.steps.push(step);
|
||||
}
|
||||
|
||||
next(index) {
|
||||
var step = this.steps[0];
|
||||
|
||||
return step.process().then(() => {
|
||||
if (this.steps.length === index+1) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.next(index+1);
|
||||
});
|
||||
}
|
||||
|
||||
start() {
|
||||
appEvents.emit('show-modal', {
|
||||
src: 'public/app/core/components/wizard/wizard.html',
|
||||
model: this
|
||||
});
|
||||
|
||||
return this.next(0);
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.service('wizardSrv', WizardSrv);
|
@ -8,9 +8,9 @@ export class SignUpCtrl {
|
||||
/** @ngInject */
|
||||
constructor(
|
||||
private $scope: any,
|
||||
private $location: any,
|
||||
private contextSrv: any,
|
||||
private backendSrv: any) {
|
||||
private backendSrv: any,
|
||||
$location: any,
|
||||
contextSrv: any) {
|
||||
|
||||
contextSrv.sidemenu = false;
|
||||
$scope.ctrl = this;
|
||||
|
@ -35,7 +35,6 @@ import {layoutSelector} from './components/layout_selector/layout_selector';
|
||||
import {switchDirective} from './components/switch';
|
||||
import {dashboardSelector} from './components/dashboard_selector';
|
||||
import {queryPartEditorDirective} from './components/query_part/query_part_editor';
|
||||
import {WizardFlow} from './components/wizard/wizard';
|
||||
import {formDropdownDirective} from './components/form_dropdown/form_dropdown';
|
||||
import 'app/core/controllers/all';
|
||||
import 'app/core/services/all';
|
||||
@ -48,7 +47,7 @@ import {assignModelProperties} from './utils/model_utils';
|
||||
import {contextSrv} from './services/context_srv';
|
||||
import {KeybindingSrv} from './services/keybindingSrv';
|
||||
import {helpModal} from './components/help/help';
|
||||
import {collapseBox} from './components/collapse_box';
|
||||
import {PasswordStrength} from './components/PasswordStrength';
|
||||
import {JsonExplorer} from './components/json_explorer/json_explorer';
|
||||
import {NavModelSrv, NavModel} from './nav_model_srv';
|
||||
import {userPicker} from './components/user_picker';
|
||||
@ -73,14 +72,12 @@ export {
|
||||
appEvents,
|
||||
dashboardSelector,
|
||||
queryPartEditorDirective,
|
||||
WizardFlow,
|
||||
colors,
|
||||
formDropdownDirective,
|
||||
assignModelProperties,
|
||||
contextSrv,
|
||||
KeybindingSrv,
|
||||
helpModal,
|
||||
collapseBox,
|
||||
JsonExplorer,
|
||||
NavModelSrv,
|
||||
NavModel,
|
||||
@ -89,4 +86,5 @@ export {
|
||||
geminiScrollbar,
|
||||
gfPageDirective,
|
||||
orgSwitcher,
|
||||
PasswordStrength,
|
||||
};
|
||||
|
@ -7,7 +7,7 @@ export class DeltaCtrl {
|
||||
observer: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $rootScope) {
|
||||
constructor($rootScope) {
|
||||
const waitForCompile = function(mutations) {
|
||||
if (mutations.length === 1) {
|
||||
this.$rootScope.appEvent('json-diff-ready');
|
||||
|
@ -4,7 +4,7 @@ define([
|
||||
function (coreModule) {
|
||||
'use strict';
|
||||
|
||||
coreModule.default.directive('passwordStrength', function() {
|
||||
coreModule.default.directive('passwordStrength2', function() {
|
||||
var template = '<div class="password-strength small" ng-if="!loginMode" ng-class="strengthClass">' +
|
||||
'<em>{{strengthText}}</em>' +
|
||||
'</div>';
|
||||
|
@ -1,7 +1,5 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
|
||||
import coreModule from '../core_module';
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
import _ from 'lodash';
|
||||
import config from 'app/core/config';
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
import {Observable} from 'vendor/npm/rxjs/Observable';
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
///<reference path="../headers/common.d.ts" />
|
||||
|
||||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
import angular from 'angular';
|
||||
|
||||
export class Profiler {
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
import './dashboard_loaders';
|
||||
|
||||
import angular from 'angular';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import {BundleLoader} from './bundle_loader';
|
||||
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
@ -10,7 +9,7 @@ export class AlertSrv {
|
||||
list: any[];
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $timeout, private $sce, private $rootScope, private $modal) {
|
||||
constructor(private $timeout, private $rootScope, private $modal) {
|
||||
this.list = [];
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,6 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
import config from 'app/core/config';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
@ -12,7 +10,7 @@ export class BackendSrv {
|
||||
private noBackendCache: boolean;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $http, private alertSrv, private $rootScope, private $q, private $timeout, private contextSrv) {
|
||||
constructor(private $http, private alertSrv, private $q, private $timeout, private contextSrv) {
|
||||
}
|
||||
|
||||
get(url, params?) {
|
||||
|
@ -6,7 +6,7 @@ import coreModule from '../core_module';
|
||||
class DynamicDirectiveSrv {
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $compile, private $parse, private $rootScope) {}
|
||||
constructor(private $compile, private $rootScope) {}
|
||||
|
||||
addDirective(element, name, scope) {
|
||||
var child = angular.element(document.createElement(name));
|
||||
|
@ -14,10 +14,7 @@ export class KeybindingSrv {
|
||||
/** @ngInject */
|
||||
constructor(
|
||||
private $rootScope,
|
||||
private $modal,
|
||||
private $location,
|
||||
private contextSrv,
|
||||
private $timeout) {
|
||||
private $location) {
|
||||
|
||||
// clear out all shortcuts on route change
|
||||
$rootScope.$on('$routeChangeSuccess', () => {
|
||||
|
@ -1,8 +1,6 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import config from 'app/core/config';
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import Drop from 'tether-drop';
|
||||
|
||||
|
@ -1,9 +1,5 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import config from 'app/core/config';
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
|
14
public/app/core/specs/PasswordStrength_specs.tsx
Normal file
14
public/app/core/specs/PasswordStrength_specs.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
// import React from 'react';
|
||||
// import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
|
||||
// import {shallow} from 'enzyme';
|
||||
//
|
||||
// import {PasswordStrength} from '../components/PasswordStrength';
|
||||
//
|
||||
// describe('PasswordStrength', () => {
|
||||
//
|
||||
// it.skip('should have class bad if length below 4', () => {
|
||||
// const wrapper = shallow(<PasswordStrength password="asd" />);
|
||||
// expect(wrapper.find(".password-strength-bad")).to.have.length(3);
|
||||
// });
|
||||
// });
|
||||
//
|
@ -168,15 +168,16 @@ export default class TimeSeries {
|
||||
if (currentValue < this.stats.min) {
|
||||
this.stats.min = currentValue;
|
||||
}
|
||||
if (this.stats.first === null){
|
||||
|
||||
if (this.stats.first === null) {
|
||||
this.stats.first = currentValue;
|
||||
}else{
|
||||
} else {
|
||||
if (previousValue > currentValue) { // counter reset
|
||||
previousDeltaUp = false;
|
||||
if (i === this.datapoints.length-1) { // reset on last
|
||||
this.stats.delta += currentValue;
|
||||
}
|
||||
}else{
|
||||
} else {
|
||||
if (previousDeltaUp) {
|
||||
this.stats.delta += currentValue - previousValue; // normal increment
|
||||
} else {
|
||||
|
@ -2,12 +2,6 @@
|
||||
|
||||
import EventEmitter from 'eventemitter3';
|
||||
|
||||
var hasOwnProp = {}.hasOwnProperty;
|
||||
|
||||
function createName(name) {
|
||||
return '$' + name;
|
||||
}
|
||||
|
||||
export class Emitter {
|
||||
emitter: any;
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user