package httpserver import ( "context" "errors" "net" "net/http" "time" "git.toki-labs.com/toki/rara/packages/go/config" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "go.uber.org/fx" "go.uber.org/zap" ) type Readiness func(context.Context) error type Params struct { fx.In Lifecycle fx.Lifecycle Config config.Config Logger *zap.Logger Registry *prometheus.Registry Readiness Readiness `optional:"true"` } func NewMux() *http.ServeMux { return http.NewServeMux() } func Start(params Params, mux *http.ServeMux) { readiness := params.Readiness if readiness == nil { readiness = func(context.Context) error { return nil } } mux.HandleFunc("GET /healthz", func(writer http.ResponseWriter, _ *http.Request) { writeJSON(writer, http.StatusOK, []byte(`{"status":"ok"}`)) }) mux.HandleFunc("GET /readyz", func(writer http.ResponseWriter, request *http.Request) { ctx, cancel := context.WithTimeout(request.Context(), 2*time.Second) defer cancel() if err := readiness(ctx); err != nil { writeJSON(writer, http.StatusServiceUnavailable, []byte(`{"status":"unavailable"}`)) return } writeJSON(writer, http.StatusOK, []byte(`{"status":"ready"}`)) }) mux.Handle("GET /metrics", promhttp.HandlerFor(params.Registry, promhttp.HandlerOpts{})) server := &http.Server{ Addr: params.Config.Service.Listen, Handler: requestLogger(params.Logger, mux), ReadHeaderTimeout: 10 * time.Second, } var listener net.Listener params.Lifecycle.Append(fx.Hook{ OnStart: func(context.Context) error { var err error listener, err = net.Listen("tcp", params.Config.Service.Listen) if err != nil { return err } params.Logger.Info( "HTTP server listening", zap.String("service", params.Config.Service.Name), zap.String("address", params.Config.Service.Listen), ) go func() { if err := server.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) { params.Logger.Error("HTTP server stopped unexpectedly", zap.Error(err)) } }() return nil }, OnStop: func(ctx context.Context) error { return server.Shutdown(ctx) }, }) } func requestLogger(logger *zap.Logger, next http.Handler) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { started := time.Now() next.ServeHTTP(writer, request) logger.Debug( "HTTP request", zap.String("method", request.Method), zap.String("path", request.URL.Path), zap.Duration("duration", time.Since(started)), ) }) } func writeJSON(writer http.ResponseWriter, status int, body []byte) { writer.Header().Set("Content-Type", "application/json") writer.WriteHeader(status) _, _ = writer.Write(body) }