From 40d0a1db5bad3277c2d63282741b58a81fade7d5 Mon Sep 17 00:00:00 2001 From: hengyoush Date: Wed, 9 Oct 2024 23:37:48 +0800 Subject: [PATCH 01/12] [Improvement] Change mysql parser log level --- agent/protocol/mysql/utils.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/agent/protocol/mysql/utils.go b/agent/protocol/mysql/utils.go index 4f3ff273..f7cafe55 100644 --- a/agent/protocol/mysql/utils.go +++ b/agent/protocol/mysql/utils.go @@ -58,7 +58,7 @@ func isOkPacket(packet *MysqlPacket) bool { } if warnings > 1000 { - common.ProtocolParserLog.Warnln("Large warnings count is a sign of misclassification of OK packet.") + common.ProtocolParserLog.Infoln("Large warnings count is a sign of misclassification of OK packet.") } // 7 byte minimum packet size in protocol 4.1. @@ -78,14 +78,14 @@ func isStmtPrepareOKPacket(packet *MysqlPacket) bool { func DissectDateTimeParam(msg string, offset *int, param *string) bool { if len(msg) < *offset+1 { - common.ProtocolParserLog.Warnln("Not enough bytes to dissect date/time param.") + common.ProtocolParserLog.Infoln("Not enough bytes to dissect date/time param.") return false } length := msg[*offset] *offset = *offset + 1 if len(msg) < *offset+int(length) { - common.ProtocolParserLog.Warnln("Not enough bytes to dissect date/time param.") + common.ProtocolParserLog.Infoln("Not enough bytes to dissect date/time param.") return false } *param = "MySQL DateTime rendering not implemented yet" @@ -97,7 +97,7 @@ func DissectFloatParam[T common.KFloat](msg string, offset *int, params *string) var t T length := int(unsafe.Sizeof(t)) if len(msg) < *offset+length { - common.ProtocolParserLog.Warnln("Not enough bytes to dissect float param.") + common.ProtocolParserLog.Infoln("Not enough bytes to dissect float param.") return false } *params = fmt.Sprintf("%f", common.LEndianBytesToFloat[T]([]byte(msg[*offset:*offset+length]))) @@ -114,7 +114,7 @@ func DissectIntParam[T common.KInt](s string, offset *int, nbytes uint, param *s func DissectInt[T common.KInt](msg string, offset *int, length int, result *T) bool { if len(msg) < *offset+length { - common.ProtocolParserLog.Warnln("Not enough bytes to dissect int param.") + common.ProtocolParserLog.Infoln("Not enough bytes to dissect int param.") return false } *result, _ = common.LEndianBytesToKInt[T]([]byte(msg[*offset:]), length) @@ -217,7 +217,7 @@ func MoreResultsExist(packet *MysqlPacket) bool { _, ok1 := processLengthEncodedInt(packet.msg, &pos) _, ok2 := processLengthEncodedInt(packet.msg, &pos) if !ok1 || !ok2 { - common.ProtocolParserLog.Warnln("Error parsing OK packet for SERVER_MORE_RESULTS_EXIST_FLAG") + common.ProtocolParserLog.Infoln("Error parsing OK packet for SERVER_MORE_RESULTS_EXIST_FLAG") return false } return int8(packet.msg[pos])&kServerMoreResultsExistFlag != 0 From 681453f955f98e84097924cf4b4ed3d3f6a1067a Mon Sep 17 00:00:00 2001 From: hengyoush Date: Thu, 10 Oct 2024 23:56:33 +0800 Subject: [PATCH 02/12] add Loggers array --- common/log.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/common/log.go b/common/log.go index 7ff0c573..fdc18746 100644 --- a/common/log.go +++ b/common/log.go @@ -10,3 +10,5 @@ var BPFEventLog *logrus.Logger = logrus.New() var UprobeLog *logrus.Logger = logrus.New() var ConntrackLog *logrus.Logger = logrus.New() var ProtocolParserLog *logrus.Logger = logrus.New() + +var Loggers []*logrus.Logger = []*logrus.Logger{DefaultLog, AgentLog, BPFLog, BPFEventLog, UprobeLog, ConntrackLog, ProtocolParserLog} From 70c53b0dfe7bcd0aaae5b98643528d83d70b449d Mon Sep 17 00:00:00 2001 From: hengyoush Date: Fri, 11 Oct 2024 21:21:57 +0800 Subject: [PATCH 03/12] feat. beautify watch command --- agent/agent.go | 28 +-- agent/render/common/common.go | 5 + agent/render/watch/watch_render.go | 265 +++++++++++++++++++++++++++++ cmd/common.go | 17 ++ cmd/root.go | 3 +- common/log.go | 40 ++++- common/type.go | 8 + go.mod | 21 +++ go.sum | 49 ++++++ 9 files changed, 413 insertions(+), 23 deletions(-) create mode 100644 agent/render/common/common.go create mode 100644 agent/render/watch/watch_render.go diff --git a/agent/agent.go b/agent/agent.go index 8100fcf2..0073c6a9 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -9,6 +9,7 @@ import ( "kyanos/agent/conn" "kyanos/agent/protocol" "kyanos/agent/render" + "kyanos/agent/render/watch" "kyanos/bpf" "kyanos/bpf/loader" "kyanos/common" @@ -40,16 +41,7 @@ func SetupAgent(options ac.AgentOptions) { statRecorder := analysis.InitStatRecorder() var recordsChannel chan *anc.AnnotatedRecord = nil - if options.AnalysisEnable { - recordsChannel = make(chan *anc.AnnotatedRecord, 1000) - resultChannel := make(chan []*analysis.ConnStat, 1000) - renderStopper := make(chan int) - analyzer := analysis.CreateAnalyzer(recordsChannel, &options.AnalysisOptions, resultChannel, renderStopper, options.Ctx) - go analyzer.Run() - - render := render.CreateRender(resultChannel, renderStopper, analyzer.AnalysisOptions) - go render.Run() - } + recordsChannel = make(chan *anc.AnnotatedRecord, 1000) pm := conn.InitProcessorManager(options.ProcessorsNum, connManager, options.MessageFilter, options.LatencyFilter, options.SizeFilter, options.TraceSide) conn.RecordFunc = func(r protocol.Record, c *conn.Connection4) error { @@ -107,8 +99,20 @@ func SetupAgent(options ac.AgentOptions) { if options.InitCompletedHook != nil { options.InitCompletedHook() } - for !stop { - time.Sleep(time.Second * 1) + + if options.AnalysisEnable { + resultChannel := make(chan []*analysis.ConnStat, 1000) + renderStopper := make(chan int) + analyzer := analysis.CreateAnalyzer(recordsChannel, &options.AnalysisOptions, resultChannel, renderStopper, options.Ctx) + go analyzer.Run() + + render := render.CreateRender(resultChannel, renderStopper, analyzer.AnalysisOptions) + go render.Run() + for !stop { + time.Sleep(time.Second * 1) + } + } else { + watch.RunWatchRender(ctx, recordsChannel) } common.AgentLog.Infoln("Kyanos Stopped") return diff --git a/agent/render/common/common.go b/agent/render/common/common.go new file mode 100644 index 00000000..30681d4c --- /dev/null +++ b/agent/render/common/common.go @@ -0,0 +1,5 @@ +package common + +import "github.com/charmbracelet/bubbles/key" + +type KeyMap map[string]key.Binding diff --git a/agent/render/watch/watch_render.go b/agent/render/watch/watch_render.go new file mode 100644 index 00000000..d5b1c6ae --- /dev/null +++ b/agent/render/watch/watch_render.go @@ -0,0 +1,265 @@ +package watch + +import ( + "context" + "fmt" + "kyanos/agent/analysis/common" + rc "kyanos/agent/render/common" + "kyanos/bpf" + c "kyanos/common" + "os" + "strconv" + "strings" + "sync" + "time" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/spinner" + "github.com/charmbracelet/bubbles/table" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +var lock *sync.Mutex = &sync.Mutex{} + +type TickMsg time.Time + +func doTick() tea.Cmd { + return tea.Tick(time.Second, func(t time.Time) tea.Msg { + return TickMsg(t) + }) +} + +type WatchRender struct { + model *model +} + +type model struct { + table table.Model + viewport viewport.Model + records *[]*common.AnnotatedRecord + spinner spinner.Model + help help.Model + chosen bool + ready bool +} + +func (m model) Init() tea.Cmd { return tea.Batch(m.spinner.Tick) } + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + switch msg := msg.(type) { + case spinner.TickMsg: + rows := m.table.Rows() + lock.Lock() + defer lock.Unlock() + if len(rows) < len(*m.records) { + records := (*m.records)[len(rows):] + idx := len(rows) + 1 + for i, record := range records { + row := table.Row{ + fmt.Sprintf("%d", i+idx), + record.ConnDesc.SimpleString(), + bpf.ProtocolNamesMap[bpf.AgentTrafficProtocolT(record.ConnDesc.Protocol)], + fmt.Sprintf("%.2f", c.ConvertDurationToMillisecondsIfNeeded(record.TotalDuration, false)), + fmt.Sprintf("%d", record.ReqSize), + fmt.Sprintf("%d", record.RespSize), + } + rows = append(rows, row) + } + m.table.SetRows(rows) + } + m.spinner, cmd = m.spinner.Update(msg) + return m, cmd + case tea.KeyMsg: + switch msg.String() { + case "esc": + if m.chosen { + m.chosen = false + } else { + if m.table.Focused() { + m.table.Blur() + } else { + m.table.Focus() + } + } + case "q", "ctrl+c": + return m, tea.Quit + case "n", "p": + if !m.chosen { + break + } + if msg.String() == "n" { + m.table.SetCursor(m.table.Cursor() + 1) + } else { + m.table.SetCursor(m.table.Cursor() - 1) + } + fallthrough + case "enter": + m.chosen = true + + if m.chosen { + selected := m.table.SelectedRow() + if selected != nil { + idx, _ := strconv.Atoi(selected[0]) + r := (*m.records)[idx-1] + line := strings.Repeat("+", m.viewport.Width) + m.viewport.SetContent("[Request]\n\n" + c.TruncateString(r.Req.FormatToString(), 1024) + "\n" + line + "\n[Response]\n\n" + + c.TruncateString(r.Resp.FormatToString(), 10240) + "\n" + line) + } else { + panic("!") + } + } + return m, nil + // return m, tea.Batch( + // tea.Printf("Let's go to %s!", m.table.SelectedRow()[1]), + // ) + } + case tea.WindowSizeMsg: + headerHeight := lipgloss.Height(m.headerView()) + footerHeight := lipgloss.Height(m.footerView()) + verticalMarginHeight := headerHeight + footerHeight + if !m.ready { + // Since this program is using the full size of the viewport we + // need to wait until we've received the window dimensions before + // we can initialize the viewport. The initial dimensions come in + // quickly, though asynchronously, which is why we wait for them + // here. + m.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight) + // m.viewport.YPosition = headerHeight + // m.viewport.SetContent(m.content) + m.ready = true + } else { + m.viewport.Width = msg.Width + m.viewport.Height = msg.Height - verticalMarginHeight + } + + } + if m.chosen { + m.viewport, cmd = m.viewport.Update(msg) + return m, cmd + } else { + m.table, cmd = m.table.Update(msg) + if cmd == nil { + cmd = doTick() + } + return m, cmd + } +} +func (m model) View() string { + if m.chosen { + selected := m.table.SelectedRow() + if selected != nil { + if !m.ready { + return "\n Initializing..." + } + return fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.viewport.View(), m.footerView()) + } else { + return "failed" + } + } else { + s := fmt.Sprintf("\n %s Events received: %d\n\n", m.spinner.View(), len(m.table.Rows())) + return s + baseStyle.Render(m.table.View()) + "\n " + m.table.HelpView() + "\n" + } +} +func (m model) headerView() string { + title := titleStyle.Render(fmt.Sprintf("Record Detail: %d (Total: %d)", m.table.Cursor()+1, len(m.table.Rows()))) + line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(title))) + return lipgloss.JoinHorizontal(lipgloss.Center, title, line) +} + +func (m model) footerView() string { + info := infoStyle.Render(fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100)) + line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(info))) + return lipgloss.JoinHorizontal(lipgloss.Center, line, info) + "\n" + m.help.View(detailViewKeyMap) +} + +var baseStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")) + +type watchKeyMap rc.KeyMap + +var ( + titleStyle = func() lipgloss.Style { + b := lipgloss.RoundedBorder() + b.Right = "├" + return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1) + }() + + infoStyle = func() lipgloss.Style { + b := lipgloss.RoundedBorder() + b.Left = "┤" + return titleStyle.BorderStyle(b) + }() + + detailViewKeyMap = watchKeyMap{ + "n": key.NewBinding( + key.WithKeys("n"), + key.WithHelp("n", "next"), + ), + "p": key.NewBinding( + key.WithKeys("p"), + key.WithHelp("p", "previous"), + ), + } +) + +func (k watchKeyMap) ShortHelp() []key.Binding { + return []key.Binding{detailViewKeyMap["n"], detailViewKeyMap["p"]} +} + +func (k watchKeyMap) FullHelp() [][]key.Binding { + return [][]key.Binding{{detailViewKeyMap["n"], detailViewKeyMap["p"]}} +} + +func RunWatchRender(ctx context.Context, ch chan *common.AnnotatedRecord) { + columns := []table.Column{ + {Title: "id", Width: 3}, + {Title: "Connection", Width: 40}, + {Title: "Protocol", Width: 10}, + {Title: "Total Time", Width: 10}, + {Title: "Req Size", Width: 10}, + {Title: "Resp Size", Width: 10}, + } + rows := []table.Row{} + t := table.New( + table.WithColumns(columns), + table.WithRows(rows), + table.WithFocused(true), + table.WithHeight(7), + table.WithWidth(96), + ) + s := table.DefaultStyles() + s.Header = s.Header. + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")). + BorderBottom(true). + Bold(false) + s.Selected = s.Selected. + Foreground(lipgloss.Color("229")). + Background(lipgloss.Color("57")). + Bold(false) + t.SetStyles(s) + records := &[]*common.AnnotatedRecord{} + m := model{t, viewport.New(100, 100), records, spinner.New(spinner.WithSpinner(spinner.Dot)), help.NewModel(), false, false} + go func(mod *model, channel chan *common.AnnotatedRecord) { + for { + select { + case <-ctx.Done(): + return + case r := <-ch: + lock.Lock() + *m.records = append(*m.records, r) + lock.Unlock() + } + } + }(&m, ch) + prog := tea.NewProgram(m, tea.WithContext(ctx), tea.WithAltScreen()) + if _, err := prog.Run(); err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } +} diff --git a/cmd/common.go b/cmd/common.go index 4f7d3ba4..9c811bcb 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -2,13 +2,16 @@ package cmd import ( "fmt" + "io" "kyanos/agent" ac "kyanos/agent/common" "kyanos/agent/protocol" "kyanos/common" + "time" "github.com/go-logr/logr" "github.com/jefurry/logrus" + "github.com/jefurry/logrus/hooks/rotatelog" "github.com/sevlyar/go-daemon" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -161,6 +164,20 @@ func InitLog() { case logrus.DebugLevel: klog.SetLogger(logr.Discard()) } + for _, l := range common.Loggers { + l.SetOut(io.Discard) + logdir := "/tmp" + if logdir != "" { + hook, err := rotatelog.NewHook( + logdir+"/kyanos.log.%Y%m%d", + rotatelog.WithMaxAge(time.Hour*24), + rotatelog.WithRotationTime(time.Hour), + ) + if err == nil { + l.Hooks.Add(hook) + } + } + } } func isValidLogLevel(level int32) bool { diff --git a/cmd/root.go b/cmd/root.go index b7e774c7..51263866 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -7,12 +7,11 @@ import ( "kyanos/common" "strings" - "github.com/jefurry/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" ) -var logger *logrus.Logger = common.DefaultLog +var logger *common.Klogger = common.DefaultLog var rootCmd = &cobra.Command{ Use: `kyanos [flags]`, diff --git a/common/log.go b/common/log.go index fdc18746..4514fb0f 100644 --- a/common/log.go +++ b/common/log.go @@ -1,14 +1,36 @@ package common -import "github.com/jefurry/logrus" +import ( + "io" -var DefaultLog *logrus.Logger = logrus.New() -var AgentLog *logrus.Logger = logrus.New() -var BPFLog *logrus.Logger = logrus.New() + "github.com/jefurry/logrus" +) -var BPFEventLog *logrus.Logger = logrus.New() -var UprobeLog *logrus.Logger = logrus.New() -var ConntrackLog *logrus.Logger = logrus.New() -var ProtocolParserLog *logrus.Logger = logrus.New() +type Klogger struct { + *logrus.Logger +} -var Loggers []*logrus.Logger = []*logrus.Logger{DefaultLog, AgentLog, BPFLog, BPFEventLog, UprobeLog, ConntrackLog, ProtocolParserLog} +/** +type LogOptionsSetter interface { + SetOutput(io.Writer) + SetPrefix(string) +}**/ + +func (k *Klogger) SetOutput(w io.Writer) { + k.SetOut(w) +} + +func (k *Klogger) SetPrefix(p string) { + +} + +var DefaultLog *Klogger = &Klogger{logrus.New()} +var AgentLog *Klogger = &Klogger{logrus.New()} +var BPFLog *Klogger = &Klogger{logrus.New()} + +var BPFEventLog *Klogger = &Klogger{logrus.New()} +var UprobeLog *Klogger = &Klogger{logrus.New()} +var ConntrackLog *Klogger = &Klogger{logrus.New()} +var ProtocolParserLog *Klogger = &Klogger{logrus.New()} + +var Loggers []*Klogger = []*Klogger{DefaultLog, AgentLog, BPFLog, BPFEventLog, UprobeLog, ConntrackLog, ProtocolParserLog} diff --git a/common/type.go b/common/type.go index 95cdc1d4..ac528a2e 100644 --- a/common/type.go +++ b/common/type.go @@ -47,3 +47,11 @@ func (c *ConnDesc) String() string { } return fmt.Sprintf("[pid=%d][protocol=%d] *%s:%d %s %s:%d", c.Pid, c.Protocol, c.LocalAddr.String(), c.LocalPort, direct, c.RemoteAddr.String(), c.RemotePort) } + +func (c *ConnDesc) SimpleString() string { + direct := "=>" + if c.Side != ClientSide { + direct = "<=" + } + return fmt.Sprintf("%s:%d %s %s:%d", c.LocalAddr.String(), c.LocalPort, direct, c.RemoteAddr.String(), c.RemotePort) +} diff --git a/go.mod b/go.mod index 244ebc82..33ef4270 100644 --- a/go.mod +++ b/go.mod @@ -29,10 +29,17 @@ require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/hcsshim v0.11.7 // indirect github.com/airbrake/gobrake v3.7.4+incompatible // indirect + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/caio/go-tdigest v3.1.0+incompatible // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/charmbracelet/bubbles v0.20.0 // indirect + github.com/charmbracelet/bubbletea v1.1.1 // indirect + github.com/charmbracelet/lipgloss v0.13.0 // indirect + github.com/charmbracelet/x/ansi v0.2.3 // indirect + github.com/charmbracelet/x/term v0.2.0 // indirect github.com/containerd/cgroups v1.1.0 // indirect github.com/containerd/continuity v0.4.2 // indirect github.com/containerd/fifo v1.1.0 // indirect @@ -44,6 +51,7 @@ require ( github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect github.com/docker/go-units v0.5.0 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-logr/logr v1.4.2 // indirect @@ -56,7 +64,12 @@ require ( github.com/hashicorp/hcl v1.0.0 // indirect github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect github.com/klauspost/compress v1.17.0 // indirect + github.com/lestrrat-go/strftime v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/locker v1.0.1 // indirect @@ -66,6 +79,12 @@ require ( github.com/moby/sys/user v0.3.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect github.com/morikuni/aec v1.0.0 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/clusters v0.0.0-20200529215643-2700303c1762 // indirect + github.com/muesli/gamut v0.3.1 // indirect + github.com/muesli/kmeans v0.3.1 // indirect + github.com/muesli/termenv v0.15.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect @@ -78,6 +97,7 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect @@ -87,6 +107,7 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect github.com/tklauser/go-sysconf v0.3.14 // indirect github.com/tklauser/numcpus v0.8.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect diff --git a/go.sum b/go.sum index 3b4cdc61..e0cd7664 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,10 @@ github.com/Microsoft/hcsshim v0.11.7 h1:vl/nj3Bar/CvJSYo7gIQPyRWc9f3c6IeSNavBTSZ github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU= github.com/airbrake/gobrake v3.7.4+incompatible h1:NHbD3yqK+qQagH42V1ZkCb9yXAMLswxI2UkQpkqjVvw= github.com/airbrake/gobrake v3.7.4+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= @@ -23,6 +27,16 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= +github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbletea v1.1.1 h1:KJ2/DnmpfqFtDNVTvYZ6zpPFL9iRCRr0qqKOCvppbPY= +github.com/charmbracelet/bubbletea v1.1.1/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= +github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= +github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= +github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= +github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= +github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= github.com/cilium/ebpf v0.14.0 h1:0PsxAjO6EjI1rcT+rkp6WcCnE0ZvfkXBYiMedJtrSUs= github.com/cilium/ebpf v0.14.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= @@ -70,6 +84,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -87,6 +103,7 @@ github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7 github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= @@ -133,8 +150,18 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lestrrat-go/strftime v1.1.0 h1:gMESpZy44/4pXLO/m+sL0yBd1W6LjgjrrD4a68Gapyg= +github.com/lestrrat-go/strftime v1.1.0/go.mod h1:uzeIB52CeUJenCo1syghlugshMysrqUT51HlxphXVeI= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -155,6 +182,19 @@ github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/clusters v0.0.0-20180605185049-a07a36e67d36/go.mod h1:mw5KDqUj0eLj/6DUNINLVJNoPTFkEuGMHtJsXLviLkY= +github.com/muesli/clusters v0.0.0-20200529215643-2700303c1762 h1:p4A2Jx7Lm3NV98VRMKlyWd3nqf8obft8NfXlAUmqd3I= +github.com/muesli/clusters v0.0.0-20200529215643-2700303c1762/go.mod h1:mw5KDqUj0eLj/6DUNINLVJNoPTFkEuGMHtJsXLviLkY= +github.com/muesli/gamut v0.3.1 h1:8hozovcrDBWLLAwuOXC+UDyO0/uNroIdXAmY/lQOMHo= +github.com/muesli/gamut v0.3.1/go.mod h1:BED0DN21PXU1YaYNwaTmX9700SRHPcWWd6Llj0zsz5k= +github.com/muesli/kmeans v0.3.1 h1:KshLQ8wAETfLWOJKMuDCVYHnafddSa1kwGh/IypGIzY= +github.com/muesli/kmeans v0.3.1/go.mod h1:8/OvJW7cHc1BpRf8URb43m+vR105DDe+Kj1WcFXYDqc= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -181,6 +221,9 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -222,6 +265,9 @@ github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZ github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= +github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= @@ -257,6 +303,7 @@ golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt7 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -286,9 +333,11 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= From 97459fb3767529627a7d2d2d3dfb51e533d8b117 Mon Sep 17 00:00:00 2001 From: hengyoush Date: Sat, 12 Oct 2024 04:28:41 +0800 Subject: [PATCH 04/12] [Feature] beautify watch command --- agent/agent.go | 2 +- agent/common/options.go | 3 ++ agent/render/watch/option.go | 16 +++++++++ agent/render/watch/watch_render.go | 56 ++++++++++++++++++++++-------- cmd/common.go | 4 ++- cmd/http.go | 18 +++++----- cmd/mysql.go | 10 +++--- cmd/redis.go | 19 +++++----- cmd/root.go | 3 +- cmd/stat.go | 5 +-- cmd/watch.go | 6 ++-- common/system.go | 13 +++++++ 12 files changed, 106 insertions(+), 49 deletions(-) create mode 100644 agent/render/watch/option.go diff --git a/agent/agent.go b/agent/agent.go index 0073c6a9..8b61e254 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -112,7 +112,7 @@ func SetupAgent(options ac.AgentOptions) { time.Sleep(time.Second * 1) } } else { - watch.RunWatchRender(ctx, recordsChannel) + watch.RunWatchRender(ctx, recordsChannel, options.WatchOptions) } common.AgentLog.Infoln("Kyanos Stopped") return diff --git a/agent/common/options.go b/agent/common/options.go index 0fa5088c..f3a61d76 100644 --- a/agent/common/options.go +++ b/agent/common/options.go @@ -9,6 +9,7 @@ import ( "kyanos/agent/conn" "kyanos/agent/metadata" "kyanos/agent/protocol" + "kyanos/agent/render/watch" "kyanos/bpf" "kyanos/common" "os" @@ -45,6 +46,7 @@ type AgentOptions struct { PerfEventBufferSizeForData int PerfEventBufferSizeForEvent int DisableOpensslUprobe bool + WatchOptions watch.WatchOptions DockerEndpoint string ContainerdEndpoint string @@ -115,5 +117,6 @@ func ValidateAndRepairOptions(options AgentOptions) AgentOptions { if newOptions.CriRuntimeEndpoint != "" { newOptions.CriRuntimeEndpoint = getEndpoint(newOptions.CriRuntimeEndpoint) } + newOptions.WatchOptions.Init() return newOptions } diff --git a/agent/render/watch/option.go b/agent/render/watch/option.go new file mode 100644 index 00000000..f52b6b19 --- /dev/null +++ b/agent/render/watch/option.go @@ -0,0 +1,16 @@ +package watch + +import "strings" + +type WatchOptions struct { + wideOutput bool + Opts string +} + +func (w *WatchOptions) Init() { + if w.Opts != "" { + if strings.Contains(w.Opts, "wide") { + w.wideOutput = true + } + } +} diff --git a/agent/render/watch/watch_render.go b/agent/render/watch/watch_render.go index d5b1c6ae..cbe37e93 100644 --- a/agent/render/watch/watch_render.go +++ b/agent/render/watch/watch_render.go @@ -8,6 +8,7 @@ import ( "kyanos/bpf" c "kyanos/common" "os" + "slices" "strconv" "strings" "sync" @@ -44,6 +45,7 @@ type model struct { help help.Model chosen bool ready bool + wide bool } func (m model) Init() tea.Cmd { return tea.Batch(m.spinner.Tick) } @@ -59,13 +61,28 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { records := (*m.records)[len(rows):] idx := len(rows) + 1 for i, record := range records { - row := table.Row{ - fmt.Sprintf("%d", i+idx), - record.ConnDesc.SimpleString(), - bpf.ProtocolNamesMap[bpf.AgentTrafficProtocolT(record.ConnDesc.Protocol)], - fmt.Sprintf("%.2f", c.ConvertDurationToMillisecondsIfNeeded(record.TotalDuration, false)), - fmt.Sprintf("%d", record.ReqSize), - fmt.Sprintf("%d", record.RespSize), + var row table.Row + if m.wide { + row = table.Row{ + fmt.Sprintf("%d", i+idx), + c.GetPidCmdString(int32(record.Pid)), + record.ConnDesc.SimpleString(), + bpf.ProtocolNamesMap[bpf.AgentTrafficProtocolT(record.ConnDesc.Protocol)], + fmt.Sprintf("%.2f", c.ConvertDurationToMillisecondsIfNeeded(record.TotalDuration, false)), + fmt.Sprintf("%d", record.ReqSize), + fmt.Sprintf("%d", record.RespSize), + fmt.Sprintf("%.2f", c.ConvertDurationToMillisecondsIfNeeded(record.BlackBoxDuration, false)), + fmt.Sprintf("%.2f", c.ConvertDurationToMillisecondsIfNeeded(record.ReadFromSocketBufferDuration, false)), + } + } else { + row = table.Row{ + fmt.Sprintf("%d", i+idx), + record.ConnDesc.SimpleString(), + bpf.ProtocolNamesMap[bpf.AgentTrafficProtocolT(record.ConnDesc.Protocol)], + fmt.Sprintf("%.2f", c.ConvertDurationToMillisecondsIfNeeded(record.TotalDuration, false)), + fmt.Sprintf("%d", record.ReqSize), + fmt.Sprintf("%d", record.RespSize), + } } rows = append(rows, row) } @@ -107,7 +124,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { r := (*m.records)[idx-1] line := strings.Repeat("+", m.viewport.Width) m.viewport.SetContent("[Request]\n\n" + c.TruncateString(r.Req.FormatToString(), 1024) + "\n" + line + "\n[Response]\n\n" + - c.TruncateString(r.Resp.FormatToString(), 10240) + "\n" + line) + c.TruncateString(r.Resp.FormatToString(), 10240)) } else { panic("!") } @@ -215,14 +232,23 @@ func (k watchKeyMap) FullHelp() [][]key.Binding { return [][]key.Binding{{detailViewKeyMap["n"], detailViewKeyMap["p"]}} } -func RunWatchRender(ctx context.Context, ch chan *common.AnnotatedRecord) { +func RunWatchRender(ctx context.Context, ch chan *common.AnnotatedRecord, options WatchOptions) { columns := []table.Column{ {Title: "id", Width: 3}, {Title: "Connection", Width: 40}, - {Title: "Protocol", Width: 10}, - {Title: "Total Time", Width: 10}, - {Title: "Req Size", Width: 10}, - {Title: "Resp Size", Width: 10}, + {Title: "Proto", Width: 5}, + {Title: "TotalTime", Width: 10}, + {Title: "ReqSize", Width: 7}, + {Title: "RespSize", Width: 8}, + } + if options.wideOutput { + columns = slices.Insert(columns, 1, table.Column{ + Title: "Proc", Width: 20, + }) + columns = append(columns, []table.Column{ + {Title: "Net/Internal", Width: 15}, + {Title: "ReadSocket", Width: 12}, + }...) } rows := []table.Row{} t := table.New( @@ -230,7 +256,7 @@ func RunWatchRender(ctx context.Context, ch chan *common.AnnotatedRecord) { table.WithRows(rows), table.WithFocused(true), table.WithHeight(7), - table.WithWidth(96), + // table.WithWidth(96), ) s := table.DefaultStyles() s.Header = s.Header. @@ -244,7 +270,7 @@ func RunWatchRender(ctx context.Context, ch chan *common.AnnotatedRecord) { Bold(false) t.SetStyles(s) records := &[]*common.AnnotatedRecord{} - m := model{t, viewport.New(100, 100), records, spinner.New(spinner.WithSpinner(spinner.Dot)), help.NewModel(), false, false} + m := model{t, viewport.New(100, 100), records, spinner.New(spinner.WithSpinner(spinner.Dot)), help.New(), false, false, options.wideOutput} go func(mod *model, channel chan *common.AnnotatedRecord) { for { select { diff --git a/cmd/common.go b/cmd/common.go index 9c811bcb..620f1b00 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -42,7 +42,9 @@ func ParseSide(side string) (common.SideEnum, error) { } } -func startAgent(options ac.AgentOptions) { +var options ac.AgentOptions + +func startAgent() { side, err := ParseSide(SidePar) if err != nil { return diff --git a/cmd/http.go b/cmd/http.go index 96960742..7799037b 100644 --- a/cmd/http.go +++ b/cmd/http.go @@ -1,7 +1,6 @@ package cmd import ( - ac "kyanos/agent/common" "kyanos/agent/protocol" "github.com/spf13/cobra" @@ -23,15 +22,14 @@ var httpCmd *cobra.Command = &cobra.Command{ if err != nil { logger.Fatalf("invalid host: %v\n", err) } - startAgent(ac.AgentOptions{ - MessageFilter: protocol.HttpFilter{ - TargetPath: path, - TargetMethods: methods, - TargetHostName: host, - }, - LatencyFilter: initLatencyFilter(cmd), - SizeFilter: initSizeFilter(cmd), - }) + options.MessageFilter = protocol.HttpFilter{ + TargetPath: path, + TargetMethods: methods, + TargetHostName: host, + } + options.LatencyFilter = initLatencyFilter(cmd) + options.SizeFilter = initSizeFilter(cmd) + startAgent() }, } diff --git a/cmd/mysql.go b/cmd/mysql.go index e2c9088e..295caa88 100644 --- a/cmd/mysql.go +++ b/cmd/mysql.go @@ -1,7 +1,6 @@ package cmd import ( - ac "kyanos/agent/common" "kyanos/agent/protocol/mysql" "github.com/spf13/cobra" @@ -11,11 +10,10 @@ var mysqlCmd *cobra.Command = &cobra.Command{ Use: "mysql", Short: "watch MYSQL message", Run: func(cmd *cobra.Command, args []string) { - startAgent(ac.AgentOptions{ - MessageFilter: mysql.MysqlFilter{}, - LatencyFilter: initLatencyFilter(cmd), - SizeFilter: initSizeFilter(cmd), - }) + options.MessageFilter = mysql.MysqlFilter{} + options.LatencyFilter = initLatencyFilter(cmd) + options.SizeFilter = initSizeFilter(cmd) + startAgent() }, } diff --git a/cmd/redis.go b/cmd/redis.go index 89a27333..7ff6a831 100644 --- a/cmd/redis.go +++ b/cmd/redis.go @@ -1,7 +1,6 @@ package cmd import ( - ac "kyanos/agent/common" "kyanos/agent/protocol" "github.com/spf13/cobra" @@ -23,15 +22,15 @@ var redisCmd *cobra.Command = &cobra.Command{ if err != nil { logger.Fatalf("invalid prefix: %v\n", err) } - startAgent(ac.AgentOptions{ - MessageFilter: protocol.RedisFilter{ - TargetCommands: commands, - TargetKeys: keys, - KeyPrefix: prefix, - }, - LatencyFilter: initLatencyFilter(cmd), - SizeFilter: initSizeFilter(cmd), - }) + + options.MessageFilter = protocol.RedisFilter{ + TargetCommands: commands, + TargetKeys: keys, + KeyPrefix: prefix, + } + options.LatencyFilter = initLatencyFilter(cmd) + options.SizeFilter = initSizeFilter(cmd) + startAgent() }, } diff --git a/cmd/root.go b/cmd/root.go index 51263866..b41a29e1 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,7 +2,6 @@ package cmd import ( "fmt" - ac "kyanos/agent/common" "kyanos/agent/metadata/k8s" "kyanos/common" "strings" @@ -34,7 +33,7 @@ sudo kyanos stat http --metrics t --group-by remote-ip sudo kyanos stat http --metrics t --samples 3 --full-body sudo kyanos stat http --metrics tq --sort-by avg --group-by remote-ip`, Run: func(cmd *cobra.Command, args []string) { - startAgent(ac.AgentOptions{}) + startAgent() }, } diff --git a/cmd/stat.go b/cmd/stat.go index d62ebb29..5104f3e0 100644 --- a/cmd/stat.go +++ b/cmd/stat.go @@ -4,7 +4,6 @@ import ( "fmt" "kyanos/agent/analysis" anc "kyanos/agent/analysis/common" - ac "kyanos/agent/common" "slices" "github.com/spf13/cobra" @@ -40,7 +39,9 @@ sudo kyanos stat http --metrics tqp `, PersistentPreRun: func(cmd *cobra.Command, args []string) { Mode = AnalysisMode }, Run: func(cmd *cobra.Command, args []string) { - startAgent(ac.AgentOptions{LatencyFilter: initLatencyFilter(cmd), SizeFilter: initSizeFilter(cmd)}) + options.LatencyFilter = initLatencyFilter(cmd) + options.SizeFilter = initSizeFilter(cmd) + startAgent() }, } var enabledMetricsString string diff --git a/cmd/watch.go b/cmd/watch.go index 465f4808..a7fba43b 100644 --- a/cmd/watch.go +++ b/cmd/watch.go @@ -2,7 +2,6 @@ package cmd import ( "fmt" - ac "kyanos/agent/common" "github.com/spf13/cobra" ) @@ -25,7 +24,9 @@ sudo kyanos watch mysql --latency 100 --req-size 1024 --resp-size 2048 if list { fmt.Println([]string{"http", "redis", "mysql"}) } else { - startAgent(ac.AgentOptions{LatencyFilter: initLatencyFilter(cmd), SizeFilter: initSizeFilter(cmd)}) + options.LatencyFilter = initLatencyFilter(cmd) + options.SizeFilter = initSizeFilter(cmd) + startAgent() } } }, @@ -36,6 +37,7 @@ func init() { watchCmd.PersistentFlags().Float64("latency", 0, "Filter based on request response time") watchCmd.PersistentFlags().Int64("req-size", 0, "Filter based on request bytes size") watchCmd.PersistentFlags().Int64("resp-size", 0, "Filter based on response bytes size") + watchCmd.PersistentFlags().StringVarP(&options.WatchOptions.Opts, "output", "o", "", "Can be `wide`") watchCmd.PersistentFlags().StringVar(&SidePar, "side", "all", "Filter based on connection side. can be: server | client") watchCmd.Flags().SortFlags = false watchCmd.PersistentFlags().SortFlags = false diff --git a/common/system.go b/common/system.go index d6f79f1d..dc794e00 100644 --- a/common/system.go +++ b/common/system.go @@ -44,3 +44,16 @@ func ProcPidRootPath(pid int, paths ...string) string { func GetAllPids() ([]int32, error) { return process.Pids() } + +func GetPidCmdString(pid int32) string { + proc, err := process.NewProcess(pid) + if err != nil { + return fmt.Sprintf("%d<%s>", pid, "unknwon") + } else { + name, err := proc.Name() + if err != nil { + return fmt.Sprintf("%d<%s>", pid, "unknwon") + } + return fmt.Sprintf("%d<%s>", pid, name) + } +} From 845b57a6ec7fd2f93ff88b833abbf6f7b3477e66 Mon Sep 17 00:00:00 2001 From: hengyoush Date: Sun, 13 Oct 2024 15:09:48 +0800 Subject: [PATCH 05/12] [Feature] Beautify stat command --- agent/agent.go | 17 +- agent/analysis/analysis.go | 25 ++- agent/analysis/classfier.go | 2 +- agent/analysis/common/types.go | 20 +- agent/render/common/common.go | 45 ++++- agent/render/render.go | 147 --------------- agent/render/stat/stat.go | 221 ++++++++++++++++++++++ agent/render/type.go | 29 --- agent/render/watch/option.go | 7 +- agent/render/watch/watch_render.go | 288 ++++++++++++++++------------- cmd/stat.go | 62 ++----- 11 files changed, 476 insertions(+), 387 deletions(-) delete mode 100644 agent/render/render.go create mode 100644 agent/render/stat/stat.go delete mode 100644 agent/render/type.go diff --git a/agent/agent.go b/agent/agent.go index 8b61e254..ef546e34 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -8,7 +8,7 @@ import ( "kyanos/agent/compatible" "kyanos/agent/conn" "kyanos/agent/protocol" - "kyanos/agent/render" + "kyanos/agent/render/stat" "kyanos/agent/render/watch" "kyanos/bpf" "kyanos/bpf/loader" @@ -16,7 +16,6 @@ import ( "os" "os/signal" "syscall" - "time" "github.com/cilium/ebpf/rlimit" ) @@ -106,14 +105,16 @@ func SetupAgent(options ac.AgentOptions) { analyzer := analysis.CreateAnalyzer(recordsChannel, &options.AnalysisOptions, resultChannel, renderStopper, options.Ctx) go analyzer.Run() - render := render.CreateRender(resultChannel, renderStopper, analyzer.AnalysisOptions) - go render.Run() - for !stop { - time.Sleep(time.Second * 1) - } + stat.StartStatRender(ctx, resultChannel, options.AnalysisOptions) + // render := render.CreateRender(resultChannel, renderStopper, analyzer.AnalysisOptions) + // go render.Run() + // for !stop { + // time.Sleep(time.Second * 1) + // } } else { watch.RunWatchRender(ctx, recordsChannel, options.WatchOptions) } - common.AgentLog.Infoln("Kyanos Stopped") + common.AgentLog.Infoln("Kyanos Stopped: ", stop) + return } diff --git a/agent/analysis/analysis.go b/agent/analysis/analysis.go index 15c844ba..ec220edd 100644 --- a/agent/analysis/analysis.go +++ b/agent/analysis/analysis.go @@ -114,24 +114,23 @@ type Analyzer struct { ctx context.Context } -func CreateAnalyzer(recordsChannel <-chan *analysis_common.AnnotatedRecord, showOption *analysis_common.AnalysisOptions, resultChannel chan<- []*ConnStat, renderStopper chan int, ctx context.Context) *Analyzer { +func CreateAnalyzer(recordsChannel <-chan *analysis_common.AnnotatedRecord, opts *analysis_common.AnalysisOptions, resultChannel chan<- []*ConnStat, renderStopper chan int, ctx context.Context) *Analyzer { stopper := make(chan int) // ac.AddToFastStopper(stopper) + opts.Init() analyzer := &Analyzer{ - Classfier: getClassfier(showOption.ClassfierType), + Classfier: getClassfier(opts.ClassfierType), recordsChannel: recordsChannel, Aggregators: make(map[ClassId]*aggregator), - AnalysisOptions: showOption, + AnalysisOptions: opts, stopper: stopper, resultChannel: resultChannel, renderStopper: renderStopper, + ctx: ctx, } - if showOption.Interval > 0 { - analyzer.ticker = time.NewTicker(time.Second * time.Duration(showOption.Interval)) - analyzer.tickerC = analyzer.ticker.C - } else { - analyzer.tickerC = make(<-chan time.Time) - } + + analyzer.ticker = time.NewTicker(time.Second * 1) + analyzer.tickerC = analyzer.ticker.C return analyzer } @@ -140,10 +139,6 @@ func (a *Analyzer) Run() { select { // case <-a.stopper: case <-a.ctx.Done(): - if a.AnalysisOptions.Interval == 0 { - a.resultChannel <- a.harvest() - time.Sleep(1 * time.Second) - } a.renderStopper <- 1 return case record := <-a.recordsChannel: @@ -161,7 +156,9 @@ func (a *Analyzer) harvest() []*ConnStat { // aggregator.reset(classId, a.analysis_common.AnalysisOptions) result = append(result, connstat) } - a.Aggregators = make(map[ClassId]*aggregator) + if a.AnalysisOptions.CleanWhenHarvest { + a.Aggregators = make(map[ClassId]*aggregator) + } return result } diff --git a/agent/analysis/classfier.go b/agent/analysis/classfier.go index 81e773ac..dc1cc205 100644 --- a/agent/analysis/classfier.go +++ b/agent/analysis/classfier.go @@ -55,7 +55,7 @@ func init() { classIdHumanReadableMap = make(map[anc.ClassfierType]ClassIdAsHumanReadable) classIdHumanReadableMap[Conn] = func(ar *anc.AnnotatedRecord) string { - return ar.ConnDesc.String() + return ar.ConnDesc.SimpleString() } classIdHumanReadableMap[HttpPath] = func(ar *anc.AnnotatedRecord) string { httpReq, ok := ar.Record.Request().(*protocol.ParsedHttpRequest) diff --git a/agent/analysis/common/types.go b/agent/analysis/common/types.go index 7efe21f2..c13cd41b 100644 --- a/agent/analysis/common/types.go +++ b/agent/analysis/common/types.go @@ -12,12 +12,16 @@ import ( type AnalysisOptions struct { EnabledMetricTypeSet MetricTypeSet SampleLimit int - DisplayLimit int - Interval int Side ac.SideEnum ClassfierType - SortBy LatencyMetric - FullRecordBody bool + CleanWhenHarvest bool +} + +func (a *AnalysisOptions) Init() { + if a.SampleLimit <= 0 { + a.SampleLimit = 10 + } + a.CleanWhenHarvest = false } type ClassfierType int @@ -68,6 +72,14 @@ const ( NoneType ) +func (m MetricType) IsTotalMeaningful() bool { + switch m { + case ResponseSize, RequestSize: + return true + default: + return false + } +} func GetMetricExtractFunc[T MetricValueType](t MetricType) MetricExtract[T] { switch t { case ResponseSize: diff --git a/agent/render/common/common.go b/agent/render/common/common.go index 30681d4c..a2196e55 100644 --- a/agent/render/common/common.go +++ b/agent/render/common/common.go @@ -1,5 +1,48 @@ package common -import "github.com/charmbracelet/bubbles/key" +import ( + "kyanos/agent/analysis/common" + "time" + + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) type KeyMap map[string]key.Binding + +type TickMsg time.Time + +func DoTick() tea.Cmd { + return tea.Tick(time.Second, func(t time.Time) tea.Msg { + return TickMsg(t) + }) +} + +var BaseTableStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")) + +var MetricTypeNames = map[common.MetricType]string{ + common.ResponseSize: "Response Size", + common.RequestSize: "Request Size", + common.TotalDuration: "Total Duration", + common.BlackBoxDuration: "BlackBox Duration", + common.ReadFromSocketBufferDuration: "Socket Read Time", +} + +var MetricTypeSampleNames = map[common.MetricType]string{ + common.ResponseSize: "Max Response Size Samples", + common.RequestSize: "Max Request Size Samples", + common.TotalDuration: "Max Total Duration", + common.BlackBoxDuration: "Max BlackBox Duration", + common.ReadFromSocketBufferDuration: "Max Socket Read Time", +} + +var MetricTypeUnit = map[common.MetricType]string{ + common.ResponseSize: "bytes", + common.RequestSize: "bytes", + common.TotalDuration: "ms", + common.BlackBoxDuration: "ms", + common.ReadFromSocketBufferDuration: "ms", +} diff --git a/agent/render/render.go b/agent/render/render.go deleted file mode 100644 index 35c48127..00000000 --- a/agent/render/render.go +++ /dev/null @@ -1,147 +0,0 @@ -package render - -import ( - "cmp" - "fmt" - "kyanos/agent/analysis" - anc "kyanos/agent/analysis/common" - "kyanos/agent/protocol" - "kyanos/common" - "slices" - "time" - - "github.com/jefurry/logrus" -) - -var log *logrus.Logger = logrus.New() - -type RenderOptions struct { -} - -type Render struct { - resultChannel <-chan []*analysis.ConnStat - stopper <-chan int - *anc.AnalysisOptions -} - -func CreateRender(resultChannel <-chan []*analysis.ConnStat, stopper chan int, options *anc.AnalysisOptions) *Render { - return &Render{ - resultChannel: resultChannel, - stopper: stopper, - AnalysisOptions: options, - } -} - -func (r *Render) Run() { - for { - select { - case <-r.stopper: - continue - // return - case records := <-r.resultChannel: - str := r.simpleRender(records) - log.Infoln(str) - } - } -} - -func (r *Render) simpleRender(constats []*analysis.ConnStat) string { - - var s string - s += fmt.Sprintf("[---------------------------------------Kyanos Stat Report %s-----------------------------------------------]\n", time.Now().Local().Format("2006-01-02 15:04:05")) - if len(r.EnabledMetricTypeSet.AllEnabledMetrciType()) == 1 { - metricType := r.EnabledMetricTypeSet.GetFirstEnabledMetricType() - slices.SortFunc(constats, func(c1, c2 *analysis.ConnStat) int { - v1 := c1.GetValueByMetricType(r.SortBy, metricType) - v2 := c2.GetValueByMetricType(r.SortBy, metricType) - return cmp.Compare(v2, v1) - }) - } - if len(constats) > r.AnalysisOptions.DisplayLimit { - constats = constats[:r.AnalysisOptions.DisplayLimit] - } - - for _, stat := range constats { - const HEADER_TEMPLATE = "%s: %s" // class type: class id - - s += fmt.Sprintf(HEADER_TEMPLATE, analysis.ClassfierTypeNames[stat.ClassfierType], stat.ClassIdAsHumanReadable(stat.ClassId)) - s += "\n" - - for metricType := range stat.SamplesMap { - const METRIC_TEMPLATE = "[ %s ] avg: %.3f%s, max: %.3f%s, P50: %.3f%s, P90: %.3f%s, P99: %.3f%s (count: %d|failed: %d)\n" - - var avg float32 - if stat.Count != 0 { - avg = float32(stat.SumMap[metricType] / float64(stat.Count)) - } - max := stat.MaxMap[metricType] - pCalc := stat.PercentileCalculators[metricType] - p50, p90, p99 := pCalc.CalculatePercentile(0.5), pCalc.CalculatePercentile(0.9), pCalc.CalculatePercentile(0.99) - unit := MetricTypeUnit[metricType] - s += fmt.Sprintf(METRIC_TEMPLATE, metricName(metricType, r.Side), avg, unit, max, unit, p50, unit, p90, unit, p99, unit, stat.Count, stat.FailedCount) - } - s += "\n" - - for metricType, records := range stat.SamplesMap { - if len(records) == 0 { - continue - } - const SAMPLES_HEADER = "[ Top%d %s Samples ]\n" - s += fmt.Sprintf(SAMPLES_HEADER, len(records), metricSampleName(metricType, r.Side)) - for i := range records { - record := records[len(records)-i-1] - s += fmt.Sprintf("----------------------------------------Top %s Sample %d---------------------------------------------\n", metricSampleName(metricType, r.Side), i+1) - if r.FullRecordBody { - s += record.String(anc.AnnotatedRecordToStringOptions{ - MetricTypeSet: r.AnalysisOptions.EnabledMetricTypeSet, - RecordToStringOptions: protocol.RecordToStringOptions{ - RecordMaxDumpBytes: 1024, - IncludeReqBody: true, - IncludeRespBody: true, - }, - IncludeSyscallStat: false, - }) - } else { - s += record.String(anc.AnnotatedRecordToStringOptions{ - MetricTypeSet: r.AnalysisOptions.EnabledMetricTypeSet, - RecordToStringOptions: protocol.RecordToStringOptions{ - RecordMaxDumpBytes: 1024, - IncludeReqSummary: true, - IncludeRespSummary: true, - }, - IncludeSyscallStat: false, - }) - } - } - } - - s += "--------------------------------------------------------------------------------------\n" - } - return s -} - -func metricName(metricType anc.MetricType, side common.SideEnum) string { - - metricName := MetricTypeNames[metricType] - if metricType == anc.BlackBoxDuration { - if side == common.ClientSide { - metricName = "Network Duration" - } else { - metricName = "Server Internal Duration" - } - } - return metricName -} - -func metricSampleName(metricType anc.MetricType, side common.SideEnum) string { - - metricName := MetricTypeNames[metricType] - if metricType == anc.BlackBoxDuration { - if side == common.ClientSide { - metricName = "Max Network Duration" - } else { - metricName = "Max Server Internal Duration" - } - } - return metricName -} diff --git a/agent/render/stat/stat.go b/agent/render/stat/stat.go new file mode 100644 index 00000000..237e9aff --- /dev/null +++ b/agent/render/stat/stat.go @@ -0,0 +1,221 @@ +package stat + +import ( + "context" + "fmt" + "kyanos/agent/analysis" + "kyanos/agent/analysis/common" + rc "kyanos/agent/render/common" + "kyanos/agent/render/watch" + "os" + "sync" + + "github.com/charmbracelet/bubbles/spinner" + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +var lock *sync.Mutex = &sync.Mutex{} + +type model struct { + statTable table.Model + sampleModel tea.Model + spinner spinner.Model + + connstats *[]*analysis.ConnStat + curConnstats *[]*analysis.ConnStat + + options common.AnalysisOptions + + chosenStat bool + chosenClassId string + + windownSizeMsg tea.WindowSizeMsg +} + +func NewModel(options common.AnalysisOptions) tea.Model { + return &model{ + statTable: initTable(options), + sampleModel: nil, + spinner: spinner.New(spinner.WithSpinner(spinner.Dot)), + connstats: nil, + options: options, + chosenStat: false, + } +} + +func initTable(options common.AnalysisOptions) table.Model { + metric := options.EnabledMetricTypeSet.GetFirstEnabledMetricType() + unit := rc.MetricTypeUnit[metric] + columns := []table.Column{ + {Title: "id", Width: 3}, + {Title: "name", Width: 40}, + {Title: fmt.Sprintf("max(%s)", unit), Width: 10}, + {Title: fmt.Sprintf("avg(%s)", unit), Width: 10}, + {Title: fmt.Sprintf("p50(%s)", unit), Width: 10}, + {Title: fmt.Sprintf("p90(%s)", unit), Width: 10}, + {Title: fmt.Sprintf("p99(%s)", unit), Width: 10}, + {Title: "count", Width: 5}, + } + if metric.IsTotalMeaningful() { + columns = append(columns, table.Column{Title: fmt.Sprintf("total(%s)", unit), Width: 12}) + } + rows := []table.Row{} + t := table.New( + table.WithColumns(columns), + table.WithRows(rows), + table.WithFocused(true), + table.WithHeight(7), + // table.WithWidth(96), + ) + s := table.DefaultStyles() + s.Header = s.Header. + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")). + BorderBottom(true). + Bold(false) + s.Selected = s.Selected. + Foreground(lipgloss.Color("229")). + Background(lipgloss.Color("57")). + Bold(false) + t.SetStyles(s) + return t +} + +func (m *model) Init() tea.Cmd { + return tea.Batch(m.spinner.Tick) +} +func (m *model) updateStatTable(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + switch msg := msg.(type) { + case spinner.TickMsg: + rows := make([]table.Row, 0) + lock.Lock() + defer lock.Unlock() + if m.connstats != nil && m.curConnstats != m.connstats { + m.curConnstats = m.connstats + metric := m.options.EnabledMetricTypeSet.GetFirstEnabledMetricType() + records := (*m.curConnstats) + var row table.Row + for i, record := range records { + pCalc := record.PercentileCalculators[metric] + p50, p90, p99 := pCalc.CalculatePercentile(0.5), pCalc.CalculatePercentile(0.9), pCalc.CalculatePercentile(0.99) + row = table.Row{ + fmt.Sprintf("%d", i), + record.ClassIdAsHumanReadable(record.ClassId), + fmt.Sprintf("%.2f", record.MaxMap[metric]), + fmt.Sprintf("%.2f", record.SumMap[metric]/float64(record.Count)), + fmt.Sprintf("%.2f", p50), + fmt.Sprintf("%.2f", p90), + fmt.Sprintf("%.2f", p99), + fmt.Sprintf("%d", record.Count), + } + if metric.IsTotalMeaningful() { + row = append(row, fmt.Sprintf("%.1f", record.SumMap[metric])) + } + rows = append(rows, row) + } + m.statTable.SetRows(rows) + } + m.spinner, cmd = m.spinner.Update(msg) + return m, cmd + case tea.WindowSizeMsg: + m.windownSizeMsg = msg + case tea.KeyMsg: + switch msg.String() { + case "esc", "q", "ctrl+c": + return m, tea.Quit + case "enter": + m.chosenStat = true + // TODO 考虑sort + cursor := m.statTable.Cursor() + metric := m.options.EnabledMetricTypeSet.GetFirstEnabledMetricType() + if m.curConnstats != nil { + records := ((*m.curConnstats)[cursor].SamplesMap[metric]) + m.sampleModel = watch.NewModel(watch.WatchOptions{ + WideOutput: true, + StaticRecord: true, + }, &records, m.windownSizeMsg) + } + + return m, m.sampleModel.Init() + } + } + m.statTable, cmd = m.statTable.Update(msg) + return m, cmd +} + +func (m *model) updateSampleTable(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "q", "ctrl+c": + m.chosenStat = false + return m, m.Init() + case "esc": + _, cmd = m.sampleModel.Update(msg) + if cmd == nil { + m.chosenStat = false + return m, m.Init() + } + default: + _, cmd = m.sampleModel.Update(msg) + } + } + return m, cmd +} + +func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + if !m.chosenStat { + return m.updateStatTable(msg) + } else { + return m.updateSampleTable(msg) + } +} + +func (m *model) viewStatTable() string { + totalCount := 0 + if m.curConnstats != nil { + for _, each := range *m.curConnstats { + totalCount += each.Count + } + } + s := fmt.Sprintf("\n %s Events received: %d\n\n", m.spinner.View(), totalCount) + + return s + rc.BaseTableStyle.Render(m.statTable.View()) + "\n " + m.statTable.HelpView() + "\n" +} + +func (m *model) viewSampleTable() string { + return m.sampleModel.View() +} + +func (m *model) View() string { + if !m.chosenStat { + return m.viewStatTable() + } else { + return m.viewSampleTable() + } +} +func StartStatRender(ctx context.Context, ch <-chan []*analysis.ConnStat, options common.AnalysisOptions) { + m := NewModel(options).(*model) + go func(mod *model, channel <-chan []*analysis.ConnStat) { + for { + select { + case <-ctx.Done(): + return + case r := <-ch: + lock.Lock() + m.connstats = &r + lock.Unlock() + } + } + }(m, ch) + + prog := tea.NewProgram(m, tea.WithContext(ctx), tea.WithAltScreen()) + if _, err := prog.Run(); err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } +} diff --git a/agent/render/type.go b/agent/render/type.go deleted file mode 100644 index 7f40f6f5..00000000 --- a/agent/render/type.go +++ /dev/null @@ -1,29 +0,0 @@ -package render - -import ( - . "kyanos/agent/analysis/common" -) - -var MetricTypeNames = map[MetricType]string{ - ResponseSize: "Response Size", - RequestSize: "Request Size", - TotalDuration: "Total Duration", - BlackBoxDuration: "BlackBox Duration", - ReadFromSocketBufferDuration: "Socket Read Time", -} - -var MetricTypeSampleNames = map[MetricType]string{ - ResponseSize: "Max Response Size Samples", - RequestSize: "Max Request Size Samples", - TotalDuration: "Max Total Duration", - BlackBoxDuration: "Max BlackBox Duration", - ReadFromSocketBufferDuration: "Max Socket Read Time", -} - -var MetricTypeUnit = map[MetricType]string{ - ResponseSize: "bytes", - RequestSize: "bytes", - TotalDuration: "ms", - BlackBoxDuration: "ms", - ReadFromSocketBufferDuration: "ms", -} diff --git a/agent/render/watch/option.go b/agent/render/watch/option.go index f52b6b19..a846d527 100644 --- a/agent/render/watch/option.go +++ b/agent/render/watch/option.go @@ -3,14 +3,15 @@ package watch import "strings" type WatchOptions struct { - wideOutput bool - Opts string + WideOutput bool + StaticRecord bool + Opts string } func (w *WatchOptions) Init() { if w.Opts != "" { if strings.Contains(w.Opts, "wide") { - w.wideOutput = true + w.WideOutput = true } } } diff --git a/agent/render/watch/watch_render.go b/agent/render/watch/watch_render.go index cbe37e93..229d18f8 100644 --- a/agent/render/watch/watch_render.go +++ b/agent/render/watch/watch_render.go @@ -12,7 +12,6 @@ import ( "strconv" "strings" "sync" - "time" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" @@ -25,69 +24,131 @@ import ( var lock *sync.Mutex = &sync.Mutex{} -type TickMsg time.Time - -func doTick() tea.Cmd { - return tea.Tick(time.Second, func(t time.Time) tea.Msg { - return TickMsg(t) - }) -} - type WatchRender struct { model *model } type model struct { - table table.Model - viewport viewport.Model - records *[]*common.AnnotatedRecord - spinner spinner.Model - help help.Model - chosen bool - ready bool - wide bool + table table.Model + viewport viewport.Model + spinner spinner.Model + help help.Model + records *[]*common.AnnotatedRecord + chosen bool + ready bool + wide bool + staticRecord bool + initialWindownSizeMsg tea.WindowSizeMsg +} + +func NewModel(options WatchOptions, records *[]*common.AnnotatedRecord, initialWindownSizeMsg tea.WindowSizeMsg) tea.Model { + var m tea.Model = &model{ + table: initTable(options), + viewport: viewport.New(100, 100), + spinner: spinner.New(spinner.WithSpinner(spinner.Dot)), + help: help.New(), + records: records, + chosen: false, + ready: false, + wide: options.WideOutput, + staticRecord: options.StaticRecord, + initialWindownSizeMsg: initialWindownSizeMsg, + } + return m +} + +func initTable(options WatchOptions) table.Model { + columns := []table.Column{ + {Title: "id", Width: 3}, + {Title: "Connection", Width: 40}, + {Title: "Proto", Width: 5}, + {Title: "TotalTime", Width: 10}, + {Title: "ReqSize", Width: 7}, + {Title: "RespSize", Width: 8}, + } + if options.WideOutput { + columns = slices.Insert(columns, 1, table.Column{ + Title: "Proc", Width: 20, + }) + columns = append(columns, []table.Column{ + {Title: "Net/Internal", Width: 15}, + {Title: "ReadSocket", Width: 12}, + }...) + } + rows := []table.Row{} + t := table.New( + table.WithColumns(columns), + table.WithRows(rows), + table.WithFocused(true), + table.WithHeight(7), + // table.WithWidth(96), + ) + s := table.DefaultStyles() + s.Header = s.Header. + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")). + BorderBottom(true). + Bold(false) + s.Selected = s.Selected. + Foreground(lipgloss.Color("229")). + Background(lipgloss.Color("57")). + Bold(false) + t.SetStyles(s) + return t } -func (m model) Init() tea.Cmd { return tea.Batch(m.spinner.Tick) } +func (m *model) Init() tea.Cmd { + if m.staticRecord { + m.updateRowsInTable() + m.updateDetailViewPortSize(m.initialWindownSizeMsg) + return nil + } else { + return tea.Batch(m.spinner.Tick) + } +} + +func (m *model) updateRowsInTable() { + rows := m.table.Rows() + if len(rows) < len(*m.records) { + records := (*m.records)[len(rows):] + idx := len(rows) + 1 + for i, record := range records { + var row table.Row + if m.wide { + row = table.Row{ + fmt.Sprintf("%d", i+idx), + c.GetPidCmdString(int32(record.Pid)), + record.ConnDesc.SimpleString(), + bpf.ProtocolNamesMap[bpf.AgentTrafficProtocolT(record.ConnDesc.Protocol)], + fmt.Sprintf("%.2f", c.ConvertDurationToMillisecondsIfNeeded(record.TotalDuration, false)), + fmt.Sprintf("%d", record.ReqSize), + fmt.Sprintf("%d", record.RespSize), + fmt.Sprintf("%.2f", c.ConvertDurationToMillisecondsIfNeeded(record.BlackBoxDuration, false)), + fmt.Sprintf("%.2f", c.ConvertDurationToMillisecondsIfNeeded(record.ReadFromSocketBufferDuration, false)), + } + } else { + row = table.Row{ + fmt.Sprintf("%d", i+idx), + record.ConnDesc.SimpleString(), + bpf.ProtocolNamesMap[bpf.AgentTrafficProtocolT(record.ConnDesc.Protocol)], + fmt.Sprintf("%.2f", c.ConvertDurationToMillisecondsIfNeeded(record.TotalDuration, false)), + fmt.Sprintf("%d", record.ReqSize), + fmt.Sprintf("%d", record.RespSize), + } + } + rows = append(rows, row) + } + m.table.SetRows(rows) + } +} -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { - case spinner.TickMsg: - rows := m.table.Rows() + case spinner.TickMsg, rc.TickMsg: lock.Lock() defer lock.Unlock() - if len(rows) < len(*m.records) { - records := (*m.records)[len(rows):] - idx := len(rows) + 1 - for i, record := range records { - var row table.Row - if m.wide { - row = table.Row{ - fmt.Sprintf("%d", i+idx), - c.GetPidCmdString(int32(record.Pid)), - record.ConnDesc.SimpleString(), - bpf.ProtocolNamesMap[bpf.AgentTrafficProtocolT(record.ConnDesc.Protocol)], - fmt.Sprintf("%.2f", c.ConvertDurationToMillisecondsIfNeeded(record.TotalDuration, false)), - fmt.Sprintf("%d", record.ReqSize), - fmt.Sprintf("%d", record.RespSize), - fmt.Sprintf("%.2f", c.ConvertDurationToMillisecondsIfNeeded(record.BlackBoxDuration, false)), - fmt.Sprintf("%.2f", c.ConvertDurationToMillisecondsIfNeeded(record.ReadFromSocketBufferDuration, false)), - } - } else { - row = table.Row{ - fmt.Sprintf("%d", i+idx), - record.ConnDesc.SimpleString(), - bpf.ProtocolNamesMap[bpf.AgentTrafficProtocolT(record.ConnDesc.Protocol)], - fmt.Sprintf("%.2f", c.ConvertDurationToMillisecondsIfNeeded(record.TotalDuration, false)), - fmt.Sprintf("%d", record.ReqSize), - fmt.Sprintf("%d", record.RespSize), - } - } - rows = append(rows, row) - } - m.table.SetRows(rows) - } + m.updateRowsInTable() m.spinner, cmd = m.spinner.Update(msg) return m, cmd case tea.KeyMsg: @@ -96,10 +157,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.chosen { m.chosen = false } else { - if m.table.Focused() { - m.table.Blur() + if m.staticRecord { + return m, nil } else { - m.table.Focus() + return m, tea.Quit } } case "q", "ctrl+c": @@ -135,24 +196,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // ) } case tea.WindowSizeMsg: - headerHeight := lipgloss.Height(m.headerView()) - footerHeight := lipgloss.Height(m.footerView()) - verticalMarginHeight := headerHeight + footerHeight - if !m.ready { - // Since this program is using the full size of the viewport we - // need to wait until we've received the window dimensions before - // we can initialize the viewport. The initial dimensions come in - // quickly, though asynchronously, which is why we wait for them - // here. - m.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight) - // m.viewport.YPosition = headerHeight - // m.viewport.SetContent(m.content) - m.ready = true - } else { - m.viewport.Width = msg.Width - m.viewport.Height = msg.Height - verticalMarginHeight - } - + m.updateDetailViewPortSize(msg) } if m.chosen { m.viewport, cmd = m.viewport.Update(msg) @@ -160,12 +204,31 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } else { m.table, cmd = m.table.Update(msg) if cmd == nil { - cmd = doTick() + cmd = rc.DoTick() } return m, cmd } } -func (m model) View() string { + +func (m *model) updateDetailViewPortSize(msg tea.WindowSizeMsg) { + headerHeight := lipgloss.Height(m.headerView()) + footerHeight := lipgloss.Height(m.footerView()) + verticalMarginHeight := headerHeight + footerHeight + if !m.ready { + // Since this program is using the full size of the viewport we + // need to wait until we've received the window dimensions before + // we can initialize the viewport. The initial dimensions come in + // quickly, though asynchronously, which is why we wait for them + // here. + m.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight) + m.ready = true + } else { + m.viewport.Width = msg.Width + m.viewport.Height = msg.Height - verticalMarginHeight + } +} + +func (m *model) View() string { if m.chosen { selected := m.table.SelectedRow() if selected != nil { @@ -177,8 +240,13 @@ func (m model) View() string { return "failed" } } else { - s := fmt.Sprintf("\n %s Events received: %d\n\n", m.spinner.View(), len(m.table.Rows())) - return s + baseStyle.Render(m.table.View()) + "\n " + m.table.HelpView() + "\n" + var s string + if !m.staticRecord { + s += fmt.Sprintf("\n %s Events received: %d\n\n", m.spinner.View(), len(m.table.Rows())) + } else { + s += fmt.Sprintf("\n Events Num: %d\n\n", len(m.table.Rows())) + } + return s + rc.BaseTableStyle.Render(m.table.View()) + "\n " + m.table.HelpView() + "\n" } } func (m model) headerView() string { @@ -193,10 +261,6 @@ func (m model) footerView() string { return lipgloss.JoinHorizontal(lipgloss.Center, line, info) + "\n" + m.help.View(detailViewKeyMap) } -var baseStyle = lipgloss.NewStyle(). - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("240")) - type watchKeyMap rc.KeyMap var ( @@ -233,56 +297,22 @@ func (k watchKeyMap) FullHelp() [][]key.Binding { } func RunWatchRender(ctx context.Context, ch chan *common.AnnotatedRecord, options WatchOptions) { - columns := []table.Column{ - {Title: "id", Width: 3}, - {Title: "Connection", Width: 40}, - {Title: "Proto", Width: 5}, - {Title: "TotalTime", Width: 10}, - {Title: "ReqSize", Width: 7}, - {Title: "RespSize", Width: 8}, - } - if options.wideOutput { - columns = slices.Insert(columns, 1, table.Column{ - Title: "Proc", Width: 20, - }) - columns = append(columns, []table.Column{ - {Title: "Net/Internal", Width: 15}, - {Title: "ReadSocket", Width: 12}, - }...) - } - rows := []table.Row{} - t := table.New( - table.WithColumns(columns), - table.WithRows(rows), - table.WithFocused(true), - table.WithHeight(7), - // table.WithWidth(96), - ) - s := table.DefaultStyles() - s.Header = s.Header. - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("240")). - BorderBottom(true). - Bold(false) - s.Selected = s.Selected. - Foreground(lipgloss.Color("229")). - Background(lipgloss.Color("57")). - Bold(false) - t.SetStyles(s) records := &[]*common.AnnotatedRecord{} - m := model{t, viewport.New(100, 100), records, spinner.New(spinner.WithSpinner(spinner.Dot)), help.New(), false, false, options.wideOutput} - go func(mod *model, channel chan *common.AnnotatedRecord) { - for { - select { - case <-ctx.Done(): - return - case r := <-ch: - lock.Lock() - *m.records = append(*m.records, r) - lock.Unlock() + m := NewModel(options, records, tea.WindowSizeMsg{}).(*model) + if !options.StaticRecord { + go func(mod *model, channel chan *common.AnnotatedRecord) { + for { + select { + case <-ctx.Done(): + return + case r := <-ch: + lock.Lock() + *m.records = append(*m.records, r) + lock.Unlock() + } } - } - }(&m, ch) + }(m, ch) + } prog := tea.NewProgram(m, tea.WithContext(ctx), tea.WithAltScreen()) if _, err := prog.Run(); err != nil { fmt.Println("Error running program:", err) diff --git a/cmd/stat.go b/cmd/stat.go index 5104f3e0..f1652ced 100644 --- a/cmd/stat.go +++ b/cmd/stat.go @@ -45,13 +45,9 @@ sudo kyanos stat http --metrics tqp }, } var enabledMetricsString string -var displayLimit int var sampleCount int var groupBy string -var interval int -var sortByPar string -var fullBody bool -var SUPPORTED_METRICS = []byte{'t', 'q', 'p', 'n', 's', 'i'} +var SUPPORTED_METRICS = []byte{'t', 'q', 'p', 'n', 's'} func validateEnabledMetricsString() error { for _, m := range []byte(enabledMetricsString) { @@ -66,60 +62,31 @@ func createAnalysisOptions() (anc.AnalysisOptions, error) { options := anc.AnalysisOptions{ EnabledMetricTypeSet: make(anc.MetricTypeSet), } - err := validateEnabledMetricsString() - if err != nil { - logger.Errorln(err) - return anc.AnalysisOptions{}, err - } - enabledMetricsBytes := []byte(enabledMetricsString) - if slices.Contains(enabledMetricsBytes, 't') { + switch enabledMetricsString { + case "t": options.EnabledMetricTypeSet[anc.TotalDuration] = true - } - if slices.Contains(enabledMetricsBytes, 'q') { + case "q": options.EnabledMetricTypeSet[anc.RequestSize] = true - } - if slices.Contains(enabledMetricsBytes, 'p') { + case "p": options.EnabledMetricTypeSet[anc.ResponseSize] = true - } - if slices.Contains(enabledMetricsBytes, 'n') || slices.Contains(enabledMetricsBytes, 'i') { + case "n": options.EnabledMetricTypeSet[anc.BlackBoxDuration] = true - } - if slices.Contains(enabledMetricsBytes, 's') { + case "s": options.EnabledMetricTypeSet[anc.ReadFromSocketBufferDuration] = true + default: + logger.Fatalf("invalid parameter: '-m %s', only support: %s", enabledMetricsString, SUPPORTED_METRICS) } + if sampleCount < 0 { sampleCount = 0 } options.SampleLimit = sampleCount - options.DisplayLimit = displayLimit for key, value := range analysis.ClassfierTypeNames { if value == groupBy { options.ClassfierType = key } } - - options.Interval = interval - - switch sortByPar { - case "avg": - options.SortBy = anc.Avg - case "max": - options.SortBy = anc.Max - case "p50": - options.SortBy = anc.P50 - case "P90": - options.SortBy = anc.P90 - case "P99": - options.SortBy = anc.P99 - default: - logger.Warnf("unknown --sort-by flag: %s, use default '%s'", sortByPar, "avg") - options.SortBy = anc.Avg - } - - if fullBody { - options.FullRecordBody = true - } return options, nil } func init() { @@ -128,22 +95,15 @@ func init() { q: request size, p: response size, n: network device latency, - i: internal application latency, - s: time spent reading from the socket buffer - You can specify these flags individually or - combine them together like: '-m pq'`) + s: time spent reading from the socket buffer`) statCmd.PersistentFlags().IntVarP(&sampleCount, "samples", "s", 0, "Specify the number of samples to be attached for each result.\n"+ "By default, only a summary is output.\n"+ "refer to the '--full-body' option.") - statCmd.PersistentFlags().BoolVar(&fullBody, "full-body", false, "Used with '--samples' option, print content of req-resp when print samples.") - statCmd.PersistentFlags().IntVarP(&displayLimit, "limit", "l", 10, "Specify the number of output results.") - statCmd.PersistentFlags().IntVarP(&interval, "interval", "i", 0, "Print statistics periodically, or if not specified, statistics will be displayed when stopped with `ctrl+c`.") statCmd.PersistentFlags().StringVarP(&groupBy, "group-by", "g", "remote-ip", "Specify aggregation dimension: \n"+ "('conn', 'local-port', 'remote-port', 'remote-ip', 'protocol', 'http-path', 'none')\n"+ "note: 'none' is aggregate all req-resp pair together") - statCmd.PersistentFlags().StringVar(&sortByPar, "sort-by", "avg", "Specify the sorting method for the output results: ('avg', 'max', 'p50', 'p90', 'p99'") // common statCmd.PersistentFlags().Float64("latency", 0, "Filter based on request response time") From ca7876643ab6680a823c83ae31ba954144d2d8da Mon Sep 17 00:00:00 2001 From: hengyoush Date: Sun, 13 Oct 2024 19:00:21 +0800 Subject: [PATCH 06/12] [Feature] Visualize watch record time detail --- agent/analysis/common/types.go | 14 +- agent/analysis/stat.go | 33 ++- agent/conn/kern_event_handler.go | 6 +- agent/render/watch/time_detail.go | 332 +++++++++++++++++++++++++++++ agent/render/watch/watch_render.go | 6 +- cmd/common.go | 3 + go.mod | 3 +- go.sum | 2 + 8 files changed, 388 insertions(+), 11 deletions(-) create mode 100644 agent/render/watch/time_detail.go diff --git a/agent/analysis/common/types.go b/agent/analysis/common/types.go index c13cd41b..8e84397f 100644 --- a/agent/analysis/common/types.go +++ b/agent/analysis/common/types.go @@ -120,6 +120,7 @@ type AnnotatedRecord struct { RespSize int TotalDuration float64 BlackBoxDuration float64 + CopyToSocketBufferDuration float64 ReadFromSocketBufferDuration float64 ReqSyscallEventDetails []SyscallEventDetail RespSyscallEventDetails []SyscallEventDetail @@ -139,8 +140,19 @@ func (a *AnnotatedRecord) GetReadFromSocketBufferDurationMills() float64 { return common.NanoToMills(int32(a.ReadFromSocketBufferDuration)) } +func (a *AnnotatedRecord) GetLastRespSyscallTime() int64 { + if len(a.RespSyscallEventDetails) == 0 { + return 0 + } else { + return int64(a.RespSyscallEventDetails[len(a.RespSyscallEventDetails)-1].Timestamp) + } +} + type SyscallEventDetail PacketEventDetail -type NicEventDetail PacketEventDetail +type NicEventDetail struct { + PacketEventDetail + Attributes map[string]any +} type PacketEventDetail struct { ByteSize int Timestamp uint64 diff --git a/agent/analysis/stat.go b/agent/analysis/stat.go index 1e358c83..f0defa2a 100644 --- a/agent/analysis/stat.go +++ b/agent/analysis/stat.go @@ -199,10 +199,13 @@ func (s *StatRecorder) ReceiveRecord(r protocol.Record, connection *conn.Connect if hasUserCopyEvents && hasTcpInEvents { annotatedRecord.ReadFromSocketBufferDuration = float64(events.userCopyEvents[len(events.userCopyEvents)-1].GetTimestamp()) - float64(events.tcpInEvents[0].GetTimestamp()) } + if hasTcpInEvents && hasNicInEvents { + annotatedRecord.CopyToSocketBufferDuration = float64(events.tcpInEvents[len(events.tcpInEvents)-1].GetTimestamp() - events.nicIngressEvents[0].GetTimestamp()) + } annotatedRecord.ReqSyscallEventDetails = KernEventsToEventDetails[analysisCommon.SyscallEventDetail](events.readSyscallEvents) annotatedRecord.RespSyscallEventDetails = KernEventsToEventDetails[analysisCommon.SyscallEventDetail](events.writeSyscallEvents) - annotatedRecord.ReqNicEventDetails = KernEventsToEventDetails[analysisCommon.NicEventDetail](events.nicIngressEvents) - annotatedRecord.RespNicEventDetails = KernEventsToEventDetails[analysisCommon.NicEventDetail](events.devOutEvents) + annotatedRecord.ReqNicEventDetails = KernEventsToNicEventDetails(events.nicIngressEvents) + annotatedRecord.RespNicEventDetails = KernEventsToNicEventDetails(events.devOutEvents) } else { if hasWriteSyscallEvents { annotatedRecord.StartTs = events.writeSyscallEvents[0].GetTimestamp() @@ -231,10 +234,13 @@ func (s *StatRecorder) ReceiveRecord(r protocol.Record, connection *conn.Connect if hasUserCopyEvents && hasTcpInEvents { annotatedRecord.ReadFromSocketBufferDuration = float64(events.userCopyEvents[len(events.userCopyEvents)-1].GetTimestamp()) - float64(events.tcpInEvents[0].GetTimestamp()) } + if hasTcpInEvents && hasNicInEvents { + annotatedRecord.CopyToSocketBufferDuration = float64(events.tcpInEvents[len(events.tcpInEvents)-1].GetTimestamp() - events.nicIngressEvents[0].GetTimestamp()) + } annotatedRecord.ReqSyscallEventDetails = KernEventsToEventDetails[analysisCommon.SyscallEventDetail](events.writeSyscallEvents) annotatedRecord.RespSyscallEventDetails = KernEventsToEventDetails[analysisCommon.SyscallEventDetail](events.readSyscallEvents) - annotatedRecord.ReqNicEventDetails = KernEventsToEventDetails[analysisCommon.NicEventDetail](events.devOutEvents) - annotatedRecord.RespNicEventDetails = KernEventsToEventDetails[analysisCommon.NicEventDetail](events.nicIngressEvents) + annotatedRecord.ReqNicEventDetails = KernEventsToNicEventDetails(events.devOutEvents) + annotatedRecord.RespNicEventDetails = KernEventsToNicEventDetails(events.nicIngressEvents) } streamEvents.DiscardEventsBySeq(events.egressKernSeq+uint64(events.egressKernLen), true) streamEvents.DiscardEventsBySeq(events.ingressKernSeq+uint64(events.ingressKernLen), false) @@ -261,7 +267,7 @@ func (s *StatRecorder) ReceiveRecord(r protocol.Record, connection *conn.Connect return nil } -func KernEventsToEventDetails[K analysisCommon.PacketEventDetail | analysisCommon.SyscallEventDetail | analysisCommon.NicEventDetail](kernEvents []conn.KernEvent) []K { +func KernEventsToEventDetails[K analysisCommon.PacketEventDetail | analysisCommon.SyscallEventDetail](kernEvents []conn.KernEvent) []K { if len(kernEvents) == 0 { return []K{} } @@ -274,6 +280,23 @@ func KernEventsToEventDetails[K analysisCommon.PacketEventDetail | analysisCommo } return result } +func KernEventsToNicEventDetails(kernEvents []conn.KernEvent) []analysisCommon.NicEventDetail { + if len(kernEvents) == 0 { + return []analysisCommon.NicEventDetail{} + } + result := make([]analysisCommon.NicEventDetail, 0) + for _, each := range kernEvents { + result = append(result, analysisCommon.NicEventDetail{ + PacketEventDetail: analysisCommon.PacketEventDetail{ + + ByteSize: each.GetLen(), + Timestamp: each.GetTimestamp(), + }, + Attributes: each.GetAttributes(), + }) + } + return result +} func getParsedMessageBySide(r protocol.Record, IsServerSide bool, direct DirectEnum) protocol.ParsedMessage { if !IsServerSide { diff --git a/agent/conn/kern_event_handler.go b/agent/conn/kern_event_handler.go index 4e6b2fcf..0cd67505 100644 --- a/agent/conn/kern_event_handler.go +++ b/agent/conn/kern_event_handler.go @@ -93,7 +93,7 @@ func (s *KernEventStream) AddKernEvent(event *bpf.AgentKernEvt) { if err != nil { ifname = "unknown" } - kernEvent.UpdateIfTimestampAttr(ifname, event.Ts) + kernEvent.UpdateIfTimestampAttr(ifname, int64(event.Ts)) } else if found { return // panic("found duplicate kern event on same seq") @@ -218,8 +218,8 @@ func (kernevent *KernEvent) GetAttributes() map[string]any { return kernevent.attributes } -func (kernevent *KernEvent) UpdateIfTimestampAttr(ifname string, time uint64) { - kernevent.attributes[fmt.Sprintf("time-%s", ifname)] = time +func (kernevent *KernEvent) UpdateIfTimestampAttr(ifname string, time int64) { + kernevent.attributes["time-"+ifname] = time } type SslEvent struct { diff --git a/agent/render/watch/time_detail.go b/agent/render/watch/time_detail.go new file mode 100644 index 00000000..c4db207c --- /dev/null +++ b/agent/render/watch/time_detail.go @@ -0,0 +1,332 @@ +package watch + +import ( + "cmp" + "fmt" + "kyanos/agent/analysis/common" + c "kyanos/common" + "slices" + "strings" + + "github.com/Ha4sh-447/flowcharts/diagrams" + "github.com/Ha4sh-447/flowcharts/draw" +) + +func ViewRecordTimeDetailAsFlowChart(r *common.AnnotatedRecord) string { + if r.Side == c.ServerSide { + return ViewRecordTimeDetailAsFlowChartForServer(r) + } else { + return ViewRecordTimeDetailAsFlowChartForClientSide(r) + } +} + +func addNicEventsDiagram(events []common.NicEventDetail, prevNicArrow *diagrams.Shape, prevTs int64, shapes *[]*diagrams.Shape, isReq bool) (*diagrams.Shape, int64) { + var arrowType diagrams.ShapeType + var connectFunc func(shape *diagrams.Shape, subShape *diagrams.Shape) + var lastShape *diagrams.Shape + if isReq { + arrowType = diagrams.RightArrow + connectFunc = diagrams.AddToRight + } else { + arrowType = diagrams.LeftArrow + connectFunc = diagrams.AddToLeft + } + nicEvents := nicEventDetailsAsNicEvents(events) + for idx, nicEvent := range nicEvents { + var nicShapeContent string + if prevTs > 0 { + nicShapeContent = fmt.Sprintf(" %s(used:%.2fms)", nicEvent.ifname, c.ConvertDurationToMillisecondsIfNeeded(float64(nicEvent.ts-int64(prevTs)), false)) + } else { + nicShapeContent = fmt.Sprintf(" %s ", nicEvent.ifname) + } + nicShape := diagrams.Shape{ + Content: nicShapeContent, + Type: diagrams.Rectangle, + IsJunction: true, + } + if prevNicArrow != nil { + if isReq || idx > 0 || prevNicArrow.Type != diagrams.DownArrow { + connectFunc(prevNicArrow, &nicShape) + } else { + // 第一个响应到达网卡 + diagrams.AddToBottom(prevNicArrow, &nicShape) + } + + *shapes = append(*shapes, prevNicArrow) + } + *shapes = append(*shapes, &nicShape) + + if idx != len(nicEvents)-1 { + nicToNext := diagrams.Shape{ + Content: fmt.Sprintf("%s to next", nicEvent.ifname), + Type: arrowType, + } + connectFunc(&nicShape, &nicToNext) + // *shapes = append(*shapes, nicToNext) + prevNicArrow = &nicToNext + } else { + // nicShape.IsLast = true + } + lastShape = &nicShape + prevTs = nicEvent.ts + + } + return lastShape, nicEvents[len(nicEvents)-1].ts +} + +func addSocketBufferDiagram(duration int64, prevDiagram *diagrams.Shape, shapes *[]*diagrams.Shape, isReq bool) *diagrams.Shape { + var arrowType diagrams.ShapeType + var connectFunc func(shape *diagrams.Shape, subShape *diagrams.Shape) + if isReq { + arrowType = diagrams.RightArrow + connectFunc = diagrams.AddToRight + } else { + arrowType = diagrams.LeftArrow + connectFunc = diagrams.AddToLeft + } + lastNicToSocketArrow := diagrams.Shape{ + Content: "", + Type: arrowType, + } + connectFunc(prevDiagram, &lastNicToSocketArrow) + socketBuffer := diagrams.Shape{ + Content: fmt.Sprintf(" Socket(used:%.2fms) ", + c.ConvertDurationToMillisecondsIfNeeded(float64(duration), false)), + Type: diagrams.Rectangle, + } + connectFunc(&lastNicToSocketArrow, &socketBuffer) + socketToAppArrow := diagrams.Shape{ + Content: "", + Type: arrowType, + } + connectFunc(&socketBuffer, &socketToAppArrow) + defer func() { + *shapes = append(*shapes, &lastNicToSocketArrow, &socketBuffer) + }() + return &socketToAppArrow +} + +func getFlowChartString(diagram *diagrams.Diagram) string { + s := diagrams.NewStore() + canvasRow := 200 + canvas := draw.NewCanvas(canvasRow, canvasRow) + canvas.Cursor.X = canvasRow / 4 + c.DefaultLog.Warningf("shapes: %v", diagram.S) + for _, shape := range diagram.S { + c.DefaultLog.Warningf("shape: %v", shape) + diagrams.RenderD(&shape, canvas, s) + } + myCanvas := ToMyCanvas(canvas) + + return myCanvas.toString() +} + +func ViewRecordTimeDetailAsFlowChartForServer(r *common.AnnotatedRecord) string { + shapes := make([]*diagrams.Shape, 0) + diagram := diagrams.New() + lastNicShape, _ := addNicEventsDiagram(r.ReqNicEventDetails, nil, 0, &shapes, true) + socketToAppArrow := addSocketBufferDiagram(int64(r.CopyToSocketBufferDuration), lastNicShape, &shapes, true) + shapes = append(shapes, socketToAppArrow) + applicationStart := diagrams.Shape{ + Content: fmt.Sprintf(" Process(used:%.2fms) ", c.ConvertDurationToMillisecondsIfNeeded(r.ReadFromSocketBufferDuration, false)), + Type: diagrams.Rectangle, + } + diagrams.AddToRight(socketToAppArrow, &applicationStart) + shapes = append(shapes, &applicationStart) + + appStartToAppEndArrow := diagrams.Shape{ + Content: "lastNicToBottomArrow", + Type: diagrams.DownArrow, + } + shapes = append(shapes, &appStartToAppEndArrow) + + applicationEnd := diagrams.Shape{ + Content: fmt.Sprintf(" Process(used:%.2fms) ", c.ConvertDurationToMillisecondsIfNeeded(r.BlackBoxDuration, false)), + Type: diagrams.Rectangle, + } + shapes = append(shapes, &applicationEnd) + diagrams.AddToBottom(&applicationStart, &appStartToAppEndArrow) + + appEndToNic0Arrow := diagrams.Shape{ + Content: "appEndToNic0Arrow", + Type: diagrams.LeftArrow, + } + diagrams.AddToLeft(&applicationEnd, &appEndToNic0Arrow) + // shapes = append(shapes, &appEndToNic0Arrow) + addNicEventsDiagram(r.RespNicEventDetails, &appEndToNic0Arrow, r.GetLastRespSyscallTime(), &shapes, false) + for _, shape := range shapes { + diagram.AddShapes(*shape) + } + return getFlowChartString(diagram) +} + +func ViewRecordTimeDetailAsFlowChartForClientSide(r *common.AnnotatedRecord) string { + shapes := make([]*diagrams.Shape, 0) + diagram := diagrams.New() + applicationStart := diagrams.Shape{ + Content: fmt.Sprintf(" Process(pid:%d) ", r.Pid), + Type: diagrams.Rectangle, + } + appToNic0 := diagrams.Shape{ + Content: "", + Type: diagrams.RightArrow, + // IsJunction: true, + } + diagrams.AddToRight(&applicationStart, &appToNic0) + shapes = append(shapes, &applicationStart) + + lastNicShape, lastNicTs := addNicEventsDiagram(r.ReqNicEventDetails, &appToNic0, int64(r.StartTs), &shapes, true) + lastNicToBottomArrow := diagrams.Shape{ + Content: "lastNicToBottomArrow", + Type: diagrams.DownArrow, + } + diagrams.AddToBottom(lastNicShape, &lastNicToBottomArrow) + lastNicShape, lastNicTs = addNicEventsDiagram(r.RespNicEventDetails, &lastNicToBottomArrow, lastNicTs, &shapes, false) + socketBufferToLeftArrow := addSocketBufferDiagram(int64(r.CopyToSocketBufferDuration), lastNicShape, &shapes, false) + + applicationEnd := diagrams.Shape{ + Content: fmt.Sprintf(" Process(used:%.2fms) ", + c.ConvertDurationToMillisecondsIfNeeded(r.ReadFromSocketBufferDuration, false)), + Type: diagrams.Rectangle, + IsLast: true, + } + diagrams.AddToLeft(socketBufferToLeftArrow, &applicationEnd) + shapes = append(shapes, socketBufferToLeftArrow, &applicationEnd) + + for _, shape := range shapes { + diagram.AddShapes(*shape) + } + return getFlowChartString(diagram) +} + +func nicEventDetailsAsNicEvents(details []common.NicEventDetail) []nicEvent { + events := make([]nicEvent, 0) + + eventMap := make(map[string]int64) + for _, detail := range details { + for key, value := range detail.Attributes { + if ifname := strings.TrimPrefix(key, "time-"); ifname != key { + eventMap[ifname] = value.(int64) + } + } + } + + for ifname, time := range eventMap { + events = append(events, nicEvent{ifname, time}) + } + + slices.SortFunc(events, func(e1, e2 nicEvent) int { + return cmp.Compare(e1.ts, e2.ts) + }) + return events +} + +type nicEvent struct { + ifname string + ts int64 +} + +// Canvas grid will be the draw area +type Canvas struct { + Rows int + Cols int + Grid [][]string + Cursor Point + Center int +} + +type Point struct { + X int + Y int +} + +func ToDrawCanvas(c *Canvas) *draw.Canvas { + newC := draw.NewCanvas(c.Rows, c.Cols) + newC.Grid = c.Grid + newC.Cursor = draw.Point(c.Cursor) + newC.Center = c.Center + return newC +} +func ToMyCanvas(c *draw.Canvas) *Canvas { + newC := NewCanvas(c.Rows, c.Cols) + newC.Grid = c.Grid + newC.Cursor = Point(c.Cursor) + newC.Center = c.Center + return newC +} + +// Create a new canvas +func NewCanvas(r, c int) *Canvas { + g := make([][]string, r) + for i := range g { + g[i] = make([]string, c) + } + for i := range g { + for j := range g[i] { + g[i][j] = " " + } + } + + p := Point{ + X: c/2 - 10, + Y: 0, + } + + return &Canvas{ + Rows: r, + Cols: c, + Grid: g, + Cursor: p, + } +} + +func Center(c *Canvas, x, y int) { + c.Cursor.X = x + c.Cursor.Y = y +} + +func (c *Canvas) CenterX() { + c.Cursor.X = 40 +} + +func (c *Canvas) Render() { + c.toString() +} + +func (c *Canvas) Save() { +} + +func (c *Canvas) toString() string { + grid := c.cleanGrid() + var str strings.Builder + + // for i := range grid { + // for j := range grid[0] { + // fmt.Printf("%s", grid[i][j]) + // } + // fmt.Println() + // } + + for _, r := range grid { + for _, c := range r { + str.WriteString(c) + } + str.WriteString("\n") + } + + return str.String() +} + +func (c *Canvas) cleanGrid() [][]string { + grid := c.Grid + var res [][]string + + for _, r := range grid { + str := strings.Join(r, "") + if strings.Compare(strings.TrimSpace(str), "") != 0 { + res = append(res, r) + } + } + + return res +} diff --git a/agent/render/watch/watch_render.go b/agent/render/watch/watch_render.go index 229d18f8..a3d18431 100644 --- a/agent/render/watch/watch_render.go +++ b/agent/render/watch/watch_render.go @@ -184,7 +184,11 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { idx, _ := strconv.Atoi(selected[0]) r := (*m.records)[idx-1] line := strings.Repeat("+", m.viewport.Width) - m.viewport.SetContent("[Request]\n\n" + c.TruncateString(r.Req.FormatToString(), 1024) + "\n" + line + "\n[Response]\n\n" + + timeDetail := ViewRecordTimeDetailAsFlowChart(r) + // m.viewport.SetContent("[Request]\n\n" + c.TruncateString(r.Req.FormatToString(), 1024) + "\n" + line + "\n[Response]\n\n" + + // c.TruncateString(r.Resp.FormatToString(), 10240)) + m.viewport.SetContent(timeDetail + "\n" + line + "\n" + + "[Request]\n\n" + c.TruncateString(r.Req.FormatToString(), 1024) + "\n" + line + "\n[Response]\n\n" + c.TruncateString(r.Resp.FormatToString(), 10240)) } else { panic("!") diff --git a/cmd/common.go b/cmd/common.go index 620f1b00..a3b41099 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -164,8 +164,11 @@ func InitLog() { case logrus.InfoLevel: fallthrough case logrus.DebugLevel: + break + default: klog.SetLogger(logr.Discard()) } + for _, l := range common.Loggers { l.SetOut(io.Discard) logdir := "/tmp" diff --git a/go.mod b/go.mod index 33ef4270..1f9943a7 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module kyanos -go 1.22.0 +go 1.22.2 toolchain go1.22.6 @@ -26,6 +26,7 @@ require ( require ( github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 // indirect + github.com/Ha4sh-447/flowcharts v0.0.0-20240802124452-44516e0e7dc8 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/hcsshim v0.11.7 // indirect github.com/airbrake/gobrake v3.7.4+incompatible // indirect diff --git a/go.sum b/go.sum index e0cd7664..fb9758c7 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0/go.mod github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Ha4sh-447/flowcharts v0.0.0-20240802124452-44516e0e7dc8 h1:9EgIMDLp0DY3546gtbTk5z1eDHTNFpsDdUvvFFE8VQA= +github.com/Ha4sh-447/flowcharts v0.0.0-20240802124452-44516e0e7dc8/go.mod h1:uTFuSFBjBNWINeStMWKl7xCjoNemWd5VQ7QVgesoBUc= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/hcsshim v0.11.7 h1:vl/nj3Bar/CvJSYo7gIQPyRWc9f3c6IeSNavBTSZNZQ= From bc67847e2581944071e09a9fbab401639b2ed861 Mon Sep 17 00:00:00 2001 From: hengyoush Date: Mon, 14 Oct 2024 01:50:32 +0800 Subject: [PATCH 07/12] [Feature] Add stat table sortby option --- agent/render/stat/stat.go | 258 ++++++++++++++++++++++++++++++++------ 1 file changed, 217 insertions(+), 41 deletions(-) diff --git a/agent/render/stat/stat.go b/agent/render/stat/stat.go index 237e9aff..a1f244cd 100644 --- a/agent/render/stat/stat.go +++ b/agent/render/stat/stat.go @@ -1,6 +1,7 @@ package stat import ( + "cmp" "context" "fmt" "kyanos/agent/analysis" @@ -8,8 +9,13 @@ import ( rc "kyanos/agent/render/common" "kyanos/agent/render/watch" "os" + "slices" + "strconv" + "strings" "sync" + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/spinner" "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" @@ -18,30 +24,101 @@ import ( var lock *sync.Mutex = &sync.Mutex{} +type SortBy int8 +type statTableKeyMap rc.KeyMap + +var sortByKeyMap = statTableKeyMap{ + "1": key.NewBinding( + key.WithKeys("1"), + key.WithHelp("1", "sort by name"), + ), + "2": key.NewBinding( + key.WithKeys("2"), + key.WithHelp("2", "sort by max"), + ), + "3": key.NewBinding( + key.WithKeys("3"), + key.WithHelp("3", "sort by avg"), + ), + "4": key.NewBinding( + key.WithKeys("4"), + key.WithHelp("4", "sort by p50"), + ), + "5": key.NewBinding( + key.WithKeys("5"), + key.WithHelp("5", "sort by p90"), + ), + "6": key.NewBinding( + key.WithKeys("6"), + key.WithHelp("6", "sort by p99"), + ), + "7": key.NewBinding( + key.WithKeys("7"), + key.WithHelp("7", "sort by count"), + ), + "8": key.NewBinding( + key.WithKeys("8"), + key.WithHelp("8", "sort by total"), + ), +} + +func (k statTableKeyMap) ShortHelp() []key.Binding { + return []key.Binding{sortByKeyMap["1"], sortByKeyMap["2"], + sortByKeyMap["3"], sortByKeyMap["4"], + sortByKeyMap["5"], sortByKeyMap["6"], + sortByKeyMap["7"], sortByKeyMap["8"], + } +} + +func (k statTableKeyMap) FullHelp() [][]key.Binding { + return [][]key.Binding{{sortByKeyMap["1"], sortByKeyMap["2"], + sortByKeyMap["3"], sortByKeyMap["4"], + sortByKeyMap["5"], sortByKeyMap["6"], + sortByKeyMap["7"], sortByKeyMap["8"], + }} +} + +const ( + none SortBy = iota + name + max + avg + p50 + p90 + p99 + count + total + end +) + type model struct { - statTable table.Model - sampleModel tea.Model - spinner spinner.Model + statTable table.Model + sampleModel tea.Model + spinner spinner.Model + additionHelp help.Model connstats *[]*analysis.ConnStat curConnstats *[]*analysis.ConnStat options common.AnalysisOptions - chosenStat bool - chosenClassId string + chosenStat bool windownSizeMsg tea.WindowSizeMsg + + sortBy SortBy + reverse bool } func NewModel(options common.AnalysisOptions) tea.Model { return &model{ - statTable: initTable(options), - sampleModel: nil, - spinner: spinner.New(spinner.WithSpinner(spinner.Dot)), - connstats: nil, - options: options, - chosenStat: false, + statTable: initTable(options), + sampleModel: nil, + spinner: spinner.New(spinner.WithSpinner(spinner.Dot)), + additionHelp: help.New(), + connstats: nil, + options: options, + chosenStat: false, } } @@ -86,38 +163,116 @@ func initTable(options common.AnalysisOptions) table.Model { func (m *model) Init() tea.Cmd { return tea.Batch(m.spinner.Tick) } + +func (m *model) updateRowsInTable() { + lock.Lock() + defer lock.Unlock() + rows := make([]table.Row, 0) + if m.connstats != nil && m.curConnstats != m.connstats { + m.curConnstats = m.connstats + m.sortConnstats(m.curConnstats) + metric := m.options.EnabledMetricTypeSet.GetFirstEnabledMetricType() + records := (*m.curConnstats) + var row table.Row + for i, record := range records { + pCalc := record.PercentileCalculators[metric] + p50, p90, p99 := pCalc.CalculatePercentile(0.5), pCalc.CalculatePercentile(0.9), pCalc.CalculatePercentile(0.99) + row = table.Row{ + fmt.Sprintf("%d", i), + record.ClassIdAsHumanReadable(record.ClassId), + fmt.Sprintf("%.2f", record.MaxMap[metric]), + fmt.Sprintf("%.2f", record.SumMap[metric]/float64(record.Count)), + fmt.Sprintf("%.2f", p50), + fmt.Sprintf("%.2f", p90), + fmt.Sprintf("%.2f", p99), + fmt.Sprintf("%d", record.Count), + } + if metric.IsTotalMeaningful() { + row = append(row, fmt.Sprintf("%.1f", record.SumMap[metric])) + } + rows = append(rows, row) + } + m.statTable.SetRows(rows) + } +} +func connstatPercentileSortFunc(c1, c2 *analysis.ConnStat, line float64, m common.MetricType, reverse bool) int { + pCalc1 := c1.PercentileCalculators[m] + value1 := pCalc1.CalculatePercentile(line) + pCalc2 := c2.PercentileCalculators[m] + value2 := pCalc2.CalculatePercentile(line) + if reverse { + return cmp.Compare(value2, value1) + } else { + return cmp.Compare(value1, value2) + + } +} +func (m *model) sortConnstats(connstats *[]*analysis.ConnStat) { + metric := m.options.EnabledMetricTypeSet.GetFirstEnabledMetricType() + switch m.sortBy { + case max: + slices.SortFunc(*connstats, func(c1, c2 *analysis.ConnStat) int { + if m.reverse { + return cmp.Compare(c2.MaxMap[metric], c1.MaxMap[metric]) + } else { + return cmp.Compare(c1.MaxMap[metric], c2.MaxMap[metric]) + } + }) + case avg: + slices.SortFunc(*connstats, func(c1, c2 *analysis.ConnStat) int { + if m.reverse { + return cmp.Compare(c2.SumMap[metric]/float64(c2.Count), c1.SumMap[metric]/float64(c1.Count)) + } else { + return cmp.Compare(c1.SumMap[metric]/float64(c1.Count), c2.SumMap[metric]/float64(c2.Count)) + } + }) + case p50: + slices.SortFunc(*connstats, func(c1, c2 *analysis.ConnStat) int { + return connstatPercentileSortFunc(c1, c2, 0.5, metric, m.reverse) + }) + case p90: + slices.SortFunc(*connstats, func(c1, c2 *analysis.ConnStat) int { + return connstatPercentileSortFunc(c1, c2, 0.9, metric, m.reverse) + }) + case p99: + slices.SortFunc(*connstats, func(c1, c2 *analysis.ConnStat) int { + return connstatPercentileSortFunc(c1, c2, 0.99, metric, m.reverse) + }) + case count: + slices.SortFunc(*connstats, func(c1, c2 *analysis.ConnStat) int { + if m.reverse { + return cmp.Compare(c2.Count, c1.Count) + } else { + return cmp.Compare(c1.Count, c2.Count) + } + }) + case total: + slices.SortFunc(*connstats, func(c1, c2 *analysis.ConnStat) int { + if m.reverse { + return cmp.Compare(c2.SumMap[metric], c1.SumMap[metric]) + } else { + return cmp.Compare(c1.SumMap[metric], c2.SumMap[metric]) + } + }) + case name: + fallthrough + default: + slices.SortFunc(*connstats, func(c1, c2 *analysis.ConnStat) int { + if m.reverse { + return cmp.Compare(c2.ClassId, c1.ClassId) + } else { + return cmp.Compare(c1.ClassId, c2.ClassId) + } + }) + + } +} + func (m *model) updateStatTable(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { case spinner.TickMsg: - rows := make([]table.Row, 0) - lock.Lock() - defer lock.Unlock() - if m.connstats != nil && m.curConnstats != m.connstats { - m.curConnstats = m.connstats - metric := m.options.EnabledMetricTypeSet.GetFirstEnabledMetricType() - records := (*m.curConnstats) - var row table.Row - for i, record := range records { - pCalc := record.PercentileCalculators[metric] - p50, p90, p99 := pCalc.CalculatePercentile(0.5), pCalc.CalculatePercentile(0.9), pCalc.CalculatePercentile(0.99) - row = table.Row{ - fmt.Sprintf("%d", i), - record.ClassIdAsHumanReadable(record.ClassId), - fmt.Sprintf("%.2f", record.MaxMap[metric]), - fmt.Sprintf("%.2f", record.SumMap[metric]/float64(record.Count)), - fmt.Sprintf("%.2f", p50), - fmt.Sprintf("%.2f", p90), - fmt.Sprintf("%.2f", p99), - fmt.Sprintf("%d", record.Count), - } - if metric.IsTotalMeaningful() { - row = append(row, fmt.Sprintf("%.1f", record.SumMap[metric])) - } - rows = append(rows, row) - } - m.statTable.SetRows(rows) - } + m.updateRowsInTable() m.spinner, cmd = m.spinner.Update(msg) return m, cmd case tea.WindowSizeMsg: @@ -126,9 +281,30 @@ func (m *model) updateStatTable(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg.String() { case "esc", "q", "ctrl+c": return m, tea.Quit + case "1", "2", "3", "4", "5", "6", "7", "8": + i, err := strconv.Atoi(strings.TrimPrefix(msg.String(), "ctrl+")) + if err == nil && (i >= int(none) && i < int(end)) && + (i >= 0 && i < len(m.statTable.Columns())) { + prevSortBy := m.sortBy + m.sortBy = SortBy(i) + m.reverse = !m.reverse + cols := m.statTable.Columns() + if prevSortBy != none { + col := &cols[prevSortBy] + col.Title = strings.TrimRight(col.Title, "↑") + col.Title = strings.TrimRight(col.Title, "↓") + } + col := &cols[m.sortBy] + if m.reverse { + col.Title = col.Title + "↓" + } else { + col.Title = col.Title + "↑" + } + m.statTable.SetColumns(cols) + m.updateRowsInTable() + } case "enter": m.chosenStat = true - // TODO 考虑sort cursor := m.statTable.Cursor() metric := m.options.EnabledMetricTypeSet.GetFirstEnabledMetricType() if m.curConnstats != nil { @@ -184,7 +360,7 @@ func (m *model) viewStatTable() string { } s := fmt.Sprintf("\n %s Events received: %d\n\n", m.spinner.View(), totalCount) - return s + rc.BaseTableStyle.Render(m.statTable.View()) + "\n " + m.statTable.HelpView() + "\n" + return s + rc.BaseTableStyle.Render(m.statTable.View()) + "\n " + m.statTable.HelpView() + "\n" + m.additionHelp.View(sortByKeyMap) } func (m *model) viewSampleTable() string { From fd890bed673d7d981ede2b07293bfafd5bc816ac Mon Sep 17 00:00:00 2001 From: hengyoush Date: Mon, 14 Oct 2024 15:07:32 +0800 Subject: [PATCH 08/12] [Feature] support batch model --- agent/agent.go | 1 - agent/analysis/analysis.go | 35 +++++++++++++----- agent/analysis/classfier.go | 56 +++++++++-------------------- agent/analysis/common/classfier.go | 31 ++++++++++++++++ agent/analysis/common/types.go | 52 +++++++++++++++++++++++++++ agent/analysis/types.go | 16 ++++----- agent/render/common/common.go | 29 +++++++++++++++ agent/render/stat/stat.go | 58 +++++++++++++++++++++++++----- cmd/stat.go | 22 +++++++++--- 9 files changed, 231 insertions(+), 69 deletions(-) create mode 100644 agent/analysis/common/classfier.go diff --git a/agent/agent.go b/agent/agent.go index ef546e34..76beedcd 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -104,7 +104,6 @@ func SetupAgent(options ac.AgentOptions) { renderStopper := make(chan int) analyzer := analysis.CreateAnalyzer(recordsChannel, &options.AnalysisOptions, resultChannel, renderStopper, options.Ctx) go analyzer.Run() - stat.StartStatRender(ctx, resultChannel, options.AnalysisOptions) // render := render.CreateRender(resultChannel, renderStopper, analyzer.AnalysisOptions) // go render.Run() diff --git a/agent/analysis/analysis.go b/agent/analysis/analysis.go index ec220edd..6a7bc862 100644 --- a/agent/analysis/analysis.go +++ b/agent/analysis/analysis.go @@ -17,19 +17,19 @@ type aggregator struct { } func createAggregatorWithHumanReadableClassId(humanReadableClassId string, - classId ClassId, aggregateOption *analysis_common.AnalysisOptions) *aggregator { + classId analysis_common.ClassId, aggregateOption *analysis_common.AnalysisOptions) *aggregator { aggregator := createAggregator(classId, aggregateOption) aggregator.HumanReadbleClassId = humanReadableClassId return aggregator } -func createAggregator(classId ClassId, aggregateOption *analysis_common.AnalysisOptions) *aggregator { +func createAggregator(classId analysis_common.ClassId, aggregateOption *analysis_common.AnalysisOptions) *aggregator { aggregator := aggregator{} aggregator.reset(classId, aggregateOption) return &aggregator } -func (a *aggregator) reset(classId ClassId, aggregateOption *analysis_common.AnalysisOptions) { +func (a *aggregator) reset(classId analysis_common.ClassId, aggregateOption *analysis_common.AnalysisOptions) { a.AnalysisOptions = aggregateOption a.ConnStat = &ConnStat{ ClassId: classId, @@ -104,7 +104,7 @@ type Analyzer struct { Classfier *analysis_common.AnalysisOptions common.SideEnum // 那一边的统计指标TODO 根据参数自动推断 - Aggregators map[ClassId]*aggregator + Aggregators map[analysis_common.ClassId]*aggregator recordsChannel <-chan *analysis_common.AnnotatedRecord stopper <-chan int resultChannel chan<- []*ConnStat @@ -112,6 +112,7 @@ type Analyzer struct { ticker *time.Ticker tickerC <-chan time.Time ctx context.Context + recordReceived int } func CreateAnalyzer(recordsChannel <-chan *analysis_common.AnnotatedRecord, opts *analysis_common.AnalysisOptions, resultChannel chan<- []*ConnStat, renderStopper chan int, ctx context.Context) *Analyzer { @@ -121,16 +122,22 @@ func CreateAnalyzer(recordsChannel <-chan *analysis_common.AnnotatedRecord, opts analyzer := &Analyzer{ Classfier: getClassfier(opts.ClassfierType), recordsChannel: recordsChannel, - Aggregators: make(map[ClassId]*aggregator), + Aggregators: make(map[analysis_common.ClassId]*aggregator), AnalysisOptions: opts, stopper: stopper, resultChannel: resultChannel, renderStopper: renderStopper, ctx: ctx, } - - analyzer.ticker = time.NewTicker(time.Second * 1) - analyzer.tickerC = analyzer.ticker.C + opts.CurrentReceivedSamples = func() int { + return analyzer.recordReceived + } + if analyzer.AnalysisOptions.EnableBatchModel() { + analyzer.tickerC = make(<-chan time.Time) + } else { + analyzer.ticker = time.NewTicker(time.Second * 1) + analyzer.tickerC = analyzer.ticker.C + } return analyzer } @@ -143,6 +150,16 @@ func (a *Analyzer) Run() { return case record := <-a.recordsChannel: a.analyze(record) + a.recordReceived++ + if a.EnableBatchModel() && a.recordReceived == a.TargetSamples { + a.resultChannel <- a.harvest() + return + } + case <-a.AnalysisOptions.HavestSignal: + a.resultChannel <- a.harvest() + if a.AnalysisOptions.EnableBatchModel() { + return + } case <-a.tickerC: a.resultChannel <- a.harvest() } @@ -157,7 +174,7 @@ func (a *Analyzer) harvest() []*ConnStat { result = append(result, connstat) } if a.AnalysisOptions.CleanWhenHarvest { - a.Aggregators = make(map[ClassId]*aggregator) + a.Aggregators = make(map[analysis_common.ClassId]*aggregator) } return result } diff --git a/agent/analysis/classfier.go b/agent/analysis/classfier.go index dc1cc205..c30c1f27 100644 --- a/agent/analysis/classfier.go +++ b/agent/analysis/classfier.go @@ -8,56 +8,34 @@ import ( "kyanos/bpf" ) -type Classfier func(*anc.AnnotatedRecord) (ClassId, error) +type Classfier func(*anc.AnnotatedRecord) (anc.ClassId, error) type ClassIdAsHumanReadable func(*anc.AnnotatedRecord) string -var ClassfierTypeNames = map[anc.ClassfierType]string{ - None: "none", - Conn: "conn", - RemotePort: "remote-port", - LocalPort: "local-port", - RemoteIp: "remote-ip", - Protocol: "protocol", - HttpPath: "http-path", - RedisCommand: "redis-command", -} - -const ( - None anc.ClassfierType = iota - Conn - RemotePort - LocalPort - RemoteIp - Protocol - - // Http - HttpPath - - // Redis - RedisCommand -) - -type ClassId string - var classfierMap map[anc.ClassfierType]Classfier var classIdHumanReadableMap map[anc.ClassfierType]ClassIdAsHumanReadable func init() { classfierMap = make(map[anc.ClassfierType]Classfier) - classfierMap[None] = func(ar *anc.AnnotatedRecord) (ClassId, error) { return "none", nil } - classfierMap[Conn] = func(ar *anc.AnnotatedRecord) (ClassId, error) { - return ClassId(ar.ConnDesc.Identity()), nil + classfierMap[anc.None] = func(ar *anc.AnnotatedRecord) (anc.ClassId, error) { return "none", nil } + classfierMap[anc.Conn] = func(ar *anc.AnnotatedRecord) (anc.ClassId, error) { + return anc.ClassId(ar.ConnDesc.Identity()), nil + } + classfierMap[anc.RemotePort] = func(ar *anc.AnnotatedRecord) (anc.ClassId, error) { + return anc.ClassId(fmt.Sprintf("%d", ar.RemotePort)), nil + } + classfierMap[anc.LocalPort] = func(ar *anc.AnnotatedRecord) (anc.ClassId, error) { + return anc.ClassId(fmt.Sprintf("%d", ar.LocalPort)), nil + } + classfierMap[anc.RemoteIp] = func(ar *anc.AnnotatedRecord) (anc.ClassId, error) { return anc.ClassId(ar.RemoteAddr.String()), nil } + classfierMap[anc.Protocol] = func(ar *anc.AnnotatedRecord) (anc.ClassId, error) { + return anc.ClassId(fmt.Sprintf("%d", ar.Protocol)), nil } - classfierMap[RemotePort] = func(ar *anc.AnnotatedRecord) (ClassId, error) { return ClassId(fmt.Sprintf("%d", ar.RemotePort)), nil } - classfierMap[LocalPort] = func(ar *anc.AnnotatedRecord) (ClassId, error) { return ClassId(fmt.Sprintf("%d", ar.LocalPort)), nil } - classfierMap[RemoteIp] = func(ar *anc.AnnotatedRecord) (ClassId, error) { return ClassId(ar.RemoteAddr.String()), nil } - classfierMap[Protocol] = func(ar *anc.AnnotatedRecord) (ClassId, error) { return ClassId(fmt.Sprintf("%d", ar.Protocol)), nil } classIdHumanReadableMap = make(map[anc.ClassfierType]ClassIdAsHumanReadable) - classIdHumanReadableMap[Conn] = func(ar *anc.AnnotatedRecord) string { + classIdHumanReadableMap[anc.Conn] = func(ar *anc.AnnotatedRecord) string { return ar.ConnDesc.SimpleString() } - classIdHumanReadableMap[HttpPath] = func(ar *anc.AnnotatedRecord) string { + classIdHumanReadableMap[anc.HttpPath] = func(ar *anc.AnnotatedRecord) string { httpReq, ok := ar.Record.Request().(*protocol.ParsedHttpRequest) if !ok { return "__not_a_http_req__" @@ -66,7 +44,7 @@ func init() { } } - classIdHumanReadableMap[Protocol] = func(ar *anc.AnnotatedRecord) string { + classIdHumanReadableMap[anc.Protocol] = func(ar *anc.AnnotatedRecord) string { return bpf.ProtocolNamesMap[bpf.AgentTrafficProtocolT(ar.Protocol)] } } diff --git a/agent/analysis/common/classfier.go b/agent/analysis/common/classfier.go new file mode 100644 index 00000000..8c482e2a --- /dev/null +++ b/agent/analysis/common/classfier.go @@ -0,0 +1,31 @@ +package common + +var ClassfierTypeNames = map[ClassfierType]string{ + None: "none", + Conn: "conn", + RemotePort: "remote-port", + LocalPort: "local-port", + RemoteIp: "remote-ip", + Protocol: "protocol", + HttpPath: "http-path", + RedisCommand: "redis-command", + Default: "default", +} + +const ( + Default ClassfierType = iota + None + Conn + RemotePort + LocalPort + RemoteIp + Protocol + + // Http + HttpPath + + // Redis + RedisCommand +) + +type ClassId string diff --git a/agent/analysis/common/types.go b/agent/analysis/common/types.go index 8e84397f..cab1eb15 100644 --- a/agent/analysis/common/types.go +++ b/agent/analysis/common/types.go @@ -15,12 +15,64 @@ type AnalysisOptions struct { Side ac.SideEnum ClassfierType CleanWhenHarvest bool + + // Fast Inspect Options + SlowMode bool + BigRespMode bool + BigReqMode bool + TargetSamples int + CurrentReceivedSamples func() int + HavestSignal chan struct{} } func (a *AnalysisOptions) Init() { if a.SampleLimit <= 0 { a.SampleLimit = 10 } + a.HavestSignal = make(chan struct{}, 10) + + if a.EnableBatchModel() { + a.CleanWhenHarvest = true + } else { + a.CleanWhenHarvest = false + } + if a.SlowMode { + a.EnabledMetricTypeSet = MetricTypeSet{ + TotalDuration: true, + } + if a.ClassfierType == Default { + a.ClassfierType = RemoteIp + } + } else if a.BigReqMode || a.BigRespMode { + if a.BigRespMode { + a.EnabledMetricTypeSet = MetricTypeSet{ + ResponseSize: true, + } + } else { + a.EnabledMetricTypeSet = MetricTypeSet{ + RequestSize: true, + } + } + if a.ClassfierType == Default { + a.ClassfierType = RemoteIp + } + } else { + if a.ClassfierType == Default { + a.ClassfierType = Conn + } + } + // temp disable batch model + // a.disableBatchModel() +} + +func (a AnalysisOptions) EnableBatchModel() bool { + return a.SlowMode || a.BigReqMode || a.BigRespMode +} + +func (a *AnalysisOptions) disableBatchModel() { + a.SlowMode = false + a.BigReqMode = false + a.BigRespMode = false a.CleanWhenHarvest = false } diff --git a/agent/analysis/types.go b/agent/analysis/types.go index 541f8be0..9a7e544f 100644 --- a/agent/analysis/types.go +++ b/agent/analysis/types.go @@ -15,24 +15,24 @@ type ConnStat struct { SumMap map[anc.MetricType]float64 Side common.SideEnum - ClassId ClassId + ClassId anc.ClassId HumanReadbleClassId string ClassfierType anc.ClassfierType } -func (c *ConnStat) ClassIdAsHumanReadable(classId ClassId) string { +func (c *ConnStat) ClassIdAsHumanReadable(classId anc.ClassId) string { switch c.ClassfierType { - case None: + case anc.None: return "All" - case Conn: + case anc.Conn: return c.HumanReadbleClassId - case RemotePort: + case anc.RemotePort: fallthrough - case LocalPort: + case anc.LocalPort: fallthrough - case RemoteIp: + case anc.RemoteIp: return string(classId) - case Protocol: + case anc.Protocol: return c.HumanReadbleClassId default: return string(classId) diff --git a/agent/render/common/common.go b/agent/render/common/common.go index a2196e55..642eceb6 100644 --- a/agent/render/common/common.go +++ b/agent/render/common/common.go @@ -7,6 +7,7 @@ import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/lucasb-eyer/go-colorful" ) type KeyMap map[string]key.Binding @@ -46,3 +47,31 @@ var MetricTypeUnit = map[common.MetricType]string{ common.BlackBoxDuration: "ms", common.ReadFromSocketBufferDuration: "ms", } + +func ColorGrid(xSteps, ySteps int) [][]string { + x0y0, _ := colorful.Hex("#F25D94") + x1y0, _ := colorful.Hex("#EDFF82") + x0y1, _ := colorful.Hex("#643AFF") + x1y1, _ := colorful.Hex("#14F9D5") + + x0 := make([]colorful.Color, ySteps) + for i := range x0 { + x0[i] = x0y0.BlendLuv(x0y1, float64(i)/float64(ySteps)) + } + + x1 := make([]colorful.Color, ySteps) + for i := range x1 { + x1[i] = x1y0.BlendLuv(x1y1, float64(i)/float64(ySteps)) + } + + grid := make([][]string, ySteps) + for x := 0; x < ySteps; x++ { + y0 := x0[x] + grid[x] = make([]string, xSteps) + for y := 0; y < xSteps; y++ { + grid[x][y] = y0.BlendLuv(x1[x], float64(y)/float64(xSteps)).Hex() + } + } + + return grid +} diff --git a/agent/render/stat/stat.go b/agent/render/stat/stat.go index a1f244cd..0acf4579 100644 --- a/agent/render/stat/stat.go +++ b/agent/render/stat/stat.go @@ -97,8 +97,9 @@ type model struct { spinner spinner.Model additionHelp help.Model - connstats *[]*analysis.ConnStat - curConnstats *[]*analysis.ConnStat + connstats *[]*analysis.ConnStat + curConnstats *[]*analysis.ConnStat + resultChannel <-chan []*analysis.ConnStat options common.AnalysisOptions @@ -127,7 +128,7 @@ func initTable(options common.AnalysisOptions) table.Model { unit := rc.MetricTypeUnit[metric] columns := []table.Column{ {Title: "id", Width: 3}, - {Title: "name", Width: 40}, + {Title: common.ClassfierTypeNames[options.ClassfierType], Width: 40}, {Title: fmt.Sprintf("max(%s)", unit), Width: 10}, {Title: fmt.Sprintf("avg(%s)", unit), Width: 10}, {Title: fmt.Sprintf("p50(%s)", unit), Width: 10}, @@ -264,14 +265,13 @@ func (m *model) sortConnstats(connstats *[]*analysis.ConnStat) { return cmp.Compare(c1.ClassId, c2.ClassId) } }) - } } func (m *model) updateStatTable(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { - case spinner.TickMsg: + case spinner.TickMsg, rc.TickMsg: m.updateRowsInTable() m.spinner, cmd = m.spinner.Update(msg) return m, cmd @@ -279,6 +279,17 @@ func (m *model) updateStatTable(msg tea.Msg) (tea.Model, tea.Cmd) { m.windownSizeMsg = msg case tea.KeyMsg: switch msg.String() { + case "c": + if m.options.EnableBatchModel() { + if len(m.statTable.Rows()) == 0 { + m.options.HavestSignal <- struct{}{} + connstats := <-m.resultChannel + m.connstats = &connstats + m.updateRowsInTable() + } + break + } + fallthrough case "esc", "q", "ctrl+c": return m, tea.Quit case "1", "2", "3", "4", "5", "6", "7", "8": @@ -358,9 +369,32 @@ func (m *model) viewStatTable() string { totalCount += each.Count } } - s := fmt.Sprintf("\n %s Events received: %d\n\n", m.spinner.View(), totalCount) + var s string - return s + rc.BaseTableStyle.Render(m.statTable.View()) + "\n " + m.statTable.HelpView() + "\n" + m.additionHelp.View(sortByKeyMap) + // s = fmt.Sprintf("\n %s Events received: %d\n\n", m.spinner.View(), totalCount) + // s += rc.BaseTableStyle.Render(m.statTable.View()) + "\n " + m.statTable.HelpView() + "\n" + m.additionHelp.View(sortByKeyMap) + if m.options.EnableBatchModel() { + + var titleStyle = lipgloss.NewStyle(). + MarginLeft(1). + MarginRight(5). + Padding(0, 1). + Italic(true). + Bold(false). + Foreground(lipgloss.Color("#FFF7DB")).Background(lipgloss.Color(rc.ColorGrid(1, 5)[2][0])) + + if len(m.statTable.Rows()) > 0 { + s += fmt.Sprintf("\n %s \n\n", titleStyle.Render(" Colleted events are here! ")) + s += rc.BaseTableStyle.Render(m.statTable.View()) + "\n " + m.statTable.HelpView() + "\n\n " + m.additionHelp.View(sortByKeyMap) + } else { + s += fmt.Sprintf("\n %s Collecting %d/%d\n\n %s\n\n", m.spinner.View(), m.options.CurrentReceivedSamples(), m.options.TargetSamples, + titleStyle.Render("Press `c` to display collected events")) + } + } else { + s = fmt.Sprintf("\n %s Events received: %d\n\n", m.spinner.View(), totalCount) + s += rc.BaseTableStyle.Render(m.statTable.View()) + "\n " + m.statTable.HelpView() + "\n\n " + m.additionHelp.View(sortByKeyMap) + } + return s } func (m *model) viewSampleTable() string { @@ -376,6 +410,8 @@ func (m *model) View() string { } func StartStatRender(ctx context.Context, ch <-chan []*analysis.ConnStat, options common.AnalysisOptions) { m := NewModel(options).(*model) + + prog := tea.NewProgram(m, tea.WithContext(ctx), tea.WithAltScreen()) go func(mod *model, channel <-chan []*analysis.ConnStat) { for { select { @@ -385,11 +421,17 @@ func StartStatRender(ctx context.Context, ch <-chan []*analysis.ConnStat, option lock.Lock() m.connstats = &r lock.Unlock() + prog.Send(rc.TickMsg{}) + if options.EnableBatchModel() { + return + } } } }(m, ch) + m.resultChannel = ch + m.sortBy = avg + m.reverse = true - prog := tea.NewProgram(m, tea.WithContext(ctx), tea.WithAltScreen()) if _, err := prog.Run(); err != nil { fmt.Println("Error running program:", err) os.Exit(1) diff --git a/cmd/stat.go b/cmd/stat.go index f1652ced..820d184b 100644 --- a/cmd/stat.go +++ b/cmd/stat.go @@ -2,7 +2,6 @@ package cmd import ( "fmt" - "kyanos/agent/analysis" anc "kyanos/agent/analysis/common" "slices" @@ -47,6 +46,10 @@ sudo kyanos stat http --metrics tqp var enabledMetricsString string var sampleCount int var groupBy string +var slowMode bool +var bigRespModel bool +var bigReqModel bool +var targetSamples int var SUPPORTED_METRICS = []byte{'t', 'q', 'p', 'n', 's'} func validateEnabledMetricsString() error { @@ -82,11 +85,16 @@ func createAnalysisOptions() (anc.AnalysisOptions, error) { } options.SampleLimit = sampleCount - for key, value := range analysis.ClassfierTypeNames { + for key, value := range anc.ClassfierTypeNames { if value == groupBy { options.ClassfierType = key } } + options.SlowMode = slowMode + options.BigReqMode = bigReqModel + options.BigRespMode = bigRespModel + options.TargetSamples = targetSamples + return options, nil } func init() { @@ -96,15 +104,21 @@ func init() { p: response size, n: network device latency, s: time spent reading from the socket buffer`) - statCmd.PersistentFlags().IntVarP(&sampleCount, "samples", "s", 0, + statCmd.PersistentFlags().IntVarP(&sampleCount, "samples-limit", "s", 0, "Specify the number of samples to be attached for each result.\n"+ "By default, only a summary is output.\n"+ "refer to the '--full-body' option.") - statCmd.PersistentFlags().StringVarP(&groupBy, "group-by", "g", "remote-ip", + statCmd.PersistentFlags().StringVarP(&groupBy, "group-by", "g", "default", "Specify aggregation dimension: \n"+ "('conn', 'local-port', 'remote-port', 'remote-ip', 'protocol', 'http-path', 'none')\n"+ "note: 'none' is aggregate all req-resp pair together") + // inspect options + statCmd.PersistentFlags().BoolVar(&slowMode, "slow", false, "Find slowest records") + statCmd.PersistentFlags().BoolVar(&bigReqModel, "bigreq", false, "Find biggest request size records") + statCmd.PersistentFlags().BoolVar(&bigRespModel, "bigresp", false, "Find biggest response size records") + statCmd.PersistentFlags().IntVar(&targetSamples, "target", 10, "") + // common statCmd.PersistentFlags().Float64("latency", 0, "Filter based on request response time") statCmd.PersistentFlags().Int64("req-size", 0, "Filter based on request bytes size") From 97cb0e33f23902a6d94f750061bf04c6b222d697 Mon Sep 17 00:00:00 2001 From: hengyoush Date: Tue, 15 Oct 2024 00:07:51 +0800 Subject: [PATCH 09/12] [Feature] Support watch stable sortable --- agent/analysis/analysis.go | 1 + agent/analysis/common/types.go | 3 +- agent/render/common/common.go | 7 + agent/render/stat/stat.go | 33 ++-- agent/render/watch/watch_render.go | 281 ++++++++++++++++++++++++----- 5 files changed, 264 insertions(+), 61 deletions(-) diff --git a/agent/analysis/analysis.go b/agent/analysis/analysis.go index 6a7bc862..0e707675 100644 --- a/agent/analysis/analysis.go +++ b/agent/analysis/analysis.go @@ -142,6 +142,7 @@ func CreateAnalyzer(recordsChannel <-chan *analysis_common.AnnotatedRecord, opts } func (a *Analyzer) Run() { + defer close(a.resultChannel) for { select { // case <-a.stopper: diff --git a/agent/analysis/common/types.go b/agent/analysis/common/types.go index cab1eb15..8842ecd3 100644 --- a/agent/analysis/common/types.go +++ b/agent/analysis/common/types.go @@ -116,7 +116,8 @@ func (m MetricTypeSet) GetFirstEnabledMetricType() MetricType { type MetricExtract[T MetricValueType] func(*AnnotatedRecord) T const ( - ResponseSize MetricType = iota + Start MetricType = iota + ResponseSize RequestSize TotalDuration BlackBoxDuration diff --git a/agent/render/common/common.go b/agent/render/common/common.go index 642eceb6..1fc38ae4 100644 --- a/agent/render/common/common.go +++ b/agent/render/common/common.go @@ -75,3 +75,10 @@ func ColorGrid(xSteps, ySteps int) [][]string { return grid } + +type SortBy int8 + +type SortOption struct { + sortBy SortBy + reverse bool +} diff --git a/agent/render/stat/stat.go b/agent/render/stat/stat.go index 0acf4579..9da1cae4 100644 --- a/agent/render/stat/stat.go +++ b/agent/render/stat/stat.go @@ -24,7 +24,6 @@ import ( var lock *sync.Mutex = &sync.Mutex{} -type SortBy int8 type statTableKeyMap rc.KeyMap var sortByKeyMap = statTableKeyMap{ @@ -79,7 +78,7 @@ func (k statTableKeyMap) FullHelp() [][]key.Binding { } const ( - none SortBy = iota + none rc.SortBy = iota name max avg @@ -107,7 +106,7 @@ type model struct { windownSizeMsg tea.WindowSizeMsg - sortBy SortBy + sortBy rc.SortBy reverse bool } @@ -284,8 +283,10 @@ func (m *model) updateStatTable(msg tea.Msg) (tea.Model, tea.Cmd) { if len(m.statTable.Rows()) == 0 { m.options.HavestSignal <- struct{}{} connstats := <-m.resultChannel - m.connstats = &connstats - m.updateRowsInTable() + if connstats != nil { + m.connstats = &connstats + m.updateRowsInTable() + } } break } @@ -297,7 +298,7 @@ func (m *model) updateStatTable(msg tea.Msg) (tea.Model, tea.Cmd) { if err == nil && (i >= int(none) && i < int(end)) && (i >= 0 && i < len(m.statTable.Columns())) { prevSortBy := m.sortBy - m.sortBy = SortBy(i) + m.sortBy = rc.SortBy(i) m.reverse = !m.reverse cols := m.statTable.Columns() if prevSortBy != none { @@ -323,7 +324,7 @@ func (m *model) updateStatTable(msg tea.Msg) (tea.Model, tea.Cmd) { m.sampleModel = watch.NewModel(watch.WatchOptions{ WideOutput: true, StaticRecord: true, - }, &records, m.windownSizeMsg) + }, &records, m.windownSizeMsg, metric, true) } return m, m.sampleModel.Init() @@ -418,12 +419,14 @@ func StartStatRender(ctx context.Context, ch <-chan []*analysis.ConnStat, option case <-ctx.Done(): return case r := <-ch: - lock.Lock() - m.connstats = &r - lock.Unlock() - prog.Send(rc.TickMsg{}) - if options.EnableBatchModel() { - return + if r != nil { + lock.Lock() + m.connstats = &r + lock.Unlock() + prog.Send(rc.TickMsg{}) + if options.EnableBatchModel() { + return + } } } } @@ -437,3 +440,7 @@ func StartStatRender(ctx context.Context, ch <-chan []*analysis.ConnStat, option os.Exit(1) } } + +func (m *model) SortBy() rc.SortBy { + return m.sortBy +} diff --git a/agent/render/watch/watch_render.go b/agent/render/watch/watch_render.go index a3d18431..c9f7d07a 100644 --- a/agent/render/watch/watch_render.go +++ b/agent/render/watch/watch_render.go @@ -1,6 +1,7 @@ package watch import ( + "cmp" "context" "fmt" "kyanos/agent/analysis/common" @@ -24,10 +25,133 @@ import ( var lock *sync.Mutex = &sync.Mutex{} -type WatchRender struct { - model *model +type watchCol struct { + name string + cmp func(c1 *common.AnnotatedRecord, c2 *common.AnnotatedRecord, reverse bool) int + data func(c *common.AnnotatedRecord) string + width int + metricType common.MetricType } +var ( + cols []watchCol + sortKeyMap watchKeyMap + idCol watchCol = watchCol{ + name: "id", + cmp: nil, + width: 5, + } + connCol watchCol = watchCol{ + name: "Connection", + cmp: func(c1, c2 *common.AnnotatedRecord, reverse bool) int { + if reverse { + return cmp.Compare(c2.ConnDesc.SimpleString(), c1.ConnDesc.SimpleString()) + } else { + return cmp.Compare(c1.ConnDesc.SimpleString(), c2.ConnDesc.SimpleString()) + } + }, + data: func(c *common.AnnotatedRecord) string { return c.ConnDesc.SimpleString() }, + width: 40, + } + protoCol watchCol = watchCol{ + name: "Proto", + cmp: func(c1, c2 *common.AnnotatedRecord, reverse bool) int { + if reverse { + return cmp.Compare(c2.Protocol, c1.Protocol) + } else { + return cmp.Compare(c1.Protocol, c2.Protocol) + } + }, + data: func(c *common.AnnotatedRecord) string { + return bpf.ProtocolNamesMap[bpf.AgentTrafficProtocolT(c.ConnDesc.Protocol)] + }, + width: 6, + } + totalTimeCol watchCol = watchCol{ + name: "TotalTime", + cmp: func(c1, c2 *common.AnnotatedRecord, reverse bool) int { + if reverse { + return cmp.Compare(c2.TotalDuration, c1.TotalDuration) + } else { + return cmp.Compare(c1.TotalDuration, c2.TotalDuration) + } + }, + data: func(r *common.AnnotatedRecord) string { + return fmt.Sprintf("%.2f", c.ConvertDurationToMillisecondsIfNeeded(r.TotalDuration, false)) + }, + width: 10, + metricType: common.TotalDuration, + } + reqSizeCol watchCol = watchCol{ + name: "ReqSize", + cmp: func(c1, c2 *common.AnnotatedRecord, reverse bool) int { + if reverse { + return cmp.Compare(c2.ReqSize, c1.ReqSize) + } else { + return cmp.Compare(c1.ReqSize, c2.ReqSize) + } + }, + data: func(c *common.AnnotatedRecord) string { return fmt.Sprintf("%d", c.ReqSize) }, + width: 10, + metricType: common.RequestSize, + } + respSizeCol watchCol = watchCol{ + name: "RespSize", + cmp: func(c1, c2 *common.AnnotatedRecord, reverse bool) int { + if reverse { + return cmp.Compare(c2.RespSize, c1.RespSize) + } else { + return cmp.Compare(c1.RespSize, c2.RespSize) + } + }, + data: func(c *common.AnnotatedRecord) string { return fmt.Sprintf("%d", c.RespSize) }, + width: 10, + metricType: common.ResponseSize, + } + processCol watchCol = watchCol{ + name: "Process", + cmp: func(c1, c2 *common.AnnotatedRecord, reverse bool) int { + if reverse { + return cmp.Compare(c2.Pid, c1.Pid) + } else { + return cmp.Compare(c1.Pid, c2.Pid) + } + }, + data: func(r *common.AnnotatedRecord) string { return c.GetPidCmdString(int32(r.Pid)) }, + width: 15, + } + netInternalCol watchCol = watchCol{ + name: "Net/Internal", + cmp: func(c1, c2 *common.AnnotatedRecord, reverse bool) int { + if reverse { + return cmp.Compare(c2.BlackBoxDuration, c1.BlackBoxDuration) + } else { + return cmp.Compare(c1.BlackBoxDuration, c2.BlackBoxDuration) + } + }, + data: func(r *common.AnnotatedRecord) string { + return fmt.Sprintf("%.2f", c.ConvertDurationToMillisecondsIfNeeded(r.BlackBoxDuration, false)) + }, + width: 13, + metricType: common.BlackBoxDuration, + } + readSocketCol watchCol = watchCol{ + name: "ReadSocketTime", + cmp: func(c1, c2 *common.AnnotatedRecord, reverse bool) int { + if reverse { + return cmp.Compare(c2.ReadFromSocketBufferDuration, c1.ReadFromSocketBufferDuration) + } else { + return cmp.Compare(c1.ReadFromSocketBufferDuration, c2.ReadFromSocketBufferDuration) + } + }, + data: func(r *common.AnnotatedRecord) string { + return fmt.Sprintf("%.2f", c.ConvertDurationToMillisecondsIfNeeded(r.ReadFromSocketBufferDuration, false)) + }, + width: 15, + metricType: common.ReadFromSocketBufferDuration, + } +) + type model struct { table table.Model viewport viewport.Model @@ -39,9 +163,12 @@ type model struct { wide bool staticRecord bool initialWindownSizeMsg tea.WindowSizeMsg + sortBy rc.SortBy + reverse bool } -func NewModel(options WatchOptions, records *[]*common.AnnotatedRecord, initialWindownSizeMsg tea.WindowSizeMsg) tea.Model { +func NewModel(options WatchOptions, records *[]*common.AnnotatedRecord, initialWindownSizeMsg tea.WindowSizeMsg, + sortBy common.MetricType, reverse bool) tea.Model { var m tea.Model = &model{ table: initTable(options), viewport: viewport.New(100, 100), @@ -54,31 +181,54 @@ func NewModel(options WatchOptions, records *[]*common.AnnotatedRecord, initialW staticRecord: options.StaticRecord, initialWindownSizeMsg: initialWindownSizeMsg, } + if sortBy != common.NoneType { + for idx, col := range cols { + if col.metricType == sortBy { + m.(*model).sortBy = rc.SortBy(idx) + m.(*model).reverse = !reverse + m.(*model).updateTableSortBy(idx) + break + } + } + } return m } -func initTable(options WatchOptions) table.Model { - columns := []table.Column{ - {Title: "id", Width: 3}, - {Title: "Connection", Width: 40}, - {Title: "Proto", Width: 5}, - {Title: "TotalTime", Width: 10}, - {Title: "ReqSize", Width: 7}, - {Title: "RespSize", Width: 8}, +func initWatchCols(wide bool) { + cols = make([]watchCol, 0) + cols = []watchCol{idCol, connCol, protoCol, totalTimeCol, reqSizeCol, respSizeCol} + if wide { + cols = slices.Insert(cols, 1, processCol) + } + cols = append(cols, netInternalCol, readSocketCol) +} + +func initDetailViewKeyMap(cols []watchCol) { + sortKeyMap = watchKeyMap{} + for idx, col := range cols { + if idx == 0 { + continue + } + idxStr := fmt.Sprintf("%d", idx) + sortKeyMap[idxStr] = key.NewBinding( + key.WithKeys(idxStr), + key.WithHelp(idxStr, fmt.Sprintf("sort by %s", col.name)), + ) } - if options.WideOutput { - columns = slices.Insert(columns, 1, table.Column{ - Title: "Proc", Width: 20, +} + +func initTable(options WatchOptions) table.Model { + initWatchCols(options.WideOutput) + columns := []table.Column{} + for _, eachCol := range cols { + columns = append(columns, table.Column{ + Title: eachCol.name, + Width: eachCol.width, }) - columns = append(columns, []table.Column{ - {Title: "Net/Internal", Width: 15}, - {Title: "ReadSocket", Width: 12}, - }...) } - rows := []table.Row{} t := table.New( table.WithColumns(columns), - table.WithRows(rows), + table.WithRows([]table.Row{}), table.WithFocused(true), table.WithHeight(7), // table.WithWidth(96), @@ -107,35 +257,31 @@ func (m *model) Init() tea.Cmd { } } +func (m *model) sortConnstats(connstats *[]*common.AnnotatedRecord) { + slices.SortFunc(*connstats, func(c1, c2 *common.AnnotatedRecord) int { + col := cols[m.sortBy] + return col.cmp(c1, c2, m.reverse) + }) +} func (m *model) updateRowsInTable() { - rows := m.table.Rows() + lock.Lock() + defer lock.Unlock() + rows := make([]table.Row, 0) if len(rows) < len(*m.records) { - records := (*m.records)[len(rows):] - idx := len(rows) + 1 + // records := (*m.records)[len(rows):] + m.sortConnstats(m.records) + records := (*m.records) + idx := 1 for i, record := range records { var row table.Row - if m.wide { - row = table.Row{ - fmt.Sprintf("%d", i+idx), - c.GetPidCmdString(int32(record.Pid)), - record.ConnDesc.SimpleString(), - bpf.ProtocolNamesMap[bpf.AgentTrafficProtocolT(record.ConnDesc.Protocol)], - fmt.Sprintf("%.2f", c.ConvertDurationToMillisecondsIfNeeded(record.TotalDuration, false)), - fmt.Sprintf("%d", record.ReqSize), - fmt.Sprintf("%d", record.RespSize), - fmt.Sprintf("%.2f", c.ConvertDurationToMillisecondsIfNeeded(record.BlackBoxDuration, false)), - fmt.Sprintf("%.2f", c.ConvertDurationToMillisecondsIfNeeded(record.ReadFromSocketBufferDuration, false)), - } - } else { - row = table.Row{ - fmt.Sprintf("%d", i+idx), - record.ConnDesc.SimpleString(), - bpf.ProtocolNamesMap[bpf.AgentTrafficProtocolT(record.ConnDesc.Protocol)], - fmt.Sprintf("%.2f", c.ConvertDurationToMillisecondsIfNeeded(record.TotalDuration, false)), - fmt.Sprintf("%d", record.ReqSize), - fmt.Sprintf("%d", record.RespSize), + for colIdx := range m.table.Columns() { + if colIdx == 0 { + row = append(row, fmt.Sprintf("%d", i+idx)) + } else { + row = append(row, cols[colIdx].data(record)) } } + rows = append(rows, row) } m.table.SetRows(rows) @@ -146,8 +292,6 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { case spinner.TickMsg, rc.TickMsg: - lock.Lock() - defer lock.Unlock() m.updateRowsInTable() m.spinner, cmd = m.spinner.Update(msg) return m, cmd @@ -165,6 +309,13 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case "q", "ctrl+c": return m, tea.Quit + case "1", "2", "3", "4", "5", "6", "7", "8": + i, err := strconv.Atoi(msg.String()) + if !m.chosen { + if err == nil { + m.updateTableSortBy(i) + } + } case "n", "p": if !m.chosen { break @@ -214,6 +365,28 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } +func (m *model) updateTableSortBy(newSortBy int) { + if newSortBy > 0 && newSortBy < len(cols) { + prevSortBy := m.sortBy + m.sortBy = rc.SortBy(newSortBy) + m.reverse = !m.reverse + cols := m.table.Columns() + if prevSortBy != 0 { + col := &cols[prevSortBy] + col.Title = strings.TrimRight(col.Title, "↑") + col.Title = strings.TrimRight(col.Title, "↓") + } + col := &cols[m.sortBy] + if m.reverse { + col.Title = col.Title + "↓" + } else { + col.Title = col.Title + "↑" + } + m.table.SetColumns(cols) + m.updateRowsInTable() + } +} + func (m *model) updateDetailViewPortSize(msg tea.WindowSizeMsg) { headerHeight := lipgloss.Height(m.headerView()) footerHeight := lipgloss.Height(m.footerView()) @@ -297,12 +470,22 @@ func (k watchKeyMap) ShortHelp() []key.Binding { } func (k watchKeyMap) FullHelp() [][]key.Binding { - return [][]key.Binding{{detailViewKeyMap["n"], detailViewKeyMap["p"]}} + result := [][]key.Binding{} + result = append(result, []key.Binding{detailViewKeyMap["n"], detailViewKeyMap["p"]}) + sortkeys := []key.Binding{} + for idx := range cols { + if idx == 0 { + continue + } + sortkeys = append(sortkeys, sortKeyMap[fmt.Sprintf("%d", idx)]) + } + result = append(result, sortkeys) + return result } func RunWatchRender(ctx context.Context, ch chan *common.AnnotatedRecord, options WatchOptions) { records := &[]*common.AnnotatedRecord{} - m := NewModel(options, records, tea.WindowSizeMsg{}).(*model) + m := NewModel(options, records, tea.WindowSizeMsg{}, common.NoneType, false).(*model) if !options.StaticRecord { go func(mod *model, channel chan *common.AnnotatedRecord) { for { @@ -323,3 +506,7 @@ func RunWatchRender(ctx context.Context, ch chan *common.AnnotatedRecord, option os.Exit(1) } } + +func (m *model) SortBy() rc.SortBy { + return m.sortBy +} From ced6de47453b28e040d1d98fec476d471bf508d7 Mon Sep 17 00:00:00 2001 From: hengyoush Date: Tue, 15 Oct 2024 00:50:03 +0800 Subject: [PATCH 10/12] fix. fix npe --- agent/render/watch/watch_render.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/agent/render/watch/watch_render.go b/agent/render/watch/watch_render.go index c9f7d07a..e5e70120 100644 --- a/agent/render/watch/watch_render.go +++ b/agent/render/watch/watch_render.go @@ -258,10 +258,12 @@ func (m *model) Init() tea.Cmd { } func (m *model) sortConnstats(connstats *[]*common.AnnotatedRecord) { - slices.SortFunc(*connstats, func(c1, c2 *common.AnnotatedRecord) int { - col := cols[m.sortBy] - return col.cmp(c1, c2, m.reverse) - }) + col := cols[m.sortBy] + if m.sortBy > 0 && col.cmp != nil { + slices.SortFunc(*connstats, func(c1, c2 *common.AnnotatedRecord) int { + return col.cmp(c1, c2, m.reverse) + }) + } } func (m *model) updateRowsInTable() { lock.Lock() From 7d0367ea0e01e579d0f2b7179db9bf2f5d95b703 Mon Sep 17 00:00:00 2001 From: hengyoush Date: Tue, 15 Oct 2024 00:50:24 +0800 Subject: [PATCH 11/12] fix. ip filter in bpf --- bpf/agent_x86_bpfel.go | 12 +++++++----- bpf/agentlagacykernel310_x86_bpfel.go | 6 +++--- bpf/gen.go | 2 +- bpf/loader/loader.go | 25 ++++++++++++------------ bpf/pktlatency.bpf.c | 23 +++++++++++----------- common/utils.go | 28 +++++++++++++++++++++++++++ common/utils_test.go | 5 +++++ 7 files changed, 69 insertions(+), 32 deletions(-) diff --git a/bpf/agent_x86_bpfel.go b/bpf/agent_x86_bpfel.go index 2e6cf2a2..46b82cfe 100644 --- a/bpf/agent_x86_bpfel.go +++ b/bpf/agent_x86_bpfel.go @@ -45,7 +45,7 @@ type AgentConnInfoT struct { Sin6Family uint16 Sin6Port uint16 Sin6Flowinfo uint32 - Sin6Addr struct{ In6U struct{ U6Addr8 [16]uint8 } } + Sin6Addr AgentIn6Addr Sin6ScopeId uint32 } } @@ -54,7 +54,7 @@ type AgentConnInfoT struct { Sin6Family uint16 Sin6Port uint16 Sin6Flowinfo uint32 - Sin6Addr struct{ In6U struct{ U6Addr8 [16]uint8 } } + Sin6Addr AgentIn6Addr Sin6ScopeId uint32 } } @@ -97,6 +97,8 @@ const ( AgentEndpointRoleTKRoleUnknown AgentEndpointRoleT = 4 ) +type AgentIn6Addr struct{ In6U struct{ U6Addr8 [16]uint8 } } + type AgentKernEvt struct { FuncName [16]int8 Ts uint64 @@ -287,7 +289,7 @@ type AgentMapSpecs struct { ControlValues *ebpf.MapSpec `ebpf:"control_values"` EnabledLocalIpv4Map *ebpf.MapSpec `ebpf:"enabled_local_ipv4_map"` EnabledLocalPortMap *ebpf.MapSpec `ebpf:"enabled_local_port_map"` - EnabledRemoteIpv4Map *ebpf.MapSpec `ebpf:"enabled_remote_ipv4_map"` + EnabledRemoteIpMap *ebpf.MapSpec `ebpf:"enabled_remote_ip_map"` EnabledRemotePortMap *ebpf.MapSpec `ebpf:"enabled_remote_port_map"` FilterMntnsMap *ebpf.MapSpec `ebpf:"filter_mntns_map"` FilterNetnsMap *ebpf.MapSpec `ebpf:"filter_netns_map"` @@ -339,7 +341,7 @@ type AgentMaps struct { ControlValues *ebpf.Map `ebpf:"control_values"` EnabledLocalIpv4Map *ebpf.Map `ebpf:"enabled_local_ipv4_map"` EnabledLocalPortMap *ebpf.Map `ebpf:"enabled_local_port_map"` - EnabledRemoteIpv4Map *ebpf.Map `ebpf:"enabled_remote_ipv4_map"` + EnabledRemoteIpMap *ebpf.Map `ebpf:"enabled_remote_ip_map"` EnabledRemotePortMap *ebpf.Map `ebpf:"enabled_remote_port_map"` FilterMntnsMap *ebpf.Map `ebpf:"filter_mntns_map"` FilterNetnsMap *ebpf.Map `ebpf:"filter_netns_map"` @@ -374,7 +376,7 @@ func (m *AgentMaps) Close() error { m.ControlValues, m.EnabledLocalIpv4Map, m.EnabledLocalPortMap, - m.EnabledRemoteIpv4Map, + m.EnabledRemoteIpMap, m.EnabledRemotePortMap, m.FilterMntnsMap, m.FilterNetnsMap, diff --git a/bpf/agentlagacykernel310_x86_bpfel.go b/bpf/agentlagacykernel310_x86_bpfel.go index 44b4010f..c81bba46 100644 --- a/bpf/agentlagacykernel310_x86_bpfel.go +++ b/bpf/agentlagacykernel310_x86_bpfel.go @@ -275,7 +275,7 @@ type AgentLagacyKernel310MapSpecs struct { ControlValues *ebpf.MapSpec `ebpf:"control_values"` EnabledLocalIpv4Map *ebpf.MapSpec `ebpf:"enabled_local_ipv4_map"` EnabledLocalPortMap *ebpf.MapSpec `ebpf:"enabled_local_port_map"` - EnabledRemoteIpv4Map *ebpf.MapSpec `ebpf:"enabled_remote_ipv4_map"` + EnabledRemoteIpMap *ebpf.MapSpec `ebpf:"enabled_remote_ip_map"` EnabledRemotePortMap *ebpf.MapSpec `ebpf:"enabled_remote_port_map"` FilterMntnsMap *ebpf.MapSpec `ebpf:"filter_mntns_map"` FilterNetnsMap *ebpf.MapSpec `ebpf:"filter_netns_map"` @@ -327,7 +327,7 @@ type AgentLagacyKernel310Maps struct { ControlValues *ebpf.Map `ebpf:"control_values"` EnabledLocalIpv4Map *ebpf.Map `ebpf:"enabled_local_ipv4_map"` EnabledLocalPortMap *ebpf.Map `ebpf:"enabled_local_port_map"` - EnabledRemoteIpv4Map *ebpf.Map `ebpf:"enabled_remote_ipv4_map"` + EnabledRemoteIpMap *ebpf.Map `ebpf:"enabled_remote_ip_map"` EnabledRemotePortMap *ebpf.Map `ebpf:"enabled_remote_port_map"` FilterMntnsMap *ebpf.Map `ebpf:"filter_mntns_map"` FilterNetnsMap *ebpf.Map `ebpf:"filter_netns_map"` @@ -362,7 +362,7 @@ func (m *AgentLagacyKernel310Maps) Close() error { m.ControlValues, m.EnabledLocalIpv4Map, m.EnabledLocalPortMap, - m.EnabledRemoteIpv4Map, + m.EnabledRemoteIpMap, m.EnabledRemotePortMap, m.FilterMntnsMap, m.FilterNetnsMap, diff --git a/bpf/gen.go b/bpf/gen.go index 2580b247..939c8f3d 100644 --- a/bpf/gen.go +++ b/bpf/gen.go @@ -1,6 +1,6 @@ package bpf -//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -type process_exit_event -type process_exec_event -type kern_evt_ssl_data -type conn_id_s_t -type sock_key -type control_value_index_t -type kern_evt -type kern_evt_data -type conn_evt_t -type conn_type_t -type conn_info_t -type endpoint_role_t -type traffic_direction_t -type traffic_protocol_t -type step_t -target amd64 Agent ./pktlatency.bpf.c -- -I./ -I$OUTPUT -I../libbpf/include/uapi -I../vmlinux/x86/ +//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -type in6_addr -type process_exit_event -type process_exec_event -type kern_evt_ssl_data -type conn_id_s_t -type sock_key -type control_value_index_t -type kern_evt -type kern_evt_data -type conn_evt_t -type conn_type_t -type conn_info_t -type endpoint_role_t -type traffic_direction_t -type traffic_protocol_t -type step_t -target amd64 Agent ./pktlatency.bpf.c -- -I./ -I$OUTPUT -I../libbpf/include/uapi -I../vmlinux/x86/ //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -type conn_id_s_t -type sock_key -type control_value_index_t -type kern_evt -type kern_evt_data -type conn_evt_t -type conn_type_t -type conn_info_t -type endpoint_role_t -type traffic_direction_t -type traffic_protocol_t -type step_t -cflags "-D LAGACY_KERNEL_310" -target amd64 AgentLagacyKernel310 ./pktlatency.bpf.c -- -I./ -I$OUTPUT -I../libbpf/include/uapi -I../vmlinux/x86/ //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -type conn_id_s_t -type sock_key -type control_value_index_t -type kern_evt -type kern_evt_data -type conn_evt_t -type conn_type_t -type conn_info_t -type endpoint_role_t -type traffic_direction_t -type traffic_protocol_t -type step_t -target amd64 Openssl102a ./openssl_1_0_2a.bpf.c -- -I./ -I$OUTPUT -I../libbpf/include/uapi -I../vmlinux/x86/ //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -type conn_id_s_t -type sock_key -type control_value_index_t -type kern_evt -type kern_evt_data -type conn_evt_t -type conn_type_t -type conn_info_t -type endpoint_role_t -type traffic_direction_t -type traffic_protocol_t -type step_t -target amd64 Openssl110a ./openssl_1_1_0a.bpf.c -- -I./ -I$OUTPUT -I../libbpf/include/uapi -I../vmlinux/x86/ diff --git a/bpf/loader/loader.go b/bpf/loader/loader.go index f7ee285f..4d67d455 100644 --- a/bpf/loader/loader.go +++ b/bpf/loader/loader.go @@ -12,6 +12,7 @@ import ( "kyanos/agent/uprobe" "kyanos/bpf" "kyanos/common" + "net" "os" "path/filepath" "slices" @@ -326,7 +327,7 @@ var socketFilterSpec = &ebpf.ProgramSpec{ func setAndValidateParameters(ctx context.Context, options *ac.AgentOptions) bool { var controlValues *ebpf.Map = bpf.GetMapFromObjs(bpf.Objs, "ControlValues") var enabledRemotePortMap *ebpf.Map = bpf.GetMapFromObjs(bpf.Objs, "EnabledRemotePortMap") - var enabledRemoteIpv4Map *ebpf.Map = bpf.GetMapFromObjs(bpf.Objs, "EnabledRemoteIpv4Map") + var enabledRemoteIpMap *ebpf.Map = bpf.GetMapFromObjs(bpf.Objs, "EnabledRemoteIpMap") var enabledLocalPortMap *ebpf.Map = bpf.GetMapFromObjs(bpf.Objs, "EnabledLocalPortMap") var filterPidMap *ebpf.Map = bpf.GetMapFromObjs(bpf.Objs, "FilterPidMap") @@ -387,22 +388,22 @@ func setAndValidateParameters(ctx context.Context, options *ac.AgentOptions) boo remoteIps := viper.GetStringSlice(common.RemoteIpsVarName) if len(remoteIps) > 0 { common.AgentLog.Infoln("filter for remote ips: ", remoteIps) - oneKeyU32 := uint32(1) - err := enabledRemoteIpv4Map.Update(&oneKeyU32, &zeroValue, ebpf.UpdateAny) + oneKeyU32 := bpf.AgentIn6Addr{} + oneKeyU32.In6U.U6Addr8[0] = 1 + err := enabledRemoteIpMap.Update(&oneKeyU32, &zeroValue, ebpf.UpdateAny) if err != nil { common.AgentLog.Errorln("Update EnabledRemoteIpv4Map failed: ", err) } for _, each := range remoteIps { - ipInt32, err := common.IPv4ToUint32(each) + ipBytes := common.NetIPToBytes(net.ParseIP(each), false) + common.AgentLog.Debugln("Update EnabledRemoteIpv4Map, key: ", ipBytes, common.BytesToNetIP(ipBytes, false)) + key := bpf.AgentIn6Addr{} + for i := range ipBytes { + key.In6U.U6Addr8[i] = ipBytes[i] + } + err = enabledRemoteIpMap.Update(&key, &zeroValue, ebpf.UpdateAny) if err != nil { - common.AgentLog.Errorf("IPv4ToUint32 parse failed, ip string is: %s, err: %v", each, err) - return false - } else { - common.AgentLog.Debugln("Update EnabledRemoteIpv4Map, key: ", ipInt32, common.IntToIP(ipInt32)) - err = enabledRemoteIpv4Map.Update(&ipInt32, &zeroValue, ebpf.UpdateAny) - if err != nil { - common.AgentLog.Errorln("Update EnabledRemoteIpv4Map failed: ", err) - } + common.AgentLog.Errorln("Update EnabledRemoteIpv4Map failed: ", err) } } } diff --git a/bpf/pktlatency.bpf.c b/bpf/pktlatency.bpf.c index 20e04f2c..295caa1e 100644 --- a/bpf/pktlatency.bpf.c +++ b/bpf/pktlatency.bpf.c @@ -14,6 +14,7 @@ char LICENSE[] SEC("license") = "Dual BSD/GPL"; +const struct in6_addr *in6_addr_unused __attribute__((unused)); const struct kern_evt *kern_evt_unused __attribute__((unused)); const struct kern_evt_ssl_data *kern_evt_ssl_data_unused __attribute__((unused)); const struct conn_evt_t *conn_evt_t_unused __attribute__((unused)); @@ -795,7 +796,7 @@ MY_BPF_HASH(write_args_map, uint64_t, struct data_args) MY_BPF_HASH(read_args_map, uint64_t, struct data_args) MY_BPF_HASH(enabled_remote_port_map, uint16_t, uint8_t) MY_BPF_HASH(enabled_local_port_map, uint16_t, uint8_t) -MY_BPF_HASH(enabled_remote_ipv4_map, uint32_t, uint8_t) +MY_BPF_HASH(enabled_remote_ip_map, struct in6_addr , uint8_t) MY_BPF_HASH(enabled_local_ipv4_map, uint32_t, uint8_t) @@ -899,16 +900,16 @@ static __always_inline bool filter_conn_info(struct conn_info_t *conn_info) { } } uint32_t one32 = 1; - // if (conn_info->raddr.in6.sin6_family == AF_INET) { // TODO @ipv6 - // uint8_t* enable_remote_ipv4_filter = bpf_map_lookup_elem(&enabled_remote_ipv4_map, &one32); - // if (enable_remote_ipv4_filter != NULL) { - // u32 addr = conn_info->raddr.in4.sin_addr.s_addr; - // uint8_t* enabled_remote_ipv4 = bpf_map_lookup_elem(&enabled_remote_ipv4_map, &addr); - // if (enabled_remote_ipv4 == NULL || conn_info->raddr.in4.sin_addr.s_addr == 0) { - // return false; - // } - // } - // } + struct in6_addr test = {0}; + test.in6_u.u6_addr8[0] = 1; + uint8_t* enable_remote_ipv4_filter = bpf_map_lookup_elem(&enabled_remote_ip_map, &test); + if (enable_remote_ipv4_filter != NULL) { + test = conn_info->raddr.in6.sin6_addr; + uint8_t* enabled_remote_ipv4 = bpf_map_lookup_elem(&enabled_remote_ip_map, &test); + if (enabled_remote_ipv4 == NULL) { + return false; + } + } return true; } static __always_inline bool create_conn_info(void* ctx, struct conn_info_t *conn_info, uint64_t tgid_fd, const struct sock_key *key, enum endpoint_role_t role, uint64_t start_ts) { diff --git a/common/utils.go b/common/utils.go index 5f13b3c7..56df5ea6 100644 --- a/common/utils.go +++ b/common/utils.go @@ -63,6 +63,19 @@ func BytesToNetIP(addr []uint8, isIpv6 bool) net.IP { return net.IP(result) } +func NetIPToBytes(ip net.IP, isIpv6 bool) []byte { + bytes := [16]byte{} + if isIpv6 { + return []byte(ip) + } else { + ipv4 := ip.To4() + for i, a := range ipv4 { + bytes[i] = a + } + return bytes[:] + } +} + func SockKeyIpToNetIP(addr []uint64, isIpv6 bool) net.IP { if isIpv6 { result := make([]byte, 0) @@ -206,6 +219,21 @@ func IPv4ToUint32(ipStr string) (uint32, error) { return result, nil } +func IPv4ToBytes(ipStr string) ([]byte, error) { + // 解析IPv4地址 + ip := net.ParseIP(ipStr) + if ip == nil { + return nil, fmt.Errorf("invalid IP address: %s", ipStr) + } + + // 检查是否为IPv4地址 + ip16 := ip.To16() + if ip16 == nil { + return nil, fmt.Errorf("not an IPv4 address: %s", ipStr) + } + return ip16, nil +} + // ipv6ToBytes converts an IPv6 address string to a []byte. func IPv6ToBytes(ipv6Addr string) ([]byte, error) { // Parse the IPv6 address diff --git a/common/utils_test.go b/common/utils_test.go index 095934bc..46275ce9 100644 --- a/common/utils_test.go +++ b/common/utils_test.go @@ -105,3 +105,8 @@ func TestBytesToIpv6(t *testing.T) { addr2, _ := common.IPv6ToBytes(ip.String()) assert.Equal(t, addr, addr2) } + +func TestIpv4ToBytes(t *testing.T) { + bytes, _ := common.IPv4ToBytes("127.0.0.1") + fmt.Println(bytes) +} From cc14934beba4f6443a7746bb55a6d29c56578969 Mon Sep 17 00:00:00 2001 From: hengyoush Date: Tue, 15 Oct 2024 21:20:27 +0800 Subject: [PATCH 12/12] [Fix] call initDetailViewKeyMap --- agent/render/watch/watch_render.go | 1 + 1 file changed, 1 insertion(+) diff --git a/agent/render/watch/watch_render.go b/agent/render/watch/watch_render.go index e5e70120..9315440c 100644 --- a/agent/render/watch/watch_render.go +++ b/agent/render/watch/watch_render.go @@ -219,6 +219,7 @@ func initDetailViewKeyMap(cols []watchCol) { func initTable(options WatchOptions) table.Model { initWatchCols(options.WideOutput) + initDetailViewKeyMap(cols) columns := []table.Column{} for _, eachCol := range cols { columns = append(columns, table.Column{