|
- package logging
-
- import (
- "errors"
- "io"
- "os"
- "path/filepath"
- "strings"
- "sync"
- "time"
-
- "github.com/rs/zerolog"
- )
-
- type Config struct {
- Level string `yaml:"level" json:"level"`
- Categories []string `yaml:"categories" json:"categories"`
- RateLimitMs int `yaml:"rate_limit_ms" json:"rate_limit_ms"`
- Stdout bool `yaml:"stdout" json:"stdout"`
- StdoutColor bool `yaml:"stdout_color" json:"stdout_color"`
- File string `yaml:"file" json:"file"`
- FileLevel string `yaml:"file_level" json:"file_level"`
- TimeFormat string `yaml:"time_format" json:"time_format"`
- DisableTime bool `yaml:"disable_time" json:"disable_time"`
- }
-
- type rateLimiter struct {
- mu sync.Mutex
- last map[string]time.Time
- limit time.Duration
- }
-
- func (r *rateLimiter) allow(key string) bool {
- if r.limit <= 0 {
- return true
- }
- now := time.Now()
- r.mu.Lock()
- defer r.mu.Unlock()
- if t, ok := r.last[key]; ok {
- if now.Sub(t) < r.limit {
- return false
- }
- }
- r.last[key] = now
- return true
- }
-
- var (
- logger zerolog.Logger
- fileLogger zerolog.Logger
- cfg Config
- cats map[string]bool
- rlim = &rateLimiter{last: map[string]time.Time{}}
- fileHandle *os.File
- )
-
- func Init(c Config) error {
- cfg = c
- if cfg.TimeFormat == "" {
- cfg.TimeFormat = "15:04:05"
- }
- cats = map[string]bool{}
- for _, c := range cfg.Categories {
- cats[strings.ToLower(strings.TrimSpace(c))] = true
- }
- rl := time.Duration(cfg.RateLimitMs) * time.Millisecond
- rlim.limit = rl
-
- level := parseLevel(cfg.Level)
- writers := make([]io.Writer, 0, 2)
- if cfg.Stdout {
- cw := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: cfg.TimeFormat, NoColor: !cfg.StdoutColor}
- if cfg.DisableTime {
- cw.PartsExclude = append(cw.PartsExclude, zerolog.TimestampFieldName)
- }
- writers = append(writers, cw)
- }
- if cfg.File != "" {
- dir := filepath.Dir(cfg.File)
- if dir != "." && dir != "" {
- if err := os.MkdirAll(dir, 0o755); err != nil {
- return err
- }
- }
- fh, err := os.OpenFile(cfg.File, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
- if err != nil {
- return err
- }
- fileHandle = fh
- writers = append(writers, fh)
- fileLevel := parseLevel(cfg.FileLevel)
- fileLogger = zerolog.New(fh).Level(fileLevel).With().Timestamp().Logger()
- }
- if len(writers) == 0 {
- return errors.New("logging: no outputs enabled")
- }
- mw := io.MultiWriter(writers...)
- logger = zerolog.New(mw).Level(level).With().Timestamp().Logger()
- return nil
- }
-
- func Close() {
- if fileHandle != nil {
- _ = fileHandle.Close()
- fileHandle = nil
- }
- }
-
- func EnabledCategory(cat string) bool {
- if len(cats) == 0 {
- return true
- }
- _, ok := cats[strings.ToLower(cat)]
- return ok
- }
-
- func logf(level zerolog.Level, cat, msg string, kv ...any) {
- if !EnabledCategory(cat) {
- return
- }
- key := cat + ":" + level.String()
- if !rlim.allow(key) {
- return
- }
- if level < logger.GetLevel() {
- return
- }
- l := logger.With().Str("cat", cat).Logger()
- e := (&l).WithLevel(level)
- for i := 0; i+1 < len(kv); i += 2 {
- k, ok := kv[i].(string)
- if !ok {
- continue
- }
- switch v := kv[i+1].(type) {
- case string:
- e = e.Str(k, v)
- case int:
- e = e.Int(k, v)
- case int64:
- e = e.Int64(k, v)
- case float64:
- e = e.Float64(k, v)
- case bool:
- e = e.Bool(k, v)
- default:
- e = e.Interface(k, v)
- }
- }
- e.Msg(msg)
- }
-
- func Debug(cat, msg string, kv ...any) { logf(zerolog.DebugLevel, cat, msg, kv...) }
- func Info(cat, msg string, kv ...any) { logf(zerolog.InfoLevel, cat, msg, kv...) }
- func Warn(cat, msg string, kv ...any) { logf(zerolog.WarnLevel, cat, msg, kv...) }
- func Error(cat, msg string, kv ...any) { logf(zerolog.ErrorLevel, cat, msg, kv...) }
-
- func parseLevel(raw string) zerolog.Level {
- s := strings.ToLower(strings.TrimSpace(raw))
- switch s {
- case "debug":
- return zerolog.DebugLevel
- case "info", "informal":
- return zerolog.InfoLevel
- case "warn", "warning":
- return zerolog.WarnLevel
- case "error":
- return zerolog.ErrorLevel
- default:
- return zerolog.InfoLevel
- }
- }
|