2-6. ログ運用 - syslog / journald / logrotate / 構造化ログ
所要時間: 50-70分(がっつりなら3セッション分) ゴール: ログの役割を整理し、syslog / journald / logrotate を使い分け、本番品質の構造化ログを設計できる コミット内容: nginx ログの集計スクリプトと自作 logrotate 設定を
~/learn/linux/day206/に保存
この章が終わるとできること
- ログの 4つの役割(デバッグ / 監査 / 性能 / セキュリティ)を区別できる
- syslog の facility / severity、
/var/log/auth.logの見方が分かる journalctlを-u--since-p-o json | jqで自在に絞れる- logrotate の
create/copytruncateを使い分けられる - 構造化ログ(JSON) を出力するアプリ設計ができる
- ログに書いてはいけない情報を即答できる
Day 11-12 とのつながり
- Day 11 (systemd) の
journalctl -u myappがここで本格運用される - Day 12 (cron) で書いた
>> /var/log/myapp.log 2>&1の出力先を、logrotate で管理する - Day 4 (grep / awk) と Day 5 (パイプ) の集計技能が、ログ調査でフル活用される
これができると何が嬉しいか
- 本番障害で「最初に見るログ」が即座に決まる ─ 迷子にならない
- ディスク満杯事故(ログで溢れる)を 設計で予防 できる
- 「ログにパスワード混入」のセキュリティ事故を未然に防げる
大前提: ログは「動いてるか確認するため」ではない
新人エンジニアは「ログ=デバッグ用の console.log」と捉えがち。それは半分しか合っていない。本番運用におけるログには 4つの異なる役割 がある:
- デバッグ: バグを再現するための痕跡
- 監査 (audit): 「誰が」「いつ」「何を」したかの法的・規制上の記録
- 性能 (performance): レイテンシ、リソース使用量の長期トレンド
- セキュリティ: 不正アクセス検知、侵入後の追跡
これらは目的が違うため、ログレベルもフォーマットも保管期間も異なる。「全部 print してファイルに書いとけば良い」は本番では通用しない。
さらに、ログには 負の側面 がある:
- ディスクを食いつぶす: 1日100GBのアクセスログで本番が停止
- 秘密情報を漏らす: ログにパスワードや個人情報が混入
- 性能劣化: 同期書き込みが処理を遅らせる
- コスト: クラウドのログ取り込み料金が月数百万円
本気のバックエンドエンジニアは「ログを書く」だけでなく 「何を、どう書くか、いつ消すか」を設計 できなければならない。
セッション①: Linux のログ機構の全体像(25-30分)
0. 録画スタート
mkdir -p ~/log ~/learn/linux/day206
cd ~/learn/linux/day206
script ~/log/linux_day206.log1. /var/log を覗く
Linux サーバーのログは 慣習的に /var/log/ 配下に集約 されています。最初のサーバー調査ではここを開いて全体像を把握するのが定石。
- 何のディレクトリか: ファイルシステム階層標準 (FHS) で定義された「システム全体のログ置き場」
- いつ見るか: サーバーに初めて SSH した時の現状把握、「何かおかしい」と感じた時の初動、ディスク使用量の調査
- 解決する具体的な問題: 「ログの場所がサービスごとに散らばっていて見落とす」を、まず
/var/log/を一覧する習慣で防ぐ。auth.logで不正アクセス、syslogで全般、nginx/で Web 系、と用途別にすぐ見るべき場所が分かるようになります
# 主要なログ
sudo ls -lah /var/log/
# よく見るログファイル
sudo tail /var/log/syslog # Debian/Ubuntu の汎用ログ
sudo tail /var/log/messages # RHEL/CentOS の汎用ログ
sudo tail /var/log/auth.log # 認証関連(SSH、sudo、su)
sudo tail /var/log/kern.log # カーネルログ
sudo ls /var/log/nginx/ # nginx の access.log と error.log
sudo ls /var/log/apt/ # パッケージ管理の履歴
/var/logの慣習Linux では「ログは /var/log/ 配下に置く」のがファイルシステム階層標準 (FHS) で決まっている。
/varは variable data(変化するデータ)の意味。重要なログファイル:
パス 内容 見るタイミング /var/log/syslog(Debian系)汎用システムログ システム全体の異常調査 /var/log/messages(RHEL系)汎用システムログ 同上 /var/log/auth.logsudo, ssh, login, su 侵入調査、不正アクセス検知 /var/log/kern.logカーネルメッセージ ハード障害、ドライバ、OOM Killer /var/log/dpkg.logパッケージ操作 「いつ何をインストールしたか」 /var/log/nginx/nginx の access/error Web アクセス調査 /var/log/journal/journald のバイナリログ systemctl/journalctl 経由で読む 多くの場合 root しか読めない (
-rw-r-----)。プログラムから読みたい場合はadmグループに参加するか、syslogグループへの追加が必要。
2. syslog プロトコルの世界
syslog はプロトコル、ファイル名ではない
「syslog」という言葉は3つの意味で使われる:
- プロトコル (RFC 3164 / 5424):プロセスが OS のログ集約機構にメッセージを送る規格
- デーモン:そのプロトコルを受けて処理するデーモン(
rsyslogdやsyslog-ng)- ファイル名:
/var/log/syslogという具体的なファイル仕組み:
[アプリ A] -- syslog() --> [rsyslogd] --> /var/log/syslog [アプリ B] -- syslog() --> [rsyslogd] --> /var/log/auth.logアプリは
syslog(3)ライブラリ関数を呼ぶだけ。どこに書くかは rsyslogd の設定が決める。
3. facility と severity(プロトコル仕様)
facility = ログの種別、severity = 緊急度
syslog メッセージは以下のメタデータを持つ:
facility(誰が出したか / 機能カテゴリ)
facility 意味 kernカーネル userユーザープロセス(汎用) メールシステム daemonシステムデーモン auth/authpriv認証 syslogsyslogd 自身 croncron local0〜local7任意の用途(アプリ独自) severity(緊急度)
severity 数値 意味 emerg0 システム使用不能 alert1 即対応必要 crit2 致命的 err3 エラー warning4 警告 notice5 通常だが注目 info6 情報 debug7 デバッグ rsyslogd の設定(
/etc/rsyslog.confや/etc/rsyslog.d/)で「auth.*は/var/log/auth.logへ」のように振り分ける。
4. logger コマンドで syslog に書いてみる
logger は シェルスクリプトから syslog/journald に正規ルートで書き込む ためのコマンド。バッチジョブのログを統合管理する時に必須です。
- 何のコマンドか: 任意のメッセージを facility / severity / tag 付きで syslog プロトコルに送出する CLI ツール
- いつ使うか: cron や systemd で動く自作シェルスクリプトのログ出力、
/var/logに自前ファイルを作らずに journald に集約したい時、ヘルスチェック結果の記録 - 解決する具体的な問題: 「シェルスクリプトのログを自前で
/tmp/mylog.txtに書いてローテーションも自前」という温床を排除。logger -t myapp-backupで書けば後からjournalctl -t myapp-backupで一発で検索できます
# 任意のメッセージを syslog に送る
logger "テストメッセージ from $USER"
# facility と severity を指定
logger -p local0.info "アプリログ的なテスト"
logger -p auth.warning "認証警告のテスト"
# tag を付ける(プロセス名相当)
logger -t myapp "myapp 起動"
# 直後に syslog で確認
sudo tail /var/log/syslog
loggerはシェルスクリプトから syslog に書く正規ルート#!/usr/bin/env bash if ! /opt/myapp/backup.sh; then logger -p local0.err -t myapp-backup "backup failed at $(date -Iseconds)" exit 1 fiこうすると syslog 経由で集約され、後で
journalctl -t myapp-backupで簡単に追える。
5. journald とバイナリログ
journalctl は systemd 環境における ログ閲覧の主役。テキストファイルではなくバイナリ形式で蓄積される代わりに、メタデータ付きの高速検索が可能になっています。
- 何のコマンドか: journald に蓄積されたバイナリ形式の構造化ログを表示・フィルタするフロントエンド
- いつ使うか: 本番障害時の最初の原因調査、リアルタイム追跡 (
-f)、unit 横断のエラー収集、JSON 出力での jq 集計、過去ブートのログ調査 - 解決する具体的な問題: 「テキスト grep ではメタデータ(PID, UID, unit)を使った絞り込みができない」を構造化検索で解決。
journalctl _SYSTEMD_UNIT=nginx.service _PID=1234のような任意フィールドでの絞り込みが可能になります
# journald のログを見る基本
journalctl # 全部
journalctl -n 100 # 直近100行
journalctl -f # リアルタイム追跡
# unit でフィルタ
journalctl -u nginx
journalctl -u myapp -f
# 期間で絞る
journalctl --since "2 hours ago"
journalctl --since "2026-05-14 10:00" --until "2026-05-14 11:00"
# 優先度(severity 相当)
journalctl -p err # err 以上(emerg, alert, crit, err)
journalctl -p warning..err # 範囲指定
# ブート単位
journalctl -b # 今回起動以降
journalctl -b -1 # 1つ前のブート
journalctl --list-boots # ブート履歴
# カーネルメッセージ(dmesg 相当)
journalctl -k
# JSON 出力(jq でパース可能)
journalctl -u myapp -o json | jq .
journalctl -u myapp -o json-pretty | head -50
# 特定フィールドだけ抽出
journalctl _SYSTEMD_UNIT=nginx.service _PID=1234ざっくり言うと(journalctl vs tail -f)
tail -f /var/log/*.log = テキストファイルを生で見る古典的アプローチ
journalctl -u <unit> = 構造化バイナリログを「unit や時刻や優先度」でフィルタする新世代
どっちも「ログ追跡」の手段だが 粒度が全く違う。
tail -f の世界
tail -f /var/log/nginx/access.lognginx が /var/log/nginx/access.log に書き続ける
↓
tail -f が末尾を追従表示
↓
新しい行が来たら画面に出る
↓
プレーンテキストで生の文字列
良い点: シンプル、grep が直接効く 辛い点: メタデータ無し、ファイル探しが面倒、複数unitの横断不可
journalctl の世界
journalctl -u nginx -fjournald がバイナリ DB として全ログを保管
↓
PID, UID, unit, ブートID等のメタデータ付き
↓
journalctl が問い合わせ
↓
unit でフィルタ → リアルタイム表示
良い点: メタデータでフィルタ無限、unit 横断検索、JSON 出力可 辛い点: 直接 grep できない(jq 経由)、バイナリなのでファイルとして編集不可
よくある作業の対比
| やりたいこと | tail -f 系 | journalctl |
|---|---|---|
| 特定アプリのリアルタイム | tail -f /var/log/myapp.log | journalctl -u myapp -f |
| 直近1時間のエラー | `grep ERROR | tail -100` |
| 過去のブート時のログ | できない(消えてる) | journalctl -b -1 |
| 全unit横断のエラー | 複数 tail で並行 | journalctl -p err 一発 |
| 特定 PID だけ | grep で頑張る | journalctl _PID=1234 |
| JSON 出力 | 自前でパース | -o json で標準サポート |
| ローテーション | logrotate 自前 | 自動 |
| メタデータ | テキストに混在 | 自動で構造化 |
「いつどっちを使うか」
nginx access.log を見たい
↓
nginx は伝統的にファイルログ
↓
tail -f /var/log/nginx/access.log
自作 Go アプリのログを見たい
↓
stdout に書いてる
↓
journalctl -u myapp -f
複数unitの障害調査
↓
journalctl -p err --since "10min ago"
一番覚えやすい説明
tail -f= ファイルの末尾を追う(古典、プレーンテキスト)journalctl= 構造化バイナリログDBへの問い合わせ(モダン、メタデータ強い)- ファイルログ(nginx等)→
tail -f - systemd 配下のアプリ →
journalctl -u - 横断検索 →
journalctl一択
6. journald の容量管理
journald は便利な反面、設定を怠るとディスクを食い潰します。本番サーバーでは必ず上限と保管期間を明示します。
- 何の設定か:
journalctl --disk-usageで現在の使用量を確認、/etc/systemd/journald.confでSystemMaxUse=やMaxRetentionSec=を設定 - いつ設定するか: 新規サーバー構築時、ディスク使用量警告が出た時、ログ量の多いアプリをデプロイする前
- 解決する具体的な問題: 「journald が
/var/log/journal/を無制限に拡大してルートFS満杯」「ブート時にメモリのみで動いていてログが揮発」を、永続化設定と上限指定で予防します
# 使用容量
journalctl --disk-usage
# 設定確認
sudo cat /etc/systemd/journald.conf
# 古いログを削除(直近2週間だけ残す)
sudo journalctl --vacuum-time=2weeks
# サイズ上限で削除(直近100MBだけ残す)
sudo journalctl --vacuum-size=100M
# 設定で永続化と上限指定
sudo nano /etc/systemd/journald.confjournald.conf の重要設定:
[Journal]
Storage=persistent # /var/log/journal/ に永続保存
SystemMaxUse=1G # 最大1GB
SystemMaxFileSize=100M # 1ファイル最大100MB
MaxRetentionSec=30day # 30日で削除
ForwardToSyslog=yes # rsyslog にも転送(移行期に有用)journald のデフォルトは「永続化なし」のことがある
/var/log/journal/ディレクトリが無いと journald はメモリ上だけでログを保持し、再起動で消える。本番で「ログがブートで消える」事故になる前に:sudo mkdir -p /var/log/journal sudo systemd-tmpfiles --create --prefix /var/log/journal sudo systemctl restart systemd-journald
セッション②: logrotate と容量管理(20-25分)
7. logrotate の役割
ざっくり言うと
ログファイルを そのまま追記し続けると、いつかディスクを食い潰してサーバーが止まる。 logrotate は 「定期的にファイルを切り替えて、古いものを圧縮・削除する」 自動家事ロボット。
まず logrotate なしの世界
Day 1
/var/log/nginx/access.log (100MB)
Day 7
/var/log/nginx/access.log (700MB)
Day 30
/var/log/nginx/access.log (3GB)
↓
tail も grep も遅くなる
↓
バックアップが膨らむ
Day 365
/var/log/nginx/access.log (36GB)
↓
ディスク満杯
↓
nginx が書き込めず停止
↓
全サービスダウン ← 事故
logrotate のフロー
毎日深夜に動く cron が logrotate を呼ぶ:
[日次 cron]
↓
logrotate /etc/logrotate.d/* を実行
↓
各設定ファイルを順に処理
例: nginx の設定
↓
1. access.log が条件を満たすか確認(daily, size, etc.)
↓
2. 満たしたら:
access.log → access.log.1
access.log.1 → access.log.2.gz
access.log.2 → access.log.3.gz
...
access.log.13 → access.log.14.gz
access.log.14 → 削除(rotate 14 を超えた)
↓
3. 新しい空の access.log を作成
↓
4. postrotate: nginx に「ログ再オープン」シグナル
↓
完了
ファイルの世代管理(イメージ図)
Day 1 access.log (今日)
Day 2 access.log (今日)
access.log.1 (昨日)
Day 3 access.log (今日)
access.log.1 (昨日)
access.log.2.gz (一昨日、圧縮済)
...
Day 14 access.log
access.log.1
access.log.2.gz ~ access.log.14.gz
Day 15 access.log
access.log.1
access.log.2.gz ~ access.log.14.gz
← Day 1 のログ (15日前) は削除済み
主要ディレクティブ対比
| ディレクティブ | 役割 | イメージ |
|---|---|---|
daily / weekly | 切り替え頻度 | 何日に1回ロールする |
rotate 14 | 何世代保持 | 14世代より古いものは捨てる |
compress | 古いものを gzip | 容量節約 |
delaycompress | 圧縮を1世代遅らせる | 直近の .1 は未圧縮(grep しやすい) |
missingok | 無くてもエラーにしない | 初回実行で死なない |
notifempty | 空ならローテしない | 無駄な空ファイルを作らない |
create 640 user grp | 新ファイルの権限 | 新しく作る .log の所有者 |
postrotate | ローテ後フック | nginx に SIGHUP 送る等 |
copytruncate vs create の選択
create 方式(推奨):
access.log を access.log.1 にリネーム
↓
新規 access.log ファイルを作成
↓
postrotate で nginx に「ログ再オープン」シグナル
↓
nginx が新しい access.log に書き始める
※ アプリが「ログ再オープン」シグナルに対応している必要あり
copytruncate 方式:
access.log の中身を access.log.1 にコピー
↓
access.log を truncate (0バイトに)
↓
アプリ側の対応は不要
※ ただしコピー中の書き込みが消える可能性
一番覚えやすい説明
- logrotate = ログを世代管理する家事ロボット(毎晩動く)
- 無しだとディスク満杯でサービス停止
daily+rotate 14+compressが定番セット- nginx 等のミドルウェアは大抵 logrotate 設定が同梱されている
- 自作アプリは 自分で
/etc/logrotate.d/myappを書くか、journald に任せる
8. logrotate の動作
logrotate は 古くからあるログローテーション標準ツール。journald で完結しないファイルベースのログ(nginx access.log など)は今もこいつが回しています。
- 何のコマンドか: 設定ファイル
/etc/logrotate.d/<service>を読み、日次 cron で「ログ切り替え・圧縮・古いものの削除」を実行 - いつ意識するか: 自作アプリがファイルにログを書く時、nginx/apache 等のミドルウェアのログ運用、ディスク使用量警告が出た時
- 解決する具体的な問題: 「アプリが
access.logに追記し続け、1ヶ月で数十GBになって本番停止」を、世代管理 (rotate 14) + 圧縮 (compress) + 古いものの自動削除で予防します
# 設定ファイル
sudo cat /etc/logrotate.conf # 全体設定
sudo ls /etc/logrotate.d/ # サービスごとの設定
# nginx の logrotate 設定例
sudo cat /etc/logrotate.d/nginx典型的な /etc/logrotate.d/nginx:
/var/log/nginx/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 640 nginx adm
sharedscripts
postrotate
if [ -f /var/run/nginx.pid ]; then
kill -USR1 `cat /var/run/nginx.pid`
fi
endscript
}
logrotate ディレクティブ完全解説
キー 意味 daily/weekly/monthlyローテーション頻度 size 100Mサイズ基準(時間と組み合わせ可) rotate 14過去14世代まで保持 compress古いログを gzip 圧縮 delaycompress圧縮を1世代遅らせる(直近の access.log.1 はそのまま) missingokファイルが無くてもエラーにしない notifempty空ならローテーションしない copytruncateコピーしてから元ファイルを空にする(ファイルディスクリプタ保持) create 640 nginx adm新ファイル作成時の権限と所有者 sharedscripts複数ファイルに同じ postrotate を1回だけ実行 postrotate ... endscriptローテーション後に実行(プロセスへの SIGHUP 等) prerotate ... endscriptローテーション前に実行
copytruncatevscreateの選択:
create(推奨): 新ファイルを別名で作る → アプリに「ログ再オープン」のシグナル送信が必要(nginx のUSR1など)copytruncate: ファイル中身をコピー → 元ファイルをtruncate(0)→ アプリ側の対応不要だが、コピー中の書き込みが消える可能性
9. logrotate を手動で叩く
設定ファイルを書いたら 「次回 cron 起動を待たずに即テスト」 したいもの。logrotate にはデバッグ実行と強制実行の2モードが用意されています。
- 何のコマンドか:
-d(debug) で「何が起こるか」を表示するだけ、-f(force) で時間条件を無視して即実行 - いつ使うか: 新規 logrotate 設定をデプロイ直後、
postrotateスクリプトの動作確認、本番投入前のドライラン - 解決する具体的な問題: 「設定を書いて翌日まで動作が分からない」を即時テストで解決、
postrotateのsystemctl reloadが失敗してログが回らない事故を本番前に検出できます
# 設定の文法チェック(実行はしない)
sudo logrotate -d /etc/logrotate.d/nginx
# 強制実行(時間条件を無視)
sudo logrotate -f /etc/logrotate.d/nginx
# 確認
ls -lh /var/log/nginx/自作 logrotate 設定の最小例
# /etc/logrotate.d/myapp として配置 /var/log/myapp/*.log { daily rotate 7 compress delaycompress missingok notifempty create 640 myapp myapp postrotate systemctl reload myapp || true endscript }
アンチパターン:ローテなしの自前ログ
// NG: 1年で50GBになり本番停止 file, _ := os.OpenFile("/var/log/myapp.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) log.SetOutput(file)改善:
- 標準出力に書く → systemd の journald に任せる(推奨)
- ファイルに書くなら logrotate 設定をセットでデプロイ
- lumberjack 等のローテーションライブラリを使う(Go なら
gopkg.in/natefinch/lumberjack.v2)
セッション③: 構造化ログとログ設計(20-30分)
10. プレーンテキストログの限界
2026-05-14 10:30:01 ERROR Failed to process order id=12345 user=alice reason="payment declined"
2026-05-14 10:30:02 INFO Processing order 67890
2026-05-14 10:30:03 ERROR Database timeout, retrying...これを解析しようとすると:
- 「id=12345 のエラーだけ抽出」→ 正規表現
- 「失敗した user の集計」→ awk + sort + uniq
- 「過去1時間の error 率」→ 専用スクリプト
すべて 正規表現とパース処理に頼る。フォーマット変更で全クエリが壊れる。
11. 構造化ログ(JSON)
{"ts":"2026-05-14T10:30:01Z","level":"error","msg":"failed to process order","order_id":12345,"user":"alice","reason":"payment declined","trace_id":"abc-123"}
{"ts":"2026-05-14T10:30:02Z","level":"info","msg":"processing order","order_id":67890,"trace_id":"def-456"}構造化ログがもたらすもの
- クエリ可能:
jq 'select(.level == "error" and .user == "alice")'- 集約可能: Elasticsearch / Loki / CloudWatch Logs Insights で集計
- トレース連携:
trace_idで分散システム横断追跡- フォーマット不変: 新フィールド追加してもクエリが壊れない
- 言語非依存: JSON は誰でも読める
業界標準形式の例:
- JSON Lines (1行1JSON):最も普及
- logfmt (
level=error msg="..."形式):Heroku / Stripe 系- OpenTelemetry Logs:分散トレースと統合
jq で集計の威力
# 過去のログから error の user 別件数 journalctl -u myapp -o json | \ jq -r 'select(.LEVEL == "error") | .USER' | \ sort | uniq -c | sort -rn # nginx の access.log(JSON 形式に設定済み前提)から # 401/403 を出しているIPトップ10 cat /var/log/nginx/access.log | \ jq -r 'select(.status == 401 or .status == 403) | .remote_addr' | \ sort | uniq -c | sort -rn | head -10
12. ログレベル設計
ログレベルの実務的な意味(多くのフレームワーク共通)
レベル 何のために書く 本番出すべきか FATAL/CRITプロセス停止級の異常 必ず ERRORリクエスト失敗、機能不能 必ず(アラート対象) WARN異常だが処理は続けられる 必ず INFO重要イベント(起動、停止、デプロイ、トランザクション完了) 必ず DEBUG開発中の詳細な動き 本番では出さない(or オンデマンドで) TRACE関数の入出力など極詳細 本番では絶対に出さない 本番デフォルト推奨:
INFO以上を出力。DEBUGを出すと量とコストが爆発。動的にレベル変更できる仕組みを入れておくとデバッグ時に便利:
- Go:
log/slogでレベル変数を環境変数化- Node:
pino/winstonで同様- Spring Boot: actuator で実行時変更可能
13. ログに書いてはいけないもの(セキュリティ)
個人情報・秘密情報をログに書くな
絶対に書いてはいけない情報:
- パスワード、API トークン、JWT、セッション ID
- クレジットカード番号、有効期限、CVV
- マイナンバー、運転免許証番号
- 個人の本人特定可能情報の不必要な詳細(PII)
- 暗号化キー、署名秘密鍵
法的リスク:
- 日本:個人情報保護法、PCI-DSS(クレカ取扱い)
- EU:GDPR、最大で全世界年間売上の4%の制裁金
- 米:CCPA(カリフォルニア州)
対策パターン
- マスキング:
card_number: "****-****-****-1234"のように下4桁のみ- ハッシュ: ユーザー識別だけしたい時は
user_hash: sha256(...)でログに書く- 構造化ログのフィールド除外: ロガー側で
passwordフィールドは常に削除- ログ収集時の sanitizer: Fluent Bit / Vector で特定フィールドを削除
アンチパターン: リクエスト全文をそのままダンプ
# NG logger.info(f"received request: {request.json()}") # → リクエストボディに password / token が含まれていると平文で残る正解:
# OK: 必要な情報だけ抽出 logger.info("request received", extra={"path": request.path, "method": request.method, "user_id": user.id})
14. 実例:nginx access.log を構造化+ jq で集計
# /etc/nginx/nginx.conf に log_format を追加
log_format json_combined escape=json '{'
'"ts":"$time_iso8601",'
'"remote_addr":"$remote_addr",'
'"method":"$request_method",'
'"path":"$request_uri",'
'"status":$status,'
'"bytes_sent":$bytes_sent,'
'"request_time":$request_time,'
'"user_agent":"$http_user_agent",'
'"referer":"$http_referer"'
'}';
access_log /var/log/nginx/access.log json_combined;# 集計クエリ例
# 上位IPトップ10
cat /var/log/nginx/access.log | jq -r '.remote_addr' | sort | uniq -c | sort -rn | head -10
# 5xx エラーの発生時間と path
cat /var/log/nginx/access.log | jq -r 'select(.status >= 500) | "\(.ts) \(.status) \(.path)"'
# レスポンスタイム p99
cat /var/log/nginx/access.log | jq '.request_time' | sort -n | awk 'BEGIN{c=0} {a[c++]=$1} END{print a[int(c*0.99)]}'
# 上位パス別の平均レスポンスタイム
cat /var/log/nginx/access.log | jq -r '[.path, .request_time] | @tsv' | \
awk '{sum[$1]+=$2; count[$1]++} END {for (p in sum) printf "%.3f %d %s\n", sum[p]/count[p], count[p], p}' | \
sort -rn | head -2015. ログ集約システムへの誘い
ローカルで journalctl や jq できるのは数台規模まで。本番ではログを 集約ストレージ に流し込む:
| 系統 | 代表 OSS / SaaS | 特徴 |
|---|---|---|
| 全文検索 | Elasticsearch + Kibana, OpenSearch | リッチなUI、コスト高 |
| ストリーミング | Loki (Grafana), Tempo | コストパフォーマンス重視 |
| クラウド | CloudWatch Logs, Cloud Logging, Datadog Logs | 運用楽、コスト青天井 |
| Splunk | Splunk Enterprise | エンプラ定番、高い |
転送エージェント:
- Fluent Bit / Fluentd: 軽量、最も普及
- Vector: Rust 製、新興だが高速
- Promtail: Loki 専用
これらは Level 3 以降のテーマだが、ローカル時点で 「JSON で構造化して出力する」 ことができていれば、集約システムへの移行はほぼノーコスト。
練習課題
課題1: nginx access.log を JSON 化して上位IPを集計
ローカルに小さな nginx を立てるか、サンプルデータでも可:
# サンプル access.log (JSON形式) を生成
cat > ~/learn/linux/day206/sample_access.log <<'EOF'
{"ts":"2026-05-14T10:00:00Z","remote_addr":"192.168.1.10","method":"GET","path":"/api/users","status":200,"bytes_sent":1234,"request_time":0.012}
{"ts":"2026-05-14T10:00:01Z","remote_addr":"192.168.1.10","method":"GET","path":"/api/posts","status":200,"bytes_sent":5678,"request_time":0.034}
{"ts":"2026-05-14T10:00:02Z","remote_addr":"203.0.113.5","method":"POST","path":"/api/login","status":401,"bytes_sent":120,"request_time":0.045}
{"ts":"2026-05-14T10:00:03Z","remote_addr":"203.0.113.5","method":"POST","path":"/api/login","status":401,"bytes_sent":120,"request_time":0.043}
{"ts":"2026-05-14T10:00:04Z","remote_addr":"203.0.113.5","method":"POST","path":"/api/login","status":401,"bytes_sent":120,"request_time":0.048}
{"ts":"2026-05-14T10:00:05Z","remote_addr":"192.168.1.10","method":"GET","path":"/api/users","status":500,"bytes_sent":80,"request_time":2.456}
EOF
cd ~/learn/linux/day206
# 集計1: IP別アクセス数
jq -r '.remote_addr' sample_access.log | sort | uniq -c | sort -rn
# 集計2: 401を連発しているIPを検出(簡易ブルートフォース検知)
jq -r 'select(.status == 401) | .remote_addr' sample_access.log | sort | uniq -c | sort -rn
# 集計3: 平均レスポンスタイム
jq '.request_time' sample_access.log | awk '{s+=$1; c++} END{print "avg:", s/c}'
# 集計4: 5xx エラーの一覧
jq -r 'select(.status >= 500) | "\(.ts) \(.status) \(.path)"' sample_access.log課題2: 自作 logrotate 設定を書く
# 自分のアプリログ向けの logrotate 設定
sudo tee /etc/logrotate.d/myapp > /dev/null <<'EOF'
/var/log/myapp/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 640 myapp myapp
sharedscripts
postrotate
systemctl reload myapp 2>/dev/null || true
endscript
}
EOF
# 文法チェック
sudo logrotate -d /etc/logrotate.d/myapp
# 強制実行
sudo logrotate -f /etc/logrotate.d/myapp
# 結果確認
ls -lh /var/log/myapp/課題3: ログレベルを意識して書き直す
# Before(典型的にダメな例)
print(f"user logged in: {user.email} password={password}")
print("done")
# After(学んだ内容を反映)
import logging
import json
logger = logging.getLogger("myapp")
# 個人情報マスキング、構造化、レベル明示
logger.info(json.dumps({
"event": "user_login",
"user_id": user.id, # email や password は書かない
"ip": request.remote_addr,
"result": "success"
}))締め: git で証跡を残す
exit
cd ~/learn/linux/day206
git init 2>/dev/null
git add .
git commit -m "feat(linux): ログ運用 - syslog/journald/logrotate と構造化ログ集計"チェックリスト
- syslog の facility / severity を説明できる
-
/var/log/auth.logで何を調べるか答えられる - journald のメリットとデメリットを3つずつ挙げられる
-
journalctlでフィルタ・期間・JSON 出力を使える - logrotate の
createとcopytruncateの違いを説明できる - 構造化ログ(JSON)の利点を3つ挙げられる
- ログに書いてはいけない情報を3つ以上即答できる
- 本番ログレベルを
INFO以上にする理由を説明できる - nginx access.log を jq で集計できる
詰まった時のチートシート
| やりたいこと | コマンド |
|---|---|
| 認証ログ確認 | sudo tail /var/log/auth.log |
| journald ログ全表示 | journalctl |
| unit でフィルタ | journalctl -u <unit> |
| 期間で絞る | journalctl --since "1 hour ago" |
| エラーのみ | journalctl -p err |
| JSON で出力 | journalctl -o json | jq . |
| journal の容量 | journalctl --disk-usage |
| 古い journal 削除 | sudo journalctl --vacuum-time=2weeks |
| logrotate 文法チェック | sudo logrotate -d /etc/logrotate.d/<file> |
| logrotate 強制実行 | sudo logrotate -f /etc/logrotate.d/<file> |
| syslog にメッセージ送信 | logger -t myapp -p local0.info "message" |
| jq で JSON 集計 | jq '<filter>' |
「実務OK」基準
- 本番障害が起きた時、journalctl で最初の手がかりを5分以内に得られる
- 新しいアプリを書く時、最初から構造化ログ(JSON)にできる
- ログにパスワードを書きそうな PR を即座にブロックできる
- ディスク満杯事故を logrotate / journald 設定で予防できる
さらに深掘りするなら
man rsyslog.conf: rsyslog の振り分けルール完全リファレンスman systemd-journald.service/man journalctlman 5 logrotate.conf: logrotate ディレクティブ完全リファレンス- RFC 5424 (syslog protocol) :プロトコル仕様の原文
- 書籍『入門 監視』(O’Reilly): ログを「監視の文脈」で位置付ける
- Loki 公式 docs (https://grafana.com/docs/loki/) : モダンな OSS ログ集約
- OpenTelemetry Logs Specification: 業界標準化の動向
アンチパターン / 初心者やらかし事例
NG 1: ログに平文パスワード・トークン
logger.info(f"User login: email={email} password={password}")→ 漏洩時に 即アカウント乗っ取り。対策: 個人情報は ID 化、機密は出さない。レビューで必ず弾く。
NG 2: logrotate なしで永遠に書き続ける
# nginx の access.log が 50GB に
df -h # / が 100% でサーバー停止→ 対策: logrotate を必ず設定。/etc/logrotate.d/myapp を最初から用意。
NG 3: copytruncate を理解せず create でローテートしてアプリがファイル離さない
/var/log/myapp.log {
daily
create # アプリが古い fd を掴んだままで、新しいファイルに書かれない
}
→ 対策: アプリが 再オープンしない タイプなら copytruncate、SIGHUP で再オープンするアプリなら postrotate で kill -HUP。
NG 4: DEBUG ログを本番で出しっぱなし
log_level: DEBUG # 開発で便利、本番で 10TB/日→ 対策: 本番は INFO 以上。環境ごとに log_level を変える。
自己評価チェックリスト
-
journalctl -u myapp --since "10 min ago"を即書ける - logrotate の設定を1つ自分で書いた
- アプリのログを JSON 構造化に書き換えた(または書き換える方法を答えられる)
- 「ログに書いてはいけない情報」を3つ即答できる
-
jq '.user_id'で nginx ログから値を抽出できる
次のレッスン: Day 14 - Level 2 総復習
明日は Level 2 を統合する実践プロジェクト。
自作 Go HTTP サーバーを systemd で常駐化し、cron で日次バックアップ、logrotate でログ管理、という一連の流れを構築する。障害シナリオ(プロセスkill / OS再起動)での 自動復活 も検証する。