add go.d dovecot (#18321)
* add go.d dovecot * rm trailing spaces
Ilya Mashchenko committed
Aug 12, 2024 at 23:24 UTC
c3ec6fae56ef241d535b7d2f18f3918da2856bbb
18 files changed
+986
-2
src/collectors/python.d.plugin/python.d.conf
+1
-1
@@ -30,7 +30,6 @@ gc_interval: 300
30
# boinc: yes
31
# ceph: yes
32
# changefinder: no
33
-# dovecot: yes
33
# this is just an example
34
example: no
35
go_expvar: no
@@ -56,6 +55,7 @@ go_expvar: no
55
adaptec_raid: no # Removed (replaced with go.d/adaptercraid).
56
apache: no # Removed (replaced with go.d/apache).
57
beanstalk: no # Removed (replaced with go.d/beanstalk).
58
+dovecot: no # Removed (replaced with go.d/dovecot).
59
elasticsearch: no # Removed (replaced with go.d/elasticsearch).
60
exim: no # Removed (replaced with go.d/exim).
61
fail2ban: no # Removed (replaced with go.d/fail2ban).
src/go/plugin/go.d/README.md
+1
@@ -71,6 +71,7 @@ see the appropriate collector readme.
71
| [docker](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/docker) | Docker Engine |
72
| [docker_engine](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/docker_engine) | Docker Engine |
73
| [dockerhub](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/dockerhub) | Docker Hub |
74
+| [dovecot](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/dovecot) | Dovecot |
75
| [elasticsearch](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/elasticsearch) | Elasticsearch/OpenSearch |
76
| [envoy](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/envoy) | Envoy |
77
| [example](https://github.com/netdata/netdata/tree/master/src/go/plugin/go.d/modules/example) | - |
src/go/plugin/go.d/config/go.d.conf
+1
@@ -36,6 +36,7 @@ modules:
36
# docker: yes
37
# docker_engine: yes
38
# dockerhub: yes
39
+# dovecot: yes
40
# elasticsearch: yes
41
# envoy: yes
42
# example: no
src/go/plugin/go.d/config/go.d/dovecot.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/dovecot#readme
3
+
4
+jobs:
5
+ - name: local
6
+ address: unix:///var/run/dovecot/old-stats
src/go/plugin/go.d/config/go.d/sd/docker.conf
+7
@@ -38,6 +38,8 @@ classify:
38
expr: '{{ or (eq .PrivatePort "8091") (match "sp" .Image "couchbase couchbase:*") }}'
39
- tags: "couchdb"
40
expr: '{{ or (eq .PrivatePort "5984") (match "sp" .Image "couchdb couchdb:*") }}'
41
+ - tags: "dovecot"
42
+ expr: '{{ or (eq .PrivatePort "24242") (match "sp" .Image "*/dovecot */dovecot:*") }}'
43
- tags: "elasticsearch"
44
expr: '{{ or (eq .PrivatePort "9200") (match "sp" .Image "elasticsearch elasticsearch:* */elasticsearch */elasticsearch:* */opensearch */opensearch:*") }}'
45
- tags: "gearman"
@@ -124,6 +126,11 @@ compose:
126
module: couchdb
127
name: docker_{{.Name}}
128
url: http://{{.Address}}
129
+ - selector: "dovecot"
130
+ template: |
131
+ module: dovecot
132
+ name: docker_{{.Name}}
133
+ address: {{.Address}}
134
- selector: "elasticsearch"
135
template: |
136
module: elasticsearch
src/go/plugin/go.d/config/go.d/sd/net_listeners.conf
+7
@@ -42,6 +42,8 @@ classify:
42
expr: '{{ and (eq .Protocol "UDP") (eq .Port "53") (eq .Comm "dnsmasq") }}'
43
- tags: "docker_engine"
44
expr: '{{ and (eq .Port "9323") (eq .Comm "dockerd") }}'
45
+ - tags: "dovecot"
46
+ expr: '{{ and (eq .Port "24242") (eq .Comm "dovecot") }}'
47
- tags: "elasticsearch"
48
expr: '{{ or (eq .Port "9200") (glob .Cmdline "*elasticsearch*" "*opensearch*") }}'
49
- tags: "envoy"
@@ -216,6 +218,11 @@ compose:
218
module: docker_engine
219
name: local
220
url: http://{{.Address}}/metrics
221
+ - selector: "dovecot"
222
+ template: |
223
+ module: dovecot
224
+ name: local
225
+ address: {{.Address}}
226
- selector: "elasticsearch"
227
template: |
228
module: elasticsearch
src/go/plugin/go.d/modules/dovecot/charts.go
new
+185
@@ -0,0 +1,185 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package dovecot
4
+
5
+import (
6
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
7
+)
8
+
9
+const (
10
+ prioSessions = module.Priority + iota
11
+ prioLogins
12
+ prioAuthenticationAttempts
13
+ prioCommands
14
+ prioPageFaults
15
+ prioContextSwitches
16
+ prioDiskIO
17
+ prioNetTraffic
18
+ prioSysCalls
19
+ prioLookups
20
+ prioCachePerformance
21
+ prioAuthCachePerformance
22
+)
23
+
24
+var charts = module.Charts{
25
+ sessionsChart.Copy(),
26
+ loginsChart.Copy(),
27
+ authAttemptsChart.Copy(),
28
+ commandsChart.Copy(),
29
+ pageFaultsChart.Copy(),
30
+ contextSwitchesChart.Copy(),
31
+ diskIOChart.Copy(),
32
+ netTrafficChart.Copy(),
33
+ sysCallsChart.Copy(),
34
+ lookupsChart.Copy(),
35
+ cacheChart.Copy(),
36
+ authCacheChart.Copy(),
37
+}
38
+
39
+var (
40
+ sessionsChart = module.Chart{
41
+ ID: "sessions",
42
+ Title: "Dovecot Active Sessions",
43
+ Units: "sessions",
44
+ Fam: "sessions",
45
+ Ctx: "dovecot.sessions",
46
+ Priority: prioSessions,
47
+ Dims: module.Dims{
48
+ {ID: "num_connected_sessions", Name: "active"},
49
+ },
50
+ }
51
+ loginsChart = module.Chart{
52
+ ID: "logins",
53
+ Title: "Dovecot Logins",
54
+ Units: "logins",
55
+ Fam: "logins",
56
+ Ctx: "dovecot.logins",
57
+ Priority: prioLogins,
58
+ Dims: module.Dims{
59
+ {ID: "num_logins", Name: "logins"},
60
+ },
61
+ }
62
+ authAttemptsChart = module.Chart{
63
+ ID: "auth",
64
+ Title: "Dovecot Authentications",
65
+ Units: "attempts/s",
66
+ Fam: "logins",
67
+ Ctx: "dovecot.auth",
68
+ Priority: prioAuthenticationAttempts,
69
+ Type: module.Stacked,
70
+ Dims: module.Dims{
71
+ {ID: "auth_successes", Name: "ok", Algo: module.Incremental},
72
+ {ID: "auth_failures", Name: "failed", Algo: module.Incremental},
73
+ },
74
+ }
75
+ commandsChart = module.Chart{
76
+ ID: "commands",
77
+ Title: "Dovecot Commands",
78
+ Units: "commands",
79
+ Fam: "commands",
80
+ Ctx: "dovecot.commands",
81
+ Priority: prioCommands,
82
+ Dims: module.Dims{
83
+ {ID: "num_cmds", Name: "commands"},
84
+ },
85
+ }
86
+ pageFaultsChart = module.Chart{
87
+ ID: "faults",
88
+ Title: "Dovecot Page Faults",
89
+ Units: "faults/s",
90
+ Fam: "page faults",
91
+ Ctx: "dovecot.faults",
92
+ Priority: prioPageFaults,
93
+ Dims: module.Dims{
94
+ {ID: "min_faults", Name: "minor", Algo: module.Incremental},
95
+ {ID: "maj_faults", Name: "major", Algo: module.Incremental},
96
+ },
97
+ }
98
+ contextSwitchesChart = module.Chart{
99
+ ID: "context_switches",
100
+ Title: "Dovecot Context Switches",
101
+ Units: "switches/s",
102
+ Fam: "context switches",
103
+ Ctx: "dovecot.context_switches",
104
+ Priority: prioContextSwitches,
105
+ Dims: module.Dims{
106
+ {ID: "vol_cs", Name: "voluntary", Algo: module.Incremental},
107
+ {ID: "invol_cs", Name: "involuntary", Algo: module.Incremental},
108
+ },
109
+ }
110
+ diskIOChart = module.Chart{
111
+ ID: "io",
112
+ Title: "Dovecot Disk I/O",
113
+ Units: "KiB/s",
114
+ Fam: "disk",
115
+ Ctx: "dovecot.io",
116
+ Priority: prioDiskIO,
117
+ Type: module.Area,
118
+ Dims: module.Dims{
119
+ {ID: "disk_input", Name: "read", Div: 1024, Algo: module.Incremental},
120
+ {ID: "disk_output", Name: "write", Mul: -1, Div: 1024, Algo: module.Incremental},
121
+ },
122
+ }
123
+ netTrafficChart = module.Chart{
124
+ ID: "net",
125
+ Title: "Dovecot Network Bandwidth",
126
+ Units: "kilobits/s",
127
+ Fam: "network",
128
+ Ctx: "dovecot.net",
129
+ Priority: prioNetTraffic,
130
+ Type: module.Area,
131
+ Dims: module.Dims{
132
+ {ID: "read_bytes", Name: "read", Mul: 8, Div: 1000, Algo: module.Incremental},
133
+ {ID: "write_bytes", Name: "write", Mul: -8, Div: 1000, Algo: module.Incremental},
134
+ },
135
+ }
136
+ sysCallsChart = module.Chart{
137
+ ID: "syscalls",
138
+ Title: "Dovecot Number of SysCalls",
139
+ Units: "syscalls/s",
140
+ Fam: "system",
141
+ Ctx: "dovecot.syscalls",
142
+ Priority: prioSysCalls,
143
+ Dims: module.Dims{
144
+ {ID: "read_count", Name: "read", Algo: module.Incremental},
145
+ {ID: "write_count", Name: "write", Algo: module.Incremental},
146
+ },
147
+ }
148
+ lookupsChart = module.Chart{
149
+ ID: "lookup",
150
+ Title: "Dovecot Lookups",
151
+ Units: "lookups/s",
152
+ Fam: "lookups",
153
+ Ctx: "dovecot.lookup",
154
+ Priority: prioLookups,
155
+ Type: module.Stacked,
156
+ Dims: module.Dims{
157
+ {ID: "mail_lookup_path", Name: "path", Algo: module.Incremental},
158
+ {ID: "mail_lookup_attr", Name: "attr", Algo: module.Incremental},
159
+ },
160
+ }
161
+ cacheChart = module.Chart{
162
+ ID: "cache",
163
+ Title: "Dovecot Cache Hits",
164
+ Units: "hits/s",
165
+ Fam: "cache",
166
+ Ctx: "dovecot.cache",
167
+ Priority: prioCachePerformance,
168
+ Dims: module.Dims{
169
+ {ID: "mail_cache_hits", Name: "hits", Algo: module.Incremental},
170
+ },
171
+ }
172
+ authCacheChart = module.Chart{
173
+ ID: "auth_cache",
174
+ Title: "Dovecot Authentication Cache",
175
+ Units: "requests/s",
176
+ Fam: "cache",
177
+ Ctx: "dovecot.auth_cache",
178
+ Priority: prioAuthCachePerformance,
179
+ Type: module.Stacked,
180
+ Dims: module.Dims{
181
+ {ID: "auth_cache_hits", Name: "hits", Algo: module.Incremental},
182
+ {ID: "auth_cache_misses", Name: "misses", Algo: module.Incremental},
183
+ },
184
+ }
185
+)
src/go/plugin/go.d/modules/dovecot/client.go
new
+54
@@ -0,0 +1,54 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package dovecot
4
+
5
+import (
6
+ "bytes"
7
+
8
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/socket"
9
+)
10
+
11
+type dovecotConn interface {
12
+ connect() error
13
+ disconnect()
14
+ queryExportGlobal() ([]byte, error)
15
+}
16
+
17
+func newDovecotConn(conf Config) dovecotConn {
18
+ return &dovecotClient{conn: socket.New(socket.Config{
19
+ Address: conf.Address,
20
+ ConnectTimeout: conf.Timeout.Duration(),
21
+ ReadTimeout: conf.Timeout.Duration(),
22
+ WriteTimeout: conf.Timeout.Duration(),
23
+ })}
24
+}
25
+
26
+type dovecotClient struct {
27
+ conn socket.Client
28
+}
29
+
30
+func (c *dovecotClient) connect() error {
31
+ return c.conn.Connect()
32
+}
33
+
34
+func (c *dovecotClient) disconnect() {
35
+ _ = c.conn.Disconnect()
36
+}
37
+
38
+func (c *dovecotClient) queryExportGlobal() ([]byte, error) {
39
+ var b bytes.Buffer
40
+ var n int
41
+
42
+ err := c.conn.Command("EXPORT\tglobal\n", func(bs []byte) bool {
43
+ b.Write(bs)
44
+ b.WriteByte('\n')
45
+
46
+ n++
47
+ return n < 2
48
+ })
49
+ if err != nil {
50
+ return nil, err
51
+ }
52
+
53
+ return b.Bytes(), nil
54
+}
src/go/plugin/go.d/modules/dovecot/collect.go
new
+89
@@ -0,0 +1,89 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package dovecot
4
+
5
+import (
6
+ "bufio"
7
+ "bytes"
8
+ "errors"
9
+ "fmt"
10
+ "strconv"
11
+ "strings"
12
+)
13
+
14
+// FIXME: drop using "old_stats" in favour of "stats" (https://doc.dovecot.org/configuration_manual/stats/openmetrics/).
15
+
16
+func (d *Dovecot) collect() (map[string]int64, error) {
17
+ if d.conn == nil {
18
+ conn, err := d.establishConn()
19
+ if err != nil {
20
+ return nil, err
21
+ }
22
+ d.conn = conn
23
+ }
24
+
25
+ stats, err := d.conn.queryExportGlobal()
26
+ if err != nil {
27
+ d.conn.disconnect()
28
+ d.conn = nil
29
+ return nil, err
30
+ }
31
+
32
+ mx := make(map[string]int64)
33
+
34
+ // https://doc.dovecot.org/configuration_manual/stats/old_statistics/#statistics-gathered
35
+ if err := d.collectExportGlobal(mx, stats); err != nil {
36
+ return nil, err
37
+ }
38
+
39
+ return mx, nil
40
+}
41
+
42
+func (d *Dovecot) collectExportGlobal(mx map[string]int64, resp []byte) error {
43
+ sc := bufio.NewScanner(bytes.NewReader(resp))
44
+
45
+ if !sc.Scan() {
46
+ return errors.New("failed to read fields line from export global response")
47
+ }
48
+ fieldsLine := strings.TrimSpace(sc.Text())
49
+
50
+ if !sc.Scan() {
51
+ return errors.New("failed to read values line from export global response")
52
+ }
53
+ valuesLine := strings.TrimSpace(sc.Text())
54
+
55
+ if fieldsLine == "" || valuesLine == "" {
56
+ return errors.New("empty fields line or values line from export global response")
57
+ }
58
+
59
+ fields := strings.Fields(fieldsLine)
60
+ values := strings.Fields(valuesLine)
61
+
62
+ if len(fields) != len(values) {
63
+ return fmt.Errorf("mismatched fields and values count: fields=%d, values=%d", len(fields), len(values))
64
+ }
65
+
66
+ for i, name := range fields {
67
+ val := values[i]
68
+
69
+ v, err := strconv.ParseInt(val, 10, 64)
70
+ if err != nil {
71
+ d.Debugf("failed to parse export value %s %s: %v", name, val, err)
72
+ continue
73
+ }
74
+
75
+ mx[name] = v
76
+ }
77
+
78
+ return nil
79
+}
80
+
81
+func (d *Dovecot) establishConn() (dovecotConn, error) {
82
+ conn := d.newConn(d.Config)
83
+
84
+ if err := conn.connect(); err != nil {
85
+ return nil, err
86
+ }
87
+
88
+ return conn, nil
89
+}
src/go/plugin/go.d/modules/dovecot/config_schema.json
new
+47
@@ -0,0 +1,47 @@
1
+{
2
+ "jsonSchema": {
3
+ "$schema": "http://json-schema.org/draft-07/schema#",
4
+ "title": "Dovecot 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 Unix or TCP socket address where the Dovecot [old_stats](https://doc.dovecot.org/configuration_manual/stats/old_statistics/#old-statistics) plugin listens for connections.",
17
+ "type": "string",
18
+ "default": "127.0.0.1:24242"
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
+ "address": {
41
+ "ui:help": "Use `unix://{path_to_socket}` for Unix socket or `{ip}:{port}` for TCP socket."
42
+ },
43
+ "timeout": {
44
+ "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)."
45
+ }
46
+ }
47
+}
src/go/plugin/go.d/modules/dovecot/dovecot.go
new
+101
@@ -0,0 +1,101 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package dovecot
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("dovecot", module.Creator{
19
+ JobConfigSchema: configSchema,
20
+ Create: func() module.Module { return New() },
21
+ Config: func() any { return &Config{} },
22
+ })
23
+}
24
+
25
+func New() *Dovecot {
26
+ return &Dovecot{
27
+ Config: Config{
28
+ Address: "127.0.0.1:24242",
29
+ Timeout: web.Duration(time.Second * 1),
30
+ },
31
+ newConn: newDovecotConn,
32
+ charts: charts.Copy(),
33
+ }
34
+}
35
+
36
+type Config struct {
37
+ UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
38
+ Address string `yaml:"address" json:"address"`
39
+ Timeout web.Duration `yaml:"timeout" json:"timeout"`
40
+}
41
+
42
+type Dovecot struct {
43
+ module.Base
44
+ Config `yaml:",inline" json:""`
45
+
46
+ charts *module.Charts
47
+
48
+ newConn func(Config) dovecotConn
49
+ conn dovecotConn
50
+}
51
+
52
+func (d *Dovecot) Configuration() any {
53
+ return d.Config
54
+}
55
+
56
+func (d *Dovecot) Init() error {
57
+ if d.Address == "" {
58
+ d.Error("config: 'address' not set")
59
+ return errors.New("address not set")
60
+ }
61
+
62
+ return nil
63
+}
64
+
65
+func (d *Dovecot) Check() error {
66
+ mx, err := d.collect()
67
+ if err != nil {
68
+ d.Error(err)
69
+ return err
70
+ }
71
+
72
+ if len(mx) == 0 {
73
+ return errors.New("no metrics collected")
74
+ }
75
+
76
+ return nil
77
+}
78
+
79
+func (d *Dovecot) Charts() *module.Charts {
80
+ return d.charts
81
+}
82
+
83
+func (d *Dovecot) Collect() map[string]int64 {
84
+ mx, err := d.collect()
85
+ if err != nil {
86
+ d.Error(err)
87
+ }
88
+
89
+ if len(mx) == 0 {
90
+ return nil
91
+ }
92
+
93
+ return mx
94
+}
95
+
96
+func (d *Dovecot) Cleanup() {
97
+ if d.conn != nil {
98
+ d.conn.disconnect()
99
+ d.conn = nil
100
+ }
101
+}
src/go/plugin/go.d/modules/dovecot/dovecot_test.go
new
+281
@@ -0,0 +1,281 @@
1
+// SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+package dovecot
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
+ dataExportGlobal, _ = os.ReadFile("testdata/export_global.txt")
21
+)
22
+
23
+func Test_testDataIsValid(t *testing.T) {
24
+ for name, data := range map[string][]byte{
25
+ "dataConfigJSON": dataConfigJSON,
26
+ "dataConfigYAML": dataConfigYAML,
27
+ "dataExportGlobal": dataExportGlobal,
28
+ } {
29
+ require.NotNil(t, data, name)
30
+ }
31
+}
32
+
33
+func TestDovecot_ConfigurationSerialize(t *testing.T) {
34
+ module.TestConfigurationSerialize(t, &Dovecot{}, dataConfigJSON, dataConfigYAML)
35
+}
36
+
37
+func TestDovecot_Init(t *testing.T) {
38
+ tests := map[string]struct {
39
+ config Config
40
+ wantFail bool
41
+ }{
42
+ "success with default config": {
43
+ wantFail: false,
44
+ config: New().Config,
45
+ },
46
+ "fails if address not set": {
47
+ wantFail: true,
48
+ config: func() Config {
49
+ conf := New().Config
50
+ conf.Address = ""
51
+ return conf
52
+ }(),
53
+ },
54
+ }
55
+
56
+ for name, test := range tests {
57
+ t.Run(name, func(t *testing.T) {
58
+ dovecot := New()
59
+ dovecot.Config = test.config
60
+
61
+ if test.wantFail {
62
+ assert.Error(t, dovecot.Init())
63
+ } else {
64
+ assert.NoError(t, dovecot.Init())
65
+ }
66
+ })
67
+ }
68
+}
69
+
70
+func TestDovecot_Cleanup(t *testing.T) {
71
+ tests := map[string]struct {
72
+ prepare func() *Dovecot
73
+ }{
74
+ "not initialized": {
75
+ prepare: func() *Dovecot {
76
+ return New()
77
+ },
78
+ },
79
+ "after check": {
80
+ prepare: func() *Dovecot {
81
+ dovecot := New()
82
+ dovecot.newConn = func(config Config) dovecotConn { return prepareMockOk() }
83
+ _ = dovecot.Check()
84
+ return dovecot
85
+ },
86
+ },
87
+ "after collect": {
88
+ prepare: func() *Dovecot {
89
+ dovecot := New()
90
+ dovecot.newConn = func(config Config) dovecotConn { return prepareMockOk() }
91
+ _ = dovecot.Collect()
92
+ return dovecot
93
+ },
94
+ },
95
+ }
96
+
97
+ for name, test := range tests {
98
+ t.Run(name, func(t *testing.T) {
99
+ dovecot := test.prepare()
100
+
101
+ assert.NotPanics(t, dovecot.Cleanup)
102
+ })
103
+ }
104
+}
105
+
106
+func TestDovecot_Charts(t *testing.T) {
107
+ assert.NotNil(t, New().Charts())
108
+}
109
+
110
+func TestDovecot_Check(t *testing.T) {
111
+ tests := map[string]struct {
112
+ prepareMock func() *mockDovecotConn
113
+ wantFail bool
114
+ }{
115
+ "success case": {
116
+ wantFail: false,
117
+ prepareMock: prepareMockOk,
118
+ },
119
+ "err on connect": {
120
+ wantFail: true,
121
+ prepareMock: prepareMockErrOnConnect,
122
+ },
123
+ "unexpected response": {
124
+ wantFail: true,
125
+ prepareMock: prepareMockUnexpectedResponse,
126
+ },
127
+ "empty response": {
128
+ wantFail: true,
129
+ prepareMock: prepareMockEmptyResponse,
130
+ },
131
+ }
132
+
133
+ for name, test := range tests {
134
+ t.Run(name, func(t *testing.T) {
135
+ dovecot := New()
136
+ mock := test.prepareMock()
137
+ dovecot.newConn = func(config Config) dovecotConn { return mock }
138
+
139
+ if test.wantFail {
140
+ assert.Error(t, dovecot.Check())
141
+ } else {
142
+ assert.NoError(t, dovecot.Check())
143
+ }
144
+ })
145
+ }
146
+}
147
+
148
+func TestDovecot_Collect(t *testing.T) {
149
+ tests := map[string]struct {
150
+ prepareMock func() *mockDovecotConn
151
+ wantMetrics map[string]int64
152
+ disconnectBeforeCleanup bool
153
+ disconnectAfterCleanup bool
154
+ }{
155
+ "success case": {
156
+ prepareMock: prepareMockOk,
157
+ disconnectBeforeCleanup: false,
158
+ disconnectAfterCleanup: true,
159
+ wantMetrics: map[string]int64{
160
+ "auth_cache_hits": 1,
161
+ "auth_cache_misses": 1,
162
+ "auth_db_tempfails": 1,
163
+ "auth_failures": 1,
164
+ "auth_master_successes": 1,
165
+ "auth_successes": 1,
166
+ "disk_input": 1,
167
+ "disk_output": 1,
168
+ "invol_cs": 1,
169
+ "mail_cache_hits": 1,
170
+ "mail_lookup_attr": 1,
171
+ "mail_lookup_path": 1,
172
+ "mail_read_bytes": 1,
173
+ "mail_read_count": 1,
174
+ "maj_faults": 1,
175
+ "min_faults": 1,
176
+ "num_cmds": 1,
177
+ "num_connected_sessions": 1,
178
+ "num_logins": 1,
179
+ "read_bytes": 1,
180
+ "read_count": 1,
181
+ "reset_timestamp": 1723481629,
182
+ "vol_cs": 1,
183
+ "write_bytes": 1,
184
+ "write_count": 1,
185
+ },
186
+ },
187
+ "unexpected response": {
188
+ prepareMock: prepareMockUnexpectedResponse,
189
+ disconnectBeforeCleanup: false,
190
+ disconnectAfterCleanup: true,
191
+ },
192
+ "empty response": {
193
+ prepareMock: prepareMockEmptyResponse,
194
+ disconnectBeforeCleanup: false,
195
+ disconnectAfterCleanup: true,
196
+ },
197
+ "err on connect": {
198
+ prepareMock: prepareMockErrOnConnect,
199
+ disconnectBeforeCleanup: false,
200
+ disconnectAfterCleanup: false,
201
+ },
202
+ "err on query stats": {
203
+ prepareMock: prepareMockErrOnQueryExportGlobal,
204
+ disconnectBeforeCleanup: true,
205
+ disconnectAfterCleanup: true,
206
+ },
207
+ }
208
+
209
+ for name, test := range tests {
210
+ t.Run(name, func(t *testing.T) {
211
+ dovecot := New()
212
+ mock := test.prepareMock()
213
+ dovecot.newConn = func(config Config) dovecotConn { return mock }
214
+
215
+ mx := dovecot.Collect()
216
+
217
+ require.Equal(t, test.wantMetrics, mx)
218
+
219
+ if len(test.wantMetrics) > 0 {
220
+ module.TestMetricsHasAllChartsDims(t, dovecot.Charts(), mx)
221
+ }
222
+
223
+ assert.Equal(t, test.disconnectBeforeCleanup, mock.disconnectCalled, "disconnect before cleanup")
224
+ dovecot.Cleanup()
225
+ assert.Equal(t, test.disconnectAfterCleanup, mock.disconnectCalled, "disconnect after cleanup")
226
+ })
227
+ }
228
+}
229
+
230
+func prepareMockOk() *mockDovecotConn {
231
+ return &mockDovecotConn{
232
+ exportGlobalResponse: dataExportGlobal,
233
+ }
234
+}
235
+
236
+func prepareMockErrOnConnect() *mockDovecotConn {
237
+ return &mockDovecotConn{
238
+ errOnConnect: true,
239
+ }
240
+}
241
+
242
+func prepareMockErrOnQueryExportGlobal() *mockDovecotConn {
243
+ return &mockDovecotConn{
244
+ errOnQueryExportGlobal: true,
245
+ }
246
+}
247
+
248
+func prepareMockUnexpectedResponse() *mockDovecotConn {
249
+ return &mockDovecotConn{
250
+ exportGlobalResponse: []byte("Lorem ipsum dolor sit amet, consectetur adipiscing elit."),
251
+ }
252
+}
253
+
254
+func prepareMockEmptyResponse() *mockDovecotConn {
255
+ return &mockDovecotConn{}
256
+}
257
+
258
+type mockDovecotConn struct {
259
+ errOnConnect bool
260
+ errOnQueryExportGlobal bool
261
+ exportGlobalResponse []byte
262
+ disconnectCalled bool
263
+}
264
+
265
+func (m *mockDovecotConn) connect() error {
266
+ if m.errOnConnect {
267
+ return errors.New("mock.connect() error")
268
+ }
269
+ return nil
270
+}
271
+
272
+func (m *mockDovecotConn) disconnect() {
273
+ m.disconnectCalled = true
274
+}
275
+
276
+func (m *mockDovecotConn) queryExportGlobal() ([]byte, error) {
277
+ if m.errOnQueryExportGlobal {
278
+ return nil, errors.New("mock.queryExportGlobal() error")
279
+ }
280
+ return m.exportGlobalResponse, nil
281
+}
src/go/plugin/go.d/modules/dovecot/metadata.yaml
new
+194
@@ -0,0 +1,194 @@
1
+plugin_name: go.d.plugin
2
+modules:
3
+ - meta:
4
+ id: collector-go.d.plugin-dovecot
5
+ plugin_name: go.d.plugin
6
+ module_name: dovecot
7
+ monitored_instance:
8
+ name: Dovecot
9
+ link: 'https://www.dovecot.org/'
10
+ categories:
11
+ - data-collection.mail-servers
12
+ icon_filename: "dovecot.svg"
13
+ related_resources:
14
+ integrations:
15
+ list: []
16
+ info_provided_to_referring_integrations:
17
+ description: ""
18
+ keywords:
19
+ - dovecot
20
+ - imap
21
+ - mail
22
+ most_popular: false
23
+ overview:
24
+ data_collection:
25
+ metrics_description: |
26
+ This collector monitors Dovecot metrics about sessions, logins, commands, page faults and more.
27
+ method_description: |
28
+ It reads the server's response to the `EXPORT\tglobal\n` command.
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 Dovecot statistics from the following default locations:
39
+
40
+ - localhost:24242
41
+ - unix:///var/run/dovecot/old-stats
42
+ limits:
43
+ description: ""
44
+ performance_impact:
45
+ description: ""
46
+ setup:
47
+ prerequisites:
48
+ list:
49
+ - title: Enable old_stats plugin
50
+ description: |
51
+ To enable `old_stats` plugin, see [Old Statistics](https://doc.dovecot.org/configuration_manual/stats/old_statistics/#old-statistics).
52
+ configuration:
53
+ file:
54
+ name: go.d/dovecot.conf
55
+ options:
56
+ description: |
57
+ The following options can be defined globally: update_every, autodetection_retry.
58
+ folding:
59
+ title: Config options
60
+ enabled: true
61
+ list:
62
+ - name: update_every
63
+ description: Data collection frequency.
64
+ default_value: 1
65
+ required: false
66
+ - name: autodetection_retry
67
+ description: Recheck interval in seconds. Zero means no recheck will be scheduled.
68
+ default_value: 0
69
+ required: false
70
+ - name: address
71
+ description: "The Unix or TCP socket address where the Dovecot [old_stats](https://doc.dovecot.org/configuration_manual/stats/old_statistics/#old-statistics) plugin listens for connections."
72
+ default_value: 127.0.0.1:24242
73
+ required: true
74
+ - name: timeout
75
+ description: Connection, read, and write timeout duration in seconds. The timeout includes name resolution.
76
+ default_value: 1
77
+ required: false
78
+ examples:
79
+ folding:
80
+ title: Config
81
+ enabled: true
82
+ list:
83
+ - name: Basic (TCP)
84
+ description: A basic example configuration.
85
+ config: |
86
+ jobs:
87
+ - name: local
88
+ address: 127.0.0.1:24242
89
+ - name: Basic (UNIX)
90
+ description: A basic example configuration using a UNIX socket.
91
+ config: |
92
+ jobs:
93
+ - name: local
94
+ address: unix:///var/run/dovecot/old-stats
95
+ - name: Multi-instance
96
+ description: |
97
+ > **Note**: When you define multiple jobs, their names must be unique.
98
+
99
+ Collecting metrics from local and remote instances.
100
+ config: |
101
+ jobs:
102
+ - name: local
103
+ address: 127.0.0.1:24242
104
+
105
+ - name: remote
106
+ address: 203.0.113.0:24242
107
+ troubleshooting:
108
+ problems:
109
+ list: []
110
+ alerts: []
111
+ metrics:
112
+ folding:
113
+ title: Metrics
114
+ enabled: false
115
+ description: ""
116
+ availability: []
117
+ scopes:
118
+ - name: global
119
+ description: "These metrics refer to the entire monitored application."
120
+ labels: []
121
+ metrics:
122
+ - name: dovecot.session
123
+ description: Dovecot Active Sessions
124
+ unit: "sessions"
125
+ chart_type: line
126
+ dimensions:
127
+ - name: active
128
+ - name: dovecot.logins
129
+ description: Dovecot Logins
130
+ unit: "logins"
131
+ chart_type: line
132
+ dimensions:
133
+ - name: logins
134
+ - name: dovecot.auth
135
+ description: Dovecot Authentications
136
+ unit: "attempts/s"
137
+ chart_type: stacked
138
+ dimensions:
139
+ - name: ok
140
+ - name: failed
141
+ - name: dovecot.commands
142
+ description: Dovecot Commands
143
+ unit: "commands"
144
+ chart_type: line
145
+ dimensions:
146
+ - name: commands
147
+ - name: dovecot.context_switches
148
+ description: Dovecot Context Switches
149
+ unit: "switches/s"
150
+ chart_type: line
151
+ dimensions:
152
+ - name: voluntary
153
+ - name: voluntary
154
+ - name: dovecot.io
155
+ description: Dovecot Disk I/O
156
+ unit: "KiB/s"
157
+ chart_type: area
158
+ dimensions:
159
+ - name: read
160
+ - name: write
161
+ - name: dovecot.net
162
+ description: Dovecot Network Bandwidth
163
+ unit: "kilobits/s"
164
+ chart_type: area
165
+ dimensions:
166
+ - name: read
167
+ - name: write
168
+ - name: dovecot.syscalls
169
+ description: Dovecot Number of SysCalls
170
+ unit: "syscalls/s"
171
+ chart_type: line
172
+ dimensions:
173
+ - name: read
174
+ - name: write
175
+ - name: dovecot.lookup
176
+ description: Dovecot Lookups
177
+ unit: "lookups/s"
178
+ chart_type: stacked
179
+ dimensions:
180
+ - name: path
181
+ - name: attr
182
+ - name: dovecot.cache
183
+ description: Dovecot Cache Hits
184
+ unit: "hits/s"
185
+ chart_type: line
186
+ dimensions:
187
+ - name: hits
188
+ - name: dovecot.auth_cache
189
+ description: Dovecot Authentication Cache
190
+ unit: "requests/s"
191
+ chart_type: stacked
192
+ dimensions:
193
+ - name: hits
194
+ - name: misses
src/go/plugin/go.d/modules/dovecot/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/dovecot/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/dovecot/testdata/export_global.txt
new
+2
@@ -0,0 +1,2 @@
1
+reset_timestamp last_update num_logins num_cmds num_connected_sessions user_cpu sys_cpu clock_time min_faults maj_faults vol_cs invol_cs disk_input disk_output read_count read_bytes write_count write_bytes mail_lookup_path mail_lookup_attr mail_read_count mail_read_bytes mail_cache_hits auth_successes auth_master_successes auth_failures auth_db_tempfails auth_cache_hits auth_cache_misses
2
+1723481629 1.111111 1 1 1 1.1 1.1 1.1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
src/go/plugin/go.d/modules/init.go
+1
@@ -25,6 +25,7 @@ import (
25
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/docker"
26
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/docker_engine"
27
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/dockerhub"
28
+ _ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/dovecot"
29
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/elasticsearch"
30
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/envoy"
31
_ "github.com/netdata/netdata/go/plugins/plugin/go.d/modules/example"
src/go/plugin/go.d/modules/openvpn/config_schema.json
+1
-1
@@ -15,7 +15,7 @@
15
"title": "Address",
16
"description": "The IP address and port where the OpenVPN [Management Interface](https://openvpn.net/community-resources/management-interface/) listens for connections.",
17
"type": "string",
18
- "default": "127.0.0.1:123"
18
+ "default": "127.0.0.1:7505"
19
},
20
"timeout": {
21
"title": "Timeout",