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"