diff --git a/cmd/root.go b/cmd/root.go index a4b31a4eb..7cee0e77f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,7 +6,7 @@ import ( "strings" "github.com/spf13/cobra" - flag "github.com/spf13/pflag" + "github.com/spf13/pflag" "github.com/spf13/viper" ) @@ -82,7 +82,7 @@ func setupVersionBuild() { // setAllConfig looks for environment variables which are capitalized versions // of the flag names with dashes replaced by underscores, and prefixed with // envPrefix plus an underscore. -func setAllConfig(v *viper.Viper, flags *flag.FlagSet, envPrefix string) error { +func setAllConfig(v *viper.Viper, flags *pflag.FlagSet, envPrefix string) error { // add cmd line flag def to viper err := v.BindPFlags(flags) if err != nil { @@ -108,7 +108,7 @@ func setAllConfig(v *viper.Viper, flags *flag.FlagSet, envPrefix string) error { // set all values from viper var flagErr error - flags.VisitAll(func(f *flag.Flag) { + flags.VisitAll(func(f *pflag.Flag) { if flagErr != nil { return } diff --git a/cmd/root_test.go b/cmd/root_test.go index 74dac2919..d59deb2e6 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -15,6 +15,8 @@ import ( "github.com/spf13/cobra" ) +// failErr calls t.Fatal if err != nil and adds the optional context to the +// error message. func failErr(t *testing.T, err error, context ...string) { ctx := strings.Join(context, "; ") if err != nil { @@ -58,10 +60,16 @@ func ExecNewRootCommand(t *testing.T, args ...string) (string, error) { return string(output), err } +// validator is a simple helper to avoid repeated `if err != nil` checks in +// validation code. One can use it to check that several pairs of things are +// equal, and at the end access an informative error message about the first +// non-equal pair encountered (or nil if all were equal) type validator struct { err error } +// Check that two things are equal, and if not set v.err to a descriptive error +// message. func (v *validator) Check(actual, expected interface{}) { if v.err != nil { return @@ -70,8 +78,14 @@ func (v *validator) Check(actual, expected interface{}) { v.err = fmt.Errorf("Actual: '%v' is not equal to '%v'", actual, expected) } } + +// Error returns the validator's error value if any v.Check call found an error. func (v *validator) Error() error { return v.err } +// commandTest represents all possible ways to configure a a pilosa command, as +// well as a function for validating whether the command worked as expected. +// args should be set to everything that comes after "pilosa" on the comand +// line. See tests like backup_test.go for examples. type commandTest struct { args []string env map[string]string @@ -79,6 +93,29 @@ type commandTest struct { validation func() error } +// executeDry sets up and executes each commandTest with the --dry-run flag set +// to true, and then executes the tests validation function. This stops +// execution after PersistentPreRunE (and so before the command's Run or RunE +// function is called). This is useful for verifying that configuration happened +// properly. +func executeDry(t *testing.T, tests []commandTest) { + for i, test := range tests { + test.args = append(test.args[:1], append([]string{"--dry-run"}, test.args[1:]...)...) + com := test.setupCommand(t) + err := com.Execute() + if err.Error() != "dry run" { + t.Fatalf("Problem with test %d, err: '%v'", i, err) + } + if err := test.validation(); err != nil { + t.Fatalf("Failed test %d due to: %v", i, err) + } + test.reset() + } +} + +// setupCommand sets up all the configuration specified in the commandTest so +// that it can be run. This includes setting environment variables, and creating +// a temp config file with the cfgFileContent string as its content. func (ct *commandTest) setupCommand(t *testing.T) *cobra.Command { // make config file cfgFile, err := ioutil.TempFile("", "") @@ -105,27 +142,13 @@ func (ct *commandTest) setupCommand(t *testing.T) *cobra.Command { return rc } +// reset the environment after setup/run of a commandTest. func (ct *commandTest) reset() { for name, _ := range ct.env { os.Setenv(name, "") } } -func executeDry(t *testing.T, tests []commandTest) { - for i, test := range tests { - test.args = append(test.args[:1], append([]string{"--dry-run"}, test.args[1:]...)...) - com := test.setupCommand(t) - err := com.Execute() - if err.Error() != "dry run" { - t.Fatalf("Problem with test %d, err: '%v'", i, err) - } - if err := test.validation(); err != nil { - t.Fatalf("Failed test %d due to: %v", i, err) - } - test.reset() - } -} - func TestRootCommand(t *testing.T) { outStr, err := ExecNewRootCommand(t, "--help") if !strings.Contains(outStr, "Usage:") || diff --git a/cmd/server.go b/cmd/server.go index 5d5035ba7..20de85a3f 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -79,6 +79,7 @@ on the configured port.`, flags.StringSliceVarP(&Server.Config.Cluster.Nodes, "cluster.hosts", "", []string{}, "Comma separated list of hosts in cluster.") flags.DurationVarP((*time.Duration)(&Server.Config.Cluster.PollingInterval), "cluster.poll-interval", "", time.Minute, "Polling interval for cluster.") // TODO what actually is this? flags.StringVarP(&Server.Config.Plugins.Path, "plugins.path", "", "", "Path to plugin directory.") + flags.StringVar(&Server.Config.LogPath, "log-path", "", "Log path") flags.DurationVarP((*time.Duration)(&Server.Config.AntiEntropy.Interval), "anti-entropy.interval", "", time.Minute*10, "Interval at which to run anti-entropy routine.") flags.StringVarP(&Server.CPUProfile, "profile.cpu", "", "", "Where to store CPU profile.") flags.DurationVarP(&Server.CPUTime, "profile.cpu-time", "", 30*time.Second, "CPU profile duration.") diff --git a/cmd/server_test.go b/cmd/server_test.go index d677d67a8..927ce8b66 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -1,6 +1,7 @@ package cmd_test import ( + "errors" "io/ioutil" "strings" "testing" @@ -23,6 +24,8 @@ func TestServerConfig(t *testing.T) { failErr(t, err, "making data dir") profFile, err := ioutil.TempFile("", "") failErr(t, err, "making temp file") + logFile, err := ioutil.TempFile("", "") + failErr(t, err, "making log file") tests := []commandTest{ // TEST 0 { @@ -73,7 +76,7 @@ data-dir = "` + actualDataDir + `" }, // TEST 2 { - args: []string{"server"}, + args: []string{"server", "--log-path", logFile.Name()}, env: map[string]string{"PILOSA_PROFILE.CPU_TIME": "1m"}, cfgFileContent: ` bind = "localhost:0" @@ -96,7 +99,16 @@ data-dir = "` + actualDataDir + `" v.Check(cmd.Server.Config.AntiEntropy.Interval, pilosa.Duration(time.Minute*11)) v.Check(cmd.Server.CPUProfile, profFile.Name()) v.Check(cmd.Server.CPUTime, time.Minute) - return v.Error() + v.Check(cmd.Server.Config.LogPath, logFile.Name()) + if v.Error() != nil { + return v.Error() + } + // confirm log file was written + info, err := logFile.Stat() + if err != nil || info.Size() == 0 { + return errors.New("Log file was not written!") + } + return nil }, }, } diff --git a/config.go b/config.go index e664c0771..8250c7f91 100644 --- a/config.go +++ b/config.go @@ -26,6 +26,8 @@ type Config struct { AntiEntropy struct { Interval Duration `toml:"interval"` } `toml:"anti-entropy"` + + LogPath string `toml:"log-path"` } // NewConfig returns an instance of Config with default options. diff --git a/db.go b/db.go index 965da17fa..abedaeead 100644 --- a/db.go +++ b/db.go @@ -86,15 +86,16 @@ func (db *DB) SetColumnLabel(v string) error { db.mu.Lock() defer db.mu.Unlock() + // Ignore if no change occurred. + if v == "" || db.columnLabel == v { + return nil + } + // Make sure columnLabel is valid name err := ValidateName(v) if err != nil { return err } - // Ignore if no change occurred. - if v == "" || db.columnLabel == v { - return nil - } // Persist meta data to disk on change. db.columnLabel = v @@ -373,14 +374,17 @@ func (db *DB) createFrame(name string, opt FrameOptions) (*Frame, error) { return nil, err } - // Update options. - if opt.RowLabel != "" { - err := ValidateName(opt.RowLabel) - if err != nil { - return nil, err - } + // Default the time quantum to what is set on the DB. + if err := f.SetTimeQuantum(db.timeQuantum); err != nil { + f.Close() + return nil, err + } + + // Set options. + if err := f.SetRowLabel(opt.RowLabel); err != nil { + f.Close() + return nil, err } - f.SetRowLabel(opt.RowLabel) // Add to database's frame lookup. db.frames[name] = f diff --git a/db_test.go b/db_test.go index 5ce17478f..f7cfc906a 100644 --- a/db_test.go +++ b/db_test.go @@ -34,6 +34,25 @@ func TestDB_CreateFrameIfNotExists(t *testing.T) { } } +// Ensure database defaults the time quantum on new frames. +func TestDB_CreateFrame_TimeQuantum(t *testing.T) { + db := MustOpenDB() + defer db.Close() + + // Set database time quantum. + if err := db.SetTimeQuantum(pilosa.TimeQuantum("YM")); err != nil { + t.Fatal(err) + } + + // Create frame. + f, err := db.CreateFrame("f", pilosa.FrameOptions{}) + if err != nil { + t.Fatal(err) + } else if q := f.TimeQuantum(); q != pilosa.TimeQuantum("YM") { + t.Fatalf("unexpected frame time quantum: %s", q) + } +} + // Ensure database can delete a frame. func TestDB_DeleteFrame(t *testing.T) { db := MustOpenDB() diff --git a/executor.go b/executor.go index db67b9d0b..efb9e8199 100644 --- a/executor.go +++ b/executor.go @@ -490,26 +490,27 @@ func (e *Executor) executeProfile(ctx context.Context, db string, c *pql.Call, o // executeClearBit executes a ClearBit() call. func (e *Executor) executeClearBit(ctx context.Context, db string, c *pql.Call, opt *ExecOptions) (bool, error) { + view, _ := c.Args["view"].(string) frame, ok := c.Args["frame"].(string) if !ok { return false, errors.New("ClearBit() frame required") } - // Lookup column label. + // Retrieve frame. d := e.Index.DB(db) if d == nil { - return false, nil + return false, ErrDatabaseNotFound } - columnLabel := d.ColumnLabel() - - // Lookup row label. - f := e.Index.Frame(db, frame) + f := d.Frame(frame) if f == nil { - return false, nil + return false, ErrFrameNotFound } + + // Retrieve labels. + columnLabel := d.ColumnLabel() rowLabel := f.RowLabel() - // Read row & column ids. + // Read fields using labels. rowID, ok := c.Args[rowLabel].(uint64) if !ok { return false, fmt.Errorf("ClearBit() field required: %s", rowLabel) @@ -520,12 +521,38 @@ func (e *Executor) executeClearBit(ctx context.Context, db string, c *pql.Call, return false, fmt.Errorf("ClearBit() field required: %s", columnLabel) } + // Clear bits for each view. + switch view { + case ViewStandard: + return e.executeClearBitView(ctx, db, c, f, view, colID, rowID, opt) + case ViewInverse: + return e.executeClearBitView(ctx, db, c, f, view, rowID, colID, opt) + case "": + var ret bool + if changed, err := e.executeClearBitView(ctx, db, c, f, ViewStandard, colID, rowID, opt); err != nil { + return ret, err + } else if changed { + ret = true + } + if changed, err := e.executeClearBitView(ctx, db, c, f, ViewInverse, rowID, colID, opt); err != nil { + return ret, err + } else if changed { + ret = true + } + return ret, nil + default: + return false, fmt.Errorf("invalid view: %s", view) + } +} + +// executeClearBitView executes a ClearBit() call for a single view. +func (e *Executor) executeClearBitView(ctx context.Context, db string, c *pql.Call, f *Frame, view string, colID, rowID uint64, opt *ExecOptions) (bool, error) { slice := colID / SliceWidth ret := false for _, node := range e.Cluster.FragmentNodes(db, slice) { // Update locally if host matches. if node.Host == e.Host { - val, err := f.ClearBit(rowID, colID, nil) + val, err := f.ClearBit(view, rowID, colID, nil) if err != nil { return false, err } else if val { @@ -550,6 +577,7 @@ func (e *Executor) executeClearBit(ctx context.Context, db string, c *pql.Call, // executeSetBit executes a SetBit() call. func (e *Executor) executeSetBit(ctx context.Context, db string, c *pql.Call, opt *ExecOptions) (bool, error) { + view, _ := c.Args["view"].(string) frame, ok := c.Args["frame"].(string) if !ok { return false, errors.New("SetBit() field required: frame") @@ -558,7 +586,7 @@ func (e *Executor) executeSetBit(ctx context.Context, db string, c *pql.Call, op // Retrieve frame. d := e.Index.DB(db) if d == nil { - return false, ErrFrameNotFound + return false, ErrDatabaseNotFound } f := d.Frame(frame) if f == nil { @@ -590,13 +618,39 @@ func (e *Executor) executeSetBit(ctx context.Context, db string, c *pql.Call, op timestamp = &t } + // Set bits for each view. + switch view { + case ViewStandard: + return e.executeSetBitView(ctx, db, c, f, view, colID, rowID, timestamp, opt) + case ViewInverse: + return e.executeSetBitView(ctx, db, c, f, view, rowID, colID, timestamp, opt) + case "": + var ret bool + if changed, err := e.executeSetBitView(ctx, db, c, f, ViewStandard, colID, rowID, timestamp, opt); err != nil { + return ret, err + } else if changed { + ret = true + } + if changed, err := e.executeSetBitView(ctx, db, c, f, ViewInverse, rowID, colID, timestamp, opt); err != nil { + return ret, err + } else if changed { + ret = true + } + return ret, nil + default: + return false, fmt.Errorf("invalid view: %s", view) + } +} + +// executeSetBitView executes a SetBit() call for a specific view. +func (e *Executor) executeSetBitView(ctx context.Context, db string, c *pql.Call, f *Frame, view string, colID, rowID uint64, timestamp *time.Time, opt *ExecOptions) (bool, error) { slice := colID / SliceWidth ret := false for _, node := range e.Cluster.FragmentNodes(db, slice) { // Update locally if host matches. if node.Host == e.Host { - val, err := f.SetBit(rowID, colID, timestamp) + val, err := f.SetBit(view, rowID, colID, timestamp) if err != nil { return false, err } else if val { diff --git a/executor_test.go b/executor_test.go index 424aba12a..b6850c54b 100644 --- a/executor_test.go +++ b/executor_test.go @@ -390,16 +390,16 @@ func TestExecutor_Execute_Range(t *testing.T) { } // Set bits. - f.MustSetBit(1, 2, MustParseTimePtr("1999-12-31 00:00")) - f.MustSetBit(1, 3, MustParseTimePtr("2000-01-01 00:00")) - f.MustSetBit(1, 4, MustParseTimePtr("2000-01-02 00:00")) - f.MustSetBit(1, 5, MustParseTimePtr("2000-02-01 00:00")) - f.MustSetBit(1, 6, MustParseTimePtr("2001-01-01 00:00")) - f.MustSetBit(1, 7, MustParseTimePtr("2002-01-01 02:00")) + f.MustSetBit(pilosa.ViewStandard, 1, 2, MustParseTimePtr("1999-12-31 00:00")) + f.MustSetBit(pilosa.ViewStandard, 1, 3, MustParseTimePtr("2000-01-01 00:00")) + f.MustSetBit(pilosa.ViewStandard, 1, 4, MustParseTimePtr("2000-01-02 00:00")) + f.MustSetBit(pilosa.ViewStandard, 1, 5, MustParseTimePtr("2000-02-01 00:00")) + f.MustSetBit(pilosa.ViewStandard, 1, 6, MustParseTimePtr("2001-01-01 00:00")) + f.MustSetBit(pilosa.ViewStandard, 1, 7, MustParseTimePtr("2002-01-01 02:00")) - f.MustSetBit(1, 2, MustParseTimePtr("1999-12-30 00:00")) // too early - f.MustSetBit(1, 2, MustParseTimePtr("2002-02-01 00:00")) // too late - f.MustSetBit(10, 2, MustParseTimePtr("2001-01-01 00:00")) // different bitmap + f.MustSetBit(pilosa.ViewStandard, 1, 2, MustParseTimePtr("1999-12-30 00:00")) // too early + f.MustSetBit(pilosa.ViewStandard, 1, 2, MustParseTimePtr("2002-02-01 00:00")) // too late + f.MustSetBit(pilosa.ViewStandard, 10, 2, MustParseTimePtr("2001-01-01 00:00")) // different bitmap e := NewExecutor(idx.Index, NewCluster(1)) if res, err := e.Execute(context.Background(), "d", MustParse(`Range(id=1, frame=f, start="1999-12-31T00:00", end="2002-01-01T03:00")`), nil, nil); err != nil { diff --git a/frame.go b/frame.go index 21db2c24d..8b7b45326 100644 --- a/frame.go +++ b/frame.go @@ -110,15 +110,17 @@ func (f *Frame) SetRowLabel(v string) error { f.mu.Lock() defer f.mu.Unlock() + // Ignore if no change occurred. + if v == "" || f.rowLabel == v { + return nil + } + + // Make sure rowLabel is valid name err := ValidateName(v) if err != nil { return err } - // Ignore if no change occurred. - if v == "" || f.rowLabel == v { - return nil - } // Persist meta data to disk on change. f.rowLabel = v @@ -353,28 +355,13 @@ func (f *Frame) newView(path, name string) *View { return view } -// SetBit sets a bit within the frame. -func (f *Frame) SetBit(rowID, colID uint64, t *time.Time) (changed bool, err error) { - // Set standard layout bits. - if v, err := f.setBit(ViewStandard, rowID, colID, t); err != nil { - return changed, err - } else if v { - changed = v +// SetBit sets a bit on a view within the frame. +func (f *Frame) SetBit(name string, rowID, colID uint64, t *time.Time) (changed bool, err error) { + // Validate view name. + if !IsValidView(name) { + return false, ErrInvalidView } - // Set inverse layout bits. - // NOTE: The row & col are transposed for the inverted view. - if v, err := f.setBit(ViewInverse, colID, rowID, t); err != nil { - return changed, err - } else if v { - changed = v - } - - return changed, nil -} - -// setBit sets a bit for a given layout (default or inverted). -func (f *Frame) setBit(name string, rowID, colID uint64, t *time.Time) (changed bool, err error) { // Retrieve view. Exit if it doesn't exist. view, err := f.CreateViewIfNotExists(name) if err != nil { @@ -411,27 +398,12 @@ func (f *Frame) setBit(name string, rowID, colID uint64, t *time.Time) (changed } // ClearBit clears a bit within the frame. -func (f *Frame) ClearBit(rowID, colID uint64, t *time.Time) (changed bool, err error) { - // Clear standard layout bits. - if v, err := f.clearBit(ViewStandard, rowID, colID, t); err != nil { - return changed, err - } else if v { - changed = v +func (f *Frame) ClearBit(name string, rowID, colID uint64, t *time.Time) (changed bool, err error) { + // Validate view name. + if !IsValidView(name) { + return false, ErrInvalidView } - // Clear inverse layout bits. - // NOTE: The row & col are transposed for the inverted view. - if v, err := f.clearBit(ViewInverse, colID, rowID, t); err != nil { - return changed, err - } else if v { - changed = v - } - - return changed, nil -} - -// clearBit clears a bit for a given layout (default or inverted). -func (f *Frame) clearBit(name string, rowID, colID uint64, t *time.Time) (changed bool, err error) { // Retrieve view. Exit if it doesn't exist. view, err := f.CreateViewIfNotExists(name) if err != nil { diff --git a/frame_test.go b/frame_test.go index 8a86f1ee8..5b659f826 100644 --- a/frame_test.go +++ b/frame_test.go @@ -119,8 +119,8 @@ func (f *Frame) Reopen() error { } // MustSetBit sets a bit on the frame. Panic on error. -func (f *Frame) MustSetBit(bitmapID, profileID uint64, t *time.Time) (changed bool) { - changed, err := f.SetBit(bitmapID, profileID, t) +func (f *Frame) MustSetBit(view string, bitmapID, profileID uint64, t *time.Time) (changed bool) { + changed, err := f.SetBit(view, bitmapID, profileID, t) if err != nil { panic(err) } diff --git a/handler_test.go b/handler_test.go index 20f76de05..257e59713 100644 --- a/handler_test.go +++ b/handler_test.go @@ -39,12 +39,14 @@ func TestHandler_Schema(t *testing.T) { if f, err := d0.CreateFrameIfNotExists("f1", pilosa.FrameOptions{}); err != nil { t.Fatal(err) - } else if _, err := f.SetBit(0, 0, nil); err != nil { + } else if _, err := f.SetBit(pilosa.ViewStandard, 0, 0, nil); err != nil { + t.Fatal(err) + } else if _, err := f.SetBit(pilosa.ViewInverse, 0, 0, nil); err != nil { t.Fatal(err) } if f, err := d1.CreateFrameIfNotExists("f0", pilosa.FrameOptions{}); err != nil { t.Fatal(err) - } else if _, err := f.SetBit(0, 0, nil); err != nil { + } else if _, err := f.SetBit(pilosa.ViewStandard, 0, 0, nil); err != nil { t.Fatal(err) } if _, err := d0.CreateFrameIfNotExists("f0", pilosa.FrameOptions{}); err != nil { @@ -57,7 +59,7 @@ func TestHandler_Schema(t *testing.T) { h.ServeHTTP(w, MustNewHTTPRequest("GET", "/schema", nil)) if w.Code != http.StatusOK { t.Fatalf("unexpected status code: %d", w.Code) - } else if body := w.Body.String(); body != `{"dbs":[{"name":"d0","frames":[{"name":"f0"},{"name":"f1","views":[{"name":"inverse"},{"name":"standard"}]}]},{"name":"d1","frames":[{"name":"f0","views":[{"name":"inverse"},{"name":"standard"}]}]}]}`+"\n" { + } else if body := w.Body.String(); body != `{"dbs":[{"name":"d0","frames":[{"name":"f0"},{"name":"f1","views":[{"name":"inverse"},{"name":"standard"}]}]},{"name":"d1","frames":[{"name":"f0","views":[{"name":"standard"}]}]}]}`+"\n" { t.Fatalf("unexpected body: %s", body) } } @@ -95,11 +97,11 @@ func TestHandler_MaxSlices_Inverse(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := f0.SetBit((1*SliceWidth)+1, 30, nil); err != nil { + if _, err := f0.SetBit(pilosa.ViewInverse, 30, (1*SliceWidth)+1, nil); err != nil { t.Fatal(err) - } else if _, err := f0.SetBit((1*SliceWidth)+2, 30, nil); err != nil { + } else if _, err := f0.SetBit(pilosa.ViewInverse, 30, (1*SliceWidth)+2, nil); err != nil { t.Fatal(err) - } else if _, err := f0.SetBit((3*SliceWidth)+4, 30, nil); err != nil { + } else if _, err := f0.SetBit(pilosa.ViewInverse, 30, (3*SliceWidth)+4, nil); err != nil { t.Fatal(err) } @@ -107,11 +109,11 @@ func TestHandler_MaxSlices_Inverse(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := f1.SetBit((0*SliceWidth)+1, 40, nil); err != nil { + if _, err := f1.SetBit(pilosa.ViewStandard, 40, (0*SliceWidth)+1, nil); err != nil { t.Fatal(err) - } else if _, err := f1.SetBit((0*SliceWidth)+2, 40, nil); err != nil { + } else if _, err := f1.SetBit(pilosa.ViewInverse, 40, (0*SliceWidth)+2, nil); err != nil { t.Fatal(err) - } else if _, err := f1.SetBit((0*SliceWidth)+4, 40, nil); err != nil { + } else if _, err := f1.SetBit(pilosa.ViewInverse, 40, (0*SliceWidth)+4, nil); err != nil { t.Fatal(err) } diff --git a/pilosa.go b/pilosa.go index d7e61e46b..93dd9fef3 100644 --- a/pilosa.go +++ b/pilosa.go @@ -20,6 +20,8 @@ var ( ErrFrameExists = errors.New("frame already exists") ErrFrameNotFound = errors.New("frame not found") + ErrInvalidView = errors.New("invalid veiw") + ErrName = errors.New("invalid database or frame's name, must match [a-z0-9_-]") // ErrFragmentNotFound is returned when a fragment does not exist. diff --git a/server/server.go b/server/server.go index cd9aa3cb0..469b5bc3b 100644 --- a/server/server.go +++ b/server/server.go @@ -68,7 +68,15 @@ func (m *Command) Run(args ...string) (err error) { } // Setup logging output. - m.Server.LogOutput = m.Stderr + if m.Config.LogPath == "" { + m.Server.LogOutput = m.Stderr + } else { + logFile, err := os.OpenFile(m.Config.LogPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600) + if err != nil { + return err + } + m.Server.LogOutput = logFile + } // Configure index. fmt.Fprintf(m.Stderr, "Using data from: %s\n", m.Config.DataDir) @@ -108,7 +116,17 @@ func normalizeHost(host string) (string, error) { // Close shuts down the server. func (m *Command) Close() error { - err := m.Server.Close() + var logErr error + serveErr := m.Server.Close() + logOutput := m.Server.LogOutput + if closer, ok := logOutput.(io.Closer); ok { + logErr = closer.Close() + } close(m.Done) - return err + if serveErr != nil && logErr != nil { + return fmt.Errorf("closing server: '%v', closing logs: '%v'", serveErr, logErr) + } else if logErr != nil { + return logErr + } + return serveErr } diff --git a/view.go b/view.go index b53cea6aa..a04764aa8 100644 --- a/view.go +++ b/view.go @@ -17,6 +17,11 @@ const ( ViewInverse = "inverse" ) +// IsValidView returns true if name is valid. +func IsValidView(name string) bool { + return name == ViewStandard || name == ViewInverse +} + // View represents a container for frame data. type View struct { mu sync.Mutex