diff --git a/agent/analysis/analysis.go b/agent/analysis/analysis.go index 0e70767..2c0d189 100644 --- a/agent/analysis/analysis.go +++ b/agent/analysis/analysis.go @@ -14,17 +14,20 @@ import ( type aggregator struct { *analysis_common.AnalysisOptions *ConnStat + isSub bool } func createAggregatorWithHumanReadableClassId(humanReadableClassId string, - classId analysis_common.ClassId, aggregateOption *analysis_common.AnalysisOptions) *aggregator { - aggregator := createAggregator(classId, aggregateOption) + classId analysis_common.ClassId, + aggregateOption *analysis_common.AnalysisOptions, isSub bool) *aggregator { + aggregator := createAggregator(classId, aggregateOption, isSub) aggregator.HumanReadbleClassId = humanReadableClassId return aggregator } -func createAggregator(classId analysis_common.ClassId, aggregateOption *analysis_common.AnalysisOptions) *aggregator { +func createAggregator(classId analysis_common.ClassId, aggregateOption *analysis_common.AnalysisOptions, isSub bool) *aggregator { aggregator := aggregator{} + aggregator.isSub = isSub aggregator.reset(classId, aggregateOption) return &aggregator } @@ -34,6 +37,10 @@ func (a *aggregator) reset(classId analysis_common.ClassId, aggregateOption *ana a.ConnStat = &ConnStat{ ClassId: classId, ClassfierType: aggregateOption.ClassfierType, + IsSub: a.isSub, + } + if a.isSub { + a.ConnStat.ClassfierType = aggregateOption.SubClassfierType } a.SamplesMap = make(map[analysis_common.MetricType][]*analysis_common.AnnotatedRecord) a.PercentileCalculators = make(map[analysis_common.MetricType]*PercentileCalculator) @@ -65,9 +72,14 @@ func (a *aggregator) receive(record *analysis_common.AnnotatedRecord) error { metricType := analysis_common.MetricType(rawMetricType) if enabled { - samples := a.SamplesMap[metricType] MetricExtract := analysis_common.GetMetricExtractFunc[float64](metricType) - a.SamplesMap[metricType] = AddToSamples(samples, record, MetricExtract, a.AnalysisOptions.SampleLimit) + samples := a.SamplesMap[metricType] + // only sample if aggregator is sub or no sub classfier + if a.isSub || a.SubClassfierType == analysis_common.None { + a.SamplesMap[metricType] = AddToSamples(samples, record, MetricExtract, a.AnalysisOptions.SampleLimit) + } else { + a.SamplesMap[metricType] = AddToSamples(samples, record, MetricExtract, 1) + } metricValue := MetricExtract(record) @@ -102,6 +114,7 @@ func AddToSamples[T analysis_common.MetricValueType](samples []*analysis_common. type Analyzer struct { Classfier + subClassfier Classfier *analysis_common.AnalysisOptions common.SideEnum // 那一边的统计指标TODO 根据参数自动推断 Aggregators map[analysis_common.ClassId]*aggregator @@ -120,7 +133,7 @@ func CreateAnalyzer(recordsChannel <-chan *analysis_common.AnnotatedRecord, opts // ac.AddToFastStopper(stopper) opts.Init() analyzer := &Analyzer{ - Classfier: getClassfier(opts.ClassfierType), + Classfier: getClassfier(opts.ClassfierType, *opts), recordsChannel: recordsChannel, Aggregators: make(map[analysis_common.ClassId]*aggregator), AnalysisOptions: opts, @@ -129,6 +142,9 @@ func CreateAnalyzer(recordsChannel <-chan *analysis_common.AnnotatedRecord, opts renderStopper: renderStopper, ctx: ctx, } + if opts.SubClassfierType != analysis_common.None { + analyzer.subClassfier = getClassfier(opts.SubClassfierType, *opts) + } opts.CurrentReceivedSamples = func() int { return analyzer.recordReceived } @@ -189,14 +205,33 @@ func (a *Analyzer) analyze(record *analysis_common.AnnotatedRecord) { if ok { humanReadableClassId := humanReadableFunc(record) a.Aggregators[class] = createAggregatorWithHumanReadableClassId(humanReadableClassId, - class, a.AnalysisOptions) + class, a.AnalysisOptions, false) } else { - a.Aggregators[class] = createAggregator(class, a.AnalysisOptions) + a.Aggregators[class] = createAggregator(class, a.AnalysisOptions, false) } aggregator = a.Aggregators[class] } aggregator.receive(record) + + if a.subClassfier != nil { + subClassId, err := a.subClassfier(record) + if err == nil { + fullClassId := class + "||" + subClassId + subAggregator, exists := a.Aggregators[fullClassId] + if !exists { + subHumanReadableFunc, ok := getClassIdHumanReadableFunc(a.AnalysisOptions.SubClassfierType, *a.AnalysisOptions) + if ok { + subHumanReadableClassId := subHumanReadableFunc(record) + a.Aggregators[fullClassId] = createAggregatorWithHumanReadableClassId(subHumanReadableClassId, fullClassId, a.AnalysisOptions, true) + } else { + a.Aggregators[fullClassId] = createAggregator(fullClassId, a.AnalysisOptions, true) + } + subAggregator = a.Aggregators[fullClassId] + } + subAggregator.receive(record) + } + } } else { common.DefaultLog.Warnf("classify error: %v\n", err) } diff --git a/agent/analysis/classfier.go b/agent/analysis/classfier.go index e9780da..fb1e803 100644 --- a/agent/analysis/classfier.go +++ b/agent/analysis/classfier.go @@ -47,7 +47,25 @@ func init() { } } + classfierMap[anc.ProtocolAdaptive] = func(ar *anc.AnnotatedRecord) (anc.ClassId, error) { + redisReq, ok := ar.Record.Request().(*protocol.RedisMessage) + if !ok { + return "_not_a_redis_req_", nil + } else { + return anc.ClassId(redisReq.Command()), nil + } + } + classIdHumanReadableMap = make(map[anc.ClassfierType]ClassIdAsHumanReadable) + classIdHumanReadableMap[anc.RemoteIp] = func(ar *anc.AnnotatedRecord) string { + return ar.ConnDesc.RemoteAddr.String() + } + classIdHumanReadableMap[anc.RemotePort] = func(ar *anc.AnnotatedRecord) string { + return fmt.Sprintf("%d", ar.ConnDesc.RemotePort) + } + classIdHumanReadableMap[anc.LocalPort] = func(ar *anc.AnnotatedRecord) string { + return fmt.Sprintf("%d", ar.ConnDesc.LocalPort) + } classIdHumanReadableMap[anc.Conn] = func(ar *anc.AnnotatedRecord) string { return ar.ConnDesc.SimpleString() } @@ -73,6 +91,33 @@ func init() { } } -func getClassfier(classfierType anc.ClassfierType) Classfier { - return classfierMap[classfierType] +func getClassfier(classfierType anc.ClassfierType, options anc.AnalysisOptions) Classfier { + if classfierType == anc.ProtocolAdaptive { + return func(ar *anc.AnnotatedRecord) (anc.ClassId, error) { + c, ok := options.ProtocolSpecificClassfiers[bpf.AgentTrafficProtocolT(ar.Protocol)] + if !ok { + return classfierMap[anc.RemoteIp](ar) + } else { + return classfierMap[c](ar) + } + } + } else { + return classfierMap[classfierType] + } +} + +func getClassIdHumanReadableFunc(classfierType anc.ClassfierType, options anc.AnalysisOptions) (ClassIdAsHumanReadable, bool) { + if classfierType == anc.ProtocolAdaptive { + return func(ar *anc.AnnotatedRecord) string { + c, ok := options.ProtocolSpecificClassfiers[bpf.AgentTrafficProtocolT(ar.Protocol)] + if !ok { + return classIdHumanReadableMap[anc.RemoteIp](ar) + } else { + return classIdHumanReadableMap[c](ar) + } + }, true + } else { + f, ok := classIdHumanReadableMap[classfierType] + return f, ok + } } diff --git a/agent/analysis/common/classfier.go b/agent/analysis/common/classfier.go index 8c482e2..827ab7d 100644 --- a/agent/analysis/common/classfier.go +++ b/agent/analysis/common/classfier.go @@ -1,15 +1,16 @@ 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", + None: "none", + Conn: "conn", + RemotePort: "remote-port", + LocalPort: "local-port", + RemoteIp: "remote-ip", + Protocol: "protocol", + HttpPath: "http-path", + RedisCommand: "redis-command", + ProtocolAdaptive: "protocol-adaptive", + Default: "default", } const ( @@ -26,6 +27,8 @@ const ( // Redis RedisCommand + + ProtocolAdaptive ) type ClassId string diff --git a/agent/analysis/common/types.go b/agent/analysis/common/types.go index 8842ecd..1011ca3 100644 --- a/agent/analysis/common/types.go +++ b/agent/analysis/common/types.go @@ -3,6 +3,7 @@ package common import ( "fmt" "kyanos/agent/protocol" + "kyanos/bpf" "kyanos/common" ac "kyanos/common" @@ -14,7 +15,9 @@ type AnalysisOptions struct { SampleLimit int Side ac.SideEnum ClassfierType - CleanWhenHarvest bool + SubClassfierType ClassfierType + ProtocolSpecificClassfiers map[bpf.AgentTrafficProtocolT]ClassfierType + CleanWhenHarvest bool // Fast Inspect Options SlowMode bool @@ -23,6 +26,9 @@ type AnalysisOptions struct { TargetSamples int CurrentReceivedSamples func() int HavestSignal chan struct{} + + // overview mode + Overview bool } func (a *AnalysisOptions) Init() { diff --git a/agent/analysis/types.go b/agent/analysis/types.go index 9a7e544..67071f4 100644 --- a/agent/analysis/types.go +++ b/agent/analysis/types.go @@ -18,6 +18,7 @@ type ConnStat struct { ClassId anc.ClassId HumanReadbleClassId string ClassfierType anc.ClassfierType + IsSub bool } func (c *ConnStat) ClassIdAsHumanReadable(classId anc.ClassId) string { @@ -31,10 +32,13 @@ func (c *ConnStat) ClassIdAsHumanReadable(classId anc.ClassId) string { case anc.LocalPort: fallthrough case anc.RemoteIp: - return string(classId) + return c.HumanReadbleClassId case anc.Protocol: return c.HumanReadbleClassId default: + if c.HumanReadbleClassId != "" { + return c.HumanReadbleClassId + } return string(classId) } } diff --git a/agent/render/stat/stat.go b/agent/render/stat/stat.go index 9da1cae..fb00957 100644 --- a/agent/render/stat/stat.go +++ b/agent/render/stat/stat.go @@ -8,6 +8,7 @@ import ( "kyanos/agent/analysis/common" rc "kyanos/agent/render/common" "kyanos/agent/render/watch" + "kyanos/bpf" "os" "slices" "strconv" @@ -92,17 +93,22 @@ const ( type model struct { statTable table.Model + subStatTable table.Model sampleModel tea.Model spinner spinner.Model additionHelp help.Model - connstats *[]*analysis.ConnStat - curConnstats *[]*analysis.ConnStat - resultChannel <-chan []*analysis.ConnStat + connstats *[]*analysis.ConnStat // receive from upstream, don't modify it + curConnstats *[]*analysis.ConnStat // after sort&filter, used to display + curSubConnstats *[]*analysis.ConnStat // after sort&filter, used to display + resultChannel <-chan []*analysis.ConnStat options common.AnalysisOptions - chosenStat bool + enableSubGroup bool + chosenSub bool + curClassId common.ClassId + chosenStat bool windownSizeMsg tea.WindowSizeMsg @@ -112,17 +118,20 @@ type model struct { func NewModel(options common.AnalysisOptions) tea.Model { return &model{ - statTable: initTable(options), - sampleModel: nil, - spinner: spinner.New(spinner.WithSpinner(spinner.Dot)), - additionHelp: help.New(), - connstats: nil, - options: options, - chosenStat: false, + statTable: initTable(options, false), + subStatTable: initTable(options, true), + sampleModel: nil, + spinner: spinner.New(spinner.WithSpinner(spinner.Dot)), + additionHelp: help.New(), + connstats: nil, + options: options, + chosenStat: false, + chosenSub: false, + enableSubGroup: options.SubClassfierType != common.None, } } -func initTable(options common.AnalysisOptions) table.Model { +func initTable(options common.AnalysisOptions, isSub bool) table.Model { metric := options.EnabledMetricTypeSet.GetFirstEnabledMetricType() unit := rc.MetricTypeUnit[metric] columns := []table.Column{ @@ -135,6 +144,12 @@ func initTable(options common.AnalysisOptions) table.Model { {Title: fmt.Sprintf("p99(%s)", unit), Width: 10}, {Title: "count", Width: 5}, } + if options.Overview { + columns = slices.Insert(columns, 2, table.Column{Title: "Protocol", Width: 10}) + } + if isSub { + columns[1] = table.Column{Title: common.ClassfierTypeNames[options.SubClassfierType], Width: 40} + } if metric.IsTotalMeaningful() { columns = append(columns, table.Column{Title: fmt.Sprintf("total(%s)", unit), Width: 12}) } @@ -163,36 +178,65 @@ func initTable(options common.AnalysisOptions) table.Model { func (m *model) Init() tea.Cmd { return tea.Batch(m.spinner.Tick) } - +func (m *model) updateConnStats() { + var topStats, subStats []*analysis.ConnStat + for _, each := range *m.connstats { + if each.IsSub && m.curClassId != "" { + if strings.HasPrefix(string(each.ClassId), string(m.curClassId)+"||") { + subStats = append(subStats, each) + } + } else if !each.IsSub { + topStats = append(topStats, each) + } + } + m.sortConnstats(&topStats) + m.sortConnstats(&subStats) + m.curConnstats = &topStats + m.curSubConnstats = &subStats +} +func renderToTable(connstats *[]*analysis.ConnStat, t *table.Model, metric common.MetricType, isSub bool, overview bool) { + records := (*connstats) + var row table.Row + rows := make([]table.Row, 0) + 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])) + } + if overview { + var protocol string + if records, ok := record.SamplesMap[metric]; ok && len(records) > 0 { + protocol = bpf.ProtocolNamesMap[bpf.AgentTrafficProtocolT(records[0].Protocol)] + } else { + protocol = "unknown" + } + row = slices.Insert(row, 2, protocol) + } + rows = append(rows, row) + } + t.SetRows(rows) +} 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) + if m.connstats != nil { + m.updateConnStats() 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) + renderToTable(m.curConnstats, &m.statTable, metric, false, m.options.Overview) + if m.enableSubGroup { + renderToTable(m.curSubConnstats, &m.subStatTable, metric, true, m.options.Overview) } - m.statTable.SetRows(rows) } } func connstatPercentileSortFunc(c1, c2 *analysis.ConnStat, line float64, m common.MetricType, reverse bool) int { @@ -292,15 +336,22 @@ func (m *model) updateStatTable(msg tea.Msg) (tea.Model, tea.Cmd) { } fallthrough case "esc", "q", "ctrl+c": - return m, tea.Quit + if m.chosenSub { + m.chosenSub = false + m.curClassId = "" + return m, nil + } else { + return m, tea.Quit + } case "1", "2", "3", "4", "5", "6", "7", "8": i, err := strconv.Atoi(strings.TrimPrefix(msg.String(), "ctrl+")) + curTable := m.curTable() if err == nil && (i >= int(none) && i < int(end)) && - (i >= 0 && i < len(m.statTable.Columns())) { + (i >= 0 && i < len(curTable.Columns())) { prevSortBy := m.sortBy m.sortBy = rc.SortBy(i) m.reverse = !m.reverse - cols := m.statTable.Columns() + cols := curTable.Columns() if prevSortBy != none { col := &cols[prevSortBy] col.Title = strings.TrimRight(col.Title, "↑") @@ -312,28 +363,57 @@ func (m *model) updateStatTable(msg tea.Msg) (tea.Model, tea.Cmd) { } else { col.Title = col.Title + "↑" } - m.statTable.SetColumns(cols) + curTable.SetColumns(cols) m.updateRowsInTable() } case "enter": - m.chosenStat = true - cursor := m.statTable.Cursor() - metric := m.options.EnabledMetricTypeSet.GetFirstEnabledMetricType() - if m.curConnstats != nil { - records := ((*m.curConnstats)[cursor].SamplesMap[metric]) + cursor := m.curTable().Cursor() + if m.enableSubGroup && !m.chosenSub { + // select sub + m.curClassId = (*m.curConnstats)[cursor].ClassId + m.chosenSub = true + } else { + m.chosenStat = true + metric := m.options.EnabledMetricTypeSet.GetFirstEnabledMetricType() + var records []*common.AnnotatedRecord + if m.enableSubGroup && m.chosenSub { + if m.curSubConnstats != nil { + records = ((*m.curSubConnstats)[cursor].SamplesMap[metric]) + } + } else { + if m.curConnstats != nil { + records = ((*m.curConnstats)[cursor].SamplesMap[metric]) + } + } + m.sampleModel = watch.NewModel(watch.WatchOptions{ WideOutput: true, StaticRecord: true, }, &records, m.windownSizeMsg, metric, true) - } - return m, m.sampleModel.Init() + return m, m.sampleModel.Init() + } } } - m.statTable, cmd = m.statTable.Update(msg) + *m.curTable(), cmd = m.curTable().Update(msg) return m, cmd } +func (m *model) curTable() *table.Model { + if m.enableSubGroup && m.chosenSub { + return &m.subStatTable + } else { + return &m.statTable + } +} +func (m *model) curConnstatsSlice() *[]*analysis.ConnStat { + if m.enableSubGroup && m.chosenSub { + return m.curSubConnstats + } else { + return m.curConnstats + } +} + func (m *model) updateSampleTable(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { @@ -364,9 +444,11 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m *model) viewStatTable() string { + curConnstats := m.curConnstatsSlice() + curTable := m.curTable() totalCount := 0 - if m.curConnstats != nil { - for _, each := range *m.curConnstats { + if curConnstats != nil { + for _, each := range *curConnstats { totalCount += each.Count } } @@ -384,16 +466,16 @@ func (m *model) viewStatTable() string { Bold(false). Foreground(lipgloss.Color("#FFF7DB")).Background(lipgloss.Color(rc.ColorGrid(1, 5)[2][0])) - if len(m.statTable.Rows()) > 0 { + if len(curTable.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) + s += rc.BaseTableStyle.Render(curTable.View()) + "\n " + curTable.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) + s += rc.BaseTableStyle.Render(curTable.View()) + "\n " + curTable.HelpView() + "\n\n " + m.additionHelp.View(sortByKeyMap) } return s } @@ -413,6 +495,7 @@ func StartStatRender(ctx context.Context, ch <-chan []*analysis.ConnStat, option m := NewModel(options).(*model) prog := tea.NewProgram(m, tea.WithContext(ctx), tea.WithAltScreen()) + go func(mod *model, channel <-chan []*analysis.ConnStat) { for { select { diff --git a/cmd/overview.go b/cmd/overview.go new file mode 100644 index 0000000..c13db5c --- /dev/null +++ b/cmd/overview.go @@ -0,0 +1,33 @@ +package cmd + +import "github.com/spf13/cobra" + +var overviewCmd = &cobra.Command{ + Use: "overview [--metrics ]", + Short: "Overview the dependencies like mysql/redis/.. in one cmd line.", + Example: ` +# Basic Usage +sudo kyanos overview +`, + PersistentPreRun: func(cmd *cobra.Command, args []string) { Mode = AnalysisMode }, + Run: func(cmd *cobra.Command, args []string) { + overview = true + groupBy = "remote-ip/protocol-adaptive" + startAgent() + }, +} + +var overview bool + +func init() { + overviewCmd.PersistentFlags().StringVarP(&enabledMetricsString, "metrics", "m", "t", `Specify the statistical dimensions, including: + t: total time taken for request response, + q: request size, + p: response size, + n: network device latency, + s: time spent reading from the socket buffer`) + + overviewCmd.Flags().SortFlags = false + overviewCmd.PersistentFlags().SortFlags = false + rootCmd.AddCommand(overviewCmd) +} diff --git a/cmd/stat.go b/cmd/stat.go index 820d184..e8aaff9 100644 --- a/cmd/stat.go +++ b/cmd/stat.go @@ -3,7 +3,9 @@ package cmd import ( "fmt" anc "kyanos/agent/analysis/common" + "kyanos/bpf" "slices" + "strings" "github.com/spf13/cobra" ) @@ -46,6 +48,7 @@ sudo kyanos stat http --metrics tqp var enabledMetricsString string var sampleCount int var groupBy string +var subGroupBy string var slowMode bool var bigRespModel bool var bigReqModel bool @@ -84,17 +87,32 @@ func createAnalysisOptions() (anc.AnalysisOptions, error) { sampleCount = 0 } options.SampleLimit = sampleCount - + if strings.Contains(groupBy, "/") { + split := strings.Split(groupBy, "/") + groupBy = split[0] + subGroupBy = split[1] + } else { + subGroupBy = "none" + } for key, value := range anc.ClassfierTypeNames { if value == groupBy { options.ClassfierType = key } + if value == subGroupBy && subGroupBy != "" { + options.SubClassfierType = key + } } options.SlowMode = slowMode options.BigReqMode = bigReqModel options.BigRespMode = bigRespModel options.TargetSamples = targetSamples + options.ProtocolSpecificClassfiers = make(map[bpf.AgentTrafficProtocolT]anc.ClassfierType) + // currently only set it hardly + options.ProtocolSpecificClassfiers[bpf.AgentTrafficProtocolTKProtocolHTTP] = anc.HttpPath + options.ProtocolSpecificClassfiers[bpf.AgentTrafficProtocolTKProtocolRedis] = anc.RedisCommand + options.ProtocolSpecificClassfiers[bpf.AgentTrafficProtocolTKProtocolMySQL] = anc.RemoteIp + options.Overview = overview return options, nil } func init() { @@ -112,6 +130,8 @@ func init() { "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(&subGroupBy, "sub-group-by", "default", + // "Specify sub aggregation dimension: like `group-by`, but before set this option you must specify `group-by`") // inspect options statCmd.PersistentFlags().BoolVar(&slowMode, "slow", false, "Find slowest records")