はじめに
前回
実運用フェーズで重要になるのが ログ と メトリクス である。
しかし動的フォーマットでログを書き散らすと、後からクエリが壊れ、モニタリング基盤が破綻する。
本稿では TypeScript の型システムを用いて、開発時にフォーマットを保証しつつ、運用コストを下げる 設計を示す。
1. 型安全ロガーの設計方針
- スキーマを中央集約: 乱立させず、共通型に依存させる。
-
enum でラベルを固定:
as const
でロガーキーをリテラル化。 - ジェネリクスでペイロード型を縛る: 不正キー・余分フィールドをコンパイルエラーに倒す。
1.1 例:イベント定義
export const LogEvents = {
USER_LOGIN: "USER_LOGIN",
ARTICLE_PUBLISH: "ARTICLE_PUBLISH",
API_ERROR: "API_ERROR",
} as const;
export type LogEvent = typeof LogEvents[keyof typeof LogEvents];
2. Zod スキーマでペイロードを定義
import { z } from "zod";
const payloadSchemas = {
USER_LOGIN: z.object({ userId: z.number(), method: z.enum(["password", "oauth"]) }),
ARTICLE_PUBLISH: z.object({ articleId: z.number(), editorId: z.number() }),
API_ERROR: z.object({ url: z.string().url(), status: z.number() }),
} as const;
- キーとスキーマを 1:1 で対応させる。
-
as const
によりオブジェクト自体が読み取り専用リテラル型になる。
3. ジェネリック Logger 関数
function log<E extends LogEvent>(event: E, data: z.infer<(typeof payloadSchemas)[E]>) {
const validated = payloadSchemas[event].parse(data);
console.log(JSON.stringify({ event, ...validated }));
}
-
event
に応じてdata
型が自動で切り替わる。 - スキーマ
parse()
でランタイム検証も実施。
3.1 使用例
log("USER_LOGIN", { userId: 42, method: "oauth" }); // ✅
// log("USER_LOGIN", { userId: "oops", method: "oauth" }); // ❌ 型エラー
4. OpenTelemetry との統合
import { metrics } from "@opentelemetry/api";
const meter = metrics.getMeter("ts-oni-app");
const loginCounter = meter.createCounter("user_login_total", {
description: "Total number of user logins",
});
function logAndMetric<E extends LogEvent>(event: E, data: z.infer<(typeof payloadSchemas)[E]>) {
log(event, data);
if (event === "USER_LOGIN") {
loginCounter.add(1, { method: data.method });
}
}
- メトリクス名とタグ をリテラル型で固定し、タイポを防止。
- OpenTelemetry Exporter(Prometheus / OTLP)側も型安全に統一できる。
5. 型安全クエリ生成(BigQuery 例)
type UserLoginRow = {
event: "USER_LOGIN";
userId: number;
method: "password" | "oauth";
timestamp: string;
};
function toSQL<T extends UserLoginRow>() {
// コンパイル時に列名が保証される
return `SELECT userId, method FROM logs WHERE event = 'USER_LOGIN'`;
}
- 行型を定義しておくことで、クエリビルダーが列名を補完できる。
6. 落とし穴と対策
落とし穴 | 原因 | 対策 |
---|---|---|
スキーマと実装が乖離 | 定義箇所が分散 | Barrel export で 1 箇所にまとめる |
イベント増加で Switch 地獄 | if/switch の分岐が肥大 | マッピングオブジェクトとリテラル型で解消 |
ログ肥大化 | 冗長ペイロード | スキーマで最小限の必須フィールドに限定 |
まとめ
-
as const
+ Zod で イベント名とペイロード型 を完全に一致させる。 - ジェネリック Logger が 不正フィールドをコンパイル時に排除。
- OpenTelemetry との統合で メトリクスも型安全 に計測可能。
これにより 開発 → 運用 の全フェーズで型システムが守りを固める。
次回は 型安全なフロー制御(Async/Result 型パターン) を扱い、エラーハンドリングをさらに高次元で統一する予定だ。