Go言語でログの一部をマスクしたい。
例えば ログの一部にパスワードが書かれているとき などに
別の文字に置き換えておきたい(マスクしたい)ときがあります。
変更前
[LOG] ID : hiro
[LOG] PASS: password
[LOG] exec: hoge.exe --id hiro --pass password
↓ マスクする
変更後
[LOG] ID : hiro
[LOG] PASS: ********
[LOG] exec: hoge.exe --id hiro --pass ********
そんなときは io.Writer
をラップしたものを作るのがお手軽です。
また io.Writer
準拠で作っておくと 汎用性がある・テストしやすい のでオススメです。
io.Writer
は interface
です。
なので以下の 指定されたメソッドを実装してあげる 必要があります。
type Writer interface { Write(p []byte) (n int, err error) }
ということでサクッと作ります。
実装
今回は マスクしたい文字列 を貯めておく実装にしてみました。
mask.go
package main
import (
"io"
"strings"
)
// MaskWriter : マスク用のWriter
type MaskWriter struct {
writer io.Writer // マスクした後に書き込む先
keywords []string // マスク対象の文字列
maskString string // 置き替える文字
}
// NewMaskWriter : マスク用のWriterを作成する
func NewMaskWriter(w io.Writer) MaskWriter {
m := MaskWriter{
writer: w,
keywords: []string{},
maskString: "********",
}
return m
}
// AddMaskWord: マスク対象の文字列を追加する
func (m *MaskWriter) AddMaskWord(word string) {
m.keywords = append(m.keywords, word)
}
// Write : 出力処理
func (m *MaskWriter) Write(p []byte) (n int, err error) {
// 置換用のリストを作成する
replaceList := []string{}
for _, word := range m.keywords {
// list = append(list, マスク対象の文字列, 置き換える文字)
replaceList = append(replaceList, word, m.maskString)
}
replacer := strings.NewReplacer(replaceList...)
// 文字を置換して書き込む
return m.writer.Write([]byte(replacer.Replace(string(p))))
}
上の実装だと、
マスクしたくない場所に同じ文字列があったときにもマスクされてしまうので
そういうケースがある場合はもう少し工夫が必要です。
念のためにテストも。
bytes.Buffer
を使えばオンメモリでテストできます。
mask_test.go
package main
import (
"bytes"
"testing"
)
func TestMaskWriter(t *testing.T) {
tests := []struct {
name string
input string
mask []string
output string
wantErr bool
}{
{
name: `mask (1 word)`,
input: `hogefugapiyo`,
mask: []string{`fuga`},
output: `hoge********piyo`,
wantErr: false,
},
{
name: `mask (2 words)`,
input: `hogefugapiyo`,
mask: []string{`fuga`, `piyo`},
output: `hoge****************`,
wantErr: false,
},
{
name: `mask (multiline)`,
input: "hoge\nfugapiyo",
mask: []string{`fuga`},
output: "hoge\n********piyo",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := &bytes.Buffer{}
ml := NewMaskWriter(got)
for _, w := range tt.mask {
ml.AddMaskWord(w)
}
_, err := ml.Write([]byte(tt.input))
if (err != nil) != tt.wantErr {
t.Errorf("MaskWriter.Write() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got.String() != tt.output {
t.Errorf("MaskWriter.Write() = %v, want %v", got.String(), tt.output)
}
})
}
}
あとは先ほど作ったものを使うだけ。
main.go
package main
import (
"log"
"os"
)
func main() {
id := `hiro`
pass := `password`
// ログファイル
logFile, err := os.Create(`output.log`)
if err != nil {
log.Fatal(err)
}
defer logFile.Close()
// マスク用Writerに変更
maskLogFile := NewMaskWriter(logFile)
maskLogFile.AddMaskWord(pass)
// ログ出力先設定
log.SetOutput(&maskLogFile)
// フォーマット設定
log.SetPrefix(`[LOG] `)
log.SetFlags(0)
// 出力
log.Printf(`ID : %s`, id)
log.Printf(`PASS: %s`, pass)
log.Printf(`exec: hoge.exe --id %s --pass %s`, id, pass)
}
output.log
[LOG] ID : hiro
[LOG] PASS: ********
[LOG] exec: hoge.exe --id hiro --pass ********
できました。
出力をわける
標準出力されるログは マスクしない で ログファイルだけマスクする 場合は、
io.MultiWriter()
を使って まとめたWriter を使えばシンプルなコードにできます。
main.go
package main
import (
"io"
"log"
"os"
)
func main() {
id := `hiro`
pass := `password`
// ログファイル
logFile, err := os.Create(`output.log`)
if err != nil {
log.Fatal(err)
}
defer logFile.Close()
// マスク用Writerに変更
maskLogFile := NewMaskWriter(logFile)
maskLogFile.AddMaskWord(pass)
// 両方に出力する
mw := io.MultiWriter(os.Stdout, &maskLogFile)
// ログ出力先設定
log.SetOutput(mw)
// フォーマット設定
log.SetPrefix(`[LOG] `)
log.SetFlags(0)
// 出力
log.Printf(`ID : %s`, id)
log.Printf(`PASS: %s`, pass)
log.Printf(`exec: hoge.exe --id %s --pass %s`, id, pass)
}
標準出力
[LOG] ID : hiro
[LOG] PASS: password
[LOG] exec: hoge.exe --id hiro --pass password
output.log
[LOG] ID : hiro
[LOG] PASS: ********
[LOG] exec: hoge.exe --id hiro --pass ********
ね、簡単でしょう?(ボブ・ロス)