|
|
from __future__ import annotations
|
|
|
|
|
|
import json
|
|
|
import logging
|
|
|
import os
|
|
|
from dataclasses import dataclass
|
|
|
from typing import Any, Callable, Optional, Union
|
|
|
from iti.applications.service.iot.alert import add_endpoint_alert_log, add_node_alert_log
|
|
|
|
|
|
try:
|
|
|
from rocketmq.client import ConsumeStatus, Message, Producer, PushConsumer
|
|
|
except Exception:
|
|
|
ConsumeStatus = None
|
|
|
Message = None
|
|
|
Producer = None
|
|
|
PushConsumer = None
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
RocketMQBody = Union[str, bytes, bytearray, dict, list]
|
|
|
ConsumeCallback = Callable[[Any], Any]
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
class RocketMQConfig:
|
|
|
namesrv_addr: str
|
|
|
producer_group: str
|
|
|
consumer_group: str
|
|
|
default_topic: str
|
|
|
default_tags: str
|
|
|
charset: str
|
|
|
access_key: str
|
|
|
secret_key: str
|
|
|
security_token: str
|
|
|
instance_name: str
|
|
|
|
|
|
@staticmethod
|
|
|
def from_env() -> "RocketMQConfig":
|
|
|
return RocketMQConfig(
|
|
|
namesrv_addr=os.getenv("ROCKETMQ_NAMESRV_ADDR", "127.0.0.1:9876").strip(),
|
|
|
producer_group=os.getenv("ROCKETMQ_PRODUCER_GROUP", "iot-collect-group").strip(),
|
|
|
consumer_group=os.getenv("ROCKETMQ_CONSUMER_GROUP", "iot-collect-group").strip(),
|
|
|
default_topic=os.getenv("ROCKETMQ_TOPIC", "iot-collect-topic").strip(),
|
|
|
default_tags=os.getenv("ROCKETMQ_TAGS", "*").strip() or "*",
|
|
|
charset=os.getenv("ROCKETMQ_CHARSET", "utf-8").strip() or "utf-8",
|
|
|
access_key=os.getenv("ROCKETMQ_ACCESS_KEY", "").strip(),
|
|
|
secret_key=os.getenv("ROCKETMQ_SECRET_KEY", "").strip(),
|
|
|
security_token=os.getenv("ROCKETMQ_SECURITY_TOKEN", "").strip(),
|
|
|
instance_name=os.getenv("ROCKETMQ_INSTANCE_NAME", "").strip(),
|
|
|
)
|
|
|
|
|
|
|
|
|
class RocketMQMgr:
|
|
|
_instance = None
|
|
|
_initialized = False
|
|
|
|
|
|
def __new__(cls):
|
|
|
if cls._instance is None:
|
|
|
cls._instance = super().__new__(cls)
|
|
|
return cls._instance
|
|
|
|
|
|
def init_app(self, app) -> None:
|
|
|
if self._initialized:
|
|
|
logger.warning("rocketmq连接已初始化,跳过重复初始化")
|
|
|
return
|
|
|
|
|
|
if Producer is None or PushConsumer is None or Message is None:
|
|
|
logger.error(
|
|
|
"rocketmq客户端库不可用,请安装 rocketmq-client-python(并确保本机可加载 librocketmq)"
|
|
|
)
|
|
|
self._initialized = False
|
|
|
return
|
|
|
|
|
|
cfg = RocketMQConfig.from_env()
|
|
|
|
|
|
if not cfg.namesrv_addr:
|
|
|
logger.error("rocketmq配置不完整,缺少: ROCKETMQ_NAMESRV_ADDR")
|
|
|
self._initialized = False
|
|
|
return
|
|
|
|
|
|
self.cfg = cfg
|
|
|
self._app = app
|
|
|
self.producer: Optional[Any] = None
|
|
|
self.consumer: Optional[Any] = None
|
|
|
self._producer_started = False
|
|
|
self._consumer_started = False
|
|
|
|
|
|
self._initialized = True
|
|
|
logger.info("rocketmq初始化成功")
|
|
|
|
|
|
def _ensure_initialized(self) -> bool:
|
|
|
if not self._initialized:
|
|
|
logger.error("rocketmq未初始化,请先调用 iot_rocketmq.init_app(app)")
|
|
|
return False
|
|
|
return True
|
|
|
|
|
|
def _apply_credentials_if_supported(self, client: Any) -> None:
|
|
|
if not self.cfg.access_key or not self.cfg.secret_key:
|
|
|
return
|
|
|
if hasattr(client, "set_session_credentials"):
|
|
|
try:
|
|
|
client.set_session_credentials(
|
|
|
self.cfg.access_key, self.cfg.secret_key, self.cfg.security_token
|
|
|
)
|
|
|
except Exception as e:
|
|
|
logger.warning(f"rocketmq设置ACL凭证失败: {e}")
|
|
|
|
|
|
def start_producer(self) -> bool:
|
|
|
if not self._ensure_initialized():
|
|
|
return False
|
|
|
if self._producer_started and self.producer:
|
|
|
return True
|
|
|
|
|
|
try:
|
|
|
producer = Producer(self.cfg.producer_group)
|
|
|
producer.set_name_server_address(self.cfg.namesrv_addr)
|
|
|
if self.cfg.instance_name and hasattr(producer, "set_instance_name"):
|
|
|
producer.set_instance_name(self.cfg.instance_name)
|
|
|
self._apply_credentials_if_supported(producer)
|
|
|
producer.start()
|
|
|
self.producer = producer
|
|
|
self._producer_started = True
|
|
|
logger.info("rocketmq producer启动成功")
|
|
|
return True
|
|
|
except Exception as e:
|
|
|
logger.error(f"rocketmq producer启动失败: {e}", exc_info=True)
|
|
|
self.producer = None
|
|
|
self._producer_started = False
|
|
|
return False
|
|
|
|
|
|
def shutdown_producer(self) -> None:
|
|
|
if not self.producer:
|
|
|
return
|
|
|
try:
|
|
|
self.producer.shutdown()
|
|
|
except Exception as e:
|
|
|
logger.warning(f"rocketmq producer关闭失败: {e}")
|
|
|
finally:
|
|
|
self.producer = None
|
|
|
self._producer_started = False
|
|
|
|
|
|
def _encode_body(self, body: RocketMQBody) -> bytes:
|
|
|
if isinstance(body, bytes):
|
|
|
return body
|
|
|
if isinstance(body, bytearray):
|
|
|
return bytes(body)
|
|
|
if isinstance(body, str):
|
|
|
return body.encode(self.cfg.charset, errors="replace")
|
|
|
return json.dumps(body, ensure_ascii=False).encode(self.cfg.charset, errors="replace")
|
|
|
|
|
|
def send_sync(
|
|
|
self,
|
|
|
body: RocketMQBody,
|
|
|
topic: Optional[str] = None,
|
|
|
tags: Optional[str] = None,
|
|
|
keys: Optional[str] = None,
|
|
|
) -> Any:
|
|
|
if not self._ensure_initialized():
|
|
|
raise RuntimeError("rocketmq未初始化")
|
|
|
if not self.start_producer() or not self.producer:
|
|
|
raise RuntimeError("rocketmq producer未启动")
|
|
|
|
|
|
real_topic = (topic or self.cfg.default_topic).strip()
|
|
|
if not real_topic:
|
|
|
logger.error("topic不能为空(可通过参数传入或设置ROCKETMQ_TOPIC)")
|
|
|
raise ValueError("topic不能为空(可通过参数传入或设置ROCKETMQ_TOPIC)")
|
|
|
|
|
|
msg = Message(real_topic)
|
|
|
if tags:
|
|
|
if hasattr(msg, "set_tags"):
|
|
|
msg.set_tags(tags)
|
|
|
if keys:
|
|
|
if hasattr(msg, "set_keys"):
|
|
|
msg.set_keys(keys)
|
|
|
msg.set_body(self._encode_body(body))
|
|
|
return self.producer.send_sync(msg)
|
|
|
|
|
|
def start_consumer(
|
|
|
self,
|
|
|
topic: Optional[str] = None,
|
|
|
tags: Optional[str] = None,
|
|
|
callback: Optional[ConsumeCallback] = None,
|
|
|
) -> bool:
|
|
|
if not self._ensure_initialized():
|
|
|
return False
|
|
|
if self._consumer_started and self.consumer:
|
|
|
return True
|
|
|
|
|
|
real_topic = (topic or self.cfg.default_topic).strip()
|
|
|
if not real_topic:
|
|
|
logger.error("consumer topic不能为空(可通过参数传入或设置ROCKETMQ_TOPIC)")
|
|
|
return False
|
|
|
|
|
|
selector_expression = (tags or self.cfg.default_tags).strip() or "*"
|
|
|
consume_cb = callback or self._default_consume_callback
|
|
|
|
|
|
try:
|
|
|
consumer = PushConsumer(self.cfg.consumer_group)
|
|
|
consumer.set_name_server_address(self.cfg.namesrv_addr)
|
|
|
if self.cfg.instance_name and hasattr(consumer, "set_instance_name"):
|
|
|
consumer.set_instance_name(self.cfg.instance_name)
|
|
|
self._apply_credentials_if_supported(consumer)
|
|
|
|
|
|
try:
|
|
|
consumer.subscribe(real_topic, consume_cb, selector_expression)
|
|
|
except TypeError:
|
|
|
consumer.subscribe(real_topic, selector_expression, consume_cb)
|
|
|
|
|
|
consumer.start()
|
|
|
self.consumer = consumer
|
|
|
self._consumer_started = True
|
|
|
logger.info("rocketmq consumer启动成功")
|
|
|
return True
|
|
|
except Exception as e:
|
|
|
logger.error(f"rocketmq consumer启动失败: {e}", exc_info=True)
|
|
|
self.consumer = None
|
|
|
self._consumer_started = False
|
|
|
return False
|
|
|
|
|
|
def shutdown_consumer(self) -> None:
|
|
|
if not self.consumer:
|
|
|
return
|
|
|
try:
|
|
|
self.consumer.shutdown()
|
|
|
except Exception as e:
|
|
|
logger.warning(f"rocketmq consumer关闭失败: {e}")
|
|
|
finally:
|
|
|
self.consumer = None
|
|
|
self._consumer_started = False
|
|
|
|
|
|
def _default_consume_callback(self, msg: Any) -> Any:
|
|
|
topic = getattr(msg, "topic", "")
|
|
|
tags = getattr(msg, "tags", "")
|
|
|
keys = getattr(msg, "keys", "")
|
|
|
body_text = getattr(msg, "body", b"").decode(self.cfg.charset, errors="replace")
|
|
|
|
|
|
# logger.info(
|
|
|
# f"[RocketMQ] topic={topic} tags={tags} keys={keys} body={body_text}"
|
|
|
# )
|
|
|
try:
|
|
|
body_dict = json.loads(body_text)
|
|
|
except Exception:
|
|
|
logger.error(f"[RocketMQ] 解析body失败: {body_text}")
|
|
|
return False
|
|
|
|
|
|
app = getattr(self, "_app", None)
|
|
|
if app is None:
|
|
|
logger.error("[RocketMQ] 未绑定Flask app,无法创建application context")
|
|
|
return False
|
|
|
|
|
|
with app.app_context():
|
|
|
if tags == b"netData" or tags == "netData":
|
|
|
add_endpoint_alert_log(body_dict["endpointId"])
|
|
|
|
|
|
elif tags == b"collectData" or tags == "collectData":
|
|
|
for node_data in body_dict["nodeDataList"]:
|
|
|
node_id = int(node_data["nodeId"])
|
|
|
alert_value = node_data["alertValue"]
|
|
|
add_node_alert_log(node_id, alert_value)
|
|
|
|
|
|
if ConsumeStatus is not None and hasattr(ConsumeStatus, "CONSUME_SUCCESS"):
|
|
|
return ConsumeStatus.CONSUME_SUCCESS
|
|
|
return True
|
|
|
|
|
|
def shutdown(self) -> None:
|
|
|
self.shutdown_consumer()
|
|
|
self.shutdown_producer()
|
|
|
|
|
|
def __del__(self):
|
|
|
try:
|
|
|
self.shutdown()
|
|
|
except Exception:
|
|
|
pass
|
|
|
|
|
|
|
|
|
iot_rocketmq = RocketMQMgr()
|