@cryptotaxi247 / netdata-1 / commits / 2b2e857af

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"