2bf7101f2d
- .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
120 lines
3.0 KiB
Go
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()
|