diff --git a/config/config.go b/config/config.go index e93181f3..e2ae6552 100644 --- a/config/config.go +++ b/config/config.go @@ -16,6 +16,7 @@ import ( "github.com/drakkan/sftpgo/kms" "github.com/drakkan/sftpgo/logger" "github.com/drakkan/sftpgo/sftpd" + "github.com/drakkan/sftpgo/telemetry" "github.com/drakkan/sftpgo/utils" "github.com/drakkan/sftpgo/version" "github.com/drakkan/sftpgo/webdavd" @@ -38,14 +39,15 @@ var ( ) type globalConfig struct { - Common common.Configuration `json:"common" mapstructure:"common"` - SFTPD sftpd.Configuration `json:"sftpd" mapstructure:"sftpd"` - FTPD ftpd.Configuration `json:"ftpd" mapstructure:"ftpd"` - WebDAVD webdavd.Configuration `json:"webdavd" mapstructure:"webdavd"` - ProviderConf dataprovider.Config `json:"data_provider" mapstructure:"data_provider"` - HTTPDConfig httpd.Conf `json:"httpd" mapstructure:"httpd"` - HTTPConfig httpclient.Config `json:"http" mapstructure:"http"` - KMSConfig kms.Configuration `json:"kms" mapstructure:"kms"` + Common common.Configuration `json:"common" mapstructure:"common"` + SFTPD sftpd.Configuration `json:"sftpd" mapstructure:"sftpd"` + FTPD ftpd.Configuration `json:"ftpd" mapstructure:"ftpd"` + WebDAVD webdavd.Configuration `json:"webdavd" mapstructure:"webdavd"` + ProviderConf dataprovider.Config `json:"data_provider" mapstructure:"data_provider"` + HTTPDConfig httpd.Conf `json:"httpd" mapstructure:"httpd"` + HTTPConfig httpclient.Config `json:"http" mapstructure:"http"` + KMSConfig kms.Configuration `json:"kms" mapstructure:"kms"` + TelemetryConfig telemetry.Conf `json:"telemetry" mapstructure:"telemetry"` } func init() { @@ -182,6 +184,10 @@ func Init() { MasterKeyPath: "", }, }, + TelemetryConfig: telemetry.Conf{ + BindPort: 10000, + BindAddress: "127.0.0.1", + }, } viper.SetEnvPrefix(configEnvPrefix) @@ -268,6 +274,16 @@ func SetKMSConfig(config kms.Configuration) { globalConf.KMSConfig = config } +// GetTelemetryConfig returns the telemetry configuration +func GetTelemetryConfig() telemetry.Conf { + return globalConf.TelemetryConfig +} + +// SetTelemetryConfig sets the telemetry configuration +func SetTelemetryConfig(config telemetry.Conf) { + globalConf.TelemetryConfig = config +} + // HasServicesToStart returns true if the config defines at least a service to start. // Supported services are SFTP, FTP and WebDAV func HasServicesToStart() bool { @@ -496,4 +512,6 @@ func setViperDefaults() { viper.SetDefault("http.skip_tls_verify", globalConf.HTTPConfig.SkipTLSVerify) viper.SetDefault("kms.secrets.url", globalConf.KMSConfig.Secrets.URL) viper.SetDefault("kms.secrets.master_key_path", globalConf.KMSConfig.Secrets.MasterKeyPath) + viper.SetDefault("telemetry.bind_port", globalConf.TelemetryConfig.BindPort) + viper.SetDefault("telemetry.bind_address", globalConf.TelemetryConfig.BindAddress) } diff --git a/config/config_test.go b/config/config_test.go index 26b6aeab..db012160 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -315,6 +315,12 @@ func TestSetGetConfig(t *testing.T) { config.SetKMSConfig(kmsConf) assert.Equal(t, kmsConf.Secrets.MasterKeyPath, config.GetKMSConfig().Secrets.MasterKeyPath) assert.Equal(t, kmsConf.Secrets.URL, config.GetKMSConfig().Secrets.URL) + telemetryConf := config.GetTelemetryConfig() + telemetryConf.BindPort = 10001 + telemetryConf.BindAddress = "0.0.0.0" + config.SetTelemetryConfig(telemetryConf) + assert.Equal(t, telemetryConf.BindPort, config.GetTelemetryConfig().BindPort) + assert.Equal(t, telemetryConf.BindAddress, config.GetTelemetryConfig().BindAddress) } func TestServiceToStart(t *testing.T) { diff --git a/service/service.go b/service/service.go index 7863ad76..fba4af16 100644 --- a/service/service.go +++ b/service/service.go @@ -128,6 +128,7 @@ func (s *Service) startServices() { ftpdConf := config.GetFTPDConfig() httpdConf := config.GetHTTPDConfig() webDavDConf := config.GetWebDAVDConfig() + telemetryConf := config.GetTelemetryConfig() if sftpdConf.BindPort > 0 { go func() { @@ -182,6 +183,21 @@ func (s *Service) startServices() { } else { logger.Debug(logSender, "", "WebDAV server not started, disabled in config file") } + if telemetryConf.BindPort > 0 { + go func() { + if err := telemetryConf.Initialize(s.Profiler); err != nil { + logger.Error(logSender, "", "could not start telemetry server: %v", err) + logger.ErrorToConsole("could not start telemetry server: %v", err) + s.Error = err + } + s.Shutdown <- true + }() + } else { + logger.Debug(logSender, "", "telemetry server not started, disabled in config file") + if s.PortableMode != 1 { + logger.DebugToConsole("telemetry server not started, disabled in config file") + } + } } // Wait blocks until the service exits diff --git a/telemetry/router.go b/telemetry/router.go new file mode 100644 index 00000000..61aa4be8 --- /dev/null +++ b/telemetry/router.go @@ -0,0 +1,32 @@ +package telemetry + +import ( + "net/http" + + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" + "github.com/go-chi/render" + + "github.com/drakkan/sftpgo/logger" + "github.com/drakkan/sftpgo/metrics" +) + +func initializeRouter(enableProfiler bool) { + router = chi.NewRouter() + + router.Use(middleware.Recoverer) + + router.Group(func(r chi.Router) { + r.Get("/healthz", func(w http.ResponseWriter, r *http.Request) { + render.PlainText(w, r, "ok") + }) + }) + + metrics.AddMetricsEndpoint(metricsPath, router) + + if enableProfiler { + logger.InfoToConsole("enabling the built-in profiler") + logger.Info(logSender, "", "enabling the built-in profiler") + router.Mount(pprofBasePath, middleware.Profiler()) + } +} diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go new file mode 100644 index 00000000..c51a4ddf --- /dev/null +++ b/telemetry/telemetry.go @@ -0,0 +1,48 @@ +// Package telemetry provides telemetry information for SFTPGo, such as: +// - health information (for health checks) +// - metrics +// - profiling information +package telemetry + +import ( + "fmt" + "net/http" + "time" + + "github.com/go-chi/chi" + + "github.com/drakkan/sftpgo/logger" +) + +const ( + logSender = "telemetry" + metricsPath = "/metrics" + pprofBasePath = "/debug" +) + +var ( + router *chi.Mux +) + +// Conf telemetry server configuration. +type Conf struct { + // The port used for serving HTTP requests. 0 disable the HTTP server. Default: 8080 + BindPort int `json:"bind_port" mapstructure:"bind_port"` + // The address to listen on. A blank value means listen on all available network interfaces. Default: "127.0.0.1" + BindAddress string `json:"bind_address" mapstructure:"bind_address"` +} + +// Initialize configures and starts the telemetry server. +func (c Conf) Initialize(enableProfiler bool) error { + logger.Debug(logSender, "", "initializing telemetry server with config %+v", c) + initializeRouter(enableProfiler) + httpServer := &http.Server{ + Addr: fmt.Sprintf("%s:%d", c.BindAddress, c.BindPort), + Handler: router, + ReadTimeout: 60 * time.Second, + WriteTimeout: 60 * time.Second, + IdleTimeout: 120 * time.Second, + MaxHeaderBytes: 1 << 16, // 64KB + } + return httpServer.ListenAndServe() +}