🛡️
uber-go/zapのフィールドマスキング方法
1. MarshalLogObject
実装方法:
- 構造体に
zapcore.ObjectMarshalerインターフェースを実装 -
MarshalLogObject()メソッドで各フィールドを明示的にエンコード -
zap.Object()を使用してログ出力
コード例:
func (u User) MarshalLogObject(enc zapcore.ObjectEncoder) error {
enc.AddString("name", u.Name)
enc.AddInt("age", u.Age)
enc.AddString("email", maskEmail(u.Email)) // マスク処理
enc.AddString("address", u.Address)
return nil
}
// 使用時
logger.Info("ユーザー情報", zap.Object("user", user))
// 出力例: {"level":"info","msg":"ユーザー情報","user":{"name":"田中花子","age":28,"email":"han***@example.com","address":"東京都渋谷区"}}
2. fmt.Stringer
実装方法:
- 型に
fmt.Stringerインターフェースを実装 -
String()メソッドでマスキングされた文字列を返す -
zap.Stringer()を使用してログ出力
コード例:
type MaskedEmail string
func (e MaskedEmail) String() string {
return maskEmail(string(e))
}
type MaskedUser struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email"`
Address string `json:"address"`
}
func (u MaskedUser) String() string {
maskedUser := struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email"`
Address string `json:"address"`
}{
Name: u.Name,
Age: u.Age,
Email: maskEmail(u.Email),
Address: u.Address,
}
jsonBytes, _ := json.Marshal(maskedUser)
return string(jsonBytes)
}
// 使用時
logger.Info("メール情報", zap.Stringer("email", MaskedEmail("test@example.com")))
// 出力例: {"level":"info","msg":"メール情報","email":"tes***@example.com"}
logger.Info("ユーザー情報", zap.Stringer("user", maskedUser))
// 出力例: {"level":"info","msg":"ユーザー情報","user":"{\"name\":\"佐藤次郎\",\"age\":35,\"email\":\"jir***@example.com\",\"address\":\"大阪府大阪市\"}"}
3. カスタムエンコーダー
※ logger全体への影響がある
実装方法:
-
zapcore.Encoderをラップしたカスタムエンコーダーを作成 -
EncodeEntry()をオーバーライドしてフィールドレベルでマスク - 既存の
zap.Any()やzap.String()をそのまま使用
コード例:
// カスタムエンコーダーの定義
type MaskingEncoder struct {
zapcore.Encoder
}
func NewMaskingEncoder(base zapcore.Encoder) *MaskingEncoder {
return &MaskingEncoder{Encoder: base}
}
func (m *MaskingEncoder) EncodeEntry(entry zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
maskedFields := make([]zapcore.Field, 0, len(fields))
for _, field := range fields {
if field.Key == "email" && field.Type == zapcore.StringType {
maskedFields = append(maskedFields, zap.String(field.Key, maskEmail(field.String)))
} else if field.Type == zapcore.ReflectType {
maskedFields = append(maskedFields, m.maskReflectField(field))
} else {
maskedFields = append(maskedFields, field)
}
}
return m.Encoder.EncodeEntry(entry, maskedFields)
}
// ref. https://pkg.go.dev/go.uber.org/zap#example-package-AdvancedConfiguration
config := zap.NewProductionConfig()
maskingEncoder := NewMaskingEncoder(zapcore.NewJSONEncoder(config.EncoderConfig))
core := zapcore.NewCore(
maskingEncoder,
zapcore.AddSync(os.Stdout),
zapcore.InfoLevel,
)
logger := zap.New(core)
// 使用時
logger.Info("ユーザー情報", zap.Any("user", user))
// 出力例: {"level":"info","msg":"ユーザー情報","user":{"name":"田中花子","age":28,"email":"han***@example.com","address":"東京都渋谷区"}}
参考
- https://betterstack.com/community/guides/logging/go/zap/
- https://github.com/uber-go/zap/issues/750
- https://pkg.go.dev/go.uber.org/zap#example-package-AdvancedConfiguration
所感
安易にzap.Anyを使わず、MarshalLogObjectを実装するのが性能面・使い勝手ではよい
Discussion