diff --git a/BACKEND_BLUEPRINT.md b/BACKEND_BLUEPRINT.md index a924bb0..2427146 100644 --- a/BACKEND_BLUEPRINT.md +++ b/BACKEND_BLUEPRINT.md @@ -52,6 +52,8 @@ ### Logging - Zap JSON logs +- Uber Zap logger will be initialized in `cmd/api/main.go` and injected into services/middleware (no global loggers). +- Logs will be written directly to a file from the application, with log rotation implemented via `lumberjack`. - Correlation/request ID in middleware - Structured error logging from middleware and service boundaries @@ -64,6 +66,7 @@ - SQL-only migrations - Keep up/down migration scripts - Run on startup in non-prod optional, required in CI/CD/deploy step +- Migrations are securely bundled into the binary using Go's `embed.FS` from a dedicated `migrations` package to isolate them from `internal` db logic. ### SQLC @@ -82,7 +85,10 @@ - `emit_interface: true` (generated `Querier` interface) - `emit_json_tags: false` (can be revisited if API structs are returned directly) - Initial queries implemented for users: `GetUser`, `CreateUser`, `DeleteUser` -- DB pool wiring into `cmd/api` and service construction is planned next +- **Goose startup migrations** have been wired into `cmd/api/main.go`, utilizing the `embed.FS` strategy and logging via Zap adapter. +- DB pool is successfully wired in `cmd/api`. +- Environment-aware Zap logger is configured (development vs production). +- **Next Planned:** Implement application-level file logging with rotation (using `lumberjack`), and build initial Gin API routes for health checks. --- diff --git a/api b/api new file mode 100755 index 0000000..c11bc9f Binary files /dev/null and b/api differ diff --git a/cmd/api/main.go b/cmd/api/main.go index acc00d2..0d0daee 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -2,16 +2,21 @@ package main import ( "context" - "log" "time" "git.kanopo.dev/rhythm/rhythm-backend/internal/config" "git.kanopo.dev/rhythm/rhythm-backend/internal/db" + "git.kanopo.dev/rhythm/rhythm-backend/internal/logger" "github.com/jackc/pgx/v5/pgxpool" ) func main() { cfg := config.Load() + log := logger.New(cfg.AppEnv) + defer log.Sync() + + log.Info("Starting rhythm") + ctx := context.Background() pool, err := pgxpool.New(ctx, cfg.DbUrl) if err != nil { @@ -25,9 +30,9 @@ func main() { if err := pool.Ping(ctx); err != nil { log.Fatalf("ping to db failed %v", err.Error()) } - log.Printf("successfully connected to database") + log.Info("successfully connected to database") - db.RunMigrations(cfg.DbUrl) + db.RunMigrations(cfg.DbUrl, log) } } diff --git a/go.mod b/go.mod index cd10afd..fc5aae9 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.26.2 require ( github.com/jackc/pgx/v5 v5.9.2 github.com/joho/godotenv v1.5.1 + github.com/pressly/goose v2.7.0+incompatible github.com/pressly/goose/v3 v3.27.0 ) @@ -13,8 +14,10 @@ require ( github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/mfridman/interpolate v0.0.2 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.1 golang.org/x/sync v0.19.0 // indirect golang.org/x/text v0.34.0 // indirect ) diff --git a/go.sum b/go.sum index 80c6095..e91ddc3 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,12 @@ github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6B github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pressly/goose v2.7.0+incompatible h1:PWejVEv07LCerQEzMMeAtjuyCKbyprZ/LBa6K5P0OCQ= +github.com/pressly/goose v2.7.0+incompatible/go.mod h1:m+QHWCqxR3k8D9l7qfzuC/djtlfzxr34mozWDYEu1z8= github.com/pressly/goose/v3 v3.27.0 h1:/D30gVTuQhu0WsNZYbJi4DMOsx1lNq+6SkLe+Wp59BM= github.com/pressly/goose/v3 v3.27.0/go.mod h1:3ZBeCXqzkgIRvrEMDkYh1guvtoJTU5oMMuDdkutoM78= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= @@ -36,6 +40,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= diff --git a/internal/config/config.go b/internal/config/config.go index b87b245..3cb9dee 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -8,7 +8,8 @@ import ( ) type Config struct { - DbUrl string + AppEnv string + DbUrl string } func Load() Config { @@ -24,8 +25,14 @@ func Load() Config { dbUrl = fmt.Sprintf("postgres://%v:%v@%v:%v/%v?sslmode=disable", username, password, host, port, name) } + appEnv := os.Getenv("APP_ENV") + if appEnv == "" { + appEnv = "development" + } + cfg := Config{ - DbUrl: dbUrl, + AppEnv: appEnv, + DbUrl: dbUrl, } return cfg diff --git a/internal/db/migration.go b/internal/db/migration.go index 461a64a..1ef36ae 100644 --- a/internal/db/migration.go +++ b/internal/db/migration.go @@ -4,17 +4,22 @@ import ( "database/sql" "log" + "git.kanopo.dev/rhythm/rhythm-backend/internal/logger" "git.kanopo.dev/rhythm/rhythm-backend/migrations" _ "github.com/jackc/pgx/v5/stdlib" "github.com/pressly/goose/v3" + "go.uber.org/zap" ) -func RunMigrations(dbURL string) { +func RunMigrations(dbURL string, zLog *zap.SugaredLogger) { db, err := sql.Open("pgx", dbURL) if err != nil { log.Fatalf("open db for migrations: %v", err.Error()) } defer db.Close() + + goose.SetLogger(&logger.GooseLogger{SugaredLogger: zLog}) + // 2. Pass the exported FS to Goose! goose.SetBaseFS(migrations.FS) if err := goose.SetDialect("postgres"); err != nil { diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..447cb4e --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,43 @@ +package logger + +import ( + "go.uber.org/zap" + "log" +) + +func New(env string) *zap.SugaredLogger { + var zapLogger *zap.Logger + var err error + + if env == "production" { + zapLogger, err = zap.NewProduction() + } else { + zapLogger, err = zap.NewDevelopment() + } + + if err != nil { + log.Fatalf("failed to initialize zap logger: %v", err) + } + + return zapLogger.Sugar() +} + +type GooseLogger struct { + *zap.SugaredLogger +} + +func (l *GooseLogger) Fatal(v ...interface{}) { + l.SugaredLogger.Fatal(v...) +} + +func (l *GooseLogger) Fatalf(format string, v ...interface{}) { + l.SugaredLogger.Fatalf(format, v...) +} + +func (l *GooseLogger) Print(v ...interface{}) { + l.SugaredLogger.Info(v...) +} + +func (l *GooseLogger) Printf(format string, v ...interface{}) { + l.SugaredLogger.Infof(format, v...) +}