@cryptotaxi247 / netdata-1 / commits / c3ec6fae5

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",