github.com/status-im/status-go@v1.1.0/telemetry/client_test.go (about) 1 package telemetry 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "net/http" 8 "net/http/httptest" 9 "os" 10 "slices" 11 "sync" 12 "testing" 13 "time" 14 15 "go.uber.org/zap" 16 "google.golang.org/protobuf/proto" 17 18 "github.com/waku-org/go-waku/waku/v2/api/publish" 19 v2protocol "github.com/waku-org/go-waku/waku/v2/protocol" 20 "github.com/waku-org/go-waku/waku/v2/protocol/pb" 21 22 "github.com/stretchr/testify/require" 23 24 "github.com/status-im/status-go/eth-node/types" 25 "github.com/status-im/status-go/protocol/transport" 26 "github.com/status-im/status-go/protocol/tt" 27 v1protocol "github.com/status-im/status-go/protocol/v1" 28 "github.com/status-im/status-go/wakuv2" 29 ) 30 31 var ( 32 testContentTopic = "/waku/1/0x12345679/rfc26" 33 ) 34 35 func createMockServer(t *testing.T, wg *sync.WaitGroup, expectedType TelemetryType, expectedCondition func(received []TelemetryRequest) (shouldSucceed bool, shouldFail bool)) *httptest.Server { 36 return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 37 if r.Method != "POST" { 38 t.Errorf("Expected 'POST' request, got '%s'", r.Method) 39 } 40 if r.URL.EscapedPath() != "/record-metrics" { 41 t.Errorf("Expected request to '/record-metrics', got '%s'", r.URL.EscapedPath()) 42 } 43 44 // Check the request body is as expected 45 var received []TelemetryRequest 46 err := json.NewDecoder(r.Body).Decode(&received) 47 if err != nil { 48 t.Fatal(err) 49 } 50 51 if expectedCondition != nil { 52 shouldSucceed, shouldFail := expectedCondition(received) 53 if shouldFail { 54 w.WriteHeader(http.StatusInternalServerError) 55 t.Fail() 56 return 57 } 58 if !shouldSucceed { 59 w.WriteHeader(http.StatusOK) 60 return 61 } 62 } else { 63 if len(received) != 1 { 64 t.Errorf("Unexpected data received: %+v", received) 65 } else { 66 if received[0].TelemetryType != expectedType { 67 t.Errorf("Unexpected telemetry type: got %v, want %v", received[0].TelemetryType, expectedType) 68 } 69 } 70 } 71 // If the data is as expected, respond with success 72 t.Log("Responding with success") 73 responseBody := []map[string]interface{}{ 74 {"status": "created"}, 75 } 76 body, err := json.Marshal(responseBody) 77 if err != nil { 78 t.Fatalf("Failed to marshal response body: %v", err) 79 } 80 w.WriteHeader(http.StatusCreated) 81 _, err = w.Write(body) 82 if err != nil { 83 t.Fatalf("Failed to write response body: %v", err) 84 } 85 wg.Done() 86 })) 87 } 88 89 func createClient(t *testing.T, mockServerURL string) *Client { 90 config := zap.NewDevelopmentConfig() 91 config.Level = zap.NewAtomicLevelAt(zap.DebugLevel) 92 logger, err := config.Build() 93 if err != nil { 94 t.Fatalf("Failed to create logger: %v", err) 95 } 96 return NewClient(logger, mockServerURL, "testUID", "testNode", "1.0", WithSendPeriod(100*time.Millisecond), WithPeerID("16Uiu2HAkvWiyFsgRhuJEb9JfjYxEkoHLgnUQmr1N5mKWnYjxYRVm")) 97 } 98 99 type expectedCondition func(received []TelemetryRequest) (shouldSucceed bool, shouldFail bool) 100 101 func withMockServer(t *testing.T, expectedType TelemetryType, expectedCondition expectedCondition, testFunc func(ctx context.Context, t *testing.T, client *Client, wg *sync.WaitGroup)) { 102 var wg sync.WaitGroup 103 wg.Add(1) // Expecting one request 104 105 mockServer := createMockServer(t, &wg, expectedType, expectedCondition) 106 defer mockServer.Close() 107 108 client := createClient(t, mockServer.URL) 109 110 ctx, cancel := context.WithCancel(context.Background()) 111 defer cancel() 112 113 testFunc(ctx, t, client, &wg) 114 115 // Wait for the request to be received 116 wg.Wait() 117 } 118 119 func TestClient_ProcessReceivedMessages(t *testing.T) { 120 withMockServer(t, ReceivedMessagesMetric, nil, func(ctx context.Context, t *testing.T, client *Client, wg *sync.WaitGroup) { 121 // Create a telemetry request to send 122 data := ReceivedMessages{ 123 Filter: transport.Filter{ 124 ChatID: "testChat", 125 PubsubTopic: "testTopic", 126 ContentTopic: types.StringToTopic(testContentTopic), 127 }, 128 SSHMessage: &types.Message{ 129 Hash: []byte("hash"), 130 Timestamp: uint32(time.Now().Unix()), 131 }, 132 Messages: []*v1protocol.StatusMessage{ 133 { 134 ApplicationLayer: v1protocol.ApplicationLayer{ 135 ID: types.HexBytes("123"), 136 Type: 1, 137 }, 138 }, 139 }, 140 } 141 142 // Send the telemetry request 143 client.Start(ctx) 144 client.PushReceivedMessages(ctx, data) 145 }) 146 } 147 148 func TestClient_ProcessReceivedEnvelope(t *testing.T) { 149 withMockServer(t, ReceivedEnvelopeMetric, nil, func(ctx context.Context, t *testing.T, client *Client, wg *sync.WaitGroup) { 150 // Create a telemetry request to send 151 envelope := v2protocol.NewEnvelope(&pb.WakuMessage{ 152 Payload: []byte{1, 2, 3, 4, 5}, 153 ContentTopic: testContentTopic, 154 Version: proto.Uint32(0), 155 Timestamp: proto.Int64(time.Now().Unix()), 156 }, 0, "") 157 158 // Send the telemetry request 159 client.Start(ctx) 160 client.PushReceivedEnvelope(ctx, envelope) 161 }) 162 } 163 164 func TestClient_ProcessSentEnvelope(t *testing.T) { 165 withMockServer(t, SentEnvelopeMetric, nil, func(ctx context.Context, t *testing.T, client *Client, wg *sync.WaitGroup) { 166 // Create a telemetry request to send 167 sentEnvelope := wakuv2.SentEnvelope{ 168 Envelope: v2protocol.NewEnvelope(&pb.WakuMessage{ 169 Payload: []byte{1, 2, 3, 4, 5}, 170 ContentTopic: testContentTopic, 171 Version: proto.Uint32(0), 172 Timestamp: proto.Int64(time.Now().Unix()), 173 }, 0, ""), 174 PublishMethod: publish.LightPush, 175 } 176 177 // Send the telemetry request 178 client.Start(ctx) 179 client.PushSentEnvelope(ctx, sentEnvelope) 180 }) 181 } 182 183 var ( 184 testENRBootstrap = "enrtree://AI4W5N5IFEUIHF5LESUAOSMV6TKWF2MB6GU2YK7PU4TYUGUNOCEPW@store.staging.status.nodes.status.im" 185 ) 186 187 func TestTelemetryUponPublishError(t *testing.T) { 188 withMockServer(t, ErrorSendingEnvelopeMetric, nil, func(ctx context.Context, t *testing.T, client *Client, wg *sync.WaitGroup) { 189 enrTreeAddress := testENRBootstrap 190 envEnrTreeAddress := os.Getenv("ENRTREE_ADDRESS") 191 if envEnrTreeAddress != "" { 192 enrTreeAddress = envEnrTreeAddress 193 } 194 195 wakuConfig := &wakuv2.Config{} 196 wakuConfig.Port = 0 197 wakuConfig.EnablePeerExchangeClient = true 198 wakuConfig.LightClient = true 199 wakuConfig.EnableDiscV5 = false 200 wakuConfig.DiscV5BootstrapNodes = []string{enrTreeAddress} 201 wakuConfig.DiscoveryLimit = 20 202 wakuConfig.ClusterID = 16 203 wakuConfig.WakuNodes = []string{enrTreeAddress} 204 wakuConfig.TelemetryServerURL = client.serverURL 205 wakuConfig.TelemetrySendPeriodMs = 500 206 w, err := wakuv2.New(nil, "", wakuConfig, nil, nil, nil, nil, nil) 207 require.NoError(t, err) 208 209 client.Start(ctx) 210 w.SetStatusTelemetryClient(client) 211 212 // Setting this forces the publish function to fail when sending a message 213 w.SkipPublishToTopic(true) 214 215 err = w.Start() 216 require.NoError(t, err) 217 218 msg := &pb.WakuMessage{ 219 Payload: []byte{1, 2, 3, 4, 5}, 220 ContentTopic: testContentTopic, 221 Version: proto.Uint32(0), 222 Timestamp: proto.Int64(time.Now().Unix()), 223 } 224 225 // This should result in a single request sent by the telemetry client 226 _, err = w.Send(wakuConfig.DefaultShardPubsubTopic, msg, nil) 227 require.NoError(t, err) 228 }) 229 } 230 231 func TestRetryCache(t *testing.T) { 232 counter := 0 233 var wg sync.WaitGroup 234 wg.Add(2) 235 236 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 237 if r.Method != "POST" { 238 t.Errorf("Expected 'POST' request, got '%s'", r.Method) 239 } 240 if r.URL.EscapedPath() != "/record-metrics" { 241 t.Errorf("Expected request to '/record-metrics', got '%s'", r.URL.EscapedPath()) 242 } 243 244 // Check the request body is as expected 245 var received []TelemetryRequest 246 err := json.NewDecoder(r.Body).Decode(&received) 247 if err != nil { 248 t.Fatal(err) 249 } 250 251 // Fail for the first request to make telemetry cache grow 252 if counter < 1 { 253 counter++ 254 w.WriteHeader(http.StatusInternalServerError) 255 wg.Done() 256 } else { 257 t.Log("Counter reached, responding with success") 258 if len(received) == 4 { 259 w.WriteHeader(http.StatusCreated) 260 responseBody := []map[string]interface{}{ 261 {"status": "created"}, 262 } 263 body, err := json.Marshal(responseBody) 264 if err != nil { 265 t.Fatalf("Failed to marshal response body: %v", err) 266 } 267 w.WriteHeader(http.StatusCreated) 268 _, err = w.Write(body) 269 if err != nil { 270 t.Fatalf("Failed to write response body: %v", err) 271 } 272 wg.Done() 273 } else { 274 t.Fatalf("Expected 4 metrics, got %d", len(received)-1) 275 } 276 } 277 })) 278 defer mockServer.Close() 279 280 ctx := context.Background() 281 282 client := createClient(t, mockServer.URL) 283 client.Start(ctx) 284 285 for i := 0; i < 3; i++ { 286 client.PushReceivedEnvelope(ctx, v2protocol.NewEnvelope(&pb.WakuMessage{ 287 Payload: []byte{1, 2, 3, 4, 5}, 288 ContentTopic: testContentTopic, 289 Version: proto.Uint32(0), 290 Timestamp: proto.Int64(time.Now().Unix()), 291 }, 0, "")) 292 } 293 294 time.Sleep(110 * time.Millisecond) 295 296 require.Equal(t, 3, len(client.telemetryRetryCache)) 297 298 client.PushReceivedEnvelope(ctx, v2protocol.NewEnvelope(&pb.WakuMessage{ 299 Payload: []byte{1, 2, 3, 4, 5}, 300 ContentTopic: testContentTopic, 301 Version: proto.Uint32(0), 302 Timestamp: proto.Int64(time.Now().Unix()), 303 }, 0, "")) 304 305 wg.Wait() 306 307 time.Sleep(100 * time.Millisecond) 308 309 require.Equal(t, 0, len(client.telemetryRetryCache)) 310 } 311 312 func TestRetryCacheCleanup(t *testing.T) { 313 ctx := context.Background() 314 315 client := createClient(t, "") 316 client.Start(ctx) 317 318 for i := 0; i < 6000; i++ { 319 client.PushReceivedEnvelope(ctx, v2protocol.NewEnvelope(&pb.WakuMessage{ 320 Payload: []byte{1, 2, 3, 4, 5}, 321 ContentTopic: testContentTopic, 322 Version: proto.Uint32(0), 323 Timestamp: proto.Int64(time.Now().Unix()), 324 }, 0, "")) 325 } 326 327 time.Sleep(110 * time.Millisecond) 328 329 require.Equal(t, 6000, len(client.telemetryRetryCache)) 330 331 client.PushReceivedEnvelope(ctx, v2protocol.NewEnvelope(&pb.WakuMessage{ 332 Payload: []byte{1, 2, 3, 4, 5}, 333 ContentTopic: testContentTopic, 334 Version: proto.Uint32(0), 335 Timestamp: proto.Int64(time.Now().Unix()), 336 }, 0, "")) 337 338 time.Sleep(210 * time.Millisecond) 339 340 require.Equal(t, 5001, len(client.telemetryRetryCache)) 341 } 342 func setDefaultConfig(config *wakuv2.Config, lightMode bool) { 343 config.ClusterID = 16 344 345 if lightMode { 346 config.EnablePeerExchangeClient = true 347 config.LightClient = true 348 config.EnableDiscV5 = false 349 } else { 350 config.EnableDiscV5 = true 351 config.EnablePeerExchangeServer = true 352 config.LightClient = false 353 config.EnablePeerExchangeClient = false 354 } 355 } 356 357 var testStoreENRBootstrap = "enrtree://AI4W5N5IFEUIHF5LESUAOSMV6TKWF2MB6GU2YK7PU4TYUGUNOCEPW@store.staging.shards.nodes.status.im" 358 359 func TestPeerCount(t *testing.T) { 360 // t.Skip("flaky test") 361 362 expectedCondition := func(received []TelemetryRequest) (shouldSucceed bool, shouldFail bool) { 363 found := slices.ContainsFunc(received, func(req TelemetryRequest) bool { 364 t.Log(req) 365 return req.TelemetryType == PeerCountMetric 366 }) 367 return found, false 368 } 369 withMockServer(t, PeerCountMetric, expectedCondition, func(ctx context.Context, t *testing.T, client *Client, wg *sync.WaitGroup) { 370 config := &wakuv2.Config{} 371 setDefaultConfig(config, false) 372 config.DiscV5BootstrapNodes = []string{testStoreENRBootstrap} 373 config.DiscoveryLimit = 20 374 config.TelemetryServerURL = client.serverURL 375 config.TelemetrySendPeriodMs = 1500 376 config.TelemetryPeerCountSendPeriod = 1500 377 w, err := wakuv2.New(nil, "shards.staging", config, nil, nil, nil, nil, nil) 378 require.NoError(t, err) 379 380 w.SetStatusTelemetryClient(client) 381 client.Start(ctx) 382 383 require.NoError(t, w.Start()) 384 385 err = tt.RetryWithBackOff(func() error { 386 if len(w.Peers()) == 0 { 387 return errors.New("no peers discovered") 388 } 389 return nil 390 }) 391 392 require.NoError(t, err) 393 394 require.NotEqual(t, 0, len(w.Peers())) 395 }) 396 } 397 398 func TestPeerId(t *testing.T) { 399 expectedCondition := func(received []TelemetryRequest) (shouldSucceed bool, shouldFail bool) { 400 var data map[string]interface{} 401 402 err := json.Unmarshal(*received[0].TelemetryData, &data) 403 if err != nil { 404 return false, true 405 } 406 407 _, ok := data["peerId"] 408 require.True(t, ok) 409 return ok, false 410 } 411 withMockServer(t, ReceivedEnvelopeMetric, expectedCondition, func(ctx context.Context, t *testing.T, client *Client, wg *sync.WaitGroup) { 412 client.Start(ctx) 413 414 client.PushReceivedEnvelope(ctx, v2protocol.NewEnvelope(&pb.WakuMessage{ 415 Payload: []byte{1, 2, 3, 4, 5}, 416 ContentTopic: testContentTopic, 417 Version: proto.Uint32(0), 418 Timestamp: proto.Int64(time.Now().Unix()), 419 }, 0, "")) 420 421 }) 422 423 } 424 425 func TestPeerCountByShard(t *testing.T) { 426 expectedCondition := func(received []TelemetryRequest) (shouldSucceed bool, shouldFail bool) { 427 found := slices.ContainsFunc(received, func(req TelemetryRequest) bool { 428 return req.TelemetryType == PeerCountByShardMetric 429 }) 430 return found, false 431 } 432 withMockServer(t, PeerCountByShardMetric, expectedCondition, func(ctx context.Context, t *testing.T, client *Client, wg *sync.WaitGroup) { 433 config := &wakuv2.Config{} 434 setDefaultConfig(config, false) 435 config.DiscV5BootstrapNodes = []string{testStoreENRBootstrap} 436 config.DiscoveryLimit = 20 437 config.TelemetryServerURL = client.serverURL 438 config.TelemetryPeerCountSendPeriod = 1500 439 config.TelemetrySendPeriodMs = 1500 440 w, err := wakuv2.New(nil, "shards.staging", config, nil, nil, nil, nil, nil) 441 require.NoError(t, err) 442 443 w.SetStatusTelemetryClient(client) 444 client.Start(ctx) 445 446 require.NoError(t, w.Start()) 447 448 err = tt.RetryWithBackOff(func() error { 449 if len(w.Peers()) == 0 { 450 return errors.New("no peers discovered") 451 } 452 return nil 453 }) 454 455 require.NoError(t, err) 456 457 require.NotEqual(t, 0, len(w.Peers())) 458 }) 459 } 460 461 func TestPeerCountByOrigin(t *testing.T) { 462 expectedCondition := func(received []TelemetryRequest) (shouldSucceed bool, shouldFail bool) { 463 found := slices.ContainsFunc(received, func(req TelemetryRequest) bool { 464 return req.TelemetryType == PeerCountByOriginMetric 465 }) 466 return found, false 467 } 468 withMockServer(t, PeerCountByOriginMetric, expectedCondition, func(ctx context.Context, t *testing.T, client *Client, wg *sync.WaitGroup) { 469 config := &wakuv2.Config{} 470 setDefaultConfig(config, false) 471 config.DiscV5BootstrapNodes = []string{testStoreENRBootstrap} 472 config.DiscoveryLimit = 20 473 config.TelemetryServerURL = client.serverURL 474 config.TelemetryPeerCountSendPeriod = 1500 475 config.TelemetrySendPeriodMs = 1500 476 w, err := wakuv2.New(nil, "shards.staging", config, nil, nil, nil, nil, nil) 477 require.NoError(t, err) 478 479 w.SetStatusTelemetryClient(client) 480 client.Start(ctx) 481 482 require.NoError(t, w.Start()) 483 484 err = tt.RetryWithBackOff(func() error { 485 if len(w.Peers()) == 0 { 486 return errors.New("no peers discovered") 487 } 488 return nil 489 }) 490 491 require.NoError(t, err) 492 493 require.NotEqual(t, 0, len(w.Peers())) 494 }) 495 }