1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TSの鬼 第16回:型安全ロギングとメトリクス—運用を型システムで守る

Posted at

はじめに

前回

実運用フェーズで重要になるのが ログメトリクス である。
しかし動的フォーマットでログを書き散らすと、後からクエリが壊れ、モニタリング基盤が破綻する。
本稿では TypeScript の型システムを用いて、開発時にフォーマットを保証しつつ、運用コストを下げる 設計を示す。


1. 型安全ロガーの設計方針

  1. スキーマを中央集約: 乱立させず、共通型に依存させる。
  2. enum でラベルを固定: as const でロガーキーをリテラル化。
  3. ジェネリクスでペイロード型を縛る: 不正キー・余分フィールドをコンパイルエラーに倒す。

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 型パターン) を扱い、エラーハンドリングをさらに高次元で統一する予定だ。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?