Logging Package

On this page 48

A comprehensive logging system powered by Clarity, providing structured logging, multiple output channels, log levels, file rotation, and debugging utilities.

Installation

bun add @stacksjs/logging

Basic Usage

import { log } from '@stacksjs/logging'

// Log messages at different levels
log.info('Application started')
log.success('User created successfully')
log.warn('API rate limit approaching')
log.error('Failed to connect to database')
log.debug('Processing request:', { userId: 123 })

Log Levels

Available Levels

import { log } from '@stacksjs/logging'

// Info - General information
log.info('Server listening on port 3000')

// Success - Successful operations
log.success('Build completed in 2.3s')

// Warning - Potential issues
log.warn('Deprecated API endpoint used')
log.warning('Memory usage above 80%') // Alias

// Error - Errors and failures
log.error('Database connection failed')
log.error(new Error('Something went wrong'))

// Debug - Detailed debugging information
log.debug('Request headers:', headers)
log.debug('SQL query:', query)

Log Level Configuration

# Set minimum log level
LOG_LEVEL=info

# Available levels: debug, info, warn, error
# debug shows all, error shows only errors

There is no Logger class to construct. logger() resolves the configured instance, and the level comes from config/logging.ts or LOG_LEVEL:

import { logger } from '@stacksjs/logging'

const instance = await logger()
await instance.debug('...')
LOG_LEVEL=debug buddy dev

For everyday use, log is the facade over that instance and needs no resolution: log.info(...), log.warn(...), log.error(...).

Debugging Utilities

Dump

import { dump, log } from '@stacksjs/logging'

// Dump variables for debugging (doesn't stop execution)
dump(user)
dump({ name: 'John', roles: ['admin'] })

// Via log
log.dump(someObject)
log.dump(complexData, 'with label')

Dump and Die

import { dd } from '@stacksjs/logging'

// Dump and exit (stops execution)
dd(user)
// Program exits after this

// Via log
log.dd(object) // Logs and exits with code 1

Echo

import { echo } from '@stacksjs/logging'

// Simple echo (less verbose than info)
echo('Simple message')
echo(object) // Pretty prints objects

Structured Logging

Logging Objects

log.info('User action', {
  userId: 123,
  action: 'login',
  ip: '192.168.1.1',
  timestamp: new Date()
})

log.error('Request failed', {
  error: error.message,
  stack: error.stack,
  requestId: 'abc-123'
})

Multiple Arguments

// Multiple values are formatted and joined
log.info('Processing', itemCount, 'items')
log.debug('User:', user, 'Roles:', roles)

Timing Operations

Time Tracking

// Start a timer
const endTimer = log.time('database-query')

// Perform operation
await database.query(sql)

// End timer and log duration
await endTimer({ rows: results.length })
// Output: [database-query] 125ms { rows: 50 }

Manual Timing

const start = Date.now()

// Do work...

log.info(`Operation completed in ${Date.now() - start}ms`)

File Logging

Default Configuration

// config/logging.ts
import type { LoggingConfig } from '@stacksjs/types'
import { storagePath } from '@stacksjs/path'

export default {
  // Set to `null` to stop writing logs to a file.
  logsPath: storagePath('logs/stacks.log'),
  deploymentsPath: storagePath('logs/deployments.log'),
  transports: [],
} satisfies LoggingConfig

The level is LOG_LEVEL in the environment, not a config key - it is the one logging setting you routinely change per run.

Log Files

Logs are written to the configured directory:

storage/logs/
  stacks-2024-01-15.log
  stacks-2024-01-16.log
  stacks-error-2024-01-15.log

File Format

[2024-01-15 10:30:45] INFO: Application started
[2024-01-15 10:30:46] SUCCESS: Database connected
[2024-01-15 10:30:47] ERROR: Failed to load config {"file":"app.config.ts"}

Error Handling Integration

Logging Errors

import { log } from '@stacksjs/logging'
import { handleError } from '@stacksjs/error-handling'

try {
  await riskyOperation()
} catch (error) {
  // Log error with context
  log.error(error, {
    context: 'riskyOperation',
    userId: currentUser.id
  })

  // Or use error handling integration
  handleError(error)
}

Error Options

log.error(error, {
  shouldExit: true,  // Exit process after logging
  silent: false,     // Show in console
  message: 'Custom error message'
})

Extra Destinations

There is no Logger class and no per-module logger to construct. Everything goes through one pipeline; to send records somewhere else, attach a transport:

import { registerTransport } from '@stacksjs/logging'

const buffer: unknown[] = []

const detach = registerTransport({
  name: 'my-log-service',
  level: 'info',

  // Must return immediately. A transport doing network I/O buffers here and
  // delivers on its own timer - blocking this call blocks the caller's log.
  log: record => void buffer.push(record),

  // Called on `beforeExit`, so the last records are not lost.
  flush: async () => { await deliver(buffer.splice(0)) },
})

// Later
detach()

A transport receives the record BEFORE formatting, so args still holds the real Error and the real context object rather than their rendered strings.

Transports can also be declared in config/logging.ts, which is the right place for one that should exist for the life of the process.

To label records instead of separating them, add context:

import { log, withLogContext } from '@stacksjs/logging'

await withLogContext({ module: 'api', requestId }, async () => {
  // Every log inside carries that context, including from functions
  // this one calls - it rides on AsyncLocalStorage, not on the call.
  await log.info('API request received')
})

Context and Metadata

Adding Context

// Log with structured context
log.info('Order processed', {
  orderId: 'ORD-12345',
  customerId: 'CUST-67890',
  amount: 99.99,
  items: 3
})

// Error with context
log.error('Payment failed', {
  orderId: 'ORD-12345',
  error: paymentError.message,
  gateway: 'stripe'
})

Request Context

// In middleware or action
async function handle(request) {
  const requestId = crypto.randomUUID()

  log.info('Request received', {
    requestId,
    method: request.method,
    path: request.url,
    userAgent: request.headers.get('user-agent')
  })

  // ... handle request

  log.info('Response sent', {
    requestId,
    status: 200,
    duration: Date.now() - start
  })
}

Console Output

Fancy Mode

When fancy: true (default), logs are formatted with colors and symbols:

 INFO  Application started
 SUCCESS  Database connected  ✓
 WARN  Memory usage high
 ERROR  Connection failed  ✗
 DEBUG  Query executed in 15ms

Plain Mode

When fancy: false, simpler output:

[INFO] Application started
[SUCCESS] Database connected
[WARN] Memory usage high
[ERROR] Connection failed

Environment-Based Configuration

// Automatic configuration based on environment
const logger = new Logger('app', {
  // More verbose in development
  level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',

  // File logging in production
  writeToFile: process.env.NODE_ENV === 'production',

  // Fancy output in development
  fancy: process.env.NODE_ENV !== 'production'
})

Performance Logging

Measuring Performance

// Time a function
const timer = log.time('expensive-operation')

const result = await expensiveOperation()

await timer({ resultSize: result.length })

Query Logging

// Log database queries
log.debug('SQL Query', {
  query: sql,
  params: bindings,
  duration: queryTime
})

Log Rotation

Logs are automatically rotated daily:

storage/logs/
  stacks-2024-01-13.log
  stacks-2024-01-14.log
  stacks-2024-01-15.log  (current)

Custom Rotation

// Configure via Clarity
import { Logger } from '@stacksjs/clarity'

const logger = new Logger('app', {
  rotation: {
    maxSize: '10MB',
    maxFiles: 30,
    compress: true
  }
})

Edge Cases

Handling Circular References

// Objects with circular references are safely stringified
const obj = { name: 'test' }
obj.self = obj

log.info('Circular object:', obj)
// Output: Circular object: {"name":"test","self":"[Circular]"}

Handling Large Objects

// Large objects are truncated in console
const largeArray = new Array(10000).fill({ data: 'value' })
log.debug('Large array:', largeArray)
// Console shows truncated version, file has full log

Async Logging

// Logging is async but you usually don't need to await
log.info('This logs asynchronously')

// Await if you need to ensure log is written
await log.info('Important message')

Graceful Shutdown

// Ensure logs are flushed before exit
process.on('SIGTERM', async () => {
  await log.info('Shutting down...')
  // Wait for logs to flush
  await new Promise(resolve => setTimeout(resolve, 100))
  process.exit(0)
})

Best Practices

Structured Logging

// Good: Structured data
log.info('User action', { userId: 1, action: 'login' })

// Avoid: String concatenation
log.info(`User ${userId} performed action ${action}`)

Appropriate Log Levels

// DEBUG: Development details
log.debug('SQL query executed', { sql, duration })

// INFO: Business events
log.info('Order created', { orderId })

// WARN: Potential issues
log.warn('API deprecation', { endpoint, deadline })

// ERROR: Failures
log.error('Payment failed', { error, orderId })

Sensitive Data

// Don't log sensitive data
log.info('User login', {
  userId: user.id,
  email: user.email,
  // password: user.password  // Never log passwords!
})

// Mask sensitive fields
log.info('Payment processed', {
  cardLast4: card.number.slice(-4),
  // cardNumber: card.number  // Never log full card numbers!
})

API Reference

Log Methods

MethodDescription
log.info(...args)Info level message
log.success(msg)Success level message
log.warn(msg)Warning level message
log.warning(msg)Alias for warn
log.error(err, opts?)Error level message
log.debug(...args)Debug level message
log.dump(...args)Dump objects for debugging
log.dd(...args)Dump and die
log.echo(...args)Simple echo
log.time(label)Start timer, returns end function

Helper Functions

FunctionDescription
dump(...args)Dump objects
dd(...args)Dump and die
echo(...args)Echo output
logger()Get the logger, once config/logging.ts has been applied

Logger Class

MethodDescription
new Logger(name, opts)Create logger
logger.info(msg)Info message
logger.success(msg)Success message
logger.warn(msg)Warning message
logger.error(msg)Error message
logger.debug(msg)Debug message
logger.time(label)Timer function