package fog import ( "fmt" "strings" "github.com/go-logr/logr" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) // TODO(aw185176): Invert level again on the actual log message so that it // corresponds to logr V()? Would need to be careful with error logs if stick // with using Zap error level as a internal sentinel. // TODO: how to handle Error vs Critical/Alert/Emergency, since those are all // just additional severities? // - dont log errors below certain levels based on verbosity of logger? // TODO: DPanic -> Alert/Critical? // fogr is a logr.Logger that uses Zap under the covers, with custom encoders // to leverage GCP structure logging features: https://cloud.google.com/logging/docs/structured-logging#special-payload-fields type fogr struct { l *zap.Logger } func New(opts ...Option) logr.Logger { o := makeOptions(opts...) c := zapcore.NewCore( newEncoder(zapcore.EncoderConfig{ LevelKey: LevelKey, TimeKey: TimeKey, MessageKey: MessageKey, StacktraceKey: StacktraceKey, NameKey: "logger", EncodeTime: zapcore.RFC3339TimeEncoder, EncodeDuration: zapcore.MillisDurationEncoder, EncodeLevel: encodeLevel(), }), zapcore.Lock(zapcore.AddSync(o.dest)), zap.NewAtomicLevelAt(zapcore.Level(o.lvl)), ) zopts := []zap.Option{ zap.AddCaller(), zap.AddCallerSkip(1), } if o.development { zopts = append(zopts, zap.Development()) } return logr.New(&fogr{zap.New(c, zopts...)}) } // Assert that fogr implements logr interfaces. var _ logr.LogSink = (*fogr)(nil) var _ logr.CallDepthLogSink = (*fogr)(nil) // Init receives optional information about the logr library for LogSink // implementations that need it. func (f *fogr) Init(ri logr.RuntimeInfo) { f.l = f.l.WithOptions(zap.AddCallerSkip(ri.CallDepth)) } // Enabled returns true if the provided log level is enabled. func (f *fogr) Enabled(lvl int) bool { return f.l.Core().Enabled(toZapLevel(lvl)) } // Info logs a non-error message with the given key/value pairs as context. // This method will only be called when Enabled(lvl) is true. See // logr.Logger.Info for more details. func (f *fogr) Info(lvl int, msg string, keysAndVals ...interface{}) { if checkedEntry := f.l.Check(toZapLevel(lvl), msg); checkedEntry != nil { checkedEntry.Write(f.handleFields(lvl, keysAndVals)...) } } // Error logs an error, with the given message and key/value pairs as context. // See logr.Logger.Error for more details. func (f *fogr) Error(err error, msg string, keysAndVals ...interface{}) { f.l.Error(msg, f.handleFields(int(zap.ErrorLevel), keysAndVals, zap.NamedError(errorKey, err))...) } // WithValues returns a new LogSink with additional key/value pairs. func (f *fogr) WithValues(keysAndValues ...interface{}) logr.LogSink { newLogger := *f newLogger.l = f.l.With(f.handleFields(-1, keysAndValues)...) return &newLogger } // WithName returns a new LogSink with the specified name appended. func (f *fogr) WithName(name string) logr.LogSink { newLogger := *f newLogger.l = f.l.Named(name) return &newLogger } // WithCallDepth returns a new LogSink with a call stack offset by depth. func (f *fogr) WithCallDepth(depth int) logr.LogSink { newLogger := *f newLogger.l = f.l.WithOptions(zap.AddCallerSkip(depth)) return &newLogger } // WithLabels decorates the input logger with string key/values pairs so that // they are added as log labels in Google Cloud, by prefixing the label keys with // an internal identifier that indicates they should be processed differently // than standard logger decorations done via WithValues. It narrows the standard // interface{} input to strings in order to align with the required data type // per the Google Cloud logging API: https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#FIELDS.labels // // If an odd number of keys and values are provided, we attempt to cast the logger // to our implementation. If successful, we log a DPanic so that uneven keysAndValues // are handled consistently with normal logging operations (Info(), Error()). // // Example usage: // // log = fog.WithLabels(log, "cmd", "my_app") func WithLabels(l logr.Logger, keysAndValues ...string) logr.Logger { if len(keysAndValues)%2 != 0 { // if the logr we were given is our implementation, leverage the zap logger // to log development panic if f, ok := l.GetSink().(*fogr); ok { f.l.WithOptions(zap.AddCallerSkip(1)).DPanic( "odd number of arguments passed as key-value pairs for logging labels", zapIt("ignored", keysAndValues), ) } // return logger unmodified return l } // prefix all keys with the internal identifier that indicates the label should // be hoisted to the GCP logging label key for i := 0; i < len(keysAndValues); { keysAndValues[i] = fmt.Sprintf("%s%s", gcpLabelKey, keysAndValues[i]) i += 2 } // cast to looser type modifiedKeysAndValues := make([]interface{}, len(keysAndValues)) for i := range keysAndValues { modifiedKeysAndValues[i] = keysAndValues[i] } // return decorated logger return l.WithValues(modifiedKeysAndValues...) } // zap levels are int8 - make sure we stay in bounds. logr itself should // ensure we never get negative values. func toZapLevel(lvl int) zapcore.Level { if lvl > 127 { lvl = 127 } // zap levels are inverted. return 0 - zapcore.Level(lvl) /* #nosec G115 */ } // handleFields converts a bunch of arbitrary key-value pairs into Zap fields. // It takes additional pre-converted Zap fields, for use with automatically // attached fields, like `error`. func (f *fogr) handleFields(lvl int, args []interface{}, additional ...zap.Field) []zap.Field { injectNumericLevel := lvl < 0 // a slightly modified version of zap.SugaredLogger.sweetenFields if len(args) == 0 { // if log level is below 0, don't add if injectNumericLevel { return additional } // fast-return if we have no sweetened fields return append(additional, zap.Int(lvlKey, lvl)) } // unlike Zap, we can be pretty sure users aren't passing structured // fields (since logr has no concept of that), so guess that we need a // little less space. // add additional field for V() level numFields := len(args)/2 + len(additional) if injectNumericLevel { numFields++ } fields := make([]zap.Field, 0, numFields) if injectNumericLevel { fields = append(fields, zap.Int(lvlKey, lvl)) } // map where we will accumulate all keys that should be hoisted to Google Cloud // log labels var googleLabels map[string]string // iterate through all of the arguments for i := 0; i < len(args); { // make sure this isn't a mismatched key for all other labels; a directly passed // key should have a value following it, this checks for the proper number of args if i == len(args)-1 { f.l.WithOptions(zap.AddCallerSkip(1)).DPanic( "odd number of arguments passed as key-value pairs for logging", zapIt("ignored key", args[i]), ) break } // process input that is passed directly and convert into key/value pair, // ensuring that the key is a string; for key/value pairs passed directly // that aren't meant for cloud logging key, val := args[i], args[i+1] keyStr, isString := key.(string) if !isString { // TODO: handle DPanic stuff // if the key isn't a string, DPanic and stop logging f.l.WithOptions(zap.AddCallerSkip(1)).DPanic( "non-string key argument passed to logging, ignoring all later arguments", zapIt("invalid key", key), ) break } if strings.HasPrefix(keyStr, gcpLabelKey) { if googleLabels == nil { googleLabels = make(map[string]string) } // because we validate in WithLabels, we can cast blindly here googleLabels[strings.TrimPrefix(keyStr, gcpLabelKey)] = val.(string) } else if strings.HasPrefix(keyStr, gcpOperationKey) { keyStr = strings.TrimPrefix(keyStr, gcpOperationKey) fields = append(fields, zapIt(operationKey, map[string]string{keyStr: val.(string)})) } else { fields = append(fields, zapIt(keyStr, val)) } i += 2 } // if we accumulated any google labels, add them now that we have finished // processing all args if googleLabels != nil { fields = append(fields, zapIt(labelsKey, googleLabels)) } return append(fields, additional...) } // zapIt converts a key-value pair into a Zap field func zapIt(field string, val interface{}) zap.Field { // Handle types that implement logr.Marshaler: log the replacement // object instead of the original one. if marshaler, ok := val.(logr.Marshaler); ok { field, val = invokeMarshaler(field, marshaler) } return zap.Any(field, val) } func invokeMarshaler(field string, m logr.Marshaler) (f string, ret interface{}) { defer func() { if r := recover(); r != nil { ret = fmt.Sprintf("PANIC=%s", r) f = field + "Error" } }() return field, m.MarshalLog() }