Skip to content
This repository has been archived by the owner on Aug 17, 2021. It is now read-only.

Commit

Permalink
Merge pull request #31 from kumina/mergepull24
Browse files Browse the repository at this point in the history
Mergepull24
  • Loading branch information
BartVerc authored Jun 29, 2020
2 parents 02ce4c3 + 7f420d0 commit 9d468c0
Show file tree
Hide file tree
Showing 4 changed files with 373 additions and 318 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions examples/server2.status
Original file line number Diff line number Diff line change
Expand Up @@ -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
363 changes: 363 additions & 0 deletions exporters/openvpn_exporter.go
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)
}
}
}
Loading

0 comments on commit 9d468c0

Please sign in to comment.