Files
lislgosms/gateway/internal/protocollog/publisher.go
T

129 lines
3.6 KiB
Go

package protocollog
import (
"context"
"encoding/json"
"fmt"
"hash/fnv"
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/redis/go-redis/v9"
)
const defaultStream = "gateway.protocol.logs"
type Event struct {
EventID string `json:"eventId"`
GatewayInstanceID string `json:"gatewayInstanceId,omitempty"`
ConnectionID string `json:"connectionId,omitempty"`
Protocol string `json:"protocol"`
Direction string `json:"direction"`
EventType string `json:"eventType"`
Status string `json:"status"`
TenantID string `json:"tenantId,omitempty"`
ApplicationID string `json:"applicationId,omitempty"`
ChannelID string `json:"channelId,omitempty"`
Account string `json:"account,omitempty"`
MessageID string `json:"messageId,omitempty"`
SubmitID string `json:"submitId,omitempty"`
GatewayMessageID string `json:"gatewayMessageId,omitempty"`
Phone string `json:"phone,omitempty"`
ResultCode string `json:"resultCode,omitempty"`
DurationMs int `json:"durationMs,omitempty"`
PayloadBytes int `json:"payloadBytes,omitempty"`
Detail map[string]any `json:"detail,omitempty"`
CreatedAt time.Time `json:"createdAt"`
}
type Publisher struct {
Redis *redis.Client
Stream string
GatewayInstanceID string
SuccessSampleRate int
MaxLen int64
published atomic.Int64
sampled atomic.Int64
errors atomic.Int64
}
func New(redisURL string) (*Publisher, error) {
if strings.TrimSpace(redisURL) == "" {
redisURL = "redis://127.0.0.1:6379"
}
options, err := redis.ParseURL(redisURL)
if err != nil {
return nil, err
}
return &Publisher{Redis: redis.NewClient(options)}, nil
}
func (p *Publisher) Publish(ctx context.Context, event Event) error {
if p == nil || p.Redis == nil {
return fmt.Errorf("protocol log Redis publisher is unavailable")
}
if event.CreatedAt.IsZero() {
event.CreatedAt = time.Now().UTC()
}
if event.GatewayInstanceID == "" {
event.GatewayInstanceID = p.GatewayInstanceID
}
if event.EventID == "" {
event.EventID = fmt.Sprintf("PL-%d-%d", event.CreatedAt.UnixNano(), p.published.Load()+p.sampled.Load()+1)
}
if !p.mustKeep(event) && !p.sample(event) {
p.sampled.Add(1)
return nil
}
data, err := json.Marshal(event)
if err != nil {
p.errors.Add(1)
return err
}
args := &redis.XAddArgs{Stream: p.stream(), Values: map[string]any{"data": string(data)}}
if p.MaxLen > 0 {
args.MaxLen = p.MaxLen
args.Approx = true
}
if err := p.Redis.XAdd(ctx, args).Err(); err != nil {
p.errors.Add(1)
return err
}
p.published.Add(1)
return nil
}
func (p *Publisher) mustKeep(event Event) bool {
if event.Status != "success" {
return true
}
code := strings.TrimSpace(event.ResultCode)
return code != "" && code != "0"
}
func (p *Publisher) sample(event Event) bool {
rate := p.SuccessSampleRate
if rate <= 0 {
rate = 10
}
if rate >= 100 {
return true
}
h := fnv.New32a()
_, _ = h.Write([]byte(event.ChannelID + "|" + event.MessageID + "|" + event.EventType + "|" + strconv.FormatInt(event.CreatedAt.UnixNano()/int64(time.Millisecond), 10)))
return int(h.Sum32()%100) < rate
}
func (p *Publisher) stream() string {
if strings.TrimSpace(p.Stream) != "" {
return p.Stream
}
return defaultStream
}
func (p *Publisher) StreamName() string { return p.stream() }
func (p *Publisher) Counts() (int64, int64, int64) {
return p.published.Load(), p.sampled.Load(), p.errors.Load()
}