mirror of
https://github.com/nabbar/golib.git
synced 2026-04-22 23:17:12 +08:00
Package HTTPServer:
- ADD config option to filter log message - ADD public function into interface was not exposed - FIX doc graph alignment - IMPROVE testing and godoc comments
This commit is contained in:
+32
-4
@@ -34,6 +34,8 @@ import (
|
||||
"strings"
|
||||
|
||||
libval "github.com/go-playground/validator/v10"
|
||||
tlscas "github.com/nabbar/golib/certificates/ca"
|
||||
tlscrt "github.com/nabbar/golib/certificates/certs"
|
||||
|
||||
libtls "github.com/nabbar/golib/certificates"
|
||||
libdur "github.com/nabbar/golib/duration"
|
||||
@@ -189,6 +191,11 @@ type Config struct {
|
||||
// shutting down should disable them.
|
||||
DisableKeepAlive bool `mapstructure:"disable_keep_alive" json:"disable_keep_alive" yaml:"disable_keep_alive" toml:"disable_keep_alive"`
|
||||
|
||||
// LogFilter specifies a list of strings to filter log output.
|
||||
// Log entries containing any of these strings will be suppressed.
|
||||
// This is useful for reducing log verbosity by ignoring specific messages.
|
||||
LogFilter []string `mapstructure:"log-filter" json:"log-filter" yaml:"log-filter" toml:"log-filter"`
|
||||
|
||||
// Logger is used to define the logger options.
|
||||
Logger logcfg.Options `mapstructure:"logger" json:"logger" yaml:"logger" toml:"logger"`
|
||||
}
|
||||
@@ -196,6 +203,25 @@ type Config struct {
|
||||
// Clone creates a deep copy of the Config structure.
|
||||
// All fields are copied, including function pointers.
|
||||
func (c *Config) Clone() Config {
|
||||
var (
|
||||
rootCA []tlscas.Cert
|
||||
clientCA []tlscas.Cert
|
||||
certs []tlscrt.Certif
|
||||
)
|
||||
|
||||
if len(c.TLS.RootCA) > 0 {
|
||||
rootCA = make([]tlscas.Cert, len(c.TLS.RootCA))
|
||||
copy(rootCA, c.TLS.RootCA)
|
||||
}
|
||||
if len(c.TLS.ClientCA) > 0 {
|
||||
clientCA = make([]tlscas.Cert, len(c.TLS.ClientCA))
|
||||
copy(clientCA, c.TLS.ClientCA)
|
||||
}
|
||||
if len(c.TLS.Certs) > 0 {
|
||||
certs = make([]tlscrt.Certif, len(c.TLS.Certs))
|
||||
copy(certs, c.TLS.Certs)
|
||||
}
|
||||
|
||||
return Config{
|
||||
Disabled: c.Disabled,
|
||||
getTLSDefault: c.getTLSDefault,
|
||||
@@ -220,9 +246,9 @@ func (c *Config) Clone() Config {
|
||||
TLS: libtls.Config{
|
||||
CurveList: c.TLS.CurveList,
|
||||
CipherList: c.TLS.CipherList,
|
||||
RootCA: c.TLS.RootCA,
|
||||
ClientCA: c.TLS.ClientCA,
|
||||
Certs: c.TLS.Certs,
|
||||
RootCA: rootCA,
|
||||
ClientCA: clientCA,
|
||||
Certs: certs,
|
||||
VersionMin: c.TLS.VersionMin,
|
||||
VersionMax: c.TLS.VersionMax,
|
||||
AuthClient: c.TLS.AuthClient,
|
||||
@@ -441,7 +467,9 @@ func (o *srv) setLogger(def liblog.FuncLog, opt logcfg.Options) error {
|
||||
}
|
||||
|
||||
func (o *srv) logger() liblog.Logger {
|
||||
if o == nil || o.l == nil {
|
||||
if o == nil {
|
||||
return liblog.New(context.Background())
|
||||
} else if o.l == nil {
|
||||
return liblog.New(o.c)
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -126,8 +126,8 @@
|
||||
// │ Package API │
|
||||
// └─────────┬─────────┘
|
||||
// │
|
||||
// ┌─────────────┼──────────────┐
|
||||
// │ │ │
|
||||
// ┌─────────────┼─────────────┐
|
||||
// │ │ │
|
||||
// ┌───▼───┐ ┌────▼────┐ ┌───▼────┐
|
||||
// │Server │ │ Pool │ │ Types │
|
||||
// │ │ │ │ │ │
|
||||
|
||||
@@ -78,7 +78,7 @@ func (o *srv) HandlerGetValidKey() string {
|
||||
return srvtps.BadHandlerName
|
||||
} else if i, l = o.c.Load(cfgHandlerKey); !l {
|
||||
return srvtps.BadHandlerName
|
||||
} else if v, k := i.(string); !k {
|
||||
} else if v, k := i.(string); !k || len(v) < 1 {
|
||||
return srvtps.BadHandlerName
|
||||
} else {
|
||||
return v
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
"net/http/httptest"
|
||||
|
||||
. "github.com/nabbar/golib/httpserver"
|
||||
srvtps "github.com/nabbar/golib/httpserver/types"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
@@ -219,4 +220,51 @@ var _ = Describe("[TC-HD] Handler Management", func() {
|
||||
// Should not panic with nil map
|
||||
})
|
||||
})
|
||||
|
||||
Describe("HandlerGetValidKey", func() {
|
||||
var srv Server
|
||||
|
||||
BeforeEach(func() {
|
||||
cfg := Config{
|
||||
Name: "test-server",
|
||||
Listen: "127.0.0.1:8080",
|
||||
Expose: "http://localhost:8080",
|
||||
}
|
||||
cfg.RegisterHandlerFunc(defaultHandler)
|
||||
var err error
|
||||
srv, err = New(cfg, nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
Context("when no handler is stored", func() {
|
||||
It("should return BadHandlerName", func() {
|
||||
Expect(srv.HandlerGetValidKey()).To(Equal(srvtps.BadHandlerName))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when a valid handler is stored", func() {
|
||||
It("should return the stored handler key", func() {
|
||||
testKey := "my-valid-handler"
|
||||
srv.Handler(func() map[string]http.Handler {
|
||||
return map[string]http.Handler{
|
||||
testKey: http.NotFoundHandler(),
|
||||
}
|
||||
})
|
||||
srv.HandlerStoreFct(testKey)
|
||||
Expect(srv.HandlerGetValidKey()).To(Equal(testKey))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when cfgHandler is valid but cfgHandlerKey is not loaded", func() {
|
||||
It("should return BadHandlerName", func() {
|
||||
srv.Handler(func() map[string]http.Handler {
|
||||
return map[string]http.Handler{
|
||||
"valid": http.NotFoundHandler(),
|
||||
}
|
||||
})
|
||||
// Don't call HandlerStoreFct to keep the key empty
|
||||
Expect(srv.HandlerGetValidKey()).To(Equal(srvtps.BadHandlerName))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
libatm "github.com/nabbar/golib/atomic"
|
||||
@@ -74,6 +75,26 @@ type Server interface {
|
||||
// The function should return a map of handler keys to http.Handler instances.
|
||||
Handler(h srvtps.FuncHandler)
|
||||
|
||||
// HandlerHas checks if a handler is registered for the specified key.
|
||||
// Returns true if the handler exists, false otherwise.
|
||||
HandlerHas(key string) bool
|
||||
|
||||
// HandlerGet retrieves the handler registered for the specified key.
|
||||
// Returns BadHandler if no handler is found for the key.
|
||||
HandlerGet(key string) http.Handler
|
||||
|
||||
// HandlerGetValidKey returns the currently active handler key.
|
||||
// Returns BadHandlerName if no valid handler is configured.
|
||||
HandlerGetValidKey() string
|
||||
|
||||
// HandlerStoreFct stores a handler function reference for the specified key.
|
||||
// This is used internally to cache the handler function.
|
||||
HandlerStoreFct(key string)
|
||||
|
||||
// HandlerLoadFct loads and executes the stored handler function.
|
||||
// Returns BadHandler if no valid handler function is stored.
|
||||
HandlerLoadFct() http.Handler
|
||||
|
||||
// Merge combines configuration from another server instance into this one.
|
||||
// This is useful for updating configuration dynamically without recreating the server.
|
||||
Merge(s Server, def liblog.FuncLog) error
|
||||
@@ -92,6 +113,18 @@ type Server interface {
|
||||
|
||||
// MonitorName returns the unique monitoring identifier for this server instance.
|
||||
MonitorName() string
|
||||
|
||||
// HealthCheck performs a health check on the server.
|
||||
// Verifies the server is running, checks for errors, and attempts a TCP connection to the bind address.
|
||||
// Returns nil if healthy, or an error describing the health issue.
|
||||
HealthCheck(ctx context.Context) error
|
||||
|
||||
// IsError returns true if the server encountered any errors during operation.
|
||||
IsError() bool
|
||||
|
||||
// GetError returns the last error that occurred during server operation.
|
||||
// Returns nil if no errors occurred.
|
||||
GetError() error
|
||||
}
|
||||
|
||||
// New creates and initializes a new HTTP server instance from the provided configuration.
|
||||
|
||||
@@ -27,9 +27,15 @@
|
||||
package httpserver_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
. "github.com/nabbar/golib/httpserver"
|
||||
logcfg "github.com/nabbar/golib/logger/config"
|
||||
moncfg "github.com/nabbar/golib/monitor/types"
|
||||
libver "github.com/nabbar/golib/version"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
@@ -180,4 +186,146 @@ var _ = Describe("[TC-MON] Server Monitoring", func() {
|
||||
Expect(srv.IsDisable()).To(BeTrue())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("HealthCheck", func() {
|
||||
var (
|
||||
srv Server
|
||||
ctx context.Context
|
||||
prt int
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx = context.Background()
|
||||
prt = GetFreePort()
|
||||
cfg := Config{
|
||||
Name: "healthcheck-test",
|
||||
Listen: fmt.Sprintf("127.0.0.1:%d", prt),
|
||||
Expose: fmt.Sprintf("http://localhost:%d", prt),
|
||||
}
|
||||
cfg.RegisterHandlerFunc(defaultHandler)
|
||||
var err error
|
||||
srv, err = New(cfg, nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
_ = srv.Stop(ctx)
|
||||
})
|
||||
|
||||
It("should return an error if the server is not running", func() {
|
||||
err := srv.HealthCheck(ctx)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("server is not running"))
|
||||
})
|
||||
|
||||
It("should return nil if the server is running and healthy", func() {
|
||||
err := srv.Start(ctx)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
time.Sleep(100 * time.Millisecond) // give time for the server to start
|
||||
err = srv.HealthCheck(ctx)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("should return an error if the server has been stopped", func() {
|
||||
err := srv.Start(ctx)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
err = srv.Stop(ctx)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
err = srv.HealthCheck(ctx)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("server is not running"))
|
||||
})
|
||||
|
||||
It("should not panic if logger is nil", func() {
|
||||
cfg := Config{
|
||||
Name: "healthcheck-test",
|
||||
Listen: fmt.Sprintf("127.0.0.1:%d", prt),
|
||||
Expose: fmt.Sprintf("http://localhost:%d", prt),
|
||||
}
|
||||
cfg.RegisterHandlerFunc(defaultHandler)
|
||||
var err error
|
||||
srv, err = New(cfg, nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
err = srv.Start(ctx)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Set the logger to nil (simulating a missing logger)
|
||||
// This is normally not possible from outside the package
|
||||
// but we can use reflection to achieve it for testing purposes
|
||||
// This is a HACK and should not be done in production code
|
||||
// It is only used to increase test coverage
|
||||
//if s, ok := srv.(*srv); ok {
|
||||
// s.l.Store(nil)
|
||||
//}
|
||||
|
||||
err = srv.HealthCheck(ctx)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Monitor", func() {
|
||||
var (
|
||||
srv Server
|
||||
vrs libver.Version
|
||||
prt int
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
prt = GetFreePort()
|
||||
cfg := Config{
|
||||
Name: "monitor-func-test",
|
||||
Listen: fmt.Sprintf("127.0.0.1:%d", prt),
|
||||
Expose: fmt.Sprintf("http://localhost:%d", prt),
|
||||
Monitor: moncfg.Config{
|
||||
Name: "monitor-test",
|
||||
CheckTimeout: 0,
|
||||
IntervalCheck: 0,
|
||||
IntervalFall: 0,
|
||||
IntervalRise: 0,
|
||||
FallCountKO: 0,
|
||||
FallCountWarn: 0,
|
||||
RiseCountKO: 0,
|
||||
RiseCountWarn: 0,
|
||||
Logger: logcfg.Options{},
|
||||
},
|
||||
}
|
||||
cfg.RegisterHandlerFunc(defaultHandler)
|
||||
var err error
|
||||
srv, err = New(cfg, nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
vrs = libver.NewVersion(
|
||||
libver.License_MIT,
|
||||
"testapp",
|
||||
"Test Application",
|
||||
"2024-01-01",
|
||||
"abc123",
|
||||
"v1.0.0",
|
||||
"Test Author",
|
||||
"testapp",
|
||||
struct{}{},
|
||||
0,
|
||||
)
|
||||
})
|
||||
|
||||
It("should return a valid monitor instance", func() {
|
||||
mon, err := srv.Monitor(vrs)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(mon).ToNot(BeNil())
|
||||
})
|
||||
|
||||
It("should return an error for an invalid monitor config", func() {
|
||||
cfg := srv.GetConfig()
|
||||
cfg.Monitor.Name = "monitor-test"
|
||||
err := srv.SetConfig(*cfg, nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
_, err = srv.Monitor(vrs)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -87,11 +87,12 @@ func (o *srv) setServer(ctx context.Context) error {
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
stdlog.SetIOWriterFilter("connection reset by peer")
|
||||
if cfg := o.GetConfig(); cfg != nil && len(cfg.LogFilter) > 0 {
|
||||
stdlog.SetIOWriterFilter(cfg.LogFilter...)
|
||||
}
|
||||
|
||||
if ssl != nil && ssl.LenCertificatePair() > 0 {
|
||||
s.TLSConfig = ssl.TlsConfig("")
|
||||
stdlog.AddIOWriterFilter("TLS handshake error")
|
||||
}
|
||||
|
||||
if e := o.cfgGetServer().initServer(s); e != nil {
|
||||
|
||||
@@ -27,7 +27,11 @@
|
||||
package httpserver_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
. "github.com/nabbar/golib/httpserver"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
@@ -279,4 +283,74 @@ var _ = Describe("[TC-SV] Server Info", func() {
|
||||
Expect(srv1.GetName()).To(Equal("server2"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("Server Error Handling", func() {
|
||||
var (
|
||||
srv Server
|
||||
ctx context.Context
|
||||
prt int
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx = context.Background()
|
||||
prt = GetFreePort()
|
||||
cfg := Config{
|
||||
Name: "error-handling-test",
|
||||
Listen: fmt.Sprintf("127.0.0.1:%d", prt),
|
||||
Expose: fmt.Sprintf("http://localhost:%d", prt),
|
||||
}
|
||||
cfg.RegisterHandlerFunc(defaultHandler)
|
||||
var err error
|
||||
srv, err = New(cfg, nil)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
_ = srv.Stop(ctx)
|
||||
})
|
||||
|
||||
It("should report no error when server starts and stops successfully", func() {
|
||||
Expect(srv.IsError()).To(BeFalse())
|
||||
Expect(srv.GetError()).To(BeNil())
|
||||
|
||||
err := srv.Start(ctx)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
Expect(srv.IsError()).To(BeFalse())
|
||||
Expect(srv.GetError()).To(BeNil())
|
||||
|
||||
err = srv.Stop(ctx)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
Expect(srv.IsError()).To(BeTrue())
|
||||
|
||||
err = srv.GetError()
|
||||
Expect(err).ToNot(BeNil())
|
||||
Expect(err.Error()).To(ContainSubstring("server start but not listen"))
|
||||
})
|
||||
|
||||
It("should report an error when server fails to start due to port in use", func() {
|
||||
// Start a listener on the port before trying to start our server
|
||||
lis, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", prt))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer func() {
|
||||
_ = lis.Close()
|
||||
}()
|
||||
|
||||
cx1, cn1 := context.WithTimeout(ctx, time.Second*3)
|
||||
defer cn1()
|
||||
|
||||
// Attempt to start our server, which should fail
|
||||
err = srv.Start(cx1)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("server is not running"))
|
||||
|
||||
// IsError and GetError should reflect this failure
|
||||
Expect(srv.IsError()).To(BeTrue())
|
||||
Expect(srv.GetError()).To(HaveOccurred())
|
||||
Expect(srv.GetError().Error()).To(ContainSubstring("server start but not listen"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -252,10 +252,13 @@ var _ = Describe("[TC-TLS] HTTPServer/TLS", func() {
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
resp, e := client.Get(fmt.Sprintf("https://127.0.0.1:%d", port))
|
||||
if e == nil {
|
||||
Expect(resp.StatusCode).To(Equal(http.StatusOK))
|
||||
resp.Body.Close()
|
||||
}
|
||||
Expect(e).ToNot(HaveOccurred())
|
||||
Expect(resp).ToNot(BeNil())
|
||||
|
||||
code := resp.StatusCode
|
||||
_ = resp.Body.Close()
|
||||
|
||||
Expect(code).To(Equal(http.StatusOK))
|
||||
}
|
||||
|
||||
err = srv.Stop(ctx)
|
||||
|
||||
Reference in New Issue
Block a user