Skip to content

Commit 02aeb31

Browse files
committed
feat: Display peer count for PSP and/or WS
1 parent 44fc0c7 commit 02aeb31

File tree

3 files changed

+119
-35
lines changed

3 files changed

+119
-35
lines changed

internal/algod/config/config.go

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,41 @@ package config
22

33
// Config represents the configuration settings for algod, including enabling P2PHybrid
44
type Config struct {
5+
EnableP2P *bool `json:"EnableP2P,omitempty"`
56
EnableP2PHybridMode *bool `json:"EnableP2PHybridMode,omitempty"`
67
}
78

89
// IsEqual compares two Config objects and returns true if all their fields have the same values, otherwise false.
910
func (c Config) IsEqual(conf Config) bool {
10-
if c.EnableP2PHybridMode == nil && conf.EnableP2PHybridMode == nil {
11-
return true
11+
// Check EnableP2P
12+
if (c.EnableP2P == nil) != (conf.EnableP2P == nil) {
13+
return false
14+
}
15+
if c.EnableP2P != nil && *c.EnableP2P != *conf.EnableP2P {
16+
return false
17+
}
18+
19+
// Check EnableP2PHybridMode
20+
if (c.EnableP2PHybridMode == nil) != (conf.EnableP2PHybridMode == nil) {
21+
return false
1222
}
13-
if c.EnableP2PHybridMode == nil || conf.EnableP2PHybridMode == nil {
23+
if c.EnableP2PHybridMode != nil && *c.EnableP2PHybridMode != *conf.EnableP2PHybridMode {
1424
return false
1525
}
16-
return *c.EnableP2PHybridMode == *conf.EnableP2PHybridMode
26+
27+
return true
1728
}
1829

1930
// MergeAlgodConfigs merges two Config objects, with non-zero and non-default fields in 'b' overriding those in 'a'.
2031
func MergeAlgodConfigs(a Config, b Config) Config {
2132
merged := a
2233

34+
if b.EnableP2P != nil {
35+
if a.EnableP2P == nil || *b.EnableP2P != *a.EnableP2P {
36+
merged.EnableP2P = b.EnableP2P
37+
}
38+
}
39+
2340
if b.EnableP2PHybridMode != nil {
2441
if a.EnableP2PHybridMode == nil || *b.EnableP2PHybridMode != *a.EnableP2PHybridMode {
2542
merged.EnableP2PHybridMode = b.EnableP2PHybridMode

internal/algod/metrics.go

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"regexp"
8+
"slices"
89
"strconv"
910
"strings"
1011
"time"
@@ -27,6 +28,14 @@ type Metrics struct {
2728
// calculated based on recent round metrics.
2829
RoundTime time.Duration
2930

31+
// PeersWS represents the total number of connections
32+
// (inbound + outbound) for the traditional WS network.
33+
PeersWS uint64
34+
35+
// PeersP2P represents the total number of connections
36+
// (inbound + outbound) for the P2P network.
37+
PeersP2P uint64
38+
3039
// TPS represents the calculated transactions per second,
3140
// based on the recent metrics over a defined window of rounds.
3241
TPS float64
@@ -79,6 +88,17 @@ type Metrics struct {
7988
// MetricsResponse represents a mapping of metric names to their integer values.
8089
type MetricsResponse map[string]uint64
8190

91+
func orderLabels(keyWithLabels string) string {
92+
if keyWithLabels == "" {
93+
return ""
94+
}
95+
96+
parts := strings.Split(keyWithLabels, ",")
97+
slices.Sort(parts)
98+
99+
return strings.Join(parts, ",")
100+
}
101+
82102
// parseMetricsContent parses Prometheus-style metrics content and returns a mapping of metric names to their integer values.
83103
// It validates the input format, extracts key-value pairs, and handles errors during parsing.
84104
func parseMetricsContent(content string) (MetricsResponse, error) {
@@ -89,29 +109,37 @@ func parseMetricsContent(content string) (MetricsResponse, error) {
89109
return nil, errors.New("invalid metrics content: content must start with #")
90110
}
91111

92-
// Regex for Metrics Format,
93-
// (.*?) - Capture metric key (name+labels, non-greedy)
94-
// \s - Space delimiter
95-
// (.*?) - Capture metric value
96-
re := regexp.MustCompile(`(?m)^([^#].*?)\s(\d*?)$`)
112+
// Main Regex to parse a single metric line:
113+
// 1. ([^#].*?) - Group 1: Metric Name
114+
// 2. (?:\{([^}]+)\})? - Group 2: Labels
115+
// 3. \s - Space delimiter
116+
// 4. (\d*?) - Group 3: Metric Value
117+
re := regexp.MustCompile(`(?m)^([^#].*?)(?:\{([^}]+)\})?\s(\d*?)$`)
97118
rows := re.FindAllStringSubmatch(content, -1)
98119

99120
// Add the strings to the map
100121
for _, row := range rows {
101-
if len(row) < 3 {
122+
if len(row) < 4 {
102123
// Shouldn't happen given the regex above, but here as a sanity check
103124
continue
104125
}
105126

106-
metricKey := strings.TrimSpace(row[1])
107-
valueStr := row[2]
127+
metricName := strings.TrimSpace(row[1])
128+
metricLabel := orderLabels(row[2])
108129

109-
metricVal, err := strconv.ParseUint(valueStr, 10, 64)
130+
var metricKey string
131+
if metricLabel != "" {
132+
metricKey = fmt.Sprintf("%s{%s}", metricName, metricLabel)
133+
} else {
134+
metricKey = metricName
135+
}
136+
137+
metricValue, err := strconv.ParseUint(row[3], 10, 64)
110138
if err != nil {
111-
return nil, fmt.Errorf("failed to parse value '%s' for metric '%s': %w", valueStr, metricKey, err)
139+
return nil, fmt.Errorf("failed to parse value '%s' for metric '%s': %w", row[3], metricKey, err)
112140
}
113141

114-
result[metricKey] = metricVal
142+
result[metricKey] = metricValue
115143
}
116144

117145
// Give the user what they asked for
@@ -144,6 +172,9 @@ func (m Metrics) Get(ctx context.Context, currentRound uint64) (Metrics, api.Res
144172
now := time.Now()
145173
diff := now.Sub(m.LastTS)
146174

175+
m.PeersWS = content["algod_network_incoming_peers"] + content["algod_network_outgoing_peers"]
176+
m.PeersP2P = content["libp2p_rcmgr_connections{dir=\"inbound\",scope=\"system\"}"] + content["libp2p_rcmgr_connections{dir=\"outbound\",scope=\"system\"}"]
177+
147178
m.TX = max(0, uint64(float64(content["algod_network_sent_bytes_total"]-m.LastTX)/diff.Seconds()))
148179
m.RX = max(0, uint64(float64(content["algod_network_received_bytes_total"]-m.LastRX)/diff.Seconds()))
149180

@@ -179,6 +210,8 @@ func NewMetrics(ctx context.Context, client api.ClientWithResponsesInterface, ht
179210
Window: 100,
180211
RoundTime: 0 * time.Second,
181212
TPS: 0,
213+
PeersWS: 0,
214+
PeersP2P: 0,
182215
RX: 0,
183216
TX: 0,
184217
RXP2P: 0,

ui/status.go

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,15 @@ func (m StatusViewModel) HandleMessage(msg tea.Msg) (StatusViewModel, tea.Cmd) {
4747

4848
// getBitRate converts a given byte rate to a human-readable string format. The output may vary from B/s to GB/s.
4949
func getBitRate(bytes uint64) string {
50-
txString := fmt.Sprintf("%d B/s ", bytes)
50+
txString := fmt.Sprintf("%d B/s", bytes)
5151
if bytes >= 1024 {
52-
txString = fmt.Sprintf("%d KB/s ", bytes/(1<<10))
52+
txString = fmt.Sprintf("%d KB/s", bytes/(1<<10))
5353
}
5454
if bytes >= uint64(float64(1024*1024)) {
55-
txString = fmt.Sprintf("%d MB/s ", bytes/(1<<20))
55+
txString = fmt.Sprintf("%d MB/s", bytes/(1<<20))
5656
}
5757
if bytes >= uint64(float64(1024*1024*1024)) {
58-
txString = fmt.Sprintf("%d GB/s ", bytes/(1<<30))
58+
txString = fmt.Sprintf("%d GB/s", bytes/(1<<30))
5959
}
6060

6161
return txString
@@ -72,7 +72,8 @@ func (m StatusViewModel) View() string {
7272
}
7373

7474
isCompact := m.TerminalWidth < 90
75-
isP2PEnabled := m.Data.Config != nil && m.Data.Config.EnableP2PHybridMode != nil && *m.Data.Config.EnableP2PHybridMode
75+
isP2PHybridEnabled := m.Data.Config != nil && m.Data.Config.EnableP2PHybridMode != nil && *m.Data.Config.EnableP2PHybridMode
76+
isP2PEnabled := m.Data.Config != nil && m.Data.Config.EnableP2P != nil && *m.Data.Config.EnableP2P && !isP2PHybridEnabled
7677

7778
var size int
7879
if isCompact {
@@ -94,42 +95,75 @@ func (m StatusViewModel) View() string {
9495
// Last Round
9596
row1 := lipgloss.JoinHorizontal(lipgloss.Left, beginning, middle, end)
9697

97-
if isP2PEnabled {
98-
end = "P2P: " + style.Green.Render("YES") + " "
98+
if isP2PHybridEnabled {
99+
end = "P2P: " + style.Green.Render("HYBRID") + " "
100+
} else if isP2PEnabled {
101+
end = "P2P: " + style.Green.Render("ONLY") + " "
99102
} else {
100103
end = "P2P: " + style.Red.Render("NO") + " "
101104
}
102105
beginning = ""
103106
middle = strings.Repeat(" ", max(0, size-(lipgloss.Width(beginning)+lipgloss.Width(end)+2)))
104107
row2 := lipgloss.JoinHorizontal(lipgloss.Left, beginning, middle, end)
105108

109+
beginning = style.Cyan.Render(" -- " + strconv.Itoa(m.Data.Metrics.Window) + " round average --")
110+
// Check metrics to confirm config
111+
hasWSData := (m.Data.Metrics.TX != 0 || m.Data.Metrics.RX != 0)
112+
hasP2PData := (m.Data.Metrics.TXP2P != 0 || m.Data.Metrics.RXP2P != 0)
113+
if isP2PHybridEnabled && (hasWSData && !hasP2PData) || (hasP2PData && !hasWSData) {
114+
// Should be P2P and WS
115+
end = style.Red.Render("Network/Config Mismatch") + " "
116+
} else if isP2PEnabled && (!hasP2PData || (hasP2PData && hasWSData)) {
117+
// Should be ONLY P2P
118+
end = style.Red.Render("Network/Config Mismatch") + " "
119+
} else if (!isP2PHybridEnabled && !isP2PEnabled) && (!hasWSData || (hasWSData && hasP2PData)) {
120+
// Should be ONLY WS
121+
end = style.Red.Render("Network/Config Mismatch") + " "
122+
} else {
123+
// Otherwise show peer count
124+
end = "Peers: "
125+
if isP2PHybridEnabled {
126+
end += fmt.Sprintf(" % 4d WS | % 4d P2P ", m.Data.Metrics.PeersWS, m.Data.Metrics.PeersP2P)
127+
} else if isP2PEnabled {
128+
end += fmt.Sprintf("%d ", m.Data.Metrics.PeersP2P)
129+
} else {
130+
end += fmt.Sprintf("%d ", m.Data.Metrics.PeersWS)
131+
}
132+
}
133+
middle = strings.Repeat(" ", max(0, size-(lipgloss.Width(beginning)+lipgloss.Width(end)+2)))
134+
row3 := lipgloss.JoinHorizontal(lipgloss.Left, beginning, middle, end)
135+
106136
roundTime := fmt.Sprintf("%.2fs", float64(m.Data.Metrics.RoundTime)/float64(time.Second))
107137
if m.Data.Status.State != algod.StableState {
108138
roundTime = "--"
109139
}
110140
beginning = style.Blue.Render(" Round time: ") + roundTime
111-
end = getBitRate(m.Data.Metrics.TX)
112-
if isP2PEnabled {
113-
end += "| " + getBitRate(m.Data.Metrics.TXP2P)
141+
end = "Tx: "
142+
if isP2PHybridEnabled {
143+
end += fmt.Sprintf("% 8s | % 8s ", getBitRate(m.Data.Metrics.TX), getBitRate(m.Data.Metrics.TXP2P))
144+
} else if isP2PEnabled {
145+
end += fmt.Sprintf("%s ", getBitRate(m.Data.Metrics.TXP2P))
146+
} else {
147+
end += fmt.Sprintf("%s ", getBitRate(m.Data.Metrics.TX))
114148
}
115-
end += style.Green.Render("TX ")
116149
middle = strings.Repeat(" ", max(0, size-(lipgloss.Width(beginning)+lipgloss.Width(end)+2)))
117-
118-
row3 := lipgloss.JoinHorizontal(lipgloss.Left, beginning, middle, end)
150+
row4 := lipgloss.JoinHorizontal(lipgloss.Left, beginning, middle, end)
119151

120152
tps := fmt.Sprintf("%.2f", m.Data.Metrics.TPS)
121153
if m.Data.Status.State != algod.StableState {
122154
tps = "--"
123155
}
124156
beginning = style.Blue.Render(" TPS: ") + tps
125-
end = getBitRate(m.Data.Metrics.RX)
126-
if isP2PEnabled {
127-
end += "| " + getBitRate(m.Data.Metrics.RXP2P)
157+
end = "Rx: "
158+
if isP2PHybridEnabled {
159+
end += fmt.Sprintf("% 8s | % 8s ", getBitRate(m.Data.Metrics.RX), getBitRate(m.Data.Metrics.RXP2P))
160+
} else if isP2PEnabled {
161+
end += fmt.Sprintf("%s ", getBitRate(m.Data.Metrics.RXP2P))
162+
} else {
163+
end += fmt.Sprintf("%s ", getBitRate(m.Data.Metrics.RX))
128164
}
129-
end += style.Green.Render("RX ")
130165
middle = strings.Repeat(" ", max(0, size-(lipgloss.Width(beginning)+lipgloss.Width(end)+2)))
131-
132-
row4 := lipgloss.JoinHorizontal(lipgloss.Left, beginning, middle, end)
166+
row5 := lipgloss.JoinHorizontal(lipgloss.Left, beginning, middle, end)
133167

134168
return style.WithTitles(
135169
"( "+style.Red.Render(fmt.Sprintf("Nodekit-%s", m.Data.Version))+" )",
@@ -138,9 +172,9 @@ func (m StatusViewModel) View() string {
138172
lipgloss.JoinVertical(lipgloss.Left,
139173
row1,
140174
row2,
141-
style.Cyan.Render(" -- "+strconv.Itoa(m.Data.Metrics.Window)+" round average --"),
142175
row3,
143176
row4,
177+
row5,
144178
),
145179
),
146180
)

0 commit comments

Comments
 (0)