Skip to content

Commit 5ebf119

Browse files
feat: add more metrics to the Prometheus endpoint
1 parent c12841b commit 5ebf119

File tree

8 files changed

+148
-11
lines changed

8 files changed

+148
-11
lines changed

caddy/admin_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,46 @@ func TestShowTheCorrectThreadDebugStatus(t *testing.T) {
8282
assert.Len(t, debugState.ThreadDebugStates, 3)
8383
}
8484

85+
func TestThreadDebugStateMetricsAfterRequests(t *testing.T) {
86+
tester := caddytest.NewTester(t)
87+
tester.InitServer(`
88+
{
89+
skip_install_trust
90+
admin localhost:2999
91+
http_port `+testPort+`
92+
93+
frankenphp {
94+
num_threads 2
95+
worker ../testdata/worker-with-counter.php 1
96+
}
97+
}
98+
99+
localhost:`+testPort+` {
100+
route {
101+
root ../testdata
102+
rewrite worker-with-counter.php
103+
php
104+
}
105+
}
106+
`, "caddyfile")
107+
108+
// make a few requests so counters are populated
109+
tester.AssertGetResponse("http://localhost:"+testPort+"/", http.StatusOK, "requests:1")
110+
tester.AssertGetResponse("http://localhost:"+testPort+"/", http.StatusOK, "requests:2")
111+
tester.AssertGetResponse("http://localhost:"+testPort+"/", http.StatusOK, "requests:3")
112+
113+
debugState := getDebugState(t, tester)
114+
115+
hasRequestCount := false
116+
for _, ts := range debugState.ThreadDebugStates {
117+
if ts.RequestCount > 0 {
118+
hasRequestCount = true
119+
assert.Greater(t, ts.MemoryUsage, int64(0), "thread %d (%s) should report memory usage", ts.Index, ts.Name)
120+
}
121+
}
122+
assert.True(t, hasRequestCount, "at least one thread should have RequestCount > 0 after serving requests")
123+
}
124+
85125
func TestAutoScaleWorkerThreads(t *testing.T) {
86126
wg := sync.WaitGroup{}
87127
maxTries := 10

debugstate.go

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package frankenphp
22

3+
// #include "frankenphp.h"
4+
import "C"
35
import (
46
"github.com/dunglas/frankenphp/internal/state"
57
)
@@ -12,6 +14,11 @@ type ThreadDebugState struct {
1214
IsWaiting bool
1315
IsBusy bool
1416
WaitingSinceMilliseconds int64
17+
CurrentURI string
18+
CurrentMethod string
19+
RequestStartedAt int64
20+
RequestCount int64
21+
MemoryUsage int64
1522
}
1623

1724
// EXPERIMENTAL: FrankenPHPDebugState prints the state of all PHP threads - debugging purposes only
@@ -39,12 +46,51 @@ func DebugState() FrankenPHPDebugState {
3946

4047
// threadDebugState creates a small jsonable status message for debugging purposes
4148
func threadDebugState(thread *phpThread) ThreadDebugState {
42-
return ThreadDebugState{
49+
isBusy := !thread.state.IsInWaitingState()
50+
51+
s := ThreadDebugState{
4352
Index: thread.threadIndex,
4453
Name: thread.name(),
4554
State: thread.state.Name(),
4655
IsWaiting: thread.state.IsInWaitingState(),
47-
IsBusy: !thread.state.IsInWaitingState(),
56+
IsBusy: isBusy,
4857
WaitingSinceMilliseconds: thread.state.WaitTime(),
4958
}
59+
60+
s.RequestCount = thread.requestCount.Load()
61+
s.MemoryUsage = int64(C.frankenphp_get_thread_memory_usage(C.uintptr_t(thread.threadIndex)))
62+
63+
if !isBusy {
64+
return s
65+
}
66+
67+
thread.handlerMu.RLock()
68+
handler := thread.handler
69+
thread.handlerMu.RUnlock()
70+
71+
if handler == nil {
72+
return s
73+
}
74+
75+
thread.contextMu.RLock()
76+
defer thread.contextMu.RUnlock()
77+
78+
fc := handler.frankenPHPContext()
79+
if fc == nil || fc.request == nil || fc.responseWriter == nil {
80+
return s
81+
}
82+
83+
if fc.originalRequest == nil {
84+
s.CurrentURI = fc.requestURI
85+
s.CurrentMethod = fc.request.Method
86+
} else {
87+
s.CurrentURI = fc.originalRequest.URL.RequestURI()
88+
s.CurrentMethod = fc.originalRequest.Method
89+
}
90+
91+
if !fc.startedAt.IsZero() {
92+
s.RequestStartedAt = fc.startedAt.UnixMilli()
93+
}
94+
95+
return s
5096
}

frankenphp.c

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,13 @@ static void frankenphp_reset_session_state(void) {
244244
}
245245
#endif
246246

247+
static frankenphp_thread_metrics *thread_metrics = NULL;
248+
247249
/* Adapted from php_request_shutdown */
248250
static void frankenphp_worker_request_shutdown() {
251+
__atomic_store_n(&thread_metrics[thread_index].last_memory_usage,
252+
zend_memory_usage(0), __ATOMIC_RELAXED);
253+
249254
/* Flush all output buffers */
250255
zend_try { php_output_end_all(); }
251256
zend_end_try();
@@ -1233,6 +1238,8 @@ int frankenphp_execute_script(char *file_name) {
12331238
sandboxed_env = NULL;
12341239
}
12351240

1241+
__atomic_store_n(&thread_metrics[thread_index].last_memory_usage,
1242+
zend_memory_usage(0), __ATOMIC_RELAXED);
12361243
php_request_shutdown((void *)0);
12371244
frankenphp_free_request_context();
12381245

@@ -1405,6 +1412,20 @@ int frankenphp_reset_opcache(void) {
14051412

14061413
int frankenphp_get_current_memory_limit() { return PG(memory_limit); }
14071414

1415+
void frankenphp_init_thread_metrics(int max_threads) {
1416+
thread_metrics = calloc(max_threads, sizeof(frankenphp_thread_metrics));
1417+
}
1418+
1419+
void frankenphp_destroy_thread_metrics(void) {
1420+
free(thread_metrics);
1421+
thread_metrics = NULL;
1422+
}
1423+
1424+
size_t frankenphp_get_thread_memory_usage(uintptr_t idx) {
1425+
return __atomic_load_n(&thread_metrics[idx].last_memory_usage,
1426+
__ATOMIC_RELAXED);
1427+
}
1428+
14081429
static zend_module_entry **modules = NULL;
14091430
static int modules_len = 0;
14101431
static int (*original_php_register_internal_extensions_func)(void) = NULL;

frankenphp.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,14 @@ zend_string *frankenphp_init_persistent_string(const char *string, size_t len);
186186
int frankenphp_reset_opcache(void);
187187
int frankenphp_get_current_memory_limit();
188188

189+
typedef struct {
190+
size_t last_memory_usage;
191+
} frankenphp_thread_metrics;
192+
193+
void frankenphp_init_thread_metrics(int max_threads);
194+
void frankenphp_destroy_thread_metrics(void);
195+
size_t frankenphp_get_thread_memory_usage(uintptr_t thread_index);
196+
189197
void register_extensions(zend_module_entry **m, int len);
190198

191199
#endif

phpmainthread.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ func initPHPThreads(numThreads int, numMaxThreads int, phpIni map[string]string)
5454
return nil, err
5555
}
5656

57+
C.frankenphp_init_thread_metrics(C.int(mainThread.maxThreads))
58+
5759
// initialize all other threads
5860
phpThreads = make([]*phpThread, mainThread.maxThreads)
5961
phpThreads[0] = initialThread
@@ -97,6 +99,7 @@ func drainPHPThreads() {
9799
doneWG.Wait()
98100
mainThread.state.Set(state.Done)
99101
mainThread.state.WaitFor(state.Reserved)
102+
C.frankenphp_destroy_thread_metrics()
100103
phpThreads = nil
101104
}
102105

phpthread.go

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"runtime"
99
"sync"
10+
"sync/atomic"
1011
"unsafe"
1112

1213
"github.com/dunglas/frankenphp/internal/state"
@@ -16,12 +17,14 @@ import (
1617
// identified by the index in the phpThreads slice
1718
type phpThread struct {
1819
runtime.Pinner
19-
threadIndex int
20-
requestChan chan contextHolder
21-
drainChan chan struct{}
22-
handlerMu sync.RWMutex
23-
handler threadHandler
24-
state *state.ThreadState
20+
threadIndex int
21+
requestChan chan contextHolder
22+
drainChan chan struct{}
23+
handlerMu sync.RWMutex
24+
handler threadHandler
25+
contextMu sync.RWMutex
26+
state *state.ThreadState
27+
requestCount atomic.Int64
2528
}
2629

2730
// threadHandler defines how the callbacks from the C thread should be handled
@@ -125,10 +128,13 @@ func (thread *phpThread) context() context.Context {
125128

126129
func (thread *phpThread) name() string {
127130
thread.handlerMu.RLock()
128-
name := thread.handler.name()
129-
thread.handlerMu.RUnlock()
131+
defer thread.handlerMu.RUnlock()
130132

131-
return name
133+
if thread.handler == nil {
134+
return "unknown"
135+
}
136+
137+
return thread.handler.name()
132138
}
133139

134140
// Pin a string that is not null-terminated

threadregular.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ func (handler *regularThread) beforeScriptExecution() string {
6060
}
6161

6262
func (handler *regularThread) afterScriptExecution(_ int) {
63+
handler.thread.requestCount.Add(1)
6364
handler.afterRequest()
6465
}
6566

@@ -88,8 +89,10 @@ func (handler *regularThread) waitForRequest() string {
8889
case ch = <-handler.thread.requestChan:
8990
}
9091

92+
handler.thread.contextMu.Lock()
9193
handler.ctx = ch.ctx
9294
handler.contextHolder.frankenPHPContext = ch.frankenPHPContext
95+
handler.thread.contextMu.Unlock()
9396
handler.state.MarkAsWaiting(false)
9497

9598
// set the scriptFilename that should be executed
@@ -98,8 +101,10 @@ func (handler *regularThread) waitForRequest() string {
98101

99102
func (handler *regularThread) afterRequest() {
100103
handler.contextHolder.frankenPHPContext.closeContext()
104+
handler.thread.contextMu.Lock()
101105
handler.contextHolder.frankenPHPContext = nil
102106
handler.ctx = nil
107+
handler.thread.contextMu.Unlock()
103108
}
104109

105110
func handleRequestWithRegularPHPThreads(ch contextHolder) error {

threadworker.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,10 @@ func tearDownWorkerScript(handler *workerThread, exitStatus int) {
131131
// make sure to close the worker request context
132132
if handler.workerFrankenPHPContext != nil {
133133
handler.workerFrankenPHPContext.closeContext()
134+
handler.thread.contextMu.Lock()
134135
handler.workerFrankenPHPContext = nil
135136
handler.workerContext = nil
137+
handler.thread.contextMu.Unlock()
136138
}
137139

138140
// on exit status 0 we just run the worker script again
@@ -235,8 +237,10 @@ func (handler *workerThread) waitForWorkerRequest() (bool, any) {
235237
case requestCH = <-handler.worker.requestChan:
236238
}
237239

240+
handler.thread.contextMu.Lock()
238241
handler.workerContext = requestCH.ctx
239242
handler.workerFrankenPHPContext = requestCH.frankenPHPContext
243+
handler.thread.contextMu.Unlock()
240244
handler.state.MarkAsWaiting(false)
241245

242246
if globalLogger.Enabled(requestCH.ctx, slog.LevelDebug) {
@@ -292,9 +296,13 @@ func go_frankenphp_finish_worker_request(threadIndex C.uintptr_t, retval *C.zval
292296
fc.handlerReturn = r
293297
}
294298

299+
thread.requestCount.Add(1)
300+
295301
fc.closeContext()
302+
thread.contextMu.Lock()
296303
thread.handler.(*workerThread).workerFrankenPHPContext = nil
297304
thread.handler.(*workerThread).workerContext = nil
305+
thread.contextMu.Unlock()
298306

299307
if globalLogger.Enabled(ctx, slog.LevelDebug) {
300308
if fc.request == nil {

0 commit comments

Comments
 (0)