Skip to content

Commit

Permalink
Feature filtering alerts and silences. (#2)
Browse files Browse the repository at this point in the history
* add filtering feature.

* add filtering feature.

* update README.md.
  • Loading branch information
pehlicd authored Sep 13, 2023
1 parent c3741c8 commit f209acd
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 11 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
59 changes: 59 additions & 0 deletions pkg/alerts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
4 changes: 4 additions & 0 deletions pkg/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down
3 changes: 2 additions & 1 deletion pkg/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()
Expand Down
42 changes: 41 additions & 1 deletion pkg/silences.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
60 changes: 51 additions & 9 deletions pkg/tui.go
Original file line number Diff line number Diff line change
@@ -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"
)

Expand All @@ -17,38 +18,69 @@ type TUI struct {
Preview *tview.TextView
Grid *tview.Grid
FooterText *tview.TextView
Filter *tview.InputField
Config Config
}

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)
tui.SidebarList.AddItem("Alerts", "", '1', tui.getAlerts)
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()
Expand All @@ -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
})
Expand Down

0 comments on commit f209acd

Please sign in to comment.