Skip to content
Snippets Groups Projects
Unverified Commit 3e8a4ef5 authored by Nick Groenen's avatar Nick Groenen
Browse files

Support scraping of "global" scope

PR #5 (itself based on #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.
parent 9bbdcdbf
No related branches found
No related tags found
No related merge requests found
...@@ -34,9 +34,40 @@ var dovecotUpDesc = prometheus.NewDesc( ...@@ -34,9 +34,40 @@ var dovecotUpDesc = prometheus.NewDesc(
[]string{"scope"}, []string{"scope"},
nil) nil)
// Converts the output of Dovecot's EXPORT command to metrics. // CollectFromReader converts the output of Dovecot's EXPORT command to metrics.
func CollectFromReader(file io.Reader, ch chan<- prometheus.Metric) error { func CollectFromReader(file io.Reader, scope string, ch chan<- prometheus.Metric) error {
scanner := bufio.NewScanner(file) if scope == "global" {
return collectGlobalMetricsFromReader(file, scope, ch)
}
return collectDetailMetricsFromReader(file, scope, ch)
}
// CollectFromFile collects dovecot statistics from the given file
func CollectFromFile(path string, scope string, ch chan<- prometheus.Metric) error {
conn, err := os.Open(path)
if err != nil {
return err
}
return CollectFromReader(conn, scope, ch)
}
// CollectFromSocket collects statistics from dovecot's stats socket.
func CollectFromSocket(path string, scope string, ch chan<- prometheus.Metric) error {
conn, err := net.Dial("unix", path)
if err != nil {
return err
}
_, err = conn.Write([]byte("EXPORT\t" + scope + "\n"))
if err != nil {
return err
}
return CollectFromReader(conn, scope, ch)
}
// collectGlobalMetricsFromReader collects dovecot "global" scope metrics from
// the supplied reader.
func collectGlobalMetricsFromReader(reader io.Reader, scope string, ch chan<- prometheus.Metric) error {
scanner := bufio.NewScanner(reader)
scanner.Split(bufio.ScanLines) scanner.Split(bufio.ScanLines)
// Read first line of input, containing the aggregation and column names. // Read first line of input, containing the aggregation and column names.
...@@ -44,25 +75,31 @@ func CollectFromReader(file io.Reader, ch chan<- prometheus.Metric) error { ...@@ -44,25 +75,31 @@ func CollectFromReader(file io.Reader, ch chan<- prometheus.Metric) error {
return fmt.Errorf("Failed to extract columns from input") return fmt.Errorf("Failed to extract columns from input")
} }
columnNames := strings.Fields(scanner.Text()) columnNames := strings.Fields(scanner.Text())
if len(columnNames) < 2 { if len(columnNames) < 1 {
return fmt.Errorf("Input does not provide any columns") return fmt.Errorf("Input does not provide any columns")
} }
columns := []*prometheus.Desc{} columns := []*prometheus.Desc{}
for _, columnName := range columnNames[1:] { for _, columnName := range columnNames {
columns = append(columns, prometheus.NewDesc( columns = append(columns, prometheus.NewDesc(
prometheus.BuildFQName("dovecot", columnNames[0], columnName), prometheus.BuildFQName("dovecot", scope, columnName),
"Help text not provided by this exporter.", "Help text not provided by this exporter.",
[]string{columnNames[0]}, []string{},
nil)) nil))
} }
// Read successive lines, containing the values. // Global metrics only have a single row containing values following the
for scanner.Scan() { // line with column names
if !scanner.Scan() {
return scanner.Err()
}
values := strings.Fields(scanner.Text()) values := strings.Fields(scanner.Text())
if len(values) != len(columns)+1 {
break if len(values) != len(columns) {
return fmt.Errorf("error while parsing row: value count does not match column count")
} }
for i, value := range values[1:] {
for i, value := range values {
f, err := strconv.ParseFloat(value, 64) f, err := strconv.ParseFloat(value, 64)
if err != nil { if err != nil {
return err return err
...@@ -71,30 +108,60 @@ func CollectFromReader(file io.Reader, ch chan<- prometheus.Metric) error { ...@@ -71,30 +108,60 @@ func CollectFromReader(file io.Reader, ch chan<- prometheus.Metric) error {
columns[i], columns[i],
prometheus.UntypedValue, prometheus.UntypedValue,
f, f,
values[0]) )
}
} }
return scanner.Err() return scanner.Err()
} }
func CollectFromFile(path string, ch chan<- prometheus.Metric) error { // collectGlobalMetricsFromReader collects dovecot "non-global" scope metrics
conn, err := os.Open(path) // from the supplied reader.
if err != nil { func collectDetailMetricsFromReader(reader io.Reader, scope string, ch chan<- prometheus.Metric) error {
return err scanner := bufio.NewScanner(reader)
scanner.Split(bufio.ScanLines)
// Read first line of input, containing the aggregation and column names.
if !scanner.Scan() {
return fmt.Errorf("Failed to extract columns from input")
} }
return CollectFromReader(conn, ch) columnNames := strings.Fields(scanner.Text())
if len(columnNames) < 2 {
return fmt.Errorf("Input does not provide any columns")
} }
func CollectFromSocket(path string, scope string, ch chan<- prometheus.Metric) error { columns := []*prometheus.Desc{}
conn, err := net.Dial("unix", path) for _, columnName := range columnNames[1:] {
if err != nil { columns = append(columns, prometheus.NewDesc(
return err prometheus.BuildFQName("dovecot", columnNames[0], columnName),
"Help text not provided by this exporter.",
[]string{columnNames[0]},
nil))
} }
_, err = conn.Write([]byte("EXPORT\t" + scope + "\n"))
// Read successive lines, containing the values.
for scanner.Scan() {
row := scanner.Text()
if strings.TrimSpace(row) == "" {
break
}
values := strings.Fields(row)
if len(values) != len(columns)+1 {
return fmt.Errorf("error while parsing rows: value count does not match column count")
}
for i, value := range values[1:] {
f, err := strconv.ParseFloat(value, 64)
if err != nil { if err != nil {
return err return err
} }
return CollectFromReader(conn, ch) ch <- prometheus.MustNewConstMetric(
columns[i],
prometheus.UntypedValue,
f,
values[0])
}
}
return scanner.Err()
} }
type DovecotExporter struct { type DovecotExporter struct {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment