From d3da3e5d588f5118a905c9b7f97b52b8eb67e670 Mon Sep 17 00:00:00 2001 From: Marcel Hernandez Date: Sun, 20 Sep 2026 18:28:29 +0200 Subject: [PATCH 1/8] fpm: start refactor of fpm_status_handle_request() As a first step towards cleaning up the current logic we extract plaintext handling into its own static function. --- sapi/fpm/fpm/fpm_status.c | 157 +++++++++++++++++++++++++++++--------- 1 file changed, 119 insertions(+), 38 deletions(-) diff --git a/sapi/fpm/fpm/fpm_status.c b/sapi/fpm/fpm/fpm_status.c index ff6ef68f3949..cabeb6b4c5e2 100644 --- a/sapi/fpm/fpm/fpm_status.c +++ b/sapi/fpm/fpm/fpm_status.c @@ -122,6 +122,121 @@ int fpm_status_export_to_zval(zval *status) } /* }}} */ +static void fpm_status_handle_plaintext(struct fpm_scoreboard_s *scoreboard_p, int full) +{ + char *buffer; + char time_buffer[64]; + time_t now_epoch; + + sapi_add_header_ex(ZEND_STRL("Content-Type: text/plain"), 1, 1); + + now_epoch = time(NULL); + strftime(time_buffer, sizeof(time_buffer) - 1, "%d/%b/%Y:%H:%M:%S %z", localtime(&scoreboard_p->start_epoch)); + + spprintf(&buffer, 0, + "pool: %s\n" + "process manager: %s\n" + "start time: %s\n" + "start since: %lu\n" + "accepted conn: %lu\n" + "listen queue: %d\n" + "max listen queue: %d\n" + "listen queue len: %u\n" + "idle processes: %d\n" + "active processes: %d\n" + "total processes: %d\n" + "max active processes: %d\n" + "max children reached: %u\n" + "slow requests: %lu\n" + "memory peak: %zu\n", + scoreboard_p->pool, + PM2STR(scoreboard_p->pm), + time_buffer, + (unsigned long) (now_epoch - scoreboard_p->start_epoch), + scoreboard_p->requests, + scoreboard_p->lq, + scoreboard_p->lq_max, + scoreboard_p->lq_len, + scoreboard_p->idle, + scoreboard_p->active, + scoreboard_p->idle + scoreboard_p->active, + scoreboard_p->active_max, + scoreboard_p->max_children_reached, + scoreboard_p->slow_rq, + scoreboard_p->memory_peak); + + PUTS(buffer); + efree(buffer); + + if (!full) { + return; + } + + unsigned int i; + struct fpm_scoreboard_proc_s *proc; + struct timeval duration, now; + float cpu; + + fpm_clock_get(&now); + + for (i = 0; i < scoreboard_p->nprocs; i++) { + if (!scoreboard_p->procs[i].used) { + continue; + } + + proc = &scoreboard_p->procs[i]; + + if (proc->cpu_duration.tv_sec == 0 && proc->cpu_duration.tv_usec == 0) { + cpu = 0.; + } else { + cpu = (proc->last_request_cpu.tms_utime + proc->last_request_cpu.tms_stime + proc->last_request_cpu.tms_cutime + proc->last_request_cpu.tms_cstime) / fpm_scoreboard_get_tick() / (proc->cpu_duration.tv_sec + proc->cpu_duration.tv_usec / 1000000.) * 100.; + } + + if (proc->request_stage == FPM_REQUEST_ACCEPTING) { + duration = proc->duration; + } else { + timersub(&now, &proc->accepted, &duration); + } + + strftime(time_buffer, sizeof(time_buffer) - 1, "%d/%b/%Y:%H:%M:%S %z", localtime(&proc->start_epoch)); + + spprintf(&buffer, 0, + "\n" + "************************\n" + "pid: %d\n" + "state: %s\n" + "start time: %s\n" + "start since: %lu\n" + "requests: %lu\n" + "request duration: %lu\n" + "request method: %s\n" + "request URI: %s%s%s\n" + "content length: %zu\n" + "user: %s\n" + "script: %s\n" + "last request cpu: %.2f\n" + "last request memory: %zu\n", + (int) proc->pid, + fpm_request_get_stage_name(proc->request_stage), + time_buffer, + (unsigned long) (now_epoch - proc->start_epoch), + proc->requests, + (unsigned long) (duration.tv_sec * 1000000UL + duration.tv_usec), + proc->request_method[0] != '\0' ? proc->request_method : "-", + proc->request_uri[0] != '\0' ? proc->request_uri : "-", + proc->query_string[0] != '\0' ? "?" : "", + proc->query_string[0] != '\0' ? proc->query_string : "", + proc->content_length, + proc->auth_user[0] != '\0' ? proc->auth_user : "-", + proc->script_filename[0] != '\0' ? proc->script_filename : "-", + proc->request_stage == FPM_REQUEST_ACCEPTING ? cpu : 0., + proc->request_stage == FPM_REQUEST_ACCEPTING ? proc->memory : 0); + + PUTS(buffer); + efree(buffer); + } +} + int fpm_status_handle_request(void) /* {{{ */ { struct fpm_scoreboard_s *scoreboard_p; @@ -433,44 +548,10 @@ int fpm_status_handle_request(void) /* {{{ */ /* TEXT */ } else { - sapi_add_header_ex(ZEND_STRL("Content-Type: text/plain"), 1, 1); - time_format = "%d/%b/%Y:%H:%M:%S %z"; - - short_syntax = - "pool: %s\n" - "process manager: %s\n" - "start time: %s\n" - "start since: %lu\n" - "accepted conn: %lu\n" - "listen queue: %d\n" - "max listen queue: %d\n" - "listen queue len: %u\n" - "idle processes: %d\n" - "active processes: %d\n" - "total processes: %d\n" - "max active processes: %d\n" - "max children reached: %u\n" - "slow requests: %lu\n" - "memory peak: %zu\n"; - - if (full) { - full_syntax = - "\n" - "************************\n" - "pid: %d\n" - "state: %s\n" - "start time: %s\n" - "start since: %lu\n" - "requests: %lu\n" - "request duration: %lu\n" - "request method: %s\n" - "request URI: %s%s%s\n" - "content length: %zu\n" - "user: %s\n" - "script: %s\n" - "last request cpu: %.2f\n" - "last request memory: %zu\n"; - } + fpm_status_handle_plaintext(scoreboard_p, full); + zend_string_release_ex(_GET_str, 0); + fpm_scoreboard_free_copy(scoreboard_p); + return 1; } now_epoch = time(NULL); From 9252517662fd774b3f87e54930e73dc50a544b1c Mon Sep 17 00:00:00 2001 From: Marcel Hernandez Date: Sun, 20 Sep 2026 22:09:25 +0200 Subject: [PATCH 2/8] fpm: refactor HTML and XML status pages into own functions --- sapi/fpm/fpm/fpm_status.c | 453 ++++++++++++++++++++++++++++---------- 1 file changed, 337 insertions(+), 116 deletions(-) diff --git a/sapi/fpm/fpm/fpm_status.c b/sapi/fpm/fpm/fpm_status.c index cabeb6b4c5e2..a9978385fb09 100644 --- a/sapi/fpm/fpm/fpm_status.c +++ b/sapi/fpm/fpm/fpm_status.c @@ -237,6 +237,330 @@ static void fpm_status_handle_plaintext(struct fpm_scoreboard_s *scoreboard_p, i } } +static void fpm_status_handle_html(struct fpm_scoreboard_s *scoreboard_p, int full) +{ + char *buffer; + char time_buffer[64]; + time_t now_epoch; + + sapi_add_header_ex(ZEND_STRL("Content-Type: text/html"), 1, 1); + + now_epoch = time(NULL); + strftime(time_buffer, sizeof(time_buffer) - 1, "%d/%b/%Y:%H:%M:%S %z", localtime(&scoreboard_p->start_epoch)); + + spprintf(&buffer, 0, + "\n" + "\n" + "PHP-FPM Status Page\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "
pool%s
process manager%s
start time%s
start since%lu
accepted conn%lu
listen queue%d
max listen queue%d
listen queue len%u
idle processes%d
active processes%d
total processes%d
max active processes%d
max children reached%u
slow requests%lu
memory peak%zu
\n", + scoreboard_p->pool, + PM2STR(scoreboard_p->pm), + time_buffer, + (unsigned long) (now_epoch - scoreboard_p->start_epoch), + scoreboard_p->requests, + scoreboard_p->lq, + scoreboard_p->lq_max, + scoreboard_p->lq_len, + scoreboard_p->idle, + scoreboard_p->active, + scoreboard_p->idle + scoreboard_p->active, + scoreboard_p->active_max, + scoreboard_p->max_children_reached, + scoreboard_p->slow_rq, + scoreboard_p->memory_peak); + + PUTS(buffer); + efree(buffer); + + if (!full) { + PUTS(""); + return; + } + + unsigned int i; + struct fpm_scoreboard_proc_s *proc; + struct timeval duration, now; + float cpu; + zend_string *tmp_request_uri_string, *tmp_query_string; + char *request_uri_string, *query_string; + + fpm_clock_get(&now); + + PUTS( + "\n" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "\n"); + + for (i = 0; i < scoreboard_p->nprocs; i++) { + if (!scoreboard_p->procs[i].used) { + continue; + } + proc = &scoreboard_p->procs[i]; + + request_uri_string = NULL; + tmp_request_uri_string = NULL; + if (proc->request_uri[0] != '\0') { + tmp_request_uri_string = php_escape_html_entities_ex( + (const unsigned char *) proc->request_uri, + strlen(proc->request_uri), 1, ENT_DISALLOWED | ENT_HTML_DOC_XML1 | ENT_COMPAT, + NULL, /* double_encode */ 1, /* quiet */ 0); + request_uri_string = ZSTR_VAL(tmp_request_uri_string); + } + + query_string = NULL; + tmp_query_string = NULL; + if (proc->query_string[0] != '\0') { + tmp_query_string = php_escape_html_entities_ex( + (const unsigned char *) proc->query_string, + strlen(proc->query_string), 1, ENT_DISALLOWED | ENT_HTML_DOC_XML1 | ENT_COMPAT, + NULL, /* double_encode */ 1, /* quiet */ 0); + if (tmp_query_string) { + query_string = ZSTR_VAL(tmp_query_string); + } + } + + if (proc->cpu_duration.tv_sec == 0 && proc->cpu_duration.tv_usec == 0) { + cpu = 0.; + } else { + cpu = (proc->last_request_cpu.tms_utime + proc->last_request_cpu.tms_stime + proc->last_request_cpu.tms_cutime + proc->last_request_cpu.tms_cstime) / fpm_scoreboard_get_tick() / (proc->cpu_duration.tv_sec + proc->cpu_duration.tv_usec / 1000000.) * 100.; + } + + if (proc->request_stage == FPM_REQUEST_ACCEPTING) { + duration = proc->duration; + } else { + timersub(&now, &proc->accepted, &duration); + } + + strftime(time_buffer, sizeof(time_buffer) - 1, "%d/%b/%Y:%H:%M:%S %z", localtime(&proc->start_epoch)); + + spprintf(&buffer, 0, + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "\n", + (int) proc->pid, + fpm_request_get_stage_name(proc->request_stage), + time_buffer, + (unsigned long) (now_epoch - proc->start_epoch), + proc->requests, + (unsigned long) (duration.tv_sec * 1000000UL + duration.tv_usec), + proc->request_method[0] != '\0' ? proc->request_method : "-", + request_uri_string ? request_uri_string : "-", + query_string ? "?" : "", + query_string ? query_string : "", + proc->content_length, + proc->auth_user[0] != '\0' ? proc->auth_user : "-", + proc->script_filename[0] != '\0' ? proc->script_filename : "-", + proc->request_stage == FPM_REQUEST_ACCEPTING ? cpu : 0., + proc->request_stage == FPM_REQUEST_ACCEPTING ? proc->memory : 0); + + PUTS(buffer); + efree(buffer); + + if (tmp_request_uri_string) { + zend_string_free(tmp_request_uri_string); + } + if (tmp_query_string) { + zend_string_free(tmp_query_string); + } + } + + PUTS("
pidstatestart timestart sincerequestsrequest durationrequest methodrequest uricontent lengthuserscriptlast request cpulast request memory
%d%s%s%lu%lu%lu%s%s%s%s%zu%s%s%.2f%zu
"); +} + +static void fpm_status_handle_xml(struct fpm_scoreboard_s *scoreboard_p, int full) +{ + char *buffer; + char time_buffer[64]; + time_t now_epoch; + + sapi_add_header_ex(ZEND_STRL("Content-Type: text/xml"), 1, 1); + + now_epoch = time(NULL); + strftime(time_buffer, sizeof(time_buffer) - 1, "%s", localtime(&scoreboard_p->start_epoch)); + + spprintf(&buffer, 0, + "\n" + "\n" + "%s\n" + "%s\n" + "%s\n" + "%lu\n" + "%lu\n" + "%d\n" + "%d\n" + "%u\n" + "%d\n" + "%d\n" + "%d\n" + "%d\n" + "%u\n" + "%lu\n" + "%zu\n", + scoreboard_p->pool, + PM2STR(scoreboard_p->pm), + time_buffer, + (unsigned long) (now_epoch - scoreboard_p->start_epoch), + scoreboard_p->requests, + scoreboard_p->lq, + scoreboard_p->lq_max, + scoreboard_p->lq_len, + scoreboard_p->idle, + scoreboard_p->active, + scoreboard_p->idle + scoreboard_p->active, + scoreboard_p->active_max, + scoreboard_p->max_children_reached, + scoreboard_p->slow_rq, + scoreboard_p->memory_peak); + + PUTS(buffer); + efree(buffer); + + if (!full) { + PUTS(""); + return; + } + + unsigned int i; + struct fpm_scoreboard_proc_s *proc; + struct timeval duration, now; + float cpu; + zend_string *tmp_request_uri_string, *tmp_query_string; + char *request_uri_string, *query_string; + + fpm_clock_get(&now); + + PUTS("\n"); + + for (i = 0; i < scoreboard_p->nprocs; i++) { + if (!scoreboard_p->procs[i].used) { + continue; + } + proc = &scoreboard_p->procs[i]; + + request_uri_string = NULL; + tmp_request_uri_string = NULL; + if (proc->request_uri[0] != '\0') { + tmp_request_uri_string = php_escape_html_entities_ex( + (const unsigned char *) proc->request_uri, + strlen(proc->request_uri), 1, ENT_DISALLOWED | ENT_HTML_DOC_XML1 | ENT_COMPAT, + NULL, /* double_encode */ 1, /* quiet */ 0); + request_uri_string = ZSTR_VAL(tmp_request_uri_string); + } + + query_string = NULL; + tmp_query_string = NULL; + if (proc->query_string[0] != '\0') { + tmp_query_string = php_escape_html_entities_ex( + (const unsigned char *) proc->query_string, + strlen(proc->query_string), 1, ENT_DISALLOWED | ENT_HTML_DOC_XML1 | ENT_COMPAT, + NULL, /* double_encode */ 1, /* quiet */ 0); + if (tmp_query_string) { + query_string = ZSTR_VAL(tmp_query_string); + } + } + + if (proc->cpu_duration.tv_sec == 0 && proc->cpu_duration.tv_usec == 0) { + cpu = 0.; + } else { + cpu = (proc->last_request_cpu.tms_utime + proc->last_request_cpu.tms_stime + proc->last_request_cpu.tms_cutime + proc->last_request_cpu.tms_cstime) / fpm_scoreboard_get_tick() / (proc->cpu_duration.tv_sec + proc->cpu_duration.tv_usec / 1000000.) * 100.; + } + + if (proc->request_stage == FPM_REQUEST_ACCEPTING) { + duration = proc->duration; + } else { + timersub(&now, &proc->accepted, &duration); + } + + strftime(time_buffer, sizeof(time_buffer) - 1, "%s", localtime(&proc->start_epoch)); + + spprintf(&buffer, 0, + "" + "%d" + "%s" + "%s" + "%lu" + "%lu" + "%lu" + "%s" + "%s%s%s" + "%zu" + "%s" + "" + "%.2f" + "%zu" + "\n", + (int) proc->pid, + fpm_request_get_stage_name(proc->request_stage), + time_buffer, + (unsigned long) (now_epoch - proc->start_epoch), + proc->requests, + (unsigned long) (duration.tv_sec * 1000000UL + duration.tv_usec), + proc->request_method[0] != '\0' ? proc->request_method : "-", + request_uri_string ? request_uri_string : "-", + query_string ? "?" : "", + query_string ? query_string : "", + proc->content_length, + proc->auth_user[0] != '\0' ? proc->auth_user : "-", + proc->script_filename[0] != '\0' ? proc->script_filename : "-", + proc->request_stage == FPM_REQUEST_ACCEPTING ? cpu : 0., + proc->request_stage == FPM_REQUEST_ACCEPTING ? proc->memory : 0); + + PUTS(buffer); + efree(buffer); + + if (tmp_request_uri_string) { + zend_string_free(tmp_request_uri_string); + } + if (tmp_query_string) { + zend_string_free(tmp_query_string); + } + } + + PUTS("\n"); +} + int fpm_status_handle_request(void) /* {{{ */ { struct fpm_scoreboard_s *scoreboard_p; @@ -322,125 +646,22 @@ int fpm_status_handle_request(void) /* {{{ */ /* HTML */ if (fpm_php_is_key_in_table(_GET_str, ZEND_STRL("html"))) { - sapi_add_header_ex(ZEND_STRL("Content-Type: text/html"), 1, 1); - time_format = "%d/%b/%Y:%H:%M:%S %z"; - encode_html = true; - - short_syntax = - "\n" - "\n" - "PHP-FPM Status Page\n" - "\n" - "\n" - "\n" - "\n" - "\n" - "\n" - "\n" - "\n" - "\n" - "\n" - "\n" - "\n" - "\n" - "\n" - "\n" - "\n" - "\n" - "
pool%s
process manager%s
start time%s
start since%lu
accepted conn%lu
listen queue%d
max listen queue%d
listen queue len%u
idle processes%d
active processes%d
total processes%d
max active processes%d
max children reached%u
slow requests%lu
memory peak%zu
\n"; - - if (!full) { - short_post = ""; - } else { - full_pre = - "\n" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "\n"; - - full_syntax = - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "" - "\n"; - - full_post = "
pidstatestart timestart sincerequestsrequest durationrequest methodrequest uricontent lengthuserscriptlast request cpulast request memory
%d%s%s%lu%lu%lu%s%s%s%s%zu%s%s%.2f%zu
"; - } + fpm_status_handle_html(scoreboard_p, full); + zend_string_release_ex(_GET_str, 0); + fpm_scoreboard_free_copy(scoreboard_p); + return 1; + } /* XML */ - } else if (fpm_php_is_key_in_table(_GET_str, ZEND_STRL("xml"))) { - sapi_add_header_ex(ZEND_STRL("Content-Type: text/xml"), 1, 1); - time_format = "%s"; - encode_html = true; - - short_syntax = - "\n" - "\n" - "%s\n" - "%s\n" - "%s\n" - "%lu\n" - "%lu\n" - "%d\n" - "%d\n" - "%u\n" - "%d\n" - "%d\n" - "%d\n" - "%d\n" - "%u\n" - "%lu\n" - "%zu\n"; - - if (!full) { - short_post = ""; - } else { - full_pre = "\n"; - full_syntax = - "" - "%d" - "%s" - "%s" - "%lu" - "%lu" - "%lu" - "%s" - "%s%s%s" - "%zu" - "%s" - "" - "%.2f" - "%zu" - "\n" - ; - full_post = "\n"; - } + if (fpm_php_is_key_in_table(_GET_str, ZEND_STRL("xml"))) { + fpm_status_handle_xml(scoreboard_p, full); + zend_string_release_ex(_GET_str, 0); + fpm_scoreboard_free_copy(scoreboard_p); + return 1; + } - /* JSON */ - } else if (fpm_php_is_key_in_table(_GET_str, ZEND_STRL("json"))) { + /* JSON */ + if (fpm_php_is_key_in_table(_GET_str, ZEND_STRL("json"))) { sapi_add_header_ex(ZEND_STRL("Content-Type: application/json"), 1, 1); time_format = "%s"; From fea2b139c460f3226a6de62295d99c82467cd583 Mon Sep 17 00:00:00 2001 From: Marcel Hernandez Date: Sun, 20 Sep 2026 22:34:20 +0200 Subject: [PATCH 3/8] fpm: refactor JSON status page into own function --- sapi/fpm/fpm/fpm_status.c | 219 +++++++++++++++++++++++++++++--------- 1 file changed, 170 insertions(+), 49 deletions(-) diff --git a/sapi/fpm/fpm/fpm_status.c b/sapi/fpm/fpm/fpm_status.c index a9978385fb09..d163c26820ba 100644 --- a/sapi/fpm/fpm/fpm_status.c +++ b/sapi/fpm/fpm/fpm_status.c @@ -561,6 +561,169 @@ static void fpm_status_handle_xml(struct fpm_scoreboard_s *scoreboard_p, int ful PUTS("\n"); } +static void fpm_status_handle_json(struct fpm_scoreboard_s *scoreboard_p, int full) +{ + char *buffer; + char time_buffer[64]; + time_t now_epoch; + + sapi_add_header_ex(ZEND_STRL("Content-Type: application/json"), 1, 1); + + now_epoch = time(NULL); + strftime(time_buffer, sizeof(time_buffer) - 1, "%s", localtime(&scoreboard_p->start_epoch)); + + spprintf(&buffer, 0, + "{" + "\"pool\":\"%s\"," + "\"process manager\":\"%s\"," + "\"start time\":%s," + "\"start since\":%lu," + "\"accepted conn\":%lu," + "\"listen queue\":%d," + "\"max listen queue\":%d," + "\"listen queue len\":%u," + "\"idle processes\":%d," + "\"active processes\":%d," + "\"total processes\":%d," + "\"max active processes\":%d," + "\"max children reached\":%u," + "\"slow requests\":%lu," + "\"memory peak\":%zu", + scoreboard_p->pool, + PM2STR(scoreboard_p->pm), + time_buffer, + (unsigned long) (now_epoch - scoreboard_p->start_epoch), + scoreboard_p->requests, + scoreboard_p->lq, + scoreboard_p->lq_max, + scoreboard_p->lq_len, + scoreboard_p->idle, + scoreboard_p->active, + scoreboard_p->idle + scoreboard_p->active, + scoreboard_p->active_max, + scoreboard_p->max_children_reached, + scoreboard_p->slow_rq, + scoreboard_p->memory_peak); + + PUTS(buffer); + efree(buffer); + + if (!full) { + PUTS("}"); + return; + } + + unsigned int i; + int first; + struct fpm_scoreboard_proc_s *proc; + struct timeval duration, now; + float cpu; + zend_string *tmp_request_uri_string, *tmp_query_string; + char *request_uri_string, *query_string; + + fpm_clock_get(&now); + + PUTS(", \"processes\":["); + + first = 1; + for (i = 0; i < scoreboard_p->nprocs; i++) { + if (!scoreboard_p->procs[i].used) { + continue; + } + proc = &scoreboard_p->procs[i]; + + if (first) { + first = 0; + } else { + PUTS(","); + } + + request_uri_string = NULL; + tmp_request_uri_string = NULL; + if (proc->request_uri[0] != '\0') { + tmp_request_uri_string = php_json_encode_string(proc->request_uri, + strlen(proc->request_uri), PHP_JSON_INVALID_UTF8_IGNORE); + request_uri_string = ZSTR_VAL(tmp_request_uri_string); + if (ZSTR_LEN(tmp_request_uri_string) >= 2) { + request_uri_string[ZSTR_LEN(tmp_request_uri_string) - 1] = '\0'; + ++request_uri_string; + } + } + + query_string = NULL; + tmp_query_string = NULL; + if (proc->query_string[0] != '\0') { + tmp_query_string = php_json_encode_string(proc->query_string, + strlen(proc->query_string), PHP_JSON_INVALID_UTF8_IGNORE); + if (tmp_query_string) { + query_string = ZSTR_VAL(tmp_query_string); + if (ZSTR_LEN(tmp_query_string) >= 2) { + query_string[ZSTR_LEN(tmp_query_string) - 1] = '\0'; + ++query_string; + } + } + } + + if (proc->cpu_duration.tv_sec == 0 && proc->cpu_duration.tv_usec == 0) { + cpu = 0.; + } else { + cpu = (proc->last_request_cpu.tms_utime + proc->last_request_cpu.tms_stime + proc->last_request_cpu.tms_cutime + proc->last_request_cpu.tms_cstime) / fpm_scoreboard_get_tick() / (proc->cpu_duration.tv_sec + proc->cpu_duration.tv_usec / 1000000.) * 100.; + } + + if (proc->request_stage == FPM_REQUEST_ACCEPTING) { + duration = proc->duration; + } else { + timersub(&now, &proc->accepted, &duration); + } + + strftime(time_buffer, sizeof(time_buffer) - 1, "%s", localtime(&proc->start_epoch)); + + spprintf(&buffer, 0, + "{" + "\"pid\":%d," + "\"state\":\"%s\"," + "\"start time\":%s," + "\"start since\":%lu," + "\"requests\":%lu," + "\"request duration\":%lu," + "\"request method\":\"%s\"," + "\"request uri\":\"%s%s%s\"," + "\"content length\":%zu," + "\"user\":\"%s\"," + "\"script\":\"%s\"," + "\"last request cpu\":%.2f," + "\"last request memory\":%zu" + "}", + (int) proc->pid, + fpm_request_get_stage_name(proc->request_stage), + time_buffer, + (unsigned long) (now_epoch - proc->start_epoch), + proc->requests, + (unsigned long) (duration.tv_sec * 1000000UL + duration.tv_usec), + proc->request_method[0] != '\0' ? proc->request_method : "-", + request_uri_string ? request_uri_string : "-", + query_string ? "?" : "", + query_string ? query_string : "", + proc->content_length, + proc->auth_user[0] != '\0' ? proc->auth_user : "-", + proc->script_filename[0] != '\0' ? proc->script_filename : "-", + proc->request_stage == FPM_REQUEST_ACCEPTING ? cpu : 0., + proc->request_stage == FPM_REQUEST_ACCEPTING ? proc->memory : 0); + + PUTS(buffer); + efree(buffer); + + if (tmp_request_uri_string) { + zend_string_free(tmp_request_uri_string); + } + if (tmp_query_string) { + zend_string_free(tmp_query_string); + } + } + + PUTS("]}"); +} + int fpm_status_handle_request(void) /* {{{ */ { struct fpm_scoreboard_s *scoreboard_p; @@ -662,56 +825,14 @@ int fpm_status_handle_request(void) /* {{{ */ /* JSON */ if (fpm_php_is_key_in_table(_GET_str, ZEND_STRL("json"))) { - sapi_add_header_ex(ZEND_STRL("Content-Type: application/json"), 1, 1); - time_format = "%s"; - - encode_json = true; - - short_syntax = - "{" - "\"pool\":\"%s\"," - "\"process manager\":\"%s\"," - "\"start time\":%s," - "\"start since\":%lu," - "\"accepted conn\":%lu," - "\"listen queue\":%d," - "\"max listen queue\":%d," - "\"listen queue len\":%u," - "\"idle processes\":%d," - "\"active processes\":%d," - "\"total processes\":%d," - "\"max active processes\":%d," - "\"max children reached\":%u," - "\"slow requests\":%lu," - "\"memory peak\":%zu"; - - if (!full) { - short_post = "}"; - } else { - full_separator = ","; - full_pre = ", \"processes\":["; - - full_syntax = "{" - "\"pid\":%d," - "\"state\":\"%s\"," - "\"start time\":%s," - "\"start since\":%lu," - "\"requests\":%lu," - "\"request duration\":%lu," - "\"request method\":\"%s\"," - "\"request uri\":\"%s%s%s\"," - "\"content length\":%zu," - "\"user\":\"%s\"," - "\"script\":\"%s\"," - "\"last request cpu\":%.2f," - "\"last request memory\":%zu" - "}"; - - full_post = "]}"; - } + fpm_status_handle_json(scoreboard_p, full); + zend_string_release_ex(_GET_str, 0); + fpm_scoreboard_free_copy(scoreboard_p); + return 1; + } - /* OpenMetrics */ - } else if (fpm_php_is_key_in_table(_GET_str, ZEND_STRL("openmetrics"))) { + /* OpenMetrics */ + if (fpm_php_is_key_in_table(_GET_str, ZEND_STRL("openmetrics"))) { sapi_add_header_ex(ZEND_STRL("Content-Type: application/openmetrics-text; version=1.0.0; charset=utf-8"), 1, 1); time_format = "%s"; From 9c1ddd98a993b2bde528cc11f6adf5381bb68adb Mon Sep 17 00:00:00 2001 From: Marcel Hernandez Date: Sun, 20 Sep 2026 22:51:28 +0200 Subject: [PATCH 4/8] fpm: refactor OpenMetrics and clean up extant dead code in main function --- sapi/fpm/fpm/fpm_status.c | 309 +++++++++----------------------------- 1 file changed, 73 insertions(+), 236 deletions(-) diff --git a/sapi/fpm/fpm/fpm_status.c b/sapi/fpm/fpm/fpm_status.c index d163c26820ba..199556f7746f 100644 --- a/sapi/fpm/fpm/fpm_status.c +++ b/sapi/fpm/fpm/fpm_status.c @@ -724,16 +724,78 @@ static void fpm_status_handle_json(struct fpm_scoreboard_s *scoreboard_p, int fu PUTS("]}"); } +static void fpm_status_handle_openmetrics(struct fpm_scoreboard_s *scoreboard_p, int full) +{ + char *buffer; + time_t now_epoch; + + sapi_add_header_ex(ZEND_STRL("Content-Type: application/openmetrics-text; version=1.0.0; charset=utf-8"), 1, 1); + + now_epoch = time(NULL); + + spprintf(&buffer, 0, + "# HELP phpfpm_up Could pool %s using a %s PM on PHP-FPM be reached?\n" + "# TYPE phpfpm_up gauge\n" + "phpfpm_up 1\n" + "# HELP phpfpm_start_since The number of seconds since FPM has started.\n" + "# TYPE phpfpm_start_since counter\n" + "phpfpm_start_since %lu\n" + "# HELP phpfpm_accepted_connections The number of requests accepted by the pool.\n" + "# TYPE phpfpm_accepted_connections counter\n" + "phpfpm_accepted_connections %lu\n" + "# HELP phpfpm_listen_queue The number of requests in the queue of pending connections.\n" + "# TYPE phpfpm_listen_queue gauge\n" + "phpfpm_listen_queue %d\n" + "# HELP phpfpm_max_listen_queue The maximum number of requests in the queue of pending connections since FPM has started.\n" + "# TYPE phpfpm_max_listen_queue counter\n" + "phpfpm_max_listen_queue %d\n" + "# TYPE phpfpm_listen_queue_length gauge\n" + "# HELP phpfpm_listen_queue_length The size of the socket queue of pending connections.\n" + "phpfpm_listen_queue_length %u\n" + "# HELP phpfpm_idle_processes The number of idle processes.\n" + "# TYPE phpfpm_idle_processes gauge\n" + "phpfpm_idle_processes %d\n" + "# HELP phpfpm_active_processes The number of active processes.\n" + "# TYPE phpfpm_active_processes gauge\n" + "phpfpm_active_processes %d\n" + "# HELP phpfpm_total_processes The number of idle + active processes.\n" + "# TYPE phpfpm_total_processes gauge\n" + "phpfpm_total_processes %d\n" + "# HELP phpfpm_max_active_processes The maximum number of active processes since FPM has started.\n" + "# TYPE phpfpm_max_active_processes counter\n" + "phpfpm_max_active_processes %d\n" + "# HELP phpfpm_max_children_reached The number of times, the process limit has been reached, when pm tries to start more children (works only for pm 'dynamic' and 'ondemand').\n" + "# TYPE phpfpm_max_children_reached counter\n" + "phpfpm_max_children_reached %u\n" + "# HELP phpfpm_slow_requests The number of requests that exceeded your 'request_slowlog_timeout' value.\n" + "# TYPE phpfpm_slow_requests counter\n" + "phpfpm_slow_requests %lu\n" + "# HELP phpfpm_memory_peak The memory usage peak since FPM has started.\n" + "# TYPE phpfpm_memory_peak gauge\n" + "phpfpm_memory_peak %zu\n" + "# EOF\n", + scoreboard_p->pool, + PM2STR(scoreboard_p->pm), + (unsigned long) (now_epoch - scoreboard_p->start_epoch), + scoreboard_p->requests, + scoreboard_p->lq, + scoreboard_p->lq_max, + scoreboard_p->lq_len, + scoreboard_p->idle, + scoreboard_p->active, + scoreboard_p->idle + scoreboard_p->active, + scoreboard_p->active_max, + scoreboard_p->max_children_reached, + scoreboard_p->slow_rq, + scoreboard_p->memory_peak); + + PUTS(buffer); + efree(buffer); +} + int fpm_status_handle_request(void) /* {{{ */ { struct fpm_scoreboard_s *scoreboard_p; - struct fpm_scoreboard_proc_s *proc; - char *buffer, *time_format, time_buffer[64]; - time_t now_epoch; - int full, has_start_time; - bool encode_html, encode_json; - char *short_syntax, *short_post; - char *full_pre, *full_syntax, *full_post, *full_separator; if (!SG(request_info).request_uri) { return 0; @@ -759,17 +821,13 @@ int fpm_status_handle_request(void) /* {{{ */ /* STATUS */ if (fpm_status_uri && !strcmp(fpm_status_uri, SG(request_info).request_uri)) { zend_string *_GET_str; + int full; fpm_request_executing(); /* full status ? */ _GET_str = ZSTR_INIT_LITERAL("_GET", 0); full = fpm_php_is_key_in_table(_GET_str, ZEND_STRL("full")); - short_syntax = short_post = NULL; - full_separator = full_pre = full_syntax = full_post = NULL; - encode_html = false; - encode_json = false; - has_start_time = 1; scoreboard_p = fpm_scoreboard_get(); if (scoreboard_p) { @@ -833,236 +891,15 @@ int fpm_status_handle_request(void) /* {{{ */ /* OpenMetrics */ if (fpm_php_is_key_in_table(_GET_str, ZEND_STRL("openmetrics"))) { - sapi_add_header_ex(ZEND_STRL("Content-Type: application/openmetrics-text; version=1.0.0; charset=utf-8"), 1, 1); - time_format = "%s"; - - short_syntax = - "# HELP phpfpm_up Could pool %s using a %s PM on PHP-FPM be reached?\n" - "# TYPE phpfpm_up gauge\n" - "phpfpm_up 1\n" - "# HELP phpfpm_start_since The number of seconds since FPM has started.\n" - "# TYPE phpfpm_start_since counter\n" - "phpfpm_start_since %lu\n" - "# HELP phpfpm_accepted_connections The number of requests accepted by the pool.\n" - "# TYPE phpfpm_accepted_connections counter\n" - "phpfpm_accepted_connections %lu\n" - "# HELP phpfpm_listen_queue The number of requests in the queue of pending connections.\n" - "# TYPE phpfpm_listen_queue gauge\n" - "phpfpm_listen_queue %d\n" - "# HELP phpfpm_max_listen_queue The maximum number of requests in the queue of pending connections since FPM has started.\n" - "# TYPE phpfpm_max_listen_queue counter\n" - "phpfpm_max_listen_queue %d\n" - "# TYPE phpfpm_listen_queue_length gauge\n" - "# HELP phpfpm_listen_queue_length The size of the socket queue of pending connections.\n" - "phpfpm_listen_queue_length %u\n" - "# HELP phpfpm_idle_processes The number of idle processes.\n" - "# TYPE phpfpm_idle_processes gauge\n" - "phpfpm_idle_processes %d\n" - "# HELP phpfpm_active_processes The number of active processes.\n" - "# TYPE phpfpm_active_processes gauge\n" - "phpfpm_active_processes %d\n" - "# HELP phpfpm_total_processes The number of idle + active processes.\n" - "# TYPE phpfpm_total_processes gauge\n" - "phpfpm_total_processes %d\n" - "# HELP phpfpm_max_active_processes The maximum number of active processes since FPM has started.\n" - "# TYPE phpfpm_max_active_processes counter\n" - "phpfpm_max_active_processes %d\n" - "# HELP phpfpm_max_children_reached The number of times, the process limit has been reached, when pm tries to start more children (works only for pm 'dynamic' and 'ondemand').\n" - "# TYPE phpfpm_max_children_reached counter\n" - "phpfpm_max_children_reached %u\n" - "# HELP phpfpm_slow_requests The number of requests that exceeded your 'request_slowlog_timeout' value.\n" - "# TYPE phpfpm_slow_requests counter\n" - "phpfpm_slow_requests %lu\n" - "# HELP phpfpm_memory_peak The memory usage peak since FPM has started.\n" - "# TYPE phpfpm_memory_peak gauge\n" - "phpfpm_memory_peak %zu\n" - "# EOF\n"; - - has_start_time = 0; - if (!full) { - short_post = ""; - } else { - full_separator = ""; - full_pre = ""; - full_syntax = ""; - full_post = ""; - } - - /* TEXT */ - } else { - fpm_status_handle_plaintext(scoreboard_p, full); + fpm_status_handle_openmetrics(scoreboard_p, full); zend_string_release_ex(_GET_str, 0); fpm_scoreboard_free_copy(scoreboard_p); return 1; } - now_epoch = time(NULL); - if (has_start_time) { - strftime(time_buffer, sizeof(time_buffer) - 1, time_format, localtime(&scoreboard_p->start_epoch)); - spprintf(&buffer, 0, short_syntax, - scoreboard_p->pool, - PM2STR(scoreboard_p->pm), - time_buffer, - (unsigned long) (now_epoch - scoreboard_p->start_epoch), - scoreboard_p->requests, - scoreboard_p->lq, - scoreboard_p->lq_max, - scoreboard_p->lq_len, - scoreboard_p->idle, - scoreboard_p->active, - scoreboard_p->idle + scoreboard_p->active, - scoreboard_p->active_max, - scoreboard_p->max_children_reached, - scoreboard_p->slow_rq, - scoreboard_p->memory_peak); - } else { - spprintf(&buffer, 0, short_syntax, - scoreboard_p->pool, - PM2STR(scoreboard_p->pm), - (unsigned long) (now_epoch - scoreboard_p->start_epoch), - scoreboard_p->requests, - scoreboard_p->lq, - scoreboard_p->lq_max, - scoreboard_p->lq_len, - scoreboard_p->idle, - scoreboard_p->active, - scoreboard_p->idle + scoreboard_p->active, - scoreboard_p->active_max, - scoreboard_p->max_children_reached, - scoreboard_p->slow_rq, - scoreboard_p->memory_peak); - } - - PUTS(buffer); - efree(buffer); + /* TEXT */ + fpm_status_handle_plaintext(scoreboard_p, full); zend_string_release_ex(_GET_str, 0); - - if (short_post) { - PUTS(short_post); - } - - /* no need to test the var 'full' */ - if (full_syntax) { - unsigned int i; - int first; - zend_string *tmp_query_string, *tmp_request_uri_string; - char *query_string, *request_uri_string; - struct timeval duration, now; - float cpu; - - fpm_clock_get(&now); - - if (full_pre) { - PUTS(full_pre); - } - - first = 1; - for (i=0; inprocs; i++) { - if (!scoreboard_p->procs[i].used) { - continue; - } - proc = &scoreboard_p->procs[i]; - - if (first) { - first = 0; - } else { - if (full_separator) { - PUTS(full_separator); - } - } - - request_uri_string = NULL; - tmp_request_uri_string = NULL; - if (proc->request_uri[0] != '\0') { - if (encode_html) { - tmp_request_uri_string = php_escape_html_entities_ex( - (const unsigned char *) proc->request_uri, - strlen(proc->request_uri), 1, ENT_DISALLOWED | ENT_HTML_DOC_XML1 | ENT_COMPAT, - NULL, /* double_encode */ 1, /* quiet */ 0); - request_uri_string = ZSTR_VAL(tmp_request_uri_string); - } else if (encode_json) { - tmp_request_uri_string = php_json_encode_string(proc->request_uri, - strlen(proc->request_uri), PHP_JSON_INVALID_UTF8_IGNORE); - request_uri_string = ZSTR_VAL(tmp_request_uri_string); - /* remove quotes around the string */ - if (ZSTR_LEN(tmp_request_uri_string) >= 2) { - request_uri_string[ZSTR_LEN(tmp_request_uri_string) - 1] = '\0'; - ++request_uri_string; - } - } else { - request_uri_string = proc->request_uri; - } - } - - query_string = NULL; - tmp_query_string = NULL; - if (proc->query_string[0] != '\0') { - if (encode_html) { - tmp_query_string = php_escape_html_entities_ex( - (const unsigned char *) proc->query_string, - strlen(proc->query_string), 1, ENT_DISALLOWED | ENT_HTML_DOC_XML1 | ENT_COMPAT, - NULL, /* double_encode */ 1, /* quiet */ 0); - } else if (encode_json) { - tmp_query_string = php_json_encode_string(proc->query_string, - strlen(proc->query_string), PHP_JSON_INVALID_UTF8_IGNORE); - } else { - query_string = proc->query_string; - } - if (tmp_query_string) { - query_string = ZSTR_VAL(tmp_query_string); - /* remove quotes around the string */ - if (encode_json && ZSTR_LEN(tmp_query_string) >= 2) { - query_string[ZSTR_LEN(tmp_query_string) - 1] = '\0'; - ++query_string; - } - } - } - - /* prevent NaN */ - if (proc->cpu_duration.tv_sec == 0 && proc->cpu_duration.tv_usec == 0) { - cpu = 0.; - } else { - cpu = (proc->last_request_cpu.tms_utime + proc->last_request_cpu.tms_stime + proc->last_request_cpu.tms_cutime + proc->last_request_cpu.tms_cstime) / fpm_scoreboard_get_tick() / (proc->cpu_duration.tv_sec + proc->cpu_duration.tv_usec / 1000000.) * 100.; - } - - if (proc->request_stage == FPM_REQUEST_ACCEPTING) { - duration = proc->duration; - } else { - timersub(&now, &proc->accepted, &duration); - } - strftime(time_buffer, sizeof(time_buffer) - 1, time_format, localtime(&proc->start_epoch)); - spprintf(&buffer, 0, full_syntax, - (int) proc->pid, - fpm_request_get_stage_name(proc->request_stage), - time_buffer, - (unsigned long) (now_epoch - proc->start_epoch), - proc->requests, - (unsigned long) (duration.tv_sec * 1000000UL + duration.tv_usec), - proc->request_method[0] != '\0' ? proc->request_method : "-", - request_uri_string ? request_uri_string : "-", - query_string ? "?" : "", - query_string ? query_string : "", - proc->content_length, - proc->auth_user[0] != '\0' ? proc->auth_user : "-", - proc->script_filename[0] != '\0' ? proc->script_filename : "-", - proc->request_stage == FPM_REQUEST_ACCEPTING ? cpu : 0., - proc->request_stage == FPM_REQUEST_ACCEPTING ? proc->memory : 0); - PUTS(buffer); - efree(buffer); - - if (tmp_request_uri_string) { - zend_string_free(tmp_request_uri_string); - } - if (tmp_query_string) { - zend_string_free(tmp_query_string); - } - } - - if (full_post) { - PUTS(full_post); - } - } - fpm_scoreboard_free_copy(scoreboard_p); return 1; } From bc2742ab1b92feef5ab749e311167344f3f20889 Mon Sep 17 00:00:00 2001 From: Marcel Hernandez Date: Sun, 20 Sep 2026 23:10:32 +0200 Subject: [PATCH 5/8] fpm: cleanup and extract a common helper for all execution paths --- sapi/fpm/fpm/fpm_status.c | 102 +++++++++++--------------------------- 1 file changed, 29 insertions(+), 73 deletions(-) diff --git a/sapi/fpm/fpm/fpm_status.c b/sapi/fpm/fpm/fpm_status.c index 199556f7746f..a896d38dc88f 100644 --- a/sapi/fpm/fpm/fpm_status.c +++ b/sapi/fpm/fpm/fpm_status.c @@ -122,6 +122,25 @@ int fpm_status_export_to_zval(zval *status) } /* }}} */ +static void fpm_status_proc_get_duration_and_cpu( + struct fpm_scoreboard_proc_s *proc, + struct timeval *now, + struct timeval *duration, + float *cpu) +{ + if (proc->request_stage == FPM_REQUEST_ACCEPTING) { + *duration = proc->duration; + } else { + timersub(now, &proc->accepted, duration); + } + + if (proc->cpu_duration.tv_sec == 0 && proc->cpu_duration.tv_usec == 0) { + *cpu = 0.; + } else { + *cpu = (proc->last_request_cpu.tms_utime + proc->last_request_cpu.tms_stime + proc->last_request_cpu.tms_cutime + proc->last_request_cpu.tms_cstime) / fpm_scoreboard_get_tick() / (proc->cpu_duration.tv_sec + proc->cpu_duration.tv_usec / 1000000.) * 100.; + } +} + static void fpm_status_handle_plaintext(struct fpm_scoreboard_s *scoreboard_p, int full) { char *buffer; @@ -186,17 +205,7 @@ static void fpm_status_handle_plaintext(struct fpm_scoreboard_s *scoreboard_p, i proc = &scoreboard_p->procs[i]; - if (proc->cpu_duration.tv_sec == 0 && proc->cpu_duration.tv_usec == 0) { - cpu = 0.; - } else { - cpu = (proc->last_request_cpu.tms_utime + proc->last_request_cpu.tms_stime + proc->last_request_cpu.tms_cutime + proc->last_request_cpu.tms_cstime) / fpm_scoreboard_get_tick() / (proc->cpu_duration.tv_sec + proc->cpu_duration.tv_usec / 1000000.) * 100.; - } - - if (proc->request_stage == FPM_REQUEST_ACCEPTING) { - duration = proc->duration; - } else { - timersub(&now, &proc->accepted, &duration); - } + fpm_status_proc_get_duration_and_cpu(proc, &now, &duration, &cpu); strftime(time_buffer, sizeof(time_buffer) - 1, "%d/%b/%Y:%H:%M:%S %z", localtime(&proc->start_epoch)); @@ -349,17 +358,7 @@ static void fpm_status_handle_html(struct fpm_scoreboard_s *scoreboard_p, int fu } } - if (proc->cpu_duration.tv_sec == 0 && proc->cpu_duration.tv_usec == 0) { - cpu = 0.; - } else { - cpu = (proc->last_request_cpu.tms_utime + proc->last_request_cpu.tms_stime + proc->last_request_cpu.tms_cutime + proc->last_request_cpu.tms_cstime) / fpm_scoreboard_get_tick() / (proc->cpu_duration.tv_sec + proc->cpu_duration.tv_usec / 1000000.) * 100.; - } - - if (proc->request_stage == FPM_REQUEST_ACCEPTING) { - duration = proc->duration; - } else { - timersub(&now, &proc->accepted, &duration); - } + fpm_status_proc_get_duration_and_cpu(proc, &now, &duration, &cpu); strftime(time_buffer, sizeof(time_buffer) - 1, "%d/%b/%Y:%H:%M:%S %z", localtime(&proc->start_epoch)); @@ -501,17 +500,7 @@ static void fpm_status_handle_xml(struct fpm_scoreboard_s *scoreboard_p, int ful } } - if (proc->cpu_duration.tv_sec == 0 && proc->cpu_duration.tv_usec == 0) { - cpu = 0.; - } else { - cpu = (proc->last_request_cpu.tms_utime + proc->last_request_cpu.tms_stime + proc->last_request_cpu.tms_cutime + proc->last_request_cpu.tms_cstime) / fpm_scoreboard_get_tick() / (proc->cpu_duration.tv_sec + proc->cpu_duration.tv_usec / 1000000.) * 100.; - } - - if (proc->request_stage == FPM_REQUEST_ACCEPTING) { - duration = proc->duration; - } else { - timersub(&now, &proc->accepted, &duration); - } + fpm_status_proc_get_duration_and_cpu(proc, &now, &duration, &cpu); strftime(time_buffer, sizeof(time_buffer) - 1, "%s", localtime(&proc->start_epoch)); @@ -664,17 +653,7 @@ static void fpm_status_handle_json(struct fpm_scoreboard_s *scoreboard_p, int fu } } - if (proc->cpu_duration.tv_sec == 0 && proc->cpu_duration.tv_usec == 0) { - cpu = 0.; - } else { - cpu = (proc->last_request_cpu.tms_utime + proc->last_request_cpu.tms_stime + proc->last_request_cpu.tms_cutime + proc->last_request_cpu.tms_cstime) / fpm_scoreboard_get_tick() / (proc->cpu_duration.tv_sec + proc->cpu_duration.tv_usec / 1000000.) * 100.; - } - - if (proc->request_stage == FPM_REQUEST_ACCEPTING) { - duration = proc->duration; - } else { - timersub(&now, &proc->accepted, &duration); - } + fpm_status_proc_get_duration_and_cpu(proc, &now, &duration, &cpu); strftime(time_buffer, sizeof(time_buffer) - 1, "%s", localtime(&proc->start_epoch)); @@ -795,8 +774,6 @@ static void fpm_status_handle_openmetrics(struct fpm_scoreboard_s *scoreboard_p, int fpm_status_handle_request(void) /* {{{ */ { - struct fpm_scoreboard_s *scoreboard_p; - if (!SG(request_info).request_uri) { return 0; } @@ -820,6 +797,7 @@ int fpm_status_handle_request(void) /* {{{ */ /* STATUS */ if (fpm_status_uri && !strcmp(fpm_status_uri, SG(request_info).request_uri)) { + struct fpm_scoreboard_s *scoreboard_p; zend_string *_GET_str; int full; @@ -865,40 +843,18 @@ int fpm_status_handle_request(void) /* {{{ */ return 1; } - /* HTML */ if (fpm_php_is_key_in_table(_GET_str, ZEND_STRL("html"))) { fpm_status_handle_html(scoreboard_p, full); - zend_string_release_ex(_GET_str, 0); - fpm_scoreboard_free_copy(scoreboard_p); - return 1; - } - - /* XML */ - if (fpm_php_is_key_in_table(_GET_str, ZEND_STRL("xml"))) { + } else if (fpm_php_is_key_in_table(_GET_str, ZEND_STRL("xml"))) { fpm_status_handle_xml(scoreboard_p, full); - zend_string_release_ex(_GET_str, 0); - fpm_scoreboard_free_copy(scoreboard_p); - return 1; - } - - /* JSON */ - if (fpm_php_is_key_in_table(_GET_str, ZEND_STRL("json"))) { + } else if (fpm_php_is_key_in_table(_GET_str, ZEND_STRL("json"))) { fpm_status_handle_json(scoreboard_p, full); - zend_string_release_ex(_GET_str, 0); - fpm_scoreboard_free_copy(scoreboard_p); - return 1; - } - - /* OpenMetrics */ - if (fpm_php_is_key_in_table(_GET_str, ZEND_STRL("openmetrics"))) { + } else if (fpm_php_is_key_in_table(_GET_str, ZEND_STRL("openmetrics"))) { fpm_status_handle_openmetrics(scoreboard_p, full); - zend_string_release_ex(_GET_str, 0); - fpm_scoreboard_free_copy(scoreboard_p); - return 1; + } else { + fpm_status_handle_plaintext(scoreboard_p, full); } - /* TEXT */ - fpm_status_handle_plaintext(scoreboard_p, full); zend_string_release_ex(_GET_str, 0); fpm_scoreboard_free_copy(scoreboard_p); return 1; From b42eebbeac2def65b4ac5618c07fbb1600a5cf25 Mon Sep 17 00:00:00 2001 From: Marcel Hernandez Date: Mon, 21 Sep 2026 00:10:58 +0200 Subject: [PATCH 6/8] fpm: implement full OpenMetrics status report --- sapi/fpm/fpm/fpm_status.c | 102 ++++++++++++++++++++++++++++++++++++-- sapi/fpm/tests/status.inc | 2 +- 2 files changed, 100 insertions(+), 4 deletions(-) diff --git a/sapi/fpm/fpm/fpm_status.c b/sapi/fpm/fpm/fpm_status.c index a896d38dc88f..8bd4faa256fe 100644 --- a/sapi/fpm/fpm/fpm_status.c +++ b/sapi/fpm/fpm/fpm_status.c @@ -2,6 +2,7 @@ #include "php.h" #include "zend_long.h" +#include "zend_smart_str.h" #include "SAPI.h" #include @@ -13,6 +14,7 @@ #include "fpm_atomic.h" #include "fpm_conf.h" #include "fpm_php.h" +#include "fpm_request.h" #include "ext/standard/html.h" #include "ext/json/php_json.h" @@ -728,8 +730,8 @@ static void fpm_status_handle_openmetrics(struct fpm_scoreboard_s *scoreboard_p, "# HELP phpfpm_max_listen_queue The maximum number of requests in the queue of pending connections since FPM has started.\n" "# TYPE phpfpm_max_listen_queue counter\n" "phpfpm_max_listen_queue %d\n" - "# TYPE phpfpm_listen_queue_length gauge\n" "# HELP phpfpm_listen_queue_length The size of the socket queue of pending connections.\n" + "# TYPE phpfpm_listen_queue_length gauge\n" "phpfpm_listen_queue_length %u\n" "# HELP phpfpm_idle_processes The number of idle processes.\n" "# TYPE phpfpm_idle_processes gauge\n" @@ -751,8 +753,7 @@ static void fpm_status_handle_openmetrics(struct fpm_scoreboard_s *scoreboard_p, "phpfpm_slow_requests %lu\n" "# HELP phpfpm_memory_peak The memory usage peak since FPM has started.\n" "# TYPE phpfpm_memory_peak gauge\n" - "phpfpm_memory_peak %zu\n" - "# EOF\n", + "phpfpm_memory_peak %zu\n", scoreboard_p->pool, PM2STR(scoreboard_p->pm), (unsigned long) (now_epoch - scoreboard_p->start_epoch), @@ -770,6 +771,101 @@ static void fpm_status_handle_openmetrics(struct fpm_scoreboard_s *scoreboard_p, PUTS(buffer); efree(buffer); + + if (!full) { + PUTS("# EOF\n"); + return; + } + + unsigned int i; + struct fpm_scoreboard_proc_s *proc; + struct timeval duration, now; + float cpu; + smart_str buf_state = {0}; + smart_str buf_requests = {0}; + smart_str buf_duration = {0}; + smart_str buf_cpu = {0}; + smart_str buf_memory = {0}; + + fpm_clock_get(&now); + + for (i = 0; i < scoreboard_p->nprocs; i++) { + if (!scoreboard_p->procs[i].used) { + continue; + } + proc = &scoreboard_p->procs[i]; + + fpm_status_proc_get_duration_and_cpu(proc, &now, &duration, &cpu); + + for (int s = FPM_REQUEST_CREATING; s <= FPM_REQUEST_FINISHED; s++) { + smart_str_append_printf(&buf_state, + "phpfpm_process_state{pool=\"%s\",child=\"%u\",state=\"%s\"} %d\n", + scoreboard_p->pool, i, fpm_request_get_stage_name(s), + proc->request_stage == s ? 1 : 0); + } + + smart_str_append_printf(&buf_requests, + "phpfpm_process_requests{pool=\"%s\",child=\"%u\"} %lu\n", + scoreboard_p->pool, i, proc->requests); + + smart_str_append_printf(&buf_duration, + "phpfpm_process_request_duration{pool=\"%s\",child=\"%u\"} %lu\n", + scoreboard_p->pool, i, + (unsigned long) (duration.tv_sec * 1000000UL + duration.tv_usec)); + + smart_str_append_printf(&buf_cpu, + "phpfpm_process_last_request_cpu{pool=\"%s\",child=\"%u\"} %.2f\n", + scoreboard_p->pool, i, + proc->request_stage == FPM_REQUEST_ACCEPTING ? cpu : 0.); + + smart_str_append_printf(&buf_memory, + "phpfpm_process_last_request_memory{pool=\"%s\",child=\"%u\"} %zu\n", + scoreboard_p->pool, i, + proc->request_stage == FPM_REQUEST_ACCEPTING ? proc->memory : 0); + } + + /* buf.s is NULL for all smart_strs when no workers have used == true */ + PUTS("# HELP phpfpm_process_state The state of the process (Idle, Running, ...).\n" + "# TYPE phpfpm_process_state gauge\n"); + smart_str_0(&buf_state); + if (buf_state.s) { + PUTS(ZSTR_VAL(buf_state.s)); + } + smart_str_free(&buf_state); + + PUTS("# HELP phpfpm_process_requests The number of requests the process has served.\n" + "# TYPE phpfpm_process_requests counter\n"); + smart_str_0(&buf_requests); + if (buf_requests.s) { + PUTS(ZSTR_VAL(buf_requests.s)); + } + smart_str_free(&buf_requests); + + PUTS("# HELP phpfpm_process_request_duration The duration in microseconds of the current or last request.\n" + "# TYPE phpfpm_process_request_duration gauge\n"); + smart_str_0(&buf_duration); + if (buf_duration.s) { + PUTS(ZSTR_VAL(buf_duration.s)); + } + smart_str_free(&buf_duration); + + PUTS("# HELP phpfpm_process_last_request_cpu The %cpu the last request consumed.\n" + "# TYPE phpfpm_process_last_request_cpu gauge\n"); + smart_str_0(&buf_cpu); + if (buf_cpu.s) { + PUTS(ZSTR_VAL(buf_cpu.s)); + } + smart_str_free(&buf_cpu); + + PUTS("# HELP phpfpm_process_last_request_memory The max amount of memory the last request consumed.\n" + "# TYPE phpfpm_process_last_request_memory gauge\n"); + smart_str_0(&buf_memory); + if (buf_memory.s) { + PUTS(ZSTR_VAL(buf_memory.s)); + } + smart_str_free(&buf_memory); + + PUTS("# EOF\n"); } int fpm_status_handle_request(void) /* {{{ */ diff --git a/sapi/fpm/tests/status.inc b/sapi/fpm/tests/status.inc index 76ef7fd55e11..f51702a0b25b 100644 --- a/sapi/fpm/tests/status.inc +++ b/sapi/fpm/tests/status.inc @@ -246,8 +246,8 @@ class Status "# HELP phpfpm_max_listen_queue The maximum number of requests in the queue of pending connections since FPM has started\.\n" . "# TYPE phpfpm_max_listen_queue counter\n" . "phpfpm_max_listen_queue " . $fields['max listen queue'] . "\n" . - "# TYPE phpfpm_listen_queue_length gauge\n" . "# HELP phpfpm_listen_queue_length The size of the socket queue of pending connections\.\n" . + "# TYPE phpfpm_listen_queue_length gauge\n" . "phpfpm_listen_queue_length " . $fields['listen queue len'] . "\n" . "# HELP phpfpm_idle_processes The number of idle processes\.\n" . "# TYPE phpfpm_idle_processes gauge\n" . From 5130807794cf0f4a31788326b32936892b0e4a08 Mon Sep 17 00:00:00 2001 From: Marcel Hernandez Date: Mon, 21 Sep 2026 00:19:40 +0200 Subject: [PATCH 7/8] fpm: test full OpenMetrics format --- sapi/fpm/tests/status-openmetrics-full.phpt | 122 ++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 sapi/fpm/tests/status-openmetrics-full.phpt diff --git a/sapi/fpm/tests/status-openmetrics-full.phpt b/sapi/fpm/tests/status-openmetrics-full.phpt new file mode 100644 index 000000000000..bd19818e00ab --- /dev/null +++ b/sapi/fpm/tests/status-openmetrics-full.phpt @@ -0,0 +1,122 @@ +--TEST-- +FPM: OpenMetrics full status with per-worker metrics +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogStartNotices(); +$tester->request()->expectEmptyBody(); + +$response = $tester->request( + 'openmetrics&full', + [], + '/status' +); + +$body = $response->getBody('application/openmetrics-text; version=1.0.0; charset=utf-8'); + +if ($body === null) { + echo "ERROR: Could not get OpenMetrics body\n"; + $tester->terminate(); + $tester->close(); + return; +} + +$ok = true; + +/* Pool-level metrics should be present */ +if (!preg_match('/^# HELP phpfpm_up /m', $body)) { + echo "ERROR: Missing phpfpm_up metric\n"; + $ok = false; +} + +/* Per-worker state metrics with one-hot encoding */ +if (!preg_match('/^# HELP phpfpm_process_state /m', $body)) { + echo "ERROR: Missing phpfpm_process_state HELP\n"; + $ok = false; +} +if (!preg_match('/^# TYPE phpfpm_process_state gauge$/m', $body)) { + echo "ERROR: Missing phpfpm_process_state TYPE\n"; + $ok = false; +} + +/* Each child should have exactly 7 state lines (one per possible state) */ +for ($child = 0; $child < 2; $child++) { + $statePattern = '/phpfpm_process_state\{pool="unconfined",child="' . $child . '",state="[^"]+"\} [01]/'; + preg_match_all($statePattern, $body, $matches); + if (count($matches[0]) !== 7) { + echo "ERROR: Expected 7 state lines for child $child, got " . count($matches[0]) . "\n"; + $ok = false; + } + /* Exactly one state should be 1 */ + $activePattern = '/phpfpm_process_state\{pool="unconfined",child="' . $child . '",state="[^"]+"\} 1/'; + preg_match_all($activePattern, $body, $matches); + if (count($matches[0]) !== 1) { + echo "ERROR: Expected exactly 1 active state for child $child, got " . count($matches[0]) . "\n"; + $ok = false; + } +} + +/* Per-worker numeric metrics */ +$workerMetrics = [ + 'phpfpm_process_requests' => 'counter', + 'phpfpm_process_request_duration' => 'gauge', + 'phpfpm_process_last_request_cpu' => 'gauge', + 'phpfpm_process_last_request_memory' => 'gauge', +]; + +foreach ($workerMetrics as $metric => $type) { + if (!preg_match('/^# HELP ' . $metric . ' /m', $body)) { + echo "ERROR: Missing $metric HELP\n"; + $ok = false; + } + if (!preg_match('/^# TYPE ' . $metric . ' ' . $type . '$/m', $body)) { + echo "ERROR: Missing $metric TYPE\n"; + $ok = false; + } + for ($child = 0; $child < 2; $child++) { + $pattern = '/' . $metric . '\{pool="unconfined",child="' . $child . '"\} /'; + if (!preg_match($pattern, $body)) { + echo "ERROR: Missing $metric for child $child\n"; + $ok = false; + } + } +} + +/* Must end with # EOF */ +if (!preg_match('/^# EOF$/m', $body)) { + echo "ERROR: Missing # EOF\n"; + $ok = false; +} + +if ($ok) { + echo "Done\n"; +} + +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +?> +--EXPECT-- +Done +--CLEAN-- + From 5b6cdb919fc3475be31c9cd58ae5c9c8701fd424 Mon Sep 17 00:00:00 2001 From: Marcel Hernandez Date: Mon, 21 Sep 2026 23:45:43 +0200 Subject: [PATCH 8/8] fpm: fix leaks and one use-after-free in sad paths of fpm_status_handle_request --- sapi/fpm/fpm/fpm_status.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sapi/fpm/fpm/fpm_status.c b/sapi/fpm/fpm/fpm_status.c index 8bd4faa256fe..6ed342cfbdbc 100644 --- a/sapi/fpm/fpm/fpm_status.c +++ b/sapi/fpm/fpm/fpm_status.c @@ -914,17 +914,19 @@ int fpm_status_handle_request(void) /* {{{ */ sapi_add_header_ex(ZEND_STRL("Expires: Thu, 01 Jan 1970 00:00:00 GMT"), 1, 1); sapi_add_header_ex(ZEND_STRL("Cache-Control: no-cache, no-store, must-revalidate, max-age=0"), 1, 1); PUTS("Internal error. Please review log file for errors."); + zend_string_release_ex(_GET_str, 0); return 1; } if (scoreboard_p->idle < 0 || scoreboard_p->active < 0) { - fpm_scoreboard_free_copy(scoreboard_p); zlog(ZLOG_ERROR, "[pool %s] invalid status values", scoreboard_p->pool); SG(sapi_headers).http_response_code = 500; sapi_add_header_ex(ZEND_STRL("Content-Type: text/plain"), 1, 1); sapi_add_header_ex(ZEND_STRL("Expires: Thu, 01 Jan 1970 00:00:00 GMT"), 1, 1); sapi_add_header_ex(ZEND_STRL("Cache-Control: no-cache, no-store, must-revalidate, max-age=0"), 1, 1); PUTS("Internal error. Please review log file for errors."); + zend_string_release_ex(_GET_str, 0); + fpm_scoreboard_free_copy(scoreboard_p); return 1; } @@ -935,6 +937,7 @@ int fpm_status_handle_request(void) /* {{{ */ /* handle HEAD */ if (SG(request_info).headers_only) { + zend_string_release_ex(_GET_str, 0); fpm_scoreboard_free_copy(scoreboard_p); return 1; }