1
0
mirror of https://github.com/drakkan/sftpgo.git synced 2025-12-05 22:17:20 +02:00

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, LogMaxAge: defaultLogMaxAge,
LogCompress: defaultLogCompress, LogCompress: defaultLogCompress,
LogVerbose: defaultLogVerbose, LogVerbose: defaultLogVerbose,
Profiler: defaultProfiler,
Shutdown: make(chan bool), Shutdown: make(chan bool),
PortableMode: 1, PortableMode: 1,
PortableUser: dataprovider.User{ PortableUser: dataprovider.User{

View File

@@ -30,6 +30,8 @@ const (
logCompressKey = "log_compress" logCompressKey = "log_compress"
logVerboseFlag = "log-verbose" logVerboseFlag = "log-verbose"
logVerboseKey = "log_verbose" logVerboseKey = "log_verbose"
profilerFlag = "profiler"
profilerKey = "profiler"
defaultConfigDir = "." defaultConfigDir = "."
defaultConfigName = config.DefaultConfigName defaultConfigName = config.DefaultConfigName
defaultLogFile = "sftpgo.log" defaultLogFile = "sftpgo.log"
@@ -38,6 +40,7 @@ const (
defaultLogMaxAge = 28 defaultLogMaxAge = 28
defaultLogCompress = false defaultLogCompress = false
defaultLogVerbose = true defaultLogVerbose = true
defaultProfiler = false
) )
var ( var (
@@ -49,6 +52,7 @@ var (
logMaxAge int logMaxAge int
logCompress bool logCompress bool
logVerbose bool logVerbose bool
profiler bool
rootCmd = &cobra.Command{ rootCmd = &cobra.Command{
Use: "sftpgo", Use: "sftpgo",
@@ -135,6 +139,13 @@ func addServeFlags(cmd *cobra.Command) {
cmd.Flags().BoolVarP(&logVerbose, logVerboseFlag, "v", viper.GetBool(logVerboseKey), "Enable verbose logs. "+ cmd.Flags().BoolVarP(&logVerbose, logVerboseFlag, "v", viper.GetBool(logVerboseKey), "Enable verbose logs. "+
"This flag can be set using SFTPGO_LOG_VERBOSE env var too.") "This flag can be set using SFTPGO_LOG_VERBOSE env var too.")
viper.BindPFlag(logVerboseKey, cmd.Flags().Lookup(logVerboseFlag)) 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 { func getCustomServeFlags() []string {
@@ -170,5 +181,8 @@ func getCustomServeFlags() []string {
if logCompress != defaultLogCompress { if logCompress != defaultLogCompress {
result = append(result, "--"+logCompressFlag+"=true") result = append(result, "--"+logCompressFlag+"=true")
} }
if profiler != defaultProfiler {
result = append(result, "--"+profilerFlag+"=true")
}
return result return result
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,13 +18,19 @@ func GetHTTPRouter() http.Handler {
return router return router
} }
func initializeRouter(staticFilesPath string) { func initializeRouter(staticFilesPath string, profiler bool) {
router = chi.NewRouter() router = chi.NewRouter()
router.Use(middleware.RequestID) router.Use(middleware.RequestID)
router.Use(middleware.RealIP) router.Use(middleware.RealIP)
router.Use(logger.NewStructuredLogger(logger.GetLogger())) router.Use(logger.NewStructuredLogger(logger.GetLogger()))
router.Use(middleware.Recoverer) 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) { router.NotFound(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sendAPIResponse(w, r, nil, "Not Found", http.StatusNotFound) sendAPIResponse(w, r, nil, "Not Found", http.StatusNotFound)
})) }))

View File

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