feat: support masking sensitive data in logx (#5003)

Signed-off-by: kevin <wanjunfeng@gmail.com>
This commit is contained in:
Kevin Wan
2025-07-18 19:51:22 +08:00
committed by GitHub
parent 1d2b0d7ab8
commit f11b78ced9
4 changed files with 119 additions and 3 deletions

21
core/logx/sensitive.go Normal file
View File

@@ -0,0 +1,21 @@
package logx
// Sensitive is an interface that defines a method for masking sensitive information in logs.
// It is typically implemented by types that contain sensitive data,
// such as passwords or personal information.
// Infov, Errorv, Debugv, and Slowv methods will call this method to mask sensitive data.
// The values in LogField will also be masked if they implement the Sensitive interface.
type Sensitive interface {
// MaskSensitive masks sensitive information in the log.
MaskSensitive() any
}
// maskSensitive returns the value returned by MaskSensitive method,
// if the value implements Sensitive interface.
func maskSensitive(v any) any {
if s, ok := v.(Sensitive); ok {
return s.MaskSensitive()
}
return v
}

View File

@@ -0,0 +1,50 @@
package logx
import (
"testing"
"github.com/stretchr/testify/assert"
)
const maskedContent = "******"
type User struct {
Name string
Pass string
}
func (u User) MaskSensitive() any {
return User{
Name: u.Name,
Pass: maskedContent,
}
}
type NonSensitiveUser struct {
Name string
Pass string
}
func TestMaskSensitive(t *testing.T) {
t.Run("sensitive", func(t *testing.T) {
user := User{
Name: "kevin",
Pass: "123",
}
mu := maskSensitive(user)
assert.Equal(t, user.Name, mu.(User).Name)
assert.Equal(t, maskedContent, mu.(User).Pass)
})
t.Run("non-sensitive", func(t *testing.T) {
user := NonSensitiveUser{
Name: "kevin",
Pass: "123",
}
mu := maskSensitive(user)
assert.Equal(t, user.Name, mu.(NonSensitiveUser).Name)
assert.Equal(t, user.Pass, mu.(NonSensitiveUser).Pass)
})
}

View File

@@ -365,19 +365,22 @@ func mergeGlobalFields(fields []LogField) []LogField {
}
func output(writer io.Writer, level string, val any, fields ...LogField) {
// only truncate string content, don't know how to truncate the values of other types.
if v, ok := val.(string); ok {
switch v := val.(type) {
case string:
// only truncate string content, don't know how to truncate the values of other types.
maxLen := atomic.LoadUint32(&maxContentLength)
if maxLen > 0 && len(v) > int(maxLen) {
val = v[:maxLen]
fields = append(fields, truncatedField)
}
case Sensitive:
val = v.MaskSensitive()
}
// +3 for timestamp, level and content
entry := make(logEntry, len(fields)+3)
for _, field := range fields {
entry[field.Key] = field.Value
entry[field.Key] = maskSensitive(field.Value)
}
switch atomic.LoadUint32(&encoding) {

View File

@@ -225,6 +225,48 @@ func TestWritePlainDuplicate(t *testing.T) {
assert.Contains(t, buf.String(), "second=c")
}
func TestLogWithSensitive(t *testing.T) {
old := atomic.SwapUint32(&encoding, plainEncodingType)
t.Cleanup(func() {
atomic.StoreUint32(&encoding, old)
})
t.Run("sensitive", func(t *testing.T) {
var buf bytes.Buffer
output(&buf, levelInfo, User{
Name: "kevin",
Pass: "123",
}, LogField{
Key: "first",
Value: "a",
}, LogField{
Key: "first",
Value: "b",
})
assert.Contains(t, buf.String(), maskedContent)
assert.NotContains(t, buf.String(), "first=a")
assert.Contains(t, buf.String(), "first=b")
})
t.Run("sensitive fields", func(t *testing.T) {
var buf bytes.Buffer
output(&buf, levelInfo, "foo", LogField{
Key: "first",
Value: User{
Name: "kevin",
Pass: "123",
},
}, LogField{
Key: "second",
Value: "b",
})
assert.Contains(t, buf.String(), "foo")
assert.Contains(t, buf.String(), "first")
assert.Contains(t, buf.String(), maskedContent)
assert.Contains(t, buf.String(), "second=b")
})
}
func TestLogWithLimitContentLength(t *testing.T) {
maxLen := atomic.LoadUint32(&maxContentLength)
atomic.StoreUint32(&maxContentLength, 10)