feat(t15): 健康检查端点 /health(pg/redis/队列四态,DOWN 返503)
- DistQueueMapper: +countByStatus (GROUP BY status 一次拿全) - HealthService(新): 聚合 pg 连通/redis ping/队列四态,status UP/DEGRADED/DOWN - HealthController(新): GET /health,DOWN 返 503 便 LB 摘流;免租户鉴权(PathWhitelist) - 联调:status=UP,pg/redis=UP,distQueue 四态 0,HTTP 200 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
package com.imutil.common;
|
||||
|
||||
import com.imutil.mapper.DistQueueMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.noear.solon.annotation.Component;
|
||||
import org.noear.solon.annotation.Inject;
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.JedisPool;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 健康检查服务(对齐设计:监控告警 - 中间件连通、dist_queue 堆积)
|
||||
* <p>
|
||||
* 聚合 PG / Redis 连通性与分发队列状态分布,输出整体 status:
|
||||
* - UP:PG + Redis 均连通
|
||||
* - DEGRADED:PG 连通、Redis 异常(限流 fail-open、缓存降级,核心链路仍可用)
|
||||
* - DOWN:PG 异常(消息落库不可用,核心受损)
|
||||
* <p>
|
||||
* 队列 pending/processing/dead 计数供运维判断是否需干预:
|
||||
* - pending 持续上涨 → worker 处理不过去,考虑扩容
|
||||
* - processing 长期不归零且无 done 增长 → 工作线程可能卡死
|
||||
* - dead 积累 → 需排查死信或重发
|
||||
*
|
||||
* @author imutil
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class HealthService {
|
||||
|
||||
@Inject
|
||||
private DistQueueMapper distQueueMapper;
|
||||
|
||||
@Inject
|
||||
private JedisPool jedisPool;
|
||||
|
||||
/**
|
||||
* 采集健康指标
|
||||
*
|
||||
* @return 含 status / pg / redis / distQueue / timestamp
|
||||
*/
|
||||
public Map<String, Object> health() {
|
||||
Map<String, Object> distQueue = new LinkedHashMap<>();
|
||||
boolean pgUp;
|
||||
try {
|
||||
List<Map<String, Object>> rows = distQueueMapper.countByStatus();
|
||||
// 四态计数初始化为 0(无记录的状态不出现在 GROUP BY 结果中)
|
||||
long[] cnt = new long[4];
|
||||
for (Map<String, Object> row : rows) {
|
||||
int st = ((Number) row.get("status")).intValue();
|
||||
long c = ((Number) row.get("cnt")).longValue();
|
||||
if (st >= 0 && st < 4) {
|
||||
cnt[st] = c;
|
||||
}
|
||||
}
|
||||
distQueue.put("pending", cnt[0]);
|
||||
distQueue.put("processing", cnt[1]);
|
||||
distQueue.put("done", cnt[2]);
|
||||
distQueue.put("dead", cnt[3]);
|
||||
pgUp = true;
|
||||
} catch (Exception e) {
|
||||
log.warn("健康检查 PG 查询失败 : {}", e.getMessage());
|
||||
distQueue.put("error", e.getMessage());
|
||||
pgUp = false;
|
||||
}
|
||||
|
||||
boolean redisUp;
|
||||
try (Jedis j = jedisPool.getResource()) {
|
||||
redisUp = "PONG".equals(j.ping());
|
||||
} catch (Exception e) {
|
||||
log.warn("健康检查 Redis ping 失败 : {}", e.getMessage());
|
||||
redisUp = false;
|
||||
}
|
||||
|
||||
String status;
|
||||
if (pgUp && redisUp) {
|
||||
status = "UP";
|
||||
} else if (pgUp) {
|
||||
status = "DEGRADED";
|
||||
} else {
|
||||
status = "DOWN";
|
||||
}
|
||||
|
||||
Map<String, Object> r = new LinkedHashMap<>();
|
||||
r.put("status", status);
|
||||
r.put("pg", pgUp ? "UP" : "DOWN");
|
||||
r.put("redis", redisUp ? "UP" : "DOWN");
|
||||
r.put("distQueue", distQueue);
|
||||
r.put("timestamp", OffsetDateTime.now().toString());
|
||||
return r;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.imutil.controller;
|
||||
|
||||
import com.imutil.common.HealthService;
|
||||
import com.imutil.model.Result;
|
||||
import org.noear.solon.annotation.Controller;
|
||||
import org.noear.solon.annotation.Inject;
|
||||
import org.noear.solon.annotation.Mapping;
|
||||
import org.noear.solon.core.handle.Context;
|
||||
import org.noear.solon.core.handle.MethodType;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 健康检查端点(运维 / LB 探针)
|
||||
* <p>
|
||||
* GET /health:返回 PG/Redis 连通 + 分发队列四态计数。
|
||||
* 免租户鉴权(见 {@link com.imutil.common.PathWhitelist})。
|
||||
* HTTP 状态码:200 = UP/DEGRADED(核心可用);503 = DOWN(PG 异常,核心受损),
|
||||
* 便于 LB/k8s 探针据状态码摘流。
|
||||
*
|
||||
* @author imutil
|
||||
*/
|
||||
@Controller
|
||||
public class HealthController {
|
||||
|
||||
@Inject
|
||||
private HealthService healthService;
|
||||
|
||||
@Mapping(value = "/health", method = MethodType.GET)
|
||||
public void health(Context ctx) throws Throwable {
|
||||
Map<String, Object> h = healthService.health();
|
||||
// DOWN 返回 503,便探针摘流;UP/DEGRADED 返回 200
|
||||
ctx.status("DOWN".equals(h.get("status")) ? 503 : 200);
|
||||
ctx.render(Result.ok(h));
|
||||
}
|
||||
}
|
||||
@@ -78,4 +78,12 @@ public interface DistQueueMapper extends BaseMapper<DistQueue> {
|
||||
"locked_by = NULL, locked_at = NULL, updated_at = now() " +
|
||||
"WHERE id = #{id} AND status = 3")
|
||||
int redeliverDead(@Param("id") Long id);
|
||||
|
||||
/**
|
||||
* 按状态聚合计数(健康检查/监控用,一次扫描拿全状态分布)
|
||||
*
|
||||
* @return 每项含 status(0=pending/1=processing/2=done/3=dead)与 cnt
|
||||
*/
|
||||
@org.apache.ibatis.annotations.Select("SELECT status, count(*) AS cnt FROM dist_queue GROUP BY status")
|
||||
java.util.List<java.util.Map<String, Object>> countByStatus();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user