障害対応の現場で「とりあえず grep」から先に進めず、原因特定が長引いてしまう——そんな経験はありませんか。ログ調査は勘や根性ではなく、時系列→範囲→粒度→要約という思考の順番と、grep・awk・sedで再現できる“手の型”を揃えるだけで一気に速くなります。
本記事は、Apache/NginxのアクセスログやPHP-FPM/WordPress由来のログを題材に、500系エラーのピーク特定/IP・UAの偏り検出/デプロイ境界の差分抽出/WordPressログイン試行の可視化/レスポンスタイム分布の把握という定番5シナリオを、コピーして即走るコマンドと期待される出力例つきで解き明かします。
各シナリオは必ず、目的 → コマンド → 出力の読み方 → 判断・次アクションの順で提示し、チーム内共有にそのまま使えるテンプレも用意しました。
対象は、SSHでサーバーに入りログを読める初中級者。GNU系(Linux)前提で、ローテートや圧縮(.gz)にも配慮します。ワンライナー20本のツール箱と再現メモの型、そして小さなサンプルログを使いながら、今日から“サクサク進む人”になるための最短ルートを手に入れてください。
準備と前提(環境・ログ形式・タイムゾーン)
ログの種類と保存場所(Apache/Nginx/PHP-FPM/WordPress)
まずは「どのログを読むか」を決めます。Webサーバは /var/log/nginx/access.log または /var/log/httpd/access_log が定番で、エラーは error.log(Nginx)や error_log(Apache)に出ます。PHP-FPM は /var/log/php-fpm/www-slow.log のような slowlog を持ち、WordPress は wp-config.php に define('WP_DEBUG_LOG', true); を設定すると wp-content/debug.log に書き出せます。どこにあるか曖昧なら、以下で当たりをつけます。
sudo ls -lh /var/log/nginx/ /var/log/httpd/ /var/log/apache2/ 2>/dev/null
sudo ls -lh /var/log/php*/*slow*.log 2>/dev/null
sudo find /var/www /var/www/html -maxdepth 2 -type f -name 'debug.log' 2>/dev/null
形式別の扱い方(combined/JSON/圧縮 .gz)
アクセスログは「combined」形式が多く、1行がリクエストの要素(IP、時刻、メソッド、URL、ステータス、サイズ、リファラ、UA)で構成されます。例は次のとおりです。
192.0.2.10 - - [09/Sep/2025:12:03:41 +0900] "GET /wp-login.php HTTP/1.1" 200 512 "-" "Mozilla/5.0 ..."
JSON 形式では各フィールドがキー付きになります。
{"time":"2025-09-09T12:03:41+09:00","client":"192.0.2.10","method":"GET","uri":"/wp-login.php","status":200,"request_time":0.123,"ua":"Mozilla/5.0 ..."}
両者で切り方が変わるので、combined では空白や引用符に注意し、ユーザーエージェントは awk -F\" '{print $(NF-1)}'(ダブルクォート区切りの末尾から2番目)で安全に取り出します。圧縮されたローテートログ(.gz)は zcat -f や zgrep を使えばそのままパイプに流せます。
zcat -f /var/log/nginx/access.log* | head
時刻とTZの揃え方(UTC正規化と期間固定)
調査対象期間を決めてから絞るのが速いです。時刻表現は epoch に正規化すると比較が容易になります。GNU date 前提で、Apache の [09/Sep/2025:12:03:41 +0900] から epoch に変換する例です。
# 例: アクセスログ1行から [..] の中身を取り出して epoch に
extract_epoch='match($0, /\[([^]]+)\]/, m) {
cmd="date -d \"" m[1] "\" +%s"
cmd | getline epoch; close(cmd)
print epoch, $0
}'
zcat -f /var/log/nginx/access.log* | awk "$extract_epoch" | head -n 3
開始・終了の境界を shell 変数に固定しておけば、再現性が上がります。
START='2025-09-09 11:00:00 +0900'
END='2025-09-09 13:00:00 +0900'
SE=$(date -d "$START" +%s); EE=$(date -d "$END" +%s)
ログ調査の思考手順(時系列→範囲→粒度→要約)
調査対象期間と境界の決め方(障害時刻・デプロイ基準)
最初に「いつからいつまで」を決めます。障害連絡の最初の報告時刻やアラート発火時刻、直前のデプロイ時刻を起点に、前後 30〜60 分の窓を設定します。窓を固定したら、そこから外のノイズを落としていきます。
フィルタ→抽出→集計→要約の4段階
パイプラインは常に同じ型です。まず時刻やステータスでフィルタし、必要なフィールドのみ抽出し、集計で傾向をつかみ、最後に短い文章で要約します。下の雛形は combined を対象にしたものです。
# 1) 時刻で絞る → 2) フィールド抽出 → 3) 集計 → 4) 整形
zcat -f /var/log/nginx/access.log* \
| awk -v SE="$SE" -v EE="$EE" '
match($0, /\[([^]]+)\]/, m) {
cmd="date -d \"" m[1] "\" +%s"; cmd|getline t; close(cmd)
if (t>=SE && t<=EE) print
}' \
| awk '{print $9}' \
| sort | uniq -c | sort -nr \
| awk '{printf "%7d %s\n", $1, $2}'
再現メモの取り方(目的・コマンド・結果・判断)
作業のたびに短いテンプレで残します。
目的: 12:00±30分の 500 発生のピーク確認と前後比較
コマンド: <貼り付け>
結果(抜粋): 12:05-12:10 に 500 が集中、/wp-json/.. が大半
判断: 直前の投稿同期APIが起点の可能性。次に URL 別分解へ。
次アクション: /wp-json 絞り込みで p90/p99 を把握し DB/外部APIを切り分け
よくある5シナリオ(再現可能なコマンド付き)
500系エラーの発生時間帯特定(Apache/Nginx)
分単位の山をつかむには、時刻を「分」に丸めてカウントするのが速いです。combined の 9 番目フィールドがステータスです。
zcat -f /var/log/nginx/access.log* \
| awk -v SE="$SE" -v EE="$EE" '
$9 ~ /^5[0-9][0-9]$/ && match($0, /\[([^]]+)\]/, m) {
cmd="date -d \"" m[1] "\" +\"%Y-%m-%d %H:%M\""
cmd|getline minute; close(cmd)
print minute
}' \
| sort | uniq -c | sort -nr | head -n 20
ピークの 10 分前後だけをさらに掘るなら、出力された分を再び境界にして抽出します。
FOCUS_START='2025-09-09 12:05:00 +0900'
FOCUS_END='2025-09-09 12:15:00 +0900'
FS=$(date -d "$FOCUS_START" +%s); FE=$(date -d "$FOCUS_END" +%s)
zcat -f /var/log/nginx/access.log* \
| awk -v SE="$FS" -v EE="$FE" '
match($0, /\[([^]]+)\]/, m) {
cmd="date -d \"" m[1] "\" +%s"; cmd|getline t; close(cmd)
if (t>=SE && t<=EE && $9 ~ /^5/) print
}'
IP/UAの偏り検出(集計→上位N)
IP アドレスの偏りは 1 フィールドで済みます。割合が必要なら総数を別途取るか、ファイルに落としてから計算します。
# IP の上位20
zcat -f /var/log/nginx/access.log* | awk '{print $1}' \
| sort | uniq -c | sort -nr | head -n 20
# UA の上位20(" で分割し NF-1 を取る)
zcat -f /var/log/nginx/access.log* | awk -F\" '{print $(NF-1)}' \
| sort | uniq -c | sort -nr | head -n 20
上位に未知のクローラや同一 ASN の塊が見えるなら、国判定や逆引きで補強し、レート制限や WAF へつなぎます(ここではコマンドは最小限に留めます)。
直近デプロイ以降の差分抽出(timestamp基準)
デプロイ境界の前後で「新規に現れた URL」を拾うと、原因候補が見えます。combined の URL は 7 番目フィールドです。
DEPLOY='2025-09-09 12:00:00 +0900'
DT=$(date -d "$DEPLOY" +%s)
# 前: DT より前、後: DT 以降
zcat -f /var/log/nginx/access.log* \
| awk -v DT="$DT" '
match($0, /\[([^]]+)\]/, m) {
cmd="date -d \"" m[1] "\" +%s"; cmd|getline t; close(cmd)
url=$7
if (t<DT) print url > "urls.before"
else print url > "urls.after"
}' && \
sort -u urls.before -o urls.before && sort -u urls.after -o urls.after && \
comm -13 urls.before urls.after | head -n 50
差分 URL 群に対して 4xx/5xx の比率を重ねると、障害に寄与している変更点が濃くなります。
grep -Ff <(comm -13 urls.before urls.after) /var/log/nginx/access.log \
| awk '{cnt[$7" "int($9/100)]++} END{for(k in cnt) printf "%sxx %s %d\n", substr(k, index(k," ")+1,1), substr(k,1,index(k," ")-1), cnt[k]}'
WordPressログイン試行の可視化(失敗→成功の遷移)
WordPress のログイン成功は多くの環境で POST /wp-login.php が 302 を返しダッシュボードへリダイレクトされます。失敗は 200 が多いです。IP ごとの失敗→成功の遷移を見ます。
# IPごとに POST /wp-login.php の 200/302 件数を出す
zcat -f /var/log/nginx/access.log* \
| awk '$6 ~ /POST/ && $7 ~ /\/wp-login\.php/ {print $1, $9}' \
| awk '{k=$1; if($2==302) s[k]++; else if($2==200) f[k]++} END{
printf "%-15s %8s %8s %8s\n", "IP","fail(200)","succ(302)","total";
for(k in f){printf "%-15s %8d %8d %8d\n", k, f[k], s[k]+0, f[k]+s[k]+0}
for(k in s) if(!(k in f)) printf "%-15s %8d %8d %8d\n", k, 0, s[k], s[k]
}'
成功が 1 件、失敗が多数の IP は総当たりの可能性があります。時間軸での濃淡を見たい場合は「分に丸めたヒートマップ用 CSV」を吐きます。
zcat -f /var/log/nginx/access.log* \
| awk '$6 ~ /POST/ && $7 ~ /\/wp-login\.php/ && match($0, /\[([^]]+)\]/, m) {
cmd="date -d \"" m[1] "\" +\"%Y-%m-%d %H:%M\""; cmd|getline minute; close(cmd)
print minute, $1, $9
}' | awk '{k=$1","$2; if($3==302) s[k]++; else if($3==200) f[k]++} END{
print "minute,ip,fail_200,succ_302";
for(k in f){printf "%s,%d,%d\n", k, f[k], (s[k]+0)}
for(k in s) if(!(k in f)) printf "%s,%d,%d\n", k, 0, s[k]
}' > wp-login_heatmap.csv
バックエンドの遅延疑い(レスポンスタイム分布)
Nginx のログフォーマットに $request_time を入れていれば、p50/p90/p99 をすぐに出せます。
# 最終フィールドが request_time であると仮定
zcat -f /var/log/nginx/access.log* \
| awk '{print $NF}' \
| awk '{a[++n]=$1} END{
asort(a); p50=a[int(n*0.50)]; p90=a[int(n*0.90)]; p99=a[int(n*0.99)]
printf "count=%d p50=%.3f p90=%.3f p99=%.3f\n", n, p50, p90, p99
}'
URL 別の平均や上位遅延 URL も合わせて見ます。request line は "GET /path HTTP/1.1" のように引用符で囲まれているので、ダブルクォートを区切りに取ります。
zcat -f /var/log/nginx/access.log* \
| awk -F\" '{split($2, r, " "); url=r[2]; rt=$NF; print url, rt}' \
| awk '{sum[$1]+=$2; cnt[$1]++} END{
for (u in sum) printf "%.3f %7d %s\n", sum[u]/cnt[u], cnt[u], u
}' | sort -nr | head -n 30
調査を速くする基礎テク
大容量ログを速く読む(LC_ALL=C/grep -F/parallel)
マルチバイト比較は遅いので、可能なら一時的に LC_ALL=C にします。固定文字列には正規表現ではなく grep -F が効きます。複数ファイルを並列に処理したいなら GNU parallel が手軽です。
export LC_ALL=C
grep -F 'wp-login.php' /var/log/nginx/access.log
ls /var/log/nginx/access.log* | parallel 'zcat -f {} | awk "{print \$9}" | sort | uniq -c'
圧縮ログ対応(zgrep/zcatの安全な使い方)
zcat -f は生ファイルと .gz の両方を透過的に開けます。cat *.gz | zcat -f のような二重展開は不要です。パイプの先頭に zcat -f を置く癖にします。
zcat -f /var/log/nginx/access.log* | wc -l
Top-N・ヒストグラム・p90/p99の出し方
ヒストグラムはバケット幅を決めて四捨五入すると簡単です。レスポンスタイムを 100ms 幅でバケる例です。
zcat -f /var/log/nginx/access.log* \
| awk '{rt=$NF; b=int(rt*10)/10; cnt[b]++} END{
for(b in cnt) printf "%.1f - %.1f %d\n", b, b+0.1, cnt[b]
}' | sort -n
ツール箱(ワンライナー20/スニペット配布)
フィルタ/抽出(ステータス・期間・URL・メソッド)
ステータス 5xx と /wp-json を同時に絞る例です。
zcat -f /var/log/nginx/access.log* \
| awk -v SE="$SE" -v EE="$EE" '
match($0, /\[([^]]+)\]/, m) {
cmd="date -d \"" m[1] "\" +%s"; cmd|getline t; close(cmd)
if (t<SE || t>EE) next
if ($9 ~ /^5/ && $7 ~ /^\/wp-json\//) print
}'
集計/ランキング(IP・UA・URL)
URL 別の 5xx 件数ランキングは次のとおりです。
zcat -f /var/log/nginx/access.log* \
| awk '$9 ~ /^5/ {print $7}' \
| sort | uniq -c | sort -nr | head -n 50
前後比較・差分(デプロイ境界/ファイル比較)
前後の 500 比率を数字で比較すると、影響度の説明が楽になります。
awk_500_ratio='
{total++; if($9 ~ /^5/) err++}
END{printf "err=%d total=%d ratio=%.2f%%\n", err+0, total+0, (err/total*100)}'
zcat -f /var/log/nginx/access.log* \
| awk -v DT="$DT" '
match($0, /\[([^]]+)\]/, m) { cmd="date -d \"" m[1] "\" +%s"; cmd|getline t; close(cmd)
if (t<DT) print > "pre.log"; else print > "post.log"
}'
awk "$awk_500_ratio" pre.log
awk "$awk_500_ratio" post.log
整形・出力(column/CSV/可読フォーマット)
最終的な貼り付けは column -t で整えると伝わります。
zcat -f /var/log/nginx/access.log* \
| awk '{print $1,$7,$9}' \
| sort | uniq -c | sort -nr | head -n 20 \
| awk '{printf "%7d %-15s %-40s %s\n", $1, $2, $3, $4}' | column -t
WordPress特化の観点
wp-login.php周辺の不正試行検知(レート制限の前提)
ログから「どの IP が何回失敗して、最終的に成功したか」を可視化してから、limit_req や 2FA 導入の判断につなげます。成功時 302、失敗時 200 の傾向を前提に置き、日次で傾向を見るとクローラや攻撃波が見えます。
# 日付別に失敗/成功 件数を集計
zcat -f /var/log/nginx/access.log* \
| awk '$7 ~ /wp-login\.php/ && match($0, /\[([^]]+)\]/, m) {
cmd="date -d \"" m[1] "\" +\"%Y-%m-%d\""; cmd|getline d; close(cmd)
if($9==302) s[d]++; else if($9==200) f[d]++
} END{for(d in f) printf "%s,fail=%d,succ=%d\n", d, f[d], (s[d]+0)}' \
| sort
プラグイン/テーマ更新後のエラー増分の把握
更新時刻を境に comm で URL の新旧差分を出し、/wp-json や /wp-admin/admin-ajax.php のエラー比率を比較します。結果が偏っていれば、変更対象のプラグイン名をレポートに明記します。
PHP-FPM slowlogとアクセスログの突合
slowlog の「該当スクリプトパス」とアクセスログの URL を突き合わせ、遅延の多い時間帯と URL を重ねます。
# slowlog からスクリプトパスを拾い、URL との対応をざっくり見る
grep -Eo 'script_filename = .*' /var/log/php-fpm/*slow*.log | awk -F' = ' '{print $2}' \
| sort -u > slow_scripts.txt
zcat -f /var/log/nginx/access.log* \
| awk -F\" '{split($2, r, " "); print r[2]}' | sort -u > urls.txt
comm -12 <(sed 's|/var/www/html||' slow_scripts.txt | sort) <(sort urls.txt) | head -n 50
アウトプットの型(共有しやすい報告書テンプレ)
200字サマリ+根拠出力(抜粋)
最初に 200 字の要約で「いつ、何が、どれくらい」起きたかを書くと、意思決定が速くなります。根拠はすぐ下に 10〜20 行の抜粋または CSV を貼り、その出し方(コマンド)を添えます。
Slack/Issue貼り付け用テンプレ
日付・担当・境界・判断・次アクションを固定フォーマットで残します。
[インシデント報告] 2025-09-09 12:00〜13:00 担当: @you
現象: 12:05〜12:10 に 500 が 240 件、/wp-json/post-sync に集中
根拠: 分別集計・URL別 5xx ランキング(抜粋を添付)
判断: 12:00 デプロイの差分 URL に一致。外部API タイムアウト疑い
次アクション: API タイムアウト 3s→6s、リトライ追加、DB インデックス確認
IP/UAマスキングと個人情報配慮
共有前に sed -E 's/([0-9]+)\.([0-9]+)\.([0-9]+)\.[0-9]+/\1.\2.\3.xxx/g' のような置換で末尾オクテットを伏せ、UA の一部を省略します。原本はアクセス権のあるストレージにのみ保存します。
演習セットとアンチパターン
サンプルログの取得と実行手順
手元で再現できるミニログを用意します。以下をファイルに保存して試してください。
cat > sample_access.log <<'LOG'
192.0.2.1 - - [09/Sep/2025:12:05:01 +0900] "GET /wp-login.php HTTP/1.1" 200 512 "-" "Mozilla/5.0 A"
192.0.2.1 - - [09/Sep/2025:12:05:12 +0900] "POST /wp-login.php HTTP/1.1" 200 620 "-" "Mozilla/5.0 A"
192.0.2.1 - - [09/Sep/2025:12:05:30 +0900] "POST /wp-login.php HTTP/1.1" 302 0 "-" "Mozilla/5.0 A"
198.51.100.9 - - [09/Sep/2025:12:06:02 +0900] "GET /wp-json/post-sync HTTP/1.1" 500 0 "-" "curl/8.0"
198.51.100.9 - - [09/Sep/2025:12:06:06 +0900] "GET /wp-json/post-sync HTTP/1.1" 500 0 "-" "curl/8.0"
203.0.113.77 - - [09/Sep/2025:12:06:20 +0900] "GET / HTTP/1.1" 200 1024 "-" "Mozilla/5.0 B"
LOG
分別 500 集計や wp-login の失敗→成功判定を先ほどのコマンドで実行し、出力が期待通りかを確認します。
SE=$(date -d '2025-09-09 12:00:00 +0900' +%s)
EE=$(date -d '2025-09-09 12:10:00 +0900' +%s)
awk -v SE="$SE" -v EE="$EE" '
$9 ~ /^5/ && match($0, /\[([^]]+)\]/, m) {
cmd="date -d \"" m[1] "\" +\"%Y-%m-%d %H:%M\""; cmd|getline minute; close(cmd)
print minute
}' sample_access.log | sort | uniq -c
合格基準と目安時間(模擬インシデント)
5 つのシナリオを 1 本ずつ 15 分以内で実行し、200 字サマリを添えて共有できれば合格です。練習を重ねれば、分単位のピーク発見から URL 特定、遅延分布の要約までを 30 分で回せるようになります。
ありがちな落とし穴(曖昧検索/フィールド崩れ/TZ混在/.gz見落とし)
grep 500 だけだとサイズや URL に含まれる「500」まで拾ってしまいます。必ずステータスのフィールドで判定してください。combined の抽出では空白を含む UA がフィールド位置をずらします。-F\" の分割を基本にすると崩れません。タイムゾーンが混在したまま比較すると前後判定を誤ります。圧縮ログを忘れて件数が合わないのも定番です。
参考・参照リンク
- Nginx
log_format公式ドキュメント - Apache HTTP Server Log Files(公式)
- GNU Awk User’s Guide
- GNU sed Manual
- GNU Coreutils
dateの書式 - PHP-FPM Slowlog(公式)
- WordPress Codex: デバッグ(WP_DEBUG_LOG)
まとめ
ログ調査の目的は、テキストの山から“都合の良い解釈”を除外し、事実に基づく共通理解を素早く作ることにあります。
いつ・どこで・どれくらいの影響が起きたのかを定量化し、仮説ではなく根拠で語ることで、復旧の意思決定が速くなり、説明責任も果たせます。これは単なるトラブル対応ではなく、ユーザー体験と事業の信頼を守る営みです。
もう一つの価値は、記録された事実を学びに変える循環をつくることです。
原因と影響範囲が明瞭になれば、再発防止の優先度が定まり、設計・運用・開発の各所に改善が波及します。ログは、担当者が変わっても消えない組織の記憶であり、引き継ぎや振り返り、コンプライアンスの土台にもなります。
だからこそ私たちはログを読むのではなく、信頼を積み上げるために記録と向き合う――それがこの学習の到達点です。
この記事で身についたスキル(実務で使える型)
- 思考の順番:時系列 → 範囲 → 粒度 → 要約を反射で回せる
- 時刻の扱い:TZ統一・epoch化・「分」丸めでピーク比較が即できる
- パイプ設計:
grep/awk/sedでフィルタ→抽出→集計→整形を一筆書き - 典型課題の即解:5xxピーク特定、IP/UAの偏り、デプロイ境界の差分、WPログイン遷移、レスポンスタイム分布
- 大容量ログ対応:
zcat -f透過読取、LC_ALL=C、grep -F、必要に応じてparallel - 共有の型:200字サマリ+根拠出力、再現メモ(目的→コマンド→読み方→次アクション)
よくある落とし穴(再確認)
grep 500の曖昧一致で誤カウント(ステータス列で判定)- UAの空白でフィールド崩れ(
-F" "ではなく-F\"や分割戦略) - TZ混在のまま比較して前後を取り違え
.gzの見落としで件数が合わない

