10 files changed,
180 insertions(+),
53 deletions(-)
Author:
Smirnov Oleksandr
ss2316544@gmail.com
Committed by:
GitHub
noreply@github.com
Committed at:
2024-09-20 22:48:36 +0300
Parent:
b617b17
jump to
M
cmd/server/main.go
··· 1 -//nolint:err113 // all errors are shown to the user so it's ok for them to be dynamic 2 1 package main 3 2 4 3 import ( ··· 14 13 "github.com/olexsmir/onasty/internal/config" 15 14 "github.com/olexsmir/onasty/internal/hasher" 16 15 "github.com/olexsmir/onasty/internal/jwtutil" 16 + "github.com/olexsmir/onasty/internal/logger" 17 17 "github.com/olexsmir/onasty/internal/mailer" 18 18 "github.com/olexsmir/onasty/internal/service/notesrv" 19 19 "github.com/olexsmir/onasty/internal/service/usersrv" ··· 33 33 } 34 34 } 35 35 36 +//nolint:err113 36 37 func run(ctx context.Context) error { 37 38 ctx, cancel := context.WithCancel(ctx) 38 39 defer cancel() 39 40 40 41 cfg := config.NewConfig() 41 - if err := setupLogger(cfg); err != nil { 42 + 43 + // logger 44 + logger, err := logger.NewCustomLogger(cfg.LogLevel, cfg.LogFormat, cfg.LogShowLine) 45 + if err != nil { 42 46 return err 43 47 } 44 48 49 + slog.SetDefault(logger) 50 + 51 + // semi dev mode 45 52 if !cfg.IsDevMode() { 46 53 gin.SetMode(gin.ReleaseMode) 47 54 } 48 55 56 + // app deps 49 57 psqlDB, err := psqlutil.Connect(ctx, cfg.PostgresDSN) 50 58 if err != nil { 51 59 return err 52 60 } 53 61 54 - // app deps 55 62 sha256Hasher := hasher.NewSHA256Hasher(cfg.PasswordSalt) 56 63 jwtTokenizer := jwtutil.NewJWTUtil(cfg.JwtSigningKey, cfg.JwtAccessTokenTTL) 57 64 mailGunMailer := mailer.NewMailgun(cfg.MailgunFrom, cfg.MailgunDomain, cfg.MailgunAPIKey) ··· 101 108 102 109 return nil 103 110 } 104 - 105 -func setupLogger(cfg *config.Config) error { 106 - loggerLevels := map[string]slog.Level{ 107 - "info": slog.LevelInfo, 108 - "debug": slog.LevelDebug, 109 - "error": slog.LevelError, 110 - "warn": slog.LevelWarn, 111 - } 112 - 113 - logLevel, ok := loggerLevels[cfg.LogLevel] 114 - if !ok { 115 - return errors.New("unknown log level") 116 - } 117 - 118 - handlerOptions := &slog.HandlerOptions{ 119 - Level: logLevel, 120 - AddSource: cfg.LogShowLine, 121 - } 122 - 123 - var slogHandler slog.Handler 124 - switch cfg.LogFormat { 125 - case "json": 126 - slogHandler = slog.NewJSONHandler(os.Stdout, handlerOptions) 127 - case "text": 128 - slogHandler = slog.NewTextHandler(os.Stdout, handlerOptions) 129 - default: 130 - return errors.New("unknown log format") 131 - } 132 - 133 - slog.SetDefault(slog.New(slogHandler)) 134 - 135 - return nil 136 -}
M
e2e/e2e_test.go
··· 16 16 "github.com/olexsmir/onasty/internal/config" 17 17 "github.com/olexsmir/onasty/internal/hasher" 18 18 "github.com/olexsmir/onasty/internal/jwtutil" 19 + "github.com/olexsmir/onasty/internal/logger" 19 20 "github.com/olexsmir/onasty/internal/mailer" 20 21 "github.com/olexsmir/onasty/internal/service/notesrv" 21 22 "github.com/olexsmir/onasty/internal/service/usersrv" ··· 76 77 e.postgresDB = db 77 78 e.stopPostgres = stop 78 79 79 - e.setupLogger() 80 80 e.initDeps() 81 81 } 82 82 ··· 88 88 // and sets up the router for tests 89 89 func (e *AppTestSuite) initDeps() { 90 90 cfg := e.getConfig() 91 + 92 + logger, err := logger.NewCustomLogger(cfg.LogLevel, cfg.LogFormat, cfg.LogShowLine) 93 + e.require.NoError(err) 94 + 95 + slog.SetDefault(logger) 91 96 92 97 e.hasher = hasher.NewSHA256Hasher(cfg.PasswordSalt) 93 98 e.jwtTokenizer = jwtutil.NewJWTUtil(cfg.JwtSigningKey, time.Hour) ··· 168 173 return db, stop, driver.Close() 169 174 } 170 175 171 -func (e *AppTestSuite) setupLogger() { 172 - slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ 173 - Level: slog.LevelDebug, 174 - AddSource: os.Getenv("LOG_SHOW_LINE") == "true", 175 - }))) 176 -} 177 - 178 176 func (e *AppTestSuite) getConfig() *config.Config { 179 177 return &config.Config{ //nolint:exhaustruct 180 178 AppEnv: "testing", ··· 185 183 JwtAccessTokenTTL: time.Hour, 186 184 JwtRefreshTokenTTL: 24 * time.Hour, 187 185 VerficationTokenTTL: 24 * time.Hour, 186 + LogShowLine: os.Getenv("LOG_SHOW_LINE") == "true", 187 + LogFormat: "text", 188 + LogLevel: "debug", 188 189 } 189 190 }
A
internal/logger/logger.go
··· 1 +package logger 2 + 3 +import ( 4 + "context" 5 + "errors" 6 + "log/slog" 7 + "os" 8 + 9 + "github.com/olexsmir/onasty/internal/transport/http/reqid" 10 +) 11 + 12 +type CustomLogger struct{ slog.Handler } 13 + 14 +//nolint:err113 15 +func NewCustomLogger(lvl, format string, showLine bool) (*slog.Logger, error) { 16 + loggerLevels := map[string]slog.Level{ 17 + "info": slog.LevelInfo, 18 + "debug": slog.LevelDebug, 19 + "error": slog.LevelError, 20 + "warn": slog.LevelWarn, 21 + } 22 + 23 + logLevel, ok := loggerLevels[lvl] 24 + if !ok { 25 + return nil, errors.New("unknown log level") 26 + } 27 + 28 + handlerOptions := &slog.HandlerOptions{ 29 + Level: logLevel, 30 + AddSource: showLine, 31 + } 32 + 33 + var slogHandler slog.Handler 34 + switch format { 35 + case "json": 36 + slogHandler = slog.NewJSONHandler(os.Stdout, handlerOptions) 37 + case "text": 38 + slogHandler = slog.NewTextHandler(os.Stdout, handlerOptions) 39 + default: 40 + return nil, errors.New("unknown log format") 41 + } 42 + 43 + return slog.New(&CustomLogger{Handler: slogHandler}), nil 44 +} 45 + 46 +func (l *CustomLogger) Handle(ctx context.Context, r slog.Record) error { 47 + if requestID := reqid.GetContext(ctx); requestID != "" { 48 + r.AddAttrs(slog.String("request_id", requestID)) 49 + } 50 + 51 + return l.Handler.Handle(ctx, r) 52 +}
M
internal/mailer/mailgun.go
··· 29 29 30 30 _, _, err := m.mg.Send(ctx, msg) 31 31 32 - slog.Info("email sent", "to", to) 33 - slog.Debug("email sent", "subject", subject, "content", content, "err", err) 32 + slog.InfoContext(ctx, "email sent", "to", to) 33 + slog.DebugContext(ctx, "email sent", "subject", subject, "content", content, "err", err) 34 34 35 35 return err 36 36 }
M
internal/service/usersrv/email.go
··· 27 27 ) error { 28 28 select { 29 29 case <-ctx.Done(): 30 - slog.Error("failed to send verfication email", "err", ctx.Err()) 30 + slog.ErrorContext(ctx, "failed to send verfication email", "err", ctx.Err()) 31 31 return ErrFailedToSendVerifcationEmail 32 32 default: 33 33 if err := u.mailer.Send( ··· 39 39 return errors.Join(ErrFailedToSendVerifcationEmail, err) 40 40 } 41 41 cancel() 42 - 43 - slog.Debug("email sent") 44 42 } 45 43 46 44 return nil
M
internal/transport/http/apiv1/response.go
··· 52 52 } 53 53 54 54 func newError(c *gin.Context, status int, msg string) { 55 - slog.Error(msg, "status", status) 55 + slog.ErrorContext(c.Request.Context(), msg, "status", status) 56 56 c.AbortWithStatusJSON(status, response{msg}) 57 57 } 58 58 59 59 func newErrorStatus(c *gin.Context, status int, msg string) { 60 - slog.Error(msg, "status", status) 60 + slog.ErrorContext(c.Request.Context(), msg, "status", status) 61 61 c.AbortWithStatus(status) 62 62 } 63 63 64 64 func newInternalError(c *gin.Context, err error, msg ...string) { 65 - slog.With("status", "internal error").Error(err.Error()) 65 + slog.ErrorContext(c.Request.Context(), err.Error(), "status", "internal error") 66 66 67 67 if len(msg) != 0 { 68 68 c.AbortWithStatusJSON(http.StatusInternalServerError, response{
M
internal/transport/http/http.go
··· 7 7 "github.com/olexsmir/onasty/internal/service/notesrv" 8 8 "github.com/olexsmir/onasty/internal/service/usersrv" 9 9 "github.com/olexsmir/onasty/internal/transport/http/apiv1" 10 + "github.com/olexsmir/onasty/internal/transport/http/reqid" 10 11 ) 11 12 12 13 type Transport struct { ··· 28 29 r := gin.New() 29 30 r.Use( 30 31 gin.Recovery(), 32 + reqid.Middleware(), 31 33 t.logger(), 32 34 ) 33 35
M
internal/transport/http/middlewares.go
··· 5 5 "time" 6 6 7 7 "github.com/gin-gonic/gin" 8 + "github.com/olexsmir/onasty/internal/transport/http/reqid" 8 9 ) 9 10 10 -// TODO: include requiest id 11 11 func (t *Transport) logger() gin.HandlerFunc { 12 12 return func(c *gin.Context) { 13 13 start := time.Now() ··· 30 30 c.Request.Context(), 31 31 lvl, 32 32 c.Errors.ByType(gin.ErrorTypePrivate).String(), 33 + slog.String("request_id", reqid.Get(c)), 33 34 slog.String("latency", latency.String()), 34 35 slog.String("method", c.Request.Method), 35 36 slog.Int("status_code", c.Writer.Status()),
A
internal/transport/http/reqid/reqid.go
··· 1 +// reqid provides gin-gonic/gin middleware to generate a requestid for each request 2 +package reqid 3 + 4 +import ( 5 + "context" 6 + 7 + "github.com/gin-gonic/gin" 8 + "github.com/gofrs/uuid/v5" 9 +) 10 + 11 +type requestIDKey string 12 + 13 +const ( 14 + RequestID requestIDKey = "request_id" 15 + 16 + headerRequestID = "X-Request-ID" 17 +) 18 + 19 +// Middleware initializes the request ID 20 +func Middleware() gin.HandlerFunc { 21 + return func(c *gin.Context) { 22 + rid := c.GetHeader(headerRequestID) 23 + if rid == "" { 24 + rid = uuid.Must(uuid.NewV4()).String() 25 + c.Request.Header.Add(headerRequestID, rid) 26 + } 27 + 28 + // set reqeust ID request context 29 + ctx := context.WithValue(c.Request.Context(), RequestID, rid) 30 + c.Request = c.Request.WithContext(ctx) 31 + 32 + // ensures that the request ID is in the response 33 + c.Header(headerRequestID, rid) 34 + c.Next() 35 + } 36 +} 37 + 38 +// Get returns the request ID 39 +func Get(c *gin.Context) string { 40 + return c.GetHeader(headerRequestID) 41 +} 42 + 43 +// GetContext returns the request ID from context 44 +func GetContext(ctx context.Context) string { 45 + rid, ok := ctx.Value(RequestID).(string) 46 + if !ok { 47 + return "" 48 + } 49 + return rid 50 +}
A
internal/transport/http/reqid/reqid_test.go
··· 1 +package reqid 2 + 3 +import ( 4 + "net/http" 5 + "net/http/httptest" 6 + "testing" 7 + 8 + "github.com/gin-gonic/gin" 9 + "github.com/stretchr/testify/assert" 10 + "github.com/stretchr/testify/require" 11 +) 12 + 13 +//nolint:gochecknoinits 14 +func init() { 15 + gin.SetMode(gin.TestMode) 16 +} 17 + 18 +func testHandler(c *gin.Context) { 19 + c.Status(http.StatusOK) 20 +} 21 + 22 +func TestMiddleware(t *testing.T) { 23 + r := gin.New() 24 + r.Use(Middleware()) 25 + r.GET("/", testHandler) 26 + 27 + w := httptest.NewRecorder() 28 + req, err := http.NewRequest(http.MethodGet, "/", nil) 29 + require.NoError(t, err) 30 + 31 + r.ServeHTTP(w, req) 32 + 33 + assert.Equal(t, http.StatusOK, w.Code) 34 + assert.NotEmpty(t, w.Header().Get(headerRequestID)) 35 +} 36 + 37 +func BenchmarkMiddleware(b *testing.B) { 38 + r := gin.New() 39 + r.Use(Middleware()) 40 + r.GET("/", testHandler) 41 + 42 + w := httptest.NewRecorder() 43 + req, err := http.NewRequest(http.MethodGet, "/", nil) 44 + require.NoError(b, err) 45 + 46 + for i := 0; i < b.N; i++ { 47 + r.ServeHTTP(w, req) 48 + } 49 +}