mirror of
https://github.com/zeromicro/go-zero.git
synced 2026-05-13 18:00:00 +08:00
feat: support masking sensitive data in logx (#5003)
Signed-off-by: kevin <wanjunfeng@gmail.com>
This commit is contained in:
21
core/logx/sensitive.go
Normal file
21
core/logx/sensitive.go
Normal 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
|
||||||
|
}
|
||||||
50
core/logx/sensitive_test.go
Normal file
50
core/logx/sensitive_test.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -365,19 +365,22 @@ func mergeGlobalFields(fields []LogField) []LogField {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func output(writer io.Writer, level string, val any, fields ...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.
|
switch v := val.(type) {
|
||||||
if v, ok := val.(string); ok {
|
case string:
|
||||||
|
// only truncate string content, don't know how to truncate the values of other types.
|
||||||
maxLen := atomic.LoadUint32(&maxContentLength)
|
maxLen := atomic.LoadUint32(&maxContentLength)
|
||||||
if maxLen > 0 && len(v) > int(maxLen) {
|
if maxLen > 0 && len(v) > int(maxLen) {
|
||||||
val = v[:maxLen]
|
val = v[:maxLen]
|
||||||
fields = append(fields, truncatedField)
|
fields = append(fields, truncatedField)
|
||||||
}
|
}
|
||||||
|
case Sensitive:
|
||||||
|
val = v.MaskSensitive()
|
||||||
}
|
}
|
||||||
|
|
||||||
// +3 for timestamp, level and content
|
// +3 for timestamp, level and content
|
||||||
entry := make(logEntry, len(fields)+3)
|
entry := make(logEntry, len(fields)+3)
|
||||||
for _, field := range fields {
|
for _, field := range fields {
|
||||||
entry[field.Key] = field.Value
|
entry[field.Key] = maskSensitive(field.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch atomic.LoadUint32(&encoding) {
|
switch atomic.LoadUint32(&encoding) {
|
||||||
|
|||||||
@@ -225,6 +225,48 @@ func TestWritePlainDuplicate(t *testing.T) {
|
|||||||
assert.Contains(t, buf.String(), "second=c")
|
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) {
|
func TestLogWithLimitContentLength(t *testing.T) {
|
||||||
maxLen := atomic.LoadUint32(&maxContentLength)
|
maxLen := atomic.LoadUint32(&maxContentLength)
|
||||||
atomic.StoreUint32(&maxContentLength, 10)
|
atomic.StoreUint32(&maxContentLength, 10)
|
||||||
|
|||||||
Reference in New Issue
Block a user