feat(metrics): Prometheus /_metrics 端点 + 响应码分类计数器

- 新增 7 个原子计数器:total_requests, response_2xx/3xx/4xx/5xx/200/404
- 在 handle_request 所有响应路径统一调用 update_metrics() 更新计数器
- 新增 /_metrics 端点,返回 Prometheus 文本格式(10 项指标)
  - 计数器:requests_total, response_2xx_total, response_3xx_total,
            response_4xx_total, response_5xx_total, response_200_total,
            response_404_total
  - 仪表盘:uptime_seconds, connections_active, connections_max
- 集成测试:新增 8 项 Prometheus 指标测试(状态码、字段检查、
  HEAD 支持、计数器递增验证)
- 编译零警告,142 单元测试 + 90 集成测试全部通过
This commit is contained in:
xfy911 2026-06-08 18:09:08 +08:00
parent c3d4b112d7
commit 8ad8ecaaef
2 changed files with 186 additions and 0 deletions

107
server.c
View File

@ -80,6 +80,31 @@ static atomic_int g_active_connections = 0;
static time_t g_server_start_time = 0;
/* 最大连接数(供健康检查端点使用) */
static uint32_t g_max_connections = 0;
/* Prometheus 指标计数器 */
static atomic_uint g_total_requests = 0;
static atomic_uint g_response_2xx = 0;
static atomic_uint g_response_3xx = 0;
static atomic_uint g_response_4xx = 0;
static atomic_uint g_response_5xx = 0;
static atomic_uint g_response_200 = 0;
static atomic_uint g_response_404 = 0;
/**
* update_metrics - Prometheus
*
* HTTP
*
* @param status HTTP
*/
static void update_metrics(int status) {
atomic_fetch_add(&g_total_requests, 1);
if (status >= 200 && status < 300) atomic_fetch_add(&g_response_2xx, 1);
else if (status >= 300 && status < 400) atomic_fetch_add(&g_response_3xx, 1);
else if (status >= 400 && status < 500) atomic_fetch_add(&g_response_4xx, 1);
else if (status >= 500) atomic_fetch_add(&g_response_5xx, 1);
if (status == 200) atomic_fetch_add(&g_response_200, 1);
if (status == 404) atomic_fetch_add(&g_response_404, 1);
}
/**
* set_nonblocking - socket
@ -362,6 +387,7 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
}
/* 格式错误 */
conn->response_status = 400;
update_metrics(conn->response_status);
static_send_error(conn->fd, 400, false);
access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len,
&req, conn->response_status, -1);
@ -377,6 +403,7 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
/* 执行中间件链 */
if (cocoon_middleware_run(&req, conn->fd) != 0) {
/* 某个中间件已短路请求,清理并返回 */
if (conn->response_status > 0) update_metrics(conn->response_status);
access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len,
&req, conn->response_status, -1);
http_request_free(&req);
@ -388,6 +415,7 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
size_t need = (size_t)req.content_length;
if (conn_read_body(conn, &req, need) != 0) {
conn->response_status = 413;
update_metrics(conn->response_status);
static_send_error(conn->fd, 413, req.keep_alive); /* Payload Too Large */
access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len,
&req, conn->response_status, -1);
@ -400,6 +428,7 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
if (req.method == HTTP_POST) {
bool keep = handle_post_request(conn->fd, &req, conn->root_dir);
conn->response_status = 200; /* POST 目前总是返回 200 */
update_metrics(conn->response_status);
access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len,
&req, conn->response_status, -1);
http_request_free(&req);
@ -412,6 +441,7 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
if (rule) {
bool keep = proxy_forward(conn->fd, &req, rule, &conn->client_addr);
conn->response_status = 200; /* 代理响应状态由后端决定,这里记录一个通用值 */
update_metrics(conn->response_status);
access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len,
&req, conn->response_status, -1);
http_request_free(&req);
@ -422,6 +452,7 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
/* 只支持 GET 和 HEAD */
if (req.method != HTTP_GET && req.method != HTTP_HEAD) {
conn->response_status = 405;
update_metrics(conn->response_status);
static_send_error(conn->fd, 405, req.keep_alive);
access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len,
&req, conn->response_status, -1);
@ -500,6 +531,76 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
send_all(conn->fd, body, (size_t)n);
conn->response_status = 200;
update_metrics(conn->response_status);
access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len,
&req, conn->response_status, n);
http_request_free(&req);
return req.keep_alive;
}
/* Prometheus 指标端点 */
if (strcmp(req.path, "/_metrics") == 0) {
time_t now = time(NULL);
time_t uptime = now - g_server_start_time;
int active = atomic_load(&g_active_connections);
unsigned int total = atomic_load(&g_total_requests);
unsigned int r2xx = atomic_load(&g_response_2xx);
unsigned int r3xx = atomic_load(&g_response_3xx);
unsigned int r4xx = atomic_load(&g_response_4xx);
unsigned int r5xx = atomic_load(&g_response_5xx);
unsigned int r200 = atomic_load(&g_response_200);
unsigned int r404 = atomic_load(&g_response_404);
char body[4096];
int n = snprintf(body, sizeof(body),
"# HELP cocoon_uptime_seconds Server uptime in seconds\n"
"# TYPE cocoon_uptime_seconds gauge\n"
"cocoon_uptime_seconds %ld\n"
"# HELP cocoon_connections_active Current active connections\n"
"# TYPE cocoon_connections_active gauge\n"
"cocoon_connections_active %d\n"
"# HELP cocoon_connections_max Maximum allowed connections\n"
"# TYPE cocoon_connections_max gauge\n"
"cocoon_connections_max %u\n"
"# HELP cocoon_requests_total Total HTTP requests served\n"
"# TYPE cocoon_requests_total counter\n"
"cocoon_requests_total %u\n"
"# HELP cocoon_response_2xx_total Total 2xx responses\n"
"# TYPE cocoon_response_2xx_total counter\n"
"cocoon_response_2xx_total %u\n"
"# HELP cocoon_response_3xx_total Total 3xx responses\n"
"# TYPE cocoon_response_3xx_total counter\n"
"cocoon_response_3xx_total %u\n"
"# HELP cocoon_response_4xx_total Total 4xx responses\n"
"# TYPE cocoon_response_4xx_total counter\n"
"cocoon_response_4xx_total %u\n"
"# HELP cocoon_response_5xx_total Total 5xx responses\n"
"# TYPE cocoon_response_5xx_total counter\n"
"cocoon_response_5xx_total %u\n"
"# HELP cocoon_response_200_total Total 200 responses\n"
"# TYPE cocoon_response_200_total counter\n"
"cocoon_response_200_total %u\n"
"# HELP cocoon_response_404_total Total 404 responses\n"
"# TYPE cocoon_response_404_total counter\n"
"cocoon_response_404_total %u\n",
uptime, active, g_max_connections,
total, r2xx, r3xx, r4xx, r5xx, r200, r404);
char header[512];
int header_len = snprintf(header, sizeof(header),
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain; version=0.0.4\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;
update_metrics(conn->response_status);
access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len,
&req, conn->response_status, n);
http_request_free(&req);
@ -517,6 +618,7 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
int n = snprintf(real_path, sizeof(real_path), "%s%s", root_normalized, req.path);
if (n < 0 || (size_t)n >= sizeof(real_path)) {
conn->response_status = 400;
update_metrics(conn->response_status);
static_send_error(conn->fd, 400, req.keep_alive);
access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len,
&req, conn->response_status, -1);
@ -530,6 +632,7 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
if (!cocoon_realpath(real_path, resolved, sizeof(resolved)) ||
strncmp(resolved, root_normalized, strlen(root_normalized)) != 0) {
conn->response_status = 403;
update_metrics(conn->response_status);
static_send_error(conn->fd, 403, req.keep_alive);
access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len,
&req, conn->response_status, -1);
@ -543,6 +646,7 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
cocoon_stat_t st;
if (cocoon_file_stat(real_path, &st) != 0) {
conn->response_status = 404;
update_metrics(conn->response_status);
static_send_error(conn->fd, 404, req.keep_alive);
access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len,
&req, conn->response_status, -1);
@ -555,6 +659,7 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
char index_path[4096];
if (snprintf(index_path, sizeof(index_path), "%s/index.html", real_path) >= (int)sizeof(index_path)) {
conn->response_status = 400;
update_metrics(conn->response_status);
static_send_error(conn->fd, 400, req.keep_alive);
access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len,
&req, conn->response_status, -1);
@ -567,6 +672,7 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
http_request_t index_req = req;
if (snprintf(index_req.path, sizeof(index_req.path), "%s/index.html", req.path) >= (int)sizeof(index_req.path)) {
conn->response_status = 400;
update_metrics(conn->response_status);
static_send_error(conn->fd, 400, req.keep_alive);
access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len,
&req, conn->response_status, -1);
@ -589,6 +695,7 @@ static bool handle_request(connection_t *conn, const char *root_dir) {
static_send_error(conn->fd, 403, req.keep_alive);
}
update_metrics(conn->response_status);
access_log_write((struct sockaddr *)&conn->client_addr, conn->addr_len,
&req, conn->response_status, -1);
http_request_free(&req);

View File

@ -1071,6 +1071,85 @@ kill_server
sleep 1
start_server
echo ""
echo "=== Prometheus 指标测试 ==="
# 先发起几个请求,让计数器有值
curl -s -o /dev/null "$BASE/"
curl -s -o /dev/null "$BASE/"
curl -s -o /dev/null "$BASE/notfound.html"
metrics_body=$(curl -s "$BASE/_metrics")
metrics_status=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/_metrics")
if [[ "$metrics_status" == "200" ]]; then
echo " ✓ /_metrics 状态码 — HTTP 200"
pass
else
echo " ✗ /_metrics 状态码 — 期望 200, 实际 $metrics_status"
fail
fi
if echo "$metrics_body" | grep -q "cocoon_requests_total"; then
echo " ✓ /_metrics 包含 requests_total 指标"
pass
else
echo " ✗ /_metrics 缺少 requests_total 指标"
fail
fi
if echo "$metrics_body" | grep -q "cocoon_response_2xx_total"; then
echo " ✓ /_metrics 包含 response_2xx_total 指标"
pass
else
echo " ✗ /_metrics 缺少 response_2xx_total 指标"
fail
fi
if echo "$metrics_body" | grep -q "cocoon_response_4xx_total"; then
echo " ✓ /_metrics 包含 response_4xx_total 指标"
pass
else
echo " ✗ /_metrics 缺少 response_4xx_total 指标"
fail
fi
if echo "$metrics_body" | grep -q "cocoon_uptime_seconds"; then
echo " ✓ /_metrics 包含 uptime_seconds 指标"
pass
else
echo " ✗ /_metrics 缺少 uptime_seconds 指标"
fail
fi
if echo "$metrics_body" | grep -q "cocoon_connections_active"; then
echo " ✓ /_metrics 包含 connections_active 指标"
pass
else
echo " ✗ /_metrics 缺少 connections_active 指标"
fail
fi
# 检查计数器是否实际在增长(至少 total >= 3因为我们发了 3 个请求 + 1 个 metrics 请求)
total_val=$(echo "$metrics_body" | grep "^cocoon_requests_total " | awk '{print $2}' || echo "0")
if [[ "$total_val" -ge 3 ]]; then
echo " ✓ requests_total 计数器正常递增 ($total_val)"
pass
else
echo " ✗ requests_total 计数器未递增 ($total_val)"
fail
fi
# 测试 HEAD 请求
metrics_head_status=$(curl -s -o /dev/null -w "%{http_code}" -I "$BASE/_metrics")
if [[ "$metrics_head_status" == "200" ]]; then
echo " ✓ /_metrics HEAD 请求 — HTTP 200"
pass
else
echo " ✗ /_metrics HEAD 请求 — 期望 200, 实际 $metrics_head_status"
fail
fi
echo ""
echo "=== 结果汇总 ==="
echo "通过: $PASS"