add handlers and pipeline
This commit is contained in:
16
can_sniffer/src/handlers/__init__.py
Normal file
16
can_sniffer/src/handlers/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
Модуль обработчиков CAN сообщений.
|
||||||
|
|
||||||
|
Предоставляет плагинную архитектуру для обработки CAN фреймов.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .base import BaseHandler
|
||||||
|
from .storage_handler import StorageHandler
|
||||||
|
from .influxdb_handler import InfluxDBHandler
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'BaseHandler',
|
||||||
|
'StorageHandler',
|
||||||
|
'InfluxDBHandler',
|
||||||
|
]
|
||||||
|
|
||||||
113
can_sniffer/src/handlers/base.py
Normal file
113
can_sniffer/src/handlers/base.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
"""
|
||||||
|
Базовый класс для обработчиков CAN сообщений.
|
||||||
|
|
||||||
|
Предоставляет интерфейс для плагинной архитектуры обработки CAN фреймов.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import List, Dict, Any, Optional
|
||||||
|
from can_frame import CANFrame
|
||||||
|
from logger import get_logger
|
||||||
|
|
||||||
|
|
||||||
|
class BaseHandler(ABC):
|
||||||
|
"""
|
||||||
|
Базовый класс для всех обработчиков CAN сообщений.
|
||||||
|
|
||||||
|
Каждый обработчик реализует pipeline для обработки CAN фреймов.
|
||||||
|
Обработчики могут быть отключены, включены и конфигурированы независимо.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name: str, enabled: bool = True):
|
||||||
|
"""
|
||||||
|
Инициализация обработчика.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Имя обработчика (для логирования и идентификации)
|
||||||
|
enabled: Включен ли обработчик (по умолчанию True)
|
||||||
|
"""
|
||||||
|
self.name = name
|
||||||
|
self.enabled = enabled
|
||||||
|
self.logger = get_logger(f"{__name__}.{name}")
|
||||||
|
self._initialized = False
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def initialize(self) -> bool:
|
||||||
|
"""
|
||||||
|
Инициализация обработчика.
|
||||||
|
|
||||||
|
Вызывается один раз при создании обработчика.
|
||||||
|
Здесь должна быть логика подключения к внешним сервисам и т.д.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True если инициализация успешна, False иначе
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def handle(self, frame: CANFrame) -> bool:
|
||||||
|
"""
|
||||||
|
Обработка одного CAN фрейма.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: CANFrame для обработки
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True если обработка успешна, False иначе
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def handle_batch(self, frames: List[CANFrame]) -> int:
|
||||||
|
"""
|
||||||
|
Обработка батча CAN фреймов.
|
||||||
|
|
||||||
|
Может быть более эффективной, чем обработка по одному.
|
||||||
|
Если обработчик не поддерживает батчинг, может просто вызывать handle() для каждого.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frames: Список CANFrame для обработки
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Количество успешно обработанных фреймов
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def flush(self) -> None:
|
||||||
|
"""
|
||||||
|
Принудительная отправка накопленных данных.
|
||||||
|
|
||||||
|
Вызывается периодически или при завершении работы.
|
||||||
|
Обработчики могут накапливать данные для батчевой отправки.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def shutdown(self) -> None:
|
||||||
|
"""
|
||||||
|
Корректное завершение работы обработчика.
|
||||||
|
|
||||||
|
Вызывается при остановке приложения.
|
||||||
|
Здесь должна быть логика закрытия соединений, сохранения данных и т.д.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_stats(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Получение статистики обработчика.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Словарь со статистикой (обработано, ошибок, очередь и т.д.)
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def is_enabled(self) -> bool:
|
||||||
|
"""Проверка, включен ли обработчик."""
|
||||||
|
return self.enabled
|
||||||
|
|
||||||
|
def is_initialized(self) -> bool:
|
||||||
|
"""Проверка, инициализирован ли обработчик."""
|
||||||
|
return self._initialized
|
||||||
|
|
||||||
144
can_sniffer/src/handlers/example_handler.py
Normal file
144
can_sniffer/src/handlers/example_handler.py
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
"""
|
||||||
|
Пример обработчика для демонстрации плагинной архитектуры.
|
||||||
|
|
||||||
|
Этот файл показывает, как легко добавить новый обработчик (например, Kafka, MQTT, WebSocket).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import List, Dict, Any
|
||||||
|
from can_frame import CANFrame
|
||||||
|
from .base import BaseHandler
|
||||||
|
|
||||||
|
|
||||||
|
class ExampleHandler(BaseHandler):
|
||||||
|
"""
|
||||||
|
Пример обработчика для демонстрации.
|
||||||
|
|
||||||
|
Этот обработчик можно использовать как шаблон для создания новых:
|
||||||
|
- KafkaHandler
|
||||||
|
- MQTTHandler
|
||||||
|
- WebSocketHandler
|
||||||
|
- FileHandler
|
||||||
|
и т.д.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, enabled: bool = True):
|
||||||
|
"""Инициализация примера обработчика."""
|
||||||
|
super().__init__(name="example", enabled=enabled)
|
||||||
|
self.processed_count = 0
|
||||||
|
self.failed_count = 0
|
||||||
|
|
||||||
|
def initialize(self) -> bool:
|
||||||
|
"""Инициализация обработчика."""
|
||||||
|
if not self.enabled:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Здесь должна быть логика подключения к внешнему сервису
|
||||||
|
# Например: self.kafka_producer = KafkaProducer(...)
|
||||||
|
# Или: self.mqtt_client = mqtt.Client(...)
|
||||||
|
self._initialized = True
|
||||||
|
self.logger.info("Example handler initialized")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to initialize example handler: {e}", exc_info=True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def handle(self, frame: CANFrame) -> bool:
|
||||||
|
"""Обработка одного CAN фрейма."""
|
||||||
|
if not self.enabled or not self._initialized:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Здесь должна быть логика отправки одного фрейма
|
||||||
|
# Например: self.kafka_producer.send('can-topic', frame.to_dict())
|
||||||
|
# Или: self.mqtt_client.publish('can/data', frame.to_dict())
|
||||||
|
|
||||||
|
self.processed_count += 1
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(
|
||||||
|
f"Failed to handle frame: {e}",
|
||||||
|
exc_info=True,
|
||||||
|
extra={"can_id": frame.can_id_hex}
|
||||||
|
)
|
||||||
|
self.failed_count += 1
|
||||||
|
return False
|
||||||
|
|
||||||
|
def handle_batch(self, frames: List[CANFrame]) -> int:
|
||||||
|
"""Обработка батча CAN фреймов."""
|
||||||
|
if not self.enabled or not self._initialized or not frames:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Здесь должна быть логика пакетной отправки
|
||||||
|
# Например: self.kafka_producer.send_batch([...])
|
||||||
|
# Или: self.mqtt_client.publish_batch([...])
|
||||||
|
|
||||||
|
processed = 0
|
||||||
|
for frame in frames:
|
||||||
|
if self.handle(frame):
|
||||||
|
processed += 1
|
||||||
|
|
||||||
|
return processed
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(
|
||||||
|
f"Failed to handle batch: {e}",
|
||||||
|
exc_info=True,
|
||||||
|
extra={"batch_size": len(frames)}
|
||||||
|
)
|
||||||
|
self.failed_count += len(frames)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def flush(self) -> None:
|
||||||
|
"""Принудительная отправка накопленных данных."""
|
||||||
|
if not self.enabled or not self._initialized:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Здесь должна быть логика принудительной отправки
|
||||||
|
# Например: self.kafka_producer.flush()
|
||||||
|
# Или: self.mqtt_client.flush()
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to flush: {e}", exc_info=True)
|
||||||
|
|
||||||
|
def shutdown(self) -> None:
|
||||||
|
"""Корректное завершение работы обработчика."""
|
||||||
|
if self._initialized:
|
||||||
|
try:
|
||||||
|
# Здесь должна быть логика закрытия соединений
|
||||||
|
# Например: self.kafka_producer.close()
|
||||||
|
# Или: self.mqtt_client.disconnect()
|
||||||
|
self.logger.info("Example handler closed")
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error closing example handler: {e}", exc_info=True)
|
||||||
|
self._initialized = False
|
||||||
|
|
||||||
|
def get_stats(self) -> Dict[str, Any]:
|
||||||
|
"""Получение статистики обработчика."""
|
||||||
|
return {
|
||||||
|
"handler": self.name,
|
||||||
|
"enabled": self.enabled,
|
||||||
|
"initialized": self._initialized,
|
||||||
|
"processed_count": self.processed_count,
|
||||||
|
"failed_count": self.failed_count
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Пример использования:
|
||||||
|
#
|
||||||
|
# from handlers import BaseHandler, StorageHandler, InfluxDBHandler
|
||||||
|
# from handlers.example_handler import ExampleHandler
|
||||||
|
# from socket_can.message_processor import MessageProcessor
|
||||||
|
#
|
||||||
|
# # Создаем кастомный pipeline
|
||||||
|
# handlers = [
|
||||||
|
# StorageHandler(enabled=True),
|
||||||
|
# InfluxDBHandler(enabled=True),
|
||||||
|
# ExampleHandler(enabled=True), # Новый обработчик!
|
||||||
|
# ]
|
||||||
|
#
|
||||||
|
# # Используем в MessageProcessor
|
||||||
|
# processor = MessageProcessor(handlers=handlers)
|
||||||
|
# processor.start()
|
||||||
|
|
||||||
132
can_sniffer/src/handlers/influxdb_handler.py
Normal file
132
can_sniffer/src/handlers/influxdb_handler.py
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
"""
|
||||||
|
Обработчик для отправки CAN сообщений в InfluxDB.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import List, Dict, Any, Optional
|
||||||
|
from can_frame import CANFrame
|
||||||
|
from .base import BaseHandler
|
||||||
|
from influxdb_handler import get_influxdb_client
|
||||||
|
from config import config
|
||||||
|
|
||||||
|
|
||||||
|
class InfluxDBHandler(BaseHandler):
|
||||||
|
"""Обработчик для отправки в InfluxDB."""
|
||||||
|
|
||||||
|
def __init__(self, enabled: Optional[bool] = None):
|
||||||
|
"""
|
||||||
|
Инициализация обработчика InfluxDB.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
enabled: Включен ли обработчик. Если None, берется из config.influxdb.enabled
|
||||||
|
"""
|
||||||
|
super().__init__(
|
||||||
|
name="influxdb",
|
||||||
|
enabled=enabled if enabled is not None else config.influxdb.enabled
|
||||||
|
)
|
||||||
|
self.influxdb_client = None
|
||||||
|
|
||||||
|
def initialize(self) -> bool:
|
||||||
|
"""Инициализация InfluxDB клиента."""
|
||||||
|
if not self.enabled:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.influxdb_client = get_influxdb_client()
|
||||||
|
self._initialized = True
|
||||||
|
self.logger.info("InfluxDB handler initialized")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to initialize InfluxDB: {e}", exc_info=True)
|
||||||
|
self.influxdb_client = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
def handle(self, frame: CANFrame) -> bool:
|
||||||
|
"""Обработка одного CAN фрейма."""
|
||||||
|
if not self.enabled or not self._initialized or not self.influxdb_client:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.influxdb_client.write_message(
|
||||||
|
interface=frame.bus,
|
||||||
|
can_id=frame.can_id,
|
||||||
|
dlc=frame.dlc,
|
||||||
|
data=frame.data,
|
||||||
|
timestamp=frame.timestamp
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(
|
||||||
|
f"Failed to send frame to InfluxDB: {e}",
|
||||||
|
exc_info=True,
|
||||||
|
extra={"can_id": frame.can_id_hex}
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def handle_batch(self, frames: List[CANFrame]) -> int:
|
||||||
|
"""Обработка батча CAN фреймов."""
|
||||||
|
if not self.enabled or not self._initialized or not self.influxdb_client or not frames:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Конвертируем CANFrame в формат для InfluxDB
|
||||||
|
influx_messages = []
|
||||||
|
for frame in frames:
|
||||||
|
influx_messages.append({
|
||||||
|
"interface": frame.bus,
|
||||||
|
"can_id": frame.can_id,
|
||||||
|
"dlc": frame.dlc,
|
||||||
|
"data": frame.data,
|
||||||
|
"timestamp": frame.timestamp # float timestamp в секундах
|
||||||
|
})
|
||||||
|
|
||||||
|
if influx_messages:
|
||||||
|
return self.influxdb_client.write_messages_batch(influx_messages)
|
||||||
|
return 0
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(
|
||||||
|
f"Failed to send frames batch to InfluxDB: {e}",
|
||||||
|
exc_info=True,
|
||||||
|
extra={"batch_size": len(frames)}
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def flush(self) -> None:
|
||||||
|
"""Принудительная отправка накопленных данных."""
|
||||||
|
# InfluxDB forwarder сам управляет flush через свой цикл
|
||||||
|
# Но можно вызвать явный flush если нужно
|
||||||
|
pass
|
||||||
|
|
||||||
|
def shutdown(self) -> None:
|
||||||
|
"""Корректное завершение работы обработчика."""
|
||||||
|
if self.influxdb_client:
|
||||||
|
try:
|
||||||
|
self.influxdb_client.close()
|
||||||
|
self.logger.info("InfluxDB handler closed")
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error closing InfluxDB: {e}", exc_info=True)
|
||||||
|
self._initialized = False
|
||||||
|
|
||||||
|
def get_stats(self) -> Dict[str, Any]:
|
||||||
|
"""Получение статистики обработчика."""
|
||||||
|
if self.influxdb_client:
|
||||||
|
try:
|
||||||
|
stats = self.influxdb_client.get_stats()
|
||||||
|
stats["handler"] = self.name
|
||||||
|
stats["enabled"] = self.enabled
|
||||||
|
stats["initialized"] = self._initialized
|
||||||
|
return stats
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {
|
||||||
|
"handler": self.name,
|
||||||
|
"enabled": self.enabled,
|
||||||
|
"initialized": self._initialized
|
||||||
|
}
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
"""Запуск InfluxDB forwarder (если используется)."""
|
||||||
|
if self.influxdb_client:
|
||||||
|
try:
|
||||||
|
self.influxdb_client.start()
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to start InfluxDB forwarder: {e}", exc_info=True)
|
||||||
|
|
||||||
119
can_sniffer/src/handlers/storage_handler.py
Normal file
119
can_sniffer/src/handlers/storage_handler.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
"""
|
||||||
|
Обработчик для сохранения CAN сообщений в SQLite.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import List, Dict, Any
|
||||||
|
from can_frame import CANFrame
|
||||||
|
from .base import BaseHandler
|
||||||
|
from storage import get_storage
|
||||||
|
|
||||||
|
|
||||||
|
class StorageHandler(BaseHandler):
|
||||||
|
"""Обработчик для сохранения в SQLite."""
|
||||||
|
|
||||||
|
def __init__(self, enabled: bool = True):
|
||||||
|
"""Инициализация обработчика storage."""
|
||||||
|
super().__init__(name="storage", enabled=enabled)
|
||||||
|
self.storage = None
|
||||||
|
|
||||||
|
def initialize(self) -> bool:
|
||||||
|
"""Инициализация storage."""
|
||||||
|
if not self.enabled:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.storage = get_storage()
|
||||||
|
self._initialized = True
|
||||||
|
self.logger.info("Storage handler initialized")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Failed to initialize storage: {e}", exc_info=True)
|
||||||
|
self.storage = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
def handle(self, frame: CANFrame) -> bool:
|
||||||
|
"""Обработка одного CAN фрейма."""
|
||||||
|
if not self.enabled or not self._initialized or not self.storage:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
message_id = self.storage.save_message(
|
||||||
|
interface=frame.bus,
|
||||||
|
can_id=frame.can_id,
|
||||||
|
dlc=frame.dlc,
|
||||||
|
data=frame.data,
|
||||||
|
timestamp=frame.timestamp
|
||||||
|
)
|
||||||
|
return message_id is not None
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(
|
||||||
|
f"Failed to save frame: {e}",
|
||||||
|
exc_info=True,
|
||||||
|
extra={"can_id": frame.can_id_hex}
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def handle_batch(self, frames: List[CANFrame]) -> int:
|
||||||
|
"""Обработка батча CAN фреймов."""
|
||||||
|
if not self.enabled or not self._initialized or not self.storage or not frames:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Конвертируем CANFrame в формат для storage
|
||||||
|
messages = []
|
||||||
|
for frame in frames:
|
||||||
|
messages.append((
|
||||||
|
frame.timestamp, # float timestamp в секундах
|
||||||
|
frame.bus,
|
||||||
|
frame.can_id,
|
||||||
|
frame.dlc,
|
||||||
|
frame.data
|
||||||
|
))
|
||||||
|
|
||||||
|
saved_count = self.storage.save_messages_batch(messages)
|
||||||
|
if saved_count != len(frames):
|
||||||
|
self.logger.warning(
|
||||||
|
f"Not all frames saved: {saved_count}/{len(frames)}",
|
||||||
|
extra={"batch_size": len(frames)}
|
||||||
|
)
|
||||||
|
return saved_count
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(
|
||||||
|
f"Failed to save frames batch: {e}",
|
||||||
|
exc_info=True,
|
||||||
|
extra={"batch_size": len(frames)}
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def flush(self) -> None:
|
||||||
|
"""Принудительная отправка накопленных данных."""
|
||||||
|
# SQLite не требует явного flush, данные сохраняются сразу
|
||||||
|
pass
|
||||||
|
|
||||||
|
def shutdown(self) -> None:
|
||||||
|
"""Корректное завершение работы обработчика."""
|
||||||
|
if self.storage:
|
||||||
|
try:
|
||||||
|
self.storage.close()
|
||||||
|
self.logger.info("Storage handler closed")
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"Error closing storage: {e}", exc_info=True)
|
||||||
|
self._initialized = False
|
||||||
|
|
||||||
|
def get_stats(self) -> Dict[str, Any]:
|
||||||
|
"""Получение статистики обработчика."""
|
||||||
|
if self.storage:
|
||||||
|
try:
|
||||||
|
stats = self.storage.get_stats()
|
||||||
|
stats["handler"] = self.name
|
||||||
|
stats["enabled"] = self.enabled
|
||||||
|
stats["initialized"] = self._initialized
|
||||||
|
return stats
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {
|
||||||
|
"handler": self.name,
|
||||||
|
"enabled": self.enabled,
|
||||||
|
"initialized": self._initialized
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Модуль для обработки CAN сообщений.
|
Модуль для обработки CAN сообщений.
|
||||||
|
|
||||||
Обрабатывает входящие CAN сообщения и сохраняет их в SQLite и InfluxDB.
|
Обрабатывает входящие CAN сообщения через pipeline обработчиков.
|
||||||
Использует очередь для асинхронной обработки, чтобы не блокировать чтение CAN сообщений.
|
Использует очередь для асинхронной обработки, чтобы не блокировать чтение CAN сообщений.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -9,167 +9,29 @@ import threading
|
|||||||
import time
|
import time
|
||||||
from queue import Queue, Empty
|
from queue import Queue, Empty
|
||||||
from typing import Optional, Dict, Any, List
|
from typing import Optional, Dict, Any, List
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
|
|
||||||
from logger import get_logger
|
from logger import get_logger
|
||||||
from config import config
|
from config import config
|
||||||
from storage import get_storage
|
|
||||||
from influxdb_handler import get_influxdb_client
|
|
||||||
from can_frame import CANFrame
|
from can_frame import CANFrame
|
||||||
|
from handlers import BaseHandler, StorageHandler, InfluxDBHandler
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MessageHandler(ABC):
|
|
||||||
"""Базовый класс для обработчиков сообщений."""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def process_batch(self, frames: List[CANFrame]) -> int:
|
|
||||||
"""
|
|
||||||
Обработка батча CAN фреймов.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
frames: Список CANFrame объектов
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Количество успешно обработанных сообщений
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_stats(self) -> Dict[str, Any]:
|
|
||||||
"""Получение статистики обработчика."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class StorageHandler(MessageHandler):
|
|
||||||
"""Обработчик для сохранения в SQLite."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Инициализация обработчика storage."""
|
|
||||||
self.logger = logger.getChild("storage_handler")
|
|
||||||
self.storage = None
|
|
||||||
self._init_storage()
|
|
||||||
|
|
||||||
def _init_storage(self) -> None:
|
|
||||||
"""Инициализация storage."""
|
|
||||||
try:
|
|
||||||
self.storage = get_storage()
|
|
||||||
self.logger.info("Storage handler initialized")
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f"Failed to initialize storage: {e}", exc_info=True)
|
|
||||||
self.storage = None
|
|
||||||
|
|
||||||
def process_batch(self, frames: List[CANFrame]) -> int:
|
|
||||||
"""Обработка батча сообщений для сохранения в SQLite."""
|
|
||||||
if not self.storage or not frames:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Конвертируем CANFrame в формат для storage
|
|
||||||
messages = []
|
|
||||||
for frame in frames:
|
|
||||||
messages.append((
|
|
||||||
frame.timestamp, # float timestamp в секундах
|
|
||||||
frame.bus,
|
|
||||||
frame.can_id,
|
|
||||||
frame.dlc,
|
|
||||||
frame.data
|
|
||||||
))
|
|
||||||
|
|
||||||
saved_count = self.storage.save_messages_batch(messages)
|
|
||||||
if saved_count != len(frames):
|
|
||||||
self.logger.warning(
|
|
||||||
f"Not all messages saved: {saved_count}/{len(frames)}",
|
|
||||||
extra={"batch_size": len(frames)}
|
|
||||||
)
|
|
||||||
return saved_count
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(
|
|
||||||
f"Failed to save messages batch: {e}",
|
|
||||||
exc_info=True,
|
|
||||||
extra={"batch_size": len(frames)}
|
|
||||||
)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def get_stats(self) -> Dict[str, Any]:
|
|
||||||
"""Получение статистики storage."""
|
|
||||||
if self.storage:
|
|
||||||
try:
|
|
||||||
return self.storage.get_stats()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return {"initialized": False}
|
|
||||||
|
|
||||||
|
|
||||||
class InfluxDBHandler(MessageHandler):
|
|
||||||
"""Обработчик для отправки в InfluxDB."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Инициализация обработчика InfluxDB."""
|
|
||||||
self.logger = logger.getChild("influxdb_handler")
|
|
||||||
self.influxdb_client = None
|
|
||||||
self._init_influxdb()
|
|
||||||
|
|
||||||
def _init_influxdb(self) -> None:
|
|
||||||
"""Инициализация InfluxDB клиента."""
|
|
||||||
if not config.influxdb.enabled:
|
|
||||||
self.logger.info("InfluxDB is disabled in configuration")
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.influxdb_client = get_influxdb_client()
|
|
||||||
self.logger.info("InfluxDB handler initialized")
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(f"Failed to initialize InfluxDB: {e}", exc_info=True)
|
|
||||||
self.influxdb_client = None
|
|
||||||
|
|
||||||
def process_batch(self, frames: List[CANFrame]) -> int:
|
|
||||||
"""Обработка батча сообщений для отправки в InfluxDB."""
|
|
||||||
if not self.influxdb_client or not frames:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Конвертируем CANFrame в формат для InfluxDB
|
|
||||||
influx_messages = []
|
|
||||||
for frame in frames:
|
|
||||||
influx_messages.append({
|
|
||||||
"interface": frame.bus,
|
|
||||||
"can_id": frame.can_id,
|
|
||||||
"dlc": frame.dlc,
|
|
||||||
"data": frame.data,
|
|
||||||
"timestamp": frame.timestamp # float timestamp в секундах
|
|
||||||
})
|
|
||||||
|
|
||||||
if influx_messages:
|
|
||||||
return self.influxdb_client.write_messages_batch(influx_messages)
|
|
||||||
return 0
|
|
||||||
except Exception as e:
|
|
||||||
self.logger.error(
|
|
||||||
f"Failed to send messages batch to InfluxDB: {e}",
|
|
||||||
exc_info=True,
|
|
||||||
extra={"batch_size": len(frames)}
|
|
||||||
)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def get_stats(self) -> Dict[str, Any]:
|
|
||||||
"""Получение статистики InfluxDB."""
|
|
||||||
if self.influxdb_client:
|
|
||||||
try:
|
|
||||||
return self.influxdb_client.get_stats()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return {"enabled": False, "initialized": False}
|
|
||||||
|
|
||||||
|
|
||||||
class MessageProcessor:
|
class MessageProcessor:
|
||||||
"""Класс для обработки и сохранения CAN сообщений с асинхронной обработкой."""
|
"""
|
||||||
|
Класс для обработки и сохранения CAN сообщений с асинхронной обработкой.
|
||||||
|
|
||||||
def __init__(self, queue_size: int = 10000):
|
Использует плагинную архитектуру обработчиков (pipeline).
|
||||||
|
Каждый обработчик реализует интерфейс BaseHandler.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, handlers: Optional[List[BaseHandler]] = None, queue_size: int = 10000):
|
||||||
"""
|
"""
|
||||||
Инициализация процессора сообщений.
|
Инициализация процессора сообщений.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
handlers: Список обработчиков для pipeline. Если None, создаются по умолчанию.
|
||||||
queue_size: Максимальный размер очереди сообщений
|
queue_size: Максимальный размер очереди сообщений
|
||||||
"""
|
"""
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
@@ -186,20 +48,59 @@ class MessageProcessor:
|
|||||||
self.queue_full_warnings = 0
|
self.queue_full_warnings = 0
|
||||||
|
|
||||||
# Инициализируем обработчики
|
# Инициализируем обработчики
|
||||||
self.handlers: List[MessageHandler] = []
|
if handlers is None:
|
||||||
self._init_handlers()
|
handlers = self._create_default_handlers()
|
||||||
|
|
||||||
def _init_handlers(self) -> None:
|
self.handlers: List[BaseHandler] = []
|
||||||
"""Инициализация обработчиков сообщений."""
|
self._init_handlers(handlers)
|
||||||
# Добавляем обработчики в порядке приоритета
|
|
||||||
self.handlers.append(StorageHandler())
|
|
||||||
|
|
||||||
if config.influxdb.enabled:
|
def _create_default_handlers(self) -> List[BaseHandler]:
|
||||||
self.handlers.append(InfluxDBHandler())
|
"""
|
||||||
|
Создание обработчиков по умолчанию из конфигурации.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Список обработчиков
|
||||||
|
"""
|
||||||
|
handlers = []
|
||||||
|
|
||||||
|
# Storage handler всегда включен
|
||||||
|
handlers.append(StorageHandler(enabled=True))
|
||||||
|
|
||||||
|
# InfluxDB handler зависит от конфигурации
|
||||||
|
handlers.append(InfluxDBHandler(enabled=None)) # None = из config
|
||||||
|
|
||||||
|
return handlers
|
||||||
|
|
||||||
|
def _init_handlers(self, handlers: List[BaseHandler]) -> None:
|
||||||
|
"""
|
||||||
|
Инициализация обработчиков.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
handlers: Список обработчиков для инициализации
|
||||||
|
"""
|
||||||
|
for handler in handlers:
|
||||||
|
if handler.is_enabled():
|
||||||
|
try:
|
||||||
|
if handler.initialize():
|
||||||
|
self.handlers.append(handler)
|
||||||
|
self.logger.info(
|
||||||
|
f"Handler '{handler.name}' initialized successfully"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.logger.warning(
|
||||||
|
f"Handler '{handler.name}' initialization failed"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(
|
||||||
|
f"Error initializing handler '{handler.name}': {e}",
|
||||||
|
exc_info=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.logger.debug(f"Handler '{handler.name}' is disabled")
|
||||||
|
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
f"Initialized {len(self.handlers)} message handlers",
|
f"Initialized {len(self.handlers)}/{len(handlers)} handlers",
|
||||||
extra={"handlers": [type(h).__name__ for h in self.handlers]}
|
extra={"handlers": [h.name for h in self.handlers]}
|
||||||
)
|
)
|
||||||
|
|
||||||
def enqueue(self, frame: CANFrame) -> bool:
|
def enqueue(self, frame: CANFrame) -> bool:
|
||||||
@@ -255,6 +156,8 @@ class MessageProcessor:
|
|||||||
batch_size = config.general.batch_size
|
batch_size = config.general.batch_size
|
||||||
batch_interval = config.general.batch_interval
|
batch_interval = config.general.batch_interval
|
||||||
last_batch_time = time.time()
|
last_batch_time = time.time()
|
||||||
|
last_flush_time = time.time()
|
||||||
|
flush_interval = 5.0 # Периодический flush обработчиков
|
||||||
|
|
||||||
while self.running or not self.message_queue.empty():
|
while self.running or not self.message_queue.empty():
|
||||||
try:
|
try:
|
||||||
@@ -282,6 +185,11 @@ class MessageProcessor:
|
|||||||
batch = []
|
batch = []
|
||||||
last_batch_time = current_time
|
last_batch_time = current_time
|
||||||
|
|
||||||
|
# Периодический flush обработчиков
|
||||||
|
if (current_time - last_flush_time) >= flush_interval:
|
||||||
|
self._flush_handlers()
|
||||||
|
last_flush_time = current_time
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
f"Error in processing loop: {e}",
|
f"Error in processing loop: {e}",
|
||||||
@@ -292,6 +200,9 @@ class MessageProcessor:
|
|||||||
if batch:
|
if batch:
|
||||||
self._process_batch(batch)
|
self._process_batch(batch)
|
||||||
|
|
||||||
|
# Финальный flush всех обработчиков
|
||||||
|
self._flush_handlers()
|
||||||
|
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
"Message processing loop stopped",
|
"Message processing loop stopped",
|
||||||
extra={
|
extra={
|
||||||
@@ -302,7 +213,7 @@ class MessageProcessor:
|
|||||||
|
|
||||||
def _process_batch(self, batch: List[CANFrame]) -> None:
|
def _process_batch(self, batch: List[CANFrame]) -> None:
|
||||||
"""
|
"""
|
||||||
Обработка батча CAN фреймов.
|
Обработка батча CAN фреймов через pipeline обработчиков.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
batch: Список CANFrame объектов
|
batch: Список CANFrame объектов
|
||||||
@@ -312,7 +223,6 @@ class MessageProcessor:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Логируем батч на уровне DEBUG (если уровень DEBUG включен)
|
# Логируем батч на уровне DEBUG (если уровень DEBUG включен)
|
||||||
# Это позволяет контролировать вывод через уровни логирования
|
|
||||||
if batch:
|
if batch:
|
||||||
first_frame = batch[0]
|
first_frame = batch[0]
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
@@ -329,13 +239,16 @@ class MessageProcessor:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Обрабатываем батч через все обработчики
|
# Обрабатываем батч через все обработчики (pipeline)
|
||||||
for handler in self.handlers:
|
for handler in self.handlers:
|
||||||
|
if not handler.is_enabled() or not handler.is_initialized():
|
||||||
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
handler.process_batch(batch)
|
handler.handle_batch(batch)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
f"Error in handler {type(handler).__name__}: {e}",
|
f"Error in handler '{handler.name}': {e}",
|
||||||
exc_info=True,
|
exc_info=True,
|
||||||
extra={"batch_size": len(batch)}
|
extra={"batch_size": len(batch)}
|
||||||
)
|
)
|
||||||
@@ -350,6 +263,18 @@ class MessageProcessor:
|
|||||||
extra={"batch_size": len(batch)}
|
extra={"batch_size": len(batch)}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _flush_handlers(self) -> None:
|
||||||
|
"""Принудительный flush всех обработчиков."""
|
||||||
|
for handler in self.handlers:
|
||||||
|
if handler.is_enabled() and handler.is_initialized():
|
||||||
|
try:
|
||||||
|
handler.flush()
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(
|
||||||
|
f"Error flushing handler '{handler.name}': {e}",
|
||||||
|
exc_info=True
|
||||||
|
)
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
"""Запуск обработки сообщений в отдельном потоке."""
|
"""Запуск обработки сообщений в отдельном потоке."""
|
||||||
if self.running:
|
if self.running:
|
||||||
@@ -358,10 +283,16 @@ class MessageProcessor:
|
|||||||
|
|
||||||
self.running = True
|
self.running = True
|
||||||
|
|
||||||
# Запускаем InfluxDB forwarder, если он есть
|
# Запускаем специальные обработчики (например, InfluxDB forwarder)
|
||||||
for handler in self.handlers:
|
for handler in self.handlers:
|
||||||
if isinstance(handler, InfluxDBHandler) and handler.influxdb_client:
|
if isinstance(handler, InfluxDBHandler) and handler.is_initialized():
|
||||||
handler.influxdb_client.start()
|
try:
|
||||||
|
handler.start()
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(
|
||||||
|
f"Failed to start handler '{handler.name}': {e}",
|
||||||
|
exc_info=True
|
||||||
|
)
|
||||||
|
|
||||||
# Запускаем поток обработки сообщений
|
# Запускаем поток обработки сообщений
|
||||||
self.processing_thread = threading.Thread(
|
self.processing_thread = threading.Thread(
|
||||||
@@ -383,16 +314,13 @@ class MessageProcessor:
|
|||||||
if self.processing_thread.is_alive():
|
if self.processing_thread.is_alive():
|
||||||
self.logger.warning("Processing thread did not stop gracefully")
|
self.logger.warning("Processing thread did not stop gracefully")
|
||||||
|
|
||||||
# Закрываем обработчики
|
# Закрываем все обработчики
|
||||||
for handler in self.handlers:
|
for handler in self.handlers:
|
||||||
try:
|
try:
|
||||||
if isinstance(handler, StorageHandler) and handler.storage:
|
handler.shutdown()
|
||||||
handler.storage.close()
|
|
||||||
elif isinstance(handler, InfluxDBHandler) and handler.influxdb_client:
|
|
||||||
handler.influxdb_client.close()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
f"Error closing handler {type(handler).__name__}: {e}",
|
f"Error shutting down handler '{handler.name}': {e}",
|
||||||
exc_info=True
|
exc_info=True
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -410,16 +338,16 @@ class MessageProcessor:
|
|||||||
"processed_count": self.processed_count,
|
"processed_count": self.processed_count,
|
||||||
"dropped_count": self.dropped_count,
|
"dropped_count": self.dropped_count,
|
||||||
"queue_size": self.message_queue.qsize(),
|
"queue_size": self.message_queue.qsize(),
|
||||||
"running": self.running
|
"running": self.running,
|
||||||
|
"handlers_count": len(self.handlers)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Добавляем статистику всех обработчиков
|
# Добавляем статистику всех обработчиков
|
||||||
for handler in self.handlers:
|
for handler in self.handlers:
|
||||||
try:
|
try:
|
||||||
handler_stats = handler.get_stats()
|
handler_stats = handler.get_stats()
|
||||||
handler_name = type(handler).__name__.replace("Handler", "").lower()
|
stats[handler.name] = handler_stats
|
||||||
stats[handler_name] = handler_stats
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.debug(f"Failed to get stats from {type(handler).__name__}: {e}")
|
self.logger.debug(f"Failed to get stats from handler '{handler.name}': {e}")
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
|
|||||||
Reference in New Issue
Block a user