package httpserver import ( "context" "encoding/json" "errors" "log/slog" "net/http" "sync/atomic" "time" ) // Server 提供 /health 和 /ready HTTP 探针。 type Server struct { appName string addr string shutdownTimeout time.Duration logger *slog.Logger drainHook DrainHook ready atomic.Bool } // DrainHook 在服务摘除 readiness 后执行,例如注销注册中心节点。 type DrainHook interface { OnDeregister(context.Context) error } // New 创建探针 HTTP 服务。 func New(appName string, addr string, shutdownTimeout time.Duration, logger *slog.Logger, drainHook DrainHook) *Server { server := &Server{ appName: appName, addr: addr, shutdownTimeout: shutdownTimeout, logger: logger, drainHook: drainHook, } server.ready.Store(true) return server } // Run 启动 HTTP 服务并在退出时优雅关闭。 func (s *Server) Run(ctx context.Context) error { mux := http.NewServeMux() mux.HandleFunc("/health", s.handleHealth) mux.HandleFunc("/heath", s.handleHealth) mux.HandleFunc("/ready", s.handleReady) httpServer := &http.Server{ Addr: s.addr, Handler: mux, ReadHeaderTimeout: 5 * time.Second, } go func() { <-ctx.Done() s.ready.Store(false) shutdownCtx, cancel := context.WithTimeout(context.Background(), s.shutdownTimeout) defer cancel() if s.drainHook != nil { if err := s.drainHook.OnDeregister(shutdownCtx); err != nil { s.logger.Error("run deregister hook failed", "error", err) } } _ = httpServer.Shutdown(shutdownCtx) }() s.logger.Info("pay http server started", "addr", s.addr) err := httpServer.ListenAndServe() if errors.Is(err, http.ErrServerClosed) { return nil } return err } func (s *Server) handleHealth(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, map[string]string{ "status": "ok", "service": s.appName, }) } func (s *Server) handleReady(w http.ResponseWriter, _ *http.Request) { if !s.ready.Load() { writeJSON(w, http.StatusServiceUnavailable, map[string]string{ "code": "not_ready", "message": "service is shutting down", }) return } writeJSON(w, http.StatusOK, map[string]string{ "status": "ready", "service": s.appName, }) } func writeJSON(w http.ResponseWriter, statusCode int, payload any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(statusCode) _ = json.NewEncoder(w).Encode(payload) }