github.com/rudderlabs/rudder-go-kit@v0.30.0/stats/statsd_test.go (about)

     1  package stats_test
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"net"
     8  	"reflect"
     9  	"strconv"
    10  	"strings"
    11  	"sync"
    12  	"sync/atomic"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/stretchr/testify/require"
    17  
    18  	"github.com/rudderlabs/rudder-go-kit/config"
    19  	"github.com/rudderlabs/rudder-go-kit/logger"
    20  	"github.com/rudderlabs/rudder-go-kit/stats"
    21  	"github.com/rudderlabs/rudder-go-kit/stats/metric"
    22  	"github.com/rudderlabs/rudder-go-kit/testhelper"
    23  )
    24  
    25  func TestStatsdMeasurementInvalidOperations(t *testing.T) {
    26  	c := config.New()
    27  	l := logger.NewFactory(c)
    28  	m := metric.NewManager()
    29  	s := stats.NewStats(c, l, m)
    30  
    31  	t.Run("counter invalid operations", func(t *testing.T) {
    32  		require.Panics(t, func() {
    33  			s.NewStat("test", stats.CountType).Gauge(1)
    34  		})
    35  		require.Panics(t, func() {
    36  			s.NewStat("test", stats.CountType).Observe(1.2)
    37  		})
    38  		require.Panics(t, func() {
    39  			s.NewStat("test", stats.CountType).RecordDuration()
    40  		})
    41  		require.Panics(t, func() {
    42  			s.NewStat("test", stats.CountType).SendTiming(1)
    43  		})
    44  		require.Panics(t, func() {
    45  			s.NewStat("test", stats.CountType).Since(time.Now())
    46  		})
    47  	})
    48  
    49  	t.Run("gauge invalid operations", func(t *testing.T) {
    50  		require.Panics(t, func() {
    51  			s.NewStat("test", stats.GaugeType).Increment()
    52  		})
    53  		require.Panics(t, func() {
    54  			s.NewStat("test", stats.GaugeType).Count(1)
    55  		})
    56  		require.Panics(t, func() {
    57  			s.NewStat("test", stats.GaugeType).Observe(1.2)
    58  		})
    59  		require.Panics(t, func() {
    60  			s.NewStat("test", stats.GaugeType).RecordDuration()
    61  		})
    62  		require.Panics(t, func() {
    63  			s.NewStat("test", stats.GaugeType).SendTiming(1)
    64  		})
    65  		require.Panics(t, func() {
    66  			s.NewStat("test", stats.GaugeType).Since(time.Now())
    67  		})
    68  	})
    69  
    70  	t.Run("histogram invalid operations", func(t *testing.T) {
    71  		require.Panics(t, func() {
    72  			s.NewStat("test", stats.HistogramType).Increment()
    73  		})
    74  		require.Panics(t, func() {
    75  			s.NewStat("test", stats.HistogramType).Count(1)
    76  		})
    77  		require.Panics(t, func() {
    78  			s.NewStat("test", stats.HistogramType).Gauge(1)
    79  		})
    80  		require.Panics(t, func() {
    81  			s.NewStat("test", stats.HistogramType).RecordDuration()
    82  		})
    83  		require.Panics(t, func() {
    84  			s.NewStat("test", stats.HistogramType).SendTiming(1)
    85  		})
    86  		require.Panics(t, func() {
    87  			s.NewStat("test", stats.HistogramType).Since(time.Now())
    88  		})
    89  	})
    90  
    91  	t.Run("timer invalid operations", func(t *testing.T) {
    92  		require.Panics(t, func() {
    93  			s.NewStat("test", stats.TimerType).Increment()
    94  		})
    95  		require.Panics(t, func() {
    96  			s.NewStat("test", stats.TimerType).Count(1)
    97  		})
    98  		require.Panics(t, func() {
    99  			s.NewStat("test", stats.TimerType).Gauge(1)
   100  		})
   101  		require.Panics(t, func() {
   102  			s.NewStat("test", stats.TimerType).Observe(1.2)
   103  		})
   104  	})
   105  }
   106  
   107  func TestStatsdMeasurementOperations(t *testing.T) {
   108  	var lastReceived atomic.Value
   109  	server := newStatsdServer(t, func(s string) { lastReceived.Store(s) })
   110  	defer server.Close()
   111  
   112  	c := config.New()
   113  	c.Set("STATSD_SERVER_URL", server.addr)
   114  	c.Set("INSTANCE_ID", "test")
   115  	c.Set("RuntimeStats.enabled", false)
   116  	c.Set("statsSamplingRate", 0.5)
   117  
   118  	l := logger.NewFactory(c)
   119  	m := metric.NewManager()
   120  	s := stats.NewStats(c, l, m)
   121  
   122  	ctx, cancel := context.WithCancel(context.Background())
   123  	defer cancel()
   124  
   125  	// start stats
   126  	require.NoError(t, s.Start(ctx, stats.DefaultGoRoutineFactory))
   127  	defer s.Stop()
   128  
   129  	t.Run("counter increment", func(t *testing.T) {
   130  		s.NewStat("test-counter", stats.CountType).Increment()
   131  
   132  		require.Eventually(t, func() bool {
   133  			return lastReceived.Load() == "test-counter,instanceName=test:1|c"
   134  		}, 2*time.Second, time.Millisecond)
   135  	})
   136  
   137  	t.Run("counter count", func(t *testing.T) {
   138  		s.NewStat("test-counter", stats.CountType).Count(10)
   139  
   140  		require.Eventually(t, func() bool {
   141  			return lastReceived.Load() == "test-counter,instanceName=test:10|c"
   142  		}, 2*time.Second, time.Millisecond)
   143  	})
   144  
   145  	t.Run("gauge", func(t *testing.T) {
   146  		s.NewStat("test-gauge", stats.GaugeType).Gauge(1234)
   147  
   148  		require.Eventually(t, func() bool {
   149  			return lastReceived.Load() == "test-gauge,instanceName=test:1234|g"
   150  		}, 2*time.Second, time.Millisecond)
   151  	})
   152  
   153  	t.Run("timer send timing", func(t *testing.T) {
   154  		s.NewStat("test-timer-1", stats.TimerType).SendTiming(10 * time.Second)
   155  
   156  		require.Eventually(t, func() bool {
   157  			return lastReceived.Load() == "test-timer-1,instanceName=test:10000|ms"
   158  		}, 2*time.Second, time.Millisecond)
   159  	})
   160  
   161  	t.Run("timer since", func(t *testing.T) {
   162  		s.NewStat("test-timer-2", stats.TimerType).Since(time.Now())
   163  
   164  		require.Eventually(t, func() bool {
   165  			return lastReceived.Load() == "test-timer-2,instanceName=test:0|ms"
   166  		}, 2*time.Second, time.Millisecond)
   167  	})
   168  
   169  	t.Run("timer RecordDuration", func(t *testing.T) {
   170  		func() {
   171  			defer s.NewStat("test-timer-4", stats.TimerType).RecordDuration()()
   172  		}()
   173  
   174  		require.Eventually(t, func() bool {
   175  			return lastReceived.Load() == "test-timer-4,instanceName=test:0|ms"
   176  		}, 2*time.Second, time.Millisecond)
   177  	})
   178  
   179  	t.Run("histogram", func(t *testing.T) {
   180  		s.NewStat("test-hist-1", stats.HistogramType).Observe(1.2)
   181  		require.Eventually(t, func() bool {
   182  			return lastReceived.Load() == "test-hist-1,instanceName=test:1.2|h"
   183  		}, 2*time.Second, time.Millisecond)
   184  	})
   185  
   186  	t.Run("tagged stats", func(t *testing.T) {
   187  		s.NewTaggedStat("test-tagged", stats.CountType, stats.Tags{"key": "value"}).Increment()
   188  		require.Eventually(t, func() bool {
   189  			return lastReceived.Load() == "test-tagged,instanceName=test,key=value:1|c"
   190  		}, 2*time.Second, time.Millisecond)
   191  
   192  		// same measurement name, different measurement type
   193  		s.NewTaggedStat("test-tagged", stats.GaugeType, stats.Tags{"key": "value"}).Gauge(22)
   194  		require.Eventually(t, func() bool {
   195  			return lastReceived.Load() == "test-tagged,instanceName=test,key=value:22|g"
   196  		}, 2*time.Second, time.Millisecond)
   197  	})
   198  
   199  	t.Run("sampled stats", func(t *testing.T) {
   200  		lastReceived.Store("")
   201  		// use the same, non-sampled counter first to make sure we don't get it from cache when we request the sampled one
   202  		counter := s.NewTaggedStat("test-tagged-sampled", stats.CountType, stats.Tags{"key": "value"})
   203  		counter.Increment()
   204  
   205  		require.Eventually(t, func() bool {
   206  			return lastReceived.Load() == "test-tagged-sampled,instanceName=test,key=value:1|c"
   207  		}, 2*time.Second, time.Millisecond)
   208  
   209  		counterSampled := s.NewSampledTaggedStat("test-tagged-sampled", stats.CountType, stats.Tags{"key": "value"})
   210  		counterSampled.Increment()
   211  		require.Eventually(t, func() bool {
   212  			if lastReceived.Load() == "test-tagged-sampled,instanceName=test,key=value:1|c|@0.5" {
   213  				return true
   214  			}
   215  			// playing with probabilities, we might or might not get the sample (0.5 -> 50% chance)
   216  			counterSampled.Increment()
   217  			return false
   218  		}, 2*time.Second, time.Millisecond)
   219  	})
   220  
   221  	t.Run("measurement with empty name", func(t *testing.T) {
   222  		s.NewStat("", stats.CountType).Increment()
   223  
   224  		require.Eventually(t, func() bool {
   225  			return lastReceived.Load() == "novalue,instanceName=test:1|c"
   226  		}, 2*time.Second, time.Millisecond)
   227  	})
   228  
   229  	t.Run("measurement with empty name and empty tag key", func(t *testing.T) {
   230  		s.NewTaggedStat(" ", stats.GaugeType, stats.Tags{"key": "value", "": "value2"}).Gauge(22)
   231  
   232  		require.Eventually(t, func() bool {
   233  			return lastReceived.Load() == "novalue,instanceName=test,key=value:22|g"
   234  		}, 2*time.Second, time.Millisecond)
   235  	})
   236  }
   237  
   238  func TestStatsdPeriodicStats(t *testing.T) {
   239  	runTest := func(t *testing.T, prepareFunc func(c *config.Config, m metric.Manager), expected []string) {
   240  		var received []string
   241  		var receivedMu sync.RWMutex
   242  		server := newStatsdServer(t, func(s string) {
   243  			if i := strings.Index(s, ":"); i > 0 {
   244  				s = s[:i]
   245  			}
   246  			receivedMu.Lock()
   247  			received = append(received, s)
   248  			receivedMu.Unlock()
   249  		})
   250  		defer server.Close()
   251  
   252  		c := config.New()
   253  		m := metric.NewManager()
   254  		t.Setenv("KUBE_NAMESPACE", "my-namespace")
   255  		c.Set("STATSD_SERVER_URL", server.addr)
   256  		c.Set("INSTANCE_ID", "test")
   257  		c.Set("RuntimeStats.enabled", true)
   258  		c.Set("RuntimeStats.statsCollectionInterval", 60)
   259  		prepareFunc(c, m)
   260  
   261  		l := logger.NewFactory(c)
   262  		s := stats.NewStats(c, l, m)
   263  
   264  		ctx, cancel := context.WithCancel(context.Background())
   265  		defer cancel()
   266  
   267  		// start stats
   268  		require.NoError(t, s.Start(ctx, stats.DefaultGoRoutineFactory))
   269  		defer s.Stop()
   270  
   271  		require.Eventually(t, func() bool {
   272  			receivedMu.RLock()
   273  			defer receivedMu.RUnlock()
   274  
   275  			if len(received) != len(expected) {
   276  				return false
   277  			}
   278  			return reflect.DeepEqual(received, expected)
   279  		}, 10*time.Second, time.Millisecond)
   280  	}
   281  
   282  	t.Run("CPU stats", func(t *testing.T) {
   283  		runTest(t, func(c *config.Config, m metric.Manager) {
   284  			c.Set("RuntimeStats.enableCPUStats", true)
   285  			c.Set("RuntimeStats.enabledMemStats", false)
   286  			c.Set("RuntimeStats.enableGCStats", false)
   287  		}, []string{
   288  			"runtime_cpu.goroutines,instanceName=test,namespace=my-namespace",
   289  			"runtime_cpu.cgo_calls,instanceName=test,namespace=my-namespace",
   290  		})
   291  	})
   292  
   293  	t.Run("Mem stats", func(t *testing.T) {
   294  		runTest(t, func(c *config.Config, m metric.Manager) {
   295  			c.Set("RuntimeStats.enableCPUStats", false)
   296  			c.Set("RuntimeStats.enabledMemStats", true)
   297  			c.Set("RuntimeStats.enableGCStats", false)
   298  		}, []string{
   299  			"runtime_mem.alloc,instanceName=test,namespace=my-namespace",
   300  			"runtime_mem.total,instanceName=test,namespace=my-namespace",
   301  			"runtime_mem.sys,instanceName=test,namespace=my-namespace",
   302  			"runtime_mem.lookups,instanceName=test,namespace=my-namespace",
   303  			"runtime_mem.malloc,instanceName=test,namespace=my-namespace",
   304  			"runtime_mem.frees,instanceName=test,namespace=my-namespace",
   305  			"runtime_mem.heap.alloc,instanceName=test,namespace=my-namespace",
   306  			"runtime_mem.heap.sys,instanceName=test,namespace=my-namespace",
   307  			"runtime_mem.heap.idle,instanceName=test,namespace=my-namespace",
   308  			"runtime_mem.heap.inuse,instanceName=test,namespace=my-namespace",
   309  			"runtime_mem.heap.released,instanceName=test,namespace=my-namespace",
   310  			"runtime_mem.heap.objects,instanceName=test,namespace=my-namespace",
   311  			"runtime_mem.stack.inuse,instanceName=test,namespace=my-namespace",
   312  			"runtime_mem.stack.sys,instanceName=test,namespace=my-namespace",
   313  			"runtime_mem.stack.mspan_inuse,instanceName=test,namespace=my-namespace",
   314  			"runtime_mem.stack.mspan_sys,instanceName=test,namespace=my-namespace",
   315  			"runtime_mem.stack.mcache_inuse,instanceName=test,namespace=my-namespace",
   316  			"runtime_mem.stack.mcache_sys,instanceName=test,namespace=my-namespace",
   317  			"runtime_mem.othersys,instanceName=test,namespace=my-namespace",
   318  		})
   319  	})
   320  
   321  	t.Run("MemGC stats", func(t *testing.T) {
   322  		runTest(t, func(c *config.Config, m metric.Manager) {
   323  			c.Set("RuntimeStats.enableCPUStats", false)
   324  			c.Set("RuntimeStats.enabledMemStats", true)
   325  			c.Set("RuntimeStats.enableGCStats", true)
   326  		}, []string{
   327  			"runtime_mem.alloc,instanceName=test,namespace=my-namespace",
   328  			"runtime_mem.total,instanceName=test,namespace=my-namespace",
   329  			"runtime_mem.sys,instanceName=test,namespace=my-namespace",
   330  			"runtime_mem.lookups,instanceName=test,namespace=my-namespace",
   331  			"runtime_mem.malloc,instanceName=test,namespace=my-namespace",
   332  			"runtime_mem.frees,instanceName=test,namespace=my-namespace",
   333  			"runtime_mem.heap.alloc,instanceName=test,namespace=my-namespace",
   334  			"runtime_mem.heap.sys,instanceName=test,namespace=my-namespace",
   335  			"runtime_mem.heap.idle,instanceName=test,namespace=my-namespace",
   336  			"runtime_mem.heap.inuse,instanceName=test,namespace=my-namespace",
   337  			"runtime_mem.heap.released,instanceName=test,namespace=my-namespace",
   338  			"runtime_mem.heap.objects,instanceName=test,namespace=my-namespace",
   339  			"runtime_mem.stack.inuse,instanceName=test,namespace=my-namespace",
   340  			"runtime_mem.stack.sys,instanceName=test,namespace=my-namespace",
   341  			"runtime_mem.stack.mspan_inuse,instanceName=test,namespace=my-namespace",
   342  			"runtime_mem.stack.mspan_sys,instanceName=test,namespace=my-namespace",
   343  			"runtime_mem.stack.mcache_inuse,instanceName=test,namespace=my-namespace",
   344  			"runtime_mem.stack.mcache_sys,instanceName=test,namespace=my-namespace",
   345  			"runtime_mem.othersys,instanceName=test,namespace=my-namespace",
   346  			"runtime_mem.gc.sys,instanceName=test,namespace=my-namespace",
   347  			"runtime_mem.gc.next,instanceName=test,namespace=my-namespace",
   348  			"runtime_mem.gc.last,instanceName=test,namespace=my-namespace",
   349  			"runtime_mem.gc.pause_total,instanceName=test,namespace=my-namespace",
   350  			"runtime_mem.gc.pause,instanceName=test,namespace=my-namespace",
   351  			"runtime_mem.gc.count,instanceName=test,namespace=my-namespace",
   352  			"runtime_mem.gc.cpu_percent,instanceName=test,namespace=my-namespace",
   353  		})
   354  	})
   355  
   356  	t.Run("Pending events", func(t *testing.T) {
   357  		runTest(t, func(c *config.Config, m metric.Manager) {
   358  			c.Set("RuntimeStats.enableCPUStats", false)
   359  			c.Set("RuntimeStats.enabledMemStats", false)
   360  			c.Set("RuntimeStats.enableGCStats", false)
   361  			m.GetRegistry(metric.PublishedMetrics).MustGetGauge(TestMeasurement{tablePrefix: "table", workspace: "workspace", destType: "destType"}).Set(1.0)
   362  		}, []string{
   363  			"test_measurement_table,instanceName=test,namespace=my-namespace,destType=destType,workspaceId=workspace",
   364  		})
   365  	})
   366  }
   367  
   368  func TestStatsdExcludedTags(t *testing.T) {
   369  	var lastReceived atomic.Value
   370  	server := newStatsdServer(t, func(s string) { lastReceived.Store(s) })
   371  	defer server.Close()
   372  
   373  	c := config.New()
   374  	c.Set("STATSD_SERVER_URL", server.addr)
   375  	c.Set("statsExcludedTags", []string{"workspaceId"})
   376  	c.Set("INSTANCE_ID", "test")
   377  	c.Set("RuntimeStats.enabled", false)
   378  
   379  	l := logger.NewFactory(c)
   380  	m := metric.NewManager()
   381  	s := stats.NewStats(c, l, m)
   382  
   383  	ctx, cancel := context.WithCancel(context.Background())
   384  	defer cancel()
   385  
   386  	// start stats
   387  	require.NoError(t, s.Start(ctx, stats.DefaultGoRoutineFactory))
   388  	defer s.Stop()
   389  
   390  	c.Set("statsExcludedTags", []string{"workspaceId"})
   391  	s.NewTaggedStat("test-workspaceId", stats.CountType, stats.Tags{"workspaceId": "value"}).Increment()
   392  	require.Eventually(t, func() bool {
   393  		return lastReceived.Load() == "test-workspaceId,instanceName=test:1|c"
   394  	}, 2*time.Second, time.Millisecond)
   395  }
   396  
   397  type statsdServer struct {
   398  	t      *testing.T
   399  	addr   string
   400  	closer io.Closer
   401  	closed chan bool
   402  }
   403  
   404  func newStatsdServer(t *testing.T, f func(string)) *statsdServer {
   405  	port, err := testhelper.GetFreePort()
   406  	require.NoError(t, err)
   407  	addr := net.JoinHostPort("localhost", strconv.Itoa(port))
   408  	s := &statsdServer{t: t, closed: make(chan bool)}
   409  	laddr, err := net.ResolveUDPAddr("udp", addr)
   410  	require.NoError(t, err)
   411  	conn, err := net.ListenUDP("udp", laddr)
   412  	require.NoError(t, err)
   413  	s.closer = conn
   414  	s.addr = conn.LocalAddr().String()
   415  	go func() {
   416  		buf := make([]byte, 4096)
   417  		for {
   418  			n, err := conn.Read(buf)
   419  			if err != nil {
   420  				s.closed <- true
   421  				return
   422  			}
   423  			s := string(buf[:n])
   424  			lines := strings.Split(s, "\n")
   425  			if n > 0 {
   426  				for _, line := range lines {
   427  					f(line)
   428  				}
   429  			}
   430  		}
   431  	}()
   432  
   433  	return s
   434  }
   435  
   436  func (s *statsdServer) Close() {
   437  	require.NoError(s.t, s.closer.Close())
   438  	<-s.closed
   439  }
   440  
   441  type TestMeasurement struct {
   442  	tablePrefix string
   443  	workspace   string
   444  	destType    string
   445  }
   446  
   447  func (r TestMeasurement) GetName() string {
   448  	return fmt.Sprintf("test_measurement_%s", r.tablePrefix)
   449  }
   450  
   451  func (r TestMeasurement) GetTags() map[string]string {
   452  	return map[string]string{
   453  		"workspaceId": r.workspace,
   454  		"destType":    r.destType,
   455  	}
   456  }