diff --git a/fs/log.go b/fs/log.go index dcd726eec..bc9e90bc8 100644 --- a/fs/log.go +++ b/fs/log.go @@ -146,8 +146,17 @@ func logSlog(level LogLevel, text string, attrs []any) { func logSlogWithObject(level LogLevel, o any, text string, attrs []any) { if o != nil { + var object string + switch o.(type) { + case fmt.Stringer, string: + object = fmt.Sprint(o) + default: + // Don't render the fields of arbitrary objects as they + // may contain sensitive data such as credentials. + object = fmt.Sprintf("%T", o) + } attrs = slices.Concat(attrs, []any{ - "object", fmt.Sprintf("%+v", o), + "object", object, "objectType", fmt.Sprintf("%T", o), }) } diff --git a/fs/log_test.go b/fs/log_test.go index 0bb04bd21..e7c790039 100644 --- a/fs/log_test.go +++ b/fs/log_test.go @@ -1,8 +1,10 @@ package fs import ( + "bytes" "encoding/json" "fmt" + "log/slog" "strconv" "testing" @@ -32,6 +34,30 @@ func TestLogValue(t *testing.T) { assert.Equal(t, "", x.String()) } +func TestLogSlogWithObject(t *testing.T) { + var buf bytes.Buffer + oldLogger := logger + defer func() { logger = oldLogger }() + SetLogger(slog.NewTextHandler(&buf, nil)) + + // Objects with a String method are rendered with it + logSlogWithObject(LogLevelError, withString{}, "message", nil) + assert.Contains(t, buf.String(), "object=hello") + + // Plain strings are rendered as themselves + buf.Reset() + logSlogWithObject(LogLevelError, "potato", "message", nil) + assert.Contains(t, buf.String(), "object=potato") + + // Anything else shows only its type as it may contain + // sensitive data such as credentials + buf.Reset() + type secrets struct{ Password string } + logSlogWithObject(LogLevelError, &secrets{Password: "SECRET"}, "message", nil) + assert.Contains(t, buf.String(), "object=*fs.secrets") + assert.NotContains(t, buf.String(), "SECRET") +} + func TestLogLevelString(t *testing.T) { for _, test := range []struct { in LogLevel