mirror of
https://github.com/ebrianne/adguard-exporter.git
synced 2025-01-03 08:17:34 +01:00
First commit
This commit is contained in:
commit
be25226b83
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
vendor
|
||||||
|
bin
|
||||||
|
tmp
|
||||||
|
.vscode
|
||||||
|
report.xml
|
||||||
|
debug
|
||||||
|
.idea/
|
80
config/configuration.go
Normal file
80
config/configuration.go
Normal 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
9
go.mod
Normal 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
137
internal/adguard/client.go
Normal 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
11
internal/adguard/model.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package adguard
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
const (
|
||||||
|
enabledStatus = "enabled"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Stats struct {
|
||||||
|
AvgProcessingTime float64 `json:"avg_processing_time"`
|
||||||
|
}
|
28
internal/metrics/metrics.go
Normal file
28
internal/metrics/metrics.go
Normal 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
70
internal/server/server.go
Normal 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
53
main.go
Normal 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))
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user