diff --git a/httpserver/config.go b/httpserver/config.go index c242c2f..f2ad7e2 100644 --- a/httpserver/config.go +++ b/httpserver/config.go @@ -31,6 +31,7 @@ import ( "fmt" "net" "net/url" + "strings" "time" "github.com/go-playground/validator/v10" @@ -132,6 +133,18 @@ type ServerConfig struct { getTLSDefault func() libtls.TLSConfig getParentContext func() context.Context + // Enabled allow to disable a server without clean his configuration + Disabled bool + + // Mandatory defined if the component for status is mandatory or not + Mandatory bool + + // TimeoutCacheInfo defined the validity time of cache for info (name, version, hash) + TimeoutCacheInfo time.Duration + + // TimeoutCacheHealth defined the validity time of cache for healthcheck of this server + TimeoutCacheHealth time.Duration + /*** http options ***/ // ReadTimeout is the maximum duration for reading the entire @@ -219,12 +232,44 @@ type ServerConfig struct { // Expose is the address use to call this server. This can be allow to use a single fqdn to multiple server" Expose string `mapstructure:"expose" json:"expose" yaml:"expose" toml:"expose" validate:"required,url"` + // HandlerKeys is an options to associate current server with a specifc handler defined by the key + // This allow to defined multiple server in only one config for different handler to start multiple api + HandlerKeys string + + // TLSMandatory is a flag to defined that TLS must be valid to start current server. + TLSMandatory bool + // TLS is the tls configuration for this server. // To allow tls on this server, at least the TLS Config option InheritDefault must be at true and the default TLS config must be set. // If you don't want any tls config, just omit or set an empty struct. TLS libtls.Config `mapstructure:"tls" json:"tls" yaml:"tls" toml:"tls"` } +func (c *ServerConfig) Clone() ServerConfig { + return ServerConfig{ + Disabled: c.Disabled, + getTLSDefault: c.getTLSDefault, + getParentContext: c.getParentContext, + ReadTimeout: c.ReadTimeout, + ReadHeaderTimeout: c.ReadHeaderTimeout, + WriteTimeout: c.WriteTimeout, + MaxHeaderBytes: c.MaxHeaderBytes, + MaxHandlers: c.MaxHandlers, + MaxConcurrentStreams: c.MaxConcurrentStreams, + MaxReadFrameSize: c.MaxReadFrameSize, + PermitProhibitedCipherSuites: c.PermitProhibitedCipherSuites, + IdleTimeout: c.IdleTimeout, + MaxUploadBufferPerConnection: c.MaxUploadBufferPerConnection, + MaxUploadBufferPerStream: c.MaxUploadBufferPerStream, + Name: c.Name, + Listen: c.Listen, + Expose: c.Expose, + HandlerKeys: strings.ToLower(c.HandlerKeys), + TLSMandatory: c.TLSMandatory, + TLS: c.TLS, + } +} + func (c *ServerConfig) SetDefaultTLS(f func() libtls.TLSConfig) { c.getTLSDefault = f } @@ -317,6 +362,10 @@ func (c ServerConfig) GetExpose() *url.URL { return add } +func (c ServerConfig) GetHandlerKey() string { + return c.HandlerKeys +} + func (c ServerConfig) Validate() liberr.Error { val := validator.New() err := val.Struct(c) diff --git a/httpserver/pool.go b/httpserver/pool.go index 4e5cef6..22f5c3e 100644 --- a/httpserver/pool.go +++ b/httpserver/pool.go @@ -28,6 +28,7 @@ package httpserver import ( "context" + "fmt" "net/http" "os" "os/signal" @@ -35,6 +36,8 @@ import ( "strings" "syscall" + "github.com/nabbar/golib/status" + "github.com/nabbar/golib/logger" "github.com/nabbar/golib/semaphore" @@ -45,7 +48,8 @@ import ( type FieldType uint8 const ( - FieldName FieldType = iota + HandlerDefault = "default" + FieldName FieldType = iota FieldBind FieldExpose ) @@ -72,8 +76,13 @@ type PoolServer interface { WaitNotify(ctx context.Context, cancel context.CancelFunc) Listen(handler http.Handler) liberr.Error + ListenMultiHandler(handler map[string]http.Handler) liberr.Error Restart() Shutdown() + + StatusInfo(bindAddress string) (name string, release string, hash string) + StatusHealth(bindAddress string) error + StatusRoute(prefix string, fctMessage status.FctMessage, sts status.RouteStatus) } func NewPool(srv ...Server) PoolServer { @@ -333,6 +342,10 @@ func (p pool) WaitNotify(ctx context.Context, cancel context.CancelFunc) { } func (p pool) Listen(handler http.Handler) liberr.Error { + return p.ListenMultiHandler(map[string]http.Handler{HandlerDefault: handler}) +} + +func (p pool) ListenMultiHandler(handler map[string]http.Handler) liberr.Error { if p.Len() < 1 { return nil } @@ -341,9 +354,23 @@ func (p pool) Listen(handler http.Handler) liberr.Error { e = ErrorPoolListen.Error(nil) logger.InfoLevel.Log("Calling listen for All Servers") + p.MapRun(func(srv Server) { - e.AddParentError(srv.Listen(handler)) + if len(handler) < 1 { + e.AddParentError(srv.Listen(nil)) + } else { + for k := range handler { + if len(handler) == 1 { + e.AddParentError(srv.Listen(handler[k])) + break + } else if strings.ToLower(k) == srv.GetHandlerKey() { + e.AddParentError(srv.Listen(handler[k])) + break + } + } + } }) + logger.InfoLevel.Log("End of Calling listen for All Servers") if !e.HasParent() { @@ -412,3 +439,26 @@ func (p pool) Shutdown() { _ = s.WaitAll() } + +func (p pool) StatusInfo(bindAddress string) (name string, release string, hash string) { + if s := p.Get(bindAddress); s != nil { + return s.StatusInfo() + } + + return fmt.Sprintf("missing server '%s'", bindAddress), "", "" +} + +func (p pool) StatusHealth(bindAddress string) error { + if s := p.Get(bindAddress); s != nil { + return s.StatusHealth() + } + + return fmt.Errorf("missing server '%s'", bindAddress) +} + +func (p pool) StatusRoute(keyPrefix string, fctMessage status.FctMessage, sts status.RouteStatus) { + p.MapRun(func(srv Server) { + bind := srv.GetBindable() + sts.ComponentNew(fmt.Sprintf("%s-%s", keyPrefix, bind), srv.StatusComponent(fctMessage)) + }) +} diff --git a/httpserver/run.go b/httpserver/run.go index 22e0dfe..4693014 100644 --- a/httpserver/run.go +++ b/httpserver/run.go @@ -30,6 +30,7 @@ package httpserver import ( "context" "errors" + "fmt" "log" "net" "net/http" @@ -45,6 +46,7 @@ import ( ) type srvRun struct { + err *atomic.Value run *atomic.Value snm string srv *http.Server @@ -54,6 +56,7 @@ type srvRun struct { type run interface { IsRunning() bool + GetError() error WaitNotify() Listen(cfg *ServerConfig, handler http.Handler) liberr.Error Restart(cfg *ServerConfig) @@ -62,6 +65,7 @@ type run interface { func newRun() run { return &srvRun{ + err: new(atomic.Value), run: new(atomic.Value), srv: nil, } @@ -87,7 +91,37 @@ func (s *srvRun) IsRunning() bool { return s.getRunning() } +func (s *srvRun) getErr() error { + if s.err == nil { + return nil + } else if i := s.err.Load(); i == nil { + return nil + } else if e, ok := i.(error); !ok { + return nil + } else if e.Error() == "" { + return nil + } else { + return e + } +} + +func (s *srvRun) setErr(e error) { + if e != nil { + s.err.Store(e) + } else { + s.err.Store(errors.New("")) + } +} + +func (s *srvRun) GetError() error { + return s.getErr() +} + func (s *srvRun) WaitNotify() { + if !s.IsRunning() { + return + } + // Wait for interrupt signal to gracefully shutdown the server with // a timeout of 5 seconds. quit := make(chan os.Signal, 1) @@ -113,6 +147,7 @@ func (s *srvRun) Listen(cfg *ServerConfig, handler http.Handler) liberr.Error { return err } + sTls := cfg.TLSMandatory bind := cfg.GetListen().Host name := cfg.Name if name == "" { @@ -181,6 +216,7 @@ func (s *srvRun) Listen(cfg *ServerConfig, handler http.Handler) liberr.Error { } if e := http2.ConfigureServer(srv, s2); e != nil { + s.setErr(e) return ErrorHTTP2Configure.ErrorParent(e) } @@ -206,7 +242,7 @@ func (s *srvRun) Listen(cfg *ServerConfig, handler http.Handler) liberr.Error { s.snm = name s.srv = srv - go func(name, host string) { + go func(name, host string, tlsMandatory bool) { defer func() { if s.ctx != nil && s.cnl != nil && s.ctx.Err() == nil { @@ -226,6 +262,8 @@ func (s *srvRun) Listen(cfg *ServerConfig, handler http.Handler) liberr.Error { s.setRunning(true) err = s.srv.ListenAndServeTLS("", "") + } else if tlsMandatory { + err = fmt.Errorf("missing valid server certificates") } else { liblog.InfoLevel.Logf("Server '%s' is starting with bindable: %s", name, host) @@ -238,9 +276,10 @@ func (s *srvRun) Listen(cfg *ServerConfig, handler http.Handler) liberr.Error { } else if err != nil && errors.Is(err, http.ErrServerClosed) { return } else if err != nil { + s.setErr(err) liblog.ErrorLevel.LogErrorCtxf(liblog.NilLevel, "Listen Server '%s'", err, name) } - }(name, bind) + }(name, bind, sTls) return nil } diff --git a/httpserver/server.go b/httpserver/server.go index e781a9a..7bf5ab1 100644 --- a/httpserver/server.go +++ b/httpserver/server.go @@ -27,11 +27,15 @@ package httpserver import ( - "context" + "fmt" "net/http" + "runtime" + "strings" "sync/atomic" "time" + "github.com/nabbar/golib/status" + liberr "github.com/nabbar/golib/errors" ) @@ -42,8 +46,7 @@ const ( type server struct { run *atomic.Value - cfg *ServerConfig - cnl context.CancelFunc + cfg ServerConfig } type Server interface { @@ -53,6 +56,7 @@ type Server interface { GetName() string GetBindable() string GetExpose() string + GetHandlerKey() string IsRunning() bool IsTLS() bool @@ -62,13 +66,16 @@ type Server interface { Listen(handler http.Handler) liberr.Error Restart() Shutdown() + + StatusInfo() (name string, release string, hash string) + StatusHealth() error + StatusComponent(message status.FctMessage) status.Component } func NewServer(cfg *ServerConfig) Server { return &server{ - cfg: cfg, + cfg: cfg.Clone(), run: new(atomic.Value), - cnl: nil, } } @@ -88,12 +95,20 @@ func (s *server) setRun(r run) { s.run.Store(r) } +func (s *server) getErr() error { + if r := s.getRun(); r == nil { + return nil + } else { + return r.GetError() + } +} + func (s *server) GetConfig() *ServerConfig { - return s.cfg + return &s.cfg } func (s *server) SetConfig(cfg *ServerConfig) { - s.cfg = cfg + s.cfg = cfg.Clone() } func (s server) GetName() string { @@ -112,6 +127,10 @@ func (s *server) GetExpose() string { return s.cfg.GetExpose().String() } +func (s *server) GetHandlerKey() string { + return s.cfg.GetHandlerKey() +} + func (s *server) IsRunning() bool { return s.getRun().IsRunning() } @@ -122,8 +141,9 @@ func (s *server) IsTLS() bool { func (s *server) Listen(handler http.Handler) liberr.Error { r := s.getRun() - e := r.Listen(s.cfg, handler) + e := r.Listen(&s.cfg, handler) s.setRun(r) + return e } @@ -150,3 +170,27 @@ func (s *server) Merge(srv Server) bool { return false } + +func (s *server) StatusInfo() (name string, release string, hash string) { + vers := strings.TrimLeft(runtime.Version(), "go") + vers = strings.TrimLeft(vers, "Go") + vers = strings.TrimLeft(vers, "GO") + + return fmt.Sprintf("%s [%s]", s.GetName(), s.GetBindable()), vers, "" +} + +func (s *server) StatusHealth() error { + if !s.cfg.Disabled && s.IsRunning() { + return nil + } else if s.cfg.Disabled { + return fmt.Errorf("server disabled") + } else if e := s.getErr(); e != nil { + return e + } else { + return fmt.Errorf("server is offline -- missing error") + } +} + +func (s *server) StatusComponent(message status.FctMessage) status.Component { + return status.NewComponent(s.cfg.Mandatory, s.StatusInfo, s.StatusHealth, message, s.cfg.TimeoutCacheInfo, s.cfg.TimeoutCacheHealth) +} diff --git a/test/test-httpserver/main.go b/test/test-httpserver/main.go index d01e2f6..91f4bb2 100644 --- a/test/test-httpserver/main.go +++ b/test/test-httpserver/main.go @@ -46,17 +46,27 @@ var tlsConfigSrv = libtls.Config{ } var cfgSrv01 = libsrv.ServerConfig{ - Name: "test-01", - Listen: "0.0.0.0:61001", - Expose: "0.0.0.0:61000", - TLS: tlsConfigSrv, + Name: "test-01", + Listen: "0.0.0.0:61001", + Expose: "0.0.0.0:61000", + TLSMandatory: false, + TLS: tlsConfigSrv, } var cfgSrv02 = libsrv.ServerConfig{ - Name: "test-02", - Listen: "0.0.0.0:61002", - Expose: "0.0.0.0:61000", - TLS: tlsConfigSrv, + Name: "test-02", + Listen: "0.0.0.0:61002", + Expose: "0.0.0.0:61000", + TLSMandatory: false, + TLS: tlsConfigSrv, +} + +var cfgSrv03 = libsrv.ServerConfig{ + Name: "test-03", + Listen: "0.0.0.0:61003", + Expose: "0.0.0.0:61000", + TLSMandatory: true, + TLS: tlsConfigSrv, } var ( @@ -77,7 +87,7 @@ func init() { ctx, cnl = context.WithCancel(context.Background()) - cfgPool = libsrv.PoolServerConfig{cfgSrv01, cfgSrv02} + cfgPool = libsrv.PoolServerConfig{cfgSrv01, cfgSrv02, cfgSrv03} cfgPool.MapUpdate(func(cfg libsrv.ServerConfig) libsrv.ServerConfig { cfg.SetParentContext(func() context.Context { return ctx @@ -112,6 +122,24 @@ func main() { go pool.WaitNotify(ctx, cnl) + go func() { + for { + time.Sleep(5 * time.Second) + if ctx.Err() != nil { + return + } + pool.MapRun(func(srv libsrv.Server) { + n, v, _ := srv.StatusInfo() + if e := srv.StatusHealth(); e != nil { + fmt.Printf("%s - %s : %v\n", n, v, e) + } else { + fmt.Printf("%s - %s : OK\n", n, v) + } + + }) + } + }() + var i = 0 for {