Files
brianmcgee e3a7790e57
buildbot/nix-eval Build done.
PR Size Review Check / pr-size-review-gate (pull_request) Successful in 24s
sizelint / sizelint (pull_request) Successful in 2m12s
buildbot/nix-build Build done.
gitea-mq Already up to date with target branch
fix breaks in gofumpt and golangci-lint from updating nixpkgs
2026-07-07 16:32:14 +01:00

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))
}
}