First commit

This commit is contained in:
Eldwan Brianne 2020-11-01 23:09:36 +01:00
commit be25226b83
8 changed files with 395 additions and 0 deletions

7
.gitignore vendored Normal file
View File

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

80
config/configuration.go Normal file
View File

@ -0,0 +1,80 @@
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"
)
// Config is the exporter CLI configuration.
type Config struct {
AdguardProtocol string `config:"adguard_protocol"`
AdguardHostname string `config:"adguard_hostname"`
AdguardPort uint16 `config:"adguard_port"`
AdguardPassword string `config:"adguard_password"`
Port string `config:"port"`
Interval time.Duration `config:"interval"`
}
func getDefaultConfig() *Config {
return &Config{
AdguardProtocol: "http",
AdguardHostname: "127.0.0.1",
AdguardPort: 80,
AdguardPassword: "",
Port: "9617",
Interval: 10 * time.Second,
}
}
// Load method loads the configuration by using both flag or environment variables.
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("- AdGuard Home exporter configuration -")
log.Println("---------------------------------------")
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 != "AdguardPassword" {
log.Println(fmt.Sprintf("%s : %v", typeField.Name, valueField.Interface()))
} else {
showAuthenticationMethod(typeField.Name, valueField.String())
}
}
log.Println("---------------------------------------")
}
func showAuthenticationMethod(name, value string) {
if len(value) > 0 {
log.Println(fmt.Sprintf("AdGuard Authentication Method : %s", name))
}
}

9
go.mod Normal file
View File

@ -0,0 +1,9 @@
module github.com/ebrianne/adguard-exporter
go 1.14
require (
github.com/heetch/confita v0.9.2
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829
golang.org/x/net v0.0.0-20190620200207-3b0461eec859
)

137
internal/adguard/client.go Normal file
View File

@ -0,0 +1,137 @@
package adguard
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
"github.com/eko/adguard-exporter/internal/metrics"
)
var (
loginURLPattern = "%s://%s:%d/login.html"
statsURLPattern = "%s://%s:%d/control/stats"
)
// Client struct is a AdGuard client to request an instance of a AdGuard ad blocker.
type Client struct {
httpClient http.Client
interval time.Duration
protocol string
hostname string
port uint16
password string
sessionID string
}
// NewClient method initializes a new AdGuard client.
func NewClient(protocol, hostname string, port uint16, password, interval time.Duration) *Client {
if protocol != "http" && protocol != "https" {
log.Printf("protocol %s is invalid. Must be http or https.", protocol)
os.Exit(1)
}
return &Client{
protocol: protocol,
hostname: hostname,
port: port,
password: password,
interval: interval,
httpClient: http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
},
}
}
// Scrape method authenticates and retrieves statistics from AdGuard JSON API
// and then pass them as Prometheus metrics.
func (c *Client) Scrape() {
for range time.Tick(c.interval) {
stats := c.getStatistics()
c.setMetrics(stats)
log.Printf("New tick of statistics: %s", stats.ToString())
}
}
func (c *Client) setMetrics(stats *Stats) {
metrics.AvgProcessingTime.WithLabelValues(c.hostname).Set(float64(stats.AvgProcessingTime))
}
func (c *Client) getPHPSessionID() (sessionID string) {
loginURL := fmt.Sprintf(loginURLPattern, c.protocol, c.hostname, c.port)
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 Adguard: %v", err)
}
for _, cookie := range resp.Cookies() {
if cookie.Name == "PHPSESSID" {
sessionID = cookie.Value
break
}
}
return
}
func (c *Client) getStatistics() *Stats {
var stats Stats
statsURL := fmt.Sprintf(statsURLPattern, c.protocol, c.hostname, c.port)
req, err := http.NewRequest("GET", statsURL, nil)
if err != nil {
log.Fatal("An error has occured when creating HTTP statistics request", err)
}
if c.isUsingPassword() {
c.authenticateRequest(req)
}
resp, err := c.httpClient.Do(req)
if err != nil {
log.Println("An error has occured during retrieving Adguard statistics", err)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println("Unable to read Adguard statistics HTTP response", err)
}
err = json.Unmarshal(body, &stats)
if err != nil {
log.Println("Unable to unmarshal Adguard statistics to statistics struct model", err)
}
return &stats
}
func (c *Client) isUsingPassword() bool {
return len(c.password) > 0
}
func (c *Client) authenticateRequest(req *http.Request) {
cookie := http.Cookie{Name: "PHPSESSID", Value: c.getPHPSessionID()}
req.AddCookie(&cookie)
}

11
internal/adguard/model.go Normal file
View File

@ -0,0 +1,11 @@
package adguard
import "fmt"
const (
enabledStatus = "enabled"
)
type Stats struct {
AvgProcessingTime float64 `json:"avg_processing_time"`
}

View File

@ -0,0 +1,28 @@
package metrics
import (
"log"
"github.com/prometheus/client_golang/prometheus"
)
var (
AvgProcessingTime = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "avg_processing_time",
Namespace: "adguard",
Help: "This represent the average processing time for a DNS query in s",
},
[]string{"hostname"},
)
)
// Init initializes all Prometheus metrics made available by AdGuard exporter.
func Init() {
initMetric("avg_processing_time", AvgProcessingTime)
}
func initMetric(name string, metric *prometheus.GaugeVec) {
prometheus.MustRegister(metric)
log.Printf("New Prometheus metric registered: %s", name)
}

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

@ -0,0 +1,70 @@
package server
import (
"log"
"net/http"
"time"
"github.com/prometheus/client_golang/prometheus/promhttp"
"golang.org/x/net/context"
)
// Server is the struct for the HTTP server.
type Server struct {
httpServer *http.Server
}
// 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) *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
}
// ListenAndServe method serves HTTP requests.
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)
}
}
// Stop method stops the HTTP server (so the exporter become unavailable).
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/ebrianne/adguard-exporter/config"
"github.com/ebrianne/adguard-exporter/internal/metrics"
"github.com/ebrianne/adguard-exporter/internal/adguard"
"github.com/ebrianne/adguard-exporter/internal/server"
)
const (
name = "adguard-exporter"
)
var (
s *server.Server
)
func main() {
conf := config.Load()
metrics.Init()
initAdguardClient(conf.AdguardProtocol, conf.AdguardHostname, conf.AdguardPort, conf.AdguardPassword, conf.Interval)
initHttpServer(conf.Port)
handleExitSignal()
}
func initAdguardClient(protocol, hostname string, port uint16, password, interval time.Duration) {
client := adguard.NewClient(protocol, hostname, port, password, interval)
go client.Scrape()
}
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))
}