117 lines
3.1 KiB
Go
117 lines
3.1 KiB
Go
// Package api wires together every HTTP-facing component of the service:
|
|
//
|
|
// - Tawhiri-compatible v1 endpoints generated from the OpenAPI spec (ogen);
|
|
// - The new v2 prediction endpoint;
|
|
// - Dataset and job admin endpoints under /api/v1/admin/;
|
|
// - Optional Prometheus-format metrics endpoint.
|
|
package api
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"predictor-refactored/internal/api/admin"
|
|
"predictor-refactored/internal/api/async"
|
|
"predictor-refactored/internal/api/middleware"
|
|
"predictor-refactored/internal/api/tawhiri"
|
|
v2 "predictor-refactored/internal/api/v2"
|
|
"predictor-refactored/internal/datasets"
|
|
"predictor-refactored/internal/elevation"
|
|
"predictor-refactored/internal/metrics"
|
|
apirest "predictor-refactored/pkg/rest"
|
|
)
|
|
|
|
// Server is the top-level HTTP server.
|
|
type Server struct {
|
|
port int
|
|
mux *http.ServeMux
|
|
log *zap.Logger
|
|
}
|
|
|
|
// Deps are the runtime dependencies the API layer needs.
|
|
type Deps struct {
|
|
Manager *datasets.Manager
|
|
Elevation *elevation.Dataset
|
|
Metrics metrics.Sink
|
|
MetricsHandler http.Handler // optional; mounted at MetricsPath when non-nil
|
|
MetricsPath string
|
|
AsyncManager *async.Manager // optional; mounts /api/v1/predictions when non-nil
|
|
Log *zap.Logger
|
|
}
|
|
|
|
// New wires the HTTP server. The returned Server is not yet started.
|
|
func New(port int, d Deps) (*Server, error) {
|
|
if d.Log == nil {
|
|
d.Log = zap.NewNop()
|
|
}
|
|
if d.Metrics == nil {
|
|
d.Metrics = metrics.Noop()
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
// ogen-generated server handles the Tawhiri-compat surface
|
|
// (GET /api/v1/prediction and GET /ready).
|
|
tw := tawhiri.New(d.Manager, d.Elevation, d.Metrics, d.Log)
|
|
ogenSrv, err := apirest.NewServer(tw, apirest.WithMiddleware(middleware.OgenLogging(d.Log)))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create ogen server: %w", err)
|
|
}
|
|
|
|
// New primary prediction endpoint.
|
|
v2h := v2.New(d.Manager, d.Elevation, d.Metrics, d.Log)
|
|
mux.Handle("/api/v2/prediction", v2h)
|
|
|
|
// Admin endpoints.
|
|
adminH := admin.New(d.Manager, d.Log)
|
|
adminH.Register(mux)
|
|
|
|
// Async prediction endpoints (optional).
|
|
if d.AsyncManager != nil {
|
|
asyncH := async.NewHandler(d.AsyncManager)
|
|
asyncH.Register(mux)
|
|
}
|
|
|
|
// Metrics endpoint.
|
|
if d.MetricsHandler != nil && d.MetricsPath != "" {
|
|
mux.Handle(d.MetricsPath, d.MetricsHandler)
|
|
}
|
|
|
|
// Fallback to the ogen-generated routes (v1 + ready) for anything else.
|
|
mux.Handle("/", ogenSrv)
|
|
|
|
return &Server{
|
|
port: port,
|
|
mux: mux,
|
|
log: d.Log,
|
|
}, nil
|
|
}
|
|
|
|
// Run starts the HTTP server and blocks until it returns.
|
|
//
|
|
// The handler chain is: CORS → request logger → mux.
|
|
func (s *Server) Run(ctx context.Context) error {
|
|
srv := &http.Server{
|
|
Addr: fmt.Sprintf(":%d", s.port),
|
|
Handler: middleware.CORS(middleware.HTTPLogging(s.log, s.mux)),
|
|
}
|
|
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
s.log.Info("HTTP server starting", zap.Int("port", s.port))
|
|
errCh <- srv.ListenAndServe()
|
|
}()
|
|
|
|
select {
|
|
case err := <-errCh:
|
|
return err
|
|
case <-ctx.Done():
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
return srv.Shutdown(shutdownCtx)
|
|
}
|
|
}
|