all repos

onasty @ bcf5056

a one-time notes service
10 files changed, 180 insertions(+), 53 deletions(-)
feat: request id (#18)

* feat(requestid): add helper package

* feat: log request id

* refactor(logger): log request_id from context

* refactor: use slog with context

* refactor: move out setting up logging into sep package

* fixup! feat(requestid): add helper package

* remove linting errs :D

* fixup! refactor: use slog with context

* refactor: make loging statement more readable

* refactor(reqid): renaming

* refactor(reqid): use gofrs/uuid

* test(reqid): add banch
Author: Smirnov Oleksandr ss2316544@gmail.com
Committed by: GitHub noreply@github.com
Committed at: 2024-09-20 22:48:36 +0300
Parent: b617b17
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
        +}