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