@cryptotaxi247 / netdata-1 / commits / 80b50196a

add go.d/exim (#18306)

Ilya Mashchenko committed Aug 11, 2024 at 21:05 UTC 80b50196ab1ee94bbdb15ab73ddc20c1a9b4b0cc
17 files changed +612 -2
src/collectors/python.d.plugin/python.d.conf
+1 -1
@@ -33,7 +33,6 @@ gc_interval: 300
33 # dovecot: yes
34 # this is just an example
35 example: no
36 -# exim: yes
36 go_expvar: no
37 # haproxy: yes
38 # monit: yes
@@ -59,6 +58,7 @@ adaptec_raid: no # Removed (replaced with go.d/adaptercraid).
58 apache: no # Removed (replaced with go.d/apache).
59 beanstalk: no # Removed (replaced with go.d/beanstalk).
60 elasticsearch: no # Removed (replaced with go.d/elasticsearch).
61 +exim: no # Removed (replaced with go.d/exim).
62 fail2ban: no # Removed (replaced with go.d/fail2ban).
63 freeradius: no # Removed (replaced with go.d/freeradius).
64 gearman: no # Removed (replaced with go.d/gearman).
src/go/plugin/go.d/README.md
+2
@@ -74,6 +74,7 @@ see the appropriate collector readme.
74 | [elasticsearch](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/elasticsearch) | Elasticsearch/OpenSearch |
75 | [envoy](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/envoy) | Envoy |
76 | [example](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/example) | - |
77 +| [exim](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/exim) | Exim |
78 | [fail2ban](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/fail2ban) | Fail2Ban Jails |
79 | [filecheck](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/filecheck) | Files and Directories |
80 | [fluentd](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/fluentd) | Fluentd |
@@ -103,6 +104,7 @@ see the appropriate collector readme.
104 | [nginx](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/nginx) | NGINX |
105 | [nginxplus](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/nginxplus) | NGINX Plus |
106 | [nginxvts](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/nginxvts) | NGINX VTS |
107 +| [nsd](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/nsd) | NSD (NLnet Labs) |
108 | [ntpd](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/ntpd) | NTP daemon |
109 | [nvme](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/nvme) | NVMe devices |
110 | [openvpn](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/openvpn) | OpenVPN |
src/go/plugin/go.d/config/go.d.conf
+2
@@ -39,6 +39,7 @@ modules:
39 # elasticsearch: yes
40 # envoy: yes
41 # example: no
42 +# exim: yes
43 # fail2ban: yes
44 # filecheck: yes
45 # fluentd: yes
@@ -67,6 +68,7 @@ modules:
68 # nginx: yes
69 # nginxplus: yes
70 # nginxvts: yes
71 +# nsd: yes
72 # ntpd: yes
73 # nvme: yes
74 # nvidia_smi: no
src/go/plugin/go.d/config/go.d/exim.conf new
+5
@@ -0,0 +1,5 @@
1 +## All available configuration options, their descriptions and default values:
2 +## https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/exim#readme
3 +
4 +jobs:
5 + - name: exim
src/go/plugin/go.d/config/go.d/nsd.conf new
+5
@@ -0,0 +1,5 @@
1 +## All available configuration options, their descriptions and default values:
2 +## https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/nsd#readme
3 +
4 +jobs:
5 + - name: nsd
src/go/plugin/go.d/modules/exim/charts.go new
+27
@@ -0,0 +1,27 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package exim
4 +
5 +import (
6 + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
7 +)
8 +
9 +const (
10 + prioQueueEmailsCount = module.Priority + iota
11 +)
12 +
13 +var charts = module.Charts{
14 + queueEmailsCountChart.Copy(),
15 +}
16 +
17 +var queueEmailsCountChart = module.Chart{
18 + ID: "qemails",
19 + Title: "Exim Queue Emails",
20 + Units: "emails",
21 + Fam: "queue",
22 + Ctx: "exim.qemails",
23 + Priority: prioQueueEmailsCount,
24 + Dims: module.Dims{
25 + {ID: "emails"},
26 + },
27 +}
src/go/plugin/go.d/modules/exim/collect.go new
+43
@@ -0,0 +1,43 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package exim
4 +
5 +import (
6 + "bufio"
7 + "bytes"
8 + "fmt"
9 + "strconv"
10 + "strings"
11 +)
12 +
13 +func (e *Exim) collect() (map[string]int64, error) {
14 + resp, err := e.exec.countMessagesInQueue()
15 + if err != nil {
16 + return nil, err
17 + }
18 +
19 + emails, err := parseResponse(resp)
20 + if err != nil {
21 + return nil, err
22 + }
23 +
24 + mx := map[string]int64{
25 + "emails": emails,
26 + }
27 +
28 + return mx, nil
29 +}
30 +
31 +func parseResponse(resp []byte) (int64, error) {
32 + sc := bufio.NewScanner(bytes.NewReader(resp))
33 + sc.Scan()
34 +
35 + line := strings.TrimSpace(sc.Text())
36 +
37 + emails, err := strconv.ParseInt(line, 10, 64)
38 + if err != nil {
39 + return 0, fmt.Errorf("invalid response '%s': %v", line, err)
40 + }
41 +
42 + return emails, nil
43 +}
src/go/plugin/go.d/modules/exim/config_schema.json new
+35
@@ -0,0 +1,35 @@
1 +{
2 + "jsonSchema": {
3 + "$schema": "http://json-schema.org/draft-07/schema#",
4 + "title": "Exim collector configuration.",
5 + "type": "object",
6 + "properties": {
7 + "update_every": {
8 + "title": "Update every",
9 + "description": "Data collection interval, measured in seconds.",
10 + "type": "integer",
11 + "minimum": 1,
12 + "default": 10
13 + },
14 + "timeout": {
15 + "title": "Timeout",
16 + "description": "Timeout for executing the binary, specified in seconds.",
17 + "type": "number",
18 + "minimum": 0.5,
19 + "default": 2
20 + }
21 + },
22 + "additionalProperties": false,
23 + "patternProperties": {
24 + "^name$": {}
25 + }
26 + },
27 + "uiSchema": {
28 + "uiOptions": {
29 + "fullPage": true
30 + },
31 + "timeout": {
32 + "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)."
33 + }
34 + }
35 +}
src/go/plugin/go.d/modules/exim/exec.go new
+47
@@ -0,0 +1,47 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package exim
4 +
5 +import (
6 + "context"
7 + "fmt"
8 + "os/exec"
9 + "time"
10 +
11 + "github.com/netdata/netdata/go/plugins/logger"
12 +)
13 +
14 +type eximBinary interface {
15 + countMessagesInQueue() ([]byte, error)
16 +}
17 +
18 +func newEximExec(ndsudoPath string, timeout time.Duration, log *logger.Logger) *eximExec {
19 + return &eximExec{
20 + Logger: log,
21 + ndsudoPath: ndsudoPath,
22 + timeout: timeout,
23 + }
24 +}
25 +
26 +type eximExec struct {
27 + *logger.Logger
28 +
29 + ndsudoPath string
30 + timeout time.Duration
31 +}
32 +
33 +func (e *eximExec) countMessagesInQueue() ([]byte, error) {
34 + ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
35 + defer cancel()
36 +
37 + cmd := exec.CommandContext(ctx, e.ndsudoPath, "exim-bpc")
38 +
39 + e.Debugf("executing '%s'", cmd)
40 +
41 + bs, err := cmd.Output()
42 + if err != nil {
43 + return nil, fmt.Errorf("error on '%s': %v", cmd, err)
44 + }
45 +
46 + return bs, nil
47 +}
src/go/plugin/go.d/modules/exim/exim.go new
+97
@@ -0,0 +1,97 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package exim
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("exim", module.Creator{
19 + JobConfigSchema: configSchema,
20 + Defaults: module.Defaults{
21 + UpdateEvery: 10,
22 + },
23 + Create: func() module.Module { return New() },
24 + Config: func() any { return &Config{} },
25 + })
26 +}
27 +
28 +func New() *Exim {
29 + return &Exim{
30 + Config: Config{
31 + Timeout: web.Duration(time.Second * 2),
32 + },
33 + charts: charts.Copy(),
34 + }
35 +}
36 +
37 +type Config struct {
38 + UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
39 + Timeout web.Duration `yaml:"timeout,omitempty" json:"timeout"`
40 +}
41 +
42 +type Exim struct {
43 + module.Base
44 + Config `yaml:",inline" json:""`
45 +
46 + charts *module.Charts
47 +
48 + exec eximBinary
49 +}
50 +
51 +func (e *Exim) Configuration() any {
52 + return e.Config
53 +}
54 +
55 +func (e *Exim) Init() error {
56 + exim, err := e.initEximExec()
57 + if err != nil {
58 + e.Errorf("exim exec initialization: %v", err)
59 + return err
60 + }
61 + e.exec = exim
62 +
63 + return nil
64 +}
65 +
66 +func (e *Exim) Check() error {
67 + mx, err := e.collect()
68 + if err != nil {
69 + e.Error(err)
70 + return err
71 + }
72 +
73 + if len(mx) == 0 {
74 + return errors.New("no metrics collected")
75 + }
76 +
77 + return nil
78 +}
79 +
80 +func (e *Exim) Charts() *module.Charts {
81 + return e.charts
82 +}
83 +
84 +func (e *Exim) Collect() map[string]int64 {
85 + mx, err := e.collect()
86 + if err != nil {
87 + e.Error(err)
88 + }
89 +
90 + if len(mx) == 0 {
91 + return nil
92 + }
93 +
94 + return mx
95 +}
96 +
97 +func (e *Exim) Cleanup() {}
src/go/plugin/go.d/modules/exim/exim_test.go new
+217
@@ -0,0 +1,217 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package exim
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 +
21 +func Test_testDataIsValid(t *testing.T) {
22 + for name, data := range map[string][]byte{
23 + "dataConfigJSON": dataConfigJSON,
24 + "dataConfigYAML": dataConfigYAML,
25 + } {
26 + require.NotNil(t, data, name)
27 +
28 + }
29 +}
30 +
31 +func TestExim_Configuration(t *testing.T) {
32 + module.TestConfigurationSerialize(t, &Exim{}, dataConfigJSON, dataConfigYAML)
33 +}
34 +
35 +func TestExim_Init(t *testing.T) {
36 + tests := map[string]struct {
37 + config Config
38 + wantFail bool
39 + }{
40 + "fails if failed to locate ndsudo": {
41 + wantFail: true,
42 + config: New().Config,
43 + },
44 + }
45 +
46 + for name, test := range tests {
47 + t.Run(name, func(t *testing.T) {
48 + exim := New()
49 + exim.Config = test.config
50 +
51 + if test.wantFail {
52 + assert.Error(t, exim.Init())
53 + } else {
54 + assert.NoError(t, exim.Init())
55 + }
56 + })
57 + }
58 +}
59 +
60 +func TestExim_Cleanup(t *testing.T) {
61 + tests := map[string]struct {
62 + prepare func() *Exim
63 + }{
64 + "not initialized exec": {
65 + prepare: func() *Exim {
66 + return New()
67 + },
68 + },
69 + "after check": {
70 + prepare: func() *Exim {
71 + exim := New()
72 + exim.exec = prepareMockOK()
73 + _ = exim.Check()
74 + return exim
75 + },
76 + },
77 + "after collect": {
78 + prepare: func() *Exim {
79 + exim := New()
80 + exim.exec = prepareMockOK()
81 + _ = exim.Collect()
82 + return exim
83 + },
84 + },
85 + }
86 +
87 + for name, test := range tests {
88 + t.Run(name, func(t *testing.T) {
89 + exim := test.prepare()
90 +
91 + assert.NotPanics(t, exim.Cleanup)
92 + })
93 + }
94 +}
95 +
96 +func TestEximCharts(t *testing.T) {
97 + assert.NotNil(t, New().Charts())
98 +}
99 +
100 +func TestExim_Check(t *testing.T) {
101 + tests := map[string]struct {
102 + prepareMock func() *mockEximExec
103 + wantFail bool
104 + }{
105 + "success case": {
106 + prepareMock: prepareMockOK,
107 + wantFail: false,
108 + },
109 + "error on exec": {
110 + prepareMock: prepareMockErr,
111 + wantFail: true,
112 + },
113 + "empty response": {
114 + prepareMock: prepareMockEmptyResponse,
115 + wantFail: true,
116 + },
117 + "unexpected response": {
118 + prepareMock: prepareMockUnexpectedResponse,
119 + wantFail: true,
120 + },
121 + }
122 +
123 + for name, test := range tests {
124 + t.Run(name, func(t *testing.T) {
125 + exim := New()
126 + mock := test.prepareMock()
127 + exim.exec = mock
128 +
129 + if test.wantFail {
130 + assert.Error(t, exim.Check())
131 + } else {
132 + assert.NoError(t, exim.Check())
133 + }
134 + })
135 + }
136 +}
137 +
138 +func TestExim_Collect(t *testing.T) {
139 + tests := map[string]struct {
140 + prepareMock func() *mockEximExec
141 + wantMetrics map[string]int64
142 + }{
143 + "success case": {
144 + prepareMock: prepareMockOK,
145 + wantMetrics: map[string]int64{
146 + "emails": 99,
147 + },
148 + },
149 + "error on exec": {
150 + prepareMock: prepareMockErr,
151 + wantMetrics: nil,
152 + },
153 + "empty response": {
154 + prepareMock: prepareMockEmptyResponse,
155 + wantMetrics: nil,
156 + },
157 + "unexpected response": {
158 + prepareMock: prepareMockUnexpectedResponse,
159 + wantMetrics: nil,
160 + },
161 + }
162 +
163 + for name, test := range tests {
164 + t.Run(name, func(t *testing.T) {
165 + exim := New()
166 + mock := test.prepareMock()
167 + exim.exec = mock
168 +
169 + mx := exim.Collect()
170 +
171 + assert.Equal(t, test.wantMetrics, mx)
172 +
173 + if len(test.wantMetrics) > 0 {
174 + assert.Len(t, *exim.Charts(), len(charts))
175 + module.TestMetricsHasAllChartsDims(t, exim.Charts(), mx)
176 + }
177 + })
178 + }
179 +}
180 +
181 +func prepareMockOK() *mockEximExec {
182 + return &mockEximExec{
183 + data: []byte("99"),
184 + }
185 +}
186 +
187 +func prepareMockErr() *mockEximExec {
188 + return &mockEximExec{
189 + err: true,
190 + }
191 +}
192 +
193 +func prepareMockEmptyResponse() *mockEximExec {
194 + return &mockEximExec{}
195 +}
196 +
197 +func prepareMockUnexpectedResponse() *mockEximExec {
198 + return &mockEximExec{
199 + data: []byte(`
200 +Lorem ipsum dolor sit amet, consectetur adipiscing elit.
201 +Nulla malesuada erat id magna mattis, eu viverra tellus rhoncus.
202 +Fusce et felis pulvinar, posuere sem non, porttitor eros.
203 +`),
204 + }
205 +}
206 +
207 +type mockEximExec struct {
208 + err bool
209 + data []byte
210 +}
211 +
212 +func (m *mockEximExec) countMessagesInQueue() ([]byte, error) {
213 + if m.err {
214 + return nil, errors.New("mock.countMessagesInQueue() error")
215 + }
216 + return m.data, nil
217 +}
src/go/plugin/go.d/modules/exim/init.go new
+23
@@ -0,0 +1,23 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package exim
4 +
5 +import (
6 + "fmt"
7 + "os"
8 + "path/filepath"
9 +
10 + "github.com/netdata/netdata/go/plugins/pkg/executable"
11 +)
12 +
13 +func (e *Exim) initEximExec() (eximBinary, error) {
14 + ndsudoPath := filepath.Join(executable.Directory, "ndsudo")
15 + if _, err := os.Stat(ndsudoPath); err != nil {
16 + return nil, fmt.Errorf("ndsudo executable not found: %v", err)
17 +
18 + }
19 +
20 + exim := newEximExec(ndsudoPath, e.Timeout.Duration(), e.Logger)
21 +
22 + return exim, nil
23 +}
src/go/plugin/go.d/modules/exim/metadata.yaml new
+100
@@ -0,0 +1,100 @@
1 +plugin_name: go.d.plugin
2 +modules:
3 + - meta:
4 + id: collector-go.d.plugin-exim
5 + plugin_name: go.d.plugin
6 + module_name: exim
7 + monitored_instance:
8 + name: Exim
9 + link: "https://www.exim.org/"
10 + icon_filename: 'exim.jpg'
11 + categories:
12 + - data-collection.mail-servers
13 + keywords:
14 + - exim
15 + - mail
16 + - email
17 + related_resources:
18 + integrations:
19 + list: []
20 + info_provided_to_referring_integrations:
21 + description: ""
22 + most_popular: false
23 + overview:
24 + data_collection:
25 + metrics_description: >
26 + This collector monitors Exim mail queue.
27 + It relies on the [`exim`](https://www.exim.org/exim-html-3.20/doc/html/spec_5.html) CLI tool but avoids directly executing the binary.
28 + Instead, it utilizes `ndsudo`, a Netdata helper specifically designed to run privileged commands securely within the Netdata environment.
29 + This approach eliminates the need to use `sudo`, improving security and potentially simplifying permission management.
30 +
31 + Executed commands:
32 +
33 + - `exim -bpc`
34 + method_description: ""
35 + supported_platforms:
36 + include: []
37 + exclude: []
38 + multi_instance: false
39 + additional_permissions:
40 + description: ""
41 + default_behavior:
42 + auto_detection:
43 + description: ""
44 + limits:
45 + description: ""
46 + performance_impact:
47 + description: ""
48 + setup:
49 + prerequisites:
50 + list: []
51 + configuration:
52 + file:
53 + name: go.d/exim.conf
54 + options:
55 + description: |
56 + The following options can be defined globally: update_every.
57 + folding:
58 + title: Config options
59 + enabled: true
60 + list:
61 + - name: update_every
62 + description: Data collection frequency.
63 + default_value: 10
64 + required: false
65 + - name: timeout
66 + description: exim binary execution timeout.
67 + default_value: 2
68 + required: false
69 + examples:
70 + folding:
71 + title: Config
72 + enabled: true
73 + list:
74 + - name: Custom update_every
75 + description: Allows you to override the default data collection interval.
76 + config: |
77 + jobs:
78 + - name: exim
79 + update_every: 5 # Collect logical volume statistics every 5 seconds
80 + troubleshooting:
81 + problems:
82 + list: []
83 + alerts: []
84 + metrics:
85 + folding:
86 + title: Metrics
87 + enabled: false
88 + description: ""
89 + availability: []
90 + scopes:
91 + - name: global
92 + description: These metrics refer to the the entire monitored application.
93 + labels: []
94 + metrics:
95 + - name: exim.qemails
96 + description: Exim Queue Emails
97 + unit: 'emails'
98 + chart_type: line
99 + dimensions:
100 + - name: emails
src/go/plugin/go.d/modules/exim/testdata/config.json new
+4
@@ -0,0 +1,4 @@
1 +{
2 + "update_every": 123,
3 + "timeout": 123.123
4 +}
src/go/plugin/go.d/modules/exim/testdata/config.yaml new
+2
@@ -0,0 +1,2 @@
1 +update_every: 123
2 +timeout: 123.123
src/go/plugin/go.d/modules/init.go
+1
@@ -28,6 +28,7 @@ import (
28 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/elasticsearch"
29 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/envoy"
30 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/example"
31 + _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/exim"
32 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/fail2ban"
33 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/filecheck"
34 _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/fluentd"
src/go/plugin/go.d/modules/nsd/nsd.go
+1 -1
@@ -55,7 +55,7 @@ func (n *Nsd) Configuration() any {
55 func (n *Nsd) Init() error {
56 nsdControl, err := n.initNsdControlExec()
57 if err != nil {
58 - n.Errorf("nvm-control exec initialization: %v", err)
58 + n.Errorf("nsd-control exec initialization: %v", err)
59 return err
60 }
61 n.exec = nsdControl