From 2ad32425590a7d80146f33058451d0d7812c8acb Mon Sep 17 00:00:00 2001 From: Marcin Jasion Date: Sat, 13 Feb 2016 19:00:42 +0100 Subject: [PATCH] Dns query input plugin --- Godeps | 1 + plugins/inputs/all/all.go | 1 + plugins/inputs/dns/README.md | 51 ++++++++++++ plugins/inputs/dns/dns.go | 143 +++++++++++++++++++++++++++++++++ plugins/inputs/dns/dns_test.go | 127 +++++++++++++++++++++++++++++ 5 files changed, 323 insertions(+) create mode 100644 plugins/inputs/dns/README.md create mode 100644 plugins/inputs/dns/dns.go create mode 100644 plugins/inputs/dns/dns_test.go diff --git a/Godeps b/Godeps index d0d2194c6..d2ac1857f 100644 --- a/Godeps +++ b/Godeps @@ -50,3 +50,4 @@ gopkg.in/dancannon/gorethink.v1 6f088135ff288deb9d5546f4c71919207f891a70 gopkg.in/fatih/pool.v2 cba550ebf9bce999a02e963296d4bc7a486cb715 gopkg.in/mgo.v2 03c9f3ee4c14c8e51ee521a6a7d0425658dd6f64 gopkg.in/yaml.v2 f7716cbe52baa25d2e9b0d0da546fcf909fc16b4 +github.com/miekg/dns e0d84d97e59bcb6561eae269c4e94d25b66822cb \ No newline at end of file diff --git a/plugins/inputs/all/all.go b/plugins/inputs/all/all.go index 79deb7c99..e6cf6b377 100644 --- a/plugins/inputs/all/all.go +++ b/plugins/inputs/all/all.go @@ -6,6 +6,7 @@ import ( _ "github.com/influxdata/telegraf/plugins/inputs/bcache" _ "github.com/influxdata/telegraf/plugins/inputs/couchdb" _ "github.com/influxdata/telegraf/plugins/inputs/disque" + _ "github.com/influxdata/telegraf/plugins/inputs/dns" _ "github.com/influxdata/telegraf/plugins/inputs/docker" _ "github.com/influxdata/telegraf/plugins/inputs/dovecot" _ "github.com/influxdata/telegraf/plugins/inputs/elasticsearch" diff --git a/plugins/inputs/dns/README.md b/plugins/inputs/dns/README.md new file mode 100644 index 000000000..eb2f99d6c --- /dev/null +++ b/plugins/inputs/dns/README.md @@ -0,0 +1,51 @@ +# DNS Input Plugin + +The DNS plugin gathers dns query times in miliseconds - like [Dig](https://en.wikipedia.org/wiki/Dig_\(command\)) + +### Configuration: + +``` +# Sample Config: +[[inputs.dns]] + ### Domains or subdomains to query + domains = ["mjasion.pl"] # required + + ### servers to query + servers = ["8.8.8.8"] # required + + ### Query record type. Posible values: A, CNAME, MX, TXT, NS. Default is "A" + recordType = "A" # optional + + ### Dns server port. 53 is default + port = 53 # optional + + ### Query timeout in seconds. Default is 2 seconds + timeout = 2 # optional +``` + +For querying more than one record type make: + +``` +[[inputs.dns]] + domains = ["mjasion.pl"] + servers = ["8.8.8.8", "8.8.4.4"] + recordType = "A" + +[[inputs.dns]] + domains = ["mjasion.pl"] + servers = ["8.8.8.8", "8.8.4.4"] + recordType = "MX" +``` + +### Tags: + +- server +- domain +- recordType + +### Example output: + +``` +./telegraf -config telegraf.conf -test -input-filter dns -test +> dns,domain=mjasion.pl,recordType=A,server=8.8.8.8 value=25.236181 1455452083165126877 +``` diff --git a/plugins/inputs/dns/dns.go b/plugins/inputs/dns/dns.go new file mode 100644 index 000000000..d8440f104 --- /dev/null +++ b/plugins/inputs/dns/dns.go @@ -0,0 +1,143 @@ +package dns + +import ( + "errors" + "fmt" + "github.com/influxdata/telegraf" + "github.com/influxdata/telegraf/plugins/inputs" + "github.com/miekg/dns" + "net" + "strconv" + "time" +) + +type Dns struct { + // Domains or subdomains to query + Domains []string + + // Server to query + Servers []string + + // Record type + RecordType string + + // DNS server port number + Port int + + // Dns query timeout in seconds. 0 means no timeout + Timeout int +} + +var sampleConfig = ` + ### Domains or subdomains to query + domains = ["mjasion.pl"] # required + + ### servers to query + servers = ["8.8.8.8"] # required + + ### Query record type. Posible values: A, CNAME, MX, TXT, NS. Default is "A" + recordType = "A" # optional + + ### Dns server port. 53 is default + port = 53 # optional + + ### Query timeout in seconds. Default is 2 seconds + timeout = 2 # optional +` + +func (d *Dns) SampleConfig() string { + return sampleConfig +} + +func (d *Dns) Description() string { + return "Query given DNS server and gives statistics" +} +func (d *Dns) Gather(acc telegraf.Accumulator) error { + d.setDefaultValues() + for _, domain := range d.Domains { + for _, server := range d.Servers { + dnsQueryTime, err := d.getDnsQueryTime(domain, server) + if err != nil { + return err + } + tags := map[string]string{ + "server": server, + "domain": domain, + "recordType": d.RecordType, + } + + acc.Add("dns", dnsQueryTime, tags) + } + } + + return nil +} + +func (d *Dns) setDefaultValues() { + if len(d.RecordType) == 0 { + d.RecordType = "A" + } + if d.Port == 0 { + d.Port = 53 + } + if d.Timeout == 0 { + d.Timeout = 2 + } +} + +func (d *Dns) getDnsQueryTime(domain string, server string) (float64, error) { + dnsQueryTime := float64(0) + + c := new(dns.Client) + c.ReadTimeout = time.Duration(d.Timeout) * time.Second + + m := new(dns.Msg) + recordType, err := d.parseRecordType() + if err != nil { + return dnsQueryTime, err + } + m.SetQuestion(dns.Fqdn(domain), recordType) + m.RecursionDesired = true + + start_time := time.Now() + r, _, err := c.Exchange(m, net.JoinHostPort(server, strconv.Itoa(d.Port))) + queryDuration := time.Since(start_time) + + if err != nil { + return dnsQueryTime, err + } + if r.Rcode != dns.RcodeSuccess { + return dnsQueryTime, errors.New(fmt.Sprintf("Invalid answer name %s after %s query for %s\n", domain, d.RecordType, domain)) + } + + dnsQueryTime = float64(queryDuration.Nanoseconds()) / 1e6 + return dnsQueryTime, nil +} + +func (d *Dns) parseRecordType() (uint16, error) { + var recordType uint16 + var error error + + switch d.RecordType { + case "A": + recordType = dns.TypeA + case "CNAME": + recordType = dns.TypeCNAME + case "MX": + recordType = dns.TypeMX + case "NS": + recordType = dns.TypeNS + case "TXT": + recordType = dns.TypeTXT + default: + error = errors.New(fmt.Sprintf("Record type %s not recognized", d.RecordType)) + } + + return recordType, error +} + +func init() { + inputs.Add("dns", func() telegraf.Input { + return &Dns{} + }) +} diff --git a/plugins/inputs/dns/dns_test.go b/plugins/inputs/dns/dns_test.go new file mode 100644 index 000000000..038ff8e40 --- /dev/null +++ b/plugins/inputs/dns/dns_test.go @@ -0,0 +1,127 @@ +package dns + +import ( + "github.com/influxdata/telegraf/testutil" + "github.com/miekg/dns" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +var servers = []string{"8.8.8.8"} +var domains = []string{"mjasion.pl"} + +func TestGathering(t *testing.T) { + var dnsConfig = Dns{ + Servers: servers, + Domains: domains, + } + var acc testutil.Accumulator + + dnsConfig.Gather(&acc) + metric, _ := acc.Get("dns") + queryTime, _ := metric.Fields["value"].(float64) + + assert.NotEqual(t, 0, queryTime) +} + +func TestGatheringMxRecord(t *testing.T) { + var dnsConfig = Dns{ + Servers: servers, + Domains: domains, + } + var acc testutil.Accumulator + dnsConfig.RecordType = "MX" + + dnsConfig.Gather(&acc) + metric, _ := acc.Get("dns") + queryTime, _ := metric.Fields["value"].(float64) + + assert.NotEqual(t, 0, queryTime) +} + +func TestMetricContainsServerAndDomainAndRecordTypeTags(t *testing.T) { + var dnsConfig = Dns{ + Servers: servers, + Domains: domains, + } + var acc testutil.Accumulator + tags := map[string]string{ + "server": "8.8.8.8", + "domain": "mjasion.pl", + "recordType": "A", + } + fields := map[string]interface{}{} + + dnsConfig.Gather(&acc) + metric, _ := acc.Get("dns") + queryTime, _ := metric.Fields["value"].(float64) + + fields["value"] = queryTime + acc.AssertContainsTaggedFields(t, "dns", fields, tags) +} + +func TestGatheringTimeout(t *testing.T) { + var dnsConfig = Dns{ + Servers: servers, + Domains: domains, + } + var acc testutil.Accumulator + dnsConfig.Port = 60054 + dnsConfig.Timeout = 1 + var err error + + channel := make(chan error, 1) + go func() { + channel <- dnsConfig.Gather(&acc) + }() + select { + case res := <-channel: + err = res + case <-time.After(time.Second * 2): + err = nil + } + + assert.Error(t, err) + assert.Contains(t, err.Error(), "i/o timeout") +} + +func TestSettingDefaultValues(t *testing.T) { + dnsConfig := Dns{} + + dnsConfig.setDefaultValues() + + assert.Equal(t, "A", dnsConfig.RecordType, "Default record type not equal 'A'") + assert.Equal(t, 53, dnsConfig.Port, "Default port number not equal 53") + assert.Equal(t, 2, dnsConfig.Timeout, "Default timeout not equal 2") +} + +func TestRecordTypeParser(t *testing.T) { + var dnsConfig = Dns{} + var recordType uint16 + var err error + + dnsConfig.RecordType = "A" + recordType, err = dnsConfig.parseRecordType() + assert.Equal(t, dns.TypeA, recordType) + + dnsConfig.RecordType = "CNAME" + recordType, err = dnsConfig.parseRecordType() + assert.Equal(t, dns.TypeCNAME, recordType) + + dnsConfig.RecordType = "MX" + recordType, err = dnsConfig.parseRecordType() + assert.Equal(t, dns.TypeMX, recordType) + + dnsConfig.RecordType = "NS" + recordType, err = dnsConfig.parseRecordType() + assert.Equal(t, dns.TypeNS, recordType) + + dnsConfig.RecordType = "TXT" + recordType, err = dnsConfig.parseRecordType() + assert.Equal(t, dns.TypeTXT, recordType) + + dnsConfig.RecordType = "nil" + recordType, err = dnsConfig.parseRecordType() + assert.Error(t, err) +}