This repository has been archived by the owner on Aug 17, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 106
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #31 from kumina/mergepull24
Mergepull24
- Loading branch information
Showing
4 changed files
with
373 additions
and
318 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} | ||
} |
Oops, something went wrong.