144 lines
3.6 KiB
Go
144 lines
3.6 KiB
Go
package http
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"net"
|
|
"net/http"
|
|
|
|
"git.clan.lol/clan/data-mesher/pkg/cluster"
|
|
"git.clan.lol/clan/data-mesher/pkg/config"
|
|
"git.clan.lol/clan/data-mesher/pkg/state"
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/labstack/echo/v4/middleware"
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
// Server represents an Echo-based HTTP server.
|
|
type Server struct {
|
|
cfg *config.Config // app config
|
|
files *state.Files
|
|
cluster *cluster.Cluster // libp2p cluster
|
|
|
|
echo *echo.Echo // HTTP server
|
|
listeners []net.Listener // TCP listeners for multi-address support
|
|
}
|
|
|
|
// NewServer initializes and returns a new Server instance with the given configuration, store, and cluster.
|
|
// It starts the server and returns an error if it fails.
|
|
func NewServer(
|
|
cfg *config.Config,
|
|
files *state.Files,
|
|
cluster *cluster.Cluster,
|
|
) (*Server, error) {
|
|
srv := &Server{
|
|
cfg: cfg,
|
|
files: files,
|
|
cluster: cluster,
|
|
}
|
|
|
|
srv.init()
|
|
|
|
return srv, nil
|
|
}
|
|
|
|
// Listen starts the HTTP server on all configured addresses and logs the server's startup status.
|
|
// Returns an error if the server fails to start or encounters an unexpected issue during execution.
|
|
func (s *Server) Listen(ctx context.Context) error {
|
|
addrs := s.cfg.HTTP.ListenAddresses
|
|
|
|
listenCfg := net.ListenConfig{}
|
|
|
|
// create listeners for all addresses
|
|
for _, addr := range addrs {
|
|
listener, err := listenCfg.Listen(ctx, "tcp", addr)
|
|
if err != nil {
|
|
// close any listeners we already created
|
|
for _, l := range s.listeners {
|
|
_ = l.Close()
|
|
}
|
|
|
|
return fmt.Errorf("failed to listen on %s: %w", addr, err)
|
|
}
|
|
|
|
s.listeners = append(s.listeners, listener)
|
|
|
|
slog.Info("HTTP server listening", slog.String("address", addr))
|
|
}
|
|
|
|
// serve on all listeners concurrently
|
|
var eg errgroup.Group
|
|
|
|
for _, listener := range s.listeners {
|
|
eg.Go(func() error {
|
|
err := s.echo.Server.Serve(listener)
|
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
return fmt.Errorf("server error on %s: %w", listener.Addr().String(), err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// wait for all servers to finish and return the first error if any
|
|
if err := eg.Wait(); err != nil {
|
|
return fmt.Errorf("listen error: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop gracefully shuts down the server, ensuring all HTTP connections and associated tasks terminate cleanly.
|
|
func (s *Server) Stop(ctx context.Context) error {
|
|
slog.Info("shutting down HTTP server")
|
|
|
|
if err := s.echo.Shutdown(ctx); err != nil {
|
|
return fmt.Errorf("failed to stop http server: %w", err)
|
|
}
|
|
|
|
slog.Info("HTTP server shutdown")
|
|
|
|
return nil
|
|
}
|
|
|
|
// init initializes the Server's Echo framework instance, sets up middleware, and registers network and cluster routes.
|
|
func (s *Server) init() {
|
|
s.echo = echo.New()
|
|
s.echo.HideBanner = true
|
|
// todo custom logger to capture remaining output from echo
|
|
|
|
// middleware
|
|
s.echo.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
|
|
LogStatus: true,
|
|
LogURI: true,
|
|
LogError: true,
|
|
HandleError: true,
|
|
LogValuesFunc: func(ctx echo.Context, v middleware.RequestLoggerValues) error {
|
|
reqCtx := ctx.Request().Context()
|
|
|
|
if v.Error == nil {
|
|
slog.InfoContext(reqCtx, "http_request", slog.String("uri", v.URI), slog.Int("status", v.Status))
|
|
} else {
|
|
slog.ErrorContext(
|
|
reqCtx, "http_request_error",
|
|
slog.String("uri", v.URI),
|
|
slog.Int("status", v.Status),
|
|
slog.Any("err", v.Error),
|
|
)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}))
|
|
|
|
// register routes
|
|
s.initFiles()
|
|
|
|
// debug: print all registered routes
|
|
for _, r := range s.echo.Routes() {
|
|
slog.Info("registered HTTP route", slog.String("method", r.Method), slog.String("path", r.Path))
|
|
}
|
|
}
|