Organize backend into product executable and shared Go modules

This commit is contained in:
Stephan D committed 2026-09-12 14:14:08 +02:00
1 parent 981e84a4b4
commit f0720ec66b
382 files changed
+1944 -6989

No files matched your search

+18
View File
@@ -0,0 +1,18 @@
package routers
import (
"git.sendico.io/tech/sxchnge/product/app/health/internal"
"git.sendico.io/tech/sxchnge/product/app/health/status"
"git.sendico.io/tech/sxchnge/shared/logging"
"github.com/go-chi/chi/v5"
)
type Health interface {
SetStatus(status health.ServiceStatus)
SetStatusWithReason(status health.ServiceStatus, reason string)
Finish()
}
func NewHealthRouter(logger mlogger.Logger, router chi.Router, endpoint string) (Health, error) {
return healthimp.NewRouter(logger, router, endpoint), nil
}
@@ -0,0 +1,76 @@
package healthimp
import (
"net/http"
"strings"
"git.sendico.io/tech/sxchnge/product/app/health/status"
"git.sendico.io/tech/sxchnge/shared/logging"
"github.com/go-chi/chi/v5"
"go.uber.org/zap"
)
type Router struct {
logger mlogger.Logger
status *Status
}
func (hr *Router) SetStatus(status health.ServiceStatus) {
hr.SetStatusWithReason(status, defaultReason(status))
}
func (hr *Router) SetStatusWithReason(status health.ServiceStatus, reason string) {
previous, changed := hr.status.transition(status)
if !changed {
return
}
reason = strings.TrimSpace(reason)
if reason == "" {
reason = defaultReason(status)
}
hr.logger.Info("Health status changed",
zap.String("previous_status", string(previous)),
zap.String("status", string(status)),
zap.String("reason", reason),
)
}
func (hr *Router) Finish() {
hr.status.Finish()
hr.logger.Debug("Stopped")
}
func (hr *Router) handle(w http.ResponseWriter, r *http.Request) {
hr.status.healthHandler()(w, r)
}
func NewRouter(logger mlogger.Logger, router chi.Router, endpoint string) *Router {
hr := Router{
logger: logger.Named("health_check"),
}
hr.status = StatusHandler(hr.logger)
logger.Debug("Installing healthcheck middleware...")
router.Group(func(r chi.Router) {
ep := endpoint + "/health"
r.Get(ep, hr.handle)
logger.Info("Health handler installed", zap.String("endpoint", ep))
})
return &hr
}
func defaultReason(status health.ServiceStatus) string {
switch status {
case health.SSCreated:
return "service created"
case health.SSStarting:
return "service is starting"
case health.SSRunning:
return "service is ready"
case health.SSTerminating:
return "service is shutting down"
default:
return "service health status changed"
}
}
@@ -0,0 +1,53 @@
package healthimp
import (
"testing"
"git.sendico.io/tech/sxchnge/product/app/health/status"
"github.com/go-chi/chi/v5"
"go.uber.org/zap"
"go.uber.org/zap/zaptest/observer"
)
func TestSetStatusLogsOnlyTransitionsWithReason(t *testing.T) {
core, observed := observer.New(zap.InfoLevel)
router := NewRouter(zap.New(core), chi.NewRouter(), "")
router.SetStatusWithReason(health.SSStarting, "observability endpoint initialized")
router.SetStatusWithReason(health.SSStarting, "duplicate readiness report")
router.SetStatusWithReason(health.SSRunning, "all configured ingestion drivers are ready")
router.SetStatus(health.SSRunning)
entries := observed.FilterMessage("Health status changed").All()
if len(entries) != 2 {
t.Fatalf("health transition log count = %d, want 2", len(entries))
}
assertTransition(t, entries[0].ContextMap(), "created", "starting", "observability endpoint initialized")
assertTransition(t, entries[1].ContextMap(), "starting", "ok", "all configured ingestion drivers are ready")
}
func TestSetStatusUsesNonEmptyDefaultReason(t *testing.T) {
core, observed := observer.New(zap.InfoLevel)
router := NewRouter(zap.New(core), chi.NewRouter(), "")
router.SetStatusWithReason(health.SSTerminating, " ")
entries := observed.FilterMessage("Health status changed").All()
if len(entries) != 1 {
t.Fatalf("health transition log count = %d, want 1", len(entries))
}
assertTransition(t, entries[0].ContextMap(), "created", "deactivating", "service is shutting down")
}
func assertTransition(t *testing.T, fields map[string]any, previous, status, reason string) {
t.Helper()
for key, want := range map[string]string{
"previous_status": previous,
"status": status,
"reason": reason,
} {
if got, ok := fields[key].(string); !ok || got != want {
t.Fatalf("%s = %v, want %q", key, fields[key], want)
}
}
}
@@ -0,0 +1,50 @@
package healthimp
import (
"net/http"
"sync"
"git.sendico.io/tech/sxchnge/product/app/health/status"
"git.sendico.io/tech/sxchnge/shared/http/response"
"git.sendico.io/tech/sxchnge/shared/logging"
)
type Status struct {
logger mlogger.Logger
mu sync.RWMutex
status health.ServiceStatus
}
func (hs *Status) healthHandler() http.HandlerFunc {
hs.mu.RLock()
status := hs.status
hs.mu.RUnlock()
return response.Ok(hs.logger, struct {
Status health.ServiceStatus `json:"status"`
}{
status,
})
}
func (hr *Status) Finish() {
hr.logger.Info("Finished")
}
func (hs *Status) transition(status health.ServiceStatus) (health.ServiceStatus, bool) {
hs.mu.Lock()
defer hs.mu.Unlock()
previous := hs.status
if previous == status {
return previous, false
}
hs.status = status
return previous, true
}
func StatusHandler(logger mlogger.Logger) *Status {
hs := Status{
status: health.SSCreated,
logger: logger.Named("status"),
}
return &hs
}
@@ -0,0 +1,10 @@
package health
type ServiceStatus string
const (
SSCreated ServiceStatus = "created"
SSStarting ServiceStatus = "starting"
SSRunning ServiceStatus = "ok"
SSTerminating ServiceStatus = "deactivating"
)