Some checks failed
Build and push adguard-exporter image / build-and-push-image (push) Failing after 11m20s
861 lines
19 KiB
Go
861 lines
19 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/joho/godotenv"
|
|
"github.com/oschwald/geoip2-golang"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
)
|
|
|
|
/*
|
|
📦 AdGuard Exporter for Prometheus
|
|
----------------------------------
|
|
Author : @znand-dev
|
|
License : MIT
|
|
Repo : https://github.com/znand-dev/adguardexporter
|
|
|
|
This Go application fetches stats from AdGuard Home via API endpoints
|
|
and exposes them as Prometheus metrics at `/metrics`.
|
|
|
|
Required ENV variables:
|
|
- ADGUARD_HOST : AdGuard Home base URL (e.g. http://192.168.1.1:3000)
|
|
- ADGUARD_USER : API username (your adguard user)
|
|
- ADGUARD_PASS : API password (your adguard pass)
|
|
- EXPORTER_PORT : Port to expose metrics (default: 9617)
|
|
- SCRAPE_INTERVAL : Interval (in seconds) to fetch new stats (default: 15)
|
|
- LOG_LEVEL : Logging level (options: DEBUG, INFO, WARN, ERROR — default: INFO)
|
|
- GEOIP_DB : GeoLite2-City.mmdb
|
|
*/
|
|
|
|
var logLevelMap = map[string]int{"ERROR": 1, "WARN": 2, "INFO": 3, "DEBUG": 4}
|
|
var currentLogLevel = 3
|
|
|
|
func initLogger() {
|
|
level := os.Getenv("LOG_LEVEL")
|
|
if level == "" {
|
|
level = "INFO"
|
|
}
|
|
if val, ok := logLevelMap[level]; ok {
|
|
currentLogLevel = val
|
|
}
|
|
}
|
|
|
|
func logX(level string, format string, args ...interface{}) {
|
|
if logLevelMap[level] <= currentLogLevel {
|
|
log.Printf("[%s] %s", level, fmt.Sprintf(format, args...))
|
|
}
|
|
}
|
|
|
|
//function hash query
|
|
func buildQueryKey(client, domain, upstream, reason, elapsed string) string {
|
|
return client + "|" + domain + "|" + upstream + "|" + reason + "|" + elapsed
|
|
}
|
|
|
|
type GeoCache struct {
|
|
Country string
|
|
Lat float64
|
|
Lon float64
|
|
TS int64
|
|
}
|
|
|
|
var (
|
|
geoDB *geoip2.Reader
|
|
geoCache = map[string]GeoCache{}
|
|
geoMutex sync.RWMutex
|
|
// cache query hash
|
|
querySeen = map[string]int64{}
|
|
queryMutex sync.Mutex
|
|
queryTTL = int64(300) // 5 menit
|
|
geoTTL = int64(86400) // 24 jam
|
|
dedupHits int64
|
|
)
|
|
|
|
type AdGuardStats struct {
|
|
NumDNSQueries float64 `json:"num_dns_queries"`
|
|
NumBlockedFiltering float64 `json:"num_blocked_filtering"`
|
|
NumReplacedParental float64 `json:"num_replaced_parental"`
|
|
AvgProcessingTime float64 `json:"avg_processing_time"`
|
|
TopQueriedDomains []map[string]float64 `json:"top_queried_domains"`
|
|
TopBlockedDomains []map[string]float64 `json:"top_blocked_domains"`
|
|
TopClients []map[string]float64 `json:"top_clients"`
|
|
TopUpstream []map[string]float64 `json:"top_upstreams_responses"`
|
|
TopUpstreamTime []map[string]float64 `json:"top_upstreams_avg_time"`
|
|
}
|
|
|
|
type AdGuardStatus struct {
|
|
Version string `json:"version"`
|
|
Language string `json:"language"`
|
|
DNSAddresses []string `json:"dns_addresses"`
|
|
DNSPort int `json:"dns_port"`
|
|
HTTPPort int `json:"http_port"`
|
|
ProtectionDisabledDuration int `json:"protection_disabled_duration"`
|
|
ProtectionEnabled bool `json:"protection_enabled"`
|
|
DHCPAvailable bool `json:"dhcp_available"`
|
|
Running bool `json:"running"`
|
|
}
|
|
|
|
type AdGuardQueryLog struct {
|
|
Data []struct {
|
|
Question struct {
|
|
Type string `json:"type"`
|
|
Name string `json:"name"`
|
|
} `json:"question"`
|
|
Answer []interface{} `json:"answer"`
|
|
Reason string `json:"reason"`
|
|
Client string `json:"client"`
|
|
Elapsed string `json:"elapsedMs"`
|
|
Upstream string `json:"upstream"`
|
|
|
|
ClientInfo struct {
|
|
Whois struct {
|
|
Country string `json:"country"`
|
|
OrgName string `json:"orgname"`
|
|
} `json:"whois"`
|
|
} `json:"client_info"`
|
|
|
|
} `json:"data"`
|
|
}
|
|
|
|
var (
|
|
|
|
dnsQueries = prometheus.NewGauge(prometheus.GaugeOpts{
|
|
Name: "adguard_dns_queries_total", Help: "Total DNS queries received",
|
|
})
|
|
|
|
blockedFiltering = prometheus.NewGauge(prometheus.GaugeOpts{
|
|
Name: "adguard_blocked_filtering_total", Help: "Total DNS queries blocked",
|
|
})
|
|
|
|
replacedParental = prometheus.NewGauge(prometheus.GaugeOpts{
|
|
Name: "adguard_replaced_parental", Help: "Total parental replaced queries",
|
|
})
|
|
|
|
avgProcessingTime = prometheus.NewGauge(prometheus.GaugeOpts{
|
|
Name: "adguard_avg_processing_time", Help: "Avg DNS processing time",
|
|
})
|
|
|
|
statusProtectionEnabled = prometheus.NewGauge(prometheus.GaugeOpts{
|
|
Name: "adguard_protection_enabled", Help: "Protection enabled (1/0)",
|
|
})
|
|
|
|
statusRunning = prometheus.NewGauge(prometheus.GaugeOpts{
|
|
Name: "adguard_running", Help: "AdGuard running",
|
|
})
|
|
|
|
statusDHCPAvailable = prometheus.NewGauge(prometheus.GaugeOpts{
|
|
Name: "adguard_dhcp_available", Help: "DHCP available",
|
|
})
|
|
|
|
statusDisabledDuration = prometheus.NewGauge(prometheus.GaugeOpts{
|
|
Name: "adguard_protection_disabled_duration_seconds",
|
|
Help: "Time since protection disabled",
|
|
})
|
|
|
|
versionInfo = prometheus.NewGaugeVec(
|
|
prometheus.GaugeOpts{
|
|
Name: "adguard_version_info",
|
|
Help: "AdGuard version",
|
|
},
|
|
[]string{"version"},
|
|
)
|
|
|
|
topQueriedDomains = prometheus.NewGaugeVec(
|
|
prometheus.GaugeOpts{
|
|
Name: "adguard_top_queried_domain_total",
|
|
Help: "Top queried domains",
|
|
},
|
|
[]string{"domain"},
|
|
)
|
|
|
|
topBlockedDomains = prometheus.NewGaugeVec(
|
|
prometheus.GaugeOpts{
|
|
Name: "adguard_top_blocked_domain_total",
|
|
Help: "Top blocked domains",
|
|
},
|
|
[]string{"domain"},
|
|
)
|
|
|
|
topClients = prometheus.NewGaugeVec(
|
|
prometheus.GaugeOpts{
|
|
Name: "adguard_top_client_total",
|
|
Help: "Top client IPs",
|
|
},
|
|
[]string{"client"},
|
|
)
|
|
|
|
topUpstreams = prometheus.NewGaugeVec(
|
|
prometheus.GaugeOpts{
|
|
Name: "adguard_top_upstream_total",
|
|
Help: "Top upstream servers",
|
|
},
|
|
[]string{"upstream"},
|
|
)
|
|
|
|
topUpstreamTime = prometheus.NewGaugeVec(
|
|
prometheus.GaugeOpts{
|
|
Name: "adguard_upstream_avg_response_time_seconds",
|
|
Help: "Avg response time per upstream",
|
|
},
|
|
[]string{"upstream"},
|
|
)
|
|
|
|
queryCountByReason = prometheus.NewCounterVec(
|
|
prometheus.CounterOpts{
|
|
Name: "adguard_query_reason_total",
|
|
Help: "Queries by reason",
|
|
},
|
|
[]string{"reason"},
|
|
)
|
|
|
|
queryCountByType = prometheus.NewCounterVec(
|
|
prometheus.CounterOpts{
|
|
Name: "adguard_query_type_total",
|
|
Help: "Queries by DNS type",
|
|
},
|
|
[]string{"type"},
|
|
)
|
|
|
|
queryHistogramByClient = prometheus.NewHistogramVec(
|
|
prometheus.HistogramOpts{
|
|
Name: "adguard_query_elapsed_ms",
|
|
Help: "Query duration by client",
|
|
Buckets: prometheus.LinearBuckets(1, 5, 10),
|
|
},
|
|
[]string{"client"},
|
|
)
|
|
|
|
queryCountByUpstream = prometheus.NewCounterVec(
|
|
prometheus.CounterOpts{
|
|
Name: "adguard_query_upstream_total",
|
|
Help: "Queries per upstream",
|
|
},
|
|
[]string{"upstream"},
|
|
)
|
|
|
|
queryCountByDomain = prometheus.NewCounterVec(
|
|
prometheus.CounterOpts{
|
|
Name: "adguard_query_domain_total",
|
|
Help: "Queries per domain",
|
|
},
|
|
[]string{"domain"},
|
|
)
|
|
|
|
queryCountClientReason = prometheus.NewCounterVec(
|
|
prometheus.CounterOpts{
|
|
Name: "adguard_query_client_reason_total",
|
|
Help: "Queries per client per reason",
|
|
},
|
|
[]string{"client", "reason"},
|
|
)
|
|
|
|
clientGeoQueries = prometheus.NewCounterVec(
|
|
prometheus.CounterOpts{
|
|
Name: "adguard_client_geo_queries",
|
|
Help: "Queries per client with geographic info",
|
|
},
|
|
[]string{"client", "country", "lat", "lon"},
|
|
)
|
|
|
|
blockedGeoQueries = prometheus.NewCounterVec(
|
|
prometheus.CounterOpts{
|
|
Name: "adguard_blocked_geo_queries",
|
|
Help: "Blocked DNS queries per client with geographic info",
|
|
},
|
|
[]string{"client", "country", "lat", "lon"},
|
|
)
|
|
|
|
upstreamLatencyHistogram = prometheus.NewHistogramVec(
|
|
prometheus.HistogramOpts{
|
|
Name: "adguard_upstream_latency_seconds",
|
|
Help: "Latency distribution per upstream DNS server",
|
|
Buckets: prometheus.ExponentialBuckets(
|
|
0.001, // 1ms
|
|
2,
|
|
10,
|
|
),
|
|
},
|
|
[]string{"upstream"},
|
|
)
|
|
|
|
exporterUp = prometheus.NewGauge(
|
|
prometheus.GaugeOpts{
|
|
Name: "adguard_exporter_up",
|
|
Help: "Exporter scrape success",
|
|
},
|
|
)
|
|
|
|
exporterScrapeDuration = prometheus.NewGauge(
|
|
prometheus.GaugeOpts{
|
|
Name: "adguard_exporter_scrape_duration_seconds",
|
|
Help: "Exporter scrape duration",
|
|
},
|
|
)
|
|
|
|
exporterErrors = prometheus.NewCounter(
|
|
prometheus.CounterOpts{
|
|
Name: "adguard_exporter_scrape_errors_total",
|
|
Help: "Total exporter errors",
|
|
},
|
|
)
|
|
|
|
exporterQueryCacheSize = prometheus.NewGauge(
|
|
prometheus.GaugeOpts{
|
|
Name: "adguard_exporter_query_cache_size",
|
|
Help: "Current number of entries in query deduplication cache",
|
|
},
|
|
)
|
|
|
|
exporterGeoCacheSize = prometheus.NewGauge(
|
|
prometheus.GaugeOpts{
|
|
Name: "adguard_exporter_geo_cache_size",
|
|
Help: "Current number of cached GeoIP entries",
|
|
},
|
|
)
|
|
|
|
exporterDedupHits = prometheus.NewCounter(
|
|
prometheus.CounterOpts{
|
|
Name: "adguard_exporter_dedup_hits_total",
|
|
Help: "Total number of duplicate queries skipped by deduplication",
|
|
},
|
|
)
|
|
|
|
queryCountByISP = prometheus.NewCounterVec(
|
|
prometheus.CounterOpts{
|
|
Name: "adguard_query_isp_total",
|
|
Help: "DNS queries grouped by ISP organization",
|
|
},
|
|
[]string{"isp", "country"},
|
|
)
|
|
)
|
|
|
|
func init() {
|
|
_ = godotenv.Load()
|
|
initLogger()
|
|
|
|
prometheus.MustRegister(
|
|
dnsQueries,
|
|
blockedFiltering,
|
|
replacedParental,
|
|
avgProcessingTime,
|
|
statusProtectionEnabled,
|
|
statusRunning,
|
|
statusDHCPAvailable,
|
|
statusDisabledDuration,
|
|
versionInfo,
|
|
topQueriedDomains,
|
|
topBlockedDomains,
|
|
topClients,
|
|
topUpstreams,
|
|
topUpstreamTime,
|
|
queryCountByReason,
|
|
queryCountByType,
|
|
queryHistogramByClient,
|
|
queryCountByUpstream,
|
|
queryCountByDomain,
|
|
queryCountClientReason,
|
|
clientGeoQueries,
|
|
blockedGeoQueries,
|
|
exporterUp,
|
|
exporterScrapeDuration,
|
|
exporterErrors,
|
|
upstreamLatencyHistogram,
|
|
exporterQueryCacheSize,
|
|
exporterGeoCacheSize,
|
|
exporterDedupHits,
|
|
queryCountByISP,
|
|
)
|
|
}
|
|
|
|
func resolveGeo(ipStr string) (GeoCache, bool) {
|
|
if geoDB == nil {
|
|
return GeoCache{}, false
|
|
}
|
|
|
|
ip := net.ParseIP(ipStr)
|
|
if ip == nil || ip.IsPrivate() || ip.IsLoopback() {
|
|
return GeoCache{}, false
|
|
}
|
|
|
|
now := time.Now().Unix()
|
|
|
|
geoMutex.RLock()
|
|
if val, ok := geoCache[ipStr]; ok {
|
|
|
|
// cache valid
|
|
if now-val.TS < geoTTL {
|
|
geoMutex.RUnlock()
|
|
return val, true
|
|
}
|
|
|
|
}
|
|
geoMutex.RUnlock()
|
|
|
|
record, err := geoDB.City(ip)
|
|
if err != nil {
|
|
return GeoCache{}, false
|
|
}
|
|
|
|
cache := GeoCache{
|
|
Country: record.Country.IsoCode,
|
|
Lat: record.Location.Latitude,
|
|
Lon: record.Location.Longitude,
|
|
TS: now,
|
|
}
|
|
|
|
geoMutex.Lock()
|
|
geoCache[ipStr] = cache
|
|
geoMutex.Unlock()
|
|
|
|
return cache, true
|
|
}
|
|
|
|
func fetchQueryLog() (*AdGuardQueryLog, error) {
|
|
|
|
host := os.Getenv("ADGUARD_HOST")
|
|
user := os.Getenv("ADGUARD_USER")
|
|
pass := os.Getenv("ADGUARD_PASS")
|
|
|
|
url := host + "/control/querylog"
|
|
|
|
req, _ := http.NewRequest("GET", url, nil)
|
|
req.SetBasicAuth(user, pass)
|
|
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer func() {
|
|
if err := resp.Body.Close(); err != nil {
|
|
logX("WARN", "failed to close response body: %v", err)
|
|
}
|
|
}()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var logData AdGuardQueryLog
|
|
|
|
err = json.Unmarshal(body, &logData)
|
|
|
|
return &logData, err
|
|
}
|
|
|
|
func fetchStats() (*AdGuardStats, error) {
|
|
|
|
host := os.Getenv("ADGUARD_HOST")
|
|
user := os.Getenv("ADGUARD_USER")
|
|
pass := os.Getenv("ADGUARD_PASS")
|
|
|
|
url := host + "/control/stats"
|
|
|
|
req, _ := http.NewRequest("GET", url, nil)
|
|
req.SetBasicAuth(user, pass)
|
|
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer func() {
|
|
if err := resp.Body.Close(); err != nil {
|
|
logX("WARN", "failed to close response body: %v", err)
|
|
}
|
|
}()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var stats AdGuardStats
|
|
|
|
err = json.Unmarshal(body, &stats)
|
|
|
|
return &stats, err
|
|
}
|
|
|
|
func fetchStatus() (*AdGuardStatus, error) {
|
|
|
|
host := os.Getenv("ADGUARD_HOST")
|
|
user := os.Getenv("ADGUARD_USER")
|
|
pass := os.Getenv("ADGUARD_PASS")
|
|
|
|
url := host + "/control/status"
|
|
|
|
req, _ := http.NewRequest("GET", url, nil)
|
|
req.SetBasicAuth(user, pass)
|
|
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer func() {
|
|
if err := resp.Body.Close(); err != nil {
|
|
logX("WARN", "failed to close response body: %v", err)
|
|
}
|
|
}()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var status AdGuardStatus
|
|
|
|
err = json.Unmarshal(body, &status)
|
|
|
|
return &status, err
|
|
}
|
|
|
|
func updateStatsMetrics() {
|
|
|
|
stats, err := fetchStats()
|
|
|
|
if err != nil {
|
|
logX("ERROR", "Failed fetch stats: %v", err)
|
|
return
|
|
}
|
|
|
|
dnsQueries.Set(stats.NumDNSQueries)
|
|
blockedFiltering.Set(stats.NumBlockedFiltering)
|
|
replacedParental.Set(stats.NumReplacedParental)
|
|
avgProcessingTime.Set(stats.AvgProcessingTime)
|
|
|
|
// reset gauge vectors
|
|
topQueriedDomains.Reset()
|
|
topBlockedDomains.Reset()
|
|
topClients.Reset()
|
|
topUpstreams.Reset()
|
|
topUpstreamTime.Reset()
|
|
|
|
// top queried domains
|
|
for _, d := range stats.TopQueriedDomains {
|
|
for domain, count := range d {
|
|
topQueriedDomains.WithLabelValues(domain).Set(count)
|
|
}
|
|
}
|
|
|
|
// top blocked domains
|
|
for _, d := range stats.TopBlockedDomains {
|
|
for domain, count := range d {
|
|
topBlockedDomains.WithLabelValues(domain).Set(count)
|
|
}
|
|
}
|
|
|
|
// top clients
|
|
for _, c := range stats.TopClients {
|
|
for client, count := range c {
|
|
topClients.WithLabelValues(client).Set(count)
|
|
}
|
|
}
|
|
|
|
// upstream responses
|
|
for _, u := range stats.TopUpstream {
|
|
for upstream, count := range u {
|
|
topUpstreams.WithLabelValues(upstream).Set(count)
|
|
}
|
|
}
|
|
|
|
// upstream avg response time
|
|
for _, u := range stats.TopUpstreamTime {
|
|
for upstream, time := range u {
|
|
topUpstreamTime.WithLabelValues(upstream).Set(time)
|
|
}
|
|
}
|
|
|
|
logX(
|
|
"DEBUG",
|
|
"Fetched stats: queries=%.0f blocked=%.0f replaced=%.0f avgTime=%.2fms topDomains=%d",
|
|
stats.NumDNSQueries,
|
|
stats.NumBlockedFiltering,
|
|
stats.NumReplacedParental,
|
|
stats.AvgProcessingTime,
|
|
len(stats.TopQueriedDomains),
|
|
)
|
|
}
|
|
|
|
func updateStatusMetrics() {
|
|
|
|
status, err := fetchStatus()
|
|
|
|
if err != nil {
|
|
logX("ERROR", "Failed fetch status: %v", err)
|
|
return
|
|
}
|
|
|
|
if status.Running {
|
|
statusRunning.Set(1)
|
|
} else {
|
|
statusRunning.Set(0)
|
|
}
|
|
|
|
if status.ProtectionEnabled {
|
|
statusProtectionEnabled.Set(1)
|
|
} else {
|
|
statusProtectionEnabled.Set(0)
|
|
}
|
|
|
|
if status.DHCPAvailable {
|
|
statusDHCPAvailable.Set(1)
|
|
} else {
|
|
statusDHCPAvailable.Set(0)
|
|
}
|
|
|
|
statusDisabledDuration.Set(float64(status.ProtectionDisabledDuration))
|
|
|
|
versionInfo.WithLabelValues(status.Version).Set(1)
|
|
|
|
logX(
|
|
"DEBUG",
|
|
"Fetched status: running=%v protection=%v DHCP=%v version=%s",
|
|
status.Running,
|
|
status.ProtectionEnabled,
|
|
status.DHCPAvailable,
|
|
status.Version,
|
|
)
|
|
}
|
|
|
|
func updateQueryLogMetrics() {
|
|
|
|
scanned := 0
|
|
processed := 0
|
|
skipped := 0
|
|
geoResolved := 0
|
|
privateClients := 0
|
|
publicClients := 0
|
|
|
|
logData, err := fetchQueryLog()
|
|
|
|
if err != nil {
|
|
logX("ERROR", "Failed querylog: %v", err)
|
|
return
|
|
}
|
|
|
|
for _, q := range logData.Data {
|
|
|
|
scanned++
|
|
|
|
ip := net.ParseIP(q.Client)
|
|
|
|
if ip != nil {
|
|
if ip.IsPrivate() || ip.IsLoopback() {
|
|
privateClients++
|
|
} else {
|
|
publicClients++
|
|
}
|
|
|
|
}
|
|
|
|
// dedup logic
|
|
key := buildQueryKey(
|
|
q.Client,
|
|
q.Question.Name,
|
|
q.Upstream,
|
|
q.Reason,
|
|
q.Elapsed,
|
|
)
|
|
|
|
now := time.Now().Unix()
|
|
|
|
queryMutex.Lock()
|
|
|
|
if ts, exists := querySeen[key]; exists {
|
|
if now-ts < queryTTL {
|
|
queryMutex.Unlock()
|
|
skipped++
|
|
exporterDedupHits.Inc()
|
|
continue
|
|
}
|
|
}
|
|
|
|
querySeen[key] = now
|
|
queryMutex.Unlock()
|
|
|
|
processed++
|
|
|
|
queryCountByReason.WithLabelValues(q.Reason).Inc()
|
|
queryCountByType.WithLabelValues(q.Question.Type).Inc()
|
|
|
|
elapsedMs, err := strconv.ParseFloat(q.Elapsed, 64)
|
|
|
|
if err == nil {
|
|
|
|
queryHistogramByClient.WithLabelValues(q.Client).Observe(elapsedMs)
|
|
|
|
if q.Upstream != "" {
|
|
upstreamLatencyHistogram.WithLabelValues(q.Upstream).Observe(elapsedMs / 1000)
|
|
}
|
|
}
|
|
|
|
queryCountByUpstream.WithLabelValues(q.Upstream).Inc()
|
|
queryCountByDomain.WithLabelValues(q.Question.Name).Inc()
|
|
queryCountClientReason.WithLabelValues(q.Client, q.Reason).Inc()
|
|
|
|
geo, ok := resolveGeo(q.Client)
|
|
|
|
if ok {
|
|
|
|
geoResolved++
|
|
|
|
clientGeoQueries.WithLabelValues(
|
|
q.Client,
|
|
geo.Country,
|
|
fmt.Sprintf("%f", geo.Lat),
|
|
fmt.Sprintf("%f", geo.Lon),
|
|
).Inc()
|
|
|
|
if q.Reason == "FilteredBlackList" ||
|
|
q.Reason == "FilteredSafeBrowsing" ||
|
|
q.Reason == "FilteredParental" {
|
|
|
|
blockedGeoQueries.WithLabelValues(
|
|
q.Client,
|
|
geo.Country,
|
|
fmt.Sprintf("%f", geo.Lat),
|
|
fmt.Sprintf("%f", geo.Lon),
|
|
).Inc()
|
|
}
|
|
}
|
|
|
|
isp := q.ClientInfo.Whois.OrgName
|
|
country := q.ClientInfo.Whois.Country
|
|
|
|
if isp != "" {
|
|
queryCountByISP.WithLabelValues(isp, country).Inc()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
logX("DEBUG",
|
|
"Querylog: scanned=%d new=%d skipped=%d geoip=%d dedupHits=%d",
|
|
scanned,
|
|
processed,
|
|
skipped,
|
|
geoResolved,
|
|
dedupHits,
|
|
)
|
|
}
|
|
|
|
//cleanup memory
|
|
func cleanupQueryCache() {
|
|
|
|
now := time.Now().Unix()
|
|
|
|
queryMutex.Lock()
|
|
|
|
for k, v := range querySeen {
|
|
|
|
if now-v > queryTTL {
|
|
delete(querySeen, k)
|
|
}
|
|
|
|
}
|
|
|
|
queryMutex.Unlock()
|
|
}
|
|
|
|
func main() {
|
|
|
|
dbPath := os.Getenv("GEOIP_DB")
|
|
|
|
if dbPath == "" {
|
|
dbPath = "GeoLite2-City.mmdb"
|
|
}
|
|
|
|
db, err := geoip2.Open(dbPath)
|
|
|
|
if err != nil {
|
|
logX("WARN", "GeoIP DB not found (%v) — geo metrics disabled", err)
|
|
} else {
|
|
geoDB = db
|
|
logX("INFO", "GeoIP database loaded")
|
|
}
|
|
|
|
scrapeIntervalStr := os.Getenv("SCRAPE_INTERVAL")
|
|
port := os.Getenv("EXPORTER_PORT")
|
|
|
|
if port == "" {
|
|
port = "9617"
|
|
}
|
|
|
|
interval, err := strconv.Atoi(scrapeIntervalStr)
|
|
|
|
if err != nil || interval < 1 {
|
|
interval = 15
|
|
}
|
|
|
|
logX("INFO", "Scrape interval set to %ds", interval)
|
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
start := time.Now()
|
|
|
|
updateStatsMetrics()
|
|
updateStatusMetrics()
|
|
updateQueryLogMetrics()
|
|
|
|
cleanupQueryCache()
|
|
|
|
duration := time.Since(start).Seconds()
|
|
|
|
exporterScrapeDuration.Set(duration)
|
|
exporterUp.Set(1)
|
|
|
|
exporterQueryCacheSize.Set(float64(len(querySeen)))
|
|
exporterGeoCacheSize.Set(float64(len(geoCache)))
|
|
|
|
logX(
|
|
"DEBUG",
|
|
"Exporter caches: query_cache_entries=%d geo_cache_entries=%d dedup_hits_total=%d",
|
|
len(querySeen),
|
|
len(geoCache),
|
|
dedupHits,
|
|
)
|
|
|
|
logX("DEBUG", "Scrape finished in %.3fs", duration)
|
|
|
|
time.Sleep(time.Duration(interval) * time.Second)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
http.Handle("/metrics", promhttp.Handler())
|
|
|
|
logX("INFO", "Starting exporter at :%s ..", port)
|
|
|
|
err = http.ListenAndServe(":"+port, nil)
|
|
|
|
if err != nil {
|
|
logX("ERROR", "Server failed: %v", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
}
|