package server import ( "context" "log/slog" "net/http" "time" "glchat/internal/config" "glchat/internal/database" "glchat/internal/httpx" "glchat/internal/meta" ) type Server struct { cfg config.Config db *database.DB logger *slog.Logger http *http.Server static *staticHandler patterns []string } func New(cfg config.Config, db *database.DB, logger *slog.Logger) *Server { s := &Server{ cfg: cfg, db: db, logger: logger, patterns: append([]string(nil), routePatterns...), static: newStaticHandler(cfg.WebRoot), } mux := http.NewServeMux() s.routes(mux) handler := httpx.Chain(mux, httpx.SecurityHeaders(cfg.FilesDomain), httpx.RequestID, httpx.Logger(logger), httpx.Recoverer(logger), httpx.JSONBodyLimit(1<<20), ) s.http = &http.Server{ Addr: cfg.ListenAddr, Handler: handler, ReadHeaderTimeout: 10 * time.Second, ReadTimeout: 30 * time.Second, WriteTimeout: 60 * time.Second, IdleTimeout: 120 * time.Second, MaxHeaderBytes: 1 << 16, } return s } var routePatterns = []string{ "GET /healthz", "GET /readyz", "GET /api/v1/meta", "GET /api/v1/openapi.json", "/", } func (s *Server) routes(mux *http.ServeMux) { mux.HandleFunc("GET /healthz", s.handleHealthz) mux.HandleFunc("GET /readyz", s.handleReadyz) mux.HandleFunc("GET /api/v1/meta", s.handleMeta) mux.HandleFunc("GET /api/v1/openapi.json", s.handleOpenAPI) mux.HandleFunc("/", s.handleFallback) } func (s *Server) Handler() http.Handler { return s.http.Handler } func (s *Server) ListenAndServe() error { return s.http.ListenAndServe() } func (s *Server) Shutdown(ctx context.Context) error { return s.http.Shutdown(ctx) } func (s *Server) handleHealthz(w http.ResponseWriter, _ *http.Request) { httpx.WriteJSON(w, http.StatusOK, map[string]any{ "status": "ok", "version": s.cfg.Version, "uptime": time.Since(startedAt).Round(time.Second).String(), }) } func (s *Server) handleReadyz(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second) defer cancel() checks := map[string]string{"database": "ok"} ready := true if err := s.db.Ready(ctx); err != nil { checks["database"] = "failed" ready = false s.logger.WarnContext(ctx, "readiness check failed", slog.Any("error", err)) } status := http.StatusOK state := "ready" if !ready { status = http.StatusServiceUnavailable state = "unavailable" } httpx.WriteJSON(w, status, map[string]any{ "status": state, "version": s.cfg.Version, "checks": checks, }) } func (s *Server) handleMeta(w http.ResponseWriter, r *http.Request) { httpx.WriteJSON(w, http.StatusOK, meta.New(s.cfg)) } func (s *Server) handleOpenAPI(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusOK) if _, err := w.Write(openAPIDocument); err != nil { s.logger.Error("write openapi document", slog.Any("error", err)) } } var startedAt = time.Now()