feat: support for textfile metrics (#13)

Add support for collecting arbitrary metrics from a textfile as well as
metrics collected from fail2ban. This allows other data to be exported
along with the fail2ban metrics (e.g. instance metadata).
Update the docker image to allow mounting a folder with a collection of
metric files to be exported. Only files ending in `.prom` with be read.
Update project README with the new functionality.
This commit is contained in:
Hector
2021-10-12 20:38:26 +00:00
parent 351d3344f7
commit 5a107cc547
7 changed files with 180 additions and 6 deletions

View File

@ -12,10 +12,12 @@ const (
)
type AppSettings struct {
VersionMode bool
MetricsPort int
Fail2BanDbPath string
Fail2BanSocketPath string
VersionMode bool
MetricsPort int
Fail2BanDbPath string
Fail2BanSocketPath string
FileCollectorPath string
FileCollectorEnabled bool
}
func Parse() *AppSettings {
@ -24,6 +26,8 @@ func Parse() *AppSettings {
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.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()
appSettings.validateFlags()
@ -42,6 +46,10 @@ func (settings *AppSettings) validateFlags() {
minServerPort, maxServerPort, settings.MetricsPort)
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 {
flag.Usage()

View File

@ -3,6 +3,7 @@ package main
import (
"fail2ban-prometheus-exporter/cfg"
"fail2ban-prometheus-exporter/export"
"fail2ban-prometheus-exporter/textfile"
"fmt"
"log"
"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() {
appSettings := cfg.Parse()
if appSettings.VersionMode {
@ -55,8 +61,13 @@ func main() {
exporter := export.NewExporter(appSettings, version)
prometheus.MustRegister(exporter)
textFileCollector := textfile.NewCollector(appSettings)
prometheus.MustRegister(textFileCollector)
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)
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)
}
}
}