@cryptotaxi247 / netdata-1 / commits / 3527c0a93

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 +}