-
Notifications
You must be signed in to change notification settings - Fork 73
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add rule validation in AnomalyDetector constructor #1341
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,6 +22,7 @@ | |
import java.time.temporal.ChronoUnit; | ||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
|
||
|
@@ -109,6 +110,7 @@ public Integer getShingleSize(Integer customShingleSize) { | |
@Deprecated | ||
public static final String DETECTION_DATE_RANGE_FIELD = "detection_date_range"; | ||
public static final String RULES_FIELD = "rules"; | ||
private static final String SUPPRESSION_RULE_ISSUE_PREFIX = "Suppression Rule Error: "; | ||
|
||
protected String detectorType; | ||
|
||
|
@@ -229,6 +231,8 @@ public AnomalyDetector( | |
issueType = ValidationIssueType.CATEGORY; | ||
} | ||
|
||
validateRules(features, rules); | ||
|
||
checkAndThrowValidationErrors(ValidationAspect.DETECTOR); | ||
|
||
this.detectorType = isHC(categoryFields) ? MULTI_ENTITY.name() : SINGLE_ENTITY.name(); | ||
|
@@ -720,4 +724,121 @@ private static Boolean onlyParseBooleanValue(XContentParser parser) throws IOExc | |
} | ||
return null; | ||
} | ||
|
||
/** | ||
* Validates each condition in the list of rules against the list of features. | ||
* Checks that: | ||
* - The feature name exists in the list of features. | ||
* - The related feature is enabled. | ||
* - The value is not NaN and is positive. | ||
* | ||
* @param features The list of available features. Must not be null. | ||
* @param rules The list of rules containing conditions to validate. Can be null. | ||
*/ | ||
private void validateRules(List<Feature> features, List<Rule> rules) { | ||
// Null check for rules | ||
if (rules == null || rules.isEmpty()) { | ||
return; // No suppression rules to validate; consider as valid | ||
} | ||
|
||
// Null check for features | ||
if (features == null || features.isEmpty()) { | ||
// Cannot proceed with validation if features are null but rules are not null | ||
this.errorMessage = SUPPRESSION_RULE_ISSUE_PREFIX + "Features are not defined while suppression rules are provided."; | ||
this.issueType = ValidationIssueType.RULE; | ||
return; | ||
} | ||
|
||
// Create a map of feature names to their enabled status for quick lookup | ||
Map<String, Boolean> featureEnabledMap = new HashMap<>(); | ||
for (Feature feature : features) { | ||
if (feature != null && feature.getName() != null) { | ||
featureEnabledMap.put(feature.getName(), feature.getEnabled()); | ||
} | ||
} | ||
|
||
// Iterate over each rule | ||
for (Rule rule : rules) { | ||
if (rule == null || rule.getConditions() == null) { | ||
// Invalid rule or conditions list is null | ||
this.errorMessage = SUPPRESSION_RULE_ISSUE_PREFIX + "A suppression rule or its conditions are not properly defined."; | ||
this.issueType = ValidationIssueType.RULE; | ||
return; | ||
} | ||
|
||
// Iterate over each condition in the rule | ||
for (Condition condition : rule.getConditions()) { | ||
if (condition == null) { | ||
// Invalid condition | ||
this.errorMessage = SUPPRESSION_RULE_ISSUE_PREFIX + "A condition within a suppression rule is not properly defined."; | ||
this.issueType = ValidationIssueType.RULE; | ||
return; | ||
} | ||
|
||
String featureName = condition.getFeatureName(); | ||
|
||
// Check if the feature name is null | ||
if (featureName == null) { | ||
// Feature name is required | ||
this.errorMessage = SUPPRESSION_RULE_ISSUE_PREFIX + "A condition is missing the feature name."; | ||
this.issueType = ValidationIssueType.RULE; | ||
return; | ||
} | ||
|
||
// Check if the feature exists | ||
if (!featureEnabledMap.containsKey(featureName)) { | ||
// Feature does not exist | ||
this.errorMessage = SUPPRESSION_RULE_ISSUE_PREFIX | ||
+ "Feature \"" | ||
+ featureName | ||
+ "\" specified in a suppression rule does not exist."; | ||
this.issueType = ValidationIssueType.RULE; | ||
return; | ||
} | ||
|
||
// Check if the feature is enabled | ||
if (!featureEnabledMap.get(featureName)) { | ||
// Feature is not enabled | ||
this.errorMessage = SUPPRESSION_RULE_ISSUE_PREFIX | ||
+ "Feature \"" | ||
+ featureName | ||
+ "\" specified in a suppression rule is not enabled."; | ||
this.issueType = ValidationIssueType.RULE; | ||
return; | ||
} | ||
|
||
// other threshold types may not have value operand | ||
ThresholdType thresholdType = condition.getThresholdType(); | ||
if (thresholdType == ThresholdType.ACTUAL_OVER_EXPECTED_MARGIN | ||
|| thresholdType == ThresholdType.EXPECTED_OVER_ACTUAL_MARGIN | ||
|| thresholdType == ThresholdType.ACTUAL_OVER_EXPECTED_RATIO | ||
|| thresholdType == ThresholdType.EXPECTED_OVER_ACTUAL_RATIO) { | ||
// Check if the value is not NaN | ||
double value = condition.getValue(); | ||
if (Double.isNaN(value)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. btw I will be changing value section to be okay with Null if threshold type is ACTUAL_IS_OVER_EXPECTED or EXPECTED_IS_OVER_ACTUAL. Will make sure those aren't passed to RCF and seen as not a rule in that case later on too There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks. |
||
// Value is NaN | ||
this.errorMessage = SUPPRESSION_RULE_ISSUE_PREFIX | ||
+ "The threshold value for feature \"" | ||
+ featureName | ||
+ "\" is not a valid number."; | ||
this.issueType = ValidationIssueType.RULE; | ||
return; | ||
} | ||
|
||
// Check if the value is positive | ||
if (value <= 0) { | ||
// Value is not positive | ||
this.errorMessage = SUPPRESSION_RULE_ISSUE_PREFIX | ||
+ "The threshold value for feature \"" | ||
+ featureName | ||
+ "\" must be a positive number."; | ||
this.issueType = ValidationIssueType.RULE; | ||
return; | ||
} | ||
} | ||
} | ||
} | ||
|
||
// All checks passed | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,9 +11,6 @@ | |
|
||
package org.opensearch.timeseries.ml; | ||
|
||
import java.util.HashMap; | ||
import java.util.Map; | ||
import java.util.Optional; | ||
import java.util.concurrent.ConcurrentHashMap; | ||
|
||
import org.opensearch.timeseries.MemoryTracker; | ||
|
@@ -55,48 +52,4 @@ public ModelState<RCFModelType> put(String key, ModelState<RCFModelType> value) | |
} | ||
return previousAssociatedState; | ||
} | ||
|
||
/** | ||
* Gets all of a config's model sizes hosted on a node | ||
* | ||
* @param configId config Id | ||
* @return a map of model id to its memory size | ||
*/ | ||
public Map<String, Long> getModelSize(String configId) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. where these just never used anymore? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, they are not used after recent refactoring. |
||
Map<String, Long> res = new HashMap<>(); | ||
super.entrySet() | ||
.stream() | ||
.filter(entry -> SingleStreamModelIdMapper.getConfigIdForModelId(entry.getKey()).equals(configId)) | ||
.forEach(entry -> { | ||
Optional<RCFModelType> modelOptional = entry.getValue().getModel(); | ||
if (modelOptional.isPresent()) { | ||
res.put(entry.getKey(), memoryTracker.estimateTRCFModelSize(modelOptional.get())); | ||
} | ||
}); | ||
return res; | ||
} | ||
|
||
/** | ||
* Checks if a model exists for the given config. | ||
* @param configId Config Id | ||
* @return `true` if the model exists, `false` otherwise. | ||
*/ | ||
public boolean doesModelExist(String configId) { | ||
return super.entrySet() | ||
.stream() | ||
.filter(entry -> SingleStreamModelIdMapper.getConfigIdForModelId(entry.getKey()).equals(configId)) | ||
.anyMatch(n -> true); | ||
} | ||
|
||
public boolean hostIfPossible(String modelId, ModelState<RCFModelType> toUpdate) { | ||
return Optional | ||
.ofNullable(toUpdate) | ||
.filter(state -> state.getModel().isPresent()) | ||
.filter(state -> memoryTracker.isHostingAllowed(modelId, state.getModel().get())) | ||
.map(state -> { | ||
super.put(modelId, toUpdate); | ||
return true; | ||
}) | ||
.orElse(false); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: could add
|| features.isEmpty()
same we do for rules aboveThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, fixed.