check-logはMackerelの公式プラグイン集のうちのログ監視を行えるツールです。例えばアプリケーションログに特定の文字列、例えばCRITみたいなのが出たらコマンドを失敗させるみたいな使い方ができるツールです。
Usage: main [OPTIONS] Application Options: -f, --file=FILE Path to log file -p, --pattern=PAT Pattern to search for. If specified multiple, they will be treated together with the AND operator --suppress-pattern Suppress pattern display -E, --exclude=PAT Pattern to exclude from matching. If specified multiple, they will be treated together with the AND operator -w, --warning-over= Trigger a warning if matched lines is over a number -c, --critical-over= Trigger a critical if matched lines is over a number --warning-level=N Warning level if pattern has a group --critical-level=N Critical level if pattern has a group -r, --return Return matched line --search-in-directory=DIR Specify the directory of files to be detected -F, --file-pattern=FILE Check a pattern of files, instead of one file -i, --icase Run a case insensitive match -s, --state-dir=DIR Dir to keep state files under --no-state Don't use state file and read whole logs --encoding= Encoding of log file --missing=(CRITICAL|WARNING|OK|UNKNOWN) Exit status when log files missing (default: UNKNOWN) --check-first Check the log on the first run Help Options: -h, --help Show this help message
読み始める前にどんな実装なんだろうを想像してたのですがinotifyを使って何かしらしているんだろう思ってたのですがどうやら違うようでした。
ざっくり流れ
ざっくり流れは以下のようになっていました。
1 オプションの解析 2 ステートファイルのロード 3 ログファイルの探索、inodeの取得 4 ステートファイルにあるskipバイト数分seek 5 seekした以降のファイルの内容を指定された文字列パターンと一致するかどうかをcheck
ステートファイルにどこまで読んだのかを記録、inodeもそこに書いてあるのでローテートしても追うことが可能というような仕組みになっているようでした。tail -f的なものとは違ったようでした。
ログローテートの対応
fluentdに似ている実装ということが分かり満足してたのですがログローテートの対応はどうなっているんだって思って調べてみました。Mackerelで例えば1分間に1回の監視設定だと12:00では監視上問題がなくても12:01になるまでの時間にエラーが多発してそのタイミングでログローテートされてしまってはエラーを見逃してしまいますがどうなっているのか調べました。結論としてはそのような事象の対応も実装されていました。
func openOldFile(f string, state *state) (*os.File, error) { fi, err := os.Stat(f) // ローテート後のファイル if err != nil { return nil, err } inode := detectInode(fi) // ローテート後のファイルのinodeを取得 if state.Inode > 0 && state.Inode != inode { // 引数で渡されたファイルのinodeとステートファイルのinodeが違う場合 if oldFile, err := findFileByInode(state.Inode, filepath.Dir(f)); err == nil { // 古いログファイルを探す oldf, err := os.Open(oldFile) if err != nil { return nil, err } oldfi, _ := oldf.Stat() // 古いログファイルの情報を取得する if oldfi.Size() > state.SkipBytes { // 古いログファイルのサイズとskipすべきバイト数を比較 oldf.Seek(state.SkipBytes, io.SeekStart) // ここにはいるのは最後に読み込んだタイミングの後にファイルが追記されていた場合 return oldf, nil // seekさせた状態のファイルディスクリプたを呼び出し元に戻す } } else if err != errFileNotFoundByInode { return nil, err } // 存在しない場合は何もしない(ディレクトリが別の場合も何もしない模様) } return nil, nil }
感想
先入観だけでいうとfsイベント監視なんだろうなと思ってたのですが全然違くてとても面白かったです。