🛡️

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":"東京都渋谷区"}}

参考

所感

安易にzap.Anyを使わず、MarshalLogObjectを実装するのが性能面・使い勝手ではよい

Discussion