Port AP collector to Go (#18170)
Co-authored-by: ilyam8 <ilya@netdata.cloud>
Fotis Voutsas committed
Jul 16, 2024 at 12:32 UTC
2b2e857aff4884964577d4954b03f0d518bc8e47
24 files changed
+1059
-456
CMakeLists.txt
-2
@@ -2781,7 +2781,6 @@ install(FILES
2781
DESTINATION usr/lib/netdata/conf.d)
2782
2783
install(PROGRAMS
2784
- src/collectors/charts.d.plugin/ap/ap.chart.sh
2784
src/collectors/charts.d.plugin/apcupsd/apcupsd.chart.sh
2785
src/collectors/charts.d.plugin/example/example.chart.sh
2786
src/collectors/charts.d.plugin/libreswan/libreswan.chart.sh
@@ -2791,7 +2790,6 @@ install(PROGRAMS
2790
DESTINATION usr/libexec/netdata/charts.d)
2791
2792
install(FILES
2794
- src/collectors/charts.d.plugin/ap/ap.conf
2793
src/collectors/charts.d.plugin/apcupsd/apcupsd.conf
2794
src/collectors/charts.d.plugin/example/example.conf
2795
src/collectors/charts.d.plugin/libreswan/libreswan.conf
src/collectors/charts.d.plugin/ap/README.md
deleted
-1
@@ -1 +0,0 @@
1
-integrations/access_points.md
\ No newline at end of file
src/collectors/charts.d.plugin/ap/ap.chart.sh
deleted
-179
@@ -1,179 +0,0 @@
1
-# shellcheck shell=bash
2
-# no need for shebang - this file is loaded from charts.d.plugin
3
-# SPDX-License-Identifier: GPL-3.0-or-later
4
-
5
-# netdata
6
-# real-time performance and health monitoring, done right!
7
-# (C) 2016 Costa Tsaousis <costa@tsaousis.gr>
8
-#
9
-
10
-# _update_every is a special variable - it holds the number of seconds
11
-# between the calls of the _update() function
12
-ap_update_every=
13
-ap_priority=6900
14
-
15
-declare -A ap_devs=()
16
-
17
-# _check is called once, to find out if this chart should be enabled or not
18
-ap_check() {
19
- require_cmd iw || return 1
20
- local ev
21
- ev=$(run iw dev | awk '
22
- BEGIN {
23
- i = "";
24
- ssid = "";
25
- ap = 0;
26
- }
27
- /^[ \t]+Interface / {
28
- if( ap == 1 ) {
29
- print "ap_devs[" i "]=\"" ssid "\""
30
- }
31
-
32
- i = $2;
33
- ssid = "";
34
- ap = 0;
35
- }
36
- /^[ \t]+ssid / { ssid = $2; }
37
- /^[ \t]+type AP$/ { ap = 1; }
38
- END {
39
- if( ap == 1 ) {
40
- print "ap_devs[" i "]=\"" ssid "\""
41
- }
42
- }
43
- ')
44
- eval "${ev}"
45
-
46
- # this should return:
47
- # - 0 to enable the chart
48
- # - 1 to disable the chart
49
-
50
- [ ${#ap_devs[@]} -gt 0 ] && return 0
51
- error "no devices found in AP mode, with 'iw dev'"
52
- return 1
53
-}
54
-
55
-# _create is called once, to create the charts
56
-ap_create() {
57
- local ssid dev
58
-
59
- for dev in "${!ap_devs[@]}"; do
60
- ssid="${ap_devs[${dev}]}"
61
-
62
- # create the chart with 3 dimensions
63
- cat << EOF
64
-CHART ap_clients.${dev} '' "Connected clients to ${ssid} on ${dev}" "clients" ${dev} ap.clients line $((ap_priority + 1)) $ap_update_every '' '' 'ap'
65
-DIMENSION clients '' absolute 1 1
66
-
67
-CHART ap_bandwidth.${dev} '' "Bandwidth for ${ssid} on ${dev}" "kilobits/s" ${dev} ap.net area $((ap_priority + 2)) $ap_update_every '' '' 'ap'
68
-DIMENSION received '' incremental 8 1024
69
-DIMENSION sent '' incremental -8 1024
70
-
71
-CHART ap_packets.${dev} '' "Packets for ${ssid} on ${dev}" "packets/s" ${dev} ap.packets line $((ap_priority + 3)) $ap_update_every '' '' 'ap'
72
-DIMENSION received '' incremental 1 1
73
-DIMENSION sent '' incremental -1 1
74
-
75
-CHART ap_issues.${dev} '' "Transmit Issues for ${ssid} on ${dev}" "issues/s" ${dev} ap.issues line $((ap_priority + 4)) $ap_update_every '' '' 'ap'
76
-DIMENSION retries 'tx retries' incremental 1 1
77
-DIMENSION failures 'tx failures' incremental -1 1
78
-
79
-CHART ap_signal.${dev} '' "Average Signal for ${ssid} on ${dev}" "dBm" ${dev} ap.signal line $((ap_priority + 5)) $ap_update_every '' '' 'ap'
80
-DIMENSION signal 'average signal' absolute 1 1000
81
-
82
-CHART ap_bitrate.${dev} '' "Bitrate for ${ssid} on ${dev}" "Mbps" ${dev} ap.bitrate line $((ap_priority + 6)) $ap_update_every '' '' 'ap'
83
-DIMENSION receive '' absolute 1 1000
84
-DIMENSION transmit '' absolute -1 1000
85
-DIMENSION expected 'expected throughput' absolute 1 1000
86
-EOF
87
- done
88
-
89
- return 0
90
-}
91
-
92
-# _update is called continuously, to collect the values
93
-ap_update() {
94
- # the first argument to this function is the microseconds since last update
95
- # pass this parameter to the BEGIN statement (see below).
96
-
97
- # do all the work to collect / calculate the values
98
- # for each dimension
99
- # remember: KEEP IT SIMPLE AND SHORT
100
-
101
- for dev in "${!ap_devs[@]}"; do
102
- echo
103
- echo "DEVICE ${dev}"
104
- iw "${dev}" station dump
105
- done | awk '
106
- function zero_data() {
107
- dev = "";
108
- c = 0;
109
- rb = 0;
110
- tb = 0;
111
- rp = 0;
112
- tp = 0;
113
- tr = 0;
114
- tf = 0;
115
- tt = 0;
116
- rt = 0;
117
- s = 0;
118
- g = 0;
119
- e = 0;
120
- }
121
- function print_device() {
122
- if(dev != "" && length(dev) > 0) {
123
- print "BEGIN ap_clients." dev;
124
- print "SET clients = " c;
125
- print "END";
126
- print "BEGIN ap_bandwidth." dev;
127
- print "SET received = " rb;
128
- print "SET sent = " tb;
129
- print "END";
130
- print "BEGIN ap_packets." dev;
131
- print "SET received = " rp;
132
- print "SET sent = " tp;
133
- print "END";
134
- print "BEGIN ap_issues." dev;
135
- print "SET retries = " tr;
136
- print "SET failures = " tf;
137
- print "END";
138
-
139
- if( c == 0 ) c = 1;
140
- print "BEGIN ap_signal." dev;
141
- print "SET signal = " int(s / c);
142
- print "END";
143
- print "BEGIN ap_bitrate." dev;
144
- print "SET receive = " int(rt / c);
145
- print "SET transmit = " int(tt / c);
146
- print "SET expected = " int(e / c);
147
- print "END";
148
- }
149
- zero_data();
150
- }
151
- BEGIN {
152
- zero_data();
153
- }
154
- /^DEVICE / {
155
- print_device();
156
- dev = $2;
157
- }
158
- /^Station/ { c++; }
159
- /^[ \t]+rx bytes:/ { rb += $3; }
160
- /^[ \t]+tx bytes:/ { tb += $3; }
161
- /^[ \t]+rx packets:/ { rp += $3; }
162
- /^[ \t]+tx packets:/ { tp += $3; }
163
- /^[ \t]+tx retries:/ { tr += $3; }
164
- /^[ \t]+tx failed:/ { tf += $3; }
165
- /^[ \t]+signal:/ { x = $2; s += x * 1000; }
166
- /^[ \t]+rx bitrate:/ { x = $3; rt += x * 1000; }
167
- /^[ \t]+tx bitrate:/ { x = $3; tt += x * 1000; }
168
- /^[ \t]+expected throughput:(.*)Mbps/ {
169
- x=$3;
170
- sub(/Mbps/, "", x);
171
- e += x * 1000;
172
- }
173
- END {
174
- print_device();
175
- }
176
- '
177
-
178
- return 0
179
-}
src/collectors/charts.d.plugin/ap/ap.conf
deleted
-23
@@ -1,23 +0,0 @@
1
-# no need for shebang - this file is loaded from charts.d.plugin
2
-
3
-# netdata
4
-# real-time performance and health monitoring, done right!
5
-# (C) 2018 Costa Tsaousis <costa@tsaousis.gr>
6
-# GPL v3+
7
-
8
-# nothing fancy to configure.
9
-# this module will run
10
-# iw dev - to find wireless devices in AP mode
11
-# iw ${dev} station dump - to get connected clients
12
-# based on the above, it generates several charts
13
-
14
-# the data collection frequency
15
-# if unset, will inherit the netdata update frequency
16
-#ap_update_every=
17
-
18
-# the charts priority on the dashboard
19
-#ap_priority=6900
20
-
21
-# the number of retries to do in case of failure
22
-# before disabling the module
23
-#ap_retries=10
src/collectors/charts.d.plugin/ap/integrations/access_points.md
deleted
-207
@@ -1,207 +0,0 @@
1
-<!--startmeta
2
-custom_edit_url: "https://github.com/netdata/netdata/edit/master/src/collectors/charts.d.plugin/ap/README.md"
3
-meta_yaml: "https://github.com/netdata/netdata/edit/master/src/collectors/charts.d.plugin/ap/metadata.yaml"
4
-sidebar_label: "Access Points"
5
-learn_status: "Published"
6
-learn_rel_path: "Collecting Metrics/Linux Systems/Network"
7
-most_popular: False
8
-message: "DO NOT EDIT THIS FILE DIRECTLY, IT IS GENERATED BY THE COLLECTOR'S metadata.yaml FILE"
9
-endmeta-->
10
-
11
-# Access Points
12
-
13
-
14
-<img src="https://netdata.cloud/img/network-wired.svg" width="150"/>
15
-
16
-
17
-Plugin: charts.d.plugin
18
-Module: ap
19
-
20
-<img src="https://img.shields.io/badge/maintained%20by-Netdata-%2300ab44" />
21
-
22
-## Overview
23
-
24
-The ap collector visualizes data related to wireless access points.
25
-
26
-It uses the `iw` command line utility to detect access points. For each interface that is of `type AP`, it then runs `iw INTERFACE station dump` and collects statistics.
27
-
28
-This collector is only supported on the following platforms:
29
-
30
-- Linux
31
-
32
-This collector only supports collecting metrics from a single instance of this integration.
33
-
34
-
35
-### Default Behavior
36
-
37
-#### Auto-Detection
38
-
39
-The plugin is able to auto-detect if you are running access points on your linux box.
40
-
41
-#### Limits
42
-
43
-The default configuration for this integration does not impose any limits on data collection.
44
-
45
-#### Performance Impact
46
-
47
-The default configuration for this integration is not expected to impose a significant performance impact on the system.
48
-
49
-
50
-## Metrics
51
-
52
-Metrics grouped by *scope*.
53
-
54
-The scope defines the instance that the metric belongs to. An instance is uniquely identified by a set of labels.
55
-
56
-
57
-
58
-### Per wireless device
59
-
60
-These metrics refer to the entire monitored application.
61
-
62
-This scope has no labels.
63
-
64
-Metrics:
65
-
66
-| Metric | Dimensions | Unit |
67
-|:------|:----------|:----|
68
-| ap.clients | clients | clients |
69
-| ap.net | received, sent | kilobits/s |
70
-| ap.packets | received, sent | packets/s |
71
-| ap.issues | retries, failures | issues/s |
72
-| ap.signal | average signal | dBm |
73
-| ap.bitrate | receive, transmit, expected | Mbps |
74
-
75
-
76
-
77
-## Alerts
78
-
79
-There are no alerts configured by default for this integration.
80
-
81
-
82
-## Setup
83
-
84
-### Prerequisites
85
-
86
-#### Install charts.d plugin
87
-
88
-If [using our official native DEB/RPM packages](/packaging/installer/UPDATE.md#determine-which-installation-method-you-used), make sure `netdata-plugin-chartsd` is installed.
89
-
90
-
91
-#### `iw` utility.
92
-
93
-Make sure the `iw` utility is installed.
94
-
95
-
96
-### Configuration
97
-
98
-#### File
99
-
100
-The configuration file name for this integration is `charts.d/ap.conf`.
101
-
102
-
103
-You can edit the configuration file using the `edit-config` script from the
104
-Netdata [config directory](/docs/netdata-agent/configuration/README.md#the-netdata-config-directory).
105
-
106
-```bash
107
-cd /etc/netdata 2>/dev/null || cd /opt/netdata/etc/netdata
108
-sudo ./edit-config charts.d/ap.conf
109
-```
110
-#### Options
111
-
112
-The config file is sourced by the charts.d plugin. It's a standard bash file.
113
-
114
-The following collapsed table contains all the options that can be configured for the ap collector.
115
-
116
-
117
-<details open><summary>Config options</summary>
118
-
119
-| Name | Description | Default | Required |
120
-|:----|:-----------|:-------|:--------:|
121
-| ap_update_every | The data collection frequency. If unset, will inherit the netdata update frequency. | 1 | no |
122
-| ap_priority | Controls the order of charts at the netdata dashboard. | 6900 | no |
123
-| ap_retries | The number of retries to do in case of failure before disabling the collector. | 10 | no |
124
-
125
-</details>
126
-
127
-#### Examples
128
-
129
-##### Change the collection frequency
130
-
131
-Specify a custom collection frequence (update_every) for this collector
132
-
133
-```yaml
134
-# the data collection frequency
135
-# if unset, will inherit the netdata update frequency
136
-ap_update_every=10
137
-
138
-# the charts priority on the dashboard
139
-#ap_priority=6900
140
-
141
-# the number of retries to do in case of failure
142
-# before disabling the module
143
-#ap_retries=10
144
-
145
-```
146
-
147
-
148
-## Troubleshooting
149
-
150
-### Debug Mode
151
-
152
-To troubleshoot issues with the `ap` collector, run the `charts.d.plugin` with the debug option enabled. The output
153
-should give you clues as to why the collector isn't working.
154
-
155
-- Navigate to the `plugins.d` directory, usually at `/usr/libexec/netdata/plugins.d/`. If that's not the case on
156
- your system, open `netdata.conf` and look for the `plugins` setting under `[directories]`.
157
-
158
- ```bash
159
- cd /usr/libexec/netdata/plugins.d/
160
- ```
161
-
162
-- Switch to the `netdata` user.
163
-
164
- ```bash
165
- sudo -u netdata -s
166
- ```
167
-
168
-- Run the `charts.d.plugin` to debug the collector:
169
-
170
- ```bash
171
- ./charts.d.plugin debug 1 ap
172
- ```
173
-
174
-### Getting Logs
175
-
176
-If you're encountering problems with the `ap` collector, follow these steps to retrieve logs and identify potential issues:
177
-
178
-- **Run the command** specific to your system (systemd, non-systemd, or Docker container).
179
-- **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.
180
-
181
-#### System with systemd
182
-
183
-Use the following command to view logs generated since the last Netdata service restart:
184
-
185
-```bash
186
-journalctl _SYSTEMD_INVOCATION_ID="$(systemctl show --value --property=InvocationID netdata)" --namespace=netdata --grep ap
187
-```
188
-
189
-#### System without systemd
190
-
191
-Locate the collector log file, typically at `/var/log/netdata/collector.log`, and use `grep` to filter for collector's name:
192
-
193
-```bash
194
-grep ap /var/log/netdata/collector.log
195
-```
196
-
197
-**Note**: This method shows logs from all restarts. Focus on the **latest entries** for troubleshooting current issues.
198
-
199
-#### Docker Container
200
-
201
-If your Netdata runs in a Docker container named "netdata" (replace if different), use this command:
202
-
203
-```bash
204
-docker logs netdata 2>&1 | grep ap
205
-```
206
-
207
-
src/collectors/charts.d.plugin/charts.d.conf
-1
@@ -33,7 +33,6 @@
33
# enable_all_charts="yes"
34
35
# BY DEFAULT ENABLED MODULES
36
-# ap=yes
36
# apcupsd=yes
37
# libreswan=yes
38
# opensips=yes
src/collectors/charts.d.plugin/charts.d.plugin.in
+1
@@ -474,6 +474,7 @@ declare -A charts_enable_keyword=(
474
)
475
476
declare -A obsolete_charts=(
477
+ ['ap']="go.d/ap"
478
['apache']="python.d.plugin module"
479
['cpu_apps']="apps.plugin"
480
['cpufreq']="proc plugin"
src/go/plugin/go.d/README.md
+1
@@ -51,6 +51,7 @@ see the appropriate collector readme.
51
|:-------------------------------------------------------------------------------------------------------------------|:-----------------------------:|
52
| [adaptec_raid](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/adaptecraid) | Adaptec Hardware RAID |
53
| [activemq](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/activemq) | ActiveMQ |
54
+| [activemq](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/ap) | Access Points |
55
| [apache](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/apache) | Apache |
56
| [bind](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/bind) | ISC Bind |
57
| [cassandra](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/cassandra) | Cassandra |
src/go/plugin/go.d/config/go.d.conf
+1
@@ -17,6 +17,7 @@ max_procs: 0
17
modules:
18
# adaptec_raid: yes
19
# activemq: yes
20
+# ap: yes
21
# apache: yes
22
# bind: yes
23
# chrony: yes
src/go/plugin/go.d/config/go.d/ap.conf
new
+6
@@ -0,0 +1,6 @@
1
+## All available configuration options, their descriptions and default values:
2
+## https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/ap#readme
3
+
4
+jobs:
5
+ - name: local
6
+ binary_path: /usr/sbin/iw
src/go/plugin/go.d/modules/ap/ap.go
new
+113
@@ -0,0 +1,113 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ap
4
+
5
+import (
6
+ _ "embed"
7
+ "errors"
8
+ "time"
9
+
10
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
11
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/web"
12
+)
13
+
14
+//go:embed "config_schema.json"
15
+var configSchema string
16
+
17
+func init() {
18
+ module.Register("ap", module.Creator{
19
+ JobConfigSchema: configSchema,
20
+ Defaults: module.Defaults{
21
+ UpdateEvery: 10,
22
+ },
23
+ Create: func() module.Module { return New() },
24
+ Config: func() any { return &Config{} },
25
+ })
26
+}
27
+
28
+func New() *AP {
29
+ return &AP{
30
+ Config: Config{
31
+ BinaryPath: "/usr/sbin/iw",
32
+ Timeout: web.Duration(time.Second * 2),
33
+ },
34
+ charts: &module.Charts{},
35
+ seenIfaces: make(map[string]*iwInterface),
36
+ }
37
+}
38
+
39
+type Config struct {
40
+ UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
41
+ Timeout web.Duration `yaml:"timeout,omitempty" json:"timeout"`
42
+ BinaryPath string `yaml:"binary_path,omitempty" json:"binary_path"`
43
+}
44
+
45
+type (
46
+ AP struct {
47
+ module.Base
48
+ Config `yaml:",inline" json:""`
49
+
50
+ charts *module.Charts
51
+
52
+ exec iwBinary
53
+
54
+ seenIfaces map[string]*iwInterface
55
+ }
56
+ iwBinary interface {
57
+ devices() ([]byte, error)
58
+ stationStatistics(ifaceName string) ([]byte, error)
59
+ }
60
+)
61
+
62
+func (a *AP) Configuration() any {
63
+ return a.Config
64
+}
65
+
66
+func (a *AP) Init() error {
67
+ if err := a.validateConfig(); err != nil {
68
+ a.Errorf("config validation: %s", err)
69
+ return err
70
+ }
71
+
72
+ iw, err := a.initIwExec()
73
+ if err != nil {
74
+ a.Errorf("iw dev exec initialization: %v", err)
75
+ return err
76
+ }
77
+ a.exec = iw
78
+
79
+ return nil
80
+}
81
+
82
+func (a *AP) Check() error {
83
+ mx, err := a.collect()
84
+ if err != nil {
85
+ a.Error(err)
86
+ return err
87
+ }
88
+
89
+ if len(mx) == 0 {
90
+ return errors.New("no metrics collected")
91
+ }
92
+
93
+ return nil
94
+}
95
+
96
+func (a *AP) Charts() *module.Charts {
97
+ return a.charts
98
+}
99
+
100
+func (a *AP) Collect() map[string]int64 {
101
+ mx, err := a.collect()
102
+ if err != nil {
103
+ a.Error(err)
104
+ }
105
+
106
+ if len(mx) == 0 {
107
+ return nil
108
+ }
109
+
110
+ return mx
111
+}
112
+
113
+func (a *AP) Cleanup() {}
src/go/plugin/go.d/modules/ap/ap_test.go
new
+292
@@ -0,0 +1,292 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ap
4
+
5
+import (
6
+ "errors"
7
+ "os"
8
+ "testing"
9
+
10
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
11
+
12
+ "github.com/stretchr/testify/assert"
13
+ "github.com/stretchr/testify/require"
14
+)
15
+
16
+var (
17
+ dataConfigJSON, _ = os.ReadFile("testdata/config.json")
18
+ dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")
19
+
20
+ dataIwDevManaged, _ = os.ReadFile("testdata/iw_dev_managed.txt")
21
+
22
+ dataIwDevAP, _ = os.ReadFile("testdata/iw_dev_ap.txt")
23
+ dataIwStationDump, _ = os.ReadFile("testdata/station_dump.txt")
24
+)
25
+
26
+func Test_testDataIsValid(t *testing.T) {
27
+ for name, data := range map[string][]byte{
28
+ "dataConfigJSON": dataConfigJSON,
29
+ "dataConfigYAML": dataConfigYAML,
30
+ "dataIwDevManaged": dataIwDevManaged,
31
+ "dataIwDevAP": dataIwDevAP,
32
+ "dataIwStationDump": dataIwStationDump,
33
+ } {
34
+ require.NotNil(t, data, name)
35
+ }
36
+}
37
+
38
+func TestAP_Configuration(t *testing.T) {
39
+ module.TestConfigurationSerialize(t, &AP{}, dataConfigJSON, dataConfigYAML)
40
+}
41
+
42
+func TestAP_Init(t *testing.T) {
43
+ tests := map[string]struct {
44
+ config Config
45
+ wantFail bool
46
+ }{
47
+ "fails if 'binary_path' is not set": {
48
+ wantFail: true,
49
+ config: Config{
50
+ BinaryPath: "",
51
+ },
52
+ },
53
+ "fails if failed to find binary": {
54
+ wantFail: true,
55
+ config: Config{
56
+ BinaryPath: "iw!!!",
57
+ },
58
+ },
59
+ }
60
+
61
+ for name, test := range tests {
62
+ t.Run(name, func(t *testing.T) {
63
+ pf := New()
64
+ pf.Config = test.config
65
+
66
+ if test.wantFail {
67
+ assert.Error(t, pf.Init())
68
+ } else {
69
+ assert.NoError(t, pf.Init())
70
+ }
71
+ })
72
+ }
73
+}
74
+
75
+func TestAP_Cleanup(t *testing.T) {
76
+ tests := map[string]struct {
77
+ prepare func() *AP
78
+ }{
79
+ "not initialized exec": {
80
+ prepare: func() *AP {
81
+ return New()
82
+ },
83
+ },
84
+ "after check": {
85
+ prepare: func() *AP {
86
+ ap := New()
87
+ ap.exec = prepareMockOk()
88
+ _ = ap.Check()
89
+ return ap
90
+ },
91
+ },
92
+ "after collect": {
93
+ prepare: func() *AP {
94
+ ap := New()
95
+ ap.exec = prepareMockOk()
96
+ _ = ap.Collect()
97
+ return ap
98
+ },
99
+ },
100
+ }
101
+
102
+ for name, test := range tests {
103
+ t.Run(name, func(t *testing.T) {
104
+ pf := test.prepare()
105
+
106
+ assert.NotPanics(t, pf.Cleanup)
107
+ })
108
+ }
109
+}
110
+
111
+func TestAP_Charts(t *testing.T) {
112
+ assert.NotNil(t, New().Charts())
113
+}
114
+
115
+func TestAP_Check(t *testing.T) {
116
+ tests := map[string]struct {
117
+ prepareMock func() *mockIwExec
118
+ wantFail bool
119
+ }{
120
+ "success case": {
121
+ wantFail: false,
122
+ prepareMock: prepareMockOk,
123
+ },
124
+ "no ap devices": {
125
+ wantFail: true,
126
+ prepareMock: prepareMockNoAPDevices,
127
+ },
128
+ "error on devices call": {
129
+ wantFail: true,
130
+ prepareMock: prepareMockErrOnDevices,
131
+ },
132
+ "error on station stats call": {
133
+ wantFail: true,
134
+ prepareMock: prepareMockErrOnStationStats,
135
+ },
136
+ "unexpected response": {
137
+ wantFail: true,
138
+ prepareMock: prepareMockUnexpectedResponse,
139
+ },
140
+ }
141
+
142
+ for name, test := range tests {
143
+ t.Run(name, func(t *testing.T) {
144
+ ap := New()
145
+ ap.exec = test.prepareMock()
146
+
147
+ if test.wantFail {
148
+ assert.Error(t, ap.Check())
149
+ } else {
150
+ assert.NoError(t, ap.Check())
151
+ }
152
+ })
153
+ }
154
+}
155
+
156
+func TestAP_Collect(t *testing.T) {
157
+ tests := map[string]struct {
158
+ prepareMock func() *mockIwExec
159
+ wantMetrics map[string]int64
160
+ wantCharts int
161
+ }{
162
+ "success case": {
163
+ prepareMock: prepareMockOk,
164
+ wantCharts: len(apChartsTmpl) * 2,
165
+ wantMetrics: map[string]int64{
166
+ "ap_wlp1s0_testing_average_signal": -34000,
167
+ "ap_wlp1s0_testing_bitrate_receive": 65500,
168
+ "ap_wlp1s0_testing_bitrate_transmit": 65000,
169
+ "ap_wlp1s0_testing_bw_received": 95117,
170
+ "ap_wlp1s0_testing_bw_sent": 8270,
171
+ "ap_wlp1s0_testing_clients": 2,
172
+ "ap_wlp1s0_testing_issues_failures": 1,
173
+ "ap_wlp1s0_testing_issues_retries": 1,
174
+ "ap_wlp1s0_testing_packets_received": 2531,
175
+ "ap_wlp1s0_testing_packets_sent": 38,
176
+ "ap_wlp1s1_testing_average_signal": -34000,
177
+ "ap_wlp1s1_testing_bitrate_receive": 65500,
178
+ "ap_wlp1s1_testing_bitrate_transmit": 65000,
179
+ "ap_wlp1s1_testing_bw_received": 95117,
180
+ "ap_wlp1s1_testing_bw_sent": 8270,
181
+ "ap_wlp1s1_testing_clients": 2,
182
+ "ap_wlp1s1_testing_issues_failures": 1,
183
+ "ap_wlp1s1_testing_issues_retries": 1,
184
+ "ap_wlp1s1_testing_packets_received": 2531,
185
+ "ap_wlp1s1_testing_packets_sent": 38,
186
+ },
187
+ },
188
+ "no ap devices": {
189
+ prepareMock: prepareMockNoAPDevices,
190
+ wantMetrics: nil,
191
+ },
192
+ "error on devices call": {
193
+ prepareMock: prepareMockErrOnDevices,
194
+ wantMetrics: nil,
195
+ },
196
+ "error on statis stats call": {
197
+ prepareMock: prepareMockErrOnStationStats,
198
+ wantMetrics: nil,
199
+ },
200
+ "unexpected response": {
201
+ prepareMock: prepareMockUnexpectedResponse,
202
+ wantMetrics: nil,
203
+ },
204
+ }
205
+
206
+ for name, test := range tests {
207
+ t.Run(name, func(t *testing.T) {
208
+ ap := New()
209
+ ap.exec = test.prepareMock()
210
+
211
+ mx := ap.Collect()
212
+
213
+ assert.Equal(t, test.wantMetrics, mx)
214
+ assert.Equal(t, test.wantCharts, len(*ap.Charts()), "Charts")
215
+ testMetricsHasAllChartsDims(t, ap, mx)
216
+ })
217
+ }
218
+}
219
+
220
+func testMetricsHasAllChartsDims(t *testing.T, ap *AP, mx map[string]int64) {
221
+ for _, chart := range *ap.Charts() {
222
+ if chart.Obsolete {
223
+ continue
224
+ }
225
+ for _, dim := range chart.Dims {
226
+ _, ok := mx[dim.ID]
227
+ assert.Truef(t, ok, "collected metrics has no data for dim '%s' chart '%s'", dim.ID, chart.ID)
228
+ }
229
+ for _, v := range chart.Vars {
230
+ _, ok := mx[v.ID]
231
+ assert.Truef(t, ok, "collected metrics has no data for var '%s' chart '%s'", v.ID, chart.ID)
232
+ }
233
+ }
234
+}
235
+
236
+func prepareMockOk() *mockIwExec {
237
+ return &mockIwExec{
238
+ devicesData: dataIwDevAP,
239
+ stationStatsData: dataIwStationDump,
240
+ }
241
+}
242
+
243
+func prepareMockNoAPDevices() *mockIwExec {
244
+ return &mockIwExec{
245
+ devicesData: dataIwDevManaged,
246
+ }
247
+}
248
+
249
+func prepareMockErrOnDevices() *mockIwExec {
250
+ return &mockIwExec{
251
+ errOnDevices: true,
252
+ }
253
+}
254
+
255
+func prepareMockErrOnStationStats() *mockIwExec {
256
+ return &mockIwExec{
257
+ devicesData: dataIwDevAP,
258
+ errOnStationStats: true,
259
+ }
260
+}
261
+
262
+func prepareMockUnexpectedResponse() *mockIwExec {
263
+ return &mockIwExec{
264
+ devicesData: []byte(`
265
+Lorem ipsum dolor sit amet, consectetur adipiscing elit.
266
+Nulla malesuada erat id magna mattis, eu viverra tellus rhoncus.
267
+Fusce et felis pulvinar, posuere sem non, porttitor eros.
268
+`),
269
+ }
270
+}
271
+
272
+type mockIwExec struct {
273
+ errOnDevices bool
274
+ errOnStationStats bool
275
+ devicesData []byte
276
+ stationStatsData []byte
277
+}
278
+
279
+func (m *mockIwExec) devices() ([]byte, error) {
280
+ if m.errOnDevices {
281
+ return nil, errors.New("mock.devices() error")
282
+ }
283
+
284
+ return m.devicesData, nil
285
+}
286
+
287
+func (m *mockIwExec) stationStatistics(_ string) ([]byte, error) {
288
+ if m.errOnStationStats {
289
+ return nil, errors.New("mock.stationStatistics() error")
290
+ }
291
+ return m.stationStatsData, nil
292
+}
src/go/plugin/go.d/modules/ap/charts.go
new
+147
@@ -0,0 +1,147 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ap
4
+
5
+import (
6
+ "fmt"
7
+ "strings"
8
+
9
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
10
+)
11
+
12
+const (
13
+ prioClients = module.Priority + iota
14
+ prioBandwidth
15
+ prioPackets
16
+ prioIssues
17
+ prioSignal
18
+ prioBitrate
19
+)
20
+
21
+var apChartsTmpl = module.Charts{
22
+ apClientsChartTmpl.Copy(),
23
+ apBandwidthChartTmpl.Copy(),
24
+ apPacketsChartTmpl.Copy(),
25
+ apIssuesChartTmpl.Copy(),
26
+ apSignalChartTmpl.Copy(),
27
+ apBitrateChartTmpl.Copy(),
28
+}
29
+
30
+var (
31
+ apClientsChartTmpl = module.Chart{
32
+ ID: "ap_%s_%s_clients",
33
+ Title: "Connected clients",
34
+ Fam: "clients",
35
+ Units: "clients",
36
+ Ctx: "ap.clients",
37
+ Type: module.Line,
38
+ Priority: prioClients,
39
+ Dims: module.Dims{
40
+ {ID: "ap_%s_%s_clients", Name: "clients"},
41
+ },
42
+ }
43
+
44
+ apBandwidthChartTmpl = module.Chart{
45
+ ID: "ap_%s_%s_bandwidth",
46
+ Title: "Bandwidth",
47
+ Units: "kilobits/s",
48
+ Fam: "traffic",
49
+ Ctx: "ap.net",
50
+ Type: module.Area,
51
+ Priority: prioBandwidth,
52
+ Dims: module.Dims{
53
+ {ID: "ap_%s_%s_bw_received", Name: "received", Algo: module.Incremental, Mul: 8, Div: 1000},
54
+ {ID: "ap_%s_%s_bw_sent", Name: "sent", Algo: module.Incremental, Mul: -8, Div: 1000},
55
+ },
56
+ }
57
+
58
+ apPacketsChartTmpl = module.Chart{
59
+ ID: "ap_%s_%s_packets",
60
+ Title: "Packets",
61
+ Fam: "packets",
62
+ Units: "packets/s",
63
+ Ctx: "ap.packets",
64
+ Type: module.Line,
65
+ Priority: prioPackets,
66
+ Dims: module.Dims{
67
+ {ID: "ap_%s_%s_packets_received", Name: "received", Algo: module.Incremental},
68
+ {ID: "ap_%s_%s_packets_sent", Name: "sent", Algo: module.Incremental, Mul: -1},
69
+ },
70
+ }
71
+
72
+ apIssuesChartTmpl = module.Chart{
73
+ ID: "ap_%s_%s_issues",
74
+ Title: "Transmit issues",
75
+ Fam: "issues",
76
+ Units: "issues/s",
77
+ Ctx: "ap.issues",
78
+ Type: module.Line,
79
+ Priority: prioIssues,
80
+ Dims: module.Dims{
81
+ {ID: "ap_%s_%s_issues_retries", Name: "tx retries", Algo: module.Incremental},
82
+ {ID: "ap_%s_%s_issues_failures", Name: "tx failures", Algo: module.Incremental, Mul: -1},
83
+ },
84
+ }
85
+
86
+ apSignalChartTmpl = module.Chart{
87
+ ID: "ap_%s_%s_signal",
88
+ Title: "Average Signal",
89
+ Units: "dBm",
90
+ Fam: "signal",
91
+ Ctx: "ap.signal",
92
+ Type: module.Line,
93
+ Priority: prioSignal,
94
+ Dims: module.Dims{
95
+ {ID: "ap_%s_%s_average_signal", Name: "average signal", Div: precision},
96
+ },
97
+ }
98
+
99
+ apBitrateChartTmpl = module.Chart{
100
+ ID: "ap_%s_%s_bitrate",
101
+ Title: "Bitrate",
102
+ Units: "Mbps",
103
+ Fam: "bitrate",
104
+ Ctx: "ap.bitrate",
105
+ Type: module.Line,
106
+ Priority: prioBitrate,
107
+ Dims: module.Dims{
108
+ {ID: "ap_%s_%s_bitrate_receive", Name: "receive", Div: precision},
109
+ {ID: "ap_%s_%s_bitrate_transmit", Name: "transmit", Mul: -1, Div: precision},
110
+ },
111
+ }
112
+)
113
+
114
+func (a *AP) addInterfaceCharts(dev *iwInterface) {
115
+ charts := apChartsTmpl.Copy()
116
+
117
+ for _, chart := range *charts {
118
+ chart.ID = fmt.Sprintf(chart.ID, dev.name, cleanSSID(dev.ssid))
119
+ chart.Labels = []module.Label{
120
+ {Key: "device", Value: dev.name},
121
+ {Key: "ssid", Value: dev.ssid},
122
+ }
123
+ for _, dim := range chart.Dims {
124
+ dim.ID = fmt.Sprintf(dim.ID, dev.name, dev.ssid)
125
+ }
126
+ }
127
+
128
+ if err := a.Charts().Add(*charts...); err != nil {
129
+ a.Warning(err)
130
+ }
131
+
132
+}
133
+
134
+func (a *AP) removeInterfaceCharts(dev *iwInterface) {
135
+ px := fmt.Sprintf("ap_%s_%s_", dev.name, cleanSSID(dev.ssid))
136
+ for _, chart := range *a.Charts() {
137
+ if strings.HasPrefix(chart.ID, px) {
138
+ chart.MarkRemove()
139
+ chart.MarkNotCreated()
140
+ }
141
+ }
142
+}
143
+
144
+func cleanSSID(ssid string) string {
145
+ r := strings.NewReplacer(" ", "_", ".", "_")
146
+ return r.Replace(ssid)
147
+}
src/go/plugin/go.d/modules/ap/collect.go
new
+221
@@ -0,0 +1,221 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ap
4
+
5
+import (
6
+ "bufio"
7
+ "bytes"
8
+ "errors"
9
+ "fmt"
10
+ "strconv"
11
+ "strings"
12
+)
13
+
14
+const precision = 1000
15
+
16
+type iwInterface struct {
17
+ name string
18
+ ssid string
19
+ typ string
20
+}
21
+
22
+type stationStats struct {
23
+ clients int64
24
+ rxBytes int64
25
+ rxPackets int64
26
+ txBytes int64
27
+ txPackets int64
28
+ txRetries int64
29
+ txFailed int64
30
+ signalAvg int64
31
+ txBitrate float64
32
+ rxBitrate float64
33
+}
34
+
35
+func (a *AP) collect() (map[string]int64, error) {
36
+ bs, err := a.exec.devices()
37
+ if err != nil {
38
+ return nil, err
39
+ }
40
+
41
+ // TODO: call this periodically, not on every data collection
42
+ apInterfaces, err := parseIwDevices(bs)
43
+ if err != nil {
44
+ return nil, fmt.Errorf("parsing AP interfaces: %v", err)
45
+ }
46
+
47
+ if len(apInterfaces) == 0 {
48
+ return nil, errors.New("no type AP interfaces found")
49
+ }
50
+
51
+ mx := make(map[string]int64)
52
+ seen := make(map[string]bool)
53
+
54
+ for _, iface := range apInterfaces {
55
+ bs, err = a.exec.stationStatistics(iface.name)
56
+ if err != nil {
57
+ return nil, fmt.Errorf("getting station statistics for %s: %v", iface, err)
58
+ }
59
+
60
+ stats, err := parseIwStationStatistics(bs)
61
+ if err != nil {
62
+ return nil, fmt.Errorf("parsing station statistics for %s: %v", iface, err)
63
+ }
64
+
65
+ key := fmt.Sprintf("%s-%s", iface.name, iface.ssid)
66
+
67
+ seen[key] = true
68
+
69
+ if _, ok := a.seenIfaces[key]; !ok {
70
+ a.seenIfaces[key] = iface
71
+ a.addInterfaceCharts(iface)
72
+ }
73
+
74
+ px := fmt.Sprintf("ap_%s_%s_", iface.name, iface.ssid)
75
+
76
+ mx[px+"clients"] = stats.clients
77
+ mx[px+"bw_received"] = stats.rxBytes
78
+ mx[px+"bw_sent"] = stats.txBytes
79
+ mx[px+"packets_received"] = stats.rxPackets
80
+ mx[px+"packets_sent"] = stats.txPackets
81
+ mx[px+"issues_retries"] = stats.txRetries
82
+ mx[px+"issues_failures"] = stats.txFailed
83
+ mx[px+"average_signal"], mx[px+"bitrate_receive"], mx[px+"bitrate_transmit"] = 0, 0, 0
84
+ if clients := float64(stats.clients); clients > 0 {
85
+ mx[px+"average_signal"] = int64(float64(stats.signalAvg) / clients * precision)
86
+ mx[px+"bitrate_receive"] = int64(stats.rxBitrate / clients * precision)
87
+ mx[px+"bitrate_transmit"] = int64(stats.txBitrate / clients * precision)
88
+ }
89
+ }
90
+
91
+ for key, iface := range a.seenIfaces {
92
+ if !seen[key] {
93
+ delete(a.seenIfaces, key)
94
+ a.removeInterfaceCharts(iface)
95
+ }
96
+ }
97
+
98
+ return mx, nil
99
+}
100
+
101
+func parseIwDevices(resp []byte) ([]*iwInterface, error) {
102
+ ifaces := make(map[string]*iwInterface)
103
+ var iface *iwInterface
104
+
105
+ sc := bufio.NewScanner(bytes.NewReader(resp))
106
+
107
+ for sc.Scan() {
108
+ line := strings.TrimSpace(sc.Text())
109
+
110
+ switch {
111
+ case strings.HasPrefix(line, "Interface"):
112
+ parts := strings.Fields(line)
113
+ if len(parts) != 2 {
114
+ return nil, fmt.Errorf("invalid interface line: '%s'", line)
115
+ }
116
+ name := parts[1]
117
+ if _, ok := ifaces[name]; !ok {
118
+ iface = &iwInterface{name: name}
119
+ ifaces[name] = iface
120
+ }
121
+ case strings.HasPrefix(line, "ssid") && iface != nil:
122
+ parts := strings.Fields(line)
123
+ if len(parts) != 2 {
124
+ return nil, fmt.Errorf("invalid ssid line: '%s'", line)
125
+ }
126
+ iface.ssid = parts[1]
127
+ case strings.HasPrefix(line, "type") && iface != nil:
128
+ parts := strings.Fields(line)
129
+ if len(parts) != 2 {
130
+ return nil, fmt.Errorf("invalid type line: '%s'", line)
131
+ }
132
+ iface.typ = parts[1]
133
+ }
134
+ }
135
+
136
+ var apIfaces []*iwInterface
137
+
138
+ for _, iface := range ifaces {
139
+ if strings.ToLower(iface.typ) == "ap" {
140
+ apIfaces = append(apIfaces, iface)
141
+ }
142
+ }
143
+
144
+ return apIfaces, nil
145
+}
146
+
147
+func parseIwStationStatistics(resp []byte) (*stationStats, error) {
148
+ var stats stationStats
149
+
150
+ sc := bufio.NewScanner(bytes.NewReader(resp))
151
+
152
+ for sc.Scan() {
153
+ line := strings.TrimSpace(sc.Text())
154
+
155
+ var v float64
156
+ var err error
157
+
158
+ switch {
159
+ case strings.HasPrefix(line, "Station"):
160
+ stats.clients++
161
+ case strings.HasPrefix(line, "rx bytes:"):
162
+ if v, err = get3rdValue(line); err == nil {
163
+ stats.rxBytes += int64(v)
164
+ }
165
+ case strings.HasPrefix(line, "rx packets:"):
166
+ if v, err = get3rdValue(line); err == nil {
167
+ stats.rxPackets += int64(v)
168
+ }
169
+ case strings.HasPrefix(line, "tx bytes:"):
170
+ if v, err = get3rdValue(line); err == nil {
171
+ stats.txBytes += int64(v)
172
+ }
173
+ case strings.HasPrefix(line, "tx packets:"):
174
+ if v, err = get3rdValue(line); err == nil {
175
+ stats.txPackets += int64(v)
176
+ }
177
+ case strings.HasPrefix(line, "tx retries:"):
178
+ if v, err = get3rdValue(line); err == nil {
179
+ stats.txRetries += int64(v)
180
+ }
181
+ case strings.HasPrefix(line, "tx failed:"):
182
+ if v, err = get3rdValue(line); err == nil {
183
+ stats.txFailed += int64(v)
184
+ }
185
+ case strings.HasPrefix(line, "signal avg:"):
186
+ if v, err = get3rdValue(line); err == nil {
187
+ stats.signalAvg += int64(v)
188
+ }
189
+ case strings.HasPrefix(line, "tx bitrate:"):
190
+ if v, err = get3rdValue(line); err == nil {
191
+ stats.txBitrate += v
192
+ }
193
+ case strings.HasPrefix(line, "rx bitrate:"):
194
+ if v, err = get3rdValue(line); err == nil {
195
+ stats.rxBitrate += v
196
+ }
197
+ default:
198
+ continue
199
+ }
200
+
201
+ if err != nil {
202
+ return nil, fmt.Errorf("parsing line '%s': %v", line, err)
203
+ }
204
+ }
205
+
206
+ return &stats, nil
207
+}
208
+
209
+func get3rdValue(line string) (float64, error) {
210
+ parts := strings.Fields(line)
211
+ if len(parts) < 3 {
212
+ return 0.0, errors.New("invalid format")
213
+ }
214
+
215
+ v := parts[2]
216
+
217
+ if v == "-" {
218
+ return 0.0, nil
219
+ }
220
+ return strconv.ParseFloat(v, 64)
221
+}
src/go/plugin/go.d/modules/ap/config_schema.json
new
+47
@@ -0,0 +1,47 @@
1
+{
2
+ "jsonSchema": {
3
+ "$schema": "http://json-schema.org/draft-07/schema#",
4
+ "title": "Access Point collector configuration.",
5
+ "type": "object",
6
+ "properties": {
7
+ "update_every": {
8
+ "title": "Update every",
9
+ "description": "Data collection interval, measured in seconds.",
10
+ "type": "integer",
11
+ "minimum": 1,
12
+ "default": 10
13
+ },
14
+ "binary_path": {
15
+ "title": "Binary path",
16
+ "description": "Path to the `iw` binary.",
17
+ "type": "string",
18
+ "default": "/usr/sbin/iw"
19
+ },
20
+ "timeout": {
21
+ "title": "Timeout",
22
+ "description": "Timeout for executing the binary, specified in seconds.",
23
+ "type": "number",
24
+ "minimum": 0.5,
25
+ "default": 2
26
+ }
27
+ },
28
+ "required": [
29
+ "binary_path"
30
+ ],
31
+ "additionalProperties": false,
32
+ "patternProperties": {
33
+ "^name$": {}
34
+ }
35
+ },
36
+ "uiSchema": {
37
+ "uiOptions": {
38
+ "fullPage": true
39
+ },
40
+ "binary_path": {
41
+ "ui:help": "If an absolute path is provided, the collector will use it directly; otherwise, it will search for the binary in directories specified in the PATH environment variable."
42
+ },
43
+ "timeout": {
44
+ "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)."
45
+ }
46
+ }
47
+}
\ No newline at end of file
src/go/plugin/go.d/modules/ap/exec.go
new
+56
@@ -0,0 +1,56 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ap
4
+
5
+import (
6
+ "context"
7
+ "fmt"
8
+ "os/exec"
9
+ "time"
10
+
11
+ "github.com/netdata/netdata/go/plugins/logger"
12
+)
13
+
14
+func newIwExec(binPath string, timeout time.Duration) *iwCliExec {
15
+ return &iwCliExec{
16
+ binPath: binPath,
17
+ timeout: timeout,
18
+ }
19
+}
20
+
21
+type iwCliExec struct {
22
+ *logger.Logger
23
+
24
+ binPath string
25
+ timeout time.Duration
26
+}
27
+
28
+func (e *iwCliExec) devices() ([]byte, error) {
29
+ ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
30
+ defer cancel()
31
+
32
+ cmd := exec.CommandContext(ctx, e.binPath, "dev")
33
+ e.Debugf("executing '%s'", cmd)
34
+
35
+ bs, err := cmd.Output()
36
+ if err != nil {
37
+ return nil, fmt.Errorf("error on '%s': %v", cmd, err)
38
+ }
39
+
40
+ return bs, nil
41
+}
42
+
43
+func (e *iwCliExec) stationStatistics(ifaceName string) ([]byte, error) {
44
+ ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
45
+ defer cancel()
46
+
47
+ cmd := exec.CommandContext(ctx, e.binPath, ifaceName, "station", "dump")
48
+ e.Debugf("executing '%s'", cmd)
49
+
50
+ bs, err := cmd.Output()
51
+ if err != nil {
52
+ return nil, fmt.Errorf("error on '%s': %v", cmd, err)
53
+ }
54
+
55
+ return bs, nil
56
+}
src/go/plugin/go.d/modules/ap/init.go
new
+37
@@ -0,0 +1,37 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package ap
4
+
5
+import (
6
+ "errors"
7
+ "os"
8
+ "os/exec"
9
+ "strings"
10
+)
11
+
12
+func (a *AP) validateConfig() error {
13
+ if a.BinaryPath == "" {
14
+ return errors.New("no iw binary path specified")
15
+ }
16
+ return nil
17
+}
18
+
19
+func (a *AP) initIwExec() (iwBinary, error) {
20
+ binPath := a.BinaryPath
21
+
22
+ if !strings.HasPrefix(binPath, "/") {
23
+ path, err := exec.LookPath(binPath)
24
+ if err != nil {
25
+ return nil, err
26
+ }
27
+ binPath = path
28
+ }
29
+
30
+ if _, err := os.Stat(binPath); err != nil {
31
+ return nil, err
32
+ }
33
+
34
+ iw := newIwExec(binPath, a.Timeout.Duration())
35
+
36
+ return iw, nil
37
+}
src/go/plugin/go.d/modules/ap/metadata.yaml
renamed
+34
-43
@@ -1,7 +1,7 @@
1
-plugin_name: charts.d.plugin
1
+plugin_name: go.d.plugin
2
modules:
3
- meta:
4
- plugin_name: charts.d.plugin
4
+ plugin_name: go.d.plugin
5
module_name: ap
6
monitored_instance:
7
name: Access Points
@@ -24,7 +24,7 @@ modules:
24
overview:
25
data_collection:
26
metrics_description: "The ap collector visualizes data related to wireless access points."
27
- method_description: "It uses the `iw` command line utility to detect access points. For each interface that is of `type AP`, it then runs `iw INTERFACE station dump` and collects statistics."
27
+ method_description: "It uses the `iw` command line utility to detect access points. Initially, the `iw dev` command is run. For each interface that is found to be of `type AP`, `iw INTERFACE station dump` is executed to collect metrics."
28
supported_platforms:
29
include: [Linux]
30
exclude: []
@@ -33,7 +33,7 @@ modules:
33
description: ""
34
default_behavior:
35
auto_detection:
36
- description: "The plugin is able to auto-detect if you are running access points on your linux box."
36
+ description: "The plugin is able to auto-detect any access points on your Linux machine."
37
limits:
38
description: ""
39
performance_impact:
@@ -41,53 +41,41 @@ modules:
41
setup:
42
prerequisites:
43
list:
44
- - title: "Install charts.d plugin"
45
- description: |
46
- If [using our official native DEB/RPM packages](/packaging/installer/UPDATE.md#determine-which-installation-method-you-used), make sure `netdata-plugin-chartsd` is installed.
44
- title: "`iw` utility."
45
description: "Make sure the `iw` utility is installed."
46
configuration:
47
file:
51
- name: charts.d/ap.conf
48
+ name: go.d/ap.conf
49
options:
50
description: |
54
- The config file is sourced by the charts.d plugin. It's a standard bash file.
55
-
56
- The following collapsed table contains all the options that can be configured for the ap collector.
51
+ The following options can be defined globally: update_every.
52
folding:
58
- title: "Config options"
53
+ title: Config options
54
enabled: true
55
list:
61
- - name: ap_update_every
62
- description: The data collection frequency. If unset, will inherit the netdata update frequency.
63
- default_value: 1
64
- required: false
65
- - name: ap_priority
66
- description: Controls the order of charts at the netdata dashboard.
67
- default_value: 6900
68
- required: false
69
- - name: ap_retries
70
- description: The number of retries to do in case of failure before disabling the collector.
56
+ - name: update_every
57
+ description: Data collection frequency.
58
default_value: 10
59
required: false
60
+ - name: binary_path
61
+ description: Path to the `iw` binary. If an absolute path is provided, the collector will use it directly; otherwise, it will search for the binary in directories specified in the PATH environment variable.
62
+ default_value: /usr/sbin/iw
63
+ required: true
64
+ - name: timeout
65
+ description: Timeout for executing the binary, specified in seconds.
66
+ default_value: 2
67
+ required: false
68
examples:
69
folding:
70
+ title: ""
71
enabled: false
76
- title: "Config"
72
list:
78
- - name: Change the collection frequency
79
- description: Specify a custom collection frequence (update_every) for this collector
73
+ - name: Custom binary path
74
+ description: The executable is not in the directories specified in the PATH environment variable.
75
config: |
81
- # the data collection frequency
82
- # if unset, will inherit the netdata update frequency
83
- ap_update_every=10
84
-
85
- # the charts priority on the dashboard
86
- #ap_priority=6900
87
-
88
- # the number of retries to do in case of failure
89
- # before disabling the module
90
- #ap_retries=10
76
+ jobs:
77
+ - name: custom_iw
78
+ binary_path: /usr/local/sbin/iw
79
troubleshooting:
80
problems:
81
list: []
@@ -101,46 +89,49 @@ modules:
89
scopes:
90
- name: wireless device
91
description: "These metrics refer to the entire monitored application."
104
- labels: []
92
+ labels:
93
+ - name: device
94
+ description: Wireless interface name
95
+ - name: ssid
96
+ description: SSID
97
metrics:
98
- name: ap.clients
107
- description: Connected clients to ${ssid} on ${dev}
99
+ description: Connected clients
100
unit: "clients"
101
chart_type: line
102
dimensions:
103
- name: clients
104
- name: ap.net
113
- description: Bandwidth for ${ssid} on ${dev}
105
+ description: Bandwidth
106
unit: "kilobits/s"
107
chart_type: area
108
dimensions:
109
- name: received
110
- name: sent
111
- name: ap.packets
120
- description: Packets for ${ssid} on ${dev}
112
+ description: Packets
113
unit: "packets/s"
114
chart_type: line
115
dimensions:
116
- name: received
117
- name: sent
118
- name: ap.issues
127
- description: Transmit Issues for ${ssid} on ${dev}
119
+ description: Transmit Issues
120
unit: "issues/s"
121
chart_type: line
122
dimensions:
123
- name: retries
124
- name: failures
125
- name: ap.signal
134
- description: Average Signal for ${ssid} on ${dev}
126
+ description: Average Signal
127
unit: "dBm"
128
chart_type: line
129
dimensions:
130
- name: average signal
131
- name: ap.bitrate
140
- description: Bitrate for ${ssid} on ${dev}
132
+ description: Bitrate
133
unit: "Mbps"
134
chart_type: line
135
dimensions:
136
- name: receive
137
- name: transmit
146
- - name: expected
src/go/plugin/go.d/modules/ap/testdata/config.json
new
+5
@@ -0,0 +1,5 @@
1
+{
2
+ "update_every": 123,
3
+ "timeout": 123.123,
4
+ "binary_path": "ok"
5
+}
src/go/plugin/go.d/modules/ap/testdata/config.yaml
new
+3
@@ -0,0 +1,3 @@
1
+update_every: 123
2
+timeout: 123.123
3
+binary_path: "ok"
src/go/plugin/go.d/modules/ap/testdata/iw_dev_ap.txt
new
+25
@@ -0,0 +1,25 @@
1
+phy#0
2
+ Interface wlp1s0
3
+ ifindex 2
4
+ wdev 0x1
5
+ addr 28:cd:c4:b8:63:cb
6
+ ssid testing
7
+ type AP
8
+ channel 1 (2412 MHz), width: 20 MHz, center1: 2412 MHz
9
+ txpower 20.00 dBm
10
+ multicast TXQ:
11
+ qsz-byt qsz-pkt flows drops marks overlmt hashcol tx-bytes tx-packets
12
+ 0 0 2 0 0 0 0 16447 226
13
+
14
+phy#1
15
+ Interface wlp1s1
16
+ ifindex 3
17
+ wdev 0x1
18
+ addr 28:cd:c4:b8:63:cc
19
+ ssid testing
20
+ type AP
21
+ channel 1 (2412 MHz), width: 20 MHz, center1: 2412 MHz
22
+ txpower 20.00 dBm
23
+ multicast TXQ:
24
+ qsz-byt qsz-pkt flows drops marks overlmt hashcol tx-bytes tx-packets
25
+ 0 0 2 0 0 0 0 16447 226
src/go/plugin/go.d/modules/ap/testdata/iw_dev_managed.txt
new
+11
@@ -0,0 +1,11 @@
1
+phy#0
2
+ Interface wlp1s0
3
+ ifindex 2
4
+ wdev 0x1
5
+ addr 28:cd:c4:b8:63:cb
6
+ type managed
7
+ channel 4 (2427 MHz), width: 20 MHz, center1: 2427 MHz
8
+ txpower 20.00 dBm
9
+ multicast TXQ:
10
+ qsz-byt qsz-pkt flows drops marks overlmt hashcol tx-bytes tx-packets
11
+ 0 0 0 0 0 0 0 0 0
src/go/plugin/go.d/modules/ap/testdata/station_dump.txt
new
+58
@@ -0,0 +1,58 @@
1
+Station 7e:0d:a5:a6:91:2b (on wlp1s0)
2
+ inactive time: 58264 ms
3
+ rx bytes: 89675
4
+ rx packets: 2446
5
+ tx bytes: 6918
6
+ tx packets: 30
7
+ tx retries: 1
8
+ tx failed: 1
9
+ rx drop misc: 0
10
+ signal: -44 [-51, -44] dBm
11
+ signal avg: -38 [-39, -39] dBm
12
+ tx bitrate: 65.0 MBit/s MCS 7
13
+ tx duration: 0 us
14
+ rx bitrate: 130.0 MBit/s MCS 15
15
+ rx duration: 0 us
16
+ authorized: yes
17
+ authenticated: yes
18
+ associated: yes
19
+ preamble: short
20
+ WMM/WME: yes
21
+ MFP: no
22
+ TDLS peer: no
23
+ DTIM period: 2
24
+ beacon interval:100
25
+ short slot time:yes
26
+ connected time: 796 seconds
27
+ associated at [boottime]: 12650.576s
28
+ associated at: 1720705279930 ms
29
+ current time: 1720706075344 ms
30
+Station fa:50:db:c1:1c:18 (on wlp1s0)
31
+ inactive time: 93 ms
32
+ rx bytes: 5442
33
+ rx packets: 85
34
+ tx bytes: 1352
35
+ tx packets: 8
36
+ tx retries: 0
37
+ tx failed: 0
38
+ rx drop misc: 0
39
+ signal: -31 [-31, -39] dBm
40
+ signal avg: -30 [-30, -38] dBm
41
+ tx bitrate: 65.0 MBit/s MCS 7
42
+ tx duration: 0 us
43
+ rx bitrate: 1.0 MBit/s
44
+ rx duration: 0 us
45
+ authorized: yes
46
+ authenticated: yes
47
+ associated: yes
48
+ preamble: short
49
+ WMM/WME: yes
50
+ MFP: no
51
+ TDLS peer: no
52
+ DTIM period: 2
53
+ beacon interval:100
54
+ short slot time:yes
55
+ connected time: 6 seconds
56
+ associated at [boottime]: 13440.167s
57
+ associated at: 1720706069520 ms
58
+ current time: 1720706075344 ms
src/go/plugin/go.d/modules/init.go
+1
@@ -5,6 +5,7 @@ package modules
5
import (
6
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/activemq"
7
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/adaptecraid"
8
+ _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/ap"
9
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/apache"
10
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/bind"
11
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/cassandra"