From 1964dde273aef266fa045f217c0a91e75d1e5c36 Mon Sep 17 00:00:00 2001 From: Hector Date: Sun, 29 Aug 2021 16:54:20 +0000 Subject: [PATCH] feat: export metrics for failed/banned counts Add new metric to track the total number of jails configured in fail2ban. Add new metrics for the current and total number of filter failures for each jail, as well as the current/total number of banned IPs per jail. The new metrics are collected by sending the `status [jail]` command to the fail2ban server and parsing the response data. --- src/exporter.go | 68 ++++++++++++++++++++++++++ src/socket/fail2banSocket.go | 95 ++++++++++++++++++++++++++++++++++-- src/socket/protocol.go | 1 + 3 files changed, 160 insertions(+), 4 deletions(-) diff --git a/src/exporter.go b/src/exporter.go index 5d568c3..97e8887 100644 --- a/src/exporter.go +++ b/src/exporter.go @@ -49,11 +49,37 @@ var ( "Number of errors found since startup.", []string{"type"}, nil, ) + metricServerPing = prometheus.NewDesc( prometheus.BuildFQName(sockNamespace, "", "up"), "Check if the fail2ban server is up", nil, nil, ) + metricJailCount = prometheus.NewDesc( + prometheus.BuildFQName(sockNamespace, "", "jail_count"), + "Number of defined jails", + nil, nil, + ) + metricJailFailedCurrent = prometheus.NewDesc( + prometheus.BuildFQName(sockNamespace, "", "jail_failed_current"), + "Number of current failures on this jail's filter", + []string{"jail"}, nil, + ) + metricJailFailedTotal = prometheus.NewDesc( + prometheus.BuildFQName(sockNamespace, "", "jail_failed_total"), + "Number of total failures on this jail's filter", + []string{"jail"}, nil, + ) + metricJailBannedCurrent = prometheus.NewDesc( + prometheus.BuildFQName(sockNamespace, "", "jail_banned_current"), + "Number of IPs currently banned in this jail", + []string{"jail"}, nil, + ) + metricJailBannedTotal = prometheus.NewDesc( + prometheus.BuildFQName(sockNamespace, "", "jail_banned_total"), + "Total number of IPs banned by this jail (includes expired bans)", + []string{"jail"}, nil, + ) ) type Exporter struct { @@ -73,6 +99,11 @@ func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { } if e.socket != nil { ch <- metricServerPing + ch <- metricJailCount + ch <- metricJailFailedCurrent + ch <- metricJailFailedTotal + ch <- metricJailBannedCurrent + ch <- metricJailBannedTotal } } @@ -86,6 +117,7 @@ func (e *Exporter) Collect(ch chan<- prometheus.Metric) { } if e.socket != nil { e.collectServerPingMetric(ch) + e.collectJailMetrics(ch) } } @@ -164,6 +196,42 @@ func (e *Exporter) collectServerPingMetric(ch chan<- prometheus.Metric) { ) } +func (e *Exporter) collectJailMetrics(ch chan<- prometheus.Metric) { + jails, err := e.socket.GetJails() + var count float64 = 0 + if err == nil { + count = float64(len(jails)) + } + ch <- prometheus.MustNewConstMetric( + metricJailCount, prometheus.GaugeValue, count, + ) + + for i := range jails { + e.collectJailStatsMetric(ch, jails[i]) + } +} + +func (e *Exporter) collectJailStatsMetric(ch chan<- prometheus.Metric, jail string) { + stats, err := e.socket.GetJailStats(jail) + if err != nil { + log.Printf("failed to get stats for jail %s: %v", jail, err) + return + } + + ch <- prometheus.MustNewConstMetric( + metricJailFailedCurrent, prometheus.GaugeValue, float64(stats.FailedCurrent), jail, + ) + ch <- prometheus.MustNewConstMetric( + metricJailFailedTotal, prometheus.GaugeValue, float64(stats.FailedTotal), jail, + ) + ch <- prometheus.MustNewConstMetric( + metricJailBannedCurrent, prometheus.GaugeValue, float64(stats.BannedCurrent), jail, + ) + ch <- prometheus.MustNewConstMetric( + metricJailBannedTotal, prometheus.GaugeValue, float64(stats.BannedTotal), jail, + ) +} + func printAppVersion() { fmt.Println(version) fmt.Printf(" build date: %s\r\n commit hash: %s\r\n built by: %s\r\n", date, commit, builtBy) diff --git a/src/socket/fail2banSocket.go b/src/socket/fail2banSocket.go index 35513c8..8cce14f 100644 --- a/src/socket/fail2banSocket.go +++ b/src/socket/fail2banSocket.go @@ -1,11 +1,12 @@ package socket import ( - "log" - "net" - + "fmt" "github.com/kisielk/og-rek" "github.com/nlpodyssey/gopickle/types" + "log" + "net" + "strings" ) type Fail2BanSocket struct { @@ -13,6 +14,13 @@ type Fail2BanSocket struct { encoder *ogórek.Encoder } +type JailStats struct { + FailedCurrent int + FailedTotal int + BannedCurrent int + BannedTotal int +} + func MustConnectToSocket(path string) *Fail2BanSocket { c, err := net.Dial("unix", path) if err != nil { @@ -37,6 +45,85 @@ func (s *Fail2BanSocket) Ping() bool { } log.Printf("unexpected response data: %s", t) } - log.Printf("unexpected response format - cannot parse: %v", response) + log.Printf("(%s) unexpected response format - cannot parse: %v", pingCommand, response) return false } + +func (s *Fail2BanSocket) GetJails() ([]string, error) { + response, err := s.sendCommand([]string{statusCommand}) + if err != nil { + return nil, err + } + + if lvl1, ok := response.(*types.Tuple); ok { + if lvl2, ok := lvl1.Get(1).(*types.List); ok { + if lvl3, ok := lvl2.Get(1).(*types.Tuple); ok { + if lvl4, ok := lvl3.Get(1).(string); ok { + splitJails := strings.Split(lvl4, ",") + return trimSpaceForAll(splitJails), nil + } + } + } + } + return nil, newBadFormatError(statusCommand, response) +} + +func (s *Fail2BanSocket) GetJailStats(jail string) (JailStats, error) { + response, err := s.sendCommand([]string{statusCommand, jail}) + if err != nil { + return JailStats{}, err + } + + stats := JailStats{ + FailedCurrent: -1, + FailedTotal: -1, + BannedCurrent: -1, + BannedTotal: -1, + } + + if lvl1, ok := response.(*types.Tuple); ok { + if lvl2, ok := lvl1.Get(1).(*types.List); ok { + if filter, ok := lvl2.Get(0).(*types.Tuple); ok { + if filterLvl1, ok := filter.Get(1).(*types.List); ok { + if filterCurrentTuple, ok := filterLvl1.Get(0).(*types.Tuple); ok { + if filterCurrent, ok := filterCurrentTuple.Get(1).(int); ok { + stats.FailedCurrent = filterCurrent + } + } + if filterTotalTuple, ok := filterLvl1.Get(1).(*types.Tuple); ok { + if filterTotal, ok := filterTotalTuple.Get(1).(int); ok { + stats.FailedTotal = filterTotal + } + } + } + } + if actions, ok := lvl2.Get(1).(*types.Tuple); ok { + if actionsLvl1, ok := actions.Get(1).(*types.List); ok { + if actionsCurrentTuple, ok := actionsLvl1.Get(0).(*types.Tuple); ok { + if actionsCurrent, ok := actionsCurrentTuple.Get(1).(int); ok { + stats.BannedCurrent = actionsCurrent + } + } + if actionsTotalTuple, ok := actionsLvl1.Get(1).(*types.Tuple); ok { + if actionsTotal, ok := actionsTotalTuple.Get(1).(int); ok { + stats.BannedTotal = actionsTotal + } + } + } + } + return stats, nil + } + } + return stats, newBadFormatError(statusCommand, response) +} + +func newBadFormatError(command string, data interface{}) error { + return fmt.Errorf("(%s) unexpected response format - cannot parse: %v", command, data) +} + +func trimSpaceForAll(slice []string) []string { + for i := range slice { + slice[i] = strings.TrimSpace(slice[i]) + } + return slice +} diff --git a/src/socket/protocol.go b/src/socket/protocol.go index 1cf533d..5320f5b 100644 --- a/src/socket/protocol.go +++ b/src/socket/protocol.go @@ -10,6 +10,7 @@ import ( const ( commandTerminator = "" pingCommand = "ping" + statusCommand = "status" socketReadBufferSize = 1024 )