diff --git a/provider.go b/provider.go index ec3b8749a..643d4dad6 100644 --- a/provider.go +++ b/provider.go @@ -5,8 +5,11 @@ import ( "database/sql" "errors" "fmt" + "io" "io/fs" + "log/slog" "math" + "os" "strconv" "strings" "sync" @@ -62,13 +65,32 @@ func NewProvider(dialect Dialect, db *sql.DB, fsys fs.FS, opts ...ProviderOption registered: make(map[int64]*Migration), excludePaths: make(map[string]bool), excludeVersions: make(map[int64]bool), - logger: &stdLogger{}, } for _, opt := range opts { if err := opt.apply(&cfg); err != nil { return nil, err } } + if cfg.logger == nil { + output := io.Discard + if cfg.verbose { + output = os.Stdout + } + replaceAttr := func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.TimeKey { + return slog.Attr{ + Key: slog.TimeKey, + Value: slog.StringValue(a.Value.Time().Format("2006-01-02 15:04:05")), + } + } + return a + } + handler := slog.NewTextHandler(output, &slog.HandlerOptions{ + Level: slog.LevelInfo, + ReplaceAttr: replaceAttr, + }) + cfg.logger = slog.New(handler).With(slog.String("logger", "goose")) + } // Allow users to specify a custom store implementation, but only if they don't specify a // dialect. If they specify a dialect, we'll use the default store implementation. if dialect == "" && cfg.store == nil { @@ -118,8 +140,9 @@ func newProvider( } // Skip adding global Go migrations if explicitly disabled. if cfg.disableGlobalRegistry { - // TODO(mf): let's add a warn-level log here to inform users if len(global) > 0. Would like - // to add this once we're on go1.21 and leverage the new slog package. + if len(global) > 0 { + cfg.logger.Warn("detected global go migrations, but global go migrations are disabled", slog.Int("count", len(global))) + } } else { for version, m := range global { if _, ok := versionToGoMigration[version]; ok { @@ -434,7 +457,7 @@ func (p *Provider) down( } // We never migrate the zero version down. if dbMigrations[0].Version == 0 { - p.printf("no migrations to run, current version: 0") + p.cfg.logger.Info("no migrations to run, current version: 0") return nil, nil } var apply []*Migration diff --git a/provider_options.go b/provider_options.go index 15ee99006..803bfc0ea 100644 --- a/provider_options.go +++ b/provider_options.go @@ -3,6 +3,7 @@ package goose import ( "errors" "fmt" + "log/slog" "github.com/pressly/goose/v3/database" "github.com/pressly/goose/v3/lock" @@ -165,10 +166,20 @@ func WithDisableVersioning(b bool) ProviderOption { }) } +// WithLogger sets the logger to use for logging. By default, goose will use a modified version of +// the [slog.NewTextHandler] with a custom format. +func WithLogger(logger *slog.Logger) ProviderOption { + return configFunc(func(c *config) error { + c.logger = logger + return nil + }) +} + type config struct { store database.Store verbose bool + logger *slog.Logger excludePaths map[string]bool excludeVersions map[int64]bool @@ -184,11 +195,6 @@ type config struct { disableVersioning bool allowMissing bool disableGlobalRegistry bool - - // Let's not expose the Logger just yet. Ideally we consolidate on the std lib slog package - // added in go1.21 and then expose that (if that's even necessary). For now, just use the std - // lib log package. - logger Logger } type configFunc func(*config) error diff --git a/provider_run.go b/provider_run.go index 58f354ac5..047cf8e01 100644 --- a/provider_run.go +++ b/provider_run.go @@ -6,8 +6,9 @@ import ( "errors" "fmt" "io/fs" + "log/slog" + "path/filepath" "runtime/debug" - "strings" "time" "github.com/pressly/goose/v3/database" @@ -66,17 +67,6 @@ func (p *Provider) prepareMigration(fsys fs.FS, m *Migration, direction bool) er return fmt.Errorf("invalid migration type: %+v", m) } -// printf is a helper function that prints the given message if verbose is enabled. It also prepends -// the "goose: " prefix to the message. -func (p *Provider) printf(msg string, args ...interface{}) { - if p.cfg.verbose { - if !strings.HasPrefix(msg, "goose:") { - msg = "goose: " + msg - } - p.cfg.logger.Printf(msg, args...) - } -} - // runMigrations runs migrations sequentially in the given direction. If the migrations list is // empty, return nil without error. func (p *Provider) runMigrations( @@ -94,7 +84,7 @@ func (p *Provider) runMigrations( if err != nil { return nil, err } - p.printf("no migrations to run, current version: %d", maxVersion) + p.cfg.logger.Info("no migrations to run", slog.Int64("current_version", maxVersion)) } return nil, nil } @@ -150,14 +140,19 @@ func (p *Provider) runMigrations( } result.Duration = time.Since(start) results = append(results, result) - p.printf("%s", result) + p.cfg.logger.Info("applied migration", + slog.String("source", filepath.Base(m.Source)), + slog.Any("direction", direction), + slog.Any("duration", result.Duration), + slog.Bool("empty", result.Empty), + ) } if !p.cfg.disableVersioning && !byOne { maxVersion, err := p.getDBMaxVersion(ctx, conn) if err != nil { return nil, err } - p.printf("successfully migrated database, current version: %d", maxVersion) + p.cfg.logger.Info("successfully migrated database", slog.Int64("current_version", maxVersion)) } return results, nil }