✨ feat(server): add /api/healthz endpoint with rich health info (#20)
Adds Kubernetes-style /api/healthz endpoint with status/version/uptime_seconds/timestamp. Non-breaking — /api/health preserved. Includes unit test (passes locally) and BDD scenario (validated by CI). Généré ~95% en autonomie par Mistral Vibe via workspace ICM ~/Work/Vibe/workspaces/healthz-feature/. Co-authored-by: Gabriel Radureau <arcodange@gmail.com> Co-committed-by: Gabriel Radureau <arcodange@gmail.com>
This commit was merged in pull request #20.
This commit is contained in:
43
pkg/server/healthz_test.go
Normal file
43
pkg/server/healthz_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"dance-lessons-coach/pkg/config"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHandleHealthz(t *testing.T) {
|
||||
// Setup
|
||||
cfg := &config.Config{}
|
||||
s := NewServer(cfg, context.Background())
|
||||
|
||||
// Create request
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/healthz", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Call handler
|
||||
s.handleHealthz(w, req)
|
||||
|
||||
// Check status code
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
// Check content type
|
||||
assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
|
||||
|
||||
// Decode response
|
||||
var resp HealthzResponse
|
||||
err := json.NewDecoder(w.Body).Decode(&resp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Assert fields
|
||||
assert.Equal(t, "healthy", resp.Status)
|
||||
assert.NotEmpty(t, resp.Version)
|
||||
assert.GreaterOrEqual(t, resp.UptimeSeconds, int64(0))
|
||||
assert.NotZero(t, resp.Timestamp)
|
||||
}
|
||||
@@ -64,6 +64,7 @@ type Server struct {
|
||||
validator *validation.Validator
|
||||
userRepo user.UserRepository
|
||||
userService user.UserService
|
||||
startedAt time.Time
|
||||
}
|
||||
|
||||
func NewServer(cfg *config.Config, readyCtx context.Context) *Server {
|
||||
@@ -89,6 +90,7 @@ func NewServer(cfg *config.Config, readyCtx context.Context) *Server {
|
||||
validator: validator,
|
||||
userRepo: userRepo,
|
||||
userService: userService,
|
||||
startedAt: time.Now(),
|
||||
}
|
||||
s.setupRoutes()
|
||||
return s
|
||||
@@ -137,6 +139,9 @@ func (s *Server) setupRoutes() {
|
||||
// Version endpoint at root level
|
||||
s.router.Get("/api/version", s.handleVersion)
|
||||
|
||||
// Kubernetes-style health endpoint at root level
|
||||
s.router.Get("/api/healthz", s.handleHealthz)
|
||||
|
||||
// API routes
|
||||
s.router.Route("/api/v1", func(r chi.Router) {
|
||||
r.Use(s.getAllMiddlewares()...)
|
||||
@@ -358,6 +363,34 @@ func (s *Server) handleVersion(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// HealthzResponse represents the Kubernetes-style health check response
|
||||
type HealthzResponse struct {
|
||||
Status string `json:"status"`
|
||||
Version string `json:"version"`
|
||||
UptimeSeconds int64 `json:"uptime_seconds"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
// handleHealthz godoc
|
||||
//
|
||||
// @Summary Kubernetes-style health check
|
||||
// @Description Returns rich health info for liveness/readiness probes
|
||||
// @Tags System/Health
|
||||
// @Produce json
|
||||
// @Success 200 {object} HealthzResponse
|
||||
// @Router /healthz [get]
|
||||
func (s *Server) handleHealthz(w http.ResponseWriter, r *http.Request) {
|
||||
log.Trace().Msg("Healthz check requested")
|
||||
resp := HealthzResponse{
|
||||
Status: "healthy",
|
||||
Version: version.Version,
|
||||
UptimeSeconds: int64(time.Since(s.startedAt).Seconds()),
|
||||
Timestamp: time.Now().UTC(),
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
func (s *Server) Router() http.Handler {
|
||||
return s.router
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user