diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-08-26 08:15:24 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-08-26 08:15:35 +0000 |
commit | f09848204fa5283d21ea43e262ee41aa578e1808 (patch) | |
tree | c62385d7adf209fa6a798635954d887f718fb3fb /src/go/plugin/go.d/modules/nsd | |
parent | Releasing debian version 1.46.3-2. (diff) | |
download | netdata-f09848204fa5283d21ea43e262ee41aa578e1808.tar.xz netdata-f09848204fa5283d21ea43e262ee41aa578e1808.zip |
Merging upstream version 1.47.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/go/plugin/go.d/modules/nsd')
l--------- | src/go/plugin/go.d/modules/nsd/README.md | 1 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/nsd/charts.go | 249 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/nsd/collect.go | 81 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/nsd/config_schema.json | 35 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/nsd/exec.go | 47 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/nsd/init.go | 23 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/nsd/integrations/nsd.md | 203 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/nsd/metadata.yaml | 272 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/nsd/nsd.go | 97 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/nsd/nsd_test.go | 337 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/nsd/stats_counters.go | 123 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/nsd/testdata/config.json | 4 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/nsd/testdata/config.yaml | 2 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/nsd/testdata/stats.txt | 95 |
14 files changed, 1569 insertions, 0 deletions
diff --git a/src/go/plugin/go.d/modules/nsd/README.md b/src/go/plugin/go.d/modules/nsd/README.md new file mode 120000 index 000000000..a5cb8c98b --- /dev/null +++ b/src/go/plugin/go.d/modules/nsd/README.md @@ -0,0 +1 @@ +integrations/nsd.md
\ No newline at end of file diff --git a/src/go/plugin/go.d/modules/nsd/charts.go b/src/go/plugin/go.d/modules/nsd/charts.go new file mode 100644 index 000000000..aed4f3098 --- /dev/null +++ b/src/go/plugin/go.d/modules/nsd/charts.go @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package nsd + +import ( + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module" +) + +const ( + prioQueries = module.Priority + iota + prioQueriesByType + prioQueriesByOpcode + prioQueriesByClass + prioQueriesByProtocol + + prioAnswersByRcode + + prioErrors + + prioDrops + + prioZones + prioZoneTransfersRequests + prioZoneTransferMemory + + prioDatabaseSize + + prioUptime +) + +var charts = module.Charts{ + queriesChart.Copy(), + queriesByTypeChart.Copy(), + queriesByOpcodeChart.Copy(), + queriesByClassChart.Copy(), + queriesByProtocolChart.Copy(), + + answersByRcodeChart.Copy(), + + zonesChart.Copy(), + zoneTransfersRequestsChart.Copy(), + zoneTransferMemoryChart.Copy(), + + databaseSizeChart.Copy(), + + errorsChart.Copy(), + + dropsChart.Copy(), + + uptimeChart.Copy(), +} + +var ( + queriesChart = module.Chart{ + ID: "queries", + Title: "Queries", + Units: "queries/s", + Fam: "queries", + Ctx: "nsd.queries", + Priority: prioQueries, + Dims: module.Dims{ + {ID: "num.queries", Name: "queries", Algo: module.Incremental}, + }, + } + queriesByTypeChart = func() module.Chart { + chart := module.Chart{ + ID: "queries_by_type", + Title: "Queries Type", + Units: "queries/s", + Fam: "queries", + Ctx: "nsd.queries_by_type", + Priority: prioQueriesByType, + Type: module.Stacked, + } + for _, v := range queryTypes { + name := v + if s, ok := queryTypeNumberMap[v]; ok { + name = s + } + chart.Dims = append(chart.Dims, &module.Dim{ + ID: "num.type." + v, + Name: name, + Algo: module.Incremental, + }) + } + return chart + }() + queriesByOpcodeChart = func() module.Chart { + chart := module.Chart{ + ID: "queries_by_opcode", + Title: "Queries Opcode", + Units: "queries/s", + Fam: "queries", + Ctx: "nsd.queries_by_opcode", + Priority: prioQueriesByOpcode, + Type: module.Stacked, + } + for _, v := range queryOpcodes { + chart.Dims = append(chart.Dims, &module.Dim{ + ID: "num.opcode." + v, + Name: v, + Algo: module.Incremental, + }) + } + return chart + }() + queriesByClassChart = func() module.Chart { + chart := module.Chart{ + ID: "queries_by_class", + Title: "Queries Class", + Units: "queries/s", + Fam: "queries", + Ctx: "nsd.queries_by_class", + Priority: prioQueriesByClass, + Type: module.Stacked, + } + for _, v := range queryClasses { + chart.Dims = append(chart.Dims, &module.Dim{ + ID: "num.class." + v, + Name: v, + Algo: module.Incremental, + }) + } + return chart + }() + queriesByProtocolChart = module.Chart{ + ID: "queries_by_protocol", + Title: "Queries Protocol", + Units: "queries/s", + Fam: "queries", + Ctx: "nsd.queries_by_protocol", + Priority: prioQueriesByProtocol, + Type: module.Stacked, + Dims: module.Dims{ + {ID: "num.udp", Name: "udp", Algo: module.Incremental}, + {ID: "num.udp6", Name: "udp6", Algo: module.Incremental}, + {ID: "num.tcp", Name: "tcp", Algo: module.Incremental}, + {ID: "num.tcp6", Name: "tcp6", Algo: module.Incremental}, + {ID: "num.tls", Name: "tls", Algo: module.Incremental}, + {ID: "num.tls6", Name: "tls6", Algo: module.Incremental}, + }, + } + + answersByRcodeChart = func() module.Chart { + chart := module.Chart{ + ID: "answers_by_rcode", + Title: "Answers Rcode", + Units: "answers/s", + Fam: "answers", + Ctx: "nsd.answers_by_rcode", + Priority: prioAnswersByRcode, + Type: module.Stacked, + } + for _, v := range answerRcodes { + chart.Dims = append(chart.Dims, &module.Dim{ + ID: "num.rcode." + v, + Name: v, + Algo: module.Incremental, + }) + } + return chart + }() + + errorsChart = module.Chart{ + ID: "errors", + Title: "Errors", + Units: "errors/s", + Fam: "errors", + Ctx: "nsd.errors", + Priority: prioErrors, + Dims: module.Dims{ + {ID: "num.rxerr", Name: "query", Algo: module.Incremental}, + {ID: "num.txerr", Name: "answer", Mul: -1, Algo: module.Incremental}, + }, + } + + dropsChart = module.Chart{ + ID: "drops", + Title: "Drops", + Units: "drops/s", + Fam: "drops", + Ctx: "nsd.drops", + Priority: prioDrops, + Dims: module.Dims{ + {ID: "num.dropped", Name: "query", Algo: module.Incremental}, + }, + } + + zonesChart = module.Chart{ + ID: "zones", + Title: "Zones", + Units: "zones", + Fam: "zones", + Ctx: "nsd.zones", + Priority: prioZones, + Dims: module.Dims{ + {ID: "zone.master", Name: "master"}, + {ID: "zone.slave", Name: "slave"}, + }, + } + zoneTransfersRequestsChart = module.Chart{ + ID: "zone_transfers_requests", + Title: "Zone Transfers", + Units: "requests/s", + Fam: "zones", + Ctx: "nsd.zone_transfers_requests", + Priority: prioZoneTransfersRequests, + Dims: module.Dims{ + {ID: "num.raxfr", Name: "AXFR", Algo: module.Incremental}, + {ID: "num.rixfr", Name: "IXFR", Algo: module.Incremental}, + }, + } + zoneTransferMemoryChart = module.Chart{ + ID: "zone_transfer_memory", + Title: "Zone Transfer Memory", + Units: "bytes", + Fam: "zones", + Ctx: "nsd.zone_transfer_memory", + Priority: prioZoneTransferMemory, + Dims: module.Dims{ + {ID: "size.xfrd.mem", Name: "used"}, + }, + } + + databaseSizeChart = module.Chart{ + ID: "database_size", + Title: "Database Size", + Units: "bytes", + Fam: "database", + Ctx: "nsd.database_size", + Priority: prioDatabaseSize, + Dims: module.Dims{ + {ID: "size.db.disk", Name: "disk"}, + {ID: "size.db.mem", Name: "mem"}, + }, + } + + uptimeChart = module.Chart{ + ID: "uptime", + Title: "Uptime", + Units: "seconds", + Fam: "uptime", + Ctx: "nsd.uptime", + Priority: prioUptime, + Dims: module.Dims{ + {ID: "time.boot", Name: "uptime"}, + }, + } +) diff --git a/src/go/plugin/go.d/modules/nsd/collect.go b/src/go/plugin/go.d/modules/nsd/collect.go new file mode 100644 index 000000000..d07341df3 --- /dev/null +++ b/src/go/plugin/go.d/modules/nsd/collect.go @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package nsd + +import ( + "bufio" + "bytes" + "errors" + "strconv" + "strings" +) + +func (n *Nsd) collect() (map[string]int64, error) { + stats, err := n.exec.stats() + if err != nil { + return nil, err + } + + if len(stats) == 0 { + return nil, errors.New("empty stats response") + } + + mx := make(map[string]int64) + + sc := bufio.NewScanner(bytes.NewReader(stats)) + + for sc.Scan() { + n.collectStatsLine(mx, sc.Text()) + } + + if len(mx) == 0 { + return nil, errors.New("unexpected stats response: no metrics found") + } + + addMissingMetrics(mx, "num.rcode.", answerRcodes) + addMissingMetrics(mx, "num.opcode.", queryOpcodes) + addMissingMetrics(mx, "num.class.", queryClasses) + addMissingMetrics(mx, "num.type.", queryTypes) + + return mx, nil +} + +func (n *Nsd) collectStatsLine(mx map[string]int64, line string) { + if line = strings.TrimSpace(line); line == "" { + return + } + + key, value, ok := strings.Cut(line, "=") + if !ok { + n.Debugf("invalid line in stats: '%s'", line) + return + } + + var v int64 + var f float64 + var err error + + switch key { + case "time.boot": + f, err = strconv.ParseFloat(value, 64) + v = int64(f) + default: + v, err = strconv.ParseInt(value, 10, 64) + } + + if err != nil { + n.Debugf("invalid value in stats line '%s': '%s'", line, value) + return + } + + mx[key] = v +} + +func addMissingMetrics(mx map[string]int64, prefix string, values []string) { + for _, v := range values { + k := prefix + v + if _, ok := mx[k]; !ok { + mx[k] = 0 + } + } +} diff --git a/src/go/plugin/go.d/modules/nsd/config_schema.json b/src/go/plugin/go.d/modules/nsd/config_schema.json new file mode 100644 index 000000000..d49107c71 --- /dev/null +++ b/src/go/plugin/go.d/modules/nsd/config_schema.json @@ -0,0 +1,35 @@ +{ + "jsonSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NSD collector configuration.", + "type": "object", + "properties": { + "update_every": { + "title": "Update every", + "description": "Data collection interval, measured in seconds.", + "type": "integer", + "minimum": 1, + "default": 10 + }, + "timeout": { + "title": "Timeout", + "description": "Timeout for executing the binary, specified in seconds.", + "type": "number", + "minimum": 0.5, + "default": 2 + } + }, + "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)." + } + } +} diff --git a/src/go/plugin/go.d/modules/nsd/exec.go b/src/go/plugin/go.d/modules/nsd/exec.go new file mode 100644 index 000000000..b05082f3c --- /dev/null +++ b/src/go/plugin/go.d/modules/nsd/exec.go @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package nsd + +import ( + "context" + "fmt" + "os/exec" + "time" + + "github.com/netdata/netdata/go/plugins/logger" +) + +type nsdControlBinary interface { + stats() ([]byte, error) +} + +func newNsdControlExec(ndsudoPath string, timeout time.Duration, log *logger.Logger) *nsdControlExec { + return &nsdControlExec{ + Logger: log, + ndsudoPath: ndsudoPath, + timeout: timeout, + } +} + +type nsdControlExec struct { + *logger.Logger + + ndsudoPath string + timeout time.Duration +} + +func (e *nsdControlExec) stats() ([]byte, error) { + ctx, cancel := context.WithTimeout(context.Background(), e.timeout) + defer cancel() + + cmd := exec.CommandContext(ctx, e.ndsudoPath, "nsd-control-stats") + + e.Debugf("executing '%s'", cmd) + + bs, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("error on '%s': %v", cmd, err) + } + + return bs, nil +} diff --git a/src/go/plugin/go.d/modules/nsd/init.go b/src/go/plugin/go.d/modules/nsd/init.go new file mode 100644 index 000000000..63843caba --- /dev/null +++ b/src/go/plugin/go.d/modules/nsd/init.go @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package nsd + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/netdata/netdata/go/plugins/pkg/executable" +) + +func (n *Nsd) initNsdControlExec() (nsdControlBinary, error) { + ndsudoPath := filepath.Join(executable.Directory, "ndsudo") + if _, err := os.Stat(ndsudoPath); err != nil { + return nil, fmt.Errorf("ndsudo executable not found: %v", err) + + } + + nsdControl := newNsdControlExec(ndsudoPath, n.Timeout.Duration(), n.Logger) + + return nsdControl, nil +} diff --git a/src/go/plugin/go.d/modules/nsd/integrations/nsd.md b/src/go/plugin/go.d/modules/nsd/integrations/nsd.md new file mode 100644 index 000000000..745b872d7 --- /dev/null +++ b/src/go/plugin/go.d/modules/nsd/integrations/nsd.md @@ -0,0 +1,203 @@ +<!--startmeta +custom_edit_url: "https://github.com/netdata/netdata/edit/master/src/go/plugin/go.d/modules/nsd/README.md" +meta_yaml: "https://github.com/netdata/netdata/edit/master/src/go/plugin/go.d/modules/nsd/metadata.yaml" +sidebar_label: "NSD" +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--> + +# NSD + + +<img src="https://netdata.cloud/img/nsd.svg" width="150"/> + + +Plugin: go.d.plugin +Module: nsd + +<img src="https://img.shields.io/badge/maintained%20by-Netdata-%2300ab44" /> + +## Overview + +This collector monitors NSD statistics like queries, zones, protocols, query types and more. 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. Instead, it utilizes `ndsudo`, a Netdata helper specifically designed to run privileged commands securely within the Netdata environment. This approach eliminates the need to use `sudo`, improving security and potentially simplifying permission management. +Executed commands: +- `nsd-control stats_noreset` + + + + +This collector is supported on all platforms. + +This collector only supports collecting metrics from a single instance of this integration. + + +### 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 NSD instance + +These metrics refer to the the entire monitored application. + +This scope has no labels. + +Metrics: + +| Metric | Dimensions | Unit | +|:------|:----------|:----| +| nsd.queries | queries | queries/s | +| nsd.queries_by_type | A, NS, MD, MF, CNAME, SOA, MB, MG, MR, NULL, WKS, PTR, HINFO, MINFO, MX, TXT, RP, AFSDB, X25, ISDN, RT, NSAP, SIG, KEY, PX, AAAA, LOC, NXT, SRV, NAPTR, KX, CERT, DNAME, OPT, APL, DS, SSHFP, IPSECKEY, RRSIG, NSEC, DNSKEY, DHCID, NSEC3, NSEC3PARAM, TLSA, SMIMEA, CDS, CDNSKEY, OPENPGPKEY, CSYNC, ZONEMD, SVCB, HTTPS, SPF, NID, L32, L64, LP, EUI48, EUI64, URI, CAA, AVC, DLV, IXFR, AXFR, MAILB, MAILA, ANY | queries/s | +| nsd.queries_by_opcode | QUERY, IQUERY, STATUS, NOTIFY, UPDATE, OTHER | queries/s | +| nsd.queries_by_class | IN, CS, CH, HS | queries/s | +| nsd.queries_by_protocol | udp, udp6, tcp, tcp6, tls, tls6 | queries/s | +| nsd.answers_by_rcode | NOERROR, FORMERR, SERVFAIL, NXDOMAIN, NOTIMP, REFUSED, YXDOMAIN, YXRRSET, NXRRSET, NOTAUTH, NOTZONE, RCODE11, RCODE12, RCODE13, RCODE14, RCODE15, BADVERS | answers/s | +| nsd.errors | query, answer | errors/s | +| nsd.drops | query | drops/s | +| nsd.zones | master, slave | zones | +| nsd.zone_transfers_requests | AXFR, IXFR | requests/s | +| nsd.zone_transfer_memory | used | bytes | +| nsd.database_size | disk, mem | bytes | +| nsd.uptime | uptime | seconds | + + + +## Alerts + +There are no alerts configured by default for this integration. + + +## Setup + +### Prerequisites + +No action required. + +### Configuration + +#### File + +The configuration file name for this integration is `go.d/nsd.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/nsd.conf +``` +#### Options + +The following options can be defined globally: update_every. + + +<details open><summary>Config options</summary> + +| Name | Description | Default | Required | +|:----|:-----------|:-------|:--------:| +| update_every | Data collection frequency. | 10 | no | +| timeout | nsd-control binary execution timeout. | 2 | no | + +</details> + +#### Examples + +##### Custom update_every + +Allows you to override the default data collection interval. + +<details open><summary>Config</summary> + +```yaml +jobs: + - name: nsd + update_every: 5 # Collect logical volume statistics every 5 seconds + +``` +</details> + + + +## Troubleshooting + +### Debug Mode + +**Important**: Debug mode is not supported for data collection jobs created via the UI using the Dyncfg feature. + +To troubleshoot issues with the `nsd` 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 nsd + ``` + +### Getting Logs + +If you're encountering problems with the `nsd` collector, follow these steps to retrieve logs and identify potential issues: + +- **Run the command** specific to your system (systemd, non-systemd, or Docker container). +- **Examine the output** for any warnings or error messages that might indicate issues. These messages should provide clues about the root cause of the problem. + +#### System with systemd + +Use the following command to view logs generated since the last Netdata service restart: + +```bash +journalctl _SYSTEMD_INVOCATION_ID="$(systemctl show --value --property=InvocationID netdata)" --namespace=netdata --grep nsd +``` + +#### System without systemd + +Locate the collector log file, typically at `/var/log/netdata/collector.log`, and use `grep` to filter for collector's name: + +```bash +grep nsd /var/log/netdata/collector.log +``` + +**Note**: This method shows logs from all restarts. Focus on the **latest entries** for troubleshooting current issues. + +#### Docker Container + +If your Netdata runs in a Docker container named "netdata" (replace if different), use this command: + +```bash +docker logs netdata 2>&1 | grep nsd +``` + + diff --git a/src/go/plugin/go.d/modules/nsd/metadata.yaml b/src/go/plugin/go.d/modules/nsd/metadata.yaml new file mode 100644 index 000000000..a31aa38af --- /dev/null +++ b/src/go/plugin/go.d/modules/nsd/metadata.yaml @@ -0,0 +1,272 @@ +plugin_name: go.d.plugin +modules: + - meta: + id: collector-go.d.plugin-nsd + plugin_name: go.d.plugin + module_name: nsd + monitored_instance: + name: NSD + link: "https://nsd.docs.nlnetlabs.nl/en/latest" + icon_filename: 'nsd.svg' + categories: + - data-collection.dns-and-dhcp-servers + keywords: + - nsd + - dns + related_resources: + integrations: + list: [] + info_provided_to_referring_integrations: + description: "" + most_popular: false + overview: + data_collection: + metrics_description: > + This collector monitors NSD statistics like queries, zones, protocols, query types and more. + 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. + Instead, it utilizes `ndsudo`, a Netdata helper specifically designed to run privileged commands securely within the Netdata environment. + This approach eliminates the need to use `sudo`, improving security and potentially simplifying permission management. + + Executed commands: + + - `nsd-control stats_noreset` + method_description: "" + supported_platforms: + include: [] + exclude: [] + multi_instance: false + additional_permissions: + description: "" + default_behavior: + auto_detection: + description: "" + limits: + description: "" + performance_impact: + description: "" + setup: + prerequisites: + list: [] + configuration: + file: + name: go.d/nsd.conf + options: + description: | + The following options can be defined globally: update_every. + folding: + title: Config options + enabled: true + list: + - name: update_every + description: Data collection frequency. + default_value: 10 + required: false + - name: timeout + description: nsd-control binary execution timeout. + default_value: 2 + required: false + examples: + folding: + title: Config + enabled: true + list: + - name: Custom update_every + description: Allows you to override the default data collection interval. + config: | + jobs: + - name: nsd + update_every: 5 # Collect logical volume statistics every 5 seconds + troubleshooting: + problems: + list: [] + alerts: [] + metrics: + folding: + title: Metrics + enabled: false + description: "" + availability: [] + scopes: + - name: global + description: These metrics refer to the the entire monitored application. + labels: [] + metrics: + - name: nsd.queries + description: Queries + unit: 'queries/s' + chart_type: line + dimensions: + - name: queries + - name: nsd.queries_by_type + description: Queries Type + unit: 'queries/s' + chart_type: stacked + dimensions: + - name: "A" + - name: "NS" + - name: "MD" + - name: "MF" + - name: "CNAME" + - name: "SOA" + - name: "MB" + - name: "MG" + - name: "MR" + - name: "NULL" + - name: "WKS" + - name: "PTR" + - name: "HINFO" + - name: "MINFO" + - name: "MX" + - name: "TXT" + - name: "RP" + - name: "AFSDB" + - name: "X25" + - name: "ISDN" + - name: "RT" + - name: "NSAP" + - name: "SIG" + - name: "KEY" + - name: "PX" + - name: "AAAA" + - name: "LOC" + - name: "NXT" + - name: "SRV" + - name: "NAPTR" + - name: "KX" + - name: "CERT" + - name: "DNAME" + - name: "OPT" + - name: "APL" + - name: "DS" + - name: "SSHFP" + - name: "IPSECKEY" + - name: "RRSIG" + - name: "NSEC" + - name: "DNSKEY" + - name: "DHCID" + - name: "NSEC3" + - name: "NSEC3PARAM" + - name: "TLSA" + - name: "SMIMEA" + - name: "CDS" + - name: "CDNSKEY" + - name: "OPENPGPKEY" + - name: "CSYNC" + - name: "ZONEMD" + - name: "SVCB" + - name: "HTTPS" + - name: "SPF" + - name: "NID" + - name: "L32" + - name: "L64" + - name: "LP" + - name: "EUI48" + - name: "EUI64" + - name: "URI" + - name: "CAA" + - name: "AVC" + - name: "DLV" + - name: "IXFR" + - name: "AXFR" + - name: "MAILB" + - name: "MAILA" + - name: "ANY" + - name: nsd.queries_by_opcode + description: Queries Opcode + unit: 'queries/s' + chart_type: stacked + dimensions: + - name: "QUERY" + - name: "IQUERY" + - name: "STATUS" + - name: "NOTIFY" + - name: "UPDATE" + - name: "OTHER" + - name: nsd.queries_by_class + description: Queries Class + unit: 'queries/s' + chart_type: stacked + dimensions: + - name: "IN" + - name: "CS" + - name: "CH" + - name: "HS" + - name: nsd.queries_by_protocol + description: Queries Protocol + unit: 'queries/s' + chart_type: stacked + dimensions: + - name: "udp" + - name: "udp6" + - name: "tcp" + - name: "tcp6" + - name: "tls" + - name: "tls6" + - name: nsd.answers_by_rcode + description: Answers Rcode + unit: 'answers/s' + chart_type: stacked + dimensions: + - name: "NOERROR" + - name: "FORMERR" + - name: "SERVFAIL" + - name: "NXDOMAIN" + - name: "NOTIMP" + - name: "REFUSED" + - name: "YXDOMAIN" + - name: "YXRRSET" + - name: "NXRRSET" + - name: "NOTAUTH" + - name: "NOTZONE" + - name: "RCODE11" + - name: "RCODE12" + - name: "RCODE13" + - name: "RCODE14" + - name: "RCODE15" + - name: "BADVERS" + - name: nsd.errors + description: Errors + unit: 'errors/s' + chart_type: line + dimensions: + - name: "query" + - name: "answer" + - name: nsd.drops + description: Drops + unit: 'drops/s' + chart_type: line + dimensions: + - name: "query" + - name: nsd.zones + description: Zones + unit: 'zones' + chart_type: line + dimensions: + - name: "master" + - name: "slave" + - name: nsd.zone_transfers_requests + description: Zone Transfers + unit: 'requests/s' + chart_type: line + dimensions: + - name: "AXFR" + - name: "IXFR" + - name: nsd.zone_transfer_memory + description: Zone Transfer Memory + unit: 'bytes' + chart_type: line + dimensions: + - name: "used" + - name: nsd.database_size + description: Database Size + unit: 'bytes' + chart_type: line + dimensions: + - name: "disk" + - name: "mem" + - name: nsd.uptime + description: Uptime + unit: 'seconds' + chart_type: line + dimensions: + - name: "uptime" diff --git a/src/go/plugin/go.d/modules/nsd/nsd.go b/src/go/plugin/go.d/modules/nsd/nsd.go new file mode 100644 index 000000000..fae0f67f3 --- /dev/null +++ b/src/go/plugin/go.d/modules/nsd/nsd.go @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package nsd + +import ( + _ "embed" + "errors" + "time" + + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module" + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/web" +) + +//go:embed "config_schema.json" +var configSchema string + +func init() { + module.Register("nsd", module.Creator{ + JobConfigSchema: configSchema, + Defaults: module.Defaults{ + UpdateEvery: 10, + }, + Create: func() module.Module { return New() }, + Config: func() any { return &Config{} }, + }) +} + +func New() *Nsd { + return &Nsd{ + Config: Config{ + Timeout: web.Duration(time.Second * 2), + }, + charts: charts.Copy(), + } +} + +type Config struct { + UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"` + Timeout web.Duration `yaml:"timeout,omitempty" json:"timeout"` +} + +type Nsd struct { + module.Base + Config `yaml:",inline" json:""` + + charts *module.Charts + + exec nsdControlBinary +} + +func (n *Nsd) Configuration() any { + return n.Config +} + +func (n *Nsd) Init() error { + nsdControl, err := n.initNsdControlExec() + if err != nil { + n.Errorf("nsd-control exec initialization: %v", err) + return err + } + n.exec = nsdControl + + return nil +} + +func (n *Nsd) Check() error { + mx, err := n.collect() + if err != nil { + n.Error(err) + return err + } + + if len(mx) == 0 { + return errors.New("no metrics collected") + } + + return nil +} + +func (n *Nsd) Charts() *module.Charts { + return n.charts +} + +func (n *Nsd) Collect() map[string]int64 { + mx, err := n.collect() + if err != nil { + n.Error(err) + } + + if len(mx) == 0 { + return nil + } + + return mx +} + +func (n *Nsd) Cleanup() {} diff --git a/src/go/plugin/go.d/modules/nsd/nsd_test.go b/src/go/plugin/go.d/modules/nsd/nsd_test.go new file mode 100644 index 000000000..24f38b512 --- /dev/null +++ b/src/go/plugin/go.d/modules/nsd/nsd_test.go @@ -0,0 +1,337 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package nsd + +import ( + "errors" + "os" + "testing" + + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + dataConfigJSON, _ = os.ReadFile("testdata/config.json") + dataConfigYAML, _ = os.ReadFile("testdata/config.yaml") + + dataStats, _ = os.ReadFile("testdata/stats.txt") +) + +func Test_testDataIsValid(t *testing.T) { + for name, data := range map[string][]byte{ + "dataConfigJSON": dataConfigJSON, + "dataConfigYAML": dataConfigYAML, + "dataStats": dataStats, + } { + require.NotNil(t, data, name) + + } +} + +func TestNsd_Configuration(t *testing.T) { + module.TestConfigurationSerialize(t, &Nsd{}, dataConfigJSON, dataConfigYAML) +} + +func TestNsd_Init(t *testing.T) { + tests := map[string]struct { + config Config + wantFail bool + }{ + "fails if failed to locate ndsudo": { + wantFail: true, + config: New().Config, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + nsd := New() + nsd.Config = test.config + + if test.wantFail { + assert.Error(t, nsd.Init()) + } else { + assert.NoError(t, nsd.Init()) + } + }) + } +} + +func TestNsd_Cleanup(t *testing.T) { + tests := map[string]struct { + prepare func() *Nsd + }{ + "not initialized exec": { + prepare: func() *Nsd { + return New() + }, + }, + "after check": { + prepare: func() *Nsd { + nsd := New() + nsd.exec = prepareMockOK() + _ = nsd.Check() + return nsd + }, + }, + "after collect": { + prepare: func() *Nsd { + nsd := New() + nsd.exec = prepareMockOK() + _ = nsd.Collect() + return nsd + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + nsd := test.prepare() + + assert.NotPanics(t, nsd.Cleanup) + }) + } +} + +func TestNsd_Charts(t *testing.T) { + assert.NotNil(t, New().Charts()) +} + +func TestNsd_Check(t *testing.T) { + tests := map[string]struct { + prepareMock func() *mockNsdControl + wantFail bool + }{ + "success case": { + prepareMock: prepareMockOK, + wantFail: false, + }, + "error on stats call": { + prepareMock: prepareMockErrOnStats, + wantFail: true, + }, + "empty response": { + prepareMock: prepareMockEmptyResponse, + wantFail: true, + }, + "unexpected response": { + prepareMock: prepareMockUnexpectedResponse, + wantFail: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + nsd := New() + mock := test.prepareMock() + nsd.exec = mock + + if test.wantFail { + assert.Error(t, nsd.Check()) + } else { + assert.NoError(t, nsd.Check()) + } + }) + } +} + +func TestNsd_Collect(t *testing.T) { + tests := map[string]struct { + prepareMock func() *mockNsdControl + wantMetrics map[string]int64 + }{ + "success case": { + prepareMock: prepareMockOK, + wantMetrics: map[string]int64{ + "num.answer_wo_aa": 1, + "num.class.CH": 0, + "num.class.CS": 0, + "num.class.HS": 0, + "num.class.IN": 1, + "num.dropped": 1, + "num.edns": 1, + "num.ednserr": 1, + "num.opcode.IQUERY": 0, + "num.opcode.NOTIFY": 0, + "num.opcode.OTHER": 0, + "num.opcode.QUERY": 1, + "num.opcode.STATUS": 0, + "num.opcode.UPDATE": 0, + "num.queries": 1, + "num.raxfr": 1, + "num.rcode.BADVERS": 0, + "num.rcode.FORMERR": 1, + "num.rcode.NOERROR": 1, + "num.rcode.NOTAUTH": 0, + "num.rcode.NOTIMP": 1, + "num.rcode.NOTZONE": 0, + "num.rcode.NXDOMAIN": 1, + "num.rcode.NXRRSET": 0, + "num.rcode.RCODE11": 0, + "num.rcode.RCODE12": 0, + "num.rcode.RCODE13": 0, + "num.rcode.RCODE14": 0, + "num.rcode.RCODE15": 0, + "num.rcode.REFUSED": 1, + "num.rcode.SERVFAIL": 1, + "num.rcode.YXDOMAIN": 1, + "num.rcode.YXRRSET": 0, + "num.rixfr": 1, + "num.rxerr": 1, + "num.tcp": 1, + "num.tcp6": 1, + "num.tls": 1, + "num.tls6": 1, + "num.truncated": 1, + "num.txerr": 1, + "num.type.A": 1, + "num.type.AAAA": 1, + "num.type.AFSDB": 1, + "num.type.APL": 1, + "num.type.AVC": 0, + "num.type.CAA": 0, + "num.type.CDNSKEY": 1, + "num.type.CDS": 1, + "num.type.CERT": 1, + "num.type.CNAME": 1, + "num.type.CSYNC": 1, + "num.type.DHCID": 1, + "num.type.DLV": 0, + "num.type.DNAME": 1, + "num.type.DNSKEY": 1, + "num.type.DS": 1, + "num.type.EUI48": 1, + "num.type.EUI64": 1, + "num.type.HINFO": 1, + "num.type.HTTPS": 1, + "num.type.IPSECKEY": 1, + "num.type.ISDN": 1, + "num.type.KEY": 1, + "num.type.KX": 1, + "num.type.L32": 1, + "num.type.L64": 1, + "num.type.LOC": 1, + "num.type.LP": 1, + "num.type.MB": 1, + "num.type.MD": 1, + "num.type.MF": 1, + "num.type.MG": 1, + "num.type.MINFO": 1, + "num.type.MR": 1, + "num.type.MX": 1, + "num.type.NAPTR": 1, + "num.type.NID": 1, + "num.type.NS": 1, + "num.type.NSAP": 1, + "num.type.NSEC": 1, + "num.type.NSEC3": 1, + "num.type.NSEC3PARAM": 1, + "num.type.NULL": 1, + "num.type.NXT": 1, + "num.type.OPENPGPKEY": 1, + "num.type.OPT": 1, + "num.type.PTR": 1, + "num.type.PX": 1, + "num.type.RP": 1, + "num.type.RRSIG": 1, + "num.type.RT": 1, + "num.type.SIG": 1, + "num.type.SMIMEA": 1, + "num.type.SOA": 1, + "num.type.SPF": 1, + "num.type.SRV": 1, + "num.type.SSHFP": 1, + "num.type.SVCB": 1, + "num.type.TLSA": 1, + "num.type.TXT": 1, + "num.type.TYPE252": 0, + "num.type.TYPE255": 0, + "num.type.URI": 0, + "num.type.WKS": 1, + "num.type.X25": 1, + "num.type.ZONEMD": 1, + "num.udp": 1, + "num.udp6": 1, + "server0.queries": 1, + "size.config.disk": 1, + "size.config.mem": 1064, + "size.db.disk": 576, + "size.db.mem": 920, + "size.xfrd.mem": 1160464, + "time.boot": 556, + "zone.master": 1, + "zone.slave": 1, + }, + }, + "error on lvs report call": { + prepareMock: prepareMockErrOnStats, + wantMetrics: nil, + }, + "empty response": { + prepareMock: prepareMockEmptyResponse, + wantMetrics: nil, + }, + "unexpected response": { + prepareMock: prepareMockUnexpectedResponse, + wantMetrics: nil, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + nsd := New() + mock := test.prepareMock() + nsd.exec = mock + + mx := nsd.Collect() + + assert.Equal(t, test.wantMetrics, mx) + + if len(test.wantMetrics) > 0 { + assert.Len(t, *nsd.Charts(), len(charts)) + module.TestMetricsHasAllChartsDims(t, nsd.Charts(), mx) + } + }) + } +} + +func prepareMockOK() *mockNsdControl { + return &mockNsdControl{ + dataStats: dataStats, + } +} + +func prepareMockErrOnStats() *mockNsdControl { + return &mockNsdControl{ + errOnStatus: true, + } +} + +func prepareMockEmptyResponse() *mockNsdControl { + return &mockNsdControl{} +} + +func prepareMockUnexpectedResponse() *mockNsdControl { + return &mockNsdControl{ + dataStats: []byte(` +Lorem ipsum dolor sit amet, consectetur adipiscing elit. +Nulla malesuada erat id magna mattis, eu viverra tellus rhoncus. +Fusce et felis pulvinar, posuere sem non, porttitor eros. +`), + } +} + +type mockNsdControl struct { + errOnStatus bool + dataStats []byte +} + +func (m *mockNsdControl) stats() ([]byte, error) { + if m.errOnStatus { + return nil, errors.New("mock.status() error") + } + return m.dataStats, nil +} diff --git a/src/go/plugin/go.d/modules/nsd/stats_counters.go b/src/go/plugin/go.d/modules/nsd/stats_counters.go new file mode 100644 index 000000000..8ebe706a5 --- /dev/null +++ b/src/go/plugin/go.d/modules/nsd/stats_counters.go @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package nsd + +// Docs: https://nsd.docs.nlnetlabs.nl/en/latest/manpages/nsd-control.html?highlight=elapsed#statistics-counters +// Source: https://github.com/NLnetLabs/nsd/blob/b4a5ccd2235a1f8f71f7c640390e409bf123c963/remote.c#L2735 + +// https://github.com/NLnetLabs/nsd/blob/b4a5ccd2235a1f8f71f7c640390e409bf123c963/remote.c#L2737 +var answerRcodes = []string{ + "NOERROR", + "FORMERR", + "SERVFAIL", + "NXDOMAIN", + "NOTIMP", + "REFUSED", + "YXDOMAIN", + "YXRRSET", + "NXRRSET", + "NOTAUTH", + "NOTZONE", + "RCODE11", + "RCODE12", + "RCODE13", + "RCODE14", + "RCODE15", + "BADVERS", +} + +// https://github.com/NLnetLabs/nsd/blob/b4a5ccd2235a1f8f71f7c640390e409bf123c963/remote.c#L2706 +var queryOpcodes = []string{ + "QUERY", + "IQUERY", + "STATUS", + "NOTIFY", + "UPDATE", + "OTHER", +} + +// https://github.com/NLnetLabs/nsd/blob/b4a5ccd2235a1f8f71f7c640390e409bf123c963/dns.c#L27 +var queryClasses = []string{ + "IN", + "CS", + "CH", + "HS", +} + +// https://github.com/NLnetLabs/nsd/blob/b4a5ccd2235a1f8f71f7c640390e409bf123c963/dns.c#L35 +var queryTypes = []string{ + "A", + "NS", + "MD", + "MF", + "CNAME", + "SOA", + "MB", + "MG", + "MR", + "NULL", + "WKS", + "PTR", + "HINFO", + "MINFO", + "MX", + "TXT", + "RP", + "AFSDB", + "X25", + "ISDN", + "RT", + "NSAP", + "SIG", + "KEY", + "PX", + "AAAA", + "LOC", + "NXT", + "SRV", + "NAPTR", + "KX", + "CERT", + "DNAME", + "OPT", + "APL", + "DS", + "SSHFP", + "IPSECKEY", + "RRSIG", + "NSEC", + "DNSKEY", + "DHCID", + "NSEC3", + "NSEC3PARAM", + "TLSA", + "SMIMEA", + "CDS", + "CDNSKEY", + "OPENPGPKEY", + "CSYNC", + "ZONEMD", + "SVCB", + "HTTPS", + "SPF", + "NID", + "L32", + "L64", + "LP", + "EUI48", + "EUI64", + "URI", + "CAA", + "AVC", + "DLV", + "TYPE252", + "TYPE255", +} + +var queryTypeNumberMap = map[string]string{ + "TYPE251": "IXFR", + "TYPE252": "AXFR", + "TYPE253": "MAILB", + "TYPE254": "MAILA", + "TYPE255": "ANY", +} diff --git a/src/go/plugin/go.d/modules/nsd/testdata/config.json b/src/go/plugin/go.d/modules/nsd/testdata/config.json new file mode 100644 index 000000000..291ecee3d --- /dev/null +++ b/src/go/plugin/go.d/modules/nsd/testdata/config.json @@ -0,0 +1,4 @@ +{ + "update_every": 123, + "timeout": 123.123 +} diff --git a/src/go/plugin/go.d/modules/nsd/testdata/config.yaml b/src/go/plugin/go.d/modules/nsd/testdata/config.yaml new file mode 100644 index 000000000..25b0b4c78 --- /dev/null +++ b/src/go/plugin/go.d/modules/nsd/testdata/config.yaml @@ -0,0 +1,2 @@ +update_every: 123 +timeout: 123.123 diff --git a/src/go/plugin/go.d/modules/nsd/testdata/stats.txt b/src/go/plugin/go.d/modules/nsd/testdata/stats.txt new file mode 100644 index 000000000..cb6d8b829 --- /dev/null +++ b/src/go/plugin/go.d/modules/nsd/testdata/stats.txt @@ -0,0 +1,95 @@ +server0.queries=1 +num.queries=1 +time.boot=556.488415 +time.elapsed=556.488415 +size.db.disk=576 +size.db.mem=920 +size.xfrd.mem=1160464 +size.config.disk=1 +size.config.mem=1064 +num.type.A=1 +num.type.NS=1 +num.type.MD=1 +num.type.MF=1 +num.type.CNAME=1 +num.type.SOA=1 +num.type.MB=1 +num.type.MG=1 +num.type.MR=1 +num.type.NULL=1 +num.type.WKS=1 +num.type.PTR=1 +num.type.HINFO=1 +num.type.MINFO=1 +num.type.MX=1 +num.type.TXT=1 +num.type.RP=1 +num.type.AFSDB=1 +num.type.X25=1 +num.type.ISDN=1 +num.type.RT=1 +num.type.NSAP=1 +num.type.SIG=1 +num.type.KEY=1 +num.type.PX=1 +num.type.AAAA=1 +num.type.LOC=1 +num.type.NXT=1 +num.type.SRV=1 +num.type.NAPTR=1 +num.type.KX=1 +num.type.CERT=1 +num.type.DNAME=1 +num.type.OPT=1 +num.type.APL=1 +num.type.DS=1 +num.type.SSHFP=1 +num.type.IPSECKEY=1 +num.type.RRSIG=1 +num.type.NSEC=1 +num.type.DNSKEY=1 +num.type.DHCID=1 +num.type.NSEC3=1 +num.type.NSEC3PARAM=1 +num.type.TLSA=1 +num.type.SMIMEA=1 +num.type.CDS=1 +num.type.CDNSKEY=1 +num.type.OPENPGPKEY=1 +num.type.CSYNC=1 +num.type.ZONEMD=1 +num.type.SVCB=1 +num.type.HTTPS=1 +num.type.SPF=1 +num.type.NID=1 +num.type.L32=1 +num.type.L64=1 +num.type.LP=1 +num.type.EUI48=1 +num.type.EUI64=1 +num.opcode.QUERY=1 +num.class.IN=1 +num.rcode.NOERROR=1 +num.rcode.FORMERR=1 +num.rcode.SERVFAIL=1 +num.rcode.NXDOMAIN=1 +num.rcode.NOTIMP=1 +num.rcode.REFUSED=1 +num.rcode.YXDOMAIN=1 +num.edns=1 +num.ednserr=1 +num.udp=1 +num.udp6=1 +num.tcp=1 +num.tcp6=1 +num.tls=1 +num.tls6=1 +num.answer_wo_aa=1 +num.rxerr=1 +num.txerr=1 +num.raxfr=1 +num.rixfr=1 +num.truncated=1 +num.dropped=1 +zone.master=1 +zone.slave=1 |