feat: support function-format parameters in Insight Data API (#93)

* feat: support function-format parameters in Insight Data API

* chore: update license header

* chore: format code

* feat: return `complex_fields` in the view API

* feat: include system cluster rollup setting in App Settings API response

* chore: update release notes

* fix: remove func `sum_func_value_in_group` from using aggregation `date_histogram`

* fix: generate unique group id for aggregation name

* fix: sort not work when using function `sum_func_value_in_group`

* feat: update view template
This commit is contained in:
silenceqi 2025-01-23 20:08:19 +08:00 committed by GitHub
parent 67447b876c
commit ca243b1ce5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 744 additions and 612 deletions

View File

@ -3567,578 +3567,3 @@ POST $[[SETUP_INDEX_PREFIX]]layout/$[[SETUP_DOC_TYPE]]/cicmhbt3q95ich72lrvg
"type": "workspace",
"is_fixed": true
}
#shard level
PUT $[[SETUP_INDEX_PREFIX]]metric/_doc/bD2jH5QB7KvGccywNCH9
{
"id": "bD2jH5QB7KvGccywNCH9",
"name": "Indexing Rate",
"key": "indexing_rate",
"level": "shard",
"formula": "bD2jH5QB7KvGccywNCH9/{{.bucket_size_in_second}}",
"items": [
{
"name": "bD2jH5QB7KvGccywNCH9",
"field": "payload.elasticsearch.shard_stats.indexing.index_total",
"statistic": "rate"
}
],
"statistics": ["rate"],
"format": "number",
"unit": "doc/s",
"builtin": true,
"created": "2025-01-09T14:30:56.63155+08:00",
"updated": "2025-01-09T14:30:56.63155+08:00"
}
PUT $[[SETUP_INDEX_PREFIX]]metric/_doc/bD2jH5QB7KvGccywNCH1
{
"id": "bD2jH5QB7KvGccywNCH1",
"name": "Shard Storage",
"key": "shard_storage",
"level": "shard",
"formula": "bD2jH5QB7KvGccywNCH1",
"items": [
{
"name": "bD2jH5QB7KvGccywNCH1",
"field": "payload.elasticsearch.shard_stats.store.size_in_bytes",
"statistic": "max"
}
],
"statistics": ["max", "min", "sum", "avg", "p99", "medium"],
"format": "bytes",
"unit": "",
"builtin": true,
"created": "2025-01-09T14:30:56.63155+08:00",
"updated": "2025-01-09T14:30:56.63155+08:00"
}
PUT $[[SETUP_INDEX_PREFIX]]metric/_doc/bD2jH5QB7KvGccywNCH5
{
"id": "bD2jH5QB7KvGccywNCH5",
"name": "Document Count",
"key": "doc_count",
"level": "shard",
"formula": "bD2jH5QB7KvGccywNCH5",
"items": [
{
"name": "bD2jH5QB7KvGccywNCH5",
"field": "payload.elasticsearch.shard_stats.docs.count",
"statistic": "max"
}
],
"statistics": ["max", "min", "sum", "avg", "p99", "medium"],
"format": "number",
"unit": "",
"builtin": true,
"created": "2025-01-09T14:30:56.63155+08:00",
"updated": "2025-01-09T14:30:56.63155+08:00"
}
PUT $[[SETUP_INDEX_PREFIX]]metric/_doc/bD2jH5QB7KvGccywNCH2
{
"id": "bD2jH5QB7KvGccywNCH2",
"name": "Search Rate",
"key": "search_rate",
"level": "shard",
"formula": "bD2jH5QB7KvGccywNCH2/{{.bucket_size_in_second}}",
"items": [
{
"name": "bD2jH5QB7KvGccywNCH2",
"field": "payload.elasticsearch.shard_stats.search.query_total",
"statistic": "rate"
}
],
"statistics": ["rate"],
"format": "number",
"unit": "doc/s",
"builtin": true,
"created": "2025-01-09T14:30:56.63155+08:00",
"updated": "2025-01-09T14:30:56.63155+08:00"
}
PUT $[[SETUP_INDEX_PREFIX]]metric/_doc/bD2jH5QB7KvGccywNCH3
{
"id": "bD2jH5QB7KvGccywNCH3",
"name": "Indexing Latency",
"key": "indexing_latency",
"level": "shard",
"formula": "bD2jH5QB7KvGccywNCx3/bD2jH5QB7KvGccywNCH3",
"items": [
{
"name": "bD2jH5QB7KvGccywNCx3",
"field": "payload.elasticsearch.shard_stats.indexing.index_total",
"statistic": "rate"
},
{
"name": "bD2jH5QB7KvGccywNCH3",
"field": "payload.elasticsearch.shard_stats.indexing.index_time_in_millis",
"statistic": "rate"
}
],
"statistics": ["rate"],
"format": "number",
"unit": "ms",
"builtin": true,
"created": "2025-01-09T14:30:56.63155+08:00",
"updated": "2025-01-09T14:30:56.63155+08:00"
}
PUT $[[SETUP_INDEX_PREFIX]]metric/_doc/bD2jH5QB7KvGccywNCH4
{
"id": "bD2jH5QB7KvGccywNCH4",
"name": "Search Latency",
"key": "search_latency",
"level": "shard",
"formula": "bD2jH5QB7KvGccywNCx4/bD2jH5QB7KvGccywNCH4",
"items": [
{
"name": "bD2jH5QB7KvGccywNCH4",
"field": "payload.elasticsearch.shard_stats.search.query_total",
"statistic": "rate"
},
{
"name": "bD2jH5QB7KvGccywNCx4",
"field": "payload.elasticsearch.shard_stats.search.query_time_in_millis",
"statistic": "rate"
}
],
"statistics": ["rate"],
"format": "number",
"unit": "ms",
"builtin": true,
"created": "2025-01-09T14:30:56.63155+08:00",
"updated": "2025-01-09T14:30:56.63155+08:00"
}
PUT $[[SETUP_INDEX_PREFIX]]metric/_doc/bD2jH5QB7KvGccywNCH6
{
"id": "bD2jH5QB7KvGccywNCH6",
"name": "Segment Count",
"key": "segment_count",
"level": "shard",
"formula": "bD2jH5QB7KvGccywNCH6",
"items": [
{
"name": "bD2jH5QB7KvGccywNCH6",
"field": "payload.elasticsearch.shard_stats.segments.count",
"statistic": "max"
}
],
"statistics": ["max", "min", "sum", "avg", "p99", "medium"],
"format": "number",
"unit": "",
"builtin": true,
"created": "2025-01-09T14:30:56.63155+08:00",
"updated": "2025-01-09T14:30:56.63155+08:00"
}
PUT $[[SETUP_INDEX_PREFIX]]metric/_doc/bD2jH5QB7KvGccywNCH7
{
"id": "bD2jH5QB7KvGccywNCH7",
"name": "Segment memory",
"key": "segment_memory",
"level": "shard",
"formula": "bD2jH5QB7KvGccywNCH7",
"items": [
{
"name": "bD2jH5QB7KvGccywNCH7",
"field": "payload.elasticsearch.shard_stats.segments.memory_in_bytes",
"statistic": "max"
}
],
"statistics": ["max", "min", "sum", "avg", "p99", "medium"],
"format": "number",
"unit": "",
"builtin": true,
"created": "2025-01-09T14:30:56.63155+08:00",
"updated": "2025-01-09T14:30:56.63155+08:00"
}
#indices level
PUT $[[SETUP_INDEX_PREFIX]]metric/_doc/aD2jH5QB7KvGccywNCH9
{
"id": "aD2jH5QB7KvGccywNCH9",
"name": "Indexing Rate",
"key": "indexing_rate",
"level": "indices",
"formula": "aD2jH5QB7KvGccywNCH9/{{.bucket_size_in_second}}",
"items": [
{
"name": "aD2jH5QB7KvGccywNCH9",
"field": "payload.elasticsearch.index_stats.primaries.indexing.index_total",
"statistic": "rate"
}
],
"statistics": ["rate"],
"format": "number",
"unit": "doc/s",
"builtin": true,
"created": "2025-01-09T14:30:56.63155+08:00",
"updated": "2025-01-09T14:30:56.63155+08:00"
}
PUT $[[SETUP_INDEX_PREFIX]]metric/_doc/aD2jH5QB7KvGccywNCH1
{
"id": "aD2jH5QB7KvGccywNCH1",
"name": "Index Storage",
"key": "index_storage",
"level": "indices",
"formula": "aD2jH5QB7KvGccywNCH1",
"items": [
{
"name": "aD2jH5QB7KvGccywNCH1",
"field": "payload.elasticsearch.index_stats.total.store.size_in_bytes",
"statistic": "max"
}
],
"statistics": ["max", "min", "sum", "avg", "p99", "medium"],
"format": "bytes",
"unit": "",
"builtin": true,
"created": "2025-01-09T14:30:56.63155+08:00",
"updated": "2025-01-09T14:30:56.63155+08:00"
}
PUT $[[SETUP_INDEX_PREFIX]]metric/_doc/aD2jH5QB7KvGccywNCH5
{
"id": "aD2jH5QB7KvGccywNCH5",
"name": "Document Count",
"key": "doc_count",
"level": "indices",
"formula": "aD2jH5QB7KvGccywNCH5",
"items": [
{
"name": "aD2jH5QB7KvGccywNCH5",
"field": "payload.elasticsearch.index_stats.total.docs.count",
"statistic": "max"
}
],
"statistics": ["max", "min", "sum", "avg", "p99", "medium"],
"format": "number",
"unit": "",
"builtin": true,
"created": "2025-01-09T14:30:56.63155+08:00",
"updated": "2025-01-09T14:30:56.63155+08:00"
}
PUT $[[SETUP_INDEX_PREFIX]]metric/_doc/aD2jH5QB7KvGccywNCH2
{
"id": "aD2jH5QB7KvGccywNCH2",
"name": "Search Rate",
"key": "search_rate",
"level": "indices",
"formula": "aD2jH5QB7KvGccywNCH2/{{.bucket_size_in_second}}",
"items": [
{
"name": "aD2jH5QB7KvGccywNCH2",
"field": "payload.elasticsearch.index_stats.total.search.query_total",
"statistic": "rate"
}
],
"statistics": ["rate"],
"format": "number",
"unit": "doc/s",
"builtin": true,
"created": "2025-01-09T14:30:56.63155+08:00",
"updated": "2025-01-09T14:30:56.63155+08:00"
}
PUT $[[SETUP_INDEX_PREFIX]]metric/_doc/aD2jH5QB7KvGccywNCH3
{
"id": "aD2jH5QB7KvGccywNCH3",
"name": "Indexing Latency",
"key": "indexing_latency",
"level": "indices",
"formula": "aD2jH5QB7KvGccywNCx3/aD2jH5QB7KvGccywNCH3",
"items": [
{
"name": "aD2jH5QB7KvGccywNCH3",
"field": "payload.elasticsearch.index_stats.primaries.indexing.index_total",
"statistic": "rate"
},
{
"name": "aD2jH5QB7KvGccywNCx3",
"field": "payload.elasticsearch.index_stats.primaries.indexing.index_time_in_millis",
"statistic": "rate"
}
],
"statistics": ["rate"],
"format": "number",
"unit": "ms",
"builtin": true,
"created": "2025-01-09T14:30:56.63155+08:00",
"updated": "2025-01-09T14:30:56.63155+08:00"
}
PUT $[[SETUP_INDEX_PREFIX]]metric/_doc/aD2jH5QB7KvGccywNCH4
{
"id": "aD2jH5QB7KvGccywNCH4",
"name": "Search Latency",
"key": "search_latency",
"level": "indices",
"formula": "aD2jH5QB7KvGccywNCx4/aD2jH5QB7KvGccywNCH4",
"items": [
{
"name": "aD2jH5QB7KvGccywNCH4",
"field": "payload.elasticsearch.index_stats.total.search.query_total",
"statistic": "rate"
},
{
"name": "aD2jH5QB7KvGccywNCx4",
"field": "payload.elasticsearch.index_stats.total.search.query_time_in_millis",
"statistic": "rate"
}
],
"statistics": ["rate"],
"format": "number",
"unit": "ms",
"builtin": true,
"created": "2025-01-09T14:30:56.63155+08:00",
"updated": "2025-01-09T14:30:56.63155+08:00"
}
PUT $[[SETUP_INDEX_PREFIX]]metric/_doc/aD2jH5QB7KvGccywNCH6
{
"id": "aD2jH5QB7KvGccywNCH6",
"name": "Segment Count",
"key": "segment_count",
"level": "indices",
"formula": "aD2jH5QB7KvGccywNCH6",
"items": [
{
"name": "aD2jH5QB7KvGccywNCH6",
"field": "payload.elasticsearch.index_stats.total.segments.count",
"statistic": "max"
}
],
"statistics": ["max", "min", "sum", "avg", "p99", "medium"],
"format": "number",
"unit": "",
"builtin": true,
"created": "2025-01-09T14:30:56.63155+08:00",
"updated": "2025-01-09T14:30:56.63155+08:00"
}
PUT $[[SETUP_INDEX_PREFIX]]metric/_doc/aD2jH5QB7KvGccywNCH7
{
"id": "aD2jH5QB7KvGccywNCH7",
"name": "Segment memory",
"key": "segment_memory",
"level": "indices",
"formula": "aD2jH5QB7KvGccywNCH7",
"items": [
{
"name": "aD2jH5QB7KvGccywNCH7",
"field": "payload.elasticsearch.index_stats.total.segments.memory_in_bytes",
"statistic": "max"
}
],
"statistics": ["max", "min", "sum", "avg", "p99", "medium"],
"format": "bytes",
"unit": "",
"builtin": true,
"created": "2025-01-09T14:30:56.63155+08:00",
"updated": "2025-01-09T14:30:56.63155+08:00"
}
#node level
PUT $[[SETUP_INDEX_PREFIX]]metric/_doc/jD2jH5QB7KvGccywNCH9
{
"id": "jD2jH5QB7KvGccywNCH9",
"name": "Indexing Rate",
"key": "indexing_rate",
"level": "node",
"formula": "jD2jH5QB7KvGccywH9/{{.bucket_size_in_second}}",
"items": [
{
"name": "jD2jH5QB7KvGccywH9",
"field": "payload.elasticsearch.node_stats.indices.indexing.index_total",
"statistic": "derivative"
}
],
"statistics": ["rate"],
"format": "number",
"unit": "doc/s",
"builtin": true,
"created": "2025-01-09T14:30:56.63155+08:00",
"updated": "2025-01-09T14:30:56.63155+08:00"
}
PUT $[[SETUP_INDEX_PREFIX]]metric/_doc/jD2jH5QB7KvGccywNCH4
{
"id": "jD2jH5QB7KvGccywNCH4",
"name": "Process CPU Usage",
"key": "process_cpu_used",
"level": "node",
"formula": "jD2jH5QB7KvGccywNCH4",
"items": [
{
"name": "jD2jH5QB7KvGccywNCH4",
"field": "payload.elasticsearch.node_stats.process.cpu.percent",
"statistic": "avg"
}
],
"statistics": ["max", "min", "sum", "avg", "p99", "medium"],
"format": "",
"unit": "%",
"builtin": true,
"created": "2025-01-09T14:30:56.63155+08:00",
"updated": "2025-01-09T14:30:56.63155+08:00"
}
PUT $[[SETUP_INDEX_PREFIX]]metric/_doc/jD2jH5QB7KvGccywNCH3
{
"id": "jD2jH5QB7KvGccywNCH3",
"name": "JVM Heap Usage",
"key": "jvm_heap_used",
"level": "node",
"formula": "jD2jH5QB7KvGccywNCH3",
"items": [
{
"name": "jD2jH5QB7KvGccywNCH3",
"field": "payload.elasticsearch.node_stats.jvm.mem.heap_used_in_bytes",
"statistic": "max"
}
],
"statistics": ["max", "min", "sum", "avg", "p99", "medium"],
"format": "bytes",
"unit": "",
"builtin": true,
"created": "2025-01-09T14:30:56.63155+08:00",
"updated": "2025-01-09T14:30:56.63155+08:00"
}
PUT $[[SETUP_INDEX_PREFIX]]metric/_doc/jD2jH5QB7KvGccywNCH1
{
"id": "jD2jH5QB7KvGccywNCH1",
"name": "Indexing Latency",
"key": "indexing_latency",
"level": "node",
"formula": "jD2jH5QB7KvGccywNCx1/jD2jH5QB7KvGccywNCH1",
"items": [
{
"name": "jD2jH5QB7KvGccywNCH1",
"field": "payload.elasticsearch.node_stats.indices.indexing.index_total",
"statistic": "rate"
},
{
"name": "jD2jH5QB7KvGccywNCx1",
"field": "payload.elasticsearch.node_stats.indices.indexing.index_time_in_millis",
"statistic": "rate"
}
],
"statistics": ["rate"],
"format": "number",
"unit": "ms",
"builtin": true,
"created": "2025-01-09T14:30:56.63155+08:00",
"updated": "2025-01-09T14:30:56.63155+08:00"
}
PUT $[[SETUP_INDEX_PREFIX]]metric/_doc/jD2jH5QB7KvGccywNCH0
{
"id": "jD2jH5QB7KvGccywNCH0",
"name": "Search Rate",
"key": "search_rate",
"level": "node",
"formula": "jD2jH5QB7KvGccywH0/{{.bucket_size_in_second}}",
"items": [
{
"name": "jD2jH5QB7KvGccywH0",
"field": "payload.elasticsearch.node_stats.indices.search.query_total",
"statistic": "derivative"
}
],
"statistics": ["rate"],
"format": "number",
"unit": "query/s",
"builtin": true,
"created": "2025-01-09T14:30:56.63155+08:00",
"updated": "2025-01-09T14:30:56.63155+08:00"
}
PUT $[[SETUP_INDEX_PREFIX]]metric/_doc/jD2jH5QB7KvGccywNCH9
{
"id": "jD2jH5QB7KvGccywNCH9",
"name": "Indexing Rate",
"key": "indexing_rate",
"level": "node",
"formula": "jD2jH5QB7KvGccywH9/{{.bucket_size_in_second}}",
"items": [
{
"name": "jD2jH5QB7KvGccywH9",
"field": "payload.elasticsearch.node_stats.indices.indexing.index_total",
"statistic": "rate"
}
],
"statistics": ["rate"],
"format": "number",
"unit": "doc/s",
"builtin": true,
"created": "2025-01-09T14:30:56.63155+08:00",
"updated": "2025-01-09T14:30:56.63155+08:00"
}
PUT $[[SETUP_INDEX_PREFIX]]metric/_doc/jD2jH5QB7KvGccywNCH5
{
"id": "jD2jH5QB7KvGccywNCH5",
"name": "Search Latency",
"key": "search_latency",
"level": "node",
"formula": "jD2jH5QB7KvGccywNCx5/jD2jH5QB7KvGccywNCH5",
"items": [
{
"name": "jD2jH5QB7KvGccywNCH5",
"field": "payload.elasticsearch.node_stats.indices.search.query_total",
"statistic": "rate"
},
{
"name": "jD2jH5QB7KvGccywNCx5",
"field": "payload.elasticsearch.node_stats.indices.search.query_time_in_millis",
"statistic": "rate"
}
],
"statistics": ["rate"],
"format": "number",
"unit": "ms",
"builtin": true,
"created": "2025-01-09T14:30:56.63155+08:00",
"updated": "2025-01-09T14:30:56.63155+08:00"
}
PUT $[[SETUP_INDEX_PREFIX]]metric/_doc/jD2jH5QB7KvGccywNCH6
{
"id": "jD2jH5QB7KvGccywNCH6",
"name": "Indices Storage",
"key": "indices_storage",
"level": "node",
"formula": "jD2jH5QB7KvGccywNCH6",
"items": [
{
"name": "jD2jH5QB7KvGccywNCH6",
"field": "payload.elasticsearch.node_stats.indices.store.size_in_bytes",
"statistic": "max"
}
],
"statistics": ["max", "min", "sum", "avg", "p99", "medium"],
"format": "bytes",
"unit": "",
"builtin": true,
"created": "2025-01-09T14:30:56.63155+08:00",
"updated": "2025-01-09T14:30:56.63155+08:00"
}
PUT $[[SETUP_INDEX_PREFIX]]metric/_doc/jD2jH5QB7KvGccywNCH7
{
"id": "jD2jH5QB7KvGccywNCH7",
"name": "Document Count",
"key": "doc_count",
"level": "node",
"formula": "jD2jH5QB7KvGccywNCH7",
"items": [
{
"name": "jD2jH5QB7KvGccywNCH7",
"field": "payload.elasticsearch.node_stats.indices.docs.count",
"statistic": "max"
}
],
"statistics": ["max", "min", "sum", "avg", "p99", "medium"],
"format": "number",
"unit": "",
"builtin": true,
"created": "2025-01-09T14:30:56.63155+08:00",
"updated": "2025-01-09T14:30:56.63155+08:00"
}

File diff suppressed because one or more lines are too long

View File

@ -11,6 +11,7 @@ Information about release notes of INFINI Console is provided here.
### Breaking changes
### Features
- Support function-format parameters in Insight Data API
- Support configuring multiple hosts when creating a cluster
### Bug fix
### Improvements

View File

@ -7,6 +7,14 @@ title: "版本历史"
这里是 INFINI Console 历史版本发布的相关说明。
## Latest (In development)
### Breaking changes
### Features
- Insight Data API 支持函数格式查询,方便拓展查询功能
- 创建集群时支持配置多个主机地址,增强集群的高可用性
### Bug fix
### Improvements
## 1.28.0 (2025-01-11)

24
main.go
View File

@ -33,6 +33,7 @@ import (
"infini.sh/framework/core/api"
"infini.sh/framework/core/host"
model2 "infini.sh/framework/core/model"
"infini.sh/framework/core/util"
elastic2 "infini.sh/framework/modules/elastic"
_ "time/tzdata"
@ -185,6 +186,29 @@ func main() {
}
return err
})
api.RegisterAppSetting("system_cluster", func() interface{} {
client := elastic.GetClient(global.MustLookupString(elastic.GlobalSystemElasticsearchID))
ver := client.GetVersion()
if ver.Distribution != elastic.Easysearch {
return map[string]interface{}{
"rollup_enabled": false,
}
}
settings, err := client.GetClusterSettings(nil)
if err != nil {
log.Errorf("failed to get cluster settings with system cluster: %v", err)
return nil
}
rollupEnabled, _ := util.GetMapValueByKeys([]string{"persistent", "rollup", "search", "enabled"}, settings)
rollupEnabledValue := false
if v, ok := rollupEnabled.(string); ok && v == "true" {
rollupEnabledValue = true
}
return map[string]interface{}{
"rollup_enabled": rollupEnabledValue,
}
})
}
if !global.Env().SetupRequired() {

View File

@ -35,6 +35,29 @@ import (
"infini.sh/framework/core/util"
)
const (
AggFuncCount = "count"
AggFuncAvg = "avg"
AggFuncSum = "sum"
AggFuncMin = "min"
AggFuncMax = "max"
AggFuncMedium = "medium"
AggFuncValueCount = "value_count"
AggFuncCardinality = "cardinality"
AggFuncDerivative = "derivative"
AggFuncRate = "rate"
AggFuncPercent99 = "p99"
AggFuncPercent95 = "p95"
AggFuncPercent90 = "p90"
AggFuncPercent80 = "p80"
AggFuncPercent50 = "p50"
AggFuncLatest = "latest"
AggFuncLatency = "latency"
AggFuncSumFuncValueInGroup = "sum_func_value_in_group"
AggFuncRateSumFuncValueInGroup = "rate_sum_func_value_in_group"
AggFuncLatencySumFuncValueInGroup = "latency_sum_func_value_in_group"
)
type Metric struct {
AggTypes []string `json:"agg_types,omitempty"`
IndexPattern string `json:"index_pattern,omitempty"`
@ -108,14 +131,55 @@ func (m *Metric) GenerateExpression() (string, error) {
return string(expressionBytes), nil
}
func (m *Metric) AutoTimeBeforeGroup() bool {
// shouldUseAggregation checks if any item's statistic or function exists in the provided aggFuncs list.
// If a match is found, it returns true; otherwise, it returns false.
func (m *Metric) shouldUseAggregation(aggFuncs []string) bool {
for _, item := range m.Items {
if item.Statistic == "derivative" {
return false
// Default to item's Statistic field
statistic := item.Statistic
// If Function is defined, use its first key as the statistic
if item.Function != nil {
for key := range item.Function {
statistic = key
break
}
}
// Check if statistic is in the aggregation function list
if util.StringInArray(aggFuncs, statistic) {
return true
}
}
return true
return false
}
// AutoTimeBeforeGroup determines if date aggregation should be applied before terms aggregation.
// Returns false if the metric uses any of the specified aggregation functions.
func (m *Metric) AutoTimeBeforeGroup() bool {
return !m.shouldUseAggregation([]string{
AggFuncDerivative,
AggFuncRate,
AggFuncLatency,
AggFuncRateSumFuncValueInGroup,
AggFuncLatencySumFuncValueInGroup,
})
}
// UseBucketSort determines whether bucket sorting should be used for aggregation.
// Returns false if the metric contains specific aggregation functions that require alternative handling.
func (m *Metric) UseBucketSort() bool {
return m.shouldUseAggregation([]string{
AggFuncDerivative,
AggFuncRate,
AggFuncLatency,
AggFuncSumFuncValueInGroup,
AggFuncRateSumFuncValueInGroup,
AggFuncLatencySumFuncValueInGroup,
})
}
func (m *Metric) ValidateSortKey() error {
if len(m.Sort) == 0 {
return nil
@ -143,6 +207,10 @@ type MetricItem struct {
Field string `json:"field"`
FieldType string `json:"field_type,omitempty"`
Statistic string `json:"statistic,omitempty"`
//Function specifies the calculation details for the metric,
//including the aggregation type and any associated parameters.
Function map[string]interface{} `json:"function,omitempty"`
}
type MetricDataItem struct {

View File

@ -128,10 +128,11 @@ func (h *APIHandler) HandleGetViewListAction(w http.ResponseWriter, req *http.Re
"title": hit.Source["title"],
"viewName": hit.Source["viewName"],
},
"score": 0,
"type": "index-pattern",
"namespaces": []string{"default"},
"updated_at": hit.Source["updated_at"],
"score": 0,
"type": "index-pattern",
"namespaces": []string{"default"},
"updated_at": hit.Source["updated_at"],
"complex_fields": hit.Source["complex_fields"],
}
savedObjects = append(savedObjects, savedObject)
}
@ -329,6 +330,8 @@ func (h *APIHandler) HandleBulkGetViewAction(w http.ResponseWriter, req *http.Re
"namespaces": []string{"default"},
"migrationVersion": map[string]interface{}{"index-pattern": "7.6.0"},
"updated_at": hit.Source["updated_at"],
"complex_fields": hit.Source["complex_fields"],
"references": []interface{}{},
}
savedObjects = append(savedObjects, savedObject)
}

View File

@ -0,0 +1,35 @@
// Copyright (C) INFINI Labs & INFINI LIMITED.
//
// The INFINI Console is offered under the GNU Affero General Public License v3.0
// and as commercial software.
//
// For commercial licensing, contact us at:
// - Website: infinilabs.com
// - Email: hello@infini.ltd
//
// Open Source licensed under AGPL V3:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
/* Copyright © INFINI Ltd. All rights reserved.
* Web: https://infinilabs.com
* Email: hello#infini.ltd */
package insight
type Function interface {
//GenerateAggregation generates aggregation for specify function, e.g. rate, latency ...
//
// metricName: The name of the metric to be calculated (used as an identifier in the aggregation).
GenerateAggregation(metricName string) (map[string]interface{}, error)
}

View File

@ -0,0 +1,86 @@
// Copyright (C) INFINI Labs & INFINI LIMITED.
//
// The INFINI Console is offered under the GNU Affero General Public License v3.0
// and as commercial software.
//
// For commercial licensing, contact us at:
// - Website: infinilabs.com
// - Email: hello@infini.ltd
//
// Open Source licensed under AGPL V3:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
/* Copyright © INFINI Ltd. All rights reserved.
* Web: https://infinilabs.com
* Email: hello#infini.ltd */
package function
import (
"fmt"
"infini.sh/framework/core/util"
)
// Latency represents a function that calculates latency as a ratio of two metrics.
// Divisor: The denominator in the latency calculation.
// Dividend: The numerator in the latency calculation.
type Latency struct {
Divisor string `json:"divisor"`
Dividend string `json:"dividend"`
}
// GenerateAggregation implements the `Function` interface and generates an aggregation for the `latency` function.
func (p *Latency) GenerateAggregation(metricName string) (map[string]interface{}, error) {
if p.Dividend == "" || p.Divisor == "" {
return nil, fmt.Errorf("empty divisor or dividend for agg func: latency")
}
var (
divisorAggID = util.GetUUID()
dividendAggID = util.GetUUID()
divisorBaseAggID = util.GetUUID()
dividendBaseAggID = util.GetUUID()
)
return util.MapStr{
dividendBaseAggID: util.MapStr{
"max": util.MapStr{
"field": p.Dividend,
},
},
dividendAggID: util.MapStr{
"derivative": util.MapStr{
"buckets_path": dividendBaseAggID,
},
},
divisorBaseAggID: util.MapStr{
"max": util.MapStr{
"field": p.Divisor,
},
},
divisorAggID: util.MapStr{
"derivative": util.MapStr{
"buckets_path": divisorBaseAggID,
},
},
metricName: util.MapStr{
"bucket_script": util.MapStr{
"buckets_path": util.MapStr{
"dividend": dividendAggID,
"divisor": divisorAggID,
},
"script": "params.dividend / params.divisor",
},
},
}, nil
}

View File

@ -0,0 +1,126 @@
// Copyright (C) INFINI Labs & INFINI LIMITED.
//
// The INFINI Console is offered under the GNU Affero General Public License v3.0
// and as commercial software.
//
// For commercial licensing, contact us at:
// - Website: infinilabs.com
// - Email: hello@infini.ltd
//
// Open Source licensed under AGPL V3:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
/* Copyright © INFINI Ltd. All rights reserved.
* Web: https://infinilabs.com
* Email: hello#infini.ltd */
package function
import (
"fmt"
"infini.sh/framework/core/util"
)
// LatencySumFuncValueInGroup represents a function that calculates the latency as a sum of values in a grouped aggregation.
// It combines a `Latency` calculation with a grouping operation and applies a specified aggregation function within each group.
// The group defines the field to group by, the aggregation function to apply, and the maximum number of groups to include.
type LatencySumFuncValueInGroup struct {
Latency // Embeds the Latency struct for fields `Divisor` and `Dividend`.
Group struct {
Field string `json:"field"` // The field used for grouping.
Func string `json:"func"` // The aggregation function to apply within each group (e.g., max, min, sum).
Size uint `json:"size"` // The maximum number of groups to return.
} `json:"group"`
}
// GenerateAggregation implements the `Function` interface and generates an aggregation for the `latency_sum_func_value_in_group` function.
func (p *LatencySumFuncValueInGroup) GenerateAggregation(metricName string) (map[string]interface{}, error) {
// Validate that the divisor and dividend are not empty.
if p.Dividend == "" || p.Divisor == "" {
return nil, fmt.Errorf("empty divisor or dividend for agg func: latency")
}
// Validate that the group field is not empty.
if p.Group.Field == "" {
return nil, fmt.Errorf("empty group field for agg func: latency_sum_func_value_in_group")
}
// Set default function to `max` if no function is provided.
if p.Group.Func == "" {
p.Group.Func = "max"
}
// Set default size for the group if none is provided.
if p.Group.Size == 0 {
p.Group.Size = 65536 // Default to a large size to include most groups.
}
var (
divisorBaseAggID = util.GetUUID()
dividendBaseAggID = util.GetUUID()
sumDivisorAggID = util.GetUUID()
sumDividendAggID = util.GetUUID()
derivativeDivisorAggID = util.GetUUID()
derivativeDividendAggID = util.GetUUID()
groupID = fmt.Sprintf("sum_group_%s", util.GetUUID())
)
return util.MapStr{
groupID: util.MapStr{
"aggs": util.MapStr{
divisorBaseAggID: util.MapStr{
p.Group.Func: util.MapStr{
"field": p.Divisor,
},
},
dividendBaseAggID: util.MapStr{
p.Group.Func: util.MapStr{
"field": p.Dividend,
},
},
},
"terms": util.MapStr{
"field": p.Group.Field,
"size": p.Group.Size,
},
},
sumDivisorAggID: util.MapStr{
"sum_bucket": util.MapStr{
"buckets_path": fmt.Sprintf("%s>%s", groupID, divisorBaseAggID),
},
},
sumDividendAggID: util.MapStr{
"sum_bucket": util.MapStr{
"buckets_path": fmt.Sprintf("%s>%s", groupID, dividendBaseAggID),
},
},
derivativeDivisorAggID: util.MapStr{
"derivative": util.MapStr{
"buckets_path": sumDivisorAggID,
},
},
derivativeDividendAggID: util.MapStr{
"derivative": util.MapStr{
"buckets_path": sumDividendAggID,
},
},
metricName: util.MapStr{
"bucket_script": util.MapStr{
"buckets_path": util.MapStr{
"dividend": derivativeDividendAggID,
"divisor": derivativeDivisorAggID,
},
"script": "params.dividend / params.divisor",
},
},
}, nil
}

View File

@ -0,0 +1,85 @@
// Copyright (C) INFINI Labs & INFINI LIMITED.
//
// The INFINI Console is offered under the GNU Affero General Public License v3.0
// and as commercial software.
//
// For commercial licensing, contact us at:
// - Website: infinilabs.com
// - Email: hello@infini.ltd
//
// Open Source licensed under AGPL V3:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
/* Copyright © INFINI Ltd. All rights reserved.
* Web: https://infinilabs.com
* Email: hello#infini.ltd */
package function
import (
"fmt"
"infini.sh/framework/core/util"
)
// Base represents a common structure for functions that operate on a specific field.
// Field: The name of the field to be aggregated.
type Base struct {
Field string `json:"field"` // The field on which the aggregation operates.
}
// Rate extends the Base structure to include scaling for rate calculations.
// Scale: A multiplier applied to the rate calculation for normalization.
type Rate struct {
Base
Scale uint `json:"scale"` // The scaling factor used in the rate calculation.
}
// GenerateAggregation implements the `Function` interface and generates an aggregation for the `rate` function.
func (p *Rate) GenerateAggregation(metricName string) (map[string]interface{}, error) {
if p.Field == "" {
return nil, fmt.Errorf("empty field for agg func: rate")
}
if p.Scale == 0 {
p.Scale = 1
}
var (
aggID = util.GetUUID()
derivativeAggID = util.GetUUID()
)
return util.MapStr{
aggID: util.MapStr{
"max": util.MapStr{
"field": p.Field,
},
},
derivativeAggID: util.MapStr{
"derivative": util.MapStr{
"buckets_path": aggID,
},
},
metricName: util.MapStr{
"bucket_script": util.MapStr{
"buckets_path": util.MapStr{
"value": derivativeAggID,
},
"script": util.MapStr{
"source": "params.value * params.scale",
"params": util.MapStr{
"scale": p.Scale,
},
},
},
},
}, nil
}

View File

@ -0,0 +1,119 @@
// Copyright (C) INFINI Labs & INFINI LIMITED.
//
// The INFINI Console is offered under the GNU Affero General Public License v3.0
// and as commercial software.
//
// For commercial licensing, contact us at:
// - Website: infinilabs.com
// - Email: hello@infini.ltd
//
// Open Source licensed under AGPL V3:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
/* Copyright © INFINI Ltd. All rights reserved.
* Web: https://infinilabs.com
* Email: hello#infini.ltd */
package function
import (
"fmt"
"infini.sh/framework/core/util"
)
// RateSumFuncValueInGroup represents a function that calculates the rate as a sum of values in a grouped aggregation.
// It combines a `Rate` calculation with a grouping operation and applies a specified aggregation function within each group.
// The group defines the field to group by, the aggregation function to apply, and the maximum number of groups to include.
type RateSumFuncValueInGroup struct {
Rate // Embeds the Rate struct for fields `Field` and `Scale`.
Group struct {
Field string `json:"field"` // The field used for grouping.
Func string `json:"func"` // The aggregation function to apply within each group (e.g., max, min, sum).
Size uint `json:"size"` // The maximum number of groups to return.
} `json:"group"`
}
// GenerateAggregation implements the `Function` interface and generates an aggregation for the `rate_sum_func_value_in_group` function.
// This function constructs an aggregation pipeline that computes the sum of values for a given field within groups, calculates the derivative,
// and then applies a bucket script to scale the resulting value by a provided `scale` factor.
func (p *RateSumFuncValueInGroup) GenerateAggregation(metricName string) (map[string]interface{}, error) {
// Validate that the field is provided for the aggregation.
if p.Field == "" {
return nil, fmt.Errorf("empty field for agg func: rate_sum_func_value_in_group")
}
// Set default scale factor to 1 if not specified.
if p.Scale == 0 {
p.Scale = 1
}
// Validate that the group field is provided.
if p.Group.Field == "" {
return nil, fmt.Errorf("empty group field for agg func: rate_sum_func_value_in_group")
}
// Set default function to `max` if no function is specified for grouping.
if p.Group.Func == "" {
p.Group.Func = "max"
}
// Set default size for the group if none is specified.
if p.Group.Size == 0 {
p.Group.Size = 65536 // Default to a large size to include most groups.
}
var (
aggID = util.GetUUID()
sumBucketAggID = util.GetUUID()
derivativeAggID = util.GetUUID()
groupID = fmt.Sprintf("sum_group_%s", util.GetUUID())
)
return util.MapStr{
groupID: util.MapStr{
"aggs": util.MapStr{
aggID: util.MapStr{
p.Group.Func: util.MapStr{
"field": p.Field,
},
},
},
"terms": util.MapStr{
"field": p.Group.Field,
"size": p.Group.Size,
},
},
sumBucketAggID: util.MapStr{
"sum_bucket": util.MapStr{
"buckets_path": fmt.Sprintf("%s>%s", groupID, aggID),
},
},
derivativeAggID: util.MapStr{
"derivative": util.MapStr{
"buckets_path": sumBucketAggID,
},
},
metricName: util.MapStr{
"bucket_script": util.MapStr{
"buckets_path": util.MapStr{
"value": derivativeAggID,
},
"script": util.MapStr{
"source": "params.value * params.scale",
"params": util.MapStr{
"scale": p.Scale,
},
},
},
},
}, nil
}

View File

@ -0,0 +1,82 @@
// Copyright (C) INFINI Labs & INFINI LIMITED.
//
// The INFINI Console is offered under the GNU Affero General Public License v3.0
// and as commercial software.
//
// For commercial licensing, contact us at:
// - Website: infinilabs.com
// - Email: hello@infini.ltd
//
// Open Source licensed under AGPL V3:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
/* Copyright © INFINI Ltd. All rights reserved.
* Web: https://infinilabs.com
* Email: hello#infini.ltd */
package function
import (
"fmt"
"infini.sh/framework/core/util"
)
// SumFuncValueInGroup represents a function to calculate a sum of values in a grouped aggregation.
// It combines a `Rate` calculation with a grouping operation and a specified aggregation function within each group.
// The group defines the field to group by, the aggregation function to apply, and the maximum number of groups to include.
type SumFuncValueInGroup struct {
Rate // Embeds the Rate struct for rate-specific fields like `Field` and `Scale`.
Group struct {
Field string `json:"field"` // The field used for grouping.
Func string `json:"func"` // The aggregation function to apply within each group (e.g., max, min, sum).
Size uint `json:"size"` // The maximum number of groups to return.
} `json:"group"`
}
// GenerateAggregation implements the `Function` interface and generates an aggregation for the `sum_func_value_in_group` function.
func (p *SumFuncValueInGroup) GenerateAggregation(metricName string) (map[string]interface{}, error) {
if p.Group.Field == "" {
return nil, fmt.Errorf("empty group field for agg func: sum_func_value_in_group")
}
// Set a default aggregation function if none is specified.
if p.Group.Func == "" {
p.Group.Func = "max"
}
// Set a default size for the group if none is specified.
if p.Group.Size == 0 {
p.Group.Size = 65536 // Default to a large size to include most groups.
}
aggID := util.GetUUID()
groupID := fmt.Sprintf("sum_group_%s", util.GetUUID())
return util.MapStr{
groupID: util.MapStr{
"aggs": util.MapStr{
aggID: util.MapStr{
p.Group.Func: util.MapStr{
"field": p.Field,
},
},
},
"terms": util.MapStr{
"field": p.Group.Field,
"size": p.Group.Size,
},
},
metricName: util.MapStr{
"sum_bucket": util.MapStr{
"buckets_path": fmt.Sprintf("%s>%s", groupID, aggID),
},
},
}, nil
}

View File

@ -29,6 +29,7 @@ package insight
import (
"fmt"
"infini.sh/console/plugin/api/insight/function"
"strconv"
"strings"
@ -37,31 +38,36 @@ import (
"infini.sh/framework/core/util"
)
func generateAgg(metricItem *insight.MetricItem, timeField string) map[string]interface{} {
func generateAgg(metricItem *insight.MetricItem, timeField string) (map[string]interface{}, error) {
var (
aggType = "value_count"
field = metricItem.Field
)
if field == "" || field == "*" {
field = "_id"
if timeField != "" {
field = timeField
} else {
field = "_id"
}
}
var percent = 0.0
var isPipeline = false
switch metricItem.Statistic {
case "max", "min", "sum", "avg", "cardinality":
case insight.AggFuncMax, insight.AggFuncMin, insight.AggFuncSum, insight.AggFuncAvg, insight.AggFuncCardinality:
aggType = metricItem.Statistic
case "count", "value_count":
case insight.AggFuncCount, insight.AggFuncValueCount:
aggType = "value_count"
case "derivative":
case insight.AggFuncDerivative:
aggType = "max"
isPipeline = true
case "medium": // from es version 6.6
case insight.AggFuncMedium: // from es version 6.6
aggType = "median_absolute_deviation"
case "p99", "p95", "p90", "p80", "p50":
case insight.AggFuncPercent99, insight.AggFuncPercent95, insight.AggFuncPercent90, insight.AggFuncPercent80, insight.AggFuncPercent50:
aggType = "percentiles"
percentStr := strings.TrimPrefix(metricItem.Statistic, "p")
percent, _ = strconv.ParseFloat(percentStr, 32)
case "latest":
case insight.AggFuncLatest:
aggType = "top_hits"
}
aggValue := util.MapStr{}
@ -89,7 +95,7 @@ func generateAgg(metricItem *insight.MetricItem, timeField string) map[string]in
},
}
if !isPipeline {
return aggs
return aggs, nil
}
pipelineAggID := util.GetUUID()
aggs[pipelineAggID] = aggs[metricItem.Name]
@ -98,7 +104,53 @@ func generateAgg(metricItem *insight.MetricItem, timeField string) map[string]in
"buckets_path": pipelineAggID,
},
}
return aggs
return aggs, nil
}
func generateAggByFunction(metricItem *insight.MetricItem, timeField string) (map[string]interface{}, error) {
if metricItem.Function == nil {
return nil, fmt.Errorf("empty function for metric item: %s", metricItem.Name)
}
if len(metricItem.Function) != 1 {
return nil, fmt.Errorf("invalid function for metric item: %s: expected exactly one function name, but got zero or multiple", metricItem.Name)
}
var (
funcName string
funcParams interface{}
)
for k, v := range metricItem.Function {
funcName = k
funcParams = v
}
if funcParams == nil {
return nil, fmt.Errorf("empty params for agg func: %s", funcName)
}
funcName = strings.ToLower(funcName)
buf := util.MustToJSONBytes(funcParams)
var generator Function
switch funcName {
case insight.AggFuncRate:
generator = &function.Rate{}
case insight.AggFuncLatency:
generator = &function.Latency{}
case insight.AggFuncSumFuncValueInGroup:
generator = &function.SumFuncValueInGroup{}
case insight.AggFuncRateSumFuncValueInGroup:
generator = &function.RateSumFuncValueInGroup{}
case insight.AggFuncLatencySumFuncValueInGroup:
generator = &function.LatencySumFuncValueInGroup{}
default:
baseParams := function.Base{}
util.MustFromJSONBytes(buf, &baseParams)
newMetricItem := &insight.MetricItem{
Name: metricItem.Name,
Field: baseParams.Field,
Statistic: funcName,
}
return generateAgg(newMetricItem, timeField)
}
util.MustFromJSONBytes(buf, &generator)
return generator.GenerateAggregation(metricItem.Name)
}
func GenerateQuery(metric *insight.Metric) (interface{}, error) {
@ -109,7 +161,18 @@ func GenerateQuery(metric *insight.Metric) (interface{}, error) {
if metricItem.Name == "" {
metricItem.Name = string(rune('a' + i))
}
metricAggs := generateAgg(&metricItem, metric.TimeField)
var (
metricAggs map[string]interface{}
err error
)
if metricItem.Function != nil {
metricAggs, err = generateAggByFunction(&metricItem, metric.TimeField)
} else {
metricAggs, err = generateAgg(&metricItem, metric.TimeField)
}
if err != nil {
return nil, err
}
if err := util.MergeFields(basicAggs, metricAggs, true); err != nil {
return nil, err
}
@ -181,19 +244,24 @@ func GenerateQuery(metric *insight.Metric) (interface{}, error) {
}
if i == grpLength-1 && len(metric.Sort) > 0 {
//use bucket sort instead of terms order when time after group
if !timeBeforeGroup && len(metric.Sort) > 0 {
basicAggs["sort_field"] = util.MapStr{
"max_bucket": util.MapStr{
"buckets_path": fmt.Sprintf("time_buckets>%s", metric.Sort[0].Key),
},
if metric.UseBucketSort() && len(metric.Sort) > 0 {
sortKey := metric.Sort[0].Key
if !timeBeforeGroup {
sortKey = "sort_field"
basicAggs[sortKey] = util.MapStr{
"max_bucket": util.MapStr{
"buckets_path": fmt.Sprintf("time_buckets>%s", metric.Sort[0].Key),
},
}
}
//using 65536 as a workaround for the terms aggregation limit; the actual limit is enforced in the bucket sort step
termsCfg["size"] = 65536
basicAggs["bucket_sorter"] = util.MapStr{
"bucket_sort": util.MapStr{
"size": limit,
"sort": []util.MapStr{
{"sort_field": util.MapStr{"order": metric.Sort[0].Direction}},
{sortKey: util.MapStr{"order": metric.Sort[0].Direction}},
},
},
}
@ -214,7 +282,9 @@ func GenerateQuery(metric *insight.Metric) (interface{}, error) {
}
termsOrder = append(termsOrder, util.MapStr{sortKey: sortItem.Direction})
}
termsCfg["order"] = termsOrder
if len(termsOrder) > 0 {
termsCfg["order"] = termsOrder
}
}
}
groupAgg := util.MapStr{