Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ DB_NAME=
DB_HOST=
DB_PORT=
PORT=
BIND_ADDR= # interface to bind to (default: "" = all interfaces); set to 127.0.0.1 in production, behind a reverse proxy
JWT_SECRET=

# Logging
Expand Down
50 changes: 45 additions & 5 deletions apps/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ import (
"apps/api/pkg/logger"
"apps/api/presentation/restapi"
"apps/api/utils"
"context"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"

"github.com/gorilla/mux"
)
Expand Down Expand Up @@ -117,10 +122,45 @@ func main() {
restapi.NewChecklistSessionRouter(checklistSessionHandler).AddRouter(router)
restapi.NewStockCheckRouter(stockCheckHandler).AddRouter(router)

router.HandleFunc("/health-check", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("health check success"))
})
startTime := time.Now()
router.HandleFunc("/health-check", restapi.NewHealthHandler(startTime).HealthCheck)

srv := &http.Server{
Addr: fmt.Sprintf("%s:%s", env.BindAddr, env.Port),
Handler: router,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
}

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

serveErrCh := make(chan error, 1)
go func() {
rootLogger.Info("server listening", slog.String("addr", srv.Addr))
serveErrCh <- srv.ListenAndServe()
}()

rootLogger.Info("server listening", slog.String("port", env.Port))
http.ListenAndServe(fmt.Sprintf(":%s", env.Port), router)
select {
case err := <-serveErrCh:
if err != nil && err != http.ErrServerClosed {
rootLogger.Error("server failed", slog.String("error", err.Error()))
os.Exit(1)
}
case <-ctx.Done():
stop()
rootLogger.Info("shutdown signal received, draining in-flight requests")

shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

if err := srv.Shutdown(shutdownCtx); err != nil {
rootLogger.Error("graceful shutdown failed", slog.String("error", err.Error()))
os.Exit(1)
}

rootLogger.Info("server shut down cleanly")
}
}
34 changes: 34 additions & 0 deletions apps/api/presentation/restapi/health_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package restapi

import (
"apps/api/pkg/buildinfo"
"encoding/json"
"net/http"
"time"
)

type HealthResponse struct {
Status string `json:"status"`
Version string `json:"version"`
Commit string `json:"commit"`
UptimeSeconds int64 `json:"uptime_seconds"`
}

type HealthHandler struct {
startTime time.Time
}

func NewHealthHandler(startTime time.Time) *HealthHandler {
return &HealthHandler{startTime: startTime}
}

func (h *HealthHandler) HealthCheck(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(HealthResponse{
Status: "ok",
Version: buildinfo.Version,
Commit: buildinfo.Version,
UptimeSeconds: int64(time.Since(h.startTime).Seconds()),
})
}
33 changes: 33 additions & 0 deletions apps/api/presentation/restapi/health_handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package restapi_test

import (
"apps/api/pkg/buildinfo"
"apps/api/presentation/restapi"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

func TestHealthHandler_HealthCheck(t *testing.T) {
startTime := time.Now().Add(-5 * time.Second)
handler := restapi.NewHealthHandler(startTime)

req := httptest.NewRequest(http.MethodGet, "/health-check", nil)
w := httptest.NewRecorder()
handler.HealthCheck(w, req)

assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "application/json", w.Header().Get("Content-Type"))

var body restapi.HealthResponse
err := json.Unmarshal(w.Body.Bytes(), &body)
assert.NoError(t, err)
assert.Equal(t, "ok", body.Status)
assert.Equal(t, buildinfo.Version, body.Version)
assert.Equal(t, buildinfo.Version, body.Commit)
assert.GreaterOrEqual(t, body.UptimeSeconds, int64(5))
}
1 change: 1 addition & 0 deletions apps/api/presentation/restapi/rental_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ func TestRentalHandler_CheckoutRentals(t *testing.T) {
PricingTiers: []domain.PricingTier{{UpToMinutes: 480, Price: 120000}},
}, nil)
rentalRepo.EXPECT().CheckoutRental(gomock.Any(), int64(1)).Return(nil)
variantRepo.EXPECT().GetVariantById(gomock.Any(), int64(1)).Return(domain.Variant{Id: 1, Product: domain.Product{Name: "Console"}}, nil)
txRepo.EXPECT().CreateTransaction(gomock.Any(), gomock.Any()).Return(domain.Transaction{Id: 1}, nil)
},
expectedStatus: http.StatusOK,
Expand Down
2 changes: 2 additions & 0 deletions apps/api/utils/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Env struct {
DbHost string
DbPort string
Port string
BindAddr string
JwtSecret string
LogLevel string
AppEnv string
Expand Down Expand Up @@ -46,6 +47,7 @@ func GetEnv() Env {
DbHost: os.Getenv("DB_HOST"),
DbPort: os.Getenv("DB_PORT"),
Port: os.Getenv("PORT"),
BindAddr: os.Getenv("BIND_ADDR"),
JwtSecret: os.Getenv("JWT_SECRET"),
LogLevel: logLevel,
AppEnv: appEnv,
Expand Down
Loading