第36章 観測ちょい入門:ログをどこに置く?🪪📊
![hex_ts_study_036[(./picture/hex_ts_study_036_observability_logging.png)
(Past chat)(Past chat)(Past chat)(Past chat)(Past chat)
第36章 観測ちょい入門:ログをどこに置く?🪪📊✨
この章は、「障害が起きたときに最短で原因に辿りつける」ようにする入門だよ😊 ヘキサゴナル的に “中心を静かに保つ🧠✨” まま、外側(Adapter)でログを整えるのがテーマ!
ちなみに今(2026/01時点)は Node.js v24 が Active LTS、TypeScript は 5.9.3 が最新として話を進めるね🧩✨ (Node.js)
1. この章のゴール 🎯✨
読み終わったら、こうなれるよ👇😊
- ログを置く場所を迷わない(中心は静か/外側で観測)🧠🪟
- **相関ID(Correlation ID)**で「このリクエストのログどれ?」が一瞬で追える🪪🔎
- **構造化ログ(JSON)**で、あとから検索・集計しやすいログになる📦📊
- File保存やHTTP入口など、Adapterの“現場”に必要なログを入れられる🧩🚪💾
2. そもそも「観測(Observability)」って?👀✨
ざっくり“三本柱”があるよ👇
- Logs(ログ):起きたことの記録(今回の主役)🪵
- Metrics(メトリクス):回数・割合・時間などの数値📈
- Traces(トレース):1リクエストの旅路(どこで遅い?)🗺️
最初はログだけでOK!でも、相関IDを入れておくと将来トレースへつながる✨(あとで気持ちよく育つ🌱)
3. ログの種類を分けるとラク😊🪄
ログって全部同じに見えるけど、実は役割が違うよ👇
- アクセス系:HTTPの開始/終了、ステータス、時間など🚪🌐
- I/O系:ファイル読めない、DB遅い、外部API落ちた💾😵💫
- デバッグ系:開発中だけ見たい情報(本番では控えめ)🧪
- 監査(Audit)系:ビジネス的に「誰が何をした」📜(これは“仕様”寄り)
この章では アクセス系 + I/O系を中心にやるよ💪✨
4. ヘキサゴナルで「ログはどこに置く?」🧭🛡️
結論これ👇😊
✅ Adapterに置いていいログ
- HTTP入口:リクエスト開始/終了、ステータス、処理時間、相関ID 🚪🌐⏱️
- FileRepository:読み書きの失敗、リトライ判断、I/O時間 💾⏱️
- Composition Root:起動時の設定(どのRepoを使う等)🏗️✨
- 例外キャッチして 中心のエラーを“外側の表現”に変換したときのログ😵💫➡️🧩
❌ 中心(domain/app)に入れないログ
- 「タイトル空は禁止」みたいな 仕様の判定ログをベタベタ残す
- ユースケースの手順を “実況中継” するログ
- HTTP/ファイルの都合を中心へ持ち込むログ
中心は「静かに正しく判断して、結果(成功/失敗)を返す」だけが美しい🧠✨ ログは外側で「見える化」するのがヘキサゴナルの勝ち筋だよ🛡️
5. 相関IDってなに?🪪✨(超だいじ!)
相関IDは「この一連の処理をまとめて追うためのID」だよ😊 HTTPだと 1リクエストにつき1つ。CLIでも 1コマンド実行につき1つ、みたいに使う。
よくある運用
- リクエストに
x-request-idが来てたらそれを使う - 来てなかったらサーバー側で新規発行
- レスポンスヘッダにも同じIDを返す(ユーザーが問い合わせしやすい)🧾✨
さらに将来の分散トレースでは、標準の traceparent ヘッダを使う世界もあるよ🌐(W3C標準) (W3C)
OpenTelemetry などはこの文脈(Trace ID/Span ID)をログへ関連付ける考え方を持ってるよ📎✨ (OpenTelemetry)
6. 実装方針:Context(相関ID)を “勝手に” 取れるようにする🪄
ここが今日のキモ👇😊
- リクエストの最初で 相関IDをContextへ入れる
- どこでログしても 自動で相関IDが混ざる
- でも中心(domain/app)は一切知らない🧠✨
Node.js には AsyncLocalStorage があって、非同期の流れをまたいでも「今のリクエストのContext」を追えるよ。しかも Stable 扱い。 (Node.js)
(async_hooks の低レベルAPIより AsyncLocalStorage を推す流れもはっきりしてる) (Node.js)
7. ハンズオン:ToDoミニに “最小ログ基盤” を入れる😊🔧
ここから手を動かすよ〜!💻✨
(ファイル名は一例。domain/ app/ adapters/ の方針はそのまま🧩)
7.1 まずは logger を用意(構造化ログ)📦🪵
Nodeのログは JSONで出すのが後から強いよ💪
高速で定番の pino を使う例にするね(速い/JSON前提で扱いやすいと言われがち) (dash0.com)
// src/adapters/observability/logger.ts
import pino from "pino";
import { getCorrelationId } from "./requestContext";
export type Logger = {
info: (obj: object, msg?: string) => void;
warn: (obj: object, msg?: string) => void;
error: (obj: object, msg?: string) => void;
debug: (obj: object, msg?: string) => void;
};
export function createLogger(): Logger {
const base = pino({
level: process.env.LOG_LEVEL ?? "info",
});
// 毎回 correlationId を混ぜる薄いラッパ(中心に入れない!)
const withCid = (obj: object) => ({
correlationId: getCorrelationId(),
...obj,
});
return {
info: (obj, msg) => base.info(withCid(obj), msg),
warn: (obj, msg) => base.warn(withCid(obj), msg),
error: (obj, msg) => base.error(withCid(obj), msg),
debug: (obj, msg) => base.debug(withCid(obj), msg),
};
}
ポイント😊
Loggerは Port にしない(今回は 観測はAdapter側の都合だから)- “毎回IDを渡す” をやめる(渡し忘れ事故が起きる😇)
7.2 Context(相関ID)を入れる箱を作る🪪📦
// src/adapters/observability/requestContext.ts
import { AsyncLocalStorage } from "node:async_hooks";
type Store = { correlationId: string };
const als = new AsyncLocalStorage<Store>();
export function runWithCorrelationId<T>(correlationId: string, fn: () => T): T {
return als.run({ correlationId }, fn);
}
export function getCorrelationId(): string | undefined {
return als.getStore()?.correlationId;
}
ここで AsyncLocalStorage を使って、非同期をまたいでも correlationId を取れるようにしたよ✨ (Node.js)
7.3 HTTP Inbound Adapter で相関IDを作って入れる🚪🌐🪪
HTTPの入口でやることは超シンプル👇😊
- 相関IDを決める(ヘッダ優先、なければ新規)
- Context に入れる
- 開始ログ / 終了ログを出す(時間も!)
Express風の例:
// src/adapters/http/requestLoggingMiddleware.ts
import type { Request, Response, NextFunction } from "express";
import { randomUUID } from "node:crypto";
import { runWithCorrelationId } from "../observability/requestContext";
import type { Logger } from "../observability/logger";
export function requestLoggingMiddleware(logger: Logger) {
return (req: Request, res: Response, next: NextFunction) => {
const cid = (req.header("x-request-id") ?? randomUUID()).toString();
res.setHeader("x-request-id", cid);
const start = Date.now();
runWithCorrelationId(cid, () => {
logger.info(
{ event: "http_request_start", method: req.method, path: req.path },
"request start"
);
res.on("finish", () => {
logger.info(
{
event: "http_request_end",
method: req.method,
path: req.path,
statusCode: res.statusCode,
durationMs: Date.now() - start,
},
"request end"
);
});
next();
});
};
}
✅ これで、以後どこでログしても correlationId が勝手に付く✨ (中心は何も知らない🧠)
7.4 Outbound Adapter(FileRepository)に “現場ログ” を入れる💾🪵
I/O系は Adapter の責任範囲なので、ここはログを置いてOK😊
// src/adapters/repositories/FileTodoRepository.ts
import type { Logger } from "../observability/logger";
export class FileTodoRepository {
constructor(
private readonly filePath: string,
private readonly logger: Logger
) {}
async loadAll(): Promise<unknown[]> {
const start = Date.now();
try {
// ...ファイル読み込み処理(省略)
const data: unknown[] = [];
this.logger.info(
{ event: "repo_load_success", repo: "FileTodoRepository", durationMs: Date.now() - start },
"load ok"
);
return data;
} catch (e) {
this.logger.error(
{ event: "repo_load_fail", repo: "FileTodoRepository", err: toErr(e), durationMs: Date.now() - start },
"load failed"
);
throw e; // 外側でラップする設計ならそこでラップしてもOK
}
}
}
function toErr(e: unknown) {
if (e instanceof Error) return { name: e.name, message: e.message, stack: e.stack };
return { message: String(e) };
}
💡 コツ
eventを固定値にすると検索が超ラク(repo_load_failで一発)🔎- エラーは “全部文字列化” じゃなく、最低
name/message/stackを持たせると強い💪
7.5 Composition Root で logger を作って Adapter に渡す🏗️🧩
Composition Root は “合体場所” だから、ここで createLogger() して配るのがキレイ😊
// src/compositionRoot.ts
import { createLogger } from "./adapters/observability/logger";
import { FileTodoRepository } from "./adapters/repositories/FileTodoRepository";
export function buildApp() {
const logger = createLogger();
const todoRepo = new FileTodoRepository("data/todos.json", logger);
// HTTP adapter も CLI adapter も logger を受け取るようにする
return { logger, todoRepo };
}
8. ログに何を入れる?おすすめ最小フィールド📌✨
迷ったらこれでOK😊
event:固定イベント名(検索キー)🔎correlationId:相関ID(自動で付く)🪪adapter/repo/useCase:どこで起きた?🧩durationMs:遅い原因に効く⏱️err:失敗の中身(ただし秘匿情報は入れない)🔒
9. 逆に「入れちゃダメ」🙅♀️🔒
- アクセストークン、Cookie、生パスワード、秘密鍵 🔑💥
- 個人情報(メール、住所、電話)を丸ごと
- リクエストボディ丸ごと(必要なら“要約”や“マスク”)
「ログは便利だけど、漏れると事故」なので、ここは最初から慎重が勝ち😊✨
10. よくある失敗あるある😇(先に潰す)
- 相関IDがない → ログが増えたのに追えない地獄になる😵💫
- ログが文章だらけ(構造化されてない) → 検索・集計・可視化が弱い😢
- 中心でログしてしまう → じわじわ中心が汚れて設計が崩れる🧨
11. ミニ課題🎀📝(やると定着する!)
課題A:CLIにも相関IDを付けよう⌨️🪪
- コマンド開始時に UUID を作る
runWithCorrelationId()で包む- FileRepoのログにも同じIDが出てくるのを確認✨
課題B:遅い処理をあぶり出そう⏱️🔥
- FileRepoの読み込みにわざと
setTimeoutを挟む durationMsを見て「遅いのどこ?」が一瞬でわかるようにする
課題C:監査ログの“置き場所”を考えてみよう📜🤔
- 「完了にした」という“仕様イベント”は、ログじゃなく ドメインイベントとして設計したくなる
- そのイベントを 外側でログ化する、みたいな分離を妄想してみてね🧠✨
12. 今日のまとめ🎁💖
- ログは Adapterで出す(中心は静かに保つ)🧠🛡️
- 相関IDは 入口で作って、Contextで伝搬🪪🪄
- 構造化ログ(JSON)+
event固定名で 検索性爆上がり📦🔎 AsyncLocalStorageは request-scope context に使える(Stable)✨ (Node.js)- 将来は
traceparentや OpenTelemetry とも自然につながる🌐✨ (W3C)
次の章(AI活用)へ行く前に、もしよければ「今のToDoミニのHTTP入口は Express ?それとも別?」みたいな形に合わせて、あなたの構成にピッタリの差分パッチとして章内コードを整形して出せるよ😊🧩