snapshot of https://github.com/centrifugal/centrifugo.git (master, 2026-09-16; blobs > 131072B omitted)
This commit is contained in:
41
internal/usage/metric.go
Normal file
41
internal/usage/metric.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package usage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// metric represents stats point.
|
||||
type metric struct {
|
||||
Id string `json:"id"`
|
||||
OrgId int `json:"org_id"`
|
||||
Name string `json:"name"`
|
||||
Metric string `json:"metric"`
|
||||
Interval int `json:"interval"`
|
||||
Value float64 `json:"value"`
|
||||
Unit string `json:"unit"`
|
||||
Time int64 `json:"time"`
|
||||
Type string `json:"mtype"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
func (m *metric) SetId() {
|
||||
sort.Strings(m.Tags)
|
||||
|
||||
buffer := bytes.NewBufferString(m.Metric)
|
||||
buffer.WriteByte(0)
|
||||
buffer.WriteString(m.Unit)
|
||||
buffer.WriteByte(0)
|
||||
buffer.WriteString(m.Type)
|
||||
buffer.WriteByte(0)
|
||||
_, _ = fmt.Fprintf(buffer, "%d", m.Interval)
|
||||
|
||||
for _, k := range m.Tags {
|
||||
buffer.WriteByte(0)
|
||||
buffer.WriteString(k)
|
||||
}
|
||||
hash := sha256.Sum256(buffer.Bytes())
|
||||
m.Id = fmt.Sprintf("%d.%x", m.OrgId, hash[:16])
|
||||
}
|
||||
615
internal/usage/usage.go
Normal file
615
internal/usage/usage.go
Normal file
@@ -0,0 +1,615 @@
|
||||
package usage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/build"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/config"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/configtypes"
|
||||
"github.com/centrifugal/centrifugo/v6/internal/consuming"
|
||||
|
||||
"github.com/centrifugal/centrifuge"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var statsRand *rand.Rand
|
||||
|
||||
var initialDelay time.Duration
|
||||
var tickInterval time.Duration
|
||||
var sendInterval time.Duration
|
||||
var metricsPrefix string
|
||||
|
||||
func init() {
|
||||
statsRand = rand.New(rand.NewSource(time.Now().Unix())) //nolint:gosec // Not used for security-sensitive purposes.
|
||||
|
||||
// Initial delay in between 24-48h. Using minute resolution here
|
||||
// is intentional to get a better time spread.
|
||||
initialDelay = time.Duration(statsRand.Intn(24*60)+24*60) * time.Minute
|
||||
tickInterval = time.Hour
|
||||
sendInterval = 24 * time.Hour
|
||||
metricsPrefix = "centrifugo."
|
||||
|
||||
// Uncomment during development (for faster timings and test prefix).
|
||||
//initialDelay = time.Duration(statsRand.Intn(30)+1) * time.Second
|
||||
//tickInterval = 10 * time.Second
|
||||
//sendInterval = time.Minute
|
||||
//metricsPrefix = "test."
|
||||
}
|
||||
|
||||
// Sender can send anonymous usage stats. Centrifugo does not collect any sensitive info.
|
||||
// Only impersonal counters to estimate installation size distribution and feature use.
|
||||
type Sender struct {
|
||||
mu sync.RWMutex
|
||||
node *centrifuge.Node
|
||||
rules *config.Container
|
||||
features Features
|
||||
maxNumNodes int
|
||||
maxNumClients int
|
||||
maxNumChannels int
|
||||
lastSentAt int64
|
||||
}
|
||||
|
||||
// Features is a helper struct to build metrics.
|
||||
type Features struct {
|
||||
// Build info.
|
||||
Version string
|
||||
Edition string
|
||||
|
||||
// Engine or broker/presence manager usage.
|
||||
EngineEnabled bool
|
||||
EngineType string
|
||||
BrokerEnabled bool
|
||||
BrokerType string
|
||||
PresenceManagerEnabled bool
|
||||
PresenceManagerType string
|
||||
|
||||
// Transports.
|
||||
Websocket bool
|
||||
HTTPStream bool
|
||||
SSE bool
|
||||
UniWebsocket bool
|
||||
UniGRPC bool
|
||||
UniSSE bool
|
||||
UniHTTPStream bool
|
||||
WebTransport bool
|
||||
|
||||
// Proxies.
|
||||
ConnectProxy bool
|
||||
RefreshProxy bool
|
||||
SubscribeProxy bool
|
||||
PublishProxy bool
|
||||
RPCProxy bool
|
||||
SubRefreshProxy bool
|
||||
SubscribeStreamProxy bool
|
||||
|
||||
EnabledConsumers []string
|
||||
|
||||
// Uses GRPC server API.
|
||||
GrpcAPI bool
|
||||
// Admin interface enabled.
|
||||
Admin bool
|
||||
// Uses automatic personal channel subscribe.
|
||||
SubscribeToPersonal bool
|
||||
// Uses Swagger
|
||||
Swagger bool
|
||||
}
|
||||
|
||||
// NewSender creates usage stats sender.
|
||||
func NewSender(node *centrifuge.Node, rules *config.Container, features Features) *Sender {
|
||||
return &Sender{
|
||||
node: node,
|
||||
rules: rules,
|
||||
features: features,
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
// LastSentUpdateNotificationOp is an op for Centrifuge Notification in which we
|
||||
// send last sent time to all nodes.
|
||||
LastSentUpdateNotificationOp = "usage_stats.last_sent_at"
|
||||
)
|
||||
|
||||
func (s *Sender) isDev() bool {
|
||||
return s.features.Version == "0.0.0"
|
||||
}
|
||||
|
||||
// Run usage stats sender. How it works:
|
||||
// First send in between 24-48h from node start.
|
||||
// After the initial delay has passed: every hour check last time stats were sent by all
|
||||
// the nodes in a Centrifugo cluster. If no points were sent in last 24h, then push metrics
|
||||
// and update push time on all nodes (broadcast current time). There is still a chance of
|
||||
// duplicate data sending – but should be rare and tolerable for the purpose.
|
||||
func (s *Sender) Run(ctx context.Context) error {
|
||||
firstTimeSend := time.Now().Add(initialDelay)
|
||||
if s.isDev() {
|
||||
log.Debug().Str("delay", initialDelay.String()).Msg("usage stats: schedule next send")
|
||||
}
|
||||
|
||||
// Wait 1/4 of a delay to randomize hourly ticks on different nodes.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(initialDelay / 4):
|
||||
}
|
||||
|
||||
if s.isDev() {
|
||||
log.Debug().Msg("usage stats: start periodic ticks")
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(tickInterval):
|
||||
if s.isDev() {
|
||||
log.Debug().Msg("usage stats: tick")
|
||||
}
|
||||
err := s.updateMaxValues()
|
||||
if err != nil {
|
||||
if s.isDev() {
|
||||
log.Error().Err(err).Msg("usage stats: error updating max values")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if time.Now().Before(firstTimeSend) {
|
||||
if s.isDev() {
|
||||
log.Debug().Msg("usage stats: too early to send first time")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
lastSentAt := s.lastSentAt
|
||||
s.mu.RUnlock()
|
||||
if lastSentAt > 0 {
|
||||
s.broadcastLastSentAt()
|
||||
}
|
||||
|
||||
if lastSentAt > 0 && time.Now().Unix() <= lastSentAt+int64(sendInterval.Seconds()) {
|
||||
if s.isDev() {
|
||||
log.Debug().Msg("usage stats: too early to send")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if s.isDev() {
|
||||
log.Debug().Msg("usage stats: sending usage stats")
|
||||
}
|
||||
metrics, err := s.prepareMetrics()
|
||||
if err != nil {
|
||||
if s.isDev() {
|
||||
log.Error().Err(err).Msg("usage stats: error preparing metrics")
|
||||
}
|
||||
continue
|
||||
}
|
||||
err = s.sendUsageStats(metrics, build.UsageStatsEndpoint, build.UsageStatsToken)
|
||||
if err != nil {
|
||||
if s.isDev() {
|
||||
log.Error().Err(err).Msg("usage stats: error sending")
|
||||
}
|
||||
continue
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.lastSentAt = time.Now().Unix()
|
||||
s.resetMaxValues()
|
||||
s.mu.Unlock()
|
||||
s.broadcastLastSentAt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type lastSentAtEnvelope struct {
|
||||
LastSentAt int64 `json:"lastSentAt"`
|
||||
}
|
||||
|
||||
func (s *Sender) broadcastLastSentAt() {
|
||||
s.mu.RLock()
|
||||
envelope := lastSentAtEnvelope{
|
||||
LastSentAt: s.lastSentAt,
|
||||
}
|
||||
data, _ := json.Marshal(envelope)
|
||||
s.mu.RUnlock()
|
||||
if err := s.node.Notify(LastSentUpdateNotificationOp, data, ""); err != nil {
|
||||
// Issue a single retry.
|
||||
if err = s.node.Notify(LastSentUpdateNotificationOp, data, ""); err != nil {
|
||||
log.Error().Err(err).Msg("usage stats: error broadcasting stats last sent at value")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateLastSentAt sets the lastSentAt received from other node only
|
||||
// if received value greater than local one (so that we can avoid sending
|
||||
// duplicated stats).
|
||||
func (s *Sender) UpdateLastSentAt(data []byte) {
|
||||
var envelope lastSentAtEnvelope
|
||||
err := json.Unmarshal(data, &envelope)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("usage stats: error decoding lastSentAtEnvelope")
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if envelope.LastSentAt > s.lastSentAt {
|
||||
if s.isDev() {
|
||||
log.Debug().Msg("usage stats: updating last sent to value from another node")
|
||||
}
|
||||
s.lastSentAt = envelope.LastSentAt
|
||||
s.resetMaxValues()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Sender) updateMaxValues() error {
|
||||
info, err := s.node.Info()
|
||||
if err != nil {
|
||||
return fmt.Errorf("usage stats: error getting info: %w", err)
|
||||
}
|
||||
|
||||
numNodes := len(info.Nodes)
|
||||
|
||||
numClients := 0
|
||||
for _, node := range info.Nodes {
|
||||
numClients += int(node.NumClients)
|
||||
}
|
||||
|
||||
numChannels := 0
|
||||
for _, node := range info.Nodes {
|
||||
numChannels += int(node.NumChannels)
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if numNodes > s.maxNumNodes {
|
||||
s.maxNumNodes = numNodes
|
||||
}
|
||||
|
||||
if numClients > s.maxNumClients {
|
||||
s.maxNumClients = numClients
|
||||
}
|
||||
|
||||
if numChannels > s.maxNumChannels {
|
||||
s.maxNumChannels = numChannels
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getHistogramMetric(val int, bounds []int, metricPrefix string) string {
|
||||
for _, bound := range bounds {
|
||||
if val <= bound {
|
||||
boundStr := strconv.Itoa(bound)
|
||||
if strings.HasSuffix(boundStr, "000000") {
|
||||
boundStr = strings.TrimSuffix(boundStr, "000000")
|
||||
boundStr += "m"
|
||||
} else if strings.HasSuffix(boundStr, "000") {
|
||||
boundStr = strings.TrimSuffix(boundStr, "000")
|
||||
boundStr += "k"
|
||||
}
|
||||
return metricPrefix + "le_" + boundStr
|
||||
}
|
||||
}
|
||||
return metricPrefix + "le_inf"
|
||||
}
|
||||
|
||||
// Lock must be held outside.
|
||||
func (s *Sender) resetMaxValues() {
|
||||
s.maxNumNodes = 0
|
||||
s.maxNumClients = 0
|
||||
s.maxNumChannels = 0
|
||||
}
|
||||
|
||||
func (s *Sender) prepareMetrics() ([]*metric, error) {
|
||||
now := time.Now().Unix()
|
||||
|
||||
createPoint := func(name string) *metric {
|
||||
finalName := metricsPrefix + "stats." + name
|
||||
md := metric{
|
||||
Name: finalName,
|
||||
Metric: finalName,
|
||||
Interval: int(sendInterval.Seconds()),
|
||||
Value: 1,
|
||||
Time: now,
|
||||
Type: "count",
|
||||
}
|
||||
md.SetId()
|
||||
return &md
|
||||
}
|
||||
|
||||
version := strings.ReplaceAll(s.features.Version, ".", "_")
|
||||
edition := strings.ToLower(s.features.Edition)
|
||||
|
||||
var metrics []*metric
|
||||
|
||||
metrics = append(metrics, createPoint("total"))
|
||||
metrics = append(metrics, createPoint("version."+version+".edition."+edition))
|
||||
metrics = append(metrics, createPoint("arch."+runtime.GOARCH+".os."+runtime.GOOS))
|
||||
|
||||
if s.features.EngineEnabled {
|
||||
metrics = append(metrics, createPoint("engine_type."+s.features.EngineType))
|
||||
}
|
||||
if s.features.BrokerEnabled {
|
||||
metrics = append(metrics, createPoint("broker_type."+s.features.BrokerType))
|
||||
}
|
||||
if s.features.PresenceManagerEnabled {
|
||||
metrics = append(metrics, createPoint("presence_manager_type."+s.features.PresenceManagerType))
|
||||
}
|
||||
|
||||
if s.features.Websocket {
|
||||
metrics = append(metrics, createPoint("transports_enabled.websocket"))
|
||||
}
|
||||
if s.features.HTTPStream {
|
||||
metrics = append(metrics, createPoint("transports_enabled.http_stream"))
|
||||
}
|
||||
if s.features.SSE {
|
||||
metrics = append(metrics, createPoint("transports_enabled.sse"))
|
||||
}
|
||||
if s.features.UniWebsocket {
|
||||
metrics = append(metrics, createPoint("transports_enabled.uni_websocket"))
|
||||
}
|
||||
if s.features.UniHTTPStream {
|
||||
metrics = append(metrics, createPoint("transports_enabled.uni_http_stream"))
|
||||
}
|
||||
if s.features.UniSSE {
|
||||
metrics = append(metrics, createPoint("transports_enabled.uni_sse"))
|
||||
}
|
||||
if s.features.UniGRPC {
|
||||
metrics = append(metrics, createPoint("transports_enabled.uni_grpc"))
|
||||
}
|
||||
if s.features.WebTransport {
|
||||
metrics = append(metrics, createPoint("transports_enabled.webtransport"))
|
||||
}
|
||||
if s.features.ConnectProxy {
|
||||
metrics = append(metrics, createPoint("proxies_enabled.connect"))
|
||||
}
|
||||
if s.features.RefreshProxy {
|
||||
metrics = append(metrics, createPoint("proxies_enabled.refresh"))
|
||||
}
|
||||
if s.features.SubscribeProxy {
|
||||
metrics = append(metrics, createPoint("proxies_enabled.subscribe"))
|
||||
}
|
||||
if s.features.PublishProxy {
|
||||
metrics = append(metrics, createPoint("proxies_enabled.publish"))
|
||||
}
|
||||
if s.features.RPCProxy {
|
||||
metrics = append(metrics, createPoint("proxies_enabled.rpc"))
|
||||
}
|
||||
if s.features.SubRefreshProxy {
|
||||
metrics = append(metrics, createPoint("proxies_enabled.sub_refresh"))
|
||||
}
|
||||
if s.features.SubscribeStreamProxy {
|
||||
metrics = append(metrics, createPoint("proxies_enabled.subscribe_stream"))
|
||||
}
|
||||
if len(s.features.EnabledConsumers) > 0 {
|
||||
metrics = append(metrics, createPoint("features_enabled.consumers"))
|
||||
}
|
||||
for _, consumerType := range s.features.EnabledConsumers {
|
||||
metrics = append(metrics, createPoint("consumers_enabled."+consumerType))
|
||||
}
|
||||
if s.features.GrpcAPI {
|
||||
metrics = append(metrics, createPoint("features_enabled.grpc_api"))
|
||||
}
|
||||
if s.features.SubscribeToPersonal {
|
||||
metrics = append(metrics, createPoint("features_enabled.user_subscribe_to_personal"))
|
||||
}
|
||||
if s.features.Admin {
|
||||
metrics = append(metrics, createPoint("features_enabled.admin_ui"))
|
||||
}
|
||||
if s.features.Swagger {
|
||||
metrics = append(metrics, createPoint("features_enabled.swagger"))
|
||||
}
|
||||
|
||||
var usesHistory bool
|
||||
var usesPresence bool
|
||||
var usesJoinLeave bool
|
||||
|
||||
namespaces := s.rules.Config().Channel.Namespaces
|
||||
chOpts := s.rules.Config().Channel.WithoutNamespace
|
||||
if chOpts.HistoryTTL > 0 && chOpts.HistorySize > 0 {
|
||||
usesHistory = true
|
||||
}
|
||||
if chOpts.Presence {
|
||||
usesPresence = true
|
||||
}
|
||||
if chOpts.JoinLeave {
|
||||
usesJoinLeave = true
|
||||
}
|
||||
for _, ns := range namespaces {
|
||||
chOpts = ns.ChannelOptions
|
||||
if chOpts.HistoryTTL > 0 && chOpts.HistorySize > 0 {
|
||||
usesHistory = true
|
||||
}
|
||||
if chOpts.Presence {
|
||||
usesPresence = true
|
||||
}
|
||||
if chOpts.JoinLeave {
|
||||
usesJoinLeave = true
|
||||
}
|
||||
}
|
||||
|
||||
if usesHistory {
|
||||
metrics = append(metrics, createPoint("features_enabled.history"))
|
||||
}
|
||||
if usesPresence {
|
||||
metrics = append(metrics, createPoint("features_enabled.presence"))
|
||||
}
|
||||
if usesJoinLeave {
|
||||
metrics = append(metrics, createPoint("features_enabled.join_leave"))
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
if s.maxNumNodes == 0 {
|
||||
s.mu.RUnlock()
|
||||
return nil, errors.New("no nodes found, skip sending")
|
||||
}
|
||||
|
||||
numNodesMetric := getHistogramMetric(
|
||||
s.maxNumNodes,
|
||||
[]int{1, 2, 3, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000},
|
||||
"num_nodes.",
|
||||
)
|
||||
|
||||
numClientsMetric := getHistogramMetric(
|
||||
s.maxNumClients,
|
||||
[]int{
|
||||
0, 5, 10, 100, 1000, 10000, 50000, 100000,
|
||||
500000, 1000000, 2000000, 3000000, 4000000,
|
||||
5000000, 10000000, 50000000, 100000000,
|
||||
},
|
||||
"num_clients.",
|
||||
)
|
||||
|
||||
numChannelsMetric := getHistogramMetric(
|
||||
s.maxNumChannels,
|
||||
[]int{
|
||||
0, 5, 10, 100, 1000, 10000, 50000, 100000,
|
||||
500000, 1000000, 2000000, 3000000, 4000000,
|
||||
5000000, 10000000, 50000000, 100000000,
|
||||
},
|
||||
"num_channels.",
|
||||
)
|
||||
|
||||
clientsPerNode := s.maxNumClients / s.maxNumNodes
|
||||
if clientsPerNode >= 1000 {
|
||||
// Insights about how many client connections per node users have.
|
||||
// We are not interested in too low numbers here.
|
||||
numClientsPerNodeMetric := getHistogramMetric(
|
||||
clientsPerNode,
|
||||
[]int{
|
||||
1000, 5000, 10000, 20000, 50000,
|
||||
100000, 200000, 500000, 1000000, 5000000,
|
||||
10000000,
|
||||
},
|
||||
"num_clients_per_node.",
|
||||
)
|
||||
metrics = append(metrics, createPoint(numClientsPerNodeMetric))
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
|
||||
metrics = append(metrics, createPoint(numNodesMetric))
|
||||
metrics = append(metrics, createPoint(numClientsMetric))
|
||||
metrics = append(metrics, createPoint(numChannelsMetric))
|
||||
metrics = append(metrics, createPoint("by_edition."+edition+"."+numNodesMetric))
|
||||
metrics = append(metrics, createPoint("by_edition."+edition+"."+numClientsMetric))
|
||||
metrics = append(metrics, createPoint("by_edition."+edition+"."+numChannelsMetric))
|
||||
|
||||
numNamespaces := s.rules.NumNamespaces()
|
||||
numNamespacesMetric := getHistogramMetric(
|
||||
numNamespaces,
|
||||
[]int{0, 1, 2, 5, 10, 50, 100, 500, 1000},
|
||||
"num_namespaces.",
|
||||
)
|
||||
metrics = append(metrics, createPoint(numNamespacesMetric))
|
||||
|
||||
numRpcNamespaces := s.rules.NumRpcNamespaces()
|
||||
numRpcNamespacesMetric := getHistogramMetric(
|
||||
numRpcNamespaces,
|
||||
[]int{0, 1, 2, 5, 10, 50, 100, 500, 1000},
|
||||
"num_rpc_namespaces.",
|
||||
)
|
||||
metrics = append(metrics, createPoint(numRpcNamespacesMetric))
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
func (s *Sender) sendUsageStats(metrics []*metric, statsEndpoint, statsToken string) error {
|
||||
data, err := json.Marshal(metrics)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s.isDev() {
|
||||
log.Debug().Str("payload", string(data)).Msg("usage stats: sending usage stats")
|
||||
} else {
|
||||
log.Trace().Str("payload", string(data)).Msg("usage stats: sending usage stats")
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
if statsEndpoint == "" {
|
||||
if s.isDev() {
|
||||
log.Debug().Msg("usage stats: skip sending due to empty endpoint")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if statsToken == "" {
|
||||
if s.isDev() {
|
||||
log.Debug().Msg("usage stats: skip sending due to empty token")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
endpoints := strings.Split(statsEndpoint, ",")
|
||||
|
||||
var success bool
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
if s.isDev() {
|
||||
log.Debug().Err(err).Msg("usage stats: can't create send request")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", "Bearer "+statsToken)
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
if s.isDev() {
|
||||
log.Debug().Err(err).Msg("usage stats: error sending request")
|
||||
}
|
||||
continue
|
||||
}
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if s.isDev() {
|
||||
log.Debug().Int("status", resp.StatusCode).Msg("usage stats: unexpected response status code")
|
||||
}
|
||||
continue
|
||||
}
|
||||
// If at least one of the endpoints received data it means success for us.
|
||||
success = true
|
||||
}
|
||||
|
||||
if !success {
|
||||
return errors.New("all endpoints failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetEnabledConsumers(consumers []consuming.ConsumerConfig) []string {
|
||||
var enabledConsumers []string
|
||||
for _, c := range consumers {
|
||||
consumerType := c.Type
|
||||
if c.Type == configtypes.ConsumerTypeKafka && c.Kafka.PublicationDataMode.Enabled {
|
||||
consumerType += "_publication_data"
|
||||
}
|
||||
if c.Enabled && !slices.Contains(enabledConsumers, consumerType) {
|
||||
enabledConsumers = append(enabledConsumers, consumerType)
|
||||
}
|
||||
}
|
||||
return enabledConsumers
|
||||
}
|
||||
82
internal/usage/usage_test.go
Normal file
82
internal/usage/usage_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package usage
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/centrifugal/centrifugo/v6/internal/config"
|
||||
|
||||
"github.com/centrifugal/centrifuge"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_getHistogramMetric(t *testing.T) {
|
||||
type args struct {
|
||||
val int
|
||||
bounds []int
|
||||
metricPrefix string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"1",
|
||||
args{0, []int{0, 10, 100, 1000, 10000, 100000}, "users."},
|
||||
"users.le_0",
|
||||
},
|
||||
{
|
||||
"2",
|
||||
args{10, []int{0, 10, 100, 1000, 10000, 100000}, "users."},
|
||||
"users.le_10",
|
||||
},
|
||||
{
|
||||
"3",
|
||||
args{30000, []int{0, 10, 100, 1000, 10000, 100000}, "users."},
|
||||
"users.le_100k",
|
||||
},
|
||||
{
|
||||
"4",
|
||||
args{3000000, []int{0, 10, 100, 1000, 10000, 100000, 10000000}, "users."},
|
||||
"users.le_10m",
|
||||
},
|
||||
{
|
||||
"5",
|
||||
args{300000, []int{0, 10, 100, 1000, 10000, 100000}, "users."},
|
||||
"users.le_inf",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := getHistogramMetric(tt.args.val, tt.args.bounds, tt.args.metricPrefix); got != tt.want {
|
||||
t.Errorf("getHistogramMetric() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func nodeWithMemoryEngine(t *testing.T) *centrifuge.Node {
|
||||
n, err := centrifuge.New(centrifuge.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = n.Run()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func TestPrepareMetrics(t *testing.T) {
|
||||
node := nodeWithMemoryEngine(t)
|
||||
cfg := config.DefaultConfig()
|
||||
cfgContainer, err := config.NewContainer(cfg)
|
||||
require.NoError(t, err)
|
||||
sender := NewSender(node, cfgContainer, Features{})
|
||||
err = sender.updateMaxValues()
|
||||
require.NoError(t, err)
|
||||
metrics, err := sender.prepareMetrics()
|
||||
require.NoError(t, err)
|
||||
require.True(t, len(metrics) > 0)
|
||||
}
|
||||
Reference in New Issue
Block a user