Files
v2-Docker/lizenzserver/events/event_bus.py
Claude Project Manager 0d7d888502 Initial commit
2025-07-05 17:51:16 +02:00

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"