review-logging-patterns について
TypeScript・JavaScript プロジェクトのログを見直し、改善するスキルです。evlog を使った構造化イベント、エラーの文脈付け、カバレッジ確認、CI の回帰ゲートを案内します。
主な機能
- 01構造化ワイドイベントログ
- 02文脈付き構造化エラー
- 03evlog の設定とカバレッジ CLI
- 04CI のスコア・ベースラインゲート
- 05監査ログのガイダンス
活用シーン
- 01散在する console.log の置き換え
- 02プロジェクトのログ方針のレビュー
- 03エラー処理への文脈の追加
- 04CI でのログカバレッジ追跡
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ evloghq/ evlog --skill review-logging-patternsインストール後、Claude Code で/review-logging-patternsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/code-review.mddrain-pipeline.mdstructured-errors.mdwide-events.md詳細
ログパターンをレビューする
TypeScript/JavaScript のコードベースにおけるログパターンをレビューし、改善します。散在する console.logs を構造化ワイドイベントに変換し、汎用的なエラーを自己文書化された構造化エラーに変換します。
使用するタイミング
- 新規または既存のプロジェクト(サポート対象のあらゆるフレームワーク)で evlog をセットアップするとき
- ログのベストプラクティスに沿ってコードをレビューするとき
- console.log 文を構造化ログに変換するとき
- より適切なコンテキストを使ってエラーハンドリングを改善するとき
- ログのドレイン、サンプリング、エンリッチメントを設定するとき
クイックリファレンス
| 作業内容 | リソース |
|---|---|
| セットアップ(CLI) | evlog init:プロジェクトに evlog を組み込む |
| プロジェクトの規約(CLI) | evlog agents:プロジェクトの AGENTS.md に evlog ブロックを書き込む |
| カバレッジマップ(CLI) | evlog map:未計測のエントリーポイントをスコアリングする |
| CI ゲート(CLI) | evlog map --min-score / --baseline:回帰をゲートする |
| ワイドイベントのパターン | references/wide-events.md |
| エラーハンドリング | references/structured-errors.md |
| コードレビューチェックリスト | references/code-review.md |
| ドレインパイプライン | references/drain-pipeline.md |
| 監査ログ | build-audit-logs スキル + ドキュメント |
監査ログ
セキュリティ上重要なアクション(認証、請求、管理者操作、データエクスポート)には、別のロガーではなく、ワイドイベント上の型付き audit フィールドである evlog の監査レイヤーを使用してください。エンドツーエンドのセットアップ(log.audit、withAudit、拒否、auditEnricher、auditOnly、signed、mockAudit)については、build-audit-logs スキルを参照してください。
log.audit({
action: 'invoice.refund',
actor: { type: 'user', id: user.id },
target: { type: 'invoice', id: invoice.id },
outcome: 'success',
})
ドキュメント:https://www.evlog.dev/use-cases/audit/overview
インストール
npm install evlog
CLI を使用する(Nuxt、Nitro、Next.js、TanStack Start、Hono、Express、Fastify では推奨)
実行ファイル evlog は evlog パッケージに同梱されています:pnpm evlog、npx evlog、bunx evlog。インストール済みの場合は @evlog/cli を実行し、そうでない場合は必要に応じて取得するため(プロジェクトに依存関係は追加されません)、まだ何もないプロジェクトでも動作します。まだ初期段階ですが、試す価値があります。ディスク上のプロジェクトを読み取るため、通信も設定も不要です。サポート対象の 7 つのフレームワークでは、一連の作業全体をカバーします:evlog の組み込み(init)、カバレッジのスコアリング(map)、CI でのスコアの固定(--min-score、--baseline)。CLI が使用できない場合、フレームワークにアダプターがまだない場合、またはユーザーが辞退した場合は、以下の手動セクションに進んでください。このスキルは CLI に依存しません。何かをインストールする前に確認してください。
1. セットアップ:evlog init
まだ evlog を使用していないプロジェクトでは、セットアップを手書きするよりも init を優先してください。フレームワークを検出し、プロジェクトにすでにあるものを読み取り、設定、ドレイン、エンリッチャー、追加機能を一度に生成できるためです。エージェントから完全にスクリプトで実行できます:
# preview everything without writing (always start here)
npx evlog init --dry-run --yes
# then apply — flags instead of prompts
npx evlog init --yes \
--service my-app \
--drain fs \
--prodDrain axiom \
--extras enrichers,pipeline,sampling \
--sampling medium
便利なフラグ:--framework(検出結果を上書き:nuxt、nitro、next、tanstack-start、hono、express、fastify)、--prodDrain(カンマ区切り:axiom、otlp、posthog、sentry、better-stack、datadog、hyperdx、sazabi、loki、clickhouse)、--extras(enrichers、pipeline、sampling、vite、error-catalog、audit-catalog、ai、better-auth)、--enrichers、--sampling(トラフィック層:all、low、medium、high、very-high)、--apps(モノレポ:対象のワークスペースパッケージ)、--no-install。適用する前に、--dry-run の出力をユーザーと確認してください。ドキュメント:https://www.evlog.dev/cli/init
2. スコアを確認する:evlog map
npx evlog map --no-write
# agents: npx evlog map --json --no-write
確認できる内容:
- プロジェクトのスコアと、まだ未対応のエントリーポイント
- 最優先で修正:修正効果が最も高い3つの箇所
- さらに改善:スコアに影響しない改善機会(カタログ、監査カバレッジ、AI ロギング、認証 ID)
- ファイルごとの確認:
npx evlog map <file> --no-writeでハンドラーの実装イメージを確認できます - 修正後に再実行し、スコアの変化を確認できます
最優先で修正する項目を上から順に対応し、変更は最小限に抑え(useLogger()、log.set()、log.audit()、createError({ why, fix }))、その後 --no-write を付けて再実行してください。ユーザーが evlog.map.json の書き込みを希望する場合に限り、--no-write を省略してください。
3. CI で固定する:--min-score と --baseline
GitHub Actions では、evlog action(uses: evloghq/action@v1 と min-score の指定)を提案してください。このアクションはプルリクエストのベースを自動的にスキャンするため、マップファイルをコミットする必要はありません。また、差分と1件のコメントに回帰を表示します。以下のコマンドは、run: ステップで同じゲートを実行します。
修正後は、スコアを維持できるようにする方法を提案してください。ここで CLI が役立ちます。
# in CI, after pnpm add -D @evlog/cli (pinned by the lockfile; ask first)
pnpm exec evlog map --min-score 80 # absolute gate: exits 1 below the threshold
pnpm exec evlog map --baseline # ratchet: exits 1 if this PR made things worse
--baseline は、新しいスキャン結果をコミット済みの evlog.map.json と比較します。比較は エントリーポイントごと、要件ごと に行われるため、あるルートに計測を追加して別のルートを壊すリファクタリングは、合計スコアが変わらなくても失敗します。合格済みのチェックをコメントで無効にした場合も回帰とみなされます。新しく追加された未計測のルートは、失敗にはならず NEW AND DARK として表示されます。手順:evlog.map.json を一度コミットし、CI に --baseline を付けた実行を追加してから、意図的な変更を受け入れる場合は --baseline なしで map を再実行してください。ドキュメント:https://www.evlog.dev/cli/ci
プロジェクトに evlog.config.ts がある場合、CI ステップではなく、そちらにゲートを設定できます。map: { minScore: 80, baseline: true } を設定すると、引数なしの evlog map でも同じようにゲートが機能し、フラグを指定した場合はフラグが優先されます。同じ map ブロックで、すべてのエントリーポイントのチェックを無効にし(rules: { 'error-catalog': 'off' })、ファイルのグロブでエントリーポイントを対象外にできます(ignore)。CLI はファイルを実行せずにこれらの値を読み込むため、リテラルで指定する必要があります。ドキュメント:https://www.evlog.dev/cli/config
まだ初期段階のため、アダプターとルールは引き続き進化しており、リリースによってスコアが変わる場合があります。ドキュメント:https://www.evlog.dev/cli/map · ルール:https://www.evlog.dev/cli/rules
フレームワークのセットアップ
Nuxt
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
env: { service: 'my-app' },
include: ['/api/**'],
},
})
useLogger、log、parseError は自動インポートされます。createError は自動インポートされません。インポートせずに使うと h3 のものが解決され、why、fix、link が失われます。evlog からインポートしてください。
// server/api/checkout.post.ts — no imports needed
export default defineEventHandler(async (event) => {
const log = useLogger(event)
log.set({ user: { id: user.id, plan: user.plan } })
return { success: true }
})
ドレイン、エンリッチメント、サンプリング、リダクションは、アプリのルートにある evlog.config.ts に設定できます。このモジュールはサーバー上で設定を読み込み、nuxt.config.ts の evlog キーで上書きできます。ブラウザのロガーが読み込むのは evlog キーのみです。
// evlog.config.ts
import { defineEvlog } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'
export default defineEvlog({
service: 'my-app',
drain: createAxiomDrain(),
})
サーバープラグイン内の Nitro フックはファイルとの併用でも引き続き機能し、イベントは両方のドレインに送られます。
// server/plugins/evlog-drain.ts
import { createAxiomDrain } from 'evlog/axiom'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', createAxiomDrain())
})
クライアントのトランスポート(Vue プラグインで自動設定):
// nuxt.config.ts
evlog: {
transport: { enabled: true }, // logs sent to /api/_evlog/ingest
}
クライアント側では、コンポーネント内で log、setIdentity、clearIdentity が自動インポートされます。
Next.js
ステップ 1:中央設定を作成します。 すべてのエクスポートはここから行います。
// lib/evlog.ts
import type { DrainContext } from 'evlog'
import { createEvlog } from 'evlog/next'
import { createUserAgentEnricher, createRequestSizeEnricher } from 'evlog/enrichers'
import { createDrainPipeline } from 'evlog/pipeline'
const enrichers = [createUserAgentEnricher(), createRequestSizeEnricher()]
const pipeline = createDrainPipeline<DrainContext>({ batch: { size: 50, intervalMs: 5000 } })
const drain = pipeline(createAxiomDrain({ dataset: 'logs', apiKey: process.env.AXIOM_API_KEY! }))
export const { withEvlog, useLogger, log, createError } = createEvlog({
service: 'my-app',
sampling: {
rates: { info: 10 },
keep: [{ status: 400 }, { duration: 1000 }],
},
routes: {
'/api/auth/**': { service: 'auth-service' },
'/api/checkout/**': { service: 'checkout-service' },
},
keep: (ctx) => {
const user = ctx.context.user as { premium?: boolean } | undefined
if (user?.premium) ctx.shouldKeep = true
},
enrich: (ctx) => {
for (const enricher of enrichers) enricher(ctx)
},
drain,
})
ステップ 2:ルートハンドラーを withEvlog() でラップします。
// app/api/checkout/route.ts
import { withEvlog, useLogger } from '@/lib/evlog'
export const POST = withEvlog(async (request: Request) => {
const log = useLogger() // Zero arguments — uses AsyncLocalStorage
log.set({ user: { id: 'user_123', plan: 'enterprise' } })
log.set({ cart: { items: 3, total: 14999 } })
return Response.json({ success: true })
})
ステップ 3:サーバーアクション。 同じ withEvlog() ラッパーを使用します。
// app/actions.ts
'use server'
import { withEvlog, useLogger } from '@/lib/evlog'
export const checkout = withEvlog(async (formData: FormData) => {
const log = useLogger()
log.set({ action: 'checkout', source: 'server-action' })
return { success: true }
})
ステップ 4:ミドルウェア(任意。x-request-id とタイミングヘッダーを設定します)。
// proxy.ts
import { evlogMiddleware } from 'evlog/next'
export const proxy = evlogMiddleware()
export const config = { matcher: ['/api/:path*'] }
ステップ 5:クライアントプロバイダー。 ルートレイアウトをラップします。
// app/layout.tsx
import { EvlogProvider } from 'evlog/next/client'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<EvlogProvider service="my-app" transport={{ enabled: true, endpoint: '/api/evlog/ingest' }}>
{children}
</EvlogProvider>
</body>
</html>
)
}
ステップ 6:クライアント側のロギング。 任意のクライアントコンポーネントで使用します。
'use client'
import { log, setIdentity, clearIdentity } from 'evlog/next/client'
setIdentity({ userId: 'usr_123' })
log.info({ action: 'checkout_click' })
clearIdentity()
ステップ 7(任意):インストルメンテーション。 起動処理とグローバルな onRequestError(withEvlog の対象外にある SSR/RSC エラー用)。ルートの instrumentation.ts で defineNodeInstrumentation(() => import('./lib/evlog')) を使用すると、Node のみを対象にし、インポートをキャッシュできます。または、register/onRequestError を手動で記述します。どちらも有効です。独自のロジックを追加する場合は、lib/evlog.ts で evlog の register/onRequestError をラップし(独自の初期化処理やメトリクスと組み合わせて)、再エクスポートします。
createEvlog() と並べて、lib/evlog.ts から createInstrumentation() をエクスポートします。lockLogger との共存については、フレームワークのドキュメントを参照してください。
ステップ 8:クライアントの取り込みエンドポイント。 クライアントログを受け取ります。
// app/api/evlog/ingest/route.ts
import { NextRequest } from 'next/server'
const VALID_LEVELS = ['info', 'error', 'warn', 'debug'] as const
export async function POST(request: NextRequest) {
const origin = request.headers.get('origin')
const host = request.headers.get('host')
if (origin && new URL(origin).host !== host) {
return Response.json({ error: 'Invalid origin' }, { status: 403 })
}
const body = await request.json()
if (!body?.timestamp || !body?.level || !VALID_LEVELS.includes(body.level)) {
return Response.json({ error: 'Invalid payload' }, { status: 400 })
}
const { service: _, ...sanitized } = body
console.log('[CLIENT LOG]', JSON.stringify({ ...sanitized, service: 'my-app', source: 'client' }))
return new Response(null, { status: 204 })
}
SvelteKit
// src/hooks.server.ts
import { initLogger } from 'evlog'
import { createEvlogHooks } from 'evlog/sveltekit'
initLogger({ env: { service: 'my-app' } })
export const { handle, handleError } = createEvlogHooks()
ルートハンドラーでは event.locals.log から、呼び出しスタック内の任意の場所では useLogger() からロガーにアクセスできます。
// src/routes/api/users/[id]/+server.ts
import { json } from '@sveltejs/kit'
export const GET = ({ locals, params }) => {
locals.log.set({ user: { id: params.id } })
return json({ id: params.id })
}
import { useLogger } from 'evlog/sveltekit'
async function findUsers() {
const log = useLogger()
log.set({ db: { query: 'SELECT * FROM users' } })
}
drain、enrich、tail sampling を含む完全なパイプライン:
import { createAxiomDrain } from 'evlog/axiom'
export const { handle, handleError } = createEvlogHooks({
include: ['/api/**'],
drain: createAxiomDrain(),
enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
keep: (ctx) => {
if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
},
})
Nitro v3
// nitro.config.ts
import { defineConfig } from 'nitro'
import evlog from 'evlog/nitro/v3'
export default defineConfig({
modules: [evlog({ env: { service: 'my-api' } })],
})
// routes/api/checkout.post.ts
import { defineHandler } from 'nitro/h3'
import { useLogger } from 'evlog/nitro/v3'
export default defineHandler(async (event) => {
const log = useLogger(event)
log.set({ action: 'checkout' })
return { ok: true }
})
TanStack Start
TanStack Start は Nitro v3 を使用します。evlog をインストールし、nitro.config.ts を追加します。
// nitro.config.ts
import { defineConfig } from 'nitro'
import evlog from 'evlog/nitro/v3'
export default defineConfig({
experimental: { asyncContext: true },
modules: [evlog({ env: { service: 'my-app' } })],
})
エラーハンドリングミドルウェアを __root.tsx に追加します。
// src/routes/__root.tsx
import { createMiddleware } from '@tanstack/react-start'
import { evlogErrorHandler } from 'evlog/nitro/v3'
export const Route = createRootRoute({
server: {
middleware: [createMiddleware().server(evlogErrorHandler)],
},
})
nitro/context の useRequest() を使用してロガーにアクセスします。
import { useRequest } from 'nitro/context'
import type { RequestLogger } from 'evlog'
const req = useRequest()
const log = req.context.log as RequestLogger
log.set({ user: { id: 'user_123' } })
Nitro v2
// nitro.config.ts
import { defineNitroConfig } from 'nitropack/config'
import evlog from 'evlog/nitro'
export default defineNitroConfig({
modules: [evlog({ env: { service: 'my-api' } })],
})
ルートでは evlog/nitro から useLogger をインポートします。Nitro v2 と v3 では、アプリのルートにある evlog.config.ts がモジュールによって読み込まれ、evlog() に渡したオプションで上書きされます。
NestJS
// src/app.module.ts
import { Module } from '@nestjs/common'
import { EvlogModule } from 'evlog/nestjs'
@Module({
imports: [EvlogModule.forRoot()],
})
export class AppModule {}
EvlogModule.forRoot() は、@nestjs/platform-express と @nestjs/platform-fastify の両方でグローバルミドルウェアを登録します。コントローラーの req.log は、どちらのプラットフォームでも evlog ロガーです。Nest の Fastify アプリで evlog/fastify も登録しないでください。すべてのリクエストで wide event が 2 件出力されます。useLogger() を使用すると、任意のコントローラーやサービスからリクエストスコープのロガーにアクセスできます。
import { useLogger } from 'evlog/nestjs'
async function findUsers() {
const log = useLogger()
log.set({ db: { query: 'SELECT * FROM users' } })
}
drain、enrich、tail sampling を含む完全なパイプライン:
import { createAxiomDrain } from 'evlog/axiom'
EvlogModule.forRoot({
include: ['/api/**'],
drain: createAxiomDrain(),
enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
keep: (ctx) => {
if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
},
})
NestJS DI を使用した非同期設定には、forRootAsync() を使用します。
EvlogModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config) => ({
drain: createAxiomDrain({ apiKey: config.get('AXIOM_API_KEY') }),
}),
})
Express
import express from 'express'
import { initLogger } from 'evlog'
import { evlog, useLogger } from 'evlog/express'
initLogger({ env: { service: 'my-api' } })
const app = express()
app.use(evlog())
app.get('/api/users', (req, res) => {
req.log.set({ users: { count: 42 } })
res.json({ users: [] })
})
useLogger() を使用すると、req を渡さずに呼び出しスタック内の任意の場所からロガーにアクセスできます。
import { useLogger } from 'evlog/express'
async function findUsers() {
const log = useLogger()
log.set({ db: { query: 'SELECT * FROM users' } })
}
drain、enrich、tail sampling を含む完全なパイプライン:
import { createAxiomDrain } from 'evlog/axiom'
app.use(evlog({
include: ['/api/**'],
drain: createAxiomDrain(),
enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
keep: (ctx) => {
if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
},
}))
Hono
import { Hono } from 'hono'
import { initLogger } from 'evlog'
import { evlog, type EvlogVariables } from 'evlog/hono'
initLogger({ env: { service: 'my-api' } })
const app = new Hono<EvlogVariables>()
app.use(evlog())
app.get('/api/users', (c) => {
const log = c.get('log')
log.set({ users: { count: 42 } })
return c.json({ users: [] })
})
ハンドラーでは c.get('log') からロガーにアクセスできます。c が使えない下位レイヤー(サービス、リポジトリ)では、evlog/hono の useLogger() を使用します。どちらも同じロガーを返します。
import { useLogger } from 'evlog/hono'
async function findUsers() {
const log = useLogger()
log.set({ db: { query: 'SELECT * FROM users' } })
}
Cloudflare Workers では、useLogger() に nodejs_compat(または nodejs_als)互換性フラグが必要です。c.get('log') はフラグの有無にかかわらず動作します。
構造化エラー:createError() をスローし、app.onError 内で parseError() を使用して、parsed.status as ContentfulStatusCode を c.json() に渡します(Hono ではステータス引数の型が number ではなく ContentfulStatusCode です)。
import { createError, parseError } from 'evlog'
import type { ContentfulStatusCode } from 'hono/utils/http-status'
app.onError((error, c) => {
c.get('log').error(error)
const parsed = parseError(error)
return c.json(
{ message: parsed.message, why: parsed.why, fix: parsed.fix, link: parsed.link },
parsed.status as ContentfulStatusCode,
)
})
drain、enrich、tail sampling を含む完全なパイプライン:
import { createAxiomDrain } from 'evlog/axiom'
app.use(evlog({
include: ['/api/**'],
drain: createAxiomDrain(),
enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
keep: (ctx) => {
if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
},
}))
Fastify
import Fastify from 'fastify'
import { initLogger } from 'evlog'
import { evlog, useLogger } from 'evlog/fastify'
initLogger({ env: { service: 'my-api' } })
const app = Fastify({ logger: false })
await app.register(evlog)
app.get('/api/users', async (request) => {
request.log.set({ users: { count: 42 } })
return { users: [] }
})
request.log は evlog の wide-event ロガーです(リクエスト上では Fastify の組み込み pino ロガーをシャドーイングします)。Fastify の pino ロガーには fastify.log から引き続きアクセスできます。
useLogger() を使用すると、request を渡さずに呼び出しスタック内の任意の場所からロガーにアクセスできます。
import { useLogger } from 'evlog/fastify'
async function findUsers() {
const log = useLogger()
log.set({ db: { query: 'SELECT * FROM users' } })
}
drain、enrich、tail sampling を含む完全なパイプライン:
import { createAxiomDrain } from 'evlog/axiom'
await app.register(evlog, {
include: ['/api/**'],
drain: createAxiomDrain(),
enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
keep: (ctx) => {
if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
},
})
Elysia
import { Elysia } from 'elysia'
import { initLogger } from 'evlog'
import { evlog, useLogger } from 'evlog/elysia'
initLogger({ env: { service: 'my-api' } })
const app = new Elysia()
.use(evlog())
.get('/api/users', ({ log }) => {
log.set({ users: { count: 42 } })
return { users: [] }
})
.listen(3000)
useLogger() を使用すると、呼び出しスタック内の任意の場所からロガーにアクセスできます。
import { useLogger } from 'evlog/elysia'
async function findUsers() {
const log = useLogger()
log.set({ db: { query: 'SELECT * FROM users' } })
}
drain、enrich、tail sampling を含む完全なパイプライン:
import { createAxiomDrain } from 'evlog/axiom'
app.use(evlog({
include: ['/api/**'],
drain: createAxiomDrain(),
enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
keep: (ctx) => {
if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
},
}))
React Router
// react-router.config.ts
import type { Config } from '@react-router/dev/config'
export default {
future: {
v8_middleware: true,
},
} satisfies Config
// app/root.tsx
import { initLogger } from 'evlog'
import { evlog } from 'evlog/react-router'
initLogger({ env: { service: 'my-api' } })
export const middleware: Route.MiddlewareFunction[] = [
evlog(),
]
ローダーとアクションでは、context.get(loggerContext) でロガーにアクセスします。
// app/routes/api.users.$id.tsx
import { loggerContext } from 'evlog/react-router'
export async function loader({ params, context }: Route.LoaderArgs) {
const log = context.get(loggerContext)
log.set({ user: { id: params.id } })
return { users: [] }
}
useLogger() を使うと、コンテキストを渡さずにコールスタック内のどこからでもロガーにアクセスできます。
import { useLogger } from 'evlog/react-router'
async function findUsers() {
const log = useLogger()
log.set({ db: { query: 'SELECT * FROM users' } })
}
ドレイン、エンリッチ、テールサンプリングを含む完全なパイプライン:
import { createAxiomDrain } from 'evlog/axiom'
export const middleware: Route.MiddlewareFunction[] = [
evlog({
include: ['/api/**'],
drain: createAxiomDrain(),
enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
keep: (ctx) => {
if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
},
}),
]
oRPC
import { os } from '@orpc/server'
import { RPCHandler } from '@orpc/server/fetch'
import { initLogger } from 'evlog'
import { evlog, withEvlog, type EvlogOrpcContext } from 'evlog/orpc'
initLogger({ env: { service: 'my-rpc' } })
const base = os.$context<EvlogOrpcContext>().use(evlog())
const router = {
ping: base.handler(({ context }) => {
context.log.set({ pinged: true })
return { ok: true }
}),
}
const handler = withEvlog(new RPCHandler(router))
export default async function fetch(request: Request) {
const { matched, response } = await handler.handle(request, { prefix: '/rpc' })
return matched ? response : new Response('Not Found', { status: 404 })
}
withEvlog() はハンドラーをラップし、一致したリクエストごとに 1 つのワイドイベントを出力します。os.use(evlog()) は、base から派生するすべてのプロシージャで context.log を使えるようにし、operation(. で連結したプロシージャパス)でワイドイベントにタグを付けます。
ユーティリティモジュールからロガーにアクセスするには、useLogger() を使います。
import { useLogger } from 'evlog/orpc'
async function chargeCard(amount: number) {
const log = useLogger()
log.set({ payment: { amount } })
}
ドレイン、エンリッチ、テールサンプリングを含む完全なパイプライン:
import { createAxiomDrain } from 'evlog/axiom'
const handler = withEvlog(new RPCHandler(router), {
include: ['/rpc/**'],
drain: createAxiomDrain(),
enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
keep: (ctx) => {
if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
},
})
Effect
Effect 4(HttpRouter、HttpApi)。routerLayer はすべてのルートに evlog() ミドルウェアを設定し、EvlogLogger サービスを提供します。initLoggerLayer はレイヤーの構築時に initLogger を呼び出し、スコープの終了時に createDrainPipeline のドレインをフラッシュします。
import { createServer } from 'node:http'
import { NodeHttpServer, NodeRuntime } from '@effect/platform-node'
import { Effect, Layer } from 'effect'
import { HttpRouter, HttpServerResponse } from 'effect/http'
import { EvlogLogger, initLoggerLayer, routerLayer } from 'evlog/effect'
const Routes = HttpRouter.add('GET', '/api/users', Effect.gen(function* () {
const log = yield* EvlogLogger
log.set({ users: { count: 42 } })
return HttpServerResponse.jsonUnsafe({ users: [] })
}))
const Server = HttpRouter.serve(Routes.pipe(Layer.provide(routerLayer())), { disableLogger: true }).pipe(
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })),
Layer.provide(initLoggerLayer({ env: { service: 'my-api' } })),
)
NodeRuntime.runMain(Layer.launch(Server))
AsyncLocalStorage も log.fork() もありません。イベントは Effect コンテキストに格納されます。ハンドラーとサービスでは yield* EvlogLogger を使います(この要件は Effect の型に現れます)。型付きフィールドには yield* useLogger<Fields>() を使い、EvlogLogger を必要としないライブラリコードでは yield* set(fields) を使います(ユニット外では何もしません)。nodejs_compat なしで Cloudflare Workers 上で動作します。
import { Effect } from 'effect'
import { useLogger } from 'evlog/effect'
const chargeCard = (amount: number) => Effect.gen(function* () {
const log = yield* useLogger<{ payment: { amount: number } }>()
log.set({ payment: { amount } })
})
失敗は Cause から読み取ります(主な失敗は error、残りは error.suppressed に格納され、純粋な中断は status: 499 になります)。Effect のルーターは、失敗した createError({ status: 402 }) に対して 500 を返します。そのため、ハンドラー内で Effect.catch を使って捕捉し、log.error(error) を呼び出し、parseError(error) から構築した HttpServerResponse.jsonUnsafe(body, { status: parsed.status }) を返します。HttpApi はミドルウェアより前に宣言済みエラーをレスポンスとしてエンコードするため、イベントにはステータスが記録されますが、error フィールドは含まれません。
HTTP 以外の処理(ジョブ、キューコンシューマー、Durable Object のアラーム)では、.pipe(withLogger('action-name', options)) を使います。Durable Object の fetch では、.pipe(withRequestLogger(request, options)) を使います。Workers では、ドレインがレスポンスの終了後も継続するよう、リクエストごとに WaitUntil を渡します(HttpRouter.toWebHandler とともに handler(request, Context.make(WaitUntil, ctx.waitUntil.bind(ctx))) を使用します)。loggerLayer() は Effect.log* の呼び出しを現在のワイドイベントにまとめます。各ユニットは受信した traceparent を引き継ぎ、withTracePropagation(client) と traceHeaders は送信先への呼び出しにそれを転送します。
ドレイン、エンリッチ、テールサンプリングを含む完全なパイプライン:
import { createAxiomDrain } from 'evlog/axiom'
routerLayer({
include: ['/api/**'],
drain: createAxiomDrain(),
enrich: (ctx) => { ctx.event.region = process.env.FLY_REGION },
keep: (ctx) => {
if (ctx.duration && ctx.duration > 2000) ctx.shouldKeep = true
},
})
Cloudflare Workers
import { initWorkersLogger, withEvlog } from 'evlog/workers'
initWorkersLogger({ env: { service: 'edge-api' } })
export default withEvlog(async (request, _env, _ctx, log) => {
log.set({ action: 'handle_request' })
return Response.json({ ok: true })
})
withEvlog はハンドラーの戻り時にリクエストごとに 1 つのワイドイベントを出力するため、手動で log.emit() を呼び出す必要はありません。非同期ドレインは waitUntil に登録され、レスポンス後も継続します。ストリーミングレスポンスでは、本文の処理が完了するまでイベントの出力が保留されます。requestId は x-request-id から取得され(フォールバックは cf-ray)、method、path、cf-ray、traceparent、および request.cf の安全なサブセットは自動的に取得されます。他のすべての統合と同じオプション(drain、enrich、keep、include、exclude、routes)を受け付けます。手動で制御する場合(スケジュールハンドラー、キュー)は、引き続き createWorkersLogger(request) と log.emit() を使用できます。Workers では ALS ベースの useLogger() は使えないため、log を明示的に渡してください。
AWS Lambda
Lambda には HTTP ミドルウェアのライフサイクルがないため、evlog はスタンドアロンの TypeScript と同様に動作します。ただし、重要なルールが 1 つあります。呼び出しごとにロガーを 1 つ作成し、モジュールレベルで共有しないでください(Lambda は実行環境を再利用するため、共有インスタンスを使うと呼び出し間でフィールドが漏洩します)。
import { initLogger, createLogger } from 'evlog'
initLogger({ env: { service: 'my-fn' } }) // once at module load (cold start)
export async function handler(event: SQSEvent) {
for (const record of event.Records) {
const log = createLogger({ messageId: record.messageId })
try {
log.set({ queue: { source: record.eventSourceARN } })
await processMessage(record)
} catch (error) {
log.error(error as Error)
throw error
} finally {
log.emit()
}
}
}
Astro
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware'
import { initLogger, createRequestLogger } from 'evlog'
initLogger({ env: { service: 'my-astro-app' } })
export const onRequest = defineMiddleware(async ({ request, locals }, next) => {
const url = new URL(request.url)
const log = createRequestLogger({ method: request.method, path: url.pathname })
locals.log = log
try {
const response = await next()
log.emit()
return response
} catch (error) {
log.error(error instanceof Error ? error : new Error(String(error)))
log.emit()
throw error
}
})
src/env.d.ts で locals.log に型を指定します(interface Locals { log: RequestLogger })。自動インポートとビルド時の DX のために、Vite プラグイン(以下)と組み合わせてください。
Vite プラグイン(Vite ベースの任意のフレームワーク)
Vite ベースのプロジェクト(SvelteKit、Astro、SolidStart、React+Vite など)では、自動初期化、自動インポート、ビルド時の機能を利用するために Vite プラグインを使用します。
// vite.config.ts
import evlog from 'evlog/vite'
export default defineConfig({
plugins: [
evlog({
service: 'my-app',
autoImports: true, // auto-import log, createEvlogError, parseError
strip: ['debug'], // remove log.debug() in production
sourceLocation: true, // inject file:line in dev + prod
client: { // client-side logging
transport: { endpoint: '/api/logs' },
},
}),
],
})
サーバー側のミドルウェア(drain、enrich、keep、routes)は、引き続きフレームワークの統合部分で設定します(例:Hono/Express/SvelteKit の evlog() ミドルウェア)。Vite プラグインが処理するのは、ビルド時の DX のみです。
スタンドアロン TypeScript
import { initLogger, createRequestLogger } from 'evlog'
initLogger({ env: { service: 'my-worker', environment: 'production' } })
const log = createRequestLogger({ jobId: job.id })
log.set({ source: job.source, recordsSynced: 150 })
log.emit() // Manual emit required in standalone
設定オプション
すべてのオプションは、Nuxt(evlog キー)、Nitro(evlog() に渡す)、Next.js(createEvlog())、スタンドアロン(initLogger())で使用できます。
また、defineEvlog() を使って記述した evlog.config.ts に設定することもできます。Nuxt と Nitro はこのファイルを自動で読み込み、モジュールのオプションがその内容を上書きします。他のフレームワークでは、このファイルをインポートして、toLoggerConfig(config) を initLogger() に、toMiddlewareOptions(config) をミドルウェアに渡します。evlog init はファイルを作成し、evlog config は解決後の設定を出力します。また、extends を使うと、アプリ間でベース設定を共有できます(1 階層)。ドキュメント:https://www.evlog.dev/cli/config
| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
env.service / service | string | 'app' | ログに表示するサービス名 |
enabled | boolean | true | グローバルな切り替え(false の場合は何もしません) |
pretty | boolean | 開発時は true | 見やすいツリー形式と JSON 形式を切り替えます |
silent | boolean | false | コンソールへの出力を抑制します。イベントは引き続き drain に送信されます |
include | string[] | すべてのルート | ログ対象のルート glob パターン |
exclude | string[] | なし | 除外するルートパターン(include より優先されます) |
routes | Record<string, { service }> | -- | ルートごとのサービス名 |
minLevel | 'debug' | 'info' | 'warn' | 'error' | 'debug' | グローバルな log API とクライアントの log に適用される固定しきい値(リクエスト全体のイベントには適用されません)。リクエストの出力量を確率的に調整するには sampling.rates を使用します |
sampling.rates | object | -- | ヘッドサンプリング:{ info: 10, warn: 50 }(0〜100%) |
sampling.keep | array | -- | テールサンプリング:[{ status: 400 }, { duration: 1000 }] |
drain | (ctx) => void | -- | drain コールバック(Next.js、スタンドアロン) |
enrich | (ctx) => void | -- | enrich コールバック(Next.js) |
keep | (ctx) => void | -- | カスタムテールサンプリングコールバック(Next.js) |
redact | boolean | RedactConfig | 本番環境では true | 本番環境ではデフォルトで有効です。無効にするには false を指定します。細かな制御にはオブジェクトを指定します |
Nitro フック(Nuxt、Nitro v2/v3)
| フック | タイミング | 用途 |
|---|---|---|
evlog:drain | enrich の後 | 外部サービスにイベントを送信します |
evlog:enrich | emit の後、コンソール出力と drain の前 | 派生コンテキストを追加します |
evlog:emit:keep | emit 中 | カスタムテールサンプリングロジック |
close | サーバーのシャットダウン時 | drain パイプラインのバッファーをフラッシュします |
Drain アダプター
| アダプター | インポート | 環境変数 |
|---|---|---|
| Axiom | evlog/axiom | AXIOM_API_KEY、AXIOM_DATASET |
| OTLP | evlog/otlp | OTLP_ENDPOINT(または標準の OTEL_EXPORTER_OTLP_[LOGS_]ENDPOINT、_HEADERS、_COMPRESSION、_PROTOCOL、OTEL_RESOURCE_ATTRIBUTES)。protocol: 'http/protobuf' はバイナリ protobuf を送信します。semanticConventions: true は OTel 属性名(http.*、exception.*、gen_ai.*)を追加します |
| HyperDX | evlog/hyperdx | HYPERDX_API_KEY(任意の HYPERDX_OTLP_ENDPOINT。デフォルトは https://in-otel.hyperdx.io) |
| Sazabi | evlog/sazabi | SAZABI_INTAKE_URL(ログストリームの取り込み URL。キーは URL に含まれます)、任意の SAZABI_SERVICE_NAME |
| Grafana Loki | evlog/loki | LOKI_ENDPOINT、任意の LOKI_API_KEY、LOKI_USER |
| ClickHouse | evlog/clickhouse | CLICKHOUSE_ENDPOINT(evlog_events テーブルが必要です)、任意の CLICKHOUSE_USER、CLICKHOUSE_PASSWORD、CLICKHOUSE_DATABASE、CLICKHOUSE_TABLE |
| PostHog | evlog/posthog | POSTHOG_API_KEY、POSTHOG_HOST |
| Sentry | evlog/sentry | SENTRY_DSN |
| Better Stack | evlog/better-stack | BETTER_STACK_API_KEY |
| Datadog | evlog/datadog | DD_API_KEY または DATADOG_API_KEY、任意の DD_SITE / DATADOG_LOGS_URL |
| Grafana Loki | evlog/loki | LOKI_ENDPOINT、任意の LOKI_API_KEY + LOKI_USER(Grafana Cloud)または LOKI_TENANT_ID(マルチテナント) |
| ClickHouse | evlog/clickhouse | CLICKHOUSE_ENDPOINT、任意の CLICKHOUSE_USER / CLICKHOUSE_PASSWORD / CLICKHOUSE_DATABASE / CLICKHOUSE_TABLE |
| ファイルシステム | evlog/fs | なし(ローカルファイルシステム) |
| メモリ | evlog/memory | なし(プロセス内リングバッファ。任意の EVLOG_MEMORY_STORE、EVLOG_MEMORY_MAX_EVENTS)。readMemoryLogs() で読み戻せます。エージェントがクエリできる開発専用ログエンドポイントに最適です |
| NuxtHub | @evlog/nuxthub(別パッケージ、Nuxt モジュール) | なし:保持期間に基づくクリーンアップを行い、NuxtHub データベースにワイドイベントを保存します(モジュールのオプションで evlog.retention: '7d' を設定します。d/h/m を受け付けます) |
| HTTP(ブラウザーからの取り込み) | evlog/http | なし(コード内で endpoint を設定します)。evlog/browser は非推奨です。同じ API ですが、次のメジャーバージョンで削除されます |
正規の環境変数名(例:AXIOM_API_KEY、BETTER_STACK_API_KEY)を使用してください。同じ名前をすべてのフレームワークで使用できます。
フレームワークごとのセットアップパターン:
// Nuxt/Nitro: server/plugins/evlog-drain.ts
import { createAxiomDrain } from 'evlog/axiom'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', createAxiomDrain())
})
// Hono / Express / Elysia: pass drain in middleware options
import { createAxiomDrain } from 'evlog/axiom'
app.use(evlog({ drain: createAxiomDrain() }))
// Fastify: pass drain in plugin options
import { createAxiomDrain } from 'evlog/axiom'
await app.register(evlog, { drain: createAxiomDrain() })
// NestJS: pass drain in module options
import { createAxiomDrain } from 'evlog/axiom'
EvlogModule.forRoot({ drain: createAxiomDrain() })
// Next.js: pass drain to createEvlog()
import { createAxiomDrain } from 'evlog/axiom'
import { createDrainPipeline } from 'evlog/pipeline'
const pipeline = createDrainPipeline<DrainContext>({ batch: { size: 50 } })
const drain = pipeline(createAxiomDrain())
// then: createEvlog({ ..., drain })
// Standalone: pass drain to initLogger()
initLogger({ env: { service: 'my-app' }, drain: createAxiomDrain() })
バッチ処理、再試行、バッファーのオーバーフロー設定については、references/drain-pipeline.md を参照してください。
エンリッチャー
組み込みのエンリッチャー:evlog/enrichers の createUserAgentEnricher()、createGeoEnricher()、createRequestSizeEnricher()、createTraceContextEnricher()。それぞれ { overwrite?: boolean } を受け付けます(デフォルトは false)。createDefaultEnrichers() を使用すると、4 つすべてを 1 回の呼び出しで組み合わせられます。
import { createDefaultEnrichers } from 'evlog/enrichers'
app.use(evlog({ enrich: createDefaultEnrichers() }))
// Nuxt/Nitro: server/plugins/evlog-enrich.ts
import { createUserAgentEnricher, createGeoEnricher } from 'evlog/enrichers'
export default defineNitroPlugin((nitroApp) => {
const enrichers = [createUserAgentEnricher(), createGeoEnricher()]
nitroApp.hooks.hook('evlog:enrich', (ctx) => {
for (const enricher of enrichers) enricher(ctx)
})
})
// Next.js: in lib/evlog.ts
createEvlog({
enrich: (ctx) => {
for (const enricher of enrichers) enricher(ctx)
ctx.event.region = process.env.VERCEL_REGION
},
})
自動レダクション(個人情報保護)
組み込みのレダクション機能は、コンソールへの出力前および drain がデータを受け取る前に、ワイドイベントから機密データを除去します。本番環境ではデフォルトで有効(NODE_ENV === 'production')で、開発環境では無効です。デバッグに十分な情報を残す、スマートな部分マスキングを使用します。
// Disable in production (opt-out)
evlog: { redact: false }
// Add custom paths on top of built-ins
evlog: {
redact: {
paths: ['user.password', 'headers.authorization'],
}
}
// Only specific built-ins
evlog: {
redact: {
builtins: ['email', 'creditCard'],
}
}
// No built-ins, only custom (uses flat [REDACTED] replacement)
evlog: {
redact: {
builtins: false,
paths: ['user.ssn'],
patterns: [/SECRET_\w+/g],
}
}
組み込みパターンとスマートマスキングの出力:
| パターン | 入力例 | マスキング後の出力 |
|---|---|---|
creditCard | 4111111111111111 | ****1111 |
email | [email protected] | a***@***.com |
ipv4 | 192.168.1.100 | ***.***.***.100 |
phone | +33 6 12 34 56 78 | +33 ****5678 |
jwt | eyJhbGciOi... | eyJ***.*** |
bearer | Bearer sk_live_abc... | Bearer *** |
iban | FR76 3000 6000 ...189 | FR76****189 |
すべてのフレームワークで動作します:Nuxt(evlog 設定)、Nitro(evlog() モジュールオプション)、Next.js(createEvlog())、スタンドアロン(initLogger())、およびすべてのミドルウェア統合(Hono、Express、Fastify、Elysia、NestJS)。
AI SDK の統合
Vercel AI SDK からトークン使用量、ツール呼び出し、モデル情報、ストリーミングのメトリクス、ツール実行時間、コスト推定、埋め込みのメタデータを取得し、ワイドイベントに記録します。evlog/ai からインポートします。ピア依存関係として ai >=6.0.168 <8.0.0 が必要です。
基本的なセットアップ(ミドルウェア)
import { createAILogger } from 'evlog/ai'
const log = useLogger(event) // or any RequestLogger
const ai = createAILogger(log)
const result = streamText({
model: ai.wrap('anthropic/claude-sonnet-4.6'), // accepts string or model object
messages,
})
ai.wrap() はモデルミドルウェアを使って、すべての LLM 呼び出しを透過的に取得します。generateText、streamText、ToolLoopAgent で動作します。
テレメトリーの統合(より詳細な可観測性)
ツールの実行時間、成功・失敗の追跡、生成処理全体の経過時間を取得するには、createEvlogIntegration() を追加します。
import { createAILogger, createEvlogIntegration } from 'evlog/ai'
const ai = createAILogger(log)
const agent = new ToolLoopAgent({
model: ai.wrap('anthropic/claude-sonnet-4.6'),
tools: { searchWeb, queryDatabase },
stopWhen: stepCountIs(5),
telemetry: {
integrations: [createEvlogIntegration(ai)],
},
})
これにより、ワイドイベントに ai.tools(ツールごとの { name, durationMs, success, error? })と ai.totalDurationMs が追加されます。
埋め込み
const { embedding, usage } = await embed({ model: embeddingModel, value: query })
ai.captureEmbed({ usage, model: 'text-embedding-3-small', dimensions: 1536 })
embedMany の場合は、バッチ数を渡します。
ai.captureEmbed({ usage, model: 'text-embedding-3-small', count: documents.length })
コスト推定
料金マップを渡すと、ワイドイベントに ai.estimatedCost が追加されます。
const ai = createAILogger(log, {
cost: {
'claude-sonnet-4.6': { input: 3, output: 15 },
'gpt-4o': { input: 2.5, output: 10 },
},
})
ワイドイベントの ai フィールド
以下が含まれます:calls、model、provider、inputTokens、outputTokens、totalTokens、cacheReadTokens、reasoningTokens、finishReason、toolCalls、steps、msToFirstChunk、msToFinish、tokensPerSecond、error、tools(テレメトリー統合経由)、totalDurationMs(テレメトリー統合経由)、embedding(captureEmbed 経由)、estimatedCost(cost オプション経由)。
検出すべきアンチパターン:
| アンチパターン | 修正方法 |
|---|---|
onFinish での手動トークン追跡 | ai.wrap():ミドルウェアが自動的に取得します |
console.log('tokens:', result.usage) | ai.wrap():ワイドイベントに構造化された ai.* フィールドを記録します |
| AI の可観測性がない | createAILogger(log) と ai.wrap() を追加します |
| ツールの実行時間を追跡していない | telemetry.integrations に createEvlogIntegration(ai) を追加します |
| コストの手動計算 | createAILogger() の cost オプションを使います |
関連機能(参考)
レビューで該当するニーズが見つかった場合は、これらを推奨してください。それぞれの詳細なドキュメントは evlog.dev にあります。
| 見つかったニーズ | evlog の解決策 | ドキュメント |
|---|---|---|
| イベントに認証済みユーザーの ID がない(Better Auth アプリ) | evlog/better-auth:identifyUser()、createAuthMiddleware()、クライアント ID の同期 | https://www.evlog.dev/use-cases/better-auth/overview |
| コードベース全体で、その場しのぎのフィールド名にばらつきがある | 型付きフィールドとエラー/監査カタログ(evlog/catalog) | https://www.evlog.dev/learn/typed-fields · https://www.evlog.dev/learn/catalogs |
| 横断的なフック(リクエストの開始/終了、クライアントログ、ロガーの拡張) | プラグイン:definePlugin | https://www.evlog.dev/extend/plugins |
| 開発中にログをリアルタイムで追跡する/ログビューアーを構築する | createStreamDrain(evlog/stream、SSE)と readFsLogs / tailFsLogs(evlog/fs) | https://www.evlog.dev/extend/stream |
| 開発中にエージェントが HTTP 経由でログを照会する必要がある | メモリーアダプターと、開発専用エンドポイントの背後に置く readMemoryLogs() | https://www.evlog.dev/integrate/adapters/self-hosted/memory |
構造化エラー
import { createError } from 'evlog' // required in Nuxt too: a bare createError is h3's
// Minimal
throw createError({ message: 'Database connection failed', status: 500 })
// Standard
throw createError({ message: 'Payment failed', status: 402, why: 'Card declined by issuer' })
// Complete
throw createError({
message: 'Payment failed',
status: 402,
why: 'Card declined by issuer - insufficient funds',
fix: 'Please use a different payment method or contact your bank',
link: 'https://docs.example.com/payments/declined',
cause: originalError,
})
// Backend-only context (wide events / drains — never HTTP body or parseError())
throw createError({
message: 'Not allowed',
status: 403,
why: 'Insufficient permissions',
internal: { correlationId: 'req_abc', resourceId: 'proj_123' },
})
フロントエンド:parseError() でユーザー向けフィールドを抽出します(internal がクライアントに返されることはありません)。
import { parseError } from 'evlog'
const error = parseError(err)
// error.message, error.status, error.why, error.fix, error.link
一般的なパターンとテンプレートについては、references/structured-errors.md を参照してください。
検出すべきアンチパターン
| アンチパターン | 修正方法 |
|---|---|
1つの関数内に複数の console.log がある | log.set() を使って単一のワイドイベントにまとめます |
throw new Error('...') | throw createError({ message, status, why, fix }) に置き換えます |
console.error(e); throw e | log.error(e); throw createError(...) に置き換えます |
| リクエストハンドラーにログがない | useLogger(event) / useLogger() / createRequestLogger() を追加します |
フラットなログデータ { uid, n, t } | グループ化されたオブジェクトを使います:{ user: {...}, cart: {...} } |
機密データをログに記録する log.set({ user: body }) | 明示的なフィールドを使います:{ user: { id: body.id, plan: body.plan } } + redact: true を有効にします |
サポート専用の ID を why / message に含める | ユーザーに表示しない診断情報には createError({ ..., internal: { ... } }) を使います |
チェックリストの全項目については、references/code-review.md を参照してください。
リファレンスファイルの読み込み
作業内容に応じて読み込み、一度にすべて読み込まないでください:
- ワイドイベントの設計 → references/wide-events.md
- エラーの改善 → references/structured-errors.md
- コードレビュー全般 → references/code-review.md
- Drain パイプラインのセットアップ → references/drain-pipeline.md