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() " +
|
"locked_by = NULL, locked_at = NULL, updated_at = now() " +
|
||||||
"WHERE id = #{id} AND status = 3")
|
"WHERE id = #{id} AND status = 3")
|
||||||
int redeliverDead(@Param("id") Long id);
|
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