Skip to content

Commit dd71a7c

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 a9d72df commit dd71a7c

7 files changed

Lines changed: 280 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{server="",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{server="",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{server="",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{server="",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{server="",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]",server="[server_name]"}`: The total number of workers.
19-
- `frankenphp_busy_workers{worker="[worker_name]",server="[server_name]"}`: The number of workers currently processing a request.
19+
- `frankenphp_busy_workers{worker="[worker_name]",server="[server_name]"}`: The number of workers currently processing a request, or a task for a background worker.
2020
- `frankenphp_worker_request_time{worker="[worker_name]",server="[server_name]"}`: The time spent processing requests by all workers.
2121
- `frankenphp_worker_request_count{worker="[worker_name]",server="[server_name]"}`: The number of requests processed by all workers.
2222
- `frankenphp_ready_workers{worker="[worker_name]",server="[server_name]"}`: The number of workers that have reached their ready point at least once: `frankenphp_handle_request()` for HTTP workers, `WorkerHandle::tick()` for background workers.
2323
- `frankenphp_worker_crashes{worker="[worker_name]",server="[server_name]"}`: The number of times a worker has unexpectedly terminated.
2424
- `frankenphp_worker_restarts{worker="[worker_name]",server="[server_name]"}`: The number of times a worker has been deliberately restarted.
25-
- `frankenphp_worker_queue_depth{worker="[worker_name]",server="[server_name]"}`: The number of queued requests.
25+
- `frankenphp_worker_queue_depth{worker="[worker_name]",server="[server_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]",server="[server_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]",server="[server_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, with a number appended when several workers share a script. `[server_name]` is the name of the `php_server` block the worker belongs to, and is empty for a worker declared in the global `frankenphp` block. The two stay apart, so a query on the worker name alone still selects that worker in every server.
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: 112 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
// Metrics reports what the workers and the threads of a FrankenPHP instance
2030
// are doing. A worker is identified by its name alone, where a worker scoped
2131
// to a server is reported as "<server name>:<name>". An implementation that
@@ -61,6 +71,14 @@ type ServerMetrics interface {
6171
StartWorkerRequestOnServer(name, server string)
6272
QueuedWorkerRequestOnServer(name, server string)
6373
DequeuedWorkerRequestOnServer(name, server string)
74+
// the tasks of background workers have no packed counterpart in
75+
// Metrics, so they take the pair under their own name
76+
// StartWorkerTask collects tasks picked up by a thread of a background worker
77+
StartWorkerTask(name, server string)
78+
// StopWorkerTask collects tasks a thread of a background worker is done with
79+
StopWorkerTask(name, server string, duration time.Duration)
80+
// WorkerTaskOutcome collects how tasks sent to a background worker ended
81+
WorkerTaskOutcome(name, server string, outcome TaskOutcome)
6482
}
6583

6684
// workerMetrics is what the runtime reports on: Metrics with the worker
@@ -80,6 +98,9 @@ type workerMetrics interface {
8098
DequeuedWorkerRequest(name, server string)
8199
QueuedRequest()
82100
DequeuedRequest()
101+
StartWorkerTask(name, server string)
102+
StopWorkerTask(name, server string, duration time.Duration)
103+
WorkerTaskOutcome(name, server string, outcome TaskOutcome)
83104
}
84105

85106
// metricsAdapter routes the worker methods to ServerMetrics when the
@@ -104,6 +125,26 @@ func packedWorkerName(name, server string) string {
104125
return server + ":" + name
105126
}
106127

128+
// the task methods have no packed form: an implementation that does not
129+
// satisfy ServerMetrics simply does not collect them
130+
func (a metricsAdapter) StartWorkerTask(name, server string) {
131+
if a.server != nil {
132+
a.server.StartWorkerTask(name, server)
133+
}
134+
}
135+
136+
func (a metricsAdapter) StopWorkerTask(name, server string, duration time.Duration) {
137+
if a.server != nil {
138+
a.server.StopWorkerTask(name, server, duration)
139+
}
140+
}
141+
142+
func (a metricsAdapter) WorkerTaskOutcome(name, server string, outcome TaskOutcome) {
143+
if a.server != nil {
144+
a.server.WorkerTaskOutcome(name, server, outcome)
145+
}
146+
}
147+
107148
func (a metricsAdapter) StartWorker(name, server string) {
108149
if a.server != nil {
109150
a.server.StartWorkerOnServer(name, server)
@@ -207,6 +248,12 @@ func (n nullMetrics) DequeuedWorkerRequest(string, string) {}
207248
func (n nullMetrics) QueuedRequest() {}
208249
func (n nullMetrics) DequeuedRequest() {}
209250

251+
func (n nullMetrics) StartWorkerTask(string, string) {}
252+
253+
func (n nullMetrics) StopWorkerTask(string, string, time.Duration) {}
254+
255+
func (n nullMetrics) WorkerTaskOutcome(string, string, TaskOutcome) {}
256+
210257
type PrometheusMetrics struct {
211258
registry prometheus.Registerer
212259
totalThreads prometheus.Gauge
@@ -219,6 +266,8 @@ type PrometheusMetrics struct {
219266
workerRequestTime *prometheus.CounterVec
220267
workerRequestCount *prometheus.CounterVec
221268
workerQueueDepth *prometheus.GaugeVec
269+
workerTaskCount *prometheus.CounterVec
270+
workerTaskTime *prometheus.CounterVec
222271
queueDepth prometheus.Gauge
223272
mu sync.RWMutex
224273
}
@@ -341,7 +390,7 @@ func (m *PrometheusMetrics) TotalWorkersOnServer(string, string, int) {
341390
m.busyWorkers = prometheus.NewGaugeVec(prometheus.GaugeOpts{
342391
Namespace: ns,
343392
Name: "busy_workers",
344-
Help: "Number of busy PHP workers for this worker",
393+
Help: "Number of busy PHP workers for this worker: processing a request, or a task for a background worker",
345394
}, basicLabels)
346395
m.mustRegister(m.busyWorkers)
347396
}
@@ -389,9 +438,30 @@ func (m *PrometheusMetrics) TotalWorkersOnServer(string, string, int) {
389438
Namespace: "frankenphp",
390439
Subsystem: sub,
391440
Name: "queue_depth",
441+
Help: "Number of queued requests for this worker, or of tasks waiting for a thread of a background worker",
392442
}, basicLabels)
393443
m.mustRegister(m.workerQueueDepth)
394444
}
445+
446+
if m.workerTaskCount == nil {
447+
m.workerTaskCount = prometheus.NewCounterVec(prometheus.CounterOpts{
448+
Namespace: ns,
449+
Subsystem: sub,
450+
Name: "task_count",
451+
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)",
452+
}, []string{"worker", "server", "outcome"})
453+
m.mustRegister(m.workerTaskCount)
454+
}
455+
456+
if m.workerTaskTime == nil {
457+
m.workerTaskTime = prometheus.NewCounterVec(prometheus.CounterOpts{
458+
Namespace: ns,
459+
Subsystem: sub,
460+
Name: "task_time",
461+
Help: "Time spent on tasks by all threads of this background worker, from pickup to the close of the task's stream",
462+
}, basicLabels)
463+
m.mustRegister(m.workerTaskTime)
464+
}
395465
}
396466

397467
func (m *PrometheusMetrics) TotalThreads(num int) {
@@ -472,6 +542,37 @@ func (m *PrometheusMetrics) DequeuedRequest() {
472542
m.queueDepth.Dec()
473543
}
474544

545+
func (m *PrometheusMetrics) StartWorkerTask(name, server string) {
546+
m.mu.RLock()
547+
defer m.mu.RUnlock()
548+
549+
if m.busyWorkers == nil {
550+
return
551+
}
552+
m.busyWorkers.WithLabelValues(name, server).Inc()
553+
}
554+
555+
func (m *PrometheusMetrics) StopWorkerTask(name, server string, duration time.Duration) {
556+
m.mu.RLock()
557+
defer m.mu.RUnlock()
558+
559+
if m.workerTaskTime == nil {
560+
return
561+
}
562+
m.busyWorkers.WithLabelValues(name, server).Dec()
563+
m.workerTaskTime.WithLabelValues(name, server).Add(duration.Seconds())
564+
}
565+
566+
func (m *PrometheusMetrics) WorkerTaskOutcome(name, server string, outcome TaskOutcome) {
567+
m.mu.RLock()
568+
defer m.mu.RUnlock()
569+
570+
if m.workerTaskCount == nil {
571+
return
572+
}
573+
m.workerTaskCount.WithLabelValues(name, server, string(outcome)).Inc()
574+
}
575+
475576
func (m *PrometheusMetrics) Shutdown() {
476577
m.mu.Lock()
477578
defer m.mu.Unlock()
@@ -511,6 +612,14 @@ func (m *PrometheusMetrics) Shutdown() {
511612
if m.workerQueueDepth != nil {
512613
m.registry.Unregister(m.workerQueueDepth)
513614
}
615+
616+
if m.workerTaskCount != nil {
617+
m.registry.Unregister(m.workerTaskCount)
618+
}
619+
620+
if m.workerTaskTime != nil {
621+
m.registry.Unregister(m.workerTaskTime)
622+
}
514623
}
515624

516625
func NewPrometheusMetrics(registry prometheus.Registerer) *PrometheusMetrics {
@@ -540,6 +649,8 @@ func NewPrometheusMetrics(registry prometheus.Registerer) *PrometheusMetrics {
540649
workerCrashes: nil,
541650
readyWorkers: nil,
542651
workerQueueDepth: nil,
652+
workerTaskCount: nil,
653+
workerTaskTime: nil,
543654
}
544655

545656
m.mustRegister(m.totalThreads)

metrics_test.go

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ func TestPrometheusMetrics_TotalWorkers(t *testing.T) {
5151
require.Nil(t, m.workerRestarts)
5252
require.Nil(t, m.workerRequestTime)
5353
require.Nil(t, m.workerRequestCount)
54+
require.Nil(t, m.workerTaskCount)
55+
require.Nil(t, m.workerTaskTime)
5456

5557
m.TotalWorkersOnServer("test_worker", "test_server", 2)
5658

@@ -61,6 +63,65 @@ func TestPrometheusMetrics_TotalWorkers(t *testing.T) {
6163
require.NotNil(t, m.workerRestarts)
6264
require.NotNil(t, m.workerRequestTime)
6365
require.NotNil(t, m.workerRequestCount)
66+
require.NotNil(t, m.workerTaskCount)
67+
require.NotNil(t, m.workerTaskTime)
68+
}
69+
70+
func TestPrometheusMetrics_WorkerTask(t *testing.T) {
71+
m := createPrometheusMetrics()
72+
m.TotalWorkersOnServer("bg_worker", "bg_server", 1)
73+
m.StartWorkerTask("bg_worker", "bg_server")
74+
m.StopWorkerTask("bg_worker", "bg_server", 3*time.Second)
75+
m.WorkerTaskOutcome("bg_worker", "bg_server", TaskOutcomeCompleted)
76+
m.WorkerTaskOutcome("bg_worker", "bg_server", TaskOutcomeTimeout)
77+
78+
inputs := []struct {
79+
name string
80+
c prometheus.Collector
81+
metadata string
82+
expect string
83+
}{
84+
{
85+
name: "Testing BusyWorkers",
86+
c: m.busyWorkers,
87+
metadata: `
88+
# HELP frankenphp_busy_workers Number of busy PHP workers for this worker: processing a request, or a task for a background worker
89+
# TYPE frankenphp_busy_workers gauge
90+
`,
91+
expect: `
92+
frankenphp_busy_workers{server="bg_server",worker="bg_worker"} 0
93+
`,
94+
},
95+
{
96+
name: "Testing WorkerTaskTime",
97+
c: m.workerTaskTime,
98+
metadata: `
99+
# 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
100+
# TYPE frankenphp_worker_task_time counter
101+
`,
102+
expect: `
103+
frankenphp_worker_task_time{server="bg_server",worker="bg_worker"} 3
104+
`,
105+
},
106+
{
107+
name: "Testing WorkerTaskCount",
108+
c: m.workerTaskCount,
109+
metadata: `
110+
# 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)
111+
# TYPE frankenphp_worker_task_count counter
112+
`,
113+
expect: `
114+
frankenphp_worker_task_count{outcome="completed",server="bg_server",worker="bg_worker"} 1
115+
frankenphp_worker_task_count{outcome="timeout",server="bg_server",worker="bg_worker"} 1
116+
`,
117+
},
118+
}
119+
120+
for _, input := range inputs {
121+
t.Run(input.name, func(t *testing.T) {
122+
require.NoError(t, testutil.CollectAndCompare(input.c, strings.NewReader(input.metadata+input.expect)))
123+
})
124+
}
64125
}
65126

66127
func TestPrometheusMetrics_StopWorkerRequest(t *testing.T) {
@@ -89,7 +150,7 @@ func TestPrometheusMetrics_StopWorkerRequest(t *testing.T) {
89150
name: "Testing BusyWorkers",
90151
c: m.busyWorkers,
91152
metadata: `
92-
# HELP frankenphp_busy_workers Number of busy PHP workers for this worker
153+
# HELP frankenphp_busy_workers Number of busy PHP workers for this worker: processing a request, or a task for a background worker
93154
# TYPE frankenphp_busy_workers gauge
94155
`,
95156
expect: `
@@ -132,7 +193,7 @@ func TestPrometheusMetrics_StartWorkerRequest(t *testing.T) {
132193
name: "Testing BusyWorkers",
133194
c: m.busyWorkers,
134195
metadata: `
135-
# HELP frankenphp_busy_workers Number of busy PHP workers for this worker
196+
# HELP frankenphp_busy_workers Number of busy PHP workers for this worker: processing a request, or a task for a background worker
136197
# TYPE frankenphp_busy_workers gauge
137198
`,
138199
expect: `
@@ -253,6 +314,13 @@ func (m *splitMetrics) StopWorkerRequestOnServer(name, server string, _ time.Dur
253314
func (m *splitMetrics) StartWorkerRequestOnServer(name, server string) { m.record(name, server) }
254315
func (m *splitMetrics) QueuedWorkerRequestOnServer(name, server string) { m.record(name, server) }
255316
func (m *splitMetrics) DequeuedWorkerRequestOnServer(name, server string) { m.record(name, server) }
317+
func (m *splitMetrics) StartWorkerTask(name, server string) { m.record(name, server) }
318+
func (m *splitMetrics) StopWorkerTask(name, server string, _ time.Duration) {
319+
m.record(name, server)
320+
}
321+
func (m *splitMetrics) WorkerTaskOutcome(name, server string, _ TaskOutcome) {
322+
m.record(name, server)
323+
}
256324

257325
func (m *splitMetrics) StartWorker(string) { m.t.Fatal("packed StartWorker called") }
258326
func (m *splitMetrics) ReadyWorker(string) { m.t.Fatal("packed ReadyWorker called") }

threadbackgroundworker.go

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

53+
// openTasks counts the tasks picked up and not closed yet: the thread
54+
// is busy rather than waiting on the threads endpoint meanwhile. Only
55+
// touched on the PHP thread, pickup and close both happen there.
56+
openTasks int
57+
5358
// stopSock holds the Go side's end of this thread's stop socket pair
5459
// (per thread so pool workers drain independently); the other end is
5560
// exposed to the script via WorkerHandle::getStream(). Wide enough
@@ -178,6 +183,7 @@ func (handler *backgroundWorkerThread) setupScript() error {
178183

179184
handler.isBootingScript = true
180185
handler.runStartedAt = time.Now()
186+
handler.openTasks = 0
181187
metrics.StartWorker(handler.worker.name, handler.worker.server.name)
182188
// the run's logger and context, not the globals: Stop() does not wait
183189
// for a callback that already started, and a shutdown finishing

0 commit comments

Comments
 (0)