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:
parent
351d3344f7
commit
5a107cc547
25
README.md
25
README.md
@ -52,6 +52,10 @@ $ fail2ban-prometheus-exporter -h
|
||||
path to the fail2ban server socket
|
||||
-version
|
||||
show version info and exit
|
||||
-collector.textfile
|
||||
enable the textfile collector
|
||||
-collector.textfile.directory string
|
||||
directory to read text files with metrics from
|
||||
```
|
||||
|
||||
**Example**
|
||||
@ -240,3 +244,24 @@ fail2ban_errors{type="db"} 0
|
||||
# TYPE fail2ban_up gauge
|
||||
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.
|
||||
|
@ -5,6 +5,8 @@
|
||||
|
||||
db_path=/app/fail2ban.sqlite3
|
||||
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.
|
||||
if [ ! -f "$db_path" ]; then
|
||||
@ -13,9 +15,14 @@ fi
|
||||
if [ ! -S "$socket_path" ]; then
|
||||
socket_path=""
|
||||
fi
|
||||
if [ -d $textfile_dir ]; then
|
||||
textfile_enabled=true
|
||||
fi
|
||||
|
||||
# Start the exporter (use exec to support graceful shutdown)
|
||||
# Inspired by: https://akomljen.com/stopping-docker-containers-gracefully/
|
||||
exec /app/fail2ban-prometheus-exporter \
|
||||
-db "$db_path" \
|
||||
-socket "$socket_path"
|
||||
-socket "$socket_path" \
|
||||
-collector.textfile=$textfile_enabled \
|
||||
-collector.textfile.directory="$textfile_dir"
|
||||
|
@ -16,6 +16,8 @@ type AppSettings struct {
|
||||
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()
|
||||
|
@ -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
48
src/textfile/collector.go
Normal 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
55
src/textfile/file.go
Normal 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
20
src/textfile/writer.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user