Initial commit
Dieser Commit ist enthalten in:
1
lizenzserver/events/__init__.py
Normale Datei
1
lizenzserver/events/__init__.py
Normale Datei
@ -0,0 +1 @@
|
||||
# Events Module
|
||||
191
lizenzserver/events/event_bus.py
Normale Datei
191
lizenzserver/events/event_bus.py
Normale Datei
@ -0,0 +1,191 @@
|
||||
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"
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren