feat(health): 添加服务器健康检查端点 /_health

- 新增 /_health 路由,返回 JSON 格式的服务器状态
- 包含:uptime、活跃连接数、最大连接数、插件列表、中间件列表
- 中间件注册表新增 cocoon_middleware_list() 查询 API
- 集成测试新增 7 项健康检查验证(状态码、JSON 字段、HEAD 支持)
- 编译零警告,142 个单元测试 + 75 项集成测试全部通过
This commit is contained in:
xfy911 2026-06-06 12:08:39 +08:00
parent b5acd7f5d1
commit 09e592d631
4 changed files with 154 additions and 1 deletions

View File

@ -242,6 +242,21 @@ void cocoon_middleware_cleanup(void) {
pthread_mutex_unlock(&g_rate_limit_mutex);
}
int cocoon_middleware_list(char names[][32], int count) {
if (!names || count <= 0) return 0;
int n = 0;
for (int i = 0; i < g_count && n < count; i++) {
if (g_registry[i].active) {
size_t len = strlen(g_registry[i].name);
if (len > 31) len = 31;
memcpy(names[n], g_registry[i].name, len);
names[n][len] = '\0';
n++;
}
}
return n;
}
/* === 内置中间件 === */
int cocoon_middleware_cors(http_request_t *req, cocoon_socket_t fd, void *user_data) {

View File

@ -130,6 +130,17 @@ int cocoon_middleware_basic_auth(http_request_t *req, cocoon_socket_t fd, void *
*/
int cocoon_middleware_rate_limit(http_request_t *req, cocoon_socket_t fd, void *user_data);
/**
* cocoon_middleware_list -
*
* 32
*
* @param names
* @param count
* @return
*/
int cocoon_middleware_list(char names[][32], int count);
/**
* cocoon_middleware_init_builtin -
*

View File

@ -28,6 +28,7 @@
#include "websocket.h"
#include "platform.h"
#include "middleware.h"
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -72,6 +73,10 @@ struct server_context {
/* 全局活跃连接计数器(线程安全) */
static atomic_int g_active_connections = 0;
/* 服务器启动时间 */
static time_t g_server_start_time = 0;
/* 最大连接数(供健康检查端点使用) */
static uint32_t g_max_connections = 0;
/**
* set_nonblocking - socket
@ -408,6 +413,83 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
return req.keep_alive;
}
/* 健康检查端点 */
if (strcmp(req.path, "/_health") == 0) {
time_t now = time(NULL);
time_t uptime = now - g_server_start_time;
int active = atomic_load(&g_active_connections);
char mw_names[16][32];
int mw_count = cocoon_middleware_list(mw_names, 16);
/* 构建 JSON */
char body[2048];
int n = snprintf(body, sizeof(body),
"{\n"
" \"status\": \"ok\",\n"
" \"version\": \"Cocoon/1.0\",\n"
" \"uptime_seconds\": %ld,\n"
" \"connections\": {\n"
" \"active\": %d,\n"
" \"max\": %u\n"
" },\n"
" \"plugins\": {\n"
" \"count\": %zu,\n"
" \"list\": [\n",
uptime, active,
g_max_connections,
cocoon_plugin_count());
for (size_t i = 0; i < cocoon_plugin_count(); i++) {
const char *path = cocoon_plugin_get_path(i);
const char *ver = cocoon_plugin_get_version(i);
n += snprintf(body + n, sizeof(body) - n,
" {\"path\":\"%s\",\"version\":\"%s\"}%s\n",
path ? path : "",
ver ? ver : "unknown",
(i + 1 < cocoon_plugin_count()) ? "," : "");
}
n += snprintf(body + n, sizeof(body) - n,
" ]\n"
" },\n"
" \"middleware\": {\n"
" \"count\": %d,\n"
" \"names\": [\n",
mw_count);
for (int i = 0; i < mw_count; i++) {
n += snprintf(body + n, sizeof(body) - n,
" \"%s\"%s\n",
mw_names[i],
(i + 1 < mw_count) ? "," : "");
}
n += snprintf(body + n, sizeof(body) - n,
" ]\n"
" }\n"
"}");
char header[512];
int header_len = snprintf(header, sizeof(header),
"HTTP/1.1 200 OK\r\n"
"Content-Type: application/json\r\n"
"Content-Length: %d\r\n"
"Connection: %s\r\n"
"Server: Cocoon/1.0\r\n"
"\r\n",
n, req.keep_alive ? "keep-alive" : "close");
send_all(conn->fd, header, (size_t)header_len);
send_all(conn->fd, body, (size_t)n);
conn->response_status = 200;
access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len,
&req, conn->response_status, n);
http_request_free(&req);
return req.keep_alive;
}
/* 安全路径拼接 */
char real_path[4096];
char root_normalized[4096];
@ -1001,6 +1083,8 @@ server_context_t *server_create(const cocoon_config_t *config) {
ctx->config.root_dir = strdup(config->root_dir);
ctx->running = 1;
g_max_connections = config->max_connections;
/* 创建监听 socket */
ctx->listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (ctx->listen_fd == COCOON_INVALID_SOCKET) {
@ -1070,6 +1154,8 @@ server_context_t *server_create(const cocoon_config_t *config) {
int server_start(server_context_t *ctx) {
if (!ctx) return COCOON_ERROR;
g_server_start_time = time(NULL);
if (ctx->config.threaded) {
/* 多线程协程模式 */
uint32_t num_workers = ctx->config.num_workers;

View File

@ -409,7 +409,48 @@ assert_access_log() {
start_server
echo ""
echo "=== 基础功能测试 ==="
echo "=== 健康检查端点测试 ==="
health_body=$(curl -s "$BASE/_health")
health_status=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/_health")
if [[ "$health_status" == "200" ]]; then
echo " ✓ /_health 状态码 — HTTP 200"
pass
else
echo " ✗ /_health 状态码 — 期望 200, 实际 $health_status"
fail
fi
if echo "$health_body" | grep -q '"status": "ok"'; then
echo " ✓ /_health 响应体 — 包含 status: ok"
pass
else
echo " ✗ /_health 响应体 — 缺少 status: ok"
fail
fi
if echo "$health_body" | grep -q '"version": "Cocoon/1.0"'; then
echo " ✓ /_health 响应体 — 包含版本信息"
pass
else
echo " ✗ /_health 响应体 — 缺少版本信息"
fail
fi
if echo "$health_body" | grep -q '"connections"'; then
echo " ✓ /_health 响应体 — 包含连接数信息"
pass
else
echo " ✗ /_health 响应体 — 缺少连接数信息"
fail
fi
if echo "$health_body" | grep -q '"middleware"'; then
echo " ✓ /_health 响应体 — 包含中间件信息"
pass
else
echo " ✗ /_health 响应体 — 缺少中间件信息"
fail
fi
assert_head_status "$BASE/_health" "200" "健康检查 HEAD"
assert_status "$BASE/_health/nonexistent" "404" "健康检查路径遍历"
assert_status "$BASE/" "200" "首页 GET"
assert_status "$BASE/index.html" "200" "index.html GET"
assert_status "$BASE/nonexist.html" "404" "404 页面"