From 4c7d43c102b8efa12791280287e666f804741198 Mon Sep 17 00:00:00 2001 From: "Y.Matsuda" Date: Thu, 19 Sep 2024 22:57:15 +0900 Subject: [PATCH] Add feature of sorting by the span latency --- go.mod | 1 + go.sum | 2 + go.work.sum | 4 + tuiexporter/internal/telemetry/sort.go | 60 +++++++ tuiexporter/internal/telemetry/sort_test.go | 150 ++++++++++++++++++ tuiexporter/internal/telemetry/store.go | 11 +- tuiexporter/internal/telemetry/store_test.go | 4 +- tuiexporter/internal/test/tracegen.go | 13 ++ tuiexporter/internal/tui/component/log.go | 3 +- tuiexporter/internal/tui/component/metric.go | 3 +- tuiexporter/internal/tui/component/page.go | 24 ++- tuiexporter/internal/tui/component/trace.go | 29 ++-- .../internal/tui/component/trace_test.go | 52 +++++- 13 files changed, 334 insertions(+), 22 deletions(-) create mode 100644 tuiexporter/internal/telemetry/sort.go create mode 100644 tuiexporter/internal/telemetry/sort_test.go diff --git a/go.mod b/go.mod index 1110218..02cfb0f 100644 --- a/go.mod +++ b/go.mod @@ -102,6 +102,7 @@ require ( github.com/hashicorp/nomad/api v0.0.0-20240717122358-3d93bd3778f3 // indirect github.com/hashicorp/serf v0.10.1 // indirect github.com/hetznercloud/hcloud-go/v2 v2.10.2 // indirect + github.com/icza/gox v0.0.0-20240829094117-5982a7a6cca1 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/ionos-cloud/sdk-go/v6 v6.1.11 // indirect diff --git a/go.sum b/go.sum index 183a607..4c7e170 100644 --- a/go.sum +++ b/go.sum @@ -332,6 +332,8 @@ github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfE github.com/hetznercloud/hcloud-go/v2 v2.10.2 h1:9gyTUPhfNbfbS40Spgij5mV5k37bOZgt8iHKCbfGs5I= github.com/hetznercloud/hcloud-go/v2 v2.10.2/go.mod h1:xQ+8KhIS62W0D78Dpi57jsufWh844gUw1az5OUvaeq8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/icza/gox v0.0.0-20240829094117-5982a7a6cca1 h1:7i7BDcTpFl5LAllCe3lrWyUlNGHc5bC6TF5VLDQI0q4= +github.com/icza/gox v0.0.0-20240829094117-5982a7a6cca1/go.mod h1:VbcN86fRkkUMPX2ufM85Um8zFndLZswoIW1eYtpAcVk= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= diff --git a/go.work.sum b/go.work.sum index c569e34..36dde16 100644 --- a/go.work.sum +++ b/go.work.sum @@ -272,6 +272,7 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= github.com/elastic/elastic-transport-go/v8 v8.6.0/go.mod h1:YLHer5cj0csTzNFXoNQ8qhtGY1GTvSqPnKWKaqQE3Hk= github.com/elastic/go-elasticsearch/v8 v8.14.0/go.mod h1:WRvnlGkSuZyp83M2U8El/LGXpCjYLrvlkSgkAH4O5I4= +github.com/elastic/lunes v0.1.0/go.mod h1:xGphYIt3XdZRtyWosHQTErsQTd4OP1p9wsbVoHelrd4= github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:bH6Xx7IW64qjjJq8M2u4dxNaBiDfKK+z/3eGDpXEQhc= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-fonts/liberation v0.3.2/go.mod h1:N0QsDLVUQPy3UYg9XAc3Uh3UDMp2Z7M1o4+X98dXkmI= @@ -327,6 +328,7 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/lightstep/go-expohisto v1.0.0/go.mod h1:xDXD0++Mu2FOaItXtdDfksfgxfV0z1TMPa+e/EUd0cs= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lyft/protoc-gen-star/v2 v2.0.3/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk= +github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= @@ -352,9 +354,11 @@ github.com/open-telemetry/opentelemetry-collector-contrib/pkg/batchpersignal v0. github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.106.1/go.mod h1:6MVXAX6OpG01Gb38KJUP/8APe2BCmGYtzKPOua05bTw= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.107.0/go.mod h1:qj9lEtkVjQUzZ7FdJTeDqqTUq9xVU9kE4F8zZnHFB9M= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.108.0/go.mod h1:3ku/cfl0FXMSc/dc9DGrhABhE6/AoYArKtl3I9QEp28= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/golden v0.109.0/go.mod h1:P7e6ch+uoSfxK+lMwfcndkHE6gWUqvWKpr7mD04KIAA= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.106.1/go.mod h1:St0VVFKzA0fNxo5RmzI4fg7ucGttd840OZ56a+ZECZs= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.107.0/go.mod h1:oG/PliNiIOUHVARyDrFdvxFvG8DUPEjMGlmxjEqeoKM= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.108.0/go.mod h1:G+N43ID1sP2CnffxkYdMyuJpep2UcGQUyq4HiAmcYSw= +github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatatest v0.109.0/go.mod h1:KvJWxR0bDk9Qh0ktw4gOFsd/ZrJ7p5KTAQueEJsaK9Q= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.106.1/go.mod h1:ehzaiDdkrww7l1Stvse5GCOAsAZOpFcgeIbB/2PqFs4= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.107.0/go.mod h1:/RtBag3LuHIkqN4bo8Erd3jCzA3gea70l9WyJ9TncXM= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/resourcetotelemetry v0.106.1/go.mod h1:qsYqM+UGm3h2M+MQnaKBeQmsBC+sIGgGNJyyQFfUCUI= diff --git a/tuiexporter/internal/telemetry/sort.go b/tuiexporter/internal/telemetry/sort.go new file mode 100644 index 0000000..1ae954e --- /dev/null +++ b/tuiexporter/internal/telemetry/sort.go @@ -0,0 +1,60 @@ +package telemetry + +import "sort" + +const ( + SORT_TYPE_NONE SortType = "none" + SORT_TYPE_LATENCY_DESC SortType = "latency-desc" + SORT_TYPE_LATENCY_ASC SortType = "latency-asc" +) + +// SortType is sort type +type SortType string + +func (t SortType) IsNone() bool { + return t == SORT_TYPE_NONE +} + +func (t SortType) IsDesc() bool { + return t == SORT_TYPE_LATENCY_DESC +} + +func (t SortType) GetHeaderLabel() string { + switch t { + case SORT_TYPE_LATENCY_DESC: + return "Latency" + case SORT_TYPE_LATENCY_ASC: + return "Latency" + } + return "N/A" +} + +func sortSvcSpans(svcSpans SvcSpans, sortType SortType) { + switch sortType { + case SORT_TYPE_NONE: + sort.Slice(svcSpans, func(i, j int) bool { + // default sort is received_at asc + return svcSpans[i].ReceivedAt.Before(svcSpans[j].ReceivedAt) + }) + case SORT_TYPE_LATENCY_DESC: + sort.Slice(svcSpans, func(i, j int) bool { + istart := svcSpans[i].Span.StartTimestamp().AsTime() + iend := svcSpans[i].Span.EndTimestamp().AsTime() + iduration := iend.Sub(istart) + jstart := svcSpans[j].Span.StartTimestamp().AsTime() + jend := svcSpans[j].Span.EndTimestamp().AsTime() + jduration := jend.Sub(jstart) + return iduration > jduration + }) + case SORT_TYPE_LATENCY_ASC: + sort.Slice(svcSpans, func(i, j int) bool { + istart := svcSpans[i].Span.StartTimestamp().AsTime() + iend := svcSpans[i].Span.EndTimestamp().AsTime() + iduration := iend.Sub(istart) + jstart := svcSpans[j].Span.StartTimestamp().AsTime() + jend := svcSpans[j].Span.EndTimestamp().AsTime() + jduration := jend.Sub(jstart) + return iduration < jduration + }) + } +} diff --git a/tuiexporter/internal/telemetry/sort_test.go b/tuiexporter/internal/telemetry/sort_test.go new file mode 100644 index 0000000..7daf2ae --- /dev/null +++ b/tuiexporter/internal/telemetry/sort_test.go @@ -0,0 +1,150 @@ +package telemetry + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/ymtdzzz/otel-tui/tuiexporter/internal/test" +) + +func TestSortType(t *testing.T) { + tests := []struct { + name string + input SortType + wantIsNone bool + wantIsDesc bool + wantHeaderLabel string + }{ + { + name: "SORT_TYPE_NONE", + input: SORT_TYPE_NONE, + wantIsNone: true, + wantIsDesc: false, + wantHeaderLabel: "N/A", + }, + { + name: "SORT_TYPE_LATENCY_DESC", + input: SORT_TYPE_LATENCY_DESC, + wantIsNone: false, + wantIsDesc: true, + wantHeaderLabel: "Latency", + }, + { + name: "SORT_TYPE_LATENCY_ASC", + input: SORT_TYPE_LATENCY_ASC, + wantIsNone: false, + wantIsDesc: false, + wantHeaderLabel: "Latency", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.wantIsNone, tt.input.IsNone()) + assert.Equal(t, tt.wantIsDesc, tt.input.IsDesc()) + assert.Equal(t, tt.wantHeaderLabel, tt.input.GetHeaderLabel()) + }) + } +} + +func TestSortSvcSpans(t *testing.T) { + baseSvcSpans := SvcSpans{ + &SpanData{ + Span: test.GenerateSpanWithDuration(t, "100ms", 100*time.Millisecond), + }, + &SpanData{ + Span: test.GenerateSpanWithDuration(t, "75µs", 50*time.Microsecond), + }, + &SpanData{ + Span: test.GenerateSpanWithDuration(t, "230ms", 230*time.Millisecond), + }, + &SpanData{ + Span: test.GenerateSpanWithDuration(t, "101ms", 101*time.Millisecond), + }, + &SpanData{ + Span: test.GenerateSpanWithDuration(t, "50ns", 50*time.Nanosecond), + }, + } + + tests := []struct { + name string + sortType SortType + input SvcSpans + want SvcSpans + }{ + { + name: "SORT_TYPE_NONE", + sortType: SORT_TYPE_NONE, + input: append(SvcSpans{}, baseSvcSpans...), + want: SvcSpans{ + &SpanData{ + Span: test.GenerateSpanWithDuration(t, "100ms", 100*time.Millisecond), + }, + &SpanData{ + Span: test.GenerateSpanWithDuration(t, "75µs", 50*time.Microsecond), + }, + &SpanData{ + Span: test.GenerateSpanWithDuration(t, "230ms", 230*time.Millisecond), + }, + &SpanData{ + Span: test.GenerateSpanWithDuration(t, "101ms", 101*time.Millisecond), + }, + &SpanData{ + Span: test.GenerateSpanWithDuration(t, "50ns", 50*time.Nanosecond), + }, + }, + }, + { + name: "SORT_TYPE_LATENCY_DESC", + sortType: SORT_TYPE_LATENCY_DESC, + input: append(SvcSpans{}, baseSvcSpans...), + want: SvcSpans{ + &SpanData{ + Span: test.GenerateSpanWithDuration(t, "230ms", 230*time.Millisecond), + }, + &SpanData{ + Span: test.GenerateSpanWithDuration(t, "101ms", 101*time.Millisecond), + }, + &SpanData{ + Span: test.GenerateSpanWithDuration(t, "100ms", 100*time.Millisecond), + }, + &SpanData{ + Span: test.GenerateSpanWithDuration(t, "75µs", 50*time.Microsecond), + }, + &SpanData{ + Span: test.GenerateSpanWithDuration(t, "50ns", 50*time.Nanosecond), + }, + }, + }, + { + name: "SORT_TYPE_LATENCY_ASC", + sortType: SORT_TYPE_LATENCY_ASC, + input: append(SvcSpans{}, baseSvcSpans...), + want: SvcSpans{ + &SpanData{ + Span: test.GenerateSpanWithDuration(t, "50ns", 50*time.Nanosecond), + }, + &SpanData{ + Span: test.GenerateSpanWithDuration(t, "75µs", 50*time.Microsecond), + }, + &SpanData{ + Span: test.GenerateSpanWithDuration(t, "100ms", 100*time.Millisecond), + }, + &SpanData{ + Span: test.GenerateSpanWithDuration(t, "101ms", 101*time.Millisecond), + }, + &SpanData{ + Span: test.GenerateSpanWithDuration(t, "230ms", 230*time.Millisecond), + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sortSvcSpans(tt.input, tt.sortType) + assert.Equal(t, tt.want, tt.input) + }) + } +} diff --git a/tuiexporter/internal/telemetry/store.go b/tuiexporter/internal/telemetry/store.go index 7095bf9..9863968 100644 --- a/tuiexporter/internal/telemetry/store.go +++ b/tuiexporter/internal/telemetry/store.go @@ -71,6 +71,7 @@ type Store struct { filterSvc string filterMetric string filterLog string + sortTrace SortType svcspans SvcSpans svcspansFiltered SvcSpans tracecache *TraceCache @@ -145,13 +146,15 @@ func (s *Store) UpdatedAt() time.Time { return s.updatedAt } -// ApplyFilterTraces applies a filter to the traces -func (s *Store) ApplyFilterTraces(svc string) { +// ApplyFilterTraces applies a filter and sort to the traces +func (s *Store) ApplyFilterTraces(svc string, sortType SortType) { s.filterSvc = svc + s.sortTrace = sortType s.svcspansFiltered = []*SpanData{} if svc == "" { s.svcspansFiltered = s.svcspans + sortSvcSpans(s.svcspansFiltered, sortType) return } @@ -165,10 +168,12 @@ func (s *Store) ApplyFilterTraces(svc string) { s.svcspansFiltered = append(s.svcspansFiltered, span) } } + + sortSvcSpans(s.svcspansFiltered, sortType) } func (s *Store) updateFilterService() { - s.ApplyFilterTraces(s.filterSvc) + s.ApplyFilterTraces(s.filterSvc, s.sortTrace) } // ApplyFilterMetrics applies a filter to the metrics diff --git a/tuiexporter/internal/telemetry/store_test.go b/tuiexporter/internal/telemetry/store_test.go index 1972ad6..9123e69 100644 --- a/tuiexporter/internal/telemetry/store_test.go +++ b/tuiexporter/internal/telemetry/store_test.go @@ -68,7 +68,7 @@ func TestStoreSpanFilters(t *testing.T) { traceID := testdata.Spans[0].TraceID().String() store.AddSpan(&payload) - store.ApplyFilterTraces("0-0") + store.ApplyFilterTraces("0-0", SORT_TYPE_NONE) assert.Equal(t, 2, len(store.svcspansFiltered)) assert.Equal(t, traceID, store.GetTraceIDByFilteredIdx(0)) assert.Equal(t, traceID, store.GetTraceIDByFilteredIdx(1)) @@ -78,7 +78,7 @@ func TestStoreSpanFilters(t *testing.T) { assert.Equal(t, "span-0-0-1", store.GetFilteredServiceSpansByIdx(0)[1].Span.Name()) // spans in test-service-2 assert.Equal(t, "span-1-0-0", store.GetFilteredServiceSpansByIdx(1)[0].Span.Name()) - store.ApplyFilterTraces("service-2") + store.ApplyFilterTraces("service-2", SORT_TYPE_NONE) assert.Equal(t, 1, len(store.svcspansFiltered)) assert.Equal(t, traceID, store.GetTraceIDByFilteredIdx(0)) assert.Equal(t, "", store.GetTraceIDByFilteredIdx(1)) diff --git a/tuiexporter/internal/test/tracegen.go b/tuiexporter/internal/test/tracegen.go index 11fccb8..b97def7 100644 --- a/tuiexporter/internal/test/tracegen.go +++ b/tuiexporter/internal/test/tracegen.go @@ -115,3 +115,16 @@ func fillSpan(t *testing.T, span ptrace.Span, traceID, resourceIndex, scopeIndex status.SetCode(ptrace.StatusCodeOk) status.SetMessage("status ok") } + +// GenerateSpanWithDuration returns a span with specified span name and duration. +func GenerateSpanWithDuration(t *testing.T, spanName string, duration time.Duration) *ptrace.Span { + t.Helper() + + span := ptrace.NewSpan() + span.SetName(spanName) + endTimeStamp := pcommon.NewTimestampFromTime(spanStartTimestamp.AsTime().Add(duration)) + span.SetStartTimestamp(spanStartTimestamp) + span.SetEndTimestamp(endTimeStamp) + + return &span +} diff --git a/tuiexporter/internal/tui/component/log.go b/tuiexporter/internal/tui/component/log.go index 382da0e..5359cd3 100644 --- a/tuiexporter/internal/tui/component/log.go +++ b/tuiexporter/internal/tui/component/log.go @@ -34,7 +34,8 @@ func NewLogDataForTable(logs *[]*telemetry.LogData) LogDataForTable { // see: https://github.com/rivo/tview/wiki/VirtualTable func (l LogDataForTable) GetCell(row, column int) *tview.TableCell { if row == 0 { - return getHeaderCell(logTableHeader[:], column) + sortType := telemetry.SORT_TYPE_NONE + return getHeaderCell(logTableHeader[:], column, &sortType) } if row > 0 && row <= len(*l.logs) { return getCellFromLog((*l.logs)[row-1], column) diff --git a/tuiexporter/internal/tui/component/metric.go b/tuiexporter/internal/tui/component/metric.go index 5d5b11b..45dc91a 100644 --- a/tuiexporter/internal/tui/component/metric.go +++ b/tuiexporter/internal/tui/component/metric.go @@ -38,7 +38,8 @@ func NewMetricDataForTable(metrics *[]*telemetry.MetricData) MetricDataForTable // see: https://github.com/rivo/tview/wiki/VirtualTable func (m MetricDataForTable) GetCell(row, column int) *tview.TableCell { if row == 0 { - return getHeaderCell(metricTableHeader[:], column) + sortType := telemetry.SORT_TYPE_NONE + return getHeaderCell(metricTableHeader[:], column, &sortType) } if row > 0 && row <= len(*m.metrics) { return getCellFromMetrics((*m.metrics)[row-1], column) diff --git a/tuiexporter/internal/tui/component/page.go b/tuiexporter/internal/tui/component/page.go index dd755f5..06732de 100644 --- a/tuiexporter/internal/tui/component/page.go +++ b/tuiexporter/internal/tui/component/page.go @@ -148,11 +148,14 @@ func (p *TUIPages) createTracePage(store *telemetry.Store) *tview.Flex { return event }) + input := "" + inputConfirmed := "" + sortType := telemetry.SORT_TYPE_NONE tableContainer.SetTitle("Traces (t)").SetBorder(true) table := tview.NewTable(). SetBorders(false). SetSelectable(true, false). - SetContent(NewSpanDataForTable(store.GetTraceCache(), store.GetFilteredSvcSpans())). + SetContent(NewSpanDataForTable(store.GetTraceCache(), store.GetFilteredSvcSpans(), &sortType)). SetSelectedFunc(func(row, _ int) { p.showTimelineByRow(store, row-1) }). @@ -161,6 +164,17 @@ func (p *TUIPages) createTracePage(store *telemetry.Store) *tview.Flex { if event.Key() == tcell.KeyCtrlL { store.Flush() return nil + } else if event.Key() == tcell.KeyCtrlS { + if sortType == telemetry.SORT_TYPE_NONE { + sortType = telemetry.SORT_TYPE_LATENCY_DESC + } else if sortType == telemetry.SORT_TYPE_LATENCY_DESC { + sortType = telemetry.SORT_TYPE_LATENCY_ASC + } else { + sortType = telemetry.SORT_TYPE_NONE + } + log.Printf("sortType: %s", sortType) + store.ApplyFilterTraces(inputConfirmed, sortType) + return nil } return event }) @@ -169,14 +183,16 @@ func (p *TUIPages) createTracePage(store *telemetry.Store) *tview.Flex { key: tcell.NewEventKey(tcell.KeyRune, '/', tcell.ModNone), description: "Search traces", }, + { + key: tcell.NewEventKey(tcell.KeyRune, 'S', tcell.ModCtrl), + description: "Toggle sort (Latency)", + }, { key: tcell.NewEventKey(tcell.KeyRune, 'L', tcell.ModCtrl), description: "Clear all data", }, }) - input := "" - inputConfirmed := "" search := tview.NewInputField(). SetLabel("Filter by service or span name (/): "). SetFieldWidth(20) @@ -192,7 +208,7 @@ func (p *TUIPages) createTracePage(store *telemetry.Store) *tview.Flex { if key == tcell.KeyEnter { inputConfirmed = input log.Println("search service name: ", inputConfirmed) - store.ApplyFilterTraces(inputConfirmed) + store.ApplyFilterTraces(inputConfirmed, sortType) } else if key == tcell.KeyEsc { search.SetText(inputConfirmed) } diff --git a/tuiexporter/internal/tui/component/trace.go b/tuiexporter/internal/tui/component/trace.go index aa9d399..fbabb9c 100644 --- a/tuiexporter/internal/tui/component/trace.go +++ b/tuiexporter/internal/tui/component/trace.go @@ -5,6 +5,7 @@ import ( "sort" "github.com/gdamore/tcell/v2" + "github.com/icza/gox/timex" "github.com/rivo/tview" "github.com/ymtdzzz/otel-tui/tuiexporter/internal/telemetry" "go.opentelemetry.io/collector/pdata/pcommon" @@ -21,15 +22,17 @@ var spanTableHeader = [5]string{ // SpanDataForTable is a wrapper for spans to be displayed in a table. type SpanDataForTable struct { tview.TableContentReadOnly - tcache *telemetry.TraceCache - spans *telemetry.SvcSpans + tcache *telemetry.TraceCache + spans *telemetry.SvcSpans + sortType *telemetry.SortType } // NewSpanDataForTable creates a new SpanDataForTable. -func NewSpanDataForTable(tcache *telemetry.TraceCache, spans *telemetry.SvcSpans) SpanDataForTable { +func NewSpanDataForTable(tcache *telemetry.TraceCache, spans *telemetry.SvcSpans, sortType *telemetry.SortType) SpanDataForTable { return SpanDataForTable{ - tcache: tcache, - spans: spans, + tcache: tcache, + spans: spans, + sortType: sortType, } } @@ -37,7 +40,7 @@ func NewSpanDataForTable(tcache *telemetry.TraceCache, spans *telemetry.SvcSpans // see: https://github.com/rivo/tview/wiki/VirtualTable func (s SpanDataForTable) GetCell(row, column int) *tview.TableCell { if row == 0 { - return getHeaderCell(spanTableHeader[:], column) + return getHeaderCell(spanTableHeader[:], column, s.sortType) } if row > 0 && row <= len(*s.spans) { return s.getCellFromSpan((*s.spans)[row-1], column) @@ -74,7 +77,7 @@ func (s SpanDataForTable) getCellFromSpan(span *telemetry.SpanData, column int) } case 2: duration := span.Span.EndTimestamp().AsTime().Sub(span.Span.StartTimestamp().AsTime()) - text = duration.String() + text = timex.Round(duration, 2).String() case 3: text = span.ReceivedAt.Local().Format("2006-01-02 15:04:05") case 4: @@ -84,14 +87,22 @@ func (s SpanDataForTable) getCellFromSpan(span *telemetry.SpanData, column int) return tview.NewTableCell(text) } -func getHeaderCell(header []string, column int) *tview.TableCell { +func getHeaderCell(header []string, column int, sortType *telemetry.SortType) *tview.TableCell { cell := tview.NewTableCell("N/A"). SetSelectable(false). SetTextColor(tcell.ColorYellow) if column >= len(header) { return cell } - cell.SetText(header[column]) + h := header[column] + if !sortType.IsNone() && sortType.GetHeaderLabel() == h { + if sortType.IsDesc() { + h = h + " ▼" + } else { + h = h + " ▲" + } + } + cell.SetText(h) return cell } diff --git a/tuiexporter/internal/tui/component/trace_test.go b/tuiexporter/internal/tui/component/trace_test.go index f6186d9..e6e77b7 100644 --- a/tuiexporter/internal/tui/component/trace_test.go +++ b/tuiexporter/internal/tui/component/trace_test.go @@ -65,7 +65,8 @@ func TestSpanDataForTable(t *testing.T) { for _, sd := range svc2sds { tcache.UpdateCache("test-service-2", sd) } - sdftable := NewSpanDataForTable(tcache, svcspans) + sortType := telemetry.SORT_TYPE_NONE + sdftable := NewSpanDataForTable(tcache, svcspans, &sortType) t.Run("GetRowCount", func(t *testing.T) { assert.Equal(t, 4, sdftable.GetRowCount()) // including header row @@ -75,7 +76,54 @@ func TestSpanDataForTable(t *testing.T) { assert.Equal(t, 5, sdftable.GetColumnCount()) }) - t.Run("GetCell", func(t *testing.T) { + t.Run("GetCell_Header", func(t *testing.T) { + tests := []struct { + name string + sortType telemetry.SortType + column int + want string + }{ + { + name: "N/A", + sortType: telemetry.SORT_TYPE_NONE, + column: 5, + want: "N/A", + }, + { + name: "Latency None", + sortType: telemetry.SORT_TYPE_NONE, + column: 2, + want: "Latency", + }, + { + name: "Latency Desc", + sortType: telemetry.SORT_TYPE_LATENCY_DESC, + column: 2, + want: "Latency ▼", + }, + { + name: "Latency Asc", + sortType: telemetry.SORT_TYPE_LATENCY_ASC, + column: 2, + want: "Latency ▲", + }, + { + name: "Service Name no effect", + sortType: telemetry.SORT_TYPE_LATENCY_DESC, + column: 1, + want: "Service Name", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sortType = tt.sortType + assert.Equal(t, tt.want, sdftable.GetCell(0, tt.column).Text) + }) + } + }) + + t.Run("GetCell_Body", func(t *testing.T) { tests := []struct { name string row int