Repository: spf13/viper (go)

Issue:
Finalize and expose the new logger interface
### Preflight Checklist

- [X] I have searched the [issue tracker](https://www.github.com/spf13/viper/issues) for an issue that matches the one I want to file, without success.

### Problem Description

A new Logger interface has been introduced to prepare dropping JWW as a dependency (at least from the code, it's still used as the default logger implementation for backward compatibility reasons).

As pointed out in #1268 though, there is no way to provide a custom implementation at the moment. While I don't necessarily see that as a critical problem (JWW works and it's a dependency anyway), we definitely want to drop JWW, so allowing users to customize the logger is inevitable.

### Proposed Solution

Expose a `WithLogger` option for Viper to allow customizing the logger implementation.

### Alternatives Considered

_No response_

### Additional Information

Before exposing the interface, we need to finalize it (ie. agree that the current contract is suitable for Viper in the long-term). The current interface is modeled directly after the JWW usage.

Logger abstractions out there:

- Logur: https://github.com/logur/logur
- Logr: https://github.com/go-logr/logr

The contract should be defined by Viper as the consumer of the interface.

Pull request description:
Add slog support
This PR adds `log/slog` support the following way:

- On Go 1.21 and above you can use `log/slog` as usual
- On Go 1.20 and below Viper falls back to [golang.org/x/exp/slog](https://golang.org/x/exp/slog)

Under the hood, Viper uses [github.com/sagikazarmark/slog-shim](https://github.com/sagikazarmark/slog-shim) to provide support slog on Go 1.20 and below.

At the same time, this PR breaks backwards compatibility with [github.com/spf13/jwalterweatherman](https://github.com/spf13/jwalterweatherman). Enabling logging through jww no longer works after this change, but since there are no compile errors the only result is no logs being output.

Enabling logging is possible by using the new `WithLogger` option:

```go
v := viper.NewWithOptions(viper.WithLogger(slog.Default()))
```

Further changes:

- `Trace` level logs are now logged as `Debug`
- The previously experimental `WithLogger` option is changed to accept an `*slog.Logger` and is now publicly available
- `Logger` interface is deprecated and no longer in use (will be removed in a later release)

Fixes #1270
Fixes #1372

Patch:
diff --git a/experimental_logger.go b/experimental_logger.go
deleted file mode 100644
index 206dad6..0000000
--- a/experimental_logger.go
+++ /dev/null
@@ -1,11 +0,0 @@
-//go:build viper_logger
-// +build viper_logger
-
-package viper
-
-// WithLogger sets a custom logger.
-func WithLogger(l Logger) Option {
-	return optionFunc(func(v *Viper) {
-		v.logger = l
-	})
-}
diff --git a/go.mod b/go.mod
index 5cabc6e..44a52c0 100644
--- a/go.mod
+++ b/go.mod
@@ -9,9 +9,9 @@ require (
 	github.com/mitchellh/mapstructure v1.5.0
 	github.com/pelletier/go-toml/v2 v2.1.0
 	github.com/sagikazarmark/crypt v0.14.0
+	github.com/sagikazarmark/slog-shim v0.1.0
 	github.com/spf13/afero v1.9.5
 	github.com/spf13/cast v1.5.1
-	github.com/spf13/jwalterweatherman v1.1.0
 	github.com/spf13/pflag v1.0.5
 	github.com/stretchr/testify v1.8.4
 	github.com/subosito/gotenv v1.6.0
@@ -67,7 +67,7 @@ require (
 	go.uber.org/multierr v1.8.0 // indirect
 	go.uber.org/zap v1.21.0 // indirect
 	golang.org/x/crypto v0.13.0 // indirect
-	golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect
+	golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
 	golang.org/x/net v0.14.0 // indirect
 	golang.org/x/oauth2 v0.11.0 // indirect
 	golang.org/x/sync v0.3.0 // indirect
diff --git a/go.sum b/go.sum
index f608c28..2c3e0b5 100644
--- a/go.sum
+++ b/go.sum
@@ -330,6 +330,8 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV
 github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 github.com/sagikazarmark/crypt v0.14.0 h1:+QD5vjd6aZd6moHuIRVL+uJO7fkhiRjMz3ldbZQY5go=
 github.com/sagikazarmark/crypt v0.14.0/go.mod h1:JuxBJUNXrVOAywrtQNTZpOeTgcL1Az5qM7jKVDOifig=
+github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
+github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
@@ -338,8 +340,6 @@ github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
 github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
 github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
 github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
-github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
-github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -417,8 +417,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
-golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
-golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
+golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
+golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
diff --git a/logger.go b/logger.go
index 3f69e98..ba09761 100644
--- a/logger.go
+++ b/logger.go
@@ -1,14 +1,16 @@
 package viper
 
 import (
-	"fmt"
+	"context"
 
-	jww "github.com/spf13/jwalterweatherman"
+	slog "github.com/sagikazarmark/slog-shim"
 )
 
 // Logger is a unified interface for various logging use cases and practices, including:
 //   - leveled logging
 //   - structured logging
+//
+// Deprecated: use `log/slog` instead.
 type Logger interface {
 	// Trace logs a Trace event.
 	//
@@ -40,38 +42,27 @@ type Logger interface {
 	Error(msg string, keyvals ...interface{})
 }
 
-type jwwLogger struct{}
-
-func (jwwLogger) Trace(msg string, keyvals ...interface{}) {
-	jww.TRACE.Printf(jwwLogMessage(msg, keyvals...))
+// WithLogger sets a custom logger.
+func WithLogger(l *slog.Logger) Option {
+	return optionFunc(func(v *Viper) {
+		v.logger = l
+	})
 }
 
-func (jwwLogger) Debug(msg string, keyvals ...interface{}) {
-	jww.DEBUG.Printf(jwwLogMessage(msg, keyvals...))
-}
+type discardHandler struct{}
 
-func (jwwLogger) Info(msg string, keyvals ...interface{}) {
-	jww.INFO.Printf(jwwLogMessage(msg, keyvals...))
+func (n *discardHandler) Enabled(_ context.Context, _ slog.Level) bool {
+	return false
 }
 
-func (jwwLogger) Warn(msg string, keyvals ...interface{}) {
-	jww.WARN.Printf(jwwLogMessage(msg, keyvals...))
+func (n *discardHandler) Handle(_ context.Context, _ slog.Record) error {
+	return nil
 }
 
-func (jwwLogger) Error(msg string, keyvals ...interface{}) {
-	jww.ERROR.Printf(jwwLogMessage(msg, keyvals...))
+func (n *discardHandler) WithAttrs(_ []slog.Attr) slog.Handler {
+	return n
 }
 
-func jwwLogMessage(msg string, keyvals ...interface{}) string {
-	out := msg
-
-	if len(keyvals) > 0 && len(keyvals)%2 == 1 {
-		keyvals = append(keyvals, nil)
-	}
-
-	for i := 0; i <= len(keyvals)-2; i += 2 {
-		out = fmt.Sprintf("%s %v=%v", out, keyvals[i], keyvals[i+1])
-	}
-
-	return out
+func (n *discardHandler) WithGroup(_ string) slog.Handler {
+	return n
 }
diff --git a/util.go b/util.go
index 8236d3a..25c832c 100644
--- a/util.go
+++ b/util.go
@@ -18,6 +18,7 @@ import (
 	"strings"
 	"unicode"
 
+	slog "github.com/sagikazarmark/slog-shim"
 	"github.com/spf13/cast"
 )
 
@@ -104,7 +105,7 @@ func insensitiveArray(a []interface{}) {
 	}
 }
 
-func absPathify(logger Logger, inPath string) string {
+func absPathify(logger *slog.Logger, inPath string) string {
 	logger.Info("trying to resolve absolute path", "path", inPath)
 
 	if inPath == "$HOME" || strings.HasPrefix(inPath, "$HOME"+string(os.PathSeparator)) {
diff --git a/viper.go b/viper.go
index 6c9f0e0..097483b 100644
--- a/viper.go
+++ b/viper.go
@@ -35,6 +35,7 @@ import (
 
 	"github.com/fsnotify/fsnotify"
 	"github.com/mitchellh/mapstructure"
+	slog "github.com/sagikazarmark/slog-shim"
 	"github.com/spf13/afero"
 	"github.com/spf13/cast"
 	"github.com/spf13/pflag"
@@ -217,7 +218,7 @@ type Viper struct {
 
 	onConfigChange func(fsnotify.Event)
 
-	logger Logger
+	logger *slog.Logger
 
 	// TODO: should probably be protected with a mutex
 	encoderRegistry *encoding.EncoderRegistry
@@ -240,7 +241,7 @@ func New() *Viper {
 	v.env = make(map[string][]string)
 	v.aliases = make(map[string]string)
 	v.typeByDefValue = false
-	v.logger = jwwLogger{}
+	v.logger = slog.New(&discardHandler{})
 
 	v.resetEncoding()
 
@@ -1838,7 +1839,7 @@ func mergeMaps(
 	for sk, sv := range src {
 		tk := keyExists(sk, tgt)
 		if tk == "" {
-			v.logger.Trace("", "tk", "\"\"", fmt.Sprintf("tgt[%s]", sk), sv)
+			v.logger.Debug("", "tk", "\"\"", fmt.Sprintf("tgt[%s]", sk), sv)
 			tgt[sk] = sv
 			if itgt != nil {
 				itgt[sk] = sv
@@ -1848,7 +1849,7 @@ func mergeMaps(
 
 		tv, ok := tgt[tk]
 		if !ok {
-			v.logger.Trace("", fmt.Sprintf("ok[%s]", tk), false, fmt.Sprintf("tgt[%s]", sk), sv)
+			v.logger.Debug("", fmt.Sprintf("ok[%s]", tk), false, fmt.Sprintf("tgt[%s]", sk), sv)
 			tgt[sk] = sv
 			if itgt != nil {
 				itgt[sk] = sv
@@ -1859,7 +1860,7 @@ func mergeMaps(
 		svType := reflect.TypeOf(sv)
 		tvType := reflect.TypeOf(tv)
 
-		v.logger.Trace(
+		v.logger.Debug(
 			"processing",
 			"key", sk,
 			"st", svType,
@@ -1870,7 +1871,7 @@ func mergeMaps(
 
 		switch ttv := tv.(type) {
 		case map[interface{}]interface{}:
-			v.logger.Trace("merging maps (must convert)")
+			v.logger.Debug("merging maps (must convert)")
 			tsv, ok := sv.(map[interface{}]interface{})
 			if !ok {
 				v.logger.Error(
@@ -1888,7 +1889,7 @@ func mergeMaps(
 			stv := castToMapStringInterface(ttv)
 			mergeMaps(ssv, stv, ttv)
 		case map[string]interface{}:
-			v.logger.Trace("merging maps")
+			v.logger.Debug("merging maps")
 			tsv, ok := sv.(map[string]interface{})
 			if !ok {
 				v.logger.Error(
@@ -1903,7 +1904,7 @@ func mergeMaps(
 			}
 			mergeMaps(tsv, ttv, nil)
 		default:
-			v.logger.Trace("setting value")
+			v.logger.Debug("setting value")
 			tgt[tk] = sv
 			if itgt != nil {
 				itgt[tk] = sv


Test patch:
diff --git a/util_test.go b/util_test.go
index 83d5daa..3740ea6 100644
--- a/util_test.go
+++ b/util_test.go
@@ -15,6 +15,8 @@ import (
 	"path/filepath"
 	"reflect"
 	"testing"
+
+	slog "github.com/sagikazarmark/slog-shim"
 )
 
 func TestCopyAndInsensitiviseMap(t *testing.T) {
@@ -85,7 +87,7 @@ func TestAbsPathify(t *testing.T) {
 	}
 
 	for _, test := range tests {
-		got := absPathify(jwwLogger{}, test.input)
+		got := absPathify(slog.Default(), test.input)
 		if got != test.output {
 			t.Errorf("Got %v\nexpected\n%q", got, test.output)
 		}