feat: consume workbench kafka events

This commit is contained in:
richjohnrambo
2026-09-24 16:56:33 +08:00
parent 7405277f27
commit 31a70c0bac
3 changed files with 167 additions and 0 deletions
+2
View File
@@ -26,6 +26,7 @@ const (
kafkaTopicGroupMessagePersist = "im.group.message.persist"
kafkaTopicMessagePush = "im.message.push"
kafkaTopicFileSecurity = "im.file.security"
kafkaTopicWorkbenchEvent = "im.workbench.event"
kafkaTopicCacheInvalidate = "im.cache.invalidate"
kafkaTopicAuditEvent = "im.audit.event"
kafkaTopicDeadLetter = "im.dead.letter"
@@ -214,6 +215,7 @@ func kafkaTopics(overrides map[string]string) map[string]string {
"group_message_persist": kafkaTopicGroupMessagePersist,
"message_push": kafkaTopicMessagePush,
"file_security": kafkaTopicFileSecurity,
"workbench_external": kafkaTopicWorkbenchEvent,
"cache_invalidate": kafkaTopicCacheInvalidate,
"audit_event": kafkaTopicAuditEvent,
"dead_letter": kafkaTopicDeadLetter,
+164
View File
@@ -0,0 +1,164 @@
package main
import (
"context"
"encoding/json"
"strings"
"time"
"github.com/segmentio/kafka-go"
"github.com/tinode/chat/server/push"
"github.com/tinode/chat/server/store"
"github.com/tinode/chat/server/store/types"
)
type kafkaWorkbenchEvent struct {
EventID string `json:"event_id"`
TraceID string `json:"trace_id,omitempty"`
TenantID types.TenantID `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"`
}
func startKafkaWorkbenchEventConsumer() {
startKafkaConsumer("workbench_external", handleKafkaWorkbenchEvent)
}
func handleKafkaWorkbenchEvent(ctx context.Context, envelope kafkaEnvelope, msg kafka.Message) error {
var event kafkaWorkbenchEvent
if err := json.Unmarshal(msg.Value, &event); err != nil {
return err
}
event.normalize()
if event.EventID == "" || event.TenantID.IsZero() || event.Action == "" || event.ResourceID == "" {
return types.ErrMalformed
}
envelope = event.kafkaEnvelope(envelope)
_, claimed, err := beginKafkaConsumeForTopic(event.TenantID, envelope, "workbench_external")
if err != nil || !claimed {
return err
}
if err = executeKafkaWorkbenchEvent(event); err != nil {
reason := kafkaCommandErrorReason(err)
failed, markErr := store.HTTPInfrastructure.MarkKafkaConsumeFailed(event.TenantID, event.EventID, kafkaBus.config.ConsumerGroup,
reason, err.Error(), time.Now().UTC().Round(time.Millisecond))
if markErr != nil {
return markErr
}
if failed != nil && failed.RetryCount >= kafkaBus.config.MaxRetryCount {
return markKafkaDeadLetter(ctx, event.TenantID, envelope, failed, reason, err.Error())
}
if failed != nil {
sleepKafkaRetryBackoff(failed.RetryCount)
}
return err
}
return store.HTTPInfrastructure.MarkKafkaConsumeSucceeded(event.TenantID, event.EventID, kafkaBus.config.ConsumerGroup, time.Now().UTC().Round(time.Millisecond))
}
func executeKafkaWorkbenchEvent(event kafkaWorkbenchEvent) error {
actor := store.EncodeUid(event.ActorUserID)
payload, err := event.realtimePayload()
if err != nil {
return err
}
pushTargets := make(map[types.Uid]push.Recipient)
for _, targetID := range event.TargetUserIDs {
target := store.EncodeUid(targetID)
if target.IsZero() {
continue
}
if target != actor && event.Title != "" {
action := event.notificationAction()
if err = createIMSystemNotification(event.TenantID, target, stableIMEventID(event.EventID, "notification", target.UserId()),
event.Action, event.Title, event.Summary, action); err != nil && err != types.ErrDuplicate {
return err
}
pushTargets[target] = push.Recipient{}
}
if err = enqueueIMBusinessEvent(event.TenantID, event.EventID, "user", target.UserId(), actor, event.Action, payload); err != nil {
return err
}
}
if len(pushTargets) > 0 {
sendPush(&push.Receipt{TenantID: event.TenantID, To: pushTargets, Payload: push.Payload{
What: push.ActMsg, Topic: "sys", Timestamp: time.Now().UTC(), ContentType: "application/json",
Content: map[string]any{"event_id": event.EventID, "event_type": event.Action, "title": event.Title, "summary": event.Summary, "action": event.notificationAction()},
}})
}
return nil
}
func (e *kafkaWorkbenchEvent) normalize() {
e.EventID = strings.TrimSpace(e.EventID)
e.TenantCode = strings.TrimSpace(e.TenantCode)
e.Module = strings.TrimSpace(e.Module)
e.Action = strings.TrimSpace(e.Action)
e.ResourceID = strings.TrimSpace(e.ResourceID)
e.Title = strings.TrimSpace(e.Title)
e.Summary = strings.TrimSpace(e.Summary)
if e.OccurredAt.IsZero() {
e.OccurredAt = time.Now().UTC().Round(time.Millisecond)
}
e.TargetUserIDs = compactInt64IDs(e.TargetUserIDs)
}
func (e kafkaWorkbenchEvent) kafkaEnvelope(base kafkaEnvelope) kafkaEnvelope {
base.EventID = e.EventID
base.TraceID = firstNonEmpty(base.TraceID, e.TraceID)
base.TenantCode = firstNonEmpty(base.TenantCode, e.TenantCode)
base.TenantID = e.TenantID
base.EventType = e.Action
base.BizType = e.Module
base.BizID = e.ResourceID
base.OccurredAt = e.OccurredAt
return base
}
func (e kafkaWorkbenchEvent) realtimePayload() (map[string]any, error) {
payload := make(map[string]any, len(e.Data)+3)
for key, value := range e.Data {
if strings.TrimSpace(key) != "" {
payload[key] = value
}
}
payload["module"] = e.Module
payload["resource_id"] = e.ResourceID
payload["event_id"] = e.EventID
return payload, nil
}
func (e kafkaWorkbenchEvent) notificationAction() types.KVMap {
action := types.KVMap{
"module": e.Module,
"resource_id": e.ResourceID,
"event_id": e.EventID,
}
for key, value := range e.Data {
if strings.TrimSpace(key) != "" {
action[key] = value
}
}
return action
}
func compactInt64IDs(ids []int64) []int64 {
out := make([]int64, 0, len(ids))
seen := make(map[int64]bool, len(ids))
for _, id := range ids {
if id <= 0 || seen[id] {
continue
}
seen[id] = true
out = append(out, id)
}
return out
}
+1
View File
@@ -757,6 +757,7 @@ func main() {
startKafkaAdminCommandConsumer()
startKafkaConfigEventConsumer()
startKafkaCacheInvalidateConsumer()
startKafkaWorkbenchEventConsumer()
startKafkaMessagePersistConsumer()
startKafkaDeadLetterConsumer()