github.com/mendersoftware/go-lib-micro@v0.0.0-20240304135804-e8e39c59b148/log/log.go (about) 1 // Copyright 2023 Northern.tech AS 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package log provides a thin wrapper over logrus, with a definition 16 // of a global root logger, its setup functions and convenience wrappers. 17 // 18 // The wrappers are introduced to reduce verbosity: 19 // - logrus.Fields becomes log.Ctx 20 // - logrus.WithFields becomes log.F(), defined on a Logger type 21 // 22 // The usage scenario in a multilayer app is as follows: 23 // - a new Logger is created in the upper layer with an initial context (request id, api method...) 24 // - it is passed to lower layer which automatically includes the context, and can further enrich it 25 // - result - logs across layers are tied together with a common context 26 // 27 // Note on concurrency: 28 // - all Loggers in fact point to the single base log, which serializes logging with its mutexes 29 // - all context is copied - each layer operates on an independent copy 30 31 package log 32 33 import ( 34 "context" 35 "fmt" 36 "io" 37 "os" 38 "path" 39 "runtime" 40 "strconv" 41 "strings" 42 "time" 43 44 "github.com/sirupsen/logrus" 45 ) 46 47 var ( 48 // log is a global logger instance 49 Log = logrus.New() 50 ) 51 52 const ( 53 envLogFormat = "LOG_FORMAT" 54 envLogLevel = "LOG_LEVEL" 55 envLogDisableCaller = "LOG_DISABLE_CALLER_CONTEXT" 56 57 logFormatJSON = "json" 58 logFormatJSONAlt = "ndjson" 59 60 logFieldCaller = "caller" 61 logFieldCallerFmt = "%s@%s:%d" 62 63 pkgSirupsen = "github.com/sirupsen/logrus" 64 ) 65 66 type loggerContextKeyType int 67 68 const ( 69 loggerContextKey loggerContextKeyType = 0 70 ) 71 72 // ContextLogger interface for components which support 73 // logging with context, via setting a logger to an exisiting one, 74 // thereby inheriting its context. 75 type ContextLogger interface { 76 UseLog(l *Logger) 77 } 78 79 // init initializes the global logger to sane defaults. 80 func init() { 81 var opts Options 82 switch strings.ToLower(os.Getenv(envLogFormat)) { 83 case logFormatJSON, logFormatJSONAlt: 84 opts.Format = FormatJSON 85 default: 86 opts.Format = FormatConsole 87 } 88 opts.Level = Level(logrus.InfoLevel) 89 if lvl := os.Getenv(envLogLevel); lvl != "" { 90 logLevel, err := logrus.ParseLevel(lvl) 91 if err == nil { 92 opts.Level = Level(logLevel) 93 } 94 } 95 opts.TimestampFormat = time.RFC3339 96 opts.DisableCaller, _ = strconv.ParseBool(os.Getenv(envLogDisableCaller)) 97 Configure(opts) 98 99 Log.ExitFunc = func(int) {} 100 } 101 102 type Level logrus.Level 103 104 const ( 105 LevelPanic = Level(logrus.PanicLevel) 106 LevelFatal = Level(logrus.FatalLevel) 107 LevelError = Level(logrus.ErrorLevel) 108 LevelWarn = Level(logrus.WarnLevel) 109 LevelInfo = Level(logrus.InfoLevel) 110 LevelDebug = Level(logrus.DebugLevel) 111 LevelTrace = Level(logrus.TraceLevel) 112 ) 113 114 type Format int 115 116 const ( 117 FormatConsole Format = iota 118 FormatJSON 119 ) 120 121 type Options struct { 122 TimestampFormat string 123 124 Level Level 125 126 DisableCaller bool 127 128 Format Format 129 130 Output io.Writer 131 } 132 133 func Configure(opts Options) { 134 Log = logrus.New() 135 136 if opts.Output != nil { 137 Log.SetOutput(opts.Output) 138 } 139 Log.SetLevel(logrus.Level(opts.Level)) 140 141 if !opts.DisableCaller { 142 Log.AddHook(ContextHook{}) 143 } 144 145 var formatter logrus.Formatter 146 147 switch opts.Format { 148 case FormatConsole: 149 formatter = &logrus.TextFormatter{ 150 FullTimestamp: true, 151 TimestampFormat: opts.TimestampFormat, 152 } 153 case FormatJSON: 154 formatter = &logrus.JSONFormatter{ 155 TimestampFormat: opts.TimestampFormat, 156 } 157 } 158 Log.Formatter = formatter 159 } 160 161 // Setup allows to override the global logger setup. 162 func Setup(debug bool) { 163 if debug { 164 Log.Level = logrus.DebugLevel 165 } 166 } 167 168 // Ctx short for log context, alias for the more verbose logrus.Fields. 169 type Ctx map[string]interface{} 170 171 // Logger is a wrapper for logrus.Entry. 172 type Logger struct { 173 *logrus.Entry 174 } 175 176 // New returns a new Logger with a given context, derived from the global Log. 177 func New(ctx Ctx) *Logger { 178 return NewFromLogger(Log, ctx) 179 } 180 181 // NewEmpty returns a new logger with empty context 182 func NewEmpty() *Logger { 183 return New(Ctx{}) 184 } 185 186 // NewFromLogger returns a new Logger derived from a given logrus.Logger, 187 // instead of the global one. 188 func NewFromLogger(log *logrus.Logger, ctx Ctx) *Logger { 189 return &Logger{log.WithFields(logrus.Fields(ctx))} 190 } 191 192 // NewFromLogger returns a new Logger derived from a given logrus.Logger, 193 // instead of the global one. 194 func NewFromEntry(log *logrus.Entry, ctx Ctx) *Logger { 195 return &Logger{log.WithFields(logrus.Fields(ctx))} 196 } 197 198 // F returns a new Logger enriched with new context fields. 199 // It's a less verbose wrapper over logrus.WithFields. 200 func (l *Logger) F(ctx Ctx) *Logger { 201 return &Logger{l.Entry.WithFields(logrus.Fields(ctx))} 202 } 203 204 func (l *Logger) Level() logrus.Level { 205 return l.Entry.Logger.Level 206 } 207 208 type ContextHook struct { 209 } 210 211 func (hook ContextHook) Levels() []logrus.Level { 212 return logrus.AllLevels 213 } 214 215 func FmtCaller(caller runtime.Frame) string { 216 return fmt.Sprintf( 217 logFieldCallerFmt, 218 path.Base(caller.Function), 219 path.Base(caller.File), 220 caller.Line, 221 ) 222 } 223 224 func (hook ContextHook) Fire(entry *logrus.Entry) error { 225 const ( 226 minCallDepth = 6 // logrus.Logger.Log 227 maxCallDepth = 8 // logrus.Logger.<Level>f 228 ) 229 var pcs [1 + maxCallDepth - minCallDepth]uintptr 230 if _, ok := entry.Data[logFieldCaller]; !ok { 231 // We don't know how deep we are in the callstack since the hook can be fired 232 // at different levels. Search between depth 6 -> 8. 233 i := runtime.Callers(minCallDepth, pcs[:]) 234 frames := runtime.CallersFrames(pcs[:i]) 235 var caller *runtime.Frame 236 for frame, _ := frames.Next(); frame.PC != 0; frame, _ = frames.Next() { 237 if !strings.HasPrefix(frame.Function, pkgSirupsen) { 238 caller = &frame 239 break 240 } 241 } 242 if caller != nil { 243 entry.Data[logFieldCaller] = FmtCaller(*caller) 244 } 245 } 246 return nil 247 } 248 249 // WithCallerContext returns a new logger with caller set to the parent caller 250 // context. The skipParents select how many caller contexts to skip, a value of 251 // 0 sets the context to the caller of this function. 252 func (l *Logger) WithCallerContext(skipParents int) *Logger { 253 const calleeDepth = 2 254 var pc [1]uintptr 255 newEntry := l 256 i := runtime.Callers(calleeDepth+skipParents, pc[:]) 257 frame, _ := runtime.CallersFrames(pc[:i]). 258 Next() 259 if frame.Func != nil { 260 newEntry = &Logger{Entry: l.Dup()} 261 newEntry.Data[logFieldCaller] = FmtCaller(frame) 262 } 263 return newEntry 264 } 265 266 // Grab an instance of Logger that may have been passed in context.Context. 267 // Returns the logger or creates a new instance if none was found in ctx. Since 268 // Logger is based on logrus.Entry, if logger instance from context is any of 269 // logrus.Logger, logrus.Entry, necessary adaption will be applied. 270 func FromContext(ctx context.Context) *Logger { 271 l := ctx.Value(loggerContextKey) 272 if l == nil { 273 return New(Ctx{}) 274 } 275 276 switch v := l.(type) { 277 case *Logger: 278 return v 279 case *logrus.Entry: 280 return NewFromEntry(v, Ctx{}) 281 case *logrus.Logger: 282 return NewFromLogger(v, Ctx{}) 283 default: 284 return New(Ctx{}) 285 } 286 } 287 288 // WithContext adds logger to context `ctx` and returns the resulting context. 289 func WithContext(ctx context.Context, log *Logger) context.Context { 290 return context.WithValue(ctx, loggerContextKey, log) 291 }