1
0
mirror of https://github.com/prometheus-community/bind_exporter.git synced 2025-07-31 21:24:22 +03:00

Extract bind statistics parsing in sub-packages

This prepares the addition of statistics v3 parsing.
This commit is contained in:
Tobias Schmidt
2016-09-09 20:14:14 -04:00
parent 1a41fed389
commit e141d9013a
7 changed files with 3573 additions and 2955 deletions

0
CHANGELOG.md Normal file
View File

60
bind/client.go Normal file
View File

@ -0,0 +1,60 @@
package bind
// Client queries the BIND API, parses the response and returns stats in a
// generic format.
type Client interface {
Stats() (Statistics, error)
}
// Common bind field types.
const (
QryRTT = "QryRTT"
)
// Statistics is a generic representation of BIND statistics.
type Statistics struct {
Server struct {
IncomingQueries []Stat
IncomingRequests []Stat
NSStats []Stat
}
Views []View
TaskManager TaskManager
}
// View represents a statistics for a single BIND view.
type View struct {
Name string
Cache []Stat
ResolverStats []Stat
ResolverQueries []Stat
}
// Stat represents a single counter value.
type Stat struct {
Name string
Counter uint
}
// Task represents a single running task.
type Task struct {
ID string `xml:"id"`
Name string `xml:"name"`
Quantum uint `xml:"quantum"`
References uint `xml:"references"`
State string `xml:"state"`
}
// TaskManager contains information about all running tasks.
type TaskManager struct {
Tasks []Task `xml:"tasks>task"`
ThreadModel ThreadModel `xml:"thread-model"`
}
// ThreadModel contains task and worker information.
type ThreadModel struct {
Type string `xml:"type"`
WorkerThreads uint `xml:"worker-threads"`
DefaultQuantum uint `xml:"default-quantum"`
TasksRunning uint `xml:"tasks-running"`
}

View File

@ -1,19 +1,15 @@
package main package v2
import ( import (
"encoding/xml" "encoding/xml"
)
const ( "github.com/digitalocean/bind_exporter/bind"
qryRTT = "QryRTT"
) )
type Zone struct { type Zone struct {
Name string `xml:"name"` Name string `xml:"name"`
Rdataclass string `xml:"rdataclass"` Rdataclass string `xml:"rdataclass"`
Serial string `xml:"serial"` Serial string `xml:"serial"`
//TODO a zone can also have a huge number of counters
// <counters>
} }
type Stat struct { type Stat struct {
@ -42,26 +38,6 @@ type Socketmgr struct {
Sockets []Socket `xml:"sockets>socket"` Sockets []Socket `xml:"sockets>socket"`
} }
type Task struct {
ID string `xml:"id"`
Name string `xml:"name"`
Quantum uint `xml:"quantum"`
References uint `xml:"references"`
State string `xml:"state"`
}
type ThreadModel struct {
Type string `xml:"type"`
WorkerThreads uint `xml:"worker-threads"`
DefaultQuantum uint `xml:"default-quantum"`
TasksRunning uint `xml:"tasks-running"`
}
type Taskmgr struct {
Tasks []Task `xml:"tasks>task"`
ThreadModel ThreadModel `xml:"thread-model"`
}
type QueriesIn struct { type QueriesIn struct {
Rdtype []Stat `xml:"rdtype"` Rdtype []Stat `xml:"rdtype"`
} }
@ -71,24 +47,20 @@ type Requests struct {
} }
type Server struct { type Server struct {
Requests Requests `xml:"requests"` //Most important stats Requests Requests `xml:"requests"`
QueriesIn QueriesIn `xml:"queries-in"` //Most important stats QueriesIn QueriesIn `xml:"queries-in"`
NsStats []Stat `xml:"nsstat"` NsStats []Stat `xml:"nsstat"`
SocketStats []Stat `xml:"socketstat"` SocketStats []Stat `xml:"socketstat"`
ZoneStats []Stat `xml:"zonestats"` ZoneStats []Stat `xml:"zonestats"`
} }
type Memory struct {
//TODO
}
type Statistics struct { type Statistics struct {
Views []View `xml:"views>view"` Views []View `xml:"views>view"`
Socketmgr Socketmgr `xml:"socketmgr"` Socketmgr Socketmgr `xml:"socketmgr"`
Taskmgr Taskmgr `xml:"taskmgr"` Taskmgr bind.TaskManager `xml:"taskmgr"`
Server Server `xml:"server"` Server Server `xml:"server"`
Memory Memory `xml:"memory"` Memory struct{} `xml:"memory"`
} }
type Bind struct { type Bind struct {
Statistics Statistics `xml:"statistics"` Statistics Statistics `xml:"statistics"`

79
bind/v2/v2.go Normal file
View File

@ -0,0 +1,79 @@
package v2
import (
"encoding/xml"
"fmt"
"io/ioutil"
"net/http"
"github.com/digitalocean/bind_exporter/bind"
)
// Client implements bind.Client and can be used to query a BIND v2 API.
type Client struct {
url string
http *http.Client
}
// NewClient returns an initialized Client.
func NewClient(url string, c *http.Client) *Client {
return &Client{
url: url,
http: c,
}
}
// Stats implements bind.Stats.
func (c *Client) Stats() (bind.Statistics, error) {
s := bind.Statistics{}
resp, err := c.http.Get(c.url)
if err != nil {
return s, fmt.Errorf("error querying stats: %s", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return s, fmt.Errorf("failed to read response: %s", err)
}
root := Isc{}
if err := xml.Unmarshal([]byte(body), &root); err != nil {
return s, fmt.Errorf("Failed to unmarshal XML response: %s", err)
}
stats := root.Bind.Statistics
for _, t := range stats.Server.QueriesIn.Rdtype {
s.Server.IncomingQueries = append(s.Server.IncomingQueries, stat(t))
}
for _, t := range stats.Server.Requests.Opcode {
s.Server.IncomingRequests = append(s.Server.IncomingRequests, stat(t))
}
for _, t := range stats.Server.NsStats {
s.Server.NSStats = append(s.Server.NSStats, stat(t))
}
for _, view := range stats.Views {
v := bind.View{Name: view.Name}
for _, t := range view.Cache {
v.Cache = append(v.Cache, stat(t))
}
for _, t := range view.Rdtype {
v.ResolverQueries = append(v.ResolverQueries, stat(t))
}
for _, t := range view.Resstat {
v.ResolverStats = append(v.ResolverStats, stat(t))
}
s.Views = append(s.Views, v)
}
s.TaskManager = stats.Taskmgr
return s, nil
}
func stat(s Stat) bind.Stat {
return bind.Stat{
Name: s.Name,
Counter: s.Counter,
}
}

View File

@ -1,12 +1,10 @@
package main package main
import ( import (
"encoding/xml"
"flag" "flag"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"math" "math"
"net"
"net/http" "net/http"
_ "net/http/pprof" _ "net/http/pprof"
"os" "os"
@ -14,6 +12,9 @@ import (
"strings" "strings"
"time" "time"
"github.com/digitalocean/bind_exporter/bind"
"github.com/digitalocean/bind_exporter/bind/v2"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/log" "github.com/prometheus/common/log"
"github.com/prometheus/common/version" "github.com/prometheus/common/version"
@ -38,7 +39,7 @@ var (
incomingRequests = prometheus.NewDesc( incomingRequests = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "incoming_requests_total"), prometheus.BuildFQName(namespace, "", "incoming_requests_total"),
"Number of incomming DNS requests.", "Number of incomming DNS requests.",
[]string{"name"}, nil, []string{"opcode"}, nil,
) )
resolverCache = prometheus.NewDesc( resolverCache = prometheus.NewDesc(
prometheus.BuildFQName(namespace, resolver, "cache_rrsets"), prometheus.BuildFQName(namespace, resolver, "cache_rrsets"),
@ -146,36 +147,21 @@ var (
) )
) )
// Exporter collects Binds stats from the given server and exports // Exporter collects Binds stats from the given server and exports them using
// them using the prometheus metrics package. // the prometheus metrics package.
type Exporter struct { type Exporter struct {
URI string client bind.Client
client *http.Client
} }
// NewExporter returns an initialized Exporter. // NewExporter returns an initialized Exporter.
func NewExporter(uri string, timeout time.Duration) *Exporter { func NewExporter(url string, timeout time.Duration) *Exporter {
return &Exporter{ return &Exporter{
URI: uri, client: v2.NewClient(url, &http.Client{Timeout: timeout}),
client: &http.Client{
Transport: &http.Transport{
Dial: func(netw, addr string) (net.Conn, error) {
c, err := net.DialTimeout(netw, addr, timeout)
if err != nil {
return nil, err
}
if err := c.SetDeadline(time.Now().Add(timeout)); err != nil {
return nil, err
}
return c, nil
},
},
},
} }
} }
// Describe describes all the metrics ever exported by the bind // Describe describes all the metrics ever exported by the bind exporter. It
// exporter. It implements prometheus.Collector. // implements prometheus.Collector.
func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
ch <- up ch <- up
ch <- incomingQueries ch <- incomingQueries
@ -193,48 +179,29 @@ func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
ch <- workerThreads ch <- workerThreads
} }
// Collect fetches the stats from configured bind location and // Collect fetches the stats from configured bind location and delivers them as
// delivers them as Prometheus metrics. It implements prometheus.Collector. // Prometheus metrics. It implements prometheus.Collector.
func (e *Exporter) Collect(ch chan<- prometheus.Metric) { func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
var status float64 stats, err := e.client.Stats()
defer func() {
ch <- prometheus.MustNewConstMetric(up, prometheus.GaugeValue, status)
}()
resp, err := e.client.Get(e.URI)
if err != nil { if err != nil {
log.Error("Error while querying Bind: ", err) ch <- prometheus.MustNewConstMetric(up, prometheus.GaugeValue, 0)
log.Error("Couldn't retrieve BIND stats: ", err)
return return
} }
defer resp.Body.Close() ch <- prometheus.MustNewConstMetric(up, prometheus.GaugeValue, 1)
body, err := ioutil.ReadAll(resp.Body) // Server statistics.
if err != nil { for _, s := range stats.Server.IncomingQueries {
log.Error("Failed to read XML response body: ", err)
return
}
status = 1
root := Isc{}
if err := xml.Unmarshal([]byte(body), &root); err != nil {
log.Error("Failed to unmarshal XML response: ", err)
return
}
stats := root.Bind.Statistics
for _, s := range stats.Server.QueriesIn.Rdtype {
ch <- prometheus.MustNewConstMetric( ch <- prometheus.MustNewConstMetric(
incomingQueries, prometheus.CounterValue, float64(s.Counter), s.Name, incomingQueries, prometheus.CounterValue, float64(s.Counter), s.Name,
) )
} }
for _, s := range stats.Server.Requests.Opcode { for _, s := range stats.Server.IncomingRequests {
ch <- prometheus.MustNewConstMetric( ch <- prometheus.MustNewConstMetric(
incomingRequests, prometheus.CounterValue, float64(s.Counter), s.Name, incomingRequests, prometheus.CounterValue, float64(s.Counter), s.Name,
) )
} }
for _, s := range stats.Server.NSStats {
for _, s := range stats.Server.NsStats {
if desc, ok := serverLabelStats[s.Name]; ok { if desc, ok := serverLabelStats[s.Name]; ok {
r := strings.TrimPrefix(s.Name, "Qry") r := strings.TrimPrefix(s.Name, "Qry")
ch <- prometheus.MustNewConstMetric( ch <- prometheus.MustNewConstMetric(
@ -243,20 +210,19 @@ func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
} }
} }
// View statistics.
for _, v := range stats.Views { for _, v := range stats.Views {
for _, s := range v.Cache { for _, s := range v.Cache {
ch <- prometheus.MustNewConstMetric( ch <- prometheus.MustNewConstMetric(
resolverCache, prometheus.GaugeValue, float64(s.Counter), v.Name, s.Name, resolverCache, prometheus.GaugeValue, float64(s.Counter), v.Name, s.Name,
) )
} }
for _, s := range v.ResolverQueries {
for _, s := range v.Rdtype {
ch <- prometheus.MustNewConstMetric( ch <- prometheus.MustNewConstMetric(
resolverQueries, prometheus.CounterValue, float64(s.Counter), v.Name, s.Name, resolverQueries, prometheus.CounterValue, float64(s.Counter), v.Name, s.Name,
) )
} }
for _, s := range v.ResolverStats {
for _, s := range v.Resstat {
if desc, ok := resolverMetricStats[s.Name]; ok { if desc, ok := resolverMetricStats[s.Name]; ok {
ch <- prometheus.MustNewConstMetric( ch <- prometheus.MustNewConstMetric(
desc, prometheus.CounterValue, float64(s.Counter), v.Name, desc, prometheus.CounterValue, float64(s.Counter), v.Name,
@ -268,8 +234,7 @@ func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
) )
} }
} }
if buckets, count, err := histogram(v.ResolverStats); err == nil {
if buckets, count, err := histogram(v.Resstat); err == nil {
ch <- prometheus.MustNewConstHistogram( ch <- prometheus.MustNewConstHistogram(
resolverQueryDuration, count, math.NaN(), buckets, v.Name, resolverQueryDuration, count, math.NaN(), buckets, v.Name,
) )
@ -277,26 +242,27 @@ func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
log.Warn("Error parsing RTT:", err) log.Warn("Error parsing RTT:", err)
} }
} }
threadModel := stats.Taskmgr.ThreadModel
// TaskManager statistics.
threadModel := stats.TaskManager.ThreadModel
ch <- prometheus.MustNewConstMetric( ch <- prometheus.MustNewConstMetric(
tasksRunning, prometheus.GaugeValue, float64(threadModel.TasksRunning), tasksRunning, prometheus.GaugeValue, float64(threadModel.TasksRunning),
) )
ch <- prometheus.MustNewConstMetric( ch <- prometheus.MustNewConstMetric(
workerThreads, prometheus.GaugeValue, float64(threadModel.WorkerThreads), workerThreads, prometheus.GaugeValue, float64(threadModel.WorkerThreads),
) )
} }
func histogram(stats []Stat) (map[float64]uint64, uint64, error) { func histogram(stats []bind.Stat) (map[float64]uint64, uint64, error) {
buckets := map[float64]uint64{} buckets := map[float64]uint64{}
var count uint64 var count uint64
for _, s := range stats { for _, s := range stats {
if strings.HasPrefix(s.Name, qryRTT) { if strings.HasPrefix(s.Name, bind.QryRTT) {
b := math.Inf(0) b := math.Inf(0)
if !strings.HasSuffix(s.Name, "+") { if !strings.HasSuffix(s.Name, "+") {
var err error var err error
rrt := strings.TrimPrefix(s.Name, qryRTT) rrt := strings.TrimPrefix(s.Name, bind.QryRTT)
b, err = strconv.ParseFloat(rrt, 32) b, err = strconv.ParseFloat(rrt, 32)
if err != nil { if err != nil {
return buckets, 0, fmt.Errorf("could not parse RTT: %s", rrt) return buckets, 0, fmt.Errorf("could not parse RTT: %s", rrt)

View File

@ -14,17 +14,31 @@ import (
func TestBindExporter(t *testing.T) { func TestBindExporter(t *testing.T) {
tests := []string{ tests := []string{
`bind_up 1`, `bind_up 1`,
`bind_incoming_queries_total{type="A"} 100`, `bind_incoming_queries_total{type="A"} 128417`,
`bind_incoming_requests_total{name="QUERY"} 100`, `bind_incoming_requests_total{opcode="QUERY"} 37634`,
`bind_responses_total{result="Success"} 100`, `bind_responses_total{result="Success"} 29313`,
`bind_query_errors_total{error="Dropped"} 237`,
`bind_query_errors_total{error="Duplicate"} 216`,
`bind_query_errors_total{error="Failure"} 2950`,
`bind_resolver_cache_rrsets{type="A",view="_default"} 34324`,
`bind_resolver_queries_total{type="CNAME",view="_default"} 28`,
`bind_resolver_response_errors_total{error="FORMERR",view="_bind"} 0`, `bind_resolver_response_errors_total{error="FORMERR",view="_bind"} 0`,
`bind_resolver_response_errors_total{error="FORMERR",view="_default"} 0`, `bind_resolver_response_errors_total{error="FORMERR",view="_default"} 42906`,
`bind_resolver_response_errors_total{error="NXDOMAIN",view="_bind"} 0`, `bind_resolver_response_errors_total{error="NXDOMAIN",view="_bind"} 0`,
`bind_resolver_response_errors_total{error="NXDOMAIN",view="_default"} 0`, `bind_resolver_response_errors_total{error="NXDOMAIN",view="_default"} 16707`,
`bind_resolver_response_errors_total{error="OtherError",view="_bind"} 0`, `bind_resolver_response_errors_total{error="OtherError",view="_bind"} 0`,
`bind_resolver_response_errors_total{error="OtherError",view="_default"} 0`, `bind_resolver_response_errors_total{error="OtherError",view="_default"} 20660`,
`bind_resolver_response_errors_total{error="SERVFAIL",view="_bind"} 0`, `bind_resolver_response_errors_total{error="SERVFAIL",view="_bind"} 0`,
`bind_resolver_response_errors_total{error="SERVFAIL",view="_default"} 0`, `bind_resolver_response_errors_total{error="SERVFAIL",view="_default"} 7596`,
`bind_resolver_response_lame_total{view="_default"} 9108`,
`bind_resolver_query_duration_seconds_bucket{view="_default",le="0.01"} 38334`,
`bind_resolver_query_duration_seconds_bucket{view="_default",le="0.1"} 113122`,
`bind_resolver_query_duration_seconds_bucket{view="_default",le="0.5"} 182658`,
`bind_resolver_query_duration_seconds_bucket{view="_default",le="0.8"} 187375`,
`bind_resolver_query_duration_seconds_bucket{view="_default",le="1.6"} 188409`,
`bind_resolver_query_duration_seconds_bucket{view="_default",le="+Inf"} 227755`,
`bind_tasks_running 8`,
`bind_worker_threads 16`,
} }
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -39,7 +53,7 @@ func TestBindExporter(t *testing.T) {
for _, test := range tests { for _, test := range tests {
if !bytes.Contains(o, []byte(test)) { if !bytes.Contains(o, []byte(test)) {
t.Errorf("expected to find %q", string(test)) t.Errorf("expected to find %q in output:\n%s", string(test), string(o))
} }
} }
} }

File diff suppressed because it is too large Load Diff