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つの異なる役割 がある:

  1. デバッグ: バグを再現するための痕跡
  2. 監査 (audit): 「誰が」「いつ」「何を」したかの法的・規制上の記録
  3. 性能 (performance): レイテンシ、リソース使用量の長期トレンド
  4. セキュリティ: 不正アクセス検知、侵入後の追跡

これらは目的が違うため、ログレベルもフォーマットも保管期間も異なる。「全部 print してファイルに書いとけば良い」は本番では通用しない。

さらに、ログには 負の側面 がある:

  • ディスクを食いつぶす: 1日100GBのアクセスログで本番が停止
  • 秘密情報を漏らす: ログにパスワードや個人情報が混入
  • 性能劣化: 同期書き込みが処理を遅らせる
  • コスト: クラウドのログ取り込み料金が月数百万円

本気のバックエンドエンジニアは「ログを書く」だけでなく 「何を、どう書くか、いつ消すか」を設計 できなければならない。


セッション①: Linux のログ機構の全体像(25-30分)

0. 録画スタート

mkdir -p ~/log ~/learn/linux/day206
cd ~/learn/linux/day206
script ~/log/linux_day206.log

1. /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/errorWeb アクセス調査
/var/log/journal/journald のバイナリログsystemctl/journalctl 経由で読む

多くの場合 root しか読めない (-rw-r-----)。プログラムから読みたい場合は adm グループに参加するか、syslog グループへの追加が必要。

2. syslog プロトコルの世界

syslog はプロトコル、ファイル名ではない

「syslog」という言葉は3つの意味で使われる:

  1. プロトコル (RFC 3164 / 5424):プロセスが OS のログ集約機構にメッセージを送る規格
  2. デーモン:そのプロトコルを受けて処理するデーモン(rsyslogdsyslog-ng
  3. ファイル名/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ユーザープロセス(汎用)
mailメールシステム
daemonシステムデーモン
auth / authpriv認証
syslogsyslogd 自身
croncron
local0local7任意の用途(アプリ独自)

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.log
nginx が /var/log/nginx/access.log に書き続ける
  ↓
tail -f が末尾を追従表示
  ↓
新しい行が来たら画面に出る
  ↓
プレーンテキストで生の文字列

良い点: シンプル、grep が直接効く 辛い点: メタデータ無し、ファイル探しが面倒、複数unitの横断不可

journalctl の世界

journalctl -u nginx -f
journald がバイナリ DB として全ログを保管
  ↓
PID, UID, unit, ブートID等のメタデータ付き
  ↓
journalctl が問い合わせ
  ↓
unit でフィルタ → リアルタイム表示

良い点: メタデータでフィルタ無限、unit 横断検索、JSON 出力可 辛い点: 直接 grep できない(jq 経由)、バイナリなのでファイルとして編集不可

よくある作業の対比

やりたいことtail -fjournalctl
特定アプリのリアルタイムtail -f /var/log/myapp.logjournalctl -u myapp -f
直近1時間のエラー`grep ERRORtail -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.confSystemMaxUse=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.conf

journald.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ローテーション前に実行

copytruncate vs create の選択

  • create(推奨): 新ファイルを別名で作る → アプリに「ログ再オープン」のシグナル送信が必要(nginx の USR1 など)
  • copytruncate: ファイル中身をコピー → 元ファイルを truncate(0) → アプリ側の対応不要だが、コピー中の書き込みが消える可能性

9. logrotate を手動で叩く

設定ファイルを書いたら 「次回 cron 起動を待たずに即テスト」 したいもの。logrotate にはデバッグ実行と強制実行の2モードが用意されています。

  • 何のコマンドか: -d (debug) で「何が起こるか」を表示するだけ、-f (force) で時間条件を無視して即実行
  • いつ使うか: 新規 logrotate 設定をデプロイ直後、postrotate スクリプトの動作確認、本番投入前のドライラン
  • 解決する具体的な問題: 「設定を書いて翌日まで動作が分からない」を即時テストで解決、postrotatesystemctl 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)

改善:

  1. 標準出力に書く → systemd の journald に任せる(推奨)
  2. ファイルに書くなら logrotate 設定をセットでデプロイ
  3. 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(カリフォルニア州)

対策パターン

  1. マスキング: card_number: "****-****-****-1234" のように下4桁のみ
  2. ハッシュ: ユーザー識別だけしたい時は user_hash: sha256(...) でログに書く
  3. 構造化ログのフィールド除外: ロガー側で password フィールドは常に削除
  4. ログ収集時の 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 -20

15. ログ集約システムへの誘い

ローカルで journalctljq できるのは数台規模まで。本番ではログを 集約ストレージ に流し込む:

系統代表 OSS / SaaS特徴
全文検索Elasticsearch + Kibana, OpenSearchリッチなUI、コスト高
ストリーミングLoki (Grafana), Tempoコストパフォーマンス重視
クラウドCloudWatch Logs, Cloud Logging, Datadog Logs運用楽、コスト青天井
SplunkSplunk 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 の createcopytruncate の違いを説明できる
  • 構造化ログ(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 journalctl
  • man 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 を掴んだままで、新しいファイルに書かれない
}

→ 対策: アプリが 再オープンしない タイプなら copytruncateSIGHUP で再オープンするアプリなら 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再起動)での 自動復活 も検証する。

Day 14: Level 2 総復習