Elasticsearch: Added support for calendar_interval in ES date histogram queries (#75459)

* Introduce support for calendar intervals in ES date histogram queries

* Add missing undef type check for ES calendar interval query support
This commit is contained in:
NikolayTsvetkov
2023-10-09 12:37:38 +02:00
committed by GitHub
parent 07266aa983
commit ce462e8cd7
5 changed files with 98 additions and 21 deletions

View File

@ -214,6 +214,7 @@ type HistogramAgg struct {
type DateHistogramAgg struct { type DateHistogramAgg struct {
Field string `json:"field"` Field string `json:"field"`
FixedInterval string `json:"fixed_interval,omitempty"` FixedInterval string `json:"fixed_interval,omitempty"`
CalendarInterval string `json:"calendar_interval,omitempty"`
MinDocCount int `json:"min_doc_count"` MinDocCount int `json:"min_doc_count"`
Missing *string `json:"missing,omitempty"` Missing *string `json:"missing,omitempty"`
ExtendedBounds *ExtendedBounds `json:"extended_bounds"` ExtendedBounds *ExtendedBounds `json:"extended_bounds"`
@ -222,6 +223,11 @@ type DateHistogramAgg struct {
TimeZone string `json:"time_zone,omitempty"` TimeZone string `json:"time_zone,omitempty"`
} }
// GetCalendarIntervals provides the list of intervals used for building calendar bucketAgg
func GetCalendarIntervals() []string {
return []string{"1w", "1M", "1q", "1y"}
}
// FiltersAggregation represents a filters aggregation // FiltersAggregation represents a filters aggregation
type FiltersAggregation struct { type FiltersAggregation struct {
Filters map[string]interface{} `json:"filters"` Filters map[string]interface{} `json:"filters"`

View File

@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend"
"golang.org/x/exp/slices"
"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"
@ -53,6 +54,7 @@ func (e *elasticsearchDataQuery) execute() (*backend.QueryDataResponse, error) {
from := e.dataQueries[0].TimeRange.From.UnixNano() / int64(time.Millisecond) from := e.dataQueries[0].TimeRange.From.UnixNano() / int64(time.Millisecond)
to := e.dataQueries[0].TimeRange.To.UnixNano() / int64(time.Millisecond) to := e.dataQueries[0].TimeRange.To.UnixNano() / int64(time.Millisecond)
for _, q := range queries { for _, q := range queries {
fmt.Printf("Query = %v", q)
if err := e.processQuery(q, ms, from, to); err != nil { if err := e.processQuery(q, ms, from, to); err != nil {
mq, _ := json.Marshal(q) mq, _ := json.Marshal(q)
e.logger.Error("Failed to process query to multisearch request builder", "error", err, "query", string(mq), "queriesLength", len(queries), "duration", time.Since(start), "stage", es.StagePrepareRequest) e.logger.Error("Failed to process query to multisearch request builder", "error", err, "query", string(mq), "queriesLength", len(queries), "duration", time.Since(start), "stage", es.StagePrepareRequest)
@ -160,12 +162,11 @@ func addDateHistogramAgg(aggBuilder es.AggBuilder, bucketAgg *BucketAgg, timeFro
field = timeField field = timeField
} }
aggBuilder.DateHistogram(bucketAgg.ID, field, func(a *es.DateHistogramAgg, b es.AggBuilder) { aggBuilder.DateHistogram(bucketAgg.ID, field, func(a *es.DateHistogramAgg, b es.AggBuilder) {
a.FixedInterval = bucketAgg.Settings.Get("interval").MustString("auto") var interval = bucketAgg.Settings.Get("interval").MustString("auto")
a.MinDocCount = bucketAgg.Settings.Get("min_doc_count").MustInt(0) if slices.Contains(es.GetCalendarIntervals(), interval) {
a.ExtendedBounds = &es.ExtendedBounds{Min: timeFrom, Max: timeTo} a.CalendarInterval = interval
a.Format = bucketAgg.Settings.Get("format").MustString(es.DateFormatEpochMS) } else {
if interval == "auto" {
if a.FixedInterval == "auto" {
// note this is not really a valid grafana-variable-handling, // note this is not really a valid grafana-variable-handling,
// because normally this would not match `$__interval_ms`, // because normally this would not match `$__interval_ms`,
// but because how we apply these in the go-code, this will work // but because how we apply these in the go-code, this will work
@ -174,7 +175,13 @@ func addDateHistogramAgg(aggBuilder es.AggBuilder, bucketAgg *BucketAgg, timeFro
// that format is not recognized where we apply these variables // that format is not recognized where we apply these variables
// in the elasticsearch datasource // in the elasticsearch datasource
a.FixedInterval = "$__interval_msms" a.FixedInterval = "$__interval_msms"
} else {
a.FixedInterval = interval
} }
}
a.MinDocCount = bucketAgg.Settings.Get("min_doc_count").MustInt(0)
a.ExtendedBounds = &es.ExtendedBounds{Min: timeFrom, Max: timeTo}
a.Format = bucketAgg.Settings.Get("format").MustString(es.DateFormatEpochMS)
if offset, err := bucketAgg.Settings.Get("offset").String(); err == nil { if offset, err := bucketAgg.Settings.Get("offset").String(); err == nil {
a.Offset = offset a.Offset = offset

View File

@ -1669,6 +1669,31 @@ func TestSettingsCasting(t *testing.T) {
assert.NotZero(t, dateHistogramAgg.FixedInterval) assert.NotZero(t, dateHistogramAgg.FixedInterval)
}) })
t.Run("Uses calendar_interval", func(t *testing.T) {
c := newFakeClient()
_, err := executeElasticsearchDataQuery(c, `{
"bucketAggs": [
{
"type": "date_histogram",
"field": "@timestamp",
"id": "2",
"settings": {
"interval": "1M"
}
}
],
"metrics": [
{ "id": "1", "type": "average", "field": "@value" }
]
}`, from, to)
assert.Nil(t, err)
sr := c.multisearchRequests[0].Requests[0]
dateHistogramAgg := sr.Aggs[0].Aggregation.Aggregation.(*es.DateHistogramAgg)
assert.NotZero(t, dateHistogramAgg.CalendarInterval)
})
}) })
}) })
@ -1755,6 +1780,21 @@ func TestSettingsCasting(t *testing.T) {
dateHistogramAgg := sr.Aggs[0].Aggregation.Aggregation.(*es.DateHistogramAgg) dateHistogramAgg := sr.Aggs[0].Aggregation.Aggregation.(*es.DateHistogramAgg)
assert.Equal(t, dateHistogramAgg.FixedInterval, "1d") assert.Equal(t, dateHistogramAgg.FixedInterval, "1d")
}) })
t.Run("Should use calendar_interval", func(t *testing.T) {
c := newFakeClient()
_, err := executeElasticsearchDataQuery(c, `{
"metrics": [{ "type": "count", "id": "1" }],
"bucketAggs": [
{ "type": "date_histogram", "id": "2", "field": "@time", "settings": { "min_doc_count": "1", "interval": "1w" } }
]
}`, from, to)
assert.Nil(t, err)
sr := c.multisearchRequests[0].Requests[0]
dateHistogramAgg := sr.Aggs[0].Aggregation.Aggregation.(*es.DateHistogramAgg)
assert.Equal(t, dateHistogramAgg.CalendarInterval, "1w")
})
}) })
} }

View File

@ -899,6 +899,25 @@ describe('ElasticQueryBuilder', () => {
expect(query.aggs['2'].date_histogram.interval).toBeUndefined(); expect(query.aggs['2'].date_histogram.interval).toBeUndefined();
expect(query.aggs['2'].date_histogram.fixed_interval).toBe('1d'); expect(query.aggs['2'].date_histogram.fixed_interval).toBe('1d');
}); });
it('should use calendar_interval', () => {
const query = builder.build({
refId: 'A',
metrics: [{ type: 'count', id: '1' }],
timeField: '@timestamp',
bucketAggs: [
{
type: 'date_histogram',
id: '2',
field: '@time',
settings: { min_doc_count: '1', interval: '1w' },
},
],
});
expect(query.aggs['2'].date_histogram.interval).toBeUndefined();
expect(query.aggs['2'].date_histogram.calendar_interval).toBe('1w');
});
}); });
}); });
}); });

View File

@ -100,6 +100,7 @@ export class ElasticQueryBuilder {
getDateHistogramAgg(aggDef: DateHistogram) { getDateHistogramAgg(aggDef: DateHistogram) {
const esAgg: any = {}; const esAgg: any = {};
const settings = aggDef.settings || {}; const settings = aggDef.settings || {};
const calendarIntervals: string[] = ['1w', '1M', '1q', '1y'];
esAgg.field = aggDef.field || this.timeField; esAgg.field = aggDef.field || this.timeField;
esAgg.min_doc_count = settings.min_doc_count || 0; esAgg.min_doc_count = settings.min_doc_count || 0;
@ -115,7 +116,11 @@ export class ElasticQueryBuilder {
const interval = settings.interval === 'auto' ? '${__interval_ms}ms' : settings.interval; const interval = settings.interval === 'auto' ? '${__interval_ms}ms' : settings.interval;
if (interval !== undefined && calendarIntervals.includes(interval)) {
esAgg.calendar_interval = interval;
} else {
esAgg.fixed_interval = interval; esAgg.fixed_interval = interval;
}
return esAgg; return esAgg;
} }