From 8800c023db04eea8df92792741b961b672abf4db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= Date: Sat, 9 Apr 2022 18:08:48 +0200 Subject: [PATCH] refactor server manager. (#57) --- internal/config/server.go | 14 +++++ internal/http/logger.go | 80 ---------------------------- internal/server/debug.go | 33 ++++++++++++ internal/server/logger.go | 74 +++++++++++++++++++++++++ internal/{http => server}/manager.go | 71 ++++++++++++++---------- neko.go | 20 +++---- 6 files changed, 175 insertions(+), 117 deletions(-) delete mode 100644 internal/http/logger.go create mode 100644 internal/server/debug.go create mode 100644 internal/server/logger.go rename internal/{http => server}/manager.go (52%) diff --git a/internal/config/server.go b/internal/config/server.go index 2e963c0..7d35626 100644 --- a/internal/config/server.go +++ b/internal/config/server.go @@ -10,6 +10,8 @@ type Server struct { Key string Bind string Static string + Proxy bool + PProf bool } func (Server) Init(cmd *cobra.Command) error { @@ -33,6 +35,16 @@ func (Server) Init(cmd *cobra.Command) error { return err } + cmd.PersistentFlags().Bool("proxy", false, "trust reverse proxy headers") + if err := viper.BindPFlag("proxy", cmd.PersistentFlags().Lookup("proxy")); err != nil { + return err + } + + cmd.PersistentFlags().Bool("pprof", false, "enable pprof endpoint available at /debug/pprof") + if err := viper.BindPFlag("pprof", cmd.PersistentFlags().Lookup("pprof")); err != nil { + return err + } + return nil } @@ -41,4 +53,6 @@ func (s *Server) Set() { s.Key = viper.GetString("key") s.Bind = viper.GetString("bind") s.Static = viper.GetString("static") + s.Proxy = viper.GetBool("proxy") + s.PProf = viper.GetBool("pprof") } diff --git a/internal/http/logger.go b/internal/http/logger.go deleted file mode 100644 index dabffea..0000000 --- a/internal/http/logger.go +++ /dev/null @@ -1,80 +0,0 @@ -package http - -import ( - "fmt" - "net/http" - "time" - - "github.com/go-chi/chi/middleware" - "github.com/rs/zerolog/log" -) - -func Logger(next http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - req := map[string]interface{}{} - - if reqID := middleware.GetReqID(r.Context()); reqID != "" { - req["id"] = reqID - } - - scheme := "http" - if r.TLS != nil { - scheme = "https" - } - - req["scheme"] = scheme - req["proto"] = r.Proto - req["method"] = r.Method - req["remote"] = r.RemoteAddr - req["agent"] = r.UserAgent() - req["uri"] = fmt.Sprintf("%s://%s%s", scheme, r.Host, r.RequestURI) - - fields := map[string]interface{}{} - fields["req"] = req - - entry := &entry{ - fields: fields, - } - - ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) - t1 := time.Now() - - defer func() { - entry.Write(ww.Status(), ww.BytesWritten(), time.Since(t1)) - }() - - next.ServeHTTP(ww, r) - } - return http.HandlerFunc(fn) -} - -type entry struct { - fields map[string]interface{} - errors []map[string]interface{} -} - -func (e *entry) Write(status, bytes int, elapsed time.Duration) { - res := map[string]interface{}{} - res["time"] = time.Now().UTC().Format(time.RFC1123) - res["status"] = status - res["bytes"] = bytes - res["elapsed"] = float64(elapsed.Nanoseconds()) / 1000000.0 - - e.fields["res"] = res - e.fields["module"] = "http" - - if len(e.errors) > 0 { - e.fields["errors"] = e.errors - log.Error().Fields(e.fields).Msgf("request failed (%d)", status) - } else { - log.Debug().Fields(e.fields).Msgf("request complete (%d)", status) - } -} - -func (e *entry) Panic(v interface{}, stack []byte) { - err := map[string]interface{}{} - err["message"] = fmt.Sprintf("%+v", v) - err["stack"] = string(stack) - - e.errors = append(e.errors, err) -} diff --git a/internal/server/debug.go b/internal/server/debug.go new file mode 100644 index 0000000..7e474c9 --- /dev/null +++ b/internal/server/debug.go @@ -0,0 +1,33 @@ +package server + +import ( + "net/http" + "net/http/pprof" + + "github.com/go-chi/chi" +) + +const pprofPath = "/debug/pprof/" + +func withPProf(router *chi.Mux) { + router.Route(pprofPath, func(r chi.Router) { + r.Get("/", pprof.Index) + + r.Get("/{action}", func(w http.ResponseWriter, r *http.Request) { + action := chi.URLParam(r, "action") + + switch action { + case "cmdline": + pprof.Cmdline(w, r) + case "profile": + pprof.Profile(w, r) + case "symbol": + pprof.Symbol(w, r) + case "trace": + pprof.Trace(w, r) + default: + pprof.Handler(action).ServeHTTP(w, r) + } + }) + }) +} diff --git a/internal/server/logger.go b/internal/server/logger.go new file mode 100644 index 0000000..1b5d5ac --- /dev/null +++ b/internal/server/logger.go @@ -0,0 +1,74 @@ +package server + +import ( + "fmt" + "net/http" + "time" + + "github.com/go-chi/chi/middleware" + "github.com/rs/zerolog" +) + +type logformatter struct { + logger zerolog.Logger +} + +func (l *logformatter) NewLogEntry(r *http.Request) middleware.LogEntry { + req := map[string]interface{}{} + + if reqID := middleware.GetReqID(r.Context()); reqID != "" { + req["id"] = reqID + } + + scheme := "http" + if r.TLS != nil { + scheme = "https" + } + + req["scheme"] = scheme + req["proto"] = r.Proto + req["method"] = r.Method + req["remote"] = r.RemoteAddr + req["agent"] = r.UserAgent() + req["uri"] = fmt.Sprintf("%s://%s%s", scheme, r.Host, r.RequestURI) + + fields := map[string]interface{}{} + fields["req"] = req + + return &logentry{ + fields: fields, + logger: l.logger, + } +} + +type logentry struct { + logger zerolog.Logger + fields map[string]interface{} + errors []map[string]interface{} +} + +func (e *logentry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra interface{}) { + res := map[string]interface{}{} + res["time"] = time.Now().UTC().Format(time.RFC1123) + res["status"] = status + res["bytes"] = bytes + res["elapsed"] = float64(elapsed.Nanoseconds()) / 1000000.0 + + e.fields["res"] = res + e.fields["module"] = "http" + + if len(e.errors) > 0 { + e.fields["errors"] = e.errors + e.logger.Error().Fields(e.fields).Msgf("request failed (%d)", status) + } else { + e.logger.Debug().Fields(e.fields).Msgf("request complete (%d)", status) + } +} + +func (e *logentry) Panic(v interface{}, stack []byte) { + err := map[string]interface{}{} + err["message"] = fmt.Sprintf("%+v", v) + err["stack"] = string(stack) + + e.errors = append(e.errors, err) +} diff --git a/internal/http/manager.go b/internal/server/manager.go similarity index 52% rename from internal/http/manager.go rename to internal/server/manager.go index a37d7f8..5bd7e19 100644 --- a/internal/http/manager.go +++ b/internal/server/manager.go @@ -1,10 +1,11 @@ -package http +package server import ( "context" "net/http" "os" "path" + "time" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" @@ -16,20 +17,27 @@ import ( "github.com/m1k1o/neko-rooms/internal/types" ) -type HttpManagerCtx struct { +type ServerManagerCtx struct { logger zerolog.Logger router *chi.Mux - http *http.Server - conf *config.Server + server *http.Server + config *config.Server } -func New(ApiManager types.ApiManager, pathPrefix string, conf *config.Server) *HttpManagerCtx { - logger := log.With().Str("module", "http").Logger() +func New(ApiManager types.ApiManager, pathPrefix string, config *config.Server) *ServerManagerCtx { + logger := log.With().Str("module", "server").Logger() router := chi.NewRouter() - router.Use(middleware.Recoverer) // Recover from panics without crashing server router.Use(middleware.RequestID) // Create a request ID for each request - router.Use(Logger) // Log API request calls using custom logger function + + // get real users ip + if config.Proxy { + router.Use(middleware.RealIP) + } + + // add http logger + router.Use(middleware.RequestLogger(&logformatter{logger})) + router.Use(middleware.Recoverer) // Recover from panics without crashing server // Basic CORS // for more ideas, see: https://developer.github.com/v3/#cross-origin-resource-sharing @@ -43,10 +51,11 @@ func New(ApiManager types.ApiManager, pathPrefix string, conf *config.Server) *H router.Route("/api", ApiManager.Mount) - if conf.Static != "" { - fs := http.FileServer(http.Dir(conf.Static)) + // serve static files + if config.Static != "" { + fs := http.FileServer(http.Dir(config.Static)) router.Get("/*", func(w http.ResponseWriter, r *http.Request) { - if _, err := os.Stat(conf.Static + r.URL.Path); !os.IsNotExist(err) { + if _, err := os.Stat(config.Static + r.URL.Path); !os.IsNotExist(err) { fs.ServeHTTP(w, r) } else { http.NotFound(w, r) @@ -58,39 +67,47 @@ func New(ApiManager types.ApiManager, pathPrefix string, conf *config.Server) *H router.Get(path.Join("/", pathPrefix, "{roomName}"), ApiManager.RoomLobby) router.Get(path.Join("/", pathPrefix, "{roomName}")+"/", ApiManager.RoomLobby) - router.NotFound(http.HandlerFunc(http.NotFound)) - - http := &http.Server{ - Addr: conf.Bind, - Handler: router, + // mount pprof endpoint + if config.PProf { + withPProf(router) + logger.Info().Msgf("with pprof endpoint at %s", pprofPath) } - return &HttpManagerCtx{ + // we could use custom 404 + router.NotFound(http.NotFound) + + return &ServerManagerCtx{ logger: logger, router: router, - http: http, - conf: conf, + server: &http.Server{ + Addr: config.Bind, + Handler: router, + }, + config: config, } } -func (s *HttpManagerCtx) Start() { - if s.conf.Cert != "" && s.conf.Key != "" { +func (s *ServerManagerCtx) Start() { + if s.config.Cert != "" && s.config.Key != "" { go func() { - if err := s.http.ListenAndServeTLS(s.conf.Cert, s.conf.Key); err != http.ErrServerClosed { + if err := s.server.ListenAndServeTLS(s.config.Cert, s.config.Key); err != http.ErrServerClosed { s.logger.Panic().Err(err).Msg("unable to start https server") } }() - s.logger.Info().Msgf("https listening on %s", s.http.Addr) + s.logger.Info().Msgf("https listening on %s", s.server.Addr) } else { go func() { - if err := s.http.ListenAndServe(); err != http.ErrServerClosed { + if err := s.server.ListenAndServe(); err != http.ErrServerClosed { s.logger.Panic().Err(err).Msg("unable to start http server") } }() - s.logger.Info().Msgf("http listening on %s", s.http.Addr) + s.logger.Info().Msgf("http listening on %s", s.server.Addr) } } -func (s *HttpManagerCtx) Shutdown() error { - return s.http.Shutdown(context.Background()) +func (s *ServerManagerCtx) Shutdown() error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + return s.server.Shutdown(ctx) } diff --git a/neko.go b/neko.go index 1acf664..07ca2c3 100644 --- a/neko.go +++ b/neko.go @@ -13,9 +13,9 @@ import ( "github.com/m1k1o/neko-rooms/internal/api" "github.com/m1k1o/neko-rooms/internal/config" - "github.com/m1k1o/neko-rooms/internal/http" "github.com/m1k1o/neko-rooms/internal/pull" "github.com/m1k1o/neko-rooms/internal/room" + "github.com/m1k1o/neko-rooms/internal/server" ) const Header = `&34 @@ -108,11 +108,11 @@ type MainCtx struct { Version *Version Configs *Configs - logger zerolog.Logger - roomManager *room.RoomManagerCtx - pullManager *pull.PullManagerCtx - apiManager *api.ApiManagerCtx - httpManager *http.HttpManagerCtx + logger zerolog.Logger + roomManager *room.RoomManagerCtx + pullManager *pull.PullManagerCtx + apiManager *api.ApiManagerCtx + serverManager *server.ServerManagerCtx } func (main *MainCtx) Preflight() { @@ -143,17 +143,17 @@ func (main *MainCtx) Start() { main.Configs.API, ) - main.httpManager = http.New( + main.serverManager = server.New( main.apiManager, main.Configs.Room.PathPrefix, main.Configs.Server, ) - main.httpManager.Start() + main.serverManager.Start() } func (main *MainCtx) Shutdown() { - err := main.httpManager.Shutdown() - main.logger.Err(err).Msg("http manager shutdown") + err := main.serverManager.Shutdown() + main.logger.Err(err).Msg("server manager shutdown") } func (main *MainCtx) ServeCommand(cmd *cobra.Command, args []string) {