From f1e2a06b46c7f45ba6562c308d2703fdfb4067b0 Mon Sep 17 00:00:00 2001 From: Mayank Saini Date: Tue, 12 May 2026 18:40:05 +0530 Subject: [PATCH] feat: add configurable `--log-level` flag (#5934) Signed-off-by: boojack Co-authored-by: boojack --- cmd/memos/log.go | 28 +++++++++++ cmd/memos/log_test.go | 107 ++++++++++++++++++++++++++++++++++++++++++ cmd/memos/main.go | 14 ++++++ 3 files changed, 149 insertions(+) create mode 100644 cmd/memos/log.go create mode 100644 cmd/memos/log_test.go diff --git a/cmd/memos/log.go b/cmd/memos/log.go new file mode 100644 index 000000000..3bc5622ac --- /dev/null +++ b/cmd/memos/log.go @@ -0,0 +1,28 @@ +package main + +import ( + "io" + "log/slog" + "strings" + + "github.com/pkg/errors" +) + +func parseSlogLevel(s string) (slog.Level, error) { + switch strings.ToLower(s) { + case "debug": + return slog.LevelDebug, nil + case "info": + return slog.LevelInfo, nil + case "warn": + return slog.LevelWarn, nil + case "error": + return slog.LevelError, nil + default: + return slog.LevelInfo, errors.Errorf("unknown log level %q: must be debug, info, warn, or error", s) + } +} + +func newLogger(level slog.Level, w io.Writer) *slog.Logger { + return slog.New(slog.NewTextHandler(w, &slog.HandlerOptions{Level: level})) +} diff --git a/cmd/memos/log_test.go b/cmd/memos/log_test.go new file mode 100644 index 000000000..b8d4397c5 --- /dev/null +++ b/cmd/memos/log_test.go @@ -0,0 +1,107 @@ +package main + +import ( + "bytes" + "context" + "log/slog" + "strings" + "testing" +) + +func TestParseSlogLevel(t *testing.T) { + tests := []struct { + input string + wantLevel slog.Level + wantErr bool + }{ + {"debug", slog.LevelDebug, false}, + {"info", slog.LevelInfo, false}, + {"warn", slog.LevelWarn, false}, + {"error", slog.LevelError, false}, + {"DEBUG", slog.LevelDebug, false}, + {"INFO", slog.LevelInfo, false}, + {"WARN", slog.LevelWarn, false}, + {"ERROR", slog.LevelError, false}, + {"invalid", slog.LevelInfo, true}, + {"", slog.LevelInfo, true}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + got, err := parseSlogLevel(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("parseSlogLevel(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr) + } + if got != tt.wantLevel { + t.Errorf("parseSlogLevel(%q) = %v, want %v", tt.input, got, tt.wantLevel) + } + }) + } +} + +func TestNewLoggerLevelFiltering(t *testing.T) { + tests := []struct { + level slog.Level + logAt slog.Level + msg string + shouldAppear bool + }{ + // debug passes all + {slog.LevelDebug, slog.LevelDebug, "debug-msg", true}, + {slog.LevelDebug, slog.LevelInfo, "info-msg", true}, + {slog.LevelDebug, slog.LevelWarn, "warn-msg", true}, + {slog.LevelDebug, slog.LevelError, "error-msg", true}, + // info suppresses debug + {slog.LevelInfo, slog.LevelDebug, "debug-suppressed", false}, + {slog.LevelInfo, slog.LevelInfo, "info-visible", true}, + {slog.LevelInfo, slog.LevelWarn, "warn-visible", true}, + // warn suppresses debug+info + {slog.LevelWarn, slog.LevelDebug, "debug-suppressed", false}, + {slog.LevelWarn, slog.LevelInfo, "info-suppressed", false}, + {slog.LevelWarn, slog.LevelWarn, "warn-visible", true}, + {slog.LevelWarn, slog.LevelError, "error-visible", true}, + // error suppresses everything below + {slog.LevelError, slog.LevelDebug, "debug-suppressed", false}, + {slog.LevelError, slog.LevelInfo, "info-suppressed", false}, + {slog.LevelError, slog.LevelWarn, "warn-suppressed", false}, + {slog.LevelError, slog.LevelError, "error-visible", true}, + } + + for _, tt := range tests { + var buf bytes.Buffer + logger := newLogger(tt.level, &buf) + logger.Log(context.TODO(), tt.logAt, tt.msg) + + appeared := strings.Contains(buf.String(), tt.msg) + if appeared != tt.shouldAppear { + t.Errorf("level=%s logAt=%s msg=%q: appeared=%v want=%v", + tt.level, tt.logAt, tt.msg, appeared, tt.shouldAppear) + } + } +} + +func TestNewLoggerOutputFormat(t *testing.T) { + var buf bytes.Buffer + logger := newLogger(slog.LevelDebug, &buf) + logger.Info("hello-world", "key", "value") + + out := buf.String() + if !strings.Contains(out, "hello-world") { + t.Errorf("expected message in output, got: %s", out) + } + if !strings.Contains(out, "key=value") { + t.Errorf("expected key=value attr in output, got: %s", out) + } + if !strings.Contains(out, "INFO") { + t.Errorf("expected level in output, got: %s", out) + } +} + +func TestNewLoggerDoesNotMutateGlobalDefault(t *testing.T) { + original := slog.Default() + var buf bytes.Buffer + _ = newLogger(slog.LevelError, &buf) + if slog.Default() != original { + t.Error("newLogger must not change slog.Default()") + } +} diff --git a/cmd/memos/main.go b/cmd/memos/main.go index ea3a490d9..3daaa6192 100644 --- a/cmd/memos/main.go +++ b/cmd/memos/main.go @@ -21,6 +21,14 @@ import ( "github.com/usememos/memos/store/db" ) +func initSlogDefault() { + level, err := parseSlogLevel(viper.GetString("log-level")) + if err != nil { + slog.Warn("invalid log-level value, defaulting to info", "error", err) + } + slog.SetDefault(newLogger(level, os.Stderr)) +} + var ( rootCmd = &cobra.Command{ Use: "memos", @@ -103,6 +111,8 @@ var ( ) func init() { + cobra.OnInitialize(initSlogDefault) + viper.SetDefault("demo", false) viper.SetDefault("driver", "sqlite") viper.SetDefault("port", 8081) @@ -116,6 +126,7 @@ func init() { rootCmd.PersistentFlags().String("dsn", "", "database source name(aka. DSN)") rootCmd.PersistentFlags().String("instance-url", "", "the url of your memos instance") rootCmd.PersistentFlags().Bool("allow-private-webhooks", false, "allow webhook URLs to resolve to private/reserved IP addresses") + rootCmd.PersistentFlags().String("log-level", "info", "log verbosity level (debug, info, warn, error)") if err := viper.BindPFlag("demo", rootCmd.PersistentFlags().Lookup("demo")); err != nil { panic(err) @@ -144,6 +155,9 @@ func init() { if err := viper.BindPFlag("allow-private-webhooks", rootCmd.PersistentFlags().Lookup("allow-private-webhooks")); err != nil { panic(err) } + if err := viper.BindPFlag("log-level", rootCmd.PersistentFlags().Lookup("log-level")); err != nil { + panic(err) + } viper.SetEnvPrefix("memos") viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))