2025-06-06 20:33:29 +08:00

19 KiB

Unified Logging System

The TCP Dashboard project uses a unified logging system that provides consistent, centralized logging across all components with advanced conditional logging capabilities.

Key Features

  • Component-based logging: Each component (e.g., bot_manager, data_collector) gets its own dedicated logger and log directory under logs/.
  • Centralized control: UnifiedLogger class manages all logger instances, ensuring consistent configuration.
  • Date-based rotation: Log files are automatically rotated daily (e.g., 2023-11-15.txt).
  • Unified format: All log messages follow [YYYY-MM-DD HH:MM:SS - LEVEL - message].
  • Verbose console logging: Optional verbose console output for real-time monitoring, controlled by environment variables.
  • Automatic cleanup: Old log files are automatically removed to save disk space.

Features

  • Component-specific directories: Each component gets its own log directory
  • Date-based file rotation: New log files created daily automatically
  • Unified format: Consistent timestamp and message format across all logs
  • Thread-safe: Safe for use in multi-threaded applications
  • Verbose console logging: Configurable console output with proper log level handling
  • Automatic log cleanup: Built-in functionality to remove old log files automatically
  • Error handling: Graceful fallback to console logging if file logging fails
  • Conditional logging: Components can operate with or without loggers
  • Error-only logging: Option to log only error-level messages
  • Hierarchical logging: Parent components can pass loggers to children
  • Logger inheritance: Consistent logging across component hierarchies

Conditional Logging System

The TCP Dashboard implements a sophisticated conditional logging system that allows components to work with or without loggers, providing maximum flexibility for different deployment scenarios.

Key Concepts

  1. Optional Logging: Components accept logger=None and function normally without logging
  2. Error-Only Mode: Components can log only error-level messages with log_errors_only=True
  3. Logger Inheritance: Parent components pass their logger to child components
  4. Hierarchical Structure: Log files are organized by component hierarchy

Component Hierarchy

Top-level Application (individual logger)
├── ProductionManager (individual logger)
│   ├── DataSaver (receives logger from ProductionManager)
│   ├── DataValidator (receives logger from ProductionManager)
│   ├── DatabaseConnection (receives logger from ProductionManager)
│   └── CollectorManager (individual logger)
│       ├── OKX collector BTC-USD (individual logger)
│       │   ├── DataAggregator (receives logger from OKX collector)
│       │   ├── DataTransformer (receives logger from OKX collector)
│       │   └── DataProcessor (receives logger from OKX collector)
│       └── Another collector...

Usage Patterns

1. No Logging

from data.collector_manager import CollectorManager
from data.exchanges.okx.collector import OKXCollector

# Components work without any logging
manager = CollectorManager(logger=None)
collector = OKXCollector("BTC-USDT", logger=None)

# No log files created, no console output
# Components function normally without exceptions

2. Normal Logging

from utils.logger import get_logger
from data.collector_manager import CollectorManager

# Create logger for the manager
logger = get_logger('production_manager')

# Manager logs all activities
manager = CollectorManager(logger=logger)

# Child components inherit the logger
collector = manager.add_okx_collector("BTC-USDT")  # Uses manager's logger

3. Error-Only Logging

from utils.logger import get_logger
from data.exchanges.okx.collector import OKXCollector

# Create logger but only log errors
logger = get_logger('critical_only')

# Only error and critical messages are logged
collector = OKXCollector(
    "BTC-USDT", 
    logger=logger, 
    log_errors_only=True
)

# Debug, info, warning messages are suppressed
# Error and critical messages are always logged

4. Hierarchical Logging

from utils.logger import get_logger
from data.collector_manager import CollectorManager

# Top-level application logger
app_logger = get_logger('tcp_dashboard')

# Production manager with its own logger
prod_logger = get_logger('production_manager')
manager = CollectorManager(logger=prod_logger)

# Individual collectors with specific loggers
btc_logger = get_logger('btc_collector')
btc_collector = OKXCollector("BTC-USDT", logger=btc_logger)

eth_collector = OKXCollector("ETH-USDT", logger=None)  # No logging

# Results in organized log structure:
# logs/tcp_dashboard/
# logs/production_manager/
# logs/btc_collector/
# (no logs for ETH collector)

5. Mixed Configuration

from utils.logger import get_logger
from data.collector_manager import CollectorManager

# System logger for normal operations
system_logger = get_logger('system')

# Critical logger for error-only components
critical_logger = get_logger('critical_only')

manager = CollectorManager(logger=system_logger)

# Different logging strategies for different collectors
btc_collector = OKXCollector("BTC-USDT", logger=system_logger)  # Full logging
eth_collector = OKXCollector("ETH-USDT", logger=critical_logger, log_errors_only=True)  # Errors only
ada_collector = OKXCollector("ADA-USDT", logger=None)  # No logging

manager.add_collector(btc_collector)
manager.add_collector(eth_collector)
manager.add_collector(ada_collector)

Implementation Details

Component Constructor Pattern

All major components follow this pattern:

class ComponentExample:
    def __init__(self, logger=None, log_errors_only=False):
        self.logger = logger
        self.log_errors_only = log_errors_only
        
    def _log_debug(self, message: str) -> None:
        """Log debug message if logger is available and not in errors-only mode."""
        if self.logger and not self.log_errors_only:
            self.logger.debug(message)
    
    def _log_info(self, message: str) -> None:
        """Log info message if logger is available and not in errors-only mode."""
        if self.logger and not self.log_errors_only:
            self.logger.info(message)
    
    def _log_warning(self, message: str) -> None:
        """Log warning message if logger is available and not in errors-only mode."""
        if self.logger and not self.log_errors_only:
            self.logger.warning(message)
    
    def _log_error(self, message: str, exc_info: bool = False) -> None:
        """Log error message if logger is available (always logs errors)."""
        if self.logger:
            self.logger.error(message, exc_info=exc_info)
    
    def _log_critical(self, message: str, exc_info: bool = False) -> None:
        """Log critical message if logger is available (always logs critical)."""
        if self.logger:
            self.logger.critical(message, exc_info=exc_info)

Child Component Pattern

Child components receive logger from parent:

class OKXCollector(BaseDataCollector):
    def __init__(self, symbol: str, logger=None, log_errors_only=False):
        super().__init__(..., logger=logger, log_errors_only=log_errors_only)
        
        # Pass logger to child components
        self._data_processor = OKXDataProcessor(
            symbol, 
            logger=self.logger  # Pass parent's logger
        )
        self._data_validator = DataValidator(logger=self.logger)
        self._data_transformer = DataTransformer(logger=self.logger)

Supported Components

The following components support conditional logging:

  1. BaseDataCollector (data/base_collector.py)

    • Parameters: logger=None, log_errors_only=False
    • Conditional logging for all collector operations
  2. CollectorManager (data/collector_manager.py)

    • Parameters: logger=None, log_errors_only=False
    • Manages multiple collectors with consistent logging
  3. OKXCollector (data/exchanges/okx/collector.py)

    • Parameters: logger=None, log_errors_only=False
    • Exchange-specific data collection with conditional logging
  4. BaseDataValidator (data/common/validation.py)

    • Parameters: logger=None
    • Data validation with optional logging
  5. OKXDataTransformer (data/exchanges/okx/data_processor.py)

    • Parameters: logger=None
    • Data processing with conditional logging

Usage

Getting a Logger

from utils.logger import get_logger

# Get logger for bot manager
logger = get_logger('bot_manager', verbose=True)

logger.info("Bot started successfully")
logger.debug("Connecting to database...")
logger.warning("API response time is high")
logger.error("Failed to execute trade", extra={'trade_id': 12345})

Configuration

The get_logger function accepts the following parameters:

Parameter Type Default Description
component_name str - Name of the component (e.g., bot_manager, data_collector)
log_level str INFO Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
verbose Optional[bool] None Enable console logging. If None, uses VERBOSE_LOGGING from .env
clean_old_logs bool True Automatically clean old log files when creating new ones
max_log_files int 30 Maximum number of log files to keep per component

Log Cleanup

Log cleanup is now based on the number of files, not age.

  • Enabled by default: clean_old_logs=True
  • Default retention: Keeps the most recent 30 log files (max_log_files=30)

Centralized Control

For consistent logging behavior across the application, it is recommended to use environment variables in an .env file instead of passing parameters to get_logger.

  • LOG_LEVEL: "INFO", "DEBUG", etc.
  • VERBOSE_LOGGING: "true" or "false"
  • CLEAN_OLD_LOGS: "true" or "false"
  • MAX_LOG_FILES: e.g., "15"

File Structure

logs/
├── bot_manager/
│   ├── 2023-11-14.txt
│   └── 2023-11-15.txt
├── data_collector/
│   ├── 2023-11-14.txt
│   └── 2023-11-15.txt
└── default_logger/
    └── 2023-11-15.txt

Best Practices

1. Component Naming

Use descriptive, consistent component names:

  • bot_manager - for bot lifecycle management
  • data_collector - for market data collection
  • strategies - for trading strategies
  • backtesting - for backtesting engine
  • dashboard - for web dashboard

2. Log Level Guidelines

  • DEBUG: Detailed diagnostic information, typically only of interest when diagnosing problems
  • INFO: General information about program execution
  • WARNING: Something unexpected happened, but the program is still working
  • ERROR: A serious problem occurred, the program couldn't perform a function
  • CRITICAL: A serious error occurred, the program may not be able to continue

3. Verbose Logging Guidelines

# Development: Use verbose logging with DEBUG level
dev_logger = get_logger('component', 'DEBUG', verbose=True, max_log_files=3)

# Production: Use INFO level with no console output
prod_logger = get_logger('component', 'INFO', verbose=False, max_log_files=30)

# Testing: Disable cleanup to preserve test logs
test_logger = get_logger('test_component', 'DEBUG', verbose=True, clean_old_logs=False)

4. Log Retention Guidelines

# High-frequency components (data collectors): shorter retention
data_logger = get_logger('data_collector', max_log_files=7)

# Important components (bot managers): longer retention
bot_logger = get_logger('bot_manager', max_log_files=30)

# Development: very short retention
dev_logger = get_logger('dev_component', max_log_files=3)

5. Message Content

# Good: Descriptive and actionable
logger.error("Failed to connect to OKX API: timeout after 30s")

# Bad: Vague and unhelpful
logger.error("Error occurred")

# Good: Include relevant context
logger.info(f"Bot {bot_id} executed trade: {symbol} {side} {quantity}@{price}")

# Good: Include duration for performance monitoring
start_time = time.time()
# ... do work ...
duration = time.time() - start_time
logger.info(f"Data aggregation completed in {duration:.2f}s")

6. Exception Handling

try:
    execute_trade(symbol, quantity, price)
    logger.info(f"Trade executed successfully: {symbol}")
except APIError as e:
    logger.error(f"API error during trade execution: {e}", exc_info=True)
    raise
except ValidationError as e:
    logger.warning(f"Trade validation failed: {e}")
    return False
except Exception as e:
    logger.critical(f"Unexpected error during trade execution: {e}", exc_info=True)
    raise

7. Performance Considerations

# Good: Efficient string formatting
logger.debug(f"Processing {len(data)} records")

# Avoid: Expensive operations in log messages unless necessary
# logger.debug(f"Data: {expensive_serialization(data)}")  # Only if needed

# Better: Check log level first for expensive operations
if logger.isEnabledFor(logging.DEBUG):
    logger.debug(f"Data: {expensive_serialization(data)}")

Migration Guide

Updating Existing Components

  1. Add logger parameter to constructor:
def __init__(self, ..., logger=None, log_errors_only=False):
  1. Add conditional logging helpers:
def _log_debug(self, message: str) -> None:
    if self.logger and not self.log_errors_only:
        self.logger.debug(message)
  1. Update all logging calls:
# Before
self.logger.info("Message")

# After
self._log_info("Message")
  1. Pass logger to child components:
child = ChildComponent(logger=self.logger)

From Standard Logging

# Old logging (if any existed)
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# New unified logging
from utils.logger import get_logger
logger = get_logger('component_name', verbose=True)

Gradual Adoption

  1. Phase 1: Add optional logger parameters to new components
  2. Phase 2: Update existing components to support conditional logging
  3. Phase 3: Implement hierarchical logging structure
  4. Phase 4: Add error-only logging mode

Testing

Testing Conditional Logging

Test Script Example

# test_conditional_logging.py
from utils.logger import get_logger
from data.collector_manager import CollectorManager
from data.exchanges.okx.collector import OKXCollector

def test_no_logging():
    """Test components work without loggers."""
    manager = CollectorManager(logger=None)
    collector = OKXCollector("BTC-USDT", logger=None)
    print("✓ No logging test passed")

def test_with_logging():
    """Test components work with loggers."""
    logger = get_logger('test_system')
    manager = CollectorManager(logger=logger)
    collector = OKXCollector("BTC-USDT", logger=logger)
    print("✓ With logging test passed")

def test_error_only():
    """Test error-only logging mode."""
    logger = get_logger('test_errors')
    collector = OKXCollector("BTC-USDT", logger=logger, log_errors_only=True)
    print("✓ Error-only logging test passed")

if __name__ == "__main__":
    test_no_logging()
    test_with_logging()
    test_error_only()
    print("✅ All conditional logging tests passed!")

Testing Changes

# Test without logger
component = MyComponent(logger=None)
# Should work without errors, no logging

# Test with logger
logger = get_logger('test_component')
component = MyComponent(logger=logger)
# Should log normally

# Test error-only mode
component = MyComponent(logger=logger, log_errors_only=True)
# Should only log errors

Basic System Test

Run a simple test to verify the logging system:

python -c "from utils.logger import get_logger; logger = get_logger('test', verbose=True); logger.info('Test message'); print('Check logs/test/ directory')"

Troubleshooting

Common Issues

  1. Permission errors: Ensure the application has write permissions to the project directory
  2. Disk space: Monitor disk usage and adjust log retention with max_log_files
  3. Threading issues: The logger is thread-safe, but check for application-level concurrency issues
  4. Too many console messages: Adjust verbose parameter or log levels

Debug Mode

Enable debug logging to troubleshoot issues:

logger = get_logger('component_name', 'DEBUG', verbose=True)

Console Output Issues

# Force console output regardless of environment
logger = get_logger('component_name', verbose=True)

# Check environment variables
import os
print(f"VERBOSE_LOGGING: {os.getenv('VERBOSE_LOGGING')}")
print(f"LOG_TO_CONSOLE: {os.getenv('LOG_TO_CONSOLE')}")

Fallback Logging

If file logging fails, the system automatically falls back to console logging with a warning message.

Integration with Existing Code

The logging system is designed to be gradually adopted:

  1. Start with new modules: Use the unified logger in new code
  2. Replace existing logging: Gradually migrate existing logging to the unified system
  3. No breaking changes: Existing code continues to work

Maintenance

Automatic Cleanup Benefits

The automatic cleanup feature provides several benefits:

  • Disk space management: Prevents log directories from growing indefinitely
  • Performance: Fewer files to scan in log directories
  • Maintenance-free: No need for external cron jobs or scripts
  • Component-specific: Each component can have different retention policies

Manual Cleanup for Special Cases

For cases requiring age-based cleanup instead of count-based:

# cleanup_logs.py
from utils.logger import cleanup_old_logs

components = ['bot_manager', 'data_collector', 'strategies', 'dashboard']
for component in components:
    cleanup_old_logs(component, days_to_keep=30)

Monitoring Disk Usage

Monitor the logs/ directory size and adjust retention policies as needed:

# Check log directory size
du -sh logs/

# Find large log files
find logs/ -name "*.txt" -size +10M

# Count log files per component
find logs/ -name "*.txt" | cut -d'/' -f2 | sort | uniq -c

This conditional logging system provides maximum flexibility while maintaining clean, maintainable code that works in all scenarios.