This is a demo site.Purchase now

Logging

Structured logging throughout the application

This boilerplate includes a structured logging utility that provides context-aware logging with multiple severity levels, making it easy to track application behavior and debug issues.

Overview

The logging system provides:

  • Structured logging - Log objects with context instead of plain strings
  • Multiple log levels - Error, warn, info, and debug
  • Context support - Attach metadata to any log entry
  • Auto-imported - Available throughout the application without explicit imports

Logger utility

The logger is located in shared/utils/logger.ts and provides structured logging with context support:

import { logger } from '@@/shared/utils/logger'

// Basic logging
logger.info('User signed up', { userId, email })
logger.warn('Rate limit approaching', { ip, count })
logger.error('Database query failed', error)

// With additional context
logger.error('Failed to process payment', error, {
  userId,
  amount,
  currency,
  paymentId,
})

Log levels

LevelWhen to useExample
errorUnexpected failures, exceptionsDatabase errors, API failures
warnConcerning but handled situationsRate limits, deprecation warnings
infoImportant state changes, successful operationsUser signups, successful payments
debugDetailed flow information (development only)Request/response details, function calls

Usage examples

Basic logging

// Information logging
logger.info('Product created', { productId, userId })

// Warning logging
logger.warn('Cache miss', { key, timestamp })

// Error logging
logger.error('Failed to send email', error)

Logging with context

Add relevant metadata to help with debugging:

logger.info('Payment processed', { 
  userId, 
  amount, 
  currency, 
  paymentId,
  processor: 'stripe',
})

logger.error('Payment failed', error, {
  userId,
  amount,
  attemptCount,
  lastError: error.message,
})

Logging in server services

export async function createProduct(data: CreateProductData) {
  logger.info('Creating product', { name: data.name, price: data.price })

  const product = await prisma.product.create({ data })

  logger.info('Product created', { productId: product.id })

  return product
}

Logging in API routes

export default defineEventHandler(async event => {
  const userId = await requireAuth(event)

  logger.info('Fetching user products', { userId })

  const products = await getProducts(userId)

  logger.info('Retrieved products', { userId, count: products.length })

  return products
})

Logging errors with context

export async function processPayment(paymentData: PaymentData) {
  try {
    const charge = await stripe.charges.create(paymentData)
    logger.info('Payment processed', { chargeId: charge.id, amount: charge.amount })
    return charge
  } catch (error) {
    logger.error('Payment processing failed', error, {
      amount: paymentData.amount,
      currency: paymentData.currency,
      customerId: paymentData.customer,
    })
    throw error
  }
}

Best practices

Use structured data

Use objects for logging instead of string concatenation:

// Good - structured data
logger.info('Payment processed', { 
  userId, 
  amount, 
  currency, 
  paymentId 
})

// Avoid - string concatenation
logger.info(`User ${userId} paid ${amount} ${currency}`)

Include relevant context

Add context that helps with debugging:

logger.error('Failed to update user', error, {
  userId,
  attemptedChanges: changes,
  timestamp: new Date().toISOString(),
})

Avoid logging sensitive data

Never log sensitive information:

// Never do this
logger.info('User login', { email, password })
logger.info('Payment details', { creditCard, cvv })

// Do this instead
logger.info('User login', { email })
logger.info('Payment processed', { last4, brand })

Choose appropriate log levels

// Error - something went wrong
logger.error('Database connection failed', error)

// Warn - concerning but not critical
logger.warn('Slow query detected', { query, duration })

// Info - important events
logger.info('User registered', { userId, email })

// Debug - detailed information (dev only)
logger.debug('Cache lookup', { key, hit: true })

API reference

logger.error(message: string, error?: unknown, context?: Record<string, unknown>)
logger.warn(message: string, context?: Record<string, unknown>)
logger.info(message: string, context?: Record<string, unknown>)
logger.debug(message: string, context?: Record<string, unknown>)

Parameters

  • message - A descriptive message about what happened
  • error (optional) - An error object (for logger.error)
  • context (optional) - Additional metadata as an object

Production logging

In production, consider:

  1. Log aggregation - Send logs to services like Datadog, LogDNA, or CloudWatch
  2. Log levels - Filter logs by severity in production
  3. Performance - Avoid logging in hot paths or tight loops
  4. Retention - Configure appropriate log retention policies

Example: Integrating with log aggregation

// In your logger utility or server middleware
import { Logger } from '@datadog/browser-logs'

export function logToDatadog(level: string, message: string, context?: object) {
  if (process.env.NODE_ENV === 'production') {
    Logger.log(message, context, level)
  }
}
  • shared/utils/logger.ts - Logger utility implementation
  • server/middleware/99-error-handler.ts - Uses logger for error logging