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):
Level | Numeric Value | Description | When to Use |
---|---|---|---|
trace | 10 | Very detailed debugging | Tracing function calls, variable states |
debug | 20 | Debugging information | Development debugging, detailed flow |
info | 30 | General information | Important events, startup messages |
warn | 40 | Warning messages | Recoverable errors, deprecation notices |
error | 50 | Error conditions | Caught exceptions, failed operations |
fatal | 60 | Fatal errors | Unrecoverable 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 trackingduration
,responseTime
: Performance metricsstatusCode
,method
,endpoint
: HTTP-related contexttable
,queryType
: Database-related contextrecipient
,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:
- Check LOG_LEVEL: Ensure it's set to
debug
ortrace
- Check NODE_ENV: Development mode enables debug by default
- Restart Server: Environment changes require restart
# Force debug mode
LOG_LEVEL=debug npm run dev
Performance Issues
If logging is impacting performance:
- Increase Log Level: Use
info
orwarn
in production - Remove Debug Logs: Clean up excessive debug statements
- 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.
Global Settings Management
Comprehensive key-value store system with group organization, encryption support, and auto-initialization for DeployStack Backend configuration management.
Email Integration Documentation
Complete email system with Nodemailer, Pug templates, SMTP configuration, and type-safe helper methods for DeployStack Backend.