review-logging-patterns — скилл для Claude Code: ревью логирования
Скилл разбирает логирование в TypeScript-проекте: находит console.log-спам, ошибки без контекста и предлагает перейти на evlog.
npx skills add evloghq/evlog --skill review-logging-patterns -a claude-codeЧто делает review-logging-patterns
Скилл проходит по коду и ищет разбросанные console.log, ошибки без структуры и места, где в лог не попадает контекст. Вместо этого он предлагает wide events и структурированные ошибки из библиотеки evlog. Работает с Nuxt, Nitro, Next.js, TanStack Start, Hono, Express и Fastify, а подсказки по настройке даёт ещё для SvelteKit, React Router, NestJS, Elysia, oRPC, Effect, Cloudflare Workers, AWS Lambda, Astro и standalone TypeScript.
Внутри — справочники по wide events, структурированным ошибкам, чек-лист код-ревью и пайплайн drain. Отдельно разобран аудит-слой для чувствительных действий: типизированное поле audit на wide events, а не второй логгер. Есть адаптеры для Axiom, OTLP, HyperDX, Sazabi, PostHog, Sentry, Better Stack, Datadog, Loki, ClickHouse, NuxtHub и Memory, плюс сэмплирование, enrichers и интеграция с AI SDK.
Если ставить CLI, то сначала ставьте сам пакет: npm install evlog. Исполняемый файл evlog идёт в комплекте, запускается через npx evlog и при отсутствии @evlog/cli подтягивает его на ходу, не добавляя зависимость в проект. Скилл просит спросить перед установкой чего-либо — это повод не запускать init вслепую, а сперва посмотреть --dry-run. Без CLI скилл тоже работает: ручные разделы никуда не деваются.
Какие задачи решает
- найти console.log-спам по всему проекту
- превратить разрозненные логи в структурированные wide events
- добавить контекст в ошибки через
createError({ why, fix }) - подключить drain в Axiom, Datadog или Sentry
- закрепить покрытие логированием в CI через
--min-score
Примеры запросов агенту
- проверь логирование в этом проекте и покажи, что переписать под evlog
- настрой evlog через
npx evlog init --dry-run --yes, покажи, что изменится - прогони
npx evlog map --no-writeи разбери FIX FIRST
Как установить review-logging-patterns
1Через Skills CLI (Vercel)
npx skills add evloghq/evlog --skill review-logging-patterns -a claude-code2Вручную: положить файлы
~/.claude/skills/review-logging-patterns/SKILL.mdПапка скилла целиком; для одного проекта — .claude/skills/review-logging-patterns/
1Через Skills CLI (Vercel)
npx skills add evloghq/evlog --skill review-logging-patterns -a codex2Вручную: положить файлы
.agents/skills/review-logging-patterns/SKILL.mdДля всех проектов — ~/.agents/skills/
1Через Skills CLI (Vercel)
npx skills add evloghq/evlog --skill review-logging-patterns -a cursor2Вручную: положить файлы
.cursor/skills/review-logging-patterns/SKILL.mdCursor читает и .agents/skills/, и .claude/skills/
1GitHub CLI
gh skill install evloghq/evlog review-logging-patterns2Вручную: положить файлы
.github/skills/review-logging-patterns/SKILL.md1Через Skills CLI (Vercel)
npx skills add evloghq/evlog --skill review-logging-patterns -a gemini-cli2Командой платформы
gemini skills install https://github.com/evloghq/evlog --path skills/review-logging-patternsВручную: положить файлы
.devin/skills/review-logging-patterns/SKILL.mdСтарый путь .windsurf/skills/ тоже читается
Через Skills CLI (Vercel)
npx skills add evloghq/evlog --skill review-logging-patterns -a opencodeClaude.ai
Упакуйте папку в zip и загрузите в Settings → Capabilities → Skills
По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов
Похожие
Рядом в теме «Код-ревью и рефакторинг»
Всё по теме «Код-ревью и рефакторинг» →Вопросы о review-logging-patterns
Как установить review-logging-patterns в Claude?
Выполните npx skills add evloghq/evlog --skill review-logging-patterns -a claude-code. Скилл окажется в ~/.claude/skills/review-logging-patterns/SKILL.md.
Что делает review-logging-patterns?
Проверяет код на паттерны логирования и предлагает переход на evlog: wide events, структурированные ошибки, drain-адаптеры, сэмплирование и enrichers.
Нужен ли evlog CLI для работы скилла?
Нет. CLI удобен для init, map и гейта в CI, но если он недоступен или адаптера под фреймворк ещё нет, скилл продолжает работу по ручным разделам.
Исходный текст
SKILL.md · MIT
---
name: review-logging-patterns
description: Review code for logging patterns and suggest evlog adoption. Optionally use the evlog CLI, shipped with the evlog package (`evlog init` to wire evlog, `evlog agents` to write the conventions into AGENTS.md, `evlog map` to score entry-point coverage, `--baseline` to gate regressions in CI) on Nuxt, Nitro, Next.js, TanStack Start, Hono, Express, and Fastify. Guides setup on those plus SvelteKit, React Router, NestJS, Express, Fastify, Elysia, oRPC, Effect, Cloudflare Workers, AWS Lambda, Astro, and standalone TypeScript. Detects console.log spam, unstructured errors, and missing context. Covers wide events, structured errors, drain adapters (Axiom, OTLP, HyperDX, Sazabi, PostHog, Sentry, Better Stack, Datadog, Loki, ClickHouse, NuxtHub, Memory), sampling, enrichers, and AI SDK integration.
license: MIT
metadata:
author: HugoRCD
version: "0.9"
---
# Review logging patterns
Review and improve logging patterns in TypeScript/JavaScript codebases. Transform scattered console.logs into structured wide events and convert generic errors into self-documenting structured errors.
## When to Use
- Setting up evlog in a new or existing project (any supported framework)
- Reviewing code for logging best practices
- Converting console.log statements to structured logging
- Improving error handling with better context
- Configuring log draining, sampling, or enrichment
## Quick Reference
| Working on... | Resource |
| ----------------------- | ------------------------------------------------------------------ |
| Setup (CLI) | [`evlog init`](https://www.evlog.dev/cli/init): wire evlog into the project |
| Project conventions (CLI) | [`evlog agents`](https://www.evlog.dev/cli/agents): write the evlog block into the project's AGENTS.md |
| Coverage map (CLI) | [`evlog map`](https://www.evlog.dev/cli/map): score dark entry points |
| CI gating (CLI) | [`evlog map --min-score / --baseline`](https://www.evlog.dev/cli/ci): gate regressions |
| Wide events patterns | [references/wide-events.md](references/wide-events.md) |
| Error handling | [references/structured-errors.md](references/structured-errors.md) |
| Code review checklist | [references/code-review.md](references/code-review.md) |
| Drain pipeline | [references/drain-pipeline.md](references/drain-pipeline.md) |
| Audit logs | [build-audit-logs](../build-audit-logs/SKILL.md) skill + [docs](https://www.evlog.dev/use-cases/audit/overview) |
## Audit logs
For security-sensitive actions (auth, billing, admin, data export), use evlog's audit layer: a typed `audit` field on wide events, not a parallel logger. See the **`build-audit-logs`** skill for end-to-end setup (`log.audit`, `withAudit`, denials, `auditEnricher`, `auditOnly`, `signed`, `mockAudit`).
```typescript
log.audit({
action: 'invoice.refund',
actor: { type: 'user', id: user.id },
target: { type: 'invoice', id: invoice.id },
outcome: 'success',
})
```
Docs: https://www.evlog.dev/use-cases/audit/overview
## Installation
```bash
npm install evlog
```
## Use the CLI (recommended on Nuxt, Nitro, Next.js, TanStack Start, Hono, Express, Fastify)
The `evlog` executable ships with the `evlog` package: `pnpm evlog`, `npx evlog`, `bunx evlog`. It runs `@evlog/cli` when installed and fetches it on demand otherwise (no dependency added to the project), so it works on a project with nothing yet. Early but worth trying. It reads the project on disk (no traffic, no config needed). On the seven supported frameworks it covers the whole loop: **wire evlog in** (`init`), **score coverage** (`map`), **lock the score in CI** (`--min-score`, `--baseline`). If the CLI is unavailable, the framework has no adapter yet, or the user declines, continue with the manual sections below; the skill does not depend on it. **Ask before installing anything**.
### 1. Setup: `evlog init`
On a project that doesn't use evlog yet, prefer `init` over hand-writing the setup, since it detects the framework, reads what the project already has, and generates config, drains, enrichers, and extras in one pass. It is fully scriptable for agents:
```bash
# 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
```
Useful flags: `--framework` (override detection: `nuxt`, `nitro`, `next`, `tanstack-start`, `hono`, `express`, `fastify`), `--prodDrain` (comma-separated: `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` (traffic tier: `all`, `low`, `medium`, `high`, `very-high`), `--apps` (monorepo: which workspace packages), `--no-install`. Review the `--dry-run` output with the user before applying. Docs: https://www.evlog.dev/cli/init
### 2. Score: `evlog map`
```bash
npx evlog map --no-write
# agents: npx evlog map --json --no-write
```
What you get:
- A project score and which entry points are still dark
- **FIX FIRST**: the three most valuable places to fix
- **GOING FURTHER**: opportunities (catalogs, audit coverage, AI logging, auth identity) that never cost points
- Per-file inspect: `npx evlog map <file> --no-write` shows the shape the handler could take
- Re-run after fixes and watch the score move
Work FIX FIRST in order, keep changes minimal (`useLogger()`, `log.set()`, `log.audit()`, `createError({ why, fix })`), then re-run with `--no-write`. Omit `--no-write` only when the user wants `evlog.map.json` written.
### 3. Lock it in CI: `--min-score` and `--baseline`
On GitHub Actions, suggest the [evlog action](https://github.com/evloghq/action) (`uses: evloghq/action@v1` with `min-score`): it scans the pull request base itself, so no map file has to be committed, and puts regressions on the diff and in one comment. The commands below are the same gate from a `run:` step.
After fixing, propose making the score durable. This is where the CLI earns its keep:
```bash
# 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` compares the fresh scan against the committed `evlog.map.json`, **per entry point and per requirement**, so a refactor that instruments one route and breaks another fails even if the total score is unchanged. Disabling a passing check with a comment counts as a regression too. New uninstrumented routes are listed as `NEW AND DARK` without failing. Workflow: commit `evlog.map.json` once, add the `--baseline` run to CI, then re-run `map` without `--baseline` to accept an intentional change. Docs: https://www.evlog.dev/cli/ci
When the project has an `evlog.config.ts`, the gate can live there instead of in the CI step: `map: { minScore: 80, baseline: true }` makes a bare `evlog map` gate the same way, and a flag still wins. The same `map` block turns checks off for every entry point (`rules: { 'error-catalog': 'off' }`) and leaves entry points out by file glob (`ignore`). The CLI reads these values without running the file, so they must be literals. Docs: https://www.evlog.dev/cli/config
Early days: adapters and rules are still evolving; expect scores to move between releases. Docs: https://www.evlog.dev/cli/map · Rules: https://www.evlog.dev/cli/rules
---
## Framework Setup
### Nuxt
```typescript
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
env: { service: 'my-app' },
include: ['/api/**'],
},
})
```
`useLogger`, `log`, and `parseError` are **auto-imported**. `createError` is not: a bare one resolves to h3's, which drops `why`, `fix`, and `link`. Import it from `evlog`.
```typescript
// 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 }
})
```
Drain, enrich, sampling and redaction can live in `evlog.config.ts` at the app root. The module loads it on the server, and the `evlog` key in `nuxt.config.ts` overrides it. The browser logger reads only the `evlog` key.
```typescript
// evlog.config.ts
import { defineEvlog } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'
export default defineEvlog({
service: 'my-app',
drain: createAxiomDrain(),
})
```
Nitro hooks in server plugins keep working next to the file, and an event reaches both drains:
```typescript
// server/plugins/evlog-drain.ts
import { createAxiomDrain } from 'evlog/axiom'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', createAxiomDrain())
})
```
Client transport (auto-configured Vue plugin):
```typescript
// nuxt.config.ts
evlog: {
transport: { enabled: true }, // logs sent to /api/_evlog/ingest
}
```
Client-side: `log`, `setIdentity`, `clearIdentity` are auto-imported in components.
### Next.js
**Step 1: Create central config.** All exports come from here:
```typescript
// 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,
})
```
**Step 2: Wrap route handlers** with `withEvlog()`:
```typescript
// 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 })
})
```
**Step 3: Server Actions.** Same `withEvlog()` wrapper:
```typescript
// 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 }
})
```
**Step 4: Middleware** (optional, sets `x-request-id` + timing headers):
```typescript
// proxy.ts
import { evlogMiddleware } from 'evlog/next'
export const proxy = evlogMiddleware()
export const config = { matcher: ['/api/:path*'] }
```
**Step 5: Client Provider.** Wrap the root layout:
```tsx
// 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>
)
}
```
**Step 6: Client logging.** In any client component:
```tsx
'use client'
import { log, setIdentity, clearIdentity } from 'evlog/next/client'
setIdentity({ userId: 'usr_123' })
log.info({ action: 'checkout_click' })
clearIdentity()
```
**Step 7 (optional): Instrumentation.** Startup plus global `onRequestError` (SSR/RSC errors outside `withEvlog`). Use `defineNodeInstrumentation(() => import('./lib/evlog'))` in root `instrumentation.ts` to gate Node + cache the import, **or** write `register`/`onRequestError` manually. Both are valid. For custom logic, wrap evlog’s `register`/`onRequestError` inside `lib/evlog.ts` (compose with your own init or metrics), then re-export.
Export `createInstrumentation()` from `lib/evlog.ts` alongside `createEvlog()`. See framework docs for coexistence with `lockLogger`.
**Step 8: Client ingest endpoint.** Receives client logs:
```typescript
// 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
```typescript
// src/hooks.server.ts
import { initLogger } from 'evlog'
import { createEvlogHooks } from 'evlog/sveltekit'
initLogger({ env: { service: 'my-app' } })
export const { handle, handleError } = createEvlogHooks()
```
Access the logger via `event.locals.log` in route handlers or `useLogger()` from anywhere in the call stack:
```typescript
// 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 })
}
```
```typescript
import { useLogger } from 'evlog/sveltekit'
async function findUsers() {
const log = useLogger()
log.set({ db: { query: 'SELECT * FROM users' } })
}
```
Full pipeline with drain, enrich, and tail sampling:
```typescript
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
```typescript
// nitro.config.ts
import { defineConfig } from 'nitro'
import evlog from 'evlog/nitro/v3'
export default defineConfig({
modules: [evlog({ env: { service: 'my-api' } })],
})
```
```typescript
// 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 uses Nitro v3. Install evlog and add a `nitro.config.ts`:
```typescript
// 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' } })],
})
```
Add the error handling middleware to `__root.tsx`:
```typescript
// 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)],
},
})
```
Use `useRequest()` from `nitro/context` to access the logger:
```typescript
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
```typescript
// nitro.config.ts
import { defineNitroConfig } from 'nitropack/config'
import evlog from 'evlog/nitro'
export default defineNitroConfig({
modules: [evlog({ env: { service: 'my-api' } })],
})
```
Import `useLogger` from `evlog/nitro` in routes. On Nitro v2 and v3, an `evlog.config.ts` at the app root is loaded by the module, and the options passed to `evlog()` override it.
### NestJS
```typescript
// src/app.module.ts
import { Module } from '@nestjs/common'
import { EvlogModule } from 'evlog/nestjs'
@Module({
imports: [EvlogModule.forRoot()],
})
export class AppModule {}
```
`EvlogModule.forRoot()` registers a global middleware, on both `@nestjs/platform-express` and `@nestjs/platform-fastify`. `req.log` in controllers is the evlog logger on both platforms. Do not also register `evlog/fastify` on a Nest Fastify app, or every request emits two wide events. Use `useLogger()` to access the request-scoped logger from any controller or service:
```typescript
import { useLogger } from 'evlog/nestjs'
async function findUsers() {
const log = useLogger()
log.set({ db: { query: 'SELECT * FROM users' } })
}
```
Full pipeline with drain, enrich, and tail sampling:
```typescript
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
},
})
```
For async configuration with NestJS DI, use `forRootAsync()`:
```typescript
EvlogModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config) => ({
drain: createAxiomDrain({ apiKey: config.get('AXIOM_API_KEY') }),
}),
})
```
### Express
```typescript
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: [] })
})
```
Use `useLogger()` to access the logger from anywhere in the call stack without passing `req`:
```typescript
import { useLogger } from 'evlog/express'
async function findUsers() {
const log = useLogger()
log.set({ db: { query: 'SELECT * FROM users' } })
}
```
Full pipeline with drain, enrich, and tail sampling:
```typescript
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
```typescript
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: [] })
})
```
Access the logger via `c.get('log')` in handlers. Use `useLogger()` from `evlog/hono` in the layers underneath (services, repositories) where `c` is not in hand. Both return the same logger:
```typescript
import { useLogger } from 'evlog/hono'
async function findUsers() {
const log = useLogger()
log.set({ db: { query: 'SELECT * FROM users' } })
}
```
On Cloudflare Workers, `useLogger()` needs the `nodejs_compat` (or `nodejs_als`) compatibility flag; `c.get('log')` works with or without it.
Structured errors: throw `createError()`, then in `app.onError` use `parseError()` and pass `parsed.status as ContentfulStatusCode` to `c.json()` (Hono types the status argument as `ContentfulStatusCode`, not `number`).
```typescript
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,
)
})
```
Full pipeline with drain, enrich, and tail sampling:
```typescript
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
```typescript
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` is the evlog wide-event logger (shadows Fastify's built-in pino logger on the request). Fastify's pino logger remains accessible via `fastify.log`.
Use `useLogger()` to access the logger from anywhere in the call stack without passing `request`:
```typescript
import { useLogger } from 'evlog/fastify'
async function findUsers() {
const log = useLogger()
log.set({ db: { query: 'SELECT * FROM users' } })
}
```
Full pipeline with drain, enrich, and tail sampling:
```typescript
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
```typescript
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)
```
Use `useLogger()` to access the logger from anywhere in the call stack:
```typescript
import { useLogger } from 'evlog/elysia'
async function findUsers() {
const log = useLogger()
log.set({ db: { query: 'SELECT * FROM users' } })
}
```
Full pipeline with drain, enrich, and tail sampling:
```typescript
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
```typescript
// react-router.config.ts
import type { Config } from '@react-router/dev/config'
export default {
future: {
v8_middleware: true,
},
} satisfies Config
```
```typescript
// app/root.tsx
import { initLogger } from 'evlog'
import { evlog } from 'evlog/react-router'
initLogger({ env: { service: 'my-api' } })
export const middleware: Route.MiddlewareFunction[] = [
evlog(),
]
```
Access the logger via `context.get(loggerContext)` in loaders and actions:
```typescript
// 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: [] }
}
```
Use `useLogger()` to access the logger from anywhere in the call stack without passing context:
```typescript
import { useLogger } from 'evlog/react-router'
async function findUsers() {
const log = useLogger()
log.set({ db: { query: 'SELECT * FROM users' } })
}
```
Full pipeline with drain, enrich, and tail sampling:
```typescript
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
```typescript
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()` wraps the handler so each matched request emits one wide event; `os.use(evlog())` exposes `context.log` on every procedure that descends from `base` and tags the wide event with `operation` (the procedure path joined with `.`).
Use `useLogger()` to access the logger from utility modules:
```typescript
import { useLogger } from 'evlog/orpc'
async function chargeCard(amount: number) {
const log = useLogger()
log.set({ payment: { amount } })
}
```
Full pipeline with drain, enrich, and tail sampling:
```typescript
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` installs the `evlog()` middleware on every route and provides the `EvlogLogger` service. `initLoggerLayer` calls `initLogger` when the layer builds and flushes a `createDrainPipeline` drain when its scope closes:
```typescript
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))
```
There is no `AsyncLocalStorage` and no `log.fork()`. The event lives in the Effect context: `yield* EvlogLogger` in handlers and services (the requirement shows up in the effect's type), `yield* useLogger<Fields>()` for typed fields, and `yield* set(fields)` in library code that should not require `EvlogLogger` (a no-op outside a unit). It runs on Cloudflare Workers without `nodejs_compat`.
```typescript
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 } })
})
```
Failures are read from the `Cause` (the primary failure as `error`, the rest under `error.suppressed`, a pure interruption as `status: 499`). Effect's router answers a failed `createError({ status: 402 })` with 500, so catch it in the handler with `Effect.catch`, call `log.error(error)`, and return `HttpServerResponse.jsonUnsafe(body, { status: parsed.status })` built from `parseError(error)`. `HttpApi` encodes declared errors as responses before the middleware: the event gets the status but no `error` field.
Non-HTTP work (jobs, queue consumers, Durable Object alarms) uses `.pipe(withLogger('action-name', options))`. A Durable Object `fetch` uses `.pipe(withRequestLogger(request, options))`. On Workers, provide `WaitUntil` per request (`handler(request, Context.make(WaitUntil, ctx.waitUntil.bind(ctx)))` with `HttpRouter.toWebHandler`) so drains outlive the response. `loggerLayer()` folds `Effect.log*` calls into the current wide event. Each unit continues an incoming `traceparent`, and `withTracePropagation(client)` and `traceHeaders` forward it on outgoing calls.
Full pipeline with drain, enrich, and tail sampling:
```typescript
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
```typescript
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` emits one wide event per request when the handler returns, with no manual `log.emit()`. Async drains are registered with `waitUntil` so they survive the response; streaming responses defer the emit until the body completes. `requestId` comes from `x-request-id` (fallback `cf-ray`); `method`, `path`, `cf-ray`, `traceparent`, and the safe subset of `request.cf` are captured automatically. It accepts the same options (`drain`, `enrich`, `keep`, `include`, `exclude`, `routes`) as every other integration. For manual control (scheduled handlers, queues),...
© HugoRCD · MIT · evloghq/evlog