diff --git a/app/logger/config.go b/app/logger/config.go index e882092e..f7b50512 100644 --- a/app/logger/config.go +++ b/app/logger/config.go @@ -1,6 +1,9 @@ package logger import ( + "fmt" + "strings" + "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -15,14 +18,19 @@ const ( JSONOutput ) +type NamedLevel struct { + Name string `yaml:"name"` + Level string `yaml:"level"` +} + type Config struct { - Production bool `yaml:"production"` - DefaultLevel string `yaml:"defaultLevel"` - NamedLevels map[string]string `yaml:"namedLevels"` - AddOutputPaths []string `yaml:"outputPaths"` - DisableStdErr bool `yaml:"disableStdErr"` - Format LogFormat `yaml:"format"` - ZapConfig *zap.Config `yaml:"-"` // optional, if set it will be used instead of other config options + Production bool `yaml:"production"` + DefaultLevel string `yaml:"defaultLevel"` + Levels []NamedLevel `yaml:"levels"` // first match will be used + AddOutputPaths []string `yaml:"outputPaths"` + DisableStdErr bool `yaml:"disableStdErr"` + Format LogFormat `yaml:"format"` + ZapConfig *zap.Config `yaml:"-"` // optional, if set it will be used instead of other config options } func (l Config) ApplyGlobal() { @@ -68,20 +76,49 @@ func (l Config) ApplyGlobal() { conf.Level = defaultLevel } } - var lvl = make(map[string]zap.AtomicLevel) - for k, v := range l.NamedLevels { - if lev, err := zap.ParseAtomicLevel(v); err == nil { - lvl[k] = lev + for _, v := range l.Levels { + if lev, err := zap.ParseAtomicLevel(v.Level); err == nil { // we need to have a minimum level of all named loggers for the main logger if lev.Level() < conf.Level.Level() { conf.Level.SetLevel(lev.Level()) } } } + lg, err := conf.Build() if err != nil { Default().Fatal("can't build logger", zap.Error(err)) } SetDefault(lg) - SetNamedLevels(lvl) + SetNamedLevels(l.Levels) +} + +// LevelsFromStr parses a string of the form "name1=DEBUG;prefix*=WARN;*=ERROR" into a slice of NamedLevel +// it may be useful to parse the log level from the OS env var +func LevelsFromStr(s string) (levels []NamedLevel) { + for _, kv := range strings.Split(s, ";") { + strings.TrimSpace(kv) + parts := strings.Split(kv, "=") + var key, value string + if len(parts) == 1 { + key = "*" + value = parts[0] + _, err := zap.ParseAtomicLevel(value) + if err != nil { + fmt.Printf("Can't parse log level %s: %s\n", parts[0], err.Error()) + continue + } + levels = append(levels, NamedLevel{Name: key, Level: value}) + } else if len(parts) == 2 { + key = parts[0] + value = parts[1] + } + _, err := zap.ParseAtomicLevel(value) + if err != nil { + fmt.Printf("Can't parse log level %s: %s\n", parts[0], err.Error()) + continue + } + levels = append(levels, NamedLevel{Name: key, Level: value}) + } + return levels } diff --git a/app/logger/log.go b/app/logger/log.go index e41c20d1..77668ad4 100644 --- a/app/logger/log.go +++ b/app/logger/log.go @@ -11,12 +11,17 @@ var ( mu sync.Mutex logger *zap.Logger loggerConfig zap.Config - namedLevels = make(map[string]zap.AtomicLevel) + namedLevels []namedLevel namedGlobs = make(map[string]glob.Glob) namedLoggers = make(map[string]CtxLogger) namedSugarLoggers = make(map[string]*zap.SugaredLogger) ) +type namedLevel struct { + name string + level zap.AtomicLevel +} + func init() { loggerConfig = zap.NewDevelopmentConfig() logger, _ = loggerConfig.Build() @@ -35,18 +40,23 @@ func SetDefault(l *zap.Logger) { // it also supports glob patterns for names, like "app*" // can be racy in case there are existing named loggers // so consider to call only once at the beginning -func SetNamedLevels(l map[string]zap.AtomicLevel) { +func SetNamedLevels(nls []NamedLevel) { mu.Lock() defer mu.Unlock() - namedLevels = l + namedLevels = namedLevels[:0] var minLevel = logger.Level() - for k, l := range namedLevels { - g, err := glob.Compile(k) - if err == nil { - namedGlobs[k] = g + for _, nl := range nls { + l, err := zap.ParseAtomicLevel(nl.Level) + if err != nil { + continue } - namedLevels[k] = l + namedLevels = append(namedLevels, namedLevel{name: nl.Name, level: l}) + g, err := glob.Compile(nl.Name) + if err == nil { + namedGlobs[nl.Name] = g + } + if l.Level() < minLevel { minLevel = l.Level() } @@ -81,23 +91,18 @@ func Default() *zap.Logger { return logger } +// getLevel returns the level for the given name +// it return the first matching name or glob pattern whatever comes first func getLevel(name string) zap.AtomicLevel { - level, ok := namedLevels[name] - if !ok { - var found bool - for globName, glob := range namedGlobs { - if glob.Match(name) { - found = true - level, _ = namedLevels[globName] - // no need to check ok, because we know that globName exists - break - } + for _, nl := range namedLevels { + if nl.name == name { + return nl.level } - if !found { - level = loggerConfig.Level + if g, ok := namedGlobs[nl.name]; ok && g.Match(name) { + return nl.level } } - return level + return zap.NewAtomicLevelAt(logger.Level()) } func NewNamed(name string, fields ...zap.Field) CtxLogger { diff --git a/app/logger/log_test.go b/app/logger/log_test.go new file mode 100644 index 00000000..41e45386 --- /dev/null +++ b/app/logger/log_test.go @@ -0,0 +1,150 @@ +package logger + +import ( + "reflect" + "testing" + + "go.uber.org/zap" +) + +func Test_getLevel1(t *testing.T) { + SetNamedLevels([]NamedLevel{ + {Name: "app", Level: "debug"}, + {Name: "app*", Level: "info"}, + {Name: "app.sub", Level: "warn"}, + {Name: "*", Level: "fatal"}, + }) + + tests := []struct { + name string + want zap.AtomicLevel + }{ + { + name: "app", + want: zap.NewAtomicLevelAt(zap.DebugLevel), + }, + { + name: "app.aaa", + want: zap.NewAtomicLevelAt(zap.InfoLevel), + }, + { + name: "app.sub", + want: zap.NewAtomicLevelAt(zap.InfoLevel), + }, + { + name: "random", + want: zap.NewAtomicLevelAt(zap.FatalLevel), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getLevel(tt.name); !reflect.DeepEqual(got, tt.want) { + t.Errorf("getLevel() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_getLevel2(t *testing.T) { + SetNamedLevels([]NamedLevel{ + {Name: "*", Level: "ERROR"}, + {Name: "app", Level: "info"}, + {Name: "app.sub", Level: "warn"}, + {Name: "*", Level: "fatal"}, + }) + + tests := []struct { + name string + want zap.AtomicLevel + }{ + { + name: "app", + want: zap.NewAtomicLevelAt(zap.ErrorLevel), + }, + { + name: "app.aaa", + want: zap.NewAtomicLevelAt(zap.ErrorLevel), + }, + { + name: "app.sub", + want: zap.NewAtomicLevelAt(zap.ErrorLevel), + }, + { + name: "random", + want: zap.NewAtomicLevelAt(zap.ErrorLevel), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getLevel(tt.name); !reflect.DeepEqual(got, tt.want) { + t.Errorf("getLevel() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_getLevel3(t *testing.T) { + SetNamedLevels([]NamedLevel{ + {Name: "app", Level: "info"}, + {Name: "*.sub", Level: "warn"}, + {Name: "*", Level: "fatal"}, + }) + + tests := []struct { + name string + want zap.AtomicLevel + }{ + { + name: "app", + want: zap.NewAtomicLevelAt(zap.InfoLevel), + }, + { + name: "app.sub", + want: zap.NewAtomicLevelAt(zap.WarnLevel), + }, + { + name: "random", + want: zap.NewAtomicLevelAt(zap.FatalLevel), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getLevel(tt.name); !reflect.DeepEqual(got, tt.want) { + t.Errorf("getLevel() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_getLevel4(t *testing.T) { + SetNamedLevels([]NamedLevel{ + {Name: "*", Level: "invalid"}, + {Name: "app", Level: "info"}, + {Name: "b", Level: "invalid"}, + }) + + tests := []struct { + name string + want zap.AtomicLevel + }{ + { + name: "app", + want: zap.NewAtomicLevelAt(zap.InfoLevel), + }, + { + name: "app.sub", + want: zap.NewAtomicLevelAt(logger.Level()), + }, + { + name: "b", + want: zap.NewAtomicLevelAt(logger.Level()), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getLevel(tt.name); !reflect.DeepEqual(got, tt.want) { + t.Errorf("getLevel() = %v, want %v", got, tt.want) + } + }) + } +}