diff --git a/cmd/soroban-rpc/internal/daemon/daemon.go b/cmd/soroban-rpc/internal/daemon/daemon.go index c9f67eada..04785988b 100644 --- a/cmd/soroban-rpc/internal/daemon/daemon.go +++ b/cmd/soroban-rpc/internal/daemon/daemon.go @@ -4,6 +4,7 @@ import ( "context" "errors" "net/http" + _ "net/http/pprof" "os" "os/signal" "syscall" @@ -192,7 +193,7 @@ func MustNew(cfg config.LocalConfig) *Daemon { } } -func Run(cfg config.LocalConfig, endpoint string) { +func Run(cfg config.LocalConfig, endpoint string, adminEndpoint string) { d := MustNew(cfg) server := &http.Server{ @@ -209,6 +210,16 @@ func Run(cfg config.LocalConfig, endpoint string) { d.logger.Fatalf("Soroban JSON RPC server encountered fatal error: %v", err) } }() + var adminServer *http.Server + if adminEndpoint != "" { + // after importing net/http/pprof, debug endpoints are implicitly registered in the default serve mux + adminServer = &http.Server{Addr: adminEndpoint, Handler: http.DefaultServeMux} + go func() { + if err := adminServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { + d.logger.Errorf("Soroban admin server encountered fatal error: %v", err) + } + }() + } // Shutdown gracefully when we receive an interrupt signal. // First server.Shutdown closes all open listeners, then closes all idle connections. @@ -226,4 +237,11 @@ func Run(cfg config.LocalConfig, endpoint string) { d.logger.Errorf("Error during Soroban JSON RPC server Shutdown: %v", err) } d.Close() + + if adminServer != nil { + if err := adminServer.Shutdown(shutdownCtx); err != nil { + // Error from closing listeners, or context timeout: + d.logger.Errorf("Error during Soroban JSON admin server Shutdown: %v", err) + } + } } diff --git a/cmd/soroban-rpc/main.go b/cmd/soroban-rpc/main.go index 7b0fbccf7..291f9ef58 100644 --- a/cmd/soroban-rpc/main.go +++ b/cmd/soroban-rpc/main.go @@ -34,7 +34,7 @@ func mustPositiveUint32(co *config.ConfigOption) error { } func main() { - var endpoint string + var endpoint, adminEndpoint string var captiveCoreHTTPPort, ingestionTimeoutMinutes, coreTimeoutSeconds, maxHealthyLedgerLatencySeconds uint var serviceConfig localConfig.LocalConfig @@ -47,6 +47,14 @@ func main() { FlagDefault: "localhost:8000", Required: false, }, + { + Name: "admin-endpoint", + Usage: "Admin endpoint to listen and serve on. WARNING: this should not be accessible from the Internet and does not use TLS. \"\" (default) disables the admin server", + OptType: types.String, + ConfigKey: &adminEndpoint, + FlagDefault: "", + Required: false, + }, { Name: "stellar-core-url", ConfigKey: &serviceConfig.StellarCoreURL, @@ -275,7 +283,7 @@ func main() { serviceConfig.IngestionTimeout = time.Duration(ingestionTimeoutMinutes) * time.Minute serviceConfig.CoreRequestTimeout = time.Duration(coreTimeoutSeconds) * time.Second serviceConfig.MaxHealthyLedgerLatency = time.Duration(maxHealthyLedgerLatencySeconds) * time.Second - daemon.Run(serviceConfig, endpoint) + daemon.Run(serviceConfig, endpoint, adminEndpoint) }, }