summaryrefslogtreecommitdiffstats
path: root/src/go/collectors/go.d.plugin/modules/ping
diff options
context:
space:
mode:
Diffstat (limited to '')
l---------src/go/collectors/go.d.plugin/modules/ping/README.md1
-rw-r--r--src/go/collectors/go.d.plugin/modules/ping/charts.go101
-rw-r--r--src/go/collectors/go.d.plugin/modules/ping/collect.go49
-rw-r--r--src/go/collectors/go.d.plugin/modules/ping/config_schema.json86
-rw-r--r--src/go/collectors/go.d.plugin/modules/ping/init.go39
-rw-r--r--src/go/collectors/go.d.plugin/modules/ping/integrations/ping.md236
-rw-r--r--src/go/collectors/go.d.plugin/modules/ping/metadata.yaml193
-rw-r--r--src/go/collectors/go.d.plugin/modules/ping/ping.go121
-rw-r--r--src/go/collectors/go.d.plugin/modules/ping/ping_test.go206
-rw-r--r--src/go/collectors/go.d.plugin/modules/ping/prober.go111
-rw-r--r--src/go/collectors/go.d.plugin/modules/ping/testdata/config.json11
-rw-r--r--src/go/collectors/go.d.plugin/modules/ping/testdata/config.yaml8
12 files changed, 1162 insertions, 0 deletions
diff --git a/src/go/collectors/go.d.plugin/modules/ping/README.md b/src/go/collectors/go.d.plugin/modules/ping/README.md
new file mode 120000
index 000000000..a1381e57b
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/ping/README.md
@@ -0,0 +1 @@
+integrations/ping.md \ No newline at end of file
diff --git a/src/go/collectors/go.d.plugin/modules/ping/charts.go b/src/go/collectors/go.d.plugin/modules/ping/charts.go
new file mode 100644
index 000000000..e117a18d5
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/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/go.d.plugin/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/collectors/go.d.plugin/modules/ping/collect.go b/src/go/collectors/go.d.plugin/modules/ping/collect.go
new file mode 100644
index 000000000..c162a2b15
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/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/collectors/go.d.plugin/modules/ping/config_schema.json b/src/go/collectors/go.d.plugin/modules/ping/config_schema.json
new file mode 100644
index 000000000..750c8fab7
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/ping/config_schema.json
@@ -0,0 +1,86 @@
+{
+ "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/collectors/go.d.plugin/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
+ }
+ },
+ "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)."
+ }
+ }
+}
diff --git a/src/go/collectors/go.d.plugin/modules/ping/init.go b/src/go/collectors/go.d.plugin/modules/ping/init.go
new file mode 100644
index 000000000..62d78c8e6
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/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/collectors/go.d.plugin/modules/ping/integrations/ping.md b/src/go/collectors/go.d.plugin/modules/ping/integrations/ping.md
new file mode 100644
index 000000000..5c347b10e
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/ping/integrations/ping.md
@@ -0,0 +1,236 @@
+<!--startmeta
+custom_edit_url: "https://github.com/netdata/netdata/edit/master/src/go/collectors/go.d.plugin/modules/ping/README.md"
+meta_yaml: "https://github.com/netdata/netdata/edit/master/src/go/collectors/go.d.plugin/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-tripe 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](https://github.com/netdata/netdata/blob/master/docs/netdata-agent/configuration.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><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><summary>Config</summary>
+
+```yaml
+jobs:
+ - name: example
+ hosts:
+ - 192.0.2.0
+ - 192.0.2.1
+
+```
+</details>
+
+##### Unprivileged mode
+
+An example configuration.
+
+<details><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><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
+
+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
+ ```
+
+
diff --git a/src/go/collectors/go.d.plugin/modules/ping/metadata.yaml b/src/go/collectors/go.d.plugin/modules/ping/metadata.yaml
new file mode 100644
index 000000000..e446e428d
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/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-tripe 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/collectors/go.d.plugin/modules/ping/ping.go b/src/go/collectors/go.d.plugin/modules/ping/ping.go
new file mode 100644
index 000000000..6f6c8a54b
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/modules/ping/ping.go
@@ -0,0 +1,121 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package ping
+
+import (
+ _ "embed"
+ "errors"
+ "time"
+
+ "github.com/netdata/netdata/go/go.d.plugin/agent/module"
+ "github.com/netdata/netdata/go/go.d.plugin/logger"
+ "github.com/netdata/netdata/go/go.d.plugin/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() },
+ })
+}
+
+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" json:"update_every"`
+ Hosts []string `yaml:"hosts" json:"hosts"`
+ Network string `yaml:"network" json:"network"`
+ Privileged bool `yaml:"privileged" json:"privileged"`
+ SendPackets int `yaml:"packets" json:"packets"`
+ Interval web.Duration `yaml:"interval" json:"interval"`
+ Interface string `yaml:"interface" 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/collectors/go.d.plugin/modules/ping/ping_test.go b/src/go/collectors/go.d.plugin/modules/ping/ping_test.go
new file mode 100644
index 000000000..856449d33
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/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/go.d.plugin/agent/module"
+ "github.com/netdata/netdata/go/go.d.plugin/logger"
+
+ 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/collectors/go.d.plugin/modules/ping/prober.go b/src/go/collectors/go.d.plugin/modules/ping/prober.go
new file mode 100644
index 000000000..e0d9925b4
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/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/go.d.plugin/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/collectors/go.d.plugin/modules/ping/testdata/config.json b/src/go/collectors/go.d.plugin/modules/ping/testdata/config.json
new file mode 100644
index 000000000..18df64529
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/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/collectors/go.d.plugin/modules/ping/testdata/config.yaml b/src/go/collectors/go.d.plugin/modules/ping/testdata/config.yaml
new file mode 100644
index 000000000..5eacb9413
--- /dev/null
+++ b/src/go/collectors/go.d.plugin/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"