diff --git a/cli/serve.go b/cli/serve.go index dd1c54f..543b342 100644 --- a/cli/serve.go +++ b/cli/serve.go @@ -1,20 +1,34 @@ package cli import ( + "os" + "os/signal" + "github.com/spf13/cobra" "github.com/tigorlazuardi/redmage/pkg/log" + "github.com/tigorlazuardi/redmage/server" ) var serveCmd = &cobra.Command{ Use: "serve", Short: "Starts the HTTP Server", SilenceUsage: true, - RunE: func(cmd *cobra.Command, args []string) error { - hostPort := cfg.String("http.host") + ":" + cfg.String("http.port") + Run: func(cmd *cobra.Command, args []string) { + server := server.New(cfg) - log.Log(cmd.Context()).Info("starting http server", "host", hostPort) + exit := make(chan struct{}, 1) - return nil + go func() { + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt) + <-sig + exit <- struct{}{} + }() + + if err := server.Start(exit); err != nil { + log.Log(cmd.Context()).Err(err).Error("failed to start server") + os.Exit(1) + } }, } diff --git a/config/default.go b/config/default.go index 79e763e..5b8896a 100644 --- a/config/default.go +++ b/config/default.go @@ -11,6 +11,7 @@ var DefaultConfig = map[string]any{ "db.string": "data.db", "db.automigrate": true, - "http.port": "8080", - "http.host": "0.0.0.0", + "http.port": "8080", + "http.host": "0.0.0.0", + "http.shutdown_timeout": "5s", } diff --git a/pkg/caller/caller.go b/pkg/caller/caller.go index dadd71e..b7ded1a 100644 --- a/pkg/caller/caller.go +++ b/pkg/caller/caller.go @@ -16,6 +16,17 @@ func (ca Caller) File() string { return ca.Frame.File } +func (ca Caller) ShortFile() string { + wd, err := os.Getwd() + if err != nil { + return ca.Frame.File + } + if after, found := strings.CutPrefix(ca.Frame.File, wd); found { + return strings.TrimPrefix(after, string(os.PathSeparator)) + } + return ca.Frame.File +} + func (ca Caller) Line() int { return ca.Frame.Line } @@ -35,7 +46,7 @@ func (ca Caller) LogValue() slog.Value { } return slog.GroupValue( - slog.String("file", ca.File()), + slog.String("file", ca.ShortFile()), slog.Int("line", ca.Line()), slog.String("function", ca.ShortFunction()), ) diff --git a/pkg/errs/errs.go b/pkg/errs/errs.go index f7787de..2a5635d 100644 --- a/pkg/errs/errs.go +++ b/pkg/errs/errs.go @@ -139,11 +139,18 @@ func (er *Err) GetDetails() []any { } func (er *Err) Log(ctx context.Context) Error { - log.Log(ctx).Caller(er.caller).Error(er.message, "error", er) + log.Log(ctx).Caller(er.caller).Err(er).Error(er.message) return er } -func Wrap(err error, message string, details ...any) Error { +func Wrap(err error) Error { + return &Err{ + origin: err, + caller: caller.New(3), + } +} + +func Wrapw(err error, message string, details ...any) Error { return &Err{ origin: err, details: details, diff --git a/pkg/log/log.go b/pkg/log/log.go index 0aa762d..f785207 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -49,6 +49,7 @@ type Entry struct { handler slog.Handler caller caller.Caller time time.Time + err error } // Log prepares a new entry to write logs. @@ -65,10 +66,18 @@ func (entry *Entry) Caller(caller caller.Caller) *Entry { return entry } +func (entry *Entry) Err(err error) *Entry { + entry.err = err + return entry +} + func (entry *Entry) Info(message string, fields ...any) { record := slog.NewRecord(entry.time, slog.LevelInfo, message, entry.getCaller().PC) record.AddAttrs(entry.getExtra()...) - record.AddAttrs(slog.Group("context", fields...)) + record.AddAttrs(slog.Group("details", fields...)) + if entry.err != nil { + record.AddAttrs(slog.Any("error", entry.err)) + } _ = entry.handler.Handle(entry.ctx, record) } @@ -76,13 +85,19 @@ func (entry *Entry) Infof(format string, args ...any) { message := fmt.Sprintf(format, args...) record := slog.NewRecord(entry.time, slog.LevelInfo, message, entry.getCaller().PC) record.AddAttrs(entry.getExtra()...) + if entry.err != nil { + record.AddAttrs(slog.Any("error", entry.err)) + } _ = entry.handler.Handle(entry.ctx, record) } func (entry *Entry) Error(message string, fields ...any) { record := slog.NewRecord(entry.time, slog.LevelError, message, entry.getCaller().PC) record.AddAttrs(entry.getExtra()...) - record.AddAttrs(slog.Group("context", fields...)) + record.AddAttrs(slog.Group("details", fields...)) + if entry.err != nil { + record.AddAttrs(slog.Any("error", entry.err)) + } _ = entry.handler.Handle(entry.ctx, record) } @@ -90,13 +105,19 @@ func (entry *Entry) Errorf(format string, args ...any) { message := fmt.Sprintf(format, args...) record := slog.NewRecord(entry.time, slog.LevelError, message, entry.getCaller().PC) record.AddAttrs(entry.getExtra()...) + if entry.err != nil { + record.AddAttrs(slog.Any("details", entry.err)) + } _ = entry.handler.Handle(entry.ctx, record) } func (entry *Entry) Debug(message string, fields ...any) { record := slog.NewRecord(entry.time, slog.LevelDebug, message, entry.getCaller().PC) record.AddAttrs(entry.getExtra()...) - record.AddAttrs(slog.Group("context", fields...)) + record.AddAttrs(slog.Group("details", fields...)) + if entry.err != nil { + record.AddAttrs(slog.Any("error", entry.err)) + } _ = entry.handler.Handle(entry.ctx, record) } @@ -104,13 +125,19 @@ func (entry *Entry) Debugf(format string, args ...any) { message := fmt.Sprintf(format, args...) record := slog.NewRecord(entry.time, slog.LevelDebug, message, entry.getCaller().PC) record.AddAttrs(entry.getExtra()...) + if entry.err != nil { + record.AddAttrs(slog.Any("error", entry.err)) + } _ = entry.handler.Handle(entry.ctx, record) } func (entry *Entry) Warn(message string, fields ...any) { record := slog.NewRecord(entry.time, slog.LevelWarn, message, entry.getCaller().PC) record.AddAttrs(entry.getExtra()...) - record.AddAttrs(slog.Group("context", fields...)) + record.AddAttrs(slog.Group("details", fields...)) + if entry.err != nil { + record.AddAttrs(slog.Any("error", entry.err)) + } _ = entry.handler.Handle(entry.ctx, record) } @@ -118,6 +145,9 @@ func (entry *Entry) Warnf(format string, args ...any) { message := fmt.Sprintf(format, args...) record := slog.NewRecord(entry.time, slog.LevelWarn, message, entry.getCaller().PC) record.AddAttrs(entry.getExtra()...) + if entry.err != nil { + record.AddAttrs(slog.Any("error", entry.err)) + } _ = entry.handler.Handle(entry.ctx, record) } diff --git a/pkg/log/pretty_handler.go b/pkg/log/pretty_handler.go index 8efe655..5da8f5c 100644 --- a/pkg/log/pretty_handler.go +++ b/pkg/log/pretty_handler.go @@ -118,10 +118,8 @@ func (pr *PrettyHandler) Handle(ctx context.Context, record slog.Record) error { _ = serializer.Handle(ctx, record) if jsonBuf.Len() > 3 { // Ignore empty json like "{}\n" _ = json.Indent(buf, jsonBuf.Bytes(), "", " ") - // json indent includes new line, no need to add extra new line. - } else { - buf.WriteByte('\n') } + buf.WriteByte('\n') pr.mu.Lock() defer pr.mu.Unlock() diff --git a/server/server.go b/server/server.go index 71681de..bdf3d8e 100644 --- a/server/server.go +++ b/server/server.go @@ -1,19 +1,44 @@ package server import ( + "context" + "errors" "net/http" "github.com/go-chi/chi/v5" "github.com/tigorlazuardi/redmage/config" + "github.com/tigorlazuardi/redmage/pkg/caller" + "github.com/tigorlazuardi/redmage/pkg/errs" + "github.com/tigorlazuardi/redmage/pkg/log" "github.com/tigorlazuardi/redmage/server/routes/api" "github.com/tigorlazuardi/redmage/server/routes/htmx" ) type Server struct { - handler http.Handler + server *http.Server + config *config.Config } -func (svr *Server) Serve() { +func (srv *Server) Start(exit <-chan struct{}) error { + errch := make(chan error, 1) + caller := caller.New(3) + go func() { + log.Log(context.Background()).Caller(caller).Info("starting http server", "address", "http://"+srv.server.Addr) + errch <- srv.server.ListenAndServe() + }() + + select { + case <-exit: + log.Log(context.Background()).Caller(caller).Info("received exit signal. shutting down server") + ctx, cancel := context.WithTimeout(context.Background(), srv.config.Duration("http.shutdown_timeout")) + defer cancel() + return srv.server.Shutdown(ctx) + case err := <-errch: + if errors.Is(err, http.ErrServerClosed) { + return nil + } + return errs.Wrap(err) + } } func New(cfg *config.Config) *Server { @@ -23,7 +48,10 @@ func New(cfg *config.Config) *Server { router.Route("/htmx", htmx.Register) - return &Server{ - handler: router, + server := &http.Server{ + Handler: router, + Addr: cfg.String("http.host") + ":" + cfg.String("http.port"), } + + return &Server{server: server, config: cfg} }