summaryrefslogtreecommitdiffstats
path: root/src/go/collectors/go.d.plugin/modules/powerdns
diff options
context:
space:
mode:
authorAustin S. Hemmelgarn <austin@netdata.cloud>2024-02-13 06:56:20 -0500
committerGitHub <noreply@github.com>2024-02-13 06:56:20 -0500
commit3a29b66132f561c910d827e8c7ae82997f7c1f30 (patch)
treea9306156631b6b188de8877f7c1dbdbe8b067804 /src/go/collectors/go.d.plugin/modules/powerdns
parent57eec3da0e51baa400037ccc4b547cb839ab6ffa (diff)
Include Go plugin sources in main repository. (#16997)
* Include Go plugin sources in main repository. * Fix CI issues. * Rename source tree.
Diffstat (limited to 'src/go/collectors/go.d.plugin/modules/powerdns')
l---------src/go/collectors/go.d.plugin/modules/powerdns/README.md1
-rw-r--r--src/go/collectors/go.d.plugin/modules/powerdns/authoritativens.go100
-rw-r--r--src/go/collectors/go.d.plugin/modules/powerdns/authoritativens_test.go334
-rw-r--r--src/go/collectors/go.d.plugin/modules/powerdns/charts.go66
-rw-r--r--src/go/collectors/go.d.plugin/modules/powerdns/collect.go101
-rw-r--r--src/go/collectors/go.d.plugin/modules/powerdns/config_schema.json59
-rw-r--r--src/go/collectors/go.d.plugin/modules/powerdns/init.go29
-rw-r--r--src/go/collectors/go.d.plugin/modules/powerdns/integrations/powerdns_authoritative_server.md223
-rw-r--r--src/go/collectors/go.d.plugin/modules/powerdns/metadata.yaml215
-rw-r--r--src/go/collectors/go.d.plugin/modules/powerdns/metrics.go13
-rw-r--r--src/go/collectors/go.d.plugin/modules/powerdns/testdata/recursor/statistics.json587
-rw-r--r--src/go/collectors/go.d.plugin/modules/powerdns/testdata/v4.3.0/statistics.json507
12 files changed, 2235 insertions, 0 deletions
diff --git a/src/go/collectors/go.d.plugin/modules/powerdns/README.md b/src/go/collectors/go.d.plugin/modules/powerdns/README.md
new file mode 120000
index 0000000000..3e5989715e
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/powerdns/README.md
@@ -0,0 +1 @@
+integrations/powerdns_authoritative_server.md \ No newline at end of file
diff --git a/src/go/collectors/go.d.plugin/modules/powerdns/authoritativens.go b/src/go/collectors/go.d.plugin/modules/powerdns/authoritativens.go
new file mode 100644
index 0000000000..07b7fdbcfe
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/powerdns/authoritativens.go
@@ -0,0 +1,100 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package powerdns
+
+import (
+ _ "embed"
+ "net/http"
+ "time"
+
+ "github.com/netdata/go.d.plugin/agent/module"
+ "github.com/netdata/go.d.plugin/pkg/web"
+)
+
+//go:embed "config_schema.json"
+var configSchema string
+
+func init() {
+ module.Register("powerdns", module.Creator{
+ JobConfigSchema: configSchema,
+ Create: func() module.Module { return New() },
+ })
+}
+
+func New() *AuthoritativeNS {
+ return &AuthoritativeNS{
+ Config: Config{
+ HTTP: web.HTTP{
+ Request: web.Request{
+ URL: "http://127.0.0.1:8081",
+ },
+ Client: web.Client{
+ Timeout: web.Duration{Duration: time.Second},
+ },
+ },
+ },
+ }
+}
+
+type Config struct {
+ web.HTTP `yaml:",inline"`
+}
+
+type AuthoritativeNS struct {
+ module.Base
+ Config `yaml:",inline"`
+
+ httpClient *http.Client
+ charts *module.Charts
+}
+
+func (ns *AuthoritativeNS) Init() bool {
+ err := ns.validateConfig()
+ if err != nil {
+ ns.Errorf("config validation: %v", err)
+ return false
+ }
+
+ client, err := ns.initHTTPClient()
+ if err != nil {
+ ns.Errorf("init HTTP client: %v", err)
+ return false
+ }
+ ns.httpClient = client
+
+ cs, err := ns.initCharts()
+ if err != nil {
+ ns.Errorf("init charts: %v", err)
+ return false
+ }
+ ns.charts = cs
+
+ return true
+}
+
+func (ns *AuthoritativeNS) Check() bool {
+ return len(ns.Collect()) > 0
+}
+
+func (ns *AuthoritativeNS) Charts() *module.Charts {
+ return ns.charts
+}
+
+func (ns *AuthoritativeNS) Collect() map[string]int64 {
+ ms, err := ns.collect()
+ if err != nil {
+ ns.Error(err)
+ }
+
+ if len(ms) == 0 {
+ return nil
+ }
+ return ms
+}
+
+func (ns *AuthoritativeNS) Cleanup() {
+ if ns.httpClient == nil {
+ return
+ }
+ ns.httpClient.CloseIdleConnections()
+}
diff --git a/src/go/collectors/go.d.plugin/modules/powerdns/authoritativens_test.go b/src/go/collectors/go.d.plugin/modules/powerdns/authoritativens_test.go
new file mode 100644
index 0000000000..71e5c6dc4d
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/powerdns/authoritativens_test.go
@@ -0,0 +1,334 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package powerdns
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "testing"
+
+ "github.com/netdata/go.d.plugin/pkg/tlscfg"
+ "github.com/netdata/go.d.plugin/pkg/web"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+var (
+ v430statistics, _ = os.ReadFile("testdata/v4.3.0/statistics.json")
+ recursorStatistics, _ = os.ReadFile("testdata/recursor/statistics.json")
+)
+
+func Test_testDataIsCorrectlyReadAndValid(t *testing.T) {
+ for name, data := range map[string][]byte{
+ "v430statistics": v430statistics,
+ "recursorStatistics": recursorStatistics,
+ } {
+ require.NotNilf(t, data, name)
+ }
+}
+
+func TestNew(t *testing.T) {
+ assert.IsType(t, (*AuthoritativeNS)(nil), New())
+}
+
+func TestRecursor_Init(t *testing.T) {
+ tests := map[string]struct {
+ config Config
+ wantFail bool
+ }{
+ "success on default config": {
+ config: New().Config,
+ },
+ "fails on unset URL": {
+ wantFail: true,
+ config: Config{
+ HTTP: web.HTTP{
+ Request: web.Request{URL: ""},
+ },
+ },
+ },
+ "fails on invalid TLSCA": {
+ wantFail: true,
+ config: Config{
+ HTTP: web.HTTP{
+ Request: web.Request{
+ URL: "http://127.0.0.1:38001",
+ },
+ Client: web.Client{
+ TLSConfig: tlscfg.TLSConfig{TLSCA: "testdata/tls"},
+ },
+ },
+ },
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ ns := New()
+ ns.Config = test.config
+
+ if test.wantFail {
+ assert.False(t, ns.Init())
+ } else {
+ assert.True(t, ns.Init())
+ }
+ })
+ }
+}
+
+func TestRecursor_Check(t *testing.T) {
+ tests := map[string]struct {
+ prepare func() (p *AuthoritativeNS, cleanup func())
+ wantFail bool
+ }{
+ "success on valid response v4.3.0": {
+ prepare: preparePowerDNSAuthoritativeNSV430,
+ },
+ "fails on response from PowerDNS Recursor": {
+ wantFail: true,
+ prepare: preparePowerDNSAuthoritativeNSRecursorData,
+ },
+ "fails on 404 response": {
+ wantFail: true,
+ prepare: preparePowerDNSAuthoritativeNS404,
+ },
+ "fails on connection refused": {
+ wantFail: true,
+ prepare: preparePowerDNSAuthoritativeNSConnectionRefused,
+ },
+ "fails on response with invalid data": {
+ wantFail: true,
+ prepare: preparePowerDNSAuthoritativeNSInvalidData,
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ recursor, cleanup := test.prepare()
+ defer cleanup()
+ require.True(t, recursor.Init())
+
+ if test.wantFail {
+ assert.False(t, recursor.Check())
+ } else {
+ assert.True(t, recursor.Check())
+ }
+ })
+ }
+}
+
+func TestRecursor_Charts(t *testing.T) {
+ recursor := New()
+ require.True(t, recursor.Init())
+ assert.NotNil(t, recursor.Charts())
+}
+
+func TestRecursor_Cleanup(t *testing.T) {
+ assert.NotPanics(t, New().Cleanup)
+}
+
+func TestRecursor_Collect(t *testing.T) {
+ tests := map[string]struct {
+ prepare func() (p *AuthoritativeNS, cleanup func())
+ wantCollected map[string]int64
+ }{
+ "success on valid response v4.3.0": {
+ prepare: preparePowerDNSAuthoritativeNSV430,
+ wantCollected: map[string]int64{
+ "corrupt-packets": 1,
+ "cpu-iowait": 513,
+ "cpu-steal": 1,
+ "deferred-cache-inserts": 1,
+ "deferred-cache-lookup": 1,
+ "deferred-packetcache-inserts": 1,
+ "deferred-packetcache-lookup": 1,
+ "dnsupdate-answers": 1,
+ "dnsupdate-changes": 1,
+ "dnsupdate-queries": 1,
+ "dnsupdate-refused": 1,
+ "fd-usage": 23,
+ "incoming-notifications": 1,
+ "key-cache-size": 1,
+ "latency": 1,
+ "meta-cache-size": 1,
+ "open-tcp-connections": 1,
+ "overload-drops": 1,
+ "packetcache-hit": 1,
+ "packetcache-miss": 1,
+ "packetcache-size": 1,
+ "qsize-q": 1,
+ "query-cache-hit": 1,
+ "query-cache-miss": 1,
+ "query-cache-size": 1,
+ "rd-queries": 1,
+ "real-memory-usage": 164507648,
+ "recursing-answers": 1,
+ "recursing-questions": 1,
+ "recursion-unanswered": 1,
+ "ring-logmessages-capacity": 10000,
+ "ring-logmessages-size": 10,
+ "ring-noerror-queries-capacity": 10000,
+ "ring-noerror-queries-size": 1,
+ "ring-nxdomain-queries-capacity": 10000,
+ "ring-nxdomain-queries-size": 1,
+ "ring-queries-capacity": 10000,
+ "ring-queries-size": 1,
+ "ring-remotes-capacity": 10000,
+ "ring-remotes-corrupt-capacity": 10000,
+ "ring-remotes-corrupt-size": 1,
+ "ring-remotes-size": 1,
+ "ring-remotes-unauth-capacity": 10000,
+ "ring-remotes-unauth-size": 1,
+ "ring-servfail-queries-capacity": 10000,
+ "ring-servfail-queries-size": 1,
+ "ring-unauth-queries-capacity": 10000,
+ "ring-unauth-queries-size": 1,
+ "security-status": 1,
+ "servfail-packets": 1,
+ "signature-cache-size": 1,
+ "signatures": 1,
+ "sys-msec": 128,
+ "tcp-answers": 1,
+ "tcp-answers-bytes": 1,
+ "tcp-queries": 1,
+ "tcp4-answers": 1,
+ "tcp4-answers-bytes": 1,
+ "tcp4-queries": 1,
+ "tcp6-answers": 1,
+ "tcp6-answers-bytes": 1,
+ "tcp6-queries": 1,
+ "timedout-packets": 1,
+ "udp-answers": 1,
+ "udp-answers-bytes": 1,
+ "udp-do-queries": 1,
+ "udp-in-errors": 1,
+ "udp-noport-errors": 1,
+ "udp-queries": 1,
+ "udp-recvbuf-errors": 1,
+ "udp-sndbuf-errors": 1,
+ "udp4-answers": 1,
+ "udp4-answers-bytes": 1,
+ "udp4-queries": 1,
+ "udp6-answers": 1,
+ "udp6-answers-bytes": 1,
+ "udp6-queries": 1,
+ "uptime": 207,
+ "user-msec": 56,
+ },
+ },
+ "fails on response from PowerDNS Recursor": {
+ prepare: preparePowerDNSAuthoritativeNSRecursorData,
+ },
+ "fails on 404 response": {
+ prepare: preparePowerDNSAuthoritativeNS404,
+ },
+ "fails on connection refused": {
+ prepare: preparePowerDNSAuthoritativeNSConnectionRefused,
+ },
+ "fails on response with invalid data": {
+ prepare: preparePowerDNSAuthoritativeNSInvalidData,
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ ns, cleanup := test.prepare()
+ defer cleanup()
+ require.True(t, ns.Init())
+
+ collected := ns.Collect()
+
+ assert.Equal(t, test.wantCollected, collected)
+ if len(test.wantCollected) > 0 {
+ ensureCollectedHasAllChartsDimsVarsIDs(t, ns, collected)
+ }
+ })
+ }
+}
+
+func ensureCollectedHasAllChartsDimsVarsIDs(t *testing.T, ns *AuthoritativeNS, collected map[string]int64) {
+ for _, chart := range *ns.Charts() {
+ if chart.Obsolete {
+ continue
+ }
+ for _, dim := range chart.Dims {
+ _, ok := collected[dim.ID]
+ assert.Truef(t, ok, "chart '%s' dim '%s': no dim in collected", dim.ID, chart.ID)
+ }
+ for _, v := range chart.Vars {
+ _, ok := collected[v.ID]
+ assert.Truef(t, ok, "chart '%s' dim '%s': no dim in collected", v.ID, chart.ID)
+ }
+ }
+}
+
+func preparePowerDNSAuthoritativeNSV430() (*AuthoritativeNS, func()) {
+ srv := preparePowerDNSAuthoritativeNSEndpoint()
+ ns := New()
+ ns.URL = srv.URL
+
+ return ns, srv.Close
+}
+
+func preparePowerDNSAuthoritativeNSRecursorData() (*AuthoritativeNS, func()) {
+ srv := preparePowerDNSRecursorEndpoint()
+ ns := New()
+ ns.URL = srv.URL
+
+ return ns, srv.Close
+}
+
+func preparePowerDNSAuthoritativeNSInvalidData() (*AuthoritativeNS, func()) {
+ srv := httptest.NewServer(http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ _, _ = w.Write([]byte("hello and\n goodbye"))
+ }))
+ ns := New()
+ ns.URL = srv.URL
+
+ return ns, srv.Close
+}
+
+func preparePowerDNSAuthoritativeNS404() (*AuthoritativeNS, func()) {
+ srv := httptest.NewServer(http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusNotFound)
+ }))
+ ns := New()
+ ns.URL = srv.URL
+
+ return ns, srv.Close
+}
+
+func preparePowerDNSAuthoritativeNSConnectionRefused() (*AuthoritativeNS, func()) {
+ ns := New()
+ ns.URL = "http://127.0.0.1:38001"
+
+ return ns, func() {}
+}
+
+func preparePowerDNSAuthoritativeNSEndpoint() *httptest.Server {
+ return httptest.NewServer(http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case urlPathLocalStatistics:
+ _, _ = w.Write(v430statistics)
+ default:
+ w.WriteHeader(http.StatusNotFound)
+ }
+ }))
+}
+
+func preparePowerDNSRecursorEndpoint() *httptest.Server {
+ return httptest.NewServer(http.HandlerFunc(
+ func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case urlPathLocalStatistics:
+ _, _ = w.Write(recursorStatistics)
+ default:
+ w.WriteHeader(http.StatusNotFound)
+ }
+ }))
+}
diff --git a/src/go/collectors/go.d.plugin/modules/powerdns/charts.go b/src/go/collectors/go.d.plugin/modules/powerdns/charts.go
new file mode 100644
index 0000000000..aa61149e69
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/powerdns/charts.go
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package powerdns
+
+import "github.com/netdata/go.d.plugin/agent/module"
+
+var charts = module.Charts{
+ {
+ ID: "questions_in",
+ Title: "Incoming questions",
+ Units: "questions/s",
+ Fam: "questions",
+ Ctx: "powerdns.questions_in",
+ Dims: module.Dims{
+ {ID: "udp-queries", Name: "udp", Algo: module.Incremental},
+ {ID: "tcp-queries", Name: "tcp", Algo: module.Incremental},
+ },
+ },
+ {
+ ID: "questions_out",
+ Title: "Outgoing questions",
+ Units: "questions/s",
+ Fam: "questions",
+ Ctx: "powerdns.questions_out",
+ Dims: module.Dims{
+ {ID: "udp-answers", Name: "udp", Algo: module.Incremental},
+ {ID: "tcp-answers", Name: "tcp", Algo: module.Incremental},
+ },
+ },
+ {
+ ID: "cache_usage",
+ Title: "Cache Usage",
+ Units: "events/s",
+ Fam: "cache",
+ Ctx: "powerdns.cache_usage",
+ Dims: module.Dims{
+ {ID: "query-cache-hit", Algo: module.Incremental},
+ {ID: "query-cache-miss", Algo: module.Incremental},
+ {ID: "packetcache-hit", Name: "packet-cache-hit", Algo: module.Incremental},
+ {ID: "packetcache-miss", Name: "packet-cache-miss", Algo: module.Incremental},
+ },
+ },
+ {
+ ID: "cache_size",
+ Title: "Cache Size",
+ Units: "entries",
+ Fam: "cache",
+ Ctx: "powerdns.cache_size",
+ Dims: module.Dims{
+ {ID: "query-cache-size", Name: "query-cache"},
+ {ID: "packetcache-size", Name: "packet-cache"},
+ {ID: "key-cache-size", Name: "key-cache"},
+ {ID: "meta-cache-size", Name: "meta-cache"},
+ },
+ },
+ {
+ ID: "latency",
+ Title: "Answer latency",
+ Units: "microseconds",
+ Fam: "latency",
+ Ctx: "powerdns.latency",
+ Dims: module.Dims{
+ {ID: "latency"},
+ },
+ },
+}
diff --git a/src/go/collectors/go.d.plugin/modules/powerdns/collect.go b/src/go/collectors/go.d.plugin/modules/powerdns/collect.go
new file mode 100644
index 0000000000..7a184d9160
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/powerdns/collect.go
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package powerdns
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "strconv"
+
+ "github.com/netdata/go.d.plugin/pkg/web"
+)
+
+const (
+ urlPathLocalStatistics = "/api/v1/servers/localhost/statistics"
+)
+
+func (ns *AuthoritativeNS) collect() (map[string]int64, error) {
+ statistics, err := ns.scrapeStatistics()
+ if err != nil {
+ return nil, err
+ }
+
+ collected := make(map[string]int64)
+
+ ns.collectStatistics(collected, statistics)
+
+ if !isPowerDNSAuthoritativeNSMetrics(collected) {
+ return nil, errors.New("returned metrics aren't PowerDNS Authoritative Server metrics")
+ }
+
+ return collected, nil
+}
+
+func isPowerDNSAuthoritativeNSMetrics(collected map[string]int64) bool {
+ // PowerDNS Recursor has same endpoint and returns data in the same format.
+ _, ok1 := collected["over-capacity-drops"]
+ _, ok2 := collected["tcp-questions"]
+ return !ok1 && !ok2
+}
+
+func (ns *AuthoritativeNS) collectStatistics(collected map[string]int64, statistics statisticMetrics) {
+ for _, s := range statistics {
+ // https://doc.powerdns.com/authoritative/http-api/statistics.html#statisticitem
+ if s.Type != "StatisticItem" {
+ continue
+ }
+
+ value, ok := s.Value.(string)
+ if !ok {
+ ns.Debugf("%s value (%v) unexpected type: want=string, got=%T.", s.Name, s.Value, s.Value)
+ continue
+ }
+
+ v, err := strconv.ParseInt(value, 10, 64)
+ if err != nil {
+ ns.Debugf("%s value (%v) parse error: %v", s.Name, s.Value, err)
+ continue
+ }
+
+ collected[s.Name] = v
+ }
+}
+
+func (ns *AuthoritativeNS) scrapeStatistics() ([]statisticMetric, error) {
+ req, _ := web.NewHTTPRequest(ns.Request)
+ req.URL.Path = urlPathLocalStatistics
+
+ var statistics statisticMetrics
+ if err := ns.doOKDecode(req, &statistics); err != nil {
+ return nil, err
+ }
+
+ return statistics, nil
+}
+
+func (ns *AuthoritativeNS) doOKDecode(req *http.Request, in interface{}) error {
+ resp, err := ns.httpClient.Do(req)
+ if err != nil {
+ return fmt.Errorf("error on HTTP request '%s': %v", req.URL, err)
+ }
+ defer closeBody(resp)
+
+ if resp.StatusCode != http.StatusOK {
+ return fmt.Errorf("'%s' returned HTTP status code: %d", req.URL, resp.StatusCode)
+ }
+
+ if err := json.NewDecoder(resp.Body).Decode(in); err != nil {
+ return fmt.Errorf("error on decoding response from '%s': %v", req.URL, err)
+ }
+ return nil
+}
+
+func closeBody(resp *http.Response) {
+ if resp != nil && resp.Body != nil {
+ _, _ = io.Copy(io.Discard, resp.Body)
+ _ = resp.Body.Close()
+ }
+}
diff --git a/src/go/collectors/go.d.plugin/modules/powerdns/config_schema.json b/src/go/collectors/go.d.plugin/modules/powerdns/config_schema.json
new file mode 100644
index 0000000000..93f8e72a2a
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/powerdns/config_schema.json
@@ -0,0 +1,59 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "go.d/powerdns job configuration schema.",
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ },
+ "timeout": {
+ "type": [
+ "string",
+ "integer"
+ ]
+ },
+ "username": {
+ "type": "string"
+ },
+ "password": {
+ "type": "string"
+ },
+ "proxy_url": {
+ "type": "string"
+ },
+ "proxy_username": {
+ "type": "string"
+ },
+ "proxy_password": {
+ "type": "string"
+ },
+ "headers": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "not_follow_redirects": {
+ "type": "boolean"
+ },
+ "tls_ca": {
+ "type": "string"
+ },
+ "tls_cert": {
+ "type": "string"
+ },
+ "tls_key": {
+ "type": "string"
+ },
+ "insecure_skip_verify": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "name",
+ "url"
+ ]
+}
diff --git a/src/go/collectors/go.d.plugin/modules/powerdns/init.go b/src/go/collectors/go.d.plugin/modules/powerdns/init.go
new file mode 100644
index 0000000000..a577db7732
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/powerdns/init.go
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package powerdns
+
+import (
+ "errors"
+ "net/http"
+
+ "github.com/netdata/go.d.plugin/agent/module"
+ "github.com/netdata/go.d.plugin/pkg/web"
+)
+
+func (ns AuthoritativeNS) validateConfig() error {
+ if ns.URL == "" {
+ return errors.New("URL not set")
+ }
+ if _, err := web.NewHTTPRequest(ns.Request); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (ns AuthoritativeNS) initHTTPClient() (*http.Client, error) {
+ return web.NewHTTPClient(ns.Client)
+}
+
+func (ns AuthoritativeNS) initCharts() (*module.Charts, error) {
+ return charts.Copy(), nil
+}
diff --git a/src/go/collectors/go.d.plugin/modules/powerdns/integrations/powerdns_authoritative_server.md b/src/go/collectors/go.d.plugin/modules/powerdns/integrations/powerdns_authoritative_server.md
new file mode 100644
index 0000000000..4c776193a5
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/powerdns/integrations/powerdns_authoritative_server.md
@@ -0,0 +1,223 @@
+<!--startmeta
+custom_edit_url: "https://github.com/netdata/go.d.plugin/edit/master/modules/powerdns/README.md"
+meta_yaml: "https://github.com/netdata/go.d.plugin/edit/master/modules/powerdns/metadata.yaml"
+sidebar_label: "PowerDNS Authoritative Server"
+learn_status: "Published"
+learn_rel_path: "Data Collection/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-->
+
+# PowerDNS Authoritative Server
+
+
+<img src="https://netdata.cloud/img/powerdns.svg" width="150"/>
+
+
+Plugin: go.d.plugin
+Module: powerdns
+
+<img src="https://img.shields.io/badge/maintained%20by-Netdata-%2300ab44" />
+
+## Overview
+
+This collector monitors PowerDNS Authoritative Server instances.
+It collects metrics from [the internal webserver](https://doc.powerdns.com/authoritative/http-api/index.html#webserver).
+
+Used endpoints:
+
+- [`/api/v1/servers/localhost/statistics`](https://doc.powerdns.com/authoritative/http-api/statistics.html)
+
+
+
+
+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 PowerDNS Authoritative Server instance
+
+These metrics refer to the entire monitored application.
+
+This scope has no labels.
+
+Metrics:
+
+| Metric | Dimensions | Unit |
+|:------|:----------|:----|
+| powerdns.questions_in | udp, tcp | questions/s |
+| powerdns.questions_out | udp, tcp | questions/s |
+| powerdns.cache_usage | query-cache-hit, query-cache-miss, packetcache-hit, packetcache-miss | events/s |
+| powerdns.cache_size | query-cache, packet-cache, key-cache, meta-cache | entries |
+| powerdns.latency | latency | microseconds |
+
+
+
+## Alerts
+
+There are no alerts configured by default for this integration.
+
+
+## Setup
+
+### Prerequisites
+
+#### Enable webserver
+
+Follow [webserver](https://doc.powerdns.com/authoritative/http-api/index.html#webserver) documentation.
+
+
+#### Enable HTTP API
+
+Follow [HTTP API](https://doc.powerdns.com/authoritative/http-api/index.html#enabling-the-api) documentation.
+
+
+
+### Configuration
+
+#### File
+
+The configuration file name for this integration is `go.d/powerdns.conf`.
+
+
+You can edit the configuration file using the `edit-config` script from the
+Netdata [config directory](https://github.com/netdata/netdata/blob/master/docs/configure/nodes.md#the-netdata-config-directory).
+
+```bash
+cd /etc/netdata 2>/dev/null || cd /opt/netdata/etc/netdata
+sudo ./edit-config go.d/powerdns.conf
+```
+#### Options
+
+The following options can be defined globally: update_every, autodetection_retry.
+
+
+<details><summary>Config 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 |
+| url | Server URL. | http://127.0.0.1:8081 | yes |
+| timeout | HTTP request timeout. | 1 | no |
+| username | Username for basic HTTP authentication. | | no |
+| password | Password for basic HTTP authentication. | | no |
+| proxy_url | Proxy URL. | | no |
+| proxy_username | Username for proxy basic HTTP authentication. | | no |
+| proxy_password | Password for proxy basic HTTP authentication. | | no |
+| method | HTTP request method. | GET | no |
+| body | HTTP request body. | | no |
+| headers | HTTP request headers. | | no |
+| not_follow_redirects | Redirect handling policy. Controls whether the client follows redirects. | no | no |
+| tls_skip_verify | Server certificate chain and hostname validation policy. Controls whether the client performs this check. | no | no |
+| tls_ca | Certification authority that the client uses when verifying the server's certificates. | | no |
+| tls_cert | Client TLS certificate. | | no |
+| tls_key | Client TLS key. | | no |
+
+</details>
+
+#### Examples
+
+##### Basic
+
+An example configuration.
+
+<details><summary>Config</summary>
+
+```yaml
+jobs:
+ - name: local
+ url: http://127.0.0.1:8081
+
+```
+</details>
+
+##### HTTP authentication
+
+Basic HTTP authentication.
+
+<details><summary>Config</summary>
+
+```yaml
+jobs:
+ - name: local
+ url: http://127.0.0.1:8081
+ username: admin
+ password: password
+
+```
+</details>
+
+##### Multi-instance
+
+> **Note**: When you define multiple jobs, their names must be unique.
+
+Local and remote instances.
+
+
+<details><summary>Config</summary>
+
+```yaml
+jobs:
+ - name: local
+ url: http://127.0.0.1:8081
+
+ - name: remote
+ url: http://203.0.113.0:8081
+
+```
+</details>
+
+
+
+## Troubleshooting
+
+### Debug Mode
+
+To troubleshoot issues with the `powerdns` 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 powerdns
+ ```
+
+
diff --git a/src/go/collectors/go.d.plugin/modules/powerdns/metadata.yaml b/src/go/collectors/go.d.plugin/modules/powerdns/metadata.yaml
new file mode 100644
index 0000000000..ea4dec0b53
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/powerdns/metadata.yaml
@@ -0,0 +1,215 @@
+plugin_name: go.d.plugin
+modules:
+ - meta:
+ id: collector-go.d.plugin-powerdns
+ plugin_name: go.d.plugin
+ module_name: powerdns
+ monitored_instance:
+ name: PowerDNS Authoritative Server
+ link: https://doc.powerdns.com/authoritative/
+ icon_filename: powerdns.svg
+ categories:
+ - data-collection.dns-and-dhcp-servers
+ keywords:
+ - powerdns
+ - dns
+ related_resources:
+ integrations:
+ list: []
+ info_provided_to_referring_integrations:
+ description: ""
+ most_popular: false
+ overview:
+ data_collection:
+ metrics_description: |
+ This collector monitors PowerDNS Authoritative Server instances.
+ It collects metrics from [the internal webserver](https://doc.powerdns.com/authoritative/http-api/index.html#webserver).
+
+ Used endpoints:
+