DeployStack Docs

Backend Log Level Configuration

The DeployStack backend uses Pino logger with Fastify for high-performance, structured logging. This guide covers everything you need to know about configuring and using log levels effectively.

Overview

DeployStack's logging system is built on industry best practices:

  • Pino Logger: Ultra-fast JSON logger for Node.js
  • Fastify Integration: Native logging support with request correlation
  • Environment-based Configuration: Automatic log level adjustment based on NODE_ENV
  • Structured Logging: JSON output for production, pretty-printed for development

Available Log Levels

Log levels are ordered by severity (lowest to highest):

LevelNumeric ValueDescriptionWhen to Use
trace10Very detailed debuggingTracing function calls, variable states
debug20Debugging informationDevelopment debugging, detailed flow
info30General informationImportant events, startup messages
warn40Warning messagesRecoverable errors, deprecation notices
error50Error conditionsCaught exceptions, failed operations
fatal60Fatal errorsUnrecoverable errors, application crashes

Configuration

Environment Variables

Set the log level using the LOG_LEVEL environment variable:

# Development - show debug information
LOG_LEVEL=debug npm run dev

# Production - show info and above
LOG_LEVEL=info npm run start

# Troubleshooting - show everything
LOG_LEVEL=trace npm run dev

# Quiet mode - only errors and fatal
LOG_LEVEL=error npm run start

Default Behavior

The logger automatically adjusts based on your environment:

// From src/fastify/config/logger.ts
export const loggerConfig: FastifyServerOptions['logger'] = {
  level: process.env.LOG_LEVEL || (process.env.NODE_ENV === 'production' ? 'info' : 'debug'),
  transport: process.env.NODE_ENV !== 'production'
    ? {
        target: 'pino-pretty',
        options: {
          colorize: true,
          translateTime: 'SYS:standard',
          ignore: 'pid,hostname'
        }
      }
    : undefined
}

Default Levels:

  • Development: debug (shows debug, info, warn, error, fatal)
  • Production: info (shows info, warn, error, fatal)

Log Output Formats

Development Format (Pretty-printed)

[2025-07-03 10:48:06.636 +0200] INFO: ✅ Database initialization completed
[2025-07-03 10:48:06.640 +0200] DEBUG: 🔄 Starting global settings initialization...
[2025-07-03 10:48:06.645 +0200] ERROR: ❌ Failed to connect to external service

Production Format (JSON)

{"level":30,"time":"2025-07-03T08:48:06.636Z","pid":1234,"hostname":"server","msg":"Database initialization completed"}
{"level":20,"time":"2025-07-03T08:48:06.640Z","pid":1234,"hostname":"server","msg":"Starting global settings initialization..."}
{"level":50,"time":"2025-07-03T08:48:06.645Z","pid":1234,"hostname":"server","msg":"Failed to connect to external service"}

Logger Parameter Injection Pattern

DeployStack follows a consistent pattern for passing logger instances to services and utilities. This ensures proper structured logging throughout the application while maintaining the Fastify logger chain for request correlation.

✅ DO: Pass Logger as Parameter to Services

// ✅ Good - Services accept logger as parameter
class EmailService {
  static async sendEmail(options: SendEmailOptions, logger: FastifyBaseLogger): Promise<EmailSendResult> {
    logger.debug({
      operation: 'send_email',
      recipient: options.to,
      template: options.template
    }, 'Sending email');
    
    try {
      // ... email sending logic
      logger.info({
        operation: 'send_email',
        messageId: result.messageId,
        recipients: result.recipients
      }, 'Email sent successfully');
      
      return result;
    } catch (error) {
      logger.error({
        operation: 'send_email',
        error,
        recipient: options.to,
        template: options.template
      }, 'Failed to send email');
      throw error;
    }
  }
}

// ✅ Good - Database functions accept logger parameter
export async function initializeDatabase(logger: FastifyBaseLogger): Promise<boolean> {
  logger.info({
    operation: 'initialize_database'
  }, 'Database initialization started');
  
  try {
    // ... database initialization logic
    logger.info({
      operation: 'initialize_database'
    }, 'Database initialized successfully');
    return true;
  } catch (error) {
    logger.error({
      operation: 'initialize_database',
      error
    }, 'Failed to initialize database');
    return false;
  }
}

✅ DO: Pass Logger from Calling Context

// ✅ Good - Pass logger from server context
await initializeDatabase(server.log);

// ✅ Good - Pass logger from request context in routes
server.post('/api/send-email', async (request, reply) => {
  const result = await EmailService.sendEmail(emailOptions, request.log);
  return result;
});

// ✅ Good - Pass logger in plugin initialization
await pluginManager.initializePlugins(server.log);

✅ DO: Use Child Loggers for Persistent Context

// ✅ Good - Create child logger with persistent context
class UserService {
  static async processUser(userId: string, logger: FastifyBaseLogger) {
    const childLogger = logger.child({ userId, service: 'UserService' });
    
    childLogger.debug('Starting user processing');
    childLogger.info('User data retrieved');
    childLogger.debug('Processing completed');
  }
}

❌ DON'T: Create Separate Logger Utilities

// ❌ Bad - Don't create separate logger utility files
// utils/logger.ts
export const logger = createLogger();

// ❌ Bad - Don't import logger utilities in services
import { logger } from '../utils/logger';

class SomeService {
  static async doSomething() {
    logger.info('This bypasses the Fastify logger chain');
  }
}

❌ DON'T: Use console.* in Services

// ❌ Bad - console.* bypasses the logging system
class DatabaseService {
  static async connect() {
    console.log('Connecting to database...'); // No structured logging
    console.error('Connection failed:', error); // No context objects
  }
}

// ✅ Good - Use passed logger parameter
class DatabaseService {
  static async connect(logger: FastifyBaseLogger) {
    logger.info({
      operation: 'database_connect'
    }, 'Connecting to database...');
    
    logger.error({
      operation: 'database_connect',
      error,
      connectionString: config.url
    }, 'Connection failed');
  }
}

Developer Best Practices

✅ DO: Use Proper Log Levels

// ✅ Good - Use appropriate log levels
server.log.debug('🔄 Starting database initialization...');
server.log.info('✅ Database connection established');
server.log.warn('⚠️ Using fallback configuration');
server.log.error('❌ Failed to process request:', error);
server.log.fatal('💀 Critical system failure:', error);

❌ DON'T: Use Manual Prefixes

// ❌ Bad - Manual prefixes defeat the purpose
server.log.info('🔄 [DEBUG] Starting database initialization...');
server.log.info('✅ [INFO] Database connection established');

// ✅ Good - Use proper log levels instead
server.log.debug('🔄 Starting database initialization...');
server.log.info('✅ Database connection established');

✅ DO: Use the Fastify Logger

// ✅ Good - Use server.log for consistent formatting
server.log.info('User authenticated successfully');

// ❌ Bad - console.log bypasses the logging system
console.log('User authenticated successfully');

✅ DO: Add Context Objects

Context objects make your logs searchable, filterable, and much more useful for debugging. Always include relevant context that helps identify what happened, where, and to whom.

// ✅ Good - Structured logging with context
server.log.info({
  userId: 'user123',
  action: 'login',
  ipAddress: '192.168.1.1',
  userAgent: 'Mozilla/5.0...',
  operation: 'user_authentication'
}, 'User login successful');

// ✅ Good - Error logging with full context
server.log.error({
  error: err,
  userId: 'user123',
  operation: 'database_query',
  table: 'users',
  queryType: 'SELECT'
}, 'Database operation failed');

// ✅ Good - Service operations with context
server.log.debug({
  recipient: '[email protected]',
  template: 'welcome',
  messageId: 'abc123',
  operation: 'send_email'
}, 'Email sent successfully');

// ✅ Good - API operations with context
server.log.warn({
  endpoint: '/api/users',
  method: 'POST',
  statusCode: 429,
  clientIp: '192.168.1.1',
  operation: 'rate_limit_exceeded'
}, 'Rate limit exceeded for client');

Best Practices for Context Objects:

  • Always include operation: A consistent field that describes what operation was being performed
  • Add identifiers: Include relevant IDs (userId, orderId, sessionId, etc.) for easy filtering
  • Include request context: IP addresses, user agents, request IDs for web requests
  • Add timing information: Duration, timestamps, or performance metrics when relevant
  • Use consistent naming: Stick to camelCase and consistent field names across your application

Examples of Good Context Properties:

  • operation: What was happening (e.g., 'send_email', 'user_login', 'database_query')
  • userId, sessionId, requestId: Identifiers for tracking
  • duration, responseTime: Performance metrics
  • statusCode, method, endpoint: HTTP-related context
  • table, queryType: Database-related context
  • recipient, template, messageId: Email-related context

✅ DO: Use Child Loggers for Context

// ✅ Good - Child logger with persistent context
function processUser(userId: string) {
  const childLogger = server.log.child({ userId });
  
  childLogger.debug('Starting user processing');
  childLogger.info('User data retrieved');
  childLogger.debug('Processing completed');
}

Common Logging Patterns

Database Operations

// Database initialization
server.log.debug('🔄 Initializing database connection...');
server.log.info('✅ Database connected successfully');

// Query operations
server.log.debug('Executing user query', { userId, query: 'SELECT * FROM users' });
server.log.warn('Slow query detected', { duration: '2.5s', query: 'complex_query' });

Authentication & Security

// Authentication events
server.log.info('User login attempt', { email: user.email, ipAddress });
server.log.warn('Failed login attempt', { email, ipAddress, reason: 'invalid_password' });
server.log.error('Security violation detected', { ipAddress, action: 'brute_force' });

API Requests

// Request processing (handled automatically by Fastify)
// Custom business logic logging
server.log.debug('Processing payment request', { amount, currency, userId });
server.log.info('Payment processed successfully', { transactionId, amount });
server.log.error('Payment failed', { error, userId, amount });

Plugin System

// Plugin lifecycle
server.log.debug('Loading plugin', { pluginId, version });
server.log.info('Plugin loaded successfully', { pluginId });
server.log.warn('Plugin deprecated', { pluginId, deprecationDate });
server.log.error('Plugin failed to load', { pluginId, error });

Fixing Console.log Issues

Important: Replace all console.log statements with proper Pino logger calls to ensure consistent formatting and log level filtering.

Problem: Inconsistent Log Output

// ❌ Problem - Mixed logging approaches
console.log('✅ [GlobalSettingsInitService] Operation completed');  // No timestamp
server.log.info('✅ Database initialization completed');             // With timestamp

Solution: Use Proper Logger

// ✅ Solution - Consistent logging
class GlobalSettingsInitService {
  private static logger = server.log.child({ service: 'GlobalSettingsInitService' });
  
  static async loadSettings() {
    this.logger.debug('Loading settings definitions...');
    this.logger.info('Settings loaded successfully');
  }
}

Passing Logger to Classes

// ✅ Good - Pass logger instance to classes
class PluginManager {
  constructor(private logger: FastifyBaseLogger) {}
  
  async loadPlugin(pluginId: string) {
    this.logger.debug('Loading plugin', { pluginId });
    this.logger.info('Plugin loaded successfully', { pluginId });
  }
}

// Usage
const pluginManager = new PluginManager(server.log.child({ component: 'PluginManager' }));

Environment-Specific Configuration

Development Environment

# .env file for development
NODE_ENV=development
LOG_LEVEL=debug

Features:

  • Pretty-printed, colorized output
  • Shows debug and trace information
  • Includes timestamps and emojis
  • Easier to read during development

Production Environment

# Production environment variables
NODE_ENV=production
LOG_LEVEL=info

Features:

  • Structured JSON output
  • Optimized for log aggregation
  • Excludes debug information
  • Better performance

Testing Environment

# Testing environment
NODE_ENV=test
LOG_LEVEL=error

Features:

  • Minimal log output during tests
  • Only shows errors and fatal messages
  • Reduces test noise

Troubleshooting

Debug Mode Not Working

If debug logs aren't showing:

  1. Check LOG_LEVEL: Ensure it's set to debug or trace
  2. Check NODE_ENV: Development mode enables debug by default
  3. Restart Server: Environment changes require restart
# Force debug mode
LOG_LEVEL=debug npm run dev

Performance Issues

If logging is impacting performance:

  1. Increase Log Level: Use info or warn in production
  2. Remove Debug Logs: Clean up excessive debug statements
  3. Use Async Logging: Pino handles this automatically

Log Aggregation

For production log management:

// Add correlation IDs for request tracking
server.addHook('onRequest', async (request) => {
  request.log = request.log.child({ 
    requestId: request.id,
    userAgent: request.headers['user-agent']
  });
});

Migration Guide

From Manual Prefixes

// Before
server.log.info('🔄 [DEBUG] Starting operation...');
server.log.info('✅ [INFO] Operation completed');
server.log.info('❌ [ERROR] Operation failed');

// After
server.log.debug('🔄 Starting operation...');
server.log.info('✅ Operation completed');
server.log.error('❌ Operation failed');

From Console.log

// Before
console.log('User logged in:', userId);
console.error('Database error:', error);

// After
server.log.info('User logged in', { userId });
server.log.error('Database error', { error, userId });

Summary

  • Use proper log levels instead of manual prefixes
  • Replace console.log with server.log for consistency
  • Add structured context to make logs searchable
  • Configure LOG_LEVEL via environment variables
  • Use child loggers for persistent context
  • Follow the patterns shown in this guide

With proper log level configuration, you'll have a production-ready logging system that scales from development to enterprise deployments.