567 lines
19 KiB
Markdown
567 lines
19 KiB
Markdown
# 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
|
|
```python
|
|
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
|
|
```python
|
|
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
|
|
```python
|
|
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
|
|
```python
|
|
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
|
|
```python
|
|
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:
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
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
|
|
|
|
```python
|
|
# 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**:
|
|
```python
|
|
def __init__(self, ..., logger=None, log_errors_only=False):
|
|
```
|
|
|
|
2. **Add conditional logging helpers**:
|
|
```python
|
|
def _log_debug(self, message: str) -> None:
|
|
if self.logger and not self.log_errors_only:
|
|
self.logger.debug(message)
|
|
```
|
|
|
|
3. **Update all logging calls**:
|
|
```python
|
|
# Before
|
|
self.logger.info("Message")
|
|
|
|
# After
|
|
self._log_info("Message")
|
|
```
|
|
|
|
4. **Pass logger to child components**:
|
|
```python
|
|
child = ChildComponent(logger=self.logger)
|
|
```
|
|
|
|
### From Standard Logging
|
|
```python
|
|
# 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
|
|
```python
|
|
# 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
|
|
|
|
```python
|
|
# 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:
|
|
```bash
|
|
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:
|
|
```python
|
|
logger = get_logger('component_name', 'DEBUG', verbose=True)
|
|
```
|
|
|
|
### Console Output Issues
|
|
|
|
```python
|
|
# 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:
|
|
```python
|
|
# 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:
|
|
```bash
|
|
# 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. |