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.
This commit is contained in:
Hector 2021-08-29 16:54:20 +00:00
parent 617d711ecf
commit 1964dde273
3 changed files with 160 additions and 4 deletions

View File

@ -49,11 +49,37 @@ var (
"Number of errors found since startup.", "Number of errors found since startup.",
[]string{"type"}, nil, []string{"type"}, nil,
) )
metricServerPing = prometheus.NewDesc( metricServerPing = prometheus.NewDesc(
prometheus.BuildFQName(sockNamespace, "", "up"), prometheus.BuildFQName(sockNamespace, "", "up"),
"Check if the fail2ban server is up", "Check if the fail2ban server is up",
nil, nil, 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 { type Exporter struct {
@ -73,6 +99,11 @@ func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
} }
if e.socket != nil { if e.socket != nil {
ch <- metricServerPing 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 { if e.socket != nil {
e.collectServerPingMetric(ch) 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() { func printAppVersion() {
fmt.Println(version) fmt.Println(version)
fmt.Printf(" build date: %s\r\n commit hash: %s\r\n built by: %s\r\n", date, commit, builtBy) fmt.Printf(" build date: %s\r\n commit hash: %s\r\n built by: %s\r\n", date, commit, builtBy)

View File

@ -1,11 +1,12 @@
package socket package socket
import ( import (
"log" "fmt"
"net"
"github.com/kisielk/og-rek" "github.com/kisielk/og-rek"
"github.com/nlpodyssey/gopickle/types" "github.com/nlpodyssey/gopickle/types"
"log"
"net"
"strings"
) )
type Fail2BanSocket struct { type Fail2BanSocket struct {
@ -13,6 +14,13 @@ type Fail2BanSocket struct {
encoder *ogórek.Encoder encoder *ogórek.Encoder
} }
type JailStats struct {
FailedCurrent int
FailedTotal int
BannedCurrent int
BannedTotal int
}
func MustConnectToSocket(path string) *Fail2BanSocket { func MustConnectToSocket(path string) *Fail2BanSocket {
c, err := net.Dial("unix", path) c, err := net.Dial("unix", path)
if err != nil { if err != nil {
@ -37,6 +45,85 @@ func (s *Fail2BanSocket) Ping() bool {
} }
log.Printf("unexpected response data: %s", t) 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 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
}

View File

@ -10,6 +10,7 @@ import (
const ( const (
commandTerminator = "<F2B_END_COMMAND>" commandTerminator = "<F2B_END_COMMAND>"
pingCommand = "ping" pingCommand = "ping"
statusCommand = "status"
socketReadBufferSize = 1024 socketReadBufferSize = 1024
) )