Go ログの一部をマスクする

Go言語でログの一部をマスクしたい。

例えば ログの一部にパスワードが書かれているとき などに
別の文字に置き換えておきたい（マスクしたい）ときがあります。

```text:変更前
[LOG] ID  : hiro
[LOG] PASS: password
[LOG] exec: hoge.exe --id hiro --pass password
```

↓ マスクする

```text:変更後
[LOG] ID  : hiro
[LOG] PASS: ********
[LOG] exec: hoge.exe --id hiro --pass ********
```

そんなときは `io.Writer` をラップしたものを作るのがお手軽です。
また `io.Writer` 準拠で作っておくと 汎用性がある・テストしやすい のでオススメです。

`io.Writer` は `interface` です。
なので以下の 指定されたメソッドを実装してあげる 必要があります。

> ```go
> type Writer interface {
>     Write(p []byte) (n int, err error)
> }
> ```
> https://golang.org/pkg/io/#Writer

ということでサクッと作ります。

## 実装

今回は マスクしたい文字列 を貯めておく実装にしてみました。

```go: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` を使えばオンメモリでテストできます。

```go: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)
			}
		})
	}
}

```

あとは先ほど作ったものを使うだけ。

```go: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)
}

```

```text:output.log
[LOG] ID  : hiro
[LOG] PASS: ********
[LOG] exec: hoge.exe --id hiro --pass ********
```

できました。

## 出力をわける

標準出力されるログは マスク***しない*** で ログファイルだけマスク***する*** 場合は、
`io.MultiWriter()` を使って まとめたWriter を使えばシンプルなコードにできます。

```go: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)
}

```

```text:標準出力
[LOG] ID  : hiro
[LOG] PASS: password
[LOG] exec: hoge.exe --id hiro --pass password
```

```text:output.log
[LOG] ID  : hiro
[LOG] PASS: ********
[LOG] exec: hoge.exe --id hiro --pass ********
```

ね、簡単でしょう？（ボブ・ロス）