github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/pkg/logger/logger.go (about)

     1  package logger
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"log"
     8  	"time"
     9  
    10  	build "github.com/cozy/cozy-stack/pkg/config"
    11  	"github.com/redis/go-redis/v9"
    12  	"github.com/sirupsen/logrus"
    13  )
    14  
    15  var debugLogger *logrus.Logger
    16  
    17  // Fields type, used to pass to [Logger.WithFields].
    18  type Fields map[string]interface{}
    19  
    20  // Logger allows to emits logs to the divers log systems.
    21  type Logger interface {
    22  	Debugf(format string, args ...interface{})
    23  	Infof(format string, args ...interface{})
    24  	Warnf(format string, args ...interface{})
    25  	Errorf(format string, args ...interface{})
    26  
    27  	Debug(msg string)
    28  	Info(msg string)
    29  	Warn(msg string)
    30  	Error(msg string)
    31  
    32  	// Generic field operations
    33  	WithField(fn string, fv interface{}) Logger
    34  	WithFields(fields Fields) Logger
    35  
    36  	// Business specific field operations.
    37  	WithTime(t time.Time) Logger
    38  	WithDomain(s string) Logger
    39  
    40  	Log(level Level, msg string)
    41  }
    42  
    43  // Options contains the configuration values of the logger system
    44  type Options struct {
    45  	Hooks  []logrus.Hook
    46  	Output io.Writer
    47  	Level  string
    48  	Redis  redis.UniversalClient
    49  }
    50  
    51  // Init initializes the logger module with the specified options.
    52  //
    53  // It also setup the global logger for go-redis. Thoses are at
    54  // Info level.
    55  func Init(opt Options) error {
    56  	level := opt.Level
    57  	if level == "" {
    58  		level = "info"
    59  	}
    60  	logLevel, err := logrus.ParseLevel(level)
    61  	if err != nil {
    62  		return err
    63  	}
    64  
    65  	// Setup the global logger in case of someone call the global functions.
    66  	setupLogger(logrus.StandardLogger(), logLevel, opt)
    67  
    68  	// Setup the debug logger used for the the domains in debug mode.
    69  	debugLogger = logrus.New()
    70  	setupLogger(debugLogger, logrus.DebugLevel, opt)
    71  
    72  	w := WithNamespace("go-redis").Writer()
    73  	l := log.New(w, "", 0)
    74  	redis.SetLogger(&contextPrint{l})
    75  
    76  	err = initDebugger(opt.Redis)
    77  	if err != nil {
    78  		return err
    79  	}
    80  	return nil
    81  }
    82  
    83  // Entry is the struct on which we can call the Debug, Info, Warn, Error
    84  // methods with the structured data accumulated.
    85  type Entry struct {
    86  	entry *logrus.Entry
    87  }
    88  
    89  // WithDomain returns a logger with the specified domain field.
    90  func WithDomain(domain string) *Entry {
    91  	e := logrus.WithField("domain", domain)
    92  	return &Entry{e}
    93  }
    94  
    95  // WithNamespace returns a logger with the specified nspace field.
    96  func WithNamespace(nspace string) *Entry {
    97  	entry := logrus.WithField("nspace", nspace)
    98  
    99  	return &Entry{entry}
   100  }
   101  
   102  // WithNamespace adds a namespace (nspace field).
   103  func (e *Entry) WithNamespace(nspace string) *Entry {
   104  	entry := e.entry.WithField("nspace", nspace)
   105  	return &Entry{entry}
   106  }
   107  
   108  // WithDomain add a domain field.
   109  func (e *Entry) WithDomain(domain string) Logger {
   110  	entry := e.entry.WithField("domain", domain)
   111  	return &Entry{entry}
   112  }
   113  
   114  // WithField adds a single field to the Entry.
   115  func (e *Entry) WithField(key string, value interface{}) Logger {
   116  	entry := e.entry.WithField(key, value)
   117  	return &Entry{entry}
   118  }
   119  
   120  // WithFields adds a map of fields to the Entry.
   121  func (e *Entry) WithFields(fields Fields) Logger {
   122  	entry := e.entry.WithFields(logrus.Fields(fields))
   123  	return &Entry{entry}
   124  }
   125  
   126  // WithTime overrides the Entry's time
   127  func (e *Entry) WithTime(t time.Time) Logger {
   128  	entry := e.entry.WithTime(t)
   129  	return &Entry{entry}
   130  }
   131  
   132  // AddHook adds a hook on a logger.
   133  func (e *Entry) AddHook(hook logrus.Hook) {
   134  	// We need to clone the underlying logger in order to add a specific hook
   135  	// only on this logger.
   136  	in := e.entry.Logger
   137  	cloned := &logrus.Logger{
   138  		Out:       in.Out,
   139  		Hooks:     make(logrus.LevelHooks, len(in.Hooks)),
   140  		Formatter: in.Formatter,
   141  		Level:     in.Level,
   142  	}
   143  	for k, v := range in.Hooks {
   144  		cloned.Hooks[k] = v
   145  	}
   146  	cloned.AddHook(hook)
   147  	e.entry.Logger = cloned
   148  }
   149  
   150  // maxLineWidth limits the number of characters of a line of log to avoid issue
   151  // with syslog.
   152  const maxLineWidth = 2000
   153  
   154  func (e *Entry) Log(level Level, msg string) {
   155  	if len(msg) > maxLineWidth {
   156  		msg = msg[:maxLineWidth-12] + " [TRUNCATED]"
   157  	}
   158  
   159  	if level == DebugLevel && e.IsDebug() {
   160  		// The domain is listed in the debug domains and the ttl is valid, use the debuglogger
   161  		// to debug
   162  		debugLogger.WithFields(e.entry.Data).Log(logrus.DebugLevel, msg)
   163  		return
   164  	}
   165  
   166  	e.entry.Log(getLogrusLevel(level), msg)
   167  }
   168  
   169  func (e *Entry) Debug(msg string) {
   170  	e.Log(DebugLevel, msg)
   171  }
   172  
   173  func (e *Entry) Info(msg string) {
   174  	e.Log(InfoLevel, msg)
   175  }
   176  
   177  func (e *Entry) Warn(msg string) {
   178  	e.Log(WarnLevel, msg)
   179  }
   180  
   181  func (e *Entry) Error(msg string) {
   182  	e.Log(ErrorLevel, msg)
   183  }
   184  
   185  func (e *Entry) Debugf(format string, args ...interface{}) {
   186  	e.Debug(fmt.Sprintf(format, args...))
   187  }
   188  
   189  func (e *Entry) Infof(format string, args ...interface{}) {
   190  	e.Info(fmt.Sprintf(format, args...))
   191  }
   192  
   193  func (e *Entry) Warnf(format string, args ...interface{}) {
   194  	e.Warn(fmt.Sprintf(format, args...))
   195  }
   196  
   197  func (e *Entry) Errorf(format string, args ...interface{}) {
   198  	e.Error(fmt.Sprintf(format, args...))
   199  }
   200  
   201  func (e *Entry) Writer() *io.PipeWriter {
   202  	return e.entry.Writer()
   203  }
   204  
   205  // IsDebug returns whether or not the debug mode is activated.
   206  func (e *Entry) IsDebug() bool {
   207  	if e.entry.Logger.Level == logrus.DebugLevel {
   208  		return true
   209  	}
   210  
   211  	domain, haveDomain := e.entry.Data["domain"].(string)
   212  	return haveDomain && debugger.ExpiresAt(domain) != nil
   213  }
   214  
   215  func setupLogger(logger *logrus.Logger, lvl logrus.Level, opt Options) {
   216  	logger.SetLevel(lvl)
   217  
   218  	if opt.Output != nil {
   219  		logger.SetOutput(opt.Output)
   220  	}
   221  
   222  	// We need to reset the hooks to avoid the accumulation of hooks for
   223  	// the global loggers in case of several calls to `Init`.
   224  	//
   225  	// This is the case for `logrus.StandardLogger()` and the tests for example.
   226  	logger.Hooks = logrus.LevelHooks{}
   227  
   228  	for _, hook := range opt.Hooks {
   229  		logger.AddHook(hook)
   230  	}
   231  
   232  	formatter := logger.Formatter.(*logrus.TextFormatter)
   233  	if build.IsDevRelease() && lvl == logrus.DebugLevel {
   234  		formatter.TimestampFormat = time.RFC3339Nano // Nanoseconds formatter
   235  	} else {
   236  		formatter.TimestampFormat = "2006-01-02T15:04:05.000Z07:00" // Milliseconds formatter
   237  	}
   238  }
   239  
   240  type contextPrint struct {
   241  	l *log.Logger
   242  }
   243  
   244  func (c contextPrint) Printf(ctx context.Context, format string, args ...interface{}) {
   245  	c.l.Printf(format, args...)
   246  }
   247  
   248  func getLogrusLevel(lvl Level) logrus.Level {
   249  	var logrusLevel logrus.Level
   250  	switch lvl {
   251  	case DebugLevel:
   252  		logrusLevel = logrus.DebugLevel
   253  	case InfoLevel:
   254  		logrusLevel = logrus.InfoLevel
   255  	case WarnLevel:
   256  		logrusLevel = logrus.WarnLevel
   257  	default:
   258  		logrusLevel = logrus.ErrorLevel
   259  	}
   260  
   261  	return logrusLevel
   262  }