Skip to content

Commit 2ea6576

Browse files
committed
[baseserver] Dedicated debug server
1 parent ca76283 commit 2ea6576

File tree

8 files changed

+238
-85
lines changed

8 files changed

+238
-85
lines changed

components/common-go/baseserver/options.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ type config struct {
1818

1919
// hostname is the hostname on which our servers will listen.
2020
hostname string
21+
// debugPort is the port we listen on for metrics, pprof, readiness and livenss checks
22+
debugPort int
2123
// grpcPort is the port we listen on for gRPC traffic
2224
grpcPort int
2325
// httpPort is the port we listen on for HTTP traffic
@@ -36,8 +38,9 @@ func defaultConfig() *config {
3638
return &config{
3739
logger: log.New(),
3840
hostname: "localhost",
39-
httpPort: 9000,
40-
grpcPort: 9001,
41+
httpPort: -1, // disabled by default
42+
grpcPort: -1, // disabled by default
43+
debugPort: 9500,
4144
closeTimeout: 5 * time.Second,
4245
healthHandler: healthcheck.NewHandler(),
4346
}
@@ -52,24 +55,29 @@ func WithHostname(hostname string) Option {
5255
}
5356
}
5457

58+
// WithHTTPPort sets the port to use for an HTTP server. Setting WithHTTPPort also enables an HTTP server on the baseserver.
5559
func WithHTTPPort(port int) Option {
5660
return func(cfg *config) error {
57-
if port < 0 {
58-
return fmt.Errorf("http must not be negative, got: %d", port)
59-
}
60-
6161
cfg.httpPort = port
6262
return nil
6363
}
6464
}
6565

66+
// WithGRPCPort sets the port to use for an HTTP server. Setting WithGRPCPort also enables a gRPC server on the baseserver.
6667
func WithGRPCPort(port int) Option {
68+
return func(cfg *config) error {
69+
cfg.grpcPort = port
70+
return nil
71+
}
72+
}
73+
74+
func WithDebugPort(port int) Option {
6775
return func(cfg *config) error {
6876
if port < 0 {
6977
return fmt.Errorf("grpc port must not be negative, got: %d", port)
7078
}
7179

72-
cfg.grpcPort = port
80+
cfg.debugPort = port
7381
return nil
7482
}
7583
}

components/common-go/baseserver/options_test.go

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ func TestOptions(t *testing.T) {
1717
logger := log.New()
1818
httpPort := 8080
1919
grpcPort := 8081
20+
debugPort := 8082
2021
timeout := 10 * time.Second
2122
hostname := "another_hostname"
2223
registry := prometheus.NewRegistry()
2324
health := healthcheck.NewHandler()
2425

2526
var opts = []Option{
2627
WithHostname(hostname),
28+
WithDebugPort(debugPort),
2729
WithHTTPPort(httpPort),
2830
WithGRPCPort(grpcPort),
2931
WithLogger(logger),
@@ -39,32 +41,41 @@ func TestOptions(t *testing.T) {
3941
hostname: hostname,
4042
grpcPort: grpcPort,
4143
httpPort: httpPort,
44+
debugPort: debugPort,
4245
closeTimeout: timeout,
4346
metricsRegistry: registry,
4447
healthHandler: health,
4548
}, cfg)
4649
}
4750

48-
func TestWithTTPPort(t *testing.T) {
49-
t.Run("negative", func(t *testing.T) {
50-
_, err := evaluateOptions(defaultConfig(), WithHTTPPort(-1))
51-
require.Error(t, err)
52-
})
51+
func TestWithHTTPPort(t *testing.T) {
52+
cfg, err := evaluateOptions(defaultConfig(), WithHTTPPort(-1))
53+
require.NoError(t, err)
54+
require.Equal(t, -1, cfg.httpPort)
5355

54-
t.Run("zero", func(t *testing.T) {
55-
_, err := evaluateOptions(defaultConfig(), WithHTTPPort(0))
56-
require.NoError(t, err)
57-
})
56+
cfg, err = evaluateOptions(defaultConfig(), WithHTTPPort(0))
57+
require.NoError(t, err)
58+
require.Equal(t, 0, cfg.httpPort)
5859
}
5960

6061
func TestWithGRPCPort(t *testing.T) {
62+
cfg, err := evaluateOptions(defaultConfig(), WithGRPCPort(-1))
63+
require.NoError(t, err)
64+
require.Equal(t, -1, cfg.grpcPort)
65+
66+
cfg, err = evaluateOptions(defaultConfig(), WithGRPCPort(0))
67+
require.NoError(t, err)
68+
require.Equal(t, 0, cfg.grpcPort)
69+
}
70+
71+
func TestWithDebugPort(t *testing.T) {
6172
t.Run("negative", func(t *testing.T) {
62-
_, err := evaluateOptions(defaultConfig(), WithGRPCPort(-1))
73+
_, err := evaluateOptions(defaultConfig(), WithDebugPort(-1))
6374
require.Error(t, err)
6475
})
6576

6677
t.Run("zero", func(t *testing.T) {
67-
_, err := evaluateOptions(defaultConfig(), WithGRPCPort(0))
78+
_, err := evaluateOptions(defaultConfig(), WithDebugPort(0))
6879
require.NoError(t, err)
6980
})
7081
}

components/common-go/baseserver/server.go

Lines changed: 128 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -31,28 +31,57 @@ func New(name string, opts ...Option) (*Server, error) {
3131
cfg: cfg,
3232
}
3333

34-
if initErr := server.initializeHTTP(); initErr != nil {
35-
return nil, fmt.Errorf("failed to initialize http server: %w", initErr)
34+
if initErr := server.initializeDebug(); initErr != nil {
35+
return nil, fmt.Errorf("failed to initialize debug server: %w", initErr)
3636
}
37-
if initErr := server.initializeGRPC(); initErr != nil {
38-
return nil, fmt.Errorf("failed to initialize grpc server: %w", initErr)
37+
38+
if server.isHTTPEnabled() {
39+
if initErr := server.initializeHTTP(); initErr != nil {
40+
return nil, fmt.Errorf("failed to initialize http server: %w", initErr)
41+
}
42+
}
43+
44+
if server.isGRPCEnabled() {
45+
if initErr := server.initializeGRPC(); initErr != nil {
46+
return nil, fmt.Errorf("failed to initialize grpc server: %w", initErr)
47+
}
3948
}
4049

4150
return server, nil
4251
}
4352

53+
// Server is a packaged server with batteries included. It is designed to be standard across components where it makes sense.
54+
// Server implements graceful shutdown making it suitable for usage in integration tests. See server_test.go.
55+
//
56+
// Server is composed of the following:
57+
// * Debug server which serves observability and debug endpoints
58+
// - /metrics for Prometheus metrics
59+
// - /pprof for Golang profiler
60+
// - /ready for kubernetes readiness check
61+
// - /live for kubernetes liveness check
62+
// * (optional) gRPC server with standard interceptors and configuration
63+
// - Started when baseserver is configured WithGRPCPort (port is non-negative)
64+
// - Use Server.GRPC() to get access to the underlying grpc.Server and register services
65+
// * (optional) HTTP server
66+
// - Currently does not come with any standard HTTP middlewares
67+
// - Started when baseserver is configured WithHTTPPort (port is non-negative)
68+
// - Use Server.HTTPMux() to get access to the root handler and register your endpoints
4469
type Server struct {
4570
// Name is the name of this server, used for logging context
4671
Name string
4772

4873
cfg *config
4974

50-
// http is an http Server
75+
// debug is an HTTP server for debug endpoints - metrics, pprof, readiness & liveness.
76+
debug *http.Server
77+
debugListener net.Listener
78+
79+
// http is an http Server, only used when port is specified in cfg
5180
http *http.Server
5281
httpMux *http.ServeMux
5382
httpListener net.Listener
5483

55-
// grpc is a grpc Server
84+
// grpc is a grpc Server, only used when port is specified in cfg
5685
grpc *grpc.Server
5786
grpcListener net.Listener
5887

@@ -62,25 +91,34 @@ type Server struct {
6291

6392
func (s *Server) ListenAndServe() error {
6493
var err error
65-
s.grpcListener, err = net.Listen("tcp", fmt.Sprintf(":%d", s.cfg.grpcPort))
94+
95+
s.debugListener, err = net.Listen("tcp", fmt.Sprintf(":%d", s.cfg.debugPort))
6696
if err != nil {
67-
return fmt.Errorf("failed to acquire port %d", s.cfg.grpcPort)
97+
return fmt.Errorf("failed to acquire port %d", s.cfg.debugPort)
6898
}
6999

70-
s.httpListener, err = net.Listen("tcp", fmt.Sprintf(":%d", s.cfg.httpPort))
71-
if err != nil {
72-
return fmt.Errorf("failed to acquire port %d", s.cfg.httpPort)
100+
if s.isGRPCEnabled() {
101+
s.grpcListener, err = net.Listen("tcp", fmt.Sprintf(":%d", s.cfg.grpcPort))
102+
if err != nil {
103+
return fmt.Errorf("failed to acquire port %d", s.cfg.grpcPort)
104+
}
105+
}
106+
107+
if s.isHTTPEnabled() {
108+
s.httpListener, err = net.Listen("tcp", fmt.Sprintf(":%d", s.cfg.httpPort))
109+
if err != nil {
110+
return fmt.Errorf("failed to acquire port %d", s.cfg.httpPort)
111+
}
73112
}
74113

75114
errors := make(chan error)
76115
defer close(errors)
77116
s.listening = make(chan struct{})
78117

118+
// Always start the debug server, we should always have metrics and other debug information.
79119
go func() {
80-
s.Logger().
81-
WithField("protocol", "grpc").
82-
Infof("Serving gRPC on %s", s.grpcListener.Addr().String())
83-
if serveErr := s.grpc.Serve(s.grpcListener); serveErr != nil {
120+
s.Logger().WithField("protocol", "http").Infof("Serving debug server on %s", s.debugListener.Addr().String())
121+
if serveErr := s.debug.Serve(s.debugListener); serveErr != nil {
84122
if s.isClosing() {
85123
return
86124
}
@@ -89,18 +127,31 @@ func (s *Server) ListenAndServe() error {
89127
}
90128
}()
91129

92-
go func() {
93-
s.Logger().
94-
WithField("protocol", "http").
95-
Infof("Serving http on %s", s.httpListener.Addr().String())
96-
if serveErr := s.http.Serve(s.httpListener); serveErr != nil {
97-
if s.isClosing() {
98-
return
130+
if s.isGRPCEnabled() {
131+
go func() {
132+
s.Logger().WithField("protocol", "grpc").Infof("Serving gRPC on %s", s.grpcListener.Addr().String())
133+
if serveErr := s.grpc.Serve(s.grpcListener); serveErr != nil {
134+
if s.isClosing() {
135+
return
136+
}
137+
138+
errors <- serveErr
99139
}
140+
}()
141+
}
100142

101-
errors <- serveErr
102-
}
103-
}()
143+
if s.isHTTPEnabled() {
144+
go func() {
145+
s.Logger().WithField("protocol", "http").Infof("Serving http on %s", s.httpListener.Addr().String())
146+
if serveErr := s.http.Serve(s.httpListener); serveErr != nil {
147+
if s.isClosing() {
148+
return
149+
}
150+
151+
errors <- serveErr
152+
}
153+
}()
154+
}
104155

105156
signals := make(chan os.Signal, 1)
106157
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
@@ -157,6 +208,15 @@ func (s *Server) GRPCAddress() string {
157208
return fmt.Sprintf("%s:%d", s.cfg.hostname, addr.Port)
158209
}
159210

211+
func (s *Server) DebugAddress() string {
212+
if s.debugListener == nil {
213+
return ""
214+
}
215+
protocol := "http"
216+
addr := s.debugListener.Addr().(*net.TCPAddr)
217+
return fmt.Sprintf("%s://%s:%d", protocol, s.cfg.hostname, addr.Port)
218+
}
219+
160220
func (s *Server) HTTPMux() *http.ServeMux {
161221
return s.httpMux
162222
}
@@ -178,17 +238,29 @@ func (s *Server) close(ctx context.Context) error {
178238
s.Logger().Info("Received graceful shutdown request.")
179239
close(s.listening)
180240

181-
s.grpc.GracefulStop()
182-
// s.grpc.GracefulStop() also closes the underlying net.Listener, we just release the reference.
183-
s.grpcListener = nil
184-
s.Logger().Info("GRPC server terminated.")
241+
if s.isGRPCEnabled() {
242+
s.grpc.GracefulStop()
243+
// s.grpc.GracefulStop() also closes the underlying net.Listener, we just release the reference.
244+
s.grpcListener = nil
245+
s.Logger().Info("GRPC server terminated.")
246+
}
185247

186-
if err := s.http.Shutdown(ctx); err != nil {
187-
return fmt.Errorf("failed to close http server: %w", err)
248+
if s.isHTTPEnabled() {
249+
if err := s.http.Shutdown(ctx); err != nil {
250+
return fmt.Errorf("failed to close http server: %w", err)
251+
}
252+
// s.http.Shutdown() also closes the underlying net.Listener, we just release the reference.
253+
s.httpListener = nil
254+
s.Logger().Info("HTTP server terminated.")
255+
}
256+
257+
// Always terminate debug server last, we want to keep it running for as long as possible
258+
if err := s.debug.Shutdown(ctx); err != nil {
259+
return fmt.Errorf("failed to close debug server: %w", err)
188260
}
189261
// s.http.Shutdown() also closes the underlying net.Listener, we just release the reference.
190-
s.httpListener = nil
191-
s.Logger().Info("HTTP server terminated.")
262+
s.debugListener = nil
263+
s.Logger().Info("Debug server terminated.")
192264

193265
return nil
194266
}
@@ -204,7 +276,7 @@ func (s *Server) isClosing() bool {
204276
}
205277

206278
func (s *Server) initializeHTTP() error {
207-
s.httpMux = s.newHTTPMux()
279+
s.httpMux = http.NewServeMux()
208280
s.http = &http.Server{
209281
Addr: fmt.Sprintf(":%d", s.cfg.httpPort),
210282
Handler: s.httpMux,
@@ -213,13 +285,16 @@ func (s *Server) initializeHTTP() error {
213285
return nil
214286
}
215287

216-
func (s *Server) newHTTPMux() *http.ServeMux {
288+
func (s *Server) initializeDebug() error {
289+
logger := s.Logger().WithField("protocol", "debug")
290+
217291
mux := http.NewServeMux()
292+
218293
mux.HandleFunc("/ready", s.cfg.healthHandler.ReadyEndpoint)
219-
s.Logger().WithField("protocol", "http").Debug("Serving readiness handler on /ready")
294+
logger.Debug("Serving readiness handler on /ready")
220295

221296
mux.HandleFunc("/live", s.cfg.healthHandler.LiveEndpoint)
222-
s.Logger().WithField("protocol", "http").Debug("Serving liveliness handler on /live")
297+
logger.Debug("Serving liveliness handler on /live")
223298

224299
// Metrics endpoint
225300
metricsHandler := promhttp.Handler()
@@ -229,12 +304,17 @@ func (s *Server) newHTTPMux() *http.ServeMux {
229304
)
230305
}
231306
mux.Handle("/metrics", metricsHandler)
232-
s.Logger().WithField("protocol", "http").Debug("Serving metrics on /metrics")
307+
logger.Debug("Serving metrics on /metrics")
233308

234309
mux.Handle(pprof.Path, pprof.Handler())
235-
s.Logger().WithField("protocol", "http").Debug("Serving profiler on /debug/pprof")
310+
logger.Debug("Serving profiler on /debug/pprof")
311+
312+
s.debug = &http.Server{
313+
Addr: fmt.Sprintf(":%d", s.cfg.debugPort),
314+
Handler: mux,
315+
}
236316

237-
return mux
317+
return nil
238318
}
239319

240320
func (s *Server) initializeGRPC() error {
@@ -251,3 +331,11 @@ func (s *Server) initializeGRPC() error {
251331

252332
return nil
253333
}
334+
335+
func (s *Server) isGRPCEnabled() bool {
336+
return s.cfg.grpcPort >= 0
337+
}
338+
339+
func (s *Server) isHTTPEnabled() bool {
340+
return s.cfg.httpPort >= 0
341+
}

0 commit comments

Comments
 (0)