Skip to content

Commit 3e8a4ef

Browse files
committed
Support scraping of "global" scope
PR kumina#5 (itself based on kumina#4) introduced support to scrape the "global" scope, however the format of these stats differs slightly from that of the others. As a result of this, combined with the silent return when `len(values) != len(columns)+1`, these metrics were being incorrectly exported as mislabeled metrics with values of 0. The changes in this PR accomodate for the format of the "global" scope and now properly scrape those as well. It will now also display an error when column counts mismatch, reducing the likelihood of silent errors in the future. Global metrics will now show up as: ``` dovecot_global_auth_cache_hits 0 dovecot_global_auth_cache_misses 0 dovecot_global_auth_db_tempfails 3 dovecot_global_auth_failures 6 dovecot_global_auth_master_successes 0 dovecot_global_auth_successes 228 dovecot_global_clock_time 0 dovecot_global_disk_input 0 dovecot_global_disk_output 0 dovecot_global_invol_cs 0 dovecot_global_last_update 1.516197189175826e+09 dovecot_global_mail_cache_hits 0 dovecot_global_mail_lookup_attr 0 dovecot_global_mail_lookup_path 0 dovecot_global_mail_read_bytes 0 dovecot_global_mail_read_count 0 dovecot_global_maj_faults 0 dovecot_global_min_faults 0 dovecot_global_num_cmds 0 dovecot_global_num_connected_sessions 234 dovecot_global_num_logins 234 dovecot_global_read_bytes 0 dovecot_global_read_count 0 dovecot_global_reset_timestamp 1.516190181e+09 dovecot_global_sys_cpu 0 dovecot_global_user_cpu 0 dovecot_global_vol_cs 0 dovecot_global_write_bytes 0 dovecot_global_write_count 0 dovecot_up{scope="global"} 1 ``` ...with the other scopes being unaffected.
1 parent 9bbdcdb commit 3e8a4ef

1 file changed

Lines changed: 92 additions & 25 deletions

File tree

dovecot_exporter.go

Lines changed: 92 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,89 @@ var dovecotUpDesc = prometheus.NewDesc(
3434
[]string{"scope"},
3535
nil)
3636

37-
// Converts the output of Dovecot's EXPORT command to metrics.
38-
func CollectFromReader(file io.Reader, ch chan<- prometheus.Metric) error {
39-
scanner := bufio.NewScanner(file)
37+
// CollectFromReader converts the output of Dovecot's EXPORT command to metrics.
38+
func CollectFromReader(file io.Reader, scope string, ch chan<- prometheus.Metric) error {
39+
if scope == "global" {
40+
return collectGlobalMetricsFromReader(file, scope, ch)
41+
}
42+
return collectDetailMetricsFromReader(file, scope, ch)
43+
}
44+
45+
// CollectFromFile collects dovecot statistics from the given file
46+
func CollectFromFile(path string, scope string, ch chan<- prometheus.Metric) error {
47+
conn, err := os.Open(path)
48+
if err != nil {
49+
return err
50+
}
51+
return CollectFromReader(conn, scope, ch)
52+
}
53+
54+
// CollectFromSocket collects statistics from dovecot's stats socket.
55+
func CollectFromSocket(path string, scope string, ch chan<- prometheus.Metric) error {
56+
conn, err := net.Dial("unix", path)
57+
if err != nil {
58+
return err
59+
}
60+
_, err = conn.Write([]byte("EXPORT\t" + scope + "\n"))
61+
if err != nil {
62+
return err
63+
}
64+
return CollectFromReader(conn, scope, ch)
65+
}
66+
67+
// collectGlobalMetricsFromReader collects dovecot "global" scope metrics from
68+
// the supplied reader.
69+
func collectGlobalMetricsFromReader(reader io.Reader, scope string, ch chan<- prometheus.Metric) error {
70+
scanner := bufio.NewScanner(reader)
71+
scanner.Split(bufio.ScanLines)
72+
73+
// Read first line of input, containing the aggregation and column names.
74+
if !scanner.Scan() {
75+
return fmt.Errorf("Failed to extract columns from input")
76+
}
77+
columnNames := strings.Fields(scanner.Text())
78+
if len(columnNames) < 1 {
79+
return fmt.Errorf("Input does not provide any columns")
80+
}
81+
82+
columns := []*prometheus.Desc{}
83+
for _, columnName := range columnNames {
84+
columns = append(columns, prometheus.NewDesc(
85+
prometheus.BuildFQName("dovecot", scope, columnName),
86+
"Help text not provided by this exporter.",
87+
[]string{},
88+
nil))
89+
}
90+
91+
// Global metrics only have a single row containing values following the
92+
// line with column names
93+
if !scanner.Scan() {
94+
return scanner.Err()
95+
}
96+
values := strings.Fields(scanner.Text())
97+
98+
if len(values) != len(columns) {
99+
return fmt.Errorf("error while parsing row: value count does not match column count")
100+
}
101+
102+
for i, value := range values {
103+
f, err := strconv.ParseFloat(value, 64)
104+
if err != nil {
105+
return err
106+
}
107+
ch <- prometheus.MustNewConstMetric(
108+
columns[i],
109+
prometheus.UntypedValue,
110+
f,
111+
)
112+
}
113+
return scanner.Err()
114+
}
115+
116+
// collectGlobalMetricsFromReader collects dovecot "non-global" scope metrics
117+
// from the supplied reader.
118+
func collectDetailMetricsFromReader(reader io.Reader, scope string, ch chan<- prometheus.Metric) error {
119+
scanner := bufio.NewScanner(reader)
40120
scanner.Split(bufio.ScanLines)
41121

42122
// Read first line of input, containing the aggregation and column names.
@@ -47,6 +127,7 @@ func CollectFromReader(file io.Reader, ch chan<- prometheus.Metric) error {
47127
if len(columnNames) < 2 {
48128
return fmt.Errorf("Input does not provide any columns")
49129
}
130+
50131
columns := []*prometheus.Desc{}
51132
for _, columnName := range columnNames[1:] {
52133
columns = append(columns, prometheus.NewDesc(
@@ -58,10 +139,16 @@ func CollectFromReader(file io.Reader, ch chan<- prometheus.Metric) error {
58139

59140
// Read successive lines, containing the values.
60141
for scanner.Scan() {
61-
values := strings.Fields(scanner.Text())
62-
if len(values) != len(columns)+1 {
142+
row := scanner.Text()
143+
if strings.TrimSpace(row) == "" {
63144
break
64145
}
146+
147+
values := strings.Fields(row)
148+
if len(values) != len(columns)+1 {
149+
return fmt.Errorf("error while parsing rows: value count does not match column count")
150+
}
151+
65152
for i, value := range values[1:] {
66153
f, err := strconv.ParseFloat(value, 64)
67154
if err != nil {
@@ -77,26 +164,6 @@ func CollectFromReader(file io.Reader, ch chan<- prometheus.Metric) error {
77164
return scanner.Err()
78165
}
79166

80-
func CollectFromFile(path string, ch chan<- prometheus.Metric) error {
81-
conn, err := os.Open(path)
82-
if err != nil {
83-
return err
84-
}
85-
return CollectFromReader(conn, ch)
86-
}
87-
88-
func CollectFromSocket(path string, scope string, ch chan<- prometheus.Metric) error {
89-
conn, err := net.Dial("unix", path)
90-
if err != nil {
91-
return err
92-
}
93-
_, err = conn.Write([]byte("EXPORT\t" + scope + "\n"))
94-
if err != nil {
95-
return err
96-
}
97-
return CollectFromReader(conn, ch)
98-
}
99-
100167
type DovecotExporter struct {
101168
scopes []string
102169
socketPath string

0 commit comments

Comments
 (0)