From 37b22b19693c162ceaec30b871bfad12ae167a3c Mon Sep 17 00:00:00 2001 From: Ed Savage Date: Thu, 21 Jan 2021 09:14:29 +0000 Subject: [PATCH] [ML] Parse JSON format config updates (#1682) * Expect configuration update requests to be received in JSON formatted strings. * Remove support for old, ini file parsing. Relates elastic/elasticsearch#67721, #1253 --- include/api/CAnomalyJobConfig.h | 16 ++- include/api/CConfigUpdater.h | 12 +- include/model/CAnomalyDetectorModelConfig.h | 6 + lib/api/CAnomalyJobConfig.cc | 137 ++++++-------------- lib/api/CConfigUpdater.cc | 95 ++++++-------- lib/api/unittest/CConfigUpdaterTest.cc | 38 +++--- lib/model/CAnomalyDetectorModelConfig.cc | 10 +- 7 files changed, 127 insertions(+), 187 deletions(-) diff --git a/include/api/CAnomalyJobConfig.h b/include/api/CAnomalyJobConfig.h index 6e1c9fec08..1514955745 100644 --- a/include/api/CAnomalyJobConfig.h +++ b/include/api/CAnomalyJobConfig.h @@ -39,6 +39,8 @@ class API_EXPORT CAnomalyJobConfig { public: class API_EXPORT CDetectorConfig { public: + static const std::string DETECTOR_RULES; + static const std::string FUNCTION; static const std::string FIELD_NAME; static const std::string BY_FIELD_NAME; @@ -287,6 +289,10 @@ class API_EXPORT CAnomalyJobConfig { bool parseRules(int detectorIndex, const std::string& rules); + bool parseRules(int detectorIndex, const rapidjson::Value& rules); + + bool parseRulesUpdate(const rapidjson::Value& rulesUpdateConfig); + private: // Convenience method intended for use by the unit tests only void addDetector(const std::string& functionName, @@ -302,13 +308,8 @@ class API_EXPORT CAnomalyJobConfig { overFieldName, partitionFieldName); } - bool processFilter(const std::string& key, const std::string& value); - - //! Process and store a scheduled event - bool processScheduledEvent(const boost::property_tree::ptree& propTree, - const std::string& key, - const std::string& value, - TIntSet& handledScheduledEvents); + bool parseRules(CDetectionRulesJsonParser::TDetectionRuleVec& detectionRules, + const rapidjson::Value& rules); bool parseRules(CDetectionRulesJsonParser::TDetectionRuleVec& detectionRules, const std::string& rules); @@ -523,6 +524,7 @@ class API_EXPORT CAnomalyJobConfig { return m_DataDescription; } const CModelPlotConfig& modelPlotConfig() const { return m_ModelConfig; } + CModelPlotConfig& modelPlotConfig() { return m_ModelConfig; } const CAnalysisLimits& analysisLimits() const { return m_AnalysisLimits; } bool isInitialized() const { return m_IsInitialized; } core_t::TTime persistInterval() const { diff --git a/include/api/CConfigUpdater.h b/include/api/CConfigUpdater.h index 6d61db0d5b..d22d13a6e8 100644 --- a/include/api/CConfigUpdater.h +++ b/include/api/CConfigUpdater.h @@ -24,13 +24,13 @@ namespace api { //! update, a control message is being sent with the requested //! configuration changes. This class is responsible for parsing //! text with the requested configuration changes and apply them. -//! The changes are expected in an ini type of syntax. +//! The changes are expected in a JSON document. //! //! IMPLEMENTATION DECISIONS:\n //! As long as the parsing of the configuration changes is //! done successfully, the updater tries to apply as many //! changes as possible even if it fails on a particular -//! change (e.g. unknown stanza name). +//! change. //! class API_EXPORT CConfigUpdater { public: @@ -41,14 +41,6 @@ class API_EXPORT CConfigUpdater { //! \param config the requested changes in an ini syntax bool update(const std::string& config); -private: - static const std::string MODEL_DEBUG_CONFIG; - static const std::string DETECTOR_RULES; - static const std::string DETECTOR_INDEX; - static const std::string RULES_JSON; - static const std::string FILTERS; - static const std::string SCHEDULED_EVENTS; - private: CAnomalyJobConfig& m_JobConfig; model::CAnomalyDetectorModelConfig& m_ModelConfig; diff --git a/include/model/CAnomalyDetectorModelConfig.h b/include/model/CAnomalyDetectorModelConfig.h index bc07e97f77..74daa98ee6 100644 --- a/include/model/CAnomalyDetectorModelConfig.h +++ b/include/model/CAnomalyDetectorModelConfig.h @@ -379,6 +379,9 @@ class MODEL_EXPORT CAnomalyDetectorModelConfig { //! Get the central confidence interval for the model debug plot. double modelPlotBoundsPercentile() const; + //! Is model plot enabled? + bool modelPlotEnabled() const; + //! Are annotations enabled for each of the models? bool modelPlotAnnotationsEnabled() const; @@ -451,6 +454,9 @@ class MODEL_EXPORT CAnomalyDetectorModelConfig { //! A cache of customized factories requested from this config. mutable TSearchKeyFactoryCPtrMap m_FactoryCache; + //! Is model plot enabled? + bool m_ModelPlotEnabled{false}; + //! Are annotations enabled for each of the models? bool m_ModelPlotAnnotationsEnabled{false}; diff --git a/lib/api/CAnomalyJobConfig.cc b/lib/api/CAnomalyJobConfig.cc index 223e62c088..dc55f57da6 100644 --- a/lib/api/CAnomalyJobConfig.cc +++ b/lib/api/CAnomalyJobConfig.cc @@ -75,6 +75,8 @@ const std::string CAnomalyJobConfig::CAnalysisConfig::SCHEDULED_EVENT_PREFIX("sc const std::string CAnomalyJobConfig::CAnalysisConfig::DESCRIPTION_SUFFIX(".description"); const std::string CAnomalyJobConfig::CAnalysisConfig::RULES_SUFFIX(".rules"); +const std::string CAnomalyJobConfig::CAnalysisConfig::CDetectorConfig::DETECTOR_RULES{"detector_rules"}; + const std::string CAnomalyJobConfig::CAnalysisConfig::CDetectorConfig::FUNCTION{"function"}; const std::string CAnomalyJobConfig::CAnalysisConfig::CDetectorConfig::FIELD_NAME{"field_name"}; const std::string CAnomalyJobConfig::CAnalysisConfig::CDetectorConfig::BY_FIELD_NAME{"by_field_name"}; @@ -316,6 +318,15 @@ const CAnomalyJobConfigReader DETECTOR_CONFIG_READER{[] { return theReader; }()}; +const CAnomalyJobConfigReader CUSTOM_RULES_UPDATE_CONFIG_READER{[] { + CAnomalyJobConfigReader theReader; + theReader.addParameter(CAnomalyJobConfig::CAnalysisConfig::CDetectorConfig::DETECTOR_INDEX, + CAnomalyJobConfigReader::E_RequiredParameter); + theReader.addParameter(CAnomalyJobConfig::CAnalysisConfig::CDetectorConfig::CUSTOM_RULES, + CAnomalyJobConfigReader::E_RequiredParameter); + return theReader; +}()}; + const CAnomalyJobConfigReader PPC_CONFIG_READER{[] { CAnomalyJobConfigReader theReader; theReader.addParameter(CAnomalyJobConfig::CAnalysisConfig::ENABLED, @@ -431,6 +442,8 @@ bool CAnomalyJobConfig::parseEventConfig(const std::string& json) { return false; } + m_ScheduledEvents.clear(); + if (doc.ObjectEmpty()) { return true; } @@ -443,6 +456,7 @@ bool CAnomalyJobConfig::parseEventConfig(const std::string& json) { const rapidjson::Value& value = doc[EVENTS]; + m_Events.clear(); m_Events.resize(value.Size()); for (unsigned int i = 0; i < value.Size(); ++i) { if (value[i].IsObject() == false) { @@ -488,9 +502,10 @@ void CAnomalyJobConfig::CEventConfig::parse(const rapidjson::Value& filterConfig scheduledEvents.emplace_back(m_Description, m_DetectionRules[0]); } -bool CAnomalyJobConfig::parseFilterConfig(const std::string& json) { +bool CAnomalyJobConfig::parseFilterConfig(const std::string& jsonString) { + rapidjson::Document doc; - if (doc.Parse<0>(json).HasParseError()) { + if (doc.Parse<0>(jsonString).HasParseError()) { LOG_ERROR(<< "An error occurred while parsing filter config from JSON: " << doc.GetParseError()); return false; @@ -502,7 +517,8 @@ bool CAnomalyJobConfig::parseFilterConfig(const std::string& json) { try { if (doc.HasMember(FILTERS) == false || doc[FILTERS].IsArray() == false) { - LOG_ERROR(<< "Missing expected array field '" << FILTERS << "'. JSON: " << json); + LOG_ERROR(<< "Missing expected array field '" << FILTERS + << "'. JSON: " << jsonString); return false; } @@ -514,7 +530,6 @@ bool CAnomalyJobConfig::parseFilterConfig(const std::string& json) { << toString(value[i])); return false; } - m_Filters[i].parse(value[i], m_RuleFilters); } } catch (CAnomalyJobConfigReader::CParseError& e) { @@ -717,114 +732,47 @@ void CAnomalyJobConfig::CAnalysisConfig::parse(const rapidjson::Value& analysisC m_MultivariateByFields = parameters[MULTIVARIATE_BY_FIELDS].fallback(false); } -// TODO: Process updates as JSON -bool CAnomalyJobConfig::CAnalysisConfig::updateFilters(const boost::property_tree::ptree& propTree) { - for (const auto& filterEntry : propTree) { - const std::string& key = filterEntry.first; - const std::string& value = filterEntry.second.data(); - if (this->processFilter(key, value) == false) { - return false; +bool CAnomalyJobConfig::CAnalysisConfig::parseRulesUpdate(const rapidjson::Value& rulesUpdateConfig) { + try { + auto parameters = CUSTOM_RULES_UPDATE_CONFIG_READER.read(rulesUpdateConfig); + int detectorIndex = parameters[CDetectorConfig::DETECTOR_INDEX].as(); + auto customRules = parameters[CDetectorConfig::CUSTOM_RULES].jsonObject(); + if (customRules != nullptr) { + m_DetectorRules[detectorIndex].clear(); + if (this->parseRules(detectorIndex, *customRules) == false) { + LOG_ERROR(<< "Failed to update detector rules for detector: " << detectorIndex); + return false; + } } - } - return true; -} - -// TODO: Process updates as JSON -bool CAnomalyJobConfig::CAnalysisConfig::processFilter(const std::string& key, - const std::string& value) { - // expected format is filter.=[json, array] - std::size_t sepPos{key.find(SUFFIX_SEPARATOR)}; - if (sepPos == std::string::npos) { - LOG_ERROR(<< "Unrecognised filter key: " + key); + } catch (CAnomalyJobConfigReader::CParseError& e) { + LOG_ERROR(<< "Error parsing events config: " << e.what()); return false; } - std::string filterId = key.substr(sepPos + 1); - core::CPatternSet& filter = m_RuleFilters[filterId]; - return filter.initFromJson(value); -} - -// TODO: Process updates as JSON -bool CAnomalyJobConfig::CAnalysisConfig::updateScheduledEvents(const boost::property_tree::ptree& propTree) { - m_ScheduledEvents.clear(); - - bool isClear = propTree.get(CLEAR, false); - if (isClear) { - return true; - } - - TIntSet handledScheduledEvents; - for (const auto& scheduledEventEntry : propTree) { - const std::string& key = scheduledEventEntry.first; - const std::string& value = scheduledEventEntry.second.data(); - if (this->processScheduledEvent(propTree, key, value, handledScheduledEvents) == false) { - return false; - } - } return true; } -// TODO: Process updates as JSON -bool CAnomalyJobConfig::CAnalysisConfig::processScheduledEvent( - const boost::property_tree::ptree& propTree, - const std::string& key, - const std::string& value, - TIntSet& handledScheduledEvents) { - // Here we pull out the "1" in "scheduledevent.1.description" - // description may contain a '.' - std::size_t sepPos{key.find(SUFFIX_SEPARATOR, SCHEDULED_EVENT_PREFIX.length() + 1)}; - if (sepPos == std::string::npos || sepPos == key.length() - 1) { - LOG_ERROR(<< "Unrecognised configuration option " << key << " = " << value); - return false; - } - - std::string indexString{key, SCHEDULED_EVENT_PREFIX.length(), - sepPos - SCHEDULED_EVENT_PREFIX.length()}; - int indexKey; - if (core::CStringUtils::stringToType(indexString, indexKey) == false) { - LOG_ERROR(<< "Cannot convert config key to integer: " << indexString); - return false; - } - - // Check if we've already seen this key - if (handledScheduledEvents.insert(indexKey).second == false) { - // Not an error - return true; - } - - std::string description{propTree.get( - boost::property_tree::ptree::path_type{ - SCHEDULED_EVENT_PREFIX + indexString + DESCRIPTION_SUFFIX, '\t'}, - EMPTY_STRING)}; - - std::string rules{propTree.get( - boost::property_tree::ptree::path_type{ - SCHEDULED_EVENT_PREFIX + indexString + RULES_SUFFIX, '\t'}, - EMPTY_STRING)}; - - CDetectionRulesJsonParser::TDetectionRuleVec detectionRules; - if (this->parseRules(detectionRules, rules) == false) { - // parseRules() will have logged the error - return false; - } +bool CAnomalyJobConfig::CAnalysisConfig::parseRules(int detectorIndex, + const rapidjson::Value& rules) { + return parseRules(m_DetectorRules[detectorIndex], rules); +} - if (detectionRules.size() != 1) { - LOG_ERROR(<< "Scheduled events must have exactly 1 rule"); +bool CAnomalyJobConfig::CAnalysisConfig::parseRules(CDetectionRulesJsonParser::TDetectionRuleVec& detectionRules, + const rapidjson::Value& rules) { + CDetectionRulesJsonParser rulesParser{m_RuleFilters}; + std::string errorString; + if (rulesParser.parseRules(rules, detectionRules, errorString) == false) { + LOG_ERROR(<< "Error parsing detector rules: " << errorString); return false; } - - m_ScheduledEvents.emplace_back(description, detectionRules[0]); - return true; } -// TODO: Process updates as JSON bool CAnomalyJobConfig::CAnalysisConfig::parseRules(int detectorIndex, const std::string& rules) { return parseRules(m_DetectorRules[detectorIndex], rules); } -// TODO: Process updates as JSON bool CAnomalyJobConfig::CAnalysisConfig::parseRules(CDetectionRulesJsonParser::TDetectionRuleVec& detectionRules, const std::string& rules) { if (rules.empty()) { @@ -921,7 +869,6 @@ bool CAnomalyJobConfig::CAnalysisConfig::CDetectorConfig::determineFunction(bool // Some functions must take a field, some mustn't and for the rest it's // optional. Validate this based on the contents of these flags after // determining the function. Similarly for by fields. - // TODO: Check how much validation is required here (if any) if parsing JSON job config. bool fieldRequired{false}; bool fieldInvalid{false}; bool byFieldRequired{false}; diff --git a/lib/api/CConfigUpdater.cc b/lib/api/CConfigUpdater.cc index 21648042ba..1e81036b3a 100644 --- a/lib/api/CConfigUpdater.cc +++ b/lib/api/CConfigUpdater.cc @@ -7,75 +7,66 @@ #include -#include -#include +#include +#include +#include namespace ml { namespace api { -const std::string CConfigUpdater::MODEL_DEBUG_CONFIG("modelPlotConfig"); -const std::string CConfigUpdater::DETECTOR_RULES("detectorRules"); -const std::string CConfigUpdater::DETECTOR_INDEX("detectorIndex"); -const std::string CConfigUpdater::RULES_JSON("rulesJson"); -const std::string CConfigUpdater::FILTERS("filters"); -const std::string CConfigUpdater::SCHEDULED_EVENTS("scheduledEvents"); - CConfigUpdater::CConfigUpdater(CAnomalyJobConfig& jobConfig, model::CAnomalyDetectorModelConfig& modelConfig) : m_JobConfig(jobConfig), m_ModelConfig(modelConfig) { } -bool CConfigUpdater::update(const std::string& config) { - boost::property_tree::ptree propTree; +bool CConfigUpdater::update(const std::string& json) { + rapidjson::Document doc; + if (doc.Parse<0>(json.c_str()).HasParseError()) { + LOG_ERROR(<< "An error occurred while parsing pattern set from JSON: " + << rapidjson::GetParseError_En(doc.GetParseError())); + return false; + } - try { - std::istringstream strm(config); - boost::property_tree::ini_parser::read_ini(strm, propTree); - } catch (boost::property_tree::ptree_error& e) { - LOG_ERROR(<< "Error parsing config from '" << config << "' : " << e.what()); + if (doc.IsObject() == false) { + LOG_ERROR(<< "Input error: expected JSON object but input was '" << json + << "'. Please report this problem."); return false; } - for (boost::property_tree::ptree::const_iterator stanzaItr = propTree.begin(); - stanzaItr != propTree.end(); ++stanzaItr) { - const std::string& stanzaName = stanzaItr->first; - const boost::property_tree::ptree& subTree = stanzaItr->second; + if (doc.HasMember(CAnomalyJobConfig::MODEL_PLOT_CONFIG)) { + if (doc[CAnomalyJobConfig::MODEL_PLOT_CONFIG].IsObject() == false) { + LOG_ERROR(<< "Input error: expected " << CAnomalyJobConfig::MODEL_PLOT_CONFIG + << " to be JSON object but input was '" << json + << "'. Please report this problem."); + return false; + } + const rapidjson::Value& value = doc[CAnomalyJobConfig::MODEL_PLOT_CONFIG]; - if (stanzaName == MODEL_DEBUG_CONFIG) { - if (m_ModelConfig.configureModelPlot(subTree) == false) { - LOG_ERROR(<< "Could not parse modelPlotConfig"); - return false; - } - } else if (stanzaName == DETECTOR_RULES) { - std::string detectorIndexString = subTree.get(DETECTOR_INDEX, std::string()); - int detectorIndex; - if (core::CStringUtils::stringToType(detectorIndexString, detectorIndex) == false) { - LOG_ERROR(<< "Invalid detector index: " << detectorIndexString); - return false; - } - std::string rulesJson = subTree.get(RULES_JSON, std::string()); - if (m_JobConfig.analysisConfig().parseRules(detectorIndex, rulesJson) == false) { - LOG_ERROR(<< "Failed to update detector rules for detector: " << detectorIndex); - return false; - } - } else if (stanzaName == FILTERS) { - // TODO: Move to JSON format for config updates. - if (m_JobConfig.analysisConfig().updateFilters(subTree) == false) { - LOG_ERROR(<< "Failed to update filters"); - return false; - } - } else if (stanzaName == SCHEDULED_EVENTS) { - // TODO: Move to JSON format for config updates. - if (m_JobConfig.analysisConfig().updateScheduledEvents(subTree) == false) { - LOG_ERROR(<< "Failed to update scheduled events"); - return false; - } - } else { - LOG_WARN(<< "Ignoring unknown update config stanza: " << stanzaName); + m_JobConfig.modelPlotConfig().parse(value); + const ml::api::CAnomalyJobConfig::CModelPlotConfig& modelPlotConfig = + m_JobConfig.modelPlotConfig(); + m_ModelConfig.configureModelPlot(modelPlotConfig.enabled(), + modelPlotConfig.annotationsEnabled(), + modelPlotConfig.terms()); + } else if (doc.HasMember(CAnomalyJobConfig::FILTERS)) { + if (m_JobConfig.parseFilterConfig(json) == false) { + LOG_ERROR(<< "Failed to parse filter config update: " << json); return false; } + m_JobConfig.initRuleFilters(); + } else if (doc.HasMember(CAnomalyJobConfig::EVENTS)) { + if (m_JobConfig.parseEventConfig(json) == false) { + LOG_ERROR(<< "Failed to parse events config update: " << json); + return false; + } + m_JobConfig.initScheduledEvents(); + } else if (doc.HasMember(CAnomalyJobConfig::CAnalysisConfig::CDetectorConfig::DETECTOR_RULES)) { + return m_JobConfig.analysisConfig().parseRulesUpdate( + doc[CAnomalyJobConfig::CAnalysisConfig::CDetectorConfig::DETECTOR_RULES]); + } else { + LOG_ERROR(<< "Unexpected JSON update message: " << json); + return false; } - return true; } } diff --git a/lib/api/unittest/CConfigUpdaterTest.cc b/lib/api/unittest/CConfigUpdaterTest.cc index c70a902e29..ea886c627e 100644 --- a/lib/api/unittest/CConfigUpdaterTest.cc +++ b/lib/api/unittest/CConfigUpdaterTest.cc @@ -49,12 +49,13 @@ BOOST_AUTO_TEST_CASE(testUpdateGivenModelPlotConfig) { terms.insert(std::string("b")); modelConfig.modelPlotTerms(terms); - std::string configUpdate("[modelPlotConfig]\nboundspercentile = 83.5\nterms = c,d\nannotations_enabled = false\n"); + std::string configUpdate{"{\"model_plot_config\":{\"enabled\":true,\"annotations_enabled\":true,\"terms\":\"c,d\"}}"}; CConfigUpdater configUpdater(jobConfig, modelConfig); BOOST_TEST_REQUIRE(configUpdater.update(configUpdate)); - BOOST_REQUIRE_EQUAL(83.5, modelConfig.modelPlotBoundsPercentile()); + BOOST_REQUIRE_EQUAL(true, modelConfig.modelPlotEnabled()); + BOOST_REQUIRE_EQUAL(true, modelConfig.modelPlotAnnotationsEnabled()); terms = modelConfig.modelPlotTerms(); BOOST_REQUIRE_EQUAL(std::size_t(2), terms.size()); @@ -74,16 +75,15 @@ BOOST_AUTO_TEST_CASE(testUpdateGivenDetectorRules) { model::CAnomalyDetectorModelConfig modelConfig = model::CAnomalyDetectorModelConfig::defaultConfig(); - std::string configUpdate0("[detectorRules]\ndetectorIndex = 0\nrulesJson = []\n"); - std::string configUpdate1("[detectorRules]\ndetectorIndex = 1\nrulesJson = " - "[{\"actions\":[\"skip_result\"],\"conditions\":[{\"applies_to\":\"typical\"," - "\"operator\":\"lt\",\"value\": 15.0}]}]"); + std::string configUpdate0{"{\"detector_rules\": {\"detector_index\":0, \"custom_rules\":[]}}"}; + std::string configUpdate1{"{\"detector_rules\": {\"detector_index\":1, \"custom_rules\":[{\"actions\":[\"skip_result\"], \"conditions\":[{\"applies_to\":\"typical\",\"operator\":\"lt\",\"value\": 15.0}]}]}}"}; CConfigUpdater configUpdater(jobConfig, modelConfig); BOOST_TEST_REQUIRE(configUpdater.update(configUpdate0)); BOOST_TEST_REQUIRE(configUpdater.update(configUpdate1)); + BOOST_REQUIRE_EQUAL(2, jobConfig.analysisConfig().detectionRules().size()); CAnomalyJobConfig::CAnalysisConfig::TIntDetectionRuleVecUMap::const_iterator itr = jobConfig.analysisConfig().detectionRules().find(0); BOOST_TEST_REQUIRE(itr->second.empty()); @@ -140,7 +140,9 @@ BOOST_AUTO_TEST_CASE(testUpdateGivenFilters) { BOOST_TEST_REQUIRE(ruleFilters["filter_2"].contains("ddd")); // Update existing ones - std::string configUpdate("[filters]\nfilter.filter_1=[\"ccc\",\"ddd\"]\nfilter.filter_2=[\"aaa\",\"bbb\"]\n"); + std::string configUpdateOld("[filters]\nfilter.filter_1=[\"ccc\",\"ddd\"]\nfilter.filter_2=[\"aaa\",\"bbb\"]\n"); + + std::string configUpdate{"{\"filters\": [{\"filter_id\": \"filter_1\", \"items\":[\"ccc\", \"ddd\"]}, {\"filter_id\": \"filter_2\", \"items\":[\"aaa\", \"bbb\"]}]}"}; CConfigUpdater configUpdater(jobConfig, modelConfig); @@ -160,7 +162,8 @@ BOOST_AUTO_TEST_CASE(testUpdateGivenFilters) { BOOST_TEST_REQUIRE(ruleFilters["filter_2"].contains("ddd") == false); // Now update by adding a new filter - configUpdate = "[filters]\nfilter.filter_3=[\"new\"]\n"; + configUpdateOld = "[filters]\nfilter.filter_3=[\"new\"]\n"; + configUpdate = "{\"filters\": [{\"filter_id\":\"filter_3\",\"items\":[\"new\"]}]}"; BOOST_TEST_REQUIRE(configUpdater.update(configUpdate)); ruleFilters = jobConfig.analysisConfig().ruleFilters(); @@ -207,17 +210,11 @@ BOOST_AUTO_TEST_CASE(testUpdateGivenScheduledEvents) { "\"conditions\":[{\"applies_to\":\"time\",\"operator\":\"gte\",\"value\": 3.0}," "{\"applies_to\":\"time\",\"operator\":\"lt\",\"value\": 4.0}]}]"; - std::stringstream configUpdate; - configUpdate << "[scheduledEvents]" - << "\n"; - configUpdate << "scheduledevent.0.description = new_event_1" - << "\n"; - configUpdate << "scheduledevent.0.rules = " << validRule2 << "\n"; - configUpdate << "scheduledevent.1.description = new_event_2" - << "\n"; - configUpdate << "scheduledevent.1.rules = " << validRule1 << "\n"; + std::string configUpdate{ + "{\"events\": [{\"description\":\"new_event_1\", \"rules\": " + validRule2 + + "}, {\"description\":\"new_event_2\", \"rules\": " + validRule1 + " }] }"}; - BOOST_TEST_REQUIRE(configUpdater.update(configUpdate.str())); + BOOST_TEST_REQUIRE(configUpdater.update(configUpdate)); const auto& events = jobConfig.analysisConfig().scheduledEvents(); BOOST_REQUIRE_EQUAL(std::size_t(2), events.size()); @@ -232,10 +229,7 @@ BOOST_AUTO_TEST_CASE(testUpdateGivenScheduledEvents) { // Now test an update that clears the events { std::stringstream configUpdate; - configUpdate << "[scheduledEvents]" - << "\n"; - configUpdate << "clear = true" - << "\n"; + configUpdate << "{\"events\":[]}"; BOOST_TEST_REQUIRE(configUpdater.update(configUpdate.str())); diff --git a/lib/model/CAnomalyDetectorModelConfig.cc b/lib/model/CAnomalyDetectorModelConfig.cc index 500d454a06..7c12879c3d 100644 --- a/lib/model/CAnomalyDetectorModelConfig.cc +++ b/lib/model/CAnomalyDetectorModelConfig.cc @@ -375,7 +375,9 @@ bool CAnomalyDetectorModelConfig::init(const boost::property_tree::ptree& propTr void CAnomalyDetectorModelConfig::configureModelPlot(bool modelPlotEnabled, bool annotationsEnabled, const std::string& terms) { - if (modelPlotEnabled) { + m_ModelPlotEnabled = modelPlotEnabled; + + if (m_ModelPlotEnabled) { m_ModelPlotBoundsPercentile = maths::CModel::DEFAULT_BOUNDS_PERCENTILE; } @@ -390,6 +392,8 @@ void CAnomalyDetectorModelConfig::configureModelPlot(bool modelPlotEnabled, if (remainder.empty() == false) { tokens.push_back(remainder); } + + m_ModelPlotTerms.clear(); for (const auto& token : tokens) { m_ModelPlotTerms.insert(token); } @@ -481,6 +485,10 @@ bool CAnomalyDetectorModelConfig::configureModelPlot(const boost::property_tree: return true; } +bool CAnomalyDetectorModelConfig::modelPlotEnabled() const { + return m_ModelPlotEnabled; +} + bool CAnomalyDetectorModelConfig::modelPlotAnnotationsEnabled() const { return m_ModelPlotAnnotationsEnabled; }