Compare commits

...

10 commits

Author SHA1 Message Date
Marc Tudurí 44b1448080
Move collector logic from main to separate package (#26)
Co-authored-by: Robert Fratto <robertfratto@gmail.com>
2023-06-20 12:00:26 +02:00
Brandon Richardson e3caa1dcf7
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.
2023-04-07 18:09:17 +02:00
dependabot[bot] 08fab8ede7
Bump github.com/prometheus/client_golang from 1.11.0 to 1.11.1 (#22)
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.11.0 to 1.11.1.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.11.0...v1.11.1)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-15 08:27:27 +01:00
Emily 3e12c69d33
Fix accidental leftovers from dashboard schema upgrade (#21) 2023-01-31 12:46:47 +01:00
Emily Ahlstrand Rager 5e0344ddec Updated dashboard schema to latest 2023-01-11 16:53:59 +01:00
Emily Ahlstrand Rager 297a498632 Fixed linting issues reported by mixtool 2023-01-11 16:53:59 +01:00
Tom Wilkie 6d9ae78e7d
Datasource template variable should be labelled 'Data Source' (#19)
Signed-off-by: Tom Wilkie <tom@grafana.com>
2021-10-19 18:55:15 +02:00
Michael Stapelberg 9df65941a0 Switch from Travis to GitHub Actions 2021-10-16 08:44:51 +02:00
Paulo Edgar Castro ba2c117d6a
Bump docker file so it builds again. (#18) 2021-10-14 20:24:47 +02:00
dependabot[bot] 3fa623b162
Bump github.com/miekg/dns from 1.1.14 to 1.1.25 (#17)
Bumps [github.com/miekg/dns](https://github.com/miekg/dns) from 1.1.14 to 1.1.25.
- [Release notes](https://github.com/miekg/dns/releases)
- [Changelog](https://github.com/miekg/dns/blob/master/Makefile.release)
- [Commits](https://github.com/miekg/dns/compare/v1.1.14...v1.1.25)

---
updated-dependencies:
- dependency-name: github.com/miekg/dns
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-06 17:36:42 +02:00
12 changed files with 597 additions and 346 deletions

30
.github/workflows/go.yml vendored Normal file
View file

@ -0,0 +1,30 @@
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Ensure all files were formatted as per gofmt
run: |
[ "$(gofmt -l $(find . -name '*.go') 2>&1)" = "" ]
- name: install binaries
run: go install github.com/google/dnsmasq_exporter
- name: build tests
run: go test -c ./collector
- name: docker build
run: docker build --pull --no-cache --rm -t=dns -f travis/Dockerfile .
- name: run tests in docker
run: docker run -v $PWD:/usr/src:ro -e TESTDATA_FILE_PATH=/usr/src/collector/testdata/dnsmasq.leases dns /bin/sh -c './collector.test -test.v'

View file

@ -1,22 +0,0 @@
# Use the (faster) container-based infrastructure, see also
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
sudo: false
dist: trusty
services:
- docker
language: go
go:
- "1.13"
go_import_path: github.com/stapelberg/dnsmasq_exporter
script:
# Check whether files are syntactically correct.
- "gofmt -l $(find . -name '*.go' | tr '\\n' ' ') >/dev/null"
# Check whether files were not gofmt'ed.
- "gosrc=$(find . -name '*.go' | tr '\\n' ' '); [ $(gofmt -l $gosrc 2>&- | wc -l) -eq 0 ] || (echo 'gofmt was not run on these files:'; gofmt -l $gosrc 2>&-; false)"
- go vet .
- go test -c
- docker build --pull --no-cache --rm -t=dns -f travis/Dockerfile .
- docker run -v $PWD:/usr/src:ro dns /bin/sh -c './dnsmasq_exporter.test -test.v'

View file

@ -1,5 +1,5 @@
# build stage
FROM golang:1.12.6-stretch AS build-env
FROM golang:1.17.2-stretch AS build-env
ADD . /src
ENV CGO_ENABLED=0
WORKDIR /src

View file

@ -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

301
collector/collector.go Normal file
View file

@ -0,0 +1,301 @@
// 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 collects dnsmasq statistics as a Prometheus collector.
package collector
import (
"bufio"
"fmt"
"log"
"os"
"strconv"
"strings"
"github.com/miekg/dns"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/sync/errgroup"
)
var (
// floatMetrics contains prometheus Gauges, keyed by the stats DNS record
// they correspond to.
floatMetrics = map[string]*prometheus.Desc{
"cachesize.bind.": prometheus.NewDesc(
"dnsmasq_cachesize",
"configured size of the DNS cache",
nil, nil,
),
"insertions.bind.": prometheus.NewDesc(
"dnsmasq_insertions",
"DNS cache insertions",
nil, nil,
),
"evictions.bind.": prometheus.NewDesc(
"dnsmasq_evictions",
"DNS cache exictions: numbers of entries which replaced an unexpired cache entry",
nil, nil,
),
"misses.bind.": prometheus.NewDesc(
"dnsmasq_misses",
"DNS cache misses: queries which had to be forwarded",
nil, nil,
),
"hits.bind.": prometheus.NewDesc(
"dnsmasq_hits",
"DNS queries answered locally (cache hits)",
nil, nil,
),
"auth.bind.": prometheus.NewDesc(
"dnsmasq_auth",
"DNS queries for authoritative zones",
nil, nil,
),
}
serversMetrics = map[string]*prometheus.Desc{
"queries": prometheus.NewDesc(
"dnsmasq_servers_queries",
"DNS queries on upstream server",
[]string{"server"}, nil,
),
"queries_failed": prometheus.NewDesc(
"dnsmasq_servers_queries_failed",
"DNS queries failed on upstream server",
[]string{"server"}, nil,
),
}
// individual lease metrics have high cardinality and are thus disabled by
// default, unless enabled with the -expose_leases flag
leaseMetrics = prometheus.NewDesc(
"dnsmasq_lease_expiry",
"Expiry time for active DHCP leases",
[]string{"mac_addr", "ip_addr", "computer_name", "client_id"},
nil,
)
leases = prometheus.NewDesc(
"dnsmasq_leases",
"Number of DHCP leases handed out",
nil, nil,
)
)
// From https://manpages.debian.org/stretch/dnsmasq-base/dnsmasq.8.en.html:
// The cache statistics are also available in the DNS as answers to queries of
// class CHAOS and type TXT in domain bind. The domain names are cachesize.bind,
// insertions.bind, evictions.bind, misses.bind, hits.bind, auth.bind and
// servers.bind. An example command to query this, using the dig utility would
// be:
// dig +short chaos txt cachesize.bind
// Config contains the configuration for the collector.
type Config struct {
DnsClient *dns.Client
DnsmasqAddr string
LeasesPath string
ExposeLeases bool
}
// Collector implements prometheus.Collector and exposes dnsmasq metrics.
type Collector struct {
cfg Config
}
type lease struct {
expiry uint64
macAddress string
ipAddress string
computerName string
clientId string
}
// New creates a new Collector.
func New(cfg Config) *Collector {
return &Collector{
cfg: cfg,
}
}
func (c *Collector) Describe(ch chan<- *prometheus.Desc) {
for _, d := range floatMetrics {
ch <- d
}
for _, d := range serversMetrics {
ch <- d
}
ch <- leases
ch <- leaseMetrics
}
func (c *Collector) Collect(ch chan<- prometheus.Metric) {
var eg errgroup.Group
eg.Go(func() error {
msg := &dns.Msg{
MsgHdr: dns.MsgHdr{
Id: dns.Id(),
RecursionDesired: true,
},
Question: []dns.Question{
question("cachesize.bind."),
question("insertions.bind."),
question("evictions.bind."),
question("misses.bind."),
question("hits.bind."),
question("auth.bind."),
question("servers.bind."),
},
}
in, _, err := c.cfg.DnsClient.Exchange(msg, c.cfg.DnsmasqAddr)
if err != nil {
return err
}
for _, a := range in.Answer {
txt, ok := a.(*dns.TXT)
if !ok {
continue
}
switch txt.Hdr.Name {
case "servers.bind.":
for _, str := range txt.Txt {
arr := strings.Fields(str)
if got, want := len(arr), 3; got != want {
return fmt.Errorf("stats DNS record servers.bind.: unexpeced number of argument in record: got %d, want %d", got, want)
}
queries, err := strconv.ParseFloat(arr[1], 64)
if err != nil {
return err
}
failedQueries, err := strconv.ParseFloat(arr[2], 64)
if err != nil {
return err
}
ch <- prometheus.MustNewConstMetric(serversMetrics["queries"], prometheus.GaugeValue, queries, arr[0])
ch <- prometheus.MustNewConstMetric(serversMetrics["queries_failed"], prometheus.GaugeValue, failedQueries, arr[0])
}
default:
g, ok := floatMetrics[txt.Hdr.Name]
if !ok {
continue // ignore unexpected answer from dnsmasq
}
if got, want := len(txt.Txt), 1; got != want {
return fmt.Errorf("stats DNS record %q: unexpected number of replies: got %d, want %d", txt.Hdr.Name, got, want)
}
f, err := strconv.ParseFloat(txt.Txt[0], 64)
if err != nil {
return err
}
ch <- prometheus.MustNewConstMetric(g, prometheus.GaugeValue, f)
}
}
return nil
})
eg.Go(func() error {
activeLeases, err := readLeaseFile(c.cfg.LeasesPath)
if err != nil {
return err
}
ch <- prometheus.MustNewConstMetric(leases, prometheus.GaugeValue, float64(len(activeLeases)))
if c.cfg.ExposeLeases {
for _, activeLease := range activeLeases {
ch <- prometheus.MustNewConstMetric(leaseMetrics, prometheus.GaugeValue, float64(activeLease.expiry),
activeLease.macAddress, activeLease.ipAddress, activeLease.computerName, activeLease.clientId)
}
}
return nil
})
if err := eg.Wait(); err != nil {
log.Printf("could not complete scrape: %v", err)
}
}
func question(name string) dns.Question {
return dns.Question{
Name: name,
Qtype: dns.TypeTXT,
Qclass: dns.ClassCHAOS,
}
}
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
}

View file

@ -1,4 +1,18 @@
package main
// 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
import (
"fmt"
@ -13,6 +27,7 @@ import (
"time"
"github.com/miekg/dns"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
@ -60,17 +75,24 @@ func TestDnsmasqExporter(t *testing.T) {
time.Sleep(10 * time.Millisecond) // do not hog the CPU
}
s := &server{
promHandler: promhttp.Handler(),
dnsClient: &dns.Client{
SingleInflight: true,
},
dnsmasqAddr: "localhost:" + port,
leasesPath: "testdata/dnsmasq.leases",
testDataFilePath := os.Getenv("TESTDATA_FILE_PATH")
if testDataFilePath == "" {
testDataFilePath = "./testdata/dnsmasq.leases"
}
cfg := Config{
DnsClient: &dns.Client{
SingleInflight: true,
},
DnsmasqAddr: "localhost:" + port,
LeasesPath: testDataFilePath,
ExposeLeases: false,
}
c := New(cfg)
t.Run("first", func(t *testing.T) {
metrics := fetchMetrics(t, s)
metrics := fetchMetrics(t, c)
want := map[string]string{
"dnsmasq_leases": "2",
"dnsmasq_cachesize": "666",
@ -85,7 +107,7 @@ func TestDnsmasqExporter(t *testing.T) {
})
t.Run("second", func(t *testing.T) {
metrics := fetchMetrics(t, s)
metrics := fetchMetrics(t, c)
want := map[string]string{
"dnsmasq_leases": "2",
"dnsmasq_cachesize": "666",
@ -107,7 +129,7 @@ func TestDnsmasqExporter(t *testing.T) {
}
t.Run("after query", func(t *testing.T) {
metrics := fetchMetrics(t, s)
metrics := fetchMetrics(t, c)
want := map[string]string{
"dnsmasq_leases": "2",
"dnsmasq_cachesize": "666",
@ -121,14 +143,42 @@ func TestDnsmasqExporter(t *testing.T) {
}
})
s.leasesPath = "testdata/dnsmasq.leases.does.not.exists"
t.Run("should not expose lease information when disabled", func(t *testing.T) {
metrics := fetchMetrics(t, c)
for key, _ := range metrics {
if strings.Contains(key, "dnsmasq_lease_expiry") {
t.Errorf("lease information should not be exposed when disabled: %v", key)
}
}
})
c.cfg.ExposeLeases = true
t.Run("with high-cardinality lease metrics enabled", func(t *testing.T) {
metrics := fetchMetrics(t, c)
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)
}
}
})
c.cfg.LeasesPath = "testdata/dnsmasq.leases.does.not.exists"
t.Run("without leases file", func(t *testing.T) {
metrics := fetchMetrics(t, s)
metrics := fetchMetrics(t, c)
want := map[string]string{
"dnsmasq_leases": "0",
"dnsmasq_cachesize": "666",
"dnsmasq_hits": "4",
"dnsmasq_hits": "6",
"dnsmasq_misses": "1",
}
for key, val := range want {
@ -140,9 +190,13 @@ func TestDnsmasqExporter(t *testing.T) {
}
func fetchMetrics(t *testing.T, s *server) map[string]string {
func fetchMetrics(t *testing.T, c *Collector) map[string]string {
reg := prometheus.NewRegistry()
reg.MustRegister(c)
handler := promhttp.HandlerFor(reg, promhttp.HandlerOpts{})
rec := httptest.NewRecorder()
s.metrics(rec, httptest.NewRequest("GET", "/metrics", nil))
handler.ServeHTTP(rec, httptest.NewRequest("GET", "/metrics", nil))
resp := rec.Result()
if got, want := resp.StatusCode, http.StatusOK; got != want {
b, _ := ioutil.ReadAll(resp.Body)

2
collector/testdata/dnsmasq.leases vendored Normal file
View file

@ -0,0 +1,2 @@
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

View file

@ -3,25 +3,37 @@
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": 34,
"iteration": 1602437170658,
"id": 94,
"links": [],
"liveNow": false,
"panels": [
{
"collapsed": false,
"datasource": null,
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"gridPos": {
"h": 1,
"w": 24,
@ -30,14 +42,24 @@
},
"id": 15,
"panels": [],
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"refId": "A"
}
],
"title": "DNS Stats",
"type": "row"
},
{
"datasource": "$datasource",
"datasource": {
"uid": "$datasource"
},
"fieldConfig": {
"defaults": {
"custom": {},
"mappings": [],
"thresholds": {
"mode": "absolute",
@ -73,11 +95,15 @@
],
"fields": "",
"values": false
}
},
"textMode": "auto"
},
"pluginVersion": "7.0.4",
"pluginVersion": "9.3.2-67a213dc85",
"targets": [
{
"datasource": {
"uid": "$datasource"
},
"expr": "sum(dnsmasq_leases{job=~\"$job\", instance=~\"$instance\"})",
"instant": false,
"interval": "",
@ -85,16 +111,15 @@
"refId": "A"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Leases",
"type": "stat"
},
{
"datasource": "$datasource",
"datasource": {
"uid": "$datasource"
},
"fieldConfig": {
"defaults": {
"custom": {},
"mappings": [],
"noValue": "0",
"thresholds": {
@ -131,11 +156,15 @@
],
"fields": "",
"values": false
}
},
"textMode": "auto"
},
"pluginVersion": "7.0.4",
"pluginVersion": "9.3.2-67a213dc85",
"targets": [
{
"datasource": {
"uid": "$datasource"
},
"expr": "sum(dnsmasq_servers_queries{job=~\"$job\", instance=~\"$instance\"})",
"instant": false,
"interval": "",
@ -143,17 +172,18 @@
"refId": "A"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Upstream Queries",
"type": "stat"
},
{
"datasource": "$datasource",
"datasource": {
"uid": "$datasource"
},
"fieldConfig": {
"defaults": {
"custom": {},
"mappings": [],
"max": 1,
"min": 0,
"noValue": "0%",
"thresholds": {
"mode": "absolute",
@ -190,11 +220,15 @@
],
"fields": "",
"values": false
}
},
"textMode": "auto"
},
"pluginVersion": "7.0.4",
"pluginVersion": "9.3.2-67a213dc85",
"targets": [
{
"datasource": {
"uid": "$datasource"
},
"expr": "sum(dnsmasq_servers_queries_failed{job=~\"$job\", instance=~\"$instance\"}) / sum(dnsmasq_servers_queries{job=~\"$job\", instance=~\"$instance\"})",
"instant": false,
"interval": "",
@ -202,14 +236,15 @@
"refId": "A"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Upstream Failed Queries",
"type": "stat"
},
{
"collapsed": false,
"datasource": null,
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"gridPos": {
"h": 1,
"w": 24,
@ -218,15 +253,27 @@
},
"id": 13,
"panels": [],
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"refId": "A"
}
],
"title": "Build Info",
"type": "row"
},
{
"datasource": "prometheus",
"datasource": {
"uid": "$datasource"
},
"fieldConfig": {
"defaults": {
"custom": {
"align": null
"displayMode": "auto",
"inspect": false
},
"mappings": [],
"thresholds": {
@ -253,11 +300,21 @@
},
"id": 4,
"options": {
"footer": {
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"showHeader": true
},
"pluginVersion": "7.0.4",
"pluginVersion": "9.3.2-67a213dc85",
"targets": [
{
"datasource": {
"uid": "$datasource"
},
"expr": "dnsmasq_exporter_build_info{job=~\"$job\", instance=~\"$instance\"}",
"format": "time_series",
"instant": true,
@ -266,14 +323,16 @@
"refId": "A"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Build Info",
"transformations": [
{
"id": "labelsToFields",
"options": {}
},
{
"id": "merge",
"options": {}
},
{
"id": "organize",
"options": {
@ -301,7 +360,10 @@
},
{
"collapsed": false,
"datasource": null,
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"gridPos": {
"h": 1,
"w": 24,
@ -310,14 +372,24 @@
},
"id": 11,
"panels": [],
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"refId": "A"
}
],
"title": "Cache Stats",
"type": "row"
},
{
"datasource": "$datasource",
"datasource": {
"uid": "$datasource"
},
"fieldConfig": {
"defaults": {
"custom": {},
"mappings": [],
"thresholds": {
"mode": "absolute",
@ -353,11 +425,15 @@
],
"fields": "",
"values": false
}
},
"textMode": "auto"
},
"pluginVersion": "7.0.4",
"pluginVersion": "9.3.2-67a213dc85",
"targets": [
{
"datasource": {
"uid": "$datasource"
},
"expr": "sum(dnsmasq_cachesize{job=~\"$job\", instance=~\"$instance\"})",
"instant": false,
"interval": "",
@ -365,16 +441,15 @@
"refId": "A"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Cache Size",
"type": "stat"
},
{
"datasource": "$datasource",
"datasource": {
"uid": "$datasource"
},
"fieldConfig": {
"defaults": {
"custom": {},
"mappings": [],
"thresholds": {
"mode": "absolute",
@ -410,11 +485,15 @@
],
"fields": "",
"values": false
}
},
"textMode": "auto"
},
"pluginVersion": "7.0.4",
"pluginVersion": "9.3.2-67a213dc85",
"targets": [
{
"datasource": {
"uid": "$datasource"
},
"expr": "sum(dnsmasq_hits{job=~\"$job\", instance=~\"$instance\"})",
"instant": false,
"interval": "",
@ -422,16 +501,15 @@
"refId": "A"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Cache Hits",
"type": "stat"
},
{
"datasource": "$datasource",
"datasource": {
"uid": "$datasource"
},
"fieldConfig": {
"defaults": {
"custom": {},
"mappings": [],
"thresholds": {
"mode": "absolute",
@ -467,11 +545,15 @@
],
"fields": "",
"values": false
}
},
"textMode": "auto"
},
"pluginVersion": "7.0.4",
"pluginVersion": "9.3.2-67a213dc85",
"targets": [
{
"datasource": {
"uid": "$datasource"
},
"expr": "sum(dnsmasq_insertions{job=~\"$job\", instance=~\"$instance\"})",
"instant": false,
"interval": "",
@ -479,26 +561,24 @@
"refId": "A"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Cache Insertions",
"type": "stat"
}
],
"schemaVersion": 25,
"schemaVersion": 37,
"style": "dark",
"tags": [],
"templating": {
"list": [
{
"current": {
"selected": true,
"text": "prometheus",
"value": "prometheus"
"selected": false,
"text": "default",
"value": "default"
},
"hide": 0,
"includeAll": false,
"label": null,
"label": "Data Source",
"multi": false,
"name": "datasource",
"options": [],
@ -510,79 +590,55 @@
"type": "datasource"
},
{
"allValue": null,
"allValue": ".+",
"current": {
"selected": true,
"text": "dnsmasq",
"value": [
"dnsmasq"
]
"selected": false,
"text": "All",
"value": "$__all"
},
"datasource": {
"uid": "$datasource"
},
"datasource": "$datasource",
"definition": "label_values(dnsmasq_exporter_build_info, job)",
"hide": 0,
"includeAll": true,
"label": null,
"label": "job",
"multi": true,
"name": "job",
"options": [
{
"selected": false,
"text": "All",
"value": "$__all"
},
{
"selected": true,
"text": "dnsmasq",
"value": "dnsmasq"
}
],
"options": [],
"query": "label_values(dnsmasq_exporter_build_info, job)",
"refresh": 0,
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"tagValuesQuery": "",
"tags": [],
"tagsQuery": "",
"type": "query",
"useTags": false
},
{
"allValue": null,
"allValue": ".+",
"current": {
"selected": true,
"text": "router.lan",
"value": [
"router.lan"
]
"selected": false,
"text": "All",
"value": "$__all"
},
"datasource": {
"uid": "$datasource"
},
"datasource": "$datasource",
"definition": "label_values(dnsmasq_exporter_build_info, instance)",
"hide": 0,
"includeAll": true,
"label": null,
"label": "instance",
"multi": true,
"name": "instance",
"options": [
{
"selected": false,
"text": "All",
"value": "$__all"
},
{
"selected": true,
"text": "router.lan",
"value": "router.lan"
}
],
"options": [],
"query": "label_values(dnsmasq_exporter_build_info, instance)",
"refresh": 0,
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"tagValuesQuery": "",
"tags": [],
"tagsQuery": "",
"type": "query",
"useTags": false
@ -609,5 +665,6 @@
"timezone": "",
"title": "dnsmasq",
"uid": "ySypWTcMk",
"version": 2
}
"version": 2,
"weekStart": ""
}

View file

@ -16,17 +16,11 @@
package main
import (
"bufio"
"flag"
"fmt"
"log"
"net/http"
"os"
"strconv"
"strings"
"golang.org/x/sync/errgroup"
"github.com/google/dnsmasq_exporter/collector"
"github.com/miekg/dns"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
@ -38,6 +32,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")
@ -50,205 +47,33 @@ var (
"path under which metrics are served")
)
var (
// floatMetrics contains prometheus Gauges, keyed by the stats DNS record
// they correspond to.
floatMetrics = map[string]prometheus.Gauge{
"cachesize.bind.": prometheus.NewGauge(prometheus.GaugeOpts{
Name: "dnsmasq_cachesize",
Help: "configured size of the DNS cache",
}),
"insertions.bind.": prometheus.NewGauge(prometheus.GaugeOpts{
Name: "dnsmasq_insertions",
Help: "DNS cache insertions",
}),
"evictions.bind.": prometheus.NewGauge(prometheus.GaugeOpts{
Name: "dnsmasq_evictions",
Help: "DNS cache exictions: numbers of entries which replaced an unexpired cache entry",
}),
"misses.bind.": prometheus.NewGauge(prometheus.GaugeOpts{
Name: "dnsmasq_misses",
Help: "DNS cache misses: queries which had to be forwarded",
}),
"hits.bind.": prometheus.NewGauge(prometheus.GaugeOpts{
Name: "dnsmasq_hits",
Help: "DNS queries answered locally (cache hits)",
}),
"auth.bind.": prometheus.NewGauge(prometheus.GaugeOpts{
Name: "dnsmasq_auth",
Help: "DNS queries for authoritative zones",
}),
}
serversMetrics = map[string]*prometheus.GaugeVec{
"queries": prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "dnsmasq_servers_queries",
Help: "DNS queries on upstream server",
},
[]string{"server"},
),
"queries_failed": prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "dnsmasq_servers_queries_failed",
Help: "DNS queries failed on upstream server",
},
[]string{"server"},
),
}
leases = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "dnsmasq_leases",
Help: "Number of DHCP leases handed out",
})
)
func init() {
for _, g := range floatMetrics {
prometheus.MustRegister(g)
}
for _, g := range serversMetrics {
prometheus.MustRegister(g)
}
prometheus.MustRegister(leases)
prometheus.MustRegister(version.NewCollector("dnsmasq_exporter"))
}
// From https://manpages.debian.org/stretch/dnsmasq-base/dnsmasq.8.en.html:
// The cache statistics are also available in the DNS as answers to queries of
// class CHAOS and type TXT in domain bind. The domain names are cachesize.bind,
// insertions.bind, evictions.bind, misses.bind, hits.bind, auth.bind and
// servers.bind. An example command to query this, using the dig utility would
// be:
// dig +short chaos txt cachesize.bind
type server struct {
promHandler http.Handler
dnsClient *dns.Client
dnsmasqAddr string
leasesPath string
}
func question(name string) dns.Question {
return dns.Question{
Name: name,
Qtype: dns.TypeTXT,
Qclass: dns.ClassCHAOS,
}
}
func (s *server) metrics(w http.ResponseWriter, r *http.Request) {
var eg errgroup.Group
eg.Go(func() error {
msg := &dns.Msg{
MsgHdr: dns.MsgHdr{
Id: dns.Id(),
RecursionDesired: true,
},
Question: []dns.Question{
question("cachesize.bind."),
question("insertions.bind."),
question("evictions.bind."),
question("misses.bind."),
question("hits.bind."),
question("auth.bind."),
question("servers.bind."),
},
}
in, _, err := s.dnsClient.Exchange(msg, s.dnsmasqAddr)
if err != nil {
return err
}
for _, a := range in.Answer {
txt, ok := a.(*dns.TXT)
if !ok {
continue
}
switch txt.Hdr.Name {
case "servers.bind.":
for _, str := range txt.Txt {
arr := strings.Fields(str)
if got, want := len(arr), 3; got != want {
return fmt.Errorf("stats DNS record servers.bind.: unexpeced number of argument in record: got %d, want %d", got, want)
}
queries, err := strconv.ParseFloat(arr[1], 64)
if err != nil {
return err
}
failedQueries, err := strconv.ParseFloat(arr[2], 64)
if err != nil {
return err
}
serversMetrics["queries"].WithLabelValues(arr[0]).Set(queries)
serversMetrics["queries_failed"].WithLabelValues(arr[0]).Set(failedQueries)
}
default:
g, ok := floatMetrics[txt.Hdr.Name]
if !ok {
continue // ignore unexpected answer from dnsmasq
}
if got, want := len(txt.Txt), 1; got != want {
return fmt.Errorf("stats DNS record %q: unexpected number of replies: got %d, want %d", txt.Hdr.Name, got, want)
}
f, err := strconv.ParseFloat(txt.Txt[0], 64)
if err != nil {
return err
}
g.Set(f)
}
}
return nil
})
eg.Go(func() error {
f, err := os.Open(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++
}
if err := scanner.Err(); err != nil {
return err
}
leases.Set(lines)
return nil
})
if err := eg.Wait(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
s.promHandler.ServeHTTP(w, r)
}
func main() {
flag.Parse()
s := &server{
promHandler: promhttp.Handler(),
dnsClient: &dns.Client{
var (
dnsClient = &dns.Client{
SingleInflight: true,
},
dnsmasqAddr: *dnsmasqAddr,
leasesPath: *leasesPath,
}
http.HandleFunc(*metricsPath, s.metrics)
}
cfg = collector.Config{
DnsClient: dnsClient,
DnsmasqAddr: *dnsmasqAddr,
LeasesPath: *leasesPath,
ExposeLeases: *exposeLeases,
}
collector = collector.New(cfg)
reg = prometheus.NewRegistry()
)
reg.MustRegister(collector)
http.Handle(*metricsPath, promhttp.HandlerFor(
prometheus.Gatherers{prometheus.DefaultGatherer, reg},
promhttp.HandlerOpts{},
))
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`<html>
<head><title>Dnsmasq Exporter</title></head>
@ -257,7 +82,6 @@ func main() {
<p><a href="` + *metricsPath + `">Metrics</a></p>
</body></html>`))
})
log.Println("Listening on", *listen)
log.Println("Service metrics under", *metricsPath)
log.Fatal(http.ListenAndServe(*listen, nil))

4
go.mod
View file

@ -3,8 +3,8 @@ module github.com/google/dnsmasq_exporter
go 1.12
require (
github.com/miekg/dns v1.1.14
github.com/prometheus/client_golang v1.11.0
github.com/miekg/dns v1.1.25
github.com/prometheus/client_golang v1.11.1
github.com/prometheus/common v0.31.1
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
)

12
go.sum
View file

@ -138,8 +138,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.1.14 h1:wkQWn9wIp4mZbwW8XV6Km6owkvRPbOiV004ZM2CkGvA=
github.com/miekg/dns v1.1.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.25 h1:dFwPR6SfLtrSwgDcIq2bcU/gVutB4sNApq2HBdqcakg=
github.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
@ -153,8 +153,9 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.11.1 h1:+4eQaD7vAZ6DsfsxB15hbE0odUjGI5ARs9yskGu1v4s=
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@ -191,6 +192,7 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@ -238,6 +240,7 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -282,6 +285,8 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -329,6 +334,7 @@ golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgw
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=

View file

@ -1,2 +0,0 @@
lease1
lease2