// Package shared provides common HTTP server bootstrap for every // Lakehouse-Go service. Each cmd/ calls Run with its name, // bind address, and a route-registration callback. The factory wires // chi, slog, /health, and graceful shutdown identically across all // five binaries — the place where uniformity beats per-service // flexibility. // // G1+ note: when queryd needs to drain a cgo DuckDB handle on // shutdown, the simple shared factory will need a per-service hook // (an io.Closer slice or an OnShutdown callback). For G0 a plain // chi.Router + http.Server.Shutdown(ctx) is sufficient. package shared import ( "context" "encoding/json" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" ) // HealthResponse is the JSON shape returned by /health on every // service. Service-specific status hooks can extend it post-G0. type HealthResponse struct { Status string `json:"status"` Service string `json:"service"` } // RegisterRoutes is the per-service callback that wires its own // routes onto the shared router AFTER /health has been mounted. type RegisterRoutes func(r chi.Router) // Run boots a chi router with slog logging, the /health endpoint, // and graceful-shutdown handling. Blocks until SIGINT/SIGTERM. func Run(serviceName, addr string, register RegisterRoutes) error { logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelInfo, })) slog.SetDefault(logger) r := chi.NewRouter() r.Use(middleware.RequestID) r.Use(middleware.RealIP) r.Use(middleware.Recoverer) r.Use(slogRequest(logger)) r.Get("/health", func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(HealthResponse{ Status: "ok", Service: serviceName, }) }) if register != nil { register(r) } srv := &http.Server{ Addr: addr, Handler: r, ReadHeaderTimeout: 10 * time.Second, } ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() errCh := make(chan error, 1) go func() { logger.Info("listening", "service", serviceName, "addr", addr) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { errCh <- err } }() select { case <-ctx.Done(): logger.Info("shutdown signal received", "service", serviceName) case err := <-errCh: return err } shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() return srv.Shutdown(shutdownCtx) } // slogRequest returns a chi middleware that logs each request via slog. // Replaces chi's default text logger so all log output stays JSON. func slogRequest(logger *slog.Logger) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) defer func() { logger.Info("http", "method", r.Method, "path", r.URL.Path, "status", ww.Status(), "dur_ms", time.Since(start).Milliseconds(), "req_id", middleware.GetReqID(r.Context()), ) }() next.ServeHTTP(ww, r) }) } }