Enhance logging capabilities across data collection components
- Added optional logger parameter to various classes including `BaseDataCollector`, `CollectorManager`, `RealTimeCandleProcessor`, and `BatchCandleProcessor` to support conditional logging. - Implemented error-only logging mode, allowing components to log only error and critical messages when specified. - Updated logging calls to utilize new helper methods for improved readability and maintainability. - Enhanced documentation to include details on the new logging system and its usage across components. - Ensured that child components inherit the logger from their parent components for consistent logging behavior.
This commit is contained in:
@@ -23,7 +23,6 @@ from .websocket import (
|
||||
from .data_processor import OKXDataProcessor
|
||||
from database.connection import get_db_manager, get_raw_data_manager
|
||||
from database.models import MarketData, RawTrade
|
||||
from utils.logger import get_logger
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -52,7 +51,9 @@ class OKXCollector(BaseDataCollector):
|
||||
component_name: Optional[str] = None,
|
||||
auto_restart: bool = True,
|
||||
health_check_interval: float = 30.0,
|
||||
store_raw_data: bool = True):
|
||||
store_raw_data: bool = True,
|
||||
logger = None,
|
||||
log_errors_only: bool = False):
|
||||
"""
|
||||
Initialize OKX collector for a single trading pair.
|
||||
|
||||
@@ -63,6 +64,8 @@ class OKXCollector(BaseDataCollector):
|
||||
auto_restart: Enable automatic restart on failures
|
||||
health_check_interval: Seconds between health checks
|
||||
store_raw_data: Whether to store raw data for debugging
|
||||
logger: Logger instance for conditional logging (None for no logging)
|
||||
log_errors_only: If True and logger provided, only log error-level messages
|
||||
"""
|
||||
# Default data types if not specified
|
||||
if data_types is None:
|
||||
@@ -79,7 +82,9 @@ class OKXCollector(BaseDataCollector):
|
||||
data_types=data_types,
|
||||
component_name=component_name,
|
||||
auto_restart=auto_restart,
|
||||
health_check_interval=health_check_interval
|
||||
health_check_interval=health_check_interval,
|
||||
logger=logger,
|
||||
log_errors_only=log_errors_only
|
||||
)
|
||||
|
||||
# OKX-specific settings
|
||||
@@ -90,7 +95,7 @@ class OKXCollector(BaseDataCollector):
|
||||
self._ws_client: Optional[OKXWebSocketClient] = None
|
||||
|
||||
# Data processor using new common framework
|
||||
self._data_processor = OKXDataProcessor(symbol, component_name=f"{component_name}_processor")
|
||||
self._data_processor = OKXDataProcessor(symbol, component_name=f"{component_name}_processor", logger=logger)
|
||||
|
||||
# Add callbacks for processed data
|
||||
self._data_processor.add_trade_callback(self._on_trade_processed)
|
||||
@@ -113,8 +118,9 @@ class OKXCollector(BaseDataCollector):
|
||||
DataType.TICKER: OKXChannelType.TICKERS.value
|
||||
}
|
||||
|
||||
self.logger.info(f"Initialized OKX collector for {symbol} with data types: {[dt.value for dt in data_types]}")
|
||||
self.logger.info(f"Using common data processing framework")
|
||||
if logger:
|
||||
logger.info(f"{component_name}: Initialized OKX collector for {symbol} with data types: {[dt.value for dt in data_types]}")
|
||||
logger.info(f"{component_name}: Using common data processing framework")
|
||||
|
||||
async def connect(self) -> bool:
|
||||
"""
|
||||
@@ -124,7 +130,8 @@ class OKXCollector(BaseDataCollector):
|
||||
True if connection successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
self.logger.info(f"Connecting OKX collector for {self.symbol}")
|
||||
if self.logger:
|
||||
self.logger.info(f"{self.component_name}: Connecting OKX collector for {self.symbol}")
|
||||
|
||||
# Initialize database managers
|
||||
self._db_manager = get_db_manager()
|
||||
@@ -146,29 +153,35 @@ class OKXCollector(BaseDataCollector):
|
||||
|
||||
# Connect to WebSocket
|
||||
if not await self._ws_client.connect(use_public=True):
|
||||
self.logger.error("Failed to connect to OKX WebSocket")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Failed to connect to OKX WebSocket")
|
||||
return False
|
||||
|
||||
self.logger.info(f"Successfully connected OKX collector for {self.symbol}")
|
||||
if self.logger:
|
||||
self.logger.info(f"{self.component_name}: Successfully connected OKX collector for {self.symbol}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error connecting OKX collector for {self.symbol}: {e}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Error connecting OKX collector for {self.symbol}: {e}")
|
||||
return False
|
||||
|
||||
async def disconnect(self) -> None:
|
||||
"""Disconnect from OKX WebSocket API."""
|
||||
try:
|
||||
self.logger.info(f"Disconnecting OKX collector for {self.symbol}")
|
||||
if self.logger:
|
||||
self.logger.info(f"{self.component_name}: Disconnecting OKX collector for {self.symbol}")
|
||||
|
||||
if self._ws_client:
|
||||
await self._ws_client.disconnect()
|
||||
self._ws_client = None
|
||||
|
||||
self.logger.info(f"Disconnected OKX collector for {self.symbol}")
|
||||
if self.logger:
|
||||
self.logger.info(f"{self.component_name}: Disconnected OKX collector for {self.symbol}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error disconnecting OKX collector for {self.symbol}: {e}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Error disconnecting OKX collector for {self.symbol}: {e}")
|
||||
|
||||
async def subscribe_to_data(self, symbols: List[str], data_types: List[DataType]) -> bool:
|
||||
"""
|
||||
@@ -182,12 +195,14 @@ class OKXCollector(BaseDataCollector):
|
||||
True if subscription successful, False otherwise
|
||||
"""
|
||||
if not self._ws_client or not self._ws_client.is_connected:
|
||||
self.logger.error("WebSocket client not connected")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: WebSocket client not connected")
|
||||
return False
|
||||
|
||||
# Validate symbol
|
||||
if self.symbol not in symbols:
|
||||
self.logger.warning(f"Symbol {self.symbol} not in subscription list: {symbols}")
|
||||
if self.logger:
|
||||
self.logger.warning(f"{self.component_name}: Symbol {self.symbol} not in subscription list: {symbols}")
|
||||
return False
|
||||
|
||||
try:
|
||||
@@ -202,25 +217,31 @@ class OKXCollector(BaseDataCollector):
|
||||
enabled=True
|
||||
)
|
||||
subscriptions.append(subscription)
|
||||
self.logger.debug(f"Added subscription: {channel} for {self.symbol}")
|
||||
if self.logger:
|
||||
self.logger.debug(f"{self.component_name}: Added subscription: {channel} for {self.symbol}")
|
||||
else:
|
||||
self.logger.warning(f"Unsupported data type: {data_type}")
|
||||
if self.logger:
|
||||
self.logger.warning(f"{self.component_name}: Unsupported data type: {data_type}")
|
||||
|
||||
if not subscriptions:
|
||||
self.logger.warning("No valid subscriptions to create")
|
||||
if self.logger:
|
||||
self.logger.warning(f"{self.component_name}: No valid subscriptions to create")
|
||||
return False
|
||||
|
||||
# Subscribe to channels
|
||||
success = await self._ws_client.subscribe(subscriptions)
|
||||
if success:
|
||||
self.logger.info(f"Successfully subscribed to {len(subscriptions)} channels for {self.symbol}")
|
||||
if self.logger:
|
||||
self.logger.info(f"{self.component_name}: Successfully subscribed to {len(subscriptions)} channels for {self.symbol}")
|
||||
return True
|
||||
else:
|
||||
self.logger.error(f"Failed to subscribe to channels for {self.symbol}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Failed to subscribe to channels for {self.symbol}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error subscribing to data for {self.symbol}: {e}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Error subscribing to data for {self.symbol}: {e}")
|
||||
return False
|
||||
|
||||
async def unsubscribe_from_data(self, symbols: List[str], data_types: List[DataType]) -> bool:
|
||||
@@ -235,7 +256,8 @@ class OKXCollector(BaseDataCollector):
|
||||
True if unsubscription successful, False otherwise
|
||||
"""
|
||||
if not self._ws_client or not self._ws_client.is_connected:
|
||||
self.logger.warning("WebSocket client not connected")
|
||||
if self.logger:
|
||||
self.logger.warning(f"{self.component_name}: WebSocket client not connected")
|
||||
return True # Consider it successful if not connected
|
||||
|
||||
try:
|
||||
@@ -257,14 +279,17 @@ class OKXCollector(BaseDataCollector):
|
||||
# Unsubscribe from channels
|
||||
success = await self._ws_client.unsubscribe(subscriptions)
|
||||
if success:
|
||||
self.logger.info(f"Successfully unsubscribed from {len(subscriptions)} channels for {self.symbol}")
|
||||
if self.logger:
|
||||
self.logger.info(f"{self.component_name}: Successfully unsubscribed from {len(subscriptions)} channels for {self.symbol}")
|
||||
return True
|
||||
else:
|
||||
self.logger.error(f"Failed to unsubscribe from channels for {self.symbol}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Failed to unsubscribe from channels for {self.symbol}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error unsubscribing from data for {self.symbol}: {e}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Error unsubscribing from data for {self.symbol}: {e}")
|
||||
return False
|
||||
|
||||
async def _process_message(self, message: Any) -> Optional[MarketDataPoint]:
|
||||
@@ -278,7 +303,8 @@ class OKXCollector(BaseDataCollector):
|
||||
MarketDataPoint if processing successful, None otherwise
|
||||
"""
|
||||
if not isinstance(message, dict):
|
||||
self.logger.warning(f"Received non-dict message: {type(message)}")
|
||||
if self.logger:
|
||||
self.logger.warning(f"{self.component_name}: Received non-dict message: {type(message)}")
|
||||
return None
|
||||
|
||||
try:
|
||||
@@ -291,11 +317,13 @@ class OKXCollector(BaseDataCollector):
|
||||
|
||||
if not success:
|
||||
self._error_count += 1
|
||||
self.logger.error(f"Message processing failed: {errors}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Message processing failed: {errors}")
|
||||
return None
|
||||
|
||||
if errors:
|
||||
self.logger.warning(f"Message processing warnings: {errors}")
|
||||
if self.logger:
|
||||
self.logger.warning(f"{self.component_name}: Message processing warnings: {errors}")
|
||||
|
||||
# Store raw data if enabled (for debugging/compliance)
|
||||
if self.store_raw_data and 'data' in message and 'arg' in message:
|
||||
@@ -310,7 +338,8 @@ class OKXCollector(BaseDataCollector):
|
||||
|
||||
except Exception as e:
|
||||
self._error_count += 1
|
||||
self.logger.error(f"Error processing message: {e}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Error processing message: {e}")
|
||||
return None
|
||||
|
||||
async def _handle_messages(self) -> None:
|
||||
@@ -340,10 +369,12 @@ class OKXCollector(BaseDataCollector):
|
||||
raw_data=data_point.data
|
||||
)
|
||||
session.add(raw_trade)
|
||||
self.logger.debug(f"Stored raw data: {data_point.data_type.value} for {data_point.symbol}")
|
||||
if self.logger:
|
||||
self.logger.debug(f"{self.component_name}: Stored raw data: {data_point.data_type.value} for {data_point.symbol}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error storing raw market data: {e}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Error storing raw market data: {e}")
|
||||
|
||||
async def _store_completed_candle(self, candle: OHLCVCandle) -> None:
|
||||
"""
|
||||
@@ -371,10 +402,12 @@ class OKXCollector(BaseDataCollector):
|
||||
trades_count=candle.trade_count
|
||||
)
|
||||
session.add(market_data)
|
||||
self.logger.info(f"Stored completed candle: {candle.symbol} {candle.timeframe} at {candle.start_time}")
|
||||
if self.logger:
|
||||
self.logger.info(f"{self.component_name}: Stored completed candle: {candle.symbol} {candle.timeframe} at {candle.start_time}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error storing completed candle: {e}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Error storing completed candle: {e}")
|
||||
|
||||
async def _store_raw_data(self, channel: str, raw_message: Dict[str, Any]) -> None:
|
||||
"""
|
||||
@@ -399,7 +432,8 @@ class OKXCollector(BaseDataCollector):
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error storing raw WebSocket data: {e}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Error storing raw WebSocket data: {e}")
|
||||
|
||||
def _on_message(self, message: Dict[str, Any]) -> None:
|
||||
"""
|
||||
@@ -412,7 +446,8 @@ class OKXCollector(BaseDataCollector):
|
||||
# Process message asynchronously
|
||||
asyncio.create_task(self._process_message(message))
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error handling WebSocket message: {e}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Error handling WebSocket message: {e}")
|
||||
|
||||
def _on_trade_processed(self, trade: StandardizedTrade) -> None:
|
||||
"""
|
||||
@@ -422,7 +457,8 @@ class OKXCollector(BaseDataCollector):
|
||||
trade: Processed standardized trade
|
||||
"""
|
||||
self._processed_trades += 1
|
||||
self.logger.debug(f"Processed trade: {trade.symbol} {trade.side} {trade.size}@{trade.price}")
|
||||
if self.logger:
|
||||
self.logger.debug(f"{self.component_name}: Processed trade: {trade.symbol} {trade.side} {trade.size}@{trade.price}")
|
||||
|
||||
def _on_candle_processed(self, candle: OHLCVCandle) -> None:
|
||||
"""
|
||||
@@ -432,7 +468,8 @@ class OKXCollector(BaseDataCollector):
|
||||
candle: Completed OHLCV candle
|
||||
"""
|
||||
self._processed_candles += 1
|
||||
self.logger.info(f"Completed candle: {candle.symbol} {candle.timeframe} O:{candle.open} H:{candle.high} L:{candle.low} C:{candle.close} V:{candle.volume}")
|
||||
if self.logger:
|
||||
self.logger.info(f"{self.component_name}: Completed candle: {candle.symbol} {candle.timeframe} O:{candle.open} H:{candle.high} L:{candle.low} C:{candle.close} V:{candle.volume}")
|
||||
|
||||
# Store completed candle in market_data table
|
||||
if candle.is_complete:
|
||||
|
||||
@@ -24,7 +24,6 @@ from ...common import (
|
||||
UnifiedDataTransformer,
|
||||
create_standardized_trade
|
||||
)
|
||||
from utils.logger import get_logger
|
||||
|
||||
|
||||
class OKXMessageType(Enum):
|
||||
@@ -81,9 +80,9 @@ class OKXDataValidator(BaseDataValidator):
|
||||
symbol patterns, and data structures.
|
||||
"""
|
||||
|
||||
def __init__(self, component_name: str = "okx_data_validator"):
|
||||
def __init__(self, component_name: str = "okx_data_validator", logger = None):
|
||||
"""Initialize OKX data validator."""
|
||||
super().__init__("okx", component_name)
|
||||
super().__init__("okx", component_name, logger)
|
||||
|
||||
# OKX-specific patterns
|
||||
self._symbol_pattern = re.compile(r'^[A-Z0-9]+-[A-Z0-9]+$') # BTC-USDT, ETH-USDC
|
||||
@@ -95,7 +94,8 @@ class OKXDataValidator(BaseDataValidator):
|
||||
'candle1m', 'candle5m', 'candle15m', 'candle1H', 'candle4H', 'candle1D'
|
||||
}
|
||||
|
||||
self.logger.debug("Initialized OKX data validator")
|
||||
if self.logger:
|
||||
self.logger.debug("Initialized OKX data validator")
|
||||
|
||||
def validate_symbol_format(self, symbol: str) -> ValidationResult:
|
||||
"""Validate OKX symbol format (e.g., BTC-USDT)."""
|
||||
@@ -423,9 +423,9 @@ class OKXDataTransformer(BaseDataTransformer):
|
||||
This class handles transformation of OKX data formats to standardized formats.
|
||||
"""
|
||||
|
||||
def __init__(self, component_name: str = "okx_data_transformer"):
|
||||
def __init__(self, component_name: str = "okx_data_transformer", logger = None):
|
||||
"""Initialize OKX data transformer."""
|
||||
super().__init__("okx", component_name)
|
||||
super().__init__("okx", component_name, logger)
|
||||
|
||||
def transform_trade_data(self, raw_data: Dict[str, Any], symbol: str) -> Optional[StandardizedTrade]:
|
||||
"""Transform OKX trade data to standardized format."""
|
||||
@@ -442,7 +442,8 @@ class OKXDataTransformer(BaseDataTransformer):
|
||||
is_milliseconds=True
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error transforming OKX trade data: {e}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Error transforming OKX trade data: {e}")
|
||||
return None
|
||||
|
||||
def transform_orderbook_data(self, raw_data: Dict[str, Any], symbol: str) -> Optional[Dict[str, Any]]:
|
||||
@@ -458,7 +459,8 @@ class OKXDataTransformer(BaseDataTransformer):
|
||||
'raw_data': raw_data
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error transforming OKX orderbook data: {e}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Error transforming OKX orderbook data: {e}")
|
||||
return None
|
||||
|
||||
def transform_ticker_data(self, raw_data: Dict[str, Any], symbol: str) -> Optional[Dict[str, Any]]:
|
||||
@@ -497,7 +499,8 @@ class OKXDataTransformer(BaseDataTransformer):
|
||||
return ticker_data
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error transforming OKX ticker data: {e}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Error transforming OKX ticker data: {e}")
|
||||
return None
|
||||
|
||||
|
||||
@@ -512,7 +515,8 @@ class OKXDataProcessor:
|
||||
def __init__(self,
|
||||
symbol: str,
|
||||
config: Optional[CandleProcessingConfig] = None,
|
||||
component_name: str = "okx_data_processor"):
|
||||
component_name: str = "okx_data_processor",
|
||||
logger = None):
|
||||
"""
|
||||
Initialize OKX data processor.
|
||||
|
||||
@@ -523,17 +527,17 @@ class OKXDataProcessor:
|
||||
"""
|
||||
self.symbol = symbol
|
||||
self.component_name = component_name
|
||||
self.logger = get_logger(self.component_name)
|
||||
self.logger = logger
|
||||
|
||||
# Core components using common utilities
|
||||
self.validator = OKXDataValidator(f"{component_name}_validator")
|
||||
self.transformer = OKXDataTransformer(f"{component_name}_transformer")
|
||||
self.unified_transformer = UnifiedDataTransformer(self.transformer, f"{component_name}_unified")
|
||||
self.validator = OKXDataValidator(f"{component_name}_validator", self.logger)
|
||||
self.transformer = OKXDataTransformer(f"{component_name}_transformer", self.logger)
|
||||
self.unified_transformer = UnifiedDataTransformer(self.transformer, f"{component_name}_unified", self.logger)
|
||||
|
||||
# Real-time candle processing using common utilities
|
||||
self.config = config or CandleProcessingConfig()
|
||||
self.candle_processor = RealTimeCandleProcessor(
|
||||
symbol, "okx", self.config, f"{component_name}_candles"
|
||||
symbol, "okx", self.config, f"{component_name}_candles", self.logger
|
||||
)
|
||||
|
||||
# Callbacks
|
||||
@@ -543,7 +547,8 @@ class OKXDataProcessor:
|
||||
# Connect candle processor callbacks
|
||||
self.candle_processor.add_candle_callback(self._emit_candle_to_callbacks)
|
||||
|
||||
self.logger.info(f"Initialized OKX data processor for {symbol} with real-time candle processing")
|
||||
if self.logger:
|
||||
self.logger.info(f"{self.component_name}: Initialized OKX data processor for {symbol} with real-time candle processing")
|
||||
|
||||
def add_trade_callback(self, callback: callable) -> None:
|
||||
"""Add callback for processed trades."""
|
||||
@@ -571,12 +576,14 @@ class OKXDataProcessor:
|
||||
validation_result = self.validator.validate_websocket_message(message)
|
||||
|
||||
if not validation_result.is_valid:
|
||||
self.logger.error(f"Message validation failed: {validation_result.errors}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Message validation failed: {validation_result.errors}")
|
||||
return False, [], validation_result.errors
|
||||
|
||||
# Log warnings if any
|
||||
if validation_result.warnings:
|
||||
self.logger.warning(f"Message validation warnings: {validation_result.warnings}")
|
||||
if self.logger:
|
||||
self.logger.warning(f"{self.component_name}: Message validation warnings: {validation_result.warnings}")
|
||||
|
||||
# Process data if it's a data message
|
||||
if 'data' in message and 'arg' in message:
|
||||
@@ -586,8 +593,9 @@ class OKXDataProcessor:
|
||||
return True, [], []
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Exception during message validation and processing: {str(e)}"
|
||||
self.logger.error(error_msg)
|
||||
error_msg = f"{self.component_name}: Exception during message validation and processing: {str(e)}"
|
||||
if self.logger:
|
||||
self.logger.error(error_msg)
|
||||
return False, [], [error_msg]
|
||||
|
||||
def _process_data_message(self, message: Dict[str, Any], expected_symbol: Optional[str] = None) -> Tuple[bool, List[MarketDataPoint], List[str]]:
|
||||
@@ -626,7 +634,8 @@ class OKXDataProcessor:
|
||||
continue
|
||||
|
||||
if validation_result.warnings:
|
||||
self.logger.warning(f"Data validation warnings: {validation_result.warnings}")
|
||||
if self.logger:
|
||||
self.logger.warning(f"{self.component_name}: Data validation warnings: {validation_result.warnings}")
|
||||
|
||||
# Create MarketDataPoint using sanitized data
|
||||
sanitized_data = validation_result.sanitized_data or data_item
|
||||
@@ -650,13 +659,14 @@ class OKXDataProcessor:
|
||||
self._process_real_time_trade(sanitized_data)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error processing data item: {e}")
|
||||
errors.append(f"Error processing data item: {str(e)}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Error processing data item: {e}")
|
||||
errors.append(f"{self.component_name}: Error processing data item: {str(e)}")
|
||||
|
||||
return len(errors) == 0, market_data_points, errors
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Exception during data message processing: {str(e)}"
|
||||
error_msg = f"{self.component_name}: Exception during data message processing: {str(e)}"
|
||||
errors.append(error_msg)
|
||||
return False, [], errors
|
||||
|
||||
@@ -675,12 +685,14 @@ class OKXDataProcessor:
|
||||
try:
|
||||
callback(standardized_trade)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error in trade callback: {e}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Error in trade callback: {e}")
|
||||
|
||||
# Note: Candle callbacks are handled by _emit_candle_to_callbacks
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error processing real-time trade: {e}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Error processing real-time trade: {e}")
|
||||
|
||||
def _emit_candle_to_callbacks(self, candle: OHLCVCandle) -> None:
|
||||
"""Emit candle to all registered callbacks."""
|
||||
@@ -688,7 +700,8 @@ class OKXDataProcessor:
|
||||
try:
|
||||
callback(candle)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error in candle callback: {e}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Error in candle callback: {e}")
|
||||
|
||||
def _channel_to_data_type(self, channel: str) -> Optional[DataType]:
|
||||
"""Convert OKX channel name to DataType enum."""
|
||||
|
||||
@@ -17,8 +17,6 @@ from dataclasses import dataclass
|
||||
import websockets
|
||||
from websockets.exceptions import ConnectionClosed, InvalidHandshake, InvalidURI
|
||||
|
||||
from utils.logger import get_logger
|
||||
|
||||
|
||||
class OKXChannelType(Enum):
|
||||
"""OKX WebSocket channel types."""
|
||||
@@ -91,7 +89,8 @@ class OKXWebSocketClient:
|
||||
ping_interval: float = 25.0,
|
||||
pong_timeout: float = 10.0,
|
||||
max_reconnect_attempts: int = 5,
|
||||
reconnect_delay: float = 5.0):
|
||||
reconnect_delay: float = 5.0,
|
||||
logger = None):
|
||||
"""
|
||||
Initialize OKX WebSocket client.
|
||||
|
||||
@@ -109,7 +108,7 @@ class OKXWebSocketClient:
|
||||
self.reconnect_delay = reconnect_delay
|
||||
|
||||
# Initialize logger
|
||||
self.logger = get_logger(self.component_name, verbose=True)
|
||||
self.logger = logger
|
||||
|
||||
# Connection management
|
||||
self._websocket: Optional[Any] = None # Changed to Any to handle different websocket types
|
||||
@@ -138,7 +137,8 @@ class OKXWebSocketClient:
|
||||
'last_message_time': None
|
||||
}
|
||||
|
||||
self.logger.info(f"Initialized OKX WebSocket client: {component_name}")
|
||||
if self.logger:
|
||||
self.logger.info(f"{self.component_name}: Initialized OKX WebSocket client")
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
@@ -184,7 +184,8 @@ class OKXWebSocketClient:
|
||||
True if connection successful, False otherwise
|
||||
"""
|
||||
if self.is_connected:
|
||||
self.logger.warning("Already connected to OKX WebSocket")
|
||||
if self.logger:
|
||||
self.logger.warning("Already connected to OKX WebSocket")
|
||||
return True
|
||||
|
||||
url = self.PUBLIC_WS_URL if use_public else self.PRIVATE_WS_URL
|
||||
@@ -194,7 +195,8 @@ class OKXWebSocketClient:
|
||||
self._connection_state = ConnectionState.CONNECTING
|
||||
|
||||
try:
|
||||
self.logger.info(f"Connecting to OKX WebSocket (attempt {attempt + 1}/{self.max_reconnect_attempts}): {url}")
|
||||
if self.logger:
|
||||
self.logger.info(f"{self.component_name}: Connecting to OKX WebSocket (attempt {attempt + 1}/{self.max_reconnect_attempts}): {url}")
|
||||
|
||||
# Create SSL context for secure connection
|
||||
ssl_context = ssl.create_default_context()
|
||||
@@ -219,25 +221,30 @@ class OKXWebSocketClient:
|
||||
# Start background tasks
|
||||
await self._start_background_tasks()
|
||||
|
||||
self.logger.info("Successfully connected to OKX WebSocket")
|
||||
if self.logger:
|
||||
self.logger.info(f"{self.component_name}: Successfully connected to OKX WebSocket")
|
||||
return True
|
||||
|
||||
except (InvalidURI, InvalidHandshake) as e:
|
||||
self.logger.error(f"Invalid WebSocket configuration: {e}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Invalid WebSocket configuration: {e}")
|
||||
self._connection_state = ConnectionState.ERROR
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
attempt_num = attempt + 1
|
||||
self.logger.error(f"Connection attempt {attempt_num} failed: {e}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Connection attempt {attempt_num} failed: {e}")
|
||||
|
||||
if attempt_num < self.max_reconnect_attempts:
|
||||
# Exponential backoff with jitter
|
||||
delay = self.reconnect_delay * (2 ** attempt) + (0.1 * attempt)
|
||||
self.logger.info(f"Retrying connection in {delay:.1f} seconds...")
|
||||
if self.logger:
|
||||
self.logger.info(f"{self.component_name}: Retrying connection in {delay:.1f} seconds...")
|
||||
await asyncio.sleep(delay)
|
||||
else:
|
||||
self.logger.error(f"All {self.max_reconnect_attempts} connection attempts failed")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: All {self.max_reconnect_attempts} connection attempts failed")
|
||||
self._connection_state = ConnectionState.ERROR
|
||||
return False
|
||||
|
||||
@@ -248,7 +255,8 @@ class OKXWebSocketClient:
|
||||
if not self._websocket:
|
||||
return
|
||||
|
||||
self.logger.info("Disconnecting from OKX WebSocket")
|
||||
if self.logger:
|
||||
self.logger.info(f"{self.component_name}: Disconnecting from OKX WebSocket")
|
||||
self._connection_state = ConnectionState.DISCONNECTED
|
||||
|
||||
# Cancel background tasks
|
||||
@@ -258,12 +266,14 @@ class OKXWebSocketClient:
|
||||
try:
|
||||
await self._websocket.close()
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Error closing WebSocket: {e}")
|
||||
if self.logger:
|
||||
self.logger.warning(f"{self.component_name}: Error closing WebSocket: {e}")
|
||||
|
||||
self._websocket = None
|
||||
self._is_authenticated = False
|
||||
|
||||
self.logger.info("Disconnected from OKX WebSocket")
|
||||
if self.logger:
|
||||
self.logger.info(f"{self.component_name}: Disconnected from OKX WebSocket")
|
||||
|
||||
async def subscribe(self, subscriptions: List[OKXSubscription]) -> bool:
|
||||
"""
|
||||
@@ -276,7 +286,8 @@ class OKXWebSocketClient:
|
||||
True if subscription successful, False otherwise
|
||||
"""
|
||||
if not self.is_connected:
|
||||
self.logger.error("Cannot subscribe: WebSocket not connected")
|
||||
if self.logger:
|
||||
self.logger.error("Cannot subscribe: WebSocket not connected")
|
||||
return False
|
||||
|
||||
try:
|
||||
@@ -295,11 +306,13 @@ class OKXWebSocketClient:
|
||||
key = f"{sub.channel}:{sub.inst_id}"
|
||||
self._subscriptions[key] = sub
|
||||
|
||||
self.logger.info(f"Subscribed to {len(subscriptions)} channels")
|
||||
if self.logger:
|
||||
self.logger.info(f"{self.component_name}: Subscribed to {len(subscriptions)} channels")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to subscribe to channels: {e}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Failed to subscribe to channels: {e}")
|
||||
return False
|
||||
|
||||
async def unsubscribe(self, subscriptions: List[OKXSubscription]) -> bool:
|
||||
@@ -313,7 +326,8 @@ class OKXWebSocketClient:
|
||||
True if unsubscription successful, False otherwise
|
||||
"""
|
||||
if not self.is_connected:
|
||||
self.logger.error("Cannot unsubscribe: WebSocket not connected")
|
||||
if self.logger:
|
||||
self.logger.error("Cannot unsubscribe: WebSocket not connected")
|
||||
return False
|
||||
|
||||
try:
|
||||
@@ -332,11 +346,13 @@ class OKXWebSocketClient:
|
||||
key = f"{sub.channel}:{sub.inst_id}"
|
||||
self._subscriptions.pop(key, None)
|
||||
|
||||
self.logger.info(f"Unsubscribed from {len(subscriptions)} channels")
|
||||
if self.logger:
|
||||
self.logger.info(f"{self.component_name}: Unsubscribed from {len(subscriptions)} channels")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to unsubscribe from channels: {e}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Failed to unsubscribe from channels: {e}")
|
||||
return False
|
||||
|
||||
def add_message_callback(self, callback: Callable[[Dict[str, Any]], None]) -> None:
|
||||
@@ -347,7 +363,8 @@ class OKXWebSocketClient:
|
||||
callback: Function to call when message received
|
||||
"""
|
||||
self._message_callbacks.append(callback)
|
||||
self.logger.debug(f"Added message callback: {callback.__name__}")
|
||||
if self.logger:
|
||||
self.logger.debug(f"{self.component_name}: Added message callback: {callback.__name__}")
|
||||
|
||||
def remove_message_callback(self, callback: Callable[[Dict[str, Any]], None]) -> None:
|
||||
"""
|
||||
@@ -358,7 +375,8 @@ class OKXWebSocketClient:
|
||||
"""
|
||||
if callback in self._message_callbacks:
|
||||
self._message_callbacks.remove(callback)
|
||||
self.logger.debug(f"Removed message callback: {callback.__name__}")
|
||||
if self.logger:
|
||||
self.logger.debug(f"{self.component_name}: Removed message callback: {callback.__name__}")
|
||||
|
||||
async def _start_background_tasks(self) -> None:
|
||||
"""Start background tasks for ping and message handling."""
|
||||
@@ -368,7 +386,8 @@ class OKXWebSocketClient:
|
||||
# Start message handler task
|
||||
self._message_handler_task = asyncio.create_task(self._message_handler())
|
||||
|
||||
self.logger.debug("Started background tasks")
|
||||
if self.logger:
|
||||
self.logger.debug(f"{self.component_name}: Started background tasks")
|
||||
|
||||
async def _stop_background_tasks(self) -> None:
|
||||
"""Stop background tasks."""
|
||||
@@ -385,7 +404,8 @@ class OKXWebSocketClient:
|
||||
self._ping_task = None
|
||||
self._message_handler_task = None
|
||||
|
||||
self.logger.debug("Stopped background tasks")
|
||||
if self.logger:
|
||||
self.logger.debug(f"{self.component_name}: Stopped background tasks")
|
||||
|
||||
async def _ping_loop(self) -> None:
|
||||
"""Background task for sending ping messages."""
|
||||
@@ -401,7 +421,8 @@ class OKXWebSocketClient:
|
||||
# Check for pong timeout
|
||||
if (self._last_ping_time > self._last_pong_time and
|
||||
current_time - self._last_ping_time > self.pong_timeout):
|
||||
self.logger.warning("Pong timeout - connection may be stale")
|
||||
if self.logger:
|
||||
self.logger.warning(f"{self.component_name}: Pong timeout - connection may be stale")
|
||||
# Don't immediately disconnect, let connection error handling deal with it
|
||||
|
||||
await asyncio.sleep(1) # Check every second
|
||||
@@ -409,7 +430,8 @@ class OKXWebSocketClient:
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error in ping loop: {e}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Error in ping loop: {e}")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def _message_handler(self) -> None:
|
||||
@@ -432,32 +454,38 @@ class OKXWebSocketClient:
|
||||
await self._process_message(message)
|
||||
|
||||
except ConnectionClosed as e:
|
||||
self.logger.warning(f"WebSocket connection closed: {e}")
|
||||
if self.logger:
|
||||
self.logger.warning(f"{self.component_name}: WebSocket connection closed: {e}")
|
||||
self._connection_state = ConnectionState.DISCONNECTED
|
||||
|
||||
# Attempt automatic reconnection if enabled
|
||||
if self._reconnect_attempts < self.max_reconnect_attempts:
|
||||
self._reconnect_attempts += 1
|
||||
self.logger.info(f"Attempting automatic reconnection ({self._reconnect_attempts}/{self.max_reconnect_attempts})")
|
||||
if self.logger:
|
||||
self.logger.info(f"{self.component_name}: Attempting automatic reconnection ({self._reconnect_attempts}/{self.max_reconnect_attempts})")
|
||||
|
||||
# Stop current tasks
|
||||
await self._stop_background_tasks()
|
||||
|
||||
# Attempt reconnection
|
||||
if await self.reconnect():
|
||||
self.logger.info("Automatic reconnection successful")
|
||||
if self.logger:
|
||||
self.logger.info(f"{self.component_name}: Automatic reconnection successful")
|
||||
continue
|
||||
else:
|
||||
self.logger.error("Automatic reconnection failed")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Automatic reconnection failed")
|
||||
break
|
||||
else:
|
||||
self.logger.error("Max reconnection attempts exceeded")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Max reconnection attempts exceeded")
|
||||
break
|
||||
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error in message handler: {e}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Error in message handler: {e}")
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def _send_message(self, message: Dict[str, Any]) -> None:
|
||||
@@ -474,14 +502,17 @@ class OKXWebSocketClient:
|
||||
message_str = json.dumps(message)
|
||||
await self._websocket.send(message_str)
|
||||
self._stats['messages_sent'] += 1
|
||||
self.logger.debug(f"Sent message: {message}")
|
||||
if self.logger:
|
||||
self.logger.debug(f"{self.component_name}: Sent message: {message}")
|
||||
|
||||
except ConnectionClosed as e:
|
||||
self.logger.error(f"Connection closed while sending message: {e}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Connection closed while sending message: {e}")
|
||||
self._connection_state = ConnectionState.DISCONNECTED
|
||||
raise OKXConnectionError(f"Connection closed: {e}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to send message: {e}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Failed to send message: {e}")
|
||||
raise OKXConnectionError(f"Failed to send message: {e}")
|
||||
|
||||
async def _send_ping(self) -> None:
|
||||
@@ -493,14 +524,17 @@ class OKXWebSocketClient:
|
||||
# OKX expects a simple "ping" string, not JSON
|
||||
await self._websocket.send("ping")
|
||||
self._stats['pings_sent'] += 1
|
||||
self.logger.debug("Sent ping to OKX")
|
||||
if self.logger:
|
||||
self.logger.debug(f"{self.component_name}: Sent ping to OKX")
|
||||
|
||||
except ConnectionClosed as e:
|
||||
self.logger.error(f"Connection closed while sending ping: {e}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Connection closed while sending ping: {e}")
|
||||
self._connection_state = ConnectionState.DISCONNECTED
|
||||
raise OKXConnectionError(f"Connection closed: {e}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to send ping: {e}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Failed to send ping: {e}")
|
||||
raise OKXConnectionError(f"Failed to send ping: {e}")
|
||||
|
||||
async def _process_message(self, message: str) -> None:
|
||||
@@ -519,7 +553,8 @@ class OKXWebSocketClient:
|
||||
if message.strip() == "pong":
|
||||
self._last_pong_time = time.time()
|
||||
self._stats['pongs_received'] += 1
|
||||
self.logger.debug("Received pong from OKX")
|
||||
if self.logger:
|
||||
self.logger.debug(f"{self.component_name}: Received pong from OKX")
|
||||
return
|
||||
|
||||
# Parse JSON message for all other responses
|
||||
@@ -529,21 +564,25 @@ class OKXWebSocketClient:
|
||||
if data.get('event') == 'pong':
|
||||
self._last_pong_time = time.time()
|
||||
self._stats['pongs_received'] += 1
|
||||
self.logger.debug("Received pong from OKX (JSON format)")
|
||||
if self.logger:
|
||||
self.logger.debug(f"{self.component_name}: Received pong from OKX (JSON format)")
|
||||
return
|
||||
|
||||
# Handle subscription confirmations
|
||||
if data.get('event') == 'subscribe':
|
||||
self.logger.info(f"Subscription confirmed: {data}")
|
||||
if self.logger:
|
||||
self.logger.info(f"{self.component_name}: Subscription confirmed: {data}")
|
||||
return
|
||||
|
||||
if data.get('event') == 'unsubscribe':
|
||||
self.logger.info(f"Unsubscription confirmed: {data}")
|
||||
if self.logger:
|
||||
self.logger.info(f"{self.component_name}: Unsubscription confirmed: {data}")
|
||||
return
|
||||
|
||||
# Handle error messages
|
||||
if data.get('event') == 'error':
|
||||
self.logger.error(f"OKX error: {data}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: OKX error: {data}")
|
||||
return
|
||||
|
||||
# Process data messages
|
||||
@@ -553,19 +592,23 @@ class OKXWebSocketClient:
|
||||
try:
|
||||
callback(data)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error in message callback {callback.__name__}: {e}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Error in message callback {callback.__name__}: {e}")
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
# Check if it's a simple string response we haven't handled
|
||||
if message.strip() in ["ping", "pong"]:
|
||||
self.logger.debug(f"Received simple message: {message.strip()}")
|
||||
if self.logger:
|
||||
self.logger.debug(f"{self.component_name}: Received simple message: {message.strip()}")
|
||||
if message.strip() == "pong":
|
||||
self._last_pong_time = time.time()
|
||||
self._stats['pongs_received'] += 1
|
||||
else:
|
||||
self.logger.error(f"Failed to parse JSON message: {e}, message: {message}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Failed to parse JSON message: {e}, message: {message}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error processing message: {e}")
|
||||
if self.logger:
|
||||
self.logger.error(f"{self.component_name}: Error processing message: {e}")
|
||||
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
"""Get connection statistics."""
|
||||
@@ -588,7 +631,8 @@ class OKXWebSocketClient:
|
||||
Returns:
|
||||
True if reconnection successful, False otherwise
|
||||
"""
|
||||
self.logger.info("Attempting to reconnect to OKX WebSocket")
|
||||
if self.logger:
|
||||
self.logger.info(f"{self.component_name}: Attempting to reconnect to OKX WebSocket")
|
||||
self._connection_state = ConnectionState.RECONNECTING
|
||||
self._stats['reconnections'] += 1
|
||||
|
||||
@@ -605,7 +649,8 @@ class OKXWebSocketClient:
|
||||
# Re-subscribe to previous subscriptions
|
||||
if self._subscriptions:
|
||||
subscriptions = list(self._subscriptions.values())
|
||||
self.logger.info(f"Re-subscribing to {len(subscriptions)} channels")
|
||||
if self.logger:
|
||||
self.logger.info(f"{self.component_name}: Re-subscribing to {len(subscriptions)} channels")
|
||||
await self.subscribe(subscriptions)
|
||||
|
||||
return success
|
||||
|
||||
Reference in New Issue
Block a user