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:
etalon
2026-06-12 18:10:30 +02:00
commit 976f5238f8
26 changed files with 2478 additions and 0 deletions
+78
View File
@@ -0,0 +1,78 @@
package httpx
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"time"
"bountyboard/internal/config"
"bountyboard/internal/metrics"
)
// shutdownBudget is the §12 graceful-shutdown drain window.
const shutdownBudget = 15 * time.Second
type Server struct {
cfg *config.Config
log *slog.Logger
metrics *metrics.Registry
checks []ReadinessCheck
startedAt time.Time
httpSrv *http.Server
}
func New(cfg *config.Config, log *slog.Logger, reg *metrics.Registry) *Server {
s := &Server{
cfg: cfg,
log: log,
metrics: reg,
startedAt: time.Now(),
}
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", s.handleHealthz)
mux.HandleFunc("GET /readyz", s.handleReadyz)
mux.HandleFunc("GET /metricsz", s.handleMetricsz)
s.httpSrv = &http.Server{
Addr: fmt.Sprintf(":%d", cfg.AppPort),
Handler: s.withMiddleware(mux),
ReadHeaderTimeout: 10 * time.Second,
ErrorLog: slog.NewLogLogger(log.Handler(), slog.LevelWarn),
}
return s
}
// Handler exposes the full middleware-wrapped handler for tests.
func (s *Server) Handler() http.Handler { return s.httpSrv.Handler }
// Run serves until ctx is canceled (SIGTERM/SIGINT), then drains in-flight
// requests within the shutdown budget.
func (s *Server) Run(ctx context.Context) error {
errCh := make(chan error, 1)
go func() {
s.log.Info("http server listening", "addr", s.httpSrv.Addr, "baseUrl", s.cfg.AppBaseURL)
if err := s.httpSrv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
errCh <- err
return
}
errCh <- nil
}()
select {
case err := <-errCh:
return fmt.Errorf("http server: %w", err)
case <-ctx.Done():
}
s.log.Info("shutting down", "budget", shutdownBudget.String())
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownBudget)
defer cancel()
if err := s.httpSrv.Shutdown(shutdownCtx); err != nil {
return fmt.Errorf("graceful shutdown: %w", err)
}
return <-errCh
}