diff --git a/README.md b/README.md index ae2af21..cf77b3e 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,8 @@ Usage of openvpn_exporter: Address to listen on for web interface and telemetry. (default ":9176") -web.telemetry-path string Path under which to expose metrics. (default "/metrics") + -ignore.individuals bool + If ignoring metrics for individuals (default false) ``` E.g: diff --git a/examples/server2.status b/examples/server2.status index d5499ed..a82eb23 100644 --- a/examples/server2.status +++ b/examples/server2.status @@ -6,11 +6,13 @@ CLIENT_LIST,redacted2,0.0.0.0:60536,0.0.0.0,2925752,3145665,Thu Mar 16 17:08:57 CLIENT_LIST,redacted3,0.0.0.0:28331,0.0.0.0,57316467,611736741,Thu Mar 16 17:08:57 2017,1489680537,UNDEF CLIENT_LIST,redacted4,0.0.0.0:52335,0.0.0.0,24289622392,70914674697,Fri Mar 17 11:16:29 2017,1489745789,UNDEF CLIENT_LIST,redacted5,0.0.0.0:51865,0.0.0.0,277017840,1544465106,Thu Mar 16 17:09:01 2017,1489680541,UNDEF +CLIENT_LIST,redacted1,0.0.0.0:19021,0.0.0.0,693438277,228390856,Thu Mar 16 17:09:03 2017,1489680543,UNDEF HEADER,ROUTING_TABLE,Virtual Address,Common Name,Real Address,Last Ref,Last Ref (time_t) ROUTING_TABLE,0.0.0.0,redacted1,0.0.0.0:19021,Tue Mar 21 10:26:48 2017,1490088408 ROUTING_TABLE,0.0.0.0,redacted5,0.0.0.0:51865,Tue Mar 21 10:38:26 2017,1490089106 ROUTING_TABLE,0.0.0.0,redacted3,0.0.0.0:28331,Tue Mar 21 10:39:06 2017,1490089146 ROUTING_TABLE,0.0.0.0,redacted4,0.0.0.0:52335,Tue Mar 21 10:39:13 2017,1490089153 ROUTING_TABLE,0.0.0.0,redacted2,0.0.0.0:60536,Thu Mar 16 17:08:58 2017,1489680538 +ROUTING_TABLE,0.0.0.0,redacted1,0.0.0.0:19021,Tue Mar 21 10:26:48 2017,1490088408 GLOBAL_STATS,Max bcast/mcast queue length,0 END diff --git a/exporters/openvpn_exporter.go b/exporters/openvpn_exporter.go new file mode 100644 index 0000000..b658898 --- /dev/null +++ b/exporters/openvpn_exporter.go @@ -0,0 +1,363 @@ +package exporters + +import ( + "bufio" + "bytes" + "fmt" + "github.com/prometheus/client_golang/prometheus" + "io" + "log" + "os" + "strconv" + "strings" + "time" +) + +type OpenvpnServerHeader struct { + LabelColumns []string + Metrics []OpenvpnServerHeaderField +} + +type OpenvpnServerHeaderField struct { + Column string + Desc *prometheus.Desc + ValueType prometheus.ValueType +} + +type OpenVPNExporter struct { + statusPaths []string + openvpnUpDesc *prometheus.Desc + openvpnStatusUpdateTimeDesc *prometheus.Desc + openvpnConnectedClientsDesc *prometheus.Desc + openvpnClientDescs map[string]*prometheus.Desc + openvpnServerHeaders map[string]OpenvpnServerHeader +} + +func NewOpenVPNExporter(statusPaths []string, ignoreIndividuals bool) (*OpenVPNExporter, error) { + // Metrics exported both for client and server statistics. + openvpnUpDesc := prometheus.NewDesc( + prometheus.BuildFQName("openvpn", "", "up"), + "Whether scraping OpenVPN's metrics was successful.", + []string{"status_path"}, nil) + openvpnStatusUpdateTimeDesc := prometheus.NewDesc( + prometheus.BuildFQName("openvpn", "", "status_update_time_seconds"), + "UNIX timestamp at which the OpenVPN statistics were updated.", + []string{"status_path"}, nil) + + // Metrics specific to OpenVPN servers. + openvpnConnectedClientsDesc := prometheus.NewDesc( + prometheus.BuildFQName("openvpn", "", "server_connected_clients"), + "Number Of Connected Clients", + []string{"status_path"}, nil) + + // Metrics specific to OpenVPN clients. + openvpnClientDescs := map[string]*prometheus.Desc{ + "TUN/TAP read bytes": prometheus.NewDesc( + prometheus.BuildFQName("openvpn", "client", "tun_tap_read_bytes_total"), + "Total amount of TUN/TAP traffic read, in bytes.", + []string{"status_path"}, nil), + "TUN/TAP write bytes": prometheus.NewDesc( + prometheus.BuildFQName("openvpn", "client", "tun_tap_write_bytes_total"), + "Total amount of TUN/TAP traffic written, in bytes.", + []string{"status_path"}, nil), + "TCP/UDP read bytes": prometheus.NewDesc( + prometheus.BuildFQName("openvpn", "client", "tcp_udp_read_bytes_total"), + "Total amount of TCP/UDP traffic read, in bytes.", + []string{"status_path"}, nil), + "TCP/UDP write bytes": prometheus.NewDesc( + prometheus.BuildFQName("openvpn", "client", "tcp_udp_write_bytes_total"), + "Total amount of TCP/UDP traffic written, in bytes.", + []string{"status_path"}, nil), + "Auth read bytes": prometheus.NewDesc( + prometheus.BuildFQName("openvpn", "client", "auth_read_bytes_total"), + "Total amount of authentication traffic read, in bytes.", + []string{"status_path"}, nil), + "pre-compress bytes": prometheus.NewDesc( + prometheus.BuildFQName("openvpn", "client", "pre_compress_bytes_total"), + "Total amount of data before compression, in bytes.", + []string{"status_path"}, nil), + "post-compress bytes": prometheus.NewDesc( + prometheus.BuildFQName("openvpn", "client", "post_compress_bytes_total"), + "Total amount of data after compression, in bytes.", + []string{"status_path"}, nil), + "pre-decompress bytes": prometheus.NewDesc( + prometheus.BuildFQName("openvpn", "client", "pre_decompress_bytes_total"), + "Total amount of data before decompression, in bytes.", + []string{"status_path"}, nil), + "post-decompress bytes": prometheus.NewDesc( + prometheus.BuildFQName("openvpn", "client", "post_decompress_bytes_total"), + "Total amount of data after decompression, in bytes.", + []string{"status_path"}, nil), + } + + var serverHeaderClientLabels []string + var serverHeaderClientLabelColumns []string + var serverHeaderRoutingLabels []string + var serverHeaderRoutingLabelColumns []string + if ignoreIndividuals { + serverHeaderClientLabels = []string{"status_path", "common_name"} + serverHeaderClientLabelColumns = []string{"Common Name"} + serverHeaderRoutingLabels = []string{"status_path", "common_name"} + serverHeaderRoutingLabelColumns = []string{"Common Name"} + } else { + serverHeaderClientLabels = []string{"status_path", "common_name", "connection_time", "real_address", "virtual_address", "username"} + serverHeaderClientLabelColumns = []string{"Common Name", "Connected Since (time_t)", "Real Address", "Virtual Address", "Username"} + serverHeaderRoutingLabels = []string{"status_path", "common_name", "real_address", "virtual_address"} + serverHeaderRoutingLabelColumns = []string{"Common Name", "Real Address", "Virtual Address"} + } + + openvpnServerHeaders := map[string]OpenvpnServerHeader{ + "CLIENT_LIST": { + LabelColumns: serverHeaderClientLabelColumns, + Metrics: []OpenvpnServerHeaderField{ + { + Column: "Bytes Received", + Desc: prometheus.NewDesc( + prometheus.BuildFQName("openvpn", "server", "client_received_bytes_total"), + "Amount of data received over a connection on the VPN server, in bytes.", + serverHeaderClientLabels, nil), + ValueType: prometheus.CounterValue, + }, + { + Column: "Bytes Sent", + Desc: prometheus.NewDesc( + prometheus.BuildFQName("openvpn", "server", "client_sent_bytes_total"), + "Amount of data sent over a connection on the VPN server, in bytes.", + serverHeaderClientLabels, nil), + ValueType: prometheus.CounterValue, + }, + }, + }, + "ROUTING_TABLE": { + LabelColumns: serverHeaderRoutingLabelColumns, + Metrics: []OpenvpnServerHeaderField{ + { + Column: "Last Ref (time_t)", + Desc: prometheus.NewDesc( + prometheus.BuildFQName("openvpn", "server", "route_last_reference_time_seconds"), + "Time at which a route was last referenced, in seconds.", + serverHeaderRoutingLabels, nil), + ValueType: prometheus.GaugeValue, + }, + }, + }, + } + + return &OpenVPNExporter{ + statusPaths: statusPaths, + openvpnUpDesc: openvpnUpDesc, + openvpnStatusUpdateTimeDesc: openvpnStatusUpdateTimeDesc, + openvpnConnectedClientsDesc: openvpnConnectedClientsDesc, + openvpnClientDescs: openvpnClientDescs, + openvpnServerHeaders: openvpnServerHeaders, + }, nil +} + +// Converts OpenVPN status information into Prometheus metrics. This +// function automatically detects whether the file contains server or +// client metrics. For server metrics, it also distinguishes between the +// version 2 and 3 file formats. +func (e *OpenVPNExporter) collectStatusFromReader(statusPath string, file io.Reader, ch chan<- prometheus.Metric) error { + reader := bufio.NewReader(file) + buf, _ := reader.Peek(18) + if bytes.HasPrefix(buf, []byte("TITLE,")) { + // Server statistics, using format version 2. + return e.collectServerStatusFromReader(statusPath, reader, ch, ",") + } else if bytes.HasPrefix(buf, []byte("TITLE\t")) { + // Server statistics, using format version 3. The only + // difference compared to version 2 is that it uses tabs + // instead of spaces. + return e.collectServerStatusFromReader(statusPath, reader, ch, "\t") + } else if bytes.HasPrefix(buf, []byte("OpenVPN STATISTICS")) { + // Client statistics. + return e.collectClientStatusFromReader(statusPath, reader, ch) + } else { + return fmt.Errorf("unexpected file contents: %q", buf) + } +} + +// Converts OpenVPN server status information into Prometheus metrics. +func (e *OpenVPNExporter) collectServerStatusFromReader(statusPath string, file io.Reader, ch chan<- prometheus.Metric, separator string) error { + scanner := bufio.NewScanner(file) + scanner.Split(bufio.ScanLines) + headersFound := map[string][]string{} + // counter of connected client + numberConnectedClient := 0 + + recordedMetrics := map[OpenvpnServerHeaderField][]string{} + + for scanner.Scan() { + fields := strings.Split(scanner.Text(), separator) + if fields[0] == "END" && len(fields) == 1 { + // Stats footer. + } else if fields[0] == "GLOBAL_STATS" { + // Global server statistics. + } else if fields[0] == "HEADER" && len(fields) > 2 { + // Column names for CLIENT_LIST and ROUTING_TABLE. + headersFound[fields[1]] = fields[2:] + } else if fields[0] == "TIME" && len(fields) == 3 { + // Time at which the statistics were updated. + timeStartStats, err := strconv.ParseFloat(fields[2], 64) + if err != nil { + return err + } + ch <- prometheus.MustNewConstMetric( + e.openvpnStatusUpdateTimeDesc, + prometheus.GaugeValue, + timeStartStats, + statusPath) + } else if fields[0] == "TITLE" && len(fields) == 2 { + // OpenVPN version number. + } else if header, ok := e.openvpnServerHeaders[fields[0]]; ok { + if fields[0] == "CLIENT_LIST" { + numberConnectedClient++ + } + // Entry that depends on a preceding HEADERS directive. + columnNames, ok := headersFound[fields[0]] + if !ok { + return fmt.Errorf("%s should be preceded by HEADERS", fields[0]) + } + if len(fields) != len(columnNames)+1 { + return fmt.Errorf("HEADER for %s describes a different number of columns", fields[0]) + } + + // Store entry values in a map indexed by column name. + columnValues := map[string]string{} + for _, column := range header.LabelColumns { + columnValues[column] = "" + } + for i, column := range columnNames { + columnValues[column] = fields[i+1] + } + + // Extract columns that should act as entry labels. + labels := []string{statusPath} + for _, column := range header.LabelColumns { + labels = append(labels, columnValues[column]) + } + + // Export relevant columns as individual metrics. + for _, metric := range header.Metrics { + if columnValue, ok := columnValues[metric.Column]; ok { + if l, _ := recordedMetrics[metric]; ! subslice(labels, l) { + value, err := strconv.ParseFloat(columnValue, 64) + if err != nil { + return err + } + ch <- prometheus.MustNewConstMetric( + metric.Desc, + metric.ValueType, + value, + labels...) + recordedMetrics[metric] = append(recordedMetrics[metric], labels...) + } else { + log.Printf("Metric entry with same labels: %s, %s", metric.Column, labels) + } + } + } + } else { + return fmt.Errorf("unsupported key: %q", fields[0]) + } + } + // add the number of connected client + ch <- prometheus.MustNewConstMetric( + e.openvpnConnectedClientsDesc, + prometheus.GaugeValue, + float64(numberConnectedClient), + statusPath) + return scanner.Err() +} + +// Does slice contain string +func contains(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} + +// Is a sub-slice of slice +func subslice(sub []string, main []string) bool { + if len(sub) > len(main) {return false} + for _, s := range sub { + if ! contains(main, s) { + return false + } + } + return true +} + +// Converts OpenVPN client status information into Prometheus metrics. +func (e *OpenVPNExporter) collectClientStatusFromReader(statusPath string, file io.Reader, ch chan<- prometheus.Metric) error { + scanner := bufio.NewScanner(file) + scanner.Split(bufio.ScanLines) + for scanner.Scan() { + fields := strings.Split(scanner.Text(), ",") + if fields[0] == "END" && len(fields) == 1 { + // Stats footer. + } else if fields[0] == "OpenVPN STATISTICS" && len(fields) == 1 { + // Stats header. + } else if fields[0] == "Updated" && len(fields) == 2 { + // Time at which the statistics were updated. + location, _ := time.LoadLocation("Local") + timeParser, err := time.ParseInLocation("Mon Jan 2 15:04:05 2006", fields[1], location) + if err != nil { + return err + } + ch <- prometheus.MustNewConstMetric( + e.openvpnStatusUpdateTimeDesc, + prometheus.GaugeValue, + float64(timeParser.Unix()), + statusPath) + } else if desc, ok := e.openvpnClientDescs[fields[0]]; ok && len(fields) == 2 { + // Traffic counters. + value, err := strconv.ParseFloat(fields[1], 64) + if err != nil { + return err + } + ch <- prometheus.MustNewConstMetric( + desc, + prometheus.CounterValue, + value, + statusPath) + } else { + return fmt.Errorf("unsupported key: %q", fields[0]) + } + } + return scanner.Err() +} + +func (e *OpenVPNExporter) collectStatusFromFile(statusPath string, ch chan<- prometheus.Metric) error { + conn, err := os.Open(statusPath) + defer conn.Close() + if err != nil { + return err + } + return e.collectStatusFromReader(statusPath, conn, ch) +} + +func (e *OpenVPNExporter) Describe(ch chan<- *prometheus.Desc) { + ch <- e.openvpnUpDesc +} + +func (e *OpenVPNExporter) Collect(ch chan<- prometheus.Metric) { + for _, statusPath := range e.statusPaths { + err := e.collectStatusFromFile(statusPath, ch) + if err == nil { + ch <- prometheus.MustNewConstMetric( + e.openvpnUpDesc, + prometheus.GaugeValue, + 1.0, + statusPath) + } else { + log.Printf("Failed to scrape showq socket: %s", err) + ch <- prometheus.MustNewConstMetric( + e.openvpnUpDesc, + prometheus.GaugeValue, + 0.0, + statusPath) + } + } +} diff --git a/main.go b/main.go index 371153d..07a6fe0 100644 --- a/main.go +++ b/main.go @@ -14,334 +14,21 @@ package main import ( - "bufio" - "bytes" "flag" - "fmt" - "io" + "github.com/kumina/openvpn_exporter/exporters" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" "log" "net/http" - "os" - "strconv" "strings" - "time" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" ) -type OpenvpnServerHeader struct { - LabelColumns []string - Metrics []OpenvpnServerHeaderField -} - -type OpenvpnServerHeaderField struct { - Column string - Desc *prometheus.Desc - ValueType prometheus.ValueType -} - -var ( - // Metrics exported both for client and server statistics. - openvpnUpDesc = prometheus.NewDesc( - prometheus.BuildFQName("openvpn", "", "up"), - "Whether scraping OpenVPN's metrics was successful.", - []string{"status_path"}, nil) - openvpnStatusUpdateTimeDesc = prometheus.NewDesc( - prometheus.BuildFQName("openvpn", "", "status_update_time_seconds"), - "UNIX timestamp at which the OpenVPN statistics were updated.", - []string{"status_path"}, nil) - - // Metrics specific to OpenVPN servers. - openvpnConnectedClientsDesc = prometheus.NewDesc( - prometheus.BuildFQName("openvpn", "", "openvpn_server_connected_clients"), - "Number Of Connected Clients", - []string{"status_path"}, nil) - - openvpnServerHeaders = map[string]OpenvpnServerHeader{ - "CLIENT_LIST": { - LabelColumns: []string{ - "Common Name", - "Connected Since (time_t)", - "Real Address", - "Virtual Address", - "Username", - }, - Metrics: []OpenvpnServerHeaderField{ - { - Column: "Bytes Received", - Desc: prometheus.NewDesc( - prometheus.BuildFQName("openvpn", "server", "client_received_bytes_total"), - "Amount of data received over a connection on the VPN server, in bytes.", - []string{"status_path", "common_name", "connection_time", "real_address", "virtual_address", "username"}, nil), - ValueType: prometheus.CounterValue, - }, - { - Column: "Bytes Sent", - Desc: prometheus.NewDesc( - prometheus.BuildFQName("openvpn", "server", "client_sent_bytes_total"), - "Amount of data sent over a connection on the VPN server, in bytes.", - []string{"status_path", "common_name", "connection_time", "real_address", "virtual_address", "username"}, nil), - ValueType: prometheus.CounterValue, - }, - }, - }, - "ROUTING_TABLE": { - LabelColumns: []string{ - "Common Name", - "Real Address", - "Virtual Address", - }, - Metrics: []OpenvpnServerHeaderField{ - { - Column: "Last Ref (time_t)", - Desc: prometheus.NewDesc( - prometheus.BuildFQName("openvpn", "server", "route_last_reference_time_seconds"), - "Time at which a route was last referenced, in seconds.", - []string{"status_path", "common_name", "real_address", "virtual_address"}, nil), - ValueType: prometheus.GaugeValue, - }, - }, - }, - } - - // Metrics specific to OpenVPN clients. - openvpnClientDescs = map[string]*prometheus.Desc{ - "TUN/TAP read bytes": prometheus.NewDesc( - prometheus.BuildFQName("openvpn", "client", "tun_tap_read_bytes_total"), - "Total amount of TUN/TAP traffic read, in bytes.", - []string{"status_path"}, nil), - "TUN/TAP write bytes": prometheus.NewDesc( - prometheus.BuildFQName("openvpn", "client", "tun_tap_write_bytes_total"), - "Total amount of TUN/TAP traffic written, in bytes.", - []string{"status_path"}, nil), - "TCP/UDP read bytes": prometheus.NewDesc( - prometheus.BuildFQName("openvpn", "client", "tcp_udp_read_bytes_total"), - "Total amount of TCP/UDP traffic read, in bytes.", - []string{"status_path"}, nil), - "TCP/UDP write bytes": prometheus.NewDesc( - prometheus.BuildFQName("openvpn", "client", "tcp_udp_write_bytes_total"), - "Total amount of TCP/UDP traffic written, in bytes.", - []string{"status_path"}, nil), - "Auth read bytes": prometheus.NewDesc( - prometheus.BuildFQName("openvpn", "client", "auth_read_bytes_total"), - "Total amount of authentication traffic read, in bytes.", - []string{"status_path"}, nil), - "pre-compress bytes": prometheus.NewDesc( - prometheus.BuildFQName("openvpn", "client", "pre_compress_bytes_total"), - "Total amount of data before compression, in bytes.", - []string{"status_path"}, nil), - "post-compress bytes": prometheus.NewDesc( - prometheus.BuildFQName("openvpn", "client", "post_compress_bytes_total"), - "Total amount of data after compression, in bytes.", - []string{"status_path"}, nil), - "pre-decompress bytes": prometheus.NewDesc( - prometheus.BuildFQName("openvpn", "client", "pre_decompress_bytes_total"), - "Total amount of data before decompression, in bytes.", - []string{"status_path"}, nil), - "post-decompress bytes": prometheus.NewDesc( - prometheus.BuildFQName("openvpn", "client", "post_decompress_bytes_total"), - "Total amount of data after decompression, in bytes.", - []string{"status_path"}, nil), - } -) - -// Converts OpenVPN status information into Prometheus metrics. This -// function automatically detects whether the file contains server or -// client metrics. For server metrics, it also distinguishes between the -// version 2 and 3 file formats. -func CollectStatusFromReader(statusPath string, file io.Reader, ch chan<- prometheus.Metric) error { - reader := bufio.NewReader(file) - buf, _ := reader.Peek(18) - if bytes.HasPrefix(buf, []byte("TITLE,")) { - // Server statistics, using format version 2. - return CollectServerStatusFromReader(statusPath, reader, ch, ",") - } else if bytes.HasPrefix(buf, []byte("TITLE\t")) { - // Server statistics, using format version 3. The only - // difference compared to version 2 is that it uses tabs - // instead of spaces. - return CollectServerStatusFromReader(statusPath, reader, ch, "\t") - } else if bytes.HasPrefix(buf, []byte("OpenVPN STATISTICS")) { - // Client statistics. - return CollectClientStatusFromReader(statusPath, reader, ch) - } else { - return fmt.Errorf("unexpected file contents: %q", buf) - } -} - -// Converts OpenVPN server status information into Prometheus metrics. -func CollectServerStatusFromReader(statusPath string, file io.Reader, ch chan<- prometheus.Metric, separator string) error { - scanner := bufio.NewScanner(file) - scanner.Split(bufio.ScanLines) - headersFound := map[string][]string{} - // counter of connected client - numberConnectedClient := 0 - - for scanner.Scan() { - fields := strings.Split(scanner.Text(), separator) - if fields[0] == "END" && len(fields) == 1 { - // Stats footer. - } else if fields[0] == "GLOBAL_STATS" { - // Global server statistics. - } else if fields[0] == "HEADER" && len(fields) > 2 { - // Column names for CLIENT_LIST and ROUTING_TABLE. - headersFound[fields[1]] = fields[2:] - } else if fields[0] == "TIME" && len(fields) == 3 { - // Time at which the statistics were updated. - timeStartStats, err := strconv.ParseFloat(fields[2], 64) - if err != nil { - return err - } - ch <- prometheus.MustNewConstMetric( - openvpnStatusUpdateTimeDesc, - prometheus.GaugeValue, - timeStartStats, - statusPath) - } else if fields[0] == "TITLE" && len(fields) == 2 { - // OpenVPN version number. - } else if header, ok := openvpnServerHeaders[fields[0]]; ok { - if fields[0] == "CLIENT_LIST"{ - numberConnectedClient ++ - } - // Entry that depends on a preceding HEADERS directive. - columnNames, ok := headersFound[fields[0]] - if !ok { - return fmt.Errorf("%s should be preceded by HEADERS", fields[0]) - } - if len(fields) != len(columnNames)+1 { - return fmt.Errorf("HEADER for %s describes a different number of columns", fields[0]) - } - - // Store entry values in a map indexed by column name. - columnValues := map[string]string{} - for _, column := range header.LabelColumns { - columnValues[column] = "" - } - for i, column := range columnNames { - columnValues[column] = fields[i+1] - } - - // Extract columns that should act as entry labels. - labels := []string{statusPath} - for _, column := range header.LabelColumns { - labels = append(labels, columnValues[column]) - } - - // Export relevant columns as individual metrics. - for _, metric := range header.Metrics { - if columnValue, ok := columnValues[metric.Column]; ok { - value, err := strconv.ParseFloat(columnValue, 64) - if err != nil { - return err - } - ch <- prometheus.MustNewConstMetric( - metric.Desc, - metric.ValueType, - value, - labels...) - } - } - } else { - return fmt.Errorf("unsupported key: %q", fields[0]) - } - } - // add the number of connected client - ch <- prometheus.MustNewConstMetric( - openvpnConnectedClientsDesc, - prometheus.GaugeValue, - float64(numberConnectedClient), - statusPath) - return scanner.Err() -} - -// Converts OpenVPN client status information into Prometheus metrics. -func CollectClientStatusFromReader(statusPath string, file io.Reader, ch chan<- prometheus.Metric) error { - scanner := bufio.NewScanner(file) - scanner.Split(bufio.ScanLines) - for scanner.Scan() { - fields := strings.Split(scanner.Text(), ",") - if fields[0] == "END" && len(fields) == 1 { - // Stats footer. - } else if fields[0] == "OpenVPN STATISTICS" && len(fields) == 1 { - // Stats header. - } else if fields[0] == "Updated" && len(fields) == 2 { - // Time at which the statistics were updated. - location, _ := time.LoadLocation("Local") - timeParser, err := time.ParseInLocation("Mon Jan 2 15:04:05 2006", fields[1], location) - if err != nil { - return err - } - ch <- prometheus.MustNewConstMetric( - openvpnStatusUpdateTimeDesc, - prometheus.GaugeValue, - float64(timeParser.Unix()), - statusPath) - } else if desc, ok := openvpnClientDescs[fields[0]]; ok && len(fields) == 2 { - // Traffic counters. - value, err := strconv.ParseFloat(fields[1], 64) - if err != nil { - return err - } - ch <- prometheus.MustNewConstMetric( - desc, - prometheus.CounterValue, - value, - statusPath) - } else { - return fmt.Errorf("unsupported key: %q", fields[0]) - } - } - return scanner.Err() -} - -func CollectStatusFromFile(statusPath string, ch chan<- prometheus.Metric) error { - conn, err := os.Open(statusPath) - defer conn.Close() - if err != nil { - return err - } - return CollectStatusFromReader(statusPath, conn, ch) -} - -type OpenVPNExporter struct { - statusPaths []string -} - -func NewOpenVPNExporter(statusPaths []string) (*OpenVPNExporter, error) { - return &OpenVPNExporter{ - statusPaths: statusPaths, - }, nil -} - -func (e *OpenVPNExporter) Describe(ch chan<- *prometheus.Desc) { - ch <- openvpnUpDesc -} - -func (e *OpenVPNExporter) Collect(ch chan<- prometheus.Metric) { - for _, statusPath := range e.statusPaths { - err := CollectStatusFromFile(statusPath, ch) - if err == nil { - ch <- prometheus.MustNewConstMetric( - openvpnUpDesc, - prometheus.GaugeValue, - 1.0, - statusPath) - } else { - log.Printf("Failed to scrape showq socket: %s", err) - ch <- prometheus.MustNewConstMetric( - openvpnUpDesc, - prometheus.GaugeValue, - 0.0, - statusPath) - } - } -} - func main() { var ( listenAddress = flag.String("web.listen-address", ":9176", "Address to listen on for web interface and telemetry.") metricsPath = flag.String("web.telemetry-path", "/metrics", "Path under which to expose metrics.") openvpnStatusPaths = flag.String("openvpn.status_paths", "examples/client.status,examples/server2.status,examples/server3.status", "Paths at which OpenVPN places its status files.") + ignoreIndividuals = flag.Bool("ignore.individuals", false, "If ignoring metrics for individuals") ) flag.Parse() @@ -349,8 +36,9 @@ func main() { log.Printf("Listen address: %v\n", *listenAddress) log.Printf("Metrics path: %v\n", *metricsPath) log.Printf("openvpn.status_path: %v\n", *openvpnStatusPaths) + log.Printf("Ignore Individuals: %v\n", *ignoreIndividuals) - exporter, err := NewOpenVPNExporter(strings.Split(*openvpnStatusPaths, ",")) + exporter, err := exporters.NewOpenVPNExporter(strings.Split(*openvpnStatusPaths, ","), *ignoreIndividuals) if err != nil { panic(err) }