diff --git a/README.md b/README.md index c5cc2a3..e16cf99 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,6 @@ You can run it using the following example and pass configuration environment va $ docker run \ -e 'PIHOLE_HOSTNAME=192.168.1.2' \ -e 'PIHOLE_PASSWORD=mypassword' \ - -e 'INTERVAL=30s' \ -e 'PORT=9617' \ -p 9617:9617 \ ekofr/pihole-exporter:latest @@ -56,7 +55,6 @@ $ API_TOKEN=$(awk -F= -v key="WEBPASSWORD" '$1==key {print $2}' /etc/pihole/setu $ docker run \ -e 'PIHOLE_HOSTNAME=192.168.1.2' \ -e "PIHOLE_API_TOKEN=$API_TOKEN" \ - -e 'INTERVAL=30s' \ -e 'PORT=9617' \ ekofr/pihole-exporter:latest ``` @@ -69,7 +67,6 @@ $ docker run \ -e 'PIHOLE_PROTOCOL=https' \ -e 'PIHOLE_HOSTNAME=192.168.1.2' \ -e 'PIHOLE_PASSWORD=mypassword' \ - -e 'INTERVAL=30s' \ -e 'PORT=9617' \ -v '/etc/ssl/certs:/etc/ssl/certs:ro' \ -p 9617:9617 \ @@ -85,7 +82,6 @@ $ docker run \ -e 'PIHOLE_HOSTNAME="192.168.1.2,192.168.1.3,192.168.1.4"' \ -e "PIHOLE_API_TOKEN="$API_TOKEN1,$API_TOKEN2,$API_TOKEN3" \ -e "PIHOLE_PORT="8080,8081,8080" \ - -e 'INTERVAL=30s' \ -e 'PORT=9617' \ ekofr/pihole-exporter:latest ``` @@ -98,7 +94,6 @@ $ docker run \ -e 'PIHOLE_HOSTNAME="192.168.1.2,192.168.1.3,192.168.1.4"' \ -e "PIHOLE_API_TOKEN="$API_TOKEN" \ -e "PIHOLE_PORT="8080" \ - -e 'INTERVAL=30s' \ -e 'PORT=9617' \ ekofr/pihole-exporter:latest ``` @@ -147,7 +142,7 @@ $ ./pihole_exporter -pihole_hostname 192.168.1.10 -pihole_api_token $API_TOKEN 2019/05/09 20:19:52 PIHoleHostname : 192.168.1.10 2019/05/09 20:19:52 PIHolePassword : azerty 2019/05/09 20:19:52 Port : 9617 -2019/05/09 20:19:52 Interval : 10s +2019/05/09 20:19:52 Timeout : 5s 2019/05/09 20:19:52 ------------------------------------ 2019/05/09 20:19:52 New Prometheus metric registered: domains_blocked 2019/05/09 20:19:52 New Prometheus metric registered: dns_queries_today @@ -182,15 +177,14 @@ scrape_configs: ## Available CLI options ```bash -# Interval of time the exporter will fetch data from PI-Hole - -interval duration (optional) (default 10s) - -# Hostname of the Raspberry PI where PI-Hole is installed +# Hostname of the host(s) where PI-Hole is installed -pihole_hostname string (optional) (default "127.0.0.1") # Password defined on the PI-Hole interface -pihole_password string (optional) +# Timeout to connect and retrieve data from a Pi-Hole instance + -timeout duration (optional) (default 5s) # WEBPASSWORD / api token defined on the PI-Hole interface at `/etc/pihole/setupVars.conf` -pihole_api_token string (optional) diff --git a/config/configuration.go b/config/configuration.go index 5f75dbd..32131bf 100644 --- a/config/configuration.go +++ b/config/configuration.go @@ -4,11 +4,12 @@ import ( "context" "errors" "fmt" - "log" "reflect" "strings" "time" + log "github.com/sirupsen/logrus" + "github.com/heetch/confita" "github.com/heetch/confita/backend" "github.com/heetch/confita/backend/env" @@ -31,7 +32,7 @@ type EnvConfig struct { PIHolePassword []string `config:"pihole_password"` PIHoleApiToken []string `config:"pihole_api_token"` Port uint16 `config:"port"` - Interval time.Duration `config:"interval"` + Timeout time.Duration `config:"timeout"` } func getDefaultEnvConfig() *EnvConfig { @@ -42,7 +43,7 @@ func getDefaultEnvConfig() *EnvConfig { PIHolePassword: []string{}, PIHoleApiToken: []string{}, Port: 9617, - Interval: 10 * time.Second, + Timeout: 5 * time.Second, } } @@ -85,6 +86,7 @@ func (c *Config) String() string { } } + buffer = removeEmptyString(buffer) return fmt.Sprintf("", &c, strings.Join(buffer, ", ")) } @@ -156,6 +158,16 @@ func extractStringConfig(data []string, idx int, hostsCount int) (bool, string, return false, "", true } +func removeEmptyString(source []string) []string { + var result []string + for _, s := range source { + if s != "" { + result = append(result, s) + } + } + return result +} + func (c Config) hostnameURL() string { s := fmt.Sprintf("%s://%s", c.PIHoleProtocol, c.PIHoleHostname) if c.PIHolePort != 0 { @@ -176,25 +188,25 @@ func (c Config) PIHoleLoginURL() string { func (c EnvConfig) show() { val := reflect.ValueOf(&c).Elem() - log.Println("------------------------------------") - log.Println("- PI-Hole exporter configuration -") - log.Println("------------------------------------") + log.Info("------------------------------------") + log.Info("- PI-Hole exporter configuration -") + log.Info("------------------------------------") for i := 0; i < val.NumField(); i++ { valueField := val.Field(i) typeField := val.Type().Field(i) // Do not print password or api token but do print the authentication method if typeField.Name != "PIHolePassword" && typeField.Name != "PIHoleApiToken" { - log.Println(fmt.Sprintf("%s : %v", typeField.Name, valueField.Interface())) + log.Info(fmt.Sprintf("%s : %v", typeField.Name, valueField.Interface())) } else { showAuthenticationMethod(typeField.Name, valueField.Len()) } } - log.Println("------------------------------------") + log.Info("------------------------------------") } func showAuthenticationMethod(name string, length int) { if length > 0 { - log.Println(fmt.Sprintf("Pi-Hole Authentication Method : %s", name)) + log.Info(fmt.Sprintf("Pi-Hole Authentication Method : %s", name)) } } diff --git a/go.mod b/go.mod index a3c8a75..0dfa482 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.17 require ( github.com/heetch/confita v0.10.0 github.com/prometheus/client_golang v1.12.1 + github.com/sirupsen/logrus v1.6.0 github.com/stretchr/testify v1.7.0 github.com/xonvanetta/shutdown v0.0.3 golang.org/x/net v0.0.0-20210525063256-abc453219eb5 @@ -15,6 +16,7 @@ require ( github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang/protobuf v1.5.2 // indirect + github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/go.sum b/go.sum index ed0c84c..3232e2d 100644 --- a/go.sum +++ b/go.sum @@ -199,6 +199,7 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= @@ -278,6 +279,7 @@ github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIH github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go index 3a50a3a..c824713 100644 --- a/internal/metrics/metrics.go +++ b/internal/metrics/metrics.go @@ -1,9 +1,8 @@ package metrics import ( - "log" - "github.com/prometheus/client_golang/prometheus" + log "github.com/sirupsen/logrus" ) var ( @@ -201,5 +200,5 @@ func Init() { func initMetric(name string, metric *prometheus.GaugeVec) { prometheus.MustRegister(metric) - log.Printf("New Prometheus metric registered: %s", name) + log.Info("New Prometheus metric registered: ", name) } diff --git a/internal/pihole/client.go b/internal/pihole/client.go index 5bb97c6..683961e 100644 --- a/internal/pihole/client.go +++ b/internal/pihole/client.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "io/ioutil" - "log" "net/http" "net/url" "os" @@ -12,22 +11,51 @@ import ( "strings" "time" + log "github.com/sirupsen/logrus" + "github.com/eko/pihole-exporter/config" "github.com/eko/pihole-exporter/internal/metrics" ) +type ClientStatus byte + +const ( + MetricsCollectionInProgress ClientStatus = iota + MetricsCollectionSuccess + MetricsCollectionError + MetricsCollectionTimeout +) + +func (status ClientStatus) String() string { + return []string{"MetricsCollectionInProgress", "MetricsCollectionSuccess", "MetricsCollectionError", "MetricsCollectionTimeout"}[status] +} + +type ClientChannel struct { + Status ClientStatus + Err error +} + +func (c *ClientChannel) String() string { + if c.Err != nil { + return fmt.Sprintf("ClientChannel", c.Status, c.Err.Error()) + } else { + return fmt.Sprintf("ClientChannel>", c.Status) + } +} + // Client struct is a PI-Hole client to request an instance of a PI-Hole ad blocker. type Client struct { httpClient http.Client interval time.Duration config *config.Config + Status chan *ClientChannel } // NewClient method initializes a new PI-Hole client. -func NewClient(config *config.Config) *Client { +func NewClient(config *config.Config, envConfig *config.EnvConfig) *Client { err := config.Validate() if err != nil { - log.Print(err) + log.Error(err) os.Exit(1) } @@ -39,7 +67,9 @@ func NewClient(config *config.Config) *Client { CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, + Timeout: envConfig.Timeout, }, + Status: make(chan *ClientChannel, 1), } } @@ -47,32 +77,24 @@ func (c *Client) String() string { return c.config.PIHoleHostname } -/* -// Metrics scrapes pihole and sets them -func (c *Client) Metrics() http.HandlerFunc { - return func(writer http.ResponseWriter, request *http.Request) { - stats, err := c.getStatistics() - if err != nil { - writer.WriteHeader(http.StatusBadRequest) - _, _ = writer.Write([]byte(err.Error())) - return - } +func (c *Client) CollectMetricsAsync(writer http.ResponseWriter, request *http.Request) { + log.Printf("Collecting from %s", c.config.PIHoleHostname) + if stats, err := c.getStatistics(); err == nil { c.setMetrics(stats) - - log.Printf("New tick of statistics: %s", stats.ToString()) - promhttp.Handler().ServeHTTP(writer, request) + c.Status <- &ClientChannel{Status: MetricsCollectionSuccess, Err: nil} + log.Printf("New tick of statistics from %s: %s", c.config.PIHoleHostname, stats) + } else { + c.Status <- &ClientChannel{Status: MetricsCollectionError, Err: err} } -}*/ +} func (c *Client) CollectMetrics(writer http.ResponseWriter, request *http.Request) error { - stats, err := c.getStatistics() if err != nil { return err } c.setMetrics(stats) - - log.Printf("New tick of statistics from %s: %s", c, stats) + log.Printf("New tick of statistics from %s: %s", c.config.PIHoleHostname, stats) return nil } @@ -137,7 +159,7 @@ func (c *Client) getPHPSessionID() (sessionID string) { resp, err := c.httpClient.Do(req) if err != nil { - log.Printf("An error has occured during login to PI-Hole: %v", err) + log.Errorf("An error has occured during login to PI-Hole: %v", err) } for _, cookie := range resp.Cookies() { diff --git a/internal/server/server.go b/internal/server/server.go index 55968b5..270b99f 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -2,7 +2,6 @@ package server import ( "fmt" - "log" "net/http" "strconv" "strings" @@ -10,6 +9,7 @@ import ( "github.com/eko/pihole-exporter/internal/pihole" "github.com/prometheus/client_golang/prometheus/promhttp" + log "github.com/sirupsen/logrus" "golang.org/x/net/context" ) @@ -22,32 +22,31 @@ type Server struct { // the different routes that will be used by Prometheus (metrics) or for monitoring (readiness, liveness). func NewServer(port uint16, clients []*pihole.Client) *Server { mux := http.NewServeMux() - httpServer := &http.Server{Addr: ":" + strconv.Itoa(int(port)), Handler: mux} + httpServer := &http.Server{ + Addr: ":" + strconv.Itoa(int(port)), + Handler: mux, + } s := &Server{ httpServer: httpServer, } - mux.HandleFunc("/metrics", - func(writer http.ResponseWriter, request *http.Request) { - errors := make([]string, 0) + mux.HandleFunc("/metrics", func(writer http.ResponseWriter, request *http.Request) { + log.Printf("request.Header: %v\n", request.Header) - for _, client := range clients { - if err := client.CollectMetrics(writer, request); err != nil { - errors = append(errors, err.Error()) - fmt.Printf("Error %s\n", err) - } + for _, client := range clients { + go client.CollectMetricsAsync(writer, request) + } + + for _, client := range clients { + status := <-client.Status + if status.Status == pihole.MetricsCollectionError { + log.Printf("An error occured while contacting %s: %s", client.GetHostname(), status.Err.Error()) } + } - if len(errors) == len(clients) { - writer.WriteHeader(http.StatusBadRequest) - body := strings.Join(errors, "\n") - _, _ = writer.Write([]byte(body)) - } - - promhttp.Handler().ServeHTTP(writer, request) - }, - ) + promhttp.Handler().ServeHTTP(writer, request) + }) mux.Handle("/readiness", s.readinessHandler()) mux.Handle("/liveness", s.livenessHandler()) @@ -73,6 +72,27 @@ func (s *Server) Stop() { s.httpServer.Shutdown(ctx) } +func (s *Server) handleMetrics(clients []*pihole.Client) http.HandlerFunc { + return func(writer http.ResponseWriter, request *http.Request) { + errors := make([]string, 0) + + for _, client := range clients { + if err := client.CollectMetrics(writer, request); err != nil { + errors = append(errors, err.Error()) + fmt.Printf("Error %s\n", err) + } + } + + if len(errors) == len(clients) { + writer.WriteHeader(http.StatusBadRequest) + body := strings.Join(errors, "\n") + _, _ = writer.Write([]byte(body)) + } + + promhttp.Handler().ServeHTTP(writer, request) + } +} + func (s *Server) readinessHandler() http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { if s.isReady() { diff --git a/main.go b/main.go index 1adb3de..ad37555 100644 --- a/main.go +++ b/main.go @@ -20,7 +20,7 @@ func main() { serverDead := make(chan struct{}) - clients := buildClients(clientConfigs) + clients := buildClients(clientConfigs, envConf) s := server.NewServer(envConf.Port, clients) go func() { @@ -43,12 +43,12 @@ func main() { log.Println("pihole-exporter HTTP server stopped") } -func buildClients(clientConfigs []config.Config) []*pihole.Client { +func buildClients(clientConfigs []config.Config, envConfig *config.EnvConfig) []*pihole.Client { clients := make([]*pihole.Client, 0, len(clientConfigs)) for i := range clientConfigs { clientConfig := &clientConfigs[i] - client := pihole.NewClient(clientConfig) + client := pihole.NewClient(clientConfig, envConfig) clients = append(clients, client) } return clients