@cryptotaxi247 / netdata-1 / commits / 19c500963

feat(go.d): add NATS collector (#19252)

add go.d/nats

Ilya Mashchenko committed Dec 19, 2024 at 22:23 UTC 19c500963e31a99d1242e943be8c471265af706a
17 files changed +1302 -4
src/go/plugin/go.d/README.md
+5 -4
@@ -14,8 +14,8 @@
14
15 All capabilities are set automatically during Netdata installation using the [official installation method](/packaging/installer/methods/kickstart.md).
16
17 -| Capability | Required by |
18 -|:--------------------|:-------------------------------------------------------------------------------------------------------:|
17 +| Capability | Required by |
18 +|:--------------------|:---------------------------------------------------------------------------------------------------------:|
19 | CAP_NET_RAW | [Ping](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/collector/ping#readme) |
20 | CAP_NET_ADMIN | [Wireguard](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/collector/wireguard#readme) |
21 | CAP_DAC_READ_SEARCH | [Filecheck](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/collector/filecheck#readme) |
@@ -25,8 +25,8 @@ All capabilities are set automatically during Netdata installation using the [of
25 <details>
26 <summary>Data Collection Modules</summary>
27
28 -| Name | Monitors |
29 -|:-------------------------------------------------------------------------------------------------------------------|:-----------------------------:|
28 +| Name | Monitors |
29 +|:---------------------------------------------------------------------------------------------------------------------|:-----------------------------:|
30 | [adaptec_raid](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/collector/adaptecraid) | Adaptec Hardware RAID |
31 | [activemq](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/collector/activemq) | ActiveMQ |
32 | [ap](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/collector/ap) | Wireless AP |
@@ -84,6 +84,7 @@ All capabilities are set automatically during Netdata installation using the [of
84 | [mongoDB](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/collector/mongodb) | MongoDB |
85 | [monit](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/collector/monit) | Monit |
86 | [mysql](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/collector/mysql) | MySQL |
87 +| [nats](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/collector/nats) | NATS |
88 | [nginx](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/collector/nginx) | NGINX |
89 | [nginxplus](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/collector/nginxplus) | NGINX Plus |
90 | [nginxunit](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/collector/nginxunit) | NGINX Unit |
src/go/plugin/go.d/collector/init.go
+1
@@ -61,6 +61,7 @@ import (
61 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/mongodb"
62 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/monit"
63 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/mysql"
64 + _ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/nats"
65 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/nginx"
66 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/nginxplus"
67 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/nginxunit"
src/go/plugin/go.d/collector/nats/charts.go new
+161
@@ -0,0 +1,161 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package nats
4 +
5 +import (
6 + "fmt"
7 +
8 + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
9 +)
10 +
11 +const (
12 + prioServerTraffic = module.Priority + iota
13 + prioServerMessages
14 + prioServerConnectionsCurrent
15 + prioServerConnectionsRate
16 + prioHttpEndpointRequests
17 + prioServerHealthProbeStatus
18 + prioServerCpuUsage
19 + prioServerMemoryUsage
20 + prioServerUptime
21 +)
22 +
23 +var serverCharts = func() module.Charts {
24 + charts := module.Charts{
25 + chartServerConnectionsCurrent.Copy(),
26 + chartServerConnectionsRate.Copy(),
27 + chartServerTraffic.Copy(),
28 + chartServerMessages.Copy(),
29 + chartServerHealthProbeStatus.Copy(),
30 + chartServerCpuUsage.Copy(),
31 + chartServerMemUsage.Copy(),
32 + chartServerUptime.Copy(),
33 + }
34 + charts = append(charts, httpEndpointCharts()...)
35 + return charts
36 +}()
37 +
38 +var (
39 + chartServerTraffic = module.Chart{
40 + ID: "server_traffic",
41 + Title: "Server Traffic",
42 + Units: "bytes/s",
43 + Fam: "traffic",
44 + Ctx: "nats.server_traffic",
45 + Priority: prioServerTraffic,
46 + Type: module.Area,
47 + Dims: module.Dims{
48 + {ID: "in_bytes", Name: "in", Algo: module.Incremental},
49 + {ID: "out_bytes", Name: "out", Mul: -1, Algo: module.Incremental},
50 + },
51 + }
52 + chartServerMessages = module.Chart{
53 + ID: "server_messages",
54 + Title: "Server Messages",
55 + Units: "messages/s",
56 + Fam: "traffic",
57 + Ctx: "nats.server_messages",
58 + Priority: prioServerMessages,
59 + Dims: module.Dims{
60 + {ID: "in_msgs", Name: "in", Algo: module.Incremental},
61 + {ID: "out_msgs", Name: "out", Mul: -1, Algo: module.Incremental},
62 + },
63 + }
64 + chartServerConnectionsCurrent = module.Chart{
65 + ID: "server_connections_current",
66 + Title: "Server Current Connections",
67 + Units: "connections",
68 + Fam: "connections",
69 + Ctx: "nats.server_connections_current",
70 + Priority: prioServerConnectionsCurrent,
71 + Dims: module.Dims{
72 + {ID: "connections", Name: "active"},
73 + },
74 + }
75 + chartServerConnectionsRate = module.Chart{
76 + ID: "server_connections_rate",
77 + Title: "Server Connections",
78 + Units: "connections/s",
79 + Fam: "connections",
80 + Ctx: "nats.server_connections_rate",
81 + Priority: prioServerConnectionsRate,
82 + Dims: module.Dims{
83 + {ID: "total_connections", Name: "connections", Algo: module.Incremental},
84 + },
85 + }
86 + chartServerHealthProbeStatus = module.Chart{
87 + ID: "server_health_probe_status",
88 + Title: "Server Health Probe Status",
89 + Units: "status",
90 + Fam: "health",
91 + Ctx: "nats.server_health_probe_status",
92 + Priority: prioServerHealthProbeStatus,
93 + Dims: module.Dims{
94 + {ID: "healthz_status_ok", Name: "ok"},
95 + {ID: "healthz_status_error", Name: "error"},
96 + },
97 + }
98 + chartServerCpuUsage = module.Chart{
99 + ID: "server_cpu_usage",
100 + Title: "Server CPU Usage",
101 + Units: "percent",
102 + Fam: "rusage",
103 + Ctx: "nats.server_cpu_usage",
104 + Priority: prioServerCpuUsage,
105 + Type: module.Area,
106 + Dims: module.Dims{
107 + {ID: "cpu", Name: "used"},
108 + },
109 + }
110 + chartServerMemUsage = module.Chart{
111 + ID: "server_mem_usage",
112 + Title: "Server Memory Usage",
113 + Units: "bytes",
114 + Fam: "rusage",
115 + Ctx: "nats.server_mem_usage",
116 + Priority: prioServerMemoryUsage,
117 + Type: module.Area,
118 + Dims: module.Dims{
119 + {ID: "mem", Name: "used"},
120 + },
121 + }
122 + chartServerUptime = module.Chart{
123 + ID: "server_uptime",
124 + Title: "Server Uptime",
125 + Units: "seconds",
126 + Fam: "uptime",
127 + Ctx: "nats.server_uptime",
128 + Priority: prioServerUptime,
129 + Dims: module.Dims{
130 + {ID: "uptime", Name: "uptime"},
131 + },
132 + }
133 +)
134 +
135 +func httpEndpointCharts() module.Charts {
136 + var charts module.Charts
137 + for _, path := range httpEndpoints {
138 + chart := httpEndpointRequestsChartTmpl.Copy()
139 + chart.ID = fmt.Sprintf(chart.ID, path)
140 + chart.Labels = []module.Label{
141 + {Key: "http_endpoint", Value: path},
142 + }
143 + for _, dim := range chart.Dims {
144 + dim.ID = fmt.Sprintf(dim.ID, path)
145 + }
146 + charts = append(charts, chart)
147 + }
148 + return charts
149 +}
150 +
151 +var httpEndpointRequestsChartTmpl = module.Chart{
152 + ID: "http_endpoint_%s_requests",
153 + Title: "HTTP Endpoint Requests",
154 + Units: "requests/s",
155 + Fam: "http requests",
156 + Ctx: "nats.http_endpoint_requests",
157 + Priority: prioHttpEndpointRequests,
158 + Dims: module.Dims{
159 + {ID: "http_endpoint_%s_req", Name: "requests", Algo: module.Incremental},
160 + },
161 +}
src/go/plugin/go.d/collector/nats/collect.go new
+85
@@ -0,0 +1,85 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package nats
4 +
5 +import (
6 + "fmt"
7 + "net/http"
8 +
9 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/metrix"
10 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/web"
11 +)
12 +
13 +const (
14 + urlPathVarz = "/varz"
15 + urlPathHealthz = "/healthz"
16 +)
17 +
18 +func (c *Collector) collect() (map[string]int64, error) {
19 + mx := make(map[string]int64)
20 +
21 + if err := c.collectVarz(mx); err != nil {
22 + return nil, err
23 + }
24 + if err := c.collectHealthz(mx); err != nil {
25 + return nil, err
26 + }
27 +
28 + return mx, nil
29 +}
30 +
31 +func (c *Collector) collectVarz(mx map[string]int64) error {
32 + // https://docs.nats.io/running-a-nats-service/nats_admin/monitoring#general-information
33 + req, err := web.NewHTTPRequestWithPath(c.RequestConfig, urlPathVarz)
34 + if err != nil {
35 + return err
36 + }
37 +
38 + var resp varzResponse
39 + if err := web.DoHTTP(c.httpClient).RequestJSON(req, &resp); err != nil {
40 + return err
41 + }
42 +
43 + mx["uptime"] = int64(resp.Now.Sub(resp.Start).Seconds())
44 + mx["in_msgs"] = resp.InMsgs
45 + mx["out_msgs"] = resp.OutMsgs
46 + mx["in_bytes"] = resp.InBytes
47 + mx["out_bytes"] = resp.OutBytes
48 + mx["slow_consumers"] = resp.SlowConsumers
49 + mx["subscriptions"] = int64(resp.Subscriptions)
50 + mx["connections"] = int64(resp.Connections)
51 + mx["total_connections"] = int64(resp.TotalConnections)
52 + mx["routes"] = int64(resp.Routes)
53 + mx["remotes"] = int64(resp.Remotes)
54 + mx["cpu"] = int64(resp.CPU)
55 + mx["mem"] = resp.Mem
56 +
57 + for _, path := range httpEndpoints {
58 + v := resp.HTTPReqStats[path]
59 + mx[fmt.Sprintf("http_endpoint_%s_req", path)] = int64(v)
60 + }
61 +
62 + return nil
63 +}
64 +
65 +func (c *Collector) collectHealthz(mx map[string]int64) error {
66 + // https://docs.nats.io/running-a-nats-service/nats_admin/monitoring#health
67 + req, err := web.NewHTTPRequestWithPath(c.RequestConfig, urlPathHealthz)
68 + if err != nil {
69 + return err
70 + }
71 +
72 + var resp healthzResponse
73 + client := web.DoHTTP(c.httpClient).OnNokCode(func(resp *http.Response) (bool, error) { return true, nil })
74 + if err := client.RequestJSON(req, &resp); err != nil {
75 + return err
76 + }
77 + if resp.Status == nil {
78 + return fmt.Errorf("healthz response missing status")
79 + }
80 +
81 + mx["healthz_status_ok"] = metrix.Bool(*resp.Status == "ok")
82 + mx["healthz_status_error"] = metrix.Bool(*resp.Status != "ok")
83 +
84 + return nil
85 +}
src/go/plugin/go.d/collector/nats/collector.go new
+113
@@ -0,0 +1,113 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package nats
4 +
5 +import (
6 + "context"
7 + _ "embed"
8 + "errors"
9 + "fmt"
10 + "net/http"
11 + "time"
12 +
13 + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
14 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/confopt"
15 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/web"
16 +)
17 +
18 +//go:embed "config_schema.json"
19 +var configSchema string
20 +
21 +func init() {
22 + module.Register("nats", module.Creator{
23 + Create: func() module.Module { return New() },
24 + JobConfigSchema: configSchema,
25 + Config: func() any { return &Config{} },
26 + })
27 +}
28 +
29 +func New() *Collector {
30 + return &Collector{
31 + Config: Config{
32 + HTTPConfig: web.HTTPConfig{
33 + RequestConfig: web.RequestConfig{
34 + URL: "http://127.0.0.1:8222",
35 + },
36 + ClientConfig: web.ClientConfig{
37 + Timeout: confopt.Duration(time.Second),
38 + },
39 + },
40 + },
41 + charts: serverCharts.Copy(),
42 + }
43 +}
44 +
45 +type Config struct {
46 + Vnode string `yaml:"vnode,omitempty" json:"vnode"`
47 + UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
48 + web.HTTPConfig `yaml:",inline" json:""`
49 +}
50 +
51 +type Collector struct {
52 + module.Base
53 + Config `yaml:",inline" json:""`
54 +
55 + charts *module.Charts
56 +
57 + httpClient *http.Client
58 +}
59 +
60 +func (c *Collector) Configuration() any {
61 + return c.Config
62 +}
63 +
64 +func (c *Collector) Init(context.Context) error {
65 + if c.URL == "" {
66 + return errors.New("URL required but not set")
67 + }
68 +
69 + httpClient, err := web.NewHTTPClient(c.ClientConfig)
70 + if err != nil {
71 + return fmt.Errorf("init HTTP client: %v", err)
72 + }
73 + c.httpClient = httpClient
74 +
75 + c.Debugf("using URL %s", c.URL)
76 + c.Debugf("using timeout: %s", c.Timeout)
77 +
78 + return nil
79 +}
80 +
81 +func (c *Collector) Check(context.Context) error {
82 + mx, err := c.collect()
83 + if err != nil {
84 + return err
85 + }
86 + if len(mx) == 0 {
87 + return errors.New("no metrics collected")
88 +
89 + }
90 + return nil
91 +}
92 +
93 +func (c *Collector) Charts() *module.Charts {
94 + return c.charts
95 +}
96 +
97 +func (c *Collector) Collect(context.Context) map[string]int64 {
98 + mx, err := c.collect()
99 + if err != nil {
100 + c.Error(err)
101 + }
102 +
103 + if len(mx) == 0 {
104 + return nil
105 + }
106 + return mx
107 +}
108 +
109 +func (c *Collector) Cleanup(context.Context) {
110 + if c.httpClient != nil {
111 + c.httpClient.CloseIdleConnections()
112 + }
113 +}
src/go/plugin/go.d/collector/nats/collector_test.go new
+282
@@ -0,0 +1,282 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package nats
4 +
5 +import (
6 + "context"
7 + "net/http"
8 + "net/http/httptest"
9 + "os"
10 + "testing"
11 +
12 + "github.com/stretchr/testify/assert"
13 + "github.com/stretchr/testify/require"
14 +
15 + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
16 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/web"
17 +)
18 +
19 +var (
20 + dataConfigJSON, _ = os.ReadFile("testdata/config.json")
21 + dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")
22 +
23 + dataVer210Varz, _ = os.ReadFile("testdata/v2.10.24/varz.json")
24 + dataVer210HealthzOk, _ = os.ReadFile("testdata/v2.10.24/healthz-ok.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 + "dataVer210Varz": dataVer210Varz,
32 + "dataVer210HealthzOk": dataVer210HealthzOk,
33 + } {
34 + require.NotNil(t, data, name)
35 + }
36 +}
37 +
38 +func TestCollector_ConfigurationSerialize(t *testing.T) {
39 + module.TestConfigurationSerialize(t, &Collector{}, dataConfigJSON, dataConfigYAML)
40 +}
41 +
42 +func TestCollector_Init(t *testing.T) {
43 + tests := map[string]struct {
44 + wantFail bool
45 + config Config
46 + }{
47 + "success with default": {
48 + wantFail: false,
49 + config: New().Config,
50 + },
51 + "fail when URL not set": {
52 + wantFail: true,
53 + config: Config{
54 + HTTPConfig: web.HTTPConfig{
55 + RequestConfig: web.RequestConfig{URL: ""},
56 + },
57 + },
58 + },
59 + }
60 +
61 + for name, test := range tests {
62 + t.Run(name, func(t *testing.T) {
63 + collr := New()
64 + collr.Config = test.config
65 +
66 + if test.wantFail {
67 + assert.Error(t, collr.Init(context.Background()))
68 + } else {
69 + assert.NoError(t, collr.Init(context.Background()))
70 + }
71 + })
72 + }
73 +}
74 +
75 +func TestCollector_Check(t *testing.T) {
76 + tests := map[string]struct {
77 + wantFail bool
78 + prepare func(t *testing.T) (nu *Collector, cleanup func())
79 + }{
80 + "success on valid response": {
81 + wantFail: false,
82 + prepare: caseOk,
83 + },
84 + "fail on unexpected JSON response": {
85 + wantFail: true,
86 + prepare: caseUnexpectedJsonResponse,
87 + },
88 + "fail on invalid data response": {
89 + wantFail: true,
90 + prepare: caseInvalidDataResponse,
91 + },
92 + "fail on connection refused": {
93 + wantFail: true,
94 + prepare: caseConnectionRefused,
95 + },
96 + "fail on 404 response": {
97 + wantFail: true,
98 + prepare: case404,
99 + },
100 + }
101 +
102 + for name, test := range tests {
103 + t.Run(name, func(t *testing.T) {
104 + collr, cleanup := test.prepare(t)
105 + defer cleanup()
106 +
107 + if test.wantFail {
108 + assert.Error(t, collr.Check(context.Background()))
109 + } else {
110 + assert.NoError(t, collr.Check(context.Background()))
111 + }
112 + })
113 + }
114 +}
115 +
116 +func TestCollector_Charts(t *testing.T) {
117 + assert.NotNil(t, New().Charts())
118 +}
119 +
120 +func TestCollector_Collect(t *testing.T) {
121 + tests := map[string]struct {
122 + prepare func(t *testing.T) (nu *Collector, cleanup func())
123 + wantNumOfCharts int
124 + wantMetrics map[string]int64
125 + }{
126 + "success on valid response": {
127 + prepare: caseOk,
128 + wantNumOfCharts: len(serverCharts),
129 + wantMetrics: map[string]int64{
130 + "connections": 0,
131 + "cpu": 0,
132 + "healthz_status_error": 0,
133 + "healthz_status_ok": 1,
134 + "http_endpoint_/_req": 3,
135 + "http_endpoint_/accountz_req": 2,
136 + "http_endpoint_/accstatz_req": 2,
137 + "http_endpoint_/connz_req": 2,
138 + "http_endpoint_/gatewayz_req": 2,
139 + "http_endpoint_/healthz_req": 2017,
140 + "http_endpoint_/ipqueuesz_req": 0,
141 + "http_endpoint_/jsz_req": 3,
142 + "http_endpoint_/leafz_req": 2,
143 + "http_endpoint_/raftz_req": 0,
144 + "http_endpoint_/routez_req": 2,
145 + "http_endpoint_/stacksz_req": 0,
146 + "http_endpoint_/subsz_req": 1,
147 + "http_endpoint_/varz_req": 3750,
148 + "in_bytes": 0,
149 + "in_msgs": 0,
150 + "mem": 21725184,
151 + "out_bytes": 0,
152 + "out_msgs": 0,
153 + "remotes": 0,
154 + "routes": 0,
155 + "slow_consumers": 0,
156 + "subscriptions": 57,
157 + "total_connections": 0,
158 + "uptime": 27513,
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 + collr, cleanup := test.prepare(t)
182 + defer cleanup()
183 +
184 + _ = collr.Check(context.Background())
185 +
186 + mx := collr.Collect(context.Background())
187 +
188 + require.Equal(t, test.wantMetrics, mx)
189 +
190 + if len(test.wantMetrics) > 0 {
191 + assert.Equal(t, test.wantNumOfCharts, len(*collr.Charts()), "want charts")
192 +
193 + module.TestMetricsHasAllChartsDims(t, collr.Charts(), mx)
194 + }
195 + })
196 + }
197 +}
198 +
199 +func caseOk(t *testing.T) (*Collector, 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 urlPathVarz:
205 + _, _ = w.Write(dataVer210Varz)
206 + case urlPathHealthz:
207 + _, _ = w.Write(dataVer210HealthzOk)
208 + default:
209 + w.WriteHeader(http.StatusNotFound)
210 + }
211 + }))
212 + collr := New()
213 + collr.URL = srv.URL
214 + require.NoError(t, collr.Init(context.Background()))
215 +
216 + return collr, srv.Close
217 +}
218 +
219 +func caseUnexpectedJsonResponse(t *testing.T) (*Collector, func()) {
220 + t.Helper()
221 + resp := `
222 +{
223 + "elephant": {
224 + "burn": false,
225 + "mountain": true,
226 + "fog": false,
227 + "skin": -1561907625,
228 + "burst": "anyway",
229 + "shadow": 1558616893
230 + },
231 + "start": "ever",
232 + "base": 2093056027,
233 + "mission": -2007590351,
234 + "victory": 999053756,
235 + "die": false
236 +}
237 +`
238 + srv := httptest.NewServer(http.HandlerFunc(
239 + func(w http.ResponseWriter, r *http.Request) {
240 + _, _ = w.Write([]byte(resp))
241 + }))
242 + collr := New()
243 + collr.URL = srv.URL
244 + require.NoError(t, collr.Init(context.Background()))
245 +
246 + return collr, srv.Close
247 +}
248 +
249 +func caseInvalidDataResponse(t *testing.T) (*Collector, func()) {
250 + t.Helper()
251 + srv := httptest.NewServer(http.HandlerFunc(
252 + func(w http.ResponseWriter, r *http.Request) {
253 + _, _ = w.Write([]byte("hello and\n goodbye"))
254 + }))
255 + collr := New()
256 + collr.URL = srv.URL
257 + require.NoError(t, collr.Init(context.Background()))
258 +
259 + return collr, srv.Close
260 +}
261 +
262 +func caseConnectionRefused(t *testing.T) (*Collector, func()) {
263 + t.Helper()
264 + collr := New()
265 + collr.URL = "http://127.0.0.1:65001"
266 + require.NoError(t, collr.Init(context.Background()))
267 +
268 + return collr, func() {}
269 +}
270 +
271 +func case404(t *testing.T) (*Collector, func()) {
272 + t.Helper()
273 + srv := httptest.NewServer(http.HandlerFunc(
274 + func(w http.ResponseWriter, r *http.Request) {
275 + w.WriteHeader(http.StatusNotFound)
276 + }))
277 + collr := New()
278 + collr.URL = srv.URL
279 + require.NoError(t, collr.Init(context.Background()))
280 +
281 + return collr, srv.Close
282 +}
src/go/plugin/go.d/collector/nats/config_schema.json new
+191
@@ -0,0 +1,191 @@
1 +{
2 + "jsonSchema": {
3 + "$schema": "http://json-schema.org/draft-07/schema#",
4 + "title": "NATS 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 NATS [monitoring endpoint](https://docs.nats.io/running-a-nats-service/nats_admin/monitoring#enabling-monitoring).",
17 + "type": "string",
18 + "default": "http://127.0.0.1:8222",
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 + "vnode": {
34 + "title": "Vnode",
35 + "description": "Associates this data collection job with a [Virtual Node](https://learn.netdata.cloud/docs/netdata-agent/configuration/organize-systems-metrics-and-alerts#virtual-nodes).",
36 + "type": "string"
37 + },
38 + "username": {
39 + "title": "Username",
40 + "description": "The username for basic authentication.",
41 + "type": "string",
42 + "sensitive": true
43 + },
44 + "password": {
45 + "title": "Password",
46 + "description": "The password for basic authentication.",
47 + "type": "string",
48 + "sensitive": true
49 + },
50 + "proxy_url": {
51 + "title": "Proxy URL",
52 + "description": "The URL of the proxy server.",
53 + "type": "string"
54 + },
55 + "proxy_username": {
56 + "title": "Proxy username",
57 + "description": "The username for proxy authentication.",
58 + "type": "string",
59 + "sensitive": true
60 + },
61 + "proxy_password": {
62 + "title": "Proxy password",
63 + "description": "The password for proxy authentication.",
64 + "type": "string",
65 + "sensitive": true
66 + },
67 + "headers": {
68 + "title": "Headers",
69 + "description": "Additional HTTP headers to include in the request.",
70 + "type": [
71 + "object",
72 + "null"
73 + ],
74 + "additionalProperties": {
75 + "type": "string"
76 + }
77 + },
78 + "tls_skip_verify": {
79 + "title": "Skip TLS verification",
80 + "description": "If set, TLS certificate verification will be skipped.",
81 + "type": "boolean"
82 + },
83 + "tls_ca": {
84 + "title": "TLS CA",
85 + "description": "The path to the CA certificate file for TLS verification.",
86 + "type": "string",
87 + "pattern": "^$|^/"
88 + },
89 + "tls_cert": {
90 + "title": "TLS certificate",
91 + "description": "The path to the client certificate file for TLS authentication.",
92 + "type": "string",
93 + "pattern": "^$|^/"
94 + },
95 + "tls_key": {
96 + "title": "TLS key",
97 + "description": "The path to the client key file for TLS authentication.",
98 + "type": "string",
99 + "pattern": "^$|^/"
100 + },
101 + "body": {
102 + "title": "Body",
103 + "type": "string"
104 + },
105 + "method": {
106 + "title": "Method",
107 + "type": "string"
108 + }
109 + },
110 + "required": [
111 + "url"
112 + ],
113 + "patternProperties": {
114 + "^name$": {}
115 + }
116 + },
117 + "uiSchema": {
118 + "uiOptions": {
119 + "fullPage": true
120 + },
121 + "body": {
122 + "ui:widget": "hidden"
123 + },
124 + "method": {
125 + "ui:widget": "hidden"
126 + },
127 + "vnode": {
128 + "ui:placeholder": "To use this option, first create a Virtual Node and then reference its name here."
129 + },
130 + "timeout": {
131 + "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)."
132 + },
133 + "username": {
134 + "ui:widget": "password"
135 + },
136 + "proxy_username": {
137 + "ui:widget": "password"
138 + },
139 + "password": {
140 + "ui:widget": "password"
141 + },
142 + "proxy_password": {
143 + "ui:widget": "password"
144 + },
145 + "ui:flavour": "tabs",
146 + "ui:options": {
147 + "tabs": [
148 + {
149 + "title": "Base",
150 + "fields": [
151 + "update_every",
152 + "url",
153 + "timeout",
154 + "not_follow_redirects",
155 + "vnode"
156 + ]
157 + },
158 + {
159 + "title": "Auth",
160 + "fields": [
161 + "username",
162 + "password"
163 + ]
164 + },
165 + {
166 + "title": "TLS",
167 + "fields": [
168 + "tls_skip_verify",
169 + "tls_ca",
170 + "tls_cert",
171 + "tls_key"
172 + ]
173 + },
174 + {
175 + "title": "Proxy",
176 + "fields": [
177 + "proxy_url",
178 + "proxy_username",
179 + "proxy_password"
180 + ]
181 + },
182 + {
183 + "title": "Headers",
184 + "fields": [
185 + "headers"
186 + ]
187 + }
188 + ]
189 + }
190 + }
191 +}
src/go/plugin/go.d/collector/nats/metadata.yml new
+248
@@ -0,0 +1,248 @@
1 +plugin_name: go.d.plugin
2 +modules:
3 + - meta:
4 + id: collector-go.d.plugin-nats
5 + plugin_name: go.d.plugin
6 + module_name: nats
7 + monitored_instance:
8 + name: NATS
9 + link: https://nats.io/
10 + categories:
11 + - data-collection.message-brokers
12 + icon_filename: nats.svg
13 + related_resources:
14 + integrations:
15 + list: []
16 + alternative_monitored_instances: []
17 + info_provided_to_referring_integrations:
18 + description: ""
19 + keywords:
20 + - nats
21 + - messaging
22 + - broker
23 + most_popular: false
24 + overview:
25 + data_collection:
26 + metrics_description: |
27 + This collector monitors the activity and performance of NATS servers.
28 + method_description: |
29 + It sends HTTP requests to the NATS HTTP server's dedicated [monitoring port](https://docs.nats.io/running-a-nats-service/nats_admin/monitoring#monitoring-nats).
30 + default_behavior:
31 + auto_detection:
32 + description: |
33 + The collector can automatically detect NATS instances running on:
34 +
35 + - localhost that are listening on port 8222
36 + - within Docker containers
37 + limits:
38 + description: ""
39 + performance_impact:
40 + description: ""
41 + additional_permissions:
42 + description: ""
43 + multi_instance: true
44 + supported_platforms:
45 + include: []
46 + exclude: []
47 + setup:
48 + prerequisites:
49 + list:
50 + - title: Enable NATS monitoring
51 + description: |
52 + See [Enable monitoring](https://docs.nats.io/running-a-nats-service/nats_admin/monitoring#enabling-monitoring).
53 + configuration:
54 + file:
55 + name: go.d/nats.conf
56 + options:
57 + description: |
58 + The following options can be defined globally: update_every, autodetection_retry.
59 + folding:
60 + title: Config options
61 + enabled: true
62 + list:
63 + - name: update_every
64 + description: Data collection frequency.
65 + default_value: 1
66 + required: false
67 + - name: autodetection_retry
68 + description: Recheck interval in seconds. Zero means no recheck will be scheduled.
69 + default_value: 0
70 + required: false
71 + - name: url
72 + description: Server URL.
73 + default_value: http://127.0.0.1:8222
74 + required: true
75 + - name: timeout
76 + description: HTTP request timeout.
77 + default_value: 1
78 + required: false
79 + - name: username
80 + description: Username for basic HTTP authentication.
81 + default_value: ""
82 + required: false
83 + - name: password
84 + description: Password for basic HTTP authentication.
85 + default_value: ""
86 + required: false
87 + - name: proxy_url
88 + description: Proxy URL.
89 + default_value: ""
90 + required: false
91 + - name: proxy_username
92 + description: Username for proxy basic HTTP authentication.
93 + default_value: ""
94 + required: false
95 + - name: proxy_password
96 + description: Password for proxy basic HTTP authentication.
97 + default_value: ""
98 + required: false
99 + - name: method
100 + description: HTTP request method.
101 + default_value: GET
102 + required: false
103 + - name: body
104 + description: HTTP request body.
105 + default_value: ""
106 + required: false
107 + - name: headers
108 + description: HTTP request headers.
109 + default_value: ""
110 + required: false
111 + - name: not_follow_redirects
112 + description: Redirect handling policy. Controls whether the client follows redirects.
113 + default_value: false
114 + required: false
115 + - name: tls_skip_verify
116 + description: Server certificate chain and hostname validation policy. Controls whether the client performs this check.
117 + default_value: false
118 + required: false
119 + - name: tls_ca
120 + description: Certification authority that the client uses when verifying the server's certificates.
121 + default_value: ""
122 + required: false
123 + - name: tls_cert
124 + description: Client TLS certificate.
125 + default_value: ""
126 + required: false
127 + - name: tls_key
128 + description: Client TLS key.
129 + default_value: ""
130 + required: false
131 + examples:
132 + folding:
133 + title: Config
134 + enabled: true
135 + list:
136 + - name: Basic
137 + description: A basic example configuration.
138 + folding:
139 + enabled: false
140 + config: |
141 + jobs:
142 + - name: local
143 + url: http://127.0.0.1:8222
144 + - name: HTTP authentication
145 + description: Basic HTTP authentication.
146 + config: |
147 + jobs:
148 + - name: local
149 + url: http://127.0.0.1:8222
150 + username: username
151 + password: password
152 + - name: HTTPS with self-signed certificate
153 + description: NATS with enabled HTTPS and self-signed certificate.
154 + config: |
155 + jobs:
156 + - name: local
157 + url: http://127.0.0.1:8222
158 + tls_skip_verify: yes
159 + - name: Multi-instance
160 + description: |
161 + > **Note**: When you define multiple jobs, their names must be unique.
162 +
163 + Collecting metrics from local and remote instances.
164 + config: |
165 + jobs:
166 + - name: local
167 + url: http://127.0.0.1:8222
168 +
169 + - name: remote
170 + url: http://192.0.2.1:8222
171 + troubleshooting:
172 + problems:
173 + list: []
174 + alerts: []
175 + metrics:
176 + folding:
177 + title: Metrics
178 + enabled: false
179 + description: ""
180 + availability: []
181 + scopes:
182 + - name: server
183 + description: These metrics refer to NATS servers.
184 + labels: []
185 + metrics:
186 + - name: nats.server_traffic
187 + description: Server Traffic
188 + unit: bytes/s
189 + chart_type: area
190 + dimensions:
191 + - name: in
192 + - name: out
193 + - name: nats.server_messages
194 + description: Server Messages
195 + unit: messages/s
196 + chart_type: line
197 + dimensions:
198 + - name: in
199 + - name: out
200 + - name: nats.server_connections_current
201 + description: Server Current Connections
202 + unit: connections
203 + chart_type: line
204 + dimensions:
205 + - name: active
206 + - name: nats.server_connections_rate
207 + description: Server Connections
208 + unit: connections/s
209 + chart_type: line
210 + dimensions:
211 + - name: connections
212 + - name: nats.server_health_probe_status
213 + description: Server Health Probe Status
214 + unit: status
215 + chart_type: line
216 + dimensions:
217 + - name: ok
218 + - name: error
219 + - name: nats.server_cpu_usage
220 + description: Server CPU Usage
221 + unit: percent
222 + chart_type: area
223 + dimensions:
224 + - name: used
225 + - name: nats.server_mem_usage
226 + description: Server Memory Usage
227 + unit: bytes
228 + chart_type: area
229 + dimensions:
230 + - name: used
231 + - name: nats.server_uptime
232 + description: Server Uptime
233 + unit: seconds
234 + chart_type: line
235 + dimensions:
236 + - name: uptime
237 + - name: http endpoint
238 + description: These metrics refer to HTTP endpoints.
239 + labels:
240 + - name: http_endpoint
241 + description: "HTTP endpoint path."
242 + metrics:
243 + - name: nats.http_endpoint_requests
244 + description: HTTP Endpoint Requests
245 + unit: requests/s
246 + chart_type: line
247 + dimensions:
248 + - name: requests
src/go/plugin/go.d/collector/nats/restapi.go new
+76
@@ -0,0 +1,76 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package nats
4 +
5 +import (
6 + "time"
7 +)
8 +
9 +// https://docs.nats.io/running-a-nats-service/nats_admin/monitoring
10 +
11 +// https://github.com/nats-io/nats-server/blob/v2.10.24/server/monitor.go#L1164
12 +type varzResponse struct {
13 + ID string `json:"server_id"`
14 + Name string `json:"server_name"`
15 + Version string `json:"version"`
16 + Proto int `json:"proto"`
17 + Host string `json:"host"`
18 + Port int `json:"port"`
19 + IP string `json:"ip,omitempty"`
20 + MaxConn int `json:"max_connections"`
21 + MaxSubs int `json:"max_subscriptions,omitempty"`
22 + PingInterval time.Duration `json:"ping_interval"`
23 + MaxPingsOut int `json:"ping_max"`
24 + HTTPHost string `json:"http_host"`
25 + HTTPPort int `json:"http_port"`
26 + HTTPBasePath string `json:"http_base_path"`
27 + HTTPSPort int `json:"https_port"`
28 + AuthTimeout float64 `json:"auth_timeout"`
29 + MaxControlLine int32 `json:"max_control_line"`
30 + MaxPayload int `json:"max_payload"`
31 + MaxPending int64 `json:"max_pending"`
32 + TLSTimeout float64 `json:"tls_timeout"`
33 + WriteDeadline time.Duration `json:"write_deadline"`
34 + Start time.Time `json:"start"`
35 + Now time.Time `json:"now"`
36 + Uptime string `json:"uptime"`
37 + Mem int64 `json:"mem"`
38 + Cores int `json:"cores"`
39 + MaxProcs int `json:"gomaxprocs"`
40 + CPU float64 `json:"cpu"`
41 + Connections int `json:"connections"`
42 + TotalConnections uint64 `json:"total_connections"`
43 + Routes int `json:"routes"`
44 + Remotes int `json:"remotes"`
45 + Leafs int `json:"leafnodes"`
46 + InMsgs int64 `json:"in_msgs"`
47 + OutMsgs int64 `json:"out_msgs"`
48 + InBytes int64 `json:"in_bytes"`
49 + OutBytes int64 `json:"out_bytes"`
50 + SlowConsumers int64 `json:"slow_consumers"`
51 + Subscriptions uint32 `json:"subscriptions"`
52 + HTTPReqStats map[string]uint64 `json:"http_req_stats"`
53 +}
54 +
55 +// //https://github.com/nats-io/nats-server/blob/v2.10.24/server/server.go#L2851
56 +var httpEndpoints = []string{
57 + "/",
58 + "/varz",
59 + "/connz",
60 + "/routez",
61 + "/gatewayz",
62 + "/leafz",
63 + "/subsz",
64 + "/stacksz",
65 + "/accountz",
66 + "/accstatz",
67 + "/jsz",
68 + "/healthz",
69 + "/ipqueuesz",
70 + "/raftz",
71 +}
72 +
73 +// https://github.com/nats-io/nats-server/blob/v2.10.24/server/monitor.go#L3125
74 +type healthzResponse struct {
75 + Status *string `json:"status"`
76 +}
src/go/plugin/go.d/collector/nats/testdata/config.json new
+22
@@ -0,0 +1,22 @@
1 +{
2 + "vnode": "ok",
3 + "update_every": 123,
4 + "url": "ok",
5 + "body": "ok",
6 + "method": "ok",
7 + "headers": {
8 + "ok": "ok"
9 + },
10 + "username": "ok",
11 + "password": "ok",
12 + "proxy_url": "ok",
13 + "proxy_username": "ok",
14 + "proxy_password": "ok",
15 + "timeout": 123.123,
16 + "not_follow_redirects": true,
17 + "tls_ca": "ok",
18 + "tls_cert": "ok",
19 + "tls_key": "ok",
20 + "tls_skip_verify": true,
21 + "force_http2": true
22 +}
src/go/plugin/go.d/collector/nats/testdata/config.yaml new
+19
@@ -0,0 +1,19 @@
1 +vnode: "ok"
2 +update_every: 123
3 +url: "ok"
4 +body: "ok"
5 +method: "ok"
6 +headers:
7 + ok: "ok"
8 +username: "ok"
9 +password: "ok"
10 +proxy_url: "ok"
11 +proxy_username: "ok"
12 +proxy_password: "ok"
13 +timeout: 123.123
14 +not_follow_redirects: yes
15 +tls_ca: "ok"
16 +tls_cert: "ok"
17 +tls_key: "ok"
18 +tls_skip_verify: yes
19 +force_http2: yes
src/go/plugin/go.d/collector/nats/testdata/v2.10.24/healthz-ok.json new
+3
@@ -0,0 +1,3 @@
1 +{
2 + "status": "ok"
3 +}
src/go/plugin/go.d/collector/nats/testdata/v2.10.24/varz.json new
+75
@@ -0,0 +1,75 @@
1 +{
2 + "server_id": "NASZPQXJ3BIJOGQHV5ZEWGI6EH3YRQPI2Z5GJRA4AZ47TC4PX4OJGY63",
3 + "server_name": "NASZPQXJ3BIJOGQHV5ZEWGI6EH3YRQPI2Z5GJRA4AZ47TC4PX4OJGY63",
4 + "version": "2.10.24",
5 + "proto": 1,
6 + "git_commit": "1d6f7ea",
7 + "go": "go1.23.4",
8 + "host": "0.0.0.0",
9 + "port": 4222,
10 + "max_connections": 65536,
11 + "ping_interval": 120000000000,
12 + "ping_max": 2,
13 + "http_host": "0.0.0.0",
14 + "http_port": 8222,
15 + "http_base_path": "",
16 + "https_port": 0,
17 + "auth_timeout": 2,
18 + "max_control_line": 4096,
19 + "max_payload": 1048576,
20 + "max_pending": 67108864,
21 + "cluster": {
22 + "name": "my_cluster",
23 + "addr": "0.0.0.0",
24 + "cluster_port": 6222,
25 + "auth_timeout": 2,
26 + "tls_timeout": 2,
27 + "pool_size": 3
28 + },
29 + "gateway": {},
30 + "leaf": {},
31 + "mqtt": {},
32 + "websocket": {},
33 + "jetstream": {},
34 + "tls_timeout": 2,
35 + "write_deadline": 10000000000,
36 + "start": "2024-12-19T11:51:48.038140697Z",
37 + "now": "2024-12-19T19:30:21.110744698Z",
38 + "uptime": "7h38m33s",
39 + "mem": 21725184,
40 + "cores": 16,
41 + "gomaxprocs": 16,
42 + "cpu": 0,
43 + "connections": 0,
44 + "total_connections": 0,
45 + "routes": 0,
46 + "remotes": 0,
47 + "leafnodes": 0,
48 + "in_msgs": 0,
49 + "out_msgs": 0,
50 + "in_bytes": 0,
51 + "out_bytes": 0,
52 + "slow_consumers": 0,
53 + "subscriptions": 57,
54 + "http_req_stats": {
55 + "/": 3,
56 + "/accountz": 2,
57 + "/accstatz": 2,
58 + "/connz": 2,
59 + "/gatewayz": 2,
60 + "/healthz": 2017,
61 + "/jsz": 3,
62 + "/leafz": 2,
63 + "/routez": 2,
64 + "/subsz": 1,
65 + "/varz": 3750
66 + },
67 + "config_load_time": "2024-12-19T11:51:48.038140697Z",
68 + "system_account": "$SYS",
69 + "slow_consumer_stats": {
70 + "clients": 0,
71 + "routes": 0,
72 + "gateways": 0,
73 + "leafs": 0
74 + }
75 +}
src/go/plugin/go.d/config/go.d.conf
+1
@@ -70,6 +70,7 @@ modules:
70 # mongodb: yes
71 # monit: yes
72 # mysql: yes
73 +# nats: yes
74 # nginx: yes
75 # nginxplus: yes
76 # nginxunit: yes
src/go/plugin/go.d/config/go.d/nats.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/collector/nats#readme
3 +
4 +#jobs:
5 +# - name: local
6 +# url: http://127.0.0.1:8222
src/go/plugin/go.d/config/go.d/sd/docker.conf
+7
@@ -58,6 +58,8 @@ classify:
58 expr: '{{ or (eq .PrivatePort "27017") (match "sp" .Image "mongo mongo:* */mongodb */mongodb:* */mongodb-community-server */mongodb-community-server:*") }}'
59 - tags: "mysql"
60 expr: '{{ or (eq .PrivatePort "3306") (match "sp" .Image "mysql mysql:* */mysql */mysql:* mariadb mariadb:* */mariadb */mariadb:* percona percona:* */percona-mysql */percona-mysql:*") }}'
61 + - tags: "nats"
62 + expr: '{{ and (eq .PrivatePort "8222") (match "sp" .Image "nats nats:*") }}'
63 - tags: "nginx"
64 expr: '{{ match "sp" .Image "nginx nginx:*" }}'
65 - tags: "nginxunit"
@@ -197,6 +199,11 @@ compose:
199 module: mysql
200 name: docker_{{.Name}}
201 dsn: netdata@tcp({{.Address}})/
202 + - selector: "nats"
203 + template: |
204 + - module: nats
205 + name: docker_{{.Name}}
206 + url: http://{{.Address}}
207 - selector: "nginx"
208 template: |
209 - module: nginx
src/go/plugin/go.d/config/go.d/sd/net_listeners.conf
+7
@@ -92,6 +92,8 @@ classify:
92 expr: '{{ or (eq .Port "2812") (eq .Comm "monit") }}'
93 - tags: "mysql"
94 expr: '{{ or (eq .Port "3306") (eq .Comm "mysqld" "mariadbd") }}'
95 + - tags: "nats"
96 + expr: '{{ and (eq .Port "8222") (eq .Comm "nats-server") }}'
97 - tags: "nginx"
98 expr: '{{ and (eq .Port "80" "8080") (eq .Comm "nginx") }}'
99 - tags: "nginxunit"
@@ -393,6 +395,11 @@ compose:
395 - module: mysql
396 name: local
397 dsn: netdata@tcp({{.Address}})/
398 + - selector: "nats"
399 + template: |
400 + - module: nats
401 + name: local
402 + url: http://{{.Address}}
403 - selector: "nginx"
404 template: |
405 - module: nginx