forked from acronis/go-appkit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhttp_server.go
More file actions
309 lines (268 loc) · 10.2 KB
/
Copy pathhttp_server.go
File metadata and controls
309 lines (268 loc) · 10.2 KB
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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
/*
Copyright © 2024 Acronis International GmbH.
Released under MIT license.
*/
package httpserver
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"os"
"strconv"
"sync/atomic"
"time"
"github.com/go-chi/chi/v5"
"github.com/prometheus/client_golang/prometheus"
"github.com/acronis/go-appkit/httpserver/middleware"
"github.com/acronis/go-appkit/log"
"github.com/acronis/go-appkit/service"
)
const (
networkTCP = "tcp"
networkUnix = "unix"
)
// systemEndpoints is a list of endpoints which are not involved in metrics collecting, and in-flight requests limiting.
var systemEndpoints = []string{"/metrics", "/healthz"}
// APIVersion is a type alias for API version.
type APIVersion = int
// APIRoute is a type alias for single API route.
type APIRoute = func(router chi.Router)
// HTTPRequestMetricsOpts represents options for HTTPRequestMetricsOpts middleware that used in HTTPServer.
type HTTPRequestMetricsOpts struct {
// Metrics opts.
Namespace string
DurationBuckets []float64
ConstLabels prometheus.Labels
// Middleware opts.
// Deprecated: GetUserAgentType be removed in the next major version. Please use CustomLabels with Context instead.
GetUserAgentType middleware.UserAgentTypeGetterFunc
GetRoutePattern middleware.RoutePatternGetterFunc
}
// Opts represents options for creating HTTPServer.
type Opts struct {
// ServiceNameInURL is a prefix for API routes (e.g., "/api/service_name/v1").
ServiceNameInURL string
// APIRoutes is a map of API versions to their route configuration functions.
APIRoutes map[APIVersion]APIRoute
// RootMiddlewares is a list of middlewares to be applied to the root router.
RootMiddlewares []func(http.Handler) http.Handler
// ErrorDomain is used for error response formatting.
ErrorDomain string
// HealthCheck is a function that performs health check logic.
HealthCheck HealthCheck
// HealthCheckContext is a function that performs context-aware health check logic.
HealthCheckContext HealthCheckContext
// MetricsHandler is a custom handler for the /metrics endpoint (e.g., Prometheus handler).
MetricsHandler http.Handler
// HTTPRequestMetrics contains options for configuring HTTP request metrics middleware.
HTTPRequestMetrics HTTPRequestMetricsOpts
// Handler is a custom HTTP handler to use instead of the default router with middlewares.
// When provided, default middlewares are not applied.
Handler http.Handler
// Listener is a pre-configured network listener to use instead of creating a new one.
// Useful for custom listener configurations or testing with mock listeners.
Listener net.Listener
}
func (opts Opts) routerOpts() RouterOpts {
return RouterOpts{
ServiceNameInURL: opts.ServiceNameInURL,
APIRoutes: opts.APIRoutes,
RootMiddlewares: opts.RootMiddlewares,
ErrorDomain: opts.ErrorDomain,
HealthCheck: opts.HealthCheck,
HealthCheckContext: opts.HealthCheckContext,
MetricsHandler: opts.MetricsHandler,
}
}
// HTTPServer represents a wrapper around http.Server with additional fields and methods.
// chi.Router is used as a handler for the server by default.
// It also implements service.Unit and service.MetricsRegisterer interfaces.
type HTTPServer struct {
// TODO: URL does not contain port when port is dynamically chosen
URL string
HTTPServer *http.Server
UnixSocketPath string
TLS TLSConfig
HTTPRouter chi.Router
Logger log.FieldLogger
ShutdownTimeout time.Duration
listener net.Listener
port int32
httpServerDone atomic.Value
httpReqPrometheusMetrics *middleware.HTTPRequestPrometheusMetrics
}
var _ service.Unit = (*HTTPServer)(nil)
var _ service.MetricsRegisterer = (*HTTPServer)(nil)
// New creates a new HTTPServer with predefined logging, metrics collecting,
// recovering after panics and health-checking functionality.
func New(cfg *Config, logger log.FieldLogger, opts Opts) (*HTTPServer, error) { //nolint // hugeParam: opts is heavy, it's ok in this case.
if opts.Handler != nil {
return newWithHandler(cfg, logger, opts.Handler, opts.Listener), nil
}
httpReqPromMetrics := middleware.NewHTTPRequestPrometheusMetricsWithOpts(
middleware.HTTPRequestPrometheusMetricsOpts{
Namespace: opts.HTTPRequestMetrics.Namespace,
DurationBuckets: opts.HTTPRequestMetrics.DurationBuckets,
ConstLabels: opts.HTTPRequestMetrics.ConstLabels,
})
router := chi.NewRouter()
if err := applyDefaultMiddlewaresToRouter(router, cfg, logger, opts, httpReqPromMetrics); err != nil {
return nil, err
}
configureRouter(router, logger, opts.routerOpts())
appSrv := newWithHandler(cfg, logger, router, opts.Listener)
appSrv.httpReqPrometheusMetrics = httpReqPromMetrics
return appSrv, nil
}
// NewWithHandler creates a new HTTPServer receiving already created http.Handler.
// Unlike the New constructor, it doesn't add any middlewares.
// Typical use case: create a chi.Router using NewRouter and pass it into NewWithHandler.
// Deprecated: Will be removed in the next major version. Please use New with Handler options instead.
func NewWithHandler(cfg *Config, logger log.FieldLogger, handler http.Handler) *HTTPServer {
return newWithHandler(cfg, logger, handler, nil)
}
func newWithHandler(cfg *Config, logger log.FieldLogger, handler http.Handler, listener net.Listener) *HTTPServer {
httpServer := &http.Server{
Addr: cfg.Address,
WriteTimeout: time.Duration(cfg.Timeouts.Write),
ReadTimeout: time.Duration(cfg.Timeouts.Read),
ReadHeaderTimeout: time.Duration(cfg.Timeouts.ReadHeader),
IdleTimeout: time.Duration(cfg.Timeouts.Idle),
Handler: handler,
}
buildServerURL := func() string {
serverURL := httpServer.Addr
if cfg.UnixSocketPath != "" {
serverURL = "localhost" // Any domain can be used here. It will not be used in unix-socket case.
}
if cfg.TLS.Enabled {
return "https://" + serverURL
}
return "http://" + serverURL
}
router, _ := handler.(chi.Router)
return &HTTPServer{
URL: buildServerURL(),
HTTPServer: httpServer,
UnixSocketPath: cfg.UnixSocketPath,
Logger: logger,
TLS: cfg.TLS,
ShutdownTimeout: time.Duration(cfg.Timeouts.Shutdown),
HTTPRouter: router,
listener: listener,
}
}
// Start starts application HTTP server in a blocking way.
// It's supposed that this method will be called in a separate goroutine.
// If a fatal error occurs, it will be sent to the fatalError channel.
func (s *HTTPServer) Start(fatalError chan<- error) {
done := make(chan struct{})
defer close(done)
s.httpServerDone.Store(done)
logger := s.Logger.With(
log.String("address", s.HTTPServer.Addr),
log.Duration("write_timeout", s.HTTPServer.WriteTimeout),
log.Duration("read_timeout", s.HTTPServer.ReadTimeout),
log.Duration("read_header_timeout", s.HTTPServer.ReadHeaderTimeout),
log.Duration("idle_timeout", s.HTTPServer.IdleTimeout),
log.Duration("shutdown_timeout", s.ShutdownTimeout),
)
if s.UnixSocketPath != "" {
logger = logger.With(log.String("unix_socket_path", s.UnixSocketPath))
if err := os.Remove(s.UnixSocketPath); err != nil && !os.IsNotExist(err) {
fatalError <- fmt.Errorf("remove unix socket file %q: %w", s.UnixSocketPath, err)
return
}
}
logger.Info("starting application HTTP server...")
var err error
if s.listener == nil {
network, addr := s.NetworkAndAddr()
if s.listener, err = net.Listen(network, addr); err != nil {
logger.Error("application HTTP server error", log.Error(err))
fatalError <- err
return
}
}
if s.listener.Addr().Network() == networkTCP {
var portStr string
_, portStr, err = net.SplitHostPort(s.listener.Addr().String())
if err != nil {
logger.Error("unexpected format of TCP listener address: unable to split host and port", log.Error(err))
fatalError <- err
return
}
var port int64
port, err = strconv.ParseInt(portStr, 10, 32)
if err != nil {
logger.Error("unexpected format of TCP listener address: no numeric port", log.Error(err))
fatalError <- err
return
}
atomic.StoreInt32(&s.port, int32(port))
}
if s.TLS.Enabled {
err = s.HTTPServer.ServeTLS(s.listener, s.TLS.Certificate, s.TLS.Key)
} else {
err = s.HTTPServer.Serve(s.listener)
}
if err != nil {
if errors.Is(err, http.ErrServerClosed) {
logger.Info("application HTTP server closed")
return
}
logger.Error("application HTTP server error", log.Error(err))
fatalError <- err
return
}
}
// Stop stops application HTTP server (gracefully or not).
func (s *HTTPServer) Stop(gracefully bool) error {
if !gracefully {
s.Logger.Info("closing application HTTP server...")
if err := s.HTTPServer.Close(); err != nil {
s.Logger.Error("application HTTP server closing error", log.Error(err))
return err
}
if done, ok := s.httpServerDone.Load().(chan struct{}); ok && done != nil {
<-done // Wait for the listener to be closed.
}
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), s.ShutdownTimeout)
defer cancel()
s.Logger.Info("shutting down application HTTP server...", log.Duration("timeout", s.ShutdownTimeout))
if err := s.HTTPServer.Shutdown(ctx); err != nil {
s.Logger.Error("application HTTP server shutting down error", log.Error(err))
return err
}
s.Logger.Info("application HTTP server shut down")
if done, ok := s.httpServerDone.Load().(chan struct{}); ok && done != nil {
<-done // Wait for the listener to be closed.
}
return nil
}
// MustRegisterMetrics registers metrics in Prometheus client and panics if any error occurs.
func (s *HTTPServer) MustRegisterMetrics() {
if s.httpReqPrometheusMetrics != nil {
s.httpReqPrometheusMetrics.MustRegister()
}
}
// UnregisterMetrics unregisters metrics in Prometheus client.
func (s *HTTPServer) UnregisterMetrics() {
if s.httpReqPrometheusMetrics != nil {
s.httpReqPrometheusMetrics.Unregister()
}
}
// NetworkAndAddr returns network type ("tcp" or "unix") and address (path to unix socket in case of "unix" network).
func (s *HTTPServer) NetworkAndAddr() (network string, addr string) {
if s.UnixSocketPath != "" {
return networkUnix, s.UnixSocketPath
}
return networkTCP, s.HTTPServer.Addr
}
func (s *HTTPServer) GetPort() int {
return int(atomic.LoadInt32(&s.port))
}