add go.d/uwsgi (#18326)
Ilya Mashchenko committed
Aug 13, 2024 at 20:16 UTC
3527c0a93abe4f52b22755af798b1c847bd68c83
17 files changed
+1342
-1
src/collectors/python.d.plugin/python.d.conf
+1
-1
@@ -45,7 +45,6 @@ go_expvar: no
45
# spigotmc: yes
46
# traefik: yes
47
# tor: yes
48
-# uwsgi: yes
48
# varnish: yes
49
# w1sensor: yes
50
# zscores: no
@@ -82,3 +81,4 @@ sensors: no # Removed (replaced with go.d/sensors).
81
squid: no # Removed (replaced with go.d/squid).
82
tomcat: no # Removed (replaced with go.d/tomcat)
83
puppet: no # Removed (replaced with go.d/puppet).
84
+uwsgi: no # Removed (replaced with go.d/uwsgi).
src/go/plugin/go.d/config/go.d.conf
+1
@@ -107,6 +107,7 @@ modules:
107
# traefik: yes
108
# upsd: yes
109
# unbound: yes
110
+# uwsgi: yes
111
# vernemq: yes
112
# vcsa: yes
113
# vsphere: yes
src/go/plugin/go.d/config/go.d/sd/net_listeners.conf
+7
@@ -124,6 +124,8 @@ classify:
124
expr: '{{ and (eq .Port "8953") (eq .Comm "unbound") }}'
125
- tags: "upsd"
126
expr: '{{ or (eq .Port "3493") (eq .Comm "upsd") }}'
127
+ - tags: "uwsgi"
128
+ expr: '{{ and (eq .Port "1717") (eq .Comm "uwsgi") }}'
129
- tags: "vernemq"
130
expr: '{{ and (eq .Port "8888") (glob .Cmdline "*vernemq*") }}'
131
- tags: "zookeeper"
@@ -469,6 +471,11 @@ compose:
471
module: upsd
472
name: local
473
address: {{.Address}}
474
+ - selector: "uwsgi"
475
+ template: |
476
+ module: uwsgi
477
+ name: local
478
+ address: {{.Address}}
479
- selector: "vernemq"
480
template: |
481
module: vernemq
src/go/plugin/go.d/config/go.d/uwsgi.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/uwsgi#readme
3
+
4
+#jobs:
5
+# - name: local
6
+# address: 127.0.0.1:1717
src/go/plugin/go.d/modules/init.go
+1
@@ -99,6 +99,7 @@ import (
99
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/traefik"
100
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/unbound"
101
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/upsd"
102
+ _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/uwsgi"
103
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/vcsa"
104
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/vernemq"
105
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/vsphere"
src/go/plugin/go.d/modules/uwsgi/charts.go
new
+275
@@ -0,0 +1,275 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package uwsgi
4
+
5
+import (
6
+ "fmt"
7
+ "strconv"
8
+ "strings"
9
+
10
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
11
+)
12
+
13
+const (
14
+ prioTransmittedData = module.Priority + iota
15
+ prioRequests
16
+ prioHarakiris
17
+ prioExceptions
18
+ prioRespawns
19
+
20
+ prioWorkerTransmittedData
21
+ prioWorkerRequests
22
+ prioWorkerDeltaRequests
23
+ prioWorkerAvgRequestTime
24
+ prioWorkerHarakiris
25
+ prioWorkerExceptions
26
+ prioWorkerStatus
27
+ prioWorkerRequestHandlingStatus
28
+ prioWorkerRespawns
29
+ prioWorkerMemoryRss
30
+ prioWorkerMemoryVsz
31
+)
32
+
33
+var charts = module.Charts{
34
+ transmittedDataChart.Copy(),
35
+ requestsChart.Copy(),
36
+ harakirisChart.Copy(),
37
+ exceptionsChart.Copy(),
38
+ respawnsChart.Copy(),
39
+}
40
+
41
+var (
42
+ transmittedDataChart = module.Chart{
43
+ ID: "transmitted_data",
44
+ Title: "UWSGI Transmitted Data",
45
+ Units: "bytes/s",
46
+ Fam: "workers",
47
+ Ctx: "uwsgi.transmitted_data",
48
+ Priority: prioTransmittedData,
49
+ Type: module.Area,
50
+ Dims: module.Dims{
51
+ {ID: "workers_tx", Name: "tx", Algo: module.Incremental},
52
+ },
53
+ }
54
+ requestsChart = module.Chart{
55
+ ID: "requests",
56
+ Title: "UWSGI Requests",
57
+ Units: "requests/s",
58
+ Fam: "workers",
59
+ Ctx: "uwsgi.requests",
60
+ Priority: prioRequests,
61
+ Dims: module.Dims{
62
+ {ID: "workers_requests", Name: "requests", Algo: module.Incremental},
63
+ },
64
+ }
65
+ harakirisChart = module.Chart{
66
+ ID: "harakiris",
67
+ Title: "UWSGI Dropped Requests",
68
+ Units: "harakiris/s",
69
+ Fam: "workers",
70
+ Ctx: "uwsgi.harakiris",
71
+ Priority: prioHarakiris,
72
+ Dims: module.Dims{
73
+ {ID: "workers_harakiris", Name: "harakiris", Algo: module.Incremental},
74
+ },
75
+ }
76
+ exceptionsChart = module.Chart{
77
+ ID: "exceptions",
78
+ Title: "UWSGI Raised Exceptions",
79
+ Units: "exceptions/s",
80
+ Fam: "workers",
81
+ Ctx: "uwsgi.exceptions",
82
+ Priority: prioExceptions,
83
+ Dims: module.Dims{
84
+ {ID: "workers_exceptions", Name: "exceptions", Algo: module.Incremental},
85
+ },
86
+ }
87
+ respawnsChart = module.Chart{
88
+ ID: "respawns",
89
+ Title: "UWSGI Respawns",
90
+ Units: "respawns/s",
91
+ Fam: "workers",
92
+ Ctx: "uwsgi.respawns",
93
+ Priority: prioRespawns,
94
+ Dims: module.Dims{
95
+ {ID: "workers_respawns", Name: "respawns", Algo: module.Incremental},
96
+ },
97
+ }
98
+)
99
+
100
+var workerChartsTmpl = module.Charts{
101
+ workerTransmittedDataChartTmpl.Copy(),
102
+ workerRequestsChartTmpl.Copy(),
103
+ workerDeltaRequestsChartTmpl.Copy(),
104
+ workerAvgRequestTimeChartTmpl.Copy(),
105
+ workerHarakirisChartTmpl.Copy(),
106
+ workerExceptionsChartTmpl.Copy(),
107
+ workerStatusChartTmpl.Copy(),
108
+ workerRequestHandlingStatusChartTmpl.Copy(),
109
+ workerRespawnsChartTmpl.Copy(),
110
+ workerMemoryRssChartTmpl.Copy(),
111
+ workerMemoryVszChartTmpl.Copy(),
112
+}
113
+
114
+var (
115
+ workerTransmittedDataChartTmpl = module.Chart{
116
+ ID: "worker_%s_transmitted_data",
117
+ Title: "UWSGI Worker Transmitted Data",
118
+ Units: "bytes/s",
119
+ Fam: "wrk transmitted data",
120
+ Ctx: "uwsgi.worker_transmitted_data",
121
+ Priority: prioWorkerTransmittedData,
122
+ Type: module.Area,
123
+ Dims: module.Dims{
124
+ {ID: "worker_%s_tx", Name: "tx", Algo: module.Incremental},
125
+ },
126
+ }
127
+ workerRequestsChartTmpl = module.Chart{
128
+ ID: "worker_%s_requests",
129
+ Title: "UWSGI Worker Requests",
130
+ Units: "requests/s",
131
+ Fam: "wrk requests",
132
+ Ctx: "uwsgi.worker_requests",
133
+ Priority: prioWorkerRequests,
134
+ Dims: module.Dims{
135
+ {ID: "worker_%s_requests", Name: "requests", Algo: module.Incremental},
136
+ },
137
+ }
138
+ workerDeltaRequestsChartTmpl = module.Chart{
139
+ ID: "worker_%s_delta_requests",
140
+ Title: "UWSGI Worker Delta Requests",
141
+ Units: "requests/s",
142
+ Fam: "wrk requests",
143
+ Ctx: "uwsgi.worker_delta_requests",
144
+ Priority: prioWorkerDeltaRequests,
145
+ Dims: module.Dims{
146
+ {ID: "worker_%s_delta_requests", Name: "delta_requests", Algo: module.Incremental},
147
+ },
148
+ }
149
+ workerAvgRequestTimeChartTmpl = module.Chart{
150
+ ID: "worker_%s_average_request_time",
151
+ Title: "UWSGI Worker Average Request Time",
152
+ Units: "milliseconds",
153
+ Fam: "wrk request time",
154
+ Ctx: "uwsgi.worker_average_request_time",
155
+ Priority: prioWorkerAvgRequestTime,
156
+ Dims: module.Dims{
157
+ {ID: "worker_%s_average_request_time", Name: "avg"},
158
+ },
159
+ }
160
+ workerHarakirisChartTmpl = module.Chart{
161
+ ID: "worker_%s_harakiris",
162
+ Title: "UWSGI Worker Dropped Requests",
163
+ Units: "harakiris/s",
164
+ Fam: "wrk harakiris",
165
+ Ctx: "uwsgi.worker_harakiris",
166
+ Priority: prioWorkerHarakiris,
167
+ Dims: module.Dims{
168
+ {ID: "worker_%s_harakiris", Name: "harakiris", Algo: module.Incremental},
169
+ },
170
+ }
171
+ workerExceptionsChartTmpl = module.Chart{
172
+ ID: "worker_%s_exceptions",
173
+ Title: "UWSGI Worker Raised Exceptions",
174
+ Units: "exceptions/s",
175
+ Fam: "wrk exceptions",
176
+ Ctx: "uwsgi.worker_exceptions",
177
+ Priority: prioWorkerExceptions,
178
+ Dims: module.Dims{
179
+ {ID: "worker_%s_exceptions", Name: "exceptions", Algo: module.Incremental},
180
+ },
181
+ }
182
+ workerStatusChartTmpl = module.Chart{
183
+ ID: "worker_%s_status",
184
+ Title: "UWSGI Worker Status",
185
+ Units: "status",
186
+ Fam: "wrk status",
187
+ Ctx: "uwsgi.status",
188
+ Priority: prioWorkerStatus,
189
+ Dims: module.Dims{
190
+ {ID: "worker_%s_status_idle", Name: "idle"},
191
+ {ID: "worker_%s_status_busy", Name: "busy"},
192
+ {ID: "worker_%s_status_cheap", Name: "cheap"},
193
+ {ID: "worker_%s_status_pause", Name: "pause"},
194
+ {ID: "worker_%s_status_sig", Name: "sig"},
195
+ },
196
+ }
197
+ workerRequestHandlingStatusChartTmpl = module.Chart{
198
+ ID: "worker_%s_request_handling_status",
199
+ Title: "UWSGI Worker Request Handling Status",
200
+ Units: "status",
201
+ Fam: "wrk status",
202
+ Ctx: "uwsgi.request_handling_status",
203
+ Priority: prioWorkerRequestHandlingStatus,
204
+ Dims: module.Dims{
205
+ {ID: "worker_%s_request_handling_status_accepting", Name: "accepting"},
206
+ {ID: "worker_%s_request_handling_status_not_accepting", Name: "not_accepting"},
207
+ },
208
+ }
209
+ workerRespawnsChartTmpl = module.Chart{
210
+ ID: "worker_%s_respawns",
211
+ Title: "UWSGI Worker Respawns",
212
+ Units: "respawns/s",
213
+ Fam: "wrk respawns",
214
+ Ctx: "uwsgi.worker_respawns",
215
+ Priority: prioWorkerRespawns,
216
+ Dims: module.Dims{
217
+ {ID: "worker_%s_respawns", Name: "respawns", Algo: module.Incremental},
218
+ },
219
+ }
220
+ workerMemoryRssChartTmpl = module.Chart{
221
+ ID: "worker_%s_memory_rss",
222
+ Title: "UWSGI Worker Memory RSS (Resident Set Size)",
223
+ Units: "bytes",
224
+ Fam: "wrk memory",
225
+ Ctx: "uwsgi.worker_memory_rss",
226
+ Priority: prioWorkerMemoryRss,
227
+ Type: module.Area,
228
+ Dims: module.Dims{
229
+ {ID: "worker_%s_memory_rss", Name: "rss"},
230
+ },
231
+ }
232
+ workerMemoryVszChartTmpl = module.Chart{
233
+ ID: "worker_%s_memory_vsz",
234
+ Title: "UWSGI Worker Memory VSZ (Virtual Memory Size)",
235
+ Units: "bytes",
236
+ Fam: "wrk memory",
237
+ Ctx: "uwsgi.worker_memory_vsz",
238
+ Priority: prioWorkerMemoryVsz,
239
+ Type: module.Area,
240
+ Dims: module.Dims{
241
+ {ID: "worker_%s_memory_vsz", Name: "vsz"},
242
+ },
243
+ }
244
+)
245
+
246
+func (u *Uwsgi) addWorkerCharts(workerID int) {
247
+ charts := workerChartsTmpl.Copy()
248
+
249
+ id := strconv.Itoa(workerID)
250
+
251
+ for _, chart := range *charts {
252
+ chart.ID = fmt.Sprintf(chart.ID, id)
253
+ chart.Labels = []module.Label{
254
+ {Key: "worker_id", Value: id},
255
+ }
256
+ for _, dim := range chart.Dims {
257
+ dim.ID = fmt.Sprintf(dim.ID, id)
258
+ }
259
+ }
260
+
261
+ if err := u.Charts().Add(*charts...); err != nil {
262
+ u.Warning(err)
263
+ }
264
+}
265
+
266
+func (u *Uwsgi) removeWorkerCharts(workerID int) {
267
+ px := fmt.Sprintf("worker_%d_", workerID)
268
+
269
+ for _, chart := range *u.Charts() {
270
+ if strings.HasPrefix(chart.ID, px) {
271
+ chart.MarkRemove()
272
+ chart.MarkNotCreated()
273
+ }
274
+ }
275
+}
src/go/plugin/go.d/modules/uwsgi/client.go
new
+64
@@ -0,0 +1,64 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package uwsgi
4
+
5
+import (
6
+ "bytes"
7
+ "fmt"
8
+
9
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/socket"
10
+)
11
+
12
+type uwsgiConn interface {
13
+ connect() error
14
+ disconnect()
15
+ queryStats() ([]byte, error)
16
+}
17
+
18
+func newUwsgiConn(conf Config) uwsgiConn {
19
+ return &uwsgiClient{conn: socket.New(socket.Config{
20
+ Address: conf.Address,
21
+ ConnectTimeout: conf.Timeout.Duration(),
22
+ ReadTimeout: conf.Timeout.Duration(),
23
+ WriteTimeout: conf.Timeout.Duration(),
24
+ })}
25
+}
26
+
27
+type uwsgiClient struct {
28
+ conn socket.Client
29
+}
30
+
31
+func (c *uwsgiClient) connect() error {
32
+ return c.conn.Connect()
33
+}
34
+
35
+func (c *uwsgiClient) disconnect() {
36
+ _ = c.conn.Disconnect()
37
+}
38
+
39
+func (c *uwsgiClient) queryStats() ([]byte, error) {
40
+ var b bytes.Buffer
41
+ var n int64
42
+ var err error
43
+ const readLineLimit = 1000 * 10
44
+
45
+ clientErr := c.conn.Command("", func(bs []byte) bool {
46
+ b.Write(bs)
47
+ b.WriteByte('\n')
48
+
49
+ if n++; n >= readLineLimit {
50
+ err = fmt.Errorf("read line limit exceeded %d", readLineLimit)
51
+ return false
52
+ }
53
+ // The server will close the connection when it has finished sending data.
54
+ return true
55
+ })
56
+ if clientErr != nil {
57
+ return nil, clientErr
58
+ }
59
+ if err != nil {
60
+ return nil, err
61
+ }
62
+
63
+ return b.Bytes(), nil
64
+}
src/go/plugin/go.d/modules/uwsgi/collect.go
new
+128
@@ -0,0 +1,128 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package uwsgi
4
+
5
+import (
6
+ "encoding/json"
7
+ "fmt"
8
+)
9
+
10
+type statsResponse struct {
11
+ Workers []workerStats `json:"workers"`
12
+}
13
+
14
+type workerStats struct {
15
+ ID int `json:"id"`
16
+ Accepting int64 `json:"accepting"`
17
+ Requests int64 `json:"requests"`
18
+ DeltaRequests int64 `json:"delta_requests"`
19
+ Exceptions int64 `json:"exceptions"`
20
+ HarakiriCount int64 `json:"harakiri_count"`
21
+ Status string `json:"status"`
22
+ RSS int64 `json:"rss"`
23
+ VSZ int64 `json:"vsz"`
24
+ RespawnCount int64 `json:"respawn_count"`
25
+ TX int64 `json:"tx"`
26
+ AvgRT int64 `json:"avg_rt"`
27
+}
28
+
29
+func (u *Uwsgi) collect() (map[string]int64, error) {
30
+ conn, err := u.establishConn()
31
+ if err != nil {
32
+ return nil, fmt.Errorf("failed to connect: %v", err)
33
+ }
34
+
35
+ defer conn.disconnect()
36
+
37
+ stats, err := conn.queryStats()
38
+ if err != nil {
39
+ return nil, fmt.Errorf("failed to query stats: %v", err)
40
+ }
41
+
42
+ mx := make(map[string]int64)
43
+
44
+ if err := u.collectStats(mx, stats); err != nil {
45
+ return nil, err
46
+ }
47
+
48
+ return mx, nil
49
+}
50
+
51
+func (u *Uwsgi) collectStats(mx map[string]int64, stats []byte) error {
52
+ var resp statsResponse
53
+ if err := json.Unmarshal(stats, &resp); err != nil {
54
+ return fmt.Errorf("failed to json decode stats response: %v", err)
55
+ }
56
+
57
+ // stats server returns an empty array if there are no workers
58
+ if resp.Workers == nil {
59
+ return fmt.Errorf("unexpected stats response: no workers found")
60
+ }
61
+
62
+ seen := make(map[int]bool)
63
+
64
+ mx["workers_tx"] = 0
65
+ mx["workers_requests"] = 0
66
+ mx["workers_harakiris"] = 0
67
+ mx["workers_exceptions"] = 0
68
+ mx["workers_respawns"] = 0
69
+
70
+ for _, w := range resp.Workers {
71
+ mx["workers_tx"] += w.TX
72
+ mx["workers_requests"] += w.Requests
73
+ mx["workers_harakiris"] += w.HarakiriCount
74
+ mx["workers_exceptions"] += w.Exceptions
75
+ mx["workers_respawns"] += w.RespawnCount
76
+
77
+ seen[w.ID] = true
78
+
79
+ if !u.seenWorkers[w.ID] {
80
+ u.seenWorkers[w.ID] = true
81
+ u.addWorkerCharts(w.ID)
82
+ }
83
+
84
+ px := fmt.Sprintf("worker_%d_", w.ID)
85
+
86
+ mx[px+"tx"] = w.TX
87
+ mx[px+"requests"] = w.Requests
88
+ mx[px+"delta_requests"] = w.DeltaRequests
89
+ mx[px+"average_request_time"] = w.AvgRT
90
+ mx[px+"harakiris"] = w.HarakiriCount
91
+ mx[px+"exceptions"] = w.Exceptions
92
+ mx[px+"respawns"] = w.RespawnCount
93
+ mx[px+"memory_rss"] = w.RSS
94
+ mx[px+"memory_vsz"] = w.VSZ
95
+
96
+ for _, v := range []string{"idle", "busy", "cheap", "pause", "sig"} {
97
+ mx[px+"status_"+v] = boolToInt(w.Status == v)
98
+ }
99
+ mx[px+"request_handling_status_accepting"] = boolToInt(w.Accepting == 1)
100
+ mx[px+"request_handling_status_not_accepting"] = boolToInt(w.Accepting == 0)
101
+ }
102
+
103
+ for id := range u.seenWorkers {
104
+ if !seen[id] {
105
+ delete(u.seenWorkers, id)
106
+ u.removeWorkerCharts(id)
107
+ }
108
+ }
109
+
110
+ return nil
111
+}
112
+
113
+func (u *Uwsgi) establishConn() (uwsgiConn, error) {
114
+ conn := u.newConn(u.Config)
115
+
116
+ if err := conn.connect(); err != nil {
117
+ return nil, err
118
+ }
119
+
120
+ return conn, nil
121
+}
122
+
123
+func boolToInt(b bool) int64 {
124
+ if b {
125
+ return 1
126
+ }
127
+ return 0
128
+}
src/go/plugin/go.d/modules/uwsgi/config_schema.json
new
+44
@@ -0,0 +1,44 @@
1
+{
2
+ "jsonSchema": {
3
+ "$schema": "http://json-schema.org/draft-07/schema#",
4
+ "title": "UWSGI 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
+ "address": {
15
+ "title": "Address",
16
+ "description": "The IP address and port where the UWSGI [Stats Server](https://uwsgi-docs.readthedocs.io/en/latest/StatsServer.html) listens for connections.",
17
+ "type": "string",
18
+ "default": "127.0.0.1:1717"
19
+ },
20
+ "timeout": {
21
+ "title": "Timeout",
22
+ "description": "Timeout for establishing a connection and communication (reading and writing) in seconds.",
23
+ "type": "number",
24
+ "minimum": 0.5,
25
+ "default": 1
26
+ }
27
+ },
28
+ "required": [
29
+ "address"
30
+ ],
31
+ "additionalProperties": false,
32
+ "patternProperties": {
33
+ "^name$": {}
34
+ }
35
+ },
36
+ "uiSchema": {
37
+ "uiOptions": {
38
+ "fullPage": true
39
+ },
40
+ "timeout": {
41
+ "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)."
42
+ }
43
+ }
44
+}
src/go/plugin/go.d/modules/uwsgi/init.go
new
+3
@@ -0,0 +1,3 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package uwsgi
src/go/plugin/go.d/modules/uwsgi/metadata.yaml
new
+215
@@ -0,0 +1,215 @@
1
+plugin_name: go.d.plugin
2
+modules:
3
+ - meta:
4
+ id: collector-go.d.plugin-uwsgi
5
+ plugin_name: go.d.plugin
6
+ module_name: uwsgi
7
+ monitored_instance:
8
+ name: uWSGI
9
+ link: https://uwsgi-docs.readthedocs.io/en/latest/
10
+ categories:
11
+ - data-collection.web-servers-and-web-proxies
12
+ icon_filename: "uwsgi.svg"
13
+ related_resources:
14
+ integrations:
15
+ list: []
16
+ info_provided_to_referring_integrations:
17
+ description: ""
18
+ keywords:
19
+ - application server
20
+ - python
21
+ - web applications
22
+ most_popular: false
23
+ overview:
24
+ data_collection:
25
+ metrics_description: |
26
+ Monitors UWSGI worker health and performance by collecting metrics like requests, transmitted data, exceptions, and harakiris.
27
+ method_description: |
28
+ It fetches [Stats Server](https://uwsgi-docs.readthedocs.io/en/latest/StatsServer.html) statistics over TCP.
29
+ supported_platforms:
30
+ include: []
31
+ exclude: []
32
+ multi_instance: true
33
+ additional_permissions:
34
+ description: ""
35
+ default_behavior:
36
+ auto_detection:
37
+ description: |
38
+ Automatically discovers and collects UWSGI statistics from the following default locations:
39
+
40
+ - localhost:1717
41
+ limits:
42
+ description: ""
43
+ performance_impact:
44
+ description: ""
45
+ setup:
46
+ prerequisites:
47
+ list:
48
+ - title: Enable the uWSGI Stats Server
49
+ description: |
50
+ See [Stats Server](https://uwsgi-docs.readthedocs.io/en/latest/StatsServer.html) for details.
51
+ configuration:
52
+ file:
53
+ name: go.d/uwsgi.conf
54
+ options:
55
+ description: |
56
+ The following options can be defined globally: update_every, autodetection_retry.
57
+ folding:
58
+ title: Config options
59
+ enabled: true
60
+ list:
61
+ - name: update_every
62
+ description: Data collection frequency.
63
+ default_value: 1
64
+ required: false
65
+ - name: autodetection_retry
66
+ description: Recheck interval in seconds. Zero means no recheck will be scheduled.
67
+ default_value: 0
68
+ required: false
69
+ - name: address
70
+ description: "The IP address and port where the UWSGI [Stats Server](https://uwsgi-docs.readthedocs.io/en/latest/StatsServer.html) listens for connections."
71
+ default_value: 127.0.0.1:1717
72
+ required: true
73
+ - name: timeout
74
+ description: Connection, read, and write timeout duration in seconds. The timeout includes name resolution.
75
+ default_value: 1
76
+ required: false
77
+ examples:
78
+ folding:
79
+ title: Config
80
+ enabled: true
81
+ list:
82
+ - name: Basic
83
+ description: A basic example configuration.
84
+ config: |
85
+ jobs:
86
+ - name: local
87
+ address: 127.0.0.1:1717
88
+ - name: Multi-instance
89
+ description: |
90
+ > **Note**: When you define multiple jobs, their names must be unique.
91
+
92
+ Collecting metrics from local and remote instances.
93
+ config: |
94
+ jobs:
95
+ - name: local
96
+ address: 127.0.0.1:1717
97
+
98
+ - name: remote
99
+ address: 203.0.113.0:1717
100
+ troubleshooting:
101
+ problems:
102
+ list: []
103
+ alerts: []
104
+ metrics:
105
+ folding:
106
+ title: Metrics
107
+ enabled: false
108
+ description: ""
109
+ availability: []
110
+ scopes:
111
+ - name: global
112
+ description: "These metrics refer to the entire monitored application."
113
+ labels: []
114
+ metrics:
115
+ - name: uwsgi.transmitted_data
116
+ description: UWSGI Transmitted Data
117
+ unit: "bytes/s"
118
+ chart_type: area
119
+ dimensions:
120
+ - name: tx
121
+ - name: uwsgi.requests
122
+ description: UWSGI Requests
123
+ unit: "requests/s"
124
+ chart_type: line
125
+ dimensions:
126
+ - name: requests
127
+ - name: uwsgi.harakiris
128
+ description: UWSGI Dropped Requests
129
+ unit: "harakiris/s"
130
+ chart_type: line
131
+ dimensions:
132
+ - name: harakiris
133
+ - name: uwsgi.respawns
134
+ description: UWSGI Respawns
135
+ unit: "respawns/s"
136
+ chart_type: line
137
+ dimensions:
138
+ - name: respawns
139
+ - name: worker
140
+ description: "These metrics refer to the Worker process."
141
+ labels:
142
+ - name: "worker_id"
143
+ description: Worker ID.
144
+ metrics:
145
+ - name: uwsgi.worker_transmitted_data
146
+ description: UWSGI Worker Transmitted Data
147
+ unit: "bytes/s"
148
+ chart_type: area
149
+ dimensions:
150
+ - name: tx
151
+ - name: uwsgi.worker_requests
152
+ description: UWSGI Worker Requests
153
+ unit: "requests/s"
154
+ chart_type: line
155
+ dimensions:
156
+ - name: requests
157
+ - name: uwsgi.worker_delta_requests
158
+ description: UWSGI Worker Delta Requests
159
+ unit: "requests/s"
160
+ chart_type: line
161
+ dimensions:
162
+ - name: delta_requests
163
+ - name: uwsgi.worker_average_request_time
164
+ description: UWSGI Worker Average Request Time
165
+ unit: "milliseconds"
166
+ chart_type: line
167
+ dimensions:
168
+ - name: avg
169
+ - name: uwsgi.worker_harakiris
170
+ description: UWSGI Worker Dropped Requests
171
+ unit: "harakiris/s"
172
+ chart_type: line
173
+ dimensions:
174
+ - name: harakiris
175
+ - name: uwsgi.worker_exceptions
176
+ description: UWSGI Worker Raised Exceptions
177
+ unit: "exceptions/s"
178
+ chart_type: line
179
+ dimensions:
180
+ - name: exceptions
181
+ - name: uwsgi.worker_status
182
+ description: UWSGI Worker Status
183
+ unit: "status"
184
+ chart_type: line
185
+ dimensions:
186
+ - name: idle
187
+ - name: busy
188
+ - name: cheap
189
+ - name: pause
190
+ - name: sig
191
+ - name: uwsgi.worker_request_handling_status
192
+ description: UWSGI Worker Request Handling Status
193
+ unit: "status"
194
+ chart_type: line
195
+ dimensions:
196
+ - name: accepting
197
+ - name: not_accepting
198
+ - name: uwsgi.worker_respawns
199
+ description: UWSGI Worker Respawns
200
+ unit: "respawns/s"
201
+ chart_type: line
202
+ dimensions:
203
+ - name: respawns
204
+ - name: uwsgi.worker_memory_rss
205
+ description: UWSGI Worker Memory RSS (Resident Set Size)
206
+ unit: "bytes"
207
+ chart_type: area
208
+ dimensions:
209
+ - name: rss
210
+ - name: uwsgi.worker_memory_vsz
211
+ description: UWSGI Worker Memory VSZ (Virtual Memory Size)
212
+ unit: "bytes"
213
+ chart_type: area
214
+ dimensions:
215
+ - name: vsz
src/go/plugin/go.d/modules/uwsgi/testdata/config.json
new
+5
@@ -0,0 +1,5 @@
1
+{
2
+ "update_every": 123,
3
+ "address": "ok",
4
+ "timeout": 123.123
5
+}
src/go/plugin/go.d/modules/uwsgi/testdata/config.yaml
new
+3
@@ -0,0 +1,3 @@
1
+update_every: 123
2
+address: "ok"
3
+timeout: 123.123
src/go/plugin/go.d/modules/uwsgi/testdata/stats.json
new
+117
@@ -0,0 +1,117 @@
1
+{
2
+ "version": "2.1.21-debian",
3
+ "listen_queue": 1,
4
+ "listen_queue_errors": 1,
5
+ "signal_queue": 1,
6
+ "load": 1,
7
+ "pid": 859919,
8
+ "uid": 1111,
9
+ "gid": 1111,
10
+ "cwd": "/home/ilyam",
11
+ "locks": [
12
+ {
13
+ "user 1": 1
14
+ },
15
+ {
16
+ "signal": 1
17
+ },
18
+ {
19
+ "filemon": 1
20
+ },
21
+ {
22
+ "timer": 1
23
+ },
24
+ {
25
+ "rbtimer": 1
26
+ },
27
+ {
28
+ "cron": 1
29
+ },
30
+ {
31
+ "rpc": 1
32
+ },
33
+ {
34
+ "snmp": 1
35
+ }
36
+ ],
37
+ "sockets": [
38
+ {
39
+ "name": ":3131",
40
+ "proto": "uwsgi",
41
+ "queue": 1,
42
+ "max_queue": 111,
43
+ "shared": 1,
44
+ "can_offload": 1
45
+ }
46
+ ],
47
+ "workers": [
48
+ {
49
+ "id": 1,
50
+ "pid": 859911,
51
+ "accepting": 1,
52
+ "requests": 1,
53
+ "delta_requests": 1,
54
+ "exceptions": 1,
55
+ "harakiri_count": 1,
56
+ "signals": 1,
57
+ "signal_queue": 1,
58
+ "status": "idle",
59
+ "rss": 1,
60
+ "vsz": 1,
61
+ "running_time": 1,
62
+ "last_spawn": 1723542786,
63
+ "respawn_count": 1,
64
+ "tx": 1,
65
+ "avg_rt": 1,
66
+ "apps": [],
67
+ "cores": [
68
+ {
69
+ "id": 1,
70
+ "requests": 1,
71
+ "static_requests": 1,
72
+ "routed_requests": 1,
73
+ "offloaded_requests": 1,
74
+ "write_errors": 1,
75
+ "read_errors": 1,
76
+ "in_request": 1,
77
+ "vars": [],
78
+ "req_info": {}
79
+ }
80
+ ]
81
+ },
82
+ {
83
+ "id": 2,
84
+ "pid": 859911,
85
+ "accepting": 1,
86
+ "requests": 1,
87
+ "delta_requests": 1,
88
+ "exceptions": 1,
89
+ "harakiri_count": 1,
90
+ "signals": 1,
91
+ "signal_queue": 1,
92
+ "status": "idle",
93
+ "rss": 1,
94
+ "vsz": 1,
95
+ "running_time": 1,
96
+ "last_spawn": 1723542786,
97
+ "respawn_count": 1,
98
+ "tx": 1,
99
+ "avg_rt": 1,
100
+ "apps": [],
101
+ "cores": [
102
+ {
103
+ "id": 1,
104
+ "requests": 1,
105
+ "static_requests": 1,
106
+ "routed_requests": 1,
107
+ "offloaded_requests": 1,
108
+ "write_errors": 1,
109
+ "read_errors": 1,
110
+ "in_request": 1,
111
+ "vars": [],
112
+ "req_info": {}
113
+ }
114
+ ]
115
+ }
116
+ ]
117
+}
src/go/plugin/go.d/modules/uwsgi/testdata/stats_no_workers.json
new
+49
@@ -0,0 +1,49 @@
1
+{
2
+ "version": "2.0.21-debian",
3
+ "listen_queue": 0,
4
+ "listen_queue_errors": 0,
5
+ "signal_queue": 0,
6
+ "load": 0,
7
+ "pid": 1267323,
8
+ "uid": 1001,
9
+ "gid": 1001,
10
+ "cwd": "/home/ilyam",
11
+ "locks": [
12
+ {
13
+ "user 0": 0
14
+ },
15
+ {
16
+ "signal": 0
17
+ },
18
+ {
19
+ "filemon": 0
20
+ },
21
+ {
22
+ "timer": 0
23
+ },
24
+ {
25
+ "rbtimer": 0
26
+ },
27
+ {
28
+ "cron": 0
29
+ },
30
+ {
31
+ "rpc": 0
32
+ },
33
+ {
34
+ "snmp": 0
35
+ }
36
+ ],
37
+ "sockets": [
38
+ {
39
+ "name": ":3031",
40
+ "proto": "uwsgi",
41
+ "queue": 0,
42
+ "max_queue": 100,
43
+ "shared": 0,
44
+ "can_offload": 0
45
+ }
46
+ ],
47
+ "workers": [
48
+ ]
49
+}
src/go/plugin/go.d/modules/uwsgi/uwsgi.go
new
+98
@@ -0,0 +1,98 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package uwsgi
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("uwsgi", module.Creator{
19
+ JobConfigSchema: configSchema,
20
+ Create: func() module.Module { return New() },
21
+ Config: func() any { return &Config{} },
22
+ })
23
+}
24
+
25
+func New() *Uwsgi {
26
+ return &Uwsgi{
27
+ Config: Config{
28
+ Address: "127.0.0.1:1717",
29
+ Timeout: web.Duration(time.Second * 1),
30
+ },
31
+ newConn: newUwsgiConn,
32
+ charts: charts.Copy(),
33
+ seenWorkers: make(map[int]bool),
34
+ }
35
+}
36
+
37
+type Config struct {
38
+ UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
39
+ Address string `yaml:"address" json:"address"`
40
+ Timeout web.Duration `yaml:"timeout" json:"timeout"`
41
+}
42
+
43
+type Uwsgi struct {
44
+ module.Base
45
+ Config `yaml:",inline" json:""`
46
+
47
+ charts *module.Charts
48
+
49
+ newConn func(Config) uwsgiConn
50
+
51
+ seenWorkers map[int]bool
52
+}
53
+
54
+func (u *Uwsgi) Configuration() any {
55
+ return u.Config
56
+}
57
+
58
+func (u *Uwsgi) Init() error {
59
+ if u.Address == "" {
60
+ u.Error("config: 'address' not set")
61
+ return errors.New("address not set")
62
+ }
63
+
64
+ return nil
65
+}
66
+
67
+func (u *Uwsgi) Check() error {
68
+ mx, err := u.collect()
69
+ if err != nil {
70
+ u.Error(err)
71
+ return err
72
+ }
73
+
74
+ if len(mx) == 0 {
75
+ return errors.New("no metrics collected")
76
+ }
77
+
78
+ return nil
79
+}
80
+
81
+func (u *Uwsgi) Charts() *module.Charts {
82
+ return u.charts
83
+}
84
+
85
+func (u *Uwsgi) Collect() map[string]int64 {
86
+ mx, err := u.collect()
87
+ if err != nil {
88
+ u.Error(err)
89
+ }
90
+
91
+ if len(mx) == 0 {
92
+ return nil
93
+ }
94
+
95
+ return mx
96
+}
97
+
98
+func (u *Uwsgi) Cleanup() {}
src/go/plugin/go.d/modules/uwsgi/uwsgi_test.go
new
+325
@@ -0,0 +1,325 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package uwsgi
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
+ dataStats, _ = os.ReadFile("testdata/stats.json")
21
+ dataStatsNoWorkers, _ = os.ReadFile("testdata/stats_no_workers.json")
22
+)
23
+
24
+func Test_testDataIsValid(t *testing.T) {
25
+ for name, data := range map[string][]byte{
26
+ "dataConfigJSON": dataConfigJSON,
27
+ "dataConfigYAML": dataConfigYAML,
28
+ "dataStats": dataStats,
29
+ "dataStatsNoWorkers": dataStatsNoWorkers,
30
+ } {
31
+ require.NotNil(t, data, name)
32
+ }
33
+}
34
+
35
+func TestUwsgi_ConfigurationSerialize(t *testing.T) {
36
+ module.TestConfigurationSerialize(t, &Uwsgi{}, dataConfigJSON, dataConfigYAML)
37
+}
38
+
39
+func TestUwsgi_Init(t *testing.T) {
40
+ tests := map[string]struct {
41
+ config Config
42
+ wantFail bool
43
+ }{
44
+ "success with default config": {
45
+ wantFail: false,
46
+ config: New().Config,
47
+ },
48
+ "fails if address not set": {
49
+ wantFail: true,
50
+ config: func() Config {
51
+ conf := New().Config
52
+ conf.Address = ""
53
+ return conf
54
+ }(),
55
+ },
56
+ }
57
+
58
+ for name, test := range tests {
59
+ t.Run(name, func(t *testing.T) {
60
+ uw := New()
61
+ uw.Config = test.config
62
+
63
+ if test.wantFail {
64
+ assert.Error(t, uw.Init())
65
+ } else {
66
+ assert.NoError(t, uw.Init())
67
+ }
68
+ })
69
+ }
70
+}
71
+
72
+func TestUwsgi_Cleanup(t *testing.T) {
73
+ tests := map[string]struct {
74
+ prepare func() *Uwsgi
75
+ }{
76
+ "not initialized": {
77
+ prepare: func() *Uwsgi {
78
+ return New()
79
+ },
80
+ },
81
+ "after check": {
82
+ prepare: func() *Uwsgi {
83
+ uw := New()
84
+ uw.newConn = func(config Config) uwsgiConn { return prepareMockOk() }
85
+ _ = uw.Check()
86
+ return uw
87
+ },
88
+ },
89
+ "after collect": {
90
+ prepare: func() *Uwsgi {
91
+ uw := New()
92
+ uw.newConn = func(config Config) uwsgiConn { return prepareMockOk() }
93
+ _ = uw.Collect()
94
+ return uw
95
+ },
96
+ },
97
+ }
98
+
99
+ for name, test := range tests {
100
+ t.Run(name, func(t *testing.T) {
101
+ uw := test.prepare()
102
+
103
+ assert.NotPanics(t, uw.Cleanup)
104
+ })
105
+ }
106
+}
107
+
108
+func TestUwsgi_Charts(t *testing.T) {
109
+ assert.NotNil(t, New().Charts())
110
+}
111
+
112
+func TestUwsgi_Check(t *testing.T) {
113
+ tests := map[string]struct {
114
+ prepareMock func() *mockUwsgiConn
115
+ wantFail bool
116
+ }{
117
+ "success case": {
118
+ wantFail: false,
119
+ prepareMock: prepareMockOk,
120
+ },
121
+ "success case no workers": {
122
+ wantFail: false,
123
+ prepareMock: prepareMockOkNoWorkers,
124
+ },
125
+ "err on connect": {
126
+ wantFail: true,
127
+ prepareMock: prepareMockErrOnConnect,
128
+ },
129
+ "unexpected response": {
130
+ wantFail: true,
131
+ prepareMock: prepareMockUnexpectedResponse,
132
+ },
133
+ "empty response": {
134
+ wantFail: true,
135
+ prepareMock: prepareMockEmptyResponse,
136
+ },
137
+ }
138
+
139
+ for name, test := range tests {
140
+ t.Run(name, func(t *testing.T) {
141
+ uw := New()
142
+ mock := test.prepareMock()
143
+ uw.newConn = func(config Config) uwsgiConn { return mock }
144
+
145
+ if test.wantFail {
146
+ assert.Error(t, uw.Check())
147
+ } else {
148
+ assert.NoError(t, uw.Check())
149
+ }
150
+ })
151
+ }
152
+}
153
+
154
+func TestUwsgi_Collect(t *testing.T) {
155
+ tests := map[string]struct {
156
+ prepareMock func() *mockUwsgiConn
157
+ wantMetrics map[string]int64
158
+ wantCharts int
159
+ disconnectBeforeCleanup bool
160
+ disconnectAfterCleanup bool
161
+ }{
162
+ "success case": {
163
+ prepareMock: prepareMockOk,
164
+ wantCharts: len(charts) + len(workerChartsTmpl)*2,
165
+ disconnectBeforeCleanup: true,
166
+ disconnectAfterCleanup: true,
167
+ wantMetrics: map[string]int64{
168
+ "worker_1_average_request_time": 1,
169
+ "worker_1_delta_requests": 1,
170
+ "worker_1_exceptions": 1,
171
+ "worker_1_harakiris": 1,
172
+ "worker_1_memory_rss": 1,
173
+ "worker_1_memory_vsz": 1,
174
+ "worker_1_request_handling_status_accepting": 1,
175
+ "worker_1_request_handling_status_not_accepting": 0,
176
+ "worker_1_requests": 1,
177
+ "worker_1_respawns": 1,
178
+ "worker_1_status_busy": 0,
179
+ "worker_1_status_cheap": 0,
180
+ "worker_1_status_idle": 1,
181
+ "worker_1_status_pause": 0,
182
+ "worker_1_status_sig": 0,
183
+ "worker_1_tx": 1,
184
+ "worker_2_average_request_time": 1,
185
+ "worker_2_delta_requests": 1,
186
+ "worker_2_exceptions": 1,
187
+ "worker_2_harakiris": 1,
188
+ "worker_2_memory_rss": 1,
189
+ "worker_2_memory_vsz": 1,
190
+ "worker_2_request_handling_status_accepting": 1,
191
+ "worker_2_request_handling_status_not_accepting": 0,
192
+ "worker_2_requests": 1,
193
+ "worker_2_respawns": 1,
194
+ "worker_2_status_busy": 0,
195
+ "worker_2_status_cheap": 0,
196
+ "worker_2_status_idle": 1,
197
+ "worker_2_status_pause": 0,
198
+ "worker_2_status_sig": 0,
199
+ "worker_2_tx": 1,
200
+ "workers_exceptions": 2,
201
+ "workers_harakiris": 2,
202
+ "workers_requests": 2,
203
+ "workers_respawns": 2,
204
+ "workers_tx": 2,
205
+ },
206
+ },
207
+ "success case no workers": {
208
+ prepareMock: prepareMockOkNoWorkers,
209
+ wantCharts: len(charts),
210
+ wantMetrics: map[string]int64{
211
+ "workers_exceptions": 0,
212
+ "workers_harakiris": 0,
213
+ "workers_requests": 0,
214
+ "workers_respawns": 0,
215
+ "workers_tx": 0,
216
+ },
217
+ disconnectBeforeCleanup: true,
218
+ disconnectAfterCleanup: true,
219
+ },
220
+ "unexpected response": {
221
+ prepareMock: prepareMockUnexpectedResponse,
222
+ wantCharts: len(charts),
223
+ disconnectBeforeCleanup: true,
224
+ disconnectAfterCleanup: true,
225
+ },
226
+ "empty response": {
227
+ prepareMock: prepareMockEmptyResponse,
228
+ wantCharts: len(charts),
229
+ disconnectBeforeCleanup: true,
230
+ disconnectAfterCleanup: true,
231
+ },
232
+ "err on connect": {
233
+ prepareMock: prepareMockErrOnConnect,
234
+ wantCharts: len(charts),
235
+ disconnectBeforeCleanup: false,
236
+ disconnectAfterCleanup: false,
237
+ },
238
+ "err on query stats": {
239
+ prepareMock: prepareMockErrOnQueryStats,
240
+ wantCharts: len(charts),
241
+ disconnectBeforeCleanup: true,
242
+ disconnectAfterCleanup: true,
243
+ },
244
+ }
245
+
246
+ for name, test := range tests {
247
+ t.Run(name, func(t *testing.T) {
248
+ uw := New()
249
+ mock := test.prepareMock()
250
+ uw.newConn = func(config Config) uwsgiConn { return mock }
251
+
252
+ mx := uw.Collect()
253
+
254
+ require.Equal(t, test.wantMetrics, mx)
255
+
256
+ if len(test.wantMetrics) > 0 {
257
+ module.TestMetricsHasAllChartsDims(t, uw.Charts(), mx)
258
+ }
259
+ assert.Equal(t, test.wantCharts, len(*uw.Charts()), "want charts")
260
+
261
+ assert.Equal(t, test.disconnectBeforeCleanup, mock.disconnectCalled, "disconnect before cleanup")
262
+ uw.Cleanup()
263
+ assert.Equal(t, test.disconnectAfterCleanup, mock.disconnectCalled, "disconnect after cleanup")
264
+ })
265
+ }
266
+}
267
+
268
+func prepareMockOk() *mockUwsgiConn {
269
+ return &mockUwsgiConn{
270
+ statsResponse: dataStats,
271
+ }
272
+}
273
+
274
+func prepareMockOkNoWorkers() *mockUwsgiConn {
275
+ return &mockUwsgiConn{
276
+ statsResponse: dataStatsNoWorkers,
277
+ }
278
+}
279
+
280
+func prepareMockErrOnConnect() *mockUwsgiConn {
281
+ return &mockUwsgiConn{
282
+ errOnConnect: true,
283
+ }
284
+}
285
+
286
+func prepareMockErrOnQueryStats() *mockUwsgiConn {
287
+ return &mockUwsgiConn{
288
+ errOnQueryStats: true,
289
+ }
290
+}
291
+
292
+func prepareMockUnexpectedResponse() *mockUwsgiConn {
293
+ return &mockUwsgiConn{
294
+ statsResponse: []byte("Lorem ipsum dolor sit amet, consectetur adipiscing elit."),
295
+ }
296
+}
297
+
298
+func prepareMockEmptyResponse() *mockUwsgiConn {
299
+ return &mockUwsgiConn{}
300
+}
301
+
302
+type mockUwsgiConn struct {
303
+ errOnConnect bool
304
+ errOnQueryStats bool
305
+ statsResponse []byte
306
+ disconnectCalled bool
307
+}
308
+
309
+func (m *mockUwsgiConn) connect() error {
310
+ if m.errOnConnect {
311
+ return errors.New("mock.connect() error")
312
+ }
313
+ return nil
314
+}
315
+
316
+func (m *mockUwsgiConn) disconnect() {
317
+ m.disconnectCalled = true
318
+}
319
+
320
+func (m *mockUwsgiConn) queryStats() ([]byte, error) {
321
+ if m.errOnQueryStats {
322
+ return nil, errors.New("mock.queryStats() error")
323
+ }
324
+ return m.statsResponse, nil
325
+}