182 lines
5.0 KiB
Go
182 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{
|
|
Topic: p.topic,
|
|
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)
|
|
}
|