第13章:ログ量と保存:増えすぎ問題と“削る設計”💽🌀
① 今日のゴール 🎯
- ログが増えすぎると 「ディスクが死ぬ」「探せない」「遅くなる」 を体感する 😇
- ログを“削る設計”(出し方の工夫)を入れて、読めるログに戻す ✂️🧾
- 保存(ローテーション/保持) をDocker側で最低限ガードできるようになる 🛡️📦
② 図(1枚)🖼️(イメージ)
- ログは放置するとこうなる👇 アプリ 📣(大量出力)→ Dockerログ保存 💽(肥大化)→ 見る人 👀(迷子)
- “削る設計”を入れるとこう👇 アプリ ✂️(必要だけ出す)+ Docker 🌀(ローテーション)→ 見る人 😌(追える)
③ 手を動かす(ハンズオン:ログ地獄→救出)🛠️🔥
この章は「わざと壊す」→「直す」がテーマです 😈➡️🩹 既にあるミニAPI(/ping /slow /boom など)に /spamlog を足します。
A. まず「ログ地獄」を作る 😇🔥
1) /spamlog を追加する(まずは最悪版)💥
ファイル構成(例)📁
- /src/app.ts
- /src/logger.ts
- compose.yml
/src/logger.ts(例:pinoでJSONログ)
import pino from "pino";
export const logger = pino({
level: process.env.LOG_LEVEL ?? "info",
});
/src/app.ts(/spamlog を追加)
import express from "express";
import { logger } from "./logger";
export const app = express();
app.get("/spamlog", (req, res) => {
const count = Math.min(Number(req.query.count ?? "2000"), 20000);
const bytes = Math.min(Number(req.query.bytes ?? "300"), 2000);
const payload = "x".repeat(bytes);
for (let i = 0; i < count; i++) {
// 💣 最悪:毎回でかいpayloadを出す(地獄の始まり)
logger.info({ i, payload }, "spam");
}
res.json({ ok: true, count, bytes });
});
2) 叩いてみる(ログの洪水)🌊
- ブラウザでもOK
- コマンドなら(PowerShellでも確実に動くように
curl.exe推奨)👇
curl.exe "http://localhost:3000/spamlog?count=5000&bytes=800"
3) ログを見る(…終わる)👀💦
docker compose logs api --tail 50
docker compose logs api --tail 20 --follow
--follow(追従)や --tail(末尾だけ)や --since(時間で絞る)は、この手の“救出”で超重要です。(Docker Documentation)
B. “削る設計”でログを救う ✂️🧾✨
ここからが本番です 😤🔥 ポイントは 「出す場所(Docker)で工夫」より先に、「出し方(アプリ)」を直す こと!
4) まず「巨大payloadをログに出さない」🙈⛔
/spamlog を “救出版” に差し替えます👇
app.get("/spamlog", (req, res) => {
const count = Math.min(Number(req.query.count ?? "2000"), 20000);
const bytes = Math.min(Number(req.query.bytes ?? "300"), 2000);
// ✅ でかい中身は捨てて、要約だけ残す(これが“削る設計”)
logger.warn({ count, bytes }, "spamlog requested");
res.json({ ok: true, count, bytes });
});
✅ 「情報として価値があるのは count と bytes」 ❌ payload本体は “コストの割に価値が低い” ので切る ✂️
5) 次に「成功ログをサンプリングする」🎲📉
全部の成功ログを残す必要はありません(特に /ping や健康診断系) サンプリングは「割合で残す」発想です🧠✨
/src/logPolicy.ts(追加)
export function shouldSampleSuccess(): boolean {
const rate = Number(process.env.LOG_SAMPLE_SUCCESS_RATE ?? "0.01"); // 1% 既定
return Math.random() < rate;
}
アクセスログのミドルウェア(例:最後に1行出す想定)
import { shouldSampleSuccess } from "./logPolicy";
import { logger } from "./logger";
app.use((req, res, next) => {
const start = Date.now();
res.on("finish", () => {
const ms = Date.now() - start;
const status = res.statusCode;
// ✅ 失敗は全部残す(あとで原因追跡に必須)
const isError = status >= 500;
// ✅ 成功はサンプルだけ
const okSampled = status < 500 && shouldSampleSuccess();
// ✅ /health みたいな超多発は、さらに絞るのもアリ
const isNoisyRoute = req.path === "/health" || req.path === "/ping";
if (isError || (okSampled && !isNoisyRoute)) {
logger.info({ method: req.method, path: req.path, status, ms }, "access");
}
});
next();
});
6) 「同じ警告を秒1回まで」みたいに抑える(レート制限)🚦⏱️
“同じエラー”が連打されると、ログが埋まって本当に大事な1行が消えます 😭
/src/rateLimitLog.ts
const last = new Map<string, number>();
export function allowLog(key: string, intervalMs: number): boolean {
const now = Date.now();
const prev = last.get(key) ?? 0;
if (now - prev < intervalMs) return false;
last.set(key, now);
return true;
}
使い方(例)👇
import { allowLog } from "./rateLimitLog";
if (allowLog("db-conn-fail", 1000)) {
logger.error({ err: "ECONNREFUSED" }, "db connection failed");
}
④ Docker側で「保存(ローテーション)」をかける 🌀📦🛡️
アプリ側で削った上で、最後の保険として Docker側の“ログ保持上限” を入れます。
7) Composeでログローテーション(localドライバ)🧰
Dockerの local ログドライバは、stdout/stderrをパフォーマンスとディスク効率に配慮した形式で保存し、既定で コンテナあたり100MB を保持しつつ 圧縮もします。さらに max-size / max-file / compress が設定できます。(Docker Documentation)
compose.yml(apiサービスに追記例)
services:
api:
# (build, ports, environment などは省略)
environment:
LOG_LEVEL: "info"
LOG_SAMPLE_SUCCESS_RATE: "0.01" # 成功ログ1%
logging:
driver: "local"
options:
max-size: "10m"
max-file: "3"
compress: "true"
これで「1ファイル10MB×最大3世代」みたいに、ログ保存が暴走しにくくなります 💽🌀
大事:設定変更は“新しく作ったコンテナ”から効くので、作り直しが必要です 🔁 (デーモン設定でも同じで、既存コンテナは自動では変わりません)(Docker Documentation)
作り直し(例)👇
docker compose up -d --force-recreate
8) json-file を使う場合の上限(max-size / max-file)🧱
json-file でも max-size / max-file でローテーションできます(設定しないと肥大化しがち)。(Docker Documentation)
また daemon.json の log-opts は 文字列で書く必要があります。(Docker Documentation)
⑤ つまづきポイント(3つ)🪤😵💫
-
Composeのlogging設定を変えたのに効かない → だいたい コンテナを作り直してない パターンです 🔁(Docker Documentation)
-
/ping や /health のログで埋まる → “成功ログはサンプル”、健康診断は“さらに絞る or 切る”が効きます ✂️💚
-
ログに秘密情報が混ざる → ログは「見える場所に出る」前提。機密や個人情報は出すと危険です 🙈🔒 (OWASPも“ログに出しちゃダメな情報”を強く警告しています)(OWASP)
⑥ ミニ課題(15分)⏳✍️
次の“ログ方針メモ”を、あなたのミニAPI用に1枚で書いてください📝✨
- 残すログ(必須):5xx、起動/停止、外部依存の失敗、想定外例外
- サンプルで残すログ:通常アクセスログ(成功系)
- 捨てる/要約するログ:/ping /health の成功、巨大ボディ、連打される同一警告
- 保存:local driver で max-size / max-file を設定(例:10m×3)
⑦ AIに投げるプロンプト例(コピペOK)🤖📋✨
- /spamlog を “要約ログだけ” に直したい
Express + TypeScript の /spamlog を、payloadをログに出さず count と bytes だけを warn で1行出すように修正して。変更差分コードで。
- 成功ログを1%だけ出すサンプリングを入れたい
アクセスログを res.finish で1行出しています。status>=500は必ず、それ以外は LOG_SAMPLE_SUCCESS_RATE(既定0.01) の確率で出すように変更して。/health と /ping は成功ログを出さない方針で。
- 同じエラーを秒1回までに抑えたい
同じキーのログを intervalMs 以内は抑制する allowLog(key, intervalMs) を Map で実装して。Node/TSで。使用例も。
まとめ 🎉
-
ログは 増えるほど価値が落ちる(読めない・探せない・コスト増)😇💸
-
解決の順番はこれ👇
- アプリで削る設計(要約・サンプル・レート制限)✂️
- Dockerで保存上限(local driver などでローテーション)🌀💽(Docker Documentation)
-
次章(第14章)で、いよいよ “集めて検索”(Loki/Grafana)に入ると、ここで整えたログがめちゃ効きます 🧲🔍📊