Elasticsearch: use semver strings to identify ES version (#33646)

* Elasticsearch: use proper semver strings to identify ES version

* Update BE & tests

* refactor BE tests

* refactor isValidOption check

* update test

* Update pkg/tsdb/elasticsearch/client/client.go

Co-authored-by: Piotr Jamróz <pm.jamroz@gmail.com>

* Update pkg/tsdb/elasticsearch/client/search_request_test.go

Co-authored-by: Piotr Jamróz <pm.jamroz@gmail.com>

* Remove leftover FIXME comment

* Add new test cases for new version format

* Docs: add documentation about version dropdown

* Update docs/sources/datasources/elasticsearch.md

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Update docs/sources/datasources/elasticsearch.md

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Update docs/sources/datasources/elasticsearch.md

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Update provisioning documentation

Co-authored-by: Piotr Jamróz <pm.jamroz@gmail.com>
Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
This commit is contained in:
Giordano Ricci
2021-05-11 09:44:00 +01:00
committed by GitHub
co-authored by Piotr Jamróz achatterjee-grafana
parent 1a504ce673
commit e98a8bd11b
26 changed files with 363 additions and 242 deletions
+1 -1
View File
@@ -154,7 +154,7 @@ Since not all datasources have the same configuration settings we only have the
| maxSeries | number | Influxdb | Max number of series/tables that Grafana processes | | maxSeries | number | Influxdb | Max number of series/tables that Grafana processes |
| httpMethod | string | Prometheus | HTTP Method. 'GET', 'POST', defaults to GET | | httpMethod | string | Prometheus | HTTP Method. 'GET', 'POST', defaults to GET |
| customQueryParameters | string | Prometheus | Query parameters to add, as a URL-encoded string. | | customQueryParameters | string | Prometheus | Query parameters to add, as a URL-encoded string. |
| esVersion | number | Elasticsearch | Elasticsearch version as a number (2/5/56/60/70) | | esVersion | string | Elasticsearch | Elasticsearch version (E.g. `7.0.0`, `7.6.1`) |
| timeField | string | Elasticsearch | Which field that should be used as timestamp | | timeField | string | Elasticsearch | Which field that should be used as timestamp |
| interval | string | Elasticsearch | Index date time format. nil(No Pattern), 'Hourly', 'Daily', 'Weekly', 'Monthly' or 'Yearly' | | interval | string | Elasticsearch | Index date time format. nil(No Pattern), 'Hourly', 'Daily', 'Weekly', 'Monthly' or 'Yearly' |
| logMessageField | string | Elasticsearch | Which field should be used as the log message | | logMessageField | string | Elasticsearch | Which field should be used as the log message |
+6 -3
View File
@@ -56,9 +56,12 @@ a time pattern for the index name or a wildcard.
### Elasticsearch version ### Elasticsearch version
Be sure to specify your Elasticsearch version in the version selection dropdown. This is very important as there are differences on how queries are composed. Select the version of your Elasticsearch data source from the version selection dropdown. Different query compositions and functionalities are available in the query editor for different versions.
Currently the versions available are `2.x`, `5.x`, `5.6+`, `6.0+` or `7.0+`. The value `5.6+` means version 5.6 or higher, but lower than 6.0. The value `6.0+` means Available Elasticsearch versions are `2.x`, `5.x`, `5.6+`, `6.0+`, and `7.0+`. Select the option that best matches your data source version.
version 6.0 or higher, but lower than 7.0. Finally, `7.0+` means version 7.0 or higher, but lower than 8.0.
Grafana assumes that you are running the lowest possible version for a specified range. This ensures that new features or breaking changes in a future Elasticsearch release will not affect your configuration.
For example, suppose you are running Elasticsearch `7.6.1` and you selected `7.0+`. If a new feature is made available for Elasticsearch `7.5.0` or newer releases, then a `7.5+` option will be available. However, your configuration will not be affected until you explicitly select the new `7.5+` option in your settings.
### Min time interval ### Min time interval
+1
View File
@@ -15,6 +15,7 @@ require (
cloud.google.com/go/storage v1.14.0 cloud.google.com/go/storage v1.14.0
cuelang.org/go v0.3.2 cuelang.org/go v0.3.2
github.com/BurntSushi/toml v0.3.1 github.com/BurntSushi/toml v0.3.1
github.com/Masterminds/semver v1.5.0
github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f github.com/VividCortex/mysqlerr v0.0.0-20170204212430-6c6b55f8796f
github.com/aws/aws-sdk-go v1.38.34 github.com/aws/aws-sdk-go v1.38.34
github.com/beevik/etree v1.1.0 github.com/beevik/etree v1.1.0
+2
View File
@@ -143,6 +143,8 @@ github.com/HdrHistogram/hdrhistogram-go v1.0.1/go.mod h1:BWJ+nMSHY3L41Zj7CA3uXnl
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk=
github.com/Masterminds/squirrel v0.0.0-20161115235646-20f192218cf5/go.mod h1:xnKTFzjGUiZtiOagBsfnvomW+nJg2usB1ZpordQWqNM= github.com/Masterminds/squirrel v0.0.0-20161115235646-20f192218cf5/go.mod h1:xnKTFzjGUiZtiOagBsfnvomW+nJg2usB1ZpordQWqNM=
+2
View File
@@ -123,6 +123,7 @@
"@types/redux-logger": "3.0.7", "@types/redux-logger": "3.0.7",
"@types/redux-mock-store": "1.0.2", "@types/redux-mock-store": "1.0.2",
"@types/reselect": "2.2.0", "@types/reselect": "2.2.0",
"@types/semver": "^6.0.0",
"@types/slate": "0.47.1", "@types/slate": "0.47.1",
"@types/slate-plain-serializer": "0.6.1", "@types/slate-plain-serializer": "0.6.1",
"@types/slate-react": "0.22.5", "@types/slate-react": "0.22.5",
@@ -302,6 +303,7 @@
"rst2html": "github:thoward/rst2html#990cb89", "rst2html": "github:thoward/rst2html#990cb89",
"rxjs": "6.6.3", "rxjs": "6.6.3",
"search-query-parser": "1.5.4", "search-query-parser": "1.5.4",
"semver": "^7.1.3",
"slate": "0.47.8", "slate": "0.47.8",
"slate-plain-serializer": "0.7.10", "slate-plain-serializer": "0.7.10",
"tether": "1.4.7", "tether": "1.4.7",
+51 -24
View File
@@ -13,6 +13,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/Masterminds/semver"
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/tsdb/interval" "github.com/grafana/grafana/pkg/tsdb/interval"
@@ -34,7 +35,7 @@ var newDatasourceHttpClient = func(ds *models.DataSource) (*http.Client, error)
// Client represents a client which can interact with elasticsearch api // Client represents a client which can interact with elasticsearch api
type Client interface { type Client interface {
GetVersion() int GetVersion() *semver.Version
GetTimeField() string GetTimeField() string
GetMinInterval(queryInterval string) (time.Duration, error) GetMinInterval(queryInterval string) (time.Duration, error)
ExecuteMultisearch(r *MultiSearchRequest) (*MultiSearchResponse, error) ExecuteMultisearch(r *MultiSearchRequest) (*MultiSearchResponse, error)
@@ -42,9 +43,38 @@ type Client interface {
EnableDebug() EnableDebug()
} }
func coerceVersion(v *simplejson.Json) (*semver.Version, error) {
versionString, err := v.String()
if err != nil {
versionNumber, err := v.Int()
if err != nil {
return nil, err
}
switch versionNumber {
case 2:
return semver.NewVersion("2.0.0")
case 5:
return semver.NewVersion("5.0.0")
case 56:
return semver.NewVersion("5.6.0")
case 60:
return semver.NewVersion("6.0.0")
case 70:
return semver.NewVersion("7.0.0")
default:
return nil, fmt.Errorf("elasticsearch version=%d is not supported", versionNumber)
}
}
return semver.NewVersion(versionString)
}
// NewClient creates a new elasticsearch client // NewClient creates a new elasticsearch client
var NewClient = func(ctx context.Context, ds *models.DataSource, timeRange plugins.DataTimeRange) (Client, error) { var NewClient = func(ctx context.Context, ds *models.DataSource, timeRange plugins.DataTimeRange) (Client, error) {
version, err := ds.JsonData.Get("esVersion").Int() version, err := coerceVersion(ds.JsonData.Get("esVersion"))
if err != nil { if err != nil {
return nil, fmt.Errorf("elasticsearch version is required, err=%v", err) return nil, fmt.Errorf("elasticsearch version is required, err=%v", err)
} }
@@ -65,34 +95,29 @@ var NewClient = func(ctx context.Context, ds *models.DataSource, timeRange plugi
return nil, err return nil, err
} }
clientLog.Debug("Creating new client", "version", version, "timeField", timeField, "indices", strings.Join(indices, ", ")) clientLog.Info("Creating new client", "version", version.String(), "timeField", timeField, "indices", strings.Join(indices, ", "))
switch version { return &baseClientImpl{
case 2, 5, 56, 60, 70: ctx: ctx,
return &baseClientImpl{ ds: ds,
ctx: ctx, version: version,
ds: ds, timeField: timeField,
version: version, indices: indices,
timeField: timeField, timeRange: timeRange,
indices: indices, }, nil
timeRange: timeRange,
}, nil
}
return nil, fmt.Errorf("elasticsearch version=%d is not supported", version)
} }
type baseClientImpl struct { type baseClientImpl struct {
ctx context.Context ctx context.Context
ds *models.DataSource ds *models.DataSource
version int version *semver.Version
timeField string timeField string
indices []string indices []string
timeRange plugins.DataTimeRange timeRange plugins.DataTimeRange
debugEnabled bool debugEnabled bool
} }
func (c *baseClientImpl) GetVersion() int { func (c *baseClientImpl) GetVersion() *semver.Version {
return c.version return c.version
} }
@@ -297,13 +322,15 @@ func (c *baseClientImpl) createMultiSearchRequests(searchRequests []*SearchReque
interval: searchReq.Interval, interval: searchReq.Interval,
} }
if c.version == 2 { if c.version.Major() < 5 {
mr.header["search_type"] = "count" mr.header["search_type"] = "count"
} } else {
allowedVersionRange, _ := semver.NewConstraint(">=5.6.0, <7.0.0")
if c.version >= 56 && c.version < 70 { if allowedVersionRange.Check(c.version) {
maxConcurrentShardRequests := c.getSettings().Get("maxConcurrentShardRequests").MustInt(256) maxConcurrentShardRequests := c.getSettings().Get("maxConcurrentShardRequests").MustInt(256)
mr.header["max_concurrent_shard_requests"] = maxConcurrentShardRequests mr.header["max_concurrent_shard_requests"] = maxConcurrentShardRequests
}
} }
multiRequests = append(multiRequests, &mr) multiRequests = append(multiRequests, &mr)
@@ -313,7 +340,7 @@ func (c *baseClientImpl) createMultiSearchRequests(searchRequests []*SearchReque
} }
func (c *baseClientImpl) getMultiSearchQueryParameters() string { func (c *baseClientImpl) getMultiSearchQueryParameters() string {
if c.version >= 70 { if c.version.Major() >= 7 {
maxConcurrentShardRequests := c.getSettings().Get("maxConcurrentShardRequests").MustInt(5) maxConcurrentShardRequests := c.getSettings().Get("maxConcurrentShardRequests").MustInt(5)
return fmt.Sprintf("max_concurrent_shard_requests=%d", maxConcurrentShardRequests) return fmt.Sprintf("max_concurrent_shard_requests=%d", maxConcurrentShardRequests)
} }
+96 -67
View File
@@ -39,10 +39,104 @@ func TestNewClient(t *testing.T) {
require.Error(t, err) require.Error(t, err)
}) })
t.Run("When unsupported version set should return error", func(t *testing.T) { t.Run("When using legacy version numbers", func(t *testing.T) {
t.Run("When unsupported version set should return error", func(t *testing.T) {
ds := &models.DataSource{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"esVersion": 6,
"timeField": "@timestamp",
}),
}
_, err := NewClient(context.Background(), ds, plugins.DataTimeRange{})
require.Error(t, err)
})
t.Run("When version 2 should return v2 client", func(t *testing.T) {
ds := &models.DataSource{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"esVersion": 2,
"timeField": "@timestamp",
}),
}
c, err := NewClient(context.Background(), ds, plugins.DataTimeRange{})
require.NoError(t, err)
assert.Equal(t, "2.0.0", c.GetVersion().String())
})
t.Run("When version 5 should return v5 client", func(t *testing.T) {
ds := &models.DataSource{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"esVersion": 5,
"timeField": "@timestamp",
}),
}
c, err := NewClient(context.Background(), ds, plugins.DataTimeRange{})
require.NoError(t, err)
assert.Equal(t, "5.0.0", c.GetVersion().String())
})
t.Run("When version 56 should return v5.6 client", func(t *testing.T) {
ds := &models.DataSource{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"esVersion": 56,
"timeField": "@timestamp",
}),
}
c, err := NewClient(context.Background(), ds, plugins.DataTimeRange{})
require.NoError(t, err)
assert.Equal(t, "5.6.0", c.GetVersion().String())
})
t.Run("When version 60 should return v6.0 client", func(t *testing.T) {
ds := &models.DataSource{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"esVersion": 60,
"timeField": "@timestamp",
}),
}
c, err := NewClient(context.Background(), ds, plugins.DataTimeRange{})
require.NoError(t, err)
assert.Equal(t, "6.0.0", c.GetVersion().String())
})
t.Run("When version 70 should return v7.0 client", func(t *testing.T) {
ds := &models.DataSource{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"esVersion": 70,
"timeField": "@timestamp",
}),
}
c, err := NewClient(context.Background(), ds, plugins.DataTimeRange{})
require.NoError(t, err)
assert.Equal(t, "7.0.0", c.GetVersion().String())
})
})
t.Run("When version is a valid semver string should create a client", func(t *testing.T) {
version := "7.2.4"
ds := &models.DataSource{ ds := &models.DataSource{
JsonData: simplejson.NewFromAny(map[string]interface{}{ JsonData: simplejson.NewFromAny(map[string]interface{}{
"esVersion": 6, "esVersion": version,
"timeField": "@timestamp",
}),
}
c, err := NewClient(context.Background(), ds, plugins.DataTimeRange{})
require.NoError(t, err)
assert.Equal(t, version, c.GetVersion().String())
})
t.Run("When version is NOT a valid semver string should return error", func(t *testing.T) {
version := "7.NOT_VALID.4"
ds := &models.DataSource{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"esVersion": version,
"timeField": "@timestamp", "timeField": "@timestamp",
}), }),
} }
@@ -50,71 +144,6 @@ func TestNewClient(t *testing.T) {
_, err := NewClient(context.Background(), ds, plugins.DataTimeRange{}) _, err := NewClient(context.Background(), ds, plugins.DataTimeRange{})
require.Error(t, err) require.Error(t, err)
}) })
t.Run("When version 2 should return v2 client", func(t *testing.T) {
ds := &models.DataSource{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"esVersion": 2,
"timeField": "@timestamp",
}),
}
c, err := NewClient(context.Background(), ds, plugins.DataTimeRange{})
require.NoError(t, err)
assert.Equal(t, 2, c.GetVersion())
})
t.Run("When version 5 should return v5 client", func(t *testing.T) {
ds := &models.DataSource{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"esVersion": 5,
"timeField": "@timestamp",
}),
}
c, err := NewClient(context.Background(), ds, plugins.DataTimeRange{})
require.NoError(t, err)
assert.Equal(t, 5, c.GetVersion())
})
t.Run("When version 56 should return v5.6 client", func(t *testing.T) {
ds := &models.DataSource{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"esVersion": 56,
"timeField": "@timestamp",
}),
}
c, err := NewClient(context.Background(), ds, plugins.DataTimeRange{})
require.NoError(t, err)
assert.Equal(t, 56, c.GetVersion())
})
t.Run("When version 60 should return v6.0 client", func(t *testing.T) {
ds := &models.DataSource{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"esVersion": 60,
"timeField": "@timestamp",
}),
}
c, err := NewClient(context.Background(), ds, plugins.DataTimeRange{})
require.NoError(t, err)
assert.Equal(t, 60, c.GetVersion())
})
t.Run("When version 70 should return v7.0 client", func(t *testing.T) {
ds := &models.DataSource{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"esVersion": 70,
"timeField": "@timestamp",
}),
}
c, err := NewClient(context.Background(), ds, plugins.DataTimeRange{})
require.NoError(t, err)
assert.Equal(t, 70, c.GetVersion())
})
} }
func TestClient_ExecuteMultisearch(t *testing.T) { func TestClient_ExecuteMultisearch(t *testing.T) {
+11 -13
View File
@@ -3,12 +3,13 @@ package es
import ( import (
"strings" "strings"
"github.com/Masterminds/semver"
"github.com/grafana/grafana/pkg/tsdb/interval" "github.com/grafana/grafana/pkg/tsdb/interval"
) )
// SearchRequestBuilder represents a builder which can build a search request // SearchRequestBuilder represents a builder which can build a search request
type SearchRequestBuilder struct { type SearchRequestBuilder struct {
version int version *semver.Version
interval interval.Interval interval interval.Interval
index string index string
size int size int
@@ -19,7 +20,7 @@ type SearchRequestBuilder struct {
} }
// NewSearchRequestBuilder create a new search request builder // NewSearchRequestBuilder create a new search request builder
func NewSearchRequestBuilder(version int, interval interval.Interval) *SearchRequestBuilder { func NewSearchRequestBuilder(version *semver.Version, interval interval.Interval) *SearchRequestBuilder {
builder := &SearchRequestBuilder{ builder := &SearchRequestBuilder{
version: version, version: version,
interval: interval, interval: interval,
@@ -87,18 +88,15 @@ func (b *SearchRequestBuilder) SortDesc(field, unmappedType string) *SearchReque
// AddDocValueField adds a doc value field to the search request // AddDocValueField adds a doc value field to the search request
func (b *SearchRequestBuilder) AddDocValueField(field string) *SearchRequestBuilder { func (b *SearchRequestBuilder) AddDocValueField(field string) *SearchRequestBuilder {
// fields field not supported on version >= 5 // fields field not supported on version >= 5
if b.version < 5 { if b.version.Major() < 5 {
b.customProps["fields"] = []string{"*", "_source"} b.customProps["fields"] = []string{"*", "_source"}
}
b.customProps["script_fields"] = make(map[string]interface{})
if b.version < 5 {
b.customProps["fielddata_fields"] = []string{field} b.customProps["fielddata_fields"] = []string{field}
} else { } else {
b.customProps["docvalue_fields"] = []string{field} b.customProps["docvalue_fields"] = []string{field}
} }
b.customProps["script_fields"] = make(map[string]interface{})
return b return b
} }
@@ -119,12 +117,12 @@ func (b *SearchRequestBuilder) Agg() AggBuilder {
// MultiSearchRequestBuilder represents a builder which can build a multi search request // MultiSearchRequestBuilder represents a builder which can build a multi search request
type MultiSearchRequestBuilder struct { type MultiSearchRequestBuilder struct {
version int version *semver.Version
requestBuilders []*SearchRequestBuilder requestBuilders []*SearchRequestBuilder
} }
// NewMultiSearchRequestBuilder creates a new multi search request builder // NewMultiSearchRequestBuilder creates a new multi search request builder
func NewMultiSearchRequestBuilder(version int) *MultiSearchRequestBuilder { func NewMultiSearchRequestBuilder(version *semver.Version) *MultiSearchRequestBuilder {
return &MultiSearchRequestBuilder{ return &MultiSearchRequestBuilder{
version: version, version: version,
} }
@@ -275,10 +273,10 @@ type AggBuilder interface {
type aggBuilderImpl struct { type aggBuilderImpl struct {
AggBuilder AggBuilder
aggDefs []*aggDef aggDefs []*aggDef
version int version *semver.Version
} }
func newAggBuilder(version int) *aggBuilderImpl { func newAggBuilder(version *semver.Version) *aggBuilderImpl {
return &aggBuilderImpl{ return &aggBuilderImpl{
aggDefs: make([]*aggDef, 0), aggDefs: make([]*aggDef, 0),
version: version, version: version,
@@ -367,7 +365,7 @@ func (b *aggBuilderImpl) Terms(key, field string, fn func(a *TermsAggregation, b
fn(innerAgg, builder) fn(innerAgg, builder)
} }
if b.version >= 60 && len(innerAgg.Order) > 0 { if b.version.Major() >= 6 && len(innerAgg.Order) > 0 {
if orderBy, exists := innerAgg.Order[termsOrderTerm]; exists { if orderBy, exists := innerAgg.Order[termsOrderTerm]; exists {
innerAgg.Order["_key"] = orderBy innerAgg.Order["_key"] = orderBy
delete(innerAgg.Order, termsOrderTerm) delete(innerAgg.Order, termsOrderTerm)
@@ -5,6 +5,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/Masterminds/semver"
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/tsdb/interval" "github.com/grafana/grafana/pkg/tsdb/interval"
@@ -15,7 +16,8 @@ func TestSearchRequest(t *testing.T) {
Convey("Test elasticsearch search request", t, func() { Convey("Test elasticsearch search request", t, func() {
timeField := "@timestamp" timeField := "@timestamp"
Convey("Given new search request builder for es version 5", func() { Convey("Given new search request builder for es version 5", func() {
b := NewSearchRequestBuilder(5, interval.Interval{Value: 15 * time.Second, Text: "15s"}) version5, _ := semver.NewVersion("5.0.0")
b := NewSearchRequestBuilder(version5, interval.Interval{Value: 15 * time.Second, Text: "15s"})
Convey("When building search request", func() { Convey("When building search request", func() {
sr, err := b.Build() sr, err := b.Build()
@@ -390,7 +392,8 @@ func TestSearchRequest(t *testing.T) {
}) })
Convey("Given new search request builder for es version 2", func() { Convey("Given new search request builder for es version 2", func() {
b := NewSearchRequestBuilder(2, interval.Interval{Value: 15 * time.Second, Text: "15s"}) version2, _ := semver.NewVersion("2.0.0")
b := NewSearchRequestBuilder(version2, interval.Interval{Value: 15 * time.Second, Text: "15s"})
Convey("When adding doc value field", func() { Convey("When adding doc value field", func() {
b.AddDocValueField(timeField) b.AddDocValueField(timeField)
@@ -446,7 +449,8 @@ func TestSearchRequest(t *testing.T) {
func TestMultiSearchRequest(t *testing.T) { func TestMultiSearchRequest(t *testing.T) {
Convey("Test elasticsearch multi search request", t, func() { Convey("Test elasticsearch multi search request", t, func() {
Convey("Given new multi search request builder", func() { Convey("Given new multi search request builder", func() {
b := NewMultiSearchRequestBuilder(0) version2, _ := semver.NewVersion("2.0.0")
b := NewMultiSearchRequestBuilder(version2)
Convey("When adding one search request", func() { Convey("When adding one search request", func() {
b.Search(interval.Interval{Value: 15 * time.Second, Text: "15s"}) b.Search(interval.Interval{Value: 15 * time.Second, Text: "15s"})
+1
View File
@@ -32,6 +32,7 @@ func (e *Executor) DataQuery(ctx context.Context, dsInfo *models.DataSource,
} }
client, err := es.NewClient(ctx, dsInfo, *tsdbQuery.TimeRange) client, err := es.NewClient(ctx, dsInfo, *tsdbQuery.TimeRange)
if err != nil { if err != nil {
return plugins.DataResponse{}, err return plugins.DataResponse{}, err
} }
@@ -5,6 +5,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/Masterminds/semver"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" es "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client"
"github.com/grafana/grafana/pkg/tsdb/interval" "github.com/grafana/grafana/pkg/tsdb/interval"
@@ -22,7 +23,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
Convey("Test execute time series query", t, func() { Convey("Test execute time series query", t, func() {
Convey("With defaults on es 2", func() { Convey("With defaults on es 2", func() {
c := newFakeClient(2) c := newFakeClient("2.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [{ "type": "date_histogram", "field": "@timestamp", "id": "2" }], "bucketAggs": [{ "type": "date_histogram", "field": "@timestamp", "id": "2" }],
@@ -43,7 +44,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
}) })
Convey("With defaults on es 5", func() { Convey("With defaults on es 5", func() {
c := newFakeClient(5) c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [{ "type": "date_histogram", "field": "@timestamp", "id": "2" }], "bucketAggs": [{ "type": "date_histogram", "field": "@timestamp", "id": "2" }],
@@ -58,7 +59,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
}) })
Convey("With multiple bucket aggs", func() { Convey("With multiple bucket aggs", func() {
c := newFakeClient(5) c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [ "bucketAggs": [
@@ -80,7 +81,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
}) })
Convey("With select field", func() { Convey("With select field", func() {
c := newFakeClient(5) c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [ "bucketAggs": [
@@ -100,7 +101,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
}) })
Convey("With term agg and order by metric agg", func() { Convey("With term agg and order by metric agg", func() {
c := newFakeClient(5) c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [ "bucketAggs": [
@@ -130,7 +131,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
}) })
Convey("With term agg and order by count metric agg", func() { Convey("With term agg and order by count metric agg", func() {
c := newFakeClient(5) c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [ "bucketAggs": [
@@ -154,7 +155,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
}) })
Convey("With term agg and order by percentiles agg", func() { Convey("With term agg and order by percentiles agg", func() {
c := newFakeClient(5) c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [ "bucketAggs": [
@@ -179,7 +180,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
}) })
Convey("With term agg and order by extended stats agg", func() { Convey("With term agg and order by extended stats agg", func() {
c := newFakeClient(5) c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [ "bucketAggs": [
@@ -204,7 +205,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
}) })
Convey("With term agg and order by term", func() { Convey("With term agg and order by term", func() {
c := newFakeClient(5) c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [ "bucketAggs": [
@@ -231,7 +232,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
}) })
Convey("With term agg and order by term with es6.x", func() { Convey("With term agg and order by term with es6.x", func() {
c := newFakeClient(60) c := newFakeClient("6.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [ "bucketAggs": [
@@ -258,7 +259,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
}) })
Convey("With metric percentiles", func() { Convey("With metric percentiles", func() {
c := newFakeClient(5) c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [ "bucketAggs": [
@@ -291,7 +292,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
}) })
Convey("With filters aggs on es 2", func() { Convey("With filters aggs on es 2", func() {
c := newFakeClient(2) c := newFakeClient("2.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [ "bucketAggs": [
@@ -322,7 +323,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
}) })
Convey("With filters aggs on es 5", func() { Convey("With filters aggs on es 5", func() {
c := newFakeClient(5) c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [ "bucketAggs": [
@@ -353,7 +354,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
}) })
Convey("With raw document metric", func() { Convey("With raw document metric", func() {
c := newFakeClient(5) c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [], "bucketAggs": [],
@@ -366,7 +367,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
}) })
Convey("With raw document metric size set", func() { Convey("With raw document metric size set", func() {
c := newFakeClient(5) c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [], "bucketAggs": [],
@@ -379,7 +380,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
}) })
Convey("With date histogram agg", func() { Convey("With date histogram agg", func() {
c := newFakeClient(5) c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [ "bucketAggs": [
@@ -405,7 +406,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
}) })
Convey("With histogram agg", func() { Convey("With histogram agg", func() {
c := newFakeClient(5) c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [ "bucketAggs": [
@@ -432,7 +433,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
}) })
Convey("With geo hash grid agg", func() { Convey("With geo hash grid agg", func() {
c := newFakeClient(5) c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [ "bucketAggs": [
@@ -457,7 +458,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
}) })
Convey("With moving average", func() { Convey("With moving average", func() {
c := newFakeClient(5) c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [ "bucketAggs": [
@@ -495,7 +496,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
}) })
Convey("With moving average doc count", func() { Convey("With moving average doc count", func() {
c := newFakeClient(5) c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [ "bucketAggs": [
@@ -527,7 +528,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
}) })
Convey("With broken moving average", func() { Convey("With broken moving average", func() {
c := newFakeClient(5) c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [ "bucketAggs": [
@@ -563,7 +564,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
}) })
Convey("With cumulative sum", func() { Convey("With cumulative sum", func() {
c := newFakeClient(5) c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [ "bucketAggs": [
@@ -601,7 +602,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
}) })
Convey("With cumulative sum doc count", func() { Convey("With cumulative sum doc count", func() {
c := newFakeClient(5) c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [ "bucketAggs": [
@@ -633,7 +634,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
}) })
Convey("With broken cumulative sum", func() { Convey("With broken cumulative sum", func() {
c := newFakeClient(5) c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [ "bucketAggs": [
@@ -669,7 +670,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
}) })
Convey("With derivative", func() { Convey("With derivative", func() {
c := newFakeClient(5) c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [ "bucketAggs": [
@@ -698,7 +699,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
}) })
Convey("With derivative doc count", func() { Convey("With derivative doc count", func() {
c := newFakeClient(5) c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [ "bucketAggs": [
@@ -727,7 +728,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
}) })
Convey("With serial_diff", func() { Convey("With serial_diff", func() {
c := newFakeClient(5) c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [ "bucketAggs": [
@@ -756,7 +757,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
}) })
Convey("With serial_diff doc count", func() { Convey("With serial_diff doc count", func() {
c := newFakeClient(5) c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [ "bucketAggs": [
@@ -785,7 +786,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
}) })
Convey("With bucket_script", func() { Convey("With bucket_script", func() {
c := newFakeClient(5) c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [ "bucketAggs": [
@@ -822,7 +823,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
}) })
Convey("With bucket_script doc count", func() { Convey("With bucket_script doc count", func() {
c := newFakeClient(5) c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [ "bucketAggs": [
@@ -862,7 +863,7 @@ func TestSettingsCasting(t *testing.T) {
to := time.Date(2018, 5, 15, 17, 55, 0, 0, time.UTC) to := time.Date(2018, 5, 15, 17, 55, 0, 0, time.UTC)
t.Run("Correctly transforms moving_average settings", func(t *testing.T) { t.Run("Correctly transforms moving_average settings", func(t *testing.T) {
c := newFakeClient(5) c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [ "bucketAggs": [
@@ -906,7 +907,7 @@ func TestSettingsCasting(t *testing.T) {
}) })
t.Run("Correctly transforms serial_diff settings", func(t *testing.T) { t.Run("Correctly transforms serial_diff settings", func(t *testing.T) {
c := newFakeClient(5) c := newFakeClient("5.0.0")
_, err := executeTsdbQuery(c, `{ _, err := executeTsdbQuery(c, `{
"timeField": "@timestamp", "timeField": "@timestamp",
"bucketAggs": [ "bucketAggs": [
@@ -935,7 +936,7 @@ func TestSettingsCasting(t *testing.T) {
} }
type fakeClient struct { type fakeClient struct {
version int version *semver.Version
timeField string timeField string
multiSearchResponse *es.MultiSearchResponse multiSearchResponse *es.MultiSearchResponse
multiSearchError error multiSearchError error
@@ -943,7 +944,8 @@ type fakeClient struct {
multisearchRequests []*es.MultiSearchRequest multisearchRequests []*es.MultiSearchRequest
} }
func newFakeClient(version int) *fakeClient { func newFakeClient(versionString string) *fakeClient {
version, _ := semver.NewVersion(versionString)
return &fakeClient{ return &fakeClient{
version: version, version: version,
timeField: "@timestamp", timeField: "@timestamp",
@@ -954,7 +956,7 @@ func newFakeClient(version int) *fakeClient {
func (c *fakeClient) EnableDebug() {} func (c *fakeClient) EnableDebug() {}
func (c *fakeClient) GetVersion() int { func (c *fakeClient) GetVersion() *semver.Version {
return c.version return c.version
} }
@@ -21,6 +21,7 @@ import {
MetricAggregationType, MetricAggregationType,
} from './aggregations'; } from './aggregations';
import { useFields } from '../../../hooks/useFields'; import { useFields } from '../../../hooks/useFields';
import { satisfies } from 'semver';
const toOption = (metric: MetricAggregation) => ({ const toOption = (metric: MetricAggregation) => ({
label: metricAggregationConfig[metric.type].label, label: metricAggregationConfig[metric.type].label,
@@ -39,7 +40,7 @@ const isBasicAggregation = (metric: MetricAggregation) => !metricAggregationConf
const getTypeOptions = ( const getTypeOptions = (
previousMetrics: MetricAggregation[], previousMetrics: MetricAggregation[],
esVersion: number esVersion: string
): Array<SelectableValue<MetricAggregationType>> => { ): Array<SelectableValue<MetricAggregationType>> => {
// we'll include Pipeline Aggregations only if at least one previous metric is a "Basic" one // we'll include Pipeline Aggregations only if at least one previous metric is a "Basic" one
const includePipelineAggregations = previousMetrics.some(isBasicAggregation); const includePipelineAggregations = previousMetrics.some(isBasicAggregation);
@@ -47,10 +48,7 @@ const getTypeOptions = (
return ( return (
Object.entries(metricAggregationConfig) Object.entries(metricAggregationConfig)
// Only showing metrics type supported by the configured version of ES // Only showing metrics type supported by the configured version of ES
.filter(([_, { minVersion = 0, maxVersion = esVersion }]) => { .filter(([_, { versionRange = '*' }]) => satisfies(esVersion, versionRange))
// TODO: Double check this
return esVersion >= minVersion && esVersion <= maxVersion;
})
// Filtering out Pipeline Aggregations if there's no basic metric selected before // Filtering out Pipeline Aggregations if there's no basic metric selected before
.filter(([_, config]) => includePipelineAggregations || !config.isPipelineAgg) .filter(([_, config]) => includePipelineAggregations || !config.isPipelineAgg)
.map(([key, { label }]) => ({ .map(([key, { label }]) => ({
@@ -112,7 +112,7 @@ export const metricAggregationConfig: MetricsConfiguration = {
label: 'Moving Average', label: 'Moving Average',
requiresField: true, requiresField: true,
isPipelineAgg: true, isPipelineAgg: true,
minVersion: 2, versionRange: '>=2.0.0',
supportsMissing: false, supportsMissing: false,
supportsMultipleBucketPaths: false, supportsMultipleBucketPaths: false,
hasSettings: true, hasSettings: true,
@@ -135,14 +135,14 @@ export const metricAggregationConfig: MetricsConfiguration = {
supportsMissing: false, supportsMissing: false,
hasMeta: false, hasMeta: false,
hasSettings: true, hasSettings: true,
minVersion: 70, versionRange: '>=7.0.0',
defaults: {}, defaults: {},
}, },
derivative: { derivative: {
label: 'Derivative', label: 'Derivative',
requiresField: true, requiresField: true,
isPipelineAgg: true, isPipelineAgg: true,
minVersion: 2, versionRange: '>=2.0.0',
supportsMissing: false, supportsMissing: false,
supportsMultipleBucketPaths: false, supportsMultipleBucketPaths: false,
hasSettings: true, hasSettings: true,
@@ -154,7 +154,7 @@ export const metricAggregationConfig: MetricsConfiguration = {
label: 'Serial Difference', label: 'Serial Difference',
requiresField: true, requiresField: true,
isPipelineAgg: true, isPipelineAgg: true,
minVersion: 2, versionRange: '>=2.0.0',
supportsMissing: false, supportsMissing: false,
supportsMultipleBucketPaths: false, supportsMultipleBucketPaths: false,
hasSettings: true, hasSettings: true,
@@ -170,7 +170,7 @@ export const metricAggregationConfig: MetricsConfiguration = {
label: 'Cumulative Sum', label: 'Cumulative Sum',
requiresField: true, requiresField: true,
isPipelineAgg: true, isPipelineAgg: true,
minVersion: 2, versionRange: '>=2.0.0',
supportsMissing: false, supportsMissing: false,
supportsMultipleBucketPaths: false, supportsMultipleBucketPaths: false,
hasSettings: true, hasSettings: true,
@@ -184,7 +184,7 @@ export const metricAggregationConfig: MetricsConfiguration = {
isPipelineAgg: true, isPipelineAgg: true,
supportsMissing: false, supportsMissing: false,
supportsMultipleBucketPaths: true, supportsMultipleBucketPaths: true,
minVersion: 2, versionRange: '>=2.0.0',
hasSettings: true, hasSettings: true,
supportsInlineScript: false, supportsInlineScript: false,
hasMeta: false, hasMeta: false,
@@ -31,7 +31,7 @@ describe('ConfigEditor', () => {
mount( mount(
<ConfigEditor <ConfigEditor
onOptionsChange={(options) => { onOptionsChange={(options) => {
expect(options.jsonData.esVersion).toBe(5); expect(options.jsonData.esVersion).toBe('5.0.0');
expect(options.jsonData.timeField).toBe('@timestamp'); expect(options.jsonData.timeField).toBe('@timestamp');
expect(options.jsonData.maxConcurrentShardRequests).toBe(256); expect(options.jsonData.maxConcurrentShardRequests).toBe(256);
}} }}
@@ -41,17 +41,10 @@ describe('ConfigEditor', () => {
}); });
it('should not apply default if values are set', () => { it('should not apply default if values are set', () => {
expect.assertions(3); const onChange = jest.fn();
mount( mount(<ConfigEditor onOptionsChange={onChange} options={createDefaultConfigOptions()} />);
<ConfigEditor
onOptionsChange={(options) => { expect(onChange).toHaveBeenCalledTimes(0);
expect(options.jsonData.esVersion).toBe(70);
expect(options.jsonData.timeField).toBe('@time');
expect(options.jsonData.maxConcurrentShardRequests).toBe(300);
}}
options={createDefaultConfigOptions()}
/>
);
}); });
}); });
@@ -2,30 +2,23 @@ import React, { useEffect } from 'react';
import { Alert, DataSourceHttpSettings } from '@grafana/ui'; import { Alert, DataSourceHttpSettings } from '@grafana/ui';
import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
import { ElasticsearchOptions } from '../types'; import { ElasticsearchOptions } from '../types';
import { defaultMaxConcurrentShardRequests, ElasticDetails } from './ElasticDetails'; import { ElasticDetails } from './ElasticDetails';
import { LogsConfig } from './LogsConfig'; import { LogsConfig } from './LogsConfig';
import { DataLinks } from './DataLinks'; import { DataLinks } from './DataLinks';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
import { coerceOptions, isValidOptions } from './utils';
export type Props = DataSourcePluginOptionsEditorProps<ElasticsearchOptions>; export type Props = DataSourcePluginOptionsEditorProps<ElasticsearchOptions>;
export const ConfigEditor = (props: Props) => {
const { options, onOptionsChange } = props;
// Apply some defaults on initial render export const ConfigEditor = (props: Props) => {
const { options: originalOptions, onOptionsChange } = props;
const options = coerceOptions(originalOptions);
useEffect(() => { useEffect(() => {
const esVersion = options.jsonData.esVersion || 5; if (!isValidOptions(originalOptions)) {
onOptionsChange({ onOptionsChange(coerceOptions(originalOptions));
...options, }
jsonData: {
...options.jsonData,
timeField: options.jsonData.timeField || '@timestamp',
esVersion,
maxConcurrentShardRequests:
options.jsonData.maxConcurrentShardRequests || defaultMaxConcurrentShardRequests(esVersion),
logMessageField: options.jsonData.logMessageField || '',
logLevelField: options.jsonData.logLevelField || '',
},
});
// We can't enforce the eslint rule here because we only want to run this once. // We can't enforce the eslint rule here because we only want to run this once.
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
@@ -39,9 +32,9 @@ export const ConfigEditor = (props: Props) => {
)} )}
<DataSourceHttpSettings <DataSourceHttpSettings
defaultUrl={'http://localhost:9200'} defaultUrl="http://localhost:9200"
dataSourceConfig={options} dataSourceConfig={options}
showAccessOptions={true} showAccessOptions
onChange={onOptionsChange} onChange={onOptionsChange}
sigV4AuthToggleEnabled={config.sigV4AuthEnabled} sigV4AuthToggleEnabled={config.sigV4AuthEnabled}
/> />
@@ -18,7 +18,7 @@ describe('ElasticDetails', () => {
it('should not render "Max concurrent Shard Requests" if version is low', () => { it('should not render "Max concurrent Shard Requests" if version is low', () => {
const options = createDefaultConfigOptions(); const options = createDefaultConfigOptions();
options.jsonData.esVersion = 5; options.jsonData.esVersion = '5.0.0';
const wrapper = mount(<ElasticDetails onChange={() => {}} value={options} />); const wrapper = mount(<ElasticDetails onChange={() => {}} value={options} />);
expect(wrapper.find('input[aria-label="Max concurrent Shard Requests input"]').length).toBe(0); expect(wrapper.find('input[aria-label="Max concurrent Shard Requests input"]').length).toBe(0);
}); });
@@ -48,16 +48,16 @@ describe('ElasticDetails', () => {
describe('version change', () => { describe('version change', () => {
const testCases = [ const testCases = [
{ version: 50, expectedMaxConcurrentShardRequests: 256 }, { version: '5.0.0', expectedMaxConcurrentShardRequests: 256 },
{ version: 50, maxConcurrentShardRequests: 50, expectedMaxConcurrentShardRequests: 50 }, { version: '5.0.0', maxConcurrentShardRequests: 50, expectedMaxConcurrentShardRequests: 50 },
{ version: 56, expectedMaxConcurrentShardRequests: 256 }, { version: '5.6.0', expectedMaxConcurrentShardRequests: 256 },
{ version: 56, maxConcurrentShardRequests: 256, expectedMaxConcurrentShardRequests: 256 }, { version: '5.6.0', maxConcurrentShardRequests: 256, expectedMaxConcurrentShardRequests: 256 },
{ version: 56, maxConcurrentShardRequests: 5, expectedMaxConcurrentShardRequests: 256 }, { version: '5.6.0', maxConcurrentShardRequests: 5, expectedMaxConcurrentShardRequests: 256 },
{ version: 56, maxConcurrentShardRequests: 200, expectedMaxConcurrentShardRequests: 200 }, { version: '5.6.0', maxConcurrentShardRequests: 200, expectedMaxConcurrentShardRequests: 200 },
{ version: 70, expectedMaxConcurrentShardRequests: 5 }, { version: '7.0.0', expectedMaxConcurrentShardRequests: 5 },
{ version: 70, maxConcurrentShardRequests: 256, expectedMaxConcurrentShardRequests: 5 }, { version: '7.0.0', maxConcurrentShardRequests: 256, expectedMaxConcurrentShardRequests: 5 },
{ version: 70, maxConcurrentShardRequests: 5, expectedMaxConcurrentShardRequests: 5 }, { version: '7.0.0', maxConcurrentShardRequests: 5, expectedMaxConcurrentShardRequests: 5 },
{ version: 70, maxConcurrentShardRequests: 6, expectedMaxConcurrentShardRequests: 6 }, { version: '7.0.0', maxConcurrentShardRequests: 6, expectedMaxConcurrentShardRequests: 6 },
]; ];
const onChangeMock = jest.fn(); const onChangeMock = jest.fn();
@@ -3,6 +3,7 @@ import { EventsWithValidation, regexValidation, LegacyForms } from '@grafana/ui'
const { Select, Input, FormField } = LegacyForms; const { Select, Input, FormField } = LegacyForms;
import { ElasticsearchOptions, Interval } from '../types'; import { ElasticsearchOptions, Interval } from '../types';
import { DataSourceSettings, SelectableValue } from '@grafana/data'; import { DataSourceSettings, SelectableValue } from '@grafana/data';
import { gte, lt } from 'semver';
const indexPatternTypes = [ const indexPatternTypes = [
{ label: 'No pattern', value: 'none' }, { label: 'No pattern', value: 'none' },
@@ -14,20 +15,18 @@ const indexPatternTypes = [
]; ];
const esVersions = [ const esVersions = [
{ label: '2.x', value: 2 }, { label: '2.x', value: '2.0.0' },
{ label: '5.x', value: 5 }, { label: '5.x', value: '5.0.0' },
{ label: '5.6+', value: 56 }, { label: '5.6+', value: '5.6.0' },
{ label: '6.0+', value: 60 }, { label: '6.0+', value: '6.0.0' },
{ label: '7.0+', value: 70 }, { label: '7.0+', value: '7.0.0' },
]; ];
type Props = { type Props = {
value: DataSourceSettings<ElasticsearchOptions>; value: DataSourceSettings<ElasticsearchOptions>;
onChange: (value: DataSourceSettings<ElasticsearchOptions>) => void; onChange: (value: DataSourceSettings<ElasticsearchOptions>) => void;
}; };
export const ElasticDetails = (props: Props) => { export const ElasticDetails = ({ value, onChange }: Props) => {
const { value, onChange } = props;
return ( return (
<> <>
<h3 className="page-heading">Elasticsearch details</h3> <h3 className="page-heading">Elasticsearch details</h3>
@@ -101,7 +100,7 @@ export const ElasticDetails = (props: Props) => {
} }
/> />
</div> </div>
{value.jsonData.esVersion >= 56 && ( {gte(value.jsonData.esVersion, '5.6.0') && (
<div className="gf-form max-width-30"> <div className="gf-form max-width-30">
<FormField <FormField
aria-label={'Max concurrent Shard Requests input'} aria-label={'Max concurrent Shard Requests input'}
@@ -207,18 +206,18 @@ const intervalHandler = (value: Props['value'], onChange: Props['onChange']) =>
} }
}; };
function getMaxConcurrenShardRequestOrDefault(maxConcurrentShardRequests: number | undefined, version: number): number { function getMaxConcurrenShardRequestOrDefault(maxConcurrentShardRequests: number | undefined, version: string): number {
if (maxConcurrentShardRequests === 5 && version < 70) { if (maxConcurrentShardRequests === 5 && lt(version, '7.0.0')) {
return 256; return 256;
} }
if (maxConcurrentShardRequests === 256 && version >= 70) { if (maxConcurrentShardRequests === 256 && gte(version, '7.0.0')) {
return 5; return 5;
} }
return maxConcurrentShardRequests || defaultMaxConcurrentShardRequests(version); return maxConcurrentShardRequests || defaultMaxConcurrentShardRequests(version);
} }
export function defaultMaxConcurrentShardRequests(version: number) { export function defaultMaxConcurrentShardRequests(version: string) {
return version >= 70 ? 5 : 256; return gte(version, '7.0.0') ? 5 : 256;
} }
@@ -5,7 +5,7 @@ import { createDatasourceSettings } from '../../../../features/datasources/mocks
export function createDefaultConfigOptions(): DataSourceSettings<ElasticsearchOptions> { export function createDefaultConfigOptions(): DataSourceSettings<ElasticsearchOptions> {
return createDatasourceSettings<ElasticsearchOptions>({ return createDatasourceSettings<ElasticsearchOptions>({
timeField: '@time', timeField: '@time',
esVersion: 70, esVersion: '7.0.0',
interval: 'Hourly', interval: 'Hourly',
timeInterval: '10s', timeInterval: '10s',
maxConcurrentShardRequests: 300, maxConcurrentShardRequests: 300,
@@ -0,0 +1,38 @@
import { DataSourceSettings } from '@grafana/data';
import { valid } from 'semver';
import { ElasticsearchOptions } from '../types';
import { coerceESVersion } from '../utils';
import { defaultMaxConcurrentShardRequests } from './ElasticDetails';
export const coerceOptions = (
options: DataSourceSettings<ElasticsearchOptions, {}>
): DataSourceSettings<ElasticsearchOptions, {}> => {
const esVersion = coerceESVersion(options.jsonData.esVersion);
return {
...options,
jsonData: {
...options.jsonData,
timeField: options.jsonData.timeField || '@timestamp',
esVersion,
maxConcurrentShardRequests:
options.jsonData.maxConcurrentShardRequests || defaultMaxConcurrentShardRequests(esVersion),
logMessageField: options.jsonData.logMessageField || '',
logLevelField: options.jsonData.logLevelField || '',
},
};
};
export const isValidOptions = (options: DataSourceSettings<ElasticsearchOptions, {}>): boolean => {
return (
// esVersion should be a valid semver string
!!valid(options.jsonData.esVersion) &&
// timeField should not be empty or nullish
!!options.jsonData.timeField &&
// maxConcurrentShardRequests should be a number AND greater than 0
!!options.jsonData.maxConcurrentShardRequests &&
// message & level fields should be defined
options.jsonData.logMessageField !== undefined &&
options.jsonData.logLevelField !== undefined
);
};
@@ -203,7 +203,7 @@ describe('ElasticDatasource', function (this: any) {
async function setupDataSource(jsonData?: Partial<ElasticsearchOptions>) { async function setupDataSource(jsonData?: Partial<ElasticsearchOptions>) {
jsonData = { jsonData = {
interval: 'Daily', interval: 'Daily',
esVersion: 2, esVersion: '2.0.0',
timeField: '@timestamp', timeField: '@timestamp',
...(jsonData || {}), ...(jsonData || {}),
}; };
@@ -39,7 +39,8 @@ import {
} from './components/QueryEditor/BucketAggregationsEditor/aggregations'; } from './components/QueryEditor/BucketAggregationsEditor/aggregations';
import { generate, Observable, of, throwError } from 'rxjs'; import { generate, Observable, of, throwError } from 'rxjs';
import { catchError, first, map, mergeMap, skipWhile, throwIfEmpty } from 'rxjs/operators'; import { catchError, first, map, mergeMap, skipWhile, throwIfEmpty } from 'rxjs/operators';
import { getScriptValue } from './utils'; import { coerceESVersion, getScriptValue } from './utils';
import { gte, lt, satisfies } from 'semver';
// Those are metadata fields as defined in https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-fields.html#_identity_metadata_fields. // Those are metadata fields as defined in https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-fields.html#_identity_metadata_fields.
// custom fields can start with underscores, therefore is not safe to exclude anything that starts with one. // custom fields can start with underscores, therefore is not safe to exclude anything that starts with one.
@@ -62,7 +63,7 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
name: string; name: string;
index: string; index: string;
timeField: string; timeField: string;
esVersion: number; esVersion: string;
interval: string; interval: string;
maxConcurrentShardRequests?: number; maxConcurrentShardRequests?: number;
queryBuilder: ElasticQueryBuilder; queryBuilder: ElasticQueryBuilder;
@@ -85,7 +86,7 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
const settingsData = instanceSettings.jsonData || ({} as ElasticsearchOptions); const settingsData = instanceSettings.jsonData || ({} as ElasticsearchOptions);
this.timeField = settingsData.timeField; this.timeField = settingsData.timeField;
this.esVersion = settingsData.esVersion; this.esVersion = coerceESVersion(settingsData.esVersion);
this.indexPattern = new IndexPattern(this.index, settingsData.interval); this.indexPattern = new IndexPattern(this.index, settingsData.interval);
this.interval = settingsData.timeInterval; this.interval = settingsData.timeInterval;
this.maxConcurrentShardRequests = settingsData.maxConcurrentShardRequests; this.maxConcurrentShardRequests = settingsData.maxConcurrentShardRequests;
@@ -256,7 +257,7 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
}; };
// fields field not supported on ES 5.x // fields field not supported on ES 5.x
if (this.esVersion < 5) { if (lt(this.esVersion, '5.0.0')) {
data['fields'] = [timeField, '_source']; data['fields'] = [timeField, '_source'];
} }
@@ -420,7 +421,7 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
index: this.indexPattern.getIndexList(timeFrom, timeTo), index: this.indexPattern.getIndexList(timeFrom, timeTo),
}; };
if (this.esVersion >= 56 && this.esVersion < 70) { if (satisfies(this.esVersion, '>=5.6.0 <7.0.0')) {
queryHeader['max_concurrent_shard_requests'] = this.maxConcurrentShardRequests; queryHeader['max_concurrent_shard_requests'] = this.maxConcurrentShardRequests;
} }
@@ -484,7 +485,7 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
* search_after feature. * search_after feature.
*/ */
showContextToggle(): boolean { showContextToggle(): boolean {
return this.esVersion > 5; return gte(this.esVersion, '5.0.0');
} }
getLogRowContext = async (row: LogRowModel, options?: RowContextOptions): Promise<{ data: DataFrame[] }> => { getLogRowContext = async (row: LogRowModel, options?: RowContextOptions): Promise<{ data: DataFrame[] }> => {
@@ -588,7 +589,7 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
const esQuery = JSON.stringify(queryObj); const esQuery = JSON.stringify(queryObj);
const searchType = queryObj.size === 0 && this.esVersion < 5 ? 'count' : 'query_then_fetch'; const searchType = queryObj.size === 0 && lt(this.esVersion, '5.0.0') ? 'count' : 'query_then_fetch';
const header = this.getQueryHeader(searchType, options.range.from, options.range.to); const header = this.getQueryHeader(searchType, options.range.from, options.range.to);
payload += header + '\n'; payload += header + '\n';
@@ -637,7 +638,6 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
// FIXME: This doesn't seem to return actual MetricFindValues, we should either change the return type // FIXME: This doesn't seem to return actual MetricFindValues, we should either change the return type
// or fix the implementation. // or fix the implementation.
getFields(type?: string, range?: TimeRange): Observable<MetricFindValue[]> { getFields(type?: string, range?: TimeRange): Observable<MetricFindValue[]> {
const configuredEsVersion = this.esVersion;
return this.get('/_mapping', range).pipe( return this.get('/_mapping', range).pipe(
map((result) => { map((result) => {
const typeMap: any = { const typeMap: any = {
@@ -706,7 +706,7 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
if (index && index.mappings) { if (index && index.mappings) {
const mappings = index.mappings; const mappings = index.mappings;
if (configuredEsVersion < 70) { if (lt(this.esVersion, '7.0.0')) {
for (const typeName in mappings) { for (const typeName in mappings) {
const properties = mappings[typeName].properties; const properties = mappings[typeName].properties;
getFieldsRecursively(properties); getFieldsRecursively(properties);
@@ -727,7 +727,7 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
} }
getTerms(queryDef: any, range = getDefaultTimeRange()): Observable<MetricFindValue[]> { getTerms(queryDef: any, range = getDefaultTimeRange()): Observable<MetricFindValue[]> {
const searchType = this.esVersion >= 5 ? 'query_then_fetch' : 'count'; const searchType = gte(this.esVersion, '5.0.0') ? 'query_then_fetch' : 'count';
const header = this.getQueryHeader(searchType, range.from, range.to); const header = this.getQueryHeader(searchType, range.from, range.to);
let esQuery = JSON.stringify(this.queryBuilder.getTermsQuery(queryDef)); let esQuery = JSON.stringify(this.queryBuilder.getTermsQuery(queryDef));
@@ -755,7 +755,7 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
} }
getMultiSearchUrl() { getMultiSearchUrl() {
if (this.esVersion >= 70 && this.maxConcurrentShardRequests) { if (gte(this.esVersion, '7.0.0') && this.maxConcurrentShardRequests) {
return `_msearch?max_concurrent_shard_requests=${this.maxConcurrentShardRequests}`; return `_msearch?max_concurrent_shard_requests=${this.maxConcurrentShardRequests}`;
} }
@@ -18,7 +18,7 @@ const dataSource = new ElasticDatasource(
database: '[asd-]YYYY.MM.DD', database: '[asd-]YYYY.MM.DD',
jsonData: { jsonData: {
interval: 'Daily', interval: 'Daily',
esVersion: 2, esVersion: '2.0.0',
timeField: '@time', timeField: '@time',
}, },
} as DataSourceInstanceSettings<ElasticsearchOptions>, } as DataSourceInstanceSettings<ElasticsearchOptions>,
@@ -1,3 +1,4 @@
import { gte, lt } from 'semver';
import { import {
Filters, Filters,
Histogram, Histogram,
@@ -19,9 +20,9 @@ import { convertOrderByToMetricId, getScriptValue } from './utils';
export class ElasticQueryBuilder { export class ElasticQueryBuilder {
timeField: string; timeField: string;
esVersion: number; esVersion: string;
constructor(options: { timeField: string; esVersion: number }) { constructor(options: { timeField: string; esVersion: string }) {
this.timeField = options.timeField; this.timeField = options.timeField;
this.esVersion = options.esVersion; this.esVersion = options.esVersion;
} }
@@ -50,7 +51,7 @@ export class ElasticQueryBuilder {
if (aggDef.settings.orderBy !== void 0) { if (aggDef.settings.orderBy !== void 0) {
queryNode.terms.order = {}; queryNode.terms.order = {};
if (aggDef.settings.orderBy === '_term' && this.esVersion >= 60) { if (aggDef.settings.orderBy === '_term' && gte(this.esVersion, '6.0.0')) {
queryNode.terms.order['_key'] = aggDef.settings.order; queryNode.terms.order['_key'] = aggDef.settings.order;
} else { } else {
queryNode.terms.order[aggDef.settings.orderBy] = aggDef.settings.order; queryNode.terms.order[aggDef.settings.orderBy] = aggDef.settings.order;
@@ -147,7 +148,7 @@ export class ElasticQueryBuilder {
]; ];
// fields field not supported on ES 5.x // fields field not supported on ES 5.x
if (this.esVersion < 5) { if (lt(this.esVersion, '5.0.0')) {
query.fields = ['*', '_source']; query.fields = ['*', '_source'];
} }
@@ -443,7 +444,7 @@ export class ElasticQueryBuilder {
switch (orderBy) { switch (orderBy) {
case 'key': case 'key':
case 'term': case 'term':
const keyname = this.esVersion >= 60 ? '_key' : '_term'; const keyname = gte(this.esVersion, '6.0.0') ? '_key' : '_term';
query.aggs['1'].terms.order[keyname] = order; query.aggs['1'].terms.order[keyname] = order;
break; break;
case 'doc_count': case 'doc_count':
@@ -1,12 +1,13 @@
import { gte, lt } from 'semver';
import { ElasticQueryBuilder } from '../query_builder'; import { ElasticQueryBuilder } from '../query_builder';
import { ElasticsearchQuery } from '../types'; import { ElasticsearchQuery } from '../types';
describe('ElasticQueryBuilder', () => { describe('ElasticQueryBuilder', () => {
const builder = new ElasticQueryBuilder({ timeField: '@timestamp', esVersion: 2 }); const builder = new ElasticQueryBuilder({ timeField: '@timestamp', esVersion: '2.0.0' });
const builder5x = new ElasticQueryBuilder({ timeField: '@timestamp', esVersion: 5 }); const builder5x = new ElasticQueryBuilder({ timeField: '@timestamp', esVersion: '5.0.0' });
const builder56 = new ElasticQueryBuilder({ timeField: '@timestamp', esVersion: 56 }); const builder56 = new ElasticQueryBuilder({ timeField: '@timestamp', esVersion: '5.6.0' });
const builder6x = new ElasticQueryBuilder({ timeField: '@timestamp', esVersion: 60 }); const builder6x = new ElasticQueryBuilder({ timeField: '@timestamp', esVersion: '6.0.0' });
const builder7x = new ElasticQueryBuilder({ timeField: '@timestamp', esVersion: 70 }); const builder7x = new ElasticQueryBuilder({ timeField: '@timestamp', esVersion: '7.0.0' });
const allBuilders = [builder, builder5x, builder56, builder6x, builder7x]; const allBuilders = [builder, builder5x, builder56, builder6x, builder7x];
@@ -91,7 +92,7 @@ describe('ElasticQueryBuilder', () => {
const query = builder.build(target, 100, '1000'); const query = builder.build(target, 100, '1000');
const firstLevel = query.aggs['2']; const firstLevel = query.aggs['2'];
if (builder.esVersion >= 60) { if (gte(builder.esVersion, '6.0.0')) {
expect(firstLevel.terms.order._key).toBe('asc'); expect(firstLevel.terms.order._key).toBe('asc');
} else { } else {
expect(firstLevel.terms.order._term).toBe('asc'); expect(firstLevel.terms.order._term).toBe('asc');
@@ -642,7 +643,7 @@ describe('ElasticQueryBuilder', () => {
} }
function checkSort(order: any, expected: string) { function checkSort(order: any, expected: string) {
if (builder.esVersion < 60) { if (lt(builder.esVersion, '6.0.0')) {
expect(order._term).toBe(expected); expect(order._term).toBe(expected);
expect(order._key).toBeUndefined(); expect(order._key).toBeUndefined();
} else { } else {
@@ -12,7 +12,7 @@ export type Interval = 'Hourly' | 'Daily' | 'Weekly' | 'Monthly' | 'Yearly';
export interface ElasticsearchOptions extends DataSourceJsonData { export interface ElasticsearchOptions extends DataSourceJsonData {
timeField: string; timeField: string;
esVersion: number; esVersion: string;
interval?: Interval; interval?: Interval;
timeInterval: string; timeInterval: string;
maxConcurrentShardRequests?: number; maxConcurrentShardRequests?: number;
@@ -27,8 +27,11 @@ interface MetricConfiguration<T extends MetricAggregationType> {
supportsInlineScript: boolean; supportsInlineScript: boolean;
supportsMissing: boolean; supportsMissing: boolean;
isPipelineAgg: boolean; isPipelineAgg: boolean;
minVersion?: number; /**
maxVersion?: number; * A valid semver range for which the metric is known to be available.
* If omitted defaults to '*'.
*/
versionRange?: string;
supportsMultipleBucketPaths: boolean; supportsMultipleBucketPaths: boolean;
isSingleMetric?: boolean; isSingleMetric?: boolean;
hasSettings: boolean; hasSettings: boolean;
@@ -4,6 +4,7 @@ import {
MetricAggregationWithInlineScript, MetricAggregationWithInlineScript,
} from './components/QueryEditor/MetricAggregationsEditor/aggregations'; } from './components/QueryEditor/MetricAggregationsEditor/aggregations';
import { metricAggregationConfig } from './components/QueryEditor/MetricAggregationsEditor/utils'; import { metricAggregationConfig } from './components/QueryEditor/MetricAggregationsEditor/utils';
import { valid } from 'semver';
export const describeMetric = (metric: MetricAggregation) => { export const describeMetric = (metric: MetricAggregation) => {
if (!isMetricAggregationWithField(metric)) { if (!isMetricAggregationWithField(metric)) {
@@ -91,3 +92,28 @@ export const convertOrderByToMetricId = (orderBy: string): string | undefined =>
*/ */
export const getScriptValue = (metric: MetricAggregationWithInlineScript) => export const getScriptValue = (metric: MetricAggregationWithInlineScript) =>
(typeof metric.settings?.script === 'object' ? metric.settings?.script?.inline : metric.settings?.script) || ''; (typeof metric.settings?.script === 'object' ? metric.settings?.script?.inline : metric.settings?.script) || '';
/**
* Coerces the a version string/number to a valid semver string.
* It takes care of also converting from the legacy format (numeric) to the new one.
* @param version
*/
export const coerceESVersion = (version: string | number): string => {
if (typeof version === 'string') {
return valid(version) || '5.0.0';
}
switch (version) {
case 2:
return '2.0.0';
case 56:
return '5.6.0';
case 60:
return '6.0.0';
case 70:
return '7.0.0';
case 5:
default:
return '5.0.0';
}
};