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

文章目录
  1. I. 报警规则定义
  2. II. 报警规则加载
    1. 1. 配置加载
    2. 2. RegisterInfo 加载
      1. 3. 报警规则加载
  3. III. ConfLoader选择并初始化
  4. IV. 小结
  5. V. 其他
    1. 相关博文
    2. 项目
    3. 个人博客: Z+|blog
    4. 声明
    5. 扫描关注,java分享

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

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

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

I. 报警规则定义

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

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

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

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

阀值类:

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 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
33
34
35
36
37
38
39
40
41
42
43
@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;
}

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

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

II. 报警规则加载

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

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

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
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);
}

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

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

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

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

1. 配置加载

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

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
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;
}
}
}

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

2. RegisterInfo 加载

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

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

一个配置文件实例

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
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
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();
}
}

3. 报警规则加载

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

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

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
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;
}

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

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

下面是一个报警配置的demo

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
{
"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"
]
}
}

III. ConfLoader选择并初始化

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

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
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!");
}
}
}

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

IV. 小结

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

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

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

应用启动.png

V. 其他

相关博文

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

项目

个人博客: Z+|blog

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

声明

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

扫描关注,java分享

QrCode

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×