报警系统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();
}
}
配置类:
@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
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. 配置加载
配置加载的实现逻辑,如下
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对象
@Data
public class RegisterInfo implements Serializable {
// 报警规则文件的路径,系统默认加载时,必填;否则选填
private String alarmConfPath;
// 最大报警类型数,非必填,默认1000
private Integer maxAlarmType;
// 默认报警用户, 必须
private String defaultAlarmUsers;
// 应用名, 必须
private String appName;
}
一个配置文件实例
appName=test
alarmConfPath=/tmp/alarmConfig
maxAlarmType=1000
defaultAlarmUsers=yihui
从配置文件中读取信息,然后初始化对象的过程就比较简单了,我这里做了一个小简化,使用反射的方式实现对象拷贝
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);
}
上面的实现目前比较简单,没有考虑父类的情况,没有考虑复杂的数据类型转换,目前只支持了基本类型的转换,后续可考虑抽象
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 对象
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
{
"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
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方式注入用户加载器,默认提供基于配置文件的加载器,且优先级最低
基本上本文说的就是下面这张图的内容了
- 【死磕Java并发】—– J.U.C之AQS:CLH同步队列
- 使用Python完成你的第一个学习项目
- CA,给了数据库,给了机器,为啥也扩不了容?
- 如何使用Anaconda设置机器学习和深度学习的Python环境
- MQ,互联网架构解耦神器
- 预测随机机器学习算法实验的重复次数
- 服务化了,没想到耦合更加严重?
- 如何在Python中扩展LSTM网络的数据
- 使用Keras的Python深度学习模型的学习率方案
- 全球电脑手机无一幸免,英特尔CPU“漏洞事件”到底多严重?
- 评估Keras深度学习模型的性能
- Python机器学习的练习二:多元线性回归
- 熔断器 Hystrix 源码解析 —— 命令合并执行
- Python机器学习的练习一:简单线性回归
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- React setState 是异步执行还是同步执行?
- sm2,sm3,sm4国密算法的纯c语言版本,使用于任何嵌入式平台
- sm2国密算法的纯c语言版本,使用于单片机平台(静态内存分配)
- 面试:mysql 事务和锁的解释
- 【STM32F407开发板用户手册】第35章 STM32F407的FSMC总线应用之驱动AD7606(8通道同步采样, 16bit, 正负10V)
- 玩转easyARM imax283A开发版(二),移植NES模拟器并增加按键驱动,让板子可以玩超级玛丽游戏
- 完了!TCP出了大事!
- redis高并发高可用
- 嵌入式linux之go语言开发(九)关于嵌入式GUI
- docker入门总结,从使用的角度谈起
- 使用 Go 语言开发 Android 应用的正确姿势探索
- Android的配置文件操作的完美封装(使用注解 反射让配置文件操作如此清晰和简单)
- Android中protobuf的使用
- 疫情监控三部曲——在STM32F103 MCU上实现(裸机版)
- Android配置文件操作模块封装,全互联网最简单好用的封装