feat(go.d.plugin): add MaxScale collector (#18859)
Ilya Mashchenko committed
Oct 24, 2024 at 16:47 UTC
0b45d3be87e43e87ea29100b34220a26691e09c2
19 files changed
+1740
-2
src/go/plugin/go.d/README.md
+1
@@ -89,6 +89,7 @@ see the appropriate collector readme.
89
| [logind](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/logind) | systemd-logind |
90
| [logstash](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/logstash) | Logstash |
91
| [lvm](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/lvm) | LVM logical volumes |
92
+| [maxscale](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/maxscale) | MaxScale |
93
| [megacli](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/megacli) | MegaCli Hardware Raid |
94
| [memcached](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/memcached) | Memcached |
95
| [mongoDB](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/mongodb) | MongoDB |
src/go/plugin/go.d/config/go.d.conf
+1
@@ -64,6 +64,7 @@ modules:
64
# logind: yes
65
# logstash: yes
66
# lvm: yes
67
+# maxscale: yes
68
# megacli: yes
69
# memcached: yes
70
# mongodb: yes
src/go/plugin/go.d/config/go.d/maxscale.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/maxscale#readme
3
+
4
+#jobs:
5
+# - name: local
6
+# url: http://127.0.0.1:8989
src/go/plugin/go.d/config/go.d/sd/docker.conf
+7
@@ -50,6 +50,8 @@ classify:
50
expr: '{{ and (eq .PrivatePort "5001") (match "sp" .Image "ipfs/kubo ipfs/kubo:*") }}'
51
- tags: "lighttpd"
52
expr: '{{ match "sp" .Image "*/lighttpd */lighttpd:*" }}'
53
+ - tags: "maxscale"
54
+ expr: '{{ or (eq .PrivatePort "8989") (match "sp" .Image "mariadb/maxscale mariadb/maxscale:*") }}'
55
- tags: "memcached"
56
expr: '{{ or (eq .PrivatePort "11211") (match "sp" .Image "memcached memcached:* */memcached */memcached:*") }}'
57
- tags: "mongodb"
@@ -175,6 +177,11 @@ compose:
177
module: lighttpd
178
name: docker_{{.Name}}
179
url: http://{{.Address}}/server-status?auto
180
+ - selector: "maxscale"
181
+ template: |
182
+ module: maxscale
183
+ name: docker_{{.Name}}
184
+ url: http://{{.Address}}
185
- selector: "memcached"
186
template: |
187
module: memcached
src/go/plugin/go.d/config/go.d/sd/net_listeners.conf
+7
@@ -82,6 +82,8 @@ classify:
82
expr: '{{ and (eq .Port "80" "8080") (eq .Comm "lighttpd") }}'
83
- tags: "logstash"
84
expr: '{{ and (eq .Port "9600") (glob .Cmdline "*logstash*") }}'
85
+ - tags: "maxscale"
86
+ expr: '{{ or (eq .Port "8989") (eq .Comm "maxscale") }}'
87
- tags: "memcached"
88
expr: '{{ or (eq .Port "11211") (eq .Comm "memcached") }}'
89
- tags: "mongodb"
@@ -359,6 +361,11 @@ compose:
361
module: logstash
362
name: local
363
url: http://{{.Address}}
364
+ - selector: "maxscale"
365
+ template: |
366
+ module: maxscale
367
+ name: local
368
+ url: http://{{.Address}}
369
- selector: "memcached"
370
template: |
371
module: memcached
src/go/plugin/go.d/modules/ceph/metadata.yaml
+2
-2
@@ -24,7 +24,7 @@ modules:
24
This collector monitors the overall health status and performance of your Ceph clusters.
25
It gathers key metrics for the entire cluster, individual Pools, and OSDs.
26
method_description: |
27
- It collects metrics by periodically issuing HTTP GET requests to the Ceph Manager [RESP API](https://docs.ceph.com/en/reef/mgr/ceph_api/#):
27
+ It collects metrics by periodically issuing HTTP GET requests to the Ceph Manager [REST API](https://docs.ceph.com/en/reef/mgr/ceph_api/#):
28
29
- [/api/monitor](https://docs.ceph.com/en/reef/mgr/ceph_api/#get--api-monitor) (only once to get the Ceph cluster id (fsid))
30
- [/api/health/minimal](https://docs.ceph.com/en/reef/mgr/ceph_api/#get--api-health-minimal)
@@ -44,7 +44,7 @@ modules:
44
- localhost that are listening on port 8443
45
- within Docker containers
46
47
- > **Note that the Ceph RESP API requires a username and password**.
47
+ > **Note that the Ceph REST API requires a username and password**.
48
> While Netdata can automatically detect Ceph Manager instances and create data collection jobs, these jobs will fail unless you provide the necessary credentials.
49
limits:
50
description: ""
src/go/plugin/go.d/modules/init.go
+1
@@ -55,6 +55,7 @@ import (
55
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/logind"
56
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/logstash"
57
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/lvm"
58
+ _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/maxscale"
59
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/megacli"
60
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/memcached"
61
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/mongodb"
src/go/plugin/go.d/modules/maxscale/charts.go
new
+224
@@ -0,0 +1,224 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package maxscale
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
+ prioPollEvents = module.Priority + iota
14
+
15
+ prioSessions
16
+ prioZombies
17
+
18
+ prioServerState
19
+ prioServerConnections
20
+
21
+ prioThreadsByState
22
+
23
+ prioCurrentFDs
24
+
25
+ prioQCCacheEfficiency
26
+ prioQCCacheOperations
27
+
28
+ prioUptime
29
+)
30
+
31
+var charts = module.Charts{
32
+ pollEventsChart.Copy(),
33
+ currentSessionsChart.Copy(),
34
+ currentZombieConnectionsChart.Copy(),
35
+ threadsByStateChart.Copy(),
36
+ currentFDsChart.Copy(),
37
+ qcCacheEfficiencyChart.Copy(),
38
+ qcCacheOperationsChart.Copy(),
39
+ uptimeChart.Copy(),
40
+}
41
+
42
+var (
43
+ pollEventsChart = module.Chart{
44
+ ID: "poll_events",
45
+ Title: "Poll Events",
46
+ Units: "events/s",
47
+ Fam: "poll events",
48
+ Ctx: "maxscale.poll_events",
49
+ Priority: prioPollEvents,
50
+ Dims: module.Dims{
51
+ {ID: "threads_reads", Name: "reads", Algo: module.Incremental},
52
+ {ID: "threads_writes", Name: "writes", Algo: module.Incremental},
53
+ {ID: "threads_accepts", Name: "accepts", Algo: module.Incremental},
54
+ {ID: "threads_errors", Name: "errors", Algo: module.Incremental},
55
+ {ID: "threads_hangups", Name: "hangups", Algo: module.Incremental},
56
+ },
57
+ }
58
+
59
+ currentSessionsChart = module.Chart{
60
+ ID: "current_sessions",
61
+ Title: "Curren Sessions",
62
+ Units: "sessions",
63
+ Fam: "sessions",
64
+ Ctx: "maxscale.current_sessions",
65
+ Priority: prioSessions,
66
+ Dims: module.Dims{
67
+ {ID: "threads_sessions", Name: "sessions"},
68
+ },
69
+ }
70
+ currentZombieConnectionsChart = module.Chart{
71
+ ID: "current_zombie_connections",
72
+ Title: "Current Zombie Connections",
73
+ Units: "connections",
74
+ Fam: "sessions",
75
+ Ctx: "maxscale.current_zombie_connections",
76
+ Priority: prioZombies,
77
+ Dims: module.Dims{
78
+ {ID: "threads_zombies", Name: "zombie"},
79
+ },
80
+ }
81
+
82
+ threadsByStateChart = func() module.Chart {
83
+ chart := module.Chart{
84
+ ID: "threads_by_state",
85
+ Title: "Threads Count by State",
86
+ Units: "threads",
87
+ Fam: "threads",
88
+ Ctx: "maxscale.threads_by_state",
89
+ Priority: prioThreadsByState,
90
+ Type: module.Stacked,
91
+ }
92
+ for _, v := range threadStates {
93
+ chart.Dims = append(chart.Dims, &module.Dim{
94
+ ID: "threads_state_" + v,
95
+ Name: strings.ToLower(v),
96
+ })
97
+ }
98
+ return chart
99
+ }()
100
+
101
+ currentFDsChart = module.Chart{
102
+ ID: "current_file_descriptors",
103
+ Title: "Current Managed File Descriptors",
104
+ Units: "fds",
105
+ Fam: "fds",
106
+ Ctx: "maxscale.current_fds",
107
+ Priority: prioCurrentFDs,
108
+ Dims: module.Dims{
109
+ {ID: "threads_current_fds", Name: "managed"},
110
+ },
111
+ }
112
+
113
+ qcCacheEfficiencyChart = module.Chart{
114
+ ID: "qc_cache_efficiency",
115
+ Title: "QC Cache Efficiency",
116
+ Units: "requests/s",
117
+ Fam: "qc cache",
118
+ Ctx: "maxscale.qc_cache_efficiency",
119
+ Priority: prioQCCacheEfficiency,
120
+ Type: module.Stacked,
121
+ Dims: module.Dims{
122
+ {ID: "threads_qc_cache_hits", Name: "hits", Algo: module.Incremental},
123
+ {ID: "threads_qc_cache_misses", Name: "misses", Algo: module.Incremental},
124
+ },
125
+ }
126
+ qcCacheOperationsChart = module.Chart{
127
+ ID: "qc_cache_operations",
128
+ Title: "QC Cache Operations",
129
+ Units: "operations/s",
130
+ Fam: "qc cache",
131
+ Ctx: "maxscale.qc_cache_operations",
132
+ Priority: prioQCCacheOperations,
133
+ Type: module.Stacked,
134
+ Dims: module.Dims{
135
+ {ID: "threads_qc_cache_inserts", Name: "inserts", Algo: module.Incremental},
136
+ {ID: "threads_qc_cache_evictions", Name: "evictions", Algo: module.Incremental},
137
+ },
138
+ }
139
+
140
+ uptimeChart = module.Chart{
141
+ ID: "uptime",
142
+ Title: "Uptime",
143
+ Units: "seconds",
144
+ Fam: "uptime",
145
+ Ctx: "maxscale.uptime",
146
+ Priority: prioUptime,
147
+ Dims: module.Dims{
148
+ {ID: "uptime"},
149
+ },
150
+ }
151
+)
152
+
153
+var serverChartsTmpl = module.Charts{
154
+ serverStateChartTmpl.Copy(),
155
+ serverCurrentConnectionsChartTmpl.Copy(),
156
+}
157
+
158
+var (
159
+ serverStateChartTmpl = func() module.Chart {
160
+ chart := module.Chart{
161
+ ID: "server_%s_state",
162
+ Title: "Server State",
163
+ Units: "state",
164
+ Fam: "servers",
165
+ Ctx: "maxscale.server_state",
166
+ Priority: prioServerState,
167
+ }
168
+ for _, v := range serverStates {
169
+ chart.Dims = append(chart.Dims, &module.Dim{
170
+ ID: "server_%s_state_" + v,
171
+ Name: strings.ToLower(cleanChartID(v)),
172
+ })
173
+ }
174
+ return chart
175
+ }()
176
+ serverCurrentConnectionsChartTmpl = module.Chart{
177
+ ID: "server_%s_current_connections",
178
+ Title: "Server Current Connections",
179
+ Units: "connections",
180
+ Fam: "servers",
181
+ Ctx: "maxscale.server_current_connections",
182
+ Priority: prioServerConnections,
183
+ Dims: module.Dims{
184
+ {ID: "server_%s_connections", Name: "connections"},
185
+ },
186
+ }
187
+)
188
+
189
+func (m *MaxScale) addServerCharts(id, addr string) {
190
+ srvCharts := serverChartsTmpl.Copy()
191
+
192
+ for _, chart := range *srvCharts {
193
+ chart.ID = fmt.Sprintf(chart.ID, id)
194
+ chart.ID = cleanChartID(chart.ID)
195
+ chart.Labels = []module.Label{
196
+ {Key: "server", Value: id},
197
+ {Key: "address", Value: addr},
198
+ }
199
+ for _, dim := range chart.Dims {
200
+ dim.ID = fmt.Sprintf(dim.ID, id)
201
+ }
202
+ }
203
+
204
+ if err := m.Charts().Add(*srvCharts...); err != nil {
205
+ m.Warning(err)
206
+ }
207
+}
208
+
209
+func (m *MaxScale) removeServerCharts(id string) {
210
+ px := fmt.Sprintf("server_%s_", id)
211
+ px = cleanChartID(px)
212
+
213
+ for _, chart := range *m.Charts() {
214
+ if strings.HasPrefix(chart.ID, px) {
215
+ chart.MarkRemove()
216
+ chart.MarkNotCreated()
217
+ }
218
+ }
219
+}
220
+
221
+func cleanChartID(id string) string {
222
+ r := strings.NewReplacer(".", "_", " ", "_")
223
+ return r.Replace(id)
224
+}
src/go/plugin/go.d/modules/maxscale/collect.go
new
+141
@@ -0,0 +1,141 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package maxscale
4
+
5
+import (
6
+ "fmt"
7
+ "net"
8
+ "strconv"
9
+ "strings"
10
+ "unicode"
11
+
12
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/web"
13
+)
14
+
15
+const (
16
+ urlPathMaxscale = "/maxscale"
17
+ urlPathMaxscaleThreads = "/maxscale/threads"
18
+ urlPathServers = "/servers"
19
+)
20
+
21
+func (m *MaxScale) collect() (map[string]int64, error) {
22
+ mx := make(map[string]int64)
23
+
24
+ if err := m.collectMaxScaleGlobal(mx); err != nil {
25
+ return nil, err
26
+ }
27
+ if err := m.collectMaxScaleThreads(mx); err != nil {
28
+ return nil, err
29
+ }
30
+ if err := m.collectServers(mx); err != nil {
31
+ return nil, err
32
+ }
33
+
34
+ return mx, nil
35
+}
36
+
37
+func (m *MaxScale) collectMaxScaleGlobal(mx map[string]int64) error {
38
+ req, err := web.NewHTTPRequestWithPath(m.RequestConfig, urlPathMaxscale)
39
+ if err != nil {
40
+ return fmt.Errorf("failed to create HTTP request: %v", err)
41
+ }
42
+
43
+ var resp maxscaleGlobalResponse
44
+
45
+ if err := web.DoHTTP(m.httpClient).RequestJSON(req, &resp); err != nil {
46
+ return err
47
+ }
48
+
49
+ if resp.Data == nil {
50
+ return fmt.Errorf("invalid response from '%s': missing expected MaxScale data", req.URL)
51
+ }
52
+
53
+ mx["uptime"] = resp.Data.Attrs.Uptime
54
+
55
+ return nil
56
+}
57
+
58
+func (m *MaxScale) collectMaxScaleThreads(mx map[string]int64) error {
59
+ req, err := web.NewHTTPRequestWithPath(m.RequestConfig, urlPathMaxscaleThreads)
60
+ if err != nil {
61
+ return fmt.Errorf("failed to create HTTP request: %v", err)
62
+ }
63
+
64
+ var resp maxscaleThreadsResponse
65
+
66
+ if err := web.DoHTTP(m.httpClient).RequestJSON(req, &resp); err != nil {
67
+ return err
68
+ }
69
+
70
+ for _, r := range resp.Data {
71
+ st := r.Attrs.Stats
72
+ mx["threads_reads"] += st.Reads
73
+ mx["threads_writes"] += st.Writes
74
+ mx["threads_errors"] += st.Errors
75
+ mx["threads_hangups"] += st.Hangups
76
+ mx["threads_accepts"] += st.Accepts
77
+ mx["threads_sessions"] += st.Sessions
78
+ mx["threads_zombies"] += st.Zombies
79
+ mx["threads_current_fds"] += st.CurrentDescriptors
80
+ mx["threads_total_fds"] += st.TotalDescriptors
81
+ mx["threads_qc_cache_inserts"] += st.QCCache.Inserts
82
+ mx["threads_qc_cache_evictions"] += st.QCCache.Evictions
83
+ mx["threads_qc_cache_hits"] += st.QCCache.Hits
84
+ mx["threads_qc_cache_misses"] += st.QCCache.Misses
85
+ for _, v := range threadStates {
86
+ mx["threads_state_"+v] = 0
87
+ }
88
+ mx["threads_state_"+st.State]++
89
+ }
90
+
91
+ return nil
92
+}
93
+
94
+func (m *MaxScale) collectServers(mx map[string]int64) error {
95
+ req, err := web.NewHTTPRequestWithPath(m.RequestConfig, urlPathServers)
96
+ if err != nil {
97
+ return fmt.Errorf("failed to create HTTP request: %v", err)
98
+ }
99
+
100
+ var resp serversResponse
101
+
102
+ if err := web.DoHTTP(m.httpClient).RequestJSON(req, &resp); err != nil {
103
+ return err
104
+ }
105
+
106
+ seen := make(map[string]bool)
107
+
108
+ for _, r := range resp.Data {
109
+ if r.ID == "" {
110
+ continue
111
+ }
112
+
113
+ seen[r.ID] = true
114
+
115
+ if !m.seenServers[r.ID] {
116
+ m.seenServers[r.ID] = true
117
+ addr := net.JoinHostPort(r.Attrs.Params.Address, strconv.Itoa(int(r.Attrs.Params.Port)))
118
+ m.addServerCharts(r.ID, addr)
119
+ }
120
+
121
+ px := fmt.Sprintf("server_%s_", r.ID)
122
+
123
+ mx[px+"connections"] = r.Attrs.Statistics.Connections
124
+
125
+ for _, v := range serverStates {
126
+ mx[px+"state_"+v] = 0
127
+ }
128
+ for _, v := range strings.FieldsFunc(r.Attrs.State, unicode.IsSpace) {
129
+ mx[px+"state_"+v] = 1
130
+ }
131
+ }
132
+
133
+ for id := range m.seenServers {
134
+ if !seen[id] {
135
+ delete(m.seenServers, id)
136
+ m.removeServerCharts(id)
137
+ }
138
+ }
139
+
140
+ return nil
141
+}
src/go/plugin/go.d/modules/maxscale/config_schema.json
new
+185
@@ -0,0 +1,185 @@
1
+{
2
+ "jsonSchema": {
3
+ "$schema": "http://json-schema.org/draft-07/schema#",
4
+ "title": "MaxScale 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": 1
13
+ },
14
+ "url": {
15
+ "title": "URL",
16
+ "description": "The URL of the MaxScale HTTP API endpoint.",
17
+ "type": "string",
18
+ "default": "http://127.0.0.1:8989",
19
+ "format": "uri"
20
+ },
21
+ "timeout": {
22
+ "title": "Timeout",
23
+ "description": "The timeout in seconds for the HTTP request.",
24
+ "type": "number",
25
+ "minimum": 0.5,
26
+ "default": 1
27
+ },
28
+ "not_follow_redirects": {
29
+ "title": "Not follow redirects",
30
+ "description": "If set, the client will not follow HTTP redirects automatically.",
31
+ "type": "boolean"
32
+ },
33
+ "username": {
34
+ "title": "Username",
35
+ "description": "The username for basic authentication.",
36
+ "type": "string",
37
+ "default": "admin",
38
+ "sensitive": true
39
+ },
40
+ "password": {
41
+ "title": "Password",
42
+ "description": "The password for basic authentication.",
43
+ "type": "string",
44
+ "default": "mariadb",
45
+ "sensitive": true
46
+ },
47
+ "proxy_url": {
48
+ "title": "Proxy URL",
49
+ "description": "The URL of the proxy server.",
50
+ "type": "string"
51
+ },
52
+ "proxy_username": {
53
+ "title": "Proxy username",
54
+ "description": "The username for proxy authentication.",
55
+ "type": "string",
56
+ "sensitive": true
57
+ },
58
+ "proxy_password": {
59
+ "title": "Proxy password",
60
+ "description": "The password for proxy authentication.",
61
+ "type": "string",
62
+ "sensitive": true
63
+ },
64
+ "headers": {
65
+ "title": "Headers",
66
+ "description": "Additional HTTP headers to include in the request.",
67
+ "type": [
68
+ "object",
69
+ "null"
70
+ ],
71
+ "additionalProperties": {
72
+ "type": "string"
73
+ }
74
+ },
75
+ "tls_skip_verify": {
76
+ "title": "Skip TLS verification",
77
+ "description": "If set, TLS certificate verification will be skipped.",
78
+ "type": "boolean"
79
+ },
80
+ "tls_ca": {
81
+ "title": "TLS CA",
82
+ "description": "The path to the CA certificate file for TLS verification.",
83
+ "type": "string",
84
+ "pattern": "^$|^/"
85
+ },
86
+ "tls_cert": {
87
+ "title": "TLS certificate",
88
+ "description": "The path to the client certificate file for TLS authentication.",
89
+ "type": "string",
90
+ "pattern": "^$|^/"
91
+ },
92
+ "tls_key": {
93
+ "title": "TLS key",
94
+ "description": "The path to the client key file for TLS authentication.",
95
+ "type": "string",
96
+ "pattern": "^$|^/"
97
+ },
98
+ "body": {
99
+ "title": "Body",
100
+ "type": "string"
101
+ },
102
+ "method": {
103
+ "title": "Method",
104
+ "type": "string"
105
+ }
106
+ },
107
+ "required": [
108
+ "url"
109
+ ],
110
+ "additionalProperties": false,
111
+ "patternProperties": {
112
+ "^name$": {}
113
+ }
114
+ },
115
+ "uiSchema": {
116
+ "uiOptions": {
117
+ "fullPage": true
118
+ },
119
+ "body": {
120
+ "ui:widget": "hidden"
121
+ },
122
+ "method": {
123
+ "ui:widget": "hidden"
124
+ },
125
+ "timeout": {
126
+ "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)."
127
+ },
128
+ "username": {
129
+ "ui:widget": "password"
130
+ },
131
+ "proxy_username": {
132
+ "ui:widget": "password"
133
+ },
134
+ "password": {
135
+ "ui:widget": "password"
136
+ },
137
+ "proxy_password": {
138
+ "ui:widget": "password"
139
+ },
140
+ "ui:flavour": "tabs",
141
+ "ui:options": {
142
+ "tabs": [
143
+ {
144
+ "title": "Base",
145
+ "fields": [
146
+ "update_every",
147
+ "url",
148
+ "timeout",
149
+ "not_follow_redirects"
150
+ ]
151
+ },
152
+ {
153
+ "title": "Auth",
154
+ "fields": [
155
+ "username",
156
+ "password"
157
+ ]
158
+ },
159
+ {
160
+ "title": "TLS",
161
+ "fields": [
162
+ "tls_skip_verify",
163
+ "tls_ca",
164
+ "tls_cert",
165
+ "tls_key"
166
+ ]
167
+ },
168
+ {
169
+ "title": "Proxy",
170
+ "fields": [
171
+ "proxy_url",
172
+ "proxy_username",
173
+ "proxy_password"
174
+ ]
175
+ },
176
+ {
177
+ "title": "Headers",
178
+ "fields": [
179
+ "headers"
180
+ ]
181
+ }
182
+ ]
183
+ }
184
+ }
185
+}
src/go/plugin/go.d/modules/maxscale/maxscale.go
new
+115
@@ -0,0 +1,115 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package maxscale
4
+
5
+import (
6
+ _ "embed"
7
+ "errors"
8
+ "fmt"
9
+ "net/http"
10
+ "time"
11
+
12
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
13
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/confopt"
14
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/web"
15
+)
16
+
17
+//go:embed "config_schema.json"
18
+var configSchema string
19
+
20
+func init() {
21
+ module.Register("maxscale", module.Creator{
22
+ JobConfigSchema: configSchema,
23
+ Create: func() module.Module { return New() },
24
+ Config: func() any { return &Config{} },
25
+ })
26
+}
27
+
28
+func New() *MaxScale {
29
+ return &MaxScale{
30
+ Config: Config{
31
+ HTTPConfig: web.HTTPConfig{
32
+ RequestConfig: web.RequestConfig{
33
+ URL: "http://127.0.0.1:8989",
34
+ Username: "admin",
35
+ Password: "mariadb",
36
+ },
37
+ ClientConfig: web.ClientConfig{
38
+ Timeout: confopt.Duration(time.Second * 1),
39
+ },
40
+ },
41
+ },
42
+ charts: charts.Copy(),
43
+ seenServers: make(map[string]bool),
44
+ }
45
+}
46
+
47
+type Config struct {
48
+ UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
49
+ web.HTTPConfig `yaml:",inline" json:""`
50
+}
51
+
52
+type MaxScale struct {
53
+ module.Base
54
+ Config `yaml:",inline" json:""`
55
+
56
+ charts *module.Charts
57
+
58
+ httpClient *http.Client
59
+
60
+ seenServers map[string]bool
61
+}
62
+
63
+func (m *MaxScale) Configuration() any {
64
+ return m.Config
65
+}
66
+
67
+func (m *MaxScale) Init() error {
68
+ if m.URL == "" {
69
+ return errors.New("URL required but not set")
70
+ }
71
+
72
+ httpClient, err := web.NewHTTPClient(m.ClientConfig)
73
+ if err != nil {
74
+ return fmt.Errorf("failed initializing http client: %w", err)
75
+ }
76
+ m.httpClient = httpClient
77
+
78
+ m.Debugf("using URL %s", m.URL)
79
+ m.Debugf("using timeout: %s", m.Timeout)
80
+
81
+ return nil
82
+}
83
+
84
+func (m *MaxScale) Check() error {
85
+ mx, err := m.collect()
86
+ if err != nil {
87
+ return err
88
+ }
89
+
90
+ if len(mx) == 0 {
91
+ return errors.New("no metrics collected")
92
+ }
93
+
94
+ return nil
95
+}
96
+
97
+func (m *MaxScale) Charts() *module.Charts {
98
+ return m.charts
99
+}
100
+
101
+func (m *MaxScale) Collect() map[string]int64 {
102
+ mx, err := m.collect()
103
+ if err != nil {
104
+ m.Error(err)
105
+ return nil
106
+ }
107
+
108
+ return mx
109
+}
110
+
111
+func (m *MaxScale) Cleanup() {
112
+ if m.httpClient != nil {
113
+ m.httpClient.CloseIdleConnections()
114
+ }
115
+}
src/go/plugin/go.d/modules/maxscale/maxscale_test.go
new
+284
@@ -0,0 +1,284 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package maxscale
4
+
5
+import (
6
+ "net/http"
7
+ "net/http/httptest"
8
+ "os"
9
+ "testing"
10
+
11
+ "github.com/stretchr/testify/assert"
12
+ "github.com/stretchr/testify/require"
13
+
14
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
15
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/web"
16
+)
17
+
18
+var (
19
+ dataConfigJSON, _ = os.ReadFile("testdata/config.json")
20
+ dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")
21
+
22
+ dataVer24MaxScale, _ = os.ReadFile("testdata/v24.02.3/maxscale.json")
23
+ dataVer24MaxScaleThreads, _ = os.ReadFile("testdata/v24.02.3/maxscale_threads.json")
24
+ dataVer24Servers, _ = os.ReadFile("testdata/v24.02.3/servers.json")
25
+)
26
+
27
+func Test_testDataIsValid(t *testing.T) {
28
+ for name, data := range map[string][]byte{
29
+ "dataConfigJSON": dataConfigJSON,
30
+ "dataConfigYAML": dataConfigYAML,
31
+ "dataVer24MaxScale": dataVer24MaxScale,
32
+ "dataVer24MaxScaleThreads": dataVer24MaxScaleThreads,
33
+ "dataVer24Servers": dataVer24Servers,
34
+ } {
35
+ require.NotNil(t, data, name)
36
+ }
37
+}
38
+
39
+func TestMaxScale_ConfigurationSerialize(t *testing.T) {
40
+ module.TestConfigurationSerialize(t, &MaxScale{}, dataConfigJSON, dataConfigYAML)
41
+}
42
+
43
+func TestMaxScale_Init(t *testing.T) {
44
+ tests := map[string]struct {
45
+ wantFail bool
46
+ config Config
47
+ }{
48
+ "success with default": {
49
+ wantFail: false,
50
+ config: New().Config,
51
+ },
52
+ "fail when URL not set": {
53
+ wantFail: true,
54
+ config: Config{
55
+ HTTPConfig: web.HTTPConfig{
56
+ RequestConfig: web.RequestConfig{URL: ""},
57
+ },
58
+ },
59
+ },
60
+ }
61
+
62
+ for name, test := range tests {
63
+ t.Run(name, func(t *testing.T) {
64
+ ms := New()
65
+ ms.Config = test.config
66
+
67
+ if test.wantFail {
68
+ assert.Error(t, ms.Init())
69
+ } else {
70
+ assert.NoError(t, ms.Init())
71
+ }
72
+ })
73
+ }
74
+}
75
+
76
+func TestMaxScale_Check(t *testing.T) {
77
+ tests := map[string]struct {
78
+ wantFail bool
79
+ prepare func(t *testing.T) (nu *MaxScale, cleanup func())
80
+ }{
81
+ "success on valid response": {
82
+ wantFail: false,
83
+ prepare: caseOk,
84
+ },
85
+ "fail on unexpected JSON response": {
86
+ wantFail: true,
87
+ prepare: caseUnexpectedJsonResponse,
88
+ },
89
+ "fail on invalid data response": {
90
+ wantFail: true,
91
+ prepare: caseInvalidDataResponse,
92
+ },
93
+ "fail on connection refused": {
94
+ wantFail: true,
95
+ prepare: caseConnectionRefused,
96
+ },
97
+ "fail on 404 response": {
98
+ wantFail: true,
99
+ prepare: case404,
100
+ },
101
+ }
102
+
103
+ for name, test := range tests {
104
+ t.Run(name, func(t *testing.T) {
105
+ ms, cleanup := test.prepare(t)
106
+ defer cleanup()
107
+
108
+ if test.wantFail {
109
+ assert.Error(t, ms.Check())
110
+ } else {
111
+ assert.NoError(t, ms.Check())
112
+ }
113
+ })
114
+ }
115
+}
116
+
117
+func TestMaxScale_Charts(t *testing.T) {
118
+ assert.NotNil(t, New().Charts())
119
+}
120
+
121
+func TestMaxScale_Collect(t *testing.T) {
122
+ tests := map[string]struct {
123
+ prepare func(t *testing.T) (nu *MaxScale, cleanup func())
124
+ wantNumOfCharts int
125
+ wantMetrics map[string]int64
126
+ }{
127
+ "success on valid response": {
128
+ prepare: caseOk,
129
+ wantNumOfCharts: len(charts) + len(serverChartsTmpl)*1,
130
+ wantMetrics: map[string]int64{
131
+ "server_kek_connections": 0,
132
+ "server_kek_state_Binlog Relay": 0,
133
+ "server_kek_state_Down": 1,
134
+ "server_kek_state_Drained": 0,
135
+ "server_kek_state_Draining": 0,
136
+ "server_kek_state_Maintenance": 0,
137
+ "server_kek_state_Master": 0,
138
+ "server_kek_state_Relay Master": 0,
139
+ "server_kek_state_Running": 0,
140
+ "server_kek_state_Slave": 0,
141
+ "server_kek_state_Synced": 0,
142
+ "threads_accepts": 0,
143
+ "threads_current_fds": 3,
144
+ "threads_errors": 0,
145
+ "threads_hangups": 0,
146
+ "threads_qc_cache_evictions": 0,
147
+ "threads_qc_cache_hits": 0,
148
+ "threads_qc_cache_inserts": 0,
149
+ "threads_qc_cache_misses": 0,
150
+ "threads_reads": 68359,
151
+ "threads_sessions": 0,
152
+ "threads_state_Active": 1,
153
+ "threads_state_Dormant": 0,
154
+ "threads_state_Draining": 0,
155
+ "threads_total_fds": 3,
156
+ "threads_writes": 0,
157
+ "threads_zombies": 0,
158
+ "uptime": 61298,
159
+ },
160
+ },
161
+ "fail on unexpected JSON response": {
162
+ prepare: caseUnexpectedJsonResponse,
163
+ wantMetrics: nil,
164
+ },
165
+ "fail on invalid data response": {
166
+ prepare: caseInvalidDataResponse,
167
+ wantMetrics: nil,
168
+ },
169
+ "fail on connection refused": {
170
+ prepare: caseConnectionRefused,
171
+ wantMetrics: nil,
172
+ },
173
+ "fail on 404 response": {
174
+ prepare: case404,
175
+ wantMetrics: nil,
176
+ },
177
+ }
178
+
179
+ for name, test := range tests {
180
+ t.Run(name, func(t *testing.T) {
181
+ ms, cleanup := test.prepare(t)
182
+ defer cleanup()
183
+
184
+ _ = ms.Check()
185
+
186
+ mx := ms.Collect()
187
+
188
+ require.Equal(t, test.wantMetrics, mx)
189
+
190
+ if len(test.wantMetrics) > 0 {
191
+ assert.Equal(t, test.wantNumOfCharts, len(*ms.Charts()), "want charts")
192
+
193
+ module.TestMetricsHasAllChartsDims(t, ms.Charts(), mx)
194
+ }
195
+ })
196
+ }
197
+}
198
+
199
+func caseOk(t *testing.T) (*MaxScale, func()) {
200
+ t.Helper()
201
+ srv := httptest.NewServer(http.HandlerFunc(
202
+ func(w http.ResponseWriter, r *http.Request) {
203
+ switch r.URL.Path {
204
+ case urlPathMaxscale:
205
+ _, _ = w.Write(dataVer24MaxScale)
206
+ case urlPathMaxscaleThreads:
207
+ _, _ = w.Write(dataVer24MaxScaleThreads)
208
+ case urlPathServers:
209
+ _, _ = w.Write(dataVer24Servers)
210
+ default:
211
+ w.WriteHeader(http.StatusNotFound)
212
+ }
213
+ }))
214
+ ms := New()
215
+ ms.URL = srv.URL
216
+ require.NoError(t, ms.Init())
217
+
218
+ return ms, srv.Close
219
+}
220
+
221
+func caseUnexpectedJsonResponse(t *testing.T) (*MaxScale, func()) {
222
+ t.Helper()
223
+ resp := `
224
+{
225
+ "elephant": {
226
+ "burn": false,
227
+ "mountain": true,
228
+ "fog": false,
229
+ "skin": -1561907625,
230
+ "burst": "anyway",
231
+ "shadow": 1558616893
232
+ },
233
+ "start": "ever",
234
+ "base": 2093056027,
235
+ "mission": -2007590351,
236
+ "victory": 999053756,
237
+ "die": false
238
+}
239
+`
240
+ srv := httptest.NewServer(http.HandlerFunc(
241
+ func(w http.ResponseWriter, r *http.Request) {
242
+ _, _ = w.Write([]byte(resp))
243
+ }))
244
+ ms := New()
245
+ ms.URL = srv.URL
246
+ require.NoError(t, ms.Init())
247
+
248
+ return ms, srv.Close
249
+}
250
+
251
+func caseInvalidDataResponse(t *testing.T) (*MaxScale, func()) {
252
+ t.Helper()
253
+ srv := httptest.NewServer(http.HandlerFunc(
254
+ func(w http.ResponseWriter, r *http.Request) {
255
+ _, _ = w.Write([]byte("hello and\n goodbye"))
256
+ }))
257
+ ms := New()
258
+ ms.URL = srv.URL
259
+ require.NoError(t, ms.Init())
260
+
261
+ return ms, srv.Close
262
+}
263
+
264
+func caseConnectionRefused(t *testing.T) (*MaxScale, func()) {
265
+ t.Helper()
266
+ ms := New()
267
+ ms.URL = "http://127.0.0.1:65001"
268
+ require.NoError(t, ms.Init())
269
+
270
+ return ms, func() {}
271
+}
272
+
273
+func case404(t *testing.T) (*MaxScale, func()) {
274
+ t.Helper()
275
+ srv := httptest.NewServer(http.HandlerFunc(
276
+ func(w http.ResponseWriter, r *http.Request) {
277
+ w.WriteHeader(http.StatusNotFound)
278
+ }))
279
+ ms := New()
280
+ ms.URL = srv.URL
281
+ require.NoError(t, ms.Init())
282
+
283
+ return ms, srv.Close
284
+}
src/go/plugin/go.d/modules/maxscale/metadata.yaml
new
+272
@@ -0,0 +1,272 @@
1
+plugin_name: go.d.plugin
2
+modules:
3
+ - meta:
4
+ id: collector-go.d.plugin-maxscale
5
+ plugin_name: go.d.plugin
6
+ module_name: maxscale
7
+ monitored_instance:
8
+ name: MaxScale
9
+ link: https://mariadb.com/kb/en/maxscale/
10
+ categories:
11
+ - data-collection.database-servers
12
+ icon_filename: maxscale.svg
13
+ related_resources:
14
+ integrations:
15
+ list: []
16
+ alternative_monitored_instances: []
17
+ info_provided_to_referring_integrations:
18
+ description: ""
19
+ keywords:
20
+ - maria
21
+ - mariadb
22
+ - maxscale
23
+ - database
24
+ - db
25
+ most_popular: false
26
+ overview:
27
+ data_collection:
28
+ metrics_description: |
29
+ This collector monitors the activity and performance of MaxScale servers.
30
+ method_description: |
31
+ It sends HTTP requests to the MaxScale [REST API](https://mariadb.com/kb/en/maxscale-24-02rest-api/).
32
+ default_behavior:
33
+ auto_detection:
34
+ description: |
35
+ The collector can automatically detect MaxScale instances running on:
36
+
37
+ - localhost that are listening on port 8989
38
+ - within Docker containers
39
+
40
+ > **Note that the MaxScale REST API requires a username and password**.
41
+ > While Netdata can automatically detect MaxScale instances and create data collection jobs, these jobs will fail unless you provide the necessary credentials.
42
+ limits:
43
+ description: ""
44
+ performance_impact:
45
+ description: ""
46
+ additional_permissions:
47
+ description: ""
48
+ multi_instance: true
49
+ supported_platforms:
50
+ include: []
51
+ exclude: []
52
+ setup:
53
+ prerequisites:
54
+ list: []
55
+ configuration:
56
+ file:
57
+ name: go.d/maxscale.conf
58
+ options:
59
+ description: |
60
+ The following options can be defined globally: update_every, autodetection_retry.
61
+ folding:
62
+ title: Config options
63
+ enabled: true
64
+ list:
65
+ - name: update_every
66
+ description: Data collection frequency.
67
+ default_value: 1
68
+ required: false
69
+ - name: autodetection_retry
70
+ description: Recheck interval in seconds. Zero means no recheck will be scheduled.
71
+ default_value: 0
72
+ required: false
73
+ - name: url
74
+ description: The URL of the MaxScale HTTP API endpoint.
75
+ default_value: http://127.0.0.1:8989
76
+ required: true
77
+ - name: timeout
78
+ description: HTTP request timeout.
79
+ default_value: 1
80
+ required: false
81
+ - name: username
82
+ description: Username for basic HTTP authentication.
83
+ default_value: "admin"
84
+ required: false
85
+ - name: password
86
+ description: Password for basic HTTP authentication.
87
+ default_value: "mariadb"
88
+ required: false
89
+ - name: proxy_url
90
+ description: Proxy URL.
91
+ default_value: ""
92
+ required: false
93
+ - name: proxy_username
94
+ description: Username for proxy basic HTTP authentication.
95
+ default_value: ""
96
+ required: false
97
+ - name: proxy_password
98
+ description: Password for proxy basic HTTP authentication.
99
+ default_value: ""
100
+ required: false
101
+ - name: method
102
+ description: HTTP request method.
103
+ default_value: GET
104
+ required: false
105
+ - name: body
106
+ description: HTTP request body.
107
+ default_value: ""
108
+ required: false
109
+ - name: headers
110
+ description: HTTP request headers.
111
+ default_value: ""
112
+ required: false
113
+ - name: not_follow_redirects
114
+ description: Redirect handling policy. Controls whether the client follows redirects.
115
+ default_value: false
116
+ required: false
117
+ - name: tls_skip_verify
118
+ description: Server certificate chain and hostname validation policy. Controls whether the client performs this check.
119
+ default_value: false
120
+ required: false
121
+ - name: tls_ca
122
+ description: Certification authority that the client uses when verifying the server's certificates.
123
+ default_value: ""
124
+ required: false
125
+ - name: tls_cert
126
+ description: Client TLS certificate.
127
+ default_value: ""
128
+ required: false
129
+ - name: tls_key
130
+ description: Client TLS key.
131
+ default_value: ""
132
+ required: false
133
+ examples:
134
+ folding:
135
+ title: Config
136
+ enabled: true
137
+ list:
138
+ - name: Basic
139
+ description: A basic example configuration.
140
+ folding:
141
+ enabled: false
142
+ config: |
143
+ jobs:
144
+ - name: local
145
+ url: http://127.0.0.1:8989
146
+ username: admin
147
+ password: mariadb
148
+ - name: HTTPS with self-signed certificate
149
+ description: MaxScale with enabled HTTPS and self-signed certificate.
150
+ config: |
151
+ jobs:
152
+ - name: local
153
+ url: http://127.0.0.1:8989
154
+ username: admin
155
+ password: mariadb
156
+ tls_skip_verify: yes
157
+ - name: Multi-instance
158
+ description: |
159
+ > **Note**: When you define multiple jobs, their names must be unique.
160
+
161
+ Collecting metrics from local and remote instances.
162
+ config: |
163
+ jobs:
164
+ - name: local
165
+ url: http://127.0.0.1:8989
166
+ username: admin
167
+ password: mariadb
168
+
169
+ - name: remote
170
+ url: http://192.0.2.1:8989
171
+ username: admin
172
+ password: mariadb
173
+ troubleshooting:
174
+ problems:
175
+ list: []
176
+ alerts: []
177
+ metrics:
178
+ folding:
179
+ title: Metrics
180
+ enabled: false
181
+ description: ""
182
+ availability: []
183
+ scopes:
184
+ - name: global
185
+ description: These metrics refer to the monitored MaxScale instance.
186
+ labels: []
187
+ metrics:
188
+ - name: maxscale.poll_events
189
+ description: Poll Events
190
+ unit: events/s
191
+ chart_type: line
192
+ dimensions:
193
+ - name: reads
194
+ - name: writes
195
+ - name: accepts
196
+ - name: errors
197
+ - name: hangups
198
+ - name: maxscale.current_sessions
199
+ description: Current Sessions
200
+ unit: sessions
201
+ chart_type: line
202
+ dimensions:
203
+ - name: sessions
204
+ - name: maxscale.current_zombie_connections
205
+ description: Current Zombie Connections
206
+ unit: connections
207
+ chart_type: line
208
+ dimensions:
209
+ - name: zombie
210
+ - name: maxscale.threads_by_state
211
+ description: Threads Count by State
212
+ unit: threads
213
+ chart_type: stacked
214
+ dimensions:
215
+ - name: active
216
+ - name: draining
217
+ - name: dormant
218
+ - name: maxscale.current_fds
219
+ description: Current Managed File Descriptors
220
+ unit: fds
221
+ chart_type: line
222
+ dimensions:
223
+ - name: managed
224
+ - name: maxscale.qc_cache_efficiency
225
+ description: QC Cache Efficiency
226
+ unit: requests/s
227
+ chart_type: stacked
228
+ dimensions:
229
+ - name: hits
230
+ - name: misses
231
+ - name: maxscale.qc_cache_operations
232
+ description: QC Cache Operations
233
+ unit: operations/s
234
+ chart_type: stacked
235
+ dimensions:
236
+ - name: inserts
237
+ - name: evictions
238
+ - name: maxscale.uptime
239
+ description: Uptime
240
+ unit: seconds
241
+ chart_type: line
242
+ dimensions:
243
+ - name: uptime
244
+ - name: server
245
+ description: These metrics refer to the MariaDB server.
246
+ labels:
247
+ - name: server
248
+ description: Server ID.
249
+ - name: address
250
+ description: Server address.
251
+ metrics:
252
+ - name: maxscale.server_state
253
+ description: Server State
254
+ unit: state
255
+ chart_type: line
256
+ dimensions:
257
+ - name: master
258
+ - name: slave
259
+ - name: running
260
+ - name: down
261
+ - name: maintenance
262
+ - name: draining
263
+ - name: drained
264
+ - name: relay_master
265
+ - name: binlog_relay
266
+ - name: synced
267
+ - name: maxscale.server_current_connections
268
+ description: Server Current connections
269
+ unit: connections
270
+ chart_type: line
271
+ dimensions:
272
+ - name: connections
src/go/plugin/go.d/modules/maxscale/restapi.go
new
+84
@@ -0,0 +1,84 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package maxscale
4
+
5
+// https://mariadb.com/kb/en/maxscale-24-02rest-api/
6
+
7
+type maxscaleGlobalResponse struct {
8
+ Data *struct {
9
+ Attrs struct {
10
+ Params struct {
11
+ Threads int64 `json:"threads"`
12
+ Passive bool `json:"passive"`
13
+ } `json:"parameters"`
14
+ Uptime int64 `json:"uptime"`
15
+ } `json:"attributes"`
16
+ } `json:"data"`
17
+}
18
+
19
+// https://github.com/mariadb-corporation/MaxScale/blob/f72af9927243f59f2b20cc835273dbe6dc158623/server/core/routingworker.cc#L3021
20
+// https://github.com/mariadb-corporation/MaxScale/blob/f72af9927243f59f2b20cc835273dbe6dc158623/maxctrl/lib/show.js#L471
21
+type maxscaleThreadsResponse struct {
22
+ Data []struct {
23
+ ID string `json:"id"`
24
+ Attrs struct {
25
+ Stats struct {
26
+ State string `json:"state"`
27
+ Reads int64 `json:"reads"`
28
+ Writes int64 `json:"writes"`
29
+ Errors int64 `json:"errors"`
30
+ Hangups int64 `json:"hangups"`
31
+ Accepts int64 `json:"accepts"`
32
+ Sessions int64 `json:"sessions"`
33
+ Zombies int64 `json:"zombies"`
34
+ CurrentDescriptors int64 `json:"current_descriptors"`
35
+ TotalDescriptors int64 `json:"total_descriptors"`
36
+ QCCache struct {
37
+ Size int64 `json:"size"`
38
+ Inserts int64 `json:"inserts"`
39
+ Hits int64 `json:"hits"`
40
+ Misses int64 `json:"misses"`
41
+ Evictions int64 `json:"evictions"`
42
+ } `json:"query_classifier_cache"`
43
+ } `json:"stats"`
44
+ } `json:"attributes"`
45
+ } `json:"data"`
46
+}
47
+
48
+// // https://github.com/mariadb-corporation/MaxScale/blob/f72af9927243f59f2b20cc835273dbe6dc158623/server/core/routingworker.cc#L3064
49
+var threadStates = []string{
50
+ "Active",
51
+ "Draining",
52
+ "Dormant",
53
+}
54
+
55
+type serversResponse struct {
56
+ Data []struct {
57
+ ID string `json:"id"`
58
+ Type string `json:"type"`
59
+ Attrs struct {
60
+ Params struct {
61
+ Address string `json:"address"`
62
+ Port int `json:"port"`
63
+ } `json:"parameters"`
64
+ State string `json:"state"`
65
+ Statistics struct {
66
+ Connections int64 `json:"connections"`
67
+ } `json:"statistics"`
68
+ } `json:"attributes"`
69
+ } `json:"data"`
70
+}
71
+
72
+// https://github.com/mariadb-corporation/MaxScale/blob/f72af9927243f59f2b20cc835273dbe6dc158623/system-test/maxtest/src/maxscales.cc#L43
73
+var serverStates = []string{
74
+ "Master",
75
+ "Slave",
76
+ "Running",
77
+ "Down",
78
+ "Maintenance",
79
+ "Draining",
80
+ "Drained",
81
+ "Relay Master",
82
+ "Binlog Relay",
83
+ "Synced",
84
+}
src/go/plugin/go.d/modules/maxscale/testdata/config.json
new
+20
@@ -0,0 +1,20 @@
1
+{
2
+ "update_every": 123,
3
+ "url": "ok",
4
+ "body": "ok",
5
+ "method": "ok",
6
+ "headers": {
7
+ "ok": "ok"
8
+ },
9
+ "username": "ok",
10
+ "password": "ok",
11
+ "proxy_url": "ok",
12
+ "proxy_username": "ok",
13
+ "proxy_password": "ok",
14
+ "timeout": 123.123,
15
+ "not_follow_redirects": true,
16
+ "tls_ca": "ok",
17
+ "tls_cert": "ok",
18
+ "tls_key": "ok",
19
+ "tls_skip_verify": true
20
+}
src/go/plugin/go.d/modules/maxscale/testdata/config.yaml
new
+17
@@ -0,0 +1,17 @@
1
+update_every: 123
2
+url: "ok"
3
+body: "ok"
4
+method: "ok"
5
+headers:
6
+ ok: "ok"
7
+username: "ok"
8
+password: "ok"
9
+proxy_url: "ok"
10
+proxy_username: "ok"
11
+proxy_password: "ok"
12
+timeout: 123.123
13
+not_follow_redirects: yes
14
+tls_ca: "ok"
15
+tls_cert: "ok"
16
+tls_key: "ok"
17
+tls_skip_verify: yes
src/go/plugin/go.d/modules/maxscale/testdata/v24.02.3/maxscale.json
new
+124
@@ -0,0 +1,124 @@
1
+{
2
+ "data": {
3
+ "attributes": {
4
+ "activated_at": "Wed, 23 Oct 2024 18:39:44 GMT",
5
+ "commit": "d77a737db314a3dab3811a1e4b81e20ac2aedbe9",
6
+ "config_sync": null,
7
+ "parameters": {
8
+ "admin_audit": false,
9
+ "admin_audit_exclude_methods": [],
10
+ "admin_audit_file": "/var/log/maxscale/admin_audit.csv",
11
+ "admin_auth": true,
12
+ "admin_enabled": true,
13
+ "admin_gui": true,
14
+ "admin_host": "0.0.0.0",
15
+ "admin_jwt_algorithm": "auto",
16
+ "admin_jwt_issuer": "maxscale",
17
+ "admin_jwt_key": null,
18
+ "admin_jwt_max_age": "86400000ms",
19
+ "admin_log_auth_failures": true,
20
+ "admin_oidc_url": null,
21
+ "admin_pam_readonly_service": null,
22
+ "admin_pam_readwrite_service": null,
23
+ "admin_port": 8989,
24
+ "admin_readonly_hosts": "%",
25
+ "admin_readwrite_hosts": "%",
26
+ "admin_secure_gui": false,
27
+ "admin_ssl_ca": null,
28
+ "admin_ssl_cert": null,
29
+ "admin_ssl_key": null,
30
+ "admin_ssl_version": "MAX",
31
+ "admin_verify_url": null,
32
+ "auth_connect_timeout": "10000ms",
33
+ "auth_read_timeout": "10000ms",
34
+ "auth_write_timeout": "10000ms",
35
+ "auto_tune": [],
36
+ "cachedir": "/var/cache/maxscale",
37
+ "config_sync_cluster": null,
38
+ "config_sync_db": "mysql",
39
+ "config_sync_interval": "5000ms",
40
+ "config_sync_password": null,
41
+ "config_sync_timeout": "10000ms",
42
+ "config_sync_user": null,
43
+ "connector_plugindir": "/usr/lib64/maxscale/plugin",
44
+ "datadir": "/var/lib/maxscale",
45
+ "debug": null,
46
+ "dump_last_statements": "never",
47
+ "execdir": "/usr/bin",
48
+ "key_manager": "none",
49
+ "language": "/var/lib/maxscale",
50
+ "libdir": "/usr/lib64/maxscale",
51
+ "load_persisted_configs": true,
52
+ "local_address": null,
53
+ "log_debug": false,
54
+ "log_info": false,
55
+ "log_notice": true,
56
+ "log_throttling": {
57
+ "count": 10,
58
+ "suppress": 10000,
59
+ "window": 1000
60
+ },
61
+ "log_warn_super_user": false,
62
+ "log_warning": true,
63
+ "logdir": "/var/log/maxscale",
64
+ "max_auth_errors_until_block": 10,
65
+ "max_read_amount": 0,
66
+ "maxlog": true,
67
+ "module_configdir": "/etc/maxscale.modules.d",
68
+ "ms_timestamp": false,
69
+ "passive": false,
70
+ "persist_runtime_changes": true,
71
+ "persistdir": "/var/lib/maxscale/maxscale.cnf.d",
72
+ "piddir": "/var/run/maxscale",
73
+ "query_classifier_cache_size": 3779569459,
74
+ "query_retries": 1,
75
+ "query_retry_timeout": "5000ms",
76
+ "rebalance_period": "0ms",
77
+ "rebalance_threshold": 20,
78
+ "rebalance_window": 10,
79
+ "retain_last_statements": 0,
80
+ "secretsdir": null,
81
+ "session_trace": 0,
82
+ "session_trace_match": null,
83
+ "skip_name_resolve": false,
84
+ "sql_mode": "default",
85
+ "syslog": false,
86
+ "threads": 1,
87
+ "threads_max": 256,
88
+ "users_refresh_interval": "0ms",
89
+ "users_refresh_time": "30000ms",
90
+ "writeq_high_water": 65536,
91
+ "writeq_low_water": 1024
92
+ },
93
+ "process_datadir": "/var/lib/maxscale/data20",
94
+ "started_at": "Wed, 23 Oct 2024 18:39:44 GMT",
95
+ "system": {
96
+ "machine": {
97
+ "cores_available": 16,
98
+ "cores_physical": 16,
99
+ "cores_virtual": 16.0,
100
+ "memory_available": 25197129728,
101
+ "memory_physical": 25197129728
102
+ },
103
+ "maxscale": {
104
+ "query_classifier_cache_size": 3779569459,
105
+ "threads": 1
106
+ },
107
+ "os": {
108
+ "machine": "x86_64",
109
+ "nodename": "pve-deb-work",
110
+ "release": "6.1.0-25-amd64",
111
+ "sysname": "Linux",
112
+ "version": "#1 SMP PREEMPT_DYNAMIC Debian 6.1.106-3 (2024-08-26)"
113
+ }
114
+ },
115
+ "uptime": 61298,
116
+ "version": "24.02.3"
117
+ },
118
+ "id": "maxscale",
119
+ "type": "maxscale"
120
+ },
121
+ "links": {
122
+ "self": "http://localhost:8989/v1/maxscale/"
123
+ }
124
+}
src/go/plugin/go.d/modules/maxscale/testdata/v24.02.3/maxscale_threads.json
new
+51
@@ -0,0 +1,51 @@
1
+{
2
+ "data": [
3
+ {
4
+ "attributes": {
5
+ "stats": {
6
+ "accepts": 0,
7
+ "avg_event_queue_length": 1,
8
+ "current_descriptors": 3,
9
+ "errors": 0,
10
+ "hangups": 0,
11
+ "listening": true,
12
+ "load": {
13
+ "last_hour": 0,
14
+ "last_minute": 0,
15
+ "last_second": 0
16
+ },
17
+ "max_event_queue_length": 1,
18
+ "max_exec_time": 0,
19
+ "max_queue_time": 0,
20
+ "memory": {
21
+ "query_classifier": 0,
22
+ "sessions": 0,
23
+ "total": 0,
24
+ "zombies": 0
25
+ },
26
+ "query_classifier_cache": {
27
+ "evictions": 0,
28
+ "hits": 0,
29
+ "inserts": 0,
30
+ "misses": 0,
31
+ "size": 0
32
+ },
33
+ "reads": 68359,
34
+ "sessions": 0,
35
+ "state": "Active",
36
+ "total_descriptors": 3,
37
+ "writes": 0,
38
+ "zombies": 0
39
+ }
40
+ },
41
+ "id": "0",
42
+ "links": {
43
+ "self": "http://localhost:8989/v1/threads/0/"
44
+ },
45
+ "type": "threads"
46
+ }
47
+ ],
48
+ "links": {
49
+ "self": "http://localhost:8989/v1/maxscale/threads/"
50
+ }
51
+}
src/go/plugin/go.d/modules/maxscale/testdata/v24.02.3/servers.json
new
+198
@@ -0,0 +1,198 @@
1
+{
2
+ "data": [
3
+ {
4
+ "attributes": {
5
+ "parameters": {
6
+ "address": "127.0.0.1",
7
+ "disk_space_threshold": null,
8
+ "extra_port": 0,
9
+ "max_routing_connections": 0,
10
+ "monitorpw": null,
11
+ "monitoruser": null,
12
+ "persistmaxtime": "0ms",
13
+ "persistpoolmax": 0,
14
+ "port": 3306,
15
+ "priority": 0,
16
+ "private_address": null,
17
+ "proxy_protocol": false,
18
+ "rank": "primary",
19
+ "replication_custom_options": null,
20
+ "socket": null,
21
+ "ssl": false,
22
+ "ssl_ca": null,
23
+ "ssl_cert": null,
24
+ "ssl_cert_verify_depth": 9,
25
+ "ssl_cipher": null,
26
+ "ssl_key": null,
27
+ "ssl_verify_peer_certificate": false,
28
+ "ssl_verify_peer_host": false,
29
+ "ssl_version": "MAX"
30
+ },
31
+ "replication_lag": -1,
32
+ "source": {
33
+ "file": "/var/lib/maxscale/maxscale.cnf.d/kek.cnf",
34
+ "type": "runtime"
35
+ },
36
+ "state": "Down",
37
+ "statistics": {
38
+ "active_operations": 0,
39
+ "adaptive_avg_select_time": "0ns",
40
+ "connection_pool_empty": 0,
41
+ "connections": 0,
42
+ "failed_auths": 0,
43
+ "max_connections": 0,
44
+ "max_pool_size": 0,
45
+ "persistent_connections": 0,
46
+ "response_time_distribution": {
47
+ "read": {
48
+ "distribution": [
49
+ {
50
+ "count": 0,
51
+ "time": "0.000001",
52
+ "total": 0
53
+ },
54
+ {
55
+ "count": 0,
56
+ "time": "0.000010",
57
+ "total": 0
58
+ },
59
+ {
60
+ "count": 0,
61
+ "time": "0.000100",
62
+ "total": 0
63
+ },
64
+ {
65
+ "count": 0,
66
+ "time": "0.001000",
67
+ "total": 0
68
+ },
69
+ {
70
+ "count": 0,
71
+ "time": "0.010000",
72
+ "total": 0
73
+ },
74
+ {
75
+ "count": 0,
76
+ "time": "0.100000",
77
+ "total": 0
78
+ },
79
+ {
80
+ "count": 0,
81
+ "time": "1.000000",
82
+ "total": 0
83
+ },
84
+ {
85
+ "count": 0,
86
+ "time": "10.000000",
87
+ "total": 0
88
+ },
89
+ {
90
+ "count": 0,
91
+ "time": "100.000000",
92
+ "total": 0
93
+ },
94
+ {
95
+ "count": 0,
96
+ "time": "1000.000000",
97
+ "total": 0
98
+ },
99
+ {
100
+ "count": 0,
101
+ "time": "10000.000000",
102
+ "total": 0
103
+ },
104
+ {
105
+ "count": 0,
106
+ "time": "100000.000000",
107
+ "total": 0
108
+ }
109
+ ],
110
+ "operation": "read",
111
+ "range_base": 10
112
+ },
113
+ "write": {
114
+ "distribution": [
115
+ {
116
+ "count": 0,
117
+ "time": "0.000001",
118
+ "total": 0
119
+ },
120
+ {
121
+ "count": 0,
122
+ "time": "0.000010",
123
+ "total": 0
124
+ },
125
+ {
126
+ "count": 0,
127
+ "time": "0.000100",
128
+ "total": 0
129
+ },
130
+ {
131
+ "count": 0,
132
+ "time": "0.001000",
133
+ "total": 0
134
+ },
135
+ {
136
+ "count": 0,
137
+ "time": "0.010000",
138
+ "total": 0
139
+ },
140
+ {
141
+ "count": 0,
142
+ "time": "0.100000",
143
+ "total": 0
144
+ },
145
+ {
146
+ "count": 0,
147
+ "time": "1.000000",
148
+ "total": 0
149
+ },
150
+ {
151
+ "count": 0,
152
+ "time": "10.000000",
153
+ "total": 0
154
+ },
155
+ {
156
+ "count": 0,
157
+ "time": "100.000000",
158
+ "total": 0
159
+ },
160
+ {
161
+ "count": 0,
162
+ "time": "1000.000000",
163
+ "total": 0
164
+ },
165
+ {
166
+ "count": 0,
167
+ "time": "10000.000000",
168
+ "total": 0
169
+ },
170
+ {
171
+ "count": 0,
172
+ "time": "100000.000000",
173
+ "total": 0
174
+ }
175
+ ],
176
+ "operation": "write",
177
+ "range_base": 10
178
+ }
179
+ },
180
+ "reused_connections": 0,
181
+ "routed_packets": 0,
182
+ "total_connections": 0
183
+ },
184
+ "uptime": 0,
185
+ "version_string": ""
186
+ },
187
+ "id": "kek",
188
+ "links": {
189
+ "self": "http://localhost:8989/v1/servers/kek/"
190
+ },
191
+ "relationships": {},
192
+ "type": "servers"
193
+ }
194
+ ],
195
+ "links": {
196
+ "self": "http://localhost:8989/v1/servers/"
197
+ }
198
+}