From 483246d8a1a48d131131b6b1006981db019131d4 Mon Sep 17 00:00:00 2001 From: Laba Subagia Date: Thu, 14 Sep 2023 20:22:38 +0800 Subject: [PATCH] feat: log --- .env.example | 1 + .env.test | 1 + .vscode/settings.json | 4 +- cmd/root.go | 6 +- cmd/server.go | 24 ++++--- go.mod | 3 +- go.sum | 11 +-- internal/adapter/handler/main.go | 4 +- internal/adapter/handler/restful/logger.go | 37 ++++++++++ .../adapter/handler/restful/middleware.go | 4 +- internal/adapter/handler/restful/server.go | 20 +++--- internal/adapter/logger/main.go | 10 +++ internal/adapter/logger/zerolog.go | 67 +++++++++++++++++++ internal/adapter/repository/main.go | 10 +-- .../adapter/repository/mongo/repository.go | 9 +-- internal/adapter/repository/sql/db/db.go | 24 ++++--- internal/adapter/repository/sql/db/logger.go | 39 +++++++++++ internal/adapter/repository/sql/repository.go | 12 ++-- internal/core/port/logger.go | 17 +++++ internal/core/service/main_test.go | 23 ++++--- internal/core/service/service.go | 4 +- internal/core/util/config.go | 6 ++ main.go | 6 +- 23 files changed, 269 insertions(+), 73 deletions(-) create mode 100644 internal/adapter/handler/restful/logger.go create mode 100644 internal/adapter/logger/main.go create mode 100644 internal/adapter/logger/zerolog.go create mode 100644 internal/adapter/repository/sql/db/logger.go create mode 100644 internal/core/port/logger.go diff --git a/.env.example b/.env.example index 6fa988a..04c208e 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,4 @@ +ENVIRONMENT=development # production POSTGRES_SOURCE=postgres://postgres:postgres@0.0.0.0:5432/realworld?sslmode=disable POSTGRES_MIGRATION_URL=file://internal/adapter/repository/sql/db/migration MONGO_SOURCE=mongodb://root:root@0.0.0.0:27017/realworld?authSource=admin diff --git a/.env.test b/.env.test index a048d06..0a9e3b3 100644 --- a/.env.test +++ b/.env.test @@ -1,3 +1,4 @@ +ENVIRONMENT=development POSTGRES_SOURCE=postgres://postgres:postgres@0.0.0.0:5432/realworld?sslmode=disable POSTGRES_MIGRATION_URL=file://../../../internal/adapter/repository/sql/db/migration MONGO_SOURCE=mongodb://root:root@0.0.0.0:27017/realworld?authSource=admin diff --git a/.vscode/settings.json b/.vscode/settings.json index 6c96dd8..34fdd21 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,6 +10,7 @@ "jackc", "labasubagia", "mapstructure", + "Msgf", "notnull", "nullzero", "oklog", @@ -22,7 +23,8 @@ "stretchr", "Upsert", "uptrace", - "writeconcern" + "writeconcern", + "zerolog" ], "go.testFlags": [ "-count=1" diff --git a/cmd/root.go b/cmd/root.go index 6af6977..16df740 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,7 +1,8 @@ package cmd import ( - "log" + "fmt" + "os" "github.com/labasubagia/realworld-backend/internal/core/util" "github.com/spf13/cobra" @@ -20,7 +21,8 @@ func init() { config, err = util.LoadConfig(".env") if err != nil { - log.Fatal("failed to load env config", err) + fmt.Fprintf(os.Stderr, "failed to load env config: %s", err) + os.Exit(1) } } diff --git a/cmd/server.go b/cmd/server.go index e20e2bb..8440222 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -2,10 +2,10 @@ package cmd import ( "fmt" - "log" "strings" "github.com/labasubagia/realworld-backend/internal/adapter/handler/restful" + "github.com/labasubagia/realworld-backend/internal/adapter/logger" "github.com/labasubagia/realworld-backend/internal/adapter/repository" "github.com/labasubagia/realworld-backend/internal/core/service" "github.com/spf13/cobra" @@ -29,35 +29,39 @@ var serverCmd = &cobra.Command{ Long: "Run gin server restful API", Run: func(cmd *cobra.Command, args []string) { + logger := logger.NewLogger(config) + port, err := cmd.Flags().GetInt("port") if err != nil { - log.Fatal("failed get port flag", err) + logger.Fatal().Err(err).Msg("failed to get flag port") } config.HTTPServerPort = port dbType, err := cmd.Flags().GetString("database") if err != nil { - log.Fatal("failed get database flag", err) + logger.Fatal().Err(err).Msg("failed to get flag database") } dbType = strings.ToLower(dbType) newRepo, exist := repository.RepoFnMap[dbType] if !exist { - log.Fatal("invalid database", dbType) + fmt.Println() + logger.Fatal().Err(err).Msg("failed to get flag port") } - repo, err := newRepo(config) + repo, err := newRepo(config, logger) if err != nil { - log.Fatal("failed to load repository", err) + logger.Fatal().Err(err).Msg("failed to load repository") } - service, err := service.NewService(config, repo) + + service, err := service.NewService(config, repo, logger) if err != nil { - log.Fatal("failed to load service", err) + logger.Fatal().Err(err).Msg("failed to load service") } - server := restful.NewServer(config, service) + server := restful.NewServer(config, service, logger) if server.Start(); err != nil { - log.Fatal("failed to load server", err) + logger.Fatal().Err(err).Msg("failed to load service") } }, } diff --git a/go.mod b/go.mod index 8dc08ba..0349d21 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,6 @@ require ( github.com/bytedance/sonic v1.10.0 // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/iasm v0.9.0 // indirect - github.com/fatih/color v1.15.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gin-contrib/sse v0.1.0 // indirect @@ -74,10 +73,10 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect + github.com/rs/zerolog v1.30.0 github.com/spf13/cobra v1.7.0 github.com/spf13/viper v1.16.0 github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect - github.com/uptrace/bun/extra/bundebug v1.1.14 github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect golang.org/x/sys v0.12.0 // indirect diff --git a/go.sum b/go.sum index 59784f1..966ff7b 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -83,8 +84,6 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= -github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= @@ -118,6 +117,7 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-migrate/migrate/v4 v4.16.2 h1:8coYbMKUyInrFk1lfGfRovTLAW7PhWp8qQDT2iKfuoA= @@ -236,6 +236,7 @@ github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= @@ -276,6 +277,9 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c= +github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= @@ -322,8 +326,6 @@ github.com/uptrace/bun v1.1.14 h1:S5vvNnjEynJ0CvnrBOD7MIRW7q/WbtvFXrdfy0lddAM= github.com/uptrace/bun v1.1.14/go.mod h1:RHk6DrIisO62dv10pUOJCz5MphXThuOTpVNYEYv7NI8= github.com/uptrace/bun/dialect/pgdialect v1.1.14 h1:b7+V1KDJPQSFYgkG/6YLXCl2uvwEY3kf/GSM7hTHRDY= github.com/uptrace/bun/dialect/pgdialect v1.1.14/go.mod h1:v6YiaXmnKQ2FlhRD2c0ZfKd+QXH09pYn4H8ojaavkKk= -github.com/uptrace/bun/extra/bundebug v1.1.14 h1:9OCGfP9ZDlh41u6OLerWdhBtJAVGXHr0xtxO4xWi6t0= -github.com/uptrace/bun/extra/bundebug v1.1.14/go.mod h1:lto3guzS2v6mnQp1+akyE+ecBLOltevDDe324NXEYdw= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= @@ -493,6 +495,7 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/adapter/handler/main.go b/internal/adapter/handler/main.go index 459ced1..85c82e2 100644 --- a/internal/adapter/handler/main.go +++ b/internal/adapter/handler/main.go @@ -6,6 +6,6 @@ import ( "github.com/labasubagia/realworld-backend/internal/core/util" ) -func NewServer(config util.Config, service port.Service) port.Server { - return restful.NewServer(config, service) +func NewServer(config util.Config, service port.Service, logger port.Logger) port.Server { + return restful.NewServer(config, service, logger) } diff --git a/internal/adapter/handler/restful/logger.go b/internal/adapter/handler/restful/logger.go new file mode 100644 index 0000000..56f7a45 --- /dev/null +++ b/internal/adapter/handler/restful/logger.go @@ -0,0 +1,37 @@ +package restful + +import ( + "io" + "net/http" + "time" + + "github.com/gin-gonic/gin" +) + +func (s *Server) Logger() gin.HandlerFunc { + return func(c *gin.Context) { + // process request + startTime := time.Now() + c.Next() + duration := time.Since(startTime) + + // log + logger := s.logger.Info() + if c.Writer.Status() >= 500 { + logger = s.logger.Error() + if c.Request != nil && c.Request.Body != nil { + if body, err := io.ReadAll(c.Request.Body); err == nil { + logger.Field("body", body) + } + } + } + logger. + Field("protocol", "http"). + Field("method", c.Request.Method). + Field("path", c.Request.URL.Path). + Field("status_code", c.Writer.Status()). + Field("status", http.StatusText(c.Writer.Status())). + Field("duration", duration). + Msg("received http request") + } +} diff --git a/internal/adapter/handler/restful/middleware.go b/internal/adapter/handler/restful/middleware.go index f9c4ecd..5aa6b58 100644 --- a/internal/adapter/handler/restful/middleware.go +++ b/internal/adapter/handler/restful/middleware.go @@ -10,9 +10,9 @@ const ( authorizationArgKey = "authorization_arg" ) -func (s *Server) AuthMiddleware(autoDenied bool) gin.HandlerFunc { +func (server *Server) AuthMiddleware(autoDenied bool) gin.HandlerFunc { return func(c *gin.Context) { - authArg, err := s.parseToken(c) + authArg, err := server.parseToken(c) if err != nil { if autoDenied { errorHandler(c, err) diff --git a/internal/adapter/handler/restful/server.go b/internal/adapter/handler/restful/server.go index a09ac3a..d5e46d3 100644 --- a/internal/adapter/handler/restful/server.go +++ b/internal/adapter/handler/restful/server.go @@ -3,7 +3,6 @@ package restful import ( "context" "fmt" - "log" "net/http" "os" "os/signal" @@ -20,12 +19,14 @@ type Server struct { config util.Config router *gin.Engine service port.Service + logger port.Logger } -func NewServer(config util.Config, service port.Service) port.Server { +func NewServer(config util.Config, service port.Service, logger port.Logger) port.Server { server := &Server{ config: config, service: service, + logger: logger, } server.setupRouter() return server @@ -33,8 +34,9 @@ func NewServer(config util.Config, service port.Service) port.Server { func (server *Server) setupRouter() { - router := gin.Default() - router.Use(cors.Default()) + gin.SetMode(gin.ReleaseMode) + router := gin.New() + router.Use(server.Logger(), gin.Recovery(), cors.Default()) router.GET("/", func(ctx *gin.Context) { ctx.JSON(http.StatusOK, gin.H{"message": "Hello World!"}) @@ -85,27 +87,27 @@ func (server *Server) Start() error { go func() { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Fatalf("listen: %s\n", err) + server.logger.Fatal().Err(err).Msg("failed listen") } }() quit := make(chan os.Signal) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit - log.Printf("Shutdown server...") + server.logger.Info().Msg("shutdown server...") ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { - log.Fatal("Server shutdown:", err) + server.logger.Fatal().Err(err).Msg("failed shutdown server") } select { case <-ctx.Done(): - log.Println("timeout of 5 seconds") + server.logger.Info().Msg("timeout in 5 seconds") } - log.Println("Server existing") + server.logger.Info().Msg("server exiting") return nil } diff --git a/internal/adapter/logger/main.go b/internal/adapter/logger/main.go new file mode 100644 index 0000000..d579d32 --- /dev/null +++ b/internal/adapter/logger/main.go @@ -0,0 +1,10 @@ +package logger + +import ( + "github.com/labasubagia/realworld-backend/internal/core/port" + "github.com/labasubagia/realworld-backend/internal/core/util" +) + +func NewLogger(config util.Config) port.Logger { + return NewZeroLogLogger(config) +} diff --git a/internal/adapter/logger/zerolog.go b/internal/adapter/logger/zerolog.go new file mode 100644 index 0000000..e41b2da --- /dev/null +++ b/internal/adapter/logger/zerolog.go @@ -0,0 +1,67 @@ +package logger + +import ( + "fmt" + "os" + "time" + + "github.com/labasubagia/realworld-backend/internal/core/port" + "github.com/labasubagia/realworld-backend/internal/core/util" + "github.com/rs/zerolog" +) + +type zeroLogLogger struct { + log zerolog.Logger + event *zerolog.Event +} + +func NewZeroLogLogger(config util.Config) port.Logger { + logger := zerolog.New(os.Stderr) + if !config.IsProduction() { + output := zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339} + logger = zerolog.New(output).With().Timestamp().Logger() + } + return &zeroLogLogger{ + log: logger, + event: logger.Info(), + } +} + +// Log Level + +func (l *zeroLogLogger) Info() port.Logger { + l.event = l.log.Info() + return l +} + +func (l *zeroLogLogger) Error() port.Logger { + l.event = l.log.Error() + return l +} + +func (l *zeroLogLogger) Fatal() port.Logger { + l.event = l.log.Fatal() + return l +} + +// Set Attributes + +func (l *zeroLogLogger) Err(err error) port.Logger { + l.event = l.event.Err(err) + return l +} + +func (l *zeroLogLogger) Field(key string, value any) port.Logger { + l.event = l.event.Any(key, value) + return l +} + +// Send + +func (l *zeroLogLogger) Msg(v ...any) { + l.event.Msg(fmt.Sprint(v...)) +} + +func (l *zeroLogLogger) Msgf(format string, v ...any) { + l.event.Msgf(format, v...) +} diff --git a/internal/adapter/repository/main.go b/internal/adapter/repository/main.go index 43b6fd5..7f84e3a 100644 --- a/internal/adapter/repository/main.go +++ b/internal/adapter/repository/main.go @@ -9,7 +9,7 @@ import ( const DefaultRepoKey = "default" -type FnNewRepo func(util.Config) (port.Repository, error) +type FnNewRepo func(util.Config, port.Logger) (port.Repository, error) var RepoFnMap = map[string]FnNewRepo{ DefaultRepoKey: sql.NewSQLRepository, @@ -17,10 +17,10 @@ var RepoFnMap = map[string]FnNewRepo{ "mongo": mongo.NewMongoRepository, } -func ListRepository(config util.Config) ([]port.Repository, error) { +func ListRepository(config util.Config, logger port.Logger) ([]port.Repository, error) { repos := []port.Repository{} for _, fn := range RepoFnMap { - repo, err := fn(config) + repo, err := fn(config, logger) if err != nil { return []port.Repository{}, err } @@ -29,6 +29,6 @@ func ListRepository(config util.Config) ([]port.Repository, error) { return repos, nil } -func NewRepository(config util.Config) (port.Repository, error) { - return RepoFnMap[DefaultRepoKey](config) +func NewRepository(config util.Config, logger port.Logger) (port.Repository, error) { + return RepoFnMap[DefaultRepoKey](config, logger) } diff --git a/internal/adapter/repository/mongo/repository.go b/internal/adapter/repository/mongo/repository.go index cfc98de..8e9be27 100644 --- a/internal/adapter/repository/mongo/repository.go +++ b/internal/adapter/repository/mongo/repository.go @@ -12,19 +12,20 @@ import ( type mongoRepo struct { db DB + logger port.Logger userRepo port.UserRepository articleRepo port.ArticleRepository } -func NewMongoRepository(config util.Config) (port.Repository, error) { +func NewMongoRepository(config util.Config, logger port.Logger) (port.Repository, error) { db, err := NewDB(config) if err != nil { return nil, err } - return create(db), nil + return create(db, logger), nil } -func create(db DB) port.Repository { +func create(db DB, logger port.Logger) port.Repository { return &mongoRepo{ db: db, userRepo: NewUserRepository(db), @@ -43,7 +44,7 @@ func (r *mongoRepo) Atomic(ctx context.Context, fn port.RepositoryAtomicCallback defer session.EndSession(ctx) _, err = session.WithTransaction(ctx, func(sessionCtx mongo.SessionContext) (any, error) { - if err := fn(create(r.db)); err != nil { + if err := fn(create(r.db, r.logger)); err != nil { return nil, intoException(err) } return nil, nil diff --git a/internal/adapter/repository/sql/db/db.go b/internal/adapter/repository/sql/db/db.go index 58ed52d..b7f1ef3 100644 --- a/internal/adapter/repository/sql/db/db.go +++ b/internal/adapter/repository/sql/db/db.go @@ -2,7 +2,7 @@ package db import ( "context" - "log" + "fmt" "github.com/golang-migrate/migrate/v4" _ "github.com/golang-migrate/migrate/v4/database/postgres" @@ -10,22 +10,24 @@ import ( "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/stdlib" "github.com/labasubagia/realworld-backend/internal/adapter/repository/sql/db/migration" + "github.com/labasubagia/realworld-backend/internal/core/port" "github.com/labasubagia/realworld-backend/internal/core/util" "github.com/uptrace/bun" "github.com/uptrace/bun/dialect/pgdialect" - "github.com/uptrace/bun/extra/bundebug" bunMigrate "github.com/uptrace/bun/migrate" ) type DB struct { config util.Config + logger port.Logger db *bun.DB } -func New(config util.Config) (*DB, error) { +func New(config util.Config, logger port.Logger) (*DB, error) { var err error database := &DB{ config: config, + logger: logger, } if database.db, err = database.connect(); err != nil { return nil, err @@ -47,21 +49,23 @@ func (db *DB) connect() (*bun.DB, error) { } sqlDB := stdlib.OpenDB(*config) database := bun.NewDB(sqlDB, pgdialect.New()) - database.AddQueryHook(bundebug.NewQueryHook(bundebug.WithVerbose(true))) + + // log + if !db.config.IsProduction() { + // database.AddQueryHook(bundebug.NewQueryHook(bundebug.WithVerbose(true))) + database.AddQueryHook(&LoggerHook{verbose: true, logger: db.logger}) + } return database, nil } func (db *DB) migrate() error { migration, err := migrate.New(db.config.PostgresMigrationURL, db.config.PostgresSource) if err != nil { - log.Printf("cannot create new migration instance: %s", err) - return err + return fmt.Errorf("cannot create new migration instance: %s", err) } if err := migration.Up(); err != nil && err != migrate.ErrNoChange { - log.Printf("failed to run migrate: %s", err) - return err + return fmt.Errorf("failed to run migrate: %s", err) } - log.Println("migration ok") return nil } @@ -82,9 +86,7 @@ func (db *DB) migrateBun() error { return err } if group.IsZero() { - log.Printf("there are no new migrations to run (database is up to date)\n") return nil } - log.Printf("migrated to %s\n", group) return nil } diff --git a/internal/adapter/repository/sql/db/logger.go b/internal/adapter/repository/sql/db/logger.go new file mode 100644 index 0000000..64b7f14 --- /dev/null +++ b/internal/adapter/repository/sql/db/logger.go @@ -0,0 +1,39 @@ +package db + +import ( + "context" + "database/sql" + "time" + + "github.com/labasubagia/realworld-backend/internal/core/port" + "github.com/uptrace/bun" +) + +type LoggerHook struct { + verbose bool + logger port.Logger +} + +func (h *LoggerHook) BeforeQuery(ctx context.Context, event *bun.QueryEvent) context.Context { + return ctx +} + +func (h *LoggerHook) AfterQuery(ctx context.Context, event *bun.QueryEvent) { + if !h.verbose { + switch event.Err { + case nil, sql.ErrNoRows, sql.ErrTxDone: + return + } + } + now := time.Now() + duration := now.Sub(event.StartTime) + + logger := h.logger.Info() + if event.Err != nil { + logger = h.logger.Error().Err(event.Err) + } + logger. + Field("duration", duration). + Field("query", event.Query). + Msgf("SQL %s", event.Operation()) +} diff --git a/internal/adapter/repository/sql/repository.go b/internal/adapter/repository/sql/repository.go index 86a3ddd..53ccef1 100644 --- a/internal/adapter/repository/sql/repository.go +++ b/internal/adapter/repository/sql/repository.go @@ -12,16 +12,17 @@ import ( type sqlRepo struct { db bun.IDB + logger port.Logger userRepo port.UserRepository articleRepo port.ArticleRepository } -func NewSQLRepository(config util.Config) (port.Repository, error) { - db, err := db.New(config) +func NewSQLRepository(config util.Config, logger port.Logger) (port.Repository, error) { + db, err := db.New(config, logger) if err != nil { return nil, err } - return create(db.DB()), nil + return create(db.DB(), logger), nil } func (r *sqlRepo) Atomic(ctx context.Context, fn port.RepositoryAtomicCallback) error { @@ -29,7 +30,7 @@ func (r *sqlRepo) Atomic(ctx context.Context, fn port.RepositoryAtomicCallback) ctx, &sql.TxOptions{Isolation: sql.LevelSerializable}, func(ctx context.Context, tx bun.Tx) error { - return fn(create(tx)) + return fn(create(tx, r.logger)) }, ) if err != nil { @@ -38,9 +39,10 @@ func (r *sqlRepo) Atomic(ctx context.Context, fn port.RepositoryAtomicCallback) return nil } -func create(db bun.IDB) port.Repository { +func create(db bun.IDB, logger port.Logger) port.Repository { return &sqlRepo{ db: db, + logger: logger, userRepo: NewUserRepository(db), articleRepo: NewArticleRepository(db), } diff --git a/internal/core/port/logger.go b/internal/core/port/logger.go new file mode 100644 index 0000000..134c222 --- /dev/null +++ b/internal/core/port/logger.go @@ -0,0 +1,17 @@ +package port + +type Logger interface { + // Level + // ? just limit to 2 + Info() Logger + Error() Logger + Fatal() Logger + Err(error) Logger + + // set attributes + Field(string, any) Logger + + // send + Msgf(string, ...any) + Msg(...any) +} diff --git a/internal/core/service/main_test.go b/internal/core/service/main_test.go index ebadb9b..37b5e7c 100644 --- a/internal/core/service/main_test.go +++ b/internal/core/service/main_test.go @@ -1,10 +1,11 @@ package service_test import ( - "log" + "fmt" "os" "testing" + "github.com/labasubagia/realworld-backend/internal/adapter/logger" "github.com/labasubagia/realworld-backend/internal/adapter/repository" "github.com/labasubagia/realworld-backend/internal/core/port" "github.com/labasubagia/realworld-backend/internal/core/service" @@ -17,8 +18,10 @@ var testService port.Service func TestMain(m *testing.M) { config, err := util.LoadConfig("../../../.env.test") if err != nil { - log.Fatal("failed load config", err) + fmt.Fprintln(os.Stderr, "failed to load config", err) + os.Exit(1) } + logger := logger.NewLogger(config) var code int @@ -27,15 +30,15 @@ func TestMain(m *testing.M) { // slower test // NOTE: add env TEST_REPO=all - repos, err := repository.ListRepository(config) + repos, err := repository.ListRepository(config, logger) if err != nil { - log.Fatal("failed load repositories", err) + logger.Fatal().Err(err).Msg("failed to load repository") } for _, repo := range repos { testRepo = repo - testService, err = service.NewService(config, testRepo) + testService, err = service.NewService(config, testRepo, logger) if err != nil { - log.Fatal("failed to init service", err) + logger.Fatal().Err(err).Msg("failed to load service") } code = m.Run() } @@ -44,14 +47,14 @@ func TestMain(m *testing.M) { // test main repo (currently used) // faster test - testRepo, err = repository.NewRepository(config) + testRepo, err = repository.NewRepository(config, logger) if err != nil { - log.Fatal("failed load main repository", err) + logger.Fatal().Err(err).Msg("failed to load repository") } - testService, err = service.NewService(config, testRepo) + testService, err = service.NewService(config, testRepo, logger) if err != nil { - log.Fatal("failed to init service", err) + logger.Fatal().Err(err).Msg("failed to load service") } code = m.Run() } diff --git a/internal/core/service/service.go b/internal/core/service/service.go index 27d4cd9..0258d77 100644 --- a/internal/core/service/service.go +++ b/internal/core/service/service.go @@ -10,6 +10,7 @@ type serviceProperty struct { config util.Config tokenMaker token.Maker repo port.Repository + logger port.Logger } type services struct { @@ -18,7 +19,7 @@ type services struct { userService port.UserService } -func NewService(config util.Config, repo port.Repository) (port.Service, error) { +func NewService(config util.Config, repo port.Repository, logger port.Logger) (port.Service, error) { tokenMaker, err := token.NewJWTMaker(config.TokenSymmetricKey) if err != nil { return nil, err @@ -27,6 +28,7 @@ func NewService(config util.Config, repo port.Repository) (port.Service, error) config: config, repo: repo, tokenMaker: tokenMaker, + logger: logger, } svc := services{ property: property, diff --git a/internal/core/util/config.go b/internal/core/util/config.go index 0a24691..3f76088 100644 --- a/internal/core/util/config.go +++ b/internal/core/util/config.go @@ -5,6 +5,8 @@ import ( ) type Config struct { + Environment string `mapstructure:"ENVIRONMENT"` + PostgresSource string `mapstructure:"POSTGRES_SOURCE"` PostgresMigrationURL string `mapstructure:"POSTGRES_MIGRATION_URL"` @@ -17,6 +19,10 @@ type Config struct { TestRepo string `mapstructure:"TEST_REPO"` } +func (c Config) IsProduction() bool { + return c.Environment == "production" +} + func (c Config) IsTestAllRepo() bool { return c.TestRepo == "all" } diff --git a/main.go b/main.go index 6ea2278..a736878 100644 --- a/main.go +++ b/main.go @@ -1,13 +1,9 @@ package main import ( - "log" - "github.com/labasubagia/realworld-backend/cmd" ) func main() { - if err := cmd.Execute(); err != nil { - log.Fatal("failed to run command", err) - } + cmd.Execute() }