129 lines
3.6 KiB
Go
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()
|
|
}
|