mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-21 23:22:34 +01:00
feat(api): add /health endpoint with Docker healthchecks
Allow Docker to distinguish a running container from one actually serving traffic by adding a /health endpoint that pings the database. Returns 200 when healthy, 503 when the DB connection is lost. Add HEALTHCHECK to Dockerfile and healthcheck config to the server service in docker-compose.yml. Also add start_period to the existing db healthcheck for consistency.
This commit is contained in:
@@ -26,6 +26,7 @@ type Config struct {
|
||||
type APIServer struct {
|
||||
sync.Mutex
|
||||
logger *zap.Logger
|
||||
db *sqlx.DB
|
||||
erupeConfig *cfg.Config
|
||||
userRepo APIUserRepo
|
||||
charRepo APICharacterRepo
|
||||
@@ -38,6 +39,7 @@ type APIServer struct {
|
||||
func NewAPIServer(config *Config) *APIServer {
|
||||
s := &APIServer{
|
||||
logger: config.Logger,
|
||||
db: config.DB,
|
||||
erupeConfig: config.ErupeConfig,
|
||||
httpServer: &http.Server{},
|
||||
}
|
||||
@@ -61,6 +63,7 @@ func (s *APIServer) Start() error {
|
||||
r.HandleFunc("/character/export", s.ExportSave)
|
||||
r.HandleFunc("/api/ss/bbs/upload.php", s.ScreenShot)
|
||||
r.HandleFunc("/api/ss/bbs/{id}", s.ScreenShotGet)
|
||||
r.HandleFunc("/health", s.Health)
|
||||
r.HandleFunc("/version", s.Version)
|
||||
handler := handlers.CORS(handlers.AllowedHeaders([]string{"Content-Type"}))(r)
|
||||
s.httpServer.Handler = handlers.LoggingHandler(os.Stdout, handler)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
@@ -443,3 +444,30 @@ func (s *APIServer) ScreenShot(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
writeResult("200")
|
||||
}
|
||||
|
||||
// Health handles GET /health, returning the server's health status.
|
||||
// It pings the database to verify connectivity.
|
||||
func (s *APIServer) Health(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if s.db == nil {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "unhealthy",
|
||||
"error": "database not configured",
|
||||
})
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
|
||||
defer cancel()
|
||||
if err := s.db.PingContext(ctx); err != nil {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "unhealthy",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ok",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -572,6 +572,44 @@ func TestNewAuthDataTimestamps(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestHealthEndpointNoDB tests the /health endpoint when no database is configured.
|
||||
func TestHealthEndpointNoDB(t *testing.T) {
|
||||
logger := NewTestLogger(t)
|
||||
defer func() { _ = logger.Sync() }()
|
||||
|
||||
server := &APIServer{
|
||||
logger: logger,
|
||||
erupeConfig: NewTestConfig(),
|
||||
db: nil,
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/health", nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
server.Health(recorder, req)
|
||||
|
||||
if recorder.Code != http.StatusServiceUnavailable {
|
||||
t.Errorf("Expected status %d, got %d", http.StatusServiceUnavailable, recorder.Code)
|
||||
}
|
||||
|
||||
if contentType := recorder.Header().Get("Content-Type"); contentType != "application/json" {
|
||||
t.Errorf("Content-Type = %v, want application/json", contentType)
|
||||
}
|
||||
|
||||
var resp map[string]string
|
||||
if err := json.NewDecoder(recorder.Body).Decode(&resp); err != nil {
|
||||
t.Fatalf("Failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if resp["status"] != "unhealthy" {
|
||||
t.Errorf("status = %q, want %q", resp["status"], "unhealthy")
|
||||
}
|
||||
|
||||
if resp["error"] != "database not configured" {
|
||||
t.Errorf("error = %q, want %q", resp["error"], "database not configured")
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkLauncherEndpoint benchmarks the launcher endpoint
|
||||
func BenchmarkLauncherEndpoint(b *testing.B) {
|
||||
logger, _ := zap.NewDevelopment()
|
||||
|
||||
Reference in New Issue
Block a user