sloghandler.go raw

   1  //go:build go1.21
   2  // +build go1.21
   3  
   4  /*
   5  Copyright 2023 The logr Authors.
   6  
   7  Licensed under the Apache License, Version 2.0 (the "License");
   8  you may not use this file except in compliance with the License.
   9  You may obtain a copy of the License at
  10  
  11      http://www.apache.org/licenses/LICENSE-2.0
  12  
  13  Unless required by applicable law or agreed to in writing, software
  14  distributed under the License is distributed on an "AS IS" BASIS,
  15  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16  See the License for the specific language governing permissions and
  17  limitations under the License.
  18  */
  19  
  20  package logr
  21  
  22  import (
  23  	"context"
  24  	"log/slog"
  25  )
  26  
  27  type slogHandler struct {
  28  	// May be nil, in which case all logs get discarded.
  29  	sink LogSink
  30  	// Non-nil if sink is non-nil and implements SlogSink.
  31  	slogSink SlogSink
  32  
  33  	// groupPrefix collects values from WithGroup calls. It gets added as
  34  	// prefix to value keys when handling a log record.
  35  	groupPrefix string
  36  
  37  	// levelBias can be set when constructing the handler to influence the
  38  	// slog.Level of log records. A positive levelBias reduces the
  39  	// slog.Level value. slog has no API to influence this value after the
  40  	// handler got created, so it can only be set indirectly through
  41  	// Logger.V.
  42  	levelBias slog.Level
  43  }
  44  
  45  var _ slog.Handler = &slogHandler{}
  46  
  47  // groupSeparator is used to concatenate WithGroup names and attribute keys.
  48  const groupSeparator = "."
  49  
  50  // GetLevel is used for black box unit testing.
  51  func (l *slogHandler) GetLevel() slog.Level {
  52  	return l.levelBias
  53  }
  54  
  55  func (l *slogHandler) Enabled(_ context.Context, level slog.Level) bool {
  56  	return l.sink != nil && (level >= slog.LevelError || l.sink.Enabled(l.levelFromSlog(level)))
  57  }
  58  
  59  func (l *slogHandler) Handle(ctx context.Context, record slog.Record) error {
  60  	if l.slogSink != nil {
  61  		// Only adjust verbosity level of log entries < slog.LevelError.
  62  		if record.Level < slog.LevelError {
  63  			record.Level -= l.levelBias
  64  		}
  65  		return l.slogSink.Handle(ctx, record)
  66  	}
  67  
  68  	// No need to check for nil sink here because Handle will only be called
  69  	// when Enabled returned true.
  70  
  71  	kvList := make([]any, 0, 2*record.NumAttrs())
  72  	record.Attrs(func(attr slog.Attr) bool {
  73  		kvList = attrToKVs(attr, l.groupPrefix, kvList)
  74  		return true
  75  	})
  76  	if record.Level >= slog.LevelError {
  77  		l.sinkWithCallDepth().Error(nil, record.Message, kvList...)
  78  	} else {
  79  		level := l.levelFromSlog(record.Level)
  80  		l.sinkWithCallDepth().Info(level, record.Message, kvList...)
  81  	}
  82  	return nil
  83  }
  84  
  85  // sinkWithCallDepth adjusts the stack unwinding so that when Error or Info
  86  // are called by Handle, code in slog gets skipped.
  87  //
  88  // This offset currently (Go 1.21.0) works for calls through
  89  // slog.New(ToSlogHandler(...)).  There's no guarantee that the call
  90  // chain won't change. Wrapping the handler will also break unwinding. It's
  91  // still better than not adjusting at all....
  92  //
  93  // This cannot be done when constructing the handler because FromSlogHandler needs
  94  // access to the original sink without this adjustment. A second copy would
  95  // work, but then WithAttrs would have to be called for both of them.
  96  func (l *slogHandler) sinkWithCallDepth() LogSink {
  97  	if sink, ok := l.sink.(CallDepthLogSink); ok {
  98  		return sink.WithCallDepth(2)
  99  	}
 100  	return l.sink
 101  }
 102  
 103  func (l *slogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
 104  	if l.sink == nil || len(attrs) == 0 {
 105  		return l
 106  	}
 107  
 108  	clone := *l
 109  	if l.slogSink != nil {
 110  		clone.slogSink = l.slogSink.WithAttrs(attrs)
 111  		clone.sink = clone.slogSink
 112  	} else {
 113  		kvList := make([]any, 0, 2*len(attrs))
 114  		for _, attr := range attrs {
 115  			kvList = attrToKVs(attr, l.groupPrefix, kvList)
 116  		}
 117  		clone.sink = l.sink.WithValues(kvList...)
 118  	}
 119  	return &clone
 120  }
 121  
 122  func (l *slogHandler) WithGroup(name string) slog.Handler {
 123  	if l.sink == nil {
 124  		return l
 125  	}
 126  	if name == "" {
 127  		// slog says to inline empty groups
 128  		return l
 129  	}
 130  	clone := *l
 131  	if l.slogSink != nil {
 132  		clone.slogSink = l.slogSink.WithGroup(name)
 133  		clone.sink = clone.slogSink
 134  	} else {
 135  		clone.groupPrefix = addPrefix(clone.groupPrefix, name)
 136  	}
 137  	return &clone
 138  }
 139  
 140  // attrToKVs appends a slog.Attr to a logr-style kvList.  It handle slog Groups
 141  // and other details of slog.
 142  func attrToKVs(attr slog.Attr, groupPrefix string, kvList []any) []any {
 143  	attrVal := attr.Value.Resolve()
 144  	if attrVal.Kind() == slog.KindGroup {
 145  		groupVal := attrVal.Group()
 146  		grpKVs := make([]any, 0, 2*len(groupVal))
 147  		prefix := groupPrefix
 148  		if attr.Key != "" {
 149  			prefix = addPrefix(groupPrefix, attr.Key)
 150  		}
 151  		for _, attr := range groupVal {
 152  			grpKVs = attrToKVs(attr, prefix, grpKVs)
 153  		}
 154  		kvList = append(kvList, grpKVs...)
 155  	} else if attr.Key != "" {
 156  		kvList = append(kvList, addPrefix(groupPrefix, attr.Key), attrVal.Any())
 157  	}
 158  
 159  	return kvList
 160  }
 161  
 162  func addPrefix(prefix, name string) string {
 163  	if prefix == "" {
 164  		return name
 165  	}
 166  	if name == "" {
 167  		return prefix
 168  	}
 169  	return prefix + groupSeparator + name
 170  }
 171  
 172  // levelFromSlog adjusts the level by the logger's verbosity and negates it.
 173  // It ensures that the result is >= 0. This is necessary because the result is
 174  // passed to a LogSink and that API did not historically document whether
 175  // levels could be negative or what that meant.
 176  //
 177  // Some example usage:
 178  //
 179  //	logrV0 := getMyLogger()
 180  //	logrV2 := logrV0.V(2)
 181  //	slogV2 := slog.New(logr.ToSlogHandler(logrV2))
 182  //	slogV2.Debug("msg") // =~ logrV2.V(4) =~ logrV0.V(6)
 183  //	slogV2.Info("msg")  // =~  logrV2.V(0) =~ logrV0.V(2)
 184  //	slogv2.Warn("msg")  // =~ logrV2.V(-4) =~ logrV0.V(0)
 185  func (l *slogHandler) levelFromSlog(level slog.Level) int {
 186  	result := -level
 187  	result += l.levelBias // in case the original Logger had a V level
 188  	if result < 0 {
 189  		result = 0 // because LogSink doesn't expect negative V levels
 190  	}
 191  	return int(result)
 192  }
 193