diff --git a/tools/migration/internal/logger.go b/tools/migration/internal/logger.go new file mode 100644 index 0000000000..7e625ac381 --- /dev/null +++ b/tools/migration/internal/logger.go @@ -0,0 +1,47 @@ +package internal + +import ( + "io" + "log" + "os" +) + +// Logger logs migration events to disk and, if initialized with verbose, +// stdout too. +type Logger struct { + closer io.WriteCloser + logger *log.Logger +} + +// NewLogger creates a new Logger. All log writes go to f, the logging file. +// If the verbose flag is true all log writes also go to stdout. +func NewLogger(wc io.WriteCloser, verbose bool) *Logger { + // by default just write to file + var w io.Writer + w = wc + if verbose { + w = io.MultiWriter(wc, os.Stdout) + } + return &Logger{ + closer: wc, + logger: log.New(w, "", 0), + } +} + +// Error logs an error to the logging output. +func (l *Logger) Error(err error) { + if err == nil { + return + } + l.logger.Printf("ERROR: %s", err.Error()) +} + +// Print logs a string to the logging output. +func (l *Logger) Print(msg string) { + l.logger.Print(msg) +} + +// Close closes the logfile backing the Logger. +func (l *Logger) Close() error { + return l.closer.Close() +} diff --git a/tools/migration/internal/logger_test.go b/tools/migration/internal/logger_test.go new file mode 100644 index 0000000000..a008858a91 --- /dev/null +++ b/tools/migration/internal/logger_test.go @@ -0,0 +1,75 @@ +package internal_test + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + . "github.com/filecoin-project/go-filecoin/tools/migration/internal" +) + +func TestLoggerWritesToFile(t *testing.T) { + f, err := ioutil.TempFile("", "logfile") + require.NoError(t, err) + defer func() { + require.NoError(t, os.Remove(f.Name())) + }() + + logger := NewLogger(f, false) + logger.Print("testing print 1") + errTest := errors.New("testing error 2") + logger.Error(errTest) + + // Reopen file so we can read new writes + out, err := ioutil.ReadFile(f.Name()) + require.NoError(t, err) + outStr := string(out) + assert.Contains(t, outStr, "testing print 1") + expectedErrStr := fmt.Sprintf("ERROR: %s", errTest.Error()) + assert.Contains(t, outStr, expectedErrStr) +} + +func TestLoggerWritesToBothVerbose(t *testing.T) { + // Point os.Stdout to a temp file + fStdout, err := ioutil.TempFile("", "stdout") + require.NoError(t, err) + defer func() { + require.NoError(t, os.Remove(fStdout.Name())) + }() + old := os.Stdout + os.Stdout = fStdout + defer func() { os.Stdout = old }() + + // Create log file + fLogFile, err := ioutil.TempFile("", "logfile") + require.NoError(t, err) + defer func() { + require.NoError(t, os.Remove(fLogFile.Name())) + }() + + // Log verbosely + logger := NewLogger(fLogFile, true) + logger.Print("test line") + errTest := errors.New("test err") + logger.Error(errTest) + expectedErrStr := fmt.Sprintf("ERROR: %s", errTest.Error()) + + // Check logfile + outLogFile, err := ioutil.ReadFile(fLogFile.Name()) + require.NoError(t, err) + outLogFileStr := string(outLogFile) + assert.Contains(t, outLogFileStr, "test line") + assert.Contains(t, outLogFileStr, expectedErrStr) + + // Check stdout alias file + outStdout, err := ioutil.ReadFile(fStdout.Name()) + require.NoError(t, err) + outStdoutStr := string(outStdout) + assert.Contains(t, outStdoutStr, "test line") + assert.Contains(t, outStdoutStr, expectedErrStr) +} diff --git a/tools/migration/internal/runner.go b/tools/migration/internal/runner.go index d9f6e620d2..a46e57e0af 100644 --- a/tools/migration/internal/runner.go +++ b/tools/migration/internal/runner.go @@ -28,17 +28,17 @@ type Migration interface { // MigrationRunner represent a migration command type MigrationRunner struct { - verbose bool + logger *Logger command string oldRepoOpt string } // NewMigrationRunner builds a MirgrationRunner for the given command and repo options -func NewMigrationRunner(verb bool, command, oldRepoOpt string) *MigrationRunner { +func NewMigrationRunner(logger *Logger, command, oldRepoOpt string) *MigrationRunner { // TODO: Issue #2585 Implement repo migration version detection and upgrade decisioning return &MigrationRunner{ - verbose: verb, + logger: logger, command: command, oldRepoOpt: oldRepoOpt, } @@ -47,5 +47,6 @@ func NewMigrationRunner(verb bool, command, oldRepoOpt string) *MigrationRunner // Run executes the MigrationRunner func (m *MigrationRunner) Run() error { // TODO: Issue #2595 Implement first repo migration - return nil + + return m.logger.Close() } diff --git a/tools/migration/internal/runner_test.go b/tools/migration/internal/runner_test.go index 197336fd23..4cbb789f06 100644 --- a/tools/migration/internal/runner_test.go +++ b/tools/migration/internal/runner_test.go @@ -1,9 +1,12 @@ package internal_test import ( + "io/ioutil" + "os" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" tf "github.com/filecoin-project/go-filecoin/testhelpers/testflags" . "github.com/filecoin-project/go-filecoin/tools/migration/internal" @@ -12,7 +15,12 @@ import ( // TODO: Issue #2595 Implement first repo migration func TestMigrationRunner_Run(t *testing.T) { tf.UnitTest(t) - - runner := NewMigrationRunner(false, "describe", "/home/filecoin-symlink") + dummyLogFile, err := ioutil.TempFile("", "logfile") + require.NoError(t, err) + logger := NewLogger(dummyLogFile, false) + defer func() { + require.NoError(t, os.Remove(dummyLogFile.Name())) + }() + runner := NewMigrationRunner(logger, "describe", "/home/filecoin-symlink") assert.NoError(t, runner.Run()) } diff --git a/tools/migration/main.go b/tools/migration/main.go index 3183370422..08082f00b5 100644 --- a/tools/migration/main.go +++ b/tools/migration/main.go @@ -6,9 +6,13 @@ import ( "os" "strings" + "github.com/mitchellh/go-homedir" + "github.com/filecoin-project/go-filecoin/tools/migration/internal" ) +const defaultLogFilePath = "~/.filecoin-migration-logs" + // USAGE is the usage of the migration tool const USAGE = ` USAGE @@ -34,6 +38,8 @@ OPTIONS This message -v --verbose Print diagnostic messages to stdout + --log-file + The path of the file for writing detailed log output EXAMPLES for a migration from version 1 to 2: @@ -60,13 +66,18 @@ func main() { // nolint: deadcode case "-h", "--help": showUsageAndExit(0) case "describe", "buildonly", "migrate", "install": - oldRepoOpt, found := findOpt("old-repo", os.Args) + logFile, err := openLogFile() + if err != nil { + exitErr(err.Error()) + } + logger := internal.NewLogger(logFile, getVerbose()) + oldRepoOpt, found := findOpt("old-repo", os.Args) if found == false { exitErr(fmt.Sprintf("Error: --old-repo is required\n%s\n", USAGE)) } - runner := internal.NewMigrationRunner(getVerbose(), command, oldRepoOpt) + runner := internal.NewMigrationRunner(logger, command, oldRepoOpt) if err := runner.Run(); err != nil { exitErr(err.Error()) } @@ -97,6 +108,22 @@ func getVerbose() bool { return res } +func openLogFile() (*os.File, error) { + path, err := getLogFilePath() + if err != nil { + return nil, err + } + return os.OpenFile(path, os.O_APPEND|os.O_CREATE, 0644) +} + +func getLogFilePath() (string, error) { + if logPath, found := findOpt("--log-file", os.Args); found { + return logPath, nil + } + + return homedir.Expand(defaultLogFilePath) +} + // findOpt fetches option values. // returns: string: value of option set with "=". If not set, returns "" // bool: true if option was found, false if not