Initialize

This commit is contained in:
Vincent Composieux 2019-05-08 23:45:04 +02:00
commit 7457599d3e
No known key found for this signature in database
GPG Key ID: 7306974ABA6382AC
11 changed files with 563 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
search-api
bin
tmp
.vscode
report.xml
debug
.idea/

19
LICENSE Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2019 Vincent Composieux <github@composieux.fr>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

46
README.md Normal file
View File

@ -0,0 +1,46 @@
# PI-Hole Prometheus Exporter
This is a Prometheus exporter for [PI-Hole](https://pi-hole.net/)'s Raspberry PI ad blocker.
## Prerequisites
* [Go](https://golang.org/doc/)
## Installation
### Manually
First, retrieve the project:
```bash
$ go get -u github.com/eko/pihole-exporter
# or
$ git clone https://github.com/eko/pihole-exporter.git
```
Then, build the binary:
```bash
$ GOOS=linux GOARCH=arm GOARM=7 go build -o pihole_exporter .
```
## Usage
In order to run the exporter, type the following command (arguments are optional):
```bash
$ ./pihole_exporter -pihole_hostname 192.168.1.10 -pihole_password azerty
```
## Available options
```bash
# Interval of time the exporter will fetch data from PI-Hole
-interval duration (optional) (default 5s)
# Hostname of the Raspberry PI 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)
# Port to be used for the exporter
-port string (optional) (default "9311")
```

65
config/configuration.go Normal file
View File

@ -0,0 +1,65 @@
package config
import (
"context"
"fmt"
"log"
"reflect"
"time"
"github.com/heetch/confita"
"github.com/heetch/confita/backend"
"github.com/heetch/confita/backend/env"
"github.com/heetch/confita/backend/flags"
)
type Config struct {
PIHoleHostname string `config:"pihole_hostname"`
PIHolePassword string `config:"pihole_password"`
Port string `config:"port"`
Interval time.Duration `config:"interval"`
}
func getDefaultConfig() *Config {
return &Config{
PIHoleHostname: "127.0.0.1",
PIHolePassword: "",
Port: "9311",
Interval: 5 * time.Second,
}
}
func Load() *Config {
loaders := []backend.Backend{
env.NewBackend(),
flags.NewBackend(),
}
loader := confita.NewLoader(loaders...)
cfg := getDefaultConfig()
err := loader.Load(context.Background(), cfg)
if err != nil {
panic(err)
}
cfg.show()
return cfg
}
func (c Config) show() {
val := reflect.ValueOf(&c).Elem()
log.Println("------------------------------------")
log.Println("- PI-Hole exporter configuration -")
log.Println("------------------------------------")
for i := 0; i < val.NumField(); i++ {
valueField := val.Field(i)
typeField := val.Type().Field(i)
log.Println(fmt.Sprintf("%s : %v", typeField.Name, valueField.Interface()))
}
log.Println("------------------------------------")
}

11
go.mod Normal file
View File

@ -0,0 +1,11 @@
module github.com/eko/pihole-exporter
go 1.12
require (
github.com/heetch/confita v0.5.1
github.com/pkg/errors v0.8.1 // indirect
github.com/prometheus/client_golang v0.9.2
github.com/stretchr/testify v1.3.0 // indirect
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c
)

33
go.sum Normal file
View File

@ -0,0 +1,33 @@
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/heetch/confita v0.5.1 h1:EiE32j+Ze0sI0YBeJDSdqTZ32uKz2XCTQIzSgwgfnvk=
github.com/heetch/confita v0.5.1/go.mod h1:S8Em4kuK8pR5vfTiaNkFLfNDMlGF/EtQUaCxDhXRpCs=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740=
github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 h1:PnBWHBf+6L0jOqq0gIVUe6Yk0/QMZ640k6NvkxcBf+8=
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nLJdBg+pBmGgkJlSaKC2KaQmTCk1XDtE=
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

118
internal/metrics/metrics.go Normal file
View File

@ -0,0 +1,118 @@
package metrics
import (
"log"
"github.com/prometheus/client_golang/prometheus"
)
var (
// DomainsBlocked - The number of domains being blocked by PI-Hole.
DomainsBlocked = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "domains_being_blocked",
Namespace: "pihole",
Help: "This represent the number of domains being blocked",
},
)
// DNSQueriesToday - The number of DNS requests made over PI-Hole over the current day.
DNSQueriesToday = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "dns_queries_today",
Namespace: "pihole",
Help: "This represent the number of DNS queries made over the current day",
},
)
// AdsBlockedToday - The number of ads blocked by PI-Hole over the current day.
AdsBlockedToday = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "ads_blocked_today",
Namespace: "pihole",
Help: "This represent the number of ads blocked over the current day",
},
)
// AdsPercentageToday - The percentage of ads blocked by PI-Hole over the current day.
AdsPercentageToday = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "ads_percentage_today",
Namespace: "pihole",
Help: "This represent the percentage of ads blocked over the current day",
},
)
// UniqueDomains - The number of unique domains seen by PI-Hole.
UniqueDomains = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "unique_domains",
Namespace: "pihole",
Help: "This represent the number of unique domains seen",
},
)
// QueriesForwarded - The number of queries forwarded by PI-Hole.
QueriesForwarded = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "queries_forwarded",
Namespace: "pihole",
Help: "This represent the number of queries forwarded",
},
)
// QueriesCached - The number of queries cached by PI-Hole.
QueriesCached = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "queries_cached",
Namespace: "pihole",
Help: "This represent the number of queries cached",
},
)
// ClientsEverSeen - The number of clients ever seen by PI-Hole.
ClientsEverSeen = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "clients_ever_seen",
Namespace: "pihole",
Help: "This represent the number of clients ever seen",
},
)
// UniqueClients - The number of unique clients seen by PI-Hole.
UniqueClients = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "unique_clients",
Namespace: "pihole",
Help: "This represent the number of unique clients seen",
},
)
// DnsQueriesAllTypes - The number of DNS queries made for all types by PI-Hole.
DnsQueriesAllTypes = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "dns_queries_all_types",
Namespace: "pihole",
Help: "This represent the number of DNS queries made for all types",
},
)
)
// Init initializes Prometheus metrics
func Init() {
initMetric(DomainsBlocked)
initMetric(DNSQueriesToday)
initMetric(AdsBlockedToday)
initMetric(AdsPercentageToday)
initMetric(UniqueDomains)
initMetric(QueriesForwarded)
initMetric(QueriesCached)
initMetric(ClientsEverSeen)
initMetric(UniqueClients)
initMetric(DnsQueriesAllTypes)
}
func initMetric(metric prometheus.Gauge) {
prometheus.MustRegister(metric)
log.Printf("New prometheus metric registered: %s", metric.Desc().String())
}

131
internal/pihole/client.go Normal file
View File

@ -0,0 +1,131 @@
package pihole
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
"github.com/eko/pihole-exporter/internal/metrics"
)
var (
loginURLPattern = "http://%s/admin/index.php?login"
statsURLPattern = "http://%s/admin/api.php?summaryRaw&overTimeData&topItems&recentItems&getQueryTypes&getForwardDestinations&getQuerySources&jsonForceObject"
)
type Client struct {
hostname string
password string
interval time.Duration
httpClient http.Client
}
func NewClient(hostname, password string, interval time.Duration) *Client {
return &Client{
hostname: hostname,
password: password,
interval: interval,
httpClient: http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
},
}
}
func (c *Client) Fetch() {
for range time.Tick(c.interval) {
sessionID := c.getPHPSessionID()
if sessionID == nil {
log.Println("Unable to retrieve session identifier")
return
}
stats := c.getStatistics(*sessionID)
log.Println("New tick of statistics", stats)
metrics.DomainsBlocked.Set(float64(stats.DomainsBeingBlocked))
metrics.DNSQueriesToday.Set(float64(stats.DNSQueriesToday))
metrics.AdsBlockedToday.Set(float64(stats.AdsBlockedToday))
metrics.AdsPercentageToday.Set(float64(stats.AdsPercentageToday))
metrics.UniqueDomains.Set(float64(stats.UniqueDomains))
metrics.QueriesForwarded.Set(float64(stats.QueriesForwarded))
metrics.QueriesCached.Set(float64(stats.QueriesCached))
metrics.ClientsEverSeen.Set(float64(stats.ClientsEverSeen))
metrics.UniqueClients.Set(float64(stats.UniqueClients))
metrics.DnsQueriesAllTypes.Set(float64(stats.DnsQueriesAllTypes))
}
}
func (c *Client) getPHPSessionID() *string {
var sessionID string
loginURL := fmt.Sprintf(loginURLPattern, c.hostname)
values := url.Values{"pw": []string{c.password}}
req, err := http.NewRequest("POST", loginURL, strings.NewReader(values.Encode()))
if err != nil {
log.Fatal("An error has occured when creating HTTP statistics request", err)
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Content-Length", strconv.Itoa(len(values.Encode())))
resp, err := c.httpClient.Do(req)
if err != nil {
log.Printf("An error has occured during login to PI-Hole: %v", err)
}
if resp.StatusCode != http.StatusFound {
log.Printf("Unable to login to PI-Hole, got a HTTP status code response '%d' instead of '%d'", resp.StatusCode, http.StatusFound)
os.Exit(1)
}
for _, cookie := range resp.Cookies() {
if cookie.Name == "PHPSESSID" {
sessionID = cookie.Value
break
}
}
return &sessionID
}
func (c *Client) getStatistics(sessionID string) *Stats {
var stats Stats
statsURL := fmt.Sprintf(statsURLPattern, c.hostname)
req, err := http.NewRequest("GET", statsURL, nil)
if err != nil {
log.Fatal("An error has occured when creating HTTP statistics request", err)
}
cookie := http.Cookie{Name: "PHPSESSID", Value: sessionID}
req.AddCookie(&cookie)
resp, err := c.httpClient.Do(req)
if err != nil {
log.Println("An error has occured during retrieving PI-Hole statistics", err)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println("Unable to read PI-Hole statistics HTTP response", err)
}
err = json.Unmarshal(body, &stats)
if err != nil {
log.Println("Unable to unmarshal PI-Hole statistics to statistics struct model", err)
}
return &stats
}

15
internal/pihole/model.go Normal file
View File

@ -0,0 +1,15 @@
package pihole
// Stats is the PI-Hole statistics JSON API corresponding model
type Stats struct {
DomainsBeingBlocked int `json:"domains_being_blocked"`
DNSQueriesToday int `json:"dns_queries_today"`
AdsBlockedToday int `json:"ads_blocked_today"`
AdsPercentageToday float64 `json:"ads_percentage_today"`
UniqueDomains int `json:"unique_domains"`
QueriesForwarded int `json:"queries_forwarded"`
QueriesCached int `json:"queries_cached"`
ClientsEverSeen int `json:"clients_ever_seen"`
UniqueClients int `json:"unique_clients"`
DnsQueriesAllTypes int `json:"dns_queries_all_types"`
}

65
internal/server/server.go Normal file
View File

@ -0,0 +1,65 @@
package server
import (
"log"
"net/http"
"time"
"github.com/prometheus/client_golang/prometheus/promhttp"
"golang.org/x/net/context"
)
type Server struct {
httpServer *http.Server
}
func NewServer(port string) *Server {
mux := http.NewServeMux()
httpServer := &http.Server{Addr: ":" + port, Handler: mux}
s := &Server{
httpServer: httpServer,
}
mux.Handle("/metrics", promhttp.Handler())
mux.Handle("/readiness", s.readinessHandler())
mux.Handle("/liveness", s.livenessHandler())
return s
}
func (s *Server) ListenAndServe() {
log.Println("Starting HTTP server")
err := s.httpServer.ListenAndServe()
if err != nil {
log.Printf("Failed to start serving HTTP requests: %v", err)
}
}
func (s *Server) Stop() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
s.httpServer.Shutdown(ctx)
}
func (s *Server) readinessHandler() http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if s.isReady() {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusNotFound)
}
})
}
func (s *Server) livenessHandler() http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusOK)
})
}
func (s *Server) isReady() bool {
return s.httpServer != nil
}

53
main.go Normal file
View File

@ -0,0 +1,53 @@
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"time"
"github.com/eko/pihole-exporter/config"
"github.com/eko/pihole-exporter/internal/metrics"
"github.com/eko/pihole-exporter/internal/pihole"
"github.com/eko/pihole-exporter/internal/server"
)
const (
name = "pihole-exporter"
)
var (
s *server.Server
)
func main() {
conf := config.Load()
metrics.Init()
initPiholeClient(conf.PIHoleHostname, conf.PIHolePassword, conf.Interval)
initHttpServer(conf.Port)
handleExitSignal()
}
func initPiholeClient(hostname, password string, interval time.Duration) {
client := pihole.NewClient(hostname, password, interval)
go client.Fetch()
}
func initHttpServer(port string) {
s = server.NewServer(port)
go s.ListenAndServe()
}
func handleExitSignal() {
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
<-stop
s.Stop()
fmt.Println(fmt.Sprintf("\n%s HTTP server stopped", name))
}