@cryptotaxi247 / netdata-1 / commits / 1cd805d51

add go.d/nsd (#18302)

Ilya Mashchenko committed Aug 11, 2024 at 19:12 UTC 1cd805d515f0f9c5bb97d5abba680d28e1f6a351
14 files changed +1367 -1
src/collectors/python.d.plugin/python.d.conf
+1 -1
@@ -38,7 +38,6 @@ go_expvar: no
38 # haproxy: yes
39 # monit: yes
40 # nvidia_smi: yes
41 -# nsd: yes
41 # openldap: yes
42 # oracledb: yes
43 # pandas: yes
@@ -73,6 +72,7 @@ memcached: no # Removed (replaced with go.d/memcached).
72 mongodb: no # Removed (replaced with go.d/mongodb).
73 mysql: no # Removed (replaced with go.d/mysql).
74 nginx: no # Removed (replaced with go.d/nginx).
75 +nsd: no # Removed (replaced with go.d/nsd).
76 postfix: no # Removed (replaced with go.d/postfix).
77 postgres: no # Removed (replaced with go.d/postgres).
78 proxysql: no # Removed (replaced with go.d/proxysql).
src/go/plugin/go.d/modules/init.go
+1
@@ -58,6 +58,7 @@ import (
58 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/nginx"
59 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/nginxplus"
60 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/nginxvts"
61 + _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/nsd"
62 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/ntpd"
63 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/nvidia_smi"
64 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/nvme"
src/go/plugin/go.d/modules/nsd/charts.go new
+249
@@ -0,0 +1,249 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package nsd
4 +
5 +import (
6 + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
7 +)
8 +
9 +const (
10 + prioQueries = module.Priority + iota
11 + prioQueriesByType
12 + prioQueriesByOpcode
13 + prioQueriesByClass
14 + prioQueriesByProtocol
15 +
16 + prioAnswersByRcode
17 +
18 + prioErrors
19 +
20 + prioDrops
21 +
22 + prioZones
23 + prioZoneTransfersRequests
24 + prioZoneTransferMemory
25 +
26 + prioDatabaseSize
27 +
28 + prioUptime
29 +)
30 +
31 +var charts = module.Charts{
32 + queriesChart.Copy(),
33 + queriesByTypeChart.Copy(),
34 + queriesByOpcodeChart.Copy(),
35 + queriesByClassChart.Copy(),
36 + queriesByProtocolChart.Copy(),
37 +
38 + answersByRcodeChart.Copy(),
39 +
40 + zonesChart.Copy(),
41 + zoneTransfersRequestsChart.Copy(),
42 + zoneTransferMemoryChart.Copy(),
43 +
44 + databaseSizeChart.Copy(),
45 +
46 + errorsChart.Copy(),
47 +
48 + dropsChart.Copy(),
49 +
50 + uptimeChart.Copy(),
51 +}
52 +
53 +var (
54 + queriesChart = module.Chart{
55 + ID: "queries",
56 + Title: "Queries",
57 + Units: "queries/s",
58 + Fam: "queries",
59 + Ctx: "nsd.queries",
60 + Priority: prioQueries,
61 + Dims: module.Dims{
62 + {ID: "num.queries", Name: "queries", Algo: module.Incremental},
63 + },
64 + }
65 + queriesByTypeChart = func() module.Chart {
66 + chart := module.Chart{
67 + ID: "queries_by_type",
68 + Title: "Queries Type",
69 + Units: "queries/s",
70 + Fam: "queries",
71 + Ctx: "nsd.queries_by_type",
72 + Priority: prioQueriesByType,
73 + Type: module.Stacked,
74 + }
75 + for _, v := range queryTypes {
76 + name := v
77 + if s, ok := queryTypeNumberMap[v]; ok {
78 + name = s
79 + }
80 + chart.Dims = append(chart.Dims, &module.Dim{
81 + ID: "num.type." + v,
82 + Name: name,
83 + Algo: module.Incremental,
84 + })
85 + }
86 + return chart
87 + }()
88 + queriesByOpcodeChart = func() module.Chart {
89 + chart := module.Chart{
90 + ID: "queries_by_opcode",
91 + Title: "Queries Opcode",
92 + Units: "queries/s",
93 + Fam: "queries",
94 + Ctx: "nsd.queries_by_opcode",
95 + Priority: prioQueriesByOpcode,
96 + Type: module.Stacked,
97 + }
98 + for _, v := range queryOpcodes {
99 + chart.Dims = append(chart.Dims, &module.Dim{
100 + ID: "num.opcode." + v,
101 + Name: v,
102 + Algo: module.Incremental,
103 + })
104 + }
105 + return chart
106 + }()
107 + queriesByClassChart = func() module.Chart {
108 + chart := module.Chart{
109 + ID: "queries_by_class",
110 + Title: "Queries Class",
111 + Units: "queries/s",
112 + Fam: "queries",
113 + Ctx: "nsd.queries_by_class",
114 + Priority: prioQueriesByClass,
115 + Type: module.Stacked,
116 + }
117 + for _, v := range queryClasses {
118 + chart.Dims = append(chart.Dims, &module.Dim{
119 + ID: "num.class." + v,
120 + Name: v,
121 + Algo: module.Incremental,
122 + })
123 + }
124 + return chart
125 + }()
126 + queriesByProtocolChart = module.Chart{
127 + ID: "queries_by_protocol",
128 + Title: "Queries Protocol",
129 + Units: "queries/s",
130 + Fam: "queries",
131 + Ctx: "nsd.queries_by_protocol",
132 + Priority: prioQueriesByProtocol,
133 + Type: module.Stacked,
134 + Dims: module.Dims{
135 + {ID: "num.udp", Name: "udp", Algo: module.Incremental},
136 + {ID: "num.udp6", Name: "udp6", Algo: module.Incremental},
137 + {ID: "num.tcp", Name: "tcp", Algo: module.Incremental},
138 + {ID: "num.tcp6", Name: "tcp6", Algo: module.Incremental},
139 + {ID: "num.tls", Name: "tls", Algo: module.Incremental},
140 + {ID: "num.tls6", Name: "tls6", Algo: module.Incremental},
141 + },
142 + }
143 +
144 + answersByRcodeChart = func() module.Chart {
145 + chart := module.Chart{
146 + ID: "answers_by_rcode",
147 + Title: "Answers Rcode",
148 + Units: "answers/s",
149 + Fam: "answers",
150 + Ctx: "nsd.answers_by_rcode",
151 + Priority: prioAnswersByRcode,
152 + Type: module.Stacked,
153 + }
154 + for _, v := range answerRcodes {
155 + chart.Dims = append(chart.Dims, &module.Dim{
156 + ID: "num.rcode." + v,
157 + Name: v,
158 + Algo: module.Incremental,
159 + })
160 + }
161 + return chart
162 + }()
163 +
164 + errorsChart = module.Chart{
165 + ID: "errors",
166 + Title: "Errors",
167 + Units: "errors/s",
168 + Fam: "errors",
169 + Ctx: "nsd.errors",
170 + Priority: prioErrors,
171 + Dims: module.Dims{
172 + {ID: "num.rxerr", Name: "query", Algo: module.Incremental},
173 + {ID: "num.txerr", Name: "answer", Mul: -1, Algo: module.Incremental},
174 + },
175 + }
176 +
177 + dropsChart = module.Chart{
178 + ID: "drops",
179 + Title: "Drops",
180 + Units: "drops/s",
181 + Fam: "drops",
182 + Ctx: "nsd.drops",
183 + Priority: prioDrops,
184 + Dims: module.Dims{
185 + {ID: "num.dropped", Name: "query", Algo: module.Incremental},
186 + },
187 + }
188 +
189 + zonesChart = module.Chart{
190 + ID: "zones",
191 + Title: "Zones",
192 + Units: "zones",
193 + Fam: "zones",
194 + Ctx: "nsd.zones",
195 + Priority: prioZones,
196 + Dims: module.Dims{
197 + {ID: "zone.master", Name: "master"},
198 + {ID: "zone.slave", Name: "slave"},
199 + },
200 + }
201 + zoneTransfersRequestsChart = module.Chart{
202 + ID: "zone_transfers_requests",
203 + Title: "Zone Transfers",
204 + Units: "requests/s",
205 + Fam: "zones",
206 + Ctx: "nsd.zone_transfers_requests",
207 + Priority: prioZoneTransfersRequests,
208 + Dims: module.Dims{
209 + {ID: "num.raxfr", Name: "AXFR", Algo: module.Incremental},
210 + {ID: "num.rixfr", Name: "IXFR", Algo: module.Incremental},
211 + },
212 + }
213 + zoneTransferMemoryChart = module.Chart{
214 + ID: "zone_transfer_memory",
215 + Title: "Zone Transfer Memory",
216 + Units: "bytes",
217 + Fam: "zones",
218 + Ctx: "nsd.zone_transfer_memory",
219 + Priority: prioZoneTransferMemory,
220 + Dims: module.Dims{
221 + {ID: "size.xfrd.mem", Name: "used"},
222 + },
223 + }
224 +
225 + databaseSizeChart = module.Chart{
226 + ID: "database_size",
227 + Title: "Database Size",
228 + Units: "bytes",
229 + Fam: "database",
230 + Ctx: "nsd.database_size",
231 + Priority: prioDatabaseSize,
232 + Dims: module.Dims{
233 + {ID: "size.db.disk", Name: "disk"},
234 + {ID: "size.db.mem", Name: "mem"},
235 + },
236 + }
237 +
238 + uptimeChart = module.Chart{
239 + ID: "uptime",
240 + Title: "Uptime",
241 + Units: "seconds",
242 + Fam: "uptime",
243 + Ctx: "nsd.uptime",
244 + Priority: prioUptime,
245 + Dims: module.Dims{
246 + {ID: "time.boot", Name: "uptime"},
247 + },
248 + }
249 +)
src/go/plugin/go.d/modules/nsd/collect.go new
+81
@@ -0,0 +1,81 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package nsd
4 +
5 +import (
6 + "bufio"
7 + "bytes"
8 + "errors"
9 + "strconv"
10 + "strings"
11 +)
12 +
13 +func (n *Nsd) collect() (map[string]int64, error) {
14 + stats, err := n.exec.stats()
15 + if err != nil {
16 + return nil, err
17 + }
18 +
19 + if len(stats) == 0 {
20 + return nil, errors.New("empty stats response")
21 + }
22 +
23 + mx := make(map[string]int64)
24 +
25 + sc := bufio.NewScanner(bytes.NewReader(stats))
26 +
27 + for sc.Scan() {
28 + n.collectStatsLine(mx, sc.Text())
29 + }
30 +
31 + if len(mx) == 0 {
32 + return nil, errors.New("unexpected stats response: no metrics found")
33 + }
34 +
35 + addMissingMetrics(mx, "num.rcode.", answerRcodes)
36 + addMissingMetrics(mx, "num.opcode.", queryOpcodes)
37 + addMissingMetrics(mx, "num.class.", queryClasses)
38 + addMissingMetrics(mx, "num.type.", queryTypes)
39 +
40 + return mx, nil
41 +}
42 +
43 +func (n *Nsd) collectStatsLine(mx map[string]int64, line string) {
44 + if line = strings.TrimSpace(line); line == "" {
45 + return
46 + }
47 +
48 + key, value, ok := strings.Cut(line, "=")
49 + if !ok {
50 + n.Debugf("invalid line in stats: '%s'", line)
51 + return
52 + }
53 +
54 + var v int64
55 + var f float64
56 + var err error
57 +
58 + switch key {
59 + case "time.boot":
60 + f, err = strconv.ParseFloat(value, 64)
61 + v = int64(f)
62 + default:
63 + v, err = strconv.ParseInt(value, 10, 64)
64 + }
65 +
66 + if err != nil {
67 + n.Debugf("invalid value in stats line '%s': '%s'", line, value)
68 + return
69 + }
70 +
71 + mx[key] = v
72 +}
73 +
74 +func addMissingMetrics(mx map[string]int64, prefix string, values []string) {
75 + for _, v := range values {
76 + k := prefix + v
77 + if _, ok := mx[k]; !ok {
78 + mx[k] = 0
79 + }
80 + }
81 +}
src/go/plugin/go.d/modules/nsd/config_schema.json new
+35
@@ -0,0 +1,35 @@
1 +{
2 + "jsonSchema": {
3 + "$schema": "http://json-schema.org/draft-07/schema#",
4 + "title": "NSD collector configuration.",
5 + "type": "object",
6 + "properties": {
7 + "update_every": {
8 + "title": "Update every",
9 + "description": "Data collection interval, measured in seconds.",
10 + "type": "integer",
11 + "minimum": 1,
12 + "default": 10
13 + },
14 + "timeout": {
15 + "title": "Timeout",
16 + "description": "Timeout for executing the binary, specified in seconds.",
17 + "type": "number",
18 + "minimum": 0.5,
19 + "default": 2
20 + }
21 + },
22 + "additionalProperties": false,
23 + "patternProperties": {
24 + "^name$": {}
25 + }
26 + },
27 + "uiSchema": {
28 + "uiOptions": {
29 + "fullPage": true
30 + },
31 + "timeout": {
32 + "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)."
33 + }
34 + }
35 +}
src/go/plugin/go.d/modules/nsd/exec.go new
+47
@@ -0,0 +1,47 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package nsd
4 +
5 +import (
6 + "context"
7 + "fmt"
8 + "os/exec"
9 + "time"
10 +
11 + "github.com/netdata/netdata/go/plugins/logger"
12 +)
13 +
14 +type nsdControlBinary interface {
15 + stats() ([]byte, error)
16 +}
17 +
18 +func newNsdControlExec(ndsudoPath string, timeout time.Duration, log *logger.Logger) *nsdControlExec {
19 + return &nsdControlExec{
20 + Logger: log,
21 + ndsudoPath: ndsudoPath,
22 + timeout: timeout,
23 + }
24 +}
25 +
26 +type nsdControlExec struct {
27 + *logger.Logger
28 +
29 + ndsudoPath string
30 + timeout time.Duration
31 +}
32 +
33 +func (e *nsdControlExec) stats() ([]byte, error) {
34 + ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
35 + defer cancel()
36 +
37 + cmd := exec.CommandContext(ctx, e.ndsudoPath, "nsd-control-stats")
38 +
39 + e.Debugf("executing '%s'", cmd)
40 +
41 + bs, err := cmd.Output()
42 + if err != nil {
43 + return nil, fmt.Errorf("error on '%s': %v", cmd, err)
44 + }
45 +
46 + return bs, nil
47 +}
src/go/plugin/go.d/modules/nsd/init.go new
+23
@@ -0,0 +1,23 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package nsd
4 +
5 +import (
6 + "fmt"
7 + "os"
8 + "path/filepath"
9 +
10 + "github.com/netdata/netdata/go/plugins/pkg/executable"
11 +)
12 +
13 +func (n *Nsd) initNsdControlExec() (nsdControlBinary, error) {
14 + ndsudoPath := filepath.Join(executable.Directory, "ndsudo")
15 + if _, err := os.Stat(ndsudoPath); err != nil {
16 + return nil, fmt.Errorf("ndsudo executable not found: %v", err)
17 +
18 + }
19 +
20 + nsdControl := newNsdControlExec(ndsudoPath, n.Timeout.Duration(), n.Logger)
21 +
22 + return nsdControl, nil
23 +}
src/go/plugin/go.d/modules/nsd/metadata.yaml new
+272
@@ -0,0 +1,272 @@
1 +plugin_name: go.d.plugin
2 +modules:
3 + - meta:
4 + id: collector-go.d.plugin-nsd
5 + plugin_name: go.d.plugin
6 + module_name: nsd
7 + monitored_instance:
8 + name: NSD
9 + link: "https://nsd.docs.nlnetlabs.nl/en/latest"
10 + icon_filename: 'nsd.svg'
11 + categories:
12 + - data-collection.dns-and-dhcp-servers
13 + keywords:
14 + - nsd
15 + - dns
16 + related_resources:
17 + integrations:
18 + list: []
19 + info_provided_to_referring_integrations:
20 + description: ""
21 + most_popular: false
22 + overview:
23 + data_collection:
24 + metrics_description: >
25 + This collector monitors NSD statistics like queries, zones, protocols, query types and more.
26 + It relies on the [`nsd-control`](https://nsd.docs.nlnetlabs.nl/en/latest/manpages/nsd-control.html) CLI tool but avoids directly executing the binary.
27 + Instead, it utilizes `ndsudo`, a Netdata helper specifically designed to run privileged commands securely within the Netdata environment.
28 + This approach eliminates the need to use `sudo`, improving security and potentially simplifying permission management.
29 +
30 + Executed commands:
31 +
32 + - `nsd-control stats_noreset`
33 + method_description: ""
34 + supported_platforms:
35 + include: []
36 + exclude: []
37 + multi_instance: false
38 + additional_permissions:
39 + description: ""
40 + default_behavior:
41 + auto_detection:
42 + description: ""
43 + limits:
44 + description: ""
45 + performance_impact:
46 + description: ""
47 + setup:
48 + prerequisites:
49 + list: []
50 + configuration:
51 + file:
52 + name: go.d/nsd.conf
53 + options:
54 + description: |
55 + The following options can be defined globally: update_every.
56 + folding:
57 + title: Config options
58 + enabled: true
59 + list:
60 + - name: update_every
61 + description: Data collection frequency.
62 + default_value: 10
63 + required: false
64 + - name: timeout
65 + description: nsd-control binary execution timeout.
66 + default_value: 2
67 + required: false
68 + examples:
69 + folding:
70 + title: Config
71 + enabled: true
72 + list:
73 + - name: Custom update_every
74 + description: Allows you to override the default data collection interval.
75 + config: |
76 + jobs:
77 + - name: nsd
78 + update_every: 5 # Collect logical volume statistics every 5 seconds
79 + troubleshooting:
80 + problems:
81 + list: []
82 + alerts: []
83 + metrics:
84 + folding:
85 + title: Metrics
86 + enabled: false
87 + description: ""
88 + availability: []
89 + scopes:
90 + - name: global
91 + description: These metrics refer to the the entire monitored application.
92 + labels: []
93 + metrics:
94 + - name: nsd.queries
95 + description: Queries
96 + unit: 'queries/s'
97 + chart_type: line
98 + dimensions:
99 + - name: queries
100 + - name: nsd.queries_by_type
101 + description: Queries Type
102 + unit: 'queries/s'
103 + chart_type: stacked
104 + dimensions:
105 + - name: "A"
106 + - name: "NS"
107 + - name: "MD"
108 + - name: "MF"
109 + - name: "CNAME"
110 + - name: "SOA"
111 + - name: "MB"
112 + - name: "MG"
113 + - name: "MR"
114 + - name: "NULL"
115 + - name: "WKS"
116 + - name: "PTR"
117 + - name: "HINFO"
118 + - name: "MINFO"
119 + - name: "MX"
120 + - name: "TXT"
121 + - name: "RP"
122 + - name: "AFSDB"
123 + - name: "X25"
124 + - name: "ISDN"
125 + - name: "RT"
126 + - name: "NSAP"
127 + - name: "SIG"
128 + - name: "KEY"
129 + - name: "PX"
130 + - name: "AAAA"
131 + - name: "LOC"
132 + - name: "NXT"
133 + - name: "SRV"
134 + - name: "NAPTR"
135 + - name: "KX"
136 + - name: "CERT"
137 + - name: "DNAME"
138 + - name: "OPT"
139 + - name: "APL"
140 + - name: "DS"
141 + - name: "SSHFP"
142 + - name: "IPSECKEY"
143 + - name: "RRSIG"
144 + - name: "NSEC"
145 + - name: "DNSKEY"
146 + - name: "DHCID"
147 + - name: "NSEC3"
148 + - name: "NSEC3PARAM"
149 + - name: "TLSA"
150 + - name: "SMIMEA"
151 + - name: "CDS"
152 + - name: "CDNSKEY"
153 + - name: "OPENPGPKEY"
154 + - name: "CSYNC"
155 + - name: "ZONEMD"
156 + - name: "SVCB"
157 + - name: "HTTPS"
158 + - name: "SPF"
159 + - name: "NID"
160 + - name: "L32"
161 + - name: "L64"
162 + - name: "LP"
163 + - name: "EUI48"
164 + - name: "EUI64"
165 + - name: "URI"
166 + - name: "CAA"
167 + - name: "AVC"
168 + - name: "DLV"
169 + - name: "IXFR"
170 + - name: "AXFR"
171 + - name: "MAILB"
172 + - name: "MAILA"
173 + - name: "ANY"
174 + - name: nsd.queries_by_opcode
175 + description: Queries Opcode
176 + unit: 'queries/s'
177 + chart_type: stacked
178 + dimensions:
179 + - name: "QUERY"
180 + - name: "IQUERY"
181 + - name: "STATUS"
182 + - name: "NOTIFY"
183 + - name: "UPDATE"
184 + - name: "OTHER"
185 + - name: nsd.queries_by_class
186 + description: Queries Class
187 + unit: 'queries/s'
188 + chart_type: stacked
189 + dimensions:
190 + - name: "IN"
191 + - name: "CS"
192 + - name: "CH"
193 + - name: "HS"
194 + - name: nsd.queries_by_protocol
195 + description: Queries Protocol
196 + unit: 'queries/s'
197 + chart_type: stacked
198 + dimensions:
199 + - name: "udp"
200 + - name: "udp6"
201 + - name: "tcp"
202 + - name: "tcp6"
203 + - name: "tls"
204 + - name: "tls6"
205 + - name: nsd.answers_by_rcode
206 + description: Answers Rcode
207 + unit: 'answers/s'
208 + chart_type: stacked
209 + dimensions:
210 + - name: "NOERROR"
211 + - name: "FORMERR"
212 + - name: "SERVFAIL"
213 + - name: "NXDOMAIN"
214 + - name: "NOTIMP"
215 + - name: "REFUSED"
216 + - name: "YXDOMAIN"
217 + - name: "YXRRSET"
218 + - name: "NXRRSET"
219 + - name: "NOTAUTH"
220 + - name: "NOTZONE"
221 + - name: "RCODE11"
222 + - name: "RCODE12"
223 + - name: "RCODE13"
224 + - name: "RCODE14"
225 + - name: "RCODE15"
226 + - name: "BADVERS"
227 + - name: nsd.errors
228 + description: Errors
229 + unit: 'errors/s'
230 + chart_type: line
231 + dimensions:
232 + - name: "query"
233 + - name: "answer"
234 + - name: nsd.drops
235 + description: Drops
236 + unit: 'drops/s'
237 + chart_type: line
238 + dimensions:
239 + - name: "query"
240 + - name: nsd.zones
241 + description: Zones
242 + unit: 'zones'
243 + chart_type: line
244 + dimensions:
245 + - name: "master"
246 + - name: "slave"
247 + - name: nsd.zone_transfers_requests
248 + description: Zone Transfers
249 + unit: 'requests/s'
250 + chart_type: line
251 + dimensions:
252 + - name: "AXFR"
253 + - name: "IXFR"
254 + - name: nsd.zone_transfer_memory
255 + description: Zone Transfer Memory
256 + unit: 'bytes'
257 + chart_type: line
258 + dimensions:
259 + - name: "used"
260 + - name: nsd.database_size
261 + description: Database Size
262 + unit: 'bytes'
263 + chart_type: line
264 + dimensions:
265 + - name: "disk"
266 + - name: "mem"
267 + - name: nsd.uptime
268 + description: Uptime
269 + unit: 'seconds'
270 + chart_type: line
271 + dimensions:
272 + - name: "uptime"
src/go/plugin/go.d/modules/nsd/nsd.go new
+97
@@ -0,0 +1,97 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package nsd
4 +
5 +import (
6 + _ "embed"
7 + "errors"
8 + "time"
9 +
10 + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
11 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/web"
12 +)
13 +
14 +//go:embed "config_schema.json"
15 +var configSchema string
16 +
17 +func init() {
18 + module.Register("nsd", module.Creator{
19 + JobConfigSchema: configSchema,
20 + Defaults: module.Defaults{
21 + UpdateEvery: 10,
22 + },
23 + Create: func() module.Module { return New() },
24 + Config: func() any { return &Config{} },
25 + })
26 +}
27 +
28 +func New() *Nsd {
29 + return &Nsd{
30 + Config: Config{
31 + Timeout: web.Duration(time.Second * 2),
32 + },
33 + charts: charts.Copy(),
34 + }
35 +}
36 +
37 +type Config struct {
38 + UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
39 + Timeout web.Duration `yaml:"timeout,omitempty" json:"timeout"`
40 +}
41 +
42 +type Nsd struct {
43 + module.Base
44 + Config `yaml:",inline" json:""`
45 +
46 + charts *module.Charts
47 +
48 + exec nsdControlBinary
49 +}
50 +
51 +func (n *Nsd) Configuration() any {
52 + return n.Config
53 +}
54 +
55 +func (n *Nsd) Init() error {
56 + nsdControl, err := n.initNsdControlExec()
57 + if err != nil {
58 + n.Errorf("nvm-control exec initialization: %v", err)
59 + return err
60 + }
61 + n.exec = nsdControl
62 +
63 + return nil
64 +}
65 +
66 +func (n *Nsd) Check() error {
67 + mx, err := n.collect()
68 + if err != nil {
69 + n.Error(err)
70 + return err
71 + }
72 +
73 + if len(mx) == 0 {
74 + return errors.New("no metrics collected")
75 + }
76 +
77 + return nil
78 +}
79 +
80 +func (n *Nsd) Charts() *module.Charts {
81 + return n.charts
82 +}
83 +
84 +func (n *Nsd) Collect() map[string]int64 {
85 + mx, err := n.collect()
86 + if err != nil {
87 + n.Error(err)
88 + }
89 +
90 + if len(mx) == 0 {
91 + return nil
92 + }
93 +
94 + return mx
95 +}
96 +
97 +func (n *Nsd) Cleanup() {}
src/go/plugin/go.d/modules/nsd/nsd_test.go new
+337
@@ -0,0 +1,337 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package nsd
4 +
5 +import (
6 + "errors"
7 + "os"
8 + "testing"
9 +
10 + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
11 +
12 + "github.com/stretchr/testify/assert"
13 + "github.com/stretchr/testify/require"
14 +)
15 +
16 +var (
17 + dataConfigJSON, _ = os.ReadFile("testdata/config.json")
18 + dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")
19 +
20 + dataStats, _ = os.ReadFile("testdata/stats.txt")
21 +)
22 +
23 +func Test_testDataIsValid(t *testing.T) {
24 + for name, data := range map[string][]byte{
25 + "dataConfigJSON": dataConfigJSON,
26 + "dataConfigYAML": dataConfigYAML,
27 + "dataStats": dataStats,
28 + } {
29 + require.NotNil(t, data, name)
30 +
31 + }
32 +}
33 +
34 +func TestNsd_Configuration(t *testing.T) {
35 + module.TestConfigurationSerialize(t, &Nsd{}, dataConfigJSON, dataConfigYAML)
36 +}
37 +
38 +func TestNsd_Init(t *testing.T) {
39 + tests := map[string]struct {
40 + config Config
41 + wantFail bool
42 + }{
43 + "fails if failed to locate ndsudo": {
44 + wantFail: true,
45 + config: New().Config,
46 + },
47 + }
48 +
49 + for name, test := range tests {
50 + t.Run(name, func(t *testing.T) {
51 + nsd := New()
52 + nsd.Config = test.config
53 +
54 + if test.wantFail {
55 + assert.Error(t, nsd.Init())
56 + } else {
57 + assert.NoError(t, nsd.Init())
58 + }
59 + })
60 + }
61 +}
62 +
63 +func TestNsd_Cleanup(t *testing.T) {
64 + tests := map[string]struct {
65 + prepare func() *Nsd
66 + }{
67 + "not initialized exec": {
68 + prepare: func() *Nsd {
69 + return New()
70 + },
71 + },
72 + "after check": {
73 + prepare: func() *Nsd {
74 + nsd := New()
75 + nsd.exec = prepareMockOK()
76 + _ = nsd.Check()
77 + return nsd
78 + },
79 + },
80 + "after collect": {
81 + prepare: func() *Nsd {
82 + nsd := New()
83 + nsd.exec = prepareMockOK()
84 + _ = nsd.Collect()
85 + return nsd
86 + },
87 + },
88 + }
89 +
90 + for name, test := range tests {
91 + t.Run(name, func(t *testing.T) {
92 + nsd := test.prepare()
93 +
94 + assert.NotPanics(t, nsd.Cleanup)
95 + })
96 + }
97 +}
98 +
99 +func TestNsd_Charts(t *testing.T) {
100 + assert.NotNil(t, New().Charts())
101 +}
102 +
103 +func TestNsd_Check(t *testing.T) {
104 + tests := map[string]struct {
105 + prepareMock func() *mockNsdControl
106 + wantFail bool
107 + }{
108 + "success case": {
109 + prepareMock: prepareMockOK,
110 + wantFail: false,
111 + },
112 + "error on stats call": {
113 + prepareMock: prepareMockErrOnStats,
114 + wantFail: true,
115 + },
116 + "empty response": {
117 + prepareMock: prepareMockEmptyResponse,
118 + wantFail: true,
119 + },
120 + "unexpected response": {
121 + prepareMock: prepareMockUnexpectedResponse,
122 + wantFail: true,
123 + },
124 + }
125 +
126 + for name, test := range tests {
127 + t.Run(name, func(t *testing.T) {
128 + nsd := New()
129 + mock := test.prepareMock()
130 + nsd.exec = mock
131 +
132 + if test.wantFail {
133 + assert.Error(t, nsd.Check())
134 + } else {
135 + assert.NoError(t, nsd.Check())
136 + }
137 + })
138 + }
139 +}
140 +
141 +func TestNsd_Collect(t *testing.T) {
142 + tests := map[string]struct {
143 + prepareMock func() *mockNsdControl
144 + wantMetrics map[string]int64
145 + }{
146 + "success case": {
147 + prepareMock: prepareMockOK,
148 + wantMetrics: map[string]int64{
149 + "num.answer_wo_aa": 1,
150 + "num.class.CH": 0,
151 + "num.class.CS": 0,
152 + "num.class.HS": 0,
153 + "num.class.IN": 1,
154 + "num.dropped": 1,
155 + "num.edns": 1,
156 + "num.ednserr": 1,
157 + "num.opcode.IQUERY": 0,
158 + "num.opcode.NOTIFY": 0,
159 + "num.opcode.OTHER": 0,
160 + "num.opcode.QUERY": 1,
161 + "num.opcode.STATUS": 0,
162 + "num.opcode.UPDATE": 0,
163 + "num.queries": 1,
164 + "num.raxfr": 1,
165 + "num.rcode.BADVERS": 0,
166 + "num.rcode.FORMERR": 1,
167 + "num.rcode.NOERROR": 1,
168 + "num.rcode.NOTAUTH": 0,
169 + "num.rcode.NOTIMP": 1,
170 + "num.rcode.NOTZONE": 0,
171 + "num.rcode.NXDOMAIN": 1,
172 + "num.rcode.NXRRSET": 0,
173 + "num.rcode.RCODE11": 0,
174 + "num.rcode.RCODE12": 0,
175 + "num.rcode.RCODE13": 0,
176 + "num.rcode.RCODE14": 0,
177 + "num.rcode.RCODE15": 0,
178 + "num.rcode.REFUSED": 1,
179 + "num.rcode.SERVFAIL": 1,
180 + "num.rcode.YXDOMAIN": 1,
181 + "num.rcode.YXRRSET": 0,
182 + "num.rixfr": 1,
183 + "num.rxerr": 1,
184 + "num.tcp": 1,
185 + "num.tcp6": 1,
186 + "num.tls": 1,
187 + "num.tls6": 1,
188 + "num.truncated": 1,
189 + "num.txerr": 1,
190 + "num.type.A": 1,
191 + "num.type.AAAA": 1,
192 + "num.type.AFSDB": 1,
193 + "num.type.APL": 1,
194 + "num.type.AVC": 0,
195 + "num.type.CAA": 0,
196 + "num.type.CDNSKEY": 1,
197 + "num.type.CDS": 1,
198 + "num.type.CERT": 1,
199 + "num.type.CNAME": 1,
200 + "num.type.CSYNC": 1,
201 + "num.type.DHCID": 1,
202 + "num.type.DLV": 0,
203 + "num.type.DNAME": 1,
204 + "num.type.DNSKEY": 1,
205 + "num.type.DS": 1,
206 + "num.type.EUI48": 1,
207 + "num.type.EUI64": 1,
208 + "num.type.HINFO": 1,
209 + "num.type.HTTPS": 1,
210 + "num.type.IPSECKEY": 1,
211 + "num.type.ISDN": 1,
212 + "num.type.KEY": 1,
213 + "num.type.KX": 1,
214 + "num.type.L32": 1,
215 + "num.type.L64": 1,
216 + "num.type.LOC": 1,
217 + "num.type.LP": 1,
218 + "num.type.MB": 1,
219 + "num.type.MD": 1,
220 + "num.type.MF": 1,
221 + "num.type.MG": 1,
222 + "num.type.MINFO": 1,
223 + "num.type.MR": 1,
224 + "num.type.MX": 1,
225 + "num.type.NAPTR": 1,
226 + "num.type.NID": 1,
227 + "num.type.NS": 1,
228 + "num.type.NSAP": 1,
229 + "num.type.NSEC": 1,
230 + "num.type.NSEC3": 1,
231 + "num.type.NSEC3PARAM": 1,
232 + "num.type.NULL": 1,
233 + "num.type.NXT": 1,
234 + "num.type.OPENPGPKEY": 1,
235 + "num.type.OPT": 1,
236 + "num.type.PTR": 1,
237 + "num.type.PX": 1,
238 + "num.type.RP": 1,
239 + "num.type.RRSIG": 1,
240 + "num.type.RT": 1,
241 + "num.type.SIG": 1,
242 + "num.type.SMIMEA": 1,
243 + "num.type.SOA": 1,
244 + "num.type.SPF": 1,
245 + "num.type.SRV": 1,
246 + "num.type.SSHFP": 1,
247 + "num.type.SVCB": 1,
248 + "num.type.TLSA": 1,
249 + "num.type.TXT": 1,
250 + "num.type.TYPE252": 0,
251 + "num.type.TYPE255": 0,
252 + "num.type.URI": 0,
253 + "num.type.WKS": 1,
254 + "num.type.X25": 1,
255 + "num.type.ZONEMD": 1,
256 + "num.udp": 1,
257 + "num.udp6": 1,
258 + "server0.queries": 1,
259 + "size.config.disk": 1,
260 + "size.config.mem": 1064,
261 + "size.db.disk": 576,
262 + "size.db.mem": 920,
263 + "size.xfrd.mem": 1160464,
264 + "time.boot": 556,
265 + "zone.master": 1,
266 + "zone.slave": 1,
267 + },
268 + },
269 + "error on lvs report call": {
270 + prepareMock: prepareMockErrOnStats,
271 + wantMetrics: nil,
272 + },
273 + "empty response": {
274 + prepareMock: prepareMockEmptyResponse,
275 + wantMetrics: nil,
276 + },
277 + "unexpected response": {
278 + prepareMock: prepareMockUnexpectedResponse,
279 + wantMetrics: nil,
280 + },
281 + }
282 +
283 + for name, test := range tests {
284 + t.Run(name, func(t *testing.T) {
285 + nsd := New()
286 + mock := test.prepareMock()
287 + nsd.exec = mock
288 +
289 + mx := nsd.Collect()
290 +
291 + assert.Equal(t, test.wantMetrics, mx)
292 +
293 + if len(test.wantMetrics) > 0 {
294 + assert.Len(t, *nsd.Charts(), len(charts))
295 + module.TestMetricsHasAllChartsDims(t, nsd.Charts(), mx)
296 + }
297 + })
298 + }
299 +}
300 +
301 +func prepareMockOK() *mockNsdControl {
302 + return &mockNsdControl{
303 + dataStats: dataStats,
304 + }
305 +}
306 +
307 +func prepareMockErrOnStats() *mockNsdControl {
308 + return &mockNsdControl{
309 + errOnStatus: true,
310 + }
311 +}
312 +
313 +func prepareMockEmptyResponse() *mockNsdControl {
314 + return &mockNsdControl{}
315 +}
316 +
317 +func prepareMockUnexpectedResponse() *mockNsdControl {
318 + return &mockNsdControl{
319 + dataStats: []byte(`
320 +Lorem ipsum dolor sit amet, consectetur adipiscing elit.
321 +Nulla malesuada erat id magna mattis, eu viverra tellus rhoncus.
322 +Fusce et felis pulvinar, posuere sem non, porttitor eros.
323 +`),
324 + }
325 +}
326 +
327 +type mockNsdControl struct {
328 + errOnStatus bool
329 + dataStats []byte
330 +}
331 +
332 +func (m *mockNsdControl) stats() ([]byte, error) {
333 + if m.errOnStatus {
334 + return nil, errors.New("mock.status() error")
335 + }
336 + return m.dataStats, nil
337 +}
src/go/plugin/go.d/modules/nsd/stats_counters.go new
+123
@@ -0,0 +1,123 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package nsd
4 +
5 +// Docs: https://nsd.docs.nlnetlabs.nl/en/latest/manpages/nsd-control.html?highlight=elapsed#statistics-counters
6 +// Source: https://github.com/NLnetLabs/nsd/blob/b4a5ccd2235a1f8f71f7c640390e409bf123c963/remote.c#L2735
7 +
8 +// https://github.com/NLnetLabs/nsd/blob/b4a5ccd2235a1f8f71f7c640390e409bf123c963/remote.c#L2737
9 +var answerRcodes = []string{
10 + "NOERROR",
11 + "FORMERR",
12 + "SERVFAIL",
13 + "NXDOMAIN",
14 + "NOTIMP",
15 + "REFUSED",
16 + "YXDOMAIN",
17 + "YXRRSET",
18 + "NXRRSET",
19 + "NOTAUTH",
20 + "NOTZONE",
21 + "RCODE11",
22 + "RCODE12",
23 + "RCODE13",
24 + "RCODE14",
25 + "RCODE15",
26 + "BADVERS",
27 +}
28 +
29 +// https://github.com/NLnetLabs/nsd/blob/b4a5ccd2235a1f8f71f7c640390e409bf123c963/remote.c#L2706
30 +var queryOpcodes = []string{
31 + "QUERY",
32 + "IQUERY",
33 + "STATUS",
34 + "NOTIFY",
35 + "UPDATE",
36 + "OTHER",
37 +}
38 +
39 +// https://github.com/NLnetLabs/nsd/blob/b4a5ccd2235a1f8f71f7c640390e409bf123c963/dns.c#L27
40 +var queryClasses = []string{
41 + "IN",
42 + "CS",
43 + "CH",
44 + "HS",
45 +}
46 +
47 +// https://github.com/NLnetLabs/nsd/blob/b4a5ccd2235a1f8f71f7c640390e409bf123c963/dns.c#L35
48 +var queryTypes = []string{
49 + "A",
50 + "NS",
51 + "MD",
52 + "MF",
53 + "CNAME",
54 + "SOA",
55 + "MB",
56 + "MG",
57 + "MR",
58 + "NULL",
59 + "WKS",
60 + "PTR",
61 + "HINFO",
62 + "MINFO",
63 + "MX",
64 + "TXT",
65 + "RP",
66 + "AFSDB",
67 + "X25",
68 + "ISDN",
69 + "RT",
70 + "NSAP",
71 + "SIG",
72 + "KEY",
73 + "PX",
74 + "AAAA",
75 + "LOC",
76 + "NXT",
77 + "SRV",
78 + "NAPTR",
79 + "KX",
80 + "CERT",
81 + "DNAME",
82 + "OPT",
83 + "APL",
84 + "DS",
85 + "SSHFP",
86 + "IPSECKEY",
87 + "RRSIG",
88 + "NSEC",
89 + "DNSKEY",
90 + "DHCID",
91 + "NSEC3",
92 + "NSEC3PARAM",
93 + "TLSA",
94 + "SMIMEA",
95 + "CDS",
96 + "CDNSKEY",
97 + "OPENPGPKEY",
98 + "CSYNC",
99 + "ZONEMD",
100 + "SVCB",
101 + "HTTPS",
102 + "SPF",
103 + "NID",
104 + "L32",
105 + "L64",
106 + "LP",
107 + "EUI48",
108 + "EUI64",
109 + "URI",
110 + "CAA",
111 + "AVC",
112 + "DLV",
113 + "TYPE252",
114 + "TYPE255",
115 +}
116 +
117 +var queryTypeNumberMap = map[string]string{
118 + "TYPE251": "IXFR",
119 + "TYPE252": "AXFR",
120 + "TYPE253": "MAILB",
121 + "TYPE254": "MAILA",
122 + "TYPE255": "ANY",
123 +}
src/go/plugin/go.d/modules/nsd/testdata/config.json new
+4
@@ -0,0 +1,4 @@
1 +{
2 + "update_every": 123,
3 + "timeout": 123.123
4 +}
src/go/plugin/go.d/modules/nsd/testdata/config.yaml new
+2
@@ -0,0 +1,2 @@
1 +update_every: 123
2 +timeout: 123.123
src/go/plugin/go.d/modules/nsd/testdata/stats.txt new
+95
@@ -0,0 +1,95 @@
1 +server0.queries=1
2 +num.queries=1
3 +time.boot=556.488415
4 +time.elapsed=556.488415
5 +size.db.disk=576
6 +size.db.mem=920
7 +size.xfrd.mem=1160464
8 +size.config.disk=1
9 +size.config.mem=1064
10 +num.type.A=1
11 +num.type.NS=1
12 +num.type.MD=1
13 +num.type.MF=1
14 +num.type.CNAME=1
15 +num.type.SOA=1
16 +num.type.MB=1
17 +num.type.MG=1
18 +num.type.MR=1
19 +num.type.NULL=1
20 +num.type.WKS=1
21 +num.type.PTR=1
22 +num.type.HINFO=1
23 +num.type.MINFO=1
24 +num.type.MX=1
25 +num.type.TXT=1
26 +num.type.RP=1
27 +num.type.AFSDB=1
28 +num.type.X25=1
29 +num.type.ISDN=1
30 +num.type.RT=1
31 +num.type.NSAP=1
32 +num.type.SIG=1
33 +num.type.KEY=1
34 +num.type.PX=1
35 +num.type.AAAA=1
36 +num.type.LOC=1
37 +num.type.NXT=1
38 +num.type.SRV=1
39 +num.type.NAPTR=1
40 +num.type.KX=1
41 +num.type.CERT=1
42 +num.type.DNAME=1
43 +num.type.OPT=1
44 +num.type.APL=1
45 +num.type.DS=1
46 +num.type.SSHFP=1
47 +num.type.IPSECKEY=1
48 +num.type.RRSIG=1
49 +num.type.NSEC=1
50 +num.type.DNSKEY=1
51 +num.type.DHCID=1
52 +num.type.NSEC3=1
53 +num.type.NSEC3PARAM=1
54 +num.type.TLSA=1
55 +num.type.SMIMEA=1
56 +num.type.CDS=1
57 +num.type.CDNSKEY=1
58 +num.type.OPENPGPKEY=1
59 +num.type.CSYNC=1
60 +num.type.ZONEMD=1
61 +num.type.SVCB=1
62 +num.type.HTTPS=1
63 +num.type.SPF=1
64 +num.type.NID=1
65 +num.type.L32=1
66 +num.type.L64=1
67 +num.type.LP=1
68 +num.type.EUI48=1
69 +num.type.EUI64=1
70 +num.opcode.QUERY=1
71 +num.class.IN=1
72 +num.rcode.NOERROR=1
73 +num.rcode.FORMERR=1
74 +num.rcode.SERVFAIL=1
75 +num.rcode.NXDOMAIN=1
76 +num.rcode.NOTIMP=1
77 +num.rcode.REFUSED=1
78 +num.rcode.YXDOMAIN=1
79 +num.edns=1
80 +num.ednserr=1
81 +num.udp=1
82 +num.udp6=1
83 +num.tcp=1
84 +num.tcp6=1
85 +num.tls=1
86 +num.tls6=1
87 +num.answer_wo_aa=1
88 +num.rxerr=1
89 +num.txerr=1
90 +num.raxfr=1
91 +num.rixfr=1
92 +num.truncated=1
93 +num.dropped=1
94 +zone.master=1
95 +zone.slave=1