...

Source file src/github.com/go-logr/logr/sloghandler.go

Documentation: github.com/go-logr/logr

     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  

View as plain text