Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BIND 9.x XML statistics input plugin #2046

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fb82ed1
Add basic skeleton of BIND nameserver stats input plugin
dswarbrick Nov 4, 2016
84e6530
Add BIND stats (XML v2) to accumulator
dswarbrick Nov 4, 2016
8852352
Factor out some common code
dswarbrick Nov 4, 2016
911783a
Add memory stats
dswarbrick Nov 4, 2016
d226acb
Condense XML stats v2 struct
dswarbrick Nov 6, 2016
e9472f7
Enhance XML stats v3 struct
dswarbrick Nov 6, 2016
70fa42f
Inject v3 stats into accumulator
dswarbrick Nov 6, 2016
f501563
Specify timeout in HTTP client, support optional detailed (per-contex…
dswarbrick Nov 8, 2016
ec16b29
Add per-view stats
dswarbrick Nov 8, 2016
775700c
Add per-view statistics for v3 stats
dswarbrick Nov 8, 2016
d72899c
Use float/int to compare stats version, drop strings requirement
dswarbrick Nov 8, 2016
a2ebfb1
Update README.md
dswarbrick Nov 8, 2016
8a534c0
Line wrapping in README.md (<= width 99)
dswarbrick Nov 8, 2016
93797f7
Use gauge instead of counter for memory stats
dswarbrick Nov 10, 2016
78cce09
Structure measurements so as to make "GROUP BY" queries feasible
dswarbrick Nov 14, 2016
5fa621c
Structure v2 measurements so as to make "GROUP BY" queries feasible
dswarbrick Nov 14, 2016
b99f359
Structure accumulate function sig
dswarbrick Nov 14, 2016
25f5c2f
Simplify measurement, use single generic bind_counter measurement wit…
dswarbrick Nov 14, 2016
b42fbf6
Simplify measurement, use single generic bind_counter measurement wit…
dswarbrick Nov 14, 2016
df2cc4b
Include url as a tag
dswarbrick Nov 14, 2016
6f266bf
Update README.md
dswarbrick Nov 14, 2016
7c2153e
Merge remote-tracking branch 'upstream/master'
dswarbrick Nov 14, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions plugins/inputs/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
_ "github.com/influxdata/telegraf/plugins/inputs/aerospike"
_ "github.com/influxdata/telegraf/plugins/inputs/apache"
_ "github.com/influxdata/telegraf/plugins/inputs/bcache"
_ "github.com/influxdata/telegraf/plugins/inputs/bind"
_ "github.com/influxdata/telegraf/plugins/inputs/cassandra"
_ "github.com/influxdata/telegraf/plugins/inputs/ceph"
_ "github.com/influxdata/telegraf/plugins/inputs/cgroup"
Expand Down
68 changes: 68 additions & 0 deletions plugins/inputs/bind/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# BIND 9 Nameserver Statistics Input Plugin

This plugin decodes the XML statistics provided by BIND 9 nameservers. Version 2 statistics
(BIND 9.6+) and version 3 statistics (BIND 9.10+) are supported.

JSON statistics are not currently supported.

### Configuration:

- **urls** []string: List of BIND XML statistics URLs to collect from. Default is
"http://localhost:8053/".
- **gather_memory_contexts** bool: Report per-context memory statistics.
- **gather_views** bool: Report per-view query statistics.

#### Configuration of BIND Daemon

Add the following to your named.conf if running Telegraf on the same host as the BIND daemon:
```
statistics-channels {
inet 127.0.0.1 port 8053;
};
```

Alternatively, specify a wildcard address (e.g., 0.0.0.0) or specific IP address of an interface to
configure the BIND daemon to listen on that address. Note that you should secure the statistics
channel with an ACL if it is publicly reachable. Consult the BIND Administrator Reference Manual
for more information.

### Measurements & Fields:

- bind_counter
- value
- bind_memory
- TotalUse
- InUse
- BlockSize
- ContextSize
- Lost
- bind_memory_context
- Total
- InUse

### Tags:

- All measurements
- url
- bind_counter
- type
- name
- view (optional)
- bind_memory_context
- id
- name

### Sample Queries:

These are some useful queries (to generate dashboards or other) to run against data from this
plugin:

```
SELECT derivative(mean("value"), 1s) FROM bind_counter \
WHERE host = 'example.com' AND type = 'qtype' AND time > now() - 6h \
GROUP BY time(1m), name
```

### Example Output:

TBC
108 changes: 108 additions & 0 deletions plugins/inputs/bind/bind.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package bind

import (
"bufio"
"encoding/xml"
"fmt"
"net/http"
"net/url"
"time"

"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/plugins/inputs"
)

type Bind struct {
Urls []string
GatherMemoryContexts bool
GatherViews bool
}

var sampleConfig = `
## An array of BIND XML statistics URI to gather stats.
## Default is "http://localhost:8053/".
urls = ["http://localhost:8053/"]
gather_memory_contexts = false
gather_views = false
`

var client = &http.Client{
Timeout: time.Duration(4 * time.Second),
}

func (b *Bind) Description() string {
return "Read BIND nameserver XML statistics"
}

func (b *Bind) SampleConfig() string {
return sampleConfig
}

func (b *Bind) Gather(acc telegraf.Accumulator) error {
var outerr error
var errch = make(chan error)

for _, u := range b.Urls {
addr, err := url.Parse(u)
if err != nil {
return fmt.Errorf("Unable to parse address '%s': %s", u, err)
}

go func(addr *url.URL) {
errch <- b.GatherUrl(addr, acc)
}(addr)
}

// Drain channel, waiting for all requests to finish and save last error
for range b.Urls {
if err := <-errch; err != nil {
outerr = err
}
}

return outerr
}

func (b *Bind) GatherUrl(addr *url.URL, acc telegraf.Accumulator) error {
resp, err := client.Get(addr.String())
if err != nil {
return fmt.Errorf("error making HTTP request to %s: %s", addr.String(), err)
}

defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("%s returned HTTP status %s", addr.String(), resp.Status)
}

// Wrap reader in a buffered reader so that we can peek ahead to determine schema version
br := bufio.NewReader(resp.Body)

if p, err := br.Peek(256); err != nil {
return fmt.Errorf("Unable to peek ahead in stream to determine statistics version: %s", err)
} else {
var xmlRoot struct {
XMLName xml.Name
Version float64 `xml:"version,attr"`
}

err := xml.Unmarshal(p, &xmlRoot)

if err != nil {
// We expect an EOF error since we only fed the decoder a small fragment
if _, ok := err.(*xml.SyntaxError); !ok {
return fmt.Errorf("XML syntax error: %s", err)
}
}

if (xmlRoot.XMLName.Local == "statistics") && (int(xmlRoot.Version) == 3) {
return b.readStatsV3(br, acc, addr.Host)
} else {
return b.readStatsV2(br, acc, addr.Host)
}
}
}

func init() {
inputs.Add("bind", func() telegraf.Input { return &Bind{} })
}
141 changes: 141 additions & 0 deletions plugins/inputs/bind/xml_stats_v2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package bind

import (
"encoding/xml"
"fmt"
"io"

"github.com/influxdata/telegraf"
)

type v2Root struct {
XMLName xml.Name
Version string `xml:"version,attr"`
Statistics v2Statistics `xml:"bind>statistics"`
}

// Omitted branches: socketmgr, taskmgr
type v2Statistics struct {
Version string `xml:"version,attr"`
Views []struct {
// Omitted branches: zones
Name string `xml:"name"`
RdTypes []v2Counter `xml:"rdtype"`
ResStats []v2Counter `xml:"resstat"`
Caches []struct {
Name string `xml:"name,attr"`
RRSets []v2Counter `xml:"rrset"`
} `xml:"cache"`
} `xml:"views>view"`
Server struct {
OpCodes []v2Counter `xml:"requests>opcode"`
RdTypes []v2Counter `xml:"queries-in>rdtype"`
NSStats []v2Counter `xml:"nsstat"`
ZoneStats []v2Counter `xml:"zonestat"`
ResStats []v2Counter `xml:"resstat"`
SockStats []v2Counter `xml:"sockstat"`
} `xml:"server"`
Memory struct {
Contexts []struct {
// Omitted nodes: references, maxinuse, blocksize, pools, hiwater, lowater
Id string `xml:"id"`
Name string `xml:"name"`
Total int `xml:"total"`
InUse int `xml:"inuse"`
} `xml:"contexts>context"`
Summary struct {
TotalUse int
InUse int
BlockSize int
ContextSize int
Lost int
} `xml:"summary"`
} `xml:"memory"`
}

// BIND statistics v2 counter struct used throughout
type v2Counter struct {
Name string `xml:"name"`
Value int `xml:"counter"`
}

// addCounter adds a v2Counter array to a Telegraf Accumulator, with the specified tags
func addCounter(acc telegraf.Accumulator, tags map[string]string, stats []v2Counter) {
fields := make(map[string]interface{})

for _, c := range stats {
tags["name"] = c.Name
fields["value"] = c.Value
acc.AddCounter("bind_counter", fields, tags)
}
}

// readStatsV2 decodes a BIND9 XML statistics version 2 document
func (b *Bind) readStatsV2(r io.Reader, acc telegraf.Accumulator, url string) error {
var stats v2Root

if err := xml.NewDecoder(r).Decode(&stats); err != nil {
return fmt.Errorf("Unable to decode XML document: %s", err)
}

tags := map[string]string{"url": url}

// Opcodes
tags["type"] = "opcode"
addCounter(acc, tags, stats.Statistics.Server.OpCodes)

// Query RDATA types
tags["type"] = "qtype"
addCounter(acc, tags, stats.Statistics.Server.RdTypes)

// Nameserver stats
tags["type"] = "nsstat"
addCounter(acc, tags, stats.Statistics.Server.NSStats)

// Zone stats
tags["type"] = "zonestat"
addCounter(acc, tags, stats.Statistics.Server.ZoneStats)

// Socket statistics
tags["type"] = "sockstat"
addCounter(acc, tags, stats.Statistics.Server.SockStats)

// Memory stats
fields := map[string]interface{}{
"TotalUse": stats.Statistics.Memory.Summary.TotalUse,
"InUse": stats.Statistics.Memory.Summary.InUse,
"BlockSize": stats.Statistics.Memory.Summary.BlockSize,
"ContextSize": stats.Statistics.Memory.Summary.ContextSize,
"Lost": stats.Statistics.Memory.Summary.Lost,
}
acc.AddGauge("bind_memory", fields, map[string]string{"url": url})

// Detailed, per-context memory stats
if b.GatherMemoryContexts {
tags := map[string]string{"url": url}

for _, c := range stats.Statistics.Memory.Contexts {
tags["id"] = c.Id
tags["name"] = c.Name
fields := map[string]interface{}{"Total": c.Total, "InUse": c.InUse}
acc.AddGauge("bind_memory_context", fields, tags)
}
}

// Detailed, per-view stats
if b.GatherViews {
for _, v := range stats.Statistics.Views {
tags := map[string]string{"url": url, "view": v.Name}

// Query RDATA types
tags["type"] = "qtype"
addCounter(acc, tags, v.RdTypes)

// Resolver stats
tags["type"] = "resstats"
addCounter(acc, tags, v.ResStats)
}
}

return nil
}
Loading