From 01168ec98f27a08986cd76893cb2b15a37fafcf5 Mon Sep 17 00:00:00 2001 From: Ed Schouten Date: Fri, 14 Apr 2017 11:16:06 +0200 Subject: [PATCH] Replace client and route counters by per-entry metrics. This allows us to graph either per-connection or the total bandwidth of the VPN server. Fixes #2 --- openvpn_exporter.go | 126 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 99 insertions(+), 27 deletions(-) diff --git a/openvpn_exporter.go b/openvpn_exporter.go index 89ebd4b..ef56623 100644 --- a/openvpn_exporter.go +++ b/openvpn_exporter.go @@ -29,6 +29,17 @@ import ( "github.com/prometheus/client_golang/prometheus" ) +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( @@ -41,14 +52,52 @@ var ( []string{"status_path"}, nil) // Metrics specific to OpenVPN servers. - openvpnServerClientsDesc = prometheus.NewDesc( - prometheus.BuildFQName("openvpn", "server", "clients"), - "Number of clients connected to the OpenVPN server.", - []string{"status_path"}, nil) - openvpnServerRoutesDesc = prometheus.NewDesc( - prometheus.BuildFQName("openvpn", "server", "routes"), - "Number of routes propagated by the OpenVPN server.", - []string{"status_path"}, nil) + openvpnServerHeaders = map[string]OpenvpnServerHeader{ + "CLIENT_LIST": OpenvpnServerHeader{ + LabelColumns: []string{ + "Common Name", + "Connected Since (time_t)", + "Real Address", + "Username", + "Virtual Address", + }, + Metrics: []OpenvpnServerHeaderField{ + 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, + }, + OpenvpnServerHeaderField{ + 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": OpenvpnServerHeader{ + LabelColumns: []string{ + "Common Name", + "Real Address", + "Virtual Address", + }, + Metrics: []OpenvpnServerHeaderField{ + 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{ @@ -118,22 +167,16 @@ func CollectStatusFromReader(statusPath string, file io.Reader, ch chan<- promet func CollectServerStatusFromReader(statusPath string, file io.Reader, ch chan<- prometheus.Metric, separator string) error { scanner := bufio.NewScanner(file) scanner.Split(bufio.ScanLines) - clients := 0 - routes := 0 + headersFound := map[string][]string{} for scanner.Scan() { fields := strings.Split(scanner.Text(), separator) - if fields[0] == "CLIENT_LIST" { - // Per-client stats. - clients++ - } else if fields[0] == "END" && len(fields) == 1 { + 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. - } else if fields[0] == "ROUTING_TABLE" { - // Per-route stats. - routes++ + headersFound[fields[1]] = fields[2:] } else if fields[0] == "TIME" && len(fields) == 3 { // Time at which the statistics were updated. time, err := strconv.ParseFloat(fields[2], 64) @@ -147,20 +190,49 @@ func CollectServerStatusFromReader(statusPath string, file io.Reader, ch chan<- statusPath) } else if fields[0] == "TITLE" && len(fields) == 2 { // OpenVPN version number. + } else if header, ok := openvpnServerHeaders[fields[0]]; ok { + // 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]) } } - ch <- prometheus.MustNewConstMetric( - openvpnServerClientsDesc, - prometheus.GaugeValue, - float64(clients), - statusPath) - ch <- prometheus.MustNewConstMetric( - openvpnServerRoutesDesc, - prometheus.GaugeValue, - float64(routes), - statusPath) return scanner.Err() }