From 51f6d08554ca8aaa39d585d7bce8c46044069702 Mon Sep 17 00:00:00 2001 From: John OHara Date: Fri, 23 Feb 2024 17:55:21 +0000 Subject: [PATCH] Integrate E-Divisive (Hunter) as a changepoint detection algorithm: Fixes #1007 - run bulk change detection once per fingerprint - Allow option to recalc datapoints for variables, or re-use - added end-to-end and injectable tests - Disable eDivisive tests unless enabled with 'ci' profile --- .github/workflows/main.yml | 4 +- docs/site/content/en/openapi/openapi.yaml | 26 ++ .../horreum/api/alerting/ChangeDetection.java | 10 +- .../horreum/api/data/ConditionConfig.java | 3 +- .../api/data/ExperimentComparison.java | 3 +- .../ChangeDetectionModelType.java | 13 +- .../EDivisiveDetectionConfig.java | 11 + .../changeDetection/FixThresholdConfig.java | 3 - .../FixedThresholdDetectionConfig.java | 6 +- .../RelativeDifferenceDetectionConfig.java | 9 +- .../datastore/BaseChangeDetectionConfig.java | 1 - .../internal/services/AlertingService.java | 1 + horreum-backend/pom.xml | 15 + .../src/main/docker/Dockerfile.jvm.base | 1 + .../changedetection/ChangeDetectionModel.java | 1 + .../changedetection/FixedThresholdModel.java | 12 +- .../changedetection/HunterEDivisiveModel.java | 280 ++++++++++++++++++ .../horreum/changedetection/ModelType.java | 6 + ...elativeDifferenceChangeDetectionModel.java | 10 +- .../horreum/mapper/ChangeDetectionMapper.java | 2 +- .../horreum/svc/AlertingServiceImpl.java | 170 +++++++---- .../resources/changeDetection/hunter.yaml | 10 + .../changedetection/EdivisiveTests.java | 208 +++++++++++++ .../horreum/svc/AlertingServiceTest.java | 25 +- .../tools/horreum/svc/BaseServiceTest.java | 23 +- .../tools/horreum/svc/RunServiceTest.java | 4 +- .../invalid/tests/resources/horreum.csv | 54 ++++ .../valid/tests/resources/horreum.csv | 54 ++++ .../tools/horreum/it/HorreumClientIT.java | 3 +- horreum-web/package-lock.json | 273 +++++++++-------- .../src/domain/alerting/RecalculateModal.tsx | 17 +- 31 files changed, 1020 insertions(+), 238 deletions(-) create mode 100644 horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/changeDetection/EDivisiveDetectionConfig.java create mode 100644 horreum-backend/src/main/java/io/hyperfoil/tools/horreum/changedetection/HunterEDivisiveModel.java create mode 100644 horreum-backend/src/main/java/io/hyperfoil/tools/horreum/changedetection/ModelType.java create mode 100644 horreum-backend/src/main/resources/changeDetection/hunter.yaml create mode 100644 horreum-backend/src/test/java/io/hyperfoil/tools/horreum/changedetection/EdivisiveTests.java create mode 100644 horreum-backend/src/test/resources/change/eDivisive/invalid/tests/resources/horreum.csv create mode 100644 horreum-backend/src/test/resources/change/eDivisive/valid/tests/resources/horreum.csv diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1f127aa6c..0f8b0df43 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -38,10 +38,12 @@ jobs: path: ~/.m2 key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-m2 + - name: Install Hunter + run: pipx install git+https://github.com/datastax-labs/hunter.git - name: Maven Version run: mvn --version - name: Build and Test - run: mvn clean install -B --file pom.xml ${{ matrix.os.build-options }} + run: mvn clean install -B --file pom.xml ${{ matrix.os.build-options }} -P ci - name: Check uncommitted changes if: matrix.os.name == 'ubuntu-latest' run: | diff --git a/docs/site/content/en/openapi/openapi.yaml b/docs/site/content/en/openapi/openapi.yaml index c35a385f3..b2cb490cf 100644 --- a/docs/site/content/en/openapi/openapi.yaml +++ b/docs/site/content/en/openapi/openapi.yaml @@ -2459,16 +2459,19 @@ components: oneOf: - $ref: '#/components/schemas/RelativeDifferenceDetectionConfig' - $ref: '#/components/schemas/FixedThresholdDetectionConfig' + - $ref: '#/components/schemas/EDivisiveDetectionConfig' discriminator: propertyName: model mapping: relativeDifference: '#/components/schemas/RelativeDifferenceDetectionConfig' fixedThreshold: '#/components/schemas/FixedThresholdDetectionConfig' + eDivisive: '#/components/schemas/EDivisiveDetectionConfig' ChangeDetectionModelType: description: Type of Change Detection Model enum: - FIXED_THRESHOLD - RELATIVE_DIFFERENCE + - EDIVISIVE type: string ComparisonResult: description: Result of performing a Comparison @@ -2806,6 +2809,19 @@ components: - ELASTICSEARCH type: string example: ELASTICSEARCH + EDivisiveDetectionConfig: + required: + - builtIn + - model + type: object + properties: + builtIn: + description: Built In + type: boolean + model: + enum: + - eDivisive + type: string ElasticsearchDatastoreConfig: description: Type of backend datastore required: @@ -3079,6 +3095,7 @@ components: FixedThresholdDetectionConfig: required: - builtIn + - model - min - max type: object @@ -3086,6 +3103,10 @@ components: builtIn: description: Built In type: boolean + model: + enum: + - fixedThreshold + type: string min: description: Lower bound for acceptable datapoint values type: object @@ -3514,6 +3535,7 @@ components: RelativeDifferenceDetectionConfig: required: - builtIn + - model - filter - window - threshold @@ -3523,6 +3545,10 @@ components: builtIn: description: Built In type: boolean + model: + enum: + - relativeDifference + type: string filter: description: Relative Difference Detection filter type: string diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/alerting/ChangeDetection.java b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/alerting/ChangeDetection.java index 313af729e..6227364d2 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/alerting/ChangeDetection.java +++ b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/alerting/ChangeDetection.java @@ -2,6 +2,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.hyperfoil.tools.horreum.api.data.changeDetection.ChangeDetectionModelType; +import io.hyperfoil.tools.horreum.api.data.changeDetection.EDivisiveDetectionConfig; import io.hyperfoil.tools.horreum.api.data.changeDetection.FixedThresholdDetectionConfig; import io.hyperfoil.tools.horreum.api.data.changeDetection.RelativeDifferenceDetectionConfig; import jakarta.validation.constraints.NotNull; @@ -20,12 +22,14 @@ public class ChangeDetection { @JsonProperty( required = true ) @Schema(type = SchemaType.OBJECT, discriminatorProperty = "model", discriminatorMapping = { - @DiscriminatorMapping(schema = RelativeDifferenceDetectionConfig.class, value = "relativeDifference"), - @DiscriminatorMapping(schema = FixedThresholdDetectionConfig.class, value = "fixedThreshold") + @DiscriminatorMapping(schema = RelativeDifferenceDetectionConfig.class, value = ChangeDetectionModelType.names.RELATIVE_DIFFERENCE), + @DiscriminatorMapping(schema = FixedThresholdDetectionConfig.class, value = ChangeDetectionModelType.names.FIXED_THRESHOLD), + @DiscriminatorMapping(schema = EDivisiveDetectionConfig.class, value = ChangeDetectionModelType.names.EDIVISIVE) }, oneOf = { RelativeDifferenceDetectionConfig.class, - FixedThresholdDetectionConfig.class + FixedThresholdDetectionConfig.class, + EDivisiveDetectionConfig.class } ) public ObjectNode config; diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/ConditionConfig.java b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/ConditionConfig.java index 9e10b9fc5..2aabda270 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/ConditionConfig.java +++ b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/ConditionConfig.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.Map; +import io.hyperfoil.tools.horreum.api.data.changeDetection.ChangeDetectionModelType; import jakarta.validation.constraints.NotNull; import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; @@ -16,7 +17,7 @@ @Schema(type = SchemaType.OBJECT, description = "A configuration object for Change detection models") public class ConditionConfig { @NotNull - @Schema(description = "Name of Change detection model", example = "fixedThreshold") + @Schema(description = "Name of Change detection model", example = ChangeDetectionModelType.names.FIXED_THRESHOLD) public String name; @NotNull @Schema(description = "UI name for change detection model", example = "Fixed Threshold") diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/ExperimentComparison.java b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/ExperimentComparison.java index 36103543c..a71aa2dd0 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/ExperimentComparison.java +++ b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/ExperimentComparison.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.hyperfoil.tools.horreum.api.data.changeDetection.ChangeDetectionModelType; import jakarta.validation.constraints.NotNull; import org.eclipse.microprofile.openapi.annotations.media.Schema; @@ -10,7 +11,7 @@ public class ExperimentComparison { @NotNull @JsonProperty( required = true ) - @Schema(description = "Name of comparison model", example = "relativeDifference") + @Schema(description = "Name of comparison model", example = ChangeDetectionModelType.names.RELATIVE_DIFFERENCE) public String model; @NotNull @JsonProperty( required = true ) diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/changeDetection/ChangeDetectionModelType.java b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/changeDetection/ChangeDetectionModelType.java index efca4adcc..789d89d40 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/changeDetection/ChangeDetectionModelType.java +++ b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/changeDetection/ChangeDetectionModelType.java @@ -7,13 +7,14 @@ import org.eclipse.microprofile.openapi.annotations.media.Schema; import java.util.Arrays; -import java.util.Optional; @Schema(type = SchemaType.STRING, required = true, description = "Type of Change Detection Model") public enum ChangeDetectionModelType { - FIXED_THRESHOLD("fixedThreshold", new TypeReference() {}), - RELATIVE_DIFFERENCE ("relativeDifference", new TypeReference() {}); + + FIXED_THRESHOLD(names.FIXED_THRESHOLD, new TypeReference() {}), + RELATIVE_DIFFERENCE (names.RELATIVE_DIFFERENCE, new TypeReference() {}), + EDIVISIVE(names.EDIVISIVE, new TypeReference() {}); private static final ChangeDetectionModelType[] VALUES = values(); private final String name; @@ -32,4 +33,10 @@ public TypeReference getTypeReference() public static ChangeDetectionModelType fromString(String str) { return Arrays.stream(VALUES).filter(v -> v.name.equals(str)).findAny().orElseThrow(() -> new IllegalArgumentException("Unknown model: " + str)); } + + public static class names { + public static final String FIXED_THRESHOLD = "fixedThreshold"; + public static final String RELATIVE_DIFFERENCE = "relativeDifference"; + public static final String EDIVISIVE = "eDivisive"; + } } diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/changeDetection/EDivisiveDetectionConfig.java b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/changeDetection/EDivisiveDetectionConfig.java new file mode 100644 index 000000000..784813ac6 --- /dev/null +++ b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/changeDetection/EDivisiveDetectionConfig.java @@ -0,0 +1,11 @@ +package io.hyperfoil.tools.horreum.api.data.changeDetection; + +import io.hyperfoil.tools.horreum.api.data.datastore.BaseChangeDetectionConfig; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +public class EDivisiveDetectionConfig extends BaseChangeDetectionConfig { + @Schema(type = SchemaType.STRING, required = true, enumeration = {ChangeDetectionModelType.names.EDIVISIVE}) + public String model; + +} diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/changeDetection/FixThresholdConfig.java b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/changeDetection/FixThresholdConfig.java index ca0eb8f97..67e78be31 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/changeDetection/FixThresholdConfig.java +++ b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/changeDetection/FixThresholdConfig.java @@ -7,9 +7,6 @@ * Concrete configuration type for io.hyperfoil.tools.horreum.changedetection.FixedThresholdModel */ public class FixThresholdConfig { - @Schema(type = SchemaType.STRING, required = true, example = "fixedThreshold", - description = "model descriminator") - public static final String model = "fixedThreshold"; @Schema(type = SchemaType.INTEGER, required = true, example = "95", description = "Threshold Value") public Double value; diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/changeDetection/FixedThresholdDetectionConfig.java b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/changeDetection/FixedThresholdDetectionConfig.java index b4b49abe0..6f59c4e0b 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/changeDetection/FixedThresholdDetectionConfig.java +++ b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/changeDetection/FixedThresholdDetectionConfig.java @@ -5,6 +5,8 @@ import org.eclipse.microprofile.openapi.annotations.media.Schema; public class FixedThresholdDetectionConfig extends BaseChangeDetectionConfig { + @Schema(type = SchemaType.STRING, required = true, enumeration = { ChangeDetectionModelType.names.FIXED_THRESHOLD }) + public String model; @Schema(type = SchemaType.OBJECT, required = true, description = "Lower bound for acceptable datapoint values") public FixThresholdConfig min; @@ -12,8 +14,4 @@ public class FixedThresholdDetectionConfig extends BaseChangeDetectionConfig { description = "Upper bound for acceptable datapoint values") public FixThresholdConfig max; - @Override - public String validateConfig() { - return null; - } } diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/changeDetection/RelativeDifferenceDetectionConfig.java b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/changeDetection/RelativeDifferenceDetectionConfig.java index 0f47c424f..1e493c58d 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/changeDetection/RelativeDifferenceDetectionConfig.java +++ b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/changeDetection/RelativeDifferenceDetectionConfig.java @@ -8,9 +8,8 @@ * Concrete configuration type for io.hyperfoil.tools.horreum.changedetection.RelativeDifferenceChangeDetectionModel */ public class RelativeDifferenceDetectionConfig extends BaseChangeDetectionConfig { - @Schema(type = SchemaType.STRING, required = true, example = "relativeDifference", - description = "model descriminator") - public static final String model = "relativeDifference"; + @Schema(type = SchemaType.STRING, required = true, enumeration = { ChangeDetectionModelType.names.RELATIVE_DIFFERENCE } ) + public String model; @Schema(type = SchemaType.STRING, required = true, example = "mean", description = "Relative Difference Detection filter") public String filter; @@ -24,8 +23,4 @@ public class RelativeDifferenceDetectionConfig extends BaseChangeDetectionConfi description = "Minimal number of preceding datapoints") public Integer minPrevious; - @Override - public String validateConfig() { - return null; - } } diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/datastore/BaseChangeDetectionConfig.java b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/datastore/BaseChangeDetectionConfig.java index 70b1c4a6e..6bf039076 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/datastore/BaseChangeDetectionConfig.java +++ b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/data/datastore/BaseChangeDetectionConfig.java @@ -16,5 +16,4 @@ public BaseChangeDetectionConfig(Boolean builtIn) { this.builtIn = builtIn; } - public abstract String validateConfig(); } diff --git a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/internal/services/AlertingService.java b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/internal/services/AlertingService.java index 23b896de9..0da0b195a 100644 --- a/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/internal/services/AlertingService.java +++ b/horreum-api/src/main/java/io/hyperfoil/tools/horreum/api/internal/services/AlertingService.java @@ -62,6 +62,7 @@ void updateChange(@Parameter(required = true) @PathParam("id") int id, void recalculateDatapoints(@Parameter(required = true) @QueryParam("test") int testId, @QueryParam("notify") boolean notify, @QueryParam("debug") boolean debug, + @QueryParam("clear") Boolean clearDatapoints, @QueryParam("from") Long from, @QueryParam("to") Long to); @GET diff --git a/horreum-backend/pom.xml b/horreum-backend/pom.xml index fe7a62f3a..2a627d9f1 100644 --- a/horreum-backend/pom.xml +++ b/horreum-backend/pom.xml @@ -11,6 +11,12 @@ jar Horreum Backend + + CiTests + + + + io.hyperfoil.tools @@ -253,6 +259,7 @@ maven-surefire-plugin ${surefire-plugin.version} + ${excludeTags} org.jboss.logmanager.LogManager ${maven.home} @@ -276,6 +283,14 @@ + + + ci + + + + + do-release diff --git a/horreum-backend/src/main/docker/Dockerfile.jvm.base b/horreum-backend/src/main/docker/Dockerfile.jvm.base index 42fbce0b3..8999a1b75 100644 --- a/horreum-backend/src/main/docker/Dockerfile.jvm.base +++ b/horreum-backend/src/main/docker/Dockerfile.jvm.base @@ -1,3 +1,4 @@ FROM registry.access.redhat.com/ubi9/openjdk-17 COPY src/main/resources/horreum.sh /deployments/ COPY src/main/resources/k8s-setup.sh /deployments/ +RUN pipx install git+ssh://git@github.com/datastax-labs/hunter \ No newline at end of file diff --git a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/changedetection/ChangeDetectionModel.java b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/changedetection/ChangeDetectionModel.java index 5a1fd4d65..f2e138357 100644 --- a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/changedetection/ChangeDetectionModel.java +++ b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/changedetection/ChangeDetectionModel.java @@ -14,5 +14,6 @@ public interface ChangeDetectionModel { ChangeDetectionModelType type(); void analyze(List dataPoints, JsonNode configuration, Consumer changeConsumer); + ModelType getType(); } diff --git a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/changedetection/FixedThresholdModel.java b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/changedetection/FixedThresholdModel.java index ba397f4d1..378ad81dc 100644 --- a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/changedetection/FixedThresholdModel.java +++ b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/changedetection/FixedThresholdModel.java @@ -19,19 +19,18 @@ @ApplicationScoped public class FixedThresholdModel implements ChangeDetectionModel { private static final Logger log = Logger.getLogger(FixedThresholdModel.class); - public static final String NAME = "fixedThreshold"; @Inject ObjectMapper mapper; @Override public ConditionConfig config() { - ConditionConfig conditionConfig = new ConditionConfig(NAME, "Fixed Threshold", "This model checks that the datapoint value is within fixed bounds.") + ConditionConfig conditionConfig = new ConditionConfig(ChangeDetectionModelType.names.FIXED_THRESHOLD, "Fixed Threshold", "This model checks that the datapoint value is within fixed bounds.") .addComponent("min", new ConditionConfig.NumberBound(), "Minimum", "Lower bound for acceptable datapoint values.") .addComponent("max", new ConditionConfig.NumberBound(), "Maximum", "Upper bound for acceptable datapoint values."); - conditionConfig.defaults.put("model", new TextNode(NAME)); - + conditionConfig.defaults.put("model", new TextNode(ChangeDetectionModelType.names.FIXED_THRESHOLD)); return conditionConfig; + } @Override @@ -71,5 +70,8 @@ public void analyze(List dataPoints, JsonNode configuration, Consu } - + @Override + public ModelType getType() { + return ModelType.CONTINOUS; + } } diff --git a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/changedetection/HunterEDivisiveModel.java b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/changedetection/HunterEDivisiveModel.java new file mode 100644 index 000000000..35a1685f8 --- /dev/null +++ b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/changedetection/HunterEDivisiveModel.java @@ -0,0 +1,280 @@ +package io.hyperfoil.tools.horreum.changedetection; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.TextNode; +import io.hyperfoil.tools.horreum.api.data.ConditionConfig; +import io.hyperfoil.tools.horreum.api.data.changeDetection.ChangeDetectionModelType; +import io.hyperfoil.tools.horreum.entity.alerting.ChangeDAO; +import io.hyperfoil.tools.horreum.entity.alerting.DataPointDAO; +import jakarta.enterprise.context.ApplicationScoped; +import org.jboss.logging.Logger; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +@ApplicationScoped +public class HunterEDivisiveModel implements ChangeDetectionModel { + private static final Logger log = Logger.getLogger(HunterEDivisiveModel.class); + private static String[] HEADERS = {"kpi", "timestamp", "datasetid"}; + + private static final Pattern datapointPattern = Pattern.compile("(?^\\d{4}-[01]\\d-[0-3]\\d\\s[0-2]\\d:[0-5]\\d:[0-5]\\d)\\s[+|-]\\d{4}\\s+(?\\d+)\\s+(?\\d+?\\.?\\d+)$"); + + @Override + public ConditionConfig config() { + ConditionConfig conditionConfig = new ConditionConfig(ChangeDetectionModelType.names.EDIVISIVE, "eDivisive - Hunter", "This model uses the Hunter eDivisive algorithm to determine change points in a continual series."); + conditionConfig.defaults.put("model", new TextNode(ChangeDetectionModelType.names.EDIVISIVE)); + + return conditionConfig; + } + + @Override + public ChangeDetectionModelType type() { + return ChangeDetectionModelType.EDIVISIVE; + } + + @Override + public void analyze(List dataPoints, JsonNode configuration, Consumer changeConsumer) { + + TmpFiles tmpFiles = null; + + try { + try { + tmpFiles = TmpFiles.instance(); + } catch (IOException e) { + log.error("Could not create temporary file for Hunter eDivisive algorithm", e); + return; + } + + assert tmpFiles.inputFile != null; + + try (final FileWriter fw = new FileWriter(tmpFiles.inputFile, true); + final PrintWriter pw = new PrintWriter(fw);) { + + Collections.reverse(dataPoints); + + //write out csv fields + pw.println(Arrays.stream(HEADERS).collect(Collectors.joining(","))); + dataPoints.forEach(dataPointDAO -> pw.println(String.format("%.2f,%s,%d", dataPointDAO.value, dataPointDAO.timestamp.toString(), dataPointDAO.id))); + + } catch (IOException e) { + log.error("Could not create file writer for Hunter eDivisive algorithm", e); + return; + } + + log.debugf("created csv output : %s", tmpFiles.inputFile.getAbsolutePath()); + + if (!validateInputCsv(tmpFiles)) { + log.errorf("could not validate: %s", tmpFiles.inputFile); + return; + } + + DataPointDAO firstDatapoint = dataPoints.get(0); + + processChangePoints( + (dataPointID) -> dataPoints.stream().filter(dataPoint -> dataPoint.id.equals(dataPointID)).findFirst(), + changeConsumer, + tmpFiles, + firstDatapoint.timestamp + ); + } finally { + if (tmpFiles != null) { + cleanupTmpFiles(tmpFiles); + } + } + } + + protected void cleanupTmpFiles(TmpFiles tmpFiles) { + if( tmpFiles.tmpdir.exists() ) { + clearDir(tmpFiles.tmpdir); + } else { + log.debugf("Trying to cleanup temp files, but they do not exist!"); + } + } + + + private void clearDir(File dir){ + Arrays.stream(dir.listFiles()).forEach(file -> { + if ( file.isDirectory() ){ + clearDir(file); + } + file.delete(); + }); + if(!dir.delete()){ + log.errorf("Failed to cleanup up temporary files: %s", dir.getAbsolutePath()); + } + } + + protected void processChangePoints(Function> changePointSupplier, Consumer changeConsumer, TmpFiles tmpFiles, Instant sinceInstance) { + String command = "hunter analyze horreum --since '" + sinceInstance.toString() + "'"; + log.debugf("Running command: %s", command); + + + List results = executeProcess(tmpFiles, false, "bash", "-l", "-c", command); + + if (results.size() > 3) { + Iterator resultIter = results.iterator(); + while (resultIter.hasNext()) { + String line = resultIter.next(); + if (line.contains("··")) { + + String change = resultIter.next().trim(); + resultIter.next(); // skip line containing '··' + String changeDetails = resultIter.next(); + + Matcher foundChange = datapointPattern.matcher(changeDetails); + + if ( foundChange.matches() ){ + String timestamp = foundChange.group("timestamp"); + Integer datapointID = Integer.parseInt(foundChange.group("dataPointId")); + + log.debugf("Found change point `%s` at `%s` for dataset: %d", change, timestamp, datapointID); + + Optional foundDataPoint = changePointSupplier.apply(datapointID); + + if (foundDataPoint.isPresent()) { + ChangeDAO changePoint = ChangeDAO.fromDatapoint(foundDataPoint.get()); + + changePoint.description = String.format("eDivisive change `%s` at `%s` for dataset: %d", change, timestamp, datapointID); + + log.trace(changePoint.description); + changeConsumer.accept(changePoint); + } else { + log.errorf("Could not find datapoint in set!"); + } + } else { + log.errorf("Could not parse hunter line: '%s'", changeDetails); + } + } + } + + } else { + log.debugf("No change points were detected in : %s", tmpFiles.tmpdir.getAbsolutePath()); + } + } + + protected boolean validateInputCsv(TmpFiles tmpFiles) { + executeProcess(tmpFiles, true, "bash", "-l", "-c", "hunter validate"); + + try(FileReader fileReader = new FileReader(tmpFiles.logFile); + BufferedReader reader = new BufferedReader(fileReader);){ + + Optional optLine = reader.lines().filter(line -> line.contains("Validation finished")).findFirst(); + if(optLine.isEmpty()) { + log.errorf("Could not validate: %s", tmpFiles.tmpdir.getAbsolutePath()); + return false; + } + if( optLine.get().contains("INVALID") ) { + log.errorf("Invalid format for: %s; see log for details: %s", tmpFiles.tmpdir.getAbsolutePath(), tmpFiles.logFile.getAbsolutePath()); + return false; + } + } catch (IOException e) { + throw new RuntimeException(e); + } + return true; + } + + @Override + public ModelType getType() { + return ModelType.BULK; + } + + + protected static class TmpFiles { + final File inputFile; + final File tmpdir; + final File confFile; + final File logFile; + + public static TmpFiles instance() throws IOException { + return new TmpFiles(); + } + + public TmpFiles() throws IOException { + tmpdir = Files.createTempDirectory("hunter").toFile(); + + Path respourcesPath = Path.of(tmpdir.getAbsolutePath(), "tests", "resources"); + Files.createDirectories(respourcesPath); + inputFile = Path.of(respourcesPath.toFile().getAbsolutePath(), "horreum.csv").toFile(); + + confFile = Path.of(respourcesPath.toFile().getAbsolutePath(), "hunter.yaml").toFile(); + logFile = Path.of(respourcesPath.toFile().getAbsolutePath(), "hunter.log").toFile(); + + try (InputStream confInputStream = HunterEDivisiveModel.class.getClassLoader().getResourceAsStream("changeDetection/hunter.yaml")) { + + + try( OutputStream confOut = new FileOutputStream(confFile)){ + confOut.write(confInputStream.readAllBytes()); + } catch (IOException e){ + log.error("Could not extract Hunter configuration from archive"); + } + + } catch (IOException e) { + log.error("Could not create temporary file for Hunter eDivisive algorithm", e); + } + + } + } + + + protected List executeProcess( TmpFiles tmpFiles, boolean redirectOutput, String... command){ + ProcessBuilder processBuilder = new ProcessBuilder(command); + Map env = processBuilder.environment(); + + env.put("HUNTER_CONFIG", tmpFiles.confFile.getAbsolutePath()); + processBuilder.directory(tmpFiles.tmpdir); + + processBuilder.redirectErrorStream(redirectOutput); + if(redirectOutput) + processBuilder.redirectOutput(ProcessBuilder.Redirect.appendTo(tmpFiles.logFile)); + + Process process = null; + try { + process = processBuilder.start(); + List results = readOutput(process.getInputStream()); + int exitCode = process.waitFor(); + + if ( exitCode != 0 ){ + log.errorf("Hunter process failed with exit code: %d", exitCode); + log.errorf("See error log for details: %s", tmpFiles.logFile.getAbsolutePath()); + return null; + } + + return results; + + } catch (IOException | InterruptedException e) { + if (process != null ) { + process.destroy(); + } + throw new RuntimeException(e); + } + } + + private List readOutput(InputStream inputStream) throws IOException { + try (BufferedReader output = new BufferedReader(new InputStreamReader(inputStream))) { + return output.lines() + .collect(Collectors.toList()); + } + } +} diff --git a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/changedetection/ModelType.java b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/changedetection/ModelType.java new file mode 100644 index 000000000..6fdf9e396 --- /dev/null +++ b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/changedetection/ModelType.java @@ -0,0 +1,6 @@ +package io.hyperfoil.tools.horreum.changedetection; + +public enum ModelType { + CONTINOUS, + BULK +} diff --git a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/changedetection/RelativeDifferenceChangeDetectionModel.java b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/changedetection/RelativeDifferenceChangeDetectionModel.java index 4e9851df6..426119df9 100644 --- a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/changedetection/RelativeDifferenceChangeDetectionModel.java +++ b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/changedetection/RelativeDifferenceChangeDetectionModel.java @@ -20,7 +20,6 @@ @ApplicationScoped public class RelativeDifferenceChangeDetectionModel implements ChangeDetectionModel { - public static final String NAME = "relativeDifference"; private static final Logger log = Logger.getLogger(RelativeDifferenceChangeDetectionModel.class); @Inject @@ -28,7 +27,7 @@ public class RelativeDifferenceChangeDetectionModel implements ChangeDetectionMo @Override public ConditionConfig config() { - ConditionConfig conditionConfig = new ConditionConfig(NAME, "Relative difference of means", + ConditionConfig conditionConfig = new ConditionConfig(ChangeDetectionModelType.names.RELATIVE_DIFFERENCE, "Relative difference of means", "This is a generic filter that splits the dataset into two subsets: the 'floating window' " + "and preceding datapoints. It calculates the mean of preceding datapoints and applies " + "the 'filter' function on the window of last datapoints; it compares these two values and " + @@ -48,7 +47,7 @@ public ConditionConfig config() { .addComponent("filter", new ConditionConfig.EnumComponent("mean").add("mean", "Mean value").add("min", "Minimum value").add("max", "Maximum value"), "Aggregation function for the floating window", "Function used to aggregate datapoints from the floating window."); - conditionConfig.defaults.put("model", new TextNode(NAME)); + conditionConfig.defaults.put("model", new TextNode(ChangeDetectionModelType.names.RELATIVE_DIFFERENCE)); return conditionConfig; } @@ -129,4 +128,9 @@ public void analyze(List dataPoints, JsonNode configuration, Consu } + + @Override + public ModelType getType() { + return ModelType.CONTINOUS; + } } diff --git a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/mapper/ChangeDetectionMapper.java b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/mapper/ChangeDetectionMapper.java index c09f4b452..2d1c4038f 100644 --- a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/mapper/ChangeDetectionMapper.java +++ b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/mapper/ChangeDetectionMapper.java @@ -1,7 +1,7 @@ package io.hyperfoil.tools.horreum.mapper; -import io.hyperfoil.tools.horreum.entity.alerting.ChangeDetectionDAO; import io.hyperfoil.tools.horreum.api.alerting.ChangeDetection; +import io.hyperfoil.tools.horreum.entity.alerting.ChangeDetectionDAO; public class ChangeDetectionMapper { diff --git a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/AlertingServiceImpl.java b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/AlertingServiceImpl.java index bee5d9b92..d55b7ac0b 100644 --- a/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/AlertingServiceImpl.java +++ b/horreum-backend/src/main/java/io/hyperfoil/tools/horreum/svc/AlertingServiceImpl.java @@ -9,6 +9,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -26,9 +27,12 @@ import io.hyperfoil.tools.horreum.api.data.*; import io.hyperfoil.tools.horreum.api.data.changeDetection.ChangeDetectionModelType; import io.hyperfoil.tools.horreum.bus.AsyncEventChannels; +import io.hyperfoil.tools.horreum.changedetection.ChangeDetectionModel; import io.hyperfoil.tools.horreum.changedetection.ChangeDetectionModelResolver; +import io.hyperfoil.tools.horreum.changedetection.ModelType; import io.hyperfoil.tools.horreum.hibernate.IntArrayType; import io.hyperfoil.tools.horreum.hibernate.JsonBinaryType; +import io.quarkus.panache.common.Parameters; import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.RolesAllowed; import jakarta.enterprise.context.ApplicationScoped; @@ -51,8 +55,6 @@ import io.hyperfoil.tools.horreum.entity.FingerprintDAO; import io.hyperfoil.tools.horreum.entity.PersistentLogDAO; import io.hyperfoil.tools.horreum.entity.alerting.*; -import io.hyperfoil.tools.horreum.changedetection.ChangeDetectionModel; -import io.hyperfoil.tools.horreum.changedetection.RelativeDifferenceChangeDetectionModel; import io.hyperfoil.tools.horreum.entity.data.DatasetDAO; import io.hyperfoil.tools.horreum.entity.data.TestDAO; @@ -267,7 +269,11 @@ public void onLabelsUpdated(Dataset.LabelsUpdatedEvent event) { sendNotifications = true; } } - recalculateDatapointsForDataset(dataset, sendNotifications, false, null); + Recalculation recalculation = new Recalculation(); + recalculation.clearDatapoints = true; + recalculation.lastDatapoint = true; + + recalculateDatapointsForDataset(dataset, sendNotifications, false, recalculation); recalculateMissingDataRules(dataset); } @@ -448,7 +454,7 @@ private void emitDatapoints(DatasetDAO dataset, boolean notify, boolean debug, R error -> logCalculationMessage(dataset, PersistentLogDAO.ERROR, "Evaluation of variable %s failed: %s", data.fullName(), error), info -> logCalculationMessage(dataset, PersistentLogDAO.INFO, "Evaluation of variable %s: %s", data.fullName(), info)); if (value != null) { - createDataPoint(dataset, finalTimestamp, data.variableId, value, notify); + createDataPoint(dataset, finalTimestamp, data.variableId, value, notify, recalculation); } else { if (recalculation != null) { recalculation.datasetsWithoutValue.put(dataset.id, dataset.getInfo()); @@ -486,7 +492,7 @@ private void emitDatapoints(DatasetDAO dataset, boolean notify, boolean debug, R } missingValueVariables.add(data.fullName()); } else { - createDataPoint(dataset, finalTimestamp, data.variableId, value, notify); + createDataPoint(dataset, finalTimestamp, data.variableId, value, notify, recalculation); } }, (data, exception, code) -> logCalculationMessage(dataset, PersistentLogDAO.ERROR, "Evaluation of variable %s failed: '%s' Code:
%s
", data.fullName(), exception.getMessage(), code), @@ -505,17 +511,28 @@ private void emitDatapoints(DatasetDAO dataset, boolean notify, boolean debug, R } @Transactional - void createDataPoint(DatasetDAO dataset, Instant timestamp, int variableId, double value, boolean notify) { - DataPointDAO dataPoint = new DataPointDAO(); - dataPoint.variable = VariableDAO.findById(variableId); - dataPoint.dataset = dataset; - dataPoint.timestamp = timestamp; - dataPoint.value = value; - dataPoint.persistAndFlush(); - DataPoint.Event event = new DataPoint.Event(DataPointMapper.from( dataPoint), dataset.testid, notify); - onNewDataPoint(event); //Test failure if we do not start a new thread and new tx - if(mediator.testMode()) - Util.registerTxSynchronization(tm, txStatus -> mediator.publishEvent(AsyncEventChannels.DATAPOINT_NEW, dataset.testid, event)); + void createDataPoint(DatasetDAO dataset, Instant timestamp, int variableId, double value, boolean notify, Recalculation recalculation) { + DataPointDAO dataPoint = null; + VariableDAO variableDAO = VariableDAO.findById(variableId); + if ( recalculation.clearDatapoints ) { + dataPoint = new DataPointDAO(); + dataPoint.variable = variableDAO; + dataPoint.dataset = dataset; + dataPoint.timestamp = timestamp; + dataPoint.value = value; + dataPoint.persistAndFlush(); + } else { + dataPoint = DataPointDAO.find("dataset = :dataset and variable = :variable", Parameters.with("dataset", dataset).and("variable", variableDAO)).firstResult(); + } + if ( dataPoint != null ) { + DataPoint.Event event = new DataPoint.Event(DataPointMapper.from(dataPoint), dataset.testid, notify); + onNewDataPoint(event, recalculation.lastDatapoint); //Test failure if we do not start a new thread and new tx + + if (mediator.testMode()) + Util.registerTxSynchronization(tm, txStatus -> mediator.publishEvent(AsyncEventChannels.DATAPOINT_NEW, dataset.testid, event)); + } else { + log.debugf("DataPoint for dataset %d, variable %d, timestamp %s, value %f not found", dataset.id, variableId, timestamp, value); + } } private void logCalculationMessage(DatasetDAO dataSet, int level, String format, Object... args) { @@ -549,7 +566,7 @@ private void logChangeDetectionMessage(int testId, int datasetId, int level, Str @WithRoles(extras = Roles.HORREUM_SYSTEM) @Transactional - void onNewDataPoint(DataPoint.Event event) { + void onNewDataPoint(DataPoint.Event event, boolean lastDatapoint) { DataPoint dataPoint = event.dataPoint; if (dataPoint.variable != null && dataPoint.variable.id != null) { VariableDAO variable = VariableDAO.findById(dataPoint.variable.id); @@ -568,7 +585,7 @@ void onNewDataPoint(DataPoint.Event event) { return current; } }); - runChangeDetection(VariableDAO.findById(variable.id), fingerprint, event.notify, true); + runChangeDetection(VariableDAO.findById(variable.id), fingerprint, event.notify, true, lastDatapoint); } else { log.warnf("Could not process new datapoint for dataset %d at %s, could not find variable by id %d ", dataPoint.datasetId, dataPoint.timestamp, dataPoint.variable == null ? -1 : dataPoint.variable.id); @@ -581,11 +598,11 @@ void onNewDataPoint(DataPoint.Event event) { @WithRoles(extras = Roles.HORREUM_SYSTEM) @Transactional - void tryRunChangeDetection(VariableDAO variable, JsonNode fingerprint, boolean notify) { - runChangeDetection(variable, fingerprint, notify, false); + void tryRunChangeDetection(VariableDAO variable, JsonNode fingerprint, boolean notify, boolean lastDatapoint) { + runChangeDetection(variable, fingerprint, notify, false, lastDatapoint); } - private void runChangeDetection(VariableDAO variable, JsonNode fingerprint, boolean notify, boolean expectExists) { + private void runChangeDetection(VariableDAO variable, JsonNode fingerprint, boolean notify, boolean expectExists, boolean lastDatapoint) { UpTo valid = validUpTo.get(new VarAndFingerprint(variable.id, fingerprint)); Instant nextTimestamp = session.createNativeQuery( "SELECT MIN(timestamp) FROM datapoint dp LEFT JOIN fingerprint fp ON dp.dataset_id = fp.dataset_id " + @@ -655,33 +672,37 @@ private void runChangeDetection(VariableDAO variable, JsonNode fingerprint, bool logChangeDetectionMessage(variable.testId, datasetId, PersistentLogDAO.ERROR, "Cannot find change detection model %s", detection.model); continue; } - model.analyze(dataPoints, detection.config, change -> { - logChangeDetectionMessage(variable.testId, datasetId, PersistentLogDAO.DEBUG, - "Change %s detected using datapoints %s", change, reversedAndLimited(dataPoints)); - DatasetDAO.Info info = session - .createNativeQuery("SELECT id, runid as \"runId\", ordinal, testid as \"testId\" FROM dataset WHERE id = ?1", Tuple.class) - .setParameter(1, change.dataset.id) - .setTupleTransformer((tuples, aliases) -> { - DatasetDAO.Info i = new DatasetDAO.Info(); - i.id = (int) tuples[0]; - i.runId = (int) tuples[1]; - i.ordinal = (int) tuples[2]; - i.testId = (int) tuples[3]; - return i; - }).getSingleResult(); - em.persist(change); - Hibernate.initialize(change.dataset.run.id); - String testName = TestDAO.findByIdOptional(variable.testId).map(test -> test.name).orElse(""); - Change.Event event = new Change.Event(ChangeMapper.from(change), testName, DatasetMapper.fromInfo(info), notify); - if(mediator.testMode()) - Util.registerTxSynchronization(tm, txStatus -> mediator.publishEvent(AsyncEventChannels.CHANGE_NEW, change.dataset.testid, event)); - mediator.executeBlocking(() -> mediator.newChange(event)) ; - }); + //Only run bulk models on the last datapoint, otherwise run on every datapoint + if (model.getType() == ModelType.CONTINOUS || (model.getType() == ModelType.BULK && lastDatapoint)) { + model.analyze(dataPoints, detection.config, change -> { + logChangeDetectionMessage(variable.testId, datasetId, PersistentLogDAO.DEBUG, + "Change %s detected using datapoints %s", change, reversedAndLimited(dataPoints)); + DatasetDAO.Info info = session + .createNativeQuery("SELECT id, runid as \"runId\", ordinal, testid as \"testId\" FROM dataset WHERE id = ?1", Tuple.class) + .setParameter(1, change.dataset.id) + .setTupleTransformer((tuples, aliases) -> { + DatasetDAO.Info i = new DatasetDAO.Info(); + i.id = (int) tuples[0]; + i.runId = (int) tuples[1]; + i.ordinal = (int) tuples[2]; + i.testId = (int) tuples[3]; + return i; + }).getSingleResult(); + em.persist(change); + Hibernate.initialize(change.dataset.run.id); + String testName = TestDAO.findByIdOptional(variable.testId).map(test -> test.name).orElse(""); + Change.Event event = new Change.Event(ChangeMapper.from(change), testName, DatasetMapper.fromInfo(info), notify); + if (mediator.testMode()) + Util.registerTxSynchronization(tm, txStatus -> mediator.publishEvent(AsyncEventChannels.CHANGE_NEW, change.dataset.testid, event)); + mediator.executeBlocking(() -> mediator.newChange(event)); + }); + } } } Util.doAfterCommit(tm, () -> { validateUpTo(variable, fingerprint, nextTimestamp); - messageBus.executeForTest(variable.testId, () -> tryRunChangeDetection(variable, fingerprint, notify)); + //assume not last datapoint if we have found more + messageBus.executeForTest(variable.testId, () -> tryRunChangeDetection(variable, fingerprint, notify, false)); }); } @@ -928,19 +949,20 @@ public void deleteChange(int id) { @RolesAllowed(Roles.TESTER) @WithRoles public void recalculateDatapoints(int testId, boolean notify, - boolean debug, Long from, Long to) { + boolean debug, Boolean clearDatapoints, Long from, Long to) { TestDAO test = TestDAO.findById(testId); if (test == null) { throw ServiceException.notFound("Test " + testId + " does not exist or is not available."); } else if (!Roles.hasRoleWithSuffix(identity, test.owner, "-tester")) { throw ServiceException.forbidden("This user cannot trigger the recalculation"); } + messageBus.executeForTest(testId, () -> { - startRecalculation(testId, notify, debug, from, to); + startRecalculation(testId, notify, debug, clearDatapoints == null ? true : clearDatapoints, from, to); }); } - void startRecalculation(int testId, boolean notify, boolean debug, Long from, Long to) { + void startRecalculation(int testId, boolean notify, boolean debug, boolean clearDatapoints, Long from, Long to) { Recalculation recalculation = new Recalculation(); Recalculation previous = recalcProgress.putIfAbsent(testId, recalculation); while (previous != null) { @@ -953,16 +975,28 @@ void startRecalculation(int testId, boolean notify, boolean debug, Long from, Lo } previous = recalcProgress.putIfAbsent(testId, recalculation); } + recalculation.clearDatapoints = clearDatapoints; + try { log.debugf("About to recalculate datapoints in test %d between %s and %s", testId, from, to); - recalculation.datasets = getDatasetsForRecalculation(testId, from, to); + //TODO:: determine if we should clear datapoints + recalculation.datasets = getDatasetsForRecalculation(testId, from, to, clearDatapoints); int numRuns = recalculation.datasets.size(); log.debugf("Starting recalculation of test %d, %d runs", testId, numRuns); int completed = 0; recalcProgress.put(testId, recalculation); - for (int datasetId : recalculation.datasets) { + //TODO:: this could be more streamlined + Map lastDatapoints = new HashMap<>(); + recalculation.datasets.entrySet().forEach( entry -> lastDatapoints.put(entry.getValue(), entry.getKey())); + Set lastDatapointSet = lastDatapoints.values().stream().collect(Collectors.toSet()); + for (int datasetId : recalculation.datasets.keySet()) { // Since the evaluation might take few moments and we're dealing potentially with thousands // of runs we'll process each run in a separate transaction + if ( lastDatapointSet.contains(datasetId) ) { + recalculation.lastDatapoint = true; + } else { + recalculation.lastDatapoint = false; + } recalculateForDataset(datasetId, notify, debug, recalculation); recalculation.progress = 100 * ++completed / numRuns; } @@ -980,18 +1014,28 @@ void startRecalculation(int testId, boolean notify, boolean debug, Long from, Lo // normally the calculation happens with system privileges anyway. @WithRoles(extras = Roles.HORREUM_SYSTEM) @Transactional - List getDatasetsForRecalculation(Integer testId, Long from, Long to) { - NativeQuery query = session - .createNativeQuery("SELECT id FROM dataset WHERE testid = ?1 AND (EXTRACT(EPOCH FROM start) * 1000 BETWEEN ?2 AND ?3) ORDER BY start", Integer.class) + Map getDatasetsForRecalculation(Integer testId, Long from, Long to, boolean clearDatapoints) { + Query query = session + .createNativeQuery("SELECT id, fingerprint FROM dataset LEFT JOIN fingerprint ON dataset.id = fingerprint.dataset_id WHERE testid = ?1 AND (EXTRACT(EPOCH FROM start) * 1000 BETWEEN ?2 AND ?3) ORDER BY start", Tuple.class) .setParameter(1, testId) .setParameter(2, from == null ? Long.MIN_VALUE : from) .setParameter(3, to == null ? Long.MAX_VALUE : to); - List ids = query.getResultList(); - DataPointDAO.delete("dataset.id in ?1", ids); - ChangeDAO.delete("dataset.id in ?1 AND confirmed = false", ids); + //use LinkedHashMap to preserve ordering of ID insertion - query is ordered on start + Map ids = new LinkedHashMap(); + query.getResultList().forEach(tuple -> + ids.put( + ((Integer) ((Tuple)tuple).get("id")).intValue(), + ((Tuple)tuple).get("fingerprint") == null ? "" : ((String) ((Tuple)tuple).get("fingerprint")).toString() + ) + ); + List datasetIDs = ids.keySet().stream().collect(Collectors.toList()); + if( clearDatapoints ) { + DataPointDAO.delete("dataset.id in ?1", datasetIDs); + } + ChangeDAO.delete("dataset.id in ?1 AND confirmed = false", datasetIDs); if (!ids.isEmpty()) { // Due to RLS policies we cannot add a record to a dataset we don't own - logCalculationMessage(testId, ids.get(0), PersistentLogDAO.INFO, "Starting recalculation of %d runs.", ids.size()); + logCalculationMessage(testId, datasetIDs.get(0) , PersistentLogDAO.INFO, "Starting recalculation of %d runs.", ids.size()); } return ids; } @@ -1119,13 +1163,13 @@ public List changeDetectionModels() { @Override public List defaultChangeDetectionConfigs() { ChangeDetectionDAO lastDatapoint = new ChangeDetectionDAO(); - lastDatapoint.model = RelativeDifferenceChangeDetectionModel.NAME; + lastDatapoint.model = ChangeDetectionModelType.names.RELATIVE_DIFFERENCE; lastDatapoint.config = JsonNodeFactory.instance.objectNode() - .put("window", 1).put("model", RelativeDifferenceChangeDetectionModel.NAME).put("filter", "mean").put("threshold", 0.2).put("minPrevious", 5); + .put("window", 1).put("model", ChangeDetectionModelType.names.RELATIVE_DIFFERENCE).put("filter", "mean").put("threshold", 0.2).put("minPrevious", 5); ChangeDetectionDAO floatingWindow = new ChangeDetectionDAO(); - floatingWindow.model = RelativeDifferenceChangeDetectionModel.NAME; + floatingWindow.model = ChangeDetectionModelType.names.RELATIVE_DIFFERENCE; floatingWindow.config = JsonNodeFactory.instance.objectNode() - .put("window", 5).put("model", RelativeDifferenceChangeDetectionModel.NAME).put("filter", "mean").put("threshold", 0.1).put("minPrevious", 5); + .put("window", 5).put("model", ChangeDetectionModelType.names.RELATIVE_DIFFERENCE).put("filter", "mean").put("threshold", 0.1).put("minPrevious", 5); return Arrays.asList(lastDatapoint, floatingWindow).stream().map(ChangeDetectionMapper::from).collect(Collectors.toList()); } @@ -1292,10 +1336,14 @@ public void checkExpectedRuns() { // Note: this class must be public - otherwise when this is used as a parameter to // a method in AlertingServiceImpl the interceptors would not be invoked. public static class Recalculation { - List datasets = Collections.emptyList(); + Map datasets = Collections.emptyMap(); int progress; boolean done; public int errors; + + boolean lastDatapoint; + boolean clearDatapoints; + Map datasetsWithoutValue = new HashMap<>(); } diff --git a/horreum-backend/src/main/resources/changeDetection/hunter.yaml b/horreum-backend/src/main/resources/changeDetection/hunter.yaml new file mode 100644 index 000000000..2098e4552 --- /dev/null +++ b/horreum-backend/src/main/resources/changeDetection/hunter.yaml @@ -0,0 +1,10 @@ +tests: + horreum: + type: csv + file: tests/resources/horreum.csv + time_column: timestamp + metrics: [kpi] + attributes: [datasetid] + csv_options: + delimiter: "," + quote_char: "\"" diff --git a/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/changedetection/EdivisiveTests.java b/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/changedetection/EdivisiveTests.java new file mode 100644 index 000000000..289f2fc6d --- /dev/null +++ b/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/changedetection/EdivisiveTests.java @@ -0,0 +1,208 @@ +package io.hyperfoil.tools.horreum.changedetection; + +import io.hyperfoil.tools.horreum.api.alerting.Change; +import io.hyperfoil.tools.horreum.api.alerting.ChangeDetection; +import io.hyperfoil.tools.horreum.api.alerting.DataPoint; +import io.hyperfoil.tools.horreum.api.data.Label; +import io.hyperfoil.tools.horreum.api.data.Schema; +import io.hyperfoil.tools.horreum.api.data.changeDetection.ChangeDetectionModelType; +import io.hyperfoil.tools.horreum.bus.AsyncEventChannels; +import io.hyperfoil.tools.horreum.entity.alerting.ChangeDAO; +import io.hyperfoil.tools.horreum.entity.alerting.DataPointDAO; +import io.hyperfoil.tools.horreum.entity.alerting.VariableDAO; +import io.hyperfoil.tools.horreum.entity.data.DatasetDAO; +import io.hyperfoil.tools.horreum.svc.BaseServiceTest; +import io.hyperfoil.tools.horreum.svc.ServiceMediator; +import io.hyperfoil.tools.horreum.test.HorreumTestProfile; +import io.hyperfoil.tools.horreum.test.PostgresResource; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import io.quarkus.test.oidc.server.OidcWiremockTestResource; +import jakarta.inject.Inject; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.*; + +@QuarkusTest +@QuarkusTestResource(PostgresResource.class) +@QuarkusTestResource(OidcWiremockTestResource.class) +@TestProfile(HorreumTestProfile.class) +@Tag("CiTests") +public class EdivisiveTests extends BaseServiceTest { + + @Inject + ChangeDetectionModelResolver resolver; + + @Inject + ServiceMediator serviceMediator; + + @Test + public void testTemporaryFiles() { + try { + HunterEDivisiveModel.TmpFiles tmpFiles = HunterEDivisiveModel.TmpFiles.instance(); + assertTrue(tmpFiles.tmpdir.exists()); + } catch (IOException e) { + fail(e); + } + } + + @Test + public void testFileStructures() { + + HunterEDivisiveModel model = (HunterEDivisiveModel) resolver.getModel(ChangeDetectionModelType.EDIVISIVE); + assertNotNull(model); + + try { + //1. Valid File Structure + HunterEDivisiveModel.TmpFiles tmpFiles = getTmpFiles("change/eDivisive/valid/tests/resources/horreum.csv"); + + //cast ChangeDetectionModel to access protected methods + boolean valid = model.validateInputCsv(tmpFiles); + + assertTrue(valid); + + //2. Invalid File Structure + tmpFiles = getTmpFiles("change/eDivisive/invalid/tests/resources/horreum.csv"); + + //cast ChangeDetectionModel to access protected methods + valid = model.validateInputCsv(tmpFiles); + + assertFalse(valid); + + + //todo:: cleanup temp files + } catch (IOException e) { + throw new RuntimeException(e); + } + + } + + @NotNull + private static HunterEDivisiveModel.TmpFiles getTmpFiles(String resource) throws IOException { + HunterEDivisiveModel.TmpFiles tmpFiles = HunterEDivisiveModel.TmpFiles.instance(); + assertTrue(tmpFiles.confFile.exists()); + + try (InputStream validCsvStream = EdivisiveTests.class.getClassLoader().getResourceAsStream(resource)) { + try( OutputStream confOut = new FileOutputStream(tmpFiles.inputFile)){ + confOut.write(validCsvStream.readAllBytes()); + } catch (IOException e){ + fail("Could not extract Hunter configuration from archive"); + } + } catch (IOException e) { + fail("Could not create temporary file for Hunter eDivisive algorithm", e); + } + return tmpFiles; + } + + @Test + public void testDetectedChangePoints(){ + + HunterEDivisiveModel model = (HunterEDivisiveModel) resolver.getModel(ChangeDetectionModelType.EDIVISIVE); + assertNotNull(model); + + List changePoints = new ArrayList<>(); + + try { + HunterEDivisiveModel.TmpFiles tmpFiles = getTmpFiles("change/eDivisive/valid/tests/resources/horreum.csv"); + + Instant sinceInstant = Instant.ofEpochSecond(1702002504); + //cast ChangeDetectionModel to access protected methods + boolean valid = model.validateInputCsv(tmpFiles); + + assertTrue(valid); + + model.processChangePoints( + (datapointID) -> { + DataPointDAO datapoint = new DataPointDAO(); + datapoint.id = datapointID; + datapoint.timestamp = Instant.now(); + datapoint.variable = new VariableDAO(); + DatasetDAO datasetDAO = new DatasetDAO(); + datasetDAO.id = datapointID; + datapoint.dataset = datasetDAO; + return Optional.of(datapoint); + }, + (changePoints::add), + tmpFiles, + sinceInstant + ); + + assertNotEquals(0, changePoints.size()); + + assertEquals(1535410, changePoints.get(0).dataset.id); + + model.cleanupTmpFiles(tmpFiles); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Test + public void testEdvisiveModelAnalyze(TestInfo info) throws Exception { + + io.hyperfoil.tools.horreum.api.data.Test test = createTest(createExampleTest(getTestName(info))); + Schema schema = createExampleSchema(info); + + ChangeDetection cd = new ChangeDetection(); + cd.model = ChangeDetectionModelType.names.EDIVISIVE; + setTestVariables(test, "Value", new Label("value", schema.id), cd); + + BlockingQueue datapointQueue = serviceMediator.getEventQueue(AsyncEventChannels.DATAPOINT_NEW, test.id); + BlockingQueue changeQueue = serviceMediator.getEventQueue(AsyncEventChannels.CHANGE_NEW, test.id); + + long ts = System.currentTimeMillis(); + uploadRun(ts, ts, runWithValue(1, schema), test.name); + assertValue(datapointQueue, 1); + uploadRun(ts + 1, ts + 1, runWithValue(2, schema), test.name); + assertValue(datapointQueue, 2); + uploadRun(ts + 2, ts + 2, runWithValue(1, schema), test.name); + assertValue(datapointQueue, 1); + uploadRun(ts + 3, ts + 3, runWithValue(2, schema), test.name); + assertValue(datapointQueue, 2); + uploadRun(ts + 3, ts + 3, runWithValue(2, schema), test.name); + assertValue(datapointQueue, 2); + uploadRun(ts + 3, ts + 3, runWithValue(1, schema), test.name); + assertValue(datapointQueue, 1); + uploadRun(ts + 3, ts + 3, runWithValue(1, schema), test.name); + assertValue(datapointQueue, 1); + uploadRun(ts + 3, ts + 3, runWithValue(2, schema), test.name); + assertValue(datapointQueue, 2); + uploadRun(ts + 3, ts + 3, runWithValue(2, schema), test.name); + assertValue(datapointQueue, 2); + + assertNull(changeQueue.poll(50, TimeUnit.MILLISECONDS)); + + int run10 = uploadRun(ts + 4, ts + 4, runWithValue(10, schema), test.name); + assertValue(datapointQueue, 10); + + Change.Event changeEvent1 = changeQueue.poll(10, TimeUnit.SECONDS); + assertNotNull(changeEvent1); + + testSerialization(changeEvent1, Change.Event.class); + + assertEquals(run10, changeEvent1.change.dataset.runId); + Pattern pattern = Pattern.compile(".*`(?\\+\\d*.\\d%)`.*", Pattern.CASE_INSENSITIVE); + Matcher matcher = pattern.matcher(changeEvent1.change.description); + boolean matchFound = matcher.find(); + assertTrue(matchFound); + assertEquals("+542.9%", matcher.group(1)); + + } +} diff --git a/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/AlertingServiceTest.java b/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/AlertingServiceTest.java index c32cf8a70..2b820c271 100644 --- a/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/AlertingServiceTest.java +++ b/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/AlertingServiceTest.java @@ -12,6 +12,7 @@ import io.hyperfoil.tools.horreum.api.alerting.*; import io.hyperfoil.tools.horreum.api.data.Dataset; import io.hyperfoil.tools.horreum.api.data.Fingerprints; +import io.hyperfoil.tools.horreum.api.data.changeDetection.ChangeDetectionModelType; import io.hyperfoil.tools.horreum.bus.AsyncEventChannels; import io.hyperfoil.tools.horreum.changedetection.RelativeDifferenceChangeDetectionModel; import io.restassured.common.mapper.TypeRef; @@ -202,15 +203,6 @@ public void testChangeDetection(TestInfo info) throws InterruptedException { assertEquals(run6, changeEvent3.change.dataset.runId); } - private void testSerialization(T event, Class eventClass) { - // test serialization and deserialization - JsonNode changeJson = Util.OBJECT_MAPPER.valueToTree(event); - try { - Util.OBJECT_MAPPER.treeToValue(changeJson, eventClass); - } catch (JsonProcessingException e) { - throw new AssertionError("Cannot deserialize " + event + " from " + changeJson.toPrettyString(), e); - } - } @org.junit.jupiter.api.Test public void testChangeDetectionWithFingerprint(TestInfo info) throws InterruptedException { @@ -250,14 +242,6 @@ public void testChangeDetectionWithFingerprint(TestInfo info) throws Interrupted assertEquals(run14, changeEvent2.dataset.runId); } - private DataPoint assertValue(BlockingQueue datapointQueue, double value) throws InterruptedException { - DataPoint.Event dpe = datapointQueue.poll(10, TimeUnit.SECONDS); - assertNotNull(dpe); - assertEquals(value, dpe.dataPoint.value); - testSerialization(dpe, DataPoint.Event.class); - return dpe.dataPoint; - } - @org.junit.jupiter.api.Test public void testFingerprintLabelsChange(TestInfo info) throws Exception { Test test = createExampleTest(getTestName(info)); @@ -606,7 +590,7 @@ public void testFixedThresholds(TestInfo info) throws InterruptedException { Test test = createTest(createExampleTest(getTestName(info))); Schema schema = createExampleSchema(info); ChangeDetection rd = new ChangeDetection(); - rd.model = FixedThresholdModel.NAME; + rd.model = ChangeDetectionModelType.names.FIXED_THRESHOLD; ObjectNode config = JsonNodeFactory.instance.objectNode(); config.putObject("min").put("value", 3).put("enabled", true).put("inclusive", true); config.putObject("max").put("value", 6).put("enabled", true).put("inclusive", false); @@ -664,10 +648,13 @@ public void testCustomTimeline(TestInfo info) throws InterruptedException { // The DataSets will be recalculated based on DataSet.start, not DataPoint.timestamp recalculateDatapoints(test.id); DataPoint.Event dp22 = datapointQueue.poll(10, TimeUnit.SECONDS); + assertNotNull(dp22); assertEquals(Instant.ofEpochSecond(1662023777), dp22.dataPoint.timestamp); DataPoint.Event dp21 = datapointQueue.poll(10, TimeUnit.SECONDS); + assertNotNull(dp21); assertEquals(Instant.ofEpochSecond(1662023776), dp21.dataPoint.timestamp); DataPoint.Event dp23 = datapointQueue.poll(10, TimeUnit.SECONDS); + assertNotNull(dp23); assertEquals(Instant.ofEpochSecond(1662023778), dp23.dataPoint.timestamp); setChangeDetectionTimeline(test, Arrays.asList("timestamp", "value"), "({ timestamp, value }) => timestamp"); @@ -780,7 +767,7 @@ public void testUpdateVariablesHandlesNegativeId(TestInfo info) throws Exception ChangeDetection problematicChangeDetection = new ChangeDetection(); problematicChangeDetection.id = -1; //UI typically sets this value - problematicChangeDetection.model = RelativeDifferenceChangeDetectionModel.NAME; + problematicChangeDetection.model = ChangeDetectionModelType.names.RELATIVE_DIFFERENCE; problematicChangeDetection.config = JsonNodeFactory.instance.objectNode().put("threshold", 0.2).put("minPrevious", 2).put("window", 2).put("filter", "mean"); List labels = Collections.singletonList("foobar"); Set cdSet = Collections.singleton(problematicChangeDetection); diff --git a/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/BaseServiceTest.java b/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/BaseServiceTest.java index 85cb44cd3..1728b7e16 100644 --- a/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/BaseServiceTest.java +++ b/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/BaseServiceTest.java @@ -20,11 +20,14 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import io.hyperfoil.tools.horreum.api.SortDirection; import io.hyperfoil.tools.horreum.api.alerting.ChangeDetection; +import io.hyperfoil.tools.horreum.api.alerting.DataPoint; import io.hyperfoil.tools.horreum.api.alerting.Variable; +import io.hyperfoil.tools.horreum.api.data.changeDetection.ChangeDetectionModelType; import io.hyperfoil.tools.horreum.api.internal.services.AlertingService; import io.hyperfoil.tools.horreum.api.report.ReportComponent; import io.hyperfoil.tools.horreum.api.report.TableReportConfig; @@ -748,7 +751,7 @@ protected ChangeDetection addChangeDetectionVariable(Test test, int schemaId) { protected ChangeDetection addChangeDetectionVariable(Test test, double threshold, int window, int schemaId) { ChangeDetection cd = new ChangeDetection(); - cd.model = RelativeDifferenceChangeDetectionModel.NAME; + cd.model = ChangeDetectionModelType.names.RELATIVE_DIFFERENCE; cd.config = JsonNodeFactory.instance.objectNode().put("threshold", threshold).put("minPrevious", window).put("window", window).put("filter", "mean"); setTestVariables(test, "Value", new Label("value", schemaId), cd); return cd; @@ -1043,4 +1046,22 @@ protected TestService.TestListing listTestSummary(String roles, String folder, i .body() .as(TestService.TestListing.class); } + + protected DataPoint assertValue(BlockingQueue datapointQueue, double value) throws InterruptedException { + DataPoint.Event dpe = datapointQueue.poll(10, TimeUnit.SECONDS); + assertNotNull(dpe); + assertEquals(value, dpe.dataPoint.value); + testSerialization(dpe, DataPoint.Event.class); + return dpe.dataPoint; + } + + protected void testSerialization(T event, Class eventClass) { + // test serialization and deserialization + JsonNode changeJson = Util.OBJECT_MAPPER.valueToTree(event); + try { + Util.OBJECT_MAPPER.treeToValue(changeJson, eventClass); + } catch (JsonProcessingException e) { + throw new AssertionError("Cannot deserialize " + event + " from " + changeJson.toPrettyString(), e); + } + } } diff --git a/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/RunServiceTest.java b/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/RunServiceTest.java index d7df3bbf6..ab2877aca 100644 --- a/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/RunServiceTest.java +++ b/horreum-backend/src/test/java/io/hyperfoil/tools/horreum/svc/RunServiceTest.java @@ -22,6 +22,7 @@ import io.hyperfoil.tools.horreum.api.SortDirection; import io.hyperfoil.tools.horreum.api.alerting.ChangeDetection; import io.hyperfoil.tools.horreum.api.alerting.Variable; +import io.hyperfoil.tools.horreum.api.data.changeDetection.ChangeDetectionModelType; import io.hyperfoil.tools.horreum.api.internal.services.AlertingService; import io.hyperfoil.tools.horreum.api.services.DatasetService; import io.hyperfoil.tools.horreum.api.services.ExperimentService; @@ -29,6 +30,7 @@ import io.hyperfoil.tools.horreum.bus.AsyncEventChannels; import io.hyperfoil.tools.horreum.mapper.DatasetMapper; import io.restassured.response.Response; +import jakarta.validation.constraints.NotNull; import jakarta.ws.rs.core.HttpHeaders; import io.hyperfoil.tools.horreum.test.HorreumTestProfile; @@ -1025,7 +1027,7 @@ public void runExperiment() throws InterruptedException { l.name = "throughput"; variable.labels.add(l.name); ChangeDetection changeDetection = new ChangeDetection(); - changeDetection.model = "relativeDifference"; + changeDetection.model = ChangeDetectionModelType.names.RELATIVE_DIFFERENCE; changeDetection.config = (ObjectNode) mapper.readTree("{" + " \"window\": 1," + diff --git a/horreum-backend/src/test/resources/change/eDivisive/invalid/tests/resources/horreum.csv b/horreum-backend/src/test/resources/change/eDivisive/invalid/tests/resources/horreum.csv new file mode 100644 index 000000000..19ee62b16 --- /dev/null +++ b/horreum-backend/src/test/resources/change/eDivisive/invalid/tests/resources/horreum.csv @@ -0,0 +1,54 @@ +metric,timestamp,datasetid +218.18,1702002504,1535215 +220.10,1702090996,1535230 +227.12,1702175834,1535245 +223.78,1702261353,1535260 +216.40,1702434430,1535275 +223.16,1702520496,1535290 +216.08,1702606686,1535305 +221.22,1702779509,1535320 +215.02,1702954811,1535335 +215.60,1703041214,1535350 +215.04,1703125229,1535365 +220.50,1703211425,1535380 +217.76,1703297654,1535395 +208.28,1704249579,1535410 +205.98,1704334469,1535425 +207.66,1704420880,1535440 +272.28,1704766638,1535455 +394.14,1704852833,1535470 +391.28,1704939710,1535485 +391.64,1705026825,1535500 +392.70,1705113908,1535515 +386.06,1705198424,1535530 +291.36,1705720892,1535545 +290.66,1705807386,1535560 +292.48,1705976446,1535575 +295.60,1706063514,1535590 +294.22,1706149942,1535605 +293.54,1706321797,1535620 +289.58,1706408174,1535635 +293.18,1706494518,1535650 +296.98,1706580981,1535665 +292.06,1706667393,1535680 +245.02,1706930289,1535695 +243.02,1707016645,1535710 +246.40,1707189452,1535725 +246.42,1707275824,1535740 +245.82,1707621387,1535755 +246.04,1707707675,1535770 +248.62,1707796869,1535785 +246.60,1707876793,1535800 +242.62,1707963295,1535815 +241.64,1708049695,1535830 +436.54,1708136103,1535845 +437.38,1708222520,1535860 +438.98,1708308946,1535875 +442.50,1708395190,1535890 +445.08,1708481712,1535905 +444.62,1708568111,1535920 +445.26,1708654497,1535935 +441.32,1708740783,1535950 +444.20,1708827306,1535965 +446.12,1708913580,1535980 +446.62,1709000081,1535995 diff --git a/horreum-backend/src/test/resources/change/eDivisive/valid/tests/resources/horreum.csv b/horreum-backend/src/test/resources/change/eDivisive/valid/tests/resources/horreum.csv new file mode 100644 index 000000000..02b2bcb46 --- /dev/null +++ b/horreum-backend/src/test/resources/change/eDivisive/valid/tests/resources/horreum.csv @@ -0,0 +1,54 @@ +kpi,timestamp,datasetid +218.18,1702002504,1535215 +220.10,1702090996,1535230 +227.12,1702175834,1535245 +223.78,1702261353,1535260 +216.40,1702434430,1535275 +223.16,1702520496,1535290 +216.08,1702606686,1535305 +221.22,1702779509,1535320 +215.02,1702954811,1535335 +215.60,1703041214,1535350 +215.04,1703125229,1535365 +220.50,1703211425,1535380 +217.76,1703297654,1535395 +208.28,1704249579,1535410 +205.98,1704334469,1535425 +207.66,1704420880,1535440 +272.28,1704766638,1535455 +394.14,1704852833,1535470 +391.28,1704939710,1535485 +391.64,1705026825,1535500 +392.70,1705113908,1535515 +386.06,1705198424,1535530 +291.36,1705720892,1535545 +290.66,1705807386,1535560 +292.48,1705976446,1535575 +295.60,1706063514,1535590 +294.22,1706149942,1535605 +293.54,1706321797,1535620 +289.58,1706408174,1535635 +293.18,1706494518,1535650 +296.98,1706580981,1535665 +292.06,1706667393,1535680 +245.02,1706930289,1535695 +243.02,1707016645,1535710 +246.40,1707189452,1535725 +246.42,1707275824,1535740 +245.82,1707621387,1535755 +246.04,1707707675,1535770 +248.62,1707796869,1535785 +246.60,1707876793,1535800 +242.62,1707963295,1535815 +241.64,1708049695,1535830 +436.54,1708136103,1535845 +437.38,1708222520,1535860 +438.98,1708308946,1535875 +442.50,1708395190,1535890 +445.08,1708481712,1535905 +444.62,1708568111,1535920 +445.26,1708654497,1535935 +441.32,1708740783,1535950 +444.20,1708827306,1535965 +446.12,1708913580,1535980 +446.62,1709000081,1535995 diff --git a/horreum-integration-tests/src/test/java/io/hyperfoil/tools/horreum/it/HorreumClientIT.java b/horreum-integration-tests/src/test/java/io/hyperfoil/tools/horreum/it/HorreumClientIT.java index 8ae8b3bf9..72736eb7c 100644 --- a/horreum-integration-tests/src/test/java/io/hyperfoil/tools/horreum/it/HorreumClientIT.java +++ b/horreum-integration-tests/src/test/java/io/hyperfoil/tools/horreum/it/HorreumClientIT.java @@ -18,6 +18,7 @@ import io.hyperfoil.tools.horreum.api.data.Run; import io.hyperfoil.tools.horreum.api.data.Schema; import io.hyperfoil.tools.horreum.api.data.Test; +import io.hyperfoil.tools.horreum.api.data.changeDetection.ChangeDetectionModelType; import io.hyperfoil.tools.horreum.api.internal.services.AlertingService; import io.hyperfoil.tools.horreum.api.services.DatasetService; import io.hyperfoil.tools.horreum.api.services.ExperimentService; @@ -238,7 +239,7 @@ public void runExperiment() { variable.order = 0; variable.labels = Collections.singletonList("throughput"); ChangeDetection changeDetection = new ChangeDetection(); - changeDetection.model = "relativeDifference"; + changeDetection.model = ChangeDetectionModelType.names.RELATIVE_DIFFERENCE; changeDetection.config = (ObjectNode) mapper.readTree("{" + " \"window\": 1," + diff --git a/horreum-web/package-lock.json b/horreum-web/package-lock.json index afc97ac89..bbcc3f407 100644 --- a/horreum-web/package-lock.json +++ b/horreum-web/package-lock.json @@ -82,11 +82,11 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.1.tgz", - "integrity": "sha512-bC49z4spJQR3j8vFtJBLqzyzFV0ciuL5HYX7qfSl3KEqeMVV+eTquRvmXxpvB0AMubRrvv7y5DILiLLPi57Ewg==", + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", "dependencies": { - "@babel/highlight": "^7.24.1", + "@babel/highlight": "^7.24.2", "picocolors": "^1.0.0" }, "engines": { @@ -94,25 +94,25 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.1.tgz", - "integrity": "sha512-Pc65opHDliVpRHuKfzI+gSA4zcgr65O4cl64fFJIWEEh8JoHIHh0Oez1Eo8Arz8zq/JhgKodQaxEwUPRtZylVA==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", + "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.1.tgz", - "integrity": "sha512-F82udohVyIgGAY2VVj/g34TpFUG606rumIHjTfVbssPg2zTR7PuuEpZcX8JA6sgBfIYmJrFtWgPvHQuJamVqZQ==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.4.tgz", + "integrity": "sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.1", - "@babel/generator": "^7.24.1", + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.4", "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.1", - "@babel/parser": "^7.24.1", + "@babel/helpers": "^7.24.4", + "@babel/parser": "^7.24.4", "@babel/template": "^7.24.0", "@babel/traverse": "^7.24.1", "@babel/types": "^7.24.0", @@ -139,9 +139,9 @@ } }, "node_modules/@babel/generator": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.1.tgz", - "integrity": "sha512-DfCRfZsBcrPEHUfuBMgbJ1Ut01Y/itOs+hY2nFLgqsqXd52/iSiVq5TITtUasIUgm+IIKdY2/1I7auiQOEeC9A==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.4.tgz", + "integrity": "sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==", "dependencies": { "@babel/types": "^7.24.0", "@jridgewell/gen-mapping": "^0.3.5", @@ -207,9 +207,9 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.1.tgz", - "integrity": "sha512-HfEWzysMyOa7xI5uQHc/OcZf67/jc+xe/RZlznWQHhbb8Pg1SkRdbK4yEi61aY8wxQA7PkSfoojtLQP/Kpe3og==", + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", + "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", "dependencies": { "@babel/types": "^7.24.0" }, @@ -290,9 +290,9 @@ } }, "node_modules/@babel/helpers": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.1.tgz", - "integrity": "sha512-BpU09QqEe6ZCHuIHFphEFgvNSrubve1FtyMton26ekZ85gRGi6LrTF7zArARp2YvyFxloeiRmtSCq5sjh1WqIg==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.4.tgz", + "integrity": "sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw==", "dependencies": { "@babel/template": "^7.24.0", "@babel/traverse": "^7.24.1", @@ -303,9 +303,9 @@ } }, "node_modules/@babel/highlight": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.1.tgz", - "integrity": "sha512-EPmDPxidWe/Ex+HTFINpvXdPHRmgSF3T8hGvzondYjmgzTQ/0EbLpSxyt+w3zzlYSk9cNBQNF9k0dT5Z2NiBjw==", + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", + "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", @@ -317,9 +317,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.1.tgz", - "integrity": "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz", + "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==", "bin": { "parser": "bin/babel-parser.js" }, @@ -356,9 +356,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", - "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz", + "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -856,9 +856,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "dev": true }, "node_modules/@jridgewell/gen-mapping": { @@ -986,18 +986,18 @@ } }, "node_modules/@patternfly/react-icons": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-5.2.1.tgz", - "integrity": "sha512-aeJ0X+U2NDe8UmI5eQiT0iuR/wmUq97UkDtx3HoZcpRb9T6eUBfysllxjRqHS8rOOspdU8OWq+CUhQ/E2ZDibg==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-5.3.0.tgz", + "integrity": "sha512-oBdaK4Gz7yivNE7jQg46sPzfZakg7oxo5aSMLc0N6haOmDEegiTurNex+h+/z0oBPqzZC+cIQRaBeXEgXGwc9Q==", "peerDependencies": { "react": "^17 || ^18", "react-dom": "^17 || ^18" } }, "node_modules/@patternfly/react-styles": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-5.2.1.tgz", - "integrity": "sha512-GT96hzI1QenBhq6Pfc51kxnj9aVLjL1zSLukKZXcYVe0HPOy0BFm90bT1Fo4e/z7V9cDYw4SqSX1XLc3O4jsTw==" + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-5.3.0.tgz", + "integrity": "sha512-/EdkURW+v7Rzw/CiEqL+NfGtLvLMGIwOEyDhvlMDbRip2usGw4HLZv3Bep0cJe29zOeY27cDVZDM1HfyXLebtw==" }, "node_modules/@patternfly/react-table": { "version": "5.2.4", @@ -1017,9 +1017,9 @@ } }, "node_modules/@patternfly/react-tokens": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-5.2.1.tgz", - "integrity": "sha512-8GYz/jnJTGAWUJt5eRAW5dtyiHPKETeFJBPGHaUQnvi/t1ZAkoy8i4Kd/RlHsDC7ktiu813SKCmlzwBwldAHKg==" + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-5.3.0.tgz", + "integrity": "sha512-24ZY5hgwt11InW3XtINM5p9Fo1hDiVor6Q4uphPZh8Mt89AsZZw1UweTaGg54I0Ah2Wzv6rkQy51LX7tZtIwjQ==" }, "node_modules/@remix-run/router": { "version": "1.15.3", @@ -1030,9 +1030,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz", - "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.16.4.tgz", + "integrity": "sha512-GkhjAaQ8oUTOKE4g4gsZ0u8K/IHU1+2WQSgS1TwTcYvL+sjbaQjNHFXbOJ6kgqGHIO1DfUhI/Sphi9GkRT9K+Q==", "cpu": [ "arm" ], @@ -1042,9 +1042,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz", - "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.16.4.tgz", + "integrity": "sha512-Bvm6D+NPbGMQOcxvS1zUl8H7DWlywSXsphAeOnVeiZLQ+0J6Is8T7SrjGTH29KtYkiY9vld8ZnpV3G2EPbom+w==", "cpu": [ "arm64" ], @@ -1054,9 +1054,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz", - "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.16.4.tgz", + "integrity": "sha512-i5d64MlnYBO9EkCOGe5vPR/EeDwjnKOGGdd7zKFhU5y8haKhQZTN2DgVtpODDMxUr4t2K90wTUJg7ilgND6bXw==", "cpu": [ "arm64" ], @@ -1066,9 +1066,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz", - "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.16.4.tgz", + "integrity": "sha512-WZupV1+CdUYehaZqjaFTClJI72fjJEgTXdf4NbW69I9XyvdmztUExBtcI2yIIU6hJtYvtwS6pkTkHJz+k08mAQ==", "cpu": [ "x64" ], @@ -1078,9 +1078,21 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz", - "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.16.4.tgz", + "integrity": "sha512-ADm/xt86JUnmAfA9mBqFcRp//RVRt1ohGOYF6yL+IFCYqOBNwy5lbEK05xTsEoJq+/tJzg8ICUtS82WinJRuIw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.16.4.tgz", + "integrity": "sha512-tJfJaXPiFAG+Jn3cutp7mCs1ePltuAgRqdDZrzb1aeE3TktWWJ+g7xK9SNlaSUFw6IU4QgOxAY4rA+wZUT5Wfg==", "cpu": [ "arm" ], @@ -1090,9 +1102,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz", - "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.16.4.tgz", + "integrity": "sha512-7dy1BzQkgYlUTapDTvK997cgi0Orh5Iu7JlZVBy1MBURk7/HSbHkzRnXZa19ozy+wwD8/SlpJnOOckuNZtJR9w==", "cpu": [ "arm64" ], @@ -1102,9 +1114,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz", - "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.16.4.tgz", + "integrity": "sha512-zsFwdUw5XLD1gQe0aoU2HVceI6NEW7q7m05wA46eUAyrkeNYExObfRFQcvA6zw8lfRc5BHtan3tBpo+kqEOxmg==", "cpu": [ "arm64" ], @@ -1113,10 +1125,22 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.16.4.tgz", + "integrity": "sha512-p8C3NnxXooRdNrdv6dBmRTddEapfESEUflpICDNKXpHvTjRRq1J82CbU5G3XfebIZyI3B0s074JHMWD36qOW6w==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz", - "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.16.4.tgz", + "integrity": "sha512-Lh/8ckoar4s4Id2foY7jNgitTOUQczwMWNYi+Mjt0eQ9LKhr6sK477REqQkmy8YHY3Ca3A2JJVdXnfb3Rrwkng==", "cpu": [ "riscv64" ], @@ -1125,10 +1149,22 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.16.4.tgz", + "integrity": "sha512-1xwwn9ZCQYuqGmulGsTZoKrrn0z2fAur2ujE60QgyDpHmBbXbxLaQiEvzJWDrscRq43c8DnuHx3QorhMTZgisQ==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz", - "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.16.4.tgz", + "integrity": "sha512-LuOGGKAJ7dfRtxVnO1i3qWc6N9sh0Em/8aZ3CezixSTM+E9Oq3OvTsvC4sm6wWjzpsIlOCnZjdluINKESflJLA==", "cpu": [ "x64" ], @@ -1138,9 +1174,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz", - "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.16.4.tgz", + "integrity": "sha512-ch86i7KkJKkLybDP2AtySFTRi5fM3KXp0PnHocHuJMdZwu7BuyIKi35BE9guMlmTpwwBTB3ljHj9IQXnTCD0vA==", "cpu": [ "x64" ], @@ -1150,9 +1186,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz", - "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.16.4.tgz", + "integrity": "sha512-Ma4PwyLfOWZWayfEsNQzTDBVW8PZ6TUUN1uFTBQbF2Chv/+sjenE86lpiEwj2FiviSmSZ4Ap4MaAfl1ciF4aSA==", "cpu": [ "arm64" ], @@ -1162,9 +1198,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz", - "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.16.4.tgz", + "integrity": "sha512-9m/ZDrQsdo/c06uOlP3W9G2ENRVzgzbSXmXHT4hwVaDQhYcRpi9bgBT0FTG9OhESxwK0WjQxYOSfv40cU+T69w==", "cpu": [ "ia32" ], @@ -1174,9 +1210,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz", - "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.16.4.tgz", + "integrity": "sha512-YunpoOAyGLDseanENHmbFvQSfVL5BxW3k7hhy0eN4rb3gS/ct75dVD0EXOWIqFT/nE8XYW6LP6vz6ctKRi0k9A==", "cpu": [ "x64" ], @@ -1345,9 +1381,9 @@ } }, "node_modules/@types/prop-types": { - "version": "15.7.11", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", - "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" }, "node_modules/@types/react": { "version": "17.0.74", @@ -1395,9 +1431,9 @@ } }, "node_modules/@types/scheduler": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", - "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==" }, "node_modules/@types/semver": { "version": "7.5.8", @@ -1802,9 +1838,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001599", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001599.tgz", - "integrity": "sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA==", + "version": "1.0.30001612", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001612.tgz", + "integrity": "sha512-lFgnZ07UhaCcsSZgWW0K5j4e69dK1u/ltrL9lTUiFOwNHs12S3UMIEYgBV0Z6C6hRDev7iRnMzzYmKabYdXF9g==", "funding": [ { "type": "opencollective", @@ -2179,9 +2215,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.710", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.710.tgz", - "integrity": "sha512-w+9yAVHoHhysCa+gln7AzbO9CdjFcL/wN/5dd+XW/Msl2d/4+WisEaCF1nty0xbAKaxdaJfgLB2296U7zZB7BA==" + "version": "1.4.747", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.747.tgz", + "integrity": "sha512-+FnSWZIAvFHbsNVmUxhEqWiaOiPMcfum1GQzlWCg/wLigVtshOsjXHyEFfmt6cFK6+HkS3QOJBv6/3OPumbBfw==" }, "node_modules/entities": { "version": "1.0.0", @@ -4050,9 +4086,9 @@ } }, "node_modules/property-information": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.4.1.tgz", - "integrity": "sha512-OHYtXfu5aI2sS2LWFSN5rgJjrQ4pCy8i1jubJLe2QvMF8JJ++HXTUIVWFLfXJoaOfvYYjk2SN8J2wFUWIGXT4w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -4255,9 +4291,9 @@ } }, "node_modules/react-smooth": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.0.tgz", - "integrity": "sha512-2NMXOBY1uVUQx1jBeENGA497HK20y6CPGYL1ZnJLeoQ8rrc3UfmOM82sRxtzpcoCkUMy4CS0RGylfuVhuFjBgg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.1.tgz", + "integrity": "sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==", "dependencies": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", @@ -4446,9 +4482,9 @@ } }, "node_modules/rollup": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz", - "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.16.4.tgz", + "integrity": "sha512-kuaTJSUbz+Wsb2ATGvEknkI12XV40vIiHmLuFlejoo7HtDok/O5eDDD0UpCVY5bBX5U5RYo8wWP83H7ZsqVEnA==", "dependencies": { "@types/estree": "1.0.5" }, @@ -4460,19 +4496,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.13.0", - "@rollup/rollup-android-arm64": "4.13.0", - "@rollup/rollup-darwin-arm64": "4.13.0", - "@rollup/rollup-darwin-x64": "4.13.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.13.0", - "@rollup/rollup-linux-arm64-gnu": "4.13.0", - "@rollup/rollup-linux-arm64-musl": "4.13.0", - "@rollup/rollup-linux-riscv64-gnu": "4.13.0", - "@rollup/rollup-linux-x64-gnu": "4.13.0", - "@rollup/rollup-linux-x64-musl": "4.13.0", - "@rollup/rollup-win32-arm64-msvc": "4.13.0", - "@rollup/rollup-win32-ia32-msvc": "4.13.0", - "@rollup/rollup-win32-x64-msvc": "4.13.0", + "@rollup/rollup-android-arm-eabi": "4.16.4", + "@rollup/rollup-android-arm64": "4.16.4", + "@rollup/rollup-darwin-arm64": "4.16.4", + "@rollup/rollup-darwin-x64": "4.16.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.16.4", + "@rollup/rollup-linux-arm-musleabihf": "4.16.4", + "@rollup/rollup-linux-arm64-gnu": "4.16.4", + "@rollup/rollup-linux-arm64-musl": "4.16.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.16.4", + "@rollup/rollup-linux-riscv64-gnu": "4.16.4", + "@rollup/rollup-linux-s390x-gnu": "4.16.4", + "@rollup/rollup-linux-x64-gnu": "4.16.4", + "@rollup/rollup-linux-x64-musl": "4.16.4", + "@rollup/rollup-win32-arm64-msvc": "4.16.4", + "@rollup/rollup-win32-ia32-msvc": "4.16.4", + "@rollup/rollup-win32-x64-msvc": "4.16.4", "fsevents": "~2.3.2" } }, diff --git a/horreum-web/src/domain/alerting/RecalculateModal.tsx b/horreum-web/src/domain/alerting/RecalculateModal.tsx index 1415679d3..44be28274 100644 --- a/horreum-web/src/domain/alerting/RecalculateModal.tsx +++ b/horreum-web/src/domain/alerting/RecalculateModal.tsx @@ -54,6 +54,7 @@ export default function RecalculateModal({ title, recalculate, cancel, message, const { alerting } = useContext(AppContext) as AppContextType; const [progress, setProgress] = useState(-1) const [debug, setDebug] = useState(false) + const [clearDatapoints, setClearDatapoints] = useState(true) const [timeRange, setTimeRange] = useState() const timer = useRef() const [result, setResult] = useState() @@ -85,9 +86,11 @@ export default function RecalculateModal({ title, recalculate, cancel, message, const timeRangeOptions: TimeRange[] = useMemo( () => [ { toString: () => "all" }, + { from: Date.now() - 15_811_200_000, to: undefined, toString: () => "last 6 months" }, + { from: Date.now() - 7_948_800_000, to: undefined, toString: () => "last 3 months" }, { from: Date.now() - 31 * 86_400_000, to: undefined, toString: () => "last month" }, - { from: Date.now() - 7 * 86_400_000, to: undefined, toString: () => "last week" }, - { from: Date.now() - 86_400_000, to: undefined, toString: () => "last 24 hours" }, + { from: Date.now() - 7 * 86_400_000, to: undefined, toString: () => "last week" }, + { from: Date.now() - 86_400_000, to: undefined, toString: () => "last 24 hours" }, ], [] ) @@ -105,9 +108,9 @@ export default function RecalculateModal({ title, recalculate, cancel, message, key={1} variant="primary" onClick={() => { - setProgress(0) alertingApi.recalculateDatapoints( testId, + clearDatapoints, debug, timeRange?.from, false, @@ -122,6 +125,7 @@ export default function RecalculateModal({ title, recalculate, cancel, message, alerting.dispatchError(error,"RECALCULATION", "Failed to start recalculation") } ) + setProgress(0) }} > {recalculate} @@ -143,8 +147,11 @@ export default function RecalculateModal({ title, recalculate, cancel, message, - - setDebug(val)} label="Write debug logs" /> + + setClearDatapoints(val)} /> + + + setDebug(val)}/> )}