github.com/facebookincubator/go-belt@v0.0.0-20230703220935-39cd348f1a38/tool/logger/implementation/logrus/logger.go (about) 1 // Copyright 2022 Meta Platforms, Inc. and affiliates. 2 // 3 // Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 // 5 // 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 // 7 // 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 // 9 // 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 // 11 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 13 // Copyright (c) Facebook, Inc. and its affiliates. 14 // 15 // This source code is licensed under the MIT license found in the 16 // LICENSE file in the root directory of this source tree. 17 18 package logrus 19 20 import ( 21 "context" 22 "fmt" 23 "os" 24 "path/filepath" 25 "runtime" 26 "strings" 27 "sync" 28 "time" 29 30 "github.com/facebookincubator/go-belt" 31 "github.com/facebookincubator/go-belt/pkg/field" 32 "github.com/facebookincubator/go-belt/pkg/valuesparser" 33 "github.com/facebookincubator/go-belt/tool/logger" 34 "github.com/facebookincubator/go-belt/tool/logger/adapter" 35 "github.com/facebookincubator/go-belt/tool/logger/types" 36 "github.com/sirupsen/logrus" 37 ) 38 39 var ( 40 // FieldNameTraceIDs is the field name used to store trace IDs. 41 FieldNameTraceIDs = "trace_id" 42 ) 43 44 // Emitter implements types.Emitter given a logrus entry. 45 type Emitter struct { 46 // LogrusEntry is the actual emitter. And Emitter structure is only a wrapper 47 // to implement another interface based on logrus.Entry. 48 // 49 // We use logrus.Entry instead of logrus.Logger to be able to store precompiled fields 50 LogrusEntry *logrus.Entry 51 } 52 53 var _ types.Emitter = (*Emitter)(nil) 54 55 // Flush implements types.Emitter 56 func (l *Emitter) Flush() {} 57 58 // CheckLevel returns true if an event of a given logging level will be logged 59 func (l *Emitter) CheckLevel(level types.Level) bool { 60 return LevelFromLogrus(l.getLogrusEntry().Level) >= level 61 } 62 63 // Emit implements types.Emitter 64 func (l *Emitter) Emit(entry *types.Entry) { 65 if entry.Level == types.LevelNone || (!l.CheckLevel(entry.Level) && entry.Level > types.LevelPanic) { 66 return 67 } 68 l.logEntry(entry) 69 } 70 71 func ptr[T any](in T) *T { 72 return &in 73 } 74 75 func (l *Emitter) logEntry(entry *types.Entry) { 76 logrusLevel := LevelToLogrus(entry.Level) 77 logrusEntry := ptr(*l.LogrusEntry) // shallow copy 78 logrusEntry.Level = logrusLevel 79 logrusEntry.Caller = nil 80 if entry.Caller.Defined() { 81 caller := &runtime.Frame{ 82 PC: uintptr(entry.Caller), 83 Func: runtime.FuncForPC(uintptr(entry.Caller)), 84 } 85 caller.Function = caller.Func.Name() 86 caller.File, caller.Line = entry.Caller.FileLine() 87 caller.Entry = entry.Caller.Entry() 88 89 // this is effectively a no-op line, see RestoreCallerHook's description: 90 logrusEntry.Caller = caller 91 // the actualizator-line: 92 logrusEntry.Context = context.WithValue( 93 context.Background(), LogrusCtxKeyCaller, caller, 94 ) 95 } 96 logrusEntry.Time = entry.Timestamp 97 if !entry.Properties.Has(EntryPropertyIgnoreFields) { 98 newFieldsCount := 0 99 if entry.Fields != nil { 100 newFieldsCount = entry.Fields.Len() 101 } 102 if newFieldsCount > 0 || entry.TraceIDs != nil { 103 fields := make(logrus.Fields, len(logrusEntry.Data)+newFieldsCount+1) 104 for k, v := range logrusEntry.Data { 105 fields[k] = v 106 } 107 if entry.Fields != nil { 108 entry.Fields.ForEachField(func(f *field.Field) bool { 109 fields[f.Key] = f.Value 110 return true 111 }) 112 } 113 logrusEntry.Data = fields 114 if entry.TraceIDs != nil { 115 logrusEntry.Data[FieldNameTraceIDs] = entry.TraceIDs 116 } 117 } 118 } 119 logrusEntry.Log(logrusLevel, entry.Message) 120 // A bug in logrus: if we call `Log(Fatal, ...` instead of `Fatal(...` then 121 // it does not call the exit function. 122 // 123 // Fixing this bug manually here: 124 if logrusLevel == logrus.FatalLevel { 125 exitFunc := logrusEntry.Logger.ExitFunc 126 if exitFunc == nil { 127 exitFunc = os.Exit 128 } 129 exitFunc(1) // we use the same exit code as the original logrus's Fatal. 130 } 131 } 132 133 // NewEmitter returns a new types.Emitter implementation based on a logrus logger. 134 // This functions takes ownership of the logrus Logger instance. 135 func NewEmitter(logger *logrus.Logger, logLevel logger.Level) *Emitter { 136 // Enforcing Trace level, because we will take care of logging levels ourselves. 137 // 138 // This is required, because logrus does not support contextual logging levels, 139 // so we handle them manually to provide with this feature. 140 logger.Level = logrus.TraceLevel 141 142 // Logrus internally duplicates Entry and does not copy the Caller, so 143 // to keep the Caller we implement a workaround hook, which manually restores 144 // it. 145 // 146 // Note: 147 // The problem is there with any ReportCaller value, because with 148 // ReportCaller disabled the Caller is just set to nil, but will 149 // ReportCaller enabled it overwrites Caller using logrus's internal logic. 150 // So either it is required to hack-around each Formatter or to use a Hook, 151 // or to call `write` method manually (which is extra unsafety). 152 logger.Hooks.Add(newRestoreCallerHook()) 153 logger.ReportCaller = false 154 155 logrusEntry := newLogrusEntry(logger, LevelToLogrus(logLevel)) 156 return &Emitter{ 157 LogrusEntry: logrusEntry, 158 } 159 } 160 161 func newLogrusEntry(logrusLogger *logrus.Logger, level logrus.Level) *logrus.Entry { 162 return &logrus.Entry{ 163 Logger: logrusLogger, 164 Level: level, 165 } 166 } 167 168 type mostlyPersistentData struct { 169 fmtBufPool *sync.Pool 170 entryPool *sync.Pool 171 preHooks types.PreHooks 172 hooks types.Hooks 173 traceIDs belt.TraceIDs 174 getCallerFunc types.GetCallerPC 175 messagePrefix string 176 entryProperties types.EntryProperties 177 } 178 179 // CompactLogger implements types.CompactLogger given a logrus logger. 180 type CompactLogger struct { 181 *mostlyPersistentData 182 emitter *Emitter 183 contextFields *field.FieldsChain 184 prepareEmitterOnce sync.Once 185 } 186 187 var _ adapter.CompactLogger = (*CompactLogger)(nil) 188 189 // Flush implements types.CompactLogger 190 func (l *CompactLogger) Flush() { 191 l.emitter.Flush() 192 for _, hook := range l.hooks { 193 hook.Flush() 194 } 195 } 196 197 func (l *CompactLogger) acquireEntry() *types.Entry { 198 entry := l.entryPool.Get().(*types.Entry) 199 entry.Timestamp = time.Now() 200 entry.Fields = l.contextFields 201 entry.Properties = append(entry.Properties, EntryPropertyIgnoreFields) 202 return entry 203 } 204 205 func (l *CompactLogger) releaseEntry(entry *types.Entry) { 206 entry.Caller = 0 207 entry.Fields = nil 208 entry.Message = "" 209 entry.Properties = entry.Properties[:0] 210 l.entryPool.Put(entry) 211 } 212 213 func (l *CompactLogger) acquireBuf() *strings.Builder { 214 return l.fmtBufPool.Get().(*strings.Builder) 215 } 216 217 func (l *CompactLogger) releaseBuf(buf *strings.Builder) { 218 if buf.Cap() > 1024 { 219 return 220 } 221 buf.Reset() 222 l.fmtBufPool.Put(buf) 223 } 224 225 // LogEntry logs the given entry. 226 func (l *CompactLogger) LogEntry(entry *types.Entry) { 227 if !l.emitter.CheckLevel(entry.Level) { 228 return 229 } 230 l.logEntry(entry) 231 } 232 func (l *CompactLogger) logEntry(entry *types.Entry) { 233 if l.messagePrefix != "" { 234 entry.Message = l.messagePrefix + entry.Message 235 } 236 if !entry.Caller.Defined() && l.getCallerFunc != nil { 237 entry.Caller = l.getCallerFunc() 238 } 239 if len(l.entryProperties) != 0 { 240 entry.Properties = append(entry.Properties, l.entryProperties) 241 } 242 if !adapter.ProcessHooks(l.hooks, entry) { 243 return 244 } 245 l.prepareEmitter() 246 l.emitter.logEntry(entry) 247 } 248 249 // WithMessagePrefix implements types.CompactLogger 250 func (l *CompactLogger) WithMessagePrefix(prefix string) adapter.CompactLogger { 251 clone := l.clone() 252 clone.messagePrefix += prefix 253 return clone 254 } 255 256 // WithEntryProperties implements types.CompactLogger. 257 func (l *CompactLogger) WithEntryProperties(props ...types.EntryProperty) adapter.CompactLogger { 258 clone := l.clone() 259 clone.entryProperties = clone.entryProperties.Add(props) 260 return clone 261 } 262 263 // LogFields implements types.CompactLogger 264 func (l *CompactLogger) LogFields(level types.Level, message string, fields field.AbstractFields) { 265 preHooksResult := adapter.LogFieldsPreprocess(l.preHooks, l.traceIDs, level, l.emitter.CheckLevel(level), message, fields) 266 if preHooksResult.Skip { 267 return 268 } 269 270 if preHooksResult.ExtraFields != nil { 271 fields = field.Slice[field.AbstractFields]{fields, preHooksResult.ExtraFields} 272 } 273 logger := l 274 if fields.Len() > 0 { 275 logger = l.WithFields(fields).(*CompactLogger) 276 } 277 entry := logger.acquireEntry() 278 defer logger.releaseEntry(entry) 279 280 entry.Level = level 281 entry.Message = message 282 logger.logEntry(entry) 283 } 284 285 // Logf implements types.CompactLogger 286 func (l *CompactLogger) Logf(level types.Level, format string, args ...any) { 287 preHooksResult := adapter.LogfPreprocess(l.preHooks, l.traceIDs, level, l.emitter.CheckLevel(level), format, args...) 288 if preHooksResult.Skip { 289 return 290 } 291 292 logger := l 293 if preHooksResult.ExtraFields != nil { 294 logger = logger.WithFields(preHooksResult.ExtraFields).(*CompactLogger) 295 } 296 297 entry := logger.acquireEntry() 298 defer logger.releaseEntry(entry) 299 300 buf := logger.acquireBuf() 301 defer logger.releaseBuf(buf) 302 fmt.Fprintf(buf, format, args...) 303 304 entry.Level = level 305 entry.Message = buf.String() 306 logger.logEntry(entry) 307 } 308 309 // Log implements types.CompactLogger 310 func (l *CompactLogger) Log(level types.Level, values ...any) { 311 preHooksResult := adapter.LogPreprocess(l.preHooks, l.traceIDs, level, l.emitter.CheckLevel(level), values...) 312 if preHooksResult.Skip { 313 return 314 } 315 316 if len(values) == 1 { 317 if entry, ok := values[0].(*logger.Entry); ok { 318 entry.Fields = field.Add(entry.Fields, preHooksResult.ExtraFields) 319 if entry.TraceIDs != nil { 320 entry.Fields = field.Add(entry.Fields, preHooksResult.ExtraFields, &field.Field{Key: FieldNameTraceIDs, Value: entry.TraceIDs}) 321 } else { 322 entry.Fields = field.Add(entry.Fields, preHooksResult.ExtraFields) 323 } 324 l.logEntry(entry) 325 return 326 } 327 } 328 329 var finalFields field.AbstractFields 330 valuesParser := valuesparser.AnySlice(values) 331 if preHooksResult.ExtraFields != nil { 332 finalFields = field.Slice[field.AbstractFields]{&valuesParser, preHooksResult.ExtraFields} 333 } else { 334 finalFields = &valuesParser 335 } 336 337 logger := l 338 if finalFields.Len() != 0 { 339 logger = logger.WithFields(finalFields).(*CompactLogger) 340 } 341 342 entry := logger.acquireEntry() 343 defer logger.releaseEntry(entry) 344 345 buf := logger.acquireBuf() 346 defer logger.releaseBuf(buf) 347 valuesParser.WriteUnparsed(buf) 348 349 entry.Level = level 350 entry.Message = buf.String() 351 logger.logEntry(entry) 352 } 353 354 // Level implements types.CompactLogger 355 func (l *CompactLogger) Level() types.Level { 356 return LevelFromLogrus(l.emitter.LogrusEntry.Level) 357 } 358 359 // WithLevel implements types.CompactLogger 360 func (l *CompactLogger) WithLevel(newLevel types.Level) adapter.CompactLogger { 361 clone := l.clone() 362 clone.emitter.LogrusEntry = newLogrusEntry(l.emitter.LogrusEntry.Logger, LevelToLogrus(newLevel)) 363 return clone 364 } 365 366 func (l *CompactLogger) branch() *CompactLogger { 367 return &CompactLogger{ 368 mostlyPersistentData: l.mostlyPersistentData, 369 emitter: &Emitter{LogrusEntry: l.emitter.getLogrusEntry()}, 370 contextFields: l.contextFields, 371 } 372 } 373 374 func (l *CompactLogger) clone() *CompactLogger { 375 clone := l.branch() 376 clone.mostlyPersistentData = &[]mostlyPersistentData{*l.mostlyPersistentData}[0] 377 return clone 378 } 379 380 // WithPreHooks implements types.CompactLogger 381 func (l *CompactLogger) WithPreHooks(preHooks ...types.PreHook) adapter.CompactLogger { 382 clone := l.clone() 383 clone.preHooks = make(types.PreHooks, len(l.preHooks)+len(preHooks)) 384 copy(clone.preHooks, l.preHooks) 385 copy(clone.preHooks[len(l.preHooks):], preHooks) 386 return clone 387 } 388 389 // WithHooks implements types.CompactLogger 390 func (l *CompactLogger) WithHooks(hooks ...types.Hook) adapter.CompactLogger { 391 clone := l.clone() 392 clone.hooks = make(types.Hooks, len(l.hooks)+len(hooks)) 393 copy(clone.hooks, l.hooks) 394 copy(clone.hooks[len(l.hooks):], hooks) 395 return clone 396 } 397 398 // WithField implements types.CompactLogger 399 func (l *CompactLogger) WithField( 400 key field.Key, 401 value field.Value, 402 props ...field.Property, 403 ) adapter.CompactLogger { 404 branch := l.branch() 405 branch.contextFields = l.contextFields.WithField(key, value, props) 406 return branch 407 } 408 409 // WithFields implements types.CompactLogger 410 func (l *CompactLogger) WithFields(fields field.AbstractFields) adapter.CompactLogger { 411 branch := l.branch() 412 branch.contextFields = l.contextFields.WithFields(fields) 413 return branch 414 } 415 416 // WithTraceIDs implements types.CompactLogger 417 func (l *CompactLogger) WithTraceIDs(allTraceIDs belt.TraceIDs, newTraceIDsCount int) belt.Tool { 418 clone := l.clone() 419 clone.traceIDs = allTraceIDs 420 clone.contextFields = clone.contextFields.WithField(FieldNameTraceIDs, allTraceIDs) 421 return clone 422 } 423 424 // WithContextFields implements types.Tool 425 func (l *CompactLogger) WithContextFields(allFields *field.FieldsChain, newFieldsCount int) belt.Tool { 426 branch := l.branch() 427 branch.contextFields = allFields 428 return branch 429 } 430 431 func (l *CompactLogger) compileEmitterFields() { 432 m := make(logrus.Fields, l.contextFields.Len()) 433 l.contextFields.ForEachField(func(f *field.Field) bool { 434 m[f.Key] = f.Value 435 return true 436 }) 437 old := l.emitter.LogrusEntry 438 l.emitter.setLogrusEntry(&logrus.Entry{ 439 Logger: old.Logger, 440 Data: m, 441 Time: old.Time, 442 Level: old.Level, 443 Caller: old.Caller, 444 Message: old.Message, 445 Context: old.Context, 446 }) 447 } 448 449 func (l *CompactLogger) prepareEmitter() { 450 l.prepareEmitterOnce.Do(func() { 451 l.compileEmitterFields() 452 }) 453 } 454 455 // Emitter implements types.Emitter 456 func (l *CompactLogger) Emitter() types.Emitter { 457 l.prepareEmitter() 458 return l.emitter 459 } 460 461 func newCompactLoggerFromLogrus(logrusLogger *logrus.Logger, level logger.Level, opts ...types.Option) *CompactLogger { 462 cfg := types.Options(opts).Config() 463 return &CompactLogger{ 464 emitter: NewEmitter(logrusLogger, level), 465 mostlyPersistentData: &mostlyPersistentData{ 466 getCallerFunc: cfg.GetCallerFunc, 467 fmtBufPool: &sync.Pool{ 468 New: func() any { 469 return &strings.Builder{} 470 }, 471 }, 472 entryPool: &sync.Pool{ 473 New: func() any { 474 return &types.Entry{} 475 }, 476 }, 477 }, 478 } 479 } 480 481 // New returns a types.Logger implementation based on a logrus Logger. 482 // 483 // This function takes ownership of the logger instance. 484 func New(logger *logrus.Logger, opts ...types.Option) types.Logger { 485 return adapter.GenericSugar{ 486 CompactLogger: newCompactLoggerFromLogrus(logger, LevelFromLogrus(logger.Level), opts...), 487 } 488 } 489 490 // DefaultLogrusLogger returns a logrus logger with the default configuration. 491 // 492 // The configuration might be changed in future. 493 // 494 // Overwritable. 495 // 496 // Do not override this anywhere but in the `main` package. 497 var DefaultLogrusLogger = func() *logrus.Logger { 498 l := logrus.New() 499 l.Formatter = &logrus.TextFormatter{ 500 CallerPrettyfier: func(caller *runtime.Frame) (string, string) { 501 return "", fmt.Sprintf("%s:%d", filepath.Base(caller.File), caller.Line) 502 }, 503 } 504 return l 505 } 506 507 // Default returns a types.Logger based on logrus, using the default configuration. 508 // 509 // The configuration might be changed in future. Use this function if you would like 510 // to delegate logger configuration to somebody else and you do not rely on specific 511 // output format. 512 func Default() types.Logger { 513 return New(DefaultLogrusLogger()) 514 }