| 1 | --- |
| 2 | name: review-logging-patterns |
| 3 | description: Review code for logging patterns and suggest evlog adoption. Guides setup on Nuxt, Next.js, TanStack Start, Nitro, Hono, Cloudflare Workers, and standalone TypeScript. Detects console.log spam, unstructured errors, and missing context. Covers wide events, structured errors, drain adapters (Axiom, OTLP, PostHog, Sentry, Better Stack), sampling, and enrichers. |
| 4 | license: MIT |
| 5 | metadata: |
| 6 | author: HugoRCD |
| 7 | version: "0.5" |
| 8 | --- |
| 9 | |
| 10 | # Review logging patterns |
| 11 | |
| 12 | 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. |
| 13 | |
| 14 | ## When to Use |
| 15 | |
| 16 | - Setting up evlog in a new or existing project (any supported framework) |
| 17 | - Reviewing code for logging best practices |
| 18 | - Converting console.log statements to structured logging |
| 19 | - Improving error handling with better context |
| 20 | - Configuring log draining, sampling, or enrichment |
| 21 | |
| 22 | ## Quick Reference |
| 23 | |
| 24 | | Working on... | Resource | |
| 25 | | ----------------------- | ------------------------------------------------------------------ | |
| 26 | | Wide events patterns | [references/wide-events.md]( |
| 27 | | Error handling | [references/structured-errors.md]( |
| 28 | | Code review checklist | [references/code-review.md]( |
| 29 | | Drain pipeline | [references/drain-pipeline.md]( |
| 30 | |
| 31 | ## Installation |
| 32 | |
| 33 | ```bash |
| 34 | npm install evlog |
| 35 | ``` |
| 36 | |
| 37 | --- |
| 38 | |
| 39 | ## Framework Setup |
| 40 | |
| 41 | ### Nuxt |
| 42 | |
| 43 | ```typescript |
| 44 | // nuxt.config.ts |
| 45 | export default defineNuxtConfig({ |
| 46 | modules: ['evlog/nuxt'], |
| 47 | evlog: { |
| 48 | env: { service: ' |
| 49 | include: ['/api/**'], |
| 50 | }, |
| 51 | }) |
| 52 | ``` |
| 53 | |
| 54 | All evlog functions (`useLogger`, `createError`, ` |
| 55 | |
| 56 | ```typescript |
| 57 | // server/api/checkout.post.ts — no imports needed |
| 58 | export default defineEventHandler(async |
| 59 | const log = |
| 60 | log.set({ |
| 61 | return { success: |
| 62 | }) |
| 63 | ``` |
| 64 | |
| 65 | Drain, enrich, and tail sampling use Nitro hooks in server plugins: |
| 66 | |
| 67 | ```typescript |
| 68 | // server/plugins/evlog-drain.ts |
| 69 | import { createAxiomDrain } |
| 70 | |
| 71 | export default defineNitroPlugin((nitroApp |
| 72 | nitroApp.hooks.hook |
| 73 | }) |
| 74 | ``` |
| 75 | |
| 76 | Client transport (auto-configured Vue plugin): |
| 77 | |
| 78 | ```typescript |
| 79 | // nuxt.config.ts |
| 80 | evlog: { |
| 81 | transport: { enabled: true |
| 82 | } |
| 83 | ``` |
| 84 | |
| 85 | Client-side: `log`, `setIdentity`, ` |
| 86 | |
| 87 | ### Next.js |
| 88 | |
| 89 | **Step 1: Create central config** — all exports come from here: |
| 90 | |
| 91 | ```typescript |
| 92 | // lib/evlog.ts |
| 93 | import type { DrainContext |
| 94 | import { createEvlog } |
| 95 | import { createUserAgentEnricher, createRequestSizeEnricher |
| 96 | import { createDrainPipeline } |
| 97 | |
| 98 | const enrichers = [ |
| 99 | const pipeline = createDrainPipeline |
| 100 | const drain = pipeline |
| 101 | |
| 102 | export const { withEvlog |
| 103 | service: 'my-app', |
| 104 | sampling: { |
| 105 | rates: { info: 10 |
| 106 | keep: [{ status: |
| 107 | }, |
| 108 | routes: { |
| 109 | '/api/auth/**': { service: |
| 110 | '/api/checkout/**': { service: |
| 111 | }, |
| 112 | keep: (ctx) |
| 113 | const user = |
| 114 | if (user?.premium |
| 115 | }, |
| 116 | enrich: (ctx) |
| 117 | for (const enricher |
| 118 | }, |
| 119 | drain, |
| 120 | }) |
| 121 | ``` |
| 122 | |
| 123 | **Step 2: Wrap route handlers** with `withEvlog() |
| 124 | |
| 125 | ```typescript |
| 126 | // app/api/checkout/route.ts |
| 127 | import { withEvlog, useLogger |
| 128 | |
| 129 | export const POST = |
| 130 | const log = |
| 131 | log.set({ |
| 132 | log.set({ |
| 133 | return Response.json |
| 134 | }) |
| 135 | ``` |
| 136 | |
| 137 | **Step 3: Server Actions** — same `withEvlog() |
| 138 | |
| 139 | ```typescript |
| 140 | // app/actions.ts |
| 141 | 'use server' |
| 142 | import { withEvlog, useLogger |
| 143 | |
| 144 | export const checkout = |
| 145 | const log = |
| 146 | log.set({ |
| 147 | return { success: |
| 148 | }) |
| 149 | ``` |
| 150 | |
| 151 | **Step 4: Middleware** (optional — sets `x-request-id |
| 152 | |
| 153 | ```typescript |
| 154 | // proxy.ts |
| 155 | import { evlogMiddleware } |
| 156 | export const proxy = |
| 157 | export const config = |
| 158 | ``` |
| 159 | |
| 160 | **Step 5: Client Provider** — wrap root layout: |
| 161 | |
| 162 | ```tsx |
| 163 | // app/layout.tsx |
| 164 | import { EvlogProvider } |
| 165 | |
| 166 | export default function Layout |
| 167 | return ( |
| 168 | <html lang=" |
| 169 | <body> |
| 170 | <EvlogProvider service |
| 171 | {children} |
| 172 | </EvlogProvider> |
| 173 | </body> |
| 174 | </html> |
| 175 | ) |
| 176 | } |
| 177 | ``` |
| 178 | |
| 179 | **Step 6: Client logging** — in any client component: |
| 180 | |
| 181 | ```tsx |
| 182 | 'use client' |
| 183 | import { log, setIdentity |
| 184 | |
| 185 | setIdentity({ userId: ' |
| 186 | log.info({ action |
| 187 | clearIdentity() |
| 188 | ``` |
| 189 | |
| 190 | **Step 7: Client ingest endpoint** — receives client logs: |
| 191 | |
| 192 | ```typescript |
| 193 | // app/api/evlog/ingest/route.ts |
| 194 | import { NextRequest } |
| 195 | |
| 196 | const VALID_LEVELS = [ |
| 197 | |
| 198 | export async function POST |
| 199 | const origin = |
| 200 | const host = |
| 201 | if (origin && |
| 202 | return Response.json |
| 203 | } |
| 204 | const body = |
| 205 | if (!body?.timestamp |
| 206 | return Response.json |
| 207 | } |
| 208 | const { service: |
| 209 | console.log('[CLIENT LOG] |
| 210 | return new Response |
| 211 | } |
| 212 | ``` |
| 213 | |
| 214 | ### Nitro v3 |
| 215 | |
| 216 | ```typescript |
| 217 | // nitro.config.ts |
| 218 | import { defineConfig } |
| 219 | import evlog from 'evlog/nitro/v3 |
| 220 | |
| 221 | export default defineConfig({ |
| 222 | modules: [evlog({ |
| 223 | }) |
| 224 | ``` |
| 225 | |
| 226 | ```typescript |
| 227 | // routes/api/checkout.post.ts |
| 228 | import { defineHandler } |
| 229 | import { useLogger } |
| 230 | |
| 231 | export default defineHandler(async |
| 232 | const log = |
| 233 | log.set({ |
| 234 | return { ok: |
| 235 | }) |
| 236 | ``` |
| 237 | |
| 238 | ### TanStack Start |
| 239 | |
| 240 | TanStack Start uses Nitro v3. Install evlog and add a `nitro.config.ts`: |
| 241 | |
| 242 | ```typescript |
| 243 | // nitro.config.ts |
| 244 | import { defineConfig } |
| 245 | import evlog from 'evlog/nitro/v3 |
| 246 | |
| 247 | export default defineConfig({ |
| 248 | experimental: { asyncContext: true |
| 249 | modules: [evlog({ |
| 250 | }) |
| 251 | ``` |
| 252 | |
| 253 | Add the error handling middleware to `__root.tsx`: |
| 254 | |
| 255 | ```typescript |
| 256 | // src/routes/__root.tsx |
| 257 | import { createMiddleware } |
| 258 | import { evlogErrorHandler } |
| 259 | |
| 260 | export const Route = |
| 261 | server: { |
| 262 | middleware: [createMiddleware(). |
| 263 | }, |
| 264 | }) |
| 265 | ``` |
| 266 | |
| 267 | Use `useRequest()` from `nitro/context` to access the logger: |
| 268 | |
| 269 | ```typescript |
| 270 | import { useRequest } |
| 271 | import type { RequestLogger |
| 272 | |
| 273 | const req = useRequest |
| 274 | const log = req |
| 275 | log.set({ user |
| 276 | ``` |
| 277 | |
| 278 | ### Nitro v2 |
| 279 | |
| 280 | ```typescript |
| 281 | // nitro.config.ts |
| 282 | import { defineNitroConfig } |
| 283 | import evlog from 'evlog/nitro |
| 284 | |
| 285 | export default defineNitroConfig({ |
| 286 | modules: [evlog({ |
| 287 | }) |
| 288 | ``` |
| 289 | |
| 290 | Import `useLogger` from `evlog/nitro` in routes. |
| 291 | |
| 292 | ### Hono |
| 293 | |
| 294 | ```typescript |
| 295 | import { serve } |
| 296 | import { Hono } |
| 297 | import { createRequestLogger, initLogger |
| 298 | |
| 299 | initLogger({ env: { |
| 300 | |
| 301 | const app = new |
| 302 | |
| 303 | app.use('*' |
| 304 | const startedAt = |
| 305 | const log = |
| 306 | c.set('log |
| 307 | try { |
| 308 | await next() |
| 309 | } catch (error) |
| 310 | log.error(error |
| 311 | throw error |
| 312 | } finally { |
| 313 | log.emit({ |
| 314 | } |
| 315 | }) |
| 316 | |
| 317 | app.get('/health' |
| 318 | |
| 319 | app.onError((error |
| 320 | const parsed = |
| 321 | return c.json |
| 322 | }) |
| 323 | |
| 324 | serve({ fetch: app |
| 325 | ``` |
| 326 | |
| 327 | ### Cloudflare Workers |
| 328 | |
| 329 | ```typescript |
| 330 | import { initWorkersLogger, createWorkersLogger |
| 331 | |
| 332 | initWorkersLogger({ env: { |
| 333 | |
| 334 | export default { |
| 335 | async fetch(request |
| 336 | const log = |
| 337 | try { |
| 338 | log.set({ |
| 339 | const response = |
| 340 | log.emit({ |
| 341 | return response |
| 342 | } catch (error) |
| 343 | log.error(error |
| 344 | log.emit({ |
| 345 | throw error |
| 346 | } |
| 347 | }, |
| 348 | } |
| 349 | ``` |
| 350 | |
| 351 | ### Standalone TypeScript |
| 352 | |
| 353 | ```typescript |
| 354 | import { initLogger, createRequestLogger |
| 355 | |
| 356 | initLogger({ env: { |
| 357 | |
| 358 | const log = createRequestLogger |
| 359 | log.set({ source |
| 360 | log.emit() // |
| 361 | ``` |
| 362 | |
| 363 | --- |
| 364 | |
| 365 | ## Configuration Options |
| 366 | |
| 367 | All options work in Nuxt (`evlog` key), Nitro (passed to `evlog()`), Next.js (` |
| 368 | |
| 369 | | Option | Type | Default | Description | |
| 370 | |--------|------|---------|-------------| |
| 371 | | `env.service` / `service` |
| 372 | | `enabled` | `boolean |
| 373 | | `pretty` | `boolean |
| 374 | | `include` | `string[] |
| 375 | | `exclude` | `string[] |
| 376 | | `routes` | `Record<string, { service }> |
| 377 | | `sampling.rates` | `object |
| 378 | | `sampling.keep` | `array |
| 379 | | `drain` | `(ctx) => void |
| 380 | | `enrich` | `(ctx) => void |
| 381 | | `keep` | `(ctx) => void |
| 382 | |
| 383 | ### Nitro Hooks (Nuxt, Nitro v2/v3) |
| 384 | |
| 385 | | Hook | When | Use | |
| 386 | |------|------|-----| |
| 387 | | `evlog:drain` | After enrichment | Send events to external services |
| 388 | | `evlog:enrich` | After emit, before drain | Add derived context |
| 389 | | `evlog:emit:keep` | During emit | Custom tail sampling logic |
| 390 | | `close` | Server shutdown | Flush drain pipeline buffers |
| 391 | |
| 392 | --- |
| 393 | |
| 394 | ## Drain Adapters |
| 395 | |
| 396 | | Adapter | Import | Env Vars | |
| 397 | |---------|--------|----------| |
| 398 | | Axiom | `evlog/axiom` | |
| 399 | | OTLP | `evlog/otlp` | |
| 400 | | PostHog | `evlog/posthog` | |
| 401 | | Sentry | `evlog/sentry` | |
| 402 | | Better Stack | `evlog/better-stack` | |
| 403 | |
| 404 | Setup pattern per framework: |
| 405 | |
| 406 | ```typescript |
| 407 | // Nuxt/Nitro: server/plugins/evlog-drain.ts |
| 408 | import { createAxiomDrain } |
| 409 | export default defineNitroPlugin((nitroApp |
| 410 | nitroApp.hooks.hook |
| 411 | }) |
| 412 | |
| 413 | // Next.js: in lib/evlog.ts, pass drain to createEvlog() |
| 414 | import { createAxiomDrain } |
| 415 | import { createDrainPipeline } |
| 416 | const pipeline = createDrainPipeline |
| 417 | const drain = pipeline |
| 418 | // then: createEvlog({ ..., drain }) |
| 419 | |
| 420 | // Standalone: pass drain to initLogger() |
| 421 | initLogger({ env: { |
| 422 | ``` |
| 423 | |
| 424 | See [references/drain-pipeline.md](references/drain-pipeline.md |
| 425 | |
| 426 | --- |
| 427 | |
| 428 | ## Enrichers |
| 429 | |
| 430 | Built-in: `createUserAgentEnricher()`, `createGeoEnricher()`, ` |
| 431 | |
| 432 | ```typescript |
| 433 | // Nuxt/Nitro: server/plugins/evlog-enrich.ts |
| 434 | import { createUserAgentEnricher, createGeoEnricher |
| 435 | export default defineNitroPlugin((nitroApp |
| 436 | const enrichers = |
| 437 | nitroApp.hooks.hook |
| 438 | for (const enricher |
| 439 | }) |
| 440 | }) |
| 441 | |
| 442 | // Next.js: in lib/evlog.ts |
| 443 | createEvlog({ |
| 444 | enrich: (ctx) |
| 445 | for (const enricher |
| 446 | ctx.event.region |
| 447 | }, |
| 448 | }) |
| 449 | ``` |
| 450 | |
| 451 | --- |
| 452 | |
| 453 | ## Structured Errors |
| 454 | |
| 455 | ```typescript |
| 456 | import { createError } |
| 457 | |
| 458 | // Minimal |
| 459 | throw createError({ message: |
| 460 | |
| 461 | // Standard |
| 462 | throw createError({ message: |
| 463 | |
| 464 | // Complete |
| 465 | throw createError({ |
| 466 | message: 'Payment failed', |
| 467 | status: 402, |
| 468 | why: 'Card declined by issuer - insufficient funds', |
| 469 | fix: 'Please use a different payment method or contact your bank', |
| 470 | link: 'https://docs.example.com/payments/declined', |
| 471 | cause: originalError, |
| 472 | }) |
| 473 | ``` |
| 474 | |
| 475 | Frontend — extract all fields with `parseError()`: |
| 476 | |
| 477 | ```typescript |
| 478 | import { parseError } |
| 479 | |
| 480 | const error = parseError |
| 481 | // error.message, error.status, error.why, error.fix, error.link |
| 482 | ``` |
| 483 | |
| 484 | See [references/structured-errors.md](references/structured-errors.md |
| 485 | |
| 486 | --- |
| 487 | |
| 488 | ## Anti-Patterns to Detect |
| 489 | |
| 490 | | Anti-Pattern | Fix | |
| 491 | |--------------|-----| |
| 492 | | Multiple `console.log` in one function | Single wide event with `log.set() |
| 493 | | `throw new Error('...')` | `throw createError({ message, status, why, fix }) |
| 494 | | `console.error(e); throw e` | `log.error(e); throw createError(...) |
| 495 | | No logging in request handlers | Add `useLogger(event)` / `useLogger() |
| 496 | | Flat log data `{ uid, n, t }` | Grouped objects: `{ user: {...}, cart: {...} } |
| 497 | | Logging sensitive data `log.set({ user: body })` | Explicit fields: `{ user: { id: body.id, plan: body.plan } } |
| 498 | |
| 499 | See [references/code-review.md](references/code-review.md |
| 500 | |
| 501 | --- |
| 502 | |
| 503 | ## Loading Reference Files |
| 504 | |
| 505 | Load based on what you're working on — **do not load all at once**: |
| 506 | |
| 507 | - Designing wide events → [references/wide-events.md](references/wide-events.md |
| 508 | - Improving errors → [references/structured-errors.md](references/structured-errors.md |
| 509 | - Full code review → [references/code-review.md](references/code-review.md |
| 510 | - Drain pipeline setup → [references/drain-pipeline.md](references/drain-pipeline.md |