mirror of
https://github.com/Graylog2/graylog2-server.git
synced 2026-03-13 09:32:21 +08:00
Feature: add numeric range aggregation support to Scripting API (#25103)
* feat: add numeric range aggregation support to Scripting API Add support for numeric range aggregations in the Scripting API, allowing users to group search results into custom numeric buckets (e.g., response times 0-100ms, 100-500ms, 500ms+). New classes: - NumberRange: value class holding optional from/to Double bounds - RangeBucket: BucketSpec implementation for numeric range buckets - ESRangeHandler, OSRangeHandler (OS2/OS3): storage backend handlers Modified: - Grouping: new "ranges" field, mutually exclusive with limit/timeunit/scaling - GroupingToBucketSpecMapper: produces RangeBucket when ranges are present - AggregationSpecToPivotMapper: respects ranges in auto-interval logic - All three ViewsBackendModule classes: register RangeBucket handlers Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add integration tests for range aggregation in ScriptingApiResourceIT Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * adding changelog * using more idiomatic code regarding the Optionals * improving conditional * records instead of Autovalue --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
4
changelog/unreleased/pr-25103.toml
Normal file
4
changelog/unreleased/pr-25103.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
type = "a"
|
||||
message = "Add range aggregation to ScriptingApi."
|
||||
|
||||
pulls = ["25103"]
|
||||
@@ -1117,6 +1117,111 @@ public class ScriptingApiResourceIT {
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@FullBackendTest
|
||||
void testRangeAggregation() {
|
||||
final ValidatableResponse validatableResponse = given()
|
||||
.spec(api.requestSpecification())
|
||||
.when()
|
||||
.body("""
|
||||
{
|
||||
"group_by": [
|
||||
{
|
||||
"field": "level",
|
||||
"ranges": [
|
||||
{"to": 2.0},
|
||||
{"from": 2.0, "to": 3.0},
|
||||
{"from": 3.0}
|
||||
]
|
||||
}
|
||||
],
|
||||
"metrics": [
|
||||
{
|
||||
"function": "count"
|
||||
}
|
||||
]
|
||||
}
|
||||
""")
|
||||
.post("/search/aggregate")
|
||||
.then()
|
||||
.log().ifStatusCodeMatches(not(200))
|
||||
.statusCode(200);
|
||||
|
||||
validatableResponse.assertThat().body("datarows", Matchers.hasSize(3));
|
||||
validateRow(validatableResponse, "*-2.0", 1);
|
||||
validateRow(validatableResponse, "2.0-3.0", 1);
|
||||
validateRow(validatableResponse, "3.0-*", 1);
|
||||
}
|
||||
|
||||
@FullBackendTest
|
||||
void testRangeAggregationWithMultipleMetrics() {
|
||||
final ValidatableResponse validatableResponse = given()
|
||||
.spec(api.requestSpecification())
|
||||
.when()
|
||||
.body("""
|
||||
{
|
||||
"group_by": [
|
||||
{
|
||||
"field": "level",
|
||||
"ranges": [
|
||||
{"to": 2.5},
|
||||
{"from": 2.5}
|
||||
]
|
||||
}
|
||||
],
|
||||
"metrics": [
|
||||
{
|
||||
"function": "count"
|
||||
},
|
||||
{
|
||||
"function": "max",
|
||||
"field": "level"
|
||||
}
|
||||
]
|
||||
}
|
||||
""")
|
||||
.post("/search/aggregate")
|
||||
.then()
|
||||
.log().ifStatusCodeMatches(not(200))
|
||||
.statusCode(200);
|
||||
|
||||
validatableResponse.assertThat().body("datarows", Matchers.hasSize(2));
|
||||
validateRow(validatableResponse, "*-2.5", 2, 2.0f);
|
||||
validateRow(validatableResponse, "2.5-*", 1, 3.0f);
|
||||
}
|
||||
|
||||
@FullBackendTest
|
||||
void testRangeAggregationSchema() {
|
||||
final ValidatableResponse validatableResponse = given()
|
||||
.spec(api.requestSpecification())
|
||||
.when()
|
||||
.body("""
|
||||
{
|
||||
"group_by": [
|
||||
{
|
||||
"field": "level",
|
||||
"ranges": [
|
||||
{"to": 2.0},
|
||||
{"from": 2.0}
|
||||
]
|
||||
}
|
||||
],
|
||||
"metrics": [
|
||||
{
|
||||
"function": "count",
|
||||
"field": "level"
|
||||
}
|
||||
]
|
||||
}
|
||||
""")
|
||||
.post("/search/aggregate")
|
||||
.then()
|
||||
.log().ifStatusCodeMatches(not(200))
|
||||
.statusCode(200);
|
||||
|
||||
validateSchema(validatableResponse, "grouping: level", "string", "level");
|
||||
validateSchema(validatableResponse, "metric: count(level)", "numeric", "level");
|
||||
}
|
||||
|
||||
private void validateSchema(ValidatableResponse response, String name, String type, String field) {
|
||||
response.assertThat().body("schema", Matchers.hasItem(
|
||||
Matchers.allOf(
|
||||
|
||||
@@ -31,6 +31,7 @@ import org.graylog.plugins.views.search.searchtypes.pivot.BucketSpec;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.Pivot;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.SeriesSpec;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.DateRangeBucket;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.RangeBucket;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.Time;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.Values;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.series.Average;
|
||||
@@ -57,6 +58,7 @@ import org.graylog.storage.elasticsearch7.views.searchtypes.pivot.ESPivot;
|
||||
import org.graylog.storage.elasticsearch7.views.searchtypes.pivot.ESPivotBucketSpecHandler;
|
||||
import org.graylog.storage.elasticsearch7.views.searchtypes.pivot.ESPivotSeriesSpecHandler;
|
||||
import org.graylog.storage.elasticsearch7.views.searchtypes.pivot.buckets.ESDateRangeHandler;
|
||||
import org.graylog.storage.elasticsearch7.views.searchtypes.pivot.buckets.ESRangeHandler;
|
||||
import org.graylog.storage.elasticsearch7.views.searchtypes.pivot.buckets.ESTimeHandler;
|
||||
import org.graylog.storage.elasticsearch7.views.searchtypes.pivot.buckets.ESValuesHandler;
|
||||
import org.graylog.storage.elasticsearch7.views.searchtypes.pivot.series.ESAverageHandler;
|
||||
@@ -106,6 +108,7 @@ public class ViewsESBackendModule extends ViewsModule {
|
||||
registerPivotBucketHandler(Values.NAME, ESValuesHandler.class);
|
||||
registerPivotBucketHandler(Time.NAME, ESTimeHandler.class);
|
||||
registerPivotBucketHandler(DateRangeBucket.NAME, ESDateRangeHandler.class);
|
||||
registerPivotBucketHandler(RangeBucket.NAME, ESRangeHandler.class);
|
||||
|
||||
bindExportBackend().to(ElasticsearchExportBackend.class);
|
||||
bindRequestStrategy().to(org.graylog.storage.elasticsearch7.views.export.SearchAfter.class);
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Graylog, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the Server Side Public License, version 1,
|
||||
* as published by MongoDB, Inc.
|
||||
*
|
||||
* 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
|
||||
* Server Side Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the Server Side Public License
|
||||
* along with this program. If not, see
|
||||
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
||||
*/
|
||||
package org.graylog.storage.elasticsearch7.views.searchtypes.pivot.buckets;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import org.graylog.plugins.views.search.Query;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.BucketSpec;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.Pivot;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.RangeBucket;
|
||||
import org.graylog.shaded.elasticsearch7.org.elasticsearch.search.aggregations.AggregationBuilder;
|
||||
import org.graylog.shaded.elasticsearch7.org.elasticsearch.search.aggregations.AggregationBuilders;
|
||||
import org.graylog.shaded.elasticsearch7.org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation;
|
||||
import org.graylog.shaded.elasticsearch7.org.elasticsearch.search.aggregations.bucket.range.ParsedRange;
|
||||
import org.graylog.shaded.elasticsearch7.org.elasticsearch.search.aggregations.bucket.range.RangeAggregationBuilder;
|
||||
import org.graylog.storage.elasticsearch7.views.ESGeneratedQueryContext;
|
||||
import org.graylog.storage.elasticsearch7.views.searchtypes.pivot.ESPivotBucketSpecHandler;
|
||||
import org.graylog.storage.elasticsearch7.views.searchtypes.pivot.PivotBucket;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class ESRangeHandler extends ESPivotBucketSpecHandler<RangeBucket> {
|
||||
private static final String AGG_NAME = "agg";
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public CreatedAggregations<AggregationBuilder> doCreateAggregation(Direction direction, String name, Pivot pivot, RangeBucket rangeBucket, ESGeneratedQueryContext queryContext, Query query) {
|
||||
AggregationBuilder root = null;
|
||||
AggregationBuilder leaf = null;
|
||||
for (final String field : rangeBucket.fields()) {
|
||||
final RangeAggregationBuilder builder = AggregationBuilders.range(name).field(field);
|
||||
rangeBucket.ranges().forEach(r -> {
|
||||
final Double from = r.from();
|
||||
final Double to = r.to();
|
||||
if (from != null && to != null) {
|
||||
builder.addRange(from, to);
|
||||
} else if (to != null) {
|
||||
builder.addUnboundedTo(to);
|
||||
} else if (from != null) {
|
||||
builder.addUnboundedFrom(from);
|
||||
}
|
||||
});
|
||||
builder.keyed(false);
|
||||
|
||||
queryContext.recordNameForPivotSpec(pivot, rangeBucket, name);
|
||||
|
||||
if (root == null) {
|
||||
root = builder;
|
||||
} else {
|
||||
leaf.subAggregation(builder);
|
||||
}
|
||||
leaf = builder;
|
||||
}
|
||||
|
||||
return CreatedAggregations.create(root, leaf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<PivotBucket> extractBuckets(Pivot pivot, BucketSpec bucketSpec, PivotBucket initialBucket) {
|
||||
final ImmutableList<String> previousKeys = initialBucket.keys();
|
||||
final MultiBucketsAggregation.Bucket previousBucket = initialBucket.bucket();
|
||||
final ParsedRange aggregation = previousBucket.getAggregations().get(AGG_NAME);
|
||||
|
||||
return aggregation.getBuckets().stream()
|
||||
.flatMap(bucket -> {
|
||||
final String bucketKey = bucket.getKeyAsString();
|
||||
final ImmutableList<String> keys = ImmutableList.<String>builder()
|
||||
.addAll(previousKeys)
|
||||
.add(bucketKey)
|
||||
.build();
|
||||
|
||||
return Stream.of(PivotBucket.create(keys, bucket, false));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import org.graylog.plugins.views.search.searchtypes.pivot.BucketSpec;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.Pivot;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.SeriesSpec;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.DateRangeBucket;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.RangeBucket;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.Time;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.Values;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.series.Average;
|
||||
@@ -60,6 +61,7 @@ import org.graylog.storage.opensearch2.views.searchtypes.pivot.OSPivot;
|
||||
import org.graylog.storage.opensearch2.views.searchtypes.pivot.OSPivotBucketSpecHandler;
|
||||
import org.graylog.storage.opensearch2.views.searchtypes.pivot.OSPivotSeriesSpecHandler;
|
||||
import org.graylog.storage.opensearch2.views.searchtypes.pivot.buckets.OSDateRangeHandler;
|
||||
import org.graylog.storage.opensearch2.views.searchtypes.pivot.buckets.OSRangeHandler;
|
||||
import org.graylog.storage.opensearch2.views.searchtypes.pivot.buckets.OSTimeHandler;
|
||||
import org.graylog.storage.opensearch2.views.searchtypes.pivot.buckets.OSValuesHandler;
|
||||
import org.graylog.storage.opensearch2.views.searchtypes.pivot.series.OSAverageHandler;
|
||||
@@ -110,6 +112,7 @@ public class ViewsOSBackendModule extends ViewsModule {
|
||||
registerPivotBucketHandler(Values.NAME, OSValuesHandler.class);
|
||||
registerPivotBucketHandler(Time.NAME, OSTimeHandler.class);
|
||||
registerPivotBucketHandler(DateRangeBucket.NAME, OSDateRangeHandler.class);
|
||||
registerPivotBucketHandler(RangeBucket.NAME, OSRangeHandler.class);
|
||||
|
||||
bindExportBackend().to(OpenSearchExportBackend.class);
|
||||
bindRequestStrategy().to(SearchAfter.class);
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Graylog, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the Server Side Public License, version 1,
|
||||
* as published by MongoDB, Inc.
|
||||
*
|
||||
* 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
|
||||
* Server Side Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the Server Side Public License
|
||||
* along with this program. If not, see
|
||||
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
||||
*/
|
||||
package org.graylog.storage.opensearch2.views.searchtypes.pivot.buckets;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import org.graylog.plugins.views.search.Query;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.BucketSpec;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.Pivot;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.RangeBucket;
|
||||
import org.graylog.shaded.opensearch2.org.opensearch.search.aggregations.AggregationBuilder;
|
||||
import org.graylog.shaded.opensearch2.org.opensearch.search.aggregations.AggregationBuilders;
|
||||
import org.graylog.shaded.opensearch2.org.opensearch.search.aggregations.bucket.MultiBucketsAggregation;
|
||||
import org.graylog.shaded.opensearch2.org.opensearch.search.aggregations.bucket.range.ParsedRange;
|
||||
import org.graylog.shaded.opensearch2.org.opensearch.search.aggregations.bucket.range.RangeAggregationBuilder;
|
||||
import org.graylog.storage.opensearch2.views.OSGeneratedQueryContext;
|
||||
import org.graylog.storage.opensearch2.views.searchtypes.pivot.OSPivotBucketSpecHandler;
|
||||
import org.graylog.storage.opensearch2.views.searchtypes.pivot.PivotBucket;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class OSRangeHandler extends OSPivotBucketSpecHandler<RangeBucket> {
|
||||
private static final String AGG_NAME = "agg";
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public CreatedAggregations<AggregationBuilder> doCreateAggregation(Direction direction, String name, Pivot pivot, RangeBucket rangeBucket, OSGeneratedQueryContext queryContext, Query query) {
|
||||
AggregationBuilder root = null;
|
||||
AggregationBuilder leaf = null;
|
||||
for (final String field : rangeBucket.fields()) {
|
||||
final RangeAggregationBuilder builder = AggregationBuilders.range(name).field(field);
|
||||
rangeBucket.ranges().forEach(r -> {
|
||||
final Double from = r.from();
|
||||
final Double to = r.to();
|
||||
if (from != null && to != null) {
|
||||
builder.addRange(from, to);
|
||||
} else if (to != null) {
|
||||
builder.addUnboundedTo(to);
|
||||
} else if (from != null) {
|
||||
builder.addUnboundedFrom(from);
|
||||
}
|
||||
});
|
||||
builder.keyed(false);
|
||||
|
||||
queryContext.recordNameForPivotSpec(pivot, rangeBucket, name);
|
||||
|
||||
if (root == null) {
|
||||
root = builder;
|
||||
} else {
|
||||
leaf.subAggregation(builder);
|
||||
}
|
||||
leaf = builder;
|
||||
}
|
||||
|
||||
return CreatedAggregations.create(root, leaf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<PivotBucket> extractBuckets(Pivot pivot, BucketSpec bucketSpec, PivotBucket initialBucket) {
|
||||
final ImmutableList<String> previousKeys = initialBucket.keys();
|
||||
final MultiBucketsAggregation.Bucket previousBucket = initialBucket.bucket();
|
||||
final ParsedRange aggregation = previousBucket.getAggregations().get(AGG_NAME);
|
||||
|
||||
return aggregation.getBuckets().stream()
|
||||
.flatMap(bucket -> {
|
||||
final String bucketKey = bucket.getKeyAsString();
|
||||
final ImmutableList<String> keys = ImmutableList.<String>builder()
|
||||
.addAll(previousKeys)
|
||||
.add(bucketKey)
|
||||
.build();
|
||||
|
||||
return Stream.of(PivotBucket.create(keys, bucket));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import org.graylog.plugins.views.search.searchtypes.pivot.BucketSpec;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.Pivot;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.SeriesSpec;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.DateRangeBucket;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.RangeBucket;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.Time;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.Values;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.series.Average;
|
||||
@@ -57,6 +58,7 @@ import org.graylog.storage.opensearch3.views.searchtypes.pivot.OSPivot;
|
||||
import org.graylog.storage.opensearch3.views.searchtypes.pivot.OSPivotBucketSpecHandler;
|
||||
import org.graylog.storage.opensearch3.views.searchtypes.pivot.OSPivotSeriesSpecHandler;
|
||||
import org.graylog.storage.opensearch3.views.searchtypes.pivot.buckets.OSDateRangeHandler;
|
||||
import org.graylog.storage.opensearch3.views.searchtypes.pivot.buckets.OSRangeHandler;
|
||||
import org.graylog.storage.opensearch3.views.searchtypes.pivot.buckets.OSTimeHandler;
|
||||
import org.graylog.storage.opensearch3.views.searchtypes.pivot.buckets.OSValuesHandler;
|
||||
import org.graylog.storage.opensearch3.views.searchtypes.pivot.series.OSAverageHandler;
|
||||
@@ -107,6 +109,7 @@ public class ViewsOSBackendModule extends ViewsModule {
|
||||
registerPivotBucketHandler(Values.NAME, OSValuesHandler.class);
|
||||
registerPivotBucketHandler(Time.NAME, OSTimeHandler.class);
|
||||
registerPivotBucketHandler(DateRangeBucket.NAME, OSDateRangeHandler.class);
|
||||
registerPivotBucketHandler(RangeBucket.NAME, OSRangeHandler.class);
|
||||
|
||||
bindExportBackend().to(OpenSearchExportBackend.class);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Graylog, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the Server Side Public License, version 1,
|
||||
* as published by MongoDB, Inc.
|
||||
*
|
||||
* 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
|
||||
* Server Side Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the Server Side Public License
|
||||
* along with this program. If not, see
|
||||
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
||||
*/
|
||||
package org.graylog.storage.opensearch3.views.searchtypes.pivot.buckets;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import org.graylog.plugins.views.search.Query;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.BucketSpec;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.Pivot;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.RangeBucket;
|
||||
import org.graylog.storage.opensearch3.views.OSGeneratedQueryContext;
|
||||
import org.graylog.storage.opensearch3.views.searchtypes.pivot.MutableNamedAggregationBuilder;
|
||||
import org.graylog.storage.opensearch3.views.searchtypes.pivot.OSPivotBucketSpecHandler;
|
||||
import org.graylog.storage.opensearch3.views.searchtypes.pivot.PivotBucket;
|
||||
import org.opensearch.client.json.JsonData;
|
||||
import org.opensearch.client.opensearch._types.aggregations.Aggregation;
|
||||
import org.opensearch.client.opensearch._types.aggregations.AggregationRange;
|
||||
import org.opensearch.client.opensearch._types.aggregations.MultiBucketBase;
|
||||
import org.opensearch.client.opensearch._types.aggregations.RangeAggregate;
|
||||
import org.opensearch.client.opensearch._types.aggregations.RangeAggregation;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class OSRangeHandler extends OSPivotBucketSpecHandler<RangeBucket> {
|
||||
private static final String AGG_NAME = "agg";
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public CreatedAggregations<MutableNamedAggregationBuilder> doCreateAggregation(Direction direction, String name, Pivot pivot, RangeBucket rangeBucket, OSGeneratedQueryContext queryContext, Query query) {
|
||||
MutableNamedAggregationBuilder root = null;
|
||||
MutableNamedAggregationBuilder leaf = null;
|
||||
for (final String field : rangeBucket.fields()) {
|
||||
final RangeAggregation.Builder rangeBuilder = new RangeAggregation.Builder()
|
||||
.field(field);
|
||||
rangeBucket.ranges().forEach(r -> {
|
||||
final AggregationRange.Builder range = new AggregationRange.Builder();
|
||||
Optional.ofNullable(r.from()).ifPresent(from -> range.from(JsonData.of(from)));
|
||||
Optional.ofNullable(r.to()).ifPresent(to -> range.to(JsonData.of(to)));
|
||||
rangeBuilder.ranges(range.build());
|
||||
});
|
||||
rangeBuilder.keyed(false);
|
||||
|
||||
queryContext.recordNameForPivotSpec(pivot, rangeBucket, name);
|
||||
|
||||
final MutableNamedAggregationBuilder builder = new MutableNamedAggregationBuilder(
|
||||
name,
|
||||
Aggregation.builder().range(rangeBuilder.build())
|
||||
);
|
||||
|
||||
if (root == null) {
|
||||
root = builder;
|
||||
} else {
|
||||
leaf.subAggregation(builder);
|
||||
}
|
||||
leaf = builder;
|
||||
}
|
||||
|
||||
return CreatedAggregations.create(root, leaf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<PivotBucket> extractBuckets(Pivot pivot, BucketSpec bucketSpecs, PivotBucket initialBucket) {
|
||||
final ImmutableList<String> previousKeys = initialBucket.keys();
|
||||
final MultiBucketBase previousBucket = initialBucket.bucket();
|
||||
final RangeAggregate aggregation = previousBucket.aggregations().get(AGG_NAME).range();
|
||||
|
||||
return aggregation.buckets().array().stream()
|
||||
.flatMap(bucket -> {
|
||||
final String bucketKey = bucket.key();
|
||||
final ImmutableList<String> keys = ImmutableList.<String>builder()
|
||||
.addAll(previousKeys)
|
||||
.add(bucketKey)
|
||||
.build();
|
||||
|
||||
return Stream.of(PivotBucket.create(keys, bucket));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -94,6 +94,7 @@ import org.graylog.plugins.views.search.searchtypes.pivot.PivotResult;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.PivotSort;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.SeriesSort;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.AutoInterval;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.RangeBucket;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.Time;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.TimeUnitInterval;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.Values;
|
||||
@@ -206,6 +207,7 @@ public class ViewsBindings extends ViewsModule {
|
||||
// pivot specs
|
||||
registerJacksonSubtype(Values.class);
|
||||
registerJacksonSubtype(Time.class);
|
||||
registerJacksonSubtype(RangeBucket.class);
|
||||
registerPivotAggregationFunction(Average.NAME, "Average", Average.class);
|
||||
registerPivotAggregationFunction(Cardinality.NAME, "Cardinality", Cardinality.class);
|
||||
registerPivotAggregationFunction(Count.NAME, "Count", Count.class);
|
||||
|
||||
@@ -80,7 +80,7 @@ public class AggregationSpecToPivotMapper implements BiFunction<AggregationReque
|
||||
}
|
||||
|
||||
private Grouping addAutoInterval(Grouping grouping) {
|
||||
return new Grouping(grouping.requestedField().name(), Optional.empty(), Optional.empty(), Optional.of(DEFAULT_SCALINGFACTOR));
|
||||
return new Grouping(grouping.requestedField().name(), Optional.empty(), Optional.empty(), Optional.of(DEFAULT_SCALINGFACTOR), Optional.empty());
|
||||
}
|
||||
|
||||
private boolean isDateType(final String fieldName, final Map<String, FieldTypes.Type> fields) {
|
||||
@@ -88,7 +88,7 @@ public class AggregationSpecToPivotMapper implements BiFunction<AggregationReque
|
||||
}
|
||||
|
||||
private boolean noBucketingParameterSetOn(Grouping grouping) {
|
||||
return grouping.timeunit().isEmpty() && grouping.scaling().isEmpty();
|
||||
return grouping.timeunit().isEmpty() && grouping.scaling().isEmpty() && grouping.ranges().isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -19,19 +19,23 @@ package org.graylog.plugins.views.search.rest.scriptingapi.mapping;
|
||||
import org.graylog.plugins.views.search.rest.scriptingapi.request.Grouping;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.BucketSpec;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.AutoInterval;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.RangeBucket;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.Time;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.TimeUnitInterval;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.Values;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class GroupingToBucketSpecMapper implements Function<Grouping, BucketSpec> {
|
||||
/**
|
||||
* Only 'scaling' or 'timeunit' or none of both should be present, this is validated in Grouping.java on deserializing the JSON
|
||||
* Only one of 'scaling', 'timeunit', or 'ranges' should be present, this is validated in Grouping.java on deserializing the JSON
|
||||
*/
|
||||
@Override
|
||||
public BucketSpec apply(final Grouping grouping) {
|
||||
if(grouping.scaling().isPresent()) {
|
||||
if(grouping.ranges().isPresent()) {
|
||||
return new RangeBucket(List.of(grouping.requestedField().name()), grouping.ranges().get());
|
||||
} else if(grouping.scaling().isPresent()) {
|
||||
return Time.builder()
|
||||
.field(grouping.requestedField().name())
|
||||
.type(Time.NAME)
|
||||
|
||||
@@ -18,11 +18,13 @@ package org.graylog.plugins.views.search.rest.scriptingapi.request;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.ValidationException;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.NumberRange;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.Values;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@@ -30,34 +32,38 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
public record Grouping(@JsonProperty("field") @Valid @NotBlank String fieldName,
|
||||
@JsonProperty("limit") Optional<Integer> limit,
|
||||
@JsonProperty("timeunit") Optional<String> timeunit,
|
||||
@JsonProperty("scaling") Optional<Double> scaling) {
|
||||
@JsonProperty("scaling") Optional<Double> scaling,
|
||||
@JsonProperty("ranges") Optional<List<NumberRange>> ranges) {
|
||||
|
||||
public Grouping(String fieldName) {
|
||||
this(fieldName, Optional.of(Values.DEFAULT_LIMIT), Optional.empty(), Optional.empty());
|
||||
this(fieldName, Optional.of(Values.DEFAULT_LIMIT), Optional.empty(), Optional.empty(), Optional.empty());
|
||||
}
|
||||
|
||||
public Grouping(@JsonProperty("field") @Valid @NotBlank String fieldName,
|
||||
@JsonProperty("limit") Optional<Integer> limit,
|
||||
@JsonProperty("timeunit") Optional<String> timeunit,
|
||||
@JsonProperty("scaling") Optional<Double> scaling) {
|
||||
@JsonProperty("scaling") Optional<Double> scaling,
|
||||
@JsonProperty("ranges") Optional<List<NumberRange>> ranges) {
|
||||
this.fieldName = fieldName;
|
||||
this.limit = limit.map(lim -> lim <= 0 ? Values.DEFAULT_LIMIT : lim);
|
||||
this.timeunit = timeunit;
|
||||
this.scaling = scaling;
|
||||
this.ranges = ranges;
|
||||
|
||||
// only one of the three following parameters are allowed to be present
|
||||
// only one of the following parameters are allowed to be present
|
||||
final AtomicInteger attrCounter = new AtomicInteger();
|
||||
limit.ifPresent(l -> attrCounter.getAndIncrement());
|
||||
timeunit.ifPresent(t -> attrCounter.getAndIncrement());
|
||||
scaling.ifPresent(s -> attrCounter.getAndIncrement());
|
||||
ranges.ifPresent(r -> attrCounter.getAndIncrement());
|
||||
if(attrCounter.get() > 1) {
|
||||
throw new ValidationException("Only one attribute out of 'limit', 'timeunit' or 'scaling' can be specified");
|
||||
throw new ValidationException("Only one attribute out of 'limit', 'timeunit', 'scaling' or 'ranges' can be specified");
|
||||
}
|
||||
}
|
||||
|
||||
public Grouping(@JsonProperty("field") @Valid @NotBlank String fieldName,
|
||||
@JsonProperty("limit") int limit) {
|
||||
this(fieldName, Optional.of(limit), Optional.empty(), Optional.empty());
|
||||
this(fieldName, Optional.of(limit), Optional.empty(), Optional.empty(), Optional.empty());
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Graylog, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the Server Side Public License, version 1,
|
||||
* as published by MongoDB, Inc.
|
||||
*
|
||||
* 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
|
||||
* Server Side Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the Server Side Public License
|
||||
* along with this program. If not, see
|
||||
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
||||
*/
|
||||
package org.graylog.plugins.views.search.searchtypes.pivot.buckets;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public record NumberRange(@Nullable Double from, @Nullable Double to) {}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Graylog, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the Server Side Public License, version 1,
|
||||
* as published by MongoDB, Inc.
|
||||
*
|
||||
* 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
|
||||
* Server Side Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the Server Side Public License
|
||||
* along with this program. If not, see
|
||||
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
||||
*/
|
||||
package org.graylog.plugins.views.search.searchtypes.pivot.buckets;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.BucketSpec;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@JsonTypeName(RangeBucket.NAME)
|
||||
public record RangeBucket(List<String> fields, List<NumberRange> ranges) implements BucketSpec {
|
||||
public static final String NAME = "range";
|
||||
|
||||
@Override
|
||||
public String type() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
@@ -16,13 +16,18 @@
|
||||
*/
|
||||
package org.graylog.plugins.views.search.rest.scriptingapi.mapping;
|
||||
|
||||
import jakarta.validation.ValidationException;
|
||||
import org.graylog.plugins.views.search.rest.scriptingapi.request.Grouping;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.BucketSpec;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.NumberRange;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.RangeBucket;
|
||||
import org.graylog.plugins.views.search.searchtypes.pivot.buckets.Values;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
@@ -68,4 +73,43 @@ class GroupingToBucketSpecMapperTest {
|
||||
.satisfies(b -> assertEquals(Values.DEFAULT_LIMIT, ((Values) b).limit()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildsRangeBucketSpecCorrectly() {
|
||||
final List<NumberRange> ranges = List.of(
|
||||
new NumberRange(null, 100.0),
|
||||
new NumberRange(100.0, 500.0),
|
||||
new NumberRange(500.0, null)
|
||||
);
|
||||
final Grouping grouping = new Grouping("took_ms", Optional.empty(), Optional.empty(), Optional.empty(), Optional.of(ranges));
|
||||
final BucketSpec bucketSpec = toTest.apply(grouping);
|
||||
|
||||
assertThat(bucketSpec)
|
||||
.isNotNull()
|
||||
.isInstanceOf(RangeBucket.class)
|
||||
.satisfies(b -> assertEquals(Collections.singletonList("took_ms"), b.fields()))
|
||||
.satisfies(b -> assertEquals(RangeBucket.NAME, b.type()))
|
||||
.satisfies(b -> assertEquals(3, ((RangeBucket) b).ranges().size()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void throwsValidationExceptionWhenRangesAndLimitBothProvided() {
|
||||
final List<NumberRange> ranges = List.of(new NumberRange(null, 100.0));
|
||||
assertThrows(ValidationException.class,
|
||||
() -> new Grouping("field", Optional.of(10), Optional.empty(), Optional.empty(), Optional.of(ranges)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void throwsValidationExceptionWhenRangesAndTimeunitBothProvided() {
|
||||
final List<NumberRange> ranges = List.of(new NumberRange(null, 100.0));
|
||||
assertThrows(ValidationException.class,
|
||||
() -> new Grouping("field", Optional.empty(), Optional.of("1h"), Optional.empty(), Optional.of(ranges)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void throwsValidationExceptionWhenRangesAndScalingBothProvided() {
|
||||
final List<NumberRange> ranges = List.of(new NumberRange(null, 100.0));
|
||||
assertThrows(ValidationException.class,
|
||||
() -> new Grouping("field", Optional.empty(), Optional.empty(), Optional.of(1.0), Optional.of(ranges)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user