review-logging-patterns

📁 hugorcd/evlog 📅 Jan 26, 2026
63
总安装量
63
周安装量
#3470
全站排名
安装命令
npx skills add https://github.com/hugorcd/evlog --skill review-logging-patterns

Agent 安装分布

opencode 51
github-copilot 43
codex 43
gemini-cli 41
claude-code 38
cursor 34

Skill 文档

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

Use this skill when:

  • Reviewing code for logging best practices
  • User asks to improve their logging
  • Converting console.log statements to structured logging
  • Improving error handling with better context
  • Setting up request-scoped logging in API routes
  • Debugging why logs are hard to search/filter

Key transformations:

  • console.log spam → wide events with useLogger(event)
  • throw new Error('...') → createError({ message, status, why, fix })
  • Scattered request logs → useLogger(event) (Nuxt/Nitro) or createRequestLogger() (standalone)

Quick Reference

Working on… Resource
Wide events patterns references/wide-events.md
Error handling references/structured-errors.md
Code review checklist references/code-review.md
Log draining & adapters See “Log Draining & Adapters” section below
Drain pipeline references/drain-pipeline.md

Important: Auto-imports in Nuxt

In Nuxt applications, all evlog functions are auto-imported – no import statements needed:

// server/api/checkout.post.ts
export default defineEventHandler(async (event) => {
  // useLogger is auto-imported - no import needed!
  const log = useLogger(event)
  log.set({ user: { id: 1, plan: 'pro' } })
  return { success: true }
})
<!-- In Vue components - log is auto-imported -->
<script setup>
log.info('checkout', 'User initiated checkout')
</script>

Core Philosophy

The Problem with Traditional Logging

// ❌ Scattered logs - impossible to correlate during incidents
console.log('Request received')
console.log('User authenticated')
console.log('Loading cart')
console.log('Processing payment')
console.log('Payment failed')

The Solution: Wide Events

// server/api/checkout.post.ts
// No import needed in Nuxt - useLogger is auto-imported!

// ✅ One comprehensive event per request
export default defineEventHandler(async (event) => {
  const log = useLogger(event)

  log.set({ user: { id: '123', plan: 'premium' } })
  log.set({ cart: { items: 3, total: 9999 } })
  log.error(error, { step: 'payment' })

  // emit() called automatically at request end
})

Anti-Patterns to Detect

1. Console.log Spam

// ❌ Multiple logs for one logical operation
console.log('Starting checkout')
console.log('User:', userId)
console.log('Cart:', cart)
console.log('Payment result:', result)

Transform to:

// ✅ Single wide event
log.info({
  action: 'checkout',
  userId,
  cart,
  result,
  duration: '1.2s'
})

2. Generic Error Messages

// ❌ Useless error
throw new Error('Something went wrong')

// ❌ Missing context
throw new Error('Payment failed')

Transform to:

import { createError } from 'evlog'

// ✅ Self-documenting error
throw createError({
  message: 'Payment failed',
  status: 402,
  why: 'Card declined by issuer',
  fix: 'Try a different payment method or contact your bank',
  link: 'https://docs.example.com/payments/declined',
  cause: originalError,
})

3. Missing Request Context

// server/api/orders.post.ts

// ❌ No way to correlate logs
export default defineEventHandler(async (event) => {
  console.log('Processing request')
  const user = await getUser(event)
  console.log('Got user', user.id)
  // ...
})

Transform to (Nuxt/Nitro):

// server/api/orders.post.ts
// useLogger is auto-imported in Nuxt - no import needed!

// ✅ Request-scoped with full context
export default defineEventHandler(async (event) => {
  const log = useLogger(event)

  const user = await getUser(event)
  log.set({ user: { id: user.id, plan: user.plan } })

  // ... do work, accumulate context ...

  // emit() called automatically
})

Transform to (Standalone TypeScript):

// scripts/process-job.ts
import { createRequestLogger } from 'evlog'

const log = createRequestLogger({ jobId: job.id, type: 'sync' })

log.set({ source: job.source, target: job.target })
// ... do work ...
log.emit()  // Manual emit for standalone usage

Installation

npm install evlog

Nuxt Integration

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['evlog/nuxt'],
  evlog: {
    env: {
      service: 'my-app',
      environment: process.env.NODE_ENV,
    },
    // Optional: only log specific routes (supports glob patterns)
    include: ['/api/**'],
    // Optional: send client logs to server (default: false)
    transport: {
      enabled: true,
    },
  },
})

Nitro Integration

// nitro.config.ts
export default defineNitroConfig({
  plugins: ['evlog/nitro'],
})

Structured Error Levels

Not all errors need the same level of detail. Use the appropriate level:

Minimal (internal errors)

throw createError({ message: 'Database connection failed', status: 500 })

Standard (user-facing errors)

throw createError({
  message: 'Payment failed',
  status: 402,
  why: 'Card declined by issuer',
})

Complete (documented errors with actionable fix)

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',
})

Frontend Integration

evlog errors work with any Nitro-powered framework. When thrown, they’re automatically converted to HTTP responses with structured data.

Use parseError() to extract all fields at the top level:

import { createError, parseError } from 'evlog'

// Backend - just throw the error
throw createError({
  message: 'Payment failed',
  status: 402,
  why: 'Card declined',
  fix: 'Try another card',
  link: 'https://docs.example.com/payments',
})

// Frontend - use parseError() for direct access
try {
  await $fetch('/api/checkout')
} catch (err) {
  const error = parseError(err)

  // Direct access: error.message, error.why, error.fix, error.link
  toast.add({
    title: error.message,
    description: error.why,
    color: 'error',
    actions: error.link
      ? [{ label: 'Learn more', onClick: () => window.open(error.link) }]
      : undefined,
  })

  if (error.fix) console.info(`💡 Fix: ${error.fix}`)
}

The difference: A generic error shows “An error occurred”. A structured error shows the message, explains why, suggests a fix, and links to docs.

Client-Side Logging

The log API works on both server and client. In Nuxt, it’s auto-imported:

// In Vue components, composables, or client-side code
log.info('checkout', 'User initiated checkout')
log.error({ action: 'payment', error: 'validation_failed' })
log.warn('form', 'Invalid email format')
log.debug({ component: 'CartDrawer', itemCount: 3 })

Client logs output to the browser console with colored tags in development.

Client Transport

To send client logs to the server for centralized logging, enable the transport:

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['evlog/nuxt'],
  evlog: {
    transport: {
      enabled: true,  // Send client logs to server
    },
  },
})

When enabled:

  1. Client logs are sent to /api/_evlog/ingest via POST
  2. Server enriches with environment context (service, version, etc.)
  3. evlog:drain hook is called with source: 'client'
  4. External services receive the log

Identify client logs in your drain hook:

nitroApp.hooks.hook('evlog:drain', async (ctx) => {
  if (ctx.event.source === 'client') {
    // Handle client logs specifically
  }
})

Log Draining & Adapters

evlog provides built-in adapters to send logs to external observability platforms.

Built-in Adapters

Adapter Import Use Case
Axiom evlog/axiom Axiom datasets for querying and dashboards
OTLP evlog/otlp OpenTelemetry for Grafana, Datadog, Honeycomb, etc.
PostHog evlog/posthog PostHog for product analytics and event tracking
Sentry evlog/sentry Sentry for error tracking and performance monitoring

Quick Setup

Axiom:

// server/plugins/evlog-drain.ts
import { createAxiomDrain } from 'evlog/axiom'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createAxiomDrain())
})

Set NUXT_AXIOM_TOKEN and NUXT_AXIOM_DATASET environment variables.

OTLP:

// server/plugins/evlog-drain.ts
import { createOTLPDrain } from 'evlog/otlp'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createOTLPDrain())
})

Set NUXT_OTLP_ENDPOINT environment variable.

PostHog:

// server/plugins/evlog-drain.ts
import { createPostHogDrain } from 'evlog/posthog'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createPostHogDrain())
})

Set NUXT_POSTHOG_API_KEY and NUXT_POSTHOG_HOST environment variables.

Sentry:

// server/plugins/evlog-drain.ts
import { createSentryDrain } from 'evlog/sentry'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', createSentryDrain())
})

Set NUXT_SENTRY_DSN environment variable.

Multiple Destinations

import { createAxiomDrain } from 'evlog/axiom'
import { createOTLPDrain } from 'evlog/otlp'

export default defineNitroPlugin((nitroApp) => {
  const axiom = createAxiomDrain()
  const otlp = createOTLPDrain()

  nitroApp.hooks.hook('evlog:drain', async (ctx) => {
    await Promise.allSettled([axiom(ctx), otlp(ctx)])
  })
})

Custom Adapter

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', async (ctx) => {
    // ctx.event contains the full wide event
    await fetch('https://your-service.com/logs', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(ctx.event),
    })
  })
})

Drain Pipeline (Production)

For production use, wrap any adapter with createDrainPipeline to get batching, retry with backoff, and buffer overflow protection. Without a pipeline, each event triggers a separate network call.

import type { DrainContext } from 'evlog'
import { createDrainPipeline } from 'evlog/pipeline'
import { createAxiomDrain } from 'evlog/axiom'

export default defineNitroPlugin((nitroApp) => {
  const pipeline = createDrainPipeline<DrainContext>({
    batch: { size: 50, intervalMs: 5000 },
    retry: { maxAttempts: 3, backoff: 'exponential' },
    onDropped: (events, error) => {
      console.error(`[evlog] Dropped ${events.length} events:`, error?.message)
    },
  })

  const drain = pipeline(createAxiomDrain())

  nitroApp.hooks.hook('evlog:drain', drain)
  nitroApp.hooks.hook('close', () => drain.flush())
})

Key options: batch.size (default 50), batch.intervalMs (default 5000), retry.maxAttempts (default 3), retry.backoff ('exponential' | 'linear' | 'fixed'), maxBufferSize (default 1000).

See references/drain-pipeline.md for full patterns and options.

Security: Preventing Sensitive Data Leakage

Wide events capture comprehensive context, making it easy to accidentally log sensitive data.

What NOT to Log

Category Examples Risk
Credentials Passwords, API keys, tokens Account compromise
Payment data Full card numbers, CVV PCI violation
Personal data (PII) SSN, unmasked emails GDPR/CCPA violation
Authentication Session tokens, JWTs Session hijacking

Safe Logging Pattern

// ❌ DANGEROUS - logs everything including password
const body = await readBody(event)
log.set({ user: body })

// ✅ SAFE - explicitly select fields
log.set({
  user: {
    id: body.id,
    plan: body.plan,
    // password: body.password ← NEVER include
  },
})

Sanitization Helpers

// server/utils/sanitize.ts
export function maskEmail(email: string): string {
  const [local, domain] = email.split('@')
  return `${local[0]}***@${domain}`
}

export function maskCard(card: string): string {
  return `****${card.slice(-4)}`
}

Review Checklist

When reviewing code, check for:

  1. Console.log statements → Replace with useLogger(event).set() or wide events
  2. Generic errors → Add status, why, fix, and link fields with createError()
  3. Scattered request logs → Use useLogger(event) (Nuxt/Nitro) or createRequestLogger() (standalone)
  4. Missing context → Add user, business, and outcome context with log.set()
  5. No duration tracking → Let emit() handle it automatically
  6. No frontend error handling → Catch errors and display toasts with structured data
  7. Sensitive data in logs → Check for passwords, tokens, full card numbers, PII
  8. Client-side logging → Use log API for debugging in Vue components
  9. Client log centralization → Enable transport.enabled: true to send client logs to server
  10. Missing log draining → Set up adapters (evlog/axiom, evlog/otlp, evlog/posthog, evlog/sentry) for production log export
  11. No drain pipeline → Wrap adapters with createDrainPipeline() for batching, retry, and buffer overflow protection

Loading Reference Files

Load reference files based on what you’re working on:

DO NOT load all files at once – load only what’s needed for the current task.