Initialize
This commit is contained in:
commit
7457599d3e
11 changed files with 563 additions and 0 deletions
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
search-api
|
||||||
|
bin
|
||||||
|
tmp
|
||||||
|
.vscode
|
||||||
|
report.xml
|
||||||
|
debug
|
||||||
|
.idea/
|
19
LICENSE
Normal file
19
LICENSE
Normal 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
46
README.md
Normal 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
65
config/configuration.go
Normal 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
11
go.mod
Normal 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
33
go.sum
Normal 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
118
internal/metrics/metrics.go
Normal 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
131
internal/pihole/client.go
Normal 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
15
internal/pihole/model.go
Normal 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
65
internal/server/server.go
Normal 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
53
main.go
Normal 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))
|
||||||
|
}
|
Loading…
Reference in a new issue