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 underlogs/. - Centralized control:
UnifiedLoggerclass 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
- Optional Logging: Components accept
logger=Noneand function normally without logging - Error-Only Mode: Components can log only error-level messages with
log_errors_only=True - Logger Inheritance: Parent components pass their logger to child components
- 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:
-
BaseDataCollector (
data/base_collector.py)- Parameters:
logger=None, log_errors_only=False - Conditional logging for all collector operations
- Parameters:
-
CollectorManager (
data/collector_manager.py)- Parameters:
logger=None, log_errors_only=False - Manages multiple collectors with consistent logging
- Parameters:
-
OKXCollector (
data/exchanges/okx/collector.py)- Parameters:
logger=None, log_errors_only=False - Exchange-specific data collection with conditional logging
- Parameters:
-
BaseDataValidator (
data/common/validation.py)- Parameters:
logger=None - Data validation with optional logging
- Parameters:
-
OKXDataTransformer (
data/exchanges/okx/data_processor.py)- Parameters:
logger=None - Data processing with conditional logging
- Parameters:
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 managementdata_collector- for market data collectionstrategies- for trading strategiesbacktesting- for backtesting enginedashboard- 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
- Add logger parameter to constructor:
def __init__(self, ..., logger=None, log_errors_only=False):
- Add conditional logging helpers:
def _log_debug(self, message: str) -> None:
if self.logger and not self.log_errors_only:
self.logger.debug(message)
- Update all logging calls:
# Before
self.logger.info("Message")
# After
self._log_info("Message")
- 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
- Phase 1: Add optional logger parameters to new components
- Phase 2: Update existing components to support conditional logging
- Phase 3: Implement hierarchical logging structure
- 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
- Permission errors: Ensure the application has write permissions to the project directory
- Disk space: Monitor disk usage and adjust log retention with
max_log_files - Threading issues: The logger is thread-safe, but check for application-level concurrency issues
- Too many console messages: Adjust
verboseparameter 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:
- Start with new modules: Use the unified logger in new code
- Replace existing logging: Gradually migrate existing logging to the unified system
- 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.