veza/veza-backend-api/internal/infrastructure/eventbus/rabbitmq.go
2025-12-03 20:29:37 +01:00

138 lines
3.6 KiB
Go

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
}