Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 24 additions & 24 deletions caddy/caddy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -828,19 +828,19 @@ func TestWorkerMetrics(t *testing.T) {

# HELP frankenphp_busy_workers Number of busy PHP workers for this worker
# TYPE frankenphp_busy_workers gauge
frankenphp_busy_workers{worker="` + workerName + `"} 0
frankenphp_busy_workers{server="0",worker="` + workerName + `"} 0

# HELP frankenphp_total_workers Total number of PHP workers for this worker
# TYPE frankenphp_total_workers gauge
frankenphp_total_workers{worker="` + workerName + `"} 2
frankenphp_total_workers{server="0",worker="` + workerName + `"} 2

# HELP frankenphp_worker_request_count
# TYPE frankenphp_worker_request_count counter
frankenphp_worker_request_count{worker="` + workerName + `"} 10
frankenphp_worker_request_count{server="0",worker="` + workerName + `"} 10

# HELP frankenphp_ready_workers Running workers that have successfully called frankenphp_handle_request at least once
# TYPE frankenphp_ready_workers gauge
frankenphp_ready_workers{worker="` + workerName + `"} 2
frankenphp_ready_workers{server="0",worker="` + workerName + `"} 2
`

ctx := caddy.ActiveContext()
Expand Down Expand Up @@ -922,19 +922,19 @@ func TestNamedWorkerMetrics(t *testing.T) {

# HELP frankenphp_busy_workers Number of busy PHP workers for this worker
# TYPE frankenphp_busy_workers gauge
frankenphp_busy_workers{worker="my_app"} 0
frankenphp_busy_workers{server="0",worker="my_app"} 0

# HELP frankenphp_total_workers Total number of PHP workers for this worker
# TYPE frankenphp_total_workers gauge
frankenphp_total_workers{worker="my_app"} 2
frankenphp_total_workers{server="0",worker="my_app"} 2

# HELP frankenphp_worker_request_count
# TYPE frankenphp_worker_request_count counter
frankenphp_worker_request_count{worker="my_app"} 10
frankenphp_worker_request_count{server="0",worker="my_app"} 10

# HELP frankenphp_ready_workers Running workers that have successfully called frankenphp_handle_request at least once
# TYPE frankenphp_ready_workers gauge
frankenphp_ready_workers{worker="my_app"} 2
frankenphp_ready_workers{server="0",worker="my_app"} 2
`

ctx := caddy.ActiveContext()
Expand Down Expand Up @@ -1018,19 +1018,19 @@ func TestAutoWorkerConfig(t *testing.T) {

# HELP frankenphp_busy_workers Number of busy PHP workers for this worker
# TYPE frankenphp_busy_workers gauge
frankenphp_busy_workers{worker="` + workerName + `"} 0
frankenphp_busy_workers{server="0",worker="` + workerName + `"} 0

# HELP frankenphp_total_workers Total number of PHP workers for this worker
# TYPE frankenphp_total_workers gauge
frankenphp_total_workers{worker="` + workerName + `"} ` + workers + `
frankenphp_total_workers{server="0",worker="` + workerName + `"} ` + workers + `

# HELP frankenphp_worker_request_count
# TYPE frankenphp_worker_request_count counter
frankenphp_worker_request_count{worker="` + workerName + `"} 10
frankenphp_worker_request_count{server="0",worker="` + workerName + `"} 10

# HELP frankenphp_ready_workers Running workers that have successfully called frankenphp_handle_request at least once
# TYPE frankenphp_ready_workers gauge
frankenphp_ready_workers{worker="` + workerName + `"} ` + workers + `
frankenphp_ready_workers{server="0",worker="` + workerName + `"} ` + workers + `
`

ctx := caddy.ActiveContext()
Expand Down Expand Up @@ -1284,7 +1284,7 @@ func TestMaxWaitTimeWorker(t *testing.T) {

expectedMetrics := `
# TYPE frankenphp_worker_queue_depth gauge
frankenphp_worker_queue_depth{worker="service"} 0
frankenphp_worker_queue_depth{server="0",worker="service"} 0
`

ctx := caddy.ActiveContext()
Expand Down Expand Up @@ -1385,21 +1385,21 @@ func TestMultiWorkersMetrics(t *testing.T) {

# HELP frankenphp_busy_workers Number of busy PHP workers for this worker
# TYPE frankenphp_busy_workers gauge
frankenphp_busy_workers{worker="service1"} 0
frankenphp_busy_workers{server="0",worker="service1"} 0

# HELP frankenphp_total_workers Total number of PHP workers for this worker
# TYPE frankenphp_total_workers gauge
frankenphp_total_workers{worker="service1"} 2
frankenphp_total_workers{worker="service2"} 3
frankenphp_total_workers{server="0",worker="service1"} 2
frankenphp_total_workers{server="0",worker="service2"} 3

# HELP frankenphp_worker_request_count
# TYPE frankenphp_worker_request_count counter
frankenphp_worker_request_count{worker="service1"} 10
frankenphp_worker_request_count{server="0",worker="service1"} 10

# HELP frankenphp_ready_workers Running workers that have successfully called frankenphp_handle_request at least once
# TYPE frankenphp_ready_workers gauge
frankenphp_ready_workers{worker="service1"} 2
frankenphp_ready_workers{worker="service2"} 3
frankenphp_ready_workers{server="0",worker="service1"} 2
frankenphp_ready_workers{server="0",worker="service2"} 3
`

ctx := caddy.ActiveContext()
Expand Down Expand Up @@ -1552,10 +1552,10 @@ func TestWorkerRestart(t *testing.T) {
expectedMetrics := `
# HELP frankenphp_ready_workers Running workers that have successfully called frankenphp_handle_request at least once
# TYPE frankenphp_ready_workers gauge
frankenphp_ready_workers{worker="service"} 1
frankenphp_ready_workers{server="0",worker="service"} 1
# HELP frankenphp_total_workers Total number of PHP workers for this worker
# TYPE frankenphp_total_workers gauge
frankenphp_total_workers{worker="service"} 1
frankenphp_total_workers{server="0",worker="service"} 1
`

require.NoError(t,
Expand All @@ -1580,13 +1580,13 @@ func TestWorkerRestart(t *testing.T) {
expectedMetrics = `
# HELP frankenphp_ready_workers Running workers that have successfully called frankenphp_handle_request at least once
# TYPE frankenphp_ready_workers gauge
frankenphp_ready_workers{worker="service"} 1
frankenphp_ready_workers{server="0",worker="service"} 1
# HELP frankenphp_total_workers Total number of PHP workers for this worker
# TYPE frankenphp_total_workers gauge
frankenphp_total_workers{worker="service"} 1
frankenphp_total_workers{server="0",worker="service"} 1
# HELP frankenphp_worker_restarts Number of PHP worker restarts for this worker
# TYPE frankenphp_worker_restarts counter
frankenphp_worker_restarts{worker="service"} 3
frankenphp_worker_restarts{server="0",worker="service"} 3
`

require.NoError(t,
Expand Down
22 changes: 22 additions & 0 deletions caddy/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"slices"
"strconv"
"strings"
"sync"

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
Expand Down Expand Up @@ -51,6 +52,8 @@ type FrankenPHPModule struct {
preparedEnvNeedsReplacement bool
logger *slog.Logger
requestOptions []frankenphp.RequestOption
scope frankenphp.Scope
scopeLabelOnce sync.Once
}

// CaddyModule returns the Caddy module information.
Expand Down Expand Up @@ -78,6 +81,14 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {

f.assignMercureHub(ctx)

// Each php_server block gets its own scope so its workers' metric
// series stay distinct from any other block's workers (the "server"
// label). Provision can be called more than once for the same module;
// only assign once.
if f.scope == 0 {
f.scope = frankenphp.NextScope()
}

loggerOpt := frankenphp.WithRequestLogger(f.logger)
for i, wc := range f.Workers {
// make the file path absolute from the public directory
Expand All @@ -92,6 +103,7 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
}

wc.requestOptions = append(wc.requestOptions, loggerOpt)
wc.options = append(wc.options, frankenphp.WithWorkerScope(f.scope))
f.Workers[i] = wc
}

Expand Down Expand Up @@ -235,6 +247,16 @@ func (f *FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ c
}
}

f.scopeLabelOnce.Do(func() {
srv, _ := ctx.Value(caddyhttp.ServerCtxKey).(*caddyhttp.Server)
if srv == nil {
return
}
if label := f.resolveScopeLabel(srv); label != "" {
frankenphp.SetScopeLabel(f.scope, label)
}
})

fr, err := frankenphp.NewRequestWithContext(
r,
append(
Expand Down
79 changes: 79 additions & 0 deletions caddy/scopelabel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package caddy

import (
"strings"

"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)

// resolveScopeLabel picks a stable, human-friendly identifier for this
// module's scope so metric/log emitters can render it (e.g.
// server="api.example.com") instead of the opaque numeric id.
// Cascade:
// 1. First host of the route's host matcher.
// 2. Caddy server name when user-set (i.e. not the auto srvN form).
// 3. First listener address of the server.
func (f *FrankenPHPModule) resolveScopeLabel(srv *caddyhttp.Server) string {
if h := findHostInRoutes(srv.Routes, f); h != "" {
return h
}
if name := srv.Name(); name != "" && !isAutoServerName(name) {
return name
}
if len(srv.Listen) > 0 {
return srv.Listen[0]
}
return ""
}

// findHostInRoutes walks routes (recursing into Subroute handlers) to
// locate the route that contains target, then returns the first host of
// that route's host matcher. Returns "" if no enclosing route or no host
// matcher is found.
func findHostInRoutes(routes caddyhttp.RouteList, target caddyhttp.MiddlewareHandler) string {
for _, route := range routes {
if !routeContainsHandler(route, target) {
continue
}
for _, mset := range route.MatcherSets {
for _, m := range mset {
hp, ok := m.(*caddyhttp.MatchHost)
if !ok || hp == nil || len(*hp) == 0 {
continue
}
return (*hp)[0]
}
}
}
return ""
}

func routeContainsHandler(route caddyhttp.Route, target caddyhttp.MiddlewareHandler) bool {
for _, h := range route.Handlers {
if h == target {
return true
}
if sub, ok := h.(*caddyhttp.Subroute); ok {
for _, r := range sub.Routes {
if routeContainsHandler(r, target) {
return true
}
}
}
}
return false
}

// isAutoServerName reports whether name is one of Caddy's auto-assigned
// server names (srv0, srv1, ...). Anything else is treated as user-set.
func isAutoServerName(name string) bool {
if !strings.HasPrefix(name, "srv") || len(name) <= 3 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems like a very common thing to prefix servers with, likely not the best check

return false
}
for _, c := range name[3:] {
if c < '0' || c > '9' {
return false
}
}
return true
}
18 changes: 9 additions & 9 deletions docs/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ When [Caddy metrics](https://caddyserver.com/docs/metrics) are enabled, FrankenP
- `frankenphp_total_threads`: The total number of PHP threads.
- `frankenphp_busy_threads`: The number of PHP threads currently processing a request (running workers always consume a thread).
- `frankenphp_queue_depth`: The number of regular queued requests
- `frankenphp_total_workers{worker="[worker_name]"}`: The total number of workers.
- `frankenphp_busy_workers{worker="[worker_name]"}`: The number of workers currently processing a request.
- `frankenphp_worker_request_time{worker="[worker_name]"}`: The time spent processing requests by all workers.
- `frankenphp_worker_request_count{worker="[worker_name]"}`: The number of requests processed by all workers.
- `frankenphp_ready_workers{worker="[worker_name]"}`: The number of workers that have called `frankenphp_handle_request` at least once.
- `frankenphp_worker_crashes{worker="[worker_name]"}`: The number of times a worker has unexpectedly terminated.
- `frankenphp_worker_restarts{worker="[worker_name]"}`: The number of times a worker has been deliberately restarted.
- `frankenphp_worker_queue_depth{worker="[worker_name]"}`: The number of queued requests.
- `frankenphp_total_workers{server="[server]",worker="[worker_name]"}`: The total number of workers.
- `frankenphp_busy_workers{server="[server]",worker="[worker_name]"}`: The number of workers currently processing a request.
- `frankenphp_worker_request_time{server="[server]",worker="[worker_name]"}`: The time spent processing requests by all workers.
- `frankenphp_worker_request_count{server="[server]",worker="[worker_name]"}`: The number of requests processed by all workers.
- `frankenphp_ready_workers{server="[server]",worker="[worker_name]"}`: The number of workers that have called `frankenphp_handle_request` at least once.
- `frankenphp_worker_crashes{server="[server]",worker="[worker_name]"}`: The number of times a worker has unexpectedly terminated.
- `frankenphp_worker_restarts{server="[server]",worker="[worker_name]"}`: The number of times a worker has been deliberately restarted.
- `frankenphp_worker_queue_depth{server="[server]",worker="[worker_name]"}`: The number of queued requests.

For worker metrics, the `[worker_name]` placeholder is replaced by the worker name in the Caddyfile, otherwise the absolute path of the worker file will be used.
For worker metrics, the `[worker_name]` placeholder is replaced by the worker name in the Caddyfile, otherwise the absolute path of the worker file will be used. The `[server]` label identifies the `php_server` block that declared the worker; the Caddy module resolves it to the first host of the route's host matcher (e.g. `api.example.com`), falling back to the user-set Caddy server name and finally to the first listener address. Same-named workers in distinct `php_server` blocks therefore stay on distinct series.
2 changes: 1 addition & 1 deletion frankenphp.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ func calculateMaxThreads(opt *opt) (numWorkers int, _ error) {
// https://github.com/php/frankenphp/issues/126
opt.workers[i].num = maxProcs
}
metrics.TotalWorkers(w.name, w.num)
metrics.TotalWorkers(ScopeLabel(w.scope), w.name, w.num)

numWorkers += opt.workers[i].num

Expand Down
Loading
Loading