fix flushing buffer, add requestId to context

This commit is contained in:
Sergey Chubaryan 2024-08-23 20:10:31 +03:00
parent c4235c3860
commit 204d718927
9 changed files with 110 additions and 120 deletions

View File

@ -5,21 +5,25 @@ import (
"sync" "sync"
) )
type BufioWrapper struct { type bufioWrapper struct {
*bufio.Writer *bufio.Writer
m *sync.RWMutex m *sync.RWMutex
} }
func (b *BufioWrapper) Write(p []byte) (nn int, err error) { func (b *bufioWrapper) Write(p []byte) (nn int, err error) {
b.m.RLock() b.m.RLock()
defer b.m.RUnlock() defer b.m.RUnlock()
return b.Writer.Write(p) return b.Writer.Write(p)
} }
func (b *BufioWrapper) Flush() error { func (b *bufioWrapper) Flush() error {
b.m.Lock() b.m.Lock()
defer b.m.Unlock() defer b.m.Unlock()
return b.Writer.Flush() return b.Writer.Flush()
} }
func (b *bufioWrapper) Close() error {
return b.Flush()
}

View File

@ -1,12 +1,14 @@
package logger package logger
import ( import (
"backend/src/request_context"
"context" "context"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog" "github.com/rs/zerolog"
) )
const RequestIdKey = "logger_request_id"
type Logger interface { type Logger interface {
Log() Event Log() Event
Warning() Event Warning() Event
@ -14,11 +16,14 @@ type Logger interface {
Fatal() Event Fatal() Event
Printf(format string, v ...any) Printf(format string, v ...any)
WithContext(ctx context.Context) Logger
WithPrefix(prefix string) Logger
} }
type logger struct { type logger struct {
prefix string prefix string
requestCtx request_context.RequestContext requestId string
zeroLogger *zerolog.Logger zeroLogger *zerolog.Logger
} }
@ -45,33 +50,38 @@ func (l logger) Printf(format string, v ...any) {
func (l logger) wrapEvent(zerologEvent *zerolog.Event) Event { func (l logger) wrapEvent(zerologEvent *zerolog.Event) Event {
var e Event = event{zerologEvent} var e Event = event{zerologEvent}
if l.requestCtx != nil { if l.requestId != "" {
e = e.Str("requestId", l.requestCtx.RequestId()) e = e.Str("requestId", l.requestId)
e = e.Str("userId", l.requestCtx.UserId()) }
if l.prefix != "" { if l.prefix != "" {
e = e.Str("prefix", l.prefix) e = e.Str("prefix", l.prefix)
}
} }
return e return e
} }
func (l logger) WithContext(ctx context.Context) Logger { func (l logger) WithContext(ctx context.Context) Logger {
if rctx, ok := ctx.(request_context.RequestContext); ok { requestIdVal := ctx.Value(RequestIdKey)
return logger{ requestId, ok := requestIdVal.(string)
prefix: l.prefix, if !ok || requestId == "" {
requestCtx: rctx, return l
zeroLogger: l.zeroLogger,
}
} }
return l return logger{
prefix: l.prefix,
requestId: requestId,
zeroLogger: l.zeroLogger,
}
} }
func (l logger) WithPrefix(prefix string) Logger { func (l logger) WithPrefix(prefix string) Logger {
return logger{ return logger{
prefix: prefix, prefix: prefix,
requestCtx: l.requestCtx, requestId: l.requestId,
zeroLogger: l.zeroLogger, zeroLogger: l.zeroLogger,
} }
} }
func SetCtxRequestId(ginCtx *gin.Context, requestId string) {
ginCtx.Set(RequestIdKey, requestId)
}

View File

@ -42,8 +42,8 @@ func New(opts NewLoggerOpts) (Logger, error) {
level = zerolog.DebugLevel level = zerolog.DebugLevel
} }
writer := bufio.NewWriterSize(io.MultiWriter(writers...), 32*1024) writer := bufio.NewWriterSize(io.MultiWriter(writers...), 8*1024)
wrapper := &BufioWrapper{writer, &sync.RWMutex{}} wrapper := &bufioWrapper{writer, &sync.RWMutex{}}
go func() { go func() {
tmr := time.NewTicker(500 * time.Millisecond) tmr := time.NewTicker(500 * time.Millisecond)
defer tmr.Stop() defer tmr.Stop()
@ -53,6 +53,7 @@ func New(opts NewLoggerOpts) (Logger, error) {
select { select {
case <-context.Background().Done(): case <-context.Background().Done():
wrapper.Flush()
return return
case <-tmr.C: case <-tmr.C:
} }

View File

@ -1,59 +0,0 @@
package request_context
import (
"context"
"time"
)
type RequestContext interface {
context.Context
RequestId() string
UserId() string
}
type Opts struct {
RequestId string
UserId string
}
func New(opts Opts) RequestContext {
return reqCtx{
requestId: opts.RequestId,
userId: opts.UserId,
}
}
type reqCtx struct {
ctx context.Context
requestId string
userId string
}
func (r reqCtx) Deadline() (deadline time.Time, ok bool) {
return r.ctx.Deadline()
}
func (r reqCtx) Done() <-chan struct{} {
return r.ctx.Done()
}
func (r reqCtx) Err() error {
return r.ctx.Err()
}
func (r reqCtx) Value(key any) any {
return r.ctx.Value(key)
}
func (r reqCtx) String() string {
return r.ctx.Err().Error()
}
func (r reqCtx) RequestId() string {
return r.requestId
}
func (r reqCtx) UserId() string {
return r.userId
}

View File

@ -2,12 +2,13 @@ package handlers
import ( import (
"backend/src/client_notifier" "backend/src/client_notifier"
"backend/src/logger"
"backend/src/server/utils" "backend/src/server/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func NewLongPoolingHandler(notifier client_notifier.ClientNotifier) gin.HandlerFunc { func NewLongPoolingHandler(logger logger.Logger, notifier client_notifier.ClientNotifier) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
user := utils.GetUserFromRequest(c) user := utils.GetUserFromRequest(c)
if user == nil { if user == nil {

View File

@ -2,6 +2,7 @@ package handlers
import ( import (
"backend/src/core/services" "backend/src/core/services"
"backend/src/logger"
"encoding/json" "encoding/json"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -19,38 +20,54 @@ type createUserOutput struct {
Name string `json:"name"` Name string `json:"name"`
} }
func NewUserCreateHandler(userService services.UserService) gin.HandlerFunc { func NewUserCreateHandler(logger logger.Logger, userService services.UserService) gin.HandlerFunc {
return func(ctx *gin.Context) { return func(c *gin.Context) {
ctxLogger := logger.WithContext(c)
params := createUserInput{} params := createUserInput{}
if err := ctx.ShouldBindJSON(&params); err != nil { if err := c.ShouldBindJSON(&params); err != nil {
ctx.AbortWithError(400, err) ctxLogger.Error().Err(err).Msg("bad input body model")
c.Data(400, "plain/text", []byte(err.Error()))
return return
} }
dto, err := userService.CreateUser(ctx, services.UserCreateParams{ dto, err := userService.CreateUser(
Email: params.Email, c,
Password: params.Password, services.UserCreateParams{
Name: params.Name, Email: params.Email,
}) Password: params.Password,
if err == services.ErrUserExists || err == services.ErrUserBadPassword { Name: params.Name,
ctx.Data(400, "plain/text", []byte(err.Error())) },
)
if err == services.ErrUserExists {
ctxLogger.Error().Err(err).Msg("user already exists")
c.Data(400, "plain/text", []byte(err.Error()))
return
}
if err == services.ErrUserBadPassword {
ctxLogger.Error().Err(err).Msg("password does not satisfy requirements")
c.Data(400, "plain/text", []byte(err.Error()))
return return
} }
if err != nil { if err != nil {
ctx.Data(500, "plain/text", []byte(err.Error())) ctxLogger.Error().Err(err).Msg("create user error")
c.Data(500, "plain/text", []byte(err.Error()))
return return
} }
resultBody, err := json.Marshal(createUserOutput{ resultBody, err := json.Marshal(
Id: dto.Id, createUserOutput{
Email: dto.Email, Id: dto.Id,
Name: dto.Name, Email: dto.Email,
}) Name: dto.Name,
},
)
if err != nil { if err != nil {
ctx.AbortWithError(500, err) ctxLogger.Error().Err(err).Msg("marshal user model error")
c.Data(500, "plain/text", []byte(err.Error()))
return return
} }
ctx.Data(200, "application/json", resultBody) c.Data(200, "application/json", resultBody)
} }
} }

View File

@ -2,6 +2,7 @@ package handlers
import ( import (
"backend/src/core/services" "backend/src/core/services"
"backend/src/logger"
"encoding/json" "encoding/json"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -16,21 +17,31 @@ type loginUserOutput struct {
Token string `json:"token"` Token string `json:"token"`
} }
func NewUserLoginHandler(userService services.UserService) gin.HandlerFunc { func NewUserLoginHandler(logger logger.Logger, userService services.UserService) gin.HandlerFunc {
return func(ctx *gin.Context) { return func(c *gin.Context) {
ctxLogger := logger.WithContext(c).WithPrefix("NewUserLoginHandler")
params := loginUserInput{} params := loginUserInput{}
if err := ctx.ShouldBindJSON(&params); err != nil { if err := c.ShouldBindJSON(&params); err != nil {
ctx.AbortWithError(400, err) ctxLogger.Error().Err(err).Msg("bad input body model")
c.AbortWithError(400, err)
return return
} }
token, err := userService.AuthenticateUser(ctx, params.Login, params.Password) token, err := userService.AuthenticateUser(c, params.Login, params.Password)
if err == services.ErrUserNotExists || err == services.ErrUserWrongPassword { if err == services.ErrUserNotExists {
ctx.AbortWithError(400, err) ctxLogger.Error().Err(err).Msg("user does not exist")
c.AbortWithError(400, err)
return
}
if err == services.ErrUserWrongPassword {
ctxLogger.Error().Err(err).Msg("wrong password")
c.AbortWithError(400, err)
return return
} }
if err != nil { if err != nil {
ctx.AbortWithError(500, err) ctxLogger.Error().Err(err).Msg("AuthenticateUser internal error")
c.AbortWithError(500, err)
return return
} }
@ -38,10 +49,11 @@ func NewUserLoginHandler(userService services.UserService) gin.HandlerFunc {
Token: token, Token: token,
}) })
if err != nil { if err != nil {
ctx.AbortWithError(500, err) ctxLogger.Error().Err(err).Msg("marshal json internal error")
c.AbortWithError(500, err)
return return
} }
ctx.Data(200, "application/json", resultBody) c.Data(200, "application/json", resultBody)
} }
} }

View File

@ -1,20 +1,22 @@
package middleware package middleware
import ( import (
"backend/src/logger" log "backend/src/logger"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
) )
func NewRequestLogMiddleware(logger logger.Logger) gin.HandlerFunc { func NewRequestLogMiddleware(logger log.Logger) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
requestId := c.GetHeader("X-Request-Id") requestId := c.GetHeader("X-Request-Id")
if requestId == "" { if requestId == "" {
requestId = uuid.New().String() requestId = uuid.New().String()
} }
log.SetCtxRequestId(c, requestId)
path := c.Request.URL.Path path := c.Request.URL.Path
if c.Request.URL.RawQuery != "" { if c.Request.URL.RawQuery != "" {
path = path + "?" + c.Request.URL.RawQuery path = path + "?" + c.Request.URL.RawQuery
@ -28,9 +30,10 @@ func NewRequestLogMiddleware(logger logger.Logger) gin.HandlerFunc {
statusCode := c.Writer.Status() statusCode := c.Writer.Status()
clientIP := c.ClientIP() clientIP := c.ClientIP()
e := logger.Log() ctxLogger := logger.WithContext(c)
e.Str("id", requestId)
e := ctxLogger.Log()
e.Str("ip", clientIP) e.Str("ip", clientIP)
e.Msgf("[REQUEST] %s %s %d %v", method, path, statusCode, latency) e.Msgf("Request %s %s %d %v", method, path, statusCode, latency)
} }
} }

View File

@ -32,20 +32,21 @@ func New(opts NewServerOpts) *Server {
} }
r := gin.New() r := gin.New()
r.Static("/webapp", "./webapp")
r.GET("/health", handlers.NewDummyHandler())
r.Use(middleware.NewRequestLogMiddleware(opts.Logger)) r.Use(middleware.NewRequestLogMiddleware(opts.Logger))
r.Use(middleware.NewRecoveryMiddleware(opts.Logger, opts.DebugMode)) r.Use(middleware.NewRecoveryMiddleware(opts.Logger, opts.DebugMode))
r.Static("/webapp", "./webapp") r.GET("/pooling", handlers.NewLongPoolingHandler(opts.Logger, opts.Notifier))
r.GET("/pooling", handlers.NewLongPoolingHandler(opts.Notifier))
linkGroup := r.Group("/s") linkGroup := r.Group("/s")
linkGroup.POST("/new", handlers.NewShortlinkCreateHandler(opts.ShortlinkService)) linkGroup.POST("/new", handlers.NewShortlinkCreateHandler(opts.ShortlinkService))
linkGroup.GET("/:linkId", handlers.NewShortlinkResolveHandler(opts.ShortlinkService)) linkGroup.GET("/:linkId", handlers.NewShortlinkResolveHandler(opts.ShortlinkService))
userGroup := r.Group("/user") userGroup := r.Group("/user")
userGroup.POST("/create", handlers.NewUserCreateHandler(opts.UserService)) userGroup.POST("/create", handlers.NewUserCreateHandler(opts.Logger, opts.UserService))
userGroup.POST("/login", handlers.NewUserLoginHandler(opts.UserService)) userGroup.POST("/login", handlers.NewUserLoginHandler(opts.Logger, opts.UserService))
dummyGroup := r.Group("/dummy") dummyGroup := r.Group("/dummy")
{ {