Skip to content

Commit c2efd1b

Browse files
feat: metrics for the tasks of background workers
The existing worker metrics apply to background workers as they are: busy_workers counts a thread holding a task, from pickup to the close of the task's stream, and worker_queue_depth counts the tasks waiting for a thread, which is the only queue a background worker has. Two new ones break tasks down: worker_task_count{worker,outcome} with completed, aborted (the script ended with the task open), abandoned (the sender closed its stream first) or timeout (no thread picked the task up in time), settled by whichever side closes first so every task counts once, and worker_task_time, the seconds spent on tasks from pickup to close. The threads endpoint follows: a background thread is busy while it holds a task and waiting otherwise, counted per thread since a script may hold several.
1 parent 50e4418 commit c2efd1b

7 files changed

Lines changed: 248 additions & 13 deletions

File tree

caddy/caddy_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -866,7 +866,7 @@ func TestWorkerMetrics(t *testing.T) {
866866
# TYPE frankenphp_busy_threads gauge
867867
frankenphp_busy_threads 2
868868
869-
# HELP frankenphp_busy_workers Number of busy PHP workers for this worker
869+
# HELP frankenphp_busy_workers Number of busy PHP workers for this worker: processing a request, or a task for a background worker
870870
# TYPE frankenphp_busy_workers gauge
871871
frankenphp_busy_workers{worker="` + workerName + `"} 0
872872
@@ -1023,7 +1023,7 @@ func TestNamedWorkerMetrics(t *testing.T) {
10231023
# TYPE frankenphp_busy_threads gauge
10241024
frankenphp_busy_threads 2
10251025
1026-
# HELP frankenphp_busy_workers Number of busy PHP workers for this worker
1026+
# HELP frankenphp_busy_workers Number of busy PHP workers for this worker: processing a request, or a task for a background worker
10271027
# TYPE frankenphp_busy_workers gauge
10281028
frankenphp_busy_workers{worker="my_app"} 0
10291029
@@ -1119,7 +1119,7 @@ func TestAutoWorkerConfig(t *testing.T) {
11191119
# TYPE frankenphp_busy_threads gauge
11201120
frankenphp_busy_threads ` + workers + `
11211121
1122-
# HELP frankenphp_busy_workers Number of busy PHP workers for this worker
1122+
# HELP frankenphp_busy_workers Number of busy PHP workers for this worker: processing a request, or a task for a background worker
11231123
# TYPE frankenphp_busy_workers gauge
11241124
frankenphp_busy_workers{worker="` + workerName + `"} 0
11251125
@@ -1386,6 +1386,7 @@ func TestMaxWaitTimeWorker(t *testing.T) {
13861386
require.NoError(t, err)
13871387

13881388
expectedMetrics := `
1389+
# HELP frankenphp_worker_queue_depth Number of queued requests for this worker, or of tasks waiting for a thread of a background worker
13891390
# TYPE frankenphp_worker_queue_depth gauge
13901391
frankenphp_worker_queue_depth{worker="service"} 0
13911392
`
@@ -1486,7 +1487,7 @@ func TestMultiWorkersMetrics(t *testing.T) {
14861487
# TYPE frankenphp_busy_threads gauge
14871488
frankenphp_busy_threads 5
14881489
1489-
# HELP frankenphp_busy_workers Number of busy PHP workers for this worker
1490+
# HELP frankenphp_busy_workers Number of busy PHP workers for this worker: processing a request, or a task for a background worker
14901491
# TYPE frankenphp_busy_workers gauge
14911492
frankenphp_busy_workers{worker="service1"} 0
14921493

docs/metrics.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ When [Caddy metrics](https://caddyserver.com/docs/metrics) are enabled, FrankenP
1616
- `frankenphp_busy_threads`: The number of PHP threads currently processing a request (running workers always consume a thread).
1717
- `frankenphp_queue_depth`: The number of regular queued requests.
1818
- `frankenphp_total_workers{worker="[worker_name]"}`: The total number of workers.
19-
- `frankenphp_busy_workers{worker="[worker_name]"}`: The number of workers currently processing a request.
19+
- `frankenphp_busy_workers{worker="[worker_name]"}`: The number of workers currently processing a request, or a task for a background worker.
2020
- `frankenphp_worker_request_time{worker="[worker_name]"}`: The time spent processing requests by all workers.
2121
- `frankenphp_worker_request_count{worker="[worker_name]"}`: The number of requests processed by all workers.
2222
- `frankenphp_ready_workers{worker="[worker_name]"}`: The number of workers that have reached their ready point at least once: `frankenphp_handle_request()` for HTTP workers, `frankenphp_get_worker_handle()` for background workers.
2323
- `frankenphp_worker_crashes{worker="[worker_name]"}`: The number of times a worker has unexpectedly terminated.
2424
- `frankenphp_worker_restarts{worker="[worker_name]"}`: The number of times a worker has been deliberately restarted.
25-
- `frankenphp_worker_queue_depth{worker="[worker_name]"}`: The number of queued requests.
25+
- `frankenphp_worker_queue_depth{worker="[worker_name]"}`: The number of queued requests, or of tasks waiting for a thread of a background worker.
26+
- `frankenphp_worker_task_count{worker="[worker_name]",outcome="[outcome]"}`: The number of tasks sent to a background worker, by outcome: `completed`, `aborted` (the script ended with the task open), `abandoned` (the sender closed its stream first) or `timeout` (no thread picked the task up in time).
27+
- `frankenphp_worker_task_time{worker="[worker_name]"}`: The time spent on tasks by all threads of a background worker, from pickup to the close of the task's stream.
2628

2729
`[worker_name]` is the worker name from the Caddyfile, or the absolute path of the worker file when it has none. Workers of a `php_server` block are prefixed with the name of that block: `<server name>:<worker name>`. They used to be reported under their bare name unless two blocks declared the same one, so dashboards and alerts built on those series need the prefix.
2830

@@ -73,8 +75,8 @@ Each entry in `ThreadDebugStates` contains:
7375
| `Index` | integer | The index of the thread. |
7476
| `Name` | string | The name of the thread (e.g., the worker file path). |
7577
| `State` | string | The internal state of the thread (e.g., `ready`, `shutting down`). |
76-
| `IsWaiting` | boolean | Whether the thread is waiting for a request. |
77-
| `IsBusy` | boolean | Whether the thread is currently processing a request. |
78+
| `IsWaiting` | boolean | Whether the thread is waiting for a request, or for a task in a background worker. |
79+
| `IsBusy` | boolean | Whether the thread is currently processing a request, or a task in a background worker. |
7880
| `WaitingSinceMilliseconds` | integer | How long the thread has been idle, in milliseconds. `0` if the thread is busy. |
7981
| `CurrentURI` | string | The URI currently being processed. Empty if the thread is idle. |
8082
| `CurrentMethod` | string | The HTTP method of the current request (e.g., `GET`, `POST`). Empty if the thread is idle. |

metrics.go

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@ const (
1616

1717
type StopReason int
1818

19+
// TaskOutcome is how a task sent to a background worker ended
20+
type TaskOutcome string
21+
22+
const (
23+
TaskOutcomeCompleted TaskOutcome = "completed" // the worker closed the task's stream
24+
TaskOutcomeAborted TaskOutcome = "aborted" // the worker's script ended with the task open
25+
TaskOutcomeAbandoned TaskOutcome = "abandoned" // the sender closed its stream first
26+
TaskOutcomeTimeout TaskOutcome = "timeout" // no thread picked the task up in time
27+
)
28+
1929
type Metrics interface {
2030
// StartWorker collects started workers
2131
StartWorker(name string)
@@ -40,6 +50,12 @@ type Metrics interface {
4050
DequeuedWorkerRequest(name string)
4151
QueuedRequest()
4252
DequeuedRequest()
53+
// StartWorkerTask collects tasks picked up by a thread of a background worker
54+
StartWorkerTask(name string)
55+
// StopWorkerTask collects tasks a thread of a background worker is done with
56+
StopWorkerTask(name string, duration time.Duration)
57+
// WorkerTaskOutcome collects how tasks sent to a background worker ended
58+
WorkerTaskOutcome(name string, outcome TaskOutcome)
4359
}
4460

4561
type nullMetrics struct{}
@@ -81,6 +97,12 @@ func (n nullMetrics) DequeuedWorkerRequest(string) {}
8197
func (n nullMetrics) QueuedRequest() {}
8298
func (n nullMetrics) DequeuedRequest() {}
8399

100+
func (n nullMetrics) StartWorkerTask(string) {}
101+
102+
func (n nullMetrics) StopWorkerTask(string, time.Duration) {}
103+
104+
func (n nullMetrics) WorkerTaskOutcome(string, TaskOutcome) {}
105+
84106
type PrometheusMetrics struct {
85107
registry prometheus.Registerer
86108
totalThreads prometheus.Gauge
@@ -93,6 +115,8 @@ type PrometheusMetrics struct {
93115
workerRequestTime *prometheus.CounterVec
94116
workerRequestCount *prometheus.CounterVec
95117
workerQueueDepth *prometheus.GaugeVec
118+
workerTaskCount *prometheus.CounterVec
119+
workerTaskTime *prometheus.CounterVec
96120
queueDepth prometheus.Gauge
97121
mu sync.RWMutex
98122
}
@@ -186,7 +210,7 @@ func (m *PrometheusMetrics) TotalWorkers(string, int) {
186210
m.busyWorkers = prometheus.NewGaugeVec(prometheus.GaugeOpts{
187211
Namespace: ns,
188212
Name: "busy_workers",
189-
Help: "Number of busy PHP workers for this worker",
213+
Help: "Number of busy PHP workers for this worker: processing a request, or a task for a background worker",
190214
}, basicLabels)
191215
m.mustRegister(m.busyWorkers)
192216
}
@@ -234,9 +258,30 @@ func (m *PrometheusMetrics) TotalWorkers(string, int) {
234258
Namespace: "frankenphp",
235259
Subsystem: sub,
236260
Name: "queue_depth",
261+
Help: "Number of queued requests for this worker, or of tasks waiting for a thread of a background worker",
237262
}, basicLabels)
238263
m.mustRegister(m.workerQueueDepth)
239264
}
265+
266+
if m.workerTaskCount == nil {
267+
m.workerTaskCount = prometheus.NewCounterVec(prometheus.CounterOpts{
268+
Namespace: ns,
269+
Subsystem: sub,
270+
Name: "task_count",
271+
Help: "Number of tasks sent to this background worker, by outcome: completed, aborted (the script ended with the task open), abandoned (the sender closed its stream first) or timeout (no thread picked the task up in time)",
272+
}, []string{"worker", "outcome"})
273+
m.mustRegister(m.workerTaskCount)
274+
}
275+
276+
if m.workerTaskTime == nil {
277+
m.workerTaskTime = prometheus.NewCounterVec(prometheus.CounterOpts{
278+
Namespace: ns,
279+
Subsystem: sub,
280+
Name: "task_time",
281+
Help: "Time spent on tasks by all threads of this background worker, from pickup to the close of the task's stream",
282+
}, basicLabels)
283+
m.mustRegister(m.workerTaskTime)
284+
}
240285
}
241286

242287
func (m *PrometheusMetrics) TotalThreads(num int) {
@@ -317,6 +362,37 @@ func (m *PrometheusMetrics) DequeuedRequest() {
317362
m.queueDepth.Dec()
318363
}
319364

365+
func (m *PrometheusMetrics) StartWorkerTask(name string) {
366+
m.mu.RLock()
367+
defer m.mu.RUnlock()
368+
369+
if m.busyWorkers == nil {
370+
return
371+
}
372+
m.busyWorkers.WithLabelValues(name).Inc()
373+
}
374+
375+
func (m *PrometheusMetrics) StopWorkerTask(name string, duration time.Duration) {
376+
m.mu.RLock()
377+
defer m.mu.RUnlock()
378+
379+
if m.workerTaskTime == nil {
380+
return
381+
}
382+
m.busyWorkers.WithLabelValues(name).Dec()
383+
m.workerTaskTime.WithLabelValues(name).Add(duration.Seconds())
384+
}
385+
386+
func (m *PrometheusMetrics) WorkerTaskOutcome(name string, outcome TaskOutcome) {
387+
m.mu.RLock()
388+
defer m.mu.RUnlock()
389+
390+
if m.workerTaskCount == nil {
391+
return
392+
}
393+
m.workerTaskCount.WithLabelValues(name, string(outcome)).Inc()
394+
}
395+
320396
func (m *PrometheusMetrics) Shutdown() {
321397
m.mu.Lock()
322398
defer m.mu.Unlock()
@@ -356,6 +432,14 @@ func (m *PrometheusMetrics) Shutdown() {
356432
if m.workerQueueDepth != nil {
357433
m.registry.Unregister(m.workerQueueDepth)
358434
}
435+
436+
if m.workerTaskCount != nil {
437+
m.registry.Unregister(m.workerTaskCount)
438+
}
439+
440+
if m.workerTaskTime != nil {
441+
m.registry.Unregister(m.workerTaskTime)
442+
}
359443
}
360444

361445
func NewPrometheusMetrics(registry prometheus.Registerer) *PrometheusMetrics {
@@ -385,6 +469,8 @@ func NewPrometheusMetrics(registry prometheus.Registerer) *PrometheusMetrics {
385469
workerCrashes: nil,
386470
readyWorkers: nil,
387471
workerQueueDepth: nil,
472+
workerTaskCount: nil,
473+
workerTaskTime: nil,
388474
}
389475

390476
m.mustRegister(m.totalThreads)

metrics_test.go

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ func TestPrometheusMetrics_TotalWorkers(t *testing.T) {
5050
require.Nil(t, m.workerRestarts)
5151
require.Nil(t, m.workerRequestTime)
5252
require.Nil(t, m.workerRequestCount)
53+
require.Nil(t, m.workerTaskCount)
54+
require.Nil(t, m.workerTaskTime)
5355

5456
m.TotalWorkers("test_worker", 2)
5557

@@ -60,6 +62,65 @@ func TestPrometheusMetrics_TotalWorkers(t *testing.T) {
6062
require.NotNil(t, m.workerRestarts)
6163
require.NotNil(t, m.workerRequestTime)
6264
require.NotNil(t, m.workerRequestCount)
65+
require.NotNil(t, m.workerTaskCount)
66+
require.NotNil(t, m.workerTaskTime)
67+
}
68+
69+
func TestPrometheusMetrics_WorkerTask(t *testing.T) {
70+
m := createPrometheusMetrics()
71+
m.TotalWorkers("bg_worker", 1)
72+
m.StartWorkerTask("bg_worker")
73+
m.StopWorkerTask("bg_worker", 3*time.Second)
74+
m.WorkerTaskOutcome("bg_worker", TaskOutcomeCompleted)
75+
m.WorkerTaskOutcome("bg_worker", TaskOutcomeTimeout)
76+
77+
inputs := []struct {
78+
name string
79+
c prometheus.Collector
80+
metadata string
81+
expect string
82+
}{
83+
{
84+
name: "Testing BusyWorkers",
85+
c: m.busyWorkers,
86+
metadata: `
87+
# HELP frankenphp_busy_workers Number of busy PHP workers for this worker: processing a request, or a task for a background worker
88+
# TYPE frankenphp_busy_workers gauge
89+
`,
90+
expect: `
91+
frankenphp_busy_workers{worker="bg_worker"} 0
92+
`,
93+
},
94+
{
95+
name: "Testing WorkerTaskTime",
96+
c: m.workerTaskTime,
97+
metadata: `
98+
# HELP frankenphp_worker_task_time Time spent on tasks by all threads of this background worker, from pickup to the close of the task's stream
99+
# TYPE frankenphp_worker_task_time counter
100+
`,
101+
expect: `
102+
frankenphp_worker_task_time{worker="bg_worker"} 3
103+
`,
104+
},
105+
{
106+
name: "Testing WorkerTaskCount",
107+
c: m.workerTaskCount,
108+
metadata: `
109+
# HELP frankenphp_worker_task_count Number of tasks sent to this background worker, by outcome: completed, aborted (the script ended with the task open), abandoned (the sender closed its stream first) or timeout (no thread picked the task up in time)
110+
# TYPE frankenphp_worker_task_count counter
111+
`,
112+
expect: `
113+
frankenphp_worker_task_count{outcome="completed",worker="bg_worker"} 1
114+
frankenphp_worker_task_count{outcome="timeout",worker="bg_worker"} 1
115+
`,
116+
},
117+
}
118+
119+
for _, input := range inputs {
120+
t.Run(input.name, func(t *testing.T) {
121+
require.NoError(t, testutil.CollectAndCompare(input.c, strings.NewReader(input.metadata+input.expect)))
122+
})
123+
}
63124
}
64125

65126
func TestPrometheusMetrics_StopWorkerRequest(t *testing.T) {
@@ -88,7 +149,7 @@ func TestPrometheusMetrics_StopWorkerRequest(t *testing.T) {
88149
name: "Testing BusyWorkers",
89150
c: m.busyWorkers,
90151
metadata: `
91-
# HELP frankenphp_busy_workers Number of busy PHP workers for this worker
152+
# HELP frankenphp_busy_workers Number of busy PHP workers for this worker: processing a request, or a task for a background worker
92153
# TYPE frankenphp_busy_workers gauge
93154
`,
94155
expect: `
@@ -131,7 +192,7 @@ func TestPrometheusMetrics_StartWorkerRequest(t *testing.T) {
131192
name: "Testing BusyWorkers",
132193
c: m.busyWorkers,
133194
metadata: `
134-
# HELP frankenphp_busy_workers Number of busy PHP workers for this worker
195+
# HELP frankenphp_busy_workers Number of busy PHP workers for this worker: processing a request, or a task for a background worker
135196
# TYPE frankenphp_busy_workers gauge
136197
`,
137198
expect: `

threadbackgroundworker.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ type backgroundWorkerThread struct {
4040
// backgroundBootWarnDelay; only touched on the PHP thread
4141
bootTimer *time.Timer
4242

43+
// openTasks counts the tasks picked up and not closed yet: the thread
44+
// is busy rather than waiting on the threads endpoint meanwhile. Only
45+
// touched on the PHP thread, pickup and close both happen there.
46+
openTasks int
47+
4348
// stopSock holds the Go side's end of this thread's stop socket pair
4449
// (per thread so pool workers drain independently); the other end is
4550
// exposed to the script via frankenphp_get_worker_handle(). Wide enough
@@ -191,6 +196,7 @@ func (handler *backgroundWorkerThread) setupScript() error {
191196
handler.dummyFrankenPHPContext = fc
192197

193198
handler.isBootingScript = true
199+
handler.openTasks = 0
194200
metrics.StartWorker(handler.worker.qualifiedName)
195201
handler.bootTimer = time.AfterFunc(backgroundBootWarnDelay, func() {
196202
if globalLogger.Enabled(globalCtx, slog.LevelWarn) {

0 commit comments

Comments
 (0)