feat: add configurable --log-level flag (#5934)
Signed-off-by: boojack <stevenlgtm@gmail.com> Co-authored-by: boojack <stevenlgtm@gmail.com>
This commit is contained in:
parent
a6024eebf1
commit
f1e2a06b46
3 changed files with 149 additions and 0 deletions
28
cmd/memos/log.go
Normal file
28
cmd/memos/log.go
Normal file
|
|
@ -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}))
|
||||||
|
}
|
||||||
107
cmd/memos/log_test.go
Normal file
107
cmd/memos/log_test.go
Normal file
|
|
@ -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()")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,14 @@ import (
|
||||||
"github.com/usememos/memos/store/db"
|
"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 (
|
var (
|
||||||
rootCmd = &cobra.Command{
|
rootCmd = &cobra.Command{
|
||||||
Use: "memos",
|
Use: "memos",
|
||||||
|
|
@ -103,6 +111,8 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
cobra.OnInitialize(initSlogDefault)
|
||||||
|
|
||||||
viper.SetDefault("demo", false)
|
viper.SetDefault("demo", false)
|
||||||
viper.SetDefault("driver", "sqlite")
|
viper.SetDefault("driver", "sqlite")
|
||||||
viper.SetDefault("port", 8081)
|
viper.SetDefault("port", 8081)
|
||||||
|
|
@ -116,6 +126,7 @@ func init() {
|
||||||
rootCmd.PersistentFlags().String("dsn", "", "database source name(aka. DSN)")
|
rootCmd.PersistentFlags().String("dsn", "", "database source name(aka. DSN)")
|
||||||
rootCmd.PersistentFlags().String("instance-url", "", "the url of your memos instance")
|
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().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 {
|
if err := viper.BindPFlag("demo", rootCmd.PersistentFlags().Lookup("demo")); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
@ -144,6 +155,9 @@ func init() {
|
||||||
if err := viper.BindPFlag("allow-private-webhooks", rootCmd.PersistentFlags().Lookup("allow-private-webhooks")); err != nil {
|
if err := viper.BindPFlag("allow-private-webhooks", rootCmd.PersistentFlags().Lookup("allow-private-webhooks")); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
if err := viper.BindPFlag("log-level", rootCmd.PersistentFlags().Lookup("log-level")); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
viper.SetEnvPrefix("memos")
|
viper.SetEnvPrefix("memos")
|
||||||
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue