feat(health): 添加服务器健康检查端点 /_health
- 新增 /_health 路由,返回 JSON 格式的服务器状态 - 包含:uptime、活跃连接数、最大连接数、插件列表、中间件列表 - 中间件注册表新增 cocoon_middleware_list() 查询 API - 集成测试新增 7 项健康检查验证(状态码、JSON 字段、HEAD 支持) - 编译零警告,142 个单元测试 + 75 项集成测试全部通过
This commit is contained in:
parent
b5acd7f5d1
commit
09e592d631
15
middleware.c
15
middleware.c
@ -242,6 +242,21 @@ void cocoon_middleware_cleanup(void) {
|
|||||||
pthread_mutex_unlock(&g_rate_limit_mutex);
|
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) {
|
int cocoon_middleware_cors(http_request_t *req, cocoon_socket_t fd, void *user_data) {
|
||||||
|
|||||||
11
middleware.h
11
middleware.h
@ -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);
|
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 - 根据配置初始化内置中间件
|
* cocoon_middleware_init_builtin - 根据配置初始化内置中间件
|
||||||
*
|
*
|
||||||
|
|||||||
86
server.c
86
server.c
@ -28,6 +28,7 @@
|
|||||||
#include "websocket.h"
|
#include "websocket.h"
|
||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
#include "middleware.h"
|
#include "middleware.h"
|
||||||
|
#include <time.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@ -72,6 +73,10 @@ struct server_context {
|
|||||||
|
|
||||||
/* 全局活跃连接计数器(线程安全) */
|
/* 全局活跃连接计数器(线程安全) */
|
||||||
static atomic_int g_active_connections = 0;
|
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 为非阻塞模式
|
* set_nonblocking - 设置 socket 为非阻塞模式
|
||||||
@ -408,6 +413,83 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
|
|||||||
return req.keep_alive;
|
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 real_path[4096];
|
||||||
char root_normalized[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->config.root_dir = strdup(config->root_dir);
|
||||||
ctx->running = 1;
|
ctx->running = 1;
|
||||||
|
|
||||||
|
g_max_connections = config->max_connections;
|
||||||
|
|
||||||
/* 创建监听 socket */
|
/* 创建监听 socket */
|
||||||
ctx->listen_fd = socket(AF_INET, SOCK_STREAM, 0);
|
ctx->listen_fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
if (ctx->listen_fd == COCOON_INVALID_SOCKET) {
|
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) {
|
int server_start(server_context_t *ctx) {
|
||||||
if (!ctx) return COCOON_ERROR;
|
if (!ctx) return COCOON_ERROR;
|
||||||
|
|
||||||
|
g_server_start_time = time(NULL);
|
||||||
|
|
||||||
if (ctx->config.threaded) {
|
if (ctx->config.threaded) {
|
||||||
/* 多线程协程模式 */
|
/* 多线程协程模式 */
|
||||||
uint32_t num_workers = ctx->config.num_workers;
|
uint32_t num_workers = ctx->config.num_workers;
|
||||||
|
|||||||
@ -409,7 +409,48 @@ assert_access_log() {
|
|||||||
start_server
|
start_server
|
||||||
|
|
||||||
echo ""
|
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/" "200" "首页 GET"
|
||||||
assert_status "$BASE/index.html" "200" "index.html GET"
|
assert_status "$BASE/index.html" "200" "index.html GET"
|
||||||
assert_status "$BASE/nonexist.html" "404" "404 页面"
|
assert_status "$BASE/nonexist.html" "404" "404 页面"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user