191 Zeilen
6.7 KiB
Python
191 Zeilen
6.7 KiB
Python
import json
|
|
import logging
|
|
from typing import Dict, Any, Callable, List
|
|
from datetime import datetime
|
|
import pika
|
|
from pika.exceptions import AMQPConnectionError
|
|
import threading
|
|
from collections import defaultdict
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class Event:
|
|
"""Base event class"""
|
|
def __init__(self, event_type: str, data: Dict[str, Any], source: str = "unknown"):
|
|
self.id = self._generate_id()
|
|
self.type = event_type
|
|
self.data = data
|
|
self.source = source
|
|
self.timestamp = datetime.utcnow().isoformat()
|
|
|
|
def _generate_id(self) -> str:
|
|
import uuid
|
|
return str(uuid.uuid4())
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
return {
|
|
"id": self.id,
|
|
"type": self.type,
|
|
"data": self.data,
|
|
"source": self.source,
|
|
"timestamp": self.timestamp
|
|
}
|
|
|
|
def to_json(self) -> str:
|
|
return json.dumps(self.to_dict())
|
|
|
|
class EventBus:
|
|
"""Event bus for pub/sub pattern with RabbitMQ backend"""
|
|
|
|
def __init__(self, rabbitmq_url: str):
|
|
self.rabbitmq_url = rabbitmq_url
|
|
self.connection = None
|
|
self.channel = None
|
|
self.exchange_name = "license_events"
|
|
self.local_handlers: Dict[str, List[Callable]] = defaultdict(list)
|
|
self._connect()
|
|
|
|
def _connect(self):
|
|
"""Establish connection to RabbitMQ"""
|
|
try:
|
|
parameters = pika.URLParameters(self.rabbitmq_url)
|
|
self.connection = pika.BlockingConnection(parameters)
|
|
self.channel = self.connection.channel()
|
|
|
|
# Declare exchange
|
|
self.channel.exchange_declare(
|
|
exchange=self.exchange_name,
|
|
exchange_type='topic',
|
|
durable=True
|
|
)
|
|
|
|
logger.info("Connected to RabbitMQ")
|
|
except AMQPConnectionError as e:
|
|
logger.error(f"Failed to connect to RabbitMQ: {e}")
|
|
# Fallback to local-only event handling
|
|
self.connection = None
|
|
self.channel = None
|
|
|
|
def publish(self, event: Event):
|
|
"""Publish an event"""
|
|
try:
|
|
# Publish to RabbitMQ if connected
|
|
if self.channel and not self.channel.is_closed:
|
|
self.channel.basic_publish(
|
|
exchange=self.exchange_name,
|
|
routing_key=event.type,
|
|
body=event.to_json(),
|
|
properties=pika.BasicProperties(
|
|
delivery_mode=2, # Make message persistent
|
|
content_type='application/json'
|
|
)
|
|
)
|
|
logger.debug(f"Published event: {event.type}")
|
|
|
|
# Also handle local subscribers
|
|
self._handle_local_event(event)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error publishing event: {e}")
|
|
# Ensure local handlers still get called
|
|
self._handle_local_event(event)
|
|
|
|
def subscribe(self, event_type: str, handler: Callable):
|
|
"""Subscribe to an event type locally"""
|
|
self.local_handlers[event_type].append(handler)
|
|
logger.debug(f"Subscribed to {event_type}")
|
|
|
|
def subscribe_queue(self, event_types: List[str], queue_name: str, handler: Callable):
|
|
"""Subscribe to events via RabbitMQ queue"""
|
|
if not self.channel:
|
|
logger.warning("RabbitMQ not connected, falling back to local subscription")
|
|
for event_type in event_types:
|
|
self.subscribe(event_type, handler)
|
|
return
|
|
|
|
try:
|
|
# Declare queue
|
|
self.channel.queue_declare(queue=queue_name, durable=True)
|
|
|
|
# Bind queue to exchange for each event type
|
|
for event_type in event_types:
|
|
self.channel.queue_bind(
|
|
exchange=self.exchange_name,
|
|
queue=queue_name,
|
|
routing_key=event_type
|
|
)
|
|
|
|
# Set up consumer
|
|
def callback(ch, method, properties, body):
|
|
try:
|
|
event_data = json.loads(body)
|
|
event = Event(
|
|
event_type=event_data['type'],
|
|
data=event_data['data'],
|
|
source=event_data['source']
|
|
)
|
|
handler(event)
|
|
ch.basic_ack(delivery_tag=method.delivery_tag)
|
|
except Exception as e:
|
|
logger.error(f"Error handling event: {e}")
|
|
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=True)
|
|
|
|
self.channel.basic_consume(queue=queue_name, on_message_callback=callback)
|
|
|
|
# Start consuming in a separate thread
|
|
consumer_thread = threading.Thread(target=self.channel.start_consuming)
|
|
consumer_thread.daemon = True
|
|
consumer_thread.start()
|
|
|
|
logger.info(f"Started consuming from queue: {queue_name}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error setting up queue subscription: {e}")
|
|
|
|
def _handle_local_event(self, event: Event):
|
|
"""Handle event with local subscribers"""
|
|
handlers = self.local_handlers.get(event.type, [])
|
|
for handler in handlers:
|
|
try:
|
|
handler(event)
|
|
except Exception as e:
|
|
logger.error(f"Error in event handler: {e}")
|
|
|
|
def close(self):
|
|
"""Close RabbitMQ connection"""
|
|
if self.connection and not self.connection.is_closed:
|
|
self.connection.close()
|
|
logger.info("Closed RabbitMQ connection")
|
|
|
|
# Event types
|
|
class EventTypes:
|
|
"""Centralized event type definitions"""
|
|
|
|
# License events
|
|
LICENSE_VALIDATED = "license.validated"
|
|
LICENSE_VALIDATION_FAILED = "license.validation.failed"
|
|
LICENSE_ACTIVATED = "license.activated"
|
|
LICENSE_DEACTIVATED = "license.deactivated"
|
|
LICENSE_TRANSFERRED = "license.transferred"
|
|
LICENSE_EXPIRED = "license.expired"
|
|
LICENSE_CREATED = "license.created"
|
|
LICENSE_UPDATED = "license.updated"
|
|
|
|
# Device events
|
|
DEVICE_ADDED = "device.added"
|
|
DEVICE_REMOVED = "device.removed"
|
|
DEVICE_BLOCKED = "device.blocked"
|
|
DEVICE_DEACTIVATED = "device.deactivated"
|
|
|
|
# Anomaly events
|
|
ANOMALY_DETECTED = "anomaly.detected"
|
|
ANOMALY_RESOLVED = "anomaly.resolved"
|
|
|
|
# Session events
|
|
SESSION_STARTED = "session.started"
|
|
SESSION_ENDED = "session.ended"
|
|
SESSION_EXPIRED = "session.expired"
|
|
|
|
# System events
|
|
RATE_LIMIT_EXCEEDED = "system.rate_limit_exceeded"
|
|
API_ERROR = "system.api_error" |