Files
etalon 976f5238f8 feat: scaffold app skeleton with config, ULID, health endpoints, compose stack (phase 1)
- internal .env loader (real env wins) + strictly validated config struct
- ULID package: 48-bit ms timestamp + 80-bit crypto randomness, sortable
- HTTP server with recovery/request-id/trusted-proxy/access-log middleware
- /healthz, /readyz (pluggable checkers), /metricsz (counter registry)
- multi-stage Dockerfile (alpine, non-root) + compose with healthchecks,
  app bound to 127.0.0.1:8787, mongo unpublished, graceful 15s shutdown

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 18:10:30 +02:00

68 lines
1.8 KiB
Go

package httpx
import (
"context"
"net/http"
"runtime"
"time"
)
// ReadinessCheck is a named dependency probe run by /readyz. Required checks
// (Mongo) gate readiness; optional ones (atomizer, work performer) are
// reported but non-fatal, per §6.
type ReadinessCheck struct {
Name string
Required bool
Probe func(ctx context.Context) error
}
// AddReadinessCheck registers a probe; later phases plug in Mongo and the
// external services here.
func (s *Server) AddReadinessCheck(c ReadinessCheck) {
s.checks = append(s.checks, c)
}
func (s *Server) handleHealthz(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
type checkResult struct {
OK bool `json:"ok"`
Required bool `json:"required"`
Error string `json:"error,omitempty"`
}
func (s *Server) handleReadyz(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
status := "ok"
httpStatus := http.StatusOK
results := make(map[string]checkResult, len(s.checks))
for _, c := range s.checks {
res := checkResult{OK: true, Required: c.Required}
if err := c.Probe(ctx); err != nil {
res.OK = false
res.Error = err.Error()
if c.Required {
status = "unavailable"
httpStatus = http.StatusServiceUnavailable
} else if status == "ok" {
status = "degraded"
}
}
results[c.Name] = res
}
writeJSON(w, httpStatus, map[string]any{"status": status, "checks": results})
}
func (s *Server) handleMetricsz(w http.ResponseWriter, _ *http.Request) {
counters, gauges := s.metrics.Snapshot()
writeJSON(w, http.StatusOK, map[string]any{
"uptimeSec": int64(time.Since(s.startedAt).Seconds()),
"goroutines": runtime.NumGoroutine(),
"counters": counters,
"gauges": gauges,
})
}