fix(cli): apply config logging, strict explicit config paths and exit codes
- commit
- 0a610cc414bc87ef59c956c5d2c3fea69a6e56d3
- author
- brice · · 2026-09-23 10:52 UTC
- parent
- cf75d01
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
6 files · +130 -32
M cmd/nefertty/main.go +63 -22
| @@ -2,11 +2,13 @@ package main | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | + | "errors" | |
| 5 | 6 | "flag" | |
| 6 | 7 | "fmt" | |
| 7 | 8 | "os" | |
| 8 | 9 | "os/signal" | |
| 9 | 10 | "runtime/debug" | |
| 11 | + | "strings" | |
| 10 | 12 | "syscall" | |
| 11 | 13 | ||
| 12 | 14 | "github.com/bnema/nefertty/internal/adapters/config" | |
| @@ -14,24 +16,47 @@ import ( | |||
| 14 | 16 | "github.com/bnema/nefertty/internal/logging" | |
| 15 | 17 | ) | |
| 16 | 18 | ||
| 17 | - | func main() { | |
| 18 | - | if err := run(); err != nil { | |
| 19 | - | os.Exit(1) | |
| 19 | + | type usageError struct{ error } | |
| 20 | + | ||
| 21 | + | func main() { os.Exit(runCode()) } | |
| 22 | + | func runCode() int { | |
| 23 | + | err := run() | |
| 24 | + | if err == nil || errors.Is(err, flag.ErrHelp) || errors.Is(err, context.Canceled) { | |
| 25 | + | return 0 | |
| 26 | + | } | |
| 27 | + | var usage usageError | |
| 28 | + | if errors.As(err, &usage) { | |
| 29 | + | return 2 | |
| 30 | + | } | |
| 31 | + | return 1 | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | func mergeDebug(configured []string, cli string) string { | |
| 35 | + | if cli == "" { | |
| 36 | + | return strings.Join(configured, ",") | |
| 37 | + | } | |
| 38 | + | if len(configured) == 0 { | |
| 39 | + | return cli | |
| 20 | 40 | } | |
| 41 | + | return strings.Join(configured, ",") + "," + cli | |
| 21 | 42 | } | |
| 22 | 43 | ||
| 23 | 44 | func run() error { | |
| 24 | 45 | if len(os.Args) > 1 && os.Args[1] == "validate-config" { | |
| 25 | 46 | if len(os.Args) > 3 { | |
| 26 | - | err := fmt.Errorf("usage: validate-config [path]") | |
| 47 | + | err := usageError{fmt.Errorf("usage: validate-config [path]")} | |
| 27 | 48 | fmt.Fprintln(os.Stderr, err) | |
| 28 | 49 | return err | |
| 29 | 50 | } | |
| 30 | 51 | path := config.DefaultPath() | |
| 52 | + | var err error | |
| 31 | 53 | if len(os.Args) == 3 { | |
| 32 | 54 | path = os.Args[2] | |
| 55 | + | _, err = config.Load(path) | |
| 56 | + | } else { | |
| 57 | + | _, err = config.LoadDefault() | |
| 33 | 58 | } | |
| 34 | - | if _, err := config.Load(path); err != nil { | |
| 59 | + | if err != nil { | |
| 35 | 60 | fmt.Fprintln(os.Stderr, err) | |
| 36 | 61 | return err | |
| 37 | 62 | } | |
| @@ -57,44 +82,60 @@ func run() error { | |||
| 57 | 82 | debugFlag := flags.String("debug", "", "debug components (comma-separated or all)") | |
| 58 | 83 | configFlag := flags.String("config", "", "config path (empty uses XDG default)") | |
| 59 | 84 | if err := flags.Parse(os.Args[1:]); err != nil { | |
| 60 | - | return err | |
| 61 | - | } | |
| 62 | - | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) | |
| 63 | - | defer stop() | |
| 64 | - | ctx, closeLog, err := logging.Open(ctx, *debugFlag) | |
| 65 | - | if err != nil { | |
| 66 | - | fmt.Fprintln(os.Stderr, err) | |
| 67 | - | return err | |
| 85 | + | if errors.Is(err, flag.ErrHelp) { | |
| 86 | + | return err | |
| 87 | + | } | |
| 88 | + | return usageError{err} | |
| 68 | 89 | } | |
| 69 | - | defer closeLog() | |
| 70 | - | log := logging.For(ctx, "app") | |
| 71 | 90 | if *backend != "drm" && *backend != "headless" { | |
| 72 | - | err := fmt.Errorf("invalid backend %q", *backend) | |
| 73 | - | log.Error().Err(err).Msg("invalid options") | |
| 91 | + | err := usageError{fmt.Errorf("invalid backend %q", *backend)} | |
| 92 | + | fmt.Fprintln(os.Stderr, err) | |
| 74 | 93 | return err | |
| 75 | 94 | } | |
| 76 | 95 | if *timeout < 0 { | |
| 77 | - | err := fmt.Errorf("negative timeout") | |
| 78 | - | log.Error().Err(err).Msg("invalid options") | |
| 96 | + | err := usageError{fmt.Errorf("negative timeout")} | |
| 97 | + | fmt.Fprintln(os.Stderr, err) | |
| 79 | 98 | return err | |
| 80 | 99 | } | |
| 81 | 100 | if flags.NArg() != 0 { | |
| 82 | - | err := fmt.Errorf("unexpected arguments: %v", flags.Args()) | |
| 83 | - | log.Error().Err(err).Msg("invalid options") | |
| 101 | + | err := usageError{fmt.Errorf("unexpected arguments: %v", flags.Args())} | |
| 102 | + | fmt.Fprintln(os.Stderr, err) | |
| 84 | 103 | return err | |
| 85 | 104 | } | |
| 86 | 105 | path := *configFlag | |
| 106 | + | var cfgErr error | |
| 107 | + | var cfg = config.Defaults() | |
| 87 | 108 | if path == "" { | |
| 88 | 109 | path = config.DefaultPath() | |
| 110 | + | cfg, cfgErr = config.LoadDefault() | |
| 111 | + | } else { | |
| 112 | + | cfg, cfgErr = config.Load(path) | |
| 113 | + | } | |
| 114 | + | if cfgErr != nil { | |
| 115 | + | fmt.Fprintln(os.Stderr, cfgErr) | |
| 116 | + | return cfgErr | |
| 117 | + | } | |
| 118 | + | selected := mergeDebug(cfg.Log.Debug, *debugFlag) | |
| 119 | + | if _, err := logging.ParseDebug(selected); err != nil { | |
| 120 | + | fmt.Fprintln(os.Stderr, err) | |
| 121 | + | return usageError{err} | |
| 89 | 122 | } | |
| 90 | - | cfg, err := config.Load(path) | |
| 123 | + | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) | |
| 124 | + | defer stop() | |
| 125 | + | ctx, closeLog, err := logging.Open(ctx, cfg.Log.Level, selected) | |
| 91 | 126 | if err != nil { | |
| 92 | 127 | fmt.Fprintln(os.Stderr, err) | |
| 93 | 128 | return err | |
| 94 | 129 | } | |
| 130 | + | defer closeLog() | |
| 131 | + | log := logging.For(ctx, "app") | |
| 95 | 132 | configLog := logging.For(ctx, "config") | |
| 96 | 133 | configLog.Info().Str("path", path).Msg("loaded config") | |
| 97 | 134 | if err := app.Run(ctx, app.Options{Backend: *backend, Config: cfg, Timeout: *timeout}); err != nil { | |
| 135 | + | // SIGINT and SIGTERM cancel the context and are clean exits. | |
| 136 | + | if ctx.Err() != nil && errors.Is(err, context.Canceled) { | |
| 137 | + | return nil | |
| 138 | + | } | |
| 98 | 139 | log.Error().Err(err).Msg("application failed") | |
| 99 | 140 | return err | |
| 100 | 141 | } | |
| @@ -2,11 +2,13 @@ package main | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | + | "errors" | |||
| 5 | "flag" | 6 | "flag" | ||
| 6 | "fmt" | 7 | "fmt" | ||
| 7 | "os" | 8 | "os" | ||
| 8 | "os/signal" | 9 | "os/signal" | ||
| 9 | "runtime/debug" | 10 | "runtime/debug" | ||
| 11 | + | "strings" | |||
| 10 | "syscall" | 12 | "syscall" | ||
| 11 | 13 | ||||
| 12 | "github.com/bnema/nefertty/internal/adapters/config" | 14 | "github.com/bnema/nefertty/internal/adapters/config" | ||
| @@ -14,24 +16,47 @@ import ( | |||||
| 14 | "github.com/bnema/nefertty/internal/logging" | 16 | "github.com/bnema/nefertty/internal/logging" | ||
| 15 | ) | 17 | ) | ||
| 16 | 18 | ||||
| 17 | - | func main() { | 19 | + | type usageError struct{ error } |
| 18 | - | if err := run(); err != nil { | 20 | + | |
| 19 | - | os.Exit(1) | 21 | + | func main() { os.Exit(runCode()) } |
| 22 | + | func runCode() int { | |||
| 23 | + | err := run() | |||
| 24 | + | if err == nil || errors.Is(err, flag.ErrHelp) || errors.Is(err, context.Canceled) { | |||
| 25 | + | return 0 | |||
| 26 | + | } | |||
| 27 | + | var usage usageError | |||
| 28 | + | if errors.As(err, &usage) { | |||
| 29 | + | return 2 | |||
| 30 | + | } | |||
| 31 | + | return 1 | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | func mergeDebug(configured []string, cli string) string { | |||
| 35 | + | if cli == "" { | |||
| 36 | + | return strings.Join(configured, ",") | |||
| 37 | + | } | |||
| 38 | + | if len(configured) == 0 { | |||
| 39 | + | return cli | |||
| 20 | } | 40 | } | ||
| 41 | + | return strings.Join(configured, ",") + "," + cli | |||
| 21 | } | 42 | } | ||
| 22 | 43 | ||||
| 23 | func run() error { | 44 | func run() error { | ||
| 24 | if len(os.Args) > 1 && os.Args[1] == "validate-config" { | 45 | if len(os.Args) > 1 && os.Args[1] == "validate-config" { | ||
| 25 | if len(os.Args) > 3 { | 46 | if len(os.Args) > 3 { | ||
| 26 | - | err := fmt.Errorf("usage: validate-config [path]") | 47 | + | err := usageError{fmt.Errorf("usage: validate-config [path]")} |
| 27 | fmt.Fprintln(os.Stderr, err) | 48 | fmt.Fprintln(os.Stderr, err) | ||
| 28 | return err | 49 | return err | ||
| 29 | } | 50 | } | ||
| 30 | path := config.DefaultPath() | 51 | path := config.DefaultPath() | ||
| 52 | + | var err error | |||
| 31 | if len(os.Args) == 3 { | 53 | if len(os.Args) == 3 { | ||
| 32 | path = os.Args[2] | 54 | path = os.Args[2] | ||
| 55 | + | _, err = config.Load(path) | |||
| 56 | + | } else { | |||
| 57 | + | _, err = config.LoadDefault() | |||
| 33 | } | 58 | } | ||
| 34 | - | if _, err := config.Load(path); err != nil { | 59 | + | if err != nil { |
| 35 | fmt.Fprintln(os.Stderr, err) | 60 | fmt.Fprintln(os.Stderr, err) | ||
| 36 | return err | 61 | return err | ||
| 37 | } | 62 | } | ||
| @@ -57,44 +82,60 @@ func run() error { | |||||
| 57 | debugFlag := flags.String("debug", "", "debug components (comma-separated or all)") | 82 | debugFlag := flags.String("debug", "", "debug components (comma-separated or all)") | ||
| 58 | configFlag := flags.String("config", "", "config path (empty uses XDG default)") | 83 | configFlag := flags.String("config", "", "config path (empty uses XDG default)") | ||
| 59 | if err := flags.Parse(os.Args[1:]); err != nil { | 84 | if err := flags.Parse(os.Args[1:]); err != nil { | ||
| 60 | - | return err | 85 | + | if errors.Is(err, flag.ErrHelp) { |
| 61 | - | } | 86 | + | return err |
| 62 | - | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) | 87 | + | } |
| 63 | - | defer stop() | 88 | + | return usageError{err} |
| 64 | - | ctx, closeLog, err := logging.Open(ctx, *debugFlag) | |||
| 65 | - | if err != nil { | |||
| 66 | - | fmt.Fprintln(os.Stderr, err) | |||
| 67 | - | return err | |||
| 68 | } | 89 | } | ||
| 69 | - | defer closeLog() | |||
| 70 | - | log := logging.For(ctx, "app") | |||
| 71 | if *backend != "drm" && *backend != "headless" { | 90 | if *backend != "drm" && *backend != "headless" { | ||
| 72 | - | err := fmt.Errorf("invalid backend %q", *backend) | 91 | + | err := usageError{fmt.Errorf("invalid backend %q", *backend)} |
| 73 | - | log.Error().Err(err).Msg("invalid options") | 92 | + | fmt.Fprintln(os.Stderr, err) |
| 74 | return err | 93 | return err | ||
| 75 | } | 94 | } | ||
| 76 | if *timeout < 0 { | 95 | if *timeout < 0 { | ||
| 77 | - | err := fmt.Errorf("negative timeout") | 96 | + | err := usageError{fmt.Errorf("negative timeout")} |
| 78 | - | log.Error().Err(err).Msg("invalid options") | 97 | + | fmt.Fprintln(os.Stderr, err) |
| 79 | return err | 98 | return err | ||
| 80 | } | 99 | } | ||
| 81 | if flags.NArg() != 0 { | 100 | if flags.NArg() != 0 { | ||
| 82 | - | err := fmt.Errorf("unexpected arguments: %v", flags.Args()) | 101 | + | err := usageError{fmt.Errorf("unexpected arguments: %v", flags.Args())} |
| 83 | - | log.Error().Err(err).Msg("invalid options") | 102 | + | fmt.Fprintln(os.Stderr, err) |
| 84 | return err | 103 | return err | ||
| 85 | } | 104 | } | ||
| 86 | path := *configFlag | 105 | path := *configFlag | ||
| 106 | + | var cfgErr error | |||
| 107 | + | var cfg = config.Defaults() | |||
| 87 | if path == "" { | 108 | if path == "" { | ||
| 88 | path = config.DefaultPath() | 109 | path = config.DefaultPath() | ||
| 110 | + | cfg, cfgErr = config.LoadDefault() | |||
| 111 | + | } else { | |||
| 112 | + | cfg, cfgErr = config.Load(path) | |||
| 113 | + | } | |||
| 114 | + | if cfgErr != nil { | |||
| 115 | + | fmt.Fprintln(os.Stderr, cfgErr) | |||
| 116 | + | return cfgErr | |||
| 117 | + | } | |||
| 118 | + | selected := mergeDebug(cfg.Log.Debug, *debugFlag) | |||
| 119 | + | if _, err := logging.ParseDebug(selected); err != nil { | |||
| 120 | + | fmt.Fprintln(os.Stderr, err) | |||
| 121 | + | return usageError{err} | |||
| 89 | } | 122 | } | ||
| 90 | - | cfg, err := config.Load(path) | 123 | + | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) |
| 124 | + | defer stop() | |||
| 125 | + | ctx, closeLog, err := logging.Open(ctx, cfg.Log.Level, selected) | |||
| 91 | if err != nil { | 126 | if err != nil { | ||
| 92 | fmt.Fprintln(os.Stderr, err) | 127 | fmt.Fprintln(os.Stderr, err) | ||
| 93 | return err | 128 | return err | ||
| 94 | } | 129 | } | ||
| 130 | + | defer closeLog() | |||
| 131 | + | log := logging.For(ctx, "app") | |||
| 95 | configLog := logging.For(ctx, "config") | 132 | configLog := logging.For(ctx, "config") | ||
| 96 | configLog.Info().Str("path", path).Msg("loaded config") | 133 | configLog.Info().Str("path", path).Msg("loaded config") | ||
| 97 | if err := app.Run(ctx, app.Options{Backend: *backend, Config: cfg, Timeout: *timeout}); err != nil { | 134 | if err := app.Run(ctx, app.Options{Backend: *backend, Config: cfg, Timeout: *timeout}); err != nil { | ||
| 135 | + | // SIGINT and SIGTERM cancel the context and are clean exits. | |||
| 136 | + | if ctx.Err() != nil && errors.Is(err, context.Canceled) { | |||
| 137 | + | return nil | |||
| 138 | + | } | |||
| 98 | log.Error().Err(err).Msg("application failed") | 139 | log.Error().Err(err).Msg("application failed") | ||
| 99 | return err | 140 | return err | ||
| 100 | } | 141 | } | ||
A cmd/nefertty/main_test.go +40 -0
| @@ -0,0 +1,40 @@ | |||
| 1 | + | package main | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "os" | |
| 5 | + | "strings" | |
| 6 | + | "testing" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | func TestMergeDebug(t *testing.T) { | |
| 10 | + | if got := mergeDebug([]string{"core", "app"}, "render,core"); got != "core,app,render,core" { | |
| 11 | + | t.Fatal(got) | |
| 12 | + | } | |
| 13 | + | if got := mergeDebug(nil, ""); got != "" { | |
| 14 | + | t.Fatal(got) | |
| 15 | + | } | |
| 16 | + | } | |
| 17 | + | func TestExitCodes(t *testing.T) { | |
| 18 | + | for _, tc := range []struct { | |
| 19 | + | args []string | |
| 20 | + | code int | |
| 21 | + | }{ | |
| 22 | + | {[]string{"nefertty", "--help"}, 0}, | |
| 23 | + | {[]string{"nefertty", "--backend=invalid"}, 2}, | |
| 24 | + | {[]string{"nefertty", "--timeout=-1s"}, 2}, | |
| 25 | + | {[]string{"nefertty", "extra"}, 2}, | |
| 26 | + | {[]string{"nefertty", "--config=/nonexistent/nefertty.toml"}, 1}, | |
| 27 | + | {[]string{"nefertty", "validate-config", "/nonexistent/nefertty.toml"}, 1}, | |
| 28 | + | } { | |
| 29 | + | old := os.Args | |
| 30 | + | os.Args = tc.args | |
| 31 | + | got := runCode() | |
| 32 | + | os.Args = old | |
| 33 | + | if got != tc.code { | |
| 34 | + | t.Errorf("%v: %d want %d", tc.args, got, tc.code) | |
| 35 | + | } | |
| 36 | + | } | |
| 37 | + | if !strings.Contains(mergeDebug([]string{"core"}, "render"), "render") { | |
| 38 | + | t.Fatal("union") | |
| 39 | + | } | |
| 40 | + | } | |
| @@ -0,0 +1,40 @@ | |||||
| 1 | + | package main | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "os" | |||
| 5 | + | "strings" | |||
| 6 | + | "testing" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | func TestMergeDebug(t *testing.T) { | |||
| 10 | + | if got := mergeDebug([]string{"core", "app"}, "render,core"); got != "core,app,render,core" { | |||
| 11 | + | t.Fatal(got) | |||
| 12 | + | } | |||
| 13 | + | if got := mergeDebug(nil, ""); got != "" { | |||
| 14 | + | t.Fatal(got) | |||
| 15 | + | } | |||
| 16 | + | } | |||
| 17 | + | func TestExitCodes(t *testing.T) { | |||
| 18 | + | for _, tc := range []struct { | |||
| 19 | + | args []string | |||
| 20 | + | code int | |||
| 21 | + | }{ | |||
| 22 | + | {[]string{"nefertty", "--help"}, 0}, | |||
| 23 | + | {[]string{"nefertty", "--backend=invalid"}, 2}, | |||
| 24 | + | {[]string{"nefertty", "--timeout=-1s"}, 2}, | |||
| 25 | + | {[]string{"nefertty", "extra"}, 2}, | |||
| 26 | + | {[]string{"nefertty", "--config=/nonexistent/nefertty.toml"}, 1}, | |||
| 27 | + | {[]string{"nefertty", "validate-config", "/nonexistent/nefertty.toml"}, 1}, | |||
| 28 | + | } { | |||
| 29 | + | old := os.Args | |||
| 30 | + | os.Args = tc.args | |||
| 31 | + | got := runCode() | |||
| 32 | + | os.Args = old | |||
| 33 | + | if got != tc.code { | |||
| 34 | + | t.Errorf("%v: %d want %d", tc.args, got, tc.code) | |||
| 35 | + | } | |||
| 36 | + | } | |||
| 37 | + | if !strings.Contains(mergeDebug([]string{"core"}, "render"), "render") { | |||
| 38 | + | t.Fatal("union") | |||
| 39 | + | } | |||
| 40 | + | } | |||
M internal/adapters/config/config.go +7 -3
| @@ -50,12 +50,16 @@ func Defaults() ports.Config { | |||
| 50 | 50 | c.Log.Debug = []string{} | |
| 51 | 51 | return c | |
| 52 | 52 | } | |
| 53 | + | func LoadDefault() (ports.Config, error) { | |
| 54 | + | c, err := Load(DefaultPath()) | |
| 55 | + | if os.IsNotExist(err) { | |
| 56 | + | return Defaults(), nil | |
| 57 | + | } | |
| 58 | + | return c, err | |
| 59 | + | } | |
| 53 | 60 | func Load(path string) (ports.Config, error) { | |
| 54 | 61 | c := Defaults() | |
| 55 | 62 | data, err := os.ReadFile(path) | |
| 56 | - | if os.IsNotExist(err) { | |
| 57 | - | return c, nil | |
| 58 | - | } | |
| 59 | 63 | if err != nil { | |
| 60 | 64 | return c, err | |
| 61 | 65 | } | |
| @@ -50,12 +50,16 @@ func Defaults() ports.Config { | |||||
| 50 | c.Log.Debug = []string{} | 50 | c.Log.Debug = []string{} | ||
| 51 | return c | 51 | return c | ||
| 52 | } | 52 | } | ||
| 53 | + | func LoadDefault() (ports.Config, error) { | |||
| 54 | + | c, err := Load(DefaultPath()) | |||
| 55 | + | if os.IsNotExist(err) { | |||
| 56 | + | return Defaults(), nil | |||
| 57 | + | } | |||
| 58 | + | return c, err | |||
| 59 | + | } | |||
| 53 | func Load(path string) (ports.Config, error) { | 60 | func Load(path string) (ports.Config, error) { | ||
| 54 | c := Defaults() | 61 | c := Defaults() | ||
| 55 | data, err := os.ReadFile(path) | 62 | data, err := os.ReadFile(path) | ||
| 56 | - | if os.IsNotExist(err) { | |||
| 57 | - | return c, nil | |||
| 58 | - | } | |||
| 59 | if err != nil { | 63 | if err != nil { | ||
| 60 | return c, err | 64 | return c, err | ||
| 61 | } | 65 | } | ||
M internal/adapters/config/config_test.go +6 -1
| @@ -17,7 +17,12 @@ func TestDefaultsAndLoad(t *testing.T) { | |||
| 17 | 17 | t.Fatalf("defaults: %+v", d) | |
| 18 | 18 | } | |
| 19 | 19 | path := filepath.Join(t.TempDir(), "missing") | |
| 20 | - | got, err := Load(path) | |
| 20 | + | _, err := Load(path) | |
| 21 | + | if !os.IsNotExist(err) { | |
| 22 | + | t.Fatalf("explicit missing: %v", err) | |
| 23 | + | } | |
| 24 | + | t.Setenv("XDG_CONFIG_HOME", t.TempDir()) | |
| 25 | + | got, err := LoadDefault() | |
| 21 | 26 | if err != nil || !reflect.DeepEqual(got, d) { | |
| 22 | 27 | t.Fatalf("missing: %v %+v", err, got) | |
| 23 | 28 | } | |
| @@ -17,7 +17,12 @@ func TestDefaultsAndLoad(t *testing.T) { | |||||
| 17 | t.Fatalf("defaults: %+v", d) | 17 | t.Fatalf("defaults: %+v", d) | ||
| 18 | } | 18 | } | ||
| 19 | path := filepath.Join(t.TempDir(), "missing") | 19 | path := filepath.Join(t.TempDir(), "missing") | ||
| 20 | - | got, err := Load(path) | 20 | + | _, err := Load(path) |
| 21 | + | if !os.IsNotExist(err) { | |||
| 22 | + | t.Fatalf("explicit missing: %v", err) | |||
| 23 | + | } | |||
| 24 | + | t.Setenv("XDG_CONFIG_HOME", t.TempDir()) | |||
| 25 | + | got, err := LoadDefault() | |||
| 21 | if err != nil || !reflect.DeepEqual(got, d) { | 26 | if err != nil || !reflect.DeepEqual(got, d) { | ||
| 22 | t.Fatalf("missing: %v %+v", err, got) | 27 | t.Fatalf("missing: %v %+v", err, got) | ||
| 23 | } | 28 | } | ||
M internal/logging/logging.go +13 -5
| @@ -19,6 +19,7 @@ var components = map[string]bool{ | |||
| 19 | 19 | } | |
| 20 | 20 | ||
| 21 | 21 | type debugKey struct{} | |
| 22 | + | type levelKey struct{} | |
| 22 | 23 | type debugSet map[string]bool | |
| 23 | 24 | ||
| 24 | 25 | func ParseDebug(value string) (map[string]bool, error) { | |
| @@ -41,7 +42,7 @@ func ParseDebug(value string) (map[string]bool, error) { | |||
| 41 | 42 | ||
| 42 | 43 | // Open rotates the previous run's log, opens a fresh JSON log and attaches the logger to ctx. | |
| 43 | 44 | // The caller must invoke close after all logging is complete. | |
| 44 | - | func Open(ctx context.Context, debug string) (context.Context, func() error, error) { | |
| 45 | + | func Open(ctx context.Context, level, debug string) (context.Context, func() error, error) { | |
| 45 | 46 | selected, err := ParseDebug(debug) | |
| 46 | 47 | if err != nil { | |
| 47 | 48 | return nil, nil, err | |
| @@ -66,12 +67,13 @@ func Open(ctx context.Context, debug string) (context.Context, func() error, err | |||
| 66 | 67 | if err != nil { | |
| 67 | 68 | return nil, nil, err | |
| 68 | 69 | } | |
| 69 | - | var output = zerowrap.Config{Level: "debug", Format: "json", Output: file} | |
| 70 | + | var output = zerowrap.Config{Level: level, Format: "json", Output: file} | |
| 70 | 71 | if term.IsTerminal(int(os.Stderr.Fd())) { | |
| 71 | 72 | output.Output = &consoleAndFile{file: file} | |
| 72 | 73 | } | |
| 73 | 74 | ctx = zerowrap.WithCtx(ctx, zerowrap.New(output)) | |
| 74 | 75 | ctx = context.WithValue(ctx, debugKey{}, debugSet(selected)) | |
| 76 | + | ctx = context.WithValue(ctx, levelKey{}, level) | |
| 75 | 77 | return ctx, file.Close, nil | |
| 76 | 78 | } | |
| 77 | 79 | ||
| @@ -79,9 +81,7 @@ type consoleAndFile struct{ file *os.File } | |||
| 79 | 81 | ||
| 80 | 82 | func (w *consoleAndFile) Write(p []byte) (int, error) { | |
| 81 | 83 | // JSON remains the canonical output; terminal users also see each entry. | |
| 82 | - | if _, err := os.Stderr.Write(p); err != nil { | |
| 83 | - | return 0, err | |
| 84 | - | } | |
| 84 | + | _, _ = os.Stderr.Write(p) | |
| 85 | 85 | return w.file.Write(p) | |
| 86 | 86 | } | |
| 87 | 87 | ||
| @@ -90,6 +90,14 @@ func For(ctx context.Context, component string) zerowrap.Logger { | |||
| 90 | 90 | log := zerowrap.FromCtxWithField(ctx, zerowrap.FieldComponent, component) | |
| 91 | 91 | selected, _ := ctx.Value(debugKey{}).(debugSet) | |
| 92 | 92 | level := zerolog.InfoLevel | |
| 93 | + | switch ctx.Value(levelKey{}) { | |
| 94 | + | case "debug": | |
| 95 | + | level = zerolog.DebugLevel | |
| 96 | + | case "warn": | |
| 97 | + | level = zerolog.WarnLevel | |
| 98 | + | case "error": | |
| 99 | + | level = zerolog.ErrorLevel | |
| 100 | + | } | |
| 93 | 101 | if selected["all"] || selected[component] { | |
| 94 | 102 | level = zerolog.DebugLevel | |
| 95 | 103 | } | |
| @@ -19,6 +19,7 @@ var components = map[string]bool{ | |||||
| 19 | } | 19 | } | ||
| 20 | 20 | ||||
| 21 | type debugKey struct{} | 21 | type debugKey struct{} | ||
| 22 | + | type levelKey struct{} | |||
| 22 | type debugSet map[string]bool | 23 | type debugSet map[string]bool | ||
| 23 | 24 | ||||
| 24 | func ParseDebug(value string) (map[string]bool, error) { | 25 | func ParseDebug(value string) (map[string]bool, error) { | ||
| @@ -41,7 +42,7 @@ func ParseDebug(value string) (map[string]bool, error) { | |||||
| 41 | 42 | ||||
| 42 | // Open rotates the previous run's log, opens a fresh JSON log and attaches the logger to ctx. | 43 | // Open rotates the previous run's log, opens a fresh JSON log and attaches the logger to ctx. | ||
| 43 | // The caller must invoke close after all logging is complete. | 44 | // The caller must invoke close after all logging is complete. | ||
| 44 | - | func Open(ctx context.Context, debug string) (context.Context, func() error, error) { | 45 | + | func Open(ctx context.Context, level, debug string) (context.Context, func() error, error) { |
| 45 | selected, err := ParseDebug(debug) | 46 | selected, err := ParseDebug(debug) | ||
| 46 | if err != nil { | 47 | if err != nil { | ||
| 47 | return nil, nil, err | 48 | return nil, nil, err | ||
| @@ -66,12 +67,13 @@ func Open(ctx context.Context, debug string) (context.Context, func() error, err | |||||
| 66 | if err != nil { | 67 | if err != nil { | ||
| 67 | return nil, nil, err | 68 | return nil, nil, err | ||
| 68 | } | 69 | } | ||
| 69 | - | var output = zerowrap.Config{Level: "debug", Format: "json", Output: file} | 70 | + | var output = zerowrap.Config{Level: level, Format: "json", Output: file} |
| 70 | if term.IsTerminal(int(os.Stderr.Fd())) { | 71 | if term.IsTerminal(int(os.Stderr.Fd())) { | ||
| 71 | output.Output = &consoleAndFile{file: file} | 72 | output.Output = &consoleAndFile{file: file} | ||
| 72 | } | 73 | } | ||
| 73 | ctx = zerowrap.WithCtx(ctx, zerowrap.New(output)) | 74 | ctx = zerowrap.WithCtx(ctx, zerowrap.New(output)) | ||
| 74 | ctx = context.WithValue(ctx, debugKey{}, debugSet(selected)) | 75 | ctx = context.WithValue(ctx, debugKey{}, debugSet(selected)) | ||
| 76 | + | ctx = context.WithValue(ctx, levelKey{}, level) | |||
| 75 | return ctx, file.Close, nil | 77 | return ctx, file.Close, nil | ||
| 76 | } | 78 | } | ||
| 77 | 79 | ||||
| @@ -79,9 +81,7 @@ type consoleAndFile struct{ file *os.File } | |||||
| 79 | 81 | ||||
| 80 | func (w *consoleAndFile) Write(p []byte) (int, error) { | 82 | func (w *consoleAndFile) Write(p []byte) (int, error) { | ||
| 81 | // JSON remains the canonical output; terminal users also see each entry. | 83 | // JSON remains the canonical output; terminal users also see each entry. | ||
| 82 | - | if _, err := os.Stderr.Write(p); err != nil { | 84 | + | _, _ = os.Stderr.Write(p) |
| 83 | - | return 0, err | |||
| 84 | - | } | |||
| 85 | return w.file.Write(p) | 85 | return w.file.Write(p) | ||
| 86 | } | 86 | } | ||
| 87 | 87 | ||||
| @@ -90,6 +90,14 @@ func For(ctx context.Context, component string) zerowrap.Logger { | |||||
| 90 | log := zerowrap.FromCtxWithField(ctx, zerowrap.FieldComponent, component) | 90 | log := zerowrap.FromCtxWithField(ctx, zerowrap.FieldComponent, component) | ||
| 91 | selected, _ := ctx.Value(debugKey{}).(debugSet) | 91 | selected, _ := ctx.Value(debugKey{}).(debugSet) | ||
| 92 | level := zerolog.InfoLevel | 92 | level := zerolog.InfoLevel | ||
| 93 | + | switch ctx.Value(levelKey{}) { | |||
| 94 | + | case "debug": | |||
| 95 | + | level = zerolog.DebugLevel | |||
| 96 | + | case "warn": | |||
| 97 | + | level = zerolog.WarnLevel | |||
| 98 | + | case "error": | |||
| 99 | + | level = zerolog.ErrorLevel | |||
| 100 | + | } | |||
| 93 | if selected["all"] || selected[component] { | 101 | if selected["all"] || selected[component] { | ||
| 94 | level = zerolog.DebugLevel | 102 | level = zerolog.DebugLevel | ||
| 95 | } | 103 | } | ||
M internal/logging/logging_test.go +1 -1
| @@ -34,7 +34,7 @@ func TestComponentLevelsAndRotation(t *testing.T) { | |||
| 34 | 34 | t.Setenv("XDG_STATE_HOME", t.TempDir()) | |
| 35 | 35 | path := filepath.Join(os.Getenv("XDG_STATE_HOME"), "nefertty", "nefertty.log") | |
| 36 | 36 | for run := 0; run < 2; run++ { | |
| 37 | - | ctx, closeLog, err := Open(context.Background(), "core") | |
| 37 | + | ctx, closeLog, err := Open(context.Background(), "info", "core") | |
| 38 | 38 | if err != nil { | |
| 39 | 39 | t.Fatal(err) | |
| 40 | 40 | } | |
| @@ -34,7 +34,7 @@ func TestComponentLevelsAndRotation(t *testing.T) { | |||||
| 34 | t.Setenv("XDG_STATE_HOME", t.TempDir()) | 34 | t.Setenv("XDG_STATE_HOME", t.TempDir()) | ||
| 35 | path := filepath.Join(os.Getenv("XDG_STATE_HOME"), "nefertty", "nefertty.log") | 35 | path := filepath.Join(os.Getenv("XDG_STATE_HOME"), "nefertty", "nefertty.log") | ||
| 36 | for run := 0; run < 2; run++ { | 36 | for run := 0; run < 2; run++ { | ||
| 37 | - | ctx, closeLog, err := Open(context.Background(), "core") | 37 | + | ctx, closeLog, err := Open(context.Background(), "info", "core") |
| 38 | if err != nil { | 38 | if err != nil { | ||
| 39 | t.Fatal(err) | 39 | t.Fatal(err) | ||
| 40 | } | 40 | } | ||