134 lines
3.2 KiB
Go
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)
|
|
}
|