You've already forked prometheus-fail2ban-exporter
							
							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:
		
							
								
								
									
										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" | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Hector
					Hector