2023-06-20 11:00:26 +01:00
|
|
|
|
// Copyright 2016 Google Inc. All Rights Reserved.
|
|
|
|
|
//
|
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
|
//
|
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
//
|
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
|
// limitations under the License.
|
|
|
|
|
|
|
|
|
|
package collector
|
2018-05-05 14:00:19 +01:00
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"io/ioutil"
|
|
|
|
|
"net"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
|
|
|
|
"os"
|
|
|
|
|
"os/exec"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/miekg/dns"
|
2023-06-20 11:00:26 +01:00
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
2018-05-05 14:00:19 +01:00
|
|
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestDnsmasqExporter(t *testing.T) {
|
|
|
|
|
// NOTE(stapelberg): dnsmasq disables DNS operation upon --port=0 (as
|
|
|
|
|
// opposed to picking a free port). Hence, we must pick one. This is
|
|
|
|
|
// inherently prone to race conditions: another process could grab the port
|
|
|
|
|
// between our ln.Close() and dnsmasq’s bind(). Ideally, dnsmasq would
|
|
|
|
|
// support grabbing a free port and announcing it, or inheriting a listening
|
|
|
|
|
// socket à la systemd socket activation.
|
|
|
|
|
|
|
|
|
|
ln, err := net.Listen("tcp", "localhost:0")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
_, port, err := net.SplitHostPort(ln.Addr().String())
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
ln.Close()
|
|
|
|
|
|
|
|
|
|
dnsmasq := exec.Command(
|
|
|
|
|
"dnsmasq",
|
|
|
|
|
"--port="+port,
|
|
|
|
|
"--no-daemon",
|
|
|
|
|
"--cache-size=666",
|
|
|
|
|
"--bind-interfaces",
|
|
|
|
|
"--interface=lo")
|
|
|
|
|
dnsmasq.Stderr = os.Stderr
|
|
|
|
|
fmt.Printf("starting %v\n", dnsmasq.Args)
|
|
|
|
|
if err := dnsmasq.Start(); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
defer dnsmasq.Process.Kill()
|
|
|
|
|
|
|
|
|
|
// Wait until dnsmasq started up
|
|
|
|
|
resolver := &dns.Client{}
|
|
|
|
|
for {
|
|
|
|
|
// Cause a cache miss (dnsmasq must forward this query)
|
|
|
|
|
var m dns.Msg
|
|
|
|
|
m.SetQuestion("localhost.", dns.TypeA)
|
|
|
|
|
if _, _, err := resolver.Exchange(&m, "localhost:"+port); err == nil {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
time.Sleep(10 * time.Millisecond) // do not hog the CPU
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-20 11:00:26 +01:00
|
|
|
|
testDataFilePath := os.Getenv("TESTDATA_FILE_PATH")
|
|
|
|
|
if testDataFilePath == "" {
|
|
|
|
|
testDataFilePath = "./testdata/dnsmasq.leases"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cfg := Config{
|
|
|
|
|
DnsClient: &dns.Client{
|
2018-05-05 14:00:19 +01:00
|
|
|
|
SingleInflight: true,
|
|
|
|
|
},
|
2023-06-20 11:00:26 +01:00
|
|
|
|
DnsmasqAddr: "localhost:" + port,
|
|
|
|
|
LeasesPath: testDataFilePath,
|
|
|
|
|
ExposeLeases: false,
|
2018-05-05 14:00:19 +01:00
|
|
|
|
}
|
|
|
|
|
|
2023-06-20 11:00:26 +01:00
|
|
|
|
c := New(cfg)
|
|
|
|
|
|
2018-05-05 14:00:19 +01:00
|
|
|
|
t.Run("first", func(t *testing.T) {
|
2023-06-20 11:00:26 +01:00
|
|
|
|
metrics := fetchMetrics(t, c)
|
2018-05-05 14:00:19 +01:00
|
|
|
|
want := map[string]string{
|
|
|
|
|
"dnsmasq_leases": "2",
|
|
|
|
|
"dnsmasq_cachesize": "666",
|
|
|
|
|
"dnsmasq_hits": "1",
|
|
|
|
|
"dnsmasq_misses": "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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("second", func(t *testing.T) {
|
2023-06-20 11:00:26 +01:00
|
|
|
|
metrics := fetchMetrics(t, c)
|
2018-05-05 14:00:19 +01:00
|
|
|
|
want := map[string]string{
|
|
|
|
|
"dnsmasq_leases": "2",
|
|
|
|
|
"dnsmasq_cachesize": "666",
|
|
|
|
|
"dnsmasq_hits": "2",
|
|
|
|
|
"dnsmasq_misses": "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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Cause a cache miss (dnsmasq must forward this query)
|
|
|
|
|
var m dns.Msg
|
|
|
|
|
m.SetQuestion("no.such.domain.invalid.", dns.TypeA)
|
|
|
|
|
if _, _, err := resolver.Exchange(&m, "localhost:"+port); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
t.Run("after query", func(t *testing.T) {
|
2023-06-20 11:00:26 +01:00
|
|
|
|
metrics := fetchMetrics(t, c)
|
2018-05-05 14:00:19 +01:00
|
|
|
|
want := map[string]string{
|
|
|
|
|
"dnsmasq_leases": "2",
|
|
|
|
|
"dnsmasq_cachesize": "666",
|
|
|
|
|
"dnsmasq_hits": "3",
|
|
|
|
|
"dnsmasq_misses": "1",
|
|
|
|
|
}
|
|
|
|
|
for key, val := range want {
|
|
|
|
|
if got, want := metrics[key], val; got != want {
|
|
|
|
|
t.Errorf("metric %q: got %q, want %q", key, got, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
2021-08-08 14:07:29 +01:00
|
|
|
|
|
2023-04-07 17:09:17 +01:00
|
|
|
|
t.Run("should not expose lease information when disabled", func(t *testing.T) {
|
2023-06-20 11:00:26 +01:00
|
|
|
|
metrics := fetchMetrics(t, c)
|
2023-04-07 17:09:17 +01:00
|
|
|
|
for key, _ := range metrics {
|
|
|
|
|
if strings.Contains(key, "dnsmasq_lease_expiry") {
|
|
|
|
|
t.Errorf("lease information should not be exposed when disabled: %v", key)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2023-06-20 11:00:26 +01:00
|
|
|
|
c.cfg.ExposeLeases = true
|
2023-04-07 17:09:17 +01:00
|
|
|
|
|
|
|
|
|
t.Run("with high-cardinality lease metrics enabled", func(t *testing.T) {
|
2023-06-20 11:00:26 +01:00
|
|
|
|
metrics := fetchMetrics(t, c)
|
2023-04-07 17:09:17 +01:00
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2023-06-20 11:00:26 +01:00
|
|
|
|
c.cfg.LeasesPath = "testdata/dnsmasq.leases.does.not.exists"
|
2021-08-08 14:07:29 +01:00
|
|
|
|
|
|
|
|
|
t.Run("without leases file", func(t *testing.T) {
|
2023-06-20 11:00:26 +01:00
|
|
|
|
metrics := fetchMetrics(t, c)
|
2021-08-08 14:07:29 +01:00
|
|
|
|
want := map[string]string{
|
|
|
|
|
"dnsmasq_leases": "0",
|
|
|
|
|
"dnsmasq_cachesize": "666",
|
2023-04-07 17:09:17 +01:00
|
|
|
|
"dnsmasq_hits": "6",
|
2021-08-08 14:07:29 +01:00
|
|
|
|
"dnsmasq_misses": "1",
|
|
|
|
|
}
|
|
|
|
|
for key, val := range want {
|
|
|
|
|
if got, want := metrics[key], val; got != want {
|
|
|
|
|
t.Errorf("metric %q: got %q, want %q", key, got, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2018-05-05 14:00:19 +01:00
|
|
|
|
}
|
|
|
|
|
|
2023-06-20 11:00:26 +01:00
|
|
|
|
func fetchMetrics(t *testing.T, c *Collector) map[string]string {
|
|
|
|
|
reg := prometheus.NewRegistry()
|
|
|
|
|
reg.MustRegister(c)
|
|
|
|
|
handler := promhttp.HandlerFor(reg, promhttp.HandlerOpts{})
|
|
|
|
|
|
2018-05-05 14:00:19 +01:00
|
|
|
|
rec := httptest.NewRecorder()
|
2023-06-20 11:00:26 +01:00
|
|
|
|
handler.ServeHTTP(rec, httptest.NewRequest("GET", "/metrics", nil))
|
2018-05-05 14:00:19 +01:00
|
|
|
|
resp := rec.Result()
|
|
|
|
|
if got, want := resp.StatusCode, http.StatusOK; got != want {
|
|
|
|
|
b, _ := ioutil.ReadAll(resp.Body)
|
|
|
|
|
t.Fatalf("unexpected HTTP status: got %v (%v), want %v", resp.Status, string(b), want)
|
|
|
|
|
}
|
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
metrics := make(map[string]string)
|
|
|
|
|
for _, line := range strings.Split(strings.TrimSpace(string(body)), "\n") {
|
|
|
|
|
if strings.HasPrefix(line, "#") {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
parts := strings.Split(line, " ")
|
|
|
|
|
if len(parts) < 2 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if !strings.HasPrefix(parts[0], "dnsmasq_") {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
metrics[parts[0]] = parts[1]
|
|
|
|
|
}
|
|
|
|
|
return metrics
|
|
|
|
|
}
|