Skip to content

Commit 2d3f002

Browse files
authored
[Backport] NETOBSERV-2359 Implement metrics display (#387)
* vendor * metrics display * add time range dropdown * update tests * fix error messages * moved heartbeat to display functions and don't crash on error * limit maximum metrics * run metrics integration test in background * reduce FPS for metrics display * fix and simplify connectivity check * force render on graph selections
1 parent 9c3f803 commit 2d3f002

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

92 files changed

+6552
-863
lines changed

.mk/dev.mk

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,8 @@ flows-mock: build ## Run flow capture using mocks
99
packets-mock: build ## Run packet capture using mocks
1010
./build/network-observability-cli get-packets --mock true
1111
tput reset
12+
13+
.PHONY: metrics-mock
14+
metrics-mock: build ## Run metrics capture using mocks
15+
./build/network-observability-cli get-metrics --mock true
16+
tput reset

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ vendors: ## Refresh vendors directory.
9898
.PHONY: compile
9999
compile: ## Build the binary
100100
@echo "### Compiling project"
101-
GOARCH=${GOARCH} go build -mod vendor -a -o $(OUTPUT)
101+
GOARCH=${GOARCH} go build ${BUILD_FLAGS} -mod vendor -o $(OUTPUT)
102102

103103
.PHONY: test
104104
test: ## Test code using go test

cmd/display.go

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
"github.com/jpillora/sizestr"
8+
"github.com/rivo/tview"
9+
)
10+
11+
const (
12+
defaultFramesPerSecond = 5 // frames per second
13+
)
14+
15+
var (
16+
app *tview.Application
17+
pages *tview.Pages
18+
mainView *tview.Flex
19+
playPauseButton *tview.Button
20+
21+
durationText = tview.NewTextView()
22+
sizeText = tview.NewTextView()
23+
countTextView = tview.NewTextView()
24+
25+
showCount = 1
26+
framesPerSecond = defaultFramesPerSecond
27+
showPopup bool
28+
paused = false
29+
errAdvancedDisplay error
30+
focus = ""
31+
)
32+
33+
func getPages() *tview.Pages {
34+
if capture == Metric {
35+
pages = tview.NewPages().AddPage("main", getMetricMain(), true, true)
36+
37+
if showPopup {
38+
pages = pages.AddPage("modal", getMetricsModal(), true, true)
39+
}
40+
} else {
41+
pages = tview.NewPages().AddPage("main", getFlowMain(), true, true)
42+
43+
if showPopup {
44+
pages = pages.AddPage("modal", getColumnsModal(), true, true)
45+
}
46+
}
47+
48+
return pages
49+
}
50+
51+
// Returns a new primitive which puts the provided primitive in the center and
52+
// sets its size to the given width and height.
53+
func getModal(p tview.Primitive, width, height int) tview.Primitive {
54+
return tview.NewFlex().
55+
AddItem(nil, 0, 1, false).
56+
AddItem(tview.NewFlex().SetDirection(tview.FlexRow).
57+
AddItem(nil, 0, 1, false).
58+
AddItem(p, height, 1, true).
59+
AddItem(nil, 0, 1, false), width, 1, true).
60+
AddItem(nil, 0, 1, false)
61+
}
62+
63+
func getInfoRow() tview.Primitive {
64+
playPauseButton = tview.NewButton(getPlayPauseText()).SetSelectedFunc(func() {
65+
pause(!paused)
66+
})
67+
infoRow := tview.NewFlex().SetDirection(tview.FlexColumn).
68+
AddItem(tview.NewTextView().SetText(getcaptureText()), 0, 1, false).
69+
AddItem(playPauseButton, 10, 0, false)
70+
if logLevel != "info" {
71+
infoRow.AddItem(tview.NewTextView().SetText(getLogLevelText()), 0, 1, false)
72+
}
73+
infoRow.AddItem(durationText.SetText(getDurationText()).SetTextAlign(tview.AlignCenter), 0, 1, false)
74+
infoRow.AddItem(sizeText.SetText(getSizeText()).SetTextAlign(tview.AlignCenter), 0, 1, false)
75+
if logLevel == "debug" {
76+
fpsText := tview.NewTextView().SetText(getFPSText()).SetTextAlign(tview.AlignCenter)
77+
infoRow.
78+
AddItem(fpsText, 0, 1, false).
79+
AddItem(tview.NewButton("-").SetSelectedFunc(func() {
80+
if framesPerSecond > 1 {
81+
framesPerSecond--
82+
}
83+
fpsText.SetText(getFPSText())
84+
}), 5, 0, false).
85+
AddItem(tview.NewButton("+").SetSelectedFunc(func() {
86+
framesPerSecond++
87+
fpsText.SetText(getFPSText())
88+
}), 5, 0, false)
89+
}
90+
infoRow.AddItem(tview.NewTextView(), 16, 0, false)
91+
return infoRow
92+
}
93+
94+
func getCountRow(useSpacer bool) *tview.Flex {
95+
countTextView.SetText(getShowCountText())
96+
countRow := tview.NewFlex().SetDirection(tview.FlexColumn).
97+
AddItem(countTextView, 0, 1, false).
98+
AddItem(tview.NewButton("-").SetSelectedFunc(func() {
99+
if showCount > 5 {
100+
if capture == Metric {
101+
showCount -= 5
102+
} else {
103+
showCount--
104+
}
105+
}
106+
countTextView.SetText(getShowCountText())
107+
updateScreen()
108+
}), 5, 0, false).
109+
AddItem(tview.NewButton("+").SetSelectedFunc(func() {
110+
if capture == Metric {
111+
showCount += 5
112+
} else {
113+
showCount++
114+
}
115+
countTextView.SetText(getShowCountText())
116+
updateScreen()
117+
}), 5, 0, false).
118+
AddItem(tview.NewTextView(), 0, 2, false)
119+
if useSpacer {
120+
countRow.AddItem(tview.NewTextView(), 16, 0, false)
121+
}
122+
return countRow
123+
}
124+
125+
func getShowCountText() string {
126+
if capture == Metric {
127+
return getMetricShowCountText()
128+
}
129+
return getFlowShowCountText()
130+
}
131+
132+
func getFPSText() string {
133+
return fmt.Sprintf("FPS: %d", framesPerSecond)
134+
}
135+
136+
func getPlayPauseText() string {
137+
if paused {
138+
return "⏸︎"
139+
}
140+
return "⏵︎"
141+
}
142+
143+
func getDurationText() string {
144+
duration := currentTime().Sub(startupTime)
145+
return fmt.Sprintf("Duration: %s ", duration.Round(time.Second))
146+
}
147+
148+
func getSizeText() string {
149+
if capture != Metric {
150+
return fmt.Sprintf("Capture size: %s", sizestr.ToString(totalBytes))
151+
}
152+
return ""
153+
}
154+
155+
func updateStatusTexts() {
156+
durationText.SetText(getDurationText())
157+
sizeText.SetText(getSizeText())
158+
}
159+
160+
func hearbeat() {
161+
for {
162+
if captureEnded {
163+
return
164+
}
165+
166+
updateStatusTexts()
167+
if capture == Metric {
168+
updatePlots()
169+
} else {
170+
updateTableAndSuggestions()
171+
}
172+
173+
// refresh
174+
if app != nil {
175+
app.Draw()
176+
}
177+
178+
time.Sleep(time.Second / time.Duration(framesPerSecond))
179+
}
180+
}
181+
182+
func pause(pause bool) {
183+
paused = pause
184+
playPauseButton.SetLabel(getPlayPauseText())
185+
updateScreen()
186+
}
187+
188+
func updateScreen() {
189+
if app != nil {
190+
showPopup = false
191+
app.SetRoot(getPages(), true)
192+
}
193+
}

cmd/flow_capture.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ var flowCmd = &cobra.Command{
2323
}
2424

2525
func runFlowCapture(_ *cobra.Command, _ []string) {
26-
captureType = "Flow"
26+
capture = Flow
2727
go startFlowCollector()
28-
createDisplay()
28+
createFlowDisplay()
2929
}
3030

3131
func startFlowCollector() {
@@ -77,7 +77,6 @@ func startFlowCollector() {
7777
}()
7878

7979
log.Debug("Ready ! Waiting for flows...")
80-
go hearbeat()
8180
for fp := range flowPackets {
8281
if !captureStarted {
8382
log.Debugf("Received first %d flows", len(flowPackets))

0 commit comments

Comments
 (0)