sloghandler.go 5.6 KB

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