mirror of
https://github.com/akvorado/akvorado.git
synced 2025-12-11 22:14:02 +01:00
common/httpserver: listen on an abstract Unix socket
And make healthcheck command use it by default. This makes the healthcheck command works whatever port the user has configured for the HTTP service.
This commit is contained in:
@@ -139,7 +139,7 @@ func TestRedis(t *testing.T) {
|
||||
Server: server,
|
||||
DB: 10,
|
||||
}
|
||||
h, err := httpserver.New(r, config, httpserver.Dependencies{Daemon: daemon.NewMock(t)})
|
||||
h, err := httpserver.New(r, "cache-test", config, httpserver.Dependencies{Daemon: daemon.NewMock(t)})
|
||||
if err != nil {
|
||||
t.Fatalf("New() error:\n%+v", err)
|
||||
}
|
||||
|
||||
@@ -32,9 +32,10 @@ type Component struct {
|
||||
t tomb.Tomb
|
||||
config Configuration
|
||||
|
||||
mux *http.ServeMux
|
||||
metrics metrics
|
||||
address net.Addr
|
||||
mux *http.ServeMux
|
||||
metrics metrics
|
||||
address net.Addr
|
||||
serviceName string
|
||||
|
||||
// GinRouter is the router exposed for /api
|
||||
GinRouter *gin.Engine
|
||||
@@ -47,14 +48,15 @@ type Dependencies struct {
|
||||
}
|
||||
|
||||
// New creates a new HTTP component.
|
||||
func New(r *reporter.Reporter, configuration Configuration, dependencies Dependencies) (*Component, error) {
|
||||
func New(r *reporter.Reporter, serviceName string, configuration Configuration, dependencies Dependencies) (*Component, error) {
|
||||
c := Component{
|
||||
r: r,
|
||||
d: &dependencies,
|
||||
config: configuration,
|
||||
|
||||
mux: http.NewServeMux(),
|
||||
GinRouter: gin.New(),
|
||||
mux: http.NewServeMux(),
|
||||
serviceName: serviceName,
|
||||
GinRouter: gin.New(),
|
||||
}
|
||||
c.initMetrics()
|
||||
c.d.Daemon.Track(&c.t, "common/http")
|
||||
@@ -104,11 +106,6 @@ func (c *Component) AddHandler(location string, handler http.Handler) {
|
||||
|
||||
// Start starts the HTTP component.
|
||||
func (c *Component) Start() error {
|
||||
if c.config.Listen == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.r.Info().Msg("starting HTTP component")
|
||||
var err error
|
||||
c.cacheStore, err = c.config.Cache.Config.New()
|
||||
if err != nil {
|
||||
@@ -116,43 +113,83 @@ func (c *Component) Start() error {
|
||||
}
|
||||
server := &http.Server{Handler: c.mux}
|
||||
|
||||
// Most of the time, if we have an error, it's here!
|
||||
c.r.Info().Str("listen", c.config.Listen).Msg("starting HTTP server")
|
||||
listener, err := net.Listen("tcp", c.config.Listen)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to listen to %v: %w", c.config.Listen, err)
|
||||
}
|
||||
c.address = listener.Addr()
|
||||
server.Addr = listener.Addr().String()
|
||||
|
||||
// Start serving requests
|
||||
c.t.Go(func() error {
|
||||
if err := server.Serve(listener); err != http.ErrServerClosed {
|
||||
c.r.Err(err).Str("listen", c.config.Listen).Msg("unable to start HTTP server")
|
||||
return fmt.Errorf("unable to start HTTP server: %w", err)
|
||||
for _, lc := range []struct {
|
||||
network string
|
||||
address string
|
||||
fatal bool
|
||||
}{
|
||||
// The regular one.
|
||||
{
|
||||
network: "tcp",
|
||||
address: c.config.Listen,
|
||||
fatal: true,
|
||||
},
|
||||
// Using abstract unix sockets for healthchecking
|
||||
{
|
||||
network: "unix",
|
||||
address: "@akvorado",
|
||||
fatal: false,
|
||||
},
|
||||
{
|
||||
network: "unix",
|
||||
address: fmt.Sprintf("@akvorado/%s", c.serviceName),
|
||||
fatal: false,
|
||||
},
|
||||
} {
|
||||
if lc.address == "" {
|
||||
continue
|
||||
}
|
||||
if lc.network == "unix" && runtime.GOOS != "linux" {
|
||||
continue
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Gracefully stop when asked to
|
||||
// Most of the time, if we have an error, it's here!
|
||||
c.r.Info().Str("listen", lc.address).Msg("starting HTTP server")
|
||||
listener, err := net.Listen(lc.network, lc.address)
|
||||
if err != nil {
|
||||
if lc.fatal {
|
||||
return fmt.Errorf("unable to listen to %v: %w", c.config.Listen, err)
|
||||
}
|
||||
c.r.Info().Err(err).Msg("cannot start HTTP server")
|
||||
continue
|
||||
}
|
||||
if lc.network == "tcp" {
|
||||
c.address = listener.Addr()
|
||||
}
|
||||
server.Addr = listener.Addr().String()
|
||||
|
||||
// Serve requests
|
||||
c.t.Go(func() error {
|
||||
if err := server.Serve(listener); err != http.ErrServerClosed {
|
||||
c.r.Err(err).Str("listen", lc.address).Msg("unable to start HTTP server")
|
||||
return fmt.Errorf("unable to start HTTP server: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
// Gracefully stop when asked to
|
||||
c.t.Go(func() error {
|
||||
<-c.t.Dying()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := server.Shutdown(ctx); err != nil {
|
||||
c.r.Err(err).Msg("unable to shutdown HTTP server")
|
||||
return fmt.Errorf("unable to shutdown HTTP server: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// In case we have no server
|
||||
c.t.Go(func() error {
|
||||
<-c.t.Dying()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := server.Shutdown(ctx); err != nil {
|
||||
c.r.Err(err).Msg("unable to shutdown HTTP server")
|
||||
return fmt.Errorf("unable to shutdown HTTP server: %w", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the HTTP component
|
||||
func (c *Component) Stop() error {
|
||||
if c.config.Listen == "" {
|
||||
return nil
|
||||
}
|
||||
c.r.Info().Msg("stopping HTTP component")
|
||||
defer c.r.Info().Msg("HTTP component stopped")
|
||||
c.t.Kill(nil)
|
||||
|
||||
@@ -4,10 +4,15 @@
|
||||
package httpserver_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"akvorado/common/daemon"
|
||||
"akvorado/common/helpers"
|
||||
"akvorado/common/httpserver"
|
||||
"akvorado/common/reporter"
|
||||
@@ -91,3 +96,49 @@ func TestGinRouterPanic(t *testing.T) {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnixSocket(t *testing.T) {
|
||||
if runtime.GOOS != "linux" {
|
||||
t.Skip("unsupported OS")
|
||||
}
|
||||
r := reporter.NewMock(t)
|
||||
config := httpserver.DefaultConfiguration()
|
||||
config.Listen = ""
|
||||
h, err := httpserver.New(r, "mock-unix-test", config, httpserver.Dependencies{Daemon: daemon.NewMock(t)})
|
||||
if err != nil {
|
||||
t.Fatalf("New() error:\n%+v", err)
|
||||
}
|
||||
helpers.StartStop(t, h)
|
||||
|
||||
h.AddHandler("/test",
|
||||
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
fmt.Fprintf(w, "Hello !")
|
||||
}))
|
||||
|
||||
// We should listen to both @akvorado and @akvorado/mock-unix-test. However,
|
||||
// we may have some parallel tests and @akvorado may not be the handler we
|
||||
// configured. Let's just test the second one.
|
||||
httpc := http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(context.Context, string, string) (net.Conn, error) {
|
||||
return net.Dial("unix", "@akvorado/mock-unix-test")
|
||||
},
|
||||
},
|
||||
}
|
||||
response, err := httpc.Get("http://unix/test")
|
||||
if err != nil {
|
||||
t.Fatalf("Get() error:\n%+v", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != 200 {
|
||||
t.Errorf("Get() status = %d instead of %d", response.StatusCode, 200)
|
||||
}
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadAll() error:\n%+v", err)
|
||||
}
|
||||
expected := "Hello !"
|
||||
if diff := helpers.Diff(string(body), expected); diff != "" {
|
||||
t.Fatalf("Get() body (-got, +want):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ func NewMock(t testing.TB, r *reporter.Reporter) *Component {
|
||||
t.Helper()
|
||||
config := DefaultConfiguration()
|
||||
config.Listen = "0.0.0.0:0"
|
||||
c, err := New(r, config, Dependencies{Daemon: daemon.NewMock(t)})
|
||||
c, err := New(r, "mock", config, Dependencies{Daemon: daemon.NewMock(t)})
|
||||
if err != nil {
|
||||
t.Fatalf("New() error:\n%+v", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user