From 3d72fa7f517e6e99af1205e965c3775dc23461f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B4=BA=E6=9C=89=E5=BF=97?= <1037617413@qq.com> Date: Mon, 23 Sep 2024 11:03:26 +0800 Subject: [PATCH] add ai threshold slow io detection to sysSentry --- .../ai_threshold_slow_io_detection.ini | 16 ++ .../tasks/ai_threshold_slow_io_detection.mod | 5 + .../test_ai_threshold_slow_io_detection.py | 165 ++++++++++++++++++ .../ai_threshold_slow_io_detection/README.md | 2 + .../__init__.py | 0 .../alarm_report.py | 49 ++++++ .../config_parser.py | 141 +++++++++++++++ .../data_access.py | 91 ++++++++++ .../detector.py | 48 +++++ .../ai_threshold_slow_io_detection/io_data.py | 74 ++++++++ .../sliding_window.py | 113 ++++++++++++ .../slow_io_detection.py | 133 ++++++++++++++ .../threshold.py | 160 +++++++++++++++++ .../ai_threshold_slow_io_detection/utils.py | 67 +++++++ src/python/setup.py | 3 +- 15 files changed, 1066 insertions(+), 1 deletion(-) create mode 100644 config/plugins/ai_threshold_slow_io_detection.ini create mode 100644 config/tasks/ai_threshold_slow_io_detection.mod create mode 100644 selftest/test/test_ai_threshold_slow_io_detection.py create mode 100644 src/python/sentryPlugins/ai_threshold_slow_io_detection/README.md create mode 100644 src/python/sentryPlugins/ai_threshold_slow_io_detection/__init__.py create mode 100644 src/python/sentryPlugins/ai_threshold_slow_io_detection/alarm_report.py create mode 100644 src/python/sentryPlugins/ai_threshold_slow_io_detection/config_parser.py create mode 100644 src/python/sentryPlugins/ai_threshold_slow_io_detection/data_access.py create mode 100644 src/python/sentryPlugins/ai_threshold_slow_io_detection/detector.py create mode 100644 src/python/sentryPlugins/ai_threshold_slow_io_detection/io_data.py create mode 100644 src/python/sentryPlugins/ai_threshold_slow_io_detection/sliding_window.py create mode 100644 src/python/sentryPlugins/ai_threshold_slow_io_detection/slow_io_detection.py create mode 100644 src/python/sentryPlugins/ai_threshold_slow_io_detection/threshold.py create mode 100644 src/python/sentryPlugins/ai_threshold_slow_io_detection/utils.py diff --git a/config/plugins/ai_threshold_slow_io_detection.ini b/config/plugins/ai_threshold_slow_io_detection.ini new file mode 100644 index 0000000..44eb928 --- /dev/null +++ b/config/plugins/ai_threshold_slow_io_detection.ini @@ -0,0 +1,16 @@ +[common] +absolute_threshold=40 +slow_io_detect_frequency=1 +log_level=info + +[algorithm] +train_data_duration=0.1 +train_update_duration=0.02 +algorithm_type=n_sigma +boxplot_parameter=1.5 +n_sigma_parameter=3 + +[sliding_window] +sliding_window_type=not_continuous +window_size=30 +window_minimum_threshold=6 \ No newline at end of file diff --git a/config/tasks/ai_threshold_slow_io_detection.mod b/config/tasks/ai_threshold_slow_io_detection.mod new file mode 100644 index 0000000..2729f72 --- /dev/null +++ b/config/tasks/ai_threshold_slow_io_detection.mod @@ -0,0 +1,5 @@ +[common] +enabled=yes +task_start=/usr/bin/python3 /usr/bin/ai_threshold_slow_io_detection +task_stop=pkill -f /usr/bin/ai_threshold_slow_io_detection +type=oneshot \ No newline at end of file diff --git a/selftest/test/test_ai_threshold_slow_io_detection.py b/selftest/test/test_ai_threshold_slow_io_detection.py new file mode 100644 index 0000000..c36fef5 --- /dev/null +++ b/selftest/test/test_ai_threshold_slow_io_detection.py @@ -0,0 +1,165 @@ +# coding: utf-8 +# Copyright (c) 2024 Huawei Technologies Co., Ltd. +# sysSentry is licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. + +import unittest +import numpy as np + +from sentryPlugins.ai_threshold_slow_io_detection.threshold import AbsoluteThreshold, BoxplotThreshold, NSigmaThreshold +from sentryPlugins.ai_threshold_slow_io_detection.sliding_window import (NotContinuousSlidingWindow, + ContinuousSlidingWindow, MedianSlidingWindow) + + +def _get_boxplot_threshold(data_list: list, parameter): + q1 = np.percentile(data_list, 25) + q3 = np.percentile(data_list, 75) + iqr = q3 - q1 + return q3 + parameter * iqr + + +def _get_n_sigma_threshold(data_list: list, parameter): + mean = np.mean(data_list) + std = np.std(data_list) + return mean + parameter * std + + +class Test(unittest.TestCase): + @classmethod + def setUpClass(cls): + print("UnitTest Begin...") + + @classmethod + def tearDownClass(cls): + print("UnitTest End...") + + def setUp(self): + print("Begin...") + + def tearDown(self): + print("End...") + + def test_absolute_threshold(self): + absolute = AbsoluteThreshold() + self.assertEqual(None, absolute.get_threshold()) + self.assertFalse(absolute.is_abnormal(5000)) + absolute.set_threshold(40) + self.assertEqual(40, absolute.get_threshold()) + self.assertTrue(absolute.is_abnormal(50)) + + def test_boxplot_threshold(self): + boxplot = BoxplotThreshold(1.5, 5, 1) + # 阶段1:尚未初始化 + self.assertEqual(None, boxplot.get_threshold()) + self.assertFalse(boxplot.is_abnormal(5000)) + # 往boxplot中插入5个元素后,会生成阈值 + data_list = [20, 20, 20, 30, 10] + for data in data_list: + boxplot.push_latest_data_to_queue(data) + # 阶段2:初始化 + boxplot_threshold = boxplot.get_threshold() + self.assertEqual(_get_boxplot_threshold(data_list, 1.5), boxplot_threshold) + self.assertTrue(boxplot.is_abnormal(5000)) + data_list.pop(0) + data_list.append(100) + boxplot.push_latest_data_to_queue(100) + # 阶段3:更新阈值 + boxplot_threshold = boxplot.get_threshold() + self.assertEqual(_get_boxplot_threshold(data_list, 1.5), boxplot_threshold) + + def test_n_sigma_threshold(self): + n_sigma = NSigmaThreshold(3, 5, 1) + self.assertEqual(None, n_sigma.get_threshold()) + self.assertFalse(n_sigma.is_abnormal(5000)) + data_list = [20, 20, 20, 30, 10] + for data in data_list: + n_sigma.push_latest_data_to_queue(data) + n_sigma_threshold = n_sigma.get_threshold() + self.assertEqual(_get_n_sigma_threshold(data_list, 3), n_sigma_threshold) + self.assertTrue(n_sigma.is_abnormal(5000)) + data_list.pop(0) + data_list.append(100) + n_sigma.push_latest_data_to_queue(100) + # 阶段3:更新阈值 + n_sigma_threshold = n_sigma.get_threshold() + self.assertEqual(_get_n_sigma_threshold(data_list, 3), n_sigma_threshold) + + def test_not_continuous_sliding_window(self): + not_continuous = NotContinuousSlidingWindow(5, 3) + boxplot_threshold = BoxplotThreshold(1.5, 10, 8) + boxplot_threshold.attach_observer(not_continuous) + data_list1 = [19, 20, 20, 20, 20, 20, 22, 24, 23, 20] + for data in data_list1: + boxplot_threshold.push_latest_data_to_queue(data) + result = not_continuous.is_slow_io_event(data) + self.assertFalse(result[0]) + self.assertEqual(23.75, boxplot_threshold.get_threshold()) + boxplot_threshold.push_latest_data_to_queue(24) + result = not_continuous.is_slow_io_event(24) + self.assertFalse(result[0]) + boxplot_threshold.push_latest_data_to_queue(25) + result = not_continuous.is_slow_io_event(25) + self.assertTrue(result[0]) + data_list2 = [20, 20, 20, 20, 20, 20] + for data in data_list2: + boxplot_threshold.push_latest_data_to_queue(data) + result = not_continuous.is_slow_io_event(data) + self.assertFalse(result[0]) + self.assertEqual(25.625, boxplot_threshold.get_threshold()) + + def test_continuous_sliding_window(self): + continuous = ContinuousSlidingWindow(5, 3) + boxplot_threshold = BoxplotThreshold(1.5, 10, 8) + boxplot_threshold.attach_observer(continuous) + data_list = [19, 20, 20, 20, 20, 20, 22, 24, 23, 20] + for data in data_list: + boxplot_threshold.push_latest_data_to_queue(data) + result = continuous.is_slow_io_event(data) + self.assertFalse(result[0]) + self.assertEqual(23.75, boxplot_threshold.get_threshold()) + # 没有三个异常点 + self.assertFalse(continuous.is_slow_io_event(25)[0]) + # 不连续的三个异常点 + self.assertFalse(continuous.is_slow_io_event(25)[0]) + # 连续的三个异常点 + self.assertTrue(continuous.is_slow_io_event(25)[0]) + + def test_median_sliding_window(self): + median = MedianSlidingWindow(5, 3) + absolute_threshold = AbsoluteThreshold(10, 8) + absolute_threshold.attach_observer(median) + absolute_threshold.set_threshold(24.5) + data_list = [24, 24, 24, 25, 25] + for data in data_list: + self.assertFalse(median.is_slow_io_event(data)[0]) + self.assertTrue(median.is_slow_io_event(25)[0]) + + def test_parse_collect_data(self): + collect = { + "read": [1.0, 2.0, 3.0, 4.0], + "write": [5.0, 6.0, 7.0, 8.0], + "flush": [9.0, 10.0, 11.0, 12.0], + "discard": [13.0, 14.0, 15.0, 16.0], + } + from io_data import BaseData + from data_access import _get_io_stage_data + + io_data = _get_io_stage_data(collect) + self.assertEqual( + io_data.read, BaseData(latency=1.0, io_dump=2.0, io_length=3.0, iops=4.0) + ) + self.assertEqual( + io_data.write, BaseData(latency=5.0, io_dump=6.0, io_length=7.0, iops=8.0) + ) + self.assertEqual( + io_data.flush, BaseData(latency=9.0, io_dump=10.0, io_length=11.0, iops=12.0) + ) + self.assertEqual( + io_data.discard, BaseData(latency=13.0, io_dump=14.0, io_length=15.0, iops=16.0) + ) diff --git a/src/python/sentryPlugins/ai_threshold_slow_io_detection/README.md b/src/python/sentryPlugins/ai_threshold_slow_io_detection/README.md new file mode 100644 index 0000000..f9b8388 --- /dev/null +++ b/src/python/sentryPlugins/ai_threshold_slow_io_detection/README.md @@ -0,0 +1,2 @@ +# slow_io_detection + diff --git a/src/python/sentryPlugins/ai_threshold_slow_io_detection/__init__.py b/src/python/sentryPlugins/ai_threshold_slow_io_detection/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/python/sentryPlugins/ai_threshold_slow_io_detection/alarm_report.py b/src/python/sentryPlugins/ai_threshold_slow_io_detection/alarm_report.py new file mode 100644 index 0000000..3f4f34e --- /dev/null +++ b/src/python/sentryPlugins/ai_threshold_slow_io_detection/alarm_report.py @@ -0,0 +1,49 @@ +# coding: utf-8 +# Copyright (c) 2024 Huawei Technologies Co., Ltd. +# sysSentry is licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. + +from syssentry.result import ResultLevel, report_result +import logging +import json + + +class AlarmReport: + TASK_NAME = "SLOW_IO_DETECTION" + + @staticmethod + def report_pass(info: str): + report_result(AlarmReport.TASK_NAME, ResultLevel.PASS, json.dumps({"msg": info})) + logging.info(f'Report {AlarmReport.TASK_NAME} PASS: {info}') + + @staticmethod + def report_fail(info: str): + report_result(AlarmReport.TASK_NAME, ResultLevel.FAIL, json.dumps({"msg": info})) + logging.info(f'Report {AlarmReport.TASK_NAME} FAIL: {info}') + + @staticmethod + def report_skip(info: str): + report_result(AlarmReport.TASK_NAME, ResultLevel.SKIP, json.dumps({"msg": info})) + logging.info(f'Report {AlarmReport.TASK_NAME} SKIP: {info}') + + @staticmethod + def report_minor_alm(info: str): + report_result(AlarmReport.TASK_NAME, ResultLevel.MINOR_ALM, json.dumps({"msg": info})) + logging.info(f'Report {AlarmReport.TASK_NAME} MINOR_ALM: {info}') + + @staticmethod + def report_major_alm(info: str): + report_result(AlarmReport.TASK_NAME, ResultLevel.MAJOR_ALM, json.dumps({"msg": info})) + logging.info(f'Report {AlarmReport.TASK_NAME} MAJOR_ALM: {info}') + + @staticmethod + def report_critical_alm(info: str): + report_result(AlarmReport.TASK_NAME, ResultLevel.CRITICAL_ALM, json.dumps({"msg": info})) + logging.info(f'Report {AlarmReport.TASK_NAME} CRITICAL_ALM: {info}') + diff --git a/src/python/sentryPlugins/ai_threshold_slow_io_detection/config_parser.py b/src/python/sentryPlugins/ai_threshold_slow_io_detection/config_parser.py new file mode 100644 index 0000000..cd4e6f1 --- /dev/null +++ b/src/python/sentryPlugins/ai_threshold_slow_io_detection/config_parser.py @@ -0,0 +1,141 @@ +# coding: utf-8 +# Copyright (c) 2024 Huawei Technologies Co., Ltd. +# sysSentry is licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. + +import configparser +import logging + + +class ConfigParser: + + DEFAULT_ABSOLUTE_THRESHOLD = 40 + DEFAULT_SLOW_IO_DETECTION_FREQUENCY = 1 + DEFAULT_LOG_LEVEL = 'info' + DEFAULT_TRAIN_DATA_DURATION = 24 + DEFAULT_TRAIN_UPDATE_DURATION = 2 + DEFAULT_ALGORITHM_TYPE = 'boxplot' + DEFAULT_N_SIGMA_PARAMETER = 3 + DEFAULT_BOXPLOT_PARAMETER = 1.5 + DEFAULT_SLIDING_WINDOW_TYPE = 'not_continuous' + DEFAULT_WINDOW_SIZE = 30 + DEFAULT_WINDOW_MINIMUM_THRESHOLD = 6 + + def __init__(self, config_file_name): + self.__boxplot_parameter = None + self.__window_minimum_threshold = None + self.__window_size = None + self.__sliding_window_type = None + self.__n_sigma_parameter = None + self.__algorithm_type = None + self.__train_update_duration = None + self.__log_level = None + self.__slow_io_detect_frequency = None + self.__absolute_threshold = None + self.__train_data_duration = None + self.__config_file_name = config_file_name + + def read_config_from_file(self): + + con = configparser.ConfigParser() + con.read(self.__config_file_name, encoding='utf-8') + + items_common = dict(con.items('common')) + items_algorithm = dict(con.items('algorithm')) + items_sliding_window = dict(con.items('sliding_window')) + + try: + self.__absolute_threshold = int(items_common.get('absolute_threshold', + ConfigParser.DEFAULT_ABSOLUTE_THRESHOLD)) + except ValueError: + self.__absolute_threshold = ConfigParser.DEFAULT_ABSOLUTE_THRESHOLD + logging.warning('absolute threshold type conversion has error, use default value.') + + try: + self.__slow_io_detect_frequency = int(items_common.get('slow_io_detect_frequency', + ConfigParser.DEFAULT_SLOW_IO_DETECTION_FREQUENCY)) + except ValueError: + self.__slow_io_detect_frequency = ConfigParser.DEFAULT_SLOW_IO_DETECTION_FREQUENCY + logging.warning('slow_io_detect_frequency type conversion has error, use default value.') + + self.__log_level = items_common.get('log_level', ConfigParser.DEFAULT_LOG_LEVEL) + + try: + self.__train_data_duration = float(items_algorithm.get('train_data_duration', + ConfigParser.DEFAULT_TRAIN_DATA_DURATION)) + except ValueError: + self.__train_data_duration = ConfigParser.DEFAULT_TRAIN_DATA_DURATION + logging.warning('train_data_duration type conversion has error, use default value.') + + try: + self.__train_update_duration = float(items_algorithm.get('train_update_duration', + ConfigParser.DEFAULT_TRAIN_UPDATE_DURATION)) + except ValueError: + self.__train_update_duration = ConfigParser.DEFAULT_TRAIN_UPDATE_DURATION + logging.warning('train_update_duration type conversion has error, use default value.') + + try: + self.__algorithm_type = items_algorithm.get('algorithm_type', ConfigParser.DEFAULT_ALGORITHM_TYPE) + except ValueError: + self.__algorithm_type = ConfigParser.DEFAULT_ALGORITHM_TYPE + logging.warning('algorithmType type conversion has error, use default value.') + + if self.__algorithm_type == 'n_sigma': + try: + self.__n_sigma_parameter = float(items_algorithm.get('n_sigma_parameter', + ConfigParser.DEFAULT_N_SIGMA_PARAMETER)) + except ValueError: + self.__n_sigma_parameter = ConfigParser.DEFAULT_N_SIGMA_PARAMETER + logging.warning('n_sigma_parameter type conversion has error, use default value.') + elif self.__algorithm_type == 'boxplot': + try: + self.__boxplot_parameter = float(items_algorithm.get('boxplot_parameter', + ConfigParser.DEFAULT_BOXPLOT_PARAMETER)) + except ValueError: + self.__boxplot_parameter = ConfigParser.DEFAULT_BOXPLOT_PARAMETER + logging.warning('boxplot_parameter type conversion has error, use default value.') + + self.__sliding_window_type = items_sliding_window.get('sliding_window_type', + ConfigParser.DEFAULT_SLIDING_WINDOW_TYPE) + + try: + self.__window_size = int(items_sliding_window.get('window_size', + ConfigParser.DEFAULT_WINDOW_SIZE)) + except ValueError: + self.__window_size = ConfigParser.DEFAULT_WINDOW_SIZE + logging.warning('window_size type conversion has error, use default value.') + + try: + self.__window_minimum_threshold = ( + int(items_sliding_window.get('window_minimum_threshold', + ConfigParser.DEFAULT_WINDOW_MINIMUM_THRESHOLD))) + except ValueError: + self.__window_minimum_threshold = ConfigParser.DEFAULT_WINDOW_MINIMUM_THRESHOLD + logging.warning('window_minimum_threshold type conversion has error, use default value.') + + def get_slow_io_detect_frequency(self): + return self.__slow_io_detect_frequency + + def get_algorithm_type(self): + return self.__algorithm_type + + def get_sliding_window_type(self): + return self.__sliding_window_type + + def get_train_data_duration_and_train_update_duration(self): + return self.__train_data_duration, self.__train_update_duration + + def get_window_size_and_window_minimum_threshold(self): + return self.__window_size, self.__window_minimum_threshold + + def get_absolute_threshold(self): + return self.__absolute_threshold + + def get_log_level(self): + return self.__log_level diff --git a/src/python/sentryPlugins/ai_threshold_slow_io_detection/data_access.py b/src/python/sentryPlugins/ai_threshold_slow_io_detection/data_access.py new file mode 100644 index 0000000..d9f3460 --- /dev/null +++ b/src/python/sentryPlugins/ai_threshold_slow_io_detection/data_access.py @@ -0,0 +1,91 @@ +# coding: utf-8 +# Copyright (c) 2024 Huawei Technologies Co., Ltd. +# sysSentry is licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. + +import json +import logging + +from sentryCollector.collect_plugin import ( + Result_Messages, + get_io_data, + is_iocollect_valid, +) +from .io_data import IOStageData, IOData + +COLLECT_STAGES = [ + "throtl", + "wbt", + "gettag", + "plug", + "bfq", + "hctx", + "requeue", + "rq_driver", + "bio", + "iocost", +] + +def check_collect_valid(period): + data_raw = is_iocollect_valid(period) + if data_raw["ret"] == 0: + try: + data = json.loads(data_raw["message"]) + except Exception as e: + logging.warning(f"get io data failed, {e}") + return [] + return [k for k in data.keys()] + else: + return [] + + +def _get_raw_data(period, disk_list): + return get_io_data( + period, + disk_list, + COLLECT_STAGES, + ["read", "write", "flush", "discard"], + ) + + +def _get_io_stage_data(data): + io_stage_data = IOStageData() + for data_type in ('read', 'write', 'flush', 'discard'): + if data_type in data: + getattr(io_stage_data, data_type).latency = data[data_type][0] + getattr(io_stage_data, data_type).io_dump = data[data_type][1] + getattr(io_stage_data, data_type).io_length = data[data_type][2] + getattr(io_stage_data, data_type).iops = data[data_type][3] + return io_stage_data + + +def get_io_data_from_collect_plug(period, disk_list): + data_raw = _get_raw_data(period, disk_list) + if data_raw["ret"] == 0: + ret = {} + try: + data = json.loads(data_raw["message"]) + except json.decoder.JSONDecodeError as e: + logging.warning(f"get io data failed, {e}") + return None + + for disk in data: + disk_data = data[disk] + disk_ret = IOData() + for k, v in disk_data.items(): + try: + getattr(disk_ret, k) + setattr(disk_ret, k, _get_io_stage_data(v)) + except AttributeError: + logging.debug(f'no attr {k}') + continue + ret[disk] = disk_ret + return ret + logging.warning(f'get io data failed with message: {data_raw["message"]}') + return None diff --git a/src/python/sentryPlugins/ai_threshold_slow_io_detection/detector.py b/src/python/sentryPlugins/ai_threshold_slow_io_detection/detector.py new file mode 100644 index 0000000..eda9825 --- /dev/null +++ b/src/python/sentryPlugins/ai_threshold_slow_io_detection/detector.py @@ -0,0 +1,48 @@ +# coding: utf-8 +# Copyright (c) 2024 Huawei Technologies Co., Ltd. +# sysSentry is licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. +import logging + +from .io_data import MetricName +from .threshold import Threshold +from .sliding_window import SlidingWindow +from .utils import get_metric_value_from_io_data_dict_by_metric_name + + +class Detector: + _metric_name: MetricName = None + _threshold: Threshold = None + _slidingWindow: SlidingWindow = None + + def __init__(self, metric_name: MetricName, threshold: Threshold, sliding_window: SlidingWindow): + self._metric_name = metric_name + self._threshold = threshold + self._slidingWindow = sliding_window + self._threshold.attach_observer(self._slidingWindow) + + def get_metric_name(self): + return self._metric_name + + def is_slow_io_event(self, io_data_dict_with_disk_name: dict): + logging.debug(f'Enter Detector: {self}') + metric_value = get_metric_value_from_io_data_dict_by_metric_name(io_data_dict_with_disk_name, self._metric_name) + if metric_value > 1e-6: + logging.debug(f'Input metric value: {str(metric_value)}') + self._threshold.push_latest_data_to_queue(metric_value) + detection_result = self._slidingWindow.is_slow_io_event(metric_value) + logging.debug(f'Detection result: {str(detection_result)}') + logging.debug(f'Exit Detector: {self}') + return detection_result + + def __repr__(self): + return (f'disk_name: {self._metric_name.get_disk_name()}, stage_name: {self._metric_name.get_stage_name()},' + f' access_type_name: {self._metric_name.get_io_access_type_name()},' + f' metric_name: {self._metric_name.get_metric_name()}, threshold_type: {self._threshold},' + f' sliding_window_type: {self._slidingWindow}') diff --git a/src/python/sentryPlugins/ai_threshold_slow_io_detection/io_data.py b/src/python/sentryPlugins/ai_threshold_slow_io_detection/io_data.py new file mode 100644 index 0000000..0e17051 --- /dev/null +++ b/src/python/sentryPlugins/ai_threshold_slow_io_detection/io_data.py @@ -0,0 +1,74 @@ +# coding: utf-8 +# Copyright (c) 2024 Huawei Technologies Co., Ltd. +# sysSentry is licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. + +from dataclasses import dataclass, field +from datetime import datetime +from typing import Optional + + +@dataclass +class BaseData: + latency: Optional[float] = field(default_factory=lambda: None) + io_dump: Optional[int] = field(default_factory=lambda: None) + io_length: Optional[int] = field(default_factory=lambda: None) + iops: Optional[int] = field(default_factory=lambda: None) + + +@dataclass +class IOStageData: + read: BaseData = field(default_factory=lambda: BaseData()) + write: BaseData = field(default_factory=lambda: BaseData()) + flush: BaseData = field(default_factory=lambda: BaseData()) + discard: BaseData = field(default_factory=lambda: BaseData()) + + +@dataclass +class IOData: + throtl: IOStageData = field(default_factory=lambda: IOStageData()) + wbt: IOStageData = field(default_factory=lambda: IOStageData()) + gettag: IOStageData = field(default_factory=lambda: IOStageData()) + iocost: IOStageData = field(default_factory=lambda: IOStageData()) + plug: IOStageData = field(default_factory=lambda: IOStageData()) + bfq: IOStageData = field(default_factory=lambda: IOStageData()) + hctx: IOStageData = field(default_factory=lambda: IOStageData()) + requeue: IOStageData = field(default_factory=lambda: IOStageData()) + rq_driver: IOStageData = field(default_factory=lambda: IOStageData()) + bio: IOStageData = field(default_factory=lambda: IOStageData()) + time_stamp: float = field(default_factory=lambda: datetime.now().timestamp()) + + +class MetricName: + _disk_name: str = None + _stage_name: str = None + _io_access_type_name: str = None + _metric_name: str = None + + def __init__(self, disk_name: str, stage_name: str, io_access_type_name: str, metric_name: str): + self._disk_name = disk_name + self._stage_name = stage_name + self._io_access_type_name = io_access_type_name + self._metric_name = metric_name + + def get_disk_name(self): + return self._disk_name + + def get_stage_name(self): + return self._stage_name + + def get_io_access_type_name(self): + return self._io_access_type_name + + def get_metric_name(self): + return self._metric_name + + def __repr__(self): + return (f'disk: {self._disk_name}, stage: {self._stage_name}, io_access_type: {self._io_access_type_name},' + f'metric: {self._metric_name}') diff --git a/src/python/sentryPlugins/ai_threshold_slow_io_detection/sliding_window.py b/src/python/sentryPlugins/ai_threshold_slow_io_detection/sliding_window.py new file mode 100644 index 0000000..d395d48 --- /dev/null +++ b/src/python/sentryPlugins/ai_threshold_slow_io_detection/sliding_window.py @@ -0,0 +1,113 @@ +# coding: utf-8 +# Copyright (c) 2024 Huawei Technologies Co., Ltd. +# sysSentry is licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. + +from enum import Enum, unique +import numpy as np + + +@unique +class SlidingWindowType(Enum): + NotContinuousSlidingWindow = 0 + ContinuousSlidingWindow = 1 + MedianSlidingWindow = 2 + + +class SlidingWindow: + _ai_threshold = None + _queue_length = None + _queue_threshold = None + _io_data_queue: list = None + _io_data_queue_abnormal_tag: list = None + + def __init__(self, queue_length: int, threshold: int): + self._queue_length = queue_length + self._queue_threshold = threshold + self._io_data_queue = [] + self._io_data_queue_abnormal_tag = [] + + def push(self, data: float): + if len(self._io_data_queue) == self._queue_length: + self._io_data_queue.pop(0) + self._io_data_queue_abnormal_tag.pop(0) + self._io_data_queue.append(data) + self._io_data_queue_abnormal_tag.append(data >= self._ai_threshold if self._ai_threshold is not None else False) + + def update(self, threshold): + if self._ai_threshold == threshold: + return + self._ai_threshold = threshold + self._io_data_queue_abnormal_tag.clear() + for data in self._io_data_queue: + self._io_data_queue_abnormal_tag.append(data >= self._ai_threshold) + + def is_slow_io_event(self, data): + return False, None, None + + def __repr__(self): + return "SlidingWindow" + + +class NotContinuousSlidingWindow(SlidingWindow): + def is_slow_io_event(self, data): + super().push(data) + if len(self._io_data_queue) < self._queue_length or self._ai_threshold is None: + return False, self._io_data_queue, self._ai_threshold + if self._io_data_queue_abnormal_tag.count(True) >= self._queue_threshold: + return True, self._io_data_queue, self._ai_threshold + return False, self._io_data_queue, self._ai_threshold + + def __repr__(self): + return "NotContinuousSlidingWindow" + + +class ContinuousSlidingWindow(SlidingWindow): + def is_slow_io_event(self, data): + super().push(data) + if len(self._io_data_queue) < self._queue_length or self._ai_threshold is None: + return False, self._io_data_queue, self._ai_threshold + consecutive_count = 0 + for tag in self._io_data_queue_abnormal_tag: + if tag: + consecutive_count += 1 + if consecutive_count >= self._queue_threshold: + return True, self._io_data_queue, self._ai_threshold + else: + consecutive_count = 0 + return False, self._io_data_queue, self._ai_threshold + + def __repr__(self): + return "ContinuousSlidingWindow" + + +class MedianSlidingWindow(SlidingWindow): + def is_slow_io_event(self, data): + super().push(data) + if len(self._io_data_queue) < self._queue_length or self._ai_threshold is None: + return False, self._io_data_queue, self._ai_threshold + median = np.median(self._io_data_queue) + if median >= self._ai_threshold: + return True, self._io_data_queue, self._ai_threshold + return False, self._io_data_queue, self._ai_threshold + + def __repr__(self): + return "MedianSlidingWindow" + + +class SlidingWindowFactory: + def get_sliding_window(self, sliding_window_type: SlidingWindowType, *args, **kwargs): + if sliding_window_type == SlidingWindowType.NotContinuousSlidingWindow: + return NotContinuousSlidingWindow(*args, **kwargs) + elif sliding_window_type == SlidingWindowType.ContinuousSlidingWindow: + return ContinuousSlidingWindow(*args, **kwargs) + elif sliding_window_type == SlidingWindowType.MedianSlidingWindow: + return MedianSlidingWindow(*args, **kwargs) + else: + return NotContinuousSlidingWindow(*args, **kwargs) diff --git a/src/python/sentryPlugins/ai_threshold_slow_io_detection/slow_io_detection.py b/src/python/sentryPlugins/ai_threshold_slow_io_detection/slow_io_detection.py new file mode 100644 index 0000000..43cf770 --- /dev/null +++ b/src/python/sentryPlugins/ai_threshold_slow_io_detection/slow_io_detection.py @@ -0,0 +1,133 @@ +# coding: utf-8 +# Copyright (c) 2024 Huawei Technologies Co., Ltd. +# sysSentry is licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. + +import time +import signal +import logging + +from .detector import Detector +from .threshold import ThresholdFactory, AbsoluteThreshold +from .sliding_window import SlidingWindowFactory +from .utils import (get_threshold_type_enum, get_sliding_window_type_enum, get_data_queue_size_and_update_size, + get_log_level) +from .config_parser import ConfigParser +from .data_access import get_io_data_from_collect_plug, check_collect_valid +from .io_data import MetricName +from .alarm_report import AlarmReport + +CONFIG_FILE = "/etc/sysSentry/plugins/ai_threshold_slow_io_detection.ini" + + +def sig_handler(signum, frame): + logging.info("receive signal: %d", signum) + AlarmReport().report_fail(f"receive signal: {signum}") + exit(signum) + + +class SlowIODetection: + _config_parser = None + _disk_list = None + _detector_name_list = [] + _detectors = {} + + def __init__(self, config_parser: ConfigParser): + self._config_parser = config_parser + self.__set_log_format() + self.__init_detector_name_list() + self.__init_detector() + + def __set_log_format(self): + log_format = "%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s" + log_level = get_log_level(self._config_parser.get_log_level()) + logging.basicConfig(level=log_level, format=log_format) + + def __init_detector_name_list(self): + self._disk_list = check_collect_valid(self._config_parser.get_slow_io_detect_frequency()) + for disk in self._disk_list: + self._detector_name_list.append(MetricName(disk, "bio", "read", "latency")) + self._detector_name_list.append(MetricName(disk, "bio", "write", "latency")) + + def __init_detector(self): + train_data_duration, train_update_duration = (self._config_parser. + get_train_data_duration_and_train_update_duration()) + slow_io_detection_frequency = self._config_parser.get_slow_io_detect_frequency() + threshold_type = get_threshold_type_enum(self._config_parser.get_algorithm_type()) + data_queue_size, update_size = get_data_queue_size_and_update_size(train_data_duration, + train_update_duration, + slow_io_detection_frequency) + sliding_window_type = get_sliding_window_type_enum(self._config_parser.get_sliding_window_type()) + window_size, window_threshold = self._config_parser.get_window_size_and_window_minimum_threshold() + + for detector_name in self._detector_name_list: + threshold = ThresholdFactory().get_threshold(threshold_type, data_queue_size=data_queue_size, + data_queue_update_size=update_size) + sliding_window = SlidingWindowFactory().get_sliding_window(sliding_window_type, queue_length=window_size, + threshold=window_threshold) + detector = Detector(detector_name, threshold, sliding_window) + # 绝对阈值的阈值初始化 + if isinstance(threshold, AbsoluteThreshold): + threshold.set_threshold(self._config_parser.get_absolute_threshold()) + self._detectors[detector_name] = detector + logging.info(f"add detector: {detector}") + + def launch(self): + while True: + logging.debug('step0. AI threshold slow io event detection is looping.') + + # Step1:获取IO数据 + io_data_dict_with_disk_name = get_io_data_from_collect_plug( + self._config_parser.get_slow_io_detect_frequency(), self._disk_list + ) + logging.debug(f'step1. Get io data: {str(io_data_dict_with_disk_name)}') + if io_data_dict_with_disk_name is None: + continue + # Step2:慢IO检测 + logging.debug('step2. Start to detection slow io event.') + slow_io_event_list = [] + for metric_name, detector in self._detectors.items(): + result = detector.is_slow_io_event(io_data_dict_with_disk_name) + if result[0]: + slow_io_event_list.append((detector.get_metric_name(), result)) + logging.debug('step2. End to detection slow io event.') + + # Step3:慢IO事件上报 + logging.debug('step3. Report slow io event to sysSentry.') + for slow_io_event in slow_io_event_list: + metric_name: MetricName = slow_io_event[0] + result = slow_io_event[1] + AlarmReport.report_major_alm(f"disk {metric_name.get_disk_name()} has slow io event." + f"stage: {metric_name.get_metric_name()}," + f"type: {metric_name.get_io_access_type_name()}," + f"metric: {metric_name.get_metric_name()}," + f"current window: {result[1]}," + f"threshold: {result[2]}") + logging.error(f"slow io event happen: {str(slow_io_event)}") + + # Step4:等待检测时间 + logging.debug('step4. Wait to start next slow io event detection loop.') + time.sleep(self._config_parser.get_slow_io_detect_frequency()) + + +def main(): + # Step1:注册消息处理函数 + signal.signal(signal.SIGINT, sig_handler) + signal.signal(signal.SIGTERM, sig_handler) + # Step2:断点恢复 + # todo: + + # Step3:读取配置 + config_file_name = CONFIG_FILE + config = ConfigParser(config_file_name) + config.read_config_from_file() + + # Step4:启动慢IO检测 + slow_io_detection = SlowIODetection(config) + slow_io_detection.launch() diff --git a/src/python/sentryPlugins/ai_threshold_slow_io_detection/threshold.py b/src/python/sentryPlugins/ai_threshold_slow_io_detection/threshold.py new file mode 100644 index 0000000..9e1ca7b --- /dev/null +++ b/src/python/sentryPlugins/ai_threshold_slow_io_detection/threshold.py @@ -0,0 +1,160 @@ +# coding: utf-8 +# Copyright (c) 2024 Huawei Technologies Co., Ltd. +# sysSentry is licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. +import logging +from enum import Enum +import queue +import numpy as np +import math + +from .sliding_window import SlidingWindow + + +class ThresholdState(Enum): + INIT = 0 + START = 1 + + +class Threshold: + threshold = None + data_queue: queue.Queue = None + data_queue_update_size: int = None + new_data_size: int = None + threshold_state: ThresholdState = None + + def __init__(self, data_queue_size: int = 10000, data_queue_update_size: int = 1000): + self._observer = None + self.data_queue = queue.Queue(data_queue_size) + self.data_queue_update_size = data_queue_update_size + self.new_data_size = 0 + self.threshold_state = ThresholdState.INIT + self.threshold = math.inf + + def set_threshold(self, threshold): + self.threshold = threshold + self.threshold_state = ThresholdState.START + self.notify_observer() + + def get_threshold(self): + if self.threshold_state == ThresholdState.INIT: + return None + return self.threshold + + def is_abnormal(self, data): + if self.threshold_state == ThresholdState.INIT: + return False + return data >= self.threshold + + # 使用观察者模式,当阈值更新时,自动同步刷新滑窗中的阈值 + def attach_observer(self, observer: SlidingWindow): + self._observer = observer + + def notify_observer(self): + if self._observer is not None: + self._observer.update(self.threshold) + + def push_latest_data_to_queue(self, data): + pass + + def __repr__(self): + return "Threshold" + + +class AbsoluteThreshold(Threshold): + def __init__(self, data_queue_size: int = 10000, data_queue_update_size: int = 1000): + super().__init__(data_queue_size, data_queue_update_size) + + def push_latest_data_to_queue(self, data): + pass + + def __repr__(self): + return "AbsoluteThreshold" + + +class BoxplotThreshold(Threshold): + def __init__(self, parameter: float = 1.5, data_queue_size: int = 10000, data_queue_update_size: int = 1000): + super().__init__(data_queue_size, data_queue_update_size) + self.parameter = parameter + + def _update_threshold(self): + data = list(self.data_queue.queue) + q1 = np.percentile(data, 25) + q3 = np.percentile(data, 75) + iqr = q3 - q1 + self.threshold = q3 + self.parameter * iqr + if self.threshold_state == ThresholdState.INIT: + self.threshold_state = ThresholdState.START + self.notify_observer() + + def push_latest_data_to_queue(self, data): + try: + self.data_queue.put(data, block=False) + except queue.Full: + self.data_queue.get() + self.data_queue.put(data) + self.new_data_size += 1 + if (self.data_queue.full() and (self.threshold_state == ThresholdState.INIT or + (self.threshold_state == ThresholdState.START and + self.new_data_size >= self.data_queue_update_size))): + self._update_threshold() + self.new_data_size = 0 + + def __repr__(self): + return "BoxplotThreshold" + + +class NSigmaThreshold(Threshold): + def __init__(self, parameter: float = 2.0, data_queue_size: int = 10000, data_queue_update_size: int = 1000): + super().__init__(data_queue_size, data_queue_update_size) + self.parameter = parameter + + def _update_threshold(self): + data = list(self.data_queue.queue) + mean = np.mean(data) + std = np.std(data) + self.threshold = mean + self.parameter * std + if self.threshold_state == ThresholdState.INIT: + self.threshold_state = ThresholdState.START + self.notify_observer() + + def push_latest_data_to_queue(self, data): + try: + self.data_queue.put(data, block=False) + except queue.Full: + self.data_queue.get() + self.data_queue.put(data) + self.new_data_size += 1 + if (self.data_queue.full() and (self.threshold_state == ThresholdState.INIT or + (self.threshold_state == ThresholdState.START and + self.new_data_size >= self.data_queue_update_size))): + self._update_threshold() + self.new_data_size = 0 + + def __repr__(self): + return "NSigmaThreshold" + + +class ThresholdType(Enum): + AbsoluteThreshold = 0 + BoxplotThreshold = 1 + NSigmaThreshold = 2 + + +class ThresholdFactory: + def get_threshold(self, threshold_type: ThresholdType, *args, **kwargs): + if threshold_type == ThresholdType.AbsoluteThreshold: + return AbsoluteThreshold(*args, **kwargs) + elif threshold_type == ThresholdType.BoxplotThreshold: + return BoxplotThreshold(*args, **kwargs) + elif threshold_type == ThresholdType.NSigmaThreshold: + return NSigmaThreshold(*args, **kwargs) + else: + raise ValueError(f"Invalid threshold type: {threshold_type}") + diff --git a/src/python/sentryPlugins/ai_threshold_slow_io_detection/utils.py b/src/python/sentryPlugins/ai_threshold_slow_io_detection/utils.py new file mode 100644 index 0000000..f66e5ed --- /dev/null +++ b/src/python/sentryPlugins/ai_threshold_slow_io_detection/utils.py @@ -0,0 +1,67 @@ +# coding: utf-8 +# Copyright (c) 2024 Huawei Technologies Co., Ltd. +# sysSentry is licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. +import logging +from dataclasses import asdict + +from .threshold import ThresholdType +from .sliding_window import SlidingWindowType +from .io_data import MetricName, IOData + +def get_threshold_type_enum(algorithm_type: str): + if algorithm_type.lower() == 'absolute': + return ThresholdType.AbsoluteThreshold + if algorithm_type.lower() == 'boxplot': + return ThresholdType.BoxplotThreshold + if algorithm_type.lower() == 'n_sigma': + return ThresholdType.NSigmaThreshold + logging.info('not found correct algorithm type, use default: boxplot.') + return ThresholdType.BoxplotThreshold + + +def get_sliding_window_type_enum(sliding_window_type: str): + if sliding_window_type.lower() == 'not_continuous': + return SlidingWindowType.NotContinuousSlidingWindow + if sliding_window_type.lower() == 'continuous': + return SlidingWindowType.ContinuousSlidingWindow + if sliding_window_type.lower() == 'median': + return SlidingWindowType.MedianSlidingWindow + logging.info('not found correct sliding window type, use default: not_continuous.') + return SlidingWindowType.NotContinuousSlidingWindow + + +def get_metric_value_from_io_data_dict_by_metric_name(io_data_dict: dict, metric_name: MetricName): + try: + io_data: IOData = io_data_dict[metric_name.get_disk_name()] + io_stage_data = asdict(io_data)[metric_name.get_stage_name()] + base_data = io_stage_data[metric_name.get_io_access_type_name()] + metric_value = base_data[metric_name.get_metric_name()] + return metric_value + except KeyError: + return None + + +def get_data_queue_size_and_update_size(training_data_duration: float, train_update_duration: float, + slow_io_detect_frequency: int): + data_queue_size = int(training_data_duration * 60 * 60 / slow_io_detect_frequency) + update_size = int(train_update_duration * 60 * 60 / slow_io_detect_frequency) + return data_queue_size, update_size + + +def get_log_level(log_level: str): + if log_level.lower() == 'debug': + return logging.DEBUG + elif log_level.lower() == 'info': + return logging.INFO + elif log_level.lower() == 'warning': + return logging.WARNING + elif log_level.lower() == 'fatal': + return logging.FATAL + return None diff --git a/src/python/setup.py b/src/python/setup.py index c28c691..dac6481 100644 --- a/src/python/setup.py +++ b/src/python/setup.py @@ -33,7 +33,8 @@ setup( 'syssentry=syssentry.syssentry:main', 'xalarmd=xalarm.xalarm_daemon:alarm_process_create', 'sentryCollector=sentryCollector.collectd:main', - 'avg_block_io=sentryPlugins.avg_block_io.avg_block_io:main' + 'avg_block_io=sentryPlugins.avg_block_io.avg_block_io:main', + 'ai_threshold_slow_io_detection=sentryPlugins.ai_threshold_slow_io_detection.slow_io_detection:main' ] }, ) -- 2.23.0