feat: add kafka admin integration
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/segmentio/kafka-go"
|
||||
"github.com/tinode/chat/server/logs"
|
||||
"github.com/tinode/chat/server/store"
|
||||
"github.com/tinode/chat/server/store/types"
|
||||
)
|
||||
|
||||
type kafkaWorkbenchPayload struct {
|
||||
Module string `json:"module"`
|
||||
Action string `json:"action"`
|
||||
ResourceID string `json:"resource_id"`
|
||||
ActorUserID string `json:"actor_user_id"`
|
||||
TargetUserIDs []string `json:"target_user_ids,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
Data map[string]any `json:"data,omitempty"`
|
||||
OccurredAt time.Time `json:"occurred_at"`
|
||||
}
|
||||
|
||||
type kafkaAuditPayload struct {
|
||||
ActorType string `json:"actor_type"`
|
||||
ActorID string `json:"actor_id"`
|
||||
Action string `json:"action"`
|
||||
Resource string `json:"resource"`
|
||||
ResourceID string `json:"resource_id"`
|
||||
Result string `json:"result"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Data map[string]any `json:"data,omitempty"`
|
||||
OccurredAt time.Time `json:"occurred_at"`
|
||||
}
|
||||
|
||||
func startKafkaWorkbenchEventConsumer() {
|
||||
startKafkaConsumer("workbench_event", handleKafkaWorkbenchEvent)
|
||||
}
|
||||
|
||||
func enqueueKafkaWorkbenchEvent(ctx context.Context, tenantID types.TenantID, actor types.Uid, module, action, resourceID string,
|
||||
targets []types.Uid, title, summary string, data map[string]any) error {
|
||||
if !kafkaEnabled() {
|
||||
return nil
|
||||
}
|
||||
targetUserIDs := make([]string, 0, len(targets))
|
||||
for _, target := range targets {
|
||||
if !target.IsZero() {
|
||||
targetUserIDs = append(targetUserIDs, target.UserId())
|
||||
}
|
||||
}
|
||||
payload := kafkaWorkbenchPayload{
|
||||
Module: module,
|
||||
Action: action,
|
||||
ResourceID: resourceID,
|
||||
ActorUserID: actor.UserId(),
|
||||
TargetUserIDs: targetUserIDs,
|
||||
Title: title,
|
||||
Summary: summary,
|
||||
Data: data,
|
||||
OccurredAt: time.Now().UTC().Round(time.Millisecond),
|
||||
}
|
||||
return enqueueKafkaTypedEvent(ctx, tenantID, "workbench_event", "workbench."+action, "workbench", resourceID, actor, payload)
|
||||
}
|
||||
|
||||
func enqueueKafkaAuditEvent(ctx context.Context, tenantID types.TenantID, actorType string, actor types.Uid,
|
||||
action, resource, resourceID, result, reason string, data map[string]any) error {
|
||||
if !kafkaEnabled() {
|
||||
return nil
|
||||
}
|
||||
actorID := ""
|
||||
if !actor.IsZero() {
|
||||
actorID = actor.UserId()
|
||||
}
|
||||
payload := kafkaAuditPayload{
|
||||
ActorType: actorType,
|
||||
ActorID: actorID,
|
||||
Action: action,
|
||||
Resource: resource,
|
||||
ResourceID: resourceID,
|
||||
Result: result,
|
||||
Reason: reason,
|
||||
Data: data,
|
||||
OccurredAt: time.Now().UTC().Round(time.Millisecond),
|
||||
}
|
||||
return enqueueKafkaTypedEvent(ctx, tenantID, "audit_event", "audit."+action, "audit", resource+":"+resourceID, actor, payload)
|
||||
}
|
||||
|
||||
func enqueueKafkaTypedEvent(_ context.Context, tenantID types.TenantID, topicName, eventType, bizType, bizID string, actor types.Uid, payload any) error {
|
||||
tenantCode := ""
|
||||
if store.Tenants != nil {
|
||||
if tenant, err := store.Tenants.GetByID(tenantID); err == nil && tenant != nil {
|
||||
tenantCode = tenant.Code
|
||||
} else {
|
||||
logs.Warn.Printf("kafka event: failed to resolve tenant code for tenant %d: %v", tenantID, err)
|
||||
}
|
||||
}
|
||||
encoded, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
eventID := stableIMEventID(topicName, tenantID, eventType, bizID, encoded)
|
||||
envelope := kafkaEnvelope{
|
||||
EventID: eventID,
|
||||
TenantCode: tenantCode,
|
||||
SourceService: "im-server",
|
||||
TargetService: "im-server",
|
||||
EventType: eventType,
|
||||
BizType: bizType,
|
||||
BizID: bizID,
|
||||
OccurredAt: time.Now().UTC(),
|
||||
Payload: encoded,
|
||||
}
|
||||
if !actor.IsZero() {
|
||||
envelope.Actor = &kafkaActor{Type: "user", ID: actor.UserId()}
|
||||
}
|
||||
return enqueueKafkaOutbox(tenantID, topicName, bizID, envelope)
|
||||
}
|
||||
|
||||
func handleKafkaWorkbenchEvent(ctx context.Context, envelope kafkaEnvelope, _ kafka.Message) error {
|
||||
tenantID, err := kafkaTenantID(envelope.TenantCode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
consume, claimed, err := beginKafkaConsumeForTopic(tenantID, envelope, "workbench_event")
|
||||
if err != nil || !claimed {
|
||||
return err
|
||||
}
|
||||
var payload kafkaWorkbenchPayload
|
||||
if err = json.Unmarshal(envelope.Payload, &payload); err != nil {
|
||||
return markKafkaDeadLetter(ctx, tenantID, envelope, consume, "MALFORMED_PAYLOAD", err.Error())
|
||||
}
|
||||
if err = executeKafkaWorkbenchEvent(tenantID, envelope, payload); err != nil {
|
||||
failed, markErr := store.HTTPInfrastructure.MarkKafkaConsumeFailed(tenantID, envelope.EventID, kafkaBus.config.ConsumerGroup,
|
||||
kafkaCommandErrorReason(err), err.Error(), time.Now().UTC().Round(time.Millisecond))
|
||||
if markErr != nil {
|
||||
return markErr
|
||||
}
|
||||
if failed != nil && failed.RetryCount >= kafkaBus.config.MaxRetryCount {
|
||||
return markKafkaDeadLetter(ctx, tenantID, envelope, failed, kafkaCommandErrorReason(err), err.Error())
|
||||
}
|
||||
sleepKafkaRetryBackoff(failed.RetryCount)
|
||||
return err
|
||||
}
|
||||
return store.HTTPInfrastructure.MarkKafkaConsumeSucceeded(tenantID, envelope.EventID, kafkaBus.config.ConsumerGroup, time.Now().UTC().Round(time.Millisecond))
|
||||
}
|
||||
|
||||
func executeKafkaWorkbenchEvent(tenantID types.TenantID, envelope kafkaEnvelope, payload kafkaWorkbenchPayload) error {
|
||||
actor := types.ParseUserId(payload.ActorUserID)
|
||||
if actor.IsZero() {
|
||||
actor = types.ParseUid(payload.ActorUserID)
|
||||
}
|
||||
for _, raw := range payload.TargetUserIDs {
|
||||
target := types.ParseUserId(raw)
|
||||
if target.IsZero() {
|
||||
continue
|
||||
}
|
||||
eventID := stableIMEventID("workbench", envelope.EventID, target.UserId())
|
||||
if target != actor && payload.Title != "" {
|
||||
if err := createIMSystemNotification(tenantID, target, stableIMEventID(eventID, "notification"), payload.Action,
|
||||
payload.Title, payload.Summary, types.KVMap{"module": payload.Module, "resource_id": payload.ResourceID}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := enqueueIMBusinessEvent(tenantID, eventID, "user", target.UserId(), actor, payload.Action, payload.Data); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user