diff --git a/cmd/twhelp/bun.go b/cmd/twhelp/bun.go index 87c3ea2..4fe3b60 100644 --- a/cmd/twhelp/bun.go +++ b/cmd/twhelp/bun.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "log/slog" "runtime" "time" @@ -78,11 +79,16 @@ type bundDBConfig struct { writeTimeout time.Duration } +const dbPingTimeout = 10 * time.Second + func newBunDB(cfg bundDBConfig) (*bun.DB, error) { db := bun.NewDB(newSQLDB(cfg), pgdialect.New()) - if err := pingDB(db); err != nil { - return nil, err + ctx, cancel := context.WithTimeout(context.Background(), dbPingTimeout) + defer cancel() + + if err := db.PingContext(ctx); err != nil { + return nil, fmt.Errorf("couldn't ping db: %w", err) } return db, nil @@ -100,15 +106,11 @@ func newSQLDB(cfg bundDBConfig) *sql.DB { return db } -const dbPingTimeout = 10 * time.Second - -func pingDB(db *bun.DB) error { - ctx, cancel := context.WithTimeout(context.Background(), dbPingTimeout) - defer cancel() - - if err := db.PingContext(ctx); err != nil { - return fmt.Errorf("couldn't ping db: %w", err) +func closeBunDB(db *bun.DB, logger *slog.Logger) { + logger.Debug("closing db connections...", slog.Int("db.openConnections", db.Stats().OpenConnections)) + if dbCloseErr := db.Close(); dbCloseErr != nil { + logger.Warn("couldn't close db connections", slog.Any("error", dbCloseErr)) + } else { + logger.Debug("db connections closed") } - - return nil } diff --git a/cmd/twhelp/cmd_consumer.go b/cmd/twhelp/cmd_consumer.go index d0d0680..685a37d 100644 --- a/cmd/twhelp/cmd_consumer.go +++ b/cmd/twhelp/cmd_consumer.go @@ -342,22 +342,12 @@ func runConsumer(c *cli.Context, name string, registerHandlers registerConsumerH if err != nil { return err } - defer func() { - logger.Debug("closing db connections...", slog.Int("db.openConnections", bunDB.Stats().OpenConnections)) - if dbCloseErr := bunDB.Close(); dbCloseErr != nil { - logger.Warn("couldn't close db connections", slog.Any("error", dbCloseErr)) - } else { - logger.Debug("db connections closed") - } - }() + defer closeBunDB(bunDB, logger) healthObserver := healthfile.LiveObserver(health.New(), "/tmp/live") defer func() { - logger.Debug("closing health observer...") if closeErr := healthObserver.Close(); closeErr != nil { logger.Warn("couldn't close health observer", slog.Any("error", closeErr)) - } else { - logger.Debug("health observer closed") } }() diff --git a/cmd/twhelp/cmd_db.go b/cmd/twhelp/cmd_db.go index 308c2a5..72f4fa6 100644 --- a/cmd/twhelp/cmd_db.go +++ b/cmd/twhelp/cmd_db.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "log/slog" "strings" @@ -48,14 +49,7 @@ var cmdDB = &cli.Command{ if err != nil { return err } - defer func() { - logger.Debug("closing db connections...", slog.Int("db.openConnections", bunDB.Stats().OpenConnections)) - if dbCloseErr := bunDB.Close(); dbCloseErr != nil { - logger.Warn("couldn't close db connections", slog.Any("error", dbCloseErr)) - } else { - logger.Debug("db connections closed") - } - }() + defer closeBunDB(bunDB, logger) migrator := migrations.NewMigrator(bunDB) @@ -63,19 +57,13 @@ var cmdDB = &cli.Command{ return fmt.Errorf("couldn't init migrator: %w", err) } + // we use c.Context instead of shutdownSignalCtx here because we still want to unlock the db + // even after one of the shutdown signals has been received + defer unlockMigrations(c.Context, migrator, logger) + if err = migrator.Lock(shutdownSignalCtx); err != nil { return fmt.Errorf("couldn't lock db: %w", err) } - defer func() { - logger.Debug("unlocking db...") - // we use c.Context instead of shutdownSignalCtx here because we still want to unlock the db - // even after one of the shutdown signals has been received - if unlockErr := migrator.Unlock(c.Context); unlockErr != nil { - logger.Warn("couldn't unlock db", slog.Any("error", unlockErr)) - } else { - logger.Debug("db unlocked") - } - }() group, err := migrator.Migrate(shutdownSignalCtx) if err != nil { @@ -110,30 +98,17 @@ var cmdDB = &cli.Command{ if err != nil { return err } - defer func() { - logger.Debug("closing db connections...", slog.Int("db.openConnections", bunDB.Stats().OpenConnections)) - if dbCloseErr := bunDB.Close(); dbCloseErr != nil { - logger.Warn("couldn't close db connections", slog.Any("error", dbCloseErr)) - } else { - logger.Debug("db connections closed") - } - }() + defer closeBunDB(bunDB, logger) migrator := migrations.NewMigrator(bunDB) + // we use c.Context instead of shutdownSignalCtx here because we still want to unlock the db + // even after one of the shutdown signals has been received + defer unlockMigrations(c.Context, migrator, logger) + if err = migrator.Lock(shutdownSignalCtx); err != nil { return fmt.Errorf("couldn't lock db: %w", err) } - defer func() { - logger.Debug("unlocking db...") - // we use c.Context instead of shutdownSignalCtx here because we still want to unlock the db - // even after one of the shutdown signals has been received - if unlockErr := migrator.Unlock(c.Context); unlockErr != nil { - logger.Warn("couldn't unlock db", slog.Any("error", unlockErr)) - } else { - logger.Debug("db unlocked") - } - }() group, err := migrator.Rollback(shutdownSignalCtx) if err != nil { @@ -212,3 +187,12 @@ var cmdDB = &cli.Command{ }, }, } + +func unlockMigrations(ctx context.Context, migrator *migrate.Migrator, logger *slog.Logger) { + logger.Debug("unlocking db...") + if unlockErr := migrator.Unlock(ctx); unlockErr != nil { + logger.Warn("couldn't unlock db", slog.Any("error", unlockErr)) + } else { + logger.Debug("db unlocked") + } +} diff --git a/cmd/twhelp/cmd_job.go b/cmd/twhelp/cmd_job.go index a3bebce..f528596 100644 --- a/cmd/twhelp/cmd_job.go +++ b/cmd/twhelp/cmd_job.go @@ -51,14 +51,7 @@ var ( if err != nil { return err } - defer func() { - logger.Debug("closing db connections...", slog.Int("db.openConnections", bunDB.Stats().OpenConnections)) - if dbCloseErr := bunDB.Close(); dbCloseErr != nil { - logger.Warn("couldn't close db connections", slog.Any("error", dbCloseErr)) - } else { - logger.Debug("db connections closed") - } - }() + defer closeBunDB(bunDB, logger) serverPublisher := adapter.NewServerWatermillPublisher( publisher, @@ -114,14 +107,7 @@ var ( if err != nil { return err } - defer func() { - logger.Debug("closing db connections...", slog.Int("db.openConnections", bunDB.Stats().OpenConnections)) - if dbCloseErr := bunDB.Close(); dbCloseErr != nil { - logger.Warn("couldn't close db connections", slog.Any("error", dbCloseErr)) - } else { - logger.Debug("db connections closed") - } - }() + defer closeBunDB(bunDB, logger) ennoblementPublisher := adapter.NewEnnoblementWatermillPublisher( publisher, @@ -183,14 +169,7 @@ var ( if err != nil { return err } - defer func() { - logger.Debug("closing db connections...", slog.Int("db.openConnections", bunDB.Stats().OpenConnections)) - if dbCloseErr := bunDB.Close(); dbCloseErr != nil { - logger.Warn("couldn't close db connections", slog.Any("error", dbCloseErr)) - } else { - logger.Debug("db connections closed") - } - }() + defer closeBunDB(bunDB, logger) tribeSnapshotPublisher := adapter.NewSnapshotWatermillPublisher( publisher, diff --git a/cmd/twhelp/cmd_serve.go b/cmd/twhelp/cmd_serve.go index 1e68219..333ab81 100644 --- a/cmd/twhelp/cmd_serve.go +++ b/cmd/twhelp/cmd_serve.go @@ -8,9 +8,9 @@ import ( "net/http" "net/url" "slices" - "strings" "time" + "gitea.dwysokinski.me/Kichiyaki/chiclientip" "gitea.dwysokinski.me/twhelp/corev3/internal/adapter" "gitea.dwysokinski.me/twhelp/corev3/internal/app" "gitea.dwysokinski.me/twhelp/corev3/internal/chislog" @@ -18,6 +18,7 @@ import ( "gitea.dwysokinski.me/twhelp/corev3/internal/port" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" + "github.com/realclientip/realclientip-go" "github.com/urfave/cli/v2" ) @@ -27,6 +28,12 @@ var ( EnvVars: []string{"API_PORT"}, Value: 9234, //nolint:gomnd } + apiServerHandlerTimeoutFlag = &cli.DurationFlag{ + Name: "api.handlerTimeout", + EnvVars: []string{"API_HANDLER_TIMEOUT"}, + Value: 5 * time.Second, //nolint:gomnd, + Usage: "https://pkg.go.dev/net/http#TimeoutHandler", + } apiServerReadTimeoutFlag = &cli.DurationFlag{ Name: "api.readTimeout", EnvVars: []string{"API_READ_TIMEOUT"}, @@ -68,6 +75,7 @@ var ( } apiServerFlags = []cli.Flag{ apiServerPortFlag, + apiServerHandlerTimeoutFlag, apiServerReadTimeoutFlag, apiServerReadHeaderTimeoutFlag, apiServerWriteTimeoutFlag, @@ -95,14 +103,7 @@ var cmdServe = &cli.Command{ if err != nil { return err } - defer func() { - logger.Debug("closing db connections...", slog.Int("db.openConnections", bunDB.Stats().OpenConnections)) - if dbCloseErr := bunDB.Close(); dbCloseErr != nil { - logger.Warn("couldn't close db connections", slog.Any("error", dbCloseErr)) - } else { - logger.Debug("db connections closed") - } - }() + defer closeBunDB(bunDB, logger) // adapters versionRepo := adapter.NewVersionBunRepository(bunDB) @@ -139,20 +140,25 @@ var cmdServe = &cli.Command{ return oapiCfgErr } - r.Use(newAPIMiddlewares(logger)...) + r.Group(func(r chi.Router) { + r.Use(middleware.Recoverer) + r.Mount(metaBasePath, port.NewMetaHTTPHandler(h)) + }) - r.Mount(metaBasePath, port.NewMetaHTTPHandler(h)) + r.Group(func(r chi.Router) { + r.Use(newAPIMiddlewares(c, logger)...) - r.Mount(apiBasePath, port.NewAPIHTTPHandler( - versionSvc, - serverSvc, - tribeSvc, - playerSvc, - villageSvc, - ennoblementSvc, - tribeChangeSvc, - port.WithOpenAPIConfig(oapiCfg), - )) + r.Mount(apiBasePath, port.NewAPIHTTPHandler( + versionSvc, + serverSvc, + tribeSvc, + playerSvc, + villageSvc, + ennoblementSvc, + tribeChangeSvc, + port.WithOpenAPIConfig(oapiCfg), + )) + }) return nil }, @@ -213,11 +219,21 @@ func newOpenAPIConfigFromFlags(c *cli.Context) (port.OpenAPIConfig, error) { }, nil } -func newAPIMiddlewares(logger *slog.Logger) chi.Middlewares { +func newAPIMiddlewares(c *cli.Context, logger *slog.Logger) chi.Middlewares { return chi.Middlewares{ - chislog.Logger(logger, chislog.WithFilter(func(r *http.Request) bool { - return !strings.HasPrefix(r.URL.Path, metaBasePath) + chiclientip.ClientIP( + realclientip.NewChainStrategy( + realclientip.Must(realclientip.NewRightmostNonPrivateStrategy("X-Forwarded-For")), + realclientip.RemoteAddrStrategy{}, + ), + ), + chislog.Logger(logger, chislog.WithIPExtractor(func(r *http.Request) string { + clientIP, _ := chiclientip.ClientIPFromContext(r.Context()) + return clientIP })), middleware.Recoverer, + func(next http.Handler) http.Handler { + return http.TimeoutHandler(next, c.Duration(apiServerHandlerTimeoutFlag.Name), "Timeout") + }, } } diff --git a/go.mod b/go.mod index 1f1eb43..f5a3492 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module gitea.dwysokinski.me/twhelp/corev3 go 1.22 require ( + gitea.dwysokinski.me/Kichiyaki/chiclientip v0.1.0 github.com/ThreeDotsLabs/watermill v1.3.5 github.com/ThreeDotsLabs/watermill-amqp/v2 v2.1.1 github.com/brianvoe/gofakeit/v7 v7.0.2 @@ -16,6 +17,7 @@ require ( github.com/google/uuid v1.6.0 github.com/oapi-codegen/runtime v1.1.1 github.com/ory/dockertest/v3 v3.10.0 + github.com/realclientip/realclientip-go v1.0.0 github.com/stretchr/testify v1.9.0 github.com/uptrace/bun v1.1.17 github.com/uptrace/bun/dbfixture v1.1.17 diff --git a/go.sum b/go.sum index e017bb6..7b0419e 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +gitea.dwysokinski.me/Kichiyaki/chiclientip v0.1.0 h1:5e5Uh+Am1PBSW1cYsbAuEhrGSZHxP7JfNeNa+FXbQ/4= +gitea.dwysokinski.me/Kichiyaki/chiclientip v0.1.0/go.mod h1:zMTruKo30+qM3dG4yhAPOxK/5hUEBhKiDSrV0VRlZnI= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -155,6 +157,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rabbitmq/amqp091-go v1.9.0 h1:qrQtyzB4H8BQgEuJwhmVQqVHB9O4+MNDJCCAcpc3Aoo= github.com/rabbitmq/amqp091-go v1.9.0/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc= +github.com/realclientip/realclientip-go v1.0.0 h1:+yPxeC0mEaJzq1BfCt2h4BxlyrvIIBzR6suDc3BEF1U= +github.com/realclientip/realclientip-go v1.0.0/go.mod h1:CXnUdVwFRcXFJIRb/dTYqbT7ud48+Pi2pFm80bxDmcI= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=