diff --git a/README.md b/README.md index fdf0659..296c8a4 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ AMTUI is a terminal-based user interface (TUI) application that allows you to in - View active alerts with details such as severity, alert name, and description. - Browse and review existing silences in Alertmanager. +- Filter alerts and silences using matchers. - Check the general status of your Alertmanager instance. ## Installation @@ -87,6 +88,7 @@ Once you've launched AMTUI, you can navigate through different sections using th - `h`: Focus on the sidebar list. - `j`: Move focus to the preview. - `k`: Move focus to the preview list. +- `CTRL + F`: Focus on the filter input. - `ESC`: Return focus to the sidebar list. ## Configuration diff --git a/pkg/alerts.go b/pkg/alerts.go index 7805ee2..eca7cca 100644 --- a/pkg/alerts.go +++ b/pkg/alerts.go @@ -74,3 +74,62 @@ func (tui *TUI) getAlerts() { tui.Preview.SetText(s2).SetTextAlign(tview.AlignLeft) }) } + +// fetch filtered alerts data from alertmanager api +func (tui *TUI) getFilteredAlerts(filter []string) { + err := tui.checkConn() + if err != nil { + tui.Errorf("%s", err) + return + } + + params := alert.NewGetAlertsParamsWithTimeout(5 * time.Second).WithContext(context.Background()).WithFilter(filter).WithActive(swag.Bool(true)).WithSilenced(swag.Bool(false)) + alerts, err := tui.amClient().Alert.GetAlerts(params) + if err != nil { + tui.Errorf("Error fetching alerts data: %s", err) + return + } + + if len(alerts.Payload) == 0 { + tui.Preview.SetText("[red]No matching alerts").SetTextAlign(tview.AlignCenter) + return + } + + tui.PreviewList.AddItem("Found "+strconv.Itoa(len(alerts.Payload))+" alerts 🔥", "", 0, nil) + + var mainText string + var alertName string + + for _, alert := range alerts.Payload { + alertByte, err := json.MarshalIndent(alert, "", " ") + if err != nil { + log.Printf("Error marshaling alert: %s", err) + continue + } + if alert.Labels["severity"] != "" { + switch alert.Labels["severity"] { + case "critical": + alertName = "[red]" + alert.Labels["alertname"] + case "warning": + alertName = "[yellow]" + alert.Labels["alertname"] + case "info": + alertName = "[blue]" + alert.Labels["alertname"] + default: + alertName = alert.Labels["alertname"] + } + } else { + alertName = alert.Labels["alertname"] + } + if alert.Annotations["description"] != "" { + mainText = alertName + " - " + alert.Annotations["description"] + } else { + mainText = alertName + } + tui.PreviewList.AddItem(mainText, fmt.Sprintf("[green]%s", string(alertByte)), 0, nil) + } + + tui.PreviewList.SetSelectedFunc(func(i int, s string, s2 string, r rune) { + tui.Preview.Clear() + tui.Preview.SetText(s2).SetTextAlign(tview.AlignLeft) + }) +} diff --git a/pkg/client.go b/pkg/client.go index 247911c..1f956d4 100644 --- a/pkg/client.go +++ b/pkg/client.go @@ -2,6 +2,10 @@ package pkg import am "github.com/prometheus/alertmanager/api/v2/client" +const ( + BasePath = "/api/v2" +) + // create alertmanager client func (tui *TUI) amClient() *am.AlertmanagerAPI { cfg := am.DefaultTransportConfig().WithHost(tui.Config.Host + ":" + tui.Config.Port).WithBasePath(BasePath).WithSchemes([]string{tui.Config.Scheme}) diff --git a/pkg/common.go b/pkg/common.go index 48dab60..79a185a 100644 --- a/pkg/common.go +++ b/pkg/common.go @@ -10,7 +10,7 @@ import ( // dial tcp connection to alertmanager to be ensure if alertmanager server is up or not func (tui *TUI) checkConn() error { - conn, err := net.DialTimeout("tcp", tui.Config.Host+":"+tui.Config.Port, 5*time.Second) + conn, err := net.DialTimeout("tcp", tui.Config.Host+":"+tui.Config.Port, 1000*time.Millisecond) if err != nil { tui.Preview.Clear() return fmt.Errorf("error connecting to alertmanager host: %s", err) @@ -25,6 +25,7 @@ func (tui *TUI) Errorf(format string, args ...interface{}) { tui.Preview.SetText(fmt.Sprintf("[red]"+format, args...)).SetTextAlign(tview.AlignLeft) } +// Clear TUI previews func (tui *TUI) ClearPreviews() { tui.PreviewList.Clear() tui.Preview.Clear() diff --git a/pkg/silences.go b/pkg/silences.go index f6e171d..85d4fd3 100644 --- a/pkg/silences.go +++ b/pkg/silences.go @@ -20,7 +20,47 @@ func (tui *TUI) getSilences() { return } - params := silence.NewGetSilencesParams().WithTimeout(10 * time.Second).WithContext(context.Background()) + params := silence.NewGetSilencesParams().WithTimeout(5 * time.Second).WithContext(context.Background()) + silences, err := tui.amClient().Silence.GetSilences(params) + if err != nil { + tui.Errorf("Error fetching silences data: %s", err) + return + } + + tui.ClearPreviews() + + if len(silences.Payload) == 0 { + tui.Preview.SetText("No silenced alerts 🔔").SetTextAlign(tview.AlignCenter) + return + } + + tui.PreviewList.SetTitle(" Silences ").SetTitleAlign(tview.AlignCenter) + tui.PreviewList.AddItem("Total silences 🔕: "+strconv.Itoa(len(silences.Payload)), "", 0, nil) + + for _, silence := range silences.Payload { + silenceByte, err := json.MarshalIndent(silence, "", " ") + if err != nil { + log.Printf("Error marshaling silence: %s", err) + continue + } + mainText := silence.EndsAt.String() + " - " + *silence.CreatedBy + " - " + *silence.Comment + tui.PreviewList.AddItem(mainText, fmt.Sprintf("[green]%s", string(silenceByte)), 0, nil) + } + + tui.PreviewList.SetSelectedFunc(func(i int, s string, s2 string, r rune) { + tui.Preview.Clear() + tui.Preview.SetText(s2).SetTextAlign(tview.AlignLeft) + }) +} + +func (tui *TUI) getFilteredSilences(filter []string) { + err := tui.checkConn() + if err != nil { + tui.Errorf("%s", err) + return + } + + params := silence.NewGetSilencesParams().WithTimeout(5 * time.Second).WithContext(context.Background()).WithFilter(filter) silences, err := tui.amClient().Silence.GetSilences(params) if err != nil { tui.Errorf("Error fetching silences data: %s", err) diff --git a/pkg/tui.go b/pkg/tui.go index e4c8928..e4369cb 100644 --- a/pkg/tui.go +++ b/pkg/tui.go @@ -1,12 +1,13 @@ package pkg import ( + "strings" + "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) const ( - BasePath = "/api/v2" TitleFooterView = "AMTUI - Alertmanager TUI Client\ngithub.com/pehlicd/amtui" ) @@ -17,6 +18,7 @@ type TUI struct { Preview *tview.TextView Grid *tview.Grid FooterText *tview.TextView + Filter *tview.InputField Config Config } @@ -24,9 +26,37 @@ func InitTUI() *TUI { tui := TUI{App: tview.NewApplication()} tui.SidebarList = tview.NewList().ShowSecondaryText(false) - tui.PreviewList = tview.NewList().ShowSecondaryText(false).SetSelectedBackgroundColor(tcell.ColorDarkSlateGray) + tui.PreviewList = tview.NewList().ShowSecondaryText(false).SetSelectedBackgroundColor(tcell.ColorIndigo).SetSelectedTextColor(tcell.ColorWhite) tui.Preview = tview.NewTextView().SetDynamicColors(true).SetRegions(true).SetScrollable(true) - tui.FooterText = tview.NewTextView().SetTextAlign(tview.AlignCenter).SetText(TitleFooterView).SetTextColor(tcell.ColorGray) + tui.Filter = tview.NewInputField().SetLabel("Filter: ").SetFieldBackgroundColor(tcell.ColorIndigo).SetLabelColor(tcell.ColorWhite).SetFieldTextColor(tcell.ColorWhite).SetDoneFunc(func(key tcell.Key) { + // check if Alerts or Silences option is selected from SidebarList or not + if tui.SidebarList.GetCurrentItem() == 2 { + tui.ClearPreviews() + tui.Preview.SetText("[red]Please select Alerts or Silences option from Navigation").SetTextAlign(tview.AlignCenter) + return + } + + // if search field is empty, return all alerts + if tui.Filter.GetText() == "" && tui.SidebarList.GetCurrentItem() == 0 { + tui.getAlerts() + return + } else if tui.Filter.GetText() != "" && tui.SidebarList.GetCurrentItem() == 0 { + // if search field is not empty, return alerts based on search field + tui.PreviewList.Clear() + filter := strings.Split(tui.Filter.GetText(), ",") + tui.getFilteredAlerts(filter) + } else if tui.Filter.GetText() == "" && tui.SidebarList.GetCurrentItem() == 1 { + tui.getSilences() + return + } else if tui.Filter.GetText() != "" && tui.SidebarList.GetCurrentItem() == 1 { + tui.PreviewList.Clear() + filter := strings.Split(tui.Filter.GetText(), ",") + tui.getFilteredSilences(filter) + } + + tui.App.SetFocus(tui.PreviewList) + }).SetPlaceholder("Custom matcher, e.g. env=\"production\"").SetPlaceholderTextColor(tcell.ColorIndigo) + tui.FooterText = tview.NewTextView().SetTextAlign(tview.AlignCenter).SetText(TitleFooterView).SetTextColor(tcell.ColorGray).SetWordWrap(true) tui.PreviewList.SetTitle("").SetTitleAlign(tview.AlignCenter).SetBorder(true) tui.SidebarList.SetTitle(" Navigation ").SetTitleAlign(tview.AlignCenter).SetBorder(true) @@ -34,21 +64,23 @@ func InitTUI() *TUI { tui.SidebarList.AddItem("Silences", "", '2', tui.getSilences) tui.SidebarList.AddItem("Status", "", '3', tui.getStatus) tui.Preview.SetTitle("").SetTitleAlign(tview.AlignCenter).SetBorder(true) + tui.Filter.SetTitle(" Filter ").SetTitleAlign(tview.AlignCenter).SetBorder(true) tui.Grid = tview.NewGrid(). - SetRows(0, 0, 3). + SetRows(3, 0, 0, 2). SetColumns(20, 0). - AddItem(tui.SidebarList, 0, 0, 2, 1, 0, 0, true). - AddItem(tui.PreviewList, 0, 1, 1, 1, 0, 0, false). - AddItem(tui.Preview, 1, 1, 1, 1, 0, 0, false). - AddItem(tui.FooterText, 2, 0, 1, 2, 0, 0, false) + AddItem(tui.SidebarList, 0, 0, 3, 1, 0, 0, true). + AddItem(tui.Filter, 0, 1, 1, 1, 0, 0, false). + AddItem(tui.PreviewList, 1, 1, 1, 1, 0, 0, false). + AddItem(tui.Preview, 2, 1, 1, 1, 0, 0, false). + AddItem(tui.FooterText, 3, 0, 1, 2, 0, 0, false) // configuration management tui.Config = initConfig() // listen for keyboard events and if q pressed, exit if l pressed in SidebarList focus on PreviewList if h is pressed in PreviewList focus on SidebarList tui.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - if event.Key() == tcell.KeyRune { + if event.Key() == tcell.KeyRune && tui.App.GetFocus() != tui.Filter { switch event.Rune() { case 'q': tui.App.Stop() @@ -73,8 +105,18 @@ func InitTUI() *TUI { return nil } } else if event.Key() == tcell.KeyEsc { + if tui.App.GetFocus() == tui.Filter { + tui.App.SetFocus(tui.PreviewList) + return nil + } tui.App.SetFocus(tui.SidebarList) return nil + } else if event.Key() == tcell.KeyCtrlF { + tui.App.SetFocus(tui.Filter) + return nil + } else if event.Key() == tcell.KeyCtrlC { + tui.App.Stop() + return nil } return event })