提交 022df2b1 作者: 方治民

feat: 新增 @UptimePush 注解,实现自动上报监控结果到 Uptime Kuma 并支持重试功能

上级 4f29817d
/* (C) 2024 YiRing, Inc. */
package com.yiring.app.job;
import com.yiring.common.annotation.UptimePush;
import com.yiring.common.constant.DateFormatter;
import com.yiring.common.core.Retriever;
import com.yiring.common.exception.UptimeException;
import com.yiring.common.util.Uptime;
import java.time.LocalDateTime;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* @author Jim
*/
@Slf4j
@Component
@EnableScheduling
@RequiredArgsConstructor
public class TestJob {
final Retriever retriever;
/**
* 一个使用 Retriever + @UptimePush 双链路重试的定时任务示例
*/
@UptimePush(
key = "BiCC4Jgoa5",
group = "Test",
name = "测试任务调度",
retryCount = 3,
retryStatus = Uptime.Status.UP
)
@Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS)
public void test() {
String time = LocalDateTime.now().format(DateFormatter.DATE_TIME);
log.info("TestJobHandler: {}", time);
Random random = new Random();
int randomCount = random.nextInt(3);
String result = retriever.execute(
ctx -> {
if (ctx.getRetryCount() > randomCount) {
return "OK";
}
throw new UptimeException("[TestJobHandler] test err: " + time);
},
Retriever::defaultPolicy
);
log.info("[Test] result: {}", result);
}
}
......@@ -18,12 +18,12 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RateLimiter {
String RATE_LIMIT_KEY = "rate_limit:";
String DEFAULT_CACHE_KEY = "rate_limit:";
/**
* 限流key
*/
String key() default RATE_LIMIT_KEY;
String key() default DEFAULT_CACHE_KEY;
/**
* 限流时间,单位秒
......
/* (C) 2023 YiRing, Inc. */
package com.yiring.common.annotation;
import com.yiring.common.util.Uptime;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Uptime Kuma 服务健康检查注解, 用于标记需要 Push 类型监控的方法
*
* @author Jim
* @version 0.1
* 2024/12/19 15:20
*/
@SuppressWarnings({ "unused" })
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface UptimePush {
String CACHE_PREFIX = "uptime_health_push:";
/**
* Push 类型的监控项唯一标识
* eg: <a href="https://uptime.yiring.com/api/push/BiCC4Jgoa5?status=up&msg=OK&ping=">BiCC4Jgoa5</a> 其中的 BiCC4Jgoa5 就是 key
*/
String key();
/**
* 监控项名称
*/
String name() default "";
/**
* 分组名名称
*/
String group() default "";
/**
* 重试次数
*/
int retryCount() default 0;
/**
* 重试时的状态标记,默认为 DOWN
*/
Uptime.Status retryStatus() default Uptime.Status.DOWN;
/**
* 重试循环,例如设置重试次数为 5,第五次会真正触发异常上报,然后后续继续出现异常触发重试又会积累 5 次后再次上报
* 默认不开启,代表当达到重试次数后,不会再走重试逻辑,按照默认的失败机制处理,直到成功一次后再重置
*/
boolean retryLoop() default false;
}
/* (C) 2023 YiRing, Inc. */
package com.yiring.common.aspect;
import cn.hutool.core.util.StrUtil;
import com.yiring.common.annotation.UptimePush;
import com.yiring.common.core.Redis;
import com.yiring.common.core.UptimeNotice;
import com.yiring.common.exception.UptimeException;
import com.yiring.common.util.Uptime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
* Uptime Push 监控项切面
*
* @author Jim
* @version 0.1
* 2023/12/19 15:17
*/
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class UptimePushAspect {
final Redis redis;
@Around("@annotation(annotation)")
public Object around(ProceedingJoinPoint point, UptimePush annotation) throws Throwable {
// 获取 Push 监控项的 key 唯一标识
String key = annotation.key();
String redisKey = UptimePush.CACHE_PREFIX + key;
int retryCount = annotation.retryCount();
// 获取当前的重试次数
Integer currentRetryCount = redis.get(redisKey, Integer.class);
Object result = null;
String err = null;
long start = System.currentTimeMillis();
try {
result = point.proceed();
// 针对有返回值的 Job 判断是否为 UptimeNotice 类实例
if (result instanceof UptimeNotice value) {
result = value.getMsg();
throw new UptimeException(value.getMsg());
}
// 开始重复计数
redis.del(redisKey);
} catch (Exception e) {
err = e.getMessage();
// 非指定 UptimeException 异常情况下才抛出
if (!(e instanceof UptimeException) || retryCount <= 0) {
throw e;
}
} finally {
// 获取重试时的状态标记
Uptime.Status status = Uptime.Status.UP;
// 构建消息内容集合
List<String> texts = new ArrayList<>();
if (StrUtil.isNotBlank(annotation.group())) {
texts.add("【" + annotation.group() + "】");
}
if (StrUtil.isNotBlank(annotation.name())) {
texts.add(
annotation.name() +
(
retryCount > 0
? "(" + Optional.ofNullable(currentRetryCount).orElse(0) + "/" + retryCount + ")"
: ""
) +
","
);
}
// 判断是否有异常消息
if (StrUtil.isNotBlank(err)) {
status = Uptime.Status.DOWN;
if (retryCount > 0) {
// 判断重试次数是否符合条件,设置重试时的上报状态
if (Objects.isNull(currentRetryCount) || currentRetryCount < retryCount) {
status = annotation.retryStatus();
// 重试次数递增 1
Long count = redis.incr(redisKey, 1);
// 追加重试次数
texts.add(StrUtil.format("已启用重试模式,预计最多重试 {} 次 \n\n", retryCount));
log.info(
"[Uptime Push] key: {}, group: {}, name: {}, retry count: {}",
key,
annotation.group(),
annotation.name(),
count
);
} else {
if (annotation.retryLoop()) {
// 开始重复计数
redis.del(redisKey);
} else {
// 追加重试衰退提示
texts.add(
StrUtil.format(
"重试超过 {} 次仍然未恢复,由于未启用循环重试模式,衰退至默认机制,请及时检查并处理 \n\n",
retryCount
)
);
}
}
}
// 追加异常消息
texts.add(err);
} else {
if (result instanceof String text) {
texts.add(text);
} else {
texts.add("OK");
}
}
// 构建上报的消息内容
String msg = StrUtil.join(" ", texts);
long ping = System.currentTimeMillis() - start;
// 上报监控结果
Uptime.notice(key, status, msg, ping);
}
return result;
}
}
......@@ -35,10 +35,10 @@ public class Result<T> implements Serializable {
/**
* 注入 I18n
*/
protected static final I18n i18n;
protected static final I18n I18N;
static {
i18n = SpringUtil.getBean(I18n.class);
I18N = SpringUtil.getBean(I18n.class);
}
/**
......@@ -228,6 +228,6 @@ public class Result<T> implements Serializable {
return null;
}
return i18n.get(message, message);
return I18N.get(message, message);
}
}
/* (C) 2021 YiRing, Inc. */
package com.yiring.common.core;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.yiring.common.util.Uptime;
import java.io.Serial;
import java.io.Serializable;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Data;
import lombok.experimental.FieldDefaults;
/**
* 标准的响应对象(所有的接口响应内容格式都应该是一致的)
*
* @author ifzm
* @version 1.1
* 2018/9/4 11:05
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@Data
@Builder
@FieldDefaults(level = AccessLevel.PRIVATE)
public class UptimeNotice implements Serializable {
@Serial
private static final long serialVersionUID = -4250534970217176081L;
/**
* 状态
*/
Uptime.Status status;
/**
* 消息
*/
String msg;
/**
* 返回成功响应内容(默认)
*
* @return UptimeNotice
* @see Uptime.Status#UP
*/
public static UptimeNotice ok() {
return ok("OK");
}
/**
* 返回自定义成功响应内容
*
* @return UptimeNotice
* @see Uptime.Status
*/
public static UptimeNotice ok(String msg) {
return UptimeNotice.builder().status(Uptime.Status.UP).msg(msg).build();
}
/**
* 返回默认的 400 错误响应
*
* @return UptimeNotice
* @see Uptime.Status#DOWN
*/
public static UptimeNotice no() {
return no("Fail");
}
/**
* 返回自定义成功响应内容
*
* @return UptimeNotice
* @see Uptime.Status
*/
public static UptimeNotice no(String msg) {
return UptimeNotice.builder().status(Uptime.Status.DOWN).msg(msg).build();
}
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.common.exception;
import java.io.Serial;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.FieldDefaults;
/**
* Uptime 异常
* 搭配 @UptimePush 注解可实现快速上报监控结果
*
* @author Jim
* @version 0.1
* 2022/3/28 11:36
*/
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class UptimeException extends RuntimeException {
@Serial
private static final long serialVersionUID = -4226669531686389671L;
/**
* 异常消息
*/
String message;
}
/* (C) 2021 YiRing, Inc. */
package com.yiring.common.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.stereotype.Component;
/**
* XxlJob 注解切面,修复无法输出异常的问题
*
* @author ifzm
* @version 0.1
*/
@Slf4j
@Aspect
@Component
@ConditionalOnClass(name = "com.xxl.job.core.handler.annotation.XxlJob")
public class XxlJobAspect {
@Pointcut("@annotation(com.xxl.job.core.handler.annotation.XxlJob)")
public void log() {}
@Around("log()")
public Object around(ProceedingJoinPoint point) throws Throwable {
try {
return point.proceed();
} catch (Exception e) {
log.error("XxlJob Execute Error: " + e.getMessage(), e);
throw e;
}
}
}
......@@ -87,7 +87,12 @@ public class Retriever {
ctx.getRetryCount(),
throwable.getMessage()
);
throw new RuntimeException(throwable);
if (throwable instanceof Exception e) {
throw e;
} else {
throw new RuntimeException(throwable.getMessage(), throwable);
}
}
);
}
......
......@@ -20,36 +20,30 @@ public class Uptime {
static String UPTIME_DOMAIN;
static {
UPTIME_DOMAIN = SpringUtil.getProperty("uptime.domain");
}
String defaultUptimeDomain = "https://uptime.yiring.com";
/**
* 内网请求 监测请求
* eg: <a href="http://uptime.health.yiring.com">uptime.health.yiring.com</a>
*
* @param key key
* @param status 状态
* @param msg 信息
*/
public void noticeInternal(String key, Status status, String msg) {
// 服务器不具备公网访问权限
// http://192.168.173.150
String domain = "http" + "://uptime.health.yiring.com";
notice(domain, key, status, msg);
String domain = SpringUtil.getProperty("uptime.domain");
if (StrUtil.isBlank(domain)) {
domain = defaultUptimeDomain;
log.warn(
"[Uptime Config] No `uptime.domain` configuration detected, using default domain: {}",
defaultUptimeDomain
);
}
UPTIME_DOMAIN = domain;
}
/**
* 公网请求 监测请求
* eg: <a href="https://uptime.yiring.com">uptime.yiring.com</a>
* 默认的通知方法
* eg: <a href="https://uptime.yiring.com">${uptime.domain}</a>
*
* @param key key
* @param status 状态
* @param msg 信息
*/
public void noticeExternal(String key, Status status, String msg) {
// 服务器具备公网访问权限
String domain = "https" + "://uptime.yiring.com";
notice(domain, key, status, msg);
public void notice(String key, Status status, String msg) {
notice(UPTIME_DOMAIN, key, status, msg, null);
}
/**
......@@ -59,13 +53,14 @@ public class Uptime {
* @param key key
* @param status 状态
* @param msg 信息
* @param ping Push ping
*/
public void notice(String key, Status status, String msg) {
public void notice(String key, Status status, String msg, Long ping) {
if (StrUtil.isBlank(UPTIME_DOMAIN)) {
throw new RuntimeException("please set ${uptime.domain} value in application config file.");
}
notice(UPTIME_DOMAIN, key, status, msg);
notice(UPTIME_DOMAIN, key, status, msg, ping);
}
/**
......@@ -75,18 +70,24 @@ public class Uptime {
* @param key Push key
* @param status Push status
* @param msg Push message
* @param ping Push ping
*/
private void notice(String domain, String key, Status status, String msg) {
String url = "{}/api/push/{}?status={}&msg={}&ping=";
private void notice(String domain, String key, Status status, String msg, Long ping) {
String url = "{}/api/push/{}?status={}&ping={}&msg={}";
String message = url;
try {
// 读取配置文件中的 uptime.domain 覆盖默认配置
if (!Objects.equals(domain, UPTIME_DOMAIN) && StrUtil.isNotEmpty(UPTIME_DOMAIN)) {
domain = UPTIME_DOMAIN;
}
HttpUtil.get(StrUtil.format(url, domain, key, status.getValue(), msg));
message = StrUtil.format(message, domain, key, status.getValue(), ping, "***");
HttpUtil.get(StrUtil.format(url, domain, key, status.getValue(), ping, msg));
} catch (Exception e) {
log.error("[Uptime Kuma] network connection failure: {}", e.getMessage(), e);
log.error("[Uptime] notice failure: {}", e.getMessage(), e);
} finally {
log.info("[Uptime] notice URL: {}", message);
}
}
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论