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 }