Skip to main content

第05章:ログの基本:まずは“標準出力”に出す 📣🖥️

① 今日のゴール 🎯

  • 「コンテナのログは 標準出力(stdout)/ 標準エラー(stderr) に出すのが基本!」を腹落ちさせる 😆
  • 起動ログリクエストログエラーログ を分けて出せるようになる 🧾✨
  • コンテナを再起動しても(同じコンテナなら)ログを追える感覚をつかむ 🔁👀

② 図(1枚)🖼️:ログが拾われる道すじ

(アプリ) console.log / console.error


stdout / stderr ← ここに出すのが超大事!📣


Docker が回収(ログドライバ経由)🧲


docker logs / docker compose logs で見える 👀

Docker はコンテナ内プロセスの stdout/stderr を回収してログとして扱います。(Docker Documentation) Node.js 側でも、console.log は stdout、console.error は stderr に出る想定で使えます。(Node.js)


③ 手を動かす(手順 5〜10個)🛠️🚀

ここでは「前章のミニAPI」に、ログをちゃんと出す仕組みを足します😊 (まだ無い人向けに、最小構成も丸ごと載せます📦)


ステップ1:ファイル構成(最小)📁

observability-lab/
compose.yml
Dockerfile
package.json
tsconfig.json
src/
server.ts
logger.ts

ステップ2:logger.ts を作る(stdout / stderr を分ける)🎚️🟢🔴

ポイントは超シンプル👇

  • 情報=stdout(console.log)
  • エラー=stderr(console.error)
// src/logger.ts
type LogLevel = "INFO" | "ERROR";

function nowIso() {
return new Date().toISOString();
}

export function logInfo(message: string, fields: Record<string, unknown> = {}) {
const line = formatLine("INFO", message, fields);
// stdout
console.log(line);
}

export function logError(message: string, fields: Record<string, unknown> = {}) {
const line = formatLine("ERROR", message, fields);
// stderr
console.error(line);
}

function formatLine(level: LogLevel, message: string, fields: Record<string, unknown>) {
// まずは「読みやすい1行」に寄せる(JSON化は第9章でやる想定)🧱
const base = `time=${nowIso()} level=${level} msg="${escapeQuotes(message)}"`;
const extras = Object.entries(fields)
.map(([k, v]) => `${k}=${escapeQuotes(String(v))}`)
.join(" ");
return extras ? `${base} ${extras}` : base;
}

function escapeQuotes(s: string) {
return s.replaceAll(`"`, `\\"`);
}

Node.js の console は stdout/stderr を使い分ける設計で説明されています。(Node.js)


ステップ3:server.ts に「起動ログ」「リクエストログ」「エラーログ」を入れる 🧾🔥

  • 起動時:boot ログ
  • リクエスト:1リクエストにつき1行(今は“超入門版”)
  • エラー:例外を拾って stderr に吐く
// src/server.ts
import express from "express";
import { logError, logInfo } from "./logger.js";

const app = express();
app.use(express.json());

// 🧾 リクエストログ(超入門)
// status と ms を finish で拾うのがポイント 👀
app.use((req, res, next) => {
const start = Date.now();

res.on("finish", () => {
const ms = Date.now() - start;
logInfo("request", {
method: req.method,
path: req.originalUrl,
status: res.statusCode,
ms,
});
});

next();
});

// 前章の想定:/ping と /slow(無ければこれでOK)🐢
app.get("/ping", (_req, res) => {
res.json({ ok: true });
});

app.get("/slow", async (_req, res) => {
await new Promise((r) => setTimeout(r, 800));
res.json({ ok: true, slow: true });
});

// わざと落とす(/boom)💥
app.get("/boom", () => {
throw new Error("BOOM! intentional error");
});

// 🧯 エラーハンドラ(ここが “stderr に出す” 本丸)
app.use((err: unknown, req: express.Request, res: express.Response, _next: express.NextFunction) => {
const e = err instanceof Error ? err : new Error("unknown error");

logError("unhandled error", {
method: req.method,
path: req.originalUrl,
name: e.name,
message: e.message,
// stack は長くなるので好みで(まずは出してOK)📌
stack: e.stack ?? "",
});

res.status(500).json({ ok: false });
});

const port = Number(process.env.PORT ?? 3000);
app.listen(port, () => {
logInfo("server started", { port });
});

ステップ4:package.json / tsconfig.json(最小)⚙️

{
"name": "observability-lab",
"private": true,
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.json",
"start": "node dist/server.js"
},
"dependencies": {
"express": "^4.19.2"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^22.10.0",
"typescript": "^5.7.0"
}
}
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Bundler",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true
},
"include": ["src"]
}

(依存バージョンは例です。ここは “動けばOK” で進めて大丈夫👌)


ステップ5:Dockerfile(ログはファイルに出さない!)📦🧠

FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npm run build

FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/server.js"]

ステップ6:compose.yml(起動して logs で見る)👀🏃‍♂️

services:
api:
build: .
ports:
- "3000:3000"
environment:
- PORT=3000

ステップ7:起動して叩く 🚀🔨

docker compose up -d --build

ブラウザや curl で叩く👇

  • /ping ✅
  • /slow 🐢
  • /boom 💥

ステップ8:ログを見る(まずはこれだけ覚えればOK)👀✨

docker compose logs -f api

docker compose logs はサービスのログを見れます(follow や tail などオプションあり)(Docker Documentation)


④ 期待する出力(例)🧾✅

だいたいこんな感じの1行ログが並べば勝ちです😆

api-1  | time=2026-02-13T04:12:10.123Z level=INFO msg="server started" port=3000
api-1 | time=2026-02-13T04:12:15.008Z level=INFO msg="request" method=GET path=/ping status=200 ms=2
api-1 | time=2026-02-13T04:12:20.552Z level=INFO msg="request" method=GET path=/slow status=200 ms=804
api-1 | time=2026-02-13T04:12:25.100Z level=ERROR msg="unhandled error" method=GET path=/boom name=Error message=BOOM! intentional error stack=Error: BOOM! intentional error ...
api-1 | time=2026-02-13T04:12:25.101Z level=INFO msg="request" method=GET path=/boom status=500 ms=1

⑤ チェック:コンテナ再起動しても見える?🔁👀

「ログって消えるの?」を体験します😊

docker compose restart api
docker compose logs api --tail=20
  • restart は「同じコンテナを再起動」なので、普通はログをさかのぼれます ✅
  • ただし docker compose down でコンテナを消すと、ログも一緒に消えます(ログの置き場所がコンテナに紐づくため)⚠️ ※このへんは “運用” で大事になるので、まずは「消える操作がある」だけ覚えればOK👍

また、docker logs は stdout/stderr を表示します。(docs.docker.jp)


⑥ つまづきポイント(3つ)🪤😵‍💫

  1. ログをファイルに書いてしまう 📄➡️💥 コンテナ内のファイルは「いつでも捨てられる」ので、まずは stdout/stderr に出すのが正解です📣(あとで必要なら“集める仕組み”を足す)

  2. console.log を消したくなる 😇 気持ちは分かる!でも最初は “見えること”が正義。 後の章で「量を減らす」「JSON化」「収集して検索」へ進みます🧱🔍

  3. エラーがログに出ない 🫥 Express はエラーハンドラを書かないと「落ちた理由」が行方不明になりがち。 この章の error handler は、とにかく“逃さない”のが目的🧯


⑦ ミニ課題(15分)⏳🧩

次を満たすように改造してみてね😊

  • /ping を叩いたら、必ず1行 リクエストログが出る ✅
  • /boom を叩いたら、ERROR が stderr 側に出てる(=console.error 経由)🔥
  • 起動ログに service=api を追加してみる(fields に足すだけ)🏷️

⑧ AIに投げるプロンプト例(コピペOK)🤖📋

Copilot / Codex にそのまま貼ってOK👇

TypeScript + Express の超入門ログを作りたいです。
要件:
- console.log は INFO (stdout)、console.error は ERROR (stderr)
- 起動ログ / リクエストログ / エラーログを出す
- ログは1行の key=value 形式(time, level, msg, method, path, status, ms など)
- Express のエラーハンドラで例外を拾って stderr に出す
src/logger.ts と src/server.ts の完成コードを提案してください。

ここまでで得られる感覚 💡✨

  • 「コンテナのログは stdout/stderr に出すと、Docker が拾ってくれる」📣🧲
  • 「docker compose logs で全部見える」👀
  • 「INFO と ERROR を分けると、後で“重要ログだけ拾う”がやりやすい」🎚️

次章(第6章)では、このログを 追いかける・絞る 操作に集中して、“最短で目的ログを掘り当てる”練習をします🏃‍♂️🔎