| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | #include "ml_config.h" |
| 4 | |
| 5 | static void ml_config_migrate() { |
| 6 | const char *config_section_ml = CONFIG_SECTION_ML; |
| 7 | |
| 8 | // Check if migration is needed by looking for old keys |
| 9 | bool has_old_keys = false; |
| 10 | if (inicfg_exists(&netdata_config, config_section_ml, "maximum num samples to train") || |
| 11 | inicfg_exists(&netdata_config, config_section_ml, "minimum num samples to train") || |
| 12 | inicfg_exists(&netdata_config, config_section_ml, "num samples to diff") || |
| 13 | inicfg_exists(&netdata_config, config_section_ml, "num samples to smooth") || |
| 14 | inicfg_exists(&netdata_config, config_section_ml, "num samples to lag") || |
| 15 | inicfg_exists(&netdata_config, config_section_ml, "random sampling ratio")) { |
| 16 | has_old_keys = true; |
| 17 | } |
| 18 | |
| 19 | // Check if new keys already exist (user manually migrated) |
| 20 | bool has_new_keys = false; |
| 21 | if (inicfg_exists(&netdata_config, config_section_ml, "training window") || |
| 22 | inicfg_exists(&netdata_config, config_section_ml, "max training vectors")) { |
| 23 | has_new_keys = true; |
| 24 | } |
| 25 | |
| 26 | // Only migrate if we have old keys but no new keys |
| 27 | if (!has_old_keys || has_new_keys) { |
| 28 | return; |
| 29 | } |
| 30 | |
| 31 | // Get the user's "high resolution" setting |
| 32 | // This is what their configuration was designed for |
| 33 | time_t global_update_every = nd_profile.update_every; |
| 34 | |
| 35 | // Read all old configuration values with defaults |
| 36 | // Users may have changed only some values, so we need proper defaults |
| 37 | unsigned old_max_train_samples = inicfg_get_number(&netdata_config, config_section_ml, |
| 38 | "maximum num samples to train", 21600); |
| 39 | unsigned old_min_train_samples = inicfg_get_number(&netdata_config, config_section_ml, |
| 40 | "minimum num samples to train", 900); |
| 41 | unsigned old_train_every = inicfg_get_duration_seconds(&netdata_config, config_section_ml, |
| 42 | "train every", 10800); |
| 43 | unsigned old_diff_n = inicfg_get_number(&netdata_config, config_section_ml, |
| 44 | "num samples to diff", 1); |
| 45 | unsigned old_smooth_n = inicfg_get_number(&netdata_config, config_section_ml, |
| 46 | "num samples to smooth", 3); |
| 47 | unsigned old_lag_n = inicfg_get_number(&netdata_config, config_section_ml, |
| 48 | "num samples to lag", 5); |
| 49 | double old_sampling_ratio = inicfg_get_double(&netdata_config, config_section_ml, |
| 50 | "random sampling ratio", 0.2); |
| 51 | |
| 52 | // Calculate time-based equivalents |
| 53 | // These preserve the exact behavior the user had configured |
| 54 | time_t training_window = old_max_train_samples * global_update_every; |
| 55 | time_t min_training_window = old_min_train_samples * global_update_every; |
| 56 | |
| 57 | // Calculate target training vectors based on old pipeline |
| 58 | // Account for data reduction from diff, smooth, and sampling |
| 59 | size_t effective_samples = old_max_train_samples; |
| 60 | if (old_diff_n > 0) effective_samples--; // Lose one sample to differencing |
| 61 | size_t max_training_vectors = (size_t)(effective_samples * old_sampling_ratio); |
| 62 | |
| 63 | // Write new configuration values |
| 64 | char window_str[32]; |
| 65 | snprintf(window_str, sizeof(window_str), "%ldh", training_window / 3600); |
| 66 | inicfg_set(&netdata_config, config_section_ml, "training window", window_str); |
| 67 | |
| 68 | snprintf(window_str, sizeof(window_str), "%ldm", min_training_window / 60); |
| 69 | inicfg_set(&netdata_config, config_section_ml, "min training window", window_str); |
| 70 | |
| 71 | inicfg_set_number(&netdata_config, config_section_ml, "max training vectors", max_training_vectors); |
| 72 | inicfg_set_number(&netdata_config, config_section_ml, "max samples to smooth", old_smooth_n); |
| 73 | |
| 74 | // Migrate unchanged values |
| 75 | inicfg_set_duration_seconds(&netdata_config, config_section_ml, "train every", old_train_every); |
| 76 | inicfg_set_number(&netdata_config, config_section_ml, "num samples to diff", old_diff_n); |
| 77 | inicfg_set_number(&netdata_config, config_section_ml, "num samples to lag", old_lag_n); |
| 78 | |
| 79 | // Mark old keys as migrated by moving them to avoid showing in netdata.conf |
| 80 | // This uses Netdata's config migration pattern |
| 81 | inicfg_move(&netdata_config, config_section_ml, "maximum num samples to train", |
| 82 | config_section_ml, "obsolete maximum num samples to train"); |
| 83 | inicfg_move(&netdata_config, config_section_ml, "minimum num samples to train", |
| 84 | config_section_ml, "obsolete minimum num samples to train"); |
| 85 | inicfg_move(&netdata_config, config_section_ml, "num samples to smooth", |
| 86 | config_section_ml, "obsolete num samples to smooth"); |
| 87 | inicfg_move(&netdata_config, config_section_ml, "random sampling ratio", |
| 88 | config_section_ml, "obsolete random sampling ratio"); |
| 89 | |
| 90 | // Log the migration |
| 91 | nd_log(NDLS_DAEMON, NDLP_NOTICE, |
| 92 | "ML configuration migrated from sample-based to time-based:"); |
| 93 | nd_log(NDLS_DAEMON, NDLP_NOTICE, |
| 94 | " Training window: %ld seconds (%ld hours) - was %u samples at %ld second intervals", |
| 95 | training_window, training_window / 3600, old_max_train_samples, global_update_every); |
| 96 | nd_log(NDLS_DAEMON, NDLP_NOTICE, |
| 97 | " Target training vectors: %zu - calculated from smoothing and sampling", |
| 98 | max_training_vectors); |
| 99 | } |
| 100 | |
| 101 | /* |
| 102 | * Global configuration instance to be shared between training and |
| 103 | * prediction threads. |
| 104 | */ |
| 105 | ml_config_t Cfg; |
| 106 | |
| 107 | template <typename T> |
| 108 | static T clamp(const T& Value, const T& Min, const T& Max) { |
| 109 | return std::max(Min, std::min(Value, Max)); |
| 110 | } |
| 111 | |
| 112 | /* |
| 113 | * Initialize global configuration variable. |
| 114 | */ |
| 115 | void ml_config_load(ml_config_t *cfg) { |
| 116 | const char *config_section_ml = CONFIG_SECTION_ML; |
| 117 | |
| 118 | // Migrate old configuration if needed |
| 119 | ml_config_migrate(); |
| 120 | |
| 121 | int enable_anomaly_detection = inicfg_get_boolean_ondemand(&netdata_config, config_section_ml, "enabled", nd_profile.ml_enabled); |
| 122 | |
| 123 | /* |
| 124 | * Read values |
| 125 | */ |
| 126 | |
| 127 | time_t training_window = inicfg_get_duration_seconds(&netdata_config, config_section_ml, "training window", 6 * 3600); |
| 128 | time_t min_training_window = inicfg_get_duration_seconds(&netdata_config, config_section_ml, "min training window", 15 * 60); |
| 129 | size_t max_training_vectors = inicfg_get_number(&netdata_config, config_section_ml, "max training vectors", 1440); |
| 130 | size_t max_samples_to_smooth = inicfg_get_number(&netdata_config, config_section_ml, "max samples to smooth", 3); |
| 131 | unsigned train_every = inicfg_get_duration_seconds(&netdata_config, config_section_ml, "train every", 3 * 3600); |
| 132 | |
| 133 | unsigned num_models_to_use = inicfg_get_number(&netdata_config, config_section_ml, "number of models per dimension", 18); |
| 134 | unsigned delete_models_older_than = inicfg_get_duration_seconds(&netdata_config, config_section_ml, "delete models older than", 60 * 60 * 24 * 7); |
| 135 | |
| 136 | unsigned diff_n = inicfg_get_number(&netdata_config, config_section_ml, "num samples to diff", 1); |
| 137 | unsigned lag_n = inicfg_get_number(&netdata_config, config_section_ml, "num samples to lag", 5); |
| 138 | |
| 139 | unsigned max_kmeans_iters = inicfg_get_number(&netdata_config, config_section_ml, "maximum number of k-means iterations", 1000); |
| 140 | |
| 141 | double dimension_anomaly_rate_threshold = inicfg_get_double(&netdata_config, config_section_ml, "dimension anomaly score threshold", 0.99); |
| 142 | |
| 143 | double host_anomaly_rate_threshold = inicfg_get_double(&netdata_config, config_section_ml, "host anomaly rate threshold", 1.0); |
| 144 | std::string anomaly_detection_grouping_method = inicfg_get(&netdata_config, config_section_ml, "anomaly detection grouping method", "average"); |
| 145 | time_t anomaly_detection_query_duration = inicfg_get_duration_seconds(&netdata_config, config_section_ml, "anomaly detection grouping duration", 5 * 60); |
| 146 | |
| 147 | size_t num_worker_threads = netdata_conf_is_parent() ? netdata_conf_cpus() / 4 : 1; |
| 148 | if (num_worker_threads < 1) num_worker_threads = 1; |
| 149 | else if (num_worker_threads > 256) num_worker_threads = 256; |
| 150 | num_worker_threads = inicfg_get_number(&netdata_config, config_section_ml, "num training threads", num_worker_threads); |
| 151 | |
| 152 | size_t flush_models_batch_size = inicfg_get_number(&netdata_config, config_section_ml, "flush models batch size", 256); |
| 153 | |
| 154 | size_t suppression_window = |
| 155 | inicfg_get_duration_seconds(&netdata_config, config_section_ml, "dimension anomaly rate suppression window", 900); |
| 156 | |
| 157 | size_t suppression_threshold = |
| 158 | inicfg_get_number(&netdata_config, config_section_ml, "dimension anomaly rate suppression threshold", suppression_window / 2); |
| 159 | |
| 160 | bool enable_statistics_charts = inicfg_get_boolean(&netdata_config, config_section_ml, "enable statistics charts", true); |
| 161 | |
| 162 | /* |
| 163 | * Clamp |
| 164 | */ |
| 165 | |
| 166 | training_window = clamp<time_t>(training_window, 1 * 3600, 24 * 3600); |
| 167 | min_training_window = clamp<time_t>(min_training_window, 1 * 900, 6 * 3600); |
| 168 | train_every = clamp<unsigned>(train_every, 1 * 3600, 6 * 3600); |
| 169 | |
| 170 | num_models_to_use = clamp<unsigned>(num_models_to_use, 1, 7 * 24); |
| 171 | delete_models_older_than = clamp<unsigned>(delete_models_older_than, 60 * 60 * 24 * 1, 60 * 60 * 24 * 7); |
| 172 | |
| 173 | diff_n = clamp(diff_n, 0u, 1u); |
| 174 | max_samples_to_smooth = clamp<size_t>(max_samples_to_smooth, 0, 5); |
| 175 | lag_n = clamp(lag_n, 1u, 5u); |
| 176 | // max_training_vectors drives the lag-extraction sampling ratio (not a |
| 177 | // hard cap on output). A floor of 2 keeps the sampler from being starved |
| 178 | // by a misconfigured value; the ceiling of 86400 (24 hours of 1-second |
| 179 | // samples) is well past anything training_window can supply. The runtime |
| 180 | // guard in ml_dimension_train_model is still required because sampling is |
| 181 | // probabilistic and can drop the emitted vector count below 2. |
| 182 | max_training_vectors = clamp<size_t>(max_training_vectors, 2, 86400); |
| 183 | |
| 184 | max_kmeans_iters = clamp(max_kmeans_iters, 500u, 1000u); |
| 185 | |
| 186 | dimension_anomaly_rate_threshold = clamp(dimension_anomaly_rate_threshold, 0.01, 5.00); |
| 187 | |
| 188 | host_anomaly_rate_threshold = clamp(host_anomaly_rate_threshold, 0.1, 10.0); |
| 189 | anomaly_detection_query_duration = clamp<time_t>(anomaly_detection_query_duration, 60, 15 * 60); |
| 190 | |
| 191 | num_worker_threads = clamp<size_t>(num_worker_threads, 4, netdata_conf_cpus()); |
| 192 | flush_models_batch_size = clamp<size_t>(flush_models_batch_size, 8, 512); |
| 193 | |
| 194 | suppression_window = clamp<size_t>(suppression_window, 1, training_window); |
| 195 | suppression_threshold = clamp<size_t>(suppression_threshold, 1, suppression_window); |
| 196 | |
| 197 | /* |
| 198 | * Validate |
| 199 | */ |
| 200 | |
| 201 | if (min_training_window >= training_window) { |
| 202 | netdata_log_error("invalid min/max training window found (%ld >= %ld)", min_training_window, training_window); |
| 203 | |
| 204 | min_training_window = 1 * 3600; |
| 205 | training_window = 6 * 3600; |
| 206 | } |
| 207 | |
| 208 | /* |
| 209 | * Assign to config instance |
| 210 | */ |
| 211 | |
| 212 | cfg->enable_anomaly_detection = enable_anomaly_detection; |
| 213 | |
| 214 | cfg->training_window = training_window; |
| 215 | cfg->min_training_window = min_training_window; |
| 216 | cfg->max_training_vectors = max_training_vectors; |
| 217 | cfg->max_samples_to_smooth = max_samples_to_smooth; |
| 218 | cfg->train_every = train_every; |
| 219 | |
| 220 | cfg->num_models_to_use = num_models_to_use; |
| 221 | cfg->delete_models_older_than = delete_models_older_than; |
| 222 | |
| 223 | cfg->diff_n = diff_n; |
| 224 | cfg->lag_n = lag_n; |
| 225 | |
| 226 | cfg->max_kmeans_iters = max_kmeans_iters; |
| 227 | |
| 228 | cfg->host_anomaly_rate_threshold = host_anomaly_rate_threshold; |
| 229 | cfg->anomaly_detection_grouping_method = |
| 230 | time_grouping_parse(anomaly_detection_grouping_method.c_str(), RRDR_GROUPING_AVERAGE); |
| 231 | cfg->anomaly_detection_query_duration = anomaly_detection_query_duration; |
| 232 | cfg->dimension_anomaly_score_threshold = dimension_anomaly_rate_threshold; |
| 233 | |
| 234 | cfg->hosts_to_skip = inicfg_get(&netdata_config, config_section_ml, "hosts to skip from training", "!*"); |
| 235 | cfg->sp_host_to_skip = simple_pattern_create(cfg->hosts_to_skip.c_str(), NULL, SIMPLE_PATTERN_EXACT, true); |
| 236 | |
| 237 | // Always exclude anomaly_detection charts from training. |
| 238 | cfg->charts_to_skip = "anomaly_detection.* "; |
| 239 | cfg->charts_to_skip += inicfg_get(&netdata_config, config_section_ml, "charts to skip from training", "netdata.*"); |
| 240 | cfg->sp_charts_to_skip = simple_pattern_create(cfg->charts_to_skip.c_str(), NULL, SIMPLE_PATTERN_EXACT, true); |
| 241 | |
| 242 | cfg->stream_anomaly_detection_charts = inicfg_get_boolean(&netdata_config, config_section_ml, "stream anomaly detection charts", true); |
| 243 | |
| 244 | cfg->num_worker_threads = num_worker_threads; |
| 245 | cfg->flush_models_batch_size = flush_models_batch_size; |
| 246 | |
| 247 | cfg->suppression_window = suppression_window; |
| 248 | cfg->suppression_threshold = suppression_threshold; |
| 249 | |
| 250 | cfg->enable_statistics_charts = enable_statistics_charts; |
| 251 | |
| 252 | if (cfg->enable_anomaly_detection == CONFIG_BOOLEAN_AUTO && default_rrd_memory_mode != RRD_DB_MODE_DBENGINE) { |
| 253 | Cfg.enable_anomaly_detection = 0; |
| 254 | inicfg_set_boolean(&netdata_config, config_section_ml, "enabled", CONFIG_BOOLEAN_NO); |
| 255 | return; |
| 256 | } |
| 257 | } |