Skip to content

Commit f11b78c

Browse files
authored
feat: support masking sensitive data in logx (#5003)
Signed-off-by: kevin <[email protected]>
1 parent 1d2b0d7 commit f11b78c

File tree

4 files changed

+119
-3
lines changed

4 files changed

+119
-3
lines changed

core/logx/sensitive.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package logx
2+
3+
// Sensitive is an interface that defines a method for masking sensitive information in logs.
4+
// It is typically implemented by types that contain sensitive data,
5+
// such as passwords or personal information.
6+
// Infov, Errorv, Debugv, and Slowv methods will call this method to mask sensitive data.
7+
// The values in LogField will also be masked if they implement the Sensitive interface.
8+
type Sensitive interface {
9+
// MaskSensitive masks sensitive information in the log.
10+
MaskSensitive() any
11+
}
12+
13+
// maskSensitive returns the value returned by MaskSensitive method,
14+
// if the value implements Sensitive interface.
15+
func maskSensitive(v any) any {
16+
if s, ok := v.(Sensitive); ok {
17+
return s.MaskSensitive()
18+
}
19+
20+
return v
21+
}

core/logx/sensitive_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package logx
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
const maskedContent = "******"
10+
11+
type User struct {
12+
Name string
13+
Pass string
14+
}
15+
16+
func (u User) MaskSensitive() any {
17+
return User{
18+
Name: u.Name,
19+
Pass: maskedContent,
20+
}
21+
}
22+
23+
type NonSensitiveUser struct {
24+
Name string
25+
Pass string
26+
}
27+
28+
func TestMaskSensitive(t *testing.T) {
29+
t.Run("sensitive", func(t *testing.T) {
30+
user := User{
31+
Name: "kevin",
32+
Pass: "123",
33+
}
34+
35+
mu := maskSensitive(user)
36+
assert.Equal(t, user.Name, mu.(User).Name)
37+
assert.Equal(t, maskedContent, mu.(User).Pass)
38+
})
39+
40+
t.Run("non-sensitive", func(t *testing.T) {
41+
user := NonSensitiveUser{
42+
Name: "kevin",
43+
Pass: "123",
44+
}
45+
46+
mu := maskSensitive(user)
47+
assert.Equal(t, user.Name, mu.(NonSensitiveUser).Name)
48+
assert.Equal(t, user.Pass, mu.(NonSensitiveUser).Pass)
49+
})
50+
}

core/logx/writer.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -365,19 +365,22 @@ func mergeGlobalFields(fields []LogField) []LogField {
365365
}
366366

367367
func output(writer io.Writer, level string, val any, fields ...LogField) {
368-
// only truncate string content, don't know how to truncate the values of other types.
369-
if v, ok := val.(string); ok {
368+
switch v := val.(type) {
369+
case string:
370+
// only truncate string content, don't know how to truncate the values of other types.
370371
maxLen := atomic.LoadUint32(&maxContentLength)
371372
if maxLen > 0 && len(v) > int(maxLen) {
372373
val = v[:maxLen]
373374
fields = append(fields, truncatedField)
374375
}
376+
case Sensitive:
377+
val = v.MaskSensitive()
375378
}
376379

377380
// +3 for timestamp, level and content
378381
entry := make(logEntry, len(fields)+3)
379382
for _, field := range fields {
380-
entry[field.Key] = field.Value
383+
entry[field.Key] = maskSensitive(field.Value)
381384
}
382385

383386
switch atomic.LoadUint32(&encoding) {

core/logx/writer_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,48 @@ func TestWritePlainDuplicate(t *testing.T) {
225225
assert.Contains(t, buf.String(), "second=c")
226226
}
227227

228+
func TestLogWithSensitive(t *testing.T) {
229+
old := atomic.SwapUint32(&encoding, plainEncodingType)
230+
t.Cleanup(func() {
231+
atomic.StoreUint32(&encoding, old)
232+
})
233+
234+
t.Run("sensitive", func(t *testing.T) {
235+
var buf bytes.Buffer
236+
output(&buf, levelInfo, User{
237+
Name: "kevin",
238+
Pass: "123",
239+
}, LogField{
240+
Key: "first",
241+
Value: "a",
242+
}, LogField{
243+
Key: "first",
244+
Value: "b",
245+
})
246+
assert.Contains(t, buf.String(), maskedContent)
247+
assert.NotContains(t, buf.String(), "first=a")
248+
assert.Contains(t, buf.String(), "first=b")
249+
})
250+
251+
t.Run("sensitive fields", func(t *testing.T) {
252+
var buf bytes.Buffer
253+
output(&buf, levelInfo, "foo", LogField{
254+
Key: "first",
255+
Value: User{
256+
Name: "kevin",
257+
Pass: "123",
258+
},
259+
}, LogField{
260+
Key: "second",
261+
Value: "b",
262+
})
263+
assert.Contains(t, buf.String(), "foo")
264+
assert.Contains(t, buf.String(), "first")
265+
assert.Contains(t, buf.String(), maskedContent)
266+
assert.Contains(t, buf.String(), "second=b")
267+
})
268+
}
269+
228270
func TestLogWithLimitContentLength(t *testing.T) {
229271
maxLen := atomic.LoadUint32(&maxContentLength)
230272
atomic.StoreUint32(&maxContentLength, 10)

0 commit comments

Comments
 (0)