summaryrefslogtreecommitdiffstats
path: root/src/go/collectors/go.d.plugin/modules/dnsquery
diff options
context:
space:
mode:
Diffstat (limited to '')
l---------src/go/collectors/go.d.plugin/modules/dnsquery/README.md1
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsquery/charts.go64
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsquery/collect.go73
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsquery/config_schema.json133
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsquery/dnsquery.go121
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsquery/dnsquery_test.go242
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsquery/init.go98
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsquery/integrations/dns_query.md181
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsquery/metadata.yaml142
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsquery/testdata/config.json16
-rw-r--r--src/go/collectors/go.d.plugin/modules/dnsquery/testdata/config.yaml11
11 files changed, 1082 insertions, 0 deletions
diff --git a/src/go/collectors/go.d.plugin/modules/dnsquery/README.md b/src/go/collectors/go.d.plugin/modules/dnsquery/README.md
new file mode 120000
index 000000000..c5baa8254
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsquery/README.md
@@ -0,0 +1 @@
+integrations/dns_query.md \ No newline at end of file
diff --git a/src/go/collectors/go.d.plugin/modules/dnsquery/charts.go b/src/go/collectors/go.d.plugin/modules/dnsquery/charts.go
new file mode 100644
index 000000000..b229d89eb
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsquery/charts.go
@@ -0,0 +1,64 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package dnsquery
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
+)
+
+const (
+ prioDNSQueryStatus = module.Priority + iota
+ prioDNSQueryTime
+)
+
+var (
+ dnsChartsTmpl = module.Charts{
+ dnsQueryStatusChartTmpl.Copy(),
+ dnsQueryTimeChartTmpl.Copy(),
+ }
+ dnsQueryStatusChartTmpl = module.Chart{
+ ID: "server_%s_record_%s_query_status",
+ Title: "DNS Query Status",
+ Units: "status",
+ Fam: "query status",
+ Ctx: "dns_query.query_status",
+ Priority: prioDNSQueryStatus,
+ Dims: module.Dims{
+ {ID: "server_%s_record_%s_query_status_success", Name: "success"},
+ {ID: "server_%s_record_%s_query_status_network_error", Name: "network_error"},
+ {ID: "server_%s_record_%s_query_status_dns_error", Name: "dns_error"},
+ },
+ }
+ dnsQueryTimeChartTmpl = module.Chart{
+ ID: "server_%s_record_%s_query_time",
+ Title: "DNS Query Time",
+ Units: "seconds",
+ Fam: "query time",
+ Ctx: "dns_query.query_time",
+ Priority: prioDNSQueryTime,
+ Dims: module.Dims{
+ {ID: "server_%s_record_%s_query_time", Name: "query_time", Div: 1e9},
+ },
+ }
+)
+
+func newDNSServerCharts(server, network, rtype string) *module.Charts {
+ charts := dnsChartsTmpl.Copy()
+
+ for _, chart := range *charts {
+ chart.ID = fmt.Sprintf(chart.ID, strings.ReplaceAll(server, ".", "_"), rtype)
+ chart.Labels = []module.Label{
+ {Key: "server", Value: server},
+ {Key: "network", Value: network},
+ {Key: "record_type", Value: rtype},
+ }
+ for _, d := range chart.Dims {
+ d.ID = fmt.Sprintf(d.ID, server, rtype)
+ }
+ }
+
+ return charts
+}
diff --git a/src/go/collectors/go.d.plugin/modules/dnsquery/collect.go b/src/go/collectors/go.d.plugin/modules/dnsquery/collect.go
new file mode 100644
index 000000000..a98e37cad
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsquery/collect.go
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package dnsquery
+
+import (
+ "math/rand"
+ "net"
+ "strconv"
+ "sync"
+ "time"
+
+ "github.com/miekg/dns"
+)
+
+func (d *DNSQuery) collect() (map[string]int64, error) {
+ if d.dnsClient == nil {
+ d.dnsClient = d.newDNSClient(d.Network, d.Timeout.Duration())
+ }
+
+ mx := make(map[string]int64)
+ domain := randomDomain(d.Domains)
+ d.Debugf("current domain : %s", domain)
+
+ var wg sync.WaitGroup
+ var mux sync.RWMutex
+ for _, srv := range d.Servers {
+ for rtypeName, rtype := range d.recordTypes {
+ wg.Add(1)
+ go func(srv, rtypeName string, rtype uint16, wg *sync.WaitGroup) {
+ defer wg.Done()
+
+ msg := new(dns.Msg)
+ msg.SetQuestion(dns.Fqdn(domain), rtype)
+ address := net.JoinHostPort(srv, strconv.Itoa(d.Port))
+
+ resp, rtt, err := d.dnsClient.Exchange(msg, address)
+
+ mux.Lock()
+ defer mux.Unlock()
+
+ px := "server_" + srv + "_record_" + rtypeName + "_"
+
+ mx[px+"query_status_success"] = 0
+ mx[px+"query_status_network_error"] = 0
+ mx[px+"query_status_dns_error"] = 0
+
+ if err != nil {
+ d.Debugf("error on querying %s after %s query for %s : %s", srv, rtypeName, domain, err)
+ mx[px+"query_status_network_error"] = 1
+ return
+ }
+
+ if resp != nil && resp.Rcode != dns.RcodeSuccess {
+ d.Debugf("invalid answer from %s after %s query for %s (rcode %d)", srv, rtypeName, domain, resp.Rcode)
+ mx[px+"query_status_dns_error"] = 1
+ } else {
+ mx[px+"query_status_success"] = 1
+ }
+ mx["server_"+srv+"_record_"+rtypeName+"_query_time"] = rtt.Nanoseconds()
+
+ }(srv, rtypeName, rtype, &wg)
+ }
+ }
+ wg.Wait()
+
+ return mx, nil
+}
+
+func randomDomain(domains []string) string {
+ src := rand.NewSource(time.Now().UnixNano())
+ r := rand.New(src)
+ return domains[r.Intn(len(domains))]
+}
diff --git a/src/go/collectors/go.d.plugin/modules/dnsquery/config_schema.json b/src/go/collectors/go.d.plugin/modules/dnsquery/config_schema.json
new file mode 100644
index 000000000..cfa6f3a14
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsquery/config_schema.json
@@ -0,0 +1,133 @@
+{
+ "jsonSchema": {
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "DNS query collector configuration.",
+ "type": "object",
+ "properties": {
+ "update_every": {
+ "title": "Update every",
+ "description": "Data collection interval, measured in seconds.",
+ "type": "integer",
+ "minimum": 1,
+ "default": 5
+ },
+ "timeout": {
+ "title": "Timeout",
+ "description": "Timeout for DNS queries, in seconds.",
+ "type": "number",
+ "default": 2
+ },
+ "network": {
+ "title": "Protocol",
+ "description": "Network protocol for DNS queries.",
+ "type": "string",
+ "enum": [
+ "udp",
+ "tcp",
+ "tcp-tls"
+ ],
+ "default": "udp"
+ },
+ "port": {
+ "title": "Port",
+ "description": "Port number for DNS servers.",
+ "type": "integer",
+ "default": 53
+ },
+ "record_types": {
+ "title": "Record types",
+ "description": "Types of DNS records to query for each server.",
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "type": "string",
+ "enum": [
+ "A",
+ "AAAA",
+ "ANY",
+ "CNAME",
+ "MX",
+ "NS",
+ "PTR",
+ "SOA",
+ "SPF",
+ "SRV",
+ "TXT"
+ ],
+ "default": "A"
+ },
+ "default": [
+ "A"
+ ],
+ "uniqueItems": true
+ },
+ "servers": {
+ "title": "Servers",
+ "description": "List of DNS servers to query.",
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "title": "DNS server",
+ "description": "IP address or hostname of the DNS server.",
+ "type": "string"
+ },
+ "default": [
+ "8.8.8.8"
+ ],
+ "uniqueItems": true,
+ "minItems": 1
+ },
+ "domains": {
+ "title": "Domains",
+ "description": "List of domains or subdomains to query. A random domain will be selected from this list at each iteration.",
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "title": "Domain",
+ "type": "string"
+ },
+ "default": [
+ "google.com",
+ "github.com"
+ ],
+ "uniqueItems": true,
+ "minItems": 1
+ }
+ },
+ "required": [
+ "domains",
+ "servers",
+ "network"
+ ],
+ "additionalProperties": false,
+ "patternProperties": {
+ "^name$": {}
+ }
+ },
+ "uiSchema": {
+ "uiOptions": {
+ "fullPage": true
+ },
+ "timeout": {
+ "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)."
+ },
+ "network": {
+ "ui:widget": "radio",
+ "ui:options": {
+ "inline": true
+ }
+ },
+ "servers": {
+ "ui:listFlavour": "list"
+ },
+ "domains": {
+ "ui:listFlavour": "list"
+ }
+ }
+}
diff --git a/src/go/collectors/go.d.plugin/modules/dnsquery/dnsquery.go b/src/go/collectors/go.d.plugin/modules/dnsquery/dnsquery.go
new file mode 100644
index 000000000..5a0df7adc
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsquery/dnsquery.go
@@ -0,0 +1,121 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package dnsquery
+
+import (
+ _ "embed"
+ "time"
+
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
+ "github.com/netdata/netdata/go/go.d.plugin/pkg/web"
+
+ "github.com/miekg/dns"
+)
+
+//go:embed "config_schema.json"
+var configSchema string
+
+func init() {
+ module.Register("dns_query", module.Creator{
+ JobConfigSchema: configSchema,
+ Defaults: module.Defaults{
+ UpdateEvery: 5,
+ },
+ Create: func() module.Module { return New() },
+ Config: func() any { return &Config{} },
+ })
+}
+
+func New() *DNSQuery {
+ return &DNSQuery{
+ Config: Config{
+ Timeout: web.Duration(time.Second * 2),
+ Network: "udp",
+ RecordTypes: []string{"A"},
+ Port: 53,
+ },
+ newDNSClient: func(network string, timeout time.Duration) dnsClient {
+ return &dns.Client{
+ Net: network,
+ ReadTimeout: timeout,
+ }
+ },
+ }
+}
+
+type Config struct {
+ UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
+ Timeout web.Duration `yaml:"timeout,omitempty" json:"timeout"`
+ Domains []string `yaml:"domains" json:"domains"`
+ Servers []string `yaml:"servers" json:"servers"`
+ Network string `yaml:"network,omitempty" json:"network"`
+ RecordType string `yaml:"record_type,omitempty" json:"record_type"`
+ RecordTypes []string `yaml:"record_types,omitempty" json:"record_types"`
+ Port int `yaml:"port,omitempty" json:"port"`
+}
+
+type (
+ DNSQuery struct {
+ module.Base
+ Config `yaml:",inline" json:""`
+
+ charts *module.Charts
+
+ dnsClient dnsClient
+ newDNSClient func(network string, duration time.Duration) dnsClient
+
+ recordTypes map[string]uint16
+ }
+ dnsClient interface {
+ Exchange(msg *dns.Msg, address string) (response *dns.Msg, rtt time.Duration, err error)
+ }
+)
+
+func (d *DNSQuery) Configuration() any {
+ return d.Config
+}
+
+func (d *DNSQuery) Init() error {
+ if err := d.verifyConfig(); err != nil {
+ d.Errorf("config validation: %v", err)
+ return err
+ }
+
+ rt, err := d.initRecordTypes()
+ if err != nil {
+ d.Errorf("init record type: %v", err)
+ return err
+ }
+ d.recordTypes = rt
+
+ charts, err := d.initCharts()
+ if err != nil {
+ d.Errorf("init charts: %v", err)
+ return err
+ }
+ d.charts = charts
+
+ return nil
+}
+
+func (d *DNSQuery) Check() error {
+ return nil
+}
+
+func (d *DNSQuery) Charts() *module.Charts {
+ return d.charts
+}
+
+func (d *DNSQuery) Collect() map[string]int64 {
+ mx, err := d.collect()
+ if err != nil {
+ d.Error(err)
+ }
+
+ if len(mx) == 0 {
+ return nil
+ }
+ return mx
+}
+
+func (d *DNSQuery) Cleanup() {}
diff --git a/src/go/collectors/go.d.plugin/modules/dnsquery/dnsquery_test.go b/src/go/collectors/go.d.plugin/modules/dnsquery/dnsquery_test.go
new file mode 100644
index 000000000..9842e54fd
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsquery/dnsquery_test.go
@@ -0,0 +1,242 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package dnsquery
+
+import (
+ "errors"
+ "os"
+ "testing"
+ "time"
+
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
+ "github.com/netdata/netdata/go/go.d.plugin/pkg/web"
+
+ "github.com/miekg/dns"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+var (
+ dataConfigJSON, _ = os.ReadFile("testdata/config.json")
+ dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")
+)
+
+func Test_testDataIsValid(t *testing.T) {
+ for name, data := range map[string][]byte{
+ "dataConfigJSON": dataConfigJSON,
+ "dataConfigYAML": dataConfigYAML,
+ } {
+ require.NotNil(t, data, name)
+ }
+}
+
+func TestDNSQuery_ConfigurationSerialize(t *testing.T) {
+ module.TestConfigurationSerialize(t, &DNSQuery{}, dataConfigJSON, dataConfigYAML)
+}
+
+func TestDNSQuery_Init(t *testing.T) {
+ tests := map[string]struct {
+ wantFail bool
+ config Config
+ }{
+ "success when all set": {
+ wantFail: false,
+ config: Config{
+ Domains: []string{"example.com"},
+ Servers: []string{"192.0.2.0"},
+ Network: "udp",
+ RecordTypes: []string{"A"},
+ Port: 53,
+ Timeout: web.Duration(time.Second),
+ },
+ },
+ "success when using deprecated record_type": {
+ wantFail: false,
+ config: Config{
+ Domains: []string{"example.com"},
+ Servers: []string{"192.0.2.0"},
+ Network: "udp",
+ RecordType: "A",
+ Port: 53,
+ Timeout: web.Duration(time.Second),
+ },
+ },
+ "fail with default": {
+ wantFail: true,
+ config: New().Config,
+ },
+ "fail when domains not set": {
+ wantFail: true,
+ config: Config{
+ Domains: nil,
+ Servers: []string{"192.0.2.0"},
+ Network: "udp",
+ RecordTypes: []string{"A"},
+ Port: 53,
+ Timeout: web.Duration(time.Second),
+ },
+ },
+ "fail when servers not set": {
+ wantFail: true,
+ config: Config{
+ Domains: []string{"example.com"},
+ Servers: nil,
+ Network: "udp",
+ RecordTypes: []string{"A"},
+ Port: 53,
+ Timeout: web.Duration(time.Second),
+ },
+ },
+ "fail when network is invalid": {
+ wantFail: true,
+ config: Config{
+ Domains: []string{"example.com"},
+ Servers: []string{"192.0.2.0"},
+ Network: "gcp",
+ RecordTypes: []string{"A"},
+ Port: 53,
+ Timeout: web.Duration(time.Second),
+ },
+ },
+ "fail when record_type is invalid": {
+ wantFail: true,
+ config: Config{
+ Domains: []string{"example.com"},
+ Servers: []string{"192.0.2.0"},
+ Network: "udp",
+ RecordTypes: []string{"B"},
+ Port: 53,
+ Timeout: web.Duration(time.Second),
+ },
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ dq := New()
+ dq.Config = test.config
+
+ if test.wantFail {
+ assert.Error(t, dq.Init())
+ } else {
+ assert.NoError(t, dq.Init())
+ }
+ })
+ }
+}
+
+func TestDNSQuery_Check(t *testing.T) {
+ tests := map[string]struct {
+ wantFail bool
+ prepare func() *DNSQuery
+ }{
+ "success when DNS query successful": {
+ wantFail: false,
+ prepare: caseDNSClientOK,
+ },
+ "success when DNS query returns an error": {
+ wantFail: false,
+ prepare: caseDNSClientErr,
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ dq := test.prepare()
+
+ require.NoError(t, dq.Init())
+
+ if test.wantFail {
+ assert.Error(t, dq.Check())
+ } else {
+ assert.NoError(t, dq.Check())
+ }
+ })
+ }
+}
+
+func TestDNSQuery_Charts(t *testing.T) {
+ dq := New()
+
+ dq.Domains = []string{"google.com"}
+ dq.Servers = []string{"192.0.2.0", "192.0.2.1"}
+ require.NoError(t, dq.Init())
+
+ assert.NotNil(t, dq.Charts())
+ assert.Len(t, *dq.Charts(), len(dnsChartsTmpl)*len(dq.Servers))
+}
+
+func TestDNSQuery_Collect(t *testing.T) {
+ tests := map[string]struct {
+ prepare func() *DNSQuery
+ wantMetrics map[string]int64
+ }{
+ "success when DNS query successful": {
+ prepare: caseDNSClientOK,
+ wantMetrics: map[string]int64{
+ "server_192.0.2.0_record_A_query_status_dns_error": 0,
+ "server_192.0.2.0_record_A_query_status_network_error": 0,
+ "server_192.0.2.0_record_A_query_status_success": 1,
+ "server_192.0.2.0_record_A_query_time": 1000000000,
+ "server_192.0.2.1_record_A_query_status_dns_error": 0,
+ "server_192.0.2.1_record_A_query_status_network_error": 0,
+ "server_192.0.2.1_record_A_query_status_success": 1,
+ "server_192.0.2.1_record_A_query_time": 1000000000,
+ },
+ },
+ "fail when DNS query returns an error": {
+ prepare: caseDNSClientErr,
+ wantMetrics: map[string]int64{
+ "server_192.0.2.0_record_A_query_status_dns_error": 0,
+ "server_192.0.2.0_record_A_query_status_network_error": 1,
+ "server_192.0.2.0_record_A_query_status_success": 0,
+ "server_192.0.2.1_record_A_query_status_dns_error": 0,
+ "server_192.0.2.1_record_A_query_status_network_error": 1,
+ "server_192.0.2.1_record_A_query_status_success": 0,
+ },
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ dq := test.prepare()
+
+ require.NoError(t, dq.Init())
+
+ mx := dq.Collect()
+
+ require.Equal(t, test.wantMetrics, mx)
+ })
+ }
+}
+
+func caseDNSClientOK() *DNSQuery {
+ dq := New()
+ dq.Domains = []string{"example.com"}
+ dq.Servers = []string{"192.0.2.0", "192.0.2.1"}
+ dq.newDNSClient = func(_ string, _ time.Duration) dnsClient {
+ return mockDNSClient{errOnExchange: false}
+ }
+ return dq
+}
+
+func caseDNSClientErr() *DNSQuery {
+ dq := New()
+ dq.Domains = []string{"example.com"}
+ dq.Servers = []string{"192.0.2.0", "192.0.2.1"}
+ dq.newDNSClient = func(_ string, _ time.Duration) dnsClient {
+ return mockDNSClient{errOnExchange: true}
+ }
+ return dq
+}
+
+type mockDNSClient struct {
+ errOnExchange bool
+}
+
+func (m mockDNSClient) Exchange(_ *dns.Msg, _ string) (response *dns.Msg, rtt time.Duration, err error) {
+ if m.errOnExchange {
+ return nil, time.Second, errors.New("mock.Exchange() error")
+ }
+ return nil, time.Second, nil
+}
diff --git a/src/go/collectors/go.d.plugin/modules/dnsquery/init.go b/src/go/collectors/go.d.plugin/modules/dnsquery/init.go
new file mode 100644
index 000000000..65af0ea2e
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsquery/init.go
@@ -0,0 +1,98 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package dnsquery
+
+import (
+ "errors"
+ "fmt"
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
+
+ "github.com/miekg/dns"
+)
+
+func (d *DNSQuery) verifyConfig() error {
+ if len(d.Domains) == 0 {
+ return errors.New("no domains specified")
+ }
+
+ if len(d.Servers) == 0 {
+ return errors.New("no servers specified")
+ }
+
+ if !(d.Network == "" || d.Network == "udp" || d.Network == "tcp" || d.Network == "tcp-tls") {
+ return fmt.Errorf("wrong network transport : %s", d.Network)
+ }
+
+ if d.RecordType != "" {
+ d.Warning("'record_type' config option is deprecated, use 'record_types' instead")
+ d.RecordTypes = append(d.RecordTypes, d.RecordType)
+ }
+
+ if len(d.RecordTypes) == 0 {
+ return errors.New("no record types specified")
+ }
+
+ return nil
+}
+
+func (d *DNSQuery) initRecordTypes() (map[string]uint16, error) {
+ types := make(map[string]uint16)
+ for _, v := range d.RecordTypes {
+ rtype, err := parseRecordType(v)
+ if err != nil {
+ return nil, err
+ }
+ types[v] = rtype
+
+ }
+
+ return types, nil
+}
+
+func (d *DNSQuery) initCharts() (*module.Charts, error) {
+ charts := module.Charts{}
+
+ for _, srv := range d.Servers {
+ for _, rtype := range d.RecordTypes {
+ cs := newDNSServerCharts(srv, d.Network, rtype)
+ if err := charts.Add(*cs...); err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ return &charts, nil
+}
+
+func parseRecordType(recordType string) (uint16, error) {
+ var rtype uint16
+
+ switch recordType {
+ case "A":
+ rtype = dns.TypeA
+ case "AAAA":
+ rtype = dns.TypeAAAA
+ case "ANY":
+ rtype = dns.TypeANY
+ case "CNAME":
+ rtype = dns.TypeCNAME
+ case "MX":
+ rtype = dns.TypeMX
+ case "NS":
+ rtype = dns.TypeNS
+ case "PTR":
+ rtype = dns.TypePTR
+ case "SOA":
+ rtype = dns.TypeSOA
+ case "SPF":
+ rtype = dns.TypeSPF
+ case "SRV":
+ rtype = dns.TypeSRV
+ case "TXT":
+ rtype = dns.TypeTXT
+ default:
+ return 0, fmt.Errorf("unknown record type : %s", recordType)
+ }
+
+ return rtype, nil
+}
diff --git a/src/go/collectors/go.d.plugin/modules/dnsquery/integrations/dns_query.md b/src/go/collectors/go.d.plugin/modules/dnsquery/integrations/dns_query.md
new file mode 100644
index 000000000..fccac8b59
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsquery/integrations/dns_query.md
@@ -0,0 +1,181 @@
+<!--startmeta
+custom_edit_url: "https://github.com/netdata/netdata/edit/master/src/go/collectors/go.d.plugin/modules/dnsquery/README.md"
+meta_yaml: "https://github.com/netdata/netdata/edit/master/src/go/collectors/go.d.plugin/modules/dnsquery/metadata.yaml"
+sidebar_label: "DNS query"
+learn_status: "Published"
+learn_rel_path: "Collecting Metrics/DNS and DHCP Servers"
+most_popular: False
+message: "DO NOT EDIT THIS FILE DIRECTLY, IT IS GENERATED BY THE COLLECTOR'S metadata.yaml FILE"
+endmeta-->
+
+# DNS query
+
+
+<img src="https://netdata.cloud/img/network-wired.svg" width="150"/>
+
+
+Plugin: go.d.plugin
+Module: dns_query
+
+<img src="https://img.shields.io/badge/maintained%20by-Netdata-%2300ab44" />
+
+## Overview
+
+This module monitors DNS query round-trip time (RTT).
+
+
+
+
+This collector is supported on all platforms.
+
+This collector supports collecting metrics from multiple instances of this integration, including remote instances.
+
+
+### Default Behavior
+
+#### Auto-Detection
+
+This integration doesn't support auto-detection.
+
+#### Limits
+
+The default configuration for this integration does not impose any limits on data collection.
+
+#### Performance Impact
+
+The default configuration for this integration is not expected to impose a significant performance impact on the system.
+
+
+## Metrics
+
+Metrics grouped by *scope*.
+
+The scope defines the instance that the metric belongs to. An instance is uniquely identified by a set of labels.
+
+
+
+### Per server
+
+These metrics refer to the DNS server.
+
+Labels:
+
+| Label | Description |
+|:-----------|:----------------|
+| server | DNS server address. |
+| network | Network protocol name (tcp, udp, tcp-tls). |
+| record_type | DNS record type (e.g. A, AAAA, CNAME). |
+
+Metrics:
+
+| Metric | Dimensions | Unit |
+|:------|:----------|:----|
+| dns_query.query_status | success, network_error, dns_error | status |
+| dns_query.query_time | query_time | seconds |
+
+
+
+## Alerts
+
+
+The following alerts are available:
+
+| Alert name | On metric | Description |
+|:------------|:----------|:------------|
+| [ dns_query_query_status ](https://github.com/netdata/netdata/blob/master/src/health/health.d/dns_query.conf) | dns_query.query_status | DNS request type ${label:record_type} to server ${label:server} is unsuccessful |
+
+
+## Setup
+
+### Prerequisites
+
+No action required.
+
+### Configuration
+
+#### File
+
+The configuration file name for this integration is `go.d/dns_query.conf`.
+
+
+You can edit the configuration file using the `edit-config` script from the
+Netdata [config directory](/docs/netdata-agent/configuration/README.md#the-netdata-config-directory).
+
+```bash
+cd /etc/netdata 2>/dev/null || cd /opt/netdata/etc/netdata
+sudo ./edit-config go.d/dns_query.conf
+```
+#### Options
+
+The following options can be defined globally: update_every, autodetection_retry.
+
+
+<details open><summary>All options</summary>
+
+| Name | Description | Default | Required |
+|:----|:-----------|:-------|:--------:|
+| update_every | Data collection frequency. | 1 | no |
+| autodetection_retry | Recheck interval in seconds. Zero means no recheck will be scheduled. | 0 | no |
+| domains | Domain or subdomains to query. The collector will choose a random domain from the list on every iteration. | | yes |
+| servers | Servers to query. | | yes |
+| port | DNS server port. | 53 | no |
+| network | Network protocol name. Available options: udp, tcp, tcp-tls. | udp | no |
+| record_types | Query record type. Available options: A, AAAA, CNAME, MX, NS, PTR, TXT, SOA, SPF, TXT, SRV. | A | no |
+| timeout | Query read timeout. | 2 | no |
+
+</details>
+
+#### Examples
+
+##### Basic
+
+An example configuration.
+
+<details open><summary>Config</summary>
+
+```yaml
+jobs:
+ - name: job1
+ record_types:
+ - A
+ - AAAA
+ domains:
+ - google.com
+ - github.com
+ - reddit.com
+ servers:
+ - 8.8.8.8
+ - 8.8.4.4
+
+```
+</details>
+
+
+
+## Troubleshooting
+
+### Debug Mode
+
+To troubleshoot issues with the `dns_query` collector, run the `go.d.plugin` with the debug option enabled. The output
+should give you clues as to why the collector isn't working.
+
+- Navigate to the `plugins.d` directory, usually at `/usr/libexec/netdata/plugins.d/`. If that's not the case on
+ your system, open `netdata.conf` and look for the `plugins` setting under `[directories]`.
+
+ ```bash
+ cd /usr/libexec/netdata/plugins.d/
+ ```
+
+- Switch to the `netdata` user.
+
+ ```bash
+ sudo -u netdata -s
+ ```
+
+- Run the `go.d.plugin` to debug the collector:
+
+ ```bash
+ ./go.d.plugin -d -m dns_query
+ ```
+
+
diff --git a/src/go/collectors/go.d.plugin/modules/dnsquery/metadata.yaml b/src/go/collectors/go.d.plugin/modules/dnsquery/metadata.yaml
new file mode 100644
index 000000000..8c199550f
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsquery/metadata.yaml
@@ -0,0 +1,142 @@
+plugin_name: go.d.plugin
+modules:
+ - meta:
+ id: collector-go.d.plugin-dns_query
+ plugin_name: go.d.plugin
+ module_name: dns_query
+ monitored_instance:
+ name: DNS query
+ link: ""
+ icon_filename: network-wired.svg
+ categories:
+ - data-collection.dns-and-dhcp-servers
+ keywords:
+ - dns
+ related_resources:
+ integrations:
+ list: []
+ info_provided_to_referring_integrations:
+ description: ""
+ most_popular: false
+ overview:
+ data_collection:
+ metrics_description: |
+ This module monitors DNS query round-trip time (RTT).
+ method_description: ""
+ supported_platforms:
+ include: []
+ exclude: []
+ multi_instance: true
+ additional_permissions:
+ description: ""
+ default_behavior:
+ auto_detection:
+ description: ""
+ limits:
+ description: ""
+ performance_impact:
+ description: ""
+ setup:
+ prerequisites:
+ list: []
+ configuration:
+ file:
+ name: go.d/dns_query.conf
+ options:
+ description: |
+ The following options can be defined globally: update_every, autodetection_retry.
+ folding:
+ title: All options
+ enabled: true
+ list:
+ - name: update_every
+ description: Data collection frequency.
+ default_value: 1
+ required: false
+ - name: autodetection_retry
+ description: Recheck interval in seconds. Zero means no recheck will be scheduled.
+ default_value: 0
+ required: false
+ - name: domains
+ description: Domain or subdomains to query. The collector will choose a random domain from the list on every iteration.
+ default_value: ""
+ required: true
+ - name: servers
+ description: Servers to query.
+ default_value: ""
+ required: true
+ - name: port
+ description: DNS server port.
+ default_value: 53
+ required: false
+ - name: network
+ description: "Network protocol name. Available options: udp, tcp, tcp-tls."
+ default_value: udp
+ required: false
+ - name: record_types
+ description: "Query record type. Available options: A, AAAA, CNAME, MX, NS, PTR, TXT, SOA, SPF, TXT, SRV."
+ default_value: A
+ required: false
+ - name: timeout
+ description: Query read timeout.
+ default_value: 2
+ required: false
+ examples:
+ folding:
+ title: Config
+ enabled: true
+ list:
+ - name: Basic
+ description: An example configuration.
+ config: |
+ jobs:
+ - name: job1
+ record_types:
+ - A
+ - AAAA
+ domains:
+ - google.com
+ - github.com
+ - reddit.com
+ servers:
+ - 8.8.8.8
+ - 8.8.4.4
+ troubleshooting:
+ problems:
+ list: []
+ alerts:
+ - name: dns_query_query_status
+ metric: dns_query.query_status
+ info: "DNS request type ${label:record_type} to server ${label:server} is unsuccessful"
+ link: https://github.com/netdata/netdata/blob/master/src/health/health.d/dns_query.conf
+ metrics:
+ folding:
+ title: Metrics
+ enabled: false
+ description: ""
+ availability: []
+ scopes:
+ - name: server
+ description: These metrics refer to the DNS server.
+ labels:
+ - name: server
+ description: DNS server address.
+ - name: network
+ description: Network protocol name (tcp, udp, tcp-tls).
+ - name: record_type
+ description: DNS record type (e.g. A, AAAA, CNAME).
+ metrics:
+ - name: dns_query.query_status
+ description: DNS Query Status
+ unit: status
+ chart_type: line
+ dimensions:
+ - name: success
+ - name: network_error
+ - name: dns_error
+ - name: dns_query.query_time
+ description: DNS Query Time
+ unit: seconds
+ chart_type: line
+ dimensions:
+ - name: query_time
diff --git a/src/go/collectors/go.d.plugin/modules/dnsquery/testdata/config.json b/src/go/collectors/go.d.plugin/modules/dnsquery/testdata/config.json
new file mode 100644
index 000000000..b16ed18c6
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsquery/testdata/config.json
@@ -0,0 +1,16 @@
+{
+ "update_every": 123,
+ "domains": [
+ "ok"
+ ],
+ "servers": [
+ "ok"
+ ],
+ "network": "ok",
+ "record_type": "ok",
+ "record_types": [
+ "ok"
+ ],
+ "port": 123,
+ "timeout": 123.123
+}
diff --git a/src/go/collectors/go.d.plugin/modules/dnsquery/testdata/config.yaml b/src/go/collectors/go.d.plugin/modules/dnsquery/testdata/config.yaml
new file mode 100644
index 000000000..6c6b014b6
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/dnsquery/testdata/config.yaml
@@ -0,0 +1,11 @@
+update_every: 123
+domains:
+ - "ok"
+servers:
+ - "ok"
+network: "ok"
+record_type: "ok"
+record_types:
+ - "ok"
+port: 123
+timeout: 123.123