feat: add more metrics to the Prometheus endpoint

This commit is contained in:
Alexandre Daubois
2026-03-26 11:37:24 +01:00
committed by GitHub
parent c12841bf89
commit 5ebf119b2b
8 changed files with 148 additions and 11 deletions
+40
View File
@@ -82,6 +82,46 @@ func TestShowTheCorrectThreadDebugStatus(t *testing.T) {
assert.Len(t, debugState.ThreadDebugStates, 3)
}
func TestThreadDebugStateMetricsAfterRequests(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port `+testPort+`
frankenphp {
num_threads 2
worker ../testdata/worker-with-counter.php 1
}
}
localhost:`+testPort+` {
route {
root ../testdata
rewrite worker-with-counter.php
php
}
}
`, "caddyfile")
// make a few requests so counters are populated
tester.AssertGetResponse("http://localhost:"+testPort+"/", http.StatusOK, "requests:1")
tester.AssertGetResponse("http://localhost:"+testPort+"/", http.StatusOK, "requests:2")
tester.AssertGetResponse("http://localhost:"+testPort+"/", http.StatusOK, "requests:3")
debugState := getDebugState(t, tester)
hasRequestCount := false
for _, ts := range debugState.ThreadDebugStates {
if ts.RequestCount > 0 {
hasRequestCount = true
assert.Greater(t, ts.MemoryUsage, int64(0), "thread %d (%s) should report memory usage", ts.Index, ts.Name)
}
}
assert.True(t, hasRequestCount, "at least one thread should have RequestCount > 0 after serving requests")
}
func TestAutoScaleWorkerThreads(t *testing.T) {
wg := sync.WaitGroup{}
maxTries := 10
+48 -2
View File
@@ -1,5 +1,7 @@
package frankenphp
// #include "frankenphp.h"
import "C"
import (
"github.com/dunglas/frankenphp/internal/state"
)
@@ -12,6 +14,11 @@ type ThreadDebugState struct {
IsWaiting bool
IsBusy bool
WaitingSinceMilliseconds int64
CurrentURI string
CurrentMethod string
RequestStartedAt int64
RequestCount int64
MemoryUsage int64
}
// EXPERIMENTAL: FrankenPHPDebugState prints the state of all PHP threads - debugging purposes only
@@ -39,12 +46,51 @@ func DebugState() FrankenPHPDebugState {
// threadDebugState creates a small jsonable status message for debugging purposes
func threadDebugState(thread *phpThread) ThreadDebugState {
return ThreadDebugState{
isBusy := !thread.state.IsInWaitingState()
s := ThreadDebugState{
Index: thread.threadIndex,
Name: thread.name(),
State: thread.state.Name(),
IsWaiting: thread.state.IsInWaitingState(),
IsBusy: !thread.state.IsInWaitingState(),
IsBusy: isBusy,
WaitingSinceMilliseconds: thread.state.WaitTime(),
}
s.RequestCount = thread.requestCount.Load()
s.MemoryUsage = int64(C.frankenphp_get_thread_memory_usage(C.uintptr_t(thread.threadIndex)))
if !isBusy {
return s
}
thread.handlerMu.RLock()
handler := thread.handler
thread.handlerMu.RUnlock()
if handler == nil {
return s
}
thread.contextMu.RLock()
defer thread.contextMu.RUnlock()
fc := handler.frankenPHPContext()
if fc == nil || fc.request == nil || fc.responseWriter == nil {
return s
}
if fc.originalRequest == nil {
s.CurrentURI = fc.requestURI
s.CurrentMethod = fc.request.Method
} else {
s.CurrentURI = fc.originalRequest.URL.RequestURI()
s.CurrentMethod = fc.originalRequest.Method
}
if !fc.startedAt.IsZero() {
s.RequestStartedAt = fc.startedAt.UnixMilli()
}
return s
}
+21
View File
@@ -244,8 +244,13 @@ static void frankenphp_reset_session_state(void) {
}
#endif
static frankenphp_thread_metrics *thread_metrics = NULL;
/* Adapted from php_request_shutdown */
static void frankenphp_worker_request_shutdown() {
__atomic_store_n(&thread_metrics[thread_index].last_memory_usage,
zend_memory_usage(0), __ATOMIC_RELAXED);
/* Flush all output buffers */
zend_try { php_output_end_all(); }
zend_end_try();
@@ -1233,6 +1238,8 @@ int frankenphp_execute_script(char *file_name) {
sandboxed_env = NULL;
}
__atomic_store_n(&thread_metrics[thread_index].last_memory_usage,
zend_memory_usage(0), __ATOMIC_RELAXED);
php_request_shutdown((void *)0);
frankenphp_free_request_context();
@@ -1405,6 +1412,20 @@ int frankenphp_reset_opcache(void) {
int frankenphp_get_current_memory_limit() { return PG(memory_limit); }
void frankenphp_init_thread_metrics(int max_threads) {
thread_metrics = calloc(max_threads, sizeof(frankenphp_thread_metrics));
}
void frankenphp_destroy_thread_metrics(void) {
free(thread_metrics);
thread_metrics = NULL;
}
size_t frankenphp_get_thread_memory_usage(uintptr_t idx) {
return __atomic_load_n(&thread_metrics[idx].last_memory_usage,
__ATOMIC_RELAXED);
}
static zend_module_entry **modules = NULL;
static int modules_len = 0;
static int (*original_php_register_internal_extensions_func)(void) = NULL;
+8
View File
@@ -186,6 +186,14 @@ zend_string *frankenphp_init_persistent_string(const char *string, size_t len);
int frankenphp_reset_opcache(void);
int frankenphp_get_current_memory_limit();
typedef struct {
size_t last_memory_usage;
} frankenphp_thread_metrics;
void frankenphp_init_thread_metrics(int max_threads);
void frankenphp_destroy_thread_metrics(void);
size_t frankenphp_get_thread_memory_usage(uintptr_t thread_index);
void register_extensions(zend_module_entry **m, int len);
#endif
+3
View File
@@ -54,6 +54,8 @@ func initPHPThreads(numThreads int, numMaxThreads int, phpIni map[string]string)
return nil, err
}
C.frankenphp_init_thread_metrics(C.int(mainThread.maxThreads))
// initialize all other threads
phpThreads = make([]*phpThread, mainThread.maxThreads)
phpThreads[0] = initialThread
@@ -97,6 +99,7 @@ func drainPHPThreads() {
doneWG.Wait()
mainThread.state.Set(state.Done)
mainThread.state.WaitFor(state.Reserved)
C.frankenphp_destroy_thread_metrics()
phpThreads = nil
}
+15 -9
View File
@@ -7,6 +7,7 @@ import (
"context"
"runtime"
"sync"
"sync/atomic"
"unsafe"
"github.com/dunglas/frankenphp/internal/state"
@@ -16,12 +17,14 @@ import (
// identified by the index in the phpThreads slice
type phpThread struct {
runtime.Pinner
threadIndex int
requestChan chan contextHolder
drainChan chan struct{}
handlerMu sync.RWMutex
handler threadHandler
state *state.ThreadState
threadIndex int
requestChan chan contextHolder
drainChan chan struct{}
handlerMu sync.RWMutex
handler threadHandler
contextMu sync.RWMutex
state *state.ThreadState
requestCount atomic.Int64
}
// threadHandler defines how the callbacks from the C thread should be handled
@@ -125,10 +128,13 @@ func (thread *phpThread) context() context.Context {
func (thread *phpThread) name() string {
thread.handlerMu.RLock()
name := thread.handler.name()
thread.handlerMu.RUnlock()
defer thread.handlerMu.RUnlock()
return name
if thread.handler == nil {
return "unknown"
}
return thread.handler.name()
}
// Pin a string that is not null-terminated
+5
View File
@@ -60,6 +60,7 @@ func (handler *regularThread) beforeScriptExecution() string {
}
func (handler *regularThread) afterScriptExecution(_ int) {
handler.thread.requestCount.Add(1)
handler.afterRequest()
}
@@ -88,8 +89,10 @@ func (handler *regularThread) waitForRequest() string {
case ch = <-handler.thread.requestChan:
}
handler.thread.contextMu.Lock()
handler.ctx = ch.ctx
handler.contextHolder.frankenPHPContext = ch.frankenPHPContext
handler.thread.contextMu.Unlock()
handler.state.MarkAsWaiting(false)
// set the scriptFilename that should be executed
@@ -98,8 +101,10 @@ func (handler *regularThread) waitForRequest() string {
func (handler *regularThread) afterRequest() {
handler.contextHolder.frankenPHPContext.closeContext()
handler.thread.contextMu.Lock()
handler.contextHolder.frankenPHPContext = nil
handler.ctx = nil
handler.thread.contextMu.Unlock()
}
func handleRequestWithRegularPHPThreads(ch contextHolder) error {
+8
View File
@@ -131,8 +131,10 @@ func tearDownWorkerScript(handler *workerThread, exitStatus int) {
// make sure to close the worker request context
if handler.workerFrankenPHPContext != nil {
handler.workerFrankenPHPContext.closeContext()
handler.thread.contextMu.Lock()
handler.workerFrankenPHPContext = nil
handler.workerContext = nil
handler.thread.contextMu.Unlock()
}
// on exit status 0 we just run the worker script again
@@ -235,8 +237,10 @@ func (handler *workerThread) waitForWorkerRequest() (bool, any) {
case requestCH = <-handler.worker.requestChan:
}
handler.thread.contextMu.Lock()
handler.workerContext = requestCH.ctx
handler.workerFrankenPHPContext = requestCH.frankenPHPContext
handler.thread.contextMu.Unlock()
handler.state.MarkAsWaiting(false)
if globalLogger.Enabled(requestCH.ctx, slog.LevelDebug) {
@@ -292,9 +296,13 @@ func go_frankenphp_finish_worker_request(threadIndex C.uintptr_t, retval *C.zval
fc.handlerReturn = r
}
thread.requestCount.Add(1)
fc.closeContext()
thread.contextMu.Lock()
thread.handler.(*workerThread).workerFrankenPHPContext = nil
thread.handler.(*workerThread).workerContext = nil
thread.contextMu.Unlock()
if globalLogger.Enabled(ctx, slog.LevelDebug) {
if fc.request == nil {