feat: add option to enable granular lease metrics (#24)
Introduce a new flag that can be used to expose each DHCP lease as a specific metric. Given the high cardinality of these metrics, exposition of lease information is disabled by default. This feature takes cues from node_exporter collectors which are disabled by default, like the `processes` or `systemd` collectors for instance. Typically, high-cardinality metrics are against prometheus best-practices but for smaller networks (like home networks) these features can be quite powerful.
This commit is contained in:
parent
08fab8ede7
commit
e3caa1dcf7
|
@ -1,6 +1,6 @@
|
|||
# dnsmasq exporter
|
||||
|
||||
[![Build Status](https://travis-ci.org/google/dnsmasq_exporter.svg?branch=master)](https://travis-ci.org/google/dnsmasq_exporter)
|
||||
[![Build Status](https://github.com/google/dnsmasq_exporter/actions/workflows/go.yml/badge.svg?branch=main)](https://github.com/google/dnsmasq_exporter/actions)
|
||||
|
||||
dnsmasq_exporter is an exporter for [Prometheus](https://prometheus.io/),
|
||||
allowing you to monitor/alert on the number of DHCP leases and various DNS
|
||||
|
@ -32,6 +32,7 @@ systemctl enable --now dnsmasq_exporter
|
|||
```
|
||||
|
||||
### Alternative usage
|
||||
|
||||
```shell
|
||||
docker build -t dnsmasq_exporter .
|
||||
docker run --restart=unless-stopped --net=host dnsmasq_exporter
|
||||
|
|
114
dnsmasq.go
114
dnsmasq.go
|
@ -38,6 +38,9 @@ var (
|
|||
"localhost:9153",
|
||||
"listen address")
|
||||
|
||||
exposeLeases = flag.Bool("expose_leases",
|
||||
false,
|
||||
"expose dnsmasq leases as metrics (high cardinality)")
|
||||
leasesPath = flag.String("leases_path",
|
||||
"/var/lib/misc/dnsmasq.leases",
|
||||
"path to the dnsmasq leases file")
|
||||
|
@ -102,6 +105,16 @@ var (
|
|||
),
|
||||
}
|
||||
|
||||
// individual lease metrics have high cardinality and are thus disabled by
|
||||
// default, unless enabled with the -expose_leases flag
|
||||
leaseMetrics = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "dnsmasq_lease_expiry",
|
||||
Help: "Expiry time for active DHCP leases",
|
||||
},
|
||||
[]string{"mac_addr", "ip_addr", "computer_name", "client_id"},
|
||||
)
|
||||
|
||||
leases = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "dnsmasq_leases",
|
||||
Help: "Number of DHCP leases handed out",
|
||||
|
@ -116,6 +129,7 @@ func init() {
|
|||
prometheus.MustRegister(g)
|
||||
}
|
||||
prometheus.MustRegister(leases)
|
||||
prometheus.MustRegister(leaseMetrics)
|
||||
prometheus.MustRegister(version.NewCollector("dnsmasq_exporter"))
|
||||
}
|
||||
|
||||
|
@ -132,6 +146,15 @@ type server struct {
|
|||
dnsClient *dns.Client
|
||||
dnsmasqAddr string
|
||||
leasesPath string
|
||||
exposeLeases bool
|
||||
}
|
||||
|
||||
type lease struct {
|
||||
expiry uint64
|
||||
macAddress string
|
||||
ipAddress string
|
||||
computerName string
|
||||
clientId string
|
||||
}
|
||||
|
||||
func question(name string) dns.Question {
|
||||
|
@ -142,6 +165,68 @@ func question(name string) dns.Question {
|
|||
}
|
||||
}
|
||||
|
||||
func parseLease(line string) (*lease, error) {
|
||||
arr := strings.Fields(line)
|
||||
if got, want := len(arr), 5; got != want {
|
||||
return nil, fmt.Errorf("illegal lease: expected %d fields, got %d", want, got)
|
||||
}
|
||||
|
||||
expires, err := strconv.ParseUint(arr[0], 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &lease{
|
||||
expiry: expires,
|
||||
macAddress: arr[1],
|
||||
ipAddress: arr[2],
|
||||
computerName: arr[3],
|
||||
clientId: arr[4],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Read the DHCP lease file with the given path and return a list of leases.
|
||||
//
|
||||
// The format of the DHCP lease file written by dnsmasq is not formally
|
||||
// documented in the dnsmasq manual but the format has been described in the
|
||||
// mailing list:
|
||||
//
|
||||
// - https://lists.thekelleys.org.uk/pipermail/dnsmasq-discuss/2006q2/000733.html
|
||||
// - https://lists.thekelleys.org.uk/pipermail/dnsmasq-discuss/2016q2/010595.html
|
||||
//
|
||||
// The DHCP lease file is written to by lease_update_file() in
|
||||
// src/lease.c, and is read by lease_init().
|
||||
func readLeaseFile(path string) ([]lease, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// ignore
|
||||
return []lease{}, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
activeLeases := []lease{}
|
||||
for scanner.Scan() {
|
||||
activeLease, err := parseLease(scanner.Text())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
activeLeases = append(activeLeases, *activeLease)
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return activeLeases, nil
|
||||
}
|
||||
|
||||
func (s *server) metrics(w http.ResponseWriter, r *http.Request) {
|
||||
var eg errgroup.Group
|
||||
|
||||
|
@ -207,26 +292,24 @@ func (s *server) metrics(w http.ResponseWriter, r *http.Request) {
|
|||
})
|
||||
|
||||
eg.Go(func() error {
|
||||
f, err := os.Open(s.leasesPath)
|
||||
activeLeases, err := readLeaseFile(s.leasesPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// ignore
|
||||
leases.Set(0)
|
||||
return nil
|
||||
}
|
||||
log.Println("warn: could not open leases file:", err)
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
scanner := bufio.NewScanner(f)
|
||||
var lines float64
|
||||
for scanner.Scan() {
|
||||
lines++
|
||||
|
||||
leases.Set(float64(len(activeLeases)))
|
||||
|
||||
if s.exposeLeases {
|
||||
for _, activeLease := range activeLeases {
|
||||
leaseMetrics.With(prometheus.Labels{
|
||||
"mac_addr": activeLease.macAddress,
|
||||
"ip_addr": activeLease.ipAddress,
|
||||
"computer_name": activeLease.computerName,
|
||||
"client_id": activeLease.clientId,
|
||||
}).Set(float64(activeLease.expiry))
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
leases.Set(lines)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
|
@ -247,6 +330,7 @@ func main() {
|
|||
},
|
||||
dnsmasqAddr: *dnsmasqAddr,
|
||||
leasesPath: *leasesPath,
|
||||
exposeLeases: *exposeLeases,
|
||||
}
|
||||
http.HandleFunc(*metricsPath, s.metrics)
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
@ -67,6 +67,7 @@ func TestDnsmasqExporter(t *testing.T) {
|
|||
},
|
||||
dnsmasqAddr: "localhost:" + port,
|
||||
leasesPath: "testdata/dnsmasq.leases",
|
||||
exposeLeases: false,
|
||||
}
|
||||
|
||||
t.Run("first", func(t *testing.T) {
|
||||
|
@ -121,6 +122,34 @@ func TestDnsmasqExporter(t *testing.T) {
|
|||
}
|
||||
})
|
||||
|
||||
t.Run("should not expose lease information when disabled", func(t *testing.T) {
|
||||
metrics := fetchMetrics(t, s)
|
||||
for key, _ := range metrics {
|
||||
if strings.Contains(key, "dnsmasq_lease_expiry") {
|
||||
t.Errorf("lease information should not be exposed when disabled: %v", key)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
s.exposeLeases = true
|
||||
|
||||
t.Run("with high-cardinality lease metrics enabled", func(t *testing.T) {
|
||||
metrics := fetchMetrics(t, s)
|
||||
want := map[string]string{
|
||||
"dnsmasq_leases": "2",
|
||||
"dnsmasq_cachesize": "666",
|
||||
"dnsmasq_hits": "5",
|
||||
"dnsmasq_misses": "1",
|
||||
"dnsmasq_lease_expiry{client_id=\"00:00:00:00:00:00\",computer_name=\"host-1\",ip_addr=\"10.10.10.10\",mac_addr=\"00:00:00:00:00:00\"}": "1.625595932e+09",
|
||||
"dnsmasq_lease_expiry{client_id=\"00:00:00:00:00:01\",computer_name=\"host-2\",ip_addr=\"10.10.10.11\",mac_addr=\"00:00:00:00:00:01\"}": "0",
|
||||
}
|
||||
for key, val := range want {
|
||||
if got, want := metrics[key], val; got != want {
|
||||
t.Errorf("metric %q: got %q, want %q", key, got, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
s.leasesPath = "testdata/dnsmasq.leases.does.not.exists"
|
||||
|
||||
t.Run("without leases file", func(t *testing.T) {
|
||||
|
@ -128,7 +157,7 @@ func TestDnsmasqExporter(t *testing.T) {
|
|||
want := map[string]string{
|
||||
"dnsmasq_leases": "0",
|
||||
"dnsmasq_cachesize": "666",
|
||||
"dnsmasq_hits": "4",
|
||||
"dnsmasq_hits": "6",
|
||||
"dnsmasq_misses": "1",
|
||||
}
|
||||
for key, val := range want {
|
||||
|
|
4
testdata/dnsmasq.leases
vendored
4
testdata/dnsmasq.leases
vendored
|
@ -1,2 +1,2 @@
|
|||
lease1
|
||||
lease2
|
||||
1625595932 00:00:00:00:00:00 10.10.10.10 host-1 00:00:00:00:00:00
|
||||
0 00:00:00:00:00:01 10.10.10.11 host-2 00:00:00:00:00:01
|
Loading…
Reference in a new issue