package eventbus import ( "context" "encoding/json" "fmt" "time" amqp "github.com/rabbitmq/amqp091-go" "go.uber.org/zap" ) // Event représente un événement métier dans le système // Suit le pattern défini dans ORIGIN_MASTER_ARCHITECTURE.md type Event struct { EventID string `json:"event_id"` EventType string `json:"event_type"` // format: {domain}.{entity}.{action}.{version} AggregateID string `json:"aggregate_id"` AggregateType string `json:"aggregate_type"` Timestamp time.Time `json:"timestamp"` Version int `json:"version"` Data map[string]interface{} `json:"data"` Metadata map[string]interface{} `json:"metadata"` } // RabbitMQClient gère la connexion et publication d'événements vers RabbitMQ // Implémentation minimale alignée avec ORIGIN pour Phase 1 type RabbitMQClient struct { conn *amqp.Connection channel *amqp.Channel exchange string logger *zap.Logger } // NewRabbitMQClient crée un nouveau client RabbitMQ // url format: amqp://user:pass@host:5672/ func NewRabbitMQClient(url, exchange string, logger *zap.Logger) (*RabbitMQClient, error) { conn, err := amqp.Dial(url) if err != nil { return nil, fmt.Errorf("failed to connect to RabbitMQ: %w", err) } channel, err := conn.Channel() if err != nil { conn.Close() return nil, fmt.Errorf("failed to open channel: %w", err) } // Déclarer l'exchange (topic type pour routing flexible) err = channel.ExchangeDeclare( exchange, // name "topic", // type true, // durable false, // auto-deleted false, // internal false, // no-wait nil, // arguments ) if err != nil { channel.Close() conn.Close() return nil, fmt.Errorf("failed to declare exchange: %w", err) } logger.Info("RabbitMQ client initialized", zap.String("exchange", exchange), zap.String("url", url), ) return &RabbitMQClient{ conn: conn, channel: channel, exchange: exchange, logger: logger, }, nil } // PublishEvent publie un événement sur RabbitMQ // routingKey format: {domain}.{entity}.{action} (ex: "auth.user.registered") func (c *RabbitMQClient) PublishEvent(ctx context.Context, event *Event) error { body, err := json.Marshal(event) if err != nil { return fmt.Errorf("failed to marshal event: %w", err) } err = c.channel.PublishWithContext( ctx, c.exchange, // exchange event.EventType, // routing key false, // mandatory false, // immediate amqp.Publishing{ ContentType: "application/json", DeliveryMode: amqp.Persistent, // messages persistent Timestamp: event.Timestamp, MessageId: event.EventID, Type: event.EventType, Body: body, }, ) if err != nil { c.logger.Error("Failed to publish event", zap.Error(err), zap.String("event_type", event.EventType), zap.String("event_id", event.EventID), ) return fmt.Errorf("failed to publish event: %w", err) } c.logger.Debug("Event published", zap.String("event_type", event.EventType), zap.String("event_id", event.EventID), zap.String("aggregate_id", event.AggregateID), ) return nil } // Close ferme proprement la connexion RabbitMQ func (c *RabbitMQClient) Close() error { if c.channel != nil { c.channel.Close() } if c.conn != nil { c.conn.Close() } c.logger.Info("RabbitMQ client closed") return nil } // HealthCheck vérifie si la connexion RabbitMQ est active func (c *RabbitMQClient) HealthCheck() error { if c.conn == nil || c.conn.IsClosed() { return fmt.Errorf("RabbitMQ connection is closed") } return nil }