Skip to content
Merged
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
21 changes: 16 additions & 5 deletions caddy/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ type FrankenPHPApp struct {
logger *slog.Logger
modules []*FrankenPHPModule
usedWorkerNames map[string]bool
httpApp *caddyhttp.App
}

var errIni = errors.New(`"php_ini" must be in the format: php_ini "<key>" "<value>"`)
Expand All @@ -86,7 +87,8 @@ func (f *FrankenPHPApp) Provision(ctx caddy.Context) error {
f.opts = make([]frankenphp.Option, 0, 7+len(options))

if httpApp, err := ctx.AppIfConfigured("http"); err == nil {
if httpApp.(*caddyhttp.App).Metrics != nil {
f.httpApp = httpApp.(*caddyhttp.App)
if f.httpApp.Metrics != nil {
f.metrics = frankenphp.NewPrometheusMetrics(ctx.GetMetricsRegistry())
}
} else {
Expand Down Expand Up @@ -126,7 +128,7 @@ func (f *FrankenPHPApp) Start() error {
// register global workers
for _, w := range f.Workers {
w.FileName = repl.ReplaceKnown(w.FileName, "")
w.Name = f.createUniqueWorkerName(w)
w.Name = f.createUniqueWorkerName(w, "")
f.opts = append(f.opts, frankenphp.WithWorkers(w.Name, w.FileName, w.Num, w.toWorkerOptions()...))
}

Expand Down Expand Up @@ -176,6 +178,7 @@ func (f *FrankenPHPApp) reset() {
f.ctx = nil
f.metrics = nil
f.usedWorkerNames = nil
f.httpApp = nil
}

// register workers and servers for "php" and "php_server" modules
Expand Down Expand Up @@ -208,7 +211,8 @@ func (f *FrankenPHPApp) registerModules(repl *caddy.Replacer) error {

// register a server instance and its workers for a single caddy module
func (f *FrankenPHPApp) registerModule(repl *caddy.Replacer, module *FrankenPHPModule) error {
server, err := frankenphp.NewServer(module.resolvedDocumentRoot, module.SplitPath, module.resolvedEnv, module.logger)
serverName := f.resolveServerName(module)
server, err := frankenphp.NewServer(serverName, module.resolvedDocumentRoot, module.SplitPath, module.resolvedEnv, module.logger)
if err != nil {
return err
}
Expand All @@ -218,7 +222,7 @@ func (f *FrankenPHPApp) registerModule(repl *caddy.Replacer, module *FrankenPHPM

for _, w := range module.Workers {
w.FileName = repl.ReplaceKnown(w.FileName, "")
w.Name = f.createUniqueWorkerName(w)
w.Name = f.createUniqueWorkerName(w, serverName)
workerOptions := append(w.toWorkerOptions(), frankenphp.WithWorkerServerScope(server))
f.opts = append(f.opts, frankenphp.WithWorkers(w.Name, w.FileName, w.Num, workerOptions...))
}
Expand All @@ -227,7 +231,9 @@ func (f *FrankenPHPApp) registerModule(repl *caddy.Replacer, module *FrankenPHPM
}

// avoid name collisions for workers
func (f *FrankenPHPApp) createUniqueWorkerName(wc workerConfig) string {
// on collision, a name is first qualified with the server name
// ("<serverName>:<name>") before falling back to a numeric postfix
func (f *FrankenPHPApp) createUniqueWorkerName(wc workerConfig, serverName string) string {
if f.usedWorkerNames == nil {
f.usedWorkerNames = make(map[string]bool)
}
Expand All @@ -243,6 +249,11 @@ func (f *FrankenPHPApp) createUniqueWorkerName(wc workerConfig) string {
f.usedWorkerNames[name] = true
break
}
if serverName != "" {
name = serverName + ":" + wc.Name
serverName = ""
continue
}
suffix++
name = fmt.Sprintf("%s_%d", wc.Name, suffix)
}
Expand Down
17 changes: 15 additions & 2 deletions caddy/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,10 +243,10 @@ func TestCreateUniqueWorkerNames(t *testing.T) {
names[i] = app.createUniqueWorkerName(workerConfig{
FileName: filename,
Name: "custom-worker-name",
})
}, "")
names[i+3] = app.createUniqueWorkerName(workerConfig{
FileName: filename,
})
}, "")
}

require.Equal(t, "custom-worker-name", names[0])
Expand All @@ -256,3 +256,16 @@ func TestCreateUniqueWorkerNames(t *testing.T) {
require.Equal(t, absFileName+"_1", names[4])
require.Equal(t, absFileName+"_2", names[5])
}

func TestCreateUniqueWorkerNamesQualifiedByServer(t *testing.T) {
app := &FrankenPHPApp{}
wc := workerConfig{FileName: "../testdata/worker-with-env.php", Name: "queue"}

require.Equal(t, "queue", app.createUniqueWorkerName(wc, "one.example.com"))
// on collision, the name is qualified with the server name
require.Equal(t, "two.example.com:queue", app.createUniqueWorkerName(wc, "two.example.com"))
// when the qualified name is also taken, fall back to the numeric postfix
require.Equal(t, "queue_1", app.createUniqueWorkerName(wc, "two.example.com"))
// workers without a server keep the numeric postfix behavior
require.Equal(t, "queue_2", app.createUniqueWorkerName(wc, ""))
}
82 changes: 82 additions & 0 deletions caddy/servername.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package caddy

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

// resolveServerName picks a stable, human-friendly name for the php_server
// block represented by module, so workers, metrics and logs can be attributed
// to a block (e.g. "api.example.com") instead of an opaque index. Cascade:
// 1. First host of the enclosing route's host matcher.
// 2. First listener address of the http server containing the module.
//
// Returns "" when the http app is not available or the module cannot be
// located in any route tree.
func (f *FrankenPHPApp) resolveServerName(module *FrankenPHPModule) string {
if f.httpApp == nil {
return ""
}

for _, srv := range f.httpApp.Servers {
if !serverContainsHandler(srv, module) {
continue
}
if h := findHostInRoutes(srv.Routes, module); h != "" {
return h
}
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 serverContainsHandler(srv *caddyhttp.Server, target caddyhttp.MiddlewareHandler) bool {
for _, route := range srv.Routes {
if routeContainsHandler(route, target) {
return true
}
}

return false
}

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
}
60 changes: 60 additions & 0 deletions caddy/servername_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package caddy

import (
"testing"

"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/stretchr/testify/require"
)

func TestResolveServerName(t *testing.T) {
moduleWithHost := &FrankenPHPModule{}
moduleWithoutHost := &FrankenPHPModule{}
moduleInSubroute := &FrankenPHPModule{}
unroutedModule := &FrankenPHPModule{}

host := caddyhttp.MatchHost{"api.example.com", "www.example.com"}

app := &FrankenPHPApp{
httpApp: &caddyhttp.App{
Servers: map[string]*caddyhttp.Server{
"srv0": {
Listen: []string{":8080"},
Routes: caddyhttp.RouteList{
{
MatcherSets: caddyhttp.MatcherSets{{&host}},
Handlers: []caddyhttp.MiddlewareHandler{moduleWithHost},
},
{
Handlers: []caddyhttp.MiddlewareHandler{moduleWithoutHost},
},
{
Handlers: []caddyhttp.MiddlewareHandler{
&caddyhttp.Subroute{
Routes: caddyhttp.RouteList{
{Handlers: []caddyhttp.MiddlewareHandler{moduleInSubroute}},
},
},
},
},
},
},
},
},
}

// first host of the enclosing route's host matcher wins
require.Equal(t, "api.example.com", app.resolveServerName(moduleWithHost))

// no host matcher: fall back to the first listener address
require.Equal(t, ":8080", app.resolveServerName(moduleWithoutHost))

// modules nested in subroutes are found too
require.Equal(t, ":8080", app.resolveServerName(moduleInSubroute))

// module not present in any route tree
require.Equal(t, "", app.resolveServerName(unroutedModule))

// no http app configured
require.Equal(t, "", (&FrankenPHPApp{}).resolveServerName(moduleWithHost))
}
19 changes: 17 additions & 2 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ import (
"fmt"
"log/slog"
"net/http"
"strconv"
"sync/atomic"

"github.com/dunglas/frankenphp/internal/fastabs"
)

// Server represents a preconfigured server block
// requests and workers can be scoped to a Server
type Server struct {
idx int
name string
root string
splitPath []string
env PreparedEnv
Expand Down Expand Up @@ -46,6 +47,9 @@ func registerServers(newServers []*Server) {
for i, s := range servers {
s.isRegistered.Store(true)
s.idx = i
if s.name == "" {
s.name = strconv.Itoa(i)
}
}
}

Expand All @@ -56,7 +60,11 @@ func unregisterServers() {
}
}

func NewServer(root string, splitPath []string, env map[string]string, logger *slog.Logger) (*Server, error) {
// NewServer creates a Server that can be registered via WithServer().
// name is a human-readable identifier used to attribute workers, metrics
// and logs to this server; when empty, it defaults to the index the
// server gets at registration time.
func NewServer(name, root string, splitPath []string, env map[string]string, logger *slog.Logger) (*Server, error) {
root, err := fastabs.FastAbs(root)
if err != nil {
return nil, err
Expand All @@ -67,6 +75,7 @@ func NewServer(root string, splitPath []string, env map[string]string, logger *s
}

s := &Server{
name: name,
root: root,
splitPath: splitPath,
env: PrepareEnv(env),
Expand All @@ -90,6 +99,12 @@ func NewServer(root string, splitPath []string, env map[string]string, logger *s
return s, nil
}

// Name returns the human-readable name of the server.
// It is empty until registration if none was passed to NewServer().
func (s *Server) Name() string {
return s.name
}

func (s *Server) addWorker(w *worker) error {
s.workers = append(s.workers, w)
if w.matchRequest != nil {
Expand Down
Loading