add profiler support

profiling is now available via the HTTP base URL /debug/pprof/

examples, use this URL to start and download a 30 seconds CPU profile:

/debug/pprof/profile?seconds=30

use this URL to profile used memory:

/debug/pprof/heap?gc=1

use this URL to profile allocated memory:

/debug/pprof/allocs?gc=1

Full docs here:

https://golang.org/pkg/net/http/pprof/
This commit is contained in:
Nicola Murino 2020-03-15 15:16:35 +01:00
parent f4e872c782
commit 81c8e8d898
9 changed files with 44 additions and 12 deletions

View file

@ -91,6 +91,7 @@ Please take a look at the usage below to customize the serving parameters`,
LogMaxAge: defaultLogMaxAge,
LogCompress: defaultLogCompress,
LogVerbose: defaultLogVerbose,
Profiler: defaultProfiler,
Shutdown: make(chan bool),
PortableMode: 1,
PortableUser: dataprovider.User{

View file

@ -30,6 +30,8 @@ const (
logCompressKey = "log_compress"
logVerboseFlag = "log-verbose"
logVerboseKey = "log_verbose"
profilerFlag = "profiler"
profilerKey = "profiler"
defaultConfigDir = "."
defaultConfigName = config.DefaultConfigName
defaultLogFile = "sftpgo.log"
@ -38,6 +40,7 @@ const (
defaultLogMaxAge = 28
defaultLogCompress = false
defaultLogVerbose = true
defaultProfiler = false
)
var (
@ -49,6 +52,7 @@ var (
logMaxAge int
logCompress bool
logVerbose bool
profiler bool
rootCmd = &cobra.Command{
Use: "sftpgo",
@ -135,6 +139,13 @@ func addServeFlags(cmd *cobra.Command) {
cmd.Flags().BoolVarP(&logVerbose, logVerboseFlag, "v", viper.GetBool(logVerboseKey), "Enable verbose logs. "+
"This flag can be set using SFTPGO_LOG_VERBOSE env var too.")
viper.BindPFlag(logVerboseKey, cmd.Flags().Lookup(logVerboseFlag))
viper.SetDefault(profilerKey, defaultProfiler)
viper.BindEnv(profilerKey, "SFTPGO_PROFILER")
cmd.Flags().BoolVarP(&profiler, profilerFlag, "p", viper.GetBool(profilerKey), "Enable the built-in profiler. "+
"The profiler will be accessible via HTTP/HTTPS using the base URL \"/debug/pprof/\". "+
"This flag can be set using SFTPGO_PROFILER env var too.")
viper.BindPFlag(profilerKey, cmd.Flags().Lookup(profilerFlag))
}
func getCustomServeFlags() []string {
@ -170,5 +181,8 @@ func getCustomServeFlags() []string {
if logCompress != defaultLogCompress {
result = append(result, "--"+logCompressFlag+"=true")
}
if profiler != defaultProfiler {
result = append(result, "--"+profilerFlag+"=true")
}
return result
}

View file

@ -25,6 +25,7 @@ Please take a look at the usage below to customize the startup options`,
LogMaxAge: logMaxAge,
LogCompress: logCompress,
LogVerbose: logVerbose,
Profiler: profiler,
Shutdown: make(chan bool),
}
if err := service.Start(); err == nil {

View file

@ -27,6 +27,7 @@ var (
LogMaxAge: logMaxAge,
LogCompress: logCompress,
LogVerbose: logVerbose,
Profiler: profiler,
Shutdown: make(chan bool),
}
winService := service.WindowsService{

View file

@ -30,6 +30,7 @@ const (
dumpDataPath = "/api/v1/dumpdata"
loadDataPath = "/api/v1/loaddata"
metricsPath = "/metrics"
pprofBasePath = "/debug"
webBasePath = "/web"
webUsersPath = "/web/users"
webUserPath = "/web/user"
@ -85,7 +86,7 @@ func SetDataProvider(provider dataprovider.Provider) {
}
// Initialize the HTTP server
func (c Conf) Initialize(configDir string) error {
func (c Conf) Initialize(configDir string, profiler bool) error {
var err error
logger.Debug(logSender, "", "initializing HTTP server with config %+v", c)
backupsPath = getConfigPath(c.BackupsPath, configDir)
@ -103,7 +104,7 @@ func (c Conf) Initialize(configDir string) error {
certificateFile := getConfigPath(c.CertificateFile, configDir)
certificateKeyFile := getConfigPath(c.CertificateKeyFile, configDir)
loadTemplates(templatesPath)
initializeRouter(staticFilesPath)
initializeRouter(staticFilesPath, profiler)
httpServer := &http.Server{
Addr: fmt.Sprintf("%s:%d", c.BindAddress, c.BindPort),
Handler: router,

View file

@ -49,6 +49,7 @@ const (
dumpDataPath = "/api/v1/dumpdata"
loadDataPath = "/api/v1/loaddata"
metricsPath = "/metrics"
pprofPath = "/debug/pprof/"
webBasePath = "/web"
webUsersPath = "/web/users"
webUserPath = "/web/user"
@ -117,7 +118,7 @@ func TestMain(m *testing.M) {
httpd.SetDataProvider(dataProvider)
go func() {
if err := httpdConf.Initialize(configDir); err != nil {
if err := httpdConf.Initialize(configDir, true); err != nil {
logger.Error(logSender, "", "could not start HTTP server: %v", err)
}
}()
@ -133,7 +134,7 @@ func TestMain(m *testing.M) {
httpdConf.CertificateKeyFile = keyPath
go func() {
if err := httpdConf.Initialize(configDir); err != nil {
if err := httpdConf.Initialize(configDir, true); err != nil {
logger.Error(logSender, "", "could not start HTTPS server: %v", err)
}
}()
@ -157,7 +158,7 @@ func TestInitialization(t *testing.T) {
httpdConf := config.GetHTTPDConfig()
httpdConf.BackupsPath = "test_backups"
httpdConf.AuthUserFile = "invalid file"
err := httpdConf.Initialize(configDir)
err := httpdConf.Initialize(configDir, true)
if err == nil {
t.Error("Inizialize must fail")
}
@ -165,14 +166,14 @@ func TestInitialization(t *testing.T) {
httpdConf.AuthUserFile = ""
httpdConf.CertificateFile = "invalid file"
httpdConf.CertificateKeyFile = "invalid file"
err = httpdConf.Initialize(configDir)
err = httpdConf.Initialize(configDir, true)
if err == nil {
t.Error("Inizialize must fail")
}
httpdConf.CertificateFile = ""
httpdConf.CertificateKeyFile = ""
httpdConf.TemplatesPath = "."
err = httpdConf.Initialize(configDir)
err = httpdConf.Initialize(configDir, true)
if err == nil {
t.Error("Inizialize must fail")
}
@ -1681,6 +1682,12 @@ func TestMetricsMock(t *testing.T) {
checkResponseCode(t, http.StatusOK, rr.Code)
}
func TestPProfEndPointMock(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, pprofPath, nil)
rr := executeRequest(req)
checkResponseCode(t, http.StatusOK, rr.Code)
}
func TestGetWebRootMock(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, "/", nil)
rr := executeRequest(req)

View file

@ -18,13 +18,19 @@ func GetHTTPRouter() http.Handler {
return router
}
func initializeRouter(staticFilesPath string) {
func initializeRouter(staticFilesPath string, profiler bool) {
router = chi.NewRouter()
router.Use(middleware.RequestID)
router.Use(middleware.RealIP)
router.Use(logger.NewStructuredLogger(logger.GetLogger()))
router.Use(middleware.Recoverer)
if profiler {
logger.InfoToConsole("enabling the built-in profiler")
logger.Info(logSender, "", "enabling the built-in profiler")
router.Mount(pprofBasePath, middleware.Profiler())
}
router.NotFound(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, nil, "Not Found", http.StatusNotFound)
}))

View file

@ -41,6 +41,7 @@ type Service struct {
LogVerbose bool
PortableMode int
PortableUser dataprovider.User
Profiler bool
Shutdown chan bool
}
@ -62,8 +63,8 @@ func (s *Service) Start() error {
}
version := utils.GetAppVersion()
logger.Info(logSender, "", "starting SFTPGo %v, config dir: %v, config file: %v, log max size: %v log max backups: %v "+
"log max age: %v log verbose: %v, log compress: %v", version.GetVersionAsString(), s.ConfigDir, s.ConfigFile, s.LogMaxSize,
s.LogMaxBackups, s.LogMaxAge, s.LogVerbose, s.LogCompress)
"log max age: %v log verbose: %v, log compress: %v, profile: %v", version.GetVersionAsString(), s.ConfigDir, s.ConfigFile,
s.LogMaxSize, s.LogMaxBackups, s.LogMaxAge, s.LogVerbose, s.LogCompress, s.Profiler)
// in portable mode we don't read configuration from file
if s.PortableMode != 1 {
config.LoadConfig(s.ConfigDir, s.ConfigFile)
@ -105,7 +106,7 @@ func (s *Service) Start() error {
httpd.SetDataProvider(dataProvider)
go func() {
if err := httpdConf.Initialize(s.ConfigDir); err != nil {
if err := httpdConf.Initialize(s.ConfigDir, s.Profiler); err != nil {
logger.Error(logSender, "", "could not start HTTP server: %v", err)
logger.ErrorToConsole("could not start HTTP server: %v", err)
}

View file

@ -197,7 +197,7 @@ func TestMain(m *testing.M) {
}()
go func() {
if err := httpdConf.Initialize(configDir); err != nil {
if err := httpdConf.Initialize(configDir, false); err != nil {
logger.Error(logSender, "", "could not start HTTP server: %v", err)
}
}()