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>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user