Files
glchat/internal/server/server.go
T
grendervill 2bf7101f2d chore(lint): настроить golangci-lint v2, gofumpt и форматирование в make
- .golangci.yml: набор линтеров под требования §14 (gosec, gocritic, sloglint,
  noctx, revive, depguard для запрета циклических зависимостей internal/*)
- gofumpt вынесен из formatters golangci-lint v2.13 (сообщал о расхождениях,
  которые сам же не устранял) и применяется отдельным инструментом
- make fmt/go-lint: проверка gofumpt + golangci-lint fmt --diff
- scripts/gitleaks-scan.sh: поддержка gitleaks 8.19+ (dir) и старых версий (detect)
- исправлены замечания линтеров: обработка ошибок Close, контекст в логах,
  NewRequestWithContext в тестах, комментарий к blank-import драйвера SQLite
2026-09-19 18:25:20 +03:00

120 lines
3.0 KiB
Go

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, r *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.ErrorContext(r.Context(), "write openapi document", slog.Any("error", err))
}
}
var startedAt = time.Now()