ちょっと話題の記事

[Bash][Tips]ログファイルを探索するために知っておきたいシェルコマンド [初心者向け]

2016.08.10

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。こむろ@札幌です。昨日・今日はとても涼しくて過ごしやすい気候です。

涼し気な写真を2枚ほど。

IMG_20160716_153917

オロロンラインの途中にある白銀の滝

IMG_20160806_181603

夕暮れ時の豊平川

はじめに

皆さんはログファイルの解析、目的の項目の抽出などはどのように行っているでしょうか?自分は普段Pythonを使い *1、テキスト処理などを行っていますが、シェルのコマンドの方が更にフットワーク軽く、文字列処理に特化した場合、かなり強力な機能があるよ、とのことなので今回はシェルを使ってログファイルのテキスト処理を実行します。

この記事の対象者は、以下のような意識低めな人たちです。 *2

  • iTerm2などのコンソール画面はそこそこ見慣れてるし
  • コマンドはそこそこ打つけど
  • 自分でシェルのコマンドを駆使してテキスト処理をしたことがないし
  • 大体ググッてコピペすればどうにかなるでしょ的なノリで生きて来た

そのため、パイプ(|)等のコマンドの要素一つ一つについては、知ってる前提ですっ飛ばします。

ちなみに自分もコマンドやシェルスクリプトに関しては、できるだけ自分で書くのを避けてきた人間なので、基本コマンド程度しか知りません(lsやgrep, findなど)

環境について

基本的に全てAmazon Linux上で動作確認しています。

  • Amazon Linux AMI release 2016.03

テキストファイルを検索する

障害の調査を行う際には、どこかの段階でログファイルそのものに深く潜って原因を探しに行くことがあるかと思います。自分はログファイルのテキストを確認する時には less を利用してます。

$ less /path/to/current_log.log
  • space でページ送り
  • : が表示されている状態で q を入力すると終了

単語を検索

less でテキストを開いた状態で画面左下に : が表示されています。

/request_id

その状態で / を入力しその後調べたい単語を入力します。 Enter で検索。

  • n で次のヒットした行までジャンプ
  • shift + n で前のヒットした行までジャンプ

tailの代わり

tail -f と同じく更新を監視するためには以下のコマンド。

$ less +F /path/to/current_log.log

もしくは

$ less /path/to/current_log.log
  • ドキュメントを開いた後に shift + f でもOK。

更新を追随して画面を更新してくれます。

  • ctrl + c で更新監視を停止。更新監視を停止していれば単語検索などは可能です。

大体これくらいしか使っていません。

目的のログの行だけ抽出

grep を使っています。

$ grep 'request_id:' /path/to/current_log.log

gzのファイルを検索したい

ログローテーション設定をしていると、アーカイブされたファイルは .gz 等で圧縮されてる事も多いかと思います。 z 付きのコマンドは、 gz ファイルをそのまま操作することができるので便利でした。

圧縮ファイルを解凍せずに閲覧

zless を利用します。

$ zless /path/to/archive_log.gz

使い方は less と同じです。アーカイブなので多分 +F はないと思います(未確認)

圧縮ファイルから目的の行を抽出

予想通りやっぱりありました。grepのgzip対応版 zgrep を利用します。

$ zgrep 'request_id:' /path/to/arcive_log.gz

圧縮されたファイルの中から、条件に合致する行を表示してくれます。

特定の項目のみを抽出する

こんなログが出力されるとしましょう。Log4jを利用したログの一例です。こちらもアーカイブされてるものとします。

2016-07-06 01:53:01.686 INFO 5818 --- [http-nio-8080-exec-8] jp.classmethod.sample.Hoge          : requestId:xxxxx-2712-eeee-a667-c7b4f0d3d8e9 hashId:seeecreeeeet==== INFO : call sample method GET statusCode:200

前提条件: ログのトレーサビリティを向上させるために、1リクエストに対して必ず一意なrequestId を発行しています。リクエストの中で処理しているログは全てこの統一した requestId に紐付いているため、障害時にはこの値をキーを探すことで原因特定が容易になります。自分の関わっている案件はこの requestId のおかげで調査時にとても効率よく作業ができています。

さて、これらのログから requestId のみを抽出したリストを作成したい場合どうしましょう。まずはやることをまとめてみます。

  1. テキストを読み込む
  2. requestId:xxxxx のみを取り出す
  3. requestId: を消す
  4. 値なしの行もあるため、空行を削除する
  5. 1リクエストでいくつも処理があるため、同じ requestId がいくつも出るため重複をなくす

1. gzファイルを読めるようにする

zless を使います。

$ zless /path/to/archive_log.gz > test.txt

2. 目的のものだけ取り出す

awk を使います。awkはテキスト処理を行うためのコマンドです。

$ awk '{ print $9 }' test.txt > test2.txt

空白で全ての項目が区切られているため、 $9requestId:xxxx にあたります。念の為確認してみます。

index 項目
1 2016-07-06
2 01:53:01.686
3. INFO
4. 5818
5. ---
6. [http-nio-8080-exec-8]
7. jp.classmethod.sample.Hoge
8. :
9. requestId:xxxxx-2712-eeee-a667-c7b4f0d3d8e9
10. hashId:seeecreeeeet====
... ....

3. requestId:を削除

sed を利用します。正規表現を使うことができるため、置換処理などが出来るようです。

$ sed 's/requestId://g' test2.txt > test3.txt

4. 空行を削除

空行を削除するにも sed を利用します。

$ sed '/^$/d' test3.txt > test4.txt

5. 重複を排除

最後に重複をなくします。

$ uniq test4.txt > final.txt

uniq はソートされている前提という記事を見かけました。こちらの指摘通り、 sort -u の方が正しい気がします。今回実際に案件で利用したものは、たまたま重複は存在しないデータだったため、 uniq は指定していませんでした。

全部つなぐ

中間ファイルにいちいち出力していられないので、全てパイプでつなぎます。

$ zless /path/to/archive_log.gz |  awk '{ print $9 }' | sed 's/requestId://g' | sed '/^$/d' | uniq > final_answer.txt

awk には zawk にあたるものがなかったため、一旦 zless でgzファイル内のテキストを読めるようにしました。gzファイルでない場合は不要かもしれません。

テキストファイルの中のJSONを操作

弊社にもたくさんエントリがありますが、jqを利用すると楽ちんでした。以下の様なJSONが記述されているログファイルを想定します。

{ "timestamp": "2016-08-10T14:37:46.263+0900", "request_id": "3e76b784-0e3c-41b9-8176-3ecbb697b711", "hash_id": "123456789==", "event_type": "Update", "details": {  "nickname": "Deleted" }}
{ "timestamp": "2016-08-10T14:37:46.987+0900", "request_id": "2cf8fedc-5fef-4e53-8bb2-0bdd28e9e377", "hash_id": "123456789==", "event_type": "Delete", "details": {}}
{ "timestamp": "2016-08-10T14:37:47.012+0900", "request_id": "e50b02d3-0434-4156-b951-5435c4c735fd", "hash_id": "123456789==", "event_type": "Delete", "details": {}}
{ "timestamp": "2016-08-10T14:37:48.012+0900", "request_id": "b21858cf-5f6c-4a3c-b581-93e21b43225a", "hash_id": "123456789==", "event_type": "Delete", "details": {}}
{ "timestamp": "2016-08-10T14:37:48.012+0900", "request_id": "b21858cf-5f6c-4a3c-b581-93e21b43225a", "hash_id": "123456789==", "event_type": "Delete", "details": {}}

1行に付き1つのJSON文字列が記載されているログです。このファイルから、 request_id のリストを作成します。

$ jq '.request_id' json_log.log

これを実行すると、 request_id の値のみを抽出することができます。

"3e76b784-0e3c-41b9-8176-3ecbb697b711"
"2cf8fedc-5fef-4e53-8bb2-0bdd28e9e377"
"e50b02d3-0434-4156-b951-5435c4c735fd"
"b21858cf-5f6c-4a3c-b581-93e21b43225a"
"b21858cf-5f6c-4a3c-b581-93e21b43225a"

重複を削除してソート

重複を削除した上にソートしたい場合は以下で良いようです。

$ jq '.request_id' json_log.log | sort -u

実行するとソートと重複削除も実行されます。

"2cf8fedc-5fef-4e53-8bb2-0bdd28e9e377"
"3e76b784-0e3c-41b9-8176-3ecbb697b711"
"b21858cf-5f6c-4a3c-b581-93e21b43225a"
"e50b02d3-0434-4156-b951-5435c4c735fd"

重複していた "b21858cf-5f6c-4a3c-b581-93e21b43225a" が消えました。

「jqのオプションでソートも重複削除も指定出来るよ」という指摘を受けました。ほんとだ!!jqすごい!

アーカイブされたgzファイルの中からrequestIdのリストを作成する

$ zless archive-20160724.gz | jq '.request_id' > id-list.txt

zless で一度展開し jq へ渡します。取り出した request_id の値を id-list.txt に保存します。

RequestIdのリストから合致するログのみ抽出

先ほど作成したRequestIdのリストに合致するもののみ、ログファイルから抽出したい場合どうしましょうか。 lesszless でテキストを開いて検索しながら確認していくのも一つの方法ですが、こちらもコマンドで処理してみます。

xargsの利用

やりたいことは以下です。

  1. requestIdのリストファイルから1つずつ値を取り出す
  2. requestIdに合致するテキストを全て出力
  3. 出力結果を保存

UNIXシェルスクリプトコマンドブック こちらの第二版を参照していたのですが、 xargs が載っていなかったため、泣く泣く自分で調べました。

xargsは標準入力を引数に受け取り、指定したコマンドを実行できるコマンドのようです。様々なオプションがありますが、今回は -I を利用します。

-I を調べてみると以下の様な使い方をすることが多いようです。

xargs -I{} <command> {}

{} これはプレースホルダーを示し、入力された引数の別名を定義するもののようです。そのため {} である必要もなく param など適当な文字列でも良いようです。ただ、 {} は滅多に衝突する名前ではないため、多用されているようです。そのため、以下の様に解釈できます。

xargs -Iパラメータ command パラメータ

command にあたるものは何でも良いようです。今回は文字列の検索をgzファイルから実行したいので以下のように記述します。

xargs -I{} zgrep {} archive-20160724.gz

全ての手順をまとめると以下のように書けました。

$ less id-list.txt | xargs -I{} zgrep {} archive-20160724.gz > filtered_log.log

こちらを実行すると、対象のrequest_idのみを抽出したログファイルが作成することができます。

まとめ

はじめてきちんとシェルのコマンドを使いました。まだまだ使っていないオプションなどもありますし、シェルスクリプトとしてもう少し色々と組み合わせた方法もあるようなので、今後も精進していこうかと思います。

ここ最近は案件で結構なサイズのログファイルを探索することも増えてきましたし、さすがに目grepは目が疲れる上に途中からゲシュタルト崩壊を起こしかけるし、となりそうなので、必要な情報のみフィルタすることは大事です。Linux系のOSさえ入っていればパッと試すことができるのもの強いですね。

参照

脚注

  1. 周囲の人に教えてもらいながら。
  2. 世のシェルスクリプトなどをバリバリ使いこなしてんぜという方には、レベルが低すぎてあくびが出る可能性があるためご注意ください。