Files
olons_server/internal/events/events.go
T

181 lines
5.0 KiB
Go

package events
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"strconv"
"strings"
"time"
"git.118im.com/im_team/im_workbench/internal/auth"
"git.118im.com/im_team/im_workbench/internal/config"
"github.com/segmentio/kafka-go"
"github.com/segmentio/kafka-go/sasl"
"github.com/segmentio/kafka-go/sasl/scram"
)
type Event struct {
EventID string `json:"event_id"`
TraceID string `json:"trace_id,omitempty"`
TenantID int64 `json:"tenant_id"`
TenantCode string `json:"tenant_code,omitempty"`
ActorUserID int64 `json:"actor_user_id"`
Module string `json:"module"`
Action string `json:"action"`
ResourceID string `json:"resource_id"`
TargetUserIDs []int64 `json:"target_user_ids,omitempty"`
Title string `json:"title,omitempty"`
Summary string `json:"summary,omitempty"`
OccurredAt time.Time `json:"occurred_at"`
Data map[string]any `json:"data,omitempty"`
}
type Publisher interface {
Publish(context.Context, Event) error
Close() error
}
type NoopPublisher struct{}
func (NoopPublisher) Publish(context.Context, Event) error { return nil }
func (NoopPublisher) Close() error { return nil }
type KafkaPublisher struct {
writer *kafka.Writer
topic string
}
func NewKafkaPublisher(cfg config.KafkaConfig) (Publisher, error) {
if !cfg.Enabled || len(cfg.Brokers) == 0 {
return NoopPublisher{}, nil
}
topic := strings.TrimSpace(cfg.Topic)
if topic == "" {
topic = "im.workbench.event"
}
transport := &kafka.Transport{}
if mechanism, err := saslMechanism(cfg); err != nil {
return nil, err
} else if mechanism != nil {
transport.SASL = mechanism
}
writer := &kafka.Writer{
Addr: kafka.TCP(cfg.Brokers...),
Topic: topic,
Balancer: &kafka.Hash{},
Transport: transport,
BatchSize: cfg.BatchSize,
RequiredAcks: kafka.RequireAll,
Async: false,
}
if writer.BatchSize <= 0 {
writer.BatchSize = 1
}
return &KafkaPublisher{writer: writer, topic: topic}, nil
}
func (p *KafkaPublisher) Publish(ctx context.Context, event Event) error {
if p == nil || p.writer == nil {
return nil
}
if strings.TrimSpace(event.EventID) == "" {
event.EventID = StableEventID(event.TenantID, event.Module, event.Action, event.ResourceID, event.ActorUserID)
}
if event.OccurredAt.IsZero() {
event.OccurredAt = time.Now().UTC().Round(time.Millisecond)
}
data, err := json.Marshal(event)
if err != nil {
return err
}
key := []byte(event.TenantCode + ":" + event.ResourceID)
if len(key) == 1 {
key = []byte(event.EventID)
}
return p.writer.WriteMessages(ctx, kafka.Message{
Key: key,
Value: data,
Headers: []kafka.Header{
{Key: "event_id", Value: []byte(event.EventID)},
{Key: "event_type", Value: []byte(event.Action)},
{Key: "tenant_code", Value: []byte(event.TenantCode)},
},
Time: event.OccurredAt,
})
}
func (p *KafkaPublisher) Close() error {
if p == nil || p.writer == nil {
return nil
}
return p.writer.Close()
}
func NewWorkbenchEvent(p *auth.Principal, traceID, module, action, resourceID string, targets []int64, title, summary string, data map[string]any) Event {
event := Event{
TraceID: strings.TrimSpace(traceID),
TenantID: p.TenantID,
TenantCode: p.TenantCode,
ActorUserID: p.UserID,
Module: strings.TrimSpace(module),
Action: strings.TrimSpace(action),
ResourceID: strings.TrimSpace(resourceID),
TargetUserIDs: compactInt64s(targets),
Title: strings.TrimSpace(title),
Summary: strings.TrimSpace(summary),
OccurredAt: time.Now().UTC().Round(time.Millisecond),
Data: data,
}
event.EventID = StableEventID(event.TenantID, event.Module, event.Action, event.ResourceID, event.ActorUserID)
return event
}
func StableEventID(tenantID int64, module, action, resourceID string, actorUserID int64) string {
raw := strings.Join([]string{
int64String(tenantID),
strings.TrimSpace(module),
strings.TrimSpace(action),
strings.TrimSpace(resourceID),
int64String(actorUserID),
}, ":")
sum := sha256.Sum256([]byte(raw))
return "wbe_" + hex.EncodeToString(sum[:])[:32]
}
func saslMechanism(cfg config.KafkaConfig) (sasl.Mechanism, error) {
if strings.TrimSpace(cfg.Username) == "" {
return nil, nil
}
switch strings.ToUpper(strings.TrimSpace(cfg.SASL)) {
case "", "SCRAM-SHA-512":
return scram.Mechanism(scram.SHA512, cfg.Username, cfg.Password)
case "SCRAM-SHA-256":
return scram.Mechanism(scram.SHA256, cfg.Username, cfg.Password)
default:
return nil, errors.New("unsupported kafka sasl mechanism")
}
}
func compactInt64s(values []int64) []int64 {
if len(values) == 0 {
return nil
}
seen := make(map[int64]bool, len(values))
items := make([]int64, 0, len(values))
for _, value := range values {
if value <= 0 || seen[value] {
continue
}
seen[value] = true
items = append(items, value)
}
return items
}
func int64String(value int64) string {
return strconv.FormatInt(value, 10)
}