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 )