ariadne/internal/platform/httpserver/server.go
2026-07-24 05:45:04 +09:00

134 lines
3.2 KiB
Go

package httpserver
import (
"context"
"encoding/json"
"errors"
"net"
"net/http"
"time"
"git.toki-labs.com/toki/ariadne/internal/platform/config"
"go.uber.org/fx"
"go.uber.org/zap"
)
type BuildInfo struct {
Version string
}
type Readiness interface {
Check(context.Context) error
}
type Server struct {
httpServer *http.Server
readiness Readiness
logger *zap.Logger
buildInfo BuildInfo
shutdownTimeout time.Duration
}
func New(
cfg config.Config,
readiness Readiness,
logger *zap.Logger,
buildInfo BuildInfo,
) *Server {
server := &Server{
readiness: readiness,
logger: logger,
buildInfo: buildInfo,
shutdownTimeout: cfg.ShutdownTimeout,
}
server.httpServer = &http.Server{
Addr: cfg.HTTPAddress,
Handler: server.routes(),
ReadHeaderTimeout: 5 * time.Second,
IdleTimeout: 60 * time.Second,
}
return server
}
func Register(lifecycle fx.Lifecycle, server *Server) {
lifecycle.Append(fx.Hook{
OnStart: server.start,
OnStop: server.stop,
})
}
func (server *Server) start(context.Context) error {
listener, err := net.Listen("tcp", server.httpServer.Addr)
if err != nil {
return err
}
server.logger.Info("HTTP server listening", zap.String("address", listener.Addr().String()))
go func() {
err := server.httpServer.Serve(listener)
if err != nil && !errors.Is(err, http.ErrServerClosed) {
server.logger.Error("HTTP server stopped unexpectedly", zap.Error(err))
}
}()
return nil
}
func (server *Server) stop(context.Context) error {
ctx, cancel := context.WithTimeout(context.Background(), server.shutdownTimeout)
defer cancel()
return server.httpServer.Shutdown(ctx)
}
func (server *Server) routes() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", server.health)
mux.HandleFunc("GET /readyz", server.ready)
return securityHeaders(mux)
}
func (server *Server) health(writer http.ResponseWriter, _ *http.Request) {
writeJSON(writer, http.StatusOK, map[string]any{
"service": "ariadne",
"status": "ok",
"version": server.buildInfo.Version,
})
}
func (server *Server) ready(writer http.ResponseWriter, request *http.Request) {
ctx, cancel := context.WithTimeout(request.Context(), 2*time.Second)
defer cancel()
if err := server.readiness.Check(ctx); err != nil {
writeJSON(writer, http.StatusServiceUnavailable, map[string]any{
"service": "ariadne",
"status": "not_ready",
"checks": map[string]string{
"database": "not_ready",
},
})
return
}
writeJSON(writer, http.StatusOK, map[string]any{
"service": "ariadne",
"status": "ready",
"checks": map[string]string{
"database": "ready",
},
})
}
func securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'")
writer.Header().Set("X-Content-Type-Options", "nosniff")
writer.Header().Set("X-Frame-Options", "DENY")
next.ServeHTTP(writer, request)
})
}
func writeJSON(writer http.ResponseWriter, status int, body any) {
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(status)
_ = json.NewEncoder(writer).Encode(body)
}