Merge branch 'add-support-for-exposing-metrics-from-text-file' into 'main'

Add support for exposing metrics from text file

See merge request hectorjsmith/fail2ban-prometheus-exporter!38
This commit is contained in:
Hector 2021-10-12 20:38:26 +00:00
commit d8ce799223
7 changed files with 180 additions and 6 deletions

View File

@ -52,6 +52,10 @@ $ fail2ban-prometheus-exporter -h
path to the fail2ban server socket path to the fail2ban server socket
-version -version
show version info and exit show version info and exit
-collector.textfile
enable the textfile collector
-collector.textfile.directory string
directory to read text files with metrics from
``` ```
**Example** **Example**
@ -240,3 +244,24 @@ fail2ban_errors{type="db"} 0
# TYPE fail2ban_up gauge # TYPE fail2ban_up gauge
fail2ban_up 1 fail2ban_up 1
``` ```
### 4.3. Textfile Metrics
For more flexibility the exporter also allows exporting metrics collected from a text file.
To enable textfile metrics:
1. Enable the collector with `-collector.textfile=true`
2. Provide the directory to read files from with the `-collector.textfile.directory` flag
Metrics collected from these files will be exposed directly alongside the other metrics without any additional processing.
This means that it is the responsibility of the file creator to ensure the format is correct.
By exporting textfile metrics an extra metric is also exported with an error count for each file:
```
# HELP textfile_error Checks for errors while reading text files
# TYPE textfile_error gauge
textfile_error{path="file.prom"} 0
```
**NOTE:** Any file not ending with `.prom` will be ignored.

View File

@ -5,6 +5,8 @@
db_path=/app/fail2ban.sqlite3 db_path=/app/fail2ban.sqlite3
socket_path=/var/run/fail2ban/fail2ban.sock socket_path=/var/run/fail2ban/fail2ban.sock
textfile_dir=/app/textfile/
textfile_enabled=false
# Blank out the file paths if they do not exist - a hacky way to only use these files if they were mounted into the container. # Blank out the file paths if they do not exist - a hacky way to only use these files if they were mounted into the container.
if [ ! -f "$db_path" ]; then if [ ! -f "$db_path" ]; then
@ -13,9 +15,14 @@ fi
if [ ! -S "$socket_path" ]; then if [ ! -S "$socket_path" ]; then
socket_path="" socket_path=""
fi fi
if [ -d $textfile_dir ]; then
textfile_enabled=true
fi
# Start the exporter (use exec to support graceful shutdown) # Start the exporter (use exec to support graceful shutdown)
# Inspired by: https://akomljen.com/stopping-docker-containers-gracefully/ # Inspired by: https://akomljen.com/stopping-docker-containers-gracefully/
exec /app/fail2ban-prometheus-exporter \ exec /app/fail2ban-prometheus-exporter \
-db "$db_path" \ -db "$db_path" \
-socket "$socket_path" -socket "$socket_path" \
-collector.textfile=$textfile_enabled \
-collector.textfile.directory="$textfile_dir"

View File

@ -12,10 +12,12 @@ const (
) )
type AppSettings struct { type AppSettings struct {
VersionMode bool VersionMode bool
MetricsPort int MetricsPort int
Fail2BanDbPath string Fail2BanDbPath string
Fail2BanSocketPath string Fail2BanSocketPath string
FileCollectorPath string
FileCollectorEnabled bool
} }
func Parse() *AppSettings { func Parse() *AppSettings {
@ -24,6 +26,8 @@ func Parse() *AppSettings {
flag.IntVar(&appSettings.MetricsPort, "port", 9191, "port to use for the metrics server") flag.IntVar(&appSettings.MetricsPort, "port", 9191, "port to use for the metrics server")
flag.StringVar(&appSettings.Fail2BanDbPath, "db", "", "path to the fail2ban sqlite database (deprecated)") flag.StringVar(&appSettings.Fail2BanDbPath, "db", "", "path to the fail2ban sqlite database (deprecated)")
flag.StringVar(&appSettings.Fail2BanSocketPath, "socket", "", "path to the fail2ban server socket") flag.StringVar(&appSettings.Fail2BanSocketPath, "socket", "", "path to the fail2ban server socket")
flag.BoolVar(&appSettings.FileCollectorEnabled, "collector.textfile", false, "enable the textfile collector")
flag.StringVar(&appSettings.FileCollectorPath, "collector.textfile.directory", "", "directory to read text files with metrics from")
flag.Parse() flag.Parse()
appSettings.validateFlags() appSettings.validateFlags()
@ -42,6 +46,10 @@ func (settings *AppSettings) validateFlags() {
minServerPort, maxServerPort, settings.MetricsPort) minServerPort, maxServerPort, settings.MetricsPort)
flagsValid = false flagsValid = false
} }
if settings.FileCollectorEnabled && settings.FileCollectorPath == "" {
fmt.Printf("file collector directory path must not be empty if collector enabled\n")
flagsValid = false
}
} }
if !flagsValid { if !flagsValid {
flag.Usage() flag.Usage()

View File

@ -3,6 +3,7 @@ package main
import ( import (
"fail2ban-prometheus-exporter/cfg" "fail2ban-prometheus-exporter/cfg"
"fail2ban-prometheus-exporter/export" "fail2ban-prometheus-exporter/export"
"fail2ban-prometheus-exporter/textfile"
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
@ -43,6 +44,11 @@ func rootHtmlHandler(w http.ResponseWriter, r *http.Request) {
} }
} }
func metricHandler(w http.ResponseWriter, r *http.Request, collector *textfile.Collector) {
promhttp.Handler().ServeHTTP(w, r)
collector.WriteTextFileMetrics(w, r)
}
func main() { func main() {
appSettings := cfg.Parse() appSettings := cfg.Parse()
if appSettings.VersionMode { if appSettings.VersionMode {
@ -55,8 +61,13 @@ func main() {
exporter := export.NewExporter(appSettings, version) exporter := export.NewExporter(appSettings, version)
prometheus.MustRegister(exporter) prometheus.MustRegister(exporter)
textFileCollector := textfile.NewCollector(appSettings)
prometheus.MustRegister(textFileCollector)
http.HandleFunc("/", rootHtmlHandler) http.HandleFunc("/", rootHtmlHandler)
http.Handle(metricsPath, promhttp.Handler()) http.HandleFunc(metricsPath, func(w http.ResponseWriter, r *http.Request) {
metricHandler(w, r, textFileCollector)
})
log.Printf("metrics available at '%s'", metricsPath) log.Printf("metrics available at '%s'", metricsPath)
svrErr := make(chan error) svrErr := make(chan error)

48
src/textfile/collector.go Normal file
View File

@ -0,0 +1,48 @@
package textfile
import (
"fail2ban-prometheus-exporter/cfg"
"github.com/prometheus/client_golang/prometheus"
"log"
)
type Collector struct {
enabled bool
folderPath string
fileMap map[string]*fileData
}
type fileData struct {
readErrors int
fileName string
fileContents []byte
}
func NewCollector(appSettings *cfg.AppSettings) *Collector {
collector := &Collector{
enabled: appSettings.FileCollectorEnabled,
folderPath: appSettings.FileCollectorPath,
fileMap: make(map[string]*fileData),
}
if collector.enabled {
log.Printf("collector.textfile directory: %s", collector.folderPath)
}
return collector
}
func (c *Collector) Describe(ch chan<- *prometheus.Desc) {
if c.enabled {
ch <- metricReadError
}
}
func (c *Collector) Collect(ch chan<- prometheus.Metric) {
if c.enabled {
c.collectFileContents()
c.collectFileErrors(ch)
}
}
func (c *Collector) appendErrorForPath(path string) {
c.fileMap[path].readErrors++
}

55
src/textfile/file.go Normal file
View File

@ -0,0 +1,55 @@
package textfile
import (
"github.com/prometheus/client_golang/prometheus"
"io/ioutil"
"log"
"path/filepath"
"strings"
)
const namespace = "textfile"
var (
metricReadError = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "error"),
"Checks for errors while reading text files",
[]string{"path"}, nil,
)
)
func (c *Collector) collectFileContents() {
files, err := ioutil.ReadDir(c.folderPath)
if err != nil {
log.Printf("error reading directory '%s': %v", c.folderPath, err)
return
}
for _, file := range files {
fileName := file.Name()
if !strings.HasSuffix(strings.ToLower(fileName), ".prom") {
continue
}
c.fileMap[fileName] = &fileData{
readErrors: 0,
fileName: fileName,
}
fullPath := filepath.Join(c.folderPath, fileName)
content, err := ioutil.ReadFile(fullPath)
if err != nil {
c.appendErrorForPath(fileName)
log.Printf("error reading contents of file '%s': %v", fileName, err)
}
c.fileMap[fileName].fileContents = content
}
}
func (c *Collector) collectFileErrors(ch chan<- prometheus.Metric) {
for _, f := range c.fileMap {
ch <- prometheus.MustNewConstMetric(
metricReadError, prometheus.GaugeValue, float64(f.readErrors), f.fileName,
)
}
}

20
src/textfile/writer.go Normal file
View File

@ -0,0 +1,20 @@
package textfile
import (
"log"
"net/http"
)
func (c *Collector) WriteTextFileMetrics(w http.ResponseWriter, r *http.Request) {
if !c.enabled {
return
}
for _, f := range c.fileMap {
_, err := w.Write(f.fileContents)
if err != nil {
c.appendErrorForPath(f.fileName)
log.Printf("error writing file contents to response writer '%s': %v", f.fileName, err)
}
}
}