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:
| Category | Examples | Risk |
|---|---|---|
| Credentials | Passwords, API keys, tokens, secrets | Account compromise |
| Payment data | Full card numbers, CVV, bank accounts | PCI compliance violation |
| Personal data (PII) | SSN, passport numbers, driver's license | Privacy laws (GDPR, CCPA) |
| Health data | Medical records, diagnoses | HIPAA violation |
| Authentication | Session tokens, JWTs, refresh tokens | Session 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',
})
Recommended Field Structure
| Category | Fields |
|---|---|
user | id, plan, role, accountAge |
request | method, path, requestId, traceId |
cart / order | items, total, currency, coupon |
payment | method, provider, last4, status |
outcome | status, 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
- Wide Events - Design effective wide events
- Structured Errors - Error handling patterns