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 }