@cryptotaxi247 / netdata-1 / commits / 9ed4cea59

Anomaly Detection MVP (#11548)

* Add support for feature extraction and K-Means clustering. This patch adds support for performing feature extraction and running the K-Means clustering algorithm on the extracted features. We use the open-source dlib library to compute the K-Means clustering centers, which has been added as a new git submodule. The build system has been updated to recognize two new options: 1) --enable-ml: build an agent with ml functionality, and 2) --enable-ml-tests: support running tests with the `-W mltest` option in netdata. The second flag is meant only for internal use. To build tests successfully, you need to install the GoogleTest framework on your machine. * Boilerplate code to track hosts/dims and init ML config options. A new opaque pointer field is added to the database's host and dimension data structures. The fields point to C++ wrapper classes that will be used to store ML-related information in follow-up patches. The ML functionality needs to iterate all tracked dimensions twice per second. To avoid locking the entire DB multiple times, we use a separate dictionary to add/remove dimensions as they are created/deleted by the database. A global configuration object is initialized during the startup of the agent. It will allow our users to specify ML-related configuration options, eg. hosts/charts to skip from training, etc. * Add support for training and prediction of dimensions. Every new host spawns a training thread which is used to train the model of each dimension. Training of dimensions is done in a non-batching mode in order to avoid impacting the generated ML model by the CPU, RAM and disk utilization of the training code itself. For performance reasons, prediction is done at the time a new value is pushed in the database. The alternative option, ie. maintaining a separate thread for prediction, would be ~3-4x times slower and would increase locking contention considerably. For similar reasons, we use a custom function to unpack storage_numbers into doubles, instead of long doubles. * Add data structures required by the anomaly detector. This patch adds two data structures that will be used by the anomaly detector in follow-up patches. The first data structure is a circular bit buffer which is being used to count the number of set bits over time. The second data structure represents an expandable, rolling window that tracks set/unset bits. It is explicitly modeled as a finite-state machine in order to make the anomaly detector's behaviour easier to test and reason about. * Add anomaly detection thread. This patch creates a new anomaly detection thread per host. Each thread maintains a BitRateWindow which is updated every second based on the anomaly status of the correspondent host. Based on the updated status of the anomaly window, we can identify the existence/absence of an anomaly event, it's start/end time and the dimensions that participate in it. * Create/insert/query anomaly events from Sqlite DB. * Create anomaly event endpoints. This patch adds two endpoints to expose information about anomaly events. The first endpoint returns the list of anomalous events within a specified time range. The second endpoint provides detailed information about a single anomaly event, ie. the list of anomalous dimensions in that event along with their anomaly rate. The `anomaly-bit` option has been added to the `/data` endpoint in order to allow users to get the anomaly status of individual dimensions per second. * Fix build failures on Ubuntu 16.04 & CentOS 7. These distros do not have toolchains with C++11 enabled by default. Replacing nullptr with NULL should be fix the build problems on these platforms when the ML feature is not enabled. * Fix `make dist` to include ML makefiles and dlib sources. Currently, we add ml/kmeans/dlib to EXTRA_DIST. We might want to generate an explicit list of source files in the future, in order to bring down the generated archive's file size. * Small changes to make the LGTM & Codacy bots happy. - Cast unused result of function calls to void. - Pass a const-ref string to Database's constructor. - Reduce the scope of a local variable in the anomaly detector. * Add user configuration option to enable/disable anomaly detection. * Do not log dimension-specific operations. Training and prediction operations happen every second for each dimension. In prep for making this PR easier to run anomaly detection for many charts & dimensions, I've removed logs that would cause log flooding. * Reset dimensions' bit counter when not above anomaly rate threshold. * Update the default config options with real values. With this patch the default configuration options will match the ones we want our users to use by default. * Update conditions for creating new ML dimensions. 1. Skip dimensions with update_every != 1, 2. Skip dimensions that come from the ML charts. With this filtering in place, any configuration value for the relevant simple_pattern expressions will work correctly. * Teach buildinfo{,json} about the ML feature. * Set --enable-ml by default in the configuration options. This patch is only meant for testing the building of the ML functionality on Github. It will be reverted once tests pass successfully. * Minor build system fixes. - Add path to json header - Enable C++ linker when ML functionality is enabled - Rename ml/ml-dummy.cc to ml/ml-dummy.c * Revert "Set --enable-ml by default in the configuration options." This reverts commit 28206952a59a577675c86194f2590ec63b60506c. We pass all Github checks when building the ML functionality, except for those that run on CentOS 7 due to not having a C++11 toolchain. * Check for missing dlib and nlohmann files. We simply check the single-source files upon which our build system depends. If they are missing, an error message notifies the user about missing git submodules which are required for the ML functionality. * Allow users to specify the maximum number of KMeans iterations. * Use dlib v19.10 v19.22 broke compatibility with CentOS 7's g++. Development of the anomaly detection used v19.10, which is the version used by most Debian and Ubuntu distribution versions that are not past EOL. No observable performance improvements/regressions specific to the K-Means algorithm occur between the two versions. * Detect and use the -std=c++11 flag when building anomaly detection. This patch automatically adds the -std=c++11 when building netdata with the ML functionality, if it's supported by the user's toolchain. With this change we are able to build the agent correctly on CentOS 7. * Restructure configuration options. - update default values, - clamp values to min/max defaults, - validate and identify conflicting values. * Add update_every configuration option. Considerring that the MVP does not support per host configuration options, the update_every option will be used to filter hosts to train. With this change anomaly detection will be supported on: - Single nodes with update_every != 1, and - Children nodes with a common update_every value that might differ from the value of the parent node. * Reorganize anomaly detection charts. This follows Andrew's suggestion to have four charts to show the number of anomalous/normal dimensions, the anomaly rate, the detector's window length, and the events that occur in the prediction step. Context and family values, along with the necessary information in the dashboard_info.js file, will be updated in a follow-up commit. * Do not dump anomaly event info in logs. * Automatically handle low "train every secs" configuration values. If a user specifies a very low value for the "train every secs", then it is possible that the time it takes to train a dimension is higher than the its allotted time. In that case, we want the training thread to: - Reduce it's CPU usage per second, and - Allow the prediction thread to proceed. We achieve this by limiting the training time of a single dimension to be equal to half the time allotted to it. This means, that the training thread will never consume more than 50% of a single core. * Automatically detect if ML functionality should be enabled. With these changes, we enable ML if: - The user has not explicitly specified --disable-ml, and - Git submodules have been checked out properly, and - The toolchain supports C++11. If the user has explicitly specified --enable-ml, the build fails if git submodules are missing, or the toolchain does not support C++11. * Disable anomaly detection by default. * Do not update charts in locked region. * Cleanup code reading configuration options. * Enable C++ linker when building ML. * Disable ML functionality for CMake builds. * Skip LGTM for dlib and nlohmann libraries. * Do not build ML if libuuid is missing. * Fix dlib path in LGTM's yaml config file. * Add chart to track duration of prediction step. * Add chart to track duration of training step. * Limit the number dimensions in an anomaly event. This will ensure our JSON results won't grow without any limit. The default ML configuration options, train approximately ~1700 dimensions in a newly-installed Netdata agent. The hard-limit is set to 2000 dimensions which: - Is well above the default number of dimensions we train, - If it is ever reached it means that the user had accidentaly a very low anomaly rate threshold, and - Considering that we sort the result by anomaly score, the cutoff dimensions will be the less anomalous, ie. the least important to investigate. * Add information about the ML charts. * Update family value in ML charts. This fix will allow us to show the individual charts in the RHS Anomaly Detection submenu. * Rename chart type s/anomalydetection/anomaly_detection/g * Expose ML feat in /info endpoint. * Export ML config through /info endpoint. * Fix CentOS 7 build. * Reduce the critical region of a host's lock. Before this change, each host had a single, dedicated lock to protect its map of dimensions from adding/deleting new dimensions while training and detecting anomalies. This was problematic because training of a single dimension can take several seconds in nodes that are under heavy load. After this change, the host's lock protects only the insertion/deletion of new dimensions, and the prediction step. For the training of dimensions we use a dedicated lock per dimension, which is responsible for protecting the dimension from deletion while training. Prediction is fast enough, even on slow machines or under heavy load, which allows us to use the host's main lock and avoid increasing the complexity of our implementation in the anomaly detector. * Improve the way we are tracking anomaly detector's performance. This change allows us to: - track the total training time per update_every period, - track the maximum training time of a single dimension per update_every period, and - export the current number of total, anomalous, normal dimensions to the /info endpoint. Also, now that we use dedicated locks per dimensions, we can train under heavy load continuously without having to sleep in order to yield the training thread and allow the prediction thread to progress. * Use samples instead of seconds in ML configuration. This commit changes the way we are handling input ML configuration options from the user. Instead of treating values as seconds, we interpret all inputs as number of update_every periods. This allows us to enable anomaly detection on hosts that have update_every != 1 second, and still produce a model for training/prediction & detection that behaves in an expected way. Tested by running anomaly detection on an agent with update_every = [1, 2, 4] seconds. * Remove unecessary log message in detection thread * Move ML configuration to global section. * Update web/gui/dashboard_info.js Co-authored-by: Andrew Maguire <andrewm4894@gmail.com> * Fix typo Co-authored-by: Andrew Maguire <andrewm4894@gmail.com> * Rebase. * Use negative logic for anomaly bit. * Add info for prediction_stats and training_stats charts. * Disable ML on PPC64EL. The CI test fails with -std=c++11 and requires -std=gnu++11 instead. However, it's not easy to quickly append the required flag to CXXFLAGS. For the time being, simply disable ML on PPC64EL and if any users require this functionality we can fix it in the future. * Add comment on why we disable ML on PPC64EL. Co-authored-by: Andrew Maguire <andrewm4894@gmail.com>

vkalintiris committed Oct 27, 2021 at 09:26 UTC 9ed4cea59042aa5e5053c4b4b3529e9d70ab83f9
50 files changed +3143 -21
.github/workflows/tests.yml
+1 -1
@@ -57,7 +57,7 @@ jobs:
57 - name: Configure
58 run: |
59 autoreconf -ivf
60 - ./configure
60 + ./configure --disable-ml
61 # XXX: Work-around for bug with libbson-1.0 in Ubuntu 18.04
62 # See: https://bugs.launchpad.net/ubuntu/+source/libmongoc/+bug/1790771
63 # https://jira.mongodb.org/browse/CDRIVER-2818
.gitmodules
+9
@@ -4,3 +4,12 @@
4 [submodule "aclk/aclk-schemas"]
5 path = aclk/aclk-schemas
6 url = https://github.com/netdata/aclk-schemas.git
7 +[submodule "ml/kmeans/dlib"]
8 + path = ml/kmeans/dlib
9 + url = https://github.com/davisking/dlib.git
10 + shallow = true
11 + ignore = dirty
12 +[submodule "ml/json"]
13 + path = ml/json
14 + url = https://github.com/nlohmann/json.git
15 + shallow = true
.lgtm.yml
+2
@@ -17,6 +17,8 @@ path_classifiers:
17 - collectors/node.d.plugin/node_modules/extend.js
18 - collectors/node.d.plugin/node_modules/net-snmp.js
19 - collectors/node.d.plugin/node_modules/pixl-xml.js
20 + - ml/kmeans/dlib/
21 + - ml/json/
22 - web/gui/lib/
23 - web/gui/src/
24 - web/gui/css/
CMakeLists.txt
+6
@@ -945,6 +945,11 @@ set(DAEMON_FILES
945 daemon/unit_test.h
946 )
947
948 +set(ML_FILES
949 + ml/ml.h
950 + ml/ml-dummy.c
951 +)
952 +
953 set(NETDATA_FILES
954 collectors/all.h
955 ${DAEMON_FILES}
@@ -954,6 +959,7 @@ set(NETDATA_FILES
959 ${CHECKS_PLUGIN_FILES}
960 ${HEALTH_PLUGIN_FILES}
961 ${IDLEJITTER_PLUGIN_FILES}
962 + ${ML_FILES}
963 ${PLUGINSD_PLUGIN_FILES}
964 ${RRD_PLUGIN_FILES}
965 ${REGISTRY_PLUGIN_FILES}
Makefile.am
+47
@@ -39,6 +39,7 @@ EXTRA_DIST = \
39 build/m4/ax_c_mallopt.m4 \
40 build/m4/tcmalloc.m4 \
41 build/m4/ax_c__generic.m4 \
42 + ml/kmeans/dlib \
43 README.md \
44 LICENSE \
45 REDISTRIBUTED.md \
@@ -117,6 +118,7 @@ SUBDIRS += \
118 claim \
119 parser \
120 spawn \
121 + ml \
122 $(NULL)
123
124 if ENABLE_ACLK
@@ -233,6 +235,44 @@ HEALTH_PLUGIN_FILES = \
235 health/health_log.c \
236 $(NULL)
237
238 +ML_FILES = \
239 + ml/ml.h \
240 + ml/ml-dummy.c \
241 + $(NULL)
242 +
243 +if ENABLE_ML
244 +ML_FILES += \
245 + ml/BitBufferCounter.h \
246 + ml/BitBufferCounter.cc \
247 + ml/BitRateWindow.h \
248 + ml/BitRateWindow.cc \
249 + ml/Config.h \
250 + ml/Config.cc \
251 + ml/Database.h \
252 + ml/Database.cc \
253 + ml/Dimension.cc \
254 + ml/Dimension.h \
255 + ml/Host.h \
256 + ml/Host.cc \
257 + ml/Query.h \
258 + ml/kmeans/KMeans.h \
259 + ml/kmeans/KMeans.cc \
260 + ml/kmeans/SamplesBuffer.h \
261 + ml/kmeans/SamplesBuffer.cc \
262 + ml/kmeans/dlib/dlib/all/source.cpp \
263 + ml/json/single_include/nlohmann/json.hpp \
264 + ml/ml.cc \
265 + ml/ml-private.h \
266 + $(NULL)
267 +endif
268 +
269 +if ENABLE_ML_TESTS
270 +ML_TESTS_FILES = \
271 + ml/kmeans/Tests.cc \
272 + ml/Tests.cc \
273 + $(NULL)
274 +endif
275 +
276 IDLEJITTER_PLUGIN_FILES = \
277 collectors/idlejitter.plugin/plugin_idlejitter.c \
278 collectors/idlejitter.plugin/plugin_idlejitter.h \
@@ -863,6 +903,8 @@ NETDATA_FILES = \
903 $(EXPORTING_ENGINE_FILES) \
904 $(CHECKS_PLUGIN_FILES) \
905 $(HEALTH_PLUGIN_FILES) \
906 + $(ML_FILES) \
907 + $(ML_TESTS_FILES) \
908 $(IDLEJITTER_PLUGIN_FILES) \
909 $(PLUGINSD_PLUGIN_FILES) \
910 $(REGISTRY_PLUGIN_FILES) \
@@ -944,6 +986,11 @@ if ACLK_NG
986 $(NULL)
987 endif
988
989 +if ENABLE_ML_TESTS
990 + netdata_LDADD += $(OPTIONAL_ML_TESTS_LIBS) \
991 + $(NULL)
992 +endif
993 +
994 if ACLK_LEGACY
995 netdata_LDADD += \
996 $(abs_top_srcdir)/externaldeps/mosquitto/libmosquitto.a \
aclk/aclk_util.h
-6
@@ -5,12 +5,6 @@
5 #include "libnetdata/libnetdata.h"
6 #include "mqtt_wss_client.h"
7
8 -// CentOS 7 has older version that doesn't define this
9 -// same goes for MacOS
10 -#ifndef UUID_STR_LEN
11 -#define UUID_STR_LEN 37
12 -#endif
13 -
8 // Helper stuff which should not have any further inside ACLK dependency
9 // and are supposed not to be needed outside of ACLK
10
configure.ac
+107 -2
@@ -184,6 +184,18 @@ AC_ARG_WITH(
184 [with_bundled_protobuf="$withval"],
185 [with_bundled_protobuf="detect"]
186 )
187 +AC_ARG_ENABLE(
188 + [ml],
189 + [AS_HELP_STRING([--enable-ml], [Enable anomaly detection @<:@default autodetect@:>@])],
190 + ,
191 + [enable_ml="detect"]
192 +)
193 +AC_ARG_ENABLE(
194 + [ml_tests],
195 + [AS_HELP_STRING([--enable-ml-tests], [Enable anomaly detection tests @<:@no@:>@])],
196 + [enable_ml_tests="yes"],
197 + [enable_ml_tests="no"]
198 +)
199
200 # -----------------------------------------------------------------------------
201 # Enforce building with C99, bail early if we can't.
@@ -1189,6 +1201,90 @@ fi
1201 AC_MSG_RESULT([${enable_plugin_perf}])
1202 AM_CONDITIONAL([ENABLE_PLUGIN_PERF], [test "${enable_plugin_perf}" = "yes"])
1203
1204 +# -----------------------------------------------------------------------------
1205 +# gtest/gmock
1206 +
1207 +AC_MSG_CHECKING([if gtest and gmock can be found])
1208 +
1209 +PKG_CHECK_MODULES([GTEST], [gtest], [have_gtest=yes], [have_gtest=no])
1210 +PKG_CHECK_MODULES([GMOCK], [gmock], [have_gmock=yes], [have_gmock=no])
1211 +
1212 +if test "${have_gtest}" = "yes" -a "${have_gmock}" = "yes"; then
1213 + OPTIONAL_GTEST_CFLAGS="${GTEST_CFLAGS} ${GMOCK_CFLAGS}"
1214 + OPTIONAL_GTEST_LIBS="${GTEST_LIBS} ${GMOCK_LIBS}"
1215 + have_gtest="yes"
1216 +else
1217 + have_gtest="no"
1218 +fi
1219 +
1220 +# -----------------------------------------------------------------------------
1221 +# ml - anomaly detection
1222 +
1223 +# Check if uuid is availabe. Fail if ML was explicitly requested.
1224 +if test "${enable_ml}" = "yes" -a "${have_uuid}" != "yes"; then
1225 + AC_MSG_ERROR([You have explicitly requested --enable-ml functionality but libuuid can not be found."])
1226 +fi
1227 +
1228 +# Check if submodules have not been fetched. Fail if ML was explicitly requested.
1229 +AC_MSG_CHECKING([if git submodules are present for machine learning functionality])
1230 +if test -f "ml/kmeans/dlib/dlib/all/source.cpp" -a -f "ml/json/single_include/nlohmann/json.hpp"; then
1231 + AC_MSG_RESULT([yes])
1232 + have_ml_submodules="yes"
1233 +else
1234 + AC_MSG_RESULT([no])
1235 + have_ml_submodules="no"
1236 +fi
1237 +
1238 +if test "${enable_ml}" = "yes" -a "${have_ml_submodules}" = "no"; then
1239 + AC_MSG_ERROR([You have explicitly requested --enable-ml functionality but it cannot be built because the required git submodules are missing.])
1240 +fi
1241 +
1242 +# Check if C++ toolchain does not support C++11. Fail if ML was explicitly requested.
1243 +AC_LANG_PUSH([C++])
1244 +AX_CHECK_COMPILE_FLAG([-std=c++11], [have_cxx11=yes], [have_cxx11=no])
1245 +AC_LANG_POP([C++])
1246 +
1247 +# PPC64LE needs -std=gnu++11 in order to build dlib. However, the rest of
1248 +# the agent's components use and have been tested only with -std=c++11.
1249 +# Skip ML compilation on that CPU until we reorganize and test the C++ flags.
1250 +if test "${host_cpu}" = "powerpc64le"; then
1251 + have_cxx11="no"
1252 +fi
1253 +
1254 +if test "${enable_ml}" = "yes" -a "${have_cxx11}" = "no"; then
1255 + AC_MSG_ERROR([You have explicitly requested --enable-ml functionality but it cannot be built without a C++11 toolchain.])
1256 +else
1257 + CXX11FLAG="$CXX11FLAG -std=c++11"
1258 +fi
1259 +
1260 +# Decide if we should build ML
1261 +if test "${enable_ml}" != "no" -a "${have_ml_submodules}" = "yes" -a "${have_cxx11}" = "yes" -a "${have_uuid}" = "yes"; then
1262 + build_ml="yes"
1263 +else
1264 + build_ml="no"
1265 +fi
1266 +
1267 +AM_CONDITIONAL([ENABLE_ML], [test "${build_ml}" = "yes"])
1268 +if test "${build_ml}" = "yes"; then
1269 + AC_DEFINE([ENABLE_ML], [1], [anomaly detection usability])
1270 + OPTIONAL_ML_CFLAGS="-DDLIB_NO_GUI_SUPPORT -I \$(abs_top_srcdir)/ml/kmeans/dlib"
1271 + OPTIONAL_ML_LIBS=""
1272 +fi
1273 +
1274 +# Decide if we should build ML tests.
1275 +if test "${build_ml}" = "yes" -a "${enable_ml_tests}" = "yes" -a "${have_gtest}" = "yes"; then
1276 + build_ml_tests="yes"
1277 +else
1278 + build_ml_tests="no"
1279 +fi
1280 +
1281 +AM_CONDITIONAL([ENABLE_ML_TESTS], [test "${build_ml_tests}" = "yes"])
1282 +if test "${build_ml_tests}" = "yes"; then
1283 + AC_DEFINE([ENABLE_ML_TESTS], [1], [anomaly detection tests])
1284 + OPTIONAL_ML_TESTS_CFLAGS="${OPTIONAL_GTEST_CFLAGS}"
1285 + OPTIONAL_ML_TESTS_LIBS="${OPTIONAL_GTEST_LIBS}"
1286 +fi
1287 +
1288 # -----------------------------------------------------------------------------
1289 # ebpf.plugin
1290
@@ -1557,7 +1653,8 @@ AC_MSG_RESULT([${enable_lto}])
1653 AM_CONDITIONAL([ENABLE_CXX_LINKER], [test "${enable_backend_kinesis}" = "yes" \
1654 -o "${enable_exporting_pubsub}" = "yes" \
1655 -o "${enable_backend_prometheus_remote_write}" = "yes" \
1560 - -o "${new_cloud_protocol}" = "yes"])
1656 + -o "${new_cloud_protocol}" = "yes" \
1657 + -o "${build_ml}" = "yes"])
1658
1659 AC_DEFINE_UNQUOTED([NETDATA_USER], ["${with_user}"], [use this user to drop privileged])
1660
@@ -1589,7 +1686,7 @@ CFLAGS="${CFLAGS} ${OPTIONAL_PROTOBUF_CFLAGS} ${OPTIONAL_MATH_CFLAGS} ${OPTIONAL
1686 ${OPTIONAL_LIBCAP_CFLAGS} ${OPTIONAL_IPMIMONITORING_CFLAGS} ${OPTIONAL_CUPS_CFLAGS} ${OPTIONAL_XENSTAT_FLAGS} \
1687 ${OPTIONAL_KINESIS_CFLAGS} ${OPTIONAL_PUBSUB_CFLAGS} ${OPTIONAL_PROMETHEUS_REMOTE_WRITE_CFLAGS} \
1688 ${OPTIONAL_MONGOC_CFLAGS} ${LWS_CFLAGS} ${OPTIONAL_JSONC_STATIC_CFLAGS} ${OPTIONAL_BPF_CFLAGS} ${OPTIONAL_JUDY_CFLAGS} \
1592 - ${OPTIONAL_ACLK_NG_CFLAGS}"
1689 + ${OPTIONAL_ACLK_NG_CFLAGS} ${OPTIONAL_ML_CFLAGS} ${OPTIONAL_ML_TESTS_CFLAGS}"
1690
1691 CXXFLAGS="${CFLAGS} ${CXX11FLAG}"
1692
@@ -1642,6 +1739,12 @@ AC_SUBST([OPTIONAL_LWS_LIBS])
1739 AC_SUBST([OPTIONAL_ACLK_NG_CFLAGS])
1740 AC_SUBST([OPTIONAL_PROTOBUF_CFLAGS])
1741 AC_SUBST([OPTIONAL_PROTOBUF_LIBS])
1742 +AC_SUBST([OPTIONAL_GTEST_CFLAGS])
1743 +AC_SUBST([OPTIONAL_GTEST_LIBS])
1744 +AC_SUBST([OPTIONAL_ML_CFLAGS])
1745 +AC_SUBST([OPTIONAL_ML_LIBS])
1746 +AC_SUBST([OPTIONAL_ML_TESTS_CFLAGS])
1747 +AC_SUBST([OPTIONAL_ML_TESTS_LIBS])
1748
1749 # -----------------------------------------------------------------------------
1750 # Check if cmocka is available - needed for unit testing
@@ -1731,6 +1834,8 @@ AC_CONFIG_FILES([
1834 exporting/tests/Makefile
1835 health/Makefile
1836 health/notifications/Makefile
1837 + ml/Makefile
1838 + ml/kmeans/Makefile
1839 libnetdata/Makefile
1840 libnetdata/tests/Makefile
1841 libnetdata/adaptive_resortable_list/Makefile
daemon/buildinfo.c
+10 -1
@@ -37,6 +37,12 @@
37 #define FEAT_NATIVE_HTTPS 0
38 #endif
39
40 +#ifdef ENABLE_ML
41 +#define FEAT_ML 1
42 +#else
43 +#define FEAT_ML 0
44 +#endif
45 +
46 // Optional libraries
47
48 #ifdef ENABLE_JSONC
@@ -224,6 +230,7 @@ void print_build_info(void) {
230 printf(" ACLK-NG New Cloud Protocol: %s\n", FEAT_YES_NO(NEW_CLOUD_PROTO));
231 printf(" ACLK Legacy: %s\n", FEAT_YES_NO(FEAT_ACLK_LEGACY));
232 printf(" TLS Host Verification: %s\n", FEAT_YES_NO(FEAT_TLS_HOST_VERIFY));
233 + printf(" Machine Learning: %s\n", FEAT_YES_NO(FEAT_ML));
234
235 printf("Libraries:\n");
236 printf(" jemalloc: %s\n", FEAT_YES_NO(FEAT_JEMALLOC));
@@ -282,7 +289,8 @@ void print_build_info_json(void) {
289 printf(" \"aclk-ng-new-cloud-proto\": %s,\n", FEAT_JSON_BOOL(NEW_CLOUD_PROTO));
290 printf(" \"aclk-legacy\": %s,\n", FEAT_JSON_BOOL(FEAT_ACLK_LEGACY));
291
285 - printf(" \"tls-host-verify\": %s\n", FEAT_JSON_BOOL(FEAT_TLS_HOST_VERIFY));
292 + printf(" \"tls-host-verify\": %s,\n", FEAT_JSON_BOOL(FEAT_TLS_HOST_VERIFY));
293 + printf(" \"machine-learning\": %s\n", FEAT_JSON_BOOL(FEAT_ML));
294 printf(" },\n");
295
296 printf(" \"libs\": {\n");
@@ -337,6 +345,7 @@ void analytics_build_info(BUFFER *b) {
345 if(NEW_CLOUD_PROTO) buffer_strcat (b, "|New Cloud Protocol Support");
346 if(FEAT_ACLK_LEGACY) buffer_strcat (b, "|ACLK Legacy");
347 if(FEAT_TLS_HOST_VERIFY) buffer_strcat (b, "|TLS Host Verification");
348 + if(FEAT_ML) buffer_strcat (b, "|Machine Learning");
349
350 if(FEAT_JEMALLOC) buffer_strcat (b, "|jemalloc");
351 if(FEAT_JSONC) buffer_strcat (b, "|JSON-C");
daemon/common.h
+3
@@ -44,6 +44,9 @@
44 // health monitoring and alarm notifications
45 #include "health/health.h"
46
47 +// anomaly detection
48 +#include "ml/ml.h"
49 +
50 // the netdata registry
51 // the registry is actually an API feature
52 #include "registry/registry.h"
daemon/main.c
+9
@@ -826,6 +826,11 @@ int main(int argc, char **argv) {
826 fprintf(stderr, "\n\nALL TESTS PASSED\n\n");
827 return 0;
828 }
829 +#ifdef ENABLE_ML_TESTS
830 + else if(strcmp(optarg, "mltest") == 0) {
831 + return test_ml(argc, argv);
832 + }
833 +#endif
834 #ifdef ENABLE_DBENGINE
835 else if(strncmp(optarg, createdataset_string, strlen(createdataset_string)) == 0) {
836 optarg += strlen(createdataset_string);
@@ -1144,6 +1149,10 @@ int main(int argc, char **argv) {
1149 set_silencers_filename();
1150 health_initialize_global_silencers();
1151
1152 + // --------------------------------------------------------------------
1153 + // Initialize ML configuration
1154 + ml_init();
1155 +
1156 // --------------------------------------------------------------------
1157 // setup process signals
1158
database/engine/rrdenginelib.h
+3 -5
@@ -3,6 +3,8 @@
3 #ifndef NETDATA_RRDENGINELIB_H
4 #define NETDATA_RRDENGINELIB_H
5
6 +#include "libnetdata/libnetdata.h"
7 +
8 /* Forward declarations */
9 struct rrdeng_page_descr;
10 struct rrdengine_instance;
@@ -12,10 +14,6 @@ struct rrdengine_instance;
14
15 #define BITS_PER_ULONG (sizeof(unsigned long) * 8)
16
15 -#ifndef UUID_STR_LEN
16 -#define UUID_STR_LEN (37)
17 -#endif
18 -
17 /* Taken from linux kernel */
18 #define BUILD_BUG_ON(condition) ((void)sizeof(char[1 - 2*!!(condition)]))
19
@@ -141,4 +139,4 @@ extern char *get_rrdeng_statistics(struct rrdengine_instance *ctx, char *str, si
139 extern int compute_multidb_diskspace();
140 extern int is_legacy_child(const char *machine_guid);
141
144 -#endif /* NETDATA_RRDENGINELIB_H */
\ No newline at end of file
142 +#endif /* NETDATA_RRDENGINELIB_H */
database/rrd.h
+9
@@ -15,6 +15,9 @@ typedef struct rrdcalctemplate RRDCALCTEMPLATE;
15 typedef struct alarm_entry ALARM_ENTRY;
16 typedef struct context_param CONTEXT_PARAM;
17
18 +typedef void *ml_host_t;
19 +typedef void *ml_dimension_t;
20 +
21 // forward declarations
22 struct rrddim_volatile;
23 struct rrdset_volatile;
@@ -421,6 +424,8 @@ struct rrddim_volatile {
424 // get the timestamp of the first entry of this metric
425 time_t (*oldest_time)(RRDDIM *rd);
426 } query_ops;
427 +
428 + ml_dimension_t ml_dimension;
429 };
430
431 // ----------------------------------------------------------------------------
@@ -876,6 +881,10 @@ struct rrdhost {
881
882 netdata_rwlock_t rrdhost_rwlock; // lock for this RRDHOST (protects rrdset_root linked list)
883
884 + // ------------------------------------------------------------------------
885 + // ML handle
886 + ml_host_t ml_host;
887 +
888 // ------------------------------------------------------------------------
889 // Support for host-level labels
890 struct label_index labels;
database/rrddim.c
+5 -1
@@ -386,7 +386,7 @@ RRDDIM *rrddim_add_custom(RRDSET *st, const char *id, const char *name, collecte
386 rd->last_collected_time.tv_sec = 0;
387 rd->last_collected_time.tv_usec = 0;
388 rd->rrdset = st;
389 - rd->state = mallocz(sizeof(*rd->state));
389 + rd->state = callocz(1, sizeof(*rd->state));
390 #ifdef ENABLE_ACLK
391 rd->state->aclk_live_status = -1;
392 #endif
@@ -454,6 +454,8 @@ RRDDIM *rrddim_add_custom(RRDSET *st, const char *id, const char *name, collecte
454
455 calc_link_to_rrddim(rd);
456
457 + ml_new_dimension(rd);
458 +
459 rrdset_unlock(st);
460 #ifdef ENABLE_ACLK
461 rrdset_flag_clear(st, RRDSET_FLAG_ACLK);
@@ -466,6 +468,8 @@ RRDDIM *rrddim_add_custom(RRDSET *st, const char *id, const char *name, collecte
468
469 void rrddim_free_custom(RRDSET *st, RRDDIM *rd, int db_rotated)
470 {
471 + ml_delete_dimension(rd);
472 +
473 #ifndef ENABLE_ACLK
474 UNUSED(db_rotated);
475 #endif
database/rrdhost.c
+4
@@ -382,6 +382,8 @@ RRDHOST *rrdhost_create(const char *hostname,
382 else localhost = host;
383 }
384
385 + ml_new_host(host);
386 +
387 info("Host '%s' (at registry as '%s') with guid '%s' initialized"
388 ", os '%s'"
389 ", timezone '%s'"
@@ -906,6 +908,8 @@ void rrdhost_free(RRDHOST *host) {
908 rrdeng_exit(host->rrdeng_ctx);
909 #endif
910
911 + ml_delete_host(host);
912 +
913 // ------------------------------------------------------------------------
914 // remove it from the indexes
915
database/rrdset.c
+11 -2
@@ -1237,13 +1237,22 @@ static inline size_t rrdset_done_interpolate(
1237 }
1238
1239 if(unlikely(!store_this_entry)) {
1240 + (void) ml_is_anomalous(rd, 0, false);
1241 +
1242 rd->state->collect_ops.store_metric(rd, next_store_ut, SN_EMPTY_SLOT);
1243 // rd->values[current_entry] = SN_EMPTY_SLOT;
1244 continue;
1245 }
1246
1247 if(likely(rd->updated && rd->collections_counter > 1 && iterations < st->gap_when_lost_iterations_above)) {
1246 - rd->state->collect_ops.store_metric(rd, next_store_ut, pack_storage_number(new_value, storage_flags));
1248 + uint32_t dim_storage_flags = storage_flags;
1249 +
1250 + if (ml_is_anomalous(rd, new_value, true)) {
1251 + // clear anomaly bit: 0 -> is anomalous, 1 -> not anomalous
1252 + dim_storage_flags &= ~ ((uint32_t) SN_ANOMALY_BIT);
1253 + }
1254 +
1255 + rd->state->collect_ops.store_metric(rd, next_store_ut, pack_storage_number(new_value, dim_storage_flags));
1256 // rd->values[current_entry] = pack_storage_number(new_value, storage_flags );
1257 rd->last_stored_value = new_value;
1258
@@ -1255,9 +1264,9 @@ static inline size_t rrdset_done_interpolate(
1264 , unpack_storage_number(rd->values[current_entry]), new_value
1265 );
1266 #endif
1258 -
1267 }
1268 else {
1269 + (void) ml_is_anomalous(rd, 0, false);
1270
1271 #ifdef NETDATA_INTERNAL_CHECKS
1272 rrdset_debug(st, "%s: STORE[%ld] = NON EXISTING "
libnetdata/config/appconfig.c
+1
@@ -796,6 +796,7 @@ void appconfig_generate(struct config *root, BUFFER *wb, int only_changed)
796 || !strcmp(co->name, CONFIG_SECTION_BACKEND)
797 || !strcmp(co->name, CONFIG_SECTION_STREAM)
798 || !strcmp(co->name, CONFIG_SECTION_HOST_LABEL)
799 + || !strcmp(co->name, CONFIG_SECTION_ML)
800 )
801 pri = 0;
802 else if(!strncmp(co->name, "plugin:", 7)) pri = 1;
libnetdata/config/appconfig.h
+1
@@ -91,6 +91,7 @@
91 #define CONFIG_SECTION_HEALTH "health"
92 #define CONFIG_SECTION_BACKEND "backend"
93 #define CONFIG_SECTION_STREAM "stream"
94 +#define CONFIG_SECTION_ML "ml"
95 #define CONFIG_SECTION_EXPORTING "exporting:global"
96 #define CONFIG_SECTION_PROMETHEUS "prometheus:exporter"
97 #define CONFIG_SECTION_HOST_LABEL "host labels"
libnetdata/libnetdata.h
+7
@@ -53,6 +53,7 @@ extern "C" {
53
54 #include <pthread.h>
55 #include <errno.h>
56 +#include <stdbool.h>
57 #include <stdio.h>
58 #include <stdlib.h>
59 #include <stdarg.h>
@@ -90,6 +91,12 @@ extern "C" {
91 #include <uv.h>
92 #include <assert.h>
93
94 +// CentOS 7 has older version that doesn't define this
95 +// same goes for MacOS
96 +#ifndef UUID_STR_LEN
97 +#define UUID_STR_LEN (37)
98 +#endif
99 +
100 #ifdef HAVE_NETINET_IN_H
101 #include <netinet/in.h>
102 #endif
ml/BitBufferCounter.cc new
+29
@@ -0,0 +1,29 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#include "BitBufferCounter.h"
4 +
5 +using namespace ml;
6 +
7 +std::vector<bool> BitBufferCounter::getBuffer() const {
8 + std::vector<bool> Buffer;
9 +
10 + for (size_t Idx = start(); Idx != (start() + size()); Idx++)
11 + Buffer.push_back(V[Idx % V.size()]);
12 +
13 + return Buffer;
14 +}
15 +
16 +void BitBufferCounter::insert(bool Bit) {
17 + if (N >= V.size())
18 + NumSetBits -= (V[start()] == true);
19 +
20 + NumSetBits += (Bit == true);
21 + V[N++ % V.size()] = Bit;
22 +}
23 +
24 +void BitBufferCounter::print(std::ostream &OS) const {
25 + std::vector<bool> Buffer = getBuffer();
26 +
27 + for (bool B : Buffer)
28 + OS << B;
29 +}
ml/BitBufferCounter.h new
+54
@@ -0,0 +1,54 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef BIT_BUFFER_COUNTER_H
4 +#define BIT_BUFFER_COUNTER_H
5 +
6 +#include "ml-private.h"
7 +
8 +namespace ml {
9 +
10 +class BitBufferCounter {
11 +public:
12 + BitBufferCounter(size_t Capacity) : V(Capacity, 0), NumSetBits(0), N(0) {}
13 +
14 + std::vector<bool> getBuffer() const;
15 +
16 + void insert(bool Bit);
17 +
18 + void print(std::ostream &OS) const;
19 +
20 + bool isFilled() const {
21 + return N >= V.size();
22 + }
23 +
24 + size_t numSetBits() const {
25 + return NumSetBits;
26 + }
27 +
28 +private:
29 + inline size_t size() const {
30 + return N < V.size() ? N : V.size();
31 + }
32 +
33 + inline size_t start() const {
34 + if (N <= V.size())
35 + return 0;
36 +
37 + return N % V.size();
38 + }
39 +
40 +private:
41 + std::vector<bool> V;
42 + size_t NumSetBits;
43 +
44 + size_t N;
45 +};
46 +
47 +} // namespace ml
48 +
49 +inline std::ostream& operator<<(std::ostream &OS, const ml::BitBufferCounter &BBC) {
50 + BBC.print(OS);
51 + return OS;
52 +}
53 +
54 +#endif /* BIT_BUFFER_COUNTER_H */
ml/BitRateWindow.cc new
+75
@@ -0,0 +1,75 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#include "BitRateWindow.h"
4 +
5 +using namespace ml;
6 +
7 +std::pair<BitRateWindow::Edge, size_t> BitRateWindow::insert(bool Bit) {
8 + Edge E;
9 +
10 + BBC.insert(Bit);
11 + switch (CurrState) {
12 + case State::NotFilled: {
13 + if (BBC.isFilled()) {
14 + if (BBC.numSetBits() < SetBitsThreshold) {
15 + CurrState = State::BelowThreshold;
16 + } else {
17 + CurrState = State::AboveThreshold;
18 + }
19 + } else {
20 + CurrState = State::NotFilled;
21 + }
22 +
23 + E = {State::NotFilled, CurrState};
24 + break;
25 + } case State::BelowThreshold: {
26 + if (BBC.numSetBits() >= SetBitsThreshold) {
27 + CurrState = State::AboveThreshold;
28 + }
29 +
30 + E = {State::BelowThreshold, CurrState};
31 + break;
32 + } case State::AboveThreshold: {
33 + if ((BBC.numSetBits() < SetBitsThreshold) ||
34 + (CurrLength == MaxLength)) {
35 + CurrState = State::Idle;
36 + }
37 +
38 + E = {State::AboveThreshold, CurrState};
39 + break;
40 + } case State::Idle: {
41 + if (CurrLength == IdleLength) {
42 + CurrState = State::NotFilled;
43 + }
44 +
45 + E = {State::Idle, CurrState};
46 + break;
47 + }
48 + }
49 +
50 + Action A = EdgeActions[E];
51 + size_t L = (this->*A)(E.first, Bit);
52 + return {E, L};
53 +}
54 +
55 +void BitRateWindow::print(std::ostream &OS) const {
56 + switch (CurrState) {
57 + case State::NotFilled:
58 + OS << "NotFilled";
59 + break;
60 + case State::BelowThreshold:
61 + OS << "BelowThreshold";
62 + break;
63 + case State::AboveThreshold:
64 + OS << "AboveThreshold";
65 + break;
66 + case State::Idle:
67 + OS << "Idle";
68 + break;
69 + default:
70 + OS << "UnknownState";
71 + break;
72 + }
73 +
74 + OS << ": " << BBC << " (Current Length: " << CurrLength << ")";
75 +}
ml/BitRateWindow.h new
+170
@@ -0,0 +1,170 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef BIT_RATE_WINDOW_H
4 +#define BIT_RATE_WINDOW_H
5 +
6 +#include "BitBufferCounter.h"
7 +#include "ml-private.h"
8 +
9 +namespace ml {
10 +
11 +class BitRateWindow {
12 +public:
13 + enum class State {
14 + NotFilled,
15 + BelowThreshold,
16 + AboveThreshold,
17 + Idle
18 + };
19 +
20 + using Edge = std::pair<State, State>;
21 + using Action = size_t (BitRateWindow::*)(State PrevState, bool NewBit);
22 +
23 +private:
24 + std::map<Edge, Action> EdgeActions = {
25 + // From == To
26 + {
27 + Edge(State::NotFilled, State::NotFilled),
28 + &BitRateWindow::onRoundtripNotFilled,
29 + },
30 + {
31 + Edge(State::BelowThreshold, State::BelowThreshold),
32 + &BitRateWindow::onRoundtripBelowThreshold,
33 + },
34 + {
35 + Edge(State::AboveThreshold, State::AboveThreshold),
36 + &BitRateWindow::onRoundtripAboveThreshold,
37 + },
38 + {
39 + Edge(State::Idle, State::Idle),
40 + &BitRateWindow::onRoundtripIdle,
41 + },
42 +
43 +
44 + // NotFilled => {BelowThreshold, AboveThreshold}
45 + {
46 + Edge(State::NotFilled, State::BelowThreshold),
47 + &BitRateWindow::onNotFilledToBelowThreshold
48 + },
49 + {
50 + Edge(State::NotFilled, State::AboveThreshold),
51 + &BitRateWindow::onNotFilledToAboveThreshold
52 + },
53 +
54 + // BelowThreshold => AboveThreshold
55 + {
56 + Edge(State::BelowThreshold, State::AboveThreshold),
57 + &BitRateWindow::onBelowToAboveThreshold
58 + },
59 +
60 + // AboveThreshold => Idle
61 + {
62 + Edge(State::AboveThreshold, State::Idle),
63 + &BitRateWindow::onAboveThresholdToIdle
64 + },
65 +
66 + // Idle => NotFilled
67 + {
68 + Edge(State::Idle, State::NotFilled),
69 + &BitRateWindow::onIdleToNotFilled
70 + },
71 + };
72 +
73 +public:
74 + BitRateWindow(size_t MinLength, size_t MaxLength, size_t IdleLength,
75 + size_t SetBitsThreshold) :
76 + MinLength(MinLength), MaxLength(MaxLength), IdleLength(IdleLength),
77 + SetBitsThreshold(SetBitsThreshold),
78 + CurrState(State::NotFilled), CurrLength(0), BBC(MinLength) {}
79 +
80 + std::pair<Edge, size_t> insert(bool Bit);
81 +
82 + void print(std::ostream &OS) const;
83 +
84 +private:
85 + size_t onRoundtripNotFilled(State PrevState, bool NewBit) {
86 + (void) PrevState, (void) NewBit;
87 +
88 + CurrLength += 1;
89 + return CurrLength;
90 + }
91 +
92 + size_t onRoundtripBelowThreshold(State PrevState, bool NewBit) {
93 + (void) PrevState, (void) NewBit;
94 +
95 + CurrLength = MinLength;
96 + return CurrLength;
97 + }
98 +
99 + size_t onRoundtripAboveThreshold(State PrevState, bool NewBit) {
100 + (void) PrevState, (void) NewBit;
101 +
102 + CurrLength += 1;
103 + return CurrLength;
104 + }
105 +
106 + size_t onRoundtripIdle(State PrevState, bool NewBit) {
107 + (void) PrevState, (void) NewBit;
108 +
109 + CurrLength += 1;
110 + return CurrLength;
111 + }
112 +
113 + size_t onNotFilledToBelowThreshold(State PrevState, bool NewBit) {
114 + (void) PrevState, (void) NewBit;
115 +
116 + CurrLength = MinLength;
117 + return CurrLength;
118 + }
119 +
120 + size_t onNotFilledToAboveThreshold(State PrevState, bool NewBit) {
121 + (void) PrevState, (void) NewBit;
122 +
123 + CurrLength += 1;
124 + return CurrLength;
125 + }
126 +
127 + size_t onBelowToAboveThreshold(State PrevState, bool NewBit) {
128 + (void) PrevState, (void) NewBit;
129 +
130 + CurrLength = MinLength;
131 + return CurrLength;
132 + }
133 +
134 + size_t onAboveThresholdToIdle(State PrevState, bool NewBit) {
135 + (void) PrevState, (void) NewBit;
136 +
137 + size_t PrevLength = CurrLength;
138 + CurrLength = 1;
139 + return PrevLength;
140 + }
141 +
142 + size_t onIdleToNotFilled(State PrevState, bool NewBit) {
143 + (void) PrevState, (void) NewBit;
144 +
145 + BBC = BitBufferCounter(MinLength);
146 + BBC.insert(NewBit);
147 +
148 + CurrLength = 1;
149 + return CurrLength;
150 + }
151 +
152 +private:
153 + size_t MinLength;
154 + size_t MaxLength;
155 + size_t IdleLength;
156 + size_t SetBitsThreshold;
157 +
158 + State CurrState;
159 + size_t CurrLength;
160 + BitBufferCounter BBC;
161 +};
162 +
163 +} // namespace ml
164 +
165 +inline std::ostream& operator<<(std::ostream &OS, const ml::BitRateWindow BRW) {
166 + BRW.print(OS);
167 + return OS;
168 +}
169 +
170 +#endif /* BIT_RATE_WINDOW_H */
ml/Config.cc new
+128
@@ -0,0 +1,128 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#include "Config.h"
4 +#include "ml-private.h"
5 +
6 +using namespace ml;
7 +
8 +/*
9 + * Global configuration instance to be shared between training and
10 + * prediction threads.
11 + */
12 +Config ml::Cfg;
13 +
14 +template <typename T>
15 +static T clamp(const T& Value, const T& Min, const T& Max) {
16 + return std::max(Min, std::min(Value, Max));
17 +}
18 +
19 +/*
20 + * Initialize global configuration variable.
21 + */
22 +void Config::readMLConfig(void) {
23 + const char *ConfigSectionML = CONFIG_SECTION_ML;
24 +
25 + bool EnableAnomalyDetection = config_get_boolean(ConfigSectionML, "enabled", false);
26 +
27 + /*
28 + * Read values
29 + */
30 +
31 + unsigned MaxTrainSamples = config_get_number(ConfigSectionML, "maximum num samples to train", 4 * 3600);
32 + unsigned MinTrainSamples = config_get_number(ConfigSectionML, "minimum num samples to train", 1 * 3600);
33 + unsigned TrainEvery = config_get_number(ConfigSectionML, "train every", 1 * 3600);
34 +
35 + unsigned DiffN = config_get_number(ConfigSectionML, "num samples to diff", 1);
36 + unsigned SmoothN = config_get_number(ConfigSectionML, "num samples to smooth", 3);
37 + unsigned LagN = config_get_number(ConfigSectionML, "num samples to lag", 5);
38 +
39 + unsigned MaxKMeansIters = config_get_number(ConfigSectionML, "maximum number of k-means iterations", 1000);
40 +
41 + double DimensionAnomalyScoreThreshold = config_get_float(ConfigSectionML, "dimension anomaly score threshold", 0.99);
42 + double HostAnomalyRateThreshold = config_get_float(ConfigSectionML, "host anomaly rate threshold", 0.01);
43 +
44 + double ADMinWindowSize = config_get_float(ConfigSectionML, "minimum window size", 30);
45 + double ADMaxWindowSize = config_get_float(ConfigSectionML, "maximum window size", 600);
46 + double ADIdleWindowSize = config_get_float(ConfigSectionML, "idle window size", 30);
47 + double ADWindowRateThreshold = config_get_float(ConfigSectionML, "window minimum anomaly rate", 0.25);
48 + double ADDimensionRateThreshold = config_get_float(ConfigSectionML, "anomaly event min dimension rate threshold", 0.05);
49 +
50 + std::string HostsToSkip = config_get(ConfigSectionML, "hosts to skip from training", "!*");
51 + std::string ChartsToSkip = config_get(ConfigSectionML, "charts to skip from training",
52 + "!system.* !cpu.* !mem.* !disk.* !disk_* "
53 + "!ip.* !ipv4.* !ipv6.* !net.* !net_* !netfilter.* "
54 + "!services.* !apps.* !groups.* !user.* !ebpf.* !netdata.* *");
55 +
56 + std::stringstream SS;
57 + SS << netdata_configured_cache_dir << "/anomaly-detection.db";
58 + Cfg.AnomalyDBPath = SS.str();
59 +
60 + /*
61 + * Clamp
62 + */
63 +
64 + MaxTrainSamples = clamp(MaxTrainSamples, 1 * 3600u, 6 * 3600u);
65 + MinTrainSamples = clamp(MinTrainSamples, 1 * 3600u, 6 * 3600u);
66 + TrainEvery = clamp(TrainEvery, 1 * 3600u, 6 * 3600u);
67 +
68 + DiffN = clamp(DiffN, 0u, 1u);
69 + SmoothN = clamp(SmoothN, 0u, 5u);
70 + LagN = clamp(LagN, 0u, 5u);
71 +
72 + MaxKMeansIters = clamp(MaxKMeansIters, 500u, 1000u);
73 +
74 + DimensionAnomalyScoreThreshold = clamp(DimensionAnomalyScoreThreshold, 0.01, 5.00);
75 + HostAnomalyRateThreshold = clamp(HostAnomalyRateThreshold, 0.01, 1.0);
76 +
77 + ADMinWindowSize = clamp(ADMinWindowSize, 30.0, 300.0);
78 + ADMaxWindowSize = clamp(ADMaxWindowSize, 60.0, 900.0);
79 + ADIdleWindowSize = clamp(ADIdleWindowSize, 30.0, 900.0);
80 + ADWindowRateThreshold = clamp(ADWindowRateThreshold, 0.01, 0.99);
81 + ADDimensionRateThreshold = clamp(ADDimensionRateThreshold, 0.01, 0.99);
82 +
83 + /*
84 + * Validate
85 + */
86 +
87 + if (MinTrainSamples >= MaxTrainSamples) {
88 + error("invalid min/max train samples found (%d >= %d)", MinTrainSamples, MaxTrainSamples);
89 +
90 + MinTrainSamples = 1 * 3600;
91 + MaxTrainSamples = 4 * 3600;
92 + }
93 +
94 + if (ADMinWindowSize >= ADMaxWindowSize) {
95 + error("invalid min/max anomaly window size found (%lf >= %lf)", ADMinWindowSize, ADMaxWindowSize);
96 +
97 + ADMinWindowSize = 30.0;
98 + ADMaxWindowSize = 600.0;
99 + }
100 +
101 + /*
102 + * Assign to config instance
103 + */
104 +
105 + Cfg.EnableAnomalyDetection = EnableAnomalyDetection;
106 +
107 + Cfg.MaxTrainSamples = MaxTrainSamples;
108 + Cfg.MinTrainSamples = MinTrainSamples;
109 + Cfg.TrainEvery = TrainEvery;
110 +
111 + Cfg.DiffN = DiffN;
112 + Cfg.SmoothN = SmoothN;
113 + Cfg.LagN = LagN;
114 +
115 + Cfg.MaxKMeansIters = MaxKMeansIters;
116 +
117 + Cfg.DimensionAnomalyScoreThreshold = DimensionAnomalyScoreThreshold;
118 + Cfg.HostAnomalyRateThreshold = HostAnomalyRateThreshold;
119 +
120 + Cfg.ADMinWindowSize = ADMinWindowSize;
121 + Cfg.ADMaxWindowSize = ADMaxWindowSize;
122 + Cfg.ADIdleWindowSize = ADIdleWindowSize;
123 + Cfg.ADWindowRateThreshold = ADWindowRateThreshold;
124 + Cfg.ADDimensionRateThreshold = ADDimensionRateThreshold;
125 +
126 + Cfg.SP_HostsToSkip = simple_pattern_create(HostsToSkip.c_str(), NULL, SIMPLE_PATTERN_EXACT);
127 + Cfg.SP_ChartsToSkip = simple_pattern_create(ChartsToSkip.c_str(), NULL, SIMPLE_PATTERN_EXACT);
128 +}
ml/Config.h new
+45
@@ -0,0 +1,45 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef ML_CONFIG_H
4 +#define ML_CONFIG_H
5 +
6 +#include "ml-private.h"
7 +
8 +namespace ml {
9 +
10 +class Config {
11 +public:
12 + bool EnableAnomalyDetection;
13 +
14 + unsigned MaxTrainSamples;
15 + unsigned MinTrainSamples;
16 + unsigned TrainEvery;
17 +
18 + unsigned DiffN;
19 + unsigned SmoothN;
20 + unsigned LagN;
21 +
22 + unsigned MaxKMeansIters;
23 +
24 + double DimensionAnomalyScoreThreshold;
25 + double HostAnomalyRateThreshold;
26 +
27 + double ADMinWindowSize;
28 + double ADMaxWindowSize;
29 + double ADIdleWindowSize;
30 + double ADWindowRateThreshold;
31 + double ADDimensionRateThreshold;
32 +
33 + SIMPLE_PATTERN *SP_HostsToSkip;
34 + SIMPLE_PATTERN *SP_ChartsToSkip;
35 +
36 + std::string AnomalyDBPath;
37 +
38 + void readMLConfig();
39 +};
40 +
41 +extern Config Cfg;
42 +
43 +} // namespace ml
44 +
45 +#endif /* ML_CONFIG_H */
ml/Database.cc new
+127
@@ -0,0 +1,127 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#include "Database.h"
4 +
5 +const char *ml::Database::SQL_CREATE_ANOMALIES_TABLE =
6 + "CREATE TABLE IF NOT EXISTS anomaly_events( "
7 + " anomaly_detector_name text NOT NULL, "
8 + " anomaly_detector_version int NOT NULL, "
9 + " host_id text NOT NULL, "
10 + " after int NOT NULL, "
11 + " before int NOT NULL, "
12 + " anomaly_event_info text, "
13 + " PRIMARY KEY( "
14 + " anomaly_detector_name, anomaly_detector_version, "
15 + " host_id, after, before "
16 + " ) "
17 + ");";
18 +
19 +const char *ml::Database::SQL_INSERT_ANOMALY =
20 + "INSERT INTO anomaly_events( "
21 + " anomaly_detector_name, anomaly_detector_version, "
22 + " host_id, after, before, anomaly_event_info) "
23 + "VALUES (?1, ?2, ?3, ?4, ?5, ?6);";
24 +
25 +const char *ml::Database::SQL_SELECT_ANOMALY =
26 + "SELECT anomaly_event_info FROM anomaly_events WHERE"
27 + " anomaly_detector_name == ?1 AND"
28 + " anomaly_detector_version == ?2 AND"
29 + " host_id == ?3 AND"
30 + " after == ?4 AND"
31 + " before == ?5;";
32 +
33 +const char *ml::Database::SQL_SELECT_ANOMALY_EVENTS =
34 + "SELECT after, before FROM anomaly_events WHERE"
35 + " anomaly_detector_name == ?1 AND"
36 + " anomaly_detector_version == ?2 AND"
37 + " host_id == ?3 AND"
38 + " after >= ?4 AND"
39 + " before <= ?5;";
40 +
41 +using namespace ml;
42 +
43 +bool Statement::prepare(sqlite3 *Conn) {
44 + if (!Conn)
45 + return false;
46 +
47 + if (ParsedStmt)
48 + return true;
49 +
50 + int RC = sqlite3_prepare_v2(Conn, RawStmt, -1, &ParsedStmt, nullptr);
51 + if (RC == SQLITE_OK)
52 + return true;
53 +
54 + std::string Msg = "Statement \"%s\" preparation failed due to \"%s\"";
55 + error(Msg.c_str(), RawStmt, sqlite3_errstr(RC));
56 +
57 + return false;
58 +}
59 +
60 +bool Statement::bindValue(size_t Pos, const std::string &Value) {
61 + int RC = sqlite3_bind_text(ParsedStmt, Pos, Value.c_str(), -1, SQLITE_TRANSIENT);
62 + if (RC == SQLITE_OK)
63 + return true;
64 +
65 + error("Failed to bind text '%s' (pos = %zu) in statement '%s'.", Value.c_str(), Pos, RawStmt);
66 + return false;
67 +}
68 +
69 +bool Statement::bindValue(size_t Pos, const int Value) {
70 + int RC = sqlite3_bind_int(ParsedStmt, Pos, Value);
71 + if (RC == SQLITE_OK)
72 + return true;
73 +
74 + error("Failed to bind integer %d (pos = %zu) in statement '%s'.", Value, Pos, RawStmt);
75 + return false;
76 +}
77 +
78 +bool Statement::resetAndClear(bool Ret) {
79 + int RC = sqlite3_reset(ParsedStmt);
80 + if (RC != SQLITE_OK) {
81 + error("Could not reset statement: '%s'", RawStmt);
82 + return false;
83 + }
84 +
85 + RC = sqlite3_clear_bindings(ParsedStmt);
86 + if (RC != SQLITE_OK) {
87 + error("Could not clear bindings in statement: '%s'", RawStmt);
88 + return false;
89 + }
90 +
91 + return Ret;
92 +}
93 +
94 +Database::Database(const std::string &Path) {
95 + // Get sqlite3 connection handle.
96 + int RC = sqlite3_open(Path.c_str(), &Conn);
97 + if (RC != SQLITE_OK) {
98 + std::string Msg = "Failed to initialize ML DB at %s, due to \"%s\"";
99 + error(Msg.c_str(), Path.c_str(), sqlite3_errstr(RC));
100 +
101 + sqlite3_close(Conn);
102 + Conn = nullptr;
103 + return;
104 + }
105 +
106 + // Create anomaly events table if it does not exist.
107 + char *ErrMsg;
108 + RC = sqlite3_exec(Conn, SQL_CREATE_ANOMALIES_TABLE, nullptr, nullptr, &ErrMsg);
109 + if (RC == SQLITE_OK)
110 + return;
111 +
112 + error("SQLite error during database initialization, rc = %d (%s)", RC, ErrMsg);
113 + error("SQLite failed statement: %s", SQL_CREATE_ANOMALIES_TABLE);
114 +
115 + sqlite3_free(ErrMsg);
116 + sqlite3_close(Conn);
117 + Conn = nullptr;
118 +}
119 +
120 +Database::~Database() {
121 + if (!Conn)
122 + return;
123 +
124 + int RC = sqlite3_close(Conn);
125 + if (RC != SQLITE_OK)
126 + error("Could not close connection properly (rc=%d)", RC);
127 +}
ml/Database.h new
+131
@@ -0,0 +1,131 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef ML_DATABASE_H
4 +#define ML_DATABASE_H
5 +
6 +#include "Dimension.h"
7 +#include "ml-private.h"
8 +
9 +#include "json/single_include/nlohmann/json.hpp"
10 +
11 +namespace ml {
12 +
13 +class Statement {
14 +public:
15 + using RowCallback = std::function<void(sqlite3_stmt *Stmt)>;
16 +
17 +public:
18 + Statement(const char *RawStmt) : RawStmt(RawStmt), ParsedStmt(nullptr) {}
19 +
20 + template<typename ...ArgTypes>
21 + bool exec(sqlite3 *Conn, RowCallback RowCb, ArgTypes ...Args) {
22 + if (!prepare(Conn))
23 + return false;
24 +
25 + switch (bind(1, Args...)) {
26 + case 0:
27 + return false;
28 + case sizeof...(Args):
29 + break;
30 + default:
31 + return resetAndClear(false);
32 + }
33 +
34 + while (true) {
35 + switch (int RC = sqlite3_step(ParsedStmt)) {
36 + case SQLITE_BUSY: case SQLITE_LOCKED:
37 + usleep(SQLITE_INSERT_DELAY * USEC_PER_MS);
38 + continue;
39 + case SQLITE_ROW:
40 + RowCb(ParsedStmt);
41 + continue;
42 + case SQLITE_DONE:
43 + return resetAndClear(true);
44 + default:
45 + error("Stepping through '%s' returned rc=%d", RawStmt, RC);
46 + return resetAndClear(false);
47 + }
48 + }
49 + }
50 +
51 + ~Statement() {
52 + if (!ParsedStmt)
53 + return;
54 +
55 + int RC = sqlite3_finalize(ParsedStmt);
56 + if (RC != SQLITE_OK)
57 + error("Could not properly finalize statement (rc=%d)", RC);
58 + }
59 +
60 +private:
61 + bool prepare(sqlite3 *Conn);
62 +
63 + bool bindValue(size_t Pos, const int Value);
64 + bool bindValue(size_t Pos, const std::string &Value);
65 +
66 + template<typename ArgType, typename ...ArgTypes>
67 + size_t bind(size_t Pos, ArgType T) {
68 + return bindValue(Pos, T);
69 + }
70 +
71 + template<typename ArgType, typename ...ArgTypes>
72 + size_t bind(size_t Pos, ArgType T, ArgTypes ...Args) {
73 + return bindValue(Pos, T) + bind(Pos + 1, Args...);
74 + }
75 +
76 + bool resetAndClear(bool Ret);
77 +
78 +private:
79 + const char *RawStmt;
80 + sqlite3_stmt *ParsedStmt;
81 +};
82 +
83 +class Database {
84 +private:
85 + static const char *SQL_CREATE_ANOMALIES_TABLE;
86 + static const char *SQL_INSERT_ANOMALY;
87 + static const char *SQL_SELECT_ANOMALY;
88 + static const char *SQL_SELECT_ANOMALY_EVENTS;
89 +
90 +public:
91 + Database(const std::string &Path);
92 +
93 + ~Database();
94 +
95 + template<typename ...ArgTypes>
96 + bool insertAnomaly(ArgTypes... Args) {
97 + Statement::RowCallback RowCb = [](sqlite3_stmt *Stmt) { (void) Stmt; };
98 + return InsertAnomalyStmt.exec(Conn, RowCb, Args...);
99 + }
100 +
101 + template<typename ...ArgTypes>
102 + bool getAnomalyInfo(nlohmann::json &Json, ArgTypes&&... Args) {
103 + Statement::RowCallback RowCb = [&](sqlite3_stmt *Stmt) {
104 + const char *Text = static_cast<const char *>(sqlite3_column_blob(Stmt, 0));
105 + Json = nlohmann::json::parse(Text);
106 + };
107 + return GetAnomalyInfoStmt.exec(Conn, RowCb, Args...);
108 + }
109 +
110 + template<typename ...ArgTypes>
111 + bool getAnomaliesInRange(std::vector<std::pair<time_t, time_t>> &V, ArgTypes&&... Args) {
112 + Statement::RowCallback RowCb = [&](sqlite3_stmt *Stmt) {
113 + V.push_back({
114 + sqlite3_column_int64(Stmt, 0),
115 + sqlite3_column_int64(Stmt, 1)
116 + });
117 + };
118 + return GetAnomaliesInRangeStmt.exec(Conn, RowCb, Args...);
119 + }
120 +
121 +private:
122 + sqlite3 *Conn;
123 +
124 + Statement InsertAnomalyStmt{SQL_INSERT_ANOMALY};
125 + Statement GetAnomalyInfoStmt{SQL_SELECT_ANOMALY};
126 + Statement GetAnomaliesInRangeStmt{SQL_SELECT_ANOMALY_EVENTS};
127 +};
128 +
129 +}
130 +
131 +#endif /* ML_DATABASE_H */
ml/Dimension.cc new
+169
@@ -0,0 +1,169 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#include "Config.h"
4 +#include "Dimension.h"
5 +#include "Query.h"
6 +
7 +using namespace ml;
8 +
9 +/*
10 + * Copy of the unpack_storage_number which allows us to convert
11 + * a storage_number to double.
12 + */
13 +static CalculatedNumber unpack_storage_number_dbl(storage_number value) {
14 + if(!value)
15 + return 0;
16 +
17 + int sign = 0, exp = 0;
18 + int factor = 10;
19 +
20 + // bit 32 = 0:positive, 1:negative
21 + if(unlikely(value & (1 << 31)))
22 + sign = 1;
23 +
24 + // bit 31 = 0:divide, 1:multiply
25 + if(unlikely(value & (1 << 30)))
26 + exp = 1;
27 +
28 + // bit 27 SN_EXISTS_100
29 + if(unlikely(value & (1 << 26)))
30 + factor = 100;
31 +
32 + // bit 26 SN_EXISTS_RESET
33 + // bit 25 SN_ANOMALY_BIT
34 +
35 + // bit 30, 29, 28 = (multiplier or divider) 0-7 (8 total)
36 + int mul = (value & ((1<<29)|(1<<28)|(1<<27))) >> 27;
37 +
38 + // bit 24 to bit 1 = the value, so remove all other bits
39 + value ^= value & ((1<<31)|(1<<30)|(1<<29)|(1<<28)|(1<<27)|(1<<26)|(1<<25)|(1<<24));
40 +
41 + CalculatedNumber CN = value;
42 +
43 + if(exp) {
44 + for(; mul; mul--)
45 + CN *= factor;
46 + }
47 + else {
48 + for( ; mul ; mul--)
49 + CN /= 10;
50 + }
51 +
52 + if(sign)
53 + CN = -CN;
54 +
55 + return CN;
56 +}
57 +
58 +std::pair<CalculatedNumber *, size_t>
59 +TrainableDimension::getCalculatedNumbers() {
60 + size_t MinN = Cfg.MinTrainSamples;
61 + size_t MaxN = Cfg.MaxTrainSamples;
62 +
63 + // Figure out what our time window should be.
64 + time_t BeforeT = now_realtime_sec() - 1;
65 + time_t AfterT = BeforeT - (MaxN * updateEvery());
66 +
67 + BeforeT -= (BeforeT % updateEvery());
68 + AfterT -= (AfterT % updateEvery());
69 +
70 + BeforeT = std::min(BeforeT, latestTime());
71 + AfterT = std::max(AfterT, oldestTime());
72 +
73 + if (AfterT >= BeforeT)
74 + return { nullptr, 0 };
75 +
76 + CalculatedNumber *CNs = new CalculatedNumber[MaxN * (Cfg.LagN + 1)]();
77 +
78 + // Start the query.
79 + unsigned Idx = 0;
80 + unsigned CollectedValues = 0;
81 + unsigned TotalValues = 0;
82 +
83 + CalculatedNumber LastValue = std::numeric_limits<CalculatedNumber>::quiet_NaN();
84 + Query Q = Query(getRD());
85 +
86 + Q.init(AfterT, BeforeT);
87 + while (!Q.isFinished()) {
88 + if (Idx == MaxN)
89 + break;
90 +
91 + auto P = Q.nextMetric();
92 + storage_number SN = P.second;
93 +
94 + if (does_storage_number_exist(SN)) {
95 + CNs[Idx] = unpack_storage_number_dbl(SN);
96 + LastValue = CNs[Idx];
97 + CollectedValues++;
98 + } else
99 + CNs[Idx] = LastValue;
100 +
101 + Idx++;
102 + }
103 + TotalValues = Idx;
104 +
105 + if (CollectedValues < MinN) {
106 + delete[] CNs;
107 + return { nullptr, 0 };
108 + }
109 +
110 + // Find first non-NaN value.
111 + for (Idx = 0; std::isnan(CNs[Idx]); Idx++, TotalValues--) { }
112 +
113 + // Overwrite NaN values.
114 + if (Idx != 0)
115 + memmove(CNs, &CNs[Idx], sizeof(CalculatedNumber) * TotalValues);
116 +
117 + return { CNs, TotalValues };
118 +}
119 +
120 +MLResult TrainableDimension::trainModel() {
121 + auto P = getCalculatedNumbers();
122 + CalculatedNumber *CNs = P.first;
123 + unsigned N = P.second;
124 +
125 + if (!CNs)
126 + return MLResult::MissingData;
127 +
128 + SamplesBuffer SB = SamplesBuffer(CNs, N, 1, Cfg.DiffN, Cfg.SmoothN, Cfg.LagN);
129 + KM.train(SB, Cfg.MaxKMeansIters);
130 + Trained = true;
131 +
132 + delete[] CNs;
133 + return MLResult::Success;
134 +}
135 +
136 +void PredictableDimension::addValue(CalculatedNumber Value, bool Exists) {
137 + if (!Exists) {
138 + CNs.clear();
139 + return;
140 + }
141 +
142 + unsigned N = Cfg.DiffN + Cfg.SmoothN + Cfg.LagN;
143 + if (CNs.size() < N) {
144 + CNs.push_back(Value);
145 + return;
146 + }
147 +
148 + std::rotate(std::begin(CNs), std::begin(CNs) + 1, std::end(CNs));
149 + CNs[N - 1] = Value;
150 +}
151 +
152 +std::pair<MLResult, bool> PredictableDimension::predict() {
153 + unsigned N = Cfg.DiffN + Cfg.SmoothN + Cfg.LagN;
154 + if (CNs.size() != N)
155 + return { MLResult::MissingData, AnomalyBit };
156 +
157 + CalculatedNumber *TmpCNs = new CalculatedNumber[N * (Cfg.LagN + 1)]();
158 + std::memcpy(TmpCNs, CNs.data(), N * sizeof(CalculatedNumber));
159 +
160 + SamplesBuffer SB = SamplesBuffer(TmpCNs, N, 1, Cfg.DiffN, Cfg.SmoothN, Cfg.LagN);
161 + AnomalyScore = computeAnomalyScore(SB);
162 + delete[] TmpCNs;
163 +
164 + if (AnomalyScore == std::numeric_limits<CalculatedNumber>::quiet_NaN())
165 + return { MLResult::NaN, AnomalyBit };
166 +
167 + AnomalyBit = AnomalyScore >= (100 * Cfg.DimensionAnomalyScoreThreshold);
168 + return { MLResult::Success, AnomalyBit };
169 +}
ml/Dimension.h new
+124
@@ -0,0 +1,124 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef ML_DIMENSION_H
4 +#define ML_DIMENSION_H
5 +
6 +#include "BitBufferCounter.h"
7 +#include "Config.h"
8 +
9 +#include "ml-private.h"
10 +
11 +namespace ml {
12 +
13 +class RrdDimension {
14 +public:
15 + RrdDimension(RRDDIM *RD) : RD(RD), Ops(&RD->state->query_ops) {
16 + std::stringstream SS;
17 + SS << RD->rrdset->id << "|" << RD->name;
18 + ID = SS.str();
19 + }
20 +
21 + RRDDIM *getRD() const { return RD; }
22 +
23 + time_t latestTime() { return Ops->latest_time(RD); }
24 +
25 + time_t oldestTime() { return Ops->oldest_time(RD); }
26 +
27 + unsigned updateEvery() const { return RD->update_every; }
28 +
29 + const std::string getID() const { return ID; }
30 +
31 + virtual ~RrdDimension() {}
32 +
33 +private:
34 + RRDDIM *RD;
35 + struct rrddim_volatile::rrddim_query_ops *Ops;
36 +
37 + std::string ID;
38 +};
39 +
40 +enum class MLResult {
41 + Success = 0,
42 + MissingData,
43 + NaN,
44 +};
45 +
46 +class TrainableDimension : public RrdDimension {
47 +public:
48 + TrainableDimension(RRDDIM *RD) :
49 + RrdDimension(RD), TrainEvery(Cfg.TrainEvery * updateEvery()) {}
50 +
51 + MLResult trainModel();
52 +
53 + CalculatedNumber computeAnomalyScore(SamplesBuffer &SB) {
54 + return Trained ? KM.anomalyScore(SB) : 0.0;
55 + }
56 +
57 + bool shouldTrain(const TimePoint &TP) const {
58 + return (LastTrainedAt + TrainEvery) < TP;
59 + }
60 +
61 + bool isTrained() const { return Trained; }
62 +
63 + double updateTrainingDuration(double Duration) {
64 + return TrainingDuration.exchange(Duration);
65 + }
66 +
67 +private:
68 + std::pair<CalculatedNumber *, size_t> getCalculatedNumbers();
69 +
70 +public:
71 + TimePoint LastTrainedAt{Seconds{0}};
72 +
73 +private:
74 + Seconds TrainEvery;
75 + KMeans KM;
76 +
77 + std::atomic<bool> Trained{false};
78 + std::atomic<double> TrainingDuration{0.0};
79 +};
80 +
81 +class PredictableDimension : public TrainableDimension {
82 +public:
83 + PredictableDimension(RRDDIM *RD) : TrainableDimension(RD) {}
84 +
85 + std::pair<MLResult, bool> predict();
86 +
87 + void addValue(CalculatedNumber Value, bool Exists);
88 +
89 + bool isAnomalous() { return AnomalyBit; }
90 +
91 +private:
92 + CalculatedNumber AnomalyScore{0.0};
93 + std::atomic<bool> AnomalyBit{false};
94 +
95 + std::vector<CalculatedNumber> CNs;
96 +};
97 +
98 +class DetectableDimension : public PredictableDimension {
99 +public:
100 + DetectableDimension(RRDDIM *RD) : PredictableDimension(RD) {}
101 +
102 + std::pair<bool, double> detect(size_t WindowLength, bool Reset) {
103 + bool AnomalyBit = isAnomalous();
104 +
105 + if (Reset)
106 + NumSetBits = BBC.numSetBits();
107 +
108 + NumSetBits += AnomalyBit;
109 + BBC.insert(AnomalyBit);
110 +
111 + double AnomalyRate = static_cast<double>(NumSetBits) / WindowLength;
112 + return { AnomalyBit, AnomalyRate };
113 + }
114 +
115 +private:
116 + BitBufferCounter BBC{static_cast<size_t>(Cfg.ADMinWindowSize)};
117 + size_t NumSetBits{0};
118 +};
119 +
120 +using Dimension = DetectableDimension;
121 +
122 +} // namespace ml
123 +
124 +#endif /* ML_DIMENSION_H */
ml/Host.cc new
+458
@@ -0,0 +1,458 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#include <dlib/statistics.h>
4 +
5 +#include "Config.h"
6 +#include "Host.h"
7 +
8 +#include "json/single_include/nlohmann/json.hpp"
9 +
10 +using namespace ml;
11 +
12 +static void updateDimensionsChart(RRDHOST *RH,
13 + collected_number NumTrainedDimensions,
14 + collected_number NumNormalDimensions,
15 + collected_number NumAnomalousDimensions) {
16 + static thread_local RRDSET *RS = nullptr;
17 + static thread_local RRDDIM *NumTotalDimensionsRD = nullptr;
18 + static thread_local RRDDIM *NumTrainedDimensionsRD = nullptr;
19 + static thread_local RRDDIM *NumNormalDimensionsRD = nullptr;
20 + static thread_local RRDDIM *NumAnomalousDimensionsRD = nullptr;
21 +
22 + if (!RS) {
23 + RS = rrdset_create(
24 + RH, // host
25 + "anomaly_detection", // type
26 + "dimensions", // id
27 + NULL, // name
28 + "dimensions", // family
29 + NULL, // ctx
30 + "Anomaly detection dimensions", // title
31 + "dimensions", // units
32 + "netdata", // plugin
33 + "ml", // module
34 + 39183, // priority
35 + RH->rrd_update_every, // update_every
36 + RRDSET_TYPE_LINE // chart_type
37 + );
38 +
39 + NumTotalDimensionsRD = rrddim_add(RS, "total", NULL,
40 + 1, 1, RRD_ALGORITHM_ABSOLUTE);
41 + NumTrainedDimensionsRD = rrddim_add(RS, "trained", NULL,
42 + 1, 1, RRD_ALGORITHM_ABSOLUTE);
43 + NumNormalDimensionsRD = rrddim_add(RS, "normal", NULL,
44 + 1, 1, RRD_ALGORITHM_ABSOLUTE);
45 + NumAnomalousDimensionsRD = rrddim_add(RS, "anomalous", NULL,
46 + 1, 1, RRD_ALGORITHM_ABSOLUTE);
47 + } else
48 + rrdset_next(RS);
49 +
50 + rrddim_set_by_pointer(RS, NumTotalDimensionsRD, NumNormalDimensions + NumAnomalousDimensions);
51 + rrddim_set_by_pointer(RS, NumTrainedDimensionsRD, NumTrainedDimensions);
52 + rrddim_set_by_pointer(RS, NumNormalDimensionsRD, NumNormalDimensions);
53 + rrddim_set_by_pointer(RS, NumAnomalousDimensionsRD, NumAnomalousDimensions);
54 +
55 + rrdset_done(RS);
56 +}
57 +
58 +static void updateRateChart(RRDHOST *RH, collected_number AnomalyRate) {
59 + static thread_local RRDSET *RS = nullptr;
60 + static thread_local RRDDIM *AnomalyRateRD = nullptr;
61 +
62 + if (!RS) {
63 + RS = rrdset_create(
64 + RH, // host
65 + "anomaly_detection", // type
66 + "anomaly_rate", // id
67 + NULL, // name
68 + "anomaly_rate", // family
69 + NULL, // ctx
70 + "Percentage of anomalous dimensions", // title
71 + "percentage", // units
72 + "netdata", // plugin
73 + "ml", // module
74 + 39184, // priority
75 + RH->rrd_update_every, // update_every
76 + RRDSET_TYPE_LINE // chart_type
77 + );
78 +
79 + AnomalyRateRD = rrddim_add(RS, "anomaly_rate", NULL,
80 + 1, 1, RRD_ALGORITHM_ABSOLUTE);
81 + } else
82 + rrdset_next(RS);
83 +
84 + rrddim_set_by_pointer(RS, AnomalyRateRD, AnomalyRate);
85 +
86 + rrdset_done(RS);
87 +}
88 +
89 +static void updateWindowLengthChart(RRDHOST *RH, collected_number WindowLength) {
90 + static thread_local RRDSET *RS = nullptr;
91 + static thread_local RRDDIM *WindowLengthRD = nullptr;
92 +
93 + if (!RS) {
94 + RS = rrdset_create(
95 + RH, // host
96 + "anomaly_detection", // type
97 + "detector_window", // id
98 + NULL, // name
99 + "detector_window", // family
100 + NULL, // ctx
101 + "Anomaly detector window length", // title
102 + "seconds", // units
103 + "netdata", // plugin
104 + "ml", // module
105 + 39185, // priority
106 + RH->rrd_update_every, // update_every
107 + RRDSET_TYPE_LINE // chart_type
108 + );
109 +
110 + WindowLengthRD = rrddim_add(RS, "duration", NULL,
111 + 1, 1, RRD_ALGORITHM_ABSOLUTE);
112 + } else
113 + rrdset_next(RS);
114 +
115 + rrddim_set_by_pointer(RS, WindowLengthRD, WindowLength * RH->rrd_update_every);
116 + rrdset_done(RS);
117 +}
118 +
119 +static void updateEventsChart(RRDHOST *RH,
120 + std::pair<BitRateWindow::Edge, size_t> P,
121 + bool ResetBitCounter,
122 + bool NewAnomalyEvent) {
123 + static thread_local RRDSET *RS = nullptr;
124 + static thread_local RRDDIM *AboveThresholdRD = nullptr;
125 + static thread_local RRDDIM *ResetBitCounterRD = nullptr;
126 + static thread_local RRDDIM *NewAnomalyEventRD = nullptr;
127 +
128 + if (!RS) {
129 + RS = rrdset_create(
130 + RH, // host
131 + "anomaly_detection", // type
132 + "detector_events", // id
133 + NULL, // name
134 + "detector_events", // family
135 + NULL, // ctx
136 + "Anomaly events triggered", // title
137 + "boolean", // units
138 + "netdata", // plugin
139 + "ml", // module
140 + 39186, // priority
141 + RH->rrd_update_every, // update_every
142 + RRDSET_TYPE_LINE // chart_type
143 + );
144 +
145 + AboveThresholdRD = rrddim_add(RS, "above_threshold", NULL,
146 + 1, 1, RRD_ALGORITHM_ABSOLUTE);
147 + ResetBitCounterRD = rrddim_add(RS, "reset_bit_counter", NULL,
148 + 1, 1, RRD_ALGORITHM_ABSOLUTE);
149 + NewAnomalyEventRD = rrddim_add(RS, "new_anomaly_event", NULL,
150 + 1, 1, RRD_ALGORITHM_ABSOLUTE);
151 + } else
152 + rrdset_next(RS);
153 +
154 + BitRateWindow::Edge E = P.first;
155 + bool AboveThreshold = E.second == BitRateWindow::State::AboveThreshold;
156 +
157 + rrddim_set_by_pointer(RS, AboveThresholdRD, AboveThreshold);
158 + rrddim_set_by_pointer(RS, ResetBitCounterRD, ResetBitCounter);
159 + rrddim_set_by_pointer(RS, NewAnomalyEventRD, NewAnomalyEvent);
160 +
161 + rrdset_done(RS);
162 +}
163 +
164 +static void updateDetectionChart(RRDHOST *RH, collected_number PredictionDuration) {
165 + static thread_local RRDSET *RS = nullptr;
166 + static thread_local RRDDIM *PredictiobDurationRD = nullptr;
167 +
168 + if (!RS) {
169 + RS = rrdset_create(
170 + RH, // host
171 + "anomaly_detection", // type
172 + "prediction_stats", // id
173 + NULL, // name
174 + "prediction_stats", // family
175 + NULL, // ctx
176 + "Time it took to run prediction", // title
177 + "milliseconds", // units
178 + "netdata", // plugin
179 + "ml", // module
180 + 39187, // priority
181 + RH->rrd_update_every, // update_every
182 + RRDSET_TYPE_LINE // chart_type
183 + );
184 +
185 + PredictiobDurationRD = rrddim_add(RS, "duration", NULL,
186 + 1, 1, RRD_ALGORITHM_ABSOLUTE);
187 + } else
188 + rrdset_next(RS);
189 +
190 + rrddim_set_by_pointer(RS, PredictiobDurationRD, PredictionDuration);
191 +
192 + rrdset_done(RS);
193 +}
194 +
195 +static void updateTrainingChart(RRDHOST *RH,
196 + collected_number TotalTrainingDuration,
197 + collected_number MaxTrainingDuration)
198 +{
199 + static thread_local RRDSET *RS = nullptr;
200 + static thread_local RRDDIM *TotalTrainingDurationRD = nullptr;
201 + static thread_local RRDDIM *MaxTrainingDurationRD = nullptr;
202 +
203 + if (!RS) {
204 + RS = rrdset_create(
205 + RH, // host
206 + "anomaly_detection", // type
207 + "training_stats", // id
208 + NULL, // name
209 + "training_stats", // family
210 + NULL, // ctx
211 + "Training step statistics", // title
212 + "milliseconds", // units
213 + "netdata", // plugin
214 + "ml", // module
215 + 39188, // priority
216 + RH->rrd_update_every, // update_every
217 + RRDSET_TYPE_LINE // chart_type
218 + );
219 +
220 + TotalTrainingDurationRD = rrddim_add(RS, "total_training_duration", NULL,
221 + 1, 1, RRD_ALGORITHM_ABSOLUTE);
222 + MaxTrainingDurationRD = rrddim_add(RS, "max_training_duration", NULL,
223 + 1, 1, RRD_ALGORITHM_ABSOLUTE);
224 + } else
225 + rrdset_next(RS);
226 +
227 + rrddim_set_by_pointer(RS, TotalTrainingDurationRD, TotalTrainingDuration);
228 + rrddim_set_by_pointer(RS, MaxTrainingDurationRD, MaxTrainingDuration);
229 +
230 + rrdset_done(RS);
231 +}
232 +
233 +void RrdHost::addDimension(Dimension *D) {
234 + std::lock_guard<std::mutex> Lock(Mutex);
235 +
236 + DimensionsMap[D->getRD()] = D;
237 +
238 + // Default construct mutex for dimension
239 + LocksMap[D];
240 +}
241 +
242 +void RrdHost::removeDimension(Dimension *D) {
243 + // Remove the dimension from the hosts map.
244 + {
245 + std::lock_guard<std::mutex> Lock(Mutex);
246 + DimensionsMap.erase(D->getRD());
247 + }
248 +
249 + // Delete the dimension by locking the mutex that protects it.
250 + {
251 + std::lock_guard<std::mutex> Lock(LocksMap[D]);
252 + delete D;
253 + }
254 +
255 + // Remove the lock entry for the deleted dimension.
256 + {
257 + std::lock_guard<std::mutex> Lock(Mutex);
258 + LocksMap.erase(D);
259 + }
260 +}
261 +
262 +void RrdHost::getConfigAsJson(nlohmann::json &Json) const {
263 + Json["version"] = 1;
264 +
265 + Json["enabled"] = Cfg.EnableAnomalyDetection;
266 +
267 + Json["min-train-samples"] = Cfg.MinTrainSamples;
268 + Json["max-train-samples"] = Cfg.MaxTrainSamples;
269 + Json["train-every"] = Cfg.TrainEvery;
270 +
271 + Json["diff-n"] = Cfg.DiffN;
272 + Json["smooth-n"] = Cfg.SmoothN;
273 + Json["lag-n"] = Cfg.LagN;
274 +
275 + Json["max-kmeans-iters"] = Cfg.MaxKMeansIters;
276 +
277 + Json["dimension-anomaly-score-threshold"] = Cfg.DimensionAnomalyScoreThreshold;
278 + Json["host-anomaly-rate-threshold"] = Cfg.HostAnomalyRateThreshold;
279 +
280 + Json["min-window-size"] = Cfg.ADMinWindowSize;
281 + Json["max-window-size"] = Cfg.ADMaxWindowSize;
282 + Json["idle-window-size"] = Cfg.ADIdleWindowSize;
283 + Json["window-rate-threshold"] = Cfg.ADWindowRateThreshold;
284 + Json["dimension-rate-threshold"] = Cfg.ADDimensionRateThreshold;
285 +}
286 +
287 +std::pair<Dimension *, Duration<double>>
288 +TrainableHost::findDimensionToTrain(const TimePoint &NowTP) {
289 + std::lock_guard<std::mutex> Lock(Mutex);
290 +
291 + Duration<double> AllottedDuration = Duration<double>{Cfg.TrainEvery * updateEvery()} / (DimensionsMap.size() + 1);
292 +
293 + for (auto &DP : DimensionsMap) {
294 + Dimension *D = DP.second;
295 +
296 + if (D->shouldTrain(NowTP)) {
297 + LocksMap[D].lock();
298 + return { D, AllottedDuration };
299 + }
300 + }
301 +
302 + return { nullptr, AllottedDuration };
303 +}
304 +
305 +void TrainableHost::trainDimension(Dimension *D, const TimePoint &NowTP) {
306 + if (D == nullptr)
307 + return;
308 +
309 + D->LastTrainedAt = NowTP + Seconds{D->updateEvery()};
310 +
311 + TimePoint StartTP = SteadyClock::now();
312 + D->trainModel();
313 + Duration<double> Duration = SteadyClock::now() - StartTP;
314 + D->updateTrainingDuration(Duration.count());
315 +
316 + {
317 + std::lock_guard<std::mutex> Lock(Mutex);
318 + LocksMap[D].unlock();
319 + }
320 +}
321 +
322 +void TrainableHost::train() {
323 + Duration<double> MaxSleepFor = Seconds{updateEvery()};
324 +
325 + while (!netdata_exit) {
326 + TimePoint NowTP = SteadyClock::now();
327 +
328 + auto P = findDimensionToTrain(NowTP);
329 + trainDimension(P.first, NowTP);
330 +
331 + Duration<double> AllottedDuration = P.second;
332 + Duration<double> RealDuration = SteadyClock::now() - NowTP;
333 +
334 + Duration<double> SleepFor;
335 + if (RealDuration >= AllottedDuration)
336 + continue;
337 +
338 + SleepFor = std::min(AllottedDuration - RealDuration, MaxSleepFor);
339 + std::this_thread::sleep_for(SleepFor);
340 + }
341 +}
342 +
343 +void DetectableHost::detectOnce() {
344 + auto P = BRW.insert(AnomalyRate >= Cfg.HostAnomalyRateThreshold);
345 + BitRateWindow::Edge Edge = P.first;
346 + size_t WindowLength = P.second;
347 +
348 + bool ResetBitCounter = (Edge.first != BitRateWindow::State::AboveThreshold);
349 + bool NewAnomalyEvent = (Edge.first == BitRateWindow::State::AboveThreshold) &&
350 + (Edge.second == BitRateWindow::State::Idle);
351 +
352 + std::vector<std::pair<double, std::string>> DimsOverThreshold;
353 +
354 + size_t NumAnomalousDimensions = 0;
355 + size_t NumNormalDimensions = 0;
356 + size_t NumTrainedDimensions = 0;
357 +
358 + double TotalTrainingDuration = 0.0;
359 + double MaxTrainingDuration = 0.0;
360 +
361 + {
362 + std::lock_guard<std::mutex> Lock(Mutex);
363 +
364 + DimsOverThreshold.reserve(DimensionsMap.size());
365 +
366 + for (auto &DP : DimensionsMap) {
367 + Dimension *D = DP.second;
368 +
369 + auto P = D->detect(WindowLength, ResetBitCounter);
370 + bool IsAnomalous = P.first;
371 + double AnomalyRate = P.second;
372 +
373 + NumTrainedDimensions += D->isTrained();
374 +
375 + double DimTrainingDuration = D->updateTrainingDuration(0.0);
376 + MaxTrainingDuration = std::max(MaxTrainingDuration, DimTrainingDuration);
377 + TotalTrainingDuration += DimTrainingDuration;
378 +
379 + if (IsAnomalous)
380 + NumAnomalousDimensions += 1;
381 +
382 + if (NewAnomalyEvent && (AnomalyRate >= Cfg.ADDimensionRateThreshold))
383 + DimsOverThreshold.push_back({ AnomalyRate, D->getID() });
384 + }
385 +
386 + if (NumAnomalousDimensions)
387 + AnomalyRate = static_cast<double>(NumAnomalousDimensions) / DimensionsMap.size();
388 + else
389 + AnomalyRate = 0.0;
390 +
391 + NumNormalDimensions = DimensionsMap.size() - NumAnomalousDimensions;
392 + }
393 +
394 + this->NumAnomalousDimensions = NumAnomalousDimensions;
395 + this->NumNormalDimensions = NumNormalDimensions;
396 + this->NumTrainedDimensions = NumTrainedDimensions;
397 +
398 + updateDimensionsChart(getRH(), NumTrainedDimensions, NumNormalDimensions, NumAnomalousDimensions);
399 + updateRateChart(getRH(), AnomalyRate * 100.0);
400 + updateWindowLengthChart(getRH(), WindowLength);
401 + updateEventsChart(getRH(), P, ResetBitCounter, NewAnomalyEvent);
402 + updateTrainingChart(getRH(), TotalTrainingDuration * 1000.0, MaxTrainingDuration * 1000.0);
403 +
404 + if (!NewAnomalyEvent || (DimsOverThreshold.size() == 0))
405 + return;
406 +
407 + std::sort(DimsOverThreshold.begin(), DimsOverThreshold.end());
408 + std::reverse(DimsOverThreshold.begin(), DimsOverThreshold.end());
409 +
410 + // Make sure the JSON response won't grow beyond a specific number
411 + // of dimensions. Log an error message if this happens, because it
412 + // most likely means that the user specified a very-low anomaly rate
413 + // threshold.
414 + size_t NumMaxDimsOverThreshold = 2000;
415 + if (DimsOverThreshold.size() > NumMaxDimsOverThreshold) {
416 + error("Found %zu dimensions over threshold. Reducing JSON result to %zu dimensions.",
417 + DimsOverThreshold.size(), NumMaxDimsOverThreshold);
418 + DimsOverThreshold.resize(NumMaxDimsOverThreshold);
419 + }
420 +
421 + nlohmann::json JsonResult = DimsOverThreshold;
422 +
423 + time_t Before = now_realtime_sec();
424 + time_t After = Before - (WindowLength * updateEvery());
425 + DB.insertAnomaly("AD1", 1, getUUID(), After, Before, JsonResult.dump(4));
426 +}
427 +
428 +void DetectableHost::detect() {
429 + std::this_thread::sleep_for(Seconds{10});
430 +
431 + while (!netdata_exit) {
432 + TimePoint StartTP = SteadyClock::now();
433 + detectOnce();
434 + TimePoint EndTP = SteadyClock::now();
435 +
436 + Duration<double> Dur = EndTP - StartTP;
437 + updateDetectionChart(getRH(), Dur.count() * 1000);
438 +
439 + std::this_thread::sleep_for(Seconds{updateEvery()});
440 + }
441 +}
442 +
443 +void DetectableHost::getDetectionInfoAsJson(nlohmann::json &Json) const {
444 + Json["anomalous-dimensions"] = NumAnomalousDimensions;
445 + Json["normal-dimensions"] = NumNormalDimensions;
446 + Json["total-dimensions"] = NumAnomalousDimensions + NumNormalDimensions;
447 + Json["trained-dimensions"] = NumTrainedDimensions;
448 +}
449 +
450 +void DetectableHost::startAnomalyDetectionThreads() {
451 + TrainingThread = std::thread(&TrainableHost::train, this);
452 + DetectionThread = std::thread(&DetectableHost::detect, this);
453 +}
454 +
455 +void DetectableHost::stopAnomalyDetectionThreads() {
456 + TrainingThread.join();
457 + DetectionThread.join();
458 +}
ml/Host.h new
+104
@@ -0,0 +1,104 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef ML_HOST_H
4 +#define ML_HOST_H
5 +
6 +#include "BitRateWindow.h"
7 +#include "Config.h"
8 +#include "Database.h"
9 +#include "Dimension.h"
10 +
11 +#include "ml-private.h"
12 +
13 +namespace ml {
14 +
15 +class RrdHost {
16 +public:
17 + RrdHost(RRDHOST *RH) : RH(RH) {}
18 +
19 + RRDHOST *getRH() { return RH; }
20 +
21 + unsigned updateEvery() { return RH->rrd_update_every; }
22 +
23 + std::string getUUID() {
24 + char S[UUID_STR_LEN];
25 + uuid_unparse_lower(RH->host_uuid, S);
26 + return S;
27 + }
28 +
29 + void addDimension(Dimension *D);
30 + void removeDimension(Dimension *D);
31 +
32 + void getConfigAsJson(nlohmann::json &Json) const;
33 +
34 + virtual ~RrdHost() {};
35 +
36 +protected:
37 + RRDHOST *RH;
38 +
39 + // Protect dimension and lock maps
40 + std::mutex Mutex;
41 +
42 + std::map<RRDDIM *, Dimension *> DimensionsMap;
43 + std::map<Dimension *, std::mutex> LocksMap;
44 +};
45 +
46 +class TrainableHost : public RrdHost {
47 +public:
48 + TrainableHost(RRDHOST *RH) : RrdHost(RH) {}
49 +
50 + void train();
51 +
52 +private:
53 + std::pair<Dimension *, Duration<double>> findDimensionToTrain(const TimePoint &NowTP);
54 + void trainDimension(Dimension *D, const TimePoint &NowTP);
55 +};
56 +
57 +class DetectableHost : public TrainableHost {
58 +public:
59 + DetectableHost(RRDHOST *RH) : TrainableHost(RH) {}
60 +
61 + void startAnomalyDetectionThreads();
62 + void stopAnomalyDetectionThreads();
63 +
64 + template<typename ...ArgTypes>
65 + bool getAnomalyInfo(ArgTypes&&... Args) {
66 + return DB.getAnomalyInfo(Args...);
67 + }
68 +
69 + template<typename ...ArgTypes>
70 + bool getAnomaliesInRange(ArgTypes&&... Args) {
71 + return DB.getAnomaliesInRange(Args...);
72 + }
73 +
74 + void getDetectionInfoAsJson(nlohmann::json &Json) const;
75 +
76 +private:
77 + void detect();
78 + void detectOnce();
79 +
80 +private:
81 + std::thread TrainingThread;
82 + std::thread DetectionThread;
83 +
84 + BitRateWindow BRW{
85 + static_cast<size_t>(Cfg.ADMinWindowSize),
86 + static_cast<size_t>(Cfg.ADMaxWindowSize),
87 + static_cast<size_t>(Cfg.ADIdleWindowSize),
88 + static_cast<size_t>(Cfg.ADMinWindowSize * Cfg.ADWindowRateThreshold)
89 + };
90 +
91 + CalculatedNumber AnomalyRate{0.0};
92 +
93 + size_t NumAnomalousDimensions{0};
94 + size_t NumNormalDimensions{0};
95 + size_t NumTrainedDimensions{0};
96 +
97 + Database DB{Cfg.AnomalyDBPath};
98 +};
99 +
100 +using Host = DetectableHost;
101 +
102 +} // namespace ml
103 +
104 +#endif /* ML_HOST_H */
ml/Makefile.am new
+8
@@ -0,0 +1,8 @@
1 +# SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +AUTOMAKE_OPTIONS = subdir-objects
4 +MAINTAINERCLEANFILES = $(srcdir)/Makefile.in
5 +
6 +SUBDIRS = \
7 + kmeans \
8 + $(NULL)
ml/Query.h new
+49
@@ -0,0 +1,49 @@
1 +#ifndef QUERY_H
2 +#define QUERY_H
3 +
4 +#include "ml-private.h"
5 +
6 +namespace ml {
7 +
8 +class Query {
9 +public:
10 + Query(RRDDIM *RD) : RD(RD) {
11 + Ops = &RD->state->query_ops;
12 + }
13 +
14 + time_t latestTime() {
15 + return Ops->latest_time(RD);
16 + }
17 +
18 + time_t oldestTime() {
19 + return Ops->oldest_time(RD);
20 + }
21 +
22 + void init(time_t AfterT, time_t BeforeT) {
23 + Ops->init(RD, &Handle, AfterT, BeforeT);
24 + }
25 +
26 + bool isFinished() {
27 + return Ops->is_finished(&Handle);
28 + }
29 +
30 + std::pair<time_t, storage_number> nextMetric() {
31 + time_t CurrT;
32 + storage_number SN = Ops->next_metric(&Handle, &CurrT);
33 + return { CurrT, SN };
34 + }
35 +
36 + ~Query() {
37 + Ops->finalize(&Handle);
38 + }
39 +
40 +private:
41 + RRDDIM *RD;
42 +
43 + struct rrddim_volatile::rrddim_query_ops *Ops;
44 + struct rrddim_query_handle Handle;
45 +};
46 +
47 +} // namespace ml
48 +
49 +#endif /* QUERY_H */
ml/Tests.cc new
+301
@@ -0,0 +1,301 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#include "BitBufferCounter.h"
4 +#include "BitRateWindow.h"
5 +
6 +#include "gtest/gtest.h"
7 +
8 +using namespace ml;
9 +
10 +TEST(BitBufferCounterTest, Cap_4) {
11 + size_t Capacity = 4;
12 + BitBufferCounter BBC(Capacity);
13 +
14 + // No bits set
15 + EXPECT_EQ(BBC.numSetBits(), 0);
16 +
17 + // All ones
18 + for (size_t Idx = 0; Idx != (2 * Capacity); Idx++) {
19 + BBC.insert(true);
20 +
21 + EXPECT_EQ(BBC.numSetBits(), std::min(Idx + 1, Capacity));
22 + }
23 +
24 + // All zeroes
25 + for (size_t Idx = 0; Idx != Capacity; Idx++) {
26 + BBC.insert(false);
27 +
28 + if (Idx < Capacity)
29 + EXPECT_EQ(BBC.numSetBits(), Capacity - (Idx + 1));
30 + else
31 + EXPECT_EQ(BBC.numSetBits(), 0);
32 + }
33 +
34 + // Even ones/zeroes
35 + for (size_t Idx = 0; Idx != (2 * Capacity); Idx++)
36 + BBC.insert(Idx % 2 == 0);
37 + EXPECT_EQ(BBC.numSetBits(), Capacity / 2);
38 +}
39 +
40 +using State = BitRateWindow::State;
41 +using Edge = BitRateWindow::Edge;
42 +using Result = std::pair<Edge, size_t>;
43 +
44 +TEST(BitRateWindowTest, Cycles) {
45 + /* Test the FSM by going through its two cycles:
46 + * 1) NotFilled -> AboveThreshold -> Idle -> NotFilled
47 + * 2) NotFilled -> BelowThreshold -> AboveThreshold -> Idle -> NotFilled
48 + *
49 + * Check the window's length on every new state transition.
50 + */
51 +
52 + size_t MinLength = 4, MaxLength = 6, IdleLength = 5;
53 + size_t SetBitsThreshold = 3;
54 +
55 + Result R;
56 + BitRateWindow BRW(MinLength, MaxLength, IdleLength, SetBitsThreshold);
57 +
58 + /*
59 + * 1st cycle
60 + */
61 +
62 + // NotFilled -> AboveThreshold
63 + R = BRW.insert(true);
64 + EXPECT_EQ(R.first, std::make_pair(State::NotFilled, State::NotFilled));
65 + R = BRW.insert(true);
66 + EXPECT_EQ(R.first, std::make_pair(State::NotFilled, State::NotFilled));
67 + R = BRW.insert(true);
68 + EXPECT_EQ(R.first, std::make_pair(State::NotFilled, State::NotFilled));
69 + R = BRW.insert(true);
70 + EXPECT_EQ(R.first, std::make_pair(State::NotFilled, State::AboveThreshold));
71 + EXPECT_EQ(R.second, MinLength);
72 +
73 + // AboveThreshold -> Idle
74 + R = BRW.insert(true);
75 + EXPECT_EQ(R.first, std::make_pair(State::AboveThreshold, State::AboveThreshold));
76 + R = BRW.insert(true);
77 + EXPECT_EQ(R.first, std::make_pair(State::AboveThreshold, State::AboveThreshold));
78 +
79 + R = BRW.insert(true);
80 + EXPECT_EQ(R.first, std::make_pair(State::AboveThreshold, State::Idle));
81 + EXPECT_EQ(R.second, MaxLength);
82 +
83 +
84 + // Idle -> NotFilled
85 + R = BRW.insert(true);
86 + EXPECT_EQ(R.first, std::make_pair(State::Idle, State::Idle));
87 + R = BRW.insert(true);
88 + EXPECT_EQ(R.first, std::make_pair(State::Idle, State::Idle));
89 + R = BRW.insert(true);
90 + EXPECT_EQ(R.first, std::make_pair(State::Idle, State::Idle));
91 + R = BRW.insert(true);
92 + EXPECT_EQ(R.first, std::make_pair(State::Idle, State::Idle));
93 + R = BRW.insert(true);
94 + EXPECT_EQ(R.first, std::make_pair(State::Idle, State::NotFilled));
95 + EXPECT_EQ(R.second, 1);
96 +
97 + // NotFilled -> AboveThreshold
98 + R = BRW.insert(true);
99 + EXPECT_EQ(R.first, std::make_pair(State::NotFilled, State::NotFilled));
100 + R = BRW.insert(true);
101 + EXPECT_EQ(R.first, std::make_pair(State::NotFilled, State::NotFilled));
102 + R = BRW.insert(true);
103 + EXPECT_EQ(R.first, std::make_pair(State::NotFilled, State::AboveThreshold));
104 + EXPECT_EQ(R.second, MinLength);
105 +
106 + /*
107 + * 2nd cycle
108 + */
109 +
110 + BRW = BitRateWindow(MinLength, MaxLength, IdleLength, SetBitsThreshold);
111 +
112 + // NotFilled -> BelowThreshold
113 + R = BRW.insert(false);
114 + EXPECT_EQ(R.first, std::make_pair(State::NotFilled, State::NotFilled));
115 + R = BRW.insert(false);
116 + EXPECT_EQ(R.first, std::make_pair(State::NotFilled, State::NotFilled));
117 + R = BRW.insert(false);
118 + EXPECT_EQ(R.first, std::make_pair(State::NotFilled, State::NotFilled));
119 + R = BRW.insert(false);
120 + EXPECT_EQ(R.first, std::make_pair(State::NotFilled, State::BelowThreshold));
121 + EXPECT_EQ(R.second, MinLength);
122 +
123 + // BelowThreshold -> BelowThreshold:
124 + // Check the state's self loop by adding set bits that will keep the
125 + // bit buffer below the specified threshold.
126 + //
127 + for (size_t Idx = 0; Idx != 2 * MaxLength; Idx++) {
128 + R = BRW.insert(Idx % 2 == 0);
129 + EXPECT_EQ(R.first, std::make_pair(State::BelowThreshold, State::BelowThreshold));
130 + EXPECT_EQ(R.second, MinLength);
131 + }
132 +
133 + // Verify that at the end of the loop the internal bit buffer contains
134 + // "1010". Do so by adding one set bit and checking that we remain below
135 + // the specified threshold.
136 + R = BRW.insert(true);
137 + EXPECT_EQ(R.first, std::make_pair(State::BelowThreshold, State::BelowThreshold));
138 + EXPECT_EQ(R.second, MinLength);
139 +
140 + // BelowThreshold -> AboveThreshold
141 + R = BRW.insert(true);
142 + EXPECT_EQ(R.first, std::make_pair(State::BelowThreshold, State::AboveThreshold));
143 + EXPECT_EQ(R.second, MinLength);
144 +
145 + // AboveThreshold -> Idle:
146 + // Do the transition without filling the max window size this time.
147 + R = BRW.insert(false);
148 + EXPECT_EQ(R.first, std::make_pair(State::AboveThreshold, State::Idle));
149 + EXPECT_EQ(R.second, MinLength);
150 +
151 + // Idle -> NotFilled
152 + R = BRW.insert(false);
153 + EXPECT_EQ(R.first, std::make_pair(State::Idle, State::Idle));
154 + R = BRW.insert(false);
155 + EXPECT_EQ(R.first, std::make_pair(State::Idle, State::Idle));
156 + R = BRW.insert(false);
157 + EXPECT_EQ(R.first, std::make_pair(State::Idle, State::Idle));
158 + R = BRW.insert(false);
159 + EXPECT_EQ(R.first, std::make_pair(State::Idle, State::Idle));
160 + R = BRW.insert(false);
161 + EXPECT_EQ(R.first, std::make_pair(State::Idle, State::NotFilled));
162 + EXPECT_EQ(R.second, 1);
163 +
164 + // NotFilled -> AboveThreshold
165 + R = BRW.insert(true);
166 + EXPECT_EQ(R.first, std::make_pair(State::NotFilled, State::NotFilled));
167 + R = BRW.insert(true);
168 + EXPECT_EQ(R.first, std::make_pair(State::NotFilled, State::NotFilled));
169 + R = BRW.insert(true);
170 + EXPECT_EQ(R.first, std::make_pair(State::NotFilled, State::AboveThreshold));
171 + EXPECT_EQ(R.second, MinLength);
172 +}
173 +
174 +TEST(BitRateWindowTest, ConsecutiveOnes) {
175 + size_t MinLength = 120, MaxLength = 240, IdleLength = 30;
176 + size_t SetBitsThreshold = 30;
177 +
178 + Result R;
179 + BitRateWindow BRW(MinLength, MaxLength, IdleLength, SetBitsThreshold);
180 +
181 + for (size_t Idx = 0; Idx != MaxLength; Idx++)
182 + R = BRW.insert(false);
183 + EXPECT_EQ(R.first, std::make_pair(State::BelowThreshold, State::BelowThreshold));
184 + EXPECT_EQ(R.second, MinLength);
185 +
186 + for (size_t Idx = 0; Idx != SetBitsThreshold; Idx++) {
187 + EXPECT_EQ(R.first, std::make_pair(State::BelowThreshold, State::BelowThreshold));
188 + R = BRW.insert(true);
189 + }
190 + EXPECT_EQ(R.first, std::make_pair(State::BelowThreshold, State::AboveThreshold));
191 + EXPECT_EQ(R.second, MinLength);
192 +
193 + // At this point the window's buffer contains:
194 + // (MinLength - SetBitsThreshold = 90) 0s, followed by
195 + // (SetBitsThreshold = 30) 1s.
196 + //
197 + // To go below the threshold, we need to add (90 + 1) more 0s in the window's
198 + // buffer. At that point, the the window's buffer will contain:
199 + // (SetBitsThreshold = 29) 1s, followed by
200 + // (MinLength - SetBitsThreshold = 91) 0s.
201 + //
202 + // Right before adding the last 0, we expect the window's length to be equal to 210,
203 + // because the bit buffer has gone through these bits:
204 + // (MinLength - SetBitsThreshold = 90) 0s, followed by
205 + // (SetBitsThreshold = 30) 1s, followed by
206 + // (MinLength - SetBitsThreshold = 90) 0s.
207 +
208 + for (size_t Idx = 0; Idx != (MinLength - SetBitsThreshold); Idx++) {
209 + R = BRW.insert(false);
210 + EXPECT_EQ(R.first, std::make_pair(State::AboveThreshold, State::AboveThreshold));
211 + }
212 + EXPECT_EQ(R.second, 2 * MinLength - SetBitsThreshold);
213 + R = BRW.insert(false);
214 + EXPECT_EQ(R.first, std::make_pair(State::AboveThreshold, State::Idle));
215 +
216 + // Continue with the Idle -> NotFilled edge.
217 + for (size_t Idx = 0; Idx != IdleLength - 1; Idx++) {
218 + R = BRW.insert(false);
219 + EXPECT_EQ(R.first, std::make_pair(State::Idle, State::Idle));
220 + }
221 + R = BRW.insert(false);
222 + EXPECT_EQ(R.first, std::make_pair(State::Idle, State::NotFilled));
223 + EXPECT_EQ(R.second, 1);
224 +}
225 +
226 +TEST(BitRateWindowTest, WithHoles) {
227 + size_t MinLength = 120, MaxLength = 240, IdleLength = 30;
228 + size_t SetBitsThreshold = 30;
229 +
230 + Result R;
231 + BitRateWindow BRW(MinLength, MaxLength, IdleLength, SetBitsThreshold);
232 +
233 + for (size_t Idx = 0; Idx != MaxLength; Idx++)
234 + R = BRW.insert(false);
235 +
236 + for (size_t Idx = 0; Idx != SetBitsThreshold / 3; Idx++)
237 + R = BRW.insert(true);
238 + for (size_t Idx = 0; Idx != SetBitsThreshold / 3; Idx++)
239 + R = BRW.insert(false);
240 + for (size_t Idx = 0; Idx != SetBitsThreshold / 3; Idx++)
241 + R = BRW.insert(true);
242 + for (size_t Idx = 0; Idx != SetBitsThreshold / 3; Idx++)
243 + R = BRW.insert(false);
244 + for (size_t Idx = 0; Idx != SetBitsThreshold / 3; Idx++)
245 + R = BRW.insert(true);
246 +
247 + EXPECT_EQ(R.first, std::make_pair(State::BelowThreshold, State::AboveThreshold));
248 + EXPECT_EQ(R.second, MinLength);
249 +
250 + // The window's bit buffer contains:
251 + // 70 0s, 10 1s, 10 0s, 10 1s, 10 0s, 10 1s.
252 + // Where: 70 = MinLength - (5 / 3) * SetBitsThresholds, ie. we need
253 + // to add (70 + 1) more zeros to make the bit buffer go below the
254 + // threshold and then the window's length should be:
255 + // 70 + 50 + 70 = 190.
256 +
257 + BitRateWindow::Edge E;
258 + do {
259 + R = BRW.insert(false);
260 + E = R.first;
261 + } while (E.first != State::AboveThreshold || E.second != State::Idle);
262 + EXPECT_EQ(R.second, 2 * MinLength - (5 * SetBitsThreshold) / 3);
263 +}
264 +
265 +TEST(BitRateWindowTest, MinWindow) {
266 + size_t MinLength = 120, MaxLength = 240, IdleLength = 30;
267 + size_t SetBitsThreshold = 30;
268 +
269 + Result R;
270 + BitRateWindow BRW(MinLength, MaxLength, IdleLength, SetBitsThreshold);
271 +
272 + BRW.insert(true);
273 + BRW.insert(false);
274 + for (size_t Idx = 2; Idx != SetBitsThreshold; Idx++)
275 + BRW.insert(true);
276 + for (size_t Idx = SetBitsThreshold; Idx != MinLength - 1; Idx++)
277 + BRW.insert(false);
278 +
279 + R = BRW.insert(true);
280 + EXPECT_EQ(R.first, std::make_pair(State::NotFilled, State::AboveThreshold));
281 + EXPECT_EQ(R.second, MinLength);
282 +
283 + R = BRW.insert(false);
284 + EXPECT_EQ(R.first, std::make_pair(State::AboveThreshold, State::Idle));
285 +}
286 +
287 +TEST(BitRateWindowTest, MaxWindow) {
288 + size_t MinLength = 100, MaxLength = 200, IdleLength = 30;
289 + size_t SetBitsThreshold = 50;
290 +
291 + Result R;
292 + BitRateWindow BRW(MinLength, MaxLength, IdleLength, SetBitsThreshold);
293 +
294 + for (size_t Idx = 0; Idx != MaxLength; Idx++)
295 + R = BRW.insert(Idx % 2 == 0);
296 + EXPECT_EQ(R.first, std::make_pair(State::AboveThreshold, State::AboveThreshold));
297 + EXPECT_EQ(R.second, MaxLength);
298 +
299 + R = BRW.insert(false);
300 + EXPECT_EQ(R.first, std::make_pair(State::AboveThreshold, State::Idle));
301 +}
ml/json new
+1
@@ -0,0 +1 @@
1 +Subproject commit 0b345b20c888f7dc8888485768e4bf9a6be29de0
ml/kmeans/KMeans.cc new
+55
@@ -0,0 +1,55 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#include "KMeans.h"
4 +#include <dlib/clustering.h>
5 +
6 +void KMeans::train(SamplesBuffer &SB, size_t MaxIterations) {
7 + std::vector<DSample> Samples = SB.preprocess();
8 +
9 + MinDist = std::numeric_limits<CalculatedNumber>::max();
10 + MaxDist = std::numeric_limits<CalculatedNumber>::min();
11 +
12 + {
13 + std::lock_guard<std::mutex> Lock(Mutex);
14 +
15 + ClusterCenters.clear();
16 +
17 + dlib::pick_initial_centers(NumClusters, ClusterCenters, Samples);
18 + dlib::find_clusters_using_kmeans(Samples, ClusterCenters, MaxIterations);
19 +
20 + for (const auto &S : Samples) {
21 + CalculatedNumber MeanDist = 0.0;
22 +
23 + for (const auto &KMCenter : ClusterCenters)
24 + MeanDist += dlib::length(KMCenter - S);
25 +
26 + MeanDist /= NumClusters;
27 +
28 + if (MeanDist < MinDist)
29 + MinDist = MeanDist;
30 +
31 + if (MeanDist > MaxDist)
32 + MaxDist = MeanDist;
33 + }
34 + }
35 +}
36 +
37 +CalculatedNumber KMeans::anomalyScore(SamplesBuffer &SB) {
38 + std::vector<DSample> DSamples = SB.preprocess();
39 +
40 + std::unique_lock<std::mutex> Lock(Mutex, std::defer_lock);
41 + if (!Lock.try_lock())
42 + return std::numeric_limits<CalculatedNumber>::quiet_NaN();
43 +
44 + CalculatedNumber MeanDist = 0.0;
45 + for (const auto &CC: ClusterCenters)
46 + MeanDist += dlib::length(CC - DSamples.back());
47 +
48 + MeanDist /= NumClusters;
49 +
50 + if (MaxDist == MinDist)
51 + return 0.0;
52 +
53 + CalculatedNumber AnomalyScore = 100.0 * std::abs((MeanDist - MinDist) / (MaxDist - MinDist));
54 + return (AnomalyScore > 100.0) ? 100.0 : AnomalyScore;
55 +}
ml/kmeans/KMeans.h new
+34
@@ -0,0 +1,34 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef KMEANS_H
4 +#define KMEANS_H
5 +
6 +#include <atomic>
7 +#include <vector>
8 +#include <limits>
9 +#include <mutex>
10 +
11 +#include "SamplesBuffer.h"
12 +
13 +class KMeans {
14 +public:
15 + KMeans(size_t NumClusters = 2) : NumClusters(NumClusters) {
16 + MinDist = std::numeric_limits<CalculatedNumber>::max();
17 + MaxDist = std::numeric_limits<CalculatedNumber>::min();
18 + };
19 +
20 + void train(SamplesBuffer &SB, size_t MaxIterations);
21 + CalculatedNumber anomalyScore(SamplesBuffer &SB);
22 +
23 +private:
24 + size_t NumClusters;
25 +
26 + std::vector<DSample> ClusterCenters;
27 +
28 + CalculatedNumber MinDist;
29 + CalculatedNumber MaxDist;
30 +
31 + std::mutex Mutex;
32 +};
33 +
34 +#endif /* KMEANS_H */
ml/kmeans/Makefile.am new
+4
@@ -0,0 +1,4 @@
1 +# SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +AUTOMAKE_OPTIONS = subdir-objects
4 +MAINTAINERCLEANFILES = $(srcdir)/Makefile.in
ml/kmeans/SamplesBuffer.cc new
+144
@@ -0,0 +1,144 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +//
3 +#include "SamplesBuffer.h"
4 +
5 +#include <fstream>
6 +#include <sstream>
7 +#include <string>
8 +
9 +void Sample::print(std::ostream &OS) const {
10 + for (size_t Idx = 0; Idx != NumDims - 1; Idx++)
11 + OS << CNs[Idx] << ", ";
12 +
13 + OS << CNs[NumDims - 1];
14 +}
15 +
16 +void SamplesBuffer::print(std::ostream &OS) const {
17 + for (size_t Idx = Preprocessed ? (DiffN + (SmoothN - 1) + (LagN)) : 0;
18 + Idx != NumSamples; Idx++) {
19 + Sample S = Preprocessed ? getPreprocessedSample(Idx) : getSample(Idx);
20 + OS << S << std::endl;
21 + }
22 +}
23 +
24 +std::vector<Sample> SamplesBuffer::getPreprocessedSamples() const {
25 + std::vector<Sample> V;
26 +
27 + for (size_t Idx = Preprocessed ? (DiffN + (SmoothN - 1) + (LagN)) : 0;
28 + Idx != NumSamples; Idx++) {
29 + Sample S = Preprocessed ? getPreprocessedSample(Idx) : getSample(Idx);
30 + V.push_back(S);
31 + }
32 +
33 + return V;
34 +}
35 +
36 +void SamplesBuffer::diffSamples() {
37 + // Panda's DataFrame default behaviour is to subtract each element from
38 + // itself. For us `DiffN = 0` means "disable diff-ing" when preprocessing
39 + // the samples buffer. This deviation will make it easier for us to test
40 + // the KMeans implementation.
41 + if (DiffN == 0)
42 + return;
43 +
44 + for (size_t Idx = 0; Idx != (NumSamples - DiffN); Idx++) {
45 + size_t High = (NumSamples - 1) - Idx;
46 + size_t Low = High - DiffN;
47 +
48 + Sample LHS = getSample(High);
49 + Sample RHS = getSample(Low);
50 +
51 + LHS.diff(RHS);
52 + }
53 +}
54 +
55 +void SamplesBuffer::smoothSamples() {
56 + // Holds the mean value of each window
57 + CalculatedNumber *AccCNs = new CalculatedNumber[NumDimsPerSample]();
58 + Sample Acc(AccCNs, NumDimsPerSample);
59 +
60 + // Used to avoid clobbering the accumulator when moving the window
61 + CalculatedNumber *TmpCNs = new CalculatedNumber[NumDimsPerSample]();
62 + Sample Tmp(TmpCNs, NumDimsPerSample);
63 +
64 + CalculatedNumber Factor = (CalculatedNumber) 1 / SmoothN;
65 +
66 + // Calculate the value of the 1st window
67 + for (size_t Idx = 0; Idx != std::min(SmoothN, NumSamples); Idx++) {
68 + Tmp.add(getSample(NumSamples - (Idx + 1)));
69 + }
70 +
71 + Acc.add(Tmp);
72 + Acc.scale(Factor);
73 +
74 + // Move the window and update the samples
75 + for (size_t Idx = NumSamples; Idx != (DiffN + SmoothN - 1); Idx--) {
76 + Sample S = getSample(Idx - 1);
77 +
78 + // Tmp <- Next window (if any)
79 + if (Idx >= (SmoothN + 1)) {
80 + Tmp.diff(S);
81 + Tmp.add(getSample(Idx - (SmoothN + 1)));
82 + }
83 +
84 + // S <- Acc
85 + S.copy(Acc);
86 +
87 + // Acc <- Tmp
88 + Acc.copy(Tmp);
89 + Acc.scale(Factor);
90 + }
91 +
92 + delete[] AccCNs;
93 + delete[] TmpCNs;
94 +}
95 +
96 +void SamplesBuffer::lagSamples() {
97 + if (LagN == 0)
98 + return;
99 +
100 + for (size_t Idx = NumSamples; Idx != LagN; Idx--) {
101 + Sample PS = getPreprocessedSample(Idx - 1);
102 + PS.lag(getSample(Idx - 1), LagN);
103 + }
104 +}
105 +
106 +std::vector<DSample> SamplesBuffer::preprocess() {
107 + assert(Preprocessed == false);
108 +
109 + std::vector<DSample> DSamples;
110 + size_t OutN = NumSamples;
111 +
112 + // Diff
113 + if (DiffN >= OutN)
114 + return DSamples;
115 + OutN -= DiffN;
116 + diffSamples();
117 +
118 + // Smooth
119 + if (SmoothN == 0 || SmoothN > OutN)
120 + return DSamples;
121 + OutN -= (SmoothN - 1);
122 + smoothSamples();
123 +
124 + // Lag
125 + if (LagN >= OutN)
126 + return DSamples;
127 + OutN -= LagN;
128 + lagSamples();
129 +
130 + DSamples.reserve(OutN);
131 + Preprocessed = true;
132 +
133 + for (size_t Idx = NumSamples - OutN; Idx != NumSamples; Idx++) {
134 + DSample DS;
135 + DS.set_size(NumDimsPerSample * (LagN + 1));
136 +
137 + const Sample PS = getPreprocessedSample(Idx);
138 + PS.initDSample(DS);
139 +
140 + DSamples.push_back(DS);
141 + }
142 +
143 + return DSamples;
144 +}
ml/kmeans/SamplesBuffer.h new
+140
@@ -0,0 +1,140 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef SAMPLES_BUFFER_H
4 +#define SAMPLES_BUFFER_H
5 +
6 +#include <iostream>
7 +#include <vector>
8 +
9 +#include <cassert>
10 +#include <cstdlib>
11 +#include <cstring>
12 +
13 +#include <dlib/matrix.h>
14 +
15 +typedef double CalculatedNumber;
16 +typedef dlib::matrix<CalculatedNumber, 0, 1> DSample;
17 +
18 +class Sample {
19 +public:
20 + Sample(CalculatedNumber *Buf, size_t N) : CNs(Buf), NumDims(N) {}
21 +
22 + void initDSample(DSample &DS) const {
23 + for (size_t Idx = 0; Idx != NumDims; Idx++)
24 + DS(Idx) = CNs[Idx];
25 + }
26 +
27 + void add(const Sample &RHS) const {
28 + assert(NumDims == RHS.NumDims);
29 +
30 + for (size_t Idx = 0; Idx != NumDims; Idx++)
31 + CNs[Idx] += RHS.CNs[Idx];
32 + };
33 +
34 + void diff(const Sample &RHS) const {
35 + assert(NumDims == RHS.NumDims);
36 +
37 + for (size_t Idx = 0; Idx != NumDims; Idx++)
38 + CNs[Idx] -= RHS.CNs[Idx];
39 + };
40 +
41 + void copy(const Sample &RHS) const {
42 + assert(NumDims == RHS.NumDims);
43 +
44 + std::memcpy(CNs, RHS.CNs, NumDims * sizeof(CalculatedNumber));
45 + }
46 +
47 + void scale(CalculatedNumber Factor) {
48 + for (size_t Idx = 0; Idx != NumDims; Idx++)
49 + CNs[Idx] *= Factor;
50 + }
51 +
52 + void lag(const Sample &S, size_t LagN) {
53 + size_t N = S.NumDims;
54 +
55 + for (size_t Idx = 0; Idx != (LagN + 1); Idx++) {
56 + Sample Src(S.CNs - (Idx * N), N);
57 + Sample Dst(CNs + (Idx * N), N);
58 + Dst.copy(Src);
59 + }
60 + }
61 +
62 + const CalculatedNumber *getCalculatedNumbers() const {
63 + return CNs;
64 + };
65 +
66 + void print(std::ostream &OS) const;
67 +
68 +private:
69 + CalculatedNumber *CNs;
70 + size_t NumDims;
71 +};
72 +
73 +inline std::ostream& operator<<(std::ostream &OS, const Sample &S) {
74 + S.print(OS);
75 + return OS;
76 +}
77 +
78 +class SamplesBuffer {
79 +public:
80 + SamplesBuffer(CalculatedNumber *CNs,
81 + size_t NumSamples, size_t NumDimsPerSample,
82 + size_t DiffN = 1, size_t SmoothN = 3, size_t LagN = 3) :
83 + CNs(CNs), NumSamples(NumSamples), NumDimsPerSample(NumDimsPerSample),
84 + DiffN(DiffN), SmoothN(SmoothN), LagN(LagN),
85 + BytesPerSample(NumDimsPerSample * sizeof(CalculatedNumber)),
86 + Preprocessed(false) {};
87 +
88 + std::vector<DSample> preprocess();
89 + std::vector<Sample> getPreprocessedSamples() const;
90 +
91 + size_t capacity() const { return NumSamples; }
92 + void print(std::ostream &OS) const;
93 +
94 +private:
95 + size_t getSampleOffset(size_t Index) const {
96 + assert(Index < NumSamples);
97 + return Index * NumDimsPerSample;
98 + }
99 +
100 + size_t getPreprocessedSampleOffset(size_t Index) const {
101 + assert(Index < NumSamples);
102 + return getSampleOffset(Index) * (LagN + 1);
103 + }
104 +
105 + void setSample(size_t Index, const Sample &S) const {
106 + size_t Offset = getSampleOffset(Index);
107 + std::memcpy(&CNs[Offset], S.getCalculatedNumbers(), BytesPerSample);
108 + }
109 +
110 + const Sample getSample(size_t Index) const {
111 + size_t Offset = getSampleOffset(Index);
112 + return Sample(&CNs[Offset], NumDimsPerSample);
113 + };
114 +
115 + const Sample getPreprocessedSample(size_t Index) const {
116 + size_t Offset = getPreprocessedSampleOffset(Index);
117 + return Sample(&CNs[Offset], NumDimsPerSample * (LagN + 1));
118 + };
119 +
120 + void diffSamples();
121 + void smoothSamples();
122 + void lagSamples();
123 +
124 +private:
125 + CalculatedNumber *CNs;
126 + size_t NumSamples;
127 + size_t NumDimsPerSample;
128 + size_t DiffN;
129 + size_t SmoothN;
130 + size_t LagN;
131 + size_t BytesPerSample;
132 + bool Preprocessed;
133 +};
134 +
135 +inline std::ostream& operator<<(std::ostream& OS, const SamplesBuffer &SB) {
136 + SB.print(OS);
137 + return OS;
138 +}
139 +
140 +#endif /* SAMPLES_BUFFER_H */
ml/kmeans/Tests.cc new
+143
@@ -0,0 +1,143 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#include "ml/ml-private.h"
4 +#include <gtest/gtest.h>
5 +
6 +/*
7 + * The SamplesBuffer class implements the functionality of the following python
8 + * code:
9 + * >> df = pd.DataFrame(data=samples)
10 + * >> df = df.diff(diff_n).dropna()
11 + * >> df = df.rolling(smooth_n).mean().dropna()
12 + * >> df = pd.concat([df.shift(n) for n in range(lag_n + 1)], axis=1).dropna()
13 + *
14 + * Its correctness has been verified by automatically generating random
15 + * data frames in Python and comparing them with the correspondent preprocessed
16 + * SampleBuffers.
17 + *
18 + * The following tests are meant to catch unintended changes in the SamplesBuffer
19 + * implementation. For development purposes, one should compare changes against
20 + * the aforementioned python code.
21 +*/
22 +
23 +TEST(SamplesBufferTest, NS_8_NDPS_1_DN_1_SN_3_LN_1) {
24 + size_t NumSamples = 8, NumDimsPerSample = 1;
25 + size_t DiffN = 1, SmoothN = 3, LagN = 3;
26 +
27 + size_t N = NumSamples * NumDimsPerSample * (LagN + 1);
28 + CalculatedNumber *CNs = new CalculatedNumber[N]();
29 +
30 + CNs[0] = 0.7568336679490107;
31 + CNs[1] = 0.4814406581763254;
32 + CNs[2] = 0.40073555156221874;
33 + CNs[3] = 0.5973257298194408;
34 + CNs[4] = 0.5334727814345868;
35 + CNs[5] = 0.2632477193454843;
36 + CNs[6] = 0.2684839023122384;
37 + CNs[7] = 0.851332948637479;
38 +
39 + SamplesBuffer SB(CNs, NumSamples, NumDimsPerSample, DiffN, SmoothN, LagN);
40 + SB.preprocess();
41 +
42 + std::vector<Sample> Samples = SB.getPreprocessedSamples();
43 + EXPECT_EQ(Samples.size(), 2);
44 +
45 + Sample S0 = Samples[0];
46 + const CalculatedNumber *S0_CNs = S0.getCalculatedNumbers();
47 + Sample S1 = Samples[1];
48 + const CalculatedNumber *S1_CNs = S1.getCalculatedNumbers();
49 +
50 + EXPECT_NEAR(S0_CNs[0], -0.109614, 0.001);
51 + EXPECT_NEAR(S0_CNs[1], -0.0458293, 0.001);
52 + EXPECT_NEAR(S0_CNs[2], 0.017344, 0.001);
53 + EXPECT_NEAR(S0_CNs[3], -0.0531693, 0.001);
54 +
55 + EXPECT_NEAR(S1_CNs[0], 0.105953, 0.001);
56 + EXPECT_NEAR(S1_CNs[1], -0.109614, 0.001);
57 + EXPECT_NEAR(S1_CNs[2], -0.0458293, 0.001);
58 + EXPECT_NEAR(S1_CNs[3], 0.017344, 0.001);
59 +
60 + delete[] CNs;
61 +}
62 +
63 +TEST(SamplesBufferTest, NS_8_NDPS_1_DN_2_SN_3_LN_2) {
64 + size_t NumSamples = 8, NumDimsPerSample = 1;
65 + size_t DiffN = 2, SmoothN = 3, LagN = 2;
66 +
67 + size_t N = NumSamples * NumDimsPerSample * (LagN + 1);
68 + CalculatedNumber *CNs = new CalculatedNumber[N]();
69 +
70 + CNs[0] = 0.20511885291342846;
71 + CNs[1] = 0.13151717360306558;
72 + CNs[2] = 0.6017085062423134;
73 + CNs[3] = 0.46256882933941545;
74 + CNs[4] = 0.7887758447877941;
75 + CNs[5] = 0.9237989080034406;
76 + CNs[6] = 0.15552559051428083;
77 + CNs[7] = 0.6309750314597955;
78 +
79 + SamplesBuffer SB(CNs, NumSamples, NumDimsPerSample, DiffN, SmoothN, LagN);
80 + SB.preprocess();
81 +
82 + std::vector<Sample> Samples = SB.getPreprocessedSamples();
83 + EXPECT_EQ(Samples.size(), 2);
84 +
85 + Sample S0 = Samples[0];
86 + const CalculatedNumber *S0_CNs = S0.getCalculatedNumbers();
87 + Sample S1 = Samples[1];
88 + const CalculatedNumber *S1_CNs = S1.getCalculatedNumbers();
89 +
90 + EXPECT_NEAR(S0_CNs[0], 0.005016, 0.001);
91 + EXPECT_NEAR(S0_CNs[1], 0.326450, 0.001);
92 + EXPECT_NEAR(S0_CNs[2], 0.304903, 0.001);
93 +
94 + EXPECT_NEAR(S1_CNs[0], -0.154948, 0.001);
95 + EXPECT_NEAR(S1_CNs[1], 0.005016, 0.001);
96 + EXPECT_NEAR(S1_CNs[2], 0.326450, 0.001);
97 +
98 + delete[] CNs;
99 +}
100 +
101 +TEST(SamplesBufferTest, NS_8_NDPS_3_DN_2_SN_4_LN_1) {
102 + size_t NumSamples = 8, NumDimsPerSample = 3;
103 + size_t DiffN = 2, SmoothN = 4, LagN = 1;
104 +
105 + size_t N = NumSamples * NumDimsPerSample * (LagN + 1);
106 + CalculatedNumber *CNs = new CalculatedNumber[N]();
107 +
108 + CNs[0] = 0.34310900399667765; CNs[1] = 0.14694315994488194; CNs[2] = 0.8246677800938796;
109 + CNs[3] = 0.48249504592307835; CNs[4] = 0.23241087965531182; CNs[5] = 0.9595348555892567;
110 + CNs[6] = 0.44281094035598334; CNs[7] = 0.5143142171362715; CNs[8] = 0.06391303014242555;
111 + CNs[9] = 0.7460491027783901; CNs[10] = 0.43887217459032923; CNs[11] = 0.2814395025355999;
112 + CNs[12] = 0.9231114281214198; CNs[13] = 0.326882401786898; CNs[14] = 0.26747939220376216;
113 + CNs[15] = 0.7787571209969636; CNs[16] =0.5851700001235088; CNs[17] = 0.34410728945321567;
114 + CNs[18] = 0.9394494507088997; CNs[19] =0.17567223681734334; CNs[20] = 0.42732886195446984;
115 + CNs[21] = 0.9460522396152958; CNs[22] =0.23462747016780894; CNs[23] = 0.35983249900892145;
116 +
117 + SamplesBuffer SB(CNs, NumSamples, NumDimsPerSample, DiffN, SmoothN, LagN);
118 + SB.preprocess();
119 +
120 + std::vector<Sample> Samples = SB.getPreprocessedSamples();
121 + EXPECT_EQ(Samples.size(), 2);
122 +
123 + Sample S0 = Samples[0];
124 + const CalculatedNumber *S0_CNs = S0.getCalculatedNumbers();
125 + Sample S1 = Samples[1];
126 + const CalculatedNumber *S1_CNs = S1.getCalculatedNumbers();
127 +
128 + EXPECT_NEAR(S0_CNs[0], 0.198225, 0.001);
129 + EXPECT_NEAR(S0_CNs[1], 0.003529, 0.001);
130 + EXPECT_NEAR(S0_CNs[2], -0.063003, 0.001);
131 + EXPECT_NEAR(S0_CNs[3], 0.219066, 0.001);
132 + EXPECT_NEAR(S0_CNs[4], 0.133175, 0.001);
133 + EXPECT_NEAR(S0_CNs[5], -0.293154, 0.001);
134 +
135 + EXPECT_NEAR(S1_CNs[0], 0.174160, 0.001);
136 + EXPECT_NEAR(S1_CNs[1], -0.135722, 0.001);
137 + EXPECT_NEAR(S1_CNs[2], 0.110452, 0.001);
138 + EXPECT_NEAR(S1_CNs[3], 0.198225, 0.001);
139 + EXPECT_NEAR(S1_CNs[4], 0.003529, 0.001);
140 + EXPECT_NEAR(S1_CNs[5], -0.063003, 0.001);
141 +
142 + delete[] CNs;
143 +}
ml/kmeans/dlib new
+1
@@ -0,0 +1 @@
1 +Subproject commit 021cbbb1c2ddec39d8dd4cb6abfbbafdf1cf4482
ml/ml-dummy.c new
+38
@@ -0,0 +1,38 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#include "ml.h"
4 +
5 +#if !defined(ENABLE_ML)
6 +
7 +void ml_init(void) {}
8 +
9 +void ml_new_host(RRDHOST *RH) { (void) RH; }
10 +
11 +void ml_delete_host(RRDHOST *RH) { (void) RH; }
12 +
13 +char *ml_get_host_info(RRDHOST *RH) { (void) RH; }
14 +
15 +void ml_new_dimension(RRDDIM *RD) { (void) RD; }
16 +
17 +void ml_delete_dimension(RRDDIM *RD) { (void) RD; }
18 +
19 +bool ml_is_anomalous(RRDDIM *RD, double Value, bool Exists) {
20 + (void) RD; (void) Value; (void) Exists;
21 + return false;
22 +}
23 +
24 +char *ml_get_anomaly_events(RRDHOST *RH, const char *AnomalyDetectorName,
25 + int AnomalyDetectorVersion, time_t After, time_t Before) {
26 + (void) RH; (void) AnomalyDetectorName;
27 + (void) AnomalyDetectorVersion; (void) After; (void) Before;
28 + return NULL;
29 +}
30 +
31 +char *ml_get_anomaly_event_info(RRDHOST *RH, const char *AnomalyDetectorName,
32 + int AnomalyDetectorVersion, time_t After, time_t Before) {
33 + (void) RH; (void) AnomalyDetectorName;
34 + (void) AnomalyDetectorVersion; (void) After; (void) Before;
35 + return NULL;
36 +}
37 +
38 +#endif
ml/ml-private.h new
+26
@@ -0,0 +1,26 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef ML_PRIVATE_H
4 +#define ML_PRIVATE_H
5 +
6 +#include "kmeans/KMeans.h"
7 +#include "ml/ml.h"
8 +
9 +#include <chrono>
10 +#include <map>
11 +#include <mutex>
12 +#include <sstream>
13 +
14 +namespace ml {
15 +
16 +using SteadyClock = std::chrono::steady_clock;
17 +using TimePoint = std::chrono::time_point<SteadyClock>;
18 +
19 +template<typename T>
20 +using Duration = std::chrono::duration<T>;
21 +
22 +using Seconds = std::chrono::seconds;
23 +
24 +} // namespace ml
25 +
26 +#endif /* ML_PRIVATE_H */
ml/ml.cc new
+153
@@ -0,0 +1,153 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#include "Config.h"
4 +#include "Dimension.h"
5 +#include "Host.h"
6 +
7 +using namespace ml;
8 +
9 +/*
10 + * Assumptions:
11 + * 1) hosts outlive their sets, and sets outlive their dimensions,
12 + * 2) dimensions always have a set that has a host.
13 + */
14 +
15 +void ml_init(void) {
16 + Cfg.readMLConfig();
17 +}
18 +
19 +void ml_new_host(RRDHOST *RH) {
20 + if (!Cfg.EnableAnomalyDetection)
21 + return;
22 +
23 + if (simple_pattern_matches(Cfg.SP_HostsToSkip, RH->hostname))
24 + return;
25 +
26 + Host *H = new Host(RH);
27 + RH->ml_host = static_cast<ml_host_t>(H);
28 +
29 + H->startAnomalyDetectionThreads();
30 +}
31 +
32 +void ml_delete_host(RRDHOST *RH) {
33 + Host *H = static_cast<Host *>(RH->ml_host);
34 + if (!H)
35 + return;
36 +
37 + H->stopAnomalyDetectionThreads();
38 +
39 + delete H;
40 + RH->ml_host = nullptr;
41 +}
42 +
43 +void ml_new_dimension(RRDDIM *RD) {
44 + RRDSET *RS = RD->rrdset;
45 +
46 + Host *H = static_cast<Host *>(RD->rrdset->rrdhost->ml_host);
47 + if (!H)
48 + return;
49 +
50 + if (static_cast<unsigned>(RD->update_every) != H->updateEvery())
51 + return;
52 +
53 + if (simple_pattern_matches(Cfg.SP_ChartsToSkip, RS->name))
54 + return;
55 +
56 + Dimension *D = new Dimension(RD);
57 + RD->state->ml_dimension = static_cast<ml_dimension_t>(D);
58 + H->addDimension(D);
59 +}
60 +
61 +void ml_delete_dimension(RRDDIM *RD) {
62 + Dimension *D = static_cast<Dimension *>(RD->state->ml_dimension);
63 + if (!D)
64 + return;
65 +
66 + Host *H = static_cast<Host *>(RD->rrdset->rrdhost->ml_host);
67 + H->removeDimension(D);
68 +
69 + RD->state->ml_dimension = nullptr;
70 +}
71 +
72 +char *ml_get_host_info(RRDHOST *RH) {
73 + nlohmann::json ConfigJson;
74 +
75 + if (RH && RH->ml_host) {
76 + Host *H = static_cast<Host *>(RH->ml_host);
77 + H->getConfigAsJson(ConfigJson);
78 + H->getDetectionInfoAsJson(ConfigJson);
79 + } else {
80 + ConfigJson["enabled"] = false;
81 + }
82 +
83 + return strdup(ConfigJson.dump(2, '\t').c_str());
84 +}
85 +
86 +bool ml_is_anomalous(RRDDIM *RD, double Value, bool Exists) {
87 + Dimension *D = static_cast<Dimension *>(RD->state->ml_dimension);
88 + if (!D)
89 + return false;
90 +
91 + D->addValue(Value, Exists);
92 + bool Result = D->predict().second;
93 + return Result;
94 +}
95 +
96 +char *ml_get_anomaly_events(RRDHOST *RH, const char *AnomalyDetectorName,
97 + int AnomalyDetectorVersion, time_t After, time_t Before) {
98 + if (!RH || !RH->ml_host) {
99 + error("No host");
100 + return nullptr;
101 + }
102 +
103 + Host *H = static_cast<Host *>(RH->ml_host);
104 + std::vector<std::pair<time_t, time_t>> TimeRanges;
105 +
106 + bool Res = H->getAnomaliesInRange(TimeRanges, AnomalyDetectorName,
107 + AnomalyDetectorVersion,
108 + H->getUUID(),
109 + After, Before);
110 + if (!Res) {
111 + error("DB result is empty");
112 + return nullptr;
113 + }
114 +
115 + nlohmann::json Json = TimeRanges;
116 + return strdup(Json.dump(4).c_str());
117 +}
118 +
119 +char *ml_get_anomaly_event_info(RRDHOST *RH, const char *AnomalyDetectorName,
120 + int AnomalyDetectorVersion, time_t After, time_t Before) {
121 + if (!RH || !RH->ml_host) {
122 + error("No host");
123 + return nullptr;
124 + }
125 +
126 + Host *H = static_cast<Host *>(RH->ml_host);
127 +
128 + nlohmann::json Json;
129 + bool Res = H->getAnomalyInfo(Json, AnomalyDetectorName,
130 + AnomalyDetectorVersion,
131 + H->getUUID(),
132 + After, Before);
133 + if (!Res) {
134 + error("DB result is empty");
135 + return nullptr;
136 + }
137 +
138 + return strdup(Json.dump(4, '\t').c_str());
139 +}
140 +
141 +#if defined(ENABLE_ML_TESTS)
142 +
143 +#include "gtest/gtest.h"
144 +
145 +int test_ml(int argc, char *argv[]) {
146 + (void) argc;
147 + (void) argv;
148 +
149 + ::testing::InitGoogleTest(&argc, argv);
150 + return RUN_ALL_TESTS();
151 +}
152 +
153 +#endif // ENABLE_ML_TESTS
ml/ml.h new
+41
@@ -0,0 +1,41 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef NETDATA_ML_H
4 +#define NETDATA_ML_H
5 +
6 +#ifdef __cplusplus
7 +extern "C" {
8 +#endif
9 +
10 +#include "daemon/common.h"
11 +
12 +typedef void* ml_host_t;
13 +typedef void* ml_dimension_t;
14 +
15 +void ml_init(void);
16 +
17 +void ml_new_host(RRDHOST *RH);
18 +void ml_delete_host(RRDHOST *RH);
19 +
20 +char *ml_get_host_info(RRDHOST *RH);
21 +
22 +void ml_new_dimension(RRDDIM *RD);
23 +void ml_delete_dimension(RRDDIM *RD);
24 +
25 +bool ml_is_anomalous(RRDDIM *RD, double value, bool exists);
26 +
27 +char *ml_get_anomaly_events(RRDHOST *RH, const char *AnomalyDetectorName,
28 + int AnomalyDetectorVersion, time_t After, time_t Before);
29 +
30 +char *ml_get_anomaly_event_info(RRDHOST *RH, const char *AnomalyDetectorName,
31 + int AnomalyDetectorVersion, time_t After, time_t Before);
32 +
33 +#if defined(ENABLE_ML_TESTS)
34 +int test_ml(int argc, char *argv[]);
35 +#endif
36 +
37 +#ifdef __cplusplus
38 +};
39 +#endif
40 +
41 +#endif /* NETDATA_ML_H */
netdata-installer.sh
+6
@@ -241,6 +241,8 @@ USAGE: ${PROGRAM} [options]
241 --disable-backend-mongodb
242 --enable-lto Enable Link-Time-Optimization. Default: disabled
243 --disable-lto
244 + --enable-ml Enable anomaly detection with machine learning. (Default: autodetect)
245 + --disable-ml
246 --disable-x86-sse Disable SSE instructions. By default SSE optimizations are enabled.
247 --use-system-lws Use a system copy of libwebsockets instead of bundling our own (default is to use the bundled copy).
248 --use-system-protobuf Use a system copy of libprotobuf instead of bundling our own (default is to use the bundled copy).
@@ -329,6 +331,10 @@ while [ -n "${1}" ]; do
331 "--enable-backend-mongodb") NETDATA_CONFIGURE_OPTIONS="${NETDATA_CONFIGURE_OPTIONS//--enable-backend-mongodb/} --enable-backend-mongodb" ;;
332 "--disable-backend-mongodb") NETDATA_CONFIGURE_OPTIONS="${NETDATA_CONFIGURE_OPTIONS//--disable-backend-mongodb/} --disable-backend-mongodb" ;;
333 "--enable-lto") NETDATA_CONFIGURE_OPTIONS="${NETDATA_CONFIGURE_OPTIONS//--enable-lto/} --enable-lto" ;;
334 + "--enable-ml") NETDATA_CONFIGURE_OPTIONS="${NETDATA_CONFIGURE_OPTIONS//--enable-ml/} --enable-ml" ;;
335 + "--disable-ml") NETDATA_CONFIGURE_OPTIONS="${NETDATA_CONFIGURE_OPTIONS//--disable-ml/} --disable-ml" ;;
336 + "--enable-ml-tests") NETDATA_CONFIGURE_OPTIONS="${NETDATA_CONFIGURE_OPTIONS//--enable-ml-tests/} --enable-ml-tests" ;;
337 + "--disable-ml-tests") NETDATA_CONFIGURE_OPTIONS="${NETDATA_CONFIGURE_OPTIONS//--disable-ml-tests/} --disable-ml-tests" ;;
338 "--disable-lto") NETDATA_CONFIGURE_OPTIONS="${NETDATA_CONFIGURE_OPTIONS//--disable-lto/} --disable-lto" ;;
339 "--disable-x86-sse") NETDATA_CONFIGURE_OPTIONS="${NETDATA_CONFIGURE_OPTIONS//--disable-x86-sse/} --disable-x86-sse" ;;
340 "--disable-telemetry") NETDATA_DISABLE_TELEMETRY=1 ;;
web/api/queries/query.c
+15 -3
@@ -389,6 +389,7 @@ static inline void do_dimension_variablestep(
389 , long dim_id_in_rrdr
390 , time_t after_wanted
391 , time_t before_wanted
392 + , uint32_t options
393 ){
394 // RRDSET *st = r->st;
395
@@ -445,7 +446,11 @@ static inline void do_dimension_variablestep(
446 // db_now has a different value than above
447 if (likely(now >= db_now)) {
448 if (likely(does_storage_number_exist(n_curr))) {
448 - value = unpack_storage_number(n_curr);
449 + if (options & RRDR_OPTION_ANOMALY_BIT)
450 + value = (n_curr & SN_ANOMALY_BIT) ? 0.0 : 100.0;
451 + else
452 + value = unpack_storage_number(n_curr);
453 +
454 if (likely(value != 0.0))
455 values_in_group_non_zero++;
456
@@ -530,6 +535,7 @@ static inline void do_dimension_fixedstep(
535 , long dim_id_in_rrdr
536 , time_t after_wanted
537 , time_t before_wanted
538 + , uint32_t options
539 ){
540 RRDSET *st = r->st;
541
@@ -593,7 +599,11 @@ static inline void do_dimension_fixedstep(
599 error("INTERNAL CHECK: Unaligned query for %s, database time: %ld, expected time: %ld", rd->id, (long)handle.rrdeng.now, (long)now);
600 }
601 #endif
596 - value = unpack_storage_number(n);
602 + if (options & RRDR_OPTION_ANOMALY_BIT)
603 + value = (n & SN_ANOMALY_BIT) ? 0.0 : 100.0;
604 + else
605 + value = unpack_storage_number(n);
606 +
607 if(likely(value != 0.0))
608 values_in_group_non_zero++;
609
@@ -1100,6 +1110,7 @@ static RRDR *rrd2rrdr_fixedstep(
1110 , c
1111 , after_wanted
1112 , before_wanted
1113 + , options
1114 );
1115
1116 if(r->od[c] & RRDR_DIMENSION_NONZERO)
@@ -1476,6 +1487,7 @@ static RRDR *rrd2rrdr_variablestep(
1487 , c
1488 , after_wanted
1489 , before_wanted
1490 + , options
1491 );
1492
1493 if(r->od[c] & RRDR_DIMENSION_NONZERO)
@@ -1644,4 +1656,4 @@ RRDR *rrd2rrdr(
1656 return rrd2rrdr_fixedstep(st, points_requested, after_requested, before_requested, group_method,
1657 resampling_time_requested, options, dimensions,
1658 rrd_update_every, first_entry_t, last_entry_t, absolute_period_requested, context_param_list);
1647 -}
\ No newline at end of file
1659 +}
web/api/queries/rrdr.h
+1
@@ -24,6 +24,7 @@ typedef enum rrdr_options {
24 RRDR_OPTION_MATCH_NAMES = 0x00008000, // when filtering dimensions, match only names
25 RRDR_OPTION_CUSTOM_VARS = 0x00010000, // when wrapping response in a JSON, return custom variables in response
26 RRDR_OPTION_ALLOW_PAST = 0x00020000, // The after parameter can extend in the past before the first entry
27 + RRDR_OPTION_ANOMALY_BIT = 0x00040000, // Return the anomaly bit stored in each collected_number
28 } RRDR_OPTIONS;
29
30 typedef enum rrdr_value_flag {
web/api/web_api_v1.c
+105
@@ -36,6 +36,7 @@ static struct {
36 , {"match-names" , 0 , RRDR_OPTION_MATCH_NAMES}
37 , {"showcustomvars" , 0 , RRDR_OPTION_CUSTOM_VARS}
38 , {"allow_past" , 0 , RRDR_OPTION_ALLOW_PAST}
39 + , {"anomaly-bit" , 0 , RRDR_OPTION_ANOMALY_BIT}
40 , { NULL, 0, 0}
41 };
42
@@ -1088,12 +1089,110 @@ inline int web_client_api_request_v1_info_fill_buffer(RRDHOST *host, BUFFER *wb)
1089
1090 buffer_strcat(wb, "\t\"metrics-count\": ");
1091 analytics_get_data(analytics_data.netdata_metrics_count, wb);
1092 + buffer_strcat(wb, ",\n");
1093 +
1094 +#if defined(ENABLE_ML)
1095 + char *ml_info = ml_get_host_info(host);
1096 +
1097 + buffer_strcat(wb, "\t\"ml-info\": ");
1098 + buffer_strcat(wb, ml_info);
1099 buffer_strcat(wb, "\n");
1100
1101 + free(ml_info);
1102 +#endif
1103 +
1104 buffer_strcat(wb, "}");
1105 return 0;
1106 }
1107
1108 +#if defined(ENABLE_ML)
1109 +int web_client_api_request_v1_anomaly_events(RRDHOST *host, struct web_client *w, char *url) {
1110 + if (!netdata_ready)
1111 + return HTTP_RESP_BACKEND_FETCH_FAILED;
1112 +
1113 + uint32_t after = 0, before = 0;
1114 +
1115 + while (url) {
1116 + char *value = mystrsep(&url, "&");
1117 + if (!value || !*value)
1118 + continue;
1119 +
1120 + char *name = mystrsep(&value, "=");
1121 + if (!name || !*name)
1122 + continue;
1123 + if (!value || !*value)
1124 + continue;
1125 +
1126 + if (!strcmp(name, "after"))
1127 + after = (uint32_t) (strtoul(value, NULL, 0) / 1000);
1128 + else if (!strcmp(name, "before"))
1129 + before = (uint32_t) (strtoul(value, NULL, 0) / 1000);
1130 + }
1131 +
1132 + char *s;
1133 + if (!before || !after)
1134 + s = strdup("{\"error\": \"missing after/before parameters\" }\n");
1135 + else {
1136 + s = ml_get_anomaly_events(host, "AD1", 1, after, before);
1137 + if (!s)
1138 + s = strdup("{\"error\": \"json string is empty\" }\n");
1139 + }
1140 +
1141 + BUFFER *wb = w->response.data;
1142 + buffer_flush(wb);
1143 +
1144 + wb->contenttype = CT_APPLICATION_JSON;
1145 + buffer_strcat(wb, s);
1146 + buffer_no_cacheable(wb);
1147 +
1148 + freez(s);
1149 +
1150 + return HTTP_RESP_OK;
1151 +}
1152 +
1153 +int web_client_api_request_v1_anomaly_event_info(RRDHOST *host, struct web_client *w, char *url) {
1154 + if (!netdata_ready)
1155 + return HTTP_RESP_BACKEND_FETCH_FAILED;
1156 +
1157 + uint32_t after = 0, before = 0;
1158 +
1159 + while (url) {
1160 + char *value = mystrsep(&url, "&");
1161 + if (!value || !*value)
1162 + continue;
1163 +
1164 + char *name = mystrsep(&value, "=");
1165 + if (!name || !*name)
1166 + continue;
1167 + if (!value || !*value)
1168 + continue;
1169 +
1170 + if (!strcmp(name, "after"))
1171 + after = (uint32_t) strtoul(value, NULL, 0);
1172 + else if (!strcmp(name, "before"))
1173 + before = (uint32_t) strtoul(value, NULL, 0);
1174 + }
1175 +
1176 + char *s;
1177 + if (!before || !after)
1178 + s = strdup("{\"error\": \"missing after/before parameters\" }\n");
1179 + else {
1180 + s = ml_get_anomaly_event_info(host, "AD1", 1, after, before);
1181 + if (!s)
1182 + s = strdup("{\"error\": \"json string is empty\" }\n");
1183 + }
1184 +
1185 + BUFFER *wb = w->response.data;
1186 + buffer_flush(wb);
1187 + wb->contenttype = CT_APPLICATION_JSON;
1188 + buffer_strcat(wb, s);
1189 + buffer_no_cacheable(wb);
1190 +
1191 + freez(s);
1192 + return HTTP_RESP_OK;
1193 +}
1194 +#endif // defined(ENABLE_ML)
1195 +
1196 inline int web_client_api_request_v1_info(RRDHOST *host, struct web_client *w, char *url) {
1197 (void)url;
1198 if (!netdata_ready) return HTTP_RESP_BACKEND_FETCH_FAILED;
@@ -1148,6 +1247,12 @@ static struct api_command {
1247 { "alarm_variables", 0, WEB_CLIENT_ACL_DASHBOARD, web_client_api_request_v1_alarm_variables },
1248 { "alarm_count", 0, WEB_CLIENT_ACL_DASHBOARD, web_client_api_request_v1_alarm_count },
1249 { "allmetrics", 0, WEB_CLIENT_ACL_DASHBOARD, web_client_api_request_v1_allmetrics },
1250 +
1251 +#if defined(ENABLE_ML)
1252 + { "anomaly_events", 0, WEB_CLIENT_ACL_DASHBOARD, web_client_api_request_v1_anomaly_events },
1253 + { "anomaly_event_info", 0, WEB_CLIENT_ACL_DASHBOARD, web_client_api_request_v1_anomaly_event_info },
1254 +#endif
1255 +
1256 { "manage/health", 0, WEB_CLIENT_ACL_MGMT, web_client_api_request_v1_mgmt_health },
1257 { "aclk", 0, WEB_CLIENT_ACL_DASHBOARD, web_client_api_request_v1_aclk_state },
1258 // terminator
web/gui/dashboard_info.js
+29
@@ -698,6 +698,11 @@ netdataDashboard.menu = {
698 info: 'Z scores scores relating to key system metrics.'
699 },
700
701 + 'anomaly_detection': {
702 + title: 'Anomaly Detection',
703 + icon: '<i class="fas fa-brain"></i>',
704 + info: 'Charts relating to anomaly detection, increased <code>anomalous</code> dimensions or a higher than usual <code>anomaly_rate</code> could be signs of some abnormal behaviour. Read our <a href="https://learn.netdata.cloud/guides/monitor/anomaly-detection" target="_blank">anomaly detection guide</a> for more details.'
705 + },
706 };
707
708
@@ -6344,4 +6349,28 @@ netdataDashboard.context = {
6349 'See <a href="https://www.freedesktop.org/software/systemd/man/systemd.slice.html#" target="_blank"> systemd.slice(5)</a>.'
6350 },
6351
6352 + 'anomaly_detection.dimensions': {
6353 + info: 'Total count of dimensions considered anomalous or normal. '
6354 + },
6355 +
6356 + 'anomaly_detection.anomaly_rate': {
6357 + info: 'Percentage of anomalous dimensions. '
6358 + },
6359 +
6360 + 'anomaly_detection.detector_window': {
6361 + info: 'The length of the active window used by the detector. '
6362 + },
6363 +
6364 + 'anomaly_detection.detector_events': {
6365 + info: 'Flags (0 or 1) to show when an anomaly event has been triggered by the detector. '
6366 + },
6367 +
6368 + 'anomaly_detection.prediction_stats': {
6369 + info: 'Diagnostic metrics relating to prediction time of anomaly detection. '
6370 + },
6371 +
6372 + 'anomaly_detection.training_stats': {
6373 + info: 'Diagnostic metrics relating to training time of anomaly detection. '
6374 + },
6375 +
6376 };