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
|
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
|
### 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:
|
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"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/heetch/confita"
|
"github.com/heetch/confita"
|
||||||
|
@ -20,24 +21,32 @@ type Config struct {
|
||||||
PIHolePort uint16 `config:"pihole_port"`
|
PIHolePort uint16 `config:"pihole_port"`
|
||||||
PIHolePassword string `config:"pihole_password"`
|
PIHolePassword string `config:"pihole_password"`
|
||||||
PIHoleApiToken string `config:"pihole_api_token"`
|
PIHoleApiToken string `config:"pihole_api_token"`
|
||||||
Port string `config:"port"`
|
}
|
||||||
|
|
||||||
|
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"`
|
Interval time.Duration `config:"interval"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDefaultConfig() *Config {
|
func getDefaultEnvConfig() *EnvConfig {
|
||||||
return &Config{
|
return &EnvConfig{
|
||||||
PIHoleProtocol: "http",
|
PIHoleProtocol: []string{"http"},
|
||||||
PIHoleHostname: "127.0.0.1",
|
PIHoleHostname: []string{"127.0.0.1"},
|
||||||
PIHolePort: 80,
|
PIHolePort: []uint16{80},
|
||||||
PIHolePassword: "",
|
PIHolePassword: []string{},
|
||||||
PIHoleApiToken: "",
|
PIHoleApiToken: []string{},
|
||||||
Port: "9617",
|
Port: 9617,
|
||||||
Interval: 10 * time.Second,
|
Interval: 10 * time.Second,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load method loads the configuration by using both flag or environment variables.
|
// Load method loads the configuration by using both flag or environment variables.
|
||||||
func Load() *Config {
|
func Load() (*EnvConfig, []Config) {
|
||||||
loaders := []backend.Backend{
|
loaders := []backend.Backend{
|
||||||
env.NewBackend(),
|
env.NewBackend(),
|
||||||
flags.NewBackend(),
|
flags.NewBackend(),
|
||||||
|
@ -45,7 +54,7 @@ func Load() *Config {
|
||||||
|
|
||||||
loader := confita.NewLoader(loaders...)
|
loader := confita.NewLoader(loaders...)
|
||||||
|
|
||||||
cfg := getDefaultConfig()
|
cfg := getDefaultEnvConfig()
|
||||||
err := loader.Load(context.Background(), cfg)
|
err := loader.Load(context.Background(), cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
@ -53,7 +62,21 @@ func Load() *Config {
|
||||||
|
|
||||||
cfg.show()
|
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
|
//Validate check if the config is valid
|
||||||
|
@ -64,6 +87,45 @@ func (c Config) Validate() error {
|
||||||
return nil
|
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 {
|
func (c Config) hostnameURL() string {
|
||||||
s := fmt.Sprintf("%s://%s", c.PIHoleProtocol, c.PIHoleHostname)
|
s := fmt.Sprintf("%s://%s", c.PIHoleProtocol, c.PIHoleHostname)
|
||||||
if c.PIHolePort != 0 {
|
if c.PIHolePort != 0 {
|
||||||
|
@ -82,7 +144,7 @@ func (c Config) PIHoleLoginURL() string {
|
||||||
return c.hostnameURL() + "/admin/index.php?login"
|
return c.hostnameURL() + "/admin/index.php?login"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Config) show() {
|
func (c EnvConfig) show() {
|
||||||
val := reflect.ValueOf(&c).Elem()
|
val := reflect.ValueOf(&c).Elem()
|
||||||
log.Println("------------------------------------")
|
log.Println("------------------------------------")
|
||||||
log.Println("- PI-Hole exporter configuration -")
|
log.Println("- PI-Hole exporter configuration -")
|
||||||
|
@ -95,14 +157,14 @@ func (c Config) show() {
|
||||||
if typeField.Name != "PIHolePassword" && typeField.Name != "PIHoleApiToken" {
|
if typeField.Name != "PIHolePassword" && typeField.Name != "PIHoleApiToken" {
|
||||||
log.Println(fmt.Sprintf("%s : %v", typeField.Name, valueField.Interface()))
|
log.Println(fmt.Sprintf("%s : %v", typeField.Name, valueField.Interface()))
|
||||||
} else {
|
} else {
|
||||||
showAuthenticationMethod(typeField.Name, valueField.String())
|
showAuthenticationMethod(typeField.Name, valueField.Len())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Println("------------------------------------")
|
log.Println("------------------------------------")
|
||||||
}
|
}
|
||||||
|
|
||||||
func showAuthenticationMethod(name, value string) {
|
func showAuthenticationMethod(name string, length int) {
|
||||||
if len(value) > 0 {
|
if length > 0 {
|
||||||
log.Println(fmt.Sprintf("Pi-Hole Authentication Method : %s", name))
|
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/config"
|
||||||
"github.com/eko/pihole-exporter/internal/metrics"
|
"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.
|
// 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)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Creating client with config %s\n", config)
|
||||||
|
|
||||||
return &Client{
|
return &Client{
|
||||||
config: config,
|
config: config,
|
||||||
httpClient: http.Client{
|
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
|
// Metrics scrapes pihole and sets them
|
||||||
func (c *Client) Metrics() http.HandlerFunc {
|
func (c *Client) Metrics() http.HandlerFunc {
|
||||||
return func(writer http.ResponseWriter, request *http.Request) {
|
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())
|
log.Printf("New tick of statistics: %s", stats.ToString())
|
||||||
promhttp.Handler().ServeHTTP(writer, request)
|
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) {
|
func (c *Client) setMetrics(stats *Stats) {
|
||||||
|
|
|
@ -31,6 +31,6 @@ type Stats struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToString method returns a string of the current statistics 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)
|
return fmt.Sprintf("%d ads blocked / %d total DNS queries", s.AdsBlockedToday, s.DNSQueriesAllTypes)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/eko/pihole-exporter/internal/pihole"
|
"github.com/eko/pihole-exporter/internal/pihole"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,15 +20,35 @@ type Server struct {
|
||||||
|
|
||||||
// NewServer method initializes a new HTTP server instance and associates
|
// 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).
|
// 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()
|
mux := http.NewServeMux()
|
||||||
httpServer := &http.Server{Addr: ":" + port, Handler: mux}
|
httpServer := &http.Server{Addr: ":" + strconv.Itoa(int(port)), Handler: mux}
|
||||||
|
|
||||||
s := &Server{
|
s := &Server{
|
||||||
httpServer: httpServer,
|
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("/readiness", s.readinessHandler())
|
||||||
mux.Handle("/liveness", s.livenessHandler())
|
mux.Handle("/liveness", s.livenessHandler())
|
||||||
|
|
||||||
|
|
18
main.go
18
main.go
|
@ -11,12 +11,15 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
conf := config.Load()
|
envConf, clientConfigs := config.Load()
|
||||||
|
|
||||||
metrics.Init()
|
metrics.Init()
|
||||||
|
|
||||||
serverDead := make(chan struct{})
|
serverDead := make(chan struct{})
|
||||||
s := server.NewServer(conf.Port, pihole.NewClient(conf))
|
|
||||||
|
clients := buildClients(clientConfigs)
|
||||||
|
|
||||||
|
s := server.NewServer(envConf.Port, clients)
|
||||||
go func() {
|
go func() {
|
||||||
s.ListenAndServe()
|
s.ListenAndServe()
|
||||||
close(serverDead)
|
close(serverDead)
|
||||||
|
@ -36,3 +39,14 @@ func main() {
|
||||||
|
|
||||||
fmt.Println("pihole-exporter HTTP server stopped")
|
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