diff options
Diffstat (limited to 'src/go/plugin/go.d/modules/ping')
l--------- | src/go/plugin/go.d/modules/ping/README.md | 1 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/ping/charts.go | 101 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/ping/collect.go | 49 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/ping/config_schema.json | 95 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/ping/init.go | 39 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/ping/integrations/ping.md | 271 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/ping/metadata.yaml | 193 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/ping/ping.go | 122 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/ping/ping_test.go | 206 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/ping/prober.go | 111 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/ping/testdata/config.json | 11 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/ping/testdata/config.yaml | 8 |
12 files changed, 1207 insertions, 0 deletions
diff --git a/src/go/plugin/go.d/modules/ping/README.md b/src/go/plugin/go.d/modules/ping/README.md new file mode 120000 index 000000000..a1381e57b --- /dev/null +++ b/src/go/plugin/go.d/modules/ping/README.md @@ -0,0 +1 @@ +integrations/ping.md
\ No newline at end of file diff --git a/src/go/plugin/go.d/modules/ping/charts.go b/src/go/plugin/go.d/modules/ping/charts.go new file mode 100644 index 000000000..04dfc17d5 --- /dev/null +++ b/src/go/plugin/go.d/modules/ping/charts.go @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package ping + +import ( + "fmt" + "strings" + + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module" +) + +const ( + prioHostRTT = module.Priority + iota + prioHostStdDevRTT + prioHostPingPacketLoss + prioHostPingPackets +) + +var hostChartsTmpl = module.Charts{ + hostRTTChartTmpl.Copy(), + hostStdDevRTTChartTmpl.Copy(), + hostPacketLossChartTmpl.Copy(), + hostPacketsChartTmpl.Copy(), +} + +var ( + hostRTTChartTmpl = module.Chart{ + ID: "host_%s_rtt", + Title: "Ping round-trip time", + Units: "milliseconds", + Fam: "latency", + Ctx: "ping.host_rtt", + Priority: prioHostRTT, + Type: module.Area, + Dims: module.Dims{ + {ID: "host_%s_min_rtt", Name: "min", Div: 1e3}, + {ID: "host_%s_max_rtt", Name: "max", Div: 1e3}, + {ID: "host_%s_avg_rtt", Name: "avg", Div: 1e3}, + }, + } + hostStdDevRTTChartTmpl = module.Chart{ + ID: "host_%s_std_dev_rtt", + Title: "Ping round-trip time standard deviation", + Units: "milliseconds", + Fam: "latency", + Ctx: "ping.host_std_dev_rtt", + Priority: prioHostStdDevRTT, + Dims: module.Dims{ + {ID: "host_%s_std_dev_rtt", Name: "std_dev", Div: 1e3}, + }, + } +) + +var hostPacketLossChartTmpl = module.Chart{ + ID: "host_%s_packet_loss", + Title: "Ping packet loss", + Units: "percentage", + Fam: "packet loss", + Ctx: "ping.host_packet_loss", + Priority: prioHostPingPacketLoss, + Dims: module.Dims{ + {ID: "host_%s_packet_loss", Name: "loss", Div: 1000}, + }, +} + +var hostPacketsChartTmpl = module.Chart{ + ID: "host_%s_packets", + Title: "Ping packets transferred", + Units: "packets", + Fam: "packets", + Ctx: "ping.host_packets", + Priority: prioHostPingPackets, + Dims: module.Dims{ + {ID: "host_%s_packets_recv", Name: "received"}, + {ID: "host_%s_packets_sent", Name: "sent"}, + }, +} + +func newHostCharts(host string) *module.Charts { + charts := hostChartsTmpl.Copy() + + for _, chart := range *charts { + chart.ID = fmt.Sprintf(chart.ID, strings.ReplaceAll(host, ".", "_")) + chart.Labels = []module.Label{ + {Key: "host", Value: host}, + } + for _, dim := range chart.Dims { + dim.ID = fmt.Sprintf(dim.ID, host) + } + } + + return charts +} + +func (p *Ping) addHostCharts(host string) { + charts := newHostCharts(host) + + if err := p.Charts().Add(*charts...); err != nil { + p.Warning(err) + } +} diff --git a/src/go/plugin/go.d/modules/ping/collect.go b/src/go/plugin/go.d/modules/ping/collect.go new file mode 100644 index 000000000..c162a2b15 --- /dev/null +++ b/src/go/plugin/go.d/modules/ping/collect.go @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package ping + +import ( + "fmt" + "sync" +) + +func (p *Ping) collect() (map[string]int64, error) { + mu := &sync.Mutex{} + mx := make(map[string]int64) + var wg sync.WaitGroup + + for _, v := range p.Hosts { + wg.Add(1) + go func(v string) { defer wg.Done(); p.pingHost(v, mx, mu) }(v) + } + wg.Wait() + + return mx, nil +} + +func (p *Ping) pingHost(host string, mx map[string]int64, mu *sync.Mutex) { + stats, err := p.prober.ping(host) + if err != nil { + p.Error(err) + return + } + + mu.Lock() + defer mu.Unlock() + + if !p.hosts[host] { + p.hosts[host] = true + p.addHostCharts(host) + } + + px := fmt.Sprintf("host_%s_", host) + if stats.PacketsRecv != 0 { + mx[px+"min_rtt"] = stats.MinRtt.Microseconds() + mx[px+"max_rtt"] = stats.MaxRtt.Microseconds() + mx[px+"avg_rtt"] = stats.AvgRtt.Microseconds() + mx[px+"std_dev_rtt"] = stats.StdDevRtt.Microseconds() + } + mx[px+"packets_recv"] = int64(stats.PacketsRecv) + mx[px+"packets_sent"] = int64(stats.PacketsSent) + mx[px+"packet_loss"] = int64(stats.PacketLoss * 1000) +} diff --git a/src/go/plugin/go.d/modules/ping/config_schema.json b/src/go/plugin/go.d/modules/ping/config_schema.json new file mode 100644 index 000000000..1168e3388 --- /dev/null +++ b/src/go/plugin/go.d/modules/ping/config_schema.json @@ -0,0 +1,95 @@ +{ + "jsonSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Ping collector configuration.", + "properties": { + "update_every": { + "title": "Update every", + "description": "Data collection interval, measured in seconds.", + "type": "integer", + "minimum": 1, + "default": 1 + }, + "privileged": { + "title": "Privileged mode", + "description": "If unset, sends unprivileged UDP ping packets (require [additional configuration](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/ping#overview)); otherwise, sends raw ICMP ping packets ([not recommended](https://github.com/netdata/netdata/issues/15410)).", + "type": "boolean", + "default": false + }, + "hosts": { + "title": "Network hosts", + "description": "List of network hosts (IP addresses or domain names) to send ping packets.", + "type": [ + "array", + "null" + ], + "items": { + "title": "Host", + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + }, + "network": { + "title": "Network", + "description": "The protocol version used for resolving the specified hosts IP addresses.", + "type": "string", + "default": "ip", + "enum": [ + "ip", + "ip4", + "ip6" + ] + }, + "packets": { + "title": "Packets", + "description": "Number of ping packets to send for each host.", + "type": "integer", + "minimum": 1, + "default": 5 + }, + "interval": { + "title": "Interval", + "description": "Timeout between sending ping packets, in seconds.", + "type": "number", + "minimum": 0.1, + "default": 0.1 + }, + "interface": { + "title": "Interface", + "description": "The name of the network interface whose IP address will be used as the source for sending ping packets.", + "type": "string", + "default": "" + } + }, + "required": [ + "hosts" + ], + "additionalProperties": false, + "patternProperties": { + "^name$": {} + } + }, + "uiSchema": { + "uiOptions": { + "fullPage": true + }, + "update_every": { + "ui:help": "Sets the frequency at which a specified number of ping packets (determined by 'packets') are sent to designated hosts." + }, + "network": { + "ui:help": "`ip` selects IPv4 or IPv6 based on system configuration, `ipv4` forces resolution to IPv4 addresses, and `ipv6` forces resolution to IPv6 addresses.", + "ui:widget": "radio", + "ui:options": { + "inline": true + } + }, + "interval": { + "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)." + }, + "hosts": { + "ui:listFlavour": "list" + } + } +} diff --git a/src/go/plugin/go.d/modules/ping/init.go b/src/go/plugin/go.d/modules/ping/init.go new file mode 100644 index 000000000..62d78c8e6 --- /dev/null +++ b/src/go/plugin/go.d/modules/ping/init.go @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package ping + +import ( + "errors" + "time" +) + +func (p *Ping) validateConfig() error { + if len(p.Hosts) == 0 { + return errors.New("'hosts' can't be empty") + } + if p.SendPackets <= 0 { + return errors.New("'send_packets' can't be <= 0") + } + return nil +} + +func (p *Ping) initProber() (prober, error) { + mul := 0.9 + if p.UpdateEvery > 1 { + mul = 0.95 + } + deadline := time.Millisecond * time.Duration(float64(p.UpdateEvery)*mul*1000) + if deadline.Milliseconds() == 0 { + return nil, errors.New("zero ping deadline") + } + + conf := pingProberConfig{ + privileged: p.Privileged, + packets: p.SendPackets, + iface: p.Interface, + interval: p.Interval.Duration(), + deadline: deadline, + } + + return p.newProber(conf, p.Logger), nil +} diff --git a/src/go/plugin/go.d/modules/ping/integrations/ping.md b/src/go/plugin/go.d/modules/ping/integrations/ping.md new file mode 100644 index 000000000..db97288b0 --- /dev/null +++ b/src/go/plugin/go.d/modules/ping/integrations/ping.md @@ -0,0 +1,271 @@ +<!--startmeta +custom_edit_url: "https://github.com/netdata/netdata/edit/master/src/go/plugin/go.d/modules/ping/README.md" +meta_yaml: "https://github.com/netdata/netdata/edit/master/src/go/plugin/go.d/modules/ping/metadata.yaml" +sidebar_label: "Ping" +learn_status: "Published" +learn_rel_path: "Collecting Metrics/Synthetic Checks" +most_popular: False +message: "DO NOT EDIT THIS FILE DIRECTLY, IT IS GENERATED BY THE COLLECTOR'S metadata.yaml FILE" +endmeta--> + +# Ping + + +<img src="https://netdata.cloud/img/globe.svg" width="150"/> + + +Plugin: go.d.plugin +Module: ping + +<img src="https://img.shields.io/badge/maintained%20by-Netdata-%2300ab44" /> + +## Overview + +This module measures round-trip time and packet loss by sending ping messages to network hosts. + +There are two operational modes: + +- privileged (send raw ICMP ping, default). Requires + CAP_NET_RAW [capability](https://man7.org/linux/man-pages/man7/capabilities.7.html) or root privileges: + > **Note**: set automatically during Netdata installation. + + ```bash + sudo setcap CAP_NET_RAW=eip <INSTALL_PREFIX>/usr/libexec/netdata/plugins.d/go.d.plugin + ``` + +- unprivileged (send UDP ping, Linux only). + Requires configuring [ping_group_range](https://www.man7.org/linux/man-pages/man7/icmp.7.html): + + ```bash + sudo sysctl -w net.ipv4.ping_group_range="0 2147483647" + ``` + To persist the change add `net.ipv4.ping_group_range=0 2147483647` to `/etc/sysctl.conf` and + execute `sudo sysctl -p`. + + + + +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 host + +These metrics refer to the remote host. + +Labels: + +| Label | Description | +|:-----------|:----------------| +| host | remote host | + +Metrics: + +| Metric | Dimensions | Unit | +|:------|:----------|:----| +| ping.host_rtt | min, max, avg | milliseconds | +| ping.host_std_dev_rtt | std_dev | milliseconds | +| ping.host_packet_loss | loss | percentage | +| ping.host_packets | received, sent | packets | + + + +## Alerts + + +The following alerts are available: + +| Alert name | On metric | Description | +|:------------|:----------|:------------| +| [ ping_host_reachable ](https://github.com/netdata/netdata/blob/master/src/health/health.d/ping.conf) | ping.host_packet_loss | network host ${lab1el:host} reachability status | +| [ ping_packet_loss ](https://github.com/netdata/netdata/blob/master/src/health/health.d/ping.conf) | ping.host_packet_loss | packet loss percentage to the network host ${label:host} over the last 10 minutes | +| [ ping_host_latency ](https://github.com/netdata/netdata/blob/master/src/health/health.d/ping.conf) | ping.host_rtt | average latency to the network host ${label:host} over the last 10 seconds | + + +## Setup + +### Prerequisites + +No action required. + +### Configuration + +#### File + +The configuration file name for this integration is `go.d/ping.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/ping.conf +``` +#### Options + +The following options can be defined globally: update_every, autodetection_retry. + + +<details open><summary>Config options</summary> + +| Name | Description | Default | Required | +|:----|:-----------|:-------|:--------:| +| update_every | Data collection frequency. | 5 | no | +| autodetection_retry | Recheck interval in seconds. Zero means no recheck will be scheduled. | 0 | no | +| hosts | Network hosts. | | yes | +| network | Allows configuration of DNS resolution. Supported options: ip (select IPv4 or IPv6), ip4 (select IPv4), ip6 (select IPv6). | ip | no | +| privileged | Ping packets type. "no" means send an "unprivileged" UDP ping, "yes" - raw ICMP ping. | yes | no | +| packets | Number of ping packets to send. | 5 | no | +| interval | Timeout between sending ping packets. | 100ms | no | + +</details> + +#### Examples + +##### IPv4 hosts + +An example configuration. + +<details open><summary>Config</summary> + +```yaml +jobs: + - name: example + hosts: + - 192.0.2.0 + - 192.0.2.1 + +``` +</details> + +##### Unprivileged mode + +An example configuration. + +<details open><summary>Config</summary> + +```yaml +jobs: + - name: example + privileged: no + hosts: + - 192.0.2.0 + - 192.0.2.1 + +``` +</details> + +##### Multi-instance + +> **Note**: When you define multiple jobs, their names must be unique. + +Multiple instances. + + +<details open><summary>Config</summary> + +```yaml +jobs: + - name: example1 + hosts: + - 192.0.2.0 + - 192.0.2.1 + + - name: example2 + packets: 10 + hosts: + - 192.0.2.3 + - 192.0.2.4 + +``` +</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 `ping` 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 ping + ``` + +### Getting Logs + +If you're encountering problems with the `ping` 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 ping +``` + +#### 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 ping /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 ping +``` + + diff --git a/src/go/plugin/go.d/modules/ping/metadata.yaml b/src/go/plugin/go.d/modules/ping/metadata.yaml new file mode 100644 index 000000000..8686d103b --- /dev/null +++ b/src/go/plugin/go.d/modules/ping/metadata.yaml @@ -0,0 +1,193 @@ +plugin_name: go.d.plugin +modules: + - meta: + id: collector-go.d.plugin-ping + plugin_name: go.d.plugin + module_name: ping + monitored_instance: + name: Ping + link: "" + icon_filename: globe.svg + categories: + - data-collection.synthetic-checks + keywords: + - ping + related_resources: + integrations: + list: [] + info_provided_to_referring_integrations: + description: "" + most_popular: false + overview: + data_collection: + metrics_description: | + This module measures round-trip time and packet loss by sending ping messages to network hosts. + + There are two operational modes: + + - privileged (send raw ICMP ping, default). Requires + CAP_NET_RAW [capability](https://man7.org/linux/man-pages/man7/capabilities.7.html) or root privileges: + > **Note**: set automatically during Netdata installation. + + ```bash + sudo setcap CAP_NET_RAW=eip <INSTALL_PREFIX>/usr/libexec/netdata/plugins.d/go.d.plugin + ``` + + - unprivileged (send UDP ping, Linux only). + Requires configuring [ping_group_range](https://www.man7.org/linux/man-pages/man7/icmp.7.html): + + ```bash + sudo sysctl -w net.ipv4.ping_group_range="0 2147483647" + ``` + To persist the change add `net.ipv4.ping_group_range=0 2147483647` to `/etc/sysctl.conf` and + execute `sudo sysctl -p`. + method_description: "" + supported_platforms: + include: [] + exclude: [] + multi_instance: true + additional_permissions: + description: "" + default_behavior: + auto_detection: + description: "" + limits: + description: "" + performance_impact: + description: "" + setup: + prerequisites: + list: [] + configuration: + file: + name: go.d/ping.conf + options: + description: | + The following options can be defined globally: update_every, autodetection_retry. + folding: + title: Config options + enabled: true + list: + - name: update_every + description: Data collection frequency. + default_value: 5 + required: false + - name: autodetection_retry + description: Recheck interval in seconds. Zero means no recheck will be scheduled. + default_value: 0 + required: false + - name: hosts + description: Network hosts. + default_value: "" + required: true + - name: network + description: "Allows configuration of DNS resolution. Supported options: ip (select IPv4 or IPv6), ip4 (select IPv4), ip6 (select IPv6)." + default_value: "ip" + required: false + - name: privileged + description: Ping packets type. "no" means send an "unprivileged" UDP ping, "yes" - raw ICMP ping. + default_value: true + required: false + - name: packets + description: Number of ping packets to send. + default_value: 5 + required: false + - name: interval + description: Timeout between sending ping packets. + default_value: 100ms + required: false + examples: + folding: + title: Config + enabled: true + list: + - name: IPv4 hosts + description: An example configuration. + config: | + jobs: + - name: example + hosts: + - 192.0.2.0 + - 192.0.2.1 + - name: Unprivileged mode + description: An example configuration. + config: | + jobs: + - name: example + privileged: no + hosts: + - 192.0.2.0 + - 192.0.2.1 + - name: Multi-instance + description: | + > **Note**: When you define multiple jobs, their names must be unique. + + Multiple instances. + config: | + jobs: + - name: example1 + hosts: + - 192.0.2.0 + - 192.0.2.1 + + - name: example2 + packets: 10 + hosts: + - 192.0.2.3 + - 192.0.2.4 + troubleshooting: + problems: + list: [] + alerts: + - name: ping_host_reachable + metric: ping.host_packet_loss + info: "network host ${lab1el:host} reachability status" + link: https://github.com/netdata/netdata/blob/master/src/health/health.d/ping.conf + - name: ping_packet_loss + metric: ping.host_packet_loss + info: "packet loss percentage to the network host ${label:host} over the last 10 minutes" + link: https://github.com/netdata/netdata/blob/master/src/health/health.d/ping.conf + - name: ping_host_latency + metric: ping.host_rtt + info: "average latency to the network host ${label:host} over the last 10 seconds" + link: https://github.com/netdata/netdata/blob/master/src/health/health.d/ping.conf + metrics: + folding: + title: Metrics + enabled: false + description: "" + availability: [] + scopes: + - name: host + description: These metrics refer to the remote host. + labels: + - name: host + description: remote host + metrics: + - name: ping.host_rtt + description: Ping round-trip time + unit: milliseconds + chart_type: line + dimensions: + - name: min + - name: max + - name: avg + - name: ping.host_std_dev_rtt + description: Ping round-trip time standard deviation + unit: milliseconds + chart_type: line + dimensions: + - name: std_dev + - name: ping.host_packet_loss + description: Ping packet loss + unit: percentage + chart_type: line + dimensions: + - name: loss + - name: ping.host_packets + description: Ping packets transferred + unit: packets + chart_type: line + dimensions: + - name: received + - name: sent diff --git a/src/go/plugin/go.d/modules/ping/ping.go b/src/go/plugin/go.d/modules/ping/ping.go new file mode 100644 index 000000000..9d1ef929f --- /dev/null +++ b/src/go/plugin/go.d/modules/ping/ping.go @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package ping + +import ( + _ "embed" + "errors" + "time" + + "github.com/netdata/netdata/go/plugins/logger" + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module" + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/web" + + probing "github.com/prometheus-community/pro-bing" +) + +//go:embed "config_schema.json" +var configSchema string + +func init() { + module.Register("ping", module.Creator{ + JobConfigSchema: configSchema, + Defaults: module.Defaults{ + UpdateEvery: 5, + }, + Create: func() module.Module { return New() }, + Config: func() any { return &Config{} }, + }) +} + +func New() *Ping { + return &Ping{ + Config: Config{ + Network: "ip", + Privileged: true, + SendPackets: 5, + Interval: web.Duration(time.Millisecond * 100), + }, + + charts: &module.Charts{}, + hosts: make(map[string]bool), + newProber: newPingProber, + } +} + +type Config struct { + UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"` + Hosts []string `yaml:"hosts" json:"hosts"` + Network string `yaml:"network,omitempty" json:"network"` + Privileged bool `yaml:"privileged" json:"privileged"` + SendPackets int `yaml:"packets,omitempty" json:"packets"` + Interval web.Duration `yaml:"interval,omitempty" json:"interval"` + Interface string `yaml:"interface,omitempty" json:"interface"` +} + +type ( + Ping struct { + module.Base + Config `yaml:",inline" json:""` + + charts *module.Charts + + prober prober + newProber func(pingProberConfig, *logger.Logger) prober + + hosts map[string]bool + } + prober interface { + ping(host string) (*probing.Statistics, error) + } +) + +func (p *Ping) Configuration() any { + return p.Config +} + +func (p *Ping) Init() error { + err := p.validateConfig() + if err != nil { + p.Errorf("config validation: %v", err) + return err + } + + pr, err := p.initProber() + if err != nil { + p.Errorf("init prober: %v", err) + return err + } + p.prober = pr + + return nil +} + +func (p *Ping) Check() error { + mx, err := p.collect() + if err != nil { + return err + } + if len(mx) == 0 { + return errors.New("no metrics collected") + + } + return nil +} + +func (p *Ping) Charts() *module.Charts { + return p.charts +} + +func (p *Ping) Collect() map[string]int64 { + mx, err := p.collect() + if err != nil { + p.Error(err) + } + + if len(mx) == 0 { + return nil + } + return mx +} + +func (p *Ping) Cleanup() {} diff --git a/src/go/plugin/go.d/modules/ping/ping_test.go b/src/go/plugin/go.d/modules/ping/ping_test.go new file mode 100644 index 000000000..52d16dd3e --- /dev/null +++ b/src/go/plugin/go.d/modules/ping/ping_test.go @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package ping + +import ( + "errors" + "os" + "testing" + "time" + + "github.com/netdata/netdata/go/plugins/logger" + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module" + + probing "github.com/prometheus-community/pro-bing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + dataConfigJSON, _ = os.ReadFile("testdata/config.json") + dataConfigYAML, _ = os.ReadFile("testdata/config.yaml") +) + +func Test_testDataIsValid(t *testing.T) { + for name, data := range map[string][]byte{ + "dataConfigJSON": dataConfigJSON, + "dataConfigYAML": dataConfigYAML, + } { + require.NotNil(t, data, name) + } +} + +func TestPing_ConfigurationSerialize(t *testing.T) { + module.TestConfigurationSerialize(t, &Ping{}, dataConfigJSON, dataConfigYAML) +} + +func TestPing_Init(t *testing.T) { + tests := map[string]struct { + wantFail bool + config Config + }{ + "fail with default": { + wantFail: true, + config: New().Config, + }, + "success when 'hosts' set": { + wantFail: false, + config: Config{ + SendPackets: 1, + Hosts: []string{"192.0.2.0"}, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + ping := New() + ping.Config = test.config + ping.UpdateEvery = 1 + + if test.wantFail { + assert.Error(t, ping.Init()) + } else { + assert.NoError(t, ping.Init()) + } + }) + } +} + +func TestPing_Charts(t *testing.T) { + assert.NotNil(t, New().Charts()) +} + +func TestPing_Cleanup(t *testing.T) { + assert.NotPanics(t, New().Cleanup) +} + +func TestPing_Check(t *testing.T) { + tests := map[string]struct { + wantFail bool + prepare func(t *testing.T) *Ping + }{ + "success when ping does not return an error": { + wantFail: false, + prepare: casePingSuccess, + }, + "fail when ping returns an error": { + wantFail: true, + prepare: casePingError, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + ping := test.prepare(t) + + if test.wantFail { + assert.Error(t, ping.Check()) + } else { + assert.NoError(t, ping.Check()) + } + }) + } +} + +func TestPing_Collect(t *testing.T) { + tests := map[string]struct { + prepare func(t *testing.T) *Ping + wantMetrics map[string]int64 + wantNumCharts int + }{ + "success when ping does not return an error": { + prepare: casePingSuccess, + wantMetrics: map[string]int64{ + "host_192.0.2.1_avg_rtt": 15000, + "host_192.0.2.1_max_rtt": 20000, + "host_192.0.2.1_min_rtt": 10000, + "host_192.0.2.1_packet_loss": 0, + "host_192.0.2.1_packets_recv": 5, + "host_192.0.2.1_packets_sent": 5, + "host_192.0.2.1_std_dev_rtt": 5000, + "host_192.0.2.2_avg_rtt": 15000, + "host_192.0.2.2_max_rtt": 20000, + "host_192.0.2.2_min_rtt": 10000, + "host_192.0.2.2_packet_loss": 0, + "host_192.0.2.2_packets_recv": 5, + "host_192.0.2.2_packets_sent": 5, + "host_192.0.2.2_std_dev_rtt": 5000, + "host_example.com_avg_rtt": 15000, + "host_example.com_max_rtt": 20000, + "host_example.com_min_rtt": 10000, + "host_example.com_packet_loss": 0, + "host_example.com_packets_recv": 5, + "host_example.com_packets_sent": 5, + "host_example.com_std_dev_rtt": 5000, + }, + wantNumCharts: 3 * len(hostChartsTmpl), + }, + "fail when ping returns an error": { + prepare: casePingError, + wantMetrics: nil, + wantNumCharts: 0, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + ping := test.prepare(t) + + mx := ping.Collect() + + require.Equal(t, test.wantMetrics, mx) + + if len(test.wantMetrics) > 0 { + assert.Len(t, *ping.Charts(), test.wantNumCharts) + } + }) + } +} + +func casePingSuccess(t *testing.T) *Ping { + ping := New() + ping.UpdateEvery = 1 + ping.Hosts = []string{"192.0.2.1", "192.0.2.2", "example.com"} + ping.newProber = func(_ pingProberConfig, _ *logger.Logger) prober { + return &mockProber{} + } + require.NoError(t, ping.Init()) + return ping +} + +func casePingError(t *testing.T) *Ping { + ping := New() + ping.UpdateEvery = 1 + ping.Hosts = []string{"192.0.2.1", "192.0.2.2", "example.com"} + ping.newProber = func(_ pingProberConfig, _ *logger.Logger) prober { + return &mockProber{errOnPing: true} + } + require.NoError(t, ping.Init()) + return ping +} + +type mockProber struct { + errOnPing bool +} + +func (m *mockProber) ping(host string) (*probing.Statistics, error) { + if m.errOnPing { + return nil, errors.New("mock.ping() error") + } + + stats := probing.Statistics{ + PacketsRecv: 5, + PacketsSent: 5, + PacketsRecvDuplicates: 0, + PacketLoss: 0, + Addr: host, + Rtts: nil, + MinRtt: time.Millisecond * 10, + MaxRtt: time.Millisecond * 20, + AvgRtt: time.Millisecond * 15, + StdDevRtt: time.Millisecond * 5, + } + + return &stats, nil +} diff --git a/src/go/plugin/go.d/modules/ping/prober.go b/src/go/plugin/go.d/modules/ping/prober.go new file mode 100644 index 000000000..70c31dcde --- /dev/null +++ b/src/go/plugin/go.d/modules/ping/prober.go @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package ping + +import ( + "errors" + "fmt" + "net" + "time" + + "github.com/netdata/netdata/go/plugins/logger" + + probing "github.com/prometheus-community/pro-bing" +) + +func newPingProber(conf pingProberConfig, log *logger.Logger) prober { + var source string + if conf.iface != "" { + if addr, err := getInterfaceIPAddress(conf.iface); err != nil { + log.Warningf("error getting interface '%s' IP address: %v", conf.iface, err) + } else { + log.Infof("interface '%s' IP address '%s', will use it as the source", conf.iface, addr) + source = addr + } + } + + return &pingProber{ + network: conf.network, + privileged: conf.privileged, + packets: conf.packets, + source: source, + interval: conf.interval, + deadline: conf.deadline, + Logger: log, + } +} + +type pingProberConfig struct { + network string + privileged bool + packets int + iface string + interval time.Duration + deadline time.Duration +} + +type pingProber struct { + *logger.Logger + + network string + privileged bool + packets int + source string + interval time.Duration + deadline time.Duration +} + +func (p *pingProber) ping(host string) (*probing.Statistics, error) { + pr := probing.New(host) + + pr.SetNetwork(p.network) + + if err := pr.Resolve(); err != nil { + return nil, fmt.Errorf("DNS lookup '%s' : %v", host, err) + } + + pr.Source = p.source + pr.RecordRtts = false + pr.Interval = p.interval + pr.Count = p.packets + pr.Timeout = p.deadline + pr.SetPrivileged(p.privileged) + pr.SetLogger(nil) + + if err := pr.Run(); err != nil { + return nil, fmt.Errorf("pinging host '%s' (ip %s): %v", pr.Addr(), pr.IPAddr(), err) + } + + stats := pr.Statistics() + + p.Debugf("ping stats for host '%s' (ip '%s'): %+v", pr.Addr(), pr.IPAddr(), stats) + + return stats, nil +} + +func getInterfaceIPAddress(ifaceName string) (ipaddr string, err error) { + iface, err := net.InterfaceByName(ifaceName) + if err != nil { + return "", err + } + + addresses, err := iface.Addrs() + if err != nil { + return "", err + } + + // FIXME: add IPv6 support + var v4Addr string + for _, addr := range addresses { + if ipnet, ok := addr.(*net.IPNet); ok && ipnet.IP.To4() != nil { + v4Addr = ipnet.IP.To4().String() + break + } + } + + if v4Addr == "" { + return "", errors.New("ipv4 addresses not found") + } + + return v4Addr, nil +} diff --git a/src/go/plugin/go.d/modules/ping/testdata/config.json b/src/go/plugin/go.d/modules/ping/testdata/config.json new file mode 100644 index 000000000..18df64529 --- /dev/null +++ b/src/go/plugin/go.d/modules/ping/testdata/config.json @@ -0,0 +1,11 @@ +{ + "update_every": 123, + "hosts": [ + "ok" + ], + "network": "ok", + "privileged": true, + "packets": 123, + "interval": 123.123, + "interface": "ok" +} diff --git a/src/go/plugin/go.d/modules/ping/testdata/config.yaml b/src/go/plugin/go.d/modules/ping/testdata/config.yaml new file mode 100644 index 000000000..5eacb9413 --- /dev/null +++ b/src/go/plugin/go.d/modules/ping/testdata/config.yaml @@ -0,0 +1,8 @@ +update_every: 123 +hosts: + - "ok" +network: "ok" +privileged: yes +packets: 123 +interval: 123.123 +interface: "ok" |