Merge pull request #104 from Galorhallen/master
Support multiple pihole instances
This commit is contained in:
commit
53b1034f19
6 changed files with 177 additions and 28 deletions
27
README.md
27
README.md
|
@ -76,6 +76,33 @@ $ docker run \
|
|||
ekofr/pihole-exporter:latest
|
||||
```
|
||||
|
||||
A single instance of pihole-exporter can monitor multiple pi-holes instances.
|
||||
To do so, you can specify a list of hostnames, protocols, passwords/API tokens and ports by separating them with commas in their respective environment variable:
|
||||
|
||||
```
|
||||
$ docker run \
|
||||
-e 'PIHOLE_PROTOCOL="http,http,http" \
|
||||
-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
|
||||
```
|
||||
|
||||
If port, protocol and API token/password is the same for all instances, you can specify them only once:
|
||||
|
||||
```
|
||||
$ docker run \
|
||||
-e 'PIHOLE_PROTOCOL=",http" \
|
||||
-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
|
||||
```
|
||||
|
||||
### From sources
|
||||
|
||||
Optionally, you can download and build it from the sources. You have to retrieve the project sources by using one of the following way:
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/heetch/confita"
|
||||
|
@ -15,29 +16,37 @@ import (
|
|||
|
||||
// Config is the exporter CLI configuration.
|
||||
type Config struct {
|
||||
PIHoleProtocol string `config:"pihole_protocol"`
|
||||
PIHoleHostname string `config:"pihole_hostname"`
|
||||
PIHolePort uint16 `config:"pihole_port"`
|
||||
PIHolePassword string `config:"pihole_password"`
|
||||
PIHoleApiToken string `config:"pihole_api_token"`
|
||||
Port string `config:"port"`
|
||||
PIHoleProtocol string `config:"pihole_protocol"`
|
||||
PIHoleHostname string `config:"pihole_hostname"`
|
||||
PIHolePort uint16 `config:"pihole_port"`
|
||||
PIHolePassword string `config:"pihole_password"`
|
||||
PIHoleApiToken string `config:"pihole_api_token"`
|
||||
}
|
||||
|
||||
type EnvConfig struct {
|
||||
PIHoleProtocol []string `config:"pihole_protocol"`
|
||||
PIHoleHostname []string `config:"pihole_hostname"`
|
||||
PIHolePort []uint16 `config:"pihole_port"`
|
||||
PIHolePassword []string `config:"pihole_password"`
|
||||
PIHoleApiToken []string `config:"pihole_api_token"`
|
||||
Port uint16 `config:"port"`
|
||||
Interval time.Duration `config:"interval"`
|
||||
}
|
||||
|
||||
func getDefaultConfig() *Config {
|
||||
return &Config{
|
||||
PIHoleProtocol: "http",
|
||||
PIHoleHostname: "127.0.0.1",
|
||||
PIHolePort: 80,
|
||||
PIHolePassword: "",
|
||||
PIHoleApiToken: "",
|
||||
Port: "9617",
|
||||
func getDefaultEnvConfig() *EnvConfig {
|
||||
return &EnvConfig{
|
||||
PIHoleProtocol: []string{"http"},
|
||||
PIHoleHostname: []string{"127.0.0.1"},
|
||||
PIHolePort: []uint16{80},
|
||||
PIHolePassword: []string{},
|
||||
PIHoleApiToken: []string{},
|
||||
Port: 9617,
|
||||
Interval: 10 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// Load method loads the configuration by using both flag or environment variables.
|
||||
func Load() *Config {
|
||||
func Load() (*EnvConfig, []Config) {
|
||||
loaders := []backend.Backend{
|
||||
env.NewBackend(),
|
||||
flags.NewBackend(),
|
||||
|
@ -45,7 +54,7 @@ func Load() *Config {
|
|||
|
||||
loader := confita.NewLoader(loaders...)
|
||||
|
||||
cfg := getDefaultConfig()
|
||||
cfg := getDefaultEnvConfig()
|
||||
err := loader.Load(context.Background(), cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
@ -53,7 +62,21 @@ func Load() *Config {
|
|||
|
||||
cfg.show()
|
||||
|
||||
return cfg
|
||||
return cfg, cfg.Split()
|
||||
}
|
||||
|
||||
func (c *Config) String() string {
|
||||
ref := reflect.ValueOf(c)
|
||||
fields := ref.Elem()
|
||||
|
||||
buffer := make([]string, fields.NumField(), fields.NumField())
|
||||
for i := 0; i < fields.NumField(); i++ {
|
||||
valueField := fields.Field(i)
|
||||
typeField := fields.Type().Field(i)
|
||||
buffer[i] = fmt.Sprintf("%s=%v", typeField.Name, valueField.Interface())
|
||||
}
|
||||
|
||||
return fmt.Sprintf("<Config@%X %s>", &c, strings.Join(buffer, ", "))
|
||||
}
|
||||
|
||||
//Validate check if the config is valid
|
||||
|
@ -64,6 +87,45 @@ func (c Config) Validate() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c EnvConfig) Split() []Config {
|
||||
result := make([]Config, 0, len(c.PIHoleHostname))
|
||||
|
||||
for i, hostname := range c.PIHoleHostname {
|
||||
config := Config{
|
||||
PIHoleHostname: hostname,
|
||||
PIHoleProtocol: c.PIHoleProtocol[i],
|
||||
PIHolePort: c.PIHolePort[i],
|
||||
}
|
||||
|
||||
if c.PIHoleApiToken != nil {
|
||||
if len(c.PIHoleApiToken) == 1 {
|
||||
if c.PIHoleApiToken[0] != "" {
|
||||
config.PIHoleApiToken = c.PIHoleApiToken[0]
|
||||
}
|
||||
} else if len(c.PIHoleApiToken) > 1 {
|
||||
if c.PIHoleApiToken[i] != "" {
|
||||
config.PIHoleApiToken = c.PIHoleApiToken[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.PIHolePassword != nil {
|
||||
if len(c.PIHolePassword) == 1 {
|
||||
if c.PIHolePassword[0] != "" {
|
||||
config.PIHolePassword = c.PIHolePassword[0]
|
||||
}
|
||||
} else if len(c.PIHolePassword) > 1 {
|
||||
if c.PIHolePassword[i] != "" {
|
||||
config.PIHolePassword = c.PIHolePassword[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, config)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (c Config) hostnameURL() string {
|
||||
s := fmt.Sprintf("%s://%s", c.PIHoleProtocol, c.PIHoleHostname)
|
||||
if c.PIHolePort != 0 {
|
||||
|
@ -82,7 +144,7 @@ func (c Config) PIHoleLoginURL() string {
|
|||
return c.hostnameURL() + "/admin/index.php?login"
|
||||
}
|
||||
|
||||
func (c Config) show() {
|
||||
func (c EnvConfig) show() {
|
||||
val := reflect.ValueOf(&c).Elem()
|
||||
log.Println("------------------------------------")
|
||||
log.Println("- PI-Hole exporter configuration -")
|
||||
|
@ -95,14 +157,14 @@ func (c Config) show() {
|
|||
if typeField.Name != "PIHolePassword" && typeField.Name != "PIHoleApiToken" {
|
||||
log.Println(fmt.Sprintf("%s : %v", typeField.Name, valueField.Interface()))
|
||||
} else {
|
||||
showAuthenticationMethod(typeField.Name, valueField.String())
|
||||
showAuthenticationMethod(typeField.Name, valueField.Len())
|
||||
}
|
||||
}
|
||||
log.Println("------------------------------------")
|
||||
}
|
||||
|
||||
func showAuthenticationMethod(name, value string) {
|
||||
if len(value) > 0 {
|
||||
func showAuthenticationMethod(name string, length int) {
|
||||
if length > 0 {
|
||||
log.Println(fmt.Sprintf("Pi-Hole Authentication Method : %s", name))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ import (
|
|||
|
||||
"github.com/eko/pihole-exporter/config"
|
||||
"github.com/eko/pihole-exporter/internal/metrics"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
// Client struct is a PI-Hole client to request an instance of a PI-Hole ad blocker.
|
||||
|
@ -32,6 +31,8 @@ func NewClient(config *config.Config) *Client {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Creating client with config %s\n", config)
|
||||
|
||||
return &Client{
|
||||
config: config,
|
||||
httpClient: http.Client{
|
||||
|
@ -42,6 +43,11 @@ func NewClient(config *config.Config) *Client {
|
|||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -56,6 +62,22 @@ func (c *Client) Metrics() http.HandlerFunc {
|
|||
log.Printf("New tick of statistics: %s", stats.ToString())
|
||||
promhttp.Handler().ServeHTTP(writer, request)
|
||||
}
|
||||
}*/
|
||||
|
||||
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)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) GetHostname() string {
|
||||
return c.config.PIHoleHostname
|
||||
}
|
||||
|
||||
func (c *Client) setMetrics(stats *Stats) {
|
||||
|
|
|
@ -31,6 +31,6 @@ type Stats struct {
|
|||
}
|
||||
|
||||
// ToString method returns a string of the current statistics struct.
|
||||
func (s *Stats) ToString() string {
|
||||
func (s *Stats) String() string {
|
||||
return fmt.Sprintf("%d ads blocked / %d total DNS queries", s.AdsBlockedToday, s.DNSQueriesAllTypes)
|
||||
}
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/eko/pihole-exporter/internal/pihole"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
|
@ -16,15 +20,35 @@ type Server struct {
|
|||
|
||||
// NewServer method initializes a new HTTP server instance and associates
|
||||
// the different routes that will be used by Prometheus (metrics) or for monitoring (readiness, liveness).
|
||||
func NewServer(port string, client *pihole.Client) *Server {
|
||||
func NewServer(port uint16, clients []*pihole.Client) *Server {
|
||||
mux := http.NewServeMux()
|
||||
httpServer := &http.Server{Addr: ":" + port, Handler: mux}
|
||||
httpServer := &http.Server{Addr: ":" + strconv.Itoa(int(port)), Handler: mux}
|
||||
|
||||
s := &Server{
|
||||
httpServer: httpServer,
|
||||
}
|
||||
|
||||
mux.Handle("/metrics", client.Metrics())
|
||||
mux.HandleFunc("/metrics",
|
||||
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)
|
||||
},
|
||||
)
|
||||
|
||||
mux.Handle("/readiness", s.readinessHandler())
|
||||
mux.Handle("/liveness", s.livenessHandler())
|
||||
|
||||
|
|
18
main.go
18
main.go
|
@ -11,12 +11,15 @@ import (
|
|||
)
|
||||
|
||||
func main() {
|
||||
conf := config.Load()
|
||||
envConf, clientConfigs := config.Load()
|
||||
|
||||
metrics.Init()
|
||||
|
||||
serverDead := make(chan struct{})
|
||||
s := server.NewServer(conf.Port, pihole.NewClient(conf))
|
||||
|
||||
clients := buildClients(clientConfigs)
|
||||
|
||||
s := server.NewServer(envConf.Port, clients)
|
||||
go func() {
|
||||
s.ListenAndServe()
|
||||
close(serverDead)
|
||||
|
@ -36,3 +39,14 @@ func main() {
|
|||
|
||||
fmt.Println("pihole-exporter HTTP server stopped")
|
||||
}
|
||||
|
||||
func buildClients(clientConfigs []config.Config) []*pihole.Client {
|
||||
clients := make([]*pihole.Client, 0, len(clientConfigs))
|
||||
for i := range clientConfigs {
|
||||
clientConfig := &clientConfigs[i]
|
||||
|
||||
client := pihole.NewClient(clientConfig)
|
||||
clients = append(clients, client)
|
||||
}
|
||||
return clients
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue