package clientstats import ( "encoding/json" "fmt" "io" "net/http" "strings" "testing" "time" "github.com/prysmaticlabs/prysm/testing/require" "github.com/sirupsen/logrus" logTest "github.com/sirupsen/logrus/hooks/test" ) func init() { logrus.SetLevel(logrus.DebugLevel) } type mockRT struct { body string status string statusCode int } func (rt *mockRT) RoundTrip(_ *http.Request) (*http.Response, error) { return &http.Response{ Status: http.StatusText(http.StatusOK), StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(rt.body)), }, nil } var _ http.RoundTripper = &mockRT{} func TestBeaconNodeScraper(t *testing.T) { bnScraper := beaconNodeScraper{} bnScraper.tripper = &mockRT{body: prometheusTestBody} r, err := bnScraper.Scrape() require.NoError(t, err, "Unexpected error calling beaconNodeScraper.Scrape") bs := &BeaconNodeStats{} err = json.NewDecoder(r).Decode(bs) require.NoError(t, err, "Unexpected error decoding result of beaconNodeScraper.Scrape") // CommonStats require.Equal(t, int64(225), bs.CPUProcessSecondsTotal) require.Equal(t, int64(1166630912), bs.MemoryProcessBytes) require.Equal(t, int64(1619586241), bs.ClientBuild) require.Equal(t, "v1.3.8-hotfix+6c0942", bs.ClientVersion) require.Equal(t, "prysm", bs.ClientName) // BeaconNodeStats require.Equal(t, int64(256552), bs.SyncBeaconHeadSlot) require.Equal(t, true, bs.SyncEth2Synced) require.Equal(t, int64(7365341184), bs.DiskBeaconchainBytesTotal) require.Equal(t, int64(37), bs.NetworkPeersConnected) require.Equal(t, true, bs.SyncEth1Connected) require.Equal(t, true, bs.SyncEth1FallbackConfigured) require.Equal(t, true, bs.SyncEth1FallbackConnected) } // helper function to wrap up all the scrape logic so tests can focus on data cases and assertions func scrapeBeaconNodeStats(body string) (*BeaconNodeStats, error) { if !strings.HasSuffix(body, "\n") { return nil, fmt.Errorf("bad test fixture -- make sure there is a trailing newline unless you want to waste time debugging tests") } bnScraper := beaconNodeScraper{} bnScraper.tripper = &mockRT{body: body} r, err := bnScraper.Scrape() if err != nil { return nil, err } bs := &BeaconNodeStats{} err = json.NewDecoder(r).Decode(bs) return bs, err } func TestInvertEth1Metrics(t *testing.T) { cases := []struct { key string body string test func(*BeaconNodeStats) bool }{ { key: "SyncEth1Connected", body: strings.Replace(prometheusTestBody, "powchain_sync_eth1_connected 1", "powchain_sync_eth1_connected 0", 1), test: func(bs *BeaconNodeStats) bool { return bs.SyncEth1Connected == false && bs.SyncEth1FallbackConfigured == true && bs.SyncEth1FallbackConnected == true }, }, { key: "SyncEth1FallbackConfigured", body: strings.Replace(prometheusTestBody, "powchain_sync_eth1_fallback_configured 1", "powchain_sync_eth1_fallback_configured 0", 1), test: func(bs *BeaconNodeStats) bool { return bs.SyncEth1Connected == true && bs.SyncEth1FallbackConfigured == false && bs.SyncEth1FallbackConnected == true }, }, { key: "SyncEth1FallbackConnected", body: strings.Replace(prometheusTestBody, "powchain_sync_eth1_fallback_connected 1", "powchain_sync_eth1_fallback_connected 0", 1), test: func(bs *BeaconNodeStats) bool { return bs.SyncEth1Connected == true && bs.SyncEth1FallbackConfigured == true && bs.SyncEth1FallbackConnected == false }, }, } for _, c := range cases { bs, err := scrapeBeaconNodeStats(c.body) require.NoError(t, err) require.Equal(t, true, c.test(bs), "BeaconNodeStats.%s was not false, with prometheus body=%s", c.key, c.body) } } func TestFalseEth2Synced(t *testing.T) { bnScraper := beaconNodeScraper{} eth2NotSynced := strings.Replace(prometheusTestBody, "beacon_head_slot 256552", "beacon_head_slot 256559", 1) bnScraper.tripper = &mockRT{body: eth2NotSynced} r, err := bnScraper.Scrape() require.NoError(t, err, "Unexpected error calling beaconNodeScraper.Scrape") bs := &BeaconNodeStats{} err = json.NewDecoder(r).Decode(bs) require.NoError(t, err, "Unexpected error decoding result of beaconNodeScraper.Scrape") require.Equal(t, false, bs.SyncEth2Synced) } func TestValidatorScraper(t *testing.T) { vScraper := validatorScraper{} vScraper.tripper = &mockRT{body: statusFixtureOneOfEach + prometheusTestBody} r, err := vScraper.Scrape() require.NoError(t, err, "Unexpected error calling validatorScraper.Scrape") vs := &ValidatorStats{} err = json.NewDecoder(r).Decode(vs) require.NoError(t, err, "Unexpected error decoding result of validatorScraper.Scrape") // CommonStats require.Equal(t, int64(225), vs.CPUProcessSecondsTotal) require.Equal(t, int64(1166630912), vs.MemoryProcessBytes) require.Equal(t, int64(1619586241), vs.ClientBuild) require.Equal(t, "v1.3.8-hotfix+6c0942", vs.ClientVersion) require.Equal(t, "prysm", vs.ClientName) require.Equal(t, int64(7), vs.ValidatorTotal) require.Equal(t, int64(1), vs.ValidatorActive) } func TestValidatorScraperAllActive(t *testing.T) { vScraper := validatorScraper{} vScraper.tripper = &mockRT{body: statusFixtureAllActive + prometheusTestBody} r, err := vScraper.Scrape() require.NoError(t, err, "Unexpected error calling validatorScraper.Scrape") vs := &ValidatorStats{} err = json.NewDecoder(r).Decode(vs) require.NoError(t, err, "Unexpected error decoding result of validatorScraper.Scrape") // CommonStats require.Equal(t, int64(4), vs.ValidatorTotal) require.Equal(t, int64(4), vs.ValidatorActive) } func TestValidatorScraperNoneActive(t *testing.T) { vScraper := validatorScraper{} vScraper.tripper = &mockRT{body: statusFixtureNoneActive + prometheusTestBody} r, err := vScraper.Scrape() require.NoError(t, err, "Unexpected error calling validatorScraper.Scrape") vs := &ValidatorStats{} err = json.NewDecoder(r).Decode(vs) require.NoError(t, err, "Unexpected error decoding result of validatorScraper.Scrape") // CommonStats require.Equal(t, int64(6), vs.ValidatorTotal) require.Equal(t, int64(0), vs.ValidatorActive) } func mockNowFunc(fixedTime time.Time) func() time.Time { return func() time.Time { return fixedTime } } func TestValidatorAPIMessageDefaults(t *testing.T) { now = mockNowFunc(time.Unix(1619811114, 123456789)) // 1+e6 ns per ms, so 123456789 ns rounded down should be 123 ms nowMillis := int64(1619811114123) vScraper := validatorScraper{} vScraper.tripper = &mockRT{body: statusFixtureOneOfEach + prometheusTestBody} r, err := vScraper.Scrape() require.NoError(t, err, "unexpected error from validatorScraper.Scrape()") vs := &ValidatorStats{} err = json.NewDecoder(r).Decode(vs) require.NoError(t, err, "Unexpected error decoding result of validatorScraper.Scrape") // CommonStats require.Equal(t, nowMillis, vs.Timestamp, "Unexpected 'timestamp' in client-stats APIMessage struct") require.Equal(t, APIVersion, vs.APIVersion, "Unexpected 'version' in client-stats APIMessage struct") require.Equal(t, ValidatorProcessName, vs.ProcessName, "Unexpected value for 'process' in client-stats APIMessage struct") } func TestBeaconNodeAPIMessageDefaults(t *testing.T) { now = mockNowFunc(time.Unix(1619811114, 123456789)) // 1+e6 ns per ms, so 123456789 ns rounded down should be 123 ms nowMillis := int64(1619811114123) bScraper := beaconNodeScraper{} bScraper.tripper = &mockRT{body: prometheusTestBody} r, err := bScraper.Scrape() require.NoError(t, err, "unexpected error from beaconNodeScraper.Scrape()") vs := &BeaconNodeStats{} err = json.NewDecoder(r).Decode(vs) require.NoError(t, err, "Unexpected error decoding result of beaconNodeScraper.Scrape") // CommonStats require.Equal(t, nowMillis, vs.Timestamp, "Unexpected 'timestamp' in client-stats APIMessage struct") require.Equal(t, APIVersion, vs.APIVersion, "Unexpected 'version' in client-stats APIMessage struct") require.Equal(t, BeaconNodeProcessName, vs.ProcessName, "Unexpected value for 'process' in client-stats APIMessage struct") } func TestBadInput(t *testing.T) { hook := logTest.NewGlobal() bnScraper := beaconNodeScraper{} bnScraper.tripper = &mockRT{body: ""} _, err := bnScraper.Scrape() require.NoError(t, err) require.LogsContain(t, hook, "Failed to get prysm_version") } var prometheusTestBody = ` # HELP process_cpu_seconds_total Total user and system CPU time spent in seconds. # TYPE process_cpu_seconds_total counter process_cpu_seconds_total 225.09 # HELP process_resident_memory_bytes Resident memory size in bytes. # TYPE process_resident_memory_bytes gauge process_resident_memory_bytes 1.166630912e+09 # HELP prysm_version # TYPE prysm_version gauge prysm_version{buildDate="1619586241",commit="51eb1540fa838cdbe467bbeb0e36ee667d449377",version="v1.3.8-hotfix+6c0942"} 1 # HELP validator_count The total number of validators # TYPE validator_count gauge validator_count{state="Active"} 210301 validator_count{state="Exited"} 10 validator_count{state="Exiting"} 0 validator_count{state="Pending"} 0 validator_count{state="Slashed"} 0 validator_count{state="Slashing"} 0 # HELP beacon_head_slot Slot of the head block of the beacon chain # TYPE beacon_head_slot gauge beacon_head_slot 256552 # HELP beacon_clock_time_slot The current slot based on the genesis time and current clock # TYPE beacon_clock_time_slot gauge beacon_clock_time_slot 256552 # HELP bcnode_disk_beaconchain_bytes_total Total hard disk space used by the beaconchain database, in bytes. May include mmap. # TYPE bcnode_disk_beaconchain_bytes_total gauge bcnode_disk_beaconchain_bytes_total 7.365341184e+09 # HELP p2p_peer_count The number of peers in a given state. # TYPE p2p_peer_count gauge p2p_peer_count{state="Bad"} 1 p2p_peer_count{state="Connected"} 37 p2p_peer_count{state="Connecting"} 0 p2p_peer_count{state="Disconnected"} 62 p2p_peer_count{state="Disconnecting"} 0 # HELP powchain_sync_eth1_connected Boolean indicating whether a fallback eth1 endpoint is currently connected: 0=false, 1=true. # TYPE powchain_sync_eth1_connected gauge powchain_sync_eth1_connected 1 # HELP powchain_sync_eth1_fallback_configured Boolean recording whether a fallback eth1 endpoint was configured: 0=false, 1=true. # TYPE powchain_sync_eth1_fallback_configured gauge powchain_sync_eth1_fallback_configured 1 # HELP powchain_sync_eth1_fallback_connected Boolean indicating whether a fallback eth1 endpoint is currently connected: 0=false, 1=true. # TYPE powchain_sync_eth1_fallback_connected gauge powchain_sync_eth1_fallback_connected 1 ` var statusFixtureOneOfEach = `# HELP validator_statuses validator statuses: 0 UNKNOWN, 1 DEPOSITED, 2 PENDING, 3 ACTIVE, 4 EXITING, 5 SLASHING, 6 EXITED # TYPE validator_statuses gauge validator_statuses{pubkey="pk0"} 0 validator_statuses{pubkey="pk1"} 1 validator_statuses{pubkey="pk2"} 2 validator_statuses{pubkey="pk3"} 3 validator_statuses{pubkey="pk4"} 4 validator_statuses{pubkey="pk5"} 5 validator_statuses{pubkey="pk6"} 6 ` var statusFixtureAllActive = `# HELP validator_statuses validator statuses: 0 UNKNOWN, 1 DEPOSITED, 2 PENDING, 3 ACTIVE, 4 EXITING, 5 SLASHING, 6 EXITED # TYPE validator_statuses gauge validator_statuses{pubkey="pk0"} 3 validator_statuses{pubkey="pk1"} 3 validator_statuses{pubkey="pk2"} 3 validator_statuses{pubkey="pk3"} 3 ` var statusFixtureNoneActive = `# HELP validator_statuses validator statuses: 0 UNKNOWN, 1 DEPOSITED, 2 PENDING, 3 ACTIVE, 4 EXITING, 5 SLASHING, 6 EXITED # TYPE validator_statuses gauge validator_statuses{pubkey="pk0"} 0 validator_statuses{pubkey="pk1"} 1 validator_statuses{pubkey="pk2"} 2 validator_statuses{pubkey="pk3"} 4 validator_statuses{pubkey="pk4"} 5 validator_statuses{pubkey="pk5"} 6 `