Core Concepts

Best Practices

Security guidelines, data sanitization, and production tips for evlog.

This guide covers security best practices and production considerations for evlog.

What NOT to Log

Wide events are powerful because they capture comprehensive context. However, this makes it easy to accidentally log sensitive data. Never log:

CategoryExamplesRisk
CredentialsPasswords, API keys, tokens, secretsAccount compromise
Payment dataFull card numbers, CVV, bank accountsPCI compliance violation
Personal data (PII)SSN, passport numbers, driver's licensePrivacy laws (GDPR, CCPA)
Health dataMedical records, diagnosesHIPAA violation
AuthenticationSession tokens, JWTs, refresh tokensSession hijacking
Logs are often accessible to your entire team and may be stored in third-party services. Treat them as semi-public.

Sanitization Patterns

Manual Field Selection

The safest approach is to explicitly select which fields to log:

// server/api/user/update.post.ts
export default defineEventHandler(async (event) => {
  const log = useLogger(event)
  const body = await readBody(event)

  // ❌ NEVER log the entire request body
  // log.set({ body })

  // ✅ Explicitly select safe fields
  log.set({
    user: {
      id: body.id,
      email: maskEmail(body.email),
      // password: body.password ← NEVER include
    },
  })
})

Helper Functions

Create utility functions to sanitize common data types:

// server/utils/sanitize.ts

/** Masks email: john.doe@example.com → j***.d**@e***.com */
export function maskEmail(email: string): string {
  const [local, domain] = email.split('@')
  if (!domain) return '***'
  const [domainName, tld] = domain.split('.')
  return `${local[0]}***@${domainName[0]}***.${tld}`
}

/** Masks card number: 4242424242424242 → ****4242 */
export function maskCard(card: string): string {
  return `****${card.slice(-4)}`
}

/** Truncates long IDs for readability */
export function truncateId(id: string, length = 8): string {
  if (id.length <= length) return id
  return `${id.slice(0, length)}...`
}

/** Removes sensitive fields from an object */
export function sanitize<T extends Record<string, unknown>>(
  obj: T,
  sensitiveKeys: string[] = ['password', 'token', 'secret', 'apiKey', 'authorization']
): Partial<T> {
  const result = { ...obj }
  for (const key of sensitiveKeys) {
    if (key in result) {
      delete result[key]
    }
  }
  return result
}

Usage:

// server/api/checkout.post.ts
export default defineEventHandler(async (event) => {
  const log = useLogger(event)
  const { user, card } = await readBody(event)

  log.set({
    user: {
      id: user.id,
      email: maskEmail(user.email),
    },
    payment: {
      last4: maskCard(card.number),
      // ❌ Never: number, cvv, expiry
    },
  })
})

Drain Hook Filtering

As a last line of defense, filter sensitive data before sending to external services:

// server/plugins/evlog-sanitize.ts
const SENSITIVE_KEYS = ['password', 'token', 'secret', 'apiKey', 'authorization', 'cookie']

function deepSanitize(obj: Record<string, unknown>): Record<string, unknown> {
  const result: Record<string, unknown> = {}

  for (const [key, value] of Object.entries(obj)) {
    if (SENSITIVE_KEYS.some(k => key.toLowerCase().includes(k))) {
      result[key] = '[REDACTED]'
    } else if (value && typeof value === 'object' && !Array.isArray(value)) {
      result[key] = deepSanitize(value as Record<string, unknown>)
    } else {
      result[key] = value
    }
  }

  return result
}

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', (ctx) => {
    // Sanitize before sending to external service
    ctx.event = deepSanitize(ctx.event) as typeof ctx.event
  })
})
Drain hook sanitization is a safety net, not a replacement for careful logging practices. Always sanitize at the source.

Production Checklist

Before deploying to production, verify:

Logging Configuration

  • Service name is set (env.service)
  • Sampling is configured for high-traffic routes
  • Log draining is set up for external service (Axiom, Loki, etc.)
  • Pretty mode is disabled in production (pretty: false)

Data Security

  • No passwords or secrets in logs
  • No full credit card numbers (only last 4 digits)
  • No API keys or tokens
  • PII is masked or omitted (emails, phone numbers)
  • Session tokens are not logged
  • Request bodies are selectively logged (not log.set({ body }))

Error Handling

  • Errors use createError() with structured fields
  • Sensitive data is not included in error messages
  • Stack traces don't expose internal paths in production

Field Naming Conventions

Use consistent, grouped field names across your codebase:

// ✅ Good - grouped and descriptive
log.set({
  user: { id, plan, accountAge },
  cart: { items, total, currency },
  payment: { method, provider, last4 },
})

// ❌ Bad - flat and abbreviated
log.set({
  uid: '123',
  n: 3,
  t: 9999,
  pm: 'card',
})
CategoryFields
userid, plan, role, accountAge
requestmethod, path, requestId, traceId
cart / orderitems, total, currency, coupon
paymentmethod, provider, last4, status
outcomestatus, duration, error

Sampling Strategy

At scale, log volume can become expensive. Use sampling wisely:

// nuxt.config.ts
export default defineNuxtConfig({
  evlog: {
    sampling: {
      // Head sampling: random percentage per level
      rates: {
        info: 10,    // 10% of success logs
        warn: 50,    // 50% of warnings
        debug: 0,    // No debug logs in prod
        error: 100,  // Always keep errors
      },
      // Tail sampling: force-keep based on outcome
      keep: [
        { duration: 1000 },           // Slow requests (≥1s)
        { status: 400 },              // Client/server errors
        { path: '/api/payments/**' }, // Critical paths
      ],
    },
  },
})
Use $production override to keep full logging in development while sampling in production. See Installation.

Next Steps

Copyright © 2026