diff --git a/backend/controller/controller.go b/backend/controller/controller.go index ca2f47ad3..896d373ce 100644 --- a/backend/controller/controller.go +++ b/backend/controller/controller.go @@ -149,6 +149,7 @@ func Start(ctx context.Context, config Config, runnerScaling scaling.RunnerScali rpc.GRPC(ftlv1connect.NewAdminServiceHandler, admin), rpc.GRPC(pbconsoleconnect.NewConsoleServiceHandler, console), rpc.HTTP("/", consoleHandler), + rpc.PProf(), ) }) diff --git a/backend/runner/runner.go b/backend/runner/runner.go index 0b9bf6283..dc0754d80 100644 --- a/backend/runner/runner.go +++ b/backend/runner/runner.go @@ -7,6 +7,8 @@ import ( "errors" "fmt" "math/rand" + "net/http" + "net/http/httputil" "net/url" "os" "path/filepath" @@ -108,6 +110,7 @@ func Start(ctx context.Context, config Config) error { return rpc.Serve(ctx, config.Bind, rpc.GRPC(ftlv1connect.NewVerbServiceHandler, svc), rpc.GRPC(ftlv1connect.NewRunnerServiceHandler, svc), + rpc.HTTP("/", svc), ) } @@ -353,6 +356,17 @@ func (s *Service) Terminate(ctx context.Context, c *connect.Request[ftlv1.Termin }), nil } +func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { + deployment, ok := s.deployment.Load().Get() + if !ok { + http.Error(w, "no deployment", http.StatusNotFound) + return + } + proxy := httputil.NewSingleHostReverseProxy(deployment.plugin.Endpoint) + proxy.ServeHTTP(w, r) + +} + func (s *Service) makeDeployment(ctx context.Context, key model.DeploymentKey, plugin *plugin.Plugin[ftlv1connect.VerbServiceClient]) *deployment { return &deployment{ ctx: ctx, diff --git a/common/plugin/serve.go b/common/plugin/serve.go index 1b1d99b12..cb3dedfc5 100644 --- a/common/plugin/serve.go +++ b/common/plugin/serve.go @@ -21,6 +21,7 @@ import ( "golang.org/x/net/http2/h2c" _ "github.com/TBD54566975/ftl/internal/automaxprocs" // Set GOMAXPROCS to match Linux container CPU quota. + ftlhttp "github.com/TBD54566975/ftl/internal/http" "github.com/TBD54566975/ftl/internal/log" "github.com/TBD54566975/ftl/internal/rpc" ) @@ -154,6 +155,7 @@ func Start[Impl any, Iface any, Config any]( reflector := grpcreflect.NewStaticReflector(servicePaths...) mux.Handle(grpcreflect.NewHandlerV1(reflector)) mux.Handle(grpcreflect.NewHandlerV1Alpha(reflector)) + ftlhttp.RegisterPprof(mux) // Start the server. http1Server := &http.Server{ diff --git a/internal/http/pprof.go b/internal/http/pprof.go new file mode 100644 index 000000000..8c48c19f4 --- /dev/null +++ b/internal/http/pprof.go @@ -0,0 +1,24 @@ +package http + +import ( + "net/http" + "net/http/pprof" +) + +// RegisterPprof registers all pprof handlers and the index on the provided ServeMux. +func RegisterPprof(mux *http.ServeMux) { + mux.HandleFunc("/debug/pprof", func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "/debug/pprof/", http.StatusFound) + }) + mux.HandleFunc("/debug/pprof/", pprof.Index) + mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) + mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + mux.HandleFunc("/debug/pprof/trace", pprof.Trace) + mux.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine")) + mux.Handle("/debug/pprof/heap", pprof.Handler("heap")) + mux.Handle("/debug/pprof/allocs", pprof.Handler("allocs")) + mux.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate")) + mux.Handle("/debug/pprof/block", pprof.Handler("block")) + mux.Handle("/debug/pprof/mutex", pprof.Handler("mutex")) +} diff --git a/internal/rpc/server.go b/internal/rpc/server.go index abf7737b1..7036fb692 100644 --- a/internal/rpc/server.go +++ b/internal/rpc/server.go @@ -9,6 +9,8 @@ import ( "strings" "time" + gaphttp "github.com/TBD54566975/ftl/internal/http" + "connectrpc.com/connect" "connectrpc.com/grpcreflect" "github.com/alecthomas/concurrency" @@ -40,6 +42,13 @@ func GRPC[Iface, Impl Pingable](constructor GRPCServerConstructor[Iface], impl I } } +// PProf adds /debug/pprof routes to the server. +func PProf() Option { + return func(so *serverOptions) { + gaphttp.RegisterPprof(so.mux) + } +} + // RawGRPC is a convenience function for registering a GRPC server with default options without Pingable. func RawGRPC[Iface, Impl any](constructor RawGRPCServerConstructor[Iface], impl Impl, options ...connect.HandlerOption) Option { return func(o *serverOptions) {