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:
commit
d8ce799223
7 changed files with 180 additions and 6 deletions
25
README.md
25
README.md
|
@ -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.
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -16,6 +16,8 @@ type AppSettings struct {
|
||||||
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()
|
||||||
|
|
|
@ -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
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 a new issue