一灰灰blog 一灰灰blog
首页
  • InfluxDB
  • MongoDB
  • MySql
  • 基础系列
  • DB系列
  • 搜索系列
  • MQ系列
  • WEB系列
  • 中间件
  • 运维
  • SpringSecurity
  • SpringCloud
  • QuickAlarm
  • QuickCrawer
  • QuickFix
  • QuickMedia
  • QuickSpi
  • QuickTask
  • 分类
  • 标签
  • 归档
  • 收藏
  • 关于
GitHub (opens new window)

一灰灰blog

资深搬运工
首页
  • InfluxDB
  • MongoDB
  • MySql
  • 基础系列
  • DB系列
  • 搜索系列
  • MQ系列
  • WEB系列
  • 中间件
  • 运维
  • SpringSecurity
  • SpringCloud
  • QuickAlarm
  • QuickCrawer
  • QuickFix
  • QuickMedia
  • QuickSpi
  • QuickTask
  • 分类
  • 标签
  • 归档
  • 收藏
  • 关于
GitHub (opens new window)
  • QuickAlarm

    • 1. 报警系统QuickAlarm设计总纲
    • 2. 报警系统QuickAlarm之报警执行器的设计与实现
    • 3. 报警系统QuickAlarm之报警规则的设定与加载
      • I. 报警规则定义
      • II. 报警规则加载
        • 1. 配置加载
        • 2. RegisterInfo 加载
      • III. ConfLoader选择并初始化
      • IV. 小结
      • V. 其他
        • 相关博文
        • 项目
        • 个人博客: Z+|blog
        • 声明
        • 扫描关注,java分享
    • 5. 报警系统QuickAlarm之频率统计及接口封装
    • 6. 报警系统QuickAlarm使用手册
    • 7. 报警系统QuickAlarm之默认报警规则扩展
    • Quick-Alarm 钉钉报警支持
  • QuickCrawer

  • QuickFix

  • QuickMedia

  • QuickSpi

  • QuickTask

  • Quick开源系列
  • QuickAlarm
一灰灰
2018-02-09

3. 报警系统QuickAlarm之报警规则的设定与加载

前面一篇是报警执行器的定义与加载已经完成,但与之对应的报警规则有是如何定义和加载的呢?

此外,既然命名为规则,那么就需要有对应的解析器,以根据报警规则和报警类型等相关输入条件,来选择对应的报警执行器,因此本文主要包括的内容就比较清晰了

  • 报警规则的定义
  • 报警规则的加载
  • 报警规则的解析以及报警执行器选择

# I. 报警规则定义

目前针对报警规则没有给出自定义配置的入口,即完全采用了默认的方案,后续可以考虑支持适用方来自定义报警规则以及解析器,这样扩展性就更强了

首先说明下我们的设计规则,我们针对不同的AlarmExecute定义了一个优先级,我们的目标是

  • 针对报警频率设置不同区间,每个区间对应一种报警类型
  • 当实际调用的报警频率达到这个区间,就选择这种报警类型
  • 同时也允许关闭根据频率选择报警器的功能,全程用一个默认
  • 每种报警类型的用户都可以自定义

针对上面的目标,我们设计的类就比较明确了

阀值类:

@Getter
@Setter
@ToString
public class AlarmThreshold implements Comparable<AlarmThreshold> {

    /**
     * 报警类型,对应 {@link IExecute#getName()}
     */
    private String alarmLevel;


    /**
     * 晋升此报警的阀值
     */
    private int threshold;


    /**
     * 对应的报警用户
     */
    private List<String> users;


    @Override
    public int compareTo(AlarmThreshold o) {
        if (o == null) {
            return -1;
        }

        return threshold - o.getThreshold();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

配置类:

@Getter
@Setter
@ToString
public class AlarmConfig {

    public static final int DEFAULT_MIN_NUM = 0;
    public static final int DEFAULT_MAX_NUM = 30;

    /**
     * 报警用户
     */
    private List<String> users;


    /**
     * 报警的阀值
     */
    private List<AlarmThreshold> alarmThreshold;


    /**
     * 最小的报警数
     */
    private int minLimit;


    /**
     * 最大的报警数
     */
    private int maxLimit;


    /**
     * 报警类型 {@link IExecute#getName()}
     */
    private String alarmLevel;


    /**
     * true 表示当报警超过当前的阀值之后, 将提升报警的程度
     */
    private boolean autoIncEmergency;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

一个报警类型对应一个AlarmConfig,这样当执行报警时,就可以很容易的获取对应的规则

同样根据定义,也可以看出报警规则比较简单,直接根据阀值区间来选择

# II. 报警规则加载

关于如何加载报警规则,想了很久,选择把这块放开,因为我们无法确定,使用方的配置是存在什么地方的,而且使用的配置是否能和我们的设计的DO兼容也是个问题,因此干脆放手,同样是通过SPI的方式来做的

我们定义规则加载接口: IConfLoader

public interface IConfLoader {

    /**
     * 加载配置到内存的操作,启动时,被调用
     *
     * @return true 表示加载成功; false 表示加载失败
     */
    default boolean load() {
        return true;
    }


    /**
     * 排序,越小优先级越高
     * <p>
     * 说明: 当系统中多个Loader存在时,会根据优先级来选择order最小的一个作为默认的Loader
     *
     * @return
     */
    default int order() {
        return 10;
    }


    /**
     * 获取注册信息
     *
     * @return
     */
    RegisterInfo getRegisterInfo();


    /**
     * 是否开启报警
     *
     * @return
     */
    boolean alarmEnable();


    /**
     * 根据报警类型,获取对应的报警规则
     *
     * @param alarmKey
     * @return
     */
    AlarmConfig getAlarmConfig(String alarmKey);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

上面的方法,可以划分为两类:

  • 加载时使用
    • load 为具体的执行加载配置到内存的方法,返回true表示加载成功
    • order 排序
    • getRegisterInfo 获取基础的配置信息(包括应用名等相关配置)
  • 业务运行时使用
    • alarmEnable : 是否开启报警 (当大量报警时,可以先关闭报警,然后再查问题)
    • getAlarmConfig:核心方法,根据报警类型,返回对应的报警规则

系统默认提供一个从配置文件中加载报警规则的方案,主要会依赖两个配置文件

  • alarm.properties : 初始化注册信息,内部保存 RegisterInfo 所需要的属性
  • alarmConfig : 保存具体的报警规则,json格式

# 1. 配置加载

配置加载的实现逻辑,如下

public class PropertiesConfLoader implements IConfLoader {

    private RegisterInfo registerInfo;

    private Map<String, AlarmConfig> cacheMap;

    public boolean load() {
        // 获取注册信息
        registerInfo = RegisterInfoLoaderHelper.load();
        if (registerInfo == null) {
            return false;
        }


        // 获取报警的配置类
        File file;
        String path = registerInfo.getAlarmConfPath();
        if (path.startsWith("/")) {
            file = new File(path);
        } else {
            URL url = this.getClass().getClassLoader().getResource(path);
            file = new File(url.getFile());
        }


        // 加载成功,才替换 cacheMap的内容; 主要是为了防止修改配置出现问题
        Map<String, AlarmConfig> tmp = init(file);
        boolean ans = tmp != null;
        // 注册配置文件的变动
        ans = ans && PropertiesConfListenerHelper.registerConfChangeListener(file, this::init);

        if (ans) {
            cacheMap = tmp;
        }
        return ans;
    }


    private Map<String, AlarmConfig> init(File file) {
        try {
            // 正常来讲,是一个完整的json串
            List<String> list = IOUtils.readLines(new FileInputStream(file), "utf-8");
            String config = Joiner.on("").join(list);
            return AlarmConfParse.parseConfig(config, Splitter.on(",").splitToList(registerInfo.getDefaultAlarmUsers()));
        } catch (IOException e) {
            log.error("load config into cacheMap error! e: {}", e);
            return null;
        }
    }


    @Override
    public RegisterInfo getRegisterInfo() {
        return registerInfo;
    }

    @Override
    public boolean alarmEnable() {
        return true;
    }

    @Override
    public AlarmConfig getAlarmConfig(String alarmKey) {
        AlarmConfig config = cacheMap.get(alarmKey);
        if (config == null) {
            return cacheMap.get(AlarmConfParse.DEFAULT_ALARM_KEY);
        } else {
            return config;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

主要查看默认的load方法即可, alarmEnable 和 getAlarmConfig还是比较简单的,看一下就知道怎么玩的

# 2. RegisterInfo 加载

上面的实现中,第一步就是从 alarm.properteis 文件中读取对应的配置,然后初始化 RegisterInfo对象

@Data
public class RegisterInfo implements Serializable {
    // 报警规则文件的路径,系统默认加载时,必填;否则选填
    private String alarmConfPath;
    // 最大报警类型数,非必填,默认1000
    private Integer maxAlarmType;
    // 默认报警用户, 必须
    private String defaultAlarmUsers;
    // 应用名, 必须
    private String appName;
}
1
2
3
4
5
6
7
8
9
10
11

一个配置文件实例

appName=test
alarmConfPath=/tmp/alarmConfig
maxAlarmType=1000
defaultAlarmUsers=yihui
1
2
3
4

从配置文件中读取信息,然后初始化对象的过程就比较简单了,我这里做了一个小简化,使用反射的方式实现对象拷贝

public static void copy(Properties source, Object dest) throws IllegalAccessException {
    Field[] fields = dest.getClass().getDeclaredFields();
    for (Field f : fields) {
        // 不修改静态变量
        if (Modifier.isStatic(f.getModifiers())) {
            continue;
        }

        f.setAccessible(true);
        // 值拷贝,因为不同数据类型的问题,所以需要对properties中获取的String类型转换一把
        f.set(dest, parseObj(source.getProperty(f.getName()), f.getType()));
    }
}

// 强制类型转换
private static <T> T parseObj(String obj, Class<T> clz) {
    return ParseFuncEnum.getFunc(clz).apply(obj);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

上面的实现目前比较简单,没有考虑父类的情况,没有考虑复杂的数据类型转换,目前只支持了基本类型的转换,后续可考虑抽象

public enum ParseFuncEnum {

    INT_PARSE(Arrays.asList(int.class, Integer.class)) {
        @Override
        public Function<String, Integer> getFunc() {
            return Integer::valueOf;
        }
    },
    LONG_PARSE(Arrays.asList(long.class, Long.class)) {
        @Override
        public Function<String, Long> getFunc() {
            return Long::valueOf;
        }
    },
    BOOLEAN_PARSE(Arrays.asList(boolean.class, Boolean.class)) {
        @Override
        public Function<String, Boolean> getFunc() {
            return Boolean::valueOf;
        }
    },
    FLOAT_PARSE(Arrays.asList(float.class, Float.class)) {
        @Override
        public Function<String, Float> getFunc() {
            return Float::valueOf;
        }
    },
    DOUBLE_PARSSE(Arrays.asList(double.class, Double.class)) {
        @Override
        public Function<String, Double> getFunc() {
            return Double::valueOf;
        }
    },
    SHORT_PARSE(Arrays.asList(short.class, Short.class)) {
        @Override
        public Function<String, Short> getFunc() {
            return Short::valueOf;
        }
    },
    BYTE_PARSE(Arrays.asList(byte.class, Byte.class)) {
        @Override
        public Function<String, Byte> getFunc() {
            return Byte::valueOf;
        }
    },
    CHAR_PARSE(Arrays.asList(char.class, Character.class)) {
        @Override
        public Function<String, Character> getFunc() {
            return s -> s.charAt(0);
        }
    },
    STRING_PARSE(Arrays.asList(String.class)) {
        @Override
        public Function<String, String> getFunc() {
            return s -> s;
        }
    },;

    private List<Class> clzList;
    public abstract <T> Function<String, T> getFunc();

    private static Map<Class, ParseFuncEnum> map = new ConcurrentHashMap<>(20);
    static {
        for (ParseFuncEnum enu : ParseFuncEnum.values()) {
            for (Class clz : enu.clzList) {
                map.put(clz, enu);
            }
        }
    }

    ParseFuncEnum(List<Class> clz) {
        this.clzList = clz;
    }

    public static <T> Function<String, T> getFunc(Class<T> clz) {
        return map.get(clz).getFunc();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

# 3. 报警规则加载

注册信息加载完毕之后,就可以获取报警规则的文件地址了,因此首先是读取配置规则的内容(我们要求是JSON格式),然后反序列化即可

将json串格式配置,反序列化为 BaseAlarmConf 对象

private static final TypeReference<Map<String, BasicAlarmConfig>> typeReference 
    = new TypeReference<Map<String, BasicAlarmConfig>>() {};

/**
 * 将json串格式的报警规则配置,映射为对应实体类
 * <p>
 * 如果传如的是null, 则采用默认的兜底配置
 * 如果传入的是非法的配置,直接返回null, 这样做的目的如下
 * <p>
 * - 启动时,直接获知配置有问题,需要修改
 * - 启动中,修改配置,此时新配置有问题,依然使用旧的配置
 *
 * @param configs
 * @return
 */
private static Map<String, BasicAlarmConfig> parseStrConfig2Map(String configs) {
    Map<String, BasicAlarmConfig> map = null;

    if (configs != null) {
        try {
            map = JSON.parseObject(configs, typeReference);
        } catch (Exception e) {
            logger.error("ConfigWrapper.parseStrConfig2Map() init config error! configs: {}, e:{}", configs, e);
            return null;
        }
    }

    if (map == null) {
        map = new HashMap<>(1);
    }


    if (!map.containsKey(DEFAULT_ALARM_KEY)) {
        map.put(DEFAULT_ALARM_KEY, DEFAULT_ALARM_CONFIG);
    }
    return map;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

需要额外说明一下,json串并没有直接的映射我们前面定义的 AlarmConfig 对象,因为在原型版本的设计的过程中,考虑到配置与内部的使用对象,可能不是特别匹配,最初的设计中,是希望直接将AlarmConfig中的alarmLevel直接替换成 AlarmExecute 实例对象的,然而在实际实现中没有这么干...,所以看源码时,这里就有点奇怪,后面完全可以干掉这个无用的逻辑

此外,就是需要给一个默认的配置项,当报警类型匹配不到对应的报警规则时,就选择默认的了

下面是一个报警配置的demo

{
    "default": {
        "level": "LOG",
        "autoIncEmergency": true,
        "max": 30,
        "min": 3,
        "threshold": [
            {
                "level": "SMS",
                "threshold": 20,
                "users": [
                    "345345345345",
                    "123123123123"
                ]
            },
            {
                "level": "WEIXIN",
                "threshold": 10,
                "users": [
                    "yihui",
                    "erhui"
                ]
            },
            {
                "level": "LOG",
                "threshold": 5,
                "users": [
                    "yihui",
                    "erhui"
                ]
            }
        ],
        "users": [
            "yihui"
        ]
    },
    "NPE": {
        "level": "WEIXIN",
        "autoIncEmergency": false,
        "max": 30,
        "min": 0,
        "threshold": [
            {
                "level": "SMS",
                "threshold": 20,
                "users": [
                    "345345345345",
                    "123123123123"
                ]
            },
            {
                "level": "WEIXIN",
                "threshold": 10,
                "users": [
                    "3h    ui",
                    "4hui"
                ]
            }
        ],
        "users": [
            "yihui"
        ]
    },
    "XXX,YYY": {
        "level": "EMAIL",
        "autoIncEmergency": true,
        "max": 30,
        "min": 3,
        "threshold": [
            {
                "level": "SMS",
                "threshold": 20,
                "users": [
                    "345345345345",
                    "123123123123"
                ]
            },
            {
                "level": "WEIXIN",
                "threshold": 10,
                "users": [
                    "yihui",
                    "erhui"
                ]
            },
            {
                "level": "EMAIL",
                "threshold": 5,
                "users": [
                    "yihui@xxx.com",
                    "erhui@xxx.com"
                ]
            }
        ],
        "users": [
            "yihui@xxx.com"
        ]
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99

# III. ConfLoader选择并初始化

前面说明,为了确保报警规则的多样性存储与加载,我们支持用户自定义加载类,所以就会有这么个ConfLoaderFactory, 来创建系统中使用的ConfLoader

public class ConfLoaderFactory {

    private static IConfLoader currentAlarmConfLoader;

    public static IConfLoader loader() {
        if (currentAlarmConfLoader == null) {
            synchronized (ConfLoaderFactory.class) {
                if (currentAlarmConfLoader == null) {
                    initConfLoader();
                }
            }
        }

        return currentAlarmConfLoader;
    }


    private static void initConfLoader() {
        Iterator<IConfLoader> iterator = ServiceLoader.load(IConfLoader.class).iterator();

        List<IConfLoader> list = new ArrayList<>();
        // 根据优先级进行排序,选择第一个加载成功的Loader
        while (iterator.hasNext()) {
            list.add(iterator.next());
        }
        list.sort(Comparator.comparingInt(IConfLoader::order));

        for (IConfLoader iConfLoader : list) {
            if (iConfLoader.load()) {
                currentAlarmConfLoader = iConfLoader;
                break;
            }
        }


        if (currentAlarmConfLoader == null) {
            throw new NoAlarmLoaderSpecifyException("no special alarmConfLoader selected!");
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

实现逻辑依旧采取了SPI机制,不够我们定义了一个优先级,默认从最高优先级的开始加载,加载成功之后,就选择这个东西了;否则继续加载下一个,当所有的ConfLoader加载完毕,都没有一个成功的,就抛出一个异常

# IV. 小结

鉴于篇幅问题,关于报警规则与报警执行器之间的关系,对应的解释器放在下一篇进行说明,简要小结一下本文内容

  • 报警规则: 采用阀值区间方式,将报警频率与报警执行器关联起来
  • 规则加载: 支持SPI方式注入用户加载器,默认提供基于配置文件的加载器,且优先级最低

基本上本文说的就是下面这张图的内容了

应用启动.png

# V. 其他

# 相关博文

  1. 报警系统QuickAlarm总纲 (opens new window)
  2. 报警系统QuickAlarm之报警执行器的设计与实现 (opens new window)
  3. 报警系统QuickAlarm之报警规则的设定与加载 (opens new window)
  4. 报警系统QuickAlarm之报警规则解析 (opens new window)
  5. 报警系统QuickAlarm之频率统计及接口封装 (opens new window)
  6. 报警系统QuickAlarm使用手册 (opens new window)
  7. 报警系统QuickAlarm之默认报警规则扩展 (opens new window)

# 项目

  • 项目地址: Quick-Alarm (opens new window)
  • 博客地址: 小灰灰Blog (opens new window)

# 个人博客: Z+|blog (opens new window)

基于hexo + github pages搭建的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

# 声明

尽信书则不如,已上内容,纯属一家之言,因本人能力一般,见识有限,如发现bug或者有更好的建议,随时欢迎批评指正,我的微博地址: 小灰灰Blog (opens new window)

# 扫描关注,java分享

QrCode

编辑 (opens new window)
#Java#技术方案
上次更新: 2021/10/15, 19:56:22
2. 报警系统QuickAlarm之报警执行器的设计与实现
5. 报警系统QuickAlarm之频率统计及接口封装

← 2. 报警系统QuickAlarm之报警执行器的设计与实现 5. 报警系统QuickAlarm之频率统计及接口封装→

最近更新
01
【WEB系列】从0到1实现自定义web参数映射器
01-23
02
【WEB系列】如何支持下划线驼峰互转的传参与返回
01-17
03
【DB系列】Mybatis之批量插入的几种姿势
01-11
更多文章>
Theme by Vdoing | Copyright © 2017-2022 一灰灰Blog
MIT License | 鄂ICP备18017282号 |
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×