diff --git a/.gitignore b/.gitignore index 0ac42cbcb4b..3aa23b45149 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules +npm-debug.log coverage/ .aws-config.json awsconfig diff --git a/CHANGELOG.md b/CHANGELOG.md index 81b315d6323..8a6f780faf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,18 @@ -# 2.6.0 (unreleased) +# 2.6.0 (2015-12-14) + +### New Features +* **Elasticsearch**: Support for pipeline aggregations Moving average and derivative, closes [#2715](https://github.com/grafana/grafana/issues/2715) +* **Elasticsearch**: Support for inline script and missing options for metrics, closes [#3500](https://github.com/grafana/grafana/issues/3500) +* **Syslog**: Support for syslog logging, closes [#3161](https://github.com/grafana/grafana/pull/3161) +* **Timepicker**: Always show refresh button even with refresh rate, closes [#3498](https://github.com/grafana/grafana/pull/3498) +* **Login**: Make it possible to change the login hint on the login page, closes [#2571](https://github.com/grafana/grafana/pull/2571) + +### Bug Fixes +* **metric editors**: Fix for clicking typeahead auto dropdown option, fixes [#3428](https://github.com/grafana/grafana/issues/3428) +* **influxdb**: Fixed issue showing Group By label only on first query, fixes [#3453](https://github.com/grafana/grafana/issues/3453) +* **logging**: Add more verbose info logging for http reqeusts, closes [#3405](https://github.com/grafana/grafana/pull/3405) + +# 2.6.0-Beta1 (2015-12-04) ### New Table Panel * **table**: New powerful and flexible table panel, closes [#215](https://github.com/grafana/grafana/issues/215) @@ -6,9 +20,9 @@ ### Enhancements * **CloudWatch**: Support for multiple AWS Credentials, closes [#3053](https://github.com/grafana/grafana/issues/3053), [#3080](https://github.com/grafana/grafana/issues/3080) * **Elasticsearch**: Support for dynamic daily indices for annotations, closes [#3061](https://github.com/grafana/grafana/issues/3061) +* **Elasticsearch**: Support for setting min_doc_count for date histogram, closes [#3416](https://github.com/grafana/grafana/issues/3416) * **Graph Panel**: Option to hide series with all zeroes from legend and tooltip, closes [#1381](https://github.com/grafana/grafana/issues/1381), [#3336](https://github.com/grafana/grafana/issues/3336) - ### Bug Fixes * **cloudwatch**: fix for handling of period for long time ranges, fixes [#3086](https://github.com/grafana/grafana/issues/3086) * **dashboard**: fix for collapse row by clicking on row title, fixes [#3065](https://github.com/grafana/grafana/issues/3065) @@ -16,6 +30,9 @@ * **graph**: layout fix for color picker when right side legend was enabled, fixes [#3093](https://github.com/grafana/grafana/issues/3093) * **elasticsearch**: disabling elastic query (via eye) caused error, fixes [#3300](https://github.com/grafana/grafana/issues/3300) +### Breaking changes +* **elasticsearch**: Manual json edited queries are not supported any more (They very barely worked in 2.5) + # 2.5 (2015-10-28) **New Feature: Mix data sources** diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index ee9d78e1470..73c38fffec0 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -20,53 +20,53 @@ }, { "ImportPath": "github.com/aws/aws-sdk-go/aws", - "Comment": "v0.10.4-18-gce51895", - "Rev": "ce51895e994693d65ab997ae48032bf13a9290b7" + "Comment": "v1.0.0", + "Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f" }, { "ImportPath": "github.com/aws/aws-sdk-go/private/endpoints", - "Comment": "v0.10.4-18-gce51895", - "Rev": "ce51895e994693d65ab997ae48032bf13a9290b7" + "Comment": "v1.0.0", + "Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f" }, { "ImportPath": "github.com/aws/aws-sdk-go/private/protocol/ec2query", - "Comment": "v0.10.4-18-gce51895", - "Rev": "ce51895e994693d65ab997ae48032bf13a9290b7" + "Comment": "v1.0.0", + "Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f" }, { "ImportPath": "github.com/aws/aws-sdk-go/private/protocol/query", - "Comment": "v0.10.4-18-gce51895", - "Rev": "ce51895e994693d65ab997ae48032bf13a9290b7" + "Comment": "v1.0.0", + "Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f" }, { "ImportPath": "github.com/aws/aws-sdk-go/private/protocol/rest", - "Comment": "v0.10.4-18-gce51895", - "Rev": "ce51895e994693d65ab997ae48032bf13a9290b7" + "Comment": "v1.0.0", + "Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f" }, { "ImportPath": "github.com/aws/aws-sdk-go/private/protocol/xml/xmlutil", - "Comment": "v0.10.4-18-gce51895", - "Rev": "ce51895e994693d65ab997ae48032bf13a9290b7" + "Comment": "v1.0.0", + "Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f" }, { "ImportPath": "github.com/aws/aws-sdk-go/private/signer/v4", - "Comment": "v0.10.4-18-gce51895", - "Rev": "ce51895e994693d65ab997ae48032bf13a9290b7" + "Comment": "v1.0.0", + "Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f" }, { "ImportPath": "github.com/aws/aws-sdk-go/private/waiter", - "Comment": "v0.10.4-18-gce51895", - "Rev": "ce51895e994693d65ab997ae48032bf13a9290b7" + "Comment": "v1.0.0", + "Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f" }, { "ImportPath": "github.com/aws/aws-sdk-go/service/cloudwatch", - "Comment": "v0.10.4-18-gce51895", - "Rev": "ce51895e994693d65ab997ae48032bf13a9290b7" + "Comment": "v1.0.0", + "Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f" }, { "ImportPath": "github.com/aws/aws-sdk-go/service/ec2", - "Comment": "v0.10.4-18-gce51895", - "Rev": "ce51895e994693d65ab997ae48032bf13a9290b7" + "Comment": "v1.0.0", + "Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f" }, { "ImportPath": "github.com/davecgh/go-spew/spew", diff --git a/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/awsutil/path_value.go b/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/awsutil/path_value.go index 8c0dc2d8d24..4d2a01e8c41 100644 --- a/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/awsutil/path_value.go +++ b/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/awsutil/path_value.go @@ -13,11 +13,11 @@ var indexRe = regexp.MustCompile(`(.+)\[(-?\d+)?\]$`) // rValuesAtPath returns a slice of values found in value v. The values // in v are explored recursively so all nested values are collected. -func rValuesAtPath(v interface{}, path string, create bool, caseSensitive bool) []reflect.Value { +func rValuesAtPath(v interface{}, path string, createPath, caseSensitive, nilTerm bool) []reflect.Value { pathparts := strings.Split(path, "||") if len(pathparts) > 1 { for _, pathpart := range pathparts { - vals := rValuesAtPath(v, pathpart, create, caseSensitive) + vals := rValuesAtPath(v, pathpart, createPath, caseSensitive, nilTerm) if len(vals) > 0 { return vals } @@ -76,7 +76,16 @@ func rValuesAtPath(v interface{}, path string, create bool, caseSensitive bool) return false }) - if create && value.Kind() == reflect.Ptr && value.IsNil() { + if nilTerm && value.Kind() == reflect.Ptr && len(components[1:]) == 0 { + if !value.IsNil() { + value.Set(reflect.Zero(value.Type())) + } + return []reflect.Value{value} + } + + if createPath && value.Kind() == reflect.Ptr && value.IsNil() { + // TODO if the value is the terminus it should not be created + // if the value to be set to its position is nil. value.Set(reflect.New(value.Type().Elem())) value = value.Elem() } else { @@ -84,7 +93,7 @@ func rValuesAtPath(v interface{}, path string, create bool, caseSensitive bool) } if value.Kind() == reflect.Slice || value.Kind() == reflect.Map { - if !create && value.IsNil() { + if !createPath && value.IsNil() { value = reflect.ValueOf(nil) } } @@ -116,7 +125,7 @@ func rValuesAtPath(v interface{}, path string, create bool, caseSensitive bool) // pull out index i := int(*index) if i >= value.Len() { // check out of bounds - if create { + if createPath { // TODO resize slice } else { continue @@ -127,7 +136,7 @@ func rValuesAtPath(v interface{}, path string, create bool, caseSensitive bool) value = reflect.Indirect(value.Index(i)) if value.Kind() == reflect.Slice || value.Kind() == reflect.Map { - if !create && value.IsNil() { + if !createPath && value.IsNil() { value = reflect.ValueOf(nil) } } @@ -176,8 +185,11 @@ func ValuesAtPath(i interface{}, path string) ([]interface{}, error) { // SetValueAtPath sets a value at the case insensitive lexical path inside // of a structure. func SetValueAtPath(i interface{}, path string, v interface{}) { - if rvals := rValuesAtPath(i, path, true, false); rvals != nil { + if rvals := rValuesAtPath(i, path, true, false, v == nil); rvals != nil { for _, rval := range rvals { + if rval.Kind() == reflect.Ptr && rval.IsNil() { + continue + } setValue(rval, v) } } diff --git a/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/awsutil/path_value_test.go b/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/awsutil/path_value_test.go index 378a4864848..b2225566fa3 100644 --- a/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/awsutil/path_value_test.go +++ b/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/awsutil/path_value_test.go @@ -105,4 +105,38 @@ func TestSetValueAtPathSuccess(t *testing.T) { assert.Equal(t, "test0", s2.B.B.C) awsutil.SetValueAtPath(&s2, "A", []Struct{{}}) assert.Equal(t, []Struct{{}}, s2.A) + + str := "foo" + + s3 := Struct{} + awsutil.SetValueAtPath(&s3, "b.b.c", str) + assert.Equal(t, "foo", s3.B.B.C) + + s3 = Struct{B: &Struct{B: &Struct{C: str}}} + awsutil.SetValueAtPath(&s3, "b.b.c", nil) + assert.Equal(t, "", s3.B.B.C) + + s3 = Struct{} + awsutil.SetValueAtPath(&s3, "b.b.c", nil) + assert.Equal(t, "", s3.B.B.C) + + s3 = Struct{} + awsutil.SetValueAtPath(&s3, "b.b.c", &str) + assert.Equal(t, "foo", s3.B.B.C) + + var s4 struct{ Name *string } + awsutil.SetValueAtPath(&s4, "Name", str) + assert.Equal(t, str, *s4.Name) + + s4 = struct{ Name *string }{} + awsutil.SetValueAtPath(&s4, "Name", nil) + assert.Equal(t, (*string)(nil), s4.Name) + + s4 = struct{ Name *string }{Name: &str} + awsutil.SetValueAtPath(&s4, "Name", nil) + assert.Equal(t, (*string)(nil), s4.Name) + + s4 = struct{ Name *string }{} + awsutil.SetValueAtPath(&s4, "Name", &str) + assert.Equal(t, str, *s4.Name) } diff --git a/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/client/client.go b/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/client/client.go index 63ab805b9c4..e639ce040d7 100644 --- a/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/client/client.go +++ b/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/client/client.go @@ -41,11 +41,20 @@ func New(cfg aws.Config, info metadata.ClientInfo, handlers request.Handlers, op Handlers: handlers, } - maxRetries := aws.IntValue(cfg.MaxRetries) - if cfg.MaxRetries == nil || maxRetries == aws.UseServiceDefaultRetries { - maxRetries = 3 + switch retryer, ok := cfg.Retryer.(request.Retryer); { + case ok: + svc.Retryer = retryer + case cfg.Retryer != nil && cfg.Logger != nil: + s := fmt.Sprintf("WARNING: %T does not implement request.Retryer; using DefaultRetryer instead", cfg.Retryer) + cfg.Logger.Log(s) + fallthrough + default: + maxRetries := aws.IntValue(cfg.MaxRetries) + if cfg.MaxRetries == nil || maxRetries == aws.UseServiceDefaultRetries { + maxRetries = 3 + } + svc.Retryer = DefaultRetryer{NumMaxRetries: maxRetries} } - svc.Retryer = DefaultRetryer{NumMaxRetries: maxRetries} svc.AddDebugHandlers() diff --git a/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/config.go b/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/config.go index f157f8b3b5a..75fcc8284b9 100644 --- a/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/config.go +++ b/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/config.go @@ -12,6 +12,9 @@ import ( // is nil also. const UseServiceDefaultRetries = -1 +// RequestRetryer is an alias for a type that implements the request.Retryer interface. +type RequestRetryer interface{} + // A Config provides service configuration for service clients. By default, // all clients will use the {defaults.DefaultConfig} structure. type Config struct { @@ -59,6 +62,21 @@ type Config struct { // configuration. MaxRetries *int + // Retryer guides how HTTP requests should be retried in case of recoverable failures. + // + // When nil or the value does not implement the request.Retryer interface, + // the request.DefaultRetryer will be used. + // + // When both Retryer and MaxRetries are non-nil, the former is used and + // the latter ignored. + // + // To set the Retryer field in a type-safe manner and with chaining, use + // the request.WithRetryer helper function: + // + // cfg := request.WithRetryer(aws.NewConfig(), myRetryer) + // + Retryer RequestRetryer + // Disables semantic parameter validation, which validates input for missing // required fields and/or other semantic request input errors. DisableParamValidation *bool @@ -217,6 +235,10 @@ func mergeInConfig(dst *Config, other *Config) { dst.MaxRetries = other.MaxRetries } + if other.Retryer != nil { + dst.Retryer = other.Retryer + } + if other.DisableParamValidation != nil { dst.DisableParamValidation = other.DisableParamValidation } diff --git a/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/request/request_pagination.go b/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/request/request_pagination.go index b19fbd70b4a..2939ec473f2 100644 --- a/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/request/request_pagination.go +++ b/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/request/request_pagination.go @@ -44,12 +44,19 @@ func (r *Request) nextPageTokens() []interface{} { } tokens := []interface{}{} + tokenAdded := false for _, outToken := range r.Operation.OutputTokens { v, _ := awsutil.ValuesAtPath(r.Data, outToken) if len(v) > 0 { tokens = append(tokens, v[0]) + tokenAdded = true + } else { + tokens = append(tokens, nil) } } + if !tokenAdded { + return nil + } return tokens } @@ -85,9 +92,10 @@ func (r *Request) NextPage() *Request { // return true to keep iterating or false to stop. func (r *Request) EachPage(fn func(data interface{}, isLastPage bool) (shouldContinue bool)) error { for page := r; page != nil; page = page.NextPage() { - page.Send() - shouldContinue := fn(page.Data, !page.HasNextPage()) - if page.Error != nil || !shouldContinue { + if err := page.Send(); err != nil { + return err + } + if getNextPage := fn(page.Data, !page.HasNextPage()); !getNextPage { return page.Error } } diff --git a/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/request/request_pagination_test.go b/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/request/request_pagination_test.go index a9863eaf97f..95a8d99fbe6 100644 --- a/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/request/request_pagination_test.go +++ b/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/request/request_pagination_test.go @@ -9,6 +9,7 @@ import ( "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/awstesting/unit" "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/aws/aws-sdk-go/service/route53" "github.com/aws/aws-sdk-go/service/s3" ) @@ -314,7 +315,69 @@ func TestPaginationTruncation(t *testing.T) { assert.Equal(t, []string{"Key1", "Key2"}, results) assert.Nil(t, err) +} +func TestPaginationNilToken(t *testing.T) { + client := route53.New(unit.Session) + + reqNum := 0 + resps := []*route53.ListResourceRecordSetsOutput{ + { + ResourceRecordSets: []*route53.ResourceRecordSet{ + {Name: aws.String("first.example.com.")}, + }, + IsTruncated: aws.Bool(true), + NextRecordName: aws.String("second.example.com."), + NextRecordType: aws.String("MX"), + NextRecordIdentifier: aws.String("second"), + MaxItems: aws.String("1"), + }, + { + ResourceRecordSets: []*route53.ResourceRecordSet{ + {Name: aws.String("second.example.com.")}, + }, + IsTruncated: aws.Bool(true), + NextRecordName: aws.String("third.example.com."), + NextRecordType: aws.String("MX"), + MaxItems: aws.String("1"), + }, + { + ResourceRecordSets: []*route53.ResourceRecordSet{ + {Name: aws.String("third.example.com.")}, + }, + IsTruncated: aws.Bool(false), + MaxItems: aws.String("1"), + }, + } + client.Handlers.Send.Clear() // mock sending + client.Handlers.Unmarshal.Clear() + client.Handlers.UnmarshalMeta.Clear() + client.Handlers.ValidateResponse.Clear() + + idents := []string{} + client.Handlers.Build.PushBack(func(r *request.Request) { + p := r.Params.(*route53.ListResourceRecordSetsInput) + idents = append(idents, aws.StringValue(p.StartRecordIdentifier)) + + }) + client.Handlers.Unmarshal.PushBack(func(r *request.Request) { + r.Data = resps[reqNum] + reqNum++ + }) + + params := &route53.ListResourceRecordSetsInput{ + HostedZoneId: aws.String("id-zone"), + } + + results := []string{} + err := client.ListResourceRecordSetsPages(params, func(p *route53.ListResourceRecordSetsOutput, last bool) bool { + results = append(results, *p.ResourceRecordSets[0].Name) + return true + }) + + assert.NoError(t, err) + assert.Equal(t, []string{"", "second", ""}, idents) + assert.Equal(t, []string{"first.example.com.", "second.example.com.", "third.example.com."}, results) } // Benchmarks diff --git a/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/request/retryer.go b/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/request/retryer.go index b06143b8358..ab6fff5ac84 100644 --- a/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/request/retryer.go +++ b/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/request/retryer.go @@ -3,6 +3,7 @@ package request import ( "time" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" ) @@ -15,6 +16,13 @@ type Retryer interface { MaxRetries() int } +// WithRetryer sets a config Retryer value to the given Config returning it +// for chaining. +func WithRetryer(cfg *aws.Config, retryer Retryer) *aws.Config { + cfg.Retryer = retryer + return cfg +} + // retryableCodes is a collection of service response codes which are retry-able // without any further action. var retryableCodes = map[string]struct{}{ diff --git a/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/version.go b/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/version.go index f4741adfb44..eec3baa1f05 100644 --- a/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/version.go +++ b/Godeps/_workspace/src/github.com/aws/aws-sdk-go/aws/version.go @@ -5,4 +5,4 @@ package aws const SDKName = "aws-sdk-go" // SDKVersion is the version of this SDK -const SDKVersion = "0.10.4" +const SDKVersion = "1.0.0" diff --git a/Godeps/_workspace/src/github.com/aws/aws-sdk-go/private/waiter/waiter.go b/Godeps/_workspace/src/github.com/aws/aws-sdk-go/private/waiter/waiter.go index 335af91dc61..e5fb13666ab 100644 --- a/Godeps/_workspace/src/github.com/aws/aws-sdk-go/private/waiter/waiter.go +++ b/Godeps/_workspace/src/github.com/aws/aws-sdk-go/private/waiter/waiter.go @@ -5,6 +5,7 @@ import ( "reflect" "time" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/awsutil" "github.com/aws/aws-sdk-go/aws/request" @@ -47,52 +48,74 @@ func (w *Waiter) Wait() error { res := method.Call([]reflect.Value{in}) req := res[0].Interface().(*request.Request) req.Handlers.Build.PushBack(request.MakeAddToUserAgentFreeFormHandler("Waiter")) - if err := req.Send(); err != nil { - return err - } + err := req.Send() for _, a := range w.Acceptors { + if err != nil && a.Matcher != "error" { + // Only matcher error is valid if there is a request error + continue + } + result := false + var vals []interface{} switch a.Matcher { - case "pathAll": - if vals, _ := awsutil.ValuesAtPath(req.Data, a.Argument); req.Error == nil && vals != nil { - result = true - for _, val := range vals { - if !awsutil.DeepEqual(val, a.Expected) { - result = false - break - } + case "pathAll", "path": + // Require all matches to be equal for result to match + vals, _ = awsutil.ValuesAtPath(req.Data, a.Argument) + result = true + for _, val := range vals { + if !awsutil.DeepEqual(val, a.Expected) { + result = false + break } } case "pathAny": - if vals, _ := awsutil.ValuesAtPath(req.Data, a.Argument); req.Error == nil && vals != nil { - for _, val := range vals { - if awsutil.DeepEqual(val, a.Expected) { - result = true - break - } + // Only a single match needs to equal for the result to match + vals, _ = awsutil.ValuesAtPath(req.Data, a.Argument) + for _, val := range vals { + if awsutil.DeepEqual(val, a.Expected) { + result = true + break } } case "status": s := a.Expected.(int) result = s == req.HTTPResponse.StatusCode + case "error": + if aerr, ok := err.(awserr.Error); ok { + result = aerr.Code() == a.Expected.(string) + } + case "pathList": + // ignored matcher + default: + logf(client, "WARNING: Waiter for %s encountered unexpected matcher: %s", + w.Config.Operation, a.Matcher) } - if result { - switch a.State { - case "success": - return nil // waiter completed - case "failure": - if req.Error == nil { - return awserr.New("ResourceNotReady", - fmt.Sprintf("failed waiting for successful resource state"), nil) - } - return req.Error // waiter failed - case "retry": - // do nothing, just retry - } - break + if !result { + // If there was no matching result found there is nothing more to do + // for this response, retry the request. + continue } + + switch a.State { + case "success": + // waiter completed + return nil + case "failure": + // Waiter failure state triggered + return awserr.New("ResourceNotReady", + fmt.Sprintf("failed waiting for successful resource state"), err) + case "retry": + // clear the error and retry the operation + err = nil + default: + logf(client, "WARNING: Waiter for %s encountered unexpected state: %s", + w.Config.Operation, a.State) + } + } + if err != nil { + return err } time.Sleep(time.Second * time.Duration(w.Delay)) @@ -101,3 +124,13 @@ func (w *Waiter) Wait() error { return awserr.New("ResourceNotReady", fmt.Sprintf("exceeded %d wait attempts", w.MaxAttempts), nil) } + +func logf(client reflect.Value, msg string, args ...interface{}) { + cfgVal := client.FieldByName("Config") + if !cfgVal.IsValid() { + return + } + if cfg, ok := cfgVal.Interface().(*aws.Config); ok && cfg.Logger != nil { + cfg.Logger.Log(fmt.Sprintf(msg, args...)) + } +} diff --git a/Godeps/_workspace/src/github.com/aws/aws-sdk-go/private/waiter/waiter_test.go b/Godeps/_workspace/src/github.com/aws/aws-sdk-go/private/waiter/waiter_test.go index 58267295b11..d545f35370f 100644 --- a/Godeps/_workspace/src/github.com/aws/aws-sdk-go/private/waiter/waiter_test.go +++ b/Godeps/_workspace/src/github.com/aws/aws-sdk-go/private/waiter/waiter_test.go @@ -1,6 +1,9 @@ package waiter_test import ( + "bytes" + "io/ioutil" + "net/http" "testing" "github.com/stretchr/testify/assert" @@ -41,22 +44,7 @@ func (c *mockClient) MockRequest(input *MockInput) (*request.Request, *MockOutpu return req, output } -var mockAcceptors = []waiter.WaitAcceptor{ - { - State: "success", - Matcher: "pathAll", - Argument: "States[].State", - Expected: "running", - }, - { - State: "failure", - Matcher: "pathAny", - Argument: "States[].State", - Expected: "stopping", - }, -} - -func TestWaiter(t *testing.T) { +func TestWaiterPathAll(t *testing.T) { svc := &mockClient{Client: awstesting.NewClient(&aws.Config{ Region: aws.String("mock-region"), })} @@ -73,13 +61,13 @@ func TestWaiter(t *testing.T) { {State: aws.String("pending")}, }, }, - { // Request 1 + { // Request 2 States: []*MockState{ {State: aws.String("running")}, {State: aws.String("pending")}, }, }, - { // Request 1 + { // Request 3 States: []*MockState{ {State: aws.String("running")}, {State: aws.String("running")}, @@ -104,7 +92,83 @@ func TestWaiter(t *testing.T) { Operation: "Mock", Delay: 0, MaxAttempts: 10, - Acceptors: mockAcceptors, + Acceptors: []waiter.WaitAcceptor{ + { + State: "success", + Matcher: "pathAll", + Argument: "States[].State", + Expected: "running", + }, + }, + } + w := waiter.Waiter{ + Client: svc, + Input: &MockInput{}, + Config: waiterCfg, + } + + err := w.Wait() + assert.NoError(t, err) + assert.Equal(t, 3, numBuiltReq) + assert.Equal(t, 3, reqNum) +} + +func TestWaiterPath(t *testing.T) { + svc := &mockClient{Client: awstesting.NewClient(&aws.Config{ + Region: aws.String("mock-region"), + })} + svc.Handlers.Send.Clear() // mock sending + svc.Handlers.Unmarshal.Clear() + svc.Handlers.UnmarshalMeta.Clear() + svc.Handlers.ValidateResponse.Clear() + + reqNum := 0 + resps := []*MockOutput{ + { // Request 1 + States: []*MockState{ + {State: aws.String("pending")}, + {State: aws.String("pending")}, + }, + }, + { // Request 2 + States: []*MockState{ + {State: aws.String("running")}, + {State: aws.String("pending")}, + }, + }, + { // Request 3 + States: []*MockState{ + {State: aws.String("running")}, + {State: aws.String("running")}, + }, + }, + } + + numBuiltReq := 0 + svc.Handlers.Build.PushBack(func(r *request.Request) { + numBuiltReq++ + }) + svc.Handlers.Unmarshal.PushBack(func(r *request.Request) { + if reqNum >= len(resps) { + assert.Fail(t, "too many polling requests made") + return + } + r.Data = resps[reqNum] + reqNum++ + }) + + waiterCfg := waiter.Config{ + Operation: "Mock", + Delay: 0, + MaxAttempts: 10, + Acceptors: []waiter.WaitAcceptor{ + { + State: "success", + Matcher: "path", + Argument: "States[].State", + Expected: "running", + }, + }, } w := waiter.Waiter{ Client: svc, @@ -135,13 +199,13 @@ func TestWaiterFailure(t *testing.T) { {State: aws.String("pending")}, }, }, - { // Request 1 + { // Request 2 States: []*MockState{ {State: aws.String("running")}, {State: aws.String("pending")}, }, }, - { // Request 1 + { // Request 3 States: []*MockState{ {State: aws.String("running")}, {State: aws.String("stopping")}, @@ -166,7 +230,20 @@ func TestWaiterFailure(t *testing.T) { Operation: "Mock", Delay: 0, MaxAttempts: 10, - Acceptors: mockAcceptors, + Acceptors: []waiter.WaitAcceptor{ + { + State: "success", + Matcher: "pathAll", + Argument: "States[].State", + Expected: "running", + }, + { + State: "failure", + Matcher: "pathAny", + Argument: "States[].State", + Expected: "stopping", + }, + }, } w := waiter.Waiter{ Client: svc, @@ -181,3 +258,134 @@ func TestWaiterFailure(t *testing.T) { assert.Equal(t, 3, numBuiltReq) assert.Equal(t, 3, reqNum) } + +func TestWaiterError(t *testing.T) { + svc := &mockClient{Client: awstesting.NewClient(&aws.Config{ + Region: aws.String("mock-region"), + })} + svc.Handlers.Send.Clear() // mock sending + svc.Handlers.Unmarshal.Clear() + svc.Handlers.UnmarshalMeta.Clear() + svc.Handlers.ValidateResponse.Clear() + + reqNum := 0 + resps := []*MockOutput{ + { // Request 1 + States: []*MockState{ + {State: aws.String("pending")}, + {State: aws.String("pending")}, + }, + }, + { // Request 2, error case + }, + { // Request 3 + States: []*MockState{ + {State: aws.String("running")}, + {State: aws.String("running")}, + }, + }, + } + + numBuiltReq := 0 + svc.Handlers.Build.PushBack(func(r *request.Request) { + numBuiltReq++ + }) + svc.Handlers.Send.PushBack(func(r *request.Request) { + if reqNum == 1 { + r.Error = awserr.New("MockException", "mock exception message", nil) + r.HTTPResponse = &http.Response{ + StatusCode: 400, + Status: http.StatusText(400), + Body: ioutil.NopCloser(bytes.NewReader([]byte{})), + } + reqNum++ + } + }) + svc.Handlers.Unmarshal.PushBack(func(r *request.Request) { + if reqNum >= len(resps) { + assert.Fail(t, "too many polling requests made") + return + } + r.Data = resps[reqNum] + reqNum++ + }) + + waiterCfg := waiter.Config{ + Operation: "Mock", + Delay: 0, + MaxAttempts: 10, + Acceptors: []waiter.WaitAcceptor{ + { + State: "success", + Matcher: "pathAll", + Argument: "States[].State", + Expected: "running", + }, + { + State: "retry", + Matcher: "error", + Argument: "", + Expected: "MockException", + }, + }, + } + w := waiter.Waiter{ + Client: svc, + Input: &MockInput{}, + Config: waiterCfg, + } + + err := w.Wait() + assert.NoError(t, err) + assert.Equal(t, 3, numBuiltReq) + assert.Equal(t, 3, reqNum) +} + +func TestWaiterStatus(t *testing.T) { + svc := &mockClient{Client: awstesting.NewClient(&aws.Config{ + Region: aws.String("mock-region"), + })} + svc.Handlers.Send.Clear() // mock sending + svc.Handlers.Unmarshal.Clear() + svc.Handlers.UnmarshalMeta.Clear() + svc.Handlers.ValidateResponse.Clear() + + reqNum := 0 + svc.Handlers.Build.PushBack(func(r *request.Request) { + reqNum++ + }) + svc.Handlers.Send.PushBack(func(r *request.Request) { + code := 200 + if reqNum == 3 { + code = 404 + } + r.HTTPResponse = &http.Response{ + StatusCode: code, + Status: http.StatusText(code), + Body: ioutil.NopCloser(bytes.NewReader([]byte{})), + } + }) + + waiterCfg := waiter.Config{ + Operation: "Mock", + Delay: 0, + MaxAttempts: 10, + Acceptors: []waiter.WaitAcceptor{ + { + State: "success", + Matcher: "status", + Argument: "", + Expected: 404, + }, + }, + } + w := waiter.Waiter{ + Client: svc, + Input: &MockInput{}, + Config: waiterCfg, + } + + err := w.Wait() + assert.NoError(t, err) + assert.Equal(t, 3, reqNum) +} diff --git a/README.md b/README.md index 99854966938..3886c32a971 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ Replace X.Y.Z by actual version number. cd $GOPATH/src/github.com/grafana/grafana go run build.go setup (only needed once to install godep) godep restore (will pull down all golang lib dependencies in your current GOPATH) -godep go run build.go build +go run build.go build ``` ### Building frontend assets diff --git a/appveyor.yml b/appveyor.yml index 993f8f2882c..83506312912 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,7 +5,7 @@ os: Windows Server 2012 R2 clone_folder: c:\gopath\src\github.com\grafana\grafana environment: - nodejs_version: "0.12.2" + nodejs_version: "4" GOPATH: c:\gopath install: diff --git a/build.go b/build.go index 4a345c5fc73..fcd7289cd6a 100644 --- a/build.go +++ b/build.go @@ -76,6 +76,14 @@ func main() { grunt("release") createLinuxPackages() + case "pkg-rpm": + grunt("release") + createRpmPackages() + + case "pkg-deb": + grunt("release") + createDebPackages() + case "latest": makeLatestDistCopies() @@ -147,7 +155,7 @@ type linuxPackageOptions struct { depends []string } -func createLinuxPackages() { +func createDebPackages() { createPackage(linuxPackageOptions{ packageType: "deb", homeDir: "/usr/share/grafana", @@ -167,7 +175,9 @@ func createLinuxPackages() { depends: []string{"adduser", "libfontconfig"}, }) +} +func createRpmPackages() { createPackage(linuxPackageOptions{ packageType: "rpm", homeDir: "/usr/share/grafana", @@ -189,6 +199,11 @@ func createLinuxPackages() { }) } +func createLinuxPackages() { + createDebPackages() + createRpmPackages() +} + func createPackage(options linuxPackageOptions) { packageRoot, _ := ioutil.TempDir("", "grafana-linux-pack") @@ -315,6 +330,8 @@ func build(pkg string, tags []string) { args = append(args, "-o", binary) args = append(args, pkg) setBuildEnv() + + runPrint("go", "version") runPrint("go", args...) // Create an md5 checksum of the binary, to be included in the archive for diff --git a/conf/defaults.ini b/conf/defaults.ini index 8b2b34f3fbc..3350954a150 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -142,6 +142,9 @@ auto_assign_org_role = Viewer # Require email validation before sign up completes verify_email_enabled = false +# Background text for the user field on the login page +login_hint = email or username + #################################### Anonymous Auth ########################## [auth.anonymous] # enable anonymous access @@ -245,6 +248,18 @@ daily_rotate = true # Expired days of log file(delete after max days), default is 7 max_days = 7 +[log.syslog] +level = +# Syslog network type and address. This can be udp, tcp, or unix. If left blank, the default unix endpoints will be used. +network = +address = + +# Syslog facility. user, daemon and local0 through local7 are valid. +facility = + +# Syslog tag. By default, the process' argv[0] is used. +tag = + #################################### AMPQ Event Publisher ########################## [event_publisher] enabled = false diff --git a/conf/sample.ini b/conf/sample.ini index e8550c66c1a..df092cda5ed 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -134,6 +134,9 @@ # Default role new users will be automatically assigned (if disabled above is set to true) ;auto_assign_org_role = Viewer +# Background text for the user field on the login page +;login_hint = email or username + #################################### Anonymous Auth ########################## [auth.anonymous] # enable anonymous access diff --git a/docs/VERSION b/docs/VERSION index 437459cd94c..e70b4523ae7 100644 --- a/docs/VERSION +++ b/docs/VERSION @@ -1 +1 @@ -2.5.0 +2.6.0 diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index f965e34c5bd..7bca7e2f01f 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -45,6 +45,7 @@ pages: - ['guides/basic_concepts.md', 'User Guides', 'Basic Concepts'] - ['guides/gettingstarted.md', 'User Guides', 'Getting Started'] +- ['guides/whats-new-in-v2-6.md', 'User Guides', "What's New in Grafana v2.6"] - ['guides/whats-new-in-v2-5.md', 'User Guides', "What's New in Grafana v2.5"] - ['guides/whats-new-in-v2-1.md', 'User Guides', "What's New in Grafana v2.1"] - ['guides/whats-new-in-v2.md', 'User Guides', "What's New in Grafana v2.0"] @@ -52,13 +53,14 @@ pages: - ['reference/graph.md', 'Reference', 'Graph Panel'] - ['reference/singlestat.md', 'Reference', 'Singlestat Panel'] +- ['reference/table_panel.md', 'Reference', 'Table Panel'] - ['reference/dashlist.md', 'Reference', 'Dashboard List Panel'] - ['reference/sharing.md', 'Reference', 'Sharing'] - ['reference/annotations.md', 'Reference', 'Annotations'] -- ['reference/timerange.md', 'Reference', 'Time Range Controls'] -- ['reference/search.md', 'Reference', 'Dashboard Search'] -- ['reference/templating.md', 'Reference', 'Templated Dashboards'] -- ['reference/scripting.md', 'Reference', 'Scripted Dashboards'] +- ['reference/timerange.md', 'Reference', 'Time Range'] +- ['reference/search.md', 'Reference', 'Search'] +- ['reference/templating.md', 'Reference', 'Templating'] +- ['reference/scripting.md', 'Reference', 'Scripting'] - ['reference/playlist.md', 'Reference', 'Playlist'] - ['reference/export_import.md', 'Reference', 'Import & Export'] - ['reference/admin.md', 'Reference', 'Administration'] diff --git a/docs/sources/datasources/cloudwatch.md b/docs/sources/datasources/cloudwatch.md index 012da9d843a..4d87c4a0743 100644 --- a/docs/sources/datasources/cloudwatch.md +++ b/docs/sources/datasources/cloudwatch.md @@ -63,15 +63,10 @@ Name | Description `namespaces()` | Returns a list of namespaces CloudWatch support. `metrics(namespace)` | Returns a list of metrics in the namespace. `dimension_keys(namespace)` | Returns a list of dimension keys in the namespace. -`dimension_values(region, namespace, metric)` | Returns a list of dimension values matching the specified `region`, `namespace` and `metric`. +`dimension_values(region, namespace, metric, dimension_key)` | Returns a list of dimension values matching the specified `region`, `namespace`, `metric` and `dimension_key`. For details about the metrics CloudWatch provides, please refer to the [CloudWatch documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/CW_Support_For_AWS.html). -If you want to filter dimension values by other dimension key/value pair, you can specify optional parameter like this. -```sql -dimension_values(region, namespace, metric, dim_key1=dim_val1,dim_key2=dim_val2,...) -``` - ![](/img/v2/cloudwatch_templating.png) ## Cost diff --git a/docs/sources/datasources/elasticsearch.md b/docs/sources/datasources/elasticsearch.md index 76b0040ecd3..de9ada2bf58 100644 --- a/docs/sources/datasources/elasticsearch.md +++ b/docs/sources/datasources/elasticsearch.md @@ -53,6 +53,35 @@ a time pattern for the index name or a wildcard. The Elasticsearch query editor allows you to select multiple metrics and group by multiple terms or filters. Use the plus and minus icons to the right to add / remove metrics or group bys. Some metrics and group by have options, click the option text to expand the the row to view and edit metric or group by options. +## Pipeline metrics + +If you have Elasticsearch 2.x and Grafana 2.6 or above then you can use pipeline metric aggregations like +**Moving Average** and **Derivative**. Elasticsearch pipeline metrics require another metric to be based on. Use the eye icon next to the metric +to hide metrics from appearing in the graph. This is useful for metrics you only have in the query to be used +in a pipeline metric. + +![](/img/elasticsearch/pipeline_metrics_editor.png) + +## Templating + +The Elasticsearch datasource supports two types of queries you can use to fill template variables with values. + +### Possible values for a field + +```json +{"find": "terms", "field": "@hostname"} +``` + +### Fields filtered by type +```json +{"find": "fields", "type": "string"} +``` + +### Multi format / All format +Use lucene format. + + + ## Annotations TODO diff --git a/docs/sources/datasources/influxdb.md b/docs/sources/datasources/influxdb.md index e57d5a63fbc..da8ca08f549 100644 --- a/docs/sources/datasources/influxdb.md +++ b/docs/sources/datasources/influxdb.md @@ -38,29 +38,47 @@ Password | Database user's password > Direct access is still supported because in some cases it may be useful to access a Data Source directly depending on the use case and topology of Grafana, the user, and the Data Source. -## InfluxDB 0.9.x +## Query Editor -![](/img/influxdb/InfluxDB_09_editor.png) +![](/img/influxdb/editor_v3.png) You find the InfluxDB editor in the metrics tab in Graph or Singlestat panel's edit mode. You enter edit mode by clicking the panel title, then edit. The editor allows you to select metrics and tags. -### Editor tag filters +### Filter data (WHERE) To add a tag filter click the plus icon to the right of the `WHERE` condition. You can remove tag filters by clicking on the tag key and select `--remove tag filter--`. -### Regex matching +**Regex matching** + You can type in regex patterns for metric names or tag filter values, be sure to wrap the regex pattern in forward slashes (`/`). Grafana will automatically adjust the filter tag condition to use the InfluxDB regex match condition operator (`=~`). -### Editor group by -To group by a tag click the plus icon after the `GROUP BY ($interval)` text. Pick a tag from the dropdown that appears. -You can remove the group by by clicking on the tag and then select `--remove group by--` from the dropdown. +### Field & Aggregation functions +In the `SELECT` row you can specify what fields and functions you want to use. If you have a +group by time you need an aggregation function. Some functions like derivative require an aggregation function. -### Editor RAW Query -You can switch to raw query mode by pressing the pen icon. +The editor tries simplify and unify this part of the query. For example: +![](/img/influxdb/select_editor.png) -> If you use Raw Query be sure your query at minimum have `WHERE $timeFilter` clause and ends with `order by asc`. +The above will generate the following InfluxDB `SELECT` clause: + +```sql +SELECT derivative(mean("value"), 10s) /10 AS "REQ/s" FROM .... +``` + +#### Select multiple fields +Use the plus button and select Field > field to add another SELECT clause. You can also +specify an asterix `*` to select all fields. + +### Group By +To group by a tag click the plus icon at the end of the GROUP BY row. Pick a tag from the dropdown that appears. +You can remove the group by by clicking on the `tag` and then click on the x icon. + +### Text Editor Mode (RAW) +You can switch to raw query mode by clicking hamburger icon and then `Switch editor mode`. + +> If you use Raw Query be sure your query at minimum have `WHERE $timeFilter` > Also please always have a group by time and an aggregation function, otherwise InfluxDB can easily return hundreds of thousands > of data points that will hang the browser. @@ -72,7 +90,15 @@ You can switch to raw query mode by pressing the pen icon. - $tag_hostname = replaced with the value of the hostname tag - You can also use [[tag_hostname]] pattern replacement syntax -### Templating +### Table query / raw data + +![](/img/influxdb/raw_data.png) + +You can remove the group by time by clicking on the `time` part and then the `x` icon. You can +change the option `Format As` to `Table` if you want to show raw data in the `Table` panel. + + +## Templating You can create a template variable in Grafana and have that variable filled with values from any InfluxDB metric exploration query. You can then use this variable in your InfluxDB metric queries. @@ -93,7 +119,7 @@ SHOW TAG VALUES WITH KEY = "hostname" WHERE region =~ /$region/ ![](/img/influxdb/templating_simple_ex1.png) -### Annotations +## Annotations Annotations allows you to overlay rich event information on top of graphs. An example query: @@ -102,10 +128,4 @@ An example query: SELECT title, description from events WHERE $timeFilter order asc ``` -### InfluxDB 0.8.x - -![](/img/v1/influxdb_editor.png) - - - diff --git a/docs/sources/guides/whats-new-in-v2-6.md b/docs/sources/guides/whats-new-in-v2-6.md new file mode 100644 index 00000000000..de1ac6d488b --- /dev/null +++ b/docs/sources/guides/whats-new-in-v2-6.md @@ -0,0 +1,124 @@ +--- +page_title: What's New in Grafana v2.6 +page_description: What's new in Grafana v2.6 +page_keywords: grafana, new, changes, features, documentation, table +--- + +# What's new in Grafana v2.6 + +## Release highlights +The release includes a new Table panel, a new InfluxDB query editor, support for Elasticsearch Pipeline Metrics and +support for multiple Cloudwatch credentials. + +## Table Panel + + +The new table panel is very flexible, supporting both multiple modes for time series as well as for +table, annotation and raw JSON data. It also provides date formating and value formating and coloring options. + +### Time series to rows + +In the most simple mode you can turn time series to rows. This means you get a `Time`, `Metric` and a `Value` column. +Where `Metric` is the name of the time series. + + + +### Table Transform +Above you see the options tab for the **Table Panel**. The most important option is the `To Table Transform`. +This option controls how the result of the metric/data query is turned into a table. + +### Column Styles +The column styles allow you control how dates and numbers are formatted. + +### Time series to columns +This transform allows you to take multiple time series and group them by time. Which will result in a `Time` column +and a column for each time series. + + + +In the screenshot above you can see how the same time series query as in the previous example can be transformed into +a different table by changing the `To Table Transform` to `Time series to columns`. + +### Time series to aggregations +This transform works very similar to the legend values in the Graph panel. Each series gets its own row. In the Options +tab you can select which aggregations you want using the plus button the Columns section. + + + +You have to think about how accurate the aggregations will be. It depends on what aggregation is used in the time series query, +how many data points are fetched, etc. The time series aggregations are calculated by Grafana after aggregation is performed +by the time series database. + +### Raw logs queries + +If you want to show documents from Elasticsearch pick `Raw Document` as the first metric. + + + +This in combination with the `JSON Data` table transform will allow you to pick which fields in the document +you want to show in the table. + + + +### Elasticsearch aggregations + +You can also make Elasticsearch aggregation queries without a `Date Histogram`. This allows you to +use Elasticsearch metric aggregations to get accurate aggregations for the selected time range. + + + +### Annotations + +The table can also show any annotations you have enabled in the dashboard. + + + +## The New InfluxDB Editor +The new InfluxDB editor is a lot more flexible and powerful. It supports nested functions, like `derivative`. +It also uses the same technique as the Graphite query editor in that it presents nested functions as chain of function +transformations. It tries to simplify and unify the complicated nature of InfluxDB's query language. + + + +In the `SELECT` row you can specify what fields and functions you want to use. If you have a +group by time you need an aggregation function. Some functions like derivative require an aggregation function. + +The editor tries simplify and unify this part of the query. For example: +![](/img/influxdb/select_editor.png) + +The above will generate the following InfluxDB `SELECT` clause: + +```sql +SELECT derivative(mean("value"), 10s) /10 AS "REQ/s" FROM .... +``` + +### Select multiple fields +Use the plus button and select Field > field to add another SELECT clause. You can also +specify an asterix `*` to select all fields. + +### Group By +To group by a tag click the plus icon at the end of the GROUP BY row. Pick a tag from the dropdown that appears. +You can remove the group by by clicking on the `tag` and then click on the x icon. + +The new editor also allows you to remove group by time and select `raw` table data. Which is very useful +in combination with the new Table panel to show raw log data stored in InfluxDB. + + + +## Pipeline metrics + +If you have Elasticsearch 2.x and Grafana 2.6 or above then you can use pipeline metric aggregations like +**Moving Average** and **Derivative**. Elasticsearch pipeline metrics require another metric to be based on. Use the eye icon next to the metric +to hide metrics from appearing in the graph. This is useful for metrics you only have in the query to be used +in a pipeline metric. + +![](/img/elasticsearch/pipeline_metrics_editor.png) + +## Changelog +For a detailed list and link to github issues for everything included in the 2.6 release please +view the [CHANGELOG.md](https://github.com/grafana/grafana/blob/master/CHANGELOG.md) file. + +- - - + +Download Grafana 2.6 now + diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index 4a734ac4400..ec6e30b35f1 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -10,13 +10,13 @@ page_keywords: grafana, installation, debian, ubuntu, guide Description | Download ------------ | ------------- -.deb for Debian-based Linux | [grafana_2.5.0_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_2.5.0_amd64.deb) +.deb for Debian-based Linux | [grafana_2.6.0_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_2.6.0_amd64.deb) ## Install - $ wget https://grafanarel.s3.amazonaws.com/builds/grafana_2.5.0_amd64.deb + $ wget https://grafanarel.s3.amazonaws.com/builds/grafana_2.6.0_amd64.deb $ sudo apt-get install -y adduser libfontconfig - $ sudo dpkg -i grafana_2.5.0_amd64.deb + $ sudo dpkg -i grafana_2.6.0_amd64.deb ## APT Repository diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index 464faa09b9d..ec4f648e44b 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -10,24 +10,24 @@ page_keywords: grafana, installation, centos, fedora, opensuse, redhat, guide Description | Download ------------ | ------------- -.RPM for CentOS / Fedora / OpenSuse / Redhat Linux | [grafana-2.5.0-1.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-2.5.0-1.x86_64.rpm) +.RPM for CentOS / Fedora / OpenSuse / Redhat Linux | [grafana-2.6.0-1.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-2.6.0-1.x86_64.rpm) ## Install from package file You can install Grafana using Yum directly. - $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-2.5.0-1.x86_64.rpm + $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-2.6.0-1.x86_64.rpm Or install manually using `rpm`. #### On CentOS / Fedora / Redhat: $ sudo yum install initscripts fontconfig - $ sudo rpm -Uvh grafana-2.5.0-1.x86_64.rpm + $ sudo rpm -Uvh grafana-2.6.0-1.x86_64.rpm #### On OpenSuse: - $ sudo rpm -i --nodeps grafana-2.5.0-1.x86_64.rpm + $ sudo rpm -i --nodeps grafana-2.6.0-1.x86_64.rpm ## Install via YUM Repository diff --git a/docs/sources/reference/table_panel.md b/docs/sources/reference/table_panel.md new file mode 100644 index 00000000000..381d16186a9 --- /dev/null +++ b/docs/sources/reference/table_panel.md @@ -0,0 +1,86 @@ +---- +page_title: Table Panel +page_description: Table Panel Reference +page_keywords: grafana, table, panel, documentation +--- + +# Table Panel + + + +The new table panel is very flexible, supporting both multiple modes for time series as well as for +table, annotation and raw JSON data. It also provides date formating and value formating and coloring options. + +To view table panels in action and test different configurations with sample data, check out the [Table Panel Showcase in the Grafana Playground](http://play.grafana.org/dashboard/db/table-panel-showcase). + +## Options overview + +The table panel has many ways to manipulate your data for optimal presentation. + + + +1. `Data`: Control how your query is transformed into a table. +2. `Table Display`: Table display options. +3. `Column Styles`: Column value formating and display options. + +## Data to Table + + + +The data section contains the **To Table Transform (1)**. This is the primary option for how your data/metric +query should be transformed into a table format. The **Columns (2)** option allows you to select what columns +you want in the table. Only applicable for some transforms. + +### Time series to rows + + + +In the most simple mode you can turn time series to rows. This means you get a `Time`, `Metric` and a `Value` column. Where `Metric` is the name of the time series. + +### Time series to columns + +![](/img/v2/table_ts_to_columns.png) + +This transform allows you to take multiple time series and group them by time. Which will result in the primary column being `Time` and a column for each time series. + +### Time series aggregations + +![](/img/v2/table_ts_to_aggregations.png) +This table transformation will lay out your table into rows by metric, allowing columns of `Avg`, `Min`, `Max`, `Total`, `Current` and `Count`. More than one column can be added. + +### Annotations +![](/img/v2/table_annotations.png) + +If you have annotations enabled in the dashboard you can have the table show them. If you configure this +mode then any queries you have in the metrics tab will be ignored. + +### JSON Data +![](/img/v2/table_json_data.png) + +If you have an Elasticsearch **Raw Document** query or an Elasticsearch query without a `date histogram` use this +transform mode and pick the columns using the **Columns** section. + +![](/img/v2/elastic_raw_doc.png) + +## Table Display + + + +1. `Pagination (Page Size)`: The table display fields allow you to control The `Pagination` (page size) is the threshold at which the table rows will be broken into pages. For example, if your table had 95 records with a pagination value of 10, your table would be split across 9 pages. +2. `Scroll`: The `scroll bar` checkbox toggles the ability to scroll within the panel, when unchecked, the panel height will grow to display all rows. +3. `Font Size`: The `font size` field allows you to increase or decrease the size for the panel, relative to the default font size. + + +## Column Styles + +The column styles allow you control how dates and numbers are formatted. + + + +1. `Name or regex`: The Name or Regex field controls what columns the rule should be applied to. The regex or name filter will be matched against the column name not against column values. +2. `Type`: The three supported types of types are `Number`, `String` and `Date`. +3. `Format`: Specify date format. Only available when `Type` is set to `Date`. +4. `Coloring` and `Thresholds`: Specify color mode and thresholds limits. +5. `Unit` and `Decimals`: Specify unit and decimal precision for numbers. +6. `Add column style rule`: Add new column rule. + diff --git a/docs/sources/versions.html_fragment b/docs/sources/versions.html_fragment index 3733641ac79..d699cc6b6ac 100644 --- a/docs/sources/versions.html_fragment +++ b/docs/sources/versions.html_fragment @@ -1,3 +1,4 @@ +
  • Version v2.6
  • Version v2.5
  • Version v2.1
  • Version v2.0
  • diff --git a/package.json b/package.json index 75136e48f1a..f23ede64e26 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "company": "Coding Instinct AB" }, "name": "grafana", - "version": "2.6.0-pre1", + "version": "3.0.0-pre1", "repository": { "type": "git", "url": "http://github.com/torkelo/grafana.git" diff --git a/packaging/publish/publish.sh b/packaging/publish/publish.sh index abccf74aa4b..1d039ae1028 100755 --- a/packaging/publish/publish.sh +++ b/packaging/publish/publish.sh @@ -1,6 +1,6 @@ #! /usr/bin/env bash -version=2.5.0 +version=2.6.0 wget https://grafanarel.s3.amazonaws.com/builds/grafana_${version}_amd64.deb diff --git a/pkg/api/cloudwatch/cloudwatch.go b/pkg/api/cloudwatch/cloudwatch.go index f4cde0ebd6a..d5abe4958ea 100644 --- a/pkg/api/cloudwatch/cloudwatch.go +++ b/pkg/api/cloudwatch/cloudwatch.go @@ -7,6 +7,7 @@ import ( "time" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awsutil" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds" "github.com/aws/aws-sdk-go/aws/ec2metadata" @@ -119,7 +120,15 @@ func handleListMetrics(req *cwRequest, c *middleware.Context) { Dimensions: reqParam.Parameters.Dimensions, } - resp, err := svc.ListMetrics(params) + var resp cloudwatch.ListMetricsOutput + err := svc.ListMetricsPages(params, + func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool { + metrics, _ := awsutil.ValuesAtPath(page, "Metrics") + for _, metric := range metrics { + resp.Metrics = append(resp.Metrics, metric.(*cloudwatch.Metric)) + } + return !lastPage + }) if err != nil { c.JsonApiErr(500, "Unable to call AWS API", err) return @@ -160,7 +169,15 @@ func handleDescribeInstances(req *cwRequest, c *middleware.Context) { params.InstanceIds = reqParam.Parameters.InstanceIds } - resp, err := svc.DescribeInstances(params) + var resp ec2.DescribeInstancesOutput + err := svc.DescribeInstancesPages(params, + func(page *ec2.DescribeInstancesOutput, lastPage bool) bool { + reservations, _ := awsutil.ValuesAtPath(page, "Reservations") + for _, reservation := range reservations { + resp.Reservations = append(resp.Reservations, reservation.(*ec2.Reservation)) + } + return !lastPage + }) if err != nil { c.JsonApiErr(500, "Unable to call AWS API", err) return diff --git a/pkg/api/cloudwatch/metrics.go b/pkg/api/cloudwatch/metrics.go index 40cf974298d..e3255eccf7e 100644 --- a/pkg/api/cloudwatch/metrics.go +++ b/pkg/api/cloudwatch/metrics.go @@ -15,31 +15,47 @@ func init() { metricsMap = map[string][]string{ "AWS/AutoScaling": {"GroupMinSize", "GroupMaxSize", "GroupDesiredCapacity", "GroupInServiceInstances", "GroupPendingInstances", "GroupStandbyInstances", "GroupTerminatingInstances", "GroupTotalInstances"}, "AWS/Billing": {"EstimatedCharges"}, - "AWS/EC2": {"CPUCreditUsage", "CPUCreditBalance", "CPUUtilization", "DiskReadOps", "DiskWriteOps", "DiskReadBytes", "DiskWriteBytes", "NetworkIn", "NetworkOut", "StatusCheckFailed", "StatusCheckFailed_Instance", "StatusCheckFailed_System"}, - "AWS/ECS": {"CPUUtilization", "MemoryUtilization"}, "AWS/CloudFront": {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"}, "AWS/CloudSearch": {"SuccessfulRequests", "SearchableDocuments", "IndexUtilization", "Partitions"}, "AWS/DynamoDB": {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReturnedItemCount", "SuccessfulRequestLatency", "SystemErrors", "ThrottledRequests", "UserErrors", "WriteThrottleEvents"}, + "AWS/ECS": {"CPUUtilization", "MemoryUtilization"}, "AWS/ElastiCache": { - "CPUUtilization", "SwapUsage", "FreeableMemory", "NetworkBytesIn", "NetworkBytesOut", + "CPUUtilization", "FreeableMemory", "NetworkBytesIn", "NetworkBytesOut", "SwapUsage", "BytesUsedForCacheItems", "BytesReadIntoMemcached", "BytesWrittenOutFromMemcached", "CasBadval", "CasHits", "CasMisses", "CmdFlush", "CmdGet", "CmdSet", "CurrConnections", "CurrItems", "DecrHits", "DecrMisses", "DeleteHits", "DeleteMisses", "Evictions", "GetHits", "GetMisses", "IncrHits", "IncrMisses", "Reclaimed", - "CurrConnections", "Evictions", "Reclaimed", "NewConnections", "BytesUsedForCache", "CacheHits", "CacheMisses", "ReplicationLag", "GetTypeCmds", "SetTypeCmds", "KeyBasedCmds", "StringBasedCmds", "HashBasedCmds", "ListBasedCmds", "SetBasedCmds", "SortedSetBasedCmds", "CurrItems", + "BytesUsedForHash", "CmdConfigGet", "CmdConfigSet", "CmdTouch", "CurrConfig", "EvictedUnfetched", "ExpiredUnfetched", "SlabsMoved", "TouchHits", "TouchMisses", + "NewConnections", "NewItems", "UnusedMemory", + "BytesUsedForCache", "CacheHits", "CacheMisses", "CurrConnections", "Evictions", "HyperLogLogBasedCmds", "NewConnections", "Reclaimed", "ReplicationBytes", "ReplicationLag", "SaveInProgress", + "CurrItems", "GetTypeCmds", "HashBasedCmds", "KeyBasedCmds", "ListBasedCmds", "SetBasedCmds", "SetTypeCmds", "SortedSetBasedCmds", "StringBasedCmds", }, - "AWS/EBS": {"VolumeReadBytes", "VolumeWriteBytes", "VolumeReadOps", "VolumeWriteOps", "VolumeTotalReadTime", "VolumeTotalWriteTime", "VolumeIdleTime", "VolumeQueueLength", "VolumeThroughputPercentage", "VolumeConsumedReadWriteOps"}, - "AWS/ELB": {"HealthyHostCount", "UnHealthyHostCount", "RequestCount", "Latency", "HTTPCode_ELB_4XX", "HTTPCode_ELB_5XX", "HTTPCode_Backend_2XX", "HTTPCode_Backend_3XX", "HTTPCode_Backend_4XX", "HTTPCode_Backend_5XX", "BackendConnectionErrors", "SurgeQueueLength", "SpilloverCount"}, - "AWS/ElasticMapReduce": {"CoreNodesPending", "CoreNodesRunning", "HBaseBackupFailed", "HBaseMostRecentBackupDuration", "HBaseTimeSinceLastSuccessfulBackup", "HDFSBytesRead", "HDFSBytesWritten", "HDFSUtilization", "IsIdle", "JobsFailed", "JobsRunning", "LiveDataNodes", "LiveTaskTrackers", "MapSlotsOpen", "MissingBlocks", "ReduceSlotsOpen", "RemainingMapTasks", "RemainingMapTasksPerSlot", "RemainingReduceTasks", "RunningMapTasks", "RunningReduceTasks", "S3BytesRead", "S3BytesWritten", "TaskNodesPending", "TaskNodesRunning", "TotalLoad"}, - "AWS/Kinesis": {"PutRecord.Bytes", "PutRecord.Latency", "PutRecord.Success", "PutRecords.Bytes", "PutRecords.Latency", "PutRecords.Records", "PutRecords.Success", "IncomingBytes", "IncomingRecords", "GetRecords.Bytes", "GetRecords.IteratorAgeMilliseconds", "GetRecords.Latency", "GetRecords.Success"}, - "AWS/ML": {"PredictCount", "PredictFailureCount"}, - "AWS/OpsWorks": {"cpu_idle", "cpu_nice", "cpu_system", "cpu_user", "cpu_waitio", "load_1", "load_5", "load_15", "memory_buffers", "memory_cached", "memory_free", "memory_swap", "memory_total", "memory_used", "procs"}, - "AWS/Redshift": {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "ReadIOPS", "ReadLatency", "ReadThroughput", "WriteIOPS", "WriteLatency", "WriteThroughput"}, - "AWS/RDS": {"BinLogDiskUsage", "CPUUtilization", "DatabaseConnections", "DiskQueueDepth", "FreeableMemory", "FreeStorageSpace", "ReplicaLag", "SwapUsage", "ReadIOPS", "WriteIOPS", "ReadLatency", "WriteLatency", "ReadThroughput", "WriteThroughput", "NetworkReceiveThroughput", "NetworkTransmitThroughput"}, - "AWS/Route53": {"HealthCheckStatus", "HealthCheckPercentageHealthy"}, - "AWS/SNS": {"NumberOfMessagesPublished", "PublishSize", "NumberOfNotificationsDelivered", "NumberOfNotificationsFailed"}, - "AWS/SQS": {"NumberOfMessagesSent", "SentMessageSize", "NumberOfMessagesReceived", "NumberOfEmptyReceives", "NumberOfMessagesDeleted", "ApproximateNumberOfMessagesDelayed", "ApproximateNumberOfMessagesVisible", "ApproximateNumberOfMessagesNotVisible"}, - "AWS/S3": {"BucketSizeBytes", "NumberOfObjects"}, - "AWS/SWF": {"DecisionTaskScheduleToStartTime", "DecisionTaskStartToCloseTime", "DecisionTasksCompleted", "StartedDecisionTasksTimedOutOnClose", "WorkflowStartToCloseTime", "WorkflowsCanceled", "WorkflowsCompleted", "WorkflowsContinuedAsNew", "WorkflowsFailed", "WorkflowsTerminated", "WorkflowsTimedOut"}, - "AWS/StorageGateway": {"CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "CloudBytesDownloaded", "CloudDownloadLatency", "CloudBytesUploaded", "UploadBufferFree", "UploadBufferPercentUsed", "UploadBufferUsed", "QueuedWrites", "ReadBytes", "ReadTime", "TotalCacheSize", "WriteBytes", "WriteTime", "WorkingStorageFree", "WorkingStoragePercentUsed", "WorkingStorageUsed", "CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "ReadBytes", "ReadTime", "WriteBytes", "WriteTime", "QueuedWrites"}, - "AWS/WorkSpaces": {"Available", "Unhealthy", "ConnectionAttempt", "ConnectionSuccess", "ConnectionFailure", "SessionLaunchTime", "InSessionLatency", "SessionDisconnect"}, + "AWS/EBS": {"VolumeReadBytes", "VolumeWriteBytes", "VolumeReadOps", "VolumeWriteOps", "VolumeTotalReadTime", "VolumeTotalWriteTime", "VolumeIdleTime", "VolumeQueueLength", "VolumeThroughputPercentage", "VolumeConsumedReadWriteOps"}, + "AWS/EC2": {"CPUCreditUsage", "CPUCreditBalance", "CPUUtilization", "DiskReadOps", "DiskWriteOps", "DiskReadBytes", "DiskWriteBytes", "NetworkIn", "NetworkOut", "StatusCheckFailed", "StatusCheckFailed_Instance", "StatusCheckFailed_System"}, + "AWS/ELB": {"HealthyHostCount", "UnHealthyHostCount", "RequestCount", "Latency", "HTTPCode_ELB_4XX", "HTTPCode_ELB_5XX", "HTTPCode_Backend_2XX", "HTTPCode_Backend_3XX", "HTTPCode_Backend_4XX", "HTTPCode_Backend_5XX", "BackendConnectionErrors", "SurgeQueueLength", "SpilloverCount"}, + "AWS/ElasticMapReduce": {"IsIdle", "JobsRunning", "JobsFailed", + "MapTasksRunning", "MapTasksRemaining", "MapSlotsOpen", "RemainingMapTasksPerSlot", "ReduceTasksRunning", "ReduceTasksRemaining", "ReduceSlotsOpen", + "CoreNodesRunning", "CoreNodesPending", "LiveDataNodes", "TaskNodesRunning", "TaskNodesPending", "LiveTaskTrackers", + "S3BytesWritten", "S3BytesRead", "HDFSUtilization", "HDFSBytesRead", "HDFSBytesWritten", "MissingBlocks", "TotalLoad", + "BackupFailed", "MostRecentBackupDuration", "TimeSinceLastSuccessfulBackup", + "IsIdle", "ContainerAllocated", "ContainerReserved", "ContainerPending", "AppsCompleted", "AppsFailed", "AppsKilled", "AppsPending", "AppsRunning", "AppsSubmitted", + "CoreNodesRunning", "CoreNodesPending", "LiveDataNodes", "MRTotalNodes", "MRActiveNodes", "MRLostNodes", "MRUnhealthyNodes", "MRDecommissionedNodes", "MRRebootedNodes", + "S3BytesWritten", "S3BytesRead", "HDFSUtilization", "HDFSBytesRead", "HDFSBytesWritten", "MissingBlocks", "CorruptBlocks", "TotalLoad", "MemoryTotalMB", "MemoryReservedMB", "MemoryAvailableMB", "MemoryAllocatedMB", "PendingDeletionBlocks", "UnderReplicatedBlocks", "DfsPendingReplicationBlocks", "CapacityRemainingGB", + "HbaseBackupFailed", "MostRecentBackupDuration", "TimeSinceLastSuccessfulBackup"}, + "AWS/ES": {"ClusterStatus.green", "ClusterStatus.yellow", "ClusterStatus.red", "Nodes", "SearchableDocuments", "DeletedDocuments", "CPUUtilization", "FreeStorageSpace", "JVMMemoryPressure", "AutomatedSnapshotFailure", "MasterCPUUtilization", "MasterFreeStorageSpace", "MasterJVMMemoryPressure", "ReadLatency", "WriteLatency", "ReadThroughput", "WriteThroughput", "DiskQueueLength", "ReadIOPS", "WriteIOPS"}, + "AWS/Kinesis": {"PutRecord.Bytes", "PutRecord.Latency", "PutRecord.Success", "PutRecords.Bytes", "PutRecords.Latency", "PutRecords.Records", "PutRecords.Success", "IncomingBytes", "IncomingRecords", "GetRecords.Bytes", "GetRecords.IteratorAgeMilliseconds", "GetRecords.Latency", "GetRecords.Success"}, + "AWS/Lambda": {"Invocations", "Errors", "Duration", "Throttles"}, + "AWS/ML": {"PredictCount", "PredictFailureCount"}, + "AWS/OpsWorks": {"cpu_idle", "cpu_nice", "cpu_system", "cpu_user", "cpu_waitio", "load_1", "load_5", "load_15", "memory_buffers", "memory_cached", "memory_free", "memory_swap", "memory_total", "memory_used", "procs"}, + "AWS/Redshift": {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "ReadIOPS", "ReadLatency", "ReadThroughput", "WriteIOPS", "WriteLatency", "WriteThroughput"}, + "AWS/RDS": {"BinLogDiskUsage", "CPUUtilization", "CPUCreditUsage", "CPUCreditBalance", "DatabaseConnections", "DiskQueueDepth", "FreeableMemory", "FreeStorageSpace", "ReplicaLag", "SwapUsage", "ReadIOPS", "WriteIOPS", "ReadLatency", "WriteLatency", "ReadThroughput", "WriteThroughput", "NetworkReceiveThroughput", "NetworkTransmitThroughput"}, + "AWS/Route53": {"HealthCheckStatus", "HealthCheckPercentageHealthy"}, + "AWS/SNS": {"NumberOfMessagesPublished", "PublishSize", "NumberOfNotificationsDelivered", "NumberOfNotificationsFailed"}, + "AWS/SQS": {"NumberOfMessagesSent", "SentMessageSize", "NumberOfMessagesReceived", "NumberOfEmptyReceives", "NumberOfMessagesDeleted", "ApproximateNumberOfMessagesDelayed", "ApproximateNumberOfMessagesVisible", "ApproximateNumberOfMessagesNotVisible"}, + "AWS/S3": {"BucketSizeBytes", "NumberOfObjects"}, + "AWS/SWF": {"DecisionTaskScheduleToStartTime", "DecisionTaskStartToCloseTime", "DecisionTasksCompleted", "StartedDecisionTasksTimedOutOnClose", "WorkflowStartToCloseTime", "WorkflowsCanceled", "WorkflowsCompleted", "WorkflowsContinuedAsNew", "WorkflowsFailed", "WorkflowsTerminated", "WorkflowsTimedOut", + "ActivityTaskScheduleToCloseTime", "ActivityTaskScheduleToStartTime", "ActivityTaskStartToCloseTime", "ActivityTasksCanceled", "ActivityTasksCompleted", "ActivityTasksFailed", "ScheduledActivityTasksTimedOutOnClose", "ScheduledActivityTasksTimedOutOnStart", "StartedActivityTasksTimedOutOnClose", "StartedActivityTasksTimedOutOnHeartbeat"}, + "AWS/StorageGateway": {"CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "CloudBytesDownloaded", "CloudDownloadLatency", "CloudBytesUploaded", "UploadBufferFree", "UploadBufferPercentUsed", "UploadBufferUsed", "QueuedWrites", "ReadBytes", "ReadTime", "TotalCacheSize", "WriteBytes", "WriteTime", "TimeSinceLastRecoveryPoint", "WorkingStorageFree", "WorkingStoragePercentUsed", "WorkingStorageUsed", + "CacheHitPercent", "CachePercentUsed", "CachePercentDirty", "ReadBytes", "ReadTime", "WriteBytes", "WriteTime", "QueuedWrites"}, + "AWS/WAF": {"AllowedRequests", "BlockedRequests", "CountedRequests"}, + "AWS/WorkSpaces": {"Available", "Unhealthy", "ConnectionAttempt", "ConnectionSuccess", "ConnectionFailure", "SessionLaunchTime", "InSessionLatency", "SessionDisconnect"}, } dimensionsMap = map[string][]string{ "AWS/AutoScaling": {"AutoScalingGroupName"}, @@ -47,13 +63,15 @@ func init() { "AWS/CloudFront": {"DistributionId", "Region"}, "AWS/CloudSearch": {}, "AWS/DynamoDB": {"TableName", "GlobalSecondaryIndexName", "Operation"}, + "AWS/ECS": {"ClusterName", "ServiceName"}, "AWS/ElastiCache": {"CacheClusterId", "CacheNodeId"}, "AWS/EBS": {"VolumeId"}, "AWS/EC2": {"AutoScalingGroupName", "ImageId", "InstanceId", "InstanceType"}, - "AWS/ECS": {"ClusterName", "ServiceName"}, "AWS/ELB": {"LoadBalancerName", "AvailabilityZone"}, - "AWS/ElasticMapReduce": {"ClusterId", "JobId"}, + "AWS/ElasticMapReduce": {"ClusterId", "JobFlowId", "JobId"}, + "AWS/ES": {}, "AWS/Kinesis": {"StreamName"}, + "AWS/Lambda": {"FunctionName"}, "AWS/ML": {"MLModelId", "RequestMode"}, "AWS/OpsWorks": {"StackId", "LayerId", "InstanceId"}, "AWS/Redshift": {"NodeID", "ClusterIdentifier"}, @@ -62,8 +80,9 @@ func init() { "AWS/SNS": {"Application", "Platform", "TopicName"}, "AWS/SQS": {"QueueName"}, "AWS/S3": {"BucketName", "StorageType"}, - "AWS/SWF": {"Domain", "ActivityTypeName", "ActivityTypeVersion"}, + "AWS/SWF": {"Domain", "WorkflowTypeName", "WorkflowTypeVersion", "ActivityTypeName", "ActivityTypeVersion"}, "AWS/StorageGateway": {"GatewayId", "GatewayName", "VolumeId"}, + "AWS/WAF": {"Rule", "WebACL"}, "AWS/WorkSpaces": {"DirectoryId", "WorkspaceId"}, } } @@ -113,6 +132,7 @@ func handleGetMetrics(req *cwRequest, c *middleware.Context) { c.JsonApiErr(404, "Unable to find namespace "+reqParam.Parameters.Namespace, nil) return } + sort.Sort(sort.StringSlice(namespaceMetrics)) result := []interface{}{} for _, name := range namespaceMetrics { @@ -136,6 +156,7 @@ func handleGetDimensions(req *cwRequest, c *middleware.Context) { c.JsonApiErr(404, "Unable to find dimension "+reqParam.Parameters.Namespace, nil) return } + sort.Sort(sort.StringSlice(dimensionValues)) result := []interface{}{} for _, name := range dimensionValues { diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index c6081af50ad..1ac241c69bf 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -65,6 +65,7 @@ func GetDataSourceById(c *middleware.Context) Response { BasicAuth: ds.BasicAuth, BasicAuthUser: ds.BasicAuthUser, BasicAuthPassword: ds.BasicAuthPassword, + WithCredentials: ds.WithCredentials, IsDefault: ds.IsDefault, JsonData: ds.JsonData, }) diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index 7af4c84f56d..50db279b25b 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -61,6 +61,7 @@ type DataSource struct { BasicAuth bool `json:"basicAuth"` BasicAuthUser string `json:"basicAuthUser"` BasicAuthPassword string `json:"basicAuthPassword"` + WithCredentials bool `json:"withCredentials"` IsDefault bool `json:"isDefault"` JsonData map[string]interface{} `json:"jsonData"` } diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index a6b27327dfd..db916764888 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -69,6 +69,9 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro if ds.BasicAuth { dsMap["basicAuth"] = util.GetBasicAuthHeader(ds.BasicAuthUser, ds.BasicAuthPassword) } + if ds.WithCredentials { + dsMap["withCredentials"] = ds.WithCredentials + } if ds.Type == m.DS_INFLUXDB_08 { dsMap["username"] = ds.User diff --git a/pkg/api/login.go b/pkg/api/login.go index d7856ebbaad..d0aace4235c 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -28,6 +28,7 @@ func LoginView(c *middleware.Context) { viewData.Settings["googleAuthEnabled"] = setting.OAuthService.Google viewData.Settings["githubAuthEnabled"] = setting.OAuthService.GitHub viewData.Settings["disableUserSignUp"] = !setting.AllowUserSignUp + viewData.Settings["loginHint"] = setting.LoginHint if !tryLoginUsingRememberCookie(c) { c.HTML(200, VIEW_INDEX, viewData) diff --git a/pkg/log/syslog.go b/pkg/log/syslog.go new file mode 100644 index 00000000000..bc1780056e8 --- /dev/null +++ b/pkg/log/syslog.go @@ -0,0 +1,95 @@ +//+build !windows,!nacl,!plan9 + +package log + +import ( + "encoding/json" + "errors" + "log/syslog" +) + +type SyslogWriter struct { + syslog *syslog.Writer + Network string `json:"network"` + Address string `json:"address"` + Facility string `json:"facility"` + Tag string `json:"tag"` +} + +func NewSyslog() LoggerInterface { + return new(SyslogWriter) +} + +func (sw *SyslogWriter) Init(config string) error { + if err := json.Unmarshal([]byte(config), sw); err != nil { + return err + } + + prio, err := parseFacility(sw.Facility) + if err != nil { + return err + } + + w, err := syslog.Dial(sw.Network, sw.Address, prio, sw.Tag) + if err != nil { + return err + } + + sw.syslog = w + return nil +} + +func (sw *SyslogWriter) WriteMsg(msg string, skip, level int) error { + var err error + + switch level { + case TRACE, DEBUG: + err = sw.syslog.Debug(msg) + case INFO: + err = sw.syslog.Info(msg) + case WARN: + err = sw.syslog.Warning(msg) + case ERROR: + err = sw.syslog.Err(msg) + case CRITICAL: + err = sw.syslog.Crit(msg) + case FATAL: + err = sw.syslog.Alert(msg) + default: + err = errors.New("invalid syslog level") + } + + return err +} + +func (sw *SyslogWriter) Destroy() { + sw.syslog.Close() +} + +func (sw *SyslogWriter) Flush() {} + +var facilities = map[string]syslog.Priority{ + "user": syslog.LOG_USER, + "daemon": syslog.LOG_DAEMON, + "local0": syslog.LOG_LOCAL0, + "local1": syslog.LOG_LOCAL1, + "local2": syslog.LOG_LOCAL2, + "local3": syslog.LOG_LOCAL3, + "local4": syslog.LOG_LOCAL4, + "local5": syslog.LOG_LOCAL5, + "local6": syslog.LOG_LOCAL6, + "local7": syslog.LOG_LOCAL7, +} + +func parseFacility(facility string) (syslog.Priority, error) { + prio, ok := facilities[facility] + if !ok { + return syslog.LOG_LOCAL0, errors.New("invalid syslog facility") + } + + return prio, nil +} + +func init() { + Register("syslog", NewSyslog) +} diff --git a/pkg/middleware/logger.go b/pkg/middleware/logger.go index eb5c7b8dde4..38262de573d 100644 --- a/pkg/middleware/logger.go +++ b/pkg/middleware/logger.go @@ -32,7 +32,12 @@ func Logger() macaron.Handler { rw := res.(macaron.ResponseWriter) c.Next() - content := fmt.Sprintf("Completed %s %v %s in %v", req.URL.Path, rw.Status(), http.StatusText(rw.Status()), time.Since(start)) + uname := c.GetCookie(setting.CookieUserName) + if len(uname) == 0 { + uname = "-" + } + + content := fmt.Sprintf("Completed %s %s \"%s %s %s\" %v %s %d bytes in %dus", c.RemoteAddr(), uname, req.Method, req.URL.Path, req.Proto, rw.Status(), http.StatusText(rw.Status()), rw.Size(), time.Since(start)/time.Microsecond) switch rw.Status() { case 200, 304: diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index 38273598ab1..88abd03c319 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -40,6 +40,7 @@ type DataSource struct { BasicAuth bool BasicAuthUser string BasicAuthPassword string + WithCredentials bool IsDefault bool JsonData map[string]interface{} @@ -83,6 +84,7 @@ type AddDataSourceCommand struct { BasicAuth bool `json:"basicAuth"` BasicAuthUser string `json:"basicAuthUser"` BasicAuthPassword string `json:"basicAuthPassword"` + WithCredentials bool `json:"withCredentials"` IsDefault bool `json:"isDefault"` JsonData map[string]interface{} `json:"jsonData"` @@ -103,6 +105,7 @@ type UpdateDataSourceCommand struct { BasicAuth bool `json:"basicAuth"` BasicAuthUser string `json:"basicAuthUser"` BasicAuthPassword string `json:"basicAuthPassword"` + WithCredentials bool `json:"withCredentials"` IsDefault bool `json:"isDefault"` JsonData map[string]interface{} `json:"jsonData"` diff --git a/pkg/services/sqlstore/datasource.go b/pkg/services/sqlstore/datasource.go index ebb2ad977b1..4e1292afaa6 100644 --- a/pkg/services/sqlstore/datasource.go +++ b/pkg/services/sqlstore/datasource.go @@ -114,12 +114,14 @@ func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error { BasicAuth: cmd.BasicAuth, BasicAuthUser: cmd.BasicAuthUser, BasicAuthPassword: cmd.BasicAuthPassword, + WithCredentials: cmd.WithCredentials, JsonData: cmd.JsonData, Updated: time.Now(), } sess.UseBool("is_default") sess.UseBool("basic_auth") + sess.UseBool("with_credentials") _, err := sess.Where("id=? and org_id=?", ds.Id, ds.OrgId).Update(ds) if err != nil { diff --git a/pkg/services/sqlstore/migrations/datasource_mig.go b/pkg/services/sqlstore/migrations/datasource_mig.go index 4f046b1f8e9..90f7dac85e6 100644 --- a/pkg/services/sqlstore/migrations/datasource_mig.go +++ b/pkg/services/sqlstore/migrations/datasource_mig.go @@ -96,4 +96,9 @@ func addDataSourceMigration(mg *Migrator) { })) mg.AddMigration("Drop old table data_source_v1 #2", NewDropTableMigration("data_source_v1")) + + // add column to activate withCredentials option + mg.AddMigration("Add column with_credentials", NewAddColumnMigration(tableV2, &Column{ + Name: "with_credentials", Type: DB_Bool, Nullable: false, Default: "0", + })) } diff --git a/pkg/services/sqlstore/migrator/column.go b/pkg/services/sqlstore/migrator/column.go index db2db6dea04..11d6a1b8b08 100644 --- a/pkg/services/sqlstore/migrator/column.go +++ b/pkg/services/sqlstore/migrator/column.go @@ -55,7 +55,7 @@ func (col *Column) StringNoPk(d Dialect) string { } if col.Default != "" { - sql += "DEFAULT " + col.Default + " " + sql += "DEFAULT " + d.Default(col) + " " } return sql diff --git a/pkg/services/sqlstore/migrator/dialect.go b/pkg/services/sqlstore/migrator/dialect.go index f2fffbb16b3..0c94eb82234 100644 --- a/pkg/services/sqlstore/migrator/dialect.go +++ b/pkg/services/sqlstore/migrator/dialect.go @@ -17,10 +17,11 @@ type Dialect interface { SqlType(col *Column) string SupportEngine() bool LikeStr() string + Default(col *Column) string CreateIndexSql(tableName string, index *Index) string CreateTableSql(table *Table) string - AddColumnSql(tableName string, Col *Column) string + AddColumnSql(tableName string, col *Column) string CopyTableData(sourceTable string, targetTable string, sourceCols []string, targetCols []string) string DropTable(tableName string) string DropIndexSql(tableName string, index *Index) string @@ -71,6 +72,10 @@ func (b *BaseDialect) EqStr() string { return "=" } +func (b *BaseDialect) Default(col *Column) string { + return col.Default +} + func (b *BaseDialect) CreateTableSql(table *Table) string { var sql string sql = "CREATE TABLE IF NOT EXISTS " diff --git a/pkg/services/sqlstore/migrator/migrations.go b/pkg/services/sqlstore/migrator/migrations.go index a65c7ec7e81..7d97abb6bb9 100644 --- a/pkg/services/sqlstore/migrator/migrations.go +++ b/pkg/services/sqlstore/migrator/migrations.go @@ -64,6 +64,10 @@ type AddColumnMigration struct { column *Column } +func NewAddColumnMigration(table Table, col *Column) *AddColumnMigration { + return &AddColumnMigration{tableName: table.Name, column: col} +} + func (m *AddColumnMigration) Table(tableName string) *AddColumnMigration { m.tableName = tableName return m diff --git a/pkg/services/sqlstore/migrator/postgres_dialect.go b/pkg/services/sqlstore/migrator/postgres_dialect.go index 3ab13f49b49..826a00a1410 100644 --- a/pkg/services/sqlstore/migrator/postgres_dialect.go +++ b/pkg/services/sqlstore/migrator/postgres_dialect.go @@ -36,6 +36,17 @@ func (db *Postgres) AutoIncrStr() string { return "" } +func (b *Postgres) Default(col *Column) string { + if col.Type == DB_Bool { + if col.Default == "0" { + return "FALSE" + } else { + return "TRUE" + } + } + return col.Default +} + func (db *Postgres) SqlType(c *Column) string { var res string switch t := c.Type; t { diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index d796d401da2..3edb5777da9 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -82,6 +82,7 @@ var ( AutoAssignOrg bool AutoAssignOrgRole string VerifyEmailEnabled bool + LoginHint string // Http auth AdminUser string @@ -174,6 +175,9 @@ func applyEnvVariableOverrides() { if len(envValue) > 0 { key.SetValue(envValue) + if strings.Contains(envKey, "PASSWORD") { + envValue = "*********" + } appliedEnvOverrides = append(appliedEnvOverrides, fmt.Sprintf("%s=%s", envKey, envValue)) } } @@ -188,6 +192,9 @@ func applyCommandLineDefaultProperties(props map[string]string) { value, exists := props[keyString] if exists { key.SetValue(value) + if strings.Contains(keyString, "password") { + value = "*********" + } appliedCommandLineProperties = append(appliedCommandLineProperties, fmt.Sprintf("%s=%s", keyString, value)) } } @@ -428,6 +435,7 @@ func NewConfigContext(args *CommandLineArgs) error { AutoAssignOrg = users.Key("auto_assign_org").MustBool(true) AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Read Only Editor", "Viewer"}) VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false) + LoginHint = users.Key("login_hint").String() // anonymous access AnonymousEnabled = Cfg.Section("auth.anonymous").Key("enabled").MustBool(false) @@ -565,6 +573,14 @@ func initLogging(args *CommandLineArgs) { "driver": sec.Key("driver").String(), "conn": sec.Key("conn").String(), } + case "syslog": + LogConfigs[i] = util.DynMap{ + "level": level, + "network": sec.Key("network").MustString(""), + "address": sec.Key("address").MustString(""), + "facility": sec.Key("facility").MustString("local7"), + "tag": sec.Key("tag").MustString(""), + } } cfgJsonBytes, _ := json.Marshal(LogConfigs[i]) diff --git a/public/app/core/controllers/login_ctrl.js b/public/app/core/controllers/login_ctrl.js index 22cb2c6f04b..df58112e871 100644 --- a/public/app/core/controllers/login_ctrl.js +++ b/public/app/core/controllers/login_ctrl.js @@ -18,6 +18,7 @@ function (angular, coreModule, config) { $scope.googleAuthEnabled = config.googleAuthEnabled; $scope.githubAuthEnabled = config.githubAuthEnabled; $scope.disableUserSignUp = config.disableUserSignUp; + $scope.loginHint = config.loginHint; $scope.loginMode = true; $scope.submitBtnText = 'Log in'; diff --git a/public/app/core/directives/metric_segment.js b/public/app/core/directives/metric_segment.js index 1d07544bb2d..bf7f7309b2b 100644 --- a/public/app/core/directives/metric_segment.js +++ b/public/app/core/directives/metric_segment.js @@ -55,8 +55,8 @@ function (_, $, coreModule) { }); }; - $scope.switchToLink = function() { - if (linkMode) { return; } + $scope.switchToLink = function(fromClick) { + if (linkMode && !fromClick) { return; } clearTimeout(cancelBlur); cancelBlur = null; @@ -69,7 +69,7 @@ function (_, $, coreModule) { $scope.inputBlur = function() { // happens long before the click event on the typeahead options // need to have long delay because the blur - cancelBlur = setTimeout($scope.switchToLink, 100); + cancelBlur = setTimeout($scope.switchToLink, 200); }; $scope.source = function(query, callback) { @@ -100,7 +100,7 @@ function (_, $, coreModule) { } $input.val(value); - $scope.switchToLink(); + $scope.switchToLink(true); return value; }; diff --git a/public/app/core/directives/value_select_dropdown.js b/public/app/core/directives/value_select_dropdown.js index 4f5076abd2f..873174c6fc6 100644 --- a/public/app/core/directives/value_select_dropdown.js +++ b/public/app/core/directives/value_select_dropdown.js @@ -156,7 +156,7 @@ function (angular, _, coreModule) { vm.selectionsChanged = function(commitChange) { vm.selectedValues = _.filter(vm.options, {selected: true}); - if (vm.selectedValues.length > 1 && vm.selectedValues.length !== vm.options.length) { + if (vm.selectedValues.length > 1) { if (vm.selectedValues[0].text === 'All') { vm.selectedValues[0].selected = false; vm.selectedValues = vm.selectedValues.slice(1, vm.selectedValues.length); diff --git a/public/app/core/routes/all.js b/public/app/core/routes/all.js index fd029a27939..218310382d9 100644 --- a/public/app/core/routes/all.js +++ b/public/app/core/routes/all.js @@ -141,6 +141,9 @@ define([ controller: 'PluginEditCtrl', resolve: loadOrgBundle, }) + .when('/global-alerts', { + templateUrl: 'app/features/dashboard/partials/globalAlerts.html', + }) .otherwise({ templateUrl: 'app/partials/error.html', controller: 'ErrorCtrl' diff --git a/public/app/core/table_model.ts b/public/app/core/table_model.ts new file mode 100644 index 00000000000..7eb8d5ad92a --- /dev/null +++ b/public/app/core/table_model.ts @@ -0,0 +1,39 @@ + +class TableModel { + columns: any[]; + rows: any[]; + type: string; + + constructor() { + this.columns = []; + this.rows = []; + this.type = 'table'; + } + + sort(options) { + if (options.col === null || this.columns.length <= options.col) { + return; + } + + this.rows.sort(function(a, b) { + a = a[options.col]; + b = b[options.col]; + if (a < b) { + return -1; + } + if (a > b) { + return 1; + } + return 0; + }); + + this.columns[options.col].sort = true; + + if (options.desc) { + this.rows.reverse(); + this.columns[options.col].desc = true; + } + } +} + +export = TableModel; diff --git a/public/app/core/utils/rangeutil.ts b/public/app/core/utils/rangeutil.ts index 9e44a50c81f..535dda7833e 100644 --- a/public/app/core/utils/rangeutil.ts +++ b/public/app/core/utils/rangeutil.ts @@ -17,9 +17,9 @@ var spans = { var rangeOptions = [ { from: 'now/d', to: 'now/d', display: 'Today', section: 2 }, - { from: 'now/d', to: 'now', display: 'The day so far', section: 2 }, + { from: 'now/d', to: 'now', display: 'Today so far', section: 2 }, { from: 'now/w', to: 'now/w', display: 'This week', section: 2 }, - { from: 'now/w', to: 'now', display: 'Week to date', section: 2 }, + { from: 'now/w', to: 'now', display: 'This week so far', section: 2 }, { from: 'now/M', to: 'now/M', display: 'This month', section: 2 }, { from: 'now/y', to: 'now/y', display: 'This year', section: 2 }, diff --git a/public/app/features/admin/partials/orgs.html b/public/app/features/admin/partials/orgs.html index 573c0799c68..01d2483c7a7 100644 --- a/public/app/features/admin/partials/orgs.html +++ b/public/app/features/admin/partials/orgs.html @@ -4,33 +4,35 @@ -
    -
    +
    +

    Organizations

    - - - - - - - - - - - - +
    IdName
    {{org.id}}{{org.name}} - - - Edit - -    - - - -
    + + + + + + + + + + + + + +
    IdName
    {{org.id}}{{org.name}} + + + Edit + +    + + + +
    -
    diff --git a/public/app/features/admin/partials/users.html b/public/app/features/admin/partials/users.html index 6f8365e75dd..c7839b755e0 100644 --- a/public/app/features/admin/partials/users.html +++ b/public/app/features/admin/partials/users.html @@ -5,38 +5,42 @@ -
    -
    +
    +

    Users

    - - - - - - - - - - - - - - - - - +
    IdNameLoginEmailGrafana Admin
    {{user.id}}{{user.name}}{{user.login}}{{user.email}}{{user.isAdmin}} - - - Edit - -    - - - -
    + + + + + + + + + + + + + + + + + + + +
    IdNameLoginEmailGrafana Admin
    {{user.id}}{{user.name}}{{user.login}}{{user.email}}{{user.isAdmin}} + + + Edit + +    + + + +
    diff --git a/public/app/features/dashboard/partials/globalAlerts.html b/public/app/features/dashboard/partials/globalAlerts.html new file mode 100644 index 00000000000..a849f50ffdd --- /dev/null +++ b/public/app/features/dashboard/partials/globalAlerts.html @@ -0,0 +1,282 @@ + + + + +
    +
    +

    Global alerts

    + +
    +
    +
      +
    • Filters:
    • +
    • Alert State
    • +
    • +
    • Dashboards
    • +
    • +
    • + + + +
    • +
    +
    +
    +
    +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + 2 selected, showing 6 of 6 total +
    • +
    +
      +
    • + +
      +
      Alert query configure alerting
      +
      +
        +
      • A
      • +
      • apps
      • +
      • +
      • fakesite
      • +
      • counters
      • +
      • requests
      • +
      • count
      • +
      • scaleToSeconds(1)
      • +
      • aliasByNode(2)
      • +
      +
      +
      +
      +
    • +
    • + +
      +
      Alert query configure alerting
      +
      +
        +
      • A
      • +
      • apps
      • +
      • +
      • fakesite
      • +
      • counters
      • +
      • requests
      • +
      • count
      • +
      • scaleToSeconds(1)
      • +
      • aliasByNode(2)
      • +
      +
      +
      +
      +
    • +
    • + +
      +
      Alert query configure alerting
      +
      +
        +
      • A
      • +
      • apps
      • +
      • +
      • fakesite
      • +
      • counters
      • +
      • requests
      • +
      • count
      • +
      • scaleToSeconds(1)
      • +
      • aliasByNode(2)
      • +
      +
      +
      +
      +
    • +
    • + +
      +
      Alert query configure alerting
      +
      +
        +
      • A
      • +
      • apps
      • +
      • +
      • fakesite
      • +
      • counters
      • +
      • requests
      • +
      • count
      • +
      • scaleToSeconds(1)
      • +
      • aliasByNode(2)
      • +
      +
      +
      +
      +
    • +
    • + +
      +
      Alert query configure alerting
      +
      +
        +
      • A
      • +
      • apps
      • +
      • +
      • fakesite
      • +
      • counters
      • +
      • requests
      • +
      • count
      • +
      • scaleToSeconds(1)
      • +
      • aliasByNode(2)
      • +
      +
      +
      +
      +
    • +
    +
    +
    diff --git a/public/app/features/dashboard/timeSrv.js b/public/app/features/dashboard/timeSrv.js index 691bfd07904..62d7b8c591e 100644 --- a/public/app/features/dashboard/timeSrv.js +++ b/public/app/features/dashboard/timeSrv.js @@ -47,8 +47,9 @@ define([ if (value.length === 15) { return moment.utc(value, 'YYYYMMDDTHHmmss'); } - var epoch = parseInt(value); - if (!_.isNaN(epoch)) { + + if (!isNaN(value)) { + var epoch = parseInt(value); return moment(epoch); } diff --git a/public/app/features/dashboard/timepicker/timepicker.html b/public/app/features/dashboard/timepicker/timepicker.html index ce976e2cc3d..66093110700 100644 --- a/public/app/features/dashboard/timepicker/timepicker.html +++ b/public/app/features/dashboard/timepicker/timepicker.html @@ -16,14 +16,12 @@   -   - Refresh every {{ctrl.dashboard.refresh}} -
  • +
  • diff --git a/public/app/features/org/datasourceEditCtrl.js b/public/app/features/org/datasourceEditCtrl.js index e2d5cc3d75d..b7f141f480e 100644 --- a/public/app/features/org/datasourceEditCtrl.js +++ b/public/app/features/org/datasourceEditCtrl.js @@ -13,7 +13,7 @@ function (angular, _, config) { $scope.httpConfigPartialSrc = 'app/features/org/partials/datasourceHttpConfig.html'; - var defaults = {name: '', type: 'graphite', url: '', access: 'proxy' }; + var defaults = {name: '', type: 'graphite', url: '', access: 'proxy', jsonData: {}}; $scope.indexPatternTypes = [ {name: 'No pattern', value: undefined}, @@ -24,6 +24,11 @@ function (angular, _, config) { {name: 'Yearly', value: 'Yearly', example: '[logstash-]YYYY'}, ]; + $scope.esVersions = [ + {name: '1.x', value: 1}, + {name: '2.x', value: 2}, + ]; + $scope.init = function() { $scope.isNew = true; $scope.datasources = []; diff --git a/public/app/features/org/partials/datasourceHttpConfig.html b/public/app/features/org/partials/datasourceHttpConfig.html index 4175e523fa1..4b5a72dbf11 100644 --- a/public/app/features/org/partials/datasourceHttpConfig.html +++ b/public/app/features/org/partials/datasourceHttpConfig.html @@ -1,44 +1,55 @@
    Http settings
    -
    -
      -
    • - Url -
    • -
    • - -
    • -
    • - Access Direct = url is used directly from browser, Proxy = Grafana backend will proxy the request -
    • -
    • - -
    • -
    -
    +
    +
    +
      +
    • + Url +
    • +
    • + +
    • +
    • + Access Direct = url is used directly from browser, Proxy = Grafana backend will proxy the request +
    • +
    • + +
    • +
    +
    +
    +
    +
      +
    • + Http Auth +
    • +
    • + +
    • +
    • + +
    • +
    +
    +
    +
    +
      +
    • + +
    • +
    • + User +
    • +
    • + +
    • +
    • + Password +
    • +
    • + +
    • +
    +
    +
    -
    -
      -
    • - Basic Auth -
    • -
    • - -
    • -
    • - User -
    • -
    • - -
    • -
    • - Password -
    • -
    • - -
    • -
    -
    -
    - - diff --git a/public/app/features/org/partials/datasources.html b/public/app/features/org/partials/datasources.html index a2e62673e70..45fe5143a18 100644 --- a/public/app/features/org/partials/datasources.html +++ b/public/app/features/org/partials/datasources.html @@ -5,47 +5,52 @@ -
    -
    +
    +

    Data sources

    No datasources defined
    - - - - - - - - - - - - - - - +
    NameUrl
    -   - {{ds.name}} - - {{ds.url}} - - - default - - - - - Edit - - - - - -
    + + + + + + + + + + + + + + + + + +
    NameUrl
    + +   {{ds.name}} + + + {{ds.url}} + + + default + + + + + Edit + + + + + +
    diff --git a/public/app/features/org/partials/orgApiKeys.html b/public/app/features/org/partials/orgApiKeys.html index 9db700ab437..cf1bedd7877 100644 --- a/public/app/features/org/partials/orgApiKeys.html +++ b/public/app/features/org/partials/orgApiKeys.html @@ -4,9 +4,8 @@ -
    -
    - +
    +

    API Keys

    @@ -32,21 +31,25 @@ - - - - - - - - - - - +
    NameRole
    {{t.name}}{{t.role}} - - - -
    + + + + + + + + + + + + + +
    NameRole
    {{t.name}}{{t.role}} + + + +
    diff --git a/public/app/features/org/partials/orgUsers.html b/public/app/features/org/partials/orgUsers.html index b54f8875f87..c6404e1f22f 100644 --- a/public/app/features/org/partials/orgUsers.html +++ b/public/app/features/org/partials/orgUsers.html @@ -4,8 +4,8 @@ -
    -
    +
    +

    Organization users

    @@ -18,21 +18,23 @@ - - - - - - - +
    LoginEmailRole
    + + + + + + + + - + -
    LoginEmailRole
    {{user.login}}{{user.email}}{{user.email}} + @@ -41,36 +43,46 @@
    -
    - {{invite.email}} - {{invite.name}} - - -   - - - - - -
    - {{invite.url}}
    - -   - - - Invited: {{invite.createdOn | date: 'shortDate'}} by {{invite.invitedBy}} - -
    -
    + + + + + + + + + + + + + + + + + + +
    EmailName
    {{invite.email}}{{invite.name}} + +   + +
    + {{invite.url}}

    +   + + + Invited: {{invite.createdOn | date: 'shortDate'}} by {{invite.invitedBy}} + +
    -
    diff --git a/public/app/features/profile/partials/profile.html b/public/app/features/profile/partials/profile.html index d246b4d3914..63506b58bd0 100644 --- a/public/app/features/profile/partials/profile.html +++ b/public/app/features/profile/partials/profile.html @@ -5,8 +5,8 @@ -
    -
    +
    +

    Profile

    @@ -62,19 +62,28 @@

    Organizations

    - - - - - - +
    Name: {{org.name}}Role: {{org.role}} - - Current - - - Select - -
    + + + + + + + + + + + + + +
    NameRole
    {{org.name}}{{org.role}} + + Current + + + Select + +
    diff --git a/public/app/features/templating/partials/editor.html b/public/app/features/templating/partials/editor.html index 484495b3951..98212225f25 100644 --- a/public/app/features/templating/partials/editor.html +++ b/public/app/features/templating/partials/editor.html @@ -129,7 +129,7 @@
  • - Auto interval steps How many steps, roughly, the interval is rounded and will not always match this count + Auto interval steps How many times should the current time range be divided to calculate the value
  • diff --git a/public/app/features/templating/templateSrv.js b/public/app/features/templating/templateSrv.js index 9a59510c6a4..267454dbecb 100644 --- a/public/app/features/templating/templateSrv.js +++ b/public/app/features/templating/templateSrv.js @@ -79,6 +79,7 @@ function (angular, _) { this.highlightVariablesAsHtml = function(str) { if (!str || !_.isString(str)) { return str; } + str = _.escape(str); this._regex.lastIndex = 0; return str.replace(this._regex, function(match, g1, g2) { if (self._values[g1 || g2]) { diff --git a/public/app/panels/graph/alerting.html b/public/app/panels/graph/alerting.html new file mode 100644 index 00000000000..9e08fc4f6cb --- /dev/null +++ b/public/app/panels/graph/alerting.html @@ -0,0 +1,220 @@ +
    + Last updated by Grafana October 4, 2015 12:15:04 by $username +
    +
    General Alerting Options
    +
    +
      +
    • + Alert Title +
    • +
    • + +
    • +
    • + Alerting Backend +
    • +
    • + +
    • +
    • + + + +
    • +
    +
    +
    +
    +
    +
    +
    Choose your query:
    +

    Select an exising query to alert on:

    +
    +
      +
    • +
    • None
    • +
    +
    +
    +
    +
      +
    • +
    • A
    • +
    • apps
    • +
    • +
    • fakesite
    • +
    • counters
    • +
    • requests
    • +
    • count
    • +
    • scaleToSeconds(1)
    • +
    • aliasByNode(2)
    • +
    • +
    +
    +
    +
    +
      +
    • +
    • B
    • +
    • Metric: us-west-2 AWS/EC2 CPUUtilization Stats: Minimum Maximum Dimensions InstanceIS = i-b0e8a447 Alias {{stat}} Period 60
    • +
    • +
    +
    +
    +
    +
      +
    • +
    • C
    • +
    • Query: avg(counters_logins) by(server) Legend Format: {{app}} - {{server}} Step: 1s Resolution: 1/2
    • +
    • +
    +
    +
    +
    +
      +
    • +
    • D
    • +
    • SELECT mean(value) FROM logins.count WHERE hostname = /$Hostname$/ GROUP BY time($internal) hostname
    • +
    • +
    +
    +
    +
    +
      +
    • +
    • E
    • +
    • Metric: apps.backend.backend_01.counters.requests.count Alias: Bristow Aggregator: Sum Downsample: 1m Aggregator Sum Tags host = test
    • +
    • +
    +
    +
    +
    +
    +

    Or write a new custom alerting query:

    +
    +
    +
      +
    • +
    • + + + +
    • +
    • + select metric +
    • +
    • + +
    • +
    +
    +
    +
    +
    +
    +
    +
    Define Your States
    +
    +
      +
    • + by +
    • +
    • + +
    • +
    • + the values in the query over the last +
    • +
    • + +
    • +
    +
    +
    +
    +
    +
    +
    +
    +
      +
    • + Warn +
    • +
    • + +
    • +
    • + +
    • +
    • + .notify +
    • +
    • + +
    • +
    • + + + +
    • +
    +
    +
    +
    +
      +
    • + Critical +
    • +
    • + +
    • +
    • + +
    • +
    • + .notify +
    • +
    • + +
    • +
    • + + + +
    • +
    +
    +
    +
    +
    +
    +
    +
    What to Say Variables | Preview
    +
    +
      +
    • + Summary +
    • +
    • + +
    • +
    +
    +
    +
    +
      +
    • + Description +
    • +
    • + +
    • +
    +
    +
    +
    +
    diff --git a/public/app/partials/login.html b/public/app/partials/login.html index 630f295ecd3..98ed0899316 100644 --- a/public/app/partials/login.html +++ b/public/app/partials/login.html @@ -24,7 +24,7 @@ User
  • - +
  • diff --git a/public/app/plugins/datasource/cloudwatch/datasource.js b/public/app/plugins/datasource/cloudwatch/datasource.js index 79f4a6a00c7..e01bdce6096 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.js +++ b/public/app/plugins/datasource/cloudwatch/datasource.js @@ -25,7 +25,7 @@ function (angular, _) { var end = convertToCloudWatchTime(options.range.to); var queries = []; - options = _.clone(options); + options = angular.copy(options); _.each(options.targets, _.bind(function(target) { if (target.hide || !target.namespace || !target.metricName || _.isEmpty(target.statistics)) { return; @@ -113,23 +113,28 @@ function (angular, _) { }); }; - CloudWatchDatasource.prototype.getDimensionValues = function(region, namespace, metricName, dimensions) { + CloudWatchDatasource.prototype.getDimensionValues = function(region, namespace, metricName, dimensionKey, filterDimensions) { var request = { region: templateSrv.replace(region), action: 'ListMetrics', parameters: { namespace: templateSrv.replace(namespace), metricName: templateSrv.replace(metricName), - dimensions: convertDimensionFormat(dimensions, {}), + dimensions: convertDimensionFormat(filterDimensions, {}), } }; return this.awsRequest(request).then(function(result) { - return _.chain(result.Metrics).map(function(metric) { - return _.pluck(metric.Dimensions, 'Value'); - }).flatten().uniq().sortBy(function(name) { - return name; - }).map(function(value) { + return _.chain(result.Metrics) + .pluck('Dimensions') + .flatten() + .filter(function(dimension) { + return dimension !== null && dimension.Name === dimensionKey; + }) + .pluck('Value') + .uniq() + .sortBy() + .map(function(value) { return {value: value, text: value}; }).value(); }); @@ -174,25 +179,14 @@ function (angular, _) { return this.getDimensionKeys(dimensionKeysQuery[1]); } - var dimensionValuesQuery = query.match(/^dimension_values\(([^,]+?),\s?([^,]+?),\s?([^,]+?)(,\s?([^)]*))?\)/); + var dimensionValuesQuery = query.match(/^dimension_values\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?)\)/); if (dimensionValuesQuery) { region = templateSrv.replace(dimensionValuesQuery[1]); namespace = templateSrv.replace(dimensionValuesQuery[2]); metricName = templateSrv.replace(dimensionValuesQuery[3]); - var dimensionPart = templateSrv.replace(dimensionValuesQuery[5]); + var dimensionKey = templateSrv.replace(dimensionValuesQuery[4]); - var dimensions = {}; - if (!_.isEmpty(dimensionPart)) { - _.each(dimensionPart.split(','), function(v) { - var t = v.split('='); - if (t.length !== 2) { - throw new Error('Invalid query format'); - } - dimensions[t[0]] = t[1]; - }); - } - - return this.getDimensionValues(region, namespace, metricName, dimensions); + return this.getDimensionValues(region, namespace, metricName, dimensionKey, {}); } var ebsVolumeIdsQuery = query.match(/^ebs_volume_ids\(([^,]+?),\s?([^,]+?)\)/); @@ -222,7 +216,7 @@ function (angular, _) { var metricName = 'EstimatedCharges'; var dimensions = {}; - return this.getDimensionValues(region, namespace, metricName, dimensions).then(function () { + return this.getDimensionValues(region, namespace, metricName, 'ServiceName', dimensions).then(function () { return { status: 'success', message: 'Data source is working', title: 'Success' }; }); }; diff --git a/public/app/plugins/datasource/cloudwatch/query_ctrl.js b/public/app/plugins/datasource/cloudwatch/query_ctrl.js index 3869a5ec715..d0f6fe5b52a 100644 --- a/public/app/plugins/datasource/cloudwatch/query_ctrl.js +++ b/public/app/plugins/datasource/cloudwatch/query_ctrl.js @@ -76,7 +76,7 @@ function (angular, _) { } }; - $scope.getDimSegments = function(segment) { + $scope.getDimSegments = function(segment, $index) { if (segment.type === 'operator') { return $q.when([]); } var target = $scope.target; @@ -85,7 +85,8 @@ function (angular, _) { if (segment.type === 'key' || segment.type === 'plus-button') { query = $scope.datasource.getDimensionKeys($scope.target.namespace); } else if (segment.type === 'value') { - query = $scope.datasource.getDimensionValues(target.region, target.namespace, target.metricName, {}); + var dimensionKey = $scope.dimSegments[$index-2].value; + query = $scope.datasource.getDimensionValues(target.region, target.namespace, target.metricName, dimensionKey, {}); } return query.then($scope.transformToSegments(true)).then(function(results) { diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts index 39d48e273b3..f3b105c5da5 100644 --- a/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts @@ -165,7 +165,7 @@ describe('CloudWatchDatasource', function() { }); }); - describeMetricFindQuery('dimension_values(us-east-1,AWS/EC2,CPUUtilization)', scenario => { + describeMetricFindQuery('dimension_values(us-east-1,AWS/EC2,CPUUtilization,InstanceId)', scenario => { scenario.setup(() => { scenario.requestResponse = { Metrics: [ diff --git a/public/app/plugins/datasource/elasticsearch/bucket_agg.js b/public/app/plugins/datasource/elasticsearch/bucket_agg.js index 014761da7f2..2a21bc17960 100644 --- a/public/app/plugins/datasource/elasticsearch/bucket_agg.js +++ b/public/app/plugins/datasource/elasticsearch/bucket_agg.js @@ -92,8 +92,10 @@ function (angular, _, queryDef) { } case 'date_histogram': { settings.interval = settings.interval || 'auto'; + settings.min_doc_count = settings.min_doc_count || 0; $scope.agg.field = $scope.target.timeField; settingsLinkText = 'Interval: ' + settings.interval; + settingsLinkText += ', Min Doc Count: ' + settings.min_doc_count; } } diff --git a/public/app/plugins/datasource/elasticsearch/datasource.js b/public/app/plugins/datasource/elasticsearch/datasource.js index 9c749b9459e..8a5dc0a0551 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.js +++ b/public/app/plugins/datasource/elasticsearch/datasource.js @@ -19,13 +19,16 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes function ElasticDatasource(datasource) { this.type = 'elasticsearch'; this.basicAuth = datasource.basicAuth; + this.withCredentials = datasource.withCredentials; this.url = datasource.url; this.name = datasource.name; this.index = datasource.index; this.timeField = datasource.jsonData.timeField; + this.esVersion = datasource.jsonData.esVersion; this.indexPattern = new IndexPattern(datasource.index, datasource.jsonData.interval); this.queryBuilder = new ElasticQueryBuilder({ - timeField: this.timeField + timeField: this.timeField, + esVersion: this.esVersion, }); } @@ -36,8 +39,10 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes data: data }; - if (this.basicAuth) { + if (this.basicAuth || this.withCredentials) { options.withCredentials = true; + } + if (this.basicAuth) { options.headers = { "Authorization": this.basicAuth }; @@ -94,7 +99,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes var payload = angular.toJson(header) + '\n' + angular.toJson(data) + '\n'; - return this._post('/_msearch', payload).then(function(res) { + return this._post('_msearch', payload).then(function(res) { var list = []; var hits = res.responses[0].hits.hits; @@ -107,7 +112,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes for (var i = 0; i < fieldNames.length; i++) { fieldValue = fieldValue[fieldNames[i]]; if (!fieldValue) { - console.log('could not find field in annotatation: ', fieldName); + console.log('could not find field in annotation: ', fieldName); return ''; } } @@ -183,12 +188,16 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes sentTargets.push(target); } + if (sentTargets.length === 0) { + return $q.when([]); + } + payload = payload.replace(/\$interval/g, options.interval); payload = payload.replace(/\$timeFrom/g, options.range.from.valueOf()); payload = payload.replace(/\$timeTo/g, options.range.to.valueOf()); payload = templateSrv.replace(payload, options.scopedVars); - return this._post('/_msearch', payload).then(function(res) { + return this._post('_msearch', payload).then(function(res) { return new ElasticResponse(sentTargets, res).getTimeSeries(); }); }; diff --git a/public/app/plugins/datasource/elasticsearch/directives.js b/public/app/plugins/datasource/elasticsearch/directives.js index 480ee79ad0a..a7ad8f6dfbf 100644 --- a/public/app/plugins/datasource/elasticsearch/directives.js +++ b/public/app/plugins/datasource/elasticsearch/directives.js @@ -22,7 +22,7 @@ function (angular) { module.directive('elasticMetricAgg', function() { return { - templateUrl: 'app/plugins/datasource/elasticsearch/partials/metricAgg.html', + templateUrl: 'app/plugins/datasource/elasticsearch/partials/metric_agg.html', controller: 'ElasticMetricAggCtrl', restrict: 'E', scope: { @@ -30,13 +30,14 @@ function (angular) { index: "=", onChange: "&", getFields: "&", + esVersion: '=' } }; }); module.directive('elasticBucketAgg', function() { return { - templateUrl: 'app/plugins/datasource/elasticsearch/partials/bucketAgg.html', + templateUrl: 'app/plugins/datasource/elasticsearch/partials/bucket_agg.html', controller: 'ElasticBucketAggCtrl', restrict: 'E', scope: { diff --git a/public/app/plugins/datasource/elasticsearch/elastic_response.js b/public/app/plugins/datasource/elasticsearch/elastic_response.js index 73050ce7f2b..32120a4519a 100644 --- a/public/app/plugins/datasource/elasticsearch/elastic_response.js +++ b/public/app/plugins/datasource/elasticsearch/elastic_response.js @@ -15,6 +15,9 @@ function (_, queryDef) { for (y = 0; y < target.metrics.length; y++) { metric = target.metrics[y]; + if (metric.hide) { + continue; + } switch(metric.type) { case 'count': { @@ -76,8 +79,12 @@ function (_, queryDef) { newSeries = { datapoints: [], metric: metric.type, field: metric.field, props: props}; for (i = 0; i < esAgg.buckets.length; i++) { bucket = esAgg.buckets[i]; - value = bucket[metric.id].value; - newSeries.datapoints.push([value, bucket.key]); + + value = bucket[metric.id]; + if (value !== undefined) { + newSeries.datapoints.push([value.value, bucket.key]); + } + } seriesList.push(newSeries); break; @@ -193,7 +200,14 @@ function (_, queryDef) { }); } - if (series.field) { + if (series.field && queryDef.isPipelineAgg(series.metric)) { + var appliedAgg = _.findWhere(target.metrics, { id: series.field }); + if (appliedAgg) { + metricName += ' ' + queryDef.describeMetric(appliedAgg); + } else { + metricName = 'Unset'; + } + } else if (series.field) { metricName += ' ' + series.field; } diff --git a/public/app/plugins/datasource/elasticsearch/metric_agg.js b/public/app/plugins/datasource/elasticsearch/metric_agg.js index c4e04dad325..60a84e01e19 100644 --- a/public/app/plugins/datasource/elasticsearch/metric_agg.js +++ b/public/app/plugins/datasource/elasticsearch/metric_agg.js @@ -11,16 +11,23 @@ function (angular, _, queryDef) { module.controller('ElasticMetricAggCtrl', function($scope, uiSegmentSrv, $q, $rootScope) { var metricAggs = $scope.target.metrics; - $scope.metricAggTypes = queryDef.metricAggTypes; + $scope.metricAggTypes = queryDef.getMetricAggTypes($scope.esVersion); $scope.extendedStats = queryDef.extendedStats; + $scope.pipelineAggOptions = []; $scope.init = function() { $scope.agg = metricAggs[$scope.index]; $scope.validateModel(); + $scope.updatePipelineAggOptions(); + }; + + $scope.updatePipelineAggOptions = function() { + $scope.pipelineAggOptions = queryDef.getPipelineAggOptions($scope.target); }; $rootScope.onAppEvent('elastic-query-updated', function() { $scope.index = _.indexOf(metricAggs, $scope.agg); + $scope.updatePipelineAggOptions(); $scope.validateModel(); }, $scope); @@ -30,17 +37,33 @@ function (angular, _, queryDef) { $scope.settingsLinkText = ''; $scope.aggDef = _.findWhere($scope.metricAggTypes, {value: $scope.agg.type}); - if (!$scope.agg.field) { + if (queryDef.isPipelineAgg($scope.agg.type)) { + $scope.agg.pipelineAgg = $scope.agg.pipelineAgg || 'select metric'; + $scope.agg.field = $scope.agg.pipelineAgg; + + var pipelineOptions = queryDef.getPipelineOptions($scope.agg); + if (pipelineOptions.length > 0) { + _.each(pipelineOptions, function(opt) { + $scope.agg.settings[opt.text] = $scope.agg.settings[opt.text] || opt.default; + }); + $scope.settingsLinkText = 'Options'; + } + } else if (!$scope.agg.field) { $scope.agg.field = 'select field'; } switch($scope.agg.type) { case 'percentiles': { $scope.agg.settings.percents = $scope.agg.settings.percents || [25,50,75,95,99]; - $scope.settingsLinkText = 'values: ' + $scope.agg.settings.percents.join(','); + $scope.settingsLinkText = 'Values: ' + $scope.agg.settings.percents.join(','); break; } case 'extended_stats': { + if (_.keys($scope.agg.meta).length === 0) { + $scope.agg.meta.std_deviation_bounds_lower = true; + $scope.agg.meta.std_deviation_bounds_upper = true; + } + var stats = _.reduce($scope.agg.meta, function(memo, val, key) { if (val) { var def = _.findWhere($scope.extendedStats, {value: key}); @@ -48,29 +71,47 @@ function (angular, _, queryDef) { } return memo; }, []); - $scope.settingsLinkText = 'Stats: ' + stats.join(', '); - if (stats.length === 0) { - $scope.agg.meta.std_deviation_bounds_lower = true; - $scope.agg.meta.std_deviation_bounds_upper = true; - } + $scope.settingsLinkText = 'Stats: ' + stats.join(', '); break; } case 'raw_document': { $scope.target.metrics = [$scope.agg]; $scope.target.bucketAggs = []; + break; + } + } + + if ($scope.aggDef.supportsInlineScript) { + // I know this stores the inline script twice + // but having it like this simplifes the query_builder + var inlineScript = $scope.agg.inlineScript; + if (inlineScript) { + $scope.agg.settings.script = {inline: inlineScript}; + } else { + delete $scope.agg.settings.script; + } + + if ($scope.settingsLinkText === '') { + $scope.settingsLinkText = 'Options'; } } }; $scope.toggleOptions = function() { $scope.showOptions = !$scope.showOptions; + $scope.updatePipelineAggOptions(); + }; + + $scope.onChangeInternal = function() { + $scope.onChange(); }; $scope.onTypeChange = function() { $scope.agg.settings = {}; $scope.agg.meta = {}; $scope.showOptions = false; + $scope.updatePipelineAggOptions(); $scope.onChange(); }; @@ -94,6 +135,14 @@ function (angular, _, queryDef) { $scope.onChange(); }; + $scope.toggleShowMetric = function() { + $scope.agg.hide = !$scope.agg.hide; + if (!$scope.agg.hide) { + delete $scope.agg.hide; + } + $scope.onChange(); + }; + $scope.init(); }); diff --git a/public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html b/public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html index 637ae78ada7..4ef9a916866 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html +++ b/public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html @@ -6,7 +6,7 @@
    -
    Search query (lucene) Use [[filterName]] in query to replace part of the query with a filter value
    +
    Search query (lucene) Use [[filterName]] in query to replace part of the query with a filter value
    diff --git a/public/app/plugins/datasource/elasticsearch/partials/bucketAgg.html b/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html similarity index 90% rename from public/app/plugins/datasource/elasticsearch/partials/bucketAgg.html rename to public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html index f6ff3f6cd93..5a84ec8f6cd 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/bucketAgg.html +++ b/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html @@ -35,9 +35,9 @@
    -
    +
      -
    • +
    • Interval
    • @@ -46,6 +46,17 @@
    +
    +
      +
    • + Min Doc Count +
    • +
    • + +
    • +
    +
    +
    diff --git a/public/app/plugins/datasource/elasticsearch/partials/config.html b/public/app/plugins/datasource/elasticsearch/partials/config.html index d1cb05801d6..81acd03809c 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/config.html +++ b/public/app/plugins/datasource/elasticsearch/partials/config.html @@ -20,7 +20,7 @@
    -
    +
    • Time field name @@ -31,3 +31,14 @@
    +
    +
      +
    • + Version +
    • +
    • + +
    • +
    +
    +
    diff --git a/public/app/plugins/datasource/elasticsearch/partials/metricAgg.html b/public/app/plugins/datasource/elasticsearch/partials/metricAgg.html deleted file mode 100644 index 65030af5655..00000000000 --- a/public/app/plugins/datasource/elasticsearch/partials/metricAgg.html +++ /dev/null @@ -1,66 +0,0 @@ -
    - - -
      -
    • - -
    • -
    • - -
    • -
    -
    -
    - -
    -
    -
    -
      -
    • - Percentiles -
    • -
    • - -
    • -
    -
    -
    -
    -
    -
      -
    • - {{stat.text}} -
    • -
    • - -
    • -
    -
    -
    -
    -
    -
      -
    • - Sigma -
    • -
    • - -
    • -
    -
    -
    -
    -
    diff --git a/public/app/plugins/datasource/elasticsearch/partials/metric_agg.html b/public/app/plugins/datasource/elasticsearch/partials/metric_agg.html new file mode 100644 index 00000000000..4da8b53181b --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/partials/metric_agg.html @@ -0,0 +1,126 @@ +
    + + +
      +
    • + +
    • +
    • + +
    • +
    +
    +
    + +
    +
    +
    +
      +
    • + Window +
    • +
    • + +
    • +
    +
    +
    +
    +
      +
    • + Model +
    • +
    • + +
    • +
    +
    +
    +
    +
      +
    • + Percentiles +
    • +
    • + +
    • +
    +
    +
    +
    +
    +
      +
    • + {{stat.text}} +
    • +
    • + +
    • +
    +
    +
    +
    +
    +
      +
    • + Sigma +
    • +
    • + +
    • +
    +
    +
    + +
    +
      +
    • + Script +
    • +
    • + +
    • +
    +
    +
    + +
    +
      +
    • + Missing + The missing parameter defines how documents that are missing a value should be treated. By default they will be ignored but it is also possible to treat them as if they had a value +
    • +
    • + +
    • +
    +
    +
    + +
    +
    diff --git a/public/app/plugins/datasource/elasticsearch/partials/query.editor.html b/public/app/plugins/datasource/elasticsearch/partials/query.editor.html index dee2401e6f3..862a3d78254 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/query.editor.html +++ b/public/app/plugins/datasource/elasticsearch/partials/query.editor.html @@ -14,7 +14,6 @@
    diff --git a/public/app/plugins/datasource/elasticsearch/query_builder.js b/public/app/plugins/datasource/elasticsearch/query_builder.js index 129c52d3f02..dd071ba137d 100644 --- a/public/app/plugins/datasource/elasticsearch/query_builder.js +++ b/public/app/plugins/datasource/elasticsearch/query_builder.js @@ -1,16 +1,22 @@ define([ - "angular" + './query_def' ], -function (angular) { +function (queryDef) { 'use strict'; function ElasticQueryBuilder(options) { this.timeField = options.timeField; + this.esVersion = options.esVersion; } ElasticQueryBuilder.prototype.getRangeFilter = function() { var filter = {}; filter[this.timeField] = {"gte": "$timeFrom", "lte": "$timeTo"}; + + if (this.esVersion >= 2) { + filter[this.timeField]["format"] = "epoch_millis"; + } + return filter; }; @@ -45,12 +51,23 @@ function (angular) { return queryNode; }; - ElasticQueryBuilder.prototype.getInterval = function(agg) { - if (agg.settings && agg.settings.interval !== 'auto') { - return agg.settings.interval; - } else { - return '$interval'; + ElasticQueryBuilder.prototype.getDateHistogramAgg = function(aggDef) { + var esAgg = {}; + var settings = aggDef.settings || {}; + esAgg.interval = settings.interval; + esAgg.field = this.timeField; + esAgg.min_doc_count = settings.min_doc_count || 0; + esAgg.extended_bounds = {min: "$timeFrom", max: "$timeTo"}; + + if (esAgg.interval === 'auto') { + esAgg.interval = "$interval"; } + + if (this.esVersion >= 2) { + esAgg.format = "epoch_millis"; + } + + return esAgg; }; ElasticQueryBuilder.prototype.getFiltersAgg = function(aggDef) { @@ -82,9 +99,11 @@ function (angular) { }; ElasticQueryBuilder.prototype.build = function(target) { - if (target.rawQuery) { - return angular.fromJson(target.rawQuery); - } + // make sure query has defaults; + target.metrics = target.metrics || [{ type: 'count', id: '1' }]; + target.dsType = 'elasticsearch'; + target.bucketAggs = target.bucketAggs || [{type: 'date_histogram', id: '2', settings: {interval: 'auto'}}]; + target.timeField = this.timeField; var i, nestedAggs, metric; var query = { @@ -123,12 +142,7 @@ function (angular) { switch(aggDef.type) { case 'date_histogram': { - esAgg["date_histogram"] = { - "interval": this.getInterval(aggDef), - "field": this.timeField, - "min_doc_count": 0, - "extended_bounds": { "min": "$timeFrom", "max": "$timeTo" } - }; + esAgg["date_histogram"] = this.getDateHistogramAgg(aggDef); break; } case 'filters': { @@ -154,14 +168,25 @@ function (angular) { continue; } - var metricAgg = {field: metric.field}; + var aggField = {}; + var metricAgg = null; + + if (queryDef.isPipelineAgg(metric.type)) { + if (metric.pipelineAgg && /^\d*$/.test(metric.pipelineAgg)) { + metricAgg = { buckets_path: metric.pipelineAgg }; + } else { + continue; + } + } else { + metricAgg = {field: metric.field}; + } + for (var prop in metric.settings) { if (metric.settings.hasOwnProperty(prop) && metric.settings[prop] !== null) { metricAgg[prop] = metric.settings[prop]; } } - var aggField = {}; aggField[metric.type] = metricAgg; nestedAggs.aggs[metric.id] = aggField; } @@ -204,5 +229,4 @@ function (angular) { }; return ElasticQueryBuilder; - }); diff --git a/public/app/plugins/datasource/elasticsearch/query_ctrl.js b/public/app/plugins/datasource/elasticsearch/query_ctrl.js index 08eb734ec33..b222733e29d 100644 --- a/public/app/plugins/datasource/elasticsearch/query_ctrl.js +++ b/public/app/plugins/datasource/elasticsearch/query_ctrl.js @@ -7,14 +7,13 @@ function (angular) { var module = angular.module('grafana.controllers'); module.controller('ElasticQueryCtrl', function($scope, $timeout, uiSegmentSrv) { + $scope.esVersion = $scope.datasource.esVersion; $scope.init = function() { var target = $scope.target; if (!target) { return; } - target.metrics = target.metrics || [{ type: 'count', id: '1' }]; - target.bucketAggs = target.bucketAggs || [{type: 'date_histogram', id: '2', settings: {interval: 'auto'}}]; - target.timeField = $scope.datasource.timeField; + $scope.queryUpdated(); }; $scope.getFields = function(type) { @@ -39,14 +38,6 @@ function (angular) { return []; }; - $scope.toggleQueryMode = function () { - if ($scope.target.rawQuery) { - delete $scope.target.rawQuery; - } else { - $scope.target.rawQuery = angular.toJson($scope.datasource.queryBuilder.build($scope.target), true); - } - }; - $scope.init(); }); diff --git a/public/app/plugins/datasource/elasticsearch/query_def.js b/public/app/plugins/datasource/elasticsearch/query_def.js index baab2378e9f..53c1fe378ef 100644 --- a/public/app/plugins/datasource/elasticsearch/query_def.js +++ b/public/app/plugins/datasource/elasticsearch/query_def.js @@ -7,13 +7,15 @@ function (_) { return { metricAggTypes: [ {text: "Count", value: 'count', requiresField: false}, - {text: "Average", value: 'avg', requiresField: true}, - {text: "Sum", value: 'sum', requiresField: true}, - {text: "Max", value: 'max', requiresField: true}, - {text: "Min", value: 'min', requiresField: true}, - {text: "Extended Stats", value: 'extended_stats', requiresField: true}, - {text: "Percentiles", value: 'percentiles', requiresField: true}, - {text: "Unique Count", value: "cardinality", requiresField: true}, + {text: "Average", value: 'avg', requiresField: true, supportsInlineScript: true, supportsMissing: true}, + {text: "Sum", value: 'sum', requiresField: true, supportsInlineScript: true, supportsMissing: true}, + {text: "Max", value: 'max', requiresField: true, supportsInlineScript: true, supportsMissing: true}, + {text: "Min", value: 'min', requiresField: true, supportsInlineScript: true, supportsMissing: true}, + {text: "Extended Stats", value: 'extended_stats', requiresField: true, supportsMissing: true, supportsInlineScript: true}, + {text: "Percentiles", value: 'percentiles', requiresField: true, supportsMissing: true, supportsInlineScript: true}, + {text: "Unique Count", value: "cardinality", requiresField: true, supportsMissing: true}, + {text: "Moving Average", value: 'moving_avg', requiresField: false, isPipelineAgg: true, minVersion: 2}, + {text: "Derivative", value: 'derivative', requiresField: false, isPipelineAgg: true, minVersion: 2 }, {text: "Raw Document", value: "raw_document", requiresField: false} ], @@ -66,6 +68,53 @@ function (_) { {text: '1d', value: '1d'}, ], + pipelineOptions: { + 'moving_avg' : [ + {text: 'window', default: 5}, + {text: 'model', default: 'simple'} + ], + 'derivative': [] + }, + + getMetricAggTypes: function(esVersion) { + return _.filter(this.metricAggTypes, function(f) { + if (f.minVersion) { + return f.minVersion <= esVersion; + } else { + return true; + } + }); + }, + + getPipelineOptions: function(metric) { + if (!this.isPipelineAgg(metric.type)) { + return []; + } + + return this.pipelineOptions[metric.type]; + }, + + isPipelineAgg: function(metricType) { + if (metricType) { + var po = this.pipelineOptions[metricType]; + return po !== null && po !== undefined; + } + + return false; + }, + + getPipelineAggOptions: function(targets) { + var self = this; + var result = []; + _.each(targets.metrics, function(metric) { + if (!self.isPipelineAgg(metric.type)) { + result.push({text: self.describeMetric(metric), value: metric.id }); + } + }); + + return result; + }, + getOrderByOptions: function(target) { var self = this; var metricRefs = []; diff --git a/public/app/plugins/datasource/elasticsearch/specs/query_builder_specs.ts b/public/app/plugins/datasource/elasticsearch/specs/query_builder_specs.ts index bcae7d6e852..0be35b20a38 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/query_builder_specs.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/query_builder_specs.ts @@ -22,14 +22,6 @@ describe('ElasticQueryBuilder', function() { expect(query.aggs["1"].date_histogram.extended_bounds.min).to.be("$timeFrom"); }); - it('with raw query', function() { - var query = builder.build({ - rawQuery: '{"query": "$lucene_query"}', - }); - - expect(query.query).to.be("$lucene_query"); - }); - it('with multiple bucket aggs', function() { var query = builder.build({ metrics: [{type: 'count', id: '1'}], @@ -44,6 +36,39 @@ describe('ElasticQueryBuilder', function() { expect(query.aggs["2"].aggs["3"].date_histogram.field).to.be("@timestamp"); }); + it('with es1.x and es2.x date histogram queries check time format', function() { + var builder_2x = new ElasticQueryBuilder({ + timeField: '@timestamp', + esVersion: 2 + }); + + var query_params = { + metrics: [], + bucketAggs: [ + {type: 'date_histogram', field: '@timestamp', id: '1'} + ], + }; + + // format should not be specified in 1.x queries + expect("format" in builder.build(query_params)["aggs"]["1"]["date_histogram"]).to.be(false); + + // 2.x query should specify format to be "epoch_millis" + expect(builder_2x.build(query_params)["aggs"]["1"]["date_histogram"]["format"]).to.be("epoch_millis"); + }); + + it('with es1.x and es2.x range filter check time format', function() { + var builder_2x = new ElasticQueryBuilder({ + timeField: '@timestamp', + esVersion: 2 + }); + + // format should not be specified in 1.x queries + expect("format" in builder.getRangeFilter()["@timestamp"]).to.be(false); + + // 2.x query should specify format to be "epoch_millis" + expect(builder_2x.getRangeFilter()["@timestamp"]["format"]).to.be("epoch_millis"); + }); + it('with select field', function() { var query = builder.build({ metrics: [{type: 'avg', field: '@value', id: '1'}], @@ -130,4 +155,89 @@ describe('ElasticQueryBuilder', function() { expect(query.size).to.be(500); }); + it('with moving average', function() { + var query = builder.build({ + metrics: [ + { + id: '3', + type: 'sum', + field: '@value' + }, + { + id: '2', + type: 'moving_avg', + field: '3', + pipelineAgg: '3' + } + ], + bucketAggs: [ + {type: 'date_histogram', field: '@timestamp', id: '3'} + ], + }); + + var firstLevel = query.aggs["3"]; + + expect(firstLevel.aggs["2"]).not.to.be(undefined); + expect(firstLevel.aggs["2"].moving_avg).not.to.be(undefined); + expect(firstLevel.aggs["2"].moving_avg.buckets_path).to.be("3"); + }); + + it('with broken moving average', function() { + var query = builder.build({ + metrics: [ + { + id: '3', + type: 'sum', + field: '@value' + }, + { + id: '2', + type: 'moving_avg', + pipelineAgg: '3' + }, + { + id: '4', + type: 'moving_avg', + pipelineAgg: 'Metric to apply moving average' + } + ], + bucketAggs: [ + { type: 'date_histogram', field: '@timestamp', id: '3' } + ], + }); + + var firstLevel = query.aggs["3"]; + + expect(firstLevel.aggs["2"]).not.to.be(undefined); + expect(firstLevel.aggs["2"].moving_avg).not.to.be(undefined); + expect(firstLevel.aggs["2"].moving_avg.buckets_path).to.be("3"); + expect(firstLevel.aggs["4"]).to.be(undefined); + }); + + it('with derivative', function() { + var query = builder.build({ + metrics: [ + { + id: '3', + type: 'sum', + field: '@value' + }, + { + id: '2', + type: 'derivative', + pipelineAgg: '3' + } + ], + bucketAggs: [ + {type: 'date_histogram', field: '@timestamp', id: '3'} + ], + }); + + var firstLevel = query.aggs["3"]; + + expect(firstLevel.aggs["2"]).not.to.be(undefined); + expect(firstLevel.aggs["2"].derivative).not.to.be(undefined); + expect(firstLevel.aggs["2"].derivative.buckets_path).to.be("3"); + }); + }); diff --git a/public/app/plugins/datasource/elasticsearch/specs/query_def_specs.ts b/public/app/plugins/datasource/elasticsearch/specs/query_def_specs.ts new file mode 100644 index 00000000000..4552226d296 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/specs/query_def_specs.ts @@ -0,0 +1,102 @@ +/// +/// + +import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; + +declare var helpers: any; +declare var QueryDef: any; + +describe('ElasticQueryDef', function() { + + describe('getPipelineAggOptions', function() { + describe('with zero targets', function() { + var response = QueryDef.getPipelineAggOptions([]); + + it('should return zero', function() { + expect(response.length).to.be(0); + }); + }); + + describe('with count and sum targets', function() { + var targets = { + metrics: [ + { type: 'count', field: '@value' }, + { type: 'sum', field: '@value' } + ] + }; + + var response = QueryDef.getPipelineAggOptions(targets); + + it('should return zero', function() { + expect(response.length).to.be(2); + }); + }); + + describe('with count and moving average targets', function() { + var targets = { + metrics: [ + { type: 'count', field: '@value' }, + { type: 'moving_avg', field: '@value' } + ] + }; + + var response = QueryDef.getPipelineAggOptions(targets); + + it('should return one', function() { + expect(response.length).to.be(1); + }); + }); + + describe('with derivatives targets', function() { + var targets = { + metrics: [ + { type: 'derivative', field: '@value' } + ] + }; + + var response = QueryDef.getPipelineAggOptions(targets); + + it('should return zero', function() { + expect(response.length).to.be(0); + }); + }); + }); + + describe('isPipelineMetric', function() { + describe('moving_avg', function() { + var result = QueryDef.isPipelineAgg('moving_avg'); + + it('is pipe line metric', function() { + expect(result).to.be(true); + }); + }); + + describe('count', function() { + var result = QueryDef.isPipelineAgg('count'); + + it('is not pipe line metric', function() { + expect(result).to.be(false); + }); + }); + }); + + describe('pipeline aggs depending on esverison', function() { + describe('using esversion undefined', function() { + it('should not get pipeline aggs', function() { + expect(QueryDef.getMetricAggTypes(undefined).length).to.be(9); + }); + }); + + describe('using esversion 1', function() { + it('should not get pipeline aggs', function() { + expect(QueryDef.getMetricAggTypes(1).length).to.be(9); + }); + }); + + describe('using esversion 2', function() { + it('should get pipeline aggs', function() { + expect(QueryDef.getMetricAggTypes(2).length).to.be(11); + }); + }); + }); +}); diff --git a/public/app/plugins/datasource/influxdb/datasource.js b/public/app/plugins/datasource/influxdb/datasource.js index 9adabe9a83f..a23937cd74c 100644 --- a/public/app/plugins/datasource/influxdb/datasource.js +++ b/public/app/plugins/datasource/influxdb/datasource.js @@ -53,6 +53,7 @@ function (angular, _, dateMath, InfluxSeries, InfluxQuery) { // replace templated variables allQueries = templateSrv.replace(allQueries, options.scopedVars); + return this._seriesQuery(allQueries).then(function(data) { if (!data || !data.results) { return []; @@ -63,13 +64,26 @@ function (angular, _, dateMath, InfluxSeries, InfluxQuery) { var result = data.results[i]; if (!result || !result.series) { continue; } - var alias = (queryTargets[i] || {}).alias; + var target = queryTargets[i]; + var alias = target.alias; if (alias) { - alias = templateSrv.replace(alias, options.scopedVars); + alias = templateSrv.replace(target.alias, options.scopedVars); } - var targetSeries = new InfluxSeries({ series: data.results[i].series, alias: alias }).getTimeSeries(); - for (y = 0; y < targetSeries.length; y++) { - seriesList.push(targetSeries[y]); + + var influxSeries = new InfluxSeries({ series: data.results[i].series, alias: alias }); + + switch(target.resultFormat) { + case 'table': { + seriesList.push(influxSeries.getTable()); + break; + } + default: { + var timeSeries = influxSeries.getTimeSeries(); + for (y = 0; y < timeSeries.length; y++) { + seriesList.push(timeSeries[y]); + } + break; + } } } diff --git a/public/app/plugins/datasource/influxdb/influx_query.ts b/public/app/plugins/datasource/influxdb/influx_query.ts index 34b86e16930..f50627c7f89 100644 --- a/public/app/plugins/datasource/influxdb/influx_query.ts +++ b/public/app/plugins/datasource/influxdb/influx_query.ts @@ -12,6 +12,8 @@ class InfluxQuery { constructor(target) { this.target = target; + target.dsType = 'influxdb'; + target.resultFormat = target.resultFormat || 'time_series'; target.tags = target.tags || []; target.groupBy = target.groupBy || [ {type: 'time', params: ['$interval']}, diff --git a/public/app/plugins/datasource/influxdb/influx_series.js b/public/app/plugins/datasource/influxdb/influx_series.js index fff3536b5e8..63495ffb9b0 100644 --- a/public/app/plugins/datasource/influxdb/influx_series.js +++ b/public/app/plugins/datasource/influxdb/influx_series.js @@ -1,7 +1,8 @@ define([ 'lodash', + 'app/core/table_model', ], -function (_) { +function (_, TableModel) { 'use strict'; function InfluxSeries(options) { @@ -108,5 +109,44 @@ function (_) { return list; }; + p.getTable = function() { + var table = new TableModel(); + var self = this; + var i, j; + + if (self.series.length === 0) { + return table; + } + + _.each(self.series, function(series, seriesIndex) { + + if (seriesIndex === 0) { + table.columns.push({text: 'Time', type: 'time'}); + _.each(_.keys(series.tags), function(key) { + table.columns.push({text: key}); + }); + for (j = 1; j < series.columns.length; j++) { + table.columns.push({text: series.columns[j]}); + } + } + + if (series.values) { + for (i = 0; i < series.values.length; i++) { + var values = series.values[i]; + if (series.tags) { + for (var key in series.tags) { + if (series.tags.hasOwnProperty(key)) { + values.splice(1, 0, series.tags[key]); + } + } + } + table.rows.push(values); + } + } + }); + + return table; + }; + return InfluxSeries; }); diff --git a/public/app/plugins/datasource/influxdb/partials/query.editor.html b/public/app/plugins/datasource/influxdb/partials/query.editor.html index 0aeb8a44224..b4451c15eda 100644 --- a/public/app/plugins/datasource/influxdb/partials/query.editor.html +++ b/public/app/plugins/datasource/influxdb/partials/query.editor.html @@ -55,12 +55,12 @@ -
    -
    - +
    +
    +
    @@ -82,7 +82,7 @@
    • - GROUP BY + GROUP BY
    • @@ -103,6 +103,12 @@
    • +
    • + Format as +
    • +
    • + +
    diff --git a/public/app/plugins/datasource/influxdb/query_ctrl.js b/public/app/plugins/datasource/influxdb/query_ctrl.js index 38f87ecd84e..c3fd9f2db11 100644 --- a/public/app/plugins/datasource/influxdb/query_ctrl.js +++ b/public/app/plugins/datasource/influxdb/query_ctrl.js @@ -20,6 +20,10 @@ function (angular, _, InfluxQueryBuilder, InfluxQuery, queryPart) { $scope.queryModel = new InfluxQuery($scope.target); $scope.queryBuilder = new InfluxQueryBuilder($scope.target); $scope.groupBySegment = uiSegmentSrv.newPlusButton(); + $scope.resultFormats = [ + {text: 'Time series', value: 'time_series'}, + {text: 'Table', value: 'table'}, + ]; if (!$scope.target.measurement) { $scope.measurementSegment = uiSegmentSrv.newSelectMeasurement(); diff --git a/public/app/plugins/datasource/influxdb/query_part.ts b/public/app/plugins/datasource/influxdb/query_part.ts index 56ea7eecd23..b30480a4c9d 100644 --- a/public/app/plugins/datasource/influxdb/query_part.ts +++ b/public/app/plugins/datasource/influxdb/query_part.ts @@ -227,7 +227,7 @@ QueryPartDef.register({ type: 'derivative', addStrategy: addTransformationStrategy, category: categories.Transformations, - params: [{ name: "duration", type: "interval", options: ['1s', '10s', '1m', '5min', '10m', '15m', '1h']}], + params: [{ name: "duration", type: "interval", options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h']}], defaultParams: ['10s'], renderer: functionRenderer, }); @@ -236,7 +236,7 @@ QueryPartDef.register({ type: 'non_negative_derivative', addStrategy: addTransformationStrategy, category: categories.Transformations, - params: [{ name: "duration", type: "interval", options: ['1s', '10s', '1m', '5min', '10m', '15m', '1h']}], + params: [{ name: "duration", type: "interval", options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h']}], defaultParams: ['10s'], renderer: functionRenderer, }); diff --git a/public/app/plugins/datasource/influxdb/specs/influx_series_specs.ts b/public/app/plugins/datasource/influxdb/specs/influx_series_specs.ts index c8c127ed759..0e8e16a6114 100644 --- a/public/app/plugins/datasource/influxdb/specs/influx_series_specs.ts +++ b/public/app/plugins/datasource/influxdb/specs/influx_series_specs.ts @@ -186,5 +186,28 @@ describe('when generating timeseries from influxdb response', function() { }); }); + describe('given table response', function() { + var options = { + alias: '', + series: [ + { + name: 'app.prod.server1.count', + tags: {}, + columns: ['time', 'datacenter', 'value'], + values: [[1431946625000, 'America', 10], [1431946626000, 'EU', 12]] + } + ] + }; + + it('should return table', function() { + var series = new InfluxSeries(options); + var table = series.getTable(); + + expect(table.type).to.be('table'); + expect(table.columns.length).to.be(3); + expect(table.rows[0]).to.eql([1431946625000, 'America', 10]); + }); + }); + }); diff --git a/public/app/plugins/datasource/prometheus/datasource.js b/public/app/plugins/datasource/prometheus/datasource.js index 509b0ce9970..391affb0c8d 100644 --- a/public/app/plugins/datasource/prometheus/datasource.js +++ b/public/app/plugins/datasource/prometheus/datasource.js @@ -23,6 +23,7 @@ function (angular, _, moment, dateMath) { this.url = datasource.url; this.directUrl = datasource.directUrl; this.basicAuth = datasource.basicAuth; + this.withCredentials = datasource.withCredentials; this.lastErrors = {}; } @@ -32,8 +33,10 @@ function (angular, _, moment, dateMath) { method: method }; - if (this.basicAuth) { + if (this.basicAuth || this.withCredentials) { options.withCredentials = true; + } + if (this.basicAuth) { options.headers = { "Authorization": this.basicAuth }; diff --git a/public/app/plugins/panels/graph/legend.js b/public/app/plugins/panels/graph/legend.js index b41717ee792..4f4a0d8ee06 100644 --- a/public/app/plugins/panels/graph/legend.js +++ b/public/app/plugins/panels/graph/legend.js @@ -151,7 +151,7 @@ function (angular, _, $) { html += '
    '; html += '
    '; - html += '' + series.label + ''; + html += '' + _.escape(series.label) + ''; html += '
    '; if (panel.legend.values) { diff --git a/public/app/plugins/panels/table/controller.ts b/public/app/plugins/panels/table/controller.ts index 8a69cbff932..8116344f1de 100644 --- a/public/app/plugins/panels/table/controller.ts +++ b/public/app/plugins/panels/table/controller.ts @@ -5,7 +5,7 @@ import _ = require('lodash'); import moment = require('moment'); import PanelMeta = require('app/features/panel/panel_meta'); -import {TableModel} from './table_model'; +import {transformDataToTable} from './transformers'; export class TablePanelCtrl { @@ -26,7 +26,7 @@ export class TablePanelCtrl { var panelDefaults = { targets: [{}], - transform: 'timeseries_to_rows', + transform: 'timeseries_to_columns', pageSize: null, showHeader: true, styles: [ @@ -104,7 +104,23 @@ export class TablePanelCtrl { }; $scope.render = function() { - $scope.table = TableModel.transform($scope.dataRaw, $scope.panel); + // automatically correct transform mode + // based on data + if ($scope.dataRaw && $scope.dataRaw.length) { + if ($scope.dataRaw[0].type === 'table') { + $scope.panel.transform = 'table'; + } else { + if ($scope.dataRaw[0].type === 'docs') { + $scope.panel.transform = 'json'; + } else { + if ($scope.panel.transform === 'table' || $scope.panel.transform === 'json') { + $scope.panel.transform = 'timeseries_to_rows'; + } + } + } + } + + $scope.table = transformDataToTable($scope.dataRaw, $scope.panel); $scope.table.sort($scope.panel.sort); panelHelper.broadcastRender($scope, $scope.table, $scope.dataRaw); }; diff --git a/public/app/plugins/panels/table/editor.html b/public/app/plugins/panels/table/editor.html index 56b49a87567..0b13c61c884 100644 --- a/public/app/plugins/panels/table/editor.html +++ b/public/app/plugins/panels/table/editor.html @@ -132,6 +132,9 @@ +
  • + invert order +
  • diff --git a/public/app/plugins/panels/table/editor.ts b/public/app/plugins/panels/table/editor.ts index 1523eb2ef12..8d120856fdc 100644 --- a/public/app/plugins/panels/table/editor.ts +++ b/public/app/plugins/panels/table/editor.ts @@ -44,11 +44,17 @@ export class TablePanelEditorCtrl { }; $scope.addColumn = function() { - $scope.panel.columns.push({text: $scope.addColumnSegment.value, value: $scope.addColumnSegment.value}); - $scope.render(); + var columns = transformers[$scope.panel.transform].getColumns($scope.dataRaw); + var column = _.findWhere(columns, {text: $scope.addColumnSegment.value}); + + if (column) { + $scope.panel.columns.push(column); + $scope.render(); + } var plusButton = uiSegmentSrv.newPlusButton(); $scope.addColumnSegment.html = plusButton.html; + $scope.addColumnSegment.value = plusButton.value; }; $scope.transformChanged = function() { @@ -93,6 +99,15 @@ export class TablePanelEditorCtrl { return col.text; }); }; + + $scope.invertColorOrder = function(index) { + var ref = $scope.panel.styles[index].colors; + var copy = ref[0]; + ref[0] = ref[2]; + ref[2] = copy; + $scope.render(); + }; + } } diff --git a/public/app/plugins/panels/table/renderer.ts b/public/app/plugins/panels/table/renderer.ts index f45d5fcc5da..530fb367521 100644 --- a/public/app/plugins/panels/table/renderer.ts +++ b/public/app/plugins/panels/table/renderer.ts @@ -112,7 +112,7 @@ export class TableRenderer { // this hack adds header content to cell (not visible) var widthHack = ''; if (addWidthHack) { - widthHack = '
    ' + this.table.columns[columnIndex].text + '
    '; + widthHack = '
    ' + this.table.columns[columnIndex].text + '
    '; } return '' + value + widthHack + ''; diff --git a/public/app/plugins/panels/table/specs/renderer_specs.ts b/public/app/plugins/panels/table/specs/renderer_specs.ts index f8fdebb9ab0..f8af1baba17 100644 --- a/public/app/plugins/panels/table/specs/renderer_specs.ts +++ b/public/app/plugins/panels/table/specs/renderer_specs.ts @@ -1,6 +1,6 @@ import {describe, beforeEach, it, sinon, expect} from 'test/lib/common'; -import {TableModel} from '../table_model'; +import TableModel = require('app/core/table_model'); import {TableRenderer} from '../renderer'; describe('when rendering table', () => { diff --git a/public/app/plugins/panels/table/specs/table_model_specs.ts b/public/app/plugins/panels/table/specs/table_model_specs.ts index ad515835730..8b137891791 100644 --- a/public/app/plugins/panels/table/specs/table_model_specs.ts +++ b/public/app/plugins/panels/table/specs/table_model_specs.ts @@ -1,51 +1 @@ -import {describe, beforeEach, it, sinon, expect} from 'test/lib/common'; - -import {TableModel} from '../table_model'; - -describe('when sorting table desc', () => { - var table; - var panel = { - sort: {col: 0, desc: true}, - }; - - beforeEach(() => { - table = new TableModel(); - table.columns = [{}, {}]; - table.rows = [[100, 12], [105, 10], [103, 11]]; - table.sort(panel.sort); - }); - - it('should sort by time', () => { - expect(table.rows[0][0]).to.be(105); - expect(table.rows[1][0]).to.be(103); - expect(table.rows[2][0]).to.be(100); - }); - - it ('should mark column being sorted', () => { - expect(table.columns[0].sort).to.be(true); - expect(table.columns[0].desc).to.be(true); - }); - -}); - -describe('when sorting table asc', () => { - var table; - var panel = { - sort: {col: 1, desc: false}, - }; - - beforeEach(() => { - table = new TableModel(); - table.columns = [{}, {}]; - table.rows = [[100, 11], [105, 15], [103, 10]]; - table.sort(panel.sort); - }); - - it('should sort by time', () => { - expect(table.rows[0][1]).to.be(10); - expect(table.rows[1][1]).to.be(11); - expect(table.rows[2][1]).to.be(15); - }); - -}); diff --git a/public/app/plugins/panels/table/specs/transformers_specs.ts b/public/app/plugins/panels/table/specs/transformers_specs.ts index bb42b997d33..e3cdf44c8b2 100644 --- a/public/app/plugins/panels/table/specs/transformers_specs.ts +++ b/public/app/plugins/panels/table/specs/transformers_specs.ts @@ -1,7 +1,6 @@ import {describe, beforeEach, it, sinon, expect} from 'test/lib/common'; -import {TableModel} from '../table_model'; -import {transformers} from '../transformers'; +import {transformers, transformDataToTable} from '../transformers'; describe('when transforming time series table', () => { var table; @@ -26,7 +25,7 @@ describe('when transforming time series table', () => { }; beforeEach(() => { - table = TableModel.transform(timeSeries, panel); + table = transformDataToTable(timeSeries, panel); }); it('should return 3 rows', () => { @@ -51,7 +50,7 @@ describe('when transforming time series table', () => { }; beforeEach(() => { - table = TableModel.transform(timeSeries, panel); + table = transformDataToTable(timeSeries, panel); }); it ('should return 3 columns', () => { @@ -80,7 +79,7 @@ describe('when transforming time series table', () => { }; beforeEach(() => { - table = TableModel.transform(timeSeries, panel); + table = transformDataToTable(timeSeries, panel); }); it('should return 2 rows', () => { @@ -133,7 +132,7 @@ describe('when transforming time series table', () => { describe('transform', function() { beforeEach(() => { - table = TableModel.transform(rawData, panel); + table = transformDataToTable(rawData, panel); }); it ('should return 2 columns', () => { @@ -164,7 +163,7 @@ describe('when transforming time series table', () => { ]; beforeEach(() => { - table = TableModel.transform(rawData, panel); + table = transformDataToTable(rawData, panel); }); it ('should return 4 columns', () => { diff --git a/public/app/plugins/panels/table/transformers.ts b/public/app/plugins/panels/table/transformers.ts index 3845e8948aa..97650977f84 100644 --- a/public/app/plugins/panels/table/transformers.ts +++ b/public/app/plugins/panels/table/transformers.ts @@ -4,6 +4,7 @@ import moment = require('moment'); import _ = require('lodash'); import flatten = require('app/core/utils/flatten'); import TimeSeries = require('app/core/time_series'); +import TableModel = require('app/core/table_model'); var transformers = {}; @@ -136,6 +137,27 @@ transformers['annotations'] = { } }; +transformers['table'] = { + description: 'Table', + getColumns: function(data) { + if (!data || data.length === 0) { + return []; + } + }, + transform: function(data, panel, model) { + if (!data || data.length === 0) { + return; + } + + if (data[0].type !== 'table') { + throw {message: 'Query result is not in table format, try using another transform.'}; + } + + model.columns = data[0].columns; + model.rows = data[0].rows; + } +}; + transformers['json'] = { description: 'JSON Data', getColumns: function(data) { @@ -197,4 +219,20 @@ transformers['json'] = { } }; -export {transformers} +function transformDataToTable(data, panel) { + var model = new TableModel(); + + if (!data || data.length === 0) { + return model; + } + + var transformer = transformers[panel.transform]; + if (!transformer) { + throw {message: 'Transformer ' + panel.transformer + ' not found'}; + } + + transformer.transform(data, panel, model); + return model; +} + +export {transformers, transformDataToTable} diff --git a/public/app/require_config.js b/public/app/require_config.js index d9f8e90cad9..66165c40017 100644 --- a/public/app/require_config.js +++ b/public/app/require_config.js @@ -35,7 +35,6 @@ require.config({ modernizr: 'vendor/modernizr-2.6.1', 'bootstrap-tagsinput': 'vendor/tagsinput/bootstrap-tagsinput', - 'aws-sdk': 'vendor/aws-sdk/dist/aws-sdk.min', }, shim: { diff --git a/public/img/CopyQuery.png b/public/img/CopyQuery.png new file mode 100644 index 00000000000..b9829c23b2f Binary files /dev/null and b/public/img/CopyQuery.png differ diff --git a/public/img/critical.svg b/public/img/critical.svg new file mode 100644 index 00000000000..4056b12563b --- /dev/null +++ b/public/img/critical.svg @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/public/img/envelope.png b/public/img/envelope.png new file mode 100644 index 00000000000..59ef8a38aba Binary files /dev/null and b/public/img/envelope.png differ diff --git a/public/img/online.svg b/public/img/online.svg new file mode 100644 index 00000000000..279fbec3a90 --- /dev/null +++ b/public/img/online.svg @@ -0,0 +1,12 @@ + + + + + + diff --git a/public/img/warn-tiny.svg b/public/img/warn-tiny.svg new file mode 100644 index 00000000000..e8e91f4452c --- /dev/null +++ b/public/img/warn-tiny.svg @@ -0,0 +1,16 @@ + + + + + + + diff --git a/public/img/warn.svg b/public/img/warn.svg new file mode 100644 index 00000000000..8caee82c3f5 --- /dev/null +++ b/public/img/warn.svg @@ -0,0 +1,22 @@ + + + + + + + diff --git a/public/less/alerting.less b/public/less/alerting.less new file mode 100644 index 00000000000..9054584e23e --- /dev/null +++ b/public/less/alerting.less @@ -0,0 +1,42 @@ +.copy-query { + display: block; + width: 30px; + height: 36px; + margin: 0; + padding: 0; + border: 0; + background: transparent url(/img/CopyQuery.png) 50% 50% no-repeat; + cursor: pointer; +} + +.alert-state { + display: inline-block; + padding-left: 30px; + background: 0 50% no-repeat; + background-size: 20px auto; +} + +.alert-state-online { + background-image: url('/img/online.svg'); +} + +.alert-state-warning { + background-image: url('/img/warn-tiny.svg'); +} + +.alert-state-critical { + background-image: url('/img/critical.svg'); +} + +.alert-notify-emails { + width: 400px; + border-right: 1px solid @black; +} + +.alert-notify-emails .bootstrap-tagsinput { + width: 394px; // offset for 8px left padding and border width +} + +.alert-notify-emails .bootstrap-tagsinput input { + border: 0; +} diff --git a/public/less/filter-controls.less b/public/less/filter-controls.less new file mode 100644 index 00000000000..287bebe3757 --- /dev/null +++ b/public/less/filter-controls.less @@ -0,0 +1,37 @@ +// ========================================================================== +// FILTER CONTROLS +// ========================================================================== + + + +// Filters +// -------------------------------------------------------------------------- + +.filter-controls-filters { + display: inline-block; + margin-bottom: 40px; +} + +.tab-pane > .filter-controls-filters { + margin-top: 20px; +} + + + +// Actions +// -------------------------------------------------------------------------- + +.filter-controls-actions { + margin: 0 0 10px; + padding: 0; + list-style: none; +} + +.filter-controls-actions > li { + display: inline-block; + margin-right: 10px; +} + +.filter-controls-actions-selected { + text-transform: uppercase; +} diff --git a/public/less/filter-list.less b/public/less/filter-list.less new file mode 100644 index 00000000000..181aedf4d3c --- /dev/null +++ b/public/less/filter-list.less @@ -0,0 +1,136 @@ +// ========================================================================== +// FILTER LIST +// ========================================================================== + + + +// List +// -------------------------------------------------------------------------- + +.filter-list { + margin: 0; + padding: 0; + list-style: none; +} + +.filter-list > li { + padding: 10px; + margin-bottom: 2px; + background: @grafanaPanelBackground; + + &:last-child { + margin: 0; + } +} + + + +// Card +// -------------------------------------------------------------------------- + +.filter-list-card { + display: table; + width: 100%; + margin: 0; + padding: 0; + list-style: none; +} + +.filter-list-card > li { + display: table-cell; + vertical-align: top; +} + +.filter-list-card-select { + width: 23px; + padding-right: 5px; +} + +.filter-list-card-title { + display: block; + font-size: 16px; + font-weight: normal; +} + +.filter-list-card-status { + color: #777; + font-size: 12px; +} + +.filter-list-card-state { + display: inline-block; + padding: 5px 0 0 28px; + background: 0 bottom no-repeat; + background-size: 24px auto; + font-size: 14px; + text-transform: uppercase; + + &.online { + background-image: url('/img/online.svg'); + color: @online; + } + + &.warn { + background-image: url('/img/warn-tiny.svg'); + color: @warn; + } + + &.critical { + background-image: url('/img/critical.svg'); + color: @critical; + } +} + +.filter-list-card-controls { + float: right; +} + +.filter-list-card-links, +.filter-list-card-config, +.filter-list-card-expand { + display: inline-block; + vertical-align: middle; +} + +.filter-list-card-link { + display: block; + color: #777; + text-align: right; + + > a { + color: #777; + } +} + +.filter-list-card-config { + padding: 8px 8px 8px 16px; + color: #777; + font-size: 25px; + + > a { + color: #777; + } +} + +.filter-list-card-expand { + width: 20px; + padding: 8px 0 8px 8px; + color: #aaa; + font-size: 30px; + text-align: center; + cursor: pointer; +} + +.filter-list-card-details { + padding: 20px 0 0 30px; +} + +.filter-list-card-details-heading { + font-weight: normal; + + > a { + float: right; + color: @blue; + font-size: 12px; + } +} diff --git a/public/less/filter-table.less b/public/less/filter-table.less new file mode 100644 index 00000000000..6729b425e51 --- /dev/null +++ b/public/less/filter-table.less @@ -0,0 +1,57 @@ +// ========================================================================== +// FILTER TABLE +// ========================================================================== + + + +// Table +// -------------------------------------------------------------------------- + +.filter-table * { + box-sizing: border-box; +} + +.filter-table { + width: 100%; + border-collapse: collapse; + // table-layout: fixed; +} + +.filter-table tr { + background: @grafanaPanelBackground; + border-bottom: 2px solid @bodyBackground; +} + +.filter-table th { + width: auto; + padding: 10px 15px 10px 0; + text-align: left; + + &:first-child { + padding-left: 15px; + } +} + +.filter-table td { + padding: 15px 15px 15px 0; + + &:first-child { + padding-left: 15px; + } +} + +.filter-table .ellipsis { + display: block; + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.filter-table .expanded { + border-color: @grafanaPanelBackground; +} + +.filter-table .expanded > td { + padding-bottom: 0; +} diff --git a/public/less/gfbox.less b/public/less/gfbox.less index eb386f740d7..46967d143c5 100644 --- a/public/less/gfbox.less +++ b/public/less/gfbox.less @@ -25,6 +25,13 @@ } } +.gf-box-header-save-btn { + padding: 7px 0; + float: right; + color: @grayLight; + font-style: italic; +} + .gf-box-body { padding: 20px; min-height: 150px; @@ -73,6 +80,10 @@ max-width: 653px; } +.page-wide { + max-width: 1000px; +} + .admin-page { max-width: 800px; margin-left: 10px; diff --git a/public/less/grafana.less b/public/less/grafana.less index 041c0112481..dcd6b4b734d 100644 --- a/public/less/grafana.less +++ b/public/less/grafana.less @@ -20,6 +20,10 @@ @import "fonts.less"; @import "tabs.less"; @import "timepicker.less"; +@import "alerting.less"; +@import "filter-controls.less"; +@import "filter-list.less"; +@import "filter-table.less"; .row-control-inner { padding:0px; diff --git a/public/less/overrides.less b/public/less/overrides.less index fb6544cd99c..291a9a765f4 100644 --- a/public/less/overrides.less +++ b/public/less/overrides.less @@ -61,7 +61,7 @@ } code, pre { - background-color: @grayLighter; + background-color: @codeTagBackground; } div.editor-row { @@ -560,6 +560,15 @@ div.flot-text { background-color: darken(@purple, 10%); } +.label-tag-email { + padding-left: 25px; + background: @black url(/img/envelope.png) 5px 50% no-repeat !important; + border-color: @black !important; + font-size: 12px; + font-weight: normal; + border-radius: 5px; +} + // inspector .inspector-request-table { @@ -578,8 +587,10 @@ div.flot-text { // pre code, pre { - background-color: @grafanaPanelBackground; + background-color: @codeTagBackground; color: @textColor; + border: 1px solid darken(@codeTagBackground, 15%); + padding: 2px; } .dropdown-menu { diff --git a/public/less/tightform.less b/public/less/tightform.less index 494497653ed..b46456e74c2 100644 --- a/public/less/tightform.less +++ b/public/less/tightform.less @@ -59,6 +59,11 @@ } } +.tight-form-flex-wrapper { + display: flex; + flex-direction: row; +} + .grafana-metric-options { margin-top: 25px; } @@ -156,6 +161,12 @@ input[type=checkbox].tight-form-checkbox { margin: 0; } +.tight-form-textarea { + height: 200px; + margin: 0; + box-sizing: border-box; +} + select.tight-form-input { border: none; border-right: 1px solid @grafanaTargetSegmentBorder; diff --git a/public/less/variables.dark.less b/public/less/variables.dark.less index 3324c3f4b86..f2b4cba1706 100644 --- a/public/less/variables.dark.less +++ b/public/less/variables.dark.less @@ -25,6 +25,12 @@ @purple: #9933CC; @variable: #32D1DF; +// Status colors +// ------------------------- +@online: #10a345; +@warn: #ffc03c; +@critical: #ed2e18; + // grafana Variables // ------------------------- @grafanaPanelBackground: @grayDarker; @@ -42,6 +48,7 @@ @grafanaTargetFuncHightlight: #444; @modalBackground: @black; +@codeTagBackground: #444; // Scaffolding // ------------------------- diff --git a/public/less/variables.light.less b/public/less/variables.light.less index f9063c4cd74..d4c0cd18998 100644 --- a/public/less/variables.light.less +++ b/public/less/variables.light.less @@ -31,6 +31,12 @@ @purple: #9954BB; @variable: #2AB2E4; +// Status colors +// ------------------------- +@online: #10a345; +@warn: #ffc03c; +@critical: #ed2e18; + // grafana Variables // ------------------------- @grafanaPanelBackground: @grayLighter; @@ -55,6 +61,7 @@ @grafanaTargetFuncHightlight: darken(@grafanaTargetBackground, 10%); @modalBackground: @bodyBackground; +@codeTagBackground: #ddd; // Scaffolding // ------------------------- diff --git a/public/test/core/table_model_specs.ts b/public/test/core/table_model_specs.ts new file mode 100644 index 00000000000..8cdeb04f8d0 --- /dev/null +++ b/public/test/core/table_model_specs.ts @@ -0,0 +1,51 @@ +import {describe, beforeEach, it, sinon, expect} from 'test/lib/common'; + +import TableModel = require('app/core/table_model'); + +describe('when sorting table desc', () => { + var table; + var panel = { + sort: {col: 0, desc: true}, + }; + + beforeEach(() => { + table = new TableModel(); + table.columns = [{}, {}]; + table.rows = [[100, 12], [105, 10], [103, 11]]; + table.sort(panel.sort); + }); + + it('should sort by time', () => { + expect(table.rows[0][0]).to.be(105); + expect(table.rows[1][0]).to.be(103); + expect(table.rows[2][0]).to.be(100); + }); + + it ('should mark column being sorted', () => { + expect(table.columns[0].sort).to.be(true); + expect(table.columns[0].desc).to.be(true); + }); + +}); + +describe('when sorting table asc', () => { + var table; + var panel = { + sort: {col: 1, desc: false}, + }; + + beforeEach(() => { + table = new TableModel(); + table.columns = [{}, {}]; + table.rows = [[100, 11], [105, 15], [103, 10]]; + table.sort(panel.sort); + }); + + it('should sort by time', () => { + expect(table.rows[0][1]).to.be(10); + expect(table.rows[1][1]).to.be(11); + expect(table.rows[2][1]).to.be(15); + }); + +}); + diff --git a/public/test/core/utils/rangeutil_specs.ts b/public/test/core/utils/rangeutil_specs.ts index 32286764c20..b1472251ff6 100644 --- a/public/test/core/utils/rangeutil_specs.ts +++ b/public/test/core/utils/rangeutil_specs.ts @@ -41,12 +41,12 @@ describe("rangeUtil", () => { it('should handle now/d', () => { var info = rangeUtil.describeTextRange('now/d'); - expect(info.display).to.be('The day so far'); + expect(info.display).to.be('Today so far'); }); it('should handle now/w', () => { var info = rangeUtil.describeTextRange('now/w'); - expect(info.display).to.be('Week to date'); + expect(info.display).to.be('This week so far'); }); }); diff --git a/public/test/mocks/dashboard-mock.js b/public/test/mocks/dashboard-mock.js index 6367093bd36..9b61108728e 100644 --- a/public/test/mocks/dashboard-mock.js +++ b/public/test/mocks/dashboard-mock.js @@ -15,7 +15,7 @@ define([], rows: [], pulldowns: [ { type: 'templating' }, { type: 'annotations' } ], nav: [ { type: 'timepicker' } ], - time: {from: '1h', to: 'now'}, + time: {from: 'now-6h', to: 'now'}, templating: { list: [] }, diff --git a/public/test/specs/time_srv_specs.js b/public/test/specs/time_srv_specs.js index 9943aae6cc3..60bdd1a4c5b 100644 --- a/public/test/specs/time_srv_specs.js +++ b/public/test/specs/time_srv_specs.js @@ -75,6 +75,14 @@ define([ expect(time.to.valueOf()).to.equal(1410337665699); }); + it('should handle bad dates', function() { + ctx.$routeParams.from = '20151126T00010%3C%2Fp%3E%3Cspan%20class'; + ctx.$routeParams.to = 'now'; + _dashboard.time.from = 'now-6h'; + ctx.service.init(_dashboard); + expect(ctx.service.time.from).to.equal('now-6h'); + expect(ctx.service.time.to).to.equal('now'); + }); }); describe('setTime', function() { diff --git a/public/vendor/flot/jquery.flot.events.js b/public/vendor/flot/jquery.flot.events.js index 48ebf2b67fd..6d8a0cf3c42 100644 --- a/public/vendor/flot/jquery.flot.events.js +++ b/public/vendor/flot/jquery.flot.events.js @@ -50,7 +50,7 @@ height: 10 }; - var _events = [], _types, _eventsEnabled = false, lastRange; + var _events = [], _types, _eventsEnabled = false; plot.getEvents = function(){ return _events; @@ -94,8 +94,6 @@ // check for first run if (_events.length < 1) { - _lastRange = xaxis.max - xaxis.min; - // check for clustering if (options.events.clustering) { var ed = _clusterEvents(options.events.types, options.events.data, xaxis.max - xaxis.min); diff --git a/tasks/options/clean.js b/tasks/options/clean.js index 49f09f10e7c..331864082dc 100644 --- a/tasks/options/clean.js +++ b/tasks/options/clean.js @@ -4,6 +4,7 @@ module.exports = function(config) { return { release: ['<%= destDir %>', '<%= tempDir %>', '<%= genDir %>'], gen: ['<%= genDir %>'], - temp: ['<%= tempDir %>'] + temp: ['<%= tempDir %>'], + css: ['<%= genDir %>/css'] }; }; diff --git a/tasks/options/watch.js b/tasks/options/watch.js index 0341b444c95..2be128a3112 100644 --- a/tasks/options/watch.js +++ b/tasks/options/watch.js @@ -1,39 +1,48 @@ -module.exports = function(config) { +module.exports = function(config, grunt) { 'use strict'; - return { - // css: { - // files: [ '<%= srcDir %>/less#<{(||)}>#*.less' ], - // tasks: ['css'], - // options: { - // spawn: false - // } - // }, + grunt.event.on('watch', function(action, filepath) { + var newPath; + grunt.log.writeln('File Changed: ' + filepath); + + if (/(\.html)$/.test(filepath)) { + newPath = filepath.replace(/^public/, 'public_gen'); + grunt.log.writeln('Copying to ' + newPath); + grunt.file.copy(filepath, newPath); + } + + if (/(\.js)$/.test(filepath)) { + newPath = filepath.replace(/^public/, 'public_gen'); + grunt.log.writeln('Copying to ' + newPath); + grunt.file.copy(filepath, newPath); + + grunt.task.run('jshint'); + grunt.task.run('jscs'); + } + + if (/(\.less)$/.test(filepath)) { + grunt.task.run('clean:css'); + grunt.task.run('css'); + } + + if (/(\.ts)$/.test(filepath)) { + //changes changed file source to that of the changed file + var option = 'typescript.build.src'; + var result = filepath; + grunt.config(option, result); + grunt.task.run('typescript:build'); + grunt.task.run('tslint'); + } + }); + + return { copy_to_gen: { files: ['<%= srcDir %>/**/*'], - tasks: [ - 'clean:gen', - 'copy:public_to_gen', - 'css', - 'typescript:build', - 'jshint', - 'jscs', - 'tslint', - 'karma:test' - ], + tasks: [], options: { spawn: false } }, - - // typescript: { - // files: ['<%= srcDir %>/app#<{(||)}>#*.ts', '<%= srcDir %>/test#<{(||)}>#*.ts'], - // tasks: ['tslint', 'typescript:build'], - // options: { - // spawn: false - // } - // } - }; };