Enhance logging system and update dependencies
- Updated `.gitignore` to exclude log files from version control. - Added `pytest` as a dependency in `pyproject.toml` for testing purposes. - Included `pytest` in `uv.lock` to ensure consistent dependency management. - Introduced comprehensive documentation for the new unified logging system in `docs/logging.md`, detailing features, usage, and configuration options.
This commit is contained in:
parent
8a378c8d69
commit
b7263b023f
4
.gitignore
vendored
4
.gitignore
vendored
@ -3,3 +3,7 @@
|
|||||||
.env.local
|
.env.local
|
||||||
.env.*
|
.env.*
|
||||||
database/migrations/versions/*
|
database/migrations/versions/*
|
||||||
|
|
||||||
|
# Exclude log files
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|||||||
474
docs/logging.md
Normal file
474
docs/logging.md
Normal file
@ -0,0 +1,474 @@
|
|||||||
|
# Unified Logging System
|
||||||
|
|
||||||
|
The TCP Dashboard project uses a unified logging system that provides consistent, centralized logging across all components.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
## Log Format
|
||||||
|
|
||||||
|
All log messages follow this unified format:
|
||||||
|
```
|
||||||
|
[YYYY-MM-DD HH:MM:SS - LEVEL - message]
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
[2024-01-15 14:30:25 - INFO - Bot started successfully]
|
||||||
|
[2024-01-15 14:30:26 - ERROR - Connection failed: timeout]
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Organization
|
||||||
|
|
||||||
|
Logs are organized in a hierarchical structure:
|
||||||
|
```
|
||||||
|
logs/
|
||||||
|
├── app/
|
||||||
|
│ ├── 2024-01-15.txt
|
||||||
|
│ └── 2024-01-16.txt
|
||||||
|
├── bot_manager/
|
||||||
|
│ ├── 2024-01-15.txt
|
||||||
|
│ └── 2024-01-16.txt
|
||||||
|
├── data_collector/
|
||||||
|
│ └── 2024-01-15.txt
|
||||||
|
└── strategies/
|
||||||
|
└── 2024-01-15.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
### Import and Initialize
|
||||||
|
|
||||||
|
```python
|
||||||
|
from utils.logger import get_logger
|
||||||
|
|
||||||
|
# Basic usage - gets logger with default settings
|
||||||
|
logger = get_logger('bot_manager')
|
||||||
|
|
||||||
|
# With verbose console output
|
||||||
|
logger = get_logger('bot_manager', verbose=True)
|
||||||
|
|
||||||
|
# With custom cleanup settings
|
||||||
|
logger = get_logger('bot_manager', clean_old_logs=True, max_log_files=7)
|
||||||
|
|
||||||
|
# All parameters
|
||||||
|
logger = get_logger(
|
||||||
|
component_name='bot_manager',
|
||||||
|
log_level='DEBUG',
|
||||||
|
verbose=True,
|
||||||
|
clean_old_logs=True,
|
||||||
|
max_log_files=14
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log Messages
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Different log levels
|
||||||
|
logger.debug("Detailed debugging information")
|
||||||
|
logger.info("General information about program execution")
|
||||||
|
logger.warning("Something unexpected happened")
|
||||||
|
logger.error("An error occurred", exc_info=True) # Include stack trace
|
||||||
|
logger.critical("A critical error occurred")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complete Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
from utils.logger import get_logger
|
||||||
|
|
||||||
|
class BotManager:
|
||||||
|
def __init__(self):
|
||||||
|
# Initialize with verbose output and keep only 7 days of logs
|
||||||
|
self.logger = get_logger('bot_manager', verbose=True, max_log_files=7)
|
||||||
|
self.logger.info("BotManager initialized")
|
||||||
|
|
||||||
|
def start_bot(self, bot_id: str):
|
||||||
|
try:
|
||||||
|
self.logger.info(f"Starting bot {bot_id}")
|
||||||
|
# Bot startup logic here
|
||||||
|
self.logger.info(f"Bot {bot_id} started successfully")
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to start bot {bot_id}: {e}", exc_info=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def stop_bot(self, bot_id: str):
|
||||||
|
self.logger.info(f"Stopping bot {bot_id}")
|
||||||
|
# Bot shutdown logic here
|
||||||
|
self.logger.info(f"Bot {bot_id} stopped")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Logger Parameters
|
||||||
|
|
||||||
|
The `get_logger()` function accepts several parameters for customization:
|
||||||
|
|
||||||
|
```python
|
||||||
|
get_logger(
|
||||||
|
component_name: str, # Required: component name
|
||||||
|
log_level: str = "INFO", # Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||||
|
verbose: Optional[bool] = None, # Console logging: True, False, or None (use env)
|
||||||
|
clean_old_logs: bool = True, # Auto-cleanup old logs
|
||||||
|
max_log_files: int = 30 # Max number of log files to keep
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log Levels
|
||||||
|
|
||||||
|
Set the log level when getting a logger:
|
||||||
|
```python
|
||||||
|
# Available levels: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||||
|
logger = get_logger('component_name', 'DEBUG') # Show all messages
|
||||||
|
logger = get_logger('component_name', 'ERROR') # Show only errors and critical
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verbose Console Logging
|
||||||
|
|
||||||
|
Control console output with the `verbose` parameter:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Explicit verbose settings
|
||||||
|
logger = get_logger('bot_manager', verbose=True) # Always show console logs
|
||||||
|
logger = get_logger('bot_manager', verbose=False) # Never show console logs
|
||||||
|
|
||||||
|
# Use environment variable (default behavior)
|
||||||
|
logger = get_logger('bot_manager', verbose=None) # Uses VERBOSE_LOGGING from .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Environment variables for console logging:
|
||||||
|
```bash
|
||||||
|
# In .env file or environment
|
||||||
|
VERBOSE_LOGGING=true # Enable verbose console logging
|
||||||
|
LOG_TO_CONSOLE=true # Alternative environment variable (backward compatibility)
|
||||||
|
```
|
||||||
|
|
||||||
|
Console output respects log levels:
|
||||||
|
- **DEBUG level**: Shows all messages (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||||
|
- **INFO level**: Shows INFO and above (INFO, WARNING, ERROR, CRITICAL)
|
||||||
|
- **WARNING level**: Shows WARNING and above (WARNING, ERROR, CRITICAL)
|
||||||
|
- **ERROR level**: Shows ERROR and above (ERROR, CRITICAL)
|
||||||
|
- **CRITICAL level**: Shows only CRITICAL messages
|
||||||
|
|
||||||
|
### Automatic Log Cleanup
|
||||||
|
|
||||||
|
Control automatic cleanup of old log files:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Enable automatic cleanup (default)
|
||||||
|
logger = get_logger('bot_manager', clean_old_logs=True, max_log_files=7)
|
||||||
|
|
||||||
|
# Disable automatic cleanup
|
||||||
|
logger = get_logger('bot_manager', clean_old_logs=False)
|
||||||
|
|
||||||
|
# Custom retention (keep 14 most recent log files)
|
||||||
|
logger = get_logger('bot_manager', max_log_files=14)
|
||||||
|
```
|
||||||
|
|
||||||
|
**How automatic cleanup works:**
|
||||||
|
- Triggered every time a new log file is created (date change)
|
||||||
|
- Keeps only the most recent `max_log_files` files
|
||||||
|
- Deletes older files automatically
|
||||||
|
- Based on file modification time, not filename
|
||||||
|
|
||||||
|
## Advanced Features
|
||||||
|
|
||||||
|
### Manual Log Cleanup
|
||||||
|
|
||||||
|
Remove old log files manually based on age:
|
||||||
|
```python
|
||||||
|
from utils.logger import cleanup_old_logs
|
||||||
|
|
||||||
|
# Remove logs older than 30 days for a specific component
|
||||||
|
cleanup_old_logs('bot_manager', days_to_keep=30)
|
||||||
|
|
||||||
|
# Or clean up logs for multiple components
|
||||||
|
for component in ['bot_manager', 'data_collector', 'strategies']:
|
||||||
|
cleanup_old_logs(component, days_to_keep=7)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling with Context
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
risky_operation()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Operation failed: {e}", exc_info=True)
|
||||||
|
# exc_info=True includes the full stack trace
|
||||||
|
```
|
||||||
|
|
||||||
|
### Structured Logging
|
||||||
|
|
||||||
|
For complex data, use structured messages:
|
||||||
|
```python
|
||||||
|
# Good: Structured information
|
||||||
|
logger.info(f"Trade executed: symbol={symbol}, price={price}, quantity={quantity}")
|
||||||
|
|
||||||
|
# Even better: JSON-like structure for parsing
|
||||||
|
logger.info(f"Trade executed", extra={
|
||||||
|
'symbol': symbol,
|
||||||
|
'price': price,
|
||||||
|
'quantity': quantity,
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Examples
|
||||||
|
|
||||||
|
### Development Environment
|
||||||
|
```python
|
||||||
|
# Verbose logging with frequent cleanup
|
||||||
|
logger = get_logger(
|
||||||
|
'bot_manager',
|
||||||
|
log_level='DEBUG',
|
||||||
|
verbose=True,
|
||||||
|
max_log_files=3 # Keep only 3 days of logs
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Environment
|
||||||
|
```python
|
||||||
|
# Minimal console output with longer retention
|
||||||
|
logger = get_logger(
|
||||||
|
'bot_manager',
|
||||||
|
log_level='INFO',
|
||||||
|
verbose=False,
|
||||||
|
max_log_files=30 # Keep 30 days of logs
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Environment
|
||||||
|
```python
|
||||||
|
# Disable cleanup for testing
|
||||||
|
logger = get_logger(
|
||||||
|
'test_component',
|
||||||
|
log_level='DEBUG',
|
||||||
|
verbose=True,
|
||||||
|
clean_old_logs=False # Don't delete logs during tests
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Create a `.env` file to control default logging behavior:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable verbose console logging globally
|
||||||
|
VERBOSE_LOGGING=true
|
||||||
|
|
||||||
|
# Alternative (backward compatibility)
|
||||||
|
LOG_TO_CONSOLE=true
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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)}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
### Migration Example
|
||||||
|
|
||||||
|
```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)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
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')"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## New Features Summary
|
||||||
|
|
||||||
|
### Verbose Parameter
|
||||||
|
- Controls console logging output
|
||||||
|
- Respects log levels (DEBUG shows all, ERROR shows only errors)
|
||||||
|
- Uses environment variables as default (`VERBOSE_LOGGING` or `LOG_TO_CONSOLE`)
|
||||||
|
- Can be explicitly set to `True`/`False` to override environment
|
||||||
|
|
||||||
|
### Automatic Cleanup
|
||||||
|
- Enabled by default (`clean_old_logs=True`)
|
||||||
|
- Triggered when new log files are created (date changes)
|
||||||
|
- Keeps most recent `max_log_files` files (default: 30)
|
||||||
|
- Component-specific retention policies
|
||||||
|
- Non-blocking operation with error handling
|
||||||
@ -33,6 +33,7 @@ dependencies = [
|
|||||||
# Development tools
|
# Development tools
|
||||||
"watchdog>=3.0.0", # For file watching and hot reload
|
"watchdog>=3.0.0", # For file watching and hot reload
|
||||||
"click>=8.0.0", # For CLI commands
|
"click>=8.0.0", # For CLI commands
|
||||||
|
"pytest>=8.3.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
|||||||
@ -24,13 +24,16 @@
|
|||||||
- `scripts/dev.py` - Development setup and management script
|
- `scripts/dev.py` - Development setup and management script
|
||||||
- `scripts/init_database.py` - Database initialization and verification script
|
- `scripts/init_database.py` - Database initialization and verification script
|
||||||
- `scripts/test_models.py` - Test script for SQLAlchemy models integration verification
|
- `scripts/test_models.py` - Test script for SQLAlchemy models integration verification
|
||||||
|
- `utils/logger.py` - Enhanced unified logging system with verbose console output, automatic cleanup, and configurable retention [USE THIS FOR ALL LOGGING]
|
||||||
- `alembic.ini` - Alembic configuration for database migrations
|
- `alembic.ini` - Alembic configuration for database migrations
|
||||||
- `requirements.txt` - Python dependencies managed by UV
|
- `requirements.txt` - Python dependencies managed by UV
|
||||||
- `docker-compose.yml` - Docker services configuration with TimescaleDB support
|
- `docker-compose.yml` - Docker services configuration with TimescaleDB support
|
||||||
- `tests/test_strategies.py` - Unit tests for strategy implementations
|
- `tests/test_strategies.py` - Unit tests for strategy implementations
|
||||||
- `tests/test_bot_manager.py` - Unit tests for bot management functionality
|
- `tests/test_bot_manager.py` - Unit tests for bot management functionality
|
||||||
- `tests/test_data_collection.py` - Unit tests for data collection and aggregation
|
- `tests/test_data_collection.py` - Unit tests for data collection and aggregation
|
||||||
|
- `tests/test_logging_enhanced.py` - Comprehensive unit tests for enhanced logging features (16 tests)
|
||||||
- `docs/setup.md` - Comprehensive setup guide for new machines and environments
|
- `docs/setup.md` - Comprehensive setup guide for new machines and environments
|
||||||
|
- `docs/logging.md` - Complete documentation for the enhanced unified logging system
|
||||||
|
|
||||||
## Tasks
|
## Tasks
|
||||||
|
|
||||||
@ -43,6 +46,7 @@
|
|||||||
- [x] 1.6 Setup Redis for pub/sub messaging
|
- [x] 1.6 Setup Redis for pub/sub messaging
|
||||||
- [x] 1.7 Create database migration scripts and initial data seeding
|
- [x] 1.7 Create database migration scripts and initial data seeding
|
||||||
- [x] 1.8 Unit test database models and connection utilities
|
- [x] 1.8 Unit test database models and connection utilities
|
||||||
|
- [x] 1.9 Add unified logging system we can use for all components
|
||||||
|
|
||||||
- [ ] 2.0 Market Data Collection and Processing System
|
- [ ] 2.0 Market Data Collection and Processing System
|
||||||
- [ ] 2.1 Implement OKX WebSocket API connector for real-time data
|
- [ ] 2.1 Implement OKX WebSocket API connector for real-time data
|
||||||
|
|||||||
1
utils/__init__.py
Normal file
1
utils/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Utils package for shared utilities
|
||||||
341
utils/logger.py
Normal file
341
utils/logger.py
Normal file
@ -0,0 +1,341 @@
|
|||||||
|
"""
|
||||||
|
Unified logging system for the TCP Dashboard project.
|
||||||
|
|
||||||
|
Provides centralized logging with:
|
||||||
|
- Component-specific log directories
|
||||||
|
- Date-based file rotation
|
||||||
|
- Unified log format: [YYYY-MM-DD HH:MM:SS - LEVEL - message]
|
||||||
|
- Thread-safe operations
|
||||||
|
- Automatic directory creation
|
||||||
|
- Verbose console logging with proper level handling
|
||||||
|
- Automatic old log cleanup
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from utils.logger import get_logger
|
||||||
|
|
||||||
|
logger = get_logger('bot_manager')
|
||||||
|
logger.info("This is an info message")
|
||||||
|
logger.error("This is an error message")
|
||||||
|
|
||||||
|
# With verbose console output
|
||||||
|
logger = get_logger('bot_manager', verbose=True)
|
||||||
|
|
||||||
|
# With custom cleanup settings
|
||||||
|
logger = get_logger('bot_manager', clean_old_logs=True, max_log_files=7)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Optional
|
||||||
|
import threading
|
||||||
|
|
||||||
|
|
||||||
|
class DateRotatingFileHandler(logging.FileHandler):
|
||||||
|
"""
|
||||||
|
Custom file handler that rotates log files based on date changes.
|
||||||
|
Creates new log files when the date changes to ensure daily separation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, log_dir: Path, component_name: str, cleanup_callback=None, max_files=30):
|
||||||
|
self.log_dir = log_dir
|
||||||
|
self.component_name = component_name
|
||||||
|
self.current_date = None
|
||||||
|
self.cleanup_callback = cleanup_callback
|
||||||
|
self.max_files = max_files
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
|
# Initialize with today's file
|
||||||
|
self._update_filename()
|
||||||
|
super().__init__(self.current_filename, mode='a', encoding='utf-8')
|
||||||
|
|
||||||
|
def _update_filename(self):
|
||||||
|
"""Update the filename based on current date."""
|
||||||
|
today = datetime.now().strftime('%Y-%m-%d')
|
||||||
|
if self.current_date != today:
|
||||||
|
self.current_date = today
|
||||||
|
self.current_filename = self.log_dir / f"{today}.txt"
|
||||||
|
|
||||||
|
# Ensure the directory exists
|
||||||
|
self.log_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Cleanup old logs if callback is provided
|
||||||
|
if self.cleanup_callback:
|
||||||
|
self.cleanup_callback(self.component_name, self.max_files)
|
||||||
|
|
||||||
|
def emit(self, record):
|
||||||
|
"""Emit a log record, rotating file if date has changed."""
|
||||||
|
with self._lock:
|
||||||
|
# Check if we need to rotate to a new file
|
||||||
|
today = datetime.now().strftime('%Y-%m-%d')
|
||||||
|
if self.current_date != today:
|
||||||
|
# Close current file
|
||||||
|
if hasattr(self, 'stream') and self.stream:
|
||||||
|
self.stream.close()
|
||||||
|
|
||||||
|
# Update filename and reopen (this will trigger cleanup)
|
||||||
|
self._update_filename()
|
||||||
|
self.baseFilename = str(self.current_filename)
|
||||||
|
self.stream = self._open()
|
||||||
|
|
||||||
|
super().emit(record)
|
||||||
|
|
||||||
|
|
||||||
|
class UnifiedLogger:
|
||||||
|
"""
|
||||||
|
Unified logger class that manages component-specific loggers with consistent formatting.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_loggers: Dict[str, logging.Logger] = {}
|
||||||
|
_lock = threading.Lock()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_logger(cls, component_name: str, log_level: str = "INFO",
|
||||||
|
verbose: Optional[bool] = None, clean_old_logs: bool = True,
|
||||||
|
max_log_files: int = 30) -> logging.Logger:
|
||||||
|
"""
|
||||||
|
Get or create a logger for the specified component.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
component_name: Name of the component (e.g., 'bot_manager', 'data_collector')
|
||||||
|
log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||||
|
verbose: Enable console logging. If None, uses VERBOSE_LOGGING from .env
|
||||||
|
clean_old_logs: Automatically clean old log files when creating new ones
|
||||||
|
max_log_files: Maximum number of log files to keep (default: 30)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Configured logger instance for the component
|
||||||
|
"""
|
||||||
|
# Create a unique key for logger configuration
|
||||||
|
logger_key = f"{component_name}_{log_level}_{verbose}_{clean_old_logs}_{max_log_files}"
|
||||||
|
|
||||||
|
with cls._lock:
|
||||||
|
if logger_key in cls._loggers:
|
||||||
|
return cls._loggers[logger_key]
|
||||||
|
|
||||||
|
# Create new logger
|
||||||
|
logger = logging.getLogger(f"tcp_dashboard.{component_name}.{hash(logger_key) % 10000}")
|
||||||
|
logger.setLevel(getattr(logging, log_level.upper()))
|
||||||
|
|
||||||
|
# Prevent duplicate handlers if logger already exists
|
||||||
|
if logger.handlers:
|
||||||
|
logger.handlers.clear()
|
||||||
|
|
||||||
|
# Create log directory for component
|
||||||
|
log_dir = Path("logs") / component_name
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Setup cleanup callback if enabled
|
||||||
|
cleanup_callback = cls._cleanup_old_logs if clean_old_logs else None
|
||||||
|
|
||||||
|
# Add date-rotating file handler
|
||||||
|
file_handler = DateRotatingFileHandler(
|
||||||
|
log_dir, component_name, cleanup_callback, max_log_files
|
||||||
|
)
|
||||||
|
file_handler.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
# Create unified formatter
|
||||||
|
formatter = logging.Formatter(
|
||||||
|
'[%(asctime)s - %(levelname)s - %(message)s]',
|
||||||
|
datefmt='%Y-%m-%d %H:%M:%S'
|
||||||
|
)
|
||||||
|
file_handler.setFormatter(formatter)
|
||||||
|
logger.addHandler(file_handler)
|
||||||
|
|
||||||
|
# Add console handler based on verbose setting
|
||||||
|
should_log_to_console = cls._should_enable_console_logging(verbose)
|
||||||
|
if should_log_to_console:
|
||||||
|
console_handler = logging.StreamHandler()
|
||||||
|
|
||||||
|
# Set console log level based on log_level with proper type handling
|
||||||
|
console_level = cls._get_console_log_level(log_level)
|
||||||
|
console_handler.setLevel(console_level)
|
||||||
|
|
||||||
|
# Use colored formatter for console if available
|
||||||
|
console_formatter = cls._get_console_formatter()
|
||||||
|
console_handler.setFormatter(console_formatter)
|
||||||
|
logger.addHandler(console_handler)
|
||||||
|
|
||||||
|
# Prevent propagation to root logger
|
||||||
|
logger.propagate = False
|
||||||
|
|
||||||
|
cls._loggers[logger_key] = logger
|
||||||
|
|
||||||
|
# Log initialization
|
||||||
|
logger.info(f"Logger initialized for component: {component_name} "
|
||||||
|
f"(verbose={should_log_to_console}, cleanup={clean_old_logs}, "
|
||||||
|
f"max_files={max_log_files})")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Fallback to console logging if file logging fails
|
||||||
|
print(f"Warning: Failed to setup file logging for {component_name}: {e}")
|
||||||
|
console_handler = logging.StreamHandler()
|
||||||
|
console_handler.setLevel(logging.INFO)
|
||||||
|
formatter = logging.Formatter('[%(asctime)s - %(levelname)s - %(message)s]')
|
||||||
|
console_handler.setFormatter(formatter)
|
||||||
|
logger.addHandler(console_handler)
|
||||||
|
logger.propagate = False
|
||||||
|
cls._loggers[logger_key] = logger
|
||||||
|
|
||||||
|
return logger
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _should_enable_console_logging(cls, verbose: Optional[bool]) -> bool:
|
||||||
|
"""
|
||||||
|
Determine if console logging should be enabled.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
verbose: Explicit verbose setting, or None to use environment variable
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if console logging should be enabled
|
||||||
|
"""
|
||||||
|
if verbose is not None:
|
||||||
|
return verbose
|
||||||
|
|
||||||
|
# Check environment variables
|
||||||
|
env_verbose = os.getenv('VERBOSE_LOGGING', 'false').lower()
|
||||||
|
env_console = os.getenv('LOG_TO_CONSOLE', 'false').lower()
|
||||||
|
|
||||||
|
return env_verbose in ('true', '1', 'yes') or env_console in ('true', '1', 'yes')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_console_log_level(cls, log_level: str) -> int:
|
||||||
|
"""
|
||||||
|
Get appropriate console log level based on file log level.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
log_level: File logging level
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Console logging level (integer)
|
||||||
|
"""
|
||||||
|
# Map file log levels to console log levels
|
||||||
|
# Generally, console should be less verbose than file
|
||||||
|
level_mapping = {
|
||||||
|
'DEBUG': logging.DEBUG, # Show all debug info on console too
|
||||||
|
'INFO': logging.INFO, # Show info and above
|
||||||
|
'WARNING': logging.WARNING, # Show warnings and above
|
||||||
|
'ERROR': logging.ERROR, # Show errors and above
|
||||||
|
'CRITICAL': logging.CRITICAL # Show only critical
|
||||||
|
}
|
||||||
|
|
||||||
|
return level_mapping.get(log_level.upper(), logging.INFO)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_console_formatter(cls) -> logging.Formatter:
|
||||||
|
"""
|
||||||
|
Get formatter for console output with potential color support.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Configured formatter for console output
|
||||||
|
"""
|
||||||
|
# Basic formatter - could be enhanced with colors in the future
|
||||||
|
return logging.Formatter(
|
||||||
|
'[%(asctime)s - %(levelname)s - %(message)s]',
|
||||||
|
datefmt='%Y-%m-%d %H:%M:%S'
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _cleanup_old_logs(cls, component_name: str, max_files: int = 30):
|
||||||
|
"""
|
||||||
|
Clean up old log files for a component, keeping only the most recent files.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
component_name: Name of the component
|
||||||
|
max_files: Maximum number of log files to keep
|
||||||
|
"""
|
||||||
|
log_dir = Path("logs") / component_name
|
||||||
|
if not log_dir.exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get all log files sorted by modification time (newest first)
|
||||||
|
log_files = sorted(
|
||||||
|
log_dir.glob("*.txt"),
|
||||||
|
key=lambda f: f.stat().st_mtime,
|
||||||
|
reverse=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Keep only the most recent max_files
|
||||||
|
files_to_delete = log_files[max_files:]
|
||||||
|
|
||||||
|
for log_file in files_to_delete:
|
||||||
|
try:
|
||||||
|
log_file.unlink()
|
||||||
|
# Only log to console to avoid recursive logging
|
||||||
|
if cls._should_enable_console_logging(None):
|
||||||
|
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - INFO - "
|
||||||
|
f"Deleted old log file: {log_file}]")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - WARNING - "
|
||||||
|
f"Failed to delete old log file {log_file}: {e}]")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def cleanup_old_logs(cls, component_name: str, days_to_keep: int = 30):
|
||||||
|
"""
|
||||||
|
Clean up old log files for a component based on age.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
component_name: Name of the component
|
||||||
|
days_to_keep: Number of days of logs to retain
|
||||||
|
"""
|
||||||
|
log_dir = Path("logs") / component_name
|
||||||
|
if not log_dir.exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
cutoff_date = datetime.now().timestamp() - (days_to_keep * 24 * 60 * 60)
|
||||||
|
|
||||||
|
for log_file in log_dir.glob("*.txt"):
|
||||||
|
if log_file.stat().st_mtime < cutoff_date:
|
||||||
|
try:
|
||||||
|
log_file.unlink()
|
||||||
|
print(f"Deleted old log file: {log_file}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to delete old log file {log_file}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# Convenience function for easy import
|
||||||
|
def get_logger(component_name: str, log_level: str = "INFO",
|
||||||
|
verbose: Optional[bool] = None, clean_old_logs: bool = True,
|
||||||
|
max_log_files: int = 30) -> logging.Logger:
|
||||||
|
"""
|
||||||
|
Get a logger instance for the specified component.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
component_name: Name of the component (e.g., 'bot_manager', 'data_collector')
|
||||||
|
log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||||
|
verbose: Enable console logging. If None, uses VERBOSE_LOGGING from .env
|
||||||
|
clean_old_logs: Automatically clean old log files when creating new ones
|
||||||
|
max_log_files: Maximum number of log files to keep (default: 30)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Configured logger instance
|
||||||
|
|
||||||
|
Example:
|
||||||
|
from utils.logger import get_logger
|
||||||
|
|
||||||
|
# Basic usage
|
||||||
|
logger = get_logger('bot_manager')
|
||||||
|
|
||||||
|
# With verbose console output
|
||||||
|
logger = get_logger('bot_manager', verbose=True)
|
||||||
|
|
||||||
|
# With custom cleanup settings
|
||||||
|
logger = get_logger('bot_manager', clean_old_logs=True, max_log_files=7)
|
||||||
|
|
||||||
|
logger.info("Bot started successfully")
|
||||||
|
logger.error("Connection failed", exc_info=True)
|
||||||
|
"""
|
||||||
|
return UnifiedLogger.get_logger(component_name, log_level, verbose, clean_old_logs, max_log_files)
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_old_logs(component_name: str, days_to_keep: int = 30):
|
||||||
|
"""
|
||||||
|
Clean up old log files for a component based on age.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
component_name: Name of the component
|
||||||
|
days_to_keep: Number of days of logs to retain (default: 30)
|
||||||
|
"""
|
||||||
|
UnifiedLogger.cleanup_old_logs(component_name, days_to_keep)
|
||||||
2
uv.lock
generated
2
uv.lock
generated
@ -403,6 +403,7 @@ dependencies = [
|
|||||||
{ name = "psycopg2-binary" },
|
{ name = "psycopg2-binary" },
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "pydantic-settings" },
|
{ name = "pydantic-settings" },
|
||||||
|
{ name = "pytest" },
|
||||||
{ name = "python-dateutil" },
|
{ name = "python-dateutil" },
|
||||||
{ name = "python-dotenv" },
|
{ name = "python-dotenv" },
|
||||||
{ name = "pytz" },
|
{ name = "pytz" },
|
||||||
@ -444,6 +445,7 @@ requires-dist = [
|
|||||||
{ name = "psycopg2-binary", specifier = ">=2.9.0" },
|
{ name = "psycopg2-binary", specifier = ">=2.9.0" },
|
||||||
{ name = "pydantic", specifier = ">=2.4.0" },
|
{ name = "pydantic", specifier = ">=2.4.0" },
|
||||||
{ name = "pydantic-settings", specifier = ">=2.1.0" },
|
{ name = "pydantic-settings", specifier = ">=2.1.0" },
|
||||||
|
{ name = "pytest", specifier = ">=8.3.5" },
|
||||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.0" },
|
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.0" },
|
||||||
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" },
|
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" },
|
||||||
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" },
|
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" },
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user