review-logging-patterns
npx skills add https://github.com/hugorcd/evlog --skill review-logging-patterns
Agent 安装分布
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.logspam â wide events withuseLogger(event)throw new Error('...')âcreateError({ message, status, why, fix })- Scattered request logs â
useLogger(event)(Nuxt/Nitro) orcreateRequestLogger()(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:
- Client logs are sent to
/api/_evlog/ingestvia POST - Server enriches with environment context (service, version, etc.)
evlog:drainhook is called withsource: 'client'- 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:
- Console.log statements â Replace with
useLogger(event).set()or wide events - Generic errors â Add
status,why,fix, andlinkfields withcreateError() - Scattered request logs â Use
useLogger(event)(Nuxt/Nitro) orcreateRequestLogger()(standalone) - Missing context â Add user, business, and outcome context with
log.set() - No duration tracking â Let
emit()handle it automatically - No frontend error handling â Catch errors and display toasts with structured data
- Sensitive data in logs â Check for passwords, tokens, full card numbers, PII
- Client-side logging â Use
logAPI for debugging in Vue components - Client log centralization â Enable
transport.enabled: trueto send client logs to server - Missing log draining â Set up adapters (
evlog/axiom,evlog/otlp,evlog/posthog,evlog/sentry) for production log export - 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:
- Designing wide events â references/wide-events.md
- Improving errors â references/structured-errors.md
- Full code review â references/code-review.md
- Drain pipeline setup â references/drain-pipeline.md
DO NOT load all files at once – load only what’s needed for the current task.