Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,7 @@ require (
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/sassoftware/relic v7.2.1+incompatible // indirect
github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2056,6 +2056,8 @@ github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/sassoftware/relic v7.2.1+incompatible h1:Pwyh1F3I0r4clFJXkSI8bOyJINGqpgjJU3DYAZeI05A=
github.com/sassoftware/relic v7.2.1+incompatible/go.mod h1:CWfAxv73/iLZ17rbyhIEq3K9hs5w6FpNMdUT//qR+zk=
github.com/sassoftware/relic/v7 v7.6.2 h1:rS44Lbv9G9eXsukknS4mSjIAuuX+lMq/FnStgmZlUv4=
Expand Down
7 changes: 5 additions & 2 deletions tool/tctl/common/top/box.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ import (
// │ Hello │
// ╰────────────────────╯
func boxedView(title string, content string, width int) string {
return boxedViewWithStyle(title, content, width, lipgloss.NewStyle().Faint(true))
}

func boxedViewWithStyle(title string, content string, width int, style lipgloss.Style) string {
rounderBorder := lipgloss.RoundedBorder()

const borderCorners = 2
Expand All @@ -55,11 +59,10 @@ func boxedView(title string, content string, width int) string {
rounderBorder.Top = ""
rounderBorder.TopRight = ""

contentStyle := lipgloss.NewStyle().
contentStyle := style.
BorderStyle(rounderBorder).
PaddingLeft(1).
PaddingRight(1).
Faint(true).
Width(width)

return renderedTitle + contentStyle.Render(content)
Expand Down
104 changes: 104 additions & 0 deletions tool/tctl/common/top/keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Teleport
// Copyright (C) 2025 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package top

import "github.com/charmbracelet/bubbles/key"

type keyMap struct {
Quit key.Binding

// Movement
Right key.Binding
Left key.Binding
Up key.Binding
Down key.Binding
Filter key.Binding

// Tabs
Common key.Binding
Backend key.Binding
Cache key.Binding
Watcher key.Binding
Audit key.Binding
Raw key.Binding
}

// newDefaultKeymap returns default keybinds used by top
func newDefaultKeymap() *keyMap {
return &keyMap{
Quit: key.NewBinding(
key.WithKeys("q", "ctrl+c"),
key.WithHelp("q", "quit"),
),
Left: key.NewBinding(
key.WithKeys("left", "esc", "shift+tab", "h"),
key.WithHelp("←", "previous"),
),
Right: key.NewBinding(
key.WithKeys("right", "tab", "l"),
key.WithHelp("→", "next"),
),
Up: key.NewBinding(
key.WithKeys("up", "k"),
key.WithHelp("↑", "up"),
),
Down: key.NewBinding(
key.WithKeys("down", "j"),
key.WithHelp("↓", "down"),
),
Common: key.NewBinding(
key.WithKeys("1"),
key.WithHelp("1", "common"),
),
Backend: key.NewBinding(
key.WithKeys("2"),
key.WithHelp("2", "backend"),
),
Cache: key.NewBinding(
key.WithKeys("3"),
key.WithHelp("3", "cache"),
),
Watcher: key.NewBinding(
key.WithKeys("4"),
key.WithHelp("4", "watcher"),
),
Audit: key.NewBinding(
key.WithKeys("5"),
key.WithHelp("5", "audit"),
),
Raw: key.NewBinding(
key.WithKeys("6"),
key.WithHelp("6", "raw"),
),

Filter: key.NewBinding(
key.WithKeys("/"),
key.WithHelp("/", "filter"),
),
}
}

func (k keyMap) ShortHelp() []key.Binding {
return []key.Binding{k.Left, k.Right, k.Quit}
}

func (k keyMap) FullHelp() [][]key.Binding {
return [][]key.Binding{
{k.Left, k.Right},
{k.Quit},
}
}
115 changes: 115 additions & 0 deletions tool/tctl/common/top/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Teleport
// Copyright (C) 2025 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package top

import (
"cmp"
"fmt"
"slices"

"github.com/charmbracelet/bubbles/list"
"github.com/dustin/go-humanize"
dto "github.com/prometheus/client_model/go"
)

// metricItem implements [list.DefaultItem] interface to allow rendering via [list.DefaultDelegate]
type metricItem struct {
name string
value string
}

func (m metricItem) Title() string { return m.name }
func (m metricItem) Description() string { return m.value }
func (m metricItem) FilterValue() string { return m.name }

// getMetricLabelString formats prometheus labels into a readable string.
//
// Example for multiple labels:
//
// "{label_key1=\"value1\",labelkey2=\"value2\"}"
//
// Empty slice will return:
//
// ""
func getMetricLabelString(labels []*dto.LabelPair) string {
var out string

if len(labels) == 0 {
return ""
}

for i, label := range labels {
if i > 0 {
out += ","
}
out += label.GetName() + "=\"" + label.GetValue() + "\""
}

return "{" + out + "}"
Comment thread
okraport marked this conversation as resolved.
}

// metricItemFromPromMetric returns a single [list.Item] from a prometheus metric family and a single metric.
func metricItemFromPromMetric(mf *dto.MetricFamily, m *dto.Metric) list.Item {
value := "n/a"

switch mf.GetType() {
case dto.MetricType_COUNTER:
value = humanize.FormatFloat("", m.GetCounter().GetValue())
case dto.MetricType_GAUGE:
value = humanize.FormatFloat("", m.GetGauge().GetValue())
case dto.MetricType_SUMMARY:
value = fmt.Sprintf("count: %d sum: %s",
m.GetSummary().GetSampleCount(),
humanize.FormatFloat("", m.GetSummary().GetSampleSum()),
)
case dto.MetricType_HISTOGRAM:
// List view does not allow enough space to make historgram buckets meaningful, only show sum and count.
value = fmt.Sprintf("count: %d sum: %s",
m.GetHistogram().GetSampleCount(),
humanize.FormatFloat("", m.GetHistogram().GetSampleSum()),
)
}

return metricItem{
name: mf.GetName() + getMetricLabelString(m.GetLabel()),
value: value,
}
}

// convertMetricsToItems converts a [metricsMsg] into a sorted slice of [list.Item] to be used in a list view.
func convertMetricsToItems(msg metricsMsg) []list.Item {

var itemCount int
for _, mf := range msg {
itemCount += len(mf.GetMetric())
}

items := make([]list.Item, 0, itemCount)

for _, mf := range msg {
for _, m := range mf.GetMetric() {
items = append(items, metricItemFromPromMetric(mf, m))
}
}

// Sort the item list to keep the display order consistent.
slices.SortFunc(items, func(i, j list.Item) int {
return cmp.Compare(i.FilterValue(), j.FilterValue())
})

return items
}
Loading
Loading