~bigbes/huntsman

huntsman/internal/app/wires.go -rw-r--r-- 3.3 KiB
766fa805 — Eugene Blikh Add BSD 2-Clause license 6 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
package app

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"net/http"
	"time"

	"go.bigb.es/auxilia/culpa"
	"go.bigb.es/auxilia/scribe"

	"sourcecraft.dev/bigbes/huntsman/internal/config"
	"sourcecraft.dev/bigbes/huntsman/internal/domain/search"
	"sourcecraft.dev/bigbes/huntsman/internal/platform/health"
	"sourcecraft.dev/bigbes/huntsman/internal/platform/observability"
	"sourcecraft.dev/bigbes/huntsman/internal/server"
)

// loggerWire builds the application's structured logger from config and
// installs it as the slog default so package-level callers pick it up.
type loggerWire struct {
	Config config.Config `config:""`

	Log *slog.Logger
}

func (w *loggerWire) Init(_ context.Context) error {
	w.Log = observability.NewLogger(w.Config.Log.Level, w.Config.Log.Format)
	slog.SetDefault(w.Log)
	return nil
}

// searchWire constructs the search service and HTTP handler. Both are
// kept on the wire so HTTP can inject the handler and other services
// can call into the service if needed later.
type searchWire struct {
	Config config.Config `config:""`

	Service *search.Service
	Handler *search.Handler
}

func (w *searchWire) Init(_ context.Context) error {
	svc, err := search.NewService(w.Config.Search.DefaultProvider, w.Config.Server.PublicURL)
	if err != nil {
		return culpa.Wrap(err, "build search service")
	}
	w.Service = svc
	w.Handler = search.NewHandler(svc)
	return nil
}

// healthWire wraps the health registry so steward owns its lifetime.
// Add upstream probes by calling Health.Register from another service's
// Init via inject:"".
type healthWire struct {
	Health *health.Health
}

func (w *healthWire) Init(_ context.Context) error {
	w.Health = health.New()
	return nil
}

// shutdownTimeout is how long http.Server.Shutdown is allowed to drain
// in-flight requests before returning.
const shutdownTimeout = 30 * time.Second

// httpServer is the root steward service. It depends on logger, search,
// and health wires (resolved by type). Init builds the chi router; Start
// runs the listener in a goroutine; Stop drains via Shutdown.
type httpServer struct {
	Config config.Config `config:""`
	Logger *loggerWire   `inject:""`
	Search *searchWire   `inject:""`
	Health *healthWire   `inject:""`

	Info BuildInfo

	srv *http.Server
}

func (s *httpServer) Init(_ context.Context) error {
	router := server.Routes(s.Logger.Log, s.Health.Health, s.Search.Handler)
	addr := fmt.Sprintf("%s:%d", s.Config.Server.Host, s.Config.Server.Port)
	s.srv = &http.Server{
		Addr:         addr,
		Handler:      router,
		ReadTimeout:  s.Config.Server.ReadTimeout,
		WriteTimeout: s.Config.Server.WriteTimeout,
		IdleTimeout:  60 * time.Second,
	}
	return nil
}

func (s *httpServer) Start(_ context.Context) error {
	go func() {
		s.Logger.Log.Info("server starting",
			"addr", s.srv.Addr,
			"version", s.Info.Version,
		)
		if err := s.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			s.Logger.Log.Error("server stopped unexpectedly",
				scribe.Err(culpa.Wrap(err, "ListenAndServe")),
			)
		}
	}()
	return nil
}

func (s *httpServer) Stop(_ context.Context) error {
	s.Logger.Log.Info("shutting down gracefully")
	shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
	defer cancel()
	if err := s.srv.Shutdown(shutdownCtx); err != nil {
		return culpa.Wrap(err, "http shutdown")
	}
	return nil
}