| 1 | --- |
| 2 | name: review-logging-patterns |
| 3 | description: Review code for logging patterns and suggest evlog adoption. Guides setup on Nuxt, Next.js, SvelteKit, Nitro, TanStack Start, React Router, NestJS, Express, Hono, Fastify, Elysia, Cloudflare Workers, and standalone TypeScript. Detects console.log spam, unstructured errors, and missing context. Covers wide events, structured errors, drain adapters (Axiom, OTLP, HyperDX, PostHog, Sentry, Better Stack, Datadog), sampling, enrichers, and AI SDK integration (token usage, tool calls, streaming metrics, telemetry integration, cost estimation, embedding metadata). |
| 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 |
| 171 | service="my-app" |
| 172 | transport={{ enabled: |
| 173 | > |
| 174 | {children} |
| 175 | </EvlogProvider> |
| 176 | </body> |
| 177 | </html> |
| 178 | ); |
| 179 | } |
| 180 | ``` |
| 181 | |
| 182 | **Step 6: Client logging** — in any client component: |
| 183 | |
| 184 | ```tsx |
| 185 | "use client"; |
| 186 | import { log, setIdentity |
| 187 | |
| 188 | setIdentity({ userId: " |
| 189 | log.info({ action |
| 190 | clearIdentity(); |
| 191 | ``` |
| 192 | |
| 193 | **Step 7 (optional): Instrumentation** — startup + global `onRequestError |
| 194 | |
| 195 | Export `createInstrumentation()` from `lib/evlog.ts` alongside ` |
| 196 | |
| 197 | **Step 8: Client ingest endpoint** — receives client logs: |
| 198 | |
| 199 | ```typescript |
| 200 | // app/api/evlog/ingest/route.ts |
| 201 | import { NextRequest } |
| 202 | |
| 203 | const VALID_LEVELS = [ |
| 204 | |
| 205 | export async function POST |
| 206 | const origin = |
| 207 | const host = |
| 208 | if (origin && |
| 209 | return Response.json |
| 210 | } |
| 211 | const body = |
| 212 | if (!body?.timestamp |
| 213 | return Response.json |
| 214 | } |
| 215 | const { service: |
| 216 | console.log( |
| 217 | "[CLIENT LOG]", |
| 218 | JSON.stringify({ |
| 219 | ); |
| 220 | return new Response |
| 221 | } |
| 222 | ``` |
| 223 | |
| 224 | ### SvelteKit |
| 225 | |
| 226 | ```typescript |
| 227 | // src/hooks.server.ts |
| 228 | import { initLogger } |
| 229 | import { createEvlogHooks } |
| 230 | |
| 231 | initLogger({ env: { |
| 232 | |
| 233 | export const { handle |
| 234 | ``` |
| 235 | |
| 236 | Access the logger via `event.locals.log` in route handlers or `useLogger()` from anywhere in the call stack: |
| 237 | |
| 238 | ```typescript |
| 239 | // src/routes/api/users/[id]/+server.ts |
| 240 | import { json } |
| 241 | |
| 242 | export const GET = |
| 243 | locals.log.set |
| 244 | return json({ |
| 245 | }; |
| 246 | ``` |
| 247 | |
| 248 | ```typescript |
| 249 | import { useLogger } |
| 250 | |
| 251 | async function findUsers() |
| 252 | const log = |
| 253 | log.set({ |
| 254 | } |
| 255 | ``` |
| 256 | |
| 257 | Full pipeline with drain, enrich, and tail sampling: |
| 258 | |
| 259 | ```typescript |
| 260 | import { createAxiomDrain } |
| 261 | |
| 262 | export const { handle |
| 263 | include: ["/api/**"], |
| 264 | drain: createAxiomDrain(), |
| 265 | enrich: (ctx) |
| 266 | ctx.event.region |
| 267 | }, |
| 268 | keep: (ctx) |
| 269 | if (ctx.duration |
| 270 | }, |
| 271 | }); |
| 272 | ``` |
| 273 | |
| 274 | ### Nitro v3 |
| 275 | |
| 276 | ```typescript |
| 277 | // nitro.config.ts |
| 278 | import { defineConfig } |
| 279 | import evlog from "evlog/nitro/v3 |
| 280 | |
| 281 | export default defineConfig({ |
| 282 | modules: [evlog({ |
| 283 | }); |
| 284 | ``` |
| 285 | |
| 286 | ```typescript |
| 287 | // routes/api/checkout.post.ts |
| 288 | import { defineHandler } |
| 289 | import { useLogger } |
| 290 | |
| 291 | export default defineHandler(async |
| 292 | const log = |
| 293 | log.set({ |
| 294 | return { ok: |
| 295 | }); |
| 296 | ``` |
| 297 | |
| 298 | ### TanStack Start |
| 299 | |
| 300 | TanStack Start uses Nitro v3. Install evlog and add a `nitro.config.ts`: |
| 301 | |
| 302 | ```typescript |
| 303 | // nitro.config.ts |
| 304 | import { defineConfig } |
| 305 | import evlog from "evlog/nitro/v3 |
| 306 | |
| 307 | export default defineConfig({ |
| 308 | experimental: { asyncContext: true |
| 309 | modules: [evlog({ |
| 310 | }); |
| 311 | ``` |
| 312 | |
| 313 | Add the error handling middleware to `__root.tsx`: |
| 314 | |
| 315 | ```typescript |
| 316 | // src/routes/__root.tsx |
| 317 | import { createMiddleware } |
| 318 | import { evlogErrorHandler } |
| 319 | |
| 320 | export const Route = |
| 321 | server: { |
| 322 | middleware: [createMiddleware(). |
| 323 | }, |
| 324 | }); |
| 325 | ``` |
| 326 | |
| 327 | Use `useRequest()` from `nitro/context` to access the logger: |
| 328 | |
| 329 | ```typescript |
| 330 | import { useRequest } |
| 331 | import type { RequestLogger |
| 332 | |
| 333 | const req = useRequest |
| 334 | const log = req |
| 335 | log.set({ user |
| 336 | ``` |
| 337 | |
| 338 | ### Nitro v2 |
| 339 | |
| 340 | ```typescript |
| 341 | // nitro.config.ts |
| 342 | import { defineNitroConfig } |
| 343 | import evlog from "evlog/nitro |
| 344 | |
| 345 | export default defineNitroConfig({ |
| 346 | modules: [evlog({ |
| 347 | }); |
| 348 | ``` |
| 349 | |
| 350 | Import `useLogger` from `evlog/nitro` in routes. |
| 351 | |
| 352 | ### NestJS |
| 353 | |
| 354 | ```typescript |
| 355 | // src/app.module.ts |
| 356 | import { Module } |
| 357 | import { EvlogModule } |
| 358 | |
| 359 | @Module({ |
| 360 | imports: [EvlogModule.forRoot |
| 361 | }) |
| 362 | export class AppModule |
| 363 | ``` |
| 364 | |
| 365 | `EvlogModule.forRoot()` registers a global middleware. Use `useLogger()` to access the request-scoped logger from any controller or service: |
| 366 | |
| 367 | ```typescript |
| 368 | import { useLogger } |
| 369 | |
| 370 | async function findUsers() |
| 371 | const log = |
| 372 | log.set({ |
| 373 | } |
| 374 | ``` |
| 375 | |
| 376 | Full pipeline with drain, enrich, and tail sampling: |
| 377 | |
| 378 | ```typescript |
| 379 | import { createAxiomDrain } |
| 380 | |
| 381 | EvlogModule.forRoot({ |
| 382 | include: ["/api/**"], |
| 383 | drain: createAxiomDrain(), |
| 384 | enrich: (ctx) |
| 385 | ctx.event.region |
| 386 | }, |
| 387 | keep: (ctx) |
| 388 | if (ctx.duration |
| 389 | }, |
| 390 | }); |
| 391 | ``` |
| 392 | |
| 393 | For async configuration with NestJS DI, use `forRootAsync()`: |
| 394 | |
| 395 | ```typescript |
| 396 | EvlogModule.forRootAsync({ |
| 397 | imports: [ConfigModule], |
| 398 | inject: [ConfigService], |
| 399 | useFactory: (config) |
| 400 | drain: createAxiomDrain({ |
| 401 | }), |
| 402 | }); |
| 403 | ``` |
| 404 | |
| 405 | ### Express |
| 406 | |
| 407 | ```typescript |
| 408 | import express from "express |
| 409 | import { initLogger } |
| 410 | import { evlog, useLogger |
| 411 | |
| 412 | initLogger({ env: { |
| 413 | |
| 414 | const app = express |
| 415 | app.use(evlog()) |
| 416 | |
| 417 | app.get("/api/users" |
| 418 | req.log.set |
| 419 | res.json({ |
| 420 | }); |
| 421 | ``` |
| 422 | |
| 423 | Use `useLogger()` to access the logger from anywhere in the call stack without passing `req`: |
| 424 | |
| 425 | ```typescript |
| 426 | import { useLogger } |
| 427 | |
| 428 | async function findUsers() |
| 429 | const log = |
| 430 | log.set({ |
| 431 | } |
| 432 | ``` |
| 433 | |
| 434 | Full pipeline with drain, enrich, and tail sampling: |
| 435 | |
| 436 | ```typescript |
| 437 | import { createAxiomDrain } |
| 438 | |
| 439 | app.use( |
| 440 | evlog({ |
| 441 | include: ["/api/**"], |
| 442 | drain: createAxiomDrain(), |
| 443 | enrich: (ctx) |
| 444 | ctx.event.region |
| 445 | }, |
| 446 | keep: (ctx) |
| 447 | if (ctx.duration |
| 448 | }, |
| 449 | }), |
| 450 | ); |
| 451 | ``` |
| 452 | |
| 453 | ### Hono |
| 454 | |
| 455 | ```typescript |
| 456 | import { Hono } |
| 457 | import { initLogger } |
| 458 | import { evlog, type |
| 459 | |
| 460 | initLogger({ env: { |
| 461 | |
| 462 | const app = new |
| 463 | app.use(evlog()) |
| 464 | |
| 465 | app.get("/api/users" |
| 466 | const log = |
| 467 | log.set({ |
| 468 | return c.json |
| 469 | }); |
| 470 | ``` |
| 471 | |
| 472 | Access the logger via `c.get('log')` in handlers. No `useLogger()` — use ` |
| 473 | |
| 474 | Structured errors: throw `createError()`, then in `app.onError` use ` |
| 475 | |
| 476 | ```typescript |
| 477 | import { createError, parseError |
| 478 | import type { ContentfulStatusCode |
| 479 | |
| 480 | app.onError((error |
| 481 | c.get("log |
| 482 | const parsed = |
| 483 | return c.json |
| 484 | { message: parsed. |
| 485 | parsed.status as |
| 486 | ); |
| 487 | }); |
| 488 | ``` |
| 489 | |
| 490 | Full pipeline with drain, enrich, and tail sampling: |
| 491 | |
| 492 | ```typescript |
| 493 | import { createAxiomDrain } |
| 494 | |
| 495 | app.use( |
| 496 | evlog({ |
| 497 | include: ["/api/**"], |
| 498 | drain: createAxiomDrain(), |
| 499 | enrich: (ctx) |
| 500 | ctx.event.region |
| 501 | }, |
| 502 | keep: (ctx) |
| 503 | if (ctx.duration |
| 504 | }, |
| 505 | }), |
| 506 | ); |
| 507 | ``` |
| 508 | |
| 509 | ### Fastify |
| 510 | |
| 511 | ```typescript |
| 512 | import Fastify from "fastify |
| 513 | import { initLogger } |
| 514 | import { evlog, useLogger |
| 515 | |
| 516 | initLogger({ env: { |
| 517 | |
| 518 | const app = Fastify |
| 519 | await app.register( |
| 520 | |
| 521 | app.get("/api/users" |
| 522 | request.log.set |
| 523 | return { users: |
| 524 | }); |
| 525 | ``` |
| 526 | |
| 527 | `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`. |
| 528 | |
| 529 | Use `useLogger()` to access the logger from anywhere in the call stack without passing `request`: |
| 530 | |
| 531 | ```typescript |
| 532 | import { useLogger } |
| 533 | |
| 534 | async function findUsers() |
| 535 | const log = |
| 536 | log.set({ |
| 537 | } |
| 538 | ``` |
| 539 | |
| 540 | Full pipeline with drain, enrich, and tail sampling: |
| 541 | |
| 542 | ```typescript |
| 543 | import { createAxiomDrain } |
| 544 | |
| 545 | await app.register( |
| 546 | include: ["/api/**"], |
| 547 | drain: createAxiomDrain(), |
| 548 | enrich: (ctx) |
| 549 | ctx.event.region |
| 550 | }, |
| 551 | keep: (ctx) |
| 552 | if (ctx.duration |
| 553 | }, |
| 554 | }); |
| 555 | ``` |
| 556 | |
| 557 | ### Elysia |
| 558 | |
| 559 | ```typescript |
| 560 | import { Elysia } |
| 561 | import { initLogger } |
| 562 | import { evlog, useLogger |
| 563 | |
| 564 | initLogger({ env: { |
| 565 | |
| 566 | const app = new |
| 567 | .use(evlog()) |
| 568 | .get("/api/users", |
| 569 | log.set({ |
| 570 | return { users: |
| 571 | }) |
| 572 | .listen(3000); |
| 573 | ``` |
| 574 | |
| 575 | Use `useLogger()` to access the logger from anywhere in the call stack: |
| 576 | |
| 577 | ```typescript |
| 578 | import { useLogger } |
| 579 | |
| 580 | async function findUsers() |
| 581 | const log = |
| 582 | log.set({ |
| 583 | } |
| 584 | ``` |
| 585 | |
| 586 | Full pipeline with drain, enrich, and tail sampling: |
| 587 | |
| 588 | ```typescript |
| 589 | import { createAxiomDrain } |
| 590 | |
| 591 | app.use( |
| 592 | evlog({ |
| 593 | include: ["/api/**"], |
| 594 | drain: createAxiomDrain(), |
| 595 | enrich: (ctx) |
| 596 | ctx.event.region |
| 597 | }, |
| 598 | keep: (ctx) |
| 599 | if (ctx.duration |
| 600 | }, |
| 601 | }), |
| 602 | ); |
| 603 | ``` |
| 604 | |
| 605 | ### React Router |
| 606 | |
| 607 | ```typescript |
| 608 | // react-router.config.ts |
| 609 | import type { Config |
| 610 | |
| 611 | export default { |
| 612 | future: { |
| 613 | v8_middleware: true, |
| 614 | }, |
| 615 | } satisfies Config; |
| 616 | ``` |
| 617 | |
| 618 | ```typescript |
| 619 | // app/root.tsx |
| 620 | import { initLogger } |
| 621 | import { evlog } |
| 622 | |
| 623 | initLogger({ env: { |
| 624 | |
| 625 | export const middleware: |
| 626 | ``` |
| 627 | |
| 628 | Access the logger via `context.get(loggerContext)` in loaders and actions: |
| 629 | |
| 630 | ```typescript |
| 631 | // app/routes/api.users.$id.tsx |
| 632 | import { loggerContext } |
| 633 | |
| 634 | export async function loader |
| 635 | const log = |
| 636 | log.set({ |
| 637 | return { users: |
| 638 | } |
| 639 | ``` |
| 640 | |
| 641 | Use `useLogger()` to access the logger from anywhere in the call stack without passing context: |
| 642 | |
| 643 | ```typescript |
| 644 | import { useLogger } |
| 645 | |
| 646 | async function findUsers() |
| 647 | const log = |
| 648 | log.set({ |
| 649 | } |
| 650 | ``` |
| 651 | |
| 652 | Full pipeline with drain, enrich, and tail sampling: |
| 653 | |
| 654 | ```typescript |
| 655 | import { createAxiomDrain } |
| 656 | |
| 657 | export const middleware: |
| 658 | evlog({ |
| 659 | include: ["/api/**"], |
| 660 | drain: createAxiomDrain(), |
| 661 | enrich: (ctx) |
| 662 | ctx.event.region |
| 663 | }, |
| 664 | keep: (ctx) |
| 665 | if (ctx.duration |
| 666 | }, |
| 667 | }), |
| 668 | ]; |
| 669 | ``` |
| 670 | |
| 671 | ### Cloudflare Workers |
| 672 | |
| 673 | ```typescript |
| 674 | import { initWorkersLogger, createWorkersLogger |
| 675 | |
| 676 | initWorkersLogger({ env: { |
| 677 | |
| 678 | export default { |
| 679 | async fetch(request |
| 680 | const log = |
| 681 | try { |
| 682 | log.set({ |
| 683 | const response = |
| 684 | log.emit({ |
| 685 | return response; |
| 686 | } catch (error) |
| 687 | log.error(error |
| 688 | log.emit({ |
| 689 | throw error; |
| 690 | } |
| 691 | }, |
| 692 | }; |
| 693 | ``` |
| 694 | |
| 695 | ### Vite Plugin (any Vite-based framework) |
| 696 | |
| 697 | For any Vite-based project (SvelteKit, Astro, SolidStart, React+Vite, etc.), use the Vite plugin for auto-init, auto-imports, and build-time features: |
| 698 | |
| 699 | ```typescript |
| 700 | // vite.config.ts |
| 701 | import evlog from "evlog/vite |
| 702 | |
| 703 | export default defineConfig({ |
| 704 | plugins: [ |
| 705 | evlog({ |
| 706 | service: "my-app", |
| 707 | autoImports: true, // |
| 708 | strip: ["debug"], |
| 709 | sourceLocation: true, // |
| 710 | client: { |
| 711 | // client-side logging |
| 712 | transport: { endpoint: " |
| 713 | }, |
| 714 | }), |
| 715 | ], |
| 716 | }); |
| 717 | ``` |
| 718 | |
| 719 | Server-side middleware (drain, enrich, keep, routes) is still configured in the framework integration (e.g., `evlog()` middleware for Hono/Express/SvelteKit). The Vite plugin handles build-time DX only. |
| 720 | |
| 721 | ### Standalone TypeScript |
| 722 | |
| 723 | ```typescript |
| 724 | import { initLogger, createRequestLogger |
| 725 | |
| 726 | initLogger({ env: { |
| 727 | |
| 728 | const log = createRequestLogger |
| 729 | log.set({ source |
| 730 | log.emit(); // |
| 731 | ``` |
| 732 | |
| 733 | --- |
| 734 | |
| 735 | ## Configuration Options |
| 736 | |
| 737 | All options work in Nuxt (`evlog` key), Nitro (passed to `evlog()`), Next.js (` |
| 738 | |
| 739 | | Option | Type | Default | Description | |
| 740 | | ------------------------- | ---------------------------------------- | |
| 741 | | `env.service` / `service` |
| 742 | | `enabled` | `boolean |
| 743 | | `pretty` | `boolean |
| 744 | | `silent` | `boolean |
| 745 | | `include` | `string[] |
| 746 | | `exclude` | `string[] |
| 747 | | `routes` | `Record<string, { service }> |
| 748 | | `minLevel` | `'debug' \| 'info' \| 'warn' \| 'error' |
| 749 | | `sampling.rates` | `object |
| 750 | | `sampling.keep` | `array |
| 751 | | `drain` | `(ctx) => void |
| 752 | | `enrich` | `(ctx) => void |
| 753 | | `keep` | `(ctx) => void |
| 754 | | `redact` | `boolean \| RedactConfig |
| 755 | |
| 756 | ### Nitro Hooks (Nuxt, Nitro v2/v3) |
| 757 | |
| 758 | | Hook | When | Use | |
| 759 | | ----------------- | ------------------------ | |
| 760 | | `evlog:drain` | After enrichment | Send events to external services |
| 761 | | `evlog:enrich` | After emit, before drain | Add derived context |
| 762 | | `evlog:emit:keep` | During emit | Custom tail sampling logic |
| 763 | | `close` | Server shutdown | Flush drain pipeline buffers |
| 764 | |
| 765 | --- |
| 766 | |
| 767 | ## Drain Adapters |
| 768 | |
| 769 | | Adapter | Import | Env Vars | |
| 770 | | --------------------- | -------------------- | |
| 771 | | Axiom | `evlog/axiom` | |
| 772 | | OTLP | `evlog/otlp` | |
| 773 | | HyperDX | `evlog/hyperdx` | |
| 774 | | PostHog | `evlog/posthog` | |
| 775 | | Sentry | `evlog/sentry` | |
| 776 | | Better Stack | `evlog/better-stack` | |
| 777 | | Datadog | `evlog/datadog` | |
| 778 | | File System | `evlog/fs` | None (local file system) |
| 779 | | HTTP (browser ingest) | `evlog/http` | None (configure |
| 780 | |
| 781 | In Nuxt/Nitro, use the `NUXT_` prefix (e.g., `NUXT_AXIOM_TOKEN`) so values are available via |
| 782 | |
| 783 | Setup pattern per framework: |
| 784 | |
| 785 | ```typescript |
| 786 | // Nuxt/Nitro: server/plugins/evlog-drain.ts |
| 787 | import { createAxiomDrain } |
| 788 | export default defineNitroPlugin((nitroApp |
| 789 | nitroApp.hooks.hook |
| 790 | }); |
| 791 | |
| 792 | // Hono / Express / Elysia: pass drain in middleware options |
| 793 | import { createAxiomDrain } |
| 794 | app.use(evlog( |
| 795 | |
| 796 | // Fastify: pass drain in plugin options |
| 797 | import { createAxiomDrain } |
| 798 | await app.register( |
| 799 | |
| 800 | // NestJS: pass drain in module options |
| 801 | import { createAxiomDrain } |
| 802 | EvlogModule.forRoot({ drain |
| 803 | |
| 804 | // Next.js: pass drain to createEvlog() |
| 805 | import { createAxiomDrain } |
| 806 | import { createDrainPipeline } |
| 807 | const pipeline = createDrainPipeline |
| 808 | const drain = pipeline |
| 809 | // then: createEvlog({ ..., drain }) |
| 810 | |
| 811 | // Standalone: pass drain to initLogger() |
| 812 | initLogger({ env: { |
| 813 | ``` |
| 814 | |
| 815 | See [references/drain-pipeline.md](references/drain-pipeline.md |
| 816 | |
| 817 | --- |
| 818 | |
| 819 | ## Enrichers |
| 820 | |
| 821 | Built-in: `createUserAgentEnricher()`, `createGeoEnricher()`, ` |
| 822 | |
| 823 | ```typescript |
| 824 | // Nuxt/Nitro: server/plugins/evlog-enrich.ts |
| 825 | import { createUserAgentEnricher, createGeoEnricher |
| 826 | export default defineNitroPlugin((nitroApp |
| 827 | const enrichers = |
| 828 | nitroApp.hooks.hook |
| 829 | for (const enricher |
| 830 | }); |
| 831 | }); |
| 832 | |
| 833 | // Next.js: in lib/evlog.ts |
| 834 | createEvlog({ |
| 835 | enrich: (ctx) |
| 836 | for (const enricher |
| 837 | ctx.event.region |
| 838 | }, |
| 839 | }); |
| 840 | ``` |
| 841 | |
| 842 | --- |
| 843 | |
| 844 | ## Auto-Redaction (PII Protection) |
| 845 | |
| 846 | Built-in redaction scrubs sensitive data from wide events **before** console output and |
| 847 | |
| 848 | ```typescript |
| 849 | // Disable in production (opt-out) |
| 850 | evlog: { redact: false |
| 851 | |
| 852 | // Add custom paths on top of built-ins |
| 853 | evlog: { |
| 854 | redact: { |
| 855 | paths: ['user.password', |
| 856 | } |
| 857 | } |
| 858 | |
| 859 | // Only specific built-ins |
| 860 | evlog: { |
| 861 | redact: { |
| 862 | builtins: ['email', |
| 863 | } |
| 864 | } |
| 865 | |
| 866 | // No built-ins, only custom (uses flat [REDACTED] replacement) |
| 867 | evlog: { |
| 868 | redact: { |
| 869 | builtins: false, |
| 870 | paths: ['user.ssn'], |
| 871 | patterns: [/SECRET_\w+/ |
| 872 | } |
| 873 | } |
| 874 | ``` |
| 875 | |
| 876 | **Built-in patterns** with smart masking output: |
| 877 | |
| 878 | | Pattern | Example Input | Masked Output | |
| 879 | | ------------ | ----------------------- | |
| 880 | | `creditCard` | `4111111111111111 |
| 881 | | `email` | `alice@example.com |
| 882 | | `ipv4` | `192.168.1.100 |
| 883 | | `phone` | `+33 6 12 34 56 78 |
| 884 | | `jwt` | `eyJhbGciOi... |
| 885 | | `bearer` | `Bearer sk_live_abc... |
| 886 | | `iban` | `FR76 3000 6000 ...189 |
| 887 | |
| 888 | Works in all frameworks: Nuxt (`evlog` config), Nitro (`evlog()` module options), Next.js (` |
| 889 | |
| 890 | --- |
| 891 | |
| 892 | ## AI SDK Integration |
| 893 | |
| 894 | Capture token usage, tool calls, model info, streaming metrics, tool execution timing, cost estimation, and embedding metadata from the Vercel AI SDK into wide events. Import from `evlog/ai`. Requires `ai >= 6.0.0` as a peer dependency. |
| 895 | |
| 896 | ### Basic setup (middleware) |
| 897 | |
| 898 | ```typescript |
| 899 | import { createAILogger } |
| 900 | |
| 901 | const log = useLogger |
| 902 | const ai = createAILogger |
| 903 | |
| 904 | const result = streamText |
| 905 | model: ai.wrap |
| 906 | messages, |
| 907 | }); |
| 908 | ``` |
| 909 | |
| 910 | `ai.wrap()` uses model middleware to transparently capture all LLM calls. Works with `generateText`, ` |
| 911 | |
| 912 | ### Telemetry integration (deeper observability) |
| 913 | |
| 914 | For tool execution timing, success/failure tracking, and total generation wall time, add `createEvlogIntegration()`: |
| 915 | |
| 916 | ```typescript |
| 917 | import { createAILogger, createEvlogIntegration |
| 918 | |
| 919 | const ai = createAILogger |
| 920 | |
| 921 | const agent = new |
| 922 | model: ai.wrap |
| 923 | tools: { searchWeb, |
| 924 | stopWhen: stepCountIs(5) |
| 925 | experimental_telemetry: { |
| 926 | isEnabled: true, |
| 927 | integrations: [createEvlogIntegration(ai |
| 928 | }, |
| 929 | }); |
| 930 | ``` |
| 931 | |
| 932 | This adds `ai.tools` (per-tool `{ name, durationMs, success, error? }`) and ` |
| 933 | |
| 934 | ### Embeddings |
| 935 | |
| 936 | ```typescript |
| 937 | const { embedding, |
| 938 | ai.captureEmbed({ usage |
| 939 | ``` |
| 940 | |
| 941 | For `embedMany`, pass the batch count: |
| 942 | |
| 943 | ```typescript |
| 944 | ai.captureEmbed({ usage |
| 945 | ``` |
| 946 | |
| 947 | ### Cost estimation |
| 948 | |
| 949 | Pass a pricing map to get `ai.estimatedCost` in the wide event: |
| 950 | |
| 951 | ```typescript |
| 952 | const ai = createAILogger |
| 953 | cost: { |
| 954 | "claude-sonnet-4.6": { input: |
| 955 | "gpt-4o": { input: |
| 956 | }, |
| 957 | }); |
| 958 | ``` |
| 959 | |
| 960 | ### Wide event ` |
| 961 | |
| 962 | Includes: `calls`, `model`, ` |
| 963 | |
| 964 | Anti-patterns to detect: |
| 965 | |
| 966 | | Anti-Pattern | Fix | |
| 967 | | -------------------------------------- | ------------------------------------------------------------------------- | |
| 968 | | Manual token tracking in `onFinish` | `ai.wrap() |
| 969 | | `console.log('tokens:', result.usage)` | `ai.wrap() |
| 970 | | No AI observability | Add `createAILogger(log)` + `ai.wrap() |
| 971 | | No tool execution timing | Add `createEvlogIntegration(ai)` to `experimental_telemetry.integrations |
| 972 | | Manual cost calculation | Use `cost` option in `createAILogger() |
| 973 | |
| 974 | --- |
| 975 | |
| 976 | ## Structured Errors |
| 977 | |
| 978 | ```typescript |
| 979 | import { createError } |
| 980 | |
| 981 | // Minimal |
| 982 | throw createError({ message: |
| 983 | |
| 984 | // Standard |
| 985 | throw createError({ message: |
| 986 | |
| 987 | // Complete |
| 988 | throw createError({ |
| 989 | message: "Payment failed", |
| 990 | status: 402, |
| 991 | why: "Card declined by issuer - insufficient funds", |
| 992 | fix: "Please use a different payment method or contact your bank", |
| 993 | link: "https://docs.example.com/payments/declined", |
| 994 | cause: originalError, |
| 995 | }); |
| 996 | |
| 997 | // Backend-only context (wide events / drains — never HTTP body or parseError()) |
| 998 | throw createError({ |
| 999 | message: "Not allowed", |
| 1000 | status: 403, |
| 1001 | why: "Insufficient permissions", |
| 1002 | internal: { correlationId: " |
| 1003 | }); |
| 1004 | ``` |
| 1005 | |
| 1006 | Frontend — extract user-facing fields with `parseError()` (`internal` is never returned to clients): |
| 1007 | |
| 1008 | ```typescript |
| 1009 | import { parseError } |
| 1010 | |
| 1011 | const error = parseError |
| 1012 | // error.message, error.status, error.why, error.fix, error.link |
| 1013 | ``` |
| 1014 | |
| 1015 | See [references/structured-errors.md](references/structured-errors.md |
| 1016 | |
| 1017 | --- |
| 1018 | |
| 1019 | ## Anti-Patterns to Detect |
| 1020 | |
| 1021 | | Anti-Pattern | Fix | |
| 1022 | | ------------------------------------------------ | ------------------------------------------------------------------------------------- | |
| 1023 | | Multiple `console.log` in one function | Single wide event with `log.set() |
| 1024 | | `throw new Error('...')` | `throw createError({ message, status, why, fix }) |
| 1025 | | `console.error(e); throw e` | `log.error(e); throw createError(...) |
| 1026 | | No logging in request handlers | Add `useLogger(event)` / `useLogger() |
| 1027 | | Flat log data `{ uid, n, t }` | Grouped objects: `{ user: {...}, cart: {...} } |
| 1028 | | Logging sensitive data `log.set({ user: body })` | Explicit fields: `{ user: { id: body.id, plan: body.plan } } |
| 1029 | | Putting support-only IDs in `why` / `message` |
| 1030 | |
| 1031 | See [references/code-review.md](references/code-review.md |
| 1032 | |
| 1033 | --- |
| 1034 | |
| 1035 | ## Loading Reference Files |
| 1036 | |
| 1037 | Load based on what you're working on — **do not load all at once**: |
| 1038 | |
| 1039 | - Designing wide events → [references/wide-events.md](references/wide-events.md |
| 1040 | - Improving errors → [references/structured-errors.md](references/structured-errors.md |
| 1041 | - Full code review → [references/code-review.md](references/code-review.md |
| 1042 | - Drain pipeline setup → [references/drain-pipeline.md](references/drain-pipeline.md |