github.com/facebookincubator/go-belt@v0.0.0-20230703220935-39cd348f1a38/tool/logger/adapter/generic_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  package adapter
    14  
    15  import (
    16  	"fmt"
    17  	"os"
    18  	"strings"
    19  	"time"
    20  
    21  	"github.com/facebookincubator/go-belt"
    22  	"github.com/facebookincubator/go-belt/pkg/field"
    23  	"github.com/facebookincubator/go-belt/pkg/valuesparser"
    24  	"github.com/facebookincubator/go-belt/tool/logger/experimental"
    25  	"github.com/facebookincubator/go-belt/tool/logger/types"
    26  )
    27  
    28  // GenericLogger is a generic implementation of types.CompactLogger given
    29  // Emitters.
    30  type GenericLogger struct {
    31  	Emitters        types.Emitters
    32  	CurrentLevel    types.Level
    33  	Fields          *field.FieldsChain
    34  	TraceIDs        belt.TraceIDs
    35  	CurrentPreHooks types.PreHooks
    36  	CurrentHooks    types.Hooks
    37  	MessagePrefix   string
    38  	GetCallerFunc   types.GetCallerPC
    39  	EntryProperties types.EntryProperties
    40  }
    41  
    42  var _ CompactLogger = (*GenericLogger)(nil)
    43  
    44  // Level implements types.CompactLogger.
    45  func (l *GenericLogger) Level() types.Level {
    46  	return l.CurrentLevel
    47  }
    48  
    49  // WithTraceIDs implements types.CompactLogger.
    50  func (l *GenericLogger) WithTraceIDs(allTraceIDs belt.TraceIDs, newTraceIDs int) belt.Tool {
    51  	newLogger := *l
    52  	newLogger.TraceIDs = allTraceIDs
    53  	return &newLogger
    54  }
    55  
    56  // WithLevel implements types.CompactLogger.
    57  func (l *GenericLogger) WithLevel(newLevel types.Level) CompactLogger {
    58  	newLogger := *l
    59  	newLogger.CurrentLevel = newLevel
    60  	return &newLogger
    61  }
    62  
    63  // WithPreHooks implements types.CompactLogger.
    64  func (l *GenericLogger) WithPreHooks(preHooks ...types.PreHook) CompactLogger {
    65  	newLogger := *l
    66  	if preHooks == nil {
    67  		newLogger.CurrentPreHooks = nil
    68  	} else {
    69  		newLogger.CurrentPreHooks = types.PreHooks{newLogger.CurrentPreHooks, types.PreHooks(preHooks)}
    70  	}
    71  	return &newLogger
    72  }
    73  
    74  // WithHooks implements types.CompactLogger.
    75  func (l *GenericLogger) WithHooks(hooks ...types.Hook) CompactLogger {
    76  	newLogger := *l
    77  	if hooks == nil {
    78  		newLogger.CurrentHooks = nil
    79  	} else {
    80  		newLogger.CurrentHooks = types.Hooks{newLogger.CurrentHooks, types.Hooks(hooks)}
    81  	}
    82  	return &newLogger
    83  }
    84  
    85  // WithField implements types.CompactLogger.
    86  func (l *GenericLogger) WithField(
    87  	key field.Key,
    88  	value field.Value,
    89  	props ...field.Property,
    90  ) CompactLogger {
    91  	newLogger := *l
    92  	newLogger.Fields = newLogger.Fields.WithField(key, value, props...)
    93  	return &newLogger
    94  }
    95  
    96  // WithFields implements types.CompactLogger.
    97  func (l *GenericLogger) WithFields(fields field.AbstractFields) CompactLogger {
    98  	newLogger := *l
    99  	if fields == nil {
   100  		return &newLogger
   101  	}
   102  	newLogger.Fields = newLogger.Fields.WithFields(fields)
   103  	return &newLogger
   104  }
   105  
   106  // WithContextFields implements types.CompactLogger.
   107  func (l *GenericLogger) WithContextFields(allFields *field.FieldsChain, newFields int) belt.Tool {
   108  	newLogger := *l
   109  	newLogger.Fields = allFields
   110  	return &newLogger
   111  }
   112  
   113  // Emitter implements types.CompactLogger.
   114  func (l *GenericLogger) Emitter() types.Emitter {
   115  	return l.Emitters
   116  }
   117  
   118  // Log implements types.CompactLogger.
   119  func (l *GenericLogger) Log(level types.Level, values ...any) {
   120  	preHooksResult := LogPreprocess(l.CurrentPreHooks, l.TraceIDs, level, l.CurrentLevel >= level, values...)
   121  	if preHooksResult.Skip {
   122  		return
   123  	}
   124  
   125  	if len(values) == 1 {
   126  		if entry, ok := values[0].(*types.Entry); ok {
   127  			entry.Fields = field.Add(entry.Fields, preHooksResult.ExtraFields)
   128  			l.emit(entry)
   129  			return
   130  		}
   131  	}
   132  
   133  	// TODO: optimize this
   134  	valuesParser := valuesparser.AnySlice(values)
   135  	fields := make(field.Fields, 0, valuesParser.Len())
   136  	valuesParser.ForEachField(func(f *field.Field) bool {
   137  		fields = append(fields, *f)
   138  		return true
   139  	})
   140  
   141  	// TODO: optimize this
   142  	var buf strings.Builder
   143  	valuesParser.WriteUnparsed(&buf)
   144  
   145  	var finalFields field.AbstractFields
   146  	if preHooksResult.ExtraFields != nil {
   147  		finalFields = field.Slice[field.AbstractFields]{fields, preHooksResult.ExtraFields}
   148  	} else {
   149  		finalFields = fields
   150  	}
   151  
   152  	entry := l.acquireEntry(level, buf.String(), finalFields, preHooksResult.ExtraEntryProperties)
   153  	defer releaseEntry(entry)
   154  
   155  	l.emit(entry)
   156  }
   157  
   158  // LogFields implements types.CompactLogger.
   159  func (l *GenericLogger) LogFields(level types.Level, message string, fields field.AbstractFields) {
   160  	preHooksResult := LogFieldsPreprocess(l.CurrentPreHooks, l.TraceIDs, level, l.CurrentLevel >= level, message, fields)
   161  	if preHooksResult.Skip {
   162  		return
   163  	}
   164  
   165  	var finalFields field.AbstractFields
   166  	if preHooksResult.ExtraFields != nil {
   167  		finalFields = field.Slice[field.AbstractFields]{fields, preHooksResult.ExtraFields}
   168  	} else {
   169  		finalFields = fields
   170  	}
   171  
   172  	entry := l.acquireEntry(level, message, finalFields, preHooksResult.ExtraEntryProperties)
   173  	defer releaseEntry(entry)
   174  
   175  	l.emit(entry)
   176  }
   177  
   178  // LogPreprocess checks logging level and calls PreHooks. Returns the total
   179  // outcome of these two steps.
   180  //
   181  // It will never return Skip=true for message logging levels Panic and Fatal, instead
   182  // it will ask to skip the logging through Entry property "experimental.EntryPropertySkipAllEmitters".
   183  //
   184  // It is a helper which is supposed to be used in the beginning of a Log implementation.
   185  func LogPreprocess(
   186  	preHooks types.PreHooks,
   187  	traceIDs belt.TraceIDs,
   188  	level types.Level,
   189  	logLevelSatisfied bool,
   190  	values ...any,
   191  ) types.PreHookResult {
   192  	if level == types.LevelNone {
   193  		return types.PreHookResult{Skip: true}
   194  	}
   195  	shouldSkip := false
   196  	if !logLevelSatisfied {
   197  		if level != types.LevelPanic && level != types.LevelFatal {
   198  			return types.PreHookResult{Skip: true}
   199  		}
   200  		shouldSkip = true
   201  	}
   202  
   203  	result := preHooks.ProcessInput(traceIDs, level, values...)
   204  	if !result.Skip {
   205  		if shouldSkip {
   206  			result.ExtraEntryProperties = append(result.ExtraEntryProperties, experimental.EntryPropertySkipAllEmitters)
   207  		}
   208  		return result
   209  	}
   210  	if level != types.LevelPanic && level != types.LevelFatal {
   211  		return result
   212  	}
   213  
   214  	result.Skip = false
   215  	result.ExtraEntryProperties = append(result.ExtraEntryProperties, experimental.EntryPropertySkipAllEmitters)
   216  	return result
   217  }
   218  
   219  // LogFieldsPreprocess checks logging level and calls PreHooks. Returns the total
   220  // outcome of these two steps.
   221  //
   222  // It will never return Skip=true for message logging levels Panic and Fatal, instead
   223  // it will ask to skip the logging through Entry property "experimental.EntryPropertySkipAllEmitters".
   224  //
   225  // It is a helper which is supposed to be used in the beginning of a LogFields implementation.
   226  func LogFieldsPreprocess(
   227  	preHooks types.PreHooks,
   228  	traceIDs belt.TraceIDs,
   229  	level types.Level,
   230  	logLevelSatisfied bool,
   231  	message string,
   232  	fields field.AbstractFields,
   233  ) types.PreHookResult {
   234  	if level == types.LevelNone {
   235  		return types.PreHookResult{Skip: true}
   236  	}
   237  	shouldSkip := false
   238  	if !logLevelSatisfied {
   239  		if level != types.LevelPanic && level != types.LevelFatal {
   240  			return types.PreHookResult{Skip: true}
   241  		}
   242  		shouldSkip = true
   243  	}
   244  
   245  	result := preHooks.ProcessInputFields(traceIDs, level, message, fields)
   246  	if !result.Skip {
   247  		if shouldSkip {
   248  			result.ExtraEntryProperties = append(result.ExtraEntryProperties, experimental.EntryPropertySkipAllEmitters)
   249  		}
   250  		return result
   251  	}
   252  	if level != types.LevelPanic && level != types.LevelFatal {
   253  		return result
   254  	}
   255  
   256  	result.Skip = false
   257  	result.ExtraEntryProperties = append(result.ExtraEntryProperties, experimental.EntryPropertySkipAllEmitters)
   258  	return result
   259  }
   260  
   261  // LogfPreprocess checks logging level and calls PreHooks. Returns the total
   262  // outcome of these two steps.
   263  //
   264  // It will never return Skip=true for message with logging levels Panic and Fatal, instead
   265  // it will ask to skip the logging through Entry property "experimental.EntryPropertySkipAllEmitters".
   266  //
   267  // It is a helper which is supposed to be used in the beginning of a Logf implementation.
   268  func LogfPreprocess(
   269  	preHooks types.PreHooks,
   270  	traceIDs belt.TraceIDs,
   271  	level types.Level,
   272  	logLevelSatisfied bool,
   273  	format string,
   274  	args ...any,
   275  ) types.PreHookResult {
   276  	if level == types.LevelNone {
   277  		return types.PreHookResult{Skip: true}
   278  	}
   279  	shouldSkip := false
   280  	if !logLevelSatisfied {
   281  		if level != types.LevelPanic && level != types.LevelFatal {
   282  			return types.PreHookResult{Skip: true}
   283  		}
   284  		shouldSkip = true
   285  	}
   286  
   287  	result := preHooks.ProcessInputf(traceIDs, level, format, args...)
   288  	if !result.Skip {
   289  		if shouldSkip {
   290  			result.ExtraEntryProperties = append(result.ExtraEntryProperties, experimental.EntryPropertySkipAllEmitters)
   291  		}
   292  		return result
   293  	}
   294  	if level != types.LevelPanic && level != types.LevelFatal {
   295  		return result
   296  	}
   297  
   298  	result.Skip = false
   299  	result.ExtraEntryProperties = append(result.ExtraEntryProperties, experimental.EntryPropertySkipAllEmitters)
   300  	return result
   301  }
   302  
   303  // Logf implements types.CompactLogger.
   304  func (l *GenericLogger) Logf(level types.Level, format string, args ...any) {
   305  	preHooksResult := LogfPreprocess(l.CurrentPreHooks, l.TraceIDs, level, l.CurrentLevel >= level, format, args...)
   306  	if preHooksResult.Skip {
   307  		return
   308  	}
   309  
   310  	entry := l.acquireEntry(level, fmt.Sprintf(format, args...), preHooksResult.ExtraFields, preHooksResult.ExtraEntryProperties)
   311  	defer releaseEntry(entry)
   312  
   313  	l.emit(entry)
   314  }
   315  
   316  // WithMessagePrefix implements types.CompactLogger.
   317  func (l *GenericLogger) WithMessagePrefix(prefix string) CompactLogger {
   318  	newLogger := *l
   319  	newLogger.MessagePrefix = newLogger.MessagePrefix + prefix
   320  	return &newLogger
   321  }
   322  
   323  // WithMessagePrefix implements types.CompactLogger.
   324  func (l *GenericLogger) WithEntryProperties(props ...types.EntryProperty) CompactLogger {
   325  	if len(props) == 0 {
   326  		return l
   327  	}
   328  	newLogger := *l
   329  	newLogger.EntryProperties = newLogger.EntryProperties.Add(props)
   330  	return &newLogger
   331  }
   332  
   333  // ProcessHooks executes hooks and never returns false for Panic and Fatal logging
   334  // levels. In case of a "false" result from hooks for Panic or Fatal it adds
   335  // EntryProperty "experimental.EntryPropertySkipAllEmitters" and returns true.
   336  //
   337  // It is a helper which is supposed to be used in Logf, LogFields and Log implementations.
   338  func ProcessHooks(hooks types.Hooks, entry *types.Entry) bool {
   339  	if hooks.ProcessLogEntry(entry) {
   340  		return true
   341  	}
   342  
   343  	if entry.Level != types.LevelPanic && entry.Level != types.LevelFatal {
   344  		return false
   345  	}
   346  
   347  	entry.Properties = append(entry.Properties, experimental.EntryPropertySkipAllEmitters)
   348  	return true
   349  }
   350  
   351  func (l *GenericLogger) emit(entry *types.Entry) {
   352  	if !ProcessHooks(l.CurrentHooks, entry) {
   353  		return
   354  	}
   355  
   356  	if !entry.Properties.Has(experimental.EntryPropertySkipAllEmitters) {
   357  		l.Emitters.Emit(entry)
   358  	}
   359  
   360  	switch entry.Level {
   361  	case types.LevelPanic, types.LevelFatal:
   362  		l.Flush()
   363  		switch entry.Level {
   364  		case types.LevelPanic:
   365  			panic(fmt.Sprintf("panic was requested with the log entry: %#v", entry))
   366  		case types.LevelFatal:
   367  			os.Exit(2)
   368  		}
   369  	}
   370  }
   371  
   372  // Flush implements types.CompactLogger.
   373  func (l *GenericLogger) Flush() {
   374  	l.Emitters.Flush()
   375  }
   376  
   377  func (l *GenericLogger) acquireEntry(level types.Level, message string, fields field.AbstractFields, props types.EntryProperties) *types.Entry {
   378  	entry := acquireEntry()
   379  	entry.Level = level
   380  	entry.Timestamp = time.Now()
   381  	entry.TraceIDs = l.TraceIDs
   382  	entry.Message = l.MessagePrefix + message
   383  	entry.Fields = l.Fields.WithFields(fields)
   384  	entry.Properties = l.EntryProperties.Add(props...)
   385  	entry.Caller = l.GetCallerFunc()
   386  	return entry
   387  }